From e72abe8e5cf89d8748de6eee338c151a6e49e398 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Fri, 14 Mar 2025 21:46:44 +0000 Subject: [PATCH] add --- .../com/example/deep_voice/AzureAsrHelper.kt | 145 +++- .../com/example/deep_voice/AzureTtsHelper.kt | 159 +++- .../com/example/deep_voice/MainActivity.kt | 222 ++--- .../deep_voice/VoiceInteractionService.kt | 14 +- android/app/src/main/res/raw/listening.mp3 | Bin 0 -> 10221 bytes lib/core/bindings/initial_binding.dart | 9 + lib/data/services/azure_asr_service.dart | 136 ++- lib/data/services/azure_tts_service.dart | 94 ++- lib/data/services/language_manager.dart | 532 ++++++++++++ .../services/volcano_translation_service.dart | 65 +- .../chat/controllers/chat_controller.dart | 6 +- .../azure_asr_test_controller.dart | 65 +- .../bindings/translation_binding.dart | 5 +- .../controllers/translation_controller.dart | 739 ++++++++--------- .../translation_detail_controller.dart | 183 ++-- .../translation_history_controller.dart | 31 +- .../views/translation_detail_view.dart | 783 ++++++++++-------- .../views/translation_history_view.dart | 17 +- .../translation/views/translation_view.dart | 61 +- 19 files changed, 2169 insertions(+), 1097 deletions(-) create mode 100644 android/app/src/main/res/raw/listening.mp3 create mode 100644 lib/data/services/language_manager.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 673658c39..da501829b 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 @@ -20,6 +20,8 @@ class AzureAsrHelper { private var currentLanguage = "zh-CN" private var subscriptionKey = "" private var serviceRegion = "" + private var isAutoDetectLanguage = false + private var supportedLanguages = arrayOf("zh-CN", "en-US") // 回音消除相关 private var echoCanceler: AcousticEchoCanceler? = null @@ -33,9 +35,10 @@ class AzureAsrHelper { private val executorService = Executors.newSingleThreadExecutor() // 初始化 SDK - fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { + fun initialize(subscriptionKey: String, serviceRegion: String, + supportedLanguages: Array = arrayOf("zh-CN", "en-US")): Boolean { try { - Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, language=$language") + Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, supportedLanguages=${supportedLanguages.joinToString()}") // 检查配置是否为空 if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) { @@ -45,14 +48,36 @@ class AzureAsrHelper { this.subscriptionKey = subscriptionKey this.serviceRegion = serviceRegion - this.currentLanguage = language + + // 设置支持的语言列表 + if (supportedLanguages.isNotEmpty()) { + this.supportedLanguages = supportedLanguages + } + + // 根据支持的语言数量决定是否启用自动语言检测 + this.isAutoDetectLanguage = supportedLanguages.size >= 2 + + // 如果只有一种语言,设置为当前语言 + if (!isAutoDetectLanguage && supportedLanguages.isNotEmpty()) { + this.currentLanguage = supportedLanguages[0] + } // 释放之前的资源 dispose() // 创建语音配置 speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) - speechConfig?.speechRecognitionLanguage = language + + // 设置为 CONVERSATION 模式 + speechConfig?.setProperty(PropertyId.SpeechServiceConnection_RecoMode, "CONVERSATION") + + if (isAutoDetectLanguage) { + // 设置自动语言检测模式为连续检测 + speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous") + } else { + // 设置指定的识别语言 + speechConfig?.speechRecognitionLanguage = currentLanguage + } // 创建 PushAudioInputStream 和 AudioConfig try { @@ -65,7 +90,14 @@ class AzureAsrHelper { // 创建识别器 Log.d(TAG, "创建语音识别器") - recognizer = SpeechRecognizer(speechConfig, audioConfig) + if (isAutoDetectLanguage) { + // 创建自动语言检测配置 + val autoDetectSourceLanguageConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) + recognizer = SpeechRecognizer(speechConfig, autoDetectSourceLanguageConfig, audioConfig) + } else { + recognizer = SpeechRecognizer(speechConfig, audioConfig) + } + Log.d(TAG, "Azure 语音服务初始化成功") return true } catch (e: Exception) { @@ -123,9 +155,11 @@ class AzureAsrHelper { return } - // 如果指定了新的语言,需要重新初始化 - if (language != null && language != currentLanguage) { - initialize(subscriptionKey, serviceRegion, language) + // 如果指定了新的语言,且不是自动检测模式,需要重新初始化 + if (language != null && language != currentLanguage && !isAutoDetectLanguage) { + // 使用指定的语言创建新的语言列表 + val newLanguages = arrayOf(language) + initialize(subscriptionKey, serviceRegion, newLanguages) } try { @@ -138,7 +172,10 @@ class AzureAsrHelper { stopRecording() if (result != null && result.reason == ResultReason.RecognizedSpeech) { - callback.onResult(result.text) + val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(result) + val detectedLanguage = autoDetectSourceLanguageResult?.language ?: "" + Log.d(TAG, "识别结果: ${result.text}, 检测到的语言: $detectedLanguage") + callback.onResult(result.text, detectedLanguage) } else { callback.onError("未能识别语音") } @@ -149,7 +186,7 @@ class AzureAsrHelper { } // 开始连续语音识别 - fun startContinuousRecognition(language: String? = null, callback: ContinuousRecognizeCallback): Boolean { + fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { if (recognizer == null) { Log.e(TAG, "SpeechRecognizer 未初始化") callback.onError("SpeechRecognizer 未初始化") @@ -161,12 +198,6 @@ class AzureAsrHelper { Log.d(TAG, "已经在进行连续识别,先停止") stopContinuousRecognition(callback) } - - // 如果指定了新的语言,需要重新初始化 - if (language != null && language != currentLanguage) { - Log.d(TAG, "语言变更,重新初始化: $language") - initialize(subscriptionKey, serviceRegion, language) - } try { Log.d(TAG, "开始重置识别器和音频资源") @@ -203,7 +234,13 @@ class AzureAsrHelper { // 重新创建识别器 try { Log.d(TAG, "创建新的语音识别器") - recognizer = SpeechRecognizer(speechConfig, audioConfig) + if (isAutoDetectLanguage) { + // 创建自动语言检测配置 + val autoDetectSourceLanguageConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) + recognizer = SpeechRecognizer(speechConfig, autoDetectSourceLanguageConfig, audioConfig) + } else { + recognizer = SpeechRecognizer(speechConfig, audioConfig) + } } catch (e: Exception) { Log.e(TAG, "创建识别器失败: ${e.message}", e) callback.onError("创建识别器失败: ${e.message}") @@ -216,8 +253,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) + val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result) + val detectedLanguage = autoDetectSourceLanguageResult?.language ?: "" + Log.d(TAG, "识别结果: ${event.result.text}, 检测到的语言: $detectedLanguage") + callback.onResult(event.result.text, detectedLanguage) } else { Log.d(TAG, "识别结果原因: ${event.result.reason}") } @@ -228,8 +267,12 @@ class AzureAsrHelper { recognizer.recognizing.addEventListener( EventHandler { _, event -> if (event.result.reason == ResultReason.RecognizingSpeech) { - // Log.d(TAG, "正在识别: ${event.result.text}") - callback.onRecognizing(event.result.text) + val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result) + val detectedLanguage = autoDetectSourceLanguageResult?.language ?: "" + if (isAutoDetectLanguage) { + Log.d(TAG, "正在识别: ${event.result.text}, 检测到的语言: $detectedLanguage") + } + callback.onRecognizing(event.result.text, detectedLanguage) } } ) @@ -452,14 +495,14 @@ class AzureAsrHelper { // 一次性识别回调接口 interface RecognizeCallback { - fun onResult(result: String) + fun onResult(result: String, detectedLanguage: String = "") fun onError(error: String) } // 连续识别回调接口 interface ContinuousRecognizeCallback { - fun onResult(result: String) - fun onRecognizing(recognizing: String) + fun onResult(result: String, detectedLanguage: String = "") + fun onRecognizing(recognizing: String, detectedLanguage: String = "") fun onSessionStarted() fun onSessionStopped() fun onCanceled(reason: String, errorDetails: String) @@ -536,5 +579,57 @@ class AzureAsrHelper { fun isEchoCancellationEnabled(): Boolean { return isEchoCancellationEnabled } - + + // 设置支持的语言列表(用于自动语言检测) + fun setSupportedLanguages(languages: Array) { + if (languages.isNotEmpty()) { + Log.d(TAG, "设置支持的语言列表: ${languages.joinToString()}") + + // 如果当前是自动检测模式且识别器已初始化,需要重新初始化以应用新的语言列表 + if (isAutoDetectLanguage && recognizer != null) { + Log.d(TAG, "自动检测模式下更新语言列表,重新初始化识别器") + initialize(subscriptionKey, serviceRegion, languages) + } else { + // 仅更新语言列表,不重新初始化 + supportedLanguages = languages + + // 如果不是自动检测模式且只有一种语言,更新当前语言 + if (!isAutoDetectLanguage && languages.size == 1) { + currentLanguage = languages[0] + } + } + } + } + + // 获取当前是否启用自动语言检测 + fun isAutoDetectLanguageEnabled(): Boolean { + return isAutoDetectLanguage + } + + // 启用自动语言检测并设置支持的语言列表 + fun enableAutoDetectLanguage(enable: Boolean, languages: Array? = null): Boolean { + // 准备语言列表 + val languagesToUse = if (languages != null && languages.isNotEmpty()) { + // 如果提供了新的语言列表,使用新的列表 + languages + } else if (enable && supportedLanguages.size < 2) { + // 如果要启用自动检测但当前支持的语言少于2种,使用默认的两种语言 + arrayOf("zh-CN", "en-US") + } else { + // 否则使用当前的语言列表 + supportedLanguages + } + + // 如果要禁用自动检测,但提供了多种语言,只使用第一种语言 + val finalLanguages = if (!enable && languagesToUse.size > 1) { + arrayOf(languagesToUse[0]) + } else { + languagesToUse + } + + Log.d(TAG, "设置自动语言检测: enable=$enable, languages=${finalLanguages.joinToString()}") + + // 重新初始化以应用新的设置 + return initialize(subscriptionKey, serviceRegion, finalLanguages) + } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt index 58ec8338d..b05e1a442 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt @@ -1,5 +1,10 @@ package com.example.deep_voice +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioDeviceInfo +import android.media.AudioManager +import android.os.Build import android.util.Log import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.audio.* @@ -17,6 +22,18 @@ class AzureTtsHelper { private var speechConfig: SpeechConfig? = null private var synthesizer: SpeechSynthesizer? = null private var isInitialized = false + private var context: Context? = null + private var audioManager: AudioManager? = null + + // 音频输出设备类型 + enum class AudioOutputType { + SPEAKER, // 扬声器 + EARPIECE, // 听筒 + AUTO // 自动选择(有耳机用耳机,没有则用听筒) + } + + // 当前音频输出设备类型 + private var currentAudioOutputType = AudioOutputType.AUTO // 用于异步操作的信号量 private val semaphore = Semaphore(0) @@ -33,9 +50,15 @@ class AzureTtsHelper { * @param subscriptionKey Azure 语音服务订阅密钥 * @param serviceRegion Azure 语音服务区域 * @param language 可选,默认语言,默认为 "zh-CN" + * @param context Android 上下文,用于获取音频服务 */ - fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { + fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN", context: Context? = null): Boolean { try { + this.context = context + if (context != null) { + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + } + // 创建语音配置 speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) @@ -60,6 +83,12 @@ class AzureTtsHelper { isInitialized = true Log.d(TAG, "TTS 引擎初始化成功") + + // 设置默认音频输出类型为自动 + if (audioManager != null) { + setAudioOutputType(AudioOutputType.AUTO) + } + return true } catch (e: Exception) { Log.e(TAG, "TTS 引擎初始化失败: ${e.message}") @@ -69,29 +98,129 @@ class AzureTtsHelper { } /** - * 设置语音 + * 设置音频输出设备类型 * - * @param voiceName 语音名称,例如 "zh-CN-XiaoxiaoNeural" + * @param outputType 音频输出设备类型 + * @return 是否设置成功 */ - fun setVoice(voiceName: String): Boolean { + fun setAudioOutputType(outputType: AudioOutputType): Boolean { if (!isInitialized) { Log.e(TAG, "TTS 引擎尚未初始化") return false } + if (audioManager == null) { + Log.e(TAG, "AudioManager 未初始化,无法设置音频输出设备") + return false + } + try { - currentVoiceName = voiceName - speechConfig?.setSpeechSynthesisVoiceName(voiceName) + currentAudioOutputType = outputType - // 重新创建合成器以应用新的语音设置 - val audioConfig = AudioConfig.fromDefaultSpeakerOutput() + when (outputType) { + AudioOutputType.SPEAKER -> { + // 使用扬声器 + audioManager?.mode = AudioManager.MODE_NORMAL + audioManager?.isSpeakerphoneOn = true + Log.d(TAG, "已设置音频输出设备为扬声器") + } + AudioOutputType.EARPIECE -> { + // 使用听筒 + audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + audioManager?.isSpeakerphoneOn = false + Log.d(TAG, "已设置音频输出设备为听筒") + } + AudioOutputType.AUTO -> { + // 检查是否有耳机连接 + if (isHeadsetConnected()) { + // 有耳机,使用耳机 + audioManager?.mode = AudioManager.MODE_NORMAL + audioManager?.isSpeakerphoneOn = false + Log.d(TAG, "已设置音频输出设备为耳机") + } else { + // 无耳机,使用听筒 + audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION + audioManager?.isSpeakerphoneOn = false + Log.d(TAG, "已设置音频输出设备为听筒") + } + } + } + + // 重新创建合成器以应用新的音频设置 + recreateSynthesizer() + + return true + } catch (e: Exception) { + Log.e(TAG, "设置音频输出设备失败: ${e.message}") + e.printStackTrace() + return false + } + } + + /** + * 检查是否有耳机连接 + */ + private fun isHeadsetConnected(): Boolean { + if (audioManager == null) return false + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // 使用新的 API 检查音频设备 + val devices = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + devices?.any { device -> + device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || + device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP || + device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO + } ?: false + } else { + // 使用旧的 API 检查耳机 + audioManager?.isWiredHeadsetOn == true || audioManager?.isBluetoothA2dpOn == true + } + } + + /** + * 重新创建语音合成器 + */ + private fun recreateSynthesizer() { + try { + // 关闭旧的合成器 synthesizer?.close() + + // 创建音频配置 + val audioConfig = AudioConfig.fromDefaultSpeakerOutput() + + // 创建新的合成器 synthesizer = SpeechSynthesizer(speechConfig, audioConfig) // 重新设置事件监听 synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCanceled?.addEventListener { _, _ -> semaphore.release() } + Log.d(TAG, "已重新创建语音合成器") + } catch (e: Exception) { + Log.e(TAG, "重新创建语音合成器失败: ${e.message}") + e.printStackTrace() + } + } + + /** + * 设置语音 + * + * @param voiceName 语音名称,例如 "zh-CN-XiaoxiaoNeural" + */ + fun setVoice(voiceName: String): Boolean { + if (!isInitialized) { + Log.e(TAG, "TTS 引擎尚未初始化") + return false + } + + try { + currentVoiceName = voiceName + speechConfig?.setSpeechSynthesisVoiceName(voiceName) + + // 重新创建合成器以应用新的语音设置 + recreateSynthesizer() + Log.d(TAG, "已设置语音: $voiceName") return true } catch (e: Exception) { @@ -143,6 +272,11 @@ class AzureTtsHelper { try { Log.d(TAG, "开始合成文本: $text") + // 如果设置为自动模式,检查耳机状态并更新设置 + if (currentAudioOutputType == AudioOutputType.AUTO) { + setAudioOutputType(AudioOutputType.AUTO) + } + // 生成 SSML val ssml = generateSsml(text) @@ -245,7 +379,7 @@ class AzureTtsHelper { try { synthesizer?.StopSpeakingAsync() - Log.d(TAG, "已停止语音合成kt") + Log.d(TAG, "已停止语音合成") return true } catch (e: Exception) { Log.e(TAG, "停止语音合成失败: ${e.message}") @@ -262,6 +396,13 @@ class AzureTtsHelper { stopSpeaking() synthesizer?.close() speechConfig?.close() + + // 恢复音频模式 + if (audioManager != null) { + audioManager?.mode = AudioManager.MODE_NORMAL + audioManager?.isSpeakerphoneOn = false + } + isInitialized = false Log.d(TAG, "TTS 引擎已释放") } catch (e: Exception) { 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 63835d178..96b4dc652 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 @@ -1,22 +1,16 @@ package com.example.deep_voice import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Environment -import android.provider.Settings 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 @@ -24,7 +18,6 @@ import android.os.Looper class MainActivity: FlutterActivity() { - private val AUDIO_PROCESSING_CHANNEL = "com.deep_voice.audio_processing" private val AZURE_ASR_CHANNEL = "com.deep_voice.azure_asr" private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events" private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts" @@ -69,12 +62,9 @@ class MainActivity: FlutterActivity() { // 使用伴生对象存储配置信息,使其在整个应用中可访问 companion object { // Azure 语音服务配置 - var azureSpeechKey: String = "2WDq2KL45QUC5Gk0o8LgQ5bOpnV1kF0ZAEWBTl76AUoG90bbYO4uJQQJ99BCAC3pKaRXJ3w3AAAYACOGzvU1" - var azureSpeechRegion: String = "eastasia" - var volcanoAiApiKey: String = "168deb3d-fd0c-4912-b9f1-aaee5c6743e6" - - // 标记是否已初始化 - var isAzureHelpersInitialized = false + var azureSpeechKey: String = "" + var azureSpeechRegion: String = "" + var volcanoAiApiKey: String = "" } override fun onCreate(savedInstanceState: Bundle?) { @@ -85,21 +75,9 @@ class MainActivity: FlutterActivity() { azureAsrHelper = AzureAsrHelper() azureTtsHelper = AzureTtsHelper() - // 初始化 Azure 服务 - // if (azureSpeechKey.isNotEmpty() && azureSpeechRegion.isNotEmpty()) { - // Log.d(TAG, "初始化本地实例的 Azure 服务配置") - // // 初始化ASR - // azureAsrHelper.initialize(azureSpeechKey, azureSpeechRegion, "zh-CN") - // // 初始化TTS - // azureTtsHelper.initialize(azureSpeechKey, azureSpeechRegion, "zh-CN") - // } else { - // Log.e(TAG, "Azure配置信息不完整,无法初始化本地实例的Azure服务") - // } - // 注册广播接收器 registerVoiceInteractionReceiver() - // 确保 Flutter 引擎准备好后立即设置事件通道 flutterEngine?.let { engine -> Log.d(TAG, "Flutter 引擎已存在,立即设置事件通道") @@ -122,7 +100,6 @@ class MainActivity: FlutterActivity() { Log.d(TAG, "开始配置 Flutter 引擎") // 先设置事件通道,确保在插件注册之前就已准备好 - Log.d(TAG, "在插件注册前设置事件通道") setupEventChannels(flutterEngine) // 然后调用父类方法和注册插件 @@ -143,65 +120,44 @@ class MainActivity: FlutterActivity() { 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") + Log.d(TAG, "语音交互事件通道开始监听") voiceInteractionEventSink = events - - // 通知 Flutter 端事件通道已准备好 - try { - events?.success(mapOf("type" to "channelReady")) - Log.d(TAG, "已发送 channelReady 事件") - - } catch (e: Exception) { - Log.e(TAG, "发送 channelReady 事件失败: ${e.message}", e) - } + events?.success(mapOf("type" to "channelReady")) } 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") + Log.d(TAG, "ASR事件通道开始监听") 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) { - - // Azure ASR 方法通道 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { @@ -209,6 +165,8 @@ class MainActivity: FlutterActivity() { val subscriptionKey = call.argument("subscriptionKey") val region = call.argument("region") val language = call.argument("language") ?: "zh-CN" + val autoDetectLanguage = call.argument("autoDetectLanguage") ?: false + val supportedLanguages = call.argument>("supportedLanguages")?.toTypedArray() ?: arrayOf("zh-CN", "en-US") if (subscriptionKey == null || region == null) { result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null) @@ -216,7 +174,7 @@ class MainActivity: FlutterActivity() { } try { - val success = azureAsrHelper.initialize(subscriptionKey, region, language) + val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages) result.success(success) } catch (e: Exception) { result.error("INITIALIZATION_ERROR", e.message, null) @@ -226,8 +184,11 @@ class MainActivity: FlutterActivity() { val language = call.argument("language") azureAsrHelper.recognizeOnce(language, object : AzureAsrHelper.RecognizeCallback { - override fun onResult(text: String) { - result.success(text) + override fun onResult(text: String, detectedLanguage: String) { + result.success(mapOf( + "text" to text, + "detectedLanguage" to detectedLanguage + )) } override fun onError(error: String) { @@ -236,24 +197,27 @@ class MainActivity: FlutterActivity() { }) } "startContinuousRecognition" -> { - val language = call.argument("language") - // 确保事件通道已准备好 - if (!isEventChannelReady()) { - Log.w(TAG, "事件通道未准备好,等待...") - if (!waitForEventChannel()) { - result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null) - return@setMethodCallHandler - } + if (azureAsrEventSink == null) { + result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null) + return@setMethodCallHandler } - val success = azureAsrHelper.startContinuousRecognition(language, object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String) { - sendAsrEvent(mapOf("type" to "result", "text" to text)) + val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { + override fun onResult(text: String, detectedLanguage: String) { + sendAsrEvent(mapOf( + "type" to "result", + "text" to text, + "detectedLanguage" to detectedLanguage + )) } - override fun onRecognizing(recognizing: String) { - sendAsrEvent(mapOf("type" to "recognizing", "text" to recognizing)) + override fun onRecognizing(recognizing: String, detectedLanguage: String) { + sendAsrEvent(mapOf( + "type" to "recognizing", + "text" to recognizing, + "detectedLanguage" to detectedLanguage + )) } override fun onSessionStarted() { @@ -286,8 +250,8 @@ class MainActivity: FlutterActivity() { } val success = azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String) {} - override fun onRecognizing(recognizing: String) {} + override fun onResult(text: String, detectedLanguage: String) {} + override fun onRecognizing(recognizing: String, detectedLanguage: String) {} override fun onSessionStarted() {} override fun onSessionStopped() {} override fun onCanceled(reason: String, errorDetails: String) {} @@ -307,6 +271,37 @@ class MainActivity: FlutterActivity() { azureAsrHelper.dispose() result.success(true) } + "enableAutoDetectLanguage" -> { + val enable = call.argument("enable") ?: false + val languages = call.argument>("languages")?.toTypedArray() + + try { + val success = azureAsrHelper.enableAutoDetectLanguage(enable, languages) + result.success(success) + } catch (e: Exception) { + result.error("AUTO_DETECT_ERROR", e.message, null) + } + } + + "setSupportedLanguages" -> { + val languages = call.argument>("languages")?.toTypedArray() + + if (languages == null || languages.isEmpty()) { + result.error("INVALID_ARGUMENTS", "语言列表不能为空", null) + return@setMethodCallHandler + } + + try { + azureAsrHelper.setSupportedLanguages(languages) + result.success(true) + } catch (e: Exception) { + result.error("SET_LANGUAGES_ERROR", e.message, null) + } + } + + "isAutoDetectLanguageEnabled" -> { + result.success(azureAsrHelper.isAutoDetectLanguageEnabled()) + } else -> { result.notImplemented() } @@ -327,7 +322,7 @@ class MainActivity: FlutterActivity() { } try { - val success = azureTtsHelper.initialize(subscriptionKey, region, language) + val success = azureTtsHelper.initialize(subscriptionKey, region, language, this) result.success(success) } catch (e: Exception) { result.error("INITIALIZATION_ERROR", e.message, null) @@ -343,6 +338,16 @@ class MainActivity: FlutterActivity() { val volume = call.argument("volume") ?: 100 result.success(azureTtsHelper.setSpeechParams(rate, pitch, volume)) } + "setAudioOutputType" -> { + val outputTypeStr = call.argument("outputType") ?: "speaker" + val outputType = when (outputTypeStr.lowercase()) { + "speaker" -> AzureTtsHelper.AudioOutputType.SPEAKER + "earpiece" -> AzureTtsHelper.AudioOutputType.EARPIECE + "auto" -> AzureTtsHelper.AudioOutputType.AUTO + else -> AzureTtsHelper.AudioOutputType.SPEAKER + } + result.success(azureTtsHelper.setAudioOutputType(outputType)) + } "speakText" -> { val text = call.argument("text") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "文本不能为空", null) @@ -407,7 +412,7 @@ class MainActivity: FlutterActivity() { } // ASR 事件发送方法 - fun sendAsrEvent(event: Map) { + private fun sendAsrEvent(event: Map) { if (azureAsrEventSink == null) return runOnUiThread { @@ -415,29 +420,11 @@ class MainActivity: FlutterActivity() { } } - // 检查事件通道是否已准备好 - fun isEventChannelReady(): Boolean { - return azureAsrEventSink != null - } - - // 等待事件通道准备好 - fun waitForEventChannel(timeoutMs: Long = 5000): Boolean { - val startTime = System.currentTimeMillis() - while (azureAsrEventSink == null) { - if (System.currentTimeMillis() - startTime > timeoutMs) { - Log.e(TAG, "等待事件通道超时") - return false - } - Thread.sleep(100) - } - return true - } - /** * 启动语音交互服务(带配置参数) */ private fun startVoiceInteractionService(call: MethodCall, result: MethodChannel.Result) { - Log.d(TAG, "启动语音交互服务(带配置参数)") + Log.d(TAG, "启动语音交互服务") // 获取配置参数 val key = call.argument("azure_speech_key") ?: "" @@ -449,9 +436,6 @@ class MainActivity: FlutterActivity() { azureSpeechRegion = region volcanoAiApiKey = aiKey - Log.d(TAG, "配置参数: key=${if (key.isNotEmpty()) key.take(5) + "..." else "空"}, region=$region, aiKey=${if (aiKey.isNotEmpty()) aiKey.take(5) + "..." else "空"}") - - // 创建 Intent 并启动服务 val serviceIntent = Intent(this, VoiceInteractionService::class.java) @@ -462,7 +446,6 @@ class MainActivity: FlutterActivity() { startService(serviceIntent) } - Log.d(TAG, "语音交互服务启动命令已发送") result.success(true) } @@ -473,7 +456,6 @@ class MainActivity: FlutterActivity() { Log.d(TAG, "停止语音交互服务") val serviceIntent = Intent(this, VoiceInteractionService::class.java) stopService(serviceIntent) - Log.d(TAG, "语音交互服务停止命令已发送") } /** @@ -485,7 +467,6 @@ class MainActivity: FlutterActivity() { action = VoiceInteractionService.ACTION_PAUSE_VOICE_INTERACTION } - // 在 Android O 及以上版本,需要使用 startForegroundService if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(intent) } else { @@ -502,8 +483,6 @@ class MainActivity: FlutterActivity() { return } - Log.d(TAG, "发送聊天记录事件到Flutter: agentId=$agentId, userMessage=${userMessage.take(20)}..., assistantMessage=${assistantMessage.take(20)}...") - runOnUiThread { try { val eventData = mapOf( @@ -514,7 +493,6 @@ class MainActivity: FlutterActivity() { "timestamp" to timestamp ) voiceInteractionEventSink?.success(eventData) - Log.d(TAG, "聊天记录事件已发送到Flutter") } catch (e: Exception) { Log.e(TAG, "发送聊天记录事件到Flutter失败: ${e.message}") } @@ -526,62 +504,19 @@ class MainActivity: FlutterActivity() { */ private fun sendVoiceInteractionEvent(event: Map) { if (voiceInteractionEventSink == null) { - Log.e(TAG, "无法发送语音交互事件:事件通道未准备好,事件内容: $event") + Log.e(TAG, "无法发送语音交互事件:事件通道未准备好") 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) } } } - /** - * 自动启动语音交互服务 - */ - private fun autoStartVoiceInteractionService() { - // 检查服务是否已经在运行 - if (VoiceInteractionService.isServiceRunning()) { - Log.d(TAG, "语音交互服务已经在运行,无需重复启动") - return - } - - Log.d(TAG, "自动启动语音交互服务") - - try { - // 创建 Intent 并启动服务 - val serviceIntent = Intent(this, VoiceInteractionService::class.java) - - // 在 Android O 及以上版本,需要使用 startForegroundService - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Log.d(TAG, "使用startForegroundService启动服务") - startForegroundService(serviceIntent) - } else { - Log.d(TAG, "使用startService启动服务") - startService(serviceIntent) - } - - Log.d(TAG, "语音交互服务自动启动命令已发送") - - // 检查服务是否成功启动 - Handler(Looper.getMainLooper()).postDelayed({ - if (VoiceInteractionService.isServiceRunning()) { - Log.d(TAG, "语音交互服务已成功启动") - } else { - Log.e(TAG, "语音交互服务启动失败") - } - }, 1000) - } catch (e: Exception) { - Log.e(TAG, "启动语音交互服务时出错: ${e.message}", e) - } - } - override fun onDestroy() { // 注销广播接收器 try { @@ -590,10 +525,11 @@ class MainActivity: FlutterActivity() { 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 eca03395c..a56c872cc 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 @@ -162,7 +162,7 @@ class VoiceInteractionService : Service() { if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { // 初始化ASR - azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") + azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN")) // 初始化TTS azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") } else { @@ -392,7 +392,7 @@ class VoiceInteractionService : Service() { val subscriptionKey = MainActivity.azureSpeechKey val serviceRegion = MainActivity.azureSpeechRegion if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { - azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") + azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN")) } } @@ -424,8 +424,8 @@ class VoiceInteractionService : Service() { updateLastActivityTime() try { - azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onRecognizing(recognizing: String) { + azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { + override fun onRecognizing(recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()) { hasSpeechDetected = true stopCurrentTTS() @@ -433,7 +433,7 @@ class VoiceInteractionService : Service() { } } - override fun onResult(result: String) { + override fun onResult(result: String, detectedLanguage: String) { if (result.isNotEmpty()) { processWithVolcanoAI(result) } @@ -484,8 +484,8 @@ class VoiceInteractionService : Service() { try { azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onResult(result: String) {} - override fun onRecognizing(recognizing: String) {} + override fun onResult(result: String, detectedLanguage: String) {} + override fun onRecognizing(recognizing: String, detectedLanguage: String) {} override fun onSessionStarted() {} override fun onSessionStopped() { isRecognitionActive = false diff --git a/android/app/src/main/res/raw/listening.mp3 b/android/app/src/main/res/raw/listening.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..cff159b455219c220bbdf1861ebcb293f4e72318 GIT binary patch literal 10221 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 literal 0 HcmV?d00001 diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index f3da3c799..d6df1de2e 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -4,6 +4,8 @@ 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 '../../data/services/language_manager.dart'; +import '../../data/services/volcano_ai_service.dart'; import '../../core/utils/logger.dart'; /// 初始绑定,用于管理全局依赖 @@ -11,6 +13,10 @@ class InitialBinding extends Bindings { @override void dependencies() { Logger.warning('InitialBinding dependencies'); + + // 语言管理器(需要最先初始化) + Get.put(LanguageManager(), permanent: true); + // Azure 语音识别服务 Get.lazyPut(() => AzureAsrService(), fenix: true); @@ -22,6 +28,9 @@ class InitialBinding extends Bindings { // 火山翻译服务 Get.lazyPut(() => VolcanoTranslationService(), fenix: true); + + // 火山AI服务 + Get.lazyPut(() => VolcanoAIService(), fenix: true); // 注意:VoiceInteractionService 在 HomeController 中初始化 Get.lazyPut(() => VoiceInteractionService(), fenix: true); diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/azure_asr_service.dart index 25766c2ed..c24ebd480 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/azure_asr_service.dart @@ -19,6 +19,12 @@ class AzureAsrService { String _currentLanguage = 'zh-CN'; String get currentLanguage => _currentLanguage; + // 自动语言检测相关 + bool _isAutoDetectLanguageEnabled = false; + bool get isAutoDetectLanguageEnabled => _isAutoDetectLanguageEnabled; + List _supportedLanguages = ['zh-CN', 'en-US']; + List get supportedLanguages => _supportedLanguages; + // 连续识别相关 bool _isContinuousRecognitionActive = false; StreamController? _eventStreamController; @@ -36,6 +42,10 @@ class AzureAsrService { String _latestRecognizedText = ''; String get latestRecognizedText => _latestRecognizedText; + // 最新检测到的语言 + String _latestDetectedLanguage = ''; + String get latestDetectedLanguage => _latestDetectedLanguage; + AzureAsrService() { _loadConfig(); _setupEventChannel(); @@ -91,10 +101,21 @@ class AzureAsrService { /// 初始化微软语音识别 SDK /// /// 返回 true 表示初始化成功,否则抛出 PlatformException - Future initialize({String language = 'zh-CN'}) async { + Future initialize({ + String language = 'zh-CN', + bool autoDetectLanguage = false, + List? supportedLanguages, + }) async { _currentLanguage = language; + _isAutoDetectLanguageEnabled = autoDetectLanguage; + + if (supportedLanguages != null && supportedLanguages.isNotEmpty) { + _supportedLanguages = supportedLanguages; + } - if (_isInitialized && _currentLanguage == language) { + if (_isInitialized && + _currentLanguage == language && + _isAutoDetectLanguageEnabled == autoDetectLanguage) { return true; } @@ -107,6 +128,8 @@ class AzureAsrService { 'subscriptionKey': _subscriptionKey, 'region': _serviceRegion, 'language': language, + 'autoDetectLanguage': autoDetectLanguage, + 'supportedLanguages': _supportedLanguages, }); _isInitialized = result; return result; @@ -126,17 +149,30 @@ class AzureAsrService { /// 执行一次性语音识别 /// - /// 返回识别的文本,如果识别失败则抛出 PlatformException - Future recognizeSpeech() async { + /// 返回识别的文本和检测到的语言,如果识别失败则抛出 PlatformException + Future recognizeSpeech() async { if (!_isInitialized) { await initialize(); } try { - final String result = await _channel.invokeMethod('recognizeOnce', { + final result = await _channel.invokeMethod('recognizeOnce', { 'language': _currentLanguage, }); - return result; + + if (result is Map) { + final String text = result['text'] as String? ?? ''; + final String detectedLanguage = result['detectedLanguage'] as String? ?? ''; + _latestRecognizedText = text; + _latestDetectedLanguage = detectedLanguage; + return RecognitionResult(text: text, detectedLanguage: detectedLanguage); + } else if (result is String) { + // 兼容旧版本 + _latestRecognizedText = result; + return RecognitionResult(text: result); + } + + throw Exception('无效的识别结果格式'); } catch (e) { Logger.error('语音识别失败: $e'); throw e; @@ -158,9 +194,7 @@ class AzureAsrService { try { _eventStreamController = StreamController.broadcast(); - final bool result = await _channel.invokeMethod('startContinuousRecognition', { - 'language': _currentLanguage, - }); + final bool result = await _channel.invokeMethod('startContinuousRecognition'); if (!result) { _cleanupEventStream(); @@ -210,18 +244,23 @@ class AzureAsrService { switch (eventType) { case 'result': final String text = eventMap['text'] as String? ?? ''; + final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; _latestRecognizedText = text; + _latestDetectedLanguage = detectedLanguage; _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.finalResult, text: text, + detectedLanguage: detectedLanguage, )); break; case 'recognizing': final String text = eventMap['text'] as String? ?? ''; + final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.intermediateResult, text: text, + detectedLanguage: detectedLanguage, )); break; @@ -293,6 +332,60 @@ class AzureAsrService { _isInitialized = false; } } + + /// 启用或禁用自动语言检测 + Future enableAutoDetectLanguage(bool enable, {List? languages}) async { + if (!_isInitialized) { + await initialize(autoDetectLanguage: enable, supportedLanguages: languages); + return _isAutoDetectLanguageEnabled; + } + + try { + final bool result = await _channel.invokeMethod('enableAutoDetectLanguage', { + 'enable': enable, + 'languages': languages, + }); + + if (result) { + _isAutoDetectLanguageEnabled = enable; + if (languages != null && languages.isNotEmpty) { + _supportedLanguages = languages; + } + } + + return result; + } catch (e) { + Logger.error('设置自动语言检测失败: $e'); + throw e; + } + } + + /// 设置支持的语言列表(用于自动语言检测) + Future setSupportedLanguages(List languages) async { + if (languages.isEmpty) { + throw ArgumentError('语言列表不能为空'); + } + + if (!_isInitialized) { + await initialize(supportedLanguages: languages); + return true; + } + + try { + final bool result = await _channel.invokeMethod('setSupportedLanguages', { + 'languages': languages, + }); + + if (result) { + _supportedLanguages = languages; + } + + return result; + } catch (e) { + Logger.error('设置支持的语言列表失败: $e'); + throw e; + } + } } /// 识别事件类型 @@ -324,17 +417,40 @@ class RecognitionEvent { /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) final String text; + /// 检测到的语言(仅在启用自动语言检测时有效) + final String detectedLanguage; + /// 错误信息(仅在 error 和 canceled 类型中有效) final String error; RecognitionEvent({ required this.type, this.text = '', + this.detectedLanguage = '', this.error = '', }); @override String toString() { - return 'RecognitionEvent{type: $type, text: $text, error: $error}'; + return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; + } +} + +/// 识别结果 +class RecognitionResult { + /// 识别文本 + final String text; + + /// 检测到的语言(仅在启用自动语言检测时有效) + final String detectedLanguage; + + RecognitionResult({ + required this.text, + this.detectedLanguage = '', + }); + + @override + String toString() { + return 'RecognitionResult{text: $text, detectedLanguage: $detectedLanguage}'; } } \ No newline at end of file diff --git a/lib/data/services/azure_tts_service.dart b/lib/data/services/azure_tts_service.dart index 47d136de3..005dc9b5e 100644 --- a/lib/data/services/azure_tts_service.dart +++ b/lib/data/services/azure_tts_service.dart @@ -13,6 +13,18 @@ class AzureTtsException implements Exception { String toString() => message; } +/// 音频输出设备类型 +enum AudioOutputType { + /// 扬声器 + speaker, + + /// 听筒 + earpiece, + + /// 自动选择(有耳机用耳机,没有则用听筒) + auto +} + /// 微软 Text-to-Speech 服务 /// /// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, @@ -30,6 +42,7 @@ class AzureTtsService extends GetxService { int _currentRate = 0; int _currentPitch = 0; int _currentVolume = 100; + AudioOutputType _currentAudioOutputType = AudioOutputType.auto; // 语音合成队列 final List<_SpeechItem> _textQueue = []; @@ -42,8 +55,10 @@ class AzureTtsService extends GetxService { // 可观察状态 final isEnabled = true.obs; final isSpeaking = false.obs; + final audioOutputType = AudioOutputType.auto.obs; String get currentVoice => _currentVoice; + AudioOutputType get currentAudioOutputType => _currentAudioOutputType; AzureTtsService() { _loadConfig(); @@ -72,6 +87,15 @@ class AzureTtsService extends GetxService { }); _isInitialized = result; + + // 确保设置默认的音频输出设备类型 + if (result) { + // 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值 + // 只需要确保 Dart 层的状态与原生层一致 + _currentAudioOutputType = AudioOutputType.auto; + audioOutputType.value = AudioOutputType.auto; + } + return result; } catch (e) { Logger.error('初始化失败: $e'); @@ -82,6 +106,7 @@ class AzureTtsService extends GetxService { /// 设置语音 Future setVoice(String voiceName) async { if (!_isInitialized) await initialize(); + if (voiceName == _currentVoice) return true; try { final result = await _channel.invokeMethod('setVoice', { @@ -96,6 +121,28 @@ class AzureTtsService extends GetxService { } } + /// 设置音频输出设备类型 + Future setAudioOutputType(AudioOutputType outputType) async { + if (!_isInitialized) await initialize(); + + try { + final String outputTypeStr = outputType.toString().split('.').last; + final result = await _channel.invokeMethod('setAudioOutputType', { + 'outputType': outputTypeStr, + }); + + if (result) { + _currentAudioOutputType = outputType; + audioOutputType.value = outputType; + } + + return result; + } catch (e) { + Logger.error('设置音频输出设备失败: $e'); + return false; + } + } + /// 设置语音参数 Future setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async { if (!_isInitialized) await initialize(); @@ -167,9 +214,14 @@ class AzureTtsService extends GetxService { } /// 添加文本到队列并播放 - Future speak(String text, {int? rate, int? pitch, int? volume}) async { + Future speak(String text, {int? rate, int? pitch, int? volume, AudioOutputType? outputType}) async { if (!isEnabled.value || text.isEmpty) return false; + // 如果指定了新的音频输出设备类型,先设置 + if (outputType != null && outputType != _currentAudioOutputType) { + await setAudioOutputType(outputType); + } + _textQueue.add(_SpeechItem( text: text, rate: rate ?? _currentRate, @@ -184,9 +236,14 @@ class AzureTtsService extends GetxService { /// 处理AI流式输出文本 /// /// 将流式输出的文本片段添加到缓冲区,并在检测到完整句子时将其送入播放队列 - Future processStreamText(String textChunk) async { + Future processStreamText(String textChunk, {AudioOutputType? outputType}) async { if (!isEnabled.value || textChunk.isEmpty) return false; + // 如果指定了新的音频输出设备类型,先设置 + if (outputType != null && outputType != _currentAudioOutputType) { + await setAudioOutputType(outputType); + } + // 添加文本到缓冲区 _streamBuffer += textChunk; @@ -242,9 +299,14 @@ class AzureTtsService extends GetxService { /// 强制处理缓冲区中的剩余文本 /// /// 在流式输出结束时调用,确保缓冲区中的所有文本都被处理 - Future flushStreamBuffer() async { + Future flushStreamBuffer({AudioOutputType? outputType}) async { if (!isEnabled.value || _streamBuffer.isEmpty) return false; + // 如果指定了新的音频输出设备类型,先设置 + if (outputType != null && outputType != _currentAudioOutputType) { + await setAudioOutputType(outputType); + } + // 先尝试使用processStreamText处理可能的完整句子 await processStreamText(''); @@ -270,9 +332,14 @@ class AzureTtsService extends GetxService { } /// 连续播放多段文本 - Future speakMultiple(List texts, {int? rate, int? pitch, int? volume}) async { + Future speakMultiple(List texts, {int? rate, int? pitch, int? volume, AudioOutputType? outputType}) async { if (!isEnabled.value || texts.isEmpty) return false; + // 如果指定了新的音频输出设备类型,先设置 + if (outputType != null && outputType != _currentAudioOutputType) { + await setAudioOutputType(outputType); + } + for (final text in texts) { if (text.isNotEmpty) { _textQueue.add(_SpeechItem( @@ -374,6 +441,25 @@ class AzureTtsService extends GetxService { if (!isEnabled.value) stop(); } + /// 切换音频输出设备类型 + Future toggleAudioOutputType() async { + AudioOutputType newType; + + switch (_currentAudioOutputType) { + case AudioOutputType.speaker: + newType = AudioOutputType.earpiece; + break; + case AudioOutputType.earpiece: + newType = AudioOutputType.auto; + break; + case AudioOutputType.auto: + newType = AudioOutputType.speaker; + break; + } + + return await setAudioOutputType(newType); + } + /// 释放资源 Future dispose() async { if (!_isInitialized) return; diff --git a/lib/data/services/language_manager.dart b/lib/data/services/language_manager.dart new file mode 100644 index 000000000..c89b2b647 --- /dev/null +++ b/lib/data/services/language_manager.dart @@ -0,0 +1,532 @@ +import 'package:get/get.dart'; +import '../../core/utils/logger.dart'; + +/// 语言信息类,包含语言的各种表示形式 +class LanguageInfo { + /// 短代码,如 'zh', 'en' + final String shortCode; + + /// 中文名称 + final String chineseName; + + /// 英文名称 + final String englishName; + + /// Azure ASR 语言代码,如 'zh-CN', 'en-US' + final String asrCode; + + /// Azure TTS 语音名称 + final String ttsVoiceName; + + /// 备注信息 + final String? note; + + LanguageInfo({ + required this.shortCode, + required this.chineseName, + required this.englishName, + required this.asrCode, + required this.ttsVoiceName, + this.note, + }); +} + +/// 语言类型管理类,提供语言信息的查询和转换功能 +class LanguageManager extends GetxService { + // 单例实例 + static LanguageManager? _instance; + + // 单例工厂方法 + factory LanguageManager() { + _instance ??= LanguageManager._internal(); + return _instance!; + } + + // 私有构造函数 + LanguageManager._internal() { + _initLanguageMap(); + } + + // 语言信息映射表,以 ASR 代码为键 + final Map _languageMap = {}; + + // 中文名称到 ASR 代码的映射 + final Map _chineseNameToAsrCodeMap = {}; + + // 英文名称到 ASR 代码的映射 + final Map _englishNameToAsrCodeMap = {}; + + // 短代码到 ASR 代码的映射 + final Map _shortCodeToAsrCodeMap = {}; + + /// 初始化语言映射表 + void _initLanguageMap() { + // 中文相关 + _addLanguage( + shortCode: 'zh', + chineseName: '中文(简体)', + englishName: 'Chinese (simplified)', + asrCode: 'zh-CN', + ttsVoiceName: 'zh-CN-XiaoxiaoNeural', + ); + + _addLanguage( + shortCode: 'zh-Hant', + chineseName: '中文(繁体)', + englishName: 'Chinese (traditional)', + asrCode: 'zh-TW', + ttsVoiceName: 'zh-TW-HsiaoChenNeural', + ); + + _addLanguage( + shortCode: 'zh-Hant-hk', + chineseName: '中文(香港繁体)', + englishName: 'Chinese (Hongkong traditional)', + asrCode: 'zh-HK', + ttsVoiceName: 'zh-HK-HiuMaanNeural', + ); + + // 英语相关 + _addLanguage( + shortCode: 'en', + chineseName: '英语', + englishName: 'English', + asrCode: 'en-US', + ttsVoiceName: 'en-US-AriaNeural', + ); + + // 日语相关 + _addLanguage( + shortCode: 'ja', + chineseName: '日语', + englishName: 'Japanese', + asrCode: 'ja-JP', + ttsVoiceName: 'ja-JP-NanamiNeural', + ); + + // 韩语相关 + _addLanguage( + shortCode: 'ko', + chineseName: '韩语', + englishName: 'Korean', + asrCode: 'ko-KR', + ttsVoiceName: 'ko-KR-SunHiNeural', + ); + + // 欧洲语言 + _addLanguage( + shortCode: 'fr', + chineseName: '法语', + englishName: 'French', + asrCode: 'fr-FR', + ttsVoiceName: 'fr-FR-DeniseNeural', + ); + + _addLanguage( + shortCode: 'es', + chineseName: '西班牙语', + englishName: 'Spanish', + asrCode: 'es-ES', + ttsVoiceName: 'es-ES-ElviraNeural', + ); + + _addLanguage( + shortCode: 'pt', + chineseName: '葡萄牙语', + englishName: 'Portuguese', + asrCode: 'pt-BR', + ttsVoiceName: 'pt-BR-FranciscaNeural', + ); + + _addLanguage( + shortCode: 'it', + chineseName: '意大利语', + englishName: 'Italian', + asrCode: 'it-IT', + ttsVoiceName: 'it-IT-ElsaNeural', + ); + + _addLanguage( + shortCode: 'de', + chineseName: '德语', + englishName: 'German', + asrCode: 'de-DE', + ttsVoiceName: 'de-DE-KatjaNeural', + ); + + _addLanguage( + shortCode: 'ru', + chineseName: '俄语', + englishName: 'Russian', + asrCode: 'ru-RU', + ttsVoiceName: 'ru-RU-SvetlanaNeural', + ); + + _addLanguage( + shortCode: 'pl', + chineseName: '波兰语', + englishName: 'Polish', + asrCode: 'pl-PL', + ttsVoiceName: 'pl-PL-AgnieszkaNeural', + ); + + _addLanguage( + shortCode: 'nl', + chineseName: '荷兰语', + englishName: 'Dutch', + asrCode: 'nl-NL', + ttsVoiceName: 'nl-NL-ColetteNeural', + ); + + _addLanguage( + shortCode: 'sv', + chineseName: '瑞典语', + englishName: 'Swedish', + asrCode: 'sv-SE', + ttsVoiceName: 'sv-SE-SofieNeural', + ); + + _addLanguage( + shortCode: 'cs', + chineseName: '捷克语', + englishName: 'Czech', + asrCode: 'cs-CZ', + ttsVoiceName: 'cs-CZ-VlastaNeural', + ); + + _addLanguage( + shortCode: 'el', + chineseName: '现代希腊语', + englishName: 'Modern Greek', + asrCode: 'el-GR', + ttsVoiceName: 'el-GR-AthinaNeural', + ); + + _addLanguage( + shortCode: 'ro', + chineseName: '罗马尼亚语', + englishName: 'Romanian', + asrCode: 'ro-RO', + ttsVoiceName: 'ro-RO-AlinaNeural', + ); + + _addLanguage( + shortCode: 'hu', + chineseName: '匈牙利语', + englishName: 'Hungarian', + asrCode: 'hu-HU', + ttsVoiceName: 'hu-HU-NoemiNeural', + ); + + _addLanguage( + shortCode: 'uk', + chineseName: '乌克兰语', + englishName: 'Ukrainian', + asrCode: 'uk-UA', + ttsVoiceName: 'uk-UA-PolinaNeural', + ); + + _addLanguage( + shortCode: 'da', + chineseName: '丹麦语', + englishName: 'Danish', + asrCode: 'da-DK', + ttsVoiceName: 'da-DK-ChristelNeural', + ); + + _addLanguage( + shortCode: 'fi', + chineseName: '芬兰语', + englishName: 'Finnish', + asrCode: 'fi-FI', + ttsVoiceName: 'fi-FI-NooraNeural', + ); + + _addLanguage( + shortCode: 'no', + chineseName: '挪威语', + englishName: 'Norwegian', + asrCode: 'nb-NO', + ttsVoiceName: 'nb-NO-IselinNeural', + ); + + _addLanguage( + shortCode: 'hr', + chineseName: '克罗地亚语', + englishName: 'Croatian', + asrCode: 'hr-HR', + ttsVoiceName: 'hr-HR-GabrijelaNeural', + ); + + _addLanguage( + shortCode: 'ca', + chineseName: '加泰隆语', + englishName: 'Catalan', + asrCode: 'ca-ES', + ttsVoiceName: 'ca-ES-JoanaNeural', + ); + + // 亚洲语言 + _addLanguage( + shortCode: 'ar', + chineseName: '阿拉伯语', + englishName: 'Arabic', + asrCode: 'ar-EG', + ttsVoiceName: 'ar-EG-SalmaNeural', + ); + + _addLanguage( + shortCode: 'th', + chineseName: '泰语', + englishName: 'Thai', + asrCode: 'th-TH', + ttsVoiceName: 'th-TH-PremwadeeNeural', + ); + + _addLanguage( + shortCode: 'vi', + chineseName: '越南语', + englishName: 'Vietnamese', + asrCode: 'vi-VN', + ttsVoiceName: 'vi-VN-HoaiMyNeural', + ); + + _addLanguage( + shortCode: 'id', + chineseName: '印尼语', + englishName: 'Indonesian', + asrCode: 'id-ID', + ttsVoiceName: 'id-ID-GadisNeural', + ); + + _addLanguage( + shortCode: 'hi', + chineseName: '印地语', + englishName: 'Hindi', + asrCode: 'hi-IN', + ttsVoiceName: 'hi-IN-SwaraNeural', + ); + + _addLanguage( + shortCode: 'tr', + chineseName: '土耳其语', + englishName: 'Turkish', + asrCode: 'tr-TR', + ttsVoiceName: 'tr-TR-EmelNeural', + ); + + _addLanguage( + shortCode: 'ms', + chineseName: '马来语', + englishName: 'Malay', + asrCode: 'ms-MY', + ttsVoiceName: 'ms-MY-YasminNeural', + ); + + _addLanguage( + shortCode: 'bn', + chineseName: '孟加拉语', + englishName: 'Bengali', + asrCode: 'bn-IN', + ttsVoiceName: 'bn-IN-TanishaaNeural', + ); + + _addLanguage( + shortCode: 'ta', + chineseName: '泰米尔语', + englishName: 'Tamil', + asrCode: 'ta-IN', + ttsVoiceName: 'ta-IN-PallaviNeural', + ); + + _addLanguage( + shortCode: 'te', + chineseName: '泰卢固语', + englishName: 'Telugu', + asrCode: 'te-IN', + ttsVoiceName: 'te-IN-ShrutiNeural', + ); + + _addLanguage( + shortCode: 'mr', + chineseName: '马拉提语', + englishName: 'Marathi', + asrCode: 'mr-IN', + ttsVoiceName: 'mr-IN-AarohiNeural', + ); + + _addLanguage( + shortCode: 'ur', + chineseName: '乌尔都语', + englishName: 'Urdu', + asrCode: 'ur-IN', + ttsVoiceName: 'ur-IN-GulNeural', + ); + + _addLanguage( + shortCode: 'fa', + chineseName: '波斯语', + englishName: 'Persian', + asrCode: 'fa-IR', + ttsVoiceName: 'fa-IR-DilaraNeural', + ); + + // 非洲语言 + _addLanguage( + shortCode: 'sw', + chineseName: '斯瓦希里语', + englishName: 'Swahili', + asrCode: 'sw-KE', + ttsVoiceName: 'sw-KE-ZuriNeural', + ); + + Logger.info('语言管理器初始化完成,共加载 ${_languageMap.length} 种语言'); + } + + /// 添加语言信息到映射表 + void _addLanguage({ + required String shortCode, + required String chineseName, + required String englishName, + required String asrCode, + required String ttsVoiceName, + String? note, + }) { + final languageInfo = LanguageInfo( + shortCode: shortCode, + chineseName: chineseName, + englishName: englishName, + asrCode: asrCode, + ttsVoiceName: ttsVoiceName, + note: note, + ); + + _languageMap[asrCode] = languageInfo; + _chineseNameToAsrCodeMap[chineseName] = asrCode; + _englishNameToAsrCodeMap[englishName] = asrCode; + _shortCodeToAsrCodeMap[shortCode] = asrCode; + } + + /// 获取所有支持的语言信息 + Map getAllLanguages() { + return Map.from(_languageMap); + } + + /// 获取所有支持的语言的中文名称 + List getAllChineseNames() { + return _chineseNameToAsrCodeMap.keys.toList(); + } + + /// 获取所有支持的语言的英文名称 + List getAllEnglishNames() { + return _englishNameToAsrCodeMap.keys.toList(); + } + + /// 根据 ASR 代码获取语言信息 + LanguageInfo? getLanguageByAsrCode(String asrCode) { + return _languageMap[asrCode]; + } + + /// 根据中文名称获取语言信息 + LanguageInfo? getLanguageByChineseName(String chineseName) { + final asrCode = _chineseNameToAsrCodeMap[chineseName]; + if (asrCode == null) return null; + return _languageMap[asrCode]; + } + + /// 根据英文名称获取语言信息 + LanguageInfo? getLanguageByEnglishName(String englishName) { + final asrCode = _englishNameToAsrCodeMap[englishName]; + if (asrCode == null) return null; + return _languageMap[asrCode]; + } + + /// 根据短代码获取语言信息 + LanguageInfo? getLanguageByShortCode(String shortCode) { + final asrCode = _shortCodeToAsrCodeMap[shortCode]; + if (asrCode == null) return null; + return _languageMap[asrCode]; + } + + /// 根据中文名称获取 ASR 代码 + String? getAsrCodeByChineseName(String chineseName) { + return _chineseNameToAsrCodeMap[chineseName]; + } + + /// 根据英文名称获取 ASR 代码 + String? getAsrCodeByEnglishName(String englishName) { + return _englishNameToAsrCodeMap[englishName]; + } + + /// 根据短代码获取 ASR 代码 + String? getAsrCodeByShortCode(String shortCode) { + return _shortCodeToAsrCodeMap[shortCode]; + } + + /// 根据 ASR 代码获取中文名称 + String? getChineseNameByAsrCode(String asrCode) { + final languageInfo = _languageMap[asrCode]; + return languageInfo?.chineseName; + } + + /// 根据 ASR 代码获取英文名称 + String? getEnglishNameByAsrCode(String asrCode) { + final languageInfo = _languageMap[asrCode]; + return languageInfo?.englishName; + } + + /// 根据 ASR 代码获取短代码 + String? getShortCodeByAsrCode(String asrCode) { + final languageInfo = _languageMap[asrCode]; + return languageInfo?.shortCode; + } + + /// 根据 ASR 代码获取 TTS 语音名称 + String? getTtsVoiceNameByAsrCode(String asrCode) { + final languageInfo = _languageMap[asrCode]; + return languageInfo?.ttsVoiceName; + } + + /// 根据中文名称获取 TTS 语音名称 + String? getTtsVoiceNameByChineseName(String chineseName) { + final asrCode = _chineseNameToAsrCodeMap[chineseName]; + if (asrCode == null) return null; + return _languageMap[asrCode]?.ttsVoiceName; + } + + /// 根据短代码获取中文名称 + String? getChineseNameByShortCode(String shortCode) { + final asrCode = _shortCodeToAsrCodeMap[shortCode]; + if (asrCode == null) return null; + return _languageMap[asrCode]?.chineseName; + } + + /// 根据短代码获取英文名称 + String? getEnglishNameByShortCode(String shortCode) { + final asrCode = _shortCodeToAsrCodeMap[shortCode]; + if (asrCode == null) return null; + return _languageMap[asrCode]?.englishName; + } + + /// 根据短代码获取 TTS 语音名称 + String? getTtsVoiceNameByShortCode(String shortCode) { + final asrCode = _shortCodeToAsrCodeMap[shortCode]; + if (asrCode == null) return null; + return _languageMap[asrCode]?.ttsVoiceName; + } + + /// 获取中文名称到 ASR 代码的映射 + Map getChineseNameToAsrCodeMap() { + return Map.from(_chineseNameToAsrCodeMap); + } + + /// 获取英文名称到 ASR 代码的映射 + Map getEnglishNameToAsrCodeMap() { + return Map.from(_englishNameToAsrCodeMap); + } + + /// 获取短代码到 ASR 代码的映射 + Map getShortCodeToAsrCodeMap() { + return Map.from(_shortCodeToAsrCodeMap); + } +} \ No newline at end of file diff --git a/lib/data/services/volcano_translation_service.dart b/lib/data/services/volcano_translation_service.dart index c55d99edb..801619d38 100644 --- a/lib/data/services/volcano_translation_service.dart +++ b/lib/data/services/volcano_translation_service.dart @@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get/get.dart'; import '../../core/utils/logger.dart'; +import 'language_manager.dart'; class VolcanoTranslationService extends GetxService { // API 配置 @@ -18,38 +19,11 @@ class VolcanoTranslationService extends GetxService { // 初始化标志 bool _isInitialized = false; - // 语言代码映射 - final Map _languageCodeMap = { - "中文 (Chinese)": "zh", - "英语 (English)": "en", - "日语 (Japanese)": "ja", - "韩语 (Korean)": "ko", - "法语 (French)": "fr", - "西班牙语 (Spanish)": "es", - "葡萄牙语 (Portuguese)": "pt", - "意大利语 (Italian)": "it", - "德语 (German)": "de", - "俄语 (Russian)": "ru", - "阿拉伯语 (Arabic)": "ar", - "泰语 (Thai)": "th", - "越南语 (Vietnamese)": "vi", - "印尼语 (Indonesian)": "id", - "印地语 (Hindi)": "hi", - "土耳其语 (Turkish)": "tr", - "波兰语 (Polish)": "pl", - "荷兰语 (Dutch)": "nl", - "瑞典语 (Swedish)": "sv", - "捷克语 (Czech)": "cs", - "希腊语 (Greek)": "el", - "罗马尼亚语 (Romanian)": "ro", - "匈牙利语 (Hungarian)": "hu", - "乌克兰语 (Ukrainian)": "uk", - "保加利亚语 (Bulgarian)": "bg", - "丹麦语 (Danish)": "da", - "芬兰语 (Finnish)": "fi", - "挪威语 (Norwegian)": "no", - "马来语 (Malay)": "ms", - }; + // 语言管理器 + final LanguageManager _languageManager = Get.find(); + + // 构造函数 + VolcanoTranslationService(); // 初始化服务 Future initialize({ @@ -75,11 +49,6 @@ class VolcanoTranslationService extends GetxService { } } - // 获取语言代码 - String _getLanguageCode(String language) { - return _languageCodeMap[language] ?? 'zh'; - } - // 生成签名 Map _generateSignature(String method, Map requestBody, Map queryParams) { // 1. 准备时间相关参数 @@ -151,16 +120,30 @@ class VolcanoTranslationService extends GetxService { // 翻译文本 Future translateText({ required String text, - required String sourceLanguage, - required String targetLanguage, + required String sourceLanguageCode, + required String targetLanguageCode, }) async { if (!_isInitialized) { return null; } try { - final sourceCode = _getLanguageCode(sourceLanguage); - final targetCode = _getLanguageCode(targetLanguage); + // 获取源语言和目标语言的短代码 + String sourceCode = ''; + String targetCode = 'zh'; + + // 从 ASR 代码获取短代码 + if (sourceLanguageCode.isNotEmpty) { + final sourceShortCode = _languageManager.getShortCodeByAsrCode(sourceLanguageCode); + if (sourceShortCode != null) { + sourceCode = sourceShortCode; + } + } + + final targetShortCode = _languageManager.getShortCodeByAsrCode(targetLanguageCode); + if (targetShortCode != null) { + targetCode = targetShortCode; + } // 构建请求体 final requestBody = { diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 138c00468..da5222328 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -86,9 +86,9 @@ class ChatController extends GetxController { timestamp: DateTime.now(), )); // 如果启用了TTS,播放欢迎消息 - if (isTtsEnabled.value) { - _ttsService.speak(agent.welcomeMessage); - } + // if (isTtsEnabled.value) { + // _ttsService.speak(agent.welcomeMessage); + // } } // 如果playVoiceOnEnter为true,自动激活语音识别 diff --git a/lib/modules/test/controllers/azure_asr_test_controller.dart b/lib/modules/test/controllers/azure_asr_test_controller.dart index 433e44ee7..bcb390d49 100644 --- a/lib/modules/test/controllers/azure_asr_test_controller.dart +++ b/lib/modules/test/controllers/azure_asr_test_controller.dart @@ -20,6 +20,7 @@ class AzureAsrTestController extends GetxController { final RxBool isListening = false.obs; final RxString recognizedText = ''.obs; final RxString partialText = ''.obs; + final RxString detectedLanguage = ''.obs; final Rx asrState = AzureAsrState.notInitialized.obs; // 语言选项 @@ -37,6 +38,10 @@ class AzureAsrTestController extends GetxController { 'ru-RU', // 俄语 ]; + // 自动语言检测 + final RxBool isAutoDetectEnabled = false.obs; + final RxList selectedSupportedLanguages = ['zh-CN', 'en-US'].obs; + // 订阅 StreamSubscription? _recognitionSubscription; @@ -49,7 +54,11 @@ class AzureAsrTestController extends GetxController { // 初始化ASR服务 Future _initializeAsr() async { try { - final success = await _azureAsrService.initialize(language: selectedLanguage.value); + final success = await _azureAsrService.initialize( + language: selectedLanguage.value, + autoDetectLanguage: isAutoDetectEnabled.value, + supportedLanguages: selectedSupportedLanguages, + ); asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; } catch (e) { Logger.error('初始化ASR服务失败: $e'); @@ -71,8 +80,9 @@ class AzureAsrTestController extends GetxController { partialText.value = ''; final result = await _azureAsrService.recognizeSpeech(); - if (result.isNotEmpty) { - recognizedText.value = result; + if (result.text.isNotEmpty) { + recognizedText.value = result.text; + detectedLanguage.value = result.detectedLanguage; } } catch (e) { Logger.error('开始简单语音识别出错: $e'); @@ -120,6 +130,7 @@ class AzureAsrTestController extends GetxController { case RecognitionEventType.finalResult: if (event.text.isNotEmpty) { recognizedText.value = event.text; + detectedLanguage.value = event.detectedLanguage; } partialText.value = ''; break; @@ -127,6 +138,9 @@ class AzureAsrTestController extends GetxController { case RecognitionEventType.intermediateResult: if (event.text.isNotEmpty) { partialText.value = event.text; + if (event.detectedLanguage.isNotEmpty) { + detectedLanguage.value = event.detectedLanguage; + } } break; @@ -209,12 +223,57 @@ class AzureAsrTestController extends GetxController { recognizedText.value = ''; partialText.value = ''; + detectedLanguage.value = ''; } catch (e) { Logger.error('更改语言出错: $e'); asrState.value = AzureAsrState.error; } } + // 切换自动语言检测 + Future toggleAutoDetectLanguage() async { + try { + isAutoDetectEnabled.value = !isAutoDetectEnabled.value; + + if (_azureAsrService.isContinuousRecognitionActive()) { + await stopContinuousRecognition(); + } + + final success = await _azureAsrService.enableAutoDetectLanguage( + isAutoDetectEnabled.value, + languages: selectedSupportedLanguages, + ); + + asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; + + recognizedText.value = ''; + partialText.value = ''; + detectedLanguage.value = ''; + } catch (e) { + Logger.error('切换自动语言检测出错: $e'); + asrState.value = AzureAsrState.error; + } + } + + // 更新支持的语言列表 + Future updateSupportedLanguages(List languages) async { + try { + if (languages.isEmpty) return; + + selectedSupportedLanguages.value = languages; + + if (_azureAsrService.isContinuousRecognitionActive()) { + await stopContinuousRecognition(); + } + + final success = await _azureAsrService.setSupportedLanguages(languages); + asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; + } catch (e) { + Logger.error('更新支持的语言列表出错: $e'); + asrState.value = AzureAsrState.error; + } + } + @override void onClose() { _recognitionSubscription?.cancel(); diff --git a/lib/modules/translation/bindings/translation_binding.dart b/lib/modules/translation/bindings/translation_binding.dart index 549ff6783..caa514c28 100644 --- a/lib/modules/translation/bindings/translation_binding.dart +++ b/lib/modules/translation/bindings/translation_binding.dart @@ -3,16 +3,15 @@ import '../controllers/translation_controller.dart'; import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/azure_tts_service.dart'; import '../../../data/services/volcano_translation_service.dart'; +import '../../../data/services/language_manager.dart'; class TranslationBinding implements Bindings { @override void dependencies() { - // 注册服务 + Get.put(LanguageManager(), permanent: true); Get.put(AzureAsrService(), permanent: true); Get.put(AzureTtsService(), permanent: true); Get.put(VolcanoTranslationService(), permanent: true); - - // 注册控制器 Get.put(TranslationController()); } } \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 898f12fe6..25dd534f3 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -6,228 +6,239 @@ import 'package:get_storage/get_storage.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/azure_tts_service.dart'; +import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; class TranslationController extends GetxController { - // Services + // 服务 final AzureAsrService _asrService = Get.find(); - final VolcanoTranslationService _translationService = - Get.find(); + final VolcanoTranslationService _translationService = Get.find(); final AzureTtsService _ttsService = Get.find(); + final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); - // Storage key + // 存储相关 static const String _historyKey = 'translation_history'; - static const int _maxHistoryItems = 100; // 最多保存100条历史记录 + static const int _maxHistoryItems = 100; - // State variables + // 状态变量 final isRecognizing = false.obs; final isTranslating = false.obs; final isTtsEnabled = true.obs; - final sourceLanguage = '中文 (Chinese)'.obs; - final targetLanguage = '英语 (English)'.obs; + + // 语言相关 + final sourceLanguage = '中文(简体)'.obs; + final targetLanguage = '英语'.obs; + final sourceLanguageCode = 'zh-CN'.obs; + final targetLanguageCode = 'en-US'.obs; + var detectedLanguageCode = ''; + + // 翻译历史和当前项 final translationHistory = [].obs; - - // 支持的语言列表 - 改为Map结构,键为显示名称,值为语言代码 - final Map supportedLanguages = { - '中文 (Chinese)': 'zh', - '英语 (English)': 'en', - '日语 (Japanese)': 'ja', - '韩语 (Korean)': 'ko', - '法语 (French)': 'fr', - '德语 (German)': 'de', - '西班牙语 (Spanish)': 'es', - '意大利语 (Italian)': 'it', - '俄语 (Russian)': 'ru', - '泰语 (Thai)': 'th', - '葡萄牙语 (Portuguese)': 'pt', - '阿拉伯语 (Arabic)': 'ar', - '越南语 (Vietnamese)': 'vi', - '印尼语 (Indonesian)': 'id', - '印地语 (Hindi)': 'hi', - '土耳其语 (Turkish)': 'tr', - '波兰语 (Polish)': 'pl', - '荷兰语 (Dutch)': 'nl', - '瑞典语 (Swedish)': 'sv', - '捷克语 (Czech)': 'cs', - '希腊语 (Greek)': 'el', - '罗马尼亚语 (Romanian)': 'ro', - '匈牙利语 (Hungarian)': 'hu', - '乌克兰语 (Ukrainian)': 'uk', - '保加利亚语 (Bulgarian)': 'bg', - '丹麦语 (Danish)': 'da', - '芬兰语 (Finnish)': 'fi', - '挪威语 (Norwegian)': 'no', - '马来语 (Malay)': 'ms', - }; - - // Current translation item final Rx currentItem = Rx(null); - - // Subscription for recognition stream - StreamSubscription? _recognitionSubscription; - - // Scroll controller for ListView + String? currentSessionId; + + // UI相关 final ScrollController scrollController = ScrollController(); - - // Timer for debouncing translation requests + + // 其他 + StreamSubscription? _recognitionSubscription; Timer? _translationDebounceTimer; + Timer? _scrollDebounceTimer; // 滚动防抖定时器 + bool _isNearBottom = true; // 标记是否在底部附近 - // 当前会话ID - String? currentSessionId; + // 获取支持的语言列表 + Map get supportedLanguages => _languageManager.getChineseNameToAsrCodeMap(); @override void onInit() { super.onInit(); + _initController(); + } - - // 加载历史记录 + // 初始化控制器 + void _initController() { + _initLanguageCodes(); loadTranslationHistory(); - - // Initialize the translation service - _initTranslationService(); - - // 初始化 Azure ASR 服务 - _initAzureAsrService(); - - // 监听部分识别结果和识别状态 + _initServices(); _setupListeners(); - - // 添加后处理,确保在历史记录加载后滚动到底部 + + // 初始化完成后滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { - // 延迟执行滚动操作,确保视图已完全渲染 + // 延迟 300ms 再次尝试滚动,确保列表已完全渲染 Future.delayed(const Duration(milliseconds: 300), () { - _scrollToBottom(); + scrollToBottom(animate: false); }); + }); } + + // 重新初始化控制器(公共方法) + void reinitialize() { + _setupListeners(); + + // 初始化完成后滚动到底部 + WidgetsBinding.instance.addPostFrameCallback((_) { + scrollToBottom(animate: false); + }); + } + + // 仅设置滚动监听器(不启动翻译) + void setupScrollListener() { + // 移除之前的监听器,避免重复添加 + scrollController.removeListener(_onScroll); + // 重新添加滚动监听器 + scrollController.addListener(_onScroll); + } + + // 初始化服务 + Future _initServices() async { + await _initTranslationService(); + await _initAzureAsrService(); + } - @override - void onClose() { - stopRecognition(); - _recognitionSubscription?.cancel(); - _translationDebounceTimer?.cancel(); - scrollController.dispose(); - super.onClose(); + // 初始化语言代码 + void _initLanguageCodes() { + final sourceAsrCode = _languageManager.getAsrCodeByChineseName(sourceLanguage.value); + if (sourceAsrCode != null) { + sourceLanguageCode.value = sourceAsrCode; + } + + final targetAsrCode = _languageManager.getAsrCodeByChineseName(targetLanguage.value); + if (targetAsrCode != null) { + targetLanguageCode.value = targetAsrCode; + } } - // Initialize the translation service + // 初始化翻译服务 Future _initTranslationService() async { try { await _translationService.initialize(); } catch (e) { - Get.snackbar( - 'Error', - 'Failed to initialize translation service: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - ); + Logger.error('初始化翻译服务失败: $e'); } } - // 初始化 Azure ASR 服务 + // 初始化语音识别服务 Future _initAzureAsrService() async { try { - // 设置识别语言 - final sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; - final recognitionLanguage = _getRecognitionLanguage(sourceCode); - - // 使用正确定义的变量顺序 + final List asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; await _ttsService.initialize(); - await _asrService.initialize(language: recognitionLanguage); - Logger.info('Azure ASR 服务初始化成功'); + await _asrService.initialize( + autoDetectLanguage: true, + supportedLanguages: asrSupportedLanguages, + ); } catch (e) { - Logger.error('初始化 Azure ASR 服务失败: $e'); + Logger.error('初始化语音识别服务失败: $e'); } } - // Toggle TTS feature - void toggleTts() { - isTtsEnabled.toggle(); + // 设置监听器 + void _setupListeners() { + // 监听历史记录变化,当有新项添加时滚动到底部 + ever(translationHistory, (_) { + if (_isNearBottom) { + scrollToBottom(); + } + }); + + // 监听当前项变化,当有新项添加时滚动到底部 + ever(currentItem, (item) { + if (item != null && _isNearBottom) { + scrollToBottom(); + } + }); + + // 监听滚动位置,判断是否在底部附近 + scrollController.addListener(_onScroll); + } + + // 滚动监听器 + void _onScroll() { + if (!scrollController.hasClients) return; + + // 判断是否在底部附近(距离底部20像素内) + final maxScroll = scrollController.position.maxScrollExtent; + final currentScroll = scrollController.position.pixels; + _isNearBottom = (maxScroll - currentScroll) < 20; } - // Start voice recognition + @override + void onClose() { + stopRecognition(); + _recognitionSubscription?.cancel(); + _translationDebounceTimer?.cancel(); + _scrollDebounceTimer?.cancel(); + scrollController.removeListener(_onScroll); + scrollController.dispose(); + super.onClose(); + } + + // 更新ASR支持的语言 + Future _updateAsrSupportedLanguages() async { + try { + final List asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; + await _asrService.setSupportedLanguages(asrSupportedLanguages); + } catch (e) { + Logger.error('更新ASR支持的语言失败: $e'); + } + } + + // 切换TTS功能 + void toggleTts() => isTtsEnabled.toggle(); + + // 开始语音识别 Future startRecognition() async { if (isRecognizing.value) return; try { - // 生成新的会话ID + await _updateAsrSupportedLanguages(); currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); - // Initialize a new translation item + // 初始化新的翻译项 currentItem.value = TranslationItem( sourceText: '', translatedText: '', - sourceLanguage: sourceLanguage.value, - targetLanguage: targetLanguage.value, + sourceLanguageCode: sourceLanguageCode.value, + targetLanguageCode: targetLanguageCode.value, timestamp: DateTime.now(), sessionId: currentSessionId!, - isFirstInSession: true, // 标记为会话中的第一条 + isFirstInSession: true, ); - - // 设置识别语言 - final sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; - final recognitionLanguage = _getRecognitionLanguage(sourceCode); - - // 确保使用正确的语言 - await _asrService.setRecognitionLanguage(recognitionLanguage); // 开始连续语音识别 final recognitionStream = await _asrService.startContinuousRecognition(); isRecognizing.value = true; - // Reset the current item - if (currentItem.value != null) { - currentItem.value!.sourceText = ''; - currentItem.value!.translatedText = ''; - } - // 监听识别结果 _recognitionSubscription?.cancel(); - _recognitionSubscription = recognitionStream.listen((event) { - if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) { - handleFinalResult(event.text); - } else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) { - handleIntermediateResult(event.text); - } else if (event.type == RecognitionEventType.error) { - isRecognizing.value = false; - Logger.error('语音识别错误: ${event.error}'); - } else if (event.type == RecognitionEventType.canceled) { - isRecognizing.value = false; - Logger.error('语音识别取消: ${event.error}'); - } - }); + _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); } catch (e) { isRecognizing.value = false; - Get.snackbar( - 'Error', - '启动语音识别失败: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM, - ); + Logger.error('启动语音识别失败: $e'); } } - // 将语言代码转换为识别语言代码 - String _getRecognitionLanguage(String languageCode) { - // 语言代码到识别语言代码的映射 - final Map languageToRecognitionCode = { - 'zh': 'zh-CN', - 'en': 'en-US', - 'ja': 'ja-JP', - 'ko': 'ko-KR', - 'fr': 'fr-FR', - 'de': 'de-DE', - 'es': 'es-ES', - 'it': 'it-IT', - 'ru': 'ru-RU', - 'pt': 'pt-BR', - 'ar': 'ar-EG', - 'th': 'th-TH', - }; - - return languageToRecognitionCode[languageCode] ?? 'zh-CN'; + // 处理识别事件 + void _handleRecognitionEvent(RecognitionEvent event) { + if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) { + detectedLanguageCode = event.detectedLanguage; + handleFinalResult(event.text); + } else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) { + if (event.detectedLanguage.isNotEmpty) { + detectedLanguageCode = event.detectedLanguage; + } + handleIntermediateResult(event.text); + } else if (event.type == RecognitionEventType.error) { + isRecognizing.value = false; + Logger.error('语音识别错误: ${event.error}'); + } else if (event.type == RecognitionEventType.canceled) { + isRecognizing.value = false; + Logger.error('语音识别取消: ${event.error}'); + } } - // Stop voice recognition + // 停止语音识别 Future stopRecognition() async { if (!isRecognizing.value) return; @@ -236,42 +247,48 @@ class TranslationController extends GetxController { _recognitionSubscription?.cancel(); _recognitionSubscription = null; isRecognizing.value = false; - - // 重置当前会话ID + + // 如果当前有中间结果,将其作为最终结果处理 + if (currentItem.value != null && currentItem.value!.sourceText.isNotEmpty) { + // 标记为最终结果 + currentItem.value!.isIntermediate = false; + + // 如果还没有翻译结果,尝试翻译 + if (currentItem.value!.translatedText.isEmpty) { + await translateText(currentItem.value!.sourceText); + } + + translationHistory.add(currentItem.value!); + saveTranslationHistory(); + } + currentSessionId = null; - - // 重置当前项 currentItem.value = null; } catch (e) { - Get.snackbar('Error', '停止语音识别失败: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM); + Logger.error('停止语音识别失败: $e'); } } - // Handle intermediate recognition result + // 处理中间识别结果 void handleIntermediateResult(String text) { if (text.isEmpty) return; - // 如果当前没有活跃的翻译项,创建一个新的 if (currentItem.value == null) { currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); - currentItem.value = TranslationItem( sourceText: text, translatedText: '', - sourceLanguage: sourceLanguage.value, - targetLanguage: targetLanguage.value, + sourceLanguageCode: sourceLanguageCode.value, + targetLanguageCode: targetLanguageCode.value, timestamp: DateTime.now(), sessionId: currentSessionId!, - isFirstInSession: translationHistory.isEmpty || - translationHistory.last.sessionId != currentSessionId, + isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId, + isIntermediate: true, // 保留标记,用于内部逻辑 ); - currentItem.refresh(); - } else { - // 更新当前翻译项 + // 更新当前项 currentItem.value!.sourceText = text; - // 通知 GetX 更新 + currentItem.value!.isIntermediate = true; // 保留标记,用于内部逻辑 currentItem.refresh(); } @@ -284,87 +301,42 @@ class TranslationController extends GetxController { // 翻译当前项 Future translateCurrentItem({bool isIntermediate = false}) async { - if (currentItem.value == null || currentItem.value!.sourceText.isEmpty) { - return; - } - - final sourceText = currentItem.value!.sourceText; - final translationResult = await translateText(sourceText); - - if (translationResult != null && isIntermediate) { - // 如果是中间结果,不播放TTS - // 滚动到底部显示最新内容 - _scrollToBottom(); - } + if (currentItem.value == null || currentItem.value!.sourceText.isEmpty) return; + await translateText(currentItem.value!.sourceText); } - // Handle final recognition result - void handleFinalResult(String text) async { - // 如果没有文本或当前项为空,直接返回 + // 处理最终识别结果 + Future handleFinalResult(String text) async { if (text.isEmpty || currentItem.value == null) return; - // 更新当前项的源文本 + // 更新当前项,但不立即添加到历史记录 currentItem.value!.sourceText = text; + currentItem.value!.isIntermediate = false; // 标记为最终结果 currentItem.refresh(); - - // 添加到历史记录 - translationHistory.add(currentItem.value!); - saveTranslationHistory(); - - // 重置当前项 - currentItem.value = null; - - // 对最终结果进行一次翻译,确保翻译结果是最新的 + + // 先翻译,保持UI稳定 final translationResult = await translateText(text); - if (translationResult != null) { - if (isTtsEnabled.value) { + + // 给UI一点时间更新 + await Future.delayed(const Duration(milliseconds: 100)); + + // 翻译完成后再添加到历史记录 + if (currentItem.value != null) { + // 确保添加到历史记录的是最终结果 + translationHistory.add(currentItem.value!); + saveTranslationHistory(); + + // 播放TTS + if (translationResult != null && isTtsEnabled.value) { await playTranslatedText(translationResult); } + + // 重置当前项,准备下一次输入 + currentItem.value = null; } - // 滚动到底部显示最新内容 - _scrollToBottom(); - } - - // 检测文本语言 - String detectLanguage(String text) { - if (text.isEmpty) return sourceLanguage.value; - - // 检测各种语言特征 - final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text); - final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text); - final bool containsJapanese = - RegExp(r'[\u3040-\u309F\u30A0-\u30FF]').hasMatch(text); - final bool containsKorean = - RegExp(r'[\uAC00-\uD7AF\u1100-\u11FF]').hasMatch(text); - final bool containsCyrillic = RegExp(r'[\u0400-\u04FF]').hasMatch(text); - final bool containsArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(text); - final bool containsThai = RegExp(r'[\u0E00-\u0E7F]').hasMatch(text); - - // 获取当前设置的源语言代码 - final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; - - // 根据文本特征判断语言 - if (containsChinese && sourceCode != 'zh') { - return '中文 (Chinese)'; - } else if (containsJapanese && sourceCode != 'ja') { - return '日语 (Japanese)'; - } else if (containsKorean && sourceCode != 'ko') { - return '韩语 (Korean)'; - } else if (containsCyrillic && sourceCode != 'ru') { - return '俄语 (Russian)'; - } else if (containsArabic && sourceCode != 'ar') { - return '阿拉伯语 (Arabic)'; - } else if (containsThai && sourceCode != 'th') { - return '泰语 (Thai)'; - } else if (containsLatin && sourceCode == 'zh') { - return '英语 (English)'; - } - - // 默认返回当前设置的源语言 - return sourceLanguage.value; } - // Translate the recognized text + // 翻译文本 Future translateText(String sourceText) async { if (sourceText.isEmpty) return null; @@ -372,53 +344,49 @@ class TranslationController extends GetxController { String? translationResult; try { - // 检测源文本语言 - final detectedLanguage = detectLanguage(sourceText); - final bool shouldSwapLanguages = detectedLanguage != sourceLanguage.value; + // 根据检测到的语言确定源语言和目标语言 + final bool shouldSwap = detectedLanguageCode == sourceLanguageCode.value; + final detectedSourceLanguageCode = shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; + final detectedTargetLanguageCode = shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value; // 调用翻译服务 - final result = await _translationService.translateText( + translationResult = await _translationService.translateText( text: sourceText, - sourceLanguage: - shouldSwapLanguages ? detectedLanguage : sourceLanguage.value, - targetLanguage: - shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value, + sourceLanguageCode: detectedSourceLanguageCode, + targetLanguageCode: detectedTargetLanguageCode, ); - if (result != null) { - translationResult = result; - - // 更新翻译结果 - if (currentItem.value != null && - currentItem.value!.sourceText.contains(sourceText)) { - // 直接更新并刷新 - currentItem.value!.translatedText = result; - currentItem.refresh(); - } else { - // 在历史记录中查找匹配的项 - final index = translationHistory.indexWhere( - (historyItem) => historyItem.sourceText == sourceText); - - if (index != -1) { - // 直接更新历史记录项 - translationHistory[index].translatedText = result; - // 通知 GetX 更新 - translationHistory.refresh(); - - saveTranslationHistory(); - } - // 滚动到底部显示最新内容 - _scrollToBottom(); - } + if (translationResult != null) { + _updateTranslationResult(sourceText, translationResult, detectedSourceLanguageCode, detectedTargetLanguageCode); } - return translationResult; } catch (e) { - Get.snackbar('Error', '翻译失败: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM); - return null; + Logger.error('翻译失败: $e'); } finally { isTranslating.value = false; } + + return translationResult; + } + + // 更新翻译结果 + void _updateTranslationResult(String sourceText, String result, String sourceCode, String targetCode) { + if (currentItem.value != null && currentItem.value!.sourceText.contains(sourceText)) { + // 更新当前项 + currentItem.value!.translatedText = result; + currentItem.value!.sourceLanguageCode = sourceCode; + currentItem.value!.targetLanguageCode = targetCode; + currentItem.refresh(); + } else { + // 更新历史记录 + final index = translationHistory.indexWhere((item) => item.sourceText == sourceText); + if (index != -1) { + translationHistory[index].translatedText = result; + translationHistory[index].sourceLanguageCode = sourceCode; + translationHistory[index].targetLanguageCode = targetCode; + translationHistory.refresh(); + saveTranslationHistory(); + } + } } // 播放翻译文本 @@ -426,112 +394,127 @@ class TranslationController extends GetxController { if (text.isEmpty) return; try { - // 获取目标语言代码 - // final targetCode = supportedLanguages[targetLanguage.value] ?? 'en'; + final bool shouldSwap = detectedLanguageCode == targetLanguageCode.value; + final detectedTargetLanguageCode = shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; + final voiceName = _languageManager.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural'; - // // 设置合适的语音 - // final voiceName = _getTtsVoiceName(targetCode); - // await _ttsService.setVoice(voiceName); - - // 播放文本 + await _ttsService.setVoice(voiceName); await _ttsService.speak(text); } catch (e) { - Get.snackbar('Error', '播放翻译失败: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM); + Logger.error('播放翻译失败: $e'); } } - - // 获取TTS语音名称 - String _getTtsVoiceName(String languageCode) { - // 语言代码到TTS语音名称的映射 - final Map languageToVoiceName = { - 'zh': 'zh-CN-XiaoxiaoNeural', - 'en': 'en-US-AriaNeural', - 'ja': 'ja-JP-NanamiNeural', - 'ko': 'ko-KR-SunHiNeural', - 'fr': 'fr-FR-DeniseNeural', - 'de': 'de-DE-KatjaNeural', - 'es': 'es-ES-ElviraNeural', - 'it': 'it-IT-ElsaNeural', - 'ru': 'ru-RU-SvetlanaNeural', - 'pt': 'pt-BR-FranciscaNeural', - 'ar': 'ar-EG-SalmaNeural', - 'th': 'th-TH-PremwadeeNeural', - }; - - return languageToVoiceName[languageCode] ?? 'en-US-AriaNeural'; - } - // 停止所有操作(语音识别和TTS) + // 停止所有操作 Future stopAll() async { try { - // 停止语音识别 if (isRecognizing.value) { await stopRecognition(); } - - // 停止TTS播放 + + // 取消所有定时器 + _translationDebounceTimer?.cancel(); + _scrollDebounceTimer?.cancel(); + await _ttsService.stop(); } catch (e) { - Get.snackbar('Error', '停止操作失败: ${e.toString()}', - snackPosition: SnackPosition.BOTTOM); + Logger.error('停止操作失败: $e'); } } // 播放特定翻译项 Future playTranslationItem(TranslationItem item) async { if (item.translatedText.isEmpty) return; - await playTranslatedText(item.translatedText); + + try { + final voiceName = _languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ?? 'en-US-AriaNeural'; + await _ttsService.setVoice(voiceName); + await _ttsService.speak(item.translatedText); + } catch (e) { + Logger.error('播放翻译项失败: $e'); + } } // 播放文本 - Future playText(String text) async { + Future playText(String text, {String? languageCode}) async { if (text.isEmpty) return; - await playTranslatedText(text); - } - - // 滚动到列表底部 - void _scrollToBottom() { - if (!scrollController.hasClients) { - Future.delayed(const Duration(milliseconds: 100), _scrollToBottom); - return; - } - + try { - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); + final voiceName = languageCode != null + ? _languageManager.getTtsVoiceNameByAsrCode(languageCode) ?? 'en-US-AriaNeural' + : _languageManager.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ?? 'en-US-AriaNeural'; + + await _ttsService.setVoice(voiceName); + await _ttsService.speak(text); } catch (e) { - // 如果滚动失败,尝试延迟后再次滚动 - Future.delayed(const Duration(milliseconds: 100), () { - if (scrollController.hasClients) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - } - }); + Logger.error('播放文本失败: $e'); } } + // 简洁高效的滚动到底部方法 + void scrollToBottom({bool animate = true}) { + if (!scrollController.hasClients) return; + + // 取消之前的定时器 + _scrollDebounceTimer?.cancel(); + + // 创建新的定时器,防抖处理 + _scrollDebounceTimer = Timer(const Duration(milliseconds: 100), () { + if (!scrollController.hasClients) return; + + + if (animate) { + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } else { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + } + }); + } + + // 底部控制栏高度 + double _bottomBarHeight = 0.0; + + // 设置底部控制栏高度 + void setBottomBarHeight(double height) { + _bottomBarHeight = height; + } + + // 获取底部控制栏高度 + double get bottomBarHeight => _bottomBarHeight; + // 切换源语言和目标语言 void swapLanguages() { - final temp = sourceLanguage.value; + // 先停止当前的翻译操作 + stopAll(); + + final tempName = sourceLanguage.value; sourceLanguage.value = targetLanguage.value; - targetLanguage.value = temp; - - // 如果正在识别,需要停止并重新开始 - if (isRecognizing.value) { - stopRecognition(); - startRecognition(); - } + targetLanguage.value = tempName; + + final tempCode = sourceLanguageCode.value; + sourceLanguageCode.value = targetLanguageCode.value; + targetLanguageCode.value = tempCode; } // 设置源语言 void setSourceLanguage(String language) { if (sourceLanguage.value != language) { + // 先停止当前的翻译操作 + stopAll(); + sourceLanguage.value = language; + + final asrCode = _languageManager.getAsrCodeByChineseName(language); + if (asrCode != null) { + sourceLanguageCode.value = asrCode; + } + + _updateAsrSupportedLanguages(); - // 如果正在识别,需要停止并重新开始 if (isRecognizing.value) { stopRecognition(); startRecognition(); @@ -542,70 +525,75 @@ class TranslationController extends GetxController { // 设置目标语言 void setTargetLanguage(String language) { if (targetLanguage.value != language) { + // 先停止当前的翻译操作 + stopAll(); + targetLanguage.value = language; + + final asrCode = _languageManager.getAsrCodeByChineseName(language); + if (asrCode != null) { + targetLanguageCode.value = asrCode; + } + + _updateAsrSupportedLanguages(); } } - // Load translation history from storage + // 加载翻译历史 void loadTranslationHistory() { final historyJson = _storage.read(_historyKey); if (historyJson != null) { try { final List historyList = jsonDecode(historyJson); - translationHistory.value = - historyList.map((item) => TranslationItem.fromJson(item)).toList(); + translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList(); } catch (e) { - // 如果解析失败,清除历史记录 _storage.remove(_historyKey); + Logger.error('加载翻译历史失败: $e'); } } } - // Save translation history to storage + // 保存翻译历史 void saveTranslationHistory() { - // 限制历史记录数量,保留最新的记录 if (translationHistory.length > _maxHistoryItems) { - translationHistory.removeRange( - 0, translationHistory.length - _maxHistoryItems); + translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems); } - final List> historyJson = - translationHistory.map((item) => item.toJson()).toList(); - _storage.write(_historyKey, jsonEncode(historyJson)); + try { + final List> historyJson = translationHistory.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, jsonEncode(historyJson)); + } catch (e) { + Logger.error('保存翻译历史失败: $e'); + } } - // Clear translation history + // 清除翻译历史 void clearTranslationHistory() { translationHistory.clear(); _storage.remove(_historyKey); } - - // 设置监听器 - void _setupListeners() { - // 在新版本中,我们通过recognitionStream直接监听事件 - // 这部分逻辑已经在startRecognition方法中实现 - Logger.info('语音识别事件监听已设置'); - } } -// Model class for translation items +// 翻译项模型 class TranslationItem { String sourceText; String translatedText; - final String sourceLanguage; - final String targetLanguage; + String sourceLanguageCode; + String targetLanguageCode; final DateTime timestamp; String sessionId; final bool isFirstInSession; + bool isIntermediate; // 是否为中间结果 TranslationItem({ required this.sourceText, required this.translatedText, - required this.sourceLanguage, - required this.targetLanguage, + required this.sourceLanguageCode, + required this.targetLanguageCode, required this.timestamp, required this.sessionId, this.isFirstInSession = false, + this.isIntermediate = false, }); factory TranslationItem.fromJson(Map json) { @@ -619,12 +607,12 @@ class TranslationItem { return TranslationItem( sourceText: json['sourceText'] ?? '', translatedText: json['translatedText'] ?? '', - sourceLanguage: json['sourceLanguage'] ?? '中文 (Chinese)', - targetLanguage: json['targetLanguage'] ?? '英语 (English)', + sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN', + targetLanguageCode: json['targetLanguageCode'] ?? 'en-US', timestamp: timestamp, - sessionId: - json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), + sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), isFirstInSession: json['isFirstInSession'] ?? false, + isIntermediate: false, // 从存储加载的都是最终结果 ); } @@ -632,11 +620,24 @@ class TranslationItem { return { 'sourceText': sourceText, 'translatedText': translatedText, - 'sourceLanguage': sourceLanguage, - 'targetLanguage': targetLanguage, + 'sourceLanguageCode': sourceLanguageCode, + 'targetLanguageCode': targetLanguageCode, 'timestamp': timestamp.toIso8601String(), 'sessionId': sessionId, 'isFirstInSession': isFirstInSession, + // 不保存isIntermediate,因为存储的都是最终结果 }; } + + // 获取源语言的中文名称 + String get sourceLanguageName { + final languageManager = Get.find(); + return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? '未知语言'; + } + + // 获取目标语言的中文名称 + String get targetLanguageName { + final languageManager = Get.find(); + return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? '未知语言'; + } } diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index 53efac1b0..f7e69f2ce 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -4,142 +4,125 @@ import '../controllers/translation_controller.dart'; import '../controllers/translation_history_controller.dart'; import 'package:get_storage/get_storage.dart'; import 'dart:async'; +import '../../../core/utils/logger.dart'; class TranslationDetailController extends GetxController { // 依赖注入 final VolcanoAIService _aiService = Get.find(); - final TranslationHistoryController historyController = Get.find(); final GetStorage _storage = GetStorage(); // 存储键前缀 static const String _summaryKeyPrefix = 'translation_summary_'; // 超时设置(秒) - final int _timeoutDuration = 30; + static const int _defaultTimeoutDuration = 30; + static const int _extendedTimeoutDuration = 60; // 状态变量 final selectedTabIndex = 0.obs; final isGeneratingSummary = false.obs; final summaryResult = ''.obs; + final isLoading = true.obs; // 当前会话信息 late final TranslationItem currentItem; late final List sessionItems; + late final TranslationHistoryController historyController; - // 初始化方法 - void initialize(TranslationItem item, List items) { - currentItem = item; - sessionItems = items; + @override + void onInit() { + super.onInit(); - // 尝试从存储中加载摘要 - loadSavedSummary(); + // 从路由参数中获取数据 + if (Get.arguments != null) { + currentItem = Get.arguments['item']; + sessionItems = Get.arguments['sessionItems']; + historyController = Get.arguments['historyController']; + + // 异步加载数据,避免阻塞主线程 + _asyncInitialize(); + } else { + Logger.error('TranslationDetailController: 未找到路由参数'); + isLoading.value = false; + } + } + + // 异步初始化方法 + Future _asyncInitialize() async { + isLoading.value = true; + + try { + // 使用 Future.microtask 将操作放到后台线程 + await Future.microtask(() { + // 尝试从存储中加载摘要 + loadSavedSummary(); + }); + } catch (e) { + Logger.error('初始化详情控制器失败: $e'); + } finally { + isLoading.value = false; + } } // 加载保存的摘要 void loadSavedSummary() { - final String? savedSummary = _storage.read(_getSummaryKey()); - if (savedSummary != null && savedSummary.isNotEmpty) { - summaryResult.value = savedSummary; + try { + final String? savedSummary = _storage.read(_getSummaryKey()); + if (savedSummary != null && savedSummary.isNotEmpty) { + summaryResult.value = savedSummary; + } + } catch (e) { + Logger.error('加载摘要失败: $e'); } } // 保存摘要 void saveSummary(String summary) { - if (summary.isNotEmpty) { + if (summary.isEmpty) return; + + try { _storage.write(_getSummaryKey(), summary); + } catch (e) { + Logger.error('保存摘要失败: $e'); } } // 获取摘要的存储键 - String _getSummaryKey() { - return _summaryKeyPrefix + currentItem.sessionId; - } + String _getSummaryKey() => _summaryKeyPrefix + currentItem.sessionId; // 切换选项卡 - void switchTab(int index) { - selectedTabIndex.value = index; - } + void switchTab(int index) => selectedTabIndex.value = index; // 播放翻译项 - void playTranslationItem(TranslationItem item) { - historyController.playTranslationItem(item); - } + void playTranslationItem(TranslationItem item) => historyController.playTranslationItem(item); // 播放文本 - void playText(String text) { - historyController.playText(text); - } + void playText(String text) => historyController.playText(text); + + // 获取合并的源文本 - String getCombinedSourceText() { - return sessionItems.map((e) => e.sourceText).join('\n\n'); - } + String getCombinedSourceText() => sessionItems.map((e) => e.sourceText).join('\n\n'); // 获取合并的翻译文本 - String getCombinedTranslatedText() { - return sessionItems.map((e) => e.translatedText).join('\n\n'); - } + String getCombinedTranslatedText() => sessionItems.map((e) => e.translatedText).join('\n\n'); // 使用延长的超时时间重试提取摘要 Future retryExtractSummaryWithExtendedTimeout(String text) async { - // 延长超时时间(60秒) - final int extendedTimeout = 60; - - isGeneratingSummary.value = true; - - try { - // 构建提示词 - final messages = [ - {'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'}, - ]; - - // 系统提示词 - const systemPrompt = ''' -你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。 -请按照以下格式生成摘要: - -1. 首先用1-2句话对整体内容进行简要概括 -2. 然后使用项目符号列表(•)列出关键点或重要信息 -3. 如果是对话内容,请特别标注出讨论的结论或达成的共识 - -请遵循以下原则: -1. 保留原文的核心信息和关键点 -2. 使用简洁、清晰的语言 -3. 保持客观,不添加个人观点 -4. 摘要总长度应该是原文的1/3或更短 -5. 确保列表中的每个要点都简明扼要,突出重点 -'''; - - String result; - try { - // 使用延长的超时限制 - result = await _aiService.sendMessage( - messages: messages, - systemPrompt: systemPrompt, - ).timeout(Duration(seconds: extendedTimeout), onTimeout: () { - throw TimeoutException('请求超时,请稍后重试'); - }); - } on TimeoutException catch (_) { - summaryResult.value = '生成摘要超时,请检查网络连接后重试'; - return summaryResult.value; - } - - summaryResult.value = result; - - // 保存摘要 - saveSummary(result); - - return result; - } catch (e) { - summaryResult.value = '生成摘要失败: ${e.toString()}'; - return summaryResult.value; - } finally { - isGeneratingSummary.value = false; - } + return _extractSummaryInternal(text, _extendedTimeoutDuration); } // 提取摘要 Future extractSummary(String text) async { + return _extractSummaryInternal(text, _defaultTimeoutDuration); + } + + // 内部摘要提取方法,避免代码重复 + Future _extractSummaryInternal(String text, int timeoutDuration) async { + if (text.isEmpty) { + return '无法生成摘要:文本为空'; + } + isGeneratingSummary.value = true; try { @@ -165,32 +148,44 @@ class TranslationDetailController extends GetxController { 5. 确保列表中的每个要点都简明扼要,突出重点 '''; - // 添加超时处理 String result; try { - // 使用Future.timeout添加超时限制 + // 使用超时限制 result = await _aiService.sendMessage( messages: messages, systemPrompt: systemPrompt, - ).timeout(Duration(seconds: _timeoutDuration), onTimeout: () { + ).timeout(Duration(seconds: timeoutDuration), onTimeout: () { throw TimeoutException('请求超时,请稍后重试'); }); } on TimeoutException catch (_) { - summaryResult.value = '生成摘要超时,请稍后重试'; + final timeoutMessage = timeoutDuration == _defaultTimeoutDuration + ? '生成摘要超时,请稍后重试' + : '生成摘要超时,请检查网络连接后重试'; + summaryResult.value = timeoutMessage; return summaryResult.value; } - summaryResult.value = result; - - // 保存摘要 - saveSummary(result); + // 更新结果并保存 + if (result.isNotEmpty) { + summaryResult.value = result; + saveSummary(result); + } return result; } catch (e) { - summaryResult.value = '生成摘要失败: ${e.toString()}'; - return '生成摘要失败: ${e.toString()}'; + final errorMessage = '生成摘要失败: ${e.toString()}'; + Logger.error(errorMessage); + summaryResult.value = errorMessage; + return errorMessage; } finally { isGeneratingSummary.value = false; } } + + @override + void onClose() { + // 确保资源被正确释放 + historyController.translationController.stopAll(); + super.onClose(); + } } \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_history_controller.dart b/lib/modules/translation/controllers/translation_history_controller.dart index 379983afc..f5679c286 100644 --- a/lib/modules/translation/controllers/translation_history_controller.dart +++ b/lib/modules/translation/controllers/translation_history_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:intl/intl.dart'; import '../controllers/translation_controller.dart'; +import 'dart:async'; // 添加异步支持 class TranslationHistoryController extends GetxController { // 引用主翻译控制器,以访问历史记录数据 @@ -15,18 +16,29 @@ class TranslationHistoryController extends GetxController { // 会话映射表,用于存储每个会话的所有项目 final Rx>> sessionMap = Rx>>({}); + // 加载状态 + final isLoading = true.obs; + @override void onInit() { super.onInit(); - // 监听翻译历史记录的变化 - ever(translationController.translationHistory, (_) { + // 确保所有翻译操作都已停止 + translationController.stopAll(); + // 异步加载数据,避免阻塞主线程 + loadHistoryData(); + } + + // 异步加载历史数据 + Future loadHistoryData() async { + isLoading.value = true; + + // 使用 compute 或 Future.microtask 在后台处理数据 + await Future.microtask(() { _groupHistoryByDate(); _groupHistoryBySession(); }); - // 初始分组 - _groupHistoryByDate(); - _groupHistoryBySession(); + isLoading.value = false; } // 按日期对历史记录进行分组 @@ -44,17 +56,12 @@ class TranslationHistoryController extends GetxController { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); - final yesterday = today.subtract(const Duration(days: 1)); - final twoDaysAgo = today.subtract(const Duration(days: 2)); final itemDate = DateTime(date.year, date.month, date.day); if (itemDate == today) { key = 'TODAY'; - } else if (itemDate == yesterday) { - key = '1 DAY AGO'; - } else if (itemDate == twoDaysAgo) { - key = '2 DAYS AGO'; - } else { + } + else { key = DateFormat('yyyy-MM-dd').format(date); } diff --git a/lib/modules/translation/views/translation_detail_view.dart b/lib/modules/translation/views/translation_detail_view.dart index 0d7e2f67c..797e5cf87 100644 --- a/lib/modules/translation/views/translation_detail_view.dart +++ b/lib/modules/translation/views/translation_detail_view.dart @@ -9,125 +9,157 @@ import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; class TranslationDetailView extends GetView { - final TranslationItem item; - final List sessionItems; - final TranslationHistoryController historyController; - - const TranslationDetailView({ - Key? key, - required this.item, - required this.sessionItems, - required this.historyController, - }) : super(key: key); + TranslationDetailView({Key? key}) : super(key: key); + + // 缓存文本,避免重复计算 + final _sourceTextCache = Rx(null); + final _translatedTextCache = Rx(null); + + // 获取源文本(带缓存) + String _getCombinedSourceText() { + if (_sourceTextCache.value == null) { + _sourceTextCache.value = controller.getCombinedSourceText(); + } + return _sourceTextCache.value!; + } + + // 获取翻译文本(带缓存) + String _getCombinedTranslatedText() { + if (_translatedTextCache.value == null) { + _translatedTextCache.value = controller.getCombinedTranslatedText(); + } + return _translatedTextCache.value!; + } @override Widget build(BuildContext context) { - // 初始化控制器 - controller.initialize(item, sessionItems); - return Scaffold( backgroundColor: Color(0xFFF5F5F8), - appBar: AppBar( - backgroundColor: Color(0xFFF5F5F8), - elevation: 0, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), + appBar: _buildAppBar(), + body: Obx(() { + // 显示加载状态 + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + children: [ + _buildTabButtons(), + _buildContentArea(), + ], + ); + }), + ); + } + + // 构建应用栏 + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Color(0xFFF5F5F8), + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: Colors.black, + size: 20.sp, ), - title: Text( - 'Detail', - style: TextStyle( + onPressed: () => Get.back(), + ), + title: Text( + 'Detail', + style: TextStyle( + color: Colors.black, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: Icon( + Icons.share, color: Colors.black, - fontSize: 24.sp, - fontWeight: FontWeight.bold, + size: 22.sp, ), + onPressed: _shareContent, ), - centerTitle: true, - actions: [ - IconButton( - icon: Icon( - Icons.share, - color: Colors.black, - size: 22.sp, - ), - onPressed: () { - _shareContent(); - }, - ), - ], - ), - body: Column( + ], + ); + } + + // 构建选项卡按钮 + Widget _buildTabButtons() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + child: Row( children: [ - // 选项卡按钮 - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), - child: Row( - children: [ - Expanded( - child: Obx(() => _buildTabButton( - title: '原始记录', - isSelected: controller.selectedTabIndex.value == 0, - onTap: () => controller.switchTab(0), - )), - ), - SizedBox(width: 12.w), - Expanded( - child: Obx(() => _buildTabButton( - title: '记录摘要', - isSelected: controller.selectedTabIndex.value == 1, - onTap: () => controller.switchTab(1), - )), - ), - ], - ), + Expanded( + child: Obx(() => _buildTabButton( + title: '原始记录', + isSelected: controller.selectedTabIndex.value == 0, + onTap: () => controller.switchTab(0), + )), ), - - // 内容区域 + SizedBox(width: 12.w), Expanded( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 16.w), - child: Obx(() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 会话时间 - Padding( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), - child: Text( - DateFormat('yyyy-MM-dd HH:mm').format(item.timestamp), - style: TextStyle( - fontSize: 14.sp, - fontWeight: FontWeight.w600, - color: Colors.grey[600], - letterSpacing: 1.0, - ), - ), - ), - SizedBox(height: 8.h), - - // 根据选中的选项卡显示不同内容 - if (controller.selectedTabIndex.value == 0) - // 原始记录 - 显示所有翻译项 - ...sessionItems.map((sessionItem) => _buildSessionItemWidget(sessionItem)).toList() - else - // 记录总结 - 显示合并后的内容 - _buildSummaryWidget(), - - SizedBox(height: 24.h), - ], - ); - }), - ), + child: Obx(() => _buildTabButton( + title: '记录摘要', + isSelected: controller.selectedTabIndex.value == 1, + onTap: () => controller.switchTab(1), + )), ), ], ), ); } + // 构建内容区域 + Widget _buildContentArea() { + return Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Obx(() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTimestamp(), + SizedBox(height: 8.h), + + // 根据选中的选项卡显示不同内容 + if (controller.selectedTabIndex.value == 0) + ..._buildOriginalRecords() + else + _buildSummaryWidget(), + + SizedBox(height: 24.h), + ], + ); + }), + ), + ); + } + + // 构建时间戳 + Widget _buildTimestamp() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h), + child: Text( + DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.grey[600], + letterSpacing: 1.0, + ), + ), + ); + } + + // 构建原始记录列表 + List _buildOriginalRecords() { + return controller.sessionItems.map((sessionItem) => _buildSessionItemWidget(sessionItem)).toList(); + } + // 构建选项卡按钮 Widget _buildTabButton({ required String title, @@ -210,30 +242,7 @@ class TranslationDetailView extends GetView { ), ), SizedBox(width: 12.w), - // 播放按钮 - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - controller.playTranslationItem(item); - }, - borderRadius: BorderRadius.circular(20.r), - splashColor: Colors.blue.withOpacity(0.2), - child: Container( - width: 28.w, - height: 28.h, - decoration: BoxDecoration( - color: Colors.blue.withAlpha(10), - shape: BoxShape.circle, - ), - child: Icon( - Icons.volume_up, - color: Colors.blue, - size: 16.sp, - ), - ), - ), - ), + _buildPlayButton(() => controller.playTranslationItem(item)), ], ), ), @@ -242,278 +251,334 @@ class TranslationDetailView extends GetView { ); } + // 构建播放按钮 + Widget _buildPlayButton(VoidCallback onTap) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20.r), + splashColor: Colors.blue.withOpacity(0.2), + child: Container( + width: 28.w, + height: 28.h, + decoration: BoxDecoration( + color: Colors.blue.withAlpha(10), + shape: BoxShape.circle, + ), + child: Icon( + Icons.volume_up, + color: Colors.blue, + size: 16.sp, + ), + ), + ), + ); + } + // 构建总结视图 Widget _buildSummaryWidget() { - final String combinedSourceText = controller.getCombinedSourceText(); + final String combinedSourceText = _getCombinedSourceText(); return Column( children: [ // 摘要内容容器 - Container( - width: double.infinity, - margin: EdgeInsets.only(bottom: 16.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(8), - blurRadius: 8, - offset: Offset(0, 2), + _buildSummaryContainer(), + + // 提取摘要按钮 + Obx(() => _buildExtractSummaryButton(combinedSourceText)), + + // 重新生成按钮 + Obx(() => _buildRegenerateSummaryButton(combinedSourceText)), + ], + ); + } + + // 构建摘要容器 + Widget _buildSummaryContainer() { + return Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 16.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(8), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Obx(() { + if (controller.summaryResult.value.isEmpty) { + return _buildEmptySummaryPrompt(); + } else { + return _buildSummaryContent(); + } + }), + ); + } + + // 构建空摘要提示 + Widget _buildEmptySummaryPrompt() { + return Padding( + padding: EdgeInsets.all(24.r), + child: Center( + child: Text( + '点击下方按钮生成会话摘要', + style: TextStyle( + fontSize: 15.sp, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.center, + ), + ), + ); + } + + // 构建摘要内容 + Widget _buildSummaryContent() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(20.r), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '摘要内容', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + SizedBox(height: 12.h), + Text( + controller.summaryResult.value, + style: TextStyle( + fontSize: 15.sp, + color: Colors.black87, + height: 1.5, + ), ), ], ), - child: Obx(() { - if (controller.summaryResult.value.isEmpty) { - // 如果没有摘要,显示提示信息 - return Padding( - padding: EdgeInsets.all(24.r), - child: Center( - child: Text( - '点击下方按钮生成会话摘要', - style: TextStyle( - fontSize: 15.sp, - color: Colors.grey[600], - fontStyle: FontStyle.italic, + ), + Divider( + height: 1, + thickness: 0.5, + color: Colors.grey[200], + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // 播放按钮 + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.playText(controller.summaryResult.value), + borderRadius: BorderRadius.circular(20.r), + splashColor: Colors.blue.withOpacity(0.2), + child: Padding( + padding: EdgeInsets.all(8.r), + child: Icon( + Icons.volume_up, + color: Colors.blue, + size: 18.sp, ), - textAlign: TextAlign.center, ), ), - ); - } else { - // 如果有摘要,显示摘要内容 - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(20.r), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '摘要内容', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - SizedBox(height: 12.h), - Text( - controller.summaryResult.value, - style: TextStyle( - fontSize: 15.sp, - color: Colors.black87, - height: 1.5, - ), - ), - ], - ), - ), - Divider( - height: 1, - thickness: 0.5, - color: Colors.grey[200], - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - // 播放按钮 - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - controller.playText(controller.summaryResult.value); - }, - borderRadius: BorderRadius.circular(20.r), - splashColor: Colors.blue.withOpacity(0.2), - child: Padding( - padding: EdgeInsets.all(8.r), - child: Icon( - Icons.volume_up, - color: Colors.blue, - size: 18.sp, - ), - ), - ), - ), - SizedBox(width: 12.w), - // 复制按钮 - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - // 复制摘要到剪贴板 - Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); - Get.snackbar( - '已复制', - '摘要已复制到剪贴板', - snackPosition: SnackPosition.BOTTOM, - duration: Duration(seconds: 2), - ); - }, - borderRadius: BorderRadius.circular(20.r), - splashColor: Colors.blue.withOpacity(0.2), - child: Padding( - padding: EdgeInsets.all(8.r), - child: Icon( - Icons.copy, - color: Colors.blue, - size: 18.sp, - ), - ), - ), - ), - ], - ), - ), - ], - ); - } - }), - ), - - // 提取摘要按钮 - Obx(() => Container( - width: double.infinity, - margin: EdgeInsets.only(bottom: 16.h), - child: ElevatedButton( - onPressed: controller.isGeneratingSummary.value || controller.summaryResult.value.isNotEmpty - ? null - : () => _extractSummary(combinedSourceText), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), ), - elevation: 0, - disabledBackgroundColor: controller.isGeneratingSummary.value - ? Colors.blue.withAlpha(128) - : Colors.grey.withAlpha(128), - ), - child: controller.isGeneratingSummary.value - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 20.w, - height: 20.h, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2.0, - ), - ), - SizedBox(width: 12.w), - Text( - '生成中...', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - ), - ), - ], - ) - : Text( - controller.summaryResult.value.isEmpty ? '提取摘要' : '已生成摘要', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, + SizedBox(width: 12.w), + // 复制按钮 + Material( + color: Colors.transparent, + child: InkWell( + onTap: _copySummaryToClipboard, + borderRadius: BorderRadius.circular(20.r), + splashColor: Colors.blue.withOpacity(0.2), + child: Padding( + padding: EdgeInsets.all(8.r), + child: Icon( + Icons.copy, + color: Colors.blue, + size: 18.sp, ), ), - ), - )), - - // 重新生成按钮 - Obx(() { - if (controller.summaryResult.value.isEmpty || controller.isGeneratingSummary.value) { - return SizedBox.shrink(); - } - - return Container( - width: double.infinity, - margin: EdgeInsets.only(bottom: 16.h), - child: OutlinedButton( - onPressed: () => _extractSummary(combinedSourceText), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.blue, - side: BorderSide(color: Colors.blue), - padding: EdgeInsets.symmetric(vertical: 12.h), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), ), ), - child: Text( - '重新生成', + ], + ), + ), + ], + ); + } + + // 复制摘要到剪贴板 + void _copySummaryToClipboard() { + Clipboard.setData(ClipboardData(text: controller.summaryResult.value)); + Get.snackbar( + '已复制', + '摘要已复制到剪贴板', + snackPosition: SnackPosition.BOTTOM, + duration: Duration(seconds: 2), + ); + } + + // 构建提取摘要按钮 + Widget _buildExtractSummaryButton(String combinedSourceText) { + final bool isDisabled = controller.isGeneratingSummary.value || controller.summaryResult.value.isNotEmpty; + + return Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 16.h), + child: ElevatedButton( + onPressed: isDisabled ? null : () => _extractSummary(combinedSourceText), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + elevation: 0, + disabledBackgroundColor: controller.isGeneratingSummary.value + ? Colors.blue.withAlpha(128) + : Colors.grey.withAlpha(128), + ), + child: controller.isGeneratingSummary.value + ? _buildGeneratingIndicator() + : Text( + controller.summaryResult.value.isEmpty ? '提取摘要' : '已生成摘要', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, ), ), - ), - ); - }), + ), + ); + } + + // 构建生成中指示器 + Widget _buildGeneratingIndicator() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20.w, + height: 20.h, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.0, + ), + ), + SizedBox(width: 12.w), + Text( + '生成中...', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), ], ); } + // 构建重新生成按钮 + Widget _buildRegenerateSummaryButton(String combinedSourceText) { + if (controller.summaryResult.value.isEmpty || controller.isGeneratingSummary.value) { + return SizedBox.shrink(); + } + + return Container( + width: double.infinity, + margin: EdgeInsets.only(bottom: 16.h), + child: OutlinedButton( + onPressed: () => _extractSummary(combinedSourceText), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.blue, + side: BorderSide(color: Colors.blue), + padding: EdgeInsets.symmetric(vertical: 12.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + ), + child: Text( + '重新生成', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } + // 提取摘要方法 void _extractSummary(String text) async { final result = await controller.extractSummary(text); // 检查是否超时,如果超时则显示重试选项 if (result.contains('超时')) { - Get.dialog( - AlertDialog( - title: Text( - '请求超时', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - ), + _showTimeoutDialog(text); + } + } + + // 显示超时对话框 + void _showTimeoutDialog(String text) { + Get.dialog( + AlertDialog( + title: Text( + '请求超时', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, ), - content: Text( - '生成摘要请求超时,可能是网络问题或服务器繁忙。', - style: TextStyle( - fontSize: 16.sp, - ), + ), + content: Text( + '生成摘要请求超时,可能是网络问题或服务器繁忙。', + style: TextStyle( + fontSize: 16.sp, ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - '取消', - style: TextStyle( - fontSize: 16.sp, - color: Colors.grey[700], - ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey[700], ), ), - TextButton( - onPressed: () { - Get.back(); - // 使用延长的超时时间重试 - controller.retryExtractSummaryWithExtendedTimeout(text); - }, - child: Text( - '重试', - style: TextStyle( - fontSize: 16.sp, - color: Colors.blue, - fontWeight: FontWeight.bold, - ), + ), + TextButton( + onPressed: () { + Get.back(); + // 使用延长的超时时间重试 + controller.retryExtractSummaryWithExtendedTimeout(text); + }, + child: Text( + '重试', + style: TextStyle( + fontSize: 16.sp, + color: Colors.blue, + fontWeight: FontWeight.bold, ), ), - ], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.r), ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), ), - ); - } + ), + ); } // 分享内容方法 @@ -522,8 +587,8 @@ class TranslationDetailView extends GetView { if (controller.selectedTabIndex.value == 0) { // 分享原始记录 - final String sourceText = controller.getCombinedSourceText(); - final String translatedText = controller.getCombinedTranslatedText(); + final String sourceText = _getCombinedSourceText(); + final String translatedText = _getCombinedTranslatedText(); shareText = '原文:\n$sourceText\n\n译文:\n$translatedText'; } else { // 分享摘要 diff --git a/lib/modules/translation/views/translation_history_view.dart b/lib/modules/translation/views/translation_history_view.dart index 110d26b97..b3f10ad8f 100644 --- a/lib/modules/translation/views/translation_history_view.dart +++ b/lib/modules/translation/views/translation_history_view.dart @@ -59,6 +59,12 @@ class TranslationHistoryView extends GetView { ); } + if (controller.isLoading.value) { + return Center( + child: CircularProgressIndicator(), + ); + } + return ListView.builder( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), itemCount: controller.sortedDates.length, @@ -184,11 +190,12 @@ class TranslationHistoryView extends GetView { final List sessionItems = controller.getSessionItems(item.sessionId); Get.to( - () => TranslationDetailView( - item: item, - sessionItems: sessionItems, - historyController: controller, - ), + () => TranslationDetailView(), + arguments: { + 'item': item, + 'sessionItems': sessionItems, + 'historyController': controller, + }, binding: TranslationDetailBinding(), ); } diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 07c30aee3..eecce7440 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -122,6 +122,9 @@ class TranslationView extends GetView { () => const TranslationHistoryView(), binding: TranslationHistoryBinding(), ); + + // 返回时不自动开始翻译,只需重新设置滚动监听器 + controller.setupScrollListener(); }, tooltip: '查看历史记录', ), @@ -146,11 +149,19 @@ class TranslationView extends GetView { // 使用 currentItem 来判断是否显示当前项 final hasCurrentItem = controller.currentItem.value != null && - (controller.currentItem.value!.sourceText.isNotEmpty || controller.currentItem.value!.translatedText.isNotEmpty); + controller.currentItem.value!.sourceText.isNotEmpty; // 只有当有实际内容时才显示当前项 + + // 计算底部内边距,确保内容不会被底部框遮住 + final bottomPadding = controller.bottomBarHeight + 20.h; // 使用控制栏高度加上额外间距 return ListView.builder( controller: controller.scrollController, - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + padding: EdgeInsets.only( + left: 16.w, + right: 16.w, + top: 16.h, + bottom: bottomPadding + ), itemCount: itemCount + (hasCurrentItem ? 1 : 0), itemBuilder: (context, index) { // Current item (being translated) @@ -159,6 +170,7 @@ class TranslationView extends GetView { controller.currentItem.value!.sourceText, controller.currentItem.value!.translatedText, true, + isIntermediate: controller.currentItem.value!.isIntermediate, ); } @@ -169,31 +181,45 @@ class TranslationView extends GetView { historyItem.translatedText, false, item: historyItem, + isIntermediate: false, // 历史项总是显示为最终结果 ); }, ); }); } - Widget _buildTranslationItemWidget(String sourceText, String translatedText, bool isCurrent, {TranslationItem? item}) { - return Container( + Widget _buildTranslationItemWidget( + String sourceText, + String translatedText, + bool isCurrent, + { + TranslationItem? item, + bool isIntermediate = false, + } + ) { + // 使用AnimatedContainer实现平滑过渡,但恢复原来的视觉样式 + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, margin: EdgeInsets.only(bottom: 24.h), decoration: BoxDecoration( - color: Colors.grey[100], + color: Colors.grey[100], // 恢复原来的背景色 borderRadius: BorderRadius.circular(12.r), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Source text - Container( + AnimatedContainer( + duration: const Duration(milliseconds: 300), width: double.infinity, padding: EdgeInsets.all(16.r), child: Text( sourceText.isEmpty && isCurrent ? '等待语音输入...' : sourceText, style: TextStyle( fontSize: 16.sp, - color: sourceText.isEmpty && isCurrent ? Colors.grey : Colors.black, + color: sourceText.isEmpty && isCurrent ? Colors.grey : Colors.black, // 恢复原来的文字颜色 + fontWeight: FontWeight.normal, // 恢复原来的字重 ), ), ), @@ -202,7 +228,8 @@ class TranslationView extends GetView { Divider(height: 1, thickness: 1, color: Colors.grey[200]), // Translated text - Container( + AnimatedContainer( + duration: const Duration(milliseconds: 300), width: double.infinity, padding: EdgeInsets.all(16.r), child: Row( @@ -213,7 +240,8 @@ class TranslationView extends GetView { translatedText.isEmpty && isCurrent ? '翻译结果将显示在这里...' : translatedText, style: TextStyle( fontSize: 16.sp, - color: translatedText.isEmpty && isCurrent ? Colors.grey : Colors.black, + color: translatedText.isEmpty && isCurrent ? Colors.grey : Colors.black, // 恢复原来的文字颜色 + fontStyle: FontStyle.normal, // 恢复原来的字体样式 ), ), ), @@ -221,7 +249,7 @@ class TranslationView extends GetView { IconButton( icon: Icon( Icons.volume_up, - color: Colors.blue, + color: Colors.blue, // 恢复原来的图标颜色 size: 20.sp, ), onPressed: () { @@ -241,7 +269,20 @@ class TranslationView extends GetView { } Widget _buildControlBar() { + // 使用GlobalKey来获取控制栏的高度 + final controlBarKey = GlobalKey(); + + // 在构建完成后获取控制栏的高度并设置到控制器中 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (controlBarKey.currentContext != null) { + final RenderBox renderBox = controlBarKey.currentContext!.findRenderObject() as RenderBox; + final height = renderBox.size.height; + controller.setBottomBarHeight(height); + } + }); + return Container( + key: controlBarKey, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), decoration: BoxDecoration( color: Colors.white,