Archilyzer · Source

archilyzer

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

commit cd08a5f7b217b47e2989a67ae057b9f46082a3f5
parent 1b45aae6c049a62ac9c3e0c3e2f2759be8010a5d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 15:43:36 -0400

docs: the teaser entry — README section, four quirks, one [Unreleased] bullet

README: the entry's fields, roles by position, how it is drawn and rendered,
its sound and level, when it is rebuilt (--chrome-only included), and what
umtool editing of its lines would need. quirks: the teaser page module behind
compose-chrome's dynamic import; random() in an ffmpeg expression advancing
only where evaluated; alimiter's auto-level and latency defaults; sub-bass
barely registering in LUFS.

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mumtool/docs/quirks.md | 30++++++++++++++++++++++++++++++
Mumtool/report-to-video/README.md | 84++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 114 insertions(+), 1 deletion(-)

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. +- **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. - **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/docs/quirks.md b/umtool/docs/quirks.md @@ -318,6 +318,36 @@ not a wall around the page modules: umtool's preview helper posts, and its routes build. A capped umtool build is the gate that catches it — the unit tests run in plain Node and pass either way. +**A teaser's page module is reached by a dynamic import from compose-chrome +too.** `chrome-teaser.mjs` carries the display face as `new URL(…, +import.meta.url)`, the same kind of asset URL as the deck's GSAP. compose-chrome +is imported statically by umtool's preview helper, so it loads the teaser page +only inside the `teaser` region's branch; nothing umtool bundles evaluates that +URL unless a teaser is composed. The face's file name has brackets +(`Archivo[wdth,wght].ttf`), so the copy in the project is +`assets/TeaserDisplay.ttf`, and the page never puts the vendored name in a URL. + +**`random()` in an ffmpeg expression advances only where it is evaluated.** +Its state is a variable that each call updates, and `if()` evaluates one +branch: a noise term gated to a hit's span draws its numbers only inside that +span, so adding or removing one hit changes the noise of every hit after it. +The teaser's noise is a hash of the sample number +(`fract(sin(n·12.9898+78.233)·43758.5453)`), the same whatever else is in the +graph, which is also what lets the onset test difference the graph with and +without one hit. + +**`alimiter` auto-levels and delays by default.** `level` is on by default and +normalises the output upward toward the limit, so a quiet card comes out loud. +`latency` is off by default, which leaves the output late by the lookahead +(the attack). The teaser's limiter is `level=0:latency=1`: measured, every +hit's onset is then on its pop's sample. + +**Sub-bass barely registers in LUFS.** K-weighting rolls off below ~100 Hz, so +a boom at 40 Hz that peaks at −6 dBFS measures far quieter than dialogue at the +same peak. The teaser's hits carry an octave for body and a band-passed noise +punch, and the limiter takes their transients, so the card can sit within +about 2 LU of the cut and still peak at −6 dBFS. + ## Rail strips and rolling counters **A slab that slides moves text that did not change.** The tally used to be four diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md @@ -174,7 +174,7 @@ the manifest names the MCP video id while the cue file lives under the URL slug. ## Manifest shape `timeline` is an ordered list; entries are `card`, `clip` or `image` (plus -`scroll`, `chart` and `ledger` — the vocabulary is open). +`scroll`, `chart`, `ledger` and `teaser` — the vocabulary is open). ```jsonc { "type": "card", "id": "ch3", "style": "chapter", "seconds": 4.0, @@ -420,6 +420,88 @@ cut off a top-level `ledger[]` — see [The claim rail](#the-claim-rail-renderra under `render.chrome`'s deck layout.** The deck replaces all of it with one persistent panel — see [The on-screen deck](#renderchrome--the-on-screen-deck). +### The `teaser` entry type + +A season teaser's "coming soon" card: a full-frame graphic, its words the +manifest's, popping in one line at a time over a dark cinematic ground, with a +trailer hit under each pop. Put it after the last clip; the ordinary crossfade +joins them, and `render.endFade` — which fades the cut's last segment — now +falls on the teaser. + +```jsonc +{ "type": "teaser", "id": "fin", "seconds": 7, + "lines": ["Pirate Software", + { "text": "The Largest Ferret Rescue in the United States", + "break": "in the United States" }, + "February 2027"], + "tail": "?", // optional: appended to the LAST line, fades in on its own + "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. + 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 + kicker (a date), and everything between a big heavy title; two lines are an + overline and a title; one is a title. +- **`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. +- **`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 + (an authored `chapter` still wins). The deck slides away over a teaser + whatever `overCards` says — it is full frame, never framed into the footage + box — and it has no pip and no QR. + +**How it is drawn.** `chrome-teaser.mjs` builds the page (pure; the cue list +is `teaserCues`, every cue a `fromTo` with its from stated, as the deck's are), +compose-chrome renders it as region `teaser` (`chrome/teaser-<id>/`, frames in +`chrome/teaser-<id>-frames/`, cached by the page's content key — changed words +are a new render), and the build encodes the frames into `segments/<id>.mp4` +at the parameters every segment shares. The display face is the vendored +Archivo (`fonts/Archivo[wdth,wght].ttf`, weight 600–900 and width 112–125 %), +copied in as the private family `TeaserDisplay`. The ground is `palette.bg` +lifted toward the accent at the centre and falling toward black at the edges, +a vignette, seeded film grain, a soft light leak drifting across, letterbox +bars that close in over the dissolve, and a slow push-in over the whole card. +Each line slams in from 1.42× (the overline from 1.25×), blurred, undershoots +to 0.968× as it lands — a flash of the accent behind it and a streak of light +through it — and settles to rest. Rendered on the ferret cut: 210 frames in +about 90 s. + +**The sound.** Synthesised in ffmpeg, no samples (`teaserAudioGraph`): under +each pop a trailer hit — a sub sine dropping from ~92 to ~40 Hz with its octave +for body on an exponential decay, a band-passed noise burst for the punch, a +low noise tail and a short low-passed echo. The title's hit is the biggest, +the overline's and the date's a little smaller, the second tier's lighter and +shorter; under the tail a low swell rises and settles as it fades in. The times +are `teaserTimes` (`deck.mjs`), the SAME the composition's cues are built from, +and each hit starts on the sample of its pop. The sum is limited at −6 dBFS +(`alimiter`, no auto-level, latency compensated) and levelled against the ferret +cut: the cut measures −17.7 LUFS integrated, the teaser −19.6. `hits: false` is +digital silence, as a card's is. + +**When it is rebuilt.** The segment is re-encoded only when its key — the +frames' render key and the whole sound graph — differs from the one recorded +beside it (`<id>.teaser.json`). Any build that reaches a teaser rebuilds it +when its words, motion or sound changed: a full build, `--only <id>`, and +**`--chrome-only`**, which builds teaser segments (they are chrome — graphics +made from the manifest, nothing fetched) while still rebuilding no clip. +`verify-build` checks each teaser's frame count and that its segment was +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}, +{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 +deck's true-still route already does for its region. + ## Chrome, not cards **The ferret-rescue cut has no cards at all** — no title, no chapter breaks, no