M1-09 follow-up: make the transcript parts actually assemble #24
No reviewers
Labels
No labels
area/agent
area/capture
area/editor
area/geo
area/security
area/sync
area/transcription
area/ui
area/vault
area/voice-memos
blocked
milestone/M1
milestone/M2
milestone/M3
milestone/M4
needs-decision
needs-hardware
p0
p1
p2
type/bug
type/enhancement
type/feature
type/idea
type/infrastructure
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Stash/stash-ios!24
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "m1-09-vtt-assembly"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #9, reopened after #23 merged.
What was wrong
#23 claimed assembly was concatenation. It was not. A rendered part ended on its last spoken word
rather than on a blank line, and a WebVTT cue block runs until a blank line — so joining two parts
put the next part's
WEBVTTsignature inside the previous cue. FFmpeg read it as words that hadbeen spoken. The signature is also only a signature at the very start of a file.
The close-out comment on #9 said "concatenating the parts yields the full transcript", and the
test behind it checked that every line of text was present. It was present. That is why neither
caught this.
Three more in the same code, from the same Codex review:
&, a<or a-->in what somebody said could change the line ortruncate it, and a blank line inside a cue ended the cue and took the rest of its own words.
59.9996rendered
00:00:60.000. WebVTT holds that field to0...59.TranscriptPartshas alreadyreleased those cues by then, so a moment of full storage cost a minute of transcript
permanently, with nothing marking the hole.
What changed
Assembly is
VTT.assemble, notcat. First part keeps its signature, the rest give theirs upat assembly. Every part on disk still carries one, because being openable on its own mid-walk is
the only reason parts exist.
Cue text is escaped, so nothing a person says can be read as syntax. Line endings normalise
and a run of them collapses to one — the single thing here that does not round-trip, and it costs
a line break rather than a word.
Timestamps round to milliseconds before they are split, and an end is always at least a
millisecond after its own start.
A failed part is retried at every later boundary and at stop, the written list stays in part
order so a retry cannot land out of sequence, and a stuck part is reported while the recording
runs — storage is the failure that happens here and the one somebody can act on, but only while
there is still a recording for the freed space to take.
The first commit is a separate concern: Wes's rewrite of the development process in
AGENTS.mdplus the
.githooksthat back it, which were sitting uncommitted in the tree. Split out so it isreviewable apart from the fix.
Validated
Device —
wes-iphone, iPhone 17 Pro, iOS 26, one three-minute recording, three parts, 21cues. Each part opens with its own signature and ends on a blank line. Assembled: one signature,
cue numbers 1–21 unbroken, starts monotonic, every end after its own start, no illegal seconds
field, FFmpeg parses it clean. Both seams exact — part 1 ends
00:01:04.260, part 2 starts00:01:04.260— with nothing duplicated or dropped across either. This is also the only checkthat the writer is still wired to the transcriber; no unit test can reach that.
Tests — 50, up from 21. Built on a WebVTT conformance reader written from the grammar with no
knowledge of
VTT.render, plus a test proving that reader rejects all four pre-fix behaviours, sothe assertions on top of it can actually fail.
make lintclean.Not validated
those words rather than the symbols, so no reserved character reached the cue text and the
escaping path never fired. Covered byte for byte by unit tests; not exercised by dictation.
Worth knowing on its own that speech-to-text spells these out.
on-screen report are covered against a synthetic block only.
Codex
Four passes — plan, code, tests, security. Every finding's disposition is in
.agent/m1-09-vtt-assembly/SUMMARY.md. The ones that changed the outcome:WEBVTTXfor a signature and deleted the line, losing a cue.finish(), so a writer that only retried atstop would have passed them.
onlyCompletedPartsAreVisiblecould not fail; renamed to what it actually asserts.Nothing was pushed yet, so the branch was rewritten rather than leaving them in reachable
history, and
codex-run.shnow strips them at the source.Two were declined and both are recorded with reasons: the
stash-docsdecision file, deferred toStash/stash-docs#3 because there is no decisions convention to write into yet, and
pre-pushvalidating the checked-out branch rather than the refs on stdin — real, but Wes's in-flight work
on the process gate rather than anything this issue touches.