M1-09: VTT part writer — one-minute transcript chunks #9
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#9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Write transcript chunks to disk roughly every minute while recording continues.
Why chunks exist at all
So the vault agent can read transcripts while Wes is still walking, and so a crash costs
a minute rather than the whole memo. This is the entire justification — it is why transcripts
chunk and audio does not.
Layout
Parts live in a sibling folder named for the capture, not loose alongside finished files,
so that any consumer sees only completed artifacts at the top level and reads the
.partsfolder deliberately when it wants to work ahead. The vault's inbox processing needs no changes.
Rules
00:02:14.000 --> 00:02:19.000, offsetfrom the start of the recording. Every part is therefore valid WebVTT on its own, and
assembly is concatenation rather than arithmetic.
it is complete, or a consumer reads half a file.
through a sentence the recognizer has not finalized, producing a truncated cue in one part
and a duplicate or dropped clause in the next. At the sixty-second mark, write out every
finalized cue and carry the in-progress one into the next part. Parts end up roughly a
minute, which matters to nothing.
Done when
A ten-minute recording produces ten well-formed parts, each independently openable, with no
duplicated or dropped text at any boundary. Concatenating them by hand yields the full
transcript.
Milestone 1 of the Stash v3 rebuild, scoped 2026-08-06. M1 is a functioning iOS app on Wes's
phone with no sync at all — the entire goal is recording voice memos reliably and getting
them off the device by hand. Sync arrives in M3.
Working agreement for every issue in this repo: feature branch, tested before the PR opens
(on-device where the label says so), then Wes reviews the PR and we walk the code together.
Docs update in the same commit. A PR that takes more than 20 minutes to review is too big —
say so and split it.
Validated on
wes-iphone, iOS 26.5.2, against four real recordings pulled off the device.Works
The issue asks for a ten-minute recording producing ten well-formed parts. The 44-minute walk
produced forty, across 39 boundaries. Checked on all four:
WEBVTTheader on every part; each opens on its own..vttfiles in.parts/— no temp file ever became visible.The 23.1s tail on the 2.5-minute recording is not a defect. Measured against the audio it is the
stretch after Wes set the phone down; the other three transcribe to within a second of the end,
so the drain at stop works.
Tests
First test target in this repository. 21 tests,
make test, plus a CI step that will run once amacOS agent exists.
TranscriptPartsholds the boundary rule and nothing else — no filesystem, nospeech engine — which is what lets ten simulated minutes run in a millisecond.
Covered: the boundary against ten minutes of cues, cue numbering across parts, a cue spanning a
boundary, the cue that crosses one opening the next part whole, a ten-minute silence producing no
empty parts, WebVTT rendered byte for byte, hours and milliseconds in timestamps, reassembly by
concatenation, the file layout and naming, and a write that cannot happen being reported rather
than thrown. Fixtures are synthetic.
Not validated
timestamps are self-consistent and the parts line up, but nobody has played a recording against
its transcript and confirmed a cue names the moment the words were said.
assembles them and nothing marks the capture as unfinished. Filed as #22.
one.
lastErrorcarries the failure to thescreen and is covered by a test, but only against a synthetic failure.
untested.
Follow-on
#22 covers writing the assembled
<base>_transcription.vttbeside the audio at stop, which iswhat the vault expects and what this issue leaves to be done by hand.
Reopened. The close-out comment above checked that concatenating the parts yields the full
transcript, and that check was wrong — it looked for the text being present, and the text was
present. What it could not see is that the parts do not join into a well-formed file.
A rendered part ended on its last spoken word rather than on a blank line. A WebVTT cue block runs
until a blank line, so joining two parts puts the next part's
WEBVTTsignature inside theprevious cue. FFmpeg reads it exactly that way:
The signature is also only a signature at the very start of a file, so even with the blank line
fixed, a repeated one is a block a conforming reader discards.
Three more in the same code, all from the same Codex review:
&, a<or a-->in what somebody actually said could change theline or truncate it, and a blank line inside a cue ended the cue and took the rest of its own
words with it.
59.9996rendered
00:00:60.000. WebVTT holds that field to0...59.TranscriptPartshas alreadyreleased those cues by the time a write can fail, so a moment of full storage cost a minute of
transcript permanently, with nothing marking the hole.
The fix
Branch
m1-09-vtt-assembly, six commits, pushed. No PR yet — device testing first.Assembly is now
VTT.assemblerather thancat: the first part keeps its signature and the restgive theirs up at assembly time. 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. Timestamps round 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 now reported while the recording runs — storage
is the failure that happens here and it is the one somebody can act on, but only while there is
still a recording for the freed space to take.
Tests
50, up from 21. The new ones are 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-fixbehaviours — so the assertions on top of it can actually fail. The byte-comparisons that existed
before were written from the same mental model as the code and could only pin its output.
Not validated
RecordingTranscription. No unit test can reach it —begin()needs the speech engine, and this test target has none. Production could stop feeding the writer
entirely and the suite would stay green. The device pass is the only thing that covers it.
Full disposition of every Codex finding across the four passes is in
.agent/m1-09-vtt-assembly/SUMMARY.md.Filed alongside: Stash/stash-docs#3, to set up the decisions convention the build standard calls
for — there is nowhere to record a design decision in
stash-docstoday.