From 94aa62ab3cb24605a00249de89ccb23d95bffc9e Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 25 Feb 2025 15:07:38 +0000 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90ms=20=E8=AF=AD=E9=9F=B3sdk?= =?UTF-8?q?=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle.kts | 6 +- .../com/example/deep_voice/MainActivity.kt | 214 ++++--- .../deep_voice/SpeechRecognitionHelper.kt | 194 ++++++ .../example/deep_voice/VolcSpeechBridge.kt | 493 --------------- .../services/voice_recognition_service.dart | 572 +++++++---------- .../chat/controllers/chat_controller.dart | 307 +++++++-- .../controllers/voice_input_controller.dart | 147 ++++- lib/modules/chat/views/chat_view.dart | 242 +++++-- .../chat/widgets/voice_input_panel.dart | 217 ++++--- lib/modules/speech_demo/speech_demo_page.dart | 588 ++++-------------- 10 files changed, 1417 insertions(+), 1563 deletions(-) create mode 100644 android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt delete mode 100644 android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2a0b701cc..7da3502ce 100644 --- a/android/app/build.gradle.kts +++ b/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 { diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index 2cac0769c..d1c44bb99 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/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) + "initialize" -> { + val subscriptionKey = call.argument("subscriptionKey") + val serviceRegion = call.argument("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) + } catch (e: Exception) { + result.error("INITIALIZATION_ERROR", e.message, null) + } + } + "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) + } } - "requestManageExternalStorage" -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - requestManageExternalStorage() + "startContinuousRecognition" -> { + try { + if (speechHelper.isContinuousRecognitionActive()) { + result.error("ALREADY_ACTIVE", "连续识别已经在进行中", null) + return@setMethodCallHandler + } + + speechHelper.startContinuousRecognition(object : SpeechRecognitionHelper.ContinuousRecognizeCallback { + override fun onResult(text: String) { + sendEvent(mapOf( + "eventType" to "finalResult", + "text" to text + )) + } + + 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) - } else { - result.success(false) + } catch (e: Exception) { + result.error("RECOGNITION_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 + "stopContinuousRecognition" -> { + try { + if (!speechHelper.isContinuousRecognitionActive()) { + result.error("NOT_ACTIVE", "连续识别未在进行中", null) + return@setMethodCallHandler + } + + 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() + else -> { + result.notImplemented() + } } } - // 准备火山语音识别环境 - 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) - } - - // 初始化火山语音识别桥接类 - volcSpeechBridge = VolcSpeechBridge(this, flutterEngine.dartExecutor.binaryMessenger) - } - - 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) + // 设置事件通道 + EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } } - } + ) } - 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"}") - - // 通知Flutter层权限状态变化 - permissionChannel.invokeMethod("manageExternalStorageResult", granted) - } - } else { - super.onActivityResult(requestCode, resultCode, data) + private fun sendEvent(event: Map) { + runOnUiThread { + eventSink?.success(event) } } - + override fun onDestroy() { - // 释放火山语音识别资源 - volcSpeechBridge?.dispose() - volcSpeechBridge = null + speechHelper.dispose() super.onDestroy() } -} +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/SpeechRecognitionHelper.kt new file mode 100644 index 000000000..488bd0f88 --- /dev/null +++ b/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 { _, event -> + if (event.result.reason == ResultReason.RecognizedSpeech) { + callback.onResult(event.result.text) + } + } + ) + + // 设置识别中事件处理(实时反馈) + recognizer.recognizing.addEventListener( + EventHandler { _, event -> + if (event.result.reason == ResultReason.RecognizingSpeech) { + callback.onRecognizing(event.result.text) + } + } + ) + + // 设置会话开始事件处理 + recognizer.sessionStarted.addEventListener( + EventHandler { _, _ -> + callback.onSessionStarted() + } + ) + + // 设置会话结束事件处理 + recognizer.sessionStopped.addEventListener( + EventHandler { _, _ -> + isContinuousRecognitionActive = false + callback.onSessionStopped() + } + ) + + // 设置取消事件处理 + recognizer.canceled.addEventListener( + EventHandler { _, 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) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt b/android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt deleted file mode 100644 index 26c2db086..000000000 --- a/android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt +++ /dev/null @@ -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("appKey") ?: "" - val accessKey = call.argument("accessKey") ?: "" - val cluster = call.argument("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) - } - } -} \ No newline at end of file diff --git a/lib/data/services/voice_recognition_service.dart b/lib/data/services/voice_recognition_service.dart index c69d63d5e..904dc0281 100644 --- a/lib/data/services/voice_recognition_service.dart +++ b/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'); +/// 微软语音识别服务 +/// +/// 该服务提供了通过平台通道与 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'); - // 事件通道,用于接收原生层的事件 - static const EventChannel _eventChannel = - EventChannel('com.example.deep_voice/speech_recognition_events'); - - // 状态变量 - final isInitialized = false.obs; - final isListening = false.obs; - final isConnecting = true.obs; + bool _isInitialized = false; + late final String _subscriptionKey; + late final String _serviceRegion; - // 识别结果 - final recognizedText = ''.obs; + // 连续识别相关 + bool _isContinuousRecognitionActive = false; + StreamController? _eventStreamController; + StreamSubscription? _eventSubscription; - // 回调函数 - Function(String)? onRecognitionResult; - Function(String)? onError; - Function()? onRecognitionComplete; - Function(String)? onConnectionStatusChanged; - Function(String)? onSentenceComplete; + // 公开的事件流 + Stream? _recognitionStream; + Stream? get recognitionStream => _recognitionStream; - // 事件流订阅 - StreamSubscription? _eventSubscription; + // 最新的识别结果 + String _latestRecognizedText = ''; + String get latestRecognizedText => _latestRecognizedText; - // 火山语音识别的 AppKey 和 AccessKey - late final String _appKey; - late final String _accessKey; - late final String _cluster; - - @override - void onInit() { - developer.log('VoiceRecognitionService: 初始化服务', name: 'VoiceRecognition'); - super.onInit(); - _loadCredentials(); - _setupEventListener(); + VoiceRecognitionService() { + _loadConfig(); } - - /// 加载火山语音识别的凭证 - 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'; - - developer.log('VoiceRecognitionService: 加载凭证 - AppKey: ${_appKey.isNotEmpty ? '已设置' : '未设置'}, AccessKey: ${_accessKey.isNotEmpty ? '已设置' : '未设置'}, Cluster: $_cluster', name: 'VoiceRecognition'); - - 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'); + + /// 从环境变量加载配置 + void _loadConfig() { + _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; + + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { + throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); } } - - /// 设置事件监听器 - void _setupEventListener() { + + /// 初始化微软语音识别 SDK + /// + /// 返回 true 表示初始化成功,否则抛出 PlatformException + Future 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 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> 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']; + // 创建事件流控制器 + _eventStreamController = StreamController.broadcast(); - developer.log('VoiceRecognitionService: 识别结果: $text, 是否最终结果: $isFinal', name: 'VoiceRecognition'); + // 设置事件监听 + _eventSubscription = _eventChannel + .receiveBroadcastStream() + .listen(_handleRecognitionEvent, onError: _handleRecognitionError); - // 更新识别文本 - recognizedText.value = text; + // 开始连续识别 + final bool result = await _channel.invokeMethod('startContinuousRecognition'); + _isContinuousRecognitionActive = result; - // 调用回调函数 - if (onRecognitionResult != null) { - onRecognitionResult!(text); - } + // 设置公开的流 + _recognitionStream = _eventStreamController!.stream; - // 如果是最终结果,调用完成回调 - if (isFinal && text.isNotEmpty) { - developer.log('VoiceRecognitionService: 句子识别完成: $text', name: 'VoiceRecognition'); - - if (onSentenceComplete != null) { - onSentenceComplete!(text); - } - - if (onRecognitionComplete != null) { - onRecognitionComplete!(); - } - } - } catch (e, stack) { - developer.log('VoiceRecognitionService: 处理识别结果失败: $e', name: 'VoiceRecognition'); - developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); + return _recognitionStream!; + } on PlatformException catch (e) { + print('开始连续语音识别失败: ${e.message}'); + _cleanupEventStream(); + throw e; } } - - /// 处理错误事件 - void _handleErrorEvent(String errorJson) { - try { - final errorData = jsonDecode(errorJson); - final errorMessage = errorData['message']; - final errorCode = errorData['code'] ?? 'unknown'; - - developer.log('VoiceRecognitionService: 语音识别错误: 代码=$errorCode, 消息=$errorMessage', name: 'VoiceRecognition', error: errorMessage); - - if (onError != null) { - onError!(errorMessage); - } - } catch (e, stack) { - developer.log('VoiceRecognitionService: 处理错误事件失败: $e', name: 'VoiceRecognition'); - developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); + + /// 停止连续语音识别 + /// + /// 返回 true 表示停止成功,否则抛出 PlatformException + Future stopContinuousRecognition() async { + if (!_isInitialized) { + throw Exception('语音识别服务未初始化,请先调用 initialize()'); } - } - - /// 处理 VAD 事件 - void _handleVadEvent(String vadState) { - // 可以根据需要处理 VAD 状态变化 - developer.log('VoiceRecognitionService: VAD 状态: $vadState', name: 'VoiceRecognition'); - } - - /// 检查引擎能力和支持的指令 - Future checkEngineCapabilities() async { + + if (!_isContinuousRecognitionActive) { + return true; // 已经停止,直接返回成功 + } + try { - developer.log('VoiceRecognitionService: 检查引擎能力', name: 'VoiceRecognition'); - - // 检查是否已初始化 - if (!isInitialized.value) { - developer.log('VoiceRecognitionService: 引擎未初始化,尝试初始化', name: 'VoiceRecognition'); - await initialize(); - } + final bool result = await _channel.invokeMethod('stopContinuousRecognition'); + _isContinuousRecognitionActive = !result; - // 调用原生方法检查引擎能力 - final result = await _methodChannel.invokeMethod('checkEngineCapabilities'); - developer.log('VoiceRecognitionService: 引擎能力检查结果: $result', name: 'VoiceRecognition'); + // 清理事件流 + _cleanupEventStream(); - 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'); + return result; + } on PlatformException catch (e) { + print('停止连续语音识别失败: ${e.message}'); + throw e; } } - - /// 初始化语音识别引擎 - Future initialize() async { - try { - developer.log('VoiceRecognitionService: 开始初始化语音引擎', name: 'VoiceRecognition'); - isConnecting.value = true; - - // 检查凭证 - if (_appKey.isEmpty || _accessKey.isEmpty) { - developer.log('VoiceRecognitionService: 凭证未设置,无法初始化', name: 'VoiceRecognition', error: '火山语音识别凭证未设置'); - throw '火山语音识别凭证未设置'; - } - - // 添加详细日志 - developer.log('VoiceRecognitionService: 使用凭证 AppKey: $_appKey, AccessKey: ${_accessKey.substring(0, 4)}***, Cluster: $_cluster', name: 'VoiceRecognition'); - - // 调用原生方法初始化引擎 - try { - final result = await _methodChannel.invokeMethod( - 'initialize', - { - 'appKey': _appKey, - 'accessKey': _accessKey, - 'cluster': _cluster, - }, - ); + + /// 检查连续识别是否活跃 + bool isContinuousRecognitionActive() { + return _isContinuousRecognitionActive; + } + + /// 处理来自原生端的识别事件 + void _handleRecognitionEvent(dynamic event) { + if (event is! Map) return; + + final Map 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; - developer.log('VoiceRecognitionService: 初始化结果: $result', name: 'VoiceRecognition'); + case 'intermediateResult': + final String text = eventMap['text'] as String? ?? ''; + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.intermediateResult, + text: text, + )); + break; - // 初始化成功后,检查引擎能力 - await checkEngineCapabilities(); - } catch (e) { - developer.log('VoiceRecognitionService: 初始化方法调用失败: $e', name: 'VoiceRecognition', error: e); + case 'sessionStarted': + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.sessionStarted, + )); + break; - // 尝试检查引擎是否已初始化 - try { - final isEngineInitialized = await _methodChannel.invokeMethod('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'); - } + case 'sessionStopped': + _isContinuousRecognitionActive = false; + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.sessionStopped, + )); + break; - // 如果检查失败或引擎未初始化,则重新抛出异常 - rethrow; - } - - // 初始化成功后,状态会通过事件通道更新 - } 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'); - } - - if (onError != null) { - onError!(errorMessage); - } - rethrow; + case 'canceled': + _isContinuousRecognitionActive = false; + final String reason = eventMap['reason'] as String? ?? ''; + final String errorDetails = eventMap['errorDetails'] as String? ?? ''; + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.canceled, + error: '$reason: $errorDetails', + )); + break; + + case 'error': + final String error = eventMap['error'] as String? ?? ''; + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.error, + error: error, + )); + break; } } - - /// 开始语音识别 - Future startRecognition() async { - try { - developer.log('VoiceRecognitionService: 开始语音识别', name: 'VoiceRecognition'); - - // 检查是否已初始化 - if (!isInitialized.value) { - developer.log('VoiceRecognitionService: 引擎未初始化,尝试初始化', name: 'VoiceRecognition'); - await initialize(); - } - - developer.log('VoiceRecognitionService: 调用原生方法 startRecognition', name: 'VoiceRecognition'); - - // 调用原生方法开始识别 - final result = await _methodChannel.invokeMethod('startRecognition'); - developer.log('VoiceRecognitionService: 开始识别结果: $result', name: 'VoiceRecognition'); - - // 状态会通过事件通道更新 - } catch (e, stack) { - developer.log('VoiceRecognitionService: 开始语音识别失败: $e', name: 'VoiceRecognition', error: e); - developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); - - if (onError != null) { - onError!('开始识别失败: $e'); - } - rethrow; - } + + /// 处理识别事件流错误 + void _handleRecognitionError(Object error) { + _eventStreamController?.addError(error); + _cleanupEventStream(); } - - /// 停止语音识别 - Future stopRecognition() async { + + /// 清理事件流资源 + void _cleanupEventStream() { + _eventSubscription?.cancel(); + _eventSubscription = null; + + _eventStreamController?.close(); + _eventStreamController = null; + + _recognitionStream = null; + _isContinuousRecognitionActive = false; + } + + /// 释放资源 + Future dispose() async { try { - developer.log('VoiceRecognitionService: 停止语音识别', name: 'VoiceRecognition'); - - // 检查是否已初始化 - if (!isInitialized.value) { - developer.log('VoiceRecognitionService: 引擎未初始化,无法停止识别', name: 'VoiceRecognition'); - return; + if (_isContinuousRecognitionActive) { + await stopContinuousRecognition(); } - developer.log('VoiceRecognitionService: 调用原生方法 stopRecognition', name: 'VoiceRecognition'); - - // 调用原生方法停止识别 - final result = await _methodChannel.invokeMethod('stopRecognition'); - developer.log('VoiceRecognitionService: 停止识别结果: $result', name: 'VoiceRecognition'); - - // 状态会通过事件通道更新 - } catch (e, stack) { - developer.log('VoiceRecognitionService: 停止语音识别失败: $e', name: 'VoiceRecognition', error: e); - developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); - - if (onError != null) { - onError!('停止识别失败: $e'); - } + await _channel.invokeMethod('dispose'); + _cleanupEventStream(); + _isInitialized = false; + } catch (e) { + print('释放语音识别资源失败: $e'); } } +} +/// 识别事件类型 +enum RecognitionEventType { + /// 最终识别结果 + finalResult, + + /// 中间识别结果(实时反馈) + intermediateResult, + + /// 会话开始 + sessionStarted, + + /// 会话结束 + sessionStopped, + + /// 识别取消 + canceled, + + /// 识别错误 + error, +} + +/// 识别事件 +class RecognitionEvent { + /// 事件类型 + final RecognitionEventType type; + + /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) + final String text; + + /// 错误信息(仅在 error 和 canceled 类型中有效) + final String error; + + RecognitionEvent({ + required this.type, + this.text = '', + this.error = '', + }); + @override - void onClose() { - developer.log('VoiceRecognitionService: 关闭服务', name: 'VoiceRecognition'); - - // 取消事件订阅 - _eventSubscription?.cancel(); - - // 停止识别 - stopRecognition(); - - super.onClose(); + String toString() { + return 'RecognitionEvent{type: $type, text: $text, error: $error}'; } } \ No newline at end of file diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index a67d70d2e..0b75ddcc7 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/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 = ''; @@ -26,7 +26,7 @@ class ChatController extends GetxController { late final TextEditingController messageController; late final ScrollController scrollController; final _storage = GetStorage(); - + // 用于存储当前正在流式生成的消息 final RxString currentStreamMessage = ''.obs; Message? _currentAssistantMessage; @@ -41,24 +41,73 @@ class ChatController extends GetxController { final isVoiceConnecting = true.obs; 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()) { + try { + final voiceController = Get.find(); + voiceController.stopRecording(); + Get.delete(); + } 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()) { + final voiceController = Get.find(); + voiceController.stopRecording(); + Get.delete(); + } + 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()) { + try { + final voiceController = Get.find(); + 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()) { @@ -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(), + ); - // 发送消息 - messageController.text = text; - sendMessage(); - messageController.clear(); + // 添加到消息列表 + messages.add(userMessage); - _scrollToBottom(); + // 保存聊天历史 + _saveChatHistory(); + + // 滚动到底部 + _scrollToBottom(animate: true); + + // 发送给AI处理 + _processAIResponse(userMessage); + } + } + + // 处理AI响应 + Future _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); + } + } + } + } + + // 处理剩余的文本 + if (!_isDisposed && + _ttsService.isEnabled.value && + _pendingTtsText.isNotEmpty) { + _ttsService.speak(_pendingTtsText); + } + _pendingTtsText = ''; + + 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; } } diff --git a/lib/modules/chat/controllers/voice_input_controller.dart b/lib/modules/chat/controllers/voice_input_controller.dart index 5e2e81d0d..f023775de 100644 --- a/lib/modules/chat/controllers/voice_input_controller.dart +++ b/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 _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 startRecording() async { + Future 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 restartRecognition() async { + if (isMuted.value) return; + + try { + // 先停止当前识别 + await stopRecording(); + // 延迟一下再重新启动 + await Future.delayed(const Duration(milliseconds: 500)); + // 重新启动连续识别 + await startContinuousRecognition(); + } catch (e) { + print('重新启动语音识别失败: $e'); } } Future 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 diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index b7593bf06..7c3d702f6 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -13,8 +13,15 @@ class ChatView extends GetView { Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { + // 停止TTS服务 final tts = Get.find(); await tts.stop(); + + // 停止语音输入 + if (controller.isVoiceInputVisible.value) { + controller.stopVoiceInput(); + } + return true; }, child: Scaffold( @@ -45,8 +52,15 @@ class ChatView extends GetView { size: 20.sp, ), onPressed: () async { + // 停止TTS服务 final tts = Get.find(); await tts.stop(); + + // 停止语音输入 + if (controller.isVoiceInputVisible.value) { + controller.stopVoiceInput(); + } + Get.back(); }, ), @@ -103,7 +117,9 @@ class ChatView extends GetView { 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 { 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 { ), ), ), - 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, + child: Center( + child: Text( + '点击开始语音识别', + style: TextStyle( + color: Colors.grey[600], + fontSize: 16.sp, ), - Expanded( - child: TextField( - controller: controller.messageController, - decoration: const InputDecoration( - hintText: '输入消息...', - border: InputBorder.none, - ), - maxLines: null, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.send, - onSubmitted: (_) => controller.sendMessage(), - ), - ), - 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), + ), + ), + ), + ), ), ); } diff --git a/lib/modules/chat/widgets/voice_input_panel.dart b/lib/modules/chat/widgets/voice_input_panel.dart index 3646ce550..57e6d5a89 100644 --- a/lib/modules/chat/widgets/voice_input_panel.dart +++ b/lib/modules/chat/widgets/voice_input_panel.dart @@ -18,113 +18,152 @@ class VoiceInputPanel extends GetView { @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), + ), + ); + }, + ), + ); + }); + } } \ No newline at end of file diff --git a/lib/modules/speech_demo/speech_demo_page.dart b/lib/modules/speech_demo/speech_demo_page.dart index f10857d2f..0070ca9b6 100644 --- a/lib/modules/speech_demo/speech_demo_page.dart +++ b/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,391 +20,70 @@ class SpeechDemoPage extends StatefulWidget { } class _SpeechDemoPageState extends State { - final VoiceRecognitionService _recognitionService = Get.find(); - 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; + late final VoiceRecognitionService _voiceService; + bool _isInitialized = false; + bool _isRecognizing = false; + String _recognizedText = ''; + String _statusMessage = '准备就绪'; - // 添加原生权限通道 - 只用于处理Android 11+的MANAGE_EXTERNAL_STORAGE权限 - static const MethodChannel _permissionChannel = MethodChannel('com.example.deep_voice/permissions'); - @override void initState() { super.initState(); - _setupPermissionListener(); - _checkPermissions(); - } - - @override - void dispose() { - _recognitionService.stopRecognition(); - super.dispose(); - } - - // 设置权限监听器 - 只监听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 _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'; - } + _initializeService(); } - // 检查是否为Android 11或更高版本 - Future _isAndroid11OrHigher() async { - if (Platform.isAndroid) { - final androidInfo = await DeviceInfoPlugin().androidInfo; - return androidInfo.version.sdkInt >= 30; // Android 11 is API 30 - } - return false; - } - - // 记录详细的权限状态 - Future _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 _requestPermissions() async { - developer.log('请求权限'); + Future _initializeService() async { + setState(() { + _statusMessage = '正在初始化语音服务...'; + }); 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)); - - // 再次检查权限状态 - try { - isManager = await _permissionChannel.invokeMethod('isExternalStorageManager') ?? false; - developer.log('MANAGE_EXTERNAL_STORAGE权限状态: $isManager'); - } catch (e) { - developer.log('检查MANAGE_EXTERNAL_STORAGE时出错: $e'); - } - } - } + // 创建服务实例(这一步会自动从环境变量加载配置) + _voiceService = VoiceRecognitionService(); - // 请求基本权限 - Map statuses = await [ - Permission.microphone, - Permission.storage, - ].request(); + // 初始化语音识别服务 + final result = await _voiceService.initialize(); - // 记录结果 - statuses.forEach((permission, status) { - developer.log('权限 $permission: $status'); + setState(() { + _isInitialized = result; + _statusMessage = '语音服务初始化成功,可以开始识别'; }); - - // 检查权限状态 - await _checkPermissions(); } catch (e) { - developer.log('请求权限时出错: $e'); - _errorMessage.value = '请求权限时出错: $e'; + setState(() { + _isInitialized = false; + _statusMessage = '初始化失败: $e'; + }); } } - // 打开应用设置 - Future _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 _startVoiceRecognition() async { + if (!_isInitialized) { + setState(() { + _statusMessage = '语音服务未初始化,请先初始化'; + }); + return; } - } - - // 初始化语音引擎 - Future _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), - ), - ); - } - } - - Future _startRecognition() async { - developer.log('开始识别'); - _errorMessage.value = ''; - _isRecognizing.value = true; - _status.value = '正在识别...'; + setState(() { + _isRecognizing = true; + _statusMessage = '正在聆听...'; + }); try { - await _recognitionService.startRecognition(); - - // 设置监听器接收识别结果 - _recognitionService.recognizedText.listen((text) { - _recognizedText.value = text; + final result = await _voiceService.recognizeSpeech(); + setState(() { + _recognizedText = result; + _isRecognizing = false; + _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), - ), - ); - } - } - - Future _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), - ), - ); - } - } - - // 获取可用的存储目录 - Future _getAvailableStorageDirectory() async { - try { - List 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; } catch (e) { - developer.log('获取可用存储目录时出错: $e'); - return null; + setState(() { + _isRecognizing = false; + _statusMessage = '识别失败: $e'; + }); } } - + @override Widget build(BuildContext context) { return Scaffold( @@ -415,126 +95,86 @@ class _SpeechDemoPageState extends State { 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()), - - // 错误消息 - 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()), - + // 状态信息 + 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, + ), + ), + ), + + 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, ), ], ),