-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathASREngine.swift
More file actions
185 lines (146 loc) · 6.1 KB
/
Copy pathASREngine.swift
File metadata and controls
185 lines (146 loc) · 6.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import Foundation
import os
/// 统一的 ASR 引擎接口
///
/// 封装了不同 ASR 后端(Streaming Paraformer、QwenASR)的差异,
/// 让 RecordingManager 无需关心具体引擎类型。
protocol ASREngine: AnyObject {
/// 将新的音频采样送入引擎
/// - Parameter samples: 16kHz monoFloat32 PCM 采样
/// - Parameter onPartialResult: 有新的部分识别结果时回调(stableText, unfixedText)
func processAudio(samples: [Float], onPartialResult: @escaping (String, String?) -> Void)
/// 刷新引擎缓冲区,获取最终识别文本
/// - Parameter completion: 返回最终文本(在 main queue 上调用)
func flush(completion: @escaping (String) -> Void)
/// 重置引擎状态,准备下一次识别
func reset()
/// 引擎是否需要外部标点处理
var needsPunctuation: Bool { get }
/// 引擎是否需要外部 ITN(逆文本规范化,如"一百二十三"→"123")
var needsITN: Bool { get }
}
// MARK: - Streaming Paraformer Engine
/// Streaming Paraformer 引擎:原生流式识别
class StreamingParaformerEngine: ASREngine {
private let recognizer: SherpaOnnxOnlineRecognizer
private let recognitionQueue: DispatchQueue
let needsPunctuation = true
let needsITN = false
init(recognizer: SherpaOnnxOnlineRecognizer, recognitionQueue: DispatchQueue) {
self.recognizer = recognizer
self.recognitionQueue = recognitionQueue
}
func processAudio(samples: [Float], onPartialResult: @escaping (String, String?) -> Void) {
recognitionQueue.async { [weak self] in
guard let self = self else { return }
self.recognizer.acceptWaveform(samples: samples)
while self.recognizer.isReady() {
self.recognizer.decode()
}
let text = self.recognizer.getResult()
if !text.isEmpty {
onPartialResult(text, nil)
}
}
}
func flush(completion: @escaping (String) -> Void) {
recognitionQueue.async { [weak self] in
guard let self = self else {
DispatchQueue.main.async { completion("") }
return
}
// is_final: 标记最终 chunk,启用短 chunk 接受 + CIF 尾部 token flush
self.recognizer.setFinalChunk()
self.recognizer.inputFinished()
while self.recognizer.isReady() {
self.recognizer.decode()
}
let text = self.recognizer.getResult()
self.recognizer.reset()
DispatchQueue.main.async {
completion(text)
}
}
}
func reset() {
recognizer.reset()
}
}
// MARK: - QwenASR Streaming Engine
/// QwenASR 流式引擎:模拟流式(chunk + rollback)
class QwenASREngine: ASREngine {
// MARK: - Constants
/// 结束时推送的最小静音样本数(0.1s × 16kHz = 1600 samples)
/// 用于刷新尾部不足一个 chunk 的音频,不会导致 decoder hallucinate
private static let flushSilenceSampleCount = 1600
// MARK: - Properties
private let recognizer: ASRStreamRecognizing
private let recognitionQueue: DispatchQueue
/// flush 进行中标志。设为 true 后,recognitionQueue 中排队的 processAudio 块会立即跳过,
/// 避免 flush 块被大量耗时的 Metal GPU 推理阻塞。
private var _isFlushing = false
private var lock = os_unfair_lock_s()
/// 线程安全读写 isFlushing
private var isFlushing: Bool {
get {
os_unfair_lock_lock(&lock)
let value = _isFlushing
os_unfair_lock_unlock(&lock)
return value
}
set {
os_unfair_lock_lock(&lock)
_isFlushing = newValue
os_unfair_lock_unlock(&lock)
}
}
let needsPunctuation = false
let needsITN = true
init(recognizer: ASRStreamRecognizing, recognitionQueue: DispatchQueue) {
self.recognizer = recognizer
self.recognitionQueue = recognitionQueue
}
func processAudio(samples: [Float], onPartialResult: @escaping (String, String?) -> Void) {
recognitionQueue.async { [weak self] in
guard let self = self, !self.isFlushing else { return }
_ = self.recognizer.pushAudio(samples: samples, finalize: false)
// stable 确认文本
let stableText = self.recognizer.getResult()
// unfixed 投机文本(rollback 窗口 + unfixed chunks 中的 token)
let unfixedText = self.recognizer.getUnfixed()
let hasContent = !stableText.isEmpty || !(unfixedText?.isEmpty ?? true)
if hasContent {
onPartialResult(stableText, unfixedText)
}
}
}
func flush(completion: @escaping (String) -> Void) {
// 先设置 flushing 标志,让 recognitionQueue 中排队的 processAudio 块立即跳过,
// 避免 flush 块被大量耗时的 Metal GPU 推理阻塞
isFlushing = true
recognitionQueue.async { [weak self] in
guard let self = self else {
DispatchQueue.main.async { completion("") }
return
}
// 推送极少量 silence(0.1s)+ finalize,让 Rust 处理尾部不足一个 chunk 的音频
// 并 commit rollback 窗口内的 token。0.1s silence 不会导致 decoder hallucinate,
// 而之前的 1s silence 在长音频上导致 decoder 重复之前的内容。
let minimalSilence = [Float](repeating: 0.0, count: Self.flushSilenceSampleCount)
_ = self.recognizer.pushAudio(samples: minimalSilence, finalize: true)
let result = self.recognizer.getResult()
self.recognizer.reset()
// 重置标志,为下一次录音做准备
self.isFlushing = false
DispatchQueue.main.async {
completion(result)
}
}
}
func reset() {
recognitionQueue.sync {
_isFlushing = false
recognizer.reset()
}
}
}