From daa9e055b40e070f58303195ae7eb240e5ef6217 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Sun, 9 Mar 2025 09:49:47 +0000 Subject: [PATCH] service --- .../com/example/deep_voice/AzureAsrHelper.kt | 48 +- .../com/example/deep_voice/MainActivity.kt | 75 +-- .../deep_voice/VoiceInteractionService.kt | 566 ++++++++++++++---- .../example/deep_voice/VolcanoAIService.kt | 4 +- lib/core/bindings/initial_binding.dart | 9 +- lib/data/services/config_service.dart | 57 -- .../services/voice_interaction_service.dart | 70 +++ lib/main.dart | 5 + .../home/controllers/home_controller.dart | 81 +++ lib/modules/home/views/home_view.dart | 62 +- macos/Podfile.lock | 7 + 11 files changed, 767 insertions(+), 217 deletions(-) delete mode 100644 lib/data/services/config_service.dart create mode 100644 lib/data/services/voice_interaction_service.dart diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt index 06be92ede..185b9b4dd 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt @@ -19,6 +19,14 @@ class AzureAsrHelper { // 初始化 SDK fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { try { + Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, language=$language") + + // 检查配置是否为空 + if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) { + Log.e(TAG, "Azure 配置信息不完整: subscriptionKey=${subscriptionKey.isEmpty()}, serviceRegion=${serviceRegion.isEmpty()}") + return false + } + this.subscriptionKey = subscriptionKey this.serviceRegion = serviceRegion this.currentLanguage = language @@ -31,13 +39,22 @@ class AzureAsrHelper { speechConfig?.speechRecognitionLanguage = language // 创建音频配置 - audioConfig = AudioConfig.fromDefaultMicrophoneInput() + Log.d(TAG, "创建默认麦克风音频配置") + try { + audioConfig = AudioConfig.fromDefaultMicrophoneInput() + Log.d(TAG, "成功创建音频配置") + } catch (e: Exception) { + Log.e(TAG, "创建音频配置失败: ${e.message}", e) + return false + } // 创建识别器 + Log.d(TAG, "创建语音识别器") recognizer = SpeechRecognizer(speechConfig, audioConfig) + Log.d(TAG, "Azure 语音服务初始化成功") return true } catch (e: Exception) { - Log.e(TAG, "初始化失败: ${e.message}") + Log.e(TAG, "初始化失败: ${e.message}", e) return false } } @@ -70,26 +87,43 @@ class AzureAsrHelper { // 开始连续语音识别 fun startContinuousRecognition(language: String? = null, callback: ContinuousRecognizeCallback): Boolean { if (recognizer == null) { + Log.e(TAG, "SpeechRecognizer 未初始化") callback.onError("SpeechRecognizer 未初始化") return false } // 如果已经在进行连续识别,先停止 if (isContinuousRecognitionActive) { + Log.d(TAG, "已经在进行连续识别,先停止") stopContinuousRecognition(callback) } // 如果指定了新的语言,需要重新初始化 if (language != null && language != currentLanguage) { + Log.d(TAG, "语言变更,重新初始化: $language") initialize(subscriptionKey, serviceRegion, language) } try { // 先发送会话开始事件 + Log.d(TAG, "准备开始连续语音识别") callback.onSessionStarted() // 重新创建识别器 + Log.d(TAG, "重新创建识别器") recognizer?.close() + + try { + // 重新创建音频配置 + audioConfig?.close() + audioConfig = AudioConfig.fromDefaultMicrophoneInput() + Log.d(TAG, "重新创建音频配置成功") + } catch (e: Exception) { + Log.e(TAG, "重新创建音频配置失败: ${e.message}", e) + callback.onError("创建音频配置失败: ${e.message}") + return false + } + recognizer = SpeechRecognizer(speechConfig, audioConfig) // 设置识别事件处理 @@ -98,7 +132,10 @@ class AzureAsrHelper { recognizer.recognized.addEventListener( EventHandler { _, event -> if (event.result.reason == ResultReason.RecognizedSpeech) { + Log.d(TAG, "识别结果: ${event.result.text}") callback.onResult(event.result.text) + } else { + Log.d(TAG, "识别结果原因: ${event.result.reason}") } } ) @@ -107,6 +144,7 @@ class AzureAsrHelper { recognizer.recognizing.addEventListener( EventHandler { _, event -> if (event.result.reason == ResultReason.RecognizingSpeech) { + Log.d(TAG, "正在识别: ${event.result.text}") callback.onRecognizing(event.result.text) } } @@ -115,6 +153,7 @@ class AzureAsrHelper { // 会话开始事件 recognizer.sessionStarted.addEventListener( EventHandler { _, _ -> + Log.d(TAG, "会话开始") isContinuousRecognitionActive = true callback.onSessionStarted() } @@ -123,6 +162,7 @@ class AzureAsrHelper { // 会话结束事件 recognizer.sessionStopped.addEventListener( EventHandler { _, _ -> + Log.d(TAG, "会话结束") isContinuousRecognitionActive = false callback.onSessionStopped() } @@ -132,18 +172,22 @@ class AzureAsrHelper { recognizer.canceled.addEventListener( EventHandler { _, event -> val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else "" + Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails") callback.onCanceled(event.reason.toString(), errorDetails) isContinuousRecognitionActive = false } ) // 开始连续识别 + Log.d(TAG, "开始连续识别") recognizer.startContinuousRecognitionAsync().get() + Log.d(TAG, "连续识别已启动") isContinuousRecognitionActive = true } return true } catch (e: Exception) { + Log.e(TAG, "开始连续识别失败: ${e.message}", e) callback.onError("开始连续识别失败: ${e.message}") isContinuousRecognitionActive = false return false diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index 1268ec35c..b9815af34 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -15,7 +15,7 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugins.GeneratedPluginRegistrant import io.flutter.embedding.android.FlutterActivity import android.content.Context -import android.content.SharedPreferences +import android.content.ComponentName class MainActivity: FlutterActivity() { @@ -24,22 +24,27 @@ class MainActivity: FlutterActivity() { private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events" private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts" private val VOICE_INTERACTION_CHANNEL = "com.deep_voice.voice_interaction" - private val CONFIG_CHANNEL = "com.example.deep_voice/config" private val TAG = "MainActivity" private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureTtsHelper: AzureTtsHelper private var azureAsrEventSink: EventChannel.EventSink? = null - private lateinit var sharedPreferences: SharedPreferences + + // 使用伴生对象存储配置信息,使其在整个应用中可访问 + companion object { + // Azure 语音服务配置 + var azureSpeechKey: String = "" + var azureSpeechRegion: String = "" + var volcanoAiApiKey: String = "" + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() - sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) - // 自动启动语音交互服务 - startVoiceInteractionService() + // 不再自动启动服务,由用户通过 Flutter 界面手动启动 + Log.d(TAG, "MainActivity 已创建,但不会自动启动 VoiceInteractionService") } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { @@ -281,6 +286,22 @@ class MainActivity: FlutterActivity() { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "startVoiceInteractionService" -> { + // 获取配置参数 + val azureSpeechKey = call.argument("azure_speech_key") ?: "" + val azureSpeechRegion = call.argument("azure_speech_region") ?: "" + val volcanoAiApiKey = call.argument("volcano_ai_api_key") ?: "" + + // 保存配置到静态变量 + MainActivity.azureSpeechKey = azureSpeechKey + MainActivity.azureSpeechRegion = azureSpeechRegion + MainActivity.volcanoAiApiKey = volcanoAiApiKey + + // 打印日志 + Log.d(TAG, "已保存配置到静态变量:") + Log.d(TAG, "azure_speech_key: ${if (azureSpeechKey.isNotEmpty()) azureSpeechKey.take(5) + "..." else "空"}") + Log.d(TAG, "azure_speech_region: $azureSpeechRegion") + Log.d(TAG, "volcano_ai_api_key: ${if (volcanoAiApiKey.isNotEmpty()) volcanoAiApiKey.take(5) + "..." else "空"}") + startVoiceInteractionService() result.success(true) } @@ -296,35 +317,6 @@ class MainActivity: FlutterActivity() { } } } - - // 设置 MethodChannel 处理程序 - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CONFIG_CHANNEL).setMethodCallHandler { call, result -> - when (call.method) { - "setConfig" -> { - val azureSpeechKey = call.argument("azure_speech_key") ?: "" - val azureSpeechRegion = call.argument("azure_speech_region") ?: "" - val volcanoAiApiKey = call.argument("volcano_ai_api_key") ?: "" - - // 保存到 SharedPreferences - val editor = sharedPreferences.edit() - editor.putString("flutter.azure_speech_key", azureSpeechKey) - editor.putString("flutter.azure_speech_region", azureSpeechRegion) - editor.putString("flutter.volcano_ai_api_key", volcanoAiApiKey) - editor.apply() - - // 打印日志 - println("已保存配置到原生 SharedPreferences:") - println("azure_speech_key: $azureSpeechKey") - println("azure_speech_region: $azureSpeechRegion") - println("volcano_ai_api_key: $volcanoAiApiKey") - - result.success(true) - } - else -> { - result.notImplemented() - } - } - } } // ASR 事件发送方法 @@ -358,8 +350,21 @@ class MainActivity: FlutterActivity() { * 启动语音交互服务 */ private fun startVoiceInteractionService() { + Log.d(TAG, "准备启动语音交互服务") val serviceIntent = Intent(this, VoiceInteractionService::class.java) + + // 添加 ACTION_MEDIA_BUTTON 动作,确保服务能够接收媒体按钮事件 + serviceIntent.action = Intent.ACTION_MEDIA_BUTTON + + // 确保明确设置组件 + serviceIntent.component = ComponentName(packageName, VoiceInteractionService::class.java.name) + + // 输出配置信息 + Log.d(TAG, "启动服务时的配置信息: subscriptionKey=${if (azureSpeechKey.isNotEmpty()) azureSpeechKey.take(5) + "..." else "空"}, serviceRegion=$azureSpeechRegion") + + Log.d(TAG, "启动服务: $serviceIntent") startService(serviceIntent) + Log.d(TAG, "语音交互服务启动完成") } /** diff --git a/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt b/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt index 6e442c182..278453e53 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt @@ -18,6 +18,8 @@ import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.content.ComponentName import android.media.AudioManager +import android.os.Handler +import android.os.Looper import java.util.concurrent.atomic.AtomicBoolean import org.json.JSONArray @@ -41,15 +43,50 @@ class VoiceInteractionService : Service() { fun isServiceRunning(): Boolean { return isRunning.get() } + + // 语音识别超时时间(毫秒) + private const val RECOGNITION_TIMEOUT = 5000L + + // 服务状态变化广播 Action + const val ACTION_SERVICE_STATUS_CHANGED = "com.example.deep_voice.ACTION_SERVICE_STATUS_CHANGED" + const val EXTRA_SERVICE_STATUS = "service_status" + const val STATUS_STARTED = "started" + const val STATUS_STOPPED = "stopped" + + // 服务状态监听器 + private var serviceStatusListener: ServiceStatusListener? = null + + // 设置服务状态监听器 + fun setServiceStatusListener(listener: ServiceStatusListener) { + serviceStatusListener = listener + } + + // 移除服务状态监听器 + fun removeServiceStatusListener() { + serviceStatusListener = null + } + + // 服务状态监听接口 + interface ServiceStatusListener { + fun onServiceStarted() + fun onServiceStopped() + } } private lateinit var mediaSession: MediaSessionCompat - private var isRecording = false - private var isProcessing = false + private var isActive = false // 合并 isRecording 和 isProcessing 为一个状态 private var lastKeyEventTime = 0L private var keyEventCount = 0 private lateinit var audioManager: AudioManager + // 超时处理 + private val handler = Handler(Looper.getMainLooper()) + private var timeoutRunnable: Runnable? = null + private var lastActivityTime = 0L + private var isTtsSpeaking = false + + + // Azure服务 private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureTtsHelper: AzureTtsHelper @@ -67,77 +104,112 @@ class VoiceInteractionService : Service() { override fun onCreate() { super.onCreate() - Log.d(TAG, "onCreate") + Log.d(TAG, "onCreate - 语音交互服务正在创建") isRunning.set(true) + isActive = false // 确保初始状态为 false + isTtsSpeaking = false // 确保初始状态为 false // 获取 AudioManager audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - // 1. 初始化Azure服务 - initAzureServices() - // 2. 初始化火山AI服务 - volcanoAIService = VolcanoAIService(applicationContext) + // 初始化服务 + initServices() - // 3. 初始化MediaSession + // 初始化 MediaSession 和按键监听 initMediaSession() - // 4. 启动前台通知(Foreground Service) + // 启动前台通知(Foreground Service) startForegroundServiceWithNotification() - // 5. 注册成为媒体按钮接收器 + // 注册成为媒体按钮接收器 registerMediaButtonReceiver() - // 6. 测试按键事件 - testKeyEvent() + // 发送服务已启动的广播 + sendServiceStatusBroadcast(STATUS_STARTED) + + // 通知监听器服务已启动 + serviceStatusListener?.onServiceStarted() + + Log.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...") } /** - * 测试按键事件,模拟一个按键事件以验证处理逻辑 + * 测试按键事件 */ private fun testKeyEvent() { - Log.d(TAG, "测试按键事件处理") + Log.d(TAG, "测试按键事件") + + // 创建一个模拟的按键事件 val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK) + + // 创建一个媒体按钮意图 val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { putExtra(Intent.EXTRA_KEY_EVENT, keyEvent) component = ComponentName(packageName, VoiceInteractionService::class.java.name) } - // MediaButtonReceiver.handleIntent 返回的是 KeyEvent 类型,而不是 Boolean + // 发送给 MediaSession 处理 MediaButtonReceiver.handleIntent(mediaSession, mediaButtonIntent) - Log.d(TAG, "测试按键事件已发送") - // 直接处理按键事件 + // // 直接处理按键事件 handleKeyEvent(keyEvent) + + Log.d(TAG, "测试按键事件已发送") + } + + /** + * 初始化所有服务 + */ + private fun initServices() { + Log.d(TAG, "初始化所有服务") + + // 1. 初始化Azure服务 + initAzureServices() + + // 2. 初始化火山AI服务 + volcanoAIService = VolcanoAIService(applicationContext) } /** * 处理按键事件 */ private fun handleKeyEvent(keyEvent: KeyEvent) { + Log.d(TAG, "处理按键事件开始: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}") + if (keyEvent.action == KeyEvent.ACTION_DOWN) { when (keyEvent.keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { - Log.d(TAG, "处理按键事件: ${keyEvent.keyCode}") + Log.d(TAG, "处理媒体按键事件: ${keyEvent.keyCode}") // 检测双击 val currentTime = System.currentTimeMillis() - if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键 + val timeDiff = currentTime - lastKeyEventTime + Log.d(TAG, "按键时间差: $timeDiff ms (上次: $lastKeyEventTime, 当前: $currentTime)") + + if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 keyEventCount++ + Log.d(TAG, "连续按键计数: $keyEventCount") + if (keyEventCount == 1) { // 这是第二次按键,即双击 Log.d(TAG, "检测到双击,开始语音交互") handleDoubleClick() keyEventCount = 0 } } else { + Log.d(TAG, "单击事件,重置计数") keyEventCount = 0 } lastKeyEventTime = currentTime } + else -> { + Log.d(TAG, "未处理的按键代码: ${keyEvent.keyCode}") + } } + } else { + Log.d(TAG, "忽略非按下事件: ${keyEvent.action}") } } @@ -157,24 +229,30 @@ class VoiceInteractionService : Service() { azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() - // 从应用的SharedPreferences获取Azure配置 - val sharedPreferences = applicationContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) - val subscriptionKey = sharedPreferences.getString("flutter.azure_speech_key", "") ?: "" - val serviceRegion = sharedPreferences.getString("flutter.azure_speech_region", "") ?: "" + // 从 MainActivity 的静态变量获取 Azure 配置 + val subscriptionKey = MainActivity.azureSpeechKey + val serviceRegion = MainActivity.azureSpeechRegion - Log.d(TAG, "Azure配置信息: subscriptionKey=$subscriptionKey, serviceRegion=$serviceRegion") + Log.d(TAG, "Azure配置信息: subscriptionKey=${if (subscriptionKey.isNotEmpty()) subscriptionKey.take(5) + "..." else "空"}, serviceRegion=${if (serviceRegion.isNotEmpty()) serviceRegion else "空"}") if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { // 初始化ASR - azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") + val asrInitResult = azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") + Log.d(TAG, "ASR初始化结果: $asrInitResult") // 初始化TTS - azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") + val ttsInitResult = azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") + Log.d(TAG, "TTS初始化结果: $ttsInitResult") + + if (!asrInitResult || !ttsInitResult) { + Log.e(TAG, "Azure服务初始化失败,语音交互功能可能无法正常工作") + } } else { Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务") + } } - + /** * 初始化 MediaSession 以捕获蓝牙耳机按键 */ @@ -202,6 +280,8 @@ class VoiceInteractionService : Service() { // 监听回调 mediaSession.setCallback(object : MediaSessionCompat.Callback() { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { + Log.d(TAG, "MediaSession.onMediaButtonEvent: $mediaButtonEvent") + // 根据 Android 版本使用不同的方式获取 KeyEvent val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) @@ -213,7 +293,7 @@ class VoiceInteractionService : Service() { Log.d(TAG, "收到媒体按钮事件: $keyEvent") if (keyEvent != null) { - Log.d(TAG, "按键代码: ${keyEvent.keyCode}, 动作: ${keyEvent.action}") + Log.d(TAG, "按键详情: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}, 时间=${keyEvent.eventTime}") if (keyEvent.action == KeyEvent.ACTION_DOWN) { when (keyEvent.keyCode) { @@ -225,25 +305,31 @@ class VoiceInteractionService : Service() { // 检测双击 val currentTime = System.currentTimeMillis() - if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键 + val timeDiff = currentTime - lastKeyEventTime + Log.d(TAG, "MediaSession 按键时间差: $timeDiff ms") + + if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 keyEventCount++ + Log.d(TAG, "MediaSession 连续按键计数: $keyEventCount") + if (keyEventCount == 1) { // 这是第二次按键,即双击 - Log.d(TAG, "检测到双击,开始语音交互") + Log.d(TAG, "MediaSession 检测到双击,开始语音交互") + // 重置处理状态,确保可以开始新的语音交互 + isActive = false handleDoubleClick() keyEventCount = 0 return true } } else { + Log.d(TAG, "MediaSession 单击事件,重置计数") keyEventCount = 0 } lastKeyEventTime = currentTime // 单击也处理 - if (keyEventCount == 0) { - Log.d(TAG, "检测到单击,开始语音交互") - handleDoubleClick() - return true - } + Log.d(TAG, "MediaSession 处理单击事件") + handleDoubleClick() + return true } } } @@ -273,15 +359,22 @@ class VoiceInteractionService : Service() { * 处理耳机双击事件 */ private fun handleDoubleClick() { - if (isProcessing) { - Log.d(TAG, "正在处理中,忽略此次点击") - return - } + Log.d(TAG, "handleDoubleClick - isActive: $isActive") + + // 无论当前状态如何,都先停止 TTS 播放 + stopCurrentTTS() - if (!isRecording) { + // 立即播放提示音,不等待语音识别开始 + playStartPrompt() + + if (!isActive) { + Log.d(TAG, "开始新的语音交互") startVoiceInteraction() } else { - stopVoiceInteraction() + Log.d(TAG, "已经在活动中,只需更新活动时间并继续") + // 更新最后活动时间 + updateLastActivityTime() + } } @@ -289,68 +382,239 @@ class VoiceInteractionService : Service() { * 开始语音交互 */ private fun startVoiceInteraction() { - if (isRecording || isProcessing) { + Log.d(TAG, "startVoiceInteraction - 当前状态: isActive=$isActive") + + if (isActive) { + Log.d(TAG, "已经在活动中,忽略此次请求") + return + } + + // 检查录音权限 + if (!checkRecordAudioPermission()) { + Log.e(TAG, "没有录音权限,无法开始语音交互") + playErrorPrompt("需要录音权限,请在设置中授予权限") + // 权限处理由 Flutter 端负责,不再尝试通知 return } - isRecording = true - isProcessing = true + isActive = true - // 播放提示音 - playStartPrompt() + // 更新最后活动时间 + updateLastActivityTime() + + // 启动超时检查 + startTimeoutCheck() + + Log.d(TAG, "设置状态: isActive=true") + + // 不再在这里播放提示音,而是在 handleDoubleClick 中播放 // 开始录音识别 - azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onRecognizing(recognizing: String) { - // 实时识别结果,可以记录日志 - Log.d(TAG, "正在识别: $recognizing") - } - - override fun onResult(result: String) { - // 最终识别结果 - Log.d(TAG, "语音识别结果: $result") + try { + Log.d(TAG, "开始调用 Azure 语音识别") + azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback { + override fun onRecognizing(recognizing: String) { + // 实时识别结果,可以记录日志 + Log.d(TAG, "正在识别: $recognizing") + + // 如果检测到用户在讲话,停止当前的 TTS 播放 + if (recognizing.isNotEmpty()) { + // 停止 TTS 播放 + stopCurrentTTS() + + // 更新最后活动时间 + updateLastActivityTime() + } + } - // 停止录音 - isRecording = false + override fun onResult(result: String) { + // 最终识别结果 + Log.d(TAG, "语音识别结果: $result") + + // 更新最后活动时间 + + // 如果有识别结果,则发送到VolcanoAI处理,但不停止录音 + if (result.isNotEmpty()) { + Log.d(TAG, "有识别结果,开始处理") + updateLastActivityTime() + + processWithVolcanoAI(result) + + // 不设置 isActive = false,保持活动状态 + } else { + // Log.d(TAG, "没有识别结果,但继续保持活动状态") + // 即使没有识别结果,也不停止活动状态 + } + } - // 如果有识别结果,则发送到VolcanoAI处理 - if (result.isNotEmpty()) { - processWithVolcanoAI(result) - } else { - isProcessing = false + override fun onSessionStarted() { + Log.d(TAG, "语音识别会话开始") + + // 更新最后活动时间 + updateLastActivityTime() } - } + + override fun onSessionStopped() { + Log.d(TAG, "语音识别会话结束") + // 确保状态被重置 + if (isActive) { + isActive = false + Log.d(TAG, "会话结束时设置状态: isActive=false") + } + + // 停止超时检查 + stopTimeoutCheck() + } + + override fun onCanceled(reason: String, errorDetails: String) { + Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails") + isActive = false + Log.d(TAG, "取消时设置状态: isActive=false") + + // 停止超时检查 + stopTimeoutCheck() + } + + override fun onError(error: String) { + Log.e(TAG, "语音识别错误: $error") + isActive = false + Log.d(TAG, "错误时设置状态: isActive=false") + + // 停止超时检查 + stopTimeoutCheck() + + // 播放错误提示音 + playErrorPrompt("语音识别出错") + } + }) + } catch (e: Exception) { + Log.e(TAG, "启动语音识别异常: ${e.message}", e) + isActive = false + Log.d(TAG, "异常时设置状态: isActive=false") - override fun onSessionStarted() { - Log.d(TAG, "语音识别会话开始") - } + // 停止超时检查 + stopTimeoutCheck() - override fun onSessionStopped() { - Log.d(TAG, "语音识别会话结束") - } + // 播放错误提示音 + playErrorPrompt("启动语音识别失败") + } + } + + /** + * 更新最后活动时间 + */ + private fun updateLastActivityTime() { + lastActivityTime = System.currentTimeMillis() + Log.d(TAG, "更新最后活动时间: $lastActivityTime") + } + + /** + * 启动超时检查 + */ + private fun startTimeoutCheck() { + // 先停止之前的超时检查 + stopTimeoutCheck() + + // 创建新的超时检查 + timeoutRunnable = Runnable { + val currentTime = System.currentTimeMillis() + val elapsedTime = currentTime - lastActivityTime - override fun onCanceled(reason: String, errorDetails: String) { - Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails") - isRecording = false - isProcessing = false - } + Log.d(TAG, "超时检查 - 已经过去 $elapsedTime ms, 是否活动中: $isActive, 是否正在播放TTS: $isTtsSpeaking") - override fun onError(error: String) { - Log.e(TAG, "语音识别错误: $error") - isRecording = false - isProcessing = false + if (isActive && !isTtsSpeaking &&elapsedTime >= RECOGNITION_TIMEOUT) { + // 超过30秒没有活动且没有TTS播放,暂停当前语音识别 + Log.d(TAG, "超过 ${RECOGNITION_TIMEOUT/1000} 秒没有活动,暂停语音识别") - // 播放错误提示音 - playErrorPrompt() + // 播放提示音 + playErrorPrompt("没有听到您说话,已暂停语音识别") + + // 暂停录音,但保持服务活动状态 + // 这样用户可以通过双击重新激活 + // azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { + // override fun onResult(result: String) { + // // 不处理结果 + // } + + // override fun onRecognizing(recognizing: String) { + // // 不处理中间结果 + // } + + // override fun onSessionStarted() { + // // 不处理会话开始事件 + // } + + // override fun onSessionStopped() { + // Log.d(TAG, "超时暂停:语音识别会话已停止") + // } + + // override fun onCanceled(reason: String, errorDetails: String) { + // Log.d(TAG, "超时暂停:语音识别已取消: $reason") + // } + + // override fun onError(error: String) { + // Log.e(TAG, "超时暂停:停止语音识别错误: $error") + // } + // }) + + // 不设置 isActive = false,保持服务活动状态 + Log.d(TAG, "保持状态: isActive=true") + + // 不停止超时检查,继续监控 + handler.postDelayed(timeoutRunnable!!, 1000) + } else if (isActive) { + // 如果仍在活动中,继续检查 + handler.postDelayed(timeoutRunnable!!, 1000) // 每秒检查一次 } - }) + } + + // 开始检查 + handler.postDelayed(timeoutRunnable!!, 1000) + Log.d(TAG, "启动超时检查") + } + + /** + * 停止超时检查 + */ + private fun stopTimeoutCheck() { + timeoutRunnable?.let { + handler.removeCallbacks(it) + timeoutRunnable = null + Log.d(TAG, "停止超时检查") + } + } + + /** + * 停止当前的 TTS 播放 + */ + private fun stopCurrentTTS() { + try { + if (isTtsSpeaking) { + Log.d(TAG, "停止当前 TTS 播放") + azureTtsHelper.stopSpeaking() + isTtsSpeaking = false + } + } catch (e: Exception) { + Log.e(TAG, "停止 TTS 播放异常: ${e.message}", e) + } + } + + /** + * 检查录音权限 + */ + private fun checkRecordAudioPermission(): Boolean { + val permission = android.Manifest.permission.RECORD_AUDIO + val result = applicationContext.checkCallingOrSelfPermission(permission) + val hasPermission = result == android.content.pm.PackageManager.PERMISSION_GRANTED + Log.d(TAG, "录音权限检查结果: $hasPermission") + return hasPermission } /** * 停止语音交互 */ private fun stopVoiceInteraction() { - if (!isRecording) { + if (!isActive) { return } @@ -380,37 +644,59 @@ class VoiceInteractionService : Service() { Log.e(TAG, "停止语音识别错误: $error") } }) - isRecording = false + isActive = false } /** * 播放开始提示音 */ private fun playStartPrompt() { - azureTtsHelper.speakText("我在听", object : AzureTtsHelper.TTSCallback { - override fun onSuccess(message: String) { - Log.d(TAG, "提示音播放完成") - } - - override fun onError(error: String) { - Log.e(TAG, "提示音播放错误: $error") - } - }) + Log.d(TAG, "开始播放提示音: '我在听'") + try { + isTtsSpeaking = true + azureTtsHelper.speakText("我在!", object : AzureTtsHelper.TTSCallback { + override fun onSuccess(message: String) { + Log.d(TAG, "提示音播放完成: $message") + isTtsSpeaking = false + + + } + + override fun onError(error: String) { + Log.e(TAG, "提示音播放错误: $error") + isTtsSpeaking = false + // 即使提示音播放失败,也继续进行语音识别 + } + }) + } catch (e: Exception) { + Log.e(TAG, "播放提示音异常: ${e.message}", e) + isTtsSpeaking = false + // 即使提示音播放失败,也继续进行语音识别 + } } /** * 播放错误提示音 */ - private fun playErrorPrompt() { - azureTtsHelper.speakText("抱歉,我没有听清", object : AzureTtsHelper.TTSCallback { - override fun onSuccess(message: String) { - Log.d(TAG, "错误提示音播放完成") - } - - override fun onError(error: String) { - Log.e(TAG, "错误提示音播放错误: $error") - } - }) + private fun playErrorPrompt(errorMessage: String = "抱歉,我没有听清") { + Log.d(TAG, "开始播放错误提示音: '$errorMessage'") + try { + isTtsSpeaking = true + azureTtsHelper.speakText(errorMessage, object : AzureTtsHelper.TTSCallback { + override fun onSuccess(message: String) { + Log.d(TAG, "错误提示音播放完成: $message") + isTtsSpeaking = false + } + + override fun onError(error: String) { + Log.e(TAG, "错误提示音播放错误: $error") + isTtsSpeaking = false + } + }) + } catch (e: Exception) { + Log.e(TAG, "播放错误提示音异常: ${e.message}", e) + isTtsSpeaking = false + } } /** @@ -420,6 +706,7 @@ class VoiceInteractionService : Service() { // 使用原生VolcanoAIService处理 Thread { try { + // 创建用户消息 val messages = JSONArray().apply { put(volcanoAIService.createUserMessage(text)) @@ -432,10 +719,10 @@ class VoiceInteractionService : Service() { speakAIResponse(response) } catch (e: Exception) { Log.e(TAG, "VolcanoAI处理错误: ${e.message}") - isProcessing = false + // 不设置 isActive = false,保持活动状态 // 播放错误提示音 - playErrorPrompt() + playErrorPrompt("AI处理出错") } }.start() } @@ -444,15 +731,27 @@ class VoiceInteractionService : Service() { * 播放AI回复 */ private fun speakAIResponse(text: String) { + Log.d(TAG, "开始播放AI回复: '${text.take(20)}...'") + + isTtsSpeaking = true azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback { override fun onSuccess(message: String) { Log.d(TAG, "AI回复播放完成") - isProcessing = false + isTtsSpeaking = false + + // 更新最后活动时间 + updateLastActivityTime() + + // 不设置 isActive = false,保持活动状态 + // 继续保持对话状态,等待用户下一次输入 + Log.d(TAG, "继续保持对话状态,等待用户下一次输入") } override fun onError(error: String) { Log.e(TAG, "AI回复播放错误: $error") - isProcessing = false + isTtsSpeaking = false + // 即使出错,也不结束活动状态,继续保持对话 + Log.d(TAG, "AI回复播放出错,但继续保持对话状态") } }) } @@ -461,6 +760,14 @@ class VoiceInteractionService : Service() { * 启动一个"前台通知",保证此Service在后台不会被系统回收 */ private fun startForegroundServiceWithNotification() { + val notification = createNotification() + startForeground(NOTIFICATION_ID, notification) + } + + /** + * 创建通知 + */ + private fun createNotification(): Notification { val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Android 8.0+ 需要创建通知渠道 @@ -484,27 +791,25 @@ class VoiceInteractionService : Service() { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) - val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) + return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("语音助手") .setContentText("双击蓝牙耳机按键即可开始语音交互") .setSmallIcon(R.mipmap.ic_launcher) .setContentIntent(pendingIntent) .build() - - startForeground(NOTIFICATION_ID, notification) } /** * 当 Service 被启动时调用 */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "onStartCommand: $intent") + Log.d(TAG, "onStartCommand: $intent, extras: ${intent?.extras}") // 处理媒体按键 Intent if (intent != null) { // 如果是媒体按钮事件,尝试手动处理 if (Intent.ACTION_MEDIA_BUTTON == intent.action) { - Log.d(TAG, "收到媒体按钮事件: $intent") + Log.d(TAG, "收到媒体按钮事件 Intent: $intent, extras: ${intent.extras}") // 根据 Android 版本使用不同的方式获取 KeyEvent val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -514,14 +819,23 @@ class VoiceInteractionService : Service() { intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent } - Log.d(TAG, "手动处理媒体按钮事件: $keyEvent") + Log.d(TAG, "从 Intent 中提取的按键事件: $keyEvent") if (keyEvent != null) { + Log.d(TAG, "准备手动处理按键事件: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}") handleKeyEvent(keyEvent) + } else { + // 即使没有 KeyEvent,也尝试触发语音交互 + Log.d(TAG, "Intent 中没有按键事件,但仍然尝试触发语音交互") + // 重置处理状态,确保可以开始新的语音交互 + isActive = false + handleDoubleClick() } } // 无论如何都要尝试让 MediaSession 处理 - MediaButtonReceiver.handleIntent(mediaSession, intent) + Log.d(TAG, "将 Intent 传递给 MediaSession 处理") + val result = MediaButtonReceiver.handleIntent(mediaSession, intent) + Log.d(TAG, "MediaSession 处理结果: $result") } return START_STICKY @@ -531,9 +845,12 @@ class VoiceInteractionService : Service() { override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { - Log.d(TAG, "onDestroy") + Log.d(TAG, "onDestroy - 语音交互服务正在销毁") super.onDestroy() + // 停止超时检查 + stopTimeoutCheck() + // 取消注册媒体按钮接收器 val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) @@ -547,5 +864,24 @@ class VoiceInteractionService : Service() { azureTtsHelper.dispose() isRunning.set(false) + + // 发送服务已停止的广播 + sendServiceStatusBroadcast(STATUS_STOPPED) + + // 通知监听器服务已停止 + serviceStatusListener?.onServiceStopped() + + Log.d(TAG, "语音交互服务已完全销毁") + } + + /** + * 发送服务状态变化广播 + */ + private fun sendServiceStatusBroadcast(status: String) { + val intent = Intent(ACTION_SERVICE_STATUS_CHANGED).apply { + putExtra(EXTRA_SERVICE_STATUS, status) + } + sendBroadcast(intent) + Log.d(TAG, "已发送服务状态广播: $status") } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt b/android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt index b1591c1db..27c884c04 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt @@ -28,8 +28,8 @@ class VolcanoAIService(private val context: Context) { private val apiKey: String get() { - val sharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) - return sharedPreferences.getString("flutter.volcano_ai_api_key", "") ?: "" + // 从 MainActivity 的静态变量获取 API Key + return MainActivity.volcanoAiApiKey } init { diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index b0dd634b8..6326b3cdf 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -3,15 +3,12 @@ import '../../data/services/azure_asr_service.dart'; import '../../data/services/azure_tts_service.dart'; import '../../data/services/chat_history_service.dart'; import '../../data/services/volcano_translation_service.dart'; -import '../../data/services/config_service.dart'; +import '../../data/services/voice_interaction_service.dart'; /// 初始绑定,用于管理全局依赖 class InitialBinding extends Bindings { @override void dependencies() { - // 配置服务 - 需要首先初始化 - Get.putAsync(() => ConfigService().init()); - // Azure 语音识别服务 Get.lazyPut(() => AzureAsrService(), fenix: true); @@ -23,6 +20,8 @@ class InitialBinding extends Bindings { // 火山翻译服务 Get.lazyPut(() => VolcanoTranslationService(), fenix: true); + + // 语音交互服务 + Get.putAsync(() => VoiceInteractionService().init()); } - } \ No newline at end of file diff --git a/lib/data/services/config_service.dart b/lib/data/services/config_service.dart deleted file mode 100644 index e34a143be..000000000 --- a/lib/data/services/config_service.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:logger/logger.dart'; -import 'package:flutter/services.dart'; - -/// 配置服务,用于管理应用配置和环境变量 -class ConfigService extends GetxService { - final Logger _logger = Logger(); - late final SharedPreferences _prefs; - - // 创建一个 MethodChannel 用于与原生代码通信 - static const MethodChannel _channel = MethodChannel('com.example.deep_voice/config'); - - // 初始化服务 - Future init() async { - try { - _prefs = await SharedPreferences.getInstance(); - - // 从 .env 读取 Azure 语音服务配置 - final azureSpeechKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY']; - final azureSpeechRegion = dotenv.env['AZURE_ASR_SERVICE_REGION']; - - // 从 .env 读取火山 AI API Key - final volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY']; - - - // 通过 MethodChannel 向原生代码传递配置值 - try { - await _channel.invokeMethod('setConfig', { - 'azure_speech_key': azureSpeechKey ?? '', - 'azure_speech_region': azureSpeechRegion ?? '', - 'volcano_ai_api_key': volcanoAiApiKey ?? '', - }); - _logger.i('已通过 MethodChannel 向原生代码传递配置值'); - } catch (e) { - _logger.e('通过 MethodChannel 传递配置值失败: $e'); - } - - return this; - } catch (e) { - _logger.e('初始化配置服务失败: $e'); - rethrow; - } - } - - // 获取配置值 - String? getString(String key) { - return _prefs.getString(key); - } - - // 设置配置值 - Future setString(String key, String value) { - return _prefs.setString(key, value); - } -} \ No newline at end of file diff --git a/lib/data/services/voice_interaction_service.dart b/lib/data/services/voice_interaction_service.dart new file mode 100644 index 000000000..53db4489f --- /dev/null +++ b/lib/data/services/voice_interaction_service.dart @@ -0,0 +1,70 @@ +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +/// 语音交互服务,用于管理原生的 VoiceInteractionService +class VoiceInteractionService extends GetxService { + static VoiceInteractionService get to => Get.find(); + + // 服务运行状态 + final _isRunning = false.obs; + bool get isRunning => _isRunning.value; + + // 方法通道 + final MethodChannel _channel = const MethodChannel('com.deep_voice.voice_interaction'); + + /// 初始化服务 + Future init() async { + // 获取服务运行状态 + await _updateServiceStatus(); + return this; + } + + /// 启动语音交互服务 + Future startService() async { + try { + // 获取配置信息 + final speechKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; + final speechRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? ''; + + // 从 .env 读取火山 AI API Key + final aiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? ''; + + // 调用原生方法,传递配置参数 + final result = await _channel.invokeMethod('startVoiceInteractionService', { + 'azure_speech_key': speechKey, + 'azure_speech_region': speechRegion, + 'volcano_ai_api_key': aiApiKey, + }) ?? false; + + await _updateServiceStatus(); + return result; + } catch (e) { + print('启动语音交互服务失败: $e'); + return false; + } + } + + /// 停止语音交互服务 + Future stopService() async { + try { + final result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; + await _updateServiceStatus(); + return result; + } catch (e) { + print('停止语音交互服务失败: $e'); + return false; + } + } + + /// 更新服务状态 + Future _updateServiceStatus() async { + try { + final status = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; + _isRunning.value = status; + } catch (e) { + print('获取语音交互服务状态失败: $e'); + _isRunning.value = false; + } + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 9c84dfc4c..c5e9938c4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -71,6 +71,11 @@ class MainApp extends StatelessWidget { translations: AppTranslations(), locale: const Locale('zh', 'CN'), fallbackLocale: const Locale('en', 'US'), + // 在应用初始化完成后,初始化 ConfigService + onInit: () { + // 延迟执行,确保应用已完全启动 + + }, ); } ); diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 442dc93a6..06f487465 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'dart:async'; import 'package:flutter/material.dart'; +import '../../../data/services/voice_interaction_service.dart'; enum ConnectStatus { initial, // 初始状态,显示添加设备 @@ -17,6 +18,10 @@ class HomeController extends GetxController { Timer? _animationTimer; bool _isDisposed = false; + // 语音服务状态 + final _voiceServiceRunning = false.obs; + bool get voiceServiceRunning => _voiceServiceRunning.value; + ConnectStatus get connectStatus => _connectStatus.value; String get searchDots => _searchDots.value; @@ -24,6 +29,9 @@ class HomeController extends GetxController { void onInit() { super.onInit(); _connectStatus.value = ConnectStatus.initial; + + // 获取语音服务状态 + _updateVoiceServiceStatus(); } @override @@ -32,6 +40,79 @@ class HomeController extends GetxController { stopSearchingAnimation(); super.onClose(); } + + /// 更新语音服务状态 + Future _updateVoiceServiceStatus() async { + if (Get.isRegistered()) { + _voiceServiceRunning.value = VoiceInteractionService.to.isRunning; + } else { + _voiceServiceRunning.value = false; + } + } + + /// 启动语音交互服务 + Future startVoiceService() async { + // 确保 ConfigService 已初始化 + + + // 确保 VoiceInteractionService 已初始化 + if (!Get.isRegistered()) { + await Get.putAsync(() => VoiceInteractionService().init()); + } + + final result = await VoiceInteractionService.to.startService(); + await _updateVoiceServiceStatus(); + + if (result) { + Get.snackbar( + '成功', + '语音交互服务已启动', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green.withOpacity(0.1), + colorText: Colors.green, + duration: const Duration(seconds: 2), + ); + } else { + Get.snackbar( + '错误', + '启动语音交互服务失败', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + duration: const Duration(seconds: 2), + ); + } + } + + /// 停止语音交互服务 + Future stopVoiceService() async { + if (!Get.isRegistered()) { + await Get.putAsync(() => VoiceInteractionService().init()); + } + + final result = await VoiceInteractionService.to.stopService(); + await _updateVoiceServiceStatus(); + + if (result) { + Get.snackbar( + '成功', + '语音交互服务已停止', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.blue.withOpacity(0.1), + colorText: Colors.blue, + duration: const Duration(seconds: 2), + ); + } else { + Get.snackbar( + '错误', + '停止语音交互服务失败', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + duration: const Duration(seconds: 2), + ); + } + } void startSearchingAnimation() { stopSearchingAnimation(); // Clear any existing timer diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 589235184..b03ea8e93 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -240,6 +240,65 @@ class HomeView extends GetView { } }); } + + /// 构建语音服务控制按钮 + Widget _buildVoiceServiceButton() { + return Obx(() { + final isRunning = controller.voiceServiceRunning; + + return Container( + width: double.infinity, + height: 56.h, + margin: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isRunning + ? [const Color(0xFFE57373), const Color(0xFFEF5350)] // 红色渐变(停止) + : [const Color(0xFF66BB6A), const Color(0xFF4CAF50)], // 绿色渐变(启动) + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + borderRadius: BorderRadius.circular(28.r), + boxShadow: [ + BoxShadow( + color: (isRunning ? const Color(0xFFE57373) : const Color(0xFF66BB6A)).withOpacity(0.3), + offset: const Offset(0, 4), + blurRadius: 12, + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: isRunning ? controller.stopVoiceService : controller.startVoiceService, + borderRadius: BorderRadius.circular(28.r), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isRunning ? Icons.stop : Icons.play_arrow, + color: Colors.white, + size: 24.sp, + ), + SizedBox(width: 12.w), + Text( + isRunning ? '停止语音服务' : '启动语音服务', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + ); + }); + } @override Widget build(BuildContext context) { @@ -272,7 +331,8 @@ class HomeView extends GetView { ), ), _buildActionButton(), - + SizedBox(height: 20.h), + _buildVoiceServiceButton(), SizedBox(height: 40.h), ], ), diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 80dbc0bc5..c014c5d55 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -23,6 +23,9 @@ PODS: - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS - speech_to_text (0.0.1): - Flutter - FlutterMacOS @@ -46,6 +49,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) @@ -77,6 +81,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin speech_to_text: :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin sqflite_darwin: @@ -96,6 +102,7 @@ SPEC CHECKSUMS: path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96