From c3cf6b9a9b29f181b6833b88c51a02fa28b49084 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 28 Jun 2025 17:38:33 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=8A=E5=B1=82?= =?UTF-8?q?=E9=80=80=E5=87=BA=E7=95=8C=E9=9D=A2=E6=97=B6=E5=85=B3=E9=97=AD?= =?UTF-8?q?AI=E6=89=A7=E8=A1=8C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/agent/controllers/agent_controller.dart | 1 + .../com/yunqiinnovation/agent_service/AgentService.kt | 9 ++++----- .../agent_service/AgentServicePlugin.kt | 10 ++++++++++ local_plugins/agent_service/lib/agent_service.dart | 10 ++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 3648b0639..da450222b 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -152,6 +152,7 @@ class AgentController extends GetxController { @override void onClose() { + AgentService.stopAiSteam(); //停止AI流 AgentService.stopConversation(); // 停止当前会话 AgentService.stopTts(); // 停止当前语音 textController.dispose(); diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index fdd51c709..5a78673c6 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -571,7 +571,7 @@ object AgentService : CoroutineScope { /** * 停止AI流输出 */ - private fun stopAiStream() { + fun stopAiStream() { if (isAiStreaming) { try { // 先更新状态,避免回调时的状态不一致 @@ -580,7 +580,8 @@ object AgentService : CoroutineScope { // 取消当前AI生成任务 currentAiJob?.cancel() currentAiJob = null - + currsessionId = "" //取消会话 + // 通知ChatAPI服务终止当前流式请求 chatApiService.cancelCurrentStream() @@ -756,9 +757,6 @@ object AgentService : CoroutineScope { responseBuilder.append(token) if (speakResponse && broadcast) { ttsService?.speakStream(token) - if (token.length > 0) { - audioPlayer?.stopAudio() - } } if (broadcast) { // 发送流式回复token @@ -774,6 +772,7 @@ object AgentService : CoroutineScope { // 视情况决定是否朗读回复 if (speakResponse && broadcast && sessionid == currsessionId){ ttsService?.flushStream() + audioPlayer?.stopAudio() } val response = responseBuilder.toString() // 发送完整回复,包含是否有图片的标记 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 64353c834..44f6013e8 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -177,6 +177,16 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) } } + "stopAiSteam" -> { + try { + AgentService.stopAiStream() + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "停止语音合成失败", e) + result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) + } + } + "clearChatHistory" -> { try { AgentService.clearChatHistory { success -> diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index fc1579a05..61cf6b5b1 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -293,6 +293,16 @@ class AgentService { } } + /// 停止语音合成 + static Future stopAiSteam() async { + try { + final bool result = await _channel.invokeMethod('stopAiSteam'); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '停止语音合成失败', e.details); + } + } + /// 停止语音合成 static Future stopTts() async { try { From 1ccb434860277dd7d441aa2af006fc40b64835db Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Mon, 30 Jun 2025 11:58:11 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=20=E9=9F=B3=E6=95=88?= =?UTF-8?q?=E6=92=AD=E6=94=BE=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentService.kt | 195 ++++++++++++++---- 1 file changed, 152 insertions(+), 43 deletions(-) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index fdd51c709..ad17ede21 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -26,6 +26,7 @@ import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.bytedance_speech.BytedanceTTS import java.util.UUID +import com.yunqiinnovation.agent_service.Utils /** * 代理服务事件监听接口 @@ -1164,6 +1165,22 @@ object AgentService : CoroutineScope { class AudioPlayer(private val context: Context) { private var mediaPlayer: MediaPlayer? = null private var isInitialized = false + private val playbackLock = Object() // 添加同步锁 + + /** + * 安全检查 MediaPlayer 是否正在播放 + */ + private fun isMediaPlayerPlaying(): Boolean { + return try { + mediaPlayer?.isPlaying == true + } catch (e: IllegalStateException) { + Log.w(TAG, "MediaPlayer 状态异常,假定未播放: ${e.message}") + false + } catch (e: Exception) { + Log.w(TAG, "检查播放状态时发生异常: ${e.message}") + false + } + } /** * 播放音频资源 @@ -1172,75 +1189,167 @@ object AgentService : CoroutineScope { * @param volume 音量大小,范围0.0-1.0,默认1.0 */ fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) { - try { - // 释放之前的资源 - release() + synchronized(playbackLock) { + try { + // 强制停止并释放之前的资源 + forceStop() - // 创建播放器并设置资源 - mediaPlayer = MediaPlayer().apply { - // 设置资源 - context.resources.openRawResourceFd(resId)?.use { fd -> - setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) - } + // 创建播放器并设置资源 + mediaPlayer = MediaPlayer().apply { + // 设置资源 + context.resources.openRawResourceFd(resId)?.use { fd -> + setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) + } - this.isLooping = isLooping // 设置循环属性 - setVolume(volume, volume) // 设置音量(左声道,右声道) + this.isLooping = isLooping // 设置循环属性 + setVolume(volume, volume) // 设置音量(左声道,右声道) - setOnCompletionListener { - if (!isLooping) { - release() + setOnCompletionListener { + if (!isLooping) { + release() + } } - } - prepare() - start() + setOnErrorListener { _, what, extra -> + Log.e(TAG, "MediaPlayer 错误: what=$what, extra=$extra") + forceRelease() + true // 返回true表示错误已处理 + } + + prepare() + start() + Log.d(TAG, "开始播放音频资源: $resId, 循环: $isLooping") + } + isInitialized = true + } catch (e: Exception) { + Log.e(TAG, "播放音频资源异常: ${e.message}", e) + forceRelease() } - } catch (e: Exception) { - Log.e(TAG, "播放音频资源异常: ${e.message}", e) - release() } } + /** + * 停止音频播放 + */ fun stopAudio() { - try { - mediaPlayer?.apply { - when { - isPlaying -> { + synchronized(playbackLock) { + try { + mediaPlayer?.apply { + if (isMediaPlayerPlaying()) { stop() Log.d(TAG, "音频已停止") - } - else -> { + } else { Log.d(TAG, "音频未在播放状态,无需停止") } } + } catch (e: IllegalStateException) { + Log.e(TAG, "MediaPlayer 状态异常,无法停止: ${e.message}", e) + forceRelease() + } catch (e: Exception) { + Log.e(TAG, "停止音频播放异常: ${e.message}", e) + forceRelease() } - } catch (e: IllegalStateException) { - Log.e(TAG, "MediaPlayer 状态异常,无法停止: ${e.message}", e) - // 重置 MediaPlayer - release() - } catch (e: Exception) { - Log.e(TAG, "停止音频播放异常: ${e.message}", e) } } /** - * 释放资源 + * 强制停止播放(不释放资源) */ - fun release() { + private fun forceStop() { try { mediaPlayer?.apply { - if (isPlaying) { - stop() + if (isMediaPlayerPlaying()) { + try { + stop() + Log.d(TAG, "强制停止音频播放") + } catch (e: Exception) { + Log.w(TAG, "强制停止时异常: ${e.message}") + } + } + try { + reset() + } catch (e: Exception) { + Log.w(TAG, "重置时异常: ${e.message}") } - reset() - release() } - mediaPlayer = null - isInitialized = false } catch (e: Exception) { - Log.e(TAG, "释放音频资源异常: ${e.message}", e) - mediaPlayer = null - isInitialized = false + Log.w(TAG, "强制停止过程中异常: ${e.message}") + } + } + + /** + * 释放资源 + */ + fun release() { + synchronized(playbackLock) { + try { + mediaPlayer?.apply { + // 安全停止播放 + if (isMediaPlayerPlaying()) { + try { + stop() + } catch (e: IllegalStateException) { + Log.w(TAG, "停止播放时状态异常: ${e.message}") + } + } + + // 安全重置 + try { + reset() + } catch (e: IllegalStateException) { + Log.w(TAG, "重置 MediaPlayer 时状态异常: ${e.message}") + } + + // 安全释放 - 修复递归调用问题 + try { + release() // 这里调用的是 MediaPlayer.release(),不是递归 + } catch (e: Exception) { + Log.w(TAG, "释放 MediaPlayer 时异常: ${e.message}") + } + } + } catch (e: Exception) { + Log.e(TAG, "释放音频资源异常: ${e.message}", e) + } finally { + // 确保变量被清理 + mediaPlayer = null + isInitialized = false + } + } + } + + /** + * 强制释放资源,不进行状态检查 + */ + private fun forceRelease() { + synchronized(playbackLock) { + try { + mediaPlayer?.apply { + try { + reset() + } catch (e: Exception) { + Log.w(TAG, "重置 MediaPlayer 失败: ${e.message}") + } + try { + release() + } catch (e: Exception) { + Log.w(TAG, "释放 MediaPlayer 失败: ${e.message}") + } + } + } catch (e: Exception) { + Log.w(TAG, "强制释放资源时发生异常: ${e.message}") + } finally { + mediaPlayer = null + isInitialized = false + } + } + } + + /** + * 检查是否正在播放 + */ + fun isPlaying(): Boolean { + synchronized(playbackLock) { + return isMediaPlayerPlaying() } } } From 5403b9518903631c03870707a6f023d0098874f0 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 30 Jun 2025 16:24:40 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=97=B6=E4=B8=8D=E7=BF=BB=E8=AF=91=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/translation_controller.dart | 3 +- .../azure_speech/AzureAsrHelper.kt | 108 ++++++++++++------ 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 0ebed9187..877ddad5b 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -161,7 +161,7 @@ class TranslationController extends GetxController { } // 加载历史记录 - loadTranslationHistory(); + //loadTranslationHistory(); // 初始化服务 _initServices(); @@ -469,6 +469,7 @@ class TranslationController extends GetxController { isIntermediate: true, ); // 添加新项后滚动到底部 + translationHistory.add(newItem); _scrollToBottom(); } else { // 更新当前项 diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 919247837..a450c0d8a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -80,7 +80,29 @@ class AzureAsrHelper(private val context: Context) { /** 使用外部提供的音频数据 */ EXTERNAL } - + /** + * 获取设备支持的最佳音频格式 + * 优先选择16000Hz,若不支持则降级到8000Hz + */ + private fun getOptimalAudioFormat(): AudioStreamFormat { + // 支持的采样率列表(按优先级排序) + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + val bufferSize = AudioRecord.getMinBufferSize( + rate, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 + + Log.i(tag, "使用采样率: ${sampleRate}Hz") + + // 创建对应的音频格式 + return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1) + } /** * 初始化Azure语音服务 * @@ -106,7 +128,7 @@ class AzureAsrHelper(private val context: Context) { // 释放之前的资源 dispose() // 1. 创建可控制的音频流 - val format = AudioStreamFormat.getWaveFormatPCM(16000, 16, 1) + val format = getOptimalAudioFormat() audioStream = AudioInputStream.createPushStream(format) // 初始化音频管理器 audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -157,7 +179,7 @@ class AzureAsrHelper(private val context: Context) { // 录音文件类 recordfile = RecordFile; //进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请 - // 创建识别器 + // // 创建识别器 // val setupSuccess = setupRecognizer() // if (setupSuccess) { // // 优化:初始化完成后进行预热 @@ -369,6 +391,8 @@ class AzureAsrHelper(private val context: Context) { audioRecord?.startRecording() startCaptureThread() // 再启动数据读取线程 isContinuousRecognitionActive = true +Log.d(tag, "识别器创建成功: ${recognizer != null}") +Log.d(tag, "开始识别任务: recognizeOnce 或 startContinuousRecognition 被调用") return true } catch (e: Exception) { @@ -705,8 +729,19 @@ class AzureAsrHelper(private val context: Context) { * 开始麦克风捕获 */ private fun initAudioRecord() { - // 设置音频参数 - val sampleRate = 16000 // 16kHz采样率(语音识别常用) + // 获取已确定的采样率 + val format = getOptimalAudioFormat() + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + val bufferSize = AudioRecord.getMinBufferSize( + rate, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 val channelConfig = AudioFormat.CHANNEL_IN_MONO // 单声道输入 val audioFormat = AudioFormat.ENCODING_PCM_16BIT // 16位PCM格式 @@ -732,35 +767,42 @@ class AzureAsrHelper(private val context: Context) { } // 3. 单独封装线程启动逻辑 - private fun startCaptureThread() { - captureThread = Thread { - // 循环条件确保录音已开始 - - val buffer = ByteArray(1024) // 4KB数据缓冲区 - while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { - // 从麦克风读取数据 - val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 - - // 仅在未暂停时处理数据 - if (!isPaused && bytesRead > 0) { - // 写入音频流(可能是网络传输或本地处理) - audioStream?.write(buffer) - // 保存到WAV文件(如果启用了录制功能) - recordfile?.saveAudioDataToWav(buffer) - } - - // 暂停时短暂休眠以减少CPU占用 - if (isPaused) { - try { - Thread.sleep(50) - } catch (e: InterruptedException) { - break // 线程被中断时退出循环 - } - } + private fun startCaptureThread() { + captureThread = Thread { + // 根据实际采样率计算缓冲区大小 + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + var bufferSize = 0 + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + bufferSize = AudioRecord.getMinBufferSize( + rate, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 + // (sampleRate * 0.032).toInt() * 2 // 32ms数据量 + + val buffer = ByteArray(bufferSize) + + while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + + // 读取音频 + val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 +Log.i("AzureASR", "AudioRecord state: ${audioRecord?.state}, recordingState: ${audioRecord?.recordingState}") +Log.i("AzureASR", "BytesRead = $bytesRead, buffer.isSilent = ${buffer.all { it == 0.toByte() }}") + + if (!isPaused && bytesRead > 0) { + + // 写入Azure流 + audioStream?.write(buffer) + + // 保存录音 + recordfile?.saveAudioDataToWav(buffer) } - - }.apply { start() } - } + } + }.apply { start() } +} /** * 停止麦克风捕获并释放所有相关资源 From 4f03705c6369226026f961898879230d3b9ce61a Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Mon, 30 Jun 2025 16:38:58 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E5=8A=A9=E6=89=8B=E5=8A=A0=E8=BD=BD=E7=BD=91=E7=BB=9C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/synchrodata.dart | 75 +++++++++++++++++++ .../controllers/meeting_controller.dart | 7 +- .../meeting_details_controller.dart | 24 +++--- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/lib/core/utils/synchrodata.dart b/lib/core/utils/synchrodata.dart index f43f0bdde..b25638663 100644 --- a/lib/core/utils/synchrodata.dart +++ b/lib/core/utils/synchrodata.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:get_storage/get_storage.dart'; +import '../../data/services/db/database_helper.dart'; import '../../data/services/db/sqflite_api.dart'; import '../../data/services/network/api.dart'; @@ -112,6 +113,80 @@ class Synchrodata { // 同步数据 static Future _synchrodata() async { + final db = await DatabaseHelper().database; var result = await Api.getOperationRecord(); + List records = result['records']; + await db.transaction((txn) async { + for (var element in records) { + final List existList = await txn.query( + 'meeting', + where: 'id = ?', + whereArgs: [element['number']], + ); + Map meetingData = { + 'id': element['number'], + 'title': element['title'] ?? '', + 'type': element['rtype'] ?? '', + 'seconds': element['seconds'] ?? 0, + 'filepath': '', + 'audiourl': element['audiourl'] ?? '', + 'tasktype': element['tasktype'] ?? 0, + 'creationtime': element['creationtime'] ?? 0, + }; + Map meetingDetailsData = { + 'meetingid': element['number'], + 'address': element['address'] ?? '', + 'personnel': element['personnel'] ?? '', + 'taskid': element['taskid'] ?? '', + 'tasktype': element['tasktype'] ?? 0, + 'overview': element['overview'] ?? '', + 'summary': element['summary'] ?? '', + 'mindmap': '', + }; + if (existList.isNotEmpty) { + await txn.update( + 'meeting', + meetingData, + where: 'id = ?', + whereArgs: [element['number']], + ); + await txn.update( + 'meetingdetails', + meetingDetailsData, + where: 'meetingid = ?', + whereArgs: [element['number']], + ); + } else { + await txn.insert('meeting', meetingData); + await txn.insert('meetingdetails', meetingDetailsData); + } + if (element['original'].isNotEmpty) { + final List existSpeakerList = await txn.query( + 'meetingspeaker', + where: 'meetingid = ?', + whereArgs: [element['number']], + ); + 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']], + ); + } + await batch.commit(); + } else { + final batch = txn.batch(); + for (var item in speakerList) { + batch.insert('meetingspeaker', item); + } + await batch.commit(); + } + } + } + }); } } diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index d75d9e666..955c2b82f 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -59,7 +59,7 @@ class MeetingController extends GetxController { 'Authorization': 'Bearer; $_token', 'Content-Type': 'application/json', }; - _getDataList(); + _initGetDataList(); addMeetingAudio(); final Map args = Get.arguments ?? {}; // 检查参数是否存在且包含有效路径 @@ -96,6 +96,11 @@ class MeetingController extends GetxController { } } + void _initGetDataList() async { + await Synchrodata.uploadOperationRecords(isSynchro: true); + _getDataList(); + } + // 请求本地数据 void _getDataList() async { List list = await SqfliteApi.getMeetingList((page - 1) * 10 + addQuantity); diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index d05950007..8ac33d0fd 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -152,6 +152,21 @@ class MeetingDetailsController extends GetxController dragOffset.value = 0; isTop.value = true; } + if (meetingData.value.filepath.isNotEmpty) { + await _loadPlayerController(); + } + if (meetingDetails.value.tasktype >= 2) { + List speakerList = await SqfliteApi.getMeetingSpeaker(id); + textList.value = List.from(speakerList); + } + if (meetingDetails.value.tasktype == 1 || + meetingDetails.value.tasktype == 2) { + summaryCreate(); + } + isLoading.value = false; + } + + Future _loadPlayerController() async { await playerController.preparePlayer( path: meetingData.value.filepath, shouldExtractWaveform: false, @@ -178,15 +193,6 @@ class MeetingDetailsController extends GetxController lottieController.stop(); } }); - if (meetingDetails.value.tasktype >= 2) { - List speakerList = await SqfliteApi.getMeetingSpeaker(id); - textList.value = List.from(speakerList); - } - if (meetingDetails.value.tasktype == 1 || - meetingDetails.value.tasktype == 2) { - summaryCreate(); - } - isLoading.value = false; } void _loadJS() async { From 47739817ffb13f9b1a955ff24e2a283719a12f5f Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 30 Jun 2025 18:03:37 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=9D=A2=E5=AF=B9?= =?UTF-8?q?=E9=9D=A2=E7=BF=BB=E8=AF=91=EF=BC=8C=E5=8A=A0=E5=85=A5=E9=99=8D?= =?UTF-8?q?=E5=99=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/asr_service.dart | 7 +- .../speech_impl/azure_asr_service.dart | 2 +- .../speech_impl/volcano_asr_api_service.dart | 2 +- .../speech_impl/volcano_asr_service.dart | 2 +- .../speech_impl/xunfei_asr_service.dart | 2 +- .../controllers/translation_controller.dart | 32 +++-- .../azure_speech/AzureAsrHelper.kt | 116 ++++++++++-------- .../azure_speech/AzureSpeechPlugin.kt | 13 +- .../azure_speech/AzureTtsHelper.kt | 8 +- .../bytedance_speech/BytedanceTTS.kt | 3 +- .../com/deep_voice/speech/ITtsService.kt | 2 +- 11 files changed, 115 insertions(+), 74 deletions(-) diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index 70f88353b..f0c372115 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -26,11 +26,16 @@ abstract class AsrService { /// 停止连续语音识别 Future stopContinuousRecognition(); + /// 停止音频输入 Future setPause(); - Future setcontinue(); + /// 继续音频输入 + Future setContinue(); + + /// 恢复原始音频状态 Future restoreOriginalAudioState(); + /// 禁用蓝牙音频 Future disableBluetoothAudio(); /// 检查连续识别是否活跃 diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index cc5ae9746..16261533c 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -429,7 +429,7 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future setcontinue() async { + Future setContinue() async { // TODO: implement setcontinue try { final bool result = await _channel.invokeMethod('setcontinue'); diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 3fbaa53ef..183f899db 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -849,7 +849,7 @@ class VolcanoAsrApiService implements AsrService { } @override - Future setcontinue() { + Future setContinue() { // TODO: implement setcontinue throw UnimplementedError(); } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index a7b3f541d..871dccb99 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -384,7 +384,7 @@ class VolcanoAsrService extends GetxService implements AsrService { } @override - Future setcontinue() { + Future setContinue() { // TODO: implement setcontinue throw UnimplementedError(); } diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index 6b5450761..5dc8d3a31 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -308,7 +308,7 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future setcontinue() { + Future setContinue() { // TODO: implement setcontinue throw UnimplementedError(); } diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 877ddad5b..dd1fc980b 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -37,10 +37,13 @@ class TranslationController extends GetxController { final bleManager = Get.find(); // 音频输入源 bool _audioSourceType = false; - // 倒计时器 - bool istts = false; +//是否开启播放 + bool isPlayback = false; + //录音计时器 final RxString recordDurationText = '00:00'.obs; + //记录计时器 Timer? _recordTimer; + //秒 int _seconds = 0; // 存储相关 static const String _historyKey = 'translation_history'; @@ -99,23 +102,24 @@ class TranslationController extends GetxController { activeSpeaker.value = id; // 监听识别结果 if (activeSpeaker.value != 0) { - istts = false; - _asrService.setcontinue(); + isPlayback = false; + _asrService.setContinue(); lastActiveSpeaker.value = id; } else { _asrService.setPause(); - if (fTFTranslationResult != '' && !istts) { - istts = true; + if (fTFTranslationResult != '' && !isPlayback) { + isPlayback = true; await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); + await Future.delayed(const Duration(milliseconds: 1300)); await playTranslatedText(fTFTranslationResult!); fTFTranslationResult = ''; } } } - // 切换TTS功能 + // 禁用蓝牙音频 void disableBluetoothAudio() => _asrService.disableBluetoothAudio(); - // 切换TTS功能 + //恢复原始音频状态 void restoreOriginalAudioState() => _asrService.restoreOriginalAudioState(); // 兼容旧版视图的计算属性 @@ -379,7 +383,8 @@ class TranslationController extends GetxController { if (currentMode.value == 'faceToFace') { _asrService.setPause(); } else { - _asrService.setcontinue(); + await _ttsService.setAudioOutputDevice(0); + _asrService.setContinue(); } isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { @@ -577,19 +582,20 @@ class TranslationController extends GetxController { fTFTranslationResult = (fTFTranslationResult ?? '') + translationResult; - if (activeSpeaker.value == 0 && !istts) { - istts = true; + 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') { - _ttsService.setAudioOutputDevice(activeSpeaker.value); await playTranslatedText(translationResult); } } else { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index a450c0d8a..3c4c6fcb0 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -80,6 +80,7 @@ class AzureAsrHelper(private val context: Context) { /** 使用外部提供的音频数据 */ EXTERNAL } + /** * 获取设备支持的最佳音频格式 * 优先选择16000Hz,若不支持则降级到8000Hz @@ -103,6 +104,7 @@ class AzureAsrHelper(private val context: Context) { // 创建对应的音频格式 return AudioStreamFormat.getWaveFormatPCM(sampleRate.toLong(), 16, 1) } + /** * 初始化Azure语音服务 * @@ -391,8 +393,7 @@ class AzureAsrHelper(private val context: Context) { audioRecord?.startRecording() startCaptureThread() // 再启动数据读取线程 isContinuousRecognitionActive = true -Log.d(tag, "识别器创建成功: ${recognizer != null}") -Log.d(tag, "开始识别任务: recognizeOnce 或 startContinuousRecognition 被调用") + return true } catch (e: Exception) { @@ -729,8 +730,8 @@ Log.d(tag, "开始识别任务: recognizeOnce 或 startContinuousRecognition 被 * 开始麦克风捕获 */ private fun initAudioRecord() { - // 获取已确定的采样率 - val format = getOptimalAudioFormat() + // 获取已确定的采样率 + val format = getOptimalAudioFormat() val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) // 查找设备支持的最佳采样率 @@ -751,58 +752,75 @@ Log.d(tag, "开始识别任务: recognizeOnce 或 startContinuousRecognition 被 channelConfig, audioFormat ) - - // 创建 AudioRecord 对象 - audioRecord = AudioRecord( - MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除) - sampleRate, - channelConfig, - audioFormat, - minBufferSize * 2 // 使用两倍缓冲区防止溢出 - ) - - - // 应用音频效果 - applyAudioEffects() - } - - // 3. 单独封装线程启动逻辑 - private fun startCaptureThread() { - captureThread = Thread { - // 根据实际采样率计算缓冲区大小 - val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) - var bufferSize = 0 - // 查找设备支持的最佳采样率 - val sampleRate = supportedSampleRates.firstOrNull { rate -> - bufferSize = AudioRecord.getMinBufferSize( - rate, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT +// 创建录音对象 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + Log.d("AzureASR", "android.os.Build.VERSION.SDK_INT") + val format = AudioFormat.Builder() + .setSampleRate(sampleRate) + .setEncoding(audioFormat) + .setChannelMask(channelConfig) + .build() + + audioRecord = AudioRecord.Builder() + .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) + .setAudioFormat(format) + .setBufferSizeInBytes(minBufferSize * 2) + .build() + } else { + // 创建 AudioRecord 对象 + audioRecord = AudioRecord( + MediaRecorder.AudioSource.VOICE_RECOGNITION, // 语音识别专用音源(自带降噪/回声消除) + sampleRate, + channelConfig, + audioFormat, + minBufferSize * 2 // 使用两倍缓冲区防止溢出 ) - bufferSize > 0 // 返回正值表示支持 - } ?: 16000 // 默认回退值 - // (sampleRate * 0.032).toInt() * 2 // 32ms数据量 - - val buffer = ByteArray(bufferSize) - while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + } + - // 读取音频 - val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 -Log.i("AzureASR", "AudioRecord state: ${audioRecord?.state}, recordingState: ${audioRecord?.recordingState}") -Log.i("AzureASR", "BytesRead = $bytesRead, buffer.isSilent = ${buffer.all { it == 0.toByte() }}") + // 检查初始化状态 + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + throw IllegalStateException("AudioRecord初始化失败") + } - if (!isPaused && bytesRead > 0) { - // 写入Azure流 - audioStream?.write(buffer) + } - // 保存录音 - recordfile?.saveAudioDataToWav(buffer) + // 3. 单独封装线程启动逻辑 + private fun startCaptureThread() { + captureThread = Thread { + // 根据实际采样率计算缓冲区大小 + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) + var bufferSize = 0 + // 查找设备支持的最佳采样率 + val sampleRate = supportedSampleRates.firstOrNull { rate -> + bufferSize = AudioRecord.getMinBufferSize( + rate, + AudioFormat.CHANNEL_IN_MONO, + AudioFormat.ENCODING_PCM_16BIT + ) + bufferSize > 0 // 返回正值表示支持 + } ?: 16000 // 默认回退值 + + val buffer = ByteArray(bufferSize) + + while (!Thread.interrupted() && audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING) { + + // 读取音频 + val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 + + if (!isPaused && bytesRead > 0) { + + // 写入Azure流 + audioStream?.write(buffer) + + // 保存录音 + recordfile?.saveAudioDataToWav(buffer) + } } - } - }.apply { start() } -} + }.apply { start() } + } /** * 停止麦克风捕获并释放所有相关资源 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 06b1d07e9..8cd740589 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 @@ -257,20 +257,24 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { "setPause" -> { azureAsrHelper.stopAudioPause(); + return } "setcontinue" -> { // azureAsrHelper.stopAudioPause(); azureAsrHelper.setAudiocontinue(); + return } "disableBluetoothAudio" -> { azureAsrHelper.disableBluetoothAudio(); + return } "restoreOriginalAudioState" -> { // azureAsrHelper.stopAudioPause(); azureAsrHelper.restoreOriginalAudioState(); + return } "startContinuousRecognition" -> { @@ -523,19 +527,20 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { "setAudioOutputDevice" -> { val type = call.argument("type") + var success =false if (type == 0) { // 默认(如果有耳机选耳机,否则使用系统扬声器) - azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) + success=azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) } else if (type == 1) { // 强制使用声器 - azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) + success=azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) } else if (type == 2) { // 强制使用耳机 - azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) + success=azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) } - result.success(true) + result.success(success) } else -> { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 2e9559c77..709b99e66 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -210,9 +210,10 @@ class AzureTtsHelper(private val context: Context) : ITtsService { * * @param device 音频输出设备类型 */ - override fun setAudioOutputDevice(device: AudioOutputDevice) { + override fun setAudioOutputDevice(device: AudioOutputDevice): Boolean{ // Azure TTS使用系统默认的音频路由 // 音频输出设备的控制需要通过Android的AudioManager实现 + try { FileLogger.e(TAG, "音频输出设备类型device=: ${device}") val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager audioManager?.let { manager -> @@ -244,6 +245,11 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } } } + return true + } catch (e: Exception) { + FileLogger.e(TAG, "设置音频输出设备失败: ${e.message}") + return false + } } /** diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt index 9db3c0e93..6493340f7 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt @@ -425,10 +425,11 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { /** * 设置音频输出设备 */ - override fun setAudioOutputDevice(device: AudioOutputDevice) { + override fun setAudioOutputDevice(device: AudioOutputDevice) : Boolean{ if (useInternalPlayer) { audioPlayer.setAudioOutputDevice(device) } + return true; } /** diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt index 00db2f2b3..5b1a07d5a 100644 --- a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt @@ -103,5 +103,5 @@ interface ITtsService { * * @param device 音频输出设备类型 */ - fun setAudioOutputDevice(device: AudioOutputDevice) + fun setAudioOutputDevice(device: AudioOutputDevice): Boolean } \ No newline at end of file From a39e80ba57dc9ea3539199dc2f2d468e4da917d1 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 30 Jun 2025 18:50:05 +0800 Subject: [PATCH 6/6] 1 --- .../kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 3c4c6fcb0..46b8bfaba 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -508,7 +508,8 @@ class AzureAsrHelper(private val context: Context) { // 停止音频处理 stopAudioProcessing() - + // 停止录音 + stopMicrophoneCapture() // 尝试强制关闭识别器 try { recognizer?.close()