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(); SummaryTaskService get _summaryTaskService => Get.find(); 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 _headers = {}; Map _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 _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 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 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, ); try { final svc = Get.isRegistered() ? Get.find() : Get.put(UsageStatsService(), permanent: true); svc.addMeetingAsrRecord(record); } catch (e) { Logger.w('MeetingASRrecord', '提交用量记录失败: ' + e.toString()); } } 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 _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 array = path.split('.'); return array[array.length - 1]; } ///轮询语音转文字任务状态 ///[taskId] 任务ID Future> _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 _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 _addTextList( int id, List utterances, String prompt, String outline) async { String text = ''; String personnel = ''; List> 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; try { final svc = Get.isRegistered() ? Get.find() : Get.put(UsageStatsService(), permanent: true); svc.completeMeetingAsrRecord( id: tid, transcriptBytes: transcriptBytes, completeTimestamp: DateTime.now().millisecondsSinceEpoch, ); } catch (e) { Logger.w('MeetingASRrecord', '完成用量记录失败: ' + e.toString()); } } catch (_) {} // 触发AI分析,分别请求总结和概览 _summaryTaskService.addTaskList(id, text, prompt, outline); } }