M1-15: Keep the unfinalised transcript tail, so a crash costs seconds instead of the last thing said #25

Closed
opened 2026-08-08 12:28:41 -04:00 by agent · 0 comments
Member

Keep the speech engine's unfinalised tail on disk during a recording, so a crash costs
seconds of transcript instead of the last thing said.

What this costs today, measured

2026-08-08-113800-9qqy4g, recorded on wes-iphone and force-quit from the app switcher without
stopping. M1-10's launch sweep recovered it:

diag.lastStop      Stop button in the app at 2026-08-07T18:12:18Z   <- the day before
diag.lastLaunch    #17 at 2026-08-08T15:41:33Z
diag.lastRecovery  Recovered 1 capture(s) at 2026-08-08T15:41:34Z

The audio is 210.0 s and decodes clean end to end. The transcript's last cue ends at 193.7 s. The
last 16 seconds of speech are gone
— the analyzer was holding them unfinalised when the process
died, and nothing on disk had them.

That gap is what LiveTranscriber.finish() exists to drain on a normal stop. A crash never reaches
it.

Wes, after reading the recovery results: "Csan we capture the unfinalized transcription and
replace once confirmed? Then we lose less data."

This is not a reversal of the volatile-results decision

SpeechModel.transcriber sets reportingOptions: [] on purpose, and AGENTS.md says why: volatile
results are live hypotheses the engine revises as it hears more, and "taking them for the
transcript means keeping a guess that was later withdrawn."

That rule stands, and this issue must not break it. A volatile hypothesis may never appear in a
transcript that a finalised cue could have covered. What is proposed is narrower: keep the tail
somewhere separate, let every finalised cue supersede it, and only fall back to it for the stretch
after the last finalised cue in a capture that died. On a normal stop it is always discarded unread.

If that separation cannot be held cleanly, close this — a transcript nobody can trust is worse than
one that is 16 seconds short.

Scope

  • Ask for .volatileResults and keep the volatile stream entirely separate from
    LiveTranscriber.cues, which stays finalised-only. The guard result.isFinal in the results
    loop is the seam.
  • Write the tail to <base>_transcription.volatile.vtt inside the existing .parts folder. M1-10's
    part parser matches <base>_transcription.NNN.vtt exactly, so assembly already ignores it and
    the folder's deletion already cleans it up. Confirm both rather than assuming.
  • Rewrite it atomically, throttled. Volatile results fire continuously and one write per revision is
    write amplification during capture, when the audio path is what matters.
  • Truncate it against the last finalised cue, so it only ever holds the stretch nothing final covers.
  • On recovery only, if the tail covers time after the last assembled cue, append it.
  • On a normal stop, discard it.

Open questions

  1. Does the assembled transcript mark provisional text? WebVTT has no notion of confidence. A
    NOTE block before it is the obvious candidate, and it is what tells a vault consumer that
    those words were never confirmed. Silent inclusion is the option to argue against.
  2. How much is actually recoverable. 16 seconds is one measurement from one force-quit. Worth
    measuring across a few before deciding the complexity is paid for.
  3. Whether asking for volatile results changes finalised behaviour. AGENTS.md records
    "Measured with the options empty: 2 results, 0 volatile." Nothing has measured the other
    configuration, and the finalised stream is the thing that must not regress.

Done when

  • A recording force-quit mid-sentence recovers with the last words present, marked however question
    1 is answered.
  • A normal stop produces a transcript with no volatile text in it at all, byte for byte the same as
    it would be today.
  • No .volatile.vtt survives beside a finished capture.

Follows M1-10 (#10) and M1-14 (#22), which established the .parts folder and the launch sweep
this builds on.

Keep the speech engine's unfinalised tail on disk during a recording, so a crash costs seconds of transcript instead of the last thing said. ## What this costs today, measured `2026-08-08-113800-9qqy4g`, recorded on `wes-iphone` and force-quit from the app switcher without stopping. M1-10's launch sweep recovered it: ``` diag.lastStop Stop button in the app at 2026-08-07T18:12:18Z <- the day before diag.lastLaunch #17 at 2026-08-08T15:41:33Z diag.lastRecovery Recovered 1 capture(s) at 2026-08-08T15:41:34Z ``` The audio is 210.0 s and decodes clean end to end. The transcript's last cue ends at 193.7 s. **The last 16 seconds of speech are gone** — the analyzer was holding them unfinalised when the process died, and nothing on disk had them. That gap is what `LiveTranscriber.finish()` exists to drain on a normal stop. A crash never reaches it. Wes, after reading the recovery results: *"Csan we capture the unfinalized transcription and replace once confirmed? Then we lose less data."* ## This is not a reversal of the volatile-results decision `SpeechModel.transcriber` sets `reportingOptions: []` on purpose, and `AGENTS.md` says why: volatile results are live hypotheses the engine revises as it hears more, and *"taking them for the transcript means keeping a guess that was later withdrawn."* That rule stands, and this issue must not break it. A volatile hypothesis may **never** appear in a transcript that a finalised cue could have covered. What is proposed is narrower: keep the tail somewhere separate, let every finalised cue supersede it, and only fall back to it for the stretch after the last finalised cue in a capture that died. On a normal stop it is always discarded unread. If that separation cannot be held cleanly, close this — a transcript nobody can trust is worse than one that is 16 seconds short. ## Scope - Ask for `.volatileResults` and keep the volatile stream **entirely separate** from `LiveTranscriber.cues`, which stays finalised-only. The `guard result.isFinal` in the results loop is the seam. - Write the tail to `<base>_transcription.volatile.vtt` inside the existing `.parts` folder. M1-10's part parser matches `<base>_transcription.NNN.vtt` exactly, so assembly already ignores it and the folder's deletion already cleans it up. Confirm both rather than assuming. - Rewrite it atomically, throttled. Volatile results fire continuously and one write per revision is write amplification during capture, when the audio path is what matters. - Truncate it against the last finalised cue, so it only ever holds the stretch nothing final covers. - On recovery only, if the tail covers time after the last assembled cue, append it. - On a normal stop, discard it. ## Open questions 1. **Does the assembled transcript mark provisional text?** WebVTT has no notion of confidence. A `NOTE` block before it is the obvious candidate, and it is what tells a vault consumer that those words were never confirmed. Silent inclusion is the option to argue against. 2. **How much is actually recoverable.** 16 seconds is one measurement from one force-quit. Worth measuring across a few before deciding the complexity is paid for. 3. **Whether asking for volatile results changes finalised behaviour.** `AGENTS.md` records *"Measured with the options empty: 2 results, 0 volatile."* Nothing has measured the other configuration, and the finalised stream is the thing that must not regress. ## Done when - A recording force-quit mid-sentence recovers with the last words present, marked however question 1 is answered. - A normal stop produces a transcript with no volatile text in it at all, byte for byte the same as it would be today. - No `.volatile.vtt` survives beside a finished capture. --- *Follows M1-10 (#10) and M1-14 (#22), which established the `.parts` folder and the launch sweep this builds on.*
agent changed title from Keep the unfinalised transcript tail, so a crash costs seconds instead of the last thing said to M1-15: Keep the unfinalised transcript tail, so a crash costs seconds instead of the last thing said 2026-08-08 19:49:17 -04:00
wk changed title from M1-15: Keep the unfinalised transcript tail, so a crash costs seconds instead of the last thing said to M1-14: Keep the unfinalised transcript tail, so a crash costs seconds instead of the last thing said 2026-08-08 20:02:14 -04:00
wk changed title from M1-14: Keep the unfinalised transcript tail, so a crash costs seconds instead of the last thing said to M1-15: Keep the unfinalised transcript tail, so a crash costs seconds instead of the last thing said 2026-08-08 20:03:09 -04:00
wk closed this issue 2026-08-09 12:02:26 -04:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios#25
No description provided.