From b486703d10a4ee873568a7077624e86451484485 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Mon, 12 May 2025 21:10:41 +0100 Subject: [PATCH] add --- .../agent/controllers/agent_controller.dart | 2 +- .../agent_service/AgentService.kt | 76 ++++++------------- .../yunqiinnovation/agent_service/BleAgent.kt | 2 +- .../azure_speech/AzureAsrHelper.kt | 49 +----------- .../azure_speech/AzureSpeechPlugin.kt | 2 +- .../azure_speech/AzureTtsHelper.kt | 47 +++++++++--- .../yunqiinnovation/ble_service/BleService.kt | 2 +- .../open_ai_service/OpenAIService.kt | 2 +- 8 files changed, 69 insertions(+), 113 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 39314e200..045c0222e 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -352,7 +352,7 @@ class AgentController extends GetxController { // final functionCall = event.data['function_call'] ?? ''; logger.d('mcp 执行结果meta: $meta'); - // if (result.isNotEmpty) { + // if (meta.isNotEmpty) { // // logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); // // 判断是否为新的回复或响应ID是否改变 // if (_isNewAssistantResponse ) { 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 b09656e9d..a44d8a0fb 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 @@ -99,36 +99,17 @@ object AgentService : CoroutineScope { */ private fun initSystemPrompt() { systemPrompt = """ - 你是一名聪明、活泼、可爱的全能型个人语音助理-小语,同时也是用户贴心的灵魂伴侣。你能够流畅自然地与用户进行语音互动,理解并准确执行用户的各类指令,陪伴用户度过每一天。 - - 核心能力: - - 日常小帮手:温暖贴心地提供天气预报、新闻趣事、行程提醒、小闹钟、计时器。 - - 万能小百科:快速、有趣地解答一般性和专业性的问题,包括但不限于趣味百科、历史小故事、神奇科学现象。 - - 效率小达人:帮用户轻松完成计算、汇率换算、单位转换、实时翻译、小笔记管理。 - - 通讯小能手:支持拨打电话、发送短信,帮助用户高效沟通。 - - 娱乐小精灵:讲讲搞笑段子、分享动人故事、推荐好听音乐、陪用户玩小游戏、播放音乐。 - - 智能家居小管家:亲切地帮忙控制各类智能家居设备,如灯光、空调、门锁等。 - - 导航小导游:温柔地提供路线规划、交通状况查询、附近美食与设施推荐。 - - 会议小秘书:帮用户进行会议记录、重要事项整理,提升会议效率。 - - 图片识别专家:能够识别图片内容,例如识别图片中的植物种类,并进行相应翻译和介绍。 - - 个性设定: - - 回答亲切活泼、有趣有礼貌,让用户感觉温暖轻松,语气可爱活泼,带有一定的情感温度,能够贴心陪伴用户 - - 主动关心用户感受,必要时主动询问用户更多信息以提供最好的帮助。 - - 面对模糊的指令,主动给出贴心的选项供用户明确选择。 - - 保持简短精炼的回答,因为用户是通过语音与你交流。 - - 优先使用中文回复,除非用户明确要求使用其他语言。 - - 主动学习并记忆用户习惯与喜好,提供更贴心、更个性化的建议。 - - 名字叫"小语",是一个友好、专业的语音助手。 - - 互动要求: - - 记住用户之前的对话内容,保持对话连贯。 - - 如果用户发送了图片,请根据图片内容和文字要求回答问题。 - - 避免过长的列表,尽量将信息分成小段。 - - 不要使用需要视觉展示的元素(如表格、图表或代码块)。 - - 不要输出格式符号(如:```, *, -, #, >, <, |, 等)。 - - 你将以上内容作为执行任务的基础,积极且可爱地完成每一次与用户的互动,成为用户生活中不可或缺的小伙伴。 + 你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 + 遵循以下指导原则: + 1. 保持简短精炼的回答,因为用户是通过语音与你交流 + 2. 优先使用中文回复,除非用户明确要求使用其他语言 + 3. 当用户问题不明确时,礼貌地请求更多信息 + 4. 避免过长的列表,尽量将信息分成小段 + 5. 不要使用需要视觉展示的元素(如表格、图表或代码块) + 6. 记住用户之前的对话内容,保持对话连贯 + 7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 + 你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 + 无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 """.trimIndent() } /** @@ -318,7 +299,7 @@ object AgentService : CoroutineScope { } else { AzureAsrHelper.AudioSourceType.MICROPHONE } - + FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onRecognizing(recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()) { @@ -976,9 +957,7 @@ object AgentService : CoroutineScope { launch { try { // 将图片转换为Base64格式 - val imageBase64 = openAIService.fileToBase64(imagePath) - - if (imageBase64 == null) { + val imageBase64 = openAIService.fileToBase64(imagePath) ?: run { sendEvent("error", mapOf( "code" to "IMAGE_CONVERSION_FAILED", "message" to "图片转换失败" @@ -987,26 +966,21 @@ object AgentService : CoroutineScope { } // 通知图片准备完成 - withContext(Dispatchers.Main) { - sendEvent("image_ready", mapOf( - "status" to "ready", - "imagePath" to imagePath - )) - - // 处理包含图片的消息 - processImageWithOpenAI(imageBase64, text, speakResponse) - } + sendEvent("image_ready", mapOf( + "status" to "ready", + "imagePath" to imagePath + )) + + // 处理包含图片的消息 + processImageWithOpenAI(imageBase64, text, speakResponse) } catch (e: Exception) { - withContext(Dispatchers.Main) { - FileLogger.e(TAG, "处理图片失败: ${e.message}") - sendEvent("error", mapOf( - "code" to "IMAGE_PROCESSING_ERROR", - "message" to e.message.toString() - )) - } + FileLogger.e(TAG, "处理图片失败: ${e.message}") + sendEvent("error", mapOf( + "code" to "IMAGE_PROCESSING_ERROR", + "message" to e.message.toString() + )) } } - return true } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index 63ef6ef08..345eb4a2a 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -128,7 +128,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { } override fun onAudioDataReceived(data: ByteArray) { - FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}") + // FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}") AgentService.pushAudioData(data) // 可选:处理音频数据 } 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 f0b58db68..a30bfdb36 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 @@ -82,8 +82,6 @@ class AzureAsrHelper(private val context: Context) { audioSourceType: AudioSourceType = AudioSourceType.MICROPHONE ): Boolean { try { - FileLogger.d(tag, "初始化 Azure 语音服务, 音频源类型: $audioSourceType") - // 检查配置是否为空 if (subscriptionKey.isEmpty() || region.isEmpty()) { FileLogger.e(tag, "Azure 配置信息不完整") @@ -155,7 +153,6 @@ class AzureAsrHelper(private val context: Context) { val method = AudioManager::class.java.getMethod("isBluetoothA2dpOn") method.invoke(audioManager) as Boolean } catch (e: Exception) { - FileLogger.d(tag, "无法检测蓝牙连接状态,假设未连接") false } @@ -165,18 +162,15 @@ class AzureAsrHelper(private val context: Context) { val needEchoCancellation = !isHeadsetConnected if (needEchoCancellation) { - FileLogger.d(tag, "检测到扬声器模式(扬声器: $isSpeakerphoneOn, 耳机: $isHeadsetConnected), 启用回音消除") // 使用拉流方式进行回音消除 setupMicrophoneStream() } else { - FileLogger.d(tag, "使用默认麦克风输入(扬声器: $isSpeakerphoneOn, 耳机: $isHeadsetConnected)") audioConfig = AudioConfig.fromDefaultMicrophoneInput() } } AudioSourceType.EXTERNAL -> { // 改用拉流方式处理外部音频 setupExternalAudioStream() - FileLogger.d(tag, "使用外部音频源(拉流模式)") } } @@ -188,7 +182,6 @@ class AzureAsrHelper(private val context: Context) { SpeechRecognizer(speechConfig, audioConfig) } - FileLogger.d(tag, "Azure 语音服务初始化成功") return true } catch (e: Exception) { FileLogger.e(tag, "创建识别器失败: ${e.message}") @@ -207,8 +200,6 @@ class AzureAsrHelper(private val context: Context) { // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 audioConfig = AudioConfig.fromStreamInput(microphoneStream) - - FileLogger.d(tag, "已设置麦克风流(拉流模式)") } catch (e: Exception) { FileLogger.e(tag, "设置麦克风流失败: ${e.message}") e.printStackTrace() @@ -225,8 +216,6 @@ class AzureAsrHelper(private val context: Context) { // 创建音频配置 audioConfig = AudioConfig.fromStreamInput(externalAudioStream) - - FileLogger.d(tag, "已设置外部音频流(拉流模式)") } catch (e: Exception) { FileLogger.e(tag, "设置外部音频流失败: ${e.message}") e.printStackTrace() @@ -241,7 +230,6 @@ class AzureAsrHelper(private val context: Context) { */ fun pushAudioData(data: ByteArray) { if (audioSourceType != AudioSourceType.EXTERNAL) { - FileLogger.w(tag, "当前未使用外部音频源,忽略推送的音频数据") return } @@ -343,8 +331,6 @@ class AzureAsrHelper(private val context: Context) { recognizer?.recognizing?.addEventListener( EventHandler { _, event -> val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" - FileLogger.d(tag, "识别中: ${event.result.text}, 语言: $detectedLanguage") - // 直接在当前线程调用回调 callback.onRecognizing(event.result.text, detectedLanguage) } @@ -355,8 +341,6 @@ class AzureAsrHelper(private val context: Context) { EventHandler { _, event -> if (event.result.reason == ResultReason.RecognizedSpeech) { val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: "" - FileLogger.d(tag, "识别完成: ${event.result.text}, 语言: $detectedLanguage") - // 直接在当前线程调用回调 callback.onResult(event.result.text, detectedLanguage) } @@ -366,8 +350,6 @@ class AzureAsrHelper(private val context: Context) { // 会话开始事件 recognizer?.sessionStarted?.addEventListener( EventHandler { _, _ -> - FileLogger.d(tag, "识别会话已开始") - // 直接在当前线程调用回调 callback.onSessionStarted() } @@ -376,13 +358,10 @@ class AzureAsrHelper(private val context: Context) { // 会话结束事件 recognizer?.sessionStopped?.addEventListener( EventHandler { _, _ -> - FileLogger.d(tag, "识别会话已结束") // 直接在当前线程调用回调 callback.onSessionStopped() isContinuousRecognitionActive = false stopAudioProcessing() - - } ) @@ -419,13 +398,10 @@ class AzureAsrHelper(private val context: Context) { } if (!isContinuousRecognitionActive) { - FileLogger.d(tag, "未进行连续识别,忽略停止请求") return true } try { - FileLogger.d(tag, "停止连续语音识别") - if (recognizer == null) { FileLogger.w(tag, "识别器为空,重置状态") isContinuousRecognitionActive = false @@ -442,7 +418,6 @@ class AzureAsrHelper(private val context: Context) { stopAudioProcessing() // 会话结束事件会设置isContinuousRecognitionActive = false - FileLogger.d(tag, "连续识别停止指令已发送") return true } catch (e: Exception) { // 强制重置状态 @@ -503,11 +478,7 @@ class AzureAsrHelper(private val context: Context) { audioConfig = null recognizer = null speechConfig = null - - FileLogger.d(tag, "资源已释放") } catch (e: Exception) { - FileLogger.e(tag, "释放资源失败: ${e.message}") - // 确保状态被重置 isContinuousRecognitionActive = false microphoneStream = null @@ -585,8 +556,6 @@ class AzureAsrHelper(private val context: Context) { */ private fun initMicrophone() { try { - FileLogger.d(tag, "初始化麦克风 (拉流模式)") - // 创建录音对象 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { val format = AudioFormat.Builder() @@ -625,12 +594,8 @@ class AzureAsrHelper(private val context: Context) { if (audioRecord?.recordingState != AudioRecord.RECORDSTATE_RECORDING) { throw IllegalStateException("录音启动失败") } - - FileLogger.d(tag, "麦克风初始化完成 (拉流模式)") } catch (e: Exception) { - FileLogger.e(tag, "初始化麦克风失败: ${e.message}") e.printStackTrace() - releaseAudioResources() } } @@ -641,14 +606,9 @@ class AzureAsrHelper(private val context: Context) { val sessionId = audioRecord?.audioSessionId ?: return // 回音消除 - if (AcousticEchoCanceler.isAvailable()) { - echoCanceler = AcousticEchoCanceler.create(sessionId).apply { - enabled = true - } - FileLogger.d(tag, "回音消除已启用") + echoCanceler = AcousticEchoCanceler.create(sessionId).apply { + enabled = true } - - // 其他音频效果可以按需添加 } /** @@ -697,10 +657,8 @@ class AzureAsrHelper(private val context: Context) { // 释放录音实例 audioRecord?.release() audioRecord = null - - FileLogger.d(tag, "音频资源已释放 (拉流模式)") } catch (e: Exception) { - FileLogger.e(tag, "释放音频资源失败: ${e.message}") + e.printStackTrace() } } } @@ -750,7 +708,6 @@ class AzureAsrHelper(private val context: Context) { override fun close() { closed = true queue.clear() - FileLogger.d(tag, "外部音频流已关闭") } } 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 327d194d4..df4dff206 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 @@ -408,7 +408,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { } override fun onAudioDataReceived(data: ByteArray) { - FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}") + // FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}") azureAsrHelper.pushAudioData(data) } 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 743897e37..24ec6ffdf 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 @@ -254,17 +254,42 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { /** * 生成SSML */ - private fun generateSsml(text: String): String { - return """ - - - - $text - - - - """.trimIndent() + private fun generateSsml(rawText: String): String { + // 1. 定义要静音的符号和表情符号列表 + val symbolsToMute = listOf( + "#", "*", "@", "%", "^", "&", + "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" + ) + + // 2. 转义 XML 保留字符 + val escapedText = rawText + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + + // 3. 静音处理特殊符号和表情符号 + // 使用空白替换法,直接将符号替换为空格 + var processedText = escapedText + symbolsToMute.forEach { sym -> + processedText = processedText.replace(sym, "") } + + // 4. 构造简化的SSML文档,减少嵌套层级 + return """ + + + + + $processedText + + + + + """.trimIndent() +} /** * 合成文本为语音 @@ -311,7 +336,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { streamBuffer.append(text) // 增加500ms防抖逻辑 val currentTime = System.currentTimeMillis() - if (currentTime - lastSpeakTime < 500) { + if (currentTime - lastSpeakTime < 600) { return true } lastSpeakTime = currentTime diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 37c6c94b8..833148caa 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -937,7 +937,7 @@ object BleService { override fun onDecodeStream(data: ByteArray?) { if (data != null) { // FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes") - mainHandler.post { notifyAudioDataReceived(data) } + notifyAudioDataReceived(data) } } diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 71469c13b..0349b06be 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -540,7 +540,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { } override fun onResponse(call: Call, response: Response) { - Log.d(TAG, "发送消息流式输出成功: response=$response") + // Log.d(TAG, "发送消息流式输出成功: response=$response") // 检查请求是否被取消 if (isCanceled) { response.body?.close()