From f0ed19f5e1e1218c54f03e3b4062561eb96addee Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 25 Feb 2025 12:36:39 +0000 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90vol=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle.kts | 6 +- android/app/src/main/AndroidManifest.xml | 11 + .../com/example/deep_voice/MainActivity.kt | 89 +++ .../example/deep_voice/VolcSpeechBridge.kt | 326 ++++++++-- .../main/res/xml/network_security_config.xml | 13 + android/settings.gradle.kts | 9 - .../services/voice_recognition_service.dart | 191 +++++- lib/modules/profile/views/profile_view.dart | 10 + lib/modules/speech_demo/speech_demo_page.dart | 561 +++++++++++++++--- 9 files changed, 1034 insertions(+), 182 deletions(-) create mode 100644 android/app/src/main/res/xml/network_security_config.xml diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index f28f0f64a..2a0b701cc 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,4 +1,8 @@ - +repositories { + google() + mavenCentral() + maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") } +} plugins { id("com.android.application") diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f46ae0c52..9bb05f38c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,11 +9,22 @@ + + + + + + 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 c81f6a651..2cac0769c 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 @@ -1,17 +1,106 @@ package com.example.deep_voice +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.provider.Settings +import android.util.Log import com.ryanheise.audioservice.AudioServiceActivity import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel class MainActivity: AudioServiceActivity() { private var volcSpeechBridge: VolcSpeechBridge? = null + 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") + } 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 -> + when (call.method) { + "openAppSettings" -> { + openAppSettings() + result.success(true) + } + "requestManageExternalStorage" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + requestManageExternalStorage() + result.success(true) + } else { + result.success(false) + } + } + "isExternalStorageManager" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + result.success(Environment.isExternalStorageManager()) + } else { + result.success(true) // 在Android 11以下版本,返回true + } + } + 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) + } + } + } + + 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) + } + } override fun onDestroy() { // 释放火山语音识别资源 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 index 3319f6cba..26c2db086 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt @@ -11,6 +11,7 @@ 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 @@ -27,13 +28,10 @@ class VolcSpeechBridge( 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 const val DIRECTIVE_START_RECOGNITION = 1 - private const val DIRECTIVE_STOP_RECOGNITION = 2 } 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 @@ -46,7 +44,8 @@ class VolcSpeechBridge( "initialize" -> { val appKey = call.argument("appKey") ?: "" val accessKey = call.argument("accessKey") ?: "" - initialize(appKey, accessKey, result) + val cluster = call.argument("cluster") ?: "volcano_asr" + initialize(appKey, accessKey, cluster, result) } "startRecognition" -> { startRecognition(result) @@ -57,6 +56,9 @@ class VolcSpeechBridge( "isInitialized" -> { result.success(isInitialized) } + "checkEngineCapabilities" -> { + checkEngineCapabilities(result) + } else -> { result.notImplemented() } @@ -78,32 +80,75 @@ class VolcSpeechBridge( /** * 初始化火山语音识别引擎 */ - fun initialize(appKey: String, accessKey: String, result: Result) { + 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() - engine?.createEngine() + engineHandler = engine?.createEngine() ?: 0 - // 设置监听器 - engine?.setListener(this) + 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("app_id", appKey) - engine?.setOptionString("access_key", accessKey) + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appKey) + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;$accessKey") - // 设置语音识别参数 - engine?.setOptionBoolean("enable_punctuation", true) // 启用标点符号 - engine?.setOptionBoolean("enable_intermediate_result", true) // 启用中间结果 - engine?.setOptionBoolean("enable_vad", true) // 启用语音活动检测 + // 设置网络参数 + 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("language", "zh") + // 设置音频来源为内置录音机 + 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) // 初始化引擎 - val ret = engine?.initEngine() + 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") @@ -127,17 +172,24 @@ class VolcSpeechBridge( 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") - val ret = engine?.sendDirective(DIRECTIVE_START_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") - // 发送状态更新到 Flutter - sendEvent("status", "listening") + // 状态会通过回调更新 } else { Log.e(TAG, "Start recognition failed with error code: $ret") result.error("START_ERROR", "Start recognition failed with error code: $ret", null) @@ -154,20 +206,28 @@ class VolcSpeechBridge( 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 ret = engine?.sendDirective(DIRECTIVE_STOP_RECOGNITION, "") - if (ret == SpeechEngineDefines.ERR_NO_ERROR) { + // 先发送音频输入完成指令 + 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") - // 发送状态更新到 Flutter - sendEvent("status", "idle") + // 状态会通过回调更新 } else { - Log.e(TAG, "Stop recognition failed with error code: $ret") - result.error("STOP_ERROR", "Stop recognition failed with error code: $ret", null) + 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) @@ -180,7 +240,10 @@ class VolcSpeechBridge( */ fun dispose() { try { - engine?.destroyEngine() + if (engineHandler != 0L) { + engine?.destroyEngine(engineHandler) + engineHandler = 0 + } engine = null isInitialized = false Log.i(TAG, "Volcengine Speech Engine released") @@ -216,36 +279,60 @@ class VolcSpeechBridge( */ 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) { - // 初始化完成消息 - 1 -> { - Log.i(TAG, "Engine initialized successfully") - isInitialized = true - - // 通知 Flutter 初始化完成 - methodResult?.success("Engine initialized") - methodResult = null - - // 发送状态更新到 Flutter - sendEvent("status", "initialized") + // 引擎启动成功 + SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { + Log.i(TAG, "Engine started successfully") + sendEvent("status", "listening") } - // 错误消息 - 2 -> { - val errorCode = messageData?.let { String(it, 0, messageLength).toIntOrNull() } ?: -1 - Log.e(TAG, "Engine error: $errorCode") - - // 通知 Flutter 识别出错 - val errorData = JSONObject().apply { - put("code", errorCode) - put("message", "Error code: $errorCode") + // 引擎关闭 + 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") + } } - - sendEvent("error", errorData.toString()) } // VAD 状态变化消息 - 3 -> { + SpeechEngineDefines.MESSAGE_TYPE_VAD_STATE -> { val state = messageData?.let { String(it, 0, messageLength).toIntOrNull() } ?: -1 Log.i(TAG, "VAD state changed: $state") @@ -258,36 +345,149 @@ class VolcSpeechBridge( sendEvent("vad", vadState) } - // 识别结果消息 - 4 -> { + // 中间识别结果 + 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) - val text = jsonObject.optString("text", "") - val isFinal = jsonObject.optBoolean("is_final", false) - - Log.i(TAG, "Recognition result: $text, isFinal: $isFinal") - // 发送识别结果到 Flutter - val resultData = JSONObject().apply { - put("text", text) - put("isFinal", isFinal) + 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) - sendEvent("result", resultData.toString()) + 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 result JSON", e) + 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/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..2986285a0 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + 127.0.0.1 + localhost + + \ No newline at end of file diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index c1c2fa423..20cc3a2da 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -25,13 +25,4 @@ plugins { id("org.jetbrains.kotlin.android") version "1.8.22" apply false } -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") } - gradlePluginPortal() - } -} - include(":app") diff --git a/lib/data/services/voice_recognition_service.dart b/lib/data/services/voice_recognition_service.dart index 35024f5fb..c69d63d5e 100644 --- a/lib/data/services/voice_recognition_service.dart +++ b/lib/data/services/voice_recognition_service.dart @@ -4,6 +4,7 @@ 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; /// 火山语音识别服务 /// 负责与原生层的火山语音识别引擎交互 @@ -37,9 +38,11 @@ class VoiceRecognitionService extends GetxService { // 火山语音识别的 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(); @@ -50,41 +53,51 @@ class VoiceRecognitionService extends GetxService { 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) { - debugPrint('警告: 火山语音识别凭证未设置,请在 .env 文件中设置 VOLC_SPEECH_APP_KEY 和 VOLC_SPEECH_ACCESS_KEY'); + developer.log('VoiceRecognitionService: 警告 - 火山语音识别凭证未设置,请在 .env 文件中设置 VOLC_SPEECH_APP_KEY 和 VOLC_SPEECH_ACCESS_KEY', name: 'VoiceRecognition'); } - } catch (e) { - debugPrint('加载火山语音识别凭证失败: $e'); + } catch (e, stack) { + developer.log('VoiceRecognitionService: 加载火山语音识别凭证失败: $e', name: 'VoiceRecognition'); + developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); } } /// 设置事件监听器 void _setupEventListener() { try { + developer.log('VoiceRecognitionService: 设置事件监听器', name: 'VoiceRecognition'); _eventSubscription = _eventChannel.receiveBroadcastStream().listen( (dynamic event) { _handleEvent(event.toString()); }, onError: (dynamic error) { - debugPrint('事件通道错误: $error'); + developer.log('VoiceRecognitionService: 事件通道错误: $error', name: 'VoiceRecognition'); if (onError != null) { onError!('事件通道错误: $error'); } }, ); - } catch (e) { - debugPrint('设置事件监听器失败: $e'); + } catch (e, stack) { + developer.log('VoiceRecognitionService: 设置事件监听器失败: $e', name: 'VoiceRecognition'); + developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); } } /// 处理来自原生层的事件 void _handleEvent(String eventJson) { 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()); @@ -98,24 +111,33 @@ class VoiceRecognitionService extends GetxService { case 'vad': _handleVadEvent(data.toString()); break; + default: + developer.log('VoiceRecognitionService: 未知事件类型: $eventName', name: 'VoiceRecognition'); + break; } - } catch (e) { - debugPrint('处理事件失败: $e'); + } catch (e, stack) { + developer.log('VoiceRecognitionService: 处理事件失败: $e', name: 'VoiceRecognition'); + developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); } } /// 处理状态事件 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; } @@ -131,6 +153,8 @@ class VoiceRecognitionService extends GetxService { final text = resultData['text']; final isFinal = resultData['isFinal']; + developer.log('VoiceRecognitionService: 识别结果: $text, 是否最终结果: $isFinal', name: 'VoiceRecognition'); + // 更新识别文本 recognizedText.value = text; @@ -141,6 +165,8 @@ class VoiceRecognitionService extends GetxService { // 如果是最终结果,调用完成回调 if (isFinal && text.isNotEmpty) { + developer.log('VoiceRecognitionService: 句子识别完成: $text', name: 'VoiceRecognition'); + if (onSentenceComplete != null) { onSentenceComplete!(text); } @@ -149,8 +175,9 @@ class VoiceRecognitionService extends GetxService { onRecognitionComplete!(); } } - } catch (e) { - debugPrint('处理识别结果失败: $e'); + } catch (e, stack) { + developer.log('VoiceRecognitionService: 处理识别结果失败: $e', name: 'VoiceRecognition'); + developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); } } @@ -159,50 +186,134 @@ class VoiceRecognitionService extends GetxService { try { final errorData = jsonDecode(errorJson); final errorMessage = errorData['message']; + final errorCode = errorData['code'] ?? 'unknown'; - debugPrint('语音识别错误: $errorMessage'); + developer.log('VoiceRecognitionService: 语音识别错误: 代码=$errorCode, 消息=$errorMessage', name: 'VoiceRecognition', error: errorMessage); if (onError != null) { onError!(errorMessage); } - } catch (e) { - debugPrint('处理错误事件失败: $e'); + } catch (e, stack) { + developer.log('VoiceRecognitionService: 处理错误事件失败: $e', name: 'VoiceRecognition'); + developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); } } /// 处理 VAD 事件 void _handleVadEvent(String vadState) { // 可以根据需要处理 VAD 状态变化 - debugPrint('VAD 状态: $vadState'); + developer.log('VoiceRecognitionService: VAD 状态: $vadState', name: 'VoiceRecognition'); + } + + /// 检查引擎能力和支持的指令 + Future checkEngineCapabilities() async { + try { + developer.log('VoiceRecognitionService: 检查引擎能力', name: 'VoiceRecognition'); + + // 检查是否已初始化 + if (!isInitialized.value) { + developer.log('VoiceRecognitionService: 引擎未初始化,尝试初始化', name: 'VoiceRecognition'); + await initialize(); + } + + // 调用原生方法检查引擎能力 + final result = await _methodChannel.invokeMethod('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'); + } } /// 初始化语音识别引擎 Future initialize() async { try { + developer.log('VoiceRecognitionService: 开始初始化语音引擎', name: 'VoiceRecognition'); isConnecting.value = true; // 检查凭证 if (_appKey.isEmpty || _accessKey.isEmpty) { + developer.log('VoiceRecognitionService: 凭证未设置,无法初始化', name: 'VoiceRecognition', error: '火山语音识别凭证未设置'); throw '火山语音识别凭证未设置'; } - // 调用原生方法初始化引擎 - final result = await _methodChannel.invokeMethod( - 'initialize', - { - 'appKey': _appKey, - 'accessKey': _accessKey, - }, - ); + // 添加详细日志 + developer.log('VoiceRecognitionService: 使用凭证 AppKey: $_appKey, AccessKey: ${_accessKey.substring(0, 4)}***, Cluster: $_cluster', name: 'VoiceRecognition'); - debugPrint('初始化结果: $result'); + // 调用原生方法初始化引擎 + try { + final result = await _methodChannel.invokeMethod( + '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('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; + } // 初始化成功后,状态会通过事件通道更新 - } catch (e) { + } catch (e, stack) { isConnecting.value = false; - debugPrint('初始化语音识别引擎失败: $e'); + 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!('初始化失败: $e'); + onError!(errorMessage); } rethrow; } @@ -211,18 +322,25 @@ class VoiceRecognitionService extends GetxService { /// 开始语音识别 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'); - debugPrint('开始识别结果: $result'); + developer.log('VoiceRecognitionService: 开始识别结果: $result', name: 'VoiceRecognition'); // 状态会通过事件通道更新 - } catch (e) { - debugPrint('开始语音识别失败: $e'); + } catch (e, stack) { + developer.log('VoiceRecognitionService: 开始语音识别失败: $e', name: 'VoiceRecognition', error: e); + developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); + if (onError != null) { onError!('开始识别失败: $e'); } @@ -233,18 +351,25 @@ class VoiceRecognitionService extends GetxService { /// 停止语音识别 Future stopRecognition() async { try { + developer.log('VoiceRecognitionService: 停止语音识别', name: 'VoiceRecognition'); + // 检查是否已初始化 if (!isInitialized.value) { + developer.log('VoiceRecognitionService: 引擎未初始化,无法停止识别', name: 'VoiceRecognition'); return; } + developer.log('VoiceRecognitionService: 调用原生方法 stopRecognition', name: 'VoiceRecognition'); + // 调用原生方法停止识别 final result = await _methodChannel.invokeMethod('stopRecognition'); - debugPrint('停止识别结果: $result'); + developer.log('VoiceRecognitionService: 停止识别结果: $result', name: 'VoiceRecognition'); // 状态会通过事件通道更新 - } catch (e) { - debugPrint('停止语音识别失败: $e'); + } catch (e, stack) { + developer.log('VoiceRecognitionService: 停止语音识别失败: $e', name: 'VoiceRecognition', error: e); + developer.log('VoiceRecognitionService: 错误堆栈: $stack', name: 'VoiceRecognition'); + if (onError != null) { onError!('停止识别失败: $e'); } @@ -253,6 +378,8 @@ class VoiceRecognitionService extends GetxService { @override void onClose() { + developer.log('VoiceRecognitionService: 关闭服务', name: 'VoiceRecognition'); + // 取消事件订阅 _eventSubscription?.cancel(); diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 1336c4c68..fd22bf44a 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -4,6 +4,7 @@ import '../controllers/profile_controller.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../data/services/volcano_tts_service.dart'; import '../../../core/widgets/common_bottom_nav.dart'; +import '../../speech_demo/speech_demo_page.dart'; class ProfileView extends GetView { const ProfileView({Key? key}) : super(key: key); @@ -109,6 +110,15 @@ class ProfileView extends GetView { }, ), const Divider(), + _buildMenuItem( + title: '语音识别测试', + icon: Icons.mic, + subtitle: '测试语音识别功能', + onTap: () { + Get.to(() => const SpeechDemoPage()); + }, + ), + const Divider(), _buildMenuItem( title: '关于', icon: Icons.info_outline, diff --git a/lib/modules/speech_demo/speech_demo_page.dart b/lib/modules/speech_demo/speech_demo_page.dart index 473265d81..f10857d2f 100644 --- a/lib/modules/speech_demo/speech_demo_page.dart +++ b/lib/modules/speech_demo/speech_demo_page.dart @@ -1,56 +1,406 @@ +import 'dart:async'; +import 'dart:developer' as developer; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; 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 '../../data/services/voice_recognition_service.dart'; class SpeechDemoPage extends StatefulWidget { const SpeechDemoPage({Key? key}) : super(key: key); @override - _SpeechDemoPageState createState() => _SpeechDemoPageState(); + State createState() => _SpeechDemoPageState(); } 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; + // 添加原生权限通道 - 只用于处理Android 11+的MANAGE_EXTERNAL_STORAGE权限 + static const MethodChannel _permissionChannel = MethodChannel('com.example.deep_voice/permissions'); + @override void initState() { super.initState(); - _initializeEngine(); + _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'; + } + } + + // 检查是否为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('请求权限'); + + 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'); + } + } + } + + // 请求基本权限 + Map statuses = await [ + Permission.microphone, + Permission.storage, + ].request(); + + // 记录结果 + statuses.forEach((permission, status) { + developer.log('权限 $permission: $status'); + }); + + // 检查权限状态 + await _checkPermissions(); + } catch (e) { + developer.log('请求权限时出错: $e'); + _errorMessage.value = '请求权限时出错: $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 _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(); - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('初始化失败: $e')), - ); + + 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 = '正在识别...'; + try { await _recognitionService.startRecognition(); + + // 设置监听器接收识别结果 + _recognitionService.recognizedText.listen((text) { + _recognizedText.value = text; + }); + } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('启动识别失败: $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) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('停止识别失败: $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; } } @@ -58,76 +408,133 @@ class _SpeechDemoPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('火山语音识别演示'), + title: const Text('语音识别演示'), ), - body: Center( + body: Padding( + padding: const EdgeInsets.all(16.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + 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(() { - String status = '准备中'; - if (_recognitionService.isConnecting.value) { - status = '连接中...'; - } else if (_recognitionService.isInitialized.value) { - status = _recognitionService.isListening.value ? '正在听...' : '就绪'; - } - return Text('状态: $status', style: const TextStyle(fontSize: 18)); - }), - - const SizedBox(height: 20), - - // 识别结果显示 - Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - width: double.infinity, - height: 200, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.all(16), - child: Obx(() { - return SingleChildScrollView( + 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( - _recognitionService.recognizedText.value, - style: const TextStyle(fontSize: 16), + '错误: ${_errorMessage.value}', + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), ), - ); - }), + ) + : const SizedBox.shrink()), + + // 识别结果显示区域 + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Obx(() => Text( + _recognizedText.value.isEmpty + ? '识别结果将显示在这里...' + : _recognizedText.value, + style: const TextStyle( + fontSize: 18.0, + ), + )), + ), + ), ), ), - - const SizedBox(height: 20), - + // 控制按钮 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Obx(() { - return ElevatedButton( - onPressed: _recognitionService.isInitialized.value && !_recognitionService.isListening.value - ? _startRecognition - : null, - child: const Text('开始识别'), - ); - }), - - const SizedBox(width: 20), - - Obx(() { - return ElevatedButton( - onPressed: _recognitionService.isListening.value - ? _stopRecognition - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - ), - child: const Text('停止识别'), - ); - }), - ], + 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('停止识别'), + )), + ], + ), ), ], ),