Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit fdac3db9daba2cb0611d21b2d4ada3ca952f935d
parent af03e1a48f86f2e477d24efb9bc1c46d373bb5af
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 18:18:49 -0400

docs: the posts feed -- README, quirks, the [Unreleased] entry and "Feed F1, as built"

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/deck-posts.md | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mumtool/docs/quirks.md | 33+++++++++++++++++++++++++++++++++
Mumtool/report-to-video/README.md | 63++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 186 insertions(+), 1 deletion(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -4,6 +4,7 @@ - **umtool's report videos keep every clip's sound on its picture.** In a crossfaded cut each clip's audio was placed by the audio's own length and its picture by the picture's, and an encoded clip's audio is routinely a few to twenty milliseconds shorter or longer than its video, so the sound drifted further ahead clip by clip: by the end of a seventeen-clip cut it was a third of a second early, and two seconds on one with title and sources cards. Each clip's sound is now padded or trimmed to exactly its picture's length before the crossfade. Every crossfaded report video changes when it is rebuilt, and is in sync; a hard-cut video was not affected. - **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. The deck changes nothing, byte for byte, in a cut whose manifest has no `render.chrome`. - **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. `posts` changes nothing in a cut that has none, and without the deck it is not drawn at all. +- **A report cut's posts can be a feed: a column beside the footage for the whole cut, each post ticking in as its clip starts, with no pause.** `render.chrome.deck.posts.layout: "feed"` (the default, `"popup"`, is the cards described above) puts every post in one column on the right, standing on the deck so the two read as one L-shaped panel around the picture. The footage of every clip and still is framed, for the whole cut, into the box left beside the column (1272×716 at 1920×1080 with the default 600 px column, against 1574×886 under the deck alone); nothing moves and nothing is held, so the cut is as long as its clips. Before the first post the column shows its header — the platform and the handle, or "Posts" when there are several authors — and an empty state. Each post ticks in at the start of the clip it belongs with, just after the crossfade into it, a clip's next ones `posts.seconds` apart (closer on a short clip): it lands at the top with an accent flare and keeps a lit rail while it is the newest, and the posts already up slide down to make room; when the column is full the oldest fade out at the bottom. Each card shows the post's date, its words up to `maxLines`, and its QR. Over a card or the teaser the column slides out of the frame with the deck and comes back after. The column is drawn by one composition for the whole cut, cached like the deck's. Switching the layout reframes every segment, so it takes a normal build (with `--skip-fetch` it re-cuts from the cached windows); `--chrome-only` over segments framed for the other layout is refused with a sentence naming them, because each segment's `<id>.cut.json` now records the box it was framed into. In umtool, the posts settings have a **layout** switch, and the live preview shows the column for the whole scrub with the footage in its box. A cut in the popup layout, or without posts, builds exactly as before. - **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, whatever it is — a clip with its hold, a closing card or a teaser — to the background colour and to silence over its final seconds, all of it when the segment is shorter, and the deck stays drawn over it. 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. Neither key changes a cut that does not set it. - **umtool's clip bench stops exactly where a range ends, and sets a clip's mute mark.** The bench's **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. A line too long to fit the frame at its smallest size is refused with a sentence saying how many characters fit (a title holds 34). 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. diff --git a/plans/deck-posts.md b/plans/deck-posts.md @@ -358,3 +358,93 @@ Found and left: - `onscreen-posts.spec.ts`'s timings at 15 fps (above) — resolved: 3c70c6d3 on the main line made the posts and mute specs follow whole-frame holds and the build's mute fade, and the merge carries it. + +## Feed F1, as built + +Branch `deck/feed-f1` from 07d1fa08. The operator's ask: the posts as a persistent, ticking feed +in a column on the right for the whole cut — with the deck, an L-shaped interface — and no +pausing. `render.chrome.deck.posts.layout: "popup" | "feed"`, default `"popup"` (everything +above, unchanged). + +| Commit | What | +|---|---| +| 17c5d446 | `deck.mjs`: `posts.layout`, `POST_LAYOUTS`, `feedGeometry`, `feedOn`, the feed's `in` in `postSchedule`, `roundPosts`; no hold, move or windows under the feed; the schedule's `layout: "feed"`. `chrome-feed.mjs` (the page, `feedCues`, `feedLayout`, `feedWho`, `FEED_MOTION`). compose-chrome region `feed`. build-video: feed framing (`deckFraming(render, {feed})`, `segmentFraming`, `framedUnderDeck`), each framed segment's `cut.json` records `framing`, `framingProblems` refuses `--chrome-only`/`--chrome-preview` over another layout's segments, `feedRegion` laid like the deck. verify-build's feed check. `chrome-feed.test.mjs` | +| 220a84f5 | umtool: the `layout` switch; the preview route composes `chrome/feed-preview` and returns `feed: {src, geometry, footage, boxes}` (`composeFeedPreview`, `segmentBoxes`, `feedPreviewDir/Src`, the files route's `feed-preview/`); `previewSchedule` follows the layout; `FeedOverlay`; the backdrop carried into the feed's box; the posts table's jump uses `in`; e2e (the posts fixture in feed, and a built `onscreen-feed-fixture`) | +| 55ac8ab4 | The room opens before a post comes in over it (push 0.4 s front-loaded, entrance 0.22 s after `in`): the first ferret render's +0.3 s still showed the new card over the ones it was pushing | +| 211f0d87 | `framingProblems` builds its sentence without a template nested in a template's `${}`: the build-trace check (`scripts/next-build-trace.test.mjs`) read the rest of build-video as one call reaching the CLI guard's `import.meta.url` (69 false findings; quirk recorded) | +| (this) | README, quirks, one `[Unreleased]` bullet, this section | + +Rulings as built: + +- **Geometry** (`feedGeometry`, numbers at 1920×1080 with the defaults): the column is + `posts.width` 600 wide, flush with the right edge and the top, down to the deck — 600×890 at + (1320, 0). The footage is as large as fits in the 1320×890 left of it with `posts.inset` 24 + clear on every side, the frame's aspect, centred: 1272×716 at (24, 87) — 66 % of the frame's + width (the deck alone: 1574×886, 82 %). Gap footage → column 24 px. `top-left` mirrors it. A + feed whose footage would be under half the frame's width is refused. Inside the column: 22 px + side padding, a 66 px header from y 26 (platform pill, the handle at 26 px or "Posts" over + several authors, an "n of N" count, "posts as the timeline reaches them"), a rule at y 102, + the stack from y 120 to 22 px off the bottom (748 px). Cards are the popup's design sized for + the column: 556 wide, 6 px rail, 24 px words on 33 px lines (≈ 30 characters a line), + `maxLines`, the date at 18 px (handle and platform only with several authors), the + `qrSize` 120 QR in a 148 px cell. +- **Timing:** post j of k on a clip ticks in at start + D + step·j, step = min(`seconds`, + (A − start − D)/k), A the clip's outgoing transition. The schedule carries `in` per post and + `layout: "feed"`, only when there are posts to draw. +- **Motion** (`FEED_MOTION`): at `in` the cards already in move down by the new card's height + plus 16 px over 0.4 s (power3.out); the new card enters from the column's outer edge 0.22 s + later over 0.6 s (expo.out); its rim and lit rail flare to 1 and settle to 0.55 while it is the + newest, and go out over 0.8 s when the next one arrives. A card pushed past the stack's bottom + fades as it moves (0.45 s) and is gone: a card is either whole in the column or out of it. + The empty state fades with the first post. Over a hidden-deck segment the column slides + 624 px out of the frame's right edge on the deck's own times (`deckChoreography`'s visibility). +- **Framing is the build's, per segment, and recorded:** a feed cut frames clips, stills and + shown cards into the feed's box; `framing: {layout, box}` in each framed segment's + `<id>.cut.json`. `--chrome-only` over segments whose box is not the cut's is refused, naming + each; a record-less segment counts as the deck's. Switching layouts is a normal build with + `--skip-fetch`. +- **The feed is one region for the whole cut**, `chrome/feed[-frames]` (`feed-preview`, + `feed-from<s>`), cached like the deck's, laid like the deck's (`-reinit_filter 0`, + `format=rgba`, `shortest=1`) after it; the build checks it is as many frames as the deck's. +- **umtool:** the preview's backdrop is a built segment carried from the box its record names + into the feed's (or the neutral frame drawn in the feed's box); the feed's iframe is up for + the whole scrub. + +Gates: + +- Unit: `chrome-feed.test.mjs` 16 (geometry, validation, `feedOn`, the feed schedule's tick-in + times, popup/no-posts schedule identity, `feedCues` stagger/push/overflow/highlight/visibility + and seek-safety, the page's nodes, escaping, cue times and no remote URL, the window, framing + and `framingProblems`, the overlay chain, compose-chrome's feed region through a stub), and 6 + in `onscreen.test.mjs`/`serve.test.mjs`. +- Identity, against 07d1fa08's modules over the ferret manifest: the popup, the hard-cut popup, + no posts, and a feed with no posts give byte-equal schedules, xfade graphs, overlay chains and + framing filters. `--only c07 --skip-fetch` without `render.chrome`: md5 + `6a92235fa12ca181bb81993129c9ee9d`. +- Ferret, scratch copy with `posts.layout: "feed"`, a FULL `--skip-fetch` build (segments + reframed) from cached windows: 356.7 s (no holds; the popup cut was 366.7 s), video 356.700 s + and audio 356.700 s; verify-build ok — deck 10,701/10,701, feed 10,701/10,701, 7 posts, no + hold, teaser 210/210; 18 chapters. Posts in at 25.7 / 29.7 (c03), 124.6 / 128.367 / 132.133 + (c06, 3.77 s apart: c06 is short), 219.333 (c12), 285.533 (c17). QR 7/7 posts (each read off + its settled frame) and 17/17 deck. The column overflows at c06's posts (the c03 cards fade out + at the bottom). Build 52.5 min wall clock under a load average of ~25: the deck rendered in + 309 s, the feed in 487 s (1.2 GB of frames), the teaser in 159 s. +- Repo gates on 211f0d87: workspace tsc clean (69 s); `test:scripts` 389 pass, 2 skipped (391); + capped umtool `next build` with the corpus linked exit 0 (41 s; 50 s on 220a84f5), link + removed. umtool e2e `onscreen-posts onscreen clip-bench build projects`: 96 passed (4.9 min, + after 27 min in the queue). A first run on 220a84f5 while the ferret build encoded (load ~25) + gave 87 passed, 9 failed — clip-bench and projects timeouts, none in the onscreen specs, both + feed tests green — and passed whole once the encode was done. + +Found and left: + +- **The popup page's clamp regex reaches the page as `/s+$/`** (the template literal eats the + backslash; quirk recorded). It runs only for a clamped card whose later paragraphs were dropped + after one that fit exactly, and strips trailing letters "s" rather than spaces. Not fixed here: + it would change the popup page and its cached windows; the feed's page writes `\\s`. +- A card is in the feed whole or not at all, so an overflowing column can show a gap at its + bottom (on ferret at 133 s: 216 px free under two cards, the third needing 288). +- The umtool preview shows the feed's composition and frames the backdrop in the feed's box; it + does not show the segments reframed (that is the build's), so a backdrop built for the deck is + carried, scaled, into the feed's box until a build reframes it. + diff --git a/umtool/docs/quirks.md b/umtool/docs/quirks.md @@ -244,6 +244,39 @@ first and filling it later an error (`gsap_timeline_registered_before_async_buil The renderer awaits `document.fonts.ready` before its first seek, so every frame sees the built timeline. +**A backslash in a page's script is a template literal's first.** The page +modules write their runtime script inside a JS template literal, where `\s` +is not an escape and becomes a plain `s`: the popup's `replace(/\s+$/, "")` +reaches the page as `replace(/s+$/, "")` (it strips trailing letters s, not +spaces, in the one case it runs: a clamped card whose paragraphs were dropped +after one that fit exactly). The feed's page writes `\\s`. Read in the +module the regex looks right; only the composed page shows the wrong one. + +**Switching the posts layout moves the footage, so it is a rebuild, not a +re-render.** The feed frames every footage segment into its own box when the +segment is built; the popup frames into the deck's. `--chrome-only` lays +chrome over the segments on disk, so over segments framed for the other +layout it would draw the column over footage (or leave a gap where it +expected some). Each framed segment's `<id>.cut.json` records `framing: +{ layout, box }`, and `--chrome-only` / `--chrome-preview` refuse, by name, +any segment whose box is not the cut's; a record-less segment was built for +the deck's box. A normal build with `--skip-fetch` reframes from the cached +windows. + +**The build-trace check reads a template literal nested in another's `${}` as +the end of the string.** `scripts/next-build-trace.test.mjs` scans each module +for path and fs calls on `import.meta.url`-derived values. One +`` `${a} (${ok ? `x ${b}` : `y`})` `` in build-video threw its string tracking +out of step, and it then read the rest of the file as one call reaching the +`import.meta.url` of the CLI guard: 69 false findings. Hoist the inner +template to a const; nothing at run time changes. + +**A whole-cut overlay is laid like the deck's, whatever it draws.** The feed's +column is a second full-length sequence; it takes the deck's `-reinit_filter +0`, `format=rgba` and `shortest=1` (its frames mix RGB and RGBA like every +HyperFrames sequence), not the posts windows' `-itsoffset` and +`eof_action=pass`, and it must be exactly as many frames as the deck's. + **`perspective` has no `t`, and its `in` counts from 1.** The footage move for posts animates one `perspective` filter (`eval=frame`), whose expressions see only `W`, `H`, `in` and `on`. Measured: the first frame has `in = 1`, and diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md @@ -239,7 +239,8 @@ whatever a manifest omits, one level deep: "qr": { "show": true, "size": 150 }, // size 80–380, and at most height − 20 "overCards": "hide", // "hide" | "show" "motion": { "out": 0.3, "in": 0.45, "pip": 0.7 }, // seconds; out/in 0–2, pip 0–3 - "posts": { "show": true, "seconds": 4, "hold": 2.5, // the manifest's `posts` (below); seconds 0.5–10; hold 0–10 (0: none) + "posts": { "show": true, "layout": "popup", // | "feed": a column beside the footage for the whole cut (below) + "seconds": 4, "hold": 2.5, // the manifest's `posts` (below); seconds 0.5–10; hold 0–10 (0: none; the feed never holds) "shift": { "scale": 0.86, "seconds": 0.6 }, // the footage makes room: scale 0.5–1, seconds 0–3; or false "position": "top-right", // | "top-left" "width": 600, "qrSize": 120, "maxLines": 7, "inset": 24 } // width 320–900 px; qrSize 80–200 and ≤ width/2; maxLines 2–14; inset 0–80 @@ -1173,6 +1174,66 @@ node umtool/report-to-video/compose-chrome.mjs <manifest.json> --region posts -- under three frames of still picture there (0.5 s under a 0.5 s crossfade) is reported as not checked. +### The posts feed (`posts.layout: "feed"`) + +The other way to draw the posts: a column for the WHOLE cut beside the +footage, instead of cards over the end of each clip. The deck stays full +width at the bottom; the column stands on it from the frame's top, so the two +read as one L-shaped surface around the picture. Nothing pauses and nothing +moves: no hold, no footage move, and the cut is as long as its segments. + +- **Geometry** (`feedGeometry`): the column is `posts.width` (600) wide, flush + with the `position` side's edge and the frame's top, down to the deck's top + edge — 600×890 at (1320, 0) at 1920×1080. The footage keeps the frame's + aspect and is as large as fits beside it with `posts.inset` (24) clear on + every side, centred: 1272×716 at (24, 87), 66 % of the frame's width against + the deck alone's 82 %. A feed whose footage would be under half the frame's + width is refused (`postsFitErrors`). +- **Framing.** Every footage segment of a feed cut — clips, stills, cards with + `overCards: "show"` — is framed into that box when it is built + (`deckFraming(render, { feed: true })`), for the whole cut. A cut is a feed + (`feedOn`) when the deck is on, `posts.show`, `layout: "feed"`, and at least + one post is drawn on a clip of the cut's whole timeline (an `--only` rebuild + frames as the full build would). Each framed segment's `<id>.cut.json` + records `framing: { layout, box }`; a still's and a card's record holds only + that. +- **Switching the layout reframes every segment**, so it takes a normal build + (with `--skip-fetch` it re-cuts from the cached windows). `--chrome-only` and + `--chrome-preview` read the records first and refuse segments framed for the + other layout, naming each one (`framingProblems`); a segment with no + framing record was built for the deck's box. +- **When** (`postSchedule`): a post has one time, `in` — its clip's start + after the incoming dissolve (start + D); a clip's next posts follow + `seconds` apart, or closer on a clip too short for that, so the last is in at + least that long before the clip leaves. Once in, a post stays. The schedule + says `layout: "feed"` and carries each post's `in` — only when there are + posts to draw, so a popup schedule and one without posts are byte-identical + to what they were. `postWindows` is empty for a feed. +- **The column** (`chrome-feed.mjs`, one composition for the whole cut, + region-local at the column, transparent outside its panel): a header (the + platform and `@handle`, or "Posts" over several authors, a count, "posts as + the timeline reaches them"), then an empty state until the first post. Each + post TICKS IN at the top, newest first: the cards already in slide down by + its height over `push` while it enters from the column's outer edge, its + accent rim flares and settles to a lit rail, which goes out when the next + one arrives. A card pushed past the column's bottom fades out as it goes — + the oldest scroll out. Over a segment the deck hides for (a full-frame card, + the teaser) the column slides out of the frame's side with the deck and + back after (`deckChoreography`'s visibility). Cards are the popup's design + sized for the column: 24 px words on 33 px lines, `maxLines`, the post's + date (the handle and platform too when there is more than one author), a + `qrSize` QR in its own cell. The plan (`feedCues`) runs in the page on the + measured card heights, embedded by `embedFn`, like the popup's. +- **Files:** `compose-chrome.mjs <manifest> --region feed` — project + `chrome/feed/`, frames `chrome/feed-frames/` and their `.key`, a window + `chrome/feed-from<s>[-frames]`, the preview `chrome/feed-preview/` (never + rendered); cached exactly as the deck's. The build renders it after the + deck, checks it is as many frames as the deck's, and lays it the deck's way + (`-reinit_filter 0`, `format=rgba`, `shortest=1`) in the crossfade concat, + the hard-cut `applyChrome` pass and `--chrome-preview`. +- **`verify-build`** checks `chrome/feed-frames` holds the whole cut's frames + and that the schedule holds and moves nothing. + ### The hold and the move, where the cut is joined `segmentJoins(schedule)` turns the schedule's holds and `moves` into one join