21 changed files with 544 additions and 2114 deletions
@ -1,105 +0,0 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
|
|||
import 'meeting_task_service.dart'; |
|||
import 'meeting_task_service2.dart'; |
|||
|
|||
/// 会议助手任务服务聚合器(统一列表与状态) |
|||
class MeetingTaskAggregator extends GetxService { |
|||
final GetStorage _storage = GetStorage(); |
|||
|
|||
final MeetingTaskService s1 = Get.find<MeetingTaskService>(); |
|||
final MeetingTaskService2 s2 = Get.find<MeetingTaskService2>(); |
|||
|
|||
RxList taskFinishList = [].obs; |
|||
RxInt tasktype = 1.obs; |
|||
int meetingId = 0; |
|||
List textList = []; |
|||
|
|||
Worker? _w1; |
|||
Worker? _w2; |
|||
Worker? _wTasktype; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_w1 = ever(s1.taskFinishList, (_) => _rebuild()); |
|||
_w2 = ever(s2.taskFinishList, (_) => _rebuild()); |
|||
_rebuild(); |
|||
} |
|||
|
|||
void setMeeting(int id) { |
|||
meetingId = id; |
|||
final String svc = |
|||
(_storage.read("meeting_service_type_$id") ?? '').toString(); |
|||
final bool isFast = svc == 'fast'; |
|||
_wTasktype?.dispose(); |
|||
if (isFast) { |
|||
_wTasktype = ever(s2.tasktype, (value) { |
|||
tasktype.value = value; |
|||
if (value == 2) { |
|||
textList = s2.textList; |
|||
} |
|||
}); |
|||
} else { |
|||
_wTasktype = ever(s1.tasktype, (value) { |
|||
tasktype.value = value; |
|||
if (value == 2) { |
|||
textList = s1.textList; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
void clearMeeting() { |
|||
meetingId = 0; |
|||
_wTasktype?.dispose(); |
|||
_wTasktype = null; |
|||
} |
|||
|
|||
void _rebuild() { |
|||
final Set<int> ids = {}; |
|||
for (var e in s1.taskFinishList) { |
|||
if (e is Map && e['id'] is int) ids.add(e['id'] as int); |
|||
} |
|||
for (var e in s2.taskFinishList) { |
|||
if (e is Map && e['id'] is int) ids.add(e['id'] as int); |
|||
} |
|||
final List unified = []; |
|||
for (final id in ids) { |
|||
final String svc = |
|||
(_storage.read("meeting_service_type_$id") ?? '').toString(); |
|||
final Map a = s1.taskFinishList.firstWhere( |
|||
(x) => x['id'] == id, |
|||
orElse: () => {}, |
|||
); |
|||
final Map b = s2.taskFinishList.firstWhere( |
|||
(x) => x['id'] == id, |
|||
orElse: () => {}, |
|||
); |
|||
int va = a.isNotEmpty ? ((a['tasktype'] ?? 0) as int) : -999; |
|||
int vb = b.isNotEmpty ? ((b['tasktype'] ?? 0) as int) : -999; |
|||
int v; |
|||
if (svc == 'fast') { |
|||
if (va != -999 && vb != -999) { |
|||
v = va >= vb ? va : vb; |
|||
} else { |
|||
v = vb != -999 ? vb : va; |
|||
} |
|||
} else if (svc == 'standard') { |
|||
if (va != -999 && vb != -999) { |
|||
v = va >= vb ? va : vb; |
|||
} else { |
|||
v = va != -999 ? va : vb; |
|||
} |
|||
} else { |
|||
if (va == -999 && vb == -999) { |
|||
continue; |
|||
} |
|||
v = va == -999 ? vb : (vb == -999 ? va : (va >= vb ? va : vb)); |
|||
} |
|||
unified.add({'id': id, 'tasktype': v}); |
|||
} |
|||
taskFinishList.value = unified; |
|||
} |
|||
} |
|||
@ -0,0 +1,205 @@ |
|||
import 'dart:convert'; |
|||
|
|||
import 'package:get/get.dart'; |
|||
|
|||
import '../db/sqflite_api.dart'; |
|||
import '../network/api.dart'; |
|||
import 'meeting_upload_service.dart'; |
|||
|
|||
class MeetingTaskPost extends GetxService { |
|||
MeetingUploadService get _uploadService => Get.find<MeetingUploadService>(); |
|||
|
|||
List _taskList = []; //待处理的会议任务列表 |
|||
|
|||
bool _isTask = false; //是否有正在处理的任务 |
|||
|
|||
RxInt updateListState = 0.obs; //是否更新首页列表 |
|||
int meetingId = 0; |
|||
RxInt tasktype = 1.obs; //任务类型,0-初始,1-等待转写中,2-转写中,3-等待总结中,4-总结中,5-完成 |
|||
List textList = []; |
|||
String personnel = ''; |
|||
String summary = ''; |
|||
String overview = ''; |
|||
|
|||
Future<void> addTask( |
|||
int id, |
|||
String audiourl, |
|||
String formlanguage, |
|||
String tolanguage, |
|||
bool isdistinguishspeaker, |
|||
int templateid, |
|||
) 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['formlanguage'] = formlanguage; |
|||
uploadData['tolanguage'] = tolanguage; |
|||
uploadData['isdistinguishspeaker'] = isdistinguishspeaker; |
|||
uploadData['templateid'] = templateid; |
|||
} else { |
|||
await submitTask( |
|||
id, |
|||
formlanguage, |
|||
tolanguage, |
|||
isdistinguishspeaker, |
|||
templateid, |
|||
); |
|||
} |
|||
} |
|||
} else { |
|||
await submitTask( |
|||
id, |
|||
formlanguage, |
|||
tolanguage, |
|||
isdistinguishspeaker, |
|||
templateid, |
|||
); |
|||
} |
|||
} |
|||
|
|||
Future<void> submitTask( |
|||
int id, |
|||
String formlanguage, |
|||
String tolanguage, |
|||
bool isdistinguishspeaker, |
|||
int templateid, |
|||
) async { |
|||
await Api.startTaskEchomeet({ |
|||
'id': id, |
|||
'formlanguage': formlanguage, |
|||
'tolanguage': tolanguage, |
|||
'isdistinguishspeaker': isdistinguishspeaker, |
|||
'templateid': templateid, |
|||
}); |
|||
_taskList.add({ |
|||
'id': id, |
|||
'tasktype': 1, |
|||
}); |
|||
await SqfliteApi.editMeetingTitle( |
|||
id, |
|||
{'tasktype': 1}, |
|||
); |
|||
updateListState.value++; |
|||
_executeTask(); |
|||
} |
|||
|
|||
Future<void> refreshTask(int id, int templateid) async { |
|||
tasktype.value = 3; |
|||
await Api.refreshTaskEchomeet({ |
|||
'id': id, |
|||
'templateid': templateid, |
|||
}); |
|||
_taskList.add({ |
|||
'id': id, |
|||
'tasktype': 3, |
|||
}); |
|||
await SqfliteApi.editMeetingTitle( |
|||
id, |
|||
{'tasktype': 3}, |
|||
); |
|||
updateListState.value++; |
|||
_executeTask(); |
|||
} |
|||
|
|||
Future<void> readTask(int id) async { |
|||
await Api.readRecordEchomeet({'id': id}); |
|||
await SqfliteApi.editMeetingTitle( |
|||
id, |
|||
{'tasktype': 6}, |
|||
); |
|||
updateListState.value++; |
|||
} |
|||
|
|||
void _executeTask() async { |
|||
if (_isTask) return; |
|||
_isTask = true; |
|||
while (_taskList.isNotEmpty) { |
|||
await Future.delayed(const Duration(seconds: 5)); |
|||
await _queryTask(); |
|||
} |
|||
_isTask = false; |
|||
} |
|||
|
|||
Future<void> removeTask(int id) async { |
|||
_taskList.removeWhere((element) => element['id'] == id); |
|||
await Api.delRecordEchomeet({ |
|||
'ids': [id], |
|||
}); |
|||
} |
|||
|
|||
void startTask(List data) { |
|||
_taskList = data; |
|||
_executeTask(); |
|||
} |
|||
|
|||
void stopTask() { |
|||
_taskList.clear(); |
|||
} |
|||
|
|||
Future<void> _queryTask() async { |
|||
if (_taskList.isEmpty) return; |
|||
List ids = _taskList.map((e) => e['id']).toList(); |
|||
var response = await Api.getMultitermRecord({ |
|||
'ids': ids, |
|||
}); |
|||
List queryList = response['records'] ?? []; |
|||
for (var element in queryList) { |
|||
int id = element['id']; |
|||
int newTasktype = element['state']; |
|||
Map taskData = _taskList.firstWhere( |
|||
(i) => i['id'] == id, |
|||
orElse: () => {}, |
|||
); |
|||
if (taskData.isNotEmpty) { |
|||
int oldTasktype = taskData['tasktype']; |
|||
if (oldTasktype != newTasktype) { |
|||
if (oldTasktype < 3 && newTasktype >= 3) { |
|||
// 转写完成 |
|||
List<dynamic> decodeList = jsonDecode(element['translate']); |
|||
List<Map<String, Object?>> speakerList = |
|||
decodeList.map((e) => Map<String, Object?>.from(e)).toList(); |
|||
await SqfliteApi.deleteMeetingSpeaker(id); |
|||
await SqfliteApi.insertMeetingSpeaker(speakerList); |
|||
await SqfliteApi.editMeeting( |
|||
id, |
|||
{'tasktype': newTasktype}, |
|||
); |
|||
if (id == meetingId) { |
|||
textList = speakerList; |
|||
tasktype.value = newTasktype; |
|||
} |
|||
} |
|||
if (oldTasktype < 5 && newTasktype >= 5) { |
|||
// 总结完成 |
|||
await SqfliteApi.editMeeting(id, { |
|||
'tasktype': newTasktype, |
|||
'personnel': element['personnel'] ?? '', |
|||
'summary': element['summary'] ?? '', |
|||
'overview': element['overview'] ?? '', |
|||
}); |
|||
if (id == meetingId) { |
|||
personnel = element['personnel'] ?? ''; |
|||
summary = element['summary'] ?? ''; |
|||
overview = element['overview'] ?? ''; |
|||
tasktype.value = newTasktype; |
|||
} |
|||
} |
|||
await SqfliteApi.editMeetingTitle( |
|||
id, |
|||
{'tasktype': newTasktype}, |
|||
); |
|||
taskData['tasktype'] = newTasktype; |
|||
updateListState.value++; |
|||
} |
|||
} |
|||
if (newTasktype >= 5) { |
|||
_taskList.removeWhere((i) => i['id'] == id); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,478 +0,0 @@ |
|||
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, |
|||
); |
|||
try { |
|||
final svc = Get.isRegistered<UsageStatsService>() |
|||
? Get.find<UsageStatsService>() |
|||
: 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<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; |
|||
try { |
|||
final svc = Get.isRegistered<UsageStatsService>() |
|||
? Get.find<UsageStatsService>() |
|||
: 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); |
|||
} |
|||
} |
|||
@ -1,971 +0,0 @@ |
|||
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 '../db/sqflite_api.dart'; |
|||
import '../../../data/models/appconfig.dart'; |
|||
import '../../../core/utils/logger.dart'; |
|||
import '../../../core/utils/synchrodata.dart'; |
|||
import '../usage_stats_service.dart'; |
|||
import '../../../data/models/meeting_usage_stats.dart'; |
|||
import '../deapsound_ai_service.dart'; |
|||
import 'meeting_upload_service.dart'; |
|||
// 移除标准版服务桥接,改为通过聚合器对外暴露统一状态 |
|||
import 'summary_task_service.dart'; |
|||
|
|||
//火山引擎录音文件识别大模型-极速版 |
|||
class MeetingTaskService2 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 _systemPrompt = ''; |
|||
String _userId = ''; |
|||
|
|||
late final String _appid; |
|||
late final String _token; |
|||
late final String _serviceUrl; |
|||
|
|||
int meetingId = 0; |
|||
RxInt tasktype = 1.obs; |
|||
List textList = []; |
|||
String summary = ''; |
|||
String overview = ''; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_getAgent(); |
|||
_getTaskList(); |
|||
} |
|||
|
|||
void _getAgent() async { |
|||
Map userInfo = _storage.read("user_info"); |
|||
_userId = userInfo['user']['uid']; |
|||
_systemPrompt = AppConfig.isInitialized() |
|||
? (AppConfig.agentsystemPrompt("meet_assistant") ?? '') |
|||
: ''; |
|||
final String base = AppConfig.env('VOLC_OPENSPEECH_BASE_URL_BIGMODEL') ?? |
|||
'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; |
|||
_appid = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? ''; |
|||
_token = AppConfig.env('VOLC_OPENSPEECH_ACCESS_TOKEN') ?? ''; |
|||
_serviceUrl = |
|||
base.endsWith('/recognize/flash') ? base : (base + '/recognize/flash'); |
|||
} |
|||
|
|||
void _getTaskList() { |
|||
_taskList = _storage.read("meeting_task_list_v2") ?? []; |
|||
taskFinishList.value = _storage.read("meeting_task_finish_list_v2") ?? []; |
|||
_executeTask(); |
|||
} |
|||
|
|||
Future<void> addTask( |
|||
int id, |
|||
String audiourl, { |
|||
bool isSpeaker = false, |
|||
String language = '', |
|||
String targetLanguage = '', |
|||
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['template'] = { |
|||
'isSpeaker': isSpeaker, |
|||
'language': language, |
|||
'targetLanguage': targetLanguage, |
|||
'prompt': prompt, |
|||
'outline': outline, |
|||
}; |
|||
_storage.write("meeting_upload_list", _uploadService.uploadList); |
|||
} else { |
|||
await addTaskList( |
|||
id, |
|||
uploadData['audiourl'], |
|||
isSpeaker: isSpeaker, |
|||
language: language, |
|||
targetLanguage: targetLanguage, |
|||
prompt: prompt, |
|||
outline: outline, |
|||
); |
|||
} |
|||
} |
|||
} else { |
|||
await addTaskList( |
|||
id, |
|||
audiourl, |
|||
isSpeaker: isSpeaker, |
|||
language: language, |
|||
targetLanguage: targetLanguage, |
|||
prompt: prompt, |
|||
outline: outline, |
|||
); |
|||
} |
|||
} |
|||
|
|||
Future<void> addTaskList( |
|||
int id, |
|||
String audiourl, { |
|||
bool isSpeaker = false, |
|||
String language = '', |
|||
String targetLanguage = '', |
|||
String prompt = '', |
|||
String outline = '', |
|||
}) async { |
|||
final String taskId = DateTime.now().millisecondsSinceEpoch.toString(); |
|||
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<UsageStatsService>() |
|||
? Get.find<UsageStatsService>() |
|||
: Get.put(UsageStatsService(), permanent: true); |
|||
svc.addMeetingAsrRecord(record); |
|||
} catch (e) { |
|||
Logger.w('MeetingASRrecord', '提交用量记录失败: ' + e.toString()); |
|||
} |
|||
} catch (_) {} |
|||
_taskList.add({ |
|||
'id': id, |
|||
'audiourl': audiourl, |
|||
'tasktype': 1, |
|||
'taskid': taskId, |
|||
'template': { |
|||
'isSpeaker': isSpeaker, |
|||
'language': language, |
|||
'targetLanguage': targetLanguage, |
|||
'prompt': prompt, |
|||
'outline': outline, |
|||
}, |
|||
}); |
|||
taskFinishList.add({'id': id, 'tasktype': 1}); |
|||
_storage.write("meeting_task_list_v2", _taskList); |
|||
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
|||
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
|||
_executeTask(); |
|||
} |
|||
|
|||
void _executeTask() async { |
|||
if (_isTask) return; |
|||
_isTask = true; |
|||
Logger.debug('开始执行任务,任务数量:${_taskList.length}'); |
|||
|
|||
while (_taskList.isNotEmpty) { |
|||
Logger.debug('任务循环执行,剩余任务:${_taskList.length}'); |
|||
await Future.delayed(const Duration(seconds: 2)); |
|||
|
|||
// 创建任务列表的副本,避免在迭代过程中修改原列表 |
|||
List tasksCopy = List.from(_taskList); |
|||
|
|||
for (var element in tasksCopy) { |
|||
Logger.debug('处理任务:${element['id']},音频:${element['audiourl']}'); |
|||
|
|||
// 检查任务是否仍在列表中(可能已被其他地方移除) |
|||
if (_taskList.any((task) => task['id'] == element['id'])) { |
|||
try { |
|||
await _fileRecognize(element); |
|||
} catch (e) { |
|||
Logger.error('处理任务失败:${element['id']},错误:$e'); |
|||
// 确保失败的任务被移除,避免无限循环 |
|||
_taskList.removeWhere((task) => task['id'] == element['id']); |
|||
_storage.write("meeting_task_list_v2", _taskList); |
|||
|
|||
// 更新任务状态为失败 |
|||
await SqfliteApi.editMeeting(element['id'], {'tasktype': 0}); |
|||
} |
|||
} else { |
|||
Logger.debug('任务 ${element['id']} 已从任务列表移除'); |
|||
} |
|||
} |
|||
|
|||
// 如果没有任务被处理,避免无限循环 |
|||
if (_taskList.length == tasksCopy.length) { |
|||
Logger.warning('本次循环未处理任何任务,为避免无限执行终止循环'); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
_isTask = false; |
|||
Logger.debug('任务执行完成,剩余任务:${_taskList.length}'); |
|||
} |
|||
|
|||
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_v2", taskFinishList); |
|||
} |
|||
} |
|||
if (isDeleteTask) { |
|||
_taskList.removeWhere((element) => element['id'] == id); |
|||
_storage.write("meeting_task_list_v2", _taskList); |
|||
} |
|||
} |
|||
|
|||
Future<Map<String, dynamic>> _recognizeAudio( |
|||
String audiourl, { |
|||
bool enableSpeakerInfo = true, |
|||
String language = '', |
|||
String? requestIdOverride, |
|||
}) async { |
|||
String requestId = |
|||
requestIdOverride ?? DateTime.now().millisecondsSinceEpoch.toString(); |
|||
|
|||
// 为每次请求生成新的headers - 适配大模型极速版API |
|||
Map<String, String> requestHeaders = { |
|||
'X-Api-App-Key': _appid, |
|||
'X-Api-Access-Key': _token, |
|||
'X-Api-Resource-Id': 'volc.bigasr.auc_turbo', |
|||
'X-Api-Request-Id': requestId, |
|||
'X-Api-Sequence': '-1', |
|||
'Content-Type': 'application/json', |
|||
}; |
|||
|
|||
// 使用大模型极速版API的新请求格式 |
|||
final Map<String, dynamic> requestPayload = { |
|||
"user": {"uid": _appid}, |
|||
"audio": {"url": audiourl}, |
|||
"request": { |
|||
"model_name": "bigmodel", |
|||
"enable_itn": true, |
|||
"enable_punc": true, |
|||
"enable_ddc": true, |
|||
"enable_speaker_info": enableSpeakerInfo, |
|||
"word_info": 1, |
|||
"show_utterances": true |
|||
} |
|||
}; |
|||
|
|||
if (language.isNotEmpty) { |
|||
requestPayload['request']['language'] = language; |
|||
} |
|||
|
|||
final String requestBody = jsonEncode(requestPayload); |
|||
|
|||
try { |
|||
// 记录请求详情 |
|||
Logger.debug('API 请求地址:$_serviceUrl'); |
|||
Logger.debug('API 请求头:$requestHeaders'); |
|||
Logger.debug('API 请求体:$requestBody'); |
|||
|
|||
final response = await http.post( |
|||
Uri.parse(_serviceUrl), |
|||
headers: requestHeaders, |
|||
body: requestBody, |
|||
); |
|||
|
|||
Logger.debug('API 响应状态码:${response.statusCode}'); |
|||
Logger.debug('API 响应头:${response.headers}'); |
|||
|
|||
if (response.statusCode != 200) { |
|||
Logger.error('识别失败,状态码:${response.statusCode},响应体:${response.body}'); |
|||
throw Exception('识别失败,状态码:${response.statusCode}'); |
|||
} |
|||
|
|||
final respDic = jsonDecode(response.body); |
|||
Logger.debug('识别接口响应:$respDic'); |
|||
|
|||
// 检查响应中是否包含资源相关信息 |
|||
if (respDic.containsKey('resource_id') || respDic.containsKey('model')) { |
|||
Logger.debug('资源校验 - 使用的资源ID:volc.bigasr.auc_turbo'); |
|||
Logger.debug( |
|||
'响应中的资源信息:${respDic['resource_id'] ?? respDic['model'] ?? '响应未包含'}'); |
|||
} |
|||
|
|||
return respDic; |
|||
} catch (e) { |
|||
Logger.error('识别请求异常:${e.toString()}'); |
|||
rethrow; |
|||
} |
|||
} |
|||
|
|||
String _getFileType(String path) { |
|||
List<String> array = path.split('.'); |
|||
return array[array.length - 1]; |
|||
} |
|||
|
|||
int _parseTime(dynamic timeValue) { |
|||
if (timeValue == null) return 0; |
|||
|
|||
if (timeValue is int) { |
|||
return timeValue; |
|||
} else if (timeValue is double) { |
|||
return timeValue.round(); |
|||
} else if (timeValue is String) { |
|||
try { |
|||
double parsed = double.parse(timeValue); |
|||
return parsed.round(); |
|||
} catch (e) { |
|||
Logger.error('时间值解析失败:$timeValue'); |
|||
return 0; |
|||
} |
|||
} else { |
|||
Logger.error('未知的时间值类型:${timeValue.runtimeType},值:$timeValue'); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
Future<void> _fileRecognize(Map item) async { |
|||
try { |
|||
Logger.debug('开始识别音频:${item['audiourl']}'); |
|||
// 识别语言选取:若设置了目标输出语言且与录音语言一致,优先让ASR自动识别,避免错配 |
|||
final String tplLang = |
|||
(((item['template'] ?? {})['language']) ?? '').toString(); |
|||
final String tplTarget = |
|||
(((item['template'] ?? {})['targetLanguage']) ?? '').toString(); |
|||
String asrLanguage = tplLang; |
|||
if (tplTarget.isNotEmpty && tplTarget == tplLang) { |
|||
asrLanguage = ''; |
|||
} |
|||
var respDic = await _recognizeAudio( |
|||
item['audiourl'], |
|||
enableSpeakerInfo: |
|||
(((item['template'] ?? {})['isSpeaker']) ?? false) as bool, |
|||
language: asrLanguage, |
|||
requestIdOverride: (item['taskid'] ?? '').toString().isEmpty |
|||
? null |
|||
: (item['taskid'] ?? '').toString(), |
|||
); |
|||
|
|||
Logger.debug('API 全量响应结构:${respDic.toString()}'); |
|||
Logger.debug('响应包含的键:${respDic.keys.toList()}'); |
|||
|
|||
// 检查响应格式并提取utterances |
|||
List utterances = []; |
|||
|
|||
// 检查不同可能的响应格式 |
|||
if (respDic['result'] != null && |
|||
respDic['result']['utterances'] != null) { |
|||
utterances = respDic['result']['utterances']; |
|||
Logger.debug('在 result.utterances 中找到 ${utterances.length} 条语句'); |
|||
} else if (respDic['resp'] != null && |
|||
respDic['resp']['utterances'] != null) { |
|||
utterances = respDic['resp']['utterances']; |
|||
Logger.debug('在 resp.utterances 中找到 ${utterances.length} 条语句'); |
|||
} else if (respDic['utterances'] != null) { |
|||
utterances = respDic['utterances']; |
|||
Logger.debug('在根级 utterances 中找到 ${utterances.length} 条语句'); |
|||
} else if (respDic['data'] != null && |
|||
respDic['data']['utterances'] != null) { |
|||
utterances = respDic['data']['utterances']; |
|||
Logger.debug('在 data.utterances 中找到 ${utterances.length} 条语句'); |
|||
} else if (respDic['response'] != null && |
|||
respDic['response']['utterances'] != null) { |
|||
utterances = respDic['response']['utterances']; |
|||
Logger.debug('在 response.utterances 中找到 ${utterances.length} 条语句'); |
|||
} else { |
|||
// 检查是否有错误信息 |
|||
if (respDic['error'] != null) { |
|||
Logger.error('接口返回错误:${respDic['error']}'); |
|||
} else if (respDic['message'] != null) { |
|||
Logger.error('接口返回消息:${respDic['message']}'); |
|||
} else { |
|||
Logger.error('未知的响应格式。可用键:${respDic.keys.toList()}'); |
|||
// 尝试查找任何包含文本的字段 |
|||
respDic.forEach((key, value) { |
|||
Logger.debug('键:$key,值类型:${value.runtimeType},值:$value'); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
if (utterances.isNotEmpty) { |
|||
Logger.debug('开始处理 ${utterances.length} 条语句'); |
|||
await _addTextList(item['id'], utterances, item['template']); |
|||
// 从任务列表中移除已完成的任务 |
|||
_taskList.removeWhere((element) => element['id'] == item['id']); |
|||
_storage.write("meeting_task_list_v2", _taskList); |
|||
Logger.debug('音频识别成功,会议ID:${item['id']}'); |
|||
|
|||
// 更新UI状态 - 如果是当前会议,触发UI更新 |
|||
if (item['id'] == meetingId) { |
|||
tasktype.value = 2; // 转写完成,进入分析阶段 |
|||
} |
|||
} else { |
|||
Logger.error('响应中未发现语句。完整响应:$respDic'); |
|||
Logger.debug('未检测到语句,开始执行无声音频检查'); |
|||
Logger.debug('无声音频检测调试信息'); |
|||
Logger.debug('响应键列表:${respDic.keys.toList()}'); |
|||
Logger.debug('响应代码:${respDic['code']}'); |
|||
Logger.debug('响应消息:${respDic['message']}'); |
|||
|
|||
// 检查是否为无声音频 |
|||
bool isSilentAudio = _checkIfSilentAudio(respDic); |
|||
Logger.debug('无声检测结果:$isSilentAudio'); |
|||
|
|||
if (isSilentAudio) { |
|||
Logger.debug('检测到无声音频,设置任务类型为 -1'); |
|||
// 设置无声音频状态 (使用特殊值 -1) |
|||
await SqfliteApi.editMeeting(item['id'], {'tasktype': -1}); |
|||
if (item['id'] == meetingId) { |
|||
Logger.debug('当前会议设置 tasktype.value 为 -1'); |
|||
tasktype.value = -1; // 无声音频 |
|||
} |
|||
|
|||
// 更新taskFinishList中的状态 |
|||
Map taskFinishData = taskFinishList.firstWhere( |
|||
(element) => element['id'] == item['id'], |
|||
orElse: () => {}, |
|||
); |
|||
if (taskFinishData.isNotEmpty) { |
|||
taskFinishData['tasktype'] = -1; |
|||
taskFinishList.refresh(); |
|||
} |
|||
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
|||
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
|||
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
|||
} else { |
|||
Logger.error('识别失败,原因未知'); |
|||
// 识别失败,更新数据库状态为失败 |
|||
await SqfliteApi.editMeeting(item['id'], {'tasktype': 0}); |
|||
if (item['id'] == meetingId) { |
|||
tasktype.value = 0; // 识别失败 |
|||
} |
|||
|
|||
// 更新taskFinishList中的状态 |
|||
Map taskFinishData = taskFinishList.firstWhere( |
|||
(element) => element['id'] == item['id'], |
|||
orElse: () => {}, |
|||
); |
|||
if (taskFinishData.isNotEmpty) { |
|||
taskFinishData['tasktype'] = 0; |
|||
taskFinishList.refresh(); |
|||
} |
|||
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
|||
} |
|||
|
|||
// 从任务列表中移除 |
|||
_taskList.removeWhere((element) => element['id'] == item['id']); |
|||
_storage.write("meeting_task_list_v2", _taskList); |
|||
} |
|||
} catch (e) { |
|||
Logger.error('文件识别流程发生异常:${e.toString()}'); |
|||
Logger.error('堆栈信息:${StackTrace.current}'); |
|||
|
|||
// 更新数据库状态为失败 |
|||
try { |
|||
await SqfliteApi.editMeeting(item['id'], {'tasktype': 0}); |
|||
} catch (dbError) { |
|||
Logger.error('数据库更新会议状态失败:$dbError'); |
|||
} |
|||
|
|||
// 出错时也要从任务列表中移除,避免无限重试 |
|||
_taskList.removeWhere((element) => element['id'] == item['id']); |
|||
_storage.write("meeting_task_list_v2", _taskList); |
|||
|
|||
// 更新UI状态 - 如果是当前会议,显示错误状态 |
|||
if (item['id'] == meetingId) { |
|||
tasktype.value = 0; // 识别失败 |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 检查是否为无声音频 |
|||
bool _checkIfSilentAudio(Map<String, dynamic> response) { |
|||
Logger.debug('检查是否为无声音频,响应:${response.toString()}'); |
|||
|
|||
// 检查API响应中的错误信息或状态码 |
|||
// 根据实际API响应格式调整检测逻辑 |
|||
|
|||
// 检查是否有错误码表示无声音频 |
|||
if (response['code'] != null) { |
|||
int code = response['code']; |
|||
Logger.debug('检测到响应代码:$code'); |
|||
// 常见的无声音频错误码 |
|||
if (code == 40001 || |
|||
code == 40002 || |
|||
code == 40003 || |
|||
code == 50001 || |
|||
code == 50002 || |
|||
code == 50003) { |
|||
Logger.debug('根据错误码判断为无声音频:$code'); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// 检查错误消息 |
|||
if (response['message'] != null) { |
|||
String message = response['message'].toString().toLowerCase(); |
|||
Logger.debug('检测到响应消息:$message'); |
|||
if (message.contains('silent') || |
|||
message.contains('no audio') || |
|||
message.contains('无声') || |
|||
message.contains('静音') || |
|||
message.contains('empty audio') || |
|||
message.contains('no speech') || |
|||
message.contains('audio too short') || |
|||
message.contains('insufficient audio')) { |
|||
Logger.debug('根据消息判断为无声音频:$message'); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// 检查result中的错误信息 |
|||
if (response['result'] != null && response['result']['error'] != null) { |
|||
String error = response['result']['error'].toString().toLowerCase(); |
|||
Logger.debug('检测到 result.error:$error'); |
|||
if (error.contains('silent') || |
|||
error.contains('no audio') || |
|||
error.contains('无声') || |
|||
error.contains('静音') || |
|||
error.contains('empty audio') || |
|||
error.contains('no speech') || |
|||
error.contains('audio too short')) { |
|||
Logger.debug('根据 result.error 判断为无声音频:$error'); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// 检查是否有音频时长信息且时长为0或极短 |
|||
if (response['result'] != null && response['result']['duration'] != null) { |
|||
double duration = (response['result']['duration'] as num).toDouble(); |
|||
Logger.debug('检测到音频时长:$duration 秒'); |
|||
if (duration <= 0.5) { |
|||
// 音频时长小于0.5秒认为是无声 |
|||
Logger.debug('根据时长过短判断为无声音频:$duration'); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// 检查utterances是否为空且没有其他明确的成功标识 |
|||
bool hasUtterances = false; |
|||
if (response['result'] != null && |
|||
response['result']['utterances'] != null) { |
|||
hasUtterances = (response['result']['utterances'] as List).isNotEmpty; |
|||
} else if (response['resp'] != null && |
|||
response['resp']['utterances'] != null) { |
|||
hasUtterances = (response['resp']['utterances'] as List).isNotEmpty; |
|||
} else if (response['utterances'] != null) { |
|||
hasUtterances = (response['utterances'] as List).isNotEmpty; |
|||
} else if (response['data'] != null && |
|||
response['data']['utterances'] != null) { |
|||
hasUtterances = (response['data']['utterances'] as List).isNotEmpty; |
|||
} else if (response['response'] != null && |
|||
response['response']['utterances'] != null) { |
|||
hasUtterances = (response['response']['utterances'] as List).isNotEmpty; |
|||
} |
|||
|
|||
// 检查文本内容是否为空 |
|||
String textContent = ''; |
|||
if (response['result'] != null && response['result']['text'] != null) { |
|||
textContent = response['result']['text'].toString().trim(); |
|||
} else if (response['text'] != null) { |
|||
textContent = response['text'].toString().trim(); |
|||
} else if (response['data'] != null && response['data']['text'] != null) { |
|||
textContent = response['data']['text'].toString().trim(); |
|||
} |
|||
|
|||
// 如果文本内容为空且音频时长大于1秒,很可能是无声音频 |
|||
if (textContent.isEmpty) { |
|||
double audioDuration = 0; |
|||
if (response['audio_info'] != null && |
|||
response['audio_info']['duration'] != null) { |
|||
audioDuration = (response['audio_info']['duration'] as num).toDouble() / |
|||
1000; // 转换为秒 |
|||
} else if (response['result'] != null && |
|||
response['result']['additions'] != null && |
|||
response['result']['additions']['duration'] != null) { |
|||
audioDuration = |
|||
(response['result']['additions']['duration'] as num).toDouble() / |
|||
1000; // 转换为秒 |
|||
} |
|||
|
|||
Logger.debug('文本内容为空,音频时长:$audioDuration 秒'); |
|||
if (audioDuration > 1.0) { |
|||
// 音频时长大于1秒但没有文本内容,认为是无声音频 |
|||
Logger.debug('判断为无声音频:文本为空且时长充足'); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// 如果没有utterances且响应状态正常,可能是无声音频 |
|||
if (!hasUtterances) { |
|||
// 检查是否有明确的成功状态码 |
|||
bool hasSuccessCode = false; |
|||
if (response['code'] != null) { |
|||
int code = response['code']; |
|||
hasSuccessCode = (code == 0 || code == 200); |
|||
} |
|||
|
|||
// 如果有成功状态码但没有utterances,很可能是无声音频 |
|||
if (hasSuccessCode) { |
|||
Logger.debug('判断为无声音频:成功状态码但无语句'); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
Logger.debug('未判定为无声音频'); |
|||
return false; |
|||
} |
|||
|
|||
Future<void> _addTextList(int id, List utterances, |
|||
[Map? templateData]) async { |
|||
Logger.debug('处理会议 $id 的 ${utterances.length} 条语句'); |
|||
String text = ''; |
|||
String personnel = ''; |
|||
List<Map<String, Object?>> list = []; |
|||
|
|||
for (int i = 0; i < utterances.length; i++) { |
|||
var element = utterances[i]; |
|||
Logger.debug('处理第 $i 条语句:${element.toString()}'); |
|||
|
|||
String speaker = ''; |
|||
String content = ''; |
|||
int startTime = 0; |
|||
int endTime = 0; |
|||
|
|||
// 提取说话人信息 - 检查多种可能的字段名 |
|||
if (element['additions'] != null && element['additions'].isNotEmpty) { |
|||
if (element['additions']['speaker'] != null) { |
|||
speaker = 'Speaker ${element['additions']['speaker']}'; |
|||
} |
|||
} else if (element['speaker'] != null) { |
|||
speaker = 'Speaker ${element['speaker']}'; |
|||
} else if (element['speaker_id'] != null) { |
|||
speaker = 'Speaker ${element['speaker_id']}'; |
|||
} |
|||
|
|||
// 提取文本内容 - 检查多种可能的字段名 |
|||
if (element['text'] != null) { |
|||
content = element['text'].toString(); |
|||
} else if (element['content'] != null) { |
|||
content = element['content'].toString(); |
|||
} else if (element['transcript'] != null) { |
|||
content = element['transcript'].toString(); |
|||
} else { |
|||
Logger.error('语句中未找到文本内容:$element'); |
|||
continue; |
|||
} |
|||
|
|||
// 提取时间信息 - 检查多种可能的字段名和格式 |
|||
if (element['start_time'] != null) { |
|||
startTime = _parseTime(element['start_time']); |
|||
} else if (element['startTime'] != null) { |
|||
startTime = _parseTime(element['startTime']); |
|||
} else if (element['begin'] != null) { |
|||
startTime = _parseTime(element['begin']); |
|||
} |
|||
|
|||
if (element['end_time'] != null) { |
|||
endTime = _parseTime(element['end_time']); |
|||
} else if (element['endTime'] != null) { |
|||
endTime = _parseTime(element['endTime']); |
|||
} else if (element['end'] != null) { |
|||
endTime = _parseTime(element['end']); |
|||
} |
|||
|
|||
if (speaker.isNotEmpty) { |
|||
text += '[$speaker]: '; |
|||
if (!personnel.contains(speaker)) { |
|||
personnel += '[$speaker] '; |
|||
} |
|||
} |
|||
|
|||
list.add({ |
|||
'meetingid': id, |
|||
'content': content, |
|||
'starttime': startTime, |
|||
'endtime': endTime, |
|||
'speaker': speaker, |
|||
}); |
|||
text += '$content\n'; |
|||
|
|||
Logger.debug('已添加语句:说话人=$speaker,内容=$content,开始=$startTime,结束=$endTime'); |
|||
} |
|||
|
|||
Logger.debug('处理完成的语句总数:${list.length}'); |
|||
var result = await SqfliteApi.insertMeetingSpeaker(list); |
|||
if (result != null) { |
|||
// 初始用于总结/概览的文本,先用原始拼接文本作为默认值 |
|||
String summarySourceText = text; |
|||
// 根据目标语言决定是否需要翻译文本列表 |
|||
final String targetLanguage = |
|||
((templateData?['targetLanguage']) ?? '').toString(); |
|||
final String sourceLanguage = |
|||
((templateData?['language']) ?? '').toString(); |
|||
bool needTranslate = false; |
|||
if (targetLanguage.isNotEmpty) { |
|||
// 若未指定录音语言,默认认为需要翻译;否则当目标语言与录音语言主码不同则翻译 |
|||
if (sourceLanguage.isEmpty) { |
|||
needTranslate = true; |
|||
} else { |
|||
String srcPrefix = sourceLanguage.toLowerCase().substring(0, 2); |
|||
String tgtPrefix = targetLanguage.toLowerCase().substring(0, 2); |
|||
needTranslate = srcPrefix != tgtPrefix; |
|||
} |
|||
} |
|||
if (needTranslate) { |
|||
try { |
|||
// 保持在“正在转写”状态,先进行翻译为目标语言 |
|||
await _translateTextList(id, targetLanguage); |
|||
} catch (e) { |
|||
Logger.error('转写文本翻译失败:${e.toString()}'); |
|||
} |
|||
// 翻译完成后再写入 personnel、设置已完成并刷新 UI |
|||
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; |
|||
} |
|||
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
|||
// 由聚合器统一对外呈现状态,不直接写入标准版列表 |
|||
// 用最新(已翻译)的文本重建总结/概览输入 |
|||
summarySourceText = ''; |
|||
for (var element in newSpeakerList) { |
|||
final String spk = (element['speaker'] ?? '').toString(); |
|||
final String cnt = (element['content'] ?? '').toString(); |
|||
if (spk.isNotEmpty) { |
|||
summarySourceText += '[$spk]: '; |
|||
} |
|||
summarySourceText += '$cnt\n'; |
|||
} |
|||
Map taskFinishData = taskFinishList.firstWhere( |
|||
(element) => element['id'] == id, |
|||
orElse: () => {}, |
|||
); |
|||
if (taskFinishData.isNotEmpty) { |
|||
taskFinishData['tasktype'] = 2; |
|||
taskFinishList.refresh(); |
|||
} |
|||
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
|||
} else { |
|||
// 原有中文直出流程 |
|||
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; |
|||
} |
|||
// 仍使用最新DB中的文本构建总结/概览输入,避免与原始拼接文本不一致 |
|||
summarySourceText = ''; |
|||
for (var element in newSpeakerList) { |
|||
final String spk = (element['speaker'] ?? '').toString(); |
|||
final String cnt = (element['content'] ?? '').toString(); |
|||
if (spk.isNotEmpty) { |
|||
summarySourceText += '[$spk]: '; |
|||
} |
|||
summarySourceText += '$cnt\n'; |
|||
} |
|||
Map taskFinishData = taskFinishList.firstWhere( |
|||
(element) => element['id'] == id, |
|||
orElse: () => {}, |
|||
); |
|||
if (taskFinishData.isNotEmpty) { |
|||
taskFinishData['tasktype'] = 2; |
|||
taskFinishList.refresh(); |
|||
} |
|||
_storage.write("meeting_task_finish_list_v2", taskFinishList); |
|||
} |
|||
// 使用统一的总结服务生成概览与总结 |
|||
final String tplPrompt = ((templateData?['prompt']) ?? '').toString(); |
|||
final String tplOutline = ((templateData?['outline']) ?? '').toString(); |
|||
final String outCode = (((templateData?['targetLanguage']) ?? |
|||
(templateData?['language']) ?? |
|||
'') as String) |
|||
.toLowerCase(); |
|||
String langDesc = ''; |
|||
if (outCode.startsWith('zh')) { |
|||
langDesc = '中文'; |
|||
} else if (outCode.startsWith('en')) { |
|||
langDesc = '英文'; |
|||
} else if (outCode.startsWith('ja')) { |
|||
langDesc = '日文'; |
|||
} else if (outCode.startsWith('ko')) { |
|||
langDesc = '韩文'; |
|||
} else if (outCode.startsWith('fr')) { |
|||
langDesc = '法文'; |
|||
} else if (outCode.startsWith('de')) { |
|||
langDesc = '德文'; |
|||
} else if (outCode.startsWith('es')) { |
|||
langDesc = '西班牙文'; |
|||
} else if (outCode.startsWith('ru')) { |
|||
langDesc = '俄文'; |
|||
} else if (outCode.startsWith('it')) { |
|||
langDesc = '意大利文'; |
|||
} else if (outCode.startsWith('pt')) { |
|||
langDesc = '葡萄牙文'; |
|||
} else if (outCode.startsWith('th')) { |
|||
langDesc = '泰文'; |
|||
} else if (outCode.startsWith('vi')) { |
|||
langDesc = '越南文'; |
|||
} |
|||
final String promptToUse = tplPrompt.isEmpty |
|||
? (langDesc.isEmpty |
|||
? '' |
|||
: '输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。') |
|||
: (langDesc.isEmpty |
|||
? tplPrompt |
|||
: tplPrompt + ' 输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。'); |
|||
final String outlineToUse = tplOutline.isEmpty |
|||
? (langDesc.isEmpty |
|||
? '' |
|||
: '输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。') |
|||
: (langDesc.isEmpty |
|||
? tplOutline |
|||
: tplOutline + ' 输出语言:' + langDesc + '。请全程使用' + langDesc + '输出。'); |
|||
try { |
|||
final details = await SqfliteApi.getMeetingDetails(id); |
|||
final tid = (details['taskid'] ?? '') as String; |
|||
final transcriptBytes = utf8.encode(summarySourceText).length; |
|||
final svc = Get.isRegistered<UsageStatsService>() |
|||
? Get.find<UsageStatsService>() |
|||
: Get.put(UsageStatsService(), permanent: true); |
|||
svc.completeMeetingAsrRecord( |
|||
id: tid, |
|||
transcriptBytes: transcriptBytes, |
|||
completeTimestamp: DateTime.now().millisecondsSinceEpoch, |
|||
); |
|||
} catch (e) { |
|||
Logger.w('MeetingASRrecord', '完成用量记录失败: ' + e.toString()); |
|||
} |
|||
|
|||
_summaryTaskService.addTaskList( |
|||
id, summarySourceText, promptToUse, outlineToUse); |
|||
} |
|||
} |
|||
|
|||
Future<void> _translateTextList(int id, String targetLanguage) async { |
|||
// 读取刚插入的文本条目 |
|||
List speakerList = await SqfliteApi.getMeetingSpeaker(id); |
|||
// 构造简洁的可翻译列表 |
|||
List toTranslate = speakerList |
|||
.map((e) => { |
|||
'id': e['id'], |
|||
'speaker': (e['speaker'] ?? '').toString(), |
|||
'content': (e['content'] ?? '').toString(), |
|||
}) |
|||
.toList(); |
|||
|
|||
final String userJson = jsonEncode({ |
|||
'targetLanguage': targetLanguage, |
|||
'items': toTranslate, |
|||
}); |
|||
|
|||
// 根据目标语言构建提示语 |
|||
String tgtName = targetLanguage.toLowerCase(); |
|||
String langDesc = '目标语言'; |
|||
if (tgtName.startsWith('zh')) { |
|||
langDesc = '中文'; |
|||
} else if (tgtName.startsWith('en')) { |
|||
langDesc = '英文'; |
|||
} else if (tgtName.startsWith('ja')) { |
|||
langDesc = '日文'; |
|||
} else if (tgtName.startsWith('ko')) { |
|||
langDesc = '韩文'; |
|||
} else if (tgtName.startsWith('fr')) { |
|||
langDesc = '法文'; |
|||
} else if (tgtName.startsWith('de')) { |
|||
langDesc = '德文'; |
|||
} else if (tgtName.startsWith('es')) { |
|||
langDesc = '西班牙文'; |
|||
} else if (tgtName.startsWith('ru')) { |
|||
langDesc = '俄文'; |
|||
} else if (tgtName.startsWith('it')) { |
|||
langDesc = '意大利文'; |
|||
} else if (tgtName.startsWith('pt')) { |
|||
langDesc = '葡萄牙文'; |
|||
} else if (tgtName.startsWith('th')) { |
|||
langDesc = '泰文'; |
|||
} else if (tgtName.startsWith('vi')) { |
|||
langDesc = '越南文'; |
|||
} |
|||
|
|||
final String systemPrompt = '你是专业的翻译助手。请将每条items中的content精准翻译为' + |
|||
langDesc + |
|||
',保持术语专业、语气自然。仅返回JSON数组,元素形如{"id":<原id>,"content":"<翻译后文本>"},不要额外说明,不要漏掉任何一条。'; |
|||
|
|||
final String? translated = await DeapsoundAIService().sendMessage( |
|||
messages: [ |
|||
{'role': 'system', 'content': systemPrompt}, |
|||
{'role': 'user', 'content': userJson}, |
|||
], |
|||
); |
|||
|
|||
if (translated == null || translated.isEmpty) return; |
|||
List<dynamic> arr; |
|||
try { |
|||
arr = jsonDecode(translated); |
|||
} catch (_) { |
|||
// 若模型返回包裹对象,尝试取字段 |
|||
try { |
|||
final obj = jsonDecode(translated); |
|||
arr = (obj is Map && obj['data'] is List) ? (obj['data'] as List) : []; |
|||
} catch (e) { |
|||
Logger.error('翻译结果 JSON 解析失败:${e.toString()}'); |
|||
return; |
|||
} |
|||
} |
|||
if (arr.isEmpty) return; |
|||
// 批量更新 |
|||
for (var item in arr) { |
|||
if (item is Map && item['id'] != null) { |
|||
await SqfliteApi.editMeetingSpeaker([ |
|||
{ |
|||
'id': item['id'], |
|||
'content': (item['content'] ?? '').toString(), |
|||
} |
|||
]); |
|||
} |
|||
} |
|||
// 刷新缓存 |
|||
if (id == meetingId) { |
|||
textList = await SqfliteApi.getMeetingSpeaker(id); |
|||
// 主动触发监听刷新 UI(MeetingDetailsController 通过监听 tasktype 同步 textList) |
|||
try { |
|||
tasktype.refresh(); |
|||
} catch (_) {} |
|||
} |
|||
} |
|||
} |
|||
@ -1,187 +0,0 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
|
|||
import '../../../core/utils/logger.dart'; |
|||
import '../db/sqflite_api.dart'; |
|||
import '../deapsound_ai_service.dart'; |
|||
import 'meeting_task_service.dart'; |
|||
import 'meeting_task_service2.dart'; |
|||
|
|||
class SummaryTaskService extends GetxService { |
|||
final _taskService = Get.find<MeetingTaskService>(); |
|||
final _taskService2 = Get.find<MeetingTaskService2>(); |
|||
|
|||
final GetStorage _storage = GetStorage(); |
|||
|
|||
List _taskList = []; |
|||
|
|||
bool _isTask = false; |
|||
|
|||
String summary = ''; //AI总结内容 |
|||
String overview = ''; //AI概览内容 |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_getTaskList(); |
|||
} |
|||
|
|||
void cancelTask() { |
|||
_taskList.clear(); |
|||
_storage.remove("summary_task_list"); |
|||
} |
|||
|
|||
void _getTaskList() { |
|||
_taskList = _storage.read("summary_task_list") ?? []; |
|||
_executeTask(); |
|||
} |
|||
|
|||
Future<void> addTaskList( |
|||
int id, String text, String prompt, String outline) async { |
|||
Logger.debug("添加总结任务列表"); |
|||
_taskList.add({ |
|||
'id': id, |
|||
'text': text, |
|||
'prompt': prompt, |
|||
'outline': outline, |
|||
}); |
|||
_storage.write("summary_task_list", _taskList); |
|||
_executeTask(); |
|||
} |
|||
|
|||
void _executeTask() async { |
|||
if (_taskList.isEmpty) return; |
|||
if (_isTask) return; |
|||
_isTask = true; |
|||
var task = _taskList.first; |
|||
_aiServiceMessage( |
|||
task['id'], |
|||
task['text'], |
|||
task['prompt'], |
|||
task['outline'], |
|||
); |
|||
Logger.debug("执行总结任务列表"); |
|||
} |
|||
|
|||
void removeTask(int id) { |
|||
_taskList.removeWhere((element) => element['id'] == id); |
|||
_storage.write("summary_task_list", _taskList); |
|||
} |
|||
|
|||
void _aiServiceMessage(int id, String text, String prompt, String outline) { |
|||
final ai = DeapsoundAIService(); |
|||
String summaryText = ''; |
|||
String overviewText = ''; |
|||
var summaryDone = false; |
|||
var overviewDone = false; |
|||
var summarySuccess = false; |
|||
var overviewSuccess = false; |
|||
|
|||
void tryFinish() { |
|||
if (summaryDone && overviewDone) { |
|||
if (summarySuccess && |
|||
overviewSuccess && |
|||
summaryText.isNotEmpty && |
|||
overviewText.isNotEmpty) { |
|||
_aiServiceValue(id, summaryText, overviewText); |
|||
} else { |
|||
_isTask = false; |
|||
_executeTask(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
final summaryStream = ai.sendMessageStream(messages: [ |
|||
{'role': 'system', 'content': prompt}, |
|||
{'role': 'user', 'content': text}, |
|||
]).timeout(const Duration(seconds: 120)); |
|||
|
|||
final overviewStream = ai.sendMessageStream(messages: [ |
|||
{'role': 'system', 'content': outline}, |
|||
{'role': 'user', 'content': text}, |
|||
]).timeout(const Duration(seconds: 120)); |
|||
|
|||
summaryStream.listen((chunk) { |
|||
summaryText += chunk; |
|||
}, onError: (error) { |
|||
summaryDone = true; |
|||
summarySuccess = false; |
|||
tryFinish(); |
|||
}, onDone: () { |
|||
summaryDone = true; |
|||
summarySuccess = true; |
|||
tryFinish(); |
|||
}, cancelOnError: true); |
|||
|
|||
overviewStream.listen((chunk) { |
|||
overviewText += chunk; |
|||
}, onError: (error) { |
|||
overviewDone = true; |
|||
overviewSuccess = false; |
|||
tryFinish(); |
|||
}, onDone: () { |
|||
overviewDone = true; |
|||
overviewSuccess = true; |
|||
tryFinish(); |
|||
}, cancelOnError: true); |
|||
} |
|||
|
|||
Future<void> _aiServiceValue( |
|||
int id, String summaryText, String overviewText) async { |
|||
final exists = _taskList.any((item) => item['id'] == id); |
|||
if (!exists) { |
|||
_isTask = false; |
|||
_executeTask(); |
|||
return; |
|||
} |
|||
await SqfliteApi.editMeeting(id, { |
|||
'tasktype': 3, |
|||
'summary': summaryText, |
|||
'overview': overviewText, |
|||
}); |
|||
|
|||
// 更新 V1 Service 状态 |
|||
if (id == _taskService.meetingId) { |
|||
summary = summaryText; |
|||
overview = overviewText; |
|||
_taskService.tasktype.value = 3; |
|||
} |
|||
|
|||
// 更新 V2 Service 状态 |
|||
if (id == _taskService2.meetingId) { |
|||
summary = summaryText; |
|||
overview = overviewText; |
|||
_taskService2.summary = summaryText; |
|||
_taskService2.overview = overviewText; |
|||
_taskService2.tasktype.value = 3; |
|||
} |
|||
|
|||
// 更新 V1 任务完成列表 |
|||
Map taskFinishData = _taskService.taskFinishList.firstWhere( |
|||
(element) => element['id'] == id, |
|||
orElse: () => {}, |
|||
); |
|||
if (taskFinishData.isNotEmpty) { |
|||
taskFinishData['tasktype'] = 3; |
|||
_taskService.taskFinishList.refresh(); |
|||
} |
|||
_storage.write("meeting_task_finish_list", _taskService.taskFinishList); |
|||
|
|||
// 更新 V2 任务完成列表 |
|||
Map taskFinishData2 = _taskService2.taskFinishList.firstWhere( |
|||
(element) => element['id'] == id, |
|||
orElse: () => {}, |
|||
); |
|||
if (taskFinishData2.isNotEmpty) { |
|||
taskFinishData2['tasktype'] = 3; |
|||
_taskService2.taskFinishList.refresh(); |
|||
} |
|||
_storage.write("meeting_task_finish_list_v2", _taskService2.taskFinishList); |
|||
|
|||
_taskList.removeWhere((element) => element['id'] == id); |
|||
_storage.write("summary_task_list", _taskList); |
|||
_isTask = false; |
|||
_executeTask(); |
|||
Logger.debug("总结任务完成"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue