diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index f181dcc77..eec0aa32c 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -123,8 +123,10 @@ class BleManager extends GetxService { RxInt get connectionStateRx => _connectionState; // 连接状态监听 + final _isCall = false.obs; final _isConnected = false.obs; bool get isConnected => _isConnected.value; + bool get isCall => _isCall.value; // 设备信息缓存 final deviceInfo = Rx(null); // 使用 Rx 包装 @@ -262,8 +264,13 @@ class BleManager extends GetxService { case BleCommands.GET_BATTERY_INFO: _updateBatteryInfoFromData(data); break; - + case BleCommands.CONTROL_CODEC: + // 处理通话状态 + print(" CONTROL_CODEC: $commandId,$data"); + //_isCall.value = data['callStatus'] as bool? ?? false; + break; default: + print(" 未处理的命令ID: $commandId,$data"); Logger.info('未处理的命令ID: $commandId'); } } @@ -283,7 +290,11 @@ class BleManager extends GetxService { break; case 'productColor': break; + case 'Error': + print(" Error: $commandName,$data"); + break; default: + print(" 未处理的命令名称: $commandName,$data"); Logger.info('未处理的命令名称: $commandName'); } } diff --git a/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart new file mode 100644 index 000000000..2d02acd1c --- /dev/null +++ b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart @@ -0,0 +1,245 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:logger/web.dart'; +import '../../../data/services/ble_manager.dart'; +import '../../../data/models/appconfig.dart'; +import '../../../data/models/appconfig_model.dart'; + +/// OTA核心服务 +/// 整合设备检查、版本管理和状态管理功能 +class OtaCoreService { + final BleManager _bleManager = Get.find(); + final int maxRetryAttempts = 3; + int currentRetryCount = 0; + + // ==================== 状态常量定义 ==================== + static const int NOT_STARTED = -1; // 未开始 + static const int VALIDATION_FILE = 0; // 验证文件 + static const int ONUPGRADE = 1; // 升级中 + static const int START_UPGRADING = 2; // 开始升级 + static const int UPGRADE_SUCCESS = 3; // 升级成功 + static const int UPGRADE_FAILED = 4; // 升级失败 + static const int CONNECTION_DISCONNECTION = 5; // 连接断开 + static const int CONNECTION_SUCCESS = 6; // 连接成功 + static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件 + + /// 获取状态描述信息 + /// [type] 状态类型 + /// 返回对应的状态描述文本 + static String getStatusMessage(int type) { + switch (type) { + case NOT_STARTED: + return "未开始".tr; + case VALIDATION_FILE: + return "验证文件中".tr; + case ONUPGRADE: + return "升级中".tr; + case START_UPGRADING: + return "开始升级".tr; + case UPGRADE_SUCCESS: + return "升级成功".tr; + case UPGRADE_FAILED: + return "升级失败".tr; + case CONNECTION_DISCONNECTION: + return "连接断开".tr; + case CONNECTION_SUCCESS: + return "连接成功".tr; + case DOWNLOADING_UP_FILES: + return "下载升级文件".tr; + default: + return "未知状态".tr; + } + } + + // ==================== 设备检查功能 ==================== + + /// 检查设备电量是否满足升级条件 + /// 返回true表示电量充足,可以进行升级 + /// 返回false表示电量不足,不能进行升级 + bool checkBatteryLevel() { + try { + final deviceInfo = _bleManager.deviceInfo.value; + + if (deviceInfo == null) { + _showBatteryCheckFailedSnackbar('无法获取设备信息,请检查设备连接'); + return false; + } + + final leftBattery = deviceInfo.leftEarBattery; + final rightBattery = deviceInfo.rightEarBattery; + + // 检查左右耳电量是否都大于30% + if (leftBattery <= 30 || rightBattery <= 30) { + _showLowBatteryDialog(leftBattery, rightBattery); + return false; + } + + return true; + } catch (e) { + _showBatteryCheckFailedSnackbar('检查设备电量时发生错误:$e'); + return false; + } + } + + /// 显示电量不足对话框 + /// [leftBattery] 左耳电量 + /// [rightBattery] 右耳电量 + void _showLowBatteryDialog(int leftBattery, int rightBattery) { + String message = '设备电量不足,无法进行固件升级\n'; + message += '升级要求:左右耳电量均需大于30%\n'; + message += '当前电量:左耳 ${leftBattery}%,右耳 ${rightBattery}%\n'; + message += '请充电后再试'; + + Get.dialog( + AlertDialog( + title: Text( + '电量不足', + style: TextStyle( + color: Colors.orange[700], + fontWeight: FontWeight.w600, + ), + ), + content: Text( + message, + style: TextStyle( + color: Colors.orange[600], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + foregroundColor: Colors.orange[700], + ), + child: const Text('确定'), + ), + ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.orange.withValues(alpha: 0.3), + width: 1, + ), + ), + backgroundColor: Colors.orange[50], + ), + ); + } + + /// 显示电量检查失败提示 + /// [message] 错误信息 + void _showBatteryCheckFailedSnackbar(String message) { + Get.snackbar( + '电量检查失败', + message, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange[600], + colorText: Colors.white, + icon: const Icon( + Icons.warning_amber, + color: Colors.white, + ), + ); + } + + // ==================== 版本管理功能 ==================== + + /// 获取当前设备版本信息 + /// 返回包含当前版本、产品ID和设备信息的Map + /// 抛出异常如果获取失败 + Future> getCurrentVersionInfo() async { + DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; + + // 重试机制获取设备信息 + while (currentRetryCount < maxRetryAttempts) { + deviceInfo = _bleManager.deviceInfo.value; + + if (deviceInfo != null && + deviceInfo.firmwareVersion.isNotEmpty && + deviceInfo.firmwareProductId.isNotEmpty) { + break; + } + + await _bleManager.fetchDeviceInfo(); + await Future.delayed(const Duration(seconds: 5)); + currentRetryCount++; + } + + if (deviceInfo == null || + deviceInfo.firmwareVersion.isEmpty || + deviceInfo.firmwareProductId.isEmpty) { + throw "获取设备信息失败,请检查连接"; + } + + // 验证固件版本格式 + final firmwareParts = deviceInfo.firmwareVersion.split('.'); + final firstPart = firmwareParts.sublist(0, 3).join('.'); + final secondPart = firmwareParts.sublist(3).join('.'); + + Logger().d("firstPart: $firstPart, secondPart: $secondPart"); + + if (firstPart != secondPart) { + throw "固件版本格式错误:${deviceInfo.firmwareVersion}"; + } + + return { + 'currentVersion': firstPart, + 'productId': deviceInfo.firmwareProductId, + 'deviceInfo': deviceInfo, + }; + } + + /// 获取最新版本信息 + /// 返回包含最新版本、更新描述和更新包地址的Map + /// 抛出异常如果获取失败 + Future> getLatestVersionInfo() async { + DBProduct? product = AppConfig.getproduct("DeapSound"); + + if (product == null) { + throw "无法获取产品信息"; + } + + return { + 'latestVersion': product.version ?? "", + 'updateDescription': product.updatedescription ?? "", + 'updatePackageAddress': product.updatepackageaddress ?? "", + }; + } + + /// 比较版本号,判断是否有新版本可用 + /// [currentVersion] 当前版本号 + /// [latestVersion] 最新版本号 + /// 返回true表示有新版本可用(支持升级和降级) + bool isNewVersionAvailable(String currentVersion, String latestVersion) { + try { + List currentParts = + currentVersion.split('.').map((e) => int.parse(e.trim())).toList(); + List latestParts = + latestVersion.split('.').map((e) => int.parse(e.trim())).toList(); + + // 确保两个版本号长度一致,短的用0补齐 + int maxLength = currentParts.length > latestParts.length + ? currentParts.length + : latestParts.length; + + while (currentParts.length < maxLength) { + currentParts.add(0); + } + while (latestParts.length < maxLength) { + latestParts.add(0); + } + + // 比较每一段,只要有不同就返回true(支持升级和降级) + for (int i = 0; i < maxLength; i++) { + if (latestParts[i] != currentParts[i]) { + return true; + } + } + + return false; + } catch (e) { + Logger().d('版本比较错误: $e'); + return false; + } + } +} diff --git a/lib/modules/ota_upgrade/services/download_manager.dart b/lib/modules/ota_upgrade/controllers/ota_download_contropller.dart similarity index 98% rename from lib/modules/ota_upgrade/services/download_manager.dart rename to lib/modules/ota_upgrade/controllers/ota_download_contropller.dart index b98aeeaac..a3f97485d 100644 --- a/lib/modules/ota_upgrade/services/download_manager.dart +++ b/lib/modules/ota_upgrade/controllers/ota_download_contropller.dart @@ -5,14 +5,14 @@ import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart'; import 'package:logger/web.dart'; -/// 文件下载管理器 +/// OTA下载服务 /// 负责处理固件包的下载、文件管理等功能 -class DownloadManager { +class OtaDownloadService { late final Dio _dio; /// 构造函数 /// 初始化Dio实例并配置SSL证书处理 - DownloadManager() { + OtaDownloadService() { _dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 30), // 增加连接超时时间 receiveTimeout: Duration.zero, @@ -150,4 +150,4 @@ class DownloadManager { throw Exception('删除旧文件失败: $e'); } } -} +} \ No newline at end of file diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 9e84456af..50d9385b4 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -4,20 +4,17 @@ import 'package:get/get.dart'; import 'package:logger/web.dart'; import 'package:ota/ota.dart'; import '../../../data/services/ble_manager.dart'; -import '../services/version_manager.dart'; -import '../services/download_manager.dart'; -import '../services/device_checker.dart'; -import '../services/ui_manager.dart'; -import '../services/ota_status_manager.dart'; +import 'ota_core_contropller.dart'; +import 'ota_download_contropller.dart'; +import '../views/ota_upgrade_popup_view.dart'; /// OTA升级主控制器 - 重构后的简化版本 /// 负责协调各个服务,管理升级流程的整体状态 class OtaUpgradeController extends GetxController { - // 服务依赖 - final _versionManager = VersionManager(); - final _downloadManager = DownloadManager(); - final _deviceChecker = DeviceChecker(); - final _uiManager = UiManager(); + // 服务依赖 - 使用新的合并后的服务 + final _coreService = OtaCoreService(); + final _downloadService = OtaDownloadService(); + final _uiService = OtaUpgradePopUp(); final _bleManager = Get.find(); final _jlota = Ota.instance; @@ -26,10 +23,10 @@ class OtaUpgradeController extends GetxController { final RxString latestVersion = ''.obs; final RxString currentVersion = ''.obs; final RxString detailedStatus = ''.obs; - final RxString updatedescription = ''.obs; // 添加缺失的更新描述属性 + final RxString updatedescription = ''.obs; final RxDouble downloadProgress = 0.0.obs; final RxDouble progress = 0.0.obs; - final RxInt type = OtaStatusManager.NOT_STARTED.obs; + final RxInt type = OtaCoreService.NOT_STARTED.obs; final RxBool isUpdateAvailable = true.obs; final RxBool isDownloading = false.obs; final RxBool isUpgrade = false.obs; @@ -68,7 +65,7 @@ class OtaUpgradeController extends GetxController { errorMessage.value = ''; // 获取当前版本信息 - final currentInfo = await _versionManager.getCurrentVersionInfo(); + final currentInfo = await _coreService.getCurrentVersionInfo(); currentVersion.value = currentInfo['currentVersion']; // 获取蓝牙地址 @@ -78,7 +75,7 @@ class OtaUpgradeController extends GetxController { } // 获取最新版本信息 - final latestInfo = await _versionManager.getLatestVersionInfo(); + final latestInfo = await _coreService.getLatestVersionInfo(); latestVersion.value = latestInfo['latestVersion']; _updatePackageAddress = latestInfo['updatePackageAddress']; @@ -87,7 +84,7 @@ class OtaUpgradeController extends GetxController { latestInfo['updateDescription'] ?? '本次更新包含性能优化和错误修复。'; // 检查是否有更新 - isUpdateAvailable.value = _versionManager.isNewVersionAvailable( + isUpdateAvailable.value = _coreService.isNewVersionAvailable( currentVersion.value, latestVersion.value, ); @@ -103,13 +100,13 @@ class OtaUpgradeController extends GetxController { Future checkUpdate() async { try { if (latestVersion.value.isNotEmpty) { - _uiManager.showUpdateDialog(latestVersion.value, _startUpdateProcess); + _uiService.showUpdateDialog(latestVersion.value, _startUpdateProcess); } else { - _uiManager.showErrorSnackbar('错误', '获取最新版本信息失败', + _uiService.showErrorSnackbar('错误', '获取最新版本信息失败', backgroundColor: Colors.orange[800]); } } catch (e) { - _uiManager.showErrorSnackbar('错误', '获取版本信息失败', + _uiService.showErrorSnackbar('错误', '获取版本信息失败', backgroundColor: Colors.orange[800]); } } @@ -123,7 +120,7 @@ class OtaUpgradeController extends GetxController { /// 检查设备状态后开始下载和升级 Future _startUpdateProcess() async { // 检查设备电量 - if (!_deviceChecker.checkBatteryLevel()) { + if (!_coreService.checkBatteryLevel()) { return; } @@ -140,9 +137,9 @@ class OtaUpgradeController extends GetxController { isDownloading.value = true; showDetailedStatus.value = true; - type.value = OtaStatusManager.DOWNLOADING_UP_FILES; + type.value = OtaCoreService.DOWNLOADING_UP_FILES; - final filePath = await _downloadManager.downloadFirmware( + final filePath = await _downloadService.downloadFirmware( downloadUrl: _updatePackageAddress, onProgress: (progress) { downloadProgress.value = progress; @@ -190,7 +187,7 @@ class OtaUpgradeController extends GetxController { /// [type] 状态类型 /// 返回对应的状态描述文本 String getStatusMessage(int type) { - return OtaStatusManager.getStatusMessage(type); + return OtaCoreService.getStatusMessage(type); } /// 监听OTA事件 @@ -216,7 +213,7 @@ class OtaUpgradeController extends GetxController { } break; case 'onStartOTA': - type.value = OtaStatusManager.START_UPGRADING; + type.value = OtaCoreService.START_UPGRADING; isUpgrade.value = true; detailedStatus.value = 'OTA升级已开始...'; break; @@ -238,7 +235,7 @@ class OtaUpgradeController extends GetxController { /// 处理升级成功 /// 更新状态并显示成功对话框 void _handleUpgradeSuccess() { - type.value = OtaStatusManager.UPGRADE_SUCCESS; + type.value = OtaCoreService.UPGRADE_SUCCESS; isUpgrade.value = false; detailedStatus.value = '升级成功完成'; showDetailedStatus.value = false; @@ -248,18 +245,18 @@ class OtaUpgradeController extends GetxController { isUpdateAvailable.value = false; // 显示成功对话框 - _uiManager.showUpgradeSuccessDialog(); + _uiService.showUpgradeSuccessDialog(); } /// 处理升级错误 /// [errorCode] 错误代码 /// 更新状态并显示错误提示 void _handleUpgradeError(dynamic errorCode) { - type.value = OtaStatusManager.UPGRADE_FAILED; + type.value = OtaCoreService.UPGRADE_FAILED; isUpgrade.value = false; detailedStatus.value = '升级失败,错误码:$errorCode'; showDetailedStatus.value = false; - _uiManager.showErrorSnackbar( + _uiService.showErrorSnackbar( '升级失败', '请检查设备连接或升级包,错误码:$errorCode', ); @@ -268,7 +265,7 @@ class OtaUpgradeController extends GetxController { /// 处理连接断开 /// 更新连接状态 void _handleConnectionLost() { - type.value = OtaStatusManager.CONNECTION_DISCONNECTION; + type.value = OtaCoreService.CONNECTION_DISCONNECTION; isUpgrade.value = false; detailedStatus.value = '设备连接断开'; showDetailedStatus.value = false; @@ -277,7 +274,7 @@ class OtaUpgradeController extends GetxController { /// 处理连接成功 /// 更新连接状态 void _handleConnectionSuccess() { - type.value = OtaStatusManager.CONNECTION_SUCCESS; + type.value = OtaCoreService.CONNECTION_SUCCESS; isUpgrade.value = true; detailedStatus.value = '设备连接成功'; } @@ -288,6 +285,6 @@ class OtaUpgradeController extends GetxController { /// 设置错误状态并显示错误提示 void _handleError(String title, String message) { errorMessage.value = message; - _uiManager.showErrorSnackbar(title, message); + _uiService.showErrorSnackbar(title, message); } } diff --git a/lib/modules/ota_upgrade/services/device_checker.dart b/lib/modules/ota_upgrade/services/device_checker.dart deleted file mode 100644 index 7b3fb2531..000000000 --- a/lib/modules/ota_upgrade/services/device_checker.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../../data/services/ble_manager.dart'; - -/// 设备检查服务 -/// 负责检查设备状态,如电量检查等 -class DeviceChecker { - final BleManager _bleManager = Get.find(); - - /// 检查设备电量是否满足升级条件 - /// 返回true表示电量充足,可以进行升级 - /// 返回false表示电量不足,不能进行升级 - bool checkBatteryLevel() { - try { - final deviceInfo = _bleManager.deviceInfo.value; - - if (deviceInfo == null) { - _showBatteryCheckFailedSnackbar('无法获取设备信息,请检查设备连接'); - return false; - } - - final leftBattery = deviceInfo.leftEarBattery; - final rightBattery = deviceInfo.rightEarBattery; - - // 检查左右耳电量是否都大于30% - if (leftBattery <= 30 || rightBattery <= 30) { - _showLowBatteryDialog(leftBattery, rightBattery); - return false; - } - - return true; - } catch (e) { - _showBatteryCheckFailedSnackbar('检查设备电量时发生错误:$e'); - return false; - } - } - - /// 显示电量不足对话框 - /// [leftBattery] 左耳电量 - /// [rightBattery] 右耳电量 - void _showLowBatteryDialog(int leftBattery, int rightBattery) { - String message = '设备电量不足,无法进行固件升级\n'; - message += '升级要求:左右耳电量均需大于30%\n'; - message += '当前电量:左耳 ${leftBattery}%,右耳 ${rightBattery}%\n'; - message += '请充电后再试'; - - Get.dialog( - AlertDialog( - title: Text( - '电量不足', - style: TextStyle( - color: Colors.orange[700], - fontWeight: FontWeight.w600, - ), - ), - content: Text( - message, - style: TextStyle( - color: Colors.orange[600], - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - style: TextButton.styleFrom( - foregroundColor: Colors.orange[700], - ), - child: const Text('确定'), - ), - ], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: Colors.orange.withValues(alpha: 0.3), - width: 1, - ), - ), - backgroundColor: Colors.orange[50], - ), - ); - } - - /// 显示电量检查失败提示 - /// [message] 错误信息 - void _showBatteryCheckFailedSnackbar(String message) { - Get.snackbar( - '电量检查失败', - message, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange[600], - colorText: Colors.white, - icon: const Icon( - Icons.warning_amber, - color: Colors.white, - ), - ); - } -} \ No newline at end of file diff --git a/lib/modules/ota_upgrade/services/ota_status_manager.dart b/lib/modules/ota_upgrade/services/ota_status_manager.dart deleted file mode 100644 index 38142633f..000000000 --- a/lib/modules/ota_upgrade/services/ota_status_manager.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:get/get.dart'; - -/// OTA升级状态管理器 -/// 负责管理OTA升级过程中的各种状态常量和状态描述 -class OtaStatusManager { - static const int NOT_STARTED = -1; // 未开始 - static const int VALIDATION_FILE = 0; // 验证文件 - static const int ONUPGRADE = 1; // 升级中 - static const int START_UPGRADING = 2; // 开始升级 - static const int UPGRADE_SUCCESS = 3; // 升级成功 - static const int UPGRADE_FAILED = 4; // 升级失败 - static const int CONNECTION_DISCONNECTION = 5; // 连接断开 - static const int CONNECTION_SUCCESS = 6; // 连接成功 - static const int DOWNLOADING_UP_FILES = 7; // 下载升级文件 - - /// 获取状态描述信息 - /// [type] 状态类型 - /// 返回对应的状态描述文本 - static String getStatusMessage(int type) { - switch (type) { - case NOT_STARTED: - return "未开始".tr; - case VALIDATION_FILE: - return "验证文件中".tr; - case ONUPGRADE: - return "升级中".tr; - case START_UPGRADING: - return "开始升级".tr; - case UPGRADE_SUCCESS: - return "升级成功".tr; - case UPGRADE_FAILED: - return "升级失败".tr; - case CONNECTION_DISCONNECTION: - return "连接断开".tr; - case CONNECTION_SUCCESS: - return "连接成功".tr; - case DOWNLOADING_UP_FILES: - return "下载升级文件".tr; - default: - return "未知状态".tr; - } - } -} \ No newline at end of file diff --git a/lib/modules/ota_upgrade/services/version_manager.dart b/lib/modules/ota_upgrade/services/version_manager.dart deleted file mode 100644 index 579da63c6..000000000 --- a/lib/modules/ota_upgrade/services/version_manager.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:logger/web.dart'; -import '../../../data/services/ble_manager.dart'; -import '../../../data/models/appconfig.dart'; -import '../../../data/models/appconfig_model.dart'; -import 'package:get/get.dart'; - -/// 版本管理服务 -/// 负责处理设备版本信息获取、版本比较等功能 -class VersionManager { - final BleManager _bleManager = Get.find(); - final int maxRetryAttempts = 3; - int currentRetryCount = 0; - - /// 获取当前设备版本信息 - /// 返回包含当前版本、产品ID和设备信息的Map - /// 抛出异常如果获取失败 - Future> getCurrentVersionInfo() async { - DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; - - // 重试机制获取设备信息 - while (currentRetryCount < maxRetryAttempts) { - deviceInfo = _bleManager.deviceInfo.value; - - if (deviceInfo != null && - deviceInfo.firmwareVersion.isNotEmpty && - deviceInfo.firmwareProductId.isNotEmpty) { - break; - } - - await _bleManager.fetchDeviceInfo(); - await Future.delayed(const Duration(seconds: 5)); - currentRetryCount++; - } - - if (deviceInfo == null || - deviceInfo.firmwareVersion.isEmpty || - deviceInfo.firmwareProductId.isEmpty) { - throw "获取设备信息失败,请检查连接"; - } - - // 验证固件版本格式 - final firmwareParts = deviceInfo.firmwareVersion.split('.'); - final firstPart = firmwareParts.sublist(0, 3).join('.'); - final secondPart = firmwareParts.sublist(3).join('.'); - - Logger().d("firstPart: $firstPart, secondPart: $secondPart"); - - if (firstPart != secondPart) { - throw "固件版本格式错误:${deviceInfo.firmwareVersion}"; - } - - return { - 'currentVersion': firstPart, - 'productId': deviceInfo.firmwareProductId, - 'deviceInfo': deviceInfo, - }; - } - - /// 获取最新版本信息 - /// 返回包含最新版本、更新描述和更新包地址的Map - /// 抛出异常如果获取失败 - Future> getLatestVersionInfo() async { - DBProduct? product = AppConfig.getproduct("DeapSound"); - - if (product == null) { - throw "无法获取产品信息"; - } - - return { - 'latestVersion': product.version ?? "", - 'updateDescription': product.updatedescription ?? "", - 'updatePackageAddress': product.updatepackageaddress ?? "", - }; - } - - /// 比较版本号,判断是否有新版本可用 - /// [currentVersion] 当前版本号 - /// [latestVersion] 最新版本号 - /// 返回true表示有新版本可用(支持升级和降级) - bool isNewVersionAvailable(String currentVersion, String latestVersion) { - try { - List currentParts = currentVersion - .split('.') - .map((e) => int.parse(e.trim())) - .toList(); - List latestParts = - latestVersion.split('.').map((e) => int.parse(e.trim())).toList(); - - // 确保两个版本号长度一致,短的用0补齐 - int maxLength = currentParts.length > latestParts.length - ? currentParts.length - : latestParts.length; - - while (currentParts.length < maxLength) { - currentParts.add(0); - } - while (latestParts.length < maxLength) { - latestParts.add(0); - } - - // 比较每一段,只要有不同就返回true(支持升级和降级) - for (int i = 0; i < maxLength; i++) { - if (latestParts[i] != currentParts[i]) { - return true; - } - } - - return false; - } catch (e) { - Logger().d('版本比较错误: $e'); - return false; - } - } -} \ No newline at end of file diff --git a/lib/modules/ota_upgrade/services/ui_manager.dart b/lib/modules/ota_upgrade/views/ota_upgrade_popup_view.dart similarity index 96% rename from lib/modules/ota_upgrade/services/ui_manager.dart rename to lib/modules/ota_upgrade/views/ota_upgrade_popup_view.dart index a2f4288d4..66ef745f5 100644 --- a/lib/modules/ota_upgrade/services/ui_manager.dart +++ b/lib/modules/ota_upgrade/views/ota_upgrade_popup_view.dart @@ -4,9 +4,9 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:logger/web.dart'; import '/routes/app_routes.dart'; -/// UI交互管理器 +/// OTA UI服务 /// 负责处理所有与用户界面交互相关的功能 -class UiManager { +class OtaUpgradePopUp { /// 显示更新确认对话框 /// [latestVersion] 最新版本号 /// [onConfirm] 确认更新的回调函数 @@ -101,7 +101,8 @@ class UiManager { /// [title] 错误标题 /// [message] 错误信息 /// [backgroundColor] 背景颜色,可选 - void showErrorSnackbar(String title, String message, {Color? backgroundColor}) { + void showErrorSnackbar(String title, String message, + {Color? backgroundColor}) { Get.snackbar( title, message, @@ -114,4 +115,4 @@ class UiManager { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 2625b8b0b..a427784c0 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -32,8 +32,7 @@ class TranslationController extends GetxController { static const String TAG = 'TranslationController'; // 服务 - final BluetoothManager _bluetoothManager = Get.find(); - final BleManager _bleManager = Get.find(); + final AsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = Get.find(); @@ -43,7 +42,8 @@ class TranslationController extends GetxController { final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); // 蓝牙服务 - final bleManager = Get.find(); + final BluetoothManager bluetoothManager = Get.find(); + final BleManager bleManager = Get.find(); //音乐播放管理 final _musiceManager = Get.find(); // 统计服务 @@ -124,6 +124,10 @@ class TranslationController extends GetxController { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); stopAll(); + + if (mode == 'call') { + _initializeCallModeTranslationService(); + } } String _getModeTitle(String mode) { @@ -277,10 +281,6 @@ class TranslationController extends GetxController { // 初始化服务 _initServices(); - // 初始化语音翻译服务 - _initializeCallModeTranslationService(); - // 🔑 reverse模式下不需要手动滚动,ListView会自然显示最新内容 - // 参考Agent模块的实现模式 } /// 预先检查权限 @@ -457,6 +457,7 @@ class TranslationController extends GetxController { // required String translationSecretKey, // String translationRegion = 'cn-north-1', // }); + // await _astService.dispose(); // 重新初始化ASR服务以支持通话音频源 await _astService.initialize(supportedLanguages: callModeLanguages); @@ -620,8 +621,8 @@ class TranslationController extends GetxController { final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); await _asrService.enableRecord( "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); - // await _astService.enableRecord( - // "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); + await _astService.enableRecord( + "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); return; } @@ -653,19 +654,6 @@ class TranslationController extends GetxController { if (isRecognizing.value) return; try { - // 每次开始识别前检查 ASR 服务是否已配置为当前语言对 - final List currentLanguages = [ - sourceLanguageCode.value, - targetLanguageCode.value - ]; - final bool needsReinit = - !_asrService.supportedLanguages.toSet().containsAll(currentLanguages); - - if (needsReinit) { - Logger.info('识别开始前发现语言变更,重新初始化ASR服务'); - await _reinitializeAsrService(); - } - currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); // 开始新的翻译会话统计 @@ -688,9 +676,14 @@ class TranslationController extends GetxController { } else if (currentMode.value == "call") { // 发送ble系统mic和dac(音乐或者通话远端)声音 bleManager.openA2DPDecoder(); + + //判断是否进入通话状态 + // if (bleManager.isCall) { _audioSourceType = true; isTtsEnabled.value = false; await _astService.startContinuousTranslation(); + // } + // await _astService.path("${dir.path}/8_mic.wav"); Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); } else { @@ -699,7 +692,7 @@ class TranslationController extends GetxController { isTtsEnabled.value = true; } - if (_bluetoothManager.currentDeviceRx.value != null) { + if (bluetoothManager.currentDeviceRx.value != null) { restoreOriginalAudioState(); } else { disableBluetoothAudio(); diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt index ac79b478a..fc92edaad 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt @@ -49,9 +49,10 @@ class IntegratedSpeechTranslationService( private const val INITIAL_SILENCE_TIMEOUT = "200" } - // 协程上下文 - private val job = SupervisorJob() - override val coroutineContext: CoroutineContext = Dispatchers.Main + job + // 改为可变的协程作用域 + private var job = SupervisorJob() + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job // Azure服务组件 private var speechConfig: SpeechConfig? = null @@ -153,6 +154,18 @@ class IntegratedSpeechTranslationService( val confidence: Float = 0f ) + /** + * 重置协程作用域 + * 用于重新初始化时清理旧的协程 + */ + private fun resetCoroutineScope() { + // 取消所有正在运行的协程 + job.cancel() + // 创建新的协程作用域 + job = SupervisorJob() + Log.d(TAG, "协程作用域已重置") + } + /** * 初始化整合服务 */ @@ -163,6 +176,14 @@ class IntegratedSpeechTranslationService( callback: ServiceEventCallback ): Boolean = withContext(Dispatchers.IO) { try { + Log.d(TAG, "开始初始化服务") + + // 重新初始化前先清理 + if (serviceState.isInitialized.get()) { + Log.d(TAG, "检测到服务已初始化,先进行清理") + dispose() + } + this@IntegratedSpeechTranslationService.eventCallback = callback serviceConfig?.let { this@IntegratedSpeechTranslationService.serviceConfig = it } @@ -309,6 +330,7 @@ class IntegratedSpeechTranslationService( Log.d(TAG, "识别完成事件: ${event.result.text},serviceConfig.sourceLanguage:${serviceConfig.sourceLanguage}") // 触发翻译流程 launch { + Log.d(TAG, "触发翻译流程: ${event.result.text}") processTranslationAndSynthesis(event.result.text) } } @@ -407,7 +429,9 @@ class IntegratedSpeechTranslationService( /** * 开始连续语音翻译 */ - fun startContinuousTranslation(): Boolean { + fun startContinuousTranslation(): Boolean { + Log.d(TAG, "尝试启动连续翻译,当前状态:初始化=${serviceState.isInitialized.get()}, 识别中=${serviceState.isRecognizing.get()}") + if (!serviceState.isInitialized.get()) { eventCallback?.onError("Service", "服务未初始化") return false @@ -419,15 +443,16 @@ class IntegratedSpeechTranslationService( } return try { - + // 启动音频处理器 audioProcessor?.startRecording() if (serviceConfig.enableContinuousRecognition) { - Log.d(TAG, "startContinuousTranslation") + Log.d(TAG, "启动连续识别") recognizer?.startContinuousRecognitionAsync() + serviceState.isRecognizing.set(true) } - Log.d(TAG, "开始连续语音翻译") + Log.d(TAG, "连续语音翻译启动成功") true } catch (e: Exception) { Log.e(TAG, "启动连续翻译失败", e) @@ -469,6 +494,7 @@ class IntegratedSpeechTranslationService( * 处理翻译和合成流程 */ private suspend fun processTranslationAndSynthesis(text: String) { + Log.d(TAG, " 翻译状态:${serviceState.isTranslating.get()}") if (serviceState.isTranslating.get()) { Log.w(TAG, "翻译正在进行中,跳过当前请求") return @@ -477,7 +503,7 @@ class IntegratedSpeechTranslationService( serviceState.isTranslating.set(true) eventCallback?.onTranslationStarted(text) eventCallback?.onStateChanged("Translation", true) - + Log.d(TAG, "翻译开始${text}") try { val translationResult = withTimeout(serviceConfig.translationTimeout) { translationService?.translateText( @@ -491,7 +517,9 @@ class IntegratedSpeechTranslationService( eventCallback?.onStateChanged("Translation", false) when { + translationResult?.success == true && !translationResult.translatedText.isNullOrEmpty() -> { + Log.d(TAG, "翻译成功${translationResult.translatedText}") eventCallback?.onTranslated( text, translationResult.translatedText, @@ -949,126 +977,71 @@ fun setAudioOutputDevice(device: com.deep_voice.speech.tts.AudioOutputDevice) { ) } - /** - * 完善的资源释放方法 - */ fun dispose() { try { - Log.d(TAG, "开始释放服务资源...") - - // 1. 设置释放标志,防止新的操作 - serviceState.isInitialized.set(false) + Log.d(TAG, "开始清理服务资源") - // 2. 停止所有活动(使用runBlocking确保同步完成) - runBlocking { - try { - withTimeout(5000) { // 5秒超时 - stopContinuousTranslation() - } - } catch (e: TimeoutCancellationException) { - Log.w(TAG, "停止识别超时,强制继续释放资源") - } - } - - // 3. 等待当前处理完成(带超时) - val maxWaitTime = 3000L // 3秒 - val startTime = System.currentTimeMillis() - while ((serviceState.isTranslating.get() || serviceState.isSynthesizing.get() || serviceState.isRecognizing.get()) - && (System.currentTimeMillis() - startTime) < maxWaitTime) { - Thread.sleep(100) - } - - // 4. 强制停止识别器 - try { - recognizer?.stopContinuousRecognitionAsync()?.get(2, TimeUnit.SECONDS) - } catch (e: Exception) { - Log.w(TAG, "停止识别器超时,强制关闭") - } - - // 5. 释放Azure资源(按顺序释放) - try { - recognizer?.close() - recognizer = null - Log.d(TAG, "语音识别器已释放") - } catch (e: Exception) { - Log.e(TAG, "释放识别器失败", e) + // 停止连续识别 + if (serviceState.isRecognizing.get()) { + stopContinuousTranslation() } - try { - synthesizer?.close() - synthesizer = null - Log.d(TAG, "语音合成器已释放") - } catch (e: Exception) { - Log.e(TAG, "释放合成器失败", e) - } + // 添加协程清理 + resetCoroutineScope() - try { - audioConfig?.close() - audioConfig = null - Log.d(TAG, "音频配置已释放") - } catch (e: Exception) { - Log.e(TAG, "释放音频配置失败", e) + // 清理识别器 + recognizer?.let { + try { + it.stopContinuousRecognitionAsync() + it.close() + } catch (e: Exception) { + Log.w(TAG, "清理识别器失败", e) + } } + recognizer = null - try { - speechConfig?.close() - speechConfig = null - Log.d(TAG, "语音配置已释放") - } catch (e: Exception) { - Log.e(TAG, "释放语音配置失败", e) + // 清理合成器 + synthesizer?.let { + try { + it.close() + } catch (e: Exception) { + Log.w(TAG, "清理合成器失败", e) + } } + synthesizer = null - // 6. 释放音频处理器 - try { - audioProcessor?.dispose() - audioProcessor = null - Log.d(TAG, "音频处理器已释放") - } catch (e: Exception) { - Log.e(TAG, "释放音频处理器失败", e) - } + // 清理音频处理器 + audioProcessor?.dispose() + audioProcessor = null - // 7. 释放录音文件 - try { - recordfile1?.closeFile(true) - recordfile1 = null - Log.d(TAG, "录音文件已释放") - } catch (e: Exception) { - Log.e(TAG, "释放录音文件失败", e) - } + // 清理音频配置 + audioConfig?.close() + audioConfig = null - // 8. 释放翻译服务 - try { - translationService?.dispose() - translationService = null - Log.d(TAG, "翻译服务已释放") - } catch (e: Exception) { - Log.e(TAG, "释放翻译服务失败", e) - } + // 清理语音配置 + speechConfig?.close() + speechConfig = null - // 9. 取消协程(使用cancelAndJoin确保完全停止) - try { - runBlocking { - job.cancelAndJoin() - } - Log.d(TAG, "协程已取消") - } catch (e: Exception) { - Log.e(TAG, "取消协程失败", e) - } + // 清理翻译服务 + translationService?.dispose() + translationService = null - // 10. 清理缓存和回调 - voiceCache.clear() - eventCallback = null - filePath = null + // 清理录音文件 + recordfile1?.closeFile(true) + recordfile1 = null - // 11. 重置状态 + // 重置状态 + serviceState.isInitialized.set(false) serviceState.isRecognizing.set(false) serviceState.isSynthesizing.set(false) serviceState.isTranslating.set(false) - Log.d(TAG, "服务资源释放完成") + // 清理缓存 + voiceCache.clear() + Log.d(TAG, "服务资源清理完成") } catch (e: Exception) { - Log.e(TAG, "释放资源过程中发生异常", e) + Log.e(TAG, "清理服务资源失败", e) } } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index cad5b8251..a7ba1e1ca 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -1035,6 +1035,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { override fun onAudioDataReceived1(data: ByteArray) { + // FileLogger.d(tag, "onAudioDataReceived1: ${data.size}") azureAstHelper.pushAudioData(data) // 可选:处理音频数据 diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt index c9d50f07e..9f29c316e 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/tools/SimpleAudioReceiver.kt @@ -129,7 +129,7 @@ class SimpleAudioReceiver(private val context: Context) { // 情况2:不在录制但队列有数据 -> 从队列获取 else if (writeQueue.isNotEmpty()) { data = writeQueue.poll() - Log.d("tag", "写入数据: ${data?.size}") + //Log.d("tag", "写入数据: ${data?.size}") bytesToWrite = data?.size ?: 0 } // 确保有有效数据再写入 @@ -139,7 +139,7 @@ class SimpleAudioReceiver(private val context: Context) { if (bytesToWrite < data.size) data.copyOf(bytesToWrite) else data try { - Log.d(TAG, "写入数据大小: ${finalData.size}") + // Log.d(TAG, "写入数据大小: ${finalData.size}") if(audioDataCallback!=null){ audioDataCallback?.onAudio(finalData) } diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift new file mode 100644 index 000000000..9c4e3791c --- /dev/null +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift @@ -0,0 +1,933 @@ +import Foundation +import MicrosoftCognitiveServicesSpeech +import AVFoundation +import os.log + +/** + * 整合的语音翻译服务 + * 集成ASR语音识别、翻译服务和TTS语音合成 + * 实现音频输入 -> 语音识别 -> 翻译 -> 语音合成 -> 音频输出的完整流程 + */ +@objc public class IntegratedSpeechTranslationService: NSObject { + + // MARK: - Constants + private static let TAG = "IntegratedSpeechService" + // 音频源配置 + public enum AudioSourceType { + /** 使用设备麦克风 */ + case microphone + + /** 使用外部提供的音频数据 */ + case external + } + + // 音频配置常量 + private static let SAMPLE_RATE: UInt32 = 16000 + private static let CHANNELS: UInt32 = 1 + private static let BITS_PER_SAMPLE: UInt32 = 16 + private static let BUFFER_SIZE = 4096 + + // 超时配置 + private static let END_SILENCE_TIMEOUT = "300" + private static let SEGMENTATION_SILENCE_TIMEOUT = "300" + private static let INITIAL_SILENCE_TIMEOUT = "200" + + // MARK: - Properties + private let log = OSLog(subsystem: "com.azure.speech", category: "IntegratedSpeechService") + + // Azure服务组件 + private var speechConfig: SPXSpeechConfiguration? + private var recognizer: SPXSpeechRecognizer? + private var synthesizer: SPXSpeechSynthesizer? + + // 翻译服务 + private var translationService: TranslationServiceInterface? + + // 音频处理 + private var audioProcessor: SimpleAudioReceiver? + private var audioConfig: SPXAudioConfiguration? + + // 录音文件处理 + private var recordFile: RecordFile? + private var filePath: String? + + // 配置管理 + private var serviceConfig = ServiceConfiguration() + + // 状态管理 + private let serviceState = ServiceState() + + // 事件回调 + private weak var eventCallback: ServiceEventCallback? + + // 语音映射缓存 + private var voiceCache: [String: String] = [:] + + // 处理队列 + private let processingQueue = DispatchQueue(label: "com.azure.speech.processing", qos: .userInitiated) + + // MARK: - Configuration Structures + + /** + * 服务配置类 + */ + public struct ServiceConfiguration { + var sourceLanguage: String = "zh-CN" + var targetLanguage: String = "en-US" + var currentVoice: String = "en-US-AriaNeural" + var speechRate: String = "0%" + var speechPitch: String = "0%" + var speechVolume: String = "100%" + var enableContinuousRecognition: Bool = true + var enableAutoLanguageDetection: Bool = false + var maxRetryAttempts: Int = 3 + var translationTimeout: TimeInterval = 10.0 + // 新增:控制是否播放合成的音频 + var enableAudioPlayback: Bool = false + + public init( + sourceLanguage: String = "zh-CN", + targetLanguage: String = "en-US", + currentVoice: String = "en-US-AriaNeural", + speechRate: String = "0%", + speechPitch: String = "0%", + speechVolume: String = "100%", + enableContinuousRecognition: Bool = true, + enableAutoLanguageDetection: Bool = false, + maxRetryAttempts: Int = 3, + translationTimeout: TimeInterval = 10.0, + enableAudioPlayback: Bool = false + ) { + self.sourceLanguage = sourceLanguage + self.targetLanguage = targetLanguage + self.currentVoice = currentVoice + self.speechRate = speechRate + self.speechPitch = speechPitch + self.speechVolume = speechVolume + self.enableContinuousRecognition = enableContinuousRecognition + self.enableAutoLanguageDetection = enableAutoLanguageDetection + self.maxRetryAttempts = maxRetryAttempts + self.translationTimeout = translationTimeout + self.enableAudioPlayback = enableAudioPlayback + } + } + + /** + * 服务状态类 + */ + public class ServiceState { + private let queue = DispatchQueue(label: "com.azure.speech.state") + private var _isInitialized = false + private var _isRecognizing = false + private var _isSynthesizing = false + private var _isTranslating = false + + var isInitialized: Bool { + get { queue.sync { _isInitialized } } + set { queue.sync { _isInitialized = newValue } } + } + + var isRecognizing: Bool { + get { queue.sync { _isRecognizing } } + set { queue.sync { _isRecognizing = newValue } } + } + + var isSynthesizing: Bool { + get { queue.sync { _isSynthesizing } } + set { queue.sync { _isSynthesizing = newValue } } + } + + var isTranslating: Bool { + get { queue.sync { _isTranslating } } + set { queue.sync { _isTranslating = newValue } } + } + } + + // MARK: - Protocols + + /** + * 服务事件回调接口 + */ + public protocol ServiceEventCallback: AnyObject { + func onServiceInitialized() + func onRecognizing(text: String, language: String, confidence: Float) + func onRecognized(text: String, language: String, confidence: Float) + func onTranslated(originalText: String, translatedText: String, targetLanguage: String) + func onTranslationStarted(text: String) + func onTranslationFailed(text: String, error: String) + func onSynthesisStarted(text: String) + func onSynthesisCompleted(text: String) + func onSynthesisFailed(text: String, error: String) + func onSynthesisProgress(text: String, progress: Float) + func onRecognitionStarted() + func onRecognitionStopped() + func onStateChanged(component: String, isActive: Bool) + func onError(component: String, error: String) + // 新增:返回合成的音频数据 + func onSynthesisAudioGenerated(text: String, audioData: Data) + } + + /** + * 翻译服务接口 + */ + public protocol TranslationServiceInterface { + func initialize(config: [String: String]) async -> Bool + func translateText( + text: String, + sourceLanguage: String, + targetLanguage: String + ) async -> TranslationResult + func dispose() + } + + /** + * 翻译结果类 + */ + public struct TranslationResult { + let success: Bool + let translatedText: String? + let error: String? + let confidence: Float + + public init(success: Bool, translatedText: String? = nil, error: String? = nil, confidence: Float = 0.0) { + self.success = success + self.translatedText = translatedText + self.error = error + self.confidence = confidence + } + } + + // MARK: - Initialization + + public override init() { + super.init() + } + + /** + * 初始化整合服务 + */ + public func initialize( + azureConfig: AzureConfiguration, + translationConfig: TranslationConfiguration, + serviceConfig: ServiceConfiguration? = nil, + callback: ServiceEventCallback + ) async -> Bool { + + do { + self.eventCallback = callback + + if let config = serviceConfig { + self.serviceConfig = config + } + + os_log("开始初始化整合服务", log: log, type: .info) + + // 初始化Azure语音服务 + if !initializeAzureServices(azureConfig: azureConfig) { + await MainActor.run { + callback.onError(component: "Initialization", error: "Azure服务初始化失败") + } + return false + } + + // 初始化翻译服务 + if !(await initializeTranslationService(translationConfig: translationConfig)) { + await MainActor.run { + callback.onError(component: "Initialization", error: "翻译服务初始化失败") + } + return false + } + + // 初始化音频处理器 + initializeAudioProcessor() + + // 设置语音识别器 + setupSpeechRecognizer() + + // 设置语音合成器 + setupSpeechSynthesizer() + + serviceState.isInitialized = true + os_log("整合服务初始化成功", log: log, type: .info) + + await MainActor.run { + callback.onServiceInitialized() + } + + recordFile = RecordFile() + return true + + } catch { + os_log("初始化失败: %@", log: log, type: .error, error.localizedDescription) + await MainActor.run { + callback.onError( + component: "Initialization", + error: "初始化失败: \(error.localizedDescription)" + ) + } + return false + } + } + + /** + * 初始化Azure服务 + */ + private func initializeAzureServices(azureConfig: AzureConfiguration) -> Bool { + do { + os_log("开始初始化Azure服务", log: log, type: .info) + + speechConfig = try SPXSpeechConfiguration(subscription: azureConfig.subscriptionKey, region: azureConfig.region) + + guard let config = speechConfig else { + return false + } + + config.speechRecognitionLanguage = serviceConfig.sourceLanguage + config.speechSynthesisVoiceName = getVoiceForLanguage(serviceConfig.targetLanguage) + config.setSpeechSynthesisOutputFormat(.riff16Khz16BitMonoPcm) + + // 优化配置 + config.setPropertyTo(Self.END_SILENCE_TIMEOUT, byName: "SpeechServiceConnection_EndSilenceTimeoutMs") + config.setPropertyTo(Self.SEGMENTATION_SILENCE_TIMEOUT, byName: "Speech_SegmentationSilenceTimeoutMs") + config.setPropertyTo(Self.INITIAL_SILENCE_TIMEOUT, byName: "SpeechServiceConnection_InitialSilenceTimeoutMs") + config.setPropertyTo("INTERACTIVE", byName: "SpeechServiceConnection_RecoMode") + + // 启用详细结果 + config.setPropertyTo("true", byName: "SpeechServiceResponse_RequestDetailedResultTrueFalse") + + // 自动语言检测 + if serviceConfig.enableAutoLanguageDetection { + config.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") + } + + os_log("Azure语音服务配置完成", log: log, type: .info) + return true + + } catch { + os_log("Azure服务初始化失败: %@", log: log, type: .error, error.localizedDescription) + return false + } + } + + /** + * 初始化翻译服务 + */ + private func initializeTranslationService(translationConfig: TranslationConfiguration) async -> Bool { + do { + translationService = VolcanoTranslationServiceImpl() + let success = await translationService?.initialize(config: translationConfig.toConfigMap()) ?? false + + if success { + os_log("翻译服务初始化完成", log: log, type: .info) + } + + return success + + } catch { + os_log("翻译服务初始化失败: %@", log: log, type: .error, error.localizedDescription) + return false + } + } + + /** + * 初始化音频处理器 + */ + private func initializeAudioProcessor() { + audioProcessor = SimpleAudioReceiver(parentHelper: self as! AzureAsrHelper) + audioProcessor?.initAudioRecord() + // 检查音频配置是否已存在 + if audioConfig == nil, let pushStream = audioProcessor?.pushAudioStream { + audioConfig = SPXAudioConfiguration(streamInput: pushStream) + os_log("设置麦克风流完成", log: log, type: .info) + } + + os_log("音频处理器初始化完成", log: log, type: .info) + } + + /** + * 设置语音识别器 + */ + private func setupSpeechRecognizer() { + guard let speechConfig = speechConfig, let audioConfig = audioConfig else { + return + } + + do { + recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig, audioConfiguration: audioConfig) + + // 识别中事件 + recognizer?.addRecognizingEventHandler { [weak self] (_, event) in + guard let self = self else { return } + let result = event.result + guard let text = result.text, !text.isEmpty else { return } + let confidence = self.extractConfidence(from: result) + os_log("识别中事件: %@", log: self.log, type: .debug, text) + + DispatchQueue.main.async { + self.eventCallback?.onRecognizing( + text: text, + language: self.serviceConfig.sourceLanguage, + confidence: confidence + ) + } + } + + // 识别完成事件 + recognizer?.addRecognizedEventHandler { [weak self] (_, event) in + guard let self = self else { return } + let result = event.result + + switch result.reason { + case .recognizedSpeech: + if let text = result.text, !text.isEmpty { + let confidence = self.extractConfidence(from: result) + + DispatchQueue.main.async { + self.eventCallback?.onRecognized( + text: text, + language: self.serviceConfig.sourceLanguage, + confidence: confidence + ) + } + + os_log("识别完成事件: %@", log: self.log, type: .info, text) + + // 触发翻译流程 + Task { + await self.processTranslationAndSynthesis(text: text) + } + } + + case .noMatch: + os_log("未识别到语音", log: self.log, type: .debug) + + default: + os_log("识别结果: %@", log: self.log, type: .info, String(describing: result.reason)) + } + } + + // 会话开始事件 + recognizer?.addSessionStartedEventHandler { [weak self] (recognizer, event) in + guard let self = self else { return } + + os_log("会话开始事件", log: self.log, type: .info) + self.serviceState.isRecognizing = true + + DispatchQueue.main.async { + self.eventCallback?.onRecognitionStarted() + self.eventCallback?.onStateChanged(component: "Recognition", isActive: true) + } + } + + // 会话停止事件 + recognizer?.addSessionStoppedEventHandler { [weak self] (recognizer, event) in + guard let self = self else { return } + + os_log("会话停止事件", log: self.log, type: .info) + self.serviceState.isRecognizing = false + + DispatchQueue.main.async { + self.eventCallback?.onRecognitionStopped() + self.eventCallback?.onStateChanged(component: "Recognition", isActive: false) + } + } + + // 取消事件 + recognizer?.addCanceledEventHandler { [weak self] (recognizer, event) in + guard let self = self else { return } + + os_log("取消事件", log: self.log, type: .info) + self.serviceState.isRecognizing = false + + let errorDetails = event.errorDetails ?? "未知错误" + + DispatchQueue.main.async { + self.eventCallback?.onError(component: "Recognition", error: "识别被取消: \(errorDetails)") + self.eventCallback?.onStateChanged(component: "Recognition", isActive: false) + } + } + + os_log("语音识别器设置完成", log: log, type: .info) + + } catch { + os_log("语音识别器设置失败: %@", log: log, type: .error, error.localizedDescription) + } + } + + /** + * 设置语音合成器 + */ + private func setupSpeechSynthesizer() { + guard let speechConfig = speechConfig else { + return + } + + do { + // 根据配置决定音频输出方式 + let audioConfig: SPXAudioConfiguration? + if serviceConfig.enableAudioPlayback { + audioConfig = SPXAudioConfiguration() + } else { + // 不输出到扬声器,只生成音频数据 + audioConfig = nil + } + + synthesizer = try SPXSpeechSynthesizer(speechConfiguration: speechConfig, audioConfiguration: audioConfig) + + // 合成开始事件 + synthesizer?.addSynthesisStartedEventHandler { [weak self] (synthesizer, event) in + guard let self = self else { return } + + os_log("合成开始事件", log: self.log, type: .info) + self.serviceState.isSynthesizing = true + + DispatchQueue.main.async { + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: true) + } + } + + // 合成进行中事件 + synthesizer?.addSynthesizingEventHandler { [weak self] (synthesizer, event) in + guard let self = self else { return } + + os_log("合成进行中事件", log: self.log, type: .debug) + // 计算进度(简化版) + let progress: Float = 0.5 + + DispatchQueue.main.async { + self.eventCallback?.onSynthesisProgress(text: "", progress: progress) + } + } + + // 合成完成事件 + synthesizer?.addSynthesisCompletedEventHandler { [weak self] (synthesizer, event) in + guard let self = self else { return } + + os_log("合成完成事件", log: self.log, type: .info) + + DispatchQueue.main.async { + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) + } + } + + // 合成取消事件 + synthesizer?.addSynthesisCanceledEventHandler { [weak self] (synthesizer, event) in + guard let self = self else { return } + + os_log("合成取消事件", log: self.log, type: .info) + self.serviceState.isSynthesizing = false + + // 修复:移除可选链,因为event.result不是可选类型 + let reason = event.result.reason + + DispatchQueue.main.async { + self.eventCallback?.onError(component: "Synthesis", error: "语音合成取消: \(reason)") + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) + } + } + + os_log("语音合成器设置完成,播放模式: %@", log: log, type: .info, serviceConfig.enableAudioPlayback ? "启用" : "禁用") + + } catch { + os_log("语音合成器设置失败: %@", log: log, type: .error, error.localizedDescription) + } + } + + // MARK: - Public Methods + + /** + * 开始连续语音翻译 + */ + public func startContinuousTranslation() -> Bool { + os_log("尝试启动连续翻译,当前状态:初始化=%@, 识别中=%@", log: log, type: .info, + serviceState.isInitialized ? "true" : "false", + serviceState.isRecognizing ? "true" : "false") + + guard serviceState.isInitialized else { + eventCallback?.onError(component: "Service", error: "服务未初始化") + return false + } + + if serviceState.isRecognizing { + os_log("语音识别已在进行中", log: log, type: .info) + return true + } + + do { + // 启动音频处理器 + audioProcessor?.startAudioRecord( audioSourceType: .external, + audioDataCallback: nil) + + if serviceConfig.enableContinuousRecognition { + os_log("启动连续识别", log: log, type: .info) + try recognizer?.startContinuousRecognition() + serviceState.isRecognizing = true + } + + os_log("连续语音翻译启动成功", log: log, type: .info) + return true + + } catch { + os_log("启动连续翻译失败: %@", log: log, type: .error, error.localizedDescription) + eventCallback?.onError(component: "Service", error: "启动失败: \(error.localizedDescription)") + return false + } + } + + /** + * 启用录音 + */ + public func enableRecord(filePath: String) { + os_log("开启录音: %@", log: log, type: .info, filePath) + + recordFile?.closeFile(isSave: true) + recordFile?.creatingFiles(atPath: filePath) + } + + /** + * 停止连续语音翻译 + */ + public func stopContinuousTranslation() { + do { + try recognizer?.stopContinuousRecognition() + audioProcessor?.stopMicrophoneCapture() + recordFile?.closeFile(isSave: true) + + // 等待当前处理完成 + while serviceState.isTranslating || serviceState.isSynthesizing { + Thread.sleep(forTimeInterval: 0.1) + } + + os_log("停止连续语音翻译", log: log, type: .info) + + } catch { + os_log("停止翻译失败: %@", log: log, type: .error, error.localizedDescription) + eventCallback?.onError(component: "Service", error: "停止失败: \(error.localizedDescription)") + } + } + + /** + * 处理翻译和合成流程 + */ + private func processTranslationAndSynthesis(text: String) async { + os_log("翻译状态: %@", log: log, type: .debug, serviceState.isTranslating ? "进行中" : "空闲") + + if serviceState.isTranslating { + os_log("翻译正在进行中,跳过当前请求", log: log, type: .info) + return + } + + serviceState.isTranslating = true + + await MainActor.run { + eventCallback?.onTranslationStarted(text: text) + eventCallback?.onStateChanged(component: "Translation", isActive: true) + } + + os_log("翻译开始: %@", log: log, type: .info, text) + + do { + let translationResult = try await withTimeout(self.serviceConfig.translationTimeout) { + // 修复:在闭包中显式使用self + return await self.translationService?.translateText( + text: text, + sourceLanguage: self.serviceConfig.sourceLanguage, + targetLanguage: self.serviceConfig.targetLanguage + ) + } + + serviceState.isTranslating = false + + await MainActor.run { + eventCallback?.onStateChanged(component: "Translation", isActive: false) + } + + if let result = translationResult, result.success, let translatedText = result.translatedText, !translatedText.isEmpty { + os_log("翻译成功: %@", log: log, type: .info, translatedText) + + await MainActor.run { + eventCallback?.onTranslated( + originalText: text, + translatedText: translatedText, + targetLanguage: serviceConfig.targetLanguage + ) + } + + // 进行语音合成 + if let audioData = synthesizeText(translatedText) { + await MainActor.run { + eventCallback?.onSynthesisAudioGenerated(text: translatedText, audioData: audioData) + } + } + } else { + let errorMessage = translationResult?.error ?? "翻译失败" + os_log("翻译失败: %@", log: log, type: .error, errorMessage) + + await MainActor.run { + eventCallback?.onTranslationFailed(text: text, error: errorMessage) + } + } + + } catch { + serviceState.isTranslating = false + + await MainActor.run { + eventCallback?.onStateChanged(component: "Translation", isActive: false) + eventCallback?.onTranslationFailed(text: text, error: "翻译超时或异常: \(error.localizedDescription)") + } + + os_log("翻译异常: %@", log: log, type: .error, error.localizedDescription) + } + } + + /** + * 语音合成 + * @param text 要合成的文本 + * @return 返回合成的音频数据,如果合成失败则返回nil + */ + /** + * 合成文本为音频数据 + */ + private func synthesizeText(_ text: String) -> Data? { + guard let synthesizer = synthesizer else { + os_log("语音合成器未初始化", log: log, type: .error) + return nil + } + + do { + let ssml = buildSSML(text: text) + os_log("开始合成语音: %@", log: log, type: .info, text) + + serviceState.isSynthesizing = true + + DispatchQueue.main.async { + self.eventCallback?.onSynthesisStarted(text: text) + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: true) + } + + let result = try synthesizer.speakSsml(ssml) + + serviceState.isSynthesizing = false + + DispatchQueue.main.async { + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) + } + + if result.reason == .synthesizingAudioCompleted { + os_log("语音合成成功", log: log, type: .info) + + DispatchQueue.main.async { + self.eventCallback?.onSynthesisCompleted(text: text) + } + + return result.audioData + } else { + let errorMessage = "合成失败: \(result.reason)" + os_log("%@", log: log, type: .error, errorMessage) + + DispatchQueue.main.async { + self.eventCallback?.onSynthesisFailed(text: text, error: errorMessage) + } + + return nil + } + + } catch { + serviceState.isSynthesizing = false + + let errorMessage = "合成异常: \(error.localizedDescription)" + os_log("%@", log: log, type: .error, errorMessage) + + DispatchQueue.main.async { + self.eventCallback?.onSynthesisFailed(text: text, error: errorMessage) + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) + } + + return nil + } + } + + /** + * 构建SSML + */ + private func buildSSML(text: String) -> String { + let voice = getVoiceForLanguage(serviceConfig.targetLanguage) + + return """ + + + + \(text) + + + + """ + } + + /** + * 根据语言获取对应的语音 + */ + private func getVoiceForLanguage(_ language: String) -> String { + if let cachedVoice = voiceCache[language] { + return cachedVoice + } + + let voice: String + switch language { + case "zh-CN": + voice = "zh-CN-XiaoxiaoNeural" + case "en-US": + voice = "en-US-AriaNeural" + case "ja-JP": + voice = "ja-JP-NanamiNeural" + case "ko-KR": + voice = "ko-KR-SunHiNeural" + case "fr-FR": + voice = "fr-FR-DeniseNeural" + case "de-DE": + voice = "de-DE-KatjaNeural" + case "es-ES": + voice = "es-ES-ElviraNeural" + default: + voice = "en-US-AriaNeural" + } + + voiceCache[language] = voice + return voice + } + + /** + * 提取置信度 + */ + private func extractConfidence(from result: SPXSpeechRecognitionResult) -> Float { + // 从结果中提取置信度,这里简化处理 + return 0.9 + } + + /** + * 更新配置 + */ + public func updateConfiguration(newConfig: ServiceConfiguration) { + serviceConfig = newConfig + + // 更新语音配置 + speechConfig?.speechRecognitionLanguage = newConfig.sourceLanguage + speechConfig?.speechSynthesisVoiceName = getVoiceForLanguage(newConfig.targetLanguage) + + os_log("服务配置已更新", log: log, type: .info) + } + + /** + * 切换语言 + */ + public func switchLanguages() { + let newConfig = ServiceConfiguration( + sourceLanguage: serviceConfig.targetLanguage, + targetLanguage: serviceConfig.sourceLanguage, + currentVoice: getVoiceForLanguage(serviceConfig.sourceLanguage), + speechRate: serviceConfig.speechRate, + speechPitch: serviceConfig.speechPitch, + speechVolume: serviceConfig.speechVolume, + enableContinuousRecognition: serviceConfig.enableContinuousRecognition, + enableAutoLanguageDetection: serviceConfig.enableAutoLanguageDetection, + maxRetryAttempts: serviceConfig.maxRetryAttempts, + translationTimeout: serviceConfig.translationTimeout, + enableAudioPlayback: serviceConfig.enableAudioPlayback + ) + + updateConfiguration(newConfig: newConfig) + + os_log("语言已切换: %@ <-> %@", log: log, type: .info, newConfig.sourceLanguage, newConfig.targetLanguage) + } + + /** + * 推送外部音频数据 + */ + public func pushAudioData(audioData: Data) { + audioProcessor?.saveAudioDataTo(data:audioData) + } + + /** + * 获取服务状态 + */ + public func getServiceStatus() -> [String: Bool] { + return [ + "isInitialized": serviceState.isInitialized, + "isRecognizing": serviceState.isRecognizing, + "isTranslating": serviceState.isTranslating, + "isSynthesizing": serviceState.isSynthesizing + ] + } + + /** + * 释放资源 + */ + public func dispose() { + os_log("开始释放服务资源", log: log, type: .info) + + // 停止所有操作 + stopContinuousTranslation() + + // 释放Azure组件 + recognizer = nil + synthesizer = nil + speechConfig = nil + audioConfig = nil + + // 释放其他组件 + audioProcessor?.releaseAudioResources() + audioProcessor = nil + + translationService?.dispose() + translationService = nil + + recordFile?.closeFile(isSave: false) + recordFile = nil + + // 清理缓存 + voiceCache.removeAll() + + // 重置状态 + serviceState.isInitialized = false + serviceState.isRecognizing = false + serviceState.isTranslating = false + serviceState.isSynthesizing = false + + // 清理回调 + eventCallback = nil + + os_log("服务资源释放完成", log: log, type: .info) + } +} + +// MARK: - Helper Extensions + +/** + * 超时错误 + */ +struct TimeoutError: Error { + let message: String +} + +/** + * 带超时的异步操作 + */ +func withTimeout(_ timeout: TimeInterval, operation: @escaping () async throws -> T) async throws -> T { + return try await withThrowingTaskGroup(of: T.self) { group in + group.addTask { + return try await operation() + } + + group.addTask { + try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + throw TimeoutError(message: "操作超时") + } + + guard let result = try await group.next() else { + throw TimeoutError(message: "操作被取消") + } + + group.cancelAll() + return result + } +} \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift new file mode 100644 index 000000000..d07ac6b9e --- /dev/null +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift @@ -0,0 +1,510 @@ +import Foundation +import CryptoKit +import os.log + +/** + * 火山翻译服务实现 + */ +public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.TranslationServiceInterface { + + // MARK: - Constants + private let tag = "VolcanoTranslationService" + private let log = OSLog(subsystem: "com.azure.speech", category: "VolcanoTranslationService") + private let baseURL = "https://translate.volcengineapi.com" + private let endpoint = "/" + private let service = "translate" + private let version = "2020-06-01" + private let action = "TranslateText" + private let algorithm = "HMAC-SHA256" + + // MARK: - Properties + private var isInitialized = false + private var accessKey = "" + private var secretKey = "" + private var region = "cn-north-1" + private var maxRetryAttempts = 3 + private var timeout: TimeInterval = 10.0 + + // HTTP客户端 + private lazy var urlSession: URLSession = { + let config = URLSessionConfiguration.default + config.timeoutIntervalForRequest = 10.0 + config.timeoutIntervalForResource = 30.0 + return URLSession(configuration: config) + }() + + // 语言代码映射 + private let languageCodeMap: [String: String] = [ + "zh-CN": "zh", + "zh-TW": "zh-Hant", + "en-US": "en", + "en-GB": "en", + "ja-JP": "ja", + "ko-KR": "ko", + "fr-FR": "fr", + "de-DE": "de", + "es-ES": "es", + "ru-RU": "ru", + "ar-SA": "ar", + "pt-BR": "pt", + "it-IT": "it", + "th-TH": "th", + "vi-VN": "vi", + "hi-IN": "hi" + ] + + // MARK: - TranslationServiceInterface Implementation + + /** + * 初始化翻译服务 + */ + public func initialize(config: [String: String]) async -> Bool { + guard let accessKey = config["accessKey"], + let secretKey = config["secretKey"] else { + os_log("翻译服务配置缺失", log: log, type: .error) + return false + } + + self.accessKey = accessKey + self.secretKey = secretKey + self.region = config["region"] ?? "cn-north-1" + + if let maxRetryStr = config["maxRetryAttempts"], let maxRetry = Int(maxRetryStr) { + self.maxRetryAttempts = maxRetry + } + + if let timeoutStr = config["timeout"], let timeoutValue = Double(timeoutStr) { + self.timeout = timeoutValue + } + + isInitialized = true + os_log("火山翻译服务初始化成功", log: log, type: .info) + return true + } + + /** + * 翻译文本 + */ + public func translateText( + text: String, + sourceLanguage: String, + targetLanguage: String + ) async -> IntegratedSpeechTranslationService.TranslationResult { + + guard isInitialized else { + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译服务未初始化" + ) + } + + guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译文本为空" + ) + } + + os_log("开始翻译: %@ -> %@, 文本: %@", log: log, type: .info, sourceLanguage, targetLanguage, text) + + return await performTranslationWithRetry( + text: text, + sourceLanguage: sourceLanguage, + targetLanguage: targetLanguage + ) + } + + /** + * 释放资源 + */ + public func dispose() { + isInitialized = false + urlSession.invalidateAndCancel() + os_log("火山翻译服务已释放", log: log, type: .info) + } + + // MARK: - Private Methods + + /** + * 带重试的翻译执行 + */ + private func performTranslationWithRetry( + text: String, + sourceLanguage: String, + targetLanguage: String + ) async -> IntegratedSpeechTranslationService.TranslationResult { + + var lastError: String = "未知错误" + + for attempt in 1...maxRetryAttempts { + os_log("翻译尝试 %d/%d", log: log, type: .debug, attempt, maxRetryAttempts) + + let result = await performTranslation( + text: text, + sourceLanguage: sourceLanguage, + targetLanguage: targetLanguage + ) + + if result.success { + os_log("翻译成功,尝试次数: %d", log: log, type: .info, attempt) + return result + } + + lastError = result.error ?? "翻译失败" + os_log("翻译尝试 %d 失败: %@", log: log, type: .default, attempt, lastError) + + // 最后一次尝试不需要等待 + if attempt < maxRetryAttempts { + let delay = TimeInterval(attempt) // 递增延迟 + try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + } + } + + os_log("翻译最终失败,已尝试 %d 次", log: log, type: .error, maxRetryAttempts) + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译失败,已重试 \(maxRetryAttempts) 次。最后错误: \(lastError)" + ) + } + + /** + * 执行翻译 + */ + private func performTranslation( + text: String, + sourceLanguage: String, + targetLanguage: String + ) async -> IntegratedSpeechTranslationService.TranslationResult { + + do { + // 转换语言代码 + let sourceCode = getLanguageCode(languageCode: sourceLanguage) + let targetCode = getLanguageCode(languageCode: targetLanguage) + + // 构建请求体 + let requestBody: [String: Any] = [ + "TargetLanguage": targetCode, + "TextList": [text] + ] + + // 如果源语言不是自动检测,则添加源语言 + var finalRequestBody = requestBody + if sourceCode != "auto" { + finalRequestBody["SourceLanguage"] = sourceCode + } + + let jsonData = try JSONSerialization.data(withJSONObject: finalRequestBody) + + // 构建查询参数 + let queryParams: [String: String] = [ + "Action": action, + "Version": version + ] + + // 生成签名 + let headers = try generateSignature( + method: "POST", + requestBody: finalRequestBody, + queryParams: queryParams + ) + + // 构建URL + var urlComponents = URLComponents(string: baseURL + endpoint)! + urlComponents.queryItems = queryParams.map { URLQueryItem(name: $0.key, value: $0.value) } + + guard let url = urlComponents.url else { + throw TranslationError.invalidURL + } + + // 创建请求 + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // 添加签名头 + for (key, value) in headers { + request.setValue(value, forHTTPHeaderField: key) + } + + os_log("发送翻译请求到: %@", log: log, type: .debug, url.absoluteString) + + // 发送请求 + let (data, response) = try await urlSession.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw TranslationError.invalidResponse + } + + os_log("翻译响应状态码: %d", log: log, type: .debug, httpResponse.statusCode) + + if httpResponse.statusCode == 200 { + // 解析响应 + guard let jsonObject = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw TranslationError.invalidJSON + } + + if let translatedText = extractTranslation(jsonResponse: jsonObject) { + os_log("翻译成功: %@", log: log, type: .info, translatedText) + return IntegratedSpeechTranslationService.TranslationResult( + success: true, + translatedText: translatedText, + confidence: 0.9 + ) + } else if let error = extractError(jsonResponse: jsonObject) { + os_log("翻译API返回错误: %@", log: log, type: .error, error) + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: error + ) + } else { + throw TranslationError.noTranslationFound + } + } else { + let errorMessage = "HTTP错误: \(httpResponse.statusCode)" + if let errorData = String(data: data, encoding: .utf8) { + os_log("HTTP错误响应: %@", log: log, type: .error, errorData) + } + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: errorMessage + ) + } + + } catch { + os_log("翻译请求异常: %@", log: log, type: .error, error.localizedDescription) + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译请求异常: \(error.localizedDescription)" + ) + } + } + + /** + * 获取语言代码 + */ + private func getLanguageCode(languageCode: String) -> String { + return languageCodeMap[languageCode] ?? languageCode + } + + /** + * 提取翻译结果 + */ + private func extractTranslation(jsonResponse: [String: Any]) -> String? { + guard let responseMetadata = jsonResponse["ResponseMetadata"] as? [String: Any], + let error = responseMetadata["Error"] as? [String: Any] else { + // 没有错误,尝试提取翻译结果 + if let translationList = jsonResponse["TranslationList"] as? [[String: Any]], + let firstTranslation = translationList.first, + let translation = firstTranslation["Translation"] as? String { + return translation + } + return nil + } + + // 有错误信息 + return nil + } + + /** + * 提取错误信息 + */ + private func extractError(jsonResponse: [String: Any]) -> String? { + if let responseMetadata = jsonResponse["ResponseMetadata"] as? [String: Any], + let error = responseMetadata["Error"] as? [String: Any] { + + let code = error["Code"] as? String ?? "UnknownError" + let message = error["Message"] as? String ?? "未知错误" + return "\(code): \(message)" + } + return nil + } + + /** + * 生成签名 + */ + private func generateSignature( + method: String, + requestBody: [String: Any], + queryParams: [String: String] + ) throws -> [String: String] { + + let now = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + let timestamp = dateFormatter.string(from: now) + + let shortDateFormatter = DateFormatter() + shortDateFormatter.dateFormat = "yyyyMMdd" + shortDateFormatter.timeZone = TimeZone(abbreviation: "UTC") + let shortDate = shortDateFormatter.string(from: now) + + // 构建规范请求 + let httpRequestMethod = method + let canonicalURI = endpoint + + // 构建规范查询字符串 + let sortedQueryParams = queryParams.sorted { $0.key < $1.key } + let canonicalQueryString = sortedQueryParams + .map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)" } + .joined(separator: "&") + + // 构建规范头部 + let host = URL(string: baseURL)!.host! + let canonicalHeaders = "host:\(host)\nx-date:\(timestamp)\n" + let signedHeaders = "host;x-date" + + // 计算请求体哈希 + let requestBodyData = try JSONSerialization.data(withJSONObject: requestBody) + let hashedRequestPayload = sha256(data: requestBodyData) + + // 构建规范请求 + let canonicalRequest = "\(httpRequestMethod)\n\(canonicalURI)\n\(canonicalQueryString)\n\(canonicalHeaders)\n\(signedHeaders)\n\(hashedRequestPayload)" + + // 创建待签字符串 + let credentialScope = "\(shortDate)/\(region)/\(service)/request" + let stringToSign = "\(algorithm)\n\(timestamp)\n\(credentialScope)\n\(sha256(string: canonicalRequest))" + + // 计算签名 + let kSecret = Data(secretKey.utf8) + let kDate = try hmacSha256(key: kSecret, data: shortDate) + let kRegion = try hmacSha256(key: kDate, data: region) + let kService = try hmacSha256(key: kRegion, data: service) + let kSigning = try hmacSha256(key: kService, data: "request") + let signature = try hmacSha256(key: kSigning, data: stringToSign) + let signatureHex = signature.map { String(format: "%02x", $0) }.joined() + + // 构建授权头 + let authorization = "\(algorithm) Credential=\(accessKey)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signatureHex)" + + return [ + "Content-Type": "application/json", + "X-Date": timestamp, + "Authorization": authorization, + "Host": host + ] + } + + /** + * SHA256哈希函数(字符串输入) + */ + private func sha256(string: String) -> String { + let data = Data(string.utf8) + return sha256(data: data) + } + + /** + * SHA256哈希函数(数据输入) + */ + private func sha256(data: Data) -> String { + let hash = SHA256.hash(data: data) + return hash.compactMap { String(format: "%02x", $0) }.joined() + } + + /** + * HMAC-SHA256函数 + */ + private func hmacSha256(key: Data, data: String) throws -> Data { + let dataToSign = Data(data.utf8) + let symmetricKey = SymmetricKey(data: key) + let signature = HMAC.authenticationCode(for: dataToSign, using: symmetricKey) + return Data(signature) + } +} + +/** + * 翻译错误枚举 + */ +enum TranslationError: Error { + case invalidURL + case invalidResponse + case invalidJSON + case noTranslationFound + case networkError(String) + case authenticationError + case rateLimitExceeded + case serviceUnavailable + + var localizedDescription: String { + switch self { + case .invalidURL: + return "无效的URL" + case .invalidResponse: + return "无效的响应" + case .invalidJSON: + return "无效的JSON格式" + case .noTranslationFound: + return "未找到翻译结果" + case .networkError(let message): + return "网络错误: \(message)" + case .authenticationError: + return "认证失败" + case .rateLimitExceeded: + return "请求频率超限" + case .serviceUnavailable: + return "服务不可用" + } + } +} + +/** + * 配置扩展 + */ +extension TranslationConfiguration { + func toConfigMap() -> [String: String] { + var config: [String: String] = [ + "accessKey": accessKey, + "secretKey": secretKey, + "region": region + ] + + if let maxRetry = maxRetryAttempts { + config["maxRetryAttempts"] = String(maxRetry) + } + + if let timeoutValue = timeout { + config["timeout"] = String(timeoutValue) + } + + return config + } +} + +/** + * 翻译配置类 + */ +public struct TranslationConfiguration { + let accessKey: String + let secretKey: String + let region: String + let maxRetryAttempts: Int? + let timeout: TimeInterval? + + public init( + accessKey: String, + secretKey: String, + region: String = "cn-north-1", + maxRetryAttempts: Int? = nil, + timeout: TimeInterval? = nil + ) { + self.accessKey = accessKey + self.secretKey = secretKey + self.region = region + self.maxRetryAttempts = maxRetryAttempts + self.timeout = timeout + } +} + +/** + * Azure配置类 + */ +public struct AzureConfiguration { + let subscriptionKey: String + let region: String + + public init(subscriptionKey: String, region: String) { + self.subscriptionKey = subscriptionKey + self.region = region + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 1a2ffebdd..1aa4eeca8 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -154,7 +154,7 @@ object BleService { val timeDiff = currentTime - lastStatisticsTime if (timeDiff >= 1000) { // 每秒统计一次 // Log.i(TAG, "每秒接收音频数据: $bytesReceivedInCurrentSecond 字节") - // Log.i(TAG, "每秒发送音频数据: $bytesSentInCurrentSecond 字节") + //Log.i(TAG, "每秒发送音频数据: $bytesSentInCurrentSecond 字节") bytesReceivedInCurrentSecond = 0 bytesSentInCurrentSecond = 0 lastStatisticsTime = currentTime @@ -1061,6 +1061,7 @@ startBytesStatistics() recordfile!!.closeFile() recordfile1!!.closeFile() } + Log.d(TAG, "收到设备响应,channelMode: $channelMode") // 声道模式描述 val channelDesc = when (channelMode) { BleConst.AUDIO_CHANNEL_LEFT -> "左声道" @@ -1073,8 +1074,23 @@ startBytesStatistics() resultMap["channelMode"] = channelMode resultMap["statusDesc"] = statusDesc resultMap["channelDesc"] = channelDesc - + + + Log.i(TAG, "编解码控制响应: $statusDesc, $channelDesc") + }else if (responseData.isNotEmpty() && responseData.size == 1) { + val codecStatus = responseData[0].toInt() and 0xFF + if(codecStatus==0x00){ + Log.d( + "BleService", + "收到设备响应:失败,codecStatus: $codecStatus" + ) + val resultMap = mutableMapOf( + "success" to true, + "command" to "Error", + "rawData" to responseData + ) + } } notifyDeviceInfoReceived(cmdType, resultMap) @@ -2082,6 +2098,7 @@ class RecordingFile(private val context: Context) { // 追加音频数据到文件 fos = FileOutputStream(currentAudioFile, true) startWriteThread() // 新增:启动写入线程 + Log.d("TAG", "创建文件: ${currentAudioFile?.absolutePath}") } // 新增:启动写入线程