Browse Source

service

weicu
wolfplus 2 years ago
parent
commit
daa9e055b4
  1. 48
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  2. 75
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  3. 566
      android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt
  4. 4
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt
  5. 9
      lib/core/bindings/initial_binding.dart
  6. 57
      lib/data/services/config_service.dart
  7. 70
      lib/data/services/voice_interaction_service.dart
  8. 5
      lib/main.dart
  9. 81
      lib/modules/home/controllers/home_controller.dart
  10. 62
      lib/modules/home/views/home_view.dart
  11. 7
      macos/Podfile.lock

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

@ -19,6 +19,14 @@ class AzureAsrHelper {
// 初始化 SDK // 初始化 SDK
fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean { fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean {
try { try {
Log.d(TAG, "开始初始化 Azure 语音服务: region=$serviceRegion, language=$language")
// 检查配置是否为空
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) {
Log.e(TAG, "Azure 配置信息不完整: subscriptionKey=${subscriptionKey.isEmpty()}, serviceRegion=${serviceRegion.isEmpty()}")
return false
}
this.subscriptionKey = subscriptionKey this.subscriptionKey = subscriptionKey
this.serviceRegion = serviceRegion this.serviceRegion = serviceRegion
this.currentLanguage = language this.currentLanguage = language
@ -31,13 +39,22 @@ class AzureAsrHelper {
speechConfig?.speechRecognitionLanguage = language speechConfig?.speechRecognitionLanguage = language
// 创建音频配置 // 创建音频配置
audioConfig = AudioConfig.fromDefaultMicrophoneInput() Log.d(TAG, "创建默认麦克风音频配置")
try {
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
Log.d(TAG, "成功创建音频配置")
} catch (e: Exception) {
Log.e(TAG, "创建音频配置失败: ${e.message}", e)
return false
}
// 创建识别器 // 创建识别器
Log.d(TAG, "创建语音识别器")
recognizer = SpeechRecognizer(speechConfig, audioConfig) recognizer = SpeechRecognizer(speechConfig, audioConfig)
Log.d(TAG, "Azure 语音服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "初始化失败: ${e.message}") Log.e(TAG, "初始化失败: ${e.message}", e)
return false return false
} }
} }
@ -70,26 +87,43 @@ class AzureAsrHelper {
// 开始连续语音识别 // 开始连续语音识别
fun startContinuousRecognition(language: String? = null, callback: ContinuousRecognizeCallback): Boolean { fun startContinuousRecognition(language: String? = null, callback: ContinuousRecognizeCallback): Boolean {
if (recognizer == null) { if (recognizer == null) {
Log.e(TAG, "SpeechRecognizer 未初始化")
callback.onError("SpeechRecognizer 未初始化") callback.onError("SpeechRecognizer 未初始化")
return false return false
} }
// 如果已经在进行连续识别,先停止 // 如果已经在进行连续识别,先停止
if (isContinuousRecognitionActive) { if (isContinuousRecognitionActive) {
Log.d(TAG, "已经在进行连续识别,先停止")
stopContinuousRecognition(callback) stopContinuousRecognition(callback)
} }
// 如果指定了新的语言,需要重新初始化 // 如果指定了新的语言,需要重新初始化
if (language != null && language != currentLanguage) { if (language != null && language != currentLanguage) {
Log.d(TAG, "语言变更,重新初始化: $language")
initialize(subscriptionKey, serviceRegion, language) initialize(subscriptionKey, serviceRegion, language)
} }
try { try {
// 先发送会话开始事件 // 先发送会话开始事件
Log.d(TAG, "准备开始连续语音识别")
callback.onSessionStarted() callback.onSessionStarted()
// 重新创建识别器 // 重新创建识别器
Log.d(TAG, "重新创建识别器")
recognizer?.close() recognizer?.close()
try {
// 重新创建音频配置
audioConfig?.close()
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
Log.d(TAG, "重新创建音频配置成功")
} catch (e: Exception) {
Log.e(TAG, "重新创建音频配置失败: ${e.message}", e)
callback.onError("创建音频配置失败: ${e.message}")
return false
}
recognizer = SpeechRecognizer(speechConfig, audioConfig) recognizer = SpeechRecognizer(speechConfig, audioConfig)
// 设置识别事件处理 // 设置识别事件处理
@ -98,7 +132,10 @@ class AzureAsrHelper {
recognizer.recognized.addEventListener( recognizer.recognized.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) { if (event.result.reason == ResultReason.RecognizedSpeech) {
Log.d(TAG, "识别结果: ${event.result.text}")
callback.onResult(event.result.text) callback.onResult(event.result.text)
} else {
Log.d(TAG, "识别结果原因: ${event.result.reason}")
} }
} }
) )
@ -107,6 +144,7 @@ class AzureAsrHelper {
recognizer.recognizing.addEventListener( recognizer.recognizing.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event -> EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) { if (event.result.reason == ResultReason.RecognizingSpeech) {
Log.d(TAG, "正在识别: ${event.result.text}")
callback.onRecognizing(event.result.text) callback.onRecognizing(event.result.text)
} }
} }
@ -115,6 +153,7 @@ class AzureAsrHelper {
// 会话开始事件 // 会话开始事件
recognizer.sessionStarted.addEventListener( recognizer.sessionStarted.addEventListener(
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
Log.d(TAG, "会话开始")
isContinuousRecognitionActive = true isContinuousRecognitionActive = true
callback.onSessionStarted() callback.onSessionStarted()
} }
@ -123,6 +162,7 @@ class AzureAsrHelper {
// 会话结束事件 // 会话结束事件
recognizer.sessionStopped.addEventListener( recognizer.sessionStopped.addEventListener(
EventHandler<SessionEventArgs> { _, _ -> EventHandler<SessionEventArgs> { _, _ ->
Log.d(TAG, "会话结束")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
callback.onSessionStopped() callback.onSessionStopped()
} }
@ -132,18 +172,22 @@ class AzureAsrHelper {
recognizer.canceled.addEventListener( recognizer.canceled.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event -> EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else "" val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else ""
Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails")
callback.onCanceled(event.reason.toString(), errorDetails) callback.onCanceled(event.reason.toString(), errorDetails)
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
} }
) )
// 开始连续识别 // 开始连续识别
Log.d(TAG, "开始连续识别")
recognizer.startContinuousRecognitionAsync().get() recognizer.startContinuousRecognitionAsync().get()
Log.d(TAG, "连续识别已启动")
isContinuousRecognitionActive = true isContinuousRecognitionActive = true
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "开始连续识别失败: ${e.message}", e)
callback.onError("开始连续识别失败: ${e.message}") callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
return false return false

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

@ -15,7 +15,7 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugins.GeneratedPluginRegistrant import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.ComponentName
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
@ -24,22 +24,27 @@ class MainActivity: FlutterActivity() {
private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events" private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events"
private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts" private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts"
private val VOICE_INTERACTION_CHANNEL = "com.deep_voice.voice_interaction" private val VOICE_INTERACTION_CHANNEL = "com.deep_voice.voice_interaction"
private val CONFIG_CHANNEL = "com.example.deep_voice/config"
private val TAG = "MainActivity" private val TAG = "MainActivity"
private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper private lateinit var azureTtsHelper: AzureTtsHelper
private var azureAsrEventSink: EventChannel.EventSink? = null private var azureAsrEventSink: EventChannel.EventSink? = null
private lateinit var sharedPreferences: SharedPreferences
// 使用伴生对象存储配置信息,使其在整个应用中可访问
companion object {
// Azure 语音服务配置
var azureSpeechKey: String = ""
var azureSpeechRegion: String = ""
var volcanoAiApiKey: String = ""
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
azureAsrHelper = AzureAsrHelper() azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper() azureTtsHelper = AzureTtsHelper()
sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
// 自动启动语音交互服务 // 不再自动启动服务,由用户通过 Flutter 界面手动启动
startVoiceInteractionService() Log.d(TAG, "MainActivity 已创建,但不会自动启动 VoiceInteractionService")
} }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
@ -281,6 +286,22 @@ class MainActivity: FlutterActivity() {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"startVoiceInteractionService" -> { "startVoiceInteractionService" -> {
// 获取配置参数
val azureSpeechKey = call.argument<String>("azure_speech_key") ?: ""
val azureSpeechRegion = call.argument<String>("azure_speech_region") ?: ""
val volcanoAiApiKey = call.argument<String>("volcano_ai_api_key") ?: ""
// 保存配置到静态变量
MainActivity.azureSpeechKey = azureSpeechKey
MainActivity.azureSpeechRegion = azureSpeechRegion
MainActivity.volcanoAiApiKey = volcanoAiApiKey
// 打印日志
Log.d(TAG, "已保存配置到静态变量:")
Log.d(TAG, "azure_speech_key: ${if (azureSpeechKey.isNotEmpty()) azureSpeechKey.take(5) + "..." else "空"}")
Log.d(TAG, "azure_speech_region: $azureSpeechRegion")
Log.d(TAG, "volcano_ai_api_key: ${if (volcanoAiApiKey.isNotEmpty()) volcanoAiApiKey.take(5) + "..." else "空"}")
startVoiceInteractionService() startVoiceInteractionService()
result.success(true) result.success(true)
} }
@ -296,35 +317,6 @@ class MainActivity: FlutterActivity() {
} }
} }
} }
// 设置 MethodChannel 处理程序
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CONFIG_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"setConfig" -> {
val azureSpeechKey = call.argument<String>("azure_speech_key") ?: ""
val azureSpeechRegion = call.argument<String>("azure_speech_region") ?: ""
val volcanoAiApiKey = call.argument<String>("volcano_ai_api_key") ?: ""
// 保存到 SharedPreferences
val editor = sharedPreferences.edit()
editor.putString("flutter.azure_speech_key", azureSpeechKey)
editor.putString("flutter.azure_speech_region", azureSpeechRegion)
editor.putString("flutter.volcano_ai_api_key", volcanoAiApiKey)
editor.apply()
// 打印日志
println("已保存配置到原生 SharedPreferences:")
println("azure_speech_key: $azureSpeechKey")
println("azure_speech_region: $azureSpeechRegion")
println("volcano_ai_api_key: $volcanoAiApiKey")
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
} }
// ASR 事件发送方法 // ASR 事件发送方法
@ -358,8 +350,21 @@ class MainActivity: FlutterActivity() {
* 启动语音交互服务 * 启动语音交互服务
*/ */
private fun startVoiceInteractionService() { private fun startVoiceInteractionService() {
Log.d(TAG, "准备启动语音交互服务")
val serviceIntent = Intent(this, VoiceInteractionService::class.java) val serviceIntent = Intent(this, VoiceInteractionService::class.java)
// 添加 ACTION_MEDIA_BUTTON 动作,确保服务能够接收媒体按钮事件
serviceIntent.action = Intent.ACTION_MEDIA_BUTTON
// 确保明确设置组件
serviceIntent.component = ComponentName(packageName, VoiceInteractionService::class.java.name)
// 输出配置信息
Log.d(TAG, "启动服务时的配置信息: subscriptionKey=${if (azureSpeechKey.isNotEmpty()) azureSpeechKey.take(5) + "..." else "空"}, serviceRegion=$azureSpeechRegion")
Log.d(TAG, "启动服务: $serviceIntent")
startService(serviceIntent) startService(serviceIntent)
Log.d(TAG, "语音交互服务启动完成")
} }
/** /**

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

@ -18,6 +18,8 @@ import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import android.content.ComponentName import android.content.ComponentName
import android.media.AudioManager import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONArray import org.json.JSONArray
@ -41,15 +43,50 @@ class VoiceInteractionService : Service() {
fun isServiceRunning(): Boolean { fun isServiceRunning(): Boolean {
return isRunning.get() return isRunning.get()
} }
// 语音识别超时时间(毫秒)
private const val RECOGNITION_TIMEOUT = 5000L
// 服务状态变化广播 Action
const val ACTION_SERVICE_STATUS_CHANGED = "com.example.deep_voice.ACTION_SERVICE_STATUS_CHANGED"
const val EXTRA_SERVICE_STATUS = "service_status"
const val STATUS_STARTED = "started"
const val STATUS_STOPPED = "stopped"
// 服务状态监听器
private var serviceStatusListener: ServiceStatusListener? = null
// 设置服务状态监听器
fun setServiceStatusListener(listener: ServiceStatusListener) {
serviceStatusListener = listener
}
// 移除服务状态监听器
fun removeServiceStatusListener() {
serviceStatusListener = null
}
// 服务状态监听接口
interface ServiceStatusListener {
fun onServiceStarted()
fun onServiceStopped()
}
} }
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
private var isRecording = false private var isActive = false // 合并 isRecording 和 isProcessing 为一个状态
private var isProcessing = false
private var lastKeyEventTime = 0L private var lastKeyEventTime = 0L
private var keyEventCount = 0 private var keyEventCount = 0
private lateinit var audioManager: AudioManager private lateinit var audioManager: AudioManager
// 超时处理
private val handler = Handler(Looper.getMainLooper())
private var timeoutRunnable: Runnable? = null
private var lastActivityTime = 0L
private var isTtsSpeaking = false
// Azure服务 // Azure服务
private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper private lateinit var azureTtsHelper: AzureTtsHelper
@ -67,77 +104,112 @@ class VoiceInteractionService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(TAG, "onCreate") Log.d(TAG, "onCreate - 语音交互服务正在创建")
isRunning.set(true) isRunning.set(true)
isActive = false // 确保初始状态为 false
isTtsSpeaking = false // 确保初始状态为 false
// 获取 AudioManager // 获取 AudioManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 1. 初始化Azure服务
initAzureServices()
// 2. 初始化火山AI服务 // 初始化服务
volcanoAIService = VolcanoAIService(applicationContext) initServices()
// 3. 初始化MediaSession // 初始化 MediaSession 和按键监听
initMediaSession() initMediaSession()
// 4. 启动前台通知(Foreground Service) // 启动前台通知(Foreground Service)
startForegroundServiceWithNotification() startForegroundServiceWithNotification()
// 5. 注册成为媒体按钮接收器 // 注册成为媒体按钮接收器
registerMediaButtonReceiver() registerMediaButtonReceiver()
// 6. 测试按键事件 // 发送服务已启动的广播
testKeyEvent() sendServiceStatusBroadcast(STATUS_STARTED)
// 通知监听器服务已启动
serviceStatusListener?.onServiceStarted()
Log.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...")
} }
/** /**
* 测试按键事件,模拟一个按键事件以验证处理逻辑 * 测试按键事件
*/ */
private fun testKeyEvent() { private fun testKeyEvent() {
Log.d(TAG, "测试按键事件处理") Log.d(TAG, "测试按键事件")
// 创建一个模拟的按键事件
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK) val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK)
// 创建一个媒体按钮意图
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(Intent.EXTRA_KEY_EVENT, keyEvent) putExtra(Intent.EXTRA_KEY_EVENT, keyEvent)
component = ComponentName(packageName, VoiceInteractionService::class.java.name) component = ComponentName(packageName, VoiceInteractionService::class.java.name)
} }
// MediaButtonReceiver.handleIntent 返回的是 KeyEvent 类型,而不是 Boolean // 发送给 MediaSession 处理
MediaButtonReceiver.handleIntent(mediaSession, mediaButtonIntent) MediaButtonReceiver.handleIntent(mediaSession, mediaButtonIntent)
Log.d(TAG, "测试按键事件已发送")
// 直接处理按键事件 // // 直接处理按键事件
handleKeyEvent(keyEvent) handleKeyEvent(keyEvent)
Log.d(TAG, "测试按键事件已发送")
}
/**
* 初始化所有服务
*/
private fun initServices() {
Log.d(TAG, "初始化所有服务")
// 1. 初始化Azure服务
initAzureServices()
// 2. 初始化火山AI服务
volcanoAIService = VolcanoAIService(applicationContext)
} }
/** /**
* 处理按键事件 * 处理按键事件
*/ */
private fun handleKeyEvent(keyEvent: KeyEvent) { private fun handleKeyEvent(keyEvent: KeyEvent) {
Log.d(TAG, "处理按键事件开始: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
if (keyEvent.action == KeyEvent.ACTION_DOWN) { if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) { when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_HEADSETHOOK,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(TAG, "处理按键事件: ${keyEvent.keyCode}") Log.d(TAG, "处理媒体按键事件: ${keyEvent.keyCode}")
// 检测双击 // 检测双击
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键 val timeDiff = currentTime - lastKeyEventTime
Log.d(TAG, "按键时间差: $timeDiff ms (上次: $lastKeyEventTime, 当前: $currentTime)")
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++ keyEventCount++
Log.d(TAG, "连续按键计数: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击 if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,开始语音交互") Log.d(TAG, "检测到双击,开始语音交互")
handleDoubleClick() handleDoubleClick()
keyEventCount = 0 keyEventCount = 0
} }
} else { } else {
Log.d(TAG, "单击事件,重置计数")
keyEventCount = 0 keyEventCount = 0
} }
lastKeyEventTime = currentTime lastKeyEventTime = currentTime
} }
else -> {
Log.d(TAG, "未处理的按键代码: ${keyEvent.keyCode}")
}
} }
} else {
Log.d(TAG, "忽略非按下事件: ${keyEvent.action}")
} }
} }
@ -157,24 +229,30 @@ class VoiceInteractionService : Service() {
azureAsrHelper = AzureAsrHelper() azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper() azureTtsHelper = AzureTtsHelper()
// 从应用的SharedPreferences获取Azure配置 // 从 MainActivity 的静态变量获取 Azure 配置
val sharedPreferences = applicationContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) val subscriptionKey = MainActivity.azureSpeechKey
val subscriptionKey = sharedPreferences.getString("flutter.azure_speech_key", "") ?: "" val serviceRegion = MainActivity.azureSpeechRegion
val serviceRegion = sharedPreferences.getString("flutter.azure_speech_region", "") ?: ""
Log.d(TAG, "Azure配置信息: subscriptionKey=$subscriptionKey, serviceRegion=$serviceRegion") Log.d(TAG, "Azure配置信息: subscriptionKey=${if (subscriptionKey.isNotEmpty()) subscriptionKey.take(5) + "..." else "空"}, serviceRegion=${if (serviceRegion.isNotEmpty()) serviceRegion else "空"}")
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) { if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) {
// 初始化ASR // 初始化ASR
azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") val asrInitResult = azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
Log.d(TAG, "ASR初始化结果: $asrInitResult")
// 初始化TTS // 初始化TTS
azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN") val ttsInitResult = azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
Log.d(TAG, "TTS初始化结果: $ttsInitResult")
if (!asrInitResult || !ttsInitResult) {
Log.e(TAG, "Azure服务初始化失败,语音交互功能可能无法正常工作")
}
} else { } else {
Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务") Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务")
} }
} }
/** /**
* 初始化 MediaSession 以捕获蓝牙耳机按键 * 初始化 MediaSession 以捕获蓝牙耳机按键
*/ */
@ -202,6 +280,8 @@ class VoiceInteractionService : Service() {
// 监听回调 // 监听回调
mediaSession.setCallback(object : MediaSessionCompat.Callback() { mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
Log.d(TAG, "MediaSession.onMediaButtonEvent: $mediaButtonEvent")
// 根据 Android 版本使用不同的方式获取 KeyEvent // 根据 Android 版本使用不同的方式获取 KeyEvent
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
@ -213,7 +293,7 @@ class VoiceInteractionService : Service() {
Log.d(TAG, "收到媒体按钮事件: $keyEvent") Log.d(TAG, "收到媒体按钮事件: $keyEvent")
if (keyEvent != null) { if (keyEvent != null) {
Log.d(TAG, "按键代码: ${keyEvent.keyCode}, 动作: ${keyEvent.action}") Log.d(TAG, "按键详情: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}, 时间=${keyEvent.eventTime}")
if (keyEvent.action == KeyEvent.ACTION_DOWN) { if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) { when (keyEvent.keyCode) {
@ -225,25 +305,31 @@ class VoiceInteractionService : Service() {
// 检测双击 // 检测双击
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键 val timeDiff = currentTime - lastKeyEventTime
Log.d(TAG, "MediaSession 按键时间差: $timeDiff ms")
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++ keyEventCount++
Log.d(TAG, "MediaSession 连续按键计数: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击 if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,开始语音交互") Log.d(TAG, "MediaSession 检测到双击,开始语音交互")
// 重置处理状态,确保可以开始新的语音交互
isActive = false
handleDoubleClick() handleDoubleClick()
keyEventCount = 0 keyEventCount = 0
return true return true
} }
} else { } else {
Log.d(TAG, "MediaSession 单击事件,重置计数")
keyEventCount = 0 keyEventCount = 0
} }
lastKeyEventTime = currentTime lastKeyEventTime = currentTime
// 单击也处理 // 单击也处理
if (keyEventCount == 0) { Log.d(TAG, "MediaSession 处理单击事件")
Log.d(TAG, "检测到单击,开始语音交互") handleDoubleClick()
handleDoubleClick() return true
return true
}
} }
} }
} }
@ -273,15 +359,22 @@ class VoiceInteractionService : Service() {
* 处理耳机双击事件 * 处理耳机双击事件
*/ */
private fun handleDoubleClick() { private fun handleDoubleClick() {
if (isProcessing) { Log.d(TAG, "handleDoubleClick - isActive: $isActive")
Log.d(TAG, "正在处理中,忽略此次点击")
return // 无论当前状态如何,都先停止 TTS 播放
} stopCurrentTTS()
if (!isRecording) { // 立即播放提示音,不等待语音识别开始
playStartPrompt()
if (!isActive) {
Log.d(TAG, "开始新的语音交互")
startVoiceInteraction() startVoiceInteraction()
} else { } else {
stopVoiceInteraction() Log.d(TAG, "已经在活动中,只需更新活动时间并继续")
// 更新最后活动时间
updateLastActivityTime()
} }
} }
@ -289,68 +382,239 @@ class VoiceInteractionService : Service() {
* 开始语音交互 * 开始语音交互
*/ */
private fun startVoiceInteraction() { private fun startVoiceInteraction() {
if (isRecording || isProcessing) { Log.d(TAG, "startVoiceInteraction - 当前状态: isActive=$isActive")
if (isActive) {
Log.d(TAG, "已经在活动中,忽略此次请求")
return
}
// 检查录音权限
if (!checkRecordAudioPermission()) {
Log.e(TAG, "没有录音权限,无法开始语音交互")
playErrorPrompt("需要录音权限,请在设置中授予权限")
// 权限处理由 Flutter 端负责,不再尝试通知
return return
} }
isRecording = true isActive = true
isProcessing = true
// 播放提示音 // 更新最后活动时间
playStartPrompt() updateLastActivityTime()
// 启动超时检查
startTimeoutCheck()
Log.d(TAG, "设置状态: isActive=true")
// 不再在这里播放提示音,而是在 handleDoubleClick 中播放
// 开始录音识别 // 开始录音识别
azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback { try {
override fun onRecognizing(recognizing: String) { Log.d(TAG, "开始调用 Azure 语音识别")
// 实时识别结果,可以记录日志 azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback {
Log.d(TAG, "正在识别: $recognizing") override fun onRecognizing(recognizing: String) {
} // 实时识别结果,可以记录日志
Log.d(TAG, "正在识别: $recognizing")
override fun onResult(result: String) {
// 最终识别结果 // 如果检测到用户在讲话,停止当前的 TTS 播放
Log.d(TAG, "语音识别结果: $result") if (recognizing.isNotEmpty()) {
// 停止 TTS 播放
stopCurrentTTS()
// 更新最后活动时间
updateLastActivityTime()
}
}
// 停止录音 override fun onResult(result: String) {
isRecording = false // 最终识别结果
Log.d(TAG, "语音识别结果: $result")
// 更新最后活动时间
// 如果有识别结果,则发送到VolcanoAI处理,但不停止录音
if (result.isNotEmpty()) {
Log.d(TAG, "有识别结果,开始处理")
updateLastActivityTime()
processWithVolcanoAI(result)
// 不设置 isActive = false,保持活动状态
} else {
// Log.d(TAG, "没有识别结果,但继续保持活动状态")
// 即使没有识别结果,也不停止活动状态
}
}
// 如果有识别结果,则发送到VolcanoAI处理 override fun onSessionStarted() {
if (result.isNotEmpty()) { Log.d(TAG, "语音识别会话开始")
processWithVolcanoAI(result)
} else { // 更新最后活动时间
isProcessing = false updateLastActivityTime()
} }
}
override fun onSessionStopped() {
Log.d(TAG, "语音识别会话结束")
// 确保状态被重置
if (isActive) {
isActive = false
Log.d(TAG, "会话结束时设置状态: isActive=false")
}
// 停止超时检查
stopTimeoutCheck()
}
override fun onCanceled(reason: String, errorDetails: String) {
Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails")
isActive = false
Log.d(TAG, "取消时设置状态: isActive=false")
// 停止超时检查
stopTimeoutCheck()
}
override fun onError(error: String) {
Log.e(TAG, "语音识别错误: $error")
isActive = false
Log.d(TAG, "错误时设置状态: isActive=false")
// 停止超时检查
stopTimeoutCheck()
// 播放错误提示音
playErrorPrompt("语音识别出错")
}
})
} catch (e: Exception) {
Log.e(TAG, "启动语音识别异常: ${e.message}", e)
isActive = false
Log.d(TAG, "异常时设置状态: isActive=false")
override fun onSessionStarted() { // 停止超时检查
Log.d(TAG, "语音识别会话开始") stopTimeoutCheck()
}
override fun onSessionStopped() { // 播放错误提示音
Log.d(TAG, "语音识别会话结束") playErrorPrompt("启动语音识别失败")
} }
}
/**
* 更新最后活动时间
*/
private fun updateLastActivityTime() {
lastActivityTime = System.currentTimeMillis()
Log.d(TAG, "更新最后活动时间: $lastActivityTime")
}
/**
* 启动超时检查
*/
private fun startTimeoutCheck() {
// 先停止之前的超时检查
stopTimeoutCheck()
// 创建新的超时检查
timeoutRunnable = Runnable {
val currentTime = System.currentTimeMillis()
val elapsedTime = currentTime - lastActivityTime
override fun onCanceled(reason: String, errorDetails: String) { Log.d(TAG, "超时检查 - 已经过去 $elapsedTime ms, 是否活动中: $isActive, 是否正在播放TTS: $isTtsSpeaking")
Log.e(TAG, "语音识别取消: $reason, 详情: $errorDetails")
isRecording = false
isProcessing = false
}
override fun onError(error: String) { if (isActive && !isTtsSpeaking &&elapsedTime >= RECOGNITION_TIMEOUT) {
Log.e(TAG, "语音识别错误: $error") // 超过30秒没有活动且没有TTS播放,暂停当前语音识别
isRecording = false Log.d(TAG, "超过 ${RECOGNITION_TIMEOUT/1000} 秒没有活动,暂停语音识别")
isProcessing = false
// 播放错误提示音 // 播放提示音
playErrorPrompt() playErrorPrompt("没有听到您说话,已暂停语音识别")
// 暂停录音,但保持服务活动状态
// 这样用户可以通过双击重新激活
// azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
// override fun onResult(result: String) {
// // 不处理结果
// }
// override fun onRecognizing(recognizing: String) {
// // 不处理中间结果
// }
// override fun onSessionStarted() {
// // 不处理会话开始事件
// }
// override fun onSessionStopped() {
// Log.d(TAG, "超时暂停:语音识别会话已停止")
// }
// override fun onCanceled(reason: String, errorDetails: String) {
// Log.d(TAG, "超时暂停:语音识别已取消: $reason")
// }
// override fun onError(error: String) {
// Log.e(TAG, "超时暂停:停止语音识别错误: $error")
// }
// })
// 不设置 isActive = false,保持服务活动状态
Log.d(TAG, "保持状态: isActive=true")
// 不停止超时检查,继续监控
handler.postDelayed(timeoutRunnable!!, 1000)
} else if (isActive) {
// 如果仍在活动中,继续检查
handler.postDelayed(timeoutRunnable!!, 1000) // 每秒检查一次
} }
}) }
// 开始检查
handler.postDelayed(timeoutRunnable!!, 1000)
Log.d(TAG, "启动超时检查")
}
/**
* 停止超时检查
*/
private fun stopTimeoutCheck() {
timeoutRunnable?.let {
handler.removeCallbacks(it)
timeoutRunnable = null
Log.d(TAG, "停止超时检查")
}
}
/**
* 停止当前的 TTS 播放
*/
private fun stopCurrentTTS() {
try {
if (isTtsSpeaking) {
Log.d(TAG, "停止当前 TTS 播放")
azureTtsHelper.stopSpeaking()
isTtsSpeaking = false
}
} catch (e: Exception) {
Log.e(TAG, "停止 TTS 播放异常: ${e.message}", e)
}
}
/**
* 检查录音权限
*/
private fun checkRecordAudioPermission(): Boolean {
val permission = android.Manifest.permission.RECORD_AUDIO
val result = applicationContext.checkCallingOrSelfPermission(permission)
val hasPermission = result == android.content.pm.PackageManager.PERMISSION_GRANTED
Log.d(TAG, "录音权限检查结果: $hasPermission")
return hasPermission
} }
/** /**
* 停止语音交互 * 停止语音交互
*/ */
private fun stopVoiceInteraction() { private fun stopVoiceInteraction() {
if (!isRecording) { if (!isActive) {
return return
} }
@ -380,37 +644,59 @@ class VoiceInteractionService : Service() {
Log.e(TAG, "停止语音识别错误: $error") Log.e(TAG, "停止语音识别错误: $error")
} }
}) })
isRecording = false isActive = false
} }
/** /**
* 播放开始提示音 * 播放开始提示音
*/ */
private fun playStartPrompt() { private fun playStartPrompt() {
azureTtsHelper.speakText("我在听", object : AzureTtsHelper.TTSCallback { Log.d(TAG, "开始播放提示音: '我在听'")
override fun onSuccess(message: String) { try {
Log.d(TAG, "提示音播放完成") isTtsSpeaking = true
} azureTtsHelper.speakText("我在!", object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
override fun onError(error: String) { Log.d(TAG, "提示音播放完成: $message")
Log.e(TAG, "提示音播放错误: $error") isTtsSpeaking = false
}
})
}
override fun onError(error: String) {
Log.e(TAG, "提示音播放错误: $error")
isTtsSpeaking = false
// 即使提示音播放失败,也继续进行语音识别
}
})
} catch (e: Exception) {
Log.e(TAG, "播放提示音异常: ${e.message}", e)
isTtsSpeaking = false
// 即使提示音播放失败,也继续进行语音识别
}
} }
/** /**
* 播放错误提示音 * 播放错误提示音
*/ */
private fun playErrorPrompt() { private fun playErrorPrompt(errorMessage: String = "抱歉,我没有听清") {
azureTtsHelper.speakText("抱歉,我没有听清", object : AzureTtsHelper.TTSCallback { Log.d(TAG, "开始播放错误提示音: '$errorMessage'")
override fun onSuccess(message: String) { try {
Log.d(TAG, "错误提示音播放完成") isTtsSpeaking = true
} azureTtsHelper.speakText(errorMessage, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
override fun onError(error: String) { Log.d(TAG, "错误提示音播放完成: $message")
Log.e(TAG, "错误提示音播放错误: $error") isTtsSpeaking = false
} }
})
override fun onError(error: String) {
Log.e(TAG, "错误提示音播放错误: $error")
isTtsSpeaking = false
}
})
} catch (e: Exception) {
Log.e(TAG, "播放错误提示音异常: ${e.message}", e)
isTtsSpeaking = false
}
} }
/** /**
@ -420,6 +706,7 @@ class VoiceInteractionService : Service() {
// 使用原生VolcanoAIService处理 // 使用原生VolcanoAIService处理
Thread { Thread {
try { try {
// 创建用户消息 // 创建用户消息
val messages = JSONArray().apply { val messages = JSONArray().apply {
put(volcanoAIService.createUserMessage(text)) put(volcanoAIService.createUserMessage(text))
@ -432,10 +719,10 @@ class VoiceInteractionService : Service() {
speakAIResponse(response) speakAIResponse(response)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "VolcanoAI处理错误: ${e.message}") Log.e(TAG, "VolcanoAI处理错误: ${e.message}")
isProcessing = false // 不设置 isActive = false,保持活动状态
// 播放错误提示音 // 播放错误提示音
playErrorPrompt() playErrorPrompt("AI处理出错")
} }
}.start() }.start()
} }
@ -444,15 +731,27 @@ class VoiceInteractionService : Service() {
* 播放AI回复 * 播放AI回复
*/ */
private fun speakAIResponse(text: String) { private fun speakAIResponse(text: String) {
Log.d(TAG, "开始播放AI回复: '${text.take(20)}...'")
isTtsSpeaking = true
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback { azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) { override fun onSuccess(message: String) {
Log.d(TAG, "AI回复播放完成") Log.d(TAG, "AI回复播放完成")
isProcessing = false isTtsSpeaking = false
// 更新最后活动时间
updateLastActivityTime()
// 不设置 isActive = false,保持活动状态
// 继续保持对话状态,等待用户下一次输入
Log.d(TAG, "继续保持对话状态,等待用户下一次输入")
} }
override fun onError(error: String) { override fun onError(error: String) {
Log.e(TAG, "AI回复播放错误: $error") Log.e(TAG, "AI回复播放错误: $error")
isProcessing = false isTtsSpeaking = false
// 即使出错,也不结束活动状态,继续保持对话
Log.d(TAG, "AI回复播放出错,但继续保持对话状态")
} }
}) })
} }
@ -461,6 +760,14 @@ class VoiceInteractionService : Service() {
* 启动一个"前台通知",保证此Service在后台不会被系统回收 * 启动一个"前台通知",保证此Service在后台不会被系统回收
*/ */
private fun startForegroundServiceWithNotification() { private fun startForegroundServiceWithNotification() {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
}
/**
* 创建通知
*/
private fun createNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Android 8.0+ 需要创建通知渠道 // Android 8.0+ 需要创建通知渠道
@ -484,27 +791,25 @@ class VoiceInteractionService : Service() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("语音助手") .setContentTitle("语音助手")
.setContentText("双击蓝牙耳机按键即可开始语音交互") .setContentText("双击蓝牙耳机按键即可开始语音交互")
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.build() .build()
startForeground(NOTIFICATION_ID, notification)
} }
/** /**
* 当 Service 被启动时调用 * 当 Service 被启动时调用
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: $intent") Log.d(TAG, "onStartCommand: $intent, extras: ${intent?.extras}")
// 处理媒体按键 Intent // 处理媒体按键 Intent
if (intent != null) { if (intent != null) {
// 如果是媒体按钮事件,尝试手动处理 // 如果是媒体按钮事件,尝试手动处理
if (Intent.ACTION_MEDIA_BUTTON == intent.action) { if (Intent.ACTION_MEDIA_BUTTON == intent.action) {
Log.d(TAG, "收到媒体按钮事件: $intent") Log.d(TAG, "收到媒体按钮事件 Intent: $intent, extras: ${intent.extras}")
// 根据 Android 版本使用不同的方式获取 KeyEvent // 根据 Android 版本使用不同的方式获取 KeyEvent
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -514,14 +819,23 @@ class VoiceInteractionService : Service() {
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent
} }
Log.d(TAG, "手动处理媒体按钮事件: $keyEvent") Log.d(TAG, "从 Intent 中提取的按键事件: $keyEvent")
if (keyEvent != null) { if (keyEvent != null) {
Log.d(TAG, "准备手动处理按键事件: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}")
handleKeyEvent(keyEvent) handleKeyEvent(keyEvent)
} else {
// 即使没有 KeyEvent,也尝试触发语音交互
Log.d(TAG, "Intent 中没有按键事件,但仍然尝试触发语音交互")
// 重置处理状态,确保可以开始新的语音交互
isActive = false
handleDoubleClick()
} }
} }
// 无论如何都要尝试让 MediaSession 处理 // 无论如何都要尝试让 MediaSession 处理
MediaButtonReceiver.handleIntent(mediaSession, intent) Log.d(TAG, "将 Intent 传递给 MediaSession 处理")
val result = MediaButtonReceiver.handleIntent(mediaSession, intent)
Log.d(TAG, "MediaSession 处理结果: $result")
} }
return START_STICKY return START_STICKY
@ -531,9 +845,12 @@ class VoiceInteractionService : Service() {
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { override fun onDestroy() {
Log.d(TAG, "onDestroy") Log.d(TAG, "onDestroy - 语音交互服务正在销毁")
super.onDestroy() super.onDestroy()
// 停止超时检查
stopTimeoutCheck()
// 取消注册媒体按钮接收器 // 取消注册媒体按钮接收器
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
@ -547,5 +864,24 @@ class VoiceInteractionService : Service() {
azureTtsHelper.dispose() azureTtsHelper.dispose()
isRunning.set(false) isRunning.set(false)
// 发送服务已停止的广播
sendServiceStatusBroadcast(STATUS_STOPPED)
// 通知监听器服务已停止
serviceStatusListener?.onServiceStopped()
Log.d(TAG, "语音交互服务已完全销毁")
}
/**
* 发送服务状态变化广播
*/
private fun sendServiceStatusBroadcast(status: String) {
val intent = Intent(ACTION_SERVICE_STATUS_CHANGED).apply {
putExtra(EXTRA_SERVICE_STATUS, status)
}
sendBroadcast(intent)
Log.d(TAG, "已发送服务状态广播: $status")
} }
} }

4
android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt

@ -28,8 +28,8 @@ class VolcanoAIService(private val context: Context) {
private val apiKey: String private val apiKey: String
get() { get() {
val sharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) // 从 MainActivity 的静态变量获取 API Key
return sharedPreferences.getString("flutter.volcano_ai_api_key", "") ?: "" return MainActivity.volcanoAiApiKey
} }
init { init {

9
lib/core/bindings/initial_binding.dart

@ -3,15 +3,12 @@ import '../../data/services/azure_asr_service.dart';
import '../../data/services/azure_tts_service.dart'; import '../../data/services/azure_tts_service.dart';
import '../../data/services/chat_history_service.dart'; import '../../data/services/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart'; import '../../data/services/volcano_translation_service.dart';
import '../../data/services/config_service.dart'; import '../../data/services/voice_interaction_service.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings { class InitialBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
// 配置服务 - 需要首先初始化
Get.putAsync<ConfigService>(() => ConfigService().init());
// Azure 语音识别服务 // Azure 语音识别服务
Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true); Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true);
@ -23,6 +20,8 @@ class InitialBinding extends Bindings {
// 火山翻译服务 // 火山翻译服务
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), fenix: true); Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(), fenix: true);
// 语音交互服务
Get.putAsync<VoiceInteractionService>(() => VoiceInteractionService().init());
} }
} }

