You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
463 lines
15 KiB
463 lines
15 KiB
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:get/get.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../../../core/utils/logger.dart';
|
|
import '../../../core/utils/synchrodata.dart';
|
|
import '../../models/meeting_usage_stats.dart';
|
|
import '../db/sqflite_api.dart';
|
|
import '../usage_stats_service.dart';
|
|
import 'meeting_upload_service.dart';
|
|
import '../../../data/models/appconfig.dart';
|
|
import 'summary_task_service.dart';
|
|
|
|
///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。
|
|
class MeetingTaskService extends GetxService {
|
|
final GetStorage _storage = GetStorage();
|
|
|
|
MeetingUploadService get _uploadService => Get.find<MeetingUploadService>();
|
|
SummaryTaskService get _summaryTaskService => Get.find<SummaryTaskService>();
|
|
|
|
List _taskList = []; //待处理的会议任务列表
|
|
RxList taskFinishList = [].obs; //已处理的会议任务列表
|
|
|
|
bool _isTask = false; //是否有正在处理的任务
|
|
|
|
String _userId = '';
|
|
|
|
// final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; //
|
|
// final String _cluster = 'volc_auc_video_flash';
|
|
//字节跳动(火山引擎)语音服务配置
|
|
// final String _appid = '2316081933';
|
|
// final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
|
|
// final String _cluster1 = 'volc.bigasr.auc';
|
|
|
|
// final String _serviceUrl1 =
|
|
// 'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; //大模型录音文件识别api地址
|
|
late final String _appid;
|
|
late final String _token;
|
|
late final String _cluster1;
|
|
late final String _serviceUrl1;
|
|
late final String _modelName;
|
|
late final String _modelVersion;
|
|
Map<String, String> _headers = {};
|
|
Map<String, String> _headers1 = {};
|
|
|
|
int meetingId = 0;
|
|
RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成
|
|
List textList = []; //转写结果列表
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
|
|
_initialize();
|
|
|
|
_getAgent();
|
|
_getTaskList();
|
|
}
|
|
|
|
void cancelTask() {
|
|
_isTask = false;
|
|
_taskList.clear();
|
|
_storage.remove("meeting_task_list");
|
|
_storage.remove("meeting_task_finish_list");
|
|
_uploadService.cancelUpload();
|
|
_summaryTaskService.cancelTask();
|
|
}
|
|
|
|
Future<bool> _initialize() async {
|
|
try {
|
|
_appid = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? '';
|
|
_token = AppConfig.env('VOLC_OPENSPEECH_ACCESS_TOKEN') ?? '';
|
|
_cluster1 = AppConfig.env('VOLC_OPENSPEECH_CLUSTER_BIGMODEL') ?? '';
|
|
_serviceUrl1 = AppConfig.env('VOLC_OPENSPEECH_BASE_URL_BIGMODEL') ?? '';
|
|
_modelName = AppConfig.env('VOLC_OPENSPEECH_MODEL_NAME') ?? 'bigmodel';
|
|
_modelVersion = AppConfig.env('VOLC_OPENSPEECH_MODEL_VERSION') ?? '400';
|
|
|
|
if (_appid.isEmpty ||
|
|
_token.isEmpty ||
|
|
_cluster1.isEmpty ||
|
|
_serviceUrl1.isEmpty ||
|
|
_modelName.isEmpty ||
|
|
_modelVersion.isEmpty) {
|
|
Logger.error('初始化会议任务服务失败:配置参数为空');
|
|
return false;
|
|
}
|
|
|
|
_headers = {
|
|
'Authorization': 'Bearer; $_token',
|
|
'Content-Type': 'application/json',
|
|
};
|
|
_headers1 = {
|
|
'X-Api-App-Key': _appid,
|
|
'X-Api-Access-Key': _token,
|
|
'X-Api-Resource-Id': _cluster1,
|
|
'X-Api-Request-Id': '',
|
|
'X-Api-Sequence': '-1',
|
|
};
|
|
} catch (e) {
|
|
Logger.error('初始化会议任务服务失败:$e');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void _getAgent() async {
|
|
Map userInfo = _storage.read("user_info");
|
|
_userId = userInfo['user']['uid'];
|
|
}
|
|
|
|
void _getTaskList() {
|
|
_taskList = _storage.read("meeting_task_list") ?? [];
|
|
taskFinishList.value = _storage.read("meeting_task_finish_list") ?? [];
|
|
_executeTask();
|
|
}
|
|
|
|
///添加会议任务
|
|
///[id] 会议ID
|
|
///[audiourl] 音频URL
|
|
///[taskid] 任务ID
|
|
///[isSpeaker] 是否为发言人
|
|
///[language] 语言
|
|
///[prompt] 提示词
|
|
///[outline] 大纲
|
|
Future<void> addTask(int id, String audiourl, String taskid, bool isSpeaker,
|
|
String language, String prompt, String outline) async {
|
|
if (audiourl.isEmpty) {
|
|
Map uploadData = _uploadService.uploadList.firstWhere(
|
|
(element) => element['id'] == id,
|
|
orElse: () => {},
|
|
);
|
|
if (uploadData.isNotEmpty) {
|
|
if (uploadData['audiourl'].isEmpty) {
|
|
uploadData['isTask'] = true;
|
|
uploadData['isSpeaker'] = isSpeaker;
|
|
uploadData['language'] = language;
|
|
uploadData['prompt'] = prompt;
|
|
uploadData['outline'] = outline;
|
|
} else {
|
|
await addTaskList(id, uploadData['audiourl'], taskid, isSpeaker,
|
|
language, prompt, outline);
|
|
}
|
|
}
|
|
} else {
|
|
await addTaskList(
|
|
id, audiourl, taskid, isSpeaker, language, prompt, outline);
|
|
}
|
|
}
|
|
|
|
///添加会议任务列表
|
|
///[id] 会议ID
|
|
///[audiourl] 音频URL
|
|
///[taskId] 任务ID
|
|
///[isSpeaker] 是否为发言人
|
|
///[language] 语言
|
|
///[prompt] 提示词
|
|
///[outline] 大纲
|
|
Future<void> addTaskList(int id, String audiourl, String taskId,
|
|
bool isSpeaker, String language, String prompt, String outline) async {
|
|
if (taskId.isEmpty) {
|
|
taskId = await _submitTask(audiourl, isSpeaker, language);
|
|
}
|
|
await SqfliteApi.editMeeting(id, {'tasktype': 1, 'taskid': taskId});
|
|
try {
|
|
final detail = await SqfliteApi.getMeetingDetails(id);
|
|
final seconds = (detail['seconds'] ?? 0) as int;
|
|
final filepath = (detail['filepath'] ?? '') as String;
|
|
int fileSizeBytes = 0;
|
|
if (filepath.isNotEmpty) {
|
|
try {
|
|
fileSizeBytes = await File(filepath).length();
|
|
} catch (_) {}
|
|
}
|
|
final record = MeetingAsrRecord(
|
|
id: taskId,
|
|
meetingId: id,
|
|
taskId: taskId,
|
|
audioSeconds: seconds,
|
|
fileSizeBytes: fileSizeBytes,
|
|
language: language,
|
|
submitTimestamp: DateTime.now().millisecondsSinceEpoch,
|
|
status: asrStatusProcessing,
|
|
transcriptBytes: 0,
|
|
completeTimestamp: 0,
|
|
);
|
|
Get.find<UsageStatsService>().addMeetingAsrRecord(record);
|
|
} catch (_) {}
|
|
_taskList.add({
|
|
'id': id,
|
|
'taskid': taskId,
|
|
'tasktype': 1,
|
|
'prompt': prompt,
|
|
'outline': outline,
|
|
});
|
|
taskFinishList.add({
|
|
'id': id,
|
|
'tasktype': 1,
|
|
});
|
|
_storage.write("meeting_task_list", _taskList);
|
|
_storage.write("meeting_task_finish_list", taskFinishList);
|
|
_executeTask();
|
|
}
|
|
|
|
///执行会议任务
|
|
void _executeTask() async {
|
|
if (_isTask) return;
|
|
_isTask = true;
|
|
while (_taskList.isNotEmpty) {
|
|
await Future.delayed(const Duration(seconds: 2));
|
|
Logger.info("lxm--轮询---会议任务列表$_taskList");
|
|
for (var task in List.from(_taskList)) {
|
|
await _fileRecognize(task);
|
|
}
|
|
}
|
|
_isTask = false;
|
|
}
|
|
|
|
void removeTask(int id, {bool isDeleteTask = false}) {
|
|
Map taskFinishData = taskFinishList.firstWhere(
|
|
(element) => element['id'] == id,
|
|
orElse: () => {},
|
|
);
|
|
if (taskFinishData.isNotEmpty) {
|
|
if (isDeleteTask || taskFinishData['tasktype'] >= 3) {
|
|
taskFinishList.remove(taskFinishData);
|
|
_storage.write("meeting_task_finish_list", taskFinishList);
|
|
}
|
|
}
|
|
if (isDeleteTask) {
|
|
_taskList.removeWhere((element) => element['id'] == id);
|
|
_storage.write("meeting_task_list", _taskList);
|
|
_summaryTaskService.removeTask(id);
|
|
}
|
|
}
|
|
|
|
//
|
|
Future<String> _submitTask(
|
|
String audiourl, bool isSpeaker, String language) async {
|
|
String format = _getFileType(audiourl);
|
|
final taskId = DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
_headers1['X-Api-Request-Id'] = taskId;
|
|
Logger.info("lxm--大模型asr请求头$_headers1");
|
|
|
|
final requestBody1 = jsonEncode({
|
|
"user": {
|
|
"uid": _userId,
|
|
},
|
|
"audio": {
|
|
"url": audiourl,
|
|
"language": language,
|
|
"format": format,
|
|
},
|
|
"request": {
|
|
"model_name": _modelName, // 模型名称,目前只有bigmodel
|
|
"model_version": _modelVersion, // 模型版本,400模型性能略有提升,且ITN有较大优化
|
|
//"enable_itn": true, // 启用文本规范化,默认为true
|
|
"enable_punc": true, // 启用标点,默认为false
|
|
//"enable_ddc": false, // 启用语义顺滑,默认为false
|
|
"enable_speaker_info": isSpeaker, // 启用说话人聚类分离,10人以内效果较好
|
|
//"enable_channel_split": false, // 启用双声道识别,默认为false
|
|
//"show_utterances": true, // 输出语音停顿、分句、分词信息
|
|
//"show_speech_rate": false, // 分句信息携带语速
|
|
//"show_volume": false, // 分句信息携带音量
|
|
//"enable_lid": false, // 启用语种识别
|
|
}
|
|
});
|
|
|
|
Logger.info("lxm--大模型asr请求体$requestBody1");
|
|
|
|
// final requestBody = jsonEncode({
|
|
// "app": {
|
|
// "appid": _appid,
|
|
// "token": _token,
|
|
// "cluster": _cluster,
|
|
// },
|
|
// "user": {
|
|
// "uid": _userId,
|
|
// },
|
|
// "audio": {
|
|
// "format": format,
|
|
// "url": audiourl,
|
|
// },
|
|
// "additions": {
|
|
// "language": language,
|
|
// "with_speaker_info": isSpeaker ? "True" : "False",
|
|
// }
|
|
// });
|
|
|
|
try {
|
|
final response = await http.post(
|
|
Uri.parse('$_serviceUrl1/submit'),
|
|
headers: _headers1,
|
|
body: requestBody1,
|
|
);
|
|
|
|
Logger.info("lxm--大模型asr响应体${response.body}");
|
|
Logger.info("lxm--大模型asr上传响应头${response.headers['x-tt-logid']}");
|
|
Logger.info(
|
|
"lxm--大模型asr上传服务端返回的状态码${response.headers['x-api-status-code']}");
|
|
Logger.info("lxm--大模型asr上传服务端返回的消息${response.headers['x-api-message']}");
|
|
|
|
if (response.statusCode != 200) {
|
|
throw Exception('Submit failed with status: ${response.statusCode}');
|
|
}
|
|
|
|
//final respDic = jsonDecode(response.body);
|
|
//final taskId = respDic['resp']['id'] as String;
|
|
//Logger.debug('taskId: $taskId');
|
|
return taskId;
|
|
} catch (e) {
|
|
Logger.error('Submit error: ${e.toString()}');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
String _getFileType(String path) {
|
|
List<String> array = path.split('.');
|
|
return array[array.length - 1];
|
|
}
|
|
|
|
///轮询语音转文字任务状态
|
|
///[taskId] 任务ID
|
|
Future<Map<String, dynamic>> _queryTask(String taskId) async {
|
|
// final queryBody = jsonEncode({
|
|
// "appid": _appid,
|
|
// "token": _token,
|
|
// "id": taskId,
|
|
// "cluster": _cluster,
|
|
// });
|
|
|
|
try {
|
|
_headers1['X-Api-Request-Id'] = taskId;
|
|
final response = await http.post(
|
|
Uri.parse('$_serviceUrl1/query'),
|
|
headers: _headers1,
|
|
body: jsonEncode({}),
|
|
);
|
|
|
|
// Logger.info("lxm---大模型asr响应体${response.body}");
|
|
// Logger.info("lxm---大模型asr响应头${response.headers}");
|
|
// Logger.info("lxm---大模型asr响应状态码${response.statusCode}");
|
|
// Logger.info("lxm---大模型asr响应体logid${response.headers['x-tt-logid']}");
|
|
// Logger.info("lxm---大模型asr服务端返回的状态码${response.headers['x-api-status-code']}");
|
|
// Logger.info("lxm---大模型asr服务端返回的消息${response.headers['x-api-message']}");
|
|
|
|
if (response.statusCode != 200) {
|
|
throw Exception('Query failed with status: ${response.statusCode}');
|
|
}
|
|
|
|
final respDic = jsonDecode(response.body);
|
|
return respDic;
|
|
} catch (e) {
|
|
Logger.error('Query error: ${e.toString()}');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
///轮询语音转文字任务状态,若任务完成则处理转写结果
|
|
///[item] 包含任务ID、会议ID、AI总结提示词、AI概览提示词的Map
|
|
Future<void> _fileRecognize(Map item) async {
|
|
try {
|
|
var respDic = await _queryTask(item['taskid']);
|
|
//var code = respDic['resp']['code'] as int;
|
|
|
|
Logger.info("lxm---大模型asr响应体结果$respDic['result']['text']");
|
|
|
|
if (respDic['result']['text'] != null &&
|
|
respDic['result']['text'] != '') {
|
|
await _addTextList(
|
|
item['id'], //会议ID
|
|
respDic['result']['utterances'], //语音转文字结果
|
|
item['prompt'], //AI总结提示词
|
|
item['outline'], //AI概览提示词
|
|
);
|
|
return;
|
|
} else {
|
|
Logger.debug("FileRecognize failed");
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Error occurred: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
///处理语音识别完成后的转写结果,包括数据库存储、状态更新和触发AI分析
|
|
///[id] 会议ID
|
|
///[utterances] 语音转文字结果
|
|
///[prompt] AI总结提示词
|
|
///[outline] AI概览提示词
|
|
Future<void> _addTextList(
|
|
int id, List utterances, String prompt, String outline) async {
|
|
String text = '';
|
|
String personnel = '';
|
|
List<Map<String, Object?>> list = [];
|
|
|
|
// 处理转写结果,将每个句子的开始时间、结束时间、发言人和内容存储到列表中
|
|
for (var element in utterances) {
|
|
String speaker = '';
|
|
if (element['additions'].isNotEmpty) {
|
|
speaker = 'Speaker ${element['additions']['speaker']}';
|
|
text += '[$speaker]: ';
|
|
if (!personnel.contains(speaker)) {
|
|
personnel += '[$speaker] ';
|
|
}
|
|
}
|
|
list.add({
|
|
'meetingid': id,
|
|
'content': element['text'],
|
|
'starttime': element['start_time'],
|
|
'endtime': element['end_time'],
|
|
'speaker': speaker,
|
|
});
|
|
text += '${element['text']}\n';
|
|
}
|
|
// 存储处理后的转写结果到数据库
|
|
var result = await SqfliteApi.insertMeetingSpeaker(list);
|
|
if (result != null) {
|
|
// 更新会议状态为“总结中”
|
|
await SqfliteApi.editMeeting(
|
|
id,
|
|
{'tasktype': 2, 'personnel': personnel},
|
|
);
|
|
List speakerList = await SqfliteApi.getMeetingSpeaker(id);
|
|
List newSpeakerList = List.from(speakerList);
|
|
Synchrodata.saveOperationRecord('update', {
|
|
'original': jsonEncode(newSpeakerList),
|
|
'id': id,
|
|
});
|
|
if (id == meetingId) {
|
|
textList = newSpeakerList;
|
|
tasktype.value = 2;
|
|
}
|
|
_taskList.removeWhere((element) => element['id'] == id);
|
|
Map taskFinishData = taskFinishList.firstWhere(
|
|
(element) => element['id'] == id,
|
|
orElse: () => {},
|
|
);
|
|
if (taskFinishData.isNotEmpty) {
|
|
taskFinishData['tasktype'] = 2;
|
|
taskFinishList.refresh();
|
|
}
|
|
_storage.write("meeting_task_list", _taskList);
|
|
_storage.write("meeting_task_finish_list", taskFinishList);
|
|
}
|
|
try {
|
|
final details = await SqfliteApi.getMeetingDetails(id);
|
|
final tid = (details['taskid'] ?? '') as String;
|
|
final transcriptBytes = utf8.encode(text).length;
|
|
Get.find<UsageStatsService>().completeMeetingAsrRecord(
|
|
id: tid,
|
|
transcriptBytes: transcriptBytes,
|
|
completeTimestamp: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
} catch (_) {}
|
|
|
|
// 触发AI分析,分别请求总结和概览
|
|
_summaryTaskService.addTaskList(id, text, prompt, outline);
|
|
}
|
|
}
|
|
|