diff --git a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt index 33f368e82..172e3776f 100644 --- a/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt +++ b/android/app/src/main/kotlin/com/saitong/voitrans/MainActivity.kt @@ -19,6 +19,11 @@ import io.flutter.plugin.common.MethodChannel // 添加这行导入 class MainActivity: FlutterActivity() { private val TAG = "MainActivity" private var pendingDeepLink: Intent? = null + private var pendingNavigationIntent: Intent? = null + + private val EXTRA_NAVIGATE_TO = "com.saitong.voitrans.extra.NAVIGATE_TO" + private val NAVIGATE_TO_MEETING_RECORD = "meetingRecord" + private val ROUTE_MEETING_RECORD = "/meeting/record" override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "🚀 MainActivity onCreate 开始") @@ -36,6 +41,11 @@ class MainActivity: FlutterActivity() { pendingDeepLink = intent Log.d(TAG, "🔗 暂存深链接,等待Flutter引擎准备完成") } + + if (intent?.hasExtra(EXTRA_NAVIGATE_TO) == true) { + pendingNavigationIntent = intent + Log.d(TAG, "🧭 暂存导航意图,等待Flutter引擎准备完成") + } // 设置日志级别,减少系统级日志 suppressSystemLogs() @@ -54,7 +64,7 @@ class MainActivity: FlutterActivity() { Log.d(TAG, "New Intent extras: ${intent.extras}") // 处理深链接 - handleDeepLink(intent) + handleNavigationIntent(intent) } override fun provideFlutterEngine(context: Context): FlutterEngine? { @@ -106,17 +116,29 @@ class MainActivity: FlutterActivity() { Handler(Looper.getMainLooper()).postDelayed({ pendingDeepLink?.let { intent -> Log.d(TAG, "🔗 处理待处理的深链接") - handleDeepLink(intent) + handleNavigationIntent(intent) pendingDeepLink = null } + + pendingNavigationIntent?.let { intent -> + Log.d(TAG, "🧭 处理待处理的导航意图") + handleNavigationIntent(intent) + pendingNavigationIntent = null + } }, 1000) // 延迟1秒确保Flutter完全准备好 Log.d(TAG, "✅ configureFlutterEngine 完成") } // 处理深链接的统一方法 - private fun handleDeepLink(intent: Intent?) { + private fun handleNavigationIntent(intent: Intent?) { intent?.let { + val navigateTo = it.getStringExtra(EXTRA_NAVIGATE_TO) + if (navigateTo == NAVIGATE_TO_MEETING_RECORD) { + pushRouteToFlutter(ROUTE_MEETING_RECORD) + return + } + if (it.action == Intent.ACTION_VIEW && it.data != null) { val uri = it.data Log.d(TAG, "🔗 处理深链接: $uri") @@ -133,16 +155,7 @@ class MainActivity: FlutterActivity() { "com.saitong.voitrans/deeplink" ) - val arguments = mapOf( - "url" to uri.toString(), - "scheme" to uri?.scheme, - "host" to uri?.host, - "path" to uri?.path, - "query" to uri?.query, - "queryParameters" to uri?.queryParameterNames?.associateWith { - uri.getQueryParameter(it) - } - ) + val arguments = buildUrlArguments(uri.toString()) Log.d(TAG, "🔗 准备发送参数: $arguments") @@ -160,6 +173,41 @@ class MainActivity: FlutterActivity() { } } + private fun pushRouteToFlutter(route: String) { + try { + val engine = flutterEngine + if (engine == null) { + Log.w(TAG, "❌ FlutterEngine 为空,无法 pushRoute") + return + } + Handler(Looper.getMainLooper()).post { + try { + engine.navigationChannel.pushRoute(route) + } catch (e: Exception) { + Log.e(TAG, "❌ pushRoute 失败", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "❌ pushRouteToFlutter 出错", e) + } + } + + private fun buildUrlArguments(url: String): Map { + return try { + val uri = android.net.Uri.parse(url) + mapOf( + "url" to url, + "scheme" to uri.scheme, + "host" to uri.host, + "path" to uri.path, + "query" to uri.query, + "queryParameters" to uri.queryParameterNames.associateWith { uri.getQueryParameter(it) } + ) + } catch (e: Exception) { + mapOf("url" to url) + } + } + override fun onDestroy() { super.onDestroy() } @@ -242,4 +290,3 @@ class MainActivity: FlutterActivity() { } } } - diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 2870209ff..94c3ecef5 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,4 +1,4 @@ -import '../../data/services/meeting/summary_task_service.dart'; +import '../../data/services/meeting/meeting_task_service.dart'; import '/data/services/pay_service.dart'; import '/data/services/qq_music.dart'; @@ -10,9 +10,6 @@ import '../../../data/services/navigation_manager.dart'; import '../../../data/services/ast_service.dart'; import '../../../data/services/speech_impl/azure_ast_service.dart'; import 'package:get/get.dart'; -import '../../data/services/meeting/meeting_task_service.dart'; -import '../../data/services/meeting/meeting_task_service2.dart'; -import '../../data/services/meeting/meeting_task_aggregator.dart'; import '../../data/services/meeting/meeting_upload_service.dart'; import '../../data/services/novice_guide_service.dart'; import '../../data/services/speech_factory.dart'; @@ -72,15 +69,6 @@ class InitialBinding extends Bindings { // 会议助手任务服务 Get.lazyPut(() => MeetingTaskService(), fenix: true); - // 会议助手任务服务(极速识别版,2小时内音频) - Get.lazyPut(() => MeetingTaskService2(), fenix: true); - // 会议助手任务服务聚合器(统一列表与状态) - Get.lazyPut(() => MeetingTaskAggregator(), - fenix: true); - - // 会议助手总结任务服务 - Get.lazyPut(() => SummaryTaskService(), fenix: true); - // 会议助手音频上传 Get.lazyPut(() => MeetingUploadService(), fenix: true); diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 2e340c301..2a9a6cd8c 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -437,6 +437,15 @@ const Map enUS = { 'audioVideoRecording': 'Audio/Video Recording', 'callRecording': 'Call Recording', 'importAudio': 'Import Audio', + 'meetingRecordExitTitle': 'Exit Recording', // 退出录音 + 'meetingRecordExitMessage': 'Choose what to do before leaving', // 请选择离开后的操作 + 'meetingRecordEndAndSave': 'End recording and save', // 结束录音并保存 + 'meetingRecordEndWithoutSave': 'End recording without saving', // 结束录音不保存 + 'meetingRecordContinueInBackground': + 'Continue recording in background', // 后台继续录音 + 'meetingRecordRecordingNotificationTitle': 'Recording in progress', // 录音进行中 + 'meetingRecordRecordingNotificationBody': + 'Recording in background. Tap to return to the app.', // 正在后台录音,点击返回应用 'transcribing': 'Transcribing...', 'summarizing': 'Summarizing...', 'analyzing': 'Analyzing...', diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index aaeecfba8..2bffe4762 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -414,6 +414,13 @@ const Map zhCN = { 'audioVideoRecording': '音、视频录音', 'callRecording': '通话录音', 'importAudio': '导入音频', + 'meetingRecordExitTitle': '退出录音', // 退出录音 + 'meetingRecordExitMessage': '请选择离开后的操作', // 请选择离开后的操作 + 'meetingRecordEndAndSave': '结束录音并保存', // 结束录音并保存 + 'meetingRecordEndWithoutSave': '结束录音不保存', // 结束录音不保存 + 'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音 + 'meetingRecordRecordingNotificationTitle': '录音进行中', // 录音进行中 + 'meetingRecordRecordingNotificationBody': '正在后台录音,点击返回应用', // 正在后台录音,点击返回应用 'transcribing': '正在转写...', 'summarizing': '正在总结...', 'analyzing': '正在分析...', diff --git a/lib/core/utils/synchrodata.dart b/lib/core/utils/synchrodata.dart index a7fa19d1d..b4c1da1eb 100644 --- a/lib/core/utils/synchrodata.dart +++ b/lib/core/utils/synchrodata.dart @@ -1,9 +1,11 @@ import 'dart:convert'; +import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import '../../data/services/db/database_helper.dart'; import '../../data/services/db/sqflite_api.dart'; +import '../../data/services/meeting/meeting_task_service.dart'; import '../../data/services/network/api.dart'; class Synchrodata { @@ -76,35 +78,35 @@ class Synchrodata { static Future uploadOperationRecords({bool isSynchro = false}) async { if (_isSynchro) return; _isSynchro = true; - if (_records.isNotEmpty || _delids.isNotEmpty) { - // 获取需要修改的全部数据 - List putRecords = []; - for (var element in _records) { - Map result = {'details': {}}; - List speakerList = []; - if (!_delids.contains(element['id'])) { - result = await SqfliteApi.getMeetingDetails(element['number']); - speakerList = await SqfliteApi.getMeetingSpeaker(element['number']); - } - putRecords.add({ - ...Map.from(result), - ...Map.from(result['details']), - 'rtype': result['type'], - 'original': jsonEncode(speakerList), - ...Map.from(element), - }); - } - var result = await Api.putOperationRecord({ - 'delids': _delids, - 'records': putRecords, - }); - if (result != null) { - _records = []; - _delids = []; - _storage.write("operation_records", _records); - _storage.write("operation_delids", _delids); - } - } + // if (_records.isNotEmpty || _delids.isNotEmpty) { + // // 获取需要修改的全部数据 + // List putRecords = []; + // for (var element in _records) { + // Map result = {'details': {}}; + // List speakerList = []; + // if (!_delids.contains(element['id'])) { + // result = await SqfliteApi.getMeetingDetails(element['number']); + // speakerList = await SqfliteApi.getMeetingSpeaker(element['number']); + // } + // putRecords.add({ + // ...Map.from(result), + // ...Map.from(result['details']), + // 'rtype': result['type'], + // 'original': jsonEncode(speakerList), + // ...Map.from(element), + // }); + // } + // var result = await Api.putOperationRecord({ + // 'delids': _delids, + // 'records': putRecords, + // }); + // if (result != null) { + // _records = []; + // _delids = []; + // _storage.write("operation_records", _records); + // _storage.write("operation_delids", _delids); + // } + // } if (isSynchro) { await _synchrodata(); } @@ -113,32 +115,40 @@ class Synchrodata { // 同步数据 static Future _synchrodata() async { - var result = await Api.getOperationRecord(); + var result = await Api.getOperationAllRecord(); List records = result['records'] ?? []; if (records.isEmpty) return; + final meetingTaskService = Get.find(); + List taskList = []; final db = await DatabaseHelper().database; await db.transaction((txn) async { for (var element in records) { + if (element['state'] > 0 && element['state'] < 5) { + taskList.add({ + 'id': element['id'], + 'tasktype': element['state'], + }); + } final List existList = await txn.query( 'meeting', where: 'id = ?', - whereArgs: [element['number']], + whereArgs: [element['id']], ); Map meetingData = { - 'id': element['number'], + 'id': element['id'], 'title': element['title'] ?? '', 'type': element['rtype'] ?? '', 'seconds': element['seconds'] ?? 0, 'audiourl': element['audiourl'] ?? '', - 'tasktype': element['tasktype'] ?? 0, - 'creationtime': element['creationtime'] ?? 0, + 'tasktype': element['state'] ?? 0, + 'creationtime': (element['creationtime'] ?? 0) * 1000, }; Map meetingDetailsData = { - 'meetingid': element['number'], + 'meetingid': element['id'], 'address': element['address'] ?? '', 'personnel': element['personnel'] ?? '', 'taskid': element['taskid'] ?? '', - 'tasktype': element['tasktype'] ?? 0, + 'tasktype': element['state'] ?? 0, 'overview': element['overview'] ?? '', 'summary': element['summary'] ?? '', 'mindmap': '', @@ -148,13 +158,13 @@ class Synchrodata { 'meeting', meetingData, where: 'id = ?', - whereArgs: [element['number']], + whereArgs: [element['id']], ); await txn.update( 'meetingdetails', meetingDetailsData, where: 'meetingid = ?', - whereArgs: [element['number']], + whereArgs: [element['id']], ); } else { await txn.insert('meeting', { @@ -163,48 +173,21 @@ class Synchrodata { }); await txn.insert('meetingdetails', meetingDetailsData); } - if (element['original'].isNotEmpty) { - final List existSpeakerList = await txn.query( + if (element['translate'].isNotEmpty) { + List speakerList = jsonDecode(element['translate']); + await txn.delete( 'meetingspeaker', where: 'meetingid = ?', - whereArgs: [element['number']], + whereArgs: [element['id']], ); - List speakerList = jsonDecode(element['original']); - if (existSpeakerList.isNotEmpty) { - final batch = txn.batch(); - for (var i = 0; i < existSpeakerList.length; i++) { - Map item = speakerList[i] ?? {}; - if (item.isNotEmpty) { - batch.update( - 'meetingspeaker', - { - 'meetingid': item['meetingid'], - 'content': item['content'], - 'starttime': item['starttime'], - 'endtime': item['endtime'], - 'speaker': item['speaker'], - }, - where: 'id = ?', - whereArgs: [existSpeakerList[i]['id']], - ); - } - } - await batch.commit(); - } else { - final batch = txn.batch(); - for (var item in speakerList) { - batch.insert('meetingspeaker', { - 'meetingid': item['meetingid'], - 'content': item['content'], - 'starttime': item['starttime'], - 'endtime': item['endtime'], - 'speaker': item['speaker'], - }); - } - await batch.commit(); + final batch = txn.batch(); + for (var item in speakerList) { + batch.insert('meetingspeaker', item); } + await batch.commit(); } } }); + meetingTaskService.startTask(taskList); } } diff --git a/lib/data/services/account_service.dart b/lib/data/services/account_service.dart index 1293ecec5..8a0e5cb70 100644 --- a/lib/data/services/account_service.dart +++ b/lib/data/services/account_service.dart @@ -3,11 +3,10 @@ import 'package:get_storage/get_storage.dart'; import 'package:logger/logger.dart'; import '../../core/utils/synchrodata.dart'; import '../../modules/login/controllers/googleLogin.dart'; -import '../../modules/login/controllers/facebookLogin.dart'; import '../services/network/api.dart'; import 'db/database_helper.dart'; -import 'meeting/meeting_task_service.dart'; import '../../data/models/user_Info.dart'; +import 'meeting/meeting_upload_service.dart'; /// 账号管理服务 /// @@ -84,8 +83,8 @@ class AccountService extends GetxService { Future signOut() async { try { // 清除会议记录任务列表 - final taskService = Get.find(); - taskService.cancelTask(); + final uploadService = Get.find(); + uploadService.cancelUpload(); // 上传会议记录操作记录 await Synchrodata.uploadOperationRecords(); diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart index 45fc7f898..8481cf800 100644 --- a/lib/data/services/db/sqflite_api.dart +++ b/lib/data/services/db/sqflite_api.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../../core/utils/synchrodata.dart'; import 'database_helper.dart'; class SqfliteApi { @@ -58,11 +57,10 @@ class SqfliteApi { static Future insertMeeting(Map meeting) async { final db = await DatabaseHelper().database; try { - int id = 0; await db.transaction((txn) async { - id = await txn.insert('meeting', meeting); + await txn.insert('meeting', meeting); await txn.insert('meetingdetails', { - 'meetingid': id, + 'meetingid': meeting['id'], 'address': '', 'personnel': '', 'taskid': '', @@ -71,12 +69,8 @@ class SqfliteApi { 'summary': '', 'mindmap': '', }); - Synchrodata.saveOperationRecord('insert', { - ...meeting, - 'id': id, - }); }); - return id; + return true; } catch (e) { Get.snackbar( 'error'.tr, // 错误 @@ -99,10 +93,6 @@ class SqfliteApi { where: 'meetingid = ?', whereArgs: [id], ); - Synchrodata.saveOperationRecord('update', { - ...meeting, - 'id': id, - }); return true; } catch (e) { Get.snackbar( @@ -168,14 +158,16 @@ class SqfliteApi { } } + // 删除商务会议助手转写内容 + static Future deleteMeetingSpeaker(id) async { + final db = await DatabaseHelper().database; + await db.delete('meetingspeaker', where: 'meetingid = ?', whereArgs: [id]); + } + // 修改商务会议助手标题 static Future editMeetingTitle(id, Map meeting) async { final db = await DatabaseHelper().database; await db.update('meeting', meeting, where: 'id = ?', whereArgs: [id]); - Synchrodata.saveOperationRecord('update', { - ...meeting, - 'id': id, - }); } // 删除商务会议助手 @@ -187,9 +179,6 @@ class SqfliteApi { .delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]); await txn .delete('meetingspeaker', where: 'meetingid = ?', whereArgs: [id]); - Synchrodata.saveOperationRecord('delete', { - 'id': id, - }); }); } diff --git a/lib/data/services/meeting/meeting_task_aggregator.dart b/lib/data/services/meeting/meeting_task_aggregator.dart deleted file mode 100644 index 5de375cda..000000000 --- a/lib/data/services/meeting/meeting_task_aggregator.dart +++ /dev/null @@ -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(); - final MeetingTaskService2 s2 = Get.find(); - - 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 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; - } -} diff --git a/lib/data/services/meeting/meeting_task_service.dart b/lib/data/services/meeting/meeting_task_service.dart index 3d7886b19..c8d7bb9e7 100644 --- a/lib/data/services/meeting/meeting_task_service.dart +++ b/lib/data/services/meeting/meeting_task_service.dart @@ -1,133 +1,34 @@ 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 '../network/api.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(); - SummaryTaskService get _summaryTaskService => Get.find(); 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 _headers = {}; - Map _headers1 = {}; - + RxInt updateListState = 0.obs; //是否更新首页列表 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 _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 addTask(int id, String audiourl, String taskid, bool isSpeaker, - String language, String prompt, String outline) async { + RxInt tasktype = 1.obs; //任务类型,0-初始,1-等待转写中,2-转写中,3-等待总结中,4-总结中,5-完成 + List textList = []; + String personnel = ''; + String summary = ''; + String overview = ''; + + Future 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, @@ -136,343 +37,169 @@ class MeetingTaskService extends GetxService { if (uploadData.isNotEmpty) { if (uploadData['audiourl'].isEmpty) { uploadData['isTask'] = true; - uploadData['isSpeaker'] = isSpeaker; - uploadData['language'] = language; - uploadData['prompt'] = prompt; - uploadData['outline'] = outline; + uploadData['formlanguage'] = formlanguage; + uploadData['tolanguage'] = tolanguage; + uploadData['isdistinguishspeaker'] = isdistinguishspeaker; + uploadData['templateid'] = templateid; } else { - await addTaskList(id, uploadData['audiourl'], taskid, isSpeaker, - language, prompt, outline); + await submitTask( + id, + formlanguage, + tolanguage, + isdistinguishspeaker, + templateid, + ); } } } else { - await addTaskList( - id, audiourl, taskid, isSpeaker, language, prompt, outline); + await submitTask( + id, + formlanguage, + tolanguage, + isdistinguishspeaker, + templateid, + ); } } - ///添加会议任务列表 - ///[id] 会议ID - ///[audiourl] 音频URL - ///[taskId] 任务ID - ///[isSpeaker] 是否为发言人 - ///[language] 语言 - ///[prompt] 提示词 - ///[outline] 大纲 - Future 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() - ? Get.find() - : Get.put(UsageStatsService(), permanent: true); - svc.addMeetingAsrRecord(record); - } catch (e) { - Logger.w('MeetingASRrecord', '提交用量记录失败: ' + e.toString()); - } - } catch (_) {} + Future 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, - 'taskid': taskId, 'tasktype': 1, - 'prompt': prompt, - 'outline': outline, }); - taskFinishList.add({ + await SqfliteApi.editMeetingTitle( + id, + {'tasktype': 1}, + ); + updateListState.value++; + _executeTask(); + } + + Future refreshTask(int id, int templateid) async { + tasktype.value = 3; + await Api.refreshTaskEchomeet({ 'id': id, - 'tasktype': 1, + 'templateid': templateid, + }); + _taskList.add({ + 'id': id, + 'tasktype': 3, }); - _storage.write("meeting_task_list", _taskList); - _storage.write("meeting_task_finish_list", taskFinishList); + await SqfliteApi.editMeetingTitle( + id, + {'tasktype': 3}, + ); + updateListState.value++; _executeTask(); } - ///执行会议任务 + Future 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: 2)); - Logger.info("lxm--轮询---会议任务列表$_taskList"); - for (var task in List.from(_taskList)) { - await _fileRecognize(task); - } + await Future.delayed(const Duration(seconds: 5)); + await _queryTask(); } _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 _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, // 启用语种识别 - } + Future removeTask(int id) async { + _taskList.removeWhere((element) => element['id'] == id); + await Api.delRecordEchomeet({ + 'ids': [id], }); - - 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 array = path.split('.'); - return array[array.length - 1]; - } - - ///轮询语音转文字任务状态 - ///[taskId] 任务ID - Future> _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; - } + void startTask(List data) { + _taskList = data; + _executeTask(); } - ///轮询语音转文字任务状态,若任务完成则处理转写结果 - ///[item] 包含任务ID、会议ID、AI总结提示词、AI概览提示词的Map - Future _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()}'); - } + void stopTask() { + _taskList.clear(); } - ///处理语音识别完成后的转写结果,包括数据库存储、状态更新和触发AI分析 - ///[id] 会议ID - ///[utterances] 语音转文字结果 - ///[prompt] AI总结提示词 - ///[outline] AI概览提示词 - Future _addTextList( - int id, List utterances, String prompt, String outline) 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); - 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, + Future _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 (taskFinishData.isNotEmpty) { - taskFinishData['tasktype'] = 2; - taskFinishList.refresh(); + if (taskData.isNotEmpty) { + int oldTasktype = taskData['tasktype']; + if (oldTasktype != newTasktype) { + if (oldTasktype < 3 && newTasktype >= 3) { + // 转写完成 + List decodeList = jsonDecode(element['translate']); + List> speakerList = + decodeList.map((e) => Map.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++; + } } - _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() - ? Get.find() - : Get.put(UsageStatsService(), permanent: true); - svc.completeMeetingAsrRecord( - id: tid, - transcriptBytes: transcriptBytes, - completeTimestamp: DateTime.now().millisecondsSinceEpoch, - ); - } catch (e) { - Logger.w('MeetingASRrecord', '完成用量记录失败: ' + e.toString()); + if (newTasktype >= 5) { + _taskList.removeWhere((i) => i['id'] == id); } - } catch (_) {} - - // 触发AI分析,分别请求总结和概览 - _summaryTaskService.addTaskList(id, text, prompt, outline); + } } } diff --git a/lib/data/services/meeting/meeting_task_service2.dart b/lib/data/services/meeting/meeting_task_service2.dart deleted file mode 100644 index ff69e20ff..000000000 --- a/lib/data/services/meeting/meeting_task_service2.dart +++ /dev/null @@ -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(); - SummaryTaskService get _summaryTaskService => Get.find(); - - 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 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 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() - ? Get.find() - : 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> _recognizeAudio( - String audiourl, { - bool enableSpeakerInfo = true, - String language = '', - String? requestIdOverride, - }) async { - String requestId = - requestIdOverride ?? DateTime.now().millisecondsSinceEpoch.toString(); - - // 为每次请求生成新的headers - 适配大模型极速版API - Map 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 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 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 _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 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 _addTextList(int id, List utterances, - [Map? templateData]) async { - Logger.debug('处理会议 $id 的 ${utterances.length} 条语句'); - String text = ''; - String personnel = ''; - List> 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() - ? Get.find() - : 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 _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 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 (_) {} - } - } -} diff --git a/lib/data/services/meeting/meeting_upload_service.dart b/lib/data/services/meeting/meeting_upload_service.dart index 4472cc409..af54033a2 100644 --- a/lib/data/services/meeting/meeting_upload_service.dart +++ b/lib/data/services/meeting/meeting_upload_service.dart @@ -4,10 +4,10 @@ import 'package:get_storage/get_storage.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/upload_oss.dart'; +import '../network/api.dart'; import '../usage_stats_service.dart'; import '../db/sqflite_api.dart'; import 'meeting_task_service.dart'; -import 'meeting_task_service2.dart'; /// 管理会议音频文件上传服务 class MeetingUploadService extends GetxService { @@ -47,26 +47,20 @@ class MeetingUploadService extends GetxService { String rootDir, { bool isUpload = true, }) async { - bool flash = false; int fileSize = 0; try { - fileSize = await File(filePath).length(); //获取文件大小 - final details = await SqfliteApi.getMeetingDetails(id); - final int seconds = (details['seconds'] ?? 0) as int; - flash = seconds > 0 && seconds <= 7200; + fileSize = await File(filePath).length(); } catch (_) {} - _storage.write("meeting_service_type_$id", flash ? 'fast' : 'standard'); uploadList.add({ 'id': id, 'filepath': filePath, 'rootdir': rootDir, 'audiourl': '', 'isTask': false, - 'isSpeaker': true, - 'language': '', - 'prompt': '', - 'outline': '', - 'useFlashASR': flash, + 'formlanguage': '', + 'tolanguage': '', + 'isdistinguishspeaker': true, + 'templateid': 0, 'fileSizeBytes': fileSize, }); _storage.write("meeting_upload_list", uploadList); @@ -85,18 +79,20 @@ class MeetingUploadService extends GetxService { if (element['audiourl'].isEmpty) { meetingId.value = element['id']; try { - int _sent = 0; - int _total = 0; + int totalProgress = 0; final audiourl = await UploadOss.upload( filepath: element['filepath'], rootDir: element['rootdir'], onSendProgress: (int count, int total) { // uploadProgress.value = count / total; - _sent = count; - _total = total; + totalProgress = total; uploadProgress.value = total > 0 ? count / total : 0.0; }, ); + await Api.upRecordEchomeet({ + 'id': element['id'], + 'audiourl': audiourl, + }); await SqfliteApi.editMeetingTitle( element['id'], {'audiourl': audiourl}, @@ -110,7 +106,8 @@ class MeetingUploadService extends GetxService { try { fileSizeBytes = element['fileSizeBytes'] ?? 0; } catch (_) {} - final transferred = _total > 0 ? _total : fileSizeBytes; + final transferred = + totalProgress > 0 ? totalProgress : fileSizeBytes; svc.recordMeetingObjectStorage( meetingId: element['id'], storageUrl: audiourl, @@ -119,31 +116,16 @@ class MeetingUploadService extends GetxService { uploadTimestamp: DateTime.now().millisecondsSinceEpoch, ); } catch (e) { - Logger.w('MeetingOSrecord', '对象存储用量记录失败: ' + e.toString()); + Logger.w('MeetingOSrecord', '对象存储用量记录失败: $e'); } if (element['isTask']) { - if (element['useFlashASR'] == true) { - final service2 = Get.find(); - await service2.addTaskList( - element['id'], - audiourl, - isSpeaker: element['isSpeaker'], - language: element['language'], - targetLanguage: '', - prompt: element['prompt'], - outline: element['outline'], - ); - } else { - await _taskService.addTaskList( - element['id'], - audiourl, - '', - element['isSpeaker'], - element['language'], - element['prompt'], - element['outline'], - ); - } + await _taskService.submitTask( + element['id'], + element['formlanguage'], + element['tolanguage'], + element['isdistinguishspeaker'], + element['templateid'], + ); } element['audiourl'] = audiourl; uploadProgress.value = 0.0; diff --git a/lib/data/services/meeting/summary_task_service.dart b/lib/data/services/meeting/summary_task_service.dart deleted file mode 100644 index c29741359..000000000 --- a/lib/data/services/meeting/summary_task_service.dart +++ /dev/null @@ -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(); - final _taskService2 = Get.find(); - - 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 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 _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("总结任务完成"); - } -} diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 77615c04d..9920fdf89 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -119,7 +119,16 @@ class Api { } // 获取会议助手数据 - static getOperationRecord([params]) { + static getOperationAllRecord([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_getallrecords', + params: params, + ); + } + + // 轮询多条会议助手转写结果 + static getMultitermRecord([params]) { return DioManager().request( NWMethod.post, '/api/home/echomeet_getrecords', @@ -291,4 +300,68 @@ class Api { params: params, ); } + + static addRecordEchomeet([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_addrecord', + params: params, + ); + } + + static upRecordEchomeet([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_uprecord', + params: params, + ); + } + + static startTaskEchomeet([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_starttask', + params: params, + ); + } + + static refreshTaskEchomeet([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_summary', + params: params, + ); + } + + static readRecordEchomeet([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_readrecord', + params: params, + ); + } + + static delRecordEchomeet([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_delrecords', + params: params, + ); + } + + static modifyRecordEchomeet([params]) { + Map data = { + 'title': 'NULL', + 'address': 'NULL', + 'personnel': 'NULL', + 'translate': 'NULL', + 'summary': 'NULL', + }; + data.addAll(params); + return DioManager().request( + NWMethod.post, + '/api/home/echomeet_modifyrecords', + params: data, + ); + } } diff --git a/lib/modules/meeting/bindings/meeting_record_binding.dart b/lib/modules/meeting/bindings/meeting_record_binding.dart index f4880d510..1a7a13aed 100644 --- a/lib/modules/meeting/bindings/meeting_record_binding.dart +++ b/lib/modules/meeting/bindings/meeting_record_binding.dart @@ -5,7 +5,9 @@ import '../controllers/meeting_record_controller.dart'; class MeetingRecordBinding extends Bindings { @override void dependencies() { - // 延迟注册 MeetingRecordController 控制器 - Get.lazyPut(() => MeetingRecordController()); + if (!Get.isRegistered()) { + Get.put(MeetingRecordController(), + permanent: true); + } } } diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 927bb9f96..aa2e1f4a3 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -9,16 +9,12 @@ 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:path_provider/path_provider.dart'; -import '../../../core/utils/synchrodata.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/meeting/meeting_task_service.dart'; -import '../../../data/services/meeting/meeting_task_service2.dart'; import '../../../data/services/meeting/meeting_upload_service.dart'; -import '../../../data/services/meeting/summary_task_service.dart'; -import '../../../data/services/meeting/meeting_task_aggregator.dart'; +import '../../../data/services/network/api.dart'; import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; @@ -31,12 +27,9 @@ class MeetingDetailsController extends GetxController late AnimationController lottieController; late final Worker _tasktypeListener; - late final Worker _tasktypeListenerV2; final _meetingUploadService = Get.find(); final _meetingTaskService = Get.find(); - final _meetingTaskService2 = Get.find(); - final _summaryTaskService = Get.find(); final PlayerController playerController = PlayerController(); @@ -95,39 +88,19 @@ class MeetingDetailsController extends GetxController ..duration = const Duration(seconds: 2); final Map args = Get.arguments ?? {}; id = args['id']; - final storage = GetStorage(); - final String svc = - (storage.read("meeting_service_type_$id") ?? '').toString(); - final bool isFast = svc == 'fast'; _meetingTaskService.meetingId = id; - _meetingTaskService2.meetingId = id; - Get.find().setMeeting(id); _getDataDetails(); _loadJS(); _tasktypeListener = ever(_meetingTaskService.tasktype, (value) { - if (isFast && value != 3) return; meetingDetails.value.tasktype = value; if (_everType <= value) { - if (!isFast && value == 2) { - textList.value = _meetingTaskService.textList; - } else if (value == 3) { - meetingDetails.value.summary = _summaryTaskService.summary; - meetingDetails.value.overview = _summaryTaskService.overview; - summaryNumberModifications.value++; - } - } - meetingDetails.refresh(); - }); - _tasktypeListenerV2 = ever(_meetingTaskService2.tasktype, (value) { - if (!isFast) return; - meetingDetails.value.tasktype = value; - if (_everType <= value) { - if (value == 2) { - textList.value = _meetingTaskService2.textList; - } else if (value == 3) { - meetingDetails.value.summary = _summaryTaskService.summary; - meetingDetails.value.overview = _summaryTaskService.overview; + if (value >= 5) { + meetingDetails.value.personnel = _meetingTaskService.personnel; + meetingDetails.value.summary = _meetingTaskService.summary; + meetingDetails.value.overview = _meetingTaskService.overview; summaryNumberModifications.value++; + } else if (value >= 3) { + textList.value = _meetingTaskService.textList; } } meetingDetails.refresh(); @@ -137,22 +110,17 @@ class MeetingDetailsController extends GetxController @override void onClose() { _tasktypeListener.dispose(); - _tasktypeListenerV2.dispose(); _currentDurationSubscription?.cancel(); _playerStateSubscription?.cancel(); tabController.dispose(); playerController.dispose(); lottieController.dispose(); - Get.find().clearMeeting(); super.onClose(); } void popInvokedWithResult(didPop, result) async { _meetingUploadService.removeUpload(id); - _meetingTaskService.removeTask(id); - _meetingTaskService2.removeTask(id); _meetingTaskService.meetingId = 0; - _meetingTaskService2.meetingId = 0; } void _getDataDetails() async { @@ -161,14 +129,17 @@ class MeetingDetailsController extends GetxController meetingDetails.value = MeetingDetailsModel.fromJson(data['details']); _everType = meetingDetails.value.tasktype + 1; _meetingTaskService.tasktype.value = meetingDetails.value.tasktype; - if (meetingDetails.value.tasktype >= 3) { + if (meetingDetails.value.tasktype == 5) { + await _meetingTaskService.readTask(id); + } + if (meetingDetails.value.tasktype >= 5) { dragOffset.value = 0; isTop.value = true; } if (meetingData.value.filepath.isNotEmpty) { await _loadPlayerController(); } - if (meetingDetails.value.tasktype >= 2) { + if (meetingDetails.value.tasktype >= 3) { List speakerList = await SqfliteApi.getMeetingSpeaker(id); textList.value = List.from(speakerList); } @@ -267,9 +238,9 @@ class MeetingDetailsController extends GetxController textList[index] = dataMap; } } - Synchrodata.saveOperationRecord('update', { - 'original': jsonEncode(textList), + await Api.modifyRecordEchomeet({ 'id': id, + 'translate': jsonEncode(textList), }); isSpeakerText.value = false; editSpeakerTextId.value = 0; @@ -284,64 +255,33 @@ class MeetingDetailsController extends GetxController } void summaryCreate( - bool isSpeaker, String language, String prompt, String outline) async { + String formlanguage, + String tolanguage, + bool isdistinguishspeaker, + int templateid, + ) async { EasyLoading.show(); _everType = 2; final String audiourl = meetingData.value.audiourl; - final int seconds = meetingData.value.seconds; - final storage = GetStorage(); - storage.write("meeting_service_type_$id", - (seconds > 0 && seconds <= 7200) ? 'fast' : 'standard'); - if (seconds > 0 && seconds <= 7200) { - await _meetingTaskService2.addTask( - id, - audiourl, - isSpeaker: isSpeaker, - language: language, - targetLanguage: '', - prompt: prompt, - outline: outline, - ); - } else { - final String taskId = meetingDetails.value.taskid; - await _meetingTaskService.addTask( - id, - audiourl, - taskId, - isSpeaker, - language, - prompt, - outline, - ); - } + await _meetingTaskService.addTask( + id, + audiourl, + formlanguage, + tolanguage, + isdistinguishspeaker, + templateid, + ); meetingDetails.value.tasktype = 1; meetingDetails.refresh(); EasyLoading.dismiss(); } - void refreshSummary(String prompt, String outline) async { - String text = ''; - for (var element in textList) { - if (element['speaker'].isNotEmpty) { - text += '[${element['speaker']}]: '; - } - text += '${element['content']}\n'; - } - _everType = 3; + void refreshSummary(int templateid) async { EasyLoading.show(); - await SqfliteApi.editMeeting(id, {'tasktype': 2}); - meetingDetails.value.tasktype = 2; + _everType = 4; + await _meetingTaskService.refreshTask(id, templateid); + meetingDetails.value.tasktype = 3; meetingDetails.refresh(); - final storage = GetStorage(); - final String svc = - (storage.read("meeting_service_type_$id") ?? '').toString(); - final bool isFast = svc == 'fast'; - if (isFast) { - _meetingTaskService2.tasktype.value = 2; - } else { - _meetingTaskService.tasktype.value = 2; - } - _summaryTaskService.addTaskList(id, text, prompt, outline); EasyLoading.dismiss(); } } diff --git a/lib/modules/meeting/controllers/meeting_home_controller.dart b/lib/modules/meeting/controllers/meeting_home_controller.dart index 7eaab1b26..1e0cf5b4d 100644 --- a/lib/modules/meeting/controllers/meeting_home_controller.dart +++ b/lib/modules/meeting/controllers/meeting_home_controller.dart @@ -19,10 +19,8 @@ import '../../../core/utils/permission_util.dart'; import 'package:share_plus/share_plus.dart'; import '../../../data/services/meeting/meeting_task_service.dart'; -import '../../../data/services/meeting/meeting_task_service2.dart'; -import '../../../data/services/meeting/meeting_task_aggregator.dart'; import '../../../data/services/meeting/meeting_upload_service.dart'; -import '../../../data/services/meeting/summary_task_service.dart'; +import '../../../data/services/network/api.dart'; import '../../../routes/app_routes.dart'; import 'meeting_controller.dart'; @@ -31,9 +29,6 @@ class MeetingHomeController extends GetxController { final meetingController = Get.find(); final meetingUploadService = Get.find(); final meetingTaskService = Get.find(); - final meetingTaskService2 = Get.find(); - final taskAggregator = Get.find(); - final summaryTaskService = Get.find(); final AudioPlayer _audioPlayer = AudioPlayer(); @@ -53,6 +48,8 @@ class MeetingHomeController extends GetxController { // 蓝牙服务 final bleManager = Get.find(); + late final Worker _updateListStateListener; + @override void onInit() { super.onInit(); @@ -68,6 +65,10 @@ class MeetingHomeController extends GetxController { final String path = args['filePath']; addAudio(path); } + _updateListStateListener = + ever(meetingTaskService.updateListState, (value) async { + _getDataList(); + }); } @override @@ -75,11 +76,13 @@ class MeetingHomeController extends GetxController { titleController.dispose(); filtrateController.dispose(); _audioPlayer.dispose(); + _updateListStateListener.dispose(); super.onClose(); } void onPopInvokedWithResult(didPop, result) { - Synchrodata.uploadOperationRecords(isSynchro: true); + // Synchrodata.uploadOperationRecords(isSynchro: true); + meetingTaskService.stopTask(); } void _initGetDataList() async { @@ -246,8 +249,16 @@ class MeetingHomeController extends GetxController { }; // 插入数据库 - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { + final response = await Api.addRecordEchomeet({ + 'rtype': 'LOCAL', + ...data, + }); + final meetingId = response['record']['id']; + final meetingResult = await SqfliteApi.insertMeeting({ + 'id': meetingId, + ...data, + }); + if (meetingResult != null) { // 安排后台上传任务 meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio'); @@ -359,8 +370,16 @@ class MeetingHomeController extends GetxController { 'creationtime': creationTime, }; - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { + final response = await Api.addRecordEchomeet({ + 'rtype': recordType, + ...data, + }); + final meetingId = response['record']['id']; + final meetingResult = await SqfliteApi.insertMeeting({ + 'id': meetingId, + ...data, + }); + if (meetingResult != null) { // 中文注释:上传分类按目录区分 final uploadBucket = archiveToLocal ? 'LocalAudio' : 'TranslatAudio'; meetingUploadService.addUpload(meetingId, usePath, uploadBucket); @@ -475,8 +494,13 @@ class MeetingHomeController extends GetxController { 'tasktype': 0, 'creationtime': creationTime, }; - final meetingId = await SqfliteApi.insertMeeting(data); - if (meetingId != null) { + final response = await Api.addRecordEchomeet({'rtype': type, ...data}); + final meetingId = response['record']['id']; + final meetingResult = await SqfliteApi.insertMeeting({ + 'id': meetingId, + ...data, + }); + if (meetingResult != null) { // 中文注释:根据类型选择上传分类;LOCAL 走 LocalAudio,其它走 ExternalAudio final uploadBucket = type == 'LOCAL' ? 'LocalAudio' : 'ExternalAudio'; meetingUploadService.addUpload( @@ -544,24 +568,24 @@ class MeetingHomeController extends GetxController { return targetPath; } -// 移除原来的_saveFile方法,替换为上面的_copyFileToLocal - - Future _saveFile(PlatformFile file) async { - final directory = await getApplicationDocumentsDirectory(); - final directoryDir = Directory('${directory.path}/ExternalAudio'); - if (!await directoryDir.exists()) { - await directoryDir.create(recursive: true); - } - final String filePath = '${directory.path}/ExternalAudio/${file.name}'; - final File localFile = File(filePath); - await localFile.writeAsBytes(file.bytes!); - return filePath; - } + // 移除原来的_saveFile方法,替换为上面的_copyFileToLocal + // Future _saveFile(PlatformFile file) async { + // final directory = await getApplicationDocumentsDirectory(); + // final directoryDir = Directory('${directory.path}/ExternalAudio'); + // if (!await directoryDir.exists()) { + // await directoryDir.create(recursive: true); + // } + // final String filePath = '${directory.path}/ExternalAudio/${file.name}'; + // final File localFile = File(filePath); + // await localFile.writeAsBytes(file.bytes!); + // return filePath; + // } // 修改标题 void editMeetingTitle(int id) async { if (titleController.text.isNotEmpty) { String title = titleController.text + titleCSuffix; + Api.modifyRecordEchomeet({'id': id, 'title': title}); SqfliteApi.editMeetingTitle(id, {'title': title}); int originalIndex = originalDataList.indexWhere((item) => item['id'] == id); @@ -589,8 +613,7 @@ class MeetingHomeController extends GetxController { SqfliteApi.deleteMeeting(id); _deleteFile(filepath); meetingUploadService.removeUpload(id, isDeleteTask: true); - meetingTaskService.removeTask(id, isDeleteTask: true); - meetingTaskService2.removeTask(id, isDeleteTask: true); + meetingTaskService.removeTask(id); } // 删除文件 diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 792618710..635d39d41 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -4,12 +4,17 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import '/core/utils/logger.dart'; +import '/core/utils/overlay_permission_util.dart'; import '/core/utils/permission_util.dart'; +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; +import 'package:native_plugin/native_plugin.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import '../../../routes/app_routes.dart'; import '../../../data/services/asr_service.dart'; import '../../../data/services/ble_manager.dart'; @@ -24,7 +29,187 @@ enum TimerStatus { /// 负责会议记录的状态管理、音频处理、语音识别和UI交互 class MeetingRecordController extends GetxController with GetTickerProviderStateMixin { - // Services + static const int _backgroundRecordingNotificationId = 91001; + + static FlutterLocalNotificationsPlugin? _localNotificationsPlugin; + final NativePlugin _nativePlugin = NativePlugin(); + Timer? _recordingFloatingBarTimer; + StreamSubscription? _recordingFloatingActionSubscription; + bool _floatingBarManuallyClosed = false; + + Future + _ensureLocalNotificationsPlugin() async { + final existing = _localNotificationsPlugin; + if (existing != null) return existing; + + final plugin = FlutterLocalNotificationsPlugin(); + const androidSettings = + AndroidInitializationSettings('@mipmap/ic_launcher'); + const iosSettings = DarwinInitializationSettings(); + const settings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + await plugin.initialize(settings); + _localNotificationsPlugin = plugin; + return plugin; + } + + Future _requestNotificationPermissionIfNeeded() async { + await PermissionUtil.instance.requestPermission( + permissionType: Permission.notification, + permissionName: 'notificationPermission'.tr, + explanationText: 'notificationPermissionExplanation'.tr, + permanentDenialText: 'notificationPermissionPermanentDenial'.tr, + settingsButtonText: 'goToSettings'.tr, + cancelButtonText: 'cancel'.tr, + ); + } + + Future showBackgroundRecordingIndicator() async { + if (Platform.isAndroid) { + if (_floatingBarManuallyClosed) return; + await _startAndroidRecordingFloatingBar(); + return; + } + + await _requestNotificationPermissionIfNeeded(); + if (!Platform.isIOS) return; + + final plugin = await _ensureLocalNotificationsPlugin(); + final details = NotificationDetails( + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBadge: false, + presentSound: false, + ), + ); + + await plugin.show( + _backgroundRecordingNotificationId, + 'meetingRecordRecordingNotificationTitle'.tr, + 'meetingRecordRecordingNotificationBody'.tr, + details, + ); + } + + Future clearBackgroundRecordingIndicator() async { + if (Platform.isAndroid) { + _floatingBarManuallyClosed = false; + await _stopAndroidRecordingFloatingBar(); + return; + } + if (!Platform.isIOS) return; + final plugin = await _ensureLocalNotificationsPlugin(); + await plugin.cancel(_backgroundRecordingNotificationId); + } + + String _formatTimeForFloatingBar(int ms) { + final totalSeconds = ms ~/ 1000; + final hours = totalSeconds ~/ 3600; + final minutes = (totalSeconds % 3600) ~/ 60; + final secs = totalSeconds % 60; + if (hours > 0) { + return '$hours:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; + } + + String _getRecordingTypeText() { + switch (audioType.value) { + case 0: + return 'liveRecording'.tr; + case 1: + return 'audioVideoRecording'.tr; + case 2: + return 'callRecording'.tr; + default: + return 'liveRecording'.tr; + } + } + + Future _startAndroidRecordingFloatingBar() async { + if (!Platform.isAndroid) return; + if (!isRecording.value) return; + if (_floatingBarManuallyClosed) return; + + final hasPermission = await OverlayPermissionUtil.hasOverlayPermission(); + if (!hasPermission) { + final granted = await OverlayPermissionUtil.requestOverlayPermission(); + if (!granted) return; + } + + final typeText = _getRecordingTypeText(); + final durationText = _formatTimeForFloatingBar(milliseconds.value); + + await _nativePlugin.showRecordingFloatingBar( + duration: durationText, + recordingType: typeText, + isPaused: timerStatus.value == TimerStatus.paused, + ); + + _recordingFloatingBarTimer?.cancel(); + _recordingFloatingBarTimer = + Timer.periodic(const Duration(seconds: 1), (_) { + if (!isRecording.value) { + _stopAndroidRecordingFloatingBar(); + return; + } + _nativePlugin.updateRecordingFloatingBar( + duration: _formatTimeForFloatingBar(milliseconds.value), + recordingType: _getRecordingTypeText(), + isPaused: timerStatus.value == TimerStatus.paused, + ); + }); + } + + Future _stopAndroidRecordingFloatingBar() async { + if (!Platform.isAndroid) return; + _recordingFloatingBarTimer?.cancel(); + _recordingFloatingBarTimer = null; + await _nativePlugin.hideRecordingFloatingBar(); + } + + Future togglePauseResumeFromFloatingBar() async { + if (!isRecording.value) return; + if (timerStatus.value == TimerStatus.running) { + await _pauseRecording(); + return; + } + if (timerStatus.value == TimerStatus.paused) { + await _resumeRecording(); + } + } + + void _setupRecordingFloatingBarActionListener() { + _recordingFloatingActionSubscription?.cancel(); + if (!Platform.isAndroid) return; + _recordingFloatingActionSubscription = + _nativePlugin.recordingFloatingActionStream.listen((action) async { + if (action.isEmpty) return; + switch (action) { + case 'togglePause': + await togglePauseResumeFromFloatingBar(); + return; + case 'openMeetingRecord': + _floatingBarManuallyClosed = false; + if (Get.currentRoute == Routes.meetingRecord) return; + Get.toNamed( + Routes.meetingRecord, + arguments: {'audioTypes': audioType.value}, + ); + return; + case 'closeFloatingBar': + _floatingBarManuallyClosed = true; + await _stopAndroidRecordingFloatingBar(); + return; + default: + return; + } + }); + } + + // 服务 final AsrService _asrService = Get.find(); final BleManager _bleManager = Get.find(); @@ -40,9 +225,11 @@ class MeetingRecordController extends GetxController final RxBool showTextContent = false.obs; final RxBool isRecording = false.obs; //final RxBool isVoiceRecognitionActive = false.obs; - final RxList waveData = [].obs; + final RxDouble rms = 0.0.obs; + + final RecorderController waveformController = RecorderController(); - // Transcription content + // 转写内容 final RxString intermediateContent = ''.obs; final RxString finalContent = ''.obs; @@ -54,56 +241,42 @@ class MeetingRecordController extends GetxController late FocusNode titleFocusNode; final Future appDocDir = getApplicationDocumentsDirectory(); - // Timing variables + // 计时变量 Timer? _timer; int _elapsedMilliseconds = 0; int? _lastStartTime; // 移除了未使用的变量和TabController StreamSubscription? _audioDataSubscription; - // 简化的音波数据管理 - final int frameIntervalMs = 300; // 300ms间隔,更稳定的帧率 - int maxVisibleBars = 60; // 最大可见波形条数量 - Timer? _audioFrameTimer; // 音频帧定时器 - double _currentFrameAmplitude = 0.0; // 当前帧累积振幅 - int _currentFrameSamples = 0; // 当前帧样本数量 - - // 性能优化:音频处理采样控制 - int _audioProcessCounter = 0; - static const int _audioProcessInterval = 1; // 每1个音频包处理1次 - - // 波形流动动画控制器 - late AnimationController waveformAnimationController; - late Animation waveformAnimation; - - // 数据更新控制器 - 统一管理数据更新和动画 - late AnimationController _dataUpdateController; - late Animation _dataUpdateAnimation; - // 设置最大可见波形条数量(根据屏幕宽度) - void setMaxVisibleBars(double screenWidth) { - const barWidth = 3.0; // 波形条宽度 - const barSpacing = 1.0; // 波形条间距 - maxVisibleBars = (screenWidth / (barWidth + barSpacing)).floor(); - } + double _smoothedRms = 0.0; + double _latestRms = 0.0; + Timer? _waveformTimer; + static const Duration _waveformTick = Duration(milliseconds: 40); + static const int _maxWaveSamples = 1200; @override void onInit() { super.onInit(); _initializeComponents(); _setupFromArguments(); - _initializeWaveformAnimation(); + _setupRecordingFloatingBarActionListener(); //generateWaveData(); } @override void onClose() { _cleanupResources(); - // 清理两个AnimationController - waveformAnimationController.dispose(); - _dataUpdateController.dispose(); super.onClose(); } + void applyRouteArgumentsIfIdle() { + if (isEditingTitle.value) return; + if (isRecording.value) return; + if (timerStatus.value != TimerStatus.stopped) return; + _setupFromArguments(); + titleEditingController.text = newName.value; + } + // 请求录音权限 Future _requestRecordPermission() async { try { @@ -127,69 +300,19 @@ class MeetingRecordController extends GetxController } } - /// Initializes UI controllers and focus nodes + /// 初始化 UI 控制器和焦点节点 void _initializeComponents() { titleEditingController = TextEditingController(text: fileName.value); titleFocusNode = FocusNode(); titleFocusNode.addListener(_handleTitleFocusChange); } - /// 初始化波形流动动画和数据更新控制器 - void _initializeWaveformAnimation() { - // 1. 数据更新控制器 - 控制数据提交频率 - _dataUpdateController = AnimationController( - duration: Duration(milliseconds: frameIntervalMs), // 250ms 一次数据更新 - vsync: this, - ); - - _dataUpdateAnimation = - Tween(begin: 0.0, end: 1.0).animate(_dataUpdateController); - - // 监听数据更新动画,每个周期结束时提交数据 - _dataUpdateAnimation.addListener(() { - if (!isRecording.value) return; - - // 每个动画周期结束时,才提交一次数据 - if (_dataUpdateController.value >= 1.0) { - _commitAudioFrame(); - - // 重置动画,继续循环 - _dataUpdateController.reset(); - if (isRecording.value) { - _dataUpdateController.forward(); - } - } - }); - - // 2. 波形流动动画控制器 - 控制视觉流动效果 - waveformAnimationController = AnimationController( - duration: Duration(milliseconds: frameIntervalMs), // 与数据更新同步 - vsync: this, - ); - - waveformAnimation = Tween( - begin: 0.0, - end: 1.0, - ).animate(CurvedAnimation( - parent: waveformAnimationController, - curve: Curves.linear, // 线性动画确保匀速流动 - )); - - // 监听动画完成,实现连续循环 - waveformAnimationController.addStatusListener((status) { - if (status == AnimationStatus.completed && isRecording.value) { - waveformAnimationController.reset(); - waveformAnimationController.forward(); - } - }); - } - - /// Sets up initial state from navigation arguments + /// 根据路由参数设置初始状态 void _setupFromArguments() { final Map args = Get.arguments ?? {}; audioType.value = args['audioTypes'] ?? 0; - // Set default filename based on audio type + // 根据音频类型设置默认文件名 switch (audioType.value) { case 0: fileName.value = "liveRecording".tr; //对应中文:现场录音 @@ -204,11 +327,13 @@ class MeetingRecordController extends GetxController newName.value = fileName.value; } - /// Cleans up resources when controller is disposed + /// 控制器销毁时清理资源 void _cleanupResources() { _timer?.cancel(); - _audioFrameTimer?.cancel(); // 清理音频帧定时器 - // AnimationController在onClose中统一清理 + _stopAndroidRecordingFloatingBar(); + _recordingFloatingActionSubscription?.cancel(); + _recordingFloatingActionSubscription = null; + _stopWaveformTimer(); _recognitionSubscription?.cancel(); _recognitionSubscription = null; _audioDataSubscription?.cancel(); @@ -218,18 +343,25 @@ class MeetingRecordController extends GetxController if (audioType.value != 0) { _bleManager.closeCodec(); } - _asrService.stopRecord(true); + if (isRecording.value) { + _asrService.stopRecord(true); + } isRecording.value = false; + rms.value = 0.0; + _smoothedRms = 0.0; + _latestRms = 0.0; + waveformController.reset(); + waveformController.dispose(); } - /// Handles focus changes in title text field + /// 处理标题输入框焦点变化 void _handleTitleFocusChange() { if (!titleFocusNode.hasFocus && isEditingTitle.value) { saveTitleChange(); } } - /// Starts title editing mode + /// 开始标题编辑模式 void startTitleEditing() { isEditingTitle.value = true; titleEditingController.text = fileName.value; @@ -239,7 +371,7 @@ class MeetingRecordController extends GetxController }); } - /// Saves new title and exits editing mode + /// 保存新标题后退出编辑状态 Future saveTitleChange() async { isEditingTitle.value = true; if (isEditingTitle.value) { @@ -249,12 +381,12 @@ class MeetingRecordController extends GetxController } } - /// Toggles visibility of transcribed text content + /// 切换转写文本内容的显示状态 Future toggleTextDisplay() async { showTextContent.toggle(); } - /// Toggles important mark status + /// 切换重要标记状态 void toggleMarked() => isMarked.toggle(); /// 格式化时间为 MM:SS.S 显示格式 @@ -270,7 +402,7 @@ class MeetingRecordController extends GetxController '.${centiseconds.toString()}'; } - /// Starts/resumes recording based on current state + /// 根据当前状态开始或恢复录音 Future toggleRecording() async { // 等待权限请求完成并获取结果 final bool granted = await _requestRecordPermission(); @@ -281,6 +413,8 @@ class MeetingRecordController extends GetxController return; } + await _requestNotificationPermissionIfNeeded(); + // 根据当前计时器状态执行相应操作 switch (timerStatus.value) { case TimerStatus.stopped: @@ -295,7 +429,7 @@ class MeetingRecordController extends GetxController } } - /// Starts recording session + /// 开始录音会话 Future _startRecording() async { // Prepare storage directory final appDir = await appDocDir; @@ -321,28 +455,24 @@ class MeetingRecordController extends GetxController // 1. 实时音频可视化 // 2. 音频质量检测 // 3. 发送到其他服务 - Logger.d("AudioData", "接收到音频数据: ${audioData.length} 字节"); - // 处理音频数据的逻辑 _processAudioData(audioData); }); isRecording.value = true; - // Update state + // 更新录音状态 fileName.value = fullFileName; newName.value = fullFileName; titleEditingController.text = fullFileName; - // 启动波形动画和音频帧定时器 - _startWaveformAnimation(); - _startAudioFrameTimer(); - // Start hardware components _startHardwareServices(); + _resetWaveform(); + _startWaveformTimer(); startTimer(); } - /// Activates appropriate hardware service based on audio type + /// 根据音频类型启动对应的硬件服务 void _startHardwareServices() { switch (audioType.value) { case 0: @@ -359,58 +489,62 @@ class MeetingRecordController extends GetxController } } - /// Pauses current recording session + /// 暂停当前录音会话 Future _pauseRecording() async { await _asrService.pauseRecord(); - _stopWaveformAnimation(); // 暂停时停止动画 - _stopAudioFrameTimer(); // 停止音频帧定时器 + _stopWaveformTimer(); + rms.value = 0.0; pauseTimer(); } - /// Resumes paused recording session + /// 恢复已暂停的录音会话 Future _resumeRecording() async { await _asrService.resumeRecord(); - _startWaveformAnimation(); // 恢复时启动动画 - _startAudioFrameTimer(); // 重启音频帧定时器 + _startWaveformTimer(); startTimer(); } - /// 处理原始Uint8List音频数据 - 只累积,不更新UI,性能优化 + /// 处理原始Uint8List音频数据 void _processAudioData(Uint8List audioData) { if (!isRecording.value) return; - - // 性能优化:不是每个音频包都处理,减少CPU占用 - _audioProcessCounter++; - if (_audioProcessCounter % _audioProcessInterval != 0) { - return; // 跳过这次处理 + final next = _calculateAmplitude(audioData); + if (_smoothedRms == 0.0) { + _smoothedRms = next; + } else { + _smoothedRms = _smoothedRms * 0.8 + next * 0.2; } + rms.value = _smoothedRms; + _latestRms = _smoothedRms; + } - // 将音频数据转换为振幅值并累积到当前帧 - final amplitude = _calculateAmplitude(audioData); - _currentFrameAmplitude += amplitude; - _currentFrameSamples++; + void _resetWaveform() { + waveformController.reset(); + waveformController.updateFrequency = _waveformTick; } - /// 提交音频帧数据到波形显示 - void _commitAudioFrame() { - // 计算当前帧的平均振幅 - double frameAmplitude = 0.0; - if (_currentFrameSamples > 0) { - frameAmplitude = _currentFrameAmplitude / _currentFrameSamples; - } + void _startWaveformTimer() { + _waveformTimer?.cancel(); + _waveformTimer = Timer.periodic(_waveformTick, (_) { + if (!isRecording.value) return; + if (timerStatus.value != TimerStatus.running) return; + _appendWaveSample(_latestRms); + }); + } - // 转换为显示数据 - final newAmplitude = frameAmplitude * 800.0; + void _stopWaveformTimer() { + _waveformTimer?.cancel(); + _waveformTimer = null; + } - // 更新波形数据 - if (waveData.length >= maxVisibleBars) { - waveData.removeAt(0); + void _appendWaveSample(double value) { + final normalized = value.clamp(0.0, 1.0); + final eased = sqrt(normalized); + final data = waveformController.waveData; + data.add(eased); + if (data.length > _maxWaveSamples) { + data.removeRange(0, data.length - _maxWaveSamples); } - waveData.add(newAmplitude); - - // 重置当前帧数据 - _currentFrameAmplitude = 0.0; - _currentFrameSamples = 0; + waveformController.notifyListeners(); } /// 计算音频数据的振幅 @@ -436,55 +570,18 @@ class MeetingRecordController extends GetxController return (rms / 32768.0).clamp(0.0, 1.0); } - /// 启动数据更新控制器 - 替代Timer - void _startAudioFrameTimer() { - _audioFrameTimer?.cancel(); // 清理旧的Timer(如果有) - _audioFrameTimer = null; - - // 启动数据更新控制器 - _dataUpdateController.forward(); - } - - /// 停止数据更新控制器 - void _stopAudioFrameTimer() { - _audioFrameTimer?.cancel(); - _audioFrameTimer = null; - - // 停止数据更新控制器 - _dataUpdateController.stop(); - _dataUpdateController.reset(); - - // 重置当前帧数据和计数器 - _currentFrameAmplitude = 0.0; - _currentFrameSamples = 0; - _audioProcessCounter = 0; // 重置音频处理计数器 - } - - /// 启动波形流动动画 - void _startWaveformAnimation() { - if (isRecording.value && !waveformAnimationController.isAnimating) { - waveformAnimationController.forward(); - } - } - - /// 停止波形动画 - void _stopWaveformAnimation() { - waveformAnimationController.stop(); - waveformAnimationController.reset(); - } - - /// Saves recording and cleans up resources + /// 保存录音并清理相关资源 Future saveRecording() async { resetTimer(); - _stopWaveformAnimation(); // 停止波形动画 - _stopAudioFrameTimer(); // 停止音频帧定时器 + _stopWaveformTimer(); if (audioType.value != 0) { _bleManager.closeCodec(); } if (!isRecording.value) return; await _asrService.stopRecord(true); + await clearBackgroundRecordingIndicator(); - // Rename file if title was edited + // 如果标题被修改则重命名文件 if (fileName.value != newName.value) { final appDir = await appDocDir; final dir = Directory("${appDir.path}/MeetingAudio/"); @@ -506,9 +603,31 @@ class MeetingRecordController extends GetxController _resetToDefaultState(); isRecording.value = false; + rms.value = 0.0; + _smoothedRms = 0.0; + _latestRms = 0.0; + _resetWaveform(); + } + + Future discardRecording() async { + resetTimer(); + _stopWaveformTimer(); + if (audioType.value != 0) { + _bleManager.closeCodec(); + } + if (isRecording.value) { + await _asrService.stopRecord(false); + } + await clearBackgroundRecordingIndicator(); + _resetToDefaultState(); + isRecording.value = false; + rms.value = 0.0; + _smoothedRms = 0.0; + _latestRms = 0.0; + _resetWaveform(); } - /// Resets controller to initial state + /// 将控制器重置到初始状态 void _resetToDefaultState() { switch (audioType.value) { case 0: @@ -527,7 +646,7 @@ class MeetingRecordController extends GetxController isMarked.value = false; } - /// TIMER CONTROL SECTION /// + /// 计时器控制部分 /// /// 启动定时器 void startTimer() { @@ -549,7 +668,7 @@ class MeetingRecordController extends GetxController } } - /// Pauses active timer + /// 暂停当前计时器 void pauseTimer() { if (timerStatus.value == TimerStatus.running) { timerStatus.value = TimerStatus.paused; @@ -562,7 +681,7 @@ class MeetingRecordController extends GetxController } } - /// Resets timer to initial state + /// 将计时器重置到初始状态 void resetTimer() { _timer?.cancel(); _timer = null; @@ -571,15 +690,4 @@ class MeetingRecordController extends GetxController seconds.value = 0; timerStatus.value = TimerStatus.stopped; } - - /// UI HELPERS SECTION /// - - /// Generates mock waveform data for visualization - void generateWaveData() { - waveData.value = List.generate(120, (i) { - final rand = (i % 20) / 20; - final baseHeight = (sin(i / 5) * 0.5 + 0.5) * 25; - return baseHeight + rand * 10; - }); - } } diff --git a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart index e8268169d..ae64f372a 100644 --- a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart @@ -26,8 +26,10 @@ class _GenerateBottomSheetState extends State { final GetStorage _storage = GetStorage(); bool _isSpeaker = true; - String _languageText = 'chineseSimplified'.tr; // 对应中文:中文普通话(简体) - String _language = 'zh-CN'; + String _formlanguageText = 'chineseSimplified'.tr; // 对应中文:中文普通话(简体) + String _formlanguage = 'zh-CN'; + String _tolanguageText = 'chineseSimplified'.tr; // 对应中文:中文普通话(简体) + String _tolanguage = 'zh-CN'; final Map _icons = { 'auto_stories': Icons.auto_stories, @@ -273,11 +275,34 @@ class _GenerateBottomSheetState extends State { ), _row( 'recordingLanguage'.tr, // 对应中文:录音语言 - value: _languageText, + value: _formlanguageText, + onTap: () { + Get.bottomSheet( + RecordLanguageBottomSheet(_formlanguage), + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ).then((value) { + if (value != null) { + setState(() { + _formlanguageText = value['name']; + _formlanguage = value['value']; + }); + } + }); + }, + ), + _row( + 'targetLanguage'.tr, // 对应中文:目标语言 + value: _tolanguageText, isBorder: false, onTap: () { Get.bottomSheet( - RecordLanguageBottomSheet(_language), + RecordLanguageBottomSheet(_tolanguage), isScrollControlled: true, backgroundColor: Colors.grey[100], shape: RoundedRectangleBorder( @@ -288,8 +313,8 @@ class _GenerateBottomSheetState extends State { ).then((value) { if (value != null) { setState(() { - _languageText = value['name']; - _language = value['value']; + _tolanguageText = value['name']; + _tolanguage = value['value']; }); } }); @@ -318,16 +343,13 @@ class _GenerateBottomSheetState extends State { _storage.write("meeting_template_list", _templateList); Get.back(); if (widget.isSummarizeAgain) { - _controller.refreshSummary( - _templateData['prompt'], - _templateData['outline'], - ); + _controller.refreshSummary(_templateData['id']); } else { _controller.summaryCreate( + _formlanguage, + _tolanguage, _isSpeaker, - _language, - _templateData['prompt'], - _templateData['outline'], + _templateData['id'], ); } }, diff --git a/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart index 188ca945e..43335adfe 100644 --- a/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/more_bottom_sheet.dart @@ -26,7 +26,7 @@ class _MoreBottomSheetState extends State { children: [ SizedBox(height: 10.w), _shareItem(Icons.refresh, 'reSummarize'.tr, //重新总结 - onTap: _detailsController.meetingDetails.value.tasktype >= 3 + onTap: _detailsController.meetingDetails.value.tasktype >= 5 ? () { Get.back(); Get.bottomSheet( diff --git a/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart index 6700edd92..e41e7d0aa 100644 --- a/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart @@ -30,10 +30,12 @@ class NavigationBarBottomSheet extends GetView { Colors.red, onTap: () async { Get.back(); - await Get.toNamed(Routes.meetingRecord, arguments: { + final result = await Get.toNamed(Routes.meetingRecord, arguments: { 'audioTypes': 0, }); - controller.addMeetingAudio(); + if (result != 'meeting_record_background') { + controller.addMeetingAudio(); + } }, ), if (controller.meetingController.type == 1) @@ -45,10 +47,12 @@ class NavigationBarBottomSheet extends GetView { onTap: controller.bleManager.isConnected ? () async { Get.back(); - await Get.toNamed(Routes.meetingRecord, arguments: { + final result = await Get.toNamed(Routes.meetingRecord, arguments: { 'audioTypes': 1, }); - controller.addMeetingAudio(); + if (result != 'meeting_record_background') { + controller.addMeetingAudio(); + } } : null, ), @@ -61,10 +65,12 @@ class NavigationBarBottomSheet extends GetView { onTap: controller.bleManager.isConnected ? () async { Get.back(); - await Get.toNamed(Routes.meetingRecord, arguments: { + final result = await Get.toNamed(Routes.meetingRecord, arguments: { 'audioTypes': 2, }); - controller.addMeetingAudio(); + if (result != 'meeting_record_background') { + controller.addMeetingAudio(); + } } : null, ), diff --git a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart index be5f16eb9..08a76eebf 100644 --- a/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart @@ -13,7 +13,6 @@ import 'package:share_plus/share_plus.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import '../../../../core/utils/synchrodata.dart'; import '../../../../data/services/version_update_service.dart'; import '../../../../data/services/network/dio_manager.dart'; import '../../controllers/meeting_details_controller.dart'; @@ -86,17 +85,9 @@ class _ShareBottomSheetState extends State { isBorder: false, onTap: () async { Get.back(); - Synchrodata.uploadOperationRecords(); final GetStorage storage = GetStorage(); String token = storage.read("logintoken") ?? ''; - String userId = ''; - Map? userInfo = storage.read("user_info"); - if (userInfo != null) { - userId = userInfo['user']['uid']; - } - - String id = - '${userId}_${_controller.meetingData.value.id}'; + int id = _controller.meetingData.value.id; // final thumbnail = await getAssetThumbnailFile( // 'assets/images/mine-background.png', @@ -126,7 +117,7 @@ class _ShareBottomSheetState extends State { ]), _box([ _shareItem(Icons.copy, 'copyTranscript'.tr, // 对应中文:复制转写 - onTap: _controller.meetingDetails.value.tasktype >= 2 + onTap: _controller.meetingDetails.value.tasktype >= 3 ? () { Get.back(); _copy(_getAllText()); @@ -134,7 +125,7 @@ class _ShareBottomSheetState extends State { : null), _shareItem(Icons.copy, 'copySummary'.tr, // 对应中文:复制总结 isBorder: false, - onTap: _controller.meetingDetails.value.tasktype >= 3 + onTap: _controller.meetingDetails.value.tasktype >= 5 ? () { Get.back(); _copy(_markdownToPlainText( @@ -151,7 +142,7 @@ class _ShareBottomSheetState extends State { 'shareMeetingAudio'.tr); // 对应中文:分享会议音频 }), _shareItem(Icons.message, 'exportTranscript'.tr, // 对应中文:导出转写 - onTap: _controller.meetingDetails.value.tasktype >= 2 + onTap: _controller.meetingDetails.value.tasktype >= 3 ? () async { Get.back(); final tempDir = await getTemporaryDirectory(); @@ -164,7 +155,7 @@ class _ShareBottomSheetState extends State { } : null), _shareItem(Icons.article, 'exportSummary'.tr, // 对应中文:导出总结 - onTap: _controller.meetingDetails.value.tasktype >= 3 + onTap: _controller.meetingDetails.value.tasktype >= 5 ? () async { Get.back(); Get.bottomSheet( @@ -214,7 +205,7 @@ class _ShareBottomSheetState extends State { _shareItem( Icons.save_alt, '保存思维导图', - onTap: _controller.meetingDetails.value.tasktype >= 3 + onTap: _controller.meetingDetails.value.tasktype >= 5 ? () { Get.back(); final mindMapController = @@ -242,7 +233,7 @@ class _ShareBottomSheetState extends State { Icons.lan, 'shareMindMap'.tr, // 对应中文:分享思维导图 isBorder: false, - onTap: _controller.meetingDetails.value.tasktype >= 3 + onTap: _controller.meetingDetails.value.tasktype >= 5 ? () { Get.back(); final mindMapController = diff --git a/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart index ba1708b90..057b42ae2 100644 --- a/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/speaker_all_bottom_sheet.dart @@ -8,8 +8,8 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:lottie/lottie.dart'; -import '../../../../core/utils/synchrodata.dart'; import '../../../../data/services/db/sqflite_api.dart'; +import '../../../../data/services/network/api.dart'; import '../../controllers/meeting_details_controller.dart'; class SpeakerAllBottomSheet extends StatefulWidget { @@ -560,10 +560,11 @@ class _SpeakerAllBottomSheetState extends State { bool isEditSummary = false; String summaryText = _controller.meetingDetails.value.summary; _controller.textList.value = newTextList; - Synchrodata.saveOperationRecord('update', { - 'original': jsonEncode(_controller.textList), + Map modifyData = { 'id': _controller.meetingData.value.id, - }); + 'translate': jsonEncode(_controller.textList), + 'personnel': personnel, + }; for (var element in personnelList) { if (summaryText.contains('[${element['original']}]')) { summaryText = summaryText.replaceAll( @@ -576,11 +577,13 @@ class _SpeakerAllBottomSheetState extends State { 'summary': summaryText, 'personnel': personnel, }); + modifyData.addAll({'summary': summaryText}); _controller.meetingDetails.value.summary = summaryText; _controller.summaryNumberModifications.value++; } else { SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); } + await Api.modifyRecordEchomeet(modifyData); _controller.meetingDetails.value.personnel = personnel; _controller.personnelNumberModifications.value++; _controller.meetingDetails.refresh(); diff --git a/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart index a93f9fa31..a9483f856 100644 --- a/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/speaker_bottom_sheet.dart @@ -6,8 +6,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import '../../../../core/utils/synchrodata.dart'; import '../../../../data/services/db/sqflite_api.dart'; +import '../../../../data/services/network/api.dart'; import '../../controllers/meeting_details_controller.dart'; class SpeakerBottomSheet extends StatefulWidget { @@ -337,9 +337,9 @@ class _SpeakerBottomSheetState extends State { dataMap['speaker'] = _speakerController.text; _controller.textList[index] = dataMap; } - Synchrodata.saveOperationRecord('update', { - 'original': jsonEncode(_controller.textList), + await Api.modifyRecordEchomeet({ 'id': _controller.meetingData.value.id, + 'translate': jsonEncode(_controller.textList), }); _addHistorySpeaker(); Get.back(); @@ -371,10 +371,11 @@ class _SpeakerBottomSheetState extends State { var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList); if (result != null) { _controller.textList.value = newTextList; - Synchrodata.saveOperationRecord('update', { - 'original': jsonEncode(_controller.textList), + Map modifyData = { 'id': _controller.meetingData.value.id, - }); + 'translate': jsonEncode(_controller.textList), + 'personnel': personnel, + }; if (_controller.meetingDetails.value.summary .contains('[${widget.speakerName}]')) { String summaryText = _controller.meetingDetails.value.summary @@ -384,11 +385,13 @@ class _SpeakerBottomSheetState extends State { 'summary': summaryText, 'personnel': personnel, }); + modifyData.addAll({'summary': summaryText}); _controller.meetingDetails.value.summary = summaryText; _controller.summaryNumberModifications.value++; } else { SqfliteApi.editMeeting(_controller.id, {'personnel': personnel}); } + await Api.modifyRecordEchomeet(modifyData); _controller.meetingDetails.value.personnel = personnel; _addHistorySpeaker(); Get.back(); diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index cd21f85a5..1769e3045 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -331,35 +331,43 @@ class MeetingDetailsView extends GetView { Obx( () => controller.isLoading.value ? const SizedBox() - : controller.meetingDetails.value.tasktype == 1 || - controller.meetingDetails.value.tasktype == 2 - ? _meetingWait('analyzing'.tr) // 对应中文:正在分析... + : controller.meetingDetails.value.tasktype > 0 && + controller.meetingDetails.value.tasktype < 5 + ? _meetingWait( + controller.meetingDetails.value.tasktype > 0 && + controller.meetingDetails.value.tasktype < + 3 + ? 'transcribing'.tr // 对应中文:正在转写... + : 'analyzing'.tr) // 对应中文:正在分析... : const OverviewTab(), ), Obx( - () => controller.meetingDetails.value.tasktype == 1 + () => controller.meetingDetails.value.tasktype > 0 && + controller.meetingDetails.value.tasktype < 3 ? _meetingWait('transcribing'.tr) // 对应中文:正在转写... : const SpeechTab(), ), Obx( - () => controller.meetingDetails.value.tasktype == 1 || - controller.meetingDetails.value.tasktype == 2 + () => controller.meetingDetails.value.tasktype > 0 && + controller.meetingDetails.value.tasktype < 5 ? _meetingWait( - controller.meetingDetails.value.tasktype == 1 + controller.meetingDetails.value.tasktype > 0 && + controller.meetingDetails.value.tasktype < 3 ? 'transcribing'.tr // 对应中文:正在转写... - : 'summarizing'.tr) // 对应中文:正在总结... + : 'analyzing'.tr) // 对应中文:正在分析... : const SummaryTab(), ), - Obx( - () => controller.meetingDetails.value.tasktype >= 3 - ? const MindMapTab() - : controller.meetingDetails.value.tasktype == 0 - ? const SizedBox() - : _meetingWait( - controller.meetingDetails.value.tasktype == 1 - ? 'transcribing'.tr // 对应中文:正在转写... - : 'summarizing'.tr), // 对应中文:正在总结... - ), + Obx(() => controller.meetingDetails.value.tasktype >= 5 + ? const MindMapTab() + : controller.meetingDetails.value.tasktype == 0 + ? const SizedBox() + : _meetingWait(controller + .meetingDetails.value.tasktype > + 0 && + controller.meetingDetails.value.tasktype < 3 + ? 'transcribing'.tr // 对应中文:正在转写... + : 'analyzing'.tr) // 对应中文:正在分析... + ), ], ), ), diff --git a/lib/modules/meeting/views/meeting_home_view.dart b/lib/modules/meeting/views/meeting_home_view.dart index 36c5d610e..c15a3f6bf 100644 --- a/lib/modules/meeting/views/meeting_home_view.dart +++ b/lib/modules/meeting/views/meeting_home_view.dart @@ -247,6 +247,30 @@ class _MeetingHomeViewState extends State } Widget _item(BuildContext context, bool isDarkMode, MeetingModel data) { + String tasktypeText = ''; + switch (data.tasktype) { + case 0: + tasktypeText = ''; + break; + case 1: + tasktypeText = 'transcribing'.tr; // 对应中文:正在转写... + break; + case 2: + tasktypeText = 'transcribing'.tr; // 对应中文:正在转写... + break; + case 3: + tasktypeText = 'summarizing'.tr; // 对应中文:正在总结... + break; + case 4: + tasktypeText = 'summarizing'.tr; // 对应中文:正在总结... + break; + case 5: + tasktypeText = 'summaryComplete'.tr; // 对应中文:总结完成 + break; + default: + tasktypeText = ''; + } + return GestureDetector( onTap: () { Get.toNamed(Routes.meetingDetails, arguments: { @@ -374,29 +398,16 @@ class _MeetingHomeViewState extends State ) : const SizedBox(); }), - Obx(() { - Map taskData = - _controller.taskAggregator.taskFinishList.firstWhere( - (element) => element['id'] == data.id, - orElse: () => {}, - ); - return taskData.isNotEmpty - ? Padding( - padding: EdgeInsets.only(top: 5.w), - child: Text( - taskData['tasktype'] == 1 - ? 'transcribing'.tr // 对应中文:正在转写... - : taskData['tasktype'] == 2 - ? 'summarizing'.tr // 对应中文:正在总结... - : 'summaryComplete'.tr, // 对应中文:总结完成 - style: TextStyle( - color: Colors.blue, - fontSize: 10.sp, - ), - ), - ) - : const SizedBox(); - }), + Padding( + padding: EdgeInsets.only(top: 5.w), + child: Text( + tasktypeText, + style: TextStyle( + color: Colors.blue, + fontSize: 10.sp, + ), + ), + ), ], ), ], diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index d0128b749..b5a285ecd 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -1,24 +1,236 @@ -import 'dart:async'; -import 'dart:math' as math; +import 'dart:ui' as ui; +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/meeting_record_controller.dart'; import '../../../core/theme/app_colors.dart'; +enum _MeetingRecordExitAction { + saveAndEnd, + endWithoutSave, + continueInBackground, + cancel, +} + class MeetingRecordView extends GetView { const MeetingRecordView({super.key}); + Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) { + return Get.dialog<_MeetingRecordExitAction>( + Dialog( + insetPadding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, + child: Container( + width: 320.w, + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + color: isDarkMode ? AppColors.cardBackground : Colors.white, + borderRadius: BorderRadius.circular(16.r), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56.w, + height: 56.w, + decoration: BoxDecoration( + color: (isDarkMode ? Colors.white : Colors.blue) + .withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(28.r), + ), + child: Icon( + Icons.mic_rounded, + size: 28.sp, + color: isDarkMode ? Colors.white : Colors.blue, + ), + ), + SizedBox(height: 14.h), + Text( + 'meetingRecordExitTitle'.tr, // 对应中文:退出录音 + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 8.h), + Text( + 'meetingRecordExitMessage'.tr, // 对应中文:请选择离开后的操作 + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + height: 1.4, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + SizedBox(height: 18.h), + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: () => Get.back( + result: _MeetingRecordExitAction.saveAndEnd, + ), + style: ElevatedButton.styleFrom( + backgroundColor: + isDarkMode ? AppColors.primary : Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + ), + child: Text( + 'meetingRecordEndAndSave'.tr, // 对应中文:结束录音并保存 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + SizedBox(height: 10.h), + OutlinedButton( + onPressed: () => Get.back( + result: _MeetingRecordExitAction.continueInBackground, + ), + style: OutlinedButton.styleFrom( + foregroundColor: + isDarkMode ? Colors.white70 : Colors.grey[800], + side: BorderSide( + color: + isDarkMode ? Colors.white30 : Colors.grey[400]!, + ), + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text( + 'meetingRecordContinueInBackground'.tr, // 对应中文:后台继续录音 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(height: 10.h), + OutlinedButton( + onPressed: () => Get.back( + result: _MeetingRecordExitAction.endWithoutSave, + ), + style: OutlinedButton.styleFrom( + foregroundColor: + isDarkMode ? Colors.red[300] : Colors.red[600], + side: BorderSide( + color: + isDarkMode ? Colors.red[300]! : Colors.red[300]!, + ), + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + ), + child: Text( + 'meetingRecordEndWithoutSave'.tr, // 对应中文:结束录音不保存 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(height: 10.h), + TextButton( + onPressed: () => + Get.back(result: _MeetingRecordExitAction.cancel), + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + side: BorderSide( + color: + isDarkMode ? Colors.white24 : Colors.grey[300]!, + ), + ), + ), + child: Text( + 'cancel'.tr, // 对应中文:取消 + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white70 : Colors.grey[700], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + barrierDismissible: true, + ); + } + @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; + controller.applyRouteArgumentsIfIdle(); return PopScope( + canPop: false, onPopInvokedWithResult: (didPop, result) async { - if (didPop) { - // 在返回时执行保存操作 - //controller.saveTitleChange(); - //await controller.saveRecording(); + if (didPop) return; + + if (!controller.isRecording.value) { + Get.back(); + Future.microtask(() { + if (Get.isRegistered()) { + Get.delete(force: true); + } + }); + return; + } + + final action = await _showExitConfirmDialog(isDarkMode); + if (action == null || action == _MeetingRecordExitAction.cancel) { + return; + } + + switch (action) { + case _MeetingRecordExitAction.saveAndEnd: + await controller.saveTitleChange(); + await controller.saveRecording(); + Get.back(result: 'meeting_record_saved'); + Future.microtask(() { + if (Get.isRegistered()) { + Get.delete(force: true); + } + }); + return; + case _MeetingRecordExitAction.endWithoutSave: + await controller.discardRecording(); + Get.back(result: 'meeting_record_discarded'); + Future.microtask(() { + if (Get.isRegistered()) { + Get.delete(force: true); + } + }); + return; + case _MeetingRecordExitAction.continueInBackground: + await controller.saveTitleChange(); + await controller.showBackgroundRecordingIndicator(); + Get.back(result: 'meeting_record_background'); + return; + case _MeetingRecordExitAction.cancel: + return; } }, child: Scaffold( @@ -29,12 +241,7 @@ class MeetingRecordView extends GetView { color: isDarkMode ? Colors.white : Colors.black, size: 20.sp, ), - onPressed: () async { - // 在返回按钮点击时执行保存操作 - controller.saveTitleChange(); - await controller.saveRecording(); - Get.back(); - }, + onPressed: () => Navigator.of(context).maybePop(), ), title: Obx(() { if (controller.isEditingTitle.value) { @@ -108,16 +315,21 @@ class MeetingRecordView extends GetView { // 使用LayoutBuilder获取准确的屏幕宽度 final screenWidth = constraints.maxWidth; - // 初始化最大可见波形条数量 - controller.setMaxVisibleBars(screenWidth); - return Column( children: [ - SizedBox(height: 24.h), + SizedBox(height: 16.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Align( + alignment: Alignment.centerLeft, + child: _buildRecordTypeBadge(isDarkMode), + ), + ), + SizedBox(height: 12.h), // 实时流动波形显示区域 Container( height: 200.h, - margin: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.zero, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -155,12 +367,49 @@ class MeetingRecordView extends GetView { borderRadius: BorderRadius.circular(12.r), child: Stack( children: [ - // 实时流动波形绘制组件 - _WaveformWidget( - controller: controller, - isDarkMode: isDarkMode, - size: Size(screenWidth - 32.w, 200.h), - ), + Obx(() { + final size = Size(screenWidth, 200.h); + final waveColor = + isDarkMode ? Colors.white : Colors.black87; + final shader = ui.Gradient.linear( + const Offset(0, 0), + Offset(size.width, 0), + [ + waveColor.withValues(alpha: 0.0), + waveColor.withValues(alpha: 0.25), + waveColor.withValues(alpha: 0.6), + waveColor.withValues(alpha: 0.95), + ], + const [0.0, 0.55, 0.8, 1.0], + ); + + return Center( + child: AudioWaveforms( + size: size, + recorderController: controller.waveformController, + backgroundColor: Colors.transparent, + waveStyle: WaveStyle( + waveThickness: 1.w, + spacing: 2.w, + waveCap: StrokeCap.round, + showMiddleLine: true, + middleLineColor: Colors.redAccent.withValues( + alpha: controller.timerStatus.value == + TimerStatus.running + ? 0.9 + : 0.0, + ), + middleLineThickness: 1.w, + showTop: true, + showBottom: true, + extendWaveform: true, + // gradient: shader, + waveColor: waveColor, + scaleFactor: 90, + ), + ), + ); + }), // 录音状态指示器 Obx(() { @@ -217,7 +466,7 @@ class MeetingRecordView extends GetView { fontWeight: FontWeight.normal, color: isDarkMode ? Colors.white : Colors.black87, fontFeatures: const [ - FontFeature.tabularFigures() + ui.FontFeature.tabularFigures() ], // 使用等宽数字,避免跳动 ), )), @@ -250,6 +499,7 @@ class MeetingRecordView extends GetView { tooltip: controller.isMarked.value ? 'unmarkFavorite'.tr : 'markFavorite'.tr, + isDarkMode: isDarkMode, ), // 录音按钮 _buildActionButton( @@ -265,6 +515,7 @@ class MeetingRecordView extends GetView { tooltip: controller.timerStatus.value == TimerStatus.running ? 'pauseRecording'.tr : 'startRecording'.tr, + isDarkMode: isDarkMode, ), // 完成按钮 _buildActionButton( @@ -276,6 +527,7 @@ class MeetingRecordView extends GetView { ), radius: 32.w, tooltip: 'finishRecording'.tr, + isDarkMode: isDarkMode, ), ], ), @@ -288,11 +540,14 @@ class MeetingRecordView extends GetView { required Widget child, required double radius, required String tooltip, + required bool isDarkMode, }) { + final Color bgColor = isDarkMode ? const Color(0xFF1E1E1E) : Colors.white; + return Tooltip( message: tooltip, child: Material( - color: Colors.white, + color: bgColor, shape: const CircleBorder(), elevation: 4, child: InkWell( @@ -300,7 +555,7 @@ class MeetingRecordView extends GetView { customBorder: const CircleBorder(), child: CircleAvatar( radius: radius, - backgroundColor: Colors.white, + backgroundColor: bgColor, child: child, ), ), @@ -308,8 +563,67 @@ class MeetingRecordView extends GetView { ); } + Widget _buildRecordTypeBadge(bool isDarkMode) { + return Obx(() { + String label; + IconData icon; + Color color; + + switch (controller.audioType.value) { + case 0: + label = 'liveRecording'.tr; + icon = Icons.mic_rounded; + color = Colors.red; + break; + case 1: + label = 'audioVideoRecording'.tr; + icon = Icons.videocam_rounded; + color = Colors.green; + break; + case 2: + default: + label = 'callRecording'.tr; + icon = Icons.phone_in_talk_rounded; + color = Colors.orange; + break; + } + + return Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h), + decoration: BoxDecoration( + color: color.withValues(alpha: isDarkMode ? 0.25 : 0.12), + borderRadius: BorderRadius.circular(999.r), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 18.sp, + color: isDarkMode ? Colors.white : color, + ), + SizedBox(width: 6.w), + Text( + label, + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + ], + ), + ); + }); + } + // 底部编辑弹窗 - void _bottomSheet(BuildContext context, bool isDarkMode) { + void _bottomSheet(BuildContext context, bool isDarkMode) async { + //如果当前计时器在运行,先暂停录音,避免编辑时继续录音 + if (controller.timerStatus.value == TimerStatus.running) { + await controller.toggleRecording(); + } + Get.bottomSheet( Container( padding: EdgeInsets.only( @@ -450,218 +764,3 @@ class MeetingRecordView extends GetView { ); } } - -/// 简化的实时流动波形绘制器 -class RealTimeWaveformPainter extends CustomPainter { - final List waveData; - final bool isDarkMode; - final bool isRecording; - final double animationValue; // 动画值,用于流动效果 - - RealTimeWaveformPainter({ - required this.waveData, - required this.isDarkMode, - required this.isRecording, - this.animationValue = 0.0, - }); - - @override - void paint(Canvas canvas, Size size) { - final centerY = size.height / 2; - - // 绘制中央基准线 - _drawCenterLine(canvas, size, centerY); - - // 只在录音状态下且有波形数据时才绘制波形 - if (isRecording && waveData.isNotEmpty) { - _drawWaveform(canvas, size, centerY); - } - } - - /// 绘制中央基准线 - void _drawCenterLine(Canvas canvas, Size size, double centerY) { - final linePaint = Paint() - ..color = - (isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15) - ..strokeWidth = 1.0; - - canvas.drawLine( - Offset(0, centerY), - Offset(size.width, centerY), - linePaint, - ); - } - - /// 绘制简化的流动波形 - void _drawWaveform(Canvas canvas, Size size, double centerY) { - const double barWidth = 3.0; // 波形条宽度 - const double barSpacing = 1.0; // 波形条间距 - const double totalBarWidth = barWidth + barSpacing; - - // 计算可显示的波形条数量 - final maxBars = (size.width / totalBarWidth).floor(); - final visibleBars = waveData.length > maxBars ? maxBars : waveData.length; - - // 计算流动偏移量(从右向左移动) - final flowOffset = animationValue * totalBarWidth; - - // 从右到左绘制波形条 - for (int i = 0; i < visibleBars; i++) { - final dataIndex = waveData.length - 1 - i; - final amplitude = waveData[dataIndex]; - - // 计算波形条的x位置(从右侧开始,向左排列) - final x = size.width - (i + 1) * totalBarWidth + flowOffset; - - // 跳过屏幕外的波形条 - if (x < -barWidth || x > size.width) continue; - - _drawWaveBar( - canvas, x, centerY, amplitude, i.toDouble(), visibleBars.toDouble()); - } - } - - /// 绘制单个波形条 - void _drawWaveBar(Canvas canvas, double x, double centerY, double amplitude, - double index, double totalBars) { - const double barWidth = 2.0; - const double maxHeight = 200.0; - const double minHeight = 4.0; // 最小基础高度 - const double containerPadding = 10.0; // 容器内边距,防止超出边界 - - // 计算可用的最大高度(考虑容器边界) - final availableHeight = (centerY - containerPadding) * 2; // 上下对称 - final effectiveMaxHeight = math.min(maxHeight, availableHeight); - - // 优化波形条高度计算:确保有基础高度 + 振幅变化 - final normalizedAmplitude = (amplitude / 100.0).clamp(0.0, 1.0); - final dynamicHeight = - normalizedAmplitude * (effectiveMaxHeight - minHeight); - final barHeight = minHeight + dynamicHeight; - - // 计算透明度(越新的波形条越不透明) - //final opacity = (1.0 - (index / totalBars) * 0.6).clamp(0.3, 1.0); - - // 创建波形条矩形 - final rect = RRect.fromRectAndRadius( - Rect.fromCenter( - center: Offset(x + barWidth / 2, centerY), - width: barWidth, - height: barHeight, // 使用优化后的高度 - ), - const Radius.circular(1.5), - ); - - // 创建渐变颜色 - final Color baseColor = isDarkMode - ? Colors.grey[500]! // 深灰色 - : Colors.grey[600]!; // 更深灰色 - - final paint = Paint() - ..color = baseColor - ..style = PaintingStyle.fill; - - canvas.drawRRect(rect, paint); - } - - @override - bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) { - // 只有在以下情况才重绘 - if (oldDelegate.isDarkMode != isDarkMode || - oldDelegate.isRecording != isRecording) { - return true; - } - - // 数据长度变化才重绘 - if (oldDelegate.waveData.length != waveData.length) { - return true; - } - - // 数据内容变化(比较最后一个值) - if (waveData.isNotEmpty && - oldDelegate.waveData.isNotEmpty && - (waveData.last - oldDelegate.waveData.last).abs() > 0.1) { - return true; - } - - // 动画值每 0.1 变化才重绘(避免每帧都重绘) - return (animationValue - oldDelegate.animationValue).abs() > 0.1; - } -} - -/// 优化的波形Widget,避免双重响应式更新 -class _WaveformWidget extends StatefulWidget { - final MeetingRecordController controller; - final bool isDarkMode; - final Size size; - - const _WaveformWidget({ - required this.controller, - required this.isDarkMode, - required this.size, - }); - - @override - State<_WaveformWidget> createState() => _WaveformWidgetState(); -} - -class _WaveformWidgetState extends State<_WaveformWidget> { - late List _waveData; - late bool _isRecording; - StreamSubscription? _waveDataSubscription; - StreamSubscription? _isRecordingSubscription; - - @override - void initState() { - super.initState(); - _waveData = List.from(widget.controller.waveData); - _isRecording = widget.controller.isRecording.value; - - // 监听 waveData 变化 - _waveDataSubscription = widget.controller.waveData.listen((data) { - if (mounted) { - setState(() { - _waveData = List.from(data); - }); - } - }); - - // 监听 isRecording 变化 - _isRecordingSubscription = widget.controller.isRecording.listen((value) { - if (mounted) { - setState(() { - _isRecording = value; - }); - } - }); - } - - @override - void dispose() { - _waveDataSubscription?.cancel(); - _isRecordingSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: AnimatedBuilder( - animation: widget.controller.waveformAnimation, - builder: (context, child) { - return CustomPaint( - size: widget.size, - painter: RealTimeWaveformPainter( - waveData: _waveData, - isDarkMode: widget.isDarkMode, - isRecording: _isRecording, - animationValue: widget.controller.waveformAnimation.value, - ), - isComplex: true, // 告诉 Flutter 这个绘制较复杂,可缓存 - willChange: false, // 数据不会频繁变化(我们已优化) - ); - }, - ), - ); - } -} diff --git a/lib/modules/meeting/views/tabs/summary_tab.dart b/lib/modules/meeting/views/tabs/summary_tab.dart index 5863fe14b..995e18d42 100644 --- a/lib/modules/meeting/views/tabs/summary_tab.dart +++ b/lib/modules/meeting/views/tabs/summary_tab.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import 'package:parchment/codecs.dart'; import '../../../../data/services/db/sqflite_api.dart'; +import '../../../../data/services/network/api.dart'; import '../../controllers/meeting_details_controller.dart'; import '../bottomSheet/ai_bottom_sheet.dart'; import '../bottomSheet/generate_bottom_sheet.dart'; @@ -133,6 +134,10 @@ class _SummaryTabState extends State { final markdown = const ParchmentMarkdownCodec().encode(_fleatherController!.document); if (markdown != _controller.meetingDetails.value.summary) { + Api.modifyRecordEchomeet({ + 'id': _controller.id, + 'summary': markdown, + }); SqfliteApi.editMeeting( _controller.id, {'summary': markdown}, @@ -272,6 +277,12 @@ class _SummaryTabState extends State { isEdit = true; } if (isEdit) { + Api.modifyRecordEchomeet({ + 'id': _controller.id, + 'address': _controller.meetingDetails.value.address, + 'personnel': + _controller.meetingDetails.value.personnel, + }); SqfliteApi.editMeeting( _controller.id, { diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index eabfbbadb..e947e795f 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -10,9 +10,6 @@ import 'package:http/http.dart' as http; import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig_model.dart'; import '../../../data/models/user_Info.dart'; -import '../../../data/services/meeting/meeting_task_service.dart'; -import '../../../data/services/meeting/meeting_upload_service.dart'; -import '../../../data/services/meeting/summary_task_service.dart'; import '../../../routes/app_routes.dart'; import '../../../data/services/network/api.dart'; import '../../../data/services/version_update_service.dart'; @@ -269,13 +266,8 @@ class SplashController extends GetxController { final isAppConfigInitialized = AppConfig.isInitialized(); // 检查登录状态和配置初始化状态 if (token != null && token.isNotEmpty && isAppConfigInitialized) { - Get.find(); - Get.find(); - Get.find(); Get.offAllNamed(Routes.pairing); } else { - _storage.remove('meeting_task_list'); - _storage.remove('meeting_task_finish_list'); _storage.remove('meeting_upload_list'); _storage.remove('operation_records'); _storage.remove('operation_delids'); diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index c9b6ec60c..de6142ab3 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -237,6 +237,7 @@ abstract class AppPages { name: Routes.meetingRecord, page: () => const MeetingRecordView(), binding: MeetingRecordBinding(), + popGesture: false, transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 26e9cf2b9..5c6415e49 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -413,7 +413,7 @@ private var companionManager: BleCompanionManager? = null // 设置服务状态 isRunning.set(true) startForeground(NOTIFICATION_ID, createNotification()) - // 初始化BLE伴侣设备管理器 - 使用单例模式获取实例 + // 初始化BLE伴侣设备管理器 - 使用单例模式获取实例 companionManager = BleCompanionManager.getInstance(this) // 初始化蓝牙监听器 initializeBluetoothListener() @@ -850,12 +850,20 @@ private var companionManager: BleCompanionManager? = null // Android 8.0+ 需要创建通知渠道 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - "语音助手代理服务", - NotificationManager.IMPORTANCE_LOW - ) - manager.createNotificationChannel(channel) + val existingChannel = manager.getNotificationChannel(CHANNEL_ID) + if (existingChannel == null) { + val channel = NotificationChannel( + CHANNEL_ID, + "Voice assistant agent", + NotificationManager.IMPORTANCE_LOW + ) + channel.description = "Voice assistant agent" + channel.setShowBadge(false) + channel.enableLights(false) + channel.enableVibration(false) + channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + manager.createNotificationChannel(channel) + } } // 点击通知打开主界面 @@ -873,15 +881,22 @@ private var companionManager: BleCompanionManager? = null null } + val appIconResId = applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info + // 构建通知 val builder = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Voitrans") - .setContentText("语音助手代理服务运行中") - .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentText("Voice assistant agent is running") + .setSmallIcon(appIconResId) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setShowWhen(false) // 设置点击意图 pendingIntent?.let { builder.setContentIntent(it) } return builder.build() } -} \ No newline at end of file +} diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt index b8e5e3056..070b042ed 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt @@ -80,6 +80,36 @@ class IntegratedSpeechTranslationService( // 语音映射缓存 private val voiceCache = ConcurrentHashMap() + + + // TTS合成中的音频缓存 + private var ttsdata: ByteArray = ByteArray(0) + + /** + * 处理合成音频分片(按1280字节对齐) + * 输入的音频数据会追加到缓存;仅当累计长度达到`multiple`的整数倍时, + * 将该整数倍长度的部分通过`onSynthesisAudioGenerated`发送;余数保留在缓存等待下次。 + */ + private fun processSynthesisAudioChunk( + text: String, + incoming: ByteArray, + multiple: Int = 1280 + ) { + if (incoming.isNotEmpty()) { + ttsdata = ttsdata + incoming + } + + val sendLen = (ttsdata.size / multiple) * multiple + if (sendLen > 0) { + val chunk = ttsdata.copyOfRange(0, sendLen) + Log.d(TAG, "按${multiple}字节对齐发送音频: ${chunk.size} 字节,缓存剩余: ${ttsdata.size - sendLen}") + eventCallback?.onSynthesisAudioGenerated(text, chunk) + ttsdata = ttsdata.copyOfRange(sendLen, ttsdata.size) + } else { + Log.d(TAG, "暂未够${multiple}字节,当前缓存: ${ttsdata.size}") + } + } + /** * 服务配置类 */ @@ -399,6 +429,7 @@ class IntegratedSpeechTranslationService( // 合成开始事件 SynthesisStarted.addEventListener { _, _ -> Log.d(TAG, "合成开始事件") + ttsdata = ByteArray(0) serviceState.isSynthesizing.set(true) eventCallback?.onStateChanged("Synthesis", true) } @@ -406,9 +437,11 @@ class IntegratedSpeechTranslationService( // 合成进行中事件 Synthesizing.addEventListener { _, event -> Log.d(TAG, "合成进行中事件") - // 计算进度(简化版) - val progress = 0.5f // 实际应用中可以根据音频数据计算 + // 计算进度(简化版) + val progress = 0.5f // 实际应用中可以根据音频数据计算 eventCallback?.onSynthesisProgress("", progress) + val incoming = event.result.audioData ?: ByteArray(0) + processSynthesisAudioChunk("", incoming, 1280) } // 合成完成事件 @@ -421,6 +454,7 @@ class IntegratedSpeechTranslationService( // 合成取消事件 SynthesisCanceled.addEventListener { _, event -> Log.d(TAG, "合成取消事件") + ttsdata = ByteArray(0) serviceState.isSynthesizing.set(false) val reason = event.result.reason eventCallback?.onError("Synthesis", "语音合成取消: $reason") @@ -605,6 +639,7 @@ class IntegratedSpeechTranslationService( try { Log.d(TAG, "开始语音合成: $text") serviceState.isSynthesizing.set(true) + eventCallback?.onSynthesisStarted(text) val ssml = generateOptimizedSsml(text) @@ -618,12 +653,13 @@ class IntegratedSpeechTranslationService( // 获取音频数据 val audioData = result.audioData - if (audioData != null && audioData.isNotEmpty()) { - // 通过回调返回音频数据 - eventCallback?.onSynthesisAudioGenerated(text, audioData) - Log.d(TAG, "音频数据大小: ${audioData.size} 字节") - } - + // if (audioData != null && audioData.isNotEmpty()) { + // // 通过回调返回音频数据 + // eventCallback?.onSynthesisAudioGenerated(text, audioData) + // Log.d(TAG, "音频数据大小: ${audioData.size} 字节") + // } + eventCallback?.onSynthesisAudioGenerated(text, ttsdata) + ttsdata = ByteArray(0) return audioData } else { Log.e(TAG, "语音合成失败: ${result?.reason}") @@ -1733,4 +1769,4 @@ class VolcanoTranslationServiceImpl : Log.e(TAG, "清理资源失败", e) } } -} \ No newline at end of file +} diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index abc20de31..ff536ae4a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -5,6 +5,7 @@ import android.os.Handler import android.os.Looper import androidx.annotation.NonNull import com.yunqiinnovation.azure_speech.utils.FileLogger +import com.yunqiinnovation.azure_speech.tools.AudioRecordingForegroundService import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -581,6 +582,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { // 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调 } FileLogger.d(tag, "音频文件名称为: ${filePath}") + + try { + AudioRecordingForegroundService.startService(context) + } catch (e: Exception) { + FileLogger.w(tag, "启动录音前台服务失败: ${e.message}") + } azureAsrHelper.enableRecord(audioSourceType, filePath, if(acceptAudioData) { // 创建音频数据回调,将音频数据发送到 Flutter 层 @@ -631,12 +638,20 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { try { if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){ isRecord = false - recordfile?.closeFile(true) + recordfile?.closeFile(isSave) } else { azureAsrHelper.stopRecord(isSave) } + + if (!azureAsrHelper.isContinuousRecognitionActive()) { + try { + AudioRecordingForegroundService.stopService(context) + } catch (e: Exception) { + FileLogger.w(tag, "停止录音前台服务失败: ${e.message}") + } + } result.success(true) } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null) @@ -1027,10 +1042,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { override fun onSynthesisAudioGenerated(text: String, audioData: ByteArray) { FileLogger.d(tag, "语音合成音频生成,文本=${text},音频数据大小=${audioData.size}") // Azure TTS返回的是RIFF WAV格式,需要去除文件头 - val pcmData = extractPcmFromWav(audioData) - if (pcmData.isNotEmpty()) { - FileLogger.d(tag, "提取PCM数据,大小=${pcmData.size}字节") - BleService.writeExternalAudioData(pcmData) + //val pcmData = extractPcmFromWav(audioData) + if (audioData.isNotEmpty()) { + FileLogger.d(tag, "提取PCM数据,大小=${audioData.size}字节") + BleService.writeExternalAudioData(audioData) } else { FileLogger.e(tag, "提取PCM数据失败,音频数据可能不是WAV格式") diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt index 0b1d1b1fb..620c08dda 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/AudioRecordingForegroundService.kt @@ -113,8 +113,10 @@ class AudioRecordingForegroundService : Service() { * @return 通知对象 */ private fun createNotification(): Notification { - // 创建点击通知时的意图(可以打开应用主界面) - val intent = packageManager.getLaunchIntentForPackage(packageName) + val intent = (packageManager.getLaunchIntentForPackage(packageName) ?: Intent()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + putExtra("com.saitong.voitrans.extra.NAVIGATE_TO", "meetingRecord") + } val pendingIntent = PendingIntent.getActivity( this, 0, @@ -123,14 +125,15 @@ class AudioRecordingForegroundService : Service() { ) return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("语音助手正在运行") - .setContentText("正在录制音频,点击返回应用") - .setSmallIcon(android.R.drawable.ic_btn_speak_now) // 使用系统默认麦克风图标 + .setContentTitle("Voitrans") + .setContentText("Recording audio, tap to return to app") + .setSmallIcon(applicationInfo.icon) .setContentIntent(pendingIntent) .setOngoing(true) // 设置为持续通知,用户无法滑动删除 .setPriority(NotificationCompat.PRIORITY_LOW) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setShowWhen(false) + .setAutoCancel(false) // 点击通知后不自动取消 .build() } -} \ No newline at end of file +} diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index a3c9cc9b3..0c065cedf 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -1039,10 +1039,10 @@ private class AstEventCallback: IntegratedSpeechTranslationService.ServiceEventC os_log("语音合成音频生成,文本=%@,音频数据大小=%d", log: plugin.log, type: .info, text, audioData.count) // Azure TTS返回的是RIFF WAV格式,需要去除文件头提取PCM数据 - let pcmData = plugin.extractPcmFromWav(audioData) - if !pcmData.isEmpty { - os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, pcmData.count) - BleService.shared.writeExternalAudioData(data: pcmData) + // let pcmData = plugin.extractPcmFromWav(audioData) + if !audioData.isEmpty { + os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, audioData.count) + BleService.shared.writeExternalAudioData(data: audioData) } else { os_log("提取PCM数据失败,音频数据可能不是WAV格式", log: plugin.log, type: .error) } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift index 97db886f4..a404cf89a 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift @@ -66,6 +66,9 @@ import os.log // 处理队列 private let processingQueue = DispatchQueue(label: "com.azure.speech.processing", qos: .userInitiated) + // 合成中的音频缓存 + private var ttsBuffer = Data() + // MARK: - Configuration Structures /** @@ -478,6 +481,7 @@ import os.log // 合成开始事件 synthesizer?.addSynthesisStartedEventHandler { [weak self] (synthesizer, event) in guard let self = self else { return } + self.ttsBuffer = Data() os_log("合成开始事件", log: self.log, type: .info) self.serviceState.isSynthesizing = true @@ -487,7 +491,7 @@ import os.log } } - // 合成进行中事件 - 修改为返回音频数据 + // 合成进行中事件 - 增量发送音频分片(按1280字节对齐) synthesizer?.addSynthesizingEventHandler { [weak self] (synthesizer, event) in guard let self = self else { return } // 安全解包audioData @@ -495,8 +499,8 @@ import os.log os_log("合成进行中事件,音频数据为空", log: self.log, type: .debug) return } - os_log("合成进行中事件,音频数据长度: %d bytes", log: self.log, type: .debug, audioData.count) - + self.processSynthesisAudioChunk(text: "", incoming: audioData, multiple: 1280) + os_log("合成进行中事件,累计缓存长度: %d bytes", log: self.log, type: .debug, self.ttsBuffer.count) } // 合成完成事件 @@ -505,16 +509,14 @@ import os.log os_log("合成完成事件", log: self.log, type: .info) // 安全解包audioData - guard let audioData = event.result.audioData else { - os_log("合成完成事件,音频数据为空", log: self.log, type: .debug) - return - } - os_log("合成完成事件,音频数据长度: %d bytes", log: self.log, type: .debug, audioData.count) + + // 发送缓存中的剩余数据(可能非1280对齐的余量) + let remaining = self.ttsBuffer + self.ttsBuffer = Data() DispatchQueue.main.async { - self.serviceState.isSynthesizing = false - // 立即返回当前生成的音频数据 - self.eventCallback?.onSynthesisAudioGenerated(text: "", audioData: audioData) - self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) + self.serviceState.isSynthesizing = false + self.eventCallback?.onSynthesisAudioGenerated(text: "", audioData: remaining) + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) } } @@ -525,6 +527,8 @@ import os.log os_log("合成取消事件", log: self.log, type: .info) self.serviceState.isSynthesizing = false + // 取消时清空缓存,避免脏数据 + self.ttsBuffer = Data() // 修复:移除可选链,因为event.result不是可选类型 let reason = event.result.reason @@ -542,6 +546,32 @@ import os.log } } + /** + * 处理合成音频分片(按multiple字节对齐,默认1280) + * - 参数 text: 正在合成的文本 + * - 参数 incoming: 新到达的音频数据块 + * - 参数 multiple: 对齐字节倍数(例如16000Hz/16bit/mono下常用1280) + * 将incoming追加到缓存,仅当累计长度达到multiple的整数倍时, + * 发送对齐部分;余数继续保留在缓存,等待后续数据到来或合成完成时一次性发送。 + */ + private func processSynthesisAudioChunk(text: String, incoming: Data?, multiple: Int = 1280) { + guard let incoming = incoming, !incoming.isEmpty else { return } + // 追加到缓存 + ttsBuffer.append(incoming) + // 计算可发送的对齐长度 + let sendLen = (ttsBuffer.count / multiple) * multiple + if sendLen > 0 { + let chunk = ttsBuffer.prefix(sendLen) + // 发送对齐部分 + let toSend = Data(chunk) + DispatchQueue.main.async { [weak self] in + self?.eventCallback?.onSynthesisAudioGenerated(text: text, audioData: toSend) + } + // 保留余数 + ttsBuffer = Data(ttsBuffer.suffix(ttsBuffer.count - sendLen)) + } + } + // MARK: - Public Methods /** diff --git a/local_plugins/native_plugin/android/src/main/AndroidManifest.xml b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml index 5153b5dc6..1acdbb46b 100644 --- a/local_plugins/native_plugin/android/src/main/AndroidManifest.xml +++ b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml @@ -1,3 +1,11 @@ + + + + + + diff --git a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java index 265796e9d..d6ebf3a68 100644 --- a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java +++ b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java @@ -2,37 +2,123 @@ package com.example.native_plugin; import androidx.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; + import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; /** NativePlugin */ -public class NativePlugin implements FlutterPlugin, MethodCallHandler { +public class NativePlugin implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private MethodChannel channel; + private EventChannel recordingFloatingActionsChannel; + private Context applicationContext; + + private static final Handler mainHandler = new Handler(Looper.getMainLooper()); + private static volatile EventChannel.EventSink recordingFloatingActionSink; @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + applicationContext = flutterPluginBinding.getApplicationContext(); channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin"); channel.setMethodCallHandler(this); + + recordingFloatingActionsChannel = + new EventChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin/recording_floating_actions"); + recordingFloatingActionsChannel.setStreamHandler(this); } @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { if (call.method.equals("getPlatformVersion")) { result.success("Android " + android.os.Build.VERSION.RELEASE); + } else if (call.method.equals("showRecordingFloatingBar")) { + String duration = call.argument("duration"); + String recordingType = call.argument("recordingType"); + Boolean isPaused = call.argument("isPaused"); + startRecordingFloatingService( + RecordingFloatingService.ACTION_SHOW, + duration, + recordingType, + isPaused != null && isPaused + ); + result.success(true); + } else if (call.method.equals("updateRecordingFloatingBar")) { + String duration = call.argument("duration"); + String recordingType = call.argument("recordingType"); + Boolean isPaused = call.argument("isPaused"); + startRecordingFloatingService( + RecordingFloatingService.ACTION_UPDATE, + duration, + recordingType, + isPaused != null && isPaused + ); + result.success(true); + } else if (call.method.equals("hideRecordingFloatingBar")) { + startRecordingFloatingService(RecordingFloatingService.ACTION_HIDE, null, null, false); + result.success(true); } else { result.notImplemented(); } } + private void startRecordingFloatingService( + String action, + String duration, + String recordingType, + boolean isPaused + ) { + if (applicationContext == null) return; + Intent intent = new Intent(applicationContext, RecordingFloatingService.class); + intent.setAction(action); + if (duration != null) { + intent.putExtra(RecordingFloatingService.EXTRA_DURATION, duration); + } + if (recordingType != null) { + intent.putExtra(RecordingFloatingService.EXTRA_RECORDING_TYPE, recordingType); + } + intent.putExtra(RecordingFloatingService.EXTRA_IS_PAUSED, isPaused); + applicationContext.startService(intent); + } + + static void emitRecordingFloatingAction(String action) { + final EventChannel.EventSink sink = recordingFloatingActionSink; + if (sink == null) return; + mainHandler.post(() -> { + try { + sink.success(action); + } catch (Exception ignored) { + } + }); + } + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + recordingFloatingActionSink = events; + } + + @Override + public void onCancel(Object arguments) { + recordingFloatingActionSink = null; + } + @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); + if (recordingFloatingActionsChannel != null) { + recordingFloatingActionsChannel.setStreamHandler(null); + } + recordingFloatingActionsChannel = null; + applicationContext = null; } } diff --git a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java new file mode 100644 index 000000000..435786212 --- /dev/null +++ b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/RecordingFloatingService.java @@ -0,0 +1,292 @@ +package com.example.native_plugin; + +import android.app.Service; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.os.IBinder; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +public class RecordingFloatingService extends Service { + static final String ACTION_SHOW = "com.example.native_plugin.action.SHOW_RECORDING_FLOATING_BAR"; + static final String ACTION_UPDATE = "com.example.native_plugin.action.UPDATE_RECORDING_FLOATING_BAR"; + static final String ACTION_HIDE = "com.example.native_plugin.action.HIDE_RECORDING_FLOATING_BAR"; + + static final String EXTRA_DURATION = "duration"; + static final String EXTRA_RECORDING_TYPE = "recordingType"; + static final String EXTRA_IS_PAUSED = "isPaused"; + + private WindowManager windowManager; + private View floatingView; + private WindowManager.LayoutParams layoutParams; + private TextView durationTextView; + private TextView typeTextView; + private TextView playPauseTextView; + private boolean isPaused = false; + + @Override + public void onCreate() { + super.onCreate(); + windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent == null) { + return START_NOT_STICKY; + } + + String action = intent.getAction(); + if (ACTION_HIDE.equals(action)) { + removeFloatingView(); + stopSelf(); + return START_NOT_STICKY; + } + + if (ACTION_SHOW.equals(action) || ACTION_UPDATE.equals(action)) { + String duration = intent.getStringExtra(EXTRA_DURATION); + String recordingType = intent.getStringExtra(EXTRA_RECORDING_TYPE); + boolean paused = intent.getBooleanExtra(EXTRA_IS_PAUSED, false); + showOrUpdate(duration, recordingType, paused); + } + + return START_STICKY; + } + + private void showOrUpdate(String duration, String recordingType, boolean paused) { + if (floatingView == null) { + createFloatingView(); + addFloatingView(); + } + updateContent(duration, recordingType, paused); + } + + private void updateContent(String duration, String recordingType, boolean paused) { + isPaused = paused; + updatePlayPauseIcon(); + if (durationTextView != null) { + durationTextView.setText(duration == null ? "" : duration); + } + if (typeTextView != null) { + String type = recordingType == null ? "" : recordingType.trim(); + if (type.isEmpty()) { + typeTextView.setVisibility(View.GONE); + } else { + typeTextView.setVisibility(View.VISIBLE); + typeTextView.setText(type); + } + } + } + + private void updatePlayPauseIcon() { + if (playPauseTextView == null) return; + playPauseTextView.setText(isPaused ? "▶" : "⏸"); + } + + private void createFloatingView() { + float density = getResources().getDisplayMetrics().density; + int touchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); + + LinearLayout container = new LinearLayout(this); + container.setOrientation(LinearLayout.HORIZONTAL); + container.setGravity(Gravity.CENTER_VERTICAL); + int paddingH = (int) (10 * density); + int paddingV = (int) (6 * density); + container.setPadding(paddingH, paddingV, paddingH, paddingV); + + GradientDrawable bg = new GradientDrawable(); + bg.setColor(Color.parseColor("#1A1A1A")); + bg.setCornerRadius(999 * density); + container.setBackground(bg); + + playPauseTextView = new TextView(this); + int playSize = (int) (26 * density); + LinearLayout.LayoutParams playLp = new LinearLayout.LayoutParams(playSize, playSize); + playLp.rightMargin = (int) (10 * density); + playPauseTextView.setLayoutParams(playLp); + playPauseTextView.setTextColor(Color.WHITE); + playPauseTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + playPauseTextView.setGravity(Gravity.CENTER); + GradientDrawable playBg = new GradientDrawable(); + playBg.setColor(Color.parseColor("#FF3B30")); + playBg.setCornerRadius(999 * density); + playPauseTextView.setBackground(playBg); + playPauseTextView.setOnClickListener(v -> { + isPaused = !isPaused; + updatePlayPauseIcon(); + NativePlugin.emitRecordingFloatingAction("togglePause"); + }); + updatePlayPauseIcon(); + container.addView(playPauseTextView); + + LinearLayout middleArea = new LinearLayout(this); + middleArea.setOrientation(LinearLayout.HORIZONTAL); + middleArea.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams middleLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + middleLp.rightMargin = (int) (10 * density); + middleArea.setLayoutParams(middleLp); + middleArea.setClickable(true); + middleArea.setFocusable(true); + + durationTextView = new TextView(this); + durationTextView.setTextColor(Color.WHITE); + durationTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + LinearLayout.LayoutParams durationLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + durationLp.rightMargin = (int) (8 * density); + durationTextView.setLayoutParams(durationLp); + durationTextView.setText("00:00"); + middleArea.addView(durationTextView); + + typeTextView = new TextView(this); + typeTextView.setTextColor(Color.parseColor("#E6E6E6")); + typeTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + LinearLayout.LayoutParams typeLp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + typeTextView.setLayoutParams(typeLp); + typeTextView.setVisibility(View.GONE); + middleArea.addView(typeTextView); + + middleArea.setOnClickListener(v -> { + bringAppToFront(); + NativePlugin.emitRecordingFloatingAction("openMeetingRecord"); + }); + + middleArea.setOnTouchListener(new View.OnTouchListener() { + private int initialY; + private float initialTouchY; + private boolean dragging; + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (layoutParams == null || windowManager == null) return false; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + initialY = layoutParams.y; + initialTouchY = event.getRawY(); + dragging = false; + return true; + case MotionEvent.ACTION_MOVE: + int dy = (int) (event.getRawY() - initialTouchY); + if (!dragging && Math.abs(dy) > touchSlop) { + dragging = true; + } + if (!dragging) return true; + + int newY = initialY + dy; + int screenHeight = getResources().getDisplayMetrics().heightPixels; + int viewHeight = floatingView == null ? 0 : floatingView.getHeight(); + int maxY = Math.max(0, screenHeight - viewHeight - (int) (24 * density)); + if (newY < 0) newY = 0; + if (newY > maxY) newY = maxY; + layoutParams.y = newY; + windowManager.updateViewLayout(floatingView, layoutParams); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (!dragging) { + v.performClick(); + } + return true; + default: + return false; + } + } + }); + + container.addView(middleArea); + + TextView close = new TextView(this); + int closeSize = (int) (22 * density); + LinearLayout.LayoutParams closeLp = new LinearLayout.LayoutParams(closeSize, closeSize); + close.setLayoutParams(closeLp); + close.setText("×"); + close.setTextColor(Color.WHITE); + close.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + close.setGravity(Gravity.CENTER); + close.setOnClickListener(v -> { + removeFloatingView(); + stopSelf(); + NativePlugin.emitRecordingFloatingAction("closeFloatingBar"); + }); + container.addView(close); + + floatingView = container; + } + + private void addFloatingView() { + if (windowManager == null || floatingView == null) return; + layoutParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + : WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT + ); + + layoutParams.gravity = Gravity.TOP | Gravity.END; + float density = getResources().getDisplayMetrics().density; + layoutParams.x = (int) (6 * density); + layoutParams.y = (int) (48 * density); + + windowManager.addView(floatingView, layoutParams); + } + + private void removeFloatingView() { + if (windowManager != null && floatingView != null) { + try { + windowManager.removeView(floatingView); + } catch (Exception ignored) { + } + floatingView = null; + layoutParams = null; + durationTextView = null; + typeTextView = null; + playPauseTextView = null; + } + } + + private void bringAppToFront() { + try { + Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName()); + if (launchIntent == null) return; + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(launchIntent); + } catch (Exception ignored) { + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + removeFloatingView(); + super.onDestroy(); + } +} diff --git a/local_plugins/native_plugin/lib/native_plugin.dart b/local_plugins/native_plugin/lib/native_plugin.dart index c54721f1f..9219477a4 100644 --- a/local_plugins/native_plugin/lib/native_plugin.dart +++ b/local_plugins/native_plugin/lib/native_plugin.dart @@ -16,4 +16,35 @@ class NativePlugin { Future disposePipContentView(int viewId) { return NativePluginPlatform.instance.disposePipContentView(viewId); } + + Future showRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + return NativePluginPlatform.instance.showRecordingFloatingBar( + duration: duration, + recordingType: recordingType, + isPaused: isPaused, + ); + } + + Future updateRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + return NativePluginPlatform.instance.updateRecordingFloatingBar( + duration: duration, + recordingType: recordingType, + isPaused: isPaused, + ); + } + + Future hideRecordingFloatingBar() { + return NativePluginPlatform.instance.hideRecordingFloatingBar(); + } + + Stream get recordingFloatingActionStream => + NativePluginPlatform.instance.recordingFloatingActionStream; } diff --git a/local_plugins/native_plugin/lib/native_plugin_method_channel.dart b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart index ec8e18ac5..c58206c2a 100644 --- a/local_plugins/native_plugin/lib/native_plugin_method_channel.dart +++ b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart @@ -9,6 +9,18 @@ class MethodChannelNativePlugin extends NativePluginPlatform { @visibleForTesting final methodChannel = const MethodChannel('native_plugin'); + final _recordingFloatingActionsChannel = const EventChannel( + 'native_plugin/recording_floating_actions', + ); + + @override + Stream get recordingFloatingActionStream { + if (kIsWeb) return Stream.empty(); + return _recordingFloatingActionsChannel.receiveBroadcastStream().map( + (event) => event?.toString() ?? '', + ); + } + @override Future getPlatformVersion() async { final version = await methodChannel.invokeMethod( @@ -34,4 +46,35 @@ class MethodChannelNativePlugin extends NativePluginPlatform { Future disposePipContentView(int viewId) async { await methodChannel.invokeMethod('disposePipContentView', viewId); } + + @override + Future showRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) async { + await methodChannel.invokeMethod('showRecordingFloatingBar', { + 'duration': duration, + 'recordingType': recordingType, + 'isPaused': isPaused, + }); + } + + @override + Future updateRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) async { + await methodChannel.invokeMethod('updateRecordingFloatingBar', { + 'duration': duration, + 'recordingType': recordingType, + 'isPaused': isPaused, + }); + } + + @override + Future hideRecordingFloatingBar() async { + await methodChannel.invokeMethod('hideRecordingFloatingBar'); + } } diff --git a/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart index c26495c2f..014548fe3 100644 --- a/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart +++ b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart @@ -44,4 +44,32 @@ abstract class NativePluginPlatform extends PlatformInterface { 'disposePipContentView() has not been implemented.', ); } + + Future showRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + throw UnimplementedError( + 'showRecordingFloatingBar() has not been implemented.', + ); + } + + Future updateRecordingFloatingBar({ + required String duration, + String? recordingType, + bool isPaused = false, + }) { + throw UnimplementedError( + 'updateRecordingFloatingBar() has not been implemented.', + ); + } + + Future hideRecordingFloatingBar() { + throw UnimplementedError( + 'hideRecordingFloatingBar() has not been implemented.', + ); + } + + Stream get recordingFloatingActionStream => Stream.empty(); }