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. 73
      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. 349
      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. 392
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt
  26. 660
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt
  27. 269
      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. 44
      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. 206
      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 {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":spotify_sdk"))
implementation(project(":deepvoice_utils"))
implementation(project(":ble_service"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
}
flutter {

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

9
android/settings.gradle.kts

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

2
lib/data/services/ble_manager.dart

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

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

@ -530,36 +530,73 @@ final responseId = event.data['responseId'] ?? '';
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
PermissionStatus status = await Permission.microphone.status;
// 如果已授权,直接返回 true
if (status.isGranted) {
// 创建待请求权限列表
List<Permission> permissionsToRequest = [];
if (Platform.isAndroid) {
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
} else if (Platform.isIOS) {
// iOS 只需要麦克风权限
permissionsToRequest.add(Permission.microphone);
}
// 批量请求所有需要的权限
final statuses = await permissionsToRequest.request();
// 检查是否所有权限都已授予
bool allGranted = true;
Permission? deniedPermission;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
deniedPermission = permission;
logger.e('权限 ${permission.toString()} 未被授予: $status');
}
});
// 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status;
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) {
return true;
}
// 如果被永久拒绝,提示用户前往设置页面
if (status.isPermanentlyDenied) {
// 处理特定权限被永久拒绝的情况
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限
if (status.isDenied) {
status = await Permission.microphone.request();
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false);
return false;
}
// 普通拒绝情况
if (deniedPermission != null) {
_showPermissionDialog(false);
}
return false;
} catch (e) {
logger.e('检查麦克风权限失败: $e');
return false;
}
}
// 获取Android SDK版本
Future<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) {

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

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

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

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

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

@ -1,15 +1,2 @@
rootProject.name = "agent_service"
// 包含项目依赖
include(":azure_speech")
include(":open_ai_service")
include(":deepvoice_utils")
include(":chat_storage")
include(":music_service")
// 设置项目路径
project(":azure_speech").projectDir = file("../../azure_speech/android")
project(":open_ai_service").projectDir = file("../../open_ai_service/android")
project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android")
project(":chat_storage").projectDir = file("../../chat_storage/android")
project(":music_service").projectDir = file("../../music_service/android")

349
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

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

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

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

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

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

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

@ -52,6 +52,6 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
dependencies {
// 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0")
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0")
implementation(project(":ble_service"))
}

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
<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.RECORD_AUDIO" />

392
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt

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

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

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

269
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

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

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

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

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.Intent
import androidx.core.content.ContextCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import com.yunqiinnovation.music_service.MusicServiceStarter
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
@ -24,7 +24,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
FileLogger.d(TAG, "Method call: ${call.method}")
Log.d(TAG, "Method call: ${call.method}")
when (call.method) {
"play" -> {
val song = call.argument<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 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("plugin.serialization") version "1.9.24"
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.deepvoice_utils"
namespace = "com.yunqiinnovation.open_ai_service"
// 目标 SDK 版本
compileSdk = 35
@ -16,7 +17,7 @@ android {
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 34
targetSdk = 33
}
// Java 语言级别兼容配置
@ -44,3 +45,20 @@ android {
}
}
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
implementation("com.anthropic:anthropic-java:0.8.0")
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
implementation("io.ktor:ktor-client-core:3.1.2")
implementation("io.ktor:ktor-client-cio:3.1.2")
implementation("io.ktor:ktor-client-okhttp:3.1.2")
implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("androidx.media:media:1.6.0")
}

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

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

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

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

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

206
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt

@ -6,7 +6,7 @@ import android.os.Looper
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
import com.yunqiinnovation.volcano_speech.utils.FileLogger
import android.util.Log
import org.json.JSONObject
import java.util.concurrent.ConcurrentLinkedQueue
@ -28,7 +28,7 @@ class VolcanoTtsHelper(private val context: Context) {
private var currentReqId = ""
// 配置参数
private var voice = "zh_male_M392_conversation_wvae_bigtts"
private var voice = "zh_female_linjianvhai_moon_bigtts"
// 文本队列管理
private val pendingTexts = ConcurrentLinkedQueue<String>()
@ -51,6 +51,10 @@ class VolcanoTtsHelper(private val context: Context) {
// 事件监听
private val listeners = mutableListOf<TtsEventListener>()
// 添加新的成员变量,用于流式文本处理
private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L
/**
* TTS事件类型
*/
@ -172,7 +176,7 @@ class VolcanoTtsHelper(private val context: Context) {
try {
SpeechEngineGenerator.PrepareEnvironment(context, null)
} catch (e: Exception) {
FileLogger.w(TAG, "准备环境时发生异常: ${e.message}")
Log.w(TAG, "准备环境时发生异常: ${e.message}")
}
// 创建引擎
@ -238,9 +242,9 @@ class VolcanoTtsHelper(private val context: Context) {
*/
private fun handleError(code: Int, message: String, e: Exception? = null) {
if (e != null) {
FileLogger.e(TAG, message, e)
Log.e(TAG, message, e)
} else {
FileLogger.e(TAG, message)
Log.e(TAG, message)
}
dispatchEvent(TtsEvent(TtsEventType.ERROR, code, message))
}
@ -250,7 +254,7 @@ class VolcanoTtsHelper(private val context: Context) {
*/
fun setVoice(voiceName: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "引擎未初始化")
Log.e(TAG, "引擎未初始化")
return false
}
@ -264,6 +268,25 @@ class VolcanoTtsHelper(private val context: Context) {
}
}
/**
* 处理文本,移除不需要的特殊字符
*/
private fun processText(text: String): String {
// 定义需要移除的特殊字符
val specialChars = listOf("#", "*")
// 移除所有定义的特殊字符
var processedText = text
specialChars.forEach { char ->
processedText = processedText.replace(char, " ")
}
// // 移除表情符号 (简单处理,移除常见的Unicode表情符号范围)
// processedText = processedText.replace(Regex("[\uD83C-\uDBFF\uDC00-\uDFFF]"), "")
return processedText
}
/**
* 单次播放文本
*/
@ -272,6 +295,9 @@ class VolcanoTtsHelper(private val context: Context) {
return false
}
// 处理文本,移除特殊字符
val processedText = processText(text)
// 停止当前播放
stop()
@ -289,10 +315,10 @@ class VolcanoTtsHelper(private val context: Context) {
configAudioCallback()
// 预备要合成的文本
currentText = text
currentText = processedText
// 单次播放模式:直接启动引擎并设置文本,自动开始合成
return startEngineWithText(text, false)
return startEngineWithText(processedText, false)
}
/**
@ -303,27 +329,83 @@ class VolcanoTtsHelper(private val context: Context) {
return false
}
// 设置连续播放模式
if (!isContinuousMode) {
isContinuousMode = true
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL)
try {
// 处理文本,移除特殊字符
val processedText = processText(text)
// 启用音频数据回调(如果需要)
configAudioCallback()
}
// 加入队列
pendingTexts.add(text)
// 如果当前引擎未启动,启动引擎
if (!isEngineStarted) {
return startEngine()
} else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) {
// 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成
return triggerNextSynthesis()
// 设置连续播放模式
if (!isContinuousMode) {
isContinuousMode = true
engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL)
// 启用音频数据回调(如果需要)
configAudioCallback()
}
// 添加新文本到缓冲区
streamBuffer.append(processedText)
// 增加防抖逻辑,避免频繁处理小片段文本
val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 600) {
return true
}
lastSpeakTime = currentTime
// 获取缓冲区中的全部文本
val currentText = streamBuffer.toString()
// 定义标点符号列表
val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':')
// 查找最后一个标点符号的位置
var lastPunctuationIndex = -1
for (i in currentText.indices.reversed()) {
if (currentText[i] in punctuationMarks) {
lastPunctuationIndex = i
break
}
}
// 如果找到标点符号,则播放到该标点符号
if (lastPunctuationIndex >= 0) {
// 提取要播放的文本(包含标点符号)
val textToSpeak = currentText.substring(0, lastPunctuationIndex + 1)
// 剩余的文本保存在缓冲区中
streamBuffer.delete(0, lastPunctuationIndex + 1)
// 加入队列播放提取的文本
pendingTexts.add(textToSpeak)
// 如果当前引擎未启动,启动引擎
if (!isEngineStarted) {
return startEngine()
} else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) {
// 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成
return triggerNextSynthesis()
}
} else if (currentText.length > 50) {
// 如果文本长度超过50个字符但没有找到标点符号,强制分段
val textToSpeak = currentText
streamBuffer.delete(0, currentText.length)
// 加入队列播放
pendingTexts.add(textToSpeak)
// 根据引擎状态触发合成
if (!isEngineStarted) {
return startEngine()
} else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) {
return triggerNextSynthesis()
}
}
return true
} catch (e: Exception) {
Log.e(TAG, "流式语音合成失败: ${e.message}", e)
return false
}
return true
}
/**
@ -411,26 +493,52 @@ class VolcanoTtsHelper(private val context: Context) {
* 播放队列中剩余文本并标记流式输入结束
*/
fun flushStream(): Boolean {
if (pendingTexts.isEmpty() && !isEngineStarted) {
isProcessingQueue = false
return true
if (!isInitialized) {
return false
}
// 如果引擎未启动,则启动引擎
if (!isEngineStarted) {
val result = startEngine()
if (result && !pendingTexts.isEmpty()) {
// 引擎启动成功后,在引擎启动回调中会自动触发第一句合成
try {
// 获取缓冲区中剩余的文本
val remainingText = streamBuffer.toString()
// 清空缓冲区
streamBuffer.clear()
// 如果缓冲区有未播放的文本,加入队列
if (remainingText.isNotEmpty()) {
pendingTexts.add(remainingText)
}
return result
}
// 如果队列为空且当前没有播放,则关闭引擎
if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) {
closeEngine()
// 如果队列为空且引擎未启动,直接返回
if (pendingTexts.isEmpty() && !isEngineStarted) {
isProcessingQueue = false
return true
}
// 如果引擎未启动,则启动引擎
if (!isEngineStarted) {
val result = startEngine()
if (result && !pendingTexts.isEmpty()) {
// 引擎启动成功后,在引擎启动回调中会自动触发第一句合成
}
return result
}
// 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成
if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer && !pendingTexts.isEmpty()) {
return triggerNextSynthesis()
}
// 如果队列为空且当前没有播放,则关闭引擎
if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) {
closeEngine()
}
return true
} catch (e: Exception) {
Log.e(TAG, "刷新流式文本失败: ${e.message}", e)
return false
}
return true
}
/**
@ -447,7 +555,7 @@ class VolcanoTtsHelper(private val context: Context) {
isSynthesisInProgress = false
return ret == SpeechEngineDefines.ERR_NO_ERROR
} catch (e: Exception) {
FileLogger.e(TAG, "关闭引擎异常", e)
Log.e(TAG, "关闭引擎异常", e)
return false
}
}
@ -521,12 +629,15 @@ class VolcanoTtsHelper(private val context: Context) {
isContinuousMode = false
synthesisFromPlayer = false
// 清空流缓冲区
streamBuffer.clear()
// 发送取消事件
dispatchEvent(TtsEvent(TtsEventType.CANCELED))
return ret == SpeechEngineDefines.ERR_NO_ERROR
} catch (e: Exception) {
FileLogger.e(TAG, "停止播放异常", e)
Log.e(TAG, "停止播放异常", e)
return false
}
}
@ -549,10 +660,11 @@ class VolcanoTtsHelper(private val context: Context) {
engine = null
isInitialized = false
pendingTexts.clear()
streamBuffer.clear()
listeners.clear()
audioDataListeners.clear()
} catch (e: Exception) {
FileLogger.e(TAG, "释放引擎异常", e)
Log.e(TAG, "释放引擎异常", e)
}
}
@ -642,7 +754,7 @@ class VolcanoTtsHelper(private val context: Context) {
val errCode = json.optInt("err_code", -1)
val errMsg = json.optString("err_msg", "未知错误")
FileLogger.e(TAG, "引擎错误: code=$errCode, msg=$errMsg")
Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg")
// 标记当前合成结束
isSynthesisInProgress = false

4
pubspec.yaml

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

Loading…
Cancel
Save