@@ -6,15 +6,18 @@ private let logger = Logger(subsystem: "com.typeless.app", category: "DualEngine
66/// 双引擎 ASR:Streaming Paraformer 实时预览 + QwenASR 流式精转写
77///
88/// 录音过程中:
9- /// - Streaming Paraformer 流式处理音频,实时输出到 HUD
9+ /// - 前期(~2s): Streaming Paraformer 流式处理音频,实时输出到 HUD(低延迟预览)
1010/// - QwenASR 同步接收相同音频流,持续增量推理
11+ /// - QwenASR 产出首段文本后,自动切换 HUD 显示源为 QwenASR(含 unfixedText)
12+ /// - 切换后 Paraformer 回调被忽略,但仍继续接收音频
1113///
1214/// 松开 Fn 后:
1315/// - QwenASR finalize 并输出最终结果(含标点)
1416///
1517/// 并发模型:
1618/// - `qwenQueue`(serial):保护 QwenASR 引擎的所有调用(pushAudio / getResult / reset)
1719/// - `paraformerEngine` 使用自己内部的 `recognitionQueue`,完全独立
20+ /// - `qwenHasOutput` 标志通过 os_unfair_lock 保护,确保跨队列安全读写
1821class DualEngineASR : ASREngine {
1922
2023 // MARK: - Constants
@@ -32,9 +35,33 @@ class DualEngineASR: ASREngine {
3235
3336 private let qwenQueue = DispatchQueue ( label: " com.typeless.dualengine.qwen " , qos: . userInitiated)
3437
38+ // MARK: - HUD 切换状态
39+
40+ /// QwenASR 是否已产出文本。一旦为 true,HUD 由 QwenASR 驱动,Paraformer 回调被忽略。
41+ /// 该标志只会从 false → true 单向转变(每次录音期间),在 reset()/flush() 中重置。
42+ /// 被 Paraformer 的 recognitionQueue 和 qwenQueue 两个队列读写,
43+ /// 使用 os_unfair_lock 保护以确保线程安全。
44+ private var _qwenHasOutput = false
45+ private var _qwenLock = os_unfair_lock_s ( )
46+
3547 let needsPunctuation = false
3648 let needsITN = true
3749
50+ /// 线程安全读取 qwenHasOutput
51+ private var qwenHasOutput : Bool {
52+ get {
53+ os_unfair_lock_lock ( & _qwenLock)
54+ let value = _qwenHasOutput
55+ os_unfair_lock_unlock ( & _qwenLock)
56+ return value
57+ }
58+ set {
59+ os_unfair_lock_lock ( & _qwenLock)
60+ _qwenHasOutput = newValue
61+ os_unfair_lock_unlock ( & _qwenLock)
62+ }
63+ }
64+
3865 // MARK: - 初始化
3966
4067 /// - Parameters:
@@ -50,13 +77,28 @@ class DualEngineASR: ASREngine {
5077 // MARK: - ASREngine 协议
5178
5279 func processAudio( samples: [ Float ] , onPartialResult: @escaping ( String , String ? ) -> Void ) {
53- // 1. 转发给 Paraformer 做实时预览
54- paraformerEngine. processAudio ( samples: samples, onPartialResult: onPartialResult)
80+ // 1. 转发给 Paraformer 做实时预览(QwenASR 接管后跳过回调)
81+ paraformerEngine. processAudio ( samples: samples) { [ weak self] text, unfixed in
82+ guard let self = self , !self . qwenHasOutput else { return }
83+ onPartialResult ( text, unfixed)
84+ }
5585
56- // 2. 同步推送给 QwenASR 流式引擎
86+ // 2. 同步推送给 QwenASR 流式引擎,产出文本后接管 HUD
5787 qwenQueue. async { [ weak self] in
5888 guard let self = self else { return }
5989 _ = self . qwenRecognizer. pushAudio ( samples: samples, finalize: false )
90+
91+ let stableText = self . qwenRecognizer. getResult ( )
92+ let unfixedText = self . qwenRecognizer. getUnfixed ( )
93+
94+ let hasContent = !stableText. isEmpty || ( unfixedText != nil && !unfixedText!. isEmpty)
95+ if hasContent {
96+ if !self . qwenHasOutput {
97+ self . qwenHasOutput = true
98+ logger. info ( " HUD 切换:QwenASR 已产出文本,接管 HUD 显示 " )
99+ }
100+ onPartialResult ( stableText, unfixedText)
101+ }
60102 }
61103 }
62104
@@ -75,6 +117,9 @@ class DualEngineASR: ASREngine {
75117 let result = self . qwenRecognizer. getResult ( )
76118 self . qwenRecognizer. reset ( )
77119
120+ // 重置 HUD 切换标志,为下一次录音做准备
121+ self . qwenHasOutput = false
122+
78123 // 同步 reset Paraformer,防止下次录音 HUD 残留上次的累积文本
79124 self . paraformerEngine. reset ( )
80125
@@ -87,6 +132,7 @@ class DualEngineASR: ASREngine {
87132 }
88133
89134 func reset( ) {
135+ qwenHasOutput = false
90136 paraformerEngine. reset ( )
91137 qwenQueue. sync {
92138 qwenRecognizer. reset ( )
0 commit comments