Archilyzer · Source

archilyzer

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

commit 1b85c81805e570141caffebc5f34258cbb932ea5
parent c2ccbfe8364587716a9d11a6ecdbb55747796ef2
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 20:58:02 -0400

docs: the teaser's dip -- the [Unreleased] entry, two quirks (geq's cost at 1080p against fade to black; -ac 1 reading a stereo ceiling at -3 dB), and "Dip F2, as built"

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/deck-posts.md | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mumtool/docs/quirks.md | 13+++++++++++++
3 files changed, 98 insertions(+), 0 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -8,6 +8,7 @@ - **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 — or, when it is within 0.02 s of the new edge, moves it onto that edge. 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. `"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 can go to black before its teaser, and the teaser rises out of the black.** A teaser entry's `"dip": { "fade": …, "black": … }` fades the whole frame before it — the footage, the on-screen deck, the posts feed and anything else drawn over the cut — to black over the previous segment's last `fade` seconds (0.3–4), its sound to silence with it, then holds `black` seconds (0–3) of black. The teaser then opens out of it: the letterbox is already closed, the ground and its light stay dark until the first line slams in and come up with its hit, and a synthesised riser swells under the black into that first hit. The deck and the feed leave under the black instead of sliding away over the crossfade. The black is the start of the teaser's own segment, so the teaser is that much longer and nothing else moves; with a dip, the teaser's `seconds` counts from where the light comes up. The fade is made where the cut is joined, so `--chrome-only` changes it without rebuilding a clip. A dip anywhere but on a teaser, on the first entry, or out of range is refused with a sentence before a build fetches anything. A cut without a dip 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/plans/deck-posts.md b/plans/deck-posts.md @@ -541,3 +541,87 @@ about 0.99; at 1.05 and 1.3 it is refused with 7.2 and 8.1. Each clip's video and audio are the same length; the measured level onsets (an 8 dB rise per 50 ms window) land on the overline, title and kicker hits at every beat — the second tier's lighter hit falls inside the title's decay, as designed. + +## Dip F2, as built + +Branch `deck/finale-dip` from e04795b7 (main 90bd8384 with `deck/feed-f1` and `deck/teaser-v1` +merged). The ask: about one to two seconds of fade to black between the last clip and the +teaser finale, for a little suspense, and a good transition from black into the finale. A +teaser entry's optional `"dip": { "fade": <s>, "black": <s> }`. + +| Commit | What | +|---|---| +| a972f050 | `deck.mjs`: `DIP_LIMITS` (fade 0.3–4 s, black 0–3 s), `DIP_RISE`, `DIP_RISER`, `validateDip` (only a teaser; both keys; no other), `dipOf`, `teaserLead(entry, D)`, `dipHideAt`, `teaserMotionOf(entry, D)`, `teaserCardSeconds`; `teaserSeconds(entry, D)` and `teaserHits(entry, D)` take the cut's transition; `validateTeaser` checks the dip and measures `seconds` as the card's; `validateTeasers` refuses a dip on the first entry, `validateCutEdits` a dip on anything but a teaser; `estimatedDuration(entry, render, D)`; the schedule's teaser segment carries `dip`; `deckChoreography` hides in an instant at `dipHideAt`. chrome-teaser: the page out of black (`dip` in `teaserCues`, the veil node and rule). compose-chrome and verify-build take the transition | +| 95663453 | build-video: `withCutEdits`' `dips`, `cutJoins` puts `dip` on the segment before a dipped teaser, its sound by `endFadeAudioFilter`; `dipWindows`, `dipVideoFilter`, `dipParts` laid last in every concat path (`xfadeConcatArgs`, `hardCutFilterArgs`, `applyChromeArgs`, `applyRail`, `previewFromSegmentsArgs`); `concatRecordText` names a dip; `buildTeaserSegment` takes the transition; the teaser's audio graph gains the riser's layer | +| 29d6581f | The riser at gain 0.22; `dip.test.mjs` | +| f002d76f | The picture by `fade` to black instead of `geq` (the `geq` form kept for a preview window that opens inside the fade); README | +| 47761930 | The join into a dipped teaser holds the outgoing segment through the overlap (`xfade` custom `expr='A'`, `acrossfade` `nofade` both sides) | +| (this) | quirks, one `[Unreleased]` bullet, this section | + +Rulings as built: + +- **The fade is the whole finished frame.** Over the previous segment's last `fade` seconds, + ending on its last frame `last` (frame `s = last − n`, n = `endFadeFrames`, is the last + untouched one), ffmpeg's `fade` out to black on the composite after every overlay (deck, feed, + posts, rail), enabled on frames (s, until): `until = last + 1 + round(black·fps)`. A fade to + black stays in yuv420p (luma 16, chroma 128); only a coloured fade needs RGB. `geq` was the + first form and cost about 0.25 s per 1080p frame (quirks); it remains only for a + `--chrome-preview` window that starts inside the fade, where `fade` cannot start. The sound is + `endFadeAudioFilter` on that segment's join, silent at the last frame's time. Both are made + where the cut is joined, so `--chrome-only` changes them; the hard-cut record names the dip. +- **No dissolve into a dipped teaser.** Over the overlap the outgoing segment plays untouched + and the teaser takes over at its end, under the black. With the ordinary dissolve the footage + was blended with the teaser's black lead and darkened by (1 − p)(1 − k) while the deck and the + feed, over it, darkened by (1 − k) alone: measured on the ferret ending, the panels stayed + visibly lit over a nearly black picture through the last half second of the fade. The + overlap's length is unchanged, so every offset, chapter and schedule time is too. +- **The black is the teaser's own lead.** `teaserLead = D + black` (D the transition as built, + 0 under `--no-xfade`): its page is black and its sound silent but for the riser until then; + the segment is the lead plus the card. No hold anywhere. The deck and feed hide in an instant + at `dipHideAt` (the dipped segment's last frame, black), not over the overlap. +- **The rise.** Bars closed from the first frame, `autoAlpha` 0 → 1 with the light; a black + `veil` over the ground and the leak, under the words: 1 → 0.45 from the end of the black to the + first impact (0.35 s, `power2.in`), → 0 over 0.3 s after it (`power2.out`). The first line's + slam starts 0.15 s after the black (the impact 0.35 s), so with a dip the card needs 0.4 s + less than without, and `seconds` (when set) is the card's from the end of the black. The leak + enters after the lead. A riser: a sub 30 → 55 Hz (squared envelope) and noise band-passed + 400 Hz–6.5 kHz (cubed), up to 1 s, ending on the first impact, released over 40 ms; its last + 100 ms measure −18.3 dBFS RMS against the hit's −13.0. The dipped teaser (B) measures + −19.7 LUFS integrated, peak −6.0 dBFS; without a dip it measures −19.4 LUFS at the same beat. +- **Without a dip nothing changes:** the ferret teaser's page sha256 at 07d1fa08 still holds + (`chrome-teaser.test.mjs`), the audio graph has no riser layer, every concat path writes the + graph it did (`dip.test.mjs` compares them), and the schedule has no `dip` key. + +The ferret ending at `beat: 1.05`, no `seconds` (card 6.8 s), D 0.5. c20 is 7.0 s (frames +0–209, its last at 6.967 s); the teaser starts at 6.5 s. + +| Dip | Teaser | Fade (frames → black) | Black to | Veil lifts | First impact | Riser | Cut | +|---|---|---|---|---|---|---|---| +| A `{0.9, 0.4}` | 7.7 s (lead 0.9) | 6.067 → 6.967 | 7.4 | 7.4 | 7.75 | 6.75–7.75 | 14.2 s | +| B `{1.2, 0.6}` | 7.9 s (lead 1.1) | 5.767 → 6.967 | 7.6 | 7.6 | 7.95 | 6.95–7.95 | 14.4 s | +| C `{1.6, 1.0}` | 8.3 s (lead 1.5) | 5.367 → 6.967 | 8.0 | 8.0 | 8.35 | 7.35–8.35 | 14.8 s | + +The previews (scratch copies of the project with the timeline `[c20, fin]`, the feed layout, +`render.endFade` 1, built with `--skip-fetch` from the cached windows, nothing fetched): video +and audio the same length in each (14.200, 14.400, 14.800 s). Every frame from c20's last +through the end of the black has YMAX 16 across the whole frame, and the mid-black frame +Y 16/16, U 128/128, V 128/128 (signalstats), deck panel and feed column included; the +contact sheets show the panel and the column dimming with the footage at mid-fade. The sound in +C is −135 dBFS from c20's last frame to the riser. + +Gates: + +- Workspace tsc (`pnpm -r exec tsc --noEmit`) clean (44 s, on a loaded machine). +- `test:scripts` on 47761930: 412 tests, 409 pass, 2 skipped, 1 failed: the queue-lock banner + case under load; `queue-lock.test.mjs` alone 11/11, three times. `dip.test.mjs` 17/17; the + report-to-video suite alone 291/291. +- e2e and the capped umtool `next build` are run at the merge, not here. + +Found and left: + +- At the bottom of the veil's lift the dark radial ground shows 8-bit banding rings for a few + frames (x264 at crf 21 over a gradient under a near-black veil); the grain sits above the + veil, where an overlay blend over black adds nothing. +- `deck.motion.in` longer than the transition no longer matters to a dipped boundary (the hide + is instant), but a popup-layout clip's posts still leave over the overlap as they always do, + now under the fade. diff --git a/umtool/docs/quirks.md b/umtool/docs/quirks.md @@ -393,6 +393,19 @@ 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. +**`geq` costs about a quarter of a second per 1080p frame.** It evaluates its +expressions per pixel through the expression parser: 90 frames of a three-plane +blend took 23 s wall on eight threads, where `fade` out to black took under a +second. A fade to BLACK stays in yuv420p (luma to 16, chroma to 128, for any +studio-range format); only `fade=…:color=` needs RGB. So a teaser's `dip` is +`fade` with an `enable` window, and the end fade (toward bg, a colour) stays a +`geq` over its one second. `fade` cannot start before its stream does, so a +`--chrome-preview` window that opens inside a dip takes the `geq` form. + +**`-ac 1` sums a stereo graph's two channels at −3 dB each.** Reading the +teaser's sound downmixed to mono measures its −6 dBFS ceiling as 0.707; read +channel 0 of a stereo decode to check the limiter. + ## Rail strips and rolling counters **A slab that slides moves text that did not change.** The tally used to be four