fix(mac,asr): harden menu-bar delivery, chunk retry, and polish validator

Retain the external target app for menu-bar paste and polish context, retry
failed middle ASR chunks once, and stop false-positive path/number violations.
This commit is contained in:
Rocky
2026-07-29 18:30:36 +08:00
parent 34be2e8dd1
commit 4c929d8b8b
17 changed files with 646 additions and 39 deletions
@@ -143,7 +143,10 @@ public actor ChunkedUtterancePipeline {
action: "preMerge",
chunkIndex: chunk.index
)
let mergedResult = await transcribeChunk(samples: preMerge.samples)
let mergedResult = await transcribeChunkWithRetry(
samples: preMerge.samples,
chunkIndex: chunk.index
)
switch mergedResult {
case .success(let text):
// Empty / whitespace merge must NOT wipe a prior good segment
@@ -182,7 +185,10 @@ public actor ChunkedUtterancePipeline {
continue
}
let result = await transcribeChunk(samples: chunk.samples)
let result = await transcribeChunkWithRetry(
samples: chunk.samples,
chunkIndex: chunk.index
)
logChunkOutcome(chunk: chunk, result: result)
switch result {
case .success(let text):
@@ -199,7 +205,10 @@ public actor ChunkedUtterancePipeline {
action: "emptyRetry",
chunkIndex: chunk.index
)
let retryResult = await transcribeChunk(samples: retry.samples)
let retryResult = await transcribeChunkWithRetry(
samples: retry.samples,
chunkIndex: chunk.index
)
switch retryResult {
case .success(let retryText):
let trimmed = retryText.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -311,6 +320,30 @@ public actor ChunkedUtterancePipeline {
}.value
}
/// Retry one failed chunk before advancing the serial worker. Keeping the
/// same PCM samples prevents a transient request failure from creating an
/// undetectable hole in an otherwise fluent stitched transcript.
private func transcribeChunkWithRetry(
samples: [Float],
chunkIndex: Int
) async -> ASRChunkResult {
let first = await transcribeChunk(samples: samples)
guard case .failure(let message) = first else { return first }
guard !cancelled, !Task.isCancelled else { return .cancelled }
FlowTrace.warn(
"pipeline.chunk.retry",
"chunk=\(chunkIndex) samples=\(samples.count) error=\(message)"
)
do {
try await Task.sleep(nanoseconds: 150_000_000)
} catch {
return .cancelled
}
guard !cancelled, !Task.isCancelled else { return .cancelled }
return await transcribeChunk(samples: samples)
}
/// Pairs each chunk's audio with the text it produced, so an empty
/// transcript can be attributed to either silent audio or a mute engine.
private func logChunkOutcome(chunk: UtteranceAudioChunk, result: ASRChunkResult) {