diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist index c90ada3a3..590c85a5f 100644 --- a/ios/Runner/GoogleService-Info.plist +++ b/ios/Runner/GoogleService-Info.plist @@ -3,9 +3,9 @@ CLIENT_ID - 907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com + 907754492821-tl2blr39g82re40sttkve36ovt6qcu6c.apps.googleusercontent.com REVERSED_CLIENT_ID - com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 + com.googleusercontent.apps.907754492821-tl2blr39g82re40sttkve36ovt6qcu6c ANDROID_CLIENT_ID 907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com API_KEY @@ -15,7 +15,7 @@ PLIST_VERSION 1 BUNDLE_ID - com.yunqiinnovation.deepsound + com.chuangxiangyunqi.deapsound PROJECT_ID deepsound-e0e3d STORAGE_BUCKET @@ -31,6 +31,6 @@ IS_SIGNIN_ENABLED GOOGLE_APP_ID - 1:907754492821:ios:246794fc44b23174145c9c + 1:907754492821:ios:e4b4f3b37e58a7d1145c9c \ No newline at end of file diff --git a/lib/core/utils/synchrodata.dart b/lib/core/utils/synchrodata.dart index b25638663..5cca0d2e1 100644 --- a/lib/core/utils/synchrodata.dart +++ b/lib/core/utils/synchrodata.dart @@ -128,7 +128,6 @@ class Synchrodata { 'title': element['title'] ?? '', 'type': element['rtype'] ?? '', 'seconds': element['seconds'] ?? 0, - 'filepath': '', 'audiourl': element['audiourl'] ?? '', 'tasktype': element['tasktype'] ?? 0, 'creationtime': element['creationtime'] ?? 0, @@ -157,7 +156,10 @@ class Synchrodata { whereArgs: [element['number']], ); } else { - await txn.insert('meeting', meetingData); + await txn.insert('meeting', { + ...meetingData, + 'filepath': '', + }); await txn.insert('meetingdetails', meetingDetailsData); } if (element['original'].isNotEmpty) { @@ -169,19 +171,34 @@ class Synchrodata { List speakerList = jsonDecode(element['original']); if (existSpeakerList.isNotEmpty) { final batch = txn.batch(); - for (var item in speakerList) { - batch.update( - 'meetingspeaker', - item, - where: 'id = ?', - whereArgs: [item['id']], - ); + 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', item); + batch.insert('meetingspeaker', { + 'meetingid': item['meetingid'], + 'content': item['content'], + 'starttime': item['starttime'], + 'endtime': item['endtime'], + 'speaker': item['speaker'], + }); } await batch.commit(); } diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index 955c2b82f..f7c34edbd 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -322,6 +322,7 @@ class MeetingController extends GetxController { } else { FilePickerResult? result = await FilePicker.platform.pickFiles( withData: true, + type: FileType.audio, ); if (result != null) { PlatformFile file = result.files.first; diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 8ac33d0fd..c97e7ed53 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_easyloading/flutter_easyloading.dart'; @@ -9,6 +11,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/synchrodata.dart'; @@ -58,6 +61,9 @@ class MeetingDetailsController extends GetxController String markmapToolbarJs = ''; String saveSvgAsPngJs = ''; + RxBool isDownload = false.obs; + RxDouble downloadProgress = 0.0.obs; + String _systemPrompt = ''; String _userId = ''; @@ -205,6 +211,30 @@ class MeetingDetailsController extends GetxController await rootBundle.loadString('assets/js/saveSvgAsPng.min.js'); } + void downloadAudio() async { + if (isDownload.value) return; + isDownload.value = true; + final directory = await getApplicationDocumentsDirectory(); + final directoryDir = Directory('${directory.path}/DownloadAudio'); + if (!await directoryDir.exists()) { + await directoryDir.create(recursive: true); + } + final fileName = meetingData.value.audiourl.split('/').last; + final String filePath = '${directory.path}/DownloadAudio/$fileName'; + await Dio().download( + meetingData.value.audiourl, + filePath, + onReceiveProgress: (int received, int total) { + downloadProgress.value = total > 0 ? received / total : 0.0; + }, + ); + meetingData.value.filepath = filePath; + meetingData.refresh(); + _loadPlayerController(); + SqfliteApi.editMeetingTitle(id, {'filePath': filePath}); + isDownload.value = false; + } + void editSpeakerText() async { if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) { Map? foundElement = editSpeakerTextList.firstWhere( diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 1dfe1df3e..8b58e7378 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -138,6 +138,7 @@ class MeetingRecordController extends GetxController _bleManager.closeCodec(); } _asrService.stopRecord(true); + _asrService.setPause(); _asrService.stopContinuousRecognition(); isRecording.value = false; } @@ -214,7 +215,7 @@ class MeetingRecordController extends GetxController _recognitionSubscription?.cancel(); _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); - + _asrService.setContinue(); // Prepare storage directory final appDir = await appDocDir; final dir = Directory("${appDir.path}/MeetingAudio/"); diff --git a/lib/modules/meeting/controllers/mind_map_controller.dart b/lib/modules/meeting/controllers/mind_map_controller.dart index 8fe7b67e6..d30244f29 100644 --- a/lib/modules/meeting/controllers/mind_map_controller.dart +++ b/lib/modules/meeting/controllers/mind_map_controller.dart @@ -27,7 +27,7 @@ class MindMapController extends GetxController { - + MindMap diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index 1217d09d2..79640b2a6 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -81,20 +81,50 @@ class MeetingDetailsView extends GetView { children: [ GestureDetector( onTap: () { - if (controller.isPlay.value) { - controller.playerController.pausePlayer(); + if (controller.meetingData.value.filepath.isEmpty) { + controller.downloadAudio(); } else { - controller.playerController.startPlayer(); + if (controller.isPlay.value) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } } }, child: Obx( - () => Icon( - controller.isPlay.value - ? Icons.pause_circle - : Icons.play_circle, - color: Colors.black, - size: 30.w, - ), + () => !controller.isLoading.value && + controller.meetingData.value.filepath.isEmpty + ? Obx( + () => Stack( + alignment: Alignment.center, + children: [ + if (controller.isDownload.value) + SizedBox( + width: 26.w, + height: 26.w, + child: CircularProgressIndicator( + value: controller.downloadProgress.value, + valueColor: + const AlwaysStoppedAnimation( + Colors.red), + strokeWidth: 2.w, + ), + ), + Icon( + Icons.download_for_offline, + color: Colors.black, + size: 30.w, + ), + ], + ), + ) + : Icon( + controller.isPlay.value + ? Icons.pause_circle + : Icons.play_circle, + color: Colors.black, + size: 30.w, + ), ), ), 4.horizontalSpace, @@ -446,20 +476,52 @@ class MeetingDetailsView extends GetView { ), GestureDetector( onTap: () { - if (controller.isPlay.value) { - controller.playerController.pausePlayer(); + if (controller.meetingData.value.filepath.isEmpty) { + controller.downloadAudio(); } else { - controller.playerController.startPlayer(); + if (controller.isPlay.value) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } } }, child: Obx( - () => Icon( - controller.isPlay.value - ? Icons.pause_circle - : Icons.play_circle, - color: Colors.black, - size: 48.w, - ), + () => !controller.isLoading.value && + controller + .meetingData.value.filepath.isEmpty + ? Obx( + () => Stack( + alignment: Alignment.center, + children: [ + if (controller.isDownload.value) + SizedBox( + width: 42.w, + height: 42.w, + child: CircularProgressIndicator( + value: controller + .downloadProgress.value, + valueColor: + const AlwaysStoppedAnimation< + Color>(Colors.red), + strokeWidth: 3.w, + ), + ), + Icon( + Icons.download_for_offline, + color: Colors.black, + size: 48.w, + ), + ], + ), + ) + : Icon( + controller.isPlay.value + ? Icons.pause_circle + : Icons.play_circle, + color: Colors.black, + size: 48.w, + ), ), ), GestureDetector( diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index dd1fc980b..b40408dbc 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -106,13 +106,16 @@ class TranslationController extends GetxController { _asrService.setContinue(); lastActiveSpeaker.value = id; } else { - _asrService.setPause(); if (fTFTranslationResult != '' && !isPlayback) { isPlayback = true; await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); await Future.delayed(const Duration(milliseconds: 1300)); await playTranslatedText(fTFTranslationResult!); fTFTranslationResult = ''; + } else if ((translationHistory.isEmpty || + !translationHistory.last.isIntermediate) && + currentMode.value == 'faceToFace') { + _asrService.setPause(); } } } @@ -165,7 +168,7 @@ class TranslationController extends GetxController { } // 加载历史记录 - //loadTranslationHistory(); + loadTranslationHistory(); // 初始化服务 _initServices(); @@ -462,6 +465,7 @@ class TranslationController extends GetxController { if (translationHistory.isEmpty || !translationHistory.last.isIntermediate) { currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); + final newItem = TranslationItem( sourceText: text, translatedText: '', @@ -474,10 +478,15 @@ class TranslationController extends GetxController { isIntermediate: true, ); // 添加新项后滚动到底部 - translationHistory.add(newItem); + if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { + _asrService.setPause(); + } else { + translationHistory.add(newItem); + } _scrollToBottom(); } else { // 更新当前项 + translationHistory.last.sourceText = text; translationHistory.refresh(); // 更新后滚动到底部 @@ -509,7 +518,9 @@ class TranslationController extends GetxController { // 更新后滚动到底部 _scrollToBottom(); saveTranslationHistory(); - + if (activeSpeaker.value == 0 && currentMode.value == 'faceToFace') { + _asrService.setPause(); + } // 使用项目的时间戳进行翻译 translateText(text, translationHistory.last.timestamp, isFinal: true); } else { @@ -526,14 +537,16 @@ class TranslationController extends GetxController { translationHistory.last.sessionId != currentSessionId, isIntermediate: false, ); - + if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { + _asrService.setPause(); + } //else { translationHistory.add(newItem); translationHistory.refresh(); _scrollToBottom(); saveTranslationHistory(); - // 使用新项目的时间戳进行翻译 translateText(text, newItem.timestamp, isFinal: true); + // } } } @@ -546,13 +559,33 @@ class TranslationController extends GetxController { String? translationResult; try { - // 根据检测到的语言确定源语言和目标语言 - final bool shouldSwap = detectedLanguageCode == sourceLanguageCode.value; - final detectedSourceLanguageCode = - shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; - final detectedTargetLanguageCode = - shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value; - + var detectedSourceLanguageCode; + var detectedTargetLanguageCode; + if ((currentMode.value == 'faceToFace' && activeSpeaker.value == 1) || + (currentMode.value == 'faceToFace' && + activeSpeaker.value == 0 && + lastActiveSpeaker.value == 1)) { + detectedLanguageCode = sourceLanguageCode.value; + detectedSourceLanguageCode = sourceLanguageCode.value; + detectedTargetLanguageCode = targetLanguageCode.value; + } else if ((currentMode.value == 'faceToFace' && + activeSpeaker.value == 2) || + (currentMode.value == 'faceToFace' && + activeSpeaker.value == 0 && + lastActiveSpeaker.value == 2)) { + // 根据检测到的语言确定源语言和目标语言 + detectedLanguageCode = targetLanguageCode.value; + detectedSourceLanguageCode = targetLanguageCode.value; + detectedTargetLanguageCode = sourceLanguageCode.value; + } else { + // 根据检测到的语言确定源语言和目标语言 + final bool shouldSwap = + detectedLanguageCode == sourceLanguageCode.value; + detectedSourceLanguageCode = + shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; + detectedTargetLanguageCode = + shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value; + } // 调用翻译服务 translationResult = await _translationService.translateText( text: sourceText, @@ -560,6 +593,7 @@ class TranslationController extends GetxController { targetLanguageCode: detectedTargetLanguageCode, ); + /// } if (translationResult != null) { // 查找匹配的项目 - 优先使用timestamp查找 int index = -1; @@ -577,26 +611,22 @@ class TranslationController extends GetxController { saveTranslationHistory(); // 更新后滚动到底部 _scrollToBottom(); - if (isFinal && currentMode.value == 'faceToFace') { - print("音频输出文本2$translationResult"); - fTFTranslationResult = - (fTFTranslationResult ?? '') + translationResult; - - if (activeSpeaker.value == 0 && - !isPlayback && - currentMode.value == 'faceToFace') { - isPlayback = true; - await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); - await Future.delayed(const Duration(milliseconds: 1300)); - await playTranslatedText(fTFTranslationResult!); - fTFTranslationResult = ''; - } - } // 播放TTS - 只在最终结果时播放 - if (isTtsEnabled.value && - isFinal && - currentMode.value != 'faceToFace') { - await playTranslatedText(translationResult); + if (isFinal) { + if (isTtsEnabled.value && currentMode.value != 'faceToFace') { + await playTranslatedText(translationResult); + } else { + fTFTranslationResult = + (fTFTranslationResult ?? '') + translationResult; + print("translationResult=$translationResult"); + if (activeSpeaker.value == 0 && !isPlayback) { + isPlayback = true; + await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); + await Future.delayed(const Duration(milliseconds: 1300)); + await playTranslatedText(fTFTranslationResult!); + fTFTranslationResult = ''; + } + } } } else { Logger.info( @@ -623,7 +653,7 @@ class TranslationController extends GetxController { final voiceName = _languageManager .getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural'; - + print("voiceName=$voiceName,text$text"); await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(text); } catch (e) { diff --git a/lib/modules/translation/views/translation_list_view.dart b/lib/modules/translation/views/translation_list_view.dart index 994882077..dd13b7521 100644 --- a/lib/modules/translation/views/translation_list_view.dart +++ b/lib/modules/translation/views/translation_list_view.dart @@ -340,7 +340,11 @@ class TranslationListView extends GetView { FileSystemEntity file, ) { final TextEditingController _renameController = TextEditingController( - text: file.path.split('/').last, + text: () { + final fileName = file.path.split('/').last; + final dotIndex = fileName.lastIndexOf('.'); + return dotIndex != -1 ? fileName.substring(0, dotIndex) : fileName; + }(), ); Get.bottomSheet( @@ -432,7 +436,7 @@ class TranslationListView extends GetView { if (newName.isEmpty) return; final dir = file.parent.path; - final newPath = '$dir/$newName'; + final newPath = '$dir/$newName.wav'; try { await file.rename(newPath); diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index e2bf87d54..e529bd98d 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -528,27 +530,29 @@ class TranslationView extends GetView { }) { bool isActive = activeSpeaker == speakerId; bool isDisabled = activeSpeaker != 0 && activeSpeaker != speakerId; - + Timer? _holdTimer; // 增加延迟触发的计时器 return Listener( onPointerDown: (_) { - if (activeSpeaker == 0) { - // 没有激活的按钮时才能激活 - if (speakerId == 1) { - // 耳机收音 - 恢复原始音频状态 - controller.restoreOriginalAudioState(); - } else { - // 手机收音 - 禁用蓝牙音频 - controller.disableBluetoothAudio(); + // 设置300ms延迟防止误触 + _holdTimer = Timer(const Duration(milliseconds: 200), () { + if (activeSpeaker == 0) { + if (speakerId == 1) { + controller.restoreOriginalAudioState(); + } else { + controller.disableBluetoothAudio(); + } + controller.setActiveSpeaker(speakerId); } - controller.setActiveSpeaker(speakerId); - } + }); }, onPointerUp: (_) { + _holdTimer?.cancel(); if (isActive) { controller.setActiveSpeaker(0); } }, onPointerCancel: (_) { + _holdTimer?.cancel(); if (isActive) { controller.setActiveSpeaker(0); } diff --git a/pubspec.yaml b/pubspec.yaml index 0298f6e64..6fb187beb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: webview_flutter: ^4.13.0 flutter_easyloading: ^3.0.5 package_info_plus: ^8.3.0 - file_picker: ^10.1.5 + file_picker: ^10.2.0 haptic_feedback: ^0.5.1+1 tencentcloud_cos_sdk_plugin: ^1.2.3 photo_view: ^0.15.0