Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
edbc81b809
  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 {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":spotify_sdk"))
implementation(project(":deepvoice_utils"))
implementation(project(":ble_service"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
}
flutter {

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

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

2
android/build.gradle.kts

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

9
android/settings.gradle.kts

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

4
lib/core/themes/theme_service.dart

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

85
lib/core/utils/logger.dart

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

43
lib/data/services/ble_manager.dart

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

8
lib/data/services/bluetooth_manager.dart

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

2
lib/data/services/chat_history_service.dart

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

6
lib/data/services/deapsound_ai_service.dart

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

6
lib/data/services/kouzi_ai_service.dart

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

4
lib/data/services/speech_factory.dart

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

12
local_plugins/agent_service/pubspec.yaml

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

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

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

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.Result
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
/** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
class AzureSpeechPlugin: FlutterPlugin {
private val tag = "AzureSpeechPlugin"
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ASR相关
private lateinit var asrChannel : MethodChannel
@ -38,9 +33,8 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已注册BLE回调
private var isRegisteredToBle = false
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
@ -66,7 +60,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
}
// 确保在主线程上发送事件
mainHandler.post {
try {
ttsEventSink?.success(event)
@ -104,6 +97,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
}
override fun onCancel(arguments: Any?) {
@ -114,19 +108,53 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
}
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(tag, "已注册BLE回调")
} else {
FileLogger.w(tag, "BLE回调注册失败")
}
} else {
FileLogger.e(tag, "BLE服务初始化失败")
/**
* 设置TTS事件监听器
*/
private fun setupTtsEventListener() {
if (!isTtsListenerAdded) {
// 添加TTS事件监听器
azureTtsHelper.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
// 将TTS事件转换为Flutter可理解的格式
val eventMap = when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> mapOf(
"type" to "synthesis_started"
)
TtsEventType.SYNTHESIS_COMPLETED -> mapOf(
"type" to "synthesis_completed"
)
TtsEventType.SYNTHESIS_CANCELED -> {
val params = event.params
mapOf(
"type" to "synthesis_canceled",
"reason" to (params["reason"] ?: ""),
"errorDetails" to (params["errorDetails"] ?: "")
)
}
TtsEventType.ERROR -> {
val params = event.params
mapOf(
"type" to "error",
"errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"),
"errorMessage" to (params["errorMessage"] ?: "未知错误")
)
}
else -> mapOf(
"type" to event.type.toString().lowercase(),
"params" to event.params
)
}
// 发送事件
sendTtsEvent(eventMap)
}
})
isTtsListenerAdded = true
}
}
// ASR方法处理器
@ -265,6 +293,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
azureAsrHelper.dispose()
result.success(true)
}
"pushAudioData" -> {
val data = call.argument<ByteArray>("data") ?: return result.error("INVALID_ARGUMENTS", "音频数据不能为空", null)
azureAsrHelper.pushAudioData(data)
result.success(true)
}
else -> {
result.notImplemented()
}
@ -279,34 +312,36 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false // 默认使用自定义播放器
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false
// 先释放现有资源
azureTtsHelper.dispose()
// 创建TTS回调
val ttsCallback = createTtsCallback()
// 初始化TTS引擎
val success = azureTtsHelper.initialize(
subscriptionKey = subscriptionKey,
region = region,
callback = ttsCallback
)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
try {
// 初始化TTS引擎(使用新的接口方法)
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = subscriptionKey,
ttsResource = region,
language = "zh-CN"
)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
}
}
// 确保设置了TTS事件监听器
setupTtsEventListener()
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null)
}
result.success(success)
}
"setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
@ -315,18 +350,28 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"speakText" -> {
val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null)
// 直接传递参数给speakText方法
val success = azureTtsHelper.speakText(text)
result.success(success)
// 直接调用speakOnce方法
try {
val success = azureTtsHelper.speakOnce(text)
result.success(success)
} catch (e: Exception) {
result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null)
}
}
"stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking())
// 直接调用stop方法
try {
val success = azureTtsHelper.stop()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null)
}
}
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
"dispose" -> {
azureTtsHelper.dispose()
azureTtsHelper.release()
result.success(true)
}
else -> {
@ -334,37 +379,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
}
}
}
/**
* 创建TTS回调
*/
private fun createTtsCallback(): AzureTtsHelper.TtsCallback {
return object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
sendTtsEvent(mapOf(
"type" to "synthesis_started"
))
}
override fun onSynthesizing() {
sendTtsEvent(mapOf(
"type" to "synthesizing"
))
}
override fun onSynthesisCompleted() {
sendTtsEvent(mapOf(
"type" to "synthesis_completed"
))
}
override fun onSynthesisCanceled() {
sendTtsEvent(mapOf(
"type" to "synthesis_canceled"
))
}
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@ -374,49 +388,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(null)
try {
// 取消所有协程
job.cancel()
// 取消BLE回调注册
if (isRegisteredToBle) {
BleService.removeCallback(this)
isRegisteredToBle = false
}
// 释放资源
azureTtsHelper.dispose()
azureTtsHelper.release()
azureAsrHelper.dispose()
} catch (e: Exception) {
FileLogger.e(tag, "释放资源失败: ${e.message}")
}
}
override fun onScanResult(devices: List<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.audio.*
import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.deep_voice.speech.tts.ITtsService
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.*
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlin.coroutines.CoroutineContext
/**
* Azure TTS Helper
@ -15,18 +19,13 @@ import kotlin.coroutines.CoroutineContext
* 基于微软Azure语音服务的TTS实现
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/
class AzureTtsHelper(private val context: Context) : CoroutineScope {
class AzureTtsHelper(private val context: Context) : ITtsService {
companion object {
private const val TAG = "AzureTtsHelper"
private const val DEFAULT_LANGUAGE = "zh-CN"
private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
}
// 协程上下文
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// Azure语音服务配置
private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null
@ -39,8 +38,9 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
private var currentPitch = "0%"
private var currentVolume = "100%"
// 全局回调
private var ttsCallback: TtsCallback? = null
// 事件监听器列表
private val eventListeners = mutableListOf<TtsEventListener>()
private val audioDataListeners = mutableListOf<AudioDataListener>()
// 自定义音频输出流
private var customAudioOutputStream: PushAudioOutputStream? = null
@ -48,54 +48,28 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 流式文本处理的缓冲区
private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L
/**
* 简化的TTS回调接口
*/
interface TtsCallback {
/**
* 合成开始
*/
fun onSynthesisStarted() {}
/**
* 合成中
*/
fun onSynthesizing() {}
/**
* 合成完成
*/
fun onSynthesisCompleted() {}
/**
* 合成取消
*/
fun onSynthesisCanceled() {}
}
/**
* 初始化TTS引擎
*
* @param subscriptionKey Azure语音服务订阅密钥
* @param region Azure语音服务区域
* @param callback 可选,全局TTS回调接口
* @param ttsAppId 未使用,保留为空即可
* @param ttsAppToken Azure语音服务订阅密钥
* @param ttsResource Azure语音服务区域
* @param language 语言代码,如"zh-CN"
* @return 是否初始化成功
*/
fun initialize(
subscriptionKey: String,
region: String,
callback: TtsCallback? = null
override fun initialize(
ttsAppId: String,
ttsAppToken: String,
ttsResource: String,
language: String
): Boolean {
try {
// 释放之前的资源
dispose()
// 存储全局回调
ttsCallback = callback
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region)
speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
@ -115,7 +89,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners()
isInitialized = true
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$region")
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource")
return true
} catch (e: Exception) {
@ -148,12 +122,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
}
/**
* 设置TTS回调
*
* @param callback TTS回调接口
* 添加TTS事件监听器
*/
override fun addListener(listener: TtsEventListener) {
if (!eventListeners.contains(listener)) {
eventListeners.add(listener)
}
}
/**
* 移除TTS事件监听器
*/
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?) {
ttsCallback = callback
private fun notifyEvent(eventType: TtsEventType, params: Map<String, Any> = emptyMap()) {
val event = TtsEvent(eventType, params)
// 复制列表防止并发修改异常
val listeners = ArrayList(eventListeners)
for (listener in listeners) {
try {
listener.onEvent(event)
} catch (e: Exception) {
FileLogger.e(TAG, "通知事件异常: ${e.message}")
}
}
}
/**
@ -163,33 +176,29 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
synthesizer?.apply {
// 合成开始事件
SynthesisStarted?.addEventListener { _, _ ->
ttsCallback?.onSynthesisStarted()
}
// 合成中事件
Synthesizing?.addEventListener { _, _ ->
ttsCallback?.onSynthesizing()
notifyEvent(TtsEventType.SYNTHESIS_STARTED)
}
// 合成完成事件
SynthesisCompleted?.addEventListener { _, _ ->
isSpeaking = false
ttsCallback?.onSynthesisCompleted()
notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
}
// 合成取消事件
SynthesisCanceled?.addEventListener { _, _ ->
SynthesisCanceled?.addEventListener { _, eventArgs ->
FileLogger.d(TAG, "语音合成取消")
isSpeaking = false
ttsCallback?.onSynthesisCanceled()
notifyEvent(TtsEventType.SYNTHESIS_CANCELED)
}
}
}
/**
* 设置语音
* 设置语音角色
*/
fun setVoice(voiceName: String): Boolean {
override fun setVoice(voiceName: String): Boolean {
if (!isInitialized) return false
try {
@ -199,6 +208,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "VOICE_SET_FAILED",
"errorMessage" to "设置语音失败: ${e.message}"
))
return false
}
}
@ -224,6 +237,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners()
} catch (e: Exception) {
FileLogger.e(TAG, "重新创建合成器失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "RECREATE_FAILED",
"errorMessage" to "重新创建合成器失败: ${e.message}"
))
}
}
@ -240,6 +257,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "PARAMS_SET_FAILED",
"errorMessage" to "设置语音参数失败: ${e.message}"
))
return false
}
}
@ -255,50 +276,52 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
* 生成SSML
*/
private fun generateSsml(rawText: String): String {
// 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf(
"#", "*", "@", "%", "^", "&",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
)
// 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf(
"#", "*", "@", "%", "^", "&",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
)
// 2. 转义 XML 保留字符
val escapedText = rawText
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
// 2. 转义 XML 保留字符
val escapedText = rawText
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
// 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空格
var processedText = escapedText
symbolsToMute.forEach { sym ->
processedText = processedText.replace(sym, "")
}
// 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空格
var processedText = escapedText
symbolsToMute.forEach { sym ->
processedText = processedText.replace(sym, "")
}
// 4. 构造简化的SSML文档,减少嵌套层级
return """
<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN">
<voice name="$currentVoice">
<mstts:express-as style="cheerful">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
<say-as interpret-as="text">$processedText</say-as>
</prosody>
</mstts:express-as>
</voice>
</speak>
""".trimIndent()
}
// 4. 构造简化的SSML文档,减少嵌套层级
return """
<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN">
<voice name="$currentVoice">
<mstts:express-as style="cheerful">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
<say-as interpret-as="text">$processedText</say-as>
</prosody>
</mstts:express-as>
</voice>
</speak>
""".trimIndent()
}
/**
* 合成文本为语音
*
* @param text 要合成的文本
* @return 是否成功开始合成
* 单次播放文本(非流式)
*/
fun speakText(text: String): Boolean {
override fun speakOnce(text: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "TTS引擎未初始化")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false
}
@ -308,38 +331,47 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = true
// 使用纯异步方式
// 直接调用SpeakSsmlAsync,SDK内部已经是异步的
synthesizer?.SpeakSsmlAsync(ssml)
// 返回true表示已开始合成请求
return true
} catch (e: Exception) {
isSpeaking = false
FileLogger.e(TAG, "语音合成异常: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "SYNTHESIS_ERROR",
"errorMessage" to "语音合成异常: ${e.message}"
))
return false
}
}
/**
* 处理流式文本并在遇到标点符号时播放
*
* @param text 收到的文本流片段
* @return 是否成功处理
* 流式合成文本
*/
fun speakStream(text: String): Boolean {
override fun speakStream(text: String): Boolean {
if (!isInitialized || text.isBlank()) {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
}
return false
}
try {
// 添加新文本到缓冲区
streamBuffer.append(text)
// 增加500ms防抖逻辑
val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 600) {
return true
}
lastSpeakTime = currentTime
val currentText = streamBuffer.toString()
// 定义标点符号列表
@ -363,24 +395,30 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
streamBuffer.delete(0, lastPunctuationIndex + 1)
// 播放提取的文本
return speakText(textToSpeak)
return speakOnce(textToSpeak)
}
// 如果没有找到标点符号,则等待更多文本
return true
} catch (e: Exception) {
FileLogger.e(TAG, "流式语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STREAM_FAILED",
"errorMessage" to "流式语音合成失败: ${e.message}"
))
return false
}
}
/**
* 播放剩余的流式文本
*
* @return 是否成功播放剩余文本
*/
fun flushStream(): Boolean {
override fun flushStream(): Boolean {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false
}
@ -397,9 +435,13 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
}
// 播放剩余文本
return speakText(remainingText)
return speakOnce(remainingText)
} catch (e: Exception) {
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "FLUSH_FAILED",
"errorMessage" to "刷新流式文本失败: ${e.message}"
))
return false
}
}
@ -407,7 +449,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/**
* 停止当前语音合成
*/
fun stopSpeaking(): Boolean {
override fun stop(): Boolean {
if (!isInitialized) return false
try {
@ -416,6 +458,11 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = false
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STOP_FAILED",
"errorMessage" to "停止语音合成失败: ${e.message}"
))
return false
}
}
@ -423,11 +470,25 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/**
* 释放资源
*/
fun dispose() {
override fun release() {
dispose()
}
/**
* 释放资源(内部方法)
*/
private fun dispose() {
try {
stopSpeaking()
// 停止播放
stop()
// 关闭合成器
synthesizer?.close()
synthesizer = null
// 关闭配置
speechConfig?.close()
speechConfig = null
// 关闭自定义音频输出流
try {
@ -437,15 +498,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 忽略关闭异常
}
// 清理引用
ttsCallback = null
// 清空流缓冲区
streamBuffer.clear()
// 取消所有协程
job.cancel()
// 重置状态
isInitialized = false
isSpeaking = false
} 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.ParcelUuid
import androidx.annotation.RequiresApi
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import java.util.regex.Pattern
/**
@ -71,7 +71,7 @@ class BleCompanionManager(private val context: Context) {
*/
fun savePairedMacAddress(macAddress: String) {
sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply()
FileLogger.i(TAG, "已保存配对设备MAC: $macAddress")
Log.i(TAG, "已保存配对设备MAC: $macAddress")
}
/**
@ -79,7 +79,7 @@ class BleCompanionManager(private val context: Context) {
*/
fun clearPairedMacAddress() {
sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply()
FileLogger.i(TAG, "已清除配对设备MAC")
Log.i(TAG, "已清除配对设备MAC")
}
/**
@ -110,7 +110,7 @@ class BleCompanionManager(private val context: Context) {
idField.get(association).toString()
} ?: emptyList()
} catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e)
Log.e(TAG, "获取关联ID失败: ${e.message}", e)
emptyList()
}
} else {
@ -135,7 +135,7 @@ class BleCompanionManager(private val context: Context) {
macAddress: String? = null
): Boolean {
if (!isCompanionDeviceManagerSupported()) {
FileLogger.e(TAG, "设备不支持CompanionDeviceManager")
Log.e(TAG, "设备不支持CompanionDeviceManager")
return false
}
@ -148,7 +148,7 @@ class BleCompanionManager(private val context: Context) {
// 使用"^设备名称$"格式创建精确匹配的正则表达式
val exactNamePattern = Pattern.compile("^$it$")
deviceFilterBuilder.setNamePattern(exactNamePattern)
FileLogger.i(TAG, "设置严格名称匹配: $it")
Log.i(TAG, "设置严格名称匹配: $it")
}
// 添加服务UUID
@ -156,9 +156,9 @@ class BleCompanionManager(private val context: Context) {
try {
val uuid = ParcelUuid(java.util.UUID.fromString(it))
deviceFilterBuilder.addServiceUuid(uuid, null)
FileLogger.i(TAG, "设置服务UUID匹配: $it")
Log.i(TAG, "设置服务UUID匹配: $it")
} catch (e: Exception) {
FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e)
Log.e(TAG, "设置服务UUID失败: ${e.message}", e)
}
}
@ -167,7 +167,7 @@ class BleCompanionManager(private val context: Context) {
try {
// 检查是否是有效的MAC地址格式
if (!isValidMacAddress(it)) {
FileLogger.e(TAG, "无效的MAC地址格式: $it")
Log.e(TAG, "无效的MAC地址格式: $it")
return false
}
@ -176,15 +176,15 @@ class BleCompanionManager(private val context: Context) {
// 设置MAC地址严格匹配
deviceFilterBuilder.setAddress(it)
FileLogger.i(TAG, "设置MAC地址严格匹配: $it")
Log.i(TAG, "设置MAC地址严格匹配: $it")
} catch (e: Exception) {
FileLogger.e(TAG, "设置MAC地址失败: ${e.message}", e)
Log.e(TAG, "设置MAC地址失败: ${e.message}", e)
return false
}
} ?: run {
// 如果没有提供MAC地址也没有提供设备名称,则返回错误
if (deviceNamePattern == null && serviceUuid == null) {
FileLogger.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一")
Log.e(TAG, "严格匹配模式下必须提供MAC地址、设备名称或服务UUID之一")
return false
}
}
@ -209,9 +209,9 @@ class BleCompanionManager(private val context: Context) {
0,
0
)
FileLogger.i(TAG, "已启动设备选择对话框")
Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) {
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
}
}
@ -228,28 +228,28 @@ class BleCompanionManager(private val context: Context) {
0,
0
)
FileLogger.i(TAG, "已启动设备选择对话框")
Log.i(TAG, "已启动设备选择对话框")
} catch (e: Exception) {
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
Log.e(TAG, "启动设备选择对话框失败: ${e.message}", e)
}
}
// API 33+ (Android 13+) 使用 onAssociationCreated
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onAssociationCreated(associationInfo: AssociationInfo) {
FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}")
Log.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}")
// 保存关联ID
try {
val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress")
val mac = macAddressMethod.invoke(associationInfo) as? String
mac?.let { savePairedMacAddress(it) }
} catch (e: Exception) {
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
Log.e(TAG, "获取设备MAC地址失败: ${e.message}", e)
}
}
override fun onFailure(error: CharSequence?) {
FileLogger.e(TAG, "设备关联失败: $error")
Log.e(TAG, "设备关联失败: $error")
}
},
null
@ -257,7 +257,7 @@ class BleCompanionManager(private val context: Context) {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e)
Log.e(TAG, "启动设备关联失败: ${e.message}", e)
return false
}
}
@ -276,7 +276,7 @@ class BleCompanionManager(private val context: Context) {
return false
}
FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode")
Log.i(TAG, "收到关联结果: resultCode=$resultCode")
if (resultCode == Activity.RESULT_OK && data != null) {
// 获取选中的蓝牙设备
@ -295,7 +295,7 @@ class BleCompanionManager(private val context: Context) {
val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID"
data.getStringExtra(extraAssociationIdKey)
} catch (e: Exception) {
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e)
Log.e(TAG, "获取关联ID失败: ${e.message}", e)
null
}
} else {
@ -309,20 +309,20 @@ class BleCompanionManager(private val context: Context) {
// 验证MAC地址格式
if (!isValidMacAddress(deviceMac)) {
FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac")
Log.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac")
return false
}
// 检查是否与预期的MAC地址匹配
if (pendingMacAddress != null && pendingMacAddress != deviceMac) {
FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配")
Log.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($pendingMacAddress) 不匹配")
// 如果要严格匹配MAC,可以在这里返回false
return false
}
// 匹配成功后,保存新的MAC地址
savePairedMacAddress(deviceMac)
FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId")
Log.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId")
// 清除临时MAC地址
pendingMacAddress = null
@ -330,10 +330,10 @@ class BleCompanionManager(private val context: Context) {
return true
} else {
FileLogger.e(TAG, "无法获取关联的蓝牙设备信息")
Log.e(TAG, "无法获取关联的蓝牙设备信息")
}
} else {
FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode")
Log.w(TAG, "设备关联被取消或失败: resultCode=$resultCode")
// 清除临时MAC地址
pendingMacAddress = null
}
@ -350,7 +350,7 @@ class BleCompanionManager(private val context: Context) {
try {
val mac = getPairedMacAddress()
if (mac.isNullOrEmpty()) {
FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址")
Log.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址")
return
}
@ -359,7 +359,7 @@ class BleCompanionManager(private val context: Context) {
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器")
Log.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器")
return
}
@ -391,9 +391,9 @@ class BleCompanionManager(private val context: Context) {
// 启动后台扫描
bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent)
FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac")
Log.i(TAG, "已注册后台扫描,目标设备MAC: $mac")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e)
Log.e(TAG, "注册后台扫描失败: ${e.message}", e)
}
}
@ -407,7 +407,7 @@ class BleCompanionManager(private val context: Context) {
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器")
Log.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器")
return
}
@ -428,9 +428,9 @@ class BleCompanionManager(private val context: Context) {
// 停止后台扫描
bluetoothLeScanner.stopScan(pendingIntent)
FileLogger.i(TAG, "已取消后台扫描")
Log.i(TAG, "已取消后台扫描")
} catch (e: Exception) {
FileLogger.e(TAG, "取消后台扫描失败: ${e.message}", e)
Log.e(TAG, "取消后台扫描失败: ${e.message}", e)
}
}
@ -454,13 +454,13 @@ class BleCompanionManager(private val context: Context) {
} else {
companionDeviceManager?.disassociate(id)
}
FileLogger.i(TAG, "已解除设备关联: $id")
Log.i(TAG, "已解除设备关联: $id")
} catch (e: Exception) {
FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e)
Log.e(TAG, "解除设备关联失败: $id, ${e.message}", e)
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e)
Log.e(TAG, "清除所有关联失败: ${e.message}", e)
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import 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 {
private val TAG = "BytedanceSpeechPlugin"
private lateinit var context: Context
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val mainHandler = Handler(Looper.getMainLooper())
companion object {
private const val TAG = "BytedanceSpeechPlugin"
// 通道名称
private const val TTS_METHOD_CHANNEL = "com.deep_voice.bytedance_tts"
private const val ASR_METHOD_CHANNEL = "com.deep_voice.bytedance_asr"
// 事件通道
private const val TTS_EVENT_CHANNEL = "com.deep_voice.bytedance_tts/events"
private const val TTS_AUDIO_CHANNEL = "com.deep_voice.bytedance_tts/audio"
private const val ASR_EVENT_CHANNEL = "com.deep_voice.bytedance_asr/events"
}
// ASR相关
// Flutter插件相关
private lateinit var ttsChannel: MethodChannel
private lateinit var asrChannel: MethodChannel
private lateinit var asrEventChannel: EventChannel
private var asrEventSink: EventChannel.EventSink? = null
private var asrInstance: BytedanceASR? = null
private lateinit var context: Context
// TTS相关
private lateinit var ttsChannel: MethodChannel
private lateinit var ttsEventChannel: EventChannel
// TTS事件通道
private var ttsEventSink: EventChannel.EventSink? = null
private var ttsInstance: BytedanceTTS? = null
private var ttsAudioSink: EventChannel.EventSink? = null
// ASR事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
println("发送ASR事件: $event")
if (asrEventSink == null) {
println("无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post {
try {
asrEventSink?.success(event)
println("ASR事件发送成功")
} catch (e: Exception) {
println("发送ASR事件失败: ${e.message}")
}
}
}
// ASR事件通道
private var asrEventSink: EventChannel.EventSink? = null
// TTS事件发送方法
private fun sendTtsEvent(event: Map<String, Any>) {
println("发送TTS事件: $event")
if (ttsEventSink == null) {
println("无法发送TTS事件:事件通道未准备好")
return
}
mainHandler.post {
try {
ttsEventSink?.success(event)
println("TTS事件发送成功")
} catch (e: Exception) {
println("发送TTS事件失败: ${e.message}")
}
}
}
// 语音服务实例
private var bytedanceTTS: BytedanceTTS? = null
private var bytedanceASR: BytedanceASR? = null
// 协程作用域
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.Main + job)
// 主线程Handler
private val mainHandler = Handler(Looper.getMainLooper())
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
// 初始化ASR通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr")
// 初始化TTS方法通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, TTS_METHOD_CHANNEL)
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR方法通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, ASR_METHOD_CHANNEL)
asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts")
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 设置TTS事件通道
setupTtsEventChannels(flutterPluginBinding)
// 初始化ASR事件通道
asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/asr_events")
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
}
override fun onCancel(arguments: Any?) {
asrEventSink = null
// 设置ASR事件通道
setupAsrEventChannel(flutterPluginBinding)
Log.d(TAG, "字节跳动语音插件已附加到Flutter引擎")
}
/**
* 设置TTS相关的事件通道
*/
private fun setupTtsEventChannels(binding: FlutterPlugin.FlutterPluginBinding) {
// TTS事件通道
EventChannel(binding.binaryMessenger, TTS_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
Log.d(TAG, "TTS事件流已建立连接")
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
Log.d(TAG, "TTS事件流已取消")
}
}
})
)
// 初始化TTS事件通道
ttsEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "bytedance_speech/tts_events")
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
// TTS音频数据通道
EventChannel(binding.binaryMessenger, TTS_AUDIO_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsAudioSink = events
Log.d(TAG, "TTS音频流已建立连接")
}
override fun onCancel(arguments: Any?) {
ttsAudioSink = null
Log.d(TAG, "TTS音频流已取消")
}
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
)
}
/**
* 设置ASR相关的事件通道
*/
private fun setupAsrEventChannel(binding: FlutterPlugin.FlutterPluginBinding) {
// ASR事件通道
EventChannel(binding.binaryMessenger, ASR_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
Log.d(TAG, "ASR事件流已建立连接")
}
override fun onCancel(arguments: Any?) {
asrEventSink = null
Log.d(TAG, "ASR事件流已取消")
}
}
})
)
}
// ASR方法处理器
inner class AsrMethodHandler : MethodCallHandler {
/**
* TTS方法处理器
*/
inner class TtsMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val appId = call.argument<String>("appId") ?: ""
val accessToken = call.argument<String>("accessToken") ?: ""
val streamMode = call.argument<Boolean>("streamMode") ?: true
val enablePunctuation = call.argument<Boolean>("enablePunctuation") ?: true
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val bits = call.argument<Int>("bits") ?: 16
val channels = call.argument<Int>("channels") ?: 1
val ttsAppId = call.argument<String>("ttsAppId") ?: ""
val ttsAppToken = call.argument<String>("ttsAppToken") ?: ""
val language = call.argument<String>("language") ?: "zh-CN"
try {
asrInstance = BytedanceASR(
context = context,
appId = appId,
accessToken = accessToken,
streamMode = streamMode,
enablePunctuation = enablePunctuation,
sampleRate = sampleRate,
bits = bits,
channels = channels,
resultCallback = { text, isFinal ->
val eventType = if (isFinal) "result" else "recognizing"
sendAsrEvent(mapOf(
"type" to eventType,
"text" to text,
"isFinal" to isFinal
))
},
errorCallback = { errorCode, errorMessage ->
sendAsrEvent(mapOf(
"type" to "error",
"errorCode" to errorCode,
"errorMessage" to errorMessage
))
}
)
result.success(true)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
initializeTTS(ttsAppId, ttsAppToken, language, result)
}
"startRecognition" -> {
if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始识别", null)
return
}
scope.launch {
try {
asrInstance?.start()
sendAsrEvent(mapOf("type" to "sessionStarted"))
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("START_ERROR", e.message, null)
}
}
}
"setVoice" -> {
val voice = call.argument<String>("voice") ?: ""
setVoice(voice, result)
}
"feedAudioData" -> {
val audioData = call.argument<ByteArray>("audioData")
if (audioData != null) {
scope.launch {
try {
asrInstance?.feedAudioData(audioData)
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("FEED_ERROR", e.message, null)
}
}
}
} else {
result.error("INVALID_ARGUMENT", "音频数据不能为空", null)
}
"speakOnce" -> {
val text = call.argument<String>("text") ?: ""
speakOnce(text, result)
}
"stopRecognition" -> {
scope.launch {
try {
asrInstance?.stop()
sendAsrEvent(mapOf("type" to "sessionStopped"))
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("STOP_ERROR", e.message, null)
}
}
}
"speakStream" -> {
val text = call.argument<String>("text") ?: ""
speakStream(text, result)
}
"dispose" -> {
try {
asrInstance?.release()
asrInstance = null
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
"flushStream" -> {
flushStream(result)
}
"stopTTS" -> {
stopTTS(result)
}
"disposeTTS" -> {
disposeTTS(result)
}
else -> {
result.notImplemented()
}
@ -221,93 +180,23 @@ class BytedanceSpeechPlugin : FlutterPlugin {
}
}
// TTS方法处理器
inner class TtsMethodHandler : MethodCallHandler {
/**
* ASR方法处理器
*/
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val appId = call.argument<String>("appId") ?: ""
val accessToken = call.argument<String>("accessToken") ?: ""
val speaker = call.argument<String>("speaker") ?: ""
val format = call.argument<String>("format") ?: "mp3"
val sampleRate = call.argument<Int>("sampleRate") ?: 24000
val enableTimestamp = call.argument<Boolean>("enableTimestamp") ?: true
try {
ttsInstance = BytedanceTTS(
context = context,
appId = appId,
accessToken = accessToken,
speaker = speaker,
format = format,
sampleRate = sampleRate,
enableTimestamp = enableTimestamp,
audioCallback = { audioData ->
sendTtsEvent(mapOf(
"type" to "audio",
"audioData" to audioData
))
},
completionCallback = {
sendTtsEvent(mapOf("type" to "completed"))
},
errorCallback = { errorCode, errorMessage ->
sendTtsEvent(mapOf(
"type" to "error",
"errorCode" to errorCode,
"errorMessage" to errorMessage
))
}
)
result.success(true)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
"initializeASR" -> {
result.notImplemented()
}
"synthesize" -> {
val text = call.argument<String>("text") ?: ""
if (ttsEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始合成", null)
return
}
scope.launch {
try {
sendTtsEvent(mapOf("type" to "started"))
ttsInstance?.synthesize(text)
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("SYNTHESIS_ERROR", e.message, null)
}
}
}
"startListening" -> {
result.notImplemented()
}
"stopSynthesis" -> {
scope.launch {
try {
ttsInstance?.stop()
scope.launch(Dispatchers.Main) {
result.success(true)
}
} catch (e: Exception) {
scope.launch(Dispatchers.Main) {
result.error("STOP_ERROR", e.message, null)
}
}
}
"stopListening" -> {
result.notImplemented()
}
"dispose" -> {
try {
ttsInstance?.release()
ttsInstance = null
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
"cancelASR" -> {
result.notImplemented()
}
else -> {
result.notImplemented()
@ -317,15 +206,274 @@ class BytedanceSpeechPlugin : FlutterPlugin {
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null)
ttsEventChannel.setStreamHandler(null)
asrChannel.setMethodCallHandler(null)
// 释放资源
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()
asrInstance?.release()
ttsInstance?.release()
asrInstance = null
ttsInstance = null
try {
val success = bytedanceTTS?.speakOnce(text) ?: false
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "合成文本异常: ${e.message}", e)
result.error(
"SPEAK_EXCEPTION",
"合成文本异常: ${e.message}",
null
)
}
}
/**
* 流式合成文本
*/
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.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import com.yunqiinnovation.deepvoice_utils.FileLogger
/** ClassicBluetoothPlugin */
class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
@ -119,7 +118,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
sendEvent(eventMap)
}
} catch (e: Exception) {
FileLogger.e(TAG, "蓝牙状态获取失败", e)
Log.e(TAG, "蓝牙状态获取失败", e)
}
}
@ -188,7 +187,7 @@ class ClassicBluetoothPlugin: FlutterPlugin, MethodCallHandler {
eventSink?.success(event)
} catch (e: Exception) {
FileLogger.e(TAG, "事件发送失败", e)
Log.e(TAG, "事件发送失败", e)
}
}
}

2
local_plugins/classic_bluetooth/pubspec.yaml

@ -9,8 +9,6 @@ environment:
dependencies:
flutter:
sdk: flutter
deepvoice_utils:
path: ../deepvoice_utils
flutter:
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 {
implementation project(':deepvoice_utils')
implementation("androidx.media:media:1.6.0")
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6
pubspec.yaml

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

Loading…
Cancel
Save