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

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);
}
}