fix(flow): improve tail capture and final chunk ASR recovery

- Add FlowUtteranceEndCoordinator with 350ms silence drain and 150ms post-roll
- Extend FinalChunkRecovery for short/empty final chunks in chunked pipeline
- Snapshot partial at mic stop and guard final transcript in FlowSessionManager
- Unify tail drain presets (iosFlow/macMLX) and expand diagnostics

Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-26 10:13:40 +00:00
parent e6f99d2744
commit c7ee891a90
16 changed files with 485 additions and 42 deletions
@@ -16,22 +16,39 @@ public struct FlowCaptureTailDrainPolicy: Sendable, Equatable {
public let silenceDurationSeconds: TimeInterval
/// Hard cap so noisy environments cannot stall finalize forever.
public let maxDrainSeconds: TimeInterval
/// Fixed post-roll after silence drain; independent of RMS (captures weak tails).
public let postRollSeconds: TimeInterval
public init(
silenceRMSThreshold: Float,
silenceDurationSeconds: TimeInterval,
maxDrainSeconds: TimeInterval
maxDrainSeconds: TimeInterval,
postRollSeconds: TimeInterval = 0
) {
self.silenceRMSThreshold = silenceRMSThreshold
self.silenceDurationSeconds = silenceDurationSeconds
self.maxDrainSeconds = maxDrainSeconds
self.postRollSeconds = postRollSeconds
}
public static let flowDefault = FlowCaptureTailDrainPolicy(
/// iOS Flow host + keyboard utterance capture.
public static let iosFlow = FlowCaptureTailDrainPolicy(
silenceRMSThreshold: 0.015,
silenceDurationSeconds: 0.25,
maxDrainSeconds: 1.5
silenceDurationSeconds: 0.35,
maxDrainSeconds: 1.5,
postRollSeconds: 0.15
)
/// Mac MLX streaming live capture.
public static let macMLX = FlowCaptureTailDrainPolicy(
silenceRMSThreshold: 0.015,
silenceDurationSeconds: 0.35,
maxDrainSeconds: 0.75,
postRollSeconds: 0.15
)
/// Backward-compatible alias for iOS Flow defaults.
public static let flowDefault = iosFlow
}
/// Metrics emitted when tail drain completes (for diagnostics and tests).
@@ -39,21 +56,25 @@ public struct FlowCaptureDrainReport: Sendable, Equatable {
public let drainDurationSeconds: Double
public let endedBySilence: Bool
public let tailSampleCount: Int
public let postRollDurationSeconds: Double
public init(
drainDurationSeconds: Double,
endedBySilence: Bool,
tailSampleCount: Int
tailSampleCount: Int,
postRollDurationSeconds: Double = 0
) {
self.drainDurationSeconds = drainDurationSeconds
self.endedBySilence = endedBySilence
self.tailSampleCount = tailSampleCount
self.postRollDurationSeconds = postRollDurationSeconds
}
public static let skipped = FlowCaptureDrainReport(
drainDurationSeconds: 0,
endedBySilence: false,
tailSampleCount: 0
tailSampleCount: 0,
postRollDurationSeconds: 0
)
}