|
|
@ -1,18 +1,17 @@ |
|
|
import Foundation |
|
|
|
|
|
import AVFoundation |
|
|
import AVFoundation |
|
|
|
|
|
import Foundation |
|
|
import MicrosoftCognitiveServicesSpeech |
|
|
import MicrosoftCognitiveServicesSpeech |
|
|
import speech |
|
|
|
|
|
import os.log |
|
|
import os.log |
|
|
|
|
|
import speech |
|
|
|
|
|
|
|
|
// 移除 Network 导入,因为我们将使用独立的 NetworkStateMonitor |
|
|
// 移除 Network 导入,因为我们将使用独立的 NetworkStateMonitor |
|
|
// import Network |
|
|
// import Network |
|
|
|
|
|
|
|
|
// MARK: - 基于微软Azure语音服务的ASR实现 |
|
|
// MARK: - 基于微软Azure语音服务的ASR实现 |
|
|
/** |
|
|
/// Azure ASR Helper |
|
|
* Azure ASR Helper |
|
|
/// |
|
|
* |
|
|
/// 基于微软Azure语音服务的ASR实现 |
|
|
* 基于微软Azure语音服务的ASR实现 |
|
|
/// 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech |
|
|
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech |
|
|
|
|
|
*/ |
|
|
|
|
|
public class AzureAsrHelper: NSObject { |
|
|
public class AzureAsrHelper: NSObject { |
|
|
private let tag = "AzureAsrHelper" |
|
|
private let tag = "AzureAsrHelper" |
|
|
// 日志对象 |
|
|
// 日志对象 |
|
|
@ -49,7 +48,6 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
private var currsessionId = "" |
|
|
private var currsessionId = "" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 配置参数 |
|
|
// 配置参数 |
|
|
private var currentLanguage = "zh-CN" |
|
|
private var currentLanguage = "zh-CN" |
|
|
private var supportedLanguages = ["zh-CN"] |
|
|
private var supportedLanguages = ["zh-CN"] |
|
|
@ -81,10 +79,6 @@ public class AzureAsrHelper: NSObject { |
|
|
// 音频处理 |
|
|
// 音频处理 |
|
|
public var audioStream: SimpleAudioReceiver? |
|
|
public var audioStream: SimpleAudioReceiver? |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public func asrProvider() -> String { |
|
|
public func asrProvider() -> String { |
|
|
return useXunfei ? "xunfei" : "azure" |
|
|
return useXunfei ? "xunfei" : "azure" |
|
|
} |
|
|
} |
|
|
@ -134,14 +128,42 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 检查是否只包含中文或英文,如果是则使用讯飞 |
|
|
// 检查是否只包含中文或英文,如果是则使用讯飞 |
|
|
let xunfeiConfigReady = !self.xunfeiAppId.isEmpty && |
|
|
let xunfeiConfigReady = |
|
|
!self.xunfeiAccessKeyId.isEmpty && |
|
|
!self.xunfeiAppId.isEmpty && !self.xunfeiAccessKeyId.isEmpty |
|
|
!self.xunfeiAccessKeySecret.isEmpty |
|
|
&& !self.xunfeiAccessKeySecret.isEmpty |
|
|
self.useXunfei = supportedLanguages.allSatisfy { $0 == "zh-CN" || $0 == "en-US" || $0 == "zh-HK" || $0 == "en-TW" } && xunfeiConfigReady |
|
|
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 = |
|
|
|
|
|
onlySupportedByXunfei && xunfeiConfigReady && !hasMultipleChineseVariants |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
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 { |
|
|
if !isAutoDetectLanguage && !supportedLanguages.isEmpty { |
|
|
self.currentLanguage = supportedLanguages[0] |
|
|
self.currentLanguage = supportedLanguages[0] |
|
|
@ -152,9 +174,12 @@ public class AzureAsrHelper: NSObject { |
|
|
// 针对翻译场景的性能优化 |
|
|
// 针对翻译场景的性能优化 |
|
|
// optimizeForTranslation() |
|
|
// optimizeForTranslation() |
|
|
if isAutoDetectLanguage { |
|
|
if isAutoDetectLanguage { |
|
|
speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) |
|
|
speechConfig?.setPropertyTo( |
|
|
|
|
|
"Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) |
|
|
// 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 |
|
|
// 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 |
|
|
os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) |
|
|
os_log( |
|
|
|
|
|
"启用语言检测模式,支持语言: %{public}@", log: log, type: .info, |
|
|
|
|
|
supportedLanguages.joined(separator: ",")) |
|
|
} else { |
|
|
} else { |
|
|
speechConfig?.speechRecognitionLanguage = currentLanguage |
|
|
speechConfig?.speechRecognitionLanguage = currentLanguage |
|
|
os_log("固定语言模式,当前语言: %{public}@", log: log, type: .info, currentLanguage) |
|
|
os_log("固定语言模式,当前语言: %{public}@", log: log, type: .info, currentLanguage) |
|
|
@ -188,7 +213,6 @@ public class AzureAsrHelper: NSObject { |
|
|
private func handleNetworkAvailable() { |
|
|
private func handleNetworkAvailable() { |
|
|
os_log("网络已恢复", log: log, type: .info) |
|
|
os_log("网络已恢复", log: log, type: .info) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !self.isNetworkRecovering { |
|
|
if !self.isNetworkRecovering { |
|
|
self.isNetworkRecovering = true |
|
|
self.isNetworkRecovering = true |
|
|
os_log("网络恢复,准备重新启动识别", log: self.log, type: .info) |
|
|
os_log("网络恢复,准备重新启动识别", log: self.log, type: .info) |
|
|
@ -196,7 +220,6 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 处理网络连接丢失(带防抖机制) |
|
|
* 处理网络连接丢失(带防抖机制) |
|
|
*/ |
|
|
*/ |
|
|
@ -205,7 +228,9 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
// 防抖机制:如果距离上次触发时间小于防抖间隔,则忽略本次触发 |
|
|
// 防抖机制:如果距离上次触发时间小于防抖间隔,则忽略本次触发 |
|
|
if currentTime - lastNetworkLostTime < networkLostDebounceInterval { |
|
|
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 |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -225,7 +250,7 @@ public class AzureAsrHelper: NSObject { |
|
|
self.lastNetworkLostTime = 0 |
|
|
self.lastNetworkLostTime = 0 |
|
|
self.isNetworkRecovering = true |
|
|
self.isNetworkRecovering = true |
|
|
} else { |
|
|
} else { |
|
|
self.continuousCallback?.onError(sessionid: currsessionId,1000, "网络连接不可用,请检查网络设置") |
|
|
self.continuousCallback?.onError(sessionid: currsessionId, 1000, "网络连接不可用,请检查网络设置") |
|
|
os_log("网络仍未恢复,继续检测", log: self.log, type: .debug) |
|
|
os_log("网络仍未恢复,继续检测", log: self.log, type: .debug) |
|
|
self.networkLostCount = 0 |
|
|
self.networkLostCount = 0 |
|
|
self.stopContinuousRecognition() |
|
|
self.stopContinuousRecognition() |
|
|
@ -233,7 +258,6 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 预初始化音频组件 |
|
|
* 预初始化音频组件 |
|
|
* 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟 |
|
|
* 在初始化阶段就准备好音频流和相关组件,减少首次启动延迟 |
|
|
@ -251,7 +275,6 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 异步启动音频录制(带防抖与状态二次校验) |
|
|
* 异步启动音频录制(带防抖与状态二次校验) |
|
|
* |
|
|
* |
|
|
@ -273,13 +296,15 @@ public class AzureAsrHelper: NSObject { |
|
|
audioSourceType: AudioSourceType, |
|
|
audioSourceType: AudioSourceType, |
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, |
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, |
|
|
completion: @escaping (Bool) -> Void |
|
|
completion: @escaping (Bool) -> Void |
|
|
) { |
|
|
) { |
|
|
let currentTime = Int64(Date().timeIntervalSince1970 * 1000) |
|
|
let currentTime = Int64(Date().timeIntervalSince1970 * 1000) |
|
|
|
|
|
|
|
|
// 防抖:距离上次启动过短,取消旧任务 |
|
|
// 防抖:距离上次启动过短,取消旧任务 |
|
|
if currentTime - lastAudioStartTime < audioOperationDebounceInterval { |
|
|
if currentTime - lastAudioStartTime < audioOperationDebounceInterval { |
|
|
audioStartWorkItem?.cancel() |
|
|
audioStartWorkItem?.cancel() |
|
|
os_log("音频启动操作被防抖过滤,距离上次启动仅%{public}dms", log: log, type: .debug, currentTime - lastAudioStartTime) |
|
|
os_log( |
|
|
|
|
|
"音频启动操作被防抖过滤,距离上次启动仅%{public}dms", log: log, type: .debug, |
|
|
|
|
|
currentTime - lastAudioStartTime) |
|
|
} else { |
|
|
} else { |
|
|
// 即使未触发防抖,也保证旧任务不会残留 |
|
|
// 即使未触发防抖,也保证旧任务不会残留 |
|
|
audioStartWorkItem?.cancel() |
|
|
audioStartWorkItem?.cancel() |
|
|
@ -312,7 +337,9 @@ public class AzureAsrHelper: NSObject { |
|
|
// 准备启动 |
|
|
// 准备启动 |
|
|
self.isAudioStarting = true |
|
|
self.isAudioStarting = true |
|
|
self.pendingStopRequest = false // 启动前清空旧的停止请求 |
|
|
self.pendingStopRequest = false // 启动前清空旧的停止请求 |
|
|
let success = self.performAudioStart(mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) |
|
|
let success = self.performAudioStart( |
|
|
|
|
|
mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback |
|
|
|
|
|
) |
|
|
self.isAudioStarting = false |
|
|
self.isAudioStarting = false |
|
|
self.isAudioStarted = success |
|
|
self.isAudioStarted = success |
|
|
|
|
|
|
|
|
@ -339,7 +366,7 @@ public class AzureAsrHelper: NSObject { |
|
|
deadline: .now() + .milliseconds(Int(audioOperationDebounceInterval)), |
|
|
deadline: .now() + .milliseconds(Int(audioOperationDebounceInterval)), |
|
|
execute: workItem |
|
|
execute: workItem |
|
|
) |
|
|
) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 执行音频启动操作 |
|
|
* 执行音频启动操作 |
|
|
@ -350,7 +377,7 @@ public class AzureAsrHelper: NSObject { |
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? |
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? |
|
|
) -> Bool { |
|
|
) -> Bool { |
|
|
do { |
|
|
do { |
|
|
if(!audioStream!.isContinuousRecognitionActive){ |
|
|
if !audioStream!.isContinuousRecognitionActive { |
|
|
audioStream?.startAudioRecord( |
|
|
audioStream?.startAudioRecord( |
|
|
mod: mod, |
|
|
mod: mod, |
|
|
audioSourceType: audioSourceType == .microphone ? .microphone : .external, |
|
|
audioSourceType: audioSourceType == .microphone ? .microphone : .external, |
|
|
@ -423,7 +450,7 @@ public class AzureAsrHelper: NSObject { |
|
|
audioSourceType: AudioSourceType = .microphone, |
|
|
audioSourceType: AudioSourceType = .microphone, |
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, |
|
|
audioDataCallback: SimpleAudioReceiver.AudioDataCallback? = nil, |
|
|
isRemoveFirstPunctuation: Bool = true |
|
|
isRemoveFirstPunctuation: Bool = true |
|
|
) -> Bool { |
|
|
) -> Bool { |
|
|
guard speechConfig != nil else { |
|
|
guard speechConfig != nil else { |
|
|
print("语音服务未初始化") |
|
|
print("语音服务未初始化") |
|
|
return false |
|
|
return false |
|
|
@ -469,15 +496,23 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 启动讯飞识别 |
|
|
// 启动讯飞识别 |
|
|
xunFeiAsrHelper?.start(callback: callback, language: currentLanguage, isAutoDetect: isAutoDetectLanguage, isRemoveFirstPunctuation: isRemoveFirstPunctuation) |
|
|
xunFeiAsrHelper?.start( |
|
|
|
|
|
callback: callback, |
|
|
|
|
|
language: currentLanguage, |
|
|
|
|
|
supportedLanguages: supportedLanguages, |
|
|
|
|
|
isAutoDetect: isAutoDetectLanguage, |
|
|
|
|
|
isRemoveFirstPunctuation: isRemoveFirstPunctuation) |
|
|
|
|
|
|
|
|
// 创建代理回调,将音频数据转发给讯飞 |
|
|
// 创建代理回调,将音频数据转发给讯飞 |
|
|
let proxyCallback = AudioDataCallbackProxy(original: audioDataCallback) { [weak self] data in |
|
|
let proxyCallback = AudioDataCallbackProxy(original: audioDataCallback) { |
|
|
|
|
|
[weak self] data in |
|
|
self?.xunFeiAsrHelper?.sendAudio(frameBuffer: data) |
|
|
self?.xunFeiAsrHelper?.sendAudio(frameBuffer: data) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 异步启动音频处理 |
|
|
// 异步启动音频处理 |
|
|
startAudioRecordAsync(mod: mod, audioSourceType: audioSourceType, audioDataCallback: proxyCallback) { [weak self] (success: Bool) in |
|
|
startAudioRecordAsync( |
|
|
|
|
|
mod: mod, audioSourceType: audioSourceType, audioDataCallback: proxyCallback |
|
|
|
|
|
) { [weak self] (success: Bool) in |
|
|
guard let self = self else { return } |
|
|
guard let self = self else { return } |
|
|
|
|
|
|
|
|
if success { |
|
|
if success { |
|
|
@ -506,20 +541,28 @@ public class AzureAsrHelper: NSObject { |
|
|
// self.audioStream?.isContinuousRecognitionActive = true |
|
|
// self.audioStream?.isContinuousRecognitionActive = true |
|
|
|
|
|
|
|
|
// 异步启动音频处理 |
|
|
// 异步启动音频处理 |
|
|
startAudioRecordAsync(mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in |
|
|
startAudioRecordAsync( |
|
|
|
|
|
mod: mod, audioSourceType: audioSourceType, audioDataCallback: audioDataCallback |
|
|
|
|
|
) { [weak self] (success: Bool) in |
|
|
guard let self = self else { return } |
|
|
guard let self = self else { return } |
|
|
|
|
|
|
|
|
if success { |
|
|
if success { |
|
|
// 只有在音频启动成功后才设置状态为 true |
|
|
// 只有在音频启动成功后才设置状态为 true |
|
|
self.audioStream?.isContinuousRecognitionActive = true |
|
|
self.audioStream?.isContinuousRecognitionActive = true |
|
|
print("startContinuousRecognition:\(self.audioStream?.isContinuousRecognitionActive)") |
|
|
print( |
|
|
|
|
|
"startContinuousRecognition:\(self.audioStream?.isContinuousRecognitionActive)" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
// 启动语音识别 |
|
|
// 启动语音识别 |
|
|
do { |
|
|
do { |
|
|
try self.recognizer?.startContinuousRecognition() |
|
|
try self.recognizer?.startContinuousRecognition() |
|
|
os_log("连续识别已启动,音频源: %{public}@", log: self.log, type: .info, audioSourceType == .microphone ? "麦克风" : "外部") |
|
|
os_log( |
|
|
|
|
|
"连续识别已启动,音频源: %{public}@", log: self.log, type: .info, |
|
|
|
|
|
audioSourceType == .microphone ? "麦克风" : "外部") |
|
|
} catch { |
|
|
} catch { |
|
|
os_log("启动连续识别失败: %{public}@", log: self.log, type: .error, error.localizedDescription) |
|
|
os_log( |
|
|
|
|
|
"启动连续识别失败: %{public}@", log: self.log, type: .error, |
|
|
|
|
|
error.localizedDescription) |
|
|
self.stopContinuousRecognition() |
|
|
self.stopContinuousRecognition() |
|
|
self.audioStream?.isContinuousRecognitionActive = false |
|
|
self.audioStream?.isContinuousRecognitionActive = false |
|
|
} |
|
|
} |
|
|
@ -535,9 +578,7 @@ public class AzureAsrHelper: NSObject { |
|
|
audioStream?.isContinuousRecognitionActive = false |
|
|
audioStream?.isContinuousRecognitionActive = false |
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 停止连续语音识别(优化:确保取消未执行的启动任务) |
|
|
* 停止连续语音识别(优化:确保取消未执行的启动任务) |
|
|
@ -566,7 +607,6 @@ public class AzureAsrHelper: NSObject { |
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
do { |
|
|
do { |
|
|
|
|
|
|
|
|
// 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动 |
|
|
// 关键:取消尚未执行的启动任务,防止 stop 后音频又被启动 |
|
|
@ -698,7 +738,6 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 设置识别器 |
|
|
* 设置识别器 |
|
|
*/ |
|
|
*/ |
|
|
@ -734,7 +773,10 @@ public class AzureAsrHelper: NSObject { |
|
|
// 创建识别器 |
|
|
// 创建识别器 |
|
|
if isAutoDetectLanguage { |
|
|
if isAutoDetectLanguage { |
|
|
// 使用正确的初始化方法 |
|
|
// 使用正确的初始化方法 |
|
|
guard let autoDetectSourceLanguageConfig = try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) else { |
|
|
guard |
|
|
|
|
|
let autoDetectSourceLanguageConfig = |
|
|
|
|
|
try? SPXAutoDetectSourceLanguageConfiguration(supportedLanguages) |
|
|
|
|
|
else { |
|
|
print("创建自动语言检测配置失败,使用默认语言配置") |
|
|
print("创建自动语言检测配置失败,使用默认语言配置") |
|
|
// 如果创建失败,使用默认的单语言配置 |
|
|
// 如果创建失败,使用默认的单语言配置 |
|
|
recognizer = try SPXSpeechRecognizer( |
|
|
recognizer = try SPXSpeechRecognizer( |
|
|
@ -764,7 +806,6 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 设置麦克风流 - 使用推流方式 |
|
|
* 设置麦克风流 - 使用推流方式 |
|
|
* 优化:避免重复初始化已存在的音频流 |
|
|
* 优化:避免重复初始化已存在的音频流 |
|
|
@ -787,8 +828,6 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 从语音识别结果中获取检测到的语言 |
|
|
* 从语音识别结果中获取检测到的语言 |
|
|
* @param result 语音识别结果 |
|
|
* @param result 语音识别结果 |
|
|
@ -803,8 +842,10 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
// 备用方案:从属性中获取语言 |
|
|
// 备用方案:从属性中获取语言 |
|
|
if let properties = result.properties, |
|
|
if let properties = result.properties, |
|
|
let language = properties.getPropertyByName("speechServiceResponse_RecognitionLanguage"), |
|
|
let language = properties.getPropertyByName( |
|
|
!language.isEmpty { |
|
|
"speechServiceResponse_RecognitionLanguage"), |
|
|
|
|
|
!language.isEmpty |
|
|
|
|
|
{ |
|
|
print("从属性中检测到语言: \(language)") |
|
|
print("从属性中检测到语言: \(language)") |
|
|
return language |
|
|
return language |
|
|
} |
|
|
} |
|
|
@ -823,8 +864,6 @@ public class AzureAsrHelper: NSObject { |
|
|
return currentLanguage |
|
|
return currentLanguage |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 动态切换语言(翻译场景中的语言切换) |
|
|
* 动态切换语言(翻译场景中的语言切换) |
|
|
* |
|
|
* |
|
|
@ -857,7 +896,9 @@ public class AzureAsrHelper: NSObject { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
os_log("语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, validatedTargetLang) |
|
|
os_log( |
|
|
|
|
|
"语言已切换: %{public}@ -> %{public}@", log: log, type: .info, validatedSourceLang, |
|
|
|
|
|
validatedTargetLang) |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
/** |
|
|
/** |
|
|
@ -870,7 +911,7 @@ public class AzureAsrHelper: NSObject { |
|
|
self.continuousCallback = callback |
|
|
self.continuousCallback = callback |
|
|
|
|
|
|
|
|
// 重设识别器 |
|
|
// 重设识别器 |
|
|
if (!setupRecognizer()) { |
|
|
if !setupRecognizer() { |
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
guard let recognizer = recognizer else { return false } |
|
|
guard let recognizer = recognizer else { return false } |
|
|
@ -888,7 +929,8 @@ public class AzureAsrHelper: NSObject { |
|
|
print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") |
|
|
print("正在识别事件=检测到语言: \(detectedLanguage), 识别中: \(text)") |
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
DispatchQueue.main.async { |
|
|
callback.onRecognizing(sessionid: self.currsessionId,result.text ?? "", detectedLanguage) |
|
|
callback.onRecognizing( |
|
|
|
|
|
sessionid: self.currsessionId, result.text ?? "", detectedLanguage) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -897,7 +939,8 @@ public class AzureAsrHelper: NSObject { |
|
|
guard let self = self else { return } |
|
|
guard let self = self else { return } |
|
|
let result = event.result |
|
|
let result = event.result |
|
|
guard result.reason == .recognizedSpeech, |
|
|
guard result.reason == .recognizedSpeech, |
|
|
!(result.text?.isEmpty ?? true) else { return } |
|
|
!(result.text?.isEmpty ?? true) |
|
|
|
|
|
else { return } |
|
|
|
|
|
|
|
|
// 使用现有的 getDetectedLanguage 方法替代 result.language |
|
|
// 使用现有的 getDetectedLanguage 方法替代 result.language |
|
|
let detectedLanguage = self.getDetectedLanguage(from: result) |
|
|
let detectedLanguage = self.getDetectedLanguage(from: result) |
|
|
@ -907,7 +950,8 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
// 在主线程回调结果 |
|
|
// 在主线程回调结果 |
|
|
DispatchQueue.main.async { |
|
|
DispatchQueue.main.async { |
|
|
callback.onResult(sessionid: self.currsessionId,result.text ?? "", detectedLanguage) |
|
|
callback.onResult( |
|
|
|
|
|
sessionid: self.currsessionId, result.text ?? "", detectedLanguage) |
|
|
self.currsessionId = UUID().uuidString |
|
|
self.currsessionId = UUID().uuidString |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -939,14 +983,12 @@ public class AzureAsrHelper: NSObject { |
|
|
|
|
|
|
|
|
os_log("识别取消: %{public}@", log: self.log, type: .error, errorDetails) |
|
|
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 |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 禁用耳机进行拾音 |
|
|
* 禁用耳机进行拾音 |
|
|
*/ |
|
|
*/ |
|
|
@ -980,10 +1022,13 @@ public class AzureAsrHelper: NSObject { |
|
|
* @param filePath 录音文件路径 |
|
|
* @param filePath 录音文件路径 |
|
|
* @param audioDataCallback 音频数据回调接口 |
|
|
* @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) |
|
|
os_log("开启录音: %{public}@", log: log, type: .info, filePath) |
|
|
self.audioSourceType = audioSourceType |
|
|
self.audioSourceType = audioSourceType |
|
|
if(audioSourceType == .external){ |
|
|
if audioSourceType == .external { |
|
|
os_log("外部音频源不在这里录音", log: log, type: .info) |
|
|
os_log("外部音频源不在这里录音", log: log, type: .info) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@ -997,12 +1042,14 @@ public class AzureAsrHelper: NSObject { |
|
|
audioStream?.recordfile = RecordFile() |
|
|
audioStream?.recordfile = RecordFile() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 修复:移除多余的 audioDataCallback 参数 |
|
|
// 修复:移除多余的 audioDataCallback 参数 |
|
|
// 修复:添加类型转换 |
|
|
// 修复:添加类型转换 |
|
|
if(!audioStream!.isContinuousRecognitionActive){ |
|
|
if !audioStream!.isContinuousRecognitionActive { |
|
|
// 异步启动音频处理 |
|
|
// 异步启动音频处理 |
|
|
startAudioRecordAsync(mod: "normal", audioSourceType: audioSourceType, audioDataCallback: audioDataCallback) { [weak self] (success: Bool) in |
|
|
startAudioRecordAsync( |
|
|
|
|
|
mod: "normal", audioSourceType: audioSourceType, |
|
|
|
|
|
audioDataCallback: audioDataCallback |
|
|
|
|
|
) { [weak self] (success: Bool) in |
|
|
guard let self = self else { return } |
|
|
guard let self = self else { return } |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
@ -1015,9 +1062,9 @@ public class AzureAsrHelper: NSObject { |
|
|
/** |
|
|
/** |
|
|
* 移动文件到新路径 |
|
|
* 移动文件到新路径 |
|
|
*/ |
|
|
*/ |
|
|
public func moveFile(sourcePath: String, destPath: String)-> Bool { |
|
|
public func moveFile(sourcePath: String, destPath: String) -> Bool { |
|
|
|
|
|
|
|
|
if(audioSourceType == .external){ |
|
|
if audioSourceType == .external { |
|
|
os_log(" 外部音频源不在这里移动文件", log: log, type: .info) |
|
|
os_log(" 外部音频源不在这里移动文件", log: log, type: .info) |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
@ -1025,12 +1072,11 @@ public class AzureAsrHelper: NSObject { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 重命名指定路径的音频文件 |
|
|
* 重命名指定路径的音频文件 |
|
|
*/ |
|
|
*/ |
|
|
public func renameFile(filePath: String, newName: String)-> Bool { |
|
|
public func renameFile(filePath: String, newName: String) -> Bool { |
|
|
if(audioSourceType == .external){ |
|
|
if audioSourceType == .external { |
|
|
os_log(" 外部音频源不在这里重命名文件", log: log, type: .info) |
|
|
os_log(" 外部音频源不在这里重命名文件", log: log, type: .info) |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
@ -1038,13 +1084,11 @@ public class AzureAsrHelper: NSObject { |
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 暂停录音 |
|
|
* 暂停录音 |
|
|
*/ |
|
|
*/ |
|
|
public func pauseRecord() { |
|
|
public func pauseRecord() { |
|
|
if(audioSourceType == .external){ |
|
|
if audioSourceType == .external { |
|
|
os_log(" 外部音频源不在这里暂停录音", log: log, type: .info) |
|
|
os_log(" 外部音频源不在这里暂停录音", log: log, type: .info) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@ -1059,7 +1103,7 @@ public class AzureAsrHelper: NSObject { |
|
|
* 继续录音 |
|
|
* 继续录音 |
|
|
*/ |
|
|
*/ |
|
|
public func resumeRecord() { |
|
|
public func resumeRecord() { |
|
|
if(audioSourceType == .external){ |
|
|
if audioSourceType == .external { |
|
|
os_log(" 外部音频源不在这里继续录音", log: log, type: .info) |
|
|
os_log(" 外部音频源不在这里继续录音", log: log, type: .info) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@ -1074,7 +1118,7 @@ public class AzureAsrHelper: NSObject { |
|
|
* 关闭录音 |
|
|
* 关闭录音 |
|
|
*/ |
|
|
*/ |
|
|
public func stopRecord(isSave: Bool) { |
|
|
public func stopRecord(isSave: Bool) { |
|
|
if(audioSourceType == .external){ |
|
|
if audioSourceType == .external { |
|
|
os_log(" 外部音频源不在这里关闭录音", log: log, type: .info) |
|
|
os_log(" 外部音频源不在这里关闭录音", log: log, type: .info) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@ -1082,17 +1126,13 @@ public class AzureAsrHelper: NSObject { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
audioStream.isRecord = false |
|
|
audioStream.isRecord = false |
|
|
if (!audioStream.isContinuousRecognitionActive) { |
|
|
if !audioStream.isContinuousRecognitionActive { |
|
|
audioStream.stopMicrophoneCapture() |
|
|
audioStream.stopMicrophoneCapture() |
|
|
} |
|
|
} |
|
|
audioStream.recordfile?.isPause = false // |
|
|
audioStream.recordfile?.isPause = false // |
|
|
audioStream.recordfile?.closeFile(isSave: true) // |
|
|
audioStream.recordfile?.closeFile(isSave: true) // |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - 回调函数 |
|
|
// MARK: - 回调函数 |
|
|
/** |
|
|
/** |
|
|
* 连续识别回调接口 |
|
|
* 连续识别回调接口 |
|
|
@ -1104,7 +1144,7 @@ public class AzureAsrHelper: NSObject { |
|
|
* @param text 识别的文本 |
|
|
* @param text 识别的文本 |
|
|
* @param detectedLanguage 检测到的语言 |
|
|
* @param detectedLanguage 检测到的语言 |
|
|
*/ |
|
|
*/ |
|
|
func onResult(sessionid:String,_ text: String, _ detectedLanguage: String) |
|
|
func onResult(sessionid: String, _ text: String, _ detectedLanguage: String) |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 识别进行中调用 |
|
|
* 识别进行中调用 |
|
|
@ -1112,17 +1152,17 @@ public class AzureAsrHelper: NSObject { |
|
|
* @param recognizing 正在识别的文本 |
|
|
* @param recognizing 正在识别的文本 |
|
|
* @param detectedLanguage 检测到的语言 |
|
|
* @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) |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 识别取消时调用 |
|
|
* 识别取消时调用 |
|
|
@ -1130,46 +1170,46 @@ public class AzureAsrHelper: NSObject { |
|
|
* @param reason 取消原因 |
|
|
* @param reason 取消原因 |
|
|
* @param errorDetails 错误详情 |
|
|
* @param errorDetails 错误详情 |
|
|
*/ |
|
|
*/ |
|
|
func onCanceled(sessionid:String ,_ reason: String, _ errorDetails: String) |
|
|
func onCanceled(sessionid: String, _ reason: String, _ errorDetails: String) |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 识别出错时调用 |
|
|
* 识别出错时调用 |
|
|
* |
|
|
* |
|
|
* @param error 错误信息 |
|
|
* @param error 错误信息 |
|
|
*/ |
|
|
*/ |
|
|
func onError(sessionid:String ,_ code: Int,_ error: String) |
|
|
func onError(sessionid: String, _ code: Int, _ error: String) |
|
|
} |
|
|
} |
|
|
/** |
|
|
/** |
|
|
* 检查网络状态 |
|
|
* 检查网络状态 |
|
|
* @return 网络是否可用 |
|
|
* @return 网络是否可用 |
|
|
*/ |
|
|
*/ |
|
|
private func checkNetworkStatus() -> Bool { |
|
|
private func checkNetworkStatus() -> Bool { |
|
|
// 直接使用 NetworkStateMonitor 的 checkNetworkStatus 方法 |
|
|
// 直接使用 NetworkStateMonitor 的 checkNetworkStatus 方法 |
|
|
let isAvailable = networkMonitor.checkNetworkStatus() |
|
|
let isAvailable = networkMonitor.checkNetworkStatus() |
|
|
|
|
|
|
|
|
if !isAvailable { |
|
|
if !isAvailable { |
|
|
os_log("网络不可用,无法启动语音识别", log: log, type: .error) |
|
|
os_log("网络不可用,无法启动语音识别", log: log, type: .error) |
|
|
continuousCallback?.onError(sessionid: self.currsessionId,1000,"网络连接不可用,请检查网络设置") |
|
|
continuousCallback?.onError(sessionid: self.currsessionId, 1000, "网络连接不可用,请检查网络设置") |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return isAvailable |
|
|
return isAvailable |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 判断是否为网络相关错误 |
|
|
* 判断是否为网络相关错误 |
|
|
* @param errorDetails 错误详情 |
|
|
* @param errorDetails 错误详情 |
|
|
* @return 是否为网络错误 |
|
|
* @return 是否为网络错误 |
|
|
*/ |
|
|
*/ |
|
|
private func isNetworkRelatedError(_ errorDetails: String) -> Bool { |
|
|
private func isNetworkRelatedError(_ errorDetails: String) -> Bool { |
|
|
let networkErrorKeywords = [ |
|
|
let networkErrorKeywords = [ |
|
|
"network", "connection", "timeout", "unreachable", |
|
|
"network", "connection", "timeout", "unreachable", |
|
|
"网络", "连接", "超时", "不可达", |
|
|
"网络", "连接", "超时", "不可达", |
|
|
"ConnectionFailure", "NetworkError", "TimeoutError" |
|
|
"ConnectionFailure", "NetworkError", "TimeoutError", |
|
|
] |
|
|
] |
|
|
|
|
|
|
|
|
let lowercaseError = errorDetails.lowercased() |
|
|
let lowercaseError = errorDetails.lowercased() |
|
|
return networkErrorKeywords.contains { lowercaseError.contains($0.lowercased()) } |
|
|
return networkErrorKeywords.contains { lowercaseError.contains($0.lowercased()) } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
// MARK: - NetworkStateListener实现 |
|
|
// MARK: - NetworkStateListener实现 |
|
|
@ -1184,8 +1224,6 @@ extension AzureAsrHelper: NetworkStateMonitor.NetworkStateListener { |
|
|
|
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func onNetworkLost() { |
|
|
func onNetworkLost() { |
|
|
os_log("网络连接丢失", log: log, type: .error) |
|
|
os_log("网络连接丢失", log: log, type: .error) |
|
|
|
|
|
|
|
|
@ -1215,6 +1253,3 @@ private class AudioDataCallbackProxy: SimpleAudioReceiver.AudioDataCallback { |
|
|
xunfeiCallback?(data) |
|
|
xunfeiCallback?(data) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|