diff --git a/lib/data/services/language_manager.dart b/lib/data/services/language_manager.dart index c89b2b647..d20f3776e 100644 --- a/lib/data/services/language_manager.dart +++ b/lib/data/services/language_manager.dart @@ -5,22 +5,22 @@ import '../../core/utils/logger.dart'; class LanguageInfo { /// 短代码,如 'zh', 'en' final String shortCode; - + /// 中文名称 final String chineseName; - + /// 英文名称 final String englishName; - + /// Azure ASR 语言代码,如 'zh-CN', 'en-US' final String asrCode; - + /// Azure TTS 语音名称 final String ttsVoiceName; - + /// 备注信息 final String? note; - + LanguageInfo({ required this.shortCode, required this.chineseName, @@ -35,30 +35,30 @@ class LanguageInfo { class LanguageManager extends GetxService { // 单例实例 static LanguageManager? _instance; - + // 单例工厂方法 factory LanguageManager() { _instance ??= LanguageManager._internal(); return _instance!; } - + // 私有构造函数 LanguageManager._internal() { _initLanguageMap(); } - + // 语言信息映射表,以 ASR 代码为键 final Map _languageMap = {}; - + // 中文名称到 ASR 代码的映射 final Map _chineseNameToAsrCodeMap = {}; - + // 英文名称到 ASR 代码的映射 final Map _englishNameToAsrCodeMap = {}; - + // 短代码到 ASR 代码的映射 final Map _shortCodeToAsrCodeMap = {}; - + /// 初始化语言映射表 void _initLanguageMap() { // 中文相关 @@ -69,7 +69,7 @@ class LanguageManager extends GetxService { asrCode: 'zh-CN', ttsVoiceName: 'zh-CN-XiaoxiaoNeural', ); - + _addLanguage( shortCode: 'zh-Hant', chineseName: '中文(繁体)', @@ -77,7 +77,7 @@ class LanguageManager extends GetxService { asrCode: 'zh-TW', ttsVoiceName: 'zh-TW-HsiaoChenNeural', ); - + _addLanguage( shortCode: 'zh-Hant-hk', chineseName: '中文(香港繁体)', @@ -85,7 +85,7 @@ class LanguageManager extends GetxService { asrCode: 'zh-HK', ttsVoiceName: 'zh-HK-HiuMaanNeural', ); - + // 英语相关 _addLanguage( shortCode: 'en', @@ -94,7 +94,7 @@ class LanguageManager extends GetxService { asrCode: 'en-US', ttsVoiceName: 'en-US-AriaNeural', ); - + // 日语相关 _addLanguage( shortCode: 'ja', @@ -103,7 +103,7 @@ class LanguageManager extends GetxService { asrCode: 'ja-JP', ttsVoiceName: 'ja-JP-NanamiNeural', ); - + // 韩语相关 _addLanguage( shortCode: 'ko', @@ -112,7 +112,7 @@ class LanguageManager extends GetxService { asrCode: 'ko-KR', ttsVoiceName: 'ko-KR-SunHiNeural', ); - + // 欧洲语言 _addLanguage( shortCode: 'fr', @@ -121,7 +121,7 @@ class LanguageManager extends GetxService { asrCode: 'fr-FR', ttsVoiceName: 'fr-FR-DeniseNeural', ); - + _addLanguage( shortCode: 'es', chineseName: '西班牙语', @@ -129,7 +129,7 @@ class LanguageManager extends GetxService { asrCode: 'es-ES', ttsVoiceName: 'es-ES-ElviraNeural', ); - + _addLanguage( shortCode: 'pt', chineseName: '葡萄牙语', @@ -137,7 +137,7 @@ class LanguageManager extends GetxService { asrCode: 'pt-BR', ttsVoiceName: 'pt-BR-FranciscaNeural', ); - + _addLanguage( shortCode: 'it', chineseName: '意大利语', @@ -145,7 +145,7 @@ class LanguageManager extends GetxService { asrCode: 'it-IT', ttsVoiceName: 'it-IT-ElsaNeural', ); - + _addLanguage( shortCode: 'de', chineseName: '德语', @@ -153,7 +153,7 @@ class LanguageManager extends GetxService { asrCode: 'de-DE', ttsVoiceName: 'de-DE-KatjaNeural', ); - + _addLanguage( shortCode: 'ru', chineseName: '俄语', @@ -161,7 +161,7 @@ class LanguageManager extends GetxService { asrCode: 'ru-RU', ttsVoiceName: 'ru-RU-SvetlanaNeural', ); - + _addLanguage( shortCode: 'pl', chineseName: '波兰语', @@ -169,7 +169,7 @@ class LanguageManager extends GetxService { asrCode: 'pl-PL', ttsVoiceName: 'pl-PL-AgnieszkaNeural', ); - + _addLanguage( shortCode: 'nl', chineseName: '荷兰语', @@ -177,7 +177,7 @@ class LanguageManager extends GetxService { asrCode: 'nl-NL', ttsVoiceName: 'nl-NL-ColetteNeural', ); - + _addLanguage( shortCode: 'sv', chineseName: '瑞典语', @@ -185,7 +185,7 @@ class LanguageManager extends GetxService { asrCode: 'sv-SE', ttsVoiceName: 'sv-SE-SofieNeural', ); - + _addLanguage( shortCode: 'cs', chineseName: '捷克语', @@ -193,7 +193,7 @@ class LanguageManager extends GetxService { asrCode: 'cs-CZ', ttsVoiceName: 'cs-CZ-VlastaNeural', ); - + _addLanguage( shortCode: 'el', chineseName: '现代希腊语', @@ -201,7 +201,7 @@ class LanguageManager extends GetxService { asrCode: 'el-GR', ttsVoiceName: 'el-GR-AthinaNeural', ); - + _addLanguage( shortCode: 'ro', chineseName: '罗马尼亚语', @@ -209,7 +209,7 @@ class LanguageManager extends GetxService { asrCode: 'ro-RO', ttsVoiceName: 'ro-RO-AlinaNeural', ); - + _addLanguage( shortCode: 'hu', chineseName: '匈牙利语', @@ -217,7 +217,7 @@ class LanguageManager extends GetxService { asrCode: 'hu-HU', ttsVoiceName: 'hu-HU-NoemiNeural', ); - + _addLanguage( shortCode: 'uk', chineseName: '乌克兰语', @@ -225,7 +225,7 @@ class LanguageManager extends GetxService { asrCode: 'uk-UA', ttsVoiceName: 'uk-UA-PolinaNeural', ); - + _addLanguage( shortCode: 'da', chineseName: '丹麦语', @@ -233,7 +233,7 @@ class LanguageManager extends GetxService { asrCode: 'da-DK', ttsVoiceName: 'da-DK-ChristelNeural', ); - + _addLanguage( shortCode: 'fi', chineseName: '芬兰语', @@ -241,7 +241,7 @@ class LanguageManager extends GetxService { asrCode: 'fi-FI', ttsVoiceName: 'fi-FI-NooraNeural', ); - + _addLanguage( shortCode: 'no', chineseName: '挪威语', @@ -249,7 +249,7 @@ class LanguageManager extends GetxService { asrCode: 'nb-NO', ttsVoiceName: 'nb-NO-IselinNeural', ); - + _addLanguage( shortCode: 'hr', chineseName: '克罗地亚语', @@ -257,7 +257,7 @@ class LanguageManager extends GetxService { asrCode: 'hr-HR', ttsVoiceName: 'hr-HR-GabrijelaNeural', ); - + _addLanguage( shortCode: 'ca', chineseName: '加泰隆语', @@ -265,7 +265,7 @@ class LanguageManager extends GetxService { asrCode: 'ca-ES', ttsVoiceName: 'ca-ES-JoanaNeural', ); - + // 亚洲语言 _addLanguage( shortCode: 'ar', @@ -274,7 +274,7 @@ class LanguageManager extends GetxService { asrCode: 'ar-EG', ttsVoiceName: 'ar-EG-SalmaNeural', ); - + _addLanguage( shortCode: 'th', chineseName: '泰语', @@ -282,7 +282,7 @@ class LanguageManager extends GetxService { asrCode: 'th-TH', ttsVoiceName: 'th-TH-PremwadeeNeural', ); - + _addLanguage( shortCode: 'vi', chineseName: '越南语', @@ -290,7 +290,7 @@ class LanguageManager extends GetxService { asrCode: 'vi-VN', ttsVoiceName: 'vi-VN-HoaiMyNeural', ); - + _addLanguage( shortCode: 'id', chineseName: '印尼语', @@ -298,7 +298,7 @@ class LanguageManager extends GetxService { asrCode: 'id-ID', ttsVoiceName: 'id-ID-GadisNeural', ); - + _addLanguage( shortCode: 'hi', chineseName: '印地语', @@ -306,7 +306,7 @@ class LanguageManager extends GetxService { asrCode: 'hi-IN', ttsVoiceName: 'hi-IN-SwaraNeural', ); - + _addLanguage( shortCode: 'tr', chineseName: '土耳其语', @@ -314,7 +314,7 @@ class LanguageManager extends GetxService { asrCode: 'tr-TR', ttsVoiceName: 'tr-TR-EmelNeural', ); - + _addLanguage( shortCode: 'ms', chineseName: '马来语', @@ -322,7 +322,7 @@ class LanguageManager extends GetxService { asrCode: 'ms-MY', ttsVoiceName: 'ms-MY-YasminNeural', ); - + _addLanguage( shortCode: 'bn', chineseName: '孟加拉语', @@ -330,7 +330,7 @@ class LanguageManager extends GetxService { asrCode: 'bn-IN', ttsVoiceName: 'bn-IN-TanishaaNeural', ); - + _addLanguage( shortCode: 'ta', chineseName: '泰米尔语', @@ -338,7 +338,7 @@ class LanguageManager extends GetxService { asrCode: 'ta-IN', ttsVoiceName: 'ta-IN-PallaviNeural', ); - + _addLanguage( shortCode: 'te', chineseName: '泰卢固语', @@ -346,7 +346,7 @@ class LanguageManager extends GetxService { asrCode: 'te-IN', ttsVoiceName: 'te-IN-ShrutiNeural', ); - + _addLanguage( shortCode: 'mr', chineseName: '马拉提语', @@ -354,7 +354,7 @@ class LanguageManager extends GetxService { asrCode: 'mr-IN', ttsVoiceName: 'mr-IN-AarohiNeural', ); - + _addLanguage( shortCode: 'ur', chineseName: '乌尔都语', @@ -362,7 +362,7 @@ class LanguageManager extends GetxService { asrCode: 'ur-IN', ttsVoiceName: 'ur-IN-GulNeural', ); - + _addLanguage( shortCode: 'fa', chineseName: '波斯语', @@ -370,7 +370,7 @@ class LanguageManager extends GetxService { asrCode: 'fa-IR', ttsVoiceName: 'fa-IR-DilaraNeural', ); - + // 非洲语言 _addLanguage( shortCode: 'sw', @@ -379,10 +379,10 @@ class LanguageManager extends GetxService { asrCode: 'sw-KE', ttsVoiceName: 'sw-KE-ZuriNeural', ); - + Logger.info('语言管理器初始化完成,共加载 ${_languageMap.length} 种语言'); } - + /// 添加语言信息到映射表 void _addLanguage({ required String shortCode, @@ -400,133 +400,133 @@ class LanguageManager extends GetxService { ttsVoiceName: ttsVoiceName, note: note, ); - + _languageMap[asrCode] = languageInfo; _chineseNameToAsrCodeMap[chineseName] = asrCode; _englishNameToAsrCodeMap[englishName] = asrCode; _shortCodeToAsrCodeMap[shortCode] = asrCode; } - + /// 获取所有支持的语言信息 Map getAllLanguages() { return Map.from(_languageMap); } - + /// 获取所有支持的语言的中文名称 List getAllChineseNames() { return _chineseNameToAsrCodeMap.keys.toList(); } - + /// 获取所有支持的语言的英文名称 List getAllEnglishNames() { return _englishNameToAsrCodeMap.keys.toList(); } - + /// 根据 ASR 代码获取语言信息 LanguageInfo? getLanguageByAsrCode(String asrCode) { return _languageMap[asrCode]; } - + /// 根据中文名称获取语言信息 LanguageInfo? getLanguageByChineseName(String chineseName) { final asrCode = _chineseNameToAsrCodeMap[chineseName]; if (asrCode == null) return null; return _languageMap[asrCode]; } - + /// 根据英文名称获取语言信息 LanguageInfo? getLanguageByEnglishName(String englishName) { final asrCode = _englishNameToAsrCodeMap[englishName]; if (asrCode == null) return null; return _languageMap[asrCode]; } - + /// 根据短代码获取语言信息 LanguageInfo? getLanguageByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]; } - + /// 根据中文名称获取 ASR 代码 String? getAsrCodeByChineseName(String chineseName) { return _chineseNameToAsrCodeMap[chineseName]; } - + /// 根据英文名称获取 ASR 代码 String? getAsrCodeByEnglishName(String englishName) { return _englishNameToAsrCodeMap[englishName]; } - + /// 根据短代码获取 ASR 代码 String? getAsrCodeByShortCode(String shortCode) { return _shortCodeToAsrCodeMap[shortCode]; } - + /// 根据 ASR 代码获取中文名称 String? getChineseNameByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.chineseName; } - + /// 根据 ASR 代码获取英文名称 String? getEnglishNameByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.englishName; } - + /// 根据 ASR 代码获取短代码 String? getShortCodeByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.shortCode; } - + /// 根据 ASR 代码获取 TTS 语音名称 String? getTtsVoiceNameByAsrCode(String asrCode) { final languageInfo = _languageMap[asrCode]; return languageInfo?.ttsVoiceName; } - + /// 根据中文名称获取 TTS 语音名称 String? getTtsVoiceNameByChineseName(String chineseName) { final asrCode = _chineseNameToAsrCodeMap[chineseName]; if (asrCode == null) return null; return _languageMap[asrCode]?.ttsVoiceName; } - + /// 根据短代码获取中文名称 String? getChineseNameByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]?.chineseName; } - + /// 根据短代码获取英文名称 String? getEnglishNameByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]?.englishName; } - + /// 根据短代码获取 TTS 语音名称 String? getTtsVoiceNameByShortCode(String shortCode) { final asrCode = _shortCodeToAsrCodeMap[shortCode]; if (asrCode == null) return null; return _languageMap[asrCode]?.ttsVoiceName; } - + /// 获取中文名称到 ASR 代码的映射 Map getChineseNameToAsrCodeMap() { return Map.from(_chineseNameToAsrCodeMap); } - + /// 获取英文名称到 ASR 代码的映射 Map getEnglishNameToAsrCodeMap() { return Map.from(_englishNameToAsrCodeMap); } - + /// 获取短代码到 ASR 代码的映射 Map getShortCodeToAsrCodeMap() { return Map.from(_shortCodeToAsrCodeMap); } -} \ No newline at end of file +} diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt index f2b170bdf..8aebb37a8 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrToAsr.kt @@ -1315,7 +1315,16 @@ class VolcanoTranslationServiceImpl : "ms-MY" to "ms", // 马来语(马来西亚) "id-ID" to "id", // 印尼语(印尼) "fil-PH" to "tl", // 菲律宾语(菲律宾) - + "sv-SE" to "sv", // 瑞典语(瑞典) + "ro-RO" to "ro", // 罗马尼亚语(罗马尼亚) + "hu-HU" to "hu", // 匈牙利语(匈牙利) + "da-DK" to "da", // 丹麦语(丹麦) + "fi-FI" to "fi", // 芬兰语(芬兰) + "nb-NO" to "nb", // 挪威语(挪威) + "hr-HR" to "hr", // 克罗地亚语(克罗地亚) + "ca-ES" to "ca", // 加泰隆语 + "mr-IN" to "mr", // 马拉地语(印度) + "ur-IN" to "ur", // 乌尔都语(印度) // 欧洲 "en-US" to "en", // 英语(美国) "en-GB" to "en", // 英语(英国) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift index 4659e56c3..97db886f4 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/IntegratedSpeechTranslationService.swift @@ -605,13 +605,36 @@ import os.log audioProcessor?.stopMicrophoneCapture() recordFile?.closeFile(isSave: true) - // 等待当前处理完成 - while serviceState.isTranslating || serviceState.isSynthesizing { - Thread.sleep(forTimeInterval: 0.1) + // 使用后台队列等待当前处理完成,避免阻塞主线程 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + + // 设置超时时间,避免无限等待 + let timeout = DispatchTime.now() + .seconds(5) + + while (self.serviceState.isTranslating || self.serviceState.isSynthesizing) && DispatchTime.now() < timeout { + Thread.sleep(forTimeInterval: 0.1) + } + + // 强制停止合成器(如果仍在运行) + if self.serviceState.isSynthesizing { + do { + try self.synthesizer?.stopSpeaking() + } catch { + os_log("停止语音合成失败: %@", log: self.log, type: .error, error.localizedDescription) + } + self.serviceState.isSynthesizing = false + + DispatchQueue.main.async { + self.eventCallback?.onStateChanged(component: "Synthesis", isActive: false) + } + } + + DispatchQueue.main.async { + os_log("停止连续语音翻译", log: self.log, type: .info) + } } - os_log("停止连续语音翻译", log: log, type: .info) - } catch { os_log("停止翻译失败: %@", log: log, type: .error, error.localizedDescription) eventCallback?.onError(component: "Service", error: "停止失败: \(error.localizedDescription)") diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift index 7ef63cdeb..5ad252aac 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/VolcanoTranslationServiceImpl.swift @@ -24,6 +24,8 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T private var region = "cn-north-1" private var maxRetryAttempts = 3 private var timeout: TimeInterval = 10.0 + private var isDisposed = false // 添加释放状态标志 + private let disposeLock = NSLock() // 添加线程安全锁 // HTTP客户端 private lazy var urlSession: URLSession = { @@ -52,7 +54,16 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T "ms-MY": "ms", // 马来语(马来西亚) "id-ID": "id", // 印尼语(印尼) "fil-PH": "tl", // 菲律宾语(菲律宾) - + "sv-SE": "sv", // 瑞典语(瑞典) + "ro-RO": "ro", // 罗马尼亚语(罗马尼亚) + "hu-HU": "hu", // 匈牙利语(匈牙利) + "da-DK": "da", // 丹麦语(丹麦) + "fi-FI": "fi", // 芬兰语(芬兰) + "nb-NO": "nb", // 挪威语(挪威) + "hr-HR": "hr", // 克罗地亚语(克罗地亚) + "ca-ES": "ca", // 加泰隆语 + "mr-IN": "mr", // 马拉地语(印度) + "ur-IN": "ur", // 乌尔都语(印度) // 欧洲 "en-US": "en", // 英语(美国) "en-GB": "en", // 英语(英国) @@ -150,11 +161,25 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T /** * 释放资源 + * 添加线程安全保护,防止重复释放 */ public func dispose() { + disposeLock.lock() + defer { disposeLock.unlock() } + + if isDisposed { + os_log("翻译服务已经释放,跳过重复释放", log: log, type: .info) + return + } + + os_log("开始释放翻译服务资源", log: log, type: .info) + isDisposed = true isInitialized = false + + // 取消并失效URLSession urlSession.invalidateAndCancel() - os_log("火山翻译服务已释放", log: log, type: .info) + + os_log("翻译服务资源释放完成", log: log, type: .info) } // MARK: - Private Methods @@ -203,6 +228,7 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T /** * 执行翻译 + * 添加服务状态检查,防止在已释放的会话中创建任务 */ private func performTranslation( text: String, @@ -210,6 +236,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T targetLanguage: String ) async -> IntegratedSpeechTranslationService.TranslationResult { + // 检查服务是否已被释放 + disposeLock.lock() + let disposed = isDisposed + disposeLock.unlock() + + if disposed { + os_log("翻译服务已释放,取消翻译请求", log: log, type: .error) + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译服务已释放" + ) + } + do { // 转换语言代码 let sourceCode = getLanguageCode(languageCode: sourceLanguage) @@ -263,6 +302,19 @@ public class VolcanoTranslationServiceImpl: IntegratedSpeechTranslationService.T os_log("发送翻译请求到: %@", log: log, type: .debug, url.absoluteString) + // 再次检查服务状态,防止在请求构建过程中被释放 + disposeLock.lock() + let stillDisposed = isDisposed + disposeLock.unlock() + + if stillDisposed { + os_log("翻译服务在请求过程中被释放,取消网络请求", log: log, type: .error) + return IntegratedSpeechTranslationService.TranslationResult( + success: false, + error: "翻译服务已释放" + ) + } + // 发送请求 let (data, response) = try await urlSession.data(for: request)