Skip to content
Merged
Changes from 3 commits
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
83 changes: 77 additions & 6 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,52 @@ 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 lock = os_unfair_lock_s()

/// flush 进行中标志。设为 true 后,qwenQueue 中排队的 processAudio 块会立即跳过,
/// 避免 flush 块被大量耗时的 Metal GPU 推理阻塞。
private var _isFlushing = false

let needsPunctuation = false
let needsITN = true

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

/// 线程安全读写 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)
}
}

// MARK: - 初始化

/// - Parameters:
Expand All @@ -50,17 +96,36 @@ 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 }
guard let self = self, !self.isFlushing else { return }
_ = self.qwenRecognizer.pushAudio(samples: samples, finalize: false)

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

let hasContent = !stableText.isEmpty || !(unfixedText?.isEmpty ?? true)
if hasContent {
if !self.qwenHasOutput {
self.qwenHasOutput = true
logger.info("HUD 切换:QwenASR 已产出文本,接管 HUD 显示")
}
onPartialResult(stableText, unfixedText)
}
}
}

func flush(completion: @escaping (String) -> Void) {
// 先设置 flushing 标志,让 qwenQueue 中排队的 processAudio 块立即跳过,
// 避免 flush 块被大量耗时的 Metal GPU 推理阻塞
isFlushing = true
Comment thread
claude[bot] marked this conversation as resolved.

qwenQueue.async { [weak self] in
guard let self = self else {
DispatchQueue.main.async { completion("") }
Expand All @@ -75,6 +140,10 @@ class DualEngineASR: ASREngine {
let result = self.qwenRecognizer.getResult()
self.qwenRecognizer.reset()

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

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

Expand All @@ -87,9 +156,11 @@ class DualEngineASR: ASREngine {
}

func reset() {
paraformerEngine.reset()
qwenQueue.sync {
_qwenHasOutput = false
_isFlushing = false
qwenRecognizer.reset()
}
paraformerEngine.reset()
}
}
Loading