feat(polish): add context safeguards, layered prompts, and output validation

Use redacted cursor neighborhood and pause-aware chunks for more natural polish,
validate protected terms with retry/local fallback, and structure bilingual prompts
for consistency and provider prefix caching.
This commit is contained in:
Rocky
2026-07-29 17:45:11 +08:00
parent 2d44423f4c
commit 34be2e8dd1
40 changed files with 1827 additions and 183 deletions
@@ -8,11 +8,18 @@ import Foundation
public struct ChunkedUtteranceSuccess: Sendable, Equatable {
public let text: String
/// Same transcript with internal pause markers, used only by polish.
public let textWithPauseMarks: String
/// Non-fatal per-chunk ASR issues (delivered as soft warning when non-empty).
public let chunkWarnings: [String]
public init(text: String, chunkWarnings: [String] = []) {
public init(
text: String,
textWithPauseMarks: String? = nil,
chunkWarnings: [String] = []
) {
self.text = text
self.textWithPauseMarks = textWithPauseMarks ?? text
self.chunkWarnings = chunkWarnings
}
}
@@ -150,7 +157,11 @@ public actor ChunkedUtterancePipeline {
)
} else {
stitcher.removeLastSegment()
stitcher.append(index: preMerge.stitchIndex, text: text)
stitcher.append(
index: preMerge.stitchIndex,
text: text,
trailingPauseSeconds: chunk.trailingPauseSeconds
)
publishPartial(from: stitcher, onPartial: onPartial)
}
case .failure(let message):
@@ -196,14 +207,26 @@ public actor ChunkedUtterancePipeline {
if retry.stitchIndex < chunk.index {
stitcher.removeLastSegment()
}
stitcher.append(index: retry.stitchIndex, text: retryText)
stitcher.append(
index: retry.stitchIndex,
text: retryText,
trailingPauseSeconds: chunk.trailingPauseSeconds
)
publishPartial(from: stitcher, onPartial: onPartial)
} else {
stitcher.append(index: chunk.index, text: text)
stitcher.append(
index: chunk.index,
text: text,
trailingPauseSeconds: chunk.trailingPauseSeconds
)
publishPartial(from: stitcher, onPartial: onPartial)
}
case .failure(let message):
stitcher.append(index: chunk.index, text: text)
stitcher.append(
index: chunk.index,
text: text,
trailingPauseSeconds: chunk.trailingPauseSeconds
)
publishPartial(from: stitcher, onPartial: onPartial)
failedChunks += 1
chunkWarnings.append(
@@ -218,7 +241,11 @@ public actor ChunkedUtterancePipeline {
return .cancelled
}
} else {
stitcher.append(index: chunk.index, text: text)
stitcher.append(
index: chunk.index,
text: text,
trailingPauseSeconds: chunk.trailingPauseSeconds
)
publishPartial(from: stitcher, onPartial: onPartial)
}
case .failure(let message):
@@ -241,6 +268,8 @@ public actor ChunkedUtterancePipeline {
_ = await feeder.value
let finalText = stitcher.composedSafely().trimmingCharacters(in: .whitespacesAndNewlines)
let markedText = stitcher.composedWithPauseMarks()
.trimmingCharacters(in: .whitespacesAndNewlines)
FlowPipelineDiagnostics.logChunkFinalize(
chunkCount: processedChunks,
lastChunkSamples: lastChunkSamples,
@@ -265,7 +294,13 @@ public actor ChunkedUtterancePipeline {
finalText,
"chunks=\(processedChunks) failedChunks=\(failedChunks) warnings=\(chunkWarnings.count)"
)
return .success(ChunkedUtteranceSuccess(text: finalText, chunkWarnings: chunkWarnings))
return .success(
ChunkedUtteranceSuccess(
text: finalText,
textWithPauseMarks: markedText,
chunkWarnings: chunkWarnings
)
)
}
private func transcribeChunk(samples: [Float]) async -> ASRChunkResult {