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

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()
}
}
}