Browse Source

完成ms 语音sdk集成

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
0c087b4911
  1. 6
      android/app/build.gradle.kts
  2. 208
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  3. 194
      android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt
  4. 493
      android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt
  5. 544
      lib/data/services/voice_recognition_service.dart
  6. 301
      lib/modules/chat/controllers/chat_controller.dart
  7. 147
      lib/modules/chat/controllers/voice_input_controller.dart
  8. 242
      lib/modules/chat/views/chat_view.dart
  9. 217
      lib/modules/chat/widgets/voice_input_panel.dart
  10. 578
      lib/modules/speech_demo/speech_demo_page.dart

6
android/app/build.gradle.kts

@ -1,7 +1,6 @@
repositories {
google()
mavenCentral()
maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") }
}
plugins {
@ -49,8 +48,9 @@ android {
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// 火山语音识别 SDK 依赖
implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.6")
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.42.0")
}
flutter {

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

@ -10,102 +10,168 @@ import android.util.Log
import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel
class MainActivity: AudioServiceActivity() {
private var volcSpeechBridge: VolcSpeechBridge? = null
private val CHANNEL = "com.example.deep_voice/speech_recognition"
private val EVENT_CHANNEL = "com.example.deep_voice/speech_recognition_events"
private val TAG = "MainActivity"
private val MANAGE_STORAGE_REQUEST_CODE = 2002
private lateinit var permissionChannel: MethodChannel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.i(TAG, "MainActivity onCreate")
}
private val speechHelper = SpeechRecognitionHelper()
private var eventSink: EventChannel.EventSink? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 设置权限通道 - 只处理Android 11+的MANAGE_EXTERNAL_STORAGE权限
permissionChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.deep_voice/permissions")
permissionChannel.setMethodCallHandler { call, result ->
// 设置方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"openAppSettings" -> {
openAppSettings()
result.success(true)
}
"requestManageExternalStorage" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
requestManageExternalStorage()
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey")
val serviceRegion = call.argument<String>("serviceRegion")
if (subscriptionKey == null || serviceRegion == null) {
result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null)
return@setMethodCallHandler
}
try {
speechHelper.initialize(subscriptionKey, serviceRegion)
result.success(true)
} else {
result.success(false)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
}
"isExternalStorageManager" -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
result.success(Environment.isExternalStorageManager())
} else {
result.success(true) // 在Android 11以下版本,返回true
"recognizeOnce" -> {
try {
speechHelper.recognizeOnce(object : SpeechRecognitionHelper.RecognizeCallback {
override fun onResult(text: String) {
result.success(text)
}
override fun onError(error: String) {
result.error("RECOGNITION_ERROR", error, null)
}
})
} catch (e: Exception) {
result.error("RECOGNITION_ERROR", e.message, null)
}
}
else -> result.notImplemented()
}
}
"startContinuousRecognition" -> {
try {
if (speechHelper.isContinuousRecognitionActive()) {
result.error("ALREADY_ACTIVE", "连续识别已经在进行中", null)
return@setMethodCallHandler
}
// 准备火山语音识别环境
try {
Log.i(TAG, "Preparing Volcano Speech Engine environment")
com.bytedance.speech.speechengine.SpeechEngineGenerator.PrepareEnvironment(applicationContext, application)
Log.i(TAG, "Volcano Speech Engine environment prepared successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to prepare Volcano Speech Engine environment", e)
}
speechHelper.startContinuousRecognition(object : SpeechRecognitionHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) {
sendEvent(mapOf(
"eventType" to "finalResult",
"text" to text
))
}
// 初始化火山语音识别桥接类
volcSpeechBridge = VolcSpeechBridge(this, flutterEngine.dartExecutor.binaryMessenger)
}
override fun onRecognizing(recognizing: String) {
sendEvent(mapOf(
"eventType" to "intermediateResult",
"text" to recognizing
))
}
override fun onSessionStarted() {
sendEvent(mapOf(
"eventType" to "sessionStarted"
))
}
override fun onSessionStopped() {
sendEvent(mapOf(
"eventType" to "sessionStopped"
))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendEvent(mapOf(
"eventType" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
))
}
override fun onError(error: String) {
sendEvent(mapOf(
"eventType" to "error",
"error" to error
))
}
})
result.success(true)
} catch (e: Exception) {
result.error("RECOGNITION_ERROR", e.message, null)
}
}
"stopContinuousRecognition" -> {
try {
if (!speechHelper.isContinuousRecognitionActive()) {
result.error("NOT_ACTIVE", "连续识别未在进行中", null)
return@setMethodCallHandler
}
private fun requestManageExternalStorage() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
Log.i(TAG, "Requesting MANAGE_EXTERNAL_STORAGE permission")
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:$packageName")
startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE)
} catch (e: Exception) {
Log.e(TAG, "Error requesting MANAGE_EXTERNAL_STORAGE", e)
// 如果上面的Intent失败,尝试打开通用的存储设置页面
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE)
speechHelper.stopContinuousRecognition(object : SpeechRecognitionHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) {}
override fun onRecognizing(recognizing: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {
sendEvent(mapOf(
"eventType" to "sessionStopped"
))
}
override fun onCanceled(reason: String, errorDetails: String) {}
override fun onError(error: String) {
result.error("STOP_ERROR", error, null)
}
})
result.success(true)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"dispose" -> {
try {
speechHelper.dispose()
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
}
else -> {
result.notImplemented()
}
}
}
}
private fun openAppSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
}
// 只处理MANAGE_EXTERNAL_STORAGE权限的结果
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == MANAGE_STORAGE_REQUEST_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val granted = Environment.isExternalStorageManager()
Log.i(TAG, "MANAGE_EXTERNAL_STORAGE permission: ${if (granted) "granted" else "denied"}")
// 设置事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
// 通知Flutter层权限状态变化
permissionChannel.invokeMethod("manageExternalStorageResult", granted)
override fun onCancel(arguments: Any?) {
eventSink = null
}
}
} else {
super.onActivityResult(requestCode, resultCode, data)
)
}
private fun sendEvent(event: Map<String, Any?>) {
runOnUiThread {
eventSink?.success(event)
}
}
override fun onDestroy() {
// 释放火山语音识别资源
volcSpeechBridge?.dispose()
volcSpeechBridge = null
speechHelper.dispose()
super.onDestroy()
}
}

194
android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt

@ -0,0 +1,194 @@
package com.example.deep_voice
import android.util.Log
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
import java.util.concurrent.ExecutionException
import java.util.function.Consumer
class SpeechRecognitionHelper {
private var recognizer: SpeechRecognizer? = null
private val TAG = "SpeechRecognitionHelper"
private var isContinuousRecognitionActive = false
// 初始化 SDK
fun initialize(subscriptionKey: String, serviceRegion: String) {
try {
val config = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
// 设置识别语言,例如中文
config.speechRecognitionLanguage = "zh-CN"
recognizer = SpeechRecognizer(config)
Log.d(TAG, "Speech SDK initialized successfully")
} catch (e: Exception) {
Log.e(TAG, "初始化失败: ${e.message}")
}
}
// 开始一次性语音识别
fun recognizeOnce(callback: RecognizeCallback) {
if (recognizer == null) {
callback.onError("SpeechRecognizer 未初始化")
return
}
try {
// 使用同步方式调用,避免 CompletableFuture 的兼容性问题
val result = recognizer?.recognizeOnceAsync()?.get()
if (result != null) {
when (result.reason) {
ResultReason.RecognizedSpeech -> {
callback.onResult(result.text)
}
else -> {
callback.onError("识别失败,原因: ${result.reason}")
}
}
} else {
callback.onError("识别结果为空")
}
} catch (e: Exception) {
when (e) {
is InterruptedException, is ExecutionException -> {
Log.e(TAG, "识别异常: ${e.message}")
callback.onError("识别异常: ${e.message}")
}
else -> {
Log.e(TAG, "未知异常: ${e.message}")
callback.onError("未知异常: ${e.message}")
}
}
}
}
// 开始连续语音识别
fun startContinuousRecognition(callback: ContinuousRecognizeCallback) {
if (recognizer == null) {
callback.onError("SpeechRecognizer 未初始化")
return
}
if (isContinuousRecognitionActive) {
callback.onError("连续识别已经在进行中")
return
}
try {
// 设置识别事件处理
recognizer?.let { recognizer ->
// 设置识别事件处理
recognizer.recognized.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
callback.onResult(event.result.text)
}
}
)
// 设置识别中事件处理(实时反馈)
recognizer.recognizing.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) {
callback.onRecognizing(event.result.text)
}
}
)
// 设置会话开始事件处理
recognizer.sessionStarted.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
callback.onSessionStarted()
}
)
// 设置会话结束事件处理
recognizer.sessionStopped.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = false
callback.onSessionStopped()
}
)
// 设置取消事件处理
recognizer.canceled.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val reason = event.reason
val errorDetails = if (reason == CancellationReason.Error) event.errorDetails else ""
callback.onCanceled(reason.toString(), errorDetails)
isContinuousRecognitionActive = false
}
)
// 开始连续识别
recognizer.startContinuousRecognitionAsync().get()
isContinuousRecognitionActive = true
Log.d(TAG, "连续识别已开始")
}
} catch (e: Exception) {
Log.e(TAG, "开始连续识别失败: ${e.message}")
callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false
}
}
// 停止连续语音识别
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback) {
if (recognizer == null) {
callback.onError("SpeechRecognizer 未初始化")
return
}
if (!isContinuousRecognitionActive) {
callback.onError("连续识别未在进行中")
return
}
try {
// 停止连续识别
recognizer?.stopContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = false
Log.d(TAG, "连续识别已停止")
callback.onSessionStopped()
} catch (e: Exception) {
Log.e(TAG, "停止连续识别失败: ${e.message}")
callback.onError("停止连续识别失败: ${e.message}")
}
}
// 检查连续识别是否活跃
fun isContinuousRecognitionActive(): Boolean {
return isContinuousRecognitionActive
}
// 释放资源
fun dispose() {
try {
if (isContinuousRecognitionActive) {
recognizer?.stopContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = false
}
recognizer?.close()
recognizer = null
Log.d(TAG, "语音识别资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源失败: ${e.message}")
}
}
// 一次性识别回调接口
interface RecognizeCallback {
fun onResult(result: String)
fun onError(error: String)
}
// 连续识别回调接口
interface ContinuousRecognizeCallback {
fun onResult(result: String)
fun onRecognizing(recognizing: String)
fun onSessionStarted()
fun onSessionStopped()
fun onCanceled(reason: String, errorDetails: String)
fun onError(error: String)
}
}

