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 b9815af34..cad447104 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 @@ -10,12 +10,17 @@ import android.util.Log import com.ryanheise.audioservice.AudioServiceActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.EventChannel 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.ComponentName +import android.content.BroadcastReceiver +import android.content.IntentFilter +import android.os.Handler +import android.os.Looper class MainActivity: FlutterActivity() { @@ -24,18 +29,49 @@ 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 VOICE_INTERACTION_EVENT_CHANNEL = "com.deep_voice.voice_interaction_events" private val TAG = "MainActivity" private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureTtsHelper: AzureTtsHelper private var azureAsrEventSink: EventChannel.EventSink? = null + private var voiceInteractionEventSink: EventChannel.EventSink? = null + + // 广播接收器 + private val voiceInteractionReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + Log.d(TAG, "收到广播: ${intent?.action}") + + when (intent?.action) { + VoiceInteractionService.ACTION_RECOGNITION_STARTED -> { + val timestamp = intent.getLongExtra("timestamp", 0) + Log.d(TAG, "收到语音识别启动广播: timestamp=$timestamp") + sendVoiceInteractionEvent(mapOf( + "type" to "recognitionStarted", + "timestamp" to timestamp + )) + } + VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED -> { + val agentId = intent.getStringExtra("agentId") ?: "default" + val userMessage = intent.getStringExtra("userMessage") ?: "" + val assistantMessage = intent.getStringExtra("assistantMessage") ?: "" + val timestamp = intent.getLongExtra("timestamp", System.currentTimeMillis()) + + Log.d(TAG, "收到聊天记录更新广播: agentId=$agentId, timestamp=$timestamp") + Log.d(TAG, "用户消息: ${userMessage.take(50)}...") + Log.d(TAG, "助手回复: ${assistantMessage.take(50)}...") + + sendChatHistoryEvent(agentId, userMessage, assistantMessage, timestamp) + } + } + } + } // 使用伴生对象存储配置信息,使其在整个应用中可访问 companion object { // Azure 语音服务配置 - var azureSpeechKey: String = "" - var azureSpeechRegion: String = "" - var volcanoAiApiKey: String = "" + var azureSpeechKey: String = "2K2VOHSuL8ZSEnbKgHSyTwo1ohj7C2gbxyVimsjpVUt6aETxVwlvJQQJ99BCACqBBLyXJ3w3AAAYACOGqCMj" + var azureSpeechRegion: String = "southeastasia" + var volcanoAiApiKey: String = "0fd7c93b-b9bf-4b0b-bca6-e284491792bf" } override fun onCreate(savedInstanceState: Bundle?) { @@ -43,16 +79,126 @@ class MainActivity: FlutterActivity() { azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() + // 注册广播接收器 + registerVoiceInteractionReceiver() + // 不再自动启动服务,由用户通过 Flutter 界面手动启动 Log.d(TAG, "MainActivity 已创建,但不会自动启动 VoiceInteractionService") + + // 确保 Flutter 引擎准备好后立即设置事件通道 + flutterEngine?.let { engine -> + Log.d(TAG, "Flutter 引擎已存在,立即设置事件通道") + setupEventChannels(engine) + } + } + + /** + * 注册语音交互服务的广播接收器 + */ + private fun registerVoiceInteractionReceiver() { + val intentFilter = IntentFilter().apply { + addAction(VoiceInteractionService.ACTION_RECOGNITION_STARTED) + addAction(VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED) + } + registerReceiver(voiceInteractionReceiver, intentFilter) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) + Log.d(TAG, "开始配置 Flutter 引擎") + + // 先设置事件通道,确保在插件注册之前就已准备好 + Log.d(TAG, "在插件注册前设置事件通道") + setupEventChannels(flutterEngine) - // 注册所有插件 + // 然后调用父类方法和注册插件 + super.configureFlutterEngine(flutterEngine) GeneratedPluginRegistrant.registerWith(flutterEngine) + // 设置方法通道 + setupMethodChannels(flutterEngine) + + Log.d(TAG, "Flutter 引擎配置完成") + } + + /** + * 设置事件通道 + */ + private fun setupEventChannels(flutterEngine: FlutterEngine) { + Log.d(TAG, "开始设置事件通道") + + try { + // 语音交互事件通道 + Log.d(TAG, "设置语音交互事件通道") + EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + Log.d(TAG, "语音交互事件通道开始监听,arguments: $arguments") + voiceInteractionEventSink = events + + // 通知 Flutter 端事件通道已准备好 + try { + events?.success(mapOf("type" to "channelReady")) + Log.d(TAG, "已发送 channelReady 事件") + + // 测试发送一个聊天记录事件 + Handler(Looper.getMainLooper()).postDelayed({ + if (voiceInteractionEventSink != null) { + Log.d(TAG, "发送测试聊天记录事件") + sendChatHistoryEvent( + "personal_assistant", + "这是一条测试用户消息", + "这是一条测试助手回复", + System.currentTimeMillis() + ) + } else { + Log.e(TAG, "无法发送测试事件:事件通道未准备好") + } + }, 5000) // 5秒后发送测试事件 + } catch (e: Exception) { + Log.e(TAG, "发送 channelReady 事件失败: ${e.message}", e) + } + } + + override fun onCancel(arguments: Any?) { + Log.d(TAG, "语音交互事件通道取消监听,arguments: $arguments") + voiceInteractionEventSink = null + } + } + ) + Log.d(TAG, "语音交互事件通道设置完成") + + // Azure ASR 事件通道 + Log.d(TAG, "设置 Azure ASR 事件通道") + EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + Log.d(TAG, "ASR事件通道开始监听,arguments: $arguments") + azureAsrEventSink = events + + // 通知 Flutter 端事件通道已准备好 + events?.success(mapOf("type" to "channelReady")) + Log.d(TAG, "已发送 ASR channelReady 事件") + } + + override fun onCancel(arguments: Any?) { + Log.d(TAG, "ASR事件通道取消监听,arguments: $arguments") + azureAsrEventSink = null + } + } + ) + Log.d(TAG, "Azure ASR 事件通道设置完成") + + } catch (e: Exception) { + Log.e(TAG, "设置事件通道时发生错误: ${e.message}", e) + } + + Log.d(TAG, "所有事件通道设置完成") + } + + /** + * 设置方法通道 + */ + private fun setupMethodChannels(flutterEngine: FlutterEngine) { // 音频处理通道 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AUDIO_PROCESSING_CHANNEL).setMethodCallHandler { call, result -> // 为所有方法返回默认值或成功状态 @@ -195,24 +341,6 @@ class MainActivity: FlutterActivity() { } } - // Azure ASR 事件通道 - EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler( - object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - Log.d(TAG, "ASR事件通道开始监听") - azureAsrEventSink = events - - // 通知 Flutter 端事件通道已准备好 - events?.success(mapOf("type" to "channelReady")) - } - - override fun onCancel(arguments: Any?) { - Log.d(TAG, "ASR事件通道取消监听") - azureAsrEventSink = null - } - } - ) - // Azure TTS 方法通道 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { @@ -286,24 +414,7 @@ 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) + startVoiceInteractionService(call, result) } "stopVoiceInteractionService" -> { stopVoiceInteractionService() @@ -312,6 +423,10 @@ class MainActivity: FlutterActivity() { "isVoiceInteractionServiceRunning" -> { result.success(VoiceInteractionService.isServiceRunning()) } + "pauseVoiceInteraction" -> { + pauseVoiceInteraction() + result.success(true) + } else -> { result.notImplemented() } @@ -347,35 +462,121 @@ class MainActivity: FlutterActivity() { } /** - * 启动语音交互服务 + * 启动语音交互服务(带配置参数) */ - private fun startVoiceInteractionService() { - Log.d(TAG, "准备启动语音交互服务") - val serviceIntent = Intent(this, VoiceInteractionService::class.java) + private fun startVoiceInteractionService(call: MethodCall, result: MethodChannel.Result) { + Log.d(TAG, "启动语音交互服务(带配置参数)") - // 添加 ACTION_MEDIA_BUTTON 动作,确保服务能够接收媒体按钮事件 - serviceIntent.action = Intent.ACTION_MEDIA_BUTTON + // 获取配置参数 + val key = call.argument("azure_speech_key") ?: "" + val region = call.argument("azure_speech_region") ?: "" + val aiKey = call.argument("volcano_ai_api_key") ?: "" - // 确保明确设置组件 - serviceIntent.component = ComponentName(packageName, VoiceInteractionService::class.java.name) + // 设置 Azure Speech 配置 + azureSpeechKey = key + azureSpeechRegion = region + volcanoAiApiKey = aiKey - // 输出配置信息 - Log.d(TAG, "启动服务时的配置信息: subscriptionKey=${if (azureSpeechKey.isNotEmpty()) azureSpeechKey.take(5) + "..." else "空"}, serviceRegion=$azureSpeechRegion") + Log.d(TAG, "配置参数: key=${if (key.isNotEmpty()) key.take(5) + "..." else "空"}, region=$region, aiKey=${if (aiKey.isNotEmpty()) aiKey.take(5) + "..." else "空"}") - Log.d(TAG, "启动服务: $serviceIntent") - startService(serviceIntent) - Log.d(TAG, "语音交互服务启动完成") + // 创建 Intent 并启动服务 + val serviceIntent = Intent(this, VoiceInteractionService::class.java) + + // 在 Android O 及以上版本,需要使用 startForegroundService + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + + Log.d(TAG, "语音交互服务启动命令已发送") + result.success(true) } /** * 停止语音交互服务 */ private fun stopVoiceInteractionService() { + Log.d(TAG, "停止语音交互服务") val serviceIntent = Intent(this, VoiceInteractionService::class.java) stopService(serviceIntent) + Log.d(TAG, "语音交互服务停止命令已发送") + } + + /** + * 暂停语音交互(停止TTS和语音识别,但保持服务运行) + */ + private fun pauseVoiceInteraction() { + Log.d(TAG, "发送暂停语音交互请求") + val intent = Intent(this, VoiceInteractionService::class.java).apply { + action = VoiceInteractionService.ACTION_PAUSE_VOICE_INTERACTION + } + + // 在 Android O 及以上版本,需要使用 startForegroundService + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } + + /** + * 发送聊天记录事件到 Flutter + */ + private fun sendChatHistoryEvent(agentId: String, userMessage: String, assistantMessage: String, timestamp: Long) { + if (voiceInteractionEventSink == null) { + Log.e(TAG, "无法发送聊天记录事件:事件通道未准备好") + return + } + + Log.d(TAG, "发送聊天记录事件到Flutter: agentId=$agentId, userMessage=${userMessage.take(20)}..., assistantMessage=${assistantMessage.take(20)}...") + + runOnUiThread { + try { + val eventData = mapOf( + "type" to "chatHistory", + "agentId" to agentId, + "userMessage" to userMessage, + "assistantMessage" to assistantMessage, + "timestamp" to timestamp + ) + voiceInteractionEventSink?.success(eventData) + Log.d(TAG, "聊天记录事件已发送到Flutter") + } catch (e: Exception) { + Log.e(TAG, "发送聊天记录事件到Flutter失败: ${e.message}") + } + } + } + + /** + * 发送语音交互事件到 Flutter + */ + private fun sendVoiceInteractionEvent(event: Map) { + if (voiceInteractionEventSink == null) { + Log.e(TAG, "无法发送语音交互事件:事件通道未准备好,事件内容: $event") + return + } + + Log.d(TAG, "发送语音交互事件到Flutter: $event") + + runOnUiThread { + try { + voiceInteractionEventSink?.success(event) + Log.d(TAG, "语音交互事件已发送到Flutter") + } catch (e: Exception) { + Log.e(TAG, "发送语音交互事件到Flutter失败: ${e.message}", e) + } + } } override fun onDestroy() { + // 注销广播接收器 + try { + unregisterReceiver(voiceInteractionReceiver) + } catch (e: Exception) { + Log.e(TAG, "注销广播接收器失败: ${e.message}") + } + azureAsrHelper.dispose() azureTtsHelper.dispose() super.onDestroy() 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 278453e53..5255dd62b 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 @@ -36,64 +36,54 @@ class VoiceInteractionService : Service() { private const val NOTIFICATION_ID = 1001 private const val CHANNEL_ID = "voice_interaction_channel" + // 语音识别超时时间(毫秒) + private const val RECOGNITION_TIMEOUT = 10000L + // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) // 获取服务运行状态 - 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" + fun isServiceRunning(): Boolean = isRunning.get() - // 服务状态监听器 - private var serviceStatusListener: ServiceStatusListener? = null - - // 设置服务状态监听器 - fun setServiceStatusListener(listener: ServiceStatusListener) { - serviceStatusListener = listener - } - - // 移除服务状态监听器 - fun removeServiceStatusListener() { - serviceStatusListener = null - } - - // 服务状态监听接口 - interface ServiceStatusListener { - fun onServiceStarted() - fun onServiceStopped() - } + // 广播 Action + const val ACTION_RECOGNITION_STARTED = "com.example.deep_voice.ACTION_RECOGNITION_STARTED" + const val ACTION_PAUSE_VOICE_INTERACTION = "com.example.deep_voice.ACTION_PAUSE_VOICE_INTERACTION" + const val ACTION_CHAT_HISTORY_UPDATED = "com.example.deep_voice.ACTION_CHAT_HISTORY_UPDATED" } - private lateinit var mediaSession: MediaSessionCompat - private var isActive = false // 合并 isRecording 和 isProcessing 为一个状态 + // 服务状态 + private var isActive = false // 服务是否活跃 + private var isRecognitionActive = false // 语音识别是否活跃 + private var isTimeoutPaused = false // 是否因超时暂停 + private var hasSpeechDetected = false // 是否检测到语音 + private var isTtsSpeaking = false // 是否正在播放TTS + + // 按键处理 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 - + // 当前用户输入 + private var currentUserInput = "" - // Azure服务 + // 服务组件 + private lateinit var mediaSession: MediaSessionCompat + private lateinit var audioManager: AudioManager private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureTtsHelper: AzureTtsHelper - - // 火山AI服务 private lateinit var volcanoAIService: VolcanoAIService + // 定时器 + private val handler = Handler(Looper.getMainLooper()) + private val serviceMonitor = object : Runnable { + override fun run() { + monitorService() + handler.postDelayed(this, 1000) // 每秒执行一次 + } + } + // 系统提示词 private val systemPrompt = """ 你是一个智能语音助手,能够简洁明了地回答用户的问题。 @@ -105,184 +95,85 @@ class VoiceInteractionService : Service() { override fun onCreate() { super.onCreate() Log.d(TAG, "onCreate - 语音交互服务正在创建") + + // 初始化状态 isRunning.set(true) - isActive = false // 确保初始状态为 false - isTtsSpeaking = false // 确保初始状态为 false - - // 获取 AudioManager - audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + resetState() - // 初始化服务 + // 初始化组件 + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager initServices() - - // 初始化 MediaSession 和按键监听 initMediaSession() - - // 启动前台通知(Foreground Service) - startForegroundServiceWithNotification() - - // 注册成为媒体按钮接收器 registerMediaButtonReceiver() - // 发送服务已启动的广播 - sendServiceStatusBroadcast(STATUS_STARTED) + // 设置为媒体播放状态 + setPlaybackState(PlaybackStateCompat.STATE_PLAYING) - // 通知监听器服务已启动 - serviceStatusListener?.onServiceStarted() + // 启动监控和前台服务 + startMonitoring() + startForegroundServiceWithNotification() Log.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...") } /** - * 测试按键事件 + * 重置所有状态 */ - private fun testKeyEvent() { - 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) - } - - // 发送给 MediaSession 处理 - MediaButtonReceiver.handleIntent(mediaSession, mediaButtonIntent) - - // // 直接处理按键事件 - handleKeyEvent(keyEvent) - - Log.d(TAG, "测试按键事件已发送") + private fun resetState() { + isActive = false + isRecognitionActive = false + isTimeoutPaused = false + hasSpeechDetected = false + isTtsSpeaking = false } /** * 初始化所有服务 */ 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}") - - // 检测双击 - val currentTime = System.currentTimeMillis() - 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}") - } - } - - /** - * 注册成为媒体按钮接收器 - */ - private fun registerMediaButtonReceiver() { - val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) - audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) - Log.d(TAG, "已注册媒体按钮接收器: $mediaButtonReceiver") - } - - /** - * 初始化Azure服务 - */ - private fun initAzureServices() { + // 初始化Azure服务 azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() - // 从 MainActivity 的静态变量获取 Azure 配置 + // 从 MainActivity 获取 Azure 配置 val subscriptionKey = MainActivity.azureSpeechKey val serviceRegion = MainActivity.azureSpeechRegion - Log.d(TAG, "Azure配置信息: subscriptionKey=${if (subscriptionKey.isNotEmpty()) subscriptionKey.take(5) + "..." else "空"}, serviceRegion=${if (serviceRegion.isNotEmpty()) serviceRegion else "空"}") - if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { // 初始化ASR - val asrInitResult = azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") - Log.d(TAG, "ASR初始化结果: $asrInitResult") - + azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") // 初始化TTS - val ttsInitResult = azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") - Log.d(TAG, "TTS初始化结果: $ttsInitResult") - - if (!asrInitResult || !ttsInitResult) { - Log.e(TAG, "Azure服务初始化失败,语音交互功能可能无法正常工作") - } + azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") } else { Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务") - + } + + // 初始化火山AI服务 + volcanoAIService = VolcanoAIService() + // 使用 MainActivity 中的 API key 初始化 + val volcanoApiKey = MainActivity.volcanoAiApiKey + if (volcanoApiKey.isNotEmpty()) { + volcanoAIService.initialize(volcanoApiKey) + } else { + Log.e(TAG, "火山AI配置信息不完整,无法初始化火山AI服务") } } /** - * 初始化 MediaSession 以捕获蓝牙耳机按键 + * 初始化 MediaSession */ private fun initMediaSession() { mediaSession = MediaSessionCompat(applicationContext, "VoiceServiceSession") - - // 支持媒体按钮和传输控件 + + // 设置标志和状态 mediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS ) - - // 设置播放状态 - val playbackStateBuilder = PlaybackStateCompat.Builder() - .setActions( - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_PLAY_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - ) - .setState(PlaybackStateCompat.STATE_PAUSED, 0, 1.0f) - mediaSession.setPlaybackState(playbackStateBuilder.build()) - - // 监听回调 + + // 设置回调 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) } else { @@ -290,47 +181,31 @@ class VoiceInteractionService : Service() { mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent } - Log.d(TAG, "收到媒体按钮事件: $keyEvent") - - if (keyEvent != null) { - Log.d(TAG, "按键详情: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}, 时间=${keyEvent.eventTime}") - - 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}") - - // 检测双击 - val currentTime = System.currentTimeMillis() - 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, "MediaSession 检测到双击,开始语音交互") - // 重置处理状态,确保可以开始新的语音交互 - isActive = false - handleDoubleClick() - keyEventCount = 0 - return true - } - } else { - Log.d(TAG, "MediaSession 单击事件,重置计数") + if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN) { + when (keyEvent.keyCode) { + KeyEvent.KEYCODE_MEDIA_PLAY, + KeyEvent.KEYCODE_MEDIA_PAUSE, + KeyEvent.KEYCODE_HEADSETHOOK, + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { + // 检测双击 + val currentTime = System.currentTimeMillis() + val timeDiff = currentTime - lastKeyEventTime + + if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 + keyEventCount++ + if (keyEventCount == 1) { // 这是第二次按键,即双击 + handleDoubleClick() keyEventCount = 0 + return true } - lastKeyEventTime = currentTime - - // 单击也处理 - Log.d(TAG, "MediaSession 处理单击事件") - handleDoubleClick() - return true + } else { + keyEventCount = 0 } + lastKeyEventTime = currentTime + + // 单击也处理 + handleDoubleClick() + return true } } } @@ -338,426 +213,324 @@ class VoiceInteractionService : Service() { } override fun onPlay() { - Log.d(TAG, "MediaSession.onPlay()") handleDoubleClick() return super.onPlay() } override fun onPause() { - Log.d(TAG, "MediaSession.onPause()") handleDoubleClick() return super.onPause() } }) - + // 激活 MediaSession mediaSession.isActive = true - Log.d(TAG, "MediaSession 已激活: ${mediaSession.isActive}") } - + /** - * 处理耳机双击事件 + * 注册媒体按钮接收器 */ - private fun handleDoubleClick() { - Log.d(TAG, "handleDoubleClick - isActive: $isActive") + private fun registerMediaButtonReceiver() { + val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) + audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) + } + + /** + * 启动监控 + */ + private fun startMonitoring() { + handler.post(serviceMonitor) + } + + /** + * 停止监控 + */ + private fun stopMonitoring() { + handler.removeCallbacks(serviceMonitor) + } + + /** + * 监控服务状态 + */ + private fun monitorService() { + // 确保服务保持活跃状态 + if (!isActive) { + isActive = true + } - // 无论当前状态如何,都先停止 TTS 播放 + // 检查语音识别状态 + if (isRecognitionActive) { + val currentTime = System.currentTimeMillis() + val elapsedTime = currentTime - lastActivityTime + + // 如果超过5秒没有检测到语音,且不在TTS播放中,暂停语音识别 + if (!hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT) { + isTimeoutPaused = true + playNotification("没有听到您说话,已暂停语音识别。双击耳机按钮可重新开始。") + stopVoiceRecognition() + } + } + } + + /** + * 处理双击事件 + */ + private fun handleDoubleClick() { + // 停止当前TTS播放 stopCurrentTTS() - // 立即播放提示音,不等待语音识别开始 - playStartPrompt() + // 播放提示音 + playPrompt("我在!") - if (!isActive) { - Log.d(TAG, "开始新的语音交互") - startVoiceInteraction() + // 设置为播放状态 + setPlaybackState(PlaybackStateCompat.STATE_PLAYING) + + // 重置超时暂停标志 + isTimeoutPaused = false + + // 启动或重置语音识别 + if (!isRecognitionActive) { + startVoiceRecognition() } else { - Log.d(TAG, "已经在活动中,只需更新活动时间并继续") - // 更新最后活动时间 updateLastActivityTime() - + hasSpeechDetected = false } } /** - * 开始语音交互 + * 开始语音识别 */ - private fun startVoiceInteraction() { - Log.d(TAG, "startVoiceInteraction - 当前状态: isActive=$isActive") - - if (isActive) { - Log.d(TAG, "已经在活动中,忽略此次请求") - return - } + private fun startVoiceRecognition() { + if (isRecognitionActive) return // 检查录音权限 if (!checkRecordAudioPermission()) { - Log.e(TAG, "没有录音权限,无法开始语音交互") - playErrorPrompt("需要录音权限,请在设置中授予权限") - // 权限处理由 Flutter 端负责,不再尝试通知 + playNotification("需要录音权限,请在设置中授予权限") return } isActive = true - - // 更新最后活动时间 + isRecognitionActive = true + hasSpeechDetected = false updateLastActivityTime() - // 启动超时检查 - startTimeoutCheck() - - Log.d(TAG, "设置状态: isActive=true") - - // 不再在这里播放提示音,而是在 handleDoubleClick 中播放 - - // 开始录音识别 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 播放 + hasSpeechDetected = true stopCurrentTTS() - - // 更新最后活动时间 updateLastActivityTime() } } 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, "没有识别结果,但继续保持活动状态") - // 即使没有识别结果,也不停止活动状态 } + + // 重置状态,继续识别 + hasSpeechDetected = false + updateLastActivityTime() } override fun onSessionStarted() { - Log.d(TAG, "语音识别会话开始") - - // 更新最后活动时间 updateLastActivityTime() + // 通知 Flutter 语音识别已启动 + notifyRecognitionStarted() } override fun onSessionStopped() { - Log.d(TAG, "语音识别会话结束") - // 确保状态被重置 - if (isActive) { - isActive = false - Log.d(TAG, "会话结束时设置状态: isActive=false") - } + isRecognitionActive = false - // 停止超时检查 - stopTimeoutCheck() + // 如果不是因超时暂停,尝试重新启动 + if (isActive && !isTimeoutPaused) { + handler.postDelayed({ + if (isActive && !isRecognitionActive && !isTtsSpeaking) { + startVoiceRecognition() + } + }, 1000) + } } override fun onCanceled(reason: String, errorDetails: String) { - Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails") - isActive = false - Log.d(TAG, "取消时设置状态: isActive=false") + isRecognitionActive = false - // 停止超时检查 - stopTimeoutCheck() + // 如果不是因超时暂停,尝试重新启动 + if (isActive && !isTimeoutPaused) { + handler.postDelayed({ + if (isActive && !isRecognitionActive && !isTtsSpeaking) { + startVoiceRecognition() + } + }, 1000) + } } override fun onError(error: String) { - Log.e(TAG, "语音识别错误: $error") - isActive = false - Log.d(TAG, "错误时设置状态: isActive=false") - - // 停止超时检查 - stopTimeoutCheck() + isRecognitionActive = false + playNotification("语音识别出错") - // 播放错误提示音 - playErrorPrompt("语音识别出错") + // 如果不是因超时暂停,尝试重新启动 + if (isActive && !isTimeoutPaused) { + handler.postDelayed({ + if (isActive && !isRecognitionActive && !isTtsSpeaking) { + startVoiceRecognition() + } + }, 2000) + } } }) } catch (e: Exception) { - Log.e(TAG, "启动语音识别异常: ${e.message}", e) - isActive = false - Log.d(TAG, "异常时设置状态: isActive=false") - - // 停止超时检查 - stopTimeoutCheck() + isRecognitionActive = false + playNotification("启动语音识别失败") - // 播放错误提示音 - playErrorPrompt("启动语音识别失败") + // 延迟重试 + handler.postDelayed({ + if (isActive && !isRecognitionActive && !isTtsSpeaking) { + startVoiceRecognition() + } + }, 3000) } } /** - * 更新最后活动时间 + * 停止语音识别 */ - private fun updateLastActivityTime() { - lastActivityTime = System.currentTimeMillis() - Log.d(TAG, "更新最后活动时间: $lastActivityTime") + private fun stopVoiceRecognition() { + if (!isRecognitionActive) return + + azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { + override fun onResult(result: String) {} + override fun onRecognizing(recognizing: String) {} + override fun onSessionStarted() {} + override fun onSessionStopped() { isRecognitionActive = false } + override fun onCanceled(reason: String, errorDetails: String) { isRecognitionActive = false } + override fun onError(error: String) { isRecognitionActive = false } + }) + + setPlaybackState(PlaybackStateCompat.STATE_PAUSED) + isRecognitionActive = false + hasSpeechDetected = false + // 不重置 isTimeoutPaused,保留暂停原因 } /** - * 启动超时检查 + * 使用VolcanoAI处理语音识别结果 */ - private fun startTimeoutCheck() { - // 先停止之前的超时检查 - stopTimeoutCheck() + private fun processWithVolcanoAI(text: String) { + // 保存当前用户输入,用于后续同步聊天记录 + currentUserInput = text - // 创建新的超时检查 - timeoutRunnable = Runnable { - val currentTime = System.currentTimeMillis() - val elapsedTime = currentTime - lastActivityTime - - Log.d(TAG, "超时检查 - 已经过去 $elapsedTime ms, 是否活动中: $isActive, 是否正在播放TTS: $isTtsSpeaking") - - if (isActive && !isTtsSpeaking &&elapsedTime >= RECOGNITION_TIMEOUT) { - // 超过30秒没有活动且没有TTS播放,暂停当前语音识别 - Log.d(TAG, "超过 ${RECOGNITION_TIMEOUT/1000} 秒没有活动,暂停语音识别") - - // 播放提示音 - playErrorPrompt("没有听到您说话,已暂停语音识别") + Thread { + try { + val messages = JSONArray().apply { + put(volcanoAIService.createUserMessage(text)) + } - // 暂停录音,但保持服务活动状态 - // 这样用户可以通过双击重新激活 - // 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") - // } - // }) + val response = volcanoAIService.sendMessage(messages, systemPrompt) - // 不设置 isActive = false,保持服务活动状态 - Log.d(TAG, "保持状态: isActive=true") + // 播放AI回复 + speakAIResponse(response) - // 不停止超时检查,继续监控 - handler.postDelayed(timeoutRunnable!!, 1000) - } else if (isActive) { - // 如果仍在活动中,继续检查 - handler.postDelayed(timeoutRunnable!!, 1000) // 每秒检查一次 + // 同步聊天记录到Flutter端 + notifyChatHistoryUpdated("personal_assistant", text, response) + } catch (e: Exception) { + Log.e(TAG, "AI处理出错: ${e.message}") + playNotification("AI处理出错") } - } - - // 开始检查 - handler.postDelayed(timeoutRunnable!!, 1000) - Log.d(TAG, "启动超时检查") + }.start() } /** - * 停止超时检查 - */ - private fun stopTimeoutCheck() { - timeoutRunnable?.let { - handler.removeCallbacks(it) - timeoutRunnable = null - Log.d(TAG, "停止超时检查") - } - } - - /** - * 停止当前的 TTS 播放 + * 播放AI回复 */ - private fun stopCurrentTTS() { - try { - if (isTtsSpeaking) { - Log.d(TAG, "停止当前 TTS 播放") - azureTtsHelper.stopSpeaking() + private fun speakAIResponse(text: String) { + isTtsSpeaking = true + azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback { + override fun onSuccess(message: String) { + isTtsSpeaking = false + updateLastActivityTime() + } + + override fun onError(error: String) { 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 playPrompt(message: String) { + isTtsSpeaking = true + azureTtsHelper.speakText(message, object : AzureTtsHelper.TTSCallback { + override fun onSuccess(message: String) { isTtsSpeaking = false } + override fun onError(error: String) { isTtsSpeaking = false } + }) } /** - * 停止语音交互 + * 播放通知提示音 */ - private fun stopVoiceInteraction() { - if (!isActive) { - return - } - - // 停止录音 - 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") + private fun playNotification(message: String) { + isTtsSpeaking = true + azureTtsHelper.speakText(message, object : AzureTtsHelper.TTSCallback { + override fun onSuccess(message: String) { + isTtsSpeaking = false + setPlaybackState(PlaybackStateCompat.STATE_PAUSED) } override fun onError(error: String) { - Log.e(TAG, "停止语音识别错误: $error") + isTtsSpeaking = false + setPlaybackState(PlaybackStateCompat.STATE_PAUSED) } }) - isActive = false } /** - * 播放开始提示音 + * 停止当前TTS播放 */ - private fun playStartPrompt() { - 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) + private fun stopCurrentTTS() { + if (isTtsSpeaking) { + azureTtsHelper.stopSpeaking() isTtsSpeaking = false - // 即使提示音播放失败,也继续进行语音识别 } } /** - * 播放错误提示音 + * 更新最后活动时间 */ - 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 - } + private fun updateLastActivityTime() { + lastActivityTime = System.currentTimeMillis() } /** - * 使用VolcanoAI处理语音识别结果 + * 设置播放状态 */ - private fun processWithVolcanoAI(text: String) { - // 使用原生VolcanoAIService处理 - Thread { - try { - - // 创建用户消息 - val messages = JSONArray().apply { - put(volcanoAIService.createUserMessage(text)) - } - - // 发送请求到VolcanoAI - val response = volcanoAIService.sendMessage(messages, systemPrompt) - - // 播放AI回复 - speakAIResponse(response) - } catch (e: Exception) { - Log.e(TAG, "VolcanoAI处理错误: ${e.message}") - // 不设置 isActive = false,保持活动状态 - - // 播放错误提示音 - playErrorPrompt("AI处理出错") - } - }.start() + private fun setPlaybackState(state: Int) { + val playbackStateBuilder = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + ) + .setState(state, 0, 1.0f) + mediaSession.setPlaybackState(playbackStateBuilder.build()) } /** - * 播放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回复播放完成") - isTtsSpeaking = false - - // 更新最后活动时间 - updateLastActivityTime() - - // 不设置 isActive = false,保持活动状态 - // 继续保持对话状态,等待用户下一次输入 - Log.d(TAG, "继续保持对话状态,等待用户下一次输入") - } - - override fun onError(error: String) { - Log.e(TAG, "AI回复播放错误: $error") - isTtsSpeaking = false - // 即使出错,也不结束活动状态,继续保持对话 - Log.d(TAG, "AI回复播放出错,但继续保持对话状态") - } - }) + private fun checkRecordAudioPermission(): Boolean { + val permission = android.Manifest.permission.RECORD_AUDIO + val result = applicationContext.checkCallingOrSelfPermission(permission) + return result == android.content.pm.PackageManager.PERMISSION_GRANTED } - + /** - * 启动一个"前台通知",保证此Service在后台不会被系统回收 + * 启动前台服务 */ private fun startForegroundServiceWithNotification() { val notification = createNotification() @@ -803,43 +576,59 @@ class VoiceInteractionService : Service() { * 当 Service 被启动时调用 */ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "onStartCommand: $intent, extras: ${intent?.extras}") - // 处理媒体按键 Intent - if (intent != null) { - // 如果是媒体按钮事件,尝试手动处理 - if (Intent.ACTION_MEDIA_BUTTON == intent.action) { - Log.d(TAG, "收到媒体按钮事件 Intent: $intent, extras: ${intent.extras}") - - // 根据 Android 版本使用不同的方式获取 KeyEvent - val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? 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() - } + if (intent?.action == Intent.ACTION_MEDIA_BUTTON) { + val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent } - // 无论如何都要尝试让 MediaSession 处理 - Log.d(TAG, "将 Intent 传递给 MediaSession 处理") - val result = MediaButtonReceiver.handleIntent(mediaSession, intent) - Log.d(TAG, "MediaSession 处理结果: $result") + if (keyEvent != null) { + handleKeyEvent(keyEvent) + } else { + handleDoubleClick() + } + } else if (intent?.action == ACTION_PAUSE_VOICE_INTERACTION) { + // 处理来自 Flutter 的暂停请求 + pauseVoiceInteraction() } + // 让 MediaSession 处理 Intent + MediaButtonReceiver.handleIntent(mediaSession, intent) + return START_STICKY } + + /** + * 处理按键事件 + */ + private fun handleKeyEvent(keyEvent: KeyEvent) { + 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 -> { + // 检测双击 + val currentTime = System.currentTimeMillis() + val timeDiff = currentTime - lastKeyEventTime + + if (timeDiff < 500) { // 500毫秒内的按键视为连续按键 + keyEventCount++ + if (keyEventCount == 1) { // 这是第二次按键,即双击 + handleDoubleClick() + keyEventCount = 0 + } + } else { + keyEventCount = 0 + } + lastKeyEventTime = currentTime + } + } + } + } @Nullable override fun onBind(intent: Intent?): IBinder? = null @@ -848,8 +637,8 @@ class VoiceInteractionService : Service() { Log.d(TAG, "onDestroy - 语音交互服务正在销毁") super.onDestroy() - // 停止超时检查 - stopTimeoutCheck() + // 停止监控 + stopMonitoring() // 取消注册媒体按钮接收器 val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) @@ -858,30 +647,66 @@ class VoiceInteractionService : Service() { // 释放资源 mediaSession.isActive = false mediaSession.release() - - // 释放Azure服务 azureAsrHelper.dispose() azureTtsHelper.dispose() isRunning.set(false) + } + + /** + * 暂停语音交互(停止TTS和语音识别,但保持服务运行) + * 用于从 Flutter 端控制服务 + */ + private fun pauseVoiceInteraction() { + Log.d(TAG, "暂停语音交互(来自Flutter的请求)") - // 发送服务已停止的广播 - sendServiceStatusBroadcast(STATUS_STOPPED) + // 停止当前TTS播放 + stopCurrentTTS() - // 通知监听器服务已停止 - serviceStatusListener?.onServiceStopped() + // 停止语音识别 + if (isRecognitionActive) { + stopVoiceRecognition() + } - Log.d(TAG, "语音交互服务已完全销毁") + // 设置为暂停状态,但保持服务活跃 + isTimeoutPaused = true + setPlaybackState(PlaybackStateCompat.STATE_PAUSED) } /** - * 发送服务状态变化广播 + * 通知 Flutter 端聊天记录已更新 */ - private fun sendServiceStatusBroadcast(status: String) { - val intent = Intent(ACTION_SERVICE_STATUS_CHANGED).apply { - putExtra(EXTRA_SERVICE_STATUS, status) + private fun notifyChatHistoryUpdated(agentId: String, userMessage: String, assistantMessage: String) { + Log.d(TAG, "通知Flutter聊天记录已更新: agentId=$agentId") + Log.d(TAG, "用户消息: ${userMessage.take(50)}...") + Log.d(TAG, "助手回复: ${assistantMessage.take(50)}...") + + // 创建广播 Intent + val intent = Intent(ACTION_CHAT_HISTORY_UPDATED).apply { + putExtra("agentId", agentId) + putExtra("userMessage", userMessage) + putExtra("assistantMessage", assistantMessage) + putExtra("timestamp", System.currentTimeMillis()) } + + // 发送广播 + sendBroadcast(intent) + Log.d(TAG, "已发送聊天记录广播") + } + + /** + * 通知 Flutter 端语音识别已启动 + */ + private fun notifyRecognitionStarted() { + Log.d(TAG, "通知Flutter语音识别已启动") + + // 创建广播 Intent + val intent = Intent(ACTION_RECOGNITION_STARTED).apply { + // 可以添加额外数据 + putExtra("timestamp", System.currentTimeMillis()) + } + + // 发送广播 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 27c884c04..39143d536 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 @@ -1,6 +1,5 @@ package com.example.deep_voice -import android.content.Context import android.util.Log import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -16,7 +15,7 @@ import java.util.concurrent.TimeUnit * * 参考Flutter端的VolcanoAIService实现,提供同步和异步的API调用方式 */ -class VolcanoAIService(private val context: Context) { +class VolcanoAIService() { private val TAG = "VolcanoAIService" private val baseUrl = "https://ark.cn-beijing.volces.com/api/v3" private val chatEndpoint = "/chat/completions" @@ -26,16 +25,26 @@ class VolcanoAIService(private val context: Context) { .writeTimeout(30, TimeUnit.SECONDS) .build() - private val apiKey: String - get() { - // 从 MainActivity 的静态变量获取 API Key - return MainActivity.volcanoAiApiKey - } + private var apiKey: String = "" + private var isInitialized = false - init { - if (apiKey.isEmpty()) { - Log.e(TAG, "火山AI配置信息不完整,请检查环境变量") + /** + * 初始化火山AI服务 + * + * @param apiKey 火山AI API密钥 + * @return 初始化是否成功 + */ + fun initialize(apiKey: String): Boolean { + this.apiKey = apiKey + isInitialized = apiKey.isNotEmpty() + + if (!isInitialized) { + Log.e(TAG, "初始化失败:API key 不能为空") + } else { + Log.d(TAG, "火山AI服务初始化成功") } + + return isInitialized } /** @@ -84,6 +93,11 @@ class VolcanoAIService(private val context: Context) { */ @Throws(VolcanoAIException::class) fun sendMessage(messages: JSONArray, systemPrompt: String): String { + // 检查是否已初始化 + if (!isInitialized || apiKey.isEmpty()) { + throw VolcanoAIException("火山AI服务未初始化或API key为空,请先调用initialize方法") + } + val fullMessages = JSONArray().apply { put(JSONObject().apply { put("role", "system") @@ -150,6 +164,12 @@ class VolcanoAIService(private val context: Context) { * @param callback 回调函数,用于接收流式输出的结果 */ fun sendMessageStream(messages: JSONArray, systemPrompt: String, callback: StreamCallback) { + // 检查是否已初始化 + if (!isInitialized || apiKey.isEmpty()) { + callback.onError(VolcanoAIException("火山AI服务未初始化或API key为空,请先调用initialize方法")) + return + } + val fullMessages = JSONArray().apply { put(JSONObject().apply { put("role", "system") diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 6326b3cdf..00fb2da94 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -4,11 +4,13 @@ import '../../data/services/azure_tts_service.dart'; import '../../data/services/chat_history_service.dart'; import '../../data/services/volcano_translation_service.dart'; import '../../data/services/voice_interaction_service.dart'; +import '../../core/utils/logger.dart'; /// 初始绑定,用于管理全局依赖 class InitialBinding extends Bindings { @override void dependencies() { + Logger.warning('InitialBinding dependencies'); // Azure 语音识别服务 Get.lazyPut(() => AzureAsrService(), fenix: true); @@ -21,7 +23,7 @@ class InitialBinding extends Bindings { // 火山翻译服务 Get.lazyPut(() => VolcanoTranslationService(), fenix: true); - // 语音交互服务 - Get.putAsync(() => VoiceInteractionService().init()); + // 语音交互服务 - 使用 putAsync 确保 init 方法被调用 + Get.putAsync(() => VoiceInteractionService().init(), permanent: true); } } \ No newline at end of file diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/azure_asr_service.dart index 9de7664d6..48e78fc58 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/azure_asr_service.dart @@ -42,8 +42,8 @@ class AzureAsrService { /// 从环境变量加载配置 void _loadConfig() { - _subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; - _serviceRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? ''; + _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); diff --git a/lib/data/services/azure_tts_service.dart b/lib/data/services/azure_tts_service.dart index ebb98c61c..e38807494 100644 --- a/lib/data/services/azure_tts_service.dart +++ b/lib/data/services/azure_tts_service.dart @@ -50,8 +50,8 @@ class AzureTtsService extends GetxService { /// 从环境变量加载配置 void _loadConfig() { - _subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; - _serviceRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? ''; + _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; } /// 初始化TTS服务 diff --git a/lib/data/services/chat_history_service.dart b/lib/data/services/chat_history_service.dart index 3f47c4bd5..6647a6b41 100644 --- a/lib/data/services/chat_history_service.dart +++ b/lib/data/services/chat_history_service.dart @@ -39,6 +39,9 @@ class ChatHistoryService extends GetxService { final jsonList = messagesToSave.map((m) => m.toJson()).toList(); final jsonString = jsonEncode(jsonList); + // 保存到存储 + await _storage.write(_getHistoryKey(agentId), jsonString); + Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}'); } catch (e) { Logger.error('保存聊天记录失败', e); diff --git a/lib/data/services/flutter_asr_service.dart b/lib/data/services/flutter_asr_service.dart index d4cdd8ca7..b44984ef7 100644 --- a/lib/data/services/flutter_asr_service.dart +++ b/lib/data/services/flutter_asr_service.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; -import 'package:speech_to_text/speech_to_text.dart'; -import 'package:speech_to_text/speech_recognition_result.dart'; +// 移除 speech_to_text 依赖 +// import 'package:speech_to_text/speech_to_text.dart'; +// import 'package:speech_to_text/speech_recognition_result.dart'; import '../../core/utils/logger.dart'; /// ASR状态枚举 @@ -14,11 +15,25 @@ enum AsrState { error // 错误状态 } +/// 监听模式枚举(与 speech_to_text 兼容) +enum ListenMode { + deviceDefault, + dictation, + search, + confirmation, +} + +/// 本地名称类(与 speech_to_text 兼容) +class LocaleName { + final String name; + final String localeId; + + LocaleName(this.name, this.localeId); +} + /// Flutter ASR服务,负责本地语音转文本功能 +/// 注意:此实现为空实现,不再依赖 speech_to_text 插件 class FlutterAsrService extends GetxService { - // 语音识别实例 - late SpeechToText _speech; - // 当前ASR状态 final Rx _asrState = AsrState.notInitialized.obs; AsrState get asrState => _asrState.value; @@ -44,7 +59,8 @@ class FlutterAsrService extends GetxService { bool get isAvailable => _isAvailable.value; // 是否正在监听 - bool get isListening => _speech.isListening; + bool _isListening = false; + bool get isListening => _isListening; // 识别结果流 final _recognitionResultsController = StreamController.broadcast(); @@ -58,37 +74,16 @@ class FlutterAsrService extends GetxService { /// 初始化语音识别 Future _initSpeech() async { - _speech = SpeechToText(); - try { - final available = await _speech.initialize( - onStatus: _onStatusChanged, - onError: _onErrorListener, - debugLogging: kDebugMode, - ); + // 空实现,不再使用 speech_to_text + _isAvailable.value = false; + _asrState.value = AsrState.error; - _isAvailable.value = available; + // 添加一个默认语言 + _locales.add(LocaleName('中文 (简体)', 'zh-CN')); + _currentLocale.value = 'zh-CN'; - if (available) { - _asrState.value = AsrState.initialized; - - // 获取可用的语言 - final locales = await _speech.locales(); - _locales.assignAll(locales); - - // 默认选择系统语言,如果支持的话 - final systemLocale = await _speech.systemLocale(); - if (systemLocale != null) { - _currentLocale.value = systemLocale.localeId; - } else if (_locales.isNotEmpty) { - _currentLocale.value = _locales.first.localeId; - } - - Logger.info('语音识别初始化成功,可用语言数量: ${_locales.length}'); - } else { - _asrState.value = AsrState.error; - Logger.error('语音识别不可用'); - } + Logger.info('FlutterAsrService 已初始化(空实现)'); } catch (e) { _asrState.value = AsrState.error; _isAvailable.value = false; @@ -110,68 +105,25 @@ class FlutterAsrService extends GetxService { bool onDevice = false, ListenMode listenMode = ListenMode.deviceDefault, }) async { - if (!_isAvailable.value) { - Logger.error('语音识别不可用,无法开始监听'); - return false; - } - - // 清空上次的识别结果 - _lastWords.value = ''; - _confidence.value = 0.0; - - try { - final started = await _speech.listen( - onResult: _onSpeechResult, - listenFor: listenFor, - pauseFor: pauseFor, - localeId: _currentLocale.value, - listenOptions: SpeechListenOptions( - partialResults: partialResults, - onDevice: onDevice, - listenMode: listenMode, - ), - ); - - if (started) { - _asrState.value = AsrState.listening; - Logger.info('开始语音识别监听'); - } else { - Logger.error('开始语音识别监听失败'); - } - - return started; - } catch (e) { - Logger.error('开始语音识别监听出错: $e'); - return false; - } + // 空实现,不再使用 speech_to_text + Logger.error('FlutterAsrService 为空实现,不支持语音识别'); + return false; } /// 停止监听 Future stopListening() async { - if (!_speech.isListening) return; - - try { - await _speech.stop(); - _asrState.value = AsrState.notListening; - Logger.info('停止语音识别监听'); - } catch (e) { - Logger.error('停止语音识别监听出错: $e'); - } + // 空实现,不再使用 speech_to_text + _isListening = false; + _asrState.value = AsrState.notListening; } /// 取消监听 Future cancelListening() async { - if (!_speech.isListening) return; - - try { - await _speech.cancel(); - _lastWords.value = ''; - _confidence.value = 0.0; - _asrState.value = AsrState.notListening; - Logger.info('取消语音识别监听'); - } catch (e) { - Logger.error('取消语音识别监听出错: $e'); - } + // 空实现,不再使用 speech_to_text + _isListening = false; + _lastWords.value = ''; + _confidence.value = 0.0; + _asrState.value = AsrState.notListening; } /// 设置当前语言 @@ -182,54 +134,8 @@ class FlutterAsrService extends GetxService { } } - /// 语音识别结果回调 - void _onSpeechResult(SpeechRecognitionResult result) { - final resultText = result.recognizedWords; - _lastWords.value = resultText; - - if (result.finalResult) { - // 计算置信度 - 仅在最终结果时有效 - if (result.alternates.isNotEmpty) { - _confidence.value = result.alternates.first.confidence; - } - - // 发送最终结果到流 - _recognitionResultsController.add(resultText); - - Logger.info('语音识别最终结果: "$resultText", 置信度: ${_confidence.value}'); - _asrState.value = AsrState.notListening; - } else { - // 部分结果 - Logger.info('语音识别部分结果: "$resultText"'); - } - } - - /// 状态变化回调 - void _onStatusChanged(String status) { - Logger.info('语音识别状态变化: $status'); - - switch (status) { - case 'listening': - _asrState.value = AsrState.listening; - break; - case 'notListening': - _asrState.value = AsrState.notListening; - break; - case 'done': - _asrState.value = AsrState.notListening; - break; - } - } - - /// 错误回调 - void _onErrorListener(dynamic error) { - Logger.error('语音识别错误: ${error.errorMsg}'); - _asrState.value = AsrState.error; - } - @override void onClose() { - _speech.cancel(); _recognitionResultsController.close(); super.onClose(); } diff --git a/lib/data/services/voice_interaction_service.dart b/lib/data/services/voice_interaction_service.dart index 53db4489f..4705cd714 100644 --- a/lib/data/services/voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_service.dart @@ -1,6 +1,11 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../services/azure_asr_service.dart'; +import '../services/azure_tts_service.dart'; +import '../services/chat_history_service.dart'; +import '../../modules/chat/models/message_model.dart'; +import '../../core/utils/logger.dart'; /// 语音交互服务,用于管理原生的 VoiceInteractionService class VoiceInteractionService extends GetxService { @@ -10,37 +15,187 @@ class VoiceInteractionService extends GetxService { final _isRunning = false.obs; bool get isRunning => _isRunning.value; + // 语音识别状态 + final _isRecognitionActive = false.obs; + bool get isRecognitionActive => _isRecognitionActive.value; + + // 聊天记录 - 可被 ChatView 监听 + final RxList personalAssistantMessages = [].obs; + // 方法通道 final MethodChannel _channel = const MethodChannel('com.deep_voice.voice_interaction'); + // 事件通道 + final EventChannel _eventChannel = const EventChannel('com.deep_voice.voice_interaction_events'); + + // 事件流订阅 + Stream? _eventStream; + + // 初始化标记,避免重复初始化 + static bool _isInitialized = false; + /// 初始化服务 Future init() async { - // 获取服务运行状态 - await _updateServiceStatus(); - return this; + // 如果已经初始化过,直接返回 + if (_isInitialized) { + return this; + } + + try { + // 获取服务运行状态 + await _updateServiceStatus(); + + // 设置事件监听 + _setupEventChannel(); + + // 加载个人助手的历史记录 + _loadPersonalAssistantHistory(); + + // 标记为已初始化 + _isInitialized = true; + return this; + } catch (e) { + Logger.error('VoiceInteractionService 初始化失败', e); + return this; + } + } + + /// 加载个人助手的历史记录 + void _loadPersonalAssistantHistory() { + if (Get.isRegistered()) { + final historyService = Get.find(); + final messages = historyService.loadHistory('personal_assistant'); + if (messages.isNotEmpty) { + personalAssistantMessages.assignAll(messages); + } + } + } + + /// 设置事件通道监听 + void _setupEventChannel() { + try { + _eventStream = _eventChannel.receiveBroadcastStream(); + _eventStream?.listen( + _handleEvent, + onError: (error) { + Logger.error('语音交互事件通道错误', error); + } + ); + } catch (e) { + Logger.error('设置事件通道监听失败', e); + } + } + + /// 处理来自原生端的事件 + void _handleEvent(dynamic event) { + try { + if (event is! Map) { + return; + } + + final type = event['type'] as String?; + if (type == null) { + return; + } + + switch (type) { + case 'channelReady': + break; + case 'recognitionStarted': + _isRecognitionActive.value = true; + + // 检测到后台服务的语音识别开始运行,主动停止Flutter端的语音识别和TTS + // _stopFlutterVoiceServices(); + break; + case 'chatHistory': + // 处理聊天记录 + _handleChatHistory(event); + break; + } + } catch (e) { + Logger.error('处理原生端事件时出错', e); + } + } + + /// 处理聊天记录事件 + void _handleChatHistory(Map event) { + try { + final agentId = event['agentId'] as String?; + final userMessage = event['userMessage'] as String?; + final assistantMessage = event['assistantMessage'] as String?; + final timestamp = event['timestamp'] as int?; + + if (agentId == null || userMessage == null || assistantMessage == null || timestamp == null) { + return; + } + + // 只处理个人助手的聊天记录 + if (agentId != 'personal_assistant') { + return; + } + + // 添加用户消息 + personalAssistantMessages.add(Message( + role: 'user', + content: userMessage, + timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), + )); + + // 添加助手消息 + personalAssistantMessages.add(Message( + role: 'assistant', + content: assistantMessage, + timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1000), // 助手消息时间戳稍后 + )); + + // 保存到历史记录 + if (Get.isRegistered()) { + final historyService = Get.find(); + historyService.saveHistory('personal_assistant', personalAssistantMessages); + } + } catch (e) { + Logger.error('处理聊天记录事件失败', e); + } + } + + /// 停止 Flutter 端的语音服务 + void _stopFlutterVoiceServices() { + // 停止 TTS + if (Get.isRegistered()) { + Get.find().stop(); + } + + // 停止 ASR + if (Get.isRegistered()) { + Get.find().stopContinuousRecognition(); + } } /// 启动语音交互服务 Future startService() async { try { - // 获取配置信息 - final speechKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; - final speechRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? ''; + // 获取 Azure 配置 + final azureKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + final azureRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; - // 从 .env 读取火山 AI API Key - final aiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? ''; + // 获取火山 AI 配置 + final volcanoApiKey = 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, + 'azure_speech_key': azureKey, + 'azure_speech_region': azureRegion, + 'volcano_ai_api_key': volcanoApiKey, }) ?? false; - await _updateServiceStatus(); + // 更新状态 + if (result) { + await _updateServiceStatus(); + } + return result; } catch (e) { - print('启动语音交互服务失败: $e'); + Logger.error('启动语音交互服务失败', e); return false; } } @@ -49,22 +204,45 @@ class VoiceInteractionService extends GetxService { Future stopService() async { try { final result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; - await _updateServiceStatus(); + + // 更新状态 + if (result) { + await _updateServiceStatus(); + } + + return result; + } catch (e) { + Logger.error('停止语音交互服务失败', e); + return false; + } + } + + /// 暂停语音交互 + Future pauseVoiceInteraction() async { + try { + final result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; return result; } catch (e) { - print('停止语音交互服务失败: $e'); + Logger.error('暂停语音交互失败', e); return false; } } - /// 更新服务状态 + /// 更新服务运行状态 Future _updateServiceStatus() async { try { final status = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; _isRunning.value = status; } catch (e) { - print('获取语音交互服务状态失败: $e'); + Logger.error('获取语音交互服务状态失败', e); _isRunning.value = false; } } + + @override + void onClose() { + // 清理资源 + _eventStream = null; + super.onClose(); + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c5e9938c4..9c84dfc4c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -71,11 +71,6 @@ class MainApp extends StatelessWidget { translations: AppTranslations(), locale: const Locale('zh', 'CN'), fallbackLocale: const Locale('en', 'US'), - // 在应用初始化完成后,初始化 ConfigService - onInit: () { - // 延迟执行,确保应用已完全启动 - - }, ); } ); diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 563f2385d..1b19c9ce9 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -9,6 +9,7 @@ import '../../../data/providers/agent_provider.dart'; import '../../../data/models/agent.dart'; import '../../../core/utils/logger.dart'; import '../../../data/services/chat_history_service.dart'; +import '../../../data/services/voice_interaction_service.dart'; class ChatController extends GetxController { // 服务 @@ -16,6 +17,7 @@ class ChatController extends GetxController { final AzureTtsService _ttsService = Get.find(); final AzureAsrService _asrService = Get.find(); final ChatHistoryService _historyService = Get.find(); + final VoiceInteractionService _voiceInteractionService = Get.find(); // 添加ScrollController用于控制列表滚动 final ScrollController scrollController = ScrollController(); @@ -46,6 +48,9 @@ class ChatController extends GetxController { // 流式响应控制 StreamSubscription? _aiResponseSubscription; StreamSubscription? _recognitionSubscription; + + // 语音交互服务消息监听 + Worker? _personalAssistantMessagesWorker; // 取消标志 bool _shouldCancelAiResponse = false; @@ -55,6 +60,8 @@ class ChatController extends GetxController { super.onInit(); Logger.info('ChatController onInit'); + + // 从路由参数获取Agent信息 final Map args = Get.arguments ?? {}; final String agentId = args['agentId'] ?? 'personal_assistant'; @@ -69,11 +76,23 @@ class ChatController extends GetxController { agent = foundAgent; + + // 暂停后台语音交互服务,避免与聊天页面的语音功能相互干扰 + // if (Get.isRegistered()) { + // await VoiceInteractionService.to.pauseVoiceInteraction(); + // Logger.info('已暂停后台语音交互服务'); + // } + // 设置TTS服务 await _ttsService.setVoice(agent.voice); // 加载聊天历史 _loadChatHistory(); + + // 如果是个人助手,监听语音交互服务的消息更新 + if (agent.id == 'personal_assistant') { + _setupPersonalAssistantMessagesListener(); + } // 如果没有历史记录,添加欢迎消息 if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) { @@ -98,6 +117,37 @@ class ChatController extends GetxController { _scrollToBottom(); }); } + + /// 设置个人助手消息监听器 + void _setupPersonalAssistantMessagesListener() { + // 取消之前的监听器(如果有) + _personalAssistantMessagesWorker?.dispose(); + + // 设置新的监听器 + _personalAssistantMessagesWorker = ever( + _voiceInteractionService.personalAssistantMessages, + (List updatedMessages) { + if (agent.id == 'personal_assistant' && updatedMessages.isNotEmpty) { + Logger.info('检测到语音交互服务的个人助手消息更新,同步到聊天视图'); + + // 中断当前响应 + _interruptCurrentResponse("检测到语音交互服务的消息更新,中断当前响应"); + + // 更新消息列表 + messages.assignAll(updatedMessages); + + // 滚动到底部 + _scrollToBottom(); + } + } + ); + + // 初始同步 + if (_voiceInteractionService.personalAssistantMessages.isNotEmpty) { + messages.assignAll(_voiceInteractionService.personalAssistantMessages); + Logger.info('初始同步语音交互服务的个人助手消息,消息数: ${_voiceInteractionService.personalAssistantMessages.length}'); + } + } @override void onClose() { @@ -107,6 +157,7 @@ class ChatController extends GetxController { scrollController.dispose(); _aiResponseSubscription?.cancel(); _recognitionSubscription?.cancel(); + _personalAssistantMessagesWorker?.dispose(); // 停止TTS _ttsService.stop(); @@ -504,10 +555,31 @@ class ChatController extends GetxController { // 删除存储的历史记录 _historyService.clearHistory(agent.id); + + // 如果是个人助手,同时清空语音交互服务的消息 + if (agent.id == 'personal_assistant' && Get.isRegistered()) { + _voiceInteractionService.personalAssistantMessages.clear(); + _voiceInteractionService.personalAssistantMessages.add(Message( + role: 'assistant', + content: agent.welcomeMessage, + timestamp: DateTime.now(), + )); + } } // 加载聊天历史 void _loadChatHistory() { + // 如果是个人助手,优先使用语音交互服务中的消息 + if (agent.id == 'personal_assistant' && + Get.isRegistered() && + _voiceInteractionService.personalAssistantMessages.isNotEmpty) { + messages.clear(); + messages.addAll(_voiceInteractionService.personalAssistantMessages); + Logger.info('从语音交互服务加载个人助手的历史记录,消息数: ${messages.length}'); + return; + } + + // 否则从历史服务加载 final history = _historyService.loadHistory(agent.id); if (history.isNotEmpty) { messages.clear(); @@ -518,6 +590,11 @@ class ChatController extends GetxController { // 保存聊天历史 void _saveChatHistory() { _historyService.saveHistory(agent.id, messages); + + // 如果是个人助手,同时更新语音交互服务的消息 + if (agent.id == 'personal_assistant' && Get.isRegistered()) { + _voiceInteractionService.personalAssistantMessages.assignAll(messages); + } } // 添加消息,供外部调用 diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 06f487465..d29d6ab43 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -1,7 +1,7 @@ import 'package:get/get.dart'; import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; -import '../../../data/services/voice_interaction_service.dart'; enum ConnectStatus { initial, // 初始状态,显示添加设备 @@ -18,10 +18,6 @@ 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; @@ -29,9 +25,6 @@ class HomeController extends GetxController { void onInit() { super.onInit(); _connectStatus.value = ConnectStatus.initial; - - // 获取语音服务状态 - _updateVoiceServiceStatus(); } @override @@ -40,79 +33,6 @@ 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 b03ea8e93..8b068ceb1 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -240,65 +240,6 @@ 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) { @@ -331,8 +272,6 @@ class HomeView extends GetView { ), ), _buildActionButton(), - SizedBox(height: 20.h), - _buildVoiceServiceButton(), SizedBox(height: 40.h), ], ), diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 96aedb2cb..e3f6821e3 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -7,6 +7,7 @@ import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/azure_tts_service.dart'; import '../../../core/utils/logger.dart'; +import '../../../data/services/voice_interaction_service.dart'; class TranslationController extends GetxController { // Services @@ -80,6 +81,12 @@ class TranslationController extends GetxController { void onInit() { super.onInit(); + // 暂停后台语音交互服务,避免与翻译页面的语音功能相互干扰 + if (Get.isRegistered()) { + VoiceInteractionService.to.pauseVoiceInteraction(); + Logger.info('已暂停后台语音交互服务'); + } + // 加载历史记录 loadTranslationHistory(); diff --git a/pubspec.yaml b/pubspec.yaml index d50ddfacc..3dcc3bb51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,8 +29,8 @@ dependencies: crypto: ^3.0.3 flutter_tts: ^4.2.2 logger: ^2.0.2+1 - shared_preferences: ^2.2.2 - # azure_speech_recognition_null_safety: ^0.9.6 + # shared_preferences: ^2.2.2 # 移除,使用 get_storage 代替 + # speech_to_text: ^7.0.0 # 移除,使用自定义的 ASR 服务 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git @@ -42,7 +42,6 @@ dependencies: record: ^5.2.1 share_plus: ^10.0.0 image_picker: ^1.1.2 - speech_to_text: ^7.0.0 audio_waveforms: ^1.3.0 dev_dependencies: