feat(polish): add question guard, ABE routing, and flow trace

Harden polish so question drafts stay questions, add local density
routing with style-specific degrade, expand fun style packs, and add
end-to-end FlowTrace logging plus offline guard eval scripts.
This commit is contained in:
Rocky
2026-07-29 16:17:16 +08:00
parent d656bac8c3
commit 65fe3a81b4
19 changed files with 1443 additions and 71 deletions
@@ -22,8 +22,20 @@ public enum UtteranceStreamChunker {
var chunkIndex = 0
func emit(upTo splitEnd: Int, isLast: Bool) {
guard splitEnd > 0, splitEnd <= buffer.count else { return }
guard splitEnd > 0, splitEnd <= buffer.count else {
FlowTrace.warn(
"pipeline.chunk.emitSkipped",
"chunk=\(chunkIndex) splitEnd=\(splitEnd) buffered=\(buffer.count)"
)
return
}
let chunkSamples = Array(buffer[..<splitEnd])
FlowTrace.pipeline(
"chunk.emit",
"chunk=\(chunkIndex) samples=\(chunkSamples.count) "
+ "seconds=\(FlowTrace.seconds(samples: chunkSamples.count, sampleRate: config.sampleRate)) "
+ "rms=\(FlowTrace.rms(chunkSamples)) isLast=\(isLast ? 1 : 0)"
)
continuation.yield(
UtteranceAudioChunk(index: chunkIndex, samples: chunkSamples, isLast: isLast)
)
@@ -36,9 +48,13 @@ public enum UtteranceStreamChunker {
}
}
var receivedSnapshots = 0
var receivedSamples = 0
for await snap in stream {
if Task.isCancelled { break }
guard !snap.samples.isEmpty else { continue }
receivedSnapshots += 1
receivedSamples += snap.samples.count
buffer.append(contentsOf: snap.samples)
while buffer.count >= config.maxChunkSamples(forChunkIndex: chunkIndex) {
@@ -51,10 +67,24 @@ public enum UtteranceStreamChunker {
}
}
FlowTrace.pipeline(
"chunk.streamEnded",
"snapshots=\(receivedSnapshots) samples=\(receivedSamples) "
+ "seconds=\(FlowTrace.seconds(samples: receivedSamples, sampleRate: config.sampleRate)) "
+ "chunksEmitted=\(chunkIndex) buffered=\(buffer.count) "
+ "cancelled=\(Task.isCancelled ? 1 : 0)"
)
if !buffer.isEmpty {
emit(upTo: buffer.count, isLast: true)
} else if chunkIndex == 0 {
// Empty utterance no chunks.
// Empty utterance no chunks. The recogniser is never
// invoked, so an empty transcript here means the mic stream
// itself was empty, not that recognition failed.
FlowTrace.warn(
"pipeline.chunk.emptyUtterance",
"snapshots=\(receivedSnapshots) samples=0 chunksEmitted=0"
)
} else {
// Stream ended exactly on a chunk boundary; prior emit holds
// all tail audio. Marker so FinalChunkRecovery paths run.