493
android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt

@ -1,493 +0,0 @@
package com.example.deep_voice
import android.app.Activity
import android.content.Context
import android.util.Log
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import org.json.JSONObject
import java.io.File
/**
* 火山语音识别桥接类
* 负责与火山语音识别 SDK 交互,并将结果通过 MethodChannel 和 EventChannel 传递给 Flutter 层
*/
class VolcSpeechBridge(
private val activity: Activity,
messenger: BinaryMessenger
) : SpeechEngine.SpeechListener {
companion object {
private const val TAG = "VolcSpeechBridge"
private const val CHANNEL_NAME = "com.example.deep_voice/speech_recognition"
private const val EVENT_CHANNEL_NAME = "com.example.deep_voice/speech_recognition_events"
}
private var engine: SpeechEngine? = null
private var engineHandler: Long = 0
private var methodResult: Result? = null
private var eventSink: EventChannel.EventSink? = null
private var isInitialized = false
// 初始化 MethodChannel 和 EventChannel
init {
// 设置 MethodChannel 处理方法调用
MethodChannel(messenger, CHANNEL_NAME).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val appKey = call.argument<String>("appKey") ?: ""
val accessKey = call.argument<String>("accessKey") ?: ""
val cluster = call.argument<String>("cluster") ?: "volcano_asr"
initialize(appKey, accessKey, cluster, result)
}
"startRecognition" -> {
startRecognition(result)
}
"stopRecognition" -> {
stopRecognition(result)
}
"isInitialized" -> {
result.success(isInitialized)
}
"checkEngineCapabilities" -> {
checkEngineCapabilities(result)
}
else -> {
result.notImplemented()
}
}
}
// 设置 EventChannel 处理事件流
EventChannel(messenger, EVENT_CHANNEL_NAME).setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
})
}
/**
* 初始化火山语音识别引擎
*/
fun initialize(appKey: String, accessKey: String, cluster: String, result: Result) {
methodResult = result
try {
Log.i(TAG, "Initializing Volcengine Speech Engine")
// 前置操作:环境依赖
SpeechEngineGenerator.PrepareEnvironment(activity.applicationContext, activity.application)
// 创建引擎实例
engine = SpeechEngineGenerator.getInstance()
engineHandler = engine?.createEngine() ?: 0
if (engineHandler == 0L) {
Log.e(TAG, "Failed to create engine instance")
result.error("CREATE_ERROR", "Failed to create engine instance", null)
return
}
// 设置引擎类型
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING, SpeechEngineDefines.ASR_ENGINE)
// 设置用户ID(必填)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_UID_STRING, "deep_voice_user")
// 设置设备ID(选填)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "deep_voice_device")
// 设置日志级别
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING, SpeechEngineDefines.LOG_LEVEL_WARN)
// 禁用日志文件,避免权限问题
engine?.setOptionBoolean(engineHandler, "enable_log_file", false)
// 设置应用 ID 和访问密钥
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appKey)
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;$accessKey")
// 设置网络参数
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING, "wss://openspeech.bytedance.com")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING, "/api/v2/asr")
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING, cluster)
// 设置音频来源为内置录音机
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, SpeechEngineDefines.RECORDER_TYPE_RECORDER)
// 启用音量获取
engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ENABLE_GET_VOLUME_BOOL, true)
// 设置最大录音时长(60秒)
engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT, 60000)
// 控制识别效果
engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_SHOW_NLU_PUNC_BOOL, true) // 返回标点符号
engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_AUTO_STOP_BOOL, true) // 开启自动判停
// 设置结果返回形式为增量返回
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING, SpeechEngineDefines.ASR_RESULT_TYPE_SINGLE)
// 设置上下文
engine?.setContext(activity.applicationContext)
// 设置监听器
engine?.setListener(this)
// 初始化引擎
Log.i(TAG, "Calling initEngine()")
val ret = engine?.initEngine(engineHandler)
Log.i(TAG, "initEngine() returned: $ret")
if (ret == SpeechEngineDefines.ERR_NO_ERROR) {
isInitialized = true
Log.i(TAG, "Volcengine Speech Engine initialized successfully")
result.success("Engine initialized")
// 发送状态更新到 Flutter
sendEvent("status", "initialized")
} else {
Log.e(TAG, "Engine initialization failed with error code: $ret")
result.error("INIT_ERROR", "Engine initialization failed with error code: $ret", null)
}
} catch (e: Exception) {
Log.e(TAG, "Initialization failed", e)
result.error("INIT_ERROR", e.message, null)
}
}
/**
* 启动语音识别
*/
fun startRecognition(result: Result) {
try {
if (!isInitialized) {
Log.e(TAG, "Cannot start recognition: Engine not initialized")
result.error("NOT_INITIALIZED", "Speech engine is not initialized", null)
return
}
Log.i(TAG, "Starting speech recognition")
// 先调用同步停止,避免SDK内部异步线程带来的问题
engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 启动引擎
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
Log.d(TAG, "Start engine directive returned: $ret")
if (ret == SpeechEngineDefines.ERR_NO_ERROR) {
Log.i(TAG, "Successfully started recognition")
result.success("Recognition started")
// 状态会通过回调更新
} else {
Log.e(TAG, "Start recognition failed with error code: $ret")
result.error("START_ERROR", "Start recognition failed with error code: $ret", null)
}
} catch (e: Exception) {
Log.e(TAG, "Start recognition failed", e)
result.error("START_ERROR", e.message, null)
}
}
/**
* 停止语音识别
*/
fun stopRecognition(result: Result) {
try {
if (!isInitialized) {
Log.e(TAG, "Cannot stop recognition: Engine not initialized")
result.error("NOT_INITIALIZED", "Speech engine is not initialized", null)
return
}
Log.i(TAG, "Stopping speech recognition")
// 先发送音频输入完成指令
val finishRet = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "")
Log.d(TAG, "Finish talking directive returned: $finishRet")
// 然后停止引擎
val stopRet = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
Log.d(TAG, "Stop engine directive returned: $stopRet")
if (finishRet == SpeechEngineDefines.ERR_NO_ERROR || stopRet == SpeechEngineDefines.ERR_NO_ERROR) {
Log.i(TAG, "Successfully stopped recognition")
result.success("Recognition stopped")
// 状态会通过回调更新
} else {
Log.e(TAG, "Stop recognition failed with error codes: finish=$finishRet, stop=$stopRet")
result.error("STOP_ERROR", "Stop recognition failed", null)
}
} catch (e: Exception) {
Log.e(TAG, "Stop recognition failed", e)
result.error("STOP_ERROR", e.message, null)
}
}
/**
* 释放资源
*/
fun dispose() {
try {
if (engineHandler != 0L) {
engine?.destroyEngine(engineHandler)
engineHandler = 0
}
engine = null
isInitialized = false
Log.i(TAG, "Volcengine Speech Engine released")
} catch (e: Exception) {
Log.e(TAG, "Error releasing engine", e)
}
}
// ------------------
// 发送事件到 Flutter
// ------------------
private fun sendEvent(eventName: String, data: Any) {
try {
val eventData = JSONObject().apply {
put("event", eventName)
put("data", data)
}
eventSink?.success(eventData.toString())
} catch (e: Exception) {
Log.e(TAG, "Error sending event", e)
}
}
// ------------------
// SpeechListener 接口回调
// ------------------
/**
* 处理语音消息回调
* 这是 SpeechListener 接口的必须实现方法
*/
override fun onSpeechMessage(messageType: Int, messageData: ByteArray?, messageLength: Int) {
try {
Log.d(TAG, "Received speech message type: $messageType, length: $messageLength")
if (messageData != null) {
val dataString = try {
String(messageData, 0, messageLength)
} catch (e: Exception) {
"Unable to convert to string: ${e.message}"
}
Log.d(TAG, "Message data: $dataString")
}
when (messageType) {
// 引擎启动成功
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> {
Log.i(TAG, "Engine started successfully")
sendEvent("status", "listening")
}
// 引擎关闭
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> {
Log.i(TAG, "Engine stopped")
sendEvent("status", "idle")
}
// 错误信息
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> {
messageData?.let {
val errorJson = String(it, 0, messageLength)
Log.e(TAG, "Engine error: $errorJson")
try {
val jsonObject = JSONObject(errorJson)
val errorCode = jsonObject.optInt("err_code", -1)
val errorMsg = jsonObject.optString("err_msg", "Unknown error")
val reqId = jsonObject.optString("req_id", "")
Log.e(TAG, "Error details: code=$errorCode, message=$errorMsg, reqId=$reqId")
// 通知 Flutter 识别出错
val errorData = JSONObject()
errorData.put("code", errorCode)
errorData.put("message", errorMsg)
errorData.put("reqId", reqId)
sendEvent("error", errorData.toString())
} catch (e: Exception) {
Log.e(TAG, "Error parsing error JSON", e)
sendEvent("error", "Error code: Unknown")
}
}
}
// VAD 状态变化消息
SpeechEngineDefines.MESSAGE_TYPE_VAD_STATE -> {
val state = messageData?.let { String(it, 0, messageLength).toIntOrNull() } ?: -1
Log.i(TAG, "VAD state changed: $state")
val vadState = when (state) {
0 -> "silence"
1 -> "speech"
else -> "unknown"
}
sendEvent("vad", vadState)
}
// 中间识别结果
SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> {
messageData?.let {
val resultJson = String(it, 0, messageLength)
Log.d(TAG, "Partial recognition result: $resultJson")
try {
val jsonObject = JSONObject(resultJson)
if (jsonObject.has("result")) {
val resultArray = jsonObject.getJSONArray("result")
if (resultArray.length() > 0) {
val result = resultArray.getJSONObject(0)
val text = result.optString("text", "")
if (text.isNotEmpty()) {
Log.i(TAG, "Partial recognition text: $text")
// 发送识别结果到 Flutter
val resultData = JSONObject()
resultData.put("text", text)
resultData.put("isFinal", false)
sendEvent("result", resultData.toString())
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing partial result JSON", e)
}
}
}
// 最终识别结果
SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> {
messageData?.let {
val resultJson = String(it, 0, messageLength)
Log.d(TAG, "Final recognition result: $resultJson")
try {
val jsonObject = JSONObject(resultJson)
if (jsonObject.has("result")) {
val resultArray = jsonObject.getJSONArray("result")
if (resultArray.length() > 0) {
val result = resultArray.getJSONObject(0)
val text = result.optString("text", "")
if (text.isNotEmpty()) {
Log.i(TAG, "Final recognition text: $text")
// 发送识别结果到 Flutter
val resultData = JSONObject()
resultData.put("text", text)
resultData.put("isFinal", true)
sendEvent("result", resultData.toString())
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing final result JSON", e)
}
}
}
// 当前音量
SpeechEngineDefines.MESSAGE_TYPE_VOLUME -> {
val volume = messageData?.let { String(it, 0, messageLength).toFloatOrNull() } ?: 0f
// 将 [0-1] 的音量值转换为 [0-100] 的整数
val volumeInt = (volume * 100).toInt().coerceIn(0, 100)
Log.d(TAG, "Volume level: $volume (normalized: $volumeInt)")
// 发送音量变化事件到 Flutter
sendEvent("volume", volumeInt)
}
else -> {
Log.d(TAG, "Received unknown message type: $messageType")
// 尝试解析未知消息类型的数据
messageData?.let {
try {
val dataString = String(it, 0, messageLength)
Log.d(TAG, "Unknown message data: $dataString")
} catch (e: Exception) {
Log.e(TAG, "Error parsing unknown message data", e)
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error processing speech message", e)
}
}
/**
* 检查引擎能力和支持的指令
*/
fun checkEngineCapabilities(result: Result) {
try {
Log.i(TAG, "Checking engine capabilities")
if (engine == null) {
Log.e(TAG, "Engine is null, cannot check capabilities")
result.error("ENGINE_NULL", "Engine is null", null)
return
}
// 获取引擎版本信息
val version = engine?.getVersion() ?: "Unknown"
Log.i(TAG, "Engine version: $version")
// 创建能力信息对象
val capabilities = JSONObject()
capabilities.put("version", version)
// 列出支持的指令
val supportedDirectives = JSONObject()
supportedDirectives.put("DIRECTIVE_START_ENGINE", SpeechEngineDefines.DIRECTIVE_START_ENGINE)
supportedDirectives.put("DIRECTIVE_STOP_ENGINE", SpeechEngineDefines.DIRECTIVE_STOP_ENGINE)
supportedDirectives.put("DIRECTIVE_FINISH_TALKING", SpeechEngineDefines.DIRECTIVE_FINISH_TALKING)
supportedDirectives.put("DIRECTIVE_SYNC_STOP_ENGINE", SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE)
supportedDirectives.put("DIRECTIVE_UPDATE_ASR_HOTWORDS", SpeechEngineDefines.DIRECTIVE_UPDATE_ASR_HOTWORDS)
capabilities.put("supported_directives", supportedDirectives)
// 列出支持的消息类型
val supportedMessageTypes = JSONObject()
supportedMessageTypes.put("MESSAGE_TYPE_ENGINE_START", SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START)
supportedMessageTypes.put("MESSAGE_TYPE_ENGINE_STOP", SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP)
supportedMessageTypes.put("MESSAGE_TYPE_ENGINE_ERROR", SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR)
supportedMessageTypes.put("MESSAGE_TYPE_PARTIAL_RESULT", SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT)
supportedMessageTypes.put("MESSAGE_TYPE_FINAL_RESULT", SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT)
supportedMessageTypes.put("MESSAGE_TYPE_VOLUME", SpeechEngineDefines.MESSAGE_TYPE_VOLUME)
supportedMessageTypes.put("MESSAGE_TYPE_VAD_STATE_CHANGED", SpeechEngineDefines.MESSAGE_TYPE_VAD_STATE)
capabilities.put("supported_message_types", supportedMessageTypes)
// 返回结果
result.success(capabilities.toString())
} catch (e: Exception) {
Log.e(TAG, "Error checking engine capabilities", e)
result.error("CHECK_ERROR", e.message, null)
}
}
}

544
lib/data/services/voice_recognition_service.dart

@ -1,391 +1,281 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'dart:developer' as developer;
/// 火山语音识别服务
/// 负责与原生层的火山语音识别引擎交互
class VoiceRecognitionService extends GetxService {
// 方法通道,用于调用原生方法
static const MethodChannel _methodChannel =
MethodChannel('com.example.deep_voice/speech_recognition');
// 事件通道,用于接收原生层的事件
static const EventChannel _eventChannel =
EventChannel('com.example.deep_voice/speech_recognition_events');
// 状态变量
final isInitialized = false.obs;
final isListening = false.obs;
final isConnecting = true.obs;
// 识别结果
final recognizedText = ''.obs;
// 回调函数
Function(String)? onRecognitionResult;
Function(String)? onError;
Function()? onRecognitionComplete;
Function(String)? onConnectionStatusChanged;
Function(String)? onSentenceComplete;
// 事件流订阅
/// 微软语音识别服务
///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class VoiceRecognitionService {
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/speech_recognition');
static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/speech_recognition_events');
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 火山语音识别的 AppKey 和 AccessKey
late final String _appKey;
late final String _accessKey;
late final String _cluster;
// 公开的事件流
Stream<RecognitionEvent>? _recognitionStream;
Stream<RecognitionEvent>? get recognitionStream => _recognitionStream;
@override
void onInit() {
developer.log('VoiceRecognitionService: 初始化服务', name: 'VoiceRecognition');
super.onInit();
_loadCredentials();
_setupEventListener();
}
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
/// 加载火山语音识别的凭证
void _loadCredentials() {
try {
_appKey = dotenv.env['VOLC_SPEECH_APP_KEY'] ?? '';
_accessKey = dotenv.env['VOLC_SPEECH_ACCESS_KEY'] ?? '';
_cluster = dotenv.env['VOLC_SPEECH_CLUSTER'] ?? 'volcano_asr';
VoiceRecognitionService() {
_loadConfig();
}
developer.log('VoiceRecognitionService: 加载凭证 - AppKey: ${_appKey.isNotEmpty ? '已设置' : '未设置'}, AccessKey: ${_accessKey.isNotEmpty ? '已设置' : '未设置'}, Cluster: $_cluster', name: 'VoiceRecognition');
/// 从环境变量加载配置
void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
if (_appKey.isEmpty || _accessKey.isEmpty) {
developer.log('VoiceRecognitionService: 警告 - 火山语音识别凭证未设置,请在 .env 文件中设置 VOLC_SPEECH_APP_KEY 和 VOLC_SPEECH_ACCESS_KEY', name: 'VoiceRecognition');
}
} catch (e, stack) {
developer.log('VoiceRecognitionService: 加载火山语音识别凭证失败: $e', name: 'VoiceRecognition');
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
}
/// 设置事件监听器
void _setupEventListener() {
/// 初始化微软语音识别 SDK
///
/// 返回 true 表示初始化成功,否则抛出 PlatformException
Future<bool> initialize() async {
if (_isInitialized) return true;
try {
developer.log('VoiceRecognitionService: 设置事件监听器', name: 'VoiceRecognition');
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
(dynamic event) {
_handleEvent(event.toString());
},
onError: (dynamic error) {
developer.log('VoiceRecognitionService: 事件通道错误: $error', name: 'VoiceRecognition');
if (onError != null) {
onError!('事件通道错误: $error');
}
},
);
} catch (e, stack) {
developer.log('VoiceRecognitionService: 设置事件监听器失败: $e', name: 'VoiceRecognition');
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey,
'serviceRegion': _serviceRegion,
});
_isInitialized = result;
return result;
} on PlatformException catch (e) {
print('语音识别初始化失败: ${e.message}');
_isInitialized = false;
throw e;
}
}
/// 处理来自原生层的事件
void _handleEvent(String eventJson) {
/// 执行一次性语音识别
///
/// 返回识别的文本,如果识别失败则抛出 PlatformException
Future<String> recognizeSpeech() async {
if (!_isInitialized) {
throw Exception('语音识别服务未初始化,请先调用 initialize()');
}
try {
developer.log('VoiceRecognitionService: 收到原始事件数据: $eventJson', name: 'VoiceRecognition');
final eventData = jsonDecode(eventJson);
final eventName = eventData['event'];
final data = eventData['data'];
developer.log('VoiceRecognitionService: 收到事件: $eventName, 数据: $data', name: 'VoiceRecognition');
switch (eventName) {
case 'status':
_handleStatusEvent(data.toString());
break;
case 'result':
_handleResultEvent(data.toString());
break;
case 'error':
_handleErrorEvent(data.toString());
break;
case 'vad':
_handleVadEvent(data.toString());
break;
default:
developer.log('VoiceRecognitionService: 未知事件类型: $eventName', name: 'VoiceRecognition');
break;
}
} catch (e, stack) {
developer.log('VoiceRecognitionService: 处理事件失败: $e', name: 'VoiceRecognition');
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
final String result = await _channel.invokeMethod('recognizeOnce');
return result;
} on PlatformException catch (e) {
print('语音识别失败: ${e.message}');
throw e;
}
}
/// 处理状态事件
void _handleStatusEvent(String status) {
developer.log('VoiceRecognitionService: 状态变更: $status', name: 'VoiceRecognition');
switch (status) {
case 'initialized':
isInitialized.value = true;
isConnecting.value = false;
developer.log('VoiceRecognitionService: 引擎初始化完成', name: 'VoiceRecognition');
break;
case 'listening':
isListening.value = true;
developer.log('VoiceRecognitionService: 开始监听', name: 'VoiceRecognition');
break;
case 'idle':
isListening.value = false;
developer.log('VoiceRecognitionService: 停止监听', name: 'VoiceRecognition');
break;
/// 开始连续语音识别
///
/// 返回一个包含识别事件的流,如果开始失败则抛出 PlatformException
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
throw Exception('语音识别服务未初始化,请先调用 initialize()');
}
if (onConnectionStatusChanged != null) {
onConnectionStatusChanged!(status);
if (_isContinuousRecognitionActive) {
throw Exception('连续语音识别已经在进行中');
}
}
/// 处理识别结果事件
void _handleResultEvent(String resultJson) {
try {
final resultData = jsonDecode(resultJson);
final text = resultData['text'];
final isFinal = resultData['isFinal'];
developer.log('VoiceRecognitionService: 识别结果: $text, 是否最终结果: $isFinal', name: 'VoiceRecognition');
// 更新识别文本
recognizedText.value = text;
// 调用回调函数
if (onRecognitionResult != null) {
onRecognitionResult!(text);
}
// 如果是最终结果,调用完成回调
if (isFinal && text.isNotEmpty) {
developer.log('VoiceRecognitionService: 句子识别完成: $text', name: 'VoiceRecognition');
// 创建事件流控制器
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 设置事件监听
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.listen(_handleRecognitionEvent, onError: _handleRecognitionError);
// 开始连续识别
final bool result = await _channel.invokeMethod('startContinuousRecognition');
_isContinuousRecognitionActive = result;
// 设置公开的流
_recognitionStream = _eventStreamController!.stream;
return _recognitionStream!;
} on PlatformException catch (e) {
print('开始连续语音识别失败: ${e.message}');
_cleanupEventStream();
throw e;
}
}
if (onSentenceComplete != null) {
onSentenceComplete!(text);
}
/// 停止连续语音识别
///
/// 返回 true 表示停止成功,否则抛出 PlatformException
Future<bool> stopContinuousRecognition() async {
if (!_isInitialized) {
throw Exception('语音识别服务未初始化,请先调用 initialize()');
}
if (onRecognitionComplete != null) {
onRecognitionComplete!();
}
}
} catch (e, stack) {
developer.log('VoiceRecognitionService: 处理识别结果失败: $e', name: 'VoiceRecognition');
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
if (!_isContinuousRecognitionActive) {
return true; // 已经停止,直接返回成功
}
}
/// 处理错误事件
void _handleErrorEvent(String errorJson) {
try {
final errorData = jsonDecode(errorJson);
final errorMessage = errorData['message'];
final errorCode = errorData['code'] ?? 'unknown';
final bool result = await _channel.invokeMethod('stopContinuousRecognition');
_isContinuousRecognitionActive = !result;
developer.log('VoiceRecognitionService: 语音识别错误: 代码=$errorCode, 消息=$errorMessage', name: 'VoiceRecognition', error: errorMessage);
// 清理事件流
_cleanupEventStream();
if (onError != null) {
onError!(errorMessage);
}
} catch (e, stack) {
developer.log('VoiceRecognitionService: 处理错误事件失败: $e', name: 'VoiceRecognition');
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
return result;
} on PlatformException catch (e) {
print('停止连续语音识别失败: ${e.message}');
throw e;
}
}
/// 处理 VAD 事件
void _handleVadEvent(String vadState) {
// 可以根据需要处理 VAD 状态变化
developer.log('VoiceRecognitionService: VAD 状态: $vadState', name: 'VoiceRecognition');
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 检查引擎能力和支持的指令
Future<void> checkEngineCapabilities() async {
try {
developer.log('VoiceRecognitionService: 检查引擎能力', name: 'VoiceRecognition');
// 检查是否已初始化
if (!isInitialized.value) {
developer.log('VoiceRecognitionService: 引擎未初始化,尝试初始化', name: 'VoiceRecognition');
await initialize();
}
// 调用原生方法检查引擎能力
final result = await _methodChannel.invokeMethod<String>('checkEngineCapabilities');
developer.log('VoiceRecognitionService: 引擎能力检查结果: $result', name: 'VoiceRecognition');
if (result != null) {
try {
final capabilities = jsonDecode(result);
developer.log('VoiceRecognitionService: 引擎版本: ${capabilities['version']}', name: 'VoiceRecognition');
if (capabilities['supported_params'] != null) {
developer.log('VoiceRecognitionService: 支持的参数: ${capabilities['supported_params']}', name: 'VoiceRecognition');
}
if (capabilities['supported_directives'] != null) {
developer.log('VoiceRecognitionService: 支持的指令: ${capabilities['supported_directives']}', name: 'VoiceRecognition');
}
} catch (e) {
developer.log('VoiceRecognitionService: 解析能力结果失败: $e', name: 'VoiceRecognition');
}
}
} catch (e, stack) {
developer.log('VoiceRecognitionService: 检查引擎能力失败: $e', name: 'VoiceRecognition', error: e);
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
}
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['eventType'] as String? ?? '';
switch (eventType) {
case 'finalResult':
final String text = eventMap['text'] as String? ?? '';
_latestRecognizedText = text;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
));
break;
/// 初始化语音识别引擎
Future<void> initialize() async {
try {
developer.log('VoiceRecognitionService: 开始初始化语音引擎', name: 'VoiceRecognition');
isConnecting.value = true;
case 'intermediateResult':
final String text = eventMap['text'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: text,
));
break;
// 检查凭证
if (_appKey.isEmpty || _accessKey.isEmpty) {
developer.log('VoiceRecognitionService: 凭证未设置,无法初始化', name: 'VoiceRecognition', error: '火山语音识别凭证未设置');
throw '火山语音识别凭证未设置';
}
case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
break;
// 添加详细日志
developer.log('VoiceRecognitionService: 使用凭证 AppKey: $_appKey, AccessKey: ${_accessKey.substring(0, 4)}***, Cluster: $_cluster', name: 'VoiceRecognition');
// 调用原生方法初始化引擎
try {
final result = await _methodChannel.invokeMethod<String>(
'initialize',
{
'appKey': _appKey,
'accessKey': _accessKey,
'cluster': _cluster,
},
);
developer.log('VoiceRecognitionService: 初始化结果: $result', name: 'VoiceRecognition');
// 初始化成功后,检查引擎能力
await checkEngineCapabilities();
} catch (e) {
developer.log('VoiceRecognitionService: 初始化方法调用失败: $e', name: 'VoiceRecognition', error: e);
// 尝试检查引擎是否已初始化
try {
final isEngineInitialized = await _methodChannel.invokeMethod<bool>('isInitialized') ?? false;
if (isEngineInitialized) {
developer.log('VoiceRecognitionService: 引擎已经初始化,忽略错误', name: 'VoiceRecognition');
isInitialized.value = true;
isConnecting.value = false;
// 引擎已初始化,检查引擎能力
await checkEngineCapabilities();
return;
}
} catch (checkError) {
developer.log('VoiceRecognitionService: 检查引擎初始化状态失败: $checkError', name: 'VoiceRecognition');
}
// 如果检查失败或引擎未初始化,则重新抛出异常
rethrow;
}
case 'sessionStopped':
_isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped,
));
break;
// 初始化成功后,状态会通过事件通道更新
} catch (e, stack) {
isConnecting.value = false;
developer.log('VoiceRecognitionService: 初始化语音识别引擎失败: $e', name: 'VoiceRecognition', error: e);
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
String errorMessage = '初始化失败: $e';
// 提供更具体的错误信息
if (e.toString().contains('error code: -7')) {
errorMessage = '初始化失败: 无法创建日志目录,请检查应用权限';
developer.log('VoiceRecognitionService: 错误代码 -7: 无法创建日志目录', name: 'VoiceRecognition');
} else if (e.toString().contains('INIT_ERROR')) {
errorMessage = '初始化失败: 引擎初始化错误,请检查网络连接和凭证';
developer.log('VoiceRecognitionService: INIT_ERROR: 引擎初始化错误', name: 'VoiceRecognition');
}
case 'canceled':
_isContinuousRecognitionActive = false;
final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.canceled,
error: '$reason: $errorDetails',
));
break;
if (onError != null) {
onError!(errorMessage);
}
rethrow;
case 'error':
final String error = eventMap['error'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break;
}
}
/// 开始语音识别
Future<void> startRecognition() async {
try {
developer.log('VoiceRecognitionService: 开始语音识别', name: 'VoiceRecognition');
// 检查是否已初始化
if (!isInitialized.value) {
developer.log('VoiceRecognitionService: 引擎未初始化,尝试初始化', name: 'VoiceRecognition');
await initialize();
}
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
_eventStreamController?.addError(error);
_cleanupEventStream();
}
developer.log('VoiceRecognitionService: 调用原生方法 startRecognition', name: 'VoiceRecognition');
/// 清理事件流资源
void _cleanupEventStream() {
_eventSubscription?.cancel();
_eventSubscription = null;
// 调用原生方法开始识别
final result = await _methodChannel.invokeMethod<String>('startRecognition');
developer.log('VoiceRecognitionService: 开始识别结果: $result', name: 'VoiceRecognition');
_eventStreamController?.close();
_eventStreamController = null;
// 状态会通过事件通道更新
} catch (e, stack) {
developer.log('VoiceRecognitionService: 开始语音识别失败: $e', name: 'VoiceRecognition', error: e);
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
_recognitionStream = null;
_isContinuousRecognitionActive = false;
}
if (onError != null) {
onError!('开始识别失败: $e');
/// 释放资源
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
rethrow;
await _channel.invokeMethod('dispose');
_cleanupEventStream();
_isInitialized = false;
} catch (e) {
print('释放语音识别资源失败: $e');
}
}
}
/// 停止语音识别
Future<void> stopRecognition() async {
try {
developer.log('VoiceRecognitionService: 停止语音识别', name: 'VoiceRecognition');
/// 识别事件类型
enum RecognitionEventType {
/// 最终识别结果
finalResult,
// 检查是否已初始化
if (!isInitialized.value) {
developer.log('VoiceRecognitionService: 引擎未初始化,无法停止识别', name: 'VoiceRecognition');
return;
}
/// 中间识别结果(实时反馈)
intermediateResult,
developer.log('VoiceRecognitionService: 调用原生方法 stopRecognition', name: 'VoiceRecognition');
/// 会话开始
sessionStarted,
// 调用原生方法停止识别
final result = await _methodChannel.invokeMethod<String>('stopRecognition');
developer.log('VoiceRecognitionService: 停止识别结果: $result', name: 'VoiceRecognition');
/// 会话结束
sessionStopped,
// 状态会通过事件通道更新
} catch (e, stack) {
developer.log('VoiceRecognitionService: 停止语音识别失败: $e', name: 'VoiceRecognition', error: e);
developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition');
/// 识别取消
canceled,
if (onError != null) {
onError!('停止识别失败: $e');
}
}
}
/// 识别错误
error,
}
@override
void onClose() {
developer.log('VoiceRecognitionService: 关闭服务', name: 'VoiceRecognition');
/// 识别事件
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
// 取消事件订阅
_eventSubscription?.cancel();
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
// 停止识别
stopRecognition();
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
super.onClose();
RecognitionEvent({
required this.type,
this.text = '',
this.error = '',
});
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, error: $error}';
}
}

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

@ -2,11 +2,11 @@ import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'dart:convert';
import 'package:flutter/rendering.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../models/message_model.dart';
import '../controllers/voice_input_controller.dart';
import 'package:flutter/services.dart';
class ChatController extends GetxController {
String agentId = '';
@ -42,23 +42,72 @@ class ChatController extends GetxController {
final isVoiceMuted = false.obs;
Message? _currentVoiceMessage;
// 输入框状态
final isInputCollapsed = false.obs;
final isVoiceMode = true.obs; // 默认为语音模式
final isRecordingVoice = false.obs; // 是否正在录音(按住说话状态)
// 添加是否在底部的状态变量
final RxBool isAtBottom = true.obs;
// 切换输入框折叠状态
void toggleInputCollapsed() {
isInputCollapsed.value = !isInputCollapsed.value;
}
// 切换语音/文本输入模式
void toggleInputMode() {
isVoiceMode.value = !isVoiceMode.value;
}
// 开始按住说话
void startPressToTalk() {
isRecordingVoice.value = true;
startVoiceInput();
}
// 结束按住说话
void endPressToTalk() {
isRecordingVoice.value = false;
stopVoiceInput();
}
@override
void onInit() {
super.onInit();
messageController = TextEditingController();
scrollController = ScrollController();
// 确保清理任何可能存在的语音输入控制器
if (Get.isRegistered<VoiceInputController>()) {
try {
final voiceController = Get.find<VoiceInputController>();
voiceController.stopRecording();
Get.delete<VoiceInputController>();
} catch (e) {
debugPrint('Error cleaning up existing VoiceInputController: $e');
}
}
_initTtsService();
_initFromArguments();
_initializeController();
// 延迟生成问候语,确保页面已完全加载
if (playVoiceOnEnter) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 添加滚动监听器
scrollController.addListener(_scrollListener);
// 确保在页面加载完成后滚动到底部
_ensureScrollToBottom();
// 添加帧回调,确保在页面完全渲染后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
// 延迟执行,确保页面已完全加载
Future.delayed(const Duration(milliseconds: 300), () {
if (!_isDisposed) {
generateSimpleGreeting();
_scrollToBottom(animate: true);
}
});
}
});
}
void _initFromArguments() {
@ -117,7 +166,7 @@ class ChatController extends GetxController {
await _generatePersonalizedGreeting();
// 保存聊天记录并立即滚动到底部
await _saveChatHistory();
_scrollToBottom();
_scrollToBottom(animate: true);
}
} catch (e) {
print('显示欢迎消息失败: $e');
@ -221,7 +270,7 @@ class ChatController extends GetxController {
// 如果有历史消息,等待下一帧再滚动到底部
if (messages.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
_scrollToBottom(animate: true);
});
}
}
@ -248,9 +297,24 @@ class ChatController extends GetxController {
}
}
void _scrollToBottom() {
void _scrollToBottom({bool animate = false}) {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
if (animate) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} else {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
} else {
// 如果 scrollController 还没有附加到 ListView,延迟执行
Future.delayed(const Duration(milliseconds: 100), () {
if (!_isDisposed) {
_scrollToBottom(animate: animate);
}
});
}
}
@ -268,7 +332,7 @@ class ChatController extends GetxController {
);
// 只在非初始化阶段滚动
if (!playVoiceOnEnter || content.isNotEmpty) {
_scrollToBottom();
_scrollToBottom(animate: false);
}
}
}
@ -295,7 +359,7 @@ class ChatController extends GetxController {
messages.add(userMessage);
clearMessage();
await _saveChatHistory();
_scrollToBottom();
_scrollToBottom(animate: true);
isLoading.value = true;
try {
@ -458,11 +522,39 @@ class ChatController extends GetxController {
// 开始语音输入
void startVoiceInput() {
// 确保先停止任何可能正在进行的语音识别会话
if (Get.isRegistered<VoiceInputController>()) {
final voiceController = Get.find<VoiceInputController>();
voiceController.stopRecording();
Get.delete<VoiceInputController>();
}
isVoiceInputVisible.value = true;
isVoiceConnecting.value = true;
isRecording.value = false;
recordingText.value = '';
// 当语音面板打开时,确保ListView滚动到适当位置,防止最后的消息被遮挡
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) {
// 计算需要额外滚动的距离(语音面板高度)
final extraScrollDistance = 120.0;
// 获取当前滚动位置
final currentPosition = scrollController.position.pixels;
final maxScrollExtent = scrollController.position.maxScrollExtent;
// 如果已经接近底部,则向上滚动一定距离,确保最后的消息可见
if (maxScrollExtent - currentPosition < extraScrollDistance) {
scrollController.animateTo(
currentPosition + extraScrollDistance,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
});
// 创建语音输入控制器
Get.put(VoiceInputController(
onRecordingResult: handleVoiceResult,
@ -488,6 +580,16 @@ class ChatController extends GetxController {
}
void stopVoiceInput() {
// 确保停止语音识别
if (Get.isRegistered<VoiceInputController>()) {
try {
final voiceController = Get.find<VoiceInputController>();
voiceController.stopRecording();
} catch (e) {
debugPrint('Error stopping voice recording: $e');
}
}
isVoiceInputVisible.value = false;
isVoiceConnecting.value = true;
isRecording.value = false;
@ -499,6 +601,14 @@ class ChatController extends GetxController {
}
_currentVoiceMessage = null;
// 当语音面板关闭时,确保ListView滚动回适当位置
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) {
// 滚动到底部,确保最新消息可见
_scrollToBottom(animate: true);
}
});
// 删除语音输入控制器
try {
if (Get.isRegistered<VoiceInputController>()) {
@ -507,54 +617,151 @@ class ChatController extends GetxController {
} catch (e) {
debugPrint('Error deleting VoiceInputController: $e');
}
// 确保按住说话状态被重置
isRecordingVoice.value = false;
}
void handleRecognizing(String text) {
if (text.isNotEmpty) {
// 如果还没有创建临时消息,创建一个
if (_currentVoiceMessage == null) {
_currentVoiceMessage = Message(
role: 'user',
content: '🎤 $text',
timestamp: DateTime.now(),
);
messages.add(_currentVoiceMessage!);
} else {
// 更新已存在的临时消息
final index = messages.indexOf(_currentVoiceMessage!);
if (index != -1) {
messages[index] = Message(
role: 'user',
content: '🎤 $text',
timestamp: _currentVoiceMessage!.timestamp,
);
}
}
_scrollToBottom();
// 更新识别中的文本,但不创建消息
recordingText.value = text;
}
}
void handleVoiceResult(String text) {
if (text.isNotEmpty) {
if (_currentVoiceMessage != null) {
final index = messages.indexOf(_currentVoiceMessage!);
if (index != -1) {
messages[index] = Message(
role: 'user',
content: text,
timestamp: _currentVoiceMessage!.timestamp,
);
// 创建一个新的用户消息
final userMessage = Message(
role: 'user',
content: text,
timestamp: DateTime.now(),
);
// 添加到消息列表
messages.add(userMessage);
// 保存聊天历史
_saveChatHistory();
// 滚动到底部
_scrollToBottom(animate: true);
// 发送给AI处理
_processAIResponse(userMessage);
}
}
// 处理AI响应
Future<void> _processAIResponse(Message userMessage) async {
if (_isDisposed) return;
try {
isLoading.value = true;
currentStreamMessage.value = '';
_currentAssistantMessage = Message(
role: 'assistant',
content: '',
timestamp: DateTime.now(),
);
messages.add(_currentAssistantMessage!);
_pendingTtsText = '';
await for (final chunk in _aiService.sendMessageStream(
messages: messages
.map((m) => {
'role': m.role,
'content': m.content,
})
.toList(),
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
currentStreamMessage.value += chunk;
_updateAssistantMessage(currentStreamMessage.value);
// 累积文本并合成
_pendingTtsText += chunk;
if (!_isDisposed && _ttsService.isEnabled.value) {
// 检查是否达到最小长度
if (_pendingTtsText.length >= _minTtsLength) {
// 找到最后一个句子结束的位置
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText);
if (lastSentenceEnd > 0) {
// 播放到最后一个句子结束的位置
String textToSpeak =
_pendingTtsText.substring(0, lastSentenceEnd + 1);
_ttsService.speak(textToSpeak);
// 保留剩余的文本
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
}
}
}
}
// 发送消息
messageController.text = text;
sendMessage();
messageController.clear();
// 处理剩余的文本
if (!_isDisposed &&
_ttsService.isEnabled.value &&
_pendingTtsText.isNotEmpty) {
_ttsService.speak(_pendingTtsText);
}
_pendingTtsText = '';
_scrollToBottom();
if (!_isDisposed) {
await _saveChatHistory();
}
} catch (e) {
if (!_isDisposed) {
if (_currentAssistantMessage != null) {
messages.remove(_currentAssistantMessage);
}
Get.snackbar(
'Error',
'Failed to get response from AI: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
} finally {
if (!_isDisposed) {
_currentAssistantMessage = null;
currentStreamMessage.value = '';
isLoading.value = false;
}
}
}
// 确保滚动到底部的方法,使用多种策略确保成功
void _ensureScrollToBottom() {
// 立即尝试滚动
_scrollToBottom();
// 延迟100ms后再次尝试滚动(等待视图构建)
Future.delayed(const Duration(milliseconds: 100), () {
if (!_isDisposed) _scrollToBottom(animate: true);
});
// 延迟500ms后再次尝试滚动(确保所有元素都已加载)
Future.delayed(const Duration(milliseconds: 500), () {
if (!_isDisposed) _scrollToBottom(animate: true);
});
// 使用帧回调确保在渲染后滚动
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_isDisposed) _scrollToBottom(animate: true);
});
}
// 滚动监听器
void _scrollListener() {
if (_isDisposed) return;
// 检测是否接近底部
if (scrollController.hasClients) {
final maxScroll = scrollController.position.maxScrollExtent;
final currentScroll = scrollController.offset;
isAtBottom.value = (maxScroll - currentScroll) < 50;
}
// 不再在这里停止语音输入,让用户可以继续说下一句
_currentVoiceMessage = null;
}
}

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

@ -1,5 +1,7 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import '../../../data/services/voice_recognition_service.dart';
class VoiceInputController extends GetxController {
// Observable states
@ -8,6 +10,12 @@ class VoiceInputController extends GetxController {
final isRecording = false.obs;
final recognizedText = ''.obs;
// 语音识别服务
late final VoiceRecognitionService _voiceService;
// 连续识别相关
StreamSubscription? _recognitionSubscription;
// Callbacks
final Function(String) onRecordingResult;
final VoidCallback onClosePanel;
@ -22,30 +30,161 @@ class VoiceInputController extends GetxController {
@override
void onInit() {
super.onInit();
// 初始化完成后更新状态
isConnecting.value = false;
_initializeVoiceService();
}
Future<void> _initializeVoiceService() async {
try {
// 创建语音识别服务实例
_voiceService = VoiceRecognitionService();
// 初始化语音识别服务
await _voiceService.initialize();
// 初始化完成后更新状态
isConnecting.value = false;
// 自动开始连续语音识别
await startContinuousRecognition();
} catch (e) {
print('初始化语音识别服务失败: $e');
Get.snackbar(
'Error',
'初始化语音识别服务失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> startRecording() async {
Future<void> startContinuousRecognition() async {
if (isConnecting.value || isMuted.value) return;
if (isRecording.value) return; // 已经在录音中
try {
isRecording.value = true;
recognizedText.value = '';
// 开始连续语音识别
final stream = await _voiceService.startContinuousRecognition();
// 订阅识别事件流
_recognitionSubscription = stream.listen((event) {
switch (event.type) {
case RecognitionEventType.intermediateResult:
// 更新中间识别结果
recognizedText.value = event.text;
// 调用识别中回调
onRecognizing(event.text);
break;
case RecognitionEventType.finalResult:
// 更新最终识别结果
if (event.text.isNotEmpty) {
recognizedText.value = event.text;
// 调用最终结果回调,发送识别到的句子
onRecordingResult(event.text);
// 清空识别文本,准备下一句
recognizedText.value = '';
// 不停止录音,继续监听下一句
}
break;
case RecognitionEventType.error:
case RecognitionEventType.canceled:
// 处理错误
print('语音识别错误: ${event.error}');
Get.snackbar(
'Error',
'语音识别错误: ${event.error}',
snackPosition: SnackPosition.BOTTOM,
);
// 尝试重新启动识别
restartRecognition();
break;
case RecognitionEventType.sessionStopped:
// 会话结束,尝试重新启动
isRecording.value = false;
restartRecognition();
break;
default:
break;
}
}, onError: (error) {
print('语音识别流错误: $error');
Get.snackbar(
'Error',
'语音识别流错误: $error',
snackPosition: SnackPosition.BOTTOM,
);
isRecording.value = false;
// 尝试重新启动识别
restartRecognition();
});
} catch (e) {
print('语音识别失败: $e');
Get.snackbar(
'Error',
'启动语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
isRecording.value = false;
}
}
// 重新启动识别
Future<void> restartRecognition() async {
if (isMuted.value) return;
try {
// 先停止当前识别
await stopRecording();
// 延迟一下再重新启动
await Future.delayed(const Duration(milliseconds: 500));
// 重新启动连续识别
await startContinuousRecognition();
} catch (e) {
print('重新启动语音识别失败: $e');
}
}
Future<void> stopRecording() async {
if (!isRecording.value) return;
isRecording.value = false;
try {
// 取消事件订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
// 停止连续识别
if (_voiceService.isContinuousRecognitionActive()) {
await _voiceService.stopContinuousRecognition();
}
isRecording.value = false;
} catch (e) {
print('停止语音识别失败: $e');
isRecording.value = false;
}
}
void toggleMute() {
isMuted.value = !isMuted.value;
if (isRecording.value && isMuted.value) {
stopRecording();
} else if (!isMuted.value && !isRecording.value) {
// 如果取消静音,自动开始识别
startContinuousRecognition();
}
}
// 手动开始录音(用户点击按钮)
void startRecording() {
if (!isRecording.value) {
startContinuousRecognition();
}
}
@override

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

@ -13,8 +13,15 @@ class ChatView extends GetView<ChatController> {
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// 停止TTS服务
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
// 停止语音输入
if (controller.isVoiceInputVisible.value) {
controller.stopVoiceInput();
}
return true;
},
child: Scaffold(
@ -45,8 +52,15 @@ class ChatView extends GetView<ChatController> {
size: 20.sp,
),
onPressed: () async {
// 停止TTS服务
final tts = Get.find<VolcanoTtsService>();
await tts.stop();
// 停止语音输入
if (controller.isVoiceInputVisible.value) {
controller.stopVoiceInput();
}
Get.back();
},
),
@ -103,7 +117,9 @@ class ChatView extends GetView<ChatController> {
controller: controller.scrollController,
padding: EdgeInsets.only(
top: 16.h,
bottom: controller.isVoiceInputVisible.value ? 140.h : 80.h,
bottom: controller.isVoiceInputVisible.value
? 200.h
: (controller.isInputCollapsed.value ? 70.h : 90.h),
),
itemCount: controller.messages.length,
itemBuilder: (context, index) => _buildMessage(controller.messages[index]),
@ -114,7 +130,9 @@ class ChatView extends GetView<ChatController> {
Obx(() {
if (!controller.isLoading.value) return const SizedBox.shrink();
return Positioned(
bottom: controller.isVoiceInputVisible.value ? 150.h : 90.h,
bottom: controller.isVoiceInputVisible.value
? 210.h
: (controller.isInputCollapsed.value ? 80.h : 100.h),
left: 0,
right: 0,
child: Container(
@ -172,48 +190,202 @@ class ChatView extends GetView<ChatController> {
),
),
),
child: Row(
children: [
Expanded(
child: Obx(() {
if (controller.isRecordingVoice.value) {
return _buildRecordingInput();
} else if (!controller.isVoiceMode.value) {
return _buildExpandedInput();
} else {
return _buildDefaultInput();
}
}),
);
}
Widget _buildDefaultInput() {
return Row(
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[100],
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: IconButton(
icon: Icon(
Icons.camera_alt,
color: Colors.grey[600],
size: 20.sp,
),
onPressed: () {
// 拍照功能
},
padding: EdgeInsets.zero,
),
),
SizedBox(width: 8.w),
Expanded(
child: GestureDetector(
onTap: () {
// 点击打开VoiceInputPanel
controller.startVoiceInput();
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w),
height: 48.h,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: Row(
children: [
IconButton(
icon: Icon(
Icons.mic,
color: Colors.grey[600],
size: 24.sp,
),
onPressed: controller.startVoiceInput,
),
Expanded(
child: TextField(
controller: controller.messageController,
decoration: const InputDecoration(
hintText: '输入消息...',
border: InputBorder.none,
),
maxLines: null,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
onSubmitted: (_) => controller.sendMessage(),
),
child: Center(
child: Text(
'点击开始语音识别',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16.sp,
),
IconButton(
icon: const Icon(Icons.send),
color: const Color(0xFF4A6FE5),
onPressed: controller.sendMessage,
),
],
),
),
),
),
],
),
SizedBox(width: 8.w),
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[100],
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: IconButton(
icon: Icon(
Icons.keyboard,
color: Colors.grey[600],
size: 20.sp,
),
onPressed: controller.toggleInputMode,
padding: EdgeInsets.zero,
),
),
],
);
}
Widget _buildExpandedInput() {
return Row(
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[100],
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: IconButton(
icon: Icon(
Icons.mic,
color: const Color(0xFF4A6FE5),
size: 20.sp,
),
onPressed: controller.toggleInputMode,
padding: EdgeInsets.zero,
),
),
SizedBox(width: 8.w),
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: TextField(
controller: controller.messageController,
decoration: InputDecoration(
hintText: '输入消息...',
hintStyle: TextStyle(
color: Colors.grey[400],
fontSize: 14.sp,
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12.h),
),
maxLines: 5,
minLines: 1,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
textInputAction: TextInputAction.send,
onSubmitted: (_) => controller.sendMessage(),
),
),
),
SizedBox(width: 8.w),
Container(
width: 40.w,
height: 40.w,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF4A6FE5),
),
child: IconButton(
icon: Icon(
Icons.send,
color: Colors.white,
size: 20.sp,
),
onPressed: controller.sendMessage,
padding: EdgeInsets.zero,
),
),
],
);
}
Widget _buildRecordingInput() {
return Container(
height: 48.h,
decoration: BoxDecoration(
color: const Color(0xFF4A6FE5),
borderRadius: BorderRadius.circular(24.r),
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
40,
(index) => Container(
width: 4.w,
height: 4.h,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.r),
),
),
),
),
),
);
}

217
lib/modules/chat/widgets/voice_input_panel.dart

@ -18,113 +18,152 @@ class VoiceInputPanel extends GetView<VoiceInputController> {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white,
Colors.pink[50]!.withOpacity(0.3),
Color(0xFFFFFFFF), // 纯白色开始
Color(0xFFE8D5F5), // 更深的紫色结束
],
stops: [0.3, 1.0], // 调整颜色分布,让白色占据更多空间
),
),
child: Container(
height: 120.h + MediaQuery.of(context).padding.bottom,
padding: EdgeInsets.only(
left: 40.w,
right: 40.w,
top: 12.h,
bottom: 12.h + MediaQuery.of(context).padding.bottom,
),
child: Row(
children: [
// 左侧静音开关
Obx(() => Container(
width: 52.w,
height: 52.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: controller.isMuted.value ? Colors.grey[200] : Colors.white,
border: Border.all(
color: controller.isMuted.value ? Colors.grey[400]! : Colors.grey[300]!,
width: 1,
),
),
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
controller.isMuted.value ? Icons.mic_off_rounded : Icons.mic_rounded,
color: controller.isConnecting.value ? Colors.grey : Colors.black87,
size: 28.sp,
),
onPressed: controller.isConnecting.value ? null : controller.toggleMute,
),
)),
SizedBox(width: 20.w),
// 中间文字
Expanded(
child: Center(
child: Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
controller.isConnecting.value ? '连接中...' : '你可以开始说话',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: controller.isConnecting.value ? Colors.grey[500] : Colors.black87,
child: SafeArea(
child: Container(
height: 120.h,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要内容:麦克风图标、音频波形、关闭按钮
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 左侧麦克风图标
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Center(
child: Icon(
Icons.mic,
color: Colors.purple.shade300,
size: 28.sp,
),
),
),
),
if (controller.isConnecting.value) ...[
SizedBox(width: 8.w),
SizedBox(
width: 32.w,
child: Row(
children: List.generate(
3,
(index) => Container(
width: 4.w,
height: 4.w,
margin: EdgeInsets.only(right: 4.w),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[400],
),
// 中间音频波形
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildAudioWaveform(),
SizedBox(height: 10.h),
// 文字提示
Text(
'你可以开始说话',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
// 右侧关闭按钮
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Center(
child: IconButton(
icon: Icon(
Icons.close,
color: Colors.grey.shade600,
size: 28.sp,
),
onPressed: onClose,
padding: EdgeInsets.zero,
),
),
),
],
],
)),
),
),
SizedBox(width: 20.w),
// 右侧关闭按钮
Container(
width: 52.w,
height: 52.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(
color: Colors.grey[300]!,
width: 1,
),
),
),
child: IconButton(
padding: EdgeInsets.zero,
icon: Icon(
Icons.close_rounded,
color: Colors.black87,
size: 28.sp,
// 底部进度条
Padding(
padding: EdgeInsets.only(top: 15.h),
child: Container(
width: 250.w,
height: 4.h,
decoration: BoxDecoration(
color: Colors.purple.withOpacity(0.2),
borderRadius: BorderRadius.circular(2.r),
),
),
),
onPressed: onClose,
),
],
),
],
),
),
),
);
}
Widget _buildAudioWaveform() {
return Obx(() {
// 根据控制器状态动态生成波形高度
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
11,
(index) {
// 创建随机高度效果,模拟声波
double height = controller.isRecording.value
? (4 + (index % 3) * 3).h // 当正在录音时显示不同高度
: 4.h; // 默认高度
return Container(
width: 4.w,
height: height,
margin: EdgeInsets.symmetric(horizontal: 2.w),
decoration: BoxDecoration(
color: controller.isRecording.value
? Colors.purple.shade300 // 录音时使用紫色
: Colors.grey.shade400, // 默认使用灰色
borderRadius: BorderRadius.circular(2.r),
),
);
},
),
);
});
}
}

