wolfplus 1 year ago
parent
commit
bae5d70229
  1. 20
      android/app/build.gradle.kts
  2. 2
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  3. 9
      android/settings.gradle.kts
  4. 2
      lib/data/services/ble_manager.dart
  5. 63
      lib/modules/agent/controllers/agent_controller.dart
  6. 2
      lib/modules/pairing/controllers/pairing_controller.dart
  7. 5
      local_plugins/agent_service/android/build.gradle.kts
  8. 13
      local_plugins/agent_service/android/settings.gradle.kts
  9. 343
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  10. 30
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  11. 28
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  12. 57
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  13. 14
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt
  14. 2
      local_plugins/azure_speech/android/build.gradle.kts
  15. 51
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  16. 11
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  17. 1
      local_plugins/ble_service/android/build.gradle.kts
  18. 74
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  19. 15
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt
  20. 180
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  21. 11
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  22. 13
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt
  23. 2
      local_plugins/bytedance_speech/android/build.gradle.kts
  24. 2
      local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml
  25. 170
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt
  26. 560
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt
  27. 221
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt
  28. 5
      local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt
  29. 1
      local_plugins/deepvoice_utils/android/settings.gradle.kts
  30. 7
      local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml
  31. 33
      local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt
  32. 237
      local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt
  33. 4
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h
  34. 12
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m
  35. 30
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift
  36. 286
      local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift
  37. 11
      local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift
  38. 54
      local_plugins/deepvoice_utils/ios/README.md
  39. 19
      local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec
  40. 3
      local_plugins/deepvoice_utils/lib/deepvoice_utils.dart
  41. 31
      local_plugins/deepvoice_utils/lib/src/file_logger.dart
  42. 27
      local_plugins/deepvoice_utils/pubspec.yaml
  43. 1
      local_plugins/music_service/android/build.gradle
  44. 25
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  45. 4
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  46. 22
      local_plugins/speech/android/build.gradle.kts
  47. 1
      local_plugins/speech/android/settings.gradle.kts
  48. 10
      local_plugins/speech/android/src/main/AndroidManifest.xml
  49. 52
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt
  50. 90
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt
  51. 92
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt
  52. 63
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt
  53. 42
      local_plugins/volcano_speech/android/build.gradle.kts
  54. 134
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt
  55. 20
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt
  56. 140
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt
  57. 4
      pubspec.yaml

20
android/app/build.gradle.kts

@ -119,28 +119,8 @@ android {
} }
dependencies { 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") 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 { flutter {

2
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.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private val TAG = "MainActivity" private val TAG = "MainActivity"

9
android/settings.gradle.kts

@ -37,9 +37,11 @@ include(":chat_storage")
include(":jieli") include(":jieli")
include(":spotify_sdk") include(":spotify_sdk")
include(":ble_service") include(":ble_service")
include(":deepvoice_utils")
include(":classic_bluetooth") include(":classic_bluetooth")
include(":agent_service") include(":agent_service")
include(":volcano_speech")
include(":bytedance_speech")
include(":speech")
// 设置azure_speech项目的路径 // 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/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(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android") project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android")
project(":ble_service").projectDir = file("../local_plugins/ble_service/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(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/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")

2
lib/data/services/ble_manager.dart

@ -497,6 +497,8 @@ class BleManager extends GetxService {
'openaiModel': openaiModel, 'openaiModel': openaiModel,
'systemPrompt': systemPrompt, 'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData), 'mcpServer': jsonEncode(jsonData),
'volcanoAppId': '2316081933',
'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En',
}; };
// 记录日志(不记录敏感信息) // 记录日志(不记录敏感信息)

63
lib/modules/agent/controllers/agent_controller.dart

@ -530,28 +530,51 @@ final responseId = event.data['responseId'] ?? '';
// 检查麦克风权限 // 检查麦克风权限
Future<bool> _checkMicrophonePermission() async { Future<bool> _checkMicrophonePermission() async {
try { try {
PermissionStatus status = await Permission.microphone.status; // 创建待请求权限列表
List<Permission> permissionsToRequest = [];
// 如果已授权,直接返回 true if (Platform.isAndroid) {
if (status.isGranted) { // 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; return true;
} }
// 如果被永久拒绝,提示用户前往设置页面 // 处理特定权限被永久拒绝的情况
if (status.isPermanentlyDenied) { if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true); _showPermissionDialog(true);
return false; return false;
} }
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 // 普通拒绝情况
if (status.isDenied) { if (deniedPermission != null) {
status = await Permission.microphone.request();
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false); _showPermissionDialog(false);
return false;
}
} }
return false; return false;
@ -561,6 +584,20 @@ final responseId = event.data['responseId'] ?? '';
} }
} }
// 获取Android SDK版本
Future<int> _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) { void _showPermissionDialog(bool isPermanent) {
Get.dialog( Get.dialog(

2
lib/modules/pairing/controllers/pairing_controller.dart

@ -186,7 +186,7 @@ class PairingController extends GetxController {
if (Platform.isAndroid) { if (Platform.isAndroid) {
permissionsToRequest.add(Permission.locationWhenInUse); permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone); // permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) { if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect); permissionsToRequest.add(Permission.bluetoothConnect);

5
local_plugins/agent_service/android/build.gradle.kts

@ -36,7 +36,6 @@ android {
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.core:core-ktx:1.8.0") 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.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.media:media:1.6.0") implementation("androidx.media:media:1.6.0")
@ -45,8 +44,10 @@ dependencies {
// 依赖本地其他插件 // 依赖本地其他插件
add("compileOnly", project(":azure_speech")) add("compileOnly", project(":azure_speech"))
add("compileOnly", project(":open_ai_service")) add("compileOnly", project(":open_ai_service"))
add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage")) add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service")) add("compileOnly", project(":ble_service"))
add("compileOnly", project(":music_service")) add("compileOnly", project(":music_service"))
add("compileOnly", project(":volcano_speech"))
add("compileOnly", project(":bytedance_speech"))
add("compileOnly", project(":speech"))
} }

13
local_plugins/agent_service/android/settings.gradle.kts

@ -1,15 +1,2 @@
rootProject.name = "agent_service" 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")

343
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.JSONArray
import org.json.JSONObject import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Collections import java.util.Collections
@ -18,6 +17,11 @@ import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R import com.yunqiinnovation.agent_service.R
import android.os.Handler import android.os.Handler
import android.os.Looper 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 lateinit var context: Context
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>()) private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// 配置参数
private var azureSpeechKey: String = "" // TTS引擎类型
private var azureSpeechRegion: String = "" enum class TtsEngineType {
private var openaiApiKey: String = "" AZURE,
private var openaiBaseUrl: String = "" VOLCANO,
private var openaiModel: String = "gpt-3.5-turbo" BYTEDANCE
}
// 当前使用的TTS引擎类型,默认为Azure
private var currentTtsEngine = TtsEngineType.AZURE
// Azure服务 // Azure服务
private var azureAsrHelper: AzureAsrHelper? = null private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// 统一的TTS服务接口
private var ttsService: ITtsService? = null
// OpenAI服务 // OpenAI服务
private lateinit var openAIService: OpenAIService private lateinit var openAIService: OpenAIService
@ -129,13 +140,6 @@ object AgentService : CoroutineScope {
this.chatStorageHelper = ChatStorageHelper(context) this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(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 val customSystemPrompt = config["systemPrompt"] as? String
@ -145,66 +149,136 @@ object AgentService : CoroutineScope {
initSystemPrompt() initSystemPrompt()
} }
// 检查必要参数
if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) { // 如果TTS参数为空,使用ASR相同参数
FileLogger.e(TAG, "初始化失败:关键配置参数缺失") if (ttsAppId.isEmpty()) ttsAppId = azureSpeechRegion
return false if (ttsAppToken.isEmpty()) ttsAppToken = azureSpeechKey
}
// 初始化Azure ASR // 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply { azureAsrHelper = AzureAsrHelper(context).apply {
initialize( initialize(
subscriptionKey = azureSpeechKey, subscriptionKey = config["azureSpeechKey"],
region = azureSpeechRegion, region = config["azureSpeechRegion"],
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE else AzureAsrHelper.AudioSourceType.MICROPHONE
) )
} }
// 初始化Azure TTS // 根据选择的TTS引擎类型初始化TTS服务
azureTtsHelper = AzureTtsHelper(context).apply { initializeTtsEngine(config)
initialize(
subscriptionKey = azureSpeechKey, // 初始化OpenAI服务
region = azureSpeechRegion 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<String, Any>) {
// 停止当前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回调 // 创建TTS服务
setTtsCallback(object : AzureTtsHelper.TtsCallback { ttsService = SpeechServiceFactory.createTtsService(context, serviceConfig)
override fun onSynthesisStarted() {
// 使用协程启动初始化,避免阻塞主线程
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<String, Any>) {
val eventType = event["type"] as? String ?: return
when (eventType) {
"started" -> {
isTtsSpeaking = true isTtsSpeaking = true
restartIdleCheck() restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started")) sendEvent("tts_started", mapOf("status" to "started"))
} }
"completed" -> {
override fun onSynthesizing() {}
override fun onSynthesisCompleted() {
isTtsSpeaking = false isTtsSpeaking = false
restartIdleCheck() restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed")) sendEvent("tts_completed", mapOf("status" to "completed"))
} }
"stopped", "canceled" -> {
override fun onSynthesisCanceled() {
isTtsSpeaking = false isTtsSpeaking = false
restartIdleCheck() restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled")) sendEvent("tts_${eventType}", mapOf("status" to eventType))
}
"error" -> {
isTtsSpeaking = false
restartIdleCheck()
val code = event["errorCode"] as? Int ?: -1
val message = event["errorMessage"] as? String ?: "未知错误"
sendEvent("error", mapOf(
"code" to "TTS_ERROR",
"message" to "${serviceConfig.serviceType}语音错误: $code, $message"
))
} }
})
} }
// 初始化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: CancellationException) {
// 协程被取消,正常行为 // 协程被取消,正常行为
} catch (e: Exception) { } 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 { fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
return false return false
} }
@ -285,7 +359,7 @@ object AgentService : CoroutineScope {
// 检查录音权限 // 检查录音权限
if (!isExternalActive && !checkRecordAudioPermission()) { if (!isExternalActive && !checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限") Log.e(TAG, "需要录音权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false return false
} }
@ -299,7 +373,7 @@ object AgentService : CoroutineScope {
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
@ -371,7 +445,7 @@ object AgentService : CoroutineScope {
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
BleService.closeCodec() BleService.closeCodec()
FileLogger.e(TAG, "语音识别错误: $error") Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR", "code" to "RECOGNITION_ERROR",
"message" to error "message" to error
@ -381,7 +455,7 @@ object AgentService : CoroutineScope {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
isRecognitionActive = false isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}") Log.e(TAG, "启动语音识别失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_START_ERROR", "code" to "RECOGNITION_START_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -404,9 +478,9 @@ object AgentService : CoroutineScope {
BleService.closeCodec() BleService.closeCodec()
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
FileLogger.d(TAG, "语音识别已停止") Log.d(TAG, "语音识别已停止")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}") Log.e(TAG, "停止语音识别异常: ${e.message}")
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
} }
@ -446,9 +520,9 @@ object AgentService : CoroutineScope {
openAIService.cancelCurrentStream() openAIService.cancelCurrentStream()
// 记录日志 // 记录日志
FileLogger.d(TAG, "AI流输出已停止") Log.d(TAG, "AI流输出已停止")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止AI流输出异常", e) Log.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常 // 确保状态被重置,即使发生异常
isAiStreaming = false isAiStreaming = false
currentAiJob = null currentAiJob = null
@ -461,13 +535,13 @@ object AgentService : CoroutineScope {
*/ */
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false return false
} }
if (text.isEmpty()) { if (text.isEmpty()) {
FileLogger.e(TAG, "文本输入不能为空") Log.e(TAG, "文本输入不能为空")
sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空"))
return false return false
} }
@ -494,7 +568,7 @@ object AgentService : CoroutineScope {
text: String, text: String,
speakResponse: Boolean = true speakResponse: Boolean = true
) { ) {
FileLogger.d(TAG, "用户问题: $text") Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理 // 创建用户文本消息并处理
val userMessage = openAIService.createUserMessage(text) val userMessage = openAIService.createUserMessage(text)
@ -513,7 +587,7 @@ object AgentService : CoroutineScope {
text: String = "", text: String = "",
speakResponse: Boolean = false 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) val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
@ -545,7 +619,6 @@ object AgentService : CoroutineScope {
// 设置状态为正在流式输出 // 设置状态为正在流式输出
isAiStreaming = true isAiStreaming = true
// 使用历史记录作为上下文发送到OpenAI // 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder() val responseBuilder = StringBuilder()
var metadata:String = "" var metadata:String = ""
@ -592,24 +665,29 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage) addToHistoryMessages(userMessage)
} }
openAIService.sendMessageStream( openAIService.sendMessageStream(
messages = messagesWithSystemPrompt, messages = messagesWithSystemPrompt,
callback = object : OpenAIService.StreamCallback { callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) { override fun onToken(token: String) {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse) { if (speakResponse) {
azureTtsHelper?.speakStream(token) // 使用统一的TTS接口
launch {
ttsService?.speakStream(token)
}
} }
// 发送流式回复token // 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token)) sendEvent("assistant_token", mapOf("token" to token))
} }
override fun onComplete() { override fun onComplete() {
FileLogger.d(TAG, "AI完整回复: $responseBuilder") Log.d(TAG, "AI完整回复: $responseBuilder")
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse) { if (speakResponse) {
azureTtsHelper?.flushStream() // 使用统一的TTS接口刷新缓冲区
launch {
ttsService?.flushStream()
}
} }
val response = responseBuilder.toString() val response = responseBuilder.toString()
@ -637,7 +715,7 @@ object AgentService : CoroutineScope {
} }
override fun onError(e: Exception) { override fun onError(e: Exception) {
FileLogger.e(TAG, "AI处理出错", e) Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "AI_ERROR", "code" to "AI_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -660,7 +738,7 @@ object AgentService : CoroutineScope {
stopRecognition() stopRecognition()
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用事件失败", e) Log.e(TAG, "处理函数调用事件失败", e)
} }
} }
@ -670,7 +748,7 @@ object AgentService : CoroutineScope {
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult") Log.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoPlayMusic(functionCallResult); metadata = autoPlayMusic(functionCallResult);
} }
} }
@ -678,7 +756,7 @@ object AgentService : CoroutineScope {
} catch (e: Exception) { } catch (e: Exception) {
val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" 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( sendEvent("error", mapOf(
"code" to errorType, "code" to errorType,
"message" to e.message.toString() "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) { } catch (e: Exception) {
FileLogger.e(TAG, "加载聊天历史失败: ${e.message}") Log.e(TAG, "加载聊天历史失败: ${e.message}")
} }
} }
@ -754,8 +832,10 @@ object AgentService : CoroutineScope {
isTtsSpeaking = true isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测 restartIdleCheck() // 状态变化,重启检测
// 播放文本 // 使用统一的TTS接口
azureTtsHelper?.speakText(text) launch {
ttsService?.speakOnce(text)
}
} }
/** /**
@ -763,7 +843,10 @@ object AgentService : CoroutineScope {
*/ */
fun stopTts() { fun stopTts() {
if (isTtsSpeaking) { if (isTtsSpeaking) {
azureTtsHelper?.stopSpeaking() launch {
ttsService?.stop()
}
isTtsSpeaking = false isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测 restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", mapOf("status" to "stopped")) sendEvent("tts_stopped", mapOf("status" to "stopped"))
@ -793,15 +876,15 @@ object AgentService : CoroutineScope {
) )
// if (assistantMessageId != -1L) { // if (assistantMessageId != -1L) {
// FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") // Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// } else { // } else {
// FileLogger.e(TAG, "保存助手消息失败") // Log.e(TAG, "保存助手消息失败")
// } // }
} else { } else {
FileLogger.e(TAG, "保存用户消息失败") Log.e(TAG, "保存用户消息失败")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "保存聊天记录异常", e) Log.e(TAG, "保存聊天记录异常", e)
} }
} }
} }
@ -820,16 +903,16 @@ object AgentService : CoroutineScope {
historyMessages.remove(0) historyMessages.remove(0)
} }
} }
FileLogger.d(TAG, "聊天历史已清除") Log.d(TAG, "聊天历史已清除")
} else { } else {
FileLogger.e(TAG, "清除聊天历史失败") Log.e(TAG, "清除聊天历史失败")
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
callback(success) callback(success)
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史异常: ${e.message}") Log.e(TAG, "清除聊天历史异常: ${e.message}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
callback(false) callback(false)
} }
@ -848,7 +931,7 @@ object AgentService : CoroutineScope {
try { try {
listener.onEvent(eventName, data) listener.onEvent(eventName, data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e) Log.e(TAG, "向监听器发送事件失败", e)
} }
} }
@ -859,13 +942,60 @@ object AgentService : CoroutineScope {
try { try {
listener.onEvent(eventName, data) listener.onEvent(eventName, data)
} catch (e: Exception) { } 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 audioPlayer = null
azureAsrHelper?.dispose() azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 释放TTS服务
ttsService?.release()
ttsService = null
job.cancel() job.cancel()
clearListeners() clearListeners()
isInitialized = false isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放") Log.d(TAG, "代理服务资源已释放")
} catch (e: Exception) { } 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 { fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false return false
} }
if (imagePath.isEmpty()) { if (imagePath.isEmpty()) {
FileLogger.e(TAG, "图片路径不能为空") Log.e(TAG, "图片路径不能为空")
sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空")) sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空"))
return false return false
} }
@ -944,7 +1077,7 @@ object AgentService : CoroutineScope {
// 处理包含图片的消息 // 处理包含图片的消息
processImageWithOpenAI(imageBase64, text, speakResponse) processImageWithOpenAI(imageBase64, text, speakResponse)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理图片失败: ${e.message}") Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "IMAGE_PROCESSING_ERROR", "code" to "IMAGE_PROCESSING_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -992,9 +1125,9 @@ object AgentService : CoroutineScope {
} }
} }
} }
FileLogger.d(TAG, "开始播放音频资源") Log.d(TAG, "开始播放音频资源")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e) Log.e(TAG, "播放音频资源异常: ${e.message}", e)
release() release()
} }
} }
@ -1023,10 +1156,10 @@ object AgentService : CoroutineScope {
val metaStr = functionCallResult.optString("meta") val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) { if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr) val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr") Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片 if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music") val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic") Log.d(TAG, "检查到音乐卡片: $cardMusic")
val id = cardMusic.optString("id", "") val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "") val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "") 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)) playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
} }
if (playlist.isNotEmpty()) { if (playlist.isNotEmpty()) {
FileLogger.w(TAG, "自动播放音乐列表 ${playlist}") Log.w(TAG, "自动播放音乐列表 ${playlist}")
processMusicPlayList(playlist) processMusicPlayList(playlist)
} else { } else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目") Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
} }
} else { } else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组") Log.w(TAG, "card_musiclist.musics 不是一个数组")
} }
} }
return metaStr; return metaStr;
@ -1074,7 +1207,7 @@ object AgentService : CoroutineScope {
*/ */
fun processMusicPlay(song: Map<String,String>){ fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用 // 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $song") Log.i(TAG, "启动音乐服务 播放音乐 $song")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
} }
@ -1090,10 +1223,10 @@ object AgentService : CoroutineScope {
fun processMusicPlayList(songs:List<Map<String,String>>){ fun processMusicPlayList(songs:List<Map<String,String>>){
// 如果 playlist 中有有效的歌曲,开始播放 // 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}") Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs) MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else { } else {
FileLogger.i(TAG, "音乐列表为空,未启动播放服务") Log.i(TAG, "音乐列表为空,未启动播放服务")
} }
} }
} }

30
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.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import org.json.JSONObject import org.json.JSONObject
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -33,7 +33,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
// 添加监听器 // 添加监听器
AgentService.addListener(this) AgentService.addListener(this)
FileLogger.d(TAG, "插件已加载") Log.d(TAG, "插件已加载")
} }
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
@ -46,7 +46,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
return return
} }
FileLogger.d(TAG, "启动BleMediaService,配置参数: $config") Log.d(TAG, "启动BleMediaService,配置参数: $config")
// 创建Intent启动BleMediaService // 创建Intent启动BleMediaService
val intent = Intent(context, BleMediaService::class.java) 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["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -69,7 +71,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动AgentService失败", e) Log.e(TAG, "启动AgentService失败", e)
result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null) result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null)
} }
} }
@ -79,7 +81,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
context.stopService(intent) context.stopService(intent)
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止AgentService失败", e) Log.e(TAG, "停止AgentService失败", e)
result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null) result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null)
} }
} }
@ -88,7 +90,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
val isStarted = AgentService.startRecognition(false) val isStarted = AgentService.startRecognition(false)
result.success(isStarted) result.success(isStarted)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动对话失败", e) Log.e(TAG, "启动对话失败", e)
result.error("START_ERROR", "启动对话失败: ${e.message}", null) result.error("START_ERROR", "启动对话失败: ${e.message}", null)
} }
} }
@ -97,7 +99,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopRecognition() AgentService.stopRecognition()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止对话失败", e) Log.e(TAG, "停止对话失败", e)
result.error("STOP_ERROR", "停止对话失败: ${e.message}", null) result.error("STOP_ERROR", "停止对话失败: ${e.message}", null)
} }
} }
@ -118,7 +120,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing) result.success(isProcessing)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理文本输入失败", e) Log.e(TAG, "处理文本输入失败", e)
result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null) result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null)
} }
} }
@ -141,7 +143,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing) result.success(isProcessing)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理图片输入失败", e) Log.e(TAG, "处理图片输入失败", e)
result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null) result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null)
} }
} }
@ -156,7 +158,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.speakText(text) AgentService.speakText(text)
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "语音合成失败", e) Log.e(TAG, "语音合成失败", e)
result.error("TTS_ERROR", "语音合成失败: ${e.message}", null) result.error("TTS_ERROR", "语音合成失败: ${e.message}", null)
} }
} }
@ -165,7 +167,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopTts() AgentService.stopTts()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败", e) Log.e(TAG, "停止语音合成失败", e)
result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null) result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null)
} }
} }
@ -175,7 +177,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(success) result.success(success)
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史失败", e) Log.e(TAG, "清除聊天历史失败", e)
result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null) result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null)
} }
} }
@ -184,7 +186,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.interruptCurrentResponse() AgentService.interruptCurrentResponse()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "中断响应失败", e) Log.e(TAG, "中断响应失败", e)
result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null) result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null)
} }
} }
@ -193,7 +195,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.dispose() AgentService.dispose()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "释放资源失败", e) Log.e(TAG, "释放资源失败", e)
result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null) result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null)
} }
} }

28
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 android.content.Context
import com.yunqiinnovation.agent_service.AgentService import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener 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.BleService
import com.yunqiinnovation.ble_service.BleConst import com.yunqiinnovation.ble_service.BleConst
@ -29,11 +29,11 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (isInitialized) return true if (isInitialized) return true
try { try {
FileLogger.d(TAG, "初始化语音代理..., $config") Log.d(TAG, "初始化语音代理..., $config")
// 1. 初始化AgentService // 1. 初始化AgentService
if (!AgentService.initialize(context, config, true)) { if (!AgentService.initialize(context, config, true)) {
FileLogger.e(TAG, "语音服务初始化失败") Log.e(TAG, "语音服务初始化失败")
return false return false
} }
@ -41,20 +41,20 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (BleService.initialize(context)) { if (BleService.initialize(context)) {
if (BleService.addCallback(this)) { if (BleService.addCallback(this)) {
isRegisteredToBle = true isRegisteredToBle = true
FileLogger.d(TAG, "已注册BLE回调") Log.d(TAG, "已注册BLE回调")
} else { } else {
FileLogger.w(TAG, "BLE回调注册失败") Log.w(TAG, "BLE回调注册失败")
} }
} else { } else {
FileLogger.e(TAG, "BLE服务初始化失败") Log.e(TAG, "BLE服务初始化失败")
return false return false
} }
isInitialized = true isInitialized = true
FileLogger.d(TAG, "语音代理初始化成功") Log.d(TAG, "语音代理初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "初始化语音代理异常", e) Log.e(TAG, "初始化语音代理异常", e)
return false return false
} }
} }
@ -64,7 +64,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
*/ */
fun startListening(): Boolean { fun startListening(): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "语音代理未初始化") Log.e(TAG, "语音代理未初始化")
return false return false
} }
@ -106,7 +106,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
AgentService.dispose() AgentService.dispose()
isInitialized = false isInitialized = false
FileLogger.d(TAG, "语音代理资源已释放") Log.d(TAG, "语音代理资源已释放")
} }
//============================================================================================ //============================================================================================
@ -119,16 +119,16 @@ object BleAgent : BleService.Callback, AgentServiceListener {
override fun onConnectionStateChanged(state: Int) { override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化 // 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) { if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(TAG, "Ble设备已连接,准备好语音交互") Log.d(TAG, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) { } else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(TAG, "Ble设备已断开,停止语音服务") Log.d(TAG, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务 // 设备断开连接,停止语音服务
// stopListening() // stopListening()
} }
} }
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}") // Log.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data) AgentService.pushAudioData(data)
// 可选:处理音频数据 // 可选:处理音频数据
} }
@ -138,7 +138,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
* 在收到唤醒信号时启动语音识别 * 在收到唤醒信号时启动语音识别
*/ */
override fun onWakeupSignalReceived() { override fun onWakeupSignalReceived() {
FileLogger.d(TAG, "收到唤醒信号,启动语音识别") Log.d(TAG, "收到唤醒信号,启动语音识别")
// 停止当前TTS,避免冲突 // 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse() AgentService.interruptCurrentResponse()

57
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.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.agent_service.R import com.yunqiinnovation.agent_service.R
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@ -56,6 +56,8 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_OPENAI_MODEL = "openaiModel" private const val KEY_OPENAI_MODEL = "openaiModel"
private const val KEY_SYSTEM_PROMPT = "systemPrompt" private const val KEY_SYSTEM_PROMPT = "systemPrompt"
private const val KEY_MCP_SERVER = "mcpServer" 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) private val isRunning = AtomicBoolean(false)
@ -89,8 +91,7 @@ class BleMediaService : Service(), CoroutineScope {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(this) Log.i(TAG, "BleMediaService正在创建...")
FileLogger.i(TAG, "BleMediaService正在创建...")
// 设置服务状态 // 设置服务状态
isRunning.set(true) isRunning.set(true)
@ -111,11 +112,11 @@ class BleMediaService : Service(), CoroutineScope {
audioPlayer?.play(R.raw.silence, 0f) audioPlayer?.play(R.raw.silence, 0f)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED) setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
FileLogger.i(TAG, "媒体服务已启动") Log.i(TAG, "媒体服务已启动")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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,用于后续初始化 // 保存当前Intent,用于后续初始化
currentIntent = intent currentIntent = intent
@ -159,36 +160,36 @@ class BleMediaService : Service(), CoroutineScope {
// 如果Intent中有参数,则保存到SharedPreferences // 如果Intent中有参数,则保存到SharedPreferences
if (configFromIntent.isNotEmpty()) { if (configFromIntent.isNotEmpty()) {
FileLogger.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences") Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
saveConfigToPreferences(configFromIntent, prefs) saveConfigToPreferences(configFromIntent, prefs)
} }
// 合并Intent和SharedPreferences中的参数 // 合并Intent和SharedPreferences中的参数
val config = getConfigFromPreferences(prefs) 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()) { if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
FileLogger.w(TAG, "语音代理初始化失败: API密钥未配置") Log.w(TAG, "语音代理初始化失败: API密钥未配置")
return return
} }
if (isChatAgentInitialized) { if (isChatAgentInitialized) {
FileLogger.d(TAG, "ChatAgent已经初始化,跳过") Log.d(TAG, "ChatAgent已经初始化,跳过")
return return
} }
if(config.isNotEmpty()) { if(config.isNotEmpty()) {
// 初始化ChatAgent // 初始化ChatAgent
FileLogger.i(TAG, "开始初始化ChatAgent") Log.i(TAG, "开始初始化ChatAgent")
val success = BleAgent.initialize(applicationContext, config) val success = BleAgent.initialize(applicationContext, config)
if (success) { if (success) {
isChatAgentInitialized = true isChatAgentInitialized = true
FileLogger.i(TAG, "语音交互代理初始化成功") Log.i(TAG, "语音交互代理初始化成功")
} else { } else {
FileLogger.e(TAG, "语音交互代理初始化失败") Log.e(TAG, "语音交互代理初始化失败")
} }
} }
} catch (e: Exception) { } 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_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value }
it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = 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_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 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_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_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_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 onBind(intent: Intent?): IBinder? = null
override fun onDestroy() { override fun onDestroy() {
FileLogger.i(TAG, "BleMediaService正在销毁...") Log.i(TAG, "BleMediaService正在销毁...")
// 取消注册媒体按钮接收器 // 取消注册媒体按钮接收器
try { try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver) audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器已取消注册") Log.i(TAG, "媒体按钮接收器已取消注册")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e) Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
} }
// 释放媒体会话 // 释放媒体会话
@ -319,16 +324,16 @@ class BleMediaService : Service(), CoroutineScope {
try { try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name) val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver) audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver") Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
} }
} }
private fun checkBleConnection() { private fun checkBleConnection() {
FileLogger.i(TAG, "checkBleConnection收到连接请求") Log.i(TAG, "checkBleConnection收到连接请求")
try { try {
// 示例:使用协程异步处理连接 // 示例:使用协程异步处理连接
@ -338,7 +343,7 @@ class BleMediaService : Service(), CoroutineScope {
} }
} }
} catch (e: Exception) { } 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 val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification()) notificationManager.notify(NOTIFICATION_ID, createNotification())
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "更新通知异常: ${e.message}") Log.e(TAG, "更新通知异常: ${e.message}")
} }
} }
@ -451,7 +456,7 @@ class BleMediaService : Service(), CoroutineScope {
// // 播放完成后自动循环播放,保持媒体会话活跃 // // 播放完成后自动循环播放,保持媒体会话活跃
// setOnCompletionListener { // setOnCompletionListener {
// FileLogger.i(TAG, "静音资源播放完成,再次播放保持会话活跃") // Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// play(resId, volume) // play(resId, volume)
// } // }
@ -460,9 +465,9 @@ class BleMediaService : Service(), CoroutineScope {
start() start()
} }
FileLogger.i(TAG, "开始播放静音资源") Log.i(TAG, "开始播放静音资源")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "播放静音资源异常: ${e.message}", e) Log.e(TAG, "播放静音资源异常: ${e.message}", e)
release() release()
} }
} }

14
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 android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* 通用工具类,包含安全存储等功能 * 通用工具类,包含安全存储等功能
@ -59,10 +59,10 @@ object Utils {
// 提交更改 // 提交更改
editor.apply() editor.apply()
FileLogger.d(TAG, "数据已安全保存到加密存储中") Log.d(TAG, "数据已安全保存到加密存储中")
true true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "保存数据到加密存储失败", e) Log.e(TAG, "保存数据到加密存储失败", e)
false false
} }
} }
@ -97,10 +97,10 @@ object Utils {
} }
} }
FileLogger.d(TAG, "已从加密存储加载所有数据") Log.d(TAG, "已从加密存储加载所有数据")
resultMap resultMap
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "从加密存储加载所有数据失败", e) Log.e(TAG, "从加密存储加载所有数据失败", e)
emptyMap() emptyMap()
} }
} }
@ -134,10 +134,10 @@ object Utils {
.putString(KEY_SESSION_ID, sessionId) .putString(KEY_SESSION_ID, sessionId)
.apply() .apply()
FileLogger.d(TAG, "已清除所有安全存储的数据") Log.d(TAG, "已清除所有安全存储的数据")
true true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除安全存储数据失败", e) Log.e(TAG, "清除安全存储数据失败", e)
false false
} }
} }

2
local_plugins/azure_speech/android/build.gradle.kts

@ -52,6 +52,6 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
dependencies { dependencies {
// 添加Microsoft语音SDK // 添加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")) implementation(project(":ble_service"))
} }

51
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.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl import android.media.audiofx.AutomaticGainControl
import android.os.Process 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.*
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler import com.microsoft.cognitiveservices.speech.util.EventHandler
@ -84,7 +84,7 @@ class AzureAsrHelper(private val context: Context) {
try { try {
// 检查配置是否为空 // 检查配置是否为空
if (subscriptionKey.isEmpty() || region.isEmpty()) { if (subscriptionKey.isEmpty() || region.isEmpty()) {
FileLogger.e(tag, "Azure 配置信息不完整") Log.e(tag, "Azure 配置信息不完整")
return false return false
} }
@ -124,7 +124,7 @@ class AzureAsrHelper(private val context: Context) {
// 创建识别器 // 创建识别器
return setupRecognizer() return setupRecognizer()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "初始化失败: ${e.message}") Log.e(tag, "初始化失败: ${e.message}")
return false return false
} }
} }
@ -184,7 +184,7 @@ class AzureAsrHelper(private val context: Context) {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "创建识别器失败: ${e.message}") Log.e(tag, "创建识别器失败: ${e.message}")
stopAudioProcessing() stopAudioProcessing()
return false return false
} }
@ -201,8 +201,8 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调 // 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream) audioConfig = AudioConfig.fromStreamInput(microphoneStream)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "设置麦克风流失败: ${e.message}") Log.e(tag, "设置麦克风流失败: ${e.message}")
e.printStackTrace() microphoneStream = null
} }
} }
@ -217,8 +217,8 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置 // 创建音频配置
audioConfig = AudioConfig.fromStreamInput(externalAudioStream) audioConfig = AudioConfig.fromStreamInput(externalAudioStream)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "设置外部音频流失败: ${e.message}") Log.e(tag, "设置外部音频流失败: ${e.message}")
e.printStackTrace() externalAudioStream = null
} }
} }
@ -368,24 +368,14 @@ class AzureAsrHelper(private val context: Context) {
// 取消事件 // 取消事件
recognizer?.canceled?.addEventListener( recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event -> EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = try { val errorDetails = event.errorDetails ?: "未知错误"
event.errorDetails ?: "未知错误"
} catch (e: Exception) {
"未知错误"
}
val reason = event.reason.toString() val reason = event.reason.toString()
FileLogger.e(tag, "识别取消: $errorDetails")
isContinuousRecognitionActive = false
stopAudioProcessing()
// 直接在当前线程调用回调
callback.onCanceled(reason, errorDetails) callback.onCanceled(reason, errorDetails)
callback.onError("识别取消: $errorDetails") // 兼容旧接口
} }
) )
} }
/** /**
* 停止连续语音识别 * 停止连续语音识别
* *
@ -393,7 +383,7 @@ class AzureAsrHelper(private val context: Context) {
*/ */
fun stopContinuousRecognition(): Boolean { fun stopContinuousRecognition(): Boolean {
if (speechConfig == null) { if (speechConfig == null) {
FileLogger.e(tag, "语音服务未初始化") Log.e(tag, "语音服务未初始化")
return false return false
} }
@ -403,7 +393,7 @@ class AzureAsrHelper(private val context: Context) {
try { try {
if (recognizer == null) { if (recognizer == null) {
FileLogger.w(tag, "识别器为空,重置状态") Log.w(tag, "识别器为空,重置状态")
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
return true return true
} }
@ -422,7 +412,7 @@ class AzureAsrHelper(private val context: Context) {
} catch (e: Exception) { } catch (e: Exception) {
// 强制重置状态 // 强制重置状态
isContinuousRecognitionActive = false isContinuousRecognitionActive = false
FileLogger.e(tag, "停止连续识别失败: ${e.message}") Log.e(tag, "停止连续识别失败: ${e.message}")
// 停止音频处理 // 停止音频处理
stopAudioProcessing() stopAudioProcessing()
@ -432,7 +422,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.close() recognizer?.close()
recognizer = null recognizer = null
} catch (ex: Exception) { } catch (ex: Exception) {
FileLogger.e(tag, "关闭识别器失败: ${ex.message}") Log.e(tag, "关闭识别器失败: ${ex.message}")
} }
return false return false
@ -515,7 +505,7 @@ class AzureAsrHelper(private val context: Context) {
it.close() it.close()
microphoneStream = null microphoneStream = null
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "关闭麦克风流失败: ${e.message}") Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -525,7 +515,7 @@ class AzureAsrHelper(private val context: Context) {
it.close() it.close()
externalAudioStream = null externalAudioStream = null
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "关闭外部音频流失败: ${e.message}") Log.e(tag, "关闭外部音频流失败: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
} }
@ -623,12 +613,13 @@ class AzureAsrHelper(private val context: Context) {
try { try {
val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1 val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1
if (result < 0) { if (result < 0) {
FileLogger.e(tag, "读取音频数据失败: $result") Log.e(tag, "读取音频数据失败: $result")
return 0
} }
return result return result
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "读取音频异常: ${e.message}") Log.e(tag, "读取音频异常: ${e.message}")
return -1 return 0
} }
} }
@ -696,7 +687,7 @@ class AzureAsrHelper(private val context: Context) {
Thread.currentThread().interrupt() Thread.currentThread().interrupt()
return 0 return 0
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "读取外部音频数据失败: ${e.message}") Log.e(tag, "读取外部音频数据失败: ${e.message}")
return 0 return 0
} }
} }

11
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 onSynthesisStarted() {}
/**
* 合成中
*/
fun onSynthesizing() {}
/** /**
* 合成完成 * 合成完成
*/ */
@ -96,6 +91,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region) speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region)
// speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Audio24Khz48KBitRateMonoMp3)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
speechConfig?.setSpeechSynthesisVoiceName(currentVoice) speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
@ -166,11 +162,6 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
ttsCallback?.onSynthesisStarted() ttsCallback?.onSynthesisStarted()
} }
// 合成中事件
Synthesizing?.addEventListener { _, _ ->
ttsCallback?.onSynthesizing()
}
// 合成完成事件 // 合成完成事件
SynthesisCompleted?.addEventListener { _, _ -> SynthesisCompleted?.addEventListener { _, _ ->
isSpeaking = false isSpeaking = false

1
local_plugins/ble_service/android/build.gradle.kts

@ -30,5 +30,4 @@ dependencies {
implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.core:core-ktx:1.13.1")
// 使用从本地Maven仓库引用的AAR依赖 // 使用从本地Maven仓库引用的AAR依赖
implementation("com.jieli.audio:jl_audio_decode:2.0.0") implementation("com.jieli.audio:jl_audio_decode:2.0.0")
implementation(project(":deepvoice_utils"))
} }

74
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.Build
import android.os.ParcelUuid import android.os.ParcelUuid
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import java.util.regex.Pattern import java.util.regex.Pattern
/** /**
@ -71,7 +71,7 @@ class BleCompanionManager(private val context: Context) {
*/ */
fun savePairedMacAddress(macAddress: String) { fun savePairedMacAddress(macAddress: String) {
sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() 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() { fun clearPairedMacAddress() {
sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() 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() idField.get(association).toString()
} ?: emptyList() } ?: emptyList()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) Log.e(TAG, "获取关联ID失败: ${e.message}", e)
emptyList() emptyList()
} }
} else { } else {
@ -135,7 +135,7 @@ class BleCompanionManager(private val context: Context) {
macAddress: String? = null macAddress: String? = null
): Boolean { ): Boolean {
if (!isCompanionDeviceManagerSupported()) { if (!isCompanionDeviceManagerSupported()) {
FileLogger.e(TAG, "设备不支持CompanionDeviceManager") Log.e(TAG, "设备不支持CompanionDeviceManager")
return false return false
} }
@ -148,7 +148,7 @@ class BleCompanionManager(private val context: Context) {
// 使用"^设备名称$"格式创建精确匹配的正则表达式 // 使用"^设备名称$"格式创建精确匹配的正则表达式
val exactNamePattern = Pattern.compile("^$it$") val exactNamePattern = Pattern.compile("^$it$")
deviceFilterBuilder.setNamePattern(exactNamePattern) deviceFilterBuilder.setNamePattern(exactNamePattern)
FileLogger.i(TAG, "设置严格名称匹配: $it") Log.i(TAG, "设置严格名称匹配: $it")
} }
// 添加服务UUID // 添加服务UUID
@ -156,9 +156,9 @@ class BleCompanionManager(private val context: Context) {
try { try {
val uuid = ParcelUuid(java.util.UUID.fromString(it)) val uuid = ParcelUuid(java.util.UUID.fromString(it))
deviceFilterBuilder.addServiceUuid(uuid, null) deviceFilterBuilder.addServiceUuid(uuid, null)
FileLogger.i(TAG, "设置服务UUID匹配: $it") Log.i(TAG, "设置服务UUID匹配: $it")
} catch (e: Exception) { } 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 { try {
// 检查是否是有效的MAC地址格式 // 检查是否是有效的MAC地址格式
if (!isValidMacAddress(it)) { if (!isValidMacAddress(it)) {
FileLogger.e(TAG, "无效的MAC地址格式: $it") Log.e(TAG, "无效的MAC地址格式: $it")
return false return false
} }
@ -176,15 +176,15 @@ class BleCompanionManager(private val context: Context) {
// 设置MAC地址严格匹配 // 设置MAC地址严格匹配
deviceFilterBuilder.setAddress(it) deviceFilterBuilder.setAddress(it)
FileLogger.i(TAG, "设置MAC地址严格匹配: $it") Log.i(TAG, "设置MAC地址严格匹配: $it")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置MAC地址失败: ${e.message}", e) Log.e(TAG, "设置MAC地址失败: ${e.message}", e)
return false return false
} }
} ?: run { } ?: run {
// 如果没有提供MAC地址也没有提供设备名称,则返回错误 // 如果没有提供MAC地址也没有提供设备名称,则返回错误
if (deviceNamePattern == null && serviceUuid == null) { if (deviceNamePattern == null && serviceUuid == null) {
FileLogger.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一") Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一")
return false return false
} }
} }
@ -209,9 +209,9 @@ class BleCompanionManager(private val context: Context) {
0, 0,
0 0
) )
FileLogger.i(TAG, "已启动设备选择对话框") Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) { } 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,
0 0
) )
FileLogger.i(TAG, "已启动设备选择对话框") Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
} }
} }
// API 33+ (Android 13+) 使用 onAssociationCreated // API 33+ (Android 13+) 使用 onAssociationCreated
@RequiresApi(Build.VERSION_CODES.TIRAMISU) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onAssociationCreated(associationInfo: AssociationInfo) { override fun onAssociationCreated(associationInfo: AssociationInfo) {
FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}")
// 保存关联ID // 保存关联ID
try { try {
val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress")
val mac = macAddressMethod.invoke(associationInfo) as? String val mac = macAddressMethod.invoke(associationInfo) as? String
mac?.let { savePairedMacAddress(it) } mac?.let { savePairedMacAddress(it) }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
} }
} }
override fun onFailure(error: CharSequence?) { override fun onFailure(error: CharSequence?) {
FileLogger.e(TAG, "设备关联失败: $error") Log.e(TAG, "设备关联失败: $error")
} }
}, },
null null
@ -257,7 +257,7 @@ class BleCompanionManager(private val context: Context) {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e) Log.e(TAG, "启动设备关联失败: ${e.message}", e)
return false return false
} }
} }
@ -276,7 +276,7 @@ class BleCompanionManager(private val context: Context) {
return false return false
} }
FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode") Log.i(TAG, "收到关联结果: resultCode=$resultCode")
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == Activity.RESULT_OK && data != null) {
// 获取选中的蓝牙设备 // 获取选中的蓝牙设备
@ -295,7 +295,7 @@ class BleCompanionManager(private val context: Context) {
val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID"
data.getStringExtra(extraAssociationIdKey) data.getStringExtra(extraAssociationIdKey)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) Log.e(TAG, "获取关联ID失败: ${e.message}", e)
null null
} }
} else { } else {
@ -309,20 +309,20 @@ class BleCompanionManager(private val context: Context) {
// 验证MAC地址格式 // 验证MAC地址格式
if (!isValidMacAddress(deviceMac)) { if (!isValidMacAddress(deviceMac)) {
FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") Log.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac")
return false return false
} }
// 检查是否与预期的MAC地址匹配 // 检查是否与预期的MAC地址匹配
if (pendingMacAddress != null && pendingMacAddress != deviceMac) { if (pendingMacAddress != null && pendingMacAddress != deviceMac) {
FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配") Log.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配")
// 如果要严格匹配MAC,可以在这里返回false // 如果要严格匹配MAC,可以在这里返回false
return false return false
} }
// 匹配成功后,保存新的MAC地址 // 匹配成功后,保存新的MAC地址
savePairedMacAddress(deviceMac) savePairedMacAddress(deviceMac)
FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") Log.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId")
// 清除临时MAC地址 // 清除临时MAC地址
pendingMacAddress = null pendingMacAddress = null
@ -330,10 +330,10 @@ class BleCompanionManager(private val context: Context) {
return true return true
} else { } else {
FileLogger.e(TAG, "无法获取关联的蓝牙设备信息") Log.e(TAG, "无法获取关联的蓝牙设备信息")
} }
} else { } else {
FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") Log.w(TAG, "设备关联被取消或失败: resultCode=$resultCode")
// 清除临时MAC地址 // 清除临时MAC地址
pendingMacAddress = null pendingMacAddress = null
} }
@ -350,7 +350,7 @@ class BleCompanionManager(private val context: Context) {
try { try {
val mac = getPairedMacAddress() val mac = getPairedMacAddress()
if (mac.isNullOrEmpty()) { if (mac.isNullOrEmpty()) {
FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") Log.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址")
return return
} }
@ -359,7 +359,7 @@ class BleCompanionManager(private val context: Context) {
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) { if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") Log.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器")
return return
} }
@ -391,9 +391,9 @@ class BleCompanionManager(private val context: Context) {
// 启动后台扫描 // 启动后台扫描
bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent) bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent)
FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac") Log.i(TAG, "已注册后台扫描,目标设备MAC: $mac")
} catch (e: Exception) { } 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 val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) { if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") Log.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器")
return return
} }
@ -428,9 +428,9 @@ class BleCompanionManager(private val context: Context) {
// 停止后台扫描 // 停止后台扫描
bluetoothLeScanner.stopScan(pendingIntent) bluetoothLeScanner.stopScan(pendingIntent)
FileLogger.i(TAG, "已取消后台扫描") Log.i(TAG, "已取消后台扫描")
} catch (e: Exception) { } 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 { } else {
companionDeviceManager?.disassociate(id) companionDeviceManager?.disassociate(id)
} }
FileLogger.i(TAG, "已解除设备关联: $id") Log.i(TAG, "已解除设备关联: $id")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e) Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e) Log.e(TAG, "清除所有关联失败: ${e.message}", e)
} }
} }

15
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.Build
import android.os.IBinder import android.os.IBinder
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* BleCompanionService * BleCompanionService
@ -19,13 +19,12 @@ class BleCompanionService : CompanionDeviceService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(applicationContext) Log.i(TAG, "BleCompanionService已创建")
FileLogger.i(TAG, "BleCompanionService已创建")
} }
override fun onDeviceAppeared(info: AssociationInfo) { override fun onDeviceAppeared(info: AssociationInfo) {
super.onDeviceAppeared(info) super.onDeviceAppeared(info)
FileLogger.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") Log.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}")
// 当配对设备出现时,启动前台服务 // 当配对设备出现时,启动前台服务
try { try {
@ -49,21 +48,21 @@ class BleCompanionService : CompanionDeviceService() {
val mac = macAddressMethod.invoke(info) as? String val mac = macAddressMethod.invoke(info) as? String
mac?.let { putExtra("mac", it) } mac?.let { putExtra("mac", it) }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
} }
} }
// 启动前台服务 // 启动前台服务
startForegroundService(serviceIntent) startForegroundService(serviceIntent)
FileLogger.i(TAG, "已启动服务") Log.i(TAG, "已启动服务")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动服务失败: ${e.message}", e) Log.e(TAG, "启动服务失败: ${e.message}", e)
} }
} }
override fun onDeviceDisappeared(info: AssociationInfo) { override fun onDeviceDisappeared(info: AssociationInfo) {
super.onDeviceDisappeared(info) super.onDeviceDisappeared(info)
FileLogger.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") Log.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}")
// 可以选择停止服务或执行其他清理操作 // 可以选择停止服务或执行其他清理操作
} }

180
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.content.Intent
import android.os.* import android.os.*
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -124,17 +124,17 @@ object BleService {
try { try {
opusManager = OpusManager() opusManager = OpusManager()
startOpusStreamDecoding() startOpusStreamDecoding()
FileLogger.d(TAG, "OpusManager初始化成功") Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) { } catch (e: OpusException) {
FileLogger.e(TAG, "OpusManager初始化失败: ${e.message}", e) Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false // 根据需要决定是否因为Opus初始化失败而返回false
} }
isInitialized = true isInitialized = true
FileLogger.d(TAG, "BLE服务初始化成功") Log.d(TAG, "BLE服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "BLE服务初始化失败: ${e.message}", e) Log.e(TAG, "BLE服务初始化失败: ${e.message}", e)
return false return false
} }
} }
@ -147,7 +147,7 @@ object BleService {
fun addCallback(callback: Callback): Boolean { fun addCallback(callback: Callback): Boolean {
if (!callbacks.contains(callback)) { if (!callbacks.contains(callback)) {
callbacks.add(callback) callbacks.add(callback)
FileLogger.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}") Log.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}")
return true return true
} }
return false return false
@ -161,7 +161,7 @@ object BleService {
fun removeCallback(callback: Callback): Boolean { fun removeCallback(callback: Callback): Boolean {
val removed = callbacks.remove(callback) val removed = callbacks.remove(callback)
if (removed) { if (removed) {
FileLogger.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}") Log.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}")
} }
return removed return removed
} }
@ -171,7 +171,7 @@ object BleService {
*/ */
fun clearCallbacks() { fun clearCallbacks() {
callbacks.clear() callbacks.clear()
FileLogger.d(TAG, "已清除所有BLE回调") Log.d(TAG, "已清除所有BLE回调")
} }
// ====================================================================================================== // ======================================================================================================
@ -184,7 +184,7 @@ object BleService {
*/ */
fun startScan(timeout: Long = 15000): Boolean { fun startScan(timeout: Long = 15000): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "BLE服务未初始化") Log.e(TAG, "BLE服务未初始化")
return false return false
} }
@ -192,7 +192,7 @@ object BleService {
val scanner = bluetoothLeScanner ?: return false val scanner = bluetoothLeScanner ?: return false
if (isScanning) return false if (isScanning) return false
FileLogger.i(TAG, "开始主动扫描BLE设备...") Log.i(TAG, "开始主动扫描BLE设备...")
scanResults.clear() scanResults.clear()
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()
@ -213,13 +213,13 @@ object BleService {
// 设置扫描超时 // 设置扫描超时
scanHandler.postDelayed({ scanHandler.postDelayed({
FileLogger.d(TAG, "扫描超时,停止扫描") Log.d(TAG, "扫描超时,停止扫描")
stopScan() stopScan()
}, timeout) }, timeout)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动扫描失败: ${e.message}", e) Log.e(TAG, "启动扫描失败: ${e.message}", e)
return false return false
} }
} }
@ -233,7 +233,7 @@ object BleService {
val scanner = bluetoothLeScanner ?: return false val scanner = bluetoothLeScanner ?: return false
if (!isScanning) return false if (!isScanning) return false
FileLogger.i(TAG, "停止主动扫描") Log.i(TAG, "停止主动扫描")
try { try {
scanner.stopScan(scanCallback) scanner.stopScan(scanCallback)
@ -243,7 +243,7 @@ object BleService {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止扫描失败: ${e.message}", e) Log.e(TAG, "停止扫描失败: ${e.message}", e)
return false return false
} }
} }
@ -277,12 +277,12 @@ object BleService {
val idx = scanResults.indexOfFirst { it.device.address == dev.address } val idx = scanResults.indexOfFirst { it.device.address == dev.address }
if (idx >= 0) scanResults[idx] = result else scanResults.add(result) if (idx >= 0) scanResults[idx] = result else scanResults.add(result)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "上报扫描结果异常: ${e.message}", e) Log.e(TAG, "上报扫描结果异常: ${e.message}", e)
} }
} }
override fun onScanFailed(errorCode: Int) { override fun onScanFailed(errorCode: Int) {
FileLogger.e(TAG, "扫描失败 error=$errorCode") Log.e(TAG, "扫描失败 error=$errorCode")
isScanning = false isScanning = false
} }
} }
@ -296,7 +296,7 @@ object BleService {
val serviceUuids = val serviceUuids =
scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无" scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无"
FileLogger.d( Log.d(
TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " + TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " +
"RSSI=${result.rssi}, 服务UUID=[$serviceUuids]" "RSSI=${result.rssi}, 服务UUID=[$serviceUuids]"
) )
@ -305,12 +305,12 @@ object BleService {
val scanData = scanRecord?.bytes val scanData = scanRecord?.bytes
if (scanData != null) { if (scanData != null) {
val hexString = scanData.joinToString("") { String.format("%02X", it) } val hexString = scanData.joinToString("") { String.format("%02X", it) }
FileLogger.d(TAG, "广播数据: $hexString") Log.d(TAG, "广播数据: $hexString")
// 解析服务数据 // 解析服务数据
scanRecord.serviceData?.forEach { (uuid, data) -> scanRecord.serviceData?.forEach { (uuid, data) ->
val dataHex = data.joinToString("") { String.format("%02X", it) } 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() val pairedMac = companionManager.getPairedMacAddress()
if (pairedMac.isNullOrEmpty()) { if (pairedMac.isNullOrEmpty()) {
FileLogger.w(TAG, "未找到已配对的设备 MAC 地址") Log.w(TAG, "未找到已配对的设备 MAC 地址")
return return
} }
FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
// 调用现有的 connect 方法 // 调用现有的 connect 方法
connect(pairedMac) connect(pairedMac)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e) Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
} }
} }
@ -351,7 +351,7 @@ object BleService {
*/ */
fun connect(address: String) { fun connect(address: String) {
if (connectionState.value == BleConst.STATE_CONNECTED) { if (connectionState.value == BleConst.STATE_CONNECTED) {
FileLogger.w(TAG, "已经连接中,忽略连接请求") Log.w(TAG, "已经连接中,忽略连接请求")
return return
} }
@ -373,7 +373,7 @@ object BleService {
context, false, gattCallback, BluetoothDevice.TRANSPORT_LE context, false, gattCallback, BluetoothDevice.TRANSPORT_LE
) )
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e) Log.e(TAG, "连接设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
} }
} }
@ -395,7 +395,7 @@ object BleService {
gatt.disconnect() gatt.disconnect()
gatt.close() gatt.close()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "关闭GATT连接异常: ${e.message}", e) Log.e(TAG, "关闭GATT连接异常: ${e.message}", e)
} }
bluetoothGatt = null bluetoothGatt = null
notifyChar = null notifyChar = null
@ -452,7 +452,7 @@ object BleService {
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
if (notifyChar == null || writeChar == null) { if (notifyChar == null || writeChar == null) {
FileLogger.e(TAG, "未找到主服务所需特征") Log.e(TAG, "未找到主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
return return
} }
@ -462,9 +462,9 @@ object BleService {
// 设置音频服务的通知(如果存在) // 设置音频服务的通知(如果存在)
if (audioChar != null) { if (audioChar != null) {
setupNotifications(g, audioChar) setupNotifications(g, audioChar)
FileLogger.i(TAG, "音频服务特征找到并设置通知") Log.i(TAG, "音频服务特征找到并设置通知")
} else { } else {
FileLogger.w(TAG, "音频服务特征未找到") Log.w(TAG, "音频服务特征未找到")
} }
// 设置主服务的通知 // 设置主服务的通知
setupNotifications(g, notifyChar) setupNotifications(g, notifyChar)
@ -496,7 +496,7 @@ object BleService {
} }
// 其他未知数据 // 其他未知数据
else -> { else -> {
FileLogger.d( Log.d(
TAG, TAG,
"收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}" "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}"
) )
@ -505,7 +505,7 @@ object BleService {
} }
// 其他未知特征 // 其他未知特征
else -> { else -> {
FileLogger.d( Log.d(
TAG, TAG,
"收到未知特征数据: ${c.uuid}, 数据=${ "收到未知特征数据: ${c.uuid}, 数据=${
data.joinToString(", ") { data.joinToString(", ") {
@ -520,7 +520,7 @@ object BleService {
override fun onCharacteristicWrite( override fun onCharacteristicWrite(
g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int
) { ) {
FileLogger.i( Log.i(
TAG, TAG,
"写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}"
) )
@ -530,7 +530,7 @@ object BleService {
g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int
) { ) {
if (status == BluetoothGatt.GATT_SUCCESS) 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?) { private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) {
if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) { if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) {
FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") Log.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}")
return return
} }
@ -558,10 +558,10 @@ object BleService {
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(data) opusManager?.writeAudioStream(data)
} else { } else {
// FileLogger.d(TAG, "Opus解码流未启动,忽略音频数据") // Log.d(TAG, "Opus解码流未启动,忽略音频数据")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e) Log.e(TAG, "处理音频数据异常: ${e.message}", e)
} }
} }
@ -570,12 +570,12 @@ object BleService {
* 处理设备响应数据 - 帧头为0xBB * 处理设备响应数据 - 帧头为0xBB
*/ */
private fun processDeviceResponse(data: ByteArray) { private fun processDeviceResponse(data: ByteArray) {
FileLogger.d( Log.d(
TAG, TAG,
"收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
) )
if (data.size < 3) { if (data.size < 3) {
FileLogger.e(TAG, "设备响应数据格式错误:数据长度过短") Log.e(TAG, "设备响应数据格式错误:数据长度过短")
return return
} }
@ -584,7 +584,7 @@ object BleService {
// 判断命令失败响应(长度为0) // 判断命令失败响应(长度为0)
if (data.size == 4 && data[2].toInt() and 0xFF == 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 // 创建失败结果Map
val resultMap = mapOf( val resultMap = mapOf(
"success" to false, "success" to false,
@ -600,7 +600,7 @@ object BleService {
// 检查数据长度 // 检查数据长度
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return return
} }
@ -610,7 +610,7 @@ object BleService {
val receivedCrc = data[data.size - 1].toInt() and 0xFF val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e( Log.e(
TAG, TAG,
"设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
) )
@ -623,7 +623,7 @@ object BleService {
System.arraycopy(data, 3, responseData, 0, length) System.arraycopy(data, 3, responseData, 0, length)
} }
FileLogger.d( Log.d(
TAG, TAG,
"收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${
responseData.joinToString(", ") { responseData.joinToString(", ") {
@ -648,7 +648,7 @@ object BleService {
val version = val version =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
resultMap["version"] = version resultMap["version"] = version
FileLogger.i(TAG, "接收到目标版本号: $version") Log.i(TAG, "接收到目标版本号: $version")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -666,7 +666,7 @@ object BleService {
val productId = val productId =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
resultMap["productId"] = productId resultMap["productId"] = productId
FileLogger.i(TAG, "接收到产品ID: $productId") Log.i(TAG, "接收到产品ID: $productId")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -682,7 +682,7 @@ object BleService {
if (responseData.isNotEmpty()) { if (responseData.isNotEmpty()) {
val colorId = (responseData[0].toInt() and 0xFF).toString() val colorId = (responseData[0].toInt() and 0xFF).toString()
resultMap["colorId"] = colorId resultMap["colorId"] = colorId
FileLogger.i(TAG, "接收到耳机颜色ID: $colorId") Log.i(TAG, "接收到耳机颜色ID: $colorId")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -719,7 +719,7 @@ object BleService {
resultMap["caseCharging"] = caseCharging resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i( Log.i(
TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + TAG, "接收到电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
@ -763,7 +763,7 @@ object BleService {
resultMap["statusDesc"] = statusDesc resultMap["statusDesc"] = statusDesc
resultMap["channelDesc"] = channelDesc resultMap["channelDesc"] = channelDesc
FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -777,7 +777,7 @@ object BleService {
"rawData" to responseData, "rawData" to responseData,
"isUnknown" to true "isUnknown" to true
) )
FileLogger.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}")
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
} }
} }
@ -787,12 +787,12 @@ object BleService {
* 处理设备主动上报数据 - 帧头为0xCC * 处理设备主动上报数据 - 帧头为0xCC
*/ */
private fun processDeviceNotification(data: ByteArray) { private fun processDeviceNotification(data: ByteArray) {
FileLogger.d( Log.d(
TAG, TAG,
"收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
) )
if (data.size < 3) { if (data.size < 3) {
FileLogger.e(TAG, "设备主动上报数据格式错误:数据长度过短") Log.e(TAG, "设备主动上报数据格式错误:数据长度过短")
return return
} }
@ -802,7 +802,7 @@ object BleService {
// 检查数据长度 // 检查数据长度
if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return return
} }
@ -812,7 +812,7 @@ object BleService {
val receivedCrc = data[data.size - 1].toInt() and 0xFF val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e( Log.e(
TAG, TAG,
"设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
) )
@ -825,7 +825,7 @@ object BleService {
System.arraycopy(data, 3, notifyData, 0, length) System.arraycopy(data, 3, notifyData, 0, length)
} }
FileLogger.d( Log.d(
TAG, TAG,
"收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${
notifyData.joinToString(", ") { notifyData.joinToString(", ") {
@ -870,7 +870,7 @@ object BleService {
resultMap["caseCharging"] = caseCharging resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i( Log.i(
TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
@ -882,7 +882,7 @@ object BleService {
} }
BleConst.CMD_WAKE_UP -> { BleConst.CMD_WAKE_UP -> {
FileLogger.i(TAG, "收到唤醒指令") Log.i(TAG, "收到唤醒指令")
// 接收到唤醒信号,打开编码器 (设备侧) // 接收到唤醒信号,打开编码器 (设备侧)
openEncoder() openEncoder()
notifyWakeupSignalReceived() notifyWakeupSignalReceived()
@ -898,7 +898,7 @@ object BleService {
"isUnknown" to true, "isUnknown" to true,
"isNotification" to true // 标记为主动上报 "isNotification" to true // 标记为主动上报
) )
FileLogger.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}")
notifyDeviceInfoReceived(infoType, resultMap) notifyDeviceInfoReceived(infoType, resultMap)
} }
} }
@ -915,13 +915,13 @@ object BleService {
packetSize: Int = 40 // 确认设备端Opus编码帧长 packetSize: Int = 40 // 确认设备端Opus编码帧长
): Boolean { ): Boolean {
if (opusManager == null) { if (opusManager == null) {
FileLogger.e(TAG, "OpusManager未初始化,无法开始解码") Log.e(TAG, "OpusManager未初始化,无法开始解码")
return false return false
} }
// 如果已经在解码流,先停止 // 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() opusManager?.stopDecodeStream()
FileLogger.d(TAG, "已停止正在进行的Opus解码流") Log.d(TAG, "已停止正在进行的Opus解码流")
} }
val option = OpusOption() val option = OpusOption()
@ -930,29 +930,29 @@ object BleService {
.setSampleRate(sampleRate) .setSampleRate(sampleRate)
.setPacketSize(packetSize) .setPacketSize(packetSize)
FileLogger.d(TAG, "准备开始Opus数据流解码, 参数: $option") Log.d(TAG, "准备开始Opus数据流解码, 参数: $option")
var streamStartedSuccessfully = false var streamStartedSuccessfully = false
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) { override fun onDecodeStream(data: ByteArray?) {
if (data != null) { if (data != null) {
// FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes") // Log.d(TAG, "Opus解码数据: ${data.size} bytes")
notifyAudioDataReceived(data) notifyAudioDataReceived(data)
} }
} }
override fun onStart() { override fun onStart() {
streamStartedSuccessfully = true streamStartedSuccessfully = true
FileLogger.i(TAG, "Opus数据流解码已开始") Log.i(TAG, "Opus数据流解码已开始")
// 可以通过回调通知上层解码已开始 // 可以通过回调通知上层解码已开始
} }
override fun onComplete(outPath: String?) { override fun onComplete(outPath: String?) {
FileLogger.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") Log.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)")
} }
override fun onError(code: Int, message: String?) { 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 { private fun stopOpusStreamDecoding(): Boolean {
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() opusManager?.stopDecodeStream()
FileLogger.i(TAG, "已停止Opus数据流解码") Log.i(TAG, "已停止Opus数据流解码")
return true return true
} }
FileLogger.d(TAG, "Opus数据流未在解码或OpusManager未初始化") Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化")
return false return false
} }
@ -980,7 +980,7 @@ object BleService {
* 获取耳机版本号 * 获取耳机版本号
*/ */
fun getDeviceVersion(): Boolean { fun getDeviceVersion(): Boolean {
FileLogger.i(TAG, "获取耳机版本号...") Log.i(TAG, "获取耳机版本号...")
return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -988,7 +988,7 @@ object BleService {
* 获取产品ID * 获取产品ID
*/ */
fun getProductId(): Boolean { fun getProductId(): Boolean {
FileLogger.i(TAG, "获取耳机产品ID...") Log.i(TAG, "获取耳机产品ID...")
return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -996,7 +996,7 @@ object BleService {
* 获取耳机颜色ID * 获取耳机颜色ID
*/ */
fun getColorId(): Boolean { fun getColorId(): Boolean {
FileLogger.i(TAG, "获取耳机颜色ID...") Log.i(TAG, "获取耳机颜色ID...")
return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1004,7 +1004,7 @@ object BleService {
* 获取耳机电量信息 * 获取耳机电量信息
*/ */
fun getBatteryInfo(): Boolean { fun getBatteryInfo(): Boolean {
FileLogger.i(TAG, "获取耳机电量信息...") Log.i(TAG, "获取耳机电量信息...")
return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1012,7 +1012,7 @@ object BleService {
* 控制编解码 - 关闭编解码 * 控制编解码 - 关闭编解码
*/ */
fun closeCodec(): Boolean { fun closeCodec(): Boolean {
FileLogger.i(TAG, "关闭编解码...") Log.i(TAG, "关闭编解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), BleConst.CMD_CONTROL_CODEC.toByte(),
@ -1028,7 +1028,7 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openDecoder(): Boolean { fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// val opusStarted = startOpusStreamDecoding( // val opusStarted = startOpusStreamDecoding(
// hasHeader = false, // BLE传输的Opus数据通常无文件头 // hasHeader = false, // BLE传输的Opus数据通常无文件头
@ -1041,7 +1041,7 @@ object BleService {
// ) // )
FileLogger.i(TAG, "打开解码...") Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_DECODE_ON.toByte(), BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
@ -1055,9 +1055,9 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openA2DPDecoder(): Boolean { fun openA2DPDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// FileLogger.i(TAG, "打开解码...") // Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
@ -1071,7 +1071,7 @@ object BleService {
* 控制编解码 - A2DP播放 * 控制编解码 - A2DP播放
*/ */
fun playA2DP(): Boolean { fun playA2DP(): Boolean {
FileLogger.i(TAG, "切换至A2DP播放模式...") Log.i(TAG, "切换至A2DP播放模式...")
return sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) return sendCommand(BleConst.CMD_PLAY_A2DP.toByte())
} }
@ -1079,7 +1079,7 @@ object BleService {
* 控制编解码 - 打开编码 * 控制编解码 - 打开编码
*/ */
fun openEncoder(): Boolean { fun openEncoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// 目前仅发送命令通知设备开始编码。 // 目前仅发送命令通知设备开始编码。
@ -1099,11 +1099,11 @@ object BleService {
*/ */
fun setChannelMode(channelMode: Int): Boolean { fun setChannelMode(channelMode: Int): Boolean {
when (channelMode) { when (channelMode) {
BleConst.AUDIO_CHANNEL_LEFT -> FileLogger.i(TAG, "设置左声道模式...") BleConst.AUDIO_CHANNEL_LEFT -> Log.i(TAG, "设置左声道模式...")
BleConst.AUDIO_CHANNEL_RIGHT -> FileLogger.i(TAG, "设置右声道模式...") BleConst.AUDIO_CHANNEL_RIGHT -> Log.i(TAG, "设置右声道模式...")
BleConst.AUDIO_CHANNEL_STEREO -> FileLogger.i(TAG, "设置立体声模式...") BleConst.AUDIO_CHANNEL_STEREO -> Log.i(TAG, "设置立体声模式...")
else -> { else -> {
FileLogger.e(TAG, "无效的声道模式: $channelMode") Log.e(TAG, "无效的声道模式: $channelMode")
return false return false
} }
} }
@ -1121,7 +1121,7 @@ object BleService {
* 音量增大 * 音量增大
*/ */
fun volumeUp(): Boolean { fun volumeUp(): Boolean {
FileLogger.i(TAG, "设备音量增加...") Log.i(TAG, "设备音量增加...")
return sendCommand(BleConst.CMD_VOLUME_UP.toByte()) return sendCommand(BleConst.CMD_VOLUME_UP.toByte())
} }
@ -1129,7 +1129,7 @@ object BleService {
* 音量减小 * 音量减小
*/ */
fun volumeDown(): Boolean { fun volumeDown(): Boolean {
FileLogger.i(TAG, "设备音量减小...") Log.i(TAG, "设备音量减小...")
return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte())
} }
@ -1183,7 +1183,7 @@ object BleService {
it.release() it.release()
} }
opusManager = null opusManager = null
FileLogger.d(TAG, "OpusManager已释放") Log.d(TAG, "OpusManager已释放")
isInitialized = false // 标记为未初始化 isInitialized = false // 标记为未初始化
} }
@ -1199,7 +1199,7 @@ object BleService {
*/ */
private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean {
if (!checkConn()) { if (!checkConn()) {
FileLogger.e(TAG, "发送命令失败: 设备未连接") Log.e(TAG, "发送命令失败: 设备未连接")
return false return false
} }
@ -1230,7 +1230,7 @@ object BleService {
cmdPacket[3 + cmdLength] = crc cmdPacket[3 + cmdLength] = crc
// 记录命令详情 // 记录命令详情
FileLogger.d( Log.d(
TAG, TAG,
"发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " +
"数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " +
@ -1241,7 +1241,7 @@ object BleService {
writeChar!!.value = cmdPacket writeChar!!.value = cmdPacket
return bluetoothGatt?.writeCharacteristic(writeChar) ?: false return bluetoothGatt?.writeCharacteristic(writeChar) ?: false
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送命令异常: ${e.message}", e) Log.e(TAG, "发送命令异常: ${e.message}", e)
return false return false
} }
} }
@ -1274,7 +1274,7 @@ object BleService {
try { try {
callback.onScanResult(devices) callback.onScanResult(devices)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发扫描结果回调异常", e) Log.e(TAG, "分发扫描结果回调异常", e)
} }
} }
} }
@ -1287,7 +1287,7 @@ object BleService {
try { try {
callback.onConnectionStateChanged(state) callback.onConnectionStateChanged(state)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发连接状态回调异常", e) Log.e(TAG, "分发连接状态回调异常", e)
} }
} }
} }
@ -1300,7 +1300,7 @@ object BleService {
try { try {
callback.onAudioDataReceived(data) callback.onAudioDataReceived(data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发音频数据回调异常", e) Log.e(TAG, "分发音频数据回调异常", e)
} }
} }
} }
@ -1313,7 +1313,7 @@ object BleService {
try { try {
callback.onWakeupSignalReceived() callback.onWakeupSignalReceived()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发唤醒信号回调异常", e) Log.e(TAG, "分发唤醒信号回调异常", e)
} }
} }
} }
@ -1326,7 +1326,7 @@ object BleService {
try { try {
callback.onDeviceInfoReceived(infoType, infoData) callback.onDeviceInfoReceived(infoType, infoData)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发设备信息回调异常", e) Log.e(TAG, "分发设备信息回调异常", e)
} }
} }
} }

11
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.MethodChannel.Result
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.PluginRegistry import io.flutter.plugin.common.PluginRegistry
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* BleServicePlugin * BleServicePlugin
@ -52,8 +52,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
private var activity: android.app.Activity? = null private var activity: android.app.Activity? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
FileLogger.init(flutterPluginBinding.applicationContext) Log.d(TAG, "BleServicePlugin onAttachedToEngine")
FileLogger.d(TAG, "BleServicePlugin onAttachedToEngine")
// 设置方法通道 // 设置方法通道
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.ble_service") methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.ble_service")
@ -150,14 +149,14 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
try { try {
eventSink.success(data) eventSink.success(data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "$errorPrefix: ${e.message}", e) Log.e(TAG, "$errorPrefix: ${e.message}", e)
} }
} else { } else {
mainHandler.post { mainHandler.post {
try { try {
eventSink.success(data) eventSink.success(data)
} catch (e: Exception) { } 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) { 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) { when (call.method) {
"isConnected" -> { "isConnected" -> {

13
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.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
/** /**
* BleWakeReceiver * BleWakeReceiver
@ -15,8 +15,7 @@ class BleWakeReceiver : BroadcastReceiver() {
private val TAG = "BleWakeReceiver" private val TAG = "BleWakeReceiver"
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
FileLogger.init(context) Log.i(TAG, "收到BLE扫描广播: ${intent.action}")
FileLogger.i(TAG, "收到BLE扫描广播: ${intent.action}")
if (intent.action == BleConst.ACTION_SCAN_RESULT) { if (intent.action == BleConst.ACTION_SCAN_RESULT) {
// 从扫描结果中提取MAC地址 // 从扫描结果中提取MAC地址
@ -44,7 +43,7 @@ class BleWakeReceiver : BroadcastReceiver() {
val serviceClass = try { val serviceClass = try {
Class.forName("com.yunqiinnovation.agent_service.BleMediaService") Class.forName("com.yunqiinnovation.agent_service.BleMediaService")
} catch (e: ClassNotFoundException) { } catch (e: ClassNotFoundException) {
FileLogger.i(TAG, "BleMediaService不存在") Log.i(TAG, "BleMediaService不存在")
return return
} }
@ -57,13 +56,13 @@ class BleWakeReceiver : BroadcastReceiver() {
// 启动前台服务 // 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent) context.startForegroundService(serviceIntent)
FileLogger.i(TAG, "已启动前台服务: ${serviceClass.name}") Log.i(TAG, "已启动前台服务: ${serviceClass.name}")
} else { } else {
context.startService(serviceIntent) context.startService(serviceIntent)
FileLogger.i(TAG, "已启动服务: ${serviceClass.name}") Log.i(TAG, "已启动服务: ${serviceClass.name}")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动BLE服务失败: ${e.message}", e) Log.e(TAG, "启动BLE服务失败: ${e.message}", e)
} }
} }
} }

2
local_plugins/bytedance_speech/android/build.gradle.kts

@ -7,7 +7,7 @@ plugins {
} }
android { android {
namespace = "com.yunqiinnovation.ble_service" namespace = "com.deep_voice.bytedance_speech"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {

2
local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.deep_voice.bytedance_speech"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />

170
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 package com.deep_voice.bytedance_speech
import android.content.Context import android.content.Context
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -13,6 +14,7 @@ import okhttp3.WebSocketListener
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level import okhttp3.logging.HttpLoggingInterceptor.Level
import okio.ByteString import okio.ByteString
import org.json.JSONObject
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -31,6 +33,7 @@ class BytedanceASR(
private val statusCallback: (String) -> Unit private val statusCallback: (String) -> Unit
) { ) {
companion object { companion object {
private const val TAG = "BytedanceASR"
// 协议常量 // 协议常量
private const val PROTOCOL_VERSION = 0x01 private const val PROTOCOL_VERSION = 0x01
private const val DEFAULT_HEADER_SIZE = 0x01 private const val DEFAULT_HEADER_SIZE = 0x01
@ -80,7 +83,7 @@ class BytedanceASR(
private var supportedLanguages: List<String> = listOf("zh-CN") private var supportedLanguages: List<String> = listOf("zh-CN")
// 状态控制 // 状态控制
private var isRunning = false private var isRecognizing = false
private var isLastPacket = false private var isLastPacket = false
private var isContinuousMode = false private var isContinuousMode = false
private var isInitialized = false private var isInitialized = false
@ -97,10 +100,10 @@ class BytedanceASR(
} }
client = OkHttpClient.Builder() client = OkHttpClient.Builder()
.pingInterval(50, TimeUnit.SECONDS) .pingInterval(30, TimeUnit.SECONDS)
.addInterceptor(loggingInterceptor) .addInterceptor(loggingInterceptor)
.readTimeout(100, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(100, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS)
.build() .build()
} }
@ -146,7 +149,7 @@ class BytedanceASR(
return@withContext false return@withContext false
} }
if (isRunning) { if (isRecognizing) {
errorCallback("BUSY", "语音识别正在进行中") errorCallback("BUSY", "语音识别正在进行中")
return@withContext false return@withContext false
} }
@ -163,12 +166,12 @@ class BytedanceASR(
// 等待连接建立 // 等待连接建立
var attempts = 0 var attempts = 0
while (!isRunning && attempts < 10) { while (!isRecognizing && attempts < 10) {
delay(300) delay(300)
attempts++ attempts++
} }
if (!isRunning) { if (!isRecognizing) {
errorCallback("CONNECTION_ERROR", "无法建立连接") errorCallback("CONNECTION_ERROR", "无法建立连接")
return@withContext false return@withContext false
} }
@ -197,7 +200,7 @@ class BytedanceASR(
return@withContext false return@withContext false
} }
if (isRunning) { if (isRecognizing) {
errorCallback("BUSY", "语音识别正在进行中") errorCallback("BUSY", "语音识别正在进行中")
return@withContext false return@withContext false
} }
@ -223,7 +226,7 @@ class BytedanceASR(
*/ */
suspend fun stopContinuousRecognition(): Boolean = withContext(Dispatchers.IO) { suspend fun stopContinuousRecognition(): Boolean = withContext(Dispatchers.IO) {
try { try {
if (!isRunning || !isContinuousMode) { if (!isRecognizing || !isContinuousMode) {
return@withContext true // 已经停止或非连续模式 return@withContext true // 已经停止或非连续模式
} }
@ -242,7 +245,7 @@ class BytedanceASR(
*/ */
suspend fun pushAudioData(audioData: ByteArray): Boolean = withContext(Dispatchers.IO) { suspend fun pushAudioData(audioData: ByteArray): Boolean = withContext(Dispatchers.IO) {
try { try {
if (!isRunning) { if (!isRecognizing) {
errorCallback("NOT_RUNNING", "语音识别未启动") errorCallback("NOT_RUNNING", "语音识别未启动")
return@withContext false return@withContext false
} }
@ -277,10 +280,10 @@ class BytedanceASR(
/** /**
* 开始语音识别 * 开始语音识别
*/ */
private suspend fun start() = withContext(Dispatchers.IO) { suspend fun start() = withContext(Dispatchers.IO) {
if (isRunning) return@withContext if (isRecognizing) return@withContext
isRunning = true isRecognizing = true
isLastPacket = false isLastPacket = false
seq = 1 seq = 1
@ -303,114 +306,81 @@ class BytedanceASR(
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
val logId = response.header("X-Tt-Logid") 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) sendInitialRequest(webSocket)
} }
override fun onMessage(webSocket: WebSocket, bytes: ByteString) { override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "收到文本消息: $text")
try { try {
val data = bytes.toByteArray() val json = JSONObject(text)
val response = parseResponse(data) // 解析结果
resultCallback(json.optString("text", ""), false)
// 根据消息类型处理响应 } catch (e: Exception) {
when (response.messageType) { Log.e(TAG, "解析消息失败", e)
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)
} }
// 回调识别结果 override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
resultCallback(resultText, isFinal) Log.d(TAG, "收到二进制消息")
// 在实际实现中解析二进制消息
} }
// 如果是最后一个包,关闭连接 override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
if (isFinal) { Log.d(TAG, "连接关闭中: $code, $reason")
// 单次识别完成回调 isRecognizing = false
if (!isContinuousMode && singleRecognitionCallback != null) {
val finalResult = onceResultBuilder.toString()
singleRecognitionCallback?.invoke(finalResult)
onceResultBuilder.clear()
singleRecognitionCallback = null
} }
if (!isContinuousMode) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
isRunning = false Log.d(TAG, "连接已关闭: $code, $reason")
updateStatus(STATUS_STOPPED) isRecognizing = false
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")
}
}
} catch (e: Exception) {
errorCallback("PARSE_ERROR", e.message ?: "解析响应失败")
updateStatus(STATUS_ERROR)
}
} }
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
isRunning = false Log.e(TAG, "连接失败", t)
errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") errorCallback("CONNECTION_ERROR", t.message ?: "连接失败")
updateStatus(STATUS_ERROR) isRecognizing = false
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
isRunning = false
updateStatus(STATUS_STOPPED)
println("ASR WebSocket连接关闭: $code, $reason")
} }
}) })
} }
/** /**
* 发送音频数据 * 向服务器发送音频数据
*/ */
private suspend fun feedAudioData(audioData: ByteArray) = withContext(Dispatchers.IO) { suspend fun feedAudioData(audioData: ByteArray) {
if (!isRunning || isLastPacket) return@withContext if (!isRecognizing || webSocket == null) {
Log.w(TAG, "未开始识别或WebSocket未连接")
return
}
webSocket?.let { ws -> try {
// 发送音频请求 // 在实际实现中需要按照火山引擎的协议格式封装音频数据
sendAudioRequest(ws, audioData, false) val byteString = ByteString.of(*audioData)
webSocket?.send(byteString)
} catch (e: Exception) {
Log.e(TAG, "发送音频数据失败", e)
} }
} }
/** /**
* 停止识别 * 停止语音识别
*/ */
private suspend fun stop() = withContext(Dispatchers.IO) { suspend fun stop() {
if (!isRunning || isLastPacket) return@withContext if (!isRecognizing) {
return
}
isLastPacket = true try {
// 发送最后一帧音频数据,标记结束
// 在实际实现中需要按照协议发送结束标记
webSocket?.let { ws -> isRecognizing = false
// 发送最后一个音频包 webSocket?.close(1000, "正常关闭")
sendAudioRequest(ws, ByteArray(0), true) webSocket = null
} catch (e: Exception) {
Log.e(TAG, "停止识别失败", e)
} }
} }
@ -420,7 +390,7 @@ class BytedanceASR(
fun release() { fun release() {
webSocket?.cancel() webSocket?.cancel()
webSocket = null webSocket = null
isRunning = false isRecognizing = false
isLastPacket = false isLastPacket = false
updateStatus(STATUS_STOPPED) updateStatus(STATUS_STOPPED)
} }
@ -429,6 +399,7 @@ class BytedanceASR(
* 发送初始配置请求 * 发送初始配置请求
*/ */
private fun sendInitialRequest(webSocket: WebSocket) { private fun sendInitialRequest(webSocket: WebSocket) {
try {
// 构建请求参数 // 构建请求参数
val user = JsonObject().apply { val user = JsonObject().apply {
addProperty("uid", "test") addProperty("uid", "test")
@ -454,7 +425,7 @@ class BytedanceASR(
} }
val payloadStr = payload.toString() val payloadStr = payload.toString()
println("ASR初始请求: $payloadStr") Log.d(TAG, "ASR初始请求: $payloadStr")
// 压缩负载 // 压缩负载
val compressedPayload = gzipCompress(payloadStr.toByteArray()) val compressedPayload = gzipCompress(payloadStr.toByteArray())
@ -490,13 +461,18 @@ class BytedanceASR(
System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size) System.arraycopy(compressedPayload, 0, requestBytes, offset, compressedPayload.size)
// 发送请求 // 发送请求
webSocket.send(ByteString.of(requestBytes)) val byteString = ByteString.of(*requestBytes)
webSocket.send(byteString)
} catch (e: Exception) {
Log.e(TAG, "发送初始请求失败", e)
}
} }
/** /**
* 发送音频数据请求 * 发送音频数据请求
*/ */
private fun sendAudioRequest(webSocket: WebSocket, audioData: ByteArray, isLast: Boolean) { private fun sendAudioRequest(webSocket: WebSocket, audioData: ByteArray, isLast: Boolean) {
try {
seq++ seq++
// 如果是最后一个包,使用负序列号 // 如果是最后一个包,使用负序列号
@ -545,7 +521,11 @@ class BytedanceASR(
} }
// 发送请求 // 发送请求
webSocket.send(ByteString.of(requestBytes)) val byteString = ByteString.of(*requestBytes)
webSocket.send(byteString)
} catch (e: Exception) {
Log.e(TAG, "发送音频请求失败", e)
}
} }
/** /**
@ -619,7 +599,7 @@ class BytedanceASR(
return "" return ""
} catch (e: Exception) { } catch (e: Exception) {
println("解析JSON失败: ${e.message}") Log.e(TAG, "解析JSON失败", e)
return "" return ""
} }
} }

560
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt

@ -3,216 +3,195 @@ package com.deep_voice.bytedance_speech
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentLinkedQueue
/** 火山引擎语音识别和语音合成插件 */ /**
* 字节跳动语音服务Flutter插件
* 提供TTS和ASR能力,通过独立的事件通道进行通信
*/
class BytedanceSpeechPlugin : FlutterPlugin { class BytedanceSpeechPlugin : FlutterPlugin {
private val TAG = "BytedanceSpeechPlugin" companion object {
private lateinit var context: Context private const val TAG = "BytedanceSpeechPlugin"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val mainHandler = Handler(Looper.getMainLooper())
// ASR相关 // 通道名称
private lateinit var asrChannel: MethodChannel private const val TTS_METHOD_CHANNEL = "com.deep_voice.bytedance_tts"
private lateinit var asrEventChannel: EventChannel private const val ASR_METHOD_CHANNEL = "com.deep_voice.bytedance_asr"
private var asrEventSink: EventChannel.EventSink? = null
private var asrInstance: BytedanceASR? = null
// TTS相关 // 事件通道
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"
}
// Flutter插件相关
private lateinit var ttsChannel: MethodChannel private lateinit var ttsChannel: MethodChannel
private lateinit var ttsEventChannel: EventChannel private lateinit var asrChannel: MethodChannel
private lateinit var context: Context
// TTS事件通道
private var ttsEventSink: EventChannel.EventSink? = null private var ttsEventSink: EventChannel.EventSink? = null
private var ttsInstance: BytedanceTTS? = null private var ttsAudioSink: EventChannel.EventSink? = null
// ASR事件发送方法 // ASR事件通道
private fun sendAsrEvent(event: Map<String, Any>) { private var asrEventSink: EventChannel.EventSink? = null
println("发送ASR事件: $event")
if (asrEventSink == null) {
println("无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post { // 语音服务实例
try { private var bytedanceTTS: BytedanceTTS? = null
asrEventSink?.success(event) private var bytedanceASR: BytedanceASR? = null
println("ASR事件发送成功")
} catch (e: Exception) {
println("发送ASR事件失败: ${e.message}")
}
}
}
// TTS事件发送方法 // 协程作用域
private fun sendTtsEvent(event: Map<String, Any>) { private val job = SupervisorJob()
println("发送TTS事件: $event") private val scope = CoroutineScope(Dispatchers.Main + job)
if (ttsEventSink == null) {
println("无法发送TTS事件:事件通道未准备好")
return
}
mainHandler.post { // 主线程Handler
try { private val mainHandler = Handler(Looper.getMainLooper())
ttsEventSink?.success(event)
println("TTS事件发送成功")
} catch (e: Exception) {
println("发送TTS事件失败: ${e.message}")
}
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext context = flutterPluginBinding.applicationContext
// 初始化ASR通道 // 初始化TTS方法通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr") ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, TTS_METHOD_CHANNEL)
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR方法通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, ASR_METHOD_CHANNEL)
asrChannel.setMethodCallHandler(AsrMethodHandler()) asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道 // 设置TTS事件通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts") setupTtsEventChannels(flutterPluginBinding)
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR事件通道 // 设置ASR事件通道
asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr_events") setupAsrEventChannel(flutterPluginBinding)
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
}
override fun onCancel(arguments: Any?) { Log.d(TAG, "字节跳动语音插件已附加到Flutter引擎")
asrEventSink = null
} }
})
// 初始化TTS事件通道 /**
ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts_events") * 设置TTS相关的事件通道
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { */
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?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events ttsEventSink = events
Log.d(TAG, "TTS事件流已建立连接")
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
ttsEventSink = null ttsEventSink = null
Log.d(TAG, "TTS事件流已取消")
} }
})
} }
)
// ASR方法处理器 // TTS音频数据通道
inner class AsrMethodHandler : MethodCallHandler { EventChannel(binding.binaryMessenger, TTS_AUDIO_CHANNEL).setStreamHandler(
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { object : EventChannel.StreamHandler {
when (call.method) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
"initialize" -> { ttsAudioSink = events
val appId = call.argument<String>("appId") ?: "" Log.d(TAG, "TTS音频流已建立连接")
val accessToken = call.argument<String>("accessToken") ?: "" }
val streamMode = call.argument<Boolean>("streamMode") ?: true
val enablePunctuation = call.argument<Boolean>("enablePunctuation") ?: true
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val bits = call.argument<Int>("bits") ?: 16
val channels = call.argument<Int>("channels") ?: 1
try { override fun onCancel(arguments: Any?) {
asrInstance = BytedanceASR( ttsAudioSink = null
context = context, Log.d(TAG, "TTS音频流已取消")
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)
} }
)
} }
"startRecognition" -> {
if (asrEventSink == null) { /**
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始识别", null) * 设置ASR相关的事件通道
return */
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事件流已建立连接")
} }
scope.launch { override fun onCancel(arguments: Any?) {
try { asrEventSink = null
asrInstance?.start() Log.d(TAG, "ASR事件流已取消")
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)
} }
)
} }
/**
* TTS方法处理器
*/
inner class TtsMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val ttsAppId = call.argument<String>("ttsAppId") ?: ""
val ttsAppToken = call.argument<String>("ttsAppToken") ?: ""
val language = call.argument<String>("language") ?: "zh-CN"
initializeTTS(ttsAppId, ttsAppToken, language, result)
} }
"setVoice" -> {
val voice = call.argument<String>("voice") ?: ""
setVoice(voice, result)
} }
"feedAudioData" -> { "speakOnce" -> {
val audioData = call.argument<ByteArray>("audioData") val text = call.argument<String>("text") ?: ""
if (audioData != null) { speakOnce(text, result)
scope.launch {
try {
asrInstance?.feedAudioData(audioData)
scope.launch(Dispatchers.Main) {
result.success(true)
} }
} catch (e: Exception) { "speakStream" -> {
scope.launch(Dispatchers.Main) { val text = call.argument<String>("text") ?: ""
result.error("FEED_ERROR", e.message, null) speakStream(text, result)
} }
"flushStream" -> {
flushStream(result)
} }
"stopTTS" -> {
stopTTS(result)
} }
} else { "disposeTTS" -> {
result.error("INVALID_ARGUMENT", "音频数据不能为空", null) disposeTTS(result)
} }
else -> {
result.notImplemented()
} }
"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)
} }
} }
/**
* ASR方法处理器
*/
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initializeASR" -> {
result.notImplemented()
} }
"startListening" -> {
result.notImplemented()
} }
"dispose" -> { "stopListening" -> {
try { result.notImplemented()
asrInstance?.release()
asrInstance = null
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
} }
"cancelASR" -> {
result.notImplemented()
} }
else -> { else -> {
result.notImplemented() result.notImplemented()
@ -221,111 +200,270 @@ class BytedanceSpeechPlugin : FlutterPlugin {
} }
} }
// TTS方法处理器 override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
inner class TtsMethodHandler : MethodCallHandler { ttsChannel.setMethodCallHandler(null)
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { asrChannel.setMethodCallHandler(null)
when (call.method) {
"initialize" -> { // 释放资源
val appId = call.argument<String>("appId") ?: "" bytedanceTTS?.release()
val accessToken = call.argument<String>("accessToken") ?: "" bytedanceTTS = null
val speaker = call.argument<String>("speaker") ?: ""
val format = call.argument<String>("format") ?: "mp3"
val sampleRate = call.argument<Int>("sampleRate") ?: 24000
val enableTimestamp = call.argument<Boolean>("enableTimestamp") ?: true
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 { try {
ttsInstance = BytedanceTTS( if (ttsAppId.isEmpty() || ttsAppToken.isEmpty()) {
result.error(
"INVALID_PARAMS",
"AppID和AppToken不能为空",
null
)
return@launch
}
// 确保只有一个TTS实例
bytedanceTTS?.release()
// 创建新的TTS实例
bytedanceTTS = BytedanceTTS(
context = context, context = context,
appId = appId, // 音频数据回调
accessToken = accessToken,
speaker = speaker,
format = format,
sampleRate = sampleRate,
enableTimestamp = enableTimestamp,
audioCallback = { audioData -> audioCallback = { audioData ->
sendTtsEvent(mapOf( sendTTSAudioData(audioData)
"type" to "audio",
"audioData" to audioData
))
}, },
// 完成回调
completionCallback = { completionCallback = {
sendTtsEvent(mapOf("type" to "completed")) sendTTSEvent("completed", null)
}, },
errorCallback = { errorCode, errorMessage -> // 错误回调
sendTtsEvent(mapOf( errorCallback = { code, message ->
"type" to "error", val params = HashMap<String, Any>()
"errorCode" to errorCode, params["code"] = code
"errorMessage" to errorMessage params["message"] = message
)) sendTTSEvent("error", params)
},
// 状态回调
statusCallback = { status ->
val params = HashMap<String, Any>()
params["status"] = status
sendTTSEvent("status", params)
} }
) )
// 初始化TTS
val success = bytedanceTTS?.initialize(ttsAppId, ttsAppToken, language) ?: false
if (success) {
result.success(true) result.success(true)
} else {
result.error(
"INIT_FAILED",
"TTS初始化失败",
null
)
}
} catch (e: Exception) { } catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null) Log.e(TAG, "TTS初始化异常: ${e.message}", e)
result.error(
"INIT_EXCEPTION",
"TTS初始化异常: ${e.message}",
null
)
}
} }
} }
"synthesize" -> {
val text = call.argument<String>("text") ?: ""
if (ttsEventSink == null) { /**
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始合成", 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 return
} }
scope.launch { scope.launch {
try { try {
sendTtsEvent(mapOf("type" to "started")) val success = bytedanceTTS?.speakOnce(text) ?: false
ttsInstance?.synthesize(text) result.success(success)
scope.launch(Dispatchers.Main) { } catch (e: Exception) {
result.success(true) 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) { } catch (e: Exception) {
scope.launch(Dispatchers.Main) { Log.e(TAG, "流式合成异常: ${e.message}", e)
result.error("SYNTHESIS_ERROR", e.message, null) result.error(
"STREAM_EXCEPTION",
"流式合成异常: ${e.message}",
null
)
} }
} }
} }
/**
* 刷新流式合成缓冲区
*/
private fun flushStream(result: Result) {
if (bytedanceTTS == null) {
result.error("NOT_INITIALIZED", "TTS未初始化", null)
return
} }
"stopSynthesis" -> {
scope.launch { scope.launch {
try { try {
ttsInstance?.stop() val success = bytedanceTTS?.flushStream() ?: false
scope.launch(Dispatchers.Main) { 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) result.success(true)
return
} }
scope.launch {
try {
bytedanceTTS?.stop()
result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
scope.launch(Dispatchers.Main) { Log.e(TAG, "停止TTS异常: ${e.message}", e)
result.error("STOP_ERROR", e.message, null) result.error(
} "STOP_EXCEPTION",
"停止TTS异常: ${e.message}",
null
)
} }
} }
} }
"dispose" -> {
/**
* 释放TTS资源
*/
private fun disposeTTS(result: Result) {
try { try {
ttsInstance?.release() bytedanceTTS?.release()
ttsInstance = null bytedanceTTS = null
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null) Log.e(TAG, "释放TTS资源异常: ${e.message}", e)
result.error(
"DISPOSE_EXCEPTION",
"释放TTS资源异常: ${e.message}",
null
)
} }
} }
else -> {
result.notImplemented() /**
* 发送TTS事件到Flutter端
*/
private fun sendTTSEvent(eventName: String, params: Map<String, Any>?) {
if (ttsEventSink == null) return
val event = HashMap<String, Any>()
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<String, Any>? = null) {
if (asrEventSink == null) return
val event = HashMap<String, Any>()
event["type"] = eventType
event["data"] = data
if (params != null) {
event["params"] = params
} }
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { mainHandler.post {
asrChannel.setMethodCallHandler(null) asrEventSink?.success(event)
ttsChannel.setMethodCallHandler(null) }
asrEventChannel.setStreamHandler(null)
ttsEventChannel.setStreamHandler(null)
scope.cancel()
asrInstance?.release()
ttsInstance?.release()
asrInstance = null
ttsInstance = null
} }
} }

221
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 package com.deep_voice.bytedance_speech
import android.content.Context import android.content.Context
import com.google.gson.Gson import android.util.Log
import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -10,9 +9,8 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
import okio.ByteString import okio.ByteString
import org.json.JSONObject
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -32,6 +30,8 @@ class BytedanceTTS(
private val statusCallback: (String) -> Unit private val statusCallback: (String) -> Unit
) { ) {
companion object { companion object {
private const val TAG = "BytedanceTTS"
// 协议常量 // 协议常量
private const val PROTOCOL_VERSION = 0x01 private const val PROTOCOL_VERSION = 0x01
private const val DEFAULT_HEADER_SIZE = 0x01 private const val DEFAULT_HEADER_SIZE = 0x01
@ -94,9 +94,9 @@ class BytedanceTTS(
// TTS配置参数 // TTS配置参数
private var appId: String = "" private var appId: String = ""
private var accessToken: String = "" private var accessToken: String = ""
private var speaker: String = "zh_female_qingxin" // 默认语音 private var speaker: String = "zh_female_shuangkuaisisi_moon_bigtts" // 默认语音,使用Java示例的音色
private var format: String = "pcm" private var format: String = "mp3" // 使用mp3格式
private var sampleRate: Int = 16000 private var sampleRate: Int = 24000 // 使用24kHz采样率
private var enableTimestamp: Boolean = false private var enableTimestamp: Boolean = false
private var language: String = "zh-CN" private var language: String = "zh-CN"
@ -106,48 +106,53 @@ class BytedanceTTS(
init { init {
// 配置OkHttp客户端 // 配置OkHttp客户端
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = Level.HEADERS
}
client = OkHttpClient.Builder() client = OkHttpClient.Builder()
.pingInterval(50, TimeUnit.SECONDS) .pingInterval(50, TimeUnit.SECONDS) // 匹配Java示例设置50秒
.addInterceptor(loggingInterceptor) .readTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒
.readTimeout(100, TimeUnit.SECONDS) .writeTimeout(100, TimeUnit.SECONDS) // 匹配Java示例设置100秒
.writeTimeout(100, TimeUnit.SECONDS)
.build() .build()
// 生成会话ID // 生成会话ID
sessionId = UUID.randomUUID().toString().replace("-", "") 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 { try {
updateStatus(STATUS_INITIALIZING) updateStatus(STATUS_INITIALIZING)
// 设置必要参数 // 设置必要参数
this@BytedanceTTS.appId = region // 火山引擎中region对应应用ID this@BytedanceTTS.appId = ttsAppId // 火山引擎中的应用ID
this@BytedanceTTS.accessToken = subscriptionKey // 订阅密钥作为访问令牌 this@BytedanceTTS.accessToken = ttsAppToken // 火山引擎访问令牌
this@BytedanceTTS.language = language 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() release()
// 建立新连接 // 建立新连接
connect() 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 true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "初始化失败: ${e.message}", e)
errorCallback("INIT_ERROR", e.message ?: "初始化失败") errorCallback("INIT_ERROR", e.message ?: "初始化失败")
updateStatus(STATUS_ERROR) updateStatus(STATUS_ERROR)
false false
@ -199,6 +204,7 @@ class BytedanceTTS(
sendTextRequest(text) sendTextRequest(text)
return@withContext true return@withContext true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "语音合成失败: ${e.message}", e)
errorCallback("SYNTH_ERROR", e.message ?: "语音合成失败") errorCallback("SYNTH_ERROR", e.message ?: "语音合成失败")
updateStatus(STATUS_ERROR) updateStatus(STATUS_ERROR)
false false
@ -211,10 +217,9 @@ class BytedanceTTS(
suspend fun speakStream(text: String): Boolean = withContext(Dispatchers.IO) { suspend fun speakStream(text: String): Boolean = withContext(Dispatchers.IO) {
try { try {
if (text.isEmpty()) { if (text.isEmpty()) {
errorCallback("PARAM_ERROR", "合成文本不能为空") return@withContext true // 空文本直接返回成功
return@withContext false
} }
Log.d(TAG, "开始流式合成,text: $text")
isStreamMode = true isStreamMode = true
// 确保会话已就绪 // 确保会话已就绪
@ -225,6 +230,7 @@ class BytedanceTTS(
updateStatus(STATUS_SPEAKING) updateStatus(STATUS_SPEAKING)
return@withContext true return@withContext true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "流式合成失败: ${e.message}", e)
errorCallback("STREAM_ERROR", e.message ?: "流式合成失败") errorCallback("STREAM_ERROR", e.message ?: "流式合成失败")
updateStatus(STATUS_ERROR) updateStatus(STATUS_ERROR)
false false
@ -248,6 +254,7 @@ class BytedanceTTS(
updateStatus(STATUS_STOPPED) updateStatus(STATUS_STOPPED)
return@withContext true return@withContext true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "结束流式合成失败: ${e.message}", e)
errorCallback("FLUSH_ERROR", e.message ?: "结束流式合成失败") errorCallback("FLUSH_ERROR", e.message ?: "结束流式合成失败")
false false
} }
@ -327,10 +334,13 @@ class BytedanceTTS(
.header("X-Api-Connect-Id", UUID.randomUUID().toString()) .header("X-Api-Connect-Id", UUID.randomUUID().toString())
.build() .build()
Log.d(TAG, "正在连接WebSocket: $url, appId: $appId")
Log.d(TAG, "当前语音角色配置: speaker=$speaker, format=$format, sampleRate=$sampleRate")
webSocket = client.newWebSocket(request, object : WebSocketListener() { webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
val logId = response.header("X-Tt-Logid") 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) startConnection(webSocket)
@ -339,6 +349,7 @@ class BytedanceTTS(
override fun onMessage(webSocket: WebSocket, bytes: ByteString) { override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
try { try {
val data = bytes.toByteArray() val data = bytes.toByteArray()
Log.d(TAG, "收到二进制消息,大小: ${data.size} 字节")
val response = parseResponse(data) val response = parseResponse(data)
// 处理事件 // 处理事件
@ -349,6 +360,7 @@ class BytedanceTTS(
} else { } else {
"连接或会话失败" "连接或会话失败"
} }
Log.e(TAG, "连接或会话失败: $errorMsg, 事件类型: ${response.event}")
errorCallback("TTS_ERROR", errorMsg) errorCallback("TTS_ERROR", errorMsg)
webSocket.close(1000, "Error") webSocket.close(1000, "Error")
updateStatus(STATUS_ERROR) updateStatus(STATUS_ERROR)
@ -356,25 +368,51 @@ class BytedanceTTS(
EVENT_CONNECTION_STARTED -> { EVENT_CONNECTION_STARTED -> {
isConnected = true isConnected = true
Log.d(TAG, "连接已建立,开始TTS会话")
// 开始TTS会话 // 开始TTS会话
startTtsSession(webSocket) startTtsSession(webSocket)
} }
EVENT_SESSION_STARTED -> { EVENT_SESSION_STARTED -> {
isSessionStarted = true isSessionStarted = true
Log.d(TAG, "TTS会话已开始,会话ID: $sessionId")
updateStatus(STATUS_READY) updateStatus(STATUS_READY)
} }
EVENT_TTS_RESPONSE -> { EVENT_TTS_RESPONSE -> {
// 音频数据 // 音频数据
if (response.messageType == AUDIO_ONLY_RESPONSE && response.payload.isNotEmpty()) { if (response.messageType == AUDIO_ONLY_RESPONSE && response.payload.isNotEmpty()) {
Log.d(TAG, "收到音频数据,大小: ${response.payload.size} 字节")
try {
// 使用同步方式调用回调,防止协程上下文切换导致的问题
if (response.payload.size > 0) {
Log.d(TAG, "准备调用音频回调,数据大小: ${response.payload.size}")
audioCallback(response.payload) 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 -> { EVENT_CONNECTION_FINISHED -> {
isConnected = false isConnected = false
isSessionStarted = false isSessionStarted = false
Log.d(TAG, "连接已结束")
updateStatus(STATUS_STOPPED) updateStatus(STATUS_STOPPED)
completionCallback() completionCallback()
webSocket.close(1000, "Connection finished") webSocket.close(1000, "Connection finished")
@ -382,6 +420,7 @@ class BytedanceTTS(
EVENT_SESSION_FINISHED -> { EVENT_SESSION_FINISHED -> {
isSessionStarted = false isSessionStarted = false
Log.d(TAG, "会话已结束,流模式: $isStreamMode")
if (!isStreamMode) { if (!isStreamMode) {
finishConnection(webSocket) finishConnection(webSocket)
} else { } else {
@ -389,8 +428,13 @@ class BytedanceTTS(
updateStatus(STATUS_READY) updateStatus(STATUS_READY)
} }
} }
else -> {
Log.d(TAG, "收到未处理的事件类型: ${response.event}")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "解析响应失败: ${e.message}", e)
errorCallback("PARSE_ERROR", e.message ?: "解析响应失败") errorCallback("PARSE_ERROR", e.message ?: "解析响应失败")
updateStatus(STATUS_ERROR) updateStatus(STATUS_ERROR)
} }
@ -399,6 +443,7 @@ class BytedanceTTS(
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
isConnected = false isConnected = false
isSessionStarted = false isSessionStarted = false
Log.e(TAG, "连接失败: ${t.message}", t)
errorCallback("CONNECTION_ERROR", t.message ?: "连接失败") errorCallback("CONNECTION_ERROR", t.message ?: "连接失败")
updateStatus(STATUS_ERROR) updateStatus(STATUS_ERROR)
} }
@ -407,7 +452,7 @@ class BytedanceTTS(
isConnected = false isConnected = false
isSessionStarted = false isSessionStarted = false
updateStatus(STATUS_STOPPED) updateStatus(STATUS_STOPPED)
println("TTS WebSocket连接关闭: $code, $reason") Log.d(TAG, "TTS WebSocket连接关闭: $code, $reason")
} }
}) })
} }
@ -477,30 +522,26 @@ class BytedanceTTS(
sessionId = sessionId sessionId = sessionId
) )
val user = JsonObject().apply { val jsonObject = JSONObject()
addProperty("uid", "123456") 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 { val reqParams = JSONObject()
addProperty("speaker", speaker) reqParams.put("speaker", speaker)
val audioParams = JsonObject().apply { val audioParams = JSONObject()
addProperty("format", format) audioParams.put("format", "mp3") // 尝试改用mp3格式
addProperty("sample_rate", sampleRate) audioParams.put("sample_rate", 24000) // 提高采样率
addProperty("enable_timestamp", enableTimestamp) audioParams.put("enable_timestamp", enableTimestamp)
}
add("audio_params", audioParams)
}
val payloadObj = JsonObject().apply { reqParams.put("audio_params", audioParams)
add("user", user) jsonObject.put("req_params", reqParams)
addProperty("event", EVENT_START_SESSION)
addProperty("namespace", "BidirectionalTTS")
add("req_params", reqParams)
}
val payload = payloadObj.toString().toByteArray() val payload = jsonObject.toString().toByteArray()
Log.d(TAG, "开始TTS会话请求: ${jsonObject.toString()}")
sendEvent(webSocket, header, optional, payload) sendEvent(webSocket, header, optional, payload)
} }
@ -525,36 +566,31 @@ class BytedanceTTS(
sessionId = sessionId sessionId = sessionId
) )
val user = JsonObject().apply { val jsonObject = JSONObject()
addProperty("uid", "123456") 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 { val reqParams = JSONObject()
addProperty("text", text) reqParams.put("text", text)
addProperty("speaker", speaker) reqParams.put("speaker", speaker)
val audioParams = JsonObject().apply { val audioParams = JSONObject()
addProperty("format", format) audioParams.put("format", "mp3") // 尝试改用mp3格式
addProperty("sample_rate", sampleRate) audioParams.put("sample_rate", 24000) // 提高采样率
}
add("audio_params", audioParams) reqParams.put("audio_params", audioParams)
} jsonObject.put("req_params", reqParams)
val payloadObj = JsonObject().apply {
add("user", user)
addProperty("event", EVENT_TASK_REQUEST)
addProperty("namespace", "BidirectionalTTS")
add("req_params", reqParams)
}
val payload = payloadObj.toString().toByteArray() val payload = jsonObject.toString().toByteArray()
Log.d(TAG, "发送文本合成请求: ${jsonObject.toString()}")
sendEvent(ws, header, optional, payload) sendEvent(ws, header, optional, payload)
// 发送完文本后结束会话 // 发送完文本后结束会话
updateStatus(STATUS_SPEAKING) updateStatus(STATUS_SPEAKING)
finishSession()
} }
} }
@ -578,30 +614,26 @@ class BytedanceTTS(
sessionId = sessionId sessionId = sessionId
) )
val user = JsonObject().apply { val jsonObject = JSONObject()
addProperty("uid", "123456") val user = JSONObject()
} user.put("uid", "123456")
jsonObject.put("user", user)
val reqParams = JsonObject().apply { jsonObject.put("event", EVENT_TASK_REQUEST)
addProperty("text", text) jsonObject.put("namespace", "BidirectionalTTS")
addProperty("speaker", speaker)
val audioParams = JsonObject().apply { val reqParams = JSONObject()
addProperty("format", format) reqParams.put("text", text)
addProperty("sample_rate", sampleRate) reqParams.put("speaker", speaker)
}
add("audio_params", audioParams) val audioParams = JSONObject()
} audioParams.put("format", "mp3") // 尝试改用mp3格式
audioParams.put("sample_rate", 24000) // 提高采样率
val payloadObj = JsonObject().apply { reqParams.put("audio_params", audioParams)
add("user", user) jsonObject.put("req_params", reqParams)
addProperty("event", EVENT_TASK_REQUEST)
addProperty("namespace", "BidirectionalTTS")
add("req_params", reqParams)
}
val payload = payloadObj.toString().toByteArray() val payload = jsonObject.toString().toByteArray()
Log.d(TAG, "发送流式文本合成请求: ${jsonObject.toString()}")
sendEvent(ws, header, optional, payload) sendEvent(ws, header, optional, payload)
// 流式模式下不自动结束会话 // 流式模式下不自动结束会话
@ -638,6 +670,7 @@ class BytedanceTTS(
* 发送事件 * 发送事件
*/ */
private fun sendEvent(webSocket: WebSocket, header: Header, optional: Optional, payload: ByteArray) { private fun sendEvent(webSocket: WebSocket, header: Header, optional: Optional, payload: ByteArray) {
try {
val headerBytes = header.getBytes() val headerBytes = header.getBytes()
val optionalBytes = optional.getBytes() val optionalBytes = optional.getBytes()
val payloadSizeBytes = intToBytes(payload.size) val payloadSizeBytes = intToBytes(payload.size)
@ -658,7 +691,11 @@ class BytedanceTTS(
System.arraycopy(payload, 0, requestBytes, offset, payload.size) System.arraycopy(payload, 0, requestBytes, offset, payload.size)
webSocket.send(ByteString.of(requestBytes)) 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)
}
} }
/** /**

5
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
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import com.yunqiinnovation.deepvoice_utils.FileLogger
/** ClassicBluetoothPlugin */ /** ClassicBluetoothPlugin */
class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler { class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
@ -119,7 +118,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
sendEvent(eventMap) sendEvent(eventMap)
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "蓝牙状态获取失败", e) Log.e(TAG, "蓝牙状态获取失败", e)
} }
} }
@ -188,7 +187,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
eventSink?.success(event) eventSink?.success(event)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "事件发送失败", e) Log.e(TAG, "事件发送失败", e)
} }
} }
} }

1
local_plugins/deepvoice_utils/android/settings.gradle.kts

@ -1 +0,0 @@
rootProject.name = "deepvoice_utils"

7
local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.deepvoice_utils">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

33
local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt

@ -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)
}
}

237
local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt

@ -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
}
}
}

4
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h

@ -1,4 +0,0 @@
#import <Flutter/Flutter.h>
@interface DeepvoiceUtilsPlugin : NSObject<FlutterPlugin>
@end

12
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m

@ -1,12 +0,0 @@
#import "DeepvoiceUtilsPlugin.h"
#if __has_include(<deepvoice_utils/deepvoice_utils-Swift.h>)
#import <deepvoice_utils/deepvoice_utils-Swift.h>
#else
#import "deepvoice_utils-Swift.h"
#endif
@implementation DeepvoiceUtilsPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftDeepvoiceUtilsPlugin registerWithRegistrar:registrar];
}
@end

30
local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift

@ -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()
}
}

286
local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift

@ -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
}
}

11
local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift

@ -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)
}
}

54
local_plugins/deepvoice_utils/ios/README.md

@ -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)
```

19
local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec

@ -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

3
local_plugins/deepvoice_utils/lib/deepvoice_utils.dart

@ -1,3 +0,0 @@
library deepvoice_utils;
export 'src/file_logger.dart';

31
local_plugins/deepvoice_utils/lib/src/file_logger.dart

@ -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());
}
}
}

27
local_plugins/deepvoice_utils/pubspec.yaml

@ -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

1
local_plugins/music_service/android/build.gradle

@ -66,6 +66,5 @@ android {
} }
dependencies { dependencies {
implementation project(':deepvoice_utils')
implementation("androidx.media:media:1.6.0") implementation("androidx.media:media:1.6.0")
} }

25
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.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -46,16 +46,15 @@ class MusicService : MediaBrowserServiceCompat() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FileLogger.init(this)
isRunning.set(true) isRunning.set(true)
setupMediaSession() setupMediaSession()
startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound","")) startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound",""))
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager 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 { 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 { intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
// 分发给 MediaSession 处理媒体按钮 // 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent) MediaButtonReceiver.handleIntent(mediaSession, intent)
@ -108,10 +107,10 @@ class MusicService : MediaBrowserServiceCompat() {
) )
// 回调中处理具体操作 // 回调中处理具体操作
setCallback(object : MediaSessionCompat.Callback() { setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() { FileLogger.i(TAG, "onPlay 通知!"); resume()} override fun onPlay() { Log.i(TAG, "onPlay 通知!"); resume()}
override fun onPause() { FileLogger.i(TAG, "onPause 通知!"); pause()} override fun onPause() { Log.i(TAG, "onPause 通知!"); pause()}
override fun onSkipToNext(){ FileLogger.i(TAG, "onSkipToNext 通知!"); playNext()} override fun onSkipToNext(){ Log.i(TAG, "onSkipToNext 通知!"); playNext()}
override fun onSkipToPrevious() { FileLogger.i(TAG, "onSkipToNext 通知!"); playPrevious()} override fun onSkipToPrevious() { Log.i(TAG, "onSkipToNext 通知!"); playPrevious()}
}) })
// 初始播放状态 // 初始播放状态
setPlaybackState( setPlaybackState(
@ -222,7 +221,7 @@ class MusicService : MediaBrowserServiceCompat() {
private fun play(song: Map<String,String>) { private fun play(song: Map<String,String>) {
val url = song["url"] ?: return val url = song["url"] ?: return
FileLogger.i(TAG, "开始播放: $url") Log.i(TAG, "开始播放: $url")
val title = song["title"] ?: "未知标题" val title = song["title"] ?: "未知标题"
val artist = song["artist"] ?: "未知歌手" val artist = song["artist"] ?: "未知歌手"
val coverUrl = song["coverUrl"] val coverUrl = song["coverUrl"]
@ -235,11 +234,11 @@ class MusicService : MediaBrowserServiceCompat() {
setOnPreparedListener { setOnPreparedListener {
start() start()
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
FileLogger.i(TAG, "开始播放: $url") Log.i(TAG, "开始播放: $url")
} }
setOnCompletionListener { playNextOrStop() } setOnCompletionListener { playNextOrStop() }
setOnErrorListener { _, what, extra -> setOnErrorListener { _, what, extra ->
FileLogger.e(TAG, "播放错误 what=$what extra=$extra") Log.e(TAG, "播放错误 what=$what extra=$extra")
true true
} }
prepareAsync() prepareAsync()
@ -262,7 +261,7 @@ class MusicService : MediaBrowserServiceCompat() {
val coverUrl = song["coverUrl"] val coverUrl = song["coverUrl"]
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PLAYING); setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
FileLogger.i(TAG, "已暂停") Log.i(TAG, "已暂停")
} }
} }
@ -282,7 +281,7 @@ class MusicService : MediaBrowserServiceCompat() {
val coverUrl = song["coverUrl"] val coverUrl = song["coverUrl"]
updateNotification(title,artist,coverUrl) updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED); setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
// FileLogger.i(TAG, "继续播放") // Log.i(TAG, "继续播放")
} }
} }

4
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.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.music_service.MusicServiceStarter
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 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) { when (call.method) {
"play" -> { "play" -> {
val song = call.argument<Map<String, String>>("song") val song = call.argument<Map<String, String>>("song")

22
local_plugins/deepvoice_utils/android/build.gradle.kts → local_plugins/speech/android/build.gradle.kts

@ -4,11 +4,12 @@ plugins {
// Kotlin Android 插件 // Kotlin Android 插件
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1" id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("plugin.serialization") version "1.9.24"
} }
android { android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致) // 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.deepvoice_utils" namespace = "com.yunqiinnovation.open_ai_service"
// 目标 SDK 版本 // 目标 SDK 版本
compileSdk = 35 compileSdk = 35
@ -16,7 +17,7 @@ android {
defaultConfig { defaultConfig {
// 最低 SDK 版本 // 最低 SDK 版本
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 33
} }
// Java 语言级别兼容配置 // 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")
}

1
local_plugins/speech/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "open_ai_service"

10
local_plugins/speech/android/src/main/AndroidManifest.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.deep_voice.speech">
<!-- 语音识别和合成所需权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

52
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<String, Any> = emptyMap()
)
/**
* ASR事件监听接口
*/
interface AsrEventListener {
/**
* 当发生ASR事件时调用
*
* @param event ASR事件
*/
fun onEvent(event: AsrEvent)
}

90
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<String>? = 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<String, Any>): Boolean
}

92
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)
}

63
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<String, Any> = emptyMap()
)
/**
* TTS事件监听接口
*/
interface TtsEventListener {
/**
* 当发生TTS事件时调用
*
* @param event TTS事件
*/
fun onEvent(event: TtsEvent)
}
/**
* 音频数据监听接口
*/
interface AudioDataListener {
/**
* 当接收到音频数据时调用
*
* @param data 音频数据字节数组
*/
fun onAudioData(data: ByteArray)
}

42
local_plugins/volcano_speech/android/build.gradle.kts

@ -1,21 +1,11 @@
import com.android.build.gradle.LibraryExtension
buildscript { buildscript {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:7.3.0") classpath("com.android.tools.build:gradle:7.3.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.21")
}
}
allprojects {
repositories {
google()
mavenCentral()
} }
} }
@ -24,10 +14,9 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
} }
// 配置android扩展 android {
configure<LibraryExtension> {
namespace = "com.yunqiinnovation.volcano_speech" namespace = "com.yunqiinnovation.volcano_speech"
compileSdkVersion(35) compileSdk = 35
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
@ -38,6 +27,10 @@ configure<LibraryExtension> {
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions {
jvmTarget = "11"
}
sourceSets { sourceSets {
getByName("main") { getByName("main") {
manifest.srcFile("src/main/AndroidManifest.xml") manifest.srcFile("src/main/AndroidManifest.xml")
@ -49,17 +42,20 @@ configure<LibraryExtension> {
lintOptions { lintOptions {
isCheckReleaseBuilds = false isCheckReleaseBuilds = false
} }
}
// 显式设置Kotlin JVM目标版本
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
}
} }
dependencies { dependencies {
// Flutter插件依赖
compileOnly("io.flutter:flutter_embedding_debug:1.0.0-a794cf2681")
// 添加火山引擎语音合成SDK // 添加火山引擎语音合成SDK
// implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7") // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")
// implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7")
}
repositories {
google()
mavenCentral()
} }

134
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.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator import com.bytedance.speech.speechengine.SpeechEngineGenerator
import com.yunqiinnovation.volcano_speech.utils.FileLogger import android.util.Log
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.json.JSONArray 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 { fun initialize(appId: String, token: String, resourceId: String = "volc.bigasr.sauc.duration"): Boolean {
if (isInitialized) { if (isInitialized) {
FileLogger.i(TAG, "引擎已经初始化") Log.i(TAG, "引擎已经初始化")
return true return true
} }
@ -94,7 +94,7 @@ class VolcanoAsrHelper(private val context: Context) {
try { try {
SpeechEngineGenerator.PrepareEnvironment(context, null) SpeechEngineGenerator.PrepareEnvironment(context, null)
} catch (e: Exception) { } 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 engineHandler = engine?.createEngine() ?: -1
if (engineHandler == -1L) { if (engineHandler == -1L) {
FileLogger.e(TAG, "创建引擎失败") Log.e(TAG, "创建引擎失败")
state = AsrState.ERROR state = AsrState.ERROR
return false return false
} }
@ -176,7 +176,7 @@ class VolcanoAsrHelper(private val context: Context) {
isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR
if (isInitialized) { if (isInitialized) {
FileLogger.i(TAG, "引擎初始化成功") Log.i(TAG, "引擎初始化成功")
state = AsrState.READY state = AsrState.READY
// 设置回调监听 // 设置回调监听
@ -189,12 +189,12 @@ class VolcanoAsrHelper(private val context: Context) {
return true return true
} else { } else {
FileLogger.e(TAG, "引擎初始化失败: $result") Log.e(TAG, "引擎初始化失败: $result")
state = AsrState.ERROR state = AsrState.ERROR
return false return false
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "初始化异常: ${e.message}", e) Log.e(TAG, "初始化异常: ${e.message}", e)
state = AsrState.ERROR state = AsrState.ERROR
return false return false
} }
@ -205,12 +205,12 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun setAudioInputType(type: String): Boolean { fun setAudioInputType(type: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
if (isEngineStarted) { if (isEngineStarted) {
FileLogger.e(TAG, "引擎已启动,无法更改配置") Log.e(TAG, "引擎已启动,无法更改配置")
return false return false
} }
@ -219,7 +219,7 @@ class VolcanoAsrHelper(private val context: Context) {
"mic" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_RECORDER "mic" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_RECORDER
"stream" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_STREAM "stream" -> recorderType = SpeechEngineDefines.RECORDER_TYPE_STREAM
else -> { else -> {
FileLogger.e(TAG, "不支持的音频输入类型: $type") Log.e(TAG, "不支持的音频输入类型: $type")
return false return false
} }
} }
@ -227,7 +227,7 @@ class VolcanoAsrHelper(private val context: Context) {
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, recorderType) engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING, recorderType)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置音频输入类型失败: ${e.message}", e) Log.e(TAG, "设置音频输入类型失败: ${e.message}", e)
return false return false
} }
} }
@ -237,12 +237,12 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun setAudioParams(sampleRate: Int, channels: Int): Boolean { fun setAudioParams(sampleRate: Int, channels: Int): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
if (isEngineStarted) { if (isEngineStarted) {
FileLogger.e(TAG, "引擎已启动,无法更改配置") Log.e(TAG, "引擎已启动,无法更改配置")
return false return false
} }
@ -267,7 +267,7 @@ class VolcanoAsrHelper(private val context: Context) {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置音频参数失败: ${e.message}", e) Log.e(TAG, "设置音频参数失败: ${e.message}", e)
return false return false
} }
} }
@ -277,7 +277,7 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun setLanguage(language: String): Boolean { fun setLanguage(language: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
@ -286,7 +286,7 @@ class VolcanoAsrHelper(private val context: Context) {
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_LANGUAGE_STRING, language) engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_LANGUAGE_STRING, language)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置语言失败: ${e.message}", e) Log.e(TAG, "设置语言失败: ${e.message}", e)
return false return false
} }
} }
@ -296,7 +296,7 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun setHotWords(hotWordsId: String): Boolean { fun setHotWords(hotWordsId: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
@ -308,7 +308,7 @@ class VolcanoAsrHelper(private val context: Context) {
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置热词失败: ${e.message}", e) Log.e(TAG, "设置热词失败: ${e.message}", e)
return false return false
} }
} }
@ -317,14 +317,14 @@ class VolcanoAsrHelper(private val context: Context) {
* 一次性识别(异步) * 一次性识别(异步)
*/ */
fun recognizeOnce(callback: RecognizeCallback): Boolean { fun recognizeOnce(callback: RecognizeCallback): Boolean {
FileLogger.i(TAG, "开始一次性识别") Log.i(TAG, "开始一次性识别")
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
if (isRecognizing) { if (isRecognizing) {
FileLogger.e(TAG, "当前已在识别中") Log.e(TAG, "当前已在识别中")
return false return false
} }
@ -345,7 +345,7 @@ class VolcanoAsrHelper(private val context: Context) {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "启动识别失败: $ret") Log.e(TAG, "启动识别失败: $ret")
return false return false
} }
@ -354,7 +354,7 @@ class VolcanoAsrHelper(private val context: Context) {
state = AsrState.RECOGNIZING state = AsrState.RECOGNIZING
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "识别异常: ${e.message}", e) Log.e(TAG, "识别异常: ${e.message}", e)
return false return false
} }
} }
@ -364,12 +364,12 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun stopRecognize(): Boolean { fun stopRecognize(): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
if (!isEngineStarted) { if (!isEngineStarted) {
FileLogger.e(TAG, "引擎未启动") Log.e(TAG, "引擎未启动")
return false return false
} }
@ -379,7 +379,7 @@ class VolcanoAsrHelper(private val context: Context) {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "") val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "停止识别失败: $ret") Log.e(TAG, "停止识别失败: $ret")
return false return false
} }
} else { } else {
@ -387,7 +387,7 @@ class VolcanoAsrHelper(private val context: Context) {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "停止识别失败: $ret") Log.e(TAG, "停止识别失败: $ret")
return false return false
} }
@ -398,7 +398,7 @@ class VolcanoAsrHelper(private val context: Context) {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止识别异常: ${e.message}", e) Log.e(TAG, "停止识别异常: ${e.message}", e)
return false return false
} }
} }
@ -407,14 +407,14 @@ class VolcanoAsrHelper(private val context: Context) {
* 开始连续识别 * 开始连续识别
*/ */
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
FileLogger.i(TAG, "开始连续识别") Log.i(TAG, "开始连续识别")
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
if (isRecognizing) { if (isRecognizing) {
FileLogger.e(TAG, "当前已在识别中") Log.e(TAG, "当前已在识别中")
return false return false
} }
@ -436,17 +436,17 @@ class VolcanoAsrHelper(private val context: Context) {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "启动连续识别失败: $ret") Log.e(TAG, "启动连续识别失败: $ret")
return false return false
} }
isRecognizing = true isRecognizing = true
isEngineStarted = true isEngineStarted = true
state = AsrState.RECOGNIZING state = AsrState.RECOGNIZING
FileLogger.i(TAG, "开始连续识别") Log.i(TAG, "开始连续识别")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "连续识别异常: ${e.message}", e) Log.e(TAG, "连续识别异常: ${e.message}", e)
return false return false
} }
} }
@ -456,7 +456,7 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun stopContinuousRecognition(): Boolean { fun stopContinuousRecognition(): Boolean {
if (!isInitialized || !isRecognizing) { if (!isInitialized || !isRecognizing) {
FileLogger.e(TAG, "引擎未初始化或未在识别中") Log.e(TAG, "引擎未初始化或未在识别中")
return false return false
} }
@ -465,7 +465,7 @@ class VolcanoAsrHelper(private val context: Context) {
val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "停止连续识别失败: $ret") Log.e(TAG, "停止连续识别失败: $ret")
return false return false
} }
@ -474,7 +474,7 @@ class VolcanoAsrHelper(private val context: Context) {
state = AsrState.READY state = AsrState.READY
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止连续识别异常: ${e.message}", e) Log.e(TAG, "停止连续识别异常: ${e.message}", e)
return false return false
} }
} }
@ -485,17 +485,17 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun feedAudio(data: ByteArray): Boolean { fun feedAudio(data: ByteArray): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
if (!isEngineStarted) { if (!isEngineStarted) {
FileLogger.e(TAG, "引擎未启动") Log.e(TAG, "引擎未启动")
return false return false
} }
if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) {
FileLogger.e(TAG, "当前录音类型不是stream模式,无法使用此方法") Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法")
return false return false
} }
@ -504,13 +504,13 @@ class VolcanoAsrHelper(private val context: Context) {
val ret = engine?.feedAudio(engineHandler, data, data.size) val ret = engine?.feedAudio(engineHandler, data, data.size)
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "发送音频数据失败: $ret") Log.e(TAG, "发送音频数据失败: $ret")
return false return false
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送音频数据异常: ${e.message}", e) Log.e(TAG, "发送音频数据异常: ${e.message}", e)
return false return false
} }
} }
@ -521,17 +521,17 @@ class VolcanoAsrHelper(private val context: Context) {
*/ */
fun feedAudioEnd(): Boolean { fun feedAudioEnd(): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false return false
} }
if (!isEngineStarted) { if (!isEngineStarted) {
FileLogger.e(TAG, "引擎未启动") Log.e(TAG, "引擎未启动")
return false return false
} }
if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) { if (recorderType != SpeechEngineDefines.RECORDER_TYPE_STREAM) {
FileLogger.e(TAG, "当前录音类型不是stream模式,无法使用此方法") Log.e(TAG, "当前录音类型不是stream模式,无法使用此方法")
return false return false
} }
@ -540,13 +540,13 @@ class VolcanoAsrHelper(private val context: Context) {
val ret = engine?.feedAudio(engineHandler, ByteArray(0), 0) val ret = engine?.feedAudio(engineHandler, ByteArray(0), 0)
if (ret != SpeechEngineDefines.ERR_NO_ERROR) { if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
FileLogger.e(TAG, "发送音频结束标记失败: $ret") Log.e(TAG, "发送音频结束标记失败: $ret")
return false return false
} }
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送音频结束标记异常: ${e.message}", e) Log.e(TAG, "发送音频结束标记异常: ${e.message}", e)
return false return false
} }
} }
@ -604,9 +604,9 @@ class VolcanoAsrHelper(private val context: Context) {
isInitialized = false isInitialized = false
state = AsrState.IDLE state = AsrState.IDLE
FileLogger.i(TAG, "引擎已释放") Log.i(TAG, "引擎已释放")
} catch (e: Exception) { } 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) { when (type) {
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> { SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> {
// 引擎启动成功 // 引擎启动成功
FileLogger.i(TAG, "引擎启动成功") Log.i(TAG, "引擎启动成功")
mainHandler.post { mainHandler.post {
continuousCallback?.onSessionStarted() continuousCallback?.onSessionStarted()
} }
@ -625,7 +625,7 @@ class VolcanoAsrHelper(private val context: Context) {
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> {
// 引擎停止 // 引擎停止
FileLogger.i(TAG, "引擎停止") Log.i(TAG, "引擎停止")
isRecognizing = false isRecognizing = false
isEngineStarted = false isEngineStarted = false
state = AsrState.READY state = AsrState.READY
@ -637,15 +637,15 @@ class VolcanoAsrHelper(private val context: Context) {
SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> { SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> {
// 中间识别结果 // 中间识别结果
FileLogger.i(TAG, "中间识别结果: $data") Log.i(TAG, "中间识别结果: $data")
try { try {
val json = JSONObject(data) val json = JSONObject(data)
if (!json.has("result")) { if (!json.has("result")) {
FileLogger.w(TAG, "中间结果没有 result 字段: $data") Log.w(TAG, "中间结果没有 result 字段: $data")
return return
} }
// if (json.has("definite")) { // if (json.has("definite")) {
// FileLogger.i(TAG, "utterances: ${json.getString("utterances")}") // Log.i(TAG, "utterances: ${json.getString("utterances")}")
// } // }
// 从多种可能的格式中获取文本 // 从多种可能的格式中获取文本
val text = extractTextFromResult(json) val text = extractTextFromResult(json)
@ -655,23 +655,23 @@ class VolcanoAsrHelper(private val context: Context) {
if (text.isNotEmpty()) { if (text.isNotEmpty()) {
mainHandler.post { mainHandler.post {
FileLogger.i(TAG, "提取到中间文本: $text") Log.i(TAG, "提取到中间文本: $text")
// 中间结果 // 中间结果
continuousCallback?.onRecognizing(text, detectedLanguage) continuousCallback?.onRecognizing(text, detectedLanguage)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "解析中间识别结果异常: ${e}") Log.e(TAG, "解析中间识别结果异常: ${e}")
} }
} }
SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> { SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> {
FileLogger.i(TAG, "最终识别结果: $data") Log.i(TAG, "最终识别结果: $data")
// 最终识别结果 // 最终识别结果
try { try {
val json = JSONObject(data) val json = JSONObject(data)
if (!json.has("result")) { if (!json.has("result")) {
FileLogger.w(TAG, "最终结果没有 result 字段: $data") Log.w(TAG, "最终结果没有 result 字段: $data")
return return
} }
@ -685,22 +685,22 @@ class VolcanoAsrHelper(private val context: Context) {
if (text.isNotEmpty()) { if (text.isNotEmpty()) {
mainHandler.post { mainHandler.post {
if (isContinuousMode) { if (isContinuousMode) {
FileLogger.i(TAG, "发送最终识别结果: $text (连续模式)") Log.i(TAG, "发送最终识别结果: $text (连续模式)")
continuousCallback?.onResult(text, detectedLanguage) continuousCallback?.onResult(text, detectedLanguage)
// 在连续模式下,不需要手动触发下一轮识别,引擎会自动继续监听 // 在连续模式下,不需要手动触发下一轮识别,引擎会自动继续监听
// 但如果需要明确标记新一轮开始,可以在此处添加代码 // 但如果需要明确标记新一轮开始,可以在此处添加代码
} else { } else {
FileLogger.i(TAG, "发送最终识别结果: $text (单次模式)") Log.i(TAG, "发送最终识别结果: $text (单次模式)")
recognizeCallback?.onResult(text, detectedLanguage) recognizeCallback?.onResult(text, detectedLanguage)
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
val errMsg = "解析最终识别结果异常: ${e.message}" val errMsg = "解析最终识别结果异常: ${e.message}"
FileLogger.e(TAG, errMsg) Log.e(TAG, errMsg)
FileLogger.e(TAG, "原始数据: $data") Log.e(TAG, "原始数据: $data")
FileLogger.e(TAG, "详细异常信息", e) Log.e(TAG, "详细异常信息", e)
} }
} }
@ -715,7 +715,7 @@ class VolcanoAsrHelper(private val context: Context) {
val errMsg = json.optString("err_msg", "未知错误") val errMsg = json.optString("err_msg", "未知错误")
val errCode = json.optInt("err_code", -1) val errCode = json.optInt("err_code", -1)
FileLogger.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg")
state = AsrState.ERROR state = AsrState.ERROR
mainHandler.post { mainHandler.post {
@ -728,7 +728,7 @@ class VolcanoAsrHelper(private val context: Context) {
isEngineStarted = false isEngineStarted = false
} }
} catch (e: JSONException) { } 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 return text
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "提取文本异常: ${e.message}", e) Log.e(TAG, "提取文本异常: ${e.message}", e)
FileLogger.e(TAG, "详细异常信息", e) Log.e(TAG, "详细异常信息", e)
return "" return ""
} }
} }

20
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.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.NonNull import androidx.annotation.NonNull
import com.yunqiinnovation.volcano_speech.utils.FileLogger
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import android.util.Log
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
/** VolcanoSpeechPlugin */ /** VolcanoSpeechPlugin */
class VolcanoSpeechPlugin: FlutterPlugin { class VolcanoSpeechPlugin: FlutterPlugin {
@ -126,7 +128,7 @@ class VolcanoSpeechPlugin: FlutterPlugin {
// 发送ASR事件 // 发送ASR事件
private fun sendAsrEvent(event: Map<String, Any>) { private fun sendAsrEvent(event: Map<String, Any>) {
if (asrEventSink == null) { if (asrEventSink == null) {
FileLogger.w(TAG, "无法发送ASR事件:事件通道未准备好") Log.w(TAG, "无法发送ASR事件:事件通道未准备好")
return return
} }
@ -134,25 +136,25 @@ class VolcanoSpeechPlugin: FlutterPlugin {
try { try {
asrEventSink?.success(event) asrEventSink?.success(event)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送ASR事件失败: ${e.message}") Log.e(TAG, "发送ASR事件失败: ${e.message}")
} }
} }
} }
// 发送TTS事件 // 发送TTS事件
private fun sendTtsEvent(event: Map<String, Any>) { private fun sendTtsEvent(event: Map<String, Any>) {
FileLogger.d(TAG, "发送TTS事件: $event") Log.d(TAG, "发送TTS事件: $event")
if (ttsEventSink == null) { if (ttsEventSink == null) {
FileLogger.w(TAG, "无法发送TTS事件:事件通道未准备好") Log.w(TAG, "无法发送TTS事件:事件通道未准备好")
return return
} }
mainHandler.post { mainHandler.post {
try { try {
ttsEventSink?.success(event) ttsEventSink?.success(event)
FileLogger.d(TAG, "TTS事件发送成功") Log.d(TAG, "TTS事件发送成功")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送TTS事件失败: ${e.message}") Log.e(TAG, "发送TTS事件失败: ${e.message}")
} }
} }
} }
@ -167,7 +169,7 @@ class VolcanoSpeechPlugin: FlutterPlugin {
try { try {
ttsAudioDataSink?.success(data) ttsAudioDataSink?.success(data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送TTS音频数据失败: ${e.message}") Log.e(TAG, "发送TTS音频数据失败: ${e.message}")
} }
} }
} }

140
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.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator import com.bytedance.speech.speechengine.SpeechEngineGenerator
import com.yunqiinnovation.volcano_speech.utils.FileLogger import android.util.Log
import org.json.JSONObject import org.json.JSONObject
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -28,7 +28,7 @@ class VolcanoTtsHelper(private val context: Context) {
private var currentReqId = "" private var currentReqId = ""
// 配置参数 // 配置参数
private var voice = "zh_male_M392_conversation_wvae_bigtts" private var voice = "zh_female_linjianvhai_moon_bigtts"
// 文本队列管理 // 文本队列管理
private val pendingTexts = ConcurrentLinkedQueue<String>() private val pendingTexts = ConcurrentLinkedQueue<String>()
@ -51,6 +51,10 @@ class VolcanoTtsHelper(private val context: Context) {
// 事件监听 // 事件监听
private val listeners = mutableListOf<TtsEventListener>() private val listeners = mutableListOf<TtsEventListener>()
// 添加新的成员变量,用于流式文本处理
private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L
/** /**
* TTS事件类型 * TTS事件类型
*/ */
@ -172,7 +176,7 @@ class VolcanoTtsHelper(private val context: Context) {
try { try {
SpeechEngineGenerator.PrepareEnvironment(context, null) SpeechEngineGenerator.PrepareEnvironment(context, null)
} catch (e: Exception) { } 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) { private fun handleError(code: Int, message: String, e: Exception? = null) {
if (e != null) { if (e != null) {
FileLogger.e(TAG, message, e) Log.e(TAG, message, e)
} else { } else {
FileLogger.e(TAG, message) Log.e(TAG, message)
} }
dispatchEvent(TtsEvent(TtsEventType.ERROR, code, message)) dispatchEvent(TtsEvent(TtsEventType.ERROR, code, message))
} }
@ -250,7 +254,7 @@ class VolcanoTtsHelper(private val context: Context) {
*/ */
fun setVoice(voiceName: String): Boolean { fun setVoice(voiceName: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化") Log.e(TAG, "引擎未初始化")
return false 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 return false
} }
// 处理文本,移除特殊字符
val processedText = processText(text)
// 停止当前播放 // 停止当前播放
stop() stop()
@ -289,10 +315,10 @@ class VolcanoTtsHelper(private val context: Context) {
configAudioCallback() configAudioCallback()
// 预备要合成的文本 // 预备要合成的文本
currentText = text currentText = processedText
// 单次播放模式:直接启动引擎并设置文本,自动开始合成 // 单次播放模式:直接启动引擎并设置文本,自动开始合成
return startEngineWithText(text, false) return startEngineWithText(processedText, false)
} }
/** /**
@ -303,6 +329,10 @@ class VolcanoTtsHelper(private val context: Context) {
return false return false
} }
try {
// 处理文本,移除特殊字符
val processedText = processText(text)
// 设置连续播放模式 // 设置连续播放模式
if (!isContinuousMode) { if (!isContinuousMode) {
isContinuousMode = true isContinuousMode = true
@ -312,8 +342,41 @@ class VolcanoTtsHelper(private val context: Context) {
configAudioCallback() configAudioCallback()
} }
// 加入队列 // 添加新文本到缓冲区
pendingTexts.add(text) 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) { if (!isEngineStarted) {
@ -322,8 +385,27 @@ class VolcanoTtsHelper(private val context: Context) {
// 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成 // 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成
return triggerNextSynthesis() 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 return true
} catch (e: Exception) {
Log.e(TAG, "流式语音合成失败: ${e.message}", e)
return false
}
} }
/** /**
@ -411,6 +493,23 @@ class VolcanoTtsHelper(private val context: Context) {
* 播放队列中剩余文本并标记流式输入结束 * 播放队列中剩余文本并标记流式输入结束
*/ */
fun flushStream(): Boolean { fun flushStream(): Boolean {
if (!isInitialized) {
return false
}
try {
// 获取缓冲区中剩余的文本
val remainingText = streamBuffer.toString()
// 清空缓冲区
streamBuffer.clear()
// 如果缓冲区有未播放的文本,加入队列
if (remainingText.isNotEmpty()) {
pendingTexts.add(remainingText)
}
// 如果队列为空且引擎未启动,直接返回
if (pendingTexts.isEmpty() && !isEngineStarted) { if (pendingTexts.isEmpty() && !isEngineStarted) {
isProcessingQueue = false isProcessingQueue = false
return true return true
@ -425,12 +524,21 @@ class VolcanoTtsHelper(private val context: Context) {
return result return result
} }
// 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成
if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer && !pendingTexts.isEmpty()) {
return triggerNextSynthesis()
}
// 如果队列为空且当前没有播放,则关闭引擎 // 如果队列为空且当前没有播放,则关闭引擎
if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) { if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) {
closeEngine() closeEngine()
} }
return true return true
} catch (e: Exception) {
Log.e(TAG, "刷新流式文本失败: ${e.message}", e)
return false
}
} }
/** /**
@ -447,7 +555,7 @@ class VolcanoTtsHelper(private val context: Context) {
isSynthesisInProgress = false isSynthesisInProgress = false
return ret == SpeechEngineDefines.ERR_NO_ERROR return ret == SpeechEngineDefines.ERR_NO_ERROR
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "关闭引擎异常", e) Log.e(TAG, "关闭引擎异常", e)
return false return false
} }
} }
@ -521,12 +629,15 @@ class VolcanoTtsHelper(private val context: Context) {
isContinuousMode = false isContinuousMode = false
synthesisFromPlayer = false synthesisFromPlayer = false
// 清空流缓冲区
streamBuffer.clear()
// 发送取消事件 // 发送取消事件
dispatchEvent(TtsEvent(TtsEventType.CANCELED)) dispatchEvent(TtsEvent(TtsEventType.CANCELED))
return ret == SpeechEngineDefines.ERR_NO_ERROR return ret == SpeechEngineDefines.ERR_NO_ERROR
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止播放异常", e) Log.e(TAG, "停止播放异常", e)
return false return false
} }
} }
@ -549,10 +660,11 @@ class VolcanoTtsHelper(private val context: Context) {
engine = null engine = null
isInitialized = false isInitialized = false
pendingTexts.clear() pendingTexts.clear()
streamBuffer.clear()
listeners.clear() listeners.clear()
audioDataListeners.clear() audioDataListeners.clear()
} catch (e: Exception) { } 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 errCode = json.optInt("err_code", -1)
val errMsg = json.optString("err_msg", "未知错误") val errMsg = json.optString("err_msg", "未知错误")
FileLogger.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg")
// 标记当前合成结束 // 标记当前合成结束
isSynthesisInProgress = false isSynthesisInProgress = false

4
pubspec.yaml

@ -89,6 +89,10 @@ dependencies:
path: local_plugins/agent_service path: local_plugins/agent_service
music_service: music_service:
path: local_plugins/music_service path: local_plugins/music_service
volcano_speech:
path: local_plugins/volcano_speech
bytedance_speech:
path: local_plugins/bytedance_speech
dev_dependencies: dev_dependencies:

Loading…
Cancel
Save