You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
16 KiB
487 lines
16 KiB
import Foundation
|
|
import AVFoundation
|
|
import MicrosoftCognitiveServicesSpeech
|
|
// 自定义语音处理组件,提供音频流处理等功能
|
|
import speech
|
|
|
|
/**
|
|
* Azure ASR Helper
|
|
*
|
|
* 基于微软Azure语音服务的ASR实现
|
|
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-recognize-speech
|
|
*/
|
|
class AzureAsrHelper: NSObject, IAsrService {
|
|
private let tag = "AzureAsrHelper"
|
|
|
|
// 核心组件
|
|
private var speechConfig: SPXSpeechConfiguration?
|
|
private var recognizer: SPXSpeechRecognizer?
|
|
private var audioConfig: SPXAudioConfiguration?
|
|
|
|
// 状态管理
|
|
private var isContinuousRecognitionActive = false
|
|
|
|
// 配置参数
|
|
private var currentLanguage = "zh-CN"
|
|
private var supportedLanguages = ["zh-CN"]
|
|
private var isAutoDetectLanguage = false
|
|
|
|
// 音频源配置
|
|
private enum AudioSourceType {
|
|
/** 使用设备麦克风 */
|
|
case microphone
|
|
|
|
/** 使用外部提供的音频数据 */
|
|
case external
|
|
}
|
|
|
|
private var audioSourceType = AudioSourceType.microphone
|
|
|
|
// 音频处理
|
|
private var externalAudioStream: ExternalAudioStream?
|
|
|
|
// 事件监听处理
|
|
private var eventListeners = NSHashTable<AnyObject>.weakObjects()
|
|
|
|
// 音频会话
|
|
private let audioSession = AVAudioSession.sharedInstance()
|
|
|
|
/**
|
|
* 初始化语音识别服务
|
|
*
|
|
* @param subscriptionKey 服务订阅密钥/访问令牌
|
|
* @param region 服务区域/应用ID
|
|
* @param supportedLanguages 支持的语言列表,如["zh-CN", "en-US"]
|
|
* @return 是否初始化成功
|
|
*/
|
|
func initialize(subscriptionKey: String, region: String, supportedLanguages: [String]?) -> Bool {
|
|
do {
|
|
// 检查配置是否为空
|
|
if subscriptionKey.isEmpty || region.isEmpty {
|
|
print("\(tag): Azure 配置信息不完整")
|
|
return false
|
|
}
|
|
|
|
// 释放之前的资源
|
|
dispose()
|
|
|
|
// 设置语言
|
|
if let supportedLanguages = supportedLanguages, !supportedLanguages.isEmpty {
|
|
self.supportedLanguages = supportedLanguages
|
|
}
|
|
|
|
// 根据支持的语言数量决定是否启用自动语言检测
|
|
self.isAutoDetectLanguage = self.supportedLanguages.count >= 2
|
|
|
|
// 如果只有一种语言,设置为当前语言
|
|
if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty {
|
|
self.currentLanguage = self.supportedLanguages[0]
|
|
}
|
|
|
|
// 创建语音配置
|
|
speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region)
|
|
|
|
if isAutoDetectLanguage {
|
|
// 直接启用语言检测模式
|
|
speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode")
|
|
print("AzureAsrHelper: 启用语言检测模式")
|
|
|
|
} else {
|
|
// 设置指定的识别语言
|
|
speechConfig?.speechRecognitionLanguage = currentLanguage
|
|
}
|
|
|
|
// 创建识别器
|
|
return setupRecognizer()
|
|
} catch {
|
|
print("\(tag): 初始化失败: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 单次识别(非流式)
|
|
*
|
|
* @param audioData 要识别的音频数据字节数组
|
|
* @param callback 识别结果回调
|
|
* @return 是否成功开始识别
|
|
*/
|
|
func recognizeOnce(audioData: Data, callback: @escaping (String) -> Void) -> Bool {
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* 启动连续识别
|
|
*
|
|
* @return 是否成功启动
|
|
*/
|
|
func startContinuousRecognition() -> Bool {
|
|
if isContinuousRecognitionActive {
|
|
return true
|
|
}
|
|
|
|
guard let recognizer = recognizer else {
|
|
print("\(tag): 启动连续识别失败: 识别器未初始化")
|
|
return false
|
|
}
|
|
|
|
do {
|
|
// 启动连续识别
|
|
try recognizer.startContinuousRecognition()
|
|
isContinuousRecognitionActive = true
|
|
return true
|
|
} catch {
|
|
print("\(tag): 启动连续识别失败: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 停止连续识别
|
|
*
|
|
* @return 是否成功停止
|
|
*/
|
|
func stopContinuousRecognition() -> Bool {
|
|
guard let recognizer = recognizer, isContinuousRecognitionActive else {
|
|
return true
|
|
}
|
|
|
|
do {
|
|
// 停止连续识别
|
|
try recognizer.stopContinuousRecognition()
|
|
isContinuousRecognitionActive = false
|
|
return true
|
|
} catch {
|
|
print("\(tag): 停止连续识别失败: \(error.localizedDescription)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 推送音频数据
|
|
* 在连续识别模式下推送新的音频数据
|
|
*
|
|
* @param audioData 要识别的音频数据字节数组
|
|
* @return 是否成功处理
|
|
*/
|
|
func pushAudioData(audioData: Data) -> Bool {
|
|
if !isContinuousRecognitionActive {
|
|
return false
|
|
}
|
|
|
|
if audioSourceType != .external {
|
|
notifyEvent(eventType: .error, params: ["error": "当前不是外部音频流模式"])
|
|
return false
|
|
}
|
|
|
|
// 将音频数据推送到外部音频流
|
|
externalAudioStream?.pushAudio(audioData)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 添加ASR事件监听器
|
|
*
|
|
* @param listener 事件监听器
|
|
*/
|
|
func addListener(_ listener: AsrEventListener) {
|
|
eventListeners.add(listener as AnyObject)
|
|
}
|
|
|
|
/**
|
|
* 移除ASR事件监听器
|
|
*
|
|
* @param listener 要移除的事件监听器
|
|
*/
|
|
func removeListener(_ listener: AsrEventListener) {
|
|
eventListeners.remove(listener as AnyObject)
|
|
}
|
|
|
|
/**
|
|
* 释放资源
|
|
* 在不再需要服务时调用,释放底层资源
|
|
*/
|
|
func dispose() {
|
|
// 停止识别
|
|
if isContinuousRecognitionActive {
|
|
_ = stopContinuousRecognition()
|
|
}
|
|
|
|
// 停止音频处理
|
|
stopAudioProcessing()
|
|
|
|
// 释放资源
|
|
recognizer = nil
|
|
speechConfig = nil
|
|
audioConfig = nil
|
|
externalAudioStream = nil
|
|
}
|
|
|
|
/**
|
|
* 设置识别参数
|
|
*
|
|
* @param params 参数键值对
|
|
* @return 是否设置成功
|
|
*/
|
|
func setParameters(_ params: [String: Any]) -> Bool {
|
|
// 未实现自定义参数,后续可以根据需要添加
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 获取连续识别是否活跃
|
|
*/
|
|
func isRecognitionActive() -> Bool {
|
|
return isContinuousRecognitionActive
|
|
}
|
|
|
|
// MARK: - 辅助方法
|
|
|
|
/**
|
|
* 创建识别器
|
|
*
|
|
* @return 是否创建成功
|
|
*/
|
|
private func setupRecognizer() -> Bool {
|
|
guard let speechConfig = speechConfig else {
|
|
print("\(tag): 无法创建识别器:语音配置不存在")
|
|
return false
|
|
}
|
|
|
|
do {
|
|
if audioSourceType == .external {
|
|
// 设置外部音频流
|
|
setupExternalAudioStream()
|
|
}
|
|
|
|
// 根据是否启用自动识别语言创建不同的识别器
|
|
if isAutoDetectLanguage && supportedLanguages.count > 0 {
|
|
// 创建语言自动检测配置
|
|
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
|
|
|
|
if let audioConfig = self.audioConfig {
|
|
// 使用指定的音频源创建识别器
|
|
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig,
|
|
autoDetectSourceLanguageConfiguration: autoDetectConfig,
|
|
audioConfiguration: audioConfig)
|
|
} else {
|
|
// 使用默认音频源创建识别器
|
|
// 创建一个默认的麦克风配置而不是传nil
|
|
let defaultAudioConfig = SPXAudioConfiguration()
|
|
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig,
|
|
autoDetectSourceLanguageConfiguration: autoDetectConfig,
|
|
audioConfiguration: defaultAudioConfig)
|
|
}
|
|
} else {
|
|
// 使用单一语言配置创建识别器
|
|
if let audioConfig = self.audioConfig {
|
|
// 使用指定的音频源创建识别器
|
|
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig,
|
|
audioConfiguration: audioConfig)
|
|
} else {
|
|
// 使用默认音频源创建识别器
|
|
// 创建一个默认的麦克风配置
|
|
let defaultAudioConfig = SPXAudioConfiguration()
|
|
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig,
|
|
audioConfiguration: defaultAudioConfig)
|
|
}
|
|
}
|
|
|
|
// 设置事件监听器
|
|
setupEventListeners()
|
|
|
|
return true
|
|
} catch {
|
|
print("\(tag): 创建识别器失败: \(error.localizedDescription)")
|
|
stopAudioProcessing()
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 停止音频处理
|
|
*/
|
|
private func stopAudioProcessing() {
|
|
// 清理外部音频流
|
|
externalAudioStream = nil
|
|
}
|
|
|
|
/**
|
|
* 设置外部音频流
|
|
*/
|
|
private func setupExternalAudioStream() {
|
|
externalAudioStream = ExternalAudioStream()
|
|
// 使用ExternalAudioStream实例中创建的pullStream
|
|
audioConfig = SPXAudioConfiguration(streamInput: externalAudioStream!.pullStream)
|
|
}
|
|
|
|
/**
|
|
* 设置事件监听器
|
|
*/
|
|
private func setupEventListeners() {
|
|
guard let recognizer = recognizer else { return }
|
|
|
|
// 修改为直接设置事件回调
|
|
recognizer.addRecognizingEventHandler { [weak self] (sender, event) in
|
|
guard let self = self else { return }
|
|
|
|
var detectedLanguage = ""
|
|
// 修改event.result的处理,不使用可选链
|
|
let result = event.result
|
|
if self.isAutoDetectLanguage {
|
|
// 处理自动语言检测结果
|
|
if let properties = result.properties,
|
|
let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") {
|
|
detectedLanguage = language
|
|
}
|
|
}
|
|
|
|
self.notifyEvent(eventType: .recognizing, params: [
|
|
"text": result.text,
|
|
"language": detectedLanguage
|
|
])
|
|
}
|
|
|
|
// 添加最终识别结果处理器
|
|
recognizer.addRecognizedEventHandler { [weak self] (sender, event) in
|
|
guard let self = self else { return }
|
|
|
|
// 修改event.result的处理,不使用可选绑定
|
|
let result = event.result
|
|
var detectedLanguage = ""
|
|
|
|
if self.isAutoDetectLanguage {
|
|
// 处理自动语言检测结果
|
|
if let properties = result.properties,
|
|
let language = properties.getPropertyByName("SpeechServiceResponse_RecognitionLanguage") {
|
|
detectedLanguage = language
|
|
}
|
|
|
|
// 尝试另一种方式获取语言
|
|
do {
|
|
let langResult = try SPXAutoDetectSourceLanguageResult(result)
|
|
if let lang = langResult.language, !lang.isEmpty {
|
|
detectedLanguage = lang
|
|
}
|
|
} catch {
|
|
// 忽略错误,继续使用之前获取的语言
|
|
}
|
|
}
|
|
|
|
self.notifyEvent(eventType: .recognized, params: [
|
|
"text": result.text,
|
|
"language": detectedLanguage
|
|
])
|
|
}
|
|
|
|
// 添加会话开始事件处理器
|
|
recognizer.addSessionStartedEventHandler { [weak self] (sender, event) in
|
|
guard let self = self else { return }
|
|
self.notifyEvent(eventType: .sessionStarted)
|
|
}
|
|
|
|
// 添加会话结束事件处理器
|
|
recognizer.addSessionStoppedEventHandler { [weak self] (sender, event) in
|
|
guard let self = self else { return }
|
|
self.isContinuousRecognitionActive = false
|
|
self.notifyEvent(eventType: .sessionStopped)
|
|
}
|
|
|
|
// 添加取消事件处理器
|
|
recognizer.addCanceledEventHandler { [weak self] (sender, event) in
|
|
guard let self = self else { return }
|
|
|
|
print("\(self.tag): 识别取消: \(event.errorDetails ?? "未知错误")")
|
|
|
|
// 标记连续识别已停止
|
|
self.isContinuousRecognitionActive = false
|
|
|
|
// 通知错误
|
|
self.notifyEvent(eventType: .canceled, params: [
|
|
"error": "识别取消",
|
|
"errorDetails": event.errorDetails ?? "未知错误",
|
|
"reason": String(event.reason.rawValue)
|
|
])
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 触发事件通知
|
|
*/
|
|
private func notifyEvent(eventType: AsrEventType, params: [String: Any] = [:]) {
|
|
let event = AsrEvent(type: eventType, params: params)
|
|
|
|
// 在主线程通知事件
|
|
DispatchQueue.main.async {
|
|
for case let listener as AsrEventListener in self.eventListeners.allObjects {
|
|
listener.onEvent(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 外部音频流 - 使用拉流模式
|
|
* 基于NSObject实现,将外部推送的音频数据转换为SDK可拉取的形式
|
|
*/
|
|
class ExternalAudioStream: NSObject {
|
|
// 内部持有一个SPXPullAudioInputStream实例
|
|
private(set) var pullStream: SPXPullAudioInputStream!
|
|
|
|
// 音频数据缓冲区
|
|
private var buffer = Data()
|
|
private let lock = NSLock()
|
|
|
|
// 记录是否已关闭
|
|
private var isClosed = false
|
|
|
|
/**
|
|
* 初始化方法
|
|
*/
|
|
override init() {
|
|
super.init()
|
|
|
|
// 创建拉流,使用官方文档推荐的参数类型
|
|
pullStream = SPXPullAudioInputStream(
|
|
readHandler: { [weak self] (data: NSMutableData, size: UInt) -> Int in
|
|
guard let self = self else { return 0 }
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
if self.buffer.isEmpty { return 0 }
|
|
let bytesToRead = min(Int(size), self.buffer.count)
|
|
data.append(self.buffer.prefix(bytesToRead))
|
|
self.buffer.removeFirst(bytesToRead)
|
|
return bytesToRead
|
|
},
|
|
closeHandler: { [weak self] in
|
|
self?.close()
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 推送音频数据到缓冲区
|
|
*
|
|
* @param data 要推送的音频数据
|
|
*/
|
|
func pushAudio(_ data: Data) {
|
|
if isClosed { return }
|
|
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
|
|
// 添加到缓冲区
|
|
buffer.append(data)
|
|
}
|
|
|
|
/**
|
|
* 关闭流
|
|
*/
|
|
func close() {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
|
|
isClosed = true
|
|
buffer = Data()
|
|
}
|
|
}
|
|
}
|