578
lib/modules/speech_demo/speech_demo_page.dart

@ -8,6 +8,7 @@ import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../data/services/voice_recognition_service.dart';
@ -19,388 +20,67 @@ class SpeechDemoPage extends StatefulWidget {
}
class _SpeechDemoPageState extends State<SpeechDemoPage> {
final VoiceRecognitionService _recognitionService = Get.find<VoiceRecognitionService>();
final RxBool _permissionsGranted = false.obs;
final RxBool _isInitializing = false.obs;
final RxBool _isRecognizing = false.obs;
final RxString _errorMessage = ''.obs;
final RxString _recognizedText = ''.obs;
final RxString _status = ''.obs;
// 添加原生权限通道 - 只用于处理Android 11+的MANAGE_EXTERNAL_STORAGE权限
static const MethodChannel _permissionChannel = MethodChannel('com.example.deep_voice/permissions');
late final VoiceRecognitionService _voiceService;
bool _isInitialized = false;
bool _isRecognizing = false;
String _recognizedText = '';
String _statusMessage = '准备就绪';
@override
void initState() {
super.initState();
_setupPermissionListener();
_checkPermissions();
}
@override
void dispose() {
_recognitionService.stopRecognition();
super.dispose();
_initializeService();
}
// 设置权限监听器 - 只监听MANAGE_EXTERNAL_STORAGE权限结果
void _setupPermissionListener() {
_permissionChannel.setMethodCallHandler((call) async {
if (call.method == 'manageExternalStorageResult') {
developer.log('Received manageExternalStorageResult from native: ${call.arguments}');
// 更新权限状态
await _checkPermissions();
}
return null;
Future<void> _initializeService() async {
setState(() {
_statusMessage = '正在初始化语音服务...';
});
}
// 检查所有必要的权限
Future<void> _checkPermissions() async {
developer.log('Checking permissions');
try {
// 检查基本权限
final micStatus = await Permission.microphone.status;
final storageStatus = await Permission.storage.status;
developer.log('=== 详细权限状态 ===');
developer.log('麦克风权限: $micStatus');
developer.log('存储权限: $storageStatus');
// 对于Android 11+,检查MANAGE_EXTERNAL_STORAGE权限
bool manageStorageGranted = true;
if (Platform.isAndroid) {
if (await _isAndroid11OrHigher()) {
// 使用原生方法检查MANAGE_EXTERNAL_STORAGE权限
try {
manageStorageGranted = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
} catch (e) {
developer.log('Error checking MANAGE_EXTERNAL_STORAGE: $e');
manageStorageGranted = await Permission.manageExternalStorage.isGranted;
}
developer.log('管理所有文件权限: $manageStorageGranted');
}
}
// 记录其他权限状态
developer.log('读取外部存储权限: ${await Permission.audio.status}');
developer.log('写入外部存储权限: ${await Permission.photos.status}');
developer.log('=====================');
// 更新权限状态
final bool basicPermissionsGranted = micStatus.isGranted && storageStatus.isGranted;
// 修改逻辑:如果是Android 11+且已授予MANAGE_EXTERNAL_STORAGE权限,则忽略普通存储权限状态
bool effectiveStoragePermission = storageStatus.isGranted;
if (Platform.isAndroid && await _isAndroid11OrHigher() && manageStorageGranted) {
developer.log('已授予MANAGE_EXTERNAL_STORAGE权限,忽略普通存储权限状态');
effectiveStoragePermission = true;
}
_permissionsGranted.value = micStatus.isGranted && (effectiveStoragePermission || manageStorageGranted);
developer.log('所有权限已授予: ${_permissionsGranted.value}');
// 如果权限已授予,初始化引擎
if (_permissionsGranted.value && !_isInitializing.value) {
_initializeEngine();
} else if (!_permissionsGranted.value) {
_errorMessage.value = '需要麦克风和存储权限才能使用语音识别功能';
}
} catch (e) {
developer.log('检查权限时出错: $e');
_errorMessage.value = '检查权限时出错: $e';
}
}
// 检查是否为Android 11或更高版本
Future<bool> _isAndroid11OrHigher() async {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
return androidInfo.version.sdkInt >= 30; // Android 11 is API 30
}
return false;
}
// 记录详细的权限状态
Future<void> _logPermissionDetails() async {
developer.log('=== 详细权限状态 ===');
developer.log('麦克风权限: ${await Permission.microphone.status}');
developer.log('存储权限: ${await Permission.storage.status}');
if (Platform.isAndroid) {
if (await _isAndroid11OrHigher()) {
bool manageStorageGranted = false;
try {
manageStorageGranted = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
} catch (e) {
developer.log('Error checking MANAGE_EXTERNAL_STORAGE: $e');
manageStorageGranted = await Permission.manageExternalStorage.isGranted;
}
developer.log('管理所有文件权限: $manageStorageGranted');
}
}
developer.log('读取外部存储权限: ${await Permission.audio.status}');
developer.log('写入外部存储权限: ${await Permission.photos.status}');
developer.log('=====================');
}
// 请求所有必要的权限
Future<void> _requestPermissions() async {
developer.log('请求权限');
try {
// 对于Android 11+,优先请求MANAGE_EXTERNAL_STORAGE权限
if (Platform.isAndroid && await _isAndroid11OrHigher()) {
bool isManager = false;
try {
isManager = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
} catch (e) {
developer.log('检查MANAGE_EXTERNAL_STORAGE时出错: $e');
isManager = await Permission.manageExternalStorage.isGranted;
}
if (!isManager) {
developer.log('请求MANAGE_EXTERNAL_STORAGE权限');
// 使用原生方法请求MANAGE_EXTERNAL_STORAGE权限
await _permissionChannel.invokeMethod('requestManageExternalStorage');
// 等待一段时间,让用户有机会授予权限
await Future.delayed(const Duration(seconds: 1));
// 创建服务实例(这一步会自动从环境变量加载配置)
_voiceService = VoiceRecognitionService();
// 再次检查权限状态
try {
isManager = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false;
developer.log('MANAGE_EXTERNAL_STORAGE权限状态: $isManager');
} catch (e) {
developer.log('检查MANAGE_EXTERNAL_STORAGE时出错: $e');
}
}
}
// 初始化语音识别服务
final result = await _voiceService.initialize();
// 请求基本权限
Map<Permission, PermissionStatus> statuses = await [
Permission.microphone,
Permission.storage,
].request();
// 记录结果
statuses.forEach((permission, status) {
developer.log('权限 $permission: $status');
setState(() {
_isInitialized = result;
_statusMessage = '语音服务初始化成功,可以开始识别';
});
// 检查权限状态
await _checkPermissions();
} catch (e) {
developer.log('请求权限时出错: $e');
_errorMessage.value = '请求权限时出错: $e';
}
}
// 打开应用设置
Future<void> _openAppSettings() async {
developer.log('打开应用设置');
try {
// 使用permission_handler打开设置
final opened = await openAppSettings();
if (!opened) {
// 如果permission_handler失败,使用原生方法
await _permissionChannel.invokeMethod('openAppSettings');
}
} catch (e) {
developer.log('打开应用设置时出错: $e');
// 使用原生方法作为备选
try {
await _permissionChannel.invokeMethod('openAppSettings');
} catch (e2) {
developer.log('通过原生通道打开应用设置时出错: $e2');
}
}
}
// 初始化语音引擎
Future<void> _initializeEngine() async {
if (_isInitializing.value) return;
_isInitializing.value = true;
_errorMessage.value = '';
_status.value = '正在初始化语音引擎...';
try {
// 再次检查权限状态
await _checkPermissions();
if (!_permissionsGranted.value) {
developer.log('权限未授予,无法初始化引擎');
_errorMessage.value = '权限未授予,无法初始化语音引擎';
_isInitializing.value = false;
return;
}
// 记录权限详情
await _logPermissionDetails();
// 测试存储权限
String? availableDir = await _getAvailableStorageDirectory();
if (availableDir != null) {
developer.log('找到可用的存储目录: $availableDir');
} else {
developer.log('警告: 未找到可用的存储目录,可能会影响语音引擎初始化');
}
// 初始化语音引擎
developer.log('初始化语音识别引擎');
await _recognitionService.initialize();
developer.log('语音识别引擎初始化成功');
_status.value = '语音引擎初始化成功,可以开始识别';
_isInitializing.value = false;
} catch (e, stackTrace) {
developer.log('初始化语音识别引擎时出错: $e');
developer.log('堆栈跟踪: $stackTrace');
// 解析错误代码
String errorDetail = '未知错误';
if (e is PlatformException) {
developer.log('PlatformException 代码: ${e.code}, 消息: ${e.message}');
if (e.code == 'INIT_ERROR') {
if (e.message?.contains('-7') == true) {
errorDetail = '无法创建日志目录 (错误码: -7),可能是存储权限问题';
} else {
errorDetail = '初始化错误: ${e.message}';
}
}
}
_errorMessage.value = '初始化语音引擎失败: $errorDetail';
_status.value = '初始化失败';
_isInitializing.value = false;
// 显示错误提示和重试选项
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('初始化失败: $errorDetail'),
action: SnackBarAction(
label: '重试',
onPressed: _initializeEngine,
),
duration: const Duration(seconds: 10),
),
);
setState(() {
_isInitialized = false;
_statusMessage = '初始化失败: $e';
});
}
}
Future<void> _startRecognition() async {
developer.log('开始识别');
_errorMessage.value = '';
_isRecognizing.value = true;
_status.value = '正在识别...';
try {
await _recognitionService.startRecognition();
// 设置监听器接收识别结果
_recognitionService.recognizedText.listen((text) {
_recognizedText.value = text;
Future<void> _startVoiceRecognition() async {
if (!_isInitialized) {
setState(() {
_statusMessage = '语音服务未初始化,请先初始化';
});
} catch (e) {
developer.log('开始识别时出错: $e');
_errorMessage.value = '启动识别失败: $e';
_isRecognizing.value = false;
_status.value = '识别启动失败';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('启动识别失败: $e'),
duration: const Duration(seconds: 3),
),
);
return;
}
}
Future<void> _stopRecognition() async {
developer.log('停止识别');
try {
await _recognitionService.stopRecognition();
_isRecognizing.value = false;
_status.value = '识别已停止';
// 保存最终识别结果
_recognizedText.value = _recognitionService.recognizedText.value;
} catch (e) {
developer.log('停止识别时出错: $e');
_errorMessage.value = '停止识别失败: $e';
_isRecognizing.value = false;
_status.value = '识别停止失败';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('停止识别失败: $e'),
duration: const Duration(seconds: 3),
),
);
}
}
setState(() {
_isRecognizing = true;
_statusMessage = '正在聆听...';
});
// 获取可用的存储目录
Future<String?> _getAvailableStorageDirectory() async {
try {
List<Directory> directoriesToTry = [];
// 应用专用外部存储目录
if (Platform.isAndroid) {
directoriesToTry.add(Directory('/storage/emulated/0/Android/data/com.example.deep_voice/files'));
}
// 应用内部存储目录
try {
final appDir = await getApplicationDocumentsDirectory();
directoriesToTry.add(appDir);
} catch (e) {
developer.log('获取应用内部存储目录失败: $e');
}
// 应用缓存目录
try {
final cacheDir = await getTemporaryDirectory();
directoriesToTry.add(cacheDir);
} catch (e) {
developer.log('获取应用缓存目录失败: $e');
}
// 尝试每个目录
for (var directory in directoriesToTry) {
try {
developer.log('测试目录可用性: ${directory.path}');
if (!await directory.exists()) {
await directory.create(recursive: true);
}
final testFile = File('${directory.path}/test_write.tmp');
await testFile.writeAsString('Test write at ${DateTime.now()}');
await testFile.readAsString(); // 验证可读
await testFile.delete();
developer.log('找到可用目录: ${directory.path}');
return directory.path;
} catch (e) {
developer.log('目录 ${directory.path} 不可用: $e');
}
}
developer.log('未找到可用的存储目录');
return null;
final result = await _voiceService.recognizeSpeech();
setState(() {
_recognizedText = result;
_isRecognizing = false;
_statusMessage = '识别完成';
});
} catch (e) {
developer.log('获取可用存储目录时出错: $e');
return null;
setState(() {
_isRecognizing = false;
_statusMessage = '识别失败: $e';
});
}
}
@ -415,126 +95,86 @@ class _SpeechDemoPageState extends State<SpeechDemoPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 权限请求部分
Obx(() => !_permissionsGranted.value
? Card(
color: Colors.amber.shade100,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'需要麦克风和存储权限',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
const Text(
'注意: 在Android 11及以上版本,您需要在设置中手动授予"管理所有文件"权限',
style: TextStyle(
fontSize: 14,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.center,
children: [
ElevatedButton(
onPressed: _requestPermissions,
child: const Text('授予权限'),
),
ElevatedButton(
onPressed: _openAppSettings,
child: const Text('打开设置'),
),
ElevatedButton(
onPressed: _logPermissionDetails,
child: const Text('检查权限'),
),
ElevatedButton(
onPressed: _initializeEngine,
child: const Text('强制初始化'),
),
],
),
],
),
),
)
: const SizedBox.shrink()),
// 状态显示
Obx(() => _status.value.isNotEmpty
? Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'状态: ${_status.value}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
)
: const SizedBox.shrink()),
// 状态信息
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_statusMessage,
style: TextStyle(
color: _isInitialized ? Colors.green[700] : Colors.red[700],
fontWeight: FontWeight.bold,
),
),
),
// 错误消息
Obx(() => _errorMessage.value.isNotEmpty
? Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'错误: ${_errorMessage.value}',
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
)
: const SizedBox.shrink()),
const SizedBox(height: 24),
// 识别结果显示区域
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Obx(() => Text(
_recognizedText.value.isEmpty
? '识别结果将显示在这里...'
: _recognizedText.value,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: _recognizedText.isEmpty
? const Center(
child: Text(
'识别结果将显示在这里',
style: TextStyle(color: Colors.grey),
),
)
: SingleChildScrollView(
child: Text(
_recognizedText,
style: const TextStyle(
fontSize: 18.0,
fontSize: 18,
height: 1.5,
),
)),
),
),
),
),
const SizedBox(height: 24),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _isRecognizing ? null : _initializeService,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Text('重新初始化'),
),
),
ElevatedButton(
onPressed: _isRecognizing || !_isInitialized ? null : _startVoiceRecognition,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
backgroundColor: Colors.blue[700],
),
child: Text(_isRecognizing ? '正在识别...' : '开始语音识别'),
),
],
),
// 控制按钮
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Obx(() => ElevatedButton(
onPressed: _permissionsGranted.value && !_isRecognizing.value && !_isInitializing.value
? _startRecognition
: null,
child: const Text('开始识别'),
)),
Obx(() => ElevatedButton(
onPressed: _isRecognizing.value ? _stopRecognition : null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('停止识别'),
)),
],
const SizedBox(height: 16),
// 提示信息
const Text(
'提示:请在安静的环境中使用,并确保已授予应用录音权限。',
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
],
),

Loading…
Cancel
Save