From 5bb6930768b95b1aa58dbcef8586a243865de57f Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 2 Feb 2026 21:29:19 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(azure=5Fspeech):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E8=AE=AF=E9=A3=9E=E8=AF=AD=E9=9F=B3=E8=AF=86=E5=88=AB=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=9A=84=E8=AF=AD=E8=A8=80=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将支持的语言代码中的错误值"en-TW"更正为"zh-TW",以正确匹配台湾中文的语言标识。同时为语音识别入口添加登录和积分检查逻辑,并在相关方法上补充注释说明。 --- .../controllers/translation_controller.dart | 2 + .../azure_speech/AzureAsrHelper.kt | 2 +- .../Sources/azure_speech/AzureAsrHelper.swift | 990 +++++++++--------- 3 files changed, 502 insertions(+), 492 deletions(-) diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index b51a9a1fe..d53e2876d 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -964,8 +964,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } /// 开始语音识别 + /// 同声翻译、通话翻译、多媒体翻译的语音识别调用入口 Future startRecognition() async { Logger.info('开始语音识别'); + //检查用户是否登录且积分是否足够,不足则提示充值 if (User.isLoggedIn() && User.instance.tradeintegral <= 0) { showRechargeDialog('translation'); return; diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 8a4fbd717..661411b7a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -130,7 +130,7 @@ class AzureAsrHelper(private val context: Context) { val xunfeiConfigReady = this.xunfeiAppId.isNotBlank() && this.xunfeiAccessKeyId.isNotBlank() && this.xunfeiAccessKeySecret.isNotBlank() - this.useXunfei = supportedLanguages.all { it == "zh-CN" || it == "en-US" || it == "zh-HK"|| it == "en-TW" } && xunfeiConfigReady + this.useXunfei = supportedLanguages.all { it == "zh-CN" || it == "en-US" || it == "zh-HK"|| it == "zh-TW" } && xunfeiConfigReady // 根据支持的语言数量决定是否启用自动语言检测 this.isAutoDetectLanguage = supportedLanguages.size >= 2 diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index f6f89fffa..7f6928539 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -1,55 +1,53 @@ -import Foundation import AVFoundation +import Foundation import MicrosoftCognitiveServicesSpeech -import speech import os.log +import speech + // 移除 Network 导入,因为我们将使用独立的 NetworkStateMonitor // import Network // MARK: - 基于微软Azure语音服务的ASR实现 -/** - * Azure ASR Helper - * - * 基于微软Azure语音服务的ASR实现 - * 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech - */ +/// Azure ASR Helper +/// +/// 基于微软Azure语音服务的ASR实现 +/// 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech public class AzureAsrHelper: NSObject { private let tag = "AzureAsrHelper" // 日志对象 private let log = OSLog(subsystem: "com.azure.speech", category: "AzureAsrHelper") - + // 核心组件 private var speechConfig: SPXSpeechConfiguration? private var recognizer: SPXSpeechRecognizer? private var audioConfig: SPXAudioConfiguration? - + // 网络监听相关 - 使用独立的NetworkStateMonitor private var networkMonitor: NetworkStateMonitor = NetworkStateMonitor() private var continuousCallback: ContinuousRecognizeCallback? private var isNetworkRecovering = false - + // 网络丢失计数器 private var networkLostCount = 0 // 最大网络丢失次数(3秒检测) private let maxNetworkLostCount = 3 - + // 防抖机制相关变量 private var lastNetworkLostTime: Int64 = 0 - private let networkLostDebounceInterval: Int64 = 3000 // 3秒防抖间隔 - + private let networkLostDebounceInterval: Int64 = 3000 // 3秒防抖间隔 + // 防抖机制相关变量 - 只保留开始操作的防抖 private var lastAudioStartTime: Int64 = 0 - private let audioOperationDebounceInterval: Int64 = 0 // 500毫秒防抖间隔 + private let audioOperationDebounceInterval: Int64 = 0 // 500毫秒防抖间隔 private var audioStartWorkItem: DispatchWorkItem? // 串行队列和状态标记,用于顺序控制音频启动/停止,避免竞态 private let audioOpQueue = DispatchQueue(label: "com.azure.speech.audio.operations") private var isAudioStarting = false private var isAudioStarted = false private var pendingStopRequest = false - + private var currsessionId = "" - // 配置参数 private var currentLanguage = "zh-CN" private var supportedLanguages = ["zh-CN"] @@ -67,31 +65,27 @@ public class AzureAsrHelper: NSObject { public enum AudioSourceType { /** 使用设备麦克风 */ case microphone - + /** 使用外部提供的音频数据 */ case external } - + public var audioSourceType = AudioSourceType.microphone private var isPhoneCallMode = false - + // 音频处理 // private var externalAudioStream: ExternalAudioPullStream? - + // 音频处理 public var audioStream: SimpleAudioReceiver? - - - - public func asrProvider() -> String { return useXunfei ? "xunfei" : "azure" } /** * 初始化Azure语音服务 - * + * * @param subscriptionKey Azure 订阅密钥 * @param region Azure 区域 * @param supportedLanguages 支持的语言数组 @@ -117,7 +111,7 @@ public class AzureAsrHelper: NSObject { os_log("Azure 配置信息不完整", log: log, type: .error) return false } - + // 释放之前的资源 dispose() // 保存配置 @@ -127,52 +121,58 @@ public class AzureAsrHelper: NSObject { self.xunfeiAppId = xunfeiAppId self.xunfeiAccessKeyId = xunfeiAccessKeyId self.xunfeiAccessKeySecret = xunfeiAccessKeySecret - + // 设置语言 if !supportedLanguages.isEmpty { self.supportedLanguages = supportedLanguages } - + // 检查是否只包含中文或英文,如果是则使用讯飞 - let xunfeiConfigReady = !self.xunfeiAppId.isEmpty && - !self.xunfeiAccessKeyId.isEmpty && - !self.xunfeiAccessKeySecret.isEmpty - self.useXunfei = supportedLanguages.allSatisfy { $0 == "zh-CN" || $0 == "en-US" || $0 == "zh-HK" || $0 == "en-TW" } && xunfeiConfigReady - + let xunfeiConfigReady = + !self.xunfeiAppId.isEmpty && !self.xunfeiAccessKeyId.isEmpty + && !self.xunfeiAccessKeySecret.isEmpty + self.useXunfei = + supportedLanguages.allSatisfy { + $0 == "zh-CN" || $0 == "en-US" || $0 == "zh-HK" || $0 == "zh-TW" + } && xunfeiConfigReady + // 根据支持的语言数量决定是否启用自动语言检测 self.isAutoDetectLanguage = supportedLanguages.count >= 2 - + // 如果只有一种语言,设置为当前语言 if !isAutoDetectLanguage && !supportedLanguages.isEmpty { self.currentLanguage = supportedLanguages[0] } - + // 创建语音配置 - speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) - // 针对翻译场景的性能优化 - // optimizeForTranslation() + speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) + // 针对翻译场景的性能优化 + // optimizeForTranslation() if isAutoDetectLanguage { - speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) + speechConfig?.setPropertyTo( + "Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 - os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) + os_log( + "启用语言检测模式,支持语言: %{public}@", log: log, type: .info, + supportedLanguages.joined(separator: ",")) } else { speechConfig?.speechRecognitionLanguage = currentLanguage os_log("固定语言模式,当前语言: %{public}@", log: log, type: .info, currentLanguage) } - + // 预初始化音频组件 preInitializeAudioComponents() - + // 初始化网络监听 - 使用独立的NetworkStateMonitor initNetworkMonitoring() - + return true } catch { os_log("初始化失败: %{public}@", log: log, type: .error, error.localizedDescription) return false } } - + /** * 初始化网络监听 * 使用独立的NetworkStateMonitor类 @@ -181,59 +181,58 @@ public class AzureAsrHelper: NSObject { networkMonitor.initialize(listener: self) os_log("网络监听已初始化", log: log, type: .info) } - + /** * 处理网络可用事件 */ private func handleNetworkAvailable() { os_log("网络已恢复", log: log, type: .info) - - if !self.isNetworkRecovering { - self.isNetworkRecovering = true - os_log("网络恢复,准备重新启动识别", log: self.log, type: .info) - } - + if !self.isNetworkRecovering { + self.isNetworkRecovering = true + os_log("网络恢复,准备重新启动识别", log: self.log, type: .info) + } + } - - - /** - * 处理网络连接丢失(带防抖机制) - */ + + /** + * 处理网络连接丢失(带防抖机制) + */ private func handleNetworkLost() { - let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // 转换为毫秒 - + let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // 转换为毫秒 + // 防抖机制:如果距离上次触发时间小于防抖间隔,则忽略本次触发 if currentTime - lastNetworkLostTime < networkLostDebounceInterval { - os_log("网络丢失事件被防抖过滤,距离上次触发仅%{public}dms", log: log, type: .debug, currentTime - lastNetworkLostTime) + os_log( + "网络丢失事件被防抖过滤,距离上次触发仅%{public}dms", log: log, type: .debug, + currentTime - lastNetworkLostTime) return } - + // 更新最后触发时间 lastNetworkLostTime = currentTime - + // 这里启动一个3秒定时器,3秒后检查网络是否恢复 DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in guard let self = self else { return } - + os_log("网络不可用", log: self.log, type: .error) let isNetworkAvailable = self.checkNetworkStatus() - + if isNetworkAvailable { // 如果网络恢复,重置计数器和防抖时间 os_log("网络已恢复,重置计数器", log: self.log, type: .info) self.lastNetworkLostTime = 0 self.isNetworkRecovering = true } else { - self.continuousCallback?.onError(sessionid: currsessionId,1000, "网络连接不可用,请检查网络设置") + self.continuousCallback?.onError(sessionid: currsessionId, 1000, "网络连接不可用,请检查网络设置") os_log("网络仍未恢复,继续检测", log: self.log, type: .debug) self.networkLostCount = 0 self.stopContinuousRecognition() } } } - - + /** * 预初始化音频组件 * 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟 @@ -243,18 +242,17 @@ public class AzureAsrHelper: NSObject { audioStream = SimpleAudioReceiver() audioStream?.initAudioRecord() } - // 预创建音频配置 + // 预创建音频配置 if let pushStream = audioStream?.pushAudioStream { - audioConfig = SPXAudioConfiguration(streamInput: pushStream) - os_log("预初始化音频组件完成", log: log, type: .info) + audioConfig = SPXAudioConfiguration(streamInput: pushStream) + os_log("预初始化音频组件完成", log: log, type: .info) } - + } - - + /** * 异步启动音频录制(带防抖与状态二次校验) - * + * * @param audioSourceType 音频源类型 * @param audioDataCallback 音频数据回调 * @param completion 完成回调(true/false) @@ -268,79 +266,83 @@ public class AzureAsrHelper: NSObject { /// 修复点: /// 1) 新任务创建前,取消旧的未执行任务,确保只有“最后一次启动请求”会生效 /// 2) 任务执行前二次校验:必须是当前挂起任务且 audioStream?.isContinuousRecognitionActive 仍为 true 才执行启动 - private func startAudioRecordAsync( - mod: String, - audioSourceType: AudioSourceType, - audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, - completion: @escaping (Bool) -> Void -) { - let currentTime = Int64(Date().timeIntervalSince1970 * 1000) - - // 防抖:距离上次启动过短,取消旧任务 - if currentTime - lastAudioStartTime < audioOperationDebounceInterval { - audioStartWorkItem?.cancel() - os_log("音频启动操作被防抖过滤,距离上次启动仅%{public}dms", log: log, type: .debug, currentTime - lastAudioStartTime) - } else { - // 即使未触发防抖,也保证旧任务不会残留 - audioStartWorkItem?.cancel() - } - lastAudioStartTime = currentTime - - // 创建新的启动任务 - let workItem = DispatchWorkItem { [weak self] in - guard let self = self else { - completion(false) - return + private func startAudioRecordAsync( + mod: String, + audioSourceType: AudioSourceType, + audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, + completion: @escaping (Bool) -> Void + ) { + let currentTime = Int64(Date().timeIntervalSince1970 * 1000) + + // 防抖:距离上次启动过短,取消旧任务 + if currentTime - lastAudioStartTime < audioOperationDebounceInterval { + audioStartWorkItem?.cancel() + os_log( + "音频启动操作被防抖过滤,距离上次启动仅%{public}dms", log: log, type: .debug, + currentTime - lastAudioStartTime) + } else { + // 即使未触发防抖,也保证旧任务不会残留 + audioStartWorkItem?.cancel() } - // print("startAudioRecordAsync\(audioStream?.isContinuousRecognitionActive)") - // // 二次校验:stop 可能已发生,确认仍需启动 - // guard self.audioStream?.isContinuousRecognitionActive else { - // os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info) - // completion(false) - // return - // } - - os_log("开始异步启动音频录制", log: self.log, type: .info) - - // 使用串行队列,保证与 stop 顺序执行,避免竞态 - self.audioOpQueue.async { [weak self] in + lastAudioStartTime = currentTime + + // 创建新的启动任务 + let workItem = DispatchWorkItem { [weak self] in guard let self = self else { - DispatchQueue.main.async { completion(false) } + completion(false) return } - - // 准备启动 - self.isAudioStarting = true - self.pendingStopRequest = false // 启动前清空旧的停止请求 - let success = self.performAudioStart(mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) - self.isAudioStarting = false - self.isAudioStarted = success - - // 如果启动过程中有 stop 请求,立即顺序停止,确保麦克风不会被重新“开启” - if self.pendingStopRequest { - self.pendingStopRequest = false - _ = self.performAudioStop() - self.isAudioStarted = false - os_log("启动完成后检测到待停止请求,已立即停止音频", log: self.log, type: .info) - } - - // 回到主队列执行完成回调 - DispatchQueue.main.async { - completion(success) + // print("startAudioRecordAsync\(audioStream?.isContinuousRecognitionActive)") + // // 二次校验:stop 可能已发生,确认仍需启动 + // guard self.audioStream?.isContinuousRecognitionActive else { + // os_log("已收到停止请求,取消启动音频录制", log: self.log, type: .info) + // completion(false) + // return + // } + + os_log("开始异步启动音频录制", log: self.log, type: .info) + + // 使用串行队列,保证与 stop 顺序执行,避免竞态 + self.audioOpQueue.async { [weak self] in + guard let self = self else { + DispatchQueue.main.async { completion(false) } + return + } + + // 准备启动 + self.isAudioStarting = true + self.pendingStopRequest = false // 启动前清空旧的停止请求 + let success = self.performAudioStart( + mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback + ) + self.isAudioStarting = false + self.isAudioStarted = success + + // 如果启动过程中有 stop 请求,立即顺序停止,确保麦克风不会被重新“开启” + if self.pendingStopRequest { + self.pendingStopRequest = false + _ = self.performAudioStop() + self.isAudioStarted = false + os_log("启动完成后检测到待停止请求,已立即停止音频", log: self.log, type: .info) + } + + // 回到主队列执行完成回调 + DispatchQueue.main.async { + completion(success) + } } } + + // 保存"当前挂起任务" + audioStartWorkItem = workItem + + // 延迟执行以实现防抖 + DispatchQueue.main.asyncAfter( + deadline: .now() + .milliseconds(Int(audioOperationDebounceInterval)), + execute: workItem + ) } - - // 保存"当前挂起任务" - audioStartWorkItem = workItem - - // 延迟执行以实现防抖 - DispatchQueue.main.asyncAfter( - deadline: .now() + .milliseconds(Int(audioOperationDebounceInterval)), - execute: workItem - ) -} - + /** * 执行音频启动操作 */ @@ -350,7 +352,7 @@ public class AzureAsrHelper: NSObject { audioDataCallback: SimpleAudioReceiver.AudioDataCallback? ) -> Bool { do { - if(!audioStream!.isContinuousRecognitionActive){ + if !audioStream!.isContinuousRecognitionActive { audioStream?.startAudioRecord( mod: mod, audioSourceType: audioSourceType == .microphone ? .microphone : .external, @@ -364,15 +366,15 @@ public class AzureAsrHelper: NSObject { return false } } - + /** * 异步停止音频录制(移除防抖机制,立即执行) - * + * * @param completion 完成回调 */ private func stopAudioRecordAsync(completion: @escaping (Bool) -> Void) { os_log("开始立即停止音频录制", log: log, type: .info) - + // 在串行队列顺序执行停止,避免与启动并发导致的竞态 audioOpQueue.async { [weak self] in guard let self = self else { @@ -385,19 +387,19 @@ public class AzureAsrHelper: NSObject { } let success = self.performAudioStop() self.isAudioStarted = false - + // 回到主队列执行完成回调 DispatchQueue.main.async { completion(success) } } } - + /** * 执行音频停止操作 */ private func performAudioStop() -> Bool { - do { + do { audioStream?.stopMicrophoneCapture() os_log("音频录制停止成功", log: log, type: .info) return true @@ -409,7 +411,7 @@ public class AzureAsrHelper: NSObject { /** * 开始连续语音识别(优化:在音频启动成功后再次校验活跃状态) - * + * * @param audioSourceType 音频源类型 * @param audioDataCallback 音频数据回调 * @param isRemoveFirstPunctuation 是否移除第一个标点符号 @@ -418,127 +420,138 @@ public class AzureAsrHelper: NSObject { * 修复点:在音频启动成功后的闭包中,先判断 audioStream?.isContinuousRecognitionActive 是否仍然为 true, * 若 stop 已发生则直接跳过 recognizer 的启动,避免“已停止但仍启动识别”的情况。 */ - public func startContinuousRecognition( - mod: String = "normal", - audioSourceType: AudioSourceType = .microphone, - audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, - isRemoveFirstPunctuation: Bool = true -) -> Bool { - guard speechConfig != nil else { - print("语音服务未初始化") - return false - } - - // 检查网络状态 - if !checkNetworkStatus() { - return false - } - - if audioStream?.isContinuousRecognitionActive == true { - print("连续识别已在进行中") - return true - } - - // 先取消任何挂起的启动任务 - cancelPendingAudioStart() - - // 延迟设置状态,直到音频启动任务真正开始执行 - self.audioSourceType = audioSourceType - print("startContinuousRecognition:准备启动") - - // 如果使用讯飞识别 - if useXunfei { - print("Using Xunfei Recognition") - guard let callback = self.continuousCallback else { - print("无法启动连续识别:回调为空") - return false + public func startContinuousRecognition( + mod: String = "normal", + audioSourceType: AudioSourceType = .microphone, + audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, + isRemoveFirstPunctuation: Bool = true + ) -> Bool { + guard speechConfig != nil else { + print("语音服务未初始化") + return false } - - if xunFeiAsrHelper == nil { - xunFeiAsrHelper = XunFeiAsrHelper( - appId: xunfeiAppId, - accessKeyId: xunfeiAccessKeyId, - accessKeySecret: xunfeiAccessKeySecret - ) - } else { - xunFeiAsrHelper?.updateConfig( - appId: xunfeiAppId, - accessKeyId: xunfeiAccessKeyId, - accessKeySecret: xunfeiAccessKeySecret - ) + + // 检查网络状态 + if !checkNetworkStatus() { + return false } - - // 启动讯飞识别 - xunFeiAsrHelper?.start(callback: callback, language: currentLanguage, isAutoDetect: isAutoDetectLanguage, isRemoveFirstPunctuation: isRemoveFirstPunctuation) - - // 创建代理回调,将音频数据转发给讯飞 - let proxyCallback = AudioDataCallbackProxy(original: audioDataCallback) { [weak self] data in - self?.xunFeiAsrHelper?.sendAudio(frameBuffer: data) + + if audioStream?.isContinuousRecognitionActive == true { + print("连续识别已在进行中") + return true } - - // 异步启动音频处理 - startAudioRecordAsync(mod: mod, audioSourceType: audioSourceType, audioDataCallback: proxyCallback) { [weak self] (success: Bool) in - guard let self = self else { return } - - if success { - self.audioStream?.isContinuousRecognitionActive = true - os_log("讯飞连续识别已启动", log: self.log, type: .info) + + // 先取消任何挂起的启动任务 + cancelPendingAudioStart() + + // 延迟设置状态,直到音频启动任务真正开始执行 + self.audioSourceType = audioSourceType + print("startContinuousRecognition:准备启动") + + // 如果使用讯飞识别 + if useXunfei { + print("Using Xunfei Recognition") + guard let callback = self.continuousCallback else { + print("无法启动连续识别:回调为空") + return false + } + + if xunFeiAsrHelper == nil { + xunFeiAsrHelper = XunFeiAsrHelper( + appId: xunfeiAppId, + accessKeyId: xunfeiAccessKeyId, + accessKeySecret: xunfeiAccessKeySecret + ) } else { - os_log("音频启动失败,无法开始讯飞识别", log: self.log, type: .error) - self.xunFeiAsrHelper?.stop() - self.audioStream?.isContinuousRecognitionActive = false + xunFeiAsrHelper?.updateConfig( + appId: xunfeiAppId, + accessKeyId: xunfeiAccessKeyId, + accessKeySecret: xunfeiAccessKeySecret + ) } - } - - return true - } - - do { - if recognizer == nil { - // 检查continuousCallback是否为空,只有在不为空时才设置事件监听器 - if let callback = self.continuousCallback { - setupEventListeners(callback: callback) + + // 启动讯飞识别 + xunFeiAsrHelper?.start( + callback: callback, language: currentLanguage, isAutoDetect: isAutoDetectLanguage, + isRemoveFirstPunctuation: isRemoveFirstPunctuation) + + // 创建代理回调,将音频数据转发给讯飞 + let proxyCallback = AudioDataCallbackProxy(original: audioDataCallback) { + [weak self] data in + self?.xunFeiAsrHelper?.sendAudio(frameBuffer: data) } + + // 异步启动音频处理 + startAudioRecordAsync( + mod: mod, audioSourceType: audioSourceType, audioDataCallback: proxyCallback + ) { [weak self] (success: Bool) in + guard let self = self else { return } + + if success { + self.audioStream?.isContinuousRecognitionActive = true + os_log("讯飞连续识别已启动", log: self.log, type: .info) + } else { + os_log("音频启动失败,无法开始讯飞识别", log: self.log, type: .error) + self.xunFeiAsrHelper?.stop() + self.audioStream?.isContinuousRecognitionActive = false + } + } + + return true } - - print("startContinuousRecognition:\(audioSourceType)") - // 移除这里的状态设置,等音频启动成功后再设置 - // self.audioStream?.isContinuousRecognitionActive = true - - // 异步启动音频处理 - startAudioRecordAsync(mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in - guard let self = self else { return } - - if success { - // 只有在音频启动成功后才设置状态为 true - self.audioStream?.isContinuousRecognitionActive = true - print("startContinuousRecognition:\(self.audioStream?.isContinuousRecognitionActive)") - - // 启动语音识别 - do { - try self.recognizer?.startContinuousRecognition() - os_log("连续识别已启动,音频源: %{public}@", log: self.log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部") - } catch { - os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription) - self.stopContinuousRecognition() + + do { + if recognizer == nil { + // 检查continuousCallback是否为空,只有在不为空时才设置事件监听器 + if let callback = self.continuousCallback { + setupEventListeners(callback: callback) + } + } + + print("startContinuousRecognition:\(audioSourceType)") + // 移除这里的状态设置,等音频启动成功后再设置 + // self.audioStream?.isContinuousRecognitionActive = true + + // 异步启动音频处理 + startAudioRecordAsync( + mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback + ) { [weak self] (success: Bool) in + guard let self = self else { return } + + if success { + // 只有在音频启动成功后才设置状态为 true + self.audioStream?.isContinuousRecognitionActive = true + print( + "startContinuousRecognition:\(self.audioStream?.isContinuousRecognitionActive)" + ) + + // 启动语音识别 + do { + try self.recognizer?.startContinuousRecognition() + os_log( + "连续识别已启动,音频源: %{public}@", log: self.log, type: .info, + audioSourceType == .microphone ? "麦克风" : "外部") + } catch { + os_log( + "启动连续识别失败: %{public}@", log: self.log, type: .error, + error.localizedDescription) + self.stopContinuousRecognition() + self.audioStream?.isContinuousRecognitionActive = false + } + } else { + os_log("音频启动失败,无法开始识别", log: self.log, type: .error) self.audioStream?.isContinuousRecognitionActive = false } - } else { - os_log("音频启动失败,无法开始识别", log: self.log, type: .error) - self.audioStream?.isContinuousRecognitionActive = false } + + return true + } catch { + stopContinuousRecognition() + audioStream?.isContinuousRecognitionActive = false + return false } - - return true - } catch { - stopContinuousRecognition() - audioStream?.isContinuousRecognitionActive = false - return false } -} - - /** * 停止连续语音识别(优化:确保取消未执行的启动任务) * - 返回: 停止操作是否成功 @@ -546,16 +559,16 @@ public class AzureAsrHelper: NSObject { public func stopContinuousRecognition() -> Bool { if useXunfei { xunFeiAsrHelper?.stop() - + // 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动 cancelPendingAudioStart() - + stopAudioRecordAsync { [weak self] (success: Bool) in - guard let self = self else { return } - if success { - self.audioStream?.isContinuousRecognitionActive = false - os_log("讯飞音频停止成功", log: self.log, type: .info) - } + guard let self = self else { return } + if success { + self.audioStream?.isContinuousRecognitionActive = false + os_log("讯飞音频停止成功", log: self.log, type: .info) + } } self.audioStream?.isContinuousRecognitionActive = false return true @@ -565,30 +578,29 @@ public class AzureAsrHelper: NSObject { os_log("语音服务未初始化", log: log, type: .error) return false } - - + do { - + // 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动 cancelPendingAudioStart() - + // 异步停止音频处理 stopAudioRecordAsync { [weak self] (success: Bool) in guard let self = self else { return } if success { - // 标记不再活跃,供启动任务二次校验 + // 标记不再活跃,供启动任务二次校验 self.audioStream?.isContinuousRecognitionActive = false os_log("音频停止成功", log: self.log, type: .info) } else { os_log("音频停止失败", log: self.log, type: .error) } } - guard let recognizer = recognizer else { - os_log("识别器为空,重置状态", log: log, type: .info) - // 仍需取消潜在的挂起启动任务,避免稍后被触发 - cancelPendingAudioStart() - return true - } + guard let recognizer = recognizer else { + os_log("识别器为空,重置状态", log: log, type: .info) + // 仍需取消潜在的挂起启动任务,避免稍后被触发 + cancelPendingAudioStart() + return true + } // 停止连续识别 try recognizer.stopContinuousRecognition() self.recognizer = nil @@ -597,32 +609,32 @@ public class AzureAsrHelper: NSObject { } catch { // 强制重置状态 audioStream?.isContinuousRecognitionActive = false - + // 关键:即使异常,也取消未执行的启动任务 cancelPendingAudioStart() - + os_log("强制停止识别失败", log: log, type: .error) - + // 异步停止音频处理 stopAudioRecordAsync { [weak self] (_: Bool) in guard let self = self else { return } os_log("强制停止音频处理完成", log: self.log, type: .info) } - + // 尝试安全地停止识别器 do { - guard let recognizer = recognizer else { - os_log("识别器为空,重置状态", log: log, type: .info) - // 仍需取消潜在的挂起启动任务,避免稍后被触发 - cancelPendingAudioStart() - return true - } + guard let recognizer = recognizer else { + os_log("识别器为空,重置状态", log: log, type: .info) + // 仍需取消潜在的挂起启动任务,避免稍后被触发 + cancelPendingAudioStart() + return true + } try recognizer.stopContinuousRecognition() self.recognizer = nil } catch { os_log("强制停止识别器失败: %@", log: log, type: .error, error.localizedDescription) } - + return false } } @@ -633,7 +645,7 @@ public class AzureAsrHelper: NSObject { public func isContinuousRecognitionActive() -> Bool { return self.audioStream?.isContinuousRecognitionActive == true } - + /** * 释放资源 */ @@ -650,19 +662,19 @@ public class AzureAsrHelper: NSObject { } audioStream?.isContinuousRecognitionActive = false } - // 取消防抖任务 - 只取消开始操作的防抖 - audioStartWorkItem?.cancel() + // 取消防抖任务 - 只取消开始操作的防抖 + audioStartWorkItem?.cancel() // 停止音频处理 audioStream?.releaseAudioResources() - // 释放网络监听资源 + // 释放网络监听资源 networkMonitor.dispose() // 释放资源 recognizer = nil speechConfig = nil audioConfig = nil - audioStream = nil - xunFeiAsrHelper?.stop() - xunFeiAsrHelper = nil + audioStream = nil + xunFeiAsrHelper?.stop() + xunFeiAsrHelper = nil // 确保状态被重置 audioStream?.isContinuousRecognitionActive = false //externalAudioStream = nil @@ -671,9 +683,9 @@ public class AzureAsrHelper: NSObject { audioStream?.isContinuousRecognitionActive = false //externalAudioStream = nil audioConfig = nil - recognizer = nil - speechConfig = nil - audioStream = nil + recognizer = nil + speechConfig = nil + audioStream = nil } } @@ -686,10 +698,10 @@ public class AzureAsrHelper: NSObject { public func setAudioConfig(sampleRate: Int, channels: Int) -> Bool { do { print("设置音频配置 - 采样率: \(sampleRate), 声道: \(channels)") - + // 设置音频接收器的配置 audioStream?.setAudioConfig(sampleRate: sampleRate, channels: channels) - + print("音频配置设置成功") return true } catch { @@ -698,43 +710,45 @@ public class AzureAsrHelper: NSObject { } } - /** * 设置识别器 */ private func setupRecognizer() -> Bool { do { print("设置识别器\(audioStream?.isContinuousRecognitionActive ?? false)") - + // 如果正在进行连续识别,先停止 - if audioStream?.isContinuousRecognitionActive == true { - try? recognizer?.stopContinuousRecognition() - audioStream?.isContinuousRecognitionActive = false - } - + if audioStream?.isContinuousRecognitionActive == true { + try? recognizer?.stopContinuousRecognition() + audioStream?.isContinuousRecognitionActive = false + } + // 【优化】只有在音频流未初始化时才创建,避免重复初始化 if audioStream == nil || audioConfig == nil { setupMicrophoneStream() } - + // 安全检查:确保 audioConfig 不为 nil guard let audioConfig = audioConfig else { os_log("音频配置未初始化,无法创建识别器", log: log, type: .error) return false } - + guard let speechConfig = speechConfig else { os_log("语音配置未初始化,无法创建识别器", log: log, type: .error) return false } - + // 清理旧的识别器 recognizer = nil - + // 创建识别器 if isAutoDetectLanguage { // 使用正确的初始化方法 - guard let autoDetectSourceLanguageConfig = try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) else { + guard + let autoDetectSourceLanguageConfig = + try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) + else { print("创建自动语言检测配置失败,使用默认语言配置") // 如果创建失败,使用默认的单语言配置 recognizer = try SPXSpeechRecognizer( @@ -743,7 +757,7 @@ public class AzureAsrHelper: NSObject { ) return true } - print("创建自动语言识别器") + print("创建自动语言识别器") recognizer = try SPXSpeechRecognizer( speechConfiguration: speechConfig, autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig, @@ -755,7 +769,7 @@ public class AzureAsrHelper: NSObject { audioConfiguration: audioConfig ) } - + return true } catch { os_log("创建识别器失败: %{public}@", log: log, type: .error, error.localizedDescription) @@ -763,8 +777,7 @@ public class AzureAsrHelper: NSObject { return false } } - - + /** * 设置麦克风流 - 使用推流方式 * 优化:避免重复初始化已存在的音频流 @@ -779,16 +792,14 @@ public class AzureAsrHelper: NSObject { audioStream?.initAudioRecord() // 确保调用初始化 } - + // 检查音频配置是否已存在 if audioConfig == nil, let pushStream = audioStream?.pushAudioStream { audioConfig = SPXAudioConfiguration(streamInput: pushStream) os_log("设置麦克风流完成", log: log, type: .info) } } - - - + /** * 从语音识别结果中获取检测到的语言 * @param result 语音识别结果 @@ -798,18 +809,20 @@ public class AzureAsrHelper: NSObject { guard isAutoDetectLanguage else { return currentLanguage } - - print("result: \(result)") - + + print("result: \(result)") + // 备用方案:从属性中获取语言 if let properties = result.properties, - let language = properties.getPropertyByName("speechServiceResponse_RecognitionLanguage"), - !language.isEmpty { + let language = properties.getPropertyByName( + "speechServiceResponse_RecognitionLanguage"), + !language.isEmpty + { print("从属性中检测到语言: \(language)") return language } - - // 使用正确的SPXAutoDetectSourceLanguageResult初始化方法 + + // 使用正确的SPXAutoDetectSourceLanguageResult初始化方法 do { let langResult = try SPXAutoDetectSourceLanguageResult(result) if let language = langResult.language, !language.isEmpty { @@ -822,12 +835,10 @@ public class AzureAsrHelper: NSObject { print("无法检测语言,使用默认语言: \(currentLanguage)") return currentLanguage } - - - + /** * 动态切换语言(翻译场景中的语言切换) - * + * * @param newSourceLanguage 新的源语言 * @param newTargetLanguage 新的目标语言 * @return 切换是否成功 @@ -838,17 +849,17 @@ public class AzureAsrHelper: NSObject { ) -> Bool { let validatedSourceLang = LanguageDetector.convertToAzureLanguageCode(newSourceLanguage) let validatedTargetLang = LanguageDetector.convertToAzureLanguageCode(newTargetLanguage) - + // 更新支持的语言 self.supportedLanguages = [validatedSourceLang, validatedTargetLang] self.currentLanguage = validatedSourceLang - + // 更新是否使用讯飞 self.useXunfei = supportedLanguages.allSatisfy { $0 == "zh-CN" || $0 == "en-US" } - + // 重新配置语言检测 self.isAutoDetectLanguage = (validatedSourceLang != validatedTargetLang) - + // 如果正在识别,需要重新设置识别器 if audioStream?.isContinuousRecognitionActive == true { let wasActive = stopContinuousRecognition() @@ -856,8 +867,10 @@ public class AzureAsrHelper: NSObject { return setupRecognizer() } } - - os_log("语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, validatedTargetLang) + + os_log( + "语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, + validatedTargetLang) return true } /** @@ -865,53 +878,56 @@ public class AzureAsrHelper: NSObject { */ public func setupEventListeners(callback: ContinuousRecognizeCallback) -> Bool { print("设置ssssss监听器:${speechConfig}") - + // 保存回调引用用于网络事件 self.continuousCallback = callback - + // 重设识别器 - if (!setupRecognizer()) { + if !setupRecognizer() { return false } guard let recognizer = recognizer else { return false } - + // 正在识别事件 - recognizer.addRecognizingEventHandler { [weak self] (_, event) in - guard let self = self else { return } - let result = event.result - guard !(result.text?.isEmpty ?? true) else { return } - - // 使用现有的 getDetectedLanguage 方法替代 result.language - let detectedLanguage = self.getDetectedLanguage(from: result) - let text = result.text - - print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") - - DispatchQueue.main.async { - callback.onRecognizing(sessionid: self.currsessionId,result.text ?? "", detectedLanguage) + recognizer.addRecognizingEventHandler { [weak self] (_, event) in + guard let self = self else { return } + let result = event.result + guard !(result.text?.isEmpty ?? true) else { return } + + // 使用现有的 getDetectedLanguage 方法替代 result.language + let detectedLanguage = self.getDetectedLanguage(from: result) + let text = result.text + + print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") + + DispatchQueue.main.async { + callback.onRecognizing( + sessionid: self.currsessionId, result.text ?? "", detectedLanguage) + } } - } - // 识别完成事件 - recognizer.addRecognizedEventHandler { [weak self] (_, event) in + // 识别完成事件 + recognizer.addRecognizedEventHandler { [weak self] (_, event) in guard let self = self else { return } - let result = event.result - guard result.reason == .recognizedSpeech, - !(result.text?.isEmpty ?? true) else { return } - - // 使用现有的 getDetectedLanguage 方法替代 result.language - let detectedLanguage = self.getDetectedLanguage(from: result) - let text = result.text - - print("识别完成事件=检测到语言: \(detectedLanguage), 识别中: \(text)") - - // 在主线程回调结果 - DispatchQueue.main.async { - callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage) - self.currsessionId = UUID().uuidString + let result = event.result + guard result.reason == .recognizedSpeech, + !(result.text?.isEmpty ?? true) + else { return } + + // 使用现有的 getDetectedLanguage 方法替代 result.language + let detectedLanguage = self.getDetectedLanguage(from: result) + let text = result.text + + print("识别完成事件=检测到语言: \(detectedLanguage), 识别中: \(text)") + + // 在主线程回调结果 + DispatchQueue.main.async { + callback.onResult( + sessionid: self.currsessionId, result.text ?? "", detectedLanguage) + self.currsessionId = UUID().uuidString + } + } - - } // 会话开始事件 recognizer.addSessionStartedEventHandler { (sender, event) in // 直接在当前线程调用回调 @@ -919,59 +935,57 @@ public class AzureAsrHelper: NSObject { callback.onSessionStarted(sessionid: self.currsessionId) print("会话开始事件:") } - + // 会话结束事件 recognizer.addSessionStoppedEventHandler { [weak self] (sender, event) in guard let self = self else { return } print("会话结束事件:") // 直接在当前线程调用回调 - callback.onSessionStopped(sessionid: self.currsessionId) - self.audioStream?.isContinuousRecognitionActive = false + callback.onSessionStopped(sessionid: self.currsessionId) + self.audioStream?.isContinuousRecognitionActive = false //self.stopAudioProcessing() } - + // 取消事件 recognizer.addCanceledEventHandler { [weak self] (_, event) in guard let self = self else { return } print("取消事件:") let errorDetails = event.errorDetails ?? "未知错误" let reason = String(describing: event.reason.rawValue) - + os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails) - - - callback.onCanceled(sessionid: self.currsessionId,reason, errorDetails) - + + callback.onCanceled(sessionid: self.currsessionId, reason, errorDetails) + } return true } - /** * 禁用耳机进行拾音 */ - public func disableBluetoothAudio() { - - do { - print("禁用耳机进行拾音") - audioStream?.isHeadphones = true - } catch { - print("disableBluetoothAudio") + public func disableBluetoothAudio() { + + do { + print("禁用耳机进行拾音") + audioStream?.isHeadphones = true + } catch { + print("disableBluetoothAudio") } } - /** + /** * 恢复耳机进行拾音 */ public func restoreOriginalAudioState() { - - do { - print("恢复耳机进行拾音") - audioStream?.isHeadphones = false - } catch { - print("restoreOriginalAudioState") + + do { + print("恢复耳机进行拾音") + audioStream?.isHeadphones = false + } catch { + print("restoreOriginalAudioState") } } - + // MARK: - 录音音频文件方法 /** @@ -980,11 +994,14 @@ public class AzureAsrHelper: NSObject { * @param filePath 录音文件路径 * @param audioDataCallback 音频数据回调接口 */ - public func enableRecord( audioSourceType: AudioSourceType = .microphone,filePath: String, audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil) { + public func enableRecord( + audioSourceType: AudioSourceType = .microphone, filePath: String, + audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil + ) { os_log("开启录音: %{public}@", log: log, type: .info, filePath) - self.audioSourceType = audioSourceType - if(audioSourceType == .external){ - os_log("外部音频源不在这里录音", log: log, type: .info) + self.audioSourceType = audioSourceType + if audioSourceType == .external { + os_log("外部音频源不在这里录音", log: log, type: .info) return } if audioStream == nil { @@ -992,60 +1009,59 @@ public class AzureAsrHelper: NSObject { audioStream = SimpleAudioReceiver() audioStream?.initAudioRecord() } - - if audioStream?.recordfile == nil { + + if audioStream?.recordfile == nil { audioStream?.recordfile = RecordFile() } - - + // 修复:移除多余的 audioDataCallback 参数 - // 修复:添加类型转换 - if(!audioStream!.isContinuousRecognitionActive){ - // 异步启动音频处理 - startAudioRecordAsync(mod: "normal", audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in - guard let self = self else { return } - + // 修复:添加类型转换 + if !audioStream!.isContinuousRecognitionActive { + // 异步启动音频处理 + startAudioRecordAsync( + mod: "normal", audioSourceType: audioSourceType, + audioDataCallback: audioDataCallback + ) { [weak self] (success: Bool) in + guard let self = self else { return } + + } } - } audioStream?.isRecord = true audioStream?.recordfile?.closeFile(isSave: true) audioStream?.recordfile?.creatingFiles(atPath: filePath) } - + /** * 移动文件到新路径 */ - public func moveFile(sourcePath: String, destPath: String)-> Bool { - - if(audioSourceType == .external){ + public func moveFile(sourcePath: String, destPath: String) -> Bool { + + if audioSourceType == .external { os_log(" 外部音频源不在这里移动文件", log: log, type: .info) return true } - audioStream?.recordfile?.moveFile(from: sourcePath, to: destPath) // Fixed method call + audioStream?.recordfile?.moveFile(from: sourcePath, to: destPath) // Fixed method call return true } - - + /** * 重命名指定路径的音频文件 */ - public func renameFile(filePath: String, newName: String)-> Bool { - if(audioSourceType == .external){ + public func renameFile(filePath: String, newName: String) -> Bool { + if audioSourceType == .external { os_log(" 外部音频源不在这里重命名文件", log: log, type: .info) return true } - audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call + audioStream?.recordfile?.renameFile(at: filePath, to: newName) // Fixed method call return true } - - - /** - * 暂停录音 - */ + /** + * 暂停录音 + */ public func pauseRecord() { - if(audioSourceType == .external){ - os_log(" 外部音频源不在这里暂停录音", log: log, type: .info) + if audioSourceType == .external { + os_log(" 外部音频源不在这里暂停录音", log: log, type: .info) return } os_log("暂停录音", log: log, type: .info) @@ -1059,40 +1075,36 @@ public class AzureAsrHelper: NSObject { * 继续录音 */ public func resumeRecord() { - if(audioSourceType == .external){ - os_log(" 外部音频源不在这里继续录音", log: log, type: .info) + if audioSourceType == .external { + os_log(" 外部音频源不在这里继续录音", log: log, type: .info) return } os_log("继续录音", log: log, type: .info) guard let audioStream = audioStream, audioStream.recordfile != nil else { return } - audioStream.isRecord = true + audioStream.isRecord = true } /** * 关闭录音 */ public func stopRecord(isSave: Bool) { - if(audioSourceType == .external){ - os_log(" 外部音频源不在这里关闭录音", log: log, type: .info) + if audioSourceType == .external { + os_log(" 外部音频源不在这里关闭录音", log: log, type: .info) return } guard let audioStream = audioStream, audioStream.recordfile != nil else { return } - audioStream.isRecord = false - if (!audioStream.isContinuousRecognitionActive) { + audioStream.isRecord = false + if !audioStream.isContinuousRecognitionActive { audioStream.stopMicrophoneCapture() } - audioStream.recordfile?.isPause = false // - audioStream.recordfile?.closeFile(isSave: true) // + audioStream.recordfile?.isPause = false // + audioStream.recordfile?.closeFile(isSave: true) // } - - - - // MARK: - 回调函数 /** * 连续识别回调接口 @@ -1100,76 +1112,76 @@ public class AzureAsrHelper: NSObject { public protocol ContinuousRecognizeCallback { /** * 返回识别结果 - * + * * @param text 识别的文本 * @param detectedLanguage 检测到的语言 */ - func onResult(sessionid:String,_ text: String, _ detectedLanguage: String) - + func onResult(sessionid: String, _ text: String, _ detectedLanguage: String) + /** * 识别进行中调用 - * + * * @param recognizing 正在识别的文本 * @param detectedLanguage 检测到的语言 */ - func onRecognizing(sessionid:String ,_ recognizing: String, _ detectedLanguage: String) - + func onRecognizing(sessionid: String, _ recognizing: String, _ detectedLanguage: String) + /** * 会话开始时调用 */ - func onSessionStarted(sessionid:String) - + func onSessionStarted(sessionid: String) + /** * 会话结束时调用 */ - func onSessionStopped(sessionid:String) - + func onSessionStopped(sessionid: String) + /** * 识别取消时调用 - * + * * @param reason 取消原因 * @param errorDetails 错误详情 */ - func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) - + func onCanceled(sessionid: String, _ reason: String, _ errorDetails: String) + /** * 识别出错时调用 - * + * * @param error 错误信息 */ - func onError(sessionid:String ,_ code: Int,_ error: String) + func onError(sessionid: String, _ code: Int, _ error: String) } /** * 检查网络状态 * @return 网络是否可用 */ -private func checkNetworkStatus() -> Bool { - // 直接使用 NetworkStateMonitor 的 checkNetworkStatus 方法 - let isAvailable = networkMonitor.checkNetworkStatus() - - if !isAvailable { - os_log("网络不可用,无法启动语音识别", log: log, type: .error) - continuousCallback?.onError(sessionid: self.currsessionId,1000,"网络连接不可用,请检查网络设置") + private func checkNetworkStatus() -> Bool { + // 直接使用 NetworkStateMonitor 的 checkNetworkStatus 方法 + let isAvailable = networkMonitor.checkNetworkStatus() + + if !isAvailable { + os_log("网络不可用,无法启动语音识别", log: log, type: .error) + continuousCallback?.onError(sessionid: self.currsessionId, 1000, "网络连接不可用,请检查网络设置") + } + + return isAvailable } - - return isAvailable -} -/** - * 判断是否为网络相关错误 - * @param errorDetails 错误详情 - * @return 是否为网络错误 - */ -private func isNetworkRelatedError(_ errorDetails: String) -> Bool { - let networkErrorKeywords = [ - "network", "connection", "timeout", "unreachable", - "网络", "连接", "超时", "不可达", - "ConnectionFailure", "NetworkError", "TimeoutError" - ] - - let lowercaseError = errorDetails.lowercased() - return networkErrorKeywords.contains { lowercaseError.contains($0.lowercased()) } -} + /** + * 判断是否为网络相关错误 + * @param errorDetails 错误详情 + * @return 是否为网络错误 + */ + private func isNetworkRelatedError(_ errorDetails: String) -> Bool { + let networkErrorKeywords = [ + "network", "connection", "timeout", "unreachable", + "网络", "连接", "超时", "不可达", + "ConnectionFailure", "NetworkError", "TimeoutError", + ] + + let lowercaseError = errorDetails.lowercased() + return networkErrorKeywords.contains { lowercaseError.contains($0.lowercased()) } + } } // MARK: - NetworkStateListener实现 @@ -1179,16 +1191,14 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener { */ func onNetworkAvailable() { os_log("网络已恢复", log: log, type: .info) - + self.handleNetworkAvailable() - + } - - - + func onNetworkLost() { os_log("网络连接丢失", log: log, type: .error) - + self.handleNetworkLost() } // MARK: - 启动任务管理 @@ -1197,24 +1207,22 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener { audioStartWorkItem?.cancel() audioStartWorkItem = nil } - + } // MARK: - AudioDataCallback Proxy private class AudioDataCallbackProxy: SimpleAudioReceiver.AudioDataCallback { private let originalCallback: SimpleAudioReceiver.AudioDataCallback? private let xunfeiCallback: ((Data) -> Void)? - + init(original: SimpleAudioReceiver.AudioDataCallback?, xunfei: ((Data) -> Void)?) { self.originalCallback = original self.xunfeiCallback = xunfei } - + func onAudio(_ data: Data) { originalCallback?.onAudio(data) xunfeiCallback?(data) } } - - From 12a49c59cdf7ba860e9388ba2df7016895387c1c Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 3 Feb 2026 10:19:34 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0ASR=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E6=97=A5=E5=BF=97=E4=BB=A5=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=B0=83=E8=AF=95=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在iOS和Android平台添加ASR引擎初始化时的详细日志记录,包括支持的语音、区域配置、音频源类型以及讯飞配置状态。这些日志有助于在部署和生产环境中快速诊断语音识别初始化问题。 --- .../azure_speech/AzureAsrHelper.kt | 11 +++++++++++ .../azure_speech/AzureSpeechPlugin.kt | 7 +++++++ .../Sources/azure_speech/AzureAsrHelper.swift | 18 ++++++++++++++++++ .../azure_speech/AzureSpeechPlugin.swift | 10 ++++++++++ 4 files changed, 46 insertions(+) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 661411b7a..a01e75561 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -127,6 +127,11 @@ class AzureAsrHelper(private val context: Context) { this.supportedLanguages = supportedLanguages } + Log.i( + tag, + "lxm---ASR initialize: supportedLanguages=${supportedLanguages.joinToString(",")}, audioSourceType=$audioSourceType" + ) + val xunfeiConfigReady = this.xunfeiAppId.isNotBlank() && this.xunfeiAccessKeyId.isNotBlank() && this.xunfeiAccessKeySecret.isNotBlank() @@ -140,6 +145,11 @@ class AzureAsrHelper(private val context: Context) { this.currentLanguage = supportedLanguages[0] } + Log.i( + tag, + "lxm---ASR initialize derived: isAutoDetectLanguage=$isAutoDetectLanguage, currentLanguage=$currentLanguage, provider=${getAsrProvider()}, xunfeiConfigured=$xunfeiConfigReady" + ) + // 创建语音配置 speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region).apply { if (isAutoDetectLanguage) { @@ -314,6 +324,7 @@ class AzureAsrHelper(private val context: Context) { } try { + Log.d(tag, "startContinuousRecognition: ${audioStream.isContinuousRecognitionActive}") xunFeiAsrHelper?.start(callback, currentLanguage, isAutoDetectLanguage, isRemoveFirstPunctuation) this.audioSourceType = audioSourceType diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 702f91aca..344d6fba1 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -423,6 +423,13 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val xunfeiAccessKeySecret = call.argument("xunfeiAccessKeySecret") ?: "" try { + val isXunfeiConfigured = xunfeiAppId.isNotBlank() && + xunfeiAccessKeyId.isNotBlank() && + xunfeiAccessKeySecret.isNotBlank() + FileLogger.i( + tag, + "lxm---ASR initialize args: supportedLanguages=${supportedLanguages.joinToString(",")}, useExternalAudio=$useExternalAudio, region=$region, xunfeiConfigured=$isXunfeiConfigured" + ) FileLogger.d(tag, "选择音频源类型: ${useExternalAudio}") // // 选择音频源类型 val audioSourceType = if (useExternalAudio) { diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 7f6928539..e12d4ed03 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -136,9 +136,27 @@ public class AzureAsrHelper: NSObject { $0 == "zh-CN" || $0 == "en-US" || $0 == "zh-HK" || $0 == "zh-TW" } && xunfeiConfigReady + os_log( + "ASR helper initialize: supportedLanguages=%{public}@, audioSourceType=%{public}@, xunfeiConfigured=%{public}@, provider=%{public}@", + log: log, + type: .info, + supportedLanguages.joined(separator: ","), + String(describing: audioSourceType), + String(xunfeiConfigReady), + asrProvider() + ) + // 根据支持的语言数量决定是否启用自动语言检测 self.isAutoDetectLanguage = supportedLanguages.count >= 2 + os_log( + "ASR helper initialize derived: isAutoDetectLanguage=%{public}@, currentLanguage=%{public}@", + log: log, + type: .info, + String(self.isAutoDetectLanguage), + self.currentLanguage + ) + // 如果只有一种语言,设置为当前语言 if !isAutoDetectLanguage && !supportedLanguages.isEmpty { self.currentLanguage = supportedLanguages[0] diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index 1a00c6ef8..a27ec5faa 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -343,6 +343,16 @@ private func sendAstEvent(_ event: [String: Any]) { let xunfeiAppId = args["xunfeiAppId"] as? String ?? "" let xunfeiAccessKeyId = args["xunfeiAccessKeyId"] as? String ?? "" let xunfeiAccessKeySecret = args["xunfeiAccessKeySecret"] as? String ?? "" + let isXunfeiConfigured = !xunfeiAppId.isEmpty && !xunfeiAccessKeyId.isEmpty && !xunfeiAccessKeySecret.isEmpty + + os_log( + "ASR initialize args: supportedLanguages=%{public}@, region=%{public}@, xunfeiConfigured=%{public}@", + log: log, + type: .info, + supportedLanguages.joined(separator: ","), + region, + String(isXunfeiConfigured) + ) // 初始化ASR引擎 let success = azureAsrHelper.initialize( From 02b5cbea76ba648610ee4dfef764d22541e91d31 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 3 Feb 2026 10:40:56 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(azure=5Fspeech):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E6=97=B6=E9=80=89=E6=8B=A9=E6=9C=80=E4=BD=B3=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E8=AF=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 扩展自动语言检测功能,使其能够从支持的语言列表中选择最佳匹配的语言代码。当检测到中文时,优先选择 zh-CN,其次 zh-HK、zh-TW 或其他 zh- 变体;检测到英文时,优先选择 en-US 或其他 en- 变体。这确保了语音识别使用最准确的语言配置,提升了多语言场景下的识别准确性。 --- .../azure_speech/AzureAsrHelper.kt | 8 +- .../azure_speech/XunFeiAsrHelper.kt | 76 ++++++++++++++----- .../Sources/azure_speech/AzureAsrHelper.swift | 5 +- .../azure_speech/XunFeiAsrHelper.swift | 49 +++++++++--- 4 files changed, 107 insertions(+), 31 deletions(-) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index a01e75561..1f6ac4e1e 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -325,7 +325,13 @@ class AzureAsrHelper(private val context: Context) { try { Log.d(tag, "startContinuousRecognition: ${audioStream.isContinuousRecognitionActive}") - xunFeiAsrHelper?.start(callback, currentLanguage, isAutoDetectLanguage, isRemoveFirstPunctuation) + xunFeiAsrHelper?.start( + callback, + currentLanguage, + supportedLanguages.toList(), + isAutoDetectLanguage, + isRemoveFirstPunctuation + ) this.audioSourceType = audioSourceType if (!audioStream.isContinuousRecognitionActive) { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt index 4ebd748ff..039a2bafd 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/XunFeiAsrHelper.kt @@ -34,6 +34,7 @@ class XunFeiAsrHelper(private val context: Context) { private var lastIntermediateResult = "" private var currentLanguage = "zh-CN" + private var supportedLanguages: List = emptyList() private var isAutoDetectLanguage = false // 是否第去除一个文字的标点符号 @@ -58,7 +59,13 @@ class XunFeiAsrHelper(private val context: Context) { return appId.isNotBlank() && accessKeyId.isNotBlank() && accessKeySecret.isNotBlank() } - fun start(callback: AzureAsrHelper.ContinuousRecognizeCallback, language: String, isAutoDetect: Boolean, isRemoveFirstPunctuation: Boolean) { + fun start( + callback: AzureAsrHelper.ContinuousRecognizeCallback, + language: String, + supportedLanguages: List, + isAutoDetect: Boolean, + isRemoveFirstPunctuation: Boolean + ) { try { webSocket?.cancel() webSocket = null @@ -70,6 +77,7 @@ class XunFeiAsrHelper(private val context: Context) { lastIntermediateResult = "" currentLanguage = language + this.supportedLanguages = supportedLanguages isAutoDetectLanguage = isAutoDetect removeFirstPunctuation = isRemoveFirstPunctuation @@ -99,15 +107,7 @@ class XunFeiAsrHelper(private val context: Context) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { Log.d(tag, "WebSocket Closed: $code / $reason") if (currentSessionId.isNotEmpty() && lastIntermediateResult.isNotEmpty()) { - val detectedLanguage = if (isAutoDetectLanguage) { - if (lastIntermediateResult.contains(Regex("[\\u4e00-\\u9fa5]"))) { - "zh-CN" - } else { - "en-US" - } - } else { - currentLanguage - } + val detectedLanguage = selectDetectedLanguage(lastIntermediateResult) callback.onResult(currentSessionId, lastIntermediateResult, detectedLanguage) } callback.onSessionStopped(currentSessionId) @@ -219,15 +219,7 @@ class XunFeiAsrHelper(private val context: Context) { return } - val detectedLanguage = if (isAutoDetectLanguage) { - if (resultTextTemp.contains(Regex("[\\u4e00-\\u9fa5]"))) { - "zh-CN" - } else { - "en-US" - } - } else { - currentLanguage - } + val detectedLanguage = selectDetectedLanguage(resultTextTemp) if (st.optInt("type") == 0) { callback.onResult(currentSessionId, resultTextTemp, detectedLanguage) @@ -254,6 +246,52 @@ class XunFeiAsrHelper(private val context: Context) { } } + private fun normalizeLanguageCode(code: String): String { + val trimmed = code.trim() + if (trimmed.isEmpty()) return trimmed + + val parts = trimmed.split("-") + val lang = parts.getOrNull(0)?.lowercase() ?: trimmed.lowercase() + val region = parts.getOrNull(1)?.uppercase() + return if (region.isNullOrEmpty()) lang else "$lang-$region" + } + + private fun findSupportedExact(target: String): String? { + val targetNorm = normalizeLanguageCode(target) + return supportedLanguages.firstOrNull { normalizeLanguageCode(it) == targetNorm } + } + + private fun findSupportedPrefix(prefix: String): String? { + val p = prefix.lowercase() + return supportedLanguages.firstOrNull { normalizeLanguageCode(it).startsWith(p) } + } + + + private fun pickPreferredChineseLanguage(): String { + return findSupportedExact("zh-CN") + ?: findSupportedExact("zh-HK") + ?: findSupportedExact("zh-TW") + ?: findSupportedPrefix("zh") + ?: currentLanguage + } + + private fun pickPreferredEnglishLanguage(): String { + return findSupportedExact("en-US") + ?: findSupportedPrefix("en") + ?: currentLanguage + } + + // 选择检测到的语言 + // 如果开启了自动检测语言,根据文本内容判断是否为中文 + // 如果是中文,选择中文语言;否则选择英文语言 + // 如果未开启自动检测语言,返回当前语言 + private fun selectDetectedLanguage(text: String): String { + if (!isAutoDetectLanguage) return currentLanguage + + val hasChinese = text.contains(Regex("[\\u4e00-\\u9fa5]")) + return if (hasChinese) pickPreferredChineseLanguage() else pickPreferredEnglishLanguage() + } + private fun getWebSocketUrl(): String { val baseWsUrl = "wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1" diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index e12d4ed03..fc8f112ca 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -490,7 +490,10 @@ public class AzureAsrHelper: NSObject { // 启动讯飞识别 xunFeiAsrHelper?.start( - callback: callback, language: currentLanguage, isAutoDetect: isAutoDetectLanguage, + callback: callback, + language: currentLanguage, + supportedLanguages: supportedLanguages, + isAutoDetect: isAutoDetectLanguage, isRemoveFirstPunctuation: isRemoveFirstPunctuation) // 创建代理回调,将音频数据转发给讯飞 diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/XunFeiAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/XunFeiAsrHelper.swift index bc63bcaee..f17d356ad 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/XunFeiAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/XunFeiAsrHelper.swift @@ -23,6 +23,7 @@ class XunFeiAsrHelper: NSObject, URLSessionWebSocketDelegate { private var lastIntermediateResult = "" private var currentLanguage = "zh-CN" + private var supportedLanguages: [String] = [] private var isAutoDetectLanguage = false // 是否第去除一个文字的标点符号 @@ -47,11 +48,12 @@ class XunFeiAsrHelper: NSObject, URLSessionWebSocketDelegate { return !appId.isEmpty && !accessKeyId.isEmpty && !accessKeySecret.isEmpty } - func start(callback: AzureAsrHelper.ContinuousRecognizeCallback, language: String, isAutoDetect: Bool, isRemoveFirstPunctuation: Bool) { + func start(callback: AzureAsrHelper.ContinuousRecognizeCallback, language: String, supportedLanguages: [String], isAutoDetect: Bool, isRemoveFirstPunctuation: Bool) { stop() self.callback = callback self.currentLanguage = language + self.supportedLanguages = supportedLanguages self.isAutoDetectLanguage = isAutoDetect self.removeFirstPunctuation = isRemoveFirstPunctuation self.currentSessionId = UUID().uuidString @@ -268,16 +270,43 @@ class XunFeiAsrHelper: NSObject, URLSessionWebSocketDelegate { } } + private func normalizeLanguageCode(_ code: String) -> String { + let trimmed = code.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { return trimmed } + let parts = trimmed.split(separator: "-") + let lang = parts.first?.lowercased() ?? trimmed.lowercased() + let region = parts.count > 1 ? parts[1].uppercased() : "" + return region.isEmpty ? lang : "\(lang)-\(region)" + } + + private func findSupportedExact(_ target: String) -> String? { + let targetNorm = normalizeLanguageCode(target) + return supportedLanguages.first { normalizeLanguageCode($0) == targetNorm } + } + + private func findSupportedPrefix(_ prefix: String) -> String? { + let p = prefix.lowercased() + return supportedLanguages.first { normalizeLanguageCode($0).hasPrefix(p) } + } + + private func pickPreferredChineseLanguage() -> String { + return findSupportedExact("zh-CN") + ?? findSupportedExact("zh-HK") + ?? findSupportedExact("zh-TW") + ?? findSupportedPrefix("zh") + ?? currentLanguage + } + + private func pickPreferredEnglishLanguage() -> String { + return findSupportedExact("en-US") + ?? findSupportedPrefix("en") + ?? currentLanguage + } + private func detectLanguage(from text: String) -> String { - if isAutoDetectLanguage { - if text.range(of: "[\\u4e00-\\u9fa5]", options: .regularExpression) != nil { - return "zh-CN" - } else { - return "en-US" - } - } else { - return currentLanguage - } + guard isAutoDetectLanguage else { return currentLanguage } + let hasChinese = text.range(of: "[\\u4e00-\\u9fa5]", options: .regularExpression) != nil + return hasChinese ? pickPreferredChineseLanguage() : pickPreferredEnglishLanguage() } private func getWebSocketUrl() -> String { From 29425b83671e41fc0c33d76089f69b2a1fd17b8e Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 3 Feb 2026 10:41:46 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(azure=5Fspeech):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A4=9A=E4=B8=AD=E6=96=87=E5=8F=98=E4=BD=93=E6=97=B6=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=90=AF=E7=94=A8=E8=AE=AF=E9=A3=9EASR=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当配置的语言列表包含两个或以上的中文变体(如 zh-CN 和 zh-TW)时,即使所有语言都在讯飞支持范围内,也应禁用讯飞ASR,因为讯飞无法同时处理多个中文方言。此修改在决定是否启用讯飞ASR的条件中增加了对多中文变体的检查。 --- .../yunqiinnovation/azure_speech/AzureAsrHelper.kt | 11 ++++++++++- .../Sources/azure_speech/AzureAsrHelper.swift | 14 ++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 1f6ac4e1e..d10835762 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -135,7 +135,16 @@ class AzureAsrHelper(private val context: Context) { val xunfeiConfigReady = this.xunfeiAppId.isNotBlank() && this.xunfeiAccessKeyId.isNotBlank() && this.xunfeiAccessKeySecret.isNotBlank() - this.useXunfei = supportedLanguages.all { it == "zh-CN" || it == "en-US" || it == "zh-HK"|| it == "zh-TW" } && xunfeiConfigReady + val onlySupportedByXunfei = supportedLanguages.all { + it == "zh-CN" || it == "en-US" || it == "zh-HK" || it == "zh-TW" + } + val chineseVariantsCount = supportedLanguages + .map { it.trim().lowercase() } + .filter { it == "zh" || it.startsWith("zh-") } + .distinct() + .size + val hasMultipleChineseVariants = chineseVariantsCount >= 2 + this.useXunfei = onlySupportedByXunfei && xunfeiConfigReady && !hasMultipleChineseVariants // 根据支持的语言数量决定是否启用自动语言检测 this.isAutoDetectLanguage = supportedLanguages.size >= 2 diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index fc8f112ca..5cc8fda70 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -131,10 +131,17 @@ public class AzureAsrHelper: NSObject { let xunfeiConfigReady = !self.xunfeiAppId.isEmpty && !self.xunfeiAccessKeyId.isEmpty && !self.xunfeiAccessKeySecret.isEmpty + let onlySupportedByXunfei = supportedLanguages.allSatisfy { + $0 == "zh-CN" || $0 == "en-US" || $0 == "zh-HK" || $0 == "zh-TW" + } + let chineseVariants = Set( + supportedLanguages + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } + .filter { $0 == "zh" || $0.hasPrefix("zh-") } + ) + let hasMultipleChineseVariants = chineseVariants.count >= 2 self.useXunfei = - supportedLanguages.allSatisfy { - $0 == "zh-CN" || $0 == "en-US" || $0 == "zh-HK" || $0 == "zh-TW" - } && xunfeiConfigReady + onlySupportedByXunfei && xunfeiConfigReady && !hasMultipleChineseVariants os_log( "ASR helper initialize: supportedLanguages=%{public}@, audioSourceType=%{public}@, xunfeiConfigured=%{public}@, provider=%{public}@", @@ -1246,4 +1253,3 @@ private class AudioDataCallbackProxy: SimpleAudioReceiver.AudioDataCallback { xunfeiCallback?(data) } } -