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.
404 lines
13 KiB
404 lines
13 KiB
import 'dart:convert';
|
|
|
|
import 'package:get/get.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
|
|
import '../../../core/utils/logger.dart';
|
|
import '../db/sqflite_api.dart';
|
|
import '../network/api.dart';
|
|
import '../../models/user_Info.dart';
|
|
import '../../../modules/meeting/model/transcribe_service.dart';
|
|
import 'meeting_upload_service.dart';
|
|
|
|
class MeetingTaskService extends GetxService {
|
|
MeetingUploadService get _uploadService => Get.find<MeetingUploadService>();
|
|
|
|
final GetStorage _storage = GetStorage();
|
|
static const String _pendingLocalRecordingTitleStorageKey =
|
|
'meeting_pending_local_recording_titles';
|
|
|
|
List _taskList = []; //待处理的会议任务列表
|
|
|
|
bool _isTask = false; //是否有正在处理的任务
|
|
|
|
RxInt updateListState = 0.obs; //是否更新首页列表
|
|
RxInt localImportState = 0.obs; //是否触发本地录音导入
|
|
int meetingId = 0;
|
|
RxInt tasktype = 1.obs; //任务类型,0-初始,1-等待转写中,2-转写中,3-等待总结中,4-总结中,5-完成
|
|
List textList = [];
|
|
String personnel = '';
|
|
String summary = '';
|
|
String overview = '';
|
|
|
|
void setPendingLocalRecordingTitle(String wavFileName, String displayTitle) {
|
|
if (wavFileName.isEmpty) return;
|
|
final data = _storage.read(_pendingLocalRecordingTitleStorageKey);
|
|
final map = (data is Map)
|
|
? Map<String, dynamic>.from(data)
|
|
: <String, dynamic>{};
|
|
map[wavFileName] = displayTitle;
|
|
_storage.write(_pendingLocalRecordingTitleStorageKey, map);
|
|
}
|
|
|
|
String? takePendingLocalRecordingTitle(String wavFileName) {
|
|
if (wavFileName.isEmpty) return null;
|
|
final data = _storage.read(_pendingLocalRecordingTitleStorageKey);
|
|
if (data is! Map) return null;
|
|
final map = Map<String, dynamic>.from(data);
|
|
final v = map.remove(wavFileName);
|
|
if (v == null) return null;
|
|
_storage.write(_pendingLocalRecordingTitleStorageKey, map);
|
|
return v.toString();
|
|
}
|
|
|
|
void requestLocalImport() {
|
|
localImportState.value++;
|
|
}
|
|
|
|
Future<void> addTask(
|
|
int id,
|
|
String audiourl,
|
|
String formlanguage,
|
|
String tolanguage,
|
|
bool isdistinguishspeaker,
|
|
int templateid,
|
|
String tid,
|
|
TranscribeService serviceType,
|
|
) 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;
|
|
uploadData['tid'] = tid;
|
|
uploadData['service_type'] = serviceType.value;
|
|
} else {
|
|
await submitTask(
|
|
id,
|
|
formlanguage,
|
|
tolanguage,
|
|
isdistinguishspeaker,
|
|
templateid,
|
|
tid,
|
|
serviceType,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
await submitTask(
|
|
id,
|
|
formlanguage,
|
|
tolanguage,
|
|
isdistinguishspeaker,
|
|
templateid,
|
|
tid,
|
|
serviceType,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> submitTask(
|
|
int id,
|
|
String formlanguage,
|
|
String tolanguage,
|
|
bool isdistinguishspeaker,
|
|
int templateid,
|
|
String tid,
|
|
TranscribeService serviceType,
|
|
) async {
|
|
await Api.startTaskEchomeet({
|
|
'id': id,
|
|
'formlanguage': formlanguage,
|
|
'tolanguage': tolanguage,
|
|
'isdistinguishspeaker': isdistinguishspeaker,
|
|
'templateid': templateid,
|
|
'tid': tid,
|
|
'service_type': serviceType.value,
|
|
});
|
|
final seconds = await _getMeetingSeconds(id);
|
|
_decrementMeetingIntegral(cost: seconds);
|
|
_taskList.add({
|
|
'id': id,
|
|
'tasktype': 1,
|
|
});
|
|
await SqfliteApi.editMeetingTitle(
|
|
id,
|
|
{'tasktype': 1},
|
|
);
|
|
updateListState.value++;
|
|
_executeTask();
|
|
}
|
|
|
|
Future<void> refreshTask(
|
|
int id,
|
|
int templateid,
|
|
String tid,
|
|
String tolanguage,
|
|
TranscribeService serviceType,
|
|
) async {
|
|
tasktype.value = 3;
|
|
String oldSummary = '';
|
|
try {
|
|
final data = await SqfliteApi.getMeetingDetails(id);
|
|
oldSummary = (data['details']?['summary'] ?? '').toString();
|
|
} catch (_) {}
|
|
await Api.refreshTaskEchomeet({
|
|
'id': id,
|
|
'templateid': templateid,
|
|
'tid': tid,
|
|
'tolanguage': tolanguage,
|
|
'service_type': serviceType.value,
|
|
});
|
|
_taskList.removeWhere((i) => i['id'] == id);
|
|
_taskList.add({
|
|
'id': id,
|
|
'tasktype': 3,
|
|
'oldSummary': oldSummary,
|
|
'pollAttempts': 0,
|
|
});
|
|
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;
|
|
try {
|
|
while (_taskList.isNotEmpty) {
|
|
await Future.delayed(const Duration(seconds: 5));
|
|
try {
|
|
await _queryTask();
|
|
} catch (e, st) {
|
|
Logger.error('MeetingTask polling _queryTask failed: $e\n$st');
|
|
}
|
|
}
|
|
} finally {
|
|
_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'] ?? [];
|
|
Logger.d(
|
|
'MeetingTask',
|
|
'poll: ids=$ids, server returned ${queryList.length} records, '
|
|
'states=${queryList.map((e) => '${e['id']}:${e['state']}').toList()}',
|
|
);
|
|
for (var element in queryList) {
|
|
int id = element['id'];
|
|
int newTasktype = element['state'];
|
|
Map taskData = _taskList.firstWhere(
|
|
(i) => i['id'] == id,
|
|
orElse: () => {},
|
|
);
|
|
|
|
// 重新总结的兜底:服务端有时会立刻回 state>=5 但 summary 还是旧的,
|
|
// 等真正的新内容生成后才更新。这里用提交重总结时记录的 oldSummary
|
|
// 做比对,只要服务端返回的 summary 还跟旧的一样,就当作未完成继续轮询,
|
|
// 避免任务被过早移出 _taskList、polling 停掉之后再也拿不到新内容。
|
|
const int kMaxStaleAttempts = 60; // ~5 分钟(按 5s 间隔)
|
|
if (taskData.isNotEmpty && newTasktype >= 5) {
|
|
final String oldSummary =
|
|
(taskData['oldSummary'] ?? '').toString();
|
|
final String returnedSummary =
|
|
(element['summary'] ?? '').toString();
|
|
int attempts = (taskData['pollAttempts'] ?? 0) as int;
|
|
if (oldSummary.isNotEmpty &&
|
|
returnedSummary == oldSummary &&
|
|
attempts < kMaxStaleAttempts) {
|
|
taskData['pollAttempts'] = attempts + 1;
|
|
Logger.d(
|
|
'MeetingTask',
|
|
're-summary still pending for id=$id '
|
|
'(server reports state=$newTasktype but summary unchanged, '
|
|
'attempt=${attempts + 1})',
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
List sqlSpeakerList = await SqfliteApi.getMeetingSpeaker(id);
|
|
textList = List.from(sqlSpeakerList);
|
|
tasktype.value = newTasktype;
|
|
}
|
|
}
|
|
if (oldTasktype < 5 && newTasktype >= 5) {
|
|
// 总结完成
|
|
// 重新总结场景下 translate 也会被服务端按新目标语言重写,
|
|
// 必须把它落进 meetingspeaker 表,否则转写 tab 仍然显示旧语言。
|
|
final translateField = element['translate'];
|
|
bool translateRefreshed = false;
|
|
Logger.d(
|
|
'MeetingTask',
|
|
'summary complete for id=$id: '
|
|
'translate ${translateField == null ? 'MISSING' : 'len=${translateField.toString().length}'}, '
|
|
'available element keys=${element.keys.toList()}',
|
|
);
|
|
if (translateField != null &&
|
|
translateField.toString().isNotEmpty) {
|
|
try {
|
|
List<dynamic> decodeList = jsonDecode(translateField);
|
|
List<Map<String, Object?>> speakerList = decodeList
|
|
.map((e) => Map<String, Object?>.from(e))
|
|
.toList();
|
|
await SqfliteApi.deleteMeetingSpeaker(id);
|
|
await SqfliteApi.insertMeetingSpeaker(speakerList);
|
|
translateRefreshed = true;
|
|
} catch (e) {
|
|
Logger.error(
|
|
'MeetingTask: failed to refresh meetingspeaker on '
|
|
'summary complete for id=$id: $e',
|
|
);
|
|
}
|
|
}
|
|
await SqfliteApi.editMeeting(id, {
|
|
'tasktype': newTasktype,
|
|
'personnel': element['personnel'] ?? '',
|
|
'summary': element['summary'] ?? '',
|
|
'overview': element['overview'] ?? '',
|
|
});
|
|
|
|
// 兜底:如果 polling 响应没带 translate(服务端可能在
|
|
// /echomeet_getrecords 里不返回完整字段),主动去
|
|
// /echomeet_getallrecords 拉一次,把新翻译同步到本地 DB。
|
|
if (!translateRefreshed) {
|
|
Logger.d(
|
|
'MeetingTask',
|
|
'translate missing from polling response; '
|
|
'triggering full sync to fetch new speakers for id=$id',
|
|
);
|
|
try {
|
|
var allResp = await Api.getOperationAllRecord();
|
|
List allRecords = allResp['records'] ?? [];
|
|
final fullRecord = allRecords.firstWhere(
|
|
(r) => r['id'] == id,
|
|
orElse: () => null,
|
|
);
|
|
if (fullRecord != null &&
|
|
fullRecord['translate'] != null &&
|
|
fullRecord['translate'].toString().isNotEmpty) {
|
|
List<dynamic> decodeList =
|
|
jsonDecode(fullRecord['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, {
|
|
'personnel': fullRecord['personnel'] ?? '',
|
|
'summary': fullRecord['summary'] ?? '',
|
|
'overview': fullRecord['overview'] ?? '',
|
|
});
|
|
Logger.d(
|
|
'MeetingTask',
|
|
'full sync wrote ${speakerList.length} speaker rows '
|
|
'for id=$id',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
Logger.error(
|
|
'MeetingTask: fallback full sync failed for id=$id: $e',
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 获取会议音频秒数
|
|
Future<int> _getMeetingSeconds(int id) async {
|
|
try {
|
|
final data = await SqfliteApi.getMeetingDetails(id);
|
|
final v = data['seconds'];
|
|
if (v is int) return v;
|
|
if (v is num) return v.toInt();
|
|
if (v is String) return int.tryParse(v) ?? 0;
|
|
return 0;
|
|
} catch (_) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// 减少用户会议积分
|
|
void _decrementMeetingIntegral({required int cost}) {
|
|
if (!User.isLoggedIn()) return;
|
|
if (cost <= 0) return;
|
|
final next = User.instance.meetintegral - cost;
|
|
User.instance.updateUserInfo({'meetintegral': next < 0 ? 0 : next});
|
|
//GetStorage().write('user_info', User.instance.toJson());
|
|
}
|
|
}
|
|
|