import 'dart:async'; import 'dart:convert'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; 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 '../../../core/utils/upload_oss.dart'; import '../../../data/models/appconfig.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/deapsound_ai_service.dart'; import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; import 'meeting_controller.dart'; class MeetingDetailsController extends GetxController with GetTickerProviderStateMixin { late TabController tabController; final RxInt currentIndex = 0.obs; late AnimationController lottieController; final _meetingController = Get.find(); final PlayerController playerController = PlayerController(); RxBool isTop = false.obs; RxInt durationMax = 0.obs; RxDouble dragOffset = 210.w.obs; RxBool isPlay = false.obs; RxInt currentDuration = 0.obs; RxInt speakerAllStarttime = (-1).obs; RxInt speakerAllEndtime = (-1).obs; int id = 0; RxBool isLoading = true.obs; Rx meetingData = MeetingModel().obs; Rx meetingDetails = MeetingDetailsModel().obs; RxList textList = [].obs; String allText = ''; RxBool isSpeakerText = false.obs; List editSpeakerTextList = []; RxInt editSpeakerTextId = 0.obs; String editSpeakerTextChange = ''; String markmapLibJs = ''; String d3Js = ''; String markmapViewJs = ''; String markmapToolbarJs = ''; String saveSvgAsPngJs = ''; String _systemPrompt = ''; String _userId = ''; bool _isDisposed = false; final String _appid = '2316081933'; final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; final String _cluster = 'volc_auc_common_flash'; final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; Map _headers = {}; @override void onInit() { super.onInit(); tabController = TabController(length: 4, vsync: this); tabController.addListener(() { if (currentIndex.value == 2 && tabController.index != 2) { FocusScope.of(Get.context!).unfocus(); } currentIndex.value = tabController.index; }); lottieController = AnimationController(vsync: this) ..duration = const Duration(seconds: 2); final Map args = Get.arguments ?? {}; id = args['id']; _headers = { 'Authorization': 'Bearer; $_token', 'Content-Type': 'application/json', }; _getAgent(); _getDataDetails(); _loadJS(); } @override void onClose() { tabController.dispose(); playerController.dispose(); _isDisposed = true; lottieController.dispose(); super.onClose(); } void popInvokedWithResult(didPop, result) async { if (meetingData.value.tasktype != meetingDetails.value.tasktype) { await SqfliteApi.editMeetingTitle( id, {'tasktype': meetingDetails.value.tasktype}); int index = _meetingController.dataList.indexWhere((item) => item['id'] == id); if (index != -1) { final dataMap = Map.from(_meetingController.dataList[index]); dataMap['tasktype'] = meetingDetails.value.tasktype; if (dataMap['taskid'].isEmpty) { dataMap['taskid'] = meetingDetails.value.taskid; } _meetingController.dataList[index] = dataMap; int taskIndex = _meetingController.dataListTask .indexWhere((item) => item['id'] == id); if (taskIndex != -1) { if (meetingDetails.value.tasktype == 1) { _meetingController.dataListTask[taskIndex] = dataMap; } else { _meetingController.dataListTask.removeAt(taskIndex); } } else { if (meetingDetails.value.tasktype == 1) { _meetingController.dataListTask.add(dataMap); } } } } _meetingController.isDisposed = false; _meetingController.queryDataListTask( _meetingController.dataListTask, isAdd: false, ); } void _getAgent() async { final GetStorage storage = GetStorage(); Map userInfo = storage.read("user_info"); _userId = userInfo['user']['uid']; _systemPrompt = AppConfig.agentsystemPrompt("meet_assistant") ?? ''; } void _getDataDetails() async { final data = await SqfliteApi.getMeetingDetails(id); meetingData.value = MeetingModel.fromJson(data); meetingDetails.value = MeetingDetailsModel.fromJson(data['details']); if (meetingDetails.value.tasktype >= 3) { dragOffset.value = 0; isTop.value = true; } await playerController.preparePlayer( path: meetingData.value.filepath, shouldExtractWaveform: false, noOfSamples: 88, ); durationMax.value = await playerController.getDuration(DurationType.max); playerController.setFinishMode(finishMode: FinishMode.pause); playerController.onCurrentDurationChanged.listen((int position) { currentDuration.value = position; if (speakerAllEndtime.value > 0) { if (position >= speakerAllEndtime.value) { playerController.pausePlayer(); speakerAllStarttime.value = -1; speakerAllEndtime.value = -1; } } }); playerController.onPlayerStateChanged.listen((PlayerState playerState) { isPlay.value = playerState.isPlaying; if (_isDisposed) return; if (playerState.isPlaying) { lottieController.repeat(); } else { lottieController.stop(); } }); if (meetingDetails.value.tasktype >= 2) { List speakerList = await SqfliteApi.getMeetingSpeaker(id); textList.value = List.from(speakerList); } if (meetingDetails.value.tasktype == 1 || meetingDetails.value.tasktype == 2) { summaryCreate(); } isLoading.value = false; } void _loadJS() async { markmapLibJs = await rootBundle.loadString('assets/js/markmap-lib.js'); d3Js = await rootBundle.loadString('assets/js/d3@7.js'); markmapViewJs = await rootBundle.loadString('assets/js/markmap-view.js'); markmapToolbarJs = await rootBundle.loadString('assets/js/markmap-toolbar.js'); saveSvgAsPngJs = await rootBundle.loadString('assets/js/saveSvgAsPng.min.js'); } void editSpeakerText() async { if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) { Map? foundElement = editSpeakerTextList.firstWhere( (element) => element['id'] == editSpeakerTextId.value, orElse: () => null, ); if (foundElement != null) { foundElement['content'] = editSpeakerTextChange; } else { editSpeakerTextList.add({ 'id': editSpeakerTextId.value, 'content': editSpeakerTextChange, }); } } if (editSpeakerTextList.isNotEmpty) { EasyLoading.show(); var result = await SqfliteApi.editMeetingSpeaker(editSpeakerTextList); if (result != null) { for (var element in editSpeakerTextList) { int index = textList.indexWhere((item) => item['id'] == element['id']); if (index != -1) { final dataMap = Map.from(textList[index]); dataMap['content'] = element['content']; textList[index] = dataMap; } } Synchrodata.saveOperationRecord('update', { 'original': jsonEncode(textList), 'id': id, }); isSpeakerText.value = false; editSpeakerTextId.value = 0; editSpeakerTextList = []; } EasyLoading.dismiss(); } else { isSpeakerText.value = false; editSpeakerTextId.value = 0; editSpeakerTextList = []; } } void summaryCreate() async { if (meetingDetails.value.tasktype <= 1) { await _audioSpeaker(); } if (allText.isEmpty) { String text = ''; for (var element in textList) { if (element['speaker'].isNotEmpty) { text += '[${element['speaker']}]: '; } text += '${element['content']}\n'; } allText = text; } if (allText.isEmpty) return; _aiServiceMessage(); } void _aiServiceMessage() { int awaitQuantity = 0; int verifyQuantity = 0; String summaryText = meetingDetails.value.summary; String overviewText = meetingDetails.value.overview; if (summaryText.isEmpty) { verifyQuantity++; DeapsoundAIService().sendMessage( messages: [ { 'role': 'system', 'content': '您将扮演一名会议总结专家,拥有超强的信息归纳与细分能力。不仅能高效处理各类通用场景会议,精准提炼会议核心内容,为后续思维导图生成奠定坚实基础。若涉及人员,人员名称保留[]。并根据内容只输出对应语言。', }, {'role': 'user', 'content': allText}, ], ).then((value) { awaitQuantity++; summaryText = value; if (awaitQuantity >= verifyQuantity) { _aiServiceValue(summaryText, overviewText); } }); } if (overviewText.isEmpty) { verifyQuantity++; DeapsoundAIService().sendMessage( messages: [ { 'role': 'system', 'content': '您将扮演一名会议事件归纳专家,能准确抓住会议中事件的关键点,并每个事件只需一句话精简总结。', }, {'role': 'user', 'content': allText}, ], ).then((value) { awaitQuantity++; overviewText = value; if (awaitQuantity >= verifyQuantity) { _aiServiceValue(summaryText, overviewText); } }); } } void _aiServiceValue(String summaryText, String overviewText) { SqfliteApi.editMeeting(id, { 'tasktype': 3, 'summary': summaryText, 'overview': overviewText, }); meetingDetails.value.tasktype = 3; meetingDetails.value.summary = summaryText; meetingDetails.value.overview = overviewText; meetingDetails.refresh(); } Future _audioSpeaker() async { EasyLoading.show(); String audiourl = meetingData.value.audiourl; if (audiourl.isEmpty) { audiourl = await UploadOss.upload( filepath: meetingData.value.filepath, rootDir: 'audio', ); meetingData.value.audiourl = audiourl; } String taskId = meetingDetails.value.taskid; if (taskId.isEmpty) { taskId = await _submitTask(audiourl); await SqfliteApi.editMeeting(id, {'tasktype': 1, 'taskid': taskId}); meetingDetails.value.taskid = taskId; meetingDetails.value.tasktype = 1; meetingDetails.refresh(); } EasyLoading.dismiss(); await _fileRecognize(taskId); } Future _submitTask(String audiourl) async { String format = _getFileType(audiourl); final requestBody = jsonEncode({ "app": { "appid": _appid, "token": _token, "cluster": _cluster, }, "user": { "uid": _userId, }, "audio": { "format": format, "url": audiourl, }, "additions": { "with_speaker_info": "True", } }); try { final response = await http.post( Uri.parse('$_serviceUrl/submit'), headers: _headers, body: requestBody, ); if (response.statusCode != 200) { throw Exception('Submit failed with status: ${response.statusCode}'); } final respDic = jsonDecode(response.body); final taskId = respDic['resp']['id'] as String; Logger.debug('taskId: $taskId'); return taskId; } catch (e) { Logger.error('Submit error: ${e.toString()}'); rethrow; } } String _getFileType(String path) { List array = path.split('.'); return array[array.length - 1]; } Future> _queryTask(String taskId) async { final queryBody = jsonEncode({ "appid": _appid, "token": _token, "id": taskId, "cluster": _cluster, }); try { final response = await http.post( Uri.parse('$_serviceUrl/query'), headers: _headers, body: queryBody, ); 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; } } Future _fileRecognize(String taskId) async { try { final startTime = DateTime.now().millisecondsSinceEpoch; if (_isDisposed) return; var respDic = await _queryTask(taskId); var code = respDic['resp']['code'] as int; if (code == 1000) { await _addTextList(respDic['resp']['utterances']); return; } else if (code < 2000) { Logger.debug("FileRecognize failed"); return; } while (!_isDisposed) { await Future.delayed(const Duration(seconds: 2)); if (_isDisposed) return; respDic = await _queryTask(taskId); code = respDic['resp']['code'] as int; if (code == 1000) { await _addTextList(respDic['resp']['utterances']); return; } else if (code < 2000) { Logger.debug("FileRecognize failed"); return; } final currentTime = DateTime.now().millisecondsSinceEpoch; if (currentTime - startTime > 300 * 1000) { Logger.debug('wait time exceeds 300s'); return; } } } catch (e) { Logger.error('Error occurred: ${e.toString()}'); } } Future _addTextList(List utterances) async { String text = ''; String personnel = ''; List> list = []; for (var element in utterances) { String speaker = ''; if (element['additions'].isNotEmpty) { speaker = 'Speaker ${element['additions']['speaker']}'; text += '[$speaker]: '; if (!personnel.contains(speaker)) { personnel += '[$speaker] '; } } list.add({ 'meetingid': id, 'content': element['text'], 'starttime': element['start_time'], 'endtime': element['end_time'], 'speaker': speaker, }); text += '${element['text']}\n'; } var result = await SqfliteApi.insertMeetingSpeaker(list); if (result != null) { await SqfliteApi.editMeeting(id, {'tasktype': 2, 'personnel': personnel}); List speakerList = await SqfliteApi.getMeetingSpeaker(id); textList.value = List.from(speakerList); Synchrodata.saveOperationRecord('update', { 'original': jsonEncode(textList), 'id': id, }); meetingDetails.value.tasktype = 2; meetingDetails.value.personnel = personnel; meetingDetails.refresh(); } allText = text; } }