Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
efb8df2fea
  1. 4
      android/app/build.gradle.kts
  2. 351
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  3. 364
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  4. 59
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt
  5. 328
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt
  6. 13
      android/settings.gradle.kts
  7. 45
      lib/core/bindings/initial_binding.dart
  8. 7
      lib/core/routes/app_pages.dart
  9. 1
      lib/core/routes/app_routes.dart
  10. 35
      lib/core/theme/app_colors.dart
  11. 80
      lib/core/theme/text_styles.dart
  12. 376
      lib/data/services/azure_asr_service.dart
  13. 151
      lib/data/services/background_agent_service.dart
  14. 281
      lib/data/services/voice_recognition_service.dart
  15. 23
      lib/data/services/volcano_asr_service.dart
  16. 1570
      lib/data/services/volcano_tts_api_service.dart
  17. 712
      lib/data/services/volcano_tts_service.dart
  18. 6
      lib/modules/chat/bindings/chat_binding.dart
  19. 148
      lib/modules/chat/controllers/chat_controller.dart
  20. 69
      lib/modules/chat/controllers/voice_input_controller.dart
  21. 12
      lib/modules/chat/views/chat_view.dart
  22. 8
      lib/modules/explore/views/explore_view.dart
  23. 4
      lib/modules/profile/views/profile_view.dart
  24. 6
      lib/modules/test/bindings/test_binding.dart
  25. 53
      lib/modules/test/controllers/asr_test_controller.dart
  26. 309
      lib/modules/test/controllers/tts_test_controller.dart
  27. 2
      lib/modules/test/views/asr_test_view.dart
  28. 219
      lib/modules/test/views/tts_test_view.dart
  29. 9
      lib/modules/translation/bindings/translation_binding.dart
  30. 334
      lib/modules/translation/controllers/translation_controller.dart
  31. 13
      lib/modules/translation/models/translation_message_model.dart
  32. 302
      lib/modules/translation/views/translation_view.dart
  33. 82
      lib/modules/translation/widgets/translation_message_item.dart
  34. 259
      lib/tools/check_volcano_asr_config.dart

4
android/app/build.gradle.kts

@ -52,8 +52,8 @@ android {
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// Microsoft 语音识别SDK(可以移除,因为我们现在使用火山语音识别SDK)
// implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.42.0")
// Microsoft 语音识别SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.42.0")
// // 添加火山语音合成SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")

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

@ -0,0 +1,351 @@
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, "连续识别已开始")
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) {
callback.onError("SpeechRecognizer 未初始化")
return false
}
if (!isContinuousRecognitionActive) {
callback.onError("连续识别未在进行中")
return false
}
try {
// 停止连续识别
recognizer?.stopContinuousRecognitionAsync()
// 等待停止完成
stopRecognitionSemaphore.acquire()
isContinuousRecognitionActive = false
Log.d(TAG, "连续识别已停止")
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)
}
}

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

@ -13,22 +13,33 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel
class MainActivity: AudioServiceActivity() {
private val SPEECH_RECOGNITION_CHANNEL = "com.example.deep_voice/speech_recognition"
private val SPEECH_RECOGNITION_EVENT_CHANNEL = "com.example.deep_voice/speech_recognition_events"
private val SPEECH_RECOGNITION_CHANNEL = "com.example.deep_voice/volcano_asr"
private val SPEECH_RECOGNITION_EVENT_CHANNEL = "com.example.deep_voice/volcano_events"
private val VOLCANO_TTS_CHANNEL = "com.example.deep_voice/volcano_tts"
private val VOLCANO_TTS_EVENT_CHANNEL = "com.example.deep_voice/volcano_tts_events"
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 TAG = "MainActivity"
private lateinit var speechHelper: SpeechRecognitionHelper
private lateinit var speechHelper: VolcanoAsrHelper
private lateinit var volcanoTtsHelper: VolcanoTtsHelper
private lateinit var azureAsrHelper: AzureAsrHelper
private var eventSink: EventChannel.EventSink? = null
private var azureEventSink: EventChannel.EventSink? = null
private var ttsEventSink: EventChannel.EventSink? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize speechHelper with context
speechHelper = SpeechRecognitionHelper(applicationContext)
speechHelper = VolcanoAsrHelper(applicationContext)
// Initialize volcanoTtsHelper with context
volcanoTtsHelper = VolcanoTtsHelper(applicationContext)
// 初始化 Azure ASR 助手
azureAsrHelper = AzureAsrHelper()
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
@ -55,7 +66,7 @@ class MainActivity: AudioServiceActivity() {
Log.d(TAG, "资源ID: ${resourceId ?: subscriptionKey}")
Log.d(TAG, "集群区域: $cluster")
val success = speechHelper.initialize(subscriptionKey, serviceRegion, cluster)
val success = speechHelper.initialize(subscriptionKey, serviceRegion, cluster, resourceId)
Log.d(TAG, "语音识别服务初始化${if (success) "成功" else "失败"}")
result.success(success)
} catch (e: Exception) {
@ -65,7 +76,7 @@ class MainActivity: AudioServiceActivity() {
}
"recognizeOnce" -> {
try {
speechHelper.recognizeOnce(object : SpeechRecognitionHelper.RecognizeCallback {
speechHelper.recognizeOnce(object : VolcanoAsrHelper.RecognizeCallback {
override fun onResult(text: String) {
result.success(text)
}
@ -85,7 +96,7 @@ class MainActivity: AudioServiceActivity() {
return@setMethodCallHandler
}
val success = speechHelper.startContinuousRecognition(object : SpeechRecognitionHelper.ContinuousRecognizeCallback {
val success = speechHelper.startContinuousRecognition(object : VolcanoAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) {
sendEvent(mapOf(
"eventType" to "finalResult",
@ -139,7 +150,7 @@ class MainActivity: AudioServiceActivity() {
return@setMethodCallHandler
}
val success = speechHelper.stopContinuousRecognition(object : SpeechRecognitionHelper.ContinuousRecognizeCallback {
val success = speechHelper.stopContinuousRecognition(object : VolcanoAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) {}
override fun onRecognizing(recognizing: String) {}
override fun onSessionStarted() {}
@ -188,6 +199,43 @@ class MainActivity: AudioServiceActivity() {
try {
val success = volcanoTtsHelper.initialize(appId, token, cluster)
// 设置播放状态回调
volcanoTtsHelper.setPlaybackCallback(object : VolcanoTtsHelper.PlaybackCallback {
override fun onPlaybackStarted() {
sendTtsEvent(mapOf(
"eventType" to "playbackStarted"
))
}
override fun onPlaybackCompleted() {
sendTtsEvent(mapOf(
"eventType" to "playbackCompleted"
))
}
override fun onPlaybackError(error: String) {
sendTtsEvent(mapOf(
"eventType" to "playbackError",
"error" to error
))
}
// 实现合成开始回调
override fun onSynthesisBegin() {
sendTtsEvent(mapOf(
"eventType" to "synthesisBegin"
))
}
// 实现合成结束回调
override fun onSynthesisEnd() {
sendTtsEvent(mapOf(
"eventType" to "synthesisEnd"
))
}
})
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
@ -202,15 +250,18 @@ class MainActivity: AudioServiceActivity() {
return@setMethodCallHandler
}
volcanoTtsHelper.synthesize(text, voiceType, object : VolcanoTtsHelper.VolcanoTtsCallback {
override fun onSuccess(audioData: ByteArray) {
result.success(audioData)
// Since synthesize method was removed, we'll use speakText instead
// and inform the caller that we no longer support direct audio data return
try {
val success = volcanoTtsHelper.speakText(text, voiceType)
if (success) {
result.success(ByteArray(0)) // Return empty byte array as placeholder
} else {
result.error("TTS_ERROR", "Failed to synthesize text", null)
}
override fun onError(error: String) {
result.error("TTS_ERROR", error, null)
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
})
}
"synthesizeSync" -> {
val text = call.argument<String>("text")
@ -221,10 +272,12 @@ class MainActivity: AudioServiceActivity() {
return@setMethodCallHandler
}
// Since synthesizeSync method was removed, we'll use speakText instead
// and inform the caller that we no longer support direct audio data return
try {
val audioData = volcanoTtsHelper.synthesizeSync(text, voiceType)
if (audioData != null) {
result.success(audioData)
val success = volcanoTtsHelper.speakText(text, voiceType)
if (success) {
result.success(ByteArray(0)) // Return empty byte array as placeholder
} else {
result.error("TTS_ERROR", "Failed to synthesize text", null)
}
@ -232,6 +285,74 @@ class MainActivity: AudioServiceActivity() {
result.error("TTS_ERROR", e.message, null)
}
}
"speakText" -> {
val text = call.argument<String>("text")
val voiceType = call.argument<String>("voiceType")
if (text == null || voiceType == null) {
result.error("INVALID_ARGUMENTS", "text and voiceType are required", null)
return@setMethodCallHandler
}
try {
Log.d(TAG, "准备播放文本: $text")
// 先停止当前播放
try {
volcanoTtsHelper.stopSpeaking()
// 添加短暂延迟,确保引擎状态已重置
Thread.sleep(100)
} catch (e: Exception) {
Log.e(TAG, "停止当前播放失败: ${e.message}")
// 继续尝试新的播放
}
val success = volcanoTtsHelper.speakText(text, voiceType)
Log.d(TAG, if (success) "成功开始播放" else "播放失败")
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "播放文本失败", e)
result.error("SPEAK_ERROR", e.message, null)
}
}
"stopSpeaking" -> {
try {
val success = volcanoTtsHelper.stopSpeaking()
result.success(success)
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"setSpeed" -> {
val speed = call.argument<Double>("speed")
if (speed == null) {
result.error("INVALID_ARGUMENTS", "speed is required", null)
return@setMethodCallHandler
}
try {
val success = volcanoTtsHelper.setSpeed(speed.toFloat())
result.success(success)
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"setVolume" -> {
val volume = call.argument<Double>("volume")
if (volume == null) {
result.error("INVALID_ARGUMENTS", "volume is required", null)
return@setMethodCallHandler
}
try {
val success = volcanoTtsHelper.setVolume(volume.toFloat())
result.success(success)
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"dispose" -> {
try {
volcanoTtsHelper.dispose()
@ -246,6 +367,174 @@ class MainActivity: AudioServiceActivity() {
}
}
// 设置 Azure ASR 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey")
val serviceRegion = call.argument<String>("serviceRegion")
val language = call.argument<String>("language") ?: "zh-CN"
if (subscriptionKey == null || serviceRegion == null) {
result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", 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) {
result.error("STOP_ERROR", error, null)
return
}
})
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"resetRecognizer" -> {
try {
val success = azureAsrHelper.resetRecognizer()
result.success(success)
} catch (e: Exception) {
result.error("RESET_ERROR", "重置识别器失败: ${e.message}", null)
}
}
"setRecognitionLanguage" -> {
try {
val language = call.argument<String>("language") ?: "zh-CN"
azureAsrHelper.setRecognitionLanguage(language)
result.success(true)
} catch (e: Exception) {
result.error("LANGUAGE_ERROR", e.message, null)
}
}
"dispose" -> {
try {
azureAsrHelper.dispose()
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
}
else -> {
result.notImplemented()
}
}
}
// 设置语音识别事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, SPEECH_RECOGNITION_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
@ -258,6 +547,32 @@ class MainActivity: AudioServiceActivity() {
}
}
)
// 设置 TTS 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOLCANO_TTS_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
}
}
)
// 设置 Azure ASR 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
azureEventSink = events
}
override fun onCancel(arguments: Any?) {
azureEventSink = null
}
}
)
}
private fun sendEvent(event: Map<String, Any?>) {
@ -266,9 +581,22 @@ class MainActivity: AudioServiceActivity() {
}
}
private fun sendTtsEvent(event: Map<String, Any?>) {
runOnUiThread {
ttsEventSink?.success(event)
}
}
private fun sendAzureEvent(event: Map<String, Any?>) {
runOnUiThread {
azureEventSink?.success(event)
}
}
override fun onDestroy() {
speechHelper.dispose()
volcanoTtsHelper.dispose()
azureAsrHelper.dispose()
super.onDestroy()
}
}

59
android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt → android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt

@ -15,8 +15,8 @@ import java.util.concurrent.TimeUnit
*
* 该类封装了火山语音SDK的语音识别功能,提供简单的接口供Flutter调用
*/
class SpeechRecognitionHelper(context: Context) : SpeechEngine.SpeechListener {
private val TAG = "SpeechRecognitionHelper"
class VolcanoAsrHelper(context: Context) : SpeechEngine.SpeechListener {
private val TAG = "VolcanoAsrHelper"
private var mSpeechEngine: SpeechEngine? = null
private var mSpeechEngineHandler: Long = -1
private var isInitialized = false
@ -44,9 +44,10 @@ class SpeechRecognitionHelper(context: Context) : SpeechEngine.SpeechListener {
* @param appId 应用ID
* @param token 应用密钥/令牌
* @param cluster 集群区域
* @param resourceId 资源ID,用于大模型流式识别API鉴权
* @return 初始化是否成功
*/
fun initialize(appId: String, token: String, cluster: String): Boolean {
fun initialize(appId: String, token: String, cluster: String, resourceId: String? = null): Boolean {
try {
// 设置基本参数
this.appId = appId
@ -54,6 +55,7 @@ class SpeechRecognitionHelper(context: Context) : SpeechEngine.SpeechListener {
this.cluster = cluster
Log.d(TAG, "初始化参数: APP_ID=${appId}, APP_KEY长度=${token.length}, 集群区域=${cluster}")
Log.d(TAG, "资源ID: ${resourceId ?: appId}")
// 确保应用上下文已设置
if (applicationContext == null) {
@ -111,28 +113,54 @@ class SpeechRecognitionHelper(context: Context) : SpeechEngine.SpeechListener {
appId
)
// 设置授权信息 - APP_TOKEN (需要添加Bearer;前缀)
// 使用大模型流式识别SDK (API v3)
Log.d(TAG, "使用大模型流式识别SDK配置 (API v3)")
// 设置API路径为大模型流式识别API
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING,
"/api/v3/sauc/bigmodel"
)
// 设置授权信息 - APP_TOKEN (不需要Bearer前缀)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING,
"Bearer;$token" // 添加Bearer;前缀
token
)
// 设置网络配置
// 设置资源ID,用于大模型流式识别API鉴权
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING,
"wss://openspeech.bytedance.com"
SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING,
resourceId
)
// 使用标准API路径
// 设置协议类型为SEED
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT,
SpeechEngineDefines.PROTOCOL_TYPE_SEED
)
// 设置ASR请求参数
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING,
"/api/v2/asr"
SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING,
"{\"force_to_speech_time\":0, \"end_window_size\":800}"
)
// 设置网络配置
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING,
"wss://openspeech.bytedance.com"
)
// 设置集群区域
// 设置集群区域 - 这对于WebSocket连接非常重要
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING,
@ -176,15 +204,16 @@ class SpeechRecognitionHelper(context: Context) : SpeechEngine.SpeechListener {
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING,
SpeechEngineDefines.ASR_RESULT_TYPE_FULL
SpeechEngineDefines.ASR_RESULT_TYPE_SINGLE
)
// 记录所有配置参数,便于调试
Log.d(TAG, "配置参数汇总:")
Log.d(TAG, "APP_ID: $appId")
Log.d(TAG, "APP_TOKEN: Bearer;${token.take(5)}***")
Log.d(TAG, "APP_TOKEN: ${token.take(5)}***")
Log.d(TAG, "CLUSTER: $cluster")
Log.d(TAG, "API_PATH: /api/v2/asr")
Log.d(TAG, "RESOURCE_ID: ${resourceId ?: appId}")
Log.d(TAG, "API_PATH: ${if (resourceId != null) "/api/v3/sauc/bigmodel" else "/api/v2/asr"}")
// 初始化引擎
val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1

328
android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt

@ -5,13 +5,12 @@ import android.util.Log
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* 火山语音合成Helper类 (SDK版本)
* 火山语音合成Helper类
*
* 该类封装了火山语音SDK的TTS功能,提供简单的接口供Flutter调用
* 实现基于官方文档的标准接入流程
*/
class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
private val TAG = "VolcanoTtsHelper"
@ -20,13 +19,8 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
private var isInitialized = false
private var applicationContext: Context? = context.applicationContext
// 同步合成相关变量
private var mSynthesisLatch: CountDownLatch? = null
private var mSynthesisAudioData: ByteArray? = null
private var mSynthesisError: String? = null
// 当前回调
private var mCurrentCallback: VolcanoTtsCallback? = null
// 播放状态回调
private var mPlaybackCallback: PlaybackCallback? = null
/**
* 初始化TTS引擎
@ -39,16 +33,16 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
try {
// 确保应用上下文已设置
if (applicationContext == null) {
Log.e(TAG, "应用上下文未设置,请先调用setContext方法")
Log.e(TAG, "应用上下文未设置")
return false
}
Log.d(TAG, "初始化火山语音TTS引擎,APP_ID: ${appId.take(3)}***,Token长度: ${token.length},集群区域: $cluster")
// 准备环境
// 1. 准备环境
SpeechEngineGenerator.PrepareEnvironment(applicationContext, null)
// 获取语音引擎实例
// 2. 创建引擎实例
mSpeechEngine = SpeechEngineGenerator.getInstance()
mSpeechEngineHandler = mSpeechEngine?.createEngine() ?: -1
@ -59,37 +53,36 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
// 设置上下文
mSpeechEngine?.setContext(applicationContext)
Log.d(TAG, "成功设置应用上下文")
// 设置引擎类型为TTS
// 3. 参数配置
// 3.1 引擎类型
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING,
SpeechEngineDefines.TTS_ENGINE
)
// 设置日志级别
// 3.2 日志级别
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING,
SpeechEngineDefines.LOG_LEVEL_WARN
)
// 设置用户ID (必需)
// 3.3 线上问题定位
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_UID_STRING,
"deep_voice_user"
)
// 设置设备ID (可选)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING,
"deep_voice_device"
)
// 设置授权信息
// 3.4 授权
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING,
@ -101,28 +94,21 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
"Bearer;$token"
)
// 设置集群
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_CLUSTER_STRING,
cluster
)
// 设置合成场景为单次合成
// 3.5 合成场景
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING,
SpeechEngineDefines.TTS_SCENARIO_TYPE_NORMAL
SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL
)
// 设置合成策略为在线合成
// 3.6 合成策略
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_WORK_MODE_INT,
SpeechEngineDefines.TTS_WORK_MODE_ONLINE
)
// 设置在线请求资源配置
// 3.7 在线请求资源配置
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_ADDRESS_STRING,
@ -133,36 +119,41 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
SpeechEngineDefines.PARAMS_KEY_TTS_URI_STRING,
"/api/v1/tts/ws_binary"
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_CLUSTER_STRING,
cluster
)
// 启用播放器
// 3.8 启用内建播放器
mSpeechEngine?.setOptionBoolean(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL,
true
)
// 设置音频流类型为媒体
// 3.9 配置内建播放器的音源
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_AUDIO_STREAM_TYPE_INT,
SpeechEngineDefines.AUDIO_STREAM_TYPE_MEDIA
)
// 设置音频格式为PCM
mSpeechEngine?.setOptionString(
// 3.10 返回音频数据
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
"tts_audio_format",
"pcm"
SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT,
2
)
// 初始化引擎
// 4. 初始化引擎实例
val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "初始化引擎失败: $ret")
return false
}
// 设置监听器
// 设置回调监听器
mSpeechEngine?.setListener(this)
isInitialized = true
@ -176,35 +167,43 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
}
/**
* 合成文本为语音
* 直接合成并播放文本
*
* @param text 要合成的文本
* @param voiceType 语音类型
* @param callback 回调接口,用于返回结果或错误
* @return 是否成功开始播放
*/
fun synthesize(text: String, voiceType: String, callback: VolcanoTtsCallback) {
fun speakText(text: String, voiceType: String): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
callback.onError("TTS引擎尚未初始化")
return
return false
}
try {
Log.d(TAG, "开始合成文本: $text,使用语音类型: $voiceType")
Log.d(TAG, "开始直接播放文本: $text,使用语音类型: $voiceType")
// 先停止当前播放
try {
Log.d(TAG, "停止当前播放")
stopSpeaking()
// 保存回调以便在onMessage中使用
mCurrentCallback = callback
// 添加短暂延迟,确保引擎状态已重置
Thread.sleep(100)
} catch (e: Exception) {
Log.e(TAG, "停止当前播放失败: ${e.message}")
// 继续尝试新的播放
}
// 设置发音人
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING,
voiceType
"other"
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING,
"common"
voiceType
)
// 设置文本
@ -214,120 +213,146 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
text
)
// 设置音频格式为PCM
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
"tts_audio_format",
"pcm"
)
// 启用音频数据回调
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT,
2
)
// 先停止引擎,避免SDK内部异步线程带来的问题
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 启动引擎,在单次合成场景下,这会自动开始合成
// 启动引擎,开始合成并播放
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
val errorMsg = "启动引擎失败: $ret"
Log.e(TAG, errorMsg)
callback.onError(errorMsg)
Log.e(TAG, "启动引擎失败: $ret")
return false
}
Log.d(TAG, "成功开始播放")
return true
} catch (e: Exception) {
val errorMsg = "语音合成异常: ${e.message}"
Log.e(TAG, errorMsg)
callback.onError(errorMsg)
Log.e(TAG, "直接播放文本异常: ${e.message}")
return false
}
}
/**
* 同步合成文本为语音
* 停止播放
*
* @param text 要合成的文本
* @param voiceType 语音类型
* @return 合成的音频数据,如果失败则返回null
* @return 是否成功停止播放
*/
fun synthesizeSync(text: String, voiceType: String): ByteArray? {
fun stopSpeaking(): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
return null
return false
}
try {
Log.d(TAG, "开始同步合成文本: $text,使用语音类型: $voiceType")
Log.d(TAG, "停止播放")
// 重置同步变量
mSynthesisLatch = CountDownLatch(1)
mSynthesisAudioData = null
mSynthesisError = null
// 停止引擎
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "停止引擎失败: $ret")
// 设置发音人
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING,
"other"
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING,
"$voiceType"
)
// 尝试再次停止引擎
try {
Log.d(TAG, "尝试再次停止引擎")
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
Log.d(TAG, "引擎已停止")
} catch (e: Exception) {
Log.e(TAG, "再次停止引擎失败: ${e.message}")
}
// 设置文本
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING,
text
)
return false
}
// 设置音频格式为PCM
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
"tts_audio_format",
"pcm"
)
// 确保引擎完全停止
try {
Log.d(TAG, "确保引擎完全停止")
val stopAgainRet = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (stopAgainRet != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "再次停止引擎失败: $stopAgainRet")
} else {
Log.d(TAG, "引擎已完全停止")
}
} catch (e: Exception) {
Log.e(TAG, "再次停止引擎异常: ${e.message}")
}
// 启用音频数据回调
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT,
2
)
return true
} catch (e: Exception) {
Log.e(TAG, "停止播放异常: ${e.message}")
e.printStackTrace()
// 先停止引擎,避免SDK内部异步线程带来的问题
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 尝试再次停止引擎
try {
Log.d(TAG, "尝试再次停止引擎")
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
Log.d(TAG, "引擎已停止")
} catch (e: Exception) {
Log.e(TAG, "再次停止引擎失败: ${e.message}")
}
// 启动引擎,在单次合成场景下,这会自动开始合成
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "启动引擎失败: $ret")
return null
return false
}
}
/**
* 设置播放速度
*
* @param speed 播放速度,范围0.5-2.0
* @return 是否设置成功
*/
fun setSpeed(speed: Float): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
return false
}
// 等待合成完成,最多等待10秒
mSynthesisLatch?.await(10, TimeUnit.SECONDS)
try {
Log.d(TAG, "设置播放速度: $speed")
// 设置播放速度 (使用新版本的API)
mSpeechEngine?.setOptionDouble(
SpeechEngineDefines.PARAMS_KEY_TTS_SPEED_RATIO_DOUBLE,
speed.toDouble()
)
if (mSynthesisError != null) {
Log.e(TAG, "同步语音合成失败: ${mSynthesisError}")
return null
return true
} catch (e: Exception) {
Log.e(TAG, "设置播放速度异常: ${e.message}")
return false
}
}
if (mSynthesisAudioData == null) {
Log.e(TAG, "同步语音合成超时或返回空数据")
return null
/**
* 设置播放音量
*
* @param volume 播放音量,范围0.0-1.0
* @return 是否设置成功
*/
fun setVolume(volume: Float): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
return false
}
Log.d(TAG, "同步语音合成成功,音频数据大小: ${mSynthesisAudioData?.size} bytes")
return mSynthesisAudioData
try {
Log.d(TAG, "设置播放音量: $volume")
// 设置播放音量 (使用新版本的API)
mSpeechEngine?.setOptionDouble(
SpeechEngineDefines.PARAMS_KEY_TTS_VOLUME_RATIO_DOUBLE,
volume.toDouble()
)
return true
} catch (e: Exception) {
Log.e(TAG, "同步语音合成异常: ${e.message}")
return null
Log.e(TAG, "设置播放音量异常: ${e.message}")
return false
}
}
/**
* 设置播放状态回调
*
* @param callback 播放状态回调
*/
fun setPlaybackCallback(callback: PlaybackCallback) {
mPlaybackCallback = callback
}
/**
@ -361,37 +386,31 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> {
Log.e(TAG, "引擎错误: $stdData")
mCurrentCallback?.onError("引擎错误: $stdData")
mSynthesisError = "引擎错误: $stdData"
mSynthesisLatch?.countDown()
// 通知播放错误
mPlaybackCallback?.onPlaybackError("引擎错误: $stdData")
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_BEGIN -> {
Log.d(TAG, "合成开始: $stdData")
// 通知合成开始
mPlaybackCallback?.onSynthesisBegin()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_END -> {
Log.d(TAG, "合成结束: $stdData")
// 通知合成结束
mPlaybackCallback?.onSynthesisEnd()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_START_PLAYING -> {
Log.d(TAG, "开始播放: $stdData")
// 通知播放开始
mPlaybackCallback?.onPlaybackStarted()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_FINISH_PLAYING -> {
Log.d(TAG, "播放结束: $stdData")
// 通知播放结束
mPlaybackCallback?.onPlaybackCompleted()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_AUDIO_DATA -> {
Log.d(TAG, "收到音频数据")
// 处理音频数据
if (data.isNotEmpty()) {
try {
mCurrentCallback?.onSuccess(data)
mSynthesisAudioData = data
mSynthesisLatch?.countDown()
} catch (e: Exception) {
Log.e(TAG, "处理音频数据失败: ${e.message}")
mCurrentCallback?.onError("处理音频数据失败: ${e.message}")
mSynthesisError = "处理音频数据失败: ${e.message}"
mSynthesisLatch?.countDown()
}
}
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS -> {
Log.d(TAG, "播放进度: $stdData")
@ -403,10 +422,17 @@ class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
}
/**
* 火山语音TTS回调接口
* 播放状态回调接口
*/
interface VolcanoTtsCallback {
fun onSuccess(audioData: ByteArray)
fun onError(error: String)
interface PlaybackCallback {
fun onPlaybackStarted()
fun onPlaybackCompleted()
fun onPlaybackError(error: String)
// 新增合成开始回调
fun onSynthesisBegin()
// 新增合成结束回调
fun onSynthesisEnd()
}
}

13
android/settings.gradle.kts

@ -1,9 +1,15 @@
// This file configures the Flutter project
// Used by the Flutter tool to generate GeneratedPluginRegistrant.java
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val localPropertiesFile = file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { properties.load(it) }
}
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
requireNotNull(flutterSdkPath) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
@ -17,9 +23,10 @@ pluginManagement {
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("dev.flutter.flutter-plugin-loader")
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

45
lib/core/bindings/initial_binding.dart

@ -3,9 +3,11 @@ import '../../core/controllers/permission_controller.dart';
import '../../data/services/audio_service.dart';
import '../../data/services/notification_service.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../data/services/volcano_voice_recognition_service.dart';
import '../../data/services/volcano_tts_service.dart';
import '../../data/services/volcano_asr_service.dart';
import '../../data/services/volcano_tts_api_service.dart';
import '../../data/services/azure_asr_service.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@ -15,14 +17,18 @@ class InitialBinding extends Bindings {
final notificationService = Get.put(NotificationService(), permanent: true);
final audioService = Get.put(AudioServiceManager(), permanent: true);
final volcanoAiService = Get.put(VolcanoAIService(), permanent: true);
final voiceRecognitionService = Get.put(VolcanoVoiceRecognitionService(), permanent: true);
final volcanoTtsService = Get.put(VolcanoTtsService(), permanent: true);
final voiceRecognitionService = Get.put(VolcanoAsrService(), permanent: true);
final volcanoTtsService = Get.put(VolcanoTtsApiService(), permanent: true);
final azureAsrService = Get.put(AzureAsrService(), permanent: true);
// 只注册全局控制器
Get.put(PermissionController(), permanent: true);
// 异步初始化各个服务,确保顺序执行
_initializeServices(notificationService, audioService);
// 初始化 Azure 服务
_initializeAzureService(azureAsrService);
}
// 单独提取初始化服务的方法,以便更好地处理错误
@ -47,4 +53,35 @@ class InitialBinding extends Bindings {
// 其他服务初始化可以在这里添加
}
// 初始化 Azure 语音识别服务
void _initializeAzureService(AzureAsrService azureAsrService) async {
try {
// 从环境变量中获取 Azure 语音识别服务的配置参数
final subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '';
final serviceRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia';
final language = dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN';
// 检查配置是否完整
if (subscriptionKey.isEmpty) {
Logger.warning('Azure 语音识别服务的订阅密钥未设置,请检查 .env 文件');
return;
}
// 初始化 Azure ASR 服务
final success = await azureAsrService.initialize(
subscriptionKey: subscriptionKey,
serviceRegion: serviceRegion,
language: language,
);
if (success) {
Logger.info('Azure 语音识别服务初始化成功');
} else {
Logger.error('Azure 语音识别服务初始化失败');
}
} catch (e) {
Logger.error('初始化 Azure 语音识别服务失败', e);
}
}
}

7
lib/core/routes/app_pages.dart

@ -10,6 +10,8 @@ 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/bindings/test_binding.dart';
import '../../modules/translation/views/translation_view.dart';
import '../../modules/translation/bindings/translation_binding.dart';
// import '../../modules/speech_demo/speech_demo_page.dart'; // This file doesn't exist
import './app_routes.dart';
@ -50,5 +52,10 @@ abstract class AppPages {
page: () => AsrTestView(),
binding: AsrTestBinding(),
),
GetPage(
name: Routes.translation,
page: () => const TranslationView(),
binding: TranslationBinding(),
),
];
}

1
lib/core/routes/app_routes.dart

@ -7,4 +7,5 @@ abstract class Routes {
static const speechDemo = '/speech_demo';
static const TTS_TEST = '/tts_test';
static const ASR_TEST = '/asr_test';
static const translation = '/translation';
}

35
lib/core/theme/app_colors.dart

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class AppColors {
// Background colors
static const Color darkBackground = Color(0xFF121212);
static const Color cardBackground = Color(0xFF1E1E1E);
// Primary colors
static const Color primary = Color(0xFF6200EE);
static const Color primaryVariant = Color(0xFF3700B3);
// Accent colors
static const Color accent = Color(0xFF03DAC6);
static const Color accentVariant = Color(0xFF018786);
// Text colors
static const Color textPrimary = Color(0xFFFFFFFF);
static const Color textSecondary = Color(0xB3FFFFFF); // 70% white
static const Color textHint = Color(0x80FFFFFF); // 50% white
// Status colors
static const Color success = Color(0xFF4CAF50);
static const Color warning = Color(0xFFFFC107);
static const Color error = Color(0xFFB00020);
// Gradient colors
static const List<Color> primaryGradient = [
Color(0xFF6200EE),
Color(0xFF9C27B0),
];
// Disabled colors
static const Color disabled = Color(0xFF757575);
static const Color disabledBackground = Color(0xFF2D2D2D);
}

80
lib/core/theme/text_styles.dart

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
class TextStyles {
// Headings
static const TextStyle heading1 = TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
height: 1.2,
);
static const TextStyle heading2 = TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
height: 1.2,
);
static const TextStyle heading3 = TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
height: 1.2,
);
// Title and subtitle
static const TextStyle title = TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
height: 1.3,
);
static const TextStyle subtitle = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
height: 1.3,
);
// Body text
static const TextStyle body = TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: AppColors.textPrimary,
height: 1.5,
);
static const TextStyle bodySmall = TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: AppColors.textPrimary,
height: 1.5,
);
// Button text
static const TextStyle button = TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
letterSpacing: 0.5,
);
// Caption and overline
static const TextStyle caption = TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColors.textSecondary,
height: 1.4,
);
static const TextStyle overline = TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
letterSpacing: 1.5,
height: 1.4,
);
}

376
lib/data/services/azure_asr_service.dart

@ -0,0 +1,376 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
/// 识别事件类型
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
recognizing,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 无匹配结果
noMatch,
/// 识别错误
error,
}
/// 识别事件
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 recognizing 类型中有效)
final String text;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String? error;
/// 取消原因(仅在 canceled 类型中有效)
final String? reason;
/// 错误详情(仅在 canceled 类型中有效)
final String? errorDetails;
RecognitionEvent({
required this.type,
this.text = '',
this.error,
this.reason,
this.errorDetails,
});
/// 从原生事件映射创建识别事件
factory RecognitionEvent.fromMap(Map<dynamic, dynamic> map) {
final eventType = map['eventType'] as String;
RecognitionEventType type;
switch (eventType) {
case 'recognizing':
type = RecognitionEventType.recognizing;
break;
case 'finalResult':
type = RecognitionEventType.finalResult;
break;
case 'sessionStarted':
type = RecognitionEventType.sessionStarted;
break;
case 'sessionStopped':
type = RecognitionEventType.sessionStopped;
break;
case 'canceled':
type = RecognitionEventType.canceled;
break;
case 'noMatch':
type = RecognitionEventType.noMatch;
break;
case 'error':
type = RecognitionEventType.error;
break;
default:
throw ArgumentError('Unknown event type: $eventType');
}
return RecognitionEvent(
type: type,
text: map['text'] as String? ?? '',
error: map['error'] as String?,
reason: map['reason'] as String?,
errorDetails: map['errorDetails'] as String?,
);
}
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, error: $error, reason: $reason, errorDetails: $errorDetails}';
}
}
/// Azure 语音识别服务
class AzureAsrService extends GetxService {
static AzureAsrService get to => Get.find();
// 方法通道
static const MethodChannel _methodChannel = MethodChannel('com.example.deep_voice/azure_asr');
// 事件通道
static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/azure_asr_events');
// 是否已初始化
bool _isInitialized = false;
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 公开的事件流
Stream<RecognitionEvent>? _recognitionStream;
Stream<RecognitionEvent>? get recognitionStream => _recognitionStream;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
/// 初始化 Azure 语音识别服务
///
/// [subscriptionKey] Azure 语音服务的订阅密钥
/// [serviceRegion] Azure 语音服务的区域
/// [language] 识别语言,默认为中文
Future<bool> initialize({
required String subscriptionKey,
required String serviceRegion,
String language = 'zh-CN',
}) async {
if (_isInitialized) return true;
try {
final result = await _methodChannel.invokeMethod<bool>(
'initialize',
{
'subscriptionKey': subscriptionKey,
'serviceRegion': serviceRegion,
'language': language,
},
);
_isInitialized = result ?? false;
if (_isInitialized) {
Logger.info('Azure 语音识别服务初始化成功');
} else {
Logger.error('Azure 语音识别服务初始化失败');
}
return _isInitialized;
} catch (e) {
Logger.error('初始化 Azure 语音识别服务失败', e);
_isInitialized = false;
return false;
}
}
/// 重置识别器
/// 在切换识别模式前调用,确保资源被正确释放
Future<bool> _resetRecognizer() async {
try {
// 如果连续识别正在进行,先停止它
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 清理事件流资源
_cleanupEventStream();
// 调用原生方法重置识别器
await _methodChannel.invokeMethod<bool>('resetRecognizer');
Logger.info('Azure 语音识别器已重置');
return true;
} catch (e) {
Logger.error('重置 Azure 语音识别器失败', e);
return false;
}
}
/// 执行一次性语音识别
Future<String> recognizeOnce() async {
_checkInitialized();
try {
// 重置识别器,确保资源被正确释放
await _resetRecognizer();
final result = await _methodChannel.invokeMethod<String>('recognizeOnce');
final text = result ?? '';
_latestRecognizedText = text;
return text;
} catch (e) {
Logger.error('一次性语音识别失败', e);
rethrow;
}
}
/// 开始连续语音识别
///
/// 返回一个布尔值,表示是否成功启动连续识别
Future<bool> startContinuousRecognition() async {
_checkInitialized();
if (_isContinuousRecognitionActive) {
Logger.warning('连续识别已经在进行中');
return false;
}
try {
// 重置识别器,确保资源被正确释放
await _resetRecognizer();
// 创建事件流控制器
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 设置事件监听
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.map<RecognitionEvent>((dynamic event) => RecognitionEvent.fromMap(event))
.listen(
(event) {
_handleRecognitionEvent(event);
_eventStreamController?.add(event);
},
onError: (error) {
Logger.error('Azure 语音识别事件流错误', error);
_eventStreamController?.addError(error);
_cleanupEventStream();
},
);
// 开始连续识别
final result = await _methodChannel.invokeMethod<bool>('startContinuousRecognition');
_isContinuousRecognitionActive = result ?? false;
// 设置公开的流
_recognitionStream = _eventStreamController?.stream;
return _isContinuousRecognitionActive;
} catch (e) {
Logger.error('开始连续语音识别失败', e);
_cleanupEventStream();
return false;
}
}
/// 停止连续语音识别
Future<bool> stopContinuousRecognition() async {
_checkInitialized();
if (!_isContinuousRecognitionActive) {
Logger.warning('连续识别未在进行中');
return true; // 已经停止,直接返回成功
}
try {
final result = await _methodChannel.invokeMethod<bool>('stopContinuousRecognition');
final success = result ?? false;
if (success) {
_isContinuousRecognitionActive = false;
// 不立即清理事件流,等待 sessionStopped 事件
}
return success;
} catch (e) {
Logger.error('停止连续语音识别失败', e);
return false;
}
}
/// 设置识别语言
Future<bool> setRecognitionLanguage(String language) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setRecognitionLanguage',
{'language': language},
);
return result ?? false;
} catch (e) {
Logger.error('设置识别语言失败', e);
return false;
}
}
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(RecognitionEvent event) {
switch (event.type) {
case RecognitionEventType.finalResult:
_latestRecognizedText = event.text;
break;
case RecognitionEventType.sessionStopped:
_isContinuousRecognitionActive = false;
// 延迟清理事件流,确保所有事件都被处理
Future.delayed(Duration(milliseconds: 500), _cleanupEventStream);
break;
case RecognitionEventType.canceled:
_isContinuousRecognitionActive = false;
// 延迟清理事件流,确保所有事件都被处理
Future.delayed(Duration(milliseconds: 500), _cleanupEventStream);
break;
default:
// 其他事件类型不需要特殊处理
break;
}
}
/// 清理事件流资源
void _cleanupEventStream() {
_eventSubscription?.cancel();
_eventSubscription = null;
// 只有在控制器存在且未关闭时才关闭
if (_eventStreamController != null && !_eventStreamController!.isClosed) {
_eventStreamController?.close();
}
_eventStreamController = null;
_recognitionStream = null;
}
/// 检查是否已初始化
void _checkInitialized() {
if (!_isInitialized) {
throw StateError('Azure 语音识别服务未初始化');
}
}
/// 释放资源
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
await _eventSubscription?.cancel();
_eventSubscription = null;
// 只有在控制器存在且未关闭时才关闭
if (_eventStreamController != null && !_eventStreamController!.isClosed) {
await _eventStreamController?.close();
}
_eventStreamController = null;
_recognitionStream = null;
await _methodChannel.invokeMethod<bool>('dispose');
_isInitialized = false;
Logger.info('Azure 语音识别服务资源已释放');
} catch (e) {
Logger.error('释放 Azure 语音识别服务资源失败', e);
}
}
@override
void onClose() {
dispose();
super.onClose();
}
}

151
lib/data/services/background_agent_service.dart

