Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 50 additions & 4 deletions Sources/DualEngineASR.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ private let logger = Logger(subsystem: "com.typeless.app", category: "DualEngine
/// 双引擎 ASR:Streaming Paraformer 实时预览 + QwenASR 流式精转写
///
/// 录音过程中:
/// - Streaming Paraformer 流式处理音频,实时输出到 HUD
/// - 前期(~2s):Streaming Paraformer 流式处理音频,实时输出到 HUD(低延迟预览)
/// - QwenASR 同步接收相同音频流,持续增量推理
/// - QwenASR 产出首段文本后,自动切换 HUD 显示源为 QwenASR(含 unfixedText)
/// - 切换后 Paraformer 回调被忽略,但仍继续接收音频
///
/// 松开 Fn 后:
/// - QwenASR finalize 并输出最终结果(含标点)
///
/// 并发模型:
/// - `qwenQueue`(serial):保护 QwenASR 引擎的所有调用(pushAudio / getResult / reset)
/// - `paraformerEngine` 使用自己内部的 `recognitionQueue`,完全独立
/// - `qwenHasOutput` 标志通过 os_unfair_lock 保护,确保跨队列安全读写
class DualEngineASR: ASREngine {

// MARK: - Constants
Expand All @@ -32,9 +35,33 @@ class DualEngineASR: ASREngine {

private let qwenQueue = DispatchQueue(label: "com.typeless.dualengine.qwen", qos: .userInitiated)

// MARK: - HUD 切换状态

/// QwenASR 是否已产出文本。一旦为 true,HUD 由 QwenASR 驱动,Paraformer 回调被忽略。
/// 该标志只会从 false → true 单向转变(每次录音期间),在 reset()/flush() 中重置。
/// 被 Paraformer 的 recognitionQueue 和 qwenQueue 两个队列读写,
/// 使用 os_unfair_lock 保护以确保线程安全。
private var _qwenHasOutput = false
private var _qwenLock = os_unfair_lock_s()

let needsPunctuation = false
let needsITN = true

/// 线程安全读取 qwenHasOutput
private var qwenHasOutput: Bool {
get {
os_unfair_lock_lock(&_qwenLock)
let value = _qwenHasOutput
os_unfair_lock_unlock(&_qwenLock)
return value
}
set {
os_unfair_lock_lock(&_qwenLock)
_qwenHasOutput = newValue
os_unfair_lock_unlock(&_qwenLock)
}
}

// MARK: - 初始化

/// - Parameters:
Expand All @@ -50,13 +77,28 @@ class DualEngineASR: ASREngine {
// MARK: - ASREngine 协议

func processAudio(samples: [Float], onPartialResult: @escaping (String, String?) -> Void) {
// 1. 转发给 Paraformer 做实时预览
paraformerEngine.processAudio(samples: samples, onPartialResult: onPartialResult)
// 1. 转发给 Paraformer 做实时预览(QwenASR 接管后跳过回调)
paraformerEngine.processAudio(samples: samples) { [weak self] text, unfixed in
guard let self = self, !self.qwenHasOutput else { return }
onPartialResult(text, unfixed)
}

// 2. 同步推送给 QwenASR 流式引擎
// 2. 同步推送给 QwenASR 流式引擎,产出文本后接管 HUD
qwenQueue.async { [weak self] in
guard let self = self else { return }
_ = self.qwenRecognizer.pushAudio(samples: samples, finalize: false)

let stableText = self.qwenRecognizer.getResult()
let unfixedText = self.qwenRecognizer.getUnfixed()

let hasContent = !stableText.isEmpty || (unfixedText != nil && !unfixedText!.isEmpty)
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
if hasContent {
if !self.qwenHasOutput {
self.qwenHasOutput = true
logger.info("HUD 切换:QwenASR 已产出文本,接管 HUD 显示")
}
onPartialResult(stableText, unfixedText)
}
}
}

Expand All @@ -75,6 +117,9 @@ class DualEngineASR: ASREngine {
let result = self.qwenRecognizer.getResult()
self.qwenRecognizer.reset()

// 重置 HUD 切换标志,为下一次录音做准备
self.qwenHasOutput = false

// 同步 reset Paraformer,防止下次录音 HUD 残留上次的累积文本
self.paraformerEngine.reset()

Expand All @@ -87,6 +132,7 @@ class DualEngineASR: ASREngine {
}

func reset() {
qwenHasOutput = false
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
paraformerEngine.reset()
qwenQueue.sync {
qwenRecognizer.reset()
Expand Down
Loading