57
lib/data/services/config_service.dart

@ -1,57 +0,0 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:logger/logger.dart';
import 'package:flutter/services.dart';
/// 配置服务,用于管理应用配置和环境变量
class ConfigService extends GetxService {
final Logger _logger = Logger();
late final SharedPreferences _prefs;
// 创建一个 MethodChannel 用于与原生代码通信
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/config');
// 初始化服务
Future<ConfigService> init() async {
try {
_prefs = await SharedPreferences.getInstance();
// 从 .env 读取 Azure 语音服务配置
final azureSpeechKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'];
final azureSpeechRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'];
// 从 .env 读取火山 AI API Key
final volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'];
// 通过 MethodChannel 向原生代码传递配置值
try {
await _channel.invokeMethod('setConfig', {
'azure_speech_key': azureSpeechKey ?? '',
'azure_speech_region': azureSpeechRegion ?? '',
'volcano_ai_api_key': volcanoAiApiKey ?? '',
});
_logger.i('已通过 MethodChannel 向原生代码传递配置值');
} catch (e) {
_logger.e('通过 MethodChannel 传递配置值失败: $e');
}
return this;
} catch (e) {
_logger.e('初始化配置服务失败: $e');
rethrow;
}
}
// 获取配置值
String? getString(String key) {
return _prefs.getString(key);
}
// 设置配置值
Future<bool> setString(String key, String value) {
return _prefs.setString(key, value);
}
}