@ -1,17 +1,18 @@
import 'package:get/get.dart';
import 'dart:async';
import 'volcano_ai_service.dart';
import 'volcano_tts_service.dart';
import 'volcano_voice_recognition_service.dart';
import 'azure_asr_service.dart';
import 'volcano_tts_api_service.dart';
import '../../modules/chat/models/message_model.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class BackgroundAgentService extends GetxService {
static BackgroundAgentService get to => Get.find();
final VolcanoAIService _aiService;
final VolcanoTtsService _ttsService;
final VolcanoVoiceRecognitionService _voiceRecognitionService;
final VolcanoTtsApiService _ttsService;
final AzureAsrService _voiceRecognitionService;
final List<Message> _messageHistory = [];
String _pendingTtsText = '';
static const int _minTtsLength = 20;
@ -19,6 +20,10 @@ class BackgroundAgentService extends GetxService {
bool _isListening = false;
StreamSubscription? _recognitionSubscription;
// TTS相关订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
// 可观察的状态
final RxBool isListening = false.obs;
final RxString recognizedText = ''.obs;
@ -55,9 +60,32 @@ class BackgroundAgentService extends GetxService {
BackgroundAgentService()
: _aiService = VolcanoAIService(),
_ttsService = Get.find<VolcanoTtsService>(),
_voiceRecognitionService = Get.find<VolcanoVoiceRecognitionService>() {
// 简化构造函数,不需要定期检查TTS状态
_ttsService = Get.find<VolcanoTtsApiService>(),
_voiceRecognitionService = Get.find<AzureAsrService>() {
// 设置TTS事件监听
_setupTtsEventListener();
}
// 设置TTS事件监听
void _setupTtsEventListener() {
_ttsEventSubscription?.cancel();
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
}
// 处理TTS事件
void _handleTtsEvent(Map<String, dynamic> event) {
final eventType = event['eventType'];
switch (eventType) {
case 'ttsSentenceStart':
_isTtsSpeaking = true;
break;
case 'ttsSentenceEnd':
case 'sessionFinished':
case 'error':
_isTtsSpeaking = false;
break;
}
}
// 启动无语音超时计时器
@ -65,24 +93,16 @@ class BackgroundAgentService extends GetxService {
// 取消之前的计时器
_noSpeechTimer?.cancel();
// 设置15秒的超时计时器
// 设置30秒的超时计时器
_noSpeechTimer = Timer(const Duration(seconds: 30), () {
// 检查TTS是否正在播放
bool isTtsPlaying = false;
try {
isTtsPlaying = _ttsService.isActuallyPlaying();
} catch (e) {
Logger.error('检查TTS播放状态失败', e);
isTtsPlaying = false;
}
if (isTtsPlaying) {
if (_isTtsSpeaking) {
// 如果TTS正在播放,重置定时器
Logger.info('TTS正在播放,重置15秒超时计时器');
Logger.info('TTS正在播放,重置30秒超时计时器');
_startNoSpeechTimer();
} else {
// 如果TTS不在播放,且15秒内没有检测到用户输入,退出交互
Logger.info('15秒内没有检测到用户输入,退出交互');
// 如果TTS不在播放,且30秒内没有检测到用户输入,退出交互
Logger.info('30秒内没有检测到用户输入,退出交互');
_exitInteraction();
}
});
@ -99,12 +119,12 @@ class BackgroundAgentService extends GetxService {
_isProcessing = false;
// 播放退出提示
try {
await _ttsService.speak("没有听到您说话,已退出语音交互");
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize("没有听到您说话,已退出语音交互", speaker);
} catch (e) {
print('播放退出提示失败: $e');
}
}
// 处理蓝牙耳机按钮触发的交互
@ -114,6 +134,16 @@ class BackgroundAgentService extends GetxService {
return;
}
// 确保TTS服务已连接
if (!_ttsService.isConnected.value) {
try {
await _ttsService.connect();
} catch (e) {
print('连接TTS服务失败: $e');
return;
}
}
// 设置循环交互标志为 true
_shouldContinueInteraction = true;
@ -135,7 +165,11 @@ class BackgroundAgentService extends GetxService {
// 重置语音识别服务
try {
await _voiceRecognitionService.initialize();
await _voiceRecognitionService.initialize(
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
);
} catch (e) {
print('重置语音识别服务失败: $e');
}
@ -151,7 +185,9 @@ class BackgroundAgentService extends GetxService {
try {
// 先播放一个简短的提示音或提示语,表示开始监听
try {
await _ttsService.speak("我在听");
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize("我在听", speaker);
} catch (e) {
print('播放提示音失败: $e');
// 继续执行,不要因为提示音失败而中断整个流程
@ -238,20 +274,25 @@ class BackgroundAgentService extends GetxService {
// 累积文本并处理TTS
_pendingTtsText += chunk;
if (_ttsService.isEnabled.value) {
if (_pendingTtsText.length >= _minTtsLength) {
// 查找最后一个完整句子的结束位置
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText);
if (lastSentenceEnd > 0) {
String textToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1);
// 提取完整的句子
String sentenceToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1);
// 只有当句子长度超过最小长度时才播放
if (sentenceToSpeak.length >= _minTtsLength) {
try {
_ttsService.speak(textToSpeak);
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
_ttsService.synthesize(sentenceToSpeak, speaker);
} catch (e) {
print('播放TTS失败: $e');
}
// 更新待处理文本,移除已播放的部分
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
}
}
}
},
onError: (e) {
print('AI响应流错误: $e');
@ -262,9 +303,11 @@ class BackgroundAgentService extends GetxService {
// 如果已取消,不处理剩余的文本
if (!isCancelled && !_shouldCancelAiResponse) {
// 处理剩余的文本
if (_ttsService.isEnabled.value && _pendingTtsText.isNotEmpty) {
if (_pendingTtsText.isNotEmpty) {
try {
_ttsService.speak(_pendingTtsText);
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
_ttsService.synthesize(_pendingTtsText, speaker);
} catch (e) {
print('播放剩余TTS失败: $e');
}
@ -288,7 +331,9 @@ class BackgroundAgentService extends GetxService {
// 播放错误提示
try {
await _ttsService.speak(fullResponse);
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize(fullResponse, speaker);
} catch (_) {}
// 启动超时计时器
@ -318,7 +363,9 @@ class BackgroundAgentService extends GetxService {
} catch (e) {
print('交互过程出错: $e');
try {
await _ttsService.speak("抱歉,出现了一些问题");
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize("抱歉,出现了一些问题", speaker);
} catch (_) {}
// 发生错误时停止循环交互
@ -342,10 +389,18 @@ class BackgroundAgentService extends GetxService {
try {
// 确保语音识别服务已初始化
if (!await _voiceRecognitionService.initialize()) {
if (!await _voiceRecognitionService.initialize(
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
)) {
// 尝试重新初始化
await Future.delayed(const Duration(milliseconds: 500));
if (!await _voiceRecognitionService.initialize()) {
if (!await _voiceRecognitionService.initialize(
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
)) {
throw Exception('无法初始化语音识别服务');
}
}
@ -398,15 +453,8 @@ class BackgroundAgentService extends GetxService {
_startNoSpeechTimer();
// 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断
bool isTtsPlaying = false;
try {
isTtsPlaying = _ttsService.isActuallyPlaying();
} catch (e) {
isTtsPlaying = false;
}
if (isTtsPlaying && event.text.trim().isNotEmpty) {
_ttsService.stop(); // 停止当前TTS播放
if (_isTtsSpeaking && event.text.trim().isNotEmpty) {
_ttsService.endSession(); // 结束当前TTS会话
// 设置标志,表示应该取消当前的AI响应
_shouldCancelAiResponse = true;
@ -476,7 +524,7 @@ class BackgroundAgentService extends GetxService {
_recognitionSubscription = null;
// 停止语音识别
await _voiceRecognitionService.stopRecognition();
await _voiceRecognitionService.stopContinuousRecognition();
// 获取最终识别结果
final result = recognizedText.value;
@ -493,7 +541,11 @@ class BackgroundAgentService extends GetxService {
// 尝试强制重置语音识别服务
try {
await _voiceRecognitionService.initialize();
await _voiceRecognitionService.initialize(
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
);
} catch (e) {
print('强制重置语音识别服务失败: $e');
}
@ -527,7 +579,12 @@ class BackgroundAgentService extends GetxService {
_silenceTimer?.cancel();
_noSpeechTimer?.cancel();
_aiResponseSubscription?.cancel();
_ttsEventSubscription?.cancel();
_shouldContinueInteraction = false;
// 断开TTS服务连接
_ttsService.disconnect();
super.onClose();
}
}

281
lib/data/services/voice_recognition_service.dart

@ -1,281 +0,0 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 微软语音识别服务
///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class VoiceRecognitionService {
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/speech_recognition');
static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/speech_recognition_events');
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 公开的事件流
Stream<RecognitionEvent>? _recognitionStream;
Stream<RecognitionEvent>? get recognitionStream => _recognitionStream;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
VoiceRecognitionService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
}
/// 初始化微软语音识别 SDK
///
/// 返回 true 表示初始化成功,否则抛出 PlatformException
Future<bool> initialize() async {
if (_isInitialized) return true;
try {
final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'serviceRegion': _serviceRegion,
});
_isInitialized = result;
return result;
} on PlatformException catch (e) {
print('语音识别初始化失败: ${e.message}');
_isInitialized = false;
throw e;
}
}
/// 执行一次性语音识别
///
/// 返回识别的文本,如果识别失败则抛出 PlatformException
Future<String> recognizeSpeech() async {
if (!_isInitialized) {
throw Exception('语音识别服务未初始化,请先调用 initialize()');
}
try {
final String result = await _channel.invokeMethod('recognizeOnce');
return result;
} on PlatformException catch (e) {
print('语音识别失败: ${e.message}');
throw e;
}
}
/// 开始连续语音识别
///
/// 返回一个包含识别事件的流,如果开始失败则抛出 PlatformException
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
throw Exception('语音识别服务未初始化,请先调用 initialize()');
}
if (_isContinuousRecognitionActive) {
throw Exception('连续语音识别已经在进行中');
}
try {
// 创建事件流控制器
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 设置事件监听
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleRecognitionEvent, onError: _handleRecognitionError);
// 开始连续识别
final bool result = await _channel.invokeMethod('startContinuousRecognition');
_isContinuousRecognitionActive = result;
// 设置公开的流
_recognitionStream = _eventStreamController!.stream;
return _recognitionStream!;
} on PlatformException catch (e) {
print('开始连续语音识别失败: ${e.message}');
_cleanupEventStream();
throw e;
}
}
/// 停止连续语音识别
///
/// 返回 true 表示停止成功,否则抛出 PlatformException
Future<bool> stopContinuousRecognition() async {
if (!_isInitialized) {
throw Exception('语音识别服务未初始化,请先调用 initialize()');
}
if (!_isContinuousRecognitionActive) {
return true; // 已经停止,直接返回成功
}
try {
final bool result = await _channel.invokeMethod('stopContinuousRecognition');
_isContinuousRecognitionActive = !result;
// 清理事件流
_cleanupEventStream();
return result;
} on PlatformException catch (e) {
print('停止连续语音识别失败: ${e.message}');
throw e;
}
}
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['eventType'] as String? ?? '';
switch (eventType) {
case 'finalResult':
final String text = eventMap['text'] as String? ?? '';
_latestRecognizedText = text;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
));
break;
case 'intermediateResult':
final String text = eventMap['text'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: text,
));
break;
case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
break;
case 'sessionStopped':
_isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
break;
case 'canceled':
_isContinuousRecognitionActive = false;
final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.canceled,
error: '$reason: $errorDetails',
));
break;
case 'error':
final String error = eventMap['error'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break;
}
}
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
_eventStreamController?.addError(error);
_cleanupEventStream();
}
/// 清理事件流资源
void _cleanupEventStream() {
_eventSubscription?.cancel();
_eventSubscription = null;
_eventStreamController?.close();
_eventStreamController = null;
_recognitionStream = null;
_isContinuousRecognitionActive = false;
}
/// 释放资源
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
await _channel.invokeMethod('dispose');
_cleanupEventStream();
_isInitialized = false;
} catch (e) {
print('释放语音识别资源失败: $e');
}
}
}
/// 识别事件类型
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 识别错误
error,
}
/// 识别事件
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
RecognitionEvent({
required this.type,
this.text = '',
this.error = '',
});
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, error: $error}';
}
}

23
lib/data/services/volcano_voice_recognition_service.dart → lib/data/services/volcano_asr_service.dart

@ -30,15 +30,16 @@ class RecognitionEvent {
/// 火山语音识别服务
///
/// 该服务提供了通过平台通道与 Android 上的火山语音识别 SDK 交互的接口
class VolcanoVoiceRecognitionService extends GetxService {
class VolcanoAsrService extends GetxService {
// 平台通道
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/volcano_asr');
static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/speech_recognition_events');
static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/volcano_asr_events');
// 配置参数
late final String _appId;
late final String _token;
late final String _cluster; // 集群区域
late final String? _resourceId; // 资源ID,用于大模型流式识别API鉴权
// 状态变量
final _isListening = false.obs;
@ -64,16 +65,19 @@ class VolcanoVoiceRecognitionService extends GetxService {
return _isListening.value;
}
VolcanoVoiceRecognitionService() {
VolcanoSrcService() {
// 从环境变量获取配置
_appId = dotenv.env['VOLCANO_ASR_APP_ID'] ?? '';
_token = dotenv.env['VOLCANO_ASR_APP_KEY'] ?? '';
_token = dotenv.env['VOLCANO_ASR_APP_TOKEN'] ?? '';
// 从环境变量获取集群区域
_cluster = dotenv.env['VOLCANO_ASR_CLUSTER'] ?? '';
// 从环境变量获取资源ID
_resourceId = dotenv.env['VOLCANO_ASR_RESOURCE_ID'];
Logger.info('火山语音识别配置: APP_ID=${_appId.isNotEmpty ? "已设置" : "未设置"}, APP_KEY=${_token.isNotEmpty ? "已设置" : "未设置"}');
Logger.info('使用标准语音识别SDK: $_useStandardASR');
Logger.info('集群区域: $_cluster');
Logger.info('资源ID: $_resourceId');
if (_appId.isEmpty || _token.isEmpty) {
_errorMessage.value = '火山语音识别配置不完整,请检查环境变量';
@ -81,6 +85,7 @@ class VolcanoVoiceRecognitionService extends GetxService {
}
}
// 获取可观察状态
RxBool get isListening => _isListening;
bool get isInitialized => _isInitialized.value;
@ -151,11 +156,13 @@ class VolcanoVoiceRecognitionService extends GetxService {
Logger.info('使用标准语音识别SDK配置 (API v2)');
Logger.info('集群区域: $_cluster');
Logger.info('资源ID: $_resourceId');
final Map<String, dynamic> params = {
'appId': _appId,
'token': _token,
'subscriptionKey': _appId,
'serviceRegion': _token,
'cluster': _cluster,
'resourceId': _resourceId,
};
final bool result = await _channel.invokeMethod('initialize', params);
@ -213,7 +220,7 @@ class VolcanoVoiceRecognitionService extends GetxService {
_errorMessage.value = '';
_recognitionResults.clear();
final bool result = await _channel.invokeMethod('startOneTimeRecognition');
final bool result = await _channel.invokeMethod('recognizeOnce');
_isListening.value = result;
if (result) {
@ -338,7 +345,7 @@ class VolcanoVoiceRecognitionService extends GetxService {
}
try {
final bool result = await _channel.invokeMethod('stopRecognition');
final bool result = await _channel.invokeMethod('stopContinuousRecognition');
_isListening.value = !result;
if (result) {

1570
lib/data/services/volcano_tts_api_service.dart

File diff suppressed because it is too large

712
lib/data/services/volcano_tts_service.dart

@ -1,10 +1,7 @@
import 'dart:async';
import 'dart:typed_data';
import 'dart:io';
import 'dart:math' as math;
import 'dart:collection';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/services.dart';
@ -21,124 +18,9 @@ class VolcanoTtsException implements Exception {
: message;
}
/// 自定义音频源,用于从字节数组读取音频数据
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility
BytesAudioSource(this._bytes);
// 添加 PCM 头信息,转换为 WAV 格式
Uint8List _addWavHeader(Uint8List pcmData) {
// PCM 参数
const int sampleRate = 16000; // 采样率
const int numChannels = 1; // 单声道
const int bitsPerSample = 16; // 16位采样
// 计算数据大小
final int dataSize = pcmData.length;
final int fileSize = 36 + dataSize;
// 创建 WAV 头
final ByteData header = ByteData(44);
// RIFF 头
header.setUint8(0, 'R'.codeUnitAt(0));
header.setUint8(1, 'I'.codeUnitAt(0));
header.setUint8(2, 'F'.codeUnitAt(0));
header.setUint8(3, 'F'.codeUnitAt(0));
// 文件大小
header.setUint32(4, fileSize, Endian.little);
// WAVE 标识
header.setUint8(8, 'W'.codeUnitAt(0));
header.setUint8(9, 'A'.codeUnitAt(0));
header.setUint8(10, 'V'.codeUnitAt(0));
header.setUint8(11, 'E'.codeUnitAt(0));
// fmt 子块
header.setUint8(12, 'f'.codeUnitAt(0));
header.setUint8(13, 'm'.codeUnitAt(0));
header.setUint8(14, 't'.codeUnitAt(0));
header.setUint8(15, ' '.codeUnitAt(0));
// 子块大小
header.setUint32(16, 16, Endian.little);
// 音频格式 (PCM = 1)
header.setUint16(20, 1, Endian.little);
// 声道数
header.setUint16(22, numChannels, Endian.little);
// 采样率
header.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 声道数 * 采样位数 / 8
header.setUint32(28, sampleRate * numChannels * bitsPerSample ~/ 8, Endian.little);
// 块对齐 = 声道数 * 采样位数 / 8
header.setUint16(32, numChannels * bitsPerSample ~/ 8, Endian.little);
// 采样位数
header.setUint16(34, bitsPerSample, Endian.little);
// data 子块
header.setUint8(36, 'd'.codeUnitAt(0));
header.setUint8(37, 'a'.codeUnitAt(0));
header.setUint8(38, 't'.codeUnitAt(0));
header.setUint8(39, 'a'.codeUnitAt(0));
// 数据大小
header.setUint32(40, dataSize, Endian.little);
// 创建完整的 WAV 数据
final Uint8List wavData = Uint8List(44 + dataSize);
wavData.setRange(0, 44, header.buffer.asUint8List());
wavData.setRange(44, 44 + dataSize, pcmData);
return wavData;
}
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
// 添加 WAV 头信息
final Uint8List wavData = _addWavHeader(_bytes);
start = start ?? 0;
end = end ?? wavData.length;
try {
final subData = wavData.sublist(start, end);
// 使用固定大小的块进行流式传输
final chunks = <List<int>>[];
var offset = 0;
while (offset < subData.length) {
final chunkSize = math.min(_bufferSize, subData.length - offset);
chunks.add(subData.sublist(offset, offset + chunkSize));
offset += chunkSize;
}
return StreamAudioResponse(
sourceLength: wavData.length,
contentLength: subData.length,
offset: start,
stream: Stream.fromIterable(chunks),
contentType: 'audio/wav',
);
} catch (e) {
print('BytesAudioSource request error: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length + 44); // PCM 数据长度 + WAV 头长度
}
/// 火山语音合成服务 (SDK版本)
/// 火山语音合成服务 (极简版)
///
/// 直接使用原生端 VolcanoTtsHelper 的功能,不处理状态回调
class VolcanoTtsService extends GetxService {
// 平台通道
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/volcano_tts');
@ -149,65 +31,31 @@ class VolcanoTtsService extends GetxService {
final String _cluster;
final String _voiceType;
// 音频播放器
late AudioPlayer _audioPlayer;
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = true.obs; // 默认启用
// 播放状态变化通知流
final _playingStateController = StreamController<bool>.broadcast();
Stream<bool> get playingStateStream => _playingStateController.stream;
StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
// 优化的句子分割正则表达式,包含更多中英文标点
static final _sentenceBreaks = RegExp(r'[。!?.!?;;::,,、]');
// 优化的句子分割参数
static const int _maxSegmentLength = 150; // 最大分段长度
static const int _optimalSegmentLength = 80; // 最佳分段长度
static const int _minSegmentLength = 5; // 最小分段长度
bool _isFetching = false;
int _consecutiveErrorCount = 0;
static const int _maxConsecutiveErrors = 3;
// 初始化状态
bool _isInitialized = false;
bool _isDisposed = false;
// 性能优化参数
final int _preloadCount = 2; // 预加载片段数量
final int _maxRetryAttempts = 1; // 不进行重试
// 服务启用状态
final isEnabled = true.obs; // 默认启用
// 音频质量参数
final double _defaultVolume = 1.0;
final double _defaultSpeed = 1.0;
VolcanoTtsService() :
// 使用新的环境变量命名
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '',
_cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '',
_voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? '' {
if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) {
throw VolcanoTtsException('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID, VOLCANO_TTS_APP_KEY 和 VOLCANO_TTS_CLUSTER');
throw VolcanoTtsException('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID, VOLCANO_TTS_APP_TOKEN 和 VOLCANO_TTS_CLUSTER');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
await _initAudioSession();
await _initAudioPlayer();
await _initializeTtsEngine();
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
@ -218,7 +66,7 @@ class VolcanoTtsService extends GetxService {
/// 初始化TTS引擎
Future<void> _initializeTtsEngine() async {
try {
print('开始初始化火山语音TTS引擎,APP_ID: ${_appId.substring(0, math.min(3, _appId.length))}***,APP_KEY: ${_token.length > 10 ? "${_token.substring(0, 5)}..." : _token}');
print('开始初始化火山语音TTS引擎');
final result = await _channel.invokeMethod<bool>('initialize', {
'appId': _appId,
@ -232,6 +80,10 @@ class VolcanoTtsService extends GetxService {
if (!_isInitialized) {
throw VolcanoTtsException('TTS引擎初始化失败');
}
// 设置默认音量和速度
await setVolume(_defaultVolume);
await setSpeed(_defaultSpeed);
} catch (e) {
print('初始化火山语音TTS引擎失败: $e');
_isInitialized = false;
@ -239,498 +91,59 @@ class VolcanoTtsService extends GetxService {
}
}
Future<void> _initAudioSession() async {
_audioSession = await AudioSession.instance;
// 针对不同平台优化音频会话配置
if (Platform.isAndroid) {
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
} else if (Platform.isIOS) {
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.duckOthers,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation,
));
}
// 确保音频会话激活并设置正确的音频模式
if (Platform.isAndroid) {
await _audioSession?.setActive(true, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation);
} else {
await _audioSession?.setActive(true);
}
}
Future<void> _initAudioPlayer() async {
if (_isDisposed) return;
_audioPlayer = AudioPlayer();
// 配置Android特定的播放参数
if (Platform.isAndroid) {
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
}
// 基本播放器配置
await _audioPlayer.setVolume(_defaultVolume);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setSpeed(_defaultSpeed);
// 设置音频缓冲配置,启用预加载以减少延迟
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
}
/// 合成并播放文本
///
/// [text] 要合成的文本
/// [voiceType] 可选的语音类型,如果提供,将覆盖默认语音类型
Future<void> speak(String text, {String? voiceType}) async {
if (!isEnabled.value || text.trim().isEmpty) return;
///
/// 返回一个Future,表示是否成功开始播放
Future<bool> speak(String text, {String? voiceType}) async {
if (text.trim().isEmpty) return false;
if (!isEnabled.value) return false;
if (!_isInitialized) {
print('TTS引擎尚未初始化');
return false;
}
try {
print('开始播放文本: "${text.length > 20 ? text.substring(0, 20) + '...' : text}"');
// 如果提供了语音类型,记录日志
if (voiceType != null && voiceType != _voiceType) {
print('使用临时语音类型: $voiceType (默认: $_voiceType)');
}
_pendingText += text;
_extractSentences();
await _startPreloadIfNeeded(voiceType: voiceType);
} catch (e, stack) {
print('语音合成失败: $e');
print('Stack trace: $stack');
_consecutiveErrorCount++;
if (_consecutiveErrorCount >= _maxConsecutiveErrors) {
print('连续错误次数过多,尝试重新初始化播放器');
await _reinitializePlayer();
_consecutiveErrorCount = 0;
}
}
}
/// 优化的文本分段算法
void _extractSentences() {
if (_pendingText.isEmpty) return;
// 查找所有句子分隔符
final matches = _sentenceBreaks.allMatches(_pendingText).toList();
if (matches.isEmpty) {
// 如果没有找到句子分隔符,按长度分割
_splitByLength(_pendingText);
_pendingText = '';
return;
}
int lastPos = 0;
String currentBatch = '';
for (final match in matches) {
final end = match.end;
final sentence = _pendingText.substring(lastPos, end).trim();
if (sentence.isEmpty) {
lastPos = end;
continue;
}
// 如果当前批次为空,直接添加句子
if (currentBatch.isEmpty) {
currentBatch = sentence;
}
// 如果当前批次加上新句子不超过最佳长度,则合并
else if ((currentBatch + sentence).length <= _optimalSegmentLength) {
currentBatch += sentence;
}
// 如果超过最佳长度,将当前批次加入队列,开始新批次
else {
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
currentBatch = sentence;
}
lastPos = end;
}
// 处理最后一个批次
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
// 处理剩余的文本
if (lastPos < _pendingText.length) {
final remaining = _pendingText.substring(lastPos).trim();
if (remaining.isNotEmpty) {
if (remaining.length <= _optimalSegmentLength) {
// 尝试与最后一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + remaining).length <= _optimalSegmentLength) {
_sentenceQueue[_sentenceQueue.length - 1] += remaining;
} else {
_sentenceQueue.add(remaining);
}
} else {
_splitByLength(remaining);
}
}
}
_pendingText = '';
}
/// 按长度分割文本,优先在空格或标点处分割
void _splitByLength(String text) {
if (text.isEmpty) return;
int start = 0;
while (start < text.length) {
// 计算理想的结束位置
int end = math.min(start + _optimalSegmentLength, text.length);
// 如果没有到达文本末尾,尝试在标点或空格处分割
if (end < text.length) {
// 在理想长度附近查找标点
int searchStart = math.max(start + _minSegmentLength, end - 20);
int searchEnd = math.min(text.length, end + 20);
String searchText = text.substring(searchStart, searchEnd);
final breakMatch = _sentenceBreaks.firstMatch(searchText);
if (breakMatch != null) {
end = searchStart + breakMatch.end;
} else {
// 如果没有找到标点,尝试在空格处分割
final spacePos = text.lastIndexOf(' ', end);
if (spacePos > start && spacePos > end - 20) {
end = spacePos + 1; // 包含空格
}
}
}
final segment = text.substring(start, end).trim();
if (segment.isNotEmpty) {
// 尝试与前一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + segment).length <= _optimalSegmentLength) {
_sentenceQueue[_sentenceQueue.length - 1] += segment;
} else {
_sentenceQueue.add(segment);
}
}
start = end;
}
}
Future<void> _startPreloadIfNeeded({String? voiceType}) async {
if (_isFetching || _sentenceQueue.isEmpty) return;
_isFetching = true;
await _fetchNextSegment(voiceType: voiceType);
}
Future<void> _fetchNextSegment({String? voiceType}) async {
if (_sentenceQueue.isEmpty) {
_isFetching = false;
return;
}
try {
// 预加载多个片段以实现流畅播放
final segments = <String>[];
while (_sentenceQueue.isNotEmpty && segments.length < _preloadCount) {
segments.add(_sentenceQueue.removeAt(0));
}
for (final segment in segments) {
print('获取音频片段: $segment');
try {
// 使用提供的语音类型或默认语音类型,只尝试一次
final audioData = await _synthesize(segment, voiceType: voiceType);
if (audioData != null) {
print('成功获取音频数据: ${audioData.length} bytes');
try {
final audioSource = BytesAudioSource(audioData);
await _playlist.add(audioSource);
print('音频片段已添加到播放列表');
// 如果是第一个片段且播放器未在播放,开始播放
if (!_audioPlayer.playing && _playlist.length == 1) {
print('开始播放');
await _audioPlayer.play();
}
} catch (e) {
print('添加音频到播放列表失败: $e');
}
} else {
print('获取音频失败: 合成返回空数据');
}
} catch (e) {
print('获取音频失败: $e');
// 不重试,继续处理下一个片段
}
}
} catch (e) {
print('预加载音频片段失败: $e');
} finally {
_isFetching = false;
}
}
/// 合成文本为音频数据
Future<Uint8List?> _synthesize(String text, {String? voiceType}) async {
if (text.isEmpty) {
return null;
}
// 使用指定的语音类型,不使用任何回退
final effectiveVoiceType = voiceType ?? _voiceType;
try {
// 调用原生方法合成语音
final result = await _channel.invokeMethod<Uint8List>('synthesizeSync', {
// 直接调用原生方法合成并播放语音
final success = await _channel.invokeMethod<bool>('speakText', {
'text': text,
'voiceType': effectiveVoiceType,
'voiceType': voiceType ?? _voiceType,
});
if (result != null && result.isNotEmpty) {
return result;
}
// 如果没有结果,返回null
return null;
return success ?? false;
} catch (e) {
// 不处理授权错误,直接抛出所有错误
print('调用原生合成方法失败: $e');
throw VolcanoTtsException('调用原生合成方法失败', e);
}
}
/// 停止播放并清空队列
Future<void> stop() async {
try {
print('停止播放');
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
// 通知播放状态变化
_notifyPlayingStateChanged(false);
// 确保清空所有待处理的音频数据
try {
// 重置播放器状态
await _audioPlayer.pause();
await _audioPlayer.seek(Duration.zero);
print('已清空所有待播放的文字和音频');
} catch (e) {
print('清空音频缓存时出错: $e');
}
} catch (e) {
print('停止播放失败: $e');
}
}
/// 检查是否实际正在播放
bool isActuallyPlaying() {
try {
// 检查是否有活跃的播放任务
final hasActiveTask = _sentenceQueue.isNotEmpty || _isFetching;
// 检查播放器状态
final isPlayerPlaying = _audioPlayer.playing;
// 检查是否有待处理的文本
final hasPendingText = _pendingText.isNotEmpty;
// 检查播放列表是否为空
final hasAudioSource = _playlist.length > 0;
// 检查当前播放位置
final currentPosition = _audioPlayer.position;
final currentDuration = _audioPlayer.duration ?? Duration.zero;
// 检查是否已经播放到末尾但播放器状态未更新
final isAtEnd = currentDuration.inMilliseconds > 0 &&
currentPosition.inMilliseconds >= currentDuration.inMilliseconds - 100;
// 如果播放器显示正在播放,但已经到达音频末尾,则认为实际上没有播放
if (isPlayerPlaying && isAtEnd) {
// 如果检测到这种情况,尝试自动修复播放器状态
_fixPlayerState();
return false;
}
// 如果有活跃任务、播放器正在播放且未到末尾、有待处理文本,则认为正在播放
return hasActiveTask || (isPlayerPlaying && !isAtEnd) || hasPendingText;
} catch (e) {
print('检查TTS实际播放状态出错: $e');
print('语音合成失败: $e');
return false;
}
}
/// 修复播放器状态
void _fixPlayerState() {
try {
// 如果播放器显示正在播放,但实际上已经到达音频末尾,尝试修复状态
Future.microtask(() async {
try {
// 如果没有更多内容要播放,停止播放器
if (_sentenceQueue.isEmpty && _pendingText.isEmpty && !_isFetching) {
await _audioPlayer.stop();
// 如果播放列表中只有已播放完的项目,清空播放列表
if (_playlist.length > 0) {
await _playlist.clear();
}
// 通知播放状态变化
_notifyPlayingStateChanged(false);
} else {
// 如果还有内容要播放,尝试播放下一个
_tryPlayNext();
}
} catch (e) {
print('修复播放器状态失败: $e');
}
});
} catch (e) {
print('尝试修复播放器状态时出错: $e');
}
}
/// 通知播放状态变化
void _notifyPlayingStateChanged(bool isPlaying) {
try {
_playingStateController.add(isPlaying);
} catch (e) {
print('通知播放状态变化失败: $e');
}
}
/// 重新初始化播放器
Future<void> _reinitializePlayer() async {
try {
await stop();
await _audioPlayer.dispose();
// 重新初始化音频会话
await _initAudioSession();
_audioPlayer = AudioPlayer();
await _audioPlayer.setVolume(_defaultVolume);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setSpeed(_defaultSpeed);
// 重新配置Android特定的播放参数
if (Platform.isAndroid) {
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
}
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
print('播放器重新初始化成功');
} catch (e) {
print('播放器重新初始化失败: $e');
}
}
/// 设置事件监听器
void _setupEventListeners() {
_playbackEventSubscription?.cancel();
_playerStateSubscription?.cancel();
_playbackEventSubscription = _audioPlayer.playbackEventStream.listen(
(event) {
if (_isDisposed) return;
if (event.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
print('播放事件流错误: $error');
_reinitializePlayer();
},
);
_playerStateSubscription = _audioPlayer.playerStateStream.listen(
(state) {
if (_isDisposed) return;
// 通知播放状态变化
final isPlaying = state.playing && _playlist.length > 0;
_notifyPlayingStateChanged(isPlaying);
if (state.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
print('播放状态流错误: $error');
_reinitializePlayer();
},
);
/// 停止播放
Future<bool> stop() async {
if (!_isInitialized) {
print('TTS引擎尚未初始化');
return false;
}
/// 尝试播放下一个音频片段
Future<void> _tryPlayNext() async {
if (_isDisposed || _playlist.length <= 0) return;
try {
if (!_audioPlayer.playing) {
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
print('停止播放');
// 减少延迟时间
await Future.delayed(const Duration(milliseconds: 20));
// 调用原生方法停止播放
final success = await _channel.invokeMethod<bool>('stopSpeaking');
if (!_isDisposed && !_audioPlayer.playing) {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
}
}
print('已停止播放');
return success ?? false;
} catch (e) {
if (e is! StateError) {
print('播放错误: $e');
await _reinitializePlayer();
}
print('停止播放失败: $e');
return false;
}
}
@ -743,40 +156,57 @@ class VolcanoTtsService extends GetxService {
}
/// 设置播放速度
Future<void> setSpeed(double speed) async {
Future<bool> setSpeed(double speed) async {
if (!_isInitialized) {
print('TTS引擎尚未初始化');
return false;
}
if (speed < 0.5 || speed > 2.0) {
throw VolcanoTtsException('播放速度必须在0.5到2.0之间');
}
try {
await _audioPlayer.setSpeed(speed);
// 设置原生播放速度
final success = await _channel.invokeMethod<bool>('setSpeed', {
'speed': speed,
});
return success ?? false;
} catch (e) {
print('设置播放速度失败: $e');
return false;
}
}
/// 设置音量
Future<void> setVolume(double volume) async {
Future<bool> setVolume(double volume) async {
if (!_isInitialized) {
print('TTS引擎尚未初始化');
return false;
}
if (volume < 0.0 || volume > 1.0) {
throw VolcanoTtsException('音量必须在0.0到1.0之间');
}
try {
await _audioPlayer.setVolume(volume);
// 设置原生音量
final success = await _channel.invokeMethod<bool>('setVolume', {
'volume': volume,
});
return success ?? false;
} catch (e) {
print('设置音量失败: $e');
return false;
}
}
/// 清理资源
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playbackEventSubscription?.cancel();
await _playerStateSubscription?.cancel();
await _audioSession?.setActive(false);
await stop();
await _audioPlayer.dispose();
await _playingStateController.close();
// 释放原生资源
try {
@ -798,4 +228,14 @@ class VolcanoTtsService extends GetxService {
return _voiceType;
}
/// 检查是否正在播放(简化版,直接返回true)
bool isActuallyPlaying() {
return true;
}
/// 检查是否正在合成(简化版,直接返回true)
bool isActuallySynthesizing() {
return true;
}
}

6
lib/modules/chat/bindings/chat_binding.dart

@ -1,13 +1,13 @@
import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
class ChatBinding implements Bindings {
@override
void dependencies() {
// Ensure TTS service is registered first
if (!Get.isRegistered<VolcanoTtsService>()) {
Get.put(VolcanoTtsService());
if (!Get.isRegistered<VolcanoTtsApiService>()) {
Get.put(VolcanoTtsApiService());
}
// Create new controller instance

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

@ -4,9 +4,10 @@ import 'package:get_storage/get_storage.dart';
import 'dart:convert';
import 'package:flutter/rendering.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../models/message_model.dart';
import '../controllers/voice_input_controller.dart';
import 'dart:async';
class ChatController extends GetxController {
String agentId = '';
@ -18,7 +19,7 @@ class ChatController extends GetxController {
bool playVoiceOnEnter = false;
final VolcanoAIService _aiService = VolcanoAIService();
late final VolcanoTtsService _ttsService;
late final VolcanoTtsApiService _ttsService;
static const String _storagePrefix = 'chat_history_';
final messages = <Message>[].obs;
@ -27,6 +28,9 @@ class ChatController extends GetxController {
late final ScrollController scrollController;
final _storage = GetStorage();
// TTS启用状态
final isTtsEnabled = true.obs;
// 用于存储当前正在流式生成的消息
final RxString currentStreamMessage = ''.obs;
Message? _currentAssistantMessage;
@ -34,6 +38,10 @@ class ChatController extends GetxController {
static const int _minTtsLength = 20;
bool _isDisposed = false;
// TTS事件订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
// 语音输入相关状态
final isRecording = false.obs;
final recordingText = ''.obs;
@ -65,13 +73,13 @@ class ChatController extends GetxController {
// 辅助方法:管理TTS状态
void _manageTtsState(bool enable) {
if (!enable && _ttsService.isEnabled.value) {
if (!enable && isTtsEnabled.value) {
// 需要禁用TTS
_ttsService.stop(); // 先停止当前播放
_ttsService.isEnabled.value = false;
} else if (enable && !_ttsService.isEnabled.value) {
_ttsService.endSession(); // 先停止当前播放
isTtsEnabled.value = false;
} else if (enable && !isTtsEnabled.value) {
// 需要启用TTS
_ttsService.isEnabled.value = true;
isTtsEnabled.value = true;
}
}
@ -84,7 +92,10 @@ class ChatController extends GetxController {
Get.delete<VoiceInputController>();
}
// 不再需要禁用TTS,因为已启用回声消除
// 如果TTS正在播放,先停止它
if (_isTtsSpeaking) {
_ttsService.endSession();
}
isVoiceInputVisible.value = true;
isVoiceConnecting.value = true;
@ -299,6 +310,13 @@ class ChatController extends GetxController {
messages.add(_currentAssistantMessage!);
_pendingTtsText = '';
// 确保在开始新的合成前停止当前播放
if (isTtsEnabled.value) {
await _ttsService.endSession();
// 添加短暂延迟确保停止完成
await Future.delayed(const Duration(milliseconds: 200));
}
await for (final chunk in _aiService.sendMessageStream(
messages: messages
.map((m) => {
@ -315,7 +333,7 @@ class ChatController extends GetxController {
// 累积文本并合成
_pendingTtsText += chunk;
if (!_isDisposed && _ttsService.isEnabled.value) {
if (!_isDisposed && isTtsEnabled.value) {
// 检查是否达到最小长度
if (_pendingTtsText.length >= _minTtsLength) {
// 找到最后一个句子结束的位置
@ -324,7 +342,10 @@ class ChatController extends GetxController {
// 播放到最后一个句子结束的位置
String textToSpeak =
_pendingTtsText.substring(0, lastSentenceEnd + 1);
_ttsService.speak(textToSpeak);
// 使用新方法替代直接调用speak
_speakText(textToSpeak);
// 保留剩余的文本
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
}
@ -334,9 +355,10 @@ class ChatController extends GetxController {
// 处理剩余的文本
if (!_isDisposed &&
_ttsService.isEnabled.value &&
isTtsEnabled.value &&
_pendingTtsText.isNotEmpty) {
_ttsService.speak(_pendingTtsText);
// 使用新方法替代直接调用speak
_speakText(_pendingTtsText);
}
_pendingTtsText = '';
@ -363,6 +385,32 @@ class ChatController extends GetxController {
}
}
// 播放文本并使用WebSocket流式合成
Future<void> _speakText(String text) async {
if (text.isEmpty || !isTtsEnabled.value) return;
try {
// 如果当前有正在进行的TTS会话,先结束它
if (_isTtsSpeaking) {
await _ttsService.endSession();
}
// 确保连接已建立
if (!_ttsService.isConnected.value) {
final connected = await _ttsService.connect();
if (!connected) {
print('无法连接到TTS服务');
return;
}
}
// 开始合成
await _ttsService.synthesize(text, 'zh_female_1'); // 使用默认说话人
} catch (e) {
print('TTS播放出错: $e');
}
}
// 确保滚动到底部的方法,使用多种策略确保成功
void _ensureScrollToBottom() {
// 立即尝试滚动
@ -465,8 +513,37 @@ class ChatController extends GetxController {
}
void _initTtsService() {
_ttsService = Get.find<VolcanoTtsService>();
_ttsService.isEnabled.value = true;
_ttsService = Get.find<VolcanoTtsApiService>();
isTtsEnabled.value = true;
// 设置TTS事件监听
_setupTtsEventListener();
}
void _setupTtsEventListener() {
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
}
void _handleTtsEvent(Map<String, dynamic> event) {
if (_isDisposed) return;
final String eventType = event['type'] as String? ?? 'unknown';
switch (eventType) {
case 'ttsSentenceStart':
_isTtsSpeaking = true;
break;
case 'ttsSentenceEnd':
case 'sessionFinished':
case 'connectionFinished':
_isTtsSpeaking = false;
break;
case 'connectionFailed':
case 'sessionFailed':
_isTtsSpeaking = false;
print('TTS错误: ${event['message']}');
break;
}
}
Future<void> _initializeController() async {
@ -511,8 +588,9 @@ class ChatController extends GetxController {
} catch (e) {
print('显示欢迎消息失败: $e');
// 如果生成问候语失败,确保至少显示静态欢迎消息
if (_ttsService.isEnabled.value) {
_ttsService.speak(welcomeMessage);
if (isTtsEnabled.value) {
// 使用新方法替代直接调用speak
_speakText(welcomeMessage);
}
// 即使问候语生成失败,如果playVoiceOnEnter为true,仍然启动语音输入
@ -561,10 +639,9 @@ class ChatController extends GetxController {
}
// 完整问候语生成完毕后,一次性播放
if (!_isDisposed &&
_ttsService.isEnabled.value &&
fullGreeting.isNotEmpty) {
await _ttsService.speak(fullGreeting);
if (!_isDisposed && isTtsEnabled.value && fullGreeting.isNotEmpty) {
// 使用新方法替代直接调用speak
await _speakText(fullGreeting);
}
} catch (e) {
print('生成问候语失败: $e');
@ -588,7 +665,11 @@ class ChatController extends GetxController {
stopVoiceInput();
// 先停止所有正在进行的操作
_ttsService.stop();
_ttsService.endSession(); // 使用endSession替代stop
// 取消TTS事件订阅
_ttsEventSubscription?.cancel();
currentStreamMessage.value = '';
_currentAssistantMessage = null;
_pendingTtsText = '';
@ -737,7 +818,7 @@ class ChatController extends GetxController {
// 累积文本并合成
_pendingTtsText += chunk;
if (!_isDisposed && _ttsService.isEnabled.value) {
if (!_isDisposed && isTtsEnabled.value) {
// 检查是否达到最小长度
if (_pendingTtsText.length >= _minTtsLength) {
// 找到最后一个句子结束的位置
@ -746,7 +827,8 @@ class ChatController extends GetxController {
// 播放到最后一个句子结束的位置
String textToSpeak =
_pendingTtsText.substring(0, lastSentenceEnd + 1);
_ttsService.speak(textToSpeak);
// 使用新方法替代直接调用speak
_speakText(textToSpeak);
// 保留剩余的文本
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
}
@ -755,10 +837,9 @@ class ChatController extends GetxController {
}
// 处理剩余的文本
if (!_isDisposed &&
_ttsService.isEnabled.value &&
_pendingTtsText.isNotEmpty) {
_ttsService.speak(_pendingTtsText);
if (!_isDisposed && isTtsEnabled.value && _pendingTtsText.isNotEmpty) {
// 使用新方法替代直接调用speak
_speakText(_pendingTtsText);
}
_pendingTtsText = '';
@ -816,10 +897,10 @@ class ChatController extends GetxController {
}
void toggleTTS() {
// 使用 VolcanoTtsService 的 toggleEnabled 方法
_ttsService.toggleEnabled();
if (!_ttsService.isEnabled.value) {
_ttsService.stop();
// 切换TTS启用状态
isTtsEnabled.toggle();
if (!isTtsEnabled.value) {
_ttsService.endSession(); // 使用endSession替代stop
}
}
@ -856,8 +937,9 @@ class ChatController extends GetxController {
}
// 完整问候语生成完毕后,一次性播放
if (!_isDisposed && _ttsService.isEnabled.value && fullGreeting.isNotEmpty) {
await _ttsService.speak(fullGreeting);
if (!_isDisposed && isTtsEnabled.value && fullGreeting.isNotEmpty) {
// 使用新方法替代直接调用speak
await _speakText(fullGreeting);
}
} catch (e) {
print('生成问候语失败: $e');

69
lib/modules/chat/controllers/voice_input_controller.dart

@ -2,9 +2,10 @@ import 'dart:async';
import 'dart:math';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import '../../../data/services/volcano_voice_recognition_service.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class VoiceInputController extends GetxController {
// Observable states
@ -17,12 +18,16 @@ class VoiceInputController extends GetxController {
final isUserSpeaking = false.obs;
// 语音识别服务
late final VolcanoVoiceRecognitionService _voiceService;
late final VolcanoTtsService _ttsService;
late final AzureAsrService _voiceService;
late final VolcanoTtsApiService _ttsService;
// 连续识别相关
StreamSubscription? _recognitionSubscription;
// TTS相关订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
// 添加一个标志,表示是否已经识别到语音
bool _hasRecognizedSpeech = false;
@ -60,19 +65,48 @@ class VoiceInputController extends GetxController {
void onInit() {
super.onInit();
_initializeVoiceService();
_ttsService = Get.find<VolcanoTtsService>();
_ttsService = Get.find<VolcanoTtsApiService>();
// 设置TTS事件监听
_setupTtsEventListener();
// 启动定期检查用户是否正在说话的计时器
_startSpeakingCheckTimer();
}
// 设置TTS事件监听
void _setupTtsEventListener() {
_ttsEventSubscription?.cancel();
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
}
// 处理TTS事件
void _handleTtsEvent(Map<String, dynamic> event) {
final eventType = event['eventType'];
switch (eventType) {
case 'ttsSentenceStart':
_isTtsSpeaking = true;
break;
case 'ttsSentenceEnd':
case 'sessionFinished':
case 'error':
_isTtsSpeaking = false;
break;
}
}
Future<void> _initializeVoiceService() async {
try {
// 获取语音识别服务实例
_voiceService = Get.find<VolcanoVoiceRecognitionService>();
_voiceService = Get.find<AzureAsrService>();
// 初始化语音识别服务
await _voiceService.initialize();
await _voiceService.initialize(
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
);
// 初始化完成后更新状态
isConnecting.value = false;
@ -124,18 +158,9 @@ class VoiceInputController extends GetxController {
isUserSpeaking.value = true;
// 如果系统正在播放TTS,检测到用户开始说话时立即中断
bool isSpeaking = false;
try {
isSpeaking = _ttsService.isActuallyPlaying();
} catch (e) {
print('获取 TTS 播放状态失败: $e');
// 如果获取失败,假设为false
isSpeaking = false;
}
if (isSpeaking && event.text.trim().isNotEmpty) {
if (_isTtsSpeaking && event.text.trim().isNotEmpty) {
print('检测到用户开始说话,中断TTS播放');
_ttsService.stop(); // 停止当前TTS播放
_ttsService.endSession(); // 结束当前TTS会话
}
// 取消之前的静默计时器
@ -201,7 +226,6 @@ class VoiceInputController extends GetxController {
break;
case RecognitionEventType.error:
// 处理错误
print('语音识别错误: ${event.error}');
Get.snackbar(
'Error',
@ -212,7 +236,7 @@ class VoiceInputController extends GetxController {
restartRecognition();
break;
case RecognitionEventType.completed:
case RecognitionEventType.sessionStopped:
// 会话结束,尝试重新启动
isRecording.value = false;
restartRecognition();
@ -280,7 +304,7 @@ class VoiceInputController extends GetxController {
// 停止连续识别
if (_voiceService.isContinuousRecognitionActive()) {
await _voiceService.stopRecognition();
await _voiceService.stopContinuousRecognition();
}
isRecording.value = false;
@ -345,6 +369,9 @@ class VoiceInputController extends GetxController {
// 取消计时器
_speakingCheckTimer?.cancel();
// 取消TTS事件订阅
_ttsEventSubscription?.cancel();
// 重置状态
isConnecting.value = false;
isMuted.value = false;

12
lib/modules/chat/views/chat_view.dart

@ -3,7 +3,7 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/chat_controller.dart';
import '../models/message_model.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../widgets/voice_input_panel.dart';
class ChatView extends GetView<ChatController> {
@ -14,8 +14,8 @@ class ChatView extends GetView<ChatController> {
return WillPopScope(
onWillPop: () async {
// 停止TTS服务
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
final tts = Get.find<VolcanoTtsApiService>();
await tts.endSession();
// 停止语音输入
if (controller.isVoiceInputVisible.value) {
@ -53,8 +53,8 @@ class ChatView extends GetView<ChatController> {
),
onPressed: () async {
// 停止TTS服务
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
final tts = Get.find<VolcanoTtsApiService>();
await tts.endSession();
// 停止语音输入
if (controller.isVoiceInputVisible.value) {
@ -90,7 +90,7 @@ class ChatView extends GetView<ChatController> {
actions: [
IconButton(
icon: Obx(() => Icon(
Get.find<VolcanoTtsService>().isEnabled.value
controller.isTtsEnabled.value
? Icons.volume_up
: Icons.volume_off,
color: Colors.black,

8
lib/modules/explore/views/explore_view.dart

@ -6,6 +6,7 @@ import '../../chat/controllers/chat_controller.dart';
import '../../chat/views/chat_view.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../core/routes/app_routes.dart';
class ExploreView extends GetView<ExploreController> {
const ExploreView({Key? key}) : super(key: key);
@ -22,6 +23,13 @@ class ExploreView extends GetView<ExploreController> {
return;
}
// 检查是否是同声翻译官
if (agent.id == 'simultaneous_interpreter') {
// 导航到翻译页面
Get.toNamed(Routes.translation);
return;
}
// 导航到聊天页面,让 binding 来处理控制器的创建
Get.toNamed('/chat', arguments: {
'agentId': agent.id,

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

@ -128,9 +128,9 @@ class ProfileView extends GetView<ProfileController> {
),
const Divider(),
_buildMenuItem(
title: '火山语音识别测试'.tr,
title: 'Azure语音识别测试'.tr,
icon: Icons.mic,
subtitle: '测试火山语音ASR功能',
subtitle: '测试Azure语音ASR功能',
onTap: () {
Get.toNamed(Routes.ASR_TEST);
},

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

@ -1,10 +1,16 @@
import 'package:get/get.dart';
import '../controllers/tts_test_controller.dart';
import '../controllers/asr_test_controller.dart';
import '../../../data/services/volcano_tts_api_service.dart';
class TtsTestBinding extends Bindings {
@override
void dependencies() {
// Make sure the VolcanoTtsApiService is registered
if (!Get.isRegistered<VolcanoTtsApiService>()) {
Get.put(VolcanoTtsApiService(), permanent: true);
}
Get.lazyPut<TtsTestController>(
() => TtsTestController(),
);

53
lib/modules/test/controllers/asr_test_controller.dart

@ -1,9 +1,10 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../data/services/volcano_voice_recognition_service.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../core/utils/logger.dart';
class AsrTestController extends GetxController {
final VolcanoVoiceRecognitionService _voiceService = Get.find<VolcanoVoiceRecognitionService>();
final AzureAsrService _azureService = Get.find<AzureAsrService>();
// 可观察状态
final isListening = false.obs;
@ -18,13 +19,7 @@ class AsrTestController extends GetxController {
void onInit() {
super.onInit();
// 监听语音识别服务的状态
_voiceService.isListening.listen((listening) {
isListening.value = listening;
if (!listening) {
errorMessage.value = '';
}
});
// 由于 AzureAsrService 没有公开的 isListening 属性,我们需要自己管理状态
}
/// 开始录音
@ -34,13 +29,15 @@ class AsrTestController extends GetxController {
if (isContinuous.value) {
// 连续识别模式
final success = await _voiceService.startContinuousRecognition();
final success = await _azureService.startContinuousRecognition();
if (!success) {
errorMessage.value = '无法启动语音识别';
return;
}
_recognitionSubscription = _voiceService.recognitionStream?.listen(
isListening.value = true;
_recognitionSubscription = _azureService.recognitionStream?.listen(
(event) {
switch (event.type) {
case RecognitionEventType.finalResult:
@ -50,6 +47,14 @@ class AsrTestController extends GetxController {
break;
case RecognitionEventType.error:
errorMessage.value = event.error ?? '未知错误';
isListening.value = false;
break;
case RecognitionEventType.canceled:
errorMessage.value = '识别已取消: ${event.reason ?? "未知原因"}';
isListening.value = false;
break;
case RecognitionEventType.sessionStopped:
isListening.value = false;
break;
default:
break;
@ -62,16 +67,20 @@ class AsrTestController extends GetxController {
);
} else {
// 一次性识别模式
final success = await _voiceService.startOneTimeRecognition();
if (!success) {
errorMessage.value = '无法启动语音识别';
return;
isListening.value = true;
try {
final result = await _azureService.recognizeOnce();
if (result.isNotEmpty) {
recognitionResults.add(result);
}
} catch (e) {
errorMessage.value = '识别失败: $e';
} finally {
isListening.value = false;
}
// 一次性识别模式下,结果会通过服务的recognitionResults获取
// 在onInit中我们已经监听了isListening状态,当识别完成时会自动更新UI
}
} catch (e) {
Logger.error('启动语音识别失败', e);
errorMessage.value = e.toString();
isListening.value = false;
}
@ -81,12 +90,18 @@ class AsrTestController extends GetxController {
void stopListening() async {
try {
if (isContinuous.value) {
await _voiceService.stopRecognition();
final success = await _azureService.stopContinuousRecognition();
if (!success) {
errorMessage.value = '停止识别失败';
}
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
isListening.value = false;
}
} catch (e) {
Logger.error('停止语音识别失败', e);
errorMessage.value = e.toString();
isListening.value = false;
}
}

309
lib/modules/test/controllers/tts_test_controller.dart

@ -1,120 +1,297 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/services/volcano_tts_service.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../../../data/services/volcano_tts_api_service.dart';
class TtsTestController extends GetxController {
final VolcanoTtsService _ttsService = Get.find<VolcanoTtsService>();
// 文本控制器
final TextEditingController textController = TextEditingController();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
// 可观察状态
final isPlaying = false.obs;
final isSpeaking = false.obs;
final errorMessage = ''.obs;
final selectedVoiceType = ''.obs;
final voiceTypes = <String>[].obs;
final isLoading = false.obs;
final statusMessage = '未连接'.obs;
// 当前播放的索引
final currentPlayingIndex = RxInt(-1);
// 固定的播报内容
final RxList<String> speechTexts = <String>[
'这是第一条播报测试,请听取合成效果。',
'这是第二条播报测试,火山语音合成支持连续多条播报。',
'这是第三条播报测试,可以测试语音合成的流畅度和连贯性。',
'这是第四条播报测试,感谢您的使用。',
].obs;
// 事件订阅
StreamSubscription? _eventSubscription;
@override
void onInit() {
super.onInit();
// 监听TTS播放状态
_ttsService.playingStateStream.listen((playing) {
isPlaying.value = playing;
if (!playing) {
// 初始化UI状态
isSpeaking.value = false;
errorMessage.value = '';
isLoading.value = false;
// 监听TTS服务状态
ever(_ttsService.isConnected, (bool connected) {
if (connected) {
statusMessage.value = '已连接';
} else {
statusMessage.value = '未连接';
isSpeaking.value = false;
}
});
// 监听TTS合成状态
ever(_ttsService.isSynthesizing, (bool synthesizing) {
isSpeaking.value = synthesizing;
});
// 监听TTS播放状态
ever(_ttsService.isPlaying.obs, (bool playing) {
isSpeaking.value = playing;
if (!playing && currentPlayingIndex.value >= 0) {
// 如果播放结束,重置当前播放索引
currentPlayingIndex.value = -1;
}
});
// 设置默认文本
textController.text = '这是一个火山语音合成测试,请点击播放按钮听取合成效果。';
// 设置事件监听
_setupEventListener();
// 连接到TTS服务
_connectToTtsService();
}
// 设置事件监听
void _setupEventListener() {
_eventSubscription?.cancel();
_eventSubscription = _ttsService.eventStream.listen(_handleEvent);
}
// 设置可用的语音类型
voiceTypes.value = [
// 趣味方言
'zh_female_wanqudashu_moon_bigtts', // 湾区大叔
'zh_female_daimengchuanmei_moon_bigtts', // 呆萌川妹
'zh_male_guozhoudege_moon_bigtts', // 广州德哥
'zh_male_beijingxiaoye_moon_bigtts', // 北京小爷
'zh_male_haoyuxiaoge_moon_bigtts', // 浩宇小哥
// 处理事件
void _handleEvent(Map<String, dynamic> event) {
final eventType = event['eventType'];
// 通用场景
'zh_male_shaonianzixin_moon_bigtts', // 少年梓辛/Brayan
switch (eventType) {
case 'connectionStarted':
statusMessage.value = '连接成功';
errorMessage.value = '';
break;
case 'connectionFailed':
statusMessage.value = '连接失败';
errorMessage.value = event['error'] ?? '未知错误';
break;
case 'sessionStarted':
statusMessage.value = '会话开始';
errorMessage.value = '';
break;
case 'sessionFailed':
statusMessage.value = '会话失败';
errorMessage.value = event['error'] ?? '未知错误';
currentPlayingIndex.value = -1;
break;
case 'ttsSentenceStart':
statusMessage.value = '开始合成';
break;
case 'ttsSentenceEnd':
statusMessage.value = '合成完成';
break;
case 'sessionFinished':
statusMessage.value = '会话结束';
currentPlayingIndex.value = -1;
break;
case 'connectionFinished':
statusMessage.value = '连接关闭';
currentPlayingIndex.value = -1;
break;
case 'error':
statusMessage.value = '错误';
errorMessage.value = event['error'] ?? '未知错误';
break;
case 'disconnected':
statusMessage.value = '连接已断开';
currentPlayingIndex.value = -1;
break;
default:
statusMessage.value = '事件: $eventType';
break;
}
// 角色扮演
'zh_female_meilinvyou_moon_bigtts', // 魅力女友
'zh_male_shenyeboke_moon_bigtts', // 深夜播客
'zh_female_sajiaonvyou_moon_bigtts', // 柔美女友
'zh_female_yuanqinvyou_moon_bigtts', // 撒娇学妹
// 强制更新UI
update();
}
];
// 连接到TTS服务
Future<void> _connectToTtsService() async {
try {
isLoading.value = true;
statusMessage.value = '正在连接...';
// 设置当前选中的语音类型
selectedVoiceType.value = _ttsService.getCurrentVoiceType();
final success = await _ttsService.connect();
// 如果当前语音类型不在列表中,添加到列表
if (!voiceTypes.contains(selectedVoiceType.value)) {
voiceTypes.add(selectedVoiceType.value);
if (!success) {
statusMessage.value = '连接失败';
errorMessage.value = '无法连接到火山语音服务';
}
} catch (e) {
statusMessage.value = '连接错误';
errorMessage.value = e.toString();
} finally {
isLoading.value = false;
}
}
/// 播放文本
void speakText() async {
final text = textController.text.trim();
if (text.isEmpty) {
errorMessage.value = '请输入要合成的文本';
/// 更新UI状态
void updateUIState() {
// 强制更新UI
update();
}
/// 播放单条文本
Future<void> speakText(int index) async {
if (index < 0 || index >= speechTexts.length || isSpeaking.value) {
return;
}
errorMessage.value = '';
try {
isLoading.value = true;
errorMessage.value = '';
currentPlayingIndex.value = index;
final text = speechTexts[index];
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
// 确保TTS服务已连接
if (!_ttsService.isConnected.value) {
await _connectToTtsService();
}
// 开始会话
await _ttsService.startSession(speaker);
// 播放文本
await _ttsService.speak(text, speaker: speaker);
// 等待播放完成
while (_ttsService.isActuallyPlaying()) {
await Future.delayed(const Duration(milliseconds: 100));
}
// 结束会话
await _ttsService.endSession();
} catch (e) {
errorMessage.value = '播放出错';
if (kDebugMode) {
print('播放出错: $e');
}
} finally {
isLoading.value = false;
currentPlayingIndex.value = -1;
}
}
/// 连续播放所有文本
Future<void> speakContinuousTexts() async {
if (isSpeaking.value || isLoading.value) {
return;
}
try {
// 获取当前语音类型
final currentVoiceType = selectedVoiceType.value;
isLoading.value = true;
isSpeaking.value = true;
errorMessage.value = '';
// 确保TTS服务已连接
if (!_ttsService.isConnected.value) {
await _connectToTtsService();
}
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
// 停止之前的播放
await _ttsService.stop();
// 开始会话
await _ttsService.startSession(speaker);
// 连续播放所有文本
for (int i = 0; i < speechTexts.length; i++) {
// 如果播放被中断,退出循环
if (!isSpeaking.value) break;
currentPlayingIndex.value = i;
statusMessage.value = '正在播放第 ${i + 1}/${speechTexts.length} 条文本';
// 直接使用选定的语音类型,不进行可用性检查
// 播放当前文本
await _ttsService.speak(speechTexts[i], speaker: speaker);
}
// 结束会话
await _ttsService.endSession();
statusMessage.value = '播放完成';
// 播放文本,传递选定的语音类型
await _ttsService.speak(text, voiceType: currentVoiceType);
} catch (e) {
errorMessage.value = e.toString();
isPlaying.value = false;
errorMessage.value = '播放出错';
if (kDebugMode) {
print('连续播放出错: $e');
}
// 如果错误包含资源授权相关信息,提供更具体的提示
final errorMsg = e.toString().toLowerCase();
if (errorMsg.contains('resource not granted') ||
errorMsg.contains('403') ||
errorMsg.contains('授权') ||
errorMsg.contains('3001')) {
errorMessage.value = '语音类型授权错误: 您可能没有权限使用当前选择的语音类型,请尝试使用基础语音类型';
// 尝试结束会话,忽略可能的错误
try {
await _ttsService.endSession();
} catch (_) {
// 忽略结束会话时的错误
}
} finally {
currentPlayingIndex.value = -1;
isSpeaking.value = false;
isLoading.value = false;
}
}
/// 停止播放
void stopSpeaking() {
void stopSpeaking() async {
try {
_ttsService.stop();
isSpeaking.value = false;
statusMessage.value = '正在停止播放...';
// 停止播放
await _ttsService.stop();
// 结束会话
await _ttsService.endSession();
// 重置UI状态
currentPlayingIndex.value = -1;
statusMessage.value = '已停止播放';
} catch (e) {
errorMessage.value = e.toString();
}
if (kDebugMode) {
print('停止播放出错: $e');
}
/// 切换语音类型
void changeVoiceType(String voiceType) {
if (voiceType != selectedVoiceType.value) {
selectedVoiceType.value = voiceType;
errorMessage.value = '';
errorMessage.value = '停止播放出错';
} finally {
isSpeaking.value = false;
isLoading.value = false;
}
}
@override
void onClose() {
textController.dispose();
// 取消事件订阅
_eventSubscription?.cancel();
// 断开TTS服务连接
_ttsService.disconnect();
super.onClose();
}
}

2
lib/modules/test/views/asr_test_view.dart

@ -9,7 +9,7 @@ class AsrTestView extends GetView<AsrTestController> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('火山语音识别测试'),
title: const Text('Azure 语音识别测试'),
centerTitle: true,
),
body: Padding(

219
lib/modules/test/views/tts_test_view.dart

@ -7,17 +7,24 @@ class TtsTestView extends GetView<TtsTestController> {
@override
Widget build(BuildContext context) {
// 强制更新控制器状态
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在下一帧渲染完成后调用
controller.updateUIState();
});
return Scaffold(
appBar: AppBar(
title: const Text('火山语音合成测试'),
title: const Text('语音合成测试'),
centerTitle: true,
),
body: Padding(
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),
@ -25,38 +32,56 @@ class TtsTestView extends GetView<TtsTestController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'语音类型',
'播放控制',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Obx(() => DropdownButton<String>(
isExpanded: true,
value: controller.selectedVoiceType.value,
onChanged: (String? newValue) {
if (newValue != null) {
controller.selectedVoiceType.value = newValue;
}
},
items: controller.voiceTypes.map<DropdownMenuItem<String>>((String value) {
// 根据语音类型ID获取友好名称
String displayName = _getVoiceTypeDisplayName(value);
return DropdownMenuItem<String>(
value: value,
child: Text(displayName),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() {
return ElevatedButton.icon(
onPressed: controller.isSpeaking.value || controller.isLoading.value
? null
: () => controller.speakContinuousTexts(),
icon: controller.isSpeaking.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2)
)
: const Icon(Icons.playlist_play),
label: Text(controller.isSpeaking.value ? '播放中...' : '全部播放'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
);
}).toList(),
}),
Obx(() => ElevatedButton.icon(
onPressed: controller.isSpeaking.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),
@ -64,72 +89,61 @@ class TtsTestView extends GetView<TtsTestController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'输入要合成的文本',
'播报内容列表',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextField(
controller: controller.textController,
maxLines: 5,
decoration: const InputDecoration(
hintText: '请输入要转换为语音的文本...',
border: OutlineInputBorder(),
),
),
],
),
),
),
const SizedBox(height: 16),
// 生成5条可点击的播报内容
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.isSpeaking.value &&
controller.currentPlayingIndex.value == index;
// 播放控制
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'播放控制',
return ListTile(
title: Text(
controller.speechTexts[index],
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
fontWeight: isPlaying ? FontWeight.bold : FontWeight.normal,
color: isPlaying ? Colors.blue : null,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() => ElevatedButton.icon(
onPressed: controller.isPlaying.value || controller.isLoading.value
? null
: () => controller.speakText(),
icon: controller.isLoading.value
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)
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.play_arrow),
label: Text(controller.isLoading.value ? '准备中...' : '播放'),
)),
ElevatedButton.icon(
onPressed: controller.isPlaying.value
? () => controller.stopSpeaking()
: null,
icon: const Icon(Icons.stop),
label: const Text('停止'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
],
onTap: controller.isSpeaking.value || controller.isLoading.value
? null
: () => controller.speakText(index),
enabled: !(controller.isSpeaking.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,
),
);
},
)),
],
),
),
@ -153,15 +167,11 @@ class TtsTestView extends GetView<TtsTestController> {
),
const SizedBox(height: 8),
Obx(() => Text(
controller.isLoading.value
? '准备中...'
: controller.isPlaying.value
? '正在播放...'
: '就绪',
controller.statusMessage.value,
style: TextStyle(
color: controller.isLoading.value
? Colors.orange
: controller.isPlaying.value
: controller.isSpeaking.value
? Colors.green
: Colors.grey,
fontWeight: FontWeight.bold,
@ -197,17 +207,6 @@ class TtsTestView extends GetView<TtsTestController> {
controller.errorMessage.value,
style: TextStyle(color: Colors.red.shade800),
),
if (controller.errorMessage.value.contains('授权错误'))
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'提示: 请尝试选择基础语音类型,如"zh_male_qingse_common"或"zh_female_qingse_common"',
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.red.shade700,
),
),
),
],
),
)
@ -219,49 +218,7 @@ class TtsTestView extends GetView<TtsTestController> {
],
),
),
),
);
}
// 根据语音类型ID获取友好名称
String _getVoiceTypeDisplayName(String voiceTypeId) {
switch (voiceTypeId) {
// 趣味方言
case 'zh_female_wanqudashu_moon_bigtts':
return '湾区大叔 (趣味方言)';
case 'zh_female_daimengchuanmei_moon_bigtts':
return '呆萌川妹 (趣味方言)';
case 'zh_male_guozhoudege_moon_bigtts':
return '广州德哥 (趣味方言)';
case 'zh_male_beijingxiaoye_moon_bigtts':
return '北京小爷 (趣味方言)';
case 'zh_male_haoyuxiaoge_moon_bigtts':
return '浩宇小哥 (趣味方言)';
// 通用场景
case 'zh_male_shaonianzixin_moon_bigtts':
return '少年梓辛/Brayan (通用场景)';
// 角色扮演
case 'zh_female_meilinvyou_moon_bigtts':
return '魅力女友 (角色扮演)';
case 'zh_male_shenyeboke_moon_bigtts':
return '深夜播客 (角色扮演)';
case 'zh_female_sajiaonvyou_moon_bigtts':
return '柔美女友 (角色扮演)';
case 'zh_female_yuanqinvyou_moon_bigtts':
return '撒娇学妹 (角色扮演)';
// 基础语音类型
case 'zh_male_qingse_common':
return '基础男声';
case 'zh_female_qingse_common':
return '基础女声';
case 'zh_male_M392_conversation_wvae_bigtts':
return '高级男声';
case 'zh_female_F392_conversation_wvae_bigtts':
return '高级女声';
default:
return voiceTypeId;
}
}
}

9
lib/modules/translation/bindings/translation_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/translation_controller.dart';
class TranslationBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TranslationController>(() => TranslationController());
}
}

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

@ -0,0 +1,334 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../data/services/volcano_asr_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../../../core/utils/logger.dart';
import '../models/translation_message_model.dart';
class TranslationController extends GetxController {
// Observable states
final isListening = false.obs;
final isTranslating = false.obs;
final sourceLanguage = "Chinese".obs;
final targetLanguage = "English".obs;
final originalText = "".obs;
final translatedText = "".obs;
final isUserSpeaking = false.obs;
final messages = <TranslationMessage>[].obs;
// TTS启用状态
final isTtsEnabled = true.obs;
// Services
late final VolcanoAsrService _voiceService;
late final VolcanoTtsApiService _ttsService;
// Stream subscriptions
StreamSubscription? _recognitionSubscription;
// Timers
Timer? _silenceTimer;
Timer? _speakingCheckTimer;
// State tracking
bool _hasRecognizedSpeech = false;
bool _hasFinalResult = false;
DateTime? _lastSpeechTime;
Completer<String>? _recognitionCompleter;
// TTS事件订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
@override
void onInit() {
super.onInit();
_initializeServices();
_startSpeakingCheckTimer();
_ttsService = Get.find<VolcanoTtsApiService>();
// 设置TTS事件监听
_setupTtsEventListener();
}
Future<void> _initializeServices() async {
try {
_voiceService = Get.find<VolcanoAsrService>();
_ttsService = Get.find<VolcanoTtsApiService>();
await _voiceService.initialize();
await startListening();
} catch (e) {
Logger.error('Failed to initialize translation services', e);
Get.snackbar(
'Error',
'Failed to initialize translation services: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> startListening() async {
if (isListening.value) return;
try {
isListening.value = true;
originalText.value = '';
translatedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
_recognitionCompleter = Completer<String>();
final success = await _voiceService.startContinuousRecognition();
if (!success) {
isListening.value = false;
throw Exception('Failed to start voice recognition');
}
_recognitionSubscription = _voiceService.recognitionStream?.listen(
(event) {
switch (event.type) {
case RecognitionEventType.recognizing:
if (event.text.isNotEmpty) {
originalText.value = event.text;
_hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now();
isUserSpeaking.value = true;
// Cancel previous silence timer
_silenceTimer?.cancel();
// Set new silence timer - if no new recognition results in 2 seconds,
// consider the user has stopped speaking
_silenceTimer = Timer(const Duration(seconds: 2), () {
if (_hasRecognizedSpeech && !_hasFinalResult) {
_processTranslation(event.text);
}
});
// If TTS is playing and user starts speaking, stop TTS
if (_isTtsSpeaking) {
_ttsService.endSession();
}
}
break;
case RecognitionEventType.finalResult:
_hasFinalResult = true;
isUserSpeaking.value = false;
if (event.text.isNotEmpty) {
originalText.value = event.text;
_hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now();
_processTranslation(event.text);
} else if (_hasRecognizedSpeech) {
// If final result is empty but we had intermediate results
_processTranslation(originalText.value);
}
// Reset for next utterance
_hasRecognizedSpeech = false;
_hasFinalResult = false;
break;
case RecognitionEventType.error:
Logger.error('Voice recognition error', event.error);
break;
default:
break;
}
},
onError: (error) {
Logger.error('Voice recognition stream error', error);
isListening.value = false;
restartListening();
}
);
} catch (e) {
Logger.error('Failed to start listening', e);
isListening.value = false;
}
}
Future<void> _processTranslation(String text) async {
if (text.isEmpty) return;
try {
isTranslating.value = true;
// In a real implementation, you would call your translation service here
// For now, we'll simulate translation with a delay
await Future.delayed(const Duration(milliseconds: 300));
// Simulate translation (in a real app, call your translation API here)
final translated = await _simulateTranslation(text);
translatedText.value = translated;
// Add to messages
messages.add(TranslationMessage(
original: text,
translated: translated,
timestamp: DateTime.now(),
isFromUser: true,
));
// Speak the translation if TTS is enabled
if (isTtsEnabled.value) {
await _synthesizeText(translated);
}
} catch (e) {
Logger.error('Translation error', e);
} finally {
isTranslating.value = false;
}
}
// This is a placeholder - in a real app, you would call your translation API
Future<String> _simulateTranslation(String text) async {
// This is just a simulation - replace with actual translation API call
if (sourceLanguage.value == "Chinese" && targetLanguage.value == "English") {
// Simulate Chinese to English translation
if (text.contains('测试')) return 'I am now starting to test the function of translating spoken Chinese into English.';
return 'Translated from Chinese: $text';
} else {
// Simulate English to Chinese translation
return '翻译自英语: $text';
}
}
void switchLanguages() {
final temp = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp;
// Clear current messages when switching languages
messages.clear();
}
Future<void> restartListening() async {
await stopListening();
await Future.delayed(const Duration(milliseconds: 500));
await startListening();
}
Future<void> stopListening() async {
if (!isListening.value) return;
try {
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
_silenceTimer?.cancel();
_silenceTimer = null;
if (_voiceService.isContinuousRecognitionActive()) {
await _voiceService.stopRecognition();
}
isListening.value = false;
isUserSpeaking.value = false;
_hasRecognizedSpeech = false;
_hasFinalResult = false;
} catch (e) {
Logger.error('Failed to stop listening', e);
}
}
void _startSpeakingCheckTimer() {
_speakingCheckTimer?.cancel();
_speakingCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
_checkIfUserIsSpeaking();
});
}
void _checkIfUserIsSpeaking() {
if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) {
final timeSinceLastSpeech = DateTime.now().difference(_lastSpeechTime!);
if (timeSinceLastSpeech.inMilliseconds < 1000) {
isUserSpeaking.value = true;
return;
}
}
isUserSpeaking.value = false;
}
void _setupTtsEventListener() {
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
}
void _handleTtsEvent(Map<String, dynamic> event) {
final String eventType = event['type'] as String? ?? 'unknown';
switch (eventType) {
case 'ttsSentenceStart':
_isTtsSpeaking = true;
break;
case 'ttsSentenceEnd':
case 'sessionFinished':
case 'connectionFinished':
_isTtsSpeaking = false;
break;
case 'connectionFailed':
case 'sessionFailed':
_isTtsSpeaking = false;
print('TTS错误: ${event['message']}');
break;
}
}
@override
void onClose() {
stopListening();
_speakingCheckTimer?.cancel();
// 取消TTS事件订阅
_ttsEventSubscription?.cancel();
// 结束TTS会话
_ttsService.endSession();
super.onClose();
}
// Add a method to synthesize text using the new TTS service
Future<void> _synthesizeText(String text) async {
if (text.isEmpty || !isTtsEnabled.value) return;
try {
// 如果当前有正在进行的TTS会话,先结束它
if (_isTtsSpeaking) {
await _ttsService.endSession();
}
// 确保连接已建立
if (!_ttsService.isConnected.value) {
final connected = await _ttsService.connect();
if (!connected) {
print('无法连接到TTS服务');
return;
}
}
// 开始合成
await _ttsService.synthesize(text, 'zh_female_1'); // 使用默认说话人
} catch (e) {
print('TTS播放出错: $e');
}
}
// Add a method to toggle TTS
void toggleTTS() {
isTtsEnabled.toggle();
if (!isTtsEnabled.value) {
_ttsService.endSession();
}
}
}

13
lib/modules/translation/models/translation_message_model.dart

@ -0,0 +1,13 @@
class TranslationMessage {
final String original;
final String translated;
final DateTime timestamp;
final bool isFromUser;
TranslationMessage({
required this.original,
required this.translated,
required this.timestamp,
required this.isFromUser,
});
}

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

@ -0,0 +1,302 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/translation_controller.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/text_styles.dart';
import '../widgets/translation_message_item.dart';
class TranslationView extends GetView<TranslationController> {
const TranslationView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: _buildLanguageSelector(),
centerTitle: true,
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Get.back(),
),
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined, color: Colors.black),
onPressed: () {
// Navigate to settings
},
),
],
),
body: Container(
color: Colors.white,
child: Column(
children: [
Expanded(
child: _buildMessagesList(),
),
_buildBottomSection(),
],
),
),
);
}
Widget _buildLanguageSelector() {
return Obx(() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
controller.sourceLanguage.value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
IconButton(
icon: const Icon(Icons.swap_horiz, color: Color(0xFF4A6FE5)),
onPressed: controller.switchLanguages,
),
Text(
controller.targetLanguage.value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
],
);
});
}
Widget _buildMessagesList() {
return Obx(() {
if (controller.messages.isEmpty) {
return _buildWelcomeMessage();
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: controller.messages.length,
reverse: true,
itemBuilder: (context, index) {
final message = controller.messages[controller.messages.length - 1 - index];
return TranslationMessageItem(message: message);
},
);
});
}
Widget _buildWelcomeMessage() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// First welcome message
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Welcome to Felo Translator. 👆 Click the button above to switch languages",
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.5,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Text(
"Welcome to Felo Translator. 👆 Click the button above to switch languages",
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
height: 1.5,
),
),
),
const SizedBox(width: 8),
Icon(Icons.volume_up, color: Colors.grey[600]),
],
),
],
),
),
const SizedBox(height: 20),
// Second instruction message
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFFEEF4FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD0DFFF),
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"👇 Click the button below ▶️ to start speaking, and Felo will automatically recognize the language for translation",
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.5,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Text(
"👇 Click the button below ▶️ to start speaking, and Felo will automatically recognize the language for translation",
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
height: 1.5,
),
),
),
const SizedBox(width: 8),
Icon(Icons.volume_up, color: Colors.grey[600]),
],
),
],
),
),
],
),
);
}
Widget _buildBottomSection() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Column(
children: [
_buildRecognitionStatus(),
const SizedBox(height: 8),
_buildControlButtons(),
],
),
);
}
Widget _buildRecognitionStatus() {
return Obx(() {
final isListening = controller.isListening.value;
final isUserSpeaking = controller.isUserSpeaking.value;
if (!isListening) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: Row(
children: [
if (isUserSpeaking)
_buildSpeakingIndicator()
else
Icon(Icons.mic, color: Colors.grey[600]),
const SizedBox(width: 12),
Expanded(
child: Text(
isUserSpeaking
? "Listening..."
: "Tap the mic button to start speaking",
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.5,
),
),
),
],
),
);
});
}
Widget _buildSpeakingIndicator() {
return SizedBox(
width: 24,
height: 24,
child: Stack(
alignment: Alignment.center,
children: [
Icon(Icons.mic, color: const Color(0xFF4A6FE5)),
Positioned.fill(
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF4A6FE5)),
),
),
],
),
);
}
Widget _buildControlButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
final isListening = controller.isListening.value;
return FloatingActionButton(
onPressed: isListening
? controller.stopListening
: controller.startListening,
backgroundColor: isListening ? Colors.red : const Color(0xFF4A6FE5),
child: Icon(
isListening ? Icons.stop : Icons.mic,
size: 24,
color: Colors.white,
),
);
}),
],
);
}
}

82
lib/modules/translation/widgets/translation_message_item.dart

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../models/translation_message_model.dart';
class TranslationMessageItem extends StatelessWidget {
final TranslationMessage message;
const TranslationMessageItem({
Key? key,
required this.message,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Original text
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: Text(
message.original,
style: TextStyle(
color: Colors.grey[800],
fontSize: 14,
height: 1.5,
),
),
),
const SizedBox(height: 8),
// Translated text
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFEEF4FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD0DFFF),
width: 0.5,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
message.translated,
style: TextStyle(
color: Colors.black87,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
),
const SizedBox(width: 8),
Icon(
Icons.volume_up,
color: Colors.grey[600],
size: 20,
),
],
),
),
],
),
);
}
}

259
lib/tools/check_volcano_asr_config.dart

@ -1,259 +0,0 @@
import 'dart:io';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
/// 检查火山语音识别服务配置
///
/// 该工具用于验证火山语音识别服务的配置是否正确,包括:
/// 1. 检查环境变量是否设置
/// 2. 检查网络连接
/// 3. 检查认证是否有效
void main() async {
// 加载环境变量
await dotenv.load();
print('======== 火山语音识别服务配置检查 ========');
// 检查环境变量
final appId = dotenv.env['VOLCANO_APP_ID'];
final appKey = dotenv.env['VOLCANO_APP_KEY'];
final cluster = dotenv.env['VOLCANO_CLUSTER'];
print('\n1. 检查环境变量:');
if (appId == null || appId.isEmpty) {
print('❌ VOLCANO_APP_ID 未设置');
} else {
print('✅ VOLCANO_APP_ID: ${appId.substring(0, 3)}***${appId.substring(appId.length - 3)} (长度: ${appId.length})');
}
if (appKey == null || appKey.isEmpty) {
print('❌ VOLCANO_APP_KEY 未设置');
} else {
print('✅ VOLCANO_APP_KEY: ${appKey.substring(0, 3)}***${appKey.substring(appKey.length - 3)} (长度: ${appKey.length})');
}
if (cluster == null || cluster.isEmpty) {
print('❌ VOLCANO_CLUSTER 未设置,将使用默认值 cn-beijing');
} else {
print('✅ VOLCANO_CLUSTER: $cluster');
}
// 使用默认值
final effectiveCluster = cluster ?? 'cn-beijing';
if (appId == null || appId.isEmpty || appKey == null || appKey.isEmpty) {
print('\n❌ 环境变量配置不完整,请检查 .env 文件');
exit(1);
}
// 检查网络连接
print('\n2. 检查网络连接:');
try {
final result = await InternetAddress.lookup('openspeech.bytedance.com');
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
print('✅ 网络连接正常,可以访问 openspeech.bytedance.com');
} else {
print('❌ 无法连接到 openspeech.bytedance.com');
}
} catch (e) {
print('❌ 网络连接异常: $e');
}
// 检查认证是否有效
print('\n3. 检查认证有效性:');
http.Response? authResponse;
try {
// 构建请求URL - 更新为大模型流式识别API路径
final url = 'https://openspeech.bytedance.com/api/v3/sauc/bigmodel';
// 构建请求头 - 不再添加Bearer前缀
final headers = {
'Content-Type': 'application/json',
'Authorization': appKey, // 不再添加Bearer前缀
};
// 构建请求体 - 添加resourceId参数
final body = jsonEncode({
'app_id': appId,
'cluster': effectiveCluster,
'resource_id': appId, // 资源ID暂时使用与APP_ID相同的值
'ping': true, // 只是ping服务,不进行实际识别
});
print('正在发送测试请求...');
print('请求URL: $url');
print('请求头: Authorization=${appKey.substring(0, 3)}***');
print('集群区域: $effectiveCluster');
// 发送请求
authResponse = await http.post(
Uri.parse(url),
headers: headers,
body: body,
).timeout(const Duration(seconds: 5));
// 检查响应
if (authResponse.statusCode == 200) {
print('✅ 认证有效,服务响应正常');
print('响应内容: ${authResponse.body}');
} else {
print('❌ 认证无效或服务异常,状态码: ${authResponse.statusCode}');
print('错误信息: ${authResponse.body}');
// 解析错误信息
try {
final errorJson = jsonDecode(authResponse.body);
final errorCode = errorJson['code'];
final errorMsg = errorJson['message'];
if (errorCode == 401) {
print('\n认证错误,可能的原因:');
print('1. APP_KEY 格式不正确');
print('2. APP_ID 与 APP_KEY 不匹配');
print('3. 账户未开通大模型流式语音识别服务或服务已过期');
print('4. 资源ID不正确或未授权');
} else if (errorCode == 400) {
print('\nWebSocket握手错误,可能的原因:');
print('1. 集群区域设置不正确 (当前: $effectiveCluster)');
print('2. 请求参数格式不正确');
print('3. 尝试使用不同的集群区域,如 cn-shanghai 或 cn-guangzhou');
} else {
print('\n未知错误:');
print('错误码: $errorCode');
print('错误信息: $errorMsg');
}
} catch (e) {
print('\n解析错误信息失败: $e');
if (authResponse.statusCode == 400) {
print('\nWebSocket握手错误,可能的原因:');
print('1. 集群区域设置不正确 (当前: $effectiveCluster)');
print('2. 请求参数格式不正确');
print('3. 尝试使用不同的集群区域,如 cn-shanghai 或 cn-guangzhou');
}
}
}
} catch (e) {
print('❌ 请求异常: $e');
}
print('\n======== 检查完成 ========');
print('\n提示: 如果您使用的是大模型流式识别SDK,请确保:');
print('1. 使用了正确的API路径: /api/v3/sauc/bigmodel');
print('2. 不要在Token前添加Bearer前缀');
print('3. 设置了正确的资源ID');
print('4. 设置了协议类型为PROTOCOL_TYPE_SEED');
print('5. 设置了正确的集群区域 (当前: $effectiveCluster)');
// 尝试其他集群区域
if (authResponse != null && authResponse.statusCode == 400) {
print('\n尝试其他集群区域:');
final alternativeClusters = [
'cn-shanghai',
'cn-guangzhou',
'cn-hongkong',
'ap-singapore',
'us-east-1',
'us-west-1'
];
bool foundWorkingCluster = false;
for (final altCluster in alternativeClusters) {
if (altCluster != effectiveCluster) {
print('\n尝试集群区域: $altCluster');
final success = await _testCluster(appId, appKey, altCluster);
if (success) {
foundWorkingCluster = true;
print('\n✅ 找到可用的集群区域: $altCluster');
print('建议在 .env 文件中设置 VOLCANO_CLUSTER=$altCluster');
// 尝试更新.env文件
try {
await _updateEnvFile(altCluster);
} catch (e) {
print('无法自动更新.env文件: $e');
}
break;
}
}
}
if (!foundWorkingCluster) {
print('\n❌ 所有集群区域测试均失败');
print('请联系火山引擎技术支持获取正确的集群区域');
}
}
}
/// 测试不同的集群区域
Future<bool> _testCluster(String appId, String appKey, String cluster) async {
try {
final url = 'https://openspeech.bytedance.com/api/v3/sauc/bigmodel';
final headers = {
'Content-Type': 'application/json',
'Authorization': appKey,
};
final body = jsonEncode({
'app_id': appId,
'cluster': cluster,
'resource_id': appId,
'ping': true,
});
final response = await http.post(
Uri.parse(url),
headers: headers,
body: body,
).timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
print('✅ 集群区域 $cluster 可用,认证有效');
return true;
} else {
print('❌ 集群区域 $cluster 不可用,状态码: ${response.statusCode}');
return false;
}
} catch (e) {
print('❌ 测试集群区域 $cluster 时出错: $e');
return false;
}
}
/// 尝试更新.env文件
Future<void> _updateEnvFile(String newCluster) async {
try {
final file = File('.env');
if (!await file.exists()) {
print('❌ .env文件不存在,无法自动更新');
return;
}
String content = await file.readAsString();
// 检查是否已有VOLCANO_CLUSTER
final clusterRegex = RegExp(r'VOLCANO_CLUSTER=.*');
if (clusterRegex.hasMatch(content)) {
// 替换现有的VOLCANO_CLUSTER
content = content.replaceAll(clusterRegex, 'VOLCANO_CLUSTER=$newCluster');
} else {
// 添加新的VOLCANO_CLUSTER
content += '\nVOLCANO_CLUSTER=$newCluster';
}
// 写入文件
await file.writeAsString(content);
print('✅ 已自动更新.env文件中的VOLCANO_CLUSTER=$newCluster');
} catch (e) {
print('❌ 更新.env文件失败: $e');
throw e;
}
}
Loading…
Cancel
Save