diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index ef8cf5ac1..26fb7a46d 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -577,7 +577,38 @@ class AgentController extends GetxController { // messages.refresh(); } break; + //识别取消(网络波动会触发) + case AgentServiceEventType.recognitionCanceled: + // 查找是否有正在识别中的消息 + final index = + messages.lastIndexWhere((msg) => msg.isRecognizing && msg.isUser); + + if (currentText.value.isNotEmpty) { + if (index >= 0) { + // 将识别中的消息转换为最终消息 + messages[index] = Message( + isUser: true, + text: currentText.value, + isRecognizing: false, + timestamp: messages[index].timestamp, + ); + // messages.refresh(); + } else { + // 如果没有找到识别中的消息,添加新消息 + messages.add(Message( + isUser: true, text: currentText.value, isRecognizing: false)); + } + isProcessing.value = true; + // 标记为新的AI回复 + _isNewAssistantResponse = true; + // 使用reverse:true时通常不需要手动滚动 + } else if (index >= 0) { + // 如果识别结果为空,移除临时的识别消息 + messages.removeAt(index); + // messages.refresh(); + } + break; case AgentServiceEventType.ttsStarted: isSpeaking.value = true; print('TTS开始播放'); 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 648b37238..da4f1a166 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 @@ -588,7 +588,7 @@ object AgentService : CoroutineScope { _hasSpeechDetected.set(true) - + Log.d(TAG, "ai识别中重置定时器") restartIdleCheck() @@ -672,7 +672,6 @@ object AgentService : CoroutineScope { "details" to errorDetails ) ) - recognizeCallback() } override fun onError(code: Int, error: String) { @@ -686,7 +685,6 @@ object AgentService : CoroutineScope { "message" to error ) ) - recognizeCallback() } }) return true 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 c9a229720..a2837aa39 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 @@ -50,8 +50,17 @@ class AzureAsrHelper(private val context: Context) { // 网络状态监听 private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor(context) private var isNetworkRecovering = false + + // 网络丢失计数器 + private var networkLostCount = 0 + // 最大网络丢失次数(3秒检测) + private val maxNetworkLostCount = 3 + + // 防抖机制相关变量 + private var lastNetworkLostTime = 0L + private val networkLostDebounceInterval = 3000L // 2秒防抖间隔 - + /** * 音频来源类型 @@ -221,61 +230,6 @@ class AzureAsrHelper(private val context: Context) { } - /** - * 执行一次性语音识别 - * - * @param callback 识别结果回调 - */ - fun recognizeOnce( - callback: RecognizeCallback, - audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE - ) { - if (speechConfig == null) { - callback.onError(1002,"语音服务未初始化") - return - } - - this.audioSourceType = audioSourceType - // 确保不在连续识别中 - if (isContinuousRecognitionActive) { - stopContinuousRecognition() - } - - try { - // 启动音频处理(若已启动则跳过) - if (!audioStream!!.isWriting()) { - audioStream!!.startAudioRecord( - when (audioSourceType) { - AudioSourceType.MICROPHONE -> SimpleAudioReceiver.AudioSourceType.MICROPHONE - AudioSourceType.EXTERNAL -> SimpleAudioReceiver.AudioSourceType.EXTERNAL - }, - null - ) - } else { - Log.d(tag, "音频已启动,跳过重复启动") - } - - // 执行同步识别 - val result = recognizer?.recognizeOnceAsync()?.get() - - // 停止音频处理 - stopAudioProcessing() - - if (result != null && result.reason == ResultReason.RecognizedSpeech) { - val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language - ?: supportedLanguages[0] - callback.onResult(result.text, detectedLanguage) - } else { - callback.onError(1004,"未能识别语音") - } - } catch (e: Exception) { - stopAudioProcessing() - callback.onError(1003,"识别异常: ${e.message}") - } - } - - - /** * 开始连续语音识别 * @param audioSourceType 音频源类型 @@ -300,11 +254,14 @@ class AzureAsrHelper(private val context: Context) { return true } if (!isRecognizerValid()) { - Log.w(tag, "识别器已失效,正在重新创建...") - if (!setupRecognizer()) { - Log.e(tag, "重新创建识别器失败") + Log.d(tag, "重新启动连续识别") + val callback = continuousCallback + if (callback == null) { + Log.e(tag, "无法重新启动连续识别:回调为空") + continuousCallback?.onError(1001,"回调为空") return false } + setupEventListeners(callback) } this.audioSourceType = audioSourceType @@ -330,7 +287,7 @@ class AzureAsrHelper(private val context: Context) { return true } catch (e: Exception) { Log.e(tag, "开始连续识别失败: ${e.message}") - audioStream!!.stopMicrophoneCapture() + stopContinuousRecognition() isContinuousRecognitionActive = false return false } @@ -399,10 +356,8 @@ class AzureAsrHelper(private val context: Context) { // 直接在当前线程调用回调 Log.d(tag, "会话结束事件") if (audioSourceType == AudioSourceType.EXTERNAL) { - callback.onSessionStopped() isContinuousRecognitionActive = false - // stopAudioProcessing() } } ) @@ -414,20 +369,7 @@ class AzureAsrHelper(private val context: Context) { val reason = event.reason.toString() Log.d(tag, "识别被取消: reason=$reason, details=$errorDetails") - - // 检查是否是网络相关错误 - if (isNetworkRelatedError(reason, errorDetails)) { - Log.d(tag, "检测到网络相关错误,停止音频处理") - stopAudioProcessingImmediately() - - // 如果不是正在恢复网络,通知上层网络错误 - if (!isNetworkRecovering) { - callback.onError(1000,"启动连续识别失败,网络连接已断开") - } - } else { - // 非网络错误,正常处理 - callback.onCanceled(reason, errorDetails) - } + callback.onCanceled(reason,errorDetails) } ) } catch (e: Exception) { @@ -448,41 +390,28 @@ class AzureAsrHelper(private val context: Context) { Log.e(tag, "语音服务未初始化") return false } - - // if (!isContinuousRecognitionActive) { - // return true - // } - try { - - isContinuousRecognitionActive = false - - if (audioSourceType == AudioSourceType.EXTERNAL) { - //pushAudioData(ByteArray(0)) - } Log.d(tag, "停止连续语音识别: ") + // 停止音频处理 + audioStream!!.stopMicrophoneCapture() // 直接停止连续识别(SDK内部已是异步操作) recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) - - // 停止音频处理 - audioStream!!.stopMicrophoneCapture() - // 会话结束事件会设置isContinuousRecognitionActive = false + + recognizer?.close() + recognizer = null + isContinuousRecognitionActive = false return true } catch (e: Exception) { // 强制重置状态 isContinuousRecognitionActive = false Log.e(tag, "停止连续识别失败: ${e.message}") - recognizer?.stopContinuousRecognitionAsync() - // 停止音频处理 + // 停止音频处理 audioStream!!.stopMicrophoneCapture() - // 尝试强制关闭识别器 - // try { - // recognizer?.close() - // recognizer = null - // } catch (ex: Exception) { - // Log.e(tag, "关闭识别器失败: ${ex.message}") - // } - + recognizer?.stopContinuousRecognitionAsync() + + recognizer?.close() + recognizer = null + isContinuousRecognitionActive = false return false } } @@ -503,12 +432,10 @@ class AzureAsrHelper(private val context: Context) { recognizer?.stopContinuousRecognitionAsync() isContinuousRecognitionActive = false } - // recognizer?.stopContinuousRecognitionAsync()?.get(1000, TimeUnit.MILLISECONDS) // 停止音频处理 stopAudioProcessing() - // 清理网络监听 - networkMonitor.dispose() - + // 清理网络监听 + clearNetworkDetection() // 停止录音 if (audioStream!!.recordfile != null) { audioStream!!.recordfile!!.closeFile(true) @@ -521,24 +448,15 @@ class AzureAsrHelper(private val context: Context) { speechConfig?.close() speechConfig = null - // 释放音频配置 audioConfig?.close() audioConfig = null - // 确保状态被重置 isContinuousRecognitionActive = false - //microphoneStream = null - audioConfig = null - recognizer = null - speechConfig = null } catch (e: Exception) { // 确保状态被重置 - isContinuousRecognitionActive = false - //microphoneStream = null - audioConfig = null recognizer = null speechConfig = null @@ -743,23 +661,48 @@ class AzureAsrHelper(private val context: Context) { * 处理网络连接可用 */ private fun handleNetworkAvailable() { - if (!isNetworkRecovering) { + // 网络恢复时重置计数器 + networkLostCount = 0 + lastNetworkLostTime = 0L + if (!isNetworkRecovering) { isNetworkRecovering = true Log.d(tag, "网络恢复,准备重新启动识别") - - // 如果之前在进行连续识别,重新启动 - Handler(Looper.getMainLooper()).postDelayed({ - restartContinuousRecognition() - }, 1000) // 延迟1秒确保网络稳定 } } /** - * 处理网络连接丢失 + * 处理网络连接丢失(带防抖机制) */ private fun handleNetworkLost() { - Log.d(tag, "网络断开,停止音频处理") + val currentTime = System.currentTimeMillis() + + // 防抖机制:如果距离上次触发时间小于防抖间隔,则忽略本次触发 + if (currentTime - lastNetworkLostTime < networkLostDebounceInterval) { + Log.d(tag, "网络丢失事件被防抖过滤,距离上次触发仅${currentTime - lastNetworkLostTime}ms") + return + } + + // 更新最后触发时间 + lastNetworkLostTime = currentTime + + + //这里启动一个3秒定时器,3秒后检查网络是否恢复 + // 启动3秒定时器,3秒后检查网络是否恢复 + Handler(Looper.getMainLooper()).postDelayed({ + Log.e(tag, "网络不可用") + val isNetworkAvailable = checkNetworkStatus() + if (isNetworkAvailable) { + // 如果网络恢复,重置计数器和防抖时间 + Log.d(tag, "网络已恢复,重置计数器") + lastNetworkLostTime = 0L + isNetworkRecovering = true + } else { + continuousCallback?.onError(1000,"网络连接不可用,请检查网络设置") + Log.d(tag,"网络仍未恢复,继续检测") + networkLostCount = 0 stopAudioProcessingImmediately() + } + }, 3000L) // 3秒延时 } /** @@ -774,30 +717,16 @@ class AzureAsrHelper(private val context: Context) { Log.e(tag, "立即停止音频处理失败: ${e.message}") } } - - /** - * 重新启动连续识别(网络恢复时使用) - */ - private fun restartContinuousRecognition() { - try { - Log.d(tag, "重新启动连续识别") - // 检查continuousCallback是否为空 - val callback = continuousCallback - if (callback == null) { - Log.e(tag, "无法重新启动连续识别:回调为空") - return - } - - setupEventListeners(callback) - isNetworkRecovering = false - Log.d(tag, "连续识别重新启动成功") - } catch (e: Exception) { - Log.e(tag, "重新启动连续识别失败: ${e.message}") - isNetworkRecovering = false - } - } - - +/** + * 清理网络检测相关资源 + */ +private fun clearNetworkDetection() { + + networkLostCount = 0 + lastNetworkLostTime = 0L } +} + + \ 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 2dcd4ff58..e568d9ac3 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 @@ -365,37 +365,6 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { } } - "recognizeOnce" -> { - val useExternalAudio = call.argument("useExternalAudio") ?: false - try { - FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") // - // 选择音频源类型 - val audioSourceType = if (useExternalAudio) { - AzureAsrHelper.AudioSourceType.EXTERNAL - } else { - AzureAsrHelper.AudioSourceType.MICROPHONE - } - // 直接使用同步方式进行一次性识别 - azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback { - override fun onResult(text: String, detectedLanguage: String) { - // 直接返回结果,不发送事件 - result.success( - mapOf( - "text" to text, - "detectedLanguage" to detectedLanguage - ) - ) - } - - override fun onError(code: Int, error: String) { - result.error(code.toString(), "错误码: $code, 错误信息: $error", null) - } - }, audioSourceType) - } catch (e: Exception) { - result.error("RECOGNITION_ERROR", "识别过程异常: ${e.message}", null) - } - } - "disableBluetoothAudio" -> { azureAsrHelper.audioStream?.disableBluetoothAudio(); diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt index c754e2e05..5ad74f4b9 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt @@ -85,8 +85,7 @@ class SimpleAudioReceiver(private val context: Context) { * 初始化音频录制 */ fun initAudioRecord() { - val format = getOptimalAudioFormat() - pushAudioStream = AudioInputStream.createPushStream(format) + isRunning.set(true) Log.d(TAG, "initAudioRecord: ${isRunning}") startWriteThread() // 再启动数据读取线程 @@ -143,8 +142,9 @@ class SimpleAudioReceiver(private val context: Context) { if(audioDataCallback!=null){ audioDataCallback?.onAudio(finalData) } + if(pushAudioStream!=null){ pushAudioStream?.write(finalData) - + } if(recordfile!=null){ recordfile?.saveAudioDataToWav(finalData) } @@ -184,6 +184,8 @@ class SimpleAudioReceiver(private val context: Context) { * @param callback 音频数据回调,可为空 */ fun startAudioRecord(audioSourceType: AudioSourceType, callback: AudioDataCallback?) { + val format = getOptimalAudioFormat() + pushAudioStream = AudioInputStream.createPushStream(format) this.audioSourceType = audioSourceType this.audioDataCallback = callback writeQueue.clear() @@ -238,6 +240,8 @@ class SimpleAudioReceiver(private val context: Context) { try { // 首先设置通话音频模式 //setupCommunicationAudioMode() + + val supportedSampleRates = intArrayOf(16000, 8000, 11025, 22050, 44100) val sampleRate = supportedSampleRates.firstOrNull { rate -> val bufferSize = AudioRecord.getMinBufferSize(rate, channelConfig, audioFormat) @@ -274,7 +278,7 @@ class SimpleAudioReceiver(private val context: Context) { } audioRecord?.startRecording() - + Log.d("TAG", "通话模式录音开始,采样率: $sampleRate Hz") } catch (e: Exception) { @@ -312,8 +316,7 @@ class SimpleAudioReceiver(private val context: Context) { //setupCommunicationAudioMode() // 停止录音 audioRecord?.startRecording() - - + } catch (e: Exception) { Log.e(TAG, "继续录音失败: ${e.message}") } @@ -328,7 +331,11 @@ class SimpleAudioReceiver(private val context: Context) { // 停止录音 audioRecord?.stop() - + + if(pushAudioStream != null){ + pushAudioStream?.close() + pushAudioStream = null + } // 恢复音频模式 这里会导致关闭麦克风卡住待优化 //restoreCommunicationAudioMode()