70
lib/data/services/voice_interaction_service.dart

@ -0,0 +1,70 @@
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 语音交互服务,用于管理原生的 VoiceInteractionService
class VoiceInteractionService extends GetxService {
static VoiceInteractionService get to => Get.find();
// 服务运行状态
final _isRunning = false.obs;
bool get isRunning => _isRunning.value;
// 方法通道
final MethodChannel _channel = const MethodChannel('com.deep_voice.voice_interaction');
/// 初始化服务
Future<VoiceInteractionService> init() async {
// 获取服务运行状态
await _updateServiceStatus();
return this;
}
/// 启动语音交互服务
Future<bool> startService() async {
try {
// 获取配置信息
final speechKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '';
final speechRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? '';
// 从 .env 读取火山 AI API Key
final aiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '';
// 调用原生方法,传递配置参数
final result = await _channel.invokeMethod<bool>('startVoiceInteractionService', {
'azure_speech_key': speechKey,
'azure_speech_region': speechRegion,
'volcano_ai_api_key': aiApiKey,
}) ?? false;
await _updateServiceStatus();
return result;
} catch (e) {
print('启动语音交互服务失败: $e');
return false;
}
}
/// 停止语音交互服务
Future<bool> stopService() async {
try {
final result = await _channel.invokeMethod<bool>('stopVoiceInteractionService') ?? false;
await _updateServiceStatus();
return result;
} catch (e) {
print('停止语音交互服务失败: $e');
return false;
}
}
/// 更新服务状态
Future<void> _updateServiceStatus() async {
try {
final status = await _channel.invokeMethod<bool>('isVoiceInteractionServiceRunning') ?? false;
_isRunning.value = status;
} catch (e) {
print('获取语音交互服务状态失败: $e');
_isRunning.value = false;
}
}
}

5
lib/main.dart

@ -71,6 +71,11 @@ class MainApp extends StatelessWidget {
translations: AppTranslations(), translations: AppTranslations(),
locale: const Locale('zh', 'CN'), locale: const Locale('zh', 'CN'),
fallbackLocale: const Locale('en', 'US'), fallbackLocale: const Locale('en', 'US'),
// 在应用初始化完成后,初始化 ConfigService
onInit: () {
// 延迟执行,确保应用已完全启动
},
); );
} }
); );

81
lib/modules/home/controllers/home_controller.dart

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../data/services/voice_interaction_service.dart';
enum ConnectStatus { enum ConnectStatus {
initial, // 初始状态,显示添加设备 initial, // 初始状态,显示添加设备
@ -17,6 +18,10 @@ class HomeController extends GetxController {
Timer? _animationTimer; Timer? _animationTimer;
bool _isDisposed = false; bool _isDisposed = false;
// 语音服务状态
final _voiceServiceRunning = false.obs;
bool get voiceServiceRunning => _voiceServiceRunning.value;
ConnectStatus get connectStatus => _connectStatus.value; ConnectStatus get connectStatus => _connectStatus.value;
String get searchDots => _searchDots.value; String get searchDots => _searchDots.value;
@ -24,6 +29,9 @@ class HomeController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
_connectStatus.value = ConnectStatus.initial; _connectStatus.value = ConnectStatus.initial;
// 获取语音服务状态
_updateVoiceServiceStatus();
} }
@override @override
@ -32,6 +40,79 @@ class HomeController extends GetxController {
stopSearchingAnimation(); stopSearchingAnimation();
super.onClose(); super.onClose();
} }
/// 更新语音服务状态
Future<void> _updateVoiceServiceStatus() async {
if (Get.isRegistered<VoiceInteractionService>()) {
_voiceServiceRunning.value = VoiceInteractionService.to.isRunning;
} else {
_voiceServiceRunning.value = false;
}
}
/// 启动语音交互服务
Future<void> startVoiceService() async {
// 确保 ConfigService 已初始化
// 确保 VoiceInteractionService 已初始化
if (!Get.isRegistered<VoiceInteractionService>()) {
await Get.putAsync(() => VoiceInteractionService().init());
}
final result = await VoiceInteractionService.to.startService();
await _updateVoiceServiceStatus();
if (result) {
Get.snackbar(
'成功',
'语音交互服务已启动',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green.withOpacity(0.1),
colorText: Colors.green,
duration: const Duration(seconds: 2),
);
} else {
Get.snackbar(
'错误',
'启动语音交互服务失败',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
duration: const Duration(seconds: 2),
);
}
}
/// 停止语音交互服务
Future<void> stopVoiceService() async {
if (!Get.isRegistered<VoiceInteractionService>()) {
await Get.putAsync(() => VoiceInteractionService().init());
}
final result = await VoiceInteractionService.to.stopService();
await _updateVoiceServiceStatus();
if (result) {
Get.snackbar(
'成功',
'语音交互服务已停止',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue.withOpacity(0.1),
colorText: Colors.blue,
duration: const Duration(seconds: 2),
);
} else {
Get.snackbar(
'错误',
'停止语音交互服务失败',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
duration: const Duration(seconds: 2),
);
}
}
void startSearchingAnimation() { void startSearchingAnimation() {
stopSearchingAnimation(); // Clear any existing timer stopSearchingAnimation(); // Clear any existing timer

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

@ -240,6 +240,65 @@ class HomeView extends GetView<HomeController> {
} }
}); });
} }
/// 构建语音服务控制按钮
Widget _buildVoiceServiceButton() {
return Obx(() {
final isRunning = controller.voiceServiceRunning;
return Container(
width: double.infinity,
height: 56.h,
margin: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isRunning
? [const Color(0xFFE57373), const Color(0xFFEF5350)] // 红色渐变(停止)
: [const Color(0xFF66BB6A), const Color(0xFF4CAF50)], // 绿色渐变(启动)
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(28.r),
boxShadow: [
BoxShadow(
color: (isRunning ? const Color(0xFFE57373) : const Color(0xFF66BB6A)).withOpacity(0.3),
offset: const Offset(0, 4),
blurRadius: 12,
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isRunning ? controller.stopVoiceService : controller.startVoiceService,
borderRadius: BorderRadius.circular(28.r),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isRunning ? Icons.stop : Icons.play_arrow,
color: Colors.white,
size: 24.sp,
),
SizedBox(width: 12.w),
Text(
isRunning ? '停止语音服务' : '启动语音服务',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
),
],
),
),
),
),
);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -272,7 +331,8 @@ class HomeView extends GetView<HomeController> {
), ),
), ),
_buildActionButton(), _buildActionButton(),
SizedBox(height: 20.h),
_buildVoiceServiceButton(),
SizedBox(height: 40.h), SizedBox(height: 40.h),
], ],
), ),

7
macos/Podfile.lock

@ -23,6 +23,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- speech_to_text (0.0.1): - speech_to_text (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@ -46,6 +49,7 @@ DEPENDENCIES:
- 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`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- speech_to_text (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin`) - 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`)
@ -77,6 +81,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
speech_to_text: speech_to_text:
:path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text/darwin
sqflite_darwin: sqflite_darwin:
@ -96,6 +102,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96

Loading…
Cancel
Save