M2-03: Seek from the transcript #44

Merged
wk merged 1 commit from m2-03-transcript-seek into main 2026-08-09 16:09:36 -04:00
Member

Closes #30.

Tapping a cue plays the memo from those words. The cue under the playhead is marked while it
plays, and the list follows it until somebody scrolls away — a button above the transport puts
following back, and so does tapping a cue.

The decisions worth reviewing

The cue path waits for its seek to land before it plays. AVPlayer.seek is asynchronous, so
playing on the next line starts the audio at the position the memo was already at. For a feature
whose only job is to start the words somebody tapped, that is the bug and not an edge case. The
scrubber does not wait, because there the finger has already put the playhead where it belongs.

A tap that arrives before the file is ready is kept. open() sets the base before it awaits
the header, so a cue tapped while the screen is still opening arrives with no item to seek.
PendingSeek carries the memo and the item it was asked for, and the status observer runs it when
the file comes good — dropping it is a tap that does nothing on exactly the memos that take
longest to open.

Playback still yields to capture, at every suspension. The gate is asked before anything moves
(open() tears down the loaded memo before it awaits, so refusing afterwards would throw away a
playhead), again at the commit, and a capture invalidates any request that was in flight across
it. That last one is a security finding: a recording that starts and fails quickly would otherwise
leave the gate clear again by the time a suspended tap resumed, and play a private memo out loud.

CueTimeline sorts for the lookup and never for the display. The reader keeps a foreign file's
cues in file order on purpose; the timeline sorts a copy, off the main actor, and answers with the
last cue that started — its end is deliberately not consulted, so a silence does not blink the
marking off and stop the list following mid-memo.

Cue rows are Buttons, so cue text is no longer selectable. A button consumes the long press
that starts a selection. Both cannot have it, and playing from a tapped cue is what the timestamps
were built for.

Validated

On the device: tapping a cue plays from those words, and on a one-hour memo the audio landed
within about half a second of the words when seeking from cues near the end. That closes the
alignment question open since #8 and #9 — nothing was systematically off, so there is no follow-up
issue against transcription.

333 unit tests pass, lint clean. New: CueTimelineTests, PendingSeekTests.

Not validated

  • The two code-review bugs and the one security finding are fixed by inspection, with unit tests
    only on the pure rules underneath them. They are timing edge cases — a tap during a slow open, a
    capture that starts and fails inside a tap.
  • Nothing on this screen was exercised by tapping in a simulator. This machine cannot post
    clicks into it, the same Accessibility limitation recorded in M1-11. The device pass is the only
    end-to-end evidence.
  • Two windows. The scroll interruption and the Follow button were not re-tested after the review
    fixes, which did not touch them.

A decision file is written for stash-docs; it is not in this PR because it is a different
repository. See the note on the issue.

Closes #30. Tapping a cue plays the memo from those words. The cue under the playhead is marked while it plays, and the list follows it until somebody scrolls away — a button above the transport puts following back, and so does tapping a cue. ## The decisions worth reviewing **The cue path waits for its seek to land before it plays.** `AVPlayer.seek` is asynchronous, so playing on the next line starts the audio at the position the memo was already at. For a feature whose only job is to start the words somebody tapped, that is the bug and not an edge case. The scrubber does not wait, because there the finger has already put the playhead where it belongs. **A tap that arrives before the file is ready is kept.** `open()` sets the base before it awaits the header, so a cue tapped while the screen is still opening arrives with no item to seek. `PendingSeek` carries the memo and the item it was asked for, and the status observer runs it when the file comes good — dropping it is a tap that does nothing on exactly the memos that take longest to open. **Playback still yields to capture, at every suspension.** The gate is asked before anything moves (`open()` tears down the loaded memo before it awaits, so refusing afterwards would throw away a playhead), again at the commit, and a capture invalidates any request that was in flight across it. That last one is a security finding: a recording that starts and fails quickly would otherwise leave the gate clear again by the time a suspended tap resumed, and play a private memo out loud. **`CueTimeline` sorts for the lookup and never for the display.** The reader keeps a foreign file's cues in file order on purpose; the timeline sorts a copy, off the main actor, and answers with the last cue that *started* — its end is deliberately not consulted, so a silence does not blink the marking off and stop the list following mid-memo. **Cue rows are `Button`s, so cue text is no longer selectable.** A button consumes the long press that starts a selection. Both cannot have it, and playing from a tapped cue is what the timestamps were built for. ## Validated On the device: tapping a cue plays from those words, and on a **one-hour** memo the audio landed within about half a second of the words when seeking from cues near the end. That closes the alignment question open since #8 and #9 — nothing was systematically off, so there is no follow-up issue against transcription. 333 unit tests pass, lint clean. New: `CueTimelineTests`, `PendingSeekTests`. ## Not validated - The two code-review bugs and the one security finding are fixed by inspection, with unit tests only on the pure rules underneath them. They are timing edge cases — a tap during a slow open, a capture that starts and fails inside a tap. - **Nothing on this screen was exercised by tapping in a simulator.** This machine cannot post clicks into it, the same Accessibility limitation recorded in M1-11. The device pass is the only end-to-end evidence. - Two windows. The scroll interruption and the Follow button were not re-tested after the review fixes, which did not touch them. A decision file is written for `stash-docs`; it is not in this PR because it is a different repository. See the note on the issue.
Tapping a cue plays the memo from those words. The cue under the playhead is
marked while it plays, and the list follows it until somebody scrolls away.

The cue path waits for its seek to land before it plays: AVPlayer.seek is
asynchronous, and playing on the next line starts the audio at the position the
memo was already at — which for this feature is not an edge case. A tap that
arrives before the file is ready is kept and applied when it becomes ready,
scoped to the memo and the item it was asked for. The capture gate is asked
before anything moves and again at the commit, and a capture invalidates any
request that was in flight across it.

Cue rows are buttons now, so their text is no longer selectable. Both cannot
have the long press, and playing from a tapped cue is what the timestamps were
built for.

Closes #30

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wk merged commit 8874b6ab73 into main 2026-08-09 16:09:36 -04:00
wk deleted branch m2-03-transcript-seek 2026-08-09 16:09:36 -04:00
Sign in to join this conversation.
No reviewers
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!44
No description provided.