Skip to content

Commit ac6218d

Browse files
ZhaoChaoqunclaude
andcommitted
refactor: split SherpaOnnxManager and extract magic numbers (P0-2, P1-4)
Split SherpaOnnxManager.swift (803 lines, 5 responsibilities) into focused components: - ModelPathResolver: resolves local model paths and checks existence - ModelDownloader: handles downloading from ModelScope/GitHub with fallback - ModelExtractionService: handles tar.bz2 extraction - SherpaOnnxManager: thin orchestrator delegating to the above Extract magic numbers as named constants in: - ChineseSpellingCorrector: logit/softmax thresholds, correction ratio limit - DualEngineASR: flush silence sample count - QwenASRRecognizer: chunk_sec, rollback, max_new_tokens, segment_sec - ASREngine: flush silence sample count - SherpaOnnxOnlineRecognizer: sample rate, feature dim, endpoint detection rules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 209cb26 commit ac6218d

10 files changed

Lines changed: 868 additions & 638 deletions

Sources/ASREngine.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ class StreamingParaformerEngine: ASREngine {
8888

8989
/// QwenASR 流式引擎:模拟流式(chunk + rollback)
9090
class QwenASREngine: ASREngine {
91+
92+
// MARK: - Constants
93+
94+
/// 结束时推送的最小静音样本数(0.1s × 16kHz = 1600 samples)
95+
/// 用于刷新尾部不足一个 chunk 的音频,不会导致 decoder hallucinate
96+
private static let flushSilenceSampleCount = 1600
97+
98+
// MARK: - Properties
99+
91100
private let recognizer: ASRStreamRecognizing
92101
private let recognitionQueue: DispatchQueue
93102

@@ -127,7 +136,7 @@ class QwenASREngine: ASREngine {
127136
// 推送极少量 silence(0.1s)+ finalize,让 Rust 处理尾部不足一个 chunk 的音频
128137
// 并 commit rollback 窗口内的 token。0.1s silence 不会导致 decoder hallucinate,
129138
// 而之前的 1s silence 在长音频上导致 decoder 重复之前的内容。
130-
let minimalSilence = [Float](repeating: 0.0, count: 1600)
139+
let minimalSilence = [Float](repeating: 0.0, count: Self.flushSilenceSampleCount)
131140
_ = self.recognizer.pushAudio(samples: minimalSilence, finalize: true)
132141

133142
let result = self.recognizer.getResult()

Sources/ChineseSpellingCorrector.swift

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ private let logger = Logger(subsystem: "com.typeless.app", category: "CSC")
88
/// 使用 ONNX Runtime C API 加载 macbert4csc-base-chinese INT8 模型,
99
/// 对 ASR 输出的中文文本进行同音字/近音字纠错。
1010
class ChineseSpellingCorrector {
11+
12+
// MARK: - Constants
13+
14+
/// ONNX Runtime intra-op 线程数
15+
private static let ortIntraOpThreads: Int32 = 2
16+
17+
/// BERT 最大序列长度
18+
private static let maxSequenceLength = 512
19+
20+
/// logit 差值阈值:预测 token 与原始 token 的 logit 差必须大于此值才执行替换
21+
private static let logitDiffThreshold: Float = 5.0
22+
23+
/// softmax top-1 概率阈值:预测 token 的 softmax 概率必须大于此值才执行替换
24+
private static let softmaxProbThreshold: Float = 0.9
25+
26+
/// 纠正比例上限:纠正的中文字符占比超过此值时丢弃所有纠正(防止上下文异常导致误纠)
27+
private static let maxCorrectionRatio: Float = 0.2
28+
29+
// MARK: - Properties
30+
1131
private let api: UnsafePointer<OrtApi>
1232
private var env: OpaquePointer? // OrtEnv*
1333
private var session: OpaquePointer? // OrtSession*
@@ -79,7 +99,7 @@ class ChineseSpellingCorrector {
7999
}
80100

81101
// 设置线程数
82-
_ = api.pointee.SetIntraOpNumThreads(sessionOptionsPtr, 2)
102+
_ = api.pointee.SetIntraOpNumThreads(sessionOptionsPtr, Self.ortIntraOpThreads)
83103

84104
// 创建 Session
85105
var sessionPtr: OpaquePointer? = nil
@@ -131,7 +151,7 @@ class ChineseSpellingCorrector {
131151
let seqLen = inputIds.count
132152

133153
guard seqLen > 2 else { return text } // 只有 [CLS] 和 [SEP]
134-
guard seqLen <= 512 else {
154+
guard seqLen <= Self.maxSequenceLength else {
135155
logger.warning("文本过长 (\(seqLen) tokens),跳过 CSC")
136156
return text
137157
}
@@ -265,7 +285,7 @@ class ChineseSpellingCorrector {
265285
// Sanity check:如果纠正比例超过 20%,说明输入可能有上下文问题,丢弃纠正
266286
if correctionCount > 0 && chineseCharCount > 0 {
267287
let correctionRatio = Float(correctionCount) / Float(chineseCharCount)
268-
if correctionRatio > 0.2 {
288+
if correctionRatio > Self.maxCorrectionRatio {
269289
logger.warning("CSC 纠正比例过高 (\(correctionCount)/\(chineseCharCount) = \(String(format: "%.0f%%", correctionRatio * 100))),丢弃纠正结果")
270290
return originalText
271291
}
@@ -305,17 +325,17 @@ class ChineseSpellingCorrector {
305325
let originalLogit = logitsPtr[logitsOffset + Int(originalId)]
306326
let logitDiff = maxVal - originalLogit
307327

308-
// 条件 1:logit 差值必须足够大(提高到 5.0,防止低置信度替换
309-
guard logitDiff > 5.0 else { return nil }
328+
// 条件 1:logit 差值必须足够大,防止低置信度替换
329+
guard logitDiff > Self.logitDiffThreshold else { return nil }
310330

311-
// 条件 2:计算 softmax 概率,top-1 概率必须 > 0.9
331+
// 条件 2:计算 softmax 概率,top-1 概率必须足够高
312332
// 为数值稳定性,对 logits 减去 maxVal 后再 softmax
313333
var expSum: Float = 0
314334
for j in 0..<vocabSize {
315335
expSum += exp(logitsPtr[logitsOffset + j] - maxVal)
316336
}
317337
let topProb = 1.0 / expSum // exp(0) / expSum since maxVal - maxVal = 0
318-
guard topProb > 0.9 else { return nil }
338+
guard topProb > Self.softmaxProbThreshold else { return nil }
319339

320340
let correctedChar = tokenizer.decode([maxIdx])
321341
if !correctedChar.isEmpty && correctedChar.count == 1 {

Sources/DualEngineASR.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ private let logger = Logger(subsystem: "com.typeless.app", category: "DualEngine
1717
/// - `paraformerEngine` 使用自己内部的 `recognitionQueue`,完全独立
1818
class DualEngineASR: ASREngine {
1919

20+
// MARK: - Constants
21+
22+
/// 结束时推送的最小静音样本数(0.1s × 16kHz = 1600 samples)
23+
/// 用于刷新 QwenASR 尾部不足一个 chunk 的音频
24+
private static let flushSilenceSampleCount = 1600
25+
2026
// MARK: - 子引擎
2127

2228
private let paraformerEngine: StreamingParaformerEngine
@@ -63,7 +69,7 @@ class DualEngineASR: ASREngine {
6369

6470
// 推送极少量 silence(0.1s)+ finalize,让 Rust 处理尾部不足一个 chunk 的音频
6571
// 并 commit rollback 窗口内的 token
66-
let minimalSilence = [Float](repeating: 0.0, count: 1600)
72+
let minimalSilence = [Float](repeating: 0.0, count: Self.flushSilenceSampleCount)
6773
_ = self.qwenRecognizer.pushAudio(samples: minimalSilence, finalize: true)
6874

6975
let result = self.qwenRecognizer.getResult()

0 commit comments

Comments
 (0)