Archilyzer · Source

archilyzer

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

commit 702b87bc3d42c2d6ac75d903af6a1a3686436859
parent 0e350ef225ddec5ceede6314741ad5028fcd26ef
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 16:39:10 -0400

docs: a teaser's `beat`, its proportions, and the length a teaser without `seconds` gets

The README states the beat's range and default, what scales with it (the
second tier, 3/7 of a beat; the tail's wait, 8/7) and what does not, that a
`seconds` too short is refused rather than played faster, and the ferret
card's needs at four beats. The teaser's [Unreleased] bullet names `beat`.

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

Diffstat:
Meditor/CHANGELOG.md | 2+-
Mumtool/report-to-video/README.md | 29+++++++++++++++++++++++------
Mumtool/report-to-video/chrome-teaser.test.mjs | 4++--
3 files changed, 26 insertions(+), 9 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -6,7 +6,7 @@ - **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 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. +- **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. `"beat"` (0.4–2.5 seconds, default 0.7) sets the time from one pop — and its hit — to the next, the second tier and the tail's wait slowing with it; `seconds` may be left out for exactly the length the beats need, and a `seconds` too short for them is refused with that length rather than played faster. 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. - **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. - **The site build image runs Node 22 and pnpm 11**, the versions the rest of the workspace runs on, instead of Node 20 and pnpm 9, which did not read the workspace's install rules. The next Build all rebuilds the image from its first step, reinstalling every dependency, before it builds any site. diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md @@ -438,12 +438,14 @@ falls on the teaser. "break": "in the United States" }, "February 2027"], "tail": "?", // optional: appended to the LAST line, fades in on its own + "beat": 0.7, // optional, default 0.7: seconds from one pop to the next "hits": true } // optional, default true: false makes the card silent ``` - **`lines`** — 1 to 5, each one line of at most 80 characters: a string, or `{ text, break }`, where `break` is the END of `text` drawn as a smaller, - wide-tracked second tier under the rest that pops a beat (0.3 s) after it. + wide-tracked second tier under the rest that pops 3/7 of a beat after it + (0.3 s at the default `beat`). The words are data: they are drawn uppercase, and kept as written everywhere else. **Roles follow position:** with three or more lines the first is a small wide-tracked overline between two accent rules, the last a mid-size @@ -458,10 +460,25 @@ falls on the teaser. as a `break`. The counts were measured in the face on ordinary words in capitals (a title holds 36–37 there); a row of only wide capitals (M, W) can still spill. -- **`seconds`** — 3 to 20. The pops land 0.55 s in (after the incoming - dissolve) and 0.7 s apart; the tail starts 0.8 s after the last and fades in - over 1.7 s; the last 1.2 s are a still hold for the end fade. A card too - short for all of it plays every beat proportionally faster. +- **`beat`** — 0.4 to 2.5 seconds, default 0.7 (`TEASER_MOTION.gap`): the + time from one line's pop to the next, and so from one hit to the next. The + pops land 0.55 s in (after the incoming dissolve) and a beat apart, counted + from the line before or from its second tier. Two waits scale with the beat + in the default's proportion, so a slower beat is the same rhythm slowed: a + second tier pops 3/7 of a beat after its line (0.3 s at 0.7) and the tail + starts 8/7 of a beat after the last pop (0.8 s). What is not the beat stays + put: the first landing, each slam (0.2 s to its hit, 0.5 s to settle), the + tail's 1.7 s fade and the swell under it, and the last 1.2 s, a still hold + for the end fade. A teaser without `beat`, or with 0.7, is the page it + always was — the same render key and the same frames. +- **`seconds`** — optional, 3 to 20. **Nothing is squeezed to fit**: a + `seconds` shorter than the beats need (the last pop, the tail's fade and the + 1.2 s hold) is refused with the length they need. Left out, the card is + exactly that long, rounded up to a tenth of a second and at least 3 s; a + card that would need more than 20 s is refused (a shorter beat, or fewer + lines). The ferret card needs 5.95 s at 0.7, 6.7 at 0.9, 7.2 at 1.05 and + 8.1 at 1.3 — its `"seconds": 7` (a little over a second more still at the + end) holds beats up to about 0.99. - **`tail`** — at most 8 characters, in the accent, set a little apart from the last line. - **The chapter** is the lines joined with " — ", the tail after the last @@ -509,7 +526,7 @@ encoded from the frames on disk. **umtool** shows a teaser as a card row named by its lines (the report page, the On-screen table, the timeline strip). Editing its lines there is not -built: it needs a writer (`updateTeaser(dir, id, {lines, tail, seconds, hits}, +built: it needs a writer (`updateTeaser(dir, id, {lines, tail, seconds, beat, hits}, {token})` through `withManifestLock` and `validateTeaser`), a route, a small form (one field per line with a break picker), and a preview — a still of the composition at a chosen second through compose-chrome's `--still`, which the diff --git a/umtool/report-to-video/chrome-teaser.test.mjs b/umtool/report-to-video/chrome-teaser.test.mjs @@ -125,7 +125,7 @@ test("the cues: one per pop at the shared times, top to bottom, every from state const lines = teaserLines(FERRET); const { cues, init, beats } = teaserCues({ lines, tail: "?", seconds: 7 }); const m = TEASER_MOTION; - // The ferret card needs no compression: the times are the motion's own. + // The times are the motion's own: nothing is ever compressed. assert.deepEqual(beats.lines.map((b) => b.at), [m.first, m.first + m.gap, m.first + m.gap + m.sub + m.gap]); assert.equal(beats.lines[1].subAt, m.first + m.gap + m.sub); // About 0.6–0.8 s apart, in order. @@ -168,7 +168,7 @@ test("nothing is squeezed: a card too short for its beats is refused with the le assert.match(validateTeaser({ ...FIVE, seconds: 3 }).join(" | "), /seconds is 3, and at a beat of 0\.7s its lines need 7\.4s \(the last pop, the tail's fade and 1\.2s still for the end fade\) -- set it to 7\.4 or more, or leave it out for exactly that/); assert.deepEqual(validateTeaser({ ...FIVE, seconds: 7.35 }), []); - // The ferret's 7 s holds its beats up to about 0.98 s; past that it is refused, not compressed. + // The ferret's 7 s holds its beats up to about 0.99 s; past that it is refused, not compressed. assert.deepEqual(validateTeaser({ ...FERRET, beat: 0.9 }), []); assert.match(validateTeaser({ ...FERRET, beat: 1.05 }).join(), /seconds is 7, and at a beat of 1\.05s its lines need 7\.2s/); // Without `seconds`, a card needing more than a teaser may run says so.