Browse Source

service

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
e1a6afba5c
  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
fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean {
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.serviceRegion = serviceRegion
this.currentLanguage = language
@ -31,13 +39,22 @@ class AzureAsrHelper {
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)
Log.d(TAG, "Azure 语音服务初始化成功")
return true
} catch (e: Exception) {
Log.e(TAG, "初始化失败: ${e.message}")
Log.e(TAG, "初始化失败: ${e.message}", e)
return false
}
}
@ -70,26 +87,43 @@ class AzureAsrHelper {
// 开始连续语音识别
fun startContinuousRecognition(language: String? = null, callback: ContinuousRecognizeCallback): Boolean {
if (recognizer == null) {
Log.e(TAG, "SpeechRecognizer 未初始化")
callback.onError("SpeechRecognizer 未初始化")
return false
}
// 如果已经在进行连续识别,先停止
if (isContinuousRecognitionActive) {
Log.d(TAG, "已经在进行连续识别,先停止")
stopContinuousRecognition(callback)
}
// 如果指定了新的语言,需要重新初始化
if (language != null && language != currentLanguage) {
Log.d(TAG, "语言变更,重新初始化: $language")
initialize(subscriptionKey, serviceRegion, language)
}
try {
// 先发送会话开始事件
Log.d(TAG, "准备开始连续语音识别")
callback.onSessionStarted()
// 重新创建识别器
Log.d(TAG, "重新创建识别器")
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)
// 设置识别事件处理
@ -98,7 +132,10 @@ class AzureAsrHelper {
recognizer.recognized.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
Log.d(TAG, "识别结果: ${event.result.text}")
callback.onResult(event.result.text)
} else {
Log.d(TAG, "识别结果原因: ${event.result.reason}")
}
}
)
@ -107,6 +144,7 @@ class AzureAsrHelper {
recognizer.recognizing.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) {
Log.d(TAG, "正在识别: ${event.result.text}")
callback.onRecognizing(event.result.text)
}
}
@ -115,6 +153,7 @@ class AzureAsrHelper {
// 会话开始事件
recognizer.sessionStarted.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
Log.d(TAG, "会话开始")
isContinuousRecognitionActive = true
callback.onSessionStarted()
}
@ -123,6 +162,7 @@ class AzureAsrHelper {
// 会话结束事件
recognizer.sessionStopped.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
Log.d(TAG, "会话结束")
isContinuousRecognitionActive = false
callback.onSessionStopped()
}
@ -132,18 +172,22 @@ class AzureAsrHelper {
recognizer.canceled.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else ""
Log.e(TAG, "识别取消: ${event.reason}, 详情: $errorDetails")
callback.onCanceled(event.reason.toString(), errorDetails)
isContinuousRecognitionActive = false
}
)
// 开始连续识别
Log.d(TAG, "开始连续识别")
recognizer.startContinuousRecognitionAsync().get()
Log.d(TAG, "连续识别已启动")
isContinuousRecognitionActive = true
}
return true
} catch (e: Exception) {
Log.e(TAG, "开始连续识别失败: ${e.message}", e)
callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = 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.embedding.android.FlutterActivity
import android.content.Context
import android.content.SharedPreferences
import android.content.ComponentName
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_TTS_CHANNEL = "com.deep_voice.azure_tts"
private val VOICE_INTERACTION_CHANNEL = "com.deep_voice.voice_interaction"
private val CONFIG_CHANNEL = "com.example.deep_voice/config"
private val TAG = "MainActivity"
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
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?) {
super.onCreate(savedInstanceState)
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
// 自动启动语音交互服务
startVoiceInteractionService()
// 不再自动启动服务,由用户通过 Flutter 界面手动启动
Log.d(TAG, "MainActivity 已创建,但不会自动启动 VoiceInteractionService")
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
@ -281,6 +286,22 @@ class MainActivity: FlutterActivity() {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"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()
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 事件发送方法
@ -358,8 +350,21 @@ class MainActivity: FlutterActivity() {
* 启动语音交互服务
*/
private fun startVoiceInteractionService() {
Log.d(TAG, "准备启动语音交互服务")
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)
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.content.ComponentName
import android.media.AudioManager
import android.os.Handler
import android.os.Looper
import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONArray
@ -41,15 +43,50 @@ class VoiceInteractionService : Service() {
fun isServiceRunning(): Boolean {
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 var isRecording = false
private var isProcessing = false
private var isActive = false // 合并 isRecording 和 isProcessing 为一个状态
private var lastKeyEventTime = 0L
private var keyEventCount = 0
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服务
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
@ -67,77 +104,112 @@ class VoiceInteractionService : Service() {
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate")
Log.d(TAG, "onCreate - 语音交互服务正在创建")
isRunning.set(true)
isActive = false // 确保初始状态为 false
isTtsSpeaking = false // 确保初始状态为 false
// 获取 AudioManager
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 1. 初始化Azure服务
initAzureServices()
// 2. 初始化火山AI服务
volcanoAIService = VolcanoAIService(applicationContext)
// 初始化服务
initServices()
// 3. 初始化MediaSession
// 初始化 MediaSession 和按键监听
initMediaSession()
// 4. 启动前台通知(Foreground Service)
// 启动前台通知(Foreground Service)
startForegroundServiceWithNotification()
// 5. 注册成为媒体按钮接收器
// 注册成为媒体按钮接收器
registerMediaButtonReceiver()
// 6. 测试按键事件
testKeyEvent()
// 发送服务已启动的广播
sendServiceStatusBroadcast(STATUS_STARTED)
// 通知监听器服务已启动
serviceStatusListener?.onServiceStarted()
Log.d(TAG, "VoiceInteractionService 初始化完成,等待蓝牙按键事件...")
}
/**
* 测试按键事件,模拟一个按键事件以验证处理逻辑
* 测试按键事件
*/
private fun testKeyEvent() {
Log.d(TAG, "测试按键事件处理")
Log.d(TAG, "测试按键事件")
// 创建一个模拟的按键事件
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK)
// 创建一个媒体按钮意图
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(Intent.EXTRA_KEY_EVENT, keyEvent)
component = ComponentName(packageName, VoiceInteractionService::class.java.name)
}
// MediaButtonReceiver.handleIntent 返回的是 KeyEvent 类型,而不是 Boolean
// 发送给 MediaSession 处理
MediaButtonReceiver.handleIntent(mediaSession, mediaButtonIntent)
Log.d(TAG, "测试按键事件已发送")
// 直接处理按键事件
// // 直接处理按键事件
handleKeyEvent(keyEvent)
Log.d(TAG, "测试按键事件已发送")
}
/**
* 初始化所有服务
*/
private fun initServices() {
Log.d(TAG, "初始化所有服务")
// 1. 初始化Azure服务
initAzureServices()
// 2. 初始化火山AI服务
volcanoAIService = VolcanoAIService(applicationContext)
}
/**
* 处理按键事件
*/
private fun handleKeyEvent(keyEvent: KeyEvent) {
Log.d(TAG, "处理按键事件开始: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_HEADSETHOOK,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(TAG, "处理按键事件: ${keyEvent.keyCode}")
Log.d(TAG, "处理媒体按键事件: ${keyEvent.keyCode}")
// 检测双击
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++
Log.d(TAG, "连续按键计数: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,开始语音交互")
handleDoubleClick()
keyEventCount = 0
}
} else {
Log.d(TAG, "单击事件,重置计数")
keyEventCount = 0
}
lastKeyEventTime = currentTime
}
else -> {
Log.d(TAG, "未处理的按键代码: ${keyEvent.keyCode}")
}
}
} else {
Log.d(TAG, "忽略非按下事件: ${keyEvent.action}")
}
}
@ -157,24 +229,30 @@ class VoiceInteractionService : Service() {
azureAsrHelper = AzureAsrHelper()
azureTtsHelper = AzureTtsHelper()
// 从应用的SharedPreferences获取Azure配置
val sharedPreferences = applicationContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val subscriptionKey = sharedPreferences.getString("flutter.azure_speech_key", "") ?: ""
val serviceRegion = sharedPreferences.getString("flutter.azure_speech_region", "") ?: ""
// 从 MainActivity 的静态变量获取 Azure 配置
val subscriptionKey = MainActivity.azureSpeechKey
val serviceRegion = MainActivity.azureSpeechRegion
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()) {
// 初始化ASR
azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
val asrInitResult = azureAsrHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
Log.d(TAG, "ASR初始化结果: $asrInitResult")
// 初始化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 {
Log.e(TAG, "Azure配置信息不完整,无法初始化Azure服务")
}
}
/**
* 初始化 MediaSession 以捕获蓝牙耳机按键
*/
@ -202,6 +280,8 @@ class VoiceInteractionService : Service() {
// 监听回调
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
Log.d(TAG, "MediaSession.onMediaButtonEvent: $mediaButtonEvent")
// 根据 Android 版本使用不同的方式获取 KeyEvent
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
@ -213,7 +293,7 @@ class VoiceInteractionService : Service() {
Log.d(TAG, "收到媒体按钮事件: $keyEvent")
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) {
when (keyEvent.keyCode) {
@ -225,25 +305,31 @@ class VoiceInteractionService : Service() {
// 检测双击
val currentTime = System.currentTimeMillis()
if (currentTime - lastKeyEventTime < 500) { // 500毫秒内的按键视为连续按键
val timeDiff = currentTime - lastKeyEventTime
Log.d(TAG, "MediaSession 按键时间差: $timeDiff ms")
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++
Log.d(TAG, "MediaSession 连续按键计数: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击
Log.d(TAG, "检测到双击,开始语音交互")
Log.d(TAG, "MediaSession 检测到双击,开始语音交互")
// 重置处理状态,确保可以开始新的语音交互
isActive = false
handleDoubleClick()
keyEventCount = 0
return true
}
} else {
Log.d(TAG, "MediaSession 单击事件,重置计数")
keyEventCount = 0
}
lastKeyEventTime = currentTime
// 单击也处理
if (keyEventCount == 0) {
Log.d(TAG, "检测到单击,开始语音交互")
handleDoubleClick()
return true
}
Log.d(TAG, "MediaSession 处理单击事件")
handleDoubleClick()
return true
}
}
}
@ -273,15 +359,22 @@ class VoiceInteractionService : Service() {
* 处理耳机双击事件
*/
private fun handleDoubleClick() {
if (isProcessing) {
Log.d(TAG, "正在处理中,忽略此次点击")
return
}
Log.d(TAG, "handleDoubleClick - isActive: $isActive")
// 无论当前状态如何,都先停止 TTS 播放
stopCurrentTTS()
if (!isRecording) {
// 立即播放提示音,不等待语音识别开始
playStartPrompt()
if (!isActive) {
Log.d(TAG, "开始新的语音交互")
startVoiceInteraction()
} else {
stopVoiceInteraction()
Log.d(TAG, "已经在活动中,只需更新活动时间并继续")
// 更新最后活动时间
updateLastActivityTime()
}
}
@ -289,68 +382,239 @@ class VoiceInteractionService : Service() {
* 开始语音交互
*/
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
}
isRecording = true
isProcessing = true
isActive = true
// 播放提示音
playStartPrompt()
// 更新最后活动时间
updateLastActivityTime()
// 启动超时检查
startTimeoutCheck()
Log.d(TAG, "设置状态: isActive=true")
// 不再在这里播放提示音,而是在 handleDoubleClick 中播放
// 开始录音识别
azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String) {
// 实时识别结果,可以记录日志
Log.d(TAG, "正在识别: $recognizing")
}
override fun onResult(result: String) {
// 最终识别结果
Log.d(TAG, "语音识别结果: $result")
try {
Log.d(TAG, "开始调用 Azure 语音识别")
azureAsrHelper.startContinuousRecognition(null, object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String) {
// 实时识别结果,可以记录日志
Log.d(TAG, "正在识别: $recognizing")
// 如果检测到用户在讲话,停止当前的 TTS 播放
if (recognizing.isNotEmpty()) {
// 停止 TTS 播放
stopCurrentTTS()
// 更新最后活动时间
updateLastActivityTime()
}
}
// 停止录音
isRecording = false
override fun onResult(result: String) {
// 最终识别结果
Log.d(TAG, "语音识别结果: $result")
// 更新最后活动时间
// 如果有识别结果,则发送到VolcanoAI处理,但不停止录音
if (result.isNotEmpty()) {
Log.d(TAG, "有识别结果,开始处理")
updateLastActivityTime()
processWithVolcanoAI(result)
// 不设置 isActive = false,保持活动状态
} else {
// Log.d(TAG, "没有识别结果,但继续保持活动状态")
// 即使没有识别结果,也不停止活动状态
}
}
// 如果有识别结果,则发送到VolcanoAI处理
if (result.isNotEmpty()) {
processWithVolcanoAI(result)
} else {
isProcessing = false
override fun onSessionStarted() {
Log.d(TAG, "语音识别会话开始")
// 更新最后活动时间
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.e(TAG, "语音识别取消: $reason, 详情: $errorDetails")
isRecording = false
isProcessing = false
}
Log.d(TAG, "超时检查 - 已经过去 $elapsedTime ms, 是否活动中: $isActive, 是否正在播放TTS: $isTtsSpeaking")
override fun onError(error: String) {
Log.e(TAG, "语音识别错误: $error")
isRecording = false
isProcessing = false
if (isActive && !isTtsSpeaking &&elapsedTime >= RECOGNITION_TIMEOUT) {
// 超过30秒没有活动且没有TTS播放,暂停当前语音识别
Log.d(TAG, "超过 ${RECOGNITION_TIMEOUT/1000} 秒没有活动,暂停语音识别")
// 播放错误提示音
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() {
if (!isRecording) {
if (!isActive) {
return
}
@ -380,37 +644,59 @@ class VoiceInteractionService : Service() {
Log.e(TAG, "停止语音识别错误: $error")
}
})
isRecording = false
isActive = false
}
/**
* 播放开始提示音
*/
private fun playStartPrompt() {
azureTtsHelper.speakText("我在听", object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "提示音播放完成")
}
override fun onError(error: String) {
Log.e(TAG, "提示音播放错误: $error")
}
})
Log.d(TAG, "开始播放提示音: '我在听'")
try {
isTtsSpeaking = true
azureTtsHelper.speakText("我在!", object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "提示音播放完成: $message")
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() {
azureTtsHelper.speakText("抱歉,我没有听清", object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "错误提示音播放完成")
}
override fun onError(error: String) {
Log.e(TAG, "错误提示音播放错误: $error")
}
})
private fun playErrorPrompt(errorMessage: String = "抱歉,我没有听清") {
Log.d(TAG, "开始播放错误提示音: '$errorMessage'")
try {
isTtsSpeaking = true
azureTtsHelper.speakText(errorMessage, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "错误提示音播放完成: $message")
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处理
Thread {
try {
// 创建用户消息
val messages = JSONArray().apply {
put(volcanoAIService.createUserMessage(text))
@ -432,10 +719,10 @@ class VoiceInteractionService : Service() {
speakAIResponse(response)
} catch (e: Exception) {
Log.e(TAG, "VolcanoAI处理错误: ${e.message}")
isProcessing = false
// 不设置 isActive = false,保持活动状态
// 播放错误提示音
playErrorPrompt()
playErrorPrompt("AI处理出错")
}
}.start()
}
@ -444,15 +731,27 @@ class VoiceInteractionService : Service() {
* 播放AI回复
*/
private fun speakAIResponse(text: String) {
Log.d(TAG, "开始播放AI回复: '${text.take(20)}...'")
isTtsSpeaking = true
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
Log.d(TAG, "AI回复播放完成")
isProcessing = false
isTtsSpeaking = false
// 更新最后活动时间
updateLastActivityTime()
// 不设置 isActive = false,保持活动状态
// 继续保持对话状态,等待用户下一次输入
Log.d(TAG, "继续保持对话状态,等待用户下一次输入")
}
override fun onError(error: String) {
Log.e(TAG, "AI回复播放错误: $error")
isProcessing = false
isTtsSpeaking = false
// 即使出错,也不结束活动状态,继续保持对话
Log.d(TAG, "AI回复播放出错,但继续保持对话状态")
}
})
}
@ -461,6 +760,14 @@ class VoiceInteractionService : Service() {
* 启动一个"前台通知",保证此Service在后台不会被系统回收
*/
private fun startForegroundServiceWithNotification() {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
}
/**
* 创建通知
*/
private fun createNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Android 8.0+ 需要创建通知渠道
@ -484,27 +791,25 @@ class VoiceInteractionService : Service() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("语音助手")
.setContentText("双击蓝牙耳机按键即可开始语音交互")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build()
startForeground(NOTIFICATION_ID, notification)
}
/**
* 当 Service 被启动时调用
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: $intent")
Log.d(TAG, "onStartCommand: $intent, extras: ${intent?.extras}")
// 处理媒体按键 Intent
if (intent != null) {
// 如果是媒体按钮事件,尝试手动处理
if (Intent.ACTION_MEDIA_BUTTON == intent.action) {
Log.d(TAG, "收到媒体按钮事件: $intent")
Log.d(TAG, "收到媒体按钮事件 Intent: $intent, extras: ${intent.extras}")
// 根据 Android 版本使用不同的方式获取 KeyEvent
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
}
Log.d(TAG, "手动处理媒体按钮事件: $keyEvent")
Log.d(TAG, "从 Intent 中提取的按键事件: $keyEvent")
if (keyEvent != null) {
Log.d(TAG, "准备手动处理按键事件: 代码=${keyEvent.keyCode}, 动作=${keyEvent.action}")
handleKeyEvent(keyEvent)
} else {
// 即使没有 KeyEvent,也尝试触发语音交互
Log.d(TAG, "Intent 中没有按键事件,但仍然尝试触发语音交互")
// 重置处理状态,确保可以开始新的语音交互
isActive = false
handleDoubleClick()
}
}
// 无论如何都要尝试让 MediaSession 处理
MediaButtonReceiver.handleIntent(mediaSession, intent)
Log.d(TAG, "将 Intent 传递给 MediaSession 处理")
val result = MediaButtonReceiver.handleIntent(mediaSession, intent)
Log.d(TAG, "MediaSession 处理结果: $result")
}
return START_STICKY
@ -531,9 +845,12 @@ class VoiceInteractionService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
Log.d(TAG, "onDestroy")
Log.d(TAG, "onDestroy - 语音交互服务正在销毁")
super.onDestroy()
// 停止超时检查
stopTimeoutCheck()
// 取消注册媒体按钮接收器
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
@ -547,5 +864,24 @@ class VoiceInteractionService : Service() {
azureTtsHelper.dispose()
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
get() {
val sharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
return sharedPreferences.getString("flutter.volcano_ai_api_key", "") ?: ""
// 从 MainActivity 的静态变量获取 API Key
return MainActivity.volcanoAiApiKey
}
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/chat_history_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 {
@override
void dependencies() {
// 配置服务 - 需要首先初始化
Get.putAsync<ConfigService>(() => ConfigService().init());
// Azure 语音识别服务
Get.lazyPut<AzureAsrService>(() => AzureAsrService(), fenix: true);
@ -23,6 +20,8 @@ class InitialBinding extends Bindings {
// 火山翻译服务
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(),
locale: const Locale('zh', 'CN'),
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 'dart:async';
import 'package:flutter/material.dart';
import '../../../data/services/voice_interaction_service.dart';
enum ConnectStatus {
initial, // 初始状态,显示添加设备
@ -17,6 +18,10 @@ class HomeController extends GetxController {
Timer? _animationTimer;
bool _isDisposed = false;
// 语音服务状态
final _voiceServiceRunning = false.obs;
bool get voiceServiceRunning => _voiceServiceRunning.value;
ConnectStatus get connectStatus => _connectStatus.value;
String get searchDots => _searchDots.value;
@ -24,6 +29,9 @@ class HomeController extends GetxController {
void onInit() {
super.onInit();
_connectStatus.value = ConnectStatus.initial;
// 获取语音服务状态
_updateVoiceServiceStatus();
}
@override
@ -32,6 +40,79 @@ class HomeController extends GetxController {
stopSearchingAnimation();
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() {
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
Widget build(BuildContext context) {
@ -272,7 +331,8 @@ class HomeView extends GetView<HomeController> {
),
),
_buildActionButton(),
SizedBox(height: 20.h),
_buildVoiceServiceButton(),
SizedBox(height: 40.h),
],
),

7
macos/Podfile.lock

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

Loading…
Cancel
Save