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: