Browse Source

before change to azure

weicu
wolfplus 2 years ago
parent
commit
a71db0939e
  1. 2
      .cursorrules
  2. 572
      android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt
  3. 54
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  4. BIN
      assets/listening.mp3
  5. 127
      lib/core/bindings/initial_binding.dart
  6. 32
      lib/core/routes/app_pages.dart
  7. 3
      lib/core/routes/app_routes.dart
  8. 95
      lib/core/utils/app_lifecycle_manager.dart
  9. 70
      lib/core/utils/audio_player_utils.dart
  10. 110
      lib/data/services/audio_processing_service.dart
  11. 23
      lib/data/services/azure_asr_service.dart
  12. 539
      lib/data/services/azure_tts_service.dart
  13. 95
      lib/data/services/background_agent_service.dart
  14. 5
      lib/data/services/chat_history_service.dart
  15. 236
      lib/data/services/flutter_asr_service.dart
  16. 364
      lib/data/services/flutter_tts_service.dart
  17. 97
      lib/data/services/function_service.dart
  18. 7
      lib/data/services/my_audio_handler.dart
  19. 1003
      lib/data/services/volcano_asr_api_service.dart
  20. 384
      lib/data/services/volcano_asr_service.dart
  21. 269
      lib/data/services/volcano_tts_api_service.dart
  22. 239
      lib/data/services/volcano_tts_service.dart
  23. 129
      lib/modules/chat/controllers/chat_controller.dart
  24. 24
      lib/modules/chat/views/chat_view.dart
  25. 2
      lib/modules/home/views/home_view.dart
  26. 38
      lib/modules/profile/views/profile_view.dart
  27. 16
      lib/modules/test/bindings/flutter_asr_test_binding.dart
  28. 10
      lib/modules/test/bindings/flutter_tts_test_binding.dart
  29. 10
      lib/modules/test/bindings/volcano_asr_test_binding.dart
  30. 154
      lib/modules/test/controllers/flutter_asr_test_controller.dart
  31. 163
      lib/modules/test/controllers/flutter_tts_test_controller.dart
  32. 218
      lib/modules/test/controllers/volcano_asr_test_controller.dart
  33. 476
      lib/modules/test/views/flutter_asr_test_view.dart
  34. 334
      lib/modules/test/views/flutter_tts_test_view.dart
  35. 234
      lib/modules/test/views/volcano_asr_test_view.dart
  36. 3
      lib/modules/translation/bindings/translation_binding.dart
  37. 14
      lib/modules/translation/controllers/translation_controller.dart
  38. 863
      local_plugins/speech_to_text/android/src/main/kotlin/com/csdcorp/speech_to_text/SpeechToTextPlugin.kt
  39. 32
      macos/Podfile.lock
  40. 5
      pubspec.yaml
  41. 3
      windows/flutter/generated_plugin_registrant.cc
  42. 1
      windows/flutter/generated_plugins.cmake

2
.cursorrules

@ -6,4 +6,4 @@
6. 尽可能避免使用try catch 6. 尽可能避免使用try catch
7. 尽可能避免打印日志,仅在出错时记录日志 7. 尽可能避免打印日志,仅在出错时记录日志
8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化 8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化
9. 请尽可能使用中文回复问题

572
android/app/src/main/kotlin/com/example/deep_voice/AudioProcessingPlugin.kt

@ -0,0 +1,572 @@
package com.example.deep_voice
import android.content.Context
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.media.AudioManager
import android.os.Build
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import android.app.Application
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.FlutterInjector
/**
* 音频处理插件
*
* 提供回音消除、降噪和自动增益控制等功能
*/
class AudioProcessingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private lateinit var audioManager: AudioManager
// 音频处理组件
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
// 当前音频会话ID
private var currentAudioSessionId: Int = -1
companion object {
private const val TAG = "AudioProcessingPlugin"
private const val CHANNEL_NAME = "com.deep_voice.audio_processing"
// 保存静态实例,确保在任何地方都能访问
@JvmStatic
private var instance: AudioProcessingPlugin? = null
// 保存应用Context
@JvmStatic
private var applicationContext: Context? = null
// 静态初始化块,尝试在类加载时就获取applicationContext
init {
try {
Log.i(TAG, "AudioProcessingPlugin静态初始化块被执行")
// 尝试通过FlutterInjector获取applicationContext
val flutterLoader = FlutterInjector.instance().flutterLoader()
if (flutterLoader.initialized()) {
Log.i(TAG, "FlutterLoader已初始化")
} else {
Log.i(TAG, "FlutterLoader尚未初始化")
}
} catch (e: Exception) {
Log.e(TAG, "静态初始化块执行失败: ${e.message}")
}
}
// 获取实例的方法
@JvmStatic
fun getInstance(): AudioProcessingPlugin? {
return instance
}
// 获取应用Context的方法
@JvmStatic
fun getApplicationContext(): Context? {
return applicationContext
}
// 设置应用Context的方法,可以从外部调用
@JvmStatic
fun setApplicationContext(ctx: Context) {
if (applicationContext == null) {
applicationContext = ctx.applicationContext
Log.i(TAG, "通过setApplicationContext方法设置了applicationContext")
}
}
}
// 构造函数,尝试获取applicationContext
init {
Log.i(TAG, "AudioProcessingPlugin实例初始化块被执行")
}
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
Log.i(TAG, "onAttachedToEngine被调用")
context = binding.applicationContext
applicationContext = context
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
channel = MethodChannel(binding.binaryMessenger, CHANNEL_NAME)
channel.setMethodCallHandler(this)
instance = this
Log.i(TAG, "音频处理插件已初始化,context: $context, applicationContext: $applicationContext")
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
channel.setMethodCallHandler(null)
releaseAudioEffects()
if (instance == this) {
instance = null
}
Log.i(TAG, "音频处理插件已释放")
}
// 实现ActivityAware接口,获取Activity上下文
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
Log.i(TAG, "onAttachedToActivity被调用")
// 如果context尚未初始化,使用Activity上下文
if (!::context.isInitialized) {
context = binding.activity
Log.i(TAG, "使用Activity上下文初始化context")
// 如果audioManager尚未初始化,初始化它
if (!::audioManager.isInitialized) {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "使用Activity上下文初始化audioManager")
}
}
}
override fun onDetachedFromActivity() {
Log.i(TAG, "onDetachedFromActivity被调用")
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"isEchoCancellationAvailable" -> {
result.success(AcousticEchoCanceler.isAvailable())
}
"isNoiseSuppressionAvailable" -> {
result.success(NoiseSuppressor.isAvailable())
}
"enableEchoCancellation" -> {
val audioSessionId = call.argument<Int>("audioSessionId")
if (audioSessionId == null) {
result.error("INVALID_ARGUMENT", "音频会话ID不能为空", null)
return
}
result.success(enableEchoCancellation(audioSessionId))
}
"disableEchoCancellation" -> {
result.success(disableEchoCancellation())
}
"enableNoiseSuppression" -> {
val audioSessionId = call.argument<Int>("audioSessionId")
if (audioSessionId == null) {
result.error("INVALID_ARGUMENT", "音频会话ID不能为空", null)
return
}
result.success(enableNoiseSuppression(audioSessionId))
}
"disableNoiseSuppression" -> {
result.success(disableNoiseSuppression())
}
"getAudioSessionId" -> {
// 尝试获取当前活动的音频会话ID
try {
Log.i(TAG, "getAudioSessionId方法被调用")
// 检查applicationContext是否已初始化
if (applicationContext != null) {
Log.i(TAG, "静态applicationContext已初始化")
// 如果context尚未初始化,使用applicationContext
if (!::context.isInitialized) {
context = applicationContext!!
Log.i(TAG, "使用静态applicationContext初始化context")
}
} else {
Log.w(TAG, "静态applicationContext尚未初始化")
}
// 检查context是否初始化
if (!::context.isInitialized) {
Log.e(TAG, "context尚未初始化,尝试从静态实例获取")
// 尝试从静态实例获取context
val staticInstance = getInstance()
if (staticInstance != null) {
try {
// 尝试访问context,如果未初始化会抛出异常
val testContext = staticInstance.context
context = testContext
Log.i(TAG, "从静态实例获取context成功")
} catch (e: UninitializedPropertyAccessException) {
Log.e(TAG, "静态实例的context也未初始化: ${e.message}")
// 尝试使用应用级别的Context
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
Log.i(TAG, "使用应用级别的Context初始化成功")
} else {
Log.e(TAG, "无法获取任何有效的Context,无法获取音频会话ID")
result.success(-1)
return
}
}
} else {
// 尝试使用应用级别的Context
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
Log.i(TAG, "使用应用级别的Context初始化成功")
} else {
Log.e(TAG, "无法获取context,无法获取音频会话ID")
result.success(-1)
return
}
}
}
// 检查audioManager是否初始化,如果没有则初始化
if (!::audioManager.isInitialized) {
Log.w(TAG, "audioManager尚未初始化,正在初始化")
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "audioManager初始化成功")
} catch (e: Exception) {
Log.e(TAG, "初始化audioManager失败: ${e.message}")
result.success(-1)
return
}
}
// 使用ensureValidAudioSessionId方法获取有效的音频会话ID
val sessionId = ensureValidAudioSessionId()
Log.i(TAG, "返回音频会话ID: $sessionId")
result.success(sessionId)
} catch (e: Exception) {
Log.e(TAG, "获取音频会话ID失败: ${e.message}")
result.success(-1)
}
}
else -> {
result.notImplemented()
}
}
}
/**
* 启用回音消除
*
* @param audioSessionId 音频会话ID
* @return 是否成功启用
*/
private fun enableEchoCancellation(audioSessionId: Int): Boolean {
if (!AcousticEchoCanceler.isAvailable()) {
Log.w(TAG, "设备不支持回音消除")
return false
}
try {
Log.i(TAG, "尝试启用回音消除,音频会话ID: $audioSessionId")
// 确保context已初始化
if (!::context.isInitialized) {
Log.w(TAG, "context尚未初始化,尝试获取")
// 尝试从静态实例或应用级别获取context
val staticInstance = getInstance()
if (staticInstance != null && try { staticInstance.context; true } catch (e: Exception) { false }) {
context = staticInstance.context
} else {
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
} else {
Log.e(TAG, "无法获取context,无法启用回音消除")
return false
}
}
}
// 确保audioManager已初始化
if (!::audioManager.isInitialized) {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "audioManager初始化成功")
} catch (e: Exception) {
Log.e(TAG, "初始化audioManager失败: ${e.message}")
return false
}
}
// 如果已经创建了回音消除器,先释放
if (echoCanceler != null) {
echoCanceler?.release()
echoCanceler = null
}
// 创建新的回音消除器
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler == null) {
Log.e(TAG, "创建回音消除器失败")
return false
}
// 启用回音消除
echoCanceler?.enabled = true
currentAudioSessionId = audioSessionId
Log.i(TAG, "回音消除已启用,音频会话ID: $audioSessionId")
return echoCanceler?.enabled == true
} catch (e: Exception) {
Log.e(TAG, "启用回音消除失败: ${e.message}")
return false
}
}
/**
* 禁用回音消除
*
* @return 是否成功禁用
*/
private fun disableEchoCancellation(): Boolean {
if (echoCanceler == null) {
return true
}
try {
echoCanceler?.enabled = false
echoCanceler?.release()
echoCanceler = null
Log.i(TAG, "回音消除已禁用")
return true
} catch (e: Exception) {
Log.e(TAG, "禁用回音消除失败: ${e.message}")
return false
}
}
/**
* 启用噪声抑制
*
* @param audioSessionId 音频会话ID
* @return 是否成功启用
*/
private fun enableNoiseSuppression(audioSessionId: Int): Boolean {
if (!NoiseSuppressor.isAvailable()) {
Log.w(TAG, "设备不支持噪声抑制")
return false
}
try {
Log.i(TAG, "尝试启用噪声抑制,音频会话ID: $audioSessionId")
// 确保context已初始化
if (!::context.isInitialized) {
Log.w(TAG, "context尚未初始化,尝试获取")
// 尝试从静态实例或应用级别获取context
val staticInstance = getInstance()
if (staticInstance != null && try { staticInstance.context; true } catch (e: Exception) { false }) {
context = staticInstance.context
} else {
val appContext = getApplicationContext()
if (appContext != null) {
context = appContext
} else {
Log.e(TAG, "无法获取context,无法启用噪声抑制")
return false
}
}
}
// 确保audioManager已初始化
if (!::audioManager.isInitialized) {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.i(TAG, "audioManager初始化成功")
} catch (e: Exception) {
Log.e(TAG, "初始化audioManager失败: ${e.message}")
return false
}
}
// 如果已经创建了噪声抑制器,先释放
if (noiseSuppressor != null) {
noiseSuppressor?.release()
noiseSuppressor = null
}
// 创建新的噪声抑制器
noiseSuppressor = NoiseSuppressor.create(audioSessionId)
if (noiseSuppressor == null) {
Log.e(TAG, "创建噪声抑制器失败")
return false
}
// 启用噪声抑制
noiseSuppressor?.enabled = true
currentAudioSessionId = audioSessionId
Log.i(TAG, "噪声抑制已启用,音频会话ID: $audioSessionId")
return noiseSuppressor?.enabled == true
} catch (e: Exception) {
Log.e(TAG, "启用噪声抑制失败: ${e.message}")
return false
}
}
/**
* 禁用噪声抑制
*
* @return 是否成功禁用
*/
private fun disableNoiseSuppression(): Boolean {
if (noiseSuppressor == null) {
return true
}
try {
noiseSuppressor?.enabled = false
noiseSuppressor?.release()
noiseSuppressor = null
Log.i(TAG, "噪声抑制已禁用")
return true
} catch (e: Exception) {
Log.e(TAG, "禁用噪声抑制失败: ${e.message}")
return false
}
}
/**
* 释放所有音频效果
*/
private fun releaseAudioEffects() {
try {
echoCanceler?.release()
echoCanceler = null
noiseSuppressor?.release()
noiseSuppressor = null
automaticGainControl?.release()
automaticGainControl = null
currentAudioSessionId = -1
Log.i(TAG, "所有音频效果已释放")
} catch (e: Exception) {
Log.e(TAG, "释放音频效果失败: ${e.message}")
}
}
/**
* 检查音频会话ID是否有效,如果无效则创建一个新的
*
* @return 有效的音频会话ID
*/
private fun ensureValidAudioSessionId(): Int {
// 如果已经有有效的音频会话ID,直接返回
if (currentAudioSessionId > 0) {
Log.i(TAG, "使用已存在的音频会话ID: $currentAudioSessionId")
return currentAudioSessionId
}
Log.i(TAG, "尝试创建有效的音频会话ID")
// 尝试从AudioManager获取音频会话ID
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ::audioManager.isInitialized) {
try {
val audioAttributes = audioManager.getActivePlaybackConfigurations()
if (audioAttributes.isNotEmpty()) {
val sessionId = audioAttributes[0].audioAttributes.usage
if (sessionId > 0) {
currentAudioSessionId = sessionId
Log.i(TAG, "从AudioManager获取到音频会话ID: $sessionId")
return sessionId
}
}
} catch (e: Exception) {
Log.e(TAG, "从AudioManager获取音频会话ID失败: ${e.message}")
}
}
// 尝试创建一个临时的AudioTrack来获取会话ID
try {
val audioTrack = android.media.AudioTrack(
android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
android.media.AudioFormat.Builder()
.setSampleRate(16000)
.setChannelMask(android.media.AudioFormat.CHANNEL_OUT_MONO)
.setEncoding(android.media.AudioFormat.ENCODING_PCM_16BIT)
.build(),
1024, // 最小缓冲区大小
android.media.AudioTrack.MODE_STREAM,
android.media.AudioManager.AUDIO_SESSION_ID_GENERATE
)
val generatedSessionId = audioTrack.audioSessionId
if (generatedSessionId > 0) {
currentAudioSessionId = generatedSessionId
Log.i(TAG, "成功创建临时AudioTrack并获取音频会话ID: $generatedSessionId")
// 释放AudioTrack资源
audioTrack.release()
return generatedSessionId
} else {
Log.w(TAG, "创建的AudioTrack未返回有效的音频会话ID")
audioTrack.release()
}
} catch (e: Exception) {
Log.e(TAG, "创建临时AudioTrack失败: ${e.message}")
}
// 尝试创建一个临时的AudioRecord来获取会话ID
try {
val minBufferSize = android.media.AudioRecord.getMinBufferSize(
16000,
android.media.AudioFormat.CHANNEL_IN_MONO,
android.media.AudioFormat.ENCODING_PCM_16BIT
)
if (minBufferSize > 0) {
val audioRecord = android.media.AudioRecord(
android.media.MediaRecorder.AudioSource.MIC,
16000,
android.media.AudioFormat.CHANNEL_IN_MONO,
android.media.AudioFormat.ENCODING_PCM_16BIT,
minBufferSize
)
val recordSessionId = audioRecord.audioSessionId
if (recordSessionId > 0) {
currentAudioSessionId = recordSessionId
Log.i(TAG, "成功创建临时AudioRecord并获取音频会话ID: $recordSessionId")
// 释放AudioRecord资源
audioRecord.release()
return recordSessionId
} else {
Log.w(TAG, "创建的AudioRecord未返回有效的音频会话ID")
audioRecord.release()
}
} else {
Log.w(TAG, "无法获取有效的AudioRecord最小缓冲区大小")
}
} catch (e: Exception) {
Log.e(TAG, "创建临时AudioRecord失败: ${e.message}")
}
// 如果以上方法都失败,使用一个固定的默认值
val defaultSessionId = 1
Log.w(TAG, "使用默认音频会话ID: $defaultSessionId")
currentAudioSessionId = defaultSessionId
return defaultSessionId
}
}

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

@ -11,28 +11,80 @@ import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity: AudioServiceActivity() { class MainActivity: AudioServiceActivity() {
private val AZURE_ASR_CHANNEL = "com.example.deep_voice/azure_asr" private val AZURE_ASR_CHANNEL = "com.example.deep_voice/azure_asr"
private val AZURE_ASR_EVENT_CHANNEL = "com.example.deep_voice/azure_asr_events" private val AZURE_ASR_EVENT_CHANNEL = "com.example.deep_voice/azure_asr_events"
private val AUDIO_PROCESSING_CHANNEL = "com.deep_voice.audio_processing"
private val TAG = "MainActivity" private val TAG = "MainActivity"
private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureAsrHelper: AzureAsrHelper
// 暂时注释掉AudioProcessingPlugin的实例化
// private val audioProcessingPlugin: AudioProcessingPlugin by lazy { AudioProcessingPlugin() }
private var azureEventSink: EventChannel.EventSink? = null private var azureEventSink: EventChannel.EventSink? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// 初始化 Azure ASR 助手 // 初始化 Azure ASR 助手
azureAsrHelper = AzureAsrHelper() azureAsrHelper = AzureAsrHelper()
// 暂时注释掉AudioProcessingPlugin的初始化
// AudioProcessingPlugin.setApplicationContext(applicationContext)
// Log.i(TAG, "在MainActivity.onCreate中初始化AudioProcessingPlugin的applicationContext")
} }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
// 暂时注释掉AudioProcessingPlugin的初始化
// AudioProcessingPlugin.setApplicationContext(applicationContext)
// Log.i(TAG, "在configureFlutterEngine中初始化AudioProcessingPlugin的applicationContext")
// 注册所有插件
GeneratedPluginRegistrant.registerWith(flutterEngine)
// 暂时注释掉AudioProcessingPlugin的注册,添加一个空的MethodCallHandler来处理调用
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AUDIO_PROCESSING_CHANNEL).setMethodCallHandler { call, result ->
// 为所有方法返回默认值或成功状态
when (call.method) {
"isEchoCancellationAvailable" -> {
// 返回false表示不可用
result.success(false)
}
"isNoiseSuppressionAvailable" -> {
// 返回false表示不可用
result.success(false)
}
"enableEchoCancellation" -> {
// 返回true表示成功,但实际上不执行任何操作
result.success(true)
}
"disableEchoCancellation" -> {
// 返回true表示成功
result.success(true)
}
"enableNoiseSuppression" -> {
// 返回true表示成功,但实际上不执行任何操作
result.success(true)
}
"disableNoiseSuppression" -> {
// 返回true表示成功
result.success(true)
}
"getAudioSessionId" -> {
// 返回一个固定的音频会话ID
result.success(1)
}
else -> {
result.notImplemented()
}
}
}
// 设置 Azure ASR 方法通道 // 设置 Azure ASR 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result ->

BIN
assets/listening.mp3

Binary file not shown.

127
lib/core/bindings/initial_binding.dart

@ -3,49 +3,90 @@ import '../../core/controllers/permission_controller.dart';
import '../../data/services/audio_service.dart'; import '../../data/services/audio_service.dart';
import '../../data/services/notification_service.dart'; import '../../data/services/notification_service.dart';
import '../../data/services/volcano_ai_service.dart'; import '../../data/services/volcano_ai_service.dart';
import '../../data/services/volcano_asr_service.dart';
import '../../data/services/volcano_tts_api_service.dart'; import '../../data/services/volcano_tts_api_service.dart';
import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_translation_service.dart';
import '../../data/services/azure_asr_service.dart'; import '../../data/services/azure_asr_service.dart';
import '../../data/services/background_agent_service.dart'; import '../../data/services/background_agent_service.dart';
import '../../data/services/chat_history_service.dart'; import '../../data/services/chat_history_service.dart';
import '../../data/services/flutter_tts_service.dart';
import '../../data/services/volcano_asr_api_service.dart';
// import '../../data/services/audio_processing_service.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import '../../core/utils/app_lifecycle_manager.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings { class InitialBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// 初始化所有服务 // 立即初始化关键服务
final notificationService = Get.put(NotificationService(), permanent: true); _initializeCriticalServices();
final audioService = Get.put(AudioServiceManager(), permanent: true);
final volcanoAiService = Get.put(VolcanoAIService(), permanent: true);
final voiceRecognitionService = Get.put(VolcanoAsrService(), permanent: true);
final volcanoTtsService = Get.put(VolcanoTtsApiService(), permanent: true);
final volcanoTranslationService = Get.put(VolcanoTranslationService(), permanent: true);
final azureAsrService = Get.put(AzureAsrService(), permanent: true);
// 注册ChatHistoryService (先注册ChatHistoryService) // 延时加载其他服务
final chatHistoryService = Get.put(ChatHistoryService(), permanent: true); _registerLazyServices();
}
// 注册后台代理服务 (后注册BackgroundAgentService) /// 初始化关键服务(立即加载)
final backgroundAgentService = Get.put(BackgroundAgentService(), permanent: true); void _initializeCriticalServices() async {
// 应用生命周期管理器是最基础的服务,需要最先初始化
Get.put(AppLifecycleManager(), permanent: true);
// 只注册全局控制器 // 通知服务和权限控制器是关键服务,需要立即初始化
Get.put(PermissionController(), permanent: true); final notificationService = Get.put(NotificationService(), permanent: true);
final permissionController = Get.put(PermissionController(), permanent: true);
// 异步初始化各个服务,确保顺序执行 // 音频服务也是基础服务,需要立即初始化
_initializeServices(notificationService, audioService); final audioService = Get.put(AudioServiceManager(), permanent: true);
// 初始化 Azure 服务 // 异步初始化通知和音频服务
_initializeAzureService(azureAsrService); _initializeNotificationAndAudioServices(notificationService, audioService);
}
// 初始化火山翻译服务 /// 注册延时加载的服务
_initializeVolcanoTranslationService(volcanoTranslationService); void _registerLazyServices() {
// 使用lazyPut注册服务,只有在首次访问时才会被实例化
// AI相关服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
Get.lazyPut<VolcanoTtsApiService>(() {
final service = VolcanoTtsApiService();
return service;
}, fenix: true);
// 语音识别服务
// Get.lazyPut<AzureAsrService>(() {
// final service = AzureAsrService();
// // 在首次访问时初始化
// _initializeAzureService(service);
// return service;
// }, fenix: true);
// 翻译服务
Get.lazyPut<VolcanoTranslationService>(() {
final service = VolcanoTranslationService();
// 在首次访问时初始化
_initializeVolcanoTranslationService(service);
return service;
}, fenix: true);
// TTS服务
// Get.lazyPut<FlutterTtsService>(() => FlutterTtsService(), fenix: true);
// 音频处理服务
// Get.lazyPut<AudioProcessingService>(() => AudioProcessingService(), fenix: true);
// 火山ASR API服务
Get.lazyPut<VolcanoAsrApiService>(() => VolcanoAsrApiService(), fenix: true);
// 聊天历史服务
Get.lazyPut<ChatHistoryService>(() => ChatHistoryService(), fenix: true);
// 后台代理服务 (依赖于聊天历史服务)
Get.lazyPut<BackgroundAgentService>(() => BackgroundAgentService(), fenix: true);
} }
// 单独提取初始化服务的方法,以便更好地处理错误 // 初始化通知和音频服务
void _initializeServices(NotificationService notificationService, AudioServiceManager audioService) async { void _initializeNotificationAndAudioServices(NotificationService notificationService, AudioServiceManager audioService) async {
try { try {
// 首先初始化通知服务 // 首先初始化通知服务
await notificationService.init().catchError((error) { await notificationService.init().catchError((error) {
@ -63,30 +104,13 @@ class InitialBinding extends Bindings {
} catch (e) { } catch (e) {
Logger.error('服务初始化过程中发生错误', e); Logger.error('服务初始化过程中发生错误', e);
} }
// 其他服务初始化可以在这里添加
} }
// 初始化 Azure 语音识别服务 // 初始化 Azure 语音识别服务
void _initializeAzureService(AzureAsrService azureAsrService) async { void _initializeAzureService(AzureAsrService azureAsrService) async {
try { try {
// 从环境变量中获取 Azure 语音识别服务的配置参数 // 直接调用初始化方法,服务内部会读取环境变量配置
final subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; final success = await azureAsrService.initialize();
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) { if (success) {
Logger.info('Azure 语音识别服务初始化成功'); Logger.info('Azure 语音识别服务初始化成功');
@ -101,23 +125,8 @@ class InitialBinding extends Bindings {
// 初始化火山翻译服务 // 初始化火山翻译服务
void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async { void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async {
try { try {
// 从环境变量中获取火山翻译服务的配置参数 // 直接调用初始化方法,服务内部会读取环境变量配置
final accessKey = dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? ''; final success = await translationService.initialize();
final secretKey = dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? '';
final region = dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1';
// 检查配置是否完整
if (accessKey.isEmpty || secretKey.isEmpty) {
Logger.warning('火山翻译服务的访问密钥未设置,请检查 .env 文件');
return;
}
// 初始化火山翻译服务
final success = await translationService.initialize(
accessKey: accessKey,
secretKey: secretKey,
region: region,
);
if (success) { if (success) {
Logger.info('火山翻译服务初始化成功'); Logger.info('火山翻译服务初始化成功');

32
lib/core/routes/app_pages.dart

@ -9,12 +9,17 @@ import '../../modules/chat/bindings/chat_binding.dart';
import '../../modules/chat/views/chat_view.dart'; import '../../modules/chat/views/chat_view.dart';
import '../../modules/test/views/tts_test_view.dart'; import '../../modules/test/views/tts_test_view.dart';
import '../../modules/test/views/asr_test_view.dart'; import '../../modules/test/views/asr_test_view.dart';
import '../../modules/test/views/volcano_asr_test_view.dart';
import '../../modules/test/views/flutter_tts_test_view.dart';
import '../../modules/test/views/flutter_asr_test_view.dart';
import '../../modules/test/bindings/test_binding.dart'; import '../../modules/test/bindings/test_binding.dart';
import '../../modules/test/bindings/volcano_asr_test_binding.dart';
import '../../modules/test/bindings/flutter_tts_test_binding.dart';
import '../../modules/test/bindings/flutter_asr_test_binding.dart';
import '../../modules/translation/views/translation_view.dart'; import '../../modules/translation/views/translation_view.dart';
import '../../modules/translation/bindings/translation_binding.dart'; import '../../modules/translation/bindings/translation_binding.dart';
import '../../modules/translation/views/translation_history_view.dart'; import '../../modules/translation/views/translation_history_view.dart';
import '../../modules/translation/bindings/translation_history_binding.dart'; import '../../modules/translation/bindings/translation_history_binding.dart';
// import '../../modules/speech_demo/speech_demo_page.dart'; // This file doesn't exist
import './app_routes.dart'; import './app_routes.dart';
abstract class AppPages { abstract class AppPages {
@ -39,11 +44,11 @@ abstract class AppPages {
page: () => const ProfileView(), page: () => const ProfileView(),
binding: ProfileBinding(), binding: ProfileBinding(),
), ),
// Commented out because SpeechDemoPage doesn't exist GetPage(
// GetPage( name: Routes.speechDemo,
// name: Routes.speechDemo, page: () => const VolcanoAsrTestView(),
// page: () => const SpeechDemoPage(), binding: VolcanoAsrTestBinding(),
// ), ),
GetPage( GetPage(
name: Routes.TTS_TEST, name: Routes.TTS_TEST,
page: () => TtsTestView(), page: () => TtsTestView(),
@ -54,6 +59,21 @@ abstract class AppPages {
page: () => AsrTestView(), page: () => AsrTestView(),
binding: AsrTestBinding(), binding: AsrTestBinding(),
), ),
GetPage(
name: Routes.VOLCANO_ASR_TEST,
page: () => const VolcanoAsrTestView(),
binding: VolcanoAsrTestBinding(),
),
GetPage(
name: Routes.FLUTTER_TTS_TEST,
page: () => const FlutterTtsTestView(),
binding: FlutterTtsTestBinding(),
),
GetPage(
name: Routes.FLUTTER_ASR_TEST,
page: () => const FlutterAsrTestView(),
binding: FlutterAsrTestBinding(),
),
GetPage( GetPage(
name: Routes.translation, name: Routes.translation,
page: () => const TranslationView(), page: () => const TranslationView(),

3
lib/core/routes/app_routes.dart

@ -7,6 +7,9 @@ abstract class Routes {
static const speechDemo = '/speech_demo'; static const speechDemo = '/speech_demo';
static const TTS_TEST = '/tts_test'; static const TTS_TEST = '/tts_test';
static const ASR_TEST = '/asr_test'; static const ASR_TEST = '/asr_test';
static const VOLCANO_ASR_TEST = '/volcano_asr_test';
static const translation = '/translation'; static const translation = '/translation';
static const translationHistory = '/translation_history'; static const translationHistory = '/translation_history';
static const FLUTTER_TTS_TEST = '/flutter_tts_test';
static const FLUTTER_ASR_TEST = '/flutter_asr_test';
} }

95
lib/core/utils/app_lifecycle_manager.dart

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../data/services/volcano_asr_api_service.dart';
import '../../data/services/volcano_tts_api_service.dart';
import 'logger.dart';
/// 应用生命周期管理器
///
/// 监控应用的前台/后台状态变化,并在状态变化时执行相应操作
class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
// 当前应用生命周期状态
final Rx<AppLifecycleState> _appLifecycleState = AppLifecycleState.resumed.obs;
AppLifecycleState get appLifecycleState => _appLifecycleState.value;
// 状态变化流
final _lifecycleStateStream = AppLifecycleState.resumed.obs;
Stream<AppLifecycleState> get lifecycleStateStream => _lifecycleStateStream.stream;
// 是否在前台
final RxBool _isInForeground = true.obs;
bool get isInForeground => _isInForeground.value;
@override
void onInit() {
super.onInit();
// 注册应用生命周期观察者
WidgetsBinding.instance.addObserver(this);
Logger.info('AppLifecycleManager 已初始化');
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final previousState = _appLifecycleState.value;
_appLifecycleState.value = state;
_lifecycleStateStream.value = state;
Logger.info('应用生命周期状态变化: $previousState -> $state');
// 根据状态变化更新前台/后台标志
switch (state) {
case AppLifecycleState.resumed:
_isInForeground.value = true;
_handleAppResumed();
break;
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
_isInForeground.value = false;
_handleAppPaused(state);
break;
default:
break;
}
}
/// 处理应用进入前台
void _handleAppResumed() {
Logger.info('应用进入前台');
// 这里不需要主动恢复服务,因为我们希望用户手动控制录音开始
// 如果正在录音,停止录音
final asrService = Get.find<VolcanoAsrApiService>();
final ttsService = Get.find<VolcanoTtsApiService>();
if (asrService.isRecording) {
Logger.info('应用进入后台,停止语音识别');
asrService.stopRecognition();
ttsService.stop();
ttsService.disconnect();
}
}
/// 处理应用进入后台
void _handleAppPaused(AppLifecycleState state) {
Logger.info('应用进入后台: $state');
// 获取 VolcanoAsrApiService 实例
final asrService = Get.find<VolcanoAsrApiService>();
final ttsService = Get.find<VolcanoTtsApiService>();
// 如果正在录音,停止录音
if (asrService.isRecording) {
Logger.info('应用进入后台,停止语音识别');
asrService.stopRecognition();
ttsService.stop();
ttsService.disconnect();
}
}
@override
void onClose() {
// 移除应用生命周期观察者
WidgetsBinding.instance.removeObserver(this);
Logger.info('AppLifecycleManager 已关闭');
super.onClose();
}
}

70
lib/core/utils/audio_player_utils.dart

@ -0,0 +1,70 @@
import 'package:just_audio/just_audio.dart';
import 'logger.dart';
/// 音频播放工具类
/// 提供播放assets及本地音频文件的功能
class AudioPlayerUtils {
// 单例模式
static final AudioPlayerUtils instance = AudioPlayerUtils._internal();
// 音频播放器
final AudioPlayer _audioPlayer = AudioPlayer();
// 构造函数
AudioPlayerUtils._internal() {
_setupAudioPlayer();
}
/// 设置音频播放器
void _setupAudioPlayer() {
// 可以根据需要添加播放状态监听等
}
/// 播放assets中的音频文件
/// [assetPath] assets资源路径,例如 'assets/listening.mp3'
Future<void> playAssetAudio(String assetPath) async {
try {
// 停止当前播放
await stopPlayback();
// 设置音频源并播放
await _audioPlayer.setAsset(assetPath);
await _audioPlayer.play();
Logger.info('播放assets音频: $assetPath');
} catch (e) {
Logger.error('播放assets音频失败', e);
}
}
/// 播放本地文件路径的音频
/// [filePath] 本地文件路径
Future<void> playLocalAudio(String filePath) async {
try {
// 停止当前播放
await stopPlayback();
// 设置音频源并播放
await _audioPlayer.setFilePath(filePath);
await _audioPlayer.play();
Logger.info('播放本地音频文件: $filePath');
} catch (e) {
Logger.error('播放本地音频文件失败', e);
}
}
/// 停止播放
Future<void> stopPlayback() async {
try {
await _audioPlayer.stop();
} catch (e) {
Logger.error('停止播放失败', e);
}
}
/// 释放资源
void dispose() {
_audioPlayer.dispose();
}
}

110
lib/data/services/audio_processing_service.dart

@ -0,0 +1,110 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
/// 音频处理服务
///
/// 提供回音消除、降噪和语音活动检测等功能
class AudioProcessingService extends GetxService {
static const MethodChannel _channel = MethodChannel('com.deep_voice.audio_processing');
// 状态变量
final _isEchoCancellationEnabled = false.obs;
final _isNoiseSuppressionEnabled = false.obs;
final _isVoiceActivityDetectionEnabled = false.obs;
// 获取可观察状态
RxBool get isEchoCancellationEnabled => _isEchoCancellationEnabled;
RxBool get isNoiseSuppressionEnabled => _isNoiseSuppressionEnabled;
RxBool get isVoiceActivityDetectionEnabled => _isVoiceActivityDetectionEnabled;
AudioProcessingService() {
_initPlatformState();
}
/// 初始化平台状态
Future<void> _initPlatformState() async {
try {
final bool isEchoCancellationAvailable = await _channel.invokeMethod('isEchoCancellationAvailable') ?? false;
final bool isNoiseSuppressionAvailable = await _channel.invokeMethod('isNoiseSuppressionAvailable') ?? false;
Logger.info('回音消除可用: $isEchoCancellationAvailable');
Logger.info('噪声抑制可用: $isNoiseSuppressionAvailable');
} catch (e) {
Logger.error('初始化音频处理服务失败: $e');
}
}
/// 为指定的音频会话ID启用回音消除
Future<bool> enableEchoCancellation(int audioSessionId) async {
try {
final bool result = await _channel.invokeMethod('enableEchoCancellation', {
'audioSessionId': audioSessionId,
}) ?? false;
_isEchoCancellationEnabled.value = result;
Logger.info('启用回音消除${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('启用回音消除失败: $e');
return false;
}
}
/// 禁用回音消除
Future<bool> disableEchoCancellation() async {
try {
final bool result = await _channel.invokeMethod('disableEchoCancellation') ?? false;
_isEchoCancellationEnabled.value = !result;
Logger.info('禁用回音消除${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('禁用回音消除失败: $e');
return false;
}
}
/// 为指定的音频会话ID启用噪声抑制
Future<bool> enableNoiseSuppression(int audioSessionId) async {
try {
final bool result = await _channel.invokeMethod('enableNoiseSuppression', {
'audioSessionId': audioSessionId,
}) ?? false;
_isNoiseSuppressionEnabled.value = result;
Logger.info('启用噪声抑制${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('启用噪声抑制失败: $e');
return false;
}
}
/// 禁用噪声抑制
Future<bool> disableNoiseSuppression() async {
try {
final bool result = await _channel.invokeMethod('disableNoiseSuppression') ?? false;
_isNoiseSuppressionEnabled.value = !result;
Logger.info('禁用噪声抑制${result ? "成功" : "失败"}');
return result;
} catch (e) {
Logger.error('禁用噪声抑制失败: $e');
return false;
}
}
/// 获取当前录音的音频会话ID
Future<int?> getAudioSessionId() async {
try {
final int? sessionId = await _channel.invokeMethod('getAudioSessionId');
Logger.info('获取音频会话ID: $sessionId');
return sessionId;
} catch (e) {
Logger.error('获取音频会话ID失败: $e');
return null;
}
}
}

23
lib/data/services/azure_asr_service.dart

@ -131,19 +131,30 @@ class AzureAsrService extends GetxService {
/// [serviceRegion] Azure 语音服务的区域 /// [serviceRegion] Azure 语音服务的区域
/// [language] 识别语言,默认为中文 /// [language] 识别语言,默认为中文
Future<bool> initialize({ Future<bool> initialize({
required String subscriptionKey, String? subscriptionKey,
required String serviceRegion, String? serviceRegion,
String language = 'zh-CN', String? language,
}) async { }) async {
if (_isInitialized) return true; if (_isInitialized) return true;
try { try {
// 从环境变量中读取配置,如果没有传入参数
final key = subscriptionKey ?? dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '';
final region = serviceRegion ?? dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia';
final lang = language ?? dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN';
// 检查配置是否完整
if (key.isEmpty) {
Logger.warning('Azure 语音识别服务的订阅密钥未设置,请检查 .env 文件');
return false;
}
final result = await _methodChannel.invokeMethod<bool>( final result = await _methodChannel.invokeMethod<bool>(
'initialize', 'initialize',
{ {
'subscriptionKey': subscriptionKey, 'subscriptionKey': key,
'serviceRegion': serviceRegion, 'serviceRegion': region,
'language': language, 'language': lang,
}, },
); );

539
lib/data/services/azure_tts_service.dart

@ -0,0 +1,539 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:path_provider/path_provider.dart';
/// 语音合成事件类型
enum SynthesisEventType {
/// 合成开始
synthesisStarted,
/// 合成进度更新
synthesisProgress,
/// 合成完成
synthesisCompleted,
/// 合成取消
synthesisCanceled,
/// 合成错误
error,
}
/// 语音合成事件
class SynthesisEvent {
/// 事件类型
final SynthesisEventType type;
/// 音频文件路径(仅在 synthesisCompleted 类型中有效)
final String? audioFilePath;
/// 进度百分比(仅在 synthesisProgress 类型中有效)
final int? progressPercent;
/// 错误信息(仅在 error 和 synthesisCanceled 类型中有效)
final String? error;
/// 取消原因(仅在 synthesisCanceled 类型中有效)
final String? reason;
/// 错误详情(仅在 synthesisCanceled 类型中有效)
final String? errorDetails;
SynthesisEvent({
required this.type,
this.audioFilePath,
this.progressPercent,
this.error,
this.reason,
this.errorDetails,
});
/// 从原生事件映射创建合成事件
factory SynthesisEvent.fromMap(Map<dynamic, dynamic> map) {
final eventType = map['eventType'] as String;
SynthesisEventType type;
switch (eventType) {
case 'synthesisStarted':
type = SynthesisEventType.synthesisStarted;
break;
case 'synthesisProgress':
type = SynthesisEventType.synthesisProgress;
break;
case 'synthesisCompleted':
type = SynthesisEventType.synthesisCompleted;
break;
case 'synthesisCanceled':
type = SynthesisEventType.synthesisCanceled;
break;
case 'error':
type = SynthesisEventType.error;
break;
default:
throw ArgumentError('未知事件类型: $eventType');
}
return SynthesisEvent(
type: type,
audioFilePath: map['audioFilePath'] as String?,
progressPercent: map['progressPercent'] as int?,
error: map['error'] as String?,
reason: map['reason'] as String?,
errorDetails: map['errorDetails'] as String?,
);
}
@override
String toString() {
return 'SynthesisEvent{type: $type, audioFilePath: $audioFilePath, progressPercent: $progressPercent, error: $error, reason: $reason, errorDetails: $errorDetails}';
}
}
/// TTS 状态
enum TtsState {
/// 空闲
idle,
/// 合成中
synthesizing,
/// 播放中
playing,
/// 已停止
stopped,
}
/// Azure 文本转语音服务
class AzureTtsService extends GetxService {
static AzureTtsService get to => Get.find();
// 方法通道
static const MethodChannel _methodChannel = MethodChannel('com.example.deep_voice/azure_tts');
// 事件通道
static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/azure_tts_events');
// 是否已初始化
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
// 当前TTS状态
final Rx<TtsState> _ttsState = TtsState.idle.obs;
TtsState get ttsState => _ttsState.value;
// 合成相关
StreamController<SynthesisEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 公开的事件流
Stream<SynthesisEvent>? _synthesisStream;
Stream<SynthesisEvent>? get synthesisStream => _synthesisStream;
// 当前语音名称
String _currentVoice = 'zh-CN-XiaoxiaoNeural';
String get currentVoice => _currentVoice;
set currentVoice(String voice) {
_currentVoice = voice;
_setVoice(voice);
}
// 语速 (-100 to 100)
int _rate = 0;
int get rate => _rate;
set rate(int value) {
if (value < -100 || value > 100) {
throw ArgumentError('语速必须在 -100 到 100 之间');
}
_rate = value;
_setSpeechRate(value);
}
// 音调 (-100 to 100)
int _pitch = 0;
int get pitch => _pitch;
set pitch(int value) {
if (value < -100 || value > 100) {
throw ArgumentError('音调必须在 -100 到 100 之间');
}
_pitch = value;
_setPitch(value);
}
// 音量 (0 to 100)
int _volume = 100;
int get volume => _volume;
set volume(int value) {
if (value < 0 || value > 100) {
throw ArgumentError('音量必须在 0 到 100 之间');
}
_volume = value;
_setVolume(value);
}
// 可用的语音列表
final RxList<Map<String, String>> _voices = <Map<String, String>>[].obs;
List<Map<String, String>> get voices => _voices;
/// 初始化 Azure 文本转语音服务
///
/// [subscriptionKey] Azure 语音服务的订阅密钥
/// [serviceRegion] Azure 语音服务的区域
/// [defaultVoice] 默认语音名称,默认为中文女声
Future<bool> initialize({
String? subscriptionKey,
String? serviceRegion,
String? defaultVoice,
}) async {
if (_isInitialized) return true;
try {
// 从环境变量中读取配置,如果没有传入参数
final key = subscriptionKey ?? dotenv.env['AZURE_TTS_SUBSCRIPTION_KEY'] ?? '';
final region = serviceRegion ?? dotenv.env['AZURE_TTS_SERVICE_REGION'] ?? 'eastasia';
final voice = defaultVoice ?? dotenv.env['AZURE_TTS_DEFAULT_VOICE'] ?? 'zh-CN-XiaoxiaoNeural';
// 检查配置是否完整
if (key.isEmpty) {
Logger.warning('Azure 文本转语音服务的订阅密钥未设置,请检查 .env 文件');
return false;
}
// 设置事件监听
_setupEventChannel();
final result = await _methodChannel.invokeMethod<bool>(
'initialize',
{
'subscriptionKey': key,
'serviceRegion': region,
'defaultVoice': voice,
},
);
_isInitialized = result ?? false;
if (_isInitialized) {
Logger.info('Azure 文本转语音服务初始化成功');
_currentVoice = voice;
// 获取可用语音列表
await getAvailableVoices();
} else {
Logger.error('Azure 文本转语音服务初始化失败');
}
return _isInitialized;
} catch (e) {
Logger.error('初始化 Azure 文本转语音服务失败', e);
_isInitialized = false;
return false;
}
}
/// 设置事件通道监听
void _setupEventChannel() {
// 创建事件流控制器
_eventStreamController = StreamController<SynthesisEvent>.broadcast();
// 设置事件监听
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.map<SynthesisEvent>((dynamic event) => SynthesisEvent.fromMap(event))
.listen(
(event) {
_handleSynthesisEvent(event);
_eventStreamController?.add(event);
},
onError: (error) {
Logger.error('Azure 文本转语音事件流错误', error);
_eventStreamController?.addError(error);
},
);
// 设置公开的流
_synthesisStream = _eventStreamController?.stream;
}
/// 处理合成事件
void _handleSynthesisEvent(SynthesisEvent event) {
switch (event.type) {
case SynthesisEventType.synthesisStarted:
_ttsState.value = TtsState.synthesizing;
break;
case SynthesisEventType.synthesisCompleted:
_ttsState.value = TtsState.idle;
break;
case SynthesisEventType.synthesisCanceled:
case SynthesisEventType.error:
_ttsState.value = TtsState.idle;
break;
default:
// 其他事件类型不需要特殊处理
break;
}
}
/// 获取可用的语音列表
Future<List<Map<String, String>>> getAvailableVoices() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<List<dynamic>>('getAvailableVoices');
if (result != null) {
final voicesList = result.map((item) {
final map = Map<String, dynamic>.from(item as Map);
return {
'name': map['name'] as String,
'locale': map['locale'] as String,
'gender': map['gender'] as String,
'displayName': map['displayName'] as String,
};
}).toList();
_voices.value = List<Map<String, String>>.from(voicesList);
return _voices;
}
return [];
} catch (e) {
Logger.error('获取可用语音列表失败', e);
return [];
}
}
/// 设置语音
Future<bool> _setVoice(String voiceName) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setVoice',
{'voiceName': voiceName},
);
return result ?? false;
} catch (e) {
Logger.error('设置语音失败', e);
return false;
}
}
/// 设置语速
Future<bool> _setSpeechRate(int rate) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setSpeechRate',
{'rate': rate},
);
return result ?? false;
} catch (e) {
Logger.error('设置语速失败', e);
return false;
}
}
/// 设置音调
Future<bool> _setPitch(int pitch) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setPitch',
{'pitch': pitch},
);
return result ?? false;
} catch (e) {
Logger.error('设置音调失败', e);
return false;
}
}
/// 设置音量
Future<bool> _setVolume(int volume) async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>(
'setVolume',
{'volume': volume},
);
return result ?? false;
} catch (e) {
Logger.error('设置音量失败', e);
return false;
}
}
/// 合成文本为语音并保存到文件
///
/// [text] 要合成的文本
/// [fileName] 保存的文件名(不包含路径和扩展名)
///
/// 返回保存的音频文件路径,如果失败则返回 null
Future<String?> synthesizeToFile(String text, String fileName) async {
_checkInitialized();
if (text.isEmpty) {
Logger.warning('合成文本为空');
return null;
}
try {
// 获取临时目录
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/$fileName.wav';
// 设置状态
_ttsState.value = TtsState.synthesizing;
final result = await _methodChannel.invokeMethod<String>(
'synthesizeToFile',
{
'text': text,
'filePath': filePath,
},
);
// 合成完成后设置状态
_ttsState.value = TtsState.idle;
return result;
} catch (e) {
Logger.error('合成文本到文件失败', e);
// 发生错误时设置状态
_ttsState.value = TtsState.idle;
return null;
}
}
/// 合成文本并直接播放
///
/// [text] 要合成并播放的文本
///
/// 返回是否成功开始播放
Future<bool> speak(String text) async {
_checkInitialized();
if (text.isEmpty) {
Logger.warning('播放文本为空');
return false;
}
try {
// 设置状态
_ttsState.value = TtsState.synthesizing;
final result = await _methodChannel.invokeMethod<bool>(
'speak',
{'text': text},
);
if (result == true) {
_ttsState.value = TtsState.playing;
} else {
_ttsState.value = TtsState.idle;
}
return result ?? false;
} catch (e) {
Logger.error('合成并播放文本失败', e);
// 发生错误时设置状态
_ttsState.value = TtsState.idle;
return false;
}
}
/// 停止播放
Future<bool> stop() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>('stop');
if (result == true) {
_ttsState.value = TtsState.stopped;
}
return result ?? false;
} catch (e) {
Logger.error('停止播放失败', e);
return false;
}
}
/// 暂停播放
Future<bool> pause() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>('pause');
return result ?? false;
} catch (e) {
Logger.error('暂停播放失败', e);
return false;
}
}
/// 恢复播放
Future<bool> resume() async {
_checkInitialized();
try {
final result = await _methodChannel.invokeMethod<bool>('resume');
return result ?? false;
} catch (e) {
Logger.error('恢复播放失败', e);
return false;
}
}
/// 检查是否已初始化
void _checkInitialized() {
if (!_isInitialized) {
throw StateError('Azure 文本转语音服务未初始化');
}
}
/// 释放资源
Future<void> dispose() async {
try {
await _eventSubscription?.cancel();
_eventSubscription = null;
// 只有在控制器存在且未关闭时才关闭
if (_eventStreamController != null && !_eventStreamController!.isClosed) {
await _eventStreamController?.close();
}
_eventStreamController = null;
_synthesisStream = null;
if (_isInitialized) {
await _methodChannel.invokeMethod<bool>('dispose');
}
_isInitialized = false;
Logger.info('Azure 文本转语音服务资源已释放');
} catch (e) {
Logger.error('释放 Azure 文本转语音服务资源失败', e);
}
}
@override
void onClose() {
dispose();
super.onClose();
}
}

95
lib/data/services/background_agent_service.dart

@ -6,15 +6,29 @@ import '../providers/agent_provider.dart';
import '../models/agent.dart'; import '../models/agent.dart';
import '../../modules/chat/controllers/chat_controller.dart'; import '../../modules/chat/controllers/chat_controller.dart';
import '../../modules/chat/models/message_model.dart'; import '../../modules/chat/models/message_model.dart';
import 'azure_asr_service.dart'; import 'volcano_asr_api_service.dart';
import 'volcano_ai_service.dart'; import 'volcano_ai_service.dart';
import 'volcano_tts_api_service.dart'; import 'volcano_tts_api_service.dart';
import 'chat_history_service.dart'; import 'chat_history_service.dart';
/// 后台代理服务,用于在应用处于后台时处理AI交互 /// 后台代理服务,用于在应用处于后台时处理AI交互
class BackgroundAgentService extends GetxService { class BackgroundAgentService extends GetxService {
// 单例实例
static BackgroundAgentService? _instance;
// 单例工厂方法
factory BackgroundAgentService() {
_instance ??= BackgroundAgentService._internal();
return _instance!;
}
// 私有构造函数
BackgroundAgentService._internal() {
Logger.info('BackgroundAgentService 已创建');
}
// 服务实例 // 服务实例
final AzureAsrService _asrService = Get.find<AzureAsrService>(); final VolcanoAsrApiService _asrService = Get.find<VolcanoAsrApiService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>(); final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>(); final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
@ -61,11 +75,6 @@ class BackgroundAgentService extends GetxService {
// 本地消息历史备份 // 本地消息历史备份
final List<Map<String, String>> _localMessageHistory = []; final List<Map<String, String>> _localMessageHistory = [];
// 构造函数
BackgroundAgentService() {
Logger.info('BackgroundAgentService 已创建');
}
// TTS是否正在播放,直接从VolcanoTtsApiService获取 // TTS是否正在播放,直接从VolcanoTtsApiService获取
bool get isTtsSpeaking => _ttsService.isPlaying; bool get isTtsSpeaking => _ttsService.isPlaying;
@ -82,7 +91,7 @@ class BackgroundAgentService extends GetxService {
// 停止语音识别 // 停止语音识别
if (isListening.value) { if (isListening.value) {
_asrService.stopContinuousRecognition(); _asrService.stopRecognition();
} }
// 取消定时器 // 取消定时器
@ -133,38 +142,44 @@ class BackgroundAgentService extends GetxService {
} }
} }
/// 启动语音识别 /// 停止语音识别
Future<void> _stopVoiceRecognition() async {
if (isListening.value) {
isListening.value = false;
// 取消订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
// 停止语音识别
_asrService.stopRecognition();
// 重置状态
recognizedText.value = '';
}
}
/// 开始语音识别
Future<void> _startVoiceRecognition() async { Future<void> _startVoiceRecognition() async {
if (isListening.value) { if (isListening.value) {
// 如果已经在监听,不需要重新启动
Logger.info('语音识别已经在运行,无需重新启动');
return; return;
} }
try { try {
// 启动语音识别 // 启动语音识别
final success = await _asrService.startContinuousRecognition(); final success = await _asrService.startRecognition();
if (success) { if (success) {
isListening.value = true; isListening.value = true;
recognizedText.value = '';
// 监听识别事件 // 监听识别结果
_recognitionSubscription = _asrService.recognitionStream?.listen( _recognitionSubscription = _asrService.recognitionStream.listen(
(event) { (event) {
// 重置无活动定时器
_resetInactivityTimer();
switch (event.type) { switch (event.type) {
case RecognitionEventType.recognizing: case RecognitionEventType.recognizing:
// 实时更新识别文本 // 实时更新识别文本
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
recognizedText.value = event.text; recognizedText.value = event.text;
// 如果正在播放TTS,检测到用户说话则中断当前播放和AI回复
if (_ttsService.isPlaying && event.text.trim().length > 3) {
_interruptCurrentResponse("检测到用户说话,中断当前响应");
}
} }
break; break;
case RecognitionEventType.finalResult: case RecognitionEventType.finalResult:
@ -175,32 +190,34 @@ class BackgroundAgentService extends GetxService {
// 处理识别到的文本 // 处理识别到的文本
_processRecognizedText(event.text); _processRecognizedText(event.text);
// 重置识别文本但保持识别状态 // 重置识别文本
recognizedText.value = ''; recognizedText.value = '';
} }
break; break;
case RecognitionEventType.sessionStopped: case RecognitionEventType.error:
case RecognitionEventType.canceled: // 错误处理
// 会话结束,表示识别在其它地方被启动,直接退出后台服务 Logger.info('语音识别错误: ${event.error}');
isListening.value = false;
Logger.info('语音识别会话被中止,退出后台代理服务'); // 不再尝试重新启动语音识别,而是直接停止交互
Logger.info('语音识别出错,停止后台代理交互');
stopInteraction(); stopInteraction();
break; break;
case RecognitionEventType.started:
default: default:
// 不做任何处理
break; break;
} }
}, },
onError: (error) { onError: (error) {
Logger.error('语音识别错误', error); Logger.info('语音识别错误:$error');
isListening.value = false;
// 如果出错,尝试重新启动语音识别 // 不再尝试重新启动语音识别,而是直接停止交互
_restartVoiceRecognition(); Logger.info('语音识别流错误,停止后台代理交互');
stopInteraction();
}, },
); );
} else { } else {
Logger.error('启动语音识别失败'); Logger.error('启动语音识别失败');
isProcessing.value = false;
} }
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败', e); Logger.error('启动语音识别失败', e);
@ -278,7 +295,7 @@ class BackgroundAgentService extends GetxService {
Future<void> _restartVoiceRecognition() async { Future<void> _restartVoiceRecognition() async {
// 停止当前的语音识别 // 停止当前的语音识别
if (isListening.value) { if (isListening.value) {
await _asrService.stopContinuousRecognition(); await _asrService.stopRecognition();
isListening.value = false; isListening.value = false;
// 取消当前的订阅 // 取消当前的订阅
@ -343,7 +360,6 @@ class BackgroundAgentService extends GetxService {
'content': message.content, 'content': message.content,
}); });
} }
Logger.info('已加载${messages.length}条聊天历史记录,代理: ${_currentAgent!.id}');
} }
} catch (e) { } catch (e) {
Logger.error('加载聊天历史记录失败', e); Logger.error('加载聊天历史记录失败', e);
@ -438,10 +454,9 @@ class BackgroundAgentService extends GetxService {
} }
// 停止语音识别 // 停止语音识别
// if (isListening.value) { if (isListening.value) {
// await _asrService.stopContinuousRecognition(); await _stopVoiceRecognition();
// isListening.value = false; }
// }
// 取消定时器 // 取消定时器
_inactivityTimer?.cancel(); _inactivityTimer?.cancel();

5
lib/data/services/chat_history_service.dart

@ -39,9 +39,7 @@ class ChatHistoryService extends GetxService {
final jsonList = messagesToSave.map((m) => m.toJson()).toList(); final jsonList = messagesToSave.map((m) => m.toJson()).toList();
final jsonString = jsonEncode(jsonList); final jsonString = jsonEncode(jsonList);
// 保存到存储
await _storage.write(_getHistoryKey(agentId), jsonString);
Logger.info('已保存${messagesToSave.length}条聊天记录,代理: $agentId');
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败', e); Logger.error('保存聊天记录失败', e);
} }
@ -62,7 +60,6 @@ class ChatHistoryService extends GetxService {
final jsonList = jsonDecode(jsonString) as List; final jsonList = jsonDecode(jsonString) as List;
final messages = jsonList.map((json) => Message.fromJson(json)).toList(); final messages = jsonList.map((json) => Message.fromJson(json)).toList();
Logger.info('已加载${messages.length}条聊天记录,代理: $agentId');
return messages; return messages;
} catch (e) { } catch (e) {
Logger.error('加载聊天记录失败', e); Logger.error('加载聊天记录失败', e);

236
lib/data/services/flutter_asr_service.dart

@ -0,0 +1,236 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:speech_to_text/speech_to_text.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
import '../../core/utils/logger.dart';
/// ASR状态枚举
enum AsrState {
notInitialized, // 未初始化
initialized, // 已初始化
listening, // 正在监听
notListening, // 未监听
error // 错误状态
}
/// Flutter ASR服务,负责本地语音转文本功能
class FlutterAsrService extends GetxService {
// 语音识别实例
late SpeechToText _speech;
// 当前ASR状态
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value;
// 可用的语音识别语言
final RxList<LocaleName> _locales = <LocaleName>[].obs;
List<LocaleName> get locales => _locales;
// 当前选择的语言
final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value;
// 识别结果
final RxString _lastWords = ''.obs;
String get lastWords => _lastWords.value;
// 识别准确度
final RxDouble _confidence = 0.0.obs;
double get confidence => _confidence.value;
// 是否可用
final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value;
// 是否正在监听
bool get isListening => _speech.isListening;
// 识别结果流
final _recognitionResultsController = StreamController<String>.broadcast();
Stream<String> get recognitionResults => _recognitionResultsController.stream;
@override
void onInit() {
super.onInit();
_initSpeech();
}
/// 初始化语音识别
Future<void> _initSpeech() async {
_speech = SpeechToText();
try {
final available = await _speech.initialize(
onStatus: _onStatusChanged,
onError: _onErrorListener,
debugLogging: kDebugMode,
);
_isAvailable.value = available;
if (available) {
_asrState.value = AsrState.initialized;
// 获取可用的语言
final locales = await _speech.locales();
_locales.assignAll(locales);
// 默认选择系统语言,如果支持的话
final systemLocale = await _speech.systemLocale();
if (systemLocale != null) {
_currentLocale.value = systemLocale.localeId;
} else if (_locales.isNotEmpty) {
_currentLocale.value = _locales.first.localeId;
}
Logger.info('语音识别初始化成功,可用语言数量: ${_locales.length}');
} else {
_asrState.value = AsrState.error;
Logger.error('语音识别不可用');
}
} catch (e) {
_asrState.value = AsrState.error;
_isAvailable.value = false;
Logger.error('语音识别初始化失败: $e');
}
}
/// 开始监听
///
/// [listenFor] 监听时长,默认30秒
/// [pauseFor] 暂停时长,默认3秒,超过此时长无语音输入会自动停止
/// [partialResults] 是否返回部分结果,默认true
/// [onDevice] 是否使用设备本地识别,默认false
/// [listenMode] 监听模式,默认设备默认模式
Future<bool> startListening({
Duration listenFor = const Duration(seconds: 30),
Duration pauseFor = const Duration(seconds: 3),
bool partialResults = true,
bool onDevice = false,
ListenMode listenMode = ListenMode.deviceDefault,
}) async {
if (!_isAvailable.value) {
Logger.error('语音识别不可用,无法开始监听');
return false;
}
// 清空上次的识别结果
_lastWords.value = '';
_confidence.value = 0.0;
try {
final started = await _speech.listen(
onResult: _onSpeechResult,
listenFor: listenFor,
pauseFor: pauseFor,
localeId: _currentLocale.value,
listenOptions: SpeechListenOptions(
partialResults: partialResults,
onDevice: onDevice,
listenMode: listenMode,
),
);
if (started) {
_asrState.value = AsrState.listening;
Logger.info('开始语音识别监听');
} else {
Logger.error('开始语音识别监听失败');
}
return started;
} catch (e) {
Logger.error('开始语音识别监听出错: $e');
return false;
}
}
/// 停止监听
Future<void> stopListening() async {
if (!_speech.isListening) return;
try {
await _speech.stop();
_asrState.value = AsrState.notListening;
Logger.info('停止语音识别监听');
} catch (e) {
Logger.error('停止语音识别监听出错: $e');
}
}
/// 取消监听
Future<void> cancelListening() async {
if (!_speech.isListening) return;
try {
await _speech.cancel();
_lastWords.value = '';
_confidence.value = 0.0;
_asrState.value = AsrState.notListening;
Logger.info('取消语音识别监听');
} catch (e) {
Logger.error('取消语音识别监听出错: $e');
}
}
/// 设置当前语言
void setLocale(String localeId) {
if (_currentLocale.value != localeId) {
_currentLocale.value = localeId;
Logger.info('设置语音识别语言: $localeId');
}
}
/// 语音识别结果回调
void _onSpeechResult(SpeechRecognitionResult result) {
final resultText = result.recognizedWords;
_lastWords.value = resultText;
if (result.finalResult) {
// 计算置信度 - 仅在最终结果时有效
if (result.alternates.isNotEmpty) {
_confidence.value = result.alternates.first.confidence;
}
// 发送最终结果到流
_recognitionResultsController.add(resultText);
Logger.info('语音识别最终结果: "$resultText", 置信度: ${_confidence.value}');
_asrState.value = AsrState.notListening;
} else {
// 部分结果
Logger.info('语音识别部分结果: "$resultText"');
}
}
/// 状态变化回调
void _onStatusChanged(String status) {
Logger.info('语音识别状态变化: $status');
switch (status) {
case 'listening':
_asrState.value = AsrState.listening;
break;
case 'notListening':
_asrState.value = AsrState.notListening;
break;
case 'done':
_asrState.value = AsrState.notListening;
break;
}
}
/// 错误回调
void _onErrorListener(dynamic error) {
Logger.error('语音识别错误: ${error.errorMsg}');
_asrState.value = AsrState.error;
}
@override
void onClose() {
_speech.cancel();
_recognitionResultsController.close();
super.onClose();
}
}

364
lib/data/services/flutter_tts_service.dart

@ -0,0 +1,364 @@
import 'dart:io';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
/// Flutter TTS服务,负责本地文本转语音功能
class FlutterTtsService extends GetxService {
late FlutterTts _flutterTts;
// 当前TTS状态
TtsState _ttsState = TtsState.stopped;
TtsState get ttsState => _ttsState;
// 可用的语音
List<Map<String, String>> _voices = [];
List<Map<String, String>> get voices => _voices;
// 当前选择的语音
String _currentVoice = '';
String get currentVoice => _currentVoice;
set currentVoice(String voice) => _currentVoice = voice;
// 语速 (0.1-2.0)
double _rate = 0.5;
double get rate => _rate;
set rate(double value) => _rate = value;
// 音量 (0.0-1.0)
double _volume = 1.0;
double get volume => _volume;
set volume(double value) => _volume = value;
// 音调 (0.5-2.0)
double _pitch = 1.0;
double get pitch => _pitch;
set pitch(double value) => _pitch = value;
// 文本队列 - 仅包含待播放的文本
List<String> _textQueue = [];
List<String> get textQueue => _textQueue;
// 当前正在播放的文本
String _currentText = '';
String get currentText => _currentText;
// 是否正在处理队列
bool _isProcessingQueue = false;
@override
void onInit() {
super.onInit();
_initTts();
}
/// 初始化TTS引擎
Future<void> _initTts() async {
_flutterTts = FlutterTts();
// 设置完成回调
_flutterTts.setCompletionHandler(() {
Logger.info('TTS播放完成: "$_currentText"');
// 清空当前播放文本
_currentText = '';
// 处理队列中的下一条文本
_processNextInQueue();
// 只有当队列为空且不再处理队列时才设置为stopped状态
if (_textQueue.isEmpty && !_isProcessingQueue) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列播放完成,设置为stopped状态');
}
});
// 设置错误回调
_flutterTts.setErrorHandler((error) {
Logger.error('Flutter TTS错误: $error');
// 清空当前播放文本
_currentText = '';
// 尝试播放下一条文本
_processNextInQueue();
// 只有当队列为空且不再处理队列时才设置为stopped状态
if (_textQueue.isEmpty && !_isProcessingQueue) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列播放出错且为空,设置为stopped状态');
}
});
// 设置开始回调
_flutterTts.setStartHandler(() {
_ttsState = TtsState.playing;
});
// 设置取消回调
_flutterTts.setCancelHandler(() {
// 清空当前播放文本
_currentText = '';
_isProcessingQueue = false;
_ttsState = TtsState.stopped;
Logger.info('TTS播放被取消,设置为stopped状态');
});
// 设置暂停回调
_flutterTts.setPauseHandler(() {
_ttsState = TtsState.paused;
});
// 设置继续回调
_flutterTts.setContinueHandler(() {
_ttsState = TtsState.playing;
});
// 加载可用语音
await _loadVoices();
Logger.info('Flutter TTS服务已初始化');
}
/// 加载可用的语音
Future<void> _loadVoices() async {
try {
if (Platform.isAndroid || Platform.isIOS) {
final voices = await _flutterTts.getVoices;
if (voices != null) {
_voices = [];
for (var voice in voices) {
if (voice is Map) {
final Map<String, String> voiceMap = {};
voice.forEach((key, value) {
if (key is String && value is String) {
voiceMap[key] = value;
} else if (key is String) {
voiceMap[key] = value.toString();
}
});
if (voiceMap.containsKey('name')) {
_voices.add(voiceMap);
}
}
}
// 如果有可用语音,设置第一个为默认
if (_voices.isNotEmpty) {
_currentVoice = _voices[0]['name'] ?? '';
await _flutterTts.setVoice({"name": _currentVoice});
}
}
}
} catch (e) {
Logger.error('加载TTS语音失败', e);
}
}
/// 设置语音
Future<void> setVoice(String voiceName) async {
try {
_currentVoice = voiceName;
await _flutterTts.setVoice({"name": voiceName});
} catch (e) {
Logger.error('设置TTS语音失败', e);
}
}
/// 设置语速
Future<void> setRate(double rate) async {
try {
_rate = rate;
await _flutterTts.setSpeechRate(rate);
} catch (e) {
Logger.error('设置TTS语速失败', e);
}
}
/// 设置音量
Future<void> setVolume(double volume) async {
try {
_volume = volume;
await _flutterTts.setVolume(volume);
} catch (e) {
Logger.error('设置TTS音量失败', e);
}
}
/// 设置音调
Future<void> setPitch(double pitch) async {
try {
_pitch = pitch;
await _flutterTts.setPitch(pitch);
} catch (e) {
Logger.error('设置TTS音调失败', e);
}
}
/// 设置语言
Future<void> setLanguage(String language) async {
try {
await _flutterTts.setLanguage(language);
} catch (e) {
Logger.error('设置TTS语言失败', e);
}
}
/// 添加文本到队列并开始处理队列(如果尚未开始)
Future<void> speak(String text) async {
if (text.isEmpty) return;
// 添加到队列
_addToQueue(text);
// 开始处理队列(如果尚未开始)
_startQueueProcessingIfNeeded();
}
/// 添加多条文本到队列并开始处理队列(如果尚未开始)
Future<void> speakTexts(List<String> texts) async {
if (texts.isEmpty) return;
// 添加所有非空文本到队列
int addedCount = 0;
for (final text in texts) {
if (text.isNotEmpty) {
_textQueue.add(text);
addedCount++;
}
}
if (addedCount > 0) {
Logger.info('已添加$addedCount条文本到TTS队列');
// 开始处理队列(如果尚未开始)
_startQueueProcessingIfNeeded();
}
}
/// 添加文本到队列
void _addToQueue(String text) {
if (text.isEmpty) return;
_textQueue.add(text);
Logger.info('已添加文本到TTS队列: "${text.length > 20 ? text.substring(0, 20) + '...' : text}"');
}
/// 开始处理队列(如果尚未开始)
void _startQueueProcessingIfNeeded() {
// 如果队列为空,不需要处理
if (_textQueue.isEmpty) return;
// 如果已经在处理队列或正在播放,不需要重新开始
if (_isProcessingQueue || _ttsState == TtsState.playing) return;
// 开始处理队列
_isProcessingQueue = true;
_processNextInQueue();
}
/// 处理队列中的下一条文本
void _processNextInQueue() {
// 如果队列为空,结束处理但不立即设置stopped状态
if (_textQueue.isEmpty) {
_currentText = '';
_isProcessingQueue = false;
// 只有当当前没有文本在播放时才设置为stopped状态
if (_currentText.isEmpty) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列为空,设置为stopped状态');
}
return;
}
// 获取队列中的第一条文本并播放
_speakNextText();
}
/// 播放队列中的下一条文本
Future<void> _speakNextText() async {
if (_textQueue.isEmpty) {
_isProcessingQueue = false;
// 只有当当前没有文本在播放时才设置为stopped状态
if (_currentText.isEmpty) {
_ttsState = TtsState.stopped;
Logger.info('TTS队列播放完成,设置为stopped状态');
}
return;
}
// 获取队列中的第一条文本
final text = _textQueue.removeAt(0);
_currentText = text;
try {
// 设置TTS参数
await _flutterTts.setSpeechRate(_rate);
await _flutterTts.setVolume(_volume);
await _flutterTts.setPitch(_pitch);
if (_currentVoice.isNotEmpty) {
await _flutterTts.setVoice({"name": _currentVoice});
}
// 播放文本
await _flutterTts.speak(text);
Logger.info('正在播放文本,队列中还有${_textQueue.length}条待播放');
} catch (e) {
Logger.error('TTS播放文本失败', e);
// 清空当前文本
_currentText = '';
// 尝试播放下一个
_processNextInQueue();
}
}
/// 停止播放并清空队列
Future<void> stop() async {
try {
await _flutterTts.stop();
// 重置队列处理状态
_textQueue.clear();
_currentText = '';
_isProcessingQueue = false;
_ttsState = TtsState.stopped;
Logger.info('已停止TTS播放并清空队列');
} catch (e) {
Logger.error('停止TTS播放失败', e);
}
}
/// 暂停播放
Future<void> pause() async {
try {
await _flutterTts.pause();
_ttsState = TtsState.paused;
} catch (e) {
Logger.error('暂停TTS播放失败', e);
}
}
/// 清空队列并停止播放
Future<void> clearQueue() async {
_textQueue.clear();
await stop();
Logger.info('已清空TTS队列');
}
@override
void onClose() {
_flutterTts.stop();
super.onClose();
}
}
/// TTS状态枚举
enum TtsState {
playing,
stopped,
paused,
}

97
lib/data/services/function_service.dart

@ -0,0 +1,97 @@
import 'dart:convert';
/// 函数服务
/// 仅实现各种功能函数,不包含AI交互逻辑
class FunctionService {
/// 天气查询工具定义
/// 返回一个可直接用于tools参数的天气查询工具定义
static Map<String, dynamic> getWeatherToolDefinition() {
return {
'type': 'function',
'function': {
'name': 'get_current_weather',
'description': '获取指定地点的当前天气信息',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': '地理位置,例如:北京市',
},
'unit': {
'type': 'string',
'description': '温度单位',
'enum': ['celsius', 'fahrenheit'],
},
},
'required': ['location'],
},
},
};
}
/// 模拟天气API实现
/// 返回一个模拟的天气信息,实际应用中应该调用真实的天气API
static Future<Map<String, dynamic>> getWeatherInfo(Map<String, dynamic> args) async {
final location = args['location'] ?? '未知地点';
final unit = args['unit'] ?? 'celsius';
// 在实际应用中,这里应该调用真实的天气API
return {
'location': location,
'temperature': unit == 'celsius' ? '25°C' : '77°F',
'conditions': '晴转多云',
'humidity': '55%',
'wind': '西南风 3级',
'date': DateTime.now().toString().split(' ')[0],
};
}
/// 解析函数调用参数
/// 从AI响应中提取函数调用参数
static Map<String, dynamic> parseFunctionCallArguments(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['function'] == null || toolCall['function']['arguments'] == null) {
throw FunctionServiceException('Invalid function call format');
}
try {
return json.decode(toolCall['function']['arguments']);
} catch (e) {
throw FunctionServiceException('Failed to parse function arguments: $e');
}
}
/// 获取函数调用ID和名称
/// 用于构建工具响应消息
static Map<String, String> getFunctionCallInfo(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['id'] == null || toolCall['function'] == null || toolCall['function']['name'] == null) {
throw FunctionServiceException('Invalid function call format');
}
return {
'id': toolCall['id'],
'name': toolCall['function']['name'],
};
}
}
/// 函数调用服务异常类
class FunctionServiceException implements Exception {
final String message;
FunctionServiceException(this.message);
@override
String toString() => message;
}

7
lib/data/services/my_audio_handler.dart

@ -132,17 +132,14 @@ class MyAudioHandler extends BaseAudioHandler {
updatePosition: Duration.zero, updatePosition: Duration.zero,
)); ));
// 如果状态变为播放,确保前台服务启动
if (playing) {
Logger.info('更新播放状态为播放中,确保前台服务启动');
}
} catch (e) { } catch (e) {
Logger.error('更新播放状态失败', e); Logger.error('更新播放状态失败', e);
} }
} }
Future<void> _handleInteraction() async { Future<void> _handleInteraction() async {
Logger.info('处理媒体按钮交互'); // Logger.info('处理媒体按钮交互');
// 防止重复处理 // 防止重复处理
if (_isHandlingInteraction) { if (_isHandlingInteraction) {

1003
lib/data/services/volcano_asr_api_service.dart

File diff suppressed because it is too large

384
lib/data/services/volcano_asr_service.dart

@ -1,384 +0,0 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
/// 识别事件类型
enum RecognitionEventType {
started,
recognizing,
finalResult,
error,
completed,
}
/// 识别事件
class RecognitionEvent {
final RecognitionEventType type;
final String text;
final String? error;
RecognitionEvent({
required this.type,
this.text = '',
this.error,
});
}
/// 火山语音识别服务
///
/// 该服务提供了通过平台通道与 Android 上的火山语音识别 SDK 交互的接口
class VolcanoAsrService extends GetxService {
// 平台通道
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/volcano_asr');
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;
final _isInitialized = false.obs;
final _errorMessage = ''.obs;
final _recognitionResults = <String>[].obs;
// 是否使用标准语音识别SDK
final bool _useStandardASR = true; // 使用标准语音识别SDK (API v2)
// 添加识别结果流控制器
final _recognitionStreamController = StreamController<RecognitionEvent>.broadcast();
// 事件订阅
StreamSubscription? _eventSubscription;
// 最新的识别结果
final _latestRecognizedText = ''.obs;
String get latestRecognizedText => _latestRecognizedText.value;
/// 检查连续识别是否处于活动状态
bool isContinuousRecognitionActive() {
return _isListening.value;
}
VolcanoSrcService() {
// 从环境变量获取配置
_appId = dotenv.env['VOLCANO_ASR_APP_ID'] ?? '';
_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 = '火山语音识别配置不完整,请检查环境变量';
Logger.error(_errorMessage.value);
}
}
// 获取可观察状态
RxBool get isListening => _isListening;
bool get isInitialized => _isInitialized.value;
String get errorMessage => _errorMessage.value;
List<String> get recognitionResults => _recognitionResults;
// 添加识别结果流getter
Stream<RecognitionEvent> get recognitionStream => _recognitionStreamController.stream;
@override
void onInit() {
super.onInit();
_setupMethodCallHandler();
}
/// 设置方法通道处理器
Future<void> _setupMethodCallHandler() async {
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'onRecognitionResult':
final String result = call.arguments as String;
_onRecognitionResult(result);
break;
case 'onRecognitionError':
final String error = call.arguments as String;
_errorMessage.value = error;
Logger.error('识别错误: $error');
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break;
case 'onListeningStateChanged':
final bool isListening = call.arguments as bool;
_isListening.value = isListening;
Logger.info('监听状态变化: $isListening');
break;
}
});
}
/// 处理识别结果
void _onRecognitionResult(String result) {
if (result.isNotEmpty) {
_recognitionResults.add(result);
// 将结果发送到流
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: result,
));
Logger.info('识别结果: $result');
}
}
/// 初始化语音识别引擎
Future<bool> initialize() async {
if (_isInitialized.value) return true;
if (_appId.isEmpty || _token.isEmpty) {
_errorMessage.value = '火山语音识别配置不完整,请检查环境变量';
Logger.error(_errorMessage.value);
return false;
}
try {
_errorMessage.value = '';
Logger.info('开始初始化火山语音识别服务,APP_ID: ${_appId.substring(0, math.min(3, _appId.length))}***,APP_KEY: ${_token.length > 10 ? "${_token.substring(0, 5)}..." : _token}');
Logger.info('使用标准语音识别SDK配置 (API v2)');
Logger.info('集群区域: $_cluster');
Logger.info('资源ID: $_resourceId');
final Map<String, dynamic> params = {
'subscriptionKey': _appId,
'serviceRegion': _token,
'cluster': _cluster,
'resourceId': _resourceId,
};
final bool result = await _channel.invokeMethod('initialize', params);
if (result) {
_isInitialized.value = true;
Logger.info('火山语音识别服务初始化成功');
} else {
_errorMessage.value = '初始化失败,请检查配置';
Logger.error('火山语音识别服务初始化失败');
Logger.info('请检查以下可能的问题:');
Logger.info('1. 确保您的APP_ID和APP_KEY正确');
Logger.info('2. 确保您的应用ID和密钥正确且有效');
Logger.info('3. 确保您的应用已被授权使用语音识别服务');
}
return result;
} catch (e) {
_errorMessage.value = '初始化异常: $e';
Logger.error('初始化火山语音识别服务异常: $e');
Logger.info('请检查以下可能的问题:');
Logger.info('1. 确保您的网络连接正常');
Logger.info('2. 确保集群区域设置正确 (当前: $_cluster)');
Logger.info('3. 确保请求参数格式正确');
Logger.info('4. 确保Token格式正确,需要添加Bearer;前缀');
_isInitialized.value = false;
return false;
}
}
/// 开始一次性识别
Future<bool> startOneTimeRecognition() async {
if (_isListening.value) {
Logger.warning('已经在进行语音识别,请先停止当前识别');
return false;
}
if (!_isInitialized.value) {
try {
final bool initialized = await initialize();
if (!initialized) {
Logger.error('语音识别服务未初始化,无法开始识别');
_errorMessage.value = '语音识别服务未初始化,无法开始识别';
return false;
}
} catch (e) {
Logger.error('初始化语音识别服务失败: $e');
_errorMessage.value = '初始化语音识别服务失败: $e';
return false;
}
}
try {
_errorMessage.value = '';
_recognitionResults.clear();
final bool result = await _channel.invokeMethod('recognizeOnce');
_isListening.value = result;
if (result) {
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.started,
text: '',
));
}
return result;
} catch (e) {
Logger.error('开始一次性识别失败: $e');
_errorMessage.value = e.toString();
return false;
}
}
/// 开始连续识别
Future<bool> startContinuousRecognition() async {
//启动前先尝试停止当前识别
if (_isListening.value) {
Logger.warning('已经在进行语音识别,请先停止当前识别');
return false;
}
if (!_isInitialized.value) {
try {
final bool initialized = await initialize();
if (!initialized) {
Logger.error('语音识别服务未初始化,无法开始识别');
_errorMessage.value = '语音识别服务未初始化,无法开始识别';
return false;
}
} catch (e) {
Logger.error('初始化语音识别服务失败: $e');
_errorMessage.value = '初始化语音识别服务失败: $e';
return false;
}
}
try {
_errorMessage.value = '';
_recognitionResults.clear();
// 设置事件监听
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleNativeEvent, onError: (error) {
_errorMessage.value = error.toString();
_isListening.value = false;
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error.toString(),
));
});
final bool result = await _channel.invokeMethod('startContinuousRecognition');
_isListening.value = result;
if (result) {
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.started,
text: '',
));
}
return result;
} catch (e) {
Logger.error('开始连续识别失败: $e');
_errorMessage.value = e.toString();
_cleanupEventStream();
return false;
}
}
/// 处理来自原生端的事件
void _handleNativeEvent(dynamic event) {
if (event is! Map) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['eventType'] as String? ?? '';
switch (eventType) {
case 'recognizing':
final String text = eventMap['text'] as String? ?? '';
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.recognizing,
text: text,
));
break;
case 'finalResult':
final String text = eventMap['text'] as String? ?? '';
_latestRecognizedText.value = text;
_recognitionResults.add(text);
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
));
break;
case 'error':
final String error = eventMap['error'] as String? ?? '未知错误';
_errorMessage.value = error;
_isListening.value = false;
_recognitionStreamController.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break;
}
}
/// 清理事件流
void _cleanupEventStream() {
_eventSubscription?.cancel();
_eventSubscription = null;
}
/// 停止识别
Future<bool> stopRecognition() async {
if (!_isListening.value) {
Logger.warning('当前没有进行语音识别');
return false;
}
try {
final bool result = await _channel.invokeMethod('stopContinuousRecognition');
_isListening.value = !result;
if (result) {
_cleanupEventStream();
}
return result;
} catch (e) {
Logger.error('停止识别失败: $e');
_errorMessage.value = e.toString();
return false;
}
}
/// 清理资源
Future<void> dispose() async {
try {
if (_isListening.value) {
await stopRecognition();
}
_cleanupEventStream();
} catch (e) {
Logger.error('清理语音识别资源失败: $e');
}
}
@override
void onClose() {
_eventSubscription?.cancel();
_recognitionStreamController.close();
super.onClose();
}
}

269
lib/data/services/volcano_tts_api_service.dart

@ -11,9 +11,39 @@ import 'package:web_socket_channel/io.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import '../../core/utils/logger.dart';
/// 火山语音合成双向流式API服务 /// 火山语音合成双向流式API服务
///
/// 提供与火山语音TTS服务的WebSocket交互功能
/// 用户需要自行管理会话状态,确保在使用完毕后调用 endSession
///
/// 基本使用流程:
/// 1. 调用 startSession 开始会话
/// 2. 调用 synthesize 合成文本
/// 3. 调用 endSession 结束会话
///
/// 或者使用 speakSingle 一次性完成上述流程
class VolcanoTtsApiService extends GetxController { class VolcanoTtsApiService extends GetxController {
// 单例实例
static VolcanoTtsApiService? _instance;
// 单例工厂方法
factory VolcanoTtsApiService() {
_instance ??= VolcanoTtsApiService._internal();
return _instance!;
}
// 私有构造函数
VolcanoTtsApiService._internal() :
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '',
_resourceId = dotenv.env['VOLCANO_TTS_RESOURCE_ID'] ?? '' {
if (_appId.isEmpty || _token.isEmpty) {
throw Exception('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID 和 VOLCANO_TTS_APP_TOKEN');
}
}
// 协议常量 // 协议常量
static const int _protocolVersion = 0x0001; static const int _protocolVersion = 0x0001;
static const int _defaultHeaderSize = 0x0001; static const int _defaultHeaderSize = 0x0001;
@ -73,7 +103,6 @@ class VolcanoTtsApiService extends GetxController {
// 状态控制 // 状态控制
bool _isConnected = false; bool _isConnected = false;
bool _isSessionActive = false;
// 音频数据控制器 // 音频数据控制器
final _audioDataController = StreamController<Uint8List>.broadcast(); final _audioDataController = StreamController<Uint8List>.broadcast();
@ -83,6 +112,9 @@ class VolcanoTtsApiService extends GetxController {
final _eventController = StreamController<Map<String, dynamic>>.broadcast(); final _eventController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get eventStream => _eventController.stream; Stream<Map<String, dynamic>> get eventStream => _eventController.stream;
// 控制器状态
bool _isDisposed = false;
// 当前状态 // 当前状态
final isConnecting = false.obs; final isConnecting = false.obs;
final isConnected = false.obs; final isConnected = false.obs;
@ -112,16 +144,6 @@ class VolcanoTtsApiService extends GetxController {
DateTime? _lastCheckTime; DateTime? _lastCheckTime;
int? _lastCheckedIndex; int? _lastCheckedIndex;
// 构造函数
VolcanoTtsApiService() :
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '',
_resourceId = dotenv.env['VOLCANO_TTS_RESOURCE_ID'] ?? '' {
if (_appId.isEmpty || _token.isEmpty) {
throw Exception('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID 和 VOLCANO_TTS_APP_TOKEN');
}
}
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -295,14 +317,14 @@ class VolcanoTtsApiService extends GetxController {
/// 连接到火山语音服务 /// 连接到火山语音服务
Future<bool> connect() async { Future<bool> connect() async {
if (_isConnected) { if (_isConnected) {
Logger.info('TTS已连接,跳过连接');
return true; // 已经连接 return true; // 已经连接
} }
print('连接');
try {
isConnecting.value = true; isConnecting.value = true;
try {
Logger.info('开始TTS连接');
// 生成连接ID // 生成连接ID
_connectionId = const Uuid().v4(); _connectionId = const Uuid().v4();
@ -313,7 +335,6 @@ class VolcanoTtsApiService extends GetxController {
if (!kIsWeb && (io.Platform.isAndroid || io.Platform.isIOS || io.Platform.isMacOS || io.Platform.isLinux || io.Platform.isWindows)) { if (!kIsWeb && (io.Platform.isAndroid || io.Platform.isIOS || io.Platform.isMacOS || io.Platform.isLinux || io.Platform.isWindows)) {
// 移动平台和桌面平台 - 使用IOWebSocketChannel // 移动平台和桌面平台 - 使用IOWebSocketChannel
_channel = IOWebSocketChannel.connect( _channel = IOWebSocketChannel.connect(
uri, uri,
headers: { headers: {
@ -334,13 +355,20 @@ class VolcanoTtsApiService extends GetxController {
// 监听消息 // 监听消息
_channel!.stream.listen( _channel!.stream.listen(
_handleMessage, _handleMessage,
onError: _handleError, onError: (error) {
// 直接处理错误,不尝试重连
Logger.error('WebSocket错误: $error');
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({
'eventType': 'error',
'error': error.toString()
});
}
},
onDone: _handleDone, onDone: _handleDone,
cancelOnError: false, cancelOnError: false,
); );
// 发送开始连接事件 // 发送开始连接事件
await _startConnection(); await _startConnection();
@ -348,32 +376,42 @@ class VolcanoTtsApiService extends GetxController {
final completer = Completer<bool>(); final completer = Completer<bool>();
// 设置超时 // 设置超时
final timeout = Timer(const Duration(seconds: 10), () { Timer? timeout;
timeout = Timer(const Duration(seconds: 10), () {
if (!completer.isCompleted) { if (!completer.isCompleted) {
Logger.error('火山TTS连接超时');
completer.complete(false); completer.complete(false);
_handleError(Exception('连接超时'));
// 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常
try {
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({
'eventType': 'connectionFailed',
'error': '连接超时'
});
} }
} catch (e) {
Logger.error('处理TTS连接超时事件失败: $e');
}
}
timeout = null;
}); });
// 监听连接事件 // 监听连接事件
final subscription = eventStream.listen((event) { final subscription = eventStream.listen((event) {
if (event['eventType'] == 'connectionStarted') { if (event['eventType'] == 'connectionStarted') {
if (!completer.isCompleted) { if (!completer.isCompleted) {
if (kDebugMode) { Logger.info('火山TTS连接成功');
print('连接成功');
}
completer.complete(true); completer.complete(true);
timeout.cancel(); timeout?.cancel();
timeout = null;
} }
} else if (event['eventType'] == 'connectionFailed') { } else if (event['eventType'] == 'connectionFailed') {
if (!completer.isCompleted) { if (!completer.isCompleted) {
if (kDebugMode) { Logger.error('火山TTS连接失败: ${event['error']}');
print('连接失败: ${event['error']}');
}
completer.complete(false); completer.complete(false);
timeout.cancel(); timeout?.cancel();
timeout = null;
} }
} }
}); });
@ -385,14 +423,13 @@ class VolcanoTtsApiService extends GetxController {
if (result) { if (result) {
_isConnected = true; _isConnected = true;
isConnected.value = true; isConnected.value = true;
Logger.info('火山TTS连接成功完成');
} }
return result; return result;
} catch (e) { } catch (e) {
if (kDebugMode) { // 只记录错误,不尝试重连
print('连接过程中发生异常: $e'); Logger.error('连接过程中发生异常: $e');
}
_handleError(e);
return false; return false;
} finally { } finally {
isConnecting.value = false; isConnecting.value = false;
@ -400,18 +437,22 @@ class VolcanoTtsApiService extends GetxController {
} }
/// 开始TTS会话 /// 开始TTS会话
///
/// 用户需要自行管理会话状态,确保在使用完毕后调用 endSession
///
/// [speaker] 发音人ID
///
/// 返回会话是否成功开始
Future<bool> startSession(String speaker) async { Future<bool> startSession(String speaker) async {
print('开始会话'); Logger.info('开始TTS会话');
try {
if (!_isConnected) { if (!_isConnected) {
final connected = await connect(); final connected = await connect();
Logger.info('TTS未连接状态, 开始连接, 连接结果: $connected');
if (!connected) return false; if (!connected) return false;
} }
if (_isSessionActive) {
return true; // 会话已经活跃
}
try {
// 生成会话ID // 生成会话ID
_sessionId = const Uuid().v4().replaceAll('-', ''); _sessionId = const Uuid().v4().replaceAll('-', '');
@ -422,24 +463,40 @@ class VolcanoTtsApiService extends GetxController {
final completer = Completer<bool>(); final completer = Completer<bool>();
// 设置超时 // 设置超时
final timeout = Timer(const Duration(seconds: 10), () { Timer? timeout;
timeout = Timer(const Duration(seconds: 10), () {
if (!completer.isCompleted) { if (!completer.isCompleted) {
Logger.error('火山TTS会话开始超时');
completer.complete(false); completer.complete(false);
_handleError(Exception('会话开始超时'));
// 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常
try {
_eventController.add({
'eventType': 'sessionFailed',
'error': '会话开始超时'
});
} catch (e) {
Logger.error('处理TTS会话超时事件失败: $e');
}
} }
timeout = null;
}); });
// 监听会话事件 // 监听会话事件
final subscription = eventStream.listen((event) { final subscription = eventStream.listen((event) {
if (event['eventType'] == 'sessionStarted') { if (event['eventType'] == 'sessionStarted') {
if (!completer.isCompleted) { if (!completer.isCompleted) {
Logger.info('火山TTS会话开始成功');
completer.complete(true); completer.complete(true);
timeout.cancel(); timeout?.cancel();
timeout = null;
} }
} else if (event['eventType'] == 'sessionFailed') { } else if (event['eventType'] == 'sessionFailed') {
if (!completer.isCompleted) { if (!completer.isCompleted) {
Logger.error('火山TTS会话开始失败: ${event['error']}');
completer.complete(false); completer.complete(false);
timeout.cancel(); timeout?.cancel();
timeout = null;
} }
} }
}); });
@ -449,29 +506,38 @@ class VolcanoTtsApiService extends GetxController {
subscription.cancel(); subscription.cancel();
if (result) { if (result) {
_isSessionActive = true; Logger.info('火山TTS会话已完全建立');
} }
return result; return result;
} catch (e) { } catch (e) {
_handleError(e); // 只记录错误,不尝试恢复
Logger.error('开始TTS会话失败: $e');
return false; return false;
} }
} }
/// 合成文本 /// 合成文本
Future<bool> synthesize(String text, String speaker) async { Future<bool> synthesize(String text, String speaker) async {
if (text.isEmpty) {
Logger.warning('合成文本为空,跳过合成');
return false;
}
try {
// 确保已连接
if (!_isConnected) { if (!_isConnected) {
final connected = await connect(); Logger.error('TTS未连接,无法合成文本');
if (!connected) return false; return false;
} }
if (!_isSessionActive) { // 确保会话已开始
final sessionStarted = await startSession(speaker); if (_sessionId == null) {
if (!sessionStarted) return false; Logger.error('TTS会话未开始,请先调用startSession');
return false;
} }
try { // 设置合成状态
isSynthesizing.value = true; isSynthesizing.value = true;
// 发送合成请求 // 发送合成请求
@ -485,11 +551,16 @@ class VolcanoTtsApiService extends GetxController {
} }
/// 结束会话 /// 结束会话
///
/// 结束当前TTS会话,释放相关资源
/// 用户应在使用完毕后调用此方法
///
/// 返回会话是否成功结束
Future<bool> endSession() async { Future<bool> endSession() async {
if (!_isSessionActive) { if (_sessionId == null) {
return true; // 没有活跃会话 return true; // 没有活跃会话
} }
print('结束会话'); Logger.info('TTS结束会话');
try { try {
// 发送结束会话事件 // 发送结束会话事件
@ -519,7 +590,8 @@ class VolcanoTtsApiService extends GetxController {
final result = await completer.future; final result = await completer.future;
subscription.cancel(); subscription.cancel();
_isSessionActive = false; // 清除会话ID
_sessionId = null;
isSynthesizing.value = false; isSynthesizing.value = false;
return result; return result;
@ -531,15 +603,14 @@ class VolcanoTtsApiService extends GetxController {
/// 断开连接 /// 断开连接
Future<bool> disconnect() async { Future<bool> disconnect() async {
print('断开连接');
if (!_isConnected) { if (!_isConnected) {
return true; // 已经断开 return true; // 已经断开
} }
print('断开TTS连接');
try { try {
// 如果有活跃会话,先结束会话 // 如果有活跃会话,先结束会话
if (_isSessionActive) { if (_sessionId != null) {
await endSession(); await endSession();
} }
@ -575,7 +646,7 @@ class VolcanoTtsApiService extends GetxController {
_channel = null; _channel = null;
_isConnected = false; _isConnected = false;
_isSessionActive = false; _sessionId = null;
isConnected.value = false; isConnected.value = false;
isSynthesizing.value = false; isSynthesizing.value = false;
@ -605,7 +676,7 @@ class VolcanoTtsApiService extends GetxController {
// 处理事件 // 处理事件
final event = response['event'] as int?; final event = response['event'] as int?;
if (event != null) { if (event != null && !_isDisposed && !_eventController.isClosed) {
switch (event) { switch (event) {
case eventConnectionStarted: case eventConnectionStarted:
_eventController.add({'eventType': 'connectionStarted'}); _eventController.add({'eventType': 'connectionStarted'});
@ -640,7 +711,10 @@ class VolcanoTtsApiService extends GetxController {
if (payload != null && payload.isNotEmpty) { if (payload != null && payload.isNotEmpty) {
try { try {
_eventController.add({'eventType': 'ttsResponse'}); _eventController.add({'eventType': 'ttsResponse'});
if (!_isDisposed && !_audioDataController.isClosed) {
_audioDataController.add(payload); _audioDataController.add(payload);
}
// 将音频数据添加到流中 // 将音频数据添加到流中
_addAudioData(payload); _addAudioData(payload);
@ -751,12 +825,14 @@ class VolcanoTtsApiService extends GetxController {
} }
} catch (e) { } catch (e) {
// 直接报告错误,不尝试恢复 // 直接报告错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'playbackError', 'eventType': 'playbackError',
'error': '添加音频到播放列表失败: $e' 'error': '添加音频到播放列表失败: $e'
}); });
} }
} }
}
/// 开始播放收集到的音频数据 /// 开始播放收集到的音频数据
Future<void> _startPlayback() async { Future<void> _startPlayback() async {
@ -784,35 +860,47 @@ class VolcanoTtsApiService extends GetxController {
await _audioPlayer.play(); await _audioPlayer.play();
} catch (e) { } catch (e) {
// 直接报告错误,不尝试恢复 // 直接报告错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'playbackError', 'eventType': 'playbackError',
'error': '开始播放失败: $e' 'error': '开始播放失败: $e'
}); });
} }
} }
}
/// 处理错误 /// 处理错误
void _handleError(dynamic error) { void _handleError(dynamic error) {
try {
// 只记录错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'error', 'eventType': 'error',
'error': error.toString() 'error': error.toString()
}); });
}
if (kDebugMode) { Logger.error('火山语音TTS错误: $error');
print('火山语音WebSocket错误: $error'); } catch (e) {
// 如果在处理错误时发生异常,记录到日志但不再抛出异常
Logger.error('处理TTS错误时发生异常: $e,原始错误: $error');
} }
} }
/// 处理连接关闭 /// 处理连接关闭
void _handleDone() { void _handleDone() {
Logger.info('TTS WebSocket连接已关闭');
// 重置所有状态
_isConnected = false; _isConnected = false;
_isSessionActive = false; _sessionId = null;
isConnected.value = false; isConnected.value = false;
isSynthesizing.value = false; isSynthesizing.value = false;
// 通知监听者连接已断开
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({'eventType': 'disconnected'}); _eventController.add({'eventType': 'disconnected'});
}
} }
/// 解析响应 /// 解析响应
@ -1202,9 +1290,18 @@ class VolcanoTtsApiService extends GetxController {
/// 释放资源 /// 释放资源
@override @override
void onClose() async { void onClose() async {
_isDisposed = true;
await disconnect(); await disconnect();
_audioDataController.close();
_eventController.close(); // 关闭控制器
if (!_audioDataController.isClosed) {
await _audioDataController.close();
}
if (!_eventController.isClosed) {
await _eventController.close();
}
// 释放资源 // 释放资源
_audioPlayer.dispose(); _audioPlayer.dispose();
@ -1223,11 +1320,6 @@ class VolcanoTtsApiService extends GetxController {
// 合成文本 // 合成文本
final success = await synthesize(text, actualSpeaker); final success = await synthesize(text, actualSpeaker);
if (!success) {
if (kDebugMode) {
print('合成文本失败');
}
}
return success; return success;
} catch (e) { } catch (e) {
@ -1260,6 +1352,11 @@ class VolcanoTtsApiService extends GetxController {
return _isPlaying.value; return _isPlaying.value;
} }
/// 检查会话是否活跃
bool isSessionActive() {
return _sessionId != null;
}
/// 设置音量 /// 设置音量
Future<void> setVolume(double volume) async { Future<void> setVolume(double volume) async {
try { try {
@ -1290,11 +1387,13 @@ class VolcanoTtsApiService extends GetxController {
/// ///
/// [text] 要合成的文本 /// [text] 要合成的文本
/// [speaker] 发音人,如果为null则使用默认发音人 /// [speaker] 发音人,如果为null则使用默认发音人
/// [autoDisconnect] 合成完成后是否自动断开连接,默认为false
/// ///
/// 返回合成是否成功 /// 返回合成是否成功
Future<bool> speakSingle(String text, {String? speaker}) async { Future<bool> speakSingle(String text, {String? speaker}) async {
if (text.isEmpty) return false; if (text.isEmpty) {
Logger.warning('合成文本为空,跳过合成');
return false;
}
try { try {
// 使用默认发音人 // 使用默认发音人
@ -1302,27 +1401,37 @@ class VolcanoTtsApiService extends GetxController {
// 1. 确保连接 // 1. 确保连接
if (!_isConnected) { if (!_isConnected) {
Logger.info('TTS未连接,开始连接');
final connected = await connect(); final connected = await connect();
if (!connected) return false; if (!connected) {
Logger.error('TTS连接失败,无法合成文本');
return false;
}
} }
// 2. 确保会话已开始 // 2. 确保会话已开始
if (!_isSessionActive) { final bool needNewSession = _sessionId == null;
if (needNewSession) {
Logger.info('TTS会话未开始,开始会话');
final sessionStarted = await startSession(actualSpeaker); final sessionStarted = await startSession(actualSpeaker);
if (!sessionStarted) return false; if (!sessionStarted) {
Logger.error('TTS会话开始失败,无法合成文本');
return false;
}
} }
// 3. 合成文本 // 3. 合成文本
isSynthesizing.value = true; isSynthesizing.value = true;
final success = await synthesize(text, actualSpeaker); final success = await synthesize(text, actualSpeaker);
// 不再自动断开连接,让调用者决定何时结束会话和断开连接 // 如果是我们开始的新会话,则结束会话
if (needNewSession) {
await endSession(); await endSession();
}
return success; return success;
} catch (e) { } catch (e) {
_handleError(e); _handleError(e);
return false; return false;
} }
} }

239
lib/data/services/volcano_tts_service.dart

@ -1,239 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:collection';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/services.dart';
/// 火山语音服务异常
class VolcanoTtsException implements Exception {
final String message;
final dynamic originalError;
VolcanoTtsException(this.message, [this.originalError]);
@override
String toString() => originalError != null
? '$message (原始错误: $originalError)'
: message;
}
/// 火山语音合成服务 (极简版)
///
/// 直接使用原生端 VolcanoTtsHelper 的功能,不处理状态回调
class VolcanoTtsService extends GetxService {
// 平台通道
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/volcano_tts');
// 配置参数
final String _appId;
final String _token;
final String _cluster;
final String _voiceType;
// 初始化状态
bool _isInitialized = false;
bool _isDisposed = false;
// 服务启用状态
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_TOKEN 和 VOLCANO_TTS_CLUSTER');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
await _initializeTtsEngine();
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
print('Stack trace: $stackTrace');
}
}
/// 初始化TTS引擎
Future<void> _initializeTtsEngine() async {
try {
print('开始初始化火山语音TTS引擎');
final result = await _channel.invokeMethod<bool>('initialize', {
'appId': _appId,
'token': _token,
'cluster': _cluster,
});
_isInitialized = result ?? false;
print('火山语音TTS引擎初始化${_isInitialized ? '成功' : '失败'}');
if (!_isInitialized) {
throw VolcanoTtsException('TTS引擎初始化失败');
}
// 设置默认音量和速度
await setVolume(_defaultVolume);
await setSpeed(_defaultSpeed);
} catch (e) {
print('初始化火山语音TTS引擎失败: $e');
_isInitialized = false;
throw VolcanoTtsException('初始化失败', e);
}
}
/// 合成并播放文本
///
/// [text] 要合成的文本
/// [voiceType] 可选的语音类型,如果提供,将覆盖默认语音类型
///
/// 返回一个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)');
}
// 直接调用原生方法合成并播放语音
final success = await _channel.invokeMethod<bool>('speakText', {
'text': text,
'voiceType': voiceType ?? _voiceType,
});
return success ?? false;
} catch (e) {
print('语音合成失败: $e');
return false;
}
}
/// 停止播放
Future<bool> stop() async {
if (!_isInitialized) {
print('TTS引擎尚未初始化');
return false;
}
try {
print('停止播放');
// 调用原生方法停止播放
final success = await _channel.invokeMethod<bool>('stopSpeaking');
print('已停止播放');
return success ?? false;
} catch (e) {
print('停止播放失败: $e');
return false;
}
}
/// 切换TTS启用状态
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
/// 设置播放速度
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 {
// 设置原生播放速度
final success = await _channel.invokeMethod<bool>('setSpeed', {
'speed': speed,
});
return success ?? false;
} catch (e) {
print('设置播放速度失败: $e');
return false;
}
}
/// 设置音量
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 {
// 设置原生音量
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 stop();
// 释放原生资源
try {
await _channel.invokeMethod('dispose');
} catch (e) {
print('释放原生TTS资源失败: $e');
}
}
@override
void onClose() async {
print('关闭TTS服务');
await _cleanupResources();
super.onClose();
}
/// 获取当前语音类型
String getCurrentVoiceType() {
return _voiceType;
}
/// 检查是否正在播放
bool get isPlaying => true;
/// 检查是否正在合成(简化版,直接返回true)
bool isActuallySynthesizing() {
return true;
}
}

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

@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart';
import '../models/message_model.dart'; import '../models/message_model.dart';
import '../../../data/services/volcano_ai_service.dart'; import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/volcano_tts_api_service.dart'; import '../../../data/services/volcano_tts_api_service.dart';
import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/volcano_asr_api_service.dart';
import '../../../data/providers/agent_provider.dart'; import '../../../data/providers/agent_provider.dart';
import '../../../data/models/agent.dart'; import '../../../data/models/agent.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
@ -14,7 +14,7 @@ class ChatController extends GetxController {
// 服务 // 服务
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>(); final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final AzureAsrService _asrService = Get.find<AzureAsrService>(); final VolcanoAsrApiService _asrService = Get.find<VolcanoAsrApiService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>(); final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
// 添加ScrollController用于控制列表滚动 // 添加ScrollController用于控制列表滚动
@ -41,6 +41,8 @@ class ChatController extends GetxController {
// 临时消息ID,用于语音识别中的消息 // 临时消息ID,用于语音识别中的消息
String? _tempRecognitionMessageId; String? _tempRecognitionMessageId;
// 临时消息的索引,用于更新临时消息
int _tempMessageIndex = -1;
// 流式响应控制 // 流式响应控制
StreamSubscription? _aiResponseSubscription; StreamSubscription? _aiResponseSubscription;
@ -86,7 +88,7 @@ class ChatController extends GetxController {
// 如果playVoiceOnEnter为true,自动激活语音识别 // 如果playVoiceOnEnter为true,自动激活语音识别
if (args['playVoiceOnEnter'] == true) { if (args['playVoiceOnEnter'] == true) {
// 延迟一段时间后激活语音识别,等待TTS播放完成
startVoiceRecognition(); startVoiceRecognition();
} }
@ -112,7 +114,7 @@ class ChatController extends GetxController {
// 停止语音识别 // 停止语音识别
if (isVoiceRecognitionActive.value) { if (isVoiceRecognitionActive.value) {
_asrService.stopContinuousRecognition(); _asrService.stopRecognition();
} }
super.onClose(); super.onClose();
@ -300,8 +302,6 @@ class ChatController extends GetxController {
// 开始语音识别 // 开始语音识别
Future<void> startVoiceRecognition() async { Future<void> startVoiceRecognition() async {
if (isVoiceRecognitionActive.value) return; if (isVoiceRecognitionActive.value) return;
// 每次收到新内容后滚动到底部 // 每次收到新内容后滚动到底部
@ -323,17 +323,19 @@ class ChatController extends GetxController {
try { try {
// 启动语音识别 // 启动语音识别
final success = await _asrService.startContinuousRecognition(); final success = await _asrService.startRecognition();
if (success) { if (success) {
isVoiceRecognitionActive.value = true; isVoiceRecognitionActive.value = true;
isListening.value = true; isListening.value = true;
recognizedText.value = ''; recognizedText.value = '';
// 不再添加"正在聆听..."的临时消息 // 重置临时消息索引
_tempMessageIndex = -1;
_tempRecognitionMessageId = null;
// 监听识别事件 // 监听识别事件
_recognitionSubscription = _asrService.recognitionStream?.listen( _recognitionSubscription = _asrService.recognitionStream.listen(
(event) { (event) {
switch (event.type) { switch (event.type) {
case RecognitionEventType.recognizing: case RecognitionEventType.recognizing:
@ -342,22 +344,18 @@ class ChatController extends GetxController {
recognizedText.value = event.text; recognizedText.value = event.text;
// 检查是否已经有临时消息 // 检查是否已经有临时消息
final tempMessageIndex = messages.indexWhere((m) => if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
m.role == 'user' &&
m.timestamp.millisecondsSinceEpoch >
DateTime.now().millisecondsSinceEpoch - 5000);
if (tempMessageIndex >= 0) {
// 更新已有的临时消息 // 更新已有的临时消息
messages[tempMessageIndex] = messages[_tempMessageIndex] =
messages[tempMessageIndex].copyWith( messages[_tempMessageIndex].copyWith(
content: event.text, content: event.text,
); );
messages.refresh();
// 每次收到新内容后滚动到底部 // 每次收到新内容后滚动到底部
Future.delayed( Future.delayed(
Duration(milliseconds: 100), _scrollToBottom); Duration(milliseconds: 100), _scrollToBottom);
// 使用refresh()方法刷新列表 // 使用refresh()方法刷新列表
messages.refresh();
} else { } else {
// 添加新的临时消息,但只有在有实际内容时 // 添加新的临时消息,但只有在有实际内容时
final tempMessage = Message( final tempMessage = Message(
@ -365,7 +363,13 @@ class ChatController extends GetxController {
content: event.text, content: event.text,
timestamp: DateTime.now(), timestamp: DateTime.now(),
); );
// 不使用ID,直接使用索引跟踪临时消息
messages.add(tempMessage); messages.add(tempMessage);
_tempMessageIndex = messages.length - 1;
messages.refresh();
// 滚动到底部显示新消息
Future.delayed(Duration(milliseconds: 100), _scrollToBottom);
} }
} }
break; break;
@ -375,58 +379,67 @@ class ChatController extends GetxController {
recognizedText.value = event.text; recognizedText.value = event.text;
// 查找并移除临时消息 // 查找并移除临时消息
final tempMessageIndex = messages.indexWhere((m) => if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
m.role == 'user' && messages.removeAt(_tempMessageIndex);
m.timestamp.millisecondsSinceEpoch >
DateTime.now().millisecondsSinceEpoch - 5000);
if (tempMessageIndex >= 0) {
messages.removeAt(tempMessageIndex);
} }
// 重置临时消息索引
_tempMessageIndex = -1;
_tempRecognitionMessageId = null;
// 发送识别到的文本 // 发送识别到的文本
sendMessage(event.text); sendMessage(event.text);
// 重置识别文本 // 重置识别文本,但不关闭语音识别框
recognizedText.value = ''; recognizedText.value = '';
} }
break; break;
case RecognitionEventType.sessionStopped: case RecognitionEventType.error:
case RecognitionEventType.canceled: // 错误处理,关闭语音识别框,不尝试重新启动
// 会话结束 Logger.error('语音识别错误: ${event.error}');
isVoiceRecognitionActive.value = false;
isListening.value = false;
// 查找并移除临时消息 // 查找并移除临时消息
final tempMessageIndex = messages.indexWhere((m) => if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
m.role == 'user' && if (messages[_tempMessageIndex].content.trim().isEmpty) {
m.timestamp.millisecondsSinceEpoch > messages.removeAt(_tempMessageIndex);
DateTime.now().millisecondsSinceEpoch - 5000); }
if (tempMessageIndex >= 0 &&
messages[tempMessageIndex].content.trim().isEmpty) {
messages.removeAt(tempMessageIndex);
} }
// 重置临时消息索引
_tempMessageIndex = -1;
_tempRecognitionMessageId = null;
// 停止语音识别
isVoiceRecognitionActive.value = false;
isListening.value = false;
_asrService.stopRecognition();
break; break;
default: default:
// 不做任何处理,保持语音识别持续运行
break; break;
} }
}, },
onError: (error) { onError: (error) {
// 错误处理,关闭语音识别框,不尝试重新启动
Logger.error('语音识别错误', error); Logger.error('语音识别错误', error);
isVoiceRecognitionActive.value = false;
isListening.value = false;
// 查找并移除临时消息 // 查找并移除临时消息
final tempMessageIndex = messages.indexWhere((m) => if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
m.role == 'user' && if (messages[_tempMessageIndex].content.trim().isEmpty) {
m.timestamp.millisecondsSinceEpoch > messages.removeAt(_tempMessageIndex);
DateTime.now().millisecondsSinceEpoch - 5000);
if (tempMessageIndex >= 0 &&
messages[tempMessageIndex].content.trim().isEmpty) {
messages.removeAt(tempMessageIndex);
} }
}
// 重置临时消息索引
_tempMessageIndex = -1;
_tempRecognitionMessageId = null;
// 停止语音识别
isVoiceRecognitionActive.value = false;
isListening.value = false;
_asrService.stopRecognition();
}, },
); );
} else { } else {
@ -444,7 +457,7 @@ class ChatController extends GetxController {
if (!isVoiceRecognitionActive.value) return; if (!isVoiceRecognitionActive.value) return;
try { try {
await _asrService.stopContinuousRecognition(); await _asrService.stopRecognition();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
@ -453,15 +466,15 @@ class ChatController extends GetxController {
recognizedText.value = ''; recognizedText.value = '';
// 查找并移除临时消息 // 查找并移除临时消息
final tempMessageIndex = messages.indexWhere((m) => if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
m.role == 'user' && if (messages[_tempMessageIndex].content.trim().isEmpty) {
m.timestamp.millisecondsSinceEpoch > messages.removeAt(_tempMessageIndex);
DateTime.now().millisecondsSinceEpoch - 5000);
if (tempMessageIndex >= 0 &&
messages[tempMessageIndex].content.trim().isEmpty) {
messages.removeAt(tempMessageIndex);
} }
}
// 重置临时消息索引和ID
_tempMessageIndex = -1;
_tempRecognitionMessageId = null;
} catch (e) { } catch (e) {
Logger.error('停止语音识别失败', e); Logger.error('停止语音识别失败', e);
} }

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

@ -365,9 +365,9 @@ class ChatView extends GetView<ChatController> {
children: List.generate( children: List.generate(
barCount, barCount,
(index) { (index) {
// 创建动态高度效果 // 创建动态高度效果,确保高度始终为正值
double height = isRec double height = isRec
? _getWaveHeight(index, isSpeaking, barCount).clamp(1.h, 22.5.h) ? _getWaveHeight(index, isSpeaking, barCount).clamp(2.h, 22.5.h)
: 2.h; : 2.h;
// 动画持续时间 // 动画持续时间
@ -401,7 +401,7 @@ class ChatView extends GetView<ChatController> {
duration: duration, duration: duration,
curve: isSpeaking ? Curves.elasticOut : Curves.easeInOut, curve: isSpeaking ? Curves.elasticOut : Curves.easeInOut,
width: isSpeaking ? 2.8.w : 2.5.w, width: isSpeaking ? 2.8.w : 2.5.w,
height: height, height: height > 0 ? height : 2.h, // 确保高度始终为正值
margin: EdgeInsets.symmetric(horizontal: 1.2.w), margin: EdgeInsets.symmetric(horizontal: 1.2.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: barColor, color: barColor,
@ -411,6 +411,7 @@ class ChatView extends GetView<ChatController> {
color: AppColors.primary.withOpacity(0.4), color: AppColors.primary.withOpacity(0.4),
blurRadius: 5, blurRadius: 5,
spreadRadius: 1.2, spreadRadius: 1.2,
offset: const Offset(0, 0), // 确保偏移量不为null
) )
] : null, ] : null,
), ),
@ -452,15 +453,19 @@ class ChatView extends GetView<ChatController> {
? 1.0 - (distanceFromMiddle / middleIndex) * 0.2 ? 1.0 - (distanceFromMiddle / middleIndex) * 0.2
: 1.0 - (distanceFromMiddle / middleIndex) * 0.5; : 1.0 - (distanceFromMiddle / middleIndex) * 0.5;
// 确保positionFactor不为负值
positionFactor = max(0.1, positionFactor);
double result;
if (isSpeaking) { if (isSpeaking) {
// 用户说话时的波形 // 用户说话时的波形
double timeFactor = 0.9 + sin(milliseconds / 600) * 0.5; double timeFactor = 0.9 + sin(milliseconds / 600) * 0.5;
// 增强中心突出效果 // 增强中心突出效果
double centerBoost = pow(positionFactor, 1.2) * 10.h; double centerBoost = pow(positionFactor, 1.2).toDouble() * 10.h;
// 增加随机性 // 增加随机性,但限制范围
double randomFactor = (Random().nextDouble() * 4 - 2) * 1.75.h; double randomFactor = (Random().nextDouble() * 3) * 1.75.h;
// 增强脉冲效果 // 增强脉冲效果
double pulseEffect = sin(milliseconds / 300) * 2.5.h * positionFactor; double pulseEffect = sin(milliseconds / 300) * 2.5.h * positionFactor;
@ -474,14 +479,17 @@ class ChatView extends GetView<ChatController> {
burstEffect = 5.h * positionFactor; burstEffect = 5.h * positionFactor;
} }
return baseHeight + (wave1 + wave2 + wave3 + wave4) * timeFactor + result = baseHeight + (wave1 + wave2 + wave3 + wave4) * timeFactor +
centerBoost + randomFactor + pulseEffect + voiceVariation + burstEffect; centerBoost + randomFactor + pulseEffect + voiceVariation + burstEffect;
} else { } else {
// 用户不说话时的波形 // 用户不说话时的波形
double idleWave = (wave1 + wave2) * 0.35 * positionFactor; double idleWave = (wave1 + wave2) * 0.35 * positionFactor;
double gentlePulse = sin(milliseconds / 800) * 1.h * positionFactor; double gentlePulse = sin(milliseconds / 800) * 1.h * positionFactor;
return baseHeight + idleWave + gentlePulse; result = baseHeight + idleWave + gentlePulse;
} }
// 确保返回值始终为正数
return max(2.h, result);
} catch (e) { } catch (e) {
// 发生错误时返回默认高度 // 发生错误时返回默认高度
return 2.h; return 2.h;

2
lib/modules/home/views/home_view.dart

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/home_controller.dart'; import '../controllers/home_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart'; import '../../../core/widgets/common_bottom_nav.dart';
import '../../../core/routes/app_routes.dart';
class HomeView extends GetView<HomeController> { class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key); const HomeView({Key? key}) : super(key: key);
@ -271,6 +272,7 @@ class HomeView extends GetView<HomeController> {
), ),
), ),
_buildActionButton(), _buildActionButton(),
SizedBox(height: 40.h), SizedBox(height: 40.h),
], ],
), ),

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

@ -71,14 +71,7 @@ class ProfileView extends GetView<ProfileController> {
// TODO: 实现关于页面 // TODO: 实现关于页面
}, },
), ),
const Divider(),
_buildMenuItem(
title: '检查更新'.tr,
icon: Icons.system_update,
onTap: () {
// TODO: 实现检查更新
},
),
const Divider(), const Divider(),
_buildMenuItem( _buildMenuItem(
title: '清除缓存'.tr, title: '清除缓存'.tr,
@ -126,6 +119,17 @@ class ProfileView extends GetView<ProfileController> {
Get.toNamed(Routes.TTS_TEST); Get.toNamed(Routes.TTS_TEST);
}, },
), ),
const Divider(),
_buildMenuItem(
title: '火山语音识别测试'.tr,
icon: Icons.mic_external_on,
subtitle: '测试火山语音ASR功能',
onTap: () {
Get.toNamed(Routes.VOLCANO_ASR_TEST);
},
),
const Divider(), const Divider(),
_buildMenuItem( _buildMenuItem(
title: 'Azure语音识别测试'.tr, title: 'Azure语音识别测试'.tr,
@ -135,6 +139,24 @@ class ProfileView extends GetView<ProfileController> {
Get.toNamed(Routes.ASR_TEST); Get.toNamed(Routes.ASR_TEST);
}, },
), ),
const Divider(),
_buildMenuItem(
title: '本地语音合成测试'.tr,
icon: Icons.speaker_phone,
subtitle: '测试Flutter TTS本地语音合成功能',
onTap: () {
Get.toNamed(Routes.FLUTTER_TTS_TEST);
},
),
const Divider(),
_buildMenuItem(
title: '本地语音识别测试'.tr,
icon: Icons.mic_none,
subtitle: '测试Flutter本地语音识别功能',
onTap: () {
Get.toNamed(Routes.FLUTTER_ASR_TEST);
},
),
], ],
), ),
bottomNavigationBar: const CommonBottomNav(currentIndex: 2), bottomNavigationBar: const CommonBottomNav(currentIndex: 2),

16
lib/modules/test/bindings/flutter_asr_test_binding.dart

@ -0,0 +1,16 @@
import 'package:get/get.dart';
import '../controllers/flutter_asr_test_controller.dart';
import '../../../data/services/flutter_asr_service.dart';
class FlutterAsrTestBinding extends Bindings {
@override
void dependencies() {
// 注册ASR服务(如果尚未注册)
if (!Get.isRegistered<FlutterAsrService>()) {
Get.lazyPut<FlutterAsrService>(() => FlutterAsrService());
}
// 注册控制器
Get.lazyPut<FlutterAsrTestController>(() => FlutterAsrTestController());
}
}

10
lib/modules/test/bindings/flutter_tts_test_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/flutter_tts_test_controller.dart';
/// Flutter TTS测试绑定
class FlutterTtsTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<FlutterTtsTestController>(() => FlutterTtsTestController());
}
}

10
lib/modules/test/bindings/volcano_asr_test_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/volcano_asr_test_controller.dart';
class VolcanoAsrTestBinding extends Bindings {
@override
void dependencies() {
// Use put instead of lazyPut to ensure the controller is created immediately
Get.put<VolcanoAsrTestController>(VolcanoAsrTestController());
}
}

154
lib/modules/test/controllers/flutter_asr_test_controller.dart

@ -0,0 +1,154 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../data/services/flutter_asr_service.dart';
import '../../../core/utils/logger.dart';
/// Flutter ASR测试控制器,演示如何使用FlutterAsrService
class FlutterAsrTestController extends GetxController {
// 获取ASR服务实例
final FlutterAsrService _asrService = Get.find<FlutterAsrService>();
// 当前ASR状态 - 使用Rx包装以便UI可以响应变化
final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value;
// 当前识别的文本 - 使用Rx包装以便UI可以响应变化
final RxString _recognizedText = ''.obs;
String get recognizedText => _recognizedText.value;
// 识别准确度 - 使用Rx包装以便UI可以响应变化
final RxDouble _confidence = 0.0.obs;
double get confidence => _confidence.value;
// 是否正在监听 - 计算属性
bool get isListening => asrState == AsrState.listening;
// 是否可用 - 使用Rx包装以便UI可以响应变化
final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value;
// 可用的语言列表 - 使用Rx包装以便UI可以响应变化
final RxList<dynamic> _locales = <dynamic>[].obs;
List<dynamic> get locales => _locales;
// 当前选择的语言 - 使用Rx包装以便UI可以响应变化
final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value;
// 历史记录 - 使用Rx包装以便UI可以响应变化
final RxList<String> _history = <String>[].obs;
List<String> get history => _history;
// 连续识别模式 - 使用Rx包装以便UI可以响应变化
final RxBool _continuousMode = false.obs;
bool get continuousMode => _continuousMode.value;
set continuousMode(bool value) => _continuousMode.value = value;
// 定时器,用于定期更新状态
late Timer _updateTimer;
// 识别结果订阅
StreamSubscription<String>? _resultSubscription;
@override
void onInit() {
super.onInit();
// 初始化状态
_updateState();
// 设置定时器,每300毫秒更新一次状态,确保UI能够及时反映服务状态变化
_updateTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
_updateState();
// 如果启用了连续模式,且当前不在监听状态,且ASR已初始化,则自动重新开始监听
if (_continuousMode.value &&
_asrState.value != AsrState.listening &&
_asrState.value != AsrState.notInitialized &&
_asrState.value != AsrState.error &&
_isAvailable.value) {
_restartListening();
}
});
// 订阅识别结果流
_resultSubscription = _asrService.recognitionResults.listen(_onFinalResult);
}
/// 更新控制器状态,从服务获取最新状态
void _updateState() {
_asrState.value = _asrService.asrState;
_recognizedText.value = _asrService.lastWords;
_confidence.value = _asrService.confidence;
_isAvailable.value = _asrService.isAvailable;
_locales.assignAll(_asrService.locales);
_currentLocale.value = _asrService.currentLocale;
}
/// 开始语音识别
Future<bool> startListening() async {
return await _asrService.startListening();
}
/// 停止语音识别
Future<void> stopListening() async {
await _asrService.stopListening();
}
/// 取消语音识别
Future<void> cancelListening() async {
await _asrService.cancelListening();
}
/// 切换连续识别模式
void toggleContinuousMode() {
_continuousMode.value = !_continuousMode.value;
Logger.info('连续识别模式: ${_continuousMode.value ? '开启' : '关闭'}');
// 如果开启连续模式且当前未在监听,则开始监听
if (_continuousMode.value && !isListening && _isAvailable.value) {
startListening();
}
}
/// 重新开始监听(用于连续模式)
Future<void> _restartListening() async {
// 添加小延迟,避免过于频繁的重启
await Future.delayed(const Duration(milliseconds: 300));
if (_continuousMode.value && !isListening && _isAvailable.value) {
Logger.info('连续模式: 重新开始监听');
await startListening();
}
}
/// 设置语言
void setLocale(String localeId) {
_asrService.setLocale(localeId);
_currentLocale.value = localeId;
}
/// 处理最终识别结果
void _onFinalResult(String result) {
if (result.isNotEmpty) {
// 添加到历史记录
_history.insert(0, result);
// 限制历史记录数量
if (_history.length > 10) {
_history.removeLast();
}
}
}
/// 清空历史记录
void clearHistory() {
_history.clear();
}
@override
void onClose() {
_updateTimer.cancel();
_resultSubscription?.cancel();
super.onClose();
}
}

163
lib/modules/test/controllers/flutter_tts_test_controller.dart

@ -0,0 +1,163 @@
import 'package:get/get.dart';
import '../../../data/services/flutter_tts_service.dart';
import '../../../core/utils/logger.dart';
import 'dart:async';
/// Flutter TTS测试控制器,演示如何使用FlutterTtsService
class FlutterTtsTestController extends GetxController {
// 获取TTS服务实例
final FlutterTtsService _ttsService = Get.find<FlutterTtsService>();
// 当前TTS状态 - 使用Rx包装以便UI可以响应变化
final Rx<TtsState> _ttsState = TtsState.stopped.obs;
TtsState get ttsState => _ttsState.value;
// 当前正在播放的文本 - 使用Rx包装以便UI可以响应变化
final RxString _currentText = ''.obs;
String get currentText => _currentText.value;
// 是否正在播放 - 计算属性
bool get isPlaying => ttsState == TtsState.playing;
// 队列中的文本 - 使用Rx包装以便UI可以响应变化
final RxList<String> _textQueue = <String>[].obs;
List<String> get textQueue => _textQueue;
// 可用的语音列表 - 使用Rx包装以便UI可以响应变化
final RxList<Map<String, String>> _voices = <Map<String, String>>[].obs;
List<Map<String, String>> get voices => _voices;
// 当前选择的语音 - 使用Rx包装以便UI可以响应变化
final RxString _currentVoice = ''.obs;
String get currentVoice => _currentVoice.value;
// 定时器,用于定期更新状态
late Timer _updateTimer;
@override
void onInit() {
super.onInit();
// 初始化状态
_updateState();
// 设置定时器,每300毫秒更新一次状态,确保UI能够及时反映服务状态变化
_updateTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
_updateState();
});
}
/// 更新控制器状态,从服务获取最新状态
void _updateState() {
// 更新TTS状态
final serviceState = _ttsService.ttsState;
if (_ttsState.value != serviceState) {
_ttsState.value = serviceState;
}
// 更新当前播放文本
final serviceText = _ttsService.currentText;
if (_currentText.value != serviceText) {
_currentText.value = serviceText;
}
// 更新队列
final serviceQueue = _ttsService.textQueue;
if (!listEquals(_textQueue, serviceQueue)) {
_textQueue.value = List.from(serviceQueue);
}
// 更新可用语音列表
final serviceVoices = _ttsService.voices;
if (!listEquals(_voices, serviceVoices)) {
_voices.value = List.from(serviceVoices);
}
// 更新当前选择的语音
final serviceCurrentVoice = _ttsService.currentVoice;
if (_currentVoice.value != serviceCurrentVoice) {
_currentVoice.value = serviceCurrentVoice;
}
}
/// 比较两个列表是否相等
bool listEquals<T>(List<T> a, List<T> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
/// 添加文本到队列并开始播放(如果尚未开始)
Future<void> speak(String text) async {
if (text.isEmpty) {
Logger.warning('文本为空,无法播放');
return;
}
await _ttsService.speak(text);
_updateState();
}
/// 添加多条文本到队列并开始播放(如果尚未开始)
Future<void> speakTexts(List<String> texts) async {
if (texts.isEmpty) {
Logger.warning('文本列表为空,无法播放');
return;
}
await _ttsService.speakTexts(texts);
_updateState();
}
/// 停止播放并清空队列
Future<void> stop() async {
await _ttsService.stop();
_updateState();
}
/// 暂停播放
Future<void> pause() async {
await _ttsService.pause();
_updateState();
}
/// 设置语音
Future<void> setVoice(String voiceName) async {
await _ttsService.setVoice(voiceName);
_updateState();
}
/// 设置语言
Future<void> setLanguage(String language) async {
await _ttsService.setLanguage(language);
}
/// 设置语速
Future<void> setRate(double rate) async {
await _ttsService.setRate(rate);
}
/// 设置音量
Future<void> setVolume(double volume) async {
await _ttsService.setVolume(volume);
}
/// 设置音调
Future<void> setPitch(double pitch) async {
await _ttsService.setPitch(pitch);
}
/// 清空队列并停止播放
Future<void> clearQueue() async {
await _ttsService.clearQueue();
_updateState();
}
@override
void onClose() {
_updateTimer.cancel();
super.onClose();
}
}

218
lib/modules/test/controllers/volcano_asr_test_controller.dart

@ -0,0 +1,218 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../data/services/volcano_asr_api_service.dart';
import '../../../core/utils/logger.dart';
class VolcanoAsrTestController extends GetxController {
// Observable variables
final isListening = false.obs;
final errorMessage = ''.obs;
final recognitionResults = <String>[].obs;
final isContinuous = false.obs;
final connectionStatus = 'Disconnected'.obs;
// 当前识别中的文本
final currentRecognizingText = ''.obs;
// 当前识别的索引
int _currentRecognitionIndex = -1;
// Services
late final VolcanoAsrApiService _asrService;
// Stream subscriptions
StreamSubscription? _recognitionSubscription;
@override
void onInit() {
super.onInit();
// Initialize the ASR service
_asrService = Get.find<VolcanoAsrApiService>();
// Subscribe to recognition stream
_recognitionSubscription = _asrService.recognitionStream.listen((event) {
switch (event.type) {
case RecognitionEventType.started:
// 开始新的识别
if (isContinuous.value) {
// 如果是连续模式,检查是否需要创建新条目
if (_currentRecognitionIndex == -1 || recognitionResults.isEmpty) {
// 如果没有当前识别索引或结果列表为空,添加新条目
recognitionResults.add('');
_currentRecognitionIndex = recognitionResults.length - 1;
}
} else {
// 非连续模式,清空结果并添加新条目
recognitionResults.clear();
recognitionResults.add('');
_currentRecognitionIndex = 0;
}
currentRecognizingText.value = '';
break;
case RecognitionEventType.recognizing:
// 实时识别结果
if (event.text.isNotEmpty) {
currentRecognizingText.value = event.text;
// 确保有一个当前条目可以更新
if (_currentRecognitionIndex == -1 || _currentRecognitionIndex >= recognitionResults.length) {
recognitionResults.add('');
_currentRecognitionIndex = recognitionResults.length - 1;
}
// 更新当前条目
recognitionResults[_currentRecognitionIndex] = event.text;
}
break;
case RecognitionEventType.finalResult:
// 最终识别结果
if (event.text.isNotEmpty) {
// 确保有一个当前条目可以更新
if (_currentRecognitionIndex == -1 || _currentRecognitionIndex >= recognitionResults.length) {
recognitionResults.add(event.text);
_currentRecognitionIndex = recognitionResults.length - 1;
} else {
// 更新当前条目为最终结果
recognitionResults[_currentRecognitionIndex] = event.text;
}
// 收到最终结果后,重置索引,为新句子做准备
_currentRecognitionIndex = -1;
// 清空当前识别文本
currentRecognizingText.value = '';
}
break;
case RecognitionEventType.error:
// 错误处理
if (event.error != null) {
errorMessage.value = event.error!;
// 如果有错误且正在识别,停止识别
if (isListening.value) {
isListening.value = false;
connectionStatus.value = 'Disconnected';
// 停止语音识别,不尝试重启
_asrService.stopRecognition();
}
}
break;
// 注意:处理其他事件类型
default:
// 其他事件类型不做特殊处理
break;
}
});
// Update connection status based on service state
ever(_asrService.isListening, (bool listening) {
isListening.value = listening;
connectionStatus.value = listening ? 'Connected' : 'Disconnected';
});
// Update error message from service
if (_asrService.errorMessage.isNotEmpty) {
errorMessage.value = _asrService.errorMessage;
}
}
@override
void onClose() {
// Cancel all subscriptions
_recognitionSubscription?.cancel();
// Disconnect from the ASR service if still connected
if (_asrService.isConnected) {
stopListening();
}
super.onClose();
}
// Start listening for speech
Future<void> startListening() async {
try {
// Clear previous error message
errorMessage.value = '';
// 如果是新的识别会话
if (!isListening.value) {
currentRecognizingText.value = '';
// 如果不是连续识别模式,则清空结果列表
if (!isContinuous.value) {
recognitionResults.clear();
_currentRecognitionIndex = -1;
}
}
// 使用麦克风录音功能
final success = await _asrService.startRecognition(
sampleRate: 16000,
bitsPerSample: 16,
channels: 1,
format: 'pcm',
codec: 'raw',
enablePunc: true,
);
if (success) {
isListening.value = true;
connectionStatus.value = 'Connected';
} else {
errorMessage.value = _asrService.errorMessage;
Logger.error('Failed to start recognition: ${_asrService.errorMessage}');
}
} catch (e) {
errorMessage.value = e.toString();
Logger.error('Error starting recognition: $e');
}
}
// 重新开始识别(用于连续识别模式)
Future<void> _restartRecognition() async {
// 短暂延迟,确保上一次识别完全结束
await Future.delayed(const Duration(milliseconds: 500));
// 如果仍处于监听状态且是连续识别模式,则开始新的识别
if (isListening.value && isContinuous.value) {
startListening();
}
}
// Stop listening for speech
void stopListening() {
try {
connectionStatus.value = 'Disconnecting...';
// 停止麦克风录音
_asrService.stopRecognition().then((_) {
isListening.value = false;
connectionStatus.value = 'Disconnected';
// 重置当前识别索引
_currentRecognitionIndex = -1;
}).catchError((e) {
errorMessage.value = e.toString();
Logger.error('Error stopping recognition: $e');
});
} catch (e) {
errorMessage.value = e.toString();
Logger.error('Error stopping recognition: $e');
}
}
// Clear recognition results
void clearResults() {
recognitionResults.clear();
currentRecognizingText.value = '';
_currentRecognitionIndex = -1;
}
}

476
lib/modules/test/views/flutter_asr_test_view.dart

@ -0,0 +1,476 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/flutter_asr_test_controller.dart';
import '../../../data/services/flutter_asr_service.dart';
/// Flutter ASR测试视图,演示如何使用FlutterAsrService
class FlutterAsrTestView extends GetView<FlutterAsrTestController> {
const FlutterAsrTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter ASR Test'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 语音识别控制卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Speech Recognition Controls',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 添加连续识别模式开关
Obx(() {
final isContinuous = controller.continuousMode;
return SwitchListTile(
title: const Text('连续识别模式'),
subtitle: Text(
isContinuous
? '已开启:识别将自动连续进行'
: '已关闭:识别完成后需手动重新开始',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
value: isContinuous,
onChanged: controller.isAvailable
? (value) => controller.toggleContinuousMode()
: null,
activeColor: Colors.blue,
dense: true,
);
}),
const Divider(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() {
final isListening = controller.isListening;
return ElevatedButton.icon(
onPressed: isListening || !controller.isAvailable
? null
: () => controller.startListening(),
icon: isListening
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.mic),
label: Text(isListening ? 'Listening...' : 'Start Listening'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
);
}),
Obx(() {
final isActive = controller.isListening;
return ElevatedButton.icon(
onPressed: isActive
? () => controller.stopListening()
: null,
icon: const Icon(Icons.stop),
label: const Text('Stop'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
);
}),
],
),
],
),
),
),
const SizedBox(height: 16),
// 语言选择卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Language Selection',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Obx(() {
final locales = controller.locales;
final currentLocale = controller.currentLocale;
if (locales.isEmpty) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'No languages available. Please check ASR settings on your device.',
style: TextStyle(
color: Colors.red,
fontStyle: FontStyle.italic,
),
),
);
}
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Select Language',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
value: currentLocale.isNotEmpty ? currentLocale : null,
items: locales.map((locale) {
final name = locale.name ?? 'Unknown';
final id = locale.localeId ?? '';
return DropdownMenuItem<String>(
value: id,
child: Text('$name (${id.isNotEmpty ? id : 'Unknown'})'),
);
}).toList(),
onChanged: controller.isListening
? null
: (value) {
if (value != null) {
controller.setLocale(value);
}
},
isExpanded: true,
hint: const Text('Select a language'),
);
}),
],
),
),
),
const SizedBox(height: 16),
// 当前识别结果卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Current Recognition',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Obx(() {
final text = controller.recognizedText;
final confidence = controller.confidence;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
width: double.infinity,
child: Text(
text.isEmpty ? 'Speak to see recognition results here' : text,
style: TextStyle(
fontSize: 16,
fontStyle: text.isEmpty ? FontStyle.italic : FontStyle.normal,
color: text.isEmpty ? Colors.grey : Colors.black,
),
),
),
if (confidence > 0)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'Confidence: ${(confidence * 100).toStringAsFixed(1)}%',
style: TextStyle(
color: _getConfidenceColor(confidence),
fontWeight: FontWeight.bold,
),
),
),
],
);
}),
],
),
),
),
const SizedBox(height: 16),
// 历史记录卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Recognition History',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Obx(() {
final hasHistory = controller.history.isNotEmpty;
return TextButton.icon(
onPressed: hasHistory ? () => controller.clearHistory() : null,
icon: const Icon(Icons.clear_all, size: 18),
label: const Text('Clear'),
style: TextButton.styleFrom(
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 8),
),
);
}),
],
),
const SizedBox(height: 8),
Obx(() {
final history = controller.history;
if (history.isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text(
'No recognition history yet',
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey,
),
),
),
);
}
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: history.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final text = history[index];
return ListTile(
title: Text(text),
leading: CircleAvatar(
backgroundColor: Colors.blue.shade100,
radius: 18,
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
dense: true,
);
},
);
}),
],
),
),
),
const SizedBox(height: 16),
// 状态显示卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Obx(() {
final state = controller.asrState;
final stateText = _getAsrStateText(state);
final stateColor = _getAsrStateColor(state);
final isContinuous = controller.continuousMode;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: stateColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: stateColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
_getAsrStateIcon(state),
color: stateColor,
),
const SizedBox(width: 8),
Text(
'Current Status: $stateText',
style: TextStyle(
color: stateColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
if (isContinuous)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Row(
children: const [
Icon(
Icons.repeat,
color: Colors.blue,
size: 16,
),
SizedBox(width: 8),
Text(
'连续识别模式已开启',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
),
],
);
}),
],
),
),
),
],
),
),
),
);
}
// 获取ASR状态文本
String _getAsrStateText(AsrState state) {
switch (state) {
case AsrState.notInitialized:
return 'Not Initialized';
case AsrState.initialized:
return 'Initialized';
case AsrState.listening:
return 'Listening';
case AsrState.notListening:
return 'Not Listening';
case AsrState.error:
return 'Error';
default:
return 'Unknown';
}
}
// 获取ASR状态颜色
Color _getAsrStateColor(AsrState state) {
switch (state) {
case AsrState.notInitialized:
return Colors.grey;
case AsrState.initialized:
return Colors.blue;
case AsrState.listening:
return Colors.green;
case AsrState.notListening:
return Colors.orange;
case AsrState.error:
return Colors.red;
default:
return Colors.grey;
}
}
// 获取ASR状态图标
IconData _getAsrStateIcon(AsrState state) {
switch (state) {
case AsrState.notInitialized:
return Icons.hourglass_empty;
case AsrState.initialized:
return Icons.check_circle;
case AsrState.listening:
return Icons.mic;
case AsrState.notListening:
return Icons.mic_off;
case AsrState.error:
return Icons.error;
default:
return Icons.help_outline;
}
}
// 获取置信度颜色
Color _getConfidenceColor(double confidence) {
if (confidence >= 0.8) {
return Colors.green;
} else if (confidence >= 0.6) {
return Colors.orange;
} else {
return Colors.red;
}
}
}

334
lib/modules/test/views/flutter_tts_test_view.dart

@ -0,0 +1,334 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/flutter_tts_test_controller.dart';
import '../../../data/services/flutter_tts_service.dart';
/// Flutter TTS测试视图,演示如何使用FlutterTtsService
class FlutterTtsTestView extends GetView<FlutterTtsTestController> {
const FlutterTtsTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter TTS Test'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 播放控制卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Playback Controls',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() {
final isPlaying = controller.isPlaying;
return ElevatedButton.icon(
onPressed: isPlaying
? null
: () => _playAllTexts(),
icon: isPlaying
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.playlist_play),
label: Text(isPlaying ? 'Playing...' : 'Play All'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
);
}),
Obx(() {
final isActive = controller.ttsState != TtsState.stopped;
return ElevatedButton.icon(
onPressed: isActive
? () => controller.stop()
: null,
icon: const Icon(Icons.stop),
label: const Text('Stop'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
);
}),
],
),
],
),
),
),
const SizedBox(height: 16),
// 语音选择卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Voice Selection',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Obx(() {
final voices = controller.voices;
final currentVoice = controller.currentVoice;
if (voices.isEmpty) {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'No voices available. Please check TTS settings on your device.',
style: TextStyle(
color: Colors.red,
fontStyle: FontStyle.italic,
),
),
);
}
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: 'Select Voice',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
value: currentVoice.isNotEmpty ? currentVoice : null,
items: voices.map((voice) {
final name = voice['name'] ?? 'Unknown';
final locale = voice['locale'] ?? '';
return DropdownMenuItem<String>(
value: name,
child: Text('$name ${locale.isNotEmpty ? '($locale)' : ''}'),
);
}).toList(),
onChanged: controller.isPlaying
? null
: (value) {
if (value != null) {
controller.setVoice(value);
}
},
isExpanded: true,
hint: const Text('Select a voice'),
);
}),
],
),
),
),
const SizedBox(height: 16),
// 播报内容列表卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Sample Texts',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 预设的播报内容
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _getSampleTexts().length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final text = _getSampleTexts()[index];
return Obx(() {
final isPlaying = controller.isPlaying;
final isCurrentPlaying = controller.isPlaying &&
controller.currentText == text;
return ListTile(
title: Text(
text,
style: TextStyle(
fontWeight: isCurrentPlaying ? FontWeight.bold : FontWeight.normal,
color: isCurrentPlaying ? Colors.blue : null,
),
),
leading: CircleAvatar(
backgroundColor: isCurrentPlaying
? Colors.blue
: Colors.blue.shade100,
radius: 18,
child: Icon(
isCurrentPlaying ? Icons.volume_up : Icons.play_arrow,
color: isCurrentPlaying ? Colors.white : Colors.blue,
size: 20,
),
),
onTap: isPlaying
? null
: () => controller.speak(text),
enabled: !isPlaying,
tileColor: isCurrentPlaying ? Colors.blue.withOpacity(0.1) : null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: isCurrentPlaying
? const BorderSide(color: Colors.blue, width: 1)
: BorderSide.none,
),
);
});
},
),
],
),
),
),
const SizedBox(height: 16),
// 状态显示卡片
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Obx(() {
final state = controller.ttsState;
final stateText = _getTtsStateText(state);
final stateColor = _getTtsStateColor(state);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: stateColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: stateColor.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
_getTtsStateIcon(state),
color: stateColor,
),
const SizedBox(width: 8),
Text(
'Current Status: $stateText',
style: TextStyle(
color: stateColor,
fontWeight: FontWeight.bold,
),
),
],
),
);
}),
],
),
),
),
],
),
),
),
);
}
// 连续播放所有文本
void _playAllTexts() {
// 使用控制器的连续播放功能播放所有文本
controller.speakTexts(_getSampleTexts());
}
// 获取TTS状态文本
String _getTtsStateText(TtsState state) {
switch (state) {
case TtsState.playing:
return 'Playing';
case TtsState.paused:
return 'Paused';
case TtsState.stopped:
return 'Stopped';
default:
return 'Unknown';
}
}
// 获取TTS状态颜色
Color _getTtsStateColor(TtsState state) {
switch (state) {
case TtsState.playing:
return Colors.green;
case TtsState.paused:
return Colors.orange;
case TtsState.stopped:
return Colors.grey;
default:
return Colors.grey;
}
}
// 获取TTS状态图标
IconData _getTtsStateIcon(TtsState state) {
switch (state) {
case TtsState.playing:
return Icons.volume_up;
case TtsState.paused:
return Icons.pause;
case TtsState.stopped:
return Icons.stop;
default:
return Icons.help_outline;
}
}
// 获取示例文本
List<String> _getSampleTexts() {
return [
'This is a Flutter TTS test. Please listen to the synthesis effect.',
'这是Flutter TTS测试,请听取合成效果。',
'Flutter TTS supports various voices and parameter adjustments.',
'Flutter TTS支持多种语音和参数调节,可以根据需要进行设置。',
];
}
}

234
lib/modules/test/views/volcano_asr_test_view.dart

@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/volcano_asr_test_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class VolcanoAsrTestView extends GetView<VolcanoAsrTestController> {
const VolcanoAsrTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('火山语音识别测试'),
centerTitle: true,
),
body: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 识别模式选择
Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'识别模式',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
Obx(() => Row(
children: [
Expanded(
child: RadioListTile<bool>(
title: const Text('一次性识别'),
value: false,
groupValue: controller.isContinuous.value,
onChanged: (value) {
if (value != null) {
controller.isContinuous.value = value;
}
},
),
),
Expanded(
child: RadioListTile<bool>(
title: const Text('连续识别'),
value: true,
groupValue: controller.isContinuous.value,
onChanged: (value) {
if (value != null) {
controller.isContinuous.value = value;
}
},
),
),
],
)),
],
),
),
),
SizedBox(height: 16.h),
// 录音控制
Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'录音控制',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16.h),
Center(
child: Obx(() => controller.isListening.value
? ElevatedButton.icon(
onPressed: () => controller.stopListening(),
icon: const Icon(Icons.stop),
label: const Text('停止录音'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 12.h,
),
),
)
: ElevatedButton.icon(
onPressed: () => controller.startListening(),
icon: const Icon(Icons.mic),
label: const Text('开始录音'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 24.w,
vertical: 12.h,
),
),
),
),
),
],
),
),
),
SizedBox(height: 16.h),
// 识别结果
Expanded(
child: Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'识别结果',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => controller.clearResults(),
tooltip: '清空结果',
),
],
),
SizedBox(height: 8.h),
Expanded(
child: Container(
padding: EdgeInsets.all(8.0.r),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4.0.r),
),
child: Obx(() => ListView.builder(
itemCount: controller.recognitionResults.length,
itemBuilder: (context, index) {
final result = controller.recognitionResults[index];
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.0.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${index + 1}. ',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Expanded(
child: Text(result),
),
],
),
);
},
)),
),
),
],
),
),
),
),
SizedBox(height: 16.h),
// 状态显示
Card(
child: Padding(
padding: EdgeInsets.all(16.0.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'状态',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
Obx(() => Text(
controller.connectionStatus.value,
style: TextStyle(
color: controller.connectionStatus.value == 'Connected'
? Colors.green
: (controller.connectionStatus.value == 'Disconnecting...'
? Colors.orange
: Colors.grey),
fontWeight: FontWeight.bold,
),
)),
SizedBox(height: 8.h),
Obx(() => controller.errorMessage.value.isNotEmpty
? Text(
'错误: ${controller.errorMessage.value}',
style: TextStyle(
color: Colors.red,
fontSize: 14.sp,
),
)
: const SizedBox.shrink()),
],
),
),
),
],
),
),
);
}
}

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

@ -1,8 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/volcano_tts_service.dart';
class TranslationBinding implements Bindings { class TranslationBinding implements Bindings {
@override @override

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

@ -6,7 +6,7 @@ import 'package:get_storage/get_storage.dart';
import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/volcano_tts_api_service.dart'; import '../../../data/services/volcano_tts_api_service.dart';
// import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
class TranslationController extends GetxController { class TranslationController extends GetxController {
// Services // Services
@ -172,11 +172,13 @@ class TranslationController extends GetxController {
} }
break; break;
case RecognitionEventType.error: case RecognitionEventType.error:
Get.snackbar( // 错误处理,停止语音识别
'Error', Logger.error('语音识别错误: ${event.error}');
'Recognition error: ${event.error}',
snackPosition: SnackPosition.BOTTOM, // 停止语音识别
); isRecognizing.value = false;
_asrService.stopContinuousRecognition();
break; break;
default: default:
break; break;

863
local_plugins/speech_to_text/android/src/main/kotlin/com/csdcorp/speech_to_text/SpeechToTextPlugin.kt

@ -0,0 +1,863 @@
package com.csdcorp.speech_to_text
import android.Manifest
import android.R.attr.data
import android.annotation.TargetApi
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothHeadset
import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.speech.*
import android.speech.SpeechRecognizer.createOnDeviceSpeechRecognizer
import android.speech.SpeechRecognizer.createSpeechRecognizer
import android.util.Log
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.PluginRegistry.Registrar
import org.json.JSONArray
import org.json.JSONObject
import java.util.*
import java.util.concurrent.Executors
enum class SpeechToTextErrors {
multipleRequests,
unimplemented,
noLanguageIntent,
recognizerNotAvailable,
missingOrInvalidArg,
missingContext,
unknown
}
enum class SpeechToTextCallbackMethods {
textRecognition,
notifyStatus,
notifyError,
soundLevelChange,
}
enum class SpeechToTextStatus {
listening,
notListening,
unavailable,
available,
done,
doneNoResult,
}
enum class ListenMode {
deviceDefault,
dictation,
search,
confirmation,
}
const val pluginChannelName = "plugin.csdcorp.com/speech_to_text"
@TargetApi(8)
/** SpeechToTextPlugin */
public class SpeechToTextPlugin :
MethodCallHandler, RecognitionListener,
PluginRegistry.RequestPermissionsResultListener, FlutterPlugin,
ActivityAware {
private var pluginContext: Context? = null
private var channel: MethodChannel? = null
private val minSdkForSpeechSupport = 21
private val brokenStopSdk = 29
private val minSdkForOnDeviceSpeechSupport = 31
private val speechToTextPermissionCode = 28521
private val missingConfidence: Double = -1.0
private var speechThresholdRms = 9
private val logTag = "SpeechToTextPlugin"
private var recognizerStops = true
private var currentActivity: Activity? = null
private var activeResult: Result? = null
private var initializedSuccessfully: Boolean = false
private var permissionToRecordAudio: Boolean = false
private var listening = false
private var debugLogging: Boolean = false
private var alwaysUseStop: Boolean = false
private var intentLookup: Boolean = false
private var noBluetoothOpt: Boolean = false // user-defined option
private var bluetoothDisabled = true // final bluetooth state (combines user-defined option and permissions)
private var resultSent: Boolean = false
private var lastOnDevice: Boolean = false
private var speechRecognizer: SpeechRecognizer? = null
private var recognizerIntent: Intent? = null
private var bluetoothAdapter: android.bluetooth.BluetoothAdapter? = null
private var pairedDevices: Set<android.bluetooth.BluetoothDevice>? = null
private var activeBluetooth: android.bluetooth.BluetoothDevice? = null
private var bluetoothHeadset: BluetoothHeadset? = null
private var previousRecognizerLang: String? = null
private var previousPartialResults: Boolean = true
private var previousListenMode: ListenMode = ListenMode.deviceDefault
private var lastFinalTime: Long = 0
private var speechStartTime: Long = 0
private var minRms: Float = 1000.0F
private var maxRms: Float = -100.0F
private val handler: Handler = Handler(Looper.getMainLooper())
private val defaultLanguageTag: String = Locale.getDefault().toLanguageTag()
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
onAttachedToEngine(flutterPluginBinding.getApplicationContext(), flutterPluginBinding.getBinaryMessenger());
}
// This static function is optional and equivalent to onAttachedToEngine. It supports the old
// pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
// plugin registration via this function while apps migrate to use the new Android APIs
// post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
//
// It is encouraged to share logic between onAttachedToEngine and registerWith to keep
// them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
// depending on the user's project. onAttachedToEngine or registerWith must both be defined
// in the same class.
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
val speechPlugin = SpeechToTextPlugin()
speechPlugin.currentActivity = registrar.activity()
registrar.addRequestPermissionsResultListener(speechPlugin)
speechPlugin.onAttachedToEngine(registrar.context(), registrar.messenger())
}
}
private fun onAttachedToEngine(applicationContext: Context, messenger: BinaryMessenger) {
this.pluginContext = applicationContext;
channel = MethodChannel(messenger, pluginChannelName)
channel?.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.pluginContext = null;
channel?.setMethodCallHandler(null)
channel = null
}
override fun onDetachedFromActivity() {
currentActivity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
currentActivity = binding.activity
binding.addRequestPermissionsResultListener(this)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
currentActivity = binding.activity
binding.addRequestPermissionsResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
currentActivity = null
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull rawrResult: Result) {
val result = ChannelResultWrapper(rawrResult)
try {
when (call.method) {
"has_permission" -> hasPermission(result)
"initialize" -> {
var dlog = call.argument<Boolean>("debugLogging")
if (null != dlog) {
debugLogging = dlog
}
var ausOpt = call.argument<Boolean>("alwaysUseStop")
if (null != ausOpt) {
alwaysUseStop = ausOpt == true
}
var iOpt = call.argument<Boolean>("intentLookup")
if (null != iOpt) {
intentLookup = iOpt == true
}
var noBtOpt = call.argument<Boolean>("noBluetooth")
if (null != noBtOpt) {
noBluetoothOpt = noBtOpt == true
}
initialize(result)
}
"listen" -> {
var localeId = call.argument<String>("localeId")
if (null == localeId) {
localeId = defaultLanguageTag
}
var partialResults = call.argument<Boolean>("partialResults")
if (null == partialResults) {
partialResults = true
}
var onDevice = call.argument<Boolean>("onDevice")
if ( null == onDevice ) {
onDevice = false
}
val listenModeIndex = call.argument<Int>("listenMode")
if ( null == listenModeIndex ) {
result.error(SpeechToTextErrors.missingOrInvalidArg.name,
"listenMode is required", null)
return
}
startListening(result, localeId, partialResults, listenModeIndex, onDevice )
}
"stop" -> stopListening(result)
"cancel" -> cancelListening(result)
"locales" -> locales(result)
else -> result.notImplemented()
}
} catch (exc: Exception) {
Log.e(logTag, "Unexpected exception", exc)
result.error(SpeechToTextErrors.unknown.name,
"Unexpected exception", exc.localizedMessage)
}
}
private fun hasPermission(result: Result) {
if (sdkVersionTooLow()) {
result.success(false)
return
}
debugLog("Start has_permission")
val localContext = pluginContext
if (localContext != null) {
val hasPerm = ContextCompat.checkSelfPermission(localContext,
Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
result.success(hasPerm)
}
}
private fun initialize(result: Result) {
if (sdkVersionTooLow()) {
result.success(false)
return
}
recognizerStops = Build.VERSION.SDK_INT != brokenStopSdk || alwaysUseStop
debugLog("Start initialize")
if (null != activeResult) {
result.error(SpeechToTextErrors.multipleRequests.name,
"Only one initialize at a time", null)
return
}
activeResult = result
initializeIfPermitted(pluginContext)
}
private fun sdkVersionTooLow(): Boolean {
if (Build.VERSION.SDK_INT < minSdkForSpeechSupport) {
return true;
}
return false;
}
private fun isNotInitialized(): Boolean {
return !initializedSuccessfully
}
private fun isListening(): Boolean {
return listening
}
private fun isNotListening(): Boolean {
return !listening
}
private fun startListening(result: Result, languageTag: String, partialResults: Boolean,
listenModeIndex: Int, onDevice: Boolean) {
if (sdkVersionTooLow() || isNotInitialized() || isListening()) {
result.success(false)
return
}
resultSent = false
createRecognizer(onDevice)
minRms = 1000.0F
maxRms = -100.0F
debugLog("Start listening")
var listenMode = ListenMode.deviceDefault
if ( listenModeIndex == ListenMode.dictation.ordinal) {
listenMode = ListenMode.dictation
}
optionallyStartBluetooth()
setupRecognizerIntent(languageTag, partialResults, listenMode, onDevice )
handler.post {
run {
speechRecognizer?.startListening(recognizerIntent)
}
}
speechStartTime = System.currentTimeMillis()
notifyListening(isRecording = true)
result.success(true)
debugLog("Start listening done")
}
private fun optionallyStartBluetooth() {
if ( bluetoothDisabled ) return
val context = pluginContext
val lbt = bluetoothAdapter
val lpaired = pairedDevices
val lhead = bluetoothHeadset
if (null != lbt && null!= lhead && null != lpaired && lbt.isEnabled) {
for (tryDevice in lpaired) {
//This loop tries to start VoiceRecognition mode on every paired device until it finds one that works(which will be the currently in use bluetooth headset)
if (lhead.startVoiceRecognition(tryDevice)) {
debugLog("Starting bluetooth voice recognition")
activeBluetooth = tryDevice;
break
}
}
}
}
private fun stopListening(result: Result) {
if (sdkVersionTooLow() || isNotInitialized() || isNotListening()) {
result.success(false)
return
}
debugLog("Stop listening")
handler.post {
run {
speechRecognizer?.stopListening()
}
}
if ( !recognizerStops ) {
destroyRecognizer()
}
notifyListening(isRecording = false)
result.success(true)
debugLog("Stop listening done")
}
private fun cancelListening(result: Result) {
if (sdkVersionTooLow() || isNotInitialized() || isNotListening()) {
result.success(false)
return
}
debugLog("Cancel listening")
handler.post {
run {
speechRecognizer?.cancel()
}
}
if ( !recognizerStops ) {
destroyRecognizer()
}
notifyListening(isRecording = false)
result.success(true)
debugLog("Cancel listening done")
}
private fun locales(result: Result) {
if (sdkVersionTooLow()) {
result.success(false)
return
}
var hasPermission = ContextCompat.checkSelfPermission(pluginContext!!,
Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
if (Build.VERSION.SDK_INT >= 33 && hasPermission) {
if ( SpeechRecognizer.isOnDeviceRecognitionAvailable(pluginContext!!)) {
// after much experimentation this was the only working iteration of the
// checkRecognitionSupport that works.
var recognizer = createOnDeviceSpeechRecognizer(pluginContext!!)
var recognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
// var recognizer = createSpeechRecognizer(pluginContext!!)
// var recognizerIntent = Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS)
recognizer?.checkRecognitionSupport(recognizerIntent, Executors.newSingleThreadExecutor(),
object : RecognitionSupportCallback {
override fun onSupportResult(recognitionSupport: RecognitionSupport) {
var details = LanguageDetailsChecker( result, debugLogging )
details.createResponse(recognitionSupport.supportedOnDeviceLanguages )
recognizer?.destroy()
}
override fun onError(error: Int) {
debugLog("error from checkRecognitionSupport: " + error)
recognizer?.destroy()
}
})
}
} else {
var detailsIntent = RecognizerIntent.getVoiceDetailsIntent(pluginContext)
if (null == detailsIntent) {
detailsIntent = Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS)
detailsIntent.setPackage("com.google.android.googlequicksearchbox")
}
pluginContext?.sendOrderedBroadcast(
detailsIntent, null, LanguageDetailsChecker(result, debugLogging),
null, Activity.RESULT_OK, null, null)
}
}
private fun notifyListening(isRecording: Boolean ) {
if ( listening == isRecording ) return;
listening = isRecording
val status = when (isRecording) {
true -> SpeechToTextStatus.listening.name
false -> SpeechToTextStatus.notListening.name
}
debugLog("Notify status:" + status)
channel?.invokeMethod(SpeechToTextCallbackMethods.notifyStatus.name, status)
if ( !isRecording ) {
val doneStatus = when( resultSent) {
false -> SpeechToTextStatus.doneNoResult.name
else -> SpeechToTextStatus.done.name
}
debugLog("Notify status:" + doneStatus )
optionallyStopBluetooth();
channel?.invokeMethod(SpeechToTextCallbackMethods.notifyStatus.name,
doneStatus )
}
}
private fun optionallyStopBluetooth() {
if ( bluetoothDisabled ) return
val lactive = activeBluetooth
val lbt = bluetoothHeadset
if (null != lactive && null != lbt ) {
debugLog("Stopping bluetooth voice recognition")
lbt.stopVoiceRecognition(lactive)
activeBluetooth = null
}
}
private fun updateResults(speechBundle: Bundle?, isFinal: Boolean) {
if (isDuplicateFinal( isFinal )) {
debugLog("Discarding duplicate final")
return
}
val userSaid = speechBundle?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
if (null != userSaid && userSaid.isNotEmpty()) {
val speechResult = JSONObject()
speechResult.put("finalResult", isFinal)
val confidence = speechBundle.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES)
val alternates = JSONArray()
for (resultIndex in 0..userSaid.size - 1) {
val speechWords = JSONObject()
speechWords.put("recognizedWords", userSaid[resultIndex])
if (null != confidence && confidence.size >= userSaid.size) {
speechWords.put("confidence", confidence[resultIndex])
} else {
speechWords.put("confidence", missingConfidence)
}
alternates.put(speechWords)
}
speechResult.put("alternates", alternates)
val jsonResult = speechResult.toString()
debugLog("Calling results callback")
resultSent = true
channel?.invokeMethod(SpeechToTextCallbackMethods.textRecognition.name,
jsonResult)
} else {
debugLog("Results null or empty")
}
}
private fun isDuplicateFinal( isFinal: Boolean ) : Boolean {
if ( !isFinal ) {
return false
}
val delta = System.currentTimeMillis() - lastFinalTime
lastFinalTime = System.currentTimeMillis()
return delta >= 0 && delta < 100
}
private fun initializeIfPermitted(context: Context?) {
val localContext = context
if (null == localContext) {
completeInitialize()
return
}
permissionToRecordAudio = ContextCompat.checkSelfPermission(localContext,
Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
val permissionToEnableBluetooth = ContextCompat.checkSelfPermission(localContext,
Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
bluetoothDisabled = !permissionToEnableBluetooth || noBluetoothOpt
debugLog("Checked permission")
if (!permissionToRecordAudio) {
val localActivity = currentActivity
if (null != localActivity) {
debugLog("Requesting permission")
var requiredPermissions = arrayOf(Manifest.permission.RECORD_AUDIO)
if ( !noBluetoothOpt ) {
requiredPermissions = requiredPermissions.plus(Manifest.permission.BLUETOOTH_CONNECT)
}
ActivityCompat.requestPermissions(localActivity, requiredPermissions, speechToTextPermissionCode)
} else {
debugLog("no permission, no activity, completing")
completeInitialize()
}
} else {
debugLog("has permission, completing")
completeInitialize()
}
debugLog("leaving initializeIfPermitted")
}
private fun completeInitialize() {
debugLog("completeInitialize")
if (permissionToRecordAudio) {
debugLog("Testing recognition availability")
val localContext = pluginContext
if (localContext != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!SpeechRecognizer.isRecognitionAvailable(localContext) && !SpeechRecognizer.isOnDeviceRecognitionAvailable(
localContext
)
) {
Log.e(logTag, "Speech recognition not available on this device")
activeResult?.error(
SpeechToTextErrors.recognizerNotAvailable.name,
"Speech recognition not available on this device", ""
)
activeResult = null
return
}
} else {
if (!SpeechRecognizer.isRecognitionAvailable(localContext)) {
Log.e(logTag, "Speech recognition not available on this device")
activeResult?.error(
SpeechToTextErrors.recognizerNotAvailable.name,
"Speech recognition not available on this device", ""
)
activeResult = null
return
}
}
setupBluetooth()
} else {
debugLog("null context during initialization")
activeResult?.success(false)
activeResult?.error(
SpeechToTextErrors.missingContext.name,
"context unexpectedly null, initialization failed", "")
activeResult = null
return
}
}
initializedSuccessfully = permissionToRecordAudio
debugLog("sending result")
activeResult?.success(permissionToRecordAudio)
debugLog("leaving complete")
activeResult = null
}
private fun setupBluetooth() {
if ( bluetoothDisabled ) return
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
pairedDevices = bluetoothAdapter?.getBondedDevices()
val mProfileListener: BluetoothProfile.ServiceListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = proxy as BluetoothHeadset
debugLog("Found a headset: " + bluetoothHeadset.toString())
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.HEADSET) {
debugLog("Clearing headset: ")
bluetoothHeadset = null
}
}
}
bluetoothAdapter?.getProfileProxy(pluginContext, mProfileListener, BluetoothProfile.HEADSET)
}
private fun Context.findComponentName(): ComponentName? {
val list: List<ResolveInfo> = packageManager.queryIntentServices(Intent(RecognitionService.SERVICE_INTERFACE), 0)
debugLog("RecognitionService, found: ${list.size}")
list.forEach() { it.serviceInfo?.let { it1 -> debugLog("RecognitionService: packageName: ${it1.packageName}, name: ${it1.name}") } }
return list.firstOrNull()?.serviceInfo?.let { ComponentName(it.packageName, it.name) }
}
private fun createRecognizer(onDevice: Boolean) {
if ( null != speechRecognizer && onDevice == lastOnDevice ) {
return
}
lastOnDevice = onDevice
speechRecognizer?.destroy()
speechRecognizer = null
handler.post {
run {
debugLog("Creating recognizer")
if (intentLookup) {
speechRecognizer = createSpeechRecognizer(
pluginContext,
pluginContext?.findComponentName()
).apply {
debugLog("Setting listener after intent lookup")
setRecognitionListener(this@SpeechToTextPlugin)
}
} else {
var supportsLocal = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && onDevice) {
supportsLocal = SpeechRecognizer.isOnDeviceRecognitionAvailable(pluginContext!!)
if (supportsLocal ) {
speechRecognizer = createOnDeviceSpeechRecognizer(pluginContext!!).apply {
debugLog("Setting on device listener")
setRecognitionListener(this@SpeechToTextPlugin)
}
}
}
if ( null == speechRecognizer) {
speechRecognizer = createSpeechRecognizer(pluginContext).apply {
debugLog("Setting default listener")
setRecognitionListener(this@SpeechToTextPlugin)
}
}
}
if (null == speechRecognizer) {
Log.e(logTag, "Speech recognizer null")
activeResult?.error(
SpeechToTextErrors.recognizerNotAvailable.name,
"Speech recognizer null", ""
)
activeResult = null
}
}
}
debugLog("before setup intent")
setupRecognizerIntent(defaultLanguageTag, true, ListenMode.deviceDefault, false )
debugLog("after setup intent")
}
private fun setupRecognizerIntent(languageTag: String, partialResults: Boolean, listenMode: ListenMode, onDevice: Boolean ) {
debugLog("setupRecognizerIntent")
if (previousRecognizerLang == null ||
previousRecognizerLang != languageTag ||
partialResults != previousPartialResults || previousListenMode != listenMode ) {
previousRecognizerLang = languageTag;
previousPartialResults = partialResults
previousListenMode = listenMode
handler.post {
run {
recognizerIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
debugLog("In RecognizerIntent apply")
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
debugLog("put model")
val localContext = pluginContext
if (null != localContext) {
putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
localContext.applicationInfo.packageName)
}
debugLog("put package")
putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, partialResults)
debugLog("put partial")
if (languageTag != Locale.getDefault().toLanguageTag()) {
putExtra(RecognizerIntent.EXTRA_LANGUAGE, languageTag);
debugLog("put languageTag")
}
if ( onDevice ) {
putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, onDevice );
}
putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,10)
}
}
}
}
}
private fun destroyRecognizer() {
handler.postDelayed( {
run {
debugLog("Recognizer destroy")
speechRecognizer?.destroy();
speechRecognizer = null;
}
}, 50 )
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean {
when (requestCode) {
speechToTextPermissionCode -> {
permissionToRecordAudio = grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
bluetoothDisabled = (grantResults.isEmpty() || grantResults.size == 1 ||
grantResults[1] != PackageManager.PERMISSION_GRANTED) ||
noBluetoothOpt
completeInitialize()
return true
}
}
return false
}
override fun onPartialResults(results: Bundle?) = updateResults(results, false)
override fun onResults(results: Bundle?) = updateResults(results, true)
override fun onEndOfSpeech() = notifyListening(isRecording = false)
override fun onError(errorCode: Int) {
val delta = System.currentTimeMillis() - speechStartTime
var errorReturn = errorCode
if ( SpeechRecognizer.ERROR_NO_MATCH == errorCode && maxRms < speechThresholdRms ) {
errorReturn = SpeechRecognizer.ERROR_SPEECH_TIMEOUT
}
debugLog( "Error $errorCode after start at $delta $minRms / $maxRms")
val errorMsg = when (errorReturn) {
SpeechRecognizer.ERROR_AUDIO -> "error_audio_error"
SpeechRecognizer.ERROR_CLIENT -> "error_client"
SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS -> "error_permission"
SpeechRecognizer.ERROR_NETWORK -> "error_network"
SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> "error_network_timeout"
SpeechRecognizer.ERROR_NO_MATCH -> "error_no_match"
SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> "error_busy"
SpeechRecognizer.ERROR_SERVER -> "error_server"
SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> "error_speech_timeout"
SpeechRecognizer.ERROR_LANGUAGE_NOT_SUPPORTED -> "error_language_not_supported"
SpeechRecognizer.ERROR_LANGUAGE_UNAVAILABLE -> "error_language_unavailable"
SpeechRecognizer.ERROR_SERVER_DISCONNECTED -> "error_server_disconnected"
SpeechRecognizer.ERROR_TOO_MANY_REQUESTS -> "error_too_many_requests"
else -> "error_unknown ($errorCode)"
}
sendError(errorMsg)
if ( isListening()) {
notifyListening(false)
}
}
private fun debugLog( msg: String ) {
if ( debugLogging ) {
Log.d( logTag, msg )
}
}
private fun sendError(errorMsg: String) {
val speechError = JSONObject()
speechError.put("errorMsg", errorMsg)
speechError.put("permanent", true)
handler.post {
run {
channel?.invokeMethod(SpeechToTextCallbackMethods.notifyError.name, speechError.toString())
}
}
}
override fun onRmsChanged(rmsdB: Float) {
if ( rmsdB < minRms ) {
minRms = rmsdB
}
if ( rmsdB > maxRms ) {
maxRms = rmsdB
}
debugLog("rmsDB $minRms / $maxRms")
handler.post {
run {
channel?.invokeMethod(SpeechToTextCallbackMethods.soundLevelChange.name, rmsdB)
}
}
}
override fun onReadyForSpeech(p0: Bundle?) {}
override fun onBufferReceived(p0: ByteArray?) {}
override fun onEvent(p0: Int, p1: Bundle?) {}
override fun onBeginningOfSpeech() {}
}
// See https://stackoverflow.com/questions/10538791/how-to-set-the-language-in-speech-recognition-on-android/10548680#10548680
class LanguageDetailsChecker(flutterResult: Result, logging: Boolean ) : BroadcastReceiver() {
private val logTag = "SpeechToTextPlugin"
private val result: Result = flutterResult
private val debugLogging: Boolean = logging
private var supportedLanguages: List<String>? = null
private var languagePreference: String? = null
override fun onReceive(context: Context, intent: Intent) {
debugLog( "Received extra language broadcast" )
val results = getResultExtras(true)
if (results.containsKey(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE)) {
languagePreference = results.getString(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE)
}
if (results.containsKey(RecognizerIntent.EXTRA_SUPPORTED_LANGUAGES)) {
debugLog( "Extra supported languages" )
supportedLanguages = results.getStringArrayList(
RecognizerIntent.EXTRA_SUPPORTED_LANGUAGES)
createResponse(supportedLanguages)
}
else {
debugLog( "No extra supported languages" )
createResponse( ArrayList<String>())
}
}
public fun createResponse(supportedLanguages: List<String>?) {
val currentLocale = Locale.getDefault()
val localeNames = ArrayList<String>()
localeNames.add(buildIdNameForLocale(currentLocale))
if (null != supportedLanguages) {
for (lang in supportedLanguages) {
if (currentLocale.toLanguageTag() == lang) {
continue
}
val locale = Locale.forLanguageTag(lang)
localeNames.add(buildIdNameForLocale(locale))
}
}
result.success(localeNames)
}
private fun buildIdNameForLocale(locale: Locale): String {
val name = locale.displayName.replace(':', ' ')
return "${locale.language}_${locale.country}:$name"
}
private fun debugLog( msg: String ) {
if ( debugLogging ) {
Log.d( logTag, msg )
}
}
}
private class ChannelResultWrapper(result: Result) : Result {
// Caller handler
val handler: Handler = Handler(Looper.getMainLooper())
val result: Result = result
// make sure to respond in the caller thread
override fun success(results: Any?) {
handler.post {
run {
result.success(results);
}
}
}
override fun error(errorCode: String, errorMessage: String?, data: Any?) {
handler.post {
run {
result.error(errorCode, errorMessage, data);
}
}
}
override fun notImplemented() {
handler.post {
run {
result.notImplemented();
}
}
}
}

32
macos/Podfile.lock

@ -6,8 +6,12 @@ PODS:
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- flutter_local_notifications (0.0.1): - flutter_local_notifications (0.0.1):
- FlutterMacOS - FlutterMacOS
- flutter_tts (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- just_audio (0.0.1): - just_audio (0.0.1):
- Flutter - Flutter
@ -17,9 +21,16 @@ PODS:
- FlutterMacOS - FlutterMacOS
- record_darwin (1.0.0): - record_darwin (1.0.0):
- FlutterMacOS - FlutterMacOS
- share_plus (0.0.1):
- FlutterMacOS
- speech_to_text (0.0.1):
- Flutter
- FlutterMacOS
- Try
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Try (2.1.1)
- window_size (0.0.2): - window_size (0.0.2):
- FlutterMacOS - FlutterMacOS
@ -27,14 +38,22 @@ DEPENDENCIES:
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`) - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/darwin`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
- flutter_tts (from `Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`) - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`)
SPEC REPOS:
trunk:
- Try
EXTERNAL SOURCES: EXTERNAL SOURCES:
audio_service: audio_service:
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin :path: Flutter/ephemeral/.symlinks/plugins/audio_service/darwin
@ -42,8 +61,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
device_info_plus: device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
flutter_local_notifications: flutter_local_notifications:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
flutter_tts:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_tts/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
just_audio: just_audio:
@ -52,6 +75,10 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
record_darwin: record_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos
share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
speech_to_text:
:path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin
sqflite_darwin: sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
window_size: window_size:
@ -61,12 +88,17 @@ SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e
device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041 device_info_plus: a56e6e74dbbd2bb92f2da12c64ddd4f67a749041
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7 flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7
flutter_tts: ae915565cc6948444b513acc8ee021993281e027
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9
PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073 PODFILE CHECKSUM: 8c4442624fc136b0afb378a2502958e01c05d073

5
pubspec.yaml

@ -22,10 +22,12 @@ dependencies:
markdown: ^7.1.1 markdown: ^7.1.1
permission_handler: ^11.4.0 permission_handler: ^11.4.0
audio_service: ^0.18.12 audio_service: ^0.18.12
web_socket_channel: ^2.4.4 web_socket_channel: ^3.0.2
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
synchronized: ^3.1.0 synchronized: ^3.1.0
intl: ^0.19.0 intl: ^0.19.0
crypto: ^3.0.3
flutter_tts: ^4.2.2
window_size: window_size:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git
@ -37,6 +39,7 @@ dependencies:
record: ^5.2.1 record: ^5.2.1
share_plus: ^10.0.0 share_plus: ^10.0.0
image_picker: ^1.1.2 image_picker: ^1.1.2
speech_to_text: ^7.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

3
windows/flutter/generated_plugin_registrant.cc

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <flutter_tts/flutter_tts_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h> #include <record_windows/record_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
@ -16,6 +17,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterTtsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterTtsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar( RecordWindowsPluginCApiRegisterWithRegistrar(

1
windows/flutter/generated_plugins.cmake

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows file_selector_windows
flutter_tts
permission_handler_windows permission_handler_windows
record_windows record_windows
share_plus share_plus

Loading…
Cancel
Save