Archilyzer · Source

archilyzer

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

commit ab896086dcd349edcd13f4b1b4d8db304408ffce
parent b160275125786b8e3db433a6d7b3d8315626ac34
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 14:21:32 -0400

umtool: README, quirks and the changelog for muteFrom, render.endFade and the fitted QR host label

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

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

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - **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 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 @@ -273,6 +273,32 @@ held clip in a hard cut grew by up to half a frame and several of them failed the length check. `postHolds` rounds a hold to whole frames before either filter sees it. +**A coloured `fade` converts the whole hard cut to RGB.** `fade=t=out:…:color=<bg>` +accepts RGB formats only (a fade to black also takes YUV), so ffmpeg inserts a +yuv420p→rgb24 scale before it -- and the concat filter, which needs every +segment in one format, then negotiates EVERY other segment to rgb24 too. The +cut's untouched frames came out different (framemd5) from the same graph +without the fade. The end fade is a `geq` blend toward bg's limited-range +BT.601 Y′CbCr instead (`#12101a` is 31/132/128, what `pad` wrote into the +segments), enabled only from its first frame; `geq` truncates, so each plane +adds 0.5 to round. + +**`afade` out writes digital silence after its fade, and copies every sample +before it.** That makes it the mute for `muteFrom`: samples before the fade are +the clip's own bit for bit (A/V sync cannot move), and samples after it are +zeros, not a quiet signal. A fade that ENDS at the mute point keeps a sound +that starts there out entirely. + +**A label fitted to a length is measured as ink, not as a box.** CSS +`letter-spacing` is added after the LAST letter too, and a box's length +includes each end glyph's side bearing, so sizing the deck's QR host by its +element's length leaves it short of the code by the trailing tracking and the +bearings. `fitHost` measures the string's ink in the loaded face with canvas +`measureText` (`actualBoundingBoxLeft + actualBoundingBoxRight`), adds the +tracking between letters only, scales the font size (everything in it is in em) +and indents the first bearing away. Measured on the ferret cut: ink rows 31–180, +the code's 31–180. + **`xfade` hands on its own pixel format.** Even the frames before its offset, which are the first input's, come out as yuv444 rather than the input's yuv420p, so a framemd5 of the crossfade's output never equals the segment's diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md @@ -187,7 +187,7 @@ the manifest names the MCP video id while the cue file lives under the URL slug. "quote": "The pre-application screening was approved by the county, dude." } ``` -Two per-clip fields exist for compilations that span sources or need a hand-cut +Three per-clip fields exist for compilations that span sources or need a hand-cut window: - **`channel`** — the archived channel this clip's cue file lives under, overriding @@ -201,6 +201,19 @@ window: sentence is an editorial decision that widening would silently undo. `lock` also handles the reverse case — a clip whose lead-in would drag in seconds of some *other* audio (a news package playing before the speaker starts). +- **`muteFrom`** — SOURCE seconds, like `start`/`end`/`cutEnd`, within the clip's + `start`–`end`: the clip's sound goes silent from that second to the end of the + clip while the picture plays on. A 40 ms fade ends exactly at `muteFrom`, so + nothing of a sound that starts there gets through and nothing clicks; from it on + the sound is digital silence, and a hold on that clip stays silent. It is made + where the cut is joined (see [The cut's edits](#the-cuts-edits-mutefrom-and-renderendfade)), + so changing it rebuilds no segment: `--chrome-only` applies it under the deck. + +`render.endFade` (seconds, default 0 = off, at most 10) fades the cut's LAST +segment — whatever it is — picture to `palette.bg` and sound to silence over its +final `endFade` seconds, reaching both on the last frame. It is for a cut that +ends on a clip; once a finale entry follows the last clip, the ordinary +crossfade into it does the job and `endFade` fades the finale instead. ### `render.chrome` — the on-screen deck's settings, and per-entry `onscreen` @@ -939,6 +952,13 @@ deck slides away for its duration when it is `"hide"` (the default) — no text handover happens across a hidden segment, because there is nothing on screen to animate. +The code's host (`JASOLYZER.PAGES.DEV`, muted, tracked 0.1 em) runs up the QR's +left side and is exactly as long as the code is tall (`deckLayout(render).qr.size`, +150 px by default), whatever the host: the page measures the string's ink in the +loaded face once at load, scales its size to fit, and indents the first +letter's side bearing away, so the ink starts on the code's bottom edge and +ends on its top. + `out/<variant>/schedule.json` (`deckSchedule()`) is the one source of *when*: the build writes it from PROBED segment durations and real source metadata; `estimateSchedule()` produces the same shape (`estimated: true`) from the @@ -1089,6 +1109,34 @@ no joins and runs the graphs it always did. chapters, a `--chrome-preview` window and `--chapters-only` all add the holds to the probed lengths (`cutOffsets`), the same sum `deckSchedule` makes. +### The cut's edits: `muteFrom` and `render.endFade` + +Both are made on one input's chain before the join, beside the hold and the +move (`cutJoins` merges them into the deck's joins; `withCutEdits` is the pure +merge), so neither touches a segment file and every graph, record and schedule +of a cut without them is what it was. They work with or without the deck, on +crossfades and hard cuts. + +- **`muteFrom` is mapped through the segment's cut record.** The build snaps a + clip's cut to the nearest silence, so its segment starts up to `snapWindow` + seconds from the manifest's `start` (or `cutStart − leadIn`). Every clip build + writes `segments/<id>.cut.json` — `{ video, start, end }`, the source seconds + it was really cut from — and `muteFrom − start` is the mute point in the + segment's clock (`muteSegmentSeconds`, in `deck.mjs`). A segment with no record + (built before records existed, or copied without it), or one whose record is + not as long as the segment, is measured from the unsnapped start instead, and + the build says so in a note. The sound chain is + `afade=t=out:st=<at − 0.04>:d=0.04` (then the hold's `apad`, if any); a mute at + or before the segment's start is `volume=0`. +- **The end fade's picture is a `geq` blend toward bg's Y′CbCr**, enabled from + the fade's first frame: frame `lastFrame − n` is the last untouched one and + `lastFrame` (the hold's clones counted) is bg. Not `fade=…:color=`, which works + in RGB only (`umtool/docs/quirks.md`). The sound is `afade` out to silence at the + last frame's time. +- **The hard-cut record names a mute and a fade** (`"mute"`, `"fade"` on a + `# join` line, only when there is one), so a cached prerail made without them, + or with other values, is never reused. + ### Two ffmpeg traps that are the deck's alone - **Mixed RGB/RGBA frames restart the whole filtergraph.** HyperFrames writes