15 changed files with 1855 additions and 430 deletions
@ -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<BleManager>(); |
||||
|
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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<int> currentParts = |
||||
|
currentVersion.split('.').map((e) => int.parse(e.trim())).toList(); |
||||
|
List<int> 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<BleManager>(); |
|
||||
|
|
||||
/// 检查设备电量是否满足升级条件 |
|
||||
/// 返回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, |
|
||||
), |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<BleManager>(); |
|
||||
final int maxRetryAttempts = 3; |
|
||||
int currentRetryCount = 0; |
|
||||
|
|
||||
/// 获取当前设备版本信息 |
|
||||
/// 返回包含当前版本、产品ID和设备信息的Map |
|
||||
/// 抛出异常如果获取失败 |
|
||||
Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<int> currentParts = currentVersion |
|
||||
.split('.') |
|
||||
.map((e) => int.parse(e.trim())) |
|
||||
.toList(); |
|
||||
List<int> 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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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 """ |
||||
|
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="\(serviceConfig.targetLanguage)"> |
||||
|
<voice name="\(voice)"> |
||||
|
<prosody rate="\(serviceConfig.speechRate)" pitch="\(serviceConfig.speechPitch)" volume="\(serviceConfig.speechVolume)"> |
||||
|
\(text) |
||||
|
</prosody> |
||||
|
</voice> |
||||
|
</speak> |
||||
|
""" |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据语言获取对应的语音 |
||||
|
*/ |
||||
|
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<T>(_ 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 |
||||
|
} |
||||
|
} |
||||
@ -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<SHA256>.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 |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue