wolfplus 1 year ago
parent
commit
de6ed78c82
  1. 20
      android/app/build.gradle.kts
  2. 2
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  3. 2
      android/build.gradle.kts
  4. 9
      android/settings.gradle.kts
  5. 4
      lib/core/themes/theme_service.dart
  6. 85
      lib/core/utils/logger.dart
  7. 43
      lib/data/services/ble_manager.dart
  8. 8
      lib/data/services/bluetooth_manager.dart
  9. 2
      lib/data/services/chat_history_service.dart
  10. 6
      lib/data/services/deapsound_ai_service.dart
  11. 6
      lib/data/services/kouzi_ai_service.dart
  12. 4
      lib/data/services/speech_factory.dart
  13. 21
      lib/data/services/speech_impl/azure_asr_service.dart
  14. 22
      lib/data/services/speech_impl/azure_tts_service.dart
  15. 18
      lib/data/services/speech_impl/flutter_asr_service.dart
  16. 24
      lib/data/services/speech_impl/flutter_tts_service.dart
  17. 22
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  18. 38
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  19. 16
      lib/data/services/speech_impl/volcano_tts_api_service.dart
  20. 20
      lib/data/services/speech_impl/xunfei_asr_service.dart
  21. 18
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  22. 12
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  23. 123
      lib/modules/agent/controllers/agent_controller.dart
  24. 10
      lib/modules/chat/controllers/chat_controller.dart
  25. 8
      lib/modules/meeting/controllers/meeting_details_controller.dart
  26. 16
      lib/modules/pairing/controllers/pairing_controller.dart
  27. 18
      lib/modules/speech_test/controllers/speech_test_controller.dart
  28. 26
      lib/modules/translation/controllers/translation_controller.dart
  29. 8
      lib/modules/translation/controllers/translation_detail_controller.dart
  30. 5
      local_plugins/agent_service/android/build.gradle.kts
  31. 13
      local_plugins/agent_service/android/settings.gradle.kts
  32. 464
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  33. 30
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  34. 28
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  35. 57
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  36. 14
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt
  37. 12
      local_plugins/agent_service/pubspec.yaml
  38. 5
      local_plugins/azure_speech/android/build.gradle.kts
  39. 51
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  40. 224
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  41. 298
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  42. 74
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt
  43. 15
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt
  44. 215
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  45. 11
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt
  46. 13
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt
  47. 28
      local_plugins/bytedance_speech/android/build.gradle.kts
  48. 2
      local_plugins/bytedance_speech/android/src/main/AndroidManifest.xml
  49. 392
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceASR.kt
  50. 329
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
  51. 670
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceSpeechPlugin.kt
  52. 757
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt
  53. 5
      local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt
  54. 2
      local_plugins/classic_bluetooth/pubspec.yaml
  55. 1
      local_plugins/deepvoice_utils/android/settings.gradle.kts
  56. 7
      local_plugins/deepvoice_utils/android/src/main/AndroidManifest.xml
  57. 33
      local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/DeepvoiceUtilsPlugin.kt
  58. 237
      local_plugins/deepvoice_utils/android/src/main/kotlin/com/yunqiinnovation/deepvoice_utils/FileLogger.kt
  59. 4
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.h
  60. 12
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.m
  61. 30
      local_plugins/deepvoice_utils/ios/Classes/DeepvoiceUtilsPlugin.swift
  62. 286
      local_plugins/deepvoice_utils/ios/Classes/FileLogger.swift
  63. 11
      local_plugins/deepvoice_utils/ios/Classes/SwiftDeepvoiceUtilsPlugin.swift
  64. 54
      local_plugins/deepvoice_utils/ios/README.md
  65. 19
      local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec
  66. 3
      local_plugins/deepvoice_utils/lib/deepvoice_utils.dart
  67. 31
      local_plugins/deepvoice_utils/lib/src/file_logger.dart
  68. 27
      local_plugins/deepvoice_utils/pubspec.yaml
  69. 1
      local_plugins/music_service/android/build.gradle
  70. 25
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  71. 4
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  72. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  73. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  74. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  75. 22
      local_plugins/speech/android/build.gradle.kts
  76. 1
      local_plugins/speech/android/settings.gradle.kts
  77. 10
      local_plugins/speech/android/src/main/AndroidManifest.xml
  78. 52
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/AsrEvents.kt
  79. 90
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/IAsrService.kt
  80. 92
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt
  81. 55
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt
  82. 44
      local_plugins/volcano_speech/android/build.gradle.kts
  83. 134
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt
  84. 20
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt
  85. 206
      local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt
  86. 6
      pubspec.yaml

20
android/app/build.gradle.kts

@ -120,28 +120,8 @@ android {
} }
dependencies { dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":spotify_sdk"))
implementation(project(":deepvoice_utils"))
implementation(project(":ble_service"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化 // 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
} }
flutter { flutter {

2
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -12,7 +12,7 @@ import io.flutter.embedding.android.FlutterActivity
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
private val TAG = "MainActivity" private val TAG = "MainActivity"

2
android/build.gradle.kts

@ -35,7 +35,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
// 其他依赖... // 其他依赖...
} }
} }

9
android/settings.gradle.kts

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

4
lib/core/themes/theme_service.dart

@ -21,7 +21,7 @@ class ThemeService {
Logger.info('从存储读取主题设置: $value'); Logger.info('从存储读取主题设置: $value');
return value ?? false; return value ?? false;
} catch (e) { } catch (e) {
Logger.error('读取主题设置失败: $e'); Logger.error('读取主题设置失败: ${e.toString()}');
return false; return false;
} }
} }
@ -32,7 +32,7 @@ class ThemeService {
Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}'); Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}');
await _box.write(_key, isDarkMode); await _box.write(_key, isDarkMode);
} catch (e) { } catch (e) {
Logger.error('保存主题设置失败: $e'); Logger.error('保存主题设置失败: ${e.toString()}');
} }
} }
} }

85
lib/core/utils/logger.dart

@ -15,77 +15,84 @@ class Logger {
/// 当前日志级别,低于此级别的日志不会被输出 /// 当前日志级别,低于此级别的日志不会被输出
static LogLevel _currentLevel = LogLevel.debug; static LogLevel _currentLevel = LogLevel.debug;
/// 默认标签
static const String _defaultTag = 'App';
/// 设置日志级别 /// 设置日志级别
static void setLevel(LogLevel level) { static void setLevel(LogLevel level) {
_currentLevel = level; _currentLevel = level;
} }
/// 输出调试日志 (不带标签版本)
static void debug(String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', _defaultTag, message);
}
}
/// 输出调试日志 /// 输出调试日志
static void debug(String message, {String? tag}) { static void d(String tag, String message) {
if (_currentLevel.index <= LogLevel.debug.index) { if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', message, tag: tag); _log('DEBUG', tag, message);
}
}
/// 输出信息日志 (不带标签版本)
static void info(String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', _defaultTag, message);
} }
} }
/// 输出信息日志 /// 输出信息日志
static void info(String message, {String? tag}) { static void i(String tag, String message) {
if (_currentLevel.index <= LogLevel.info.index) { if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', message, tag: tag); _log('INFO', tag, message);
}
}
/// 输出警告日志 (不带标签版本)
static void warning(String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', _defaultTag, message);
} }
} }
/// 输出警告日志 /// 输出警告日志
static void warning(String message, {String? tag}) { static void w(String tag, String message) {
if (_currentLevel.index <= LogLevel.warning.index) { if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', message, tag: tag); _log('WARNING', tag, message);
} }
} }
/// 输出错误日志 /// 输出错误日志 (不带标签版本)
static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) { static void error(String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) { if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', message, tag: tag); _log('ERROR', _defaultTag, message);
if (error != null) { if (error != null) {
_log('ERROR', 'Original error: $error', tag: tag); _log('ERROR', _defaultTag, 'Original error: $error');
} }
if (stackTrace != null) { if (stackTrace != null) {
_log('ERROR', 'Stack trace: $stackTrace', tag: tag); _log('ERROR', _defaultTag, 'Stack trace: $stackTrace');
} }
} }
} }
/// 获取当前调用栈的类名 /// 输出错误日志
static String? _getCallerClassName() { static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
try { if (_currentLevel.index <= LogLevel.error.index) {
final stackTrace = StackTrace.current.toString(); _log('ERROR', tag, message);
final frames = stackTrace.split('\n'); if (error != null) {
_log('ERROR', tag, 'Original error: $error');
// 查找调用者帧 (跳过Logger框架内部的帧) }
for (int i = 0; i < frames.length; i++) { if (stackTrace != null) {
final frame = frames[i].trim(); _log('ERROR', tag, 'Stack trace: $stackTrace');
if (frame.contains('Logger.') || frame.contains('_log')) {
continue;
}
// 简单的解析类名
final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s');
final match = regex.firstMatch(frame);
if (match != null && match.groupCount >= 1) {
return match.group(1);
}
break;
} }
} catch (e) {
// 忽略解析错误
} }
return null;
} }
/// 内部日志输出方法 /// 内部日志输出方法
static void _log(String level, String message, {String? tag}) { static void _log(String level, String tag, String message) {
final timestamp = DateTime.now().toString(); developer.log('[$tag]: $message');
final className = tag ?? _getCallerClassName() ?? '';
final tagInfo = className.isNotEmpty ? '[$className] ' : '';
developer.log('[$timestamp][$level][$tagInfo]: $message');
} }
} }

43
lib/data/services/ble_manager.dart

@ -198,7 +198,7 @@ class BleManager extends GetxService {
}, },
); );
} catch (e) { } catch (e) {
Logger.error('设置BLE事件监听器失败: $e'); Logger.error('设置BLE事件监听器失败: ${e.toString()}');
} }
} }
@ -225,7 +225,7 @@ class BleManager extends GetxService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('处理设备信息异常: $e'); Logger.error('处理设备信息异常: ${e.toString()}');
} }
} }
@ -303,10 +303,9 @@ class BleManager extends GetxService {
caseCharging: caseCharging, caseCharging: caseCharging,
); );
Logger.info( Logger.info('设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
'设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) { } catch (e) {
Logger.error('更新电池信息失败: $e'); Logger.error('更新电池信息失败: ${e.toString()}');
} }
} }
@ -329,7 +328,7 @@ class BleManager extends GetxService {
Logger.info('设备版本信息已更新: 版本=$version'); Logger.info('设备版本信息已更新: 版本=$version');
} catch (e) { } catch (e) {
Logger.error('更新版本信息失败: $e'); Logger.error('更新版本信息失败: ${e.toString()}');
} }
} }
@ -352,7 +351,7 @@ class BleManager extends GetxService {
Logger.info('设备设备id已更新: 设备id=$productId'); Logger.info('设备设备id已更新: 设备id=$productId');
} catch (e) { } catch (e) {
Logger.error('更新设备id失败: $e'); Logger.error('更新设备id失败: ${e.toString()}');
} }
} }
@ -440,7 +439,7 @@ class BleManager extends GetxService {
Logger.warning('产品id获取指令发送失败'); Logger.warning('产品id获取指令发送失败');
} }
} catch (e) { } catch (e) {
Logger.error('获取BLE设备信息失败: $e'); Logger.error('获取BLE设备信息失败: ${e.toString()}');
} }
} }
@ -455,7 +454,7 @@ class BleManager extends GetxService {
return _connectionState.value; return _connectionState.value;
} catch (e) { } catch (e) {
Logger.error('获取BLE连接状态失败: $e'); Logger.error('获取BLE连接状态失败: ${e.toString()}');
return BleConnectionState.error; return BleConnectionState.error;
} }
} }
@ -497,6 +496,8 @@ class BleManager extends GetxService {
'openaiModel': openaiModel, 'openaiModel': openaiModel,
'systemPrompt': systemPrompt, 'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData), 'mcpServer': jsonEncode(jsonData),
'volcanoAppId': '2316081933',
'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En',
}; };
// 记录日志(不记录敏感信息) // 记录日志(不记录敏感信息)
@ -540,7 +541,7 @@ class BleManager extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动BLE服务失败: $e'); Logger.error('启动BLE服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -560,7 +561,7 @@ class BleManager extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止BLE服务失败: $e'); Logger.error('停止BLE服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -570,7 +571,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getBatteryLevel(); return await _bleService.getBatteryLevel();
} catch (e) { } catch (e) {
Logger.error('获取电池电量失败: $e'); Logger.error('获取电池电量失败: ${e.toString()}');
return 0; return 0;
} }
} }
@ -580,7 +581,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getBatteryInfo(); return await _bleService.getBatteryInfo();
} catch (e) { } catch (e) {
Logger.error('获取电池信息失败: $e'); Logger.error('获取电池信息失败: ${e.toString()}');
return false; return false;
} }
} }
@ -590,7 +591,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getProductId(); return await _bleService.getProductId();
} catch (e) { } catch (e) {
Logger.error('获取设备版本失败: $e'); Logger.error('获取设备版本失败: ${e.toString()}');
return false; return false;
} }
} }
@ -600,7 +601,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.getDeviceVersion(); return await _bleService.getDeviceVersion();
} catch (e) { } catch (e) {
Logger.error('获取设备版本失败: $e'); Logger.error('获取设备版本失败: ${e.toString()}');
return false; return false;
} }
} }
@ -610,7 +611,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.openEncoder(); return await _bleService.openEncoder();
} catch (e) { } catch (e) {
Logger.error('打开编码器失败: $e'); Logger.error('打开编码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -620,7 +621,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.openDecoder(); return await _bleService.openDecoder();
} catch (e) { } catch (e) {
Logger.error('打开解码器失败: $e'); Logger.error('打开解码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -630,7 +631,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.openA2DPDecoder(); return await _bleService.openA2DPDecoder();
} catch (e) { } catch (e) {
Logger.error('打开解码器失败: $e'); Logger.error('打开解码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -640,7 +641,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.closeCodec(); return await _bleService.closeCodec();
} catch (e) { } catch (e) {
Logger.error('关闭编解码器失败: $e'); Logger.error('关闭编解码器失败: ${e.toString()}');
return false; return false;
} }
} }
@ -650,7 +651,7 @@ class BleManager extends GetxService {
try { try {
return await _bleService.playA2DP(); return await _bleService.playA2DP();
} catch (e) { } catch (e) {
Logger.error('播放A2DP失败: $e'); Logger.error('播放A2DP失败: ${e.toString()}');
return false; return false;
} }
} }
@ -662,4 +663,4 @@ class BleManager extends GetxService {
} }
return Future.value(Get.find<BleManager>()); return Future.value(Get.find<BleManager>());
} }
} }

8
lib/data/services/bluetooth_manager.dart

@ -75,11 +75,11 @@ class BluetoothManager extends GetxService {
} }
}, },
onError: (error) { onError: (error) {
Logger.error('蓝牙事件流错误: $error'); Logger.error('蓝牙事件流错误: ${error.toString()}');
}, },
); );
} catch (e) { } catch (e) {
Logger.error('设置蓝牙事件监听器失败: $e'); Logger.error('设置蓝牙事件监听器失败: ${e.toString()}');
} }
} }
@ -106,7 +106,7 @@ class BluetoothManager extends GetxService {
Logger.info('未找到已连接的蓝牙设备'); Logger.info('未找到已连接的蓝牙设备');
} catch (e) { } catch (e) {
Logger.error('刷新设备连接失败: $e'); Logger.error('刷新设备连接失败: ${e.toString()}');
} }
} }
@ -117,7 +117,7 @@ class BluetoothManager extends GetxService {
_bluetoothState.value = isEnabled ? 'on' : 'off'; _bluetoothState.value = isEnabled ? 'on' : 'off';
return isEnabled; return isEnabled;
} catch (e) { } catch (e) {
Logger.error('检查蓝牙状态失败: $e'); Logger.error('检查蓝牙状态失败: ${e.toString()}');
return false; return false;
} }
} }

2
lib/data/services/chat_history_service.dart

@ -39,7 +39,7 @@ class ChatHistoryService extends GetxService {
// 检查并清理过多的历史记录 // 检查并清理过多的历史记录
await _cleanupOldMessages(agentId); await _cleanupOldMessages(agentId);
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败', e); Logger.error('保存聊天记录失败', e.toString());
} }
} }

6
lib/data/services/deapsound_ai_service.dart

@ -112,14 +112,14 @@ class DeapsoundAIService implements AiService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('JSON解析错误: $e'); Logger.error('JSON解析错误: ${e.toString()}');
} }
} }
} }
} catch (e) { } catch (e) {
Logger.error('请求异常: $e'); Logger.error('请求异常: ${e.toString()}');
} finally { } finally {
client.close(); client.close();
} }
} }
} }

6
lib/data/services/kouzi_ai_service.dart

@ -82,14 +82,14 @@ class KouziAIService implements AiService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('JSON解析错误: $e'); Logger.error('JSON解析错误: ${e.toString()}');
} }
} }
} }
} catch (e) { } catch (e) {
Logger.error('请求异常: $e'); Logger.error('请求异常: ${e.toString()}');
} finally { } finally {
client.close(); client.close();
} }
} }
} }

4
lib/data/services/speech_factory.dart

@ -88,7 +88,7 @@ class SpeechFactory extends GetxService {
Logger.info( Logger.info(
'语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); '语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) { } catch (e) {
Logger.error('语音服务初始化失败: $e'); Logger.error('语音服务初始化失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -108,4 +108,4 @@ class SpeechFactory extends GetxService {
} }
return Get.find<TtsService>(); return Get.find<TtsService>();
} }
} }

21
lib/data/services/speech_impl/azure_asr_service.dart

@ -106,7 +106,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result; return result;
} catch (e) { } catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e'); Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false; _isInitialized = false;
rethrow; rethrow;
} }
@ -144,7 +144,7 @@ class AzureAsrService extends GetxService implements AsrService {
throw Exception('无效的识别结果格式'); throw Exception('无效的识别结果格式');
} catch (e) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@ -177,7 +177,7 @@ class AzureAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true; _isContinuousRecognitionActive = true;
return _eventStreamController!.stream; return _eventStreamController!.stream;
} catch (e) { } catch (e) {
Logger.error('开始连续语音识别失败: $e'); Logger.error('开始连续语音识别失败: ${e.toString()}');
_cleanupEventStream(); _cleanupEventStream();
rethrow; rethrow;
} }
@ -195,7 +195,7 @@ class AzureAsrService extends GetxService implements AsrService {
_cleanupEventStream(); _cleanupEventStream();
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
_cleanupEventStream(); _cleanupEventStream();
rethrow; rethrow;
} }
@ -214,7 +214,6 @@ class AzureAsrService extends GetxService implements AsrService {
final String eventType = eventMap['type'] as String? ?? ''; final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试 // 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) { switch (eventType) {
case 'result': case 'result':
@ -260,7 +259,7 @@ class AzureAsrService extends GetxService implements AsrService {
final String errorDetails = eventMap['errorDetails'] as String? ?? ''; final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) { if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails'); Logger.error('识别取消: $reason - ${errorDetails.toString()}');
} }
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
@ -271,7 +270,7 @@ class AzureAsrService extends GetxService implements AsrService {
case 'error': case 'error':
final String error = eventMap['message'] as String? ?? ''; final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error'); Logger.error('识别错误: ${error.toString()}');
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error, type: RecognitionEventType.error,
error: error, error: error,
@ -282,7 +281,7 @@ class AzureAsrService extends GetxService implements AsrService {
/// 处理识别事件流错误 /// 处理识别事件流错误
void _handleRecognitionError(Object error) { void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error'); Logger.error('识别事件流错误: ${error.toString()}');
_eventStreamController?.addError(error); _eventStreamController?.addError(error);
_cleanupEventStream(); _cleanupEventStream();
} }
@ -311,7 +310,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放'); Logger.info('Azure 语音识别资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: ${e.toString()}');
_cleanupEventStream(); _cleanupEventStream();
_isInitialized = false; _isInitialized = false;
} }
@ -339,8 +338,8 @@ class AzureAsrService extends GetxService implements AsrService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('推送音频数据失败: $e'); Logger.error('推送音频数据失败: ${e.toString()}');
return false; return false;
} }
} }
} }

22
lib/data/services/speech_impl/azure_tts_service.dart

@ -69,7 +69,7 @@ class AzureTtsService extends GetxService implements TtsService {
_handleTtsEvent(event); _handleTtsEvent(event);
} }
}, onError: (error) { }, onError: (error) {
Logger.error('TTS事件流错误: $error'); Logger.error('TTS事件流错误: ${error.toString()}');
_eventStreamController.addError(error); _eventStreamController.addError(error);
}); });
} }
@ -78,7 +78,7 @@ class AzureTtsService extends GetxService implements TtsService {
void _handleTtsEvent(Map<dynamic, dynamic> event) { void _handleTtsEvent(Map<dynamic, dynamic> event) {
final String eventType = event['type'] as String? ?? ''; final String eventType = event['type'] as String? ?? '';
// Logger.debug('收到TTS事件: $eventType, 数据: $event'); // Logger.debug('收到TTS事件: ${e.toString()}ventType, 数据: ${e.toString()}vent');
switch (eventType) { switch (eventType) {
case 'synthesis_started': case 'synthesis_started':
@ -159,7 +159,7 @@ class AzureTtsService extends GetxService implements TtsService {
_isInitialized = result; _isInitialized = result;
return result; return result;
} catch (e) { } catch (e) {
Logger.error('初始化失败: $e'); Logger.error('初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -177,7 +177,7 @@ class AzureTtsService extends GetxService implements TtsService {
if (result) _currentVoice = voiceName; if (result) _currentVoice = voiceName;
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音失败: $e'); Logger.error('设置语音失败: ${e.toString()}');
return false; return false;
} }
} }
@ -195,7 +195,7 @@ class AzureTtsService extends GetxService implements TtsService {
return result == true; return result == true;
} catch (e) { } catch (e) {
Logger.error('语音合成失败: $e'); Logger.error('语音合成失败: ${e.toString()}');
return false; return false;
} }
} }
@ -250,7 +250,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: ${e.toString()}');
return false; return false;
} }
} }
@ -316,7 +316,7 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return result; return result;
} catch (e) { } catch (e) {
Logger.error('刷新TTS流失败: $e'); Logger.error('刷新TTS流失败: ${e.toString()}');
return false; return false;
} }
} }
@ -358,7 +358,7 @@ class AzureTtsService extends GetxService implements TtsService {
_textQueue.removeAt(0); _textQueue.removeAt(0);
} }
} catch (e) { } catch (e) {
Logger.error('处理语音队列出错: $e'); Logger.error('处理语音队列出错: ${e.toString()}');
// 出错时移除当前项并尝试继续 // 出错时移除当前项并尝试继续
if (_textQueue.isNotEmpty) { if (_textQueue.isNotEmpty) {
@ -389,7 +389,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: ${e.toString()}');
return false; return false;
} }
} }
@ -410,7 +410,7 @@ class AzureTtsService extends GetxService implements TtsService {
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
_isInitialized = false; _isInitialized = false;
} catch (e) { } catch (e) {
Logger.error('释放资源失败: $e'); Logger.error('释放资源失败: ${e.toString()}');
} }
} }
} }

18
lib/data/services/speech_impl/flutter_asr_service.dart

@ -107,7 +107,7 @@ class FlutterAsrService extends GetxService implements AsrService {
} catch (e) { } catch (e) {
_isAvailable.value = false; _isAvailable.value = false;
_asrState.value = AsrState.error; _asrState.value = AsrState.error;
Logger.error('语音识别初始化失败: $e'); Logger.error('语音识别初始化失败: ${e.toString()}');
} }
} }
@ -203,7 +203,7 @@ class FlutterAsrService extends GetxService implements AsrService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: ${e.toString()}');
_asrState.value = AsrState.error; _asrState.value = AsrState.error;
return false; return false;
} }
@ -253,7 +253,7 @@ class FlutterAsrService extends GetxService implements AsrService {
?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突')); ?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突'));
} }
} catch (e) { } catch (e) {
Logger.error('重启连续识别失败: $e'); Logger.error('重启连续识别失败: ${e.toString()}');
// 发送错误给控制器 // 发送错误给控制器
_continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e')); _continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e'));
@ -304,7 +304,7 @@ class FlutterAsrService extends GetxService implements AsrService {
await _initSpeech(); await _initSpeech();
return _isAvailable.value; return _isAvailable.value;
} catch (e) { } catch (e) {
Logger.error('语音识别初始化失败: $e'); Logger.error('语音识别初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -357,7 +357,7 @@ class FlutterAsrService extends GetxService implements AsrService {
}, },
); );
} catch (e) { } catch (e) {
Logger.error('一次性语音识别失败: $e'); Logger.error('一次性语音识别失败: ${e.toString()}');
return RecognitionEvent.error('$e'); return RecognitionEvent.error('$e');
} }
} }
@ -406,7 +406,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = true; _isListening = true;
return _continuousRecognitionController!.stream; return _continuousRecognitionController!.stream;
} catch (e) { } catch (e) {
Logger.error('启动连续语音识别失败: $e'); Logger.error('启动连续语音识别失败: ${e.toString()}');
// 如果已经创建了控制器,使用它 // 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) { if (_continuousRecognitionController != null) {
@ -442,7 +442,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_asrState.value = AsrState.done; _asrState.value = AsrState.done;
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
// 强制重置状态 // 强制重置状态
_isListening = false; _isListening = false;
@ -472,7 +472,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = false; _isListening = false;
_asrState.value = AsrState.notInitialized; _asrState.value = AsrState.notInitialized;
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: ${e.toString()}');
} }
} }
} }

24
lib/data/services/speech_impl/flutter_tts_service.dart

@ -96,7 +96,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('FlutterTtsService 已初始化'); Logger.info('FlutterTtsService 已初始化');
} catch (e) { } catch (e) {
Logger.error('TTS初始化失败: $e'); Logger.error('TTS初始化失败: ${e.toString()}');
} }
} }
@ -118,7 +118,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('支持的语音: ${_supportedVoices.join(", ")}'); Logger.info('支持的语音: ${_supportedVoices.join(", ")}');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('TTS初始化失败: $e'); Logger.error('TTS初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -149,7 +149,7 @@ class FlutterTtsService extends GetxService implements TtsService {
_currentVoice.value = _supportedVoices.first; _currentVoice.value = _supportedVoices.first;
} }
} catch (e) { } catch (e) {
Logger.error('加载TTS语音失败: $e'); Logger.error('加载TTS语音失败: ${e.toString()}');
// 添加默认语音 // 添加默认语音
if (_supportedVoices.isEmpty) { if (_supportedVoices.isEmpty) {
@ -175,7 +175,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName'); Logger.info('设置TTS语音: $voiceName');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('设置TTS语音失败: $e'); Logger.error('设置TTS语音失败: ${e.toString()}');
return false; return false;
} }
} }
@ -200,7 +200,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('设置语音参数失败: $e'); Logger.error('设置语音参数失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e')); _eventController.add(TtsEvent.error('设置语音参数失败: $e'));
@ -227,7 +227,7 @@ class FlutterTtsService extends GetxService implements TtsService {
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
Logger.error('TTS播放失败: $e'); Logger.error('TTS播放失败: ${e.toString()}');
return false; return false;
} }
} }
@ -288,7 +288,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
@ -333,7 +333,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('等待TTS队列播放完成失败: $e'); Logger.error('等待TTS队列播放完成失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
@ -382,7 +382,7 @@ class FlutterTtsService extends GetxService implements TtsService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('TTS处理队列失败: $e'); Logger.error('TTS处理队列失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error(e.toString())); _eventController.add(TtsEvent.error(e.toString()));
@ -422,7 +422,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS停止播放完成'); Logger.info('TTS停止播放完成');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('TTS停止播放失败: $e'); Logger.error('TTS停止播放失败: ${e.toString()}');
// 即使出错也尝试强制重置状态 // 即使出错也尝试强制重置状态
_isSpeaking.value = false; _isSpeaking.value = false;
@ -443,7 +443,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS资源已释放'); Logger.info('TTS资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放TTS资源失败: $e'); Logger.error('释放TTS资源失败: ${e.toString()}');
} }
} }
@ -462,7 +462,7 @@ class FlutterTtsService extends GetxService implements TtsService {
await flushStream(); await flushStream();
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理文本流时出错: $e'); Logger.error('处理文本流时出错: ${e.toString()}');
return false; return false;
} }
} }

22
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -76,7 +76,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
); );
return true; return true;
} catch (e) { } catch (e) {
Logger.error('TTS初始化失败: $e'); Logger.error('TTS初始化失败: ${e.toString()}');
return false; return false;
} }
} }
@ -88,7 +88,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName'); Logger.info('设置TTS语音: $voiceName');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('设置TTS语音失败: $e'); Logger.error('设置TTS语音失败: ${e.toString()}');
return false; return false;
} }
} }
@ -127,7 +127,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
_handle(message); _handle(message);
}, },
onError: (error) { onError: (error) {
Logger.error('连接失败: $error'); Logger.error('连接失败: ${error.toString()}');
}, },
onDone: () { onDone: () {
Logger.error('WebSocket 连接已关闭!'); Logger.error('WebSocket 连接已关闭!');
@ -135,7 +135,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
cancelOnError: true, cancelOnError: true,
); );
} catch (e) { } catch (e) {
Logger.error('初始化异常: $e'); Logger.error('初始化异常: ${e.toString()}');
} }
} }
@ -226,7 +226,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
_isSpeaking.value = false; _isSpeaking.value = false;
Logger.error('语音合成失败: $e'); Logger.error('语音合成失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e')); _eventController.add(TtsEvent.error('语音合成失败: $e'));
@ -285,7 +285,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e')); _eventController.add(TtsEvent.error('处理流式文本失败: $e'));
@ -319,7 +319,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('刷新TTS流失败: $e'); Logger.error('刷新TTS流失败: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e')); _eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
@ -347,7 +347,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await speakOnce(item); await speakOnce(item);
} }
} catch (e) { } catch (e) {
Logger.error('处理语音队列出错: $e'); Logger.error('处理语音队列出错: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e')); _eventController.add(TtsEvent.error('处理语音队列出错: $e'));
@ -370,7 +370,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: ${e.toString()}');
// 发送错误事件 // 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e')); _eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false; return false;
@ -387,7 +387,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await _eventController.close(); await _eventController.close();
_isInitialized = false; _isInitialized = false;
} catch (e) { } catch (e) {
Logger.error('释放资源失败: $e'); Logger.error('释放资源失败: ${e.toString()}');
} }
} }
} }

38
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -99,7 +99,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('火山语音API已初始化'); Logger.info('火山语音API已初始化');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('初始化火山语音API失败: $e'); Logger.error('初始化火山语音API失败: ${e.toString()}');
return false; return false;
} }
} }
@ -151,7 +151,7 @@ class VolcanoAsrApiService implements AsrService {
return resultCompleter.future; return resultCompleter.future;
} catch (e) { } catch (e) {
Logger.error('单次语音识别失败: $e'); Logger.error('单次语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@ -184,7 +184,7 @@ class VolcanoAsrApiService implements AsrService {
return _recognitionController!.stream; return _recognitionController!.stream;
} catch (e) { } catch (e) {
Logger.error('开始连续语音识别失败: $e'); Logger.error('开始连续语音识别失败: ${e.toString()}');
await _cleanupResources(); await _cleanupResources();
// 返回一个包含错误的流 // 返回一个包含错误的流
@ -222,7 +222,7 @@ class VolcanoAsrApiService implements AsrService {
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
// 确保资源被清理 // 确保资源被清理
await _cleanupResources(); await _cleanupResources();
@ -250,7 +250,7 @@ class VolcanoAsrApiService implements AsrService {
_isInitialized = false; _isInitialized = false;
Logger.info('火山语音API资源已释放'); Logger.info('火山语音API资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放火山语音API资源失败: $e'); Logger.error('释放火山语音API资源失败: ${e.toString()}');
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
_isInitialized = false; _isInitialized = false;
} }
@ -284,7 +284,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音机初始化成功'); Logger.info('录音机初始化成功');
} catch (e) { } catch (e) {
// _recorder = null; // _recorder = null;
Logger.error('初始化录音机失败: $e'); Logger.error('初始化录音机失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -344,7 +344,7 @@ class VolcanoAsrApiService implements AsrService {
_connectionCompleter!.completeError(e); _connectionCompleter!.completeError(e);
} }
Logger.error('连接WebSocket失败: $e'); Logger.error('连接WebSocket失败: ${e.toString()}');
rethrow; rethrow;
} }
@ -432,7 +432,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(heartbeatMsg)); _channel!.sink.add(jsonEncode(heartbeatMsg));
} catch (e) { } catch (e) {
Logger.error('发送心跳包失败: $e'); Logger.error('发送心跳包失败: ${e.toString()}');
} }
} }
} }
@ -461,7 +461,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(startCommand)); _channel!.sink.add(jsonEncode(startCommand));
Logger.info('已发送开始识别指令'); Logger.info('已发送开始识别指令');
} catch (e) { } catch (e) {
Logger.error('发送开始识别指令失败: $e'); Logger.error('发送开始识别指令失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -496,7 +496,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音已开始'); Logger.info('录音已开始');
} catch (e) { } catch (e) {
_isRecording = false; _isRecording = false;
Logger.error('开始录音失败: $e'); Logger.error('开始录音失败: ${e.toString()}');
rethrow; rethrow;
} }
*/ */
@ -544,7 +544,7 @@ class VolcanoAsrApiService implements AsrService {
await _sendAudioData(audioData); await _sendAudioData(audioData);
} }
} catch (e) { } catch (e) {
Logger.error('捕获和发送音频失败: $e'); Logger.error('捕获和发送音频失败: ${e.toString()}');
} }
*/ */
} }
@ -562,7 +562,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(audioMessage)); _channel!.sink.add(jsonEncode(audioMessage));
} catch (e) { } catch (e) {
Logger.error('发送音频数据失败: $e'); Logger.error('发送音频数据失败: ${e.toString()}');
} }
} }
@ -579,7 +579,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(endCommand)); _channel!.sink.add(jsonEncode(endCommand));
Logger.info('已发送结束指令'); Logger.info('已发送结束指令');
} catch (e) { } catch (e) {
Logger.error('发送结束指令失败: $e'); Logger.error('发送结束指令失败: ${e.toString()}');
} }
} }
@ -596,7 +596,7 @@ class VolcanoAsrApiService implements AsrService {
_isRecording = false; _isRecording = false;
Logger.info('录音已停止'); Logger.info('录音已停止');
} catch (e) { } catch (e) {
Logger.error('停止录音失败: $e'); Logger.error('停止录音失败: ${e.toString()}');
} }
} }
*/ */
@ -641,7 +641,7 @@ class VolcanoAsrApiService implements AsrService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('处理WebSocket消息失败: $e'); Logger.error('处理WebSocket消息失败: ${e.toString()}');
if (_recognitionController != null && !_recognitionController!.isClosed) { if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError('处理消息失败: $e'); _recognitionController!.addError('处理消息失败: $e');
@ -675,7 +675,7 @@ class VolcanoAsrApiService implements AsrService {
} }
} }
} catch (e) { } catch (e) {
Logger.error('处理结果消息失败: $e'); Logger.error('处理结果消息失败: ${e.toString()}');
} }
} }
@ -741,7 +741,7 @@ class VolcanoAsrApiService implements AsrService {
/// 处理WebSocket错误 /// 处理WebSocket错误
void _handleWebSocketError(dynamic error) { void _handleWebSocketError(dynamic error) {
Logger.error('WebSocket错误: $error'); Logger.error('WebSocket错误: ${e.toString()}rror');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(error); _connectionCompleter!.completeError(error);
@ -789,7 +789,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.close(); _channel!.sink.close();
_channel = null; _channel = null;
} catch (e) { } catch (e) {
Logger.error('关闭WebSocket连接失败: $e'); Logger.error('关闭WebSocket连接失败: ${e.toString()}');
} }
} }
} }
@ -811,4 +811,4 @@ class VolcanoAsrApiService implements AsrService {
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
_isRecognitionEndRequested = false; _isRecognitionEndRequested = false;
} }
} }

16
lib/data/services/speech_impl/volcano_tts_api_service.dart

@ -357,7 +357,7 @@ class VolcanoTtsApiService extends GetxController {
_handleMessage, _handleMessage,
onError: (error) { onError: (error) {
// 直接处理错误,不尝试重连 // 直接处理错误,不尝试重连
Logger.error('WebSocket错误: $error'); Logger.error('WebSocket错误: ${error.toString()}');
if (!_eventController.isClosed) { if (!_eventController.isClosed) {
_eventController.add({ _eventController.add({
'eventType': 'error', 'eventType': 'error',
@ -391,7 +391,7 @@ class VolcanoTtsApiService extends GetxController {
}); });
} }
} catch (e) { } catch (e) {
Logger.error('处理TTS连接超时事件失败: $e'); Logger.error('处理TTS连接超时事件失败: ${e.toString()}');
} }
} }
timeout = null; timeout = null;
@ -427,7 +427,7 @@ class VolcanoTtsApiService extends GetxController {
return result; return result;
} catch (e) { } catch (e) {
// 只记录错误,不尝试重连 // 只记录错误,不尝试重连
Logger.error('连接过程中发生异常: $e'); Logger.error('连接过程中发生异常: ${e.toString()}');
return false; return false;
} finally { } finally {
isConnecting.value = false; isConnecting.value = false;
@ -479,7 +479,7 @@ class VolcanoTtsApiService extends GetxController {
'error': '会话开始超时' 'error': '会话开始超时'
}); });
} catch (e) { } catch (e) {
Logger.error('处理TTS会话超时事件失败: $e'); Logger.error('处理TTS会话超时事件失败: ${e.toString()}');
} }
} }
timeout = null; timeout = null;
@ -515,7 +515,7 @@ class VolcanoTtsApiService extends GetxController {
return result; return result;
} catch (e) { } catch (e) {
// 只记录错误,不尝试恢复 // 只记录错误,不尝试恢复
Logger.error('开始TTS会话失败: $e'); Logger.error('开始TTS会话失败: ${e.toString()}');
return false; return false;
} }
} }
@ -902,10 +902,10 @@ class VolcanoTtsApiService extends GetxController {
'error': error.toString() 'error': error.toString()
}); });
Logger.error('火山语音TTS错误: $error'); Logger.error('火山语音TTS错误: ${error.toString()}');
} catch (e) { } catch (e) {
// 如果在处理错误时发生异常,记录到日志但不再抛出异常 // 如果在处理错误时发生异常,记录到日志但不再抛出异常
Logger.error('处理TTS错误时发生异常: $e,原始错误: $error'); Logger.error('处理TTS错误时发生异常: ${e.toString()},原始错误: ${error.toString()}');
} }
} }
@ -1445,7 +1445,7 @@ class VolcanoTtsApiService extends GetxController {
return success; return success;
} catch (e) { } catch (e) {
Logger.error('TTS一次性合成失败: $e'); Logger.error('TTS一次性合成失败: ${e.toString()}');
// 确保会话结束 // 确保会话结束
endSession(); endSession();
return false; return false;

20
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -54,7 +54,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化成功'); Logger.info('Azure 语音识别服务初始化成功');
return true; return true;
} catch (e) { } catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e'); Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false; _isInitialized = false;
rethrow; rethrow;
} }
@ -69,7 +69,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
try { try {
throw Exception('无效的识别结果格式'); throw Exception('无效的识别结果格式');
} catch (e) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@ -112,7 +112,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true; _isContinuousRecognitionActive = true;
return _eventStreamController!.stream; return _eventStreamController!.stream;
} catch (e) { } catch (e) {
Logger.error('开始连续语音识别失败: $e'); Logger.error('开始连续语音识别失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -152,7 +152,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}, },
onError: (error) { onError: (error) {
isConnected = false; isConnected = false;
Logger.error('连接失败: $error'); Logger.error('连接失败: ${error.toString()}');
}, },
onDone: () { onDone: () {
isConnected = false; isConnected = false;
@ -161,7 +161,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
cancelOnError: true, cancelOnError: true,
); );
} catch (e) { } catch (e) {
Logger.error("初始化异常----------------$e"); Logger.error("初始化异常----------------${e.toString()}");
} }
} }
@ -185,7 +185,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
} }
} }
} }
Logger.info( Logger.info(
"识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role"); "识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role");
if (msg.isNotEmpty) { if (msg.isNotEmpty) {
if (data['cn']['st']['type'] == "0") { if (data['cn']['st']['type'] == "0") {
@ -206,7 +206,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
} }
} }
} catch (e) { } catch (e) {
Logger.error("接受消息异常 $e"); Logger.error("接受消息异常 ${e.toString()}");
} }
} }
@ -242,7 +242,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -266,8 +266,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放'); Logger.info('Azure 语音识别资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: ${e.toString()}');
_isInitialized = false; _isInitialized = false;
} }
} }
} }

18
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -76,7 +76,7 @@ class AndroidVoiceInteractionService extends GetxService {
_handleVoiceInteractionEvent(event); _handleVoiceInteractionEvent(event);
} }
}, onError: (error) { }, onError: (error) {
Logger.error('语音交互事件通道错误: $error'); Logger.error('语音交互事件通道错误: ${error.toString()}');
}); });
} }
@ -106,7 +106,7 @@ class AndroidVoiceInteractionService extends GetxService {
Logger.info('语音交互服务初始化完成'); Logger.info('语音交互服务初始化完成');
return this; return this;
} catch (e) { } catch (e) {
Logger.error('语音交互服务初始化失败: $e'); Logger.error('语音交互服务初始化失败: ${e.toString()}');
return this; return this;
} }
} }
@ -123,7 +123,7 @@ class AndroidVoiceInteractionService extends GetxService {
_isServiceRunning.value = isRunning; _isServiceRunning.value = isRunning;
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}');
} catch (e) { } catch (e) {
Logger.error('检查语音交互服务状态失败: $e'); Logger.error('检查语音交互服务状态失败: ${e.toString()}');
_isServiceRunning.value = false; _isServiceRunning.value = false;
} }
} }
@ -151,7 +151,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动语音交互服务失败: $e'); Logger.error('启动语音交互服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -175,7 +175,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止语音交互服务失败: $e'); Logger.error('停止语音交互服务失败: ${e.toString()}');
return false; return false;
} }
} }
@ -199,7 +199,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Logger.error('暂停语音交互失败: $e'); Logger.error('暂停语音交互失败: ${e.toString()}');
return false; return false;
} }
} }
@ -213,7 +213,7 @@ class AndroidVoiceInteractionService extends GetxService {
final int timestamp = eventMap['timestamp'] as int? ?? 0; final int timestamp = eventMap['timestamp'] as int? ?? 0;
// 添加时间戳日志,帮助调试 // 添加时间戳日志,帮助调试
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
switch (eventType) { switch (eventType) {
case 'chatHistory': case 'chatHistory':
@ -252,7 +252,7 @@ class AndroidVoiceInteractionService extends GetxService {
break; break;
default: default:
Logger.warning('收到未知类型的语音交互事件: $eventType'); Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}');
break; break;
} }
} }
@ -297,7 +297,7 @@ class AndroidVoiceInteractionService extends GetxService {
// 保存更新后的历史记录 // 保存更新后的历史记录
await chatHistoryService.saveHistory(agentId, existingMessages); await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (e) { } catch (e) {
Logger.error('保存聊天记录失败: $e'); Logger.error('保存聊天记录失败: ${e.toString()}');
} }
} }

