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.
971 lines
35 KiB
971 lines
35 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 '../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 (_) {}
|
|
}
|
|
}
|
|
}
|
|
|