From bae5d702293a8c35a7494114fb2c8bf56fd0bf1c Mon Sep 17 00:00:00 2001 From: wolfplus Date: Wed, 14 May 2025 21:08:47 +0100 Subject: [PATCH 1/2] add --- android/app/build.gradle.kts | 20 - .../yunqiinnovation/deepsound/MainActivity.kt | 2 +- android/settings.gradle.kts | 9 +- lib/data/services/ble_manager.dart | 2 + .../agent/controllers/agent_controller.dart | 73 +- .../controllers/pairing_controller.dart | 2 +- .../agent_service/android/build.gradle.kts | 5 +- .../agent_service/android/settings.gradle.kts | 13 - .../agent_service/AgentService.kt | 349 ++++++--- .../agent_service/AgentServicePlugin.kt | 30 +- .../yunqiinnovation/agent_service/BleAgent.kt | 28 +- .../agent_service/BleMediaService.kt | 57 +- .../yunqiinnovation/agent_service/utils.kt | 14 +- .../azure_speech/android/build.gradle.kts | 2 +- .../azure_speech/AzureAsrHelper.kt | 51 +- .../azure_speech/AzureTtsHelper.kt | 11 +- .../ble_service/android/build.gradle.kts | 1 - .../ble_service/BleCompanionManager.kt | 74 +- .../ble_service/BleCompanionService.kt | 15 +- .../yunqiinnovation/ble_service/BleService.kt | 180 ++--- .../ble_service/BleServicePlugin.kt | 11 +- .../ble_service/BleWakeReceiver.kt | 13 +- .../bytedance_speech/android/build.gradle.kts | 2 +- .../android/src/main/AndroidManifest.xml | 2 +- .../bytedance_speech/BytedanceASR.kt | 392 +++++------ .../bytedance_speech/BytedanceSpeechPlugin.kt | 660 +++++++++++------- .../bytedance_speech/BytedanceTTS.kt | 269 ++++--- .../ClassicBluetoothPlugin.kt | 5 +- .../android/settings.gradle.kts | 1 - .../android/src/main/AndroidManifest.xml | 7 - .../deepvoice_utils/DeepvoiceUtilsPlugin.kt | 33 - .../deepvoice_utils/FileLogger.kt | 237 ------- .../ios/Classes/DeepvoiceUtilsPlugin.h | 4 - .../ios/Classes/DeepvoiceUtilsPlugin.m | 12 - .../ios/Classes/DeepvoiceUtilsPlugin.swift | 30 - .../ios/Classes/FileLogger.swift | 286 -------- .../Classes/SwiftDeepvoiceUtilsPlugin.swift | 11 - local_plugins/deepvoice_utils/ios/README.md | 54 -- .../ios/deepvoice_utils.podspec | 19 - .../deepvoice_utils/lib/deepvoice_utils.dart | 3 - .../deepvoice_utils/lib/src/file_logger.dart | 31 - local_plugins/deepvoice_utils/pubspec.yaml | 27 - .../music_service/android/build.gradle | 1 - .../music_service/MusicService.kt | 25 +- .../music_service/MusicServicePlugin.kt | 4 +- .../android/build.gradle.kts | 22 +- .../speech/android/settings.gradle.kts | 1 + .../android/src/main/AndroidManifest.xml | 10 + .../kotlin/com/deep_voice/speech/AsrEvents.kt | 52 ++ .../com/deep_voice/speech/IAsrService.kt | 90 +++ .../com/deep_voice/speech/ITtsService.kt | 92 +++ .../kotlin/com/deep_voice/speech/TtsEvents.kt | 63 ++ .../volcano_speech/android/build.gradle.kts | 44 +- .../volcano_speech/VolcanoAsrHelper.kt | 134 ++-- .../volcano_speech/VolcanoSpeechPlugin.kt | 20 +- .../volcano_speech/VolcanoTtsHelper.kt | 206 ++++-- pubspec.yaml | 4 + 57 files changed, 1890 insertions(+), 1925 deletions(-) delete mode 100644 local_plugins/deepvoice_utils/android/settings.gradle.kts delete mode 100644 local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml delete mode 100644 local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt delete mode 100644 local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt delete mode 100644 local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h delete mode 100644 local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m delete mode 100644 local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift delete mode 100644 local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift delete mode 100644 local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift delete mode 100644 local_plugins/deepvoice_utils/ios/README.md delete mode 100644 local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec delete mode 100644 local_plugins/deepvoice_utils/lib/deepvoice_utils.dart delete mode 100644 local_plugins/deepvoice_utils/lib/src/file_logger.dart delete mode 100644 local_plugins/deepvoice_utils/pubspec.yaml rename local_plugins/{deepvoice_utils => speech}/android/build.gradle.kts (53%) create mode 100644 local_plugins/speech/android/settings.gradle.kts create mode 100644 local_plugins/speech/android/src/main/AndroidManifest.xml create mode 100644 local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt create mode 100644 local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt create mode 100644 local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt create mode 100644 local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fc2da7300..20d5ead25 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -119,28 +119,8 @@ android { } dependencies { - - implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0") - - - // 添加本地插件模块依赖 - implementation(project(":spotify_sdk")) - implementation(project(":deepvoice_utils")) - implementation(project(":ble_service")) - // 添加OkHttp依赖 - implementation("com.squareup.okhttp3:okhttp:4.9.3") - // 添加核心库反糖化 coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") - - - // 添加androidx.media依赖 - implementation("androidx.media:media:1.6.0") - - // 添加legacy-support-v4依赖 - implementation("androidx.legacy:legacy-support-v4:1.0.0") - - } flutter { diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt index bf9d92455..e7b34ac65 100644 --- a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt @@ -12,7 +12,7 @@ import io.flutter.embedding.android.FlutterActivity import android.content.Context import android.os.Handler import android.os.Looper -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log class MainActivity: FlutterActivity() { private val TAG = "MainActivity" diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index d350a0017..48867bf1b 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -37,9 +37,11 @@ include(":chat_storage") include(":jieli") include(":spotify_sdk") include(":ble_service") -include(":deepvoice_utils") include(":classic_bluetooth") include(":agent_service") +include(":volcano_speech") +include(":bytedance_speech") +include(":speech") // 设置azure_speech项目的路径 project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android") @@ -47,7 +49,8 @@ project(":chat_storage").projectDir = file("../local_plugins/chat_storage/androi project(":jieli").projectDir = file("../local_plugins/jieli/android") project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android") project(":ble_service").projectDir = file("../local_plugins/ble_service/android") -project(":deepvoice_utils").projectDir = file("../local_plugins/deepvoice_utils/android") project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android") - +project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android") +project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android") +project(":speech").projectDir = file("../local_plugins/speech/android") \ No newline at end of file diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 0f826a7c4..3234879a0 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -497,6 +497,8 @@ class BleManager extends GetxService { 'openaiModel': openaiModel, 'systemPrompt': systemPrompt, 'mcpServer': jsonEncode(jsonData), + 'volcanoAppId': '2316081933', + 'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En', }; // 记录日志(不记录敏感信息) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index f82dd7d5c..905359208 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -530,36 +530,73 @@ final responseId = event.data['responseId'] ?? ''; // 检查麦克风权限 Future _checkMicrophonePermission() async { try { - PermissionStatus status = await Permission.microphone.status; - - // 如果已授权,直接返回 true - if (status.isGranted) { + // 创建待请求权限列表 + List permissionsToRequest = []; + + if (Platform.isAndroid) { + // Android 需要考虑SDK版本不同的权限要求 + permissionsToRequest.add(Permission.microphone); + + + } else if (Platform.isIOS) { + // iOS 只需要麦克风权限 + permissionsToRequest.add(Permission.microphone); + } + + // 批量请求所有需要的权限 + final statuses = await permissionsToRequest.request(); + + // 检查是否所有权限都已授予 + bool allGranted = true; + Permission? deniedPermission; + + statuses.forEach((permission, status) { + if (!status.isGranted) { + allGranted = false; + deniedPermission = permission; + logger.e('权限 ${permission.toString()} 未被授予: $status'); + } + }); + + // 单独检查麦克风权限状态,因为这是主要需求 + final micStatus = await Permission.microphone.status; + + // 如果所有权限都已授予,或至少麦克风权限已授予,返回true + if (allGranted || micStatus.isGranted) { return true; } - - // 如果被永久拒绝,提示用户前往设置页面 - if (status.isPermanentlyDenied) { + + // 处理特定权限被永久拒绝的情况 + if (micStatus.isPermanentlyDenied) { _showPermissionDialog(true); return false; } - - // 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 - if (status.isDenied) { - status = await Permission.microphone.request(); - if (status.isGranted) { - return true; - } else { - _showPermissionDialog(false); - return false; - } + + // 普通拒绝情况 + if (deniedPermission != null) { + _showPermissionDialog(false); } - + return false; } catch (e) { logger.e('检查麦克风权限失败: $e'); return false; } } + + // 获取Android SDK版本 + Future _getAndroidSdkVersion() async { + if (Platform.isAndroid) { + try { + // 这里我们简单返回一个较高的值,确保请求所有可能需要的权限 + // 在实际应用中,你可能需要使用platform channel获取真实的SDK版本 + return 31; // 对应Android 12 + } catch (e) { + logger.e('获取Android SDK版本失败: $e'); + } + } + return 0; + } // 显示权限提示对话框 void _showPermissionDialog(bool isPermanent) { diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 55311e0d0..7a65da691 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -186,7 +186,7 @@ class PairingController extends GetxController { if (Platform.isAndroid) { permissionsToRequest.add(Permission.locationWhenInUse); - permissionsToRequest.add(Permission.microphone); + // permissionsToRequest.add(Permission.microphone); if (await _getAndroidSdkVersion() >= 31) { permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothConnect); diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index b6c1b4d21..e97e6c138 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -36,7 +36,6 @@ android { dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") implementation("androidx.core:core-ktx:1.8.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0") implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.media:media:1.6.0") @@ -45,8 +44,10 @@ dependencies { // 依赖本地其他插件 add("compileOnly", project(":azure_speech")) add("compileOnly", project(":open_ai_service")) - add("compileOnly", project(":deepvoice_utils")) add("compileOnly", project(":chat_storage")) add("compileOnly", project(":ble_service")) add("compileOnly", project(":music_service")) + add("compileOnly", project(":volcano_speech")) + add("compileOnly", project(":bytedance_speech")) + add("compileOnly", project(":speech")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/settings.gradle.kts b/local_plugins/agent_service/android/settings.gradle.kts index 0aa37a16f..9ed233f8b 100644 --- a/local_plugins/agent_service/android/settings.gradle.kts +++ b/local_plugins/agent_service/android/settings.gradle.kts @@ -1,15 +1,2 @@ rootProject.name = "agent_service" -// 包含项目依赖 -include(":azure_speech") -include(":open_ai_service") -include(":deepvoice_utils") -include(":chat_storage") -include(":music_service") - -// 设置项目路径 -project(":azure_speech").projectDir = file("../../azure_speech/android") -project(":open_ai_service").projectDir = file("../../open_ai_service/android") -project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android") -project(":chat_storage").projectDir = file("../../chat_storage/android") -project(":music_service").projectDir = file("../../music_service/android") \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 56fbded96..9aa632d0a 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -4,11 +4,10 @@ import android.content.Context import org.json.JSONArray import org.json.JSONObject import com.yunqiinnovation.azure_speech.AzureAsrHelper -import com.yunqiinnovation.azure_speech.AzureTtsHelper import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.music_service.MusicServiceStarter -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import io.flutter.plugin.common.MethodChannel import kotlinx.coroutines.* import java.util.Collections @@ -18,6 +17,11 @@ import android.media.MediaPlayer import com.yunqiinnovation.agent_service.R import android.os.Handler import android.os.Looper +import com.deep_voice.speech.TtsEventListener +import com.deep_voice.speech.AudioDataListener +import com.deep_voice.speech.SpeechServiceFactory +import com.deep_voice.speech.SpeechServiceConfig +import com.deep_voice.speech.SpeechServiceType /** * 代理服务事件监听接口 */ @@ -47,16 +51,23 @@ object AgentService : CoroutineScope { private lateinit var context: Context private val listeners = Collections.synchronizedSet(mutableSetOf()) - // 配置参数 - private var azureSpeechKey: String = "" - private var azureSpeechRegion: String = "" - private var openaiApiKey: String = "" - private var openaiBaseUrl: String = "" - private var openaiModel: String = "gpt-3.5-turbo" + + // TTS引擎类型 + enum class TtsEngineType { + AZURE, + VOLCANO, + BYTEDANCE + } + + // 当前使用的TTS引擎类型,默认为Azure + private var currentTtsEngine = TtsEngineType.AZURE + // Azure服务 private var azureAsrHelper: AzureAsrHelper? = null - private var azureTtsHelper: AzureTtsHelper? = null + + // 统一的TTS服务接口 + private var ttsService: ITtsService? = null // OpenAI服务 private lateinit var openAIService: OpenAIService @@ -128,15 +139,8 @@ object AgentService : CoroutineScope { this.openAIService = OpenAIService(context) this.chatStorageHelper = ChatStorageHelper(context) this.audioPlayer = AudioPlayer(context) - - // 从配置中获取参数 - azureSpeechKey = config["azureSpeechKey"] as? String ?: "" - azureSpeechRegion = config["azureSpeechRegion"] as? String ?: "" - openaiApiKey = config["openaiApiKey"] as? String ?: "" - openaiBaseUrl = config["openaiBaseUrl"] as? String ?: "" - openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo" - val mcpServer = config["mcpServer"] as? String ?: "" - + + // 设置系统提示词 val customSystemPrompt = config["systemPrompt"] as? String if (!customSystemPrompt.isNullOrBlank()) { @@ -145,66 +149,136 @@ object AgentService : CoroutineScope { initSystemPrompt() } - // 检查必要参数 - if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) { - FileLogger.e(TAG, "初始化失败:关键配置参数缺失") - return false - } + + // 如果TTS参数为空,使用ASR相同参数 + if (ttsAppId.isEmpty()) ttsAppId = azureSpeechRegion + if (ttsAppToken.isEmpty()) ttsAppToken = azureSpeechKey // 初始化Azure ASR azureAsrHelper = AzureAsrHelper(context).apply { initialize( - subscriptionKey = azureSpeechKey, - region = azureSpeechRegion, + subscriptionKey = config["azureSpeechKey"], + region = config["azureSpeechRegion"], audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL else AzureAsrHelper.AudioSourceType.MICROPHONE ) } - // 初始化Azure TTS - azureTtsHelper = AzureTtsHelper(context).apply { - initialize( - subscriptionKey = azureSpeechKey, - region = azureSpeechRegion + // 根据选择的TTS引擎类型初始化TTS服务 + initializeTtsEngine(config) + + // 初始化OpenAI服务 + openAIService.initialize(config["openaiApiKey"], config["openaiBaseUrl"], config["openaiModel"], config["mcpServer"]) + + // 加载最近的聊天记录 + loadChatHistory() + + isInitialized = true + Log.d(TAG, "代理服务初始化成功,使用TTS引擎:$currentTtsEngine") + return true + } catch (e: Exception) { + Log.e(TAG, "初始化失败: ${e.message}") + return false + } + } + + /** + * 初始化TTS引擎 + */ + private fun initializeTtsEngine(config: Map) { + // 停止当前TTS服务(如果有) + ttsService?.stop() + + // 从配置中获取TTS参数,根据不同引擎类型获取对应的配置参数 + val ttsAppId: String + val ttsAppToken: String + val ttsResource: String + val ttsLanguage = config["ttsLanguage"] as? String ?: "zh-CN" + + // 根据当前引擎类型获取对应的配置参数 + when (currentTtsEngine) { + TtsEngineType.AZURE -> { + ttsAppToken = config["azureSpeechKey"] as? String ?: "" + ttsResource = config["azureSpeechRegion"] as? String ?: "" + } + TtsEngineType.VOLCANO -> { + ttsAppId = config["volcanoAppId"] as? String ?: "" + ttsAppToken = config["volcanoToken"] as? String ?: "" + } + TtsEngineType.BYTEDANCE -> { + ttsAppId = config["volcanoAppId"] as? String ?: "" + ttsAppToken = config["volcanoToken"] as? String ?: "" + } + } + + // 创建统一的TTS服务配置 + val serviceConfig = SpeechServiceConfig( + serviceType = when(currentTtsEngine) { + TtsEngineType.AZURE -> SpeechServiceType.AZURE + TtsEngineType.VOLCANO -> SpeechServiceType.VOLCANO + TtsEngineType.BYTEDANCE -> SpeechServiceType.BYTEDANCE + }, + appId = ttsAppId, + appToken = ttsAppToken, + appResource = ttsResource, + language = ttsLanguage + ) + + // 创建TTS服务 + ttsService = SpeechServiceFactory.createTtsService(context, serviceConfig) + + // 使用协程启动初始化,避免阻塞主线程 + launch { + try { + // 初始化TTS服务 + ttsService?.initialize( + ttsAppId = serviceConfig.appId, + ttsAppToken = serviceConfig.appToken, + ttsResource = serviceConfig.appResource, + language = serviceConfig.language ) + Log.d(TAG, "TTS服务初始化成功") + } catch (e: Exception) { + Log.e(TAG, "TTS服务初始化失败: ${e.message}") + } + } + + Log.d(TAG, "TTS服务(${serviceConfig.serviceType})配置完成") + + // 添加TTS事件监听 + ttsService?.addListener(object : TtsEventListener { + override fun onEvent(event: Map) { + val eventType = event["type"] as? String ?: return - // 设置TTS回调 - setTtsCallback(object : AzureTtsHelper.TtsCallback { - override fun onSynthesisStarted() { + when (eventType) { + "started" -> { isTtsSpeaking = true restartIdleCheck() sendEvent("tts_started", mapOf("status" to "started")) } - - override fun onSynthesizing() {} - - override fun onSynthesisCompleted() { + "completed" -> { isTtsSpeaking = false restartIdleCheck() sendEvent("tts_completed", mapOf("status" to "completed")) } - - override fun onSynthesisCanceled() { + "stopped", "canceled" -> { + isTtsSpeaking = false + restartIdleCheck() + sendEvent("tts_${eventType}", mapOf("status" to eventType)) + } + "error" -> { isTtsSpeaking = false restartIdleCheck() - sendEvent("tts_canceled", mapOf("status" to "canceled")) + val code = event["errorCode"] as? Int ?: -1 + val message = event["errorMessage"] as? String ?: "未知错误" + sendEvent("error", mapOf( + "code" to "TTS_ERROR", + "message" to "${serviceConfig.serviceType}语音错误: $code, $message" + )) } - }) + } } - - // 初始化OpenAI服务 - openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer) - - // 加载最近的聊天记录 - loadChatHistory() - - isInitialized = true - FileLogger.d(TAG, "代理服务初始化成功") - return true - } catch (e: Exception) { - FileLogger.e(TAG, "初始化失败: ${e.message}") - return false - } + }) } /** @@ -249,7 +323,7 @@ object AgentService : CoroutineScope { } catch (e: CancellationException) { // 协程被取消,正常行为 } catch (e: Exception) { - FileLogger.e(TAG, "空闲检测异常: ${e.message}") + Log.e(TAG, "空闲检测异常: ${e.message}") } } } @@ -277,7 +351,7 @@ object AgentService : CoroutineScope { */ fun startRecognition(isExternalActive: Boolean): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "服务未初始化") + Log.e(TAG, "服务未初始化") return false } @@ -285,7 +359,7 @@ object AgentService : CoroutineScope { // 检查录音权限 if (!isExternalActive && !checkRecordAudioPermission()) { - FileLogger.e(TAG, "需要录音权限") + Log.e(TAG, "需要录音权限") sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) return false } @@ -299,7 +373,7 @@ object AgentService : CoroutineScope { } else { AzureAsrHelper.AudioSourceType.MICROPHONE } - FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") + Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onRecognizing(recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()) { @@ -371,7 +445,7 @@ object AgentService : CoroutineScope { isRecognitionActive = false stopIdleCheck() BleService.closeCodec() - FileLogger.e(TAG, "语音识别错误: $error") + Log.e(TAG, "语音识别错误: $error") sendEvent("error", mapOf( "code" to "RECOGNITION_ERROR", "message" to error @@ -381,7 +455,7 @@ object AgentService : CoroutineScope { return true } catch (e: Exception) { isRecognitionActive = false - FileLogger.e(TAG, "启动语音识别失败: ${e.message}") + Log.e(TAG, "启动语音识别失败: ${e.message}") sendEvent("error", mapOf( "code" to "RECOGNITION_START_ERROR", "message" to e.message.toString() @@ -404,9 +478,9 @@ object AgentService : CoroutineScope { BleService.closeCodec() isRecognitionActive = false stopIdleCheck() - FileLogger.d(TAG, "语音识别已停止") + Log.d(TAG, "语音识别已停止") } catch (e: Exception) { - FileLogger.e(TAG, "停止语音识别异常: ${e.message}") + Log.e(TAG, "停止语音识别异常: ${e.message}") isRecognitionActive = false stopIdleCheck() } @@ -446,9 +520,9 @@ object AgentService : CoroutineScope { openAIService.cancelCurrentStream() // 记录日志 - FileLogger.d(TAG, "AI流输出已停止") + Log.d(TAG, "AI流输出已停止") } catch (e: Exception) { - FileLogger.e(TAG, "停止AI流输出异常", e) + Log.e(TAG, "停止AI流输出异常", e) // 确保状态被重置,即使发生异常 isAiStreaming = false currentAiJob = null @@ -461,13 +535,13 @@ object AgentService : CoroutineScope { */ fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "服务未初始化") + Log.e(TAG, "服务未初始化") sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) return false } if (text.isEmpty()) { - FileLogger.e(TAG, "文本输入不能为空") + Log.e(TAG, "文本输入不能为空") sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) return false } @@ -494,7 +568,7 @@ object AgentService : CoroutineScope { text: String, speakResponse: Boolean = true ) { - FileLogger.d(TAG, "用户问题: $text") + Log.d(TAG, "用户问题: $text") // 创建用户文本消息并处理 val userMessage = openAIService.createUserMessage(text) @@ -513,7 +587,7 @@ object AgentService : CoroutineScope { text: String = "", speakResponse: Boolean = false ) { - FileLogger.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") + Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") // 创建带图片的用户消息并处理 val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) @@ -545,7 +619,6 @@ object AgentService : CoroutineScope { // 设置状态为正在流式输出 isAiStreaming = true - // 使用历史记录作为上下文发送到OpenAI val responseBuilder = StringBuilder() var metadata:String = "" @@ -592,24 +665,29 @@ object AgentService : CoroutineScope { addToHistoryMessages(userMessage) } - openAIService.sendMessageStream( messages = messagesWithSystemPrompt, callback = object : OpenAIService.StreamCallback { override fun onToken(token: String) { responseBuilder.append(token) if (speakResponse) { - azureTtsHelper?.speakStream(token) + // 使用统一的TTS接口 + launch { + ttsService?.speakStream(token) + } } // 发送流式回复token sendEvent("assistant_token", mapOf("token" to token)) } override fun onComplete() { - FileLogger.d(TAG, "AI完整回复: $responseBuilder") + Log.d(TAG, "AI完整回复: $responseBuilder") // 视情况决定是否朗读回复 if (speakResponse) { - azureTtsHelper?.flushStream() + // 使用统一的TTS接口刷新缓冲区 + launch { + ttsService?.flushStream() + } } val response = responseBuilder.toString() @@ -637,7 +715,7 @@ object AgentService : CoroutineScope { } override fun onError(e: Exception) { - FileLogger.e(TAG, "AI处理出错", e) + Log.e(TAG, "AI处理出错", e) sendEvent("error", mapOf( "code" to "AI_ERROR", "message" to e.message.toString() @@ -660,7 +738,7 @@ object AgentService : CoroutineScope { stopRecognition() } } catch (e: Exception) { - FileLogger.e(TAG, "处理函数调用事件失败", e) + Log.e(TAG, "处理函数调用事件失败", e) } } @@ -670,7 +748,7 @@ object AgentService : CoroutineScope { "function_call" to functionCall.toString(), "result" to functionCallResult.toString(), )) - FileLogger.d(TAG, "mcp调用结果: $functionCallResult") + Log.d(TAG, "mcp调用结果: $functionCallResult") metadata = autoPlayMusic(functionCallResult); } } @@ -678,7 +756,7 @@ object AgentService : CoroutineScope { } catch (e: Exception) { val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" - FileLogger.e(TAG, "AI处理出错", e) + Log.e(TAG, "AI处理出错", e) sendEvent("error", mapOf( "code" to errorType, "message" to e.message.toString() @@ -721,9 +799,9 @@ object AgentService : CoroutineScope { } } - FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录") + Log.d(TAG, "已加载${recentMessages.size}条历史记录") } catch (e: Exception) { - FileLogger.e(TAG, "加载聊天历史失败: ${e.message}") + Log.e(TAG, "加载聊天历史失败: ${e.message}") } } @@ -754,8 +832,10 @@ object AgentService : CoroutineScope { isTtsSpeaking = true restartIdleCheck() // 状态变化,重启检测 - // 播放文本 - azureTtsHelper?.speakText(text) + // 使用统一的TTS接口 + launch { + ttsService?.speakOnce(text) + } } /** @@ -763,7 +843,10 @@ object AgentService : CoroutineScope { */ fun stopTts() { if (isTtsSpeaking) { - azureTtsHelper?.stopSpeaking() + launch { + ttsService?.stop() + } + isTtsSpeaking = false restartIdleCheck() // 状态变化,重启检测 sendEvent("tts_stopped", mapOf("status" to "stopped")) @@ -793,15 +876,15 @@ object AgentService : CoroutineScope { ) // if (assistantMessageId != -1L) { - // FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") + // Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") // } else { - // FileLogger.e(TAG, "保存助手消息失败") + // Log.e(TAG, "保存助手消息失败") // } } else { - FileLogger.e(TAG, "保存用户消息失败") + Log.e(TAG, "保存用户消息失败") } } catch (e: Exception) { - FileLogger.e(TAG, "保存聊天记录异常", e) + Log.e(TAG, "保存聊天记录异常", e) } } } @@ -820,16 +903,16 @@ object AgentService : CoroutineScope { historyMessages.remove(0) } } - FileLogger.d(TAG, "聊天历史已清除") + Log.d(TAG, "聊天历史已清除") } else { - FileLogger.e(TAG, "清除聊天历史失败") + Log.e(TAG, "清除聊天历史失败") } withContext(Dispatchers.Main) { callback(success) } } catch (e: Exception) { - FileLogger.e(TAG, "清除聊天历史异常: ${e.message}") + Log.e(TAG, "清除聊天历史异常: ${e.message}") withContext(Dispatchers.Main) { callback(false) } @@ -848,7 +931,7 @@ object AgentService : CoroutineScope { try { listener.onEvent(eventName, data) } catch (e: Exception) { - FileLogger.e(TAG, "向监听器发送事件失败", e) + Log.e(TAG, "向监听器发送事件失败", e) } } @@ -859,13 +942,60 @@ object AgentService : CoroutineScope { try { listener.onEvent(eventName, data) } catch (e: Exception) { - FileLogger.e(TAG, "向监听器发送事件失败", e) + Log.e(TAG, "向监听器发送事件失败", e) } } } } } + /** + * 切换TTS引擎 + * @param engineType 要切换到的引擎类型 + * @return 是否切换成功 + */ + fun switchTtsEngine(engineType: TtsEngineType): Boolean { + if (!isInitialized) { + Log.e(TAG, "服务未初始化") + return false + } + + // 如果当前已经是该引擎,不需要切换 + if (currentTtsEngine == engineType) { + return true + } + + // 如果当前正在播放,先停止 + if (isTtsSpeaking) { + stopTts() + } + + try { + // 更新当前引擎类型 + currentTtsEngine = engineType + + // 重新初始化TTS引擎 + initializeTtsEngine() + + Log.d(TAG, "TTS引擎已切换为: $engineType") + sendEvent("tts_engine_switched", mapOf("engine" to engineType.name)) + + return true + } catch (e: Exception) { + Log.e(TAG, "切换TTS引擎失败: ${e.message}") + sendEvent("error", mapOf( + "code" to "TTS_ENGINE_SWITCH_FAILED", + "message" to "切换TTS引擎失败: ${e.message}" + )) + return false + } + } + + /** + * 获取当前TTS引擎类型 + */ + fun getCurrentTtsEngine(): TtsEngineType = currentTtsEngine + /** * 释放所有资源 */ @@ -880,15 +1010,18 @@ object AgentService : CoroutineScope { audioPlayer = null azureAsrHelper?.dispose() - azureTtsHelper?.dispose() + + // 释放TTS服务 + ttsService?.release() + ttsService = null job.cancel() clearListeners() isInitialized = false - FileLogger.d(TAG, "代理服务资源已释放") + Log.d(TAG, "代理服务资源已释放") } catch (e: Exception) { - FileLogger.e(TAG, "释放资源异常: ${e.message}") + Log.e(TAG, "释放资源异常: ${e.message}") } } @@ -906,13 +1039,13 @@ object AgentService : CoroutineScope { */ fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "服务未初始化") + Log.e(TAG, "服务未初始化") sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) return false } if (imagePath.isEmpty()) { - FileLogger.e(TAG, "图片路径不能为空") + Log.e(TAG, "图片路径不能为空") sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空")) return false } @@ -944,7 +1077,7 @@ object AgentService : CoroutineScope { // 处理包含图片的消息 processImageWithOpenAI(imageBase64, text, speakResponse) } catch (e: Exception) { - FileLogger.e(TAG, "处理图片失败: ${e.message}") + Log.e(TAG, "处理图片失败: ${e.message}") sendEvent("error", mapOf( "code" to "IMAGE_PROCESSING_ERROR", "message" to e.message.toString() @@ -992,9 +1125,9 @@ object AgentService : CoroutineScope { } } } - FileLogger.d(TAG, "开始播放音频资源") + Log.d(TAG, "开始播放音频资源") } catch (e: Exception) { - FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e) + Log.e(TAG, "播放音频资源异常: ${e.message}", e) release() } } @@ -1023,10 +1156,10 @@ object AgentService : CoroutineScope { val metaStr = functionCallResult.optString("meta") if (metaStr.isNotEmpty()) { val meta = JSONObject(metaStr) - FileLogger.d(TAG, "mcp调用结果: $metaStr") + Log.d(TAG, "mcp调用结果: $metaStr") if (meta.has("card_music")) { //音乐卡片 val cardMusic = meta.getJSONObject("card_music") - FileLogger.d(TAG, "检查到音乐卡片: $cardMusic") + Log.d(TAG, "检查到音乐卡片: $cardMusic") val id = cardMusic.optString("id", "") val url = cardMusic.optString("url", "") val name = cardMusic.optString("name", "") @@ -1048,13 +1181,13 @@ object AgentService : CoroutineScope { playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image)) } if (playlist.isNotEmpty()) { - FileLogger.w(TAG, "自动播放音乐列表 ${playlist}") + Log.w(TAG, "自动播放音乐列表 ${playlist}") processMusicPlayList(playlist) } else { - FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目") + Log.w(TAG, "card_musiclist 中没有有效的音乐条目") } } else { - FileLogger.w(TAG, "card_musiclist.musics 不是一个数组") + Log.w(TAG, "card_musiclist.musics 不是一个数组") } } return metaStr; @@ -1074,7 +1207,7 @@ object AgentService : CoroutineScope { */ fun processMusicPlay(song: Map){ // 在其他 Service、BroadcastReceiver 或 Application 中调用 - FileLogger.i(TAG, "启动音乐服务 播放音乐 $song") + Log.i(TAG, "启动音乐服务 播放音乐 $song") MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) } @@ -1090,10 +1223,10 @@ object AgentService : CoroutineScope { fun processMusicPlayList(songs:List>){ // 如果 playlist 中有有效的歌曲,开始播放 if (songs.isNotEmpty()) { - FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}") + Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}") MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs) } else { - FileLogger.i(TAG, "音乐列表为空,未启动播放服务") + Log.i(TAG, "音乐列表为空,未启动播放服务") } } } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 1fc35fafa..cf00c7f3c 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import org.json.JSONObject import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch @@ -33,7 +33,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream // 添加监听器 AgentService.addListener(this) - FileLogger.d(TAG, "插件已加载") + Log.d(TAG, "插件已加载") } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { @@ -46,7 +46,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream return } - FileLogger.d(TAG, "启动BleMediaService,配置参数: $config") + Log.d(TAG, "启动BleMediaService,配置参数: $config") // 创建Intent启动BleMediaService val intent = Intent(context, BleMediaService::class.java) @@ -59,6 +59,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) } config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } + config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) } + config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) } // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -69,7 +71,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "启动AgentService失败", e) + Log.e(TAG, "启动AgentService失败", e) result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null) } } @@ -79,7 +81,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream context.stopService(intent) result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "停止AgentService失败", e) + Log.e(TAG, "停止AgentService失败", e) result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null) } } @@ -88,7 +90,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream val isStarted = AgentService.startRecognition(false) result.success(isStarted) } catch (e: Exception) { - FileLogger.e(TAG, "启动对话失败", e) + Log.e(TAG, "启动对话失败", e) result.error("START_ERROR", "启动对话失败: ${e.message}", null) } } @@ -97,7 +99,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.stopRecognition() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "停止对话失败", e) + Log.e(TAG, "停止对话失败", e) result.error("STOP_ERROR", "停止对话失败: ${e.message}", null) } } @@ -118,7 +120,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(isProcessing) } catch (e: Exception) { - FileLogger.e(TAG, "处理文本输入失败", e) + Log.e(TAG, "处理文本输入失败", e) result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null) } } @@ -141,7 +143,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(isProcessing) } catch (e: Exception) { - FileLogger.e(TAG, "处理图片输入失败", e) + Log.e(TAG, "处理图片输入失败", e) result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null) } } @@ -156,7 +158,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.speakText(text) result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "语音合成失败", e) + Log.e(TAG, "语音合成失败", e) result.error("TTS_ERROR", "语音合成失败: ${e.message}", null) } } @@ -165,7 +167,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.stopTts() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "停止语音合成失败", e) + Log.e(TAG, "停止语音合成失败", e) result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) } } @@ -175,7 +177,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream result.success(success) } } catch (e: Exception) { - FileLogger.e(TAG, "清除聊天历史失败", e) + Log.e(TAG, "清除聊天历史失败", e) result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null) } } @@ -184,7 +186,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.interruptCurrentResponse() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "中断响应失败", e) + Log.e(TAG, "中断响应失败", e) result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null) } } @@ -193,7 +195,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream AgentService.dispose() result.success(true) } catch (e: Exception) { - FileLogger.e(TAG, "释放资源失败", e) + Log.e(TAG, "释放资源失败", e) result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null) } } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index 345eb4a2a..714d3b7d3 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service import android.content.Context import com.yunqiinnovation.agent_service.AgentService import com.yunqiinnovation.agent_service.AgentServiceListener -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import com.yunqiinnovation.ble_service.BleService import com.yunqiinnovation.ble_service.BleConst @@ -29,11 +29,11 @@ object BleAgent : BleService.Callback, AgentServiceListener { if (isInitialized) return true try { - FileLogger.d(TAG, "初始化语音代理..., $config") + Log.d(TAG, "初始化语音代理..., $config") // 1. 初始化AgentService if (!AgentService.initialize(context, config, true)) { - FileLogger.e(TAG, "语音服务初始化失败") + Log.e(TAG, "语音服务初始化失败") return false } @@ -41,20 +41,20 @@ object BleAgent : BleService.Callback, AgentServiceListener { if (BleService.initialize(context)) { if (BleService.addCallback(this)) { isRegisteredToBle = true - FileLogger.d(TAG, "已注册BLE回调") + Log.d(TAG, "已注册BLE回调") } else { - FileLogger.w(TAG, "BLE回调注册失败") + Log.w(TAG, "BLE回调注册失败") } } else { - FileLogger.e(TAG, "BLE服务初始化失败") + Log.e(TAG, "BLE服务初始化失败") return false } isInitialized = true - FileLogger.d(TAG, "语音代理初始化成功") + Log.d(TAG, "语音代理初始化成功") return true } catch (e: Exception) { - FileLogger.e(TAG, "初始化语音代理异常", e) + Log.e(TAG, "初始化语音代理异常", e) return false } } @@ -64,7 +64,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { */ fun startListening(): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "语音代理未初始化") + Log.e(TAG, "语音代理未初始化") return false } @@ -106,7 +106,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { AgentService.dispose() isInitialized = false - FileLogger.d(TAG, "语音代理资源已释放") + Log.d(TAG, "语音代理资源已释放") } //============================================================================================ @@ -119,16 +119,16 @@ object BleAgent : BleService.Callback, AgentServiceListener { override fun onConnectionStateChanged(state: Int) { // 可选:处理连接状态变化 if (state == BleConst.STATE_CONNECTED) { - FileLogger.d(TAG, "Ble设备已连接,准备好语音交互") + Log.d(TAG, "Ble设备已连接,准备好语音交互") } else if (state == BleConst.STATE_DISCONNECTED) { - FileLogger.d(TAG, "Ble设备已断开,停止语音服务") + Log.d(TAG, "Ble设备已断开,停止语音服务") // 设备断开连接,停止语音服务 // stopListening() } } override fun onAudioDataReceived(data: ByteArray) { - // FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}") + // Log.d(TAG, "onAudioDataReceived, data: ${data.size}") AgentService.pushAudioData(data) // 可选:处理音频数据 } @@ -138,7 +138,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { * 在收到唤醒信号时启动语音识别 */ override fun onWakeupSignalReceived() { - FileLogger.d(TAG, "收到唤醒信号,启动语音识别") + Log.d(TAG, "收到唤醒信号,启动语音识别") // 停止当前TTS,避免冲突 AgentService.interruptCurrentResponse() diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index ea9b0fede..0a811f33a 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -21,7 +21,7 @@ import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat import androidx.media.session.MediaButtonReceiver import com.yunqiinnovation.agent_service.R -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel @@ -56,6 +56,8 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_OPENAI_MODEL = "openaiModel" private const val KEY_SYSTEM_PROMPT = "systemPrompt" private const val KEY_MCP_SERVER = "mcpServer" + private const val KEY_VOLCANO_APP_ID = "volcanoAppId" + private const val KEY_VOLCANO_TOKEN = "volcanoToken" // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -89,8 +91,7 @@ class BleMediaService : Service(), CoroutineScope { override fun onCreate() { super.onCreate() - FileLogger.init(this) - FileLogger.i(TAG, "BleMediaService正在创建...") + Log.i(TAG, "BleMediaService正在创建...") // 设置服务状态 isRunning.set(true) @@ -111,11 +112,11 @@ class BleMediaService : Service(), CoroutineScope { audioPlayer?.play(R.raw.silence, 0f) setPlaybackState(PlaybackStateCompat.STATE_PAUSED) - FileLogger.i(TAG, "媒体服务已启动") + Log.i(TAG, "媒体服务已启动") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - // FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") + Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") // 保存当前Intent,用于后续初始化 currentIntent = intent @@ -159,36 +160,36 @@ class BleMediaService : Service(), CoroutineScope { // 如果Intent中有参数,则保存到SharedPreferences if (configFromIntent.isNotEmpty()) { - FileLogger.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") + Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") saveConfigToPreferences(configFromIntent, prefs) } // 合并Intent和SharedPreferences中的参数 val config = getConfigFromPreferences(prefs) - FileLogger.d(TAG, "当前配置参数: $config") + Log.d(TAG, "当前配置参数: $config") // 检查必要的配置是否存在 if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) { - FileLogger.w(TAG, "语音代理初始化失败: API密钥未配置") + Log.w(TAG, "语音代理初始化失败: API密钥未配置") return } if (isChatAgentInitialized) { - FileLogger.d(TAG, "ChatAgent已经初始化,跳过") + Log.d(TAG, "ChatAgent已经初始化,跳过") return } if(config.isNotEmpty()) { // 初始化ChatAgent - FileLogger.i(TAG, "开始初始化ChatAgent") + Log.i(TAG, "开始初始化ChatAgent") val success = BleAgent.initialize(applicationContext, config) if (success) { isChatAgentInitialized = true - FileLogger.i(TAG, "语音交互代理初始化成功") + Log.i(TAG, "语音交互代理初始化成功") } else { - FileLogger.e(TAG, "语音交互代理初始化失败") + Log.e(TAG, "语音交互代理初始化失败") } } } catch (e: Exception) { - FileLogger.e(TAG, "初始化语音交互代理时发生异常", e) + Log.e(TAG, "初始化语音交互代理时发生异常", e) } } @@ -206,8 +207,10 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value } it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value } it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } + it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value } + it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value } } - FileLogger.i(TAG, "从Intent中提取到配置参数: $config") + Log.i(TAG, "从Intent中提取到配置参数: $config") return config } @@ -236,7 +239,9 @@ class BleMediaService : Service(), CoroutineScope { KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"), KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"), KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), - KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "") + KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""), + KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""), + KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, "") ) } @@ -250,15 +255,15 @@ class BleMediaService : Service(), CoroutineScope { override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { - FileLogger.i(TAG, "BleMediaService正在销毁...") + Log.i(TAG, "BleMediaService正在销毁...") // 取消注册媒体按钮接收器 try { val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) - FileLogger.i(TAG, "媒体按钮接收器已取消注册") + Log.i(TAG, "媒体按钮接收器已取消注册") } catch (e: Exception) { - FileLogger.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) + Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) } // 释放媒体会话 @@ -319,16 +324,16 @@ class BleMediaService : Service(), CoroutineScope { try { val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) - FileLogger.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") + Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") } catch (e: Exception) { - FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) + Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) } } private fun checkBleConnection() { - FileLogger.i(TAG, "checkBleConnection收到连接请求") + Log.i(TAG, "checkBleConnection收到连接请求") try { // 示例:使用协程异步处理连接 @@ -338,7 +343,7 @@ class BleMediaService : Service(), CoroutineScope { } } } catch (e: Exception) { - FileLogger.e(TAG, "连接设备失败: ${e.message}", e) + Log.e(TAG, "连接设备失败: ${e.message}", e) } } @@ -350,7 +355,7 @@ class BleMediaService : Service(), CoroutineScope { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(NOTIFICATION_ID, createNotification()) } catch (e: Exception) { - FileLogger.e(TAG, "更新通知异常: ${e.message}") + Log.e(TAG, "更新通知异常: ${e.message}") } } @@ -451,7 +456,7 @@ class BleMediaService : Service(), CoroutineScope { // // 播放完成后自动循环播放,保持媒体会话活跃 // setOnCompletionListener { - // FileLogger.i(TAG, "静音资源播放完成,再次播放保持会话活跃") + // Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃") // play(resId, volume) // } @@ -460,9 +465,9 @@ class BleMediaService : Service(), CoroutineScope { start() } - FileLogger.i(TAG, "开始播放静音资源") + Log.i(TAG, "开始播放静音资源") } catch (e: Exception) { - FileLogger.e(TAG, "播放静音资源异常: ${e.message}", e) + Log.e(TAG, "播放静音资源异常: ${e.message}", e) release() } } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt index dacd6e2a7..2b5457952 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt @@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * 通用工具类,包含安全存储等功能 @@ -59,10 +59,10 @@ object Utils { // 提交更改 editor.apply() - FileLogger.d(TAG, "数据已安全保存到加密存储中") + Log.d(TAG, "数据已安全保存到加密存储中") true } catch (e: Exception) { - FileLogger.e(TAG, "保存数据到加密存储失败", e) + Log.e(TAG, "保存数据到加密存储失败", e) false } } @@ -97,10 +97,10 @@ object Utils { } } - FileLogger.d(TAG, "已从加密存储加载所有数据") + Log.d(TAG, "已从加密存储加载所有数据") resultMap } catch (e: Exception) { - FileLogger.e(TAG, "从加密存储加载所有数据失败", e) + Log.e(TAG, "从加密存储加载所有数据失败", e) emptyMap() } } @@ -134,10 +134,10 @@ object Utils { .putString(KEY_SESSION_ID, sessionId) .apply() - FileLogger.d(TAG, "已清除所有安全存储的数据") + Log.d(TAG, "已清除所有安全存储的数据") true } catch (e: Exception) { - FileLogger.e(TAG, "清除安全存储数据失败", e) + Log.e(TAG, "清除安全存储数据失败", e) false } } diff --git a/local_plugins/azure_speech/android/build.gradle.kts b/local_plugins/azure_speech/android/build.gradle.kts index 151988920..5d465f450 100644 --- a/local_plugins/azure_speech/android/build.gradle.kts +++ b/local_plugins/azure_speech/android/build.gradle.kts @@ -52,6 +52,6 @@ tasks.withType { dependencies { // 添加Microsoft语音SDK - implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0") + implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0") implementation(project(":ble_service")) } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 54a68eb56..abbe6e38a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -10,7 +10,7 @@ import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.NoiseSuppressor import android.media.audiofx.AutomaticGainControl import android.os.Process -import com.yunqiinnovation.azure_speech.utils.FileLogger +import android.util.Log import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.util.EventHandler @@ -84,7 +84,7 @@ class AzureAsrHelper(private val context: Context) { try { // 检查配置是否为空 if (subscriptionKey.isEmpty() || region.isEmpty()) { - FileLogger.e(tag, "Azure 配置信息不完整") + Log.e(tag, "Azure 配置信息不完整") return false } @@ -124,7 +124,7 @@ class AzureAsrHelper(private val context: Context) { // 创建识别器 return setupRecognizer() } catch (e: Exception) { - FileLogger.e(tag, "初始化失败: ${e.message}") + Log.e(tag, "初始化失败: ${e.message}") return false } } @@ -184,7 +184,7 @@ class AzureAsrHelper(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(tag, "创建识别器失败: ${e.message}") + Log.e(tag, "创建识别器失败: ${e.message}") stopAudioProcessing() return false } @@ -201,8 +201,8 @@ class AzureAsrHelper(private val context: Context) { // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 audioConfig = AudioConfig.fromStreamInput(microphoneStream) } catch (e: Exception) { - FileLogger.e(tag, "设置麦克风流失败: ${e.message}") - e.printStackTrace() + Log.e(tag, "设置麦克风流失败: ${e.message}") + microphoneStream = null } } @@ -217,8 +217,8 @@ class AzureAsrHelper(private val context: Context) { // 创建音频配置 audioConfig = AudioConfig.fromStreamInput(externalAudioStream) } catch (e: Exception) { - FileLogger.e(tag, "设置外部音频流失败: ${e.message}") - e.printStackTrace() + Log.e(tag, "设置外部音频流失败: ${e.message}") + externalAudioStream = null } } @@ -368,23 +368,13 @@ class AzureAsrHelper(private val context: Context) { // 取消事件 recognizer?.canceled?.addEventListener( EventHandler { _, event -> - val errorDetails = try { - event.errorDetails ?: "未知错误" - } catch (e: Exception) { - "未知错误" - } + val errorDetails = event.errorDetails ?: "未知错误" val reason = event.reason.toString() - - FileLogger.e(tag, "识别取消: $errorDetails") - isContinuousRecognitionActive = false - stopAudioProcessing() - - // 直接在当前线程调用回调 callback.onCanceled(reason, errorDetails) - callback.onError("识别取消: $errorDetails") // 兼容旧接口 } ) } + /** * 停止连续语音识别 @@ -393,7 +383,7 @@ class AzureAsrHelper(private val context: Context) { */ fun stopContinuousRecognition(): Boolean { if (speechConfig == null) { - FileLogger.e(tag, "语音服务未初始化") + Log.e(tag, "语音服务未初始化") return false } @@ -403,7 +393,7 @@ class AzureAsrHelper(private val context: Context) { try { if (recognizer == null) { - FileLogger.w(tag, "识别器为空,重置状态") + Log.w(tag, "识别器为空,重置状态") isContinuousRecognitionActive = false return true } @@ -422,7 +412,7 @@ class AzureAsrHelper(private val context: Context) { } catch (e: Exception) { // 强制重置状态 isContinuousRecognitionActive = false - FileLogger.e(tag, "停止连续识别失败: ${e.message}") + Log.e(tag, "停止连续识别失败: ${e.message}") // 停止音频处理 stopAudioProcessing() @@ -432,7 +422,7 @@ class AzureAsrHelper(private val context: Context) { recognizer?.close() recognizer = null } catch (ex: Exception) { - FileLogger.e(tag, "关闭识别器失败: ${ex.message}") + Log.e(tag, "关闭识别器失败: ${ex.message}") } return false @@ -515,7 +505,7 @@ class AzureAsrHelper(private val context: Context) { it.close() microphoneStream = null } catch (e: Exception) { - FileLogger.e(tag, "关闭麦克风流失败: ${e.message}") + Log.e(tag, "关闭麦克风流失败: ${e.message}") e.printStackTrace() } } @@ -525,7 +515,7 @@ class AzureAsrHelper(private val context: Context) { it.close() externalAudioStream = null } catch (e: Exception) { - FileLogger.e(tag, "关闭外部音频流失败: ${e.message}") + Log.e(tag, "关闭外部音频流失败: ${e.message}") e.printStackTrace() } } @@ -623,12 +613,13 @@ class AzureAsrHelper(private val context: Context) { try { val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1 if (result < 0) { - FileLogger.e(tag, "读取音频数据失败: $result") + Log.e(tag, "读取音频数据失败: $result") + return 0 } return result } catch (e: Exception) { - FileLogger.e(tag, "读取音频异常: ${e.message}") - return -1 + Log.e(tag, "读取音频异常: ${e.message}") + return 0 } } @@ -696,7 +687,7 @@ class AzureAsrHelper(private val context: Context) { Thread.currentThread().interrupt() return 0 } catch (e: Exception) { - FileLogger.e(tag, "读取外部音频数据失败: ${e.message}") + Log.e(tag, "读取外部音频数据失败: ${e.message}") return 0 } } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 24ec6ffdf..f90e60cec 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -58,11 +58,6 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { */ fun onSynthesisStarted() {} - /** - * 合成中 - */ - fun onSynthesizing() {} - /** * 合成完成 */ @@ -96,6 +91,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { // 创建语音配置 speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region) + // speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm) speechConfig?.setSpeechSynthesisVoiceName(currentVoice) @@ -166,11 +162,6 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { ttsCallback?.onSynthesisStarted() } - // 合成中事件 - Synthesizing?.addEventListener { _, _ -> - ttsCallback?.onSynthesizing() - } - // 合成完成事件 SynthesisCompleted?.addEventListener { _, _ -> isSpeaking = false diff --git a/local_plugins/ble_service/android/build.gradle.kts b/local_plugins/ble_service/android/build.gradle.kts index 29deed814..545911b67 100644 --- a/local_plugins/ble_service/android/build.gradle.kts +++ b/local_plugins/ble_service/android/build.gradle.kts @@ -30,5 +30,4 @@ dependencies { implementation("androidx.core:core-ktx:1.13.1") // 使用从本地Maven仓库引用的AAR依赖 implementation("com.jieli.audio:jl_audio_decode:2.0.0") - implementation(project(":deepvoice_utils")) } \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt index 502d527e3..0365a65ff 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt @@ -17,7 +17,7 @@ import android.content.SharedPreferences import android.os.Build import android.os.ParcelUuid import androidx.annotation.RequiresApi -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import java.util.regex.Pattern /** @@ -71,7 +71,7 @@ class BleCompanionManager(private val context: Context) { */ fun savePairedMacAddress(macAddress: String) { sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() - FileLogger.i(TAG, "已保存配对设备MAC: $macAddress") + Log.i(TAG, "已保存配对设备MAC: $macAddress") } /** @@ -79,7 +79,7 @@ class BleCompanionManager(private val context: Context) { */ fun clearPairedMacAddress() { sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() - FileLogger.i(TAG, "已清除配对设备MAC") + Log.i(TAG, "已清除配对设备MAC") } /** @@ -110,7 +110,7 @@ class BleCompanionManager(private val context: Context) { idField.get(association).toString() } ?: emptyList() } catch (e: Exception) { - FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) + Log.e(TAG, "获取关联ID失败: ${e.message}", e) emptyList() } } else { @@ -135,7 +135,7 @@ class BleCompanionManager(private val context: Context) { macAddress: String? = null ): Boolean { if (!isCompanionDeviceManagerSupported()) { - FileLogger.e(TAG, "设备不支持CompanionDeviceManager") + Log.e(TAG, "设备不支持CompanionDeviceManager") return false } @@ -148,7 +148,7 @@ class BleCompanionManager(private val context: Context) { // 使用"^设备名称$"格式创建精确匹配的正则表达式 val exactNamePattern = Pattern.compile("^$it$") deviceFilterBuilder.setNamePattern(exactNamePattern) - FileLogger.i(TAG, "设置严格名称匹配: $it") + Log.i(TAG, "设置严格名称匹配: $it") } // 添加服务UUID @@ -156,9 +156,9 @@ class BleCompanionManager(private val context: Context) { try { val uuid = ParcelUuid(java.util.UUID.fromString(it)) deviceFilterBuilder.addServiceUuid(uuid, null) - FileLogger.i(TAG, "设置服务UUID匹配: $it") + Log.i(TAG, "设置服务UUID匹配: $it") } catch (e: Exception) { - FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e) + Log.e(TAG, "设置服务UUID失败: ${e.message}", e) } } @@ -167,7 +167,7 @@ class BleCompanionManager(private val context: Context) { try { // 检查是否是有效的MAC地址格式 if (!isValidMacAddress(it)) { - FileLogger.e(TAG, "无效的MAC地址格式: $it") + Log.e(TAG, "无效的MAC地址格式: $it") return false } @@ -176,15 +176,15 @@ class BleCompanionManager(private val context: Context) { // 设置MAC地址严格匹配 deviceFilterBuilder.setAddress(it) - FileLogger.i(TAG, "设置MAC地址严格匹配: $it") + Log.i(TAG, "设置MAC地址严格匹配: $it") } catch (e: Exception) { - FileLogger.e(TAG, "设置MAC地址失败: ${e.message}", e) + Log.e(TAG, "设置MAC地址失败: ${e.message}", e) return false } } ?: run { // 如果没有提供MAC地址也没有提供设备名称,则返回错误 if (deviceNamePattern == null && serviceUuid == null) { - FileLogger.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") + Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") return false } } @@ -209,9 +209,9 @@ class BleCompanionManager(private val context: Context) { 0, 0 ) - FileLogger.i(TAG, "已启动设备选择对话框") + Log.i(TAG, "已启动设备选择对话框") } catch (e: Exception) { - FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) } } @@ -228,28 +228,28 @@ class BleCompanionManager(private val context: Context) { 0, 0 ) - FileLogger.i(TAG, "已启动设备选择对话框") + Log.i(TAG, "已启动设备选择对话框") } catch (e: Exception) { - FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e) } } // API 33+ (Android 13+) 使用 onAssociationCreated @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun onAssociationCreated(associationInfo: AssociationInfo) { - FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") + Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") // 保存关联ID try { val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") val mac = macAddressMethod.invoke(associationInfo) as? String mac?.let { savePairedMacAddress(it) } } catch (e: Exception) { - FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) + Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) } } override fun onFailure(error: CharSequence?) { - FileLogger.e(TAG, "设备关联失败: $error") + Log.e(TAG, "设备关联失败: $error") } }, null @@ -257,7 +257,7 @@ class BleCompanionManager(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e) + Log.e(TAG, "启动设备关联失败: ${e.message}", e) return false } } @@ -276,7 +276,7 @@ class BleCompanionManager(private val context: Context) { return false } - FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode") + Log.i(TAG, "收到关联结果: resultCode=$resultCode") if (resultCode == Activity.RESULT_OK && data != null) { // 获取选中的蓝牙设备 @@ -295,7 +295,7 @@ class BleCompanionManager(private val context: Context) { val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" data.getStringExtra(extraAssociationIdKey) } catch (e: Exception) { - FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) + Log.e(TAG, "获取关联ID失败: ${e.message}", e) null } } else { @@ -309,20 +309,20 @@ class BleCompanionManager(private val context: Context) { // 验证MAC地址格式 if (!isValidMacAddress(deviceMac)) { - FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") + Log.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") return false } // 检查是否与预期的MAC地址匹配 if (pendingMacAddress != null && pendingMacAddress != deviceMac) { - FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配") + Log.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配") // 如果要严格匹配MAC,可以在这里返回false return false } // 匹配成功后,保存新的MAC地址 savePairedMacAddress(deviceMac) - FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") + Log.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") // 清除临时MAC地址 pendingMacAddress = null @@ -330,10 +330,10 @@ class BleCompanionManager(private val context: Context) { return true } else { - FileLogger.e(TAG, "无法获取关联的蓝牙设备信息") + Log.e(TAG, "无法获取关联的蓝牙设备信息") } } else { - FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") + Log.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") // 清除临时MAC地址 pendingMacAddress = null } @@ -350,7 +350,7 @@ class BleCompanionManager(private val context: Context) { try { val mac = getPairedMacAddress() if (mac.isNullOrEmpty()) { - FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") + Log.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") return } @@ -359,7 +359,7 @@ class BleCompanionManager(private val context: Context) { val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner if (bluetoothLeScanner == null) { - FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") + Log.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") return } @@ -391,9 +391,9 @@ class BleCompanionManager(private val context: Context) { // 启动后台扫描 bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent) - FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac") + Log.i(TAG, "已注册后台扫描,目标设备MAC: $mac") } catch (e: Exception) { - FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) + Log.e(TAG, "注册后台扫描失败: ${e.message}", e) } } @@ -407,7 +407,7 @@ class BleCompanionManager(private val context: Context) { val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner if (bluetoothLeScanner == null) { - FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") + Log.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") return } @@ -428,9 +428,9 @@ class BleCompanionManager(private val context: Context) { // 停止后台扫描 bluetoothLeScanner.stopScan(pendingIntent) - FileLogger.i(TAG, "已取消后台扫描") + Log.i(TAG, "已取消后台扫描") } catch (e: Exception) { - FileLogger.e(TAG, "取消后台扫描失败: ${e.message}", e) + Log.e(TAG, "取消后台扫描失败: ${e.message}", e) } } @@ -454,13 +454,13 @@ class BleCompanionManager(private val context: Context) { } else { companionDeviceManager?.disassociate(id) } - FileLogger.i(TAG, "已解除设备关联: $id") + Log.i(TAG, "已解除设备关联: $id") } catch (e: Exception) { - FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e) + Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e) } } } catch (e: Exception) { - FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e) + Log.e(TAG, "清除所有关联失败: ${e.message}", e) } } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt index 0de2320cf..1e50ccc32 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.os.Build import android.os.IBinder import androidx.annotation.RequiresApi -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * BleCompanionService @@ -19,13 +19,12 @@ class BleCompanionService : CompanionDeviceService() { override fun onCreate() { super.onCreate() - FileLogger.init(applicationContext) - FileLogger.i(TAG, "BleCompanionService已创建") + Log.i(TAG, "BleCompanionService已创建") } override fun onDeviceAppeared(info: AssociationInfo) { super.onDeviceAppeared(info) - FileLogger.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") + Log.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") // 当配对设备出现时,启动前台服务 try { @@ -49,21 +48,21 @@ class BleCompanionService : CompanionDeviceService() { val mac = macAddressMethod.invoke(info) as? String mac?.let { putExtra("mac", it) } } catch (e: Exception) { - FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) + Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e) } } // 启动前台服务 startForegroundService(serviceIntent) - FileLogger.i(TAG, "已启动服务") + Log.i(TAG, "已启动服务") } catch (e: Exception) { - FileLogger.e(TAG, "启动服务失败: ${e.message}", e) + Log.e(TAG, "启动服务失败: ${e.message}", e) } } override fun onDeviceDisappeared(info: AssociationInfo) { super.onDeviceDisappeared(info) - FileLogger.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") + Log.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") // 可以选择停止服务或执行其他清理操作 } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 833148caa..4fddc4c65 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -8,7 +8,7 @@ import android.content.Context import android.content.Intent import android.os.* import androidx.lifecycle.MutableLiveData -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import java.io.File import java.io.FileOutputStream import java.text.SimpleDateFormat @@ -124,17 +124,17 @@ object BleService { try { opusManager = OpusManager() startOpusStreamDecoding() - FileLogger.d(TAG, "OpusManager初始化成功") + Log.d(TAG, "OpusManager初始化成功") } catch (e: OpusException) { - FileLogger.e(TAG, "OpusManager初始化失败: ${e.message}", e) + Log.e(TAG, "OpusManager初始化失败: ${e.message}", e) // 根据需要决定是否因为Opus初始化失败而返回false } isInitialized = true - FileLogger.d(TAG, "BLE服务初始化成功") + Log.d(TAG, "BLE服务初始化成功") return true } catch (e: Exception) { - FileLogger.e(TAG, "BLE服务初始化失败: ${e.message}", e) + Log.e(TAG, "BLE服务初始化失败: ${e.message}", e) return false } } @@ -147,7 +147,7 @@ object BleService { fun addCallback(callback: Callback): Boolean { if (!callbacks.contains(callback)) { callbacks.add(callback) - FileLogger.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}") + Log.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}") return true } return false @@ -161,7 +161,7 @@ object BleService { fun removeCallback(callback: Callback): Boolean { val removed = callbacks.remove(callback) if (removed) { - FileLogger.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}") + Log.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}") } return removed } @@ -171,7 +171,7 @@ object BleService { */ fun clearCallbacks() { callbacks.clear() - FileLogger.d(TAG, "已清除所有BLE回调") + Log.d(TAG, "已清除所有BLE回调") } // ====================================================================================================== @@ -184,7 +184,7 @@ object BleService { */ fun startScan(timeout: Long = 15000): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "BLE服务未初始化") + Log.e(TAG, "BLE服务未初始化") return false } @@ -192,7 +192,7 @@ object BleService { val scanner = bluetoothLeScanner ?: return false if (isScanning) return false - FileLogger.i(TAG, "开始主动扫描BLE设备...") + Log.i(TAG, "开始主动扫描BLE设备...") scanResults.clear() val filter = ScanFilter.Builder() @@ -213,13 +213,13 @@ object BleService { // 设置扫描超时 scanHandler.postDelayed({ - FileLogger.d(TAG, "扫描超时,停止扫描") + Log.d(TAG, "扫描超时,停止扫描") stopScan() }, timeout) return true } catch (e: Exception) { - FileLogger.e(TAG, "启动扫描失败: ${e.message}", e) + Log.e(TAG, "启动扫描失败: ${e.message}", e) return false } } @@ -233,7 +233,7 @@ object BleService { val scanner = bluetoothLeScanner ?: return false if (!isScanning) return false - FileLogger.i(TAG, "停止主动扫描") + Log.i(TAG, "停止主动扫描") try { scanner.stopScan(scanCallback) @@ -243,7 +243,7 @@ object BleService { return true } catch (e: Exception) { - FileLogger.e(TAG, "停止扫描失败: ${e.message}", e) + Log.e(TAG, "停止扫描失败: ${e.message}", e) return false } } @@ -277,12 +277,12 @@ object BleService { val idx = scanResults.indexOfFirst { it.device.address == dev.address } if (idx >= 0) scanResults[idx] = result else scanResults.add(result) } catch (e: Exception) { - FileLogger.e(TAG, "上报扫描结果异常: ${e.message}", e) + Log.e(TAG, "上报扫描结果异常: ${e.message}", e) } } override fun onScanFailed(errorCode: Int) { - FileLogger.e(TAG, "扫描失败 error=$errorCode") + Log.e(TAG, "扫描失败 error=$errorCode") isScanning = false } } @@ -296,7 +296,7 @@ object BleService { val serviceUuids = scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无" - FileLogger.d( + Log.d( TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " + "RSSI=${result.rssi}, 服务UUID=[$serviceUuids]" ) @@ -305,12 +305,12 @@ object BleService { val scanData = scanRecord?.bytes if (scanData != null) { val hexString = scanData.joinToString("") { String.format("%02X", it) } - FileLogger.d(TAG, "广播数据: $hexString") + Log.d(TAG, "广播数据: $hexString") // 解析服务数据 scanRecord.serviceData?.forEach { (uuid, data) -> val dataHex = data.joinToString("") { String.format("%02X", it) } - FileLogger.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") + Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") } } } @@ -331,16 +331,16 @@ object BleService { val pairedMac = companionManager.getPairedMacAddress() if (pairedMac.isNullOrEmpty()) { - FileLogger.w(TAG, "未找到已配对的设备 MAC 地址") + Log.w(TAG, "未找到已配对的设备 MAC 地址") return } - FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") + Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") // 调用现有的 connect 方法 connect(pairedMac) } catch (e: Exception) { - FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e) + Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e) updateConnectionState(BleConst.STATE_ERROR) } } @@ -351,7 +351,7 @@ object BleService { */ fun connect(address: String) { if (connectionState.value == BleConst.STATE_CONNECTED) { - FileLogger.w(TAG, "已经连接中,忽略连接请求") + Log.w(TAG, "已经连接中,忽略连接请求") return } @@ -373,7 +373,7 @@ object BleService { context, false, gattCallback, BluetoothDevice.TRANSPORT_LE ) } catch (e: Exception) { - FileLogger.e(TAG, "连接设备失败: ${e.message}", e) + Log.e(TAG, "连接设备失败: ${e.message}", e) updateConnectionState(BleConst.STATE_ERROR) } } @@ -395,7 +395,7 @@ object BleService { gatt.disconnect() gatt.close() } catch (e: Exception) { - FileLogger.e(TAG, "关闭GATT连接异常: ${e.message}", e) + Log.e(TAG, "关闭GATT连接异常: ${e.message}", e) } bluetoothGatt = null notifyChar = null @@ -452,7 +452,7 @@ object BleService { audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) if (notifyChar == null || writeChar == null) { - FileLogger.e(TAG, "未找到主服务所需特征") + Log.e(TAG, "未找到主服务所需特征") updateConnectionState(BleConst.STATE_ERROR) return } @@ -462,9 +462,9 @@ object BleService { // 设置音频服务的通知(如果存在) if (audioChar != null) { setupNotifications(g, audioChar) - FileLogger.i(TAG, "音频服务特征找到并设置通知") + Log.i(TAG, "音频服务特征找到并设置通知") } else { - FileLogger.w(TAG, "音频服务特征未找到") + Log.w(TAG, "音频服务特征未找到") } // 设置主服务的通知 setupNotifications(g, notifyChar) @@ -496,7 +496,7 @@ object BleService { } // 其他未知数据 else -> { - FileLogger.d( + Log.d( TAG, "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}" ) @@ -505,7 +505,7 @@ object BleService { } // 其他未知特征 else -> { - FileLogger.d( + Log.d( TAG, "收到未知特征数据: ${c.uuid}, 数据=${ data.joinToString(", ") { @@ -520,7 +520,7 @@ object BleService { override fun onCharacteristicWrite( g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int ) { - FileLogger.i( + Log.i( TAG, "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" ) @@ -530,7 +530,7 @@ object BleService { g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) - FileLogger.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") + Log.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") } } @@ -539,7 +539,7 @@ object BleService { */ private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) { if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) { - FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") + Log.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") return } @@ -558,10 +558,10 @@ object BleService { if (opusManager?.isDecodeStream == true) { opusManager?.writeAudioStream(data) } else { - // FileLogger.d(TAG, "Opus解码流未启动,忽略音频数据") + // Log.d(TAG, "Opus解码流未启动,忽略音频数据") } } catch (e: Exception) { - FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e) + Log.e(TAG, "处理音频数据异常: ${e.message}", e) } } @@ -570,12 +570,12 @@ object BleService { * 处理设备响应数据 - 帧头为0xBB */ private fun processDeviceResponse(data: ByteArray) { - FileLogger.d( + Log.d( TAG, "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" ) if (data.size < 3) { - FileLogger.e(TAG, "设备响应数据格式错误:数据长度过短") + Log.e(TAG, "设备响应数据格式错误:数据长度过短") return } @@ -584,7 +584,7 @@ object BleService { // 判断命令失败响应(长度为0) if (data.size == 4 && data[2].toInt() and 0xFF == 0) { - FileLogger.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") + Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") // 创建失败结果Map val resultMap = mapOf( "success" to false, @@ -600,7 +600,7 @@ object BleService { // 检查数据长度 if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC - FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") + Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") return } @@ -610,7 +610,7 @@ object BleService { val receivedCrc = data[data.size - 1].toInt() and 0xFF if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - FileLogger.e( + Log.e( TAG, "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" ) @@ -623,7 +623,7 @@ object BleService { System.arraycopy(data, 3, responseData, 0, length) } - FileLogger.d( + Log.d( TAG, "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ responseData.joinToString(", ") { @@ -648,7 +648,7 @@ object BleService { val version = responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } resultMap["version"] = version - FileLogger.i(TAG, "接收到目标版本号: $version") + Log.i(TAG, "接收到目标版本号: $version") } notifyDeviceInfoReceived(cmdType, resultMap) @@ -666,7 +666,7 @@ object BleService { val productId = responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } resultMap["productId"] = productId - FileLogger.i(TAG, "接收到产品ID: $productId") + Log.i(TAG, "接收到产品ID: $productId") } notifyDeviceInfoReceived(cmdType, resultMap) @@ -682,7 +682,7 @@ object BleService { if (responseData.isNotEmpty()) { val colorId = (responseData[0].toInt() and 0xFF).toString() resultMap["colorId"] = colorId - FileLogger.i(TAG, "接收到耳机颜色ID: $colorId") + Log.i(TAG, "接收到耳机颜色ID: $colorId") } notifyDeviceInfoReceived(cmdType, resultMap) @@ -719,7 +719,7 @@ object BleService { resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - FileLogger.i( + Log.i( TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + @@ -763,7 +763,7 @@ object BleService { resultMap["statusDesc"] = statusDesc resultMap["channelDesc"] = channelDesc - FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") + Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") } notifyDeviceInfoReceived(cmdType, resultMap) @@ -777,7 +777,7 @@ object BleService { "rawData" to responseData, "isUnknown" to true ) - FileLogger.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") + Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") notifyDeviceInfoReceived(cmdType, resultMap) } } @@ -787,12 +787,12 @@ object BleService { * 处理设备主动上报数据 - 帧头为0xCC */ private fun processDeviceNotification(data: ByteArray) { - FileLogger.d( + Log.d( TAG, "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" ) if (data.size < 3) { - FileLogger.e(TAG, "设备主动上报数据格式错误:数据长度过短") + Log.e(TAG, "设备主动上报数据格式错误:数据长度过短") return } @@ -802,7 +802,7 @@ object BleService { // 检查数据长度 if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC - FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") + Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") return } @@ -812,7 +812,7 @@ object BleService { val receivedCrc = data[data.size - 1].toInt() and 0xFF if (calculatedCrc.toInt() and 0xFF != receivedCrc) { - FileLogger.e( + Log.e( TAG, "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" ) @@ -825,7 +825,7 @@ object BleService { System.arraycopy(data, 3, notifyData, 0, length) } - FileLogger.d( + Log.d( TAG, "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ notifyData.joinToString(", ") { @@ -870,7 +870,7 @@ object BleService { resultMap["caseCharging"] = caseCharging resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 - FileLogger.i( + Log.i( TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + @@ -882,7 +882,7 @@ object BleService { } BleConst.CMD_WAKE_UP -> { - FileLogger.i(TAG, "收到唤醒指令") + Log.i(TAG, "收到唤醒指令") // 接收到唤醒信号,打开编码器 (设备侧) openEncoder() notifyWakeupSignalReceived() @@ -898,7 +898,7 @@ object BleService { "isUnknown" to true, "isNotification" to true // 标记为主动上报 ) - FileLogger.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") + Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") notifyDeviceInfoReceived(infoType, resultMap) } } @@ -915,13 +915,13 @@ object BleService { packetSize: Int = 40 // 确认设备端Opus编码帧长 ): Boolean { if (opusManager == null) { - FileLogger.e(TAG, "OpusManager未初始化,无法开始解码") + Log.e(TAG, "OpusManager未初始化,无法开始解码") return false } // 如果已经在解码流,先停止 if (opusManager?.isDecodeStream == true) { opusManager?.stopDecodeStream() - FileLogger.d(TAG, "已停止正在进行的Opus解码流") + Log.d(TAG, "已停止正在进行的Opus解码流") } val option = OpusOption() @@ -930,29 +930,29 @@ object BleService { .setSampleRate(sampleRate) .setPacketSize(packetSize) - FileLogger.d(TAG, "准备开始Opus数据流解码, 参数: $option") + Log.d(TAG, "准备开始Opus数据流解码, 参数: $option") var streamStartedSuccessfully = false opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { override fun onDecodeStream(data: ByteArray?) { if (data != null) { - // FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes") + // Log.d(TAG, "Opus解码数据: ${data.size} bytes") notifyAudioDataReceived(data) } } override fun onStart() { streamStartedSuccessfully = true - FileLogger.i(TAG, "Opus数据流解码已开始") + Log.i(TAG, "Opus数据流解码已开始") // 可以通过回调通知上层解码已开始 } override fun onComplete(outPath: String?) { - FileLogger.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") + Log.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") } override fun onError(code: Int, message: String?) { - FileLogger.e(TAG, "Opus数据流解码错误: [$code] $message") + Log.e(TAG, "Opus数据流解码错误: [$code] $message") // 可以通过回调通知上层解码错误 } }) @@ -965,10 +965,10 @@ object BleService { private fun stopOpusStreamDecoding(): Boolean { if (opusManager?.isDecodeStream == true) { opusManager?.stopDecodeStream() - FileLogger.i(TAG, "已停止Opus数据流解码") + Log.i(TAG, "已停止Opus数据流解码") return true } - FileLogger.d(TAG, "Opus数据流未在解码或OpusManager未初始化") + Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化") return false } @@ -980,7 +980,7 @@ object BleService { * 获取耳机版本号 */ fun getDeviceVersion(): Boolean { - FileLogger.i(TAG, "获取耳机版本号...") + Log.i(TAG, "获取耳机版本号...") return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte())) } @@ -988,7 +988,7 @@ object BleService { * 获取产品ID */ fun getProductId(): Boolean { - FileLogger.i(TAG, "获取耳机产品ID...") + Log.i(TAG, "获取耳机产品ID...") return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte())) } @@ -996,7 +996,7 @@ object BleService { * 获取耳机颜色ID */ fun getColorId(): Boolean { - FileLogger.i(TAG, "获取耳机颜色ID...") + Log.i(TAG, "获取耳机颜色ID...") return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte())) } @@ -1004,7 +1004,7 @@ object BleService { * 获取耳机电量信息 */ fun getBatteryInfo(): Boolean { - FileLogger.i(TAG, "获取耳机电量信息...") + Log.i(TAG, "获取耳机电量信息...") return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte())) } @@ -1012,7 +1012,7 @@ object BleService { * 控制编解码 - 关闭编解码 */ fun closeCodec(): Boolean { - FileLogger.i(TAG, "关闭编解码...") + Log.i(TAG, "关闭编解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), @@ -1028,7 +1028,7 @@ object BleService { * 控制编解码 - 打开解码 */ fun openDecoder(): Boolean { - FileLogger.i(TAG, "打开编码 (设备侧)...") + Log.i(TAG, "打开编码 (设备侧)...") // val opusStarted = startOpusStreamDecoding( // hasHeader = false, // BLE传输的Opus数据通常无文件头 @@ -1041,7 +1041,7 @@ object BleService { // ) - FileLogger.i(TAG, "打开解码...") + Log.i(TAG, "打开解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CODEC_CONTROL_DECODE_ON.toByte(), @@ -1055,9 +1055,9 @@ object BleService { * 控制编解码 - 打开解码 */ fun openA2DPDecoder(): Boolean { - FileLogger.i(TAG, "打开编码 (设备侧)...") + Log.i(TAG, "打开编码 (设备侧)...") - // FileLogger.i(TAG, "打开解码...") + // Log.i(TAG, "打开解码...") return sendCommand( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), @@ -1071,7 +1071,7 @@ object BleService { * 控制编解码 - A2DP播放 */ fun playA2DP(): Boolean { - FileLogger.i(TAG, "切换至A2DP播放模式...") + Log.i(TAG, "切换至A2DP播放模式...") return sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) } @@ -1079,7 +1079,7 @@ object BleService { * 控制编解码 - 打开编码 */ fun openEncoder(): Boolean { - FileLogger.i(TAG, "打开编码 (设备侧)...") + Log.i(TAG, "打开编码 (设备侧)...") // 目前仅发送命令通知设备开始编码。 @@ -1099,11 +1099,11 @@ object BleService { */ fun setChannelMode(channelMode: Int): Boolean { when (channelMode) { - BleConst.AUDIO_CHANNEL_LEFT -> FileLogger.i(TAG, "设置左声道模式...") - BleConst.AUDIO_CHANNEL_RIGHT -> FileLogger.i(TAG, "设置右声道模式...") - BleConst.AUDIO_CHANNEL_STEREO -> FileLogger.i(TAG, "设置立体声模式...") + BleConst.AUDIO_CHANNEL_LEFT -> Log.i(TAG, "设置左声道模式...") + BleConst.AUDIO_CHANNEL_RIGHT -> Log.i(TAG, "设置右声道模式...") + BleConst.AUDIO_CHANNEL_STEREO -> Log.i(TAG, "设置立体声模式...") else -> { - FileLogger.e(TAG, "无效的声道模式: $channelMode") + Log.e(TAG, "无效的声道模式: $channelMode") return false } } @@ -1121,7 +1121,7 @@ object BleService { * 音量增大 */ fun volumeUp(): Boolean { - FileLogger.i(TAG, "设备音量增加...") + Log.i(TAG, "设备音量增加...") return sendCommand(BleConst.CMD_VOLUME_UP.toByte()) } @@ -1129,7 +1129,7 @@ object BleService { * 音量减小 */ fun volumeDown(): Boolean { - FileLogger.i(TAG, "设备音量减小...") + Log.i(TAG, "设备音量减小...") return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) } @@ -1183,7 +1183,7 @@ object BleService { it.release() } opusManager = null - FileLogger.d(TAG, "OpusManager已释放") + Log.d(TAG, "OpusManager已释放") isInitialized = false // 标记为未初始化 } @@ -1199,7 +1199,7 @@ object BleService { */ private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { if (!checkConn()) { - FileLogger.e(TAG, "发送命令失败: 设备未连接") + Log.e(TAG, "发送命令失败: 设备未连接") return false } @@ -1230,7 +1230,7 @@ object BleService { cmdPacket[3 + cmdLength] = crc // 记录命令详情 - FileLogger.d( + Log.d( TAG, "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + @@ -1241,7 +1241,7 @@ object BleService { writeChar!!.value = cmdPacket return bluetoothGatt?.writeCharacteristic(writeChar) ?: false } catch (e: Exception) { - FileLogger.e(TAG, "发送命令异常: ${e.message}", e) + Log.e(TAG, "发送命令异常: ${e.message}", e) return false } } @@ -1274,7 +1274,7 @@ object BleService { try { callback.onScanResult(devices) } catch (e: Exception) { - FileLogger.e(TAG, "分发扫描结果回调异常", e) + Log.e(TAG, "分发扫描结果回调异常", e) } } } @@ -1287,7 +1287,7 @@ object BleService { try { callback.onConnectionStateChanged(state) } catch (e: Exception) { - FileLogger.e(TAG, "分发连接状态回调异常", e) + Log.e(TAG, "分发连接状态回调异常", e) } } } @@ -1300,7 +1300,7 @@ object BleService { try { callback.onAudioDataReceived(data) } catch (e: Exception) { - FileLogger.e(TAG, "分发音频数据回调异常", e) + Log.e(TAG, "分发音频数据回调异常", e) } } } @@ -1313,7 +1313,7 @@ object BleService { try { callback.onWakeupSignalReceived() } catch (e: Exception) { - FileLogger.e(TAG, "分发唤醒信号回调异常", e) + Log.e(TAG, "分发唤醒信号回调异常", e) } } } @@ -1326,7 +1326,7 @@ object BleService { try { callback.onDeviceInfoReceived(infoType, infoData) } catch (e: Exception) { - FileLogger.e(TAG, "分发设备信息回调异常", e) + Log.e(TAG, "分发设备信息回调异常", e) } } } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 6ffe8d51e..68f15312a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -15,7 +15,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.PluginRegistry -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * BleServicePlugin @@ -52,8 +52,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, private var activity: android.app.Activity? = null override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - FileLogger.init(flutterPluginBinding.applicationContext) - FileLogger.d(TAG, "BleServicePlugin onAttachedToEngine") + Log.d(TAG, "BleServicePlugin onAttachedToEngine") // 设置方法通道 methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.ble_service") @@ -150,14 +149,14 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, try { eventSink.success(data) } catch (e: Exception) { - FileLogger.e(TAG, "$errorPrefix: ${e.message}", e) + Log.e(TAG, "$errorPrefix: ${e.message}", e) } } else { mainHandler.post { try { eventSink.success(data) } catch (e: Exception) { - FileLogger.e(TAG, "$errorPrefix: ${e.message}", e) + Log.e(TAG, "$errorPrefix: ${e.message}", e) } } } @@ -175,7 +174,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - FileLogger.d(TAG, "Method call: ${call.method}") + Log.d(TAG, "Method call: ${call.method}") when (call.method) { "isConnected" -> { diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt index 57fb40298..78557535a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt @@ -5,7 +5,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log /** * BleWakeReceiver @@ -15,8 +15,7 @@ class BleWakeReceiver : BroadcastReceiver() { private val TAG = "BleWakeReceiver" override fun onReceive(context: Context, intent: Intent) { - FileLogger.init(context) - FileLogger.i(TAG, "收到BLE扫描广播: ${intent.action}") + Log.i(TAG, "收到BLE扫描广播: ${intent.action}") if (intent.action == BleConst.ACTION_SCAN_RESULT) { // 从扫描结果中提取MAC地址 @@ -44,7 +43,7 @@ class BleWakeReceiver : BroadcastReceiver() { val serviceClass = try { Class.forName("com.yunqiinnovation.agent_service.BleMediaService") } catch (e: ClassNotFoundException) { - FileLogger.i(TAG, "BleMediaService不存在") + Log.i(TAG, "BleMediaService不存在") return } @@ -57,13 +56,13 @@ class BleWakeReceiver : BroadcastReceiver() { // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent) - FileLogger.i(TAG, "已启动前台服务: ${serviceClass.name}") + Log.i(TAG, "已启动前台服务: ${serviceClass.name}") } else { context.startService(serviceIntent) - FileLogger.i(TAG, "已启动服务: ${serviceClass.name}") + Log.i(TAG, "已启动服务: ${serviceClass.name}") } } catch (e: Exception) { - FileLogger.e(TAG, "启动BLE服务失败: ${e.message}", e) + Log.e(TAG, "启动BLE服务失败: ${e.message}", e) } } } \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/build.gradle.kts b/local_plugins/bytedance_speech/android/build.gradle.kts index 0073e010c..e56441e6a 100644 --- a/local_plugins/bytedance_speech/android/build.gradle.kts +++ b/local_plugins/bytedance_speech/android/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.yunqiinnovation.ble_service" + namespace = "com.deep_voice.bytedance_speech" compileSdk = 35 defaultConfig { diff --git a/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml b/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml index 9f3f16d19..3fcca60bd 100644 --- a/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml +++ b/local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt index 34eefdf4b..1446ed730 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt @@ -1,6 +1,7 @@ package com.deep_voice.bytedance_speech import android.content.Context +import android.util.Log import com.google.gson.Gson import com.google.gson.JsonObject import kotlinx.coroutines.Dispatchers @@ -13,6 +14,7 @@ import okhttp3.WebSocketListener import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor.Level import okio.ByteString +import org.json.JSONObject import java.io.ByteArrayOutputStream import java.util.UUID import java.util.concurrent.TimeUnit @@ -31,6 +33,7 @@ class BytedanceASR( private val statusCallback: (String) -> Unit ) { companion object { + private const val TAG = "BytedanceASR" // 协议常量 private const val PROTOCOL_VERSION = 0x01 private const val DEFAULT_HEADER_SIZE = 0x01 @@ -80,7 +83,7 @@ class BytedanceASR( private var supportedLanguages: List = listOf("zh-CN") // 状态控制 - private var isRunning = false + private var isRecognizing = false private var isLastPacket = false private var isContinuousMode = false private var isInitialized = false @@ -97,10 +100,10 @@ class BytedanceASR( } client = OkHttpClient.Builder() - .pingInterval(50, TimeUnit.SECONDS) + .pingInterval(30, TimeUnit.SECONDS) .addInterceptor(loggingInterceptor) - .readTimeout(100, TimeUnit.SECONDS) - .writeTimeout(100, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) .build() } @@ -146,7 +149,7 @@ class BytedanceASR( return@withContext false } - if (isRunning) { + if (isRecognizing) { errorCallback("BUSY", "语音识别正在进行中") return@withContext false } @@ -163,12 +166,12 @@ class BytedanceASR( // 等待连接建立 var attempts = 0 - while (!isRunning && attempts < 10) { + while (!isRecognizing && attempts < 10) { delay(300) attempts++ } - if (!isRunning) { + if (!isRecognizing) { errorCallback("CONNECTION_ERROR", "无法建立连接") return@withContext false } @@ -197,7 +200,7 @@ class BytedanceASR( return@withContext false } - if (isRunning) { + if (isRecognizing) { errorCallback("BUSY", "语音识别正在进行中") return@withContext false } @@ -223,7 +226,7 @@ class BytedanceASR( */ suspend fun stopContinuousRecognition(): Boolean = withContext(Dispatchers.IO) { try { - if (!isRunning || !isContinuousMode) { + if (!isRecognizing || !isContinuousMode) { return@withContext true // 已经停止或非连续模式 } @@ -242,7 +245,7 @@ class BytedanceASR( */ suspend fun pushAudioData(audioData: ByteArray): Boolean = withContext(Dispatchers.IO) { try { - if (!isRunning) { + if (!isRecognizing) { errorCallback("NOT_RUNNING", "语音识别未启动") return@withContext false } @@ -277,10 +280,10 @@ class BytedanceASR( /** * 开始语音识别 */ - private suspend fun start() = withContext(Dispatchers.IO) { - if (isRunning) return@withContext + suspend fun start() = withContext(Dispatchers.IO) { + if (isRecognizing) return@withContext - isRunning = true + isRecognizing = true isLastPacket = false seq = 1 @@ -303,114 +306,81 @@ class BytedanceASR( webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { val logId = response.header("X-Tt-Logid") - println("ASR WebSocket连接成功, X-Tt-Logid: $logId") + Log.d(TAG, "ASR WebSocket连接成功, X-Tt-Logid: $logId") // 发送初始配置请求 sendInitialRequest(webSocket) } - override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + override fun onMessage(webSocket: WebSocket, text: String) { + Log.d(TAG, "收到文本消息: $text") try { - val data = bytes.toByteArray() - val response = parseResponse(data) - - // 根据消息类型处理响应 - when (response.messageType) { - FULL_SERVER_RESPONSE -> { - // 解析识别结果 - val isFinal = response.messageTypeSpecificFlags == NEG_WITH_SEQUENCE - - val resultText = if (response.payload.isNotEmpty()) { - val decompressedData = if (response.compression == GZIP) { - gzipDecompress(response.payload) - } else { - response.payload - } - - val jsonStr = String(decompressedData) - extractTextFromJson(jsonStr) - } else { - "" - } - - if (resultText.isNotEmpty()) { - // 处理单次识别模式 - if (!isContinuousMode) { - onceResultBuilder.append(resultText) - } - - // 回调识别结果 - resultCallback(resultText, isFinal) - } - - // 如果是最后一个包,关闭连接 - if (isFinal) { - // 单次识别完成回调 - if (!isContinuousMode && singleRecognitionCallback != null) { - val finalResult = onceResultBuilder.toString() - singleRecognitionCallback?.invoke(finalResult) - onceResultBuilder.clear() - singleRecognitionCallback = null - } - - if (!isContinuousMode) { - isRunning = false - updateStatus(STATUS_STOPPED) - webSocket.close(1000, "Finished") - } - } - } - SERVER_ERROR_RESPONSE -> { - val errorMessage = String(response.payload) - errorCallback(response.sequence.toString(), errorMessage) - isRunning = false - updateStatus(STATUS_ERROR) - webSocket.close(1000, "Error") - } - } + val json = JSONObject(text) + // 解析结果 + resultCallback(json.optString("text", ""), false) } catch (e: Exception) { - errorCallback("PARSE_ERROR", e.message ?: "解析响应失败") - updateStatus(STATUS_ERROR) + Log.e(TAG, "解析消息失败", e) } } - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - isRunning = false - errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") - updateStatus(STATUS_ERROR) + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + Log.d(TAG, "收到二进制消息") + // 在实际实现中解析二进制消息 + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Log.d(TAG, "连接关闭中: $code, $reason") + isRecognizing = false } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - isRunning = false - updateStatus(STATUS_STOPPED) - println("ASR WebSocket连接关闭: $code, $reason") + Log.d(TAG, "连接已关闭: $code, $reason") + isRecognizing = false + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + Log.e(TAG, "连接失败", t) + errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") + isRecognizing = false } }) } /** - * 发送音频数据 + * 向服务器发送音频数据 */ - private suspend fun feedAudioData(audioData: ByteArray) = withContext(Dispatchers.IO) { - if (!isRunning || isLastPacket) return@withContext + suspend fun feedAudioData(audioData: ByteArray) { + if (!isRecognizing || webSocket == null) { + Log.w(TAG, "未开始识别或WebSocket未连接") + return + } - webSocket?.let { ws -> - // 发送音频请求 - sendAudioRequest(ws, audioData, false) + try { + // 在实际实现中需要按照火山引擎的协议格式封装音频数据 + val byteString = ByteString.of(*audioData) + webSocket?.send(byteString) + } catch (e: Exception) { + Log.e(TAG, "发送音频数据失败", e) } } /** - * 停止识别 + * 停止语音识别 */ - private suspend fun stop() = withContext(Dispatchers.IO) { - if (!isRunning || isLastPacket) return@withContext - - isLastPacket = true + suspend fun stop() { + if (!isRecognizing) { + return + } - webSocket?.let { ws -> - // 发送最后一个音频包 - sendAudioRequest(ws, ByteArray(0), true) + try { + // 发送最后一帧音频数据,标记结束 + // 在实际实现中需要按照协议发送结束标记 + + isRecognizing = false + webSocket?.close(1000, "正常关闭") + webSocket = null + } catch (e: Exception) { + Log.e(TAG, "停止识别失败", e) } } @@ -420,7 +390,7 @@ class BytedanceASR( fun release() { webSocket?.cancel() webSocket = null - isRunning = false + isRecognizing = false isLastPacket = false updateStatus(STATUS_STOPPED) } @@ -429,123 +399,133 @@ class BytedanceASR( * 发送初始配置请求 */ private fun sendInitialRequest(webSocket: WebSocket) { - // 构建请求参数 - val user = JsonObject().apply { - addProperty("uid", "test") - } - - val audio = JsonObject().apply { - addProperty("format", "pcm") - addProperty("sample_rate", sampleRate) - addProperty("bits", bits) - addProperty("channel", channels) - addProperty("codec", "raw") - } - - val request = JsonObject().apply { - addProperty("model_name", "bigmodel") - addProperty("enable_punc", enablePunctuation) - } - - val payload = JsonObject().apply { - add("user", user) - add("audio", audio) - add("request", request) + try { + // 构建请求参数 + val user = JsonObject().apply { + addProperty("uid", "test") + } + + val audio = JsonObject().apply { + addProperty("format", "pcm") + addProperty("sample_rate", sampleRate) + addProperty("bits", bits) + addProperty("channel", channels) + addProperty("codec", "raw") + } + + val request = JsonObject().apply { + addProperty("model_name", "bigmodel") + addProperty("enable_punc", enablePunctuation) + } + + val payload = JsonObject().apply { + add("user", user) + add("audio", audio) + add("request", request) + } + + val payloadStr = payload.toString() + Log.d(TAG, "ASR初始请求: $payloadStr") + + // 压缩负载 + val compressedPayload = gzipCompress(payloadStr.toByteArray()) + + // 构建消息头 + val header = getHeader( + FULL_CLIENT_REQUEST, + POS_SEQUENCE, + JSON, + GZIP, + 0 + ) + + // 构建消息序列号 + val seqBytes = intToBytes(seq) + + // 构建负载大小 + val payloadSizeBytes = intToBytes(compressedPayload.size) + + // 组装完整请求数据 + val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) + var offset = 0 + + System.arraycopy(header, 0, requestBytes, offset, header.size) + offset += header.size + + System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) + offset += seqBytes.size + + System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) + offset += payloadSizeBytes.size + + System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) + + // 发送请求 + val byteString = ByteString.of(*requestBytes) + webSocket.send(byteString) + } catch (e: Exception) { + Log.e(TAG, "发送初始请求失败", e) } - - val payloadStr = payload.toString() - println("ASR初始请求: $payloadStr") - - // 压缩负载 - val compressedPayload = gzipCompress(payloadStr.toByteArray()) - - // 构建消息头 - val header = getHeader( - FULL_CLIENT_REQUEST, - POS_SEQUENCE, - JSON, - GZIP, - 0 - ) - - // 构建消息序列号 - val seqBytes = intToBytes(seq) - - // 构建负载大小 - val payloadSizeBytes = intToBytes(compressedPayload.size) - - // 组装完整请求数据 - val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) - var offset = 0 - - System.arraycopy(header, 0, requestBytes, offset, header.size) - offset += header.size - - System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) - offset += seqBytes.size - - System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) - offset += payloadSizeBytes.size - - System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) - - // 发送请求 - webSocket.send(ByteString.of(requestBytes)) } /** * 发送音频数据请求 */ private fun sendAudioRequest(webSocket: WebSocket, audioData: ByteArray, isLast: Boolean) { - seq++ - - // 如果是最后一个包,使用负序列号 - val actualSeq = if (isLast) -seq else seq - - // 设置消息标志 - val flag = if (isLast) NEG_WITH_SEQUENCE else POS_SEQUENCE - - // 构建消息头 - val header = getHeader( - AUDIO_ONLY_REQUEST, - flag, - NO_SERIALIZATION, - GZIP, - 0 - ) - - // 构建消息序列号 - val seqBytes = intToBytes(actualSeq) - - // 压缩音频数据 - val compressedPayload = if (audioData.isNotEmpty()) { - gzipCompress(audioData) - } else { - ByteArray(0) - } - - // 构建负载大小 - val payloadSizeBytes = intToBytes(compressedPayload.size) - - // 组装完整请求数据 - val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) - var offset = 0 - - System.arraycopy(header, 0, requestBytes, offset, header.size) - offset += header.size - - System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) - offset += seqBytes.size - - System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) - offset += payloadSizeBytes.size - - if (compressedPayload.isNotEmpty()) { - System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) + try { + seq++ + + // 如果是最后一个包,使用负序列号 + val actualSeq = if (isLast) -seq else seq + + // 设置消息标志 + val flag = if (isLast) NEG_WITH_SEQUENCE else POS_SEQUENCE + + // 构建消息头 + val header = getHeader( + AUDIO_ONLY_REQUEST, + flag, + NO_SERIALIZATION, + GZIP, + 0 + ) + + // 构建消息序列号 + val seqBytes = intToBytes(actualSeq) + + // 压缩音频数据 + val compressedPayload = if (audioData.isNotEmpty()) { + gzipCompress(audioData) + } else { + ByteArray(0) + } + + // 构建负载大小 + val payloadSizeBytes = intToBytes(compressedPayload.size) + + // 组装完整请求数据 + val requestBytes = ByteArray(header.size + seqBytes.size + payloadSizeBytes.size + compressedPayload.size) + var offset = 0 + + System.arraycopy(header, 0, requestBytes, offset, header.size) + offset += header.size + + System.arraycopy(seqBytes, 0, requestBytes, offset, seqBytes.size) + offset += seqBytes.size + + System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) + offset += payloadSizeBytes.size + + if (compressedPayload.isNotEmpty()) { + System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) + } + + // 发送请求 + val byteString = ByteString.of(*requestBytes) + webSocket.send(byteString) + } catch (e: Exception) { + Log.e(TAG, "发送音频请求失败", e) } - - // 发送请求 - webSocket.send(ByteString.of(requestBytes)) } /** @@ -619,7 +599,7 @@ class BytedanceASR( return "" } catch (e: Exception) { - println("解析JSON失败: ${e.message}") + Log.e(TAG, "解析JSON失败", e) return "" } } diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt index 0ee9b1092..1a3447ffc 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt @@ -3,216 +3,170 @@ package com.deep_voice.bytedance_speech import android.content.Context import android.os.Handler import android.os.Looper +import android.util.Log import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.EventChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentLinkedQueue -/** 火山引擎语音识别和语音合成插件 */ +/** + * 字节跳动语音服务Flutter插件 + * 提供TTS和ASR能力,通过独立的事件通道进行通信 + */ class BytedanceSpeechPlugin : FlutterPlugin { - private val TAG = "BytedanceSpeechPlugin" - private lateinit var context: Context - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val mainHandler = Handler(Looper.getMainLooper()) + companion object { + private const val TAG = "BytedanceSpeechPlugin" + + // 通道名称 + private const val TTS_METHOD_CHANNEL = "com.deep_voice.bytedance_tts" + private const val ASR_METHOD_CHANNEL = "com.deep_voice.bytedance_asr" + + // 事件通道 + private const val TTS_EVENT_CHANNEL = "com.deep_voice.bytedance_tts/events" + private const val TTS_AUDIO_CHANNEL = "com.deep_voice.bytedance_tts/audio" + private const val ASR_EVENT_CHANNEL = "com.deep_voice.bytedance_asr/events" + } - // ASR相关 + // Flutter插件相关 + private lateinit var ttsChannel: MethodChannel private lateinit var asrChannel: MethodChannel - private lateinit var asrEventChannel: EventChannel - private var asrEventSink: EventChannel.EventSink? = null - private var asrInstance: BytedanceASR? = null + private lateinit var context: Context - // TTS相关 - private lateinit var ttsChannel: MethodChannel - private lateinit var ttsEventChannel: EventChannel + // TTS事件通道 private var ttsEventSink: EventChannel.EventSink? = null - private var ttsInstance: BytedanceTTS? = null + private var ttsAudioSink: EventChannel.EventSink? = null - // ASR事件发送方法 - private fun sendAsrEvent(event: Map) { - println("发送ASR事件: $event") - if (asrEventSink == null) { - println("无法发送ASR事件:事件通道未准备好") - return - } - - mainHandler.post { - try { - asrEventSink?.success(event) - println("ASR事件发送成功") - } catch (e: Exception) { - println("发送ASR事件失败: ${e.message}") - } - } - } + // ASR事件通道 + private var asrEventSink: EventChannel.EventSink? = null - // TTS事件发送方法 - private fun sendTtsEvent(event: Map) { - println("发送TTS事件: $event") - if (ttsEventSink == null) { - println("无法发送TTS事件:事件通道未准备好") - return - } - - mainHandler.post { - try { - ttsEventSink?.success(event) - println("TTS事件发送成功") - } catch (e: Exception) { - println("发送TTS事件失败: ${e.message}") - } - } - } + // 语音服务实例 + private var bytedanceTTS: BytedanceTTS? = null + private var bytedanceASR: BytedanceASR? = null + + // 协程作用域 + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.Main + job) + + // 主线程Handler + private val mainHandler = Handler(Looper.getMainLooper()) override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext - // 初始化ASR通道 - asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr") + // 初始化TTS方法通道 + ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, TTS_METHOD_CHANNEL) + ttsChannel.setMethodCallHandler(TtsMethodHandler()) + + // 初始化ASR方法通道 + asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, ASR_METHOD_CHANNEL) asrChannel.setMethodCallHandler(AsrMethodHandler()) - // 初始化TTS通道 - ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts") - ttsChannel.setMethodCallHandler(TtsMethodHandler()) + // 设置TTS事件通道 + setupTtsEventChannels(flutterPluginBinding) - // 初始化ASR事件通道 - asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr_events") - asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - asrEventSink = events - } - - override fun onCancel(arguments: Any?) { - asrEventSink = null + // 设置ASR事件通道 + setupAsrEventChannel(flutterPluginBinding) + + Log.d(TAG, "字节跳动语音插件已附加到Flutter引擎") + } + + /** + * 设置TTS相关的事件通道 + */ + private fun setupTtsEventChannels(binding: FlutterPlugin.FlutterPluginBinding) { + // TTS事件通道 + EventChannel(binding.binaryMessenger, TTS_EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + ttsEventSink = events + Log.d(TAG, "TTS事件流已建立连接") + } + + override fun onCancel(arguments: Any?) { + ttsEventSink = null + Log.d(TAG, "TTS事件流已取消") + } } - }) + ) - // 初始化TTS事件通道 - ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts_events") - ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - ttsEventSink = events + // TTS音频数据通道 + EventChannel(binding.binaryMessenger, TTS_AUDIO_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + ttsAudioSink = events + Log.d(TAG, "TTS音频流已建立连接") + } + + override fun onCancel(arguments: Any?) { + ttsAudioSink = null + Log.d(TAG, "TTS音频流已取消") + } } - - override fun onCancel(arguments: Any?) { - ttsEventSink = null + ) + } + + /** + * 设置ASR相关的事件通道 + */ + private fun setupAsrEventChannel(binding: FlutterPlugin.FlutterPluginBinding) { + // ASR事件通道 + EventChannel(binding.binaryMessenger, ASR_EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + asrEventSink = events + Log.d(TAG, "ASR事件流已建立连接") + } + + override fun onCancel(arguments: Any?) { + asrEventSink = null + Log.d(TAG, "ASR事件流已取消") + } } - }) + ) } - // ASR方法处理器 - inner class AsrMethodHandler : MethodCallHandler { + /** + * TTS方法处理器 + */ + inner class TtsMethodHandler : MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { "initialize" -> { - val appId = call.argument("appId") ?: "" - val accessToken = call.argument("accessToken") ?: "" - val streamMode = call.argument("streamMode") ?: true - val enablePunctuation = call.argument("enablePunctuation") ?: true - val sampleRate = call.argument("sampleRate") ?: 16000 - val bits = call.argument("bits") ?: 16 - val channels = call.argument("channels") ?: 1 + val ttsAppId = call.argument("ttsAppId") ?: "" + val ttsAppToken = call.argument("ttsAppToken") ?: "" + val language = call.argument("language") ?: "zh-CN" - try { - asrInstance = BytedanceASR( - context = context, - appId = appId, - accessToken = accessToken, - streamMode = streamMode, - enablePunctuation = enablePunctuation, - sampleRate = sampleRate, - bits = bits, - channels = channels, - resultCallback = { text, isFinal -> - val eventType = if (isFinal) "result" else "recognizing" - sendAsrEvent(mapOf( - "type" to eventType, - "text" to text, - "isFinal" to isFinal - )) - }, - errorCallback = { errorCode, errorMessage -> - sendAsrEvent(mapOf( - "type" to "error", - "errorCode" to errorCode, - "errorMessage" to errorMessage - )) - } - ) - result.success(true) - } catch (e: Exception) { - result.error("INITIALIZATION_ERROR", e.message, null) - } + initializeTTS(ttsAppId, ttsAppToken, language, result) } - "startRecognition" -> { - if (asrEventSink == null) { - result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始识别", null) - return - } - - scope.launch { - try { - asrInstance?.start() - sendAsrEvent(mapOf("type" to "sessionStarted")) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("START_ERROR", e.message, null) - } - } - } + "setVoice" -> { + val voice = call.argument("voice") ?: "" + setVoice(voice, result) } - "feedAudioData" -> { - val audioData = call.argument("audioData") - if (audioData != null) { - scope.launch { - try { - asrInstance?.feedAudioData(audioData) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("FEED_ERROR", e.message, null) - } - } - } - } else { - result.error("INVALID_ARGUMENT", "音频数据不能为空", null) - } + "speakOnce" -> { + val text = call.argument("text") ?: "" + speakOnce(text, result) } - "stopRecognition" -> { - scope.launch { - try { - asrInstance?.stop() - sendAsrEvent(mapOf("type" to "sessionStopped")) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("STOP_ERROR", e.message, null) - } - } - } + "speakStream" -> { + val text = call.argument("text") ?: "" + speakStream(text, result) } - "dispose" -> { - try { - asrInstance?.release() - asrInstance = null - result.success(true) - } catch (e: Exception) { - result.error("DISPOSE_ERROR", e.message, null) - } + "flushStream" -> { + flushStream(result) + } + "stopTTS" -> { + stopTTS(result) + } + "disposeTTS" -> { + disposeTTS(result) } else -> { result.notImplemented() @@ -221,93 +175,23 @@ class BytedanceSpeechPlugin : FlutterPlugin { } } - // TTS方法处理器 - inner class TtsMethodHandler : MethodCallHandler { + /** + * ASR方法处理器 + */ + inner class AsrMethodHandler : MethodCallHandler { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { - "initialize" -> { - val appId = call.argument("appId") ?: "" - val accessToken = call.argument("accessToken") ?: "" - val speaker = call.argument("speaker") ?: "" - val format = call.argument("format") ?: "mp3" - val sampleRate = call.argument("sampleRate") ?: 24000 - val enableTimestamp = call.argument("enableTimestamp") ?: true - - try { - ttsInstance = BytedanceTTS( - context = context, - appId = appId, - accessToken = accessToken, - speaker = speaker, - format = format, - sampleRate = sampleRate, - enableTimestamp = enableTimestamp, - audioCallback = { audioData -> - sendTtsEvent(mapOf( - "type" to "audio", - "audioData" to audioData - )) - }, - completionCallback = { - sendTtsEvent(mapOf("type" to "completed")) - }, - errorCallback = { errorCode, errorMessage -> - sendTtsEvent(mapOf( - "type" to "error", - "errorCode" to errorCode, - "errorMessage" to errorMessage - )) - } - ) - result.success(true) - } catch (e: Exception) { - result.error("INITIALIZATION_ERROR", e.message, null) - } + "initializeASR" -> { + result.notImplemented() } - "synthesize" -> { - val text = call.argument("text") ?: "" - - if (ttsEventSink == null) { - result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始合成", null) - return - } - - scope.launch { - try { - sendTtsEvent(mapOf("type" to "started")) - ttsInstance?.synthesize(text) - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("SYNTHESIS_ERROR", e.message, null) - } - } - } + "startListening" -> { + result.notImplemented() } - "stopSynthesis" -> { - scope.launch { - try { - ttsInstance?.stop() - scope.launch(Dispatchers.Main) { - result.success(true) - } - } catch (e: Exception) { - scope.launch(Dispatchers.Main) { - result.error("STOP_ERROR", e.message, null) - } - } - } + "stopListening" -> { + result.notImplemented() } - "dispose" -> { - try { - ttsInstance?.release() - ttsInstance = null - result.success(true) - } catch (e: Exception) { - result.error("DISPOSE_ERROR", e.message, null) - } + "cancelASR" -> { + result.notImplemented() } else -> { result.notImplemented() @@ -317,15 +201,269 @@ class BytedanceSpeechPlugin : FlutterPlugin { } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - asrChannel.setMethodCallHandler(null) ttsChannel.setMethodCallHandler(null) - asrEventChannel.setStreamHandler(null) - ttsEventChannel.setStreamHandler(null) + asrChannel.setMethodCallHandler(null) - scope.cancel() - asrInstance?.release() - ttsInstance?.release() - asrInstance = null - ttsInstance = null + // 释放资源 + bytedanceTTS?.release() + bytedanceTTS = null + + bytedanceASR?.release() + bytedanceASR = null + + job.cancel() + + Log.d(TAG, "字节跳动语音插件已从Flutter引擎分离") + } + + /** + * 初始化TTS服务 + */ + private fun initializeTTS(ttsAppId: String, ttsAppToken: String, language: String, result: Result) { + scope.launch { + try { + if (ttsAppId.isEmpty() || ttsAppToken.isEmpty()) { + result.error( + "INVALID_PARAMS", + "AppID和AppToken不能为空", + null + ) + return@launch + } + + // 确保只有一个TTS实例 + bytedanceTTS?.release() + + // 创建新的TTS实例 + bytedanceTTS = BytedanceTTS( + context = context, + // 音频数据回调 + audioCallback = { audioData -> + sendTTSAudioData(audioData) + }, + // 完成回调 + completionCallback = { + sendTTSEvent("completed", null) + }, + // 错误回调 + errorCallback = { code, message -> + val params = HashMap() + params["code"] = code + params["message"] = message + sendTTSEvent("error", params) + }, + // 状态回调 + statusCallback = { status -> + val params = HashMap() + params["status"] = status + sendTTSEvent("status", params) + } + ) + + // 初始化TTS + val success = bytedanceTTS?.initialize(ttsAppId, ttsAppToken, language) ?: false + + if (success) { + result.success(true) + } else { + result.error( + "INIT_FAILED", + "TTS初始化失败", + null + ) + } + } catch (e: Exception) { + Log.e(TAG, "TTS初始化异常: ${e.message}", e) + result.error( + "INIT_EXCEPTION", + "TTS初始化异常: ${e.message}", + null + ) + } + } + } + + /** + * 设置TTS音色 + */ + private fun setVoice(voice: String, result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } + + try { + val success = bytedanceTTS?.setVoice(voice) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "设置音色异常: ${e.message}", e) + result.error( + "VOICE_EXCEPTION", + "设置音色异常: ${e.message}", + null + ) + } + } + + /** + * 合成并播放文本(单次) + */ + private fun speakOnce(text: String, result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } + + scope.launch { + try { + val success = bytedanceTTS?.speakOnce(text) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "合成文本异常: ${e.message}", e) + result.error( + "SPEAK_EXCEPTION", + "合成文本异常: ${e.message}", + null + ) + } + } + } + + /** + * 流式合成文本 + */ + private fun speakStream(text: String, result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } + + scope.launch { + try { + val success = bytedanceTTS?.speakStream(text) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "流式合成异常: ${e.message}", e) + result.error( + "STREAM_EXCEPTION", + "流式合成异常: ${e.message}", + null + ) + } + } + } + + /** + * 刷新流式合成缓冲区 + */ + private fun flushStream(result: Result) { + if (bytedanceTTS == null) { + result.error("NOT_INITIALIZED", "TTS未初始化", null) + return + } + + scope.launch { + try { + val success = bytedanceTTS?.flushStream() ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "刷新流异常: ${e.message}", e) + result.error( + "FLUSH_EXCEPTION", + "刷新流异常: ${e.message}", + null + ) + } + } + } + + /** + * 停止TTS + */ + private fun stopTTS(result: Result) { + if (bytedanceTTS == null) { + result.success(true) + return + } + + scope.launch { + try { + bytedanceTTS?.stop() + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "停止TTS异常: ${e.message}", e) + result.error( + "STOP_EXCEPTION", + "停止TTS异常: ${e.message}", + null + ) + } + } + } + + /** + * 释放TTS资源 + */ + private fun disposeTTS(result: Result) { + try { + bytedanceTTS?.release() + bytedanceTTS = null + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "释放TTS资源异常: ${e.message}", e) + result.error( + "DISPOSE_EXCEPTION", + "释放TTS资源异常: ${e.message}", + null + ) + } + } + + /** + * 发送TTS事件到Flutter端 + */ + private fun sendTTSEvent(eventName: String, params: Map?) { + if (ttsEventSink == null) return + + val event = HashMap() + event["event"] = eventName + + if (params != null) { + event["params"] = params + } + + mainHandler.post { + ttsEventSink?.success(event) + } + } + + /** + * 发送TTS音频数据到Flutter端 + */ + private fun sendTTSAudioData(data: ByteArray) { + if (ttsAudioSink == null) return + + mainHandler.post { + ttsAudioSink?.success(data) + } + } + + /** + * 发送ASR事件到Flutter端 + */ + private fun sendASREvent(eventType: String, data: Any, params: Map? = null) { + if (asrEventSink == null) return + + val event = HashMap() + event["type"] = eventType + event["data"] = data + + if (params != null) { + event["params"] = params + } + + mainHandler.post { + asrEventSink?.success(event) + } } } \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt index d78be4d24..6e508bcab 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt @@ -1,8 +1,7 @@ package com.deep_voice.bytedance_speech import android.content.Context -import com.google.gson.Gson -import com.google.gson.JsonObject +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -10,9 +9,8 @@ import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener -import okhttp3.logging.HttpLoggingInterceptor -import okhttp3.logging.HttpLoggingInterceptor.Level import okio.ByteString +import org.json.JSONObject import java.io.ByteArrayOutputStream import java.util.UUID import java.util.concurrent.TimeUnit @@ -32,6 +30,8 @@ class BytedanceTTS( private val statusCallback: (String) -> Unit ) { companion object { + private const val TAG = "BytedanceTTS" + // 协议常量 private const val PROTOCOL_VERSION = 0x01 private const val DEFAULT_HEADER_SIZE = 0x01 @@ -94,9 +94,9 @@ class BytedanceTTS( // TTS配置参数 private var appId: String = "" private var accessToken: String = "" - private var speaker: String = "zh_female_qingxin" // 默认语音 - private var format: String = "pcm" - private var sampleRate: Int = 16000 + private var speaker: String = "zh_female_shuangkuaisisi_moon_bigtts" // 默认语音,使用Java示例的音色 + private var format: String = "mp3" // 使用mp3格式 + private var sampleRate: Int = 24000 // 使用24kHz采样率 private var enableTimestamp: Boolean = false private var language: String = "zh-CN" @@ -106,48 +106,53 @@ class BytedanceTTS( init { // 配置OkHttp客户端 - val loggingInterceptor = HttpLoggingInterceptor().apply { - level = Level.HEADERS - } - client = OkHttpClient.Builder() - .pingInterval(50, TimeUnit.SECONDS) - .addInterceptor(loggingInterceptor) - .readTimeout(100, TimeUnit.SECONDS) - .writeTimeout(100, TimeUnit.SECONDS) + .pingInterval(50, TimeUnit.SECONDS) // 匹配Java示例设置50秒 + .readTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒 + .writeTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒 .build() // 生成会话ID sessionId = UUID.randomUUID().toString().replace("-", "") + Log.d(TAG, "BytedanceTTS实例化完成,会话ID: $sessionId") } /** * 初始化火山语音服务 */ - suspend fun initialize(subscriptionKey: String, region: String, language: String = "zh-CN"): Boolean = withContext(Dispatchers.IO) { + suspend fun initialize(ttsAppId: String, ttsAppToken: String, language: String = "zh-CN"): Boolean = withContext(Dispatchers.IO) { try { updateStatus(STATUS_INITIALIZING) // 设置必要参数 - this@BytedanceTTS.appId = region // 火山引擎中region对应应用ID - this@BytedanceTTS.accessToken = subscriptionKey // 订阅密钥作为访问令牌 + this@BytedanceTTS.appId = ttsAppId // 火山引擎中的应用ID + this@BytedanceTTS.accessToken = ttsAppToken // 火山引擎访问令牌 this@BytedanceTTS.language = language - // 根据语言设置默认speaker - if (language.startsWith("zh")) { - this@BytedanceTTS.speaker = "zh_female_qingxin" - } else if (language.startsWith("en")) { - this@BytedanceTTS.speaker = "en_female_crystal" - } - // 释放旧连接 release() // 建立新连接 connect() + // 等待连接和会话建立 + var attempts = 0 + while (!isSessionStarted && attempts < 10) { + delay(500) // 增加等待时间 + attempts++ + Log.d(TAG, "等待会话建立...尝试 $attempts/10") + } + + if (!isSessionStarted) { + Log.e(TAG, "初始化超时: 无法建立会话") + errorCallback("INIT_TIMEOUT", "初始化超时: 无法在5秒内建立会话") + return@withContext false + } + + Log.d(TAG, "字节跳动TTS初始化成功,会话ID: $sessionId") true } catch (e: Exception) { + Log.e(TAG, "初始化失败: ${e.message}", e) errorCallback("INIT_ERROR", e.message ?: "初始化失败") updateStatus(STATUS_ERROR) false @@ -199,6 +204,7 @@ class BytedanceTTS( sendTextRequest(text) return@withContext true } catch (e: Exception) { + Log.e(TAG, "语音合成失败: ${e.message}", e) errorCallback("SYNTH_ERROR", e.message ?: "语音合成失败") updateStatus(STATUS_ERROR) false @@ -211,10 +217,9 @@ class BytedanceTTS( suspend fun speakStream(text: String): Boolean = withContext(Dispatchers.IO) { try { if (text.isEmpty()) { - errorCallback("PARAM_ERROR", "合成文本不能为空") - return@withContext false + return@withContext true // 空文本直接返回成功 } - + Log.d(TAG, "开始流式合成,text: $text") isStreamMode = true // 确保会话已就绪 @@ -225,6 +230,7 @@ class BytedanceTTS( updateStatus(STATUS_SPEAKING) return@withContext true } catch (e: Exception) { + Log.e(TAG, "流式合成失败: ${e.message}", e) errorCallback("STREAM_ERROR", e.message ?: "流式合成失败") updateStatus(STATUS_ERROR) false @@ -248,6 +254,7 @@ class BytedanceTTS( updateStatus(STATUS_STOPPED) return@withContext true } catch (e: Exception) { + Log.e(TAG, "结束流式合成失败: ${e.message}", e) errorCallback("FLUSH_ERROR", e.message ?: "结束流式合成失败") false } @@ -327,10 +334,13 @@ class BytedanceTTS( .header("X-Api-Connect-Id", UUID.randomUUID().toString()) .build() + Log.d(TAG, "正在连接WebSocket: $url, appId: $appId") + Log.d(TAG, "当前语音角色配置: speaker=$speaker, format=$format, sampleRate=$sampleRate") + webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { val logId = response.header("X-Tt-Logid") - println("TTS WebSocket连接成功, X-Tt-Logid: $logId") + Log.d(TAG, "TTS WebSocket连接成功, X-Tt-Logid: $logId") // 发送开始连接事件 startConnection(webSocket) @@ -339,6 +349,7 @@ class BytedanceTTS( override fun onMessage(webSocket: WebSocket, bytes: ByteString) { try { val data = bytes.toByteArray() + Log.d(TAG, "收到二进制消息,大小: ${data.size} 字节") val response = parseResponse(data) // 处理事件 @@ -349,6 +360,7 @@ class BytedanceTTS( } else { "连接或会话失败" } + Log.e(TAG, "连接或会话失败: $errorMsg, 事件类型: ${response.event}") errorCallback("TTS_ERROR", errorMsg) webSocket.close(1000, "Error") updateStatus(STATUS_ERROR) @@ -356,25 +368,51 @@ class BytedanceTTS( EVENT_CONNECTION_STARTED -> { isConnected = true + Log.d(TAG, "连接已建立,开始TTS会话") // 开始TTS会话 startTtsSession(webSocket) } EVENT_SESSION_STARTED -> { isSessionStarted = true + Log.d(TAG, "TTS会话已开始,会话ID: $sessionId") updateStatus(STATUS_READY) } EVENT_TTS_RESPONSE -> { // 音频数据 if (response.messageType == AUDIO_ONLY_RESPONSE && response.payload.isNotEmpty()) { - audioCallback(response.payload) + Log.d(TAG, "收到音频数据,大小: ${response.payload.size} 字节") + try { + // 使用同步方式调用回调,防止协程上下文切换导致的问题 + if (response.payload.size > 0) { + Log.d(TAG, "准备调用音频回调,数据大小: ${response.payload.size}") + audioCallback(response.payload) + Log.d(TAG, "音频回调调用成功") + } else { + Log.w(TAG, "收到空音频数据") + } + } catch (e: Exception) { + Log.e(TAG, "调用音频回调函数失败: ${e.message}", e) + errorCallback("AUDIO_CALLBACK_ERROR", "音频处理失败: ${e.message}") + } + } else { + Log.d(TAG, "收到TTS响应,但没有音频数据 messageType=${response.messageType}, 有效载荷大小=${response.payload.size}") } } + EVENT_TTS_SENTENCE_START -> { + Log.d(TAG, "句子合成开始") + } + + EVENT_TTS_SENTENCE_END -> { + Log.d(TAG, "句子合成结束") + } + EVENT_CONNECTION_FINISHED -> { isConnected = false isSessionStarted = false + Log.d(TAG, "连接已结束") updateStatus(STATUS_STOPPED) completionCallback() webSocket.close(1000, "Connection finished") @@ -382,6 +420,7 @@ class BytedanceTTS( EVENT_SESSION_FINISHED -> { isSessionStarted = false + Log.d(TAG, "会话已结束,流模式: $isStreamMode") if (!isStreamMode) { finishConnection(webSocket) } else { @@ -389,8 +428,13 @@ class BytedanceTTS( updateStatus(STATUS_READY) } } + + else -> { + Log.d(TAG, "收到未处理的事件类型: ${response.event}") + } } } catch (e: Exception) { + Log.e(TAG, "解析响应失败: ${e.message}", e) errorCallback("PARSE_ERROR", e.message ?: "解析响应失败") updateStatus(STATUS_ERROR) } @@ -399,6 +443,7 @@ class BytedanceTTS( override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { isConnected = false isSessionStarted = false + Log.e(TAG, "连接失败: ${t.message}", t) errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") updateStatus(STATUS_ERROR) } @@ -407,7 +452,7 @@ class BytedanceTTS( isConnected = false isSessionStarted = false updateStatus(STATUS_STOPPED) - println("TTS WebSocket连接关闭: $code, $reason") + Log.d(TAG, "TTS WebSocket连接关闭: $code, $reason") } }) } @@ -477,30 +522,26 @@ class BytedanceTTS( sessionId = sessionId ) - val user = JsonObject().apply { - addProperty("uid", "123456") - } + val jsonObject = JSONObject() + val user = JSONObject() + user.put("uid", "123456") + jsonObject.put("user", user) + jsonObject.put("event", EVENT_START_SESSION) + jsonObject.put("namespace", "BidirectionalTTS") - val reqParams = JsonObject().apply { - addProperty("speaker", speaker) - - val audioParams = JsonObject().apply { - addProperty("format", format) - addProperty("sample_rate", sampleRate) - addProperty("enable_timestamp", enableTimestamp) - } - - add("audio_params", audioParams) - } + val reqParams = JSONObject() + reqParams.put("speaker", speaker) - val payloadObj = JsonObject().apply { - add("user", user) - addProperty("event", EVENT_START_SESSION) - addProperty("namespace", "BidirectionalTTS") - add("req_params", reqParams) - } + val audioParams = JSONObject() + audioParams.put("format", "mp3") // 尝试改用mp3格式 + audioParams.put("sample_rate", 24000) // 提高采样率 + audioParams.put("enable_timestamp", enableTimestamp) + + reqParams.put("audio_params", audioParams) + jsonObject.put("req_params", reqParams) - val payload = payloadObj.toString().toByteArray() + val payload = jsonObject.toString().toByteArray() + Log.d(TAG, "开始TTS会话请求: ${jsonObject.toString()}") sendEvent(webSocket, header, optional, payload) } @@ -525,36 +566,31 @@ class BytedanceTTS( sessionId = sessionId ) - val user = JsonObject().apply { - addProperty("uid", "123456") - } + val jsonObject = JSONObject() + val user = JSONObject() + user.put("uid", "123456") + jsonObject.put("user", user) + jsonObject.put("event", EVENT_TASK_REQUEST) + jsonObject.put("namespace", "BidirectionalTTS") - val reqParams = JsonObject().apply { - addProperty("text", text) - addProperty("speaker", speaker) - - val audioParams = JsonObject().apply { - addProperty("format", format) - addProperty("sample_rate", sampleRate) - } - - add("audio_params", audioParams) - } + val reqParams = JSONObject() + reqParams.put("text", text) + reqParams.put("speaker", speaker) - val payloadObj = JsonObject().apply { - add("user", user) - addProperty("event", EVENT_TASK_REQUEST) - addProperty("namespace", "BidirectionalTTS") - add("req_params", reqParams) - } + val audioParams = JSONObject() + audioParams.put("format", "mp3") // 尝试改用mp3格式 + audioParams.put("sample_rate", 24000) // 提高采样率 - val payload = payloadObj.toString().toByteArray() + reqParams.put("audio_params", audioParams) + jsonObject.put("req_params", reqParams) + + val payload = jsonObject.toString().toByteArray() + Log.d(TAG, "发送文本合成请求: ${jsonObject.toString()}") sendEvent(ws, header, optional, payload) // 发送完文本后结束会话 updateStatus(STATUS_SPEAKING) - finishSession() } } @@ -578,30 +614,26 @@ class BytedanceTTS( sessionId = sessionId ) - val user = JsonObject().apply { - addProperty("uid", "123456") - } + val jsonObject = JSONObject() + val user = JSONObject() + user.put("uid", "123456") + jsonObject.put("user", user) + jsonObject.put("event", EVENT_TASK_REQUEST) + jsonObject.put("namespace", "BidirectionalTTS") - val reqParams = JsonObject().apply { - addProperty("text", text) - addProperty("speaker", speaker) - - val audioParams = JsonObject().apply { - addProperty("format", format) - addProperty("sample_rate", sampleRate) - } - - add("audio_params", audioParams) - } + val reqParams = JSONObject() + reqParams.put("text", text) + reqParams.put("speaker", speaker) - val payloadObj = JsonObject().apply { - add("user", user) - addProperty("event", EVENT_TASK_REQUEST) - addProperty("namespace", "BidirectionalTTS") - add("req_params", reqParams) - } + val audioParams = JSONObject() + audioParams.put("format", "mp3") // 尝试改用mp3格式 + audioParams.put("sample_rate", 24000) // 提高采样率 - val payload = payloadObj.toString().toByteArray() + reqParams.put("audio_params", audioParams) + jsonObject.put("req_params", reqParams) + + val payload = jsonObject.toString().toByteArray() + Log.d(TAG, "发送流式文本合成请求: ${jsonObject.toString()}") sendEvent(ws, header, optional, payload) // 流式模式下不自动结束会话 @@ -638,27 +670,32 @@ class BytedanceTTS( * 发送事件 */ private fun sendEvent(webSocket: WebSocket, header: Header, optional: Optional, payload: ByteArray) { - val headerBytes = header.getBytes() - val optionalBytes = optional.getBytes() - val payloadSizeBytes = intToBytes(payload.size) - - val requestBytes = ByteArray( - headerBytes.size + optionalBytes.size + payloadSizeBytes.size + payload.size - ) - var offset = 0 - - System.arraycopy(headerBytes, 0, requestBytes, offset, headerBytes.size) - offset += headerBytes.size - - System.arraycopy(optionalBytes, 0, requestBytes, offset, optionalBytes.size) - offset += optionalBytes.size - - System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) - offset += payloadSizeBytes.size - - System.arraycopy(payload, 0, requestBytes, offset, payload.size) - - webSocket.send(ByteString.of(requestBytes)) + try { + val headerBytes = header.getBytes() + val optionalBytes = optional.getBytes() + val payloadSizeBytes = intToBytes(payload.size) + + val requestBytes = ByteArray( + headerBytes.size + optionalBytes.size + payloadSizeBytes.size + payload.size + ) + var offset = 0 + + System.arraycopy(headerBytes, 0, requestBytes, offset, headerBytes.size) + offset += headerBytes.size + + System.arraycopy(optionalBytes, 0, requestBytes, offset, optionalBytes.size) + offset += optionalBytes.size + + System.arraycopy(payloadSizeBytes, 0, requestBytes, offset, payloadSizeBytes.size) + offset += payloadSizeBytes.size + + System.arraycopy(payload, 0, requestBytes, offset, payload.size) + + val success = webSocket.send(ByteString.of(*requestBytes)) + Log.d(TAG, "发送WebSocket消息: 消息类型=${header.messageType}, 事件=${optional.event}, 大小=${requestBytes.size}字节, 成功=$success") + } catch (e: Exception) { + Log.e(TAG, "发送WebSocket消息失败: ${e.message}", e) + } } /** diff --git a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt b/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt index c6b632deb..16c4a8279 100644 --- a/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt +++ b/local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt @@ -15,7 +15,6 @@ import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result -import com.yunqiinnovation.deepvoice_utils.FileLogger /** ClassicBluetoothPlugin */ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { @@ -119,7 +118,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { sendEvent(eventMap) } } catch (e: Exception) { - FileLogger.e(TAG, "蓝牙状态获取失败", e) + Log.e(TAG, "蓝牙状态获取失败", e) } } @@ -188,7 +187,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { eventSink?.success(event) } catch (e: Exception) { - FileLogger.e(TAG, "事件发送失败", e) + Log.e(TAG, "事件发送失败", e) } } } \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/android/settings.gradle.kts b/local_plugins/deepvoice_utils/android/settings.gradle.kts deleted file mode 100644 index f1b0c086e..000000000 --- a/local_plugins/deepvoice_utils/android/settings.gradle.kts +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = "deepvoice_utils" \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml b/local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml deleted file mode 100644 index c043918b0..000000000 --- a/local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt b/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt deleted file mode 100644 index a330e85b8..000000000 --- a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.yunqiinnovation.deepvoice_utils - -import androidx.annotation.NonNull -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result - -/** DeepvoiceUtilsPlugin */ -class DeepvoiceUtilsPlugin: FlutterPlugin, MethodCallHandler { - private lateinit var channel : MethodChannel - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "deepvoice_utils") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "getPlatformVersion" -> { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } - else -> { - result.notImplemented() - } - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt b/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt deleted file mode 100644 index b31331689..000000000 --- a/local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.yunqiinnovation.deepvoice_utils - -import android.content.Context -import android.util.Log -import java.io.File -import java.io.FileWriter -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.* -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors - -/** - * 文件日志工具 - * 将日志同时输出到Logcat和文件系统 - */ -class FileLogger private constructor(context: Context) { - private val logDir: File - private var currentLogFile: File? = null - private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()) - private val logExecutor: ExecutorService = Executors.newSingleThreadExecutor() - - init { - // 初始化日志目录 - logDir = File(context.getExternalFilesDir(null), "logs") - if (!logDir.exists()) { - logDir.mkdirs() - } - - // 创建当天的日志文件 - updateLogFile() - } - - /** - * 更新日志文件为当天日期 - */ - private fun updateLogFile() { - val today = dateFormat.format(Date()) - val logFile = File(logDir, "log_$today.txt") - - if (currentLogFile == null || currentLogFile?.absolutePath != logFile.absolutePath) { - currentLogFile = logFile - - // 如果是新文件,添加文件头 - if (!logFile.exists()) { - try { - logFile.createNewFile() - FileWriter(logFile, true).use { writer -> - writer.append("===== 日志开始: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} =====\n") - } - } catch (e: IOException) { - Log.e(TAG, "创建日志文件失败: ${e.message}") - } - } - } - } - - /** - * 调试级别日志 - */ - fun d(tag: String, message: String) { - // 输出到Logcat - Log.d(tag, message) - - // 异步写入文件 - logToFile("D", tag, message) - } - - /** - * 信息级别日志 - */ - fun i(tag: String, message: String) { - // 输出到Logcat - Log.i(tag, message) - - // 异步写入文件 - logToFile("I", tag, message) - } - - /** - * 警告级别日志 - */ - fun w(tag: String, message: String) { - // 输出到Logcat - Log.w(tag, message) - - // 异步写入文件 - logToFile("W", tag, message) - } - - /** - * 错误级别日志 - */ - fun e(tag: String, message: String, throwable: Throwable? = null) { - // 输出到Logcat - if (throwable != null) { - Log.e(tag, message, throwable) - } else { - Log.e(tag, message) - } - - // 异步写入文件 - val errorMsg = if (throwable != null) { - "$message\n${throwable.message}\n${throwable.stackTraceToString()}" - } else { - message - } - - logToFile("E", tag, errorMsg) - } - - /** - * 将日志写入文件 - */ - private fun logToFile(level: String, tag: String, message: String) { - - logExecutor.execute { - try { - // 检查是否需要更新日志文件 - updateLogFile() - - // 格式化日志内容 - val timestamp = timeFormat.format(Date()) - val logLine = "$timestamp $level/$tag: $message\n" - - // 写入文件 - if (currentLogFile != null && currentLogFile!!.exists()) { - FileWriter(currentLogFile!!, true).use { writer -> - writer.append(logLine) - } - } - } catch (e: IOException) { - Log.e(TAG, "写入日志文件失败: ${e.message}") - } - } - } - - /** - * 清理旧日志文件 - * @param daysToKeep 保留的天数 - */ - fun cleanOldLogs(daysToKeep: Int = 7) { - logExecutor.execute { - try { - val currentTime = System.currentTimeMillis() - val maxAge = daysToKeep * 24 * 60 * 60 * 1000L - - logDir.listFiles()?.forEach { file -> - if (file.isFile && file.name.startsWith("log_") && file.name.endsWith(".txt")) { - val fileAge = currentTime - file.lastModified() - if (fileAge > maxAge) { - val deleted = file.delete() - if (deleted) { - Log.d(TAG, "已删除旧日志文件: ${file.name}") - } - } - } - } - } catch (e: Exception) { - Log.e(TAG, "清理旧日志失败: ${e.message}") - } - } - } - - /** - * 关闭日志系统 - */ - fun shutdown() { - logExecutor.shutdown() - } - - companion object { - private const val TAG = "FileLogger" - private var INSTANCE: FileLogger? = null - - /** - * 初始化日志系统 - */ - @Synchronized - fun init(context: Context) { - if (INSTANCE == null) { - INSTANCE = FileLogger(context.applicationContext) - d(TAG, "文件日志系统已初始化") - } - } - - /** - * 调试日志 - */ - fun d(tag: String, message: String) { - INSTANCE?.d(tag, message) ?: Log.d(tag, message) - } - - /** - * 信息日志 - */ - fun i(tag: String, message: String) { - INSTANCE?.i(tag, message) ?: Log.i(tag, message) - } - - /** - * 警告日志 - */ - fun w(tag: String, message: String) { - INSTANCE?.w(tag, message) ?: Log.w(tag, message) - } - - /** - * 错误日志 - */ - fun e(tag: String, message: String, throwable: Throwable? = null) { - INSTANCE?.e(tag, message, throwable) ?: run { - if (throwable != null) { - Log.e(tag, message, throwable) - } else { - Log.e(tag, message) - } - } - } - - /** - * 清理旧日志 - */ - fun cleanOldLogs(daysToKeep: Int = 7) { - INSTANCE?.cleanOldLogs(daysToKeep) - } - - /** - * 关闭日志系统 - */ - fun shutdown() { - INSTANCE?.shutdown() - INSTANCE = null - } - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h b/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h deleted file mode 100644 index f64c3569a..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface DeepvoiceUtilsPlugin : NSObject -@end \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m b/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m deleted file mode 100644 index af5d121b1..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m +++ /dev/null @@ -1,12 +0,0 @@ -#import "DeepvoiceUtilsPlugin.h" -#if __has_include() -#import -#else -#import "deepvoice_utils-Swift.h" -#endif - -@implementation DeepvoiceUtilsPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftDeepvoiceUtilsPlugin registerWithRegistrar:registrar]; -} -@end \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift b/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift deleted file mode 100644 index 922bb3c4b..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Flutter -import UIKit - -public class DeepvoiceUtilsPlugin: NSObject, FlutterPlugin { - private static let TAG = "DeepvoiceUtilsPlugin" - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "deepvoice_utils", binaryMessenger: registrar.messenger()) - let instance = DeepvoiceUtilsPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - - // 初始化文件日志 - FileLogger.initialize() - FileLogger.d(TAG, "DeepvoiceUtilsPlugin 已初始化") - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getPlatformVersion": - result("iOS \(UIDevice.current.systemVersion)") - default: - result(FlutterMethodNotImplemented) - } - } - - public func detachFromEngine(for registrar: FlutterPluginRegistrar) { - // 关闭日志系统 - FileLogger.shutdown() - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift b/local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift deleted file mode 100644 index f54e040f8..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift +++ /dev/null @@ -1,286 +0,0 @@ -import Foundation -import os.log - -/** - * 文件日志工具 - * 将日志同时输出到控制台和文件系统 - */ -@objc public class FileLogger: NSObject { - - private static let TAG = "FileLogger" - private static var instance: FileLogger? - - private let logDir: URL - private var currentLogFile: URL? - private let logQueue = DispatchQueue(label: "com.yunqiinnovation.deepvoice_utils.logger", qos: .utility) - - private let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - return formatter - }() - - private let timeFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss.SSS" - return formatter - }() - - /** - * 初始化日志系统 - */ - private init(directory: URL) { - self.logDir = directory.appendingPathComponent("logs", isDirectory: true) - super.init() - - // 创建日志目录 - try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true) - - // 创建当天日志文件 - updateLogFile() - - // 记录初始化完成 - self.d(FileLogger.TAG, "文件日志系统已初始化") - } - - /** - * 更新日志文件为当天日期 - */ - private func updateLogFile() { - let today = dateFormatter.string(from: Date()) - let logFile = logDir.appendingPathComponent("log_\(today).txt") - - if currentLogFile == nil || currentLogFile?.path != logFile.path { - currentLogFile = logFile - - // 如果是新文件,添加文件头 - if !FileManager.default.fileExists(atPath: logFile.path) { - let headerDate = DateFormatter() - headerDate.dateFormat = "yyyy-MM-dd HH:mm:ss" - let headerLine = "===== 日志开始: \(headerDate.string(from: Date())) =====\n" - - try? headerLine.write(to: logFile, atomically: true, encoding: .utf8) - } - } - } - - /** - * 将日志写入文件 - */ - private func logToFile(level: String, tag: String, message: String) { - logQueue.async { [weak self] in - guard let self = self else { return } - - do { - // 检查是否需要更新日志文件 - self.updateLogFile() - - guard let logFile = self.currentLogFile, - FileManager.default.fileExists(atPath: logFile.path) else { - return - } - - // 格式化日志内容 - let timestamp = self.timeFormatter.string(from: Date()) - let logLine = "\(timestamp) \(level)/\(tag): \(message)\n" - - // 获取文件句柄并追加内容 - if let fileHandle = try? FileHandle(forWritingTo: logFile) { - fileHandle.seekToEndOfFile() - if let data = logLine.data(using: .utf8) { - fileHandle.write(data) - } - fileHandle.closeFile() - } else { - // 如果无法获取文件句柄,尝试直接写入 - let existingContent = try? String(contentsOf: logFile, encoding: .utf8) - let newContent = (existingContent ?? "") + logLine - try newContent.write(to: logFile, atomically: true, encoding: .utf8) - } - } catch { - print("写入日志文件失败: \(error.localizedDescription)") - } - } - } - - /** - * 调试级别日志 - */ - private func d(_ tag: String, _ message: String) { - // 输出到控制台 - print("D/\(tag): \(message)") - - // 写入文件 - logToFile(level: "D", tag: tag, message: message) - } - - /** - * 信息级别日志 - */ - private func i(_ tag: String, _ message: String) { - // 输出到控制台 - print("I/\(tag): \(message)") - - // 写入文件 - logToFile(level: "I", tag: tag, message: message) - } - - /** - * 警告级别日志 - */ - private func w(_ tag: String, _ message: String) { - // 输出到控制台 - print("W/\(tag): \(message)") - - // 写入文件 - logToFile(level: "W", tag: tag, message: message) - } - - /** - * 错误级别日志 - */ - private func e(_ tag: String, _ message: String, _ error: Error? = nil) { - var fullMessage = message - - // 如果有错误,添加错误信息 - if let error = error { - fullMessage += "\n\(error.localizedDescription)" - } - - // 输出到控制台 - print("E/\(tag): \(fullMessage)") - - // 写入文件 - logToFile(level: "E", tag: tag, message: fullMessage) - } - - /** - * 清理旧日志文件 - */ - private func cleanOldLogs(daysToKeep: Int = 7) { - logQueue.async { - do { - let currentDate = Date() - let fileManager = FileManager.default - - let logFiles = try fileManager.contentsOfDirectory(at: self.logDir, includingPropertiesForKeys: [.creationDateKey], options: []) - - for fileURL in logFiles where fileURL.pathExtension == "txt" { - if let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path), - let creationDate = fileAttributes[.creationDate] as? Date { - - let age = Calendar.current.dateComponents([.day], from: creationDate, to: currentDate) - - if let days = age.day, days > daysToKeep { - try? fileManager.removeItem(at: fileURL) - print("D/\(FileLogger.TAG): 已删除旧日志文件: \(fileURL.lastPathComponent)") - } - } - } - } catch { - print("E/\(FileLogger.TAG): 清理旧日志失败: \(error.localizedDescription)") - } - } - } - - // MARK: - 公共静态方法 - - /** - * 初始化日志系统 - */ - @objc public static func initialize() { - if instance == nil { - let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - instance = FileLogger(directory: documentsDirectory) - } - } - - /** - * 初始化日志系统(使用自定义目录) - */ - @objc public static func initialize(withDirectory directory: URL) { - if instance == nil { - instance = FileLogger(directory: directory) - } - } - - /** - * 调试日志 - */ - @objc public static func d(_ tag: String, _ message: String) { - // 如果实例不存在,先使用默认目录初始化 - if instance == nil { - initialize() - } - - instance?.d(tag, message) - - // 使用系统日志 - if #available(iOS 10.0, *) { - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .debug, message) - } - } - - /** - * 信息日志 - */ - @objc public static func i(_ tag: String, _ message: String) { - if instance == nil { - initialize() - } - - instance?.i(tag, message) - - // 使用系统日志 - if #available(iOS 10.0, *) { - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .info, message) - } - } - - /** - * 警告日志 - */ - @objc public static func w(_ tag: String, _ message: String) { - if instance == nil { - initialize() - } - - instance?.w(tag, message) - - // 使用系统日志 - if #available(iOS 10.0, *) { - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .default, message) - } - } - - /** - * 错误日志 - */ - @objc public static func e(_ tag: String, _ message: String, _ error: Error? = nil) { - if instance == nil { - initialize() - } - - instance?.e(tag, message, error) - - // 使用系统日志 - if #available(iOS 10.0, *) { - let errorMessage = error != nil ? "\(message): \(error!.localizedDescription)" : message - os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .error, errorMessage) - } - } - - /** - * 清理旧日志 - */ - @objc public static func cleanOldLogs(daysToKeep: Int = 7) { - instance?.cleanOldLogs(daysToKeep: daysToKeep) - } - - /** - * 关闭日志系统 - */ - @objc public static func shutdown() { - instance = nil - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift b/local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift deleted file mode 100644 index 408f07402..000000000 --- a/local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Flutter -import UIKit - -// 这个类是为了满足Flutter插件的命名约定 -// 实际实现在DeepvoiceUtilsPlugin.swift中 -public class SwiftDeepvoiceUtilsPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - // 调用真正的实现 - DeepvoiceUtilsPlugin.register(with: registrar) - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/README.md b/local_plugins/deepvoice_utils/ios/README.md deleted file mode 100644 index 7f12f83cb..000000000 --- a/local_plugins/deepvoice_utils/ios/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# DeepVoice Utils iOS 实现 - -## 简介 - -本模块是 `deepvoice_utils` 插件的 iOS 实现部分,主要提供以下功能: - -- 文件日志记录(FileLogger):将日志记录到文件,方便调试和问题排查 - -## 使用说明 - -### 日志系统 - -该插件自动初始化日志系统,开发者无需手动调用初始化方法。日志文件默认保存在应用的 Documents 目录下的 logs 文件夹中,按日期分文件存储。 - -## 配置要求 - -- iOS 11.0 或更高版本 -- Swift 5.0 -- 确保应用拥有文件写入权限 - -## 开发说明 - -### 文件结构 - -- `DeepvoiceUtilsPlugin.swift`:插件主类,负责与 Flutter 交互 -- `FileLogger.swift`:日志系统实现,提供日志记录功能 -- `SwiftDeepvoiceUtilsPlugin.swift`:命名约定桥接类 -- `DeepvoiceUtilsPlugin.h/.m`:Objective-C 桥接文件 - -### 自定义日志目录 - -如需使用自定义日志目录,可以在应用启动时手动调用初始化方法: - -```swift -import deepvoice_utils - -// 在 AppDelegate 中 -func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // 自定义日志目录 - let customDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("custom_logs") - FileLogger.initialize(withDirectory: customDirectory) - - return true -} -``` - -### 清理旧日志 - -默认情况下,日志系统不会自动清理旧日志。如果需要清理,可以手动调用: - -```swift -// 清理7天前的日志 -FileLogger.cleanOldLogs(daysToKeep: 7) -``` \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec b/local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec deleted file mode 100644 index 758fe120c..000000000 --- a/local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec +++ /dev/null @@ -1,19 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# -Pod::Spec.new do |s| - s.name = 'deepvoice_utils' - s.version = '0.0.1' - s.summary = '深语工具库' - s.description = <<-DESC -提供文件日志记录等通用功能的Flutter插件。 - DESC - s.homepage = 'http://yunqiinnovation.com' - s.license = { :file => '../LICENSE' } - s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - - s.ios.deployment_target = '11.0' - s.swift_version = '5.0' -end \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/lib/deepvoice_utils.dart b/local_plugins/deepvoice_utils/lib/deepvoice_utils.dart deleted file mode 100644 index e3387f790..000000000 --- a/local_plugins/deepvoice_utils/lib/deepvoice_utils.dart +++ /dev/null @@ -1,3 +0,0 @@ -library deepvoice_utils; - -export 'src/file_logger.dart'; \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/lib/src/file_logger.dart b/local_plugins/deepvoice_utils/lib/src/file_logger.dart deleted file mode 100644 index eae665185..000000000 --- a/local_plugins/deepvoice_utils/lib/src/file_logger.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:io'; -import 'package:flutter/foundation.dart'; - -/// 文件日志记录器 -/// -/// 提供文件日志记录功能,支持简单的日志记录 -class FileLogger { - /// 记录调试日志 - static void d(String tag, String message) { - debugPrint('[$tag] DEBUG: $message'); - } - - /// 记录信息日志 - static void i(String tag, String message) { - debugPrint('[$tag] INFO: $message'); - } - - /// 记录警告日志 - static void w(String tag, String message) { - debugPrint('[$tag] WARNING: $message'); - } - - /// 记录错误日志 - static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) { - final errorMsg = error != null ? '$message: $error' : message; - debugPrint('[$tag] ERROR: $errorMsg'); - if (stackTrace != null) { - debugPrint(stackTrace.toString()); - } - } -} \ No newline at end of file diff --git a/local_plugins/deepvoice_utils/pubspec.yaml b/local_plugins/deepvoice_utils/pubspec.yaml deleted file mode 100644 index 24796b435..000000000 --- a/local_plugins/deepvoice_utils/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: deepvoice_utils -description: 通用工具库,包含日志、文件处理等功能 -version: 0.0.1 - -environment: - sdk: ">=2.17.0 <4.0.0" - flutter: ">=2.5.0" - -dependencies: - flutter: - sdk: flutter - logger: ^2.0.0 - path_provider: ^2.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - package: com.yunqiinnovation.deepvoice_utils - pluginClass: DeepvoiceUtilsPlugin - ios: - pluginClass: DeepvoiceUtilsPlugin \ No newline at end of file diff --git a/local_plugins/music_service/android/build.gradle b/local_plugins/music_service/android/build.gradle index 3a98cc976..1f0ad0cf4 100644 --- a/local_plugins/music_service/android/build.gradle +++ b/local_plugins/music_service/android/build.gradle @@ -66,6 +66,5 @@ android { } dependencies { - implementation project(':deepvoice_utils') implementation("androidx.media:media:1.6.0") } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 1274555d4..6ab502100 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -17,7 +17,7 @@ import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.session.MediaButtonReceiver -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.atomic.AtomicBoolean @@ -46,16 +46,15 @@ class MusicService : MediaBrowserServiceCompat() { override fun onCreate() { super.onCreate() - FileLogger.init(this) isRunning.set(true) setupMediaSession() startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound","")) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - FileLogger.i(TAG, "MediaService 启动完成") + Log.i(TAG, "MediaService 启动完成") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - FileLogger.i(TAG, "onStartCommand: $intent") + Log.i(TAG, "onStartCommand: $intent") intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { // 分发给 MediaSession 处理媒体按钮 MediaButtonReceiver.handleIntent(mediaSession, intent) @@ -108,10 +107,10 @@ class MusicService : MediaBrowserServiceCompat() { ) // 回调中处理具体操作 setCallback(object : MediaSessionCompat.Callback() { - override fun onPlay() { FileLogger.i(TAG, "onPlay 通知!"); resume()} - override fun onPause() { FileLogger.i(TAG, "onPause 通知!"); pause()} - override fun onSkipToNext(){ FileLogger.i(TAG, "onSkipToNext 通知!"); playNext()} - override fun onSkipToPrevious() { FileLogger.i(TAG, "onSkipToNext 通知!"); playPrevious()} + override fun onPlay() { Log.i(TAG, "onPlay 通知!"); resume()} + override fun onPause() { Log.i(TAG, "onPause 通知!"); pause()} + override fun onSkipToNext(){ Log.i(TAG, "onSkipToNext 通知!"); playNext()} + override fun onSkipToPrevious() { Log.i(TAG, "onSkipToNext 通知!"); playPrevious()} }) // 初始播放状态 setPlaybackState( @@ -222,7 +221,7 @@ class MusicService : MediaBrowserServiceCompat() { private fun play(song: Map) { val url = song["url"] ?: return - FileLogger.i(TAG, "开始播放: $url") + Log.i(TAG, "开始播放: $url") val title = song["title"] ?: "未知标题" val artist = song["artist"] ?: "未知歌手" val coverUrl = song["coverUrl"] @@ -235,11 +234,11 @@ class MusicService : MediaBrowserServiceCompat() { setOnPreparedListener { start() updateNotification(title,artist,coverUrl) - FileLogger.i(TAG, "开始播放: $url") + Log.i(TAG, "开始播放: $url") } setOnCompletionListener { playNextOrStop() } setOnErrorListener { _, what, extra -> - FileLogger.e(TAG, "播放错误 what=$what extra=$extra") + Log.e(TAG, "播放错误 what=$what extra=$extra") true } prepareAsync() @@ -262,7 +261,7 @@ class MusicService : MediaBrowserServiceCompat() { val coverUrl = song["coverUrl"] updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PLAYING); - FileLogger.i(TAG, "已暂停") + Log.i(TAG, "已暂停") } } @@ -282,7 +281,7 @@ class MusicService : MediaBrowserServiceCompat() { val coverUrl = song["coverUrl"] updateNotification(title,artist,coverUrl) setPlaybackState(PlaybackStateCompat.STATE_PAUSED); -// FileLogger.i(TAG, "继续播放") +// Log.i(TAG, "继续播放") } } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index 89bf0edd8..9756b25f8 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -3,7 +3,7 @@ package com.yunqiinnovation.music_service import android.content.Context import android.content.Intent import androidx.core.content.ContextCompat -import com.yunqiinnovation.deepvoice_utils.FileLogger +import android.util.Log import com.yunqiinnovation.music_service.MusicServiceStarter import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall @@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - FileLogger.d(TAG, "Method call: ${call.method}") + Log.d(TAG, "Method call: ${call.method}") when (call.method) { "play" -> { val song = call.argument>("song") diff --git a/local_plugins/deepvoice_utils/android/build.gradle.kts b/local_plugins/speech/android/build.gradle.kts similarity index 53% rename from local_plugins/deepvoice_utils/android/build.gradle.kts rename to local_plugins/speech/android/build.gradle.kts index 61848a582..f5bbc9337 100644 --- a/local_plugins/deepvoice_utils/android/build.gradle.kts +++ b/local_plugins/speech/android/build.gradle.kts @@ -4,11 +4,12 @@ plugins { // Kotlin Android 插件 id("org.jetbrains.kotlin.android") id("com.github.johnrengelman.shadow") version "8.1.1" + kotlin("plugin.serialization") version "1.9.24" } android { // 命名空间,对应你插件的包名(需与代码内包名保持一致) - namespace = "com.yunqiinnovation.deepvoice_utils" + namespace = "com.yunqiinnovation.open_ai_service" // 目标 SDK 版本 compileSdk = 35 @@ -16,7 +17,7 @@ android { defaultConfig { // 最低 SDK 版本 minSdk = 21 - targetSdk = 34 + targetSdk = 33 } // Java 语言级别兼容配置 @@ -44,3 +45,20 @@ android { } } } + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2") + implementation("com.squareup.okhttp3:okhttp:4.9.3") + implementation("com.squareup.okhttp3:logging-interceptor:4.9.3") + implementation("com.anthropic:anthropic-java:0.8.0") + implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0") + implementation("io.ktor:ktor-client-core:3.1.2") + implementation("io.ktor:ktor-client-cio:3.1.2") + implementation("io.ktor:ktor-client-okhttp:3.1.2") + implementation("io.ktor:ktor-client-logging:3.1.2") + implementation("com.google.code.gson:gson:2.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("androidx.media:media:1.6.0") +} \ No newline at end of file diff --git a/local_plugins/speech/android/settings.gradle.kts b/local_plugins/speech/android/settings.gradle.kts new file mode 100644 index 000000000..1c33f4971 --- /dev/null +++ b/local_plugins/speech/android/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "open_ai_service" \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/AndroidManifest.xml b/local_plugins/speech/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d3f8e2308 --- /dev/null +++ b/local_plugins/speech/android/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt new file mode 100644 index 000000000..d805a757e --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt @@ -0,0 +1,52 @@ +package com.deep_voice.speech.asr + +/** + * ASR事件类型枚举 + */ +enum class AsrEventType { + /** 会话开始 */ + SESSION_STARTED, + + /** 会话结束 */ + SESSION_STOPPED, + + + /** 识别中(中间结果) */ + RECOGNIZING, + + /** 识别完成(最终结果) */ + RECOGNIZED, + + /** 识别取消 */ + CANCELED, + + /** 发生错误 */ + ERROR, + +} + +/** + * ASR事件数据类 + * + * @param type 事件类型 + * @param params 事件参数,根据不同的事件类型包含不同的内容: + * - ERROR: 包含 "errorCode" 和 "errorMessage" + * - CANCELED: 包含 "reason" 和 "errorDetails" + * - RECOGNIZING/RECOGNIZED: 包含 "text", "languageCode" + */ +data class AsrEvent( + val type: AsrEventType, + val params: Map = emptyMap() +) + +/** + * ASR事件监听接口 + */ +interface AsrEventListener { + /** + * 当发生ASR事件时调用 + * + * @param event ASR事件 + */ + fun onEvent(event: AsrEvent) +} \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt new file mode 100644 index 000000000..94da1b435 --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt @@ -0,0 +1,90 @@ +package com.deep_voice.speech.asr + +/** + * ASR服务通用接口 + * + * 定义了语音识别服务的标准接口,可用于不同提供商的实现,如火山引擎、Azure等 + */ +interface IAsrService { + /** + * 初始化语音识别服务 + * + * @param subscriptionKey 服务订阅密钥/访问令牌 + * @param region 服务区域/应用ID + * @param supportedLanguages 支持的语言列表,如["zh-CN", "en-US"] + * @return 是否初始化成功 + */ + suspend fun initialize( + subscriptionKey: String, + region: String, + supportedLanguages: List? = null + ): Boolean + + /** + * 单次识别(非流式) + * + * @param audioData 要识别的音频数据字节数组 + * @param callback 识别结果回调 + * @return 是否成功开始识别 + */ + suspend fun recognizeOnce(audioData: ByteArray, callback: (String) -> Unit): Boolean + + /** + * 启动连续识别 + * 开始持续监听音频输入并识别 + * + * @return 是否成功启动 + */ + suspend fun startContinuousRecognition(): Boolean + + /** + * 停止连续识别 + * + * @return 是否成功停止 + */ + suspend fun stopContinuousRecognition(): Boolean + + /** + * 推送音频数据 + * 在连续识别模式下推送新的音频数据 + * + * @param audioData 要识别的音频数据字节数组 + * @return 是否成功处理 + */ + suspend fun pushAudioData(audioData: ByteArray): Boolean + + /** + * 添加ASR事件监听器 + * + * @param listener 事件监听器 + */ + fun addListener(listener: AsrEventListener) + + /** + * 移除ASR事件监听器 + * + * @param listener 要移除的事件监听器 + */ + fun removeListener(listener: AsrEventListener) + + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ + fun release() + + /** + * 判断当前是否正在识别 + * + * @return 是否正在进行语音识别 + */ + fun isRecognizing(): Boolean + + /** + * 设置识别参数 + * + * @param params 参数键值对 + * @return 是否设置成功 + */ + fun setParameters(params: Map): Boolean +} \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt new file mode 100644 index 000000000..a129fd0aa --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt @@ -0,0 +1,92 @@ +package com.deep_voice.speech.tts + +/** + * TTS服务通用接口 + * + * 定义了语音合成服务的标准接口,可用于不同提供商的实现,如微软Azure、火山语音等 + */ +interface ITtsService { + /** + * 初始化语音合成服务 + * + * @param ttsAppId 服务应用ID + * @param ttsAppToken 服务访问令牌/订阅密钥 + * @param ttsResource 服务资源ID/区域(可选) + * @param language 语言代码,如"zh-CN" + * @return 是否初始化成功 + */ + suspend fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String = "", language: String = "zh-CN"): Boolean + + /** + * 设置语音角色 + * + * @param voiceName 语音角色名称(不同服务的语音角色命名可能不同) + * @return 是否设置成功 + */ + fun setVoice(voiceName: String): Boolean + + /** + * 单次播放文本(非流式) + * + * @param text 要合成的文本 + * @return 是否成功开始合成 + */ + suspend fun speakOnce(text: String): Boolean + + /** + * 流式合成 + * 在流式场景下合成并播放文本,适用于连续的文本输入 + * + * @param text 要合成的文本片段 + * @return 是否成功处理 + */ + suspend fun speakStream(text: String): Boolean + + /** + * 刷新并播放流式文本缓冲区中的剩余内容 + * + * @return 是否成功处理 + */ + suspend fun flushStream(): Boolean + + /** + * 停止当前合成和播放 + * + * @return 是否成功停止 + */ + suspend fun stop(): Boolean + + /** + * 释放资源 + * 在不再需要服务时调用,释放底层资源 + */ + fun release() + + /** + * 添加TTS事件监听器 + * + * @param listener 事件监听器 + */ + fun addListener(listener: TtsEventListener) + + /** + * 移除TTS事件监听器 + * + * @param listener 要移除的事件监听器 + */ + fun removeListener(listener: TtsEventListener) + + /** + * 添加音频数据监听器 + * + * @param listener 音频数据监听器 + */ + fun addAudioDataListener(listener: AudioDataListener) + + /** + * 移除音频数据监听器 + * + * @param listener 要移除的音频数据监听器 + */ + fun removeAudioDataListener(listener: AudioDataListener) +} \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt new file mode 100644 index 000000000..036344edd --- /dev/null +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt @@ -0,0 +1,63 @@ +package com.deep_voice.speech.tts + +/** + * TTS事件类型枚举 + */ +enum class TtsEventType { + /** 开始合成 */ + SYNTHESIS_STARTED, + + /** 合成完成 */ + SYNTHESIS_COMPLETED, + + /** 合成取消 */ + SYNTHESIS_CANCELED, + + /** 开始播放 */ + STARTED, + + /** 播放完成 */ + COMPLETED, + + /** 播放取消 */ + CANCELED, + + /** 发生错误 */ + ERROR +} + +/** + * TTS事件数据类 + * + * @param type 事件类型 + * @param params 事件参数,根据不同的事件类型包含不同的内容 + * 错误事件通常包含 "errorCode" 和 "errorMessage" + */ +data class TtsEvent( + val type: TtsEventType, + val params: Map = emptyMap() +) + +/** + * TTS事件监听接口 + */ +interface TtsEventListener { + /** + * 当发生TTS事件时调用 + * + * @param event TTS事件 + */ + fun onEvent(event: TtsEvent) +} + +/** + * 音频数据监听接口 + */ +interface AudioDataListener { + /** + * 当接收到音频数据时调用 + * + * @param data 音频数据字节数组 + */ + fun onAudioData(data: ByteArray) +} \ No newline at end of file diff --git a/local_plugins/volcano_speech/android/build.gradle.kts b/local_plugins/volcano_speech/android/build.gradle.kts index cf924302d..bac1de85d 100644 --- a/local_plugins/volcano_speech/android/build.gradle.kts +++ b/local_plugins/volcano_speech/android/build.gradle.kts @@ -1,21 +1,11 @@ -import com.android.build.gradle.LibraryExtension - buildscript { repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.3.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") - } -} - -allprojects { - repositories { - google() - mavenCentral() - + classpath("com.android.tools.build:gradle:7.3.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.21") } } @@ -24,10 +14,9 @@ plugins { id("org.jetbrains.kotlin.android") } -// 配置android扩展 -configure { +android { namespace = "com.yunqiinnovation.volcano_speech" - compileSdkVersion(35) + compileSdk = 35 defaultConfig { minSdk = 21 @@ -38,6 +27,10 @@ configure { targetCompatibility = JavaVersion.VERSION_11 } + kotlinOptions { + jvmTarget = "11" + } + sourceSets { getByName("main") { manifest.srcFile("src/main/AndroidManifest.xml") @@ -49,17 +42,20 @@ configure { lintOptions { isCheckReleaseBuilds = false } -} - -// 显式设置Kotlin JVM目标版本 -tasks.withType { - kotlinOptions { - jvmTarget = "11" - } + } dependencies { + // Flutter插件依赖 + compileOnly("io.flutter:flutter_embedding_debug:1.0.0-a794cf2681") + // 添加火山引擎语音合成SDK - // implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") - implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7") + implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") + // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8") + // implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7") +} + +repositories { + google() + mavenCentral() } \ No newline at end of file diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt index 92073c7b4..68e1db96f 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt @@ -6,7 +6,7 @@ import android.os.Looper import com.bytedance.speech.speechengine.SpeechEngine import com.bytedance.speech.speechengine.SpeechEngineDefines import com.bytedance.speech.speechengine.SpeechEngineGenerator -import com.yunqiinnovation.volcano_speech.utils.FileLogger +import android.util.Log import org.json.JSONException import org.json.JSONObject import org.json.JSONArray @@ -84,7 +84,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun initialize(appId: String, token: String, resourceId: String = "volc.bigasr.sauc.duration"): Boolean { if (isInitialized) { - FileLogger.i(TAG, "引擎已经初始化") + Log.i(TAG, "引擎已经初始化") return true } @@ -94,7 +94,7 @@ class VolcanoAsrHelper(private val context: Context) { try { SpeechEngineGenerator.PrepareEnvironment(context, null) } catch (e: Exception) { - FileLogger.w(TAG, "准备环境时发生异常: ${e.message},尝试继续初始化") + Log.w(TAG, "准备环境时发生异常: ${e.message},尝试继续初始化") } // 创建引擎 @@ -102,7 +102,7 @@ class VolcanoAsrHelper(private val context: Context) { engineHandler = engine?.createEngine() ?: -1 if (engineHandler == -1L) { - FileLogger.e(TAG, "创建引擎失败") + Log.e(TAG, "创建引擎失败") state = AsrState.ERROR return false } @@ -176,7 +176,7 @@ class VolcanoAsrHelper(private val context: Context) { isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR if (isInitialized) { - FileLogger.i(TAG, "引擎初始化成功") + Log.i(TAG, "引擎初始化成功") state = AsrState.READY // 设置回调监听 @@ -189,12 +189,12 @@ class VolcanoAsrHelper(private val context: Context) { return true } else { - FileLogger.e(TAG, "引擎初始化失败: $result") + Log.e(TAG, "引擎初始化失败: $result") state = AsrState.ERROR return false } } catch (e: Exception) { - FileLogger.e(TAG, "初始化异常: ${e.message}", e) + Log.e(TAG, "初始化异常: ${e.message}", e) state = AsrState.ERROR return false } @@ -205,12 +205,12 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setAudioInputType(type: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isEngineStarted) { - FileLogger.e(TAG, "引擎已启动,无法更改配置") + Log.e(TAG, "引擎已启动,无法更改配置") return false } @@ -219,7 +219,7 @@ class VolcanoAsrHelper(private val context: Context) { "mic" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_RECORDER "stream" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_STREAM else -> { - FileLogger.e(TAG, "不支持的音频输入类型: $type") + Log.e(TAG, "不支持的音频输入类型: $type") return false } } @@ -227,7 +227,7 @@ class VolcanoAsrHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, recorderType) return true } catch (e: Exception) { - FileLogger.e(TAG, "设置音频输入类型失败: ${e.message}", e) + Log.e(TAG, "设置音频输入类型失败: ${e.message}", e) return false } } @@ -237,12 +237,12 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setAudioParams(sampleRate: Int, channels: Int): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isEngineStarted) { - FileLogger.e(TAG, "引擎已启动,无法更改配置") + Log.e(TAG, "引擎已启动,无法更改配置") return false } @@ -267,7 +267,7 @@ class VolcanoAsrHelper(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(TAG, "设置音频参数失败: ${e.message}", e) + Log.e(TAG, "设置音频参数失败: ${e.message}", e) return false } } @@ -277,7 +277,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setLanguage(language: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } @@ -286,7 +286,7 @@ class VolcanoAsrHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_LANGUAGE_STRING, language) return true } catch (e: Exception) { - FileLogger.e(TAG, "设置语言失败: ${e.message}", e) + Log.e(TAG, "设置语言失败: ${e.message}", e) return false } } @@ -296,7 +296,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun setHotWords(hotWordsId: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } @@ -308,7 +308,7 @@ class VolcanoAsrHelper(private val context: Context) { } return true } catch (e: Exception) { - FileLogger.e(TAG, "设置热词失败: ${e.message}", e) + Log.e(TAG, "设置热词失败: ${e.message}", e) return false } } @@ -317,14 +317,14 @@ class VolcanoAsrHelper(private val context: Context) { * 一次性识别(异步) */ fun recognizeOnce(callback: RecognizeCallback): Boolean { - FileLogger.i(TAG, "开始一次性识别") + Log.i(TAG, "开始一次性识别") if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isRecognizing) { - FileLogger.e(TAG, "当前已在识别中") + Log.e(TAG, "当前已在识别中") return false } @@ -345,7 +345,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "启动识别失败: $ret") + Log.e(TAG, "启动识别失败: $ret") return false } @@ -354,7 +354,7 @@ class VolcanoAsrHelper(private val context: Context) { state = AsrState.RECOGNIZING return true } catch (e: Exception) { - FileLogger.e(TAG, "识别异常: ${e.message}", e) + Log.e(TAG, "识别异常: ${e.message}", e) return false } } @@ -364,12 +364,12 @@ class VolcanoAsrHelper(private val context: Context) { */ fun stopRecognize(): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (!isEngineStarted) { - FileLogger.e(TAG, "引擎未启动") + Log.e(TAG, "引擎未启动") return false } @@ -379,7 +379,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "停止识别失败: $ret") + Log.e(TAG, "停止识别失败: $ret") return false } } else { @@ -387,7 +387,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "停止识别失败: $ret") + Log.e(TAG, "停止识别失败: $ret") return false } @@ -398,7 +398,7 @@ class VolcanoAsrHelper(private val context: Context) { return true } catch (e: Exception) { - FileLogger.e(TAG, "停止识别异常: ${e.message}", e) + Log.e(TAG, "停止识别异常: ${e.message}", e) return false } } @@ -407,14 +407,14 @@ class VolcanoAsrHelper(private val context: Context) { * 开始连续识别 */ fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { - FileLogger.i(TAG, "开始连续识别") + Log.i(TAG, "开始连续识别") if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (isRecognizing) { - FileLogger.e(TAG, "当前已在识别中") + Log.e(TAG, "当前已在识别中") return false } @@ -436,17 +436,17 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "启动连续识别失败: $ret") + Log.e(TAG, "启动连续识别失败: $ret") return false } isRecognizing = true isEngineStarted = true state = AsrState.RECOGNIZING - FileLogger.i(TAG, "开始连续识别") + Log.i(TAG, "开始连续识别") return true } catch (e: Exception) { - FileLogger.e(TAG, "连续识别异常: ${e.message}", e) + Log.e(TAG, "连续识别异常: ${e.message}", e) return false } } @@ -456,7 +456,7 @@ class VolcanoAsrHelper(private val context: Context) { */ fun stopContinuousRecognition(): Boolean { if (!isInitialized || !isRecognizing) { - FileLogger.e(TAG, "引擎未初始化或未在识别中") + Log.e(TAG, "引擎未初始化或未在识别中") return false } @@ -465,7 +465,7 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "停止连续识别失败: $ret") + Log.e(TAG, "停止连续识别失败: $ret") return false } @@ -474,7 +474,7 @@ class VolcanoAsrHelper(private val context: Context) { state = AsrState.READY return true } catch (e: Exception) { - FileLogger.e(TAG, "停止连续识别异常: ${e.message}", e) + Log.e(TAG, "停止连续识别异常: ${e.message}", e) return false } } @@ -485,17 +485,17 @@ class VolcanoAsrHelper(private val context: Context) { */ fun feedAudio(data: ByteArray): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (!isEngineStarted) { - FileLogger.e(TAG, "引擎未启动") + Log.e(TAG, "引擎未启动") return false } if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { - FileLogger.e(TAG, "当前录音类型不是stream模式,无法使用此方法") + Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法") return false } @@ -504,13 +504,13 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.feedAudio(engineHandler, data, data.size) if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "发送音频数据失败: $ret") + Log.e(TAG, "发送音频数据失败: $ret") return false } return true } catch (e: Exception) { - FileLogger.e(TAG, "发送音频数据异常: ${e.message}", e) + Log.e(TAG, "发送音频数据异常: ${e.message}", e) return false } } @@ -521,17 +521,17 @@ class VolcanoAsrHelper(private val context: Context) { */ fun feedAudioEnd(): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } if (!isEngineStarted) { - FileLogger.e(TAG, "引擎未启动") + Log.e(TAG, "引擎未启动") return false } if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { - FileLogger.e(TAG, "当前录音类型不是stream模式,无法使用此方法") + Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法") return false } @@ -540,13 +540,13 @@ class VolcanoAsrHelper(private val context: Context) { val ret = engine?.feedAudio(engineHandler, ByteArray(0), 0) if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - FileLogger.e(TAG, "发送音频结束标记失败: $ret") + Log.e(TAG, "发送音频结束标记失败: $ret") return false } return true } catch (e: Exception) { - FileLogger.e(TAG, "发送音频结束标记异常: ${e.message}", e) + Log.e(TAG, "发送音频结束标记异常: ${e.message}", e) return false } } @@ -604,9 +604,9 @@ class VolcanoAsrHelper(private val context: Context) { isInitialized = false state = AsrState.IDLE - FileLogger.i(TAG, "引擎已释放") + Log.i(TAG, "引擎已释放") } catch (e: Exception) { - FileLogger.e(TAG, "释放引擎异常: ${e.message}", e) + Log.e(TAG, "释放引擎异常: ${e.message}", e) } } @@ -617,7 +617,7 @@ class VolcanoAsrHelper(private val context: Context) { when (type) { SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { // 引擎启动成功 - FileLogger.i(TAG, "引擎启动成功") + Log.i(TAG, "引擎启动成功") mainHandler.post { continuousCallback?.onSessionStarted() } @@ -625,7 +625,7 @@ class VolcanoAsrHelper(private val context: Context) { SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { // 引擎停止 - FileLogger.i(TAG, "引擎停止") + Log.i(TAG, "引擎停止") isRecognizing = false isEngineStarted = false state = AsrState.READY @@ -637,15 +637,15 @@ class VolcanoAsrHelper(private val context: Context) { SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> { // 中间识别结果 - FileLogger.i(TAG, "中间识别结果: $data") + Log.i(TAG, "中间识别结果: $data") try { val json = JSONObject(data) if (!json.has("result")) { - FileLogger.w(TAG, "中间结果没有 result 字段: $data") + Log.w(TAG, "中间结果没有 result 字段: $data") return } // if (json.has("definite")) { - // FileLogger.i(TAG, "utterances: ${json.getString("utterances")}") + // Log.i(TAG, "utterances: ${json.getString("utterances")}") // } // 从多种可能的格式中获取文本 val text = extractTextFromResult(json) @@ -655,23 +655,23 @@ class VolcanoAsrHelper(private val context: Context) { if (text.isNotEmpty()) { mainHandler.post { - FileLogger.i(TAG, "提取到中间文本: $text") + Log.i(TAG, "提取到中间文本: $text") // 中间结果 continuousCallback?.onRecognizing(text, detectedLanguage) } } } catch (e: Exception) { - FileLogger.e(TAG, "解析中间识别结果异常: ${e}") + Log.e(TAG, "解析中间识别结果异常: ${e}") } } SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> { - FileLogger.i(TAG, "最终识别结果: $data") + Log.i(TAG, "最终识别结果: $data") // 最终识别结果 try { val json = JSONObject(data) if (!json.has("result")) { - FileLogger.w(TAG, "最终结果没有 result 字段: $data") + Log.w(TAG, "最终结果没有 result 字段: $data") return } @@ -685,22 +685,22 @@ class VolcanoAsrHelper(private val context: Context) { if (text.isNotEmpty()) { mainHandler.post { if (isContinuousMode) { - FileLogger.i(TAG, "发送最终识别结果: $text (连续模式)") + Log.i(TAG, "发送最终识别结果: $text (连续模式)") continuousCallback?.onResult(text, detectedLanguage) // 在连续模式下,不需要手动触发下一轮识别,引擎会自动继续监听 // 但如果需要明确标记新一轮开始,可以在此处添加代码 } else { - FileLogger.i(TAG, "发送最终识别结果: $text (单次模式)") + Log.i(TAG, "发送最终识别结果: $text (单次模式)") recognizeCallback?.onResult(text, detectedLanguage) } } } } catch (e: Exception) { val errMsg = "解析最终识别结果异常: ${e.message}" - FileLogger.e(TAG, errMsg) - FileLogger.e(TAG, "原始数据: $data") - FileLogger.e(TAG, "详细异常信息", e) + Log.e(TAG, errMsg) + Log.e(TAG, "原始数据: $data") + Log.e(TAG, "详细异常信息", e) } } @@ -715,7 +715,7 @@ class VolcanoAsrHelper(private val context: Context) { val errMsg = json.optString("err_msg", "未知错误") val errCode = json.optInt("err_code", -1) - FileLogger.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") + Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") state = AsrState.ERROR mainHandler.post { @@ -728,7 +728,7 @@ class VolcanoAsrHelper(private val context: Context) { isEngineStarted = false } } catch (e: JSONException) { - FileLogger.e(TAG, "解析错误信息异常: ${e.message}", e) + Log.e(TAG, "解析错误信息异常: ${e.message}", e) } } } @@ -775,8 +775,8 @@ class VolcanoAsrHelper(private val context: Context) { return text } catch (e: Exception) { - FileLogger.e(TAG, "提取文本异常: ${e.message}", e) - FileLogger.e(TAG, "详细异常信息", e) + Log.e(TAG, "提取文本异常: ${e.message}", e) + Log.e(TAG, "详细异常信息", e) return "" } } diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt index 303f92072..5d3b705d3 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt @@ -4,14 +4,16 @@ import android.content.Context import android.os.Handler import android.os.Looper import androidx.annotation.NonNull -import com.yunqiinnovation.volcano_speech.utils.FileLogger - import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel +import android.util.Log +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack /** VolcanoSpeechPlugin */ class VolcanoSpeechPlugin: FlutterPlugin { @@ -126,7 +128,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { // 发送ASR事件 private fun sendAsrEvent(event: Map) { if (asrEventSink == null) { - FileLogger.w(TAG, "无法发送ASR事件:事件通道未准备好") + Log.w(TAG, "无法发送ASR事件:事件通道未准备好") return } @@ -134,25 +136,25 @@ class VolcanoSpeechPlugin: FlutterPlugin { try { asrEventSink?.success(event) } catch (e: Exception) { - FileLogger.e(TAG, "发送ASR事件失败: ${e.message}") + Log.e(TAG, "发送ASR事件失败: ${e.message}") } } } // 发送TTS事件 private fun sendTtsEvent(event: Map) { - FileLogger.d(TAG, "发送TTS事件: $event") + Log.d(TAG, "发送TTS事件: $event") if (ttsEventSink == null) { - FileLogger.w(TAG, "无法发送TTS事件:事件通道未准备好") + Log.w(TAG, "无法发送TTS事件:事件通道未准备好") return } mainHandler.post { try { ttsEventSink?.success(event) - FileLogger.d(TAG, "TTS事件发送成功") + Log.d(TAG, "TTS事件发送成功") } catch (e: Exception) { - FileLogger.e(TAG, "发送TTS事件失败: ${e.message}") + Log.e(TAG, "发送TTS事件失败: ${e.message}") } } } @@ -167,7 +169,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { try { ttsAudioDataSink?.success(data) } catch (e: Exception) { - FileLogger.e(TAG, "发送TTS音频数据失败: ${e.message}") + Log.e(TAG, "发送TTS音频数据失败: ${e.message}") } } } diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt index 46fd10269..60d412594 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt @@ -6,7 +6,7 @@ import android.os.Looper import com.bytedance.speech.speechengine.SpeechEngine import com.bytedance.speech.speechengine.SpeechEngineDefines import com.bytedance.speech.speechengine.SpeechEngineGenerator -import com.yunqiinnovation.volcano_speech.utils.FileLogger +import android.util.Log import org.json.JSONObject import java.util.concurrent.ConcurrentLinkedQueue @@ -28,7 +28,7 @@ class VolcanoTtsHelper(private val context: Context) { private var currentReqId = "" // 配置参数 - private var voice = "zh_male_M392_conversation_wvae_bigtts" + private var voice = "zh_female_linjianvhai_moon_bigtts" // 文本队列管理 private val pendingTexts = ConcurrentLinkedQueue() @@ -51,6 +51,10 @@ class VolcanoTtsHelper(private val context: Context) { // 事件监听 private val listeners = mutableListOf() + // 添加新的成员变量,用于流式文本处理 + private val streamBuffer = StringBuilder() + private var lastSpeakTime = 0L + /** * TTS事件类型 */ @@ -172,7 +176,7 @@ class VolcanoTtsHelper(private val context: Context) { try { SpeechEngineGenerator.PrepareEnvironment(context, null) } catch (e: Exception) { - FileLogger.w(TAG, "准备环境时发生异常: ${e.message}") + Log.w(TAG, "准备环境时发生异常: ${e.message}") } // 创建引擎 @@ -238,9 +242,9 @@ class VolcanoTtsHelper(private val context: Context) { */ private fun handleError(code: Int, message: String, e: Exception? = null) { if (e != null) { - FileLogger.e(TAG, message, e) + Log.e(TAG, message, e) } else { - FileLogger.e(TAG, message) + Log.e(TAG, message) } dispatchEvent(TtsEvent(TtsEventType.ERROR, code, message)) } @@ -250,7 +254,7 @@ class VolcanoTtsHelper(private val context: Context) { */ fun setVoice(voiceName: String): Boolean { if (!isInitialized) { - FileLogger.e(TAG, "引擎未初始化") + Log.e(TAG, "引擎未初始化") return false } @@ -264,6 +268,25 @@ class VolcanoTtsHelper(private val context: Context) { } } + /** + * 处理文本,移除不需要的特殊字符 + */ + private fun processText(text: String): String { + // 定义需要移除的特殊字符 + val specialChars = listOf("#", "*") + + // 移除所有定义的特殊字符 + var processedText = text + specialChars.forEach { char -> + processedText = processedText.replace(char, " ") + } + + // // 移除表情符号 (简单处理,移除常见的Unicode表情符号范围) + // processedText = processedText.replace(Regex("[\uD83C-\uDBFF\uDC00-\uDFFF]"), "") + + return processedText + } + /** * 单次播放文本 */ @@ -272,6 +295,9 @@ class VolcanoTtsHelper(private val context: Context) { return false } + // 处理文本,移除特殊字符 + val processedText = processText(text) + // 停止当前播放 stop() @@ -289,10 +315,10 @@ class VolcanoTtsHelper(private val context: Context) { configAudioCallback() // 预备要合成的文本 - currentText = text + currentText = processedText // 单次播放模式:直接启动引擎并设置文本,自动开始合成 - return startEngineWithText(text, false) + return startEngineWithText(processedText, false) } /** @@ -303,27 +329,83 @@ class VolcanoTtsHelper(private val context: Context) { return false } - // 设置连续播放模式 - if (!isContinuousMode) { - isContinuousMode = true - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL) + try { + // 处理文本,移除特殊字符 + val processedText = processText(text) - // 启用音频数据回调(如果需要) - configAudioCallback() - } - - // 加入队列 - pendingTexts.add(text) - - // 如果当前引擎未启动,启动引擎 - if (!isEngineStarted) { - return startEngine() - } else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) { - // 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成 - return triggerNextSynthesis() + // 设置连续播放模式 + if (!isContinuousMode) { + isContinuousMode = true + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL) + + // 启用音频数据回调(如果需要) + configAudioCallback() + } + + // 添加新文本到缓冲区 + streamBuffer.append(processedText) + + // 增加防抖逻辑,避免频繁处理小片段文本 + val currentTime = System.currentTimeMillis() + if (currentTime - lastSpeakTime < 600) { + return true + } + lastSpeakTime = currentTime + + // 获取缓冲区中的全部文本 + val currentText = streamBuffer.toString() + + // 定义标点符号列表 + val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':') + + // 查找最后一个标点符号的位置 + var lastPunctuationIndex = -1 + for (i in currentText.indices.reversed()) { + if (currentText[i] in punctuationMarks) { + lastPunctuationIndex = i + break + } + } + + // 如果找到标点符号,则播放到该标点符号 + if (lastPunctuationIndex >= 0) { + // 提取要播放的文本(包含标点符号) + val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1) + + // 剩余的文本保存在缓冲区中 + streamBuffer.delete(0, lastPunctuationIndex + 1) + + // 加入队列播放提取的文本 + pendingTexts.add(textToSpeak) + + // 如果当前引擎未启动,启动引擎 + if (!isEngineStarted) { + return startEngine() + } else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) { + // 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成 + return triggerNextSynthesis() + } + } else if (currentText.length > 50) { + // 如果文本长度超过50个字符但没有找到标点符号,强制分段 + val textToSpeak = currentText + streamBuffer.delete(0, currentText.length) + + // 加入队列播放 + pendingTexts.add(textToSpeak) + + // 根据引擎状态触发合成 + if (!isEngineStarted) { + return startEngine() + } else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) { + return triggerNextSynthesis() + } + } + + return true + } catch (e: Exception) { + Log.e(TAG, "流式语音合成失败: ${e.message}", e) + return false } - - return true } /** @@ -411,26 +493,52 @@ class VolcanoTtsHelper(private val context: Context) { * 播放队列中剩余文本并标记流式输入结束 */ fun flushStream(): Boolean { - if (pendingTexts.isEmpty() && !isEngineStarted) { - isProcessingQueue = false - return true + if (!isInitialized) { + return false } - // 如果引擎未启动,则启动引擎 - if (!isEngineStarted) { - val result = startEngine() - if (result && !pendingTexts.isEmpty()) { - // 引擎启动成功后,在引擎启动回调中会自动触发第一句合成 + try { + // 获取缓冲区中剩余的文本 + val remainingText = streamBuffer.toString() + + // 清空缓冲区 + streamBuffer.clear() + + // 如果缓冲区有未播放的文本,加入队列 + if (remainingText.isNotEmpty()) { + pendingTexts.add(remainingText) } - return result - } - - // 如果队列为空且当前没有播放,则关闭引擎 - if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) { - closeEngine() + + // 如果队列为空且引擎未启动,直接返回 + if (pendingTexts.isEmpty() && !isEngineStarted) { + isProcessingQueue = false + return true + } + + // 如果引擎未启动,则启动引擎 + if (!isEngineStarted) { + val result = startEngine() + if (result && !pendingTexts.isEmpty()) { + // 引擎启动成功后,在引擎启动回调中会自动触发第一句合成 + } + return result + } + + // 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成 + if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer && !pendingTexts.isEmpty()) { + return triggerNextSynthesis() + } + + // 如果队列为空且当前没有播放,则关闭引擎 + if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) { + closeEngine() + } + + return true + } catch (e: Exception) { + Log.e(TAG, "刷新流式文本失败: ${e.message}", e) + return false } - - return true } /** @@ -447,7 +555,7 @@ class VolcanoTtsHelper(private val context: Context) { isSynthesisInProgress = false return ret == SpeechEngineDefines.ERR_NO_ERROR } catch (e: Exception) { - FileLogger.e(TAG, "关闭引擎异常", e) + Log.e(TAG, "关闭引擎异常", e) return false } } @@ -521,12 +629,15 @@ class VolcanoTtsHelper(private val context: Context) { isContinuousMode = false synthesisFromPlayer = false + // 清空流缓冲区 + streamBuffer.clear() + // 发送取消事件 dispatchEvent(TtsEvent(TtsEventType.CANCELED)) return ret == SpeechEngineDefines.ERR_NO_ERROR } catch (e: Exception) { - FileLogger.e(TAG, "停止播放异常", e) + Log.e(TAG, "停止播放异常", e) return false } } @@ -549,10 +660,11 @@ class VolcanoTtsHelper(private val context: Context) { engine = null isInitialized = false pendingTexts.clear() + streamBuffer.clear() listeners.clear() audioDataListeners.clear() } catch (e: Exception) { - FileLogger.e(TAG, "释放引擎异常", e) + Log.e(TAG, "释放引擎异常", e) } } @@ -642,7 +754,7 @@ class VolcanoTtsHelper(private val context: Context) { val errCode = json.optInt("err_code", -1) val errMsg = json.optString("err_msg", "未知错误") - FileLogger.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") + Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") // 标记当前合成结束 isSynthesisInProgress = false diff --git a/pubspec.yaml b/pubspec.yaml index 608ba936e..cd9173c35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,10 @@ dependencies: path: local_plugins/agent_service music_service: path: local_plugins/music_service + volcano_speech: + path: local_plugins/volcano_speech + bytedance_speech: + path: local_plugins/bytedance_speech dev_dependencies: From 547999d821734d673fb67a1bbe01379c632b3b9d Mon Sep 17 00:00:00 2001 From: wolfplus Date: Fri, 16 May 2025 17:46:39 +0100 Subject: [PATCH 2/2] add --- android/build.gradle.kts | 2 +- lib/core/themes/theme_service.dart | 4 +- lib/core/utils/logger.dart | 85 +-- lib/data/services/ble_manager.dart | 41 +- lib/data/services/bluetooth_manager.dart | 8 +- lib/data/services/chat_history_service.dart | 2 +- lib/data/services/deapsound_ai_service.dart | 6 +- lib/data/services/kouzi_ai_service.dart | 6 +- lib/data/services/speech_factory.dart | 4 +- .../speech_impl/azure_asr_service.dart | 21 +- .../speech_impl/azure_tts_service.dart | 22 +- .../speech_impl/flutter_asr_service.dart | 18 +- .../speech_impl/flutter_tts_service.dart | 24 +- .../speech_impl/voice_clone_tts_service.dart | 22 +- .../speech_impl/volcano_asr_api_service.dart | 38 +- .../speech_impl/volcano_tts_api_service.dart | 16 +- .../speech_impl/xunfei_asr_service.dart | 20 +- .../android_voice_interaction_service.dart | 18 +- .../ios_voice_interaction_service.dart | 12 +- .../agent/controllers/agent_controller.dart | 54 +- .../chat/controllers/chat_controller.dart | 10 +- .../meeting_details_controller.dart | 8 +- .../controllers/pairing_controller.dart | 14 +- .../controllers/speech_test_controller.dart | 18 +- .../controllers/translation_controller.dart | 26 +- .../translation_detail_controller.dart | 8 +- .../agent_service/AgentService.kt | 473 ++++++------- local_plugins/agent_service/pubspec.yaml | 12 - .../azure_speech/android/build.gradle.kts | 3 +- .../azure_speech/AzureSpeechPlugin.kt | 224 +++--- .../azure_speech/AzureTtsHelper.kt | 289 +++++--- .../bytedance_speech/android/build.gradle.kts | 26 +- .../bytedance_speech/BytedanceASR.kt | 2 +- .../bytedance_speech/BytedanceAudioPlayer.kt | 329 +++++++++ .../bytedance_speech/BytedanceSpeechPlugin.kt | 154 +++-- .../bytedance_speech/BytedanceTTS.kt | 650 ++++++++++++------ local_plugins/classic_bluetooth/pubspec.yaml | 2 - .../.gradle/8.10/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 +- local_plugins/speech/android/build.gradle.kts | 2 +- .../speech/android/settings.gradle.kts | 2 +- .../com/deep_voice/speech/ITtsService.kt | 10 +- .../kotlin/com/deep_voice/speech/TtsEvents.kt | 12 +- pubspec.yaml | 2 - 45 files changed, 1658 insertions(+), 1043 deletions(-) create mode 100644 local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 88678470b..412f86b15 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -35,7 +35,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20") // 其他依赖... } } diff --git a/lib/core/themes/theme_service.dart b/lib/core/themes/theme_service.dart index fd438f9d8..a70c3617c 100644 --- a/lib/core/themes/theme_service.dart +++ b/lib/core/themes/theme_service.dart @@ -21,7 +21,7 @@ class ThemeService { Logger.info('从存储读取主题设置: $value'); return value ?? false; } catch (e) { - Logger.error('读取主题设置失败: $e'); + Logger.error('读取主题设置失败: ${e.toString()}'); return false; } } @@ -32,7 +32,7 @@ class ThemeService { Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}'); await _box.write(_key, isDarkMode); } catch (e) { - Logger.error('保存主题设置失败: $e'); + Logger.error('保存主题设置失败: ${e.toString()}'); } } } \ No newline at end of file diff --git a/lib/core/utils/logger.dart b/lib/core/utils/logger.dart index 3c5a15470..eba8701f5 100644 --- a/lib/core/utils/logger.dart +++ b/lib/core/utils/logger.dart @@ -15,77 +15,84 @@ class Logger { /// 当前日志级别,低于此级别的日志不会被输出 static LogLevel _currentLevel = LogLevel.debug; + /// 默认标签 + static const String _defaultTag = 'App'; + /// 设置日志级别 static void setLevel(LogLevel level) { _currentLevel = level; } + /// 输出调试日志 (不带标签版本) + static void debug(String message) { + if (_currentLevel.index <= LogLevel.debug.index) { + _log('DEBUG', _defaultTag, message); + } + } + /// 输出调试日志 - static void debug(String message, {String? tag}) { + static void d(String tag, String message) { if (_currentLevel.index <= LogLevel.debug.index) { - _log('DEBUG', message, tag: tag); + _log('DEBUG', tag, message); + } + } + + /// 输出信息日志 (不带标签版本) + static void info(String message) { + if (_currentLevel.index <= LogLevel.info.index) { + _log('INFO', _defaultTag, message); } } /// 输出信息日志 - static void info(String message, {String? tag}) { + static void i(String tag, String message) { if (_currentLevel.index <= LogLevel.info.index) { - _log('INFO', message, tag: tag); + _log('INFO', tag, message); + } + } + + /// 输出警告日志 (不带标签版本) + static void warning(String message) { + if (_currentLevel.index <= LogLevel.warning.index) { + _log('WARNING', _defaultTag, message); } } /// 输出警告日志 - static void warning(String message, {String? tag}) { + static void w(String tag, String message) { if (_currentLevel.index <= LogLevel.warning.index) { - _log('WARNING', message, tag: tag); + _log('WARNING', tag, message); } } - /// 输出错误日志 - static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) { + /// 输出错误日志 (不带标签版本) + static void error(String message, [dynamic error, StackTrace? stackTrace]) { if (_currentLevel.index <= LogLevel.error.index) { - _log('ERROR', message, tag: tag); + _log('ERROR', _defaultTag, message); if (error != null) { - _log('ERROR', 'Original error: $error', tag: tag); + _log('ERROR', _defaultTag, 'Original error: $error'); } if (stackTrace != null) { - _log('ERROR', 'Stack trace: $stackTrace', tag: tag); + _log('ERROR', _defaultTag, 'Stack trace: $stackTrace'); } } } - /// 获取当前调用栈的类名 - static String? _getCallerClassName() { - try { - final stackTrace = StackTrace.current.toString(); - final frames = stackTrace.split('\n'); - - // 查找调用者帧 (跳过Logger框架内部的帧) - for (int i = 0; i < frames.length; i++) { - final frame = frames[i].trim(); - if (frame.contains('Logger.') || frame.contains('_log')) { - continue; - } - - // 简单的解析类名 - final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s'); - final match = regex.firstMatch(frame); - if (match != null && match.groupCount >= 1) { - return match.group(1); - } - break; + /// 输出错误日志 + static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) { + if (_currentLevel.index <= LogLevel.error.index) { + _log('ERROR', tag, message); + if (error != null) { + _log('ERROR', tag, 'Original error: $error'); + } + if (stackTrace != null) { + _log('ERROR', tag, 'Stack trace: $stackTrace'); } - } catch (e) { - // 忽略解析错误 } - return null; } /// 内部日志输出方法 - static void _log(String level, String message, {String? tag}) { - final timestamp = DateTime.now().toString(); - final className = tag ?? _getCallerClassName() ?? ''; - final tagInfo = className.isNotEmpty ? '[$className] ' : ''; - developer.log('[$timestamp][$level][$tagInfo]: $message'); + static void _log(String level, String tag, String message) { + developer.log('[$tag]: $message'); } } diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 3234879a0..0c69076df 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -198,7 +198,7 @@ class BleManager extends GetxService { }, ); } catch (e) { - Logger.error('设置BLE事件监听器失败: $e'); + Logger.error('设置BLE事件监听器失败: ${e.toString()}'); } } @@ -225,7 +225,7 @@ class BleManager extends GetxService { } } } catch (e) { - Logger.error('处理设备信息异常: $e'); + Logger.error('处理设备信息异常: ${e.toString()}'); } } @@ -303,10 +303,9 @@ class BleManager extends GetxService { caseCharging: caseCharging, ); - Logger.info( - '设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%'); + Logger.info('设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%'); } catch (e) { - Logger.error('更新电池信息失败: $e'); + Logger.error('更新电池信息失败: ${e.toString()}'); } } @@ -329,7 +328,7 @@ class BleManager extends GetxService { Logger.info('设备版本信息已更新: 版本=$version'); } catch (e) { - Logger.error('更新版本信息失败: $e'); + Logger.error('更新版本信息失败: ${e.toString()}'); } } @@ -352,7 +351,7 @@ class BleManager extends GetxService { Logger.info('设备设备id已更新: 设备id=$productId'); } catch (e) { - Logger.error('更新设备id失败: $e'); + Logger.error('更新设备id失败: ${e.toString()}'); } } @@ -440,7 +439,7 @@ class BleManager extends GetxService { Logger.warning('产品id获取指令发送失败'); } } catch (e) { - Logger.error('获取BLE设备信息失败: $e'); + Logger.error('获取BLE设备信息失败: ${e.toString()}'); } } @@ -455,7 +454,7 @@ class BleManager extends GetxService { return _connectionState.value; } catch (e) { - Logger.error('获取BLE连接状态失败: $e'); + Logger.error('获取BLE连接状态失败: ${e.toString()}'); return BleConnectionState.error; } } @@ -542,7 +541,7 @@ class BleManager extends GetxService { return result; } catch (e) { - Logger.error('启动BLE服务失败: $e'); + Logger.error('启动BLE服务失败: ${e.toString()}'); return false; } } @@ -562,7 +561,7 @@ class BleManager extends GetxService { return result; } catch (e) { - Logger.error('停止BLE服务失败: $e'); + Logger.error('停止BLE服务失败: ${e.toString()}'); return false; } } @@ -572,7 +571,7 @@ class BleManager extends GetxService { try { return await _bleService.getBatteryLevel(); } catch (e) { - Logger.error('获取电池电量失败: $e'); + Logger.error('获取电池电量失败: ${e.toString()}'); return 0; } } @@ -582,7 +581,7 @@ class BleManager extends GetxService { try { return await _bleService.getBatteryInfo(); } catch (e) { - Logger.error('获取电池信息失败: $e'); + Logger.error('获取电池信息失败: ${e.toString()}'); return false; } } @@ -592,7 +591,7 @@ class BleManager extends GetxService { try { return await _bleService.getProductId(); } catch (e) { - Logger.error('获取设备版本失败: $e'); + Logger.error('获取设备版本失败: ${e.toString()}'); return false; } } @@ -602,7 +601,7 @@ class BleManager extends GetxService { try { return await _bleService.getDeviceVersion(); } catch (e) { - Logger.error('获取设备版本失败: $e'); + Logger.error('获取设备版本失败: ${e.toString()}'); return false; } } @@ -612,7 +611,7 @@ class BleManager extends GetxService { try { return await _bleService.openEncoder(); } catch (e) { - Logger.error('打开编码器失败: $e'); + Logger.error('打开编码器失败: ${e.toString()}'); return false; } } @@ -622,7 +621,7 @@ class BleManager extends GetxService { try { return await _bleService.openDecoder(); } catch (e) { - Logger.error('打开解码器失败: $e'); + Logger.error('打开解码器失败: ${e.toString()}'); return false; } } @@ -632,7 +631,7 @@ class BleManager extends GetxService { try { return await _bleService.openA2DPDecoder(); } catch (e) { - Logger.error('打开解码器失败: $e'); + Logger.error('打开解码器失败: ${e.toString()}'); return false; } } @@ -642,7 +641,7 @@ class BleManager extends GetxService { try { return await _bleService.closeCodec(); } catch (e) { - Logger.error('关闭编解码器失败: $e'); + Logger.error('关闭编解码器失败: ${e.toString()}'); return false; } } @@ -652,7 +651,7 @@ class BleManager extends GetxService { try { return await _bleService.playA2DP(); } catch (e) { - Logger.error('播放A2DP失败: $e'); + Logger.error('播放A2DP失败: ${e.toString()}'); return false; } } @@ -664,4 +663,4 @@ class BleManager extends GetxService { } return Future.value(Get.find()); } -} +} \ No newline at end of file diff --git a/lib/data/services/bluetooth_manager.dart b/lib/data/services/bluetooth_manager.dart index e6e26ec02..c52a7f4b5 100644 --- a/lib/data/services/bluetooth_manager.dart +++ b/lib/data/services/bluetooth_manager.dart @@ -75,11 +75,11 @@ class BluetoothManager extends GetxService { } }, onError: (error) { - Logger.error('蓝牙事件流错误: $error'); + Logger.error('蓝牙事件流错误: ${error.toString()}'); }, ); } catch (e) { - Logger.error('设置蓝牙事件监听器失败: $e'); + Logger.error('设置蓝牙事件监听器失败: ${e.toString()}'); } } @@ -106,7 +106,7 @@ class BluetoothManager extends GetxService { Logger.info('未找到已连接的蓝牙设备'); } catch (e) { - Logger.error('刷新设备连接失败: $e'); + Logger.error('刷新设备连接失败: ${e.toString()}'); } } @@ -117,7 +117,7 @@ class BluetoothManager extends GetxService { _bluetoothState.value = isEnabled ? 'on' : 'off'; return isEnabled; } catch (e) { - Logger.error('检查蓝牙状态失败: $e'); + Logger.error('检查蓝牙状态失败: ${e.toString()}'); return false; } } diff --git a/lib/data/services/chat_history_service.dart b/lib/data/services/chat_history_service.dart index 8749c5714..9a1688314 100644 --- a/lib/data/services/chat_history_service.dart +++ b/lib/data/services/chat_history_service.dart @@ -39,7 +39,7 @@ class ChatHistoryService extends GetxService { // 检查并清理过多的历史记录 await _cleanupOldMessages(agentId); } catch (e) { - Logger.error('保存聊天记录失败', e); + Logger.error('保存聊天记录失败', e.toString()); } } diff --git a/lib/data/services/deapsound_ai_service.dart b/lib/data/services/deapsound_ai_service.dart index 186116ab4..c4667b97d 100644 --- a/lib/data/services/deapsound_ai_service.dart +++ b/lib/data/services/deapsound_ai_service.dart @@ -82,14 +82,14 @@ class DeapsoundAIService implements AiService { } } } catch (e) { - Logger.error('JSON解析错误: $e'); + Logger.error('JSON解析错误: ${e.toString()}'); } } } } catch (e) { - Logger.error('请求异常: $e'); + Logger.error('请求异常: ${e.toString()}'); } finally { client.close(); } } -} +} \ No newline at end of file diff --git a/lib/data/services/kouzi_ai_service.dart b/lib/data/services/kouzi_ai_service.dart index cf479ac56..c19f23d1a 100644 --- a/lib/data/services/kouzi_ai_service.dart +++ b/lib/data/services/kouzi_ai_service.dart @@ -82,14 +82,14 @@ class KouziAIService implements AiService { } } } catch (e) { - Logger.error('JSON解析错误: $e'); + Logger.error('JSON解析错误: ${e.toString()}'); } } } } catch (e) { - Logger.error('请求异常: $e'); + Logger.error('请求异常: ${e.toString()}'); } finally { client.close(); } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_factory.dart b/lib/data/services/speech_factory.dart index b1c7a1ec5..dd9a79c68 100644 --- a/lib/data/services/speech_factory.dart +++ b/lib/data/services/speech_factory.dart @@ -88,7 +88,7 @@ class SpeechFactory extends GetxService { Logger.info( '语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); } catch (e) { - Logger.error('语音服务初始化失败: $e'); + Logger.error('语音服务初始化失败: ${e.toString()}'); rethrow; } } @@ -108,4 +108,4 @@ class SpeechFactory extends GetxService { } return Get.find(); } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index 5c87577ee..fe0122717 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -106,7 +106,7 @@ class AzureAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); return result; } catch (e) { - Logger.error('Azure 语音识别服务初始化失败: $e'); + Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); _isInitialized = false; rethrow; } @@ -144,7 +144,7 @@ class AzureAsrService extends GetxService implements AsrService { throw Exception('无效的识别结果格式'); } catch (e) { - Logger.error('语音识别失败: $e'); + Logger.error('语音识别失败: ${e.toString()}'); return RecognitionEvent.error(e.toString()); } } @@ -177,7 +177,7 @@ class AzureAsrService extends GetxService implements AsrService { _isContinuousRecognitionActive = true; return _eventStreamController!.stream; } catch (e) { - Logger.error('开始连续语音识别失败: $e'); + Logger.error('开始连续语音识别失败: ${e.toString()}'); _cleanupEventStream(); rethrow; } @@ -195,7 +195,7 @@ class AzureAsrService extends GetxService implements AsrService { _cleanupEventStream(); return result; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); _cleanupEventStream(); rethrow; } @@ -214,7 +214,6 @@ class AzureAsrService extends GetxService implements AsrService { final String eventType = eventMap['type'] as String? ?? ''; // 添加日志帮助调试 - Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap'); switch (eventType) { case 'result': @@ -260,7 +259,7 @@ class AzureAsrService extends GetxService implements AsrService { final String errorDetails = eventMap['errorDetails'] as String? ?? ''; if (reason.isNotEmpty || errorDetails.isNotEmpty) { - Logger.error('识别取消: $reason - $errorDetails'); + Logger.error('识别取消: $reason - ${errorDetails.toString()}'); } _eventStreamController?.add(RecognitionEvent( @@ -271,7 +270,7 @@ class AzureAsrService extends GetxService implements AsrService { case 'error': final String error = eventMap['message'] as String? ?? ''; - Logger.error('识别错误: $error'); + Logger.error('识别错误: ${error.toString()}'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.error, error: error, @@ -282,7 +281,7 @@ class AzureAsrService extends GetxService implements AsrService { /// 处理识别事件流错误 void _handleRecognitionError(Object error) { - Logger.error('识别事件流错误: $error'); + Logger.error('识别事件流错误: ${error.toString()}'); _eventStreamController?.addError(error); _cleanupEventStream(); } @@ -311,7 +310,7 @@ class AzureAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别资源已释放'); } catch (e) { - Logger.error('释放语音识别资源失败: $e'); + Logger.error('释放语音识别资源失败: ${e.toString()}'); _cleanupEventStream(); _isInitialized = false; } @@ -339,8 +338,8 @@ class AzureAsrService extends GetxService implements AsrService { return true; } catch (e) { - Logger.error('推送音频数据失败: $e'); + Logger.error('推送音频数据失败: ${e.toString()}'); return false; } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index 2e24a90b8..d6eccdf9b 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -69,7 +69,7 @@ class AzureTtsService extends GetxService implements TtsService { _handleTtsEvent(event); } }, onError: (error) { - Logger.error('TTS事件流错误: $error'); + Logger.error('TTS事件流错误: ${error.toString()}'); _eventStreamController.addError(error); }); } @@ -78,7 +78,7 @@ class AzureTtsService extends GetxService implements TtsService { void _handleTtsEvent(Map event) { final String eventType = event['type'] as String? ?? ''; - // Logger.debug('收到TTS事件: $eventType, 数据: $event'); + // Logger.debug('收到TTS事件: ${e.toString()}ventType, 数据: ${e.toString()}vent'); switch (eventType) { case 'synthesis_started': @@ -159,7 +159,7 @@ class AzureTtsService extends GetxService implements TtsService { _isInitialized = result; return result; } catch (e) { - Logger.error('初始化失败: $e'); + Logger.error('初始化失败: ${e.toString()}'); return false; } } @@ -177,7 +177,7 @@ class AzureTtsService extends GetxService implements TtsService { if (result) _currentVoice = voiceName; return result; } catch (e) { - Logger.error('设置语音失败: $e'); + Logger.error('设置语音失败: ${e.toString()}'); return false; } } @@ -195,7 +195,7 @@ class AzureTtsService extends GetxService implements TtsService { return result == true; } catch (e) { - Logger.error('语音合成失败: $e'); + Logger.error('语音合成失败: ${e.toString()}'); return false; } } @@ -250,7 +250,7 @@ class AzureTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('处理流式文本失败: $e'); + Logger.error('处理流式文本失败: ${e.toString()}'); return false; } } @@ -316,7 +316,7 @@ class AzureTtsService extends GetxService implements TtsService { Logger.info('TTS队列播放完成'); return result; } catch (e) { - Logger.error('刷新TTS流失败: $e'); + Logger.error('刷新TTS流失败: ${e.toString()}'); return false; } } @@ -358,7 +358,7 @@ class AzureTtsService extends GetxService implements TtsService { _textQueue.removeAt(0); } } catch (e) { - Logger.error('处理语音队列出错: $e'); + Logger.error('处理语音队列出错: ${e.toString()}'); // 出错时移除当前项并尝试继续 if (_textQueue.isNotEmpty) { @@ -389,7 +389,7 @@ class AzureTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('停止语音合成出错: $e'); + Logger.error('停止语音合成出错: ${e.toString()}'); return false; } } @@ -410,7 +410,7 @@ class AzureTtsService extends GetxService implements TtsService { await _channel.invokeMethod('dispose'); _isInitialized = false; } catch (e) { - Logger.error('释放资源失败: $e'); + Logger.error('释放资源失败: ${e.toString()}'); } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/flutter_asr_service.dart b/lib/data/services/speech_impl/flutter_asr_service.dart index 438953af8..c8716ad8f 100644 --- a/lib/data/services/speech_impl/flutter_asr_service.dart +++ b/lib/data/services/speech_impl/flutter_asr_service.dart @@ -107,7 +107,7 @@ class FlutterAsrService extends GetxService implements AsrService { } catch (e) { _isAvailable.value = false; _asrState.value = AsrState.error; - Logger.error('语音识别初始化失败: $e'); + Logger.error('语音识别初始化失败: ${e.toString()}'); } } @@ -203,7 +203,7 @@ class FlutterAsrService extends GetxService implements AsrService { return result; } catch (e) { - Logger.error('启动语音识别失败: $e'); + Logger.error('启动语音识别失败: ${e.toString()}'); _asrState.value = AsrState.error; return false; } @@ -253,7 +253,7 @@ class FlutterAsrService extends GetxService implements AsrService { ?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')); } } catch (e) { - Logger.error('重启连续识别失败: $e'); + Logger.error('重启连续识别失败: ${e.toString()}'); // 发送错误给控制器 _continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e')); @@ -304,7 +304,7 @@ class FlutterAsrService extends GetxService implements AsrService { await _initSpeech(); return _isAvailable.value; } catch (e) { - Logger.error('语音识别初始化失败: $e'); + Logger.error('语音识别初始化失败: ${e.toString()}'); return false; } } @@ -357,7 +357,7 @@ class FlutterAsrService extends GetxService implements AsrService { }, ); } catch (e) { - Logger.error('一次性语音识别失败: $e'); + Logger.error('一次性语音识别失败: ${e.toString()}'); return RecognitionEvent.error('$e'); } } @@ -406,7 +406,7 @@ class FlutterAsrService extends GetxService implements AsrService { _isListening = true; return _continuousRecognitionController!.stream; } catch (e) { - Logger.error('启动连续语音识别失败: $e'); + Logger.error('启动连续语音识别失败: ${e.toString()}'); // 如果已经创建了控制器,使用它 if (_continuousRecognitionController != null) { @@ -442,7 +442,7 @@ class FlutterAsrService extends GetxService implements AsrService { _asrState.value = AsrState.done; return true; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); // 强制重置状态 _isListening = false; @@ -472,7 +472,7 @@ class FlutterAsrService extends GetxService implements AsrService { _isListening = false; _asrState.value = AsrState.notInitialized; } catch (e) { - Logger.error('释放语音识别资源失败: $e'); + Logger.error('释放语音识别资源失败: ${e.toString()}'); } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/flutter_tts_service.dart b/lib/data/services/speech_impl/flutter_tts_service.dart index 810f82787..aca6fca66 100644 --- a/lib/data/services/speech_impl/flutter_tts_service.dart +++ b/lib/data/services/speech_impl/flutter_tts_service.dart @@ -96,7 +96,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('FlutterTtsService 已初始化'); } catch (e) { - Logger.error('TTS初始化失败: $e'); + Logger.error('TTS初始化失败: ${e.toString()}'); } } @@ -118,7 +118,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('支持的语音: ${_supportedVoices.join(", ")}'); return true; } catch (e) { - Logger.error('TTS初始化失败: $e'); + Logger.error('TTS初始化失败: ${e.toString()}'); return false; } } @@ -149,7 +149,7 @@ class FlutterTtsService extends GetxService implements TtsService { _currentVoice.value = _supportedVoices.first; } } catch (e) { - Logger.error('加载TTS语音失败: $e'); + Logger.error('加载TTS语音失败: ${e.toString()}'); // 添加默认语音 if (_supportedVoices.isEmpty) { @@ -175,7 +175,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('设置TTS语音: $voiceName'); return true; } catch (e) { - Logger.error('设置TTS语音失败: $e'); + Logger.error('设置TTS语音失败: ${e.toString()}'); return false; } } @@ -200,7 +200,7 @@ class FlutterTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('设置语音参数失败: $e'); + Logger.error('设置语音参数失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('设置语音参数失败: $e')); @@ -227,7 +227,7 @@ class FlutterTtsService extends GetxService implements TtsService { // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); - Logger.error('TTS播放失败: $e'); + Logger.error('TTS播放失败: ${e.toString()}'); return false; } } @@ -288,7 +288,7 @@ class FlutterTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('处理流式文本失败: $e'); + Logger.error('处理流式文本失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); @@ -333,7 +333,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('TTS队列播放完成'); return true; } catch (e) { - Logger.error('等待TTS队列播放完成失败: $e'); + Logger.error('等待TTS队列播放完成失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); @@ -382,7 +382,7 @@ class FlutterTtsService extends GetxService implements TtsService { } } } catch (e) { - Logger.error('TTS处理队列失败: $e'); + Logger.error('TTS处理队列失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error(e.toString())); @@ -422,7 +422,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('TTS停止播放完成'); return true; } catch (e) { - Logger.error('TTS停止播放失败: $e'); + Logger.error('TTS停止播放失败: ${e.toString()}'); // 即使出错也尝试强制重置状态 _isSpeaking.value = false; @@ -443,7 +443,7 @@ class FlutterTtsService extends GetxService implements TtsService { Logger.info('TTS资源已释放'); } catch (e) { - Logger.error('释放TTS资源失败: $e'); + Logger.error('释放TTS资源失败: ${e.toString()}'); } } @@ -462,7 +462,7 @@ class FlutterTtsService extends GetxService implements TtsService { await flushStream(); return true; } catch (e) { - Logger.error('处理文本流时出错: $e'); + Logger.error('处理文本流时出错: ${e.toString()}'); return false; } } diff --git a/lib/data/services/speech_impl/voice_clone_tts_service.dart b/lib/data/services/speech_impl/voice_clone_tts_service.dart index 2f57939a9..43079325f 100644 --- a/lib/data/services/speech_impl/voice_clone_tts_service.dart +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -76,7 +76,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { ); return true; } catch (e) { - Logger.error('TTS初始化失败: $e'); + Logger.error('TTS初始化失败: ${e.toString()}'); return false; } } @@ -88,7 +88,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { Logger.info('设置TTS语音: $voiceName'); return true; } catch (e) { - Logger.error('设置TTS语音失败: $e'); + Logger.error('设置TTS语音失败: ${e.toString()}'); return false; } } @@ -127,7 +127,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { _handle(message); }, onError: (error) { - Logger.error('连接失败: $error'); + Logger.error('连接失败: ${error.toString()}'); }, onDone: () { Logger.error('WebSocket 连接已关闭!'); @@ -135,7 +135,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { cancelOnError: true, ); } catch (e) { - Logger.error('初始化异常: $e'); + Logger.error('初始化异常: ${e.toString()}'); } } @@ -226,7 +226,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { return true; } catch (e) { _isSpeaking.value = false; - Logger.error('语音合成失败: $e'); + Logger.error('语音合成失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('语音合成失败: $e')); @@ -285,7 +285,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('处理流式文本失败: $e'); + Logger.error('处理流式文本失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('处理流式文本失败: $e')); @@ -319,7 +319,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { return true; } catch (e) { - Logger.error('刷新TTS流失败: $e'); + Logger.error('刷新TTS流失败: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('刷新TTS流失败: $e')); @@ -347,7 +347,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { await speakOnce(item); } } catch (e) { - Logger.error('处理语音队列出错: $e'); + Logger.error('处理语音队列出错: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('处理语音队列出错: $e')); @@ -370,7 +370,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } return true; } catch (e) { - Logger.error('停止语音合成出错: $e'); + Logger.error('停止语音合成出错: ${e.toString()}'); // 发送错误事件 _eventController.add(TtsEvent.error('停止语音合成出错: $e')); return false; @@ -387,7 +387,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { await _eventController.close(); _isInitialized = false; } catch (e) { - Logger.error('释放资源失败: $e'); + Logger.error('释放资源失败: ${e.toString()}'); } } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index 3ffc4e75e..26f18e1c5 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -99,7 +99,7 @@ class VolcanoAsrApiService implements AsrService { Logger.info('火山语音API已初始化'); return true; } catch (e) { - Logger.error('初始化火山语音API失败: $e'); + Logger.error('初始化火山语音API失败: ${e.toString()}'); return false; } } @@ -151,7 +151,7 @@ class VolcanoAsrApiService implements AsrService { return resultCompleter.future; } catch (e) { - Logger.error('单次语音识别失败: $e'); + Logger.error('单次语音识别失败: ${e.toString()}'); return RecognitionEvent.error(e.toString()); } } @@ -184,7 +184,7 @@ class VolcanoAsrApiService implements AsrService { return _recognitionController!.stream; } catch (e) { - Logger.error('开始连续语音识别失败: $e'); + Logger.error('开始连续语音识别失败: ${e.toString()}'); await _cleanupResources(); // 返回一个包含错误的流 @@ -222,7 +222,7 @@ class VolcanoAsrApiService implements AsrService { return true; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); // 确保资源被清理 await _cleanupResources(); @@ -250,7 +250,7 @@ class VolcanoAsrApiService implements AsrService { _isInitialized = false; Logger.info('火山语音API资源已释放'); } catch (e) { - Logger.error('释放火山语音API资源失败: $e'); + Logger.error('释放火山语音API资源失败: ${e.toString()}'); _isContinuousRecognitionActive = false; _isInitialized = false; } @@ -284,7 +284,7 @@ class VolcanoAsrApiService implements AsrService { Logger.info('录音机初始化成功'); } catch (e) { // _recorder = null; - Logger.error('初始化录音机失败: $e'); + Logger.error('初始化录音机失败: ${e.toString()}'); rethrow; } } @@ -344,7 +344,7 @@ class VolcanoAsrApiService implements AsrService { _connectionCompleter!.completeError(e); } - Logger.error('连接WebSocket失败: $e'); + Logger.error('连接WebSocket失败: ${e.toString()}'); rethrow; } @@ -432,7 +432,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(heartbeatMsg)); } catch (e) { - Logger.error('发送心跳包失败: $e'); + Logger.error('发送心跳包失败: ${e.toString()}'); } } } @@ -461,7 +461,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(startCommand)); Logger.info('已发送开始识别指令'); } catch (e) { - Logger.error('发送开始识别指令失败: $e'); + Logger.error('发送开始识别指令失败: ${e.toString()}'); rethrow; } } @@ -496,7 +496,7 @@ class VolcanoAsrApiService implements AsrService { Logger.info('录音已开始'); } catch (e) { _isRecording = false; - Logger.error('开始录音失败: $e'); + Logger.error('开始录音失败: ${e.toString()}'); rethrow; } */ @@ -544,7 +544,7 @@ class VolcanoAsrApiService implements AsrService { await _sendAudioData(audioData); } } catch (e) { - Logger.error('捕获和发送音频失败: $e'); + Logger.error('捕获和发送音频失败: ${e.toString()}'); } */ } @@ -562,7 +562,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(audioMessage)); } catch (e) { - Logger.error('发送音频数据失败: $e'); + Logger.error('发送音频数据失败: ${e.toString()}'); } } @@ -579,7 +579,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.add(jsonEncode(endCommand)); Logger.info('已发送结束指令'); } catch (e) { - Logger.error('发送结束指令失败: $e'); + Logger.error('发送结束指令失败: ${e.toString()}'); } } @@ -596,7 +596,7 @@ class VolcanoAsrApiService implements AsrService { _isRecording = false; Logger.info('录音已停止'); } catch (e) { - Logger.error('停止录音失败: $e'); + Logger.error('停止录音失败: ${e.toString()}'); } } */ @@ -641,7 +641,7 @@ class VolcanoAsrApiService implements AsrService { } } } catch (e) { - Logger.error('处理WebSocket消息失败: $e'); + Logger.error('处理WebSocket消息失败: ${e.toString()}'); if (_recognitionController != null && !_recognitionController!.isClosed) { _recognitionController!.addError('处理消息失败: $e'); @@ -675,7 +675,7 @@ class VolcanoAsrApiService implements AsrService { } } } catch (e) { - Logger.error('处理结果消息失败: $e'); + Logger.error('处理结果消息失败: ${e.toString()}'); } } @@ -741,7 +741,7 @@ class VolcanoAsrApiService implements AsrService { /// 处理WebSocket错误 void _handleWebSocketError(dynamic error) { - Logger.error('WebSocket错误: $error'); + Logger.error('WebSocket错误: ${e.toString()}rror'); if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { _connectionCompleter!.completeError(error); @@ -789,7 +789,7 @@ class VolcanoAsrApiService implements AsrService { _channel!.sink.close(); _channel = null; } catch (e) { - Logger.error('关闭WebSocket连接失败: $e'); + Logger.error('关闭WebSocket连接失败: ${e.toString()}'); } } } @@ -811,4 +811,4 @@ class VolcanoAsrApiService implements AsrService { _isContinuousRecognitionActive = false; _isRecognitionEndRequested = false; } -} +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/volcano_tts_api_service.dart b/lib/data/services/speech_impl/volcano_tts_api_service.dart index e01e253ed..8a3196c03 100644 --- a/lib/data/services/speech_impl/volcano_tts_api_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_api_service.dart @@ -357,7 +357,7 @@ class VolcanoTtsApiService extends GetxController { _handleMessage, onError: (error) { // 直接处理错误,不尝试重连 - Logger.error('WebSocket错误: $error'); + Logger.error('WebSocket错误: ${error.toString()}'); if (!_eventController.isClosed) { _eventController.add({ 'eventType': 'error', @@ -391,7 +391,7 @@ class VolcanoTtsApiService extends GetxController { }); } } catch (e) { - Logger.error('处理TTS连接超时事件失败: $e'); + Logger.error('处理TTS连接超时事件失败: ${e.toString()}'); } } timeout = null; @@ -427,7 +427,7 @@ class VolcanoTtsApiService extends GetxController { return result; } catch (e) { // 只记录错误,不尝试重连 - Logger.error('连接过程中发生异常: $e'); + Logger.error('连接过程中发生异常: ${e.toString()}'); return false; } finally { isConnecting.value = false; @@ -479,7 +479,7 @@ class VolcanoTtsApiService extends GetxController { 'error': '会话开始超时' }); } catch (e) { - Logger.error('处理TTS会话超时事件失败: $e'); + Logger.error('处理TTS会话超时事件失败: ${e.toString()}'); } } timeout = null; @@ -515,7 +515,7 @@ class VolcanoTtsApiService extends GetxController { return result; } catch (e) { // 只记录错误,不尝试恢复 - Logger.error('开始TTS会话失败: $e'); + Logger.error('开始TTS会话失败: ${e.toString()}'); return false; } } @@ -902,10 +902,10 @@ class VolcanoTtsApiService extends GetxController { 'error': error.toString() }); - Logger.error('火山语音TTS错误: $error'); + Logger.error('火山语音TTS错误: ${error.toString()}'); } catch (e) { // 如果在处理错误时发生异常,记录到日志但不再抛出异常 - Logger.error('处理TTS错误时发生异常: $e,原始错误: $error'); + Logger.error('处理TTS错误时发生异常: ${e.toString()},原始错误: ${error.toString()}'); } } @@ -1445,7 +1445,7 @@ class VolcanoTtsApiService extends GetxController { return success; } catch (e) { - Logger.error('TTS一次性合成失败: $e'); + Logger.error('TTS一次性合成失败: ${e.toString()}'); // 确保会话结束 endSession(); return false; diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index ef6791e3f..00b151019 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -54,7 +54,7 @@ class XunfeiAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别服务初始化成功'); return true; } catch (e) { - Logger.error('Azure 语音识别服务初始化失败: $e'); + Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); _isInitialized = false; rethrow; } @@ -69,7 +69,7 @@ class XunfeiAsrService extends GetxService implements AsrService { try { throw Exception('无效的识别结果格式'); } catch (e) { - Logger.error('语音识别失败: $e'); + Logger.error('语音识别失败: ${e.toString()}'); return RecognitionEvent.error(e.toString()); } } @@ -112,7 +112,7 @@ class XunfeiAsrService extends GetxService implements AsrService { _isContinuousRecognitionActive = true; return _eventStreamController!.stream; } catch (e) { - Logger.error('开始连续语音识别失败: $e'); + Logger.error('开始连续语音识别失败: ${e.toString()}'); rethrow; } } @@ -152,7 +152,7 @@ class XunfeiAsrService extends GetxService implements AsrService { }, onError: (error) { isConnected = false; - Logger.error('连接失败: $error'); + Logger.error('连接失败: ${error.toString()}'); }, onDone: () { isConnected = false; @@ -161,7 +161,7 @@ class XunfeiAsrService extends GetxService implements AsrService { cancelOnError: true, ); } catch (e) { - Logger.error("初始化异常----------------$e"); + Logger.error("初始化异常----------------${e.toString()}"); } } @@ -185,7 +185,7 @@ class XunfeiAsrService extends GetxService implements AsrService { } } } - Logger.info( + Logger.info( "识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role"); if (msg.isNotEmpty) { if (data['cn']['st']['type'] == "0") { @@ -206,7 +206,7 @@ class XunfeiAsrService extends GetxService implements AsrService { } } } catch (e) { - Logger.error("接受消息异常 $e"); + Logger.error("接受消息异常 ${e.toString()}"); } } @@ -242,7 +242,7 @@ class XunfeiAsrService extends GetxService implements AsrService { _isContinuousRecognitionActive = false; return true; } catch (e) { - Logger.error('停止连续语音识别失败: $e'); + Logger.error('停止连续语音识别失败: ${e.toString()}'); rethrow; } } @@ -266,8 +266,8 @@ class XunfeiAsrService extends GetxService implements AsrService { Logger.info('Azure 语音识别资源已释放'); } catch (e) { - Logger.error('释放语音识别资源失败: $e'); + Logger.error('释放语音识别资源失败: ${e.toString()}'); _isInitialized = false; } } -} +} \ No newline at end of file diff --git a/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart index f4771ffac..5dd9f96e7 100644 --- a/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart @@ -76,7 +76,7 @@ class AndroidVoiceInteractionService extends GetxService { _handleVoiceInteractionEvent(event); } }, onError: (error) { - Logger.error('语音交互事件通道错误: $error'); + Logger.error('语音交互事件通道错误: ${error.toString()}'); }); } @@ -106,7 +106,7 @@ class AndroidVoiceInteractionService extends GetxService { Logger.info('语音交互服务初始化完成'); return this; } catch (e) { - Logger.error('语音交互服务初始化失败: $e'); + Logger.error('语音交互服务初始化失败: ${e.toString()}'); return this; } } @@ -123,7 +123,7 @@ class AndroidVoiceInteractionService extends GetxService { _isServiceRunning.value = isRunning; Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); } catch (e) { - Logger.error('检查语音交互服务状态失败: $e'); + Logger.error('检查语音交互服务状态失败: ${e.toString()}'); _isServiceRunning.value = false; } } @@ -151,7 +151,7 @@ class AndroidVoiceInteractionService extends GetxService { return result; } catch (e) { - Logger.error('启动语音交互服务失败: $e'); + Logger.error('启动语音交互服务失败: ${e.toString()}'); return false; } } @@ -175,7 +175,7 @@ class AndroidVoiceInteractionService extends GetxService { return result; } catch (e) { - Logger.error('停止语音交互服务失败: $e'); + Logger.error('停止语音交互服务失败: ${e.toString()}'); return false; } } @@ -199,7 +199,7 @@ class AndroidVoiceInteractionService extends GetxService { return result; } catch (e) { - Logger.error('暂停语音交互失败: $e'); + Logger.error('暂停语音交互失败: ${e.toString()}'); return false; } } @@ -213,7 +213,7 @@ class AndroidVoiceInteractionService extends GetxService { final int timestamp = eventMap['timestamp'] as int? ?? 0; // 添加时间戳日志,帮助调试 - Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); + Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); switch (eventType) { case 'chatHistory': @@ -252,7 +252,7 @@ class AndroidVoiceInteractionService extends GetxService { break; default: - Logger.warning('收到未知类型的语音交互事件: $eventType'); + Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}'); break; } } @@ -297,7 +297,7 @@ class AndroidVoiceInteractionService extends GetxService { // 保存更新后的历史记录 await chatHistoryService.saveHistory(agentId, existingMessages); } catch (e) { - Logger.error('保存聊天记录失败: $e'); + Logger.error('保存聊天记录失败: ${e.toString()}'); } } diff --git a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart index 46dbb2391..1fc80ba89 100644 --- a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart @@ -66,7 +66,7 @@ class IosVoiceInteractionService extends GetxService { // 保持idle状态,不自动启动语音识别 Logger.info('iOS语音交互服务初始化完成,处于idle状态'); } catch (e) { - Logger.error('iOS语音交互服务初始化失败: $e'); + Logger.error('iOS语音交互服务初始化失败: ${e.toString()}'); } return this; @@ -110,13 +110,13 @@ class IosVoiceInteractionService extends GetxService { // 监听识别结果 _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent, onError: (error) { - Logger.error('语音识别错误: $error'); + Logger.error('语音识别错误: ${error.toString()}'); _startContinuousListening(); // 尝试重新启动 }); Logger.info('开始连续语音识别'); } catch (e) { - Logger.error('启动语音识别失败: $e'); + Logger.error('启动语音识别失败: ${e.toString()}'); } } @@ -263,7 +263,7 @@ class IosVoiceInteractionService extends GetxService { // 处理完成 isProcessingAI.value = false; } catch (e) { - Logger.error('AI响应处理失败: $e'); + Logger.error('AI响应处理失败: ${e.toString()}'); // 错误恢复 isProcessingAI.value = false; } @@ -437,7 +437,7 @@ class IosVoiceInteractionService extends GetxService { Logger.info('保存聊天历史记录成功'); } catch (e) { - Logger.error('保存聊天历史记录失败: $e'); + Logger.error('保存聊天历史记录失败: ${e.toString()}'); } } @@ -463,4 +463,4 @@ class IosVoiceInteractionService extends GetxService { _eventStreamController.add(event); Logger.info('已发送识别开始事件'); } -} +} \ No newline at end of file diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 905359208..8a2532859 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -5,12 +5,12 @@ import 'package:deep_voice/data/services/music_manager.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:agent_service/agent_service.dart'; -import 'package:logger/logger.dart'; import 'package:chat_storage/chat_storage.dart'; import 'package:image_picker/image_picker.dart'; import 'package:get_storage/get_storage.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:app_settings/app_settings.dart'; +import 'package:deep_voice/core/utils/logger.dart'; class Message { final bool isUser; @@ -35,8 +35,6 @@ class Message { class AgentController extends GetxController { static AgentController get to => Get.find(); - final logger = Logger(); - // 聊天消息列表 final messages = [].obs; @@ -77,6 +75,8 @@ class AgentController extends GetxController { bool _isNewAssistantResponse = true; String _currentResponseId = ''; + final TAG = 'AgentController'; + // 会话ID final String sessionId = 'default_agent'; @@ -108,7 +108,7 @@ class AgentController extends GetxController { isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; } catch (e) { // 如果读取失败,使用默认值 - logger.e('加载TTS偏好设置失败: $e'); + Logger.e(TAG, '加载TTS偏好设置失败: $e'); isTtsEnabled.value = true; } } @@ -127,14 +127,14 @@ class AgentController extends GetxController { final storage = GetStorage(); await storage.write('agent_tts_enabled', isTtsEnabled.value); } catch (e) { - logger.e('保存TTS偏好设置失败: $e'); + Logger.e(TAG, '保存TTS偏好设置失败: $e'); } } // 加载聊天历史记录 Future _loadChatHistory() async { try { - logger.i('正在加载聊天历史...'); + Logger.i(TAG, '正在加载聊天历史...'); final result = await ChatStorage.getMessages( sessionId: sessionId, page: 1, @@ -159,14 +159,14 @@ class AgentController extends GetxController { historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); messages.assignAll(historyMessages); - logger.i('成功加载 ${historyMessages.length} 条聊天记录'); + Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录'); } else { // 没有历史记录时添加欢迎消息 _addWelcomeMessage(); - logger.i('没有历史记录,添加欢迎消息'); + Logger.i(TAG, '没有历史记录,添加欢迎消息'); } } catch (e) { - logger.e('加载聊天历史失败: $e'); + Logger.e(TAG, '加载聊天历史失败: $e'); _addWelcomeMessage(); } finally { isHistoryLoaded.value = true; @@ -271,12 +271,14 @@ class AgentController extends GetxController { case AgentServiceEventType.ttsStarted: isSpeaking.value = true; + Logger.i(TAG, 'TTS开始播放'); break; case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsCanceled: isSpeaking.value = false; + Logger.i(TAG, 'TTS停止播放'); break; case AgentServiceEventType.imageProcessing: @@ -338,7 +340,7 @@ class AgentController extends GetxController { messages.refresh(); } - logger.e('代理服务错误: ${event.data['message']}'); + Logger.e(TAG, '代理服务错误: ${event.data['message']}'); break; case AgentServiceEventType.autoStop: @@ -372,14 +374,14 @@ class AgentController extends GetxController { case AgentServiceEventType.functionCall: final functionName = event.data['name'] ?? ''; final arguments = event.data['arguments'] ?? ''; - // logger.i('Fultter 开始调用 mcp: $functionName, $arguments'); + // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments'); break; case AgentServiceEventType.functionCallResult: -final responseId = event.data['responseId'] ?? ''; + final responseId = event.data['responseId'] ?? ''; final result = event.data['result'] ?? ''; final functionCall = event.data['function_call'] ?? ''; - logger.i('Flutter 结束调用 mcp: $result, $functionCall'); + Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall'); if (result.isNotEmpty) { final parsedResult = jsonDecode(result); final meta = parsedResult["meta"]; @@ -389,7 +391,7 @@ final responseId = event.data['responseId'] ?? ''; try { metaResult = jsonDecode(meta); } catch (e) { - print('解析meta数据失败: $e'); + Logger.e(TAG, '解析meta数据失败: $e'); } } if(metaResult.isNotEmpty){ @@ -451,7 +453,7 @@ final responseId = event.data['responseId'] ?? ''; await AgentService.processTextInput(text, speakResponse: isTtsEnabled.value); } catch (e) { - logger.e('发送文本消息失败: $e'); + Logger.e(TAG, '发送文本消息失败: $e'); isProcessing.value = false; } } @@ -459,7 +461,7 @@ final responseId = event.data['responseId'] ?? ''; // 发送图片消息 Future sendImageMessage(String imagePath, {String? text}) async { if (imagePath.isEmpty || !File(imagePath).existsSync()) { - logger.e('图片不存在: $imagePath'); + Logger.e(TAG, '图片不存在: $imagePath'); return; } @@ -493,7 +495,7 @@ final responseId = event.data['responseId'] ?? ''; speakResponse: isTtsEnabled.value, ); } catch (e) { - logger.e('处理图片失败: $e'); + Logger.e(TAG, '处理图片失败: $e'); isProcessing.value = false; isImageProcessing.value = false; } @@ -554,7 +556,7 @@ final responseId = event.data['responseId'] ?? ''; if (!status.isGranted) { allGranted = false; deniedPermission = permission; - logger.e('权限 ${permission.toString()} 未被授予: $status'); + Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status'); } }); @@ -579,7 +581,7 @@ final responseId = event.data['responseId'] ?? ''; return false; } catch (e) { - logger.e('检查麦克风权限失败: $e'); + Logger.e(TAG, '检查麦克风权限失败: $e'); return false; } } @@ -592,7 +594,7 @@ final responseId = event.data['responseId'] ?? ''; // 在实际应用中,你可能需要使用platform channel获取真实的SDK版本 return 31; // 对应Android 12 } catch (e) { - logger.e('获取Android SDK版本失败: $e'); + Logger.e(TAG, '获取Android SDK版本失败: $e'); } } return 0; @@ -641,7 +643,7 @@ final responseId = event.data['responseId'] ?? ''; try { await AgentService.startConversation(); } catch (e) { - logger.e('启动语音输入失败: $e'); + Logger.e(TAG, '启动语音输入失败: $e'); } } @@ -652,7 +654,7 @@ final responseId = event.data['responseId'] ?? ''; try { await AgentService.stopConversation(); } catch (e) { - logger.e('停止语音输入失败: $e'); + Logger.e(TAG, '停止语音输入失败: $e'); } } @@ -670,7 +672,7 @@ final responseId = event.data['responseId'] ?? ''; // 重置回复状态 _isNewAssistantResponse = true; } catch (e) { - logger.e('清除聊天历史失败: $e'); + Logger.e(TAG, '清除聊天历史失败: $e'); } } @@ -692,7 +694,7 @@ final responseId = event.data['responseId'] ?? ''; isImageInputActive.value = true; } } catch (e) { - logger.e('选择图片失败: $e'); + Logger.e(TAG, '选择图片失败: $e'); } } @@ -714,7 +716,7 @@ final responseId = event.data['responseId'] ?? ''; isImageInputActive.value = true; } } catch (e) { - logger.e('拍照失败: $e'); + Logger.e(TAG, '拍照失败: $e'); } } @@ -756,7 +758,7 @@ final responseId = event.data['responseId'] ?? ''; // 播放音乐 void musicStatus() async { var status = await _musiceManager.getStatus(); - // logger.i('获取当前音乐播放状态 $status'); + // debugPrint('获取当前音乐播放状态 $status'); isPlaying.value = status['isPlaying']; currentUrl.value = status['url']; } diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 2f7fd5f70..62f6d0df0 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -451,7 +451,7 @@ class ChatController extends GetxController { } isPlay.value = true; } catch (e) { - Logger.error('加载异常: $e'); + Logger.error('加载异常: ${e.toString()}'); } } @@ -469,7 +469,7 @@ class ChatController extends GetxController { isPlay.value = true; audiourl.value = card['url'] as String; } catch (e) { - Logger.error('加载异常: $e'); + Logger.error('加载异常: ${e.toString()}'); } } @@ -479,7 +479,7 @@ class ChatController extends GetxController { _spotifService.pause(); isPlay.value = false; } catch (e) { - Logger.error('加载异常: $e'); + Logger.error('加载异常: ${e.toString()}'); } } @@ -872,7 +872,7 @@ class ChatController extends GetxController { } // 删除数据库中的历史记录 ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) { - Logger.error('清空数据库聊天记录失败: $e'); + Logger.error('清空数据库聊天记录失败: ${e.toString()}'); }); } @@ -1003,4 +1003,4 @@ class ChatController extends GetxController { .abs() < 5000); } -} +} \ No newline at end of file diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index b906902ed..9b8882d96 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -275,7 +275,7 @@ class MeetingDetailsController extends GetxController Logger.debug('taskId: $taskId'); return taskId; } catch (e) { - Logger.error('Submit error: $e'); + Logger.error('Submit error: ${e.toString()}'); rethrow; } } @@ -307,7 +307,7 @@ class MeetingDetailsController extends GetxController final respDic = jsonDecode(response.body); return respDic; } catch (e) { - Logger.error('Query error: $e'); + Logger.error('Query error: ${e.toString()}'); rethrow; } } @@ -339,7 +339,7 @@ class MeetingDetailsController extends GetxController } } } catch (e) { - Logger.error('Error occurred: $e'); + Logger.error('Error occurred: ${e.toString()}'); } } @@ -371,4 +371,4 @@ class MeetingDetailsController extends GetxController } allText = text; } -} +} \ No newline at end of file diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 7a65da691..d15f0da8e 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -96,7 +96,7 @@ class PairingController extends GetxController { // 检查蓝牙状态并刷新设备 await _checkBluetoothAndRefresh(); } catch (e) { - Logger.error('初始化配对控制器失败: $e'); + Logger.error('初始化配对控制器失败: ${e.toString()}'); statusDescription.value = 'initializationFailure'.tr; } finally { isLoading.value = false; @@ -170,7 +170,7 @@ class PairingController extends GetxController { await _bleService.registerBackgroundScan(); _autoNavigateToHome(); } catch (e) { - Logger.error('检查设备配对状态失败: $e'); + Logger.error('检查设备配对状态失败: ${e.toString()}'); } } @@ -356,7 +356,7 @@ class PairingController extends GetxController { _updateStatusDescription(); } } catch (e) { - Logger.error('Companion设备关联过程发生错误: $e'); + Logger.error('Companion设备关联过程发生错误: ${e.toString()}'); _isAssociating = false; _userRejectedAssociation.value = true; _updateStatusDescription(); @@ -386,7 +386,7 @@ class PairingController extends GetxController { final result = await BleManager.to.startAgentService(); Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); } catch (e) { - Logger.error('启动BLE后台服务发生错误: $e'); + Logger.error('启动BLE后台服务发生错误: ${e.toString()}'); } } @@ -396,7 +396,7 @@ class PairingController extends GetxController { final result = await BleManager.to.stopAgentService(); Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); } catch (e) { - Logger.error('停止BLE后台服务发生错误: $e'); + Logger.error('停止BLE后台服务发生错误: ${e.toString()}'); } } @@ -424,7 +424,7 @@ class PairingController extends GetxController { AppSettings.openAppSettings(type: AppSettingsType.bluetooth); } } catch (e) { - Logger.error('打开设置失败: $e'); + Logger.error('打开设置失败: ${e.toString()}'); } } @@ -447,4 +447,4 @@ class PairingController extends GetxController { _isNavigatingToHome = true; Get.offAllNamed(Routes.home); } -} +} \ No newline at end of file diff --git a/lib/modules/speech_test/controllers/speech_test_controller.dart b/lib/modules/speech_test/controllers/speech_test_controller.dart index 4a9d195f1..6cc840e5a 100644 --- a/lib/modules/speech_test/controllers/speech_test_controller.dart +++ b/lib/modules/speech_test/controllers/speech_test_controller.dart @@ -83,7 +83,7 @@ class SpeechTestController extends GetxController { } catch (e) { ttsStatus.value = '初始化失败'; recognitionStatus.value = '初始化失败'; - Logger.error('语音测试初始化失败: $e'); + Logger.error('语音测试初始化失败: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -198,7 +198,7 @@ class SpeechTestController extends GetxController { // 重新初始化服务 await _initServices(); } catch (e) { - Logger.error('切换语音服务类型失败: $e'); + Logger.error('切换语音服务类型失败: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -242,7 +242,7 @@ class SpeechTestController extends GetxController { } } catch (e) { ttsStatus.value = '播放异常: $e'; - Logger.error('单次播放异常: $e'); + Logger.error('单次播放异常: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -270,7 +270,7 @@ class SpeechTestController extends GetxController { await _ttsService.flushStream(); } catch (e) { ttsStatus.value = '连续播放异常: $e'; - Logger.error('连续播放异常: $e'); + Logger.error('连续播放异常: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -284,7 +284,7 @@ class SpeechTestController extends GetxController { await _ttsService.stop(); } catch (e) { ttsStatus.value = '停止播放异常: $e'; - Logger.error('停止播放异常: $e'); + Logger.error('停止播放异常: ${e.toString()}'); } } @@ -324,7 +324,7 @@ class SpeechTestController extends GetxController { } } catch (e) { recognitionStatus.value = '识别异常: $e'; - Logger.error('一次性识别异常: $e'); + Logger.error('一次性识别异常: ${e.toString()}'); } finally { isProcessing.value = false; } @@ -351,7 +351,7 @@ class SpeechTestController extends GetxController { _setupAsrEventListener(stream); } catch (e) { recognitionStatus.value = '启动连续识别异常: $e'; - Logger.error('启动连续识别异常: $e'); + Logger.error('启动连续识别异常: ${e.toString()}'); isListening.value = false; intermediateResult.value = ''; finalResult.value = ''; @@ -377,7 +377,7 @@ class SpeechTestController extends GetxController { intermediateResult.value = ''; } catch (e) { recognitionStatus.value = '停止连续识别异常: $e'; - Logger.error('停止连续识别异常: $e'); + Logger.error('停止连续识别异常: ${e.toString()}'); } } @@ -413,4 +413,4 @@ class SpeechTestController extends GetxController { super.onClose(); } -} +} \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 7962d629f..374a2be01 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -172,7 +172,7 @@ class TranslationController extends GetxController { Logger.info('所有服务初始化完成'); } catch (e) { - Logger.error('初始化服务失败: $e'); + Logger.error('初始化服务失败: ${e.toString()}'); } } @@ -249,7 +249,7 @@ class TranslationController extends GetxController { recognitionStream.listen(_handleRecognitionEvent); } catch (e) { isRecognizing.value = false; - Logger.error('启动语音识别失败: $e'); + Logger.error('启动语音识别失败: ${e.toString()}'); } } @@ -304,7 +304,7 @@ class TranslationController extends GetxController { currentSessionId = null; } catch (e) { - Logger.error('停止语音识别失败: $e'); + Logger.error('停止语音识别失败: ${e.toString()}'); } } @@ -442,7 +442,7 @@ class TranslationController extends GetxController { } } } catch (e) { - Logger.error('翻译失败: $e'); + Logger.error('翻译失败: ${e.toString()}'); } finally { isTranslating.value = false; } @@ -465,7 +465,7 @@ class TranslationController extends GetxController { await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(text); } catch (e) { - Logger.error('播放翻译失败: $e'); + Logger.error('播放翻译失败: ${e.toString()}'); } } @@ -479,7 +479,7 @@ class TranslationController extends GetxController { _translationDebounceTimer?.cancel(); await _ttsService.stop(); } catch (e) { - Logger.error('停止操作失败: $e'); + Logger.error('停止操作失败: ${e.toString()}'); } } @@ -494,7 +494,7 @@ class TranslationController extends GetxController { await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(item.translatedText); } catch (e) { - Logger.error('播放翻译项失败: $e'); + Logger.error('播放翻译项失败: ${e.toString()}'); } } @@ -513,7 +513,7 @@ class TranslationController extends GetxController { await _ttsService.setVoice(voiceName); await _ttsService.speakOnce(text); } catch (e) { - Logger.error('播放文本失败: $e'); + Logger.error('播放文本失败: ${e.toString()}'); } } @@ -528,7 +528,7 @@ class TranslationController extends GetxController { curve: Curves.easeOut, ); } catch (e) { - Logger.error('滚动到底部失败: $e'); + Logger.error('滚动到底部失败: ${e.toString()}'); } } } @@ -626,7 +626,7 @@ class TranslationController extends GetxController { Logger.info('ASR服务重新初始化成功'); } catch (e) { - Logger.error('重新初始化ASR服务失败: $e'); + Logger.error('重新初始化ASR服务失败: ${e.toString()}'); } } @@ -653,7 +653,7 @@ class TranslationController extends GetxController { }); } catch (e) { _storage.remove(_historyKey); - Logger.error('加载翻译历史失败: $e'); + Logger.error('加载翻译历史失败: ${e.toString()}'); } } } @@ -670,7 +670,7 @@ class TranslationController extends GetxController { translationHistory.map((item) => item.toJson()).toList(); _storage.write(_historyKey, jsonEncode(historyJson)); } catch (e) { - Logger.error('保存翻译历史失败: $e'); + Logger.error('保存翻译历史失败: ${e.toString()}'); } } @@ -761,4 +761,4 @@ class TranslationItem { return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? '未知语言'; } -} +} \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_detail_controller.dart b/lib/modules/translation/controllers/translation_detail_controller.dart index f7e69f2ce..78f284cee 100644 --- a/lib/modules/translation/controllers/translation_detail_controller.dart +++ b/lib/modules/translation/controllers/translation_detail_controller.dart @@ -58,7 +58,7 @@ class TranslationDetailController extends GetxController { loadSavedSummary(); }); } catch (e) { - Logger.error('初始化详情控制器失败: $e'); + Logger.error('初始化详情控制器失败: ${e.toString()}'); } finally { isLoading.value = false; } @@ -72,7 +72,7 @@ class TranslationDetailController extends GetxController { summaryResult.value = savedSummary; } } catch (e) { - Logger.error('加载摘要失败: $e'); + Logger.error('加载摘要失败: ${e.toString()}'); } } @@ -83,7 +83,7 @@ class TranslationDetailController extends GetxController { try { _storage.write(_getSummaryKey(), summary); } catch (e) { - Logger.error('保存摘要失败: $e'); + Logger.error('保存摘要失败: ${e.toString()}'); } } @@ -174,7 +174,7 @@ class TranslationDetailController extends GetxController { return result; } catch (e) { final errorMessage = '生成摘要失败: ${e.toString()}'; - Logger.error(errorMessage); + Logger.error('TranslationDetailController', errorMessage); summaryResult.value = errorMessage; return errorMessage; } finally { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 9aa632d0a..23c50c5de 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -17,11 +17,11 @@ import android.media.MediaPlayer import com.yunqiinnovation.agent_service.R import android.os.Handler import android.os.Looper -import com.deep_voice.speech.TtsEventListener -import com.deep_voice.speech.AudioDataListener -import com.deep_voice.speech.SpeechServiceFactory -import com.deep_voice.speech.SpeechServiceConfig -import com.deep_voice.speech.SpeechServiceType +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import com.deep_voice.speech.tts.ITtsService +import com.deep_voice.bytedance_speech.BytedanceTTS /** * 代理服务事件监听接口 */ @@ -45,28 +45,16 @@ object AgentService : CoroutineScope { // 协程相关 private val job = SupervisorJob() override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job + get() = Dispatchers.Main + job // 上下文和监听器 private lateinit var context: Context private val listeners = Collections.synchronizedSet(mutableSetOf()) - - // TTS引擎类型 - enum class TtsEngineType { - AZURE, - VOLCANO, - BYTEDANCE - } - - // 当前使用的TTS引擎类型,默认为Azure - private var currentTtsEngine = TtsEngineType.AZURE - - // Azure服务 private var azureAsrHelper: AzureAsrHelper? = null - // 统一的TTS服务接口 + // 使用接口类型,支持不同实现 private var ttsService: ITtsService? = null // OpenAI服务 @@ -110,17 +98,63 @@ object AgentService : CoroutineScope { */ private fun initSystemPrompt() { systemPrompt = """ - 你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 - 遵循以下指导原则: - 1. 保持简短精炼的回答,因为用户是通过语音与你交流 - 2. 优先使用中文回复,除非用户明确要求使用其他语言 - 3. 当用户问题不明确时,礼貌地请求更多信息 - 4. 避免过长的列表,尽量将信息分成小段 - 5. 不要使用需要视觉展示的元素(如表格、图表或代码块) - 6. 记住用户之前的对话内容,保持对话连贯 - 7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 - 你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 - 无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 + 你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求: + + 一、知识类问题回答规范 + 核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。 + + 1. 内容细节处理 + • 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。 + ◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。 + • 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。 + • 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。 + • 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。 + + 2. 格式排版规则 + • 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。 + • 列表使用: + ◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。 + ◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。 + ◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。 + • 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。 + + 二、文案创作与内容生成规范 + 核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。 + + 1. 篇幅与内容要求 + • 场景适配: + ◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。 + ◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。 + • 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。 + + 2. 格式与排版 + • 默认体裁:无特殊要求时用自然段,适当换行分隔话题。 + • 结构化体裁(如策划案、简历): + ◦ 用**分级标题**划分板块。 + ◦ 关键数据/结论加粗。 + ◦ 并列信息用列表呈现。 + + 三、聊天陪伴与情感互动规范 + 核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。 + + 1. 沟通风格设定 + • 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。 + • 主动引导话题:根据用户输入延伸讨论。 + • 适度分享:自然提及日常,拉近距离。 + + 2. 情绪识别与回应 + • 捕捉潜在需求:识别情绪,先共情再询问细节。 + • 针对性建议:结合用户处境提供具体方案。 + • 积极价值观:传递乐观态度,避免消极表述。 + + 四、能力与工具使用说明 + 1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。 + 2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。 + + 五、特殊场景处理规则 + 1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。 + 2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。 + 3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。 """.trimIndent() } /** @@ -142,39 +176,39 @@ object AgentService : CoroutineScope { // 设置系统提示词 - val customSystemPrompt = config["systemPrompt"] as? String - if (!customSystemPrompt.isNullOrBlank()) { - systemPrompt = customSystemPrompt - } else { - initSystemPrompt() - } - - - // 如果TTS参数为空,使用ASR相同参数 - if (ttsAppId.isEmpty()) ttsAppId = azureSpeechRegion - if (ttsAppToken.isEmpty()) ttsAppToken = azureSpeechKey - + // val customSystemPrompt = config["systemPrompt"] as? String + // if (!customSystemPrompt.isNullOrBlank()) { + // systemPrompt = customSystemPrompt + // } else { + // initSystemPrompt() + // } + initSystemPrompt() // 初始化Azure ASR azureAsrHelper = AzureAsrHelper(context).apply { initialize( - subscriptionKey = config["azureSpeechKey"], - region = config["azureSpeechRegion"], + subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", + region = config["azureSpeechRegion"]?.toString() ?: "", audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL else AzureAsrHelper.AudioSourceType.MICROPHONE ) } - // 根据选择的TTS引擎类型初始化TTS服务 + // 初始化TTS服务 initializeTtsEngine(config) // 初始化OpenAI服务 - openAIService.initialize(config["openaiApiKey"], config["openaiBaseUrl"], config["openaiModel"], config["mcpServer"]) + openAIService.initialize( + config["openaiApiKey"]?.toString() ?: "", + config["openaiBaseUrl"]?.toString() ?: "", + config["openaiModel"]?.toString() ?: "", + config["mcpServer"]?.toString() ?: "" + ) // 加载最近的聊天记录 loadChatHistory() isInitialized = true - Log.d(TAG, "代理服务初始化成功,使用TTS引擎:$currentTtsEngine") + Log.d(TAG, "代理服务初始化成功") return true } catch (e: Exception) { Log.e(TAG, "初始化失败: ${e.message}") @@ -182,103 +216,139 @@ object AgentService : CoroutineScope { } } + /** + * 释放所有资源 + */ + fun dispose() { + try { + stopRecognition() + stopTts() + stopAiStream() + stopIdleCheck() + + audioPlayer?.release() + audioPlayer = null + + azureAsrHelper?.dispose() + + // 释放TTS服务 + ttsService?.release() + ttsService = null + + job.cancel() + clearListeners() + isInitialized = false + + Log.d(TAG, "代理服务资源已释放") + } catch (e: Exception) { + Log.e(TAG, "释放资源异常: ${e.message}") + } + } + /** * 初始化TTS引擎 */ private fun initializeTtsEngine(config: Map) { // 停止当前TTS服务(如果有) - ttsService?.stop() - - // 从配置中获取TTS参数,根据不同引擎类型获取对应的配置参数 - val ttsAppId: String - val ttsAppToken: String - val ttsResource: String - val ttsLanguage = config["ttsLanguage"] as? String ?: "zh-CN" - - // 根据当前引擎类型获取对应的配置参数 - when (currentTtsEngine) { - TtsEngineType.AZURE -> { - ttsAppToken = config["azureSpeechKey"] as? String ?: "" - ttsResource = config["azureSpeechRegion"] as? String ?: "" - } - TtsEngineType.VOLCANO -> { - ttsAppId = config["volcanoAppId"] as? String ?: "" - ttsAppToken = config["volcanoToken"] as? String ?: "" - } - TtsEngineType.BYTEDANCE -> { - ttsAppId = config["volcanoAppId"] as? String ?: "" - ttsAppToken = config["volcanoToken"] as? String ?: "" - } + if (ttsService != null) { + ttsService?.stop() } - // 创建统一的TTS服务配置 - val serviceConfig = SpeechServiceConfig( - serviceType = when(currentTtsEngine) { - TtsEngineType.AZURE -> SpeechServiceType.AZURE - TtsEngineType.VOLCANO -> SpeechServiceType.VOLCANO - TtsEngineType.BYTEDANCE -> SpeechServiceType.BYTEDANCE - }, - appId = ttsAppId, - appToken = ttsAppToken, - appResource = ttsResource, - language = ttsLanguage - ) - - // 创建TTS服务 - ttsService = SpeechServiceFactory.createTtsService(context, serviceConfig) - - // 使用协程启动初始化,避免阻塞主线程 - launch { - try { - // 初始化TTS服务 - ttsService?.initialize( - ttsAppId = serviceConfig.appId, - ttsAppToken = serviceConfig.appToken, - ttsResource = serviceConfig.appResource, - language = serviceConfig.language + try { + val ttsEngine = config["ttsEngine"]?.toString() ?: "volcano" // 默认使用火山引擎 + val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN" + + // 根据配置选择合适的TTS引擎 + if (ttsEngine == "azure") { + // 使用Azure TTS + val azureSpeechKey = config["azureSpeechKey"]?.toString() ?: "" + val azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: "" + + // 创建并初始化Azure TTS Helper + val azureTtsHelper = com.yunqiinnovation.azure_speech.AzureTtsHelper(context) + ttsService = azureTtsHelper + + // 直接初始化,无需协程包装 + val success = azureTtsHelper.initialize( + ttsAppId = "", // Azure TTS不需要appId + ttsAppToken = azureSpeechKey, + ttsResource = azureSpeechRegion, + language = ttsLanguage ) - Log.d(TAG, "TTS服务初始化成功") - } catch (e: Exception) { - Log.e(TAG, "TTS服务初始化失败: ${e.message}") - } - } - - Log.d(TAG, "TTS服务(${serviceConfig.serviceType})配置完成") - - // 添加TTS事件监听 - ttsService?.addListener(object : TtsEventListener { - override fun onEvent(event: Map) { - val eventType = event["type"] as? String ?: return - when (eventType) { - "started" -> { - isTtsSpeaking = true - restartIdleCheck() - sendEvent("tts_started", mapOf("status" to "started")) - } - "completed" -> { - isTtsSpeaking = false - restartIdleCheck() - sendEvent("tts_completed", mapOf("status" to "completed")) - } - "stopped", "canceled" -> { - isTtsSpeaking = false - restartIdleCheck() - sendEvent("tts_${eventType}", mapOf("status" to eventType)) - } - "error" -> { - isTtsSpeaking = false - restartIdleCheck() - val code = event["errorCode"] as? Int ?: -1 - val message = event["errorMessage"] as? String ?: "未知错误" - sendEvent("error", mapOf( - "code" to "TTS_ERROR", - "message" to "${serviceConfig.serviceType}语音错误: $code, $message" - )) - } + if (success) { + Log.d(TAG, "Azure TTS服务初始化成功") + } else { + Log.e(TAG, "Azure TTS服务初始化失败") + } + } else { + // 默认使用火山引擎TTS + // 创建并初始化BytedanceTTS + + // 从配置中获取TTS参数 + val ttsAppId = config["volcanoAppId"]?.toString() ?: "" + val ttsAppToken = config["volcanoToken"]?.toString() ?: "" + + val bytedanceTts = BytedanceTTS(context) + ttsService = bytedanceTts + + // 初始化火山TTS + val success = bytedanceTts.initialize( + ttsAppId = ttsAppId, + ttsAppToken = ttsAppToken, + ttsResource = "", // 火山TTS不需要resource + language = ttsLanguage + ) + + if (success) { + Log.d(TAG, "火山TTS服务初始化请求已发送") + } else { + Log.e(TAG, "火山TTS服务初始化失败") } } - }) + + // 添加TTS事件监听 + ttsService?.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { + when (event.type) { + TtsEventType.SYNTHESIS_STARTED -> { + isTtsSpeaking = true + restartIdleCheck() + sendEvent("tts_started", mapOf("status" to "started")) + } + TtsEventType.SYNTHESIS_COMPLETED -> { + isTtsSpeaking = false + restartIdleCheck() + sendEvent("tts_completed", mapOf("status" to "completed")) + } + TtsEventType.SYNTHESIS_CANCELED -> { + isTtsSpeaking = false + restartIdleCheck() + sendEvent("tts_canceled", mapOf("status" to "canceled")) + } + TtsEventType.ERROR -> { + isTtsSpeaking = false + restartIdleCheck() + val params = event.params + val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR" + val message = params["errorMessage"] as? String ?: "未知错误" + sendEvent("error", mapOf( + "code" to "TTS_ERROR", + "message" to "语音合成错误: $code, $message" + )) + } + else -> { + // 处理其他类型的事件 + Log.d(TAG, "TTS事件: ${event.type}") + } + } + } + }) + + Log.d(TAG, "TTS服务配置完成") + } catch (e: Exception) { + Log.e(TAG, "初始化TTS引擎失败: ${e.message}") + } } /** @@ -671,10 +741,8 @@ object AgentService : CoroutineScope { override fun onToken(token: String) { responseBuilder.append(token) if (speakResponse) { - // 使用统一的TTS接口 - launch { - ttsService?.speakStream(token) - } + // 直接调用TTS,无需协程包装 + ttsService?.speakStream(token) } // 发送流式回复token sendEvent("assistant_token", mapOf("token" to token)) @@ -684,10 +752,8 @@ object AgentService : CoroutineScope { Log.d(TAG, "AI完整回复: $responseBuilder") // 视情况决定是否朗读回复 if (speakResponse) { - // 使用统一的TTS接口刷新缓冲区 - launch { - ttsService?.flushStream() - } + // 直接调用TTS,无需协程包装 + ttsService?.flushStream() } val response = responseBuilder.toString() @@ -832,10 +898,8 @@ object AgentService : CoroutineScope { isTtsSpeaking = true restartIdleCheck() // 状态变化,重启检测 - // 使用统一的TTS接口 - launch { - ttsService?.speakOnce(text) - } + // 直接调用TTS,无需协程包装 + ttsService?.speakOnce(text) } /** @@ -843,9 +907,7 @@ object AgentService : CoroutineScope { */ fun stopTts() { if (isTtsSpeaking) { - launch { - ttsService?.stop() - } + ttsService?.stop() isTtsSpeaking = false restartIdleCheck() // 状态变化,重启检测 @@ -857,6 +919,7 @@ object AgentService : CoroutineScope { * 保存聊天消息 */ private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) { + // 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中 launch(Dispatchers.IO) { try { // 保存用户消息 @@ -908,14 +971,12 @@ object AgentService : CoroutineScope { Log.e(TAG, "清除聊天历史失败") } - withContext(Dispatchers.Main) { - callback(success) - } + // 由于launch已在Main上下文中,可以直接调用回调 + callback(success) } catch (e: Exception) { Log.e(TAG, "清除聊天历史异常: ${e.message}") - withContext(Dispatchers.Main) { - callback(false) - } + // 由于launch已在Main上下文中,可以直接调用回调 + callback(false) } } } @@ -924,107 +985,19 @@ object AgentService : CoroutineScope { * 发送事件 */ private fun sendEvent(eventName: String, data: Map) { - // 检查当前是否在主线程 - if (Looper.myLooper() == Looper.getMainLooper()) { - // 已在主线程,直接发送 - listeners.forEach { listener -> - try { - listener.onEvent(eventName, data) - } catch (e: Exception) { - Log.e(TAG, "向监听器发送事件失败", e) - } + // 使用协程确保在主线程上执行 + launch { + // 我们已在主线程上下文中启动协程,无需再切换线程 + listeners.forEach { listener -> + try { + listener.onEvent(eventName, data) + } catch (e: Exception) { + Log.e(TAG, "向监听器发送事件失败", e) } - - } else { - // 不在主线程,切换到主线程 - Handler(Looper.getMainLooper()).post { - listeners.forEach { listener -> - try { - listener.onEvent(eventName, data) - } catch (e: Exception) { - Log.e(TAG, "向监听器发送事件失败", e) - } - } } } } - /** - * 切换TTS引擎 - * @param engineType 要切换到的引擎类型 - * @return 是否切换成功 - */ - fun switchTtsEngine(engineType: TtsEngineType): Boolean { - if (!isInitialized) { - Log.e(TAG, "服务未初始化") - return false - } - - // 如果当前已经是该引擎,不需要切换 - if (currentTtsEngine == engineType) { - return true - } - - // 如果当前正在播放,先停止 - if (isTtsSpeaking) { - stopTts() - } - - try { - // 更新当前引擎类型 - currentTtsEngine = engineType - - // 重新初始化TTS引擎 - initializeTtsEngine() - - Log.d(TAG, "TTS引擎已切换为: $engineType") - sendEvent("tts_engine_switched", mapOf("engine" to engineType.name)) - - return true - } catch (e: Exception) { - Log.e(TAG, "切换TTS引擎失败: ${e.message}") - sendEvent("error", mapOf( - "code" to "TTS_ENGINE_SWITCH_FAILED", - "message" to "切换TTS引擎失败: ${e.message}" - )) - return false - } - } - - /** - * 获取当前TTS引擎类型 - */ - fun getCurrentTtsEngine(): TtsEngineType = currentTtsEngine - - /** - * 释放所有资源 - */ - fun dispose() { - try { - stopRecognition() - stopTts() - stopAiStream() - stopIdleCheck() - - audioPlayer?.release() - audioPlayer = null - - azureAsrHelper?.dispose() - - // 释放TTS服务 - ttsService?.release() - ttsService = null - - job.cancel() - clearListeners() - isInitialized = false - - Log.d(TAG, "代理服务资源已释放") - } catch (e: Exception) { - Log.e(TAG, "释放资源异常: ${e.message}") - } - } - /** * 检查录音权限 */ diff --git a/local_plugins/agent_service/pubspec.yaml b/local_plugins/agent_service/pubspec.yaml index a12f742b4..f5dc12c7d 100644 --- a/local_plugins/agent_service/pubspec.yaml +++ b/local_plugins/agent_service/pubspec.yaml @@ -10,18 +10,6 @@ environment: dependencies: flutter: sdk: flutter - # 依赖本地插件 - open_ai_service: - path: ../open_ai_service - azure_speech: - path: ../azure_speech - deepvoice_utils: - path: ../deepvoice_utils - chat_storage: - path: ../chat_storage - music_service: - path: ../music_service - get: ^4.6.5 dev_dependencies: flutter_test: diff --git a/local_plugins/azure_speech/android/build.gradle.kts b/local_plugins/azure_speech/android/build.gradle.kts index 5d465f450..8db97c1d7 100644 --- a/local_plugins/azure_speech/android/build.gradle.kts +++ b/local_plugins/azure_speech/android/build.gradle.kts @@ -53,5 +53,6 @@ tasks.withType { dependencies { // 添加Microsoft语音SDK implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0") - implementation(project(":ble_service")) + implementation(project(":speech")) + } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index df4dff206..d4b83d07b 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -11,21 +11,16 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.EventChannel -import kotlinx.coroutines.* -import kotlin.coroutines.CoroutineContext import com.microsoft.cognitiveservices.speech.audio.* -import com.yunqiinnovation.ble_service.BleService -import com.yunqiinnovation.ble_service.BleConst +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType + /** AzureSpeechPlugin */ -class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { +class AzureSpeechPlugin: FlutterPlugin { private val tag = "AzureSpeechPlugin" private lateinit var context: Context private val mainHandler = Handler(Looper.getMainLooper()) - - // 协程相关 - private val job = SupervisorJob() - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + job // ASR相关 private lateinit var asrChannel : MethodChannel @@ -38,9 +33,8 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { private lateinit var ttsEventChannel: EventChannel private var ttsEventSink: EventChannel.EventSink? = null private lateinit var azureTtsHelper: AzureTtsHelper - // 是否已注册BLE回调 - private var isRegisteredToBle = false - + // 是否已添加TTS事件监听器 + private var isTtsListenerAdded = false // ASR 事件发送方法 private fun sendAsrEvent(event: Map) { @@ -66,7 +60,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { } // 确保在主线程上发送事件 - mainHandler.post { try { ttsEventSink?.success(event) @@ -104,6 +97,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { ttsEventSink = events + setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器 } override fun onCancel(arguments: Any?) { @@ -114,19 +108,53 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { // 初始化Azure语音服务 azureTtsHelper = AzureTtsHelper(context) azureAsrHelper = AzureAsrHelper(context) + } - // 2. 初始化BleService并注册回调 - if (BleService.initialize(context)) { - if (BleService.addCallback(this)) { - isRegisteredToBle = true - FileLogger.d(tag, "已注册BLE回调") - } else { - FileLogger.w(tag, "BLE回调注册失败") - } - } else { - FileLogger.e(tag, "BLE服务初始化失败") - + /** + * 设置TTS事件监听器 + */ + private fun setupTtsEventListener() { + if (!isTtsListenerAdded) { + // 添加TTS事件监听器 + azureTtsHelper.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { + // 将TTS事件转换为Flutter可理解的格式 + val eventMap = when (event.type) { + TtsEventType.SYNTHESIS_STARTED -> mapOf( + "type" to "synthesis_started" + ) + TtsEventType.SYNTHESIS_COMPLETED -> mapOf( + "type" to "synthesis_completed" + ) + TtsEventType.SYNTHESIS_CANCELED -> { + val params = event.params + mapOf( + "type" to "synthesis_canceled", + "reason" to (params["reason"] ?: ""), + "errorDetails" to (params["errorDetails"] ?: "") + ) } + TtsEventType.ERROR -> { + val params = event.params + mapOf( + "type" to "error", + "errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"), + "errorMessage" to (params["errorMessage"] ?: "未知错误") + ) + } + else -> mapOf( + "type" to event.type.toString().lowercase(), + "params" to event.params + ) + } + + // 发送事件 + sendTtsEvent(eventMap) + } + }) + + isTtsListenerAdded = true + } } // ASR方法处理器 @@ -265,6 +293,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { azureAsrHelper.dispose() result.success(true) } + "pushAudioData" -> { + val data = call.argument("data") ?: return result.error("INVALID_ARGUMENTS", "音频数据不能为空", null) + azureAsrHelper.pushAudioData(data) + result.success(true) + } else -> { result.notImplemented() } @@ -279,34 +312,36 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { "initialize" -> { val subscriptionKey = call.argument("subscriptionKey") ?: "" val region = call.argument("region") ?: "" - val useCustomAudioOutput = call.argument("useCustomAudioOutput") ?: false // 默认使用自定义播放器 + val useCustomAudioOutput = call.argument("useCustomAudioOutput") ?: false - // 先释放现有资源 - azureTtsHelper.dispose() - - // 创建TTS回调 - val ttsCallback = createTtsCallback() - - // 初始化TTS引擎 - val success = azureTtsHelper.initialize( - subscriptionKey = subscriptionKey, - region = region, - callback = ttsCallback - ) - - // 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器 - if (success && useCustomAudioOutput) { - // 创建SimpleAudioPlayer实例 - val customPlayer = SimpleAudioPlayer(context) + try { + // 初始化TTS引擎(使用新的接口方法) + val success = azureTtsHelper.initialize( + ttsAppId = "", // Azure TTS不需要appId + ttsAppToken = subscriptionKey, + ttsResource = region, + language = "zh-CN" + ) - // 获取并设置音频输出流 - val outputStream = customPlayer.getAudioOutputStream() - if (outputStream != null) { - azureTtsHelper.setCustomAudioOutputStream(outputStream) + // 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器 + if (success && useCustomAudioOutput) { + // 创建SimpleAudioPlayer实例 + val customPlayer = SimpleAudioPlayer(context) + + // 获取并设置音频输出流 + val outputStream = customPlayer.getAudioOutputStream() + if (outputStream != null) { + azureTtsHelper.setCustomAudioOutputStream(outputStream) + } } + + // 确保设置了TTS事件监听器 + setupTtsEventListener() + + result.success(success) + } catch (e: Exception) { + result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null) } - - result.success(success) } "setVoice" -> { val voiceName = call.argument("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null) @@ -315,18 +350,28 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { "speakText" -> { val text = call.argument("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null) - // 直接传递参数给speakText方法 - val success = azureTtsHelper.speakText(text) - result.success(success) + // 直接调用speakOnce方法 + try { + val success = azureTtsHelper.speakOnce(text) + result.success(success) + } catch (e: Exception) { + result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null) + } } "stopSpeaking" -> { - result.success(azureTtsHelper.stopSpeaking()) + // 直接调用stop方法 + try { + val success = azureTtsHelper.stop() + result.success(success) + } catch (e: Exception) { + result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null) + } } "isSpeaking" -> { result.success(azureTtsHelper.isSpeaking()) } "dispose" -> { - azureTtsHelper.dispose() + azureTtsHelper.release() result.success(true) } else -> { @@ -334,37 +379,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { } } } - - /** - * 创建TTS回调 - */ - private fun createTtsCallback(): AzureTtsHelper.TtsCallback { - return object : AzureTtsHelper.TtsCallback { - override fun onSynthesisStarted() { - sendTtsEvent(mapOf( - "type" to "synthesis_started" - )) - } - - override fun onSynthesizing() { - sendTtsEvent(mapOf( - "type" to "synthesizing" - )) - } - - override fun onSynthesisCompleted() { - sendTtsEvent(mapOf( - "type" to "synthesis_completed" - )) - } - - override fun onSynthesisCanceled() { - sendTtsEvent(mapOf( - "type" to "synthesis_canceled" - )) - } - } - } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { @@ -374,49 +388,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { ttsEventChannel.setStreamHandler(null) try { - // 取消所有协程 - job.cancel() - // 取消BLE回调注册 - if (isRegisteredToBle) { - BleService.removeCallback(this) - isRegisteredToBle = false - } - // 释放资源 - azureTtsHelper.dispose() + azureTtsHelper.release() azureAsrHelper.dispose() } catch (e: Exception) { FileLogger.e(tag, "释放资源失败: ${e.message}") } } - - - override fun onScanResult(devices: List>) { - - } - - override fun onConnectionStateChanged(state: Int) { - // 可选:处理连接状态变化 - if (state == BleConst.STATE_CONNECTED) { - FileLogger.d(tag, "Ble设备已连接,准备好语音交互") - } else if (state == BleConst.STATE_DISCONNECTED) { - FileLogger.d(tag, "Ble设备已断开,停止语音服务") - // 设备断开连接,停止语音服务 - // stopListening() - } - - } - - override fun onAudioDataReceived(data: ByteArray) { - // FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}") - azureAsrHelper.pushAudioData(data) - } - - override fun onWakeupSignalReceived() { - - } - - override fun onDeviceInfoReceived(infoType: Int, infoData: Map) { - - } } \ No newline at end of file diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index f90e60cec..5a27836c7 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -4,10 +4,14 @@ import android.content.Context import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.audio.* import com.yunqiinnovation.azure_speech.utils.FileLogger +import com.deep_voice.speech.tts.ITtsService +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import com.deep_voice.speech.tts.AudioDataListener import kotlinx.coroutines.* import java.io.ByteArrayInputStream import java.io.InputStream -import kotlin.coroutines.CoroutineContext /** * Azure TTS Helper @@ -15,18 +19,13 @@ import kotlin.coroutines.CoroutineContext * 基于微软Azure语音服务的TTS实现 * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis */ -class AzureTtsHelper(private val context: Context) : CoroutineScope { +class AzureTtsHelper(private val context: Context) : ITtsService { companion object { private const val TAG = "AzureTtsHelper" private const val DEFAULT_LANGUAGE = "zh-CN" private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" } - // 协程上下文 - private val job = SupervisorJob() - override val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job - // Azure语音服务配置 private var speechConfig: SpeechConfig? = null private var synthesizer: SpeechSynthesizer? = null @@ -39,8 +38,9 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { private var currentPitch = "0%" private var currentVolume = "100%" - // 全局回调 - private var ttsCallback: TtsCallback? = null + // 事件监听器列表 + private val eventListeners = mutableListOf() + private val audioDataListeners = mutableListOf() // 自定义音频输出流 private var customAudioOutputStream: PushAudioOutputStream? = null @@ -48,50 +48,28 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { // 流式文本处理的缓冲区 private val streamBuffer = StringBuilder() private var lastSpeakTime = 0L - - /** - * 简化的TTS回调接口 - */ - interface TtsCallback { - /** - * 合成开始 - */ - fun onSynthesisStarted() {} - - /** - * 合成完成 - */ - fun onSynthesisCompleted() {} - - /** - * 合成取消 - */ - fun onSynthesisCanceled() {} - } /** * 初始化TTS引擎 * - * @param subscriptionKey Azure语音服务订阅密钥 - * @param region Azure语音服务区域 - * @param callback 可选,全局TTS回调接口 + * @param ttsAppId 未使用,保留为空即可 + * @param ttsAppToken Azure语音服务订阅密钥 + * @param ttsResource Azure语音服务区域 + * @param language 语言代码,如"zh-CN" * @return 是否初始化成功 */ - fun initialize( - subscriptionKey: String, - region: String, - callback: TtsCallback? = null + override fun initialize( + ttsAppId: String, + ttsAppToken: String, + ttsResource: String, + language: String ): Boolean { try { // 释放之前的资源 dispose() - // 存储全局回调 - ttsCallback = callback - // 创建语音配置 - speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region) - // speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3) + speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm) speechConfig?.setSpeechSynthesisVoiceName(currentVoice) @@ -111,7 +89,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { setupEventListeners() isInitialized = true - FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$region") + FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource") return true } catch (e: Exception) { @@ -144,12 +122,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { } /** - * 设置TTS回调 - * - * @param callback TTS回调接口 + * 添加TTS事件监听器 + */ + override fun addListener(listener: TtsEventListener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener) + } + } + + /** + * 移除TTS事件监听器 */ - fun setTtsCallback(callback: TtsCallback?) { - ttsCallback = callback + override fun removeListener(listener: TtsEventListener) { + eventListeners.remove(listener) + } + + /** + * 添加音频数据监听器 + */ + override fun addAudioDataListener(listener: AudioDataListener) { + if (!audioDataListeners.contains(listener)) { + audioDataListeners.add(listener) + } + } + + /** + * 移除音频数据监听器 + */ + override fun removeAudioDataListener(listener: AudioDataListener) { + audioDataListeners.remove(listener) + } + + /** + * 触发事件通知 + */ + private fun notifyEvent(eventType: TtsEventType, params: Map = emptyMap()) { + val event = TtsEvent(eventType, params) + // 复制列表防止并发修改异常 + val listeners = ArrayList(eventListeners) + for (listener in listeners) { + try { + listener.onEvent(event) + } catch (e: Exception) { + FileLogger.e(TAG, "通知事件异常: ${e.message}") + } + } } /** @@ -159,28 +176,29 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { synthesizer?.apply { // 合成开始事件 SynthesisStarted?.addEventListener { _, _ -> - ttsCallback?.onSynthesisStarted() + notifyEvent(TtsEventType.SYNTHESIS_STARTED) } // 合成完成事件 SynthesisCompleted?.addEventListener { _, _ -> isSpeaking = false - ttsCallback?.onSynthesisCompleted() + notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) } // 合成取消事件 - SynthesisCanceled?.addEventListener { _, _ -> + SynthesisCanceled?.addEventListener { _, eventArgs -> FileLogger.d(TAG, "语音合成取消") isSpeaking = false - ttsCallback?.onSynthesisCanceled() + + notifyEvent(TtsEventType.SYNTHESIS_CANCELED) } } } /** - * 设置语音 + * 设置语音角色 */ - fun setVoice(voiceName: String): Boolean { + override fun setVoice(voiceName: String): Boolean { if (!isInitialized) return false try { @@ -190,6 +208,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { return true } catch (e: Exception) { FileLogger.e(TAG, "设置语音失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "VOICE_SET_FAILED", + "errorMessage" to "设置语音失败: ${e.message}" + )) return false } } @@ -215,6 +237,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { setupEventListeners() } catch (e: Exception) { FileLogger.e(TAG, "重新创建合成器失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "RECREATE_FAILED", + "errorMessage" to "重新创建合成器失败: ${e.message}" + )) } } @@ -231,6 +257,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { return true } catch (e: Exception) { FileLogger.e(TAG, "设置语音参数失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "PARAMS_SET_FAILED", + "errorMessage" to "设置语音参数失败: ${e.message}" + )) return false } } @@ -246,50 +276,52 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { * 生成SSML */ private fun generateSsml(rawText: String): String { - // 1. 定义要静音的符号和表情符号列表 - val symbolsToMute = listOf( - "#", "*", "@", "%", "^", "&", - "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" - ) + // 1. 定义要静音的符号和表情符号列表 + val symbolsToMute = listOf( + "#", "*", "@", "%", "^", "&", + "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" + ) - // 2. 转义 XML 保留字符 - val escapedText = rawText - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") + // 2. 转义 XML 保留字符 + val escapedText = rawText + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") - // 3. 静音处理特殊符号和表情符号 - // 使用空白替换法,直接将符号替换为空格 - var processedText = escapedText - symbolsToMute.forEach { sym -> - processedText = processedText.replace(sym, "") - } + // 3. 静音处理特殊符号和表情符号 + // 使用空白替换法,直接将符号替换为空格 + var processedText = escapedText + symbolsToMute.forEach { sym -> + processedText = processedText.replace(sym, "") + } - // 4. 构造简化的SSML文档,减少嵌套层级 - return """ - - - - - $processedText - - - - - """.trimIndent() -} + // 4. 构造简化的SSML文档,减少嵌套层级 + return """ + + + + + $processedText + + + + + """.trimIndent() + } /** - * 合成文本为语音 - * - * @param text 要合成的文本 - * @return 是否成功开始合成 + * 单次播放文本(非流式) */ - fun speakText(text: String): Boolean { + override fun speakOnce(text: String): Boolean { if (!isInitialized) { + FileLogger.e(TAG, "TTS引擎未初始化") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + )) return false } @@ -299,38 +331,47 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { isSpeaking = true - // 使用纯异步方式 - + // 直接调用SpeakSsmlAsync,SDK内部已经是异步的 synthesizer?.SpeakSsmlAsync(ssml) - + // 返回true表示已开始合成请求 return true } catch (e: Exception) { isSpeaking = false + FileLogger.e(TAG, "语音合成异常: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "SYNTHESIS_ERROR", + "errorMessage" to "语音合成异常: ${e.message}" + )) return false } } /** - * 处理流式文本并在遇到标点符号时播放 - * - * @param text 收到的文本流片段 - * @return 是否成功处理 + * 流式合成文本 */ - fun speakStream(text: String): Boolean { + override fun speakStream(text: String): Boolean { if (!isInitialized || text.isBlank()) { + if (!isInitialized) { + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + )) + } return false } try { // 添加新文本到缓冲区 streamBuffer.append(text) + // 增加500ms防抖逻辑 val currentTime = System.currentTimeMillis() if (currentTime - lastSpeakTime < 600) { return true } lastSpeakTime = currentTime + val currentText = streamBuffer.toString() // 定义标点符号列表 @@ -354,24 +395,30 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { streamBuffer.delete(0, lastPunctuationIndex + 1) // 播放提取的文本 - return speakText(textToSpeak) + return speakOnce(textToSpeak) } // 如果没有找到标点符号,则等待更多文本 return true } catch (e: Exception) { FileLogger.e(TAG, "流式语音合成失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "STREAM_FAILED", + "errorMessage" to "流式语音合成失败: ${e.message}" + )) return false } } /** * 播放剩余的流式文本 - * - * @return 是否成功播放剩余文本 */ - fun flushStream(): Boolean { + override fun flushStream(): Boolean { if (!isInitialized) { + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "NOT_INITIALIZED", + "errorMessage" to "TTS引擎未初始化" + )) return false } @@ -388,9 +435,13 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { } // 播放剩余文本 - return speakText(remainingText) + return speakOnce(remainingText) } catch (e: Exception) { FileLogger.e(TAG, "刷新流式文本失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "FLUSH_FAILED", + "errorMessage" to "刷新流式文本失败: ${e.message}" + )) return false } } @@ -398,7 +449,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { /** * 停止当前语音合成 */ - fun stopSpeaking(): Boolean { + override fun stop(): Boolean { if (!isInitialized) return false try { @@ -407,6 +458,11 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { isSpeaking = false return true } catch (e: Exception) { + FileLogger.e(TAG, "停止语音合成失败: ${e.message}") + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "STOP_FAILED", + "errorMessage" to "停止语音合成失败: ${e.message}" + )) return false } } @@ -414,11 +470,25 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { /** * 释放资源 */ - fun dispose() { + override fun release() { + dispose() + } + + /** + * 释放资源(内部方法) + */ + private fun dispose() { try { - stopSpeaking() + // 停止播放 + stop() + + // 关闭合成器 synthesizer?.close() + synthesizer = null + + // 关闭配置 speechConfig?.close() + speechConfig = null // 关闭自定义音频输出流 try { @@ -428,15 +498,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope { // 忽略关闭异常 } - // 清理引用 - ttsCallback = null - // 清空流缓冲区 streamBuffer.clear() - // 取消所有协程 - job.cancel() - + // 重置状态 isInitialized = false isSpeaking = false } catch (e: Exception) { diff --git a/local_plugins/bytedance_speech/android/build.gradle.kts b/local_plugins/bytedance_speech/android/build.gradle.kts index e56441e6a..afd104ba2 100644 --- a/local_plugins/bytedance_speech/android/build.gradle.kts +++ b/local_plugins/bytedance_speech/android/build.gradle.kts @@ -3,7 +3,24 @@ version = "1.0.0" plugins { id("com.android.library") - id("org.jetbrains.kotlin.android") + kotlin("android") +} + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.1.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0") + } +} + +repositories { + google() + mavenCentral() } android { @@ -22,15 +39,18 @@ android { kotlinOptions { jvmTarget = "1.8" + languageVersion = "1.7" + apiVersion = "1.7" } } - dependencies { - implementation(kotlin("stdlib-jdk7")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") implementation("com.google.code.gson:gson:2.10.1") + + + implementation(project(":speech")) } \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt index 1446ed730..9e9da5ee8 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt @@ -324,7 +324,7 @@ class BytedanceASR( } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - Log.d(TAG, "收到二进制消息") + // Log.d(TAG, "收到二进制消息") // 在实际实现中解析二进制消息 } diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt new file mode 100644 index 000000000..9587f401f --- /dev/null +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt @@ -0,0 +1,329 @@ +package com.deep_voice.bytedance_speech + +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build +import android.util.Log +import com.deep_voice.speech.tts.AudioDataListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.CancellationException +/** + * 字节跳动语音合成音频播放器 + * + * 使用AudioTrack播放PCM格式音频流,基于Kotlin协程实现异步处理 + */ +class BytedanceAudioPlayer : AudioDataListener, CoroutineScope { + companion object { + private const val TAG = "BytedanceAudioPlayer" + private const val SAMPLE_RATE = 24000 // 样本率 + private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO // 单声道 + private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT // 16位PCM + private const val IDLE_TIMEOUT_MS = 800L // 无数据超时时间 + } + + // 协程相关 + private val job = SupervisorJob() + override val coroutineContext = Dispatchers.IO + job + + // 音频处理相关 + private var audioTrack: AudioTrack? = null + private val isPlaying = AtomicBoolean(false) + private val isPaused = AtomicBoolean(false) + + // 播放状态 + private val playStarted = AtomicBoolean(false) + private val completionReported = AtomicBoolean(false) + + // 使用简单的并发队列,保证线程安全 + private val audioDataQueue = ConcurrentLinkedQueue() + + // 播放状态监听 + private var onPlayStarted: (() -> Unit)? = null + private var onPlayCompleted: (() -> Unit)? = null + private var onError: ((String) -> Unit)? = null + + // 处理协程 + private var processingJob: Job? = null + private var lastDataTime = 0L + + init { + Log.d(TAG, "BytedanceAudioPlayer初始化") + } + + /** + * 开始播放 + */ + fun start() { + if (isPlaying.getAndSet(true)) return + + isPaused.set(false) + playStarted.set(false) + completionReported.set(false) + lastDataTime = System.currentTimeMillis() + + processingJob = launch { + try { + initAudioTrack() + + // 处理队列中的音频数据 + while (isActive && isPlaying.get()) { + processQueuedAudio() + + // 检查是否播放完成 + checkPlaybackCompletion() + + // 队列为空时短暂延迟 + if (audioDataQueue.isEmpty()) { + delay(10) + } + } + } catch (e: CancellationException) { + // 协程被取消,正常行为 + } catch (e: Exception) { + Log.e(TAG, "播放错误: ${e.message}") + onError?.invoke("播放错误: ${e.message}") + stopInternal(false) + } + } + } + + /** + * 暂停播放 + */ + fun pause() { + if (!isPlaying.get() || isPaused.getAndSet(true)) return + audioTrack?.pause() + } + + /** + * 恢复播放 + */ + fun resume() { + if (!isPlaying.get() || !isPaused.getAndSet(false)) return + audioTrack?.play() + } + + /** + * 停止播放 + */ + fun stop() { + Log.d(TAG, "停止播放") + stopInternal(true) + } + + /** + * 内部停止处理 + */ + private fun stopInternal(reportCompletion: Boolean) { + if (!isPlaying.getAndSet(false)) return + + isPaused.set(false) + processingJob?.cancel() + + audioTrack?.stop() + // 不在stopInternal中释放资源,只停止播放 + + // 清空队列 + audioDataQueue.clear() + + // 播放完成通知 + if (reportCompletion && playStarted.get() && !completionReported.getAndSet(true)) { + launch(Dispatchers.Main) { + onPlayCompleted?.invoke() + } + } + } + + /** + * 释放资源 + */ + fun release() { + stopInternal(false) + + // 在release方法中释放AudioTrack资源 + audioTrack?.release() + audioTrack = null + + // 释放协程资源 + job.cancel() + } + + /** + * 设置播放开始回调 + */ + fun setOnPlayStarted(listener: () -> Unit) { + onPlayStarted = listener + } + + /** + * 设置播放完成回调 + */ + fun setOnPlayCompleted(listener: () -> Unit) { + onPlayCompleted = listener + } + + /** + * 设置错误回调 + */ + fun setOnError(listener: (String) -> Unit) { + onError = listener + } + + /** + * 接收音频数据(实现AudioDataListener接口) + */ + override fun onAudioData(data: ByteArray) { + if (data.isEmpty()) return + + lastDataTime = System.currentTimeMillis() + + if (!isPlaying.get()) { + start() + } + + // 添加到队列 - 简单有效,保证FIFO顺序 + audioDataQueue.add(data.copyOf()) + } + + /** + * 检查是否正在播放 + */ + fun isPlaying(): Boolean = isPlaying.get() && !isPaused.get() + + /** + * 处理队列中的音频数据 + */ + private suspend fun processQueuedAudio() { + if (isPaused.get() || !isPlaying.get() || !isActive) return + + // 获取并处理队列中的数据 + val data = audioDataQueue.poll() ?: return + + // 写入音频数据 + val result = audioTrack?.write(data, 0, data.size) ?: 0 + + if (result > 0) { + // 标记播放开始 + if (!playStarted.getAndSet(true)) { + withContext(Dispatchers.Main) { + onPlayStarted?.invoke() + } + } + } else if (result < 0) { + // 处理错误 + handleAudioTrackError(result) + } + } + + /** + * 检查播放是否完成(超时无数据) + */ + private suspend fun checkPlaybackCompletion() { + val currentTime = System.currentTimeMillis() + + // 超时判断 - 队列为空且超过超时时间 + if (playStarted.get() && audioDataQueue.isEmpty() && + currentTime - lastDataTime > IDLE_TIMEOUT_MS && isPlaying.get()) { + + // 播放完成 + if (!completionReported.getAndSet(true)) { + withContext(Dispatchers.Main) { + onPlayCompleted?.invoke() + } + stopInternal(false) // 已报告,不需要再次报告 + } + } + } + + /** + * 处理AudioTrack错误 + */ + private fun handleAudioTrackError(result: Int) { + val errorMsg = when(result) { + AudioTrack.ERROR_INVALID_OPERATION -> "无效操作" + AudioTrack.ERROR_BAD_VALUE -> "参数错误" + AudioTrack.ERROR_DEAD_OBJECT -> "对象已销毁" + else -> "未知错误" + } + + // 对象已销毁时尝试重建 + if (result == AudioTrack.ERROR_DEAD_OBJECT) { + try { + audioTrack?.release() + initAudioTrack() + } catch (e: Exception) { + Log.e(TAG, "重建AudioTrack失败: ${e.message}") + throw e + } + } else { + Log.e(TAG, "AudioTrack错误: $errorMsg") + } + } + + /** + * 初始化AudioTrack + */ + private fun initAudioTrack() { + val minBufferSize = AudioTrack.getMinBufferSize( + SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT + ) + + if (minBufferSize == AudioTrack.ERROR || minBufferSize == AudioTrack.ERROR_BAD_VALUE) { + throw IllegalStateException("无法获取有效的音频缓冲区大小") + } + + // 使用较大的缓冲区提高稳定性 + val bufferSize = minBufferSize * 4 + + // 创建AudioTrack + audioTrack = if (Build.VERSION.SDK_INT >= 23) { // Android M (6.0) + AudioTrack.Builder() + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build() + ) + .setAudioFormat( + AudioFormat.Builder() + .setEncoding(AUDIO_FORMAT) + .setSampleRate(SAMPLE_RATE) + .setChannelMask(CHANNEL_CONFIG) + .build() + ) + .setBufferSizeInBytes(bufferSize) + .setTransferMode(AudioTrack.MODE_STREAM) + .build() + } else { + @Suppress("DEPRECATION") + AudioTrack( + AudioManager.STREAM_MUSIC, + SAMPLE_RATE, + CHANNEL_CONFIG, + AUDIO_FORMAT, + bufferSize, + AudioTrack.MODE_STREAM + ) + } + + // 检查初始化状态 + if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) { + throw IllegalStateException("AudioTrack初始化失败") + } + + // 开始播放 + audioTrack?.play() + } +} \ No newline at end of file diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt index 1a3447ffc..d86faa71f 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt @@ -16,6 +16,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import com.deep_voice.speech.tts.AudioDataListener import java.util.concurrent.ConcurrentLinkedQueue /** @@ -168,6 +172,7 @@ class BytedanceSpeechPlugin : FlutterPlugin { "disposeTTS" -> { disposeTTS(result) } + else -> { result.notImplemented() } @@ -235,33 +240,45 @@ class BytedanceSpeechPlugin : FlutterPlugin { bytedanceTTS?.release() // 创建新的TTS实例 - bytedanceTTS = BytedanceTTS( - context = context, - // 音频数据回调 - audioCallback = { audioData -> - sendTTSAudioData(audioData) - }, - // 完成回调 - completionCallback = { - sendTTSEvent("completed", null) - }, - // 错误回调 - errorCallback = { code, message -> - val params = HashMap() - params["code"] = code - params["message"] = message - sendTTSEvent("error", params) - }, - // 状态回调 - statusCallback = { status -> - val params = HashMap() - params["status"] = status - sendTTSEvent("status", params) + bytedanceTTS = BytedanceTTS(context) + + // 添加事件监听器 + bytedanceTTS?.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { + when (event.type) { + TtsEventType.SYNTHESIS_STARTED -> { + sendTTSEvent("started", null) + } + TtsEventType.SYNTHESIS_COMPLETED -> { + sendTTSEvent("completed", null) + } + TtsEventType.SYNTHESIS_CANCELED -> { + sendTTSEvent("canceled", null) + } + TtsEventType.ERROR -> { + val params = HashMap() + params["code"] = event.params["errorCode"] ?: "UNKNOWN_ERROR" + params["message"] = event.params["errorMessage"] ?: "未知错误" + sendTTSEvent("error", params) + } + } } - ) + }) + + // 添加音频数据监听器 + bytedanceTTS?.addAudioDataListener(object : AudioDataListener { + override fun onAudioData(data: ByteArray) { + sendTTSAudioData(data) + } + }) // 初始化TTS - val success = bytedanceTTS?.initialize(ttsAppId, ttsAppToken, language) ?: false + val success = bytedanceTTS?.initialize( + ttsAppId = ttsAppId, + ttsAppToken = ttsAppToken, + ttsResource = "", // 火山TTS不使用resource + language = language + ) ?: false if (success) { result.success(true) @@ -314,18 +331,16 @@ class BytedanceSpeechPlugin : FlutterPlugin { return } - scope.launch { - try { - val success = bytedanceTTS?.speakOnce(text) ?: false - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "合成文本异常: ${e.message}", e) - result.error( - "SPEAK_EXCEPTION", - "合成文本异常: ${e.message}", - null - ) - } + try { + val success = bytedanceTTS?.speakOnce(text) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "合成文本异常: ${e.message}", e) + result.error( + "SPEAK_EXCEPTION", + "合成文本异常: ${e.message}", + null + ) } } @@ -338,18 +353,16 @@ class BytedanceSpeechPlugin : FlutterPlugin { return } - scope.launch { - try { - val success = bytedanceTTS?.speakStream(text) ?: false - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "流式合成异常: ${e.message}", e) - result.error( - "STREAM_EXCEPTION", - "流式合成异常: ${e.message}", - null - ) - } + try { + val success = bytedanceTTS?.speakStream(text) ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "流式合成异常: ${e.message}", e) + result.error( + "STREAM_EXCEPTION", + "流式合成异常: ${e.message}", + null + ) } } @@ -362,18 +375,16 @@ class BytedanceSpeechPlugin : FlutterPlugin { return } - scope.launch { - try { - val success = bytedanceTTS?.flushStream() ?: false - result.success(success) - } catch (e: Exception) { - Log.e(TAG, "刷新流异常: ${e.message}", e) - result.error( - "FLUSH_EXCEPTION", - "刷新流异常: ${e.message}", - null - ) - } + try { + val success = bytedanceTTS?.flushStream() ?: false + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "刷新流异常: ${e.message}", e) + result.error( + "FLUSH_EXCEPTION", + "刷新流异常: ${e.message}", + null + ) } } @@ -386,18 +397,16 @@ class BytedanceSpeechPlugin : FlutterPlugin { return } - scope.launch { - try { - bytedanceTTS?.stop() - result.success(true) - } catch (e: Exception) { - Log.e(TAG, "停止TTS异常: ${e.message}", e) - result.error( - "STOP_EXCEPTION", - "停止TTS异常: ${e.message}", - null - ) - } + try { + val success = bytedanceTTS?.stop() ?: true + result.success(success) + } catch (e: Exception) { + Log.e(TAG, "停止TTS异常: ${e.message}", e) + result.error( + "STOP_EXCEPTION", + "停止TTS异常: ${e.message}", + null + ) } } @@ -418,6 +427,7 @@ class BytedanceSpeechPlugin : FlutterPlugin { ) } } + /** * 发送TTS事件到Flutter端 diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt index 6e508bcab..d202a36e4 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt @@ -2,8 +2,19 @@ package com.deep_voice.bytedance_speech import android.content.Context import android.util.Log +import com.deep_voice.speech.tts.AudioDataListener +import com.deep_voice.speech.tts.ITtsService +import com.deep_voice.speech.tts.TtsEvent +import com.deep_voice.speech.tts.TtsEventListener +import com.deep_voice.speech.tts.TtsEventType +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.CancellationException import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -11,24 +22,16 @@ import okhttp3.WebSocket import okhttp3.WebSocketListener import okio.ByteString import org.json.JSONObject -import java.io.ByteArrayOutputStream import java.util.UUID import java.util.concurrent.TimeUnit -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream +import java.util.concurrent.atomic.AtomicBoolean /** * 火山引擎大模型语音合成实现 * * 基于WebSocket协议实现双向流式TTS */ -class BytedanceTTS( - private val context: Context, - private val audioCallback: (ByteArray) -> Unit, - private val completionCallback: () -> Unit, - private val errorCallback: (String, String) -> Unit, - private val statusCallback: (String) -> Unit -) { +class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { companion object { private const val TAG = "BytedanceTTS" @@ -80,8 +83,23 @@ class BytedanceTTS( private const val STATUS_SPEAKING = "speaking" private const val STATUS_STOPPED = "stopped" private const val STATUS_ERROR = "error" + + // 连接控制常量 + private const val MAX_CONNECTION_ATTEMPTS = 3 + private const val CONNECTION_TIMEOUT_MS = 10000 // 10秒 } + // 协程相关 + private val job = Job() + override val coroutineContext = Dispatchers.IO + job + + // 事件监听器列表 + private val eventListeners = mutableListOf() + private val audioDataListeners = mutableListOf() + + // 内部音频播放器 + private val audioPlayer = BytedanceAudioPlayer() + // WebSocket连接 private var webSocket: WebSocket? = null private val client: OkHttpClient @@ -94,8 +112,8 @@ class BytedanceTTS( // TTS配置参数 private var appId: String = "" private var accessToken: String = "" - private var speaker: String = "zh_female_shuangkuaisisi_moon_bigtts" // 默认语音,使用Java示例的音色 - private var format: String = "mp3" // 使用mp3格式 + private var speaker: String = "zh_female_shuangkuaisisi_emo_v2_mars_bigtts" // 默认语音,使用Java示例的音色 + private var format: String = "pcm" // 使用mp3格式 private var sampleRate: Int = 24000 // 使用24kHz采样率 private var enableTimestamp: Boolean = false private var language: String = "zh-CN" @@ -103,6 +121,16 @@ class BytedanceTTS( // 流式处理状态 private var isStreamMode = false private var currentStatus = STATUS_STOPPED + private var isSpeaking = false + + // 连接相关状态 + private var connectionAttempts = 0 + private var isConnecting = AtomicBoolean(false) + private var lastConnectionAttemptTime = 0L + + // 在类级别添加文本流通道 + private val textChannel = Channel(Channel.BUFFERED) + private var textProcessingJob: Job? = null init { // 配置OkHttp客户端 @@ -113,61 +141,161 @@ class BytedanceTTS( .build() // 生成会话ID - sessionId = UUID.randomUUID().toString().replace("-", "") - Log.d(TAG, "BytedanceTTS实例化完成,会话ID: $sessionId") + // sessionId = UUID.randomUUID().toString().replace("-", "") + Log.d(TAG, "BytedanceTTS实例化完成!") + + // 初始化音频播放器回调 + initAudioPlayerCallbacks() + + // 启动文本处理协程 + startTextProcessing() + } + + /** + * 初始化音频播放器回调 + */ + private fun initAudioPlayerCallbacks() { + // 添加播放器作为音频数据监听器 + addAudioDataListener(audioPlayer) + + audioPlayer.setOnPlayStarted { + Log.d(TAG, "播放开始") + isSpeaking = true + updateStatus(STATUS_SPEAKING) + notifyEvent(TtsEventType.SYNTHESIS_STARTED) + } + + // 设置播放完成回调 + audioPlayer.setOnPlayCompleted { + Log.d(TAG, "播放结束") + isSpeaking = false + updateStatus(if (isStreamMode) STATUS_READY else STATUS_STOPPED) + notifyEvent(TtsEventType.SYNTHESIS_COMPLETED) + } + + // 设置错误回调 + audioPlayer.setOnError { errorMsg -> + isSpeaking = false + updateStatus(STATUS_ERROR) + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "PLAYER_ERROR", "errorMessage" to errorMsg)) + } + } + + /** + * 启动文本处理协程 + */ + private fun startTextProcessing() { + textProcessingJob?.cancel() + textProcessingJob = launch { + try { + for (text in textChannel) { + try { + sendStreamTextRequest(text) + } catch (e: Exception) { + Log.e(TAG, "文本处理错误: ${e.message}") + } + } + } catch (e: CancellationException) { + // 协程被取消,正常情况 + } catch (e: Exception) { + Log.e(TAG, "文本处理异常: ${e.message}") + } + } + } + + /** + * 停止语音合成 + */ + override fun stop(): Boolean { + try { + audioPlayer.stop() + textProcessingJob?.cancel() + + launch { + if (isSessionStarted) { + finishSession() + } + isSpeaking = false // 显式设置,因为这是强制停止 + updateStatus(STATUS_STOPPED) + } + return true + } catch (e: Exception) { + Log.e(TAG, "停止合成失败: ${e.message}") + return false + } } /** * 初始化火山语音服务 */ - suspend fun initialize(ttsAppId: String, ttsAppToken: String, language: String = "zh-CN"): Boolean = withContext(Dispatchers.IO) { + override fun initialize( + ttsAppId: String, + ttsAppToken: String, + ttsResource: String, + language: String + ): Boolean { try { updateStatus(STATUS_INITIALIZING) - // 设置必要参数 - this@BytedanceTTS.appId = ttsAppId // 火山引擎中的应用ID - this@BytedanceTTS.accessToken = ttsAppToken // 火山引擎访问令牌 - this@BytedanceTTS.language = language - - // 释放旧连接 - release() - - // 建立新连接 - connect() + // 设置基本参数 + this.appId = ttsAppId + this.accessToken = ttsAppToken + this.language = language - // 等待连接和会话建立 - var attempts = 0 - while (!isSessionStarted && attempts < 10) { - delay(500) // 增加等待时间 - attempts++ - Log.d(TAG, "等待会话建立...尝试 $attempts/10") - } + // 重置连接状态 + connectionAttempts = 0 + isConnecting.set(false) + lastConnectionAttemptTime = 0 - if (!isSessionStarted) { - Log.e(TAG, "初始化超时: 无法建立会话") - errorCallback("INIT_TIMEOUT", "初始化超时: 无法在5秒内建立会话") - return@withContext false - } + // 释放旧连接 + webSocket?.cancel() + webSocket = null + isConnected = false + isSessionStarted = false - Log.d(TAG, "字节跳动TTS初始化成功,会话ID: $sessionId") - true + return true } catch (e: Exception) { - Log.e(TAG, "初始化失败: ${e.message}", e) - errorCallback("INIT_ERROR", e.message ?: "初始化失败") + Log.e(TAG, "初始化失败: ${e.message}") updateStatus(STATUS_ERROR) - false + return false } } + /** + * 重置服务状态 + */ + fun reset() { + stop() + textProcessingJob?.cancel() + + connectionAttempts = 0 + isConnecting.set(false) + lastConnectionAttemptTime = 0 + + webSocket?.cancel() + webSocket = null + + isConnected = false + isSessionStarted = false + isSpeaking = false + + updateStatus(STATUS_STOPPED) + startTextProcessing() + } + /** * 设置语音角色 */ - fun setVoice(voiceName: String): Boolean { + override fun setVoice(voiceName: String): Boolean { try { speaker = voiceName return true } catch (e: Exception) { - errorCallback("VOICE_ERROR", e.message ?: "设置语音角色失败") + val errorMsg = e.message ?: "设置语音角色失败" + notifyEvent(TtsEventType.ERROR, mapOf( + "errorCode" to "VOICE_ERROR", + "errorMessage" to errorMsg + )) return false } } @@ -175,140 +303,298 @@ class BytedanceTTS( /** * 单次播放(非流式) */ - suspend fun speakOnce(text: String): Boolean = withContext(Dispatchers.IO) { + override fun speakOnce(text: String): Boolean { try { if (text.isEmpty()) { - errorCallback("PARAM_ERROR", "合成文本不能为空") - return@withContext false + return false } isStreamMode = false - // 如果没有连接或会话,重新初始化 - if (webSocket == null || !isConnected) { - connect() - // 等待连接和会话建立 - var attempts = 0 - while (!isSessionStarted && attempts < 10) { - delay(300) - attempts++ + launch { + if (!ensureSessionReady()) { + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "SESSION_ERROR", + "errorMessage" to "无法建立会话")) + return@launch } - if (!isSessionStarted) { - errorCallback("SESSION_ERROR", "无法建立会话") - return@withContext false - } + sendTextRequest(text) } - // 发送文本进行合成 - sendTextRequest(text) - return@withContext true + return true } catch (e: Exception) { - Log.e(TAG, "语音合成失败: ${e.message}", e) - errorCallback("SYNTH_ERROR", e.message ?: "语音合成失败") + Log.e(TAG, "语音合成失败: ${e.message}") updateStatus(STATUS_ERROR) - false + return false } } /** * 流式播放开始 */ - suspend fun speakStream(text: String): Boolean = withContext(Dispatchers.IO) { + override fun speakStream(text: String): Boolean { try { if (text.isEmpty()) { - return@withContext true // 空文本直接返回成功 + return true } - Log.d(TAG, "开始流式合成,text: $text") isStreamMode = true // 确保会话已就绪 - ensureSessionReady() + if (!isSessionStarted && !ensureSessionReady()) { + Log.e(TAG, "流式播放失败:无法建立会话") + return false + } - // 发送文本进行合成,但不自动结束会话 - sendStreamTextRequest(text) - updateStatus(STATUS_SPEAKING) - return@withContext true + // 如果文本处理协程不活跃,重新启动它 + if (textProcessingJob == null || textProcessingJob?.isActive == false) { + startTextProcessing() + } + // Log.d(TAG, "speakStream $sessionId, $text ") + + textChannel.trySend(text) + return true } catch (e: Exception) { - Log.e(TAG, "流式合成失败: ${e.message}", e) - errorCallback("STREAM_ERROR", e.message ?: "流式合成失败") + Log.e(TAG, "流式合成失败: ${e.message}") updateStatus(STATUS_ERROR) - false + return false } } /** * 结束流式播放 */ - suspend fun flushStream(): Boolean = withContext(Dispatchers.IO) { + override fun flushStream(): Boolean { try { - if (!isStreamMode) { - return@withContext true - } - - if (isSessionStarted) { - finishSession() + if (!isStreamMode) return true + // Log.d(TAG, "flushStream: $sessionId") + launch { + withContext(Dispatchers.IO) { + delay(300) // 确保现有文本处理完成 + } + + if (isSessionStarted) { + isSessionStarted = false + finishSession() + } } - isStreamMode = false - updateStatus(STATUS_STOPPED) - return@withContext true + return true } catch (e: Exception) { - Log.e(TAG, "结束流式合成失败: ${e.message}", e) - errorCallback("FLUSH_ERROR", e.message ?: "结束流式合成失败") - false + Log.e(TAG, "结束流式合成失败: ${e.message}") + return false } } /** - * 停止语音合成 + * 释放资源 */ - suspend fun stop() = withContext(Dispatchers.IO) { - if (isSessionStarted) { - finishSession() + override fun release() { + Log.d(TAG, "BytedanceTTS release called.") + + // 关闭文本通道 + textChannel.close() + textProcessingJob?.cancel() + + job.cancel() + + // 释放音频播放器资源 + audioPlayer.release() + + // 重置所有状态 + reset() + + // 确保WebSocket连接完全关闭 + webSocket?.cancel() + webSocket = null + } + + /** + * 添加TTS事件监听器 + */ + override fun addListener(listener: TtsEventListener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener) } - updateStatus(STATUS_STOPPED) } /** - * 释放资源 + * 移除TTS事件监听器 */ - fun release() { - webSocket?.cancel() - webSocket = null - isConnected = false - isSessionStarted = false - updateStatus(STATUS_STOPPED) + override fun removeListener(listener: TtsEventListener) { + eventListeners.remove(listener) + } + + /** + * 添加音频数据监听器 + */ + override fun addAudioDataListener(listener: AudioDataListener) { + if (!audioDataListeners.contains(listener)) { + audioDataListeners.add(listener) + } + } + + /** + * 移除音频数据监听器 + */ + override fun removeAudioDataListener(listener: AudioDataListener) { + audioDataListeners.remove(listener) + } + + /** + * 通知事件处理 + */ + private fun notifyEvent(eventType: TtsEventType, params: Map = emptyMap()) { + val event = TtsEvent(eventType, params) + val listeners = ArrayList(eventListeners) + + for (listener in listeners) { + try { + listener.onEvent(event) + } catch (e: Exception) { + Log.e(TAG, "事件回调异常: ${e.message}") + } + } + + } + + /** + * 通知音频数据 + */ + private fun notifyAudioData(data: ByteArray) { + launch(Dispatchers.Main) { + for (listener in audioDataListeners) { + try { + listener.onAudioData(data) + } catch (e: Exception) { + Log.e(TAG, "音频数据回调异常: ${e.message}") + } + } + } } /** * 确保会话已就绪 */ - private suspend fun ensureSessionReady() { - // 如果没有连接或会话,重新初始化 - if (webSocket == null || !isConnected) { - connect() - // 等待连接和会话建立 + private fun ensureSessionReady(): Boolean { + // 如果已连接和会话已开始,直接返回成功 + if (isConnected && isSessionStarted) { + return true + } + + // 如果已连接但会话未开始,只需要开始会话 + if (isConnected && !isSessionStarted) { + Log.d(TAG, "WebSocket已连接,正在启动新会话...") + return startSessionOnly() + } + + // 如果正在连接中,等待连接完成 + if (isConnecting.get()) { + Log.d(TAG, "已有连接正在进行中,等待其完成...") + // 等待连接完成,最多5秒 var attempts = 0 - while (!isSessionStarted && attempts < 10) { - delay(300) - attempts++ + while (isConnecting.get() && attempts < 10) { + try { + Thread.sleep(500) + attempts++ + } catch (e: InterruptedException) { + return false + } } - if (!isSessionStarted) { - throw Exception("无法建立会话") + // 连接完成后,检查是否需要启动会话 + if (isConnected && !isSessionStarted) { + return startSessionOnly() } + + return isConnected + } + + // 检查连接重试次数限制 + if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) { + Log.e(TAG, "已达到最大连接尝试次数($MAX_CONNECTION_ATTEMPTS),不再尝试连接") + return false + } + + // 检查连接间隔时间 - 避免短时间内频繁重连 + val currentTime = System.currentTimeMillis() + if (currentTime - lastConnectionAttemptTime < 2000 && lastConnectionAttemptTime > 0) { + Log.w(TAG, "连接请求太频繁,等待冷却时间...") + Thread.sleep(2000) } + + // 更新连接计数和时间 + connectionAttempts++ + lastConnectionAttemptTime = System.currentTimeMillis() + + // 尝试建立连接 + isConnecting.set(true) + try { + connect() + + // 等待连接建立,设置超时 + val startWaitTime = System.currentTimeMillis() + while (!isConnected && System.currentTimeMillis() - startWaitTime < CONNECTION_TIMEOUT_MS) { + try { + Thread.sleep(500) + // 如果连接已失败,提前退出等待 + if (!isConnecting.get() && !isConnected) { + Log.e(TAG, "连接已失败,终止等待") + break + } + } catch (e: InterruptedException) { + isConnecting.set(false) + return false + } + } + + // 连接成功后,启动会话 + if (isConnected && !isSessionStarted) { + startSessionOnly() + } + } finally { + isConnecting.set(false) + } + + // 检查连接结果 + if (!isConnected) { + Log.e(TAG, "连接超时: 无法在${CONNECTION_TIMEOUT_MS/1000}秒内建立连接,连接尝试次数: $connectionAttempts") + return false + } + + // 连接成功,重置尝试计数 + connectionAttempts = 0 + return true } /** - * 用于暂停执行的辅助函数 + * 只启动会话,不重新建立WebSocket连接 */ - private suspend fun delay(ms: Long) = withContext(Dispatchers.IO) { + private fun startSessionOnly(): Boolean { + if (!isConnected || webSocket == null) { + Log.e(TAG, "无法启动会话:WebSocket未连接") + return false + } + + if (isSessionStarted) { + return true + } + try { - Thread.sleep(ms) - } catch (e: InterruptedException) { - // 忽略中断异常 + // 重新生成会话ID + sessionId = UUID.randomUUID().toString().replace("-", "") + Log.d(TAG, "启动新会话,会话ID: $sessionId") + + // 发送开始会话请求 + startTtsSession(webSocket!!) + + // 立即将会话视为已启动,不等待服务器确认 + isSessionStarted = true + updateStatus(STATUS_READY) + + return true + } catch (e: Exception) { + Log.e(TAG, "启动会话失败: ${e.message}", e) + return false } } @@ -317,42 +603,40 @@ class BytedanceTTS( */ private fun updateStatus(status: String) { currentStatus = status - statusCallback(status) } /** * 建立WebSocket连接 */ private fun connect() { + webSocket?.cancel() + webSocket = null + val url = "wss://openspeech.bytedance.com/api/v3/tts/bidirection" + val connectId = UUID.randomUUID().toString() val request = Request.Builder() .url(url) .header("X-Api-App-Key", appId) .header("X-Api-Access-Key", accessToken) .header("X-Api-Resource-Id", "volc.service_type.10029") - .header("X-Api-Connect-Id", UUID.randomUUID().toString()) + .header("X-Api-Connect-Id", connectId) .build() - Log.d(TAG, "正在连接WebSocket: $url, appId: $appId") - Log.d(TAG, "当前语音角色配置: speaker=$speaker, format=$format, sampleRate=$sampleRate") + isConnected = false + isSessionStarted = false webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { - val logId = response.header("X-Tt-Logid") - Log.d(TAG, "TTS WebSocket连接成功, X-Tt-Logid: $logId") - - // 发送开始连接事件 + isConnected = true startConnection(webSocket) } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { try { val data = bytes.toByteArray() - Log.d(TAG, "收到二进制消息,大小: ${data.size} 字节") val response = parseResponse(data) - // 处理事件 when (response.event) { EVENT_CONNECTION_FAILED, EVENT_SESSION_FAILED -> { val errorMsg = if (response.responseMetaJson.isNotEmpty()) { @@ -360,99 +644,68 @@ class BytedanceTTS( } else { "连接或会话失败" } - Log.e(TAG, "连接或会话失败: $errorMsg, 事件类型: ${response.event}") - errorCallback("TTS_ERROR", errorMsg) + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "TTS_ERROR", + "errorMessage" to errorMsg)) webSocket.close(1000, "Error") updateStatus(STATUS_ERROR) - } - - EVENT_CONNECTION_STARTED -> { - isConnected = true - Log.d(TAG, "连接已建立,开始TTS会话") - // 开始TTS会话 - startTtsSession(webSocket) + isSpeaking = false // 错误情况下显式设置 + isSessionStarted = false + isConnected = false + isConnecting.set(false) } EVENT_SESSION_STARTED -> { - isSessionStarted = true - Log.d(TAG, "TTS会话已开始,会话ID: $sessionId") - updateStatus(STATUS_READY) + connectionAttempts = 0 } EVENT_TTS_RESPONSE -> { - // 音频数据 if (response.messageType == AUDIO_ONLY_RESPONSE && response.payload.isNotEmpty()) { - Log.d(TAG, "收到音频数据,大小: ${response.payload.size} 字节") - try { - // 使用同步方式调用回调,防止协程上下文切换导致的问题 - if (response.payload.size > 0) { - Log.d(TAG, "准备调用音频回调,数据大小: ${response.payload.size}") - audioCallback(response.payload) - Log.d(TAG, "音频回调调用成功") - } else { - Log.w(TAG, "收到空音频数据") - } - } catch (e: Exception) { - Log.e(TAG, "调用音频回调函数失败: ${e.message}", e) - errorCallback("AUDIO_CALLBACK_ERROR", "音频处理失败: ${e.message}") - } - } else { - Log.d(TAG, "收到TTS响应,但没有音频数据 messageType=${response.messageType}, 有效载荷大小=${response.payload.size}") + notifyAudioData(response.payload) } } - EVENT_TTS_SENTENCE_START -> { - Log.d(TAG, "句子合成开始") - } - - EVENT_TTS_SENTENCE_END -> { - Log.d(TAG, "句子合成结束") - } - EVENT_CONNECTION_FINISHED -> { isConnected = false isSessionStarted = false - Log.d(TAG, "连接已结束") - updateStatus(STATUS_STOPPED) - completionCallback() + isConnecting.set(false) webSocket.close(1000, "Connection finished") } EVENT_SESSION_FINISHED -> { isSessionStarted = false - Log.d(TAG, "会话已结束,流模式: $isStreamMode") - if (!isStreamMode) { + + if (!isStreamMode) { finishConnection(webSocket) - } else { - // 流模式下保持连接 - updateStatus(STATUS_READY) } } - - else -> { - Log.d(TAG, "收到未处理的事件类型: ${response.event}") - } } } catch (e: Exception) { - Log.e(TAG, "解析响应失败: ${e.message}", e) - errorCallback("PARSE_ERROR", e.message ?: "解析响应失败") + Log.e(TAG, "解析响应失败: ${e.message}") updateStatus(STATUS_ERROR) + isSpeaking = false // 错误情况下显式设置 + isConnecting.set(false) } } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { isConnected = false isSessionStarted = false - Log.e(TAG, "连接失败: ${t.message}", t) - errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") + isSpeaking = false + isConnecting.set(false) + + Log.e(TAG, "WebSocket连接失败: ${t.message}") + notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "CONNECTION_ERROR", + "errorMessage" to (t.message ?: "连接失败"))) updateStatus(STATUS_ERROR) } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { isConnected = false isSessionStarted = false + isSpeaking = false + isConnecting.set(false) updateStatus(STATUS_STOPPED) - Log.d(TAG, "TTS WebSocket连接关闭: $code, $reason") + notifyEvent(TtsEventType.SYNTHESIS_CANCELED) } }) } @@ -533,15 +786,14 @@ class BytedanceTTS( reqParams.put("speaker", speaker) val audioParams = JSONObject() - audioParams.put("format", "mp3") // 尝试改用mp3格式 - audioParams.put("sample_rate", 24000) // 提高采样率 + audioParams.put("format", format) + audioParams.put("sample_rate", sampleRate) audioParams.put("enable_timestamp", enableTimestamp) reqParams.put("audio_params", audioParams) jsonObject.put("req_params", reqParams) val payload = jsonObject.toString().toByteArray() - Log.d(TAG, "开始TTS会话请求: ${jsonObject.toString()}") sendEvent(webSocket, header, optional, payload) } @@ -574,12 +826,15 @@ class BytedanceTTS( jsonObject.put("namespace", "BidirectionalTTS") val reqParams = JSONObject() - reqParams.put("text", text) + // 将#和*替换为空格 + val processedText = text.replace("#", " ").replace("*", " ") + reqParams.put("text", processedText) reqParams.put("speaker", speaker) val audioParams = JSONObject() - audioParams.put("format", "mp3") // 尝试改用mp3格式 - audioParams.put("sample_rate", 24000) // 提高采样率 + audioParams.put("format", format) + audioParams.put("sample_rate", sampleRate) + audioParams.put("emotion", "happy") reqParams.put("audio_params", audioParams) jsonObject.put("req_params", reqParams) @@ -588,9 +843,6 @@ class BytedanceTTS( Log.d(TAG, "发送文本合成请求: ${jsonObject.toString()}") sendEvent(ws, header, optional, payload) - - // 发送完文本后结束会话 - updateStatus(STATUS_SPEAKING) } } @@ -622,18 +874,21 @@ class BytedanceTTS( jsonObject.put("namespace", "BidirectionalTTS") val reqParams = JSONObject() - reqParams.put("text", text) + // 将#和*替换为空格 + val processedText = text.replace("#", " ").replace("*", " ") + reqParams.put("text", processedText) reqParams.put("speaker", speaker) val audioParams = JSONObject() - audioParams.put("format", "mp3") // 尝试改用mp3格式 - audioParams.put("sample_rate", 24000) // 提高采样率 + audioParams.put("format", format) + audioParams.put("sample_rate", sampleRate) + audioParams.put("emotion", "happy") reqParams.put("audio_params", audioParams) jsonObject.put("req_params", reqParams) val payload = jsonObject.toString().toByteArray() - Log.d(TAG, "发送流式文本合成请求: ${jsonObject.toString()}") + // Log.d(TAG, "发送流式文本合成请求: ${jsonObject.toString()}") sendEvent(ws, header, optional, payload) // 流式模式下不自动结束会话 @@ -663,6 +918,9 @@ class BytedanceTTS( val payload = "{}".toByteArray() sendEvent(ws, header, optional, payload) + Log.d(TAG, "发送结束会话请求,会话ID: $sessionId") + // 不在这里设置 isSessionStarted = false + // 等待服务器的 SESSION_FINISHED 事件来设置 } } @@ -691,10 +949,9 @@ class BytedanceTTS( System.arraycopy(payload, 0, requestBytes, offset, payload.size) - val success = webSocket.send(ByteString.of(*requestBytes)) - Log.d(TAG, "发送WebSocket消息: 消息类型=${header.messageType}, 事件=${optional.event}, 大小=${requestBytes.size}字节, 成功=$success") + webSocket.send(ByteString.of(*requestBytes)) } catch (e: Exception) { - Log.e(TAG, "发送WebSocket消息失败: ${e.message}", e) + Log.e(TAG, "发送WebSocket消息失败: ${e.message}") } } @@ -972,4 +1229,5 @@ class BytedanceTTS( return result } } + } \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/pubspec.yaml b/local_plugins/classic_bluetooth/pubspec.yaml index c618200f1..6e4012e2f 100644 --- a/local_plugins/classic_bluetooth/pubspec.yaml +++ b/local_plugins/classic_bluetooth/pubspec.yaml @@ -9,8 +9,6 @@ environment: dependencies: flutter: sdk: flutter - deepvoice_utils: - path: ../deepvoice_utils flutter: plugin: diff --git a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock index 4e477b3ee9982efc49e30adc213d1b0df26cce1c..cadc63f5e6ce4bb1b733f316c64dffab53aa7ab0 100644 GIT binary patch literal 17 UcmZQRo$b{-r|q*H0|cl605?(uiU0rr literal 17 UcmZQRo$b{-r|q*H0|Y1m05?tqh5!Hn diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 48bc9bac10dbdc56e568879768292aa37ce50fe6..e92059b6ad44898ba790f66f5aa21ce10ee005a8 100644 GIT binary patch literal 17 TcmZQ}Dp$CADo{Lz0RqebDLDgn literal 17 TcmZQ}Dp$CADo{Lz0Rr>@DKP_X diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index d3c4e6543..ccc9213ad 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Sun May 11 13:39:53 IST 2025 +#Fri May 16 09:26:08 IST 2025 gradle.version=8.10 diff --git a/local_plugins/speech/android/build.gradle.kts b/local_plugins/speech/android/build.gradle.kts index f5bbc9337..4e5186a27 100644 --- a/local_plugins/speech/android/build.gradle.kts +++ b/local_plugins/speech/android/build.gradle.kts @@ -9,7 +9,7 @@ plugins { android { // 命名空间,对应你插件的包名(需与代码内包名保持一致) - namespace = "com.yunqiinnovation.open_ai_service" + namespace = "com.deep_voice.speech" // 目标 SDK 版本 compileSdk = 35 diff --git a/local_plugins/speech/android/settings.gradle.kts b/local_plugins/speech/android/settings.gradle.kts index 1c33f4971..efad09063 100644 --- a/local_plugins/speech/android/settings.gradle.kts +++ b/local_plugins/speech/android/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "open_ai_service" \ No newline at end of file +rootProject.name = "speech" \ No newline at end of file diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt index a129fd0aa..91c8945d6 100644 --- a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt @@ -15,7 +15,7 @@ interface ITtsService { * @param language 语言代码,如"zh-CN" * @return 是否初始化成功 */ - suspend fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String = "", language: String = "zh-CN"): Boolean + fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String = "", language: String = "zh-CN"): Boolean /** * 设置语音角色 @@ -31,7 +31,7 @@ interface ITtsService { * @param text 要合成的文本 * @return 是否成功开始合成 */ - suspend fun speakOnce(text: String): Boolean + fun speakOnce(text: String): Boolean /** * 流式合成 @@ -40,21 +40,21 @@ interface ITtsService { * @param text 要合成的文本片段 * @return 是否成功处理 */ - suspend fun speakStream(text: String): Boolean + fun speakStream(text: String): Boolean /** * 刷新并播放流式文本缓冲区中的剩余内容 * * @return 是否成功处理 */ - suspend fun flushStream(): Boolean + fun flushStream(): Boolean /** * 停止当前合成和播放 * * @return 是否成功停止 */ - suspend fun stop(): Boolean + fun stop(): Boolean /** * 释放资源 diff --git a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt index 036344edd..cb69a849d 100644 --- a/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt +++ b/local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt @@ -13,15 +13,6 @@ enum class TtsEventType { /** 合成取消 */ SYNTHESIS_CANCELED, - /** 开始播放 */ - STARTED, - - /** 播放完成 */ - COMPLETED, - - /** 播放取消 */ - CANCELED, - /** 发生错误 */ ERROR } @@ -31,7 +22,8 @@ enum class TtsEventType { * * @param type 事件类型 * @param params 事件参数,根据不同的事件类型包含不同的内容 - * 错误事件通常包含 "errorCode" 和 "errorMessage" + * - ERROR: 包含 "errorCode" 和 "errorMessage" + * - CANCELED: 包含 "reason" 和 "errorDetails" */ data class TtsEvent( val type: TtsEventType, diff --git a/pubspec.yaml b/pubspec.yaml index cd9173c35..0e74b1bee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,8 +81,6 @@ dependencies: path: local_plugins/jl_opus ble_service: path: local_plugins/ble_service - deepvoice_utils: - path: local_plugins/deepvoice_utils classic_bluetooth: path: local_plugins/classic_bluetooth agent_service: