Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
e72abe8e5c
  1. 145
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  2. 159
      android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt
  3. 222
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  4. 14
      android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt
  5. BIN
      android/app/src/main/res/raw/listening.mp3
  6. 9
      lib/core/bindings/initial_binding.dart
  7. 136
      lib/data/services/azure_asr_service.dart
  8. 94
      lib/data/services/azure_tts_service.dart
  9. 532
      lib/data/services/language_manager.dart
  10. 65
      lib/data/services/volcano_translation_service.dart
  11. 6
      lib/modules/chat/controllers/chat_controller.dart
  12. 65
      lib/modules/test/controllers/azure_asr_test_controller.dart
  13. 5
      lib/modules/translation/bindings/translation_binding.dart
  14. 739
      lib/modules/translation/controllers/translation_controller.dart
  15. 183
      lib/modules/translation/controllers/translation_detail_controller.dart
  16. 31
      lib/modules/translation/controllers/translation_history_controller.dart
  17. 783
      lib/modules/translation/views/translation_detail_view.dart
  18. 17
      lib/modules/translation/views/translation_history_view.dart
  19. 61
      lib/modules/translation/views/translation_view.dart

145
android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt

@ -20,6 +20,8 @@ class AzureAsrHelper {
private var currentLanguage = "zh-CN" private var currentLanguage = "zh-CN"
private var subscriptionKey = "" private var subscriptionKey = ""
private var serviceRegion = "" private var serviceRegion = ""
private var isAutoDetectLanguage = false
private var supportedLanguages = arrayOf("zh-CN", "en-US")
// 回音消除相关 // 回音消除相关
private var echoCanceler: AcousticEchoCanceler? = null private var echoCanceler: AcousticEchoCanceler? = null
@ -33,9 +35,10 @@ class AzureAsrHelper {
private val executorService = Executors.newSingleThreadExecutor() private val executorService = Executors.newSingleThreadExecutor()
// 初始化 SDK // 初始化 SDK
fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { fun initialize(subscriptionKey: String, serviceRegion: String,
supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US")): Boolean {
try { try {
Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, language=$language") Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, supportedLanguages=${supportedLanguages.joinToString()}")
// 检查配置是否为空 // 检查配置是否为空
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) { if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) {
@ -45,14 +48,36 @@ class AzureAsrHelper {
this.subscriptionKey = subscriptionKey this.subscriptionKey = subscriptionKey
this.serviceRegion = serviceRegion 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() dispose()
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) 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 // 创建 PushAudioInputStream 和 AudioConfig
try { try {
@ -65,7 +90,14 @@ class AzureAsrHelper {
// 创建识别器 // 创建识别器
Log.d(TAG, "创建语音识别器") 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 语音服务初始化成功") Log.d(TAG, "Azure 语音服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -123,9 +155,11 @@ class AzureAsrHelper {
return return
} }
// 如果指定了新的语言,需要重新初始化 // 如果指定了新的语言,且不是自动检测模式,需要重新初始化
if (language != null && language != currentLanguage) { if (language != null && language != currentLanguage && !isAutoDetectLanguage) {
initialize(subscriptionKey, serviceRegion, language) // 使用指定的语言创建新的语言列表
val newLanguages = arrayOf(language)
initialize(subscriptionKey, serviceRegion, newLanguages)
} }
try { try {
@ -138,7 +172,10 @@ class AzureAsrHelper {
stopRecording() stopRecording()
if (result != null && result.reason == ResultReason.RecognizedSpeech) { 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 { } else {
callback.onError("未能识别语音") callback.onError("未能识别语音")
} }
@ -149,7 +186,7 @@ class AzureAsrHelper {
} }
// 开始连续语音识别 // 开始连续语音识别
fun startContinuousRecognition(language: String? = null, callback: ContinuousRecognizeCallback): Boolean { fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (recognizer == null) { if (recognizer == null) {
Log.e(TAG, "SpeechRecognizer 未初始化") Log.e(TAG, "SpeechRecognizer 未初始化")
callback.onError("SpeechRecognizer 未初始化") callback.onError("SpeechRecognizer 未初始化")
@ -161,12 +198,6 @@ class AzureAsrHelper {
Log.d(TAG, "已经在进行连续识别,先停止") Log.d(TAG, "已经在进行连续识别,先停止")
stopContinuousRecognition(callback) stopContinuousRecognition(callback)
} }
// 如果指定了新的语言,需要重新初始化
if (language != null && language != currentLanguage) {
Log.d(TAG, "语言变更,重新初始化: $language")
initialize(subscriptionKey, serviceRegion, language)
}
try { try {
Log.d(TAG, "开始重置识别器和音频资源") Log.d(TAG, "开始重置识别器和音频资源")
@ -203,7 +234,13 @@ class AzureAsrHelper {
// 重新创建识别器 // 重新创建识别器
try { try {
Log.d(TAG, "创建新的语音识别器") 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) { } catch (e: Exception) {
Log.e(TAG, "创建识别器失败: ${e.message}", e) Log.e(TAG, "创建识别器失败: ${e.message}", e)
callback.onError("创建识别器失败: ${e.message}") callback.onError("创建识别器失败: ${e.message}")
@ -216,8 +253,10 @@ class AzureAsrHelper {
recognizer.recognized.addEventListener( recognizer.recognized.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) { if (event.result.reason == ResultReason.RecognizedSpeech) {
Log.d(TAG, "识别结果: ${event.result.text}") val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result)
callback.onResult(event.result.text) val detectedLanguage = autoDetectSourceLanguageResult?.language ?: ""
Log.d(TAG, "识别结果: ${event.result.text}, 检测到的语言: $detectedLanguage")
callback.onResult(event.result.text, detectedLanguage)
} else { } else {
Log.d(TAG, "识别结果原因: ${event.result.reason}") Log.d(TAG, "识别结果原因: ${event.result.reason}")
} }
@ -228,8 +267,12 @@ class AzureAsrHelper {
recognizer.recognizing.addEventListener( recognizer.recognizing.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) { if (event.result.reason == ResultReason.RecognizingSpeech) {
// Log.d(TAG, "正在识别: ${event.result.text}") val autoDetectSourceLanguageResult = AutoDetectSourceLanguageResult.fromResult(event.result)
callback.onRecognizing(event.result.text) 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 { interface RecognizeCallback {
fun onResult(result: String) fun onResult(result: String, detectedLanguage: String = "")
fun onError(error: String) fun onError(error: String)
} }
// 连续识别回调接口 // 连续识别回调接口
interface ContinuousRecognizeCallback { interface ContinuousRecognizeCallback {
fun onResult(result: String) fun onResult(result: String, detectedLanguage: String = "")
fun onRecognizing(recognizing: String) fun onRecognizing(recognizing: String, detectedLanguage: String = "")
fun onSessionStarted() fun onSessionStarted()
fun onSessionStopped() fun onSessionStopped()
fun onCanceled(reason: String, errorDetails: String) fun onCanceled(reason: String, errorDetails: String)
@ -536,5 +579,57 @@ class AzureAsrHelper {
fun isEchoCancellationEnabled(): Boolean { fun isEchoCancellationEnabled(): Boolean {
return isEchoCancellationEnabled return isEchoCancellationEnabled
} }
// 设置支持的语言列表(用于自动语言检测)
fun setSupportedLanguages(languages: Array<String>) {
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<String>? = 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)
}
} }

159
android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt

@ -1,5 +1,10 @@
package com.example.deep_voice 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 android.util.Log
import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
@ -17,6 +22,18 @@ class AzureTtsHelper {
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null private var synthesizer: SpeechSynthesizer? = null
private var isInitialized = false 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) private val semaphore = Semaphore(0)
@ -33,9 +50,15 @@ class AzureTtsHelper {
* @param subscriptionKey Azure 语音服务订阅密钥 * @param subscriptionKey Azure 语音服务订阅密钥
* @param serviceRegion Azure 语音服务区域 * @param serviceRegion Azure 语音服务区域
* @param language 可选,默认语言,默认为 "zh-CN" * @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 { try {
this.context = context
if (context != null) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
}
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
@ -60,6 +83,12 @@ class AzureTtsHelper {
isInitialized = true isInitialized = true
Log.d(TAG, "TTS 引擎初始化成功") Log.d(TAG, "TTS 引擎初始化成功")
// 设置默认音频输出类型为自动
if (audioManager != null) {
setAudioOutputType(AudioOutputType.AUTO)
}
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "TTS 引擎初始化失败: ${e.message}") 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) { if (!isInitialized) {
Log.e(TAG, "TTS 引擎尚未初始化") Log.e(TAG, "TTS 引擎尚未初始化")
return false return false
} }
if (audioManager == null) {
Log.e(TAG, "AudioManager 未初始化,无法设置音频输出设备")
return false
}
try { try {
currentVoiceName = voiceName currentAudioOutputType = outputType
speechConfig?.setSpeechSynthesisVoiceName(voiceName)
// 重新创建合成器以应用新的语音设置 when (outputType) {
val audioConfig = AudioConfig.fromDefaultSpeakerOutput() 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() synthesizer?.close()
// 创建音频配置
val audioConfig = AudioConfig.fromDefaultSpeakerOutput()
// 创建新的合成器
synthesizer = SpeechSynthesizer(speechConfig, audioConfig) synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
// 重新设置事件监听 // 重新设置事件监听
synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() } synthesizer?.SynthesisCompleted?.addEventListener { _, _ -> semaphore.release() }
synthesizer?.SynthesisCanceled?.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") Log.d(TAG, "已设置语音: $voiceName")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -143,6 +272,11 @@ class AzureTtsHelper {
try { try {
Log.d(TAG, "开始合成文本: $text") Log.d(TAG, "开始合成文本: $text")
// 如果设置为自动模式,检查耳机状态并更新设置
if (currentAudioOutputType == AudioOutputType.AUTO) {
setAudioOutputType(AudioOutputType.AUTO)
}
// 生成 SSML // 生成 SSML
val ssml = generateSsml(text) val ssml = generateSsml(text)
@ -245,7 +379,7 @@ class AzureTtsHelper {
try { try {
synthesizer?.StopSpeakingAsync() synthesizer?.StopSpeakingAsync()
Log.d(TAG, "已停止语音合成kt") Log.d(TAG, "已停止语音合成")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "停止语音合成失败: ${e.message}") Log.e(TAG, "停止语音合成失败: ${e.message}")
@ -262,6 +396,13 @@ class AzureTtsHelper {
stopSpeaking() stopSpeaking()
synthesizer?.close() synthesizer?.close()
speechConfig?.close() speechConfig?.close()
// 恢复音频模式
if (audioManager != null) {
audioManager?.mode = AudioManager.MODE_NORMAL
audioManager?.isSpeakerphoneOn = false
}
isInitialized = false isInitialized = false
Log.d(TAG, "TTS 引擎已释放") Log.d(TAG, "TTS 引擎已释放")
} catch (e: Exception) { } catch (e: Exception) {

222
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt

@ -1,22 +1,16 @@
package com.example.deep_voice package com.example.deep_voice
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.util.Log import android.util.Log
import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugins.GeneratedPluginRegistrant import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import android.content.Context import android.content.Context
import android.content.ComponentName
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Handler import android.os.Handler
@ -24,7 +18,6 @@ import android.os.Looper
class MainActivity: FlutterActivity() { 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_CHANNEL = "com.deep_voice.azure_asr"
private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events" private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events"
private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts" private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts"
@ -69,12 +62,9 @@ class MainActivity: FlutterActivity() {
// 使用伴生对象存储配置信息,使其在整个应用中可访问 // 使用伴生对象存储配置信息,使其在整个应用中可访问
companion object { companion object {
// Azure 语音服务配置 // Azure 语音服务配置
var azureSpeechKey: String = "2WDq2KL45QUC5Gk0o8LgQ5bOpnV1kF0ZAEWBTl76AUoG90bbYO4uJQQJ99BCAC3pKaRXJ3w3AAAYACOGzvU1" var azureSpeechKey: String = ""
var azureSpeechRegion: String = "eastasia" var azureSpeechRegion: String = ""
var volcanoAiApiKey: String = "168deb3d-fd0c-4912-b9f1-aaee5c6743e6" var volcanoAiApiKey: String = ""
// 标记是否已初始化
var isAzureHelpersInitialized = false
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -85,21 +75,9 @@ class MainActivity: FlutterActivity() {
azureAsrHelper = AzureAsrHelper() azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper() 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() registerVoiceInteractionReceiver()
// 确保 Flutter 引擎准备好后立即设置事件通道 // 确保 Flutter 引擎准备好后立即设置事件通道
flutterEngine?.let { engine -> flutterEngine?.let { engine ->
Log.d(TAG, "Flutter 引擎已存在,立即设置事件通道") Log.d(TAG, "Flutter 引擎已存在,立即设置事件通道")
@ -122,7 +100,6 @@ class MainActivity: FlutterActivity() {
Log.d(TAG, "开始配置 Flutter 引擎") Log.d(TAG, "开始配置 Flutter 引擎")
// 先设置事件通道,确保在插件注册之前就已准备好 // 先设置事件通道,确保在插件注册之前就已准备好
Log.d(TAG, "在插件注册前设置事件通道")
setupEventChannels(flutterEngine) setupEventChannels(flutterEngine)
// 然后调用父类方法和注册插件 // 然后调用父类方法和注册插件
@ -143,65 +120,44 @@ class MainActivity: FlutterActivity() {
try { try {
// 语音交互事件通道 // 语音交互事件通道
Log.d(TAG, "设置语音交互事件通道")
EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_EVENT_CHANNEL).setStreamHandler( EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler { object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "语音交互事件通道开始监听,arguments: $arguments") Log.d(TAG, "语音交互事件通道开始监听")
voiceInteractionEventSink = events voiceInteractionEventSink = events
events?.success(mapOf("type" to "channelReady"))
// 通知 Flutter 端事件通道已准备好
try {
events?.success(mapOf("type" to "channelReady"))
Log.d(TAG, "已发送 channelReady 事件")
} catch (e: Exception) {
Log.e(TAG, "发送 channelReady 事件失败: ${e.message}", e)
}
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
Log.d(TAG, "语音交互事件通道取消监听,arguments: $arguments")
voiceInteractionEventSink = null voiceInteractionEventSink = null
} }
} }
) )
Log.d(TAG, "语音交互事件通道设置完成")
// Azure ASR 事件通道 // Azure ASR 事件通道
Log.d(TAG, "设置 Azure ASR 事件通道")
EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler( EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler { object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "ASR事件通道开始监听,arguments: $arguments") Log.d(TAG, "ASR事件通道开始监听")
azureAsrEventSink = events azureAsrEventSink = events
// 通知 Flutter 端事件通道已准备好
events?.success(mapOf("type" to "channelReady")) events?.success(mapOf("type" to "channelReady"))
Log.d(TAG, "已发送 ASR channelReady 事件")
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
Log.d(TAG, "ASR事件通道取消监听,arguments: $arguments")
azureAsrEventSink = null azureAsrEventSink = null
} }
} }
) )
Log.d(TAG, "Azure ASR 事件通道设置完成")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "设置事件通道时发生错误: ${e.message}", e) Log.e(TAG, "设置事件通道时发生错误: ${e.message}", e)
} }
Log.d(TAG, "所有事件通道设置完成")
} }
/** /**
* 设置方法通道 * 设置方法通道
*/ */
private fun setupMethodChannels(flutterEngine: FlutterEngine) { private fun setupMethodChannels(flutterEngine: FlutterEngine) {
// Azure ASR 方法通道 // Azure ASR 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
@ -209,6 +165,8 @@ class MainActivity: FlutterActivity() {
val subscriptionKey = call.argument<String>("subscriptionKey") val subscriptionKey = call.argument<String>("subscriptionKey")
val region = call.argument<String>("region") val region = call.argument<String>("region")
val language = call.argument<String>("language") ?: "zh-CN" val language = call.argument<String>("language") ?: "zh-CN"
val autoDetectLanguage = call.argument<Boolean>("autoDetectLanguage") ?: false
val supportedLanguages = call.argument<List<String>>("supportedLanguages")?.toTypedArray() ?: arrayOf("zh-CN", "en-US")
if (subscriptionKey == null || region == null) { if (subscriptionKey == null || region == null) {
result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null) result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null)
@ -216,7 +174,7 @@ class MainActivity: FlutterActivity() {
} }
try { try {
val success = azureAsrHelper.initialize(subscriptionKey, region, language) val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages)
result.success(success) result.success(success)
} catch (e: Exception) { } catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null) result.error("INITIALIZATION_ERROR", e.message, null)
@ -226,8 +184,11 @@ class MainActivity: FlutterActivity() {
val language = call.argument<String>("language") val language = call.argument<String>("language")
azureAsrHelper.recognizeOnce(language, object : AzureAsrHelper.RecognizeCallback { azureAsrHelper.recognizeOnce(language, object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String) { override fun onResult(text: String, detectedLanguage: String) {
result.success(text) result.success(mapOf(
"text" to text,
"detectedLanguage" to detectedLanguage
))
} }
override fun onError(error: String) { override fun onError(error: String) {
@ -236,24 +197,27 @@ class MainActivity: FlutterActivity() {
}) })
} }
"startContinuousRecognition" -> { "startContinuousRecognition" -> {
val language = call.argument<String>("language")
// 确保事件通道已准备好 // 确保事件通道已准备好
if (!isEventChannelReady()) { if (azureAsrEventSink == null) {
Log.w(TAG, "事件通道未准备好,等待...") result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
if (!waitForEventChannel()) { return@setMethodCallHandler
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
return@setMethodCallHandler
}
} }
val success = azureAsrHelper.startContinuousRecognition(language, object : AzureAsrHelper.ContinuousRecognizeCallback { val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) { override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf("type" to "result", "text" to text)) sendAsrEvent(mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
))
} }
override fun onRecognizing(recognizing: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
sendAsrEvent(mapOf("type" to "recognizing", "text" to recognizing)) sendAsrEvent(mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
))
} }
override fun onSessionStarted() { override fun onSessionStarted() {
@ -286,8 +250,8 @@ class MainActivity: FlutterActivity() {
} }
val success = azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { val success = azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) {} override fun onResult(text: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String) {} override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {} override fun onSessionStarted() {}
override fun onSessionStopped() {} override fun onSessionStopped() {}
override fun onCanceled(reason: String, errorDetails: String) {} override fun onCanceled(reason: String, errorDetails: String) {}
@ -307,6 +271,37 @@ class MainActivity: FlutterActivity() {
azureAsrHelper.dispose() azureAsrHelper.dispose()
result.success(true) result.success(true)
} }
"enableAutoDetectLanguage" -> {
val enable = call.argument<Boolean>("enable") ?: false
val languages = call.argument<List<String>>("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<List<String>>("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 -> { else -> {
result.notImplemented() result.notImplemented()
} }
@ -327,7 +322,7 @@ class MainActivity: FlutterActivity() {
} }
try { try {
val success = azureTtsHelper.initialize(subscriptionKey, region, language) val success = azureTtsHelper.initialize(subscriptionKey, region, language, this)
result.success(success) result.success(success)
} catch (e: Exception) { } catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null) result.error("INITIALIZATION_ERROR", e.message, null)
@ -343,6 +338,16 @@ class MainActivity: FlutterActivity() {
val volume = call.argument<Int>("volume") ?: 100 val volume = call.argument<Int>("volume") ?: 100
result.success(azureTtsHelper.setSpeechParams(rate, pitch, volume)) result.success(azureTtsHelper.setSpeechParams(rate, pitch, volume))
} }
"setAudioOutputType" -> {
val outputTypeStr = call.argument<String>("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" -> { "speakText" -> {
val text = call.argument<String>("text") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "文本不能为空", null) val text = call.argument<String>("text") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "文本不能为空", null)
@ -407,7 +412,7 @@ class MainActivity: FlutterActivity() {
} }
// ASR 事件发送方法 // ASR 事件发送方法
fun sendAsrEvent(event: Map<String, Any>) { private fun sendAsrEvent(event: Map<String, Any>) {
if (azureAsrEventSink == null) return if (azureAsrEventSink == null) return
runOnUiThread { 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) { private fun startVoiceInteractionService(call: MethodCall, result: MethodChannel.Result) {
Log.d(TAG, "启动语音交互服务(带配置参数)") Log.d(TAG, "启动语音交互服务")
// 获取配置参数 // 获取配置参数
val key = call.argument<String>("azure_speech_key") ?: "" val key = call.argument<String>("azure_speech_key") ?: ""
@ -449,9 +436,6 @@ class MainActivity: FlutterActivity() {
azureSpeechRegion = region azureSpeechRegion = region
volcanoAiApiKey = aiKey volcanoAiApiKey = aiKey
Log.d(TAG, "配置参数: key=${if (key.isNotEmpty()) key.take(5) + "..." else "空"}, region=$region, aiKey=${if (aiKey.isNotEmpty()) aiKey.take(5) + "..." else "空"}")
// 创建 Intent 并启动服务 // 创建 Intent 并启动服务
val serviceIntent = Intent(this, VoiceInteractionService::class.java) val serviceIntent = Intent(this, VoiceInteractionService::class.java)
@ -462,7 +446,6 @@ class MainActivity: FlutterActivity() {
startService(serviceIntent) startService(serviceIntent)
} }
Log.d(TAG, "语音交互服务启动命令已发送")
result.success(true) result.success(true)
} }
@ -473,7 +456,6 @@ class MainActivity: FlutterActivity() {
Log.d(TAG, "停止语音交互服务") Log.d(TAG, "停止语音交互服务")
val serviceIntent = Intent(this, VoiceInteractionService::class.java) val serviceIntent = Intent(this, VoiceInteractionService::class.java)
stopService(serviceIntent) stopService(serviceIntent)
Log.d(TAG, "语音交互服务停止命令已发送")
} }
/** /**
@ -485,7 +467,6 @@ class MainActivity: FlutterActivity() {
action = VoiceInteractionService.ACTION_PAUSE_VOICE_INTERACTION action = VoiceInteractionService.ACTION_PAUSE_VOICE_INTERACTION
} }
// 在 Android O 及以上版本,需要使用 startForegroundService
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent) startForegroundService(intent)
} else { } else {
@ -502,8 +483,6 @@ class MainActivity: FlutterActivity() {
return return
} }
Log.d(TAG, "发送聊天记录事件到Flutter: agentId=$agentId, userMessage=${userMessage.take(20)}..., assistantMessage=${assistantMessage.take(20)}...")
runOnUiThread { runOnUiThread {
try { try {
val eventData = mapOf( val eventData = mapOf(
@ -514,7 +493,6 @@ class MainActivity: FlutterActivity() {
"timestamp" to timestamp "timestamp" to timestamp
) )
voiceInteractionEventSink?.success(eventData) voiceInteractionEventSink?.success(eventData)
Log.d(TAG, "聊天记录事件已发送到Flutter")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送聊天记录事件到Flutter失败: ${e.message}") Log.e(TAG, "发送聊天记录事件到Flutter失败: ${e.message}")
} }
@ -526,62 +504,19 @@ class MainActivity: FlutterActivity() {
*/ */
private fun sendVoiceInteractionEvent(event: Map<String, Any>) { private fun sendVoiceInteractionEvent(event: Map<String, Any>) {
if (voiceInteractionEventSink == null) { if (voiceInteractionEventSink == null) {
Log.e(TAG, "无法发送语音交互事件:事件通道未准备好,事件内容: $event") Log.e(TAG, "无法发送语音交互事件:事件通道未准备好")
return return
} }
Log.d(TAG, "发送语音交互事件到Flutter: $event")
runOnUiThread { runOnUiThread {
try { try {
voiceInteractionEventSink?.success(event) voiceInteractionEventSink?.success(event)
Log.d(TAG, "语音交互事件已发送到Flutter")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送语音交互事件到Flutter失败: ${e.message}", e) 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() { override fun onDestroy() {
// 注销广播接收器 // 注销广播接收器
try { try {
@ -590,10 +525,11 @@ class MainActivity: FlutterActivity() {
Log.e(TAG, "注销广播接收器失败: ${e.message}") Log.e(TAG, "注销广播接收器失败: ${e.message}")
} }
// 在这里释放本地实例 // 释放资源
azureAsrHelper.dispose() azureAsrHelper.dispose()
azureTtsHelper.dispose() azureTtsHelper.dispose()
super.onDestroy() super.onDestroy()
} }
} }

14
android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt

@ -162,7 +162,7 @@ class VoiceInteractionService : Service() {
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) {
// 初始化ASR // 初始化ASR
azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN"))
// 初始化TTS // 初始化TTS
azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
} else { } else {
@ -392,7 +392,7 @@ class VoiceInteractionService : Service() {
val subscriptionKey = MainActivity.azureSpeechKey val subscriptionKey = MainActivity.azureSpeechKey
val serviceRegion = MainActivity.azureSpeechRegion val serviceRegion = MainActivity.azureSpeechRegion
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { 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() updateLastActivityTime()
try { try {
azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
hasSpeechDetected = true hasSpeechDetected = true
stopCurrentTTS() stopCurrentTTS()
@ -433,7 +433,7 @@ class VoiceInteractionService : Service() {
} }
} }
override fun onResult(result: String) { override fun onResult(result: String, detectedLanguage: String) {
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
processWithVolcanoAI(result) processWithVolcanoAI(result)
} }
@ -484,8 +484,8 @@ class VoiceInteractionService : Service() {
try { try {
azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(result: String) {} override fun onResult(result: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String) {} override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {} override fun onSessionStarted() {}
override fun onSessionStopped() { override fun onSessionStopped() {
isRecognitionActive = false isRecognitionActive = false

BIN
android/app/src/main/res/raw/listening.mp3

Binary file not shown.

9
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/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_translation_service.dart';
import '../../data/services/voice_interaction_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'; import '../../core/utils/logger.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
@ -11,6 +13,10 @@ class InitialBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Logger.warning('InitialBinding dependencies'); Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化)
Get.put<LanguageManager>(LanguageManager(), permanent: true);
// Azure 语音识别服务 // Azure 语音识别服务
Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true); Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true);
@ -22,6 +28,9 @@ class InitialBinding extends Bindings {
// 火山翻译服务 // 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), fenix: true); Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), fenix: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 注意:VoiceInteractionService 在 HomeController 中初始化 // 注意:VoiceInteractionService 在 HomeController 中初始化
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), fenix: true); Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(), fenix: true);

136
lib/data/services/azure_asr_service.dart

@ -19,6 +19,12 @@ class AzureAsrService {
String _currentLanguage = 'zh-CN'; String _currentLanguage = 'zh-CN';
String get currentLanguage => _currentLanguage; String get currentLanguage => _currentLanguage;
// 自动语言检测相关
bool _isAutoDetectLanguageEnabled = false;
bool get isAutoDetectLanguageEnabled => _isAutoDetectLanguageEnabled;
List<String> _supportedLanguages = ['zh-CN', 'en-US'];
List<String> get supportedLanguages => _supportedLanguages;
// 连续识别相关 // 连续识别相关
bool _isContinuousRecognitionActive = false; bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController; StreamController<RecognitionEvent>? _eventStreamController;
@ -36,6 +42,10 @@ class AzureAsrService {
String _latestRecognizedText = ''; String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText; String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言
String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage;
AzureAsrService() { AzureAsrService() {
_loadConfig(); _loadConfig();
_setupEventChannel(); _setupEventChannel();
@ -91,10 +101,21 @@ class AzureAsrService {
/// 初始化微软语音识别 SDK /// 初始化微软语音识别 SDK
/// ///
/// 返回 true 表示初始化成功,否则抛出 PlatformException /// 返回 true 表示初始化成功,否则抛出 PlatformException
Future<bool> initialize({String language = 'zh-CN'}) async { Future<bool> initialize({
String language = 'zh-CN',
bool autoDetectLanguage = false,
List<String>? supportedLanguages,
}) async {
_currentLanguage = language; _currentLanguage = language;
_isAutoDetectLanguageEnabled = autoDetectLanguage;
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
_supportedLanguages = supportedLanguages;
}
if (_isInitialized && _currentLanguage == language) { if (_isInitialized &&
_currentLanguage == language &&
_isAutoDetectLanguageEnabled == autoDetectLanguage) {
return true; return true;
} }
@ -107,6 +128,8 @@ class AzureAsrService {
'subscriptionKey': _subscriptionKey, 'subscriptionKey': _subscriptionKey,
'region': _serviceRegion, 'region': _serviceRegion,
'language': language, 'language': language,
'autoDetectLanguage': autoDetectLanguage,
'supportedLanguages': _supportedLanguages,
}); });
_isInitialized = result; _isInitialized = result;
return result; return result;
@ -126,17 +149,30 @@ class AzureAsrService {
/// 执行一次性语音识别 /// 执行一次性语音识别
/// ///
/// 返回识别的文本,如果识别失败则抛出 PlatformException /// 返回识别的文本和检测到的语言,如果识别失败则抛出 PlatformException
Future<String> recognizeSpeech() async { Future<RecognitionResult> recognizeSpeech() async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }
try { try {
final String result = await _channel.invokeMethod('recognizeOnce', { final result = await _channel.invokeMethod('recognizeOnce', {
'language': _currentLanguage, '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) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: $e');
throw e; throw e;
@ -158,9 +194,7 @@ class AzureAsrService {
try { try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast(); _eventStreamController = StreamController<RecognitionEvent>.broadcast();
final bool result = await _channel.invokeMethod('startContinuousRecognition', { final bool result = await _channel.invokeMethod('startContinuousRecognition');
'language': _currentLanguage,
});
if (!result) { if (!result) {
_cleanupEventStream(); _cleanupEventStream();
@ -210,18 +244,23 @@ class AzureAsrService {
switch (eventType) { switch (eventType) {
case 'result': case 'result':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text; _latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.finalResult, type: RecognitionEventType.finalResult,
text: text, text: text,
detectedLanguage: detectedLanguage,
)); ));
break; break;
case 'recognizing': case 'recognizing':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult, type: RecognitionEventType.intermediateResult,
text: text, text: text,
detectedLanguage: detectedLanguage,
)); ));
break; break;
@ -293,6 +332,60 @@ class AzureAsrService {
_isInitialized = false; _isInitialized = false;
} }
} }
/// 启用或禁用自动语言检测
Future<bool> enableAutoDetectLanguage(bool enable, {List<String>? 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<bool> setSupportedLanguages(List<String> 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 类型中有效) /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text; final String text;
/// 检测到的语言(仅在启用自动语言检测时有效)
final String detectedLanguage;
/// 错误信息(仅在 error 和 canceled 类型中有效) /// 错误信息(仅在 error 和 canceled 类型中有效)
final String error; final String error;
RecognitionEvent({ RecognitionEvent({
required this.type, required this.type,
this.text = '', this.text = '',
this.detectedLanguage = '',
this.error = '', this.error = '',
}); });
@override @override
String toString() { 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}';
} }
} }

94
lib/data/services/azure_tts_service.dart

@ -13,6 +13,18 @@ class AzureTtsException implements Exception {
String toString() => message; String toString() => message;
} }
/// 音频输出设备类型
enum AudioOutputType {
/// 扬声器
speaker,
/// 听筒
earpiece,
/// 自动选择(有耳机用耳机,没有则用听筒)
auto
}
/// 微软 Text-to-Speech 服务 /// 微软 Text-to-Speech 服务
/// ///
/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, /// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互,
@ -30,6 +42,7 @@ class AzureTtsService extends GetxService {
int _currentRate = 0; int _currentRate = 0;
int _currentPitch = 0; int _currentPitch = 0;
int _currentVolume = 100; int _currentVolume = 100;
AudioOutputType _currentAudioOutputType = AudioOutputType.auto;
// 语音合成队列 // 语音合成队列
final List<_SpeechItem> _textQueue = []; final List<_SpeechItem> _textQueue = [];
@ -42,8 +55,10 @@ class AzureTtsService extends GetxService {
// 可观察状态 // 可观察状态
final isEnabled = true.obs; final isEnabled = true.obs;
final isSpeaking = false.obs; final isSpeaking = false.obs;
final audioOutputType = AudioOutputType.auto.obs;
String get currentVoice => _currentVoice; String get currentVoice => _currentVoice;
AudioOutputType get currentAudioOutputType => _currentAudioOutputType;
AzureTtsService() { AzureTtsService() {
_loadConfig(); _loadConfig();
@ -72,6 +87,15 @@ class AzureTtsService extends GetxService {
}); });
_isInitialized = result; _isInitialized = result;
// 确保设置默认的音频输出设备类型
if (result) {
// 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值
// 只需要确保 Dart 层的状态与原生层一致
_currentAudioOutputType = AudioOutputType.auto;
audioOutputType.value = AudioOutputType.auto;
}
return result; return result;
} catch (e) { } catch (e) {
Logger.error('初始化失败: $e'); Logger.error('初始化失败: $e');
@ -82,6 +106,7 @@ class AzureTtsService extends GetxService {
/// 设置语音 /// 设置语音
Future<bool> setVoice(String voiceName) async { Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (voiceName == _currentVoice) return true;
try { try {
final result = await _channel.invokeMethod('setVoice', { final result = await _channel.invokeMethod('setVoice', {
@ -96,6 +121,28 @@ class AzureTtsService extends GetxService {
} }
} }
/// 设置音频输出设备类型
Future<bool> 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<bool> setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async { Future<bool> setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
@ -167,9 +214,14 @@ class AzureTtsService extends GetxService {
} }
/// 添加文本到队列并播放 /// 添加文本到队列并播放
Future<bool> speak(String text, {int? rate, int? pitch, int? volume}) async { Future<bool> speak(String text, {int? rate, int? pitch, int? volume, AudioOutputType? outputType}) async {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
_textQueue.add(_SpeechItem( _textQueue.add(_SpeechItem(
text: text, text: text,
rate: rate ?? _currentRate, rate: rate ?? _currentRate,
@ -184,9 +236,14 @@ class AzureTtsService extends GetxService {
/// 处理AI流式输出文本 /// 处理AI流式输出文本
/// ///
/// 将流式输出的文本片段添加到缓冲区,并在检测到完整句子时将其送入播放队列 /// 将流式输出的文本片段添加到缓冲区,并在检测到完整句子时将其送入播放队列
Future<bool> processStreamText(String textChunk) async { Future<bool> processStreamText(String textChunk, {AudioOutputType? outputType}) async {
if (!isEnabled.value || textChunk.isEmpty) return false; if (!isEnabled.value || textChunk.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
// 添加文本到缓冲区 // 添加文本到缓冲区
_streamBuffer += textChunk; _streamBuffer += textChunk;
@ -242,9 +299,14 @@ class AzureTtsService extends GetxService {
/// 强制处理缓冲区中的剩余文本 /// 强制处理缓冲区中的剩余文本
/// ///
/// 在流式输出结束时调用,确保缓冲区中的所有文本都被处理 /// 在流式输出结束时调用,确保缓冲区中的所有文本都被处理
Future<bool> flushStreamBuffer() async { Future<bool> flushStreamBuffer({AudioOutputType? outputType}) async {
if (!isEnabled.value || _streamBuffer.isEmpty) return false; if (!isEnabled.value || _streamBuffer.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
// 先尝试使用processStreamText处理可能的完整句子 // 先尝试使用processStreamText处理可能的完整句子
await processStreamText(''); await processStreamText('');
@ -270,9 +332,14 @@ class AzureTtsService extends GetxService {
} }
/// 连续播放多段文本 /// 连续播放多段文本
Future<bool> speakMultiple(List<String> texts, {int? rate, int? pitch, int? volume}) async { Future<bool> speakMultiple(List<String> texts, {int? rate, int? pitch, int? volume, AudioOutputType? outputType}) async {
if (!isEnabled.value || texts.isEmpty) return false; if (!isEnabled.value || texts.isEmpty) return false;
// 如果指定了新的音频输出设备类型,先设置
if (outputType != null && outputType != _currentAudioOutputType) {
await setAudioOutputType(outputType);
}
for (final text in texts) { for (final text in texts) {
if (text.isNotEmpty) { if (text.isNotEmpty) {
_textQueue.add(_SpeechItem( _textQueue.add(_SpeechItem(
@ -374,6 +441,25 @@ class AzureTtsService extends GetxService {
if (!isEnabled.value) stop(); if (!isEnabled.value) stop();
} }
/// 切换音频输出设备类型
Future<bool> 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<void> dispose() async { Future<void> dispose() async {
if (!_isInitialized) return; if (!_isInitialized) return;

532
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<String, LanguageInfo> _languageMap = {};
// 中文名称到 ASR 代码的映射
final Map<String, String> _chineseNameToAsrCodeMap = {};
// 英文名称到 ASR 代码的映射
final Map<String, String> _englishNameToAsrCodeMap = {};
// 短代码到 ASR 代码的映射
final Map<String, String> _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<String, LanguageInfo> getAllLanguages() {
return Map.from(_languageMap);
}
/// 获取所有支持的语言的中文名称
List<String> getAllChineseNames() {
return _chineseNameToAsrCodeMap.keys.toList();
}
/// 获取所有支持的语言的英文名称
List<String> 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<String, String> getChineseNameToAsrCodeMap() {
return Map.from(_chineseNameToAsrCodeMap);
}
/// 获取英文名称到 ASR 代码的映射
Map<String, String> getEnglishNameToAsrCodeMap() {
return Map.from(_englishNameToAsrCodeMap);
}
/// 获取短代码到 ASR 代码的映射
Map<String, String> getShortCodeToAsrCodeMap() {
return Map.from(_shortCodeToAsrCodeMap);
}
}

65
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:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import 'language_manager.dart';
class VolcanoTranslationService extends GetxService { class VolcanoTranslationService extends GetxService {
// API 配置 // API 配置
@ -18,38 +19,11 @@ class VolcanoTranslationService extends GetxService {
// 初始化标志 // 初始化标志
bool _isInitialized = false; bool _isInitialized = false;
// 语言代码映射 // 语言管理器
final Map<String, String> _languageCodeMap = { final LanguageManager _languageManager = Get.find<LanguageManager>();
"中文 (Chinese)": "zh",
"英语 (English)": "en", // 构造函数
"日语 (Japanese)": "ja", VolcanoTranslationService();
"韩语 (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",
};
// 初始化服务 // 初始化服务
Future<bool> initialize({ Future<bool> initialize({
@ -75,11 +49,6 @@ class VolcanoTranslationService extends GetxService {
} }
} }
// 获取语言代码
String _getLanguageCode(String language) {
return _languageCodeMap[language] ?? 'zh';
}
// 生成签名 // 生成签名
Map<String, String> _generateSignature(String method, Map<String, dynamic> requestBody, Map<String, String> queryParams) { Map<String, String> _generateSignature(String method, Map<String, dynamic> requestBody, Map<String, String> queryParams) {
// 1. 准备时间相关参数 // 1. 准备时间相关参数
@ -151,16 +120,30 @@ class VolcanoTranslationService extends GetxService {
// 翻译文本 // 翻译文本
Future<String?> translateText({ Future<String?> translateText({
required String text, required String text,
required String sourceLanguage, required String sourceLanguageCode,
required String targetLanguage, required String targetLanguageCode,
}) async { }) async {
if (!_isInitialized) { if (!_isInitialized) {
return null; return null;
} }
try { 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 = { final requestBody = {

6
lib/modules/chat/controllers/chat_controller.dart

@ -86,9 +86,9 @@ class ChatController extends GetxController {
timestamp: DateTime.now(), timestamp: DateTime.now(),
)); ));
// 如果启用了TTS,播放欢迎消息 // 如果启用了TTS,播放欢迎消息
if (isTtsEnabled.value) { // if (isTtsEnabled.value) {
_ttsService.speak(agent.welcomeMessage); // _ttsService.speak(agent.welcomeMessage);
} // }
} }
// 如果playVoiceOnEnter为true,自动激活语音识别 // 如果playVoiceOnEnter为true,自动激活语音识别

65
lib/modules/test/controllers/azure_asr_test_controller.dart

@ -20,6 +20,7 @@ class AzureAsrTestController extends GetxController {
final RxBool isListening = false.obs; final RxBool isListening = false.obs;
final RxString recognizedText = ''.obs; final RxString recognizedText = ''.obs;
final RxString partialText = ''.obs; final RxString partialText = ''.obs;
final RxString detectedLanguage = ''.obs;
final Rx<AzureAsrState> asrState = AzureAsrState.notInitialized.obs; final Rx<AzureAsrState> asrState = AzureAsrState.notInitialized.obs;
// 语言选项 // 语言选项
@ -37,6 +38,10 @@ class AzureAsrTestController extends GetxController {
'ru-RU', // 俄语 'ru-RU', // 俄语
]; ];
// 自动语言检测
final RxBool isAutoDetectEnabled = false.obs;
final RxList<String> selectedSupportedLanguages = <String>['zh-CN', 'en-US'].obs;
// 订阅 // 订阅
StreamSubscription? _recognitionSubscription; StreamSubscription? _recognitionSubscription;
@ -49,7 +54,11 @@ class AzureAsrTestController extends GetxController {
// 初始化ASR服务 // 初始化ASR服务
Future<void> _initializeAsr() async { Future<void> _initializeAsr() async {
try { 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; asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error;
} catch (e) { } catch (e) {
Logger.error('初始化ASR服务失败: $e'); Logger.error('初始化ASR服务失败: $e');
@ -71,8 +80,9 @@ class AzureAsrTestController extends GetxController {
partialText.value = ''; partialText.value = '';
final result = await _azureAsrService.recognizeSpeech(); final result = await _azureAsrService.recognizeSpeech();
if (result.isNotEmpty) { if (result.text.isNotEmpty) {
recognizedText.value = result; recognizedText.value = result.text;
detectedLanguage.value = result.detectedLanguage;
} }
} catch (e) { } catch (e) {
Logger.error('开始简单语音识别出错: $e'); Logger.error('开始简单语音识别出错: $e');
@ -120,6 +130,7 @@ class AzureAsrTestController extends GetxController {
case RecognitionEventType.finalResult: case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
recognizedText.value = event.text; recognizedText.value = event.text;
detectedLanguage.value = event.detectedLanguage;
} }
partialText.value = ''; partialText.value = '';
break; break;
@ -127,6 +138,9 @@ class AzureAsrTestController extends GetxController {
case RecognitionEventType.intermediateResult: case RecognitionEventType.intermediateResult:
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
partialText.value = event.text; partialText.value = event.text;
if (event.detectedLanguage.isNotEmpty) {
detectedLanguage.value = event.detectedLanguage;
}
} }
break; break;
@ -209,12 +223,57 @@ class AzureAsrTestController extends GetxController {
recognizedText.value = ''; recognizedText.value = '';
partialText.value = ''; partialText.value = '';
detectedLanguage.value = '';
} catch (e) { } catch (e) {
Logger.error('更改语言出错: $e'); Logger.error('更改语言出错: $e');
asrState.value = AzureAsrState.error; asrState.value = AzureAsrState.error;
} }
} }
// 切换自动语言检测
Future<void> 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<void> updateSupportedLanguages(List<String> 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 @override
void onClose() { void onClose() {
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();

5
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_asr_service.dart';
import '../../../data/services/azure_tts_service.dart'; import '../../../data/services/azure_tts_service.dart';
import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/language_manager.dart';
class TranslationBinding implements Bindings { class TranslationBinding implements Bindings {
@override @override
void dependencies() { void dependencies() {
// 注册服务 Get.put(LanguageManager(), permanent: true);
Get.put(AzureAsrService(), permanent: true); Get.put(AzureAsrService(), permanent: true);
Get.put(AzureTtsService(), permanent: true); Get.put(AzureTtsService(), permanent: true);
Get.put(VolcanoTranslationService(), permanent: true); Get.put(VolcanoTranslationService(), permanent: true);
// 注册控制器
Get.put(TranslationController()); Get.put(TranslationController());
} }
} }

739
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/volcano_translation_service.dart';
import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/azure_tts_service.dart'; import '../../../data/services/azure_tts_service.dart';
import '../../../data/services/language_manager.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
class TranslationController extends GetxController { class TranslationController extends GetxController {
// Services // 服务
final AzureAsrService _asrService = Get.find<AzureAsrService>(); final AzureAsrService _asrService = Get.find<AzureAsrService>();
final VolcanoTranslationService _translationService = final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
Get.find<VolcanoTranslationService>();
final AzureTtsService _ttsService = Get.find<AzureTtsService>(); final AzureTtsService _ttsService = Get.find<AzureTtsService>();
final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
// Storage key // 存储相关
static const String _historyKey = 'translation_history'; static const String _historyKey = 'translation_history';
static const int _maxHistoryItems = 100; // 最多保存100条历史记录 static const int _maxHistoryItems = 100;
// State variables // 状态变量
final isRecognizing = false.obs; final isRecognizing = false.obs;
final isTranslating = false.obs; final isTranslating = false.obs;
final isTtsEnabled = true.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 = <TranslationItem>[].obs; final translationHistory = <TranslationItem>[].obs;
// 支持的语言列表 - 改为Map结构,键为显示名称,值为语言代码
final Map<String, String> 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<TranslationItem?> currentItem = Rx<TranslationItem?>(null); final Rx<TranslationItem?> currentItem = Rx<TranslationItem?>(null);
String? currentSessionId;
// Subscription for recognition stream
StreamSubscription? _recognitionSubscription; // UI相关
// Scroll controller for ListView
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
// Timer for debouncing translation requests // 其他
StreamSubscription? _recognitionSubscription;
Timer? _translationDebounceTimer; Timer? _translationDebounceTimer;
Timer? _scrollDebounceTimer; // 滚动防抖定时器
bool _isNearBottom = true; // 标记是否在底部附近
// 当前会话ID // 获取支持的语言列表
String? currentSessionId; Map<String, String> get supportedLanguages => _languageManager.getChineseNameToAsrCodeMap();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initController();
}
// 初始化控制器
// 加载历史记录 void _initController() {
_initLanguageCodes();
loadTranslationHistory(); loadTranslationHistory();
_initServices();
// Initialize the translation service
_initTranslationService();
// 初始化 Azure ASR 服务
_initAzureAsrService();
// 监听部分识别结果和识别状态
_setupListeners(); _setupListeners();
// 添加后处理,确保在历史记录加载后滚动到底部 // 初始化完成后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// 延迟执行滚动操作,确保视图已完全渲染 // 延迟 300ms 再次尝试滚动,确保列表已完全渲染
Future.delayed(const Duration(milliseconds: 300), () { 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<void> _initServices() async {
await _initTranslationService();
await _initAzureAsrService();
}
@override // 初始化语言代码
void onClose() { void _initLanguageCodes() {
stopRecognition(); final sourceAsrCode = _languageManager.getAsrCodeByChineseName(sourceLanguage.value);
_recognitionSubscription?.cancel(); if (sourceAsrCode != null) {
_translationDebounceTimer?.cancel(); sourceLanguageCode.value = sourceAsrCode;
scrollController.dispose(); }
super.onClose();
final targetAsrCode = _languageManager.getAsrCodeByChineseName(targetLanguage.value);
if (targetAsrCode != null) {
targetLanguageCode.value = targetAsrCode;
}
} }
// Initialize the translation service // 初始化翻译服务
Future<void> _initTranslationService() async { Future<void> _initTranslationService() async {
try { try {
await _translationService.initialize(); await _translationService.initialize();
} catch (e) { } catch (e) {
Get.snackbar( Logger.error('初始化翻译服务失败: $e');
'Error',
'Failed to initialize translation service: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
} }
} }
// 初始化 Azure ASR 服务 // 初始化语音识别服务
Future<void> _initAzureAsrService() async { Future<void> _initAzureAsrService() async {
try { try {
// 设置识别语言 final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
final sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
final recognitionLanguage = _getRecognitionLanguage(sourceCode);
// 使用正确定义的变量顺序
await _ttsService.initialize(); await _ttsService.initialize();
await _asrService.initialize(language: recognitionLanguage); await _asrService.initialize(
Logger.info('Azure ASR 服务初始化成功'); autoDetectLanguage: true,
supportedLanguages: asrSupportedLanguages,
);
} catch (e) { } catch (e) {
Logger.error('初始化 Azure ASR 服务失败: $e'); Logger.error('初始化语音识别服务失败: $e');
} }
} }
// Toggle TTS feature // 设置监听器
void toggleTts() { void _setupListeners() {
isTtsEnabled.toggle(); // 监听历史记录变化,当有新项添加时滚动到底部
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<void> _updateAsrSupportedLanguages() async {
try {
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value];
await _asrService.setSupportedLanguages(asrSupportedLanguages);
} catch (e) {
Logger.error('更新ASR支持的语言失败: $e');
}
}
// 切换TTS功能
void toggleTts() => isTtsEnabled.toggle();
// 开始语音识别
Future<void> startRecognition() async { Future<void> startRecognition() async {
if (isRecognizing.value) return; if (isRecognizing.value) return;
try { try {
// 生成新的会话ID await _updateAsrSupportedLanguages();
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// Initialize a new translation item // 初始化新的翻译项
currentItem.value = TranslationItem( currentItem.value = TranslationItem(
sourceText: '', sourceText: '',
translatedText: '', translatedText: '',
sourceLanguage: sourceLanguage.value, sourceLanguageCode: sourceLanguageCode.value,
targetLanguage: targetLanguage.value, targetLanguageCode: targetLanguageCode.value,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId!, 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(); final recognitionStream = await _asrService.startContinuousRecognition();
isRecognizing.value = true; isRecognizing.value = true;
// Reset the current item
if (currentItem.value != null) {
currentItem.value!.sourceText = '';
currentItem.value!.translatedText = '';
}
// 监听识别结果 // 监听识别结果
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = recognitionStream.listen((event) { _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent);
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}');
}
});
} catch (e) { } catch (e) {
isRecognizing.value = false; isRecognizing.value = false;
Get.snackbar( Logger.error('启动语音识别失败: $e');
'Error',
'启动语音识别失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
} }
} }
// 将语言代码转换为识别语言代码 // 处理识别事件
String _getRecognitionLanguage(String languageCode) { void _handleRecognitionEvent(RecognitionEvent event) {
// 语言代码到识别语言代码的映射 if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) {
final Map<String, String> languageToRecognitionCode = { detectedLanguageCode = event.detectedLanguage;
'zh': 'zh-CN', handleFinalResult(event.text);
'en': 'en-US', } else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) {
'ja': 'ja-JP', if (event.detectedLanguage.isNotEmpty) {
'ko': 'ko-KR', detectedLanguageCode = event.detectedLanguage;
'fr': 'fr-FR', }
'de': 'de-DE', handleIntermediateResult(event.text);
'es': 'es-ES', } else if (event.type == RecognitionEventType.error) {
'it': 'it-IT', isRecognizing.value = false;
'ru': 'ru-RU', Logger.error('语音识别错误: ${event.error}');
'pt': 'pt-BR', } else if (event.type == RecognitionEventType.canceled) {
'ar': 'ar-EG', isRecognizing.value = false;
'th': 'th-TH', Logger.error('语音识别取消: ${event.error}');
}; }
return languageToRecognitionCode[languageCode] ?? 'zh-CN';
} }
// Stop voice recognition // 停止语音识别
Future<void> stopRecognition() async { Future<void> stopRecognition() async {
if (!isRecognizing.value) return; if (!isRecognizing.value) return;
@ -236,42 +247,48 @@ class TranslationController extends GetxController {
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
isRecognizing.value = false; 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; currentSessionId = null;
// 重置当前项
currentItem.value = null; currentItem.value = null;
} catch (e) { } catch (e) {
Get.snackbar('Error', '停止语音识别失败: ${e.toString()}', Logger.error('停止语音识别失败: $e');
snackPosition: SnackPosition.BOTTOM);
} }
} }
// Handle intermediate recognition result // 处理中间识别结果
void handleIntermediateResult(String text) { void handleIntermediateResult(String text) {
if (text.isEmpty) return; if (text.isEmpty) return;
// 如果当前没有活跃的翻译项,创建一个新的
if (currentItem.value == null) { if (currentItem.value == null) {
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
currentItem.value = TranslationItem( currentItem.value = TranslationItem(
sourceText: text, sourceText: text,
translatedText: '', translatedText: '',
sourceLanguage: sourceLanguage.value, sourceLanguageCode: sourceLanguageCode.value,
targetLanguage: targetLanguage.value, targetLanguageCode: targetLanguageCode.value,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId!, sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty || isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId,
translationHistory.last.sessionId != currentSessionId, isIntermediate: true, // 保留标记,用于内部逻辑
); );
currentItem.refresh();
} else { } else {
// 更新当前翻译项 // 更新当前项
currentItem.value!.sourceText = text; currentItem.value!.sourceText = text;
// 通知 GetX 更新 currentItem.value!.isIntermediate = true; // 保留标记,用于内部逻辑
currentItem.refresh(); currentItem.refresh();
} }
@ -284,87 +301,42 @@ class TranslationController extends GetxController {
// 翻译当前项 // 翻译当前项
Future<void> translateCurrentItem({bool isIntermediate = false}) async { Future<void> translateCurrentItem({bool isIntermediate = false}) async {
if (currentItem.value == null || currentItem.value!.sourceText.isEmpty) { if (currentItem.value == null || currentItem.value!.sourceText.isEmpty) return;
return; await translateText(currentItem.value!.sourceText);
}
final sourceText = currentItem.value!.sourceText;
final translationResult = await translateText(sourceText);
if (translationResult != null && isIntermediate) {
// 如果是中间结果,不播放TTS
// 滚动到底部显示最新内容
_scrollToBottom();
}
} }
// Handle final recognition result // 处理最终识别结果
void handleFinalResult(String text) async { Future<void> handleFinalResult(String text) async {
// 如果没有文本或当前项为空,直接返回
if (text.isEmpty || currentItem.value == null) return; if (text.isEmpty || currentItem.value == null) return;
// 更新当前项的源文本 // 更新当前项,但不立即添加到历史记录
currentItem.value!.sourceText = text; currentItem.value!.sourceText = text;
currentItem.value!.isIntermediate = false; // 标记为最终结果
currentItem.refresh(); currentItem.refresh();
// 添加到历史记录 // 先翻译,保持UI稳定
translationHistory.add(currentItem.value!);
saveTranslationHistory();
// 重置当前项
currentItem.value = null;
// 对最终结果进行一次翻译,确保翻译结果是最新的
final translationResult = await translateText(text); 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); 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<String?> translateText(String sourceText) async { Future<String?> translateText(String sourceText) async {
if (sourceText.isEmpty) return null; if (sourceText.isEmpty) return null;
@ -372,53 +344,49 @@ class TranslationController extends GetxController {
String? translationResult; String? translationResult;
try { try {
// 检测源文本语言 // 根据检测到的语言确定源语言和目标语言
final detectedLanguage = detectLanguage(sourceText); final bool shouldSwap = detectedLanguageCode == sourceLanguageCode.value;
final bool shouldSwapLanguages = detectedLanguage != sourceLanguage.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, text: sourceText,
sourceLanguage: sourceLanguageCode: detectedSourceLanguageCode,
shouldSwapLanguages ? detectedLanguage : sourceLanguage.value, targetLanguageCode: detectedTargetLanguageCode,
targetLanguage:
shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value,
); );
if (result != null) { if (translationResult != null) {
translationResult = result; _updateTranslationResult(sourceText, translationResult, detectedSourceLanguageCode, detectedTargetLanguageCode);
// 更新翻译结果
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();
}
} }
return translationResult;
} catch (e) { } catch (e) {
Get.snackbar('Error', '翻译失败: ${e.toString()}', Logger.error('翻译失败: $e');
snackPosition: SnackPosition.BOTTOM);
return null;
} finally { } finally {
isTranslating.value = false; 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; if (text.isEmpty) return;
try { try {
// 获取目标语言代码 final bool shouldSwap = detectedLanguageCode == targetLanguageCode.value;
// final targetCode = supportedLanguages[targetLanguage.value] ?? 'en'; final detectedTargetLanguageCode = shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
final voiceName = _languageManager.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural';
// // 设置合适的语音 await _ttsService.setVoice(voiceName);
// final voiceName = _getTtsVoiceName(targetCode);
// await _ttsService.setVoice(voiceName);
// 播放文本
await _ttsService.speak(text); await _ttsService.speak(text);
} catch (e) { } catch (e) {
Get.snackbar('Error', '播放翻译失败: ${e.toString()}', Logger.error('播放翻译失败: $e');
snackPosition: SnackPosition.BOTTOM);
} }
} }
// 获取TTS语音名称
String _getTtsVoiceName(String languageCode) {
// 语言代码到TTS语音名称的映射
final Map<String, String> 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<void> stopAll() async { Future<void> stopAll() async {
try { try {
// 停止语音识别
if (isRecognizing.value) { if (isRecognizing.value) {
await stopRecognition(); await stopRecognition();
} }
// 停止TTS播放 // 取消所有定时器
_translationDebounceTimer?.cancel();
_scrollDebounceTimer?.cancel();
await _ttsService.stop(); await _ttsService.stop();
} catch (e) { } catch (e) {
Get.snackbar('Error', '停止操作失败: ${e.toString()}', Logger.error('停止操作失败: $e');
snackPosition: SnackPosition.BOTTOM);
} }
} }
// 播放特定翻译项 // 播放特定翻译项
Future<void> playTranslationItem(TranslationItem item) async { Future<void> playTranslationItem(TranslationItem item) async {
if (item.translatedText.isEmpty) return; 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<void> playText(String text) async { Future<void> playText(String text, {String? languageCode}) async {
if (text.isEmpty) return; if (text.isEmpty) return;
await playTranslatedText(text);
}
// 滚动到列表底部
void _scrollToBottom() {
if (!scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
return;
}
try { try {
scrollController.animateTo( final voiceName = languageCode != null
scrollController.position.maxScrollExtent, ? _languageManager.getTtsVoiceNameByAsrCode(languageCode) ?? 'en-US-AriaNeural'
duration: const Duration(milliseconds: 300), : _languageManager.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ?? 'en-US-AriaNeural';
curve: Curves.easeOut,
); await _ttsService.setVoice(voiceName);
await _ttsService.speak(text);
} catch (e) { } catch (e) {
// 如果滚动失败,尝试延迟后再次滚动 Logger.error('播放文本失败: $e');
Future.delayed(const Duration(milliseconds: 100), () {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
} }
} }
// 简洁高效的滚动到底部方法
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() { void swapLanguages() {
final temp = sourceLanguage.value; // 先停止当前的翻译操作
stopAll();
final tempName = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value; sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp; targetLanguage.value = tempName;
// 如果正在识别,需要停止并重新开始 final tempCode = sourceLanguageCode.value;
if (isRecognizing.value) { sourceLanguageCode.value = targetLanguageCode.value;
stopRecognition(); targetLanguageCode.value = tempCode;
startRecognition();
}
} }
// 设置源语言 // 设置源语言
void setSourceLanguage(String language) { void setSourceLanguage(String language) {
if (sourceLanguage.value != language) { if (sourceLanguage.value != language) {
// 先停止当前的翻译操作
stopAll();
sourceLanguage.value = language; sourceLanguage.value = language;
final asrCode = _languageManager.getAsrCodeByChineseName(language);
if (asrCode != null) {
sourceLanguageCode.value = asrCode;
}
_updateAsrSupportedLanguages();
// 如果正在识别,需要停止并重新开始
if (isRecognizing.value) { if (isRecognizing.value) {
stopRecognition(); stopRecognition();
startRecognition(); startRecognition();
@ -542,70 +525,75 @@ class TranslationController extends GetxController {
// 设置目标语言 // 设置目标语言
void setTargetLanguage(String language) { void setTargetLanguage(String language) {
if (targetLanguage.value != language) { if (targetLanguage.value != language) {
// 先停止当前的翻译操作
stopAll();
targetLanguage.value = language; targetLanguage.value = language;
final asrCode = _languageManager.getAsrCodeByChineseName(language);
if (asrCode != null) {
targetLanguageCode.value = asrCode;
}
_updateAsrSupportedLanguages();
} }
} }
// Load translation history from storage // 加载翻译历史
void loadTranslationHistory() { void loadTranslationHistory() {
final historyJson = _storage.read(_historyKey); final historyJson = _storage.read(_historyKey);
if (historyJson != null) { if (historyJson != null) {
try { try {
final List<dynamic> historyList = jsonDecode(historyJson); final List<dynamic> historyList = jsonDecode(historyJson);
translationHistory.value = translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList();
historyList.map((item) => TranslationItem.fromJson(item)).toList();
} catch (e) { } catch (e) {
// 如果解析失败,清除历史记录
_storage.remove(_historyKey); _storage.remove(_historyKey);
Logger.error('加载翻译历史失败: $e');
} }
} }
} }
// Save translation history to storage // 保存翻译历史
void saveTranslationHistory() { void saveTranslationHistory() {
// 限制历史记录数量,保留最新的记录
if (translationHistory.length > _maxHistoryItems) { if (translationHistory.length > _maxHistoryItems) {
translationHistory.removeRange( translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems);
0, translationHistory.length - _maxHistoryItems);
} }
final List<Map<String, dynamic>> historyJson = try {
translationHistory.map((item) => item.toJson()).toList(); final List<Map<String, dynamic>> historyJson = translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson)); _storage.write(_historyKey, jsonEncode(historyJson));
} catch (e) {
Logger.error('保存翻译历史失败: $e');
}
} }
// Clear translation history // 清除翻译历史
void clearTranslationHistory() { void clearTranslationHistory() {
translationHistory.clear(); translationHistory.clear();
_storage.remove(_historyKey); _storage.remove(_historyKey);
} }
// 设置监听器
void _setupListeners() {
// 在新版本中,我们通过recognitionStream直接监听事件
// 这部分逻辑已经在startRecognition方法中实现
Logger.info('语音识别事件监听已设置');
}
} }
// Model class for translation items // 翻译项模型
class TranslationItem { class TranslationItem {
String sourceText; String sourceText;
String translatedText; String translatedText;
final String sourceLanguage; String sourceLanguageCode;
final String targetLanguage; String targetLanguageCode;
final DateTime timestamp; final DateTime timestamp;
String sessionId; String sessionId;
final bool isFirstInSession; final bool isFirstInSession;
bool isIntermediate; // 是否为中间结果
TranslationItem({ TranslationItem({
required this.sourceText, required this.sourceText,
required this.translatedText, required this.translatedText,
required this.sourceLanguage, required this.sourceLanguageCode,
required this.targetLanguage, required this.targetLanguageCode,
required this.timestamp, required this.timestamp,
required this.sessionId, required this.sessionId,
this.isFirstInSession = false, this.isFirstInSession = false,
this.isIntermediate = false,
}); });
factory TranslationItem.fromJson(Map<String, dynamic> json) { factory TranslationItem.fromJson(Map<String, dynamic> json) {
@ -619,12 +607,12 @@ class TranslationItem {
return TranslationItem( return TranslationItem(
sourceText: json['sourceText'] ?? '', sourceText: json['sourceText'] ?? '',
translatedText: json['translatedText'] ?? '', translatedText: json['translatedText'] ?? '',
sourceLanguage: json['sourceLanguage'] ?? '中文 (Chinese)', sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN',
targetLanguage: json['targetLanguage'] ?? '英语 (English)', targetLanguageCode: json['targetLanguageCode'] ?? 'en-US',
timestamp: timestamp, timestamp: timestamp,
sessionId: sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: json['isFirstInSession'] ?? false, isFirstInSession: json['isFirstInSession'] ?? false,
isIntermediate: false, // 从存储加载的都是最终结果
); );
} }
@ -632,11 +620,24 @@ class TranslationItem {
return { return {
'sourceText': sourceText, 'sourceText': sourceText,
'translatedText': translatedText, 'translatedText': translatedText,
'sourceLanguage': sourceLanguage, 'sourceLanguageCode': sourceLanguageCode,
'targetLanguage': targetLanguage, 'targetLanguageCode': targetLanguageCode,
'timestamp': timestamp.toIso8601String(), 'timestamp': timestamp.toIso8601String(),
'sessionId': sessionId, 'sessionId': sessionId,
'isFirstInSession': isFirstInSession, 'isFirstInSession': isFirstInSession,
// 不保存isIntermediate,因为存储的都是最终结果
}; };
} }
// 获取源语言的中文名称
String get sourceLanguageName {
final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? '未知语言';
}
// 获取目标语言的中文名称
String get targetLanguageName {
final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? '未知语言';
}
} }

183
lib/modules/translation/controllers/translation_detail_controller.dart

@ -4,142 +4,125 @@ import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart'; import '../controllers/translation_history_controller.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'dart:async'; import 'dart:async';
import '../../../core/utils/logger.dart';
class TranslationDetailController extends GetxController { class TranslationDetailController extends GetxController {
// 依赖注入 // 依赖注入
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final TranslationHistoryController historyController = Get.find<TranslationHistoryController>();
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
// 存储键前缀 // 存储键前缀
static const String _summaryKeyPrefix = 'translation_summary_'; 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 selectedTabIndex = 0.obs;
final isGeneratingSummary = false.obs; final isGeneratingSummary = false.obs;
final summaryResult = ''.obs; final summaryResult = ''.obs;
final isLoading = true.obs;
// 当前会话信息 // 当前会话信息
late final TranslationItem currentItem; late final TranslationItem currentItem;
late final List<TranslationItem> sessionItems; late final List<TranslationItem> sessionItems;
late final TranslationHistoryController historyController;
// 初始化方法 @override
void initialize(TranslationItem item, List<TranslationItem> items) { void onInit() {
currentItem = item; super.onInit();
sessionItems = items;
// 尝试从存储中加载摘要 // 从路由参数中获取数据
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<void> _asyncInitialize() async {
isLoading.value = true;
try {
// 使用 Future.microtask 将操作放到后台线程
await Future.microtask(() {
// 尝试从存储中加载摘要
loadSavedSummary();
});
} catch (e) {
Logger.error('初始化详情控制器失败: $e');
} finally {
isLoading.value = false;
}
} }
// 加载保存的摘要 // 加载保存的摘要
void loadSavedSummary() { void loadSavedSummary() {
final String? savedSummary = _storage.read<String>(_getSummaryKey()); try {
if (savedSummary != null && savedSummary.isNotEmpty) { final String? savedSummary = _storage.read<String>(_getSummaryKey());
summaryResult.value = savedSummary; if (savedSummary != null && savedSummary.isNotEmpty) {
summaryResult.value = savedSummary;
}
} catch (e) {
Logger.error('加载摘要失败: $e');
} }
} }
// 保存摘要 // 保存摘要
void saveSummary(String summary) { void saveSummary(String summary) {
if (summary.isNotEmpty) { if (summary.isEmpty) return;
try {
_storage.write(_getSummaryKey(), summary); _storage.write(_getSummaryKey(), summary);
} catch (e) {
Logger.error('保存摘要失败: $e');
} }
} }
// 获取摘要的存储键 // 获取摘要的存储键
String _getSummaryKey() { String _getSummaryKey() => _summaryKeyPrefix + currentItem.sessionId;
return _summaryKeyPrefix + currentItem.sessionId;
}
// 切换选项卡 // 切换选项卡
void switchTab(int index) { void switchTab(int index) => selectedTabIndex.value = index;
selectedTabIndex.value = index;
}
// 播放翻译项 // 播放翻译项
void playTranslationItem(TranslationItem item) { void playTranslationItem(TranslationItem item) => historyController.playTranslationItem(item);
historyController.playTranslationItem(item);
}
// 播放文本 // 播放文本
void playText(String text) { void playText(String text) => historyController.playText(text);
historyController.playText(text);
}
// 获取合并的源文本 // 获取合并的源文本
String getCombinedSourceText() { String getCombinedSourceText() => sessionItems.map((e) => e.sourceText).join('\n\n');
return sessionItems.map((e) => e.sourceText).join('\n\n');
}
// 获取合并的翻译文本 // 获取合并的翻译文本
String getCombinedTranslatedText() { String getCombinedTranslatedText() => sessionItems.map((e) => e.translatedText).join('\n\n');
return sessionItems.map((e) => e.translatedText).join('\n\n');
}
// 使用延长的超时时间重试提取摘要 // 使用延长的超时时间重试提取摘要
Future<String> retryExtractSummaryWithExtendedTimeout(String text) async { Future<String> retryExtractSummaryWithExtendedTimeout(String text) async {
// 延长超时时间(60秒) return _extractSummaryInternal(text, _extendedTimeoutDuration);
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;
}
} }
// 提取摘要 // 提取摘要
Future<String> extractSummary(String text) async { Future<String> extractSummary(String text) async {
return _extractSummaryInternal(text, _defaultTimeoutDuration);
}
// 内部摘要提取方法,避免代码重复
Future<String> _extractSummaryInternal(String text, int timeoutDuration) async {
if (text.isEmpty) {
return '无法生成摘要:文本为空';
}
isGeneratingSummary.value = true; isGeneratingSummary.value = true;
try { try {
@ -165,32 +148,44 @@ class TranslationDetailController extends GetxController {
5. 确保列表中的每个要点都简明扼要,突出重点 5. 确保列表中的每个要点都简明扼要,突出重点
'''; ''';
// 添加超时处理
String result; String result;
try { try {
// 使用Future.timeout添加超时限制 // 使用超时限制
result = await _aiService.sendMessage( result = await _aiService.sendMessage(
messages: messages, messages: messages,
systemPrompt: systemPrompt, systemPrompt: systemPrompt,
).timeout(Duration(seconds: _timeoutDuration), onTimeout: () { ).timeout(Duration(seconds: timeoutDuration), onTimeout: () {
throw TimeoutException('请求超时,请稍后重试'); throw TimeoutException('请求超时,请稍后重试');
}); });
} on TimeoutException catch (_) { } on TimeoutException catch (_) {
summaryResult.value = '生成摘要超时,请稍后重试'; final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
? '生成摘要超时,请稍后重试'
: '生成摘要超时,请检查网络连接后重试';
summaryResult.value = timeoutMessage;
return summaryResult.value; return summaryResult.value;
} }
summaryResult.value = result; // 更新结果并保存
if (result.isNotEmpty) {
// 保存摘要 summaryResult.value = result;
saveSummary(result); saveSummary(result);
}
return result; return result;
} catch (e) { } catch (e) {
summaryResult.value = '生成摘要失败: ${e.toString()}'; final errorMessage = '生成摘要失败: ${e.toString()}';
return '生成摘要失败: ${e.toString()}'; Logger.error(errorMessage);
summaryResult.value = errorMessage;
return errorMessage;
} finally { } finally {
isGeneratingSummary.value = false; isGeneratingSummary.value = false;
} }
} }
@override
void onClose() {
// 确保资源被正确释放
historyController.translationController.stopAll();
super.onClose();
}
} }

31
lib/modules/translation/controllers/translation_history_controller.dart

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
import 'dart:async'; // 添加异步支持
class TranslationHistoryController extends GetxController { class TranslationHistoryController extends GetxController {
// 引用主翻译控制器,以访问历史记录数据 // 引用主翻译控制器,以访问历史记录数据
@ -15,18 +16,29 @@ class TranslationHistoryController extends GetxController {
// 会话映射表,用于存储每个会话的所有项目 // 会话映射表,用于存储每个会话的所有项目
final Rx<Map<String, List<TranslationItem>>> sessionMap = Rx<Map<String, List<TranslationItem>>>({}); final Rx<Map<String, List<TranslationItem>>> sessionMap = Rx<Map<String, List<TranslationItem>>>({});
// 加载状态
final isLoading = true.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 监听翻译历史记录的变化 // 确保所有翻译操作都已停止
ever(translationController.translationHistory, (_) { translationController.stopAll();
// 异步加载数据,避免阻塞主线程
loadHistoryData();
}
// 异步加载历史数据
Future<void> loadHistoryData() async {
isLoading.value = true;
// 使用 compute 或 Future.microtask 在后台处理数据
await Future.microtask(() {
_groupHistoryByDate(); _groupHistoryByDate();
_groupHistoryBySession(); _groupHistoryBySession();
}); });
// 初始分组 isLoading.value = false;
_groupHistoryByDate();
_groupHistoryBySession();
} }
// 按日期对历史记录进行分组 // 按日期对历史记录进行分组
@ -44,17 +56,12 @@ class TranslationHistoryController extends GetxController {
final now = DateTime.now(); final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day); 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); final itemDate = DateTime(date.year, date.month, date.day);
if (itemDate == today) { if (itemDate == today) {
key = 'TODAY'; key = 'TODAY';
} else if (itemDate == yesterday) { }
key = '1 DAY AGO'; else {
} else if (itemDate == twoDaysAgo) {
key = '2 DAYS AGO';
} else {
key = DateFormat('yyyy-MM-dd').format(date); key = DateFormat('yyyy-MM-dd').format(date);
} }

783
lib/modules/translation/views/translation_detail_view.dart

@ -9,125 +9,157 @@ import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class TranslationDetailView extends GetView<TranslationDetailController> { class TranslationDetailView extends GetView<TranslationDetailController> {
final TranslationItem item; TranslationDetailView({Key? key}) : super(key: key);
final List<TranslationItem> sessionItems;
final TranslationHistoryController historyController; // 缓存文本,避免重复计算
final _sourceTextCache = Rx<String?>(null);
const TranslationDetailView({ final _translatedTextCache = Rx<String?>(null);
Key? key,
required this.item, // 获取源文本(带缓存)
required this.sessionItems, String _getCombinedSourceText() {
required this.historyController, if (_sourceTextCache.value == null) {
}) : super(key: key); _sourceTextCache.value = controller.getCombinedSourceText();
}
return _sourceTextCache.value!;
}
// 获取翻译文本(带缓存)
String _getCombinedTranslatedText() {
if (_translatedTextCache.value == null) {
_translatedTextCache.value = controller.getCombinedTranslatedText();
}
return _translatedTextCache.value!;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 初始化控制器
controller.initialize(item, sessionItems);
return Scaffold( return Scaffold(
backgroundColor: Color(0xFFF5F5F8), backgroundColor: Color(0xFFF5F5F8),
appBar: AppBar( appBar: _buildAppBar(),
backgroundColor: Color(0xFFF5F5F8), body: Obx(() {
elevation: 0, // 显示加载状态
leading: IconButton( if (controller.isLoading.value) {
icon: Icon( return const Center(child: CircularProgressIndicator());
Icons.arrow_back_ios, }
color: Colors.black,
size: 20.sp, return Column(
), children: [
onPressed: () => Get.back(), _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( onPressed: () => Get.back(),
'Detail', ),
style: TextStyle( 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, color: Colors.black,
fontSize: 24.sp, size: 22.sp,
fontWeight: FontWeight.bold,
), ),
onPressed: _shareContent,
), ),
centerTitle: true, ],
actions: [ );
IconButton( }
icon: Icon(
Icons.share, // 构建选项卡按钮
color: Colors.black, Widget _buildTabButtons() {
size: 22.sp, return Padding(
), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
onPressed: () { child: Row(
_shareContent();
},
),
],
),
body: Column(
children: [ children: [
// 选项卡按钮 Expanded(
Padding( child: Obx(() => _buildTabButton(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), title: '原始记录',
child: Row( isSelected: controller.selectedTabIndex.value == 0,
children: [ onTap: () => controller.switchTab(0),
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),
)),
),
],
),
), ),
SizedBox(width: 12.w),
// 内容区域
Expanded( Expanded(
child: SingleChildScrollView( child: Obx(() => _buildTabButton(
padding: EdgeInsets.symmetric(horizontal: 16.w), title: '记录摘要',
child: Obx(() { isSelected: controller.selectedTabIndex.value == 1,
return Column( onTap: () => controller.switchTab(1),
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),
],
);
}),
),
), ),
], ],
), ),
); );
} }
// 构建内容区域
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<Widget> _buildOriginalRecords() {
return controller.sessionItems.map((sessionItem) => _buildSessionItemWidget(sessionItem)).toList();
}
// 构建选项卡按钮 // 构建选项卡按钮
Widget _buildTabButton({ Widget _buildTabButton({
required String title, required String title,
@ -210,30 +242,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
), ),
), ),
SizedBox(width: 12.w), SizedBox(width: 12.w),
// 播放按钮 _buildPlayButton(() => controller.playTranslationItem(item)),
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,
),
),
),
),
], ],
), ),
), ),
@ -242,278 +251,334 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
); );
} }
// 构建播放按钮
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() { Widget _buildSummaryWidget() {
final String combinedSourceText = controller.getCombinedSourceText(); final String combinedSourceText = _getCombinedSourceText();
return Column( return Column(
children: [ children: [
// 摘要内容容器 // 摘要内容容器
Container( _buildSummaryContainer(),
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h), // 提取摘要按钮
decoration: BoxDecoration( Obx(() => _buildExtractSummaryButton(combinedSourceText)),
color: Colors.white,
borderRadius: BorderRadius.circular(16.r), // 重新生成按钮
boxShadow: [ Obx(() => _buildRegenerateSummaryButton(combinedSourceText)),
BoxShadow( ],
color: Colors.black.withAlpha(8), );
blurRadius: 8, }
offset: Offset(0, 2),
// 构建摘要容器
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) { Divider(
// 如果没有摘要,显示提示信息 height: 1,
return Padding( thickness: 0.5,
padding: EdgeInsets.all(24.r), color: Colors.grey[200],
child: Center( ),
child: Text( Padding(
'点击下方按钮生成会话摘要', padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r),
style: TextStyle( child: Row(
fontSize: 15.sp, mainAxisAlignment: MainAxisAlignment.end,
color: Colors.grey[600], children: [
fontStyle: FontStyle.italic, // 播放按钮
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, SizedBox(width: 12.w),
disabledBackgroundColor: controller.isGeneratingSummary.value // 复制按钮
? Colors.blue.withAlpha(128) Material(
: Colors.grey.withAlpha(128), color: Colors.transparent,
), child: InkWell(
child: controller.isGeneratingSummary.value onTap: _copySummaryToClipboard,
? Row( borderRadius: BorderRadius.circular(20.r),
mainAxisAlignment: MainAxisAlignment.center, splashColor: Colors.blue.withOpacity(0.2),
children: [ child: Padding(
SizedBox( padding: EdgeInsets.all(8.r),
width: 20.w, child: Icon(
height: 20.h, Icons.copy,
child: CircularProgressIndicator( color: Colors.blue,
color: Colors.white, size: 18.sp,
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,
), ),
), ),
),
)),
// 重新生成按钮
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( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, 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 { void _extractSummary(String text) async {
final result = await controller.extractSummary(text); final result = await controller.extractSummary(text);
// 检查是否超时,如果超时则显示重试选项 // 检查是否超时,如果超时则显示重试选项
if (result.contains('超时')) { if (result.contains('超时')) {
Get.dialog( _showTimeoutDialog(text);
AlertDialog( }
title: Text( }
'请求超时',
style: TextStyle( // 显示超时对话框
fontSize: 18.sp, void _showTimeoutDialog(String text) {
fontWeight: FontWeight.bold, Get.dialog(
), AlertDialog(
title: Text(
'请求超时',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
), ),
content: Text( ),
'生成摘要请求超时,可能是网络问题或服务器繁忙。', content: Text(
style: TextStyle( '生成摘要请求超时,可能是网络问题或服务器繁忙。',
fontSize: 16.sp, style: TextStyle(
), fontSize: 16.sp,
), ),
actions: [ ),
TextButton( actions: [
onPressed: () => Get.back(), TextButton(
child: Text( onPressed: () => Get.back(),
'取消', child: Text(
style: TextStyle( '取消',
fontSize: 16.sp, style: TextStyle(
color: Colors.grey[700], fontSize: 16.sp,
), color: Colors.grey[700],
), ),
), ),
TextButton( ),
onPressed: () { TextButton(
Get.back(); onPressed: () {
// 使用延长的超时时间重试 Get.back();
controller.retryExtractSummaryWithExtendedTimeout(text); // 使用延长的超时时间重试
}, controller.retryExtractSummaryWithExtendedTimeout(text);
child: Text( },
'重试', child: Text(
style: TextStyle( '重试',
fontSize: 16.sp, style: TextStyle(
color: Colors.blue, fontSize: 16.sp,
fontWeight: FontWeight.bold, 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<TranslationDetailController> {
if (controller.selectedTabIndex.value == 0) { if (controller.selectedTabIndex.value == 0) {
// 分享原始记录 // 分享原始记录
final String sourceText = controller.getCombinedSourceText(); final String sourceText = _getCombinedSourceText();
final String translatedText = controller.getCombinedTranslatedText(); final String translatedText = _getCombinedTranslatedText();
shareText = '原文:\n$sourceText\n\n译文:\n$translatedText'; shareText = '原文:\n$sourceText\n\n译文:\n$translatedText';
} else { } else {
// 分享摘要 // 分享摘要

17
lib/modules/translation/views/translation_history_view.dart

@ -59,6 +59,12 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
); );
} }
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder( return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
itemCount: controller.sortedDates.length, itemCount: controller.sortedDates.length,
@ -184,11 +190,12 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
final List<TranslationItem> sessionItems = controller.getSessionItems(item.sessionId); final List<TranslationItem> sessionItems = controller.getSessionItems(item.sessionId);
Get.to( Get.to(
() => TranslationDetailView( () => TranslationDetailView(),
item: item, arguments: {
sessionItems: sessionItems, 'item': item,
historyController: controller, 'sessionItems': sessionItems,
), 'historyController': controller,
},
binding: TranslationDetailBinding(), binding: TranslationDetailBinding(),
); );
} }

61
lib/modules/translation/views/translation_view.dart

@ -122,6 +122,9 @@ class TranslationView extends GetView<TranslationController> {
() => const TranslationHistoryView(), () => const TranslationHistoryView(),
binding: TranslationHistoryBinding(), binding: TranslationHistoryBinding(),
); );
// 返回时不自动开始翻译,只需重新设置滚动监听器
controller.setupScrollListener();
}, },
tooltip: '查看历史记录', tooltip: '查看历史记录',
), ),
@ -146,11 +149,19 @@ class TranslationView extends GetView<TranslationController> {
// 使用 currentItem 来判断是否显示当前项 // 使用 currentItem 来判断是否显示当前项
final hasCurrentItem = controller.currentItem.value != null && 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( return ListView.builder(
controller: controller.scrollController, 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), itemCount: itemCount + (hasCurrentItem ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
// Current item (being translated) // Current item (being translated)
@ -159,6 +170,7 @@ class TranslationView extends GetView<TranslationController> {
controller.currentItem.value!.sourceText, controller.currentItem.value!.sourceText,
controller.currentItem.value!.translatedText, controller.currentItem.value!.translatedText,
true, true,
isIntermediate: controller.currentItem.value!.isIntermediate,
); );
} }
@ -169,31 +181,45 @@ class TranslationView extends GetView<TranslationController> {
historyItem.translatedText, historyItem.translatedText,
false, false,
item: historyItem, item: historyItem,
isIntermediate: false, // 历史项总是显示为最终结果
); );
}, },
); );
}); });
} }
Widget _buildTranslationItemWidget(String sourceText, String translatedText, bool isCurrent, {TranslationItem? item}) { Widget _buildTranslationItemWidget(
return Container( 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), margin: EdgeInsets.only(bottom: 24.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color: Colors.grey[100], // 恢复原来的背景色
borderRadius: BorderRadius.circular(12.r), borderRadius: BorderRadius.circular(12.r),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Source text // Source text
Container( AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: double.infinity, width: double.infinity,
padding: EdgeInsets.all(16.r), padding: EdgeInsets.all(16.r),
child: Text( child: Text(
sourceText.isEmpty && isCurrent ? '等待语音输入...' : sourceText, sourceText.isEmpty && isCurrent ? '等待语音输入...' : sourceText,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, 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<TranslationController> {
Divider(height: 1, thickness: 1, color: Colors.grey[200]), Divider(height: 1, thickness: 1, color: Colors.grey[200]),
// Translated text // Translated text
Container( AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: double.infinity, width: double.infinity,
padding: EdgeInsets.all(16.r), padding: EdgeInsets.all(16.r),
child: Row( child: Row(
@ -213,7 +240,8 @@ class TranslationView extends GetView<TranslationController> {
translatedText.isEmpty && isCurrent ? '翻译结果将显示在这里...' : translatedText, translatedText.isEmpty && isCurrent ? '翻译结果将显示在这里...' : translatedText,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, 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<TranslationController> {
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.volume_up, Icons.volume_up,
color: Colors.blue, color: Colors.blue, // 恢复原来的图标颜色
size: 20.sp, size: 20.sp,
), ),
onPressed: () { onPressed: () {
@ -241,7 +269,20 @@ class TranslationView extends GetView<TranslationController> {
} }
Widget _buildControlBar() { 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( return Container(
key: controlBarKey,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,

Loading…
Cancel
Save