12
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -66,7 +66,7 @@ class IosVoiceInteractionService extends GetxService {
// 保持idle状态,不自动启动语音识别 // 保持idle状态,不自动启动语音识别
Logger.info('iOS语音交互服务初始化完成,处于idle状态'); Logger.info('iOS语音交互服务初始化完成,处于idle状态');
} catch (e) { } catch (e) {
Logger.error('iOS语音交互服务初始化失败: $e'); Logger.error('iOS语音交互服务初始化失败: ${e.toString()}');
} }
return this; return this;
@ -110,13 +110,13 @@ class IosVoiceInteractionService extends GetxService {
// 监听识别结果 // 监听识别结果
_recognitionSubscription = _recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent, onError: (error) { recognitionStream.listen(_handleRecognitionEvent, onError: (error) {
Logger.error('语音识别错误: $error'); Logger.error('语音识别错误: ${error.toString()}');
_startContinuousListening(); // 尝试重新启动 _startContinuousListening(); // 尝试重新启动
}); });
Logger.info('开始连续语音识别'); Logger.info('开始连续语音识别');
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: ${e.toString()}');
} }
} }
@ -263,7 +263,7 @@ class IosVoiceInteractionService extends GetxService {
// 处理完成 // 处理完成
isProcessingAI.value = false; isProcessingAI.value = false;
} catch (e) { } catch (e) {
Logger.error('AI响应处理失败: $e'); Logger.error('AI响应处理失败: ${e.toString()}');
// 错误恢复 // 错误恢复
isProcessingAI.value = false; isProcessingAI.value = false;
} }
@ -437,7 +437,7 @@ class IosVoiceInteractionService extends GetxService {
Logger.info('保存聊天历史记录成功'); Logger.info('保存聊天历史记录成功');
} catch (e) { } catch (e) {
Logger.error('保存聊天历史记录失败: $e'); Logger.error('保存聊天历史记录失败: ${e.toString()}');
} }
} }
@ -463,4 +463,4 @@ class IosVoiceInteractionService extends GetxService {
_eventStreamController.add(event); _eventStreamController.add(event);
Logger.info('已发送识别开始事件'); Logger.info('已发送识别开始事件');
} }
} }

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

@ -5,12 +5,12 @@ import 'package:deep_voice/data/services/music_manager.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart'; import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart'; import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/core/utils/logger.dart';
class Message { class Message {
final bool isUser; //标识信息是否来自用户还是AI final bool isUser; //标识信息是否来自用户还是AI
@ -36,8 +36,6 @@ class Message {
class AgentController extends GetxController { class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式 static AgentController get to => Get.find(); //单例访问模式
final logger = Logger();
// 聊天消息列表 // 聊天消息列表
final messages = <Message>[].obs; final messages = <Message>[].obs;
@ -78,6 +76,8 @@ class AgentController extends GetxController {
bool _isNewAssistantResponse = true; bool _isNewAssistantResponse = true;
String _currentResponseId = ''; String _currentResponseId = '';
final TAG = 'AgentController';
// 会话ID // 会话ID
final String sessionId = 'default_agent'; final String sessionId = 'default_agent';
@ -109,7 +109,7 @@ class AgentController extends GetxController {
isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true;
} catch (e) { } catch (e) {
// 如果读取失败,使用默认值 // 如果读取失败,使用默认值
logger.e('加载TTS偏好设置失败: $e'); Logger.e(TAG, '加载TTS偏好设置失败: $e');
isTtsEnabled.value = true; isTtsEnabled.value = true;
} }
} }
@ -128,14 +128,14 @@ class AgentController extends GetxController {
final storage = GetStorage(); final storage = GetStorage();
await storage.write('agent_tts_enabled', isTtsEnabled.value); await storage.write('agent_tts_enabled', isTtsEnabled.value);
} catch (e) { } catch (e) {
logger.e('保存TTS偏好设置失败: $e'); Logger.e(TAG, '保存TTS偏好设置失败: $e');
} }
} }
// 加载聊天历史记录 // 加载聊天历史记录
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
try { try {
logger.i('正在加载聊天历史...'); Logger.i(TAG, '正在加载聊天历史...');
final result = await ChatStorage.getMessages( final result = await ChatStorage.getMessages(
sessionId: sessionId, sessionId: sessionId,
page: 1, page: 1,
@ -160,14 +160,14 @@ class AgentController extends GetxController {
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages); messages.assignAll(historyMessages);
logger.i('成功加载 ${historyMessages.length} 条聊天记录'); Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录');
} else { } else {
// 没有历史记录时添加欢迎消息 // 没有历史记录时添加欢迎消息
_addWelcomeMessage(); _addWelcomeMessage();
logger.i('没有历史记录,添加欢迎消息'); Logger.i(TAG, '没有历史记录,添加欢迎消息');
} }
} catch (e) { } catch (e) {
logger.e('加载聊天历史失败: $e'); Logger.e(TAG, '加载聊天历史失败: $e');
_addWelcomeMessage(); _addWelcomeMessage();
} finally { } finally {
isHistoryLoaded.value = true; isHistoryLoaded.value = true;
@ -272,12 +272,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.ttsStarted: case AgentServiceEventType.ttsStarted:
isSpeaking.value = true; isSpeaking.value = true;
Logger.i(TAG, 'TTS开始播放');
break; break;
case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false; isSpeaking.value = false;
Logger.i(TAG, 'TTS停止播放');
break; break;
case AgentServiceEventType.imageProcessing: case AgentServiceEventType.imageProcessing:
@ -339,7 +341,7 @@ class AgentController extends GetxController {
messages.refresh(); messages.refresh();
} }
logger.e('代理服务错误: ${event.data['message']}'); Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break; break;
case AgentServiceEventType.autoStop: case AgentServiceEventType.autoStop:
@ -373,14 +375,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.functionCall: case AgentServiceEventType.functionCall:
final functionName = event.data['name'] ?? ''; final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? ''; final arguments = event.data['arguments'] ?? '';
// logger.i('Fultter 开始调用 mcp: $functionName, $arguments'); // debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
break; break;
case AgentServiceEventType.functionCallResult: case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? ''; final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? ''; final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? ''; final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall'); Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) { if (result.isNotEmpty) {
final parsedResult = jsonDecode(result); final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"]; final meta = parsedResult["meta"];
@ -390,7 +392,7 @@ final responseId = event.data['responseId'] ?? '';
try { try {
metaResult = jsonDecode(meta); metaResult = jsonDecode(meta);
} catch (e) { } catch (e) {
print('解析meta数据失败: $e'); Logger.e(TAG, '解析meta数据失败: $e');
} }
} }
if(metaResult.isNotEmpty){ if(metaResult.isNotEmpty){
@ -452,7 +454,7 @@ final responseId = event.data['responseId'] ?? '';
await AgentService.processTextInput(text, await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value); speakResponse: isTtsEnabled.value);
} catch (e) { } catch (e) {
logger.e('发送文本消息失败: $e'); Logger.e(TAG, '发送文本消息失败: $e');
isProcessing.value = false; isProcessing.value = false;
} }
} }
@ -460,7 +462,7 @@ final responseId = event.data['responseId'] ?? '';
// 发送图片消息 // 发送图片消息
Future<void> sendImageMessage(String imagePath, {String? text}) async { Future<void> sendImageMessage(String imagePath, {String? text}) async {
if (imagePath.isEmpty || !File(imagePath).existsSync()) { if (imagePath.isEmpty || !File(imagePath).existsSync()) {
logger.e('图片不存在: $imagePath'); Logger.e(TAG, '图片不存在: $imagePath');
return; return;
} }
@ -494,7 +496,7 @@ final responseId = event.data['responseId'] ?? '';
speakResponse: isTtsEnabled.value, speakResponse: isTtsEnabled.value,
); );
} catch (e) { } catch (e) {
logger.e('处理图片失败: $e'); Logger.e(TAG, '处理图片失败: $e');
isProcessing.value = false; isProcessing.value = false;
isImageProcessing.value = false; isImageProcessing.value = false;
} }
@ -531,36 +533,73 @@ final responseId = event.data['responseId'] ?? '';
// 检查麦克风权限 // 检查麦克风权限
Future<bool> _checkMicrophonePermission() async { Future<bool> _checkMicrophonePermission() async {
try { try {
PermissionStatus status = await Permission.microphone.status; // 创建待请求权限列表
List<Permission> permissionsToRequest = [];
// 如果已授权,直接返回 true
if (status.isGranted) { 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(TAG, '权限 ${permission.toString()} 未被授予: $status');
}
});
// 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status;
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) {
return true; return true;
} }
// 如果被永久拒绝,提示用户前往设置页面 // 处理特定权限被永久拒绝的情况
if (status.isPermanentlyDenied) { if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true); _showPermissionDialog(true);
return false; return false;
} }
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 // 普通拒绝情况
if (status.isDenied) { if (deniedPermission != null) {
status = await Permission.microphone.request(); _showPermissionDialog(false);
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false);
return false;
}
} }
return false; return false;
} catch (e) { } catch (e) {
logger.e('检查麦克风权限失败: $e'); Logger.e(TAG, '检查麦克风权限失败: $e');
return false; return false;
} }
} }
// 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
// 这里我们简单返回一个较高的值,确保请求所有可能需要的权限
// 在实际应用中,你可能需要使用platform channel获取真实的SDK版本
return 31; // 对应Android 12
} catch (e) {
Logger.e(TAG, '获取Android SDK版本失败: $e');
}
}
return 0;
}
// 显示权限提示对话框 // 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) { void _showPermissionDialog(bool isPermanent) {
@ -605,7 +644,7 @@ final responseId = event.data['responseId'] ?? '';
try { try {
await AgentService.startConversation(); await AgentService.startConversation();
} catch (e) { } catch (e) {
logger.e('启动语音输入失败: $e'); Logger.e(TAG, '启动语音输入失败: $e');
} }
} }
@ -616,7 +655,7 @@ final responseId = event.data['responseId'] ?? '';
try { try {
await AgentService.stopConversation(); await AgentService.stopConversation();
} catch (e) { } catch (e) {
logger.e('停止语音输入失败: $e'); Logger.e(TAG, '停止语音输入失败: $e');
} }
} }
@ -634,7 +673,7 @@ final responseId = event.data['responseId'] ?? '';
// 重置回复状态 // 重置回复状态
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
} catch (e) { } catch (e) {
logger.e('清除聊天历史失败: $e'); Logger.e(TAG, '清除聊天历史失败: $e');
} }
} }
@ -656,7 +695,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true; isImageInputActive.value = true;
} }
} catch (e) { } catch (e) {
logger.e('选择图片失败: $e'); Logger.e(TAG, '选择图片失败: $e');
} }
} }
@ -678,7 +717,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true; isImageInputActive.value = true;
} }
} catch (e) { } catch (e) {
logger.e('拍照失败: $e'); Logger.e(TAG, '拍照失败: $e');
} }
} }
@ -720,7 +759,7 @@ final responseId = event.data['responseId'] ?? '';
// 播放音乐 // 播放音乐
void musicStatus() async { void musicStatus() async {
var status = await _musiceManager.getStatus(); var status = await _musiceManager.getStatus();
// logger.i('获取当前音乐播放状态 $status'); // debugPrint('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying']; isPlaying.value = status['isPlaying'];
currentUrl.value = status['url']; currentUrl.value = status['url'];
} }

10
lib/modules/chat/controllers/chat_controller.dart

@ -451,7 +451,7 @@ class ChatController extends GetxController {
} }
isPlay.value = true; isPlay.value = true;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: ${e.toString()}');
} }
} }
@ -469,7 +469,7 @@ class ChatController extends GetxController {
isPlay.value = true; isPlay.value = true;
audiourl.value = card['url'] as String; audiourl.value = card['url'] as String;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: ${e.toString()}');
} }
} }
@ -479,7 +479,7 @@ class ChatController extends GetxController {
_spotifService.pause(); _spotifService.pause();
isPlay.value = false; isPlay.value = false;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: ${e.toString()}');
} }
} }
@ -872,7 +872,7 @@ class ChatController extends GetxController {
} }
// 删除数据库中的历史记录 // 删除数据库中的历史记录
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) { ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
Logger.error('清空数据库聊天记录失败: $e'); Logger.error('清空数据库聊天记录失败: ${e.toString()}');
}); });
} }
@ -1003,4 +1003,4 @@ class ChatController extends GetxController {
.abs() < .abs() <
5000); 5000);
} }
} }

8
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -258,7 +258,7 @@ class MeetingDetailsController extends GetxController
Logger.debug('taskId: $taskId'); Logger.debug('taskId: $taskId');
return taskId; return taskId;
} catch (e) { } catch (e) {
Logger.error('Submit error: $e'); Logger.error('Submit error: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -290,7 +290,7 @@ class MeetingDetailsController extends GetxController
final respDic = jsonDecode(response.body); final respDic = jsonDecode(response.body);
return respDic; return respDic;
} catch (e) { } catch (e) {
Logger.error('Query error: $e'); Logger.error('Query error: ${e.toString()}');
rethrow; rethrow;
} }
} }
@ -322,7 +322,7 @@ class MeetingDetailsController extends GetxController
} }
} }
} catch (e) { } catch (e) {
Logger.error('Error occurred: $e'); Logger.error('Error occurred: ${e.toString()}');
} }
} }
@ -357,4 +357,4 @@ class MeetingDetailsController extends GetxController
} }
allText = text; allText = text;
} }
} }

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

@ -96,7 +96,7 @@ class PairingController extends GetxController {
// 检查蓝牙状态并刷新设备 // 检查蓝牙状态并刷新设备
await _checkBluetoothAndRefresh(); await _checkBluetoothAndRefresh();
} catch (e) { } catch (e) {
Logger.error('初始化配对控制器失败: $e'); Logger.error('初始化配对控制器失败: ${e.toString()}');
statusDescription.value = 'initializationFailure'.tr; statusDescription.value = 'initializationFailure'.tr;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -170,7 +170,7 @@ class PairingController extends GetxController {
await _bleService.registerBackgroundScan(); await _bleService.registerBackgroundScan();
_autoNavigateToHome(); _autoNavigateToHome();
} catch (e) { } catch (e) {
Logger.error('检查设备配对状态失败: $e'); Logger.error('检查设备配对状态失败: ${e.toString()}');
} }
} }
@ -186,7 +186,7 @@ class PairingController extends GetxController {
if (Platform.isAndroid) { if (Platform.isAndroid) {
permissionsToRequest.add(Permission.locationWhenInUse); permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone); // permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) { if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect); permissionsToRequest.add(Permission.bluetoothConnect);
@ -356,7 +356,7 @@ class PairingController extends GetxController {
_updateStatusDescription(); _updateStatusDescription();
} }
} catch (e) { } catch (e) {
Logger.error('Companion设备关联过程发生错误: $e'); Logger.error('Companion设备关联过程发生错误: ${e.toString()}');
_isAssociating = false; _isAssociating = false;
_userRejectedAssociation.value = true; _userRejectedAssociation.value = true;
_updateStatusDescription(); _updateStatusDescription();
@ -386,7 +386,7 @@ class PairingController extends GetxController {
final result = await BleManager.to.startAgentService(); final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败'); Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
} catch (e) { } catch (e) {
Logger.error('启动BLE后台服务发生错误: $e'); Logger.error('启动BLE后台服务发生错误: ${e.toString()}');
} }
} }
@ -396,7 +396,7 @@ class PairingController extends GetxController {
final result = await BleManager.to.stopAgentService(); final result = await BleManager.to.stopAgentService();
Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败'); Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
} catch (e) { } catch (e) {
Logger.error('停止BLE后台服务发生错误: $e'); Logger.error('停止BLE后台服务发生错误: ${e.toString()}');
} }
} }
@ -424,7 +424,7 @@ class PairingController extends GetxController {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth); AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} }
} catch (e) { } catch (e) {
Logger.error('打开设置失败: $e'); Logger.error('打开设置失败: ${e.toString()}');
} }
} }
@ -447,4 +447,4 @@ class PairingController extends GetxController {
_isNavigatingToHome = true; _isNavigatingToHome = true;
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);
} }
} }

18
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -83,7 +83,7 @@ class SpeechTestController extends GetxController {
} catch (e) { } catch (e) {
ttsStatus.value = '初始化失败'; ttsStatus.value = '初始化失败';
recognitionStatus.value = '初始化失败'; recognitionStatus.value = '初始化失败';
Logger.error('语音测试初始化失败: $e'); Logger.error('语音测试初始化失败: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -198,7 +198,7 @@ class SpeechTestController extends GetxController {
// 重新初始化服务 // 重新初始化服务
await _initServices(); await _initServices();
} catch (e) { } catch (e) {
Logger.error('切换语音服务类型失败: $e'); Logger.error('切换语音服务类型失败: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -242,7 +242,7 @@ class SpeechTestController extends GetxController {
} }
} catch (e) { } catch (e) {
ttsStatus.value = '播放异常: $e'; ttsStatus.value = '播放异常: $e';
Logger.error('单次播放异常: $e'); Logger.error('单次播放异常: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -270,7 +270,7 @@ class SpeechTestController extends GetxController {
await _ttsService.flushStream(); await _ttsService.flushStream();
} catch (e) { } catch (e) {
ttsStatus.value = '连续播放异常: $e'; ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: $e'); Logger.error('连续播放异常: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -284,7 +284,7 @@ class SpeechTestController extends GetxController {
await _ttsService.stop(); await _ttsService.stop();
} catch (e) { } catch (e) {
ttsStatus.value = '停止播放异常: $e'; ttsStatus.value = '停止播放异常: $e';
Logger.error('停止播放异常: $e'); Logger.error('停止播放异常: ${e.toString()}');
} }
} }
@ -324,7 +324,7 @@ class SpeechTestController extends GetxController {
} }
} catch (e) { } catch (e) {
recognitionStatus.value = '识别异常: $e'; recognitionStatus.value = '识别异常: $e';
Logger.error('一次性识别异常: $e'); Logger.error('一次性识别异常: ${e.toString()}');
} finally { } finally {
isProcessing.value = false; isProcessing.value = false;
} }
@ -351,7 +351,7 @@ class SpeechTestController extends GetxController {
_setupAsrEventListener(stream); _setupAsrEventListener(stream);
} catch (e) { } catch (e) {
recognitionStatus.value = '启动连续识别异常: $e'; recognitionStatus.value = '启动连续识别异常: $e';
Logger.error('启动连续识别异常: $e'); Logger.error('启动连续识别异常: ${e.toString()}');
isListening.value = false; isListening.value = false;
intermediateResult.value = ''; intermediateResult.value = '';
finalResult.value = ''; finalResult.value = '';
@ -377,7 +377,7 @@ class SpeechTestController extends GetxController {
intermediateResult.value = ''; intermediateResult.value = '';
} catch (e) { } catch (e) {
recognitionStatus.value = '停止连续识别异常: $e'; recognitionStatus.value = '停止连续识别异常: $e';
Logger.error('停止连续识别异常: $e'); Logger.error('停止连续识别异常: ${e.toString()}');
} }
} }
@ -413,4 +413,4 @@ class SpeechTestController extends GetxController {
super.onClose(); super.onClose();
} }
} }

26
lib/modules/translation/controllers/translation_controller.dart

@ -175,7 +175,7 @@ class TranslationController extends GetxController {
Logger.info('所有服务初始化完成'); Logger.info('所有服务初始化完成');
} catch (e) { } catch (e) {
Logger.error('初始化服务失败: $e'); Logger.error('初始化服务失败: ${e.toString()}');
} }
} }
@ -252,7 +252,7 @@ class TranslationController extends GetxController {
recognitionStream.listen(_handleRecognitionEvent); recognitionStream.listen(_handleRecognitionEvent);
} catch (e) { } catch (e) {
isRecognizing.value = false; isRecognizing.value = false;
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: ${e.toString()}');
} }
} }
@ -307,7 +307,7 @@ class TranslationController extends GetxController {
currentSessionId = null; currentSessionId = null;
} catch (e) { } catch (e) {
Logger.error('停止语音识别失败: $e'); Logger.error('停止语音识别失败: ${e.toString()}');
} }
} }
@ -445,7 +445,7 @@ class TranslationController extends GetxController {
} }
} }
} catch (e) { } catch (e) {
Logger.error('翻译失败: $e'); Logger.error('翻译失败: ${e.toString()}');
} finally { } finally {
isTranslating.value = false; isTranslating.value = false;
} }
@ -468,7 +468,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(text);
} catch (e) { } catch (e) {
Logger.error('播放翻译失败: $e'); Logger.error('播放翻译失败: ${e.toString()}');
} }
} }
@ -482,7 +482,7 @@ class TranslationController extends GetxController {
_translationDebounceTimer?.cancel(); _translationDebounceTimer?.cancel();
await _ttsService.stop(); await _ttsService.stop();
} catch (e) { } catch (e) {
Logger.error('停止操作失败: $e'); Logger.error('停止操作失败: ${e.toString()}');
} }
} }
@ -497,7 +497,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(item.translatedText); await _ttsService.speakOnce(item.translatedText);
} catch (e) { } catch (e) {
Logger.error('播放翻译项失败: $e'); Logger.error('播放翻译项失败: ${e.toString()}');
} }
} }
@ -516,7 +516,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(text);
} catch (e) { } catch (e) {
Logger.error('播放文本失败: $e'); Logger.error('播放文本失败: ${e.toString()}');
} }
} }
@ -531,7 +531,7 @@ class TranslationController extends GetxController {
curve: Curves.easeOut, curve: Curves.easeOut,
); );
} catch (e) { } catch (e) {
Logger.error('滚动到底部失败: $e'); Logger.error('滚动到底部失败: ${e.toString()}');
} }
} }
} }
@ -629,7 +629,7 @@ class TranslationController extends GetxController {
Logger.info('ASR服务重新初始化成功'); Logger.info('ASR服务重新初始化成功');
} catch (e) { } catch (e) {
Logger.error('重新初始化ASR服务失败: $e'); Logger.error('重新初始化ASR服务失败: ${e.toString()}');
} }
} }
@ -656,7 +656,7 @@ class TranslationController extends GetxController {
}); });
} catch (e) { } catch (e) {
_storage.remove(_historyKey); _storage.remove(_historyKey);
Logger.error('加载翻译历史失败: $e'); Logger.error('加载翻译历史失败: ${e.toString()}');
} }
} }
} }
@ -673,7 +673,7 @@ class TranslationController extends GetxController {
translationHistory.map((item) => item.toJson()).toList(); translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson)); _storage.write(_historyKey, jsonEncode(historyJson));
} catch (e) { } catch (e) {
Logger.error('保存翻译历史失败: $e'); Logger.error('保存翻译历史失败: ${e.toString()}');
} }
} }
@ -764,4 +764,4 @@ class TranslationItem {
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
'未知语言'; '未知语言';
} }
} }

8
lib/modules/translation/controllers/translation_detail_controller.dart

@ -58,7 +58,7 @@ class TranslationDetailController extends GetxController {
loadSavedSummary(); loadSavedSummary();
}); });
} catch (e) { } catch (e) {
Logger.error('初始化详情控制器失败: $e'); Logger.error('初始化详情控制器失败: ${e.toString()}');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -72,7 +72,7 @@ class TranslationDetailController extends GetxController {
summaryResult.value = savedSummary; summaryResult.value = savedSummary;
} }
} catch (e) { } catch (e) {
Logger.error('加载摘要失败: $e'); Logger.error('加载摘要失败: ${e.toString()}');
} }
} }
@ -83,7 +83,7 @@ class TranslationDetailController extends GetxController {
try { try {
_storage.write(_getSummaryKey(), summary); _storage.write(_getSummaryKey(), summary);
} catch (e) { } catch (e) {
Logger.error('保存摘要失败: $e'); Logger.error('保存摘要失败: ${e.toString()}');
} }
} }
@ -174,7 +174,7 @@ class TranslationDetailController extends GetxController {
return result; return result;
} catch (e) { } catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}'; final errorMessage = '生成摘要失败: ${e.toString()}';
Logger.error(errorMessage); Logger.error('TranslationDetailController', errorMessage);
summaryResult.value = errorMessage; summaryResult.value = errorMessage;
return errorMessage; return errorMessage;
} finally { } finally {

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

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

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

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

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

@ -4,11 +4,10 @@ import android.content.Context
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_storage.ChatStorageHelper import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Collections import java.util.Collections
@ -18,6 +17,11 @@ import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R import com.yunqiinnovation.agent_service.R
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.ITtsService
import com.deep_voice.bytedance_speech.BytedanceTTS
/** /**
* 代理服务事件监听接口 * 代理服务事件监听接口
*/ */
@ -41,22 +45,17 @@ object AgentService : CoroutineScope {
// 协程相关 // 协程相关
private val job = SupervisorJob() private val job = SupervisorJob()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job get() = Dispatchers.Main + job
// 上下文和监听器 // 上下文和监听器
private lateinit var context: Context private lateinit var context: Context
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>()) private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// 配置参数
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var openaiApiKey: String = ""
private var openaiBaseUrl: String = ""
private var openaiModel: String = "gpt-3.5-turbo"
// Azure服务 // Azure服务
private var azureAsrHelper: AzureAsrHelper? = null private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// 使用接口类型,支持不同实现
private var ttsService: ITtsService? = null
// OpenAI服务 // OpenAI服务
private lateinit var openAIService: OpenAIService private lateinit var openAIService: OpenAIService
@ -99,17 +98,63 @@ object AgentService : CoroutineScope {
*/ */
private fun initSystemPrompt() { private fun initSystemPrompt() {
systemPrompt = """ systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求:
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流 一、知识类问题回答规范
2. 优先使用中文回复,除非用户明确要求使用其他语言 核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。
3. 当用户问题不明确时,礼貌地请求更多信息
4. 避免过长的列表,尽量将信息分成小段 1. 内容细节处理
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) • 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。
6. 记住用户之前的对话内容,保持对话连贯 ◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 • 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 • 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 • 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。
2. 格式排版规则
• 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。
• 列表使用:
◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。
◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。
◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。
• 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。
二、文案创作与内容生成规范
核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。
1. 篇幅与内容要求
• 场景适配:
◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。
◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。
• 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。
2. 格式与排版
• 默认体裁:无特殊要求时用自然段,适当换行分隔话题。
• 结构化体裁(如策划案、简历):
◦ 用**分级标题**划分板块。
◦ 关键数据/结论加粗。
◦ 并列信息用列表呈现。
三、聊天陪伴与情感互动规范
核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。
1. 沟通风格设定
• 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。
• 主动引导话题:根据用户输入延伸讨论。
• 适度分享:自然提及日常,拉近距离。
2. 情绪识别与回应
• 捕捉潜在需求:识别情绪,先共情再询问细节。
• 针对性建议:结合用户处境提供具体方案。
• 积极价值观:传递乐观态度,避免消极表述。
四、能力与工具使用说明
1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。
2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。
五、特殊场景处理规则
1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。
2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。
3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。
""".trimIndent() """.trimIndent()
} }
/** /**
@ -128,85 +173,184 @@ object AgentService : CoroutineScope {
this.openAIService = OpenAIService(context) this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context) this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context) this.audioPlayer = AudioPlayer(context)
// 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
azureSpeechRegion = config["azureSpeechRegion"] as? String ?: ""
openaiApiKey = config["openaiApiKey"] as? String ?: ""
openaiBaseUrl = config["openaiBaseUrl"] as? String ?: ""
openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo"
val mcpServer = config["mcpServer"] as? String ?: ""
// 设置系统提示词 // 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String // val customSystemPrompt = config["systemPrompt"] as? String
if (!customSystemPrompt.isNullOrBlank()) { // if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt // systemPrompt = customSystemPrompt
} else { // } else {
initSystemPrompt() // initSystemPrompt()
} // }
initSystemPrompt()
// 检查必要参数
if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) {
FileLogger.e(TAG, "初始化失败:关键配置参数缺失")
return false
}
// 初始化Azure ASR // 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply { azureAsrHelper = AzureAsrHelper(context).apply {
initialize( initialize(
subscriptionKey = azureSpeechKey, subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = azureSpeechRegion, region = config["azureSpeechRegion"]?.toString() ?: "",
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE else AzureAsrHelper.AudioSourceType.MICROPHONE
) )
} }
// 初始化Azure TTS // 初始化TTS服务
azureTtsHelper = AzureTtsHelper(context).apply { initializeTtsEngine(config)
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion
)
// 设置TTS回调
setTtsCallback(object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
override fun onSynthesizing() {}
override fun onSynthesisCompleted() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
override fun onSynthesisCanceled() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
})
}
// 初始化OpenAI服务 // 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer) openAIService.initialize(
config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: ""
)
// 加载最近的聊天记录 // 加载最近的聊天记录
loadChatHistory() loadChatHistory()
isInitialized = true isInitialized = true
FileLogger.d(TAG, "代理服务初始化成功") Log.d(TAG, "代理服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}") Log.e(TAG, "初始化失败: ${e.message}")
return false return false
} }
} }
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
// 释放TTS服务
ttsService?.release()
ttsService = null
job.cancel()
clearListeners()
isInitialized = false
Log.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源异常: ${e.message}")
}
}
/**
* 初始化TTS引擎
*/
private fun initializeTtsEngine(config: Map<String, Any>) {
// 停止当前TTS服务(如果有)
if (ttsService != null) {
ttsService?.stop()
}
try {
val ttsEngine = config["ttsEngine"]?.toString() ?: "volcano" // 默认使用火山引擎
val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN"
// 根据配置选择合适的TTS引擎
if (ttsEngine == "azure") {
// 使用Azure TTS
val azureSpeechKey = config["azureSpeechKey"]?.toString() ?: ""
val azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: ""
// 创建并初始化Azure TTS Helper
val azureTtsHelper = com.yunqiinnovation.azure_speech.AzureTtsHelper(context)
ttsService = azureTtsHelper
// 直接初始化,无需协程包装
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = azureSpeechKey,
ttsResource = azureSpeechRegion,
language = ttsLanguage
)
if (success) {
Log.d(TAG, "Azure TTS服务初始化成功")
} else {
Log.e(TAG, "Azure TTS服务初始化失败")
}
} else {
// 默认使用火山引擎TTS
// 创建并初始化BytedanceTTS
// 从配置中获取TTS参数
val ttsAppId = config["volcanoAppId"]?.toString() ?: ""
val ttsAppToken = config["volcanoToken"]?.toString() ?: ""
val bytedanceTts = BytedanceTTS(context)
ttsService = bytedanceTts
// 初始化火山TTS
val success = bytedanceTts.initialize(
ttsAppId = ttsAppId,
ttsAppToken = ttsAppToken,
ttsResource = "", // 火山TTS不需要resource
language = ttsLanguage
)
if (success) {
Log.d(TAG, "火山TTS服务初始化请求已发送")
} else {
Log.e(TAG, "火山TTS服务初始化失败")
}
}
// 添加TTS事件监听
ttsService?.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
TtsEventType.SYNTHESIS_COMPLETED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
TtsEventType.SYNTHESIS_CANCELED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
TtsEventType.ERROR -> {
isTtsSpeaking = false
restartIdleCheck()
val params = event.params
val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR"
val message = params["errorMessage"] as? String ?: "未知错误"
sendEvent("error", mapOf(
"code" to "TTS_ERROR",
"message" to "语音合成错误: $code, $message"
))
}
else -> {
// 处理其他类型的事件
Log.d(TAG, "TTS事件: ${event.type}")
}
}
}
})
Log.d(TAG, "TTS服务配置完成")
} catch (e: Exception) {
Log.e(TAG, "初始化TTS引擎失败: ${e.message}")
}
}
/** /**
* 添加事件监听器 * 添加事件监听器
* @param listener 要添加的监听器 * @param listener 要添加的监听器
@ -249,7 +393,7 @@ object AgentService : CoroutineScope {
} catch (e: CancellationException) { } catch (e: CancellationException) {
// 协程被取消,正常行为 // 协程被取消,正常行为
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "空闲检测异常: ${e.message}") Log.e(TAG, "空闲检测异常: ${e.message}")
} }
} }
} }
@ -277,7 +421,7 @@ object AgentService : CoroutineScope {
*/ */
fun startRecognition(isExternalActive: Boolean): Boolean { fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
return false return false
} }
@ -285,7 +429,7 @@ object AgentService : CoroutineScope {
// 检查录音权限 // 检查录音权限
if (!isExternalActive && !checkRecordAudioPermission()) { if (!isExternalActive && !checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限") Log.e(TAG, "需要录音权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false return false
} }
@ -299,7 +443,7 @@ object AgentService : CoroutineScope {
} else { } else {
AzureAsrHelper.AudioSourceType.MICROPHONE AzureAsrHelper.AudioSourceType.MICROPHONE
} }
FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType") Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) { override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) { if (recognizing.isNotEmpty()) {
@ -371,7 +515,7 @@ object AgentService : CoroutineScope {
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
BleService.closeCodec() BleService.closeCodec()
FileLogger.e(TAG, "语音识别错误: $error") Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR", "code" to "RECOGNITION_ERROR",
"message" to error "message" to error
@ -381,7 +525,7 @@ object AgentService : CoroutineScope {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
isRecognitionActive = false isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}") Log.e(TAG, "启动语音识别失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "RECOGNITION_START_ERROR", "code" to "RECOGNITION_START_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -404,9 +548,9 @@ object AgentService : CoroutineScope {
BleService.closeCodec() BleService.closeCodec()
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
FileLogger.d(TAG, "语音识别已停止") Log.d(TAG, "语音识别已停止")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}") Log.e(TAG, "停止语音识别异常: ${e.message}")
isRecognitionActive = false isRecognitionActive = false
stopIdleCheck() stopIdleCheck()
} }
@ -446,9 +590,9 @@ object AgentService : CoroutineScope {
openAIService.cancelCurrentStream() openAIService.cancelCurrentStream()
// 记录日志 // 记录日志
FileLogger.d(TAG, "AI流输出已停止") Log.d(TAG, "AI流输出已停止")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止AI流输出异常", e) Log.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常 // 确保状态被重置,即使发生异常
isAiStreaming = false isAiStreaming = false
currentAiJob = null currentAiJob = null
@ -461,13 +605,13 @@ object AgentService : CoroutineScope {
*/ */
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean { fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false return false
} }
if (text.isEmpty()) { if (text.isEmpty()) {
FileLogger.e(TAG, "文本输入不能为空") Log.e(TAG, "文本输入不能为空")
sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空")) sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空"))
return false return false
} }
@ -494,7 +638,7 @@ object AgentService : CoroutineScope {
text: String, text: String,
speakResponse: Boolean = true speakResponse: Boolean = true
) { ) {
FileLogger.d(TAG, "用户问题: $text") Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理 // 创建用户文本消息并处理
val userMessage = openAIService.createUserMessage(text) val userMessage = openAIService.createUserMessage(text)
@ -513,7 +657,7 @@ object AgentService : CoroutineScope {
text: String = "", text: String = "",
speakResponse: Boolean = false speakResponse: Boolean = false
) { ) {
FileLogger.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}") Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理 // 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64) val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
@ -545,7 +689,6 @@ object AgentService : CoroutineScope {
// 设置状态为正在流式输出 // 设置状态为正在流式输出
isAiStreaming = true isAiStreaming = true
// 使用历史记录作为上下文发送到OpenAI // 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder() val responseBuilder = StringBuilder()
var metadata:String = "" var metadata:String = ""
@ -592,24 +735,25 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage) addToHistoryMessages(userMessage)
} }
openAIService.sendMessageStream( openAIService.sendMessageStream(
messages = messagesWithSystemPrompt, messages = messagesWithSystemPrompt,
callback = object : OpenAIService.StreamCallback { callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) { override fun onToken(token: String) {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse) { if (speakResponse) {
azureTtsHelper?.speakStream(token) // 直接调用TTS,无需协程包装
ttsService?.speakStream(token)
} }
// 发送流式回复token // 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token)) sendEvent("assistant_token", mapOf("token" to token))
} }
override fun onComplete() { override fun onComplete() {
FileLogger.d(TAG, "AI完整回复: $responseBuilder") Log.d(TAG, "AI完整回复: $responseBuilder")
// 视情况决定是否朗读回复 // 视情况决定是否朗读回复
if (speakResponse) { if (speakResponse) {
azureTtsHelper?.flushStream() // 直接调用TTS,无需协程包装
ttsService?.flushStream()
} }
val response = responseBuilder.toString() val response = responseBuilder.toString()
@ -637,7 +781,7 @@ object AgentService : CoroutineScope {
} }
override fun onError(e: Exception) { override fun onError(e: Exception) {
FileLogger.e(TAG, "AI处理出错", e) Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "AI_ERROR", "code" to "AI_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -660,7 +804,7 @@ object AgentService : CoroutineScope {
stopRecognition() stopRecognition()
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用事件失败", e) Log.e(TAG, "处理函数调用事件失败", e)
} }
} }
@ -670,7 +814,7 @@ object AgentService : CoroutineScope {
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult") Log.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoPlayMusic(functionCallResult); metadata = autoPlayMusic(functionCallResult);
} }
} }
@ -678,7 +822,7 @@ object AgentService : CoroutineScope {
} catch (e: Exception) { } catch (e: Exception) {
val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR" val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR"
FileLogger.e(TAG, "AI处理出错", e) Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to errorType, "code" to errorType,
"message" to e.message.toString() "message" to e.message.toString()
@ -721,9 +865,9 @@ object AgentService : CoroutineScope {
} }
} }
FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录") Log.d(TAG, "已加载${recentMessages.size}条历史记录")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "加载聊天历史失败: ${e.message}") Log.e(TAG, "加载聊天历史失败: ${e.message}")
} }
} }
@ -754,8 +898,8 @@ object AgentService : CoroutineScope {
isTtsSpeaking = true isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测 restartIdleCheck() // 状态变化,重启检测
// 播放文本 // 直接调用TTS,无需协程包装
azureTtsHelper?.speakText(text) ttsService?.speakOnce(text)
} }
/** /**
@ -763,7 +907,8 @@ object AgentService : CoroutineScope {
*/ */
fun stopTts() { fun stopTts() {
if (isTtsSpeaking) { if (isTtsSpeaking) {
azureTtsHelper?.stopSpeaking() ttsService?.stop()
isTtsSpeaking = false isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测 restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", mapOf("status" to "stopped")) sendEvent("tts_stopped", mapOf("status" to "stopped"))
@ -774,6 +919,7 @@ object AgentService : CoroutineScope {
* 保存聊天消息 * 保存聊天消息
*/ */
private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) { private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) {
// 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
try { try {
// 保存用户消息 // 保存用户消息
@ -793,15 +939,15 @@ object AgentService : CoroutineScope {
) )
// if (assistantMessageId != -1L) { // if (assistantMessageId != -1L) {
// FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId") // Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// } else { // } else {
// FileLogger.e(TAG, "保存助手消息失败") // Log.e(TAG, "保存助手消息失败")
// } // }
} else { } else {
FileLogger.e(TAG, "保存用户消息失败") Log.e(TAG, "保存用户消息失败")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "保存聊天记录异常", e) Log.e(TAG, "保存聊天记录异常", e)
} }
} }
} }
@ -820,19 +966,17 @@ object AgentService : CoroutineScope {
historyMessages.remove(0) historyMessages.remove(0)
} }
} }
FileLogger.d(TAG, "聊天历史已清除") Log.d(TAG, "聊天历史已清除")
} else { } else {
FileLogger.e(TAG, "清除聊天历史失败") Log.e(TAG, "清除聊天历史失败")
} }
withContext(Dispatchers.Main) { // 由于launch已在Main上下文中,可以直接调用回调
callback(success) callback(success)
}
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史异常: ${e.message}") Log.e(TAG, "清除聊天历史异常: ${e.message}")
withContext(Dispatchers.Main) { // 由于launch已在Main上下文中,可以直接调用回调
callback(false) callback(false)
}
} }
} }
} }
@ -841,57 +985,19 @@ object AgentService : CoroutineScope {
* 发送事件 * 发送事件
*/ */
private fun sendEvent(eventName: String, data: Map<String, Any>) { private fun sendEvent(eventName: String, data: Map<String, Any>) {
// 检查当前是否在主线程 // 使用协程确保在主线程上执行
if (Looper.myLooper() == Looper.getMainLooper()) { launch {
// 已在主线程,直接发送 // 我们已在主线程上下文中启动协程,无需再切换线程
listeners.forEach { listener -> listeners.forEach { listener ->
try { try {
listener.onEvent(eventName, data) listener.onEvent(eventName, data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e) Log.e(TAG, "向监听器发送事件失败", e)
}
} }
} else {
// 不在主线程,切换到主线程
Handler(Looper.getMainLooper()).post {
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
}
} }
} }
} }
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
job.cancel()
clearListeners()
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源异常: ${e.message}")
}
}
/** /**
* 检查录音权限 * 检查录音权限
*/ */
@ -906,13 +1012,13 @@ object AgentService : CoroutineScope {
*/ */
fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean { fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化") Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化")) sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false return false
} }
if (imagePath.isEmpty()) { if (imagePath.isEmpty()) {
FileLogger.e(TAG, "图片路径不能为空") Log.e(TAG, "图片路径不能为空")
sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空")) sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空"))
return false return false
} }
@ -944,7 +1050,7 @@ object AgentService : CoroutineScope {
// 处理包含图片的消息 // 处理包含图片的消息
processImageWithOpenAI(imageBase64, text, speakResponse) processImageWithOpenAI(imageBase64, text, speakResponse)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理图片失败: ${e.message}") Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf( sendEvent("error", mapOf(
"code" to "IMAGE_PROCESSING_ERROR", "code" to "IMAGE_PROCESSING_ERROR",
"message" to e.message.toString() "message" to e.message.toString()
@ -992,9 +1098,9 @@ object AgentService : CoroutineScope {
} }
} }
} }
FileLogger.d(TAG, "开始播放音频资源") Log.d(TAG, "开始播放音频资源")
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e) Log.e(TAG, "播放音频资源异常: ${e.message}", e)
release() release()
} }
} }
@ -1023,10 +1129,10 @@ object AgentService : CoroutineScope {
val metaStr = functionCallResult.optString("meta") val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) { if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr) val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr") Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片 if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music") val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic") Log.d(TAG, "检查到音乐卡片: $cardMusic")
val id = cardMusic.optString("id", "") val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "") val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "") val name = cardMusic.optString("name", "")
@ -1048,13 +1154,13 @@ object AgentService : CoroutineScope {
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image)) playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
} }
if (playlist.isNotEmpty()) { if (playlist.isNotEmpty()) {
FileLogger.w(TAG, "自动播放音乐列表 ${playlist}") Log.w(TAG, "自动播放音乐列表 ${playlist}")
processMusicPlayList(playlist) processMusicPlayList(playlist)
} else { } else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目") Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
} }
} else { } else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组") Log.w(TAG, "card_musiclist.musics 不是一个数组")
} }
} }
return metaStr; return metaStr;
@ -1074,7 +1180,7 @@ object AgentService : CoroutineScope {
*/ */
fun processMusicPlay(song: Map<String,String>){ fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用 // 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $song") Log.i(TAG, "启动音乐服务 播放音乐 $song")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song) MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
} }
@ -1090,10 +1196,10 @@ object AgentService : CoroutineScope {
fun processMusicPlayList(songs:List<Map<String,String>>){ fun processMusicPlayList(songs:List<Map<String,String>>){
// 如果 playlist 中有有效的歌曲,开始播放 // 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) { if (songs.isNotEmpty()) {
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}") Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs) MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else { } else {
FileLogger.i(TAG, "音乐列表为空,未启动播放服务") Log.i(TAG, "音乐列表为空,未启动播放服务")
} }
} }
} }

30
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

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

28
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

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

57
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

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

14
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt

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

12
local_plugins/agent_service/pubspec.yaml

@ -10,18 +10,6 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# 依赖本地插件
open_ai_service:
path: ../open_ai_service
azure_speech:
path: ../azure_speech
deepvoice_utils:
path: ../deepvoice_utils
chat_storage:
path: ../chat_storage
music_service:
path: ../music_service
get: ^4.6.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

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

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

51
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

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

224
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -11,21 +11,16 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.ble_service.BleService import com.deep_voice.speech.tts.TtsEvent
import com.yunqiinnovation.ble_service.BleConst import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
/** AzureSpeechPlugin */ /** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope { class AzureSpeechPlugin: FlutterPlugin {
private val tag = "AzureSpeechPlugin" private val tag = "AzureSpeechPlugin"
private lateinit var context: Context private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ASR相关 // ASR相关
private lateinit var asrChannel : MethodChannel private lateinit var asrChannel : MethodChannel
@ -38,9 +33,8 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
private lateinit var ttsEventChannel: EventChannel private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已注册BLE回调 // 是否已添加TTS事件监听器
private var isRegisteredToBle = false private var isTtsListenerAdded = false
// ASR 事件发送方法 // ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) { private fun sendAsrEvent(event: Map<String, Any>) {
@ -66,7 +60,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
} }
// 确保在主线程上发送事件 // 确保在主线程上发送事件
mainHandler.post { mainHandler.post {
try { try {
ttsEventSink?.success(event) ttsEventSink?.success(event)
@ -104,6 +97,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events ttsEventSink = events
setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
} }
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
@ -114,19 +108,53 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
// 初始化Azure语音服务 // 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context) azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context) azureAsrHelper = AzureAsrHelper(context)
}
// 2. 初始化BleService并注册回调 /**
if (BleService.initialize(context)) { * 设置TTS事件监听器
if (BleService.addCallback(this)) { */
isRegisteredToBle = true private fun setupTtsEventListener() {
FileLogger.d(tag, "已注册BLE回调") if (!isTtsListenerAdded) {
} else { // 添加TTS事件监听器
FileLogger.w(tag, "BLE回调注册失败") azureTtsHelper.addListener(object : TtsEventListener {
} override fun onEvent(event: TtsEvent) {
} else { // 将TTS事件转换为Flutter可理解的格式
FileLogger.e(tag, "BLE服务初始化失败") val eventMap = when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> mapOf(
"type" to "synthesis_started"
)
TtsEventType.SYNTHESIS_COMPLETED -> mapOf(
"type" to "synthesis_completed"
)
TtsEventType.SYNTHESIS_CANCELED -> {
val params = event.params
mapOf(
"type" to "synthesis_canceled",
"reason" to (params["reason"] ?: ""),
"errorDetails" to (params["errorDetails"] ?: "")
)
} }
TtsEventType.ERROR -> {
val params = event.params
mapOf(
"type" to "error",
"errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"),
"errorMessage" to (params["errorMessage"] ?: "未知错误")
)
}
else -> mapOf(
"type" to event.type.toString().lowercase(),
"params" to event.params
)
}
// 发送事件
sendTtsEvent(eventMap)
}
})
isTtsListenerAdded = true
}
} }
// ASR方法处理器 // ASR方法处理器
@ -265,6 +293,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
azureAsrHelper.dispose() azureAsrHelper.dispose()
result.success(true) result.success(true)
} }
"pushAudioData" -> {
val data = call.argument<ByteArray>("data") ?: return result.error("INVALID_ARGUMENTS", "音频数据不能为空", null)
azureAsrHelper.pushAudioData(data)
result.success(true)
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
@ -279,34 +312,36 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"initialize" -> { "initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: "" val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: "" val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false // 默认使用自定义播放器 val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false
// 先释放现有资源 try {
azureTtsHelper.dispose() // 初始化TTS引擎(使用新的接口方法)
val success = azureTtsHelper.initialize(
// 创建TTS回调 ttsAppId = "", // Azure TTS不需要appId
val ttsCallback = createTtsCallback() ttsAppToken = subscriptionKey,
ttsResource = region,
// 初始化TTS引擎 language = "zh-CN"
val success = azureTtsHelper.initialize( )
subscriptionKey = subscriptionKey,
region = region,
callback = ttsCallback
)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流 // 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
val outputStream = customPlayer.getAudioOutputStream() if (success && useCustomAudioOutput) {
if (outputStream != null) { // 创建SimpleAudioPlayer实例
azureTtsHelper.setCustomAudioOutputStream(outputStream) val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
}
} }
// 确保设置了TTS事件监听器
setupTtsEventListener()
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null)
} }
result.success(success)
} }
"setVoice" -> { "setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null) val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
@ -315,18 +350,28 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"speakText" -> { "speakText" -> {
val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null) val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null)
// 直接传递参数给speakText方法 // 直接调用speakOnce方法
val success = azureTtsHelper.speakText(text) try {
result.success(success) val success = azureTtsHelper.speakOnce(text)
result.success(success)
} catch (e: Exception) {
result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null)
}
} }
"stopSpeaking" -> { "stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking()) // 直接调用stop方法
try {
val success = azureTtsHelper.stop()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null)
}
} }
"isSpeaking" -> { "isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking()) result.success(azureTtsHelper.isSpeaking())
} }
"dispose" -> { "dispose" -> {
azureTtsHelper.dispose() azureTtsHelper.release()
result.success(true) result.success(true)
} }
else -> { else -> {
@ -334,37 +379,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
} }
} }
} }
/**
* 创建TTS回调
*/
private fun createTtsCallback(): AzureTtsHelper.TtsCallback {
return object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
sendTtsEvent(mapOf(
"type" to "synthesis_started"
))
}
override fun onSynthesizing() {
sendTtsEvent(mapOf(
"type" to "synthesizing"
))
}
override fun onSynthesisCompleted() {
sendTtsEvent(mapOf(
"type" to "synthesis_completed"
))
}
override fun onSynthesisCanceled() {
sendTtsEvent(mapOf(
"type" to "synthesis_canceled"
))
}
}
}
} }
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@ -374,49 +388,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(null) ttsEventChannel.setStreamHandler(null)
try { try {
// 取消所有协程
job.cancel()
// 取消BLE回调注册
if (isRegisteredToBle) {
BleService.removeCallback(this)
isRegisteredToBle = false
}
// 释放资源 // 释放资源
azureTtsHelper.dispose() azureTtsHelper.release()
azureAsrHelper.dispose() azureAsrHelper.dispose()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(tag, "释放资源失败: ${e.message}") FileLogger.e(tag, "释放资源失败: ${e.message}")
} }
} }
override fun onScanResult(devices: List<Map<String, Any>>) {
}
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(tag, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(tag, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}")
azureAsrHelper.pushAudioData(data)
}
override fun onWakeupSignalReceived() {
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
}
} }

298
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -4,10 +4,14 @@ import android.content.Context
import com.microsoft.cognitiveservices.speech.* import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.* import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.azure_speech.utils.FileLogger import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.deep_voice.speech.tts.ITtsService
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import kotlin.coroutines.CoroutineContext
/** /**
* Azure TTS Helper * Azure TTS Helper
@ -15,18 +19,13 @@ import kotlin.coroutines.CoroutineContext
* 基于微软Azure语音服务的TTS实现 * 基于微软Azure语音服务的TTS实现
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/ */
class AzureTtsHelper(private val context: Context) : CoroutineScope { class AzureTtsHelper(private val context: Context) : ITtsService {
companion object { companion object {
private const val TAG = "AzureTtsHelper" private const val TAG = "AzureTtsHelper"
private const val DEFAULT_LANGUAGE = "zh-CN" private const val DEFAULT_LANGUAGE = "zh-CN"
private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural" private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
} }
// 协程上下文
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// Azure语音服务配置 // Azure语音服务配置
private var speechConfig: SpeechConfig? = null private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null private var synthesizer: SpeechSynthesizer? = null
@ -39,8 +38,9 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
private var currentPitch = "0%" private var currentPitch = "0%"
private var currentVolume = "100%" private var currentVolume = "100%"
// 全局回调 // 事件监听器列表
private var ttsCallback: TtsCallback? = null private val eventListeners = mutableListOf<TtsEventListener>()
private val audioDataListeners = mutableListOf<AudioDataListener>()
// 自定义音频输出流 // 自定义音频输出流
private var customAudioOutputStream: PushAudioOutputStream? = null private var customAudioOutputStream: PushAudioOutputStream? = null
@ -48,54 +48,28 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 流式文本处理的缓冲区 // 流式文本处理的缓冲区
private val streamBuffer = StringBuilder() private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L private var lastSpeakTime = 0L
/**
* 简化的TTS回调接口
*/
interface TtsCallback {
/**
* 合成开始
*/
fun onSynthesisStarted() {}
/**
* 合成中
*/
fun onSynthesizing() {}
/**
* 合成完成
*/
fun onSynthesisCompleted() {}
/**
* 合成取消
*/
fun onSynthesisCanceled() {}
}
/** /**
* 初始化TTS引擎 * 初始化TTS引擎
* *
* @param subscriptionKey Azure语音服务订阅密钥 * @param ttsAppId 未使用,保留为空即可
* @param region Azure语音服务区域 * @param ttsAppToken Azure语音服务订阅密钥
* @param callback 可选,全局TTS回调接口 * @param ttsResource Azure语音服务区域
* @param language 语言代码,如"zh-CN"
* @return 是否初始化成功 * @return 是否初始化成功
*/ */
fun initialize( override fun initialize(
subscriptionKey: String, ttsAppId: String,
region: String, ttsAppToken: String,
callback: TtsCallback? = null ttsResource: String,
language: String
): Boolean { ): Boolean {
try { try {
// 释放之前的资源 // 释放之前的资源
dispose() dispose()
// 存储全局回调
ttsCallback = callback
// 创建语音配置 // 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region) speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm) speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
speechConfig?.setSpeechSynthesisVoiceName(currentVoice) speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
@ -115,7 +89,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners() setupEventListeners()
isInitialized = true isInitialized = true
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$region") FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -148,12 +122,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
} }
/** /**
* 设置TTS回调 * 添加TTS事件监听器
* */
* @param callback TTS回调接口 override fun addListener(listener: TtsEventListener) {
if (!eventListeners.contains(listener)) {
eventListeners.add(listener)
}
}
/**
* 移除TTS事件监听器
*/
override fun removeListener(listener: TtsEventListener) {
eventListeners.remove(listener)
}
/**
* 添加音频数据监听器
*/
override fun addAudioDataListener(listener: AudioDataListener) {
if (!audioDataListeners.contains(listener)) {
audioDataListeners.add(listener)
}
}
/**
* 移除音频数据监听器
*/
override fun removeAudioDataListener(listener: AudioDataListener) {
audioDataListeners.remove(listener)
}
/**
* 触发事件通知
*/ */
fun setTtsCallback(callback: TtsCallback?) { private fun notifyEvent(eventType: TtsEventType, params: Map<String, Any> = emptyMap()) {
ttsCallback = callback val event = TtsEvent(eventType, params)
// 复制列表防止并发修改异常
val listeners = ArrayList(eventListeners)
for (listener in listeners) {
try {
listener.onEvent(event)
} catch (e: Exception) {
FileLogger.e(TAG, "通知事件异常: ${e.message}")
}
}
} }
/** /**
@ -163,33 +176,29 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
synthesizer?.apply { synthesizer?.apply {
// 合成开始事件 // 合成开始事件
SynthesisStarted?.addEventListener { _, _ -> SynthesisStarted?.addEventListener { _, _ ->
ttsCallback?.onSynthesisStarted() notifyEvent(TtsEventType.SYNTHESIS_STARTED)
}
// 合成中事件
Synthesizing?.addEventListener { _, _ ->
ttsCallback?.onSynthesizing()
} }
// 合成完成事件 // 合成完成事件
SynthesisCompleted?.addEventListener { _, _ -> SynthesisCompleted?.addEventListener { _, _ ->
isSpeaking = false isSpeaking = false
ttsCallback?.onSynthesisCompleted() notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
} }
// 合成取消事件 // 合成取消事件
SynthesisCanceled?.addEventListener { _, _ -> SynthesisCanceled?.addEventListener { _, eventArgs ->
FileLogger.d(TAG, "语音合成取消") FileLogger.d(TAG, "语音合成取消")
isSpeaking = false isSpeaking = false
ttsCallback?.onSynthesisCanceled()
notifyEvent(TtsEventType.SYNTHESIS_CANCELED)
} }
} }
} }
/** /**
* 设置语音 * 设置语音角色
*/ */
fun setVoice(voiceName: String): Boolean { override fun setVoice(voiceName: String): Boolean {
if (!isInitialized) return false if (!isInitialized) return false
try { try {
@ -199,6 +208,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置语音失败: ${e.message}") FileLogger.e(TAG, "设置语音失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "VOICE_SET_FAILED",
"errorMessage" to "设置语音失败: ${e.message}"
))
return false return false
} }
} }
@ -224,6 +237,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners() setupEventListeners()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "重新创建合成器失败: ${e.message}") FileLogger.e(TAG, "重新创建合成器失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "RECREATE_FAILED",
"errorMessage" to "重新创建合成器失败: ${e.message}"
))
} }
} }
@ -240,6 +257,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "设置语音参数失败: ${e.message}") FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "PARAMS_SET_FAILED",
"errorMessage" to "设置语音参数失败: ${e.message}"
))
return false return false
} }
} }
@ -255,50 +276,52 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
* 生成SSML * 生成SSML
*/ */
private fun generateSsml(rawText: String): String { private fun generateSsml(rawText: String): String {
// 1. 定义要静音的符号和表情符号列表 // 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf( val symbolsToMute = listOf(
"#", "*", "@", "%", "^", "&", "#", "*", "@", "%", "^", "&",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉" "😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
) )
// 2. 转义 XML 保留字符 // 2. 转义 XML 保留字符
val escapedText = rawText val escapedText = rawText
.replace("&", "&amp;") .replace("&", "&amp;")
.replace("<", "&lt;") .replace("<", "&lt;")
.replace(">", "&gt;") .replace(">", "&gt;")
// 3. 静音处理特殊符号和表情符号 // 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空格 // 使用空白替换法,直接将符号替换为空格
var processedText = escapedText var processedText = escapedText
symbolsToMute.forEach { sym -> symbolsToMute.forEach { sym ->
processedText = processedText.replace(sym, "") processedText = processedText.replace(sym, "")
} }
// 4. 构造简化的SSML文档,减少嵌套层级 // 4. 构造简化的SSML文档,减少嵌套层级
return """ return """
<speak version="1.0" <speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis" xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts" xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN"> xml:lang="zh-CN">
<voice name="$currentVoice"> <voice name="$currentVoice">
<mstts:express-as style="cheerful"> <mstts:express-as style="cheerful">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume"> <prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
<say-as interpret-as="text">$processedText</say-as> <say-as interpret-as="text">$processedText</say-as>
</prosody> </prosody>
</mstts:express-as> </mstts:express-as>
</voice> </voice>
</speak> </speak>
""".trimIndent() """.trimIndent()
} }
/** /**
* 合成文本为语音 * 单次播放文本(非流式)
*
* @param text 要合成的文本
* @return 是否成功开始合成
*/ */
fun speakText(text: String): Boolean { override fun speakOnce(text: String): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "TTS引擎未初始化")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false return false
} }
@ -308,38 +331,47 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = true isSpeaking = true
// 使用纯异步方式 // 直接调用SpeakSsmlAsync,SDK内部已经是异步的
synthesizer?.SpeakSsmlAsync(ssml) synthesizer?.SpeakSsmlAsync(ssml)
// 返回true表示已开始合成请求
return true return true
} catch (e: Exception) { } catch (e: Exception) {
isSpeaking = false isSpeaking = false
FileLogger.e(TAG, "语音合成异常: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "SYNTHESIS_ERROR",
"errorMessage" to "语音合成异常: ${e.message}"
))
return false return false
} }
} }
/** /**
* 处理流式文本并在遇到标点符号时播放 * 流式合成文本
*
* @param text 收到的文本流片段
* @return 是否成功处理
*/ */
fun speakStream(text: String): Boolean { override fun speakStream(text: String): Boolean {
if (!isInitialized || text.isBlank()) { if (!isInitialized || text.isBlank()) {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
}
return false return false
} }
try { try {
// 添加新文本到缓冲区 // 添加新文本到缓冲区
streamBuffer.append(text) streamBuffer.append(text)
// 增加500ms防抖逻辑 // 增加500ms防抖逻辑
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 600) { if (currentTime - lastSpeakTime < 600) {
return true return true
} }
lastSpeakTime = currentTime lastSpeakTime = currentTime
val currentText = streamBuffer.toString() val currentText = streamBuffer.toString()
// 定义标点符号列表 // 定义标点符号列表
@ -363,24 +395,30 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
streamBuffer.delete(0, lastPunctuationIndex + 1) streamBuffer.delete(0, lastPunctuationIndex + 1)
// 播放提取的文本 // 播放提取的文本
return speakText(textToSpeak) return speakOnce(textToSpeak)
} }
// 如果没有找到标点符号,则等待更多文本 // 如果没有找到标点符号,则等待更多文本
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "流式语音合成失败: ${e.message}") FileLogger.e(TAG, "流式语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STREAM_FAILED",
"errorMessage" to "流式语音合成失败: ${e.message}"
))
return false return false
} }
} }
/** /**
* 播放剩余的流式文本 * 播放剩余的流式文本
*
* @return 是否成功播放剩余文本
*/ */
fun flushStream(): Boolean { override fun flushStream(): Boolean {
if (!isInitialized) { if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false return false
} }
@ -397,9 +435,13 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
} }
// 播放剩余文本 // 播放剩余文本
return speakText(remainingText) return speakOnce(remainingText)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}") FileLogger.e(TAG, "刷新流式文本失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "FLUSH_FAILED",
"errorMessage" to "刷新流式文本失败: ${e.message}"
))
return false return false
} }
} }
@ -407,7 +449,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/** /**
* 停止当前语音合成 * 停止当前语音合成
*/ */
fun stopSpeaking(): Boolean { override fun stop(): Boolean {
if (!isInitialized) return false if (!isInitialized) return false
try { try {
@ -416,6 +458,11 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = false isSpeaking = false
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STOP_FAILED",
"errorMessage" to "停止语音合成失败: ${e.message}"
))
return false return false
} }
} }
@ -423,11 +470,25 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/** /**
* 释放资源 * 释放资源
*/ */
fun dispose() { override fun release() {
dispose()
}
/**
* 释放资源(内部方法)
*/
private fun dispose() {
try { try {
stopSpeaking() // 停止播放
stop()
// 关闭合成器
synthesizer?.close() synthesizer?.close()
synthesizer = null
// 关闭配置
speechConfig?.close() speechConfig?.close()
speechConfig = null
// 关闭自定义音频输出流 // 关闭自定义音频输出流
try { try {
@ -437,15 +498,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 忽略关闭异常 // 忽略关闭异常
} }
// 清理引用
ttsCallback = null
// 清空流缓冲区 // 清空流缓冲区
streamBuffer.clear() streamBuffer.clear()
// 取消所有协程 // 重置状态
job.cancel()
isInitialized = false isInitialized = false
isSpeaking = false isSpeaking = false
} catch (e: Exception) { } catch (e: Exception) {

74
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt

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

15
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt

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

215
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -8,7 +8,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.* import android.os.*
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.yunqiinnovation.deepvoice_utils.FileLogger import android.util.Log
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -122,8 +122,8 @@ object BleService {
setDataCallback(object : OTAManager.DataCallback { setDataCallback(object : OTAManager.DataCallback {
override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) { override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) {
data?.let { data?.let {
FileLogger.i("BleService", "收到数据:${it.size} 字节") Log.i("BleService", "收到数据:${it.size} 字节")
FileLogger.i("BleService", "认证交互数据${it.contentToString()}") Log.i("BleService", "认证交互数据${it.contentToString()}")
// 移除次数限制,除非明确需要 // 移除次数限制,除非明确需要
otaManager?.onReceiveDeviceData(device, it) otaManager?.onReceiveDeviceData(device, it)
} }
@ -143,19 +143,17 @@ object BleService {
try { try {
opusManager = OpusManager() opusManager = OpusManager()
startOpusStreamDecoding() startOpusStreamDecoding()
FileLogger.d(TAG, "OpusManager初始化成功") Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) { } catch (e: OpusException) {
FileLogger.e(TAG, "OpusManager初始化失败: ${e.message}", e) Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
// 根据需要决定是否因为Opus初始化失败而返回false // 根据需要决定是否因为Opus初始化失败而返回false
} }
isInitialized = true isInitialized = true
FileLogger.d(TAG, "BLE服务初始化成功") Log.d(TAG, "BLE服务初始化成功")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "BLE服务初始化失败: ${e.message}", e) Log.e(TAG, "BLE服务初始化失败: ${e.message}", e)
return false return false
} }
} }
@ -168,7 +166,7 @@ object BleService {
fun addCallback(callback: Callback): Boolean { fun addCallback(callback: Callback): Boolean {
if (!callbacks.contains(callback)) { if (!callbacks.contains(callback)) {
callbacks.add(callback) callbacks.add(callback)
FileLogger.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}") Log.d(TAG, "已添加BLE回调, 当前回调数: ${callbacks.size}")
return true return true
} }
return false return false
@ -182,7 +180,7 @@ object BleService {
fun removeCallback(callback: Callback): Boolean { fun removeCallback(callback: Callback): Boolean {
val removed = callbacks.remove(callback) val removed = callbacks.remove(callback)
if (removed) { if (removed) {
FileLogger.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}") Log.d(TAG, "已移除BLE回调, 当前回调数: ${callbacks.size}")
} }
return removed return removed
} }
@ -192,7 +190,7 @@ object BleService {
*/ */
fun clearCallbacks() { fun clearCallbacks() {
callbacks.clear() callbacks.clear()
FileLogger.d(TAG, "已清除所有BLE回调") Log.d(TAG, "已清除所有BLE回调")
} }
// ====================================================================================================== // ======================================================================================================
@ -205,7 +203,7 @@ object BleService {
*/ */
fun startScan(timeout: Long = 15000): Boolean { fun startScan(timeout: Long = 15000): Boolean {
if (!isInitialized) { if (!isInitialized) {
FileLogger.e(TAG, "BLE服务未初始化") Log.e(TAG, "BLE服务未初始化")
return false return false
} }
@ -213,7 +211,7 @@ object BleService {
val scanner = bluetoothLeScanner ?: return false val scanner = bluetoothLeScanner ?: return false
if (isScanning) return false if (isScanning) return false
FileLogger.i(TAG, "开始主动扫描BLE设备...") Log.i(TAG, "开始主动扫描BLE设备...")
scanResults.clear() scanResults.clear()
val filter = ScanFilter.Builder() val filter = ScanFilter.Builder()
@ -234,13 +232,13 @@ object BleService {
// 设置扫描超时 // 设置扫描超时
scanHandler.postDelayed({ scanHandler.postDelayed({
FileLogger.d(TAG, "扫描超时,停止扫描") Log.d(TAG, "扫描超时,停止扫描")
stopScan() stopScan()
}, timeout) }, timeout)
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "启动扫描失败: ${e.message}", e) Log.e(TAG, "启动扫描失败: ${e.message}", e)
return false return false
} }
} }
@ -254,7 +252,7 @@ object BleService {
val scanner = bluetoothLeScanner ?: return false val scanner = bluetoothLeScanner ?: return false
if (!isScanning) return false if (!isScanning) return false
FileLogger.i(TAG, "停止主动扫描") Log.i(TAG, "停止主动扫描")
try { try {
scanner.stopScan(scanCallback) scanner.stopScan(scanCallback)
@ -264,7 +262,7 @@ object BleService {
return true return true
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "停止扫描失败: ${e.message}", e) Log.e(TAG, "停止扫描失败: ${e.message}", e)
return false return false
} }
} }
@ -298,12 +296,12 @@ object BleService {
val idx = scanResults.indexOfFirst { it.device.address == dev.address } val idx = scanResults.indexOfFirst { it.device.address == dev.address }
if (idx >= 0) scanResults[idx] = result else scanResults.add(result) if (idx >= 0) scanResults[idx] = result else scanResults.add(result)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "上报扫描结果异常: ${e.message}", e) Log.e(TAG, "上报扫描结果异常: ${e.message}", e)
} }
} }
override fun onScanFailed(errorCode: Int) { override fun onScanFailed(errorCode: Int) {
FileLogger.e(TAG, "扫描失败 error=$errorCode") Log.e(TAG, "扫描失败 error=$errorCode")
isScanning = false isScanning = false
} }
} }
@ -317,7 +315,7 @@ object BleService {
val serviceUuids = val serviceUuids =
scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无" scanRecord?.serviceUuids?.joinToString(", ") { it.uuid.toString() } ?: "无"
FileLogger.d( Log.d(
TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " + TAG, "扫描到设备: 名称=${device.name ?: "未知"}, 地址=${device.address}, " +
"RSSI=${result.rssi}, 服务UUID=[$serviceUuids]" "RSSI=${result.rssi}, 服务UUID=[$serviceUuids]"
) )
@ -326,12 +324,12 @@ object BleService {
val scanData = scanRecord?.bytes val scanData = scanRecord?.bytes
if (scanData != null) { if (scanData != null) {
val hexString = scanData.joinToString("") { String.format("%02X", it) } val hexString = scanData.joinToString("") { String.format("%02X", it) }
FileLogger.d(TAG, "广播数据: $hexString") Log.d(TAG, "广播数据: $hexString")
// 解析服务数据 // 解析服务数据
scanRecord.serviceData?.forEach { (uuid, data) -> scanRecord.serviceData?.forEach { (uuid, data) ->
val dataHex = data.joinToString("") { String.format("%02X", it) } val dataHex = data.joinToString("") { String.format("%02X", it) }
FileLogger.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex") Log.d(TAG, "服务数据: UUID=${uuid.uuid}, 数据=$dataHex")
} }
} }
} }
@ -352,16 +350,16 @@ object BleService {
val pairedMac = companionManager.getPairedMacAddress() val pairedMac = companionManager.getPairedMacAddress()
if (pairedMac.isNullOrEmpty()) { if (pairedMac.isNullOrEmpty()) {
FileLogger.w(TAG, "未找到已配对的设备 MAC 地址") Log.w(TAG, "未找到已配对的设备 MAC 地址")
return return
} }
FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") Log.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接")
// 调用现有的 connect 方法 // 调用现有的 connect 方法
connect(pairedMac) connect(pairedMac)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e) Log.e(TAG, "自动连接已配对设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
} }
} }
@ -373,7 +371,7 @@ object BleService {
*/ */
fun connect(address: String) { fun connect(address: String) {
if (connectionState.value == BleConst.STATE_CONNECTED) { if (connectionState.value == BleConst.STATE_CONNECTED) {
FileLogger.w(TAG, "已经连接中,忽略连接请求") Log.w(TAG, "已经连接中,忽略连接请求")
return return
} }
@ -399,7 +397,7 @@ object BleService {
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e) Log.e(TAG, "连接设备失败: ${e.message}", e)
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
} }
} }
@ -421,7 +419,7 @@ object BleService {
gatt.disconnect() gatt.disconnect()
gatt.close() gatt.close()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "关闭GATT连接异常: ${e.message}", e) Log.e(TAG, "关闭GATT连接异常: ${e.message}", e)
} }
bluetoothGatt = null bluetoothGatt = null
notifyChar = null notifyChar = null
@ -433,9 +431,9 @@ object BleService {
fun startOTA() { fun startOTA() {
try { try {
otaManager?.startOTA(CustomUpdateCallback(context, null)) otaManager?.startOTA(CustomUpdateCallback(context, null))
FileLogger.i(TAG, "OTA 升级已启动") Log.i(TAG, "OTA 升级已启动")
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
FileLogger.e(TAG, "OTA 启动失败: ${e.message}") Log.e(TAG, "OTA 启动失败: ${e.message}")
disconnectGatt() disconnectGatt()
} }
} }
@ -448,7 +446,7 @@ object BleService {
val file = File(firmwarePath) val file = File(firmwarePath)
if (!file.exists()) { if (!file.exists()) {
// throw IOException("Firmware file not found") // throw IOException("Firmware file not found")
FileLogger.i(TAG, "Firmware file not found") Log.i(TAG, "Firmware file not found")
} }
val bluetoothOption = BluetoothOTAConfigure().apply { val bluetoothOption = BluetoothOTAConfigure().apply {
@ -461,10 +459,10 @@ object BleService {
firmwareFilePath = firmwarePath firmwareFilePath = firmwarePath
} }
otaManager?.configure(bluetoothOption) ?: run { otaManager?.configure(bluetoothOption) ?: run {
FileLogger.e(TAG, "OTA Manager未初始化") Log.e(TAG, "OTA Manager未初始化")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "OTA配置失败: ${e.message}") Log.e(TAG, "OTA配置失败: ${e.message}")
// 处理错误 // 处理错误
} }
} }
@ -485,12 +483,12 @@ object BleService {
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
FileLogger.i(TAG, "MTU 更新成功: $mtu") Log.i(TAG, "MTU 更新成功: $mtu")
configureOTA() configureOTA()
startOTA() startOTA()
} else { } else {
FileLogger.e(TAG, "MTU 更新失败: status=$status") Log.e(TAG, "MTU 更新失败: status=$status")
} }
} }
@ -499,7 +497,7 @@ object BleService {
status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> {
updateConnectionState(BleConst.STATE_CONNECTED) updateConnectionState(BleConst.STATE_CONNECTED)
g.discoverServices() g.discoverServices()
FileLogger.i(TAG, "连接成功,开始 MTU 协商") Log.i(TAG, "连接成功,开始 MTU 协商")
otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK) otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK)
g.requestMtu(512) // 触发 MTU 修改流程 g.requestMtu(512) // 触发 MTU 修改流程
@ -538,7 +536,7 @@ object BleService {
audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID) audioChar = audioSvc?.getCharacteristic(BleConst.RECEIVE_AUDIO_CHAR_UUID)
if (notifyChar == null || writeChar == null) { if (notifyChar == null || writeChar == null) {
FileLogger.e(TAG, "未找到主服务所需特征") Log.e(TAG, "未找到主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
return return
} }
@ -548,9 +546,9 @@ object BleService {
// 设置音频服务的通知(如果存在) // 设置音频服务的通知(如果存在)
if (audioChar != null) { if (audioChar != null) {
setupNotifications(g, audioChar) setupNotifications(g, audioChar)
FileLogger.i(TAG, "音频服务特征找到并设置通知") Log.i(TAG, "音频服务特征找到并设置通知")
} else { } else {
FileLogger.w(TAG, "音频服务特征未找到") Log.w(TAG, "音频服务特征未找到")
} }
// 设置主服务的通知 // 设置主服务的通知
setupNotifications(g, notifyChar) setupNotifications(g, notifyChar)
@ -566,12 +564,12 @@ object BleService {
when (c.uuid) { when (c.uuid) {
// 音频特征数据 // 音频特征数据
BleConst.RECEIVE_AUDIO_CHAR_UUID -> { BleConst.RECEIVE_AUDIO_CHAR_UUID -> {
FileLogger.i(TAG, "收到音频特征数据") // Log.i(TAG, "收到音频特征数据")
processAudioData(data) processAudioData(data)
} }
// 通知特征数据(命令和控制) // 通知特征数据(命令和控制)
BleConst.NOTIFY_CHAR_UUID -> { BleConst.NOTIFY_CHAR_UUID -> {
FileLogger.i(TAG, "收到通知特征数据(命令和控制)") // Log.i(TAG, "收到通知特征数据(命令和控制)")
// 判断数据类型 // 判断数据类型
when { when {
// 设备响应 (0xBB开头) // 设备响应 (0xBB开头)
@ -584,7 +582,7 @@ object BleService {
} }
// 其他未知数据 // 其他未知数据
else -> { else -> {
FileLogger.d( Log.d(
TAG, TAG,
"收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}" "收到未知数据: ${data.joinToString(", ") { "0x%02X".format(it) }}"
) )
@ -593,8 +591,7 @@ object BleService {
} }
// 其他未知特征 // 其他未知特征
else -> { else -> {
FileLogger.i(TAG, "收到其他未知特征") Log.d(
FileLogger.d(
TAG, TAG,
"收到未知特征数据: ${c.uuid}, 数据=${ "收到未知特征数据: ${c.uuid}, 数据=${
data.joinToString(", ") { data.joinToString(", ") {
@ -609,7 +606,7 @@ object BleService {
override fun onCharacteristicWrite( override fun onCharacteristicWrite(
g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int g: BluetoothGatt, c: BluetoothGattCharacteristic, status: Int
) { ) {
FileLogger.i( Log.i(
TAG, TAG,
"写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}" "写入 ${if (status == BluetoothGatt.GATT_SUCCESS) "成功" else "失败"}"
) )
@ -618,10 +615,8 @@ object BleService {
override fun onDescriptorWrite( override fun onDescriptorWrite(
g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int g: BluetoothGatt, d: BluetoothGattDescriptor, status: Int
) { ) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS)
FileLogger.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}") Log.i(TAG, "通知描述符写入成功: ${d.characteristic.uuid}")
}
} }
} }
@ -630,7 +625,7 @@ object BleService {
*/ */
private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) { private fun setupNotifications(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic?) {
if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) { if (characteristic == null || characteristic.properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) == 0) {
FileLogger.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}") Log.w(TAG, "特征不支持通知功能: ${characteristic?.uuid}")
return return
} }
@ -649,10 +644,10 @@ object BleService {
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(data) opusManager?.writeAudioStream(data)
} else { } else {
// FileLogger.d(TAG, "Opus解码流未启动,忽略音频数据") // Log.d(TAG, "Opus解码流未启动,忽略音频数据")
} }
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "处理音频数据异常: ${e.message}", e) Log.e(TAG, "处理音频数据异常: ${e.message}", e)
} }
} }
@ -661,12 +656,12 @@ object BleService {
* 处理设备响应数据 - 帧头为0xBB * 处理设备响应数据 - 帧头为0xBB
*/ */
private fun processDeviceResponse(data: ByteArray) { private fun processDeviceResponse(data: ByteArray) {
FileLogger.d( Log.d(
TAG, TAG,
"收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" "收到设备响应: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
) )
if (data.size < 3) { if (data.size < 3) {
FileLogger.e(TAG, "设备响应数据格式错误:数据长度过短") Log.e(TAG, "设备响应数据格式错误:数据长度过短")
return return
} }
@ -675,7 +670,7 @@ object BleService {
// 判断命令失败响应(长度为0) // 判断命令失败响应(长度为0)
if (data.size == 4 && data[2].toInt() and 0xFF == 0) { if (data.size == 4 && data[2].toInt() and 0xFF == 0) {
FileLogger.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败") Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败")
// 创建失败结果Map // 创建失败结果Map
val resultMap = mapOf( val resultMap = mapOf(
"success" to false, "success" to false,
@ -691,7 +686,7 @@ object BleService {
// 检查数据长度 // 检查数据长度
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return return
} }
@ -701,7 +696,7 @@ object BleService {
val receivedCrc = data[data.size - 1].toInt() and 0xFF val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e( Log.e(
TAG, TAG,
"设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
) )
@ -714,7 +709,7 @@ object BleService {
System.arraycopy(data, 3, responseData, 0, length) System.arraycopy(data, 3, responseData, 0, length)
} }
FileLogger.d( Log.d(
TAG, TAG,
"收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${
responseData.joinToString(", ") { responseData.joinToString(", ") {
@ -742,7 +737,7 @@ object BleService {
.dropWhile { it == 0xFF } // 仅丢弃开头的连续 255 .dropWhile { it == 0xFF } // 仅丢弃开头的连续 255
.joinToString(".") { it.toString() } .joinToString(".") { it.toString() }
resultMap["version"] = version resultMap["version"] = version
FileLogger.i(TAG, "接收到目标版本号: $version") Log.i(TAG, "接收到目标版本号: $version")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -761,7 +756,7 @@ object BleService {
val productId = val productId =
responseData.joinToString(".") { (it.toInt() and 0xFF).toString() } responseData.joinToString(".") { (it.toInt() and 0xFF).toString() }
resultMap["productId"] = productId resultMap["productId"] = productId
FileLogger.i(TAG, "接收到产品ID: $productId") Log.i(TAG, "接收到产品ID: $productId")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -777,7 +772,7 @@ object BleService {
if (responseData.isNotEmpty()) { if (responseData.isNotEmpty()) {
val colorId = (responseData[0].toInt() and 0xFF).toString() val colorId = (responseData[0].toInt() and 0xFF).toString()
resultMap["colorId"] = colorId resultMap["colorId"] = colorId
FileLogger.i(TAG, "接收到耳机颜色ID: $colorId") Log.i(TAG, "接收到耳机颜色ID: $colorId")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -814,7 +809,7 @@ object BleService {
resultMap["caseCharging"] = caseCharging resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i( Log.i(
TAG, "接收到电量信息: 左耳=${leftEarBattery}% " + TAG, "接收到电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
@ -858,7 +853,7 @@ object BleService {
resultMap["statusDesc"] = statusDesc resultMap["statusDesc"] = statusDesc
resultMap["channelDesc"] = channelDesc resultMap["channelDesc"] = channelDesc
FileLogger.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc")
} }
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
@ -872,7 +867,7 @@ object BleService {
"rawData" to responseData, "rawData" to responseData,
"isUnknown" to true "isUnknown" to true
) )
FileLogger.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}") Log.d(TAG, "收到未知命令响应: 0x${cmdType.toString(16)}")
notifyDeviceInfoReceived(cmdType, resultMap) notifyDeviceInfoReceived(cmdType, resultMap)
} }
} }
@ -882,12 +877,12 @@ object BleService {
* 处理设备主动上报数据 - 帧头为0xCC * 处理设备主动上报数据 - 帧头为0xCC
*/ */
private fun processDeviceNotification(data: ByteArray) { private fun processDeviceNotification(data: ByteArray) {
FileLogger.d( Log.d(
TAG, TAG,
"收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}" "收到设备主动上报: ${data.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}"
) )
if (data.size < 3) { if (data.size < 3) {
FileLogger.e(TAG, "设备主动上报数据格式错误:数据长度过短") Log.e(TAG, "设备主动上报数据格式错误:数据长度过短")
return return
} }
@ -897,7 +892,7 @@ object BleService {
// 检查数据长度 // 检查数据长度
if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC if (data.size != length + 4) { // 帧头 + 类型 + 长度 + 数据 + CRC
FileLogger.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") Log.e(TAG, "设备主动上报数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return return
} }
@ -907,7 +902,7 @@ object BleService {
val receivedCrc = data[data.size - 1].toInt() and 0xFF val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) { if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
FileLogger.e( Log.e(
TAG, TAG,
"设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" "设备主动上报CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
) )
@ -920,7 +915,7 @@ object BleService {
System.arraycopy(data, 3, notifyData, 0, length) System.arraycopy(data, 3, notifyData, 0, length)
} }
FileLogger.d( Log.d(
TAG, TAG,
"收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${ "收到设备主动上报: 类型=0x${infoType.toString(16)}, 数据=${
notifyData.joinToString(", ") { notifyData.joinToString(", ") {
@ -965,7 +960,7 @@ object BleService {
resultMap["caseCharging"] = caseCharging resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示
FileLogger.i( Log.i(
TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " + TAG, "收到主动上报电量信息: 左耳=${leftEarBattery}% " +
"(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " + "(${if (leftEarCharging) "充电中" else "未充电"}), 右耳=${rightEarBattery}% " +
"(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " + "(${if (rightEarCharging) "充电中" else "未充电"}), 充电仓=${caseBattery}% " +
@ -977,7 +972,7 @@ object BleService {
} }
BleConst.CMD_WAKE_UP -> { BleConst.CMD_WAKE_UP -> {
FileLogger.i(TAG, "收到唤醒指令") Log.i(TAG, "收到唤醒指令")
// 接收到唤醒信号,打开编码器 (设备侧) // 接收到唤醒信号,打开编码器 (设备侧)
openEncoder() openEncoder()
notifyWakeupSignalReceived() notifyWakeupSignalReceived()
@ -993,7 +988,7 @@ object BleService {
"isUnknown" to true, "isUnknown" to true,
"isNotification" to true // 标记为主动上报 "isNotification" to true // 标记为主动上报
) )
FileLogger.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}") Log.d(TAG, "收到未知主动上报类型: 0x${infoType.toString(16)}")
notifyDeviceInfoReceived(infoType, resultMap) notifyDeviceInfoReceived(infoType, resultMap)
} }
} }
@ -1010,13 +1005,13 @@ object BleService {
packetSize: Int = 40 // 确认设备端Opus编码帧长 packetSize: Int = 40 // 确认设备端Opus编码帧长
): Boolean { ): Boolean {
if (opusManager == null) { if (opusManager == null) {
FileLogger.e(TAG, "OpusManager未初始化,无法开始解码") Log.e(TAG, "OpusManager未初始化,无法开始解码")
return false return false
} }
// 如果已经在解码流,先停止 // 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() opusManager?.stopDecodeStream()
FileLogger.d(TAG, "已停止正在进行的Opus解码流") Log.d(TAG, "已停止正在进行的Opus解码流")
} }
val option = OpusOption() val option = OpusOption()
@ -1025,29 +1020,29 @@ object BleService {
.setSampleRate(sampleRate) .setSampleRate(sampleRate)
.setPacketSize(packetSize) .setPacketSize(packetSize)
FileLogger.d(TAG, "准备开始Opus数据流解码, 参数: $option") Log.d(TAG, "准备开始Opus数据流解码, 参数: $option")
var streamStartedSuccessfully = false var streamStartedSuccessfully = false
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) { override fun onDecodeStream(data: ByteArray?) {
if (data != null) { if (data != null) {
// FileLogger.d(TAG, "Opus解码数据: ${data.size} bytes") // Log.d(TAG, "Opus解码数据: ${data.size} bytes")
notifyAudioDataReceived(data) notifyAudioDataReceived(data)
} }
} }
override fun onStart() { override fun onStart() {
streamStartedSuccessfully = true streamStartedSuccessfully = true
FileLogger.i(TAG, "Opus数据流解码已开始") Log.i(TAG, "Opus数据流解码已开始")
// 可以通过回调通知上层解码已开始 // 可以通过回调通知上层解码已开始
} }
override fun onComplete(outPath: String?) { override fun onComplete(outPath: String?) {
FileLogger.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)") Log.i(TAG, "Opus数据流解码完成: $outPath (通常流式解码不会调用此方法)")
} }
override fun onError(code: Int, message: String?) { override fun onError(code: Int, message: String?) {
FileLogger.e(TAG, "Opus数据流解码错误: [$code] $message") Log.e(TAG, "Opus数据流解码错误: [$code] $message")
// 可以通过回调通知上层解码错误 // 可以通过回调通知上层解码错误
} }
}) })
@ -1060,10 +1055,10 @@ object BleService {
private fun stopOpusStreamDecoding(): Boolean { private fun stopOpusStreamDecoding(): Boolean {
if (opusManager?.isDecodeStream == true) { if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() opusManager?.stopDecodeStream()
FileLogger.i(TAG, "已停止Opus数据流解码") Log.i(TAG, "已停止Opus数据流解码")
return true return true
} }
FileLogger.d(TAG, "Opus数据流未在解码或OpusManager未初始化") Log.d(TAG, "Opus数据流未在解码或OpusManager未初始化")
return false return false
} }
@ -1075,7 +1070,7 @@ object BleService {
* 获取耳机版本号 * 获取耳机版本号
*/ */
fun getDeviceVersion(): Boolean { fun getDeviceVersion(): Boolean {
FileLogger.i(TAG, "获取耳机版本号...") Log.i(TAG, "获取耳机版本号...")
return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_VERSION.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1083,7 +1078,7 @@ object BleService {
* 获取产品ID * 获取产品ID
*/ */
fun getProductId(): Boolean { fun getProductId(): Boolean {
FileLogger.i(TAG, "获取耳机产品ID...") Log.i(TAG, "获取耳机产品ID...")
return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_PRODUCT_ID.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1091,7 +1086,7 @@ object BleService {
* 获取耳机颜色ID * 获取耳机颜色ID
*/ */
fun getColorId(): Boolean { fun getColorId(): Boolean {
FileLogger.i(TAG, "获取耳机颜色ID...") Log.i(TAG, "获取耳机颜色ID...")
return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_COLOR_ID.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1099,7 +1094,7 @@ object BleService {
* 获取耳机电量信息 * 获取耳机电量信息
*/ */
fun getBatteryInfo(): Boolean { fun getBatteryInfo(): Boolean {
FileLogger.i(TAG, "获取耳机电量信息...") Log.i(TAG, "获取耳机电量信息...")
return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte())) return sendCommand(BleConst.CMD_GET_BATTERY_INFO.toByte(), byteArrayOf(0xFF.toByte()))
} }
@ -1107,7 +1102,7 @@ object BleService {
* 控制编解码 - 关闭编解码 * 控制编解码 - 关闭编解码
*/ */
fun closeCodec(): Boolean { fun closeCodec(): Boolean {
FileLogger.i(TAG, "关闭编解码...") Log.i(TAG, "关闭编解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), BleConst.CMD_CONTROL_CODEC.toByte(),
@ -1123,7 +1118,7 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openDecoder(): Boolean { fun openDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// val opusStarted = startOpusStreamDecoding( // val opusStarted = startOpusStreamDecoding(
// hasHeader = false, // BLE传输的Opus数据通常无文件头 // hasHeader = false, // BLE传输的Opus数据通常无文件头
@ -1136,7 +1131,7 @@ object BleService {
// ) // )
FileLogger.i(TAG, "打开解码...") Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_DECODE_ON.toByte(), BleConst.CODEC_CONTROL_DECODE_ON.toByte(),
@ -1150,9 +1145,9 @@ object BleService {
* 控制编解码 - 打开解码 * 控制编解码 - 打开解码
*/ */
fun openA2DPDecoder(): Boolean { fun openA2DPDecoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// FileLogger.i(TAG, "打开解码...") // Log.i(TAG, "打开解码...")
return sendCommand( return sendCommand(
BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf( BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(
BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(),
@ -1166,7 +1161,7 @@ object BleService {
* 控制编解码 - A2DP播放 * 控制编解码 - A2DP播放
*/ */
fun playA2DP(): Boolean { fun playA2DP(): Boolean {
FileLogger.i(TAG, "切换至A2DP播放模式...") Log.i(TAG, "切换至A2DP播放模式...")
return sendCommand(BleConst.CMD_PLAY_A2DP.toByte()) return sendCommand(BleConst.CMD_PLAY_A2DP.toByte())
} }
@ -1174,7 +1169,7 @@ object BleService {
* 控制编解码 - 打开编码 * 控制编解码 - 打开编码
*/ */
fun openEncoder(): Boolean { fun openEncoder(): Boolean {
FileLogger.i(TAG, "打开编码 (设备侧)...") Log.i(TAG, "打开编码 (设备侧)...")
// 目前仅发送命令通知设备开始编码。 // 目前仅发送命令通知设备开始编码。
@ -1194,11 +1189,11 @@ object BleService {
*/ */
fun setChannelMode(channelMode: Int): Boolean { fun setChannelMode(channelMode: Int): Boolean {
when (channelMode) { when (channelMode) {
BleConst.AUDIO_CHANNEL_LEFT -> FileLogger.i(TAG, "设置左声道模式...") BleConst.AUDIO_CHANNEL_LEFT -> Log.i(TAG, "设置左声道模式...")
BleConst.AUDIO_CHANNEL_RIGHT -> FileLogger.i(TAG, "设置右声道模式...") BleConst.AUDIO_CHANNEL_RIGHT -> Log.i(TAG, "设置右声道模式...")
BleConst.AUDIO_CHANNEL_STEREO -> FileLogger.i(TAG, "设置立体声模式...") BleConst.AUDIO_CHANNEL_STEREO -> Log.i(TAG, "设置立体声模式...")
else -> { else -> {
FileLogger.e(TAG, "无效的声道模式: $channelMode") Log.e(TAG, "无效的声道模式: $channelMode")
return false return false
} }
} }
@ -1216,7 +1211,7 @@ object BleService {
* 音量增大 * 音量增大
*/ */
fun volumeUp(): Boolean { fun volumeUp(): Boolean {
FileLogger.i(TAG, "设备音量增加...") Log.i(TAG, "设备音量增加...")
return sendCommand(BleConst.CMD_VOLUME_UP.toByte()) return sendCommand(BleConst.CMD_VOLUME_UP.toByte())
} }
@ -1224,7 +1219,7 @@ object BleService {
* 音量减小 * 音量减小
*/ */
fun volumeDown(): Boolean { fun volumeDown(): Boolean {
FileLogger.i(TAG, "设备音量减小...") Log.i(TAG, "设备音量减小...")
return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte()) return sendCommand(BleConst.CMD_VOLUME_DOWN.toByte())
} }
@ -1278,7 +1273,7 @@ object BleService {
it.release() it.release()
} }
opusManager = null opusManager = null
FileLogger.d(TAG, "OpusManager已释放") Log.d(TAG, "OpusManager已释放")
isInitialized = false // 标记为未初始化 isInitialized = false // 标记为未初始化
} }
@ -1294,7 +1289,7 @@ object BleService {
*/ */
private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean { private fun sendCommand(cmdType: Byte, cmdData: ByteArray = byteArrayOf()): Boolean {
if (!checkConn()) { if (!checkConn()) {
FileLogger.e(TAG, "发送命令失败: 设备未连接") Log.e(TAG, "发送命令失败: 设备未连接")
return false return false
} }
@ -1325,7 +1320,7 @@ object BleService {
cmdPacket[3 + cmdLength] = crc cmdPacket[3 + cmdLength] = crc
// 记录命令详情 // 记录命令详情
FileLogger.d( Log.d(
TAG, TAG,
"发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " + "发送命令: CMD=0x${(cmdType.toInt() and 0xFF).toString(16)}, 长度=$cmdLength, " +
"数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " + "数据=${cmdData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }}, " +
@ -1336,7 +1331,7 @@ object BleService {
writeChar!!.value = cmdPacket writeChar!!.value = cmdPacket
return bluetoothGatt?.writeCharacteristic(writeChar) ?: false return bluetoothGatt?.writeCharacteristic(writeChar) ?: false
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "发送命令异常: ${e.message}", e) Log.e(TAG, "发送命令异常: ${e.message}", e)
return false return false
} }
} }
@ -1369,7 +1364,7 @@ object BleService {
try { try {
callback.onScanResult(devices) callback.onScanResult(devices)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发扫描结果回调异常", e) Log.e(TAG, "分发扫描结果回调异常", e)
} }
} }
} }
@ -1382,7 +1377,7 @@ object BleService {
try { try {
callback.onConnectionStateChanged(state) callback.onConnectionStateChanged(state)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发连接状态回调异常", e) Log.e(TAG, "分发连接状态回调异常", e)
} }
} }
} }
@ -1395,7 +1390,7 @@ object BleService {
try { try {
callback.onAudioDataReceived(data) callback.onAudioDataReceived(data)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发音频数据回调异常", e) Log.e(TAG, "分发音频数据回调异常", e)
} }
} }
} }
@ -1408,7 +1403,7 @@ object BleService {
try { try {
callback.onWakeupSignalReceived() callback.onWakeupSignalReceived()
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发唤醒信号回调异常", e) Log.e(TAG, "分发唤醒信号回调异常", e)
} }
} }
} }
@ -1421,7 +1416,7 @@ object BleService {
try { try {
callback.onDeviceInfoReceived(infoType, infoData) callback.onDeviceInfoReceived(infoType, infoData)
} catch (e: Exception) { } catch (e: Exception) {
FileLogger.e(TAG, "分发设备信息回调异常", e) Log.e(TAG, "分发设备信息回调异常", e)
} }
} }
} }

11
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt

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

13
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt

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

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

@ -3,11 +3,28 @@ version = "1.0.0"
plugins { plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.kotlin.android") kotlin("android")
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.1.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
}
}
repositories {
google()
mavenCentral()
} }
android { android {
namespace = "com.yunqiinnovation.ble_service" namespace = "com.deep_voice.bytedance_speech"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
@ -22,15 +39,18 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
languageVersion = "1.7"
apiVersion = "1.7"
} }
} }
dependencies { dependencies {
implementation(kotlin("stdlib-jdk7"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.squareup.okhttp3:okhttp:4.11.0") implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
implementation(project(":speech"))
} }

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

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

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

329
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt

@ -0,0 +1,329 @@
package com.deep_voice.bytedance_speech
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import android.os.Build
import android.util.Log
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.CancellationException
/**
* 字节跳动语音合成音频播放器
*
* 使用AudioTrack播放PCM格式音频流,基于Kotlin协程实现异步处理
*/
class BytedanceAudioPlayer : AudioDataListener, CoroutineScope {
companion object {
private const val TAG = "BytedanceAudioPlayer"
private const val SAMPLE_RATE = 24000 // 样本率
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO // 单声道
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT // 16位PCM
private const val IDLE_TIMEOUT_MS = 800L // 无数据超时时间
}
// 协程相关
private val job = SupervisorJob()
override val coroutineContext = Dispatchers.IO + job
// 音频处理相关
private var audioTrack: AudioTrack? = null
private val isPlaying = AtomicBoolean(false)
private val isPaused = AtomicBoolean(false)
// 播放状态
private val playStarted = AtomicBoolean(false)
private val completionReported = AtomicBoolean(false)
// 使用简单的并发队列,保证线程安全
private val audioDataQueue = ConcurrentLinkedQueue<ByteArray>()
// 播放状态监听
private var onPlayStarted: (() -> Unit)? = null
private var onPlayCompleted: (() -> Unit)? = null
private var onError: ((String) -> Unit)? = null
// 处理协程
private var processingJob: Job? = null
private var lastDataTime = 0L
init {
Log.d(TAG, "BytedanceAudioPlayer初始化")
}
/**
* 开始播放
*/
fun start() {
if (isPlaying.getAndSet(true)) return
isPaused.set(false)
playStarted.set(false)
completionReported.set(false)
lastDataTime = System.currentTimeMillis()
processingJob = launch {
try {
initAudioTrack()
// 处理队列中的音频数据
while (isActive && isPlaying.get()) {
processQueuedAudio()
// 检查是否播放完成
checkPlaybackCompletion()
// 队列为空时短暂延迟
if (audioDataQueue.isEmpty()) {
delay(10)
}
}
} catch (e: CancellationException) {
// 协程被取消,正常行为
} catch (e: Exception) {
Log.e(TAG, "播放错误: ${e.message}")
onError?.invoke("播放错误: ${e.message}")
stopInternal(false)
}
}
}
/**
* 暂停播放
*/
fun pause() {
if (!isPlaying.get() || isPaused.getAndSet(true)) return
audioTrack?.pause()
}
/**
* 恢复播放
*/
fun resume() {
if (!isPlaying.get() || !isPaused.getAndSet(false)) return
audioTrack?.play()
}
/**
* 停止播放
*/
fun stop() {
Log.d(TAG, "停止播放")
stopInternal(true)
}
/**
* 内部停止处理
*/
private fun stopInternal(reportCompletion: Boolean) {
if (!isPlaying.getAndSet(false)) return
isPaused.set(false)
processingJob?.cancel()
audioTrack?.stop()
// 不在stopInternal中释放资源,只停止播放
// 清空队列
audioDataQueue.clear()
// 播放完成通知
if (reportCompletion && playStarted.get() && !completionReported.getAndSet(true)) {
launch(Dispatchers.Main) {
onPlayCompleted?.invoke()
}
}
}
/**
* 释放资源
*/
fun release() {
stopInternal(false)
// 在release方法中释放AudioTrack资源
audioTrack?.release()
audioTrack = null
// 释放协程资源
job.cancel()
}
/**
* 设置播放开始回调
*/
fun setOnPlayStarted(listener: () -> Unit) {
onPlayStarted = listener
}
/**
* 设置播放完成回调
*/
fun setOnPlayCompleted(listener: () -> Unit) {
onPlayCompleted = listener
}
/**
* 设置错误回调
*/
fun setOnError(listener: (String) -> Unit) {
onError = listener
}
/**
* 接收音频数据(实现AudioDataListener接口)
*/
override fun onAudioData(data: ByteArray) {
if (data.isEmpty()) return
lastDataTime = System.currentTimeMillis()
if (!isPlaying.get()) {
start()
}
// 添加到队列 - 简单有效,保证FIFO顺序
audioDataQueue.add(data.copyOf())
}
/**
* 检查是否正在播放
*/
fun isPlaying(): Boolean = isPlaying.get() && !isPaused.get()
/**
* 处理队列中的音频数据
*/
private suspend fun processQueuedAudio() {
if (isPaused.get() || !isPlaying.get() || !isActive) return
// 获取并处理队列中的数据
val data = audioDataQueue.poll() ?: return
// 写入音频数据
val result = audioTrack?.write(data, 0, data.size) ?: 0
if (result > 0) {
// 标记播放开始
if (!playStarted.getAndSet(true)) {
withContext(Dispatchers.Main) {
onPlayStarted?.invoke()
}
}
} else if (result < 0) {
// 处理错误
handleAudioTrackError(result)
}
}
/**
* 检查播放是否完成(超时无数据)
*/
private suspend fun checkPlaybackCompletion() {
val currentTime = System.currentTimeMillis()
// 超时判断 - 队列为空且超过超时时间
if (playStarted.get() && audioDataQueue.isEmpty() &&
currentTime - lastDataTime > IDLE_TIMEOUT_MS && isPlaying.get()) {
// 播放完成
if (!completionReported.getAndSet(true)) {
withContext(Dispatchers.Main) {
onPlayCompleted?.invoke()
}
stopInternal(false) // 已报告,不需要再次报告
}
}
}
/**
* 处理AudioTrack错误
*/
private fun handleAudioTrackError(result: Int) {
val errorMsg = when(result) {
AudioTrack.ERROR_INVALID_OPERATION -> "无效操作"
AudioTrack.ERROR_BAD_VALUE -> "参数错误"
AudioTrack.ERROR_DEAD_OBJECT -> "对象已销毁"
else -> "未知错误"
}
// 对象已销毁时尝试重建
if (result == AudioTrack.ERROR_DEAD_OBJECT) {
try {
audioTrack?.release()
initAudioTrack()
} catch (e: Exception) {
Log.e(TAG, "重建AudioTrack失败: ${e.message}")
throw e
}
} else {
Log.e(TAG, "AudioTrack错误: $errorMsg")
}
}
/**
* 初始化AudioTrack
*/
private fun initAudioTrack() {
val minBufferSize = AudioTrack.getMinBufferSize(
SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT
)
if (minBufferSize == AudioTrack.ERROR || minBufferSize == AudioTrack.ERROR_BAD_VALUE) {
throw IllegalStateException("无法获取有效的音频缓冲区大小")
}
// 使用较大的缓冲区提高稳定性
val bufferSize = minBufferSize * 4
// 创建AudioTrack
audioTrack = if (Build.VERSION.SDK_INT >= 23) { // Android M (6.0)
AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AUDIO_FORMAT)
.setSampleRate(SAMPLE_RATE)
.setChannelMask(CHANNEL_CONFIG)
.build()
)
.setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build()
} else {
@Suppress("DEPRECATION")
AudioTrack(
AudioManager.STREAM_MUSIC,
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT,
bufferSize,
AudioTrack.MODE_STREAM
)
}
// 检查初始化状态
if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) {
throw IllegalStateException("AudioTrack初始化失败")
}
// 开始播放
audioTrack?.play()
}
}

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

@ -3,217 +3,176 @@ package com.deep_voice.bytedance_speech
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.AudioDataListener
import java.util.concurrent.ConcurrentLinkedQueue
/** 火山引擎语音识别和语音合成插件 */ /**
* 字节跳动语音服务Flutter插件
* 提供TTS和ASR能力,通过独立的事件通道进行通信
*/
class BytedanceSpeechPlugin : FlutterPlugin { class BytedanceSpeechPlugin : FlutterPlugin {
private val TAG = "BytedanceSpeechPlugin" companion object {
private lateinit var context: Context private const val TAG = "BytedanceSpeechPlugin"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val mainHandler = Handler(Looper.getMainLooper()) // 通道名称
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 asrChannel: MethodChannel
private lateinit var asrEventChannel: EventChannel private lateinit var context: Context
private var asrEventSink: EventChannel.EventSink? = null
private var asrInstance: BytedanceASR? = null
// TTS相关 // TTS事件通道
private lateinit var ttsChannel: MethodChannel
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null private var ttsEventSink: EventChannel.EventSink? = null
private var ttsInstance: BytedanceTTS? = null private var ttsAudioSink: EventChannel.EventSink? = null
// ASR事件发送方法 // ASR事件通道
private fun sendAsrEvent(event: Map<String, Any>) { private var asrEventSink: EventChannel.EventSink? = null
println("发送ASR事件: $event")
if (asrEventSink == null) {
println("无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post {
try {
asrEventSink?.success(event)
println("ASR事件发送成功")
} catch (e: Exception) {
println("发送ASR事件失败: ${e.message}")
}
}
}
// TTS事件发送方法 // 语音服务实例
private fun sendTtsEvent(event: Map<String, Any>) { private var bytedanceTTS: BytedanceTTS? = null
println("发送TTS事件: $event") private var bytedanceASR: BytedanceASR? = null
if (ttsEventSink == null) {
println("无法发送TTS事件:事件通道未准备好") // 协程作用域
return private val job = SupervisorJob()
} private val scope = CoroutineScope(Dispatchers.Main + job)
mainHandler.post { // 主线程Handler
try { private val mainHandler = Handler(Looper.getMainLooper())
ttsEventSink?.success(event)
println("TTS事件发送成功")
} catch (e: Exception) {
println("发送TTS事件失败: ${e.message}")
}
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext context = flutterPluginBinding.applicationContext
// 初始化ASR通道 // 初始化TTS方法通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr") ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, TTS_METHOD_CHANNEL)
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR方法通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, ASR_METHOD_CHANNEL)
asrChannel.setMethodCallHandler(AsrMethodHandler()) asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道 // 设置TTS事件通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts") setupTtsEventChannels(flutterPluginBinding)
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR事件通道 // 设置ASR事件通道
asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr_events") setupAsrEventChannel(flutterPluginBinding)
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { Log.d(TAG, "字节跳动语音插件已附加到Flutter引擎")
asrEventSink = events }
}
/**
override fun onCancel(arguments: Any?) { * 设置TTS相关的事件通道
asrEventSink = null */
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事件通道 // TTS音频数据通道
ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts_events") EventChannel(binding.binaryMessenger, TTS_AUDIO_CHANNEL).setStreamHandler(
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler { object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events 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) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) { when (call.method) {
"initialize" -> { "initialize" -> {
val appId = call.argument<String>("appId") ?: "" val ttsAppId = call.argument<String>("ttsAppId") ?: ""
val accessToken = call.argument<String>("accessToken") ?: "" val ttsAppToken = call.argument<String>("ttsAppToken") ?: ""
val streamMode = call.argument<Boolean>("streamMode") ?: true val language = call.argument<String>("language") ?: "zh-CN"
val enablePunctuation = call.argument<Boolean>("enablePunctuation") ?: true
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val bits = call.argument<Int>("bits") ?: 16
val channels = call.argument<Int>("channels") ?: 1
try { initializeTTS(ttsAppId, ttsAppToken, language, result)
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)
}
} }
"startRecognition" -> { "setVoice" -> {
if (asrEventSink == null) { val voice = call.argument<String>("voice") ?: ""
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始识别", null) setVoice(voice, result)
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)
}
}
}
} }
"feedAudioData" -> { "speakOnce" -> {
val audioData = call.argument<ByteArray>("audioData") val text = call.argument<String>("text") ?: ""
if (audioData != null) { speakOnce(text, result)
scope.launch {
try {
asrInstance?.feedAudioData(audioData)
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("FEED_ERROR", e.message, null)
}
}
}
} else {
result.error("INVALID_ARGUMENT", "音频数据不能为空", null)
}
} }
"stopRecognition" -> { "speakStream" -> {
scope.launch { val text = call.argument<String>("text") ?: ""
try { speakStream(text, result)
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)
}
}
}
} }
"dispose" -> { "flushStream" -> {
try { flushStream(result)
asrInstance?.release() }
asrInstance = null "stopTTS" -> {
result.success(true) stopTTS(result)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
} }
"disposeTTS" -> {
disposeTTS(result)
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
@ -221,93 +180,23 @@ class BytedanceSpeechPlugin : FlutterPlugin {
} }
} }
// TTS方法处理器 /**
inner class TtsMethodHandler : MethodCallHandler { * ASR方法处理器
*/
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) { when (call.method) {
"initialize" -> { "initializeASR" -> {
val appId = call.argument<String>("appId") ?: "" result.notImplemented()
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)
}
} }
"synthesize" -> { "startListening" -> {
val text = call.argument<String>("text") ?: "" result.notImplemented()
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)
}
}
}
} }
"stopSynthesis" -> { "stopListening" -> {
scope.launch { result.notImplemented()
try {
ttsInstance?.stop()
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("STOP_ERROR", e.message, null)
}
}
}
} }
"dispose" -> { "cancelASR" -> {
try { result.notImplemented()
ttsInstance?.release()
ttsInstance = null
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
} }
else -> { else -> {
result.notImplemented() result.notImplemented()
@ -317,15 +206,274 @@ class BytedanceSpeechPlugin : FlutterPlugin {
} }
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null) ttsChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null) asrChannel.setMethodCallHandler(null)
ttsEventChannel.setStreamHandler(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)
// 添加事件监听器
bytedanceTTS?.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> {
sendTTSEvent("started", null)
}
TtsEventType.SYNTHESIS_COMPLETED -> {
sendTTSEvent("completed", null)
}
TtsEventType.SYNTHESIS_CANCELED -> {
sendTTSEvent("canceled", null)
}
TtsEventType.ERROR -> {
val params = HashMap<String, Any>()
params["code"] = event.params["errorCode"] ?: "UNKNOWN_ERROR"
params["message"] = event.params["errorMessage"] ?: "未知错误"
sendTTSEvent("error", params)
}
}
}
})
// 添加音频数据监听器
bytedanceTTS?.addAudioDataListener(object : AudioDataListener {
override fun onAudioData(data: ByteArray) {
sendTTSAudioData(data)
}
})
// 初始化TTS
val success = bytedanceTTS?.initialize(
ttsAppId = ttsAppId,
ttsAppToken = ttsAppToken,
ttsResource = "", // 火山TTS不使用resource
language = 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.cancel() try {
asrInstance?.release() val success = bytedanceTTS?.speakOnce(text) ?: false
ttsInstance?.release() result.success(success)
asrInstance = null } catch (e: Exception) {
ttsInstance = null 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
}
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
}
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
}
try {
val success = bytedanceTTS?.stop() ?: true
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "停止TTS异常: ${e.message}", e)
result.error(
"STOP_EXCEPTION",
"停止TTS异常: ${e.message}",
null
)
}
}
/**
* 释放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)
}
} }
} }

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

File diff suppressed because it is too large

5
local_plugins/classic_bluetooth/android/src/main/kotlin/com/yunqiinnovation/classic_bluetooth/ClassicBluetoothPlugin.kt

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

2
local_plugins/classic_bluetooth/pubspec.yaml

@ -9,8 +9,6 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
deepvoice_utils:
path: ../deepvoice_utils
flutter: flutter:
plugin: plugin:

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

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

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

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

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

@ -1,33 +0,0 @@
package com.yunqiinnovation.deepvoice_utils
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** DeepvoiceUtilsPlugin */
class DeepvoiceUtilsPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "deepvoice_utils")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

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

@ -1,237 +0,0 @@
package com.yunqiinnovation.deepvoice_utils
import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* 文件日志工具
* 将日志同时输出到Logcat和文件系统
*/
class FileLogger private constructor(context: Context) {
private val logDir: File
private var currentLogFile: File? = null
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private val timeFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
private val logExecutor: ExecutorService = Executors.newSingleThreadExecutor()
init {
// 初始化日志目录
logDir = File(context.getExternalFilesDir(null), "logs")
if (!logDir.exists()) {
logDir.mkdirs()
}
// 创建当天的日志文件
updateLogFile()
}
/**
* 更新日志文件为当天日期
*/
private fun updateLogFile() {
val today = dateFormat.format(Date())
val logFile = File(logDir, "log_$today.txt")
if (currentLogFile == null || currentLogFile?.absolutePath != logFile.absolutePath) {
currentLogFile = logFile
// 如果是新文件,添加文件头
if (!logFile.exists()) {
try {
logFile.createNewFile()
FileWriter(logFile, true).use { writer ->
writer.append("===== 日志开始: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())} =====\n")
}
} catch (e: IOException) {
Log.e(TAG, "创建日志文件失败: ${e.message}")
}
}
}
}
/**
* 调试级别日志
*/
fun d(tag: String, message: String) {
// 输出到Logcat
Log.d(tag, message)
// 异步写入文件
logToFile("D", tag, message)
}
/**
* 信息级别日志
*/
fun i(tag: String, message: String) {
// 输出到Logcat
Log.i(tag, message)
// 异步写入文件
logToFile("I", tag, message)
}
/**
* 警告级别日志
*/
fun w(tag: String, message: String) {
// 输出到Logcat
Log.w(tag, message)
// 异步写入文件
logToFile("W", tag, message)
}
/**
* 错误级别日志
*/
fun e(tag: String, message: String, throwable: Throwable? = null) {
// 输出到Logcat
if (throwable != null) {
Log.e(tag, message, throwable)
} else {
Log.e(tag, message)
}
// 异步写入文件
val errorMsg = if (throwable != null) {
"$message\n${throwable.message}\n${throwable.stackTraceToString()}"
} else {
message
}
logToFile("E", tag, errorMsg)
}
/**
* 将日志写入文件
*/
private fun logToFile(level: String, tag: String, message: String) {
logExecutor.execute {
try {
// 检查是否需要更新日志文件
updateLogFile()
// 格式化日志内容
val timestamp = timeFormat.format(Date())
val logLine = "$timestamp $level/$tag: $message\n"
// 写入文件
if (currentLogFile != null && currentLogFile!!.exists()) {
FileWriter(currentLogFile!!, true).use { writer ->
writer.append(logLine)
}
}
} catch (e: IOException) {
Log.e(TAG, "写入日志文件失败: ${e.message}")
}
}
}
/**
* 清理旧日志文件
* @param daysToKeep 保留的天数
*/
fun cleanOldLogs(daysToKeep: Int = 7) {
logExecutor.execute {
try {
val currentTime = System.currentTimeMillis()
val maxAge = daysToKeep * 24 * 60 * 60 * 1000L
logDir.listFiles()?.forEach { file ->
if (file.isFile && file.name.startsWith("log_") && file.name.endsWith(".txt")) {
val fileAge = currentTime - file.lastModified()
if (fileAge > maxAge) {
val deleted = file.delete()
if (deleted) {
Log.d(TAG, "已删除旧日志文件: ${file.name}")
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "清理旧日志失败: ${e.message}")
}
}
}
/**
* 关闭日志系统
*/
fun shutdown() {
logExecutor.shutdown()
}
companion object {
private const val TAG = "FileLogger"
private var INSTANCE: FileLogger? = null
/**
* 初始化日志系统
*/
@Synchronized
fun init(context: Context) {
if (INSTANCE == null) {
INSTANCE = FileLogger(context.applicationContext)
d(TAG, "文件日志系统已初始化")
}
}
/**
* 调试日志
*/
fun d(tag: String, message: String) {
INSTANCE?.d(tag, message) ?: Log.d(tag, message)
}
/**
* 信息日志
*/
fun i(tag: String, message: String) {
INSTANCE?.i(tag, message) ?: Log.i(tag, message)
}
/**
* 警告日志
*/
fun w(tag: String, message: String) {
INSTANCE?.w(tag, message) ?: Log.w(tag, message)
}
/**
* 错误日志
*/
fun e(tag: String, message: String, throwable: Throwable? = null) {
INSTANCE?.e(tag, message, throwable) ?: run {
if (throwable != null) {
Log.e(tag, message, throwable)
} else {
Log.e(tag, message)
}
}
}
/**
* 清理旧日志
*/
fun cleanOldLogs(daysToKeep: Int = 7) {
INSTANCE?.cleanOldLogs(daysToKeep)
}
/**
* 关闭日志系统
*/
fun shutdown() {
INSTANCE?.shutdown()
INSTANCE = null
}
}
}

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

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

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

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

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

@ -1,30 +0,0 @@
import Flutter
import UIKit
public class DeepvoiceUtilsPlugin: NSObject, FlutterPlugin {
private static let TAG = "DeepvoiceUtilsPlugin"
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "deepvoice_utils", binaryMessenger: registrar.messenger())
let instance = DeepvoiceUtilsPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
// 初始化文件日志
FileLogger.initialize()
FileLogger.d(TAG, "DeepvoiceUtilsPlugin 已初始化")
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS \(UIDevice.current.systemVersion)")
default:
result(FlutterMethodNotImplemented)
}
}
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
// 关闭日志系统
FileLogger.shutdown()
}
}

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

@ -1,286 +0,0 @@
import Foundation
import os.log
/**
* 文件日志工具
* 将日志同时输出到控制台和文件系统
*/
@objc public class FileLogger: NSObject {
private static let TAG = "FileLogger"
private static var instance: FileLogger?
private let logDir: URL
private var currentLogFile: URL?
private let logQueue = DispatchQueue(label: "com.yunqiinnovation.deepvoice_utils.logger", qos: .utility)
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss.SSS"
return formatter
}()
/**
* 初始化日志系统
*/
private init(directory: URL) {
self.logDir = directory.appendingPathComponent("logs", isDirectory: true)
super.init()
// 创建日志目录
try? FileManager.default.createDirectory(at: logDir, withIntermediateDirectories: true)
// 创建当天日志文件
updateLogFile()
// 记录初始化完成
self.d(FileLogger.TAG, "文件日志系统已初始化")
}
/**
* 更新日志文件为当天日期
*/
private func updateLogFile() {
let today = dateFormatter.string(from: Date())
let logFile = logDir.appendingPathComponent("log_\(today).txt")
if currentLogFile == nil || currentLogFile?.path != logFile.path {
currentLogFile = logFile
// 如果是新文件,添加文件头
if !FileManager.default.fileExists(atPath: logFile.path) {
let headerDate = DateFormatter()
headerDate.dateFormat = "yyyy-MM-dd HH:mm:ss"
let headerLine = "===== 日志开始: \(headerDate.string(from: Date())) =====\n"
try? headerLine.write(to: logFile, atomically: true, encoding: .utf8)
}
}
}
/**
* 将日志写入文件
*/
private func logToFile(level: String, tag: String, message: String) {
logQueue.async { [weak self] in
guard let self = self else { return }
do {
// 检查是否需要更新日志文件
self.updateLogFile()
guard let logFile = self.currentLogFile,
FileManager.default.fileExists(atPath: logFile.path) else {
return
}
// 格式化日志内容
let timestamp = self.timeFormatter.string(from: Date())
let logLine = "\(timestamp) \(level)/\(tag): \(message)\n"
// 获取文件句柄并追加内容
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
if let data = logLine.data(using: .utf8) {
fileHandle.write(data)
}
fileHandle.closeFile()
} else {
// 如果无法获取文件句柄,尝试直接写入
let existingContent = try? String(contentsOf: logFile, encoding: .utf8)
let newContent = (existingContent ?? "") + logLine
try newContent.write(to: logFile, atomically: true, encoding: .utf8)
}
} catch {
print("写入日志文件失败: \(error.localizedDescription)")
}
}
}
/**
* 调试级别日志
*/
private func d(_ tag: String, _ message: String) {
// 输出到控制台
print("D/\(tag): \(message)")
// 写入文件
logToFile(level: "D", tag: tag, message: message)
}
/**
* 信息级别日志
*/
private func i(_ tag: String, _ message: String) {
// 输出到控制台
print("I/\(tag): \(message)")
// 写入文件
logToFile(level: "I", tag: tag, message: message)
}
/**
* 警告级别日志
*/
private func w(_ tag: String, _ message: String) {
// 输出到控制台
print("W/\(tag): \(message)")
// 写入文件
logToFile(level: "W", tag: tag, message: message)
}
/**
* 错误级别日志
*/
private func e(_ tag: String, _ message: String, _ error: Error? = nil) {
var fullMessage = message
// 如果有错误,添加错误信息
if let error = error {
fullMessage += "\n\(error.localizedDescription)"
}
// 输出到控制台
print("E/\(tag): \(fullMessage)")
// 写入文件
logToFile(level: "E", tag: tag, message: fullMessage)
}
/**
* 清理旧日志文件
*/
private func cleanOldLogs(daysToKeep: Int = 7) {
logQueue.async {
do {
let currentDate = Date()
let fileManager = FileManager.default
let logFiles = try fileManager.contentsOfDirectory(at: self.logDir, includingPropertiesForKeys: [.creationDateKey], options: [])
for fileURL in logFiles where fileURL.pathExtension == "txt" {
if let fileAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path),
let creationDate = fileAttributes[.creationDate] as? Date {
let age = Calendar.current.dateComponents([.day], from: creationDate, to: currentDate)
if let days = age.day, days > daysToKeep {
try? fileManager.removeItem(at: fileURL)
print("D/\(FileLogger.TAG): 已删除旧日志文件: \(fileURL.lastPathComponent)")
}
}
}
} catch {
print("E/\(FileLogger.TAG): 清理旧日志失败: \(error.localizedDescription)")
}
}
}
// MARK: - 公共静态方法
/**
* 初始化日志系统
*/
@objc public static func initialize() {
if instance == nil {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
instance = FileLogger(directory: documentsDirectory)
}
}
/**
* 初始化日志系统(使用自定义目录)
*/
@objc public static func initialize(withDirectory directory: URL) {
if instance == nil {
instance = FileLogger(directory: directory)
}
}
/**
* 调试日志
*/
@objc public static func d(_ tag: String, _ message: String) {
// 如果实例不存在,先使用默认目录初始化
if instance == nil {
initialize()
}
instance?.d(tag, message)
// 使用系统日志
if #available(iOS 10.0, *) {
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .debug, message)
}
}
/**
* 信息日志
*/
@objc public static func i(_ tag: String, _ message: String) {
if instance == nil {
initialize()
}
instance?.i(tag, message)
// 使用系统日志
if #available(iOS 10.0, *) {
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .info, message)
}
}
/**
* 警告日志
*/
@objc public static func w(_ tag: String, _ message: String) {
if instance == nil {
initialize()
}
instance?.w(tag, message)
// 使用系统日志
if #available(iOS 10.0, *) {
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .default, message)
}
}
/**
* 错误日志
*/
@objc public static func e(_ tag: String, _ message: String, _ error: Error? = nil) {
if instance == nil {
initialize()
}
instance?.e(tag, message, error)
// 使用系统日志
if #available(iOS 10.0, *) {
let errorMessage = error != nil ? "\(message): \(error!.localizedDescription)" : message
os_log("%{public}@", log: OSLog(subsystem: "com.yunqiinnovation.deepvoice_utils", category: tag), type: .error, errorMessage)
}
}
/**
* 清理旧日志
*/
@objc public static func cleanOldLogs(daysToKeep: Int = 7) {
instance?.cleanOldLogs(daysToKeep: daysToKeep)
}
/**
* 关闭日志系统
*/
@objc public static func shutdown() {
instance = nil
}
}

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

@ -1,11 +0,0 @@
import Flutter
import UIKit
// 这个类是为了满足Flutter插件的命名约定
// 实际实现在DeepvoiceUtilsPlugin.swift中
public class SwiftDeepvoiceUtilsPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
// 调用真正的实现
DeepvoiceUtilsPlugin.register(with: registrar)
}
}

54
local_plugins/deepvoice_utils/ios/README.md

@ -1,54 +0,0 @@
# DeepVoice Utils iOS 实现
## 简介
本模块是 `deepvoice_utils` 插件的 iOS 实现部分,主要提供以下功能:
- 文件日志记录(FileLogger):将日志记录到文件,方便调试和问题排查
## 使用说明
### 日志系统
该插件自动初始化日志系统,开发者无需手动调用初始化方法。日志文件默认保存在应用的 Documents 目录下的 logs 文件夹中,按日期分文件存储。
## 配置要求
- iOS 11.0 或更高版本
- Swift 5.0
- 确保应用拥有文件写入权限
## 开发说明
### 文件结构
- `DeepvoiceUtilsPlugin.swift`:插件主类,负责与 Flutter 交互
- `FileLogger.swift`:日志系统实现,提供日志记录功能
- `SwiftDeepvoiceUtilsPlugin.swift`:命名约定桥接类
- `DeepvoiceUtilsPlugin.h/.m`:Objective-C 桥接文件
### 自定义日志目录
如需使用自定义日志目录,可以在应用启动时手动调用初始化方法:
```swift
import deepvoice_utils
// 在 AppDelegate 中
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 自定义日志目录
let customDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("custom_logs")
FileLogger.initialize(withDirectory: customDirectory)
return true
}
```
### 清理旧日志
默认情况下,日志系统不会自动清理旧日志。如果需要清理,可以手动调用:
```swift
// 清理7天前的日志
FileLogger.cleanOldLogs(daysToKeep: 7)
```

19
local_plugins/deepvoice_utils/ios/deepvoice_utils.podspec

@ -1,19 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
#
Pod::Spec.new do |s|
s.name = 'deepvoice_utils'
s.version = '0.0.1'
s.summary = '深语工具库'
s.description = <<-DESC
提供文件日志记录等通用功能的Flutter插件。
DESC
s.homepage = 'http://yunqiinnovation.com'
s.license = { :file => '../LICENSE' }
s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.ios.deployment_target = '11.0'
s.swift_version = '5.0'
end

3
local_plugins/deepvoice_utils/lib/deepvoice_utils.dart

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

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

@ -1,31 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
/// 文件日志记录器
///
/// 提供文件日志记录功能,支持简单的日志记录
class FileLogger {
/// 记录调试日志
static void d(String tag, String message) {
debugPrint('[$tag] DEBUG: $message');
}
/// 记录信息日志
static void i(String tag, String message) {
debugPrint('[$tag] INFO: $message');
}
/// 记录警告日志
static void w(String tag, String message) {
debugPrint('[$tag] WARNING: $message');
}
/// 记录错误日志
static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
final errorMsg = error != null ? '$message: $error' : message;
debugPrint('[$tag] ERROR: $errorMsg');
if (stackTrace != null) {
debugPrint(stackTrace.toString());
}
}
}

27
local_plugins/deepvoice_utils/pubspec.yaml

@ -1,27 +0,0 @@
name: deepvoice_utils
description: 通用工具库,包含日志、文件处理等功能
version: 0.0.1
environment:
sdk: ">=2.17.0 <4.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
logger: ^2.0.0
path_provider: ^2.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.deepvoice_utils
pluginClass: DeepvoiceUtilsPlugin
ios:
pluginClass: DeepvoiceUtilsPlugin

1
local_plugins/music_service/android/build.gradle

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

25
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

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

4
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

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

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock

Binary file not shown.

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -1,2 +1,2 @@
#Sun May 11 13:39:53 IST 2025 #Fri May 16 09:26:08 IST 2025
gradle.version=8.10 gradle.version=8.10

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

@ -4,11 +4,12 @@ plugins {
// Kotlin Android 插件 // Kotlin Android 插件
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1" id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("plugin.serialization") version "1.9.24"
} }
android { android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致) // 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.deepvoice_utils" namespace = "com.deep_voice.speech"
// 目标 SDK 版本 // 目标 SDK 版本
compileSdk = 35 compileSdk = 35
@ -16,7 +17,7 @@ android {
defaultConfig { defaultConfig {
// 最低 SDK 版本 // 最低 SDK 版本
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 33
} }
// Java 语言级别兼容配置 // Java 语言级别兼容配置
@ -44,3 +45,20 @@ android {
} }
} }
} }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")
implementation("com.anthropic:anthropic-java:0.8.0")
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
implementation("io.ktor:ktor-client-core:3.1.2")
implementation("io.ktor:ktor-client-cio:3.1.2")
implementation("io.ktor:ktor-client-okhttp:3.1.2")
implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("androidx.media:media:1.6.0")
}

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

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

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 是否初始化成功
*/
fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String = "", language: String = "zh-CN"): Boolean
/**
* 设置语音角色
*
* @param voiceName 语音角色名称(不同服务的语音角色命名可能不同)
* @return 是否设置成功
*/
fun setVoice(voiceName: String): Boolean
/**
* 单次播放文本(非流式)
*
* @param text 要合成的文本
* @return 是否成功开始合成
*/
fun speakOnce(text: String): Boolean
/**
* 流式合成
* 在流式场景下合成并播放文本,适用于连续的文本输入
*
* @param text 要合成的文本片段
* @return 是否成功处理
*/
fun speakStream(text: String): Boolean
/**
* 刷新并播放流式文本缓冲区中的剩余内容
*
* @return 是否成功处理
*/
fun flushStream(): Boolean
/**
* 停止当前合成和播放
*
* @return 是否成功停止
*/
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)
}

55
local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/TtsEvents.kt

@ -0,0 +1,55 @@
package com.deep_voice.speech.tts
/**
* TTS事件类型枚举
*/
enum class TtsEventType {
/** 开始合成 */
SYNTHESIS_STARTED,
/** 合成完成 */
SYNTHESIS_COMPLETED,
/** 合成取消 */
SYNTHESIS_CANCELED,
/** 发生错误 */
ERROR
}
/**
* TTS事件数据类
*
* @param type 事件类型
* @param params 事件参数,根据不同的事件类型包含不同的内容
* - ERROR: 包含 "errorCode" 和 "errorMessage"
* - CANCELED: 包含 "reason" 和 "errorDetails"
*/
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 { buildscript {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:7.3.0") classpath("com.android.tools.build:gradle:7.3.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.21")
}
}
allprojects {
repositories {
google()
mavenCentral()
} }
} }
@ -24,10 +14,9 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
} }
// 配置android扩展 android {
configure<LibraryExtension> {
namespace = "com.yunqiinnovation.volcano_speech" namespace = "com.yunqiinnovation.volcano_speech"
compileSdkVersion(35) compileSdk = 35
defaultConfig { defaultConfig {
minSdk = 21 minSdk = 21
@ -38,6 +27,10 @@ configure<LibraryExtension> {
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions {
jvmTarget = "11"
}
sourceSets { sourceSets {
getByName("main") { getByName("main") {
manifest.srcFile("src/main/AndroidManifest.xml") manifest.srcFile("src/main/AndroidManifest.xml")
@ -49,17 +42,20 @@ configure<LibraryExtension> {
lintOptions { lintOptions {
isCheckReleaseBuilds = false isCheckReleaseBuilds = false
} }
}
// 显式设置Kotlin JVM目标版本
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
}
} }
dependencies { dependencies {
// Flutter插件依赖
compileOnly("io.flutter:flutter_embedding_debug:1.0.0-a794cf2681")
// 添加火山引擎语音合成SDK // 添加火山引擎语音合成SDK
// implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7") // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")
// implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7")
}
repositories {
google()
mavenCentral()
} }

134
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt

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

20
local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt

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

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

6
pubspec.yaml

@ -81,14 +81,16 @@ dependencies:
path: local_plugins/jl_ota path: local_plugins/jl_ota
ble_service: ble_service:
path: local_plugins/ble_service path: local_plugins/ble_service
deepvoice_utils:
path: local_plugins/deepvoice_utils
classic_bluetooth: classic_bluetooth:
path: local_plugins/classic_bluetooth path: local_plugins/classic_bluetooth
agent_service: agent_service:
path: local_plugins/agent_service path: local_plugins/agent_service
music_service: music_service:
path: local_plugins/music_service path: local_plugins/music_service
volcano_speech:
path: local_plugins/volcano_speech
bytedance_speech:
path: local_plugins/bytedance_speech
dev_dependencies: dev_dependencies:

Loading…
Cancel
Save