From d32dcb3e8efcdf019f19ef824a84be2e3bf8aeac Mon Sep 17 00:00:00 2001 From: wolfplus Date: Fri, 7 Mar 2025 17:19:17 +0000 Subject: [PATCH] before azure fultter tts --- .../com/example/deep_voice/AzureAsrHelper.kt | 353 ------------ .../com/example/deep_voice/MainActivity.kt | 241 +++----- .../azure_tts_test_controller.dart | 161 ++++++ lib/core/bindings/initial_binding.dart | 36 +- lib/core/routes/app_pages.dart | 24 +- lib/core/routes/app_routes.dart | 2 + lib/core/utils/app_lifecycle_manager.dart | 24 +- lib/data/services/azure_tts_service.dart | 539 ------------------ .../services/background_agent_service.dart | 81 ++- lib/data/services/flutter_tts_service.dart | 2 +- .../services/volcano_asr_api_service.dart | 6 +- .../services/volcano_tts_api_service.dart | 136 +++-- .../chat/controllers/chat_controller.dart | 464 +++++++-------- lib/modules/profile/views/profile_view.dart | 15 +- .../test/bindings/azure_tts_test_binding.dart | 11 + lib/modules/test/bindings/test_binding.dart | 12 +- ...er.dart => azure_asr_test_controller.dart} | 2 +- .../azure_tts_test_controller.dart | 315 ++++++++++ ....dart => volcano_tts_test_controller.dart} | 2 +- ...est_view.dart => azure_asr_test_view.dart} | 6 +- .../test/views/azure_tts_test_view.dart | 280 +++++++++ ...t_view.dart => volcano_tts_test_view.dart} | 8 +- .../controllers/translation_controller.dart | 2 +- pubspec.yaml | 1 + 24 files changed, 1291 insertions(+), 1432 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt create mode 100644 lib/controllers/azure_tts_test_controller.dart delete mode 100644 lib/data/services/azure_tts_service.dart create mode 100644 lib/modules/test/bindings/azure_tts_test_binding.dart rename lib/modules/test/controllers/{asr_test_controller.dart => azure_asr_test_controller.dart} (98%) create mode 100644 lib/modules/test/controllers/azure_tts_test_controller.dart rename lib/modules/test/controllers/{tts_test_controller.dart => volcano_tts_test_controller.dart} (99%) rename lib/modules/test/views/{asr_test_view.dart => azure_asr_test_view.dart} (98%) create mode 100644 lib/modules/test/views/azure_tts_test_view.dart rename lib/modules/test/views/{tts_test_view.dart => volcano_tts_test_view.dart} (97%) diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt deleted file mode 100644 index ef2ea95a1..000000000 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt +++ /dev/null @@ -1,353 +0,0 @@ -package com.example.deep_voice - -import android.util.Log -import com.microsoft.cognitiveservices.speech.* -import com.microsoft.cognitiveservices.speech.audio.* -import com.microsoft.cognitiveservices.speech.util.EventHandler -import java.util.concurrent.ExecutionException -import java.util.concurrent.Semaphore - -/** - * Azure 语音识别助手类 - * 提供 Azure 语音识别服务的封装,支持一次性识别和连续识别 - */ -class AzureAsrHelper { - private var recognizer: SpeechRecognizer? = null - private var audioConfig: AudioConfig? = null - private var speechConfig: SpeechConfig? = null - private val TAG = "AzureAsrHelper" - private var isContinuousRecognitionActive = false - - // 用于同步的信号量 - private val stopRecognitionSemaphore = Semaphore(0) - - // 存储最后一次初始化的参数 - private var lastSubscriptionKey: String = "" - private var lastServiceRegion: String = "" - private var lastLanguage: String = "zh-CN" - - /** - * 初始化 Azure 语音识别服务 - * @param subscriptionKey Azure 语音服务的订阅密钥 - * @param serviceRegion Azure 语音服务的区域 - * @param language 识别语言,默认为中文 - * @return 是否初始化成功 - */ - fun initialize( - subscriptionKey: String, - serviceRegion: String, - language: String = "zh-CN" - ): Boolean { - try { - // 保存初始化参数,以便重置时使用 - lastSubscriptionKey = subscriptionKey - lastServiceRegion = serviceRegion - lastLanguage = language - - // 释放之前的资源 - dispose() - - // 创建语音配置 - speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion) - speechConfig?.speechRecognitionLanguage = language - - // 创建音频配置,使用默认麦克风 - audioConfig = AudioConfig.fromDefaultMicrophoneInput() - - // 创建识别器 - recognizer = SpeechRecognizer(speechConfig, audioConfig) - - Log.d(TAG, "Azure 语音识别服务初始化成功") - return true - } catch (e: Exception) { - Log.e(TAG, "Azure 语音识别服务初始化失败: ${e.message}") - e.printStackTrace() - return false - } - } - - /** - * 重置识别器 - * 在切换识别模式前调用,确保资源被正确释放 - * @return 是否成功重置 - */ - fun resetRecognizer(): Boolean { - try { - Log.d(TAG, "开始重置 Azure 语音识别器") - - // 如果连续识别正在进行,先停止它 - if (isContinuousRecognitionActive) { - try { - recognizer?.stopContinuousRecognitionAsync()?.get() - isContinuousRecognitionActive = false - } catch (e: Exception) { - Log.e(TAG, "停止连续识别失败: ${e.message}") - } - } - - // 关闭并释放当前的识别器 - recognizer?.close() - recognizer = null - - audioConfig?.close() - audioConfig = null - - speechConfig?.close() - speechConfig = null - - // 使用上次的参数重新创建识别器 - if (lastSubscriptionKey.isNotEmpty() && lastServiceRegion.isNotEmpty()) { - speechConfig = SpeechConfig.fromSubscription(lastSubscriptionKey, lastServiceRegion) - speechConfig?.speechRecognitionLanguage = lastLanguage - - audioConfig = AudioConfig.fromDefaultMicrophoneInput() - - recognizer = SpeechRecognizer(speechConfig, audioConfig) - - Log.d(TAG, "Azure 语音识别器重置成功") - return true - } else { - Log.e(TAG, "无法重置 Azure 语音识别器:缺少初始化参数") - return false - } - } catch (e: Exception) { - Log.e(TAG, "重置 Azure 语音识别器失败: ${e.message}") - e.printStackTrace() - return false - } - } - - /** - * 执行一次性语音识别 - * @param callback 识别结果回调 - */ - fun recognizeOnce(callback: RecognizeCallback) { - if (recognizer == null) { - callback.onError("SpeechRecognizer 未初始化") - return - } - - try { - // 使用同步方式调用,避免 CompletableFuture 的兼容性问题 - val result = recognizer?.recognizeOnceAsync()?.get() - - if (result != null) { - when (result.reason) { - ResultReason.RecognizedSpeech -> { - callback.onResult(result.text) - } - ResultReason.NoMatch -> { - callback.onError("未能识别语音") - } - ResultReason.Canceled -> { - val cancellation = CancellationDetails.fromResult(result) - callback.onError("识别被取消,原因: ${cancellation.reason}, 错误详情: ${cancellation.errorDetails}") - } - else -> { - callback.onError("识别失败,原因: ${result.reason}") - } - } - } else { - callback.onError("识别结果为空") - } - } catch (e: Exception) { - when (e) { - is InterruptedException, is ExecutionException -> { - Log.e(TAG, "识别异常: ${e.message}") - callback.onError("识别异常: ${e.message}") - } - else -> { - Log.e(TAG, "未知异常: ${e.message}") - callback.onError("未知异常: ${e.message}") - } - } - } - } - - /** - * 开始连续语音识别 - * @param callback 连续识别结果回调 - * @return 是否成功启动连续识别 - */ - fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - if (recognizer == null) { - callback.onError("SpeechRecognizer 未初始化") - return false - } - - if (isContinuousRecognitionActive) { - callback.onError("连续识别已经在进行中") - return false - } - - try { - // 设置识别事件处理 - recognizer?.let { recognizer -> - // 设置识别事件处理(最终结果) - recognizer.recognized.addEventListener( - EventHandler { _, event -> - if (event.result.reason == ResultReason.RecognizedSpeech) { - callback.onResult(event.result.text) - } else if (event.result.reason == ResultReason.NoMatch) { - callback.onNoMatch() - } - } - ) - - // 设置识别中事件处理(实时反馈) - recognizer.recognizing.addEventListener( - EventHandler { _, event -> - if (event.result.reason == ResultReason.RecognizingSpeech) { - callback.onRecognizing(event.result.text) - } - } - ) - - // 设置会话开始事件处理 - recognizer.sessionStarted.addEventListener( - EventHandler { _, _ -> - callback.onSessionStarted() - } - ) - - // 设置会话结束事件处理 - recognizer.sessionStopped.addEventListener( - EventHandler { _, _ -> - isContinuousRecognitionActive = false - callback.onSessionStopped() - stopRecognitionSemaphore.release() - } - ) - - // 设置取消事件处理 - recognizer.canceled.addEventListener( - EventHandler { _, event -> - val reason = event.reason - val errorDetails = if (reason == CancellationReason.Error) event.errorDetails else "" - callback.onCanceled(reason.toString(), errorDetails) - isContinuousRecognitionActive = false - stopRecognitionSemaphore.release() - } - ) - - // 开始连续识别 - recognizer.startContinuousRecognitionAsync().get() - isContinuousRecognitionActive = true - Log.d(TAG, "连续识别已开始kt") - return true - } - - return false - } catch (e: Exception) { - Log.e(TAG, "开始连续识别失败: ${e.message}") - e.printStackTrace() - callback.onError("开始连续识别失败: ${e.message}") - isContinuousRecognitionActive = false - return false - } - } - - /** - * 停止连续语音识别 - * @param callback 连续识别结果回调 - * @return 是否成功停止连续识别 - */ - fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - if (recognizer == null) { - Log.w(TAG, "SpeechRecognizer 未初始化,无需停止") - callback.onSessionStopped() // 通知已停止 - return true // 未初始化,视为已停止 - } - - if (!isContinuousRecognitionActive) { - Log.w(TAG, "连续识别未在进行中,无需停止") - callback.onSessionStopped() // 通知已停止 - return true // 已经不在进行中,视为成功 - } - - try { - // 停止连续识别 - recognizer?.stopContinuousRecognitionAsync() - - // 等待停止完成 - stopRecognitionSemaphore.acquire() - - isContinuousRecognitionActive = false - Log.d(TAG, "连续识别已停止kt") - callback.onSessionStopped() - return true - } catch (e: Exception) { - Log.e(TAG, "停止连续识别失败: ${e.message}") - e.printStackTrace() - callback.onError("停止连续识别失败: ${e.message}") - return false - } - } - - /** - * 检查连续识别是否活跃 - * @return 连续识别是否活跃 - */ - fun isContinuousRecognitionActive(): Boolean { - return isContinuousRecognitionActive - } - - /** - * 设置识别语言 - * @param language 语言代码,例如 "zh-CN", "en-US" - */ - fun setRecognitionLanguage(language: String) { - speechConfig?.speechRecognitionLanguage = language - } - - /** - * 释放资源 - */ - fun dispose() { - try { - if (isContinuousRecognitionActive) { - try { - recognizer?.stopContinuousRecognitionAsync()?.get() - } catch (e: Exception) { - Log.e(TAG, "停止连续识别失败: ${e.message}") - } - isContinuousRecognitionActive = false - } - - recognizer?.close() - recognizer = null - - audioConfig?.close() - audioConfig = null - - speechConfig?.close() - speechConfig = null - - Log.d(TAG, "Azure 语音识别资源已释放") - } catch (e: Exception) { - Log.e(TAG, "释放资源失败: ${e.message}") - e.printStackTrace() - } - } - - /** - * 一次性识别回调接口 - */ - interface RecognizeCallback { - fun onResult(result: String) - fun onError(error: String) - } - - /** - * 连续识别回调接口 - */ - interface ContinuousRecognizeCallback { - fun onResult(text: String) - fun onRecognizing(recognizing: String) - fun onSessionStarted() - fun onSessionStopped() - fun onCanceled(reason: String, errorDetails: String) - fun onNoMatch() - fun onError(error: String) - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index 7c2c4ee49..ae899a3d9 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -16,22 +16,32 @@ import io.flutter.plugins.GeneratedPluginRegistrant class MainActivity: AudioServiceActivity() { - private val AZURE_ASR_CHANNEL = "com.example.deep_voice/azure_asr" - private val AZURE_ASR_EVENT_CHANNEL = "com.example.deep_voice/azure_asr_events" private val AUDIO_PROCESSING_CHANNEL = "com.deep_voice.audio_processing" + // 移除 Azure TTS 相关通道 + // private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts" + // private val AZURE_TTS_EVENT_CHANNEL = "com.deep_voice.azure_tts_events" private val TAG = "MainActivity" - private lateinit var azureAsrHelper: AzureAsrHelper + // 移除 Azure ASR 助手 + // private lateinit var azureAsrHelper: AzureAsrHelper // 暂时注释掉AudioProcessingPlugin的实例化 // private val audioProcessingPlugin: AudioProcessingPlugin by lazy { AudioProcessingPlugin() } - private var azureEventSink: EventChannel.EventSink? = null + // 移除 Azure TTS 助手 + // private lateinit var azureTtsHelper: AzureTtsHelper + + // 移除 Azure 事件接收器 + // private var azureAsrEventSink: EventChannel.EventSink? = null + // private var azureTtsEventSink: EventChannel.EventSink? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // 初始化 Azure ASR 助手 - azureAsrHelper = AzureAsrHelper() + // 移除 Azure ASR 助手初始化 + // azureAsrHelper = AzureAsrHelper() + + // 移除 Azure TTS 助手初始化 + // azureTtsHelper = AzureTtsHelper(applicationContext) // 暂时注释掉AudioProcessingPlugin的初始化 // AudioProcessingPlugin.setApplicationContext(applicationContext) @@ -86,206 +96,95 @@ class MainActivity: AudioServiceActivity() { } } - // 设置 Azure ASR 方法通道 - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result -> + // 移除 Azure ASR 方法通道 + + // 移除 Azure TTS 方法通道和相关代码 + /* + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result -> when (call.method) { "initialize" -> { val subscriptionKey = call.argument("subscriptionKey") - val serviceRegion = call.argument("serviceRegion") - val language = call.argument("language") ?: "zh-CN" + val region = call.argument("region") - if (subscriptionKey == null || serviceRegion == null) { - result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null) + if (subscriptionKey == null || region == null) { + result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null) return@setMethodCallHandler } - try { - Log.d(TAG, "初始化 Azure 语音识别服务,区域: $serviceRegion") - val success = azureAsrHelper.initialize(subscriptionKey, serviceRegion, language) - Log.d(TAG, "Azure 语音识别服务初始化${if (success) "成功" else "失败"}") - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "初始化 Azure 语音识别服务失败", e) - result.error("INITIALIZATION_ERROR", e.message, null) - } - } - "recognizeOnce" -> { - try { - azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback { - override fun onResult(text: String) { - result.success(text) - } - - override fun onError(error: String) { - result.error("RECOGNITION_ERROR", error, null) - } - }) - } catch (e: Exception) { - result.error("RECOGNITION_ERROR", e.message, null) - } - } - "startContinuousRecognition" -> { - try { - if (azureAsrHelper.isContinuousRecognitionActive()) { - result.error("ALREADY_ACTIVE", "连续识别已经在进行中", null) - return@setMethodCallHandler - } - - val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String) { - sendAzureEvent(mapOf( - "eventType" to "finalResult", - "text" to text - )) - } - - override fun onRecognizing(recognizing: String) { - sendAzureEvent(mapOf( - "eventType" to "recognizing", - "text" to recognizing - )) - } - - override fun onSessionStarted() { - sendAzureEvent(mapOf( - "eventType" to "sessionStarted" - )) - } - - override fun onSessionStopped() { - sendAzureEvent(mapOf( - "eventType" to "sessionStopped" - )) - } - - override fun onCanceled(reason: String, errorDetails: String) { - sendAzureEvent(mapOf( - "eventType" to "canceled", - "reason" to reason, - "errorDetails" to errorDetails - )) - } - - override fun onNoMatch() { - sendAzureEvent(mapOf( - "eventType" to "noMatch" - )) - } - - override fun onError(error: String) { - sendAzureEvent(mapOf( - "eventType" to "error", - "error" to error - )) - } - }) - result.success(success) - } catch (e: Exception) { - result.error("RECOGNITION_ERROR", e.message, null) - } - } - "stopContinuousRecognition" -> { - try { - val success = azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { - override fun onResult(text: String) { - // 不需要处理 - } - - override fun onRecognizing(recognizing: String) { - // 不需要处理 - } - - override fun onSessionStarted() { - // 不需要处理 - } - - override fun onSessionStopped() { - sendAzureEvent(mapOf( - "eventType" to "sessionStopped" - )) - } - - override fun onCanceled(reason: String, errorDetails: String) { - // 不需要处理 - } - - override fun onNoMatch() { - // 不需要处理 - } - - override fun onError(error: String) { - // 记录错误但不中断流程,返回成功 - Log.w(TAG, "停止语音识别时出现警告: $error") - sendAzureEvent(mapOf( - "eventType" to "warning", - "message" to error - )) - // 不调用 result.error,让流程继续 - } - }) - result.success(success) - } catch (e: Exception) { - // 记录异常但返回成功,因为即使出错,识别也已经不再活动 - Log.e(TAG, "停止语音识别时出现异常: ${e.message}") - result.success(true) - } + val success = azureTtsHelper.initialize(subscriptionKey, region) + result.success(success) } - "resetRecognizer" -> { - try { - val success = azureAsrHelper.resetRecognizer() - result.success(success) - } catch (e: Exception) { - result.error("RESET_ERROR", "重置识别器失败: ${e.message}", null) + "setVoice" -> { + val voiceName = call.argument("voiceName") + + if (voiceName == null) { + result.error("INVALID_ARGUMENTS", "语音名称不能为空", null) + return@setMethodCallHandler } + + val success = azureTtsHelper.setVoice(voiceName) + result.success(success) } - "setRecognitionLanguage" -> { - try { - val language = call.argument("language") ?: "zh-CN" - azureAsrHelper.setRecognitionLanguage(language) - result.success(true) - } catch (e: Exception) { - result.error("LANGUAGE_ERROR", e.message, null) + "synthesizeToSpeaker" -> { + val text = call.argument("text") + + if (text == null) { + result.error("INVALID_ARGUMENTS", "文本不能为空", null) + return@setMethodCallHandler } + + azureTtsHelper.synthesizeToSpeaker(text, result) } - "dispose" -> { - try { - azureAsrHelper.dispose() - result.success(true) - } catch (e: Exception) { - result.error("DISPOSE_ERROR", e.message, null) + "synthesizeToFile" -> { + val text = call.argument("text") + val filePath = call.argument("filePath") + + if (text == null || filePath == null) { + result.error("INVALID_ARGUMENTS", "文本和文件路径不能为空", null) + return@setMethodCallHandler } + + azureTtsHelper.synthesizeToFile(text, filePath, result) } else -> { result.notImplemented() } } } + */ - + // 移除 Azure ASR 事件通道 - // 设置 Azure ASR 事件通道 - EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler( + // 移除 Azure TTS 事件通道 + /* + EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_EVENT_CHANNEL).setStreamHandler( object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - azureEventSink = events + azureTtsEventSink = events + azureTtsHelper.setEventSink(events) } override fun onCancel(arguments: Any?) { - azureEventSink = null + azureTtsEventSink = null + azureTtsHelper.setEventSink(null) } } ) + */ } + // 移除 ASR 事件发送方法 - private fun sendAzureEvent(event: Map) { - runOnUiThread { - azureEventSink?.success(event) - } + // 移除 TTS 事件发送方法 + /* + fun sendTtsEvent(event: Map) { + azureTtsEventSink?.success(event) } + */ override fun onDestroy() { - azureAsrHelper.dispose() + // 移除 Azure TTS 资源释放 + // azureTtsHelper.dispose() super.onDestroy() } } diff --git a/lib/controllers/azure_tts_test_controller.dart b/lib/controllers/azure_tts_test_controller.dart new file mode 100644 index 000000000..a13b122f4 --- /dev/null +++ b/lib/controllers/azure_tts_test_controller.dart @@ -0,0 +1,161 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import '../data/services/azure_tts_service.dart'; + +class AzureTtsTestController extends GetxController { + final AzureTtsService _ttsService = Get.find(); + final Logger _logger = Logger(); + + // 状态变量 + final RxBool isInitialized = false.obs; + final RxBool isProcessing = false.obs; + final RxString statusMessage = ''.obs; + final RxString lastFilePath = ''.obs; + + @override + void onInit() { + super.onInit(); + + // 监听TTS状态 + _ttsService.ttsStatus.listen((status) { + final statusStr = status['status'] as String?; + + switch (statusStr) { + case 'started': + isProcessing.value = true; + statusMessage.value = '正在合成语音...'; + break; + case 'completed': + isProcessing.value = false; + statusMessage.value = '语音合成完成'; + + if (status.containsKey('filePath')) { + lastFilePath.value = status['filePath'] as String; + } + break; + case 'error': + isProcessing.value = false; + statusMessage.value = '语音合成失败: ${status['message'] ?? '未知错误'}'; + break; + default: + statusMessage.value = '未知状态: $statusStr'; + break; + } + }); + } + + /// 初始化Azure TTS服务 + Future initialize({ + required String subscriptionKey, + required String region, + }) async { + try { + isProcessing.value = true; + statusMessage.value = '正在初始化Azure TTS...'; + + await _ttsService.initialize( + subscriptionKey: subscriptionKey, + region: region, + ); + + isInitialized.value = _ttsService.isInitialized; + + if (isInitialized.value) { + statusMessage.value = 'Azure TTS初始化成功'; + } else { + statusMessage.value = 'Azure TTS初始化失败'; + } + } catch (e) { + isInitialized.value = false; + statusMessage.value = '初始化异常: $e'; + _logger.e('初始化异常: $e'); + } finally { + isProcessing.value = false; + } + } + + /// 设置语音 + Future setVoice(String voiceName) async { + if (!isInitialized.value) { + statusMessage.value = 'Azure TTS未初始化,请先初始化'; + return; + } + + try { + isProcessing.value = true; + statusMessage.value = '正在设置语音...'; + + final success = await _ttsService.setVoice(voiceName); + + if (success) { + statusMessage.value = '语音设置成功: $voiceName'; + } else { + statusMessage.value = '语音设置失败'; + } + } catch (e) { + statusMessage.value = '设置语音异常: $e'; + _logger.e('设置语音异常: $e'); + } finally { + isProcessing.value = false; + } + } + + /// 合成语音到扬声器 + Future speakText(String text) async { + if (!isInitialized.value) { + statusMessage.value = 'Azure TTS未初始化,请先初始化'; + return; + } + + if (text.isEmpty) { + statusMessage.value = '文本不能为空'; + return; + } + + try { + isProcessing.value = true; + statusMessage.value = '正在合成语音...'; + + final success = await _ttsService.synthesizeToSpeaker(text); + + if (!success) { + statusMessage.value = '语音合成失败'; + } + } catch (e) { + statusMessage.value = '语音合成异常: $e'; + _logger.e('语音合成异常: $e'); + isProcessing.value = false; + } + } + + /// 合成语音到文件 + Future synthesizeToFile(String text, {String? fileName}) async { + if (!isInitialized.value) { + statusMessage.value = 'Azure TTS未初始化,请先初始化'; + return; + } + + if (text.isEmpty) { + statusMessage.value = '文本不能为空'; + return; + } + + try { + isProcessing.value = true; + statusMessage.value = '正在合成语音到文件...'; + + final filePath = await _ttsService.synthesizeToFile(text, fileName: fileName); + + if (filePath != null) { + lastFilePath.value = filePath; + } else { + statusMessage.value = '语音合成到文件失败'; + isProcessing.value = false; + } + } catch (e) { + statusMessage.value = '语音合成到文件异常: $e'; + _logger.e('语音合成到文件异常: $e'); + isProcessing.value = false; + } + } +} \ No newline at end of file diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 57768f9d0..48e390b5e 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -5,7 +5,8 @@ import '../../data/services/notification_service.dart'; import '../../data/services/volcano_ai_service.dart'; import '../../data/services/volcano_tts_api_service.dart'; import '../../data/services/volcano_translation_service.dart'; -import '../../data/services/azure_asr_service.dart'; +import '../../data/services/azure_tts_service.dart'; + import '../../data/services/background_agent_service.dart'; import '../../data/services/chat_history_service.dart'; import '../../data/services/flutter_tts_service.dart'; @@ -53,6 +54,9 @@ class InitialBinding extends Bindings { return service; }, fenix: true); + // Azure TTS 服务 + Get.lazyPut(() => AzureTtsService(), fenix: true); + // 语音识别服务 // Get.lazyPut(() { // final service = AzureAsrService(); @@ -95,7 +99,7 @@ class InitialBinding extends Bindings { }); // 等待一段时间,确保通知权限请求完成 - await Future.delayed(Duration(seconds: 1)); + // await Future.delayed(Duration(seconds: 1)); // 然后初始化音频服务 await audioService.init().then((_) { @@ -106,21 +110,21 @@ class InitialBinding extends Bindings { } } - // 初始化 Azure 语音识别服务 - void _initializeAzureService(AzureAsrService azureAsrService) async { - try { - // 直接调用初始化方法,服务内部会读取环境变量配置 - final success = await azureAsrService.initialize(); + // // 初始化 Azure 语音识别服务 + // void _initializeAzureService(AzureAsrService azureAsrService) async { + // try { + // // 直接调用初始化方法,服务内部会读取环境变量配置 + // final success = await azureAsrService.initialize(); - if (success) { - Logger.info('Azure 语音识别服务初始化成功'); - } else { - Logger.error('Azure 语音识别服务初始化失败'); - } - } catch (e) { - Logger.error('初始化 Azure 语音识别服务失败', e); - } - } + // if (success) { + // Logger.info('Azure 语音识别服务初始化成功'); + // } else { + // Logger.error('Azure 语音识别服务初始化失败'); + // } + // } catch (e) { + // Logger.error('初始化 Azure 语音识别服务失败', e); + // } + // } // 初始化火山翻译服务 void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async { diff --git a/lib/core/routes/app_pages.dart b/lib/core/routes/app_pages.dart index ea881e161..edea2c51c 100644 --- a/lib/core/routes/app_pages.dart +++ b/lib/core/routes/app_pages.dart @@ -7,13 +7,15 @@ import '../../modules/profile/views/profile_view.dart'; import '../../modules/profile/bindings/profile_binding.dart'; import '../../modules/chat/bindings/chat_binding.dart'; import '../../modules/chat/views/chat_view.dart'; -import '../../modules/test/views/tts_test_view.dart'; -import '../../modules/test/views/asr_test_view.dart'; +import '../../modules/test/views/volcano_tts_test_view.dart'; +import '../../modules/test/views/azure_asr_test_view.dart'; +import '../../modules/test/views/azure_tts_test_view.dart'; import '../../modules/test/views/volcano_asr_test_view.dart'; import '../../modules/test/views/flutter_tts_test_view.dart'; import '../../modules/test/views/flutter_asr_test_view.dart'; import '../../modules/test/bindings/test_binding.dart'; import '../../modules/test/bindings/volcano_asr_test_binding.dart'; +import '../../modules/test/bindings/azure_tts_test_binding.dart'; import '../../modules/test/bindings/flutter_tts_test_binding.dart'; import '../../modules/test/bindings/flutter_asr_test_binding.dart'; import '../../modules/translation/views/translation_view.dart'; @@ -51,13 +53,23 @@ abstract class AppPages { ), GetPage( name: Routes.TTS_TEST, - page: () => TtsTestView(), - binding: TtsTestBinding(), + page: () => VolcanoTtsTestView(), + binding: VolcanoTtsTestBinding(), ), GetPage( name: Routes.ASR_TEST, - page: () => AsrTestView(), - binding: AsrTestBinding(), + page: () => const AzureAsrTestView(), + binding: AzureAsrTestBinding(), + ), + GetPage( + name: Routes.AZURE_ASR_TEST, + page: () => const AzureAsrTestView(), + binding: AzureAsrTestBinding(), + ), + GetPage( + name: Routes.AZURE_TTS_TEST, + page: () => const AzureTtsTestView(), + binding: AzureTtsTestBinding(), ), GetPage( name: Routes.VOLCANO_ASR_TEST, diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index 9173470d6..ae5e19d87 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -7,6 +7,8 @@ abstract class Routes { static const speechDemo = '/speech_demo'; static const TTS_TEST = '/tts_test'; static const ASR_TEST = '/asr_test'; + static const AZURE_ASR_TEST = '/azure_asr_test'; + static const AZURE_TTS_TEST = '/azure_tts_test'; static const VOLCANO_ASR_TEST = '/volcano_asr_test'; static const translation = '/translation'; static const translationHistory = '/translation_history'; diff --git a/lib/core/utils/app_lifecycle_manager.dart b/lib/core/utils/app_lifecycle_manager.dart index ecb3a14a6..483cc53d2 100644 --- a/lib/core/utils/app_lifecycle_manager.dart +++ b/lib/core/utils/app_lifecycle_manager.dart @@ -60,12 +60,12 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { // 如果正在录音,停止录音 final asrService = Get.find(); final ttsService = Get.find(); - if (asrService.isRecording) { - Logger.info('应用进入后台,停止语音识别'); - asrService.stopRecognition(); - ttsService.stop(); - ttsService.disconnect(); - } + // if (asrService.isRecording) { + // Logger.info('应用进入后台,停止语音识别'); + // asrService.stopRecognition(); + // ttsService.stop(); + // ttsService.disconnect(); + // } } /// 处理应用进入后台 @@ -76,13 +76,13 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { final asrService = Get.find(); final ttsService = Get.find(); // 如果正在录音,停止录音 - if (asrService.isRecording) { - Logger.info('应用进入后台,停止语音识别'); - asrService.stopRecognition(); - ttsService.stop(); - ttsService.disconnect(); + // if (asrService.isRecording) { + // Logger.info('应用进入后台,停止语音识别'); + // asrService.stopRecognition(); + // ttsService.stop(); + // ttsService.disconnect(); - } + // } } @override diff --git a/lib/data/services/azure_tts_service.dart b/lib/data/services/azure_tts_service.dart deleted file mode 100644 index 05ca1616b..000000000 --- a/lib/data/services/azure_tts_service.dart +++ /dev/null @@ -1,539 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import '../../core/utils/logger.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:path_provider/path_provider.dart'; - -/// 语音合成事件类型 -enum SynthesisEventType { - /// 合成开始 - synthesisStarted, - - /// 合成进度更新 - synthesisProgress, - - /// 合成完成 - synthesisCompleted, - - /// 合成取消 - synthesisCanceled, - - /// 合成错误 - error, -} - -/// 语音合成事件 -class SynthesisEvent { - /// 事件类型 - final SynthesisEventType type; - - /// 音频文件路径(仅在 synthesisCompleted 类型中有效) - final String? audioFilePath; - - /// 进度百分比(仅在 synthesisProgress 类型中有效) - final int? progressPercent; - - /// 错误信息(仅在 error 和 synthesisCanceled 类型中有效) - final String? error; - - /// 取消原因(仅在 synthesisCanceled 类型中有效) - final String? reason; - - /// 错误详情(仅在 synthesisCanceled 类型中有效) - final String? errorDetails; - - SynthesisEvent({ - required this.type, - this.audioFilePath, - this.progressPercent, - this.error, - this.reason, - this.errorDetails, - }); - - /// 从原生事件映射创建合成事件 - factory SynthesisEvent.fromMap(Map map) { - final eventType = map['eventType'] as String; - - SynthesisEventType type; - switch (eventType) { - case 'synthesisStarted': - type = SynthesisEventType.synthesisStarted; - break; - case 'synthesisProgress': - type = SynthesisEventType.synthesisProgress; - break; - case 'synthesisCompleted': - type = SynthesisEventType.synthesisCompleted; - break; - case 'synthesisCanceled': - type = SynthesisEventType.synthesisCanceled; - break; - case 'error': - type = SynthesisEventType.error; - break; - default: - throw ArgumentError('未知事件类型: $eventType'); - } - - return SynthesisEvent( - type: type, - audioFilePath: map['audioFilePath'] as String?, - progressPercent: map['progressPercent'] as int?, - error: map['error'] as String?, - reason: map['reason'] as String?, - errorDetails: map['errorDetails'] as String?, - ); - } - - @override - String toString() { - return 'SynthesisEvent{type: $type, audioFilePath: $audioFilePath, progressPercent: $progressPercent, error: $error, reason: $reason, errorDetails: $errorDetails}'; - } -} - -/// TTS 状态 -enum TtsState { - /// 空闲 - idle, - - /// 合成中 - synthesizing, - - /// 播放中 - playing, - - /// 已停止 - stopped, -} - -/// Azure 文本转语音服务 -class AzureTtsService extends GetxService { - static AzureTtsService get to => Get.find(); - - // 方法通道 - static const MethodChannel _methodChannel = MethodChannel('com.example.deep_voice/azure_tts'); - - // 事件通道 - static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/azure_tts_events'); - - // 是否已初始化 - bool _isInitialized = false; - bool get isInitialized => _isInitialized; - - // 当前TTS状态 - final Rx _ttsState = TtsState.idle.obs; - TtsState get ttsState => _ttsState.value; - - // 合成相关 - StreamController? _eventStreamController; - StreamSubscription? _eventSubscription; - - // 公开的事件流 - Stream? _synthesisStream; - Stream? get synthesisStream => _synthesisStream; - - // 当前语音名称 - String _currentVoice = 'zh-CN-XiaoxiaoNeural'; - String get currentVoice => _currentVoice; - set currentVoice(String voice) { - _currentVoice = voice; - _setVoice(voice); - } - - // 语速 (-100 to 100) - int _rate = 0; - int get rate => _rate; - set rate(int value) { - if (value < -100 || value > 100) { - throw ArgumentError('语速必须在 -100 到 100 之间'); - } - _rate = value; - _setSpeechRate(value); - } - - // 音调 (-100 to 100) - int _pitch = 0; - int get pitch => _pitch; - set pitch(int value) { - if (value < -100 || value > 100) { - throw ArgumentError('音调必须在 -100 到 100 之间'); - } - _pitch = value; - _setPitch(value); - } - - // 音量 (0 to 100) - int _volume = 100; - int get volume => _volume; - set volume(int value) { - if (value < 0 || value > 100) { - throw ArgumentError('音量必须在 0 到 100 之间'); - } - _volume = value; - _setVolume(value); - } - - // 可用的语音列表 - final RxList> _voices = >[].obs; - List> get voices => _voices; - - /// 初始化 Azure 文本转语音服务 - /// - /// [subscriptionKey] Azure 语音服务的订阅密钥 - /// [serviceRegion] Azure 语音服务的区域 - /// [defaultVoice] 默认语音名称,默认为中文女声 - Future initialize({ - String? subscriptionKey, - String? serviceRegion, - String? defaultVoice, - }) async { - if (_isInitialized) return true; - - try { - // 从环境变量中读取配置,如果没有传入参数 - final key = subscriptionKey ?? dotenv.env['AZURE_TTS_SUBSCRIPTION_KEY'] ?? ''; - final region = serviceRegion ?? dotenv.env['AZURE_TTS_SERVICE_REGION'] ?? 'eastasia'; - final voice = defaultVoice ?? dotenv.env['AZURE_TTS_DEFAULT_VOICE'] ?? 'zh-CN-XiaoxiaoNeural'; - - // 检查配置是否完整 - if (key.isEmpty) { - Logger.warning('Azure 文本转语音服务的订阅密钥未设置,请检查 .env 文件'); - return false; - } - - // 设置事件监听 - _setupEventChannel(); - - final result = await _methodChannel.invokeMethod( - 'initialize', - { - 'subscriptionKey': key, - 'serviceRegion': region, - 'defaultVoice': voice, - }, - ); - - _isInitialized = result ?? false; - - if (_isInitialized) { - Logger.info('Azure 文本转语音服务初始化成功'); - _currentVoice = voice; - - // 获取可用语音列表 - await getAvailableVoices(); - } else { - Logger.error('Azure 文本转语音服务初始化失败'); - } - - return _isInitialized; - } catch (e) { - Logger.error('初始化 Azure 文本转语音服务失败', e); - _isInitialized = false; - return false; - } - } - - /// 设置事件通道监听 - void _setupEventChannel() { - // 创建事件流控制器 - _eventStreamController = StreamController.broadcast(); - - // 设置事件监听 - _eventSubscription = _eventChannel - .receiveBroadcastStream() - .map((dynamic event) => SynthesisEvent.fromMap(event)) - .listen( - (event) { - _handleSynthesisEvent(event); - _eventStreamController?.add(event); - }, - onError: (error) { - Logger.error('Azure 文本转语音事件流错误', error); - _eventStreamController?.addError(error); - }, - ); - - // 设置公开的流 - _synthesisStream = _eventStreamController?.stream; - } - - /// 处理合成事件 - void _handleSynthesisEvent(SynthesisEvent event) { - switch (event.type) { - case SynthesisEventType.synthesisStarted: - _ttsState.value = TtsState.synthesizing; - break; - case SynthesisEventType.synthesisCompleted: - _ttsState.value = TtsState.idle; - break; - case SynthesisEventType.synthesisCanceled: - case SynthesisEventType.error: - _ttsState.value = TtsState.idle; - break; - default: - // 其他事件类型不需要特殊处理 - break; - } - } - - /// 获取可用的语音列表 - Future>> getAvailableVoices() async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod>('getAvailableVoices'); - - if (result != null) { - final voicesList = result.map((item) { - final map = Map.from(item as Map); - return { - 'name': map['name'] as String, - 'locale': map['locale'] as String, - 'gender': map['gender'] as String, - 'displayName': map['displayName'] as String, - }; - }).toList(); - - _voices.value = List>.from(voicesList); - return _voices; - } - - return []; - } catch (e) { - Logger.error('获取可用语音列表失败', e); - return []; - } - } - - /// 设置语音 - Future _setVoice(String voiceName) async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod( - 'setVoice', - {'voiceName': voiceName}, - ); - return result ?? false; - } catch (e) { - Logger.error('设置语音失败', e); - return false; - } - } - - /// 设置语速 - Future _setSpeechRate(int rate) async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod( - 'setSpeechRate', - {'rate': rate}, - ); - return result ?? false; - } catch (e) { - Logger.error('设置语速失败', e); - return false; - } - } - - /// 设置音调 - Future _setPitch(int pitch) async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod( - 'setPitch', - {'pitch': pitch}, - ); - return result ?? false; - } catch (e) { - Logger.error('设置音调失败', e); - return false; - } - } - - /// 设置音量 - Future _setVolume(int volume) async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod( - 'setVolume', - {'volume': volume}, - ); - return result ?? false; - } catch (e) { - Logger.error('设置音量失败', e); - return false; - } - } - - /// 合成文本为语音并保存到文件 - /// - /// [text] 要合成的文本 - /// [fileName] 保存的文件名(不包含路径和扩展名) - /// - /// 返回保存的音频文件路径,如果失败则返回 null - Future synthesizeToFile(String text, String fileName) async { - _checkInitialized(); - - if (text.isEmpty) { - Logger.warning('合成文本为空'); - return null; - } - - try { - // 获取临时目录 - final tempDir = await getTemporaryDirectory(); - final filePath = '${tempDir.path}/$fileName.wav'; - - // 设置状态 - _ttsState.value = TtsState.synthesizing; - - final result = await _methodChannel.invokeMethod( - 'synthesizeToFile', - { - 'text': text, - 'filePath': filePath, - }, - ); - - // 合成完成后设置状态 - _ttsState.value = TtsState.idle; - - return result; - } catch (e) { - Logger.error('合成文本到文件失败', e); - - // 发生错误时设置状态 - _ttsState.value = TtsState.idle; - - return null; - } - } - - /// 合成文本并直接播放 - /// - /// [text] 要合成并播放的文本 - /// - /// 返回是否成功开始播放 - Future speak(String text) async { - _checkInitialized(); - - if (text.isEmpty) { - Logger.warning('播放文本为空'); - return false; - } - - try { - // 设置状态 - _ttsState.value = TtsState.synthesizing; - - final result = await _methodChannel.invokeMethod( - 'speak', - {'text': text}, - ); - - if (result == true) { - _ttsState.value = TtsState.playing; - } else { - _ttsState.value = TtsState.idle; - } - - return result ?? false; - } catch (e) { - Logger.error('合成并播放文本失败', e); - - // 发生错误时设置状态 - _ttsState.value = TtsState.idle; - - return false; - } - } - - /// 停止播放 - Future stop() async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod('stop'); - - if (result == true) { - _ttsState.value = TtsState.stopped; - } - - return result ?? false; - } catch (e) { - Logger.error('停止播放失败', e); - return false; - } - } - - /// 暂停播放 - Future pause() async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod('pause'); - return result ?? false; - } catch (e) { - Logger.error('暂停播放失败', e); - return false; - } - } - - /// 恢复播放 - Future resume() async { - _checkInitialized(); - - try { - final result = await _methodChannel.invokeMethod('resume'); - return result ?? false; - } catch (e) { - Logger.error('恢复播放失败', e); - return false; - } - } - - /// 检查是否已初始化 - void _checkInitialized() { - if (!_isInitialized) { - throw StateError('Azure 文本转语音服务未初始化'); - } - } - - /// 释放资源 - Future dispose() async { - try { - await _eventSubscription?.cancel(); - _eventSubscription = null; - - // 只有在控制器存在且未关闭时才关闭 - if (_eventStreamController != null && !_eventStreamController!.isClosed) { - await _eventStreamController?.close(); - } - _eventStreamController = null; - - _synthesisStream = null; - - if (_isInitialized) { - await _methodChannel.invokeMethod('dispose'); - } - - _isInitialized = false; - Logger.info('Azure 文本转语音服务资源已释放'); - } catch (e) { - Logger.error('释放 Azure 文本转语音服务资源失败', e); - } - } - - @override - void onClose() { - dispose(); - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/data/services/background_agent_service.dart b/lib/data/services/background_agent_service.dart index 3e91c1c2c..d8c54ce45 100644 --- a/lib/data/services/background_agent_service.dart +++ b/lib/data/services/background_agent_service.dart @@ -84,9 +84,10 @@ class BackgroundAgentService extends GetxService { _recognitionSubscription?.cancel(); _aiResponseSubscription?.cancel(); - // 停止TTS + // 停止TTS并结束会话 if (_ttsService.isPlaying) { _ttsService.stop(); + _ttsService.endSession(); } // 停止语音识别 @@ -106,7 +107,7 @@ class BackgroundAgentService extends GetxService { Future startAgentInteraction(String agentId) async { // 先说"我在听",然后再播放欢迎消息 const listeningMessage = "我在听"; - _ttsService.speakSingle(listeningMessage); + _ttsService.speakOnce(listeningMessage); // 防止重复处理 if (isProcessing.value) { @@ -180,13 +181,18 @@ class BackgroundAgentService extends GetxService { // 实时更新识别文本 if (event.text.isNotEmpty) { recognizedText.value = event.text; + Logger.info('处理语音识别中间结果: $event.text'); + // 只有当TTS正在播放或AI响应流正在接收时才中断 + if (_ttsService.isPlaying || _aiResponseSubscription != null) { + _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); + } } break; case RecognitionEventType.finalResult: // 最终结果 if (event.text.isNotEmpty) { recognizedText.value = event.text; - + Logger.info('处理语音识别最终结果: $event.text'); // 处理识别到的文本 _processRecognizedText(event.text); @@ -226,19 +232,43 @@ class BackgroundAgentService extends GetxService { } } + /// 中断当前响应 + void _interruptCurrentResponse(String reason) { + Logger.info(reason); + + // 停止TTS并结束会话 + if (_ttsService.isPlaying) { + _ttsService.stop(); + _ttsService.endSession(); + } + + // 取消AI响应流订阅 + if (_aiResponseSubscription != null) { + _aiResponseSubscription!.cancel(); + _aiResponseSubscription = null; + + // 记录中断事件 + Logger.info('已中断AI响应流和TTS播放'); + } + } + /// 处理识别到的文本 Future _processRecognizedText(String text) async { if (text.trim().isEmpty || _currentAgent == null) return; try { + // 在处理新的文本前,确保中断任何正在进行的响应 + _interruptCurrentResponse("处理新的用户输入,中断当前响应"); + // 添加用户消息到历史记录 _addMessageToHistory('user', text); - // 启动TTS会话,使用当前Agent的voice - _ttsService.startSession(_currentAgent!.voice); - // 获取AI响应 + // 重置取消标志 String fullResponse = ''; + // 启动TTS会话,使用当前Agent的voice + await _ttsService.startSession(_currentAgent!.voice); + // 获取AI响应流 final responseStream = _aiService.sendMessageStream( messages: messageHistory, @@ -250,7 +280,7 @@ class BackgroundAgentService extends GetxService { (chunk) { fullResponse += chunk; - // 如果TTS启用,发送到TTS + // 发送到TTS _ttsService.speak(chunk); // 重置无活动定时器 @@ -263,28 +293,37 @@ class BackgroundAgentService extends GetxService { // 结束TTS会话 _ttsService.endSession(); + // 清除AI响应流订阅引用 + _aiResponseSubscription = null; + // 注意:我们不再在这里重启语音识别,因为它应该已经在运行 }, onError: (error) { Logger.error('AI响应错误', error); + // 结束TTS会话 + _ttsService.endSession(); + // 播放错误消息 const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; - _ttsService.speak(errorMessage); + _ttsService.speakOnce(errorMessage); // 添加错误消息到历史记录 _addMessageToHistory('assistant', errorMessage); - // 结束TTS会话 - _ttsService.endSession(); + // 清除AI响应流订阅引用 + _aiResponseSubscription = null; }, ); } catch (e) { Logger.error('处理识别文本失败', e); + // 结束TTS会话 + _ttsService.endSession(); + // 播放错误消息 const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; - _ttsService.speak(errorMessage); + _ttsService.speakOnce(errorMessage); // 添加错误消息到历史记录 _addMessageToHistory('assistant', errorMessage); @@ -405,20 +444,6 @@ class BackgroundAgentService extends GetxService { } } - /// 中断当前响应 - void _interruptCurrentResponse(String reason) { - Logger.info(reason); - - // 停止TTS - if (_ttsService.isPlaying) { - _ttsService.stop(); - } - - // 取消AI响应流订阅 - _aiResponseSubscription?.cancel(); - _aiResponseSubscription = null; - } - /// 响应新的媒体事件(双击媒体按钮) Future respondToNewMediaEvent() async { // 不管当前状态如何,总是中断当前响应 @@ -426,9 +451,8 @@ class BackgroundAgentService extends GetxService { // 回答"我在听" const listeningMessage = "我在听"; - _ttsService.speakSingle(listeningMessage); + await _ttsService.speakOnce(listeningMessage); - // 不需要重启语音识别,因为它应该已经在运行 // 如果识别会话已经终止,确保重新启动 if (!isListening.value) { await _restartVoiceRecognition(); @@ -448,9 +472,10 @@ class BackgroundAgentService extends GetxService { _recognitionSubscription?.cancel(); _aiResponseSubscription?.cancel(); - // 停止TTS + // 停止TTS并结束会话 if (_ttsService.isPlaying) { _ttsService.stop(); + _ttsService.endSession(); } // 停止语音识别 diff --git a/lib/data/services/flutter_tts_service.dart b/lib/data/services/flutter_tts_service.dart index 87eaded74..453218ebb 100644 --- a/lib/data/services/flutter_tts_service.dart +++ b/lib/data/services/flutter_tts_service.dart @@ -241,7 +241,7 @@ class FlutterTtsService extends GetxService { if (text.isEmpty) return; _textQueue.add(text); - Logger.info('已添加文本到TTS队列: "${text.length > 20 ? text.substring(0, 20) + '...' : text}"'); + Logger.info('已添加文本到TTS队列: "${text.length > 20 ? "${text.substring(0, 20)}..." : text}"'); } /// 开始处理队列(如果尚未开始) diff --git a/lib/data/services/volcano_asr_api_service.dart b/lib/data/services/volcano_asr_api_service.dart index f62252bb6..4007667e7 100644 --- a/lib/data/services/volcano_asr_api_service.dart +++ b/lib/data/services/volcano_asr_api_service.dart @@ -271,7 +271,7 @@ class VolcanoAsrApiService extends GetxService { // 序列化并压缩payload final String payloadStr = jsonEncode(payload); - Logger.info('发送全客户端请求: $payloadStr'); + // Logger.info('发送全客户端请求: $payloadStr'); final List payloadBytes = await _gzipCompress(utf8.encode(payloadStr)); // 增加序列号 @@ -552,7 +552,7 @@ class VolcanoAsrApiService extends GetxService { text: text, )); - Logger.info('添加最终识别结果: $text'); + // Logger.info('添加最终识别结果: $text'); // 重要:在这里不要断开连接,而是继续监听下一句话 // 但可以清空当前的中间结果状态,准备接收新的语音输入 @@ -576,7 +576,7 @@ class VolcanoAsrApiService extends GetxService { text: text, )); - Logger.info('添加中间识别结果: $text'); + // Logger.info('添加中间识别结果: $text'); } } } diff --git a/lib/data/services/volcano_tts_api_service.dart b/lib/data/services/volcano_tts_api_service.dart index e4dfb6195..043a3ab5c 100644 --- a/lib/data/services/volcano_tts_api_service.dart +++ b/lib/data/services/volcano_tts_api_service.dart @@ -23,7 +23,7 @@ import '../../core/utils/logger.dart'; /// 2. 调用 synthesize 合成文本 /// 3. 调用 endSession 结束会话 /// -/// 或者使用 speakSingle 一次性完成上述流程 +/// 或者使用 speakOnce 一次性完成上述流程 class VolcanoTtsApiService extends GetxController { // 单例实例 static VolcanoTtsApiService? _instance; @@ -34,6 +34,9 @@ class VolcanoTtsApiService extends GetxController { return _instance!; } + // 会话状态管理 - 只保留基本状态标志 + bool _isSessionActive = false; + // 私有构造函数 VolcanoTtsApiService._internal() : _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '', @@ -112,9 +115,6 @@ class VolcanoTtsApiService extends GetxController { final _eventController = StreamController>.broadcast(); Stream> get eventStream => _eventController.stream; - // 控制器状态 - bool _isDisposed = false; - // 当前状态 final isConnecting = false.obs; final isConnected = false.obs; @@ -358,7 +358,7 @@ class VolcanoTtsApiService extends GetxController { onError: (error) { // 直接处理错误,不尝试重连 Logger.error('WebSocket错误: $error'); - if (!_isDisposed && !_eventController.isClosed) { + if (!_eventController.isClosed) { _eventController.add({ 'eventType': 'error', 'error': error.toString() @@ -384,7 +384,7 @@ class VolcanoTtsApiService extends GetxController { // 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常 try { - if (!_isDisposed && !_eventController.isClosed) { + if (!_eventController.isClosed) { _eventController.add({ 'eventType': 'connectionFailed', 'error': '连接超时' @@ -401,7 +401,6 @@ class VolcanoTtsApiService extends GetxController { final subscription = eventStream.listen((event) { if (event['eventType'] == 'connectionStarted') { if (!completer.isCompleted) { - Logger.info('火山TTS连接成功'); completer.complete(true); timeout?.cancel(); timeout = null; @@ -423,7 +422,6 @@ class VolcanoTtsApiService extends GetxController { if (result) { _isConnected = true; isConnected.value = true; - Logger.info('火山TTS连接成功完成'); } return result; @@ -444,18 +442,23 @@ class VolcanoTtsApiService extends GetxController { /// /// 返回会话是否成功开始 Future startSession(String speaker) async { - Logger.info('开始TTS会话'); + // 如果会话已经活跃,直接返回成功 + if (_isSessionActive && _sessionId != null) { + Logger.info('TTS会话已存在,跳过创建'); + return true; + } try { if (!_isConnected) { final connected = await connect(); - Logger.info('TTS未连接状态, 开始连接, 连接结果: $connected'); if (!connected) return false; } // 生成会话ID _sessionId = const Uuid().v4().replaceAll('-', ''); - + + Logger.info('开始TTS会话: ${_sessionId!}'); + // 发送开始会话事件 await _startTtsSession(speaker); @@ -486,7 +489,6 @@ class VolcanoTtsApiService extends GetxController { final subscription = eventStream.listen((event) { if (event['eventType'] == 'sessionStarted') { if (!completer.isCompleted) { - Logger.info('火山TTS会话开始成功'); completer.complete(true); timeout?.cancel(); timeout = null; @@ -504,9 +506,10 @@ class VolcanoTtsApiService extends GetxController { // 等待会话结果 final result = await completer.future; subscription.cancel(); - + if (result) { - Logger.info('火山TTS会话已完全建立'); + // 更新会话状态 + _isSessionActive = true; } return result; @@ -532,9 +535,13 @@ class VolcanoTtsApiService extends GetxController { } // 确保会话已开始 - if (_sessionId == null) { - Logger.error('TTS会话未开始,请先调用startSession'); + if (!_isSessionActive || _sessionId == null) { + // 需要创建新会话 + final sessionStarted = await startSession(speaker); + if (!sessionStarted) { + Logger.error('TTS会话开始失败,无法合成文本'); return false; + } } // 设置合成状态 @@ -557,11 +564,12 @@ class VolcanoTtsApiService extends GetxController { /// /// 返回会话是否成功结束 Future endSession() async { - if (_sessionId == null) { + if (!_isSessionActive || _sessionId == null) { return true; // 没有活跃会话 } - Logger.info('TTS结束会话'); - + + Logger.info('TTS结束会话: ${_sessionId!}'); + try { // 发送结束会话事件 await _finishSession(); @@ -575,7 +583,9 @@ class VolcanoTtsApiService extends GetxController { completer.complete(true); // 超时也认为成功 } }); - + isSynthesizing.value = false; + _sessionId = null; + // 监听会话事件 final subscription = eventStream.listen((event) { if (event['eventType'] == 'sessionFinished') { @@ -590,13 +600,14 @@ class VolcanoTtsApiService extends GetxController { final result = await completer.future; subscription.cancel(); - // 清除会话ID - _sessionId = null; - isSynthesizing.value = false; + // 重置会话状态 + _isSessionActive = false; return result; } catch (e) { _handleError(e); + // 即使出错,也重置会话状态 + _isSessionActive = false; return false; } } @@ -610,7 +621,7 @@ class VolcanoTtsApiService extends GetxController { try { // 如果有活跃会话,先结束会话 - if (_sessionId != null) { + if (_isSessionActive) { await endSession(); } @@ -649,6 +660,7 @@ class VolcanoTtsApiService extends GetxController { _sessionId = null; isConnected.value = false; isSynthesizing.value = false; + _isSessionActive = false; return result; } catch (e) { @@ -665,6 +677,12 @@ class VolcanoTtsApiService extends GetxController { return; } + // 检查控制器是否已关闭 + if (_eventController.isClosed || _audioDataController.isClosed) { + Logger.warning('TTS服务控制器已关闭,无法处理消息'); + return; + } + // 转换为Uint8List以提高处理效率 final messageData = Uint8List.fromList(message); @@ -676,7 +694,7 @@ class VolcanoTtsApiService extends GetxController { // 处理事件 final event = response['event'] as int?; - if (event != null && !_isDisposed && !_eventController.isClosed) { + if (event != null && !_eventController.isClosed) { switch (event) { case eventConnectionStarted: _eventController.add({'eventType': 'connectionStarted'}); @@ -712,7 +730,7 @@ class VolcanoTtsApiService extends GetxController { try { _eventController.add({'eventType': 'ttsResponse'}); - if (!_isDisposed && !_audioDataController.isClosed) { + if (!_audioDataController.isClosed) { _audioDataController.add(payload); } @@ -825,7 +843,7 @@ class VolcanoTtsApiService extends GetxController { } } catch (e) { // 直接报告错误,不尝试恢复 - if (!_isDisposed && !_eventController.isClosed) { + if (!_eventController.isClosed) { _eventController.add({ 'eventType': 'playbackError', 'error': '添加音频到播放列表失败: $e' @@ -860,7 +878,7 @@ class VolcanoTtsApiService extends GetxController { await _audioPlayer.play(); } catch (e) { // 直接报告错误,不尝试恢复 - if (!_isDisposed && !_eventController.isClosed) { + if (!_eventController.isClosed) { _eventController.add({ 'eventType': 'playbackError', 'error': '开始播放失败: $e' @@ -872,14 +890,18 @@ class VolcanoTtsApiService extends GetxController { /// 处理错误 void _handleError(dynamic error) { try { - // 只记录错误,不尝试恢复 - if (!_isDisposed && !_eventController.isClosed) { - _eventController.add({ - 'eventType': 'error', - 'error': error.toString() - }); + // 检查控制器是否已关闭 + if (_eventController.isClosed) { + Logger.warning('TTS事件控制器已关闭,无法发送错误事件'); + return; } + // 只记录错误,不尝试恢复 + _eventController.add({ + 'eventType': 'error', + 'error': error.toString() + }); + Logger.error('火山语音TTS错误: $error'); } catch (e) { // 如果在处理错误时发生异常,记录到日志但不再抛出异常 @@ -896,9 +918,10 @@ class VolcanoTtsApiService extends GetxController { _sessionId = null; isConnected.value = false; isSynthesizing.value = false; + _isSessionActive = false; // 通知监听者连接已断开 - if (!_isDisposed && !_eventController.isClosed) { + if (!_eventController.isClosed) { _eventController.add({'eventType': 'disconnected'}); } } @@ -1193,7 +1216,6 @@ class VolcanoTtsApiService extends GetxController { final optional = _createOptional(eventFinishSession, _sessionId, -1); final payload = utf8.encode('{}'); - await _sendEvent(header, optional, payload); } @@ -1290,8 +1312,6 @@ class VolcanoTtsApiService extends GetxController { /// 释放资源 @override void onClose() async { - _isDisposed = true; - await disconnect(); // 关闭控制器 @@ -1306,6 +1326,10 @@ class VolcanoTtsApiService extends GetxController { // 释放资源 _audioPlayer.dispose(); + // 清除单例实例,确保下次获取时创建新实例 + _instance = null; + _isSessionActive = false; + super.onClose(); } @@ -1337,8 +1361,8 @@ class VolcanoTtsApiService extends GetxController { // 清空播放列表 await _playlist.clear(); - // 结束当前会话 - await endSession(); + // // 结束当前会话 + // await endSession(); // 清空音频缓冲区 _audioBuffer.clear(); @@ -1353,8 +1377,8 @@ class VolcanoTtsApiService extends GetxController { } /// 检查会话是否活跃 - bool isSessionActive() { - return _sessionId != null; + bool hasActiveSession() { + return _isSessionActive && _sessionId != null; } /// 设置音量 @@ -1380,16 +1404,13 @@ class VolcanoTtsApiService extends GetxController { // 这个方法在当前实现中不需要特殊处理 } - /// 单句合成辅助函数 - /// - /// 一次性完成连接、会话创建、合成和播放的过程 /// 适合单句或少量文本的快速合成场景 /// /// [text] 要合成的文本 /// [speaker] 发音人,如果为null则使用默认发音人 /// /// 返回合成是否成功 - Future speakSingle(String text, {String? speaker}) async { + Future speakOnce(String text, {String? speaker}) async { if (text.isEmpty) { Logger.warning('合成文本为空,跳过合成'); return false; @@ -1409,29 +1430,24 @@ class VolcanoTtsApiService extends GetxController { } } - // 2. 确保会话已开始 - final bool needNewSession = _sessionId == null; - if (needNewSession) { - Logger.info('TTS会话未开始,开始会话'); - final sessionStarted = await startSession(actualSpeaker); - if (!sessionStarted) { - Logger.error('TTS会话开始失败,无法合成文本'); - return false; - } + // 2. 开始会话 + final sessionStarted = await startSession(actualSpeaker); + if (!sessionStarted) { + Logger.error('TTS会话开始失败,无法合成文本'); + return false; } // 3. 合成文本 isSynthesizing.value = true; final success = await synthesize(text, actualSpeaker); - // 如果是我们开始的新会话,则结束会话 - if (needNewSession) { - await endSession(); - } + endSession(); return success; } catch (e) { - _handleError(e); + Logger.error('TTS一次性合成失败: $e'); + // 确保会话结束 + endSession(); return false; } } diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index f1dd1da24..0d036682f 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -39,9 +39,7 @@ class ChatController extends GetxController { final RxString recognizedText = ''.obs; final RxBool isListening = false.obs; - // 临时消息ID,用于语音识别中的消息 - String? _tempRecognitionMessageId; - // 临时消息的索引,用于更新临时消息 + // 临时消息索引,用于更新临时消息 int _tempMessageIndex = -1; // 流式响应控制 @@ -54,7 +52,8 @@ class ChatController extends GetxController { @override void onInit() { super.onInit(); - + Logger.info('ChatController onInit'); + // 从路由参数获取Agent信息 final Map args = Get.arguments ?? {}; final String agentId = args['agentId'] ?? 'personal_assistant'; @@ -79,16 +78,14 @@ class ChatController extends GetxController { content: agent.welcomeMessage, timestamp: DateTime.now(), )); - // 如果启用了TTS,播放欢迎消息 + // 如果启用了TTS,播放欢迎消息 if (isTtsEnabled.value) { - _ttsService.speakSingle(agent.welcomeMessage); + _ttsService.speak(agent.welcomeMessage); } } - // 如果playVoiceOnEnter为true,自动激活语音识别 if (args['playVoiceOnEnter'] == true) { - startVoiceRecognition(); } @@ -100,19 +97,19 @@ class ChatController extends GetxController { @override void onClose() { - // 关闭ScrollController + Logger.info('ChatController onClose'); + + // 清理资源 scrollController.dispose(); - - // 取消所有订阅 _aiResponseSubscription?.cancel(); _recognitionSubscription?.cancel(); - - // 停止TTS + + // 停止TTS并结束会话 if (_ttsService.isPlaying) { _ttsService.stop(); + _ttsService.endSession(); } - - // 停止语音识别 + if (isVoiceRecognitionActive.value) { _asrService.stopRecognition(); } @@ -135,14 +132,42 @@ class ChatController extends GetxController { } } + /// 中断当前响应 + void _interruptCurrentResponse(String reason) { + Logger.info('${reason}'); + + // 停止TTS并结束会话 + if (_ttsService.isPlaying) { + _ttsService.stop(); + _ttsService.endSession(); + } + + // 取消AI响应流订阅 + if (_aiResponseSubscription != null) { + _shouldCancelAiResponse = true; + _aiResponseSubscription!.cancel(); + _aiResponseSubscription = null; + + // 将加载中的消息转换为正常消息,保留已输出的内容 + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + messages[index] = messages[index].copyWith( + isLoading: false, + ); + messages.refresh(); + } + + // 重置发送状态 + isSending.value = false; + } + } + // 发送消息 Future sendMessage(String text) async { if (text.trim().isEmpty) return; - // 停止TTS - if (_ttsService.isPlaying) { - _ttsService.stop(); - } + // 中断当前响应 + _interruptCurrentResponse("发送新消息,中断当前响应"); // 添加用户消息 final userMessage = Message( @@ -151,9 +176,7 @@ class ChatController extends GetxController { timestamp: DateTime.now(), ); messages.add(userMessage); - - // 滚动到底部显示用户消息 - Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + _scrollToBottom(); // 清空输入框 inputText.value = ''; @@ -161,37 +184,22 @@ class ChatController extends GetxController { // 添加加载中的消息 final loadingMessage = Message.loading(); messages.add(loadingMessage); - - // 再次滚动到底部显示加载中的消息 - Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + _scrollToBottom(); isSending.value = true; try { // 构建消息历史 - final List> messageHistory = []; - - // 只取最近的10条消息作为上下文 - final recentMessages = messages.where((m) => !m.isLoading).toList(); - final startIndex = - recentMessages.length > 10 ? recentMessages.length - 10 : 0; - - for (int i = startIndex; i < recentMessages.length; i++) { - final m = recentMessages[i]; - messageHistory.add({ - 'role': m.role, - 'content': m.content, - }); - } + final List> messageHistory = _buildMessageHistory(); // 重置取消标志 _shouldCancelAiResponse = false; - // 准备TTS会话 + // 启动TTS会话(仅当TTS启用时) if (isTtsEnabled.value) { await _ttsService.startSession(agent.voice); } - + String fullResponse = ''; // 获取AI响应流 @@ -207,96 +215,123 @@ class ChatController extends GetxController { fullResponse += chunk; - // 更新加载中的消息内容,但保持isLoading状态 - final index = messages.indexWhere((m) => m.isLoading); - if (index != -1) { - // 直接更新消息内容,然后使用refresh()触发UI更新 - messages[index] = messages[index].copyWith( - content: fullResponse, - isLoading: true, // 明确设置为true以确保状态一致 - ); - - // 使用refresh()方法刷新列表,触发UI更新 - messages.refresh(); - - // 每次收到新内容后滚动到底部 - Future.delayed(Duration(milliseconds: 100), _scrollToBottom); - } + // 更新加载中的消息内容 + _updateLoadingMessage(fullResponse); // 如果TTS启用,发送到TTS - if (isTtsEnabled.value && !_shouldCancelAiResponse) { - // 直接将文本块发送到TTS,服务会自己处理断句 + if (isTtsEnabled.value) { _ttsService.speak(chunk); } }, onDone: () { if (_shouldCancelAiResponse) return; - - // 完成响应,此时才将isLoading设为false - final index = messages.indexWhere((m) => m.isLoading); - if (index != -1) { - messages[index] = messages[index].copyWith( - content: fullResponse, - isLoading: false, - ); - - // 滚动到底部显示完整回复 - Future.delayed(Duration(milliseconds: 100), _scrollToBottom); - } - - // 结束TTS会话 + + // 结束TTS会话(仅当TTS启用时) if (isTtsEnabled.value) { _ttsService.endSession(); } - isSending.value = false; - + // 完成响应,将isLoading设为false + _finalizeResponse(fullResponse); + // 保存聊天历史 _saveChatHistory(); }, onError: (error) { Logger.error('AI响应错误', error); - - // 更新加载中的消息为错误消息 - final index = messages.indexWhere((m) => m.isLoading); - if (index != -1) { - messages[index] = messages[index].copyWith( - content: '抱歉,我遇到了一些问题,无法回应您的请求。', - isLoading: false, - ); - } - - // 结束TTS会话 + + // 结束TTS会话(仅当TTS启用时) if (isTtsEnabled.value) { _ttsService.endSession(); } - isSending.value = false; + // 更新加载中的消息为错误消息 + _handleResponseError(); }, ); } catch (e) { Logger.error('发送消息失败', e); - - // 更新加载中的消息为错误消息 - final index = messages.indexWhere((m) => m.isLoading); - if (index != -1) { - messages[index] = messages[index].copyWith( - content: '抱歉,我遇到了一些问题,无法回应您的请求。', - isLoading: false, - ); + + // 结束TTS会话(仅当TTS启用时) + if (isTtsEnabled.value) { + _ttsService.endSession(); } + + _handleResponseError(); + } + } - isSending.value = false; + // 构建消息历史 + List> _buildMessageHistory() { + final List> messageHistory = []; + + // 只取最近的10条消息作为上下文 + final recentMessages = messages.where((m) => !m.isLoading).toList(); + final startIndex = recentMessages.length > 10 ? recentMessages.length - 10 : 0; + + for (int i = startIndex; i < recentMessages.length; i++) { + final m = recentMessages[i]; + messageHistory.add({ + 'role': m.role, + 'content': m.content, + }); + } + + return messageHistory; + } + + // 更新加载中的消息 + void _updateLoadingMessage(String content) { + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + messages[index] = messages[index].copyWith( + content: content, + isLoading: true, + ); + messages.refresh(); + _scrollToBottom(); + } + } + + // 完成响应 + void _finalizeResponse(String content) { + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + messages[index] = messages[index].copyWith( + content: content, + isLoading: false, + ); + messages.refresh(); + _scrollToBottom(); } + + isSending.value = false; + _aiResponseSubscription = null; + } + + // 处理响应错误 + void _handleResponseError() { + final index = messages.indexWhere((m) => m.isLoading); + if (index != -1) { + messages[index] = messages[index].copyWith( + content: '抱歉,我遇到了一些问题,无法回应您的请求。', + isLoading: false, + ); + messages.refresh(); + } + + isSending.value = false; + _aiResponseSubscription = null; } // 切换TTS状态 void toggleTts() { isTtsEnabled.toggle(); - // 如果关闭TTS,停止当前播放 + // 如果关闭TTS,停止当前播放并结束会话 if (!isTtsEnabled.value && _ttsService.isPlaying) { _ttsService.stop(); + _ttsService.endSession(); } } @@ -304,23 +339,6 @@ class ChatController extends GetxController { Future startVoiceRecognition() async { if (isVoiceRecognitionActive.value) return; - // 每次收到新内容后滚动到底部 - Future.delayed(Duration(milliseconds: 100), _scrollToBottom); - // 停止TTS - if (_ttsService.isPlaying) { - _ttsService.stop(); - - // 如果正在接收AI响应,取消它 - if (isSending.value) { - _shouldCancelAiResponse = true; - _aiResponseSubscription?.cancel(); - - // 移除加载中的消息 - messages.removeWhere((m) => m.isLoading); - isSending.value = false; - } - } - try { // 启动语音识别 final success = await _asrService.startRecognition(); @@ -332,115 +350,11 @@ class ChatController extends GetxController { // 重置临时消息索引 _tempMessageIndex = -1; - _tempRecognitionMessageId = null; // 监听识别事件 _recognitionSubscription = _asrService.recognitionStream.listen( - (event) { - switch (event.type) { - case RecognitionEventType.recognizing: - // 实时更新识别文本 - if (event.text.isNotEmpty) { - recognizedText.value = event.text; - - // 检查是否已经有临时消息 - if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { - // 更新已有的临时消息 - messages[_tempMessageIndex] = - messages[_tempMessageIndex].copyWith( - content: event.text, - ); - messages.refresh(); - - // 每次收到新内容后滚动到底部 - Future.delayed( - Duration(milliseconds: 100), _scrollToBottom); - // 使用refresh()方法刷新列表 - } else { - // 添加新的临时消息,但只有在有实际内容时 - final tempMessage = Message( - role: 'user', - content: event.text, - timestamp: DateTime.now(), - ); - // 不使用ID,直接使用索引跟踪临时消息 - messages.add(tempMessage); - _tempMessageIndex = messages.length - 1; - messages.refresh(); - - // 滚动到底部显示新消息 - Future.delayed(Duration(milliseconds: 100), _scrollToBottom); - } - } - break; - case RecognitionEventType.finalResult: - // 最终结果 - if (event.text.isNotEmpty) { - recognizedText.value = event.text; - - // 查找并移除临时消息 - if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { - messages.removeAt(_tempMessageIndex); - } - - // 重置临时消息索引 - _tempMessageIndex = -1; - _tempRecognitionMessageId = null; - - // 发送识别到的文本 - sendMessage(event.text); - - // 重置识别文本,但不关闭语音识别框 - recognizedText.value = ''; - } - break; - case RecognitionEventType.error: - // 错误处理,关闭语音识别框,不尝试重新启动 - Logger.error('语音识别错误: ${event.error}'); - - // 查找并移除临时消息 - if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { - if (messages[_tempMessageIndex].content.trim().isEmpty) { - messages.removeAt(_tempMessageIndex); - } - } - - // 重置临时消息索引 - _tempMessageIndex = -1; - _tempRecognitionMessageId = null; - - // 停止语音识别 - isVoiceRecognitionActive.value = false; - isListening.value = false; - _asrService.stopRecognition(); - - break; - default: - // 不做任何处理,保持语音识别持续运行 - break; - } - }, - onError: (error) { - // 错误处理,关闭语音识别框,不尝试重新启动 - Logger.error('语音识别错误', error); - - // 查找并移除临时消息 - if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { - if (messages[_tempMessageIndex].content.trim().isEmpty) { - messages.removeAt(_tempMessageIndex); - } - } - - // 重置临时消息索引 - _tempMessageIndex = -1; - _tempRecognitionMessageId = null; - - // 停止语音识别 - isVoiceRecognitionActive.value = false; - isListening.value = false; - _asrService.stopRecognition(); - - }, + _handleRecognitionEvent, + onError: _handleRecognitionError, ); } else { Logger.error('启动语音识别失败'); @@ -452,6 +366,111 @@ class ChatController extends GetxController { } } + // 处理语音识别事件 + void _handleRecognitionEvent(RecognitionEvent event) { + switch (event.type) { + case RecognitionEventType.recognizing: + _handleRecognizing(event.text); + break; + case RecognitionEventType.finalResult: + _handleFinalResult(event.text); + break; + case RecognitionEventType.error: + _handleRecognitionEventError(event.error); + break; + default: + // 不做任何处理 + break; + } + } + + // 处理语音识别中的状态 + void _handleRecognizing(String text) { + if (text.isEmpty) return; + + recognizedText.value = text; + + // 用户开始讲话时,立即停止当前TTS和中断AI响应 + if (_ttsService.isPlaying || _aiResponseSubscription != null) { + _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); + } + + // 更新或创建临时消息 + if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { + // 更新已有的临时消息 + messages[_tempMessageIndex] = messages[_tempMessageIndex].copyWith( + content: text, + ); + messages.refresh(); + } else { + // 添加新的临时消息 + final tempMessage = Message( + role: 'user', + content: text, + timestamp: DateTime.now(), + ); + messages.add(tempMessage); + _tempMessageIndex = messages.length - 1; + messages.refresh(); + } + + _scrollToBottom(); + } + + // 处理语音识别最终结果 + void _handleFinalResult(String text) { + if (text.isEmpty) return; + + Logger.info('处理语音识别最终结果: $text'); + recognizedText.value = text; + + // 查找并移除临时消息 + if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { + messages.removeAt(_tempMessageIndex); + } + + // 重置临时消息索引 + _tempMessageIndex = -1; + + // 发送识别到的文本 + sendMessage(text); + + // 重置识别文本,但不关闭语音识别框 + recognizedText.value = ''; + } + + // 处理语音识别事件错误 + void _handleRecognitionEventError(String? error) { + Logger.error('语音识别错误: ${error ?? "未知错误"}'); + + _cleanupRecognition(); + } + + // 处理语音识别流错误 + void _handleRecognitionError(dynamic error) { + Logger.error('语音识别错误', error); + + _cleanupRecognition(); + } + + // 清理语音识别资源 + void _cleanupRecognition() { + // 查找并移除临时消息 + if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { + if (messages[_tempMessageIndex].content.trim().isEmpty) { + messages.removeAt(_tempMessageIndex); + } + } + + // 重置临时消息索引 + _tempMessageIndex = -1; + + // 停止语音识别 + isVoiceRecognitionActive.value = false; + isListening.value = false; + _asrService.stopRecognition(); + } + // 停止语音识别 Future stopVoiceRecognition() async { if (!isVoiceRecognitionActive.value) return; @@ -472,9 +491,8 @@ class ChatController extends GetxController { } } - // 重置临时消息索引和ID + // 重置临时消息索引 _tempMessageIndex = -1; - _tempRecognitionMessageId = null; } catch (e) { Logger.error('停止语音识别失败', e); } @@ -515,8 +533,6 @@ class ChatController extends GetxController { void addMessage(Message message) { messages.add(message); _saveChatHistory(); - - // 滚动到底部显示新添加的消息 - Future.delayed(Duration(milliseconds: 100), _scrollToBottom); + _scrollToBottom(); } } diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 770960f08..1f7d47b55 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -119,7 +119,16 @@ class ProfileView extends GetView { Get.toNamed(Routes.TTS_TEST); }, ), - + + const Divider(), + _buildMenuItem( + title: 'Azure语音合成测试'.tr, + icon: Icons.record_voice_over, + subtitle: '测试Azure语音TTS功能', + onTap: () { + Get.toNamed(Routes.AZURE_TTS_TEST); + }, + ), const Divider(), _buildMenuItem( @@ -130,13 +139,13 @@ class ProfileView extends GetView { Get.toNamed(Routes.VOLCANO_ASR_TEST); }, ), - const Divider(), + const Divider(), _buildMenuItem( title: 'Azure语音识别测试'.tr, icon: Icons.mic, subtitle: '测试Azure语音ASR功能', onTap: () { - Get.toNamed(Routes.ASR_TEST); + Get.toNamed(Routes.AZURE_ASR_TEST); }, ), const Divider(), diff --git a/lib/modules/test/bindings/azure_tts_test_binding.dart b/lib/modules/test/bindings/azure_tts_test_binding.dart new file mode 100644 index 000000000..93ccb3312 --- /dev/null +++ b/lib/modules/test/bindings/azure_tts_test_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import '../controllers/azure_tts_test_controller.dart'; + +class AzureTtsTestBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => AzureTtsTestController(), + ); + } +} \ No newline at end of file diff --git a/lib/modules/test/bindings/test_binding.dart b/lib/modules/test/bindings/test_binding.dart index cb859c5bc..9f369a763 100644 --- a/lib/modules/test/bindings/test_binding.dart +++ b/lib/modules/test/bindings/test_binding.dart @@ -1,9 +1,9 @@ import 'package:get/get.dart'; -import '../controllers/tts_test_controller.dart'; -import '../controllers/asr_test_controller.dart'; +import '../controllers/volcano_tts_test_controller.dart'; +import '../controllers/azure_asr_test_controller.dart'; import '../../../data/services/volcano_tts_api_service.dart'; -class TtsTestBinding extends Bindings { +class VolcanoTtsTestBinding extends Bindings { @override void dependencies() { // Make sure the VolcanoTtsApiService is registered @@ -12,14 +12,14 @@ class TtsTestBinding extends Bindings { } // Use put instead of lazyPut to ensure the controller is created immediately - Get.put(TtsTestController()); + Get.put(VolcanoTtsTestController()); } } -class AsrTestBinding extends Bindings { +class AzureAsrTestBinding extends Bindings { @override void dependencies() { // Use put instead of lazyPut to ensure the controller is created immediately - Get.put(AsrTestController()); + Get.put(AzureAsrTestController()); } } \ No newline at end of file diff --git a/lib/modules/test/controllers/asr_test_controller.dart b/lib/modules/test/controllers/azure_asr_test_controller.dart similarity index 98% rename from lib/modules/test/controllers/asr_test_controller.dart rename to lib/modules/test/controllers/azure_asr_test_controller.dart index e55589c8f..b9f9d1442 100644 --- a/lib/modules/test/controllers/asr_test_controller.dart +++ b/lib/modules/test/controllers/azure_asr_test_controller.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import '../../../data/services/azure_asr_service.dart'; import '../../../core/utils/logger.dart'; -class AsrTestController extends GetxController { +class AzureAsrTestController extends GetxController { final AzureAsrService _azureService = Get.find(); // 可观察状态 diff --git a/lib/modules/test/controllers/azure_tts_test_controller.dart b/lib/modules/test/controllers/azure_tts_test_controller.dart new file mode 100644 index 000000000..edaec042d --- /dev/null +++ b/lib/modules/test/controllers/azure_tts_test_controller.dart @@ -0,0 +1,315 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import '../../../data/services/azure_tts_service.dart'; +import '../../../core/utils/logger.dart'; + +// 导入 TtsState 枚举 +import '../../../data/services/azure_tts_service.dart' show TtsState; + +class AzureTtsTestController extends GetxController { + final AzureTtsService _ttsService = Get.find(); + + // 可观察状态 + final isSpeaking = false.obs; + final errorMessage = ''.obs; + final isLoading = false.obs; + final statusMessage = '未连接'.obs; + + // 添加实际音频播放状态 + final isAudioPlaying = false.obs; + + // 播放状态检查定时器 + Timer? _playbackCheckTimer; + + // 当前播放的索引 + final currentPlayingIndex = RxInt(-1); + + // 当前使用的语音 + final currentVoice = 'zh-CN-XiaoxiaoNeural'.obs; + + // 可用的语音列表 + final List> availableVoices = [ + {'id': 'zh-CN-XiaoxiaoNeural', 'name': '晓晓(女声)'}, + {'id': 'zh-CN-YunxiNeural', 'name': '云希(男声)'}, + {'id': 'zh-CN-YunyangNeural', 'name': '云扬(男声)'}, + {'id': 'zh-CN-XiaohanNeural', 'name': '晓涵(女声)'}, + {'id': 'zh-CN-XiaomoNeural', 'name': '晓墨(女声)'}, + {'id': 'zh-CN-XiaoxuanNeural', 'name': '晓萱(女声)'}, + {'id': 'en-US-AriaNeural', 'name': 'Aria(英语女声)'}, + ]; + + // 固定的播报内容 + final RxList speechTexts = [ + '这是第一条播报测试,请听取Azure合成效果。', + '这是第二条播报测试,Azure语音合成支持连续多条播报。', + '这是第三条播报测试,可以测试语音合成的流畅度和连贯性。', + 'This is an English test sentence. Azure Text-to-Speech supports multiple languages.', + ].obs; + + // 事件订阅 + StreamSubscription? _eventSubscription; + + @override + void onInit() { + super.onInit(); + + // 初始化UI状态 + isSpeaking.value = false; + isAudioPlaying.value = false; + errorMessage.value = ''; + isLoading.value = false; + + // 监听TTS服务状态 + _setupEventListener(); + + // 启动播放状态检查定时器 + _startPlaybackCheckTimer(); + } + + // 设置事件监听 + void _setupEventListener() { + _eventSubscription?.cancel(); + _eventSubscription = _ttsService.synthesisStream?.listen(_handleEvent); + } + + // 处理事件 + void _handleEvent(SynthesisEvent event) { + switch (event.type) { + case SynthesisEventType.synthesisStarted: + statusMessage.value = '合成开始'; + errorMessage.value = ''; + break; + case SynthesisEventType.synthesisCompleted: + statusMessage.value = '合成完成'; + errorMessage.value = ''; + break; + case SynthesisEventType.synthesisCanceled: + statusMessage.value = '合成取消'; + errorMessage.value = event.reason ?? '未知原因'; + currentPlayingIndex.value = -1; + break; + case SynthesisEventType.error: + statusMessage.value = '错误'; + errorMessage.value = event.error ?? '未知错误'; + currentPlayingIndex.value = -1; + break; + default: + statusMessage.value = '事件: ${event.type}'; + break; + } + + // 强制更新UI + update(); + } + + /// 更新UI状态 + void updateUIState() { + // 强制更新UI + update(); + } + + // 启动播放状态检查定时器 + void _startPlaybackCheckTimer() { + _playbackCheckTimer?.cancel(); + _playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { + // 更新实际播放状态 + final actuallyPlaying = _ttsService.ttsState == TtsState.playing || _ttsService.ttsState == TtsState.synthesizing; + + // 只有状态变化时才更新 + if (isAudioPlaying.value != actuallyPlaying) { + isAudioPlaying.value = actuallyPlaying; + // 更新UI + update(); + } + }); + } + + /// 连续播放所有文本 + Future speakContinuousTexts() async { + if (isLoading.value) { + return; + } + + try { + isLoading.value = true; + isSpeaking.value = true; + errorMessage.value = ''; + + final speaker = currentVoice.value; + + // 设置语音参数 + _ttsService.currentVoice = speaker; + + + // 连续播放所有文本 + for (int i = 0; i < speechTexts.length; i++) { + // 如果播放被中断,退出循环 + if (!isSpeaking.value) break; + + // 在主线程上更新UI + WidgetsBinding.instance.addPostFrameCallback((_) { + currentPlayingIndex.value = i; + statusMessage.value = '正在播放第 ${i + 1}/${speechTexts.length} 条文本'; + update(); + }); + + // 播放当前文本,不等待完成 + _ttsService.speak(speechTexts[i]); + + // 短暂延迟,确保文本被送入队列 + await Future.delayed(const Duration(milliseconds: 300)); + } + + // 在主线程上更新UI + WidgetsBinding.instance.addPostFrameCallback((_) { + statusMessage.value = '所有文本已送入播放队列'; + update(); + }); + + + } catch (e) { + errorMessage.value = '播放出错'; + if (kDebugMode) { + print('连续播放出错: $e'); + } + } finally { + isLoading.value = false; + + // 强制更新UI + update(); + } + } + + /// 播放单条文本 + Future playSingle(int index) async { + if (index < 0 || index >= speechTexts.length) { + return; + } + + try { + isSpeaking.value = true; + errorMessage.value = ''; + currentPlayingIndex.value = index; + statusMessage.value = '准备播放...'; + + final text = speechTexts[index]; + final speaker = currentVoice.value; + + // 设置语音参数 + _ttsService.currentVoice = speaker; + + // 使用异步操作,避免阻塞主线程 + Future(() { + // 播放文本,不等待完成 + _ttsService.speak(text); + + // 在主线程上更新UI + WidgetsBinding.instance.addPostFrameCallback((_) { + statusMessage.value = '文本已送入播放队列'; + update(); + }); + }); + + } catch (e) { + errorMessage.value = '播放出错'; + if (kDebugMode) { + print('播放出错: $e'); + } + } + } + + /// 停止播放 + void stopSpeaking() { + try { + isSpeaking.value = false; + statusMessage.value = '正在停止播放...'; + + // 使用异步操作,避免阻塞主线程 + Future(() async { + // 停止播放 + await _ttsService.stop(); + + // 在主线程上更新UI + WidgetsBinding.instance.addPostFrameCallback((_) { + // 重置UI状态 + currentPlayingIndex.value = -1; + statusMessage.value = '已停止播放'; + update(); + }); + }); + + } catch (e) { + if (kDebugMode) { + print('停止播放出错: $e'); + } + errorMessage.value = '停止播放出错'; + } + } + + // 设置当前语音 + void setVoice(String voiceId) { + currentVoice.value = voiceId; + } + + // 获取当前语音名称 + String getCurrentVoiceName() { + final voice = availableVoices.firstWhere( + (v) => v['id'] == currentVoice.value, + orElse: () => {'id': currentVoice.value, 'name': '未知语音'}, + ); + return voice['name'] ?? '未知语音'; + } + + @override + void onClose() { + // 取消事件订阅 + _eventSubscription?.cancel(); + + // 取消播放状态检查定时器 + _playbackCheckTimer?.cancel(); + + super.onClose(); + } + + /// 初始化Azure TTS服务 + Future initialize({ + String? subscriptionKey, + String? region, + }) async { + try { + isLoading.value = true; + statusMessage.value = '正在初始化Azure TTS...'; + + if (subscriptionKey == null || region == null) { + statusMessage.value = '尝试从环境变量读取配置...'; + } + + await _ttsService.initialize( + subscriptionKey: subscriptionKey, + region: region, + ); + + // 检查是否初始化成功 + final isSuccess = _ttsService.isInitialized; + + if (isSuccess) { + isSpeaking.value = false; + statusMessage.value = 'Azure TTS初始化成功'; + } else { + statusMessage.value = 'Azure TTS初始化失败'; + } + } catch (e) { + statusMessage.value = '初始化异常: $e'; + errorMessage.value = e.toString(); + if (kDebugMode) { + print('初始化异常: $e'); + } + } finally { + isLoading.value = false; + // 强制更新UI + update(); + } + } +} \ No newline at end of file diff --git a/lib/modules/test/controllers/tts_test_controller.dart b/lib/modules/test/controllers/volcano_tts_test_controller.dart similarity index 99% rename from lib/modules/test/controllers/tts_test_controller.dart rename to lib/modules/test/controllers/volcano_tts_test_controller.dart index 87b09d4a5..5113b0520 100644 --- a/lib/modules/test/controllers/tts_test_controller.dart +++ b/lib/modules/test/controllers/volcano_tts_test_controller.dart @@ -4,7 +4,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import '../../../data/services/volcano_tts_api_service.dart'; -class TtsTestController extends GetxController { +class VolcanoTtsTestController extends GetxController { final VolcanoTtsApiService _ttsService = Get.find(); // 可观察状态 diff --git a/lib/modules/test/views/asr_test_view.dart b/lib/modules/test/views/azure_asr_test_view.dart similarity index 98% rename from lib/modules/test/views/asr_test_view.dart rename to lib/modules/test/views/azure_asr_test_view.dart index d902e6725..85fd3f013 100644 --- a/lib/modules/test/views/asr_test_view.dart +++ b/lib/modules/test/views/azure_asr_test_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../controllers/asr_test_controller.dart'; +import '../controllers/azure_asr_test_controller.dart'; -class AsrTestView extends GetView { - const AsrTestView({Key? key}) : super(key: key); +class AzureAsrTestView extends GetView { + const AzureAsrTestView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/lib/modules/test/views/azure_tts_test_view.dart b/lib/modules/test/views/azure_tts_test_view.dart new file mode 100644 index 000000000..61b5760a5 --- /dev/null +++ b/lib/modules/test/views/azure_tts_test_view.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../controllers/azure_tts_test_controller.dart'; + +class AzureTtsTestView extends GetView { + const AzureTtsTestView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // 强制更新控制器状态 + WidgetsBinding.instance.addPostFrameCallback((_) { + // 在下一帧渲染完成后调用 + controller.updateUIState(); + + // 尝试从环境变量初始化 + controller.initialize(); + }); + + return Scaffold( + appBar: AppBar( + title: const Text('Azure 语音合成测试'), + centerTitle: true, + actions: [ + // 添加初始化按钮 + IconButton( + icon: const Icon(Icons.refresh), + tooltip: '从环境变量初始化', + onPressed: () => controller.initialize(), + ), + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 播放控制 + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '播放控制', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Obx(() { + return ElevatedButton.icon( + onPressed: controller.isAudioPlaying.value || controller.isLoading.value + ? null + : () => controller.speakContinuousTexts(), + icon: controller.isAudioPlaying.value + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2) + ) + : const Icon(Icons.playlist_play), + label: Text(controller.isAudioPlaying.value ? '播放中...' : '全部播放'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + ); + }), + Obx(() => ElevatedButton.icon( + onPressed: controller.isAudioPlaying.value + ? () => controller.stopSpeaking() + : null, + icon: const Icon(Icons.stop), + label: const Text('停止'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + )), + ], + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // 播报内容列表 + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '播报内容列表', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + // 生成可点击的播报内容 + Obx(() => ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.speechTexts.length, + separatorBuilder: (context, index) => const Divider(height: 1), + itemBuilder: (context, index) { + final isPlaying = controller.isAudioPlaying.value && + controller.currentPlayingIndex.value == index; + + return ListTile( + title: Text( + controller.speechTexts[index], + style: TextStyle( + fontWeight: isPlaying ? FontWeight.bold : FontWeight.normal, + color: isPlaying ? Colors.blue : null, + ), + ), + leading: CircleAvatar( + backgroundColor: isPlaying ? Colors.blue : Colors.grey.shade200, + radius: 18, + child: Icon( + isPlaying ? Icons.volume_up : Icons.play_arrow, + color: isPlaying ? Colors.white : Colors.black, + size: 20, + ), + ), + trailing: isPlaying + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, + onTap: controller.isAudioPlaying.value || controller.isLoading.value + ? null + : () => controller.playSingle(index), + enabled: !(controller.isAudioPlaying.value || controller.isLoading.value), + tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: isPlaying + ? const BorderSide(color: Colors.blue, width: 1) + : BorderSide.none, + ), + ); + }, + )), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // 语音选择 + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '语音选择', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: controller.availableVoices.map((voice) { + final id = voice['id'] ?? ''; + final name = voice['name'] ?? ''; + return Obx(() => ChoiceChip( + label: Text(name), + selected: controller.currentVoice.value == id, + onSelected: (selected) { + if (selected) { + controller.setVoice(id); + } + }, + selectedColor: Colors.blue, + labelStyle: TextStyle( + color: controller.currentVoice.value == id + ? Colors.white + : Colors.black, + ), + )); + }).toList(), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // 状态显示 + Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Obx(() => Text( + controller.statusMessage.value, + style: TextStyle( + color: controller.isLoading.value + ? Colors.orange + : controller.isAudioPlaying.value + ? Colors.green + : Colors.grey, + fontWeight: FontWeight.bold, + ), + )), + const SizedBox(height: 8), + Obx(() => controller.errorMessage.value.isNotEmpty + ? Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.red.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.error_outline, color: Colors.red, size: 16), + SizedBox(width: 8), + Text( + '错误', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + SizedBox(height: 4), + Text( + controller.errorMessage.value, + style: TextStyle(color: Colors.red.shade800), + ), + ], + ), + ) + : const SizedBox.shrink()), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/test/views/tts_test_view.dart b/lib/modules/test/views/volcano_tts_test_view.dart similarity index 97% rename from lib/modules/test/views/tts_test_view.dart rename to lib/modules/test/views/volcano_tts_test_view.dart index 27c2a33ee..5bae14c57 100644 --- a/lib/modules/test/views/tts_test_view.dart +++ b/lib/modules/test/views/volcano_tts_test_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../controllers/tts_test_controller.dart'; +import '../controllers/volcano_tts_test_controller.dart'; -class TtsTestView extends GetView { - const TtsTestView({Key? key}) : super(key: key); +class VolcanoTtsTestView extends GetView { + const VolcanoTtsTestView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -15,7 +15,7 @@ class TtsTestView extends GetView { return Scaffold( appBar: AppBar( - title: const Text('语音合成测试'), + title: const Text('火山语音合成测试'), centerTitle: true, ), body: SingleChildScrollView( diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 697e2594d..f6eebe855 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -386,7 +386,7 @@ class TranslationController extends GetxController { if (text.isEmpty) return; try { - await _ttsService.speakSingle(text); + await _ttsService.speakOnce(text); } catch (e) { Get.snackbar('Error', '播放翻译失败: ${e.toString()}', snackPosition: SnackPosition.BOTTOM); diff --git a/pubspec.yaml b/pubspec.yaml index 03c31084a..d84ab6466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: intl: ^0.19.0 crypto: ^3.0.3 flutter_tts: ^4.2.2 + logger: ^2.0.2+1 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git