commit e38c05d1c69c70698f333f029cacd8f02cd5412f
parent b10db121fa0e28489fc8472b40c3ff94a7cc366e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 16:12:28 -0400
plans: the clip bench's exact playback and mute mark (slice B1), as built; one [Unreleased] bullet
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 29 insertions(+), 0 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -5,6 +5,7 @@
- **umtool's report videos can wear an on-screen deck: one panel under the footage for the whole cut, with a pip timeline, a title per clip, its source and date, and its QR.** A report manifest whose `render` says `"chrome": { "engine": "hyperframes", "layout": "deck" }` scales the footage into a box above a 190 px panel (both sizes are settings) and draws, over the whole cut, one unlabelled pip per clip on a track that fills as the cut plays, the clip's own title from `onscreen.title`, a subtitle naming the recording and its date (the channel too when the cut spans more than one; `onscreen.subtitle` replaces it), and the clip's QR. At each clip change the marker travels to the next pip and the title, subtitle and QR hand over; over a card the panel slides away and comes back after. The citation header, the corner QR and the section footer are not drawn on such a cut, and chapters take the clip's on-screen title. Every setting (sizes, spacing, date format, what the subtitle names, whether cards keep the panel, the motion's timings) is in `render.chrome.deck` and checked when it is saved; an unknown or out-of-range one is refused with a sentence saying why. The panel is rendered once per cut by HyperFrames (pinned to 0.8.24; `HYPERFRAMES_PKG` or `HYPERFRAMES_BIN` override it) and reused until its text or settings change. `build-video.mjs --chrome-only` redraws it over the built segments without rebuilding or fetching anything, `--no-chrome` builds the framed cut without it, and `--chrome-preview <at> <dur>` renders a short window. In umtool, the report page has an **On-screen** section — a switch, the settings, a table of every entry's title and subtitle with the automatic subtitle as its placeholder and a character counter, a live preview with a scrubber, a true still, **Re-render on-screen** and the built video — and the clip bench has on-screen title and subtitle fields with the panel previewed over the clip. A manifest without `render.chrome` builds exactly as before, byte for byte.
- **A report cut that wears the on-screen deck can show posts — Bluesky or X statements — as cards over the footage.** A report manifest's `posts` list (each with its platform, handle, date, words and link) is drawn near the end of the clip each post belongs with: the clip whose recording most closely precedes it by date, unless the post names one with `attachTo`; `hide` leaves one out. A clip's posts appear four seconds apart and stack down a column at the frame's top right; as the first appears, the footage eases aside (to 86 % of its box, at the far side) to make room, and the clip's last frame is held, in silence, for 2.5 seconds so the last post can be read; then they all leave together in the change to the next clip, which comes in at the normal size. When the column is full the oldest slide up and out. Each card slides in from the edge of the frame and flares in the deck's accent as it lands; it has an accent rail down its edge and shows the post's date, a platform label ("Bluesky" or "X") beside `@handle`, its words in paragraphs up to seven lines with an ellipsis, and a QR of the post's link, in the deck's colours and faces. The hold and the move are made where the cut is joined, not in a clip, so `--chrome-only` changes them without rebuilding one; chapters and the deck's timing count the hold. The timing, the hold (`hold`, 0 turns it off), the move (`shift`: its scale and seconds, or `false`), the column's side, width and inset, the QR size and the line limit are settings under `render.chrome.deck.posts`, and a bad post or setting is refused with a sentence before a build fetches anything. Only the seconds the cards are up are rendered, one short sequence per clip, cached like the deck; `--chrome-only`, `--chrome-preview` and a hard-cut cut lay them as they lay the deck, and `--no-chrome` draws neither — though it still holds and moves the footage, which are part of the cut rather than the chrome. A first post that appears inside the hold still moves the footage, and a hold is a whole number of frames. A cut without `posts` builds exactly as before, and without the deck `posts` is not drawn at all.
- **A report clip can go silent partway through, a report cut can fade out at its end, and the deck's QR names its site in larger type.** A clip's `muteFrom` (in the recording's own seconds, inside the clip) silences it from that second to its end while the picture plays on, after a 40 ms fade that ends there, so nothing clicks and no next word leaks in; a hold on that clip stays silent. `render.endFade` (seconds; 0, the default, is off) fades the cut's last segment to the background colour and to silence over its final seconds, the hold included, and the deck stays drawn over it; once a closing card follows the last clip, the ordinary crossfade into it does that job instead. Both are applied where the cut is joined, so `--chrome-only` changes them without rebuilding a clip, and a value out of range is refused with a sentence before a build fetches anything. Each clip build now writes `<id>.cut.json` beside its segment, saying where in the recording the segment really starts after its cut was snapped to a silence; `muteFrom` is measured from it, and a segment built before this measures from the clip's unsnapped start and says so. The site's name beside the deck's QR is now exactly as long as the code is tall, for any site. A cut without `muteFrom` or `endFade` builds exactly as before.
+- **umtool's clip bench stops exactly where a range ends, and sets a clip's mute mark.** **play selection**, the edge auditions, the auto-audition and a click on a transcript line now play the window's sound through the browser's Web Audio, from a decode made on the server by ffmpeg — the same timeline the build cuts on — and each stops on the audio clock where its range ends, at every speed. They used to play on the video element and were stopped when it next reported its time, which overran the end by up to a quarter of a second, by a different amount each time. The picture follows, muted. If the sound cannot be decoded, the video element plays as before and the bench says the playback is approximate and why. The mute mark sets the clip's `muteFrom`: `m` puts it at the playhead, **pick on waveform** puts it where you click, `;` and `'` nudge it (with shift, by half a second), and `M` or **clear mute** removes it. It is saved with the window like the edges, every playback goes silent at it with the build's own 40 ms fade, and a window save that would leave it outside the clip is refused unless the same save moves or clears it. The decoded sound is served by a new `GET /api/report/audio`, at most 120 seconds of a cached window at a time, as WAV.
- **A report cut can end on a teaser card: a few lines popping in over a dark cinematic ground, with a trailer hit under each.** A report manifest's `teaser` entry (`lines`, `seconds`, an optional `tail`) is a full-frame card drawn from its own words, one to five lines each popping in top to bottom with a scale overshoot, a blur that sharpens, and a flash of the accent; with three or more lines the first is a small overline, the last a mid-size date, and the ones between a big title. A line written as `{ "text": …, "break": … }` draws its ending as a smaller second tier a beat later, and the tail fades in after the last line on its own. Under each pop is a synthesised boom, the title's the biggest, and under the tail a low swell; `"hits": false` makes the card silent. Put after the last clip, it joins with the ordinary crossfade and takes the cut's end fade. It is rendered once and re-rendered when its words change, `--chrome-only` included, and its chapter is its lines. umtool shows it as a card row named by its lines; its words are edited in the manifest.
- **A report cut with `transition: 0` builds when its output folder was given as a relative path.** The hard-cut concat listed its segments relative to the working directory, and ffmpeg reads that list relative to the list file's own folder, so every hard-cut build with a relative `--out` failed at the concat. The list now names each segment by its full path.
- **`archilyzer doctor` checks the image Build all builds sites in.** When a container engine answers, a new **build image** section says whether the image named under **Settings → Build pipeline** is there, when it was built and how big it is. It warns when the image is missing, or older than the last change to its Dockerfile, and prints the one command that rebuilds it. Build all still builds or refreshes the image itself before it builds any site; the warning tells you ahead of time that the next Build all will spend that time. With no container engine the check is skipped in one line, and with no corpus it is only a note. It never fails the doctor.
diff --git a/plans/deck-posts.md b/plans/deck-posts.md
@@ -268,6 +268,34 @@ Found and left:
length in the cut (`apad=whole_dur`, `atrim=end`) before the join, so a `muteFrom` lands on its
picture too. Every crossfaded cut's graph changed; `av-sync.test.mjs` holds it with real ffmpeg.
+## Bench B1, as built
+
+Branch `deck/bench-b1`, merged as f619d659. umtool's clip bench plays every bounded range exactly,
+and sets the clip's `muteFrom` that B2 builds.
+
+| Commit | What |
+|---|---|
+| 5d7926f4 | `updateClip` takes `muteFrom` (source seconds inside the clip after the patch, rounded like an edge; null or empty deletes it), and a window save that would leave the mark outside the new extent is refused unless the same patch moves or clears it. `PUT /api/report/window` whitelists it and `/api/report/clip` returns it. `GET /api/report/audio` serves a cached window's sound, picked by `/api/report/raw`'s membership rule, decoded by ffmpeg to 16-bit PCM WAV over an absolute span of at most 120 s. `lib/report/playback.mjs` is the arithmetic both sides use (decode span, buffer schedule, playhead, mute ramp, the element fallback's stop test, the muteFrom rule, the WAV header), unit-tested |
+| bf927658 | The bench: **play selection**, the edge auditions, the auto-audition and a transcript line play from the decoded window with an `AudioBufferSourceNode`, started at an exact buffer offset and stopped at a context time (exact at every speed); the picture follows muted and the playhead reads the audio clock. One decode per window (the whole window up to 120 s, else the selection plus 30 s each side). When the decode fails, the element plays, stopped per animation frame by remaining time, and the bench says the playback is approximate and why. The mute mark: `m` at the playhead, **pick on waveform**, `;`/`'` to nudge (shift for 0.5 s), `M`/**clear mute**; drafted like the edges, saved by **save window** or a confirmation. Each playback is recorded as `data-play-*` attributes, and the specs check the stop against an AudioWorklet tap of the bench's output |
+| b9debbd7 | The audio-clock playhead paints at about 30 Hz; `m` reads it through a ref; the exact-stop spec checks the start against the schedule rather than the first non-zero frame |
+| e74c7a90 | The muted picture is re-seeked when it drifts more than 0.25 s from the audio clock; the element's own pause at the end of a playback no longer moves the playhead |
+
+After the merge, on the main line:
+
+- 3e3f251f: the bench's mute preview is the build's — one `MUTE_FADE` (0.04 s, from `deck.mjs`),
+ a ramp that ends at the mark. B1 had faded over 0.05 s.
+- 3c70c6d3: the posts and mute specs follow whole-frame holds and the build's mute fade.
+
+Gates (from the merge): the stop measured on the ferret c20 window, 0 of 20 trials off its
+schedule, where the element it replaces overran by about 230 ms; the specs check the stop to within
+one render quantum at 1× and 2×. umtool
+e2e `clip-bench onscreen onscreen-posts build projects report-fetch-via-editor` 97/97. Reviewed.
+
+Found after the merge, by the finale review, and fixed in the fix pass below: the build refused a
+`muteFrom` that `updateClip` accepted within its 0.02 s tolerance (LOW-1); `MUTE_FADE` pulled
+`deck.mjs` into the bench's client bundle (LOW-5); the audio route spawned a bare `ffmpeg` and did
+not stop the decode when the request was aborted (LOW-6).
+
## Teaser T1, as built
Branch `deck/teaser-t1` from 810e423e. A `teaser` timeline entry: a full-frame season-teaser card