15 changed files with 1854 additions and 429 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