Browse Source

before azure fultter tts

weicu
wolfplus 2 years ago
parent
commit
d7e3a571b6
  1. 353
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  2. 241
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  3. 161
      lib/controllers/azure_tts_test_controller.dart
  4. 36
      lib/core/bindings/initial_binding.dart
  5. 24
      lib/core/routes/app_pages.dart
  6. 2
      lib/core/routes/app_routes.dart
  7. 24
      lib/core/utils/app_lifecycle_manager.dart
  8. 539
      lib/data/services/azure_tts_service.dart
  9. 81
      lib/data/services/background_agent_service.dart
  10. 2
      lib/data/services/flutter_tts_service.dart
  11. 6
      lib/data/services/volcano_asr_api_service.dart
  12. 136
      lib/data/services/volcano_tts_api_service.dart
  13. 464
      lib/modules/chat/controllers/chat_controller.dart
  14. 15
      lib/modules/profile/views/profile_view.dart
  15. 11
      lib/modules/test/bindings/azure_tts_test_binding.dart
  16. 12
      lib/modules/test/bindings/test_binding.dart
  17. 2
      lib/modules/test/controllers/azure_asr_test_controller.dart
  18. 315
      lib/modules/test/controllers/azure_tts_test_controller.dart
  19. 2
      lib/modules/test/controllers/volcano_tts_test_controller.dart
  20. 6
      lib/modules/test/views/azure_asr_test_view.dart
  21. 280
      lib/modules/test/views/azure_tts_test_view.dart
  22. 8
      lib/modules/test/views/volcano_tts_test_view.dart
  23. 2
      lib/modules/translation/controllers/translation_controller.dart
  24. 1
      pubspec.yaml

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

@ -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<SpeechRecognitionEventArgs> { _, 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<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) {
callback.onRecognizing(event.result.text)
}
}
)
// 设置会话开始事件处理
recognizer.sessionStarted.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
callback.onSessionStarted()
}
)
// 设置会话结束事件处理
recognizer.sessionStopped.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = false
callback.onSessionStopped()
stopRecognitionSemaphore.release()
}
)
// 设置取消事件处理
recognizer.canceled.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, 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)
}
}

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

@ -16,22 +16,32 @@ import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: AudioServiceActivity() { 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" 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 val TAG = "MainActivity"
private lateinit var azureAsrHelper: AzureAsrHelper // 移除 Azure ASR 助手
// private lateinit var azureAsrHelper: AzureAsrHelper
// 暂时注释掉AudioProcessingPlugin的实例化 // 暂时注释掉AudioProcessingPlugin的实例化
// private val audioProcessingPlugin: AudioProcessingPlugin by lazy { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// 初始化 Azure ASR 助手 // 移除 Azure ASR 助手初始化
azureAsrHelper = AzureAsrHelper() // azureAsrHelper = AzureAsrHelper()
// 移除 Azure TTS 助手初始化
// azureTtsHelper = AzureTtsHelper(applicationContext)
// 暂时注释掉AudioProcessingPlugin的初始化 // 暂时注释掉AudioProcessingPlugin的初始化
// AudioProcessingPlugin.setApplicationContext(applicationContext) // AudioProcessingPlugin.setApplicationContext(applicationContext)
@ -86,206 +96,95 @@ class MainActivity: AudioServiceActivity() {
} }
} }
// 设置 Azure ASR 方法通道 // 移除 Azure ASR 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result ->
// 移除 Azure TTS 方法通道和相关代码
/*
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"initialize" -> { "initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") val subscriptionKey = call.argument<String>("subscriptionKey")
val serviceRegion = call.argument<String>("serviceRegion") val region = call.argument<String>("region")
val language = call.argument<String>("language") ?: "zh-CN"
if (subscriptionKey == null || serviceRegion == null) { if (subscriptionKey == null || region == null) {
result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null) result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null)
return@setMethodCallHandler return@setMethodCallHandler
} }
try { val success = azureTtsHelper.initialize(subscriptionKey, region)
Log.d(TAG, "初始化 Azure 语音识别服务,区域: $serviceRegion") result.success(success)
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)
}
} }
"resetRecognizer" -> { "setVoice" -> {
try { val voiceName = call.argument<String>("voiceName")
val success = azureAsrHelper.resetRecognizer()
result.success(success) if (voiceName == null) {
} catch (e: Exception) { result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
result.error("RESET_ERROR", "重置识别器失败: ${e.message}", null) return@setMethodCallHandler
} }
val success = azureTtsHelper.setVoice(voiceName)
result.success(success)
} }
"setRecognitionLanguage" -> { "synthesizeToSpeaker" -> {
try { val text = call.argument<String>("text")
val language = call.argument<String>("language") ?: "zh-CN"
azureAsrHelper.setRecognitionLanguage(language) if (text == null) {
result.success(true) result.error("INVALID_ARGUMENTS", "文本不能为空", null)
} catch (e: Exception) { return@setMethodCallHandler
result.error("LANGUAGE_ERROR", e.message, null)
} }
azureTtsHelper.synthesizeToSpeaker(text, result)
} }
"dispose" -> { "synthesizeToFile" -> {
try { val text = call.argument<String>("text")
azureAsrHelper.dispose() val filePath = call.argument<String>("filePath")
result.success(true)
} catch (e: Exception) { if (text == null || filePath == null) {
result.error("DISPOSE_ERROR", e.message, null) result.error("INVALID_ARGUMENTS", "文本和文件路径不能为空", null)
return@setMethodCallHandler
} }
azureTtsHelper.synthesizeToFile(text, filePath, result)
} }
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
} }
} }
*/
// 移除 Azure ASR 事件通道
// 设置 Azure ASR 事件通道 // 移除 Azure TTS 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler( /*
EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_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?) {
azureEventSink = events azureTtsEventSink = events
azureTtsHelper.setEventSink(events)
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
azureEventSink = null azureTtsEventSink = null
azureTtsHelper.setEventSink(null)
} }
} }
) )
*/
} }
// 移除 ASR 事件发送方法
private fun sendAzureEvent(event: Map<String, Any?>) { // 移除 TTS 事件发送方法
runOnUiThread { /*
azureEventSink?.success(event) fun sendTtsEvent(event: Map<String, Any>) {
} azureTtsEventSink?.success(event)
} }
*/
override fun onDestroy() { override fun onDestroy() {
azureAsrHelper.dispose() // 移除 Azure TTS 资源释放
// azureTtsHelper.dispose()
super.onDestroy() super.onDestroy()
} }
} }

161
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<AzureTtsService>();
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<void> 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<void> 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<void> 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<void> 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;
}
}
}

36
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_ai_service.dart';
import '../../data/services/volcano_tts_api_service.dart'; import '../../data/services/volcano_tts_api_service.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_tts_service.dart';
import '../../data/services/background_agent_service.dart'; import '../../data/services/background_agent_service.dart';
import '../../data/services/chat_history_service.dart'; import '../../data/services/chat_history_service.dart';
import '../../data/services/flutter_tts_service.dart'; import '../../data/services/flutter_tts_service.dart';
@ -53,6 +54,9 @@ class InitialBinding extends Bindings {
return service; return service;
}, fenix: true); }, fenix: true);
// Azure TTS 服务
Get.lazyPut<AzureTtsService>(() => AzureTtsService(), fenix: true);
// 语音识别服务 // 语音识别服务
// Get.lazyPut<AzureAsrService>(() { // Get.lazyPut<AzureAsrService>(() {
// final service = AzureAsrService(); // 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((_) { await audioService.init().then((_) {
@ -106,21 +110,21 @@ class InitialBinding extends Bindings {
} }
} }
// 初始化 Azure 语音识别服务 // // 初始化 Azure 语音识别服务
void _initializeAzureService(AzureAsrService azureAsrService) async { // void _initializeAzureService(AzureAsrService azureAsrService) async {
try { // try {
// 直接调用初始化方法,服务内部会读取环境变量配置 // // 直接调用初始化方法,服务内部会读取环境变量配置
final success = await azureAsrService.initialize(); // final success = await azureAsrService.initialize();
if (success) { // if (success) {
Logger.info('Azure 语音识别服务初始化成功'); // Logger.info('Azure 语音识别服务初始化成功');
} else { // } else {
Logger.error('Azure 语音识别服务初始化失败'); // Logger.error('Azure 语音识别服务初始化失败');
} // }
} catch (e) { // } catch (e) {
Logger.error('初始化 Azure 语音识别服务失败', e); // Logger.error('初始化 Azure 语音识别服务失败', e);
} // }
} // }
// 初始化火山翻译服务 // 初始化火山翻译服务
void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async { void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async {

24
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/profile/bindings/profile_binding.dart';
import '../../modules/chat/bindings/chat_binding.dart'; import '../../modules/chat/bindings/chat_binding.dart';
import '../../modules/chat/views/chat_view.dart'; import '../../modules/chat/views/chat_view.dart';
import '../../modules/test/views/tts_test_view.dart'; import '../../modules/test/views/volcano_tts_test_view.dart';
import '../../modules/test/views/asr_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/volcano_asr_test_view.dart';
import '../../modules/test/views/flutter_tts_test_view.dart'; import '../../modules/test/views/flutter_tts_test_view.dart';
import '../../modules/test/views/flutter_asr_test_view.dart'; import '../../modules/test/views/flutter_asr_test_view.dart';
import '../../modules/test/bindings/test_binding.dart'; import '../../modules/test/bindings/test_binding.dart';
import '../../modules/test/bindings/volcano_asr_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_tts_test_binding.dart';
import '../../modules/test/bindings/flutter_asr_test_binding.dart'; import '../../modules/test/bindings/flutter_asr_test_binding.dart';
import '../../modules/translation/views/translation_view.dart'; import '../../modules/translation/views/translation_view.dart';
@ -51,13 +53,23 @@ abstract class AppPages {
), ),
GetPage( GetPage(
name: Routes.TTS_TEST, name: Routes.TTS_TEST,
page: () => TtsTestView(), page: () => VolcanoTtsTestView(),
binding: TtsTestBinding(), binding: VolcanoTtsTestBinding(),
), ),
GetPage( GetPage(
name: Routes.ASR_TEST, name: Routes.ASR_TEST,
page: () => AsrTestView(), page: () => const AzureAsrTestView(),
binding: AsrTestBinding(), 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( GetPage(
name: Routes.VOLCANO_ASR_TEST, name: Routes.VOLCANO_ASR_TEST,

2
lib/core/routes/app_routes.dart

@ -7,6 +7,8 @@ abstract class Routes {
static const speechDemo = '/speech_demo'; static const speechDemo = '/speech_demo';
static const TTS_TEST = '/tts_test'; static const TTS_TEST = '/tts_test';
static const ASR_TEST = '/asr_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 VOLCANO_ASR_TEST = '/volcano_asr_test';
static const translation = '/translation'; static const translation = '/translation';
static const translationHistory = '/translation_history'; static const translationHistory = '/translation_history';

24
lib/core/utils/app_lifecycle_manager.dart

@ -60,12 +60,12 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
// 如果正在录音,停止录音 // 如果正在录音,停止录音
final asrService = Get.find<VolcanoAsrApiService>(); final asrService = Get.find<VolcanoAsrApiService>();
final ttsService = Get.find<VolcanoTtsApiService>(); final ttsService = Get.find<VolcanoTtsApiService>();
if (asrService.isRecording) { // if (asrService.isRecording) {
Logger.info('应用进入后台,停止语音识别'); // Logger.info('应用进入后台,停止语音识别');
asrService.stopRecognition(); // asrService.stopRecognition();
ttsService.stop(); // ttsService.stop();
ttsService.disconnect(); // ttsService.disconnect();
} // }
} }
/// 处理应用进入后台 /// 处理应用进入后台
@ -76,13 +76,13 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
final asrService = Get.find<VolcanoAsrApiService>(); final asrService = Get.find<VolcanoAsrApiService>();
final ttsService = Get.find<VolcanoTtsApiService>(); final ttsService = Get.find<VolcanoTtsApiService>();
// 如果正在录音,停止录音 // 如果正在录音,停止录音
if (asrService.isRecording) { // if (asrService.isRecording) {
Logger.info('应用进入后台,停止语音识别'); // Logger.info('应用进入后台,停止语音识别');
asrService.stopRecognition(); // asrService.stopRecognition();
ttsService.stop(); // ttsService.stop();
ttsService.disconnect(); // ttsService.disconnect();
} // }
} }
@override @override

539
lib/data/services/azure_tts_service.dart

@ -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<dynamic, dynamic> 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 = TtsState.idle.obs;
TtsState get ttsState => _ttsState.value;
// 合成相关
StreamController<SynthesisEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 公开的事件流
Stream<SynthesisEvent>? _synthesisStream;
Stream<SynthesisEvent>? 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<Map<String, String>> _voices = <Map<String, String>>[].obs;
List<Map<String, String>> get voices => _voices;
/// 初始化 Azure 文本转语音服务
///
/// [subscriptionKey] Azure 语音服务的订阅密钥
/// [serviceRegion] Azure 语音服务的区域
/// [defaultVoice] 默认语音名称,默认为中文女声
Future<bool> 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<bool>(
'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<SynthesisEvent>.broadcast();
// 设置事件监听
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.map<SynthesisEvent>((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<List<Map<String, String>>> getAvailableVoices() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<List<dynamic>>('getAvailableVoices');
if (result != null) {
final voicesList = result.map((item) {
final map = Map<String, dynamic>.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<Map<String, String>>.from(voicesList);
return _voices;
}
return [];
} catch (e) {
Logger.error('获取可用语音列表失败', e);
return [];
}
}
/// 设置语音
Future<bool> _setVoice(String voiceName) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setVoice',
{'voiceName': voiceName},
);
return result ?? false;
} catch (e) {
Logger.error('设置语音失败', e);
return false;
}
}
/// 设置语速
Future<bool> _setSpeechRate(int rate) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setSpeechRate',
{'rate': rate},
);
return result ?? false;
} catch (e) {
Logger.error('设置语速失败', e);
return false;
}
}
/// 设置音调
Future<bool> _setPitch(int pitch) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setPitch',
{'pitch': pitch},
);
return result ?? false;
} catch (e) {
Logger.error('设置音调失败', e);
return false;
}
}
/// 设置音量
Future<bool> _setVolume(int volume) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setVolume',
{'volume': volume},
);
return result ?? false;
} catch (e) {
Logger.error('设置音量失败', e);
return false;
}
}
/// 合成文本为语音并保存到文件
///
/// [text] 要合成的文本
/// [fileName] 保存的文件名(不包含路径和扩展名)
///
/// 返回保存的音频文件路径,如果失败则返回 null
Future<String?> 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<String>(
'synthesizeToFile',
{
'text': text,
'filePath': filePath,
},
);
// 合成完成后设置状态
_ttsState.value = TtsState.idle;
return result;
} catch (e) {
Logger.error('合成文本到文件失败', e);
// 发生错误时设置状态
_ttsState.value = TtsState.idle;
return null;
}
}
/// 合成文本并直接播放
///
/// [text] 要合成并播放的文本
///
/// 返回是否成功开始播放
Future<bool> speak(String text) async {
_checkInitialized();
if (text.isEmpty) {
Logger.warning('播放文本为空');
return false;
}
try {
// 设置状态
_ttsState.value = TtsState.synthesizing;
final result = await _methodChannel.invokeMethod<bool>(
'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<bool> stop() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>('stop');
if (result == true) {
_ttsState.value = TtsState.stopped;
}
return result ?? false;
} catch (e) {
Logger.error('停止播放失败', e);
return false;
}
}
/// 暂停播放
Future<bool> pause() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>('pause');
return result ?? false;
} catch (e) {
Logger.error('暂停播放失败', e);
return false;
}
}
/// 恢复播放
Future<bool> resume() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>('resume');
return result ?? false;
} catch (e) {
Logger.error('恢复播放失败', e);
return false;
}
}
/// 检查是否已初始化
void _checkInitialized() {
if (!_isInitialized) {
throw StateError('Azure 文本转语音服务未初始化');
}
}
/// 释放资源
Future<void> 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<bool>('dispose');
}
_isInitialized = false;
Logger.info('Azure 文本转语音服务资源已释放');
} catch (e) {
Logger.error('释放 Azure 文本转语音服务资源失败', e);
}
}
@override
void onClose() {
dispose();
super.onClose();
}
}

81
lib/data/services/background_agent_service.dart

@ -84,9 +84,10 @@ class BackgroundAgentService extends GetxService {
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_aiResponseSubscription?.cancel(); _aiResponseSubscription?.cancel();
// 停止TTS // 停止TTS并结束会话
if (_ttsService.isPlaying) { if (_ttsService.isPlaying) {
_ttsService.stop(); _ttsService.stop();
_ttsService.endSession();
} }
// 停止语音识别 // 停止语音识别
@ -106,7 +107,7 @@ class BackgroundAgentService extends GetxService {
Future<void> startAgentInteraction(String agentId) async { Future<void> startAgentInteraction(String agentId) async {
// 先说"我在听",然后再播放欢迎消息 // 先说"我在听",然后再播放欢迎消息
const listeningMessage = "我在听"; const listeningMessage = "我在听";
_ttsService.speakSingle(listeningMessage); _ttsService.speakOnce(listeningMessage);
// 防止重复处理 // 防止重复处理
if (isProcessing.value) { if (isProcessing.value) {
@ -180,13 +181,18 @@ class BackgroundAgentService extends GetxService {
// 实时更新识别文本 // 实时更新识别文本
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
recognizedText.value = event.text; recognizedText.value = event.text;
Logger.info('处理语音识别中间结果: $event.text');
// 只有当TTS正在播放或AI响应流正在接收时才中断
if (_ttsService.isPlaying || _aiResponseSubscription != null) {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
} }
break; break;
case RecognitionEventType.finalResult: case RecognitionEventType.finalResult:
// 最终结果 // 最终结果
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
recognizedText.value = event.text; recognizedText.value = event.text;
Logger.info('处理语音识别最终结果: $event.text');
// 处理识别到的文本 // 处理识别到的文本
_processRecognizedText(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<void> _processRecognizedText(String text) async { Future<void> _processRecognizedText(String text) async {
if (text.trim().isEmpty || _currentAgent == null) return; if (text.trim().isEmpty || _currentAgent == null) return;
try { try {
// 在处理新的文本前,确保中断任何正在进行的响应
_interruptCurrentResponse("处理新的用户输入,中断当前响应");
// 添加用户消息到历史记录 // 添加用户消息到历史记录
_addMessageToHistory('user', text); _addMessageToHistory('user', text);
// 启动TTS会话,使用当前Agent的voice // 重置取消标志
_ttsService.startSession(_currentAgent!.voice);
// 获取AI响应
String fullResponse = ''; String fullResponse = '';
// 启动TTS会话,使用当前Agent的voice
await _ttsService.startSession(_currentAgent!.voice);
// 获取AI响应流 // 获取AI响应流
final responseStream = _aiService.sendMessageStream( final responseStream = _aiService.sendMessageStream(
messages: messageHistory, messages: messageHistory,
@ -250,7 +280,7 @@ class BackgroundAgentService extends GetxService {
(chunk) { (chunk) {
fullResponse += chunk; fullResponse += chunk;
// 如果TTS启用,发送到TTS // 发送到TTS
_ttsService.speak(chunk); _ttsService.speak(chunk);
// 重置无活动定时器 // 重置无活动定时器
@ -263,28 +293,37 @@ class BackgroundAgentService extends GetxService {
// 结束TTS会话 // 结束TTS会话
_ttsService.endSession(); _ttsService.endSession();
// 清除AI响应流订阅引用
_aiResponseSubscription = null;
// 注意:我们不再在这里重启语音识别,因为它应该已经在运行 // 注意:我们不再在这里重启语音识别,因为它应该已经在运行
}, },
onError: (error) { onError: (error) {
Logger.error('AI响应错误', error); Logger.error('AI响应错误', error);
// 结束TTS会话
_ttsService.endSession();
// 播放错误消息 // 播放错误消息
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speak(errorMessage); _ttsService.speakOnce(errorMessage);
// 添加错误消息到历史记录 // 添加错误消息到历史记录
_addMessageToHistory('assistant', errorMessage); _addMessageToHistory('assistant', errorMessage);
// 结束TTS会话 // 清除AI响应流订阅引用
_ttsService.endSession(); _aiResponseSubscription = null;
}, },
); );
} catch (e) { } catch (e) {
Logger.error('处理识别文本失败', e); Logger.error('处理识别文本失败', e);
// 结束TTS会话
_ttsService.endSession();
// 播放错误消息 // 播放错误消息
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speak(errorMessage); _ttsService.speakOnce(errorMessage);
// 添加错误消息到历史记录 // 添加错误消息到历史记录
_addMessageToHistory('assistant', 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<void> respondToNewMediaEvent() async { Future<void> respondToNewMediaEvent() async {
// 不管当前状态如何,总是中断当前响应 // 不管当前状态如何,总是中断当前响应
@ -426,9 +451,8 @@ class BackgroundAgentService extends GetxService {
// 回答"我在听" // 回答"我在听"
const listeningMessage = "我在听"; const listeningMessage = "我在听";
_ttsService.speakSingle(listeningMessage); await _ttsService.speakOnce(listeningMessage);
// 不需要重启语音识别,因为它应该已经在运行
// 如果识别会话已经终止,确保重新启动 // 如果识别会话已经终止,确保重新启动
if (!isListening.value) { if (!isListening.value) {
await _restartVoiceRecognition(); await _restartVoiceRecognition();
@ -448,9 +472,10 @@ class BackgroundAgentService extends GetxService {
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_aiResponseSubscription?.cancel(); _aiResponseSubscription?.cancel();
// 停止TTS // 停止TTS并结束会话
if (_ttsService.isPlaying) { if (_ttsService.isPlaying) {
_ttsService.stop(); _ttsService.stop();
_ttsService.endSession();
} }
// 停止语音识别 // 停止语音识别

2
lib/data/services/flutter_tts_service.dart

@ -241,7 +241,7 @@ class FlutterTtsService extends GetxService {
if (text.isEmpty) return; if (text.isEmpty) return;
_textQueue.add(text); _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}"');
} }
/// 开始处理队列(如果尚未开始) /// 开始处理队列(如果尚未开始)

6
lib/data/services/volcano_asr_api_service.dart

@ -271,7 +271,7 @@ class VolcanoAsrApiService extends GetxService {
// 序列化并压缩payload // 序列化并压缩payload
final String payloadStr = jsonEncode(payload); final String payloadStr = jsonEncode(payload);
Logger.info('发送全客户端请求: $payloadStr'); // Logger.info('发送全客户端请求: $payloadStr');
final List<int> payloadBytes = await _gzipCompress(utf8.encode(payloadStr)); final List<int> payloadBytes = await _gzipCompress(utf8.encode(payloadStr));
// 增加序列号 // 增加序列号
@ -552,7 +552,7 @@ class VolcanoAsrApiService extends GetxService {
text: text, text: text,
)); ));
Logger.info('添加最终识别结果: $text'); // Logger.info('添加最终识别结果: $text');
// 重要:在这里不要断开连接,而是继续监听下一句话 // 重要:在这里不要断开连接,而是继续监听下一句话
// 但可以清空当前的中间结果状态,准备接收新的语音输入 // 但可以清空当前的中间结果状态,准备接收新的语音输入
@ -576,7 +576,7 @@ class VolcanoAsrApiService extends GetxService {
text: text, text: text,
)); ));
Logger.info('添加中间识别结果: $text'); // Logger.info('添加中间识别结果: $text');
} }
} }
} }

136
lib/data/services/volcano_tts_api_service.dart

@ -23,7 +23,7 @@ import '../../core/utils/logger.dart';
/// 2. 调用 synthesize 合成文本 /// 2. 调用 synthesize 合成文本
/// 3. 调用 endSession 结束会话 /// 3. 调用 endSession 结束会话
/// ///
/// 或者使用 speakSingle 一次性完成上述流程 /// 或者使用 speakOnce 一次性完成上述流程
class VolcanoTtsApiService extends GetxController { class VolcanoTtsApiService extends GetxController {
// 单例实例 // 单例实例
static VolcanoTtsApiService? _instance; static VolcanoTtsApiService? _instance;
@ -34,6 +34,9 @@ class VolcanoTtsApiService extends GetxController {
return _instance!; return _instance!;
} }
// 会话状态管理 - 只保留基本状态标志
bool _isSessionActive = false;
// 私有构造函数 // 私有构造函数
VolcanoTtsApiService._internal() : VolcanoTtsApiService._internal() :
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '', _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
@ -112,9 +115,6 @@ class VolcanoTtsApiService extends GetxController {
final _eventController = StreamController<Map<String, dynamic>>.broadcast(); final _eventController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get eventStream => _eventController.stream; Stream<Map<String, dynamic>> get eventStream => _eventController.stream;
// 控制器状态
bool _isDisposed = false;
// 当前状态 // 当前状态
final isConnecting = false.obs; final isConnecting = false.obs;
final isConnected = false.obs; final isConnected = false.obs;
@ -358,7 +358,7 @@ class VolcanoTtsApiService extends GetxController {
onError: (error) { onError: (error) {
// 直接处理错误,不尝试重连 // 直接处理错误,不尝试重连
Logger.error('WebSocket错误: $error'); Logger.error('WebSocket错误: $error');
if (!_isDisposed && !_eventController.isClosed) { if (!_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'error', 'eventType': 'error',
'error': error.toString() 'error': error.toString()
@ -384,7 +384,7 @@ class VolcanoTtsApiService extends GetxController {
// 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常 // 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常
try { try {
if (!_isDisposed && !_eventController.isClosed) { if (!_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'connectionFailed', 'eventType': 'connectionFailed',
'error': '连接超时' 'error': '连接超时'
@ -401,7 +401,6 @@ class VolcanoTtsApiService extends GetxController {
final subscription = eventStream.listen((event) { final subscription = eventStream.listen((event) {
if (event['eventType'] == 'connectionStarted') { if (event['eventType'] == 'connectionStarted') {
if (!completer.isCompleted) { if (!completer.isCompleted) {
Logger.info('火山TTS连接成功');
completer.complete(true); completer.complete(true);
timeout?.cancel(); timeout?.cancel();
timeout = null; timeout = null;
@ -423,7 +422,6 @@ class VolcanoTtsApiService extends GetxController {
if (result) { if (result) {
_isConnected = true; _isConnected = true;
isConnected.value = true; isConnected.value = true;
Logger.info('火山TTS连接成功完成');
} }
return result; return result;
@ -444,18 +442,23 @@ class VolcanoTtsApiService extends GetxController {
/// ///
/// 返回会话是否成功开始 /// 返回会话是否成功开始
Future<bool> startSession(String speaker) async { Future<bool> startSession(String speaker) async {
Logger.info('开始TTS会话'); // 如果会话已经活跃,直接返回成功
if (_isSessionActive && _sessionId != null) {
Logger.info('TTS会话已存在,跳过创建');
return true;
}
try { try {
if (!_isConnected) { if (!_isConnected) {
final connected = await connect(); final connected = await connect();
Logger.info('TTS未连接状态, 开始连接, 连接结果: $connected');
if (!connected) return false; if (!connected) return false;
} }
// 生成会话ID // 生成会话ID
_sessionId = const Uuid().v4().replaceAll('-', ''); _sessionId = const Uuid().v4().replaceAll('-', '');
Logger.info('开始TTS会话: ${_sessionId!}');
// 发送开始会话事件 // 发送开始会话事件
await _startTtsSession(speaker); await _startTtsSession(speaker);
@ -486,7 +489,6 @@ class VolcanoTtsApiService extends GetxController {
final subscription = eventStream.listen((event) { final subscription = eventStream.listen((event) {
if (event['eventType'] == 'sessionStarted') { if (event['eventType'] == 'sessionStarted') {
if (!completer.isCompleted) { if (!completer.isCompleted) {
Logger.info('火山TTS会话开始成功');
completer.complete(true); completer.complete(true);
timeout?.cancel(); timeout?.cancel();
timeout = null; timeout = null;
@ -504,9 +506,10 @@ class VolcanoTtsApiService extends GetxController {
// 等待会话结果 // 等待会话结果
final result = await completer.future; final result = await completer.future;
subscription.cancel(); subscription.cancel();
if (result) { if (result) {
Logger.info('火山TTS会话已完全建立'); // 更新会话状态
_isSessionActive = true;
} }
return result; return result;
@ -532,9 +535,13 @@ class VolcanoTtsApiService extends GetxController {
} }
// 确保会话已开始 // 确保会话已开始
if (_sessionId == null) { if (!_isSessionActive || _sessionId == null) {
Logger.error('TTS会话未开始,请先调用startSession'); // 需要创建新会话
final sessionStarted = await startSession(speaker);
if (!sessionStarted) {
Logger.error('TTS会话开始失败,无法合成文本');
return false; return false;
}
} }
// 设置合成状态 // 设置合成状态
@ -557,11 +564,12 @@ class VolcanoTtsApiService extends GetxController {
/// ///
/// 返回会话是否成功结束 /// 返回会话是否成功结束
Future<bool> endSession() async { Future<bool> endSession() async {
if (_sessionId == null) { if (!_isSessionActive || _sessionId == null) {
return true; // 没有活跃会话 return true; // 没有活跃会话
} }
Logger.info('TTS结束会话');
Logger.info('TTS结束会话: ${_sessionId!}');
try { try {
// 发送结束会话事件 // 发送结束会话事件
await _finishSession(); await _finishSession();
@ -575,7 +583,9 @@ class VolcanoTtsApiService extends GetxController {
completer.complete(true); // 超时也认为成功 completer.complete(true); // 超时也认为成功
} }
}); });
isSynthesizing.value = false;
_sessionId = null;
// 监听会话事件 // 监听会话事件
final subscription = eventStream.listen((event) { final subscription = eventStream.listen((event) {
if (event['eventType'] == 'sessionFinished') { if (event['eventType'] == 'sessionFinished') {
@ -590,13 +600,14 @@ class VolcanoTtsApiService extends GetxController {
final result = await completer.future; final result = await completer.future;
subscription.cancel(); subscription.cancel();
// 清除会话ID // 重置会话状态
_sessionId = null; _isSessionActive = false;
isSynthesizing.value = false;
return result; return result;
} catch (e) { } catch (e) {
_handleError(e); _handleError(e);
// 即使出错,也重置会话状态
_isSessionActive = false;
return false; return false;
} }
} }
@ -610,7 +621,7 @@ class VolcanoTtsApiService extends GetxController {
try { try {
// 如果有活跃会话,先结束会话 // 如果有活跃会话,先结束会话
if (_sessionId != null) { if (_isSessionActive) {
await endSession(); await endSession();
} }
@ -649,6 +660,7 @@ class VolcanoTtsApiService extends GetxController {
_sessionId = null; _sessionId = null;
isConnected.value = false; isConnected.value = false;
isSynthesizing.value = false; isSynthesizing.value = false;
_isSessionActive = false;
return result; return result;
} catch (e) { } catch (e) {
@ -665,6 +677,12 @@ class VolcanoTtsApiService extends GetxController {
return; return;
} }
// 检查控制器是否已关闭
if (_eventController.isClosed || _audioDataController.isClosed) {
Logger.warning('TTS服务控制器已关闭,无法处理消息');
return;
}
// 转换为Uint8List以提高处理效率 // 转换为Uint8List以提高处理效率
final messageData = Uint8List.fromList(message); final messageData = Uint8List.fromList(message);
@ -676,7 +694,7 @@ class VolcanoTtsApiService extends GetxController {
// 处理事件 // 处理事件
final event = response['event'] as int?; final event = response['event'] as int?;
if (event != null && !_isDisposed && !_eventController.isClosed) { if (event != null && !_eventController.isClosed) {
switch (event) { switch (event) {
case eventConnectionStarted: case eventConnectionStarted:
_eventController.add({'eventType': 'connectionStarted'}); _eventController.add({'eventType': 'connectionStarted'});
@ -712,7 +730,7 @@ class VolcanoTtsApiService extends GetxController {
try { try {
_eventController.add({'eventType': 'ttsResponse'}); _eventController.add({'eventType': 'ttsResponse'});
if (!_isDisposed && !_audioDataController.isClosed) { if (!_audioDataController.isClosed) {
_audioDataController.add(payload); _audioDataController.add(payload);
} }
@ -825,7 +843,7 @@ class VolcanoTtsApiService extends GetxController {
} }
} catch (e) { } catch (e) {
// 直接报告错误,不尝试恢复 // 直接报告错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) { if (!_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'playbackError', 'eventType': 'playbackError',
'error': '添加音频到播放列表失败: $e' 'error': '添加音频到播放列表失败: $e'
@ -860,7 +878,7 @@ class VolcanoTtsApiService extends GetxController {
await _audioPlayer.play(); await _audioPlayer.play();
} catch (e) { } catch (e) {
// 直接报告错误,不尝试恢复 // 直接报告错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) { if (!_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'playbackError', 'eventType': 'playbackError',
'error': '开始播放失败: $e' 'error': '开始播放失败: $e'
@ -872,14 +890,18 @@ class VolcanoTtsApiService extends GetxController {
/// 处理错误 /// 处理错误
void _handleError(dynamic error) { void _handleError(dynamic error) {
try { try {
// 只记录错误,不尝试恢复 // 检查控制器是否已关闭
if (!_isDisposed && !_eventController.isClosed) { if (_eventController.isClosed) {
_eventController.add({ Logger.warning('TTS事件控制器已关闭,无法发送错误事件');
'eventType': 'error', return;
'error': error.toString()
});
} }
// 只记录错误,不尝试恢复
_eventController.add({
'eventType': 'error',
'error': error.toString()
});
Logger.error('火山语音TTS错误: $error'); Logger.error('火山语音TTS错误: $error');
} catch (e) { } catch (e) {
// 如果在处理错误时发生异常,记录到日志但不再抛出异常 // 如果在处理错误时发生异常,记录到日志但不再抛出异常
@ -896,9 +918,10 @@ class VolcanoTtsApiService extends GetxController {
_sessionId = null; _sessionId = null;
isConnected.value = false; isConnected.value = false;
isSynthesizing.value = false; isSynthesizing.value = false;
_isSessionActive = false;
// 通知监听者连接已断开 // 通知监听者连接已断开
if (!_isDisposed && !_eventController.isClosed) { if (!_eventController.isClosed) {
_eventController.add({'eventType': 'disconnected'}); _eventController.add({'eventType': 'disconnected'});
} }
} }
@ -1193,7 +1216,6 @@ class VolcanoTtsApiService extends GetxController {
final optional = _createOptional(eventFinishSession, _sessionId, -1); final optional = _createOptional(eventFinishSession, _sessionId, -1);
final payload = utf8.encode('{}'); final payload = utf8.encode('{}');
await _sendEvent(header, optional, payload); await _sendEvent(header, optional, payload);
} }
@ -1290,8 +1312,6 @@ class VolcanoTtsApiService extends GetxController {
/// 释放资源 /// 释放资源
@override @override
void onClose() async { void onClose() async {
_isDisposed = true;
await disconnect(); await disconnect();
// 关闭控制器 // 关闭控制器
@ -1306,6 +1326,10 @@ class VolcanoTtsApiService extends GetxController {
// 释放资源 // 释放资源
_audioPlayer.dispose(); _audioPlayer.dispose();
// 清除单例实例,确保下次获取时创建新实例
_instance = null;
_isSessionActive = false;
super.onClose(); super.onClose();
} }
@ -1337,8 +1361,8 @@ class VolcanoTtsApiService extends GetxController {
// 清空播放列表 // 清空播放列表
await _playlist.clear(); await _playlist.clear();
// 结束当前会话 // // 结束当前会话
await endSession(); // await endSession();
// 清空音频缓冲区 // 清空音频缓冲区
_audioBuffer.clear(); _audioBuffer.clear();
@ -1353,8 +1377,8 @@ class VolcanoTtsApiService extends GetxController {
} }
/// 检查会话是否活跃 /// 检查会话是否活跃
bool isSessionActive() { bool hasActiveSession() {
return _sessionId != null; return _isSessionActive && _sessionId != null;
} }
/// 设置音量 /// 设置音量
@ -1380,16 +1404,13 @@ class VolcanoTtsApiService extends GetxController {
// 这个方法在当前实现中不需要特殊处理 // 这个方法在当前实现中不需要特殊处理
} }
/// 单句合成辅助函数
///
/// 一次性完成连接、会话创建、合成和播放的过程
/// 适合单句或少量文本的快速合成场景 /// 适合单句或少量文本的快速合成场景
/// ///
/// [text] 要合成的文本 /// [text] 要合成的文本
/// [speaker] 发音人,如果为null则使用默认发音人 /// [speaker] 发音人,如果为null则使用默认发音人
/// ///
/// 返回合成是否成功 /// 返回合成是否成功
Future<bool> speakSingle(String text, {String? speaker}) async { Future<bool> speakOnce(String text, {String? speaker}) async {
if (text.isEmpty) { if (text.isEmpty) {
Logger.warning('合成文本为空,跳过合成'); Logger.warning('合成文本为空,跳过合成');
return false; return false;
@ -1409,29 +1430,24 @@ class VolcanoTtsApiService extends GetxController {
} }
} }
// 2. 确保会话已开始 // 2. 开始会话
final bool needNewSession = _sessionId == null; final sessionStarted = await startSession(actualSpeaker);
if (needNewSession) { if (!sessionStarted) {
Logger.info('TTS会话未开始,开始会话'); Logger.error('TTS会话开始失败,无法合成文本');
final sessionStarted = await startSession(actualSpeaker); return false;
if (!sessionStarted) {
Logger.error('TTS会话开始失败,无法合成文本');
return false;
}
} }
// 3. 合成文本 // 3. 合成文本
isSynthesizing.value = true; isSynthesizing.value = true;
final success = await synthesize(text, actualSpeaker); final success = await synthesize(text, actualSpeaker);
// 如果是我们开始的新会话,则结束会话 endSession();
if (needNewSession) {
await endSession();
}
return success; return success;
} catch (e) { } catch (e) {
_handleError(e); Logger.error('TTS一次性合成失败: $e');
// 确保会话结束
endSession();
return false; return false;
} }
} }

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

@ -39,9 +39,7 @@ class ChatController extends GetxController {
final RxString recognizedText = ''.obs; final RxString recognizedText = ''.obs;
final RxBool isListening = false.obs; final RxBool isListening = false.obs;
// 临时消息ID,用于语音识别中的消息 // 临时消息索引,用于更新临时消息
String? _tempRecognitionMessageId;
// 临时消息的索引,用于更新临时消息
int _tempMessageIndex = -1; int _tempMessageIndex = -1;
// 流式响应控制 // 流式响应控制
@ -54,7 +52,8 @@ class ChatController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
Logger.info('ChatController onInit');
// 从路由参数获取Agent信息 // 从路由参数获取Agent信息
final Map<String, dynamic> args = Get.arguments ?? {}; final Map<String, dynamic> args = Get.arguments ?? {};
final String agentId = args['agentId'] ?? 'personal_assistant'; final String agentId = args['agentId'] ?? 'personal_assistant';
@ -79,16 +78,14 @@ class ChatController extends GetxController {
content: agent.welcomeMessage, content: agent.welcomeMessage,
timestamp: DateTime.now(), timestamp: DateTime.now(),
)); ));
// 如果启用了TTS,播放欢迎消息 // 如果启用了TTS,播放欢迎消息
if (isTtsEnabled.value) { if (isTtsEnabled.value) {
_ttsService.speakSingle(agent.welcomeMessage); _ttsService.speak(agent.welcomeMessage);
} }
} }
// 如果playVoiceOnEnter为true,自动激活语音识别 // 如果playVoiceOnEnter为true,自动激活语音识别
if (args['playVoiceOnEnter'] == true) { if (args['playVoiceOnEnter'] == true) {
startVoiceRecognition(); startVoiceRecognition();
} }
@ -100,19 +97,19 @@ class ChatController extends GetxController {
@override @override
void onClose() { void onClose() {
// 关闭ScrollController Logger.info('ChatController onClose');
// 清理资源
scrollController.dispose(); scrollController.dispose();
// 取消所有订阅
_aiResponseSubscription?.cancel(); _aiResponseSubscription?.cancel();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
// 停止TTS // 停止TTS并结束会话
if (_ttsService.isPlaying) { if (_ttsService.isPlaying) {
_ttsService.stop(); _ttsService.stop();
_ttsService.endSession();
} }
// 停止语音识别
if (isVoiceRecognitionActive.value) { if (isVoiceRecognitionActive.value) {
_asrService.stopRecognition(); _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<void> sendMessage(String text) async { Future<void> sendMessage(String text) async {
if (text.trim().isEmpty) return; if (text.trim().isEmpty) return;
// 停止TTS // 中断当前响应
if (_ttsService.isPlaying) { _interruptCurrentResponse("发送新消息,中断当前响应");
_ttsService.stop();
}
// 添加用户消息 // 添加用户消息
final userMessage = Message( final userMessage = Message(
@ -151,9 +176,7 @@ class ChatController extends GetxController {
timestamp: DateTime.now(), timestamp: DateTime.now(),
); );
messages.add(userMessage); messages.add(userMessage);
_scrollToBottom();
// 滚动到底部显示用户消息
Future.delayed(Duration(milliseconds: 100), _scrollToBottom);
// 清空输入框 // 清空输入框
inputText.value = ''; inputText.value = '';
@ -161,37 +184,22 @@ class ChatController extends GetxController {
// 添加加载中的消息 // 添加加载中的消息
final loadingMessage = Message.loading(); final loadingMessage = Message.loading();
messages.add(loadingMessage); messages.add(loadingMessage);
_scrollToBottom();
// 再次滚动到底部显示加载中的消息
Future.delayed(Duration(milliseconds: 100), _scrollToBottom);
isSending.value = true; isSending.value = true;
try { try {
// 构建消息历史 // 构建消息历史
final List<Map<String, String>> messageHistory = []; final List<Map<String, String>> messageHistory = _buildMessageHistory();
// 只取最近的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,
});
}
// 重置取消标志 // 重置取消标志
_shouldCancelAiResponse = false; _shouldCancelAiResponse = false;
// 准备TTS会话 // 启动TTS会话(仅当TTS启用时)
if (isTtsEnabled.value) { if (isTtsEnabled.value) {
await _ttsService.startSession(agent.voice); await _ttsService.startSession(agent.voice);
} }
String fullResponse = ''; String fullResponse = '';
// 获取AI响应流 // 获取AI响应流
@ -207,96 +215,123 @@ class ChatController extends GetxController {
fullResponse += chunk; fullResponse += chunk;
// 更新加载中的消息内容,但保持isLoading状态 // 更新加载中的消息内容
final index = messages.indexWhere((m) => m.isLoading); _updateLoadingMessage(fullResponse);
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);
}
// 如果TTS启用,发送到TTS // 如果TTS启用,发送到TTS
if (isTtsEnabled.value && !_shouldCancelAiResponse) { if (isTtsEnabled.value) {
// 直接将文本块发送到TTS,服务会自己处理断句
_ttsService.speak(chunk); _ttsService.speak(chunk);
} }
}, },
onDone: () { onDone: () {
if (_shouldCancelAiResponse) return; if (_shouldCancelAiResponse) return;
// 完成响应,此时才将isLoading设为false // 结束TTS会话(仅当TTS启用时)
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会话
if (isTtsEnabled.value) { if (isTtsEnabled.value) {
_ttsService.endSession(); _ttsService.endSession();
} }
isSending.value = false; // 完成响应,将isLoading设为false
_finalizeResponse(fullResponse);
// 保存聊天历史 // 保存聊天历史
_saveChatHistory(); _saveChatHistory();
}, },
onError: (error) { onError: (error) {
Logger.error('AI响应错误', error); Logger.error('AI响应错误', error);
// 更新加载中的消息为错误消息 // 结束TTS会话(仅当TTS启用时)
final index = messages.indexWhere((m) => m.isLoading);
if (index != -1) {
messages[index] = messages[index].copyWith(
content: '抱歉,我遇到了一些问题,无法回应您的请求。',
isLoading: false,
);
}
// 结束TTS会话
if (isTtsEnabled.value) { if (isTtsEnabled.value) {
_ttsService.endSession(); _ttsService.endSession();
} }
isSending.value = false; // 更新加载中的消息为错误消息
_handleResponseError();
}, },
); );
} catch (e) { } catch (e) {
Logger.error('发送消息失败', e); Logger.error('发送消息失败', e);
// 更新加载中的消息为错误消息 // 结束TTS会话(仅当TTS启用时)
final index = messages.indexWhere((m) => m.isLoading); if (isTtsEnabled.value) {
if (index != -1) { _ttsService.endSession();
messages[index] = messages[index].copyWith(
content: '抱歉,我遇到了一些问题,无法回应您的请求。',
isLoading: false,
);
} }
_handleResponseError();
}
}
isSending.value = false; // 构建消息历史
List<Map<String, String>> _buildMessageHistory() {
final List<Map<String, String>> 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状态 // 切换TTS状态
void toggleTts() { void toggleTts() {
isTtsEnabled.toggle(); isTtsEnabled.toggle();
// 如果关闭TTS,停止当前播放 // 如果关闭TTS,停止当前播放并结束会话
if (!isTtsEnabled.value && _ttsService.isPlaying) { if (!isTtsEnabled.value && _ttsService.isPlaying) {
_ttsService.stop(); _ttsService.stop();
_ttsService.endSession();
} }
} }
@ -304,23 +339,6 @@ class ChatController extends GetxController {
Future<void> startVoiceRecognition() async { Future<void> startVoiceRecognition() async {
if (isVoiceRecognitionActive.value) return; 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 { try {
// 启动语音识别 // 启动语音识别
final success = await _asrService.startRecognition(); final success = await _asrService.startRecognition();
@ -332,115 +350,11 @@ class ChatController extends GetxController {
// 重置临时消息索引 // 重置临时消息索引
_tempMessageIndex = -1; _tempMessageIndex = -1;
_tempRecognitionMessageId = null;
// 监听识别事件 // 监听识别事件
_recognitionSubscription = _asrService.recognitionStream.listen( _recognitionSubscription = _asrService.recognitionStream.listen(
(event) { _handleRecognitionEvent,
switch (event.type) { onError: _handleRecognitionError,
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();
},
); );
} else { } else {
Logger.error('启动语音识别失败'); 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<void> stopVoiceRecognition() async { Future<void> stopVoiceRecognition() async {
if (!isVoiceRecognitionActive.value) return; if (!isVoiceRecognitionActive.value) return;
@ -472,9 +491,8 @@ class ChatController extends GetxController {
} }
} }
// 重置临时消息索引和ID // 重置临时消息索引
_tempMessageIndex = -1; _tempMessageIndex = -1;
_tempRecognitionMessageId = null;
} catch (e) { } catch (e) {
Logger.error('停止语音识别失败', e); Logger.error('停止语音识别失败', e);
} }
@ -515,8 +533,6 @@ class ChatController extends GetxController {
void addMessage(Message message) { void addMessage(Message message) {
messages.add(message); messages.add(message);
_saveChatHistory(); _saveChatHistory();
_scrollToBottom();
// 滚动到底部显示新添加的消息
Future.delayed(Duration(milliseconds: 100), _scrollToBottom);
} }
} }

15
lib/modules/profile/views/profile_view.dart

@ -119,7 +119,16 @@ class ProfileView extends GetView<ProfileController> {
Get.toNamed(Routes.TTS_TEST); 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(), const Divider(),
_buildMenuItem( _buildMenuItem(
@ -130,13 +139,13 @@ class ProfileView extends GetView<ProfileController> {
Get.toNamed(Routes.VOLCANO_ASR_TEST); Get.toNamed(Routes.VOLCANO_ASR_TEST);
}, },
), ),
const Divider(), const Divider(),
_buildMenuItem( _buildMenuItem(
title: 'Azure语音识别测试'.tr, title: 'Azure语音识别测试'.tr,
icon: Icons.mic, icon: Icons.mic,
subtitle: '测试Azure语音ASR功能', subtitle: '测试Azure语音ASR功能',
onTap: () { onTap: () {
Get.toNamed(Routes.ASR_TEST); Get.toNamed(Routes.AZURE_ASR_TEST);
}, },
), ),
const Divider(), const Divider(),

11
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>(
() => AzureTtsTestController(),
);
}
}

12
lib/modules/test/bindings/test_binding.dart

@ -1,9 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/tts_test_controller.dart'; import '../controllers/volcano_tts_test_controller.dart';
import '../controllers/asr_test_controller.dart'; import '../controllers/azure_asr_test_controller.dart';
import '../../../data/services/volcano_tts_api_service.dart'; import '../../../data/services/volcano_tts_api_service.dart';
class TtsTestBinding extends Bindings { class VolcanoTtsTestBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Make sure the VolcanoTtsApiService is registered // 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 // Use put instead of lazyPut to ensure the controller is created immediately
Get.put<TtsTestController>(TtsTestController()); Get.put<VolcanoTtsTestController>(VolcanoTtsTestController());
} }
} }
class AsrTestBinding extends Bindings { class AzureAsrTestBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// Use put instead of lazyPut to ensure the controller is created immediately // Use put instead of lazyPut to ensure the controller is created immediately
Get.put<AsrTestController>(AsrTestController()); Get.put<AzureAsrTestController>(AzureAsrTestController());
} }
} }

2
lib/modules/test/controllers/asr_test_controller.dart → 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 '../../../data/services/azure_asr_service.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
class AsrTestController extends GetxController { class AzureAsrTestController extends GetxController {
final AzureAsrService _azureService = Get.find<AzureAsrService>(); final AzureAsrService _azureService = Get.find<AzureAsrService>();
// 可观察状态 // 可观察状态

315
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<AzureTtsService>();
// 可观察状态
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<Map<String, String>> 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<String> speechTexts = <String>[
'这是第一条播报测试,请听取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<void> 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<void> 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<void> 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();
}
}
}

2
lib/modules/test/controllers/tts_test_controller.dart → lib/modules/test/controllers/volcano_tts_test_controller.dart

@ -4,7 +4,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../../data/services/volcano_tts_api_service.dart'; import '../../../data/services/volcano_tts_api_service.dart';
class TtsTestController extends GetxController { class VolcanoTtsTestController extends GetxController {
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>(); final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
// 可观察状态 // 可观察状态

6
lib/modules/test/views/asr_test_view.dart → lib/modules/test/views/azure_asr_test_view.dart

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/asr_test_controller.dart'; import '../controllers/azure_asr_test_controller.dart';
class AsrTestView extends GetView<AsrTestController> { class AzureAsrTestView extends GetView<AzureAsrTestController> {
const AsrTestView({Key? key}) : super(key: key); const AzureAsrTestView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

280
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<AzureTtsTestController> {
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()),
],
),
),
),
],
),
),
),
);
}
}

8
lib/modules/test/views/tts_test_view.dart → lib/modules/test/views/volcano_tts_test_view.dart

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/tts_test_controller.dart'; import '../controllers/volcano_tts_test_controller.dart';
class TtsTestView extends GetView<TtsTestController> { class VolcanoTtsTestView extends GetView<VolcanoTtsTestController> {
const TtsTestView({Key? key}) : super(key: key); const VolcanoTtsTestView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -15,7 +15,7 @@ class TtsTestView extends GetView<TtsTestController> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('语音合成测试'), title: const Text('火山语音合成测试'),
centerTitle: true, centerTitle: true,
), ),
body: SingleChildScrollView( body: SingleChildScrollView(

2
lib/modules/translation/controllers/translation_controller.dart

@ -386,7 +386,7 @@ class TranslationController extends GetxController {
if (text.isEmpty) return; if (text.isEmpty) return;
try { try {
await _ttsService.speakSingle(text); await _ttsService.speakOnce(text);
} catch (e) { } catch (e) {
Get.snackbar('Error', '播放翻译失败: ${e.toString()}', Get.snackbar('Error', '播放翻译失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM); snackPosition: SnackPosition.BOTTOM);

1
pubspec.yaml

@ -28,6 +28,7 @@ dependencies:
intl: ^0.19.0 intl: ^0.19.0
crypto: ^3.0.3 crypto: ^3.0.3
flutter_tts: ^4.2.2 flutter_tts: ^4.2.2
logger: ^2.0.2+1
window_size: window_size:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git

Loading…
Cancel
Save