Archilyzer · Source

archilyzer

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

commit bc3b28a8551ef9cf64d35941c9edb015ab117d6a
parent 13998fd903f461d9b4fab6063297af6fcaa28f3e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun,  4 Oct 2026 17:14:20 -0400

records: report-to-video README on the audio tier, the poster and src/cues clips; an [Unreleased] bullet

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mumtool/report-to-video/README.md | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mumtool/report-to-video/build-video.mjs | 8+++++---
Mumtool/report-to-video/package.json | 1+
4 files changed, 74 insertions(+), 5 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **A report video can play a clip that has only sound, and a clip can be a file beside the manifest.** When a clip's source has no picture, `build-video.mjs` plays it under a poster: a card with the clip's channel, title and date, the size of the picture area, with the sound's waveform moving along its foot (`render.audioPoster.waveform: false` keeps it still). The segment matches every other one in size, frame rate and sound, and the header, footer and on-screen deck are drawn over it as over footage. A video's saved sound (`audio.mp3` and the like in its folder) is now a source the build can cut from, after every saved picture: before any download when the clip has no picture to fetch (`"audioOnly": true` on the clip, `"preferLocalAudio": true` in `render`, or a podcast or feed record), and otherwise only when nothing can be downloaded (`--no-network`, `--skip-fetch`, or a record with no page); `--no-network` lists such clips as playing from audio only instead of refusing them. A clip may also give `"src"` (a video or audio file) and `"cues"` (its transcript, either a `transcript.cues.json` or a `parakeet-stitch` transcript), both relative to the manifest, instead of a channel and video: it plays the whole file unless `start`/`end` cut inside it, `resolve-windows.mjs` widens it with those cues, it gets no QR unless it has a `citeUrl`, and a path that leaves the manifest's folder (or an absolute one, without `"allowAbsoluteSrc": true` in `render`), a missing file or an unreadable transcript stops the build before anything runs, naming the clip. - **A report build cuts from media already on disk before it downloads anything, and `--no-network` makes sure it never does.** For each clip, `build-video.mjs` now looks, in order, in the project's own `out/clips-raw`, in the clip windows the editor fetched into the channel (`channels/<slug>/data/<id>/clips/`), and in a saved whole source video (through the saved-video store's pointer, or a `source-media` file still in the video's folder), and cuts from the first that holds the clip plus its fetch pad; only when none does is the window downloaded. A file that is a link to a drive that is not mounted counts as not there, and the next place is tried. The build prints one line per clip naming where its source came from (`raw-cache`, `corpus-window`, `saved-video`, or a network fetch). With `--no-network`, every clip's source is found before anything is rendered, and if any clip would need a download the build stops at once and lists each one (its position in the timeline, channel, video and the span it needs). umtool's clip bench reads the same three places, so a clip it shows as fetched is one the build cuts from without downloading. - **umtool's report videos can show a highlighted sentence from a saved article.** `node umtool/report-to-video/shoot-page.mjs --page <saved page.html> --quote "<sentence>" --out <shot.png>` opens a web page saved to disk, finds the sentence in its text, highlights it and saves a PNG of the paragraph that holds it, ready to be a report manifest's `image` entry. `--batch <items.json> --out <dir>` does a list of `{ id, page, quote, context? }` at once and writes `<id>.png` for each plus a `results.json` recording each shot's crop, the matched text and the block it shot. The page is opened offline: nothing is fetched except files saved beside it, and its own scripts do not run unless `--js` is given. The sentence is found whether its quotes and apostrophes are curly or straight, across links and emphasis, and through non-breaking spaces, soft hyphens and line breaks in the page's source. A sentence that is not on the page is listed in `results.json` and on the terminal, and the run ends with an error rather than leaving it out. `--color` sets the highlight; `context` picks one occurrence of a sentence that appears more than once. - **A clip or whole-recording fetch can name the tallest source video it wants.** The MCP's `fetch_clip` takes `maxHeight`, `fetch-via-editor.mjs` takes `--max-height`, and the editor's fetch endpoint takes `maxHeight`: a whole number of pixels from 144 to 2160; anything else is refused before anything is fetched. A window is fetched at or under that height (720 when none is given, as before). A whole recording asked for at 720 or less is saved as the **Video 720p** quality, and above 720 at the original quality; with no height it follows the channel's, else the global, source video quality, as before. umtool's whole-source fetch from the clip bench now asks at the report's `render.maxHeightSource`. A file already on disk is returned as it is and never fetched again for a different height; the answer now gives its height (a window's is read from the file, a whole recording's from what its persist recorded) and says when it is taller than the height asked for. diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md @@ -160,7 +160,22 @@ order, and the first place holding the clip's **padded** span (its extent plus (and picture height) read by one `ffprobe` — run only when tiers 1 and 2 missed. -Only a miss in all three fetches. The channels tree is the one the clip bench +4. **`audio`** — the recording's sound alone, `data/<id>/audio.<ext>` (`mp3`, + then `m4a`, then `opus`, then any other: `aac`, `ogg`, `wav`, `flac`, an + audio `webm` or `mp4`), the window `[0, duration]`. Only when allowed, by one + rule (`sources.mjs` `audioAllowed`): + - **before the network** when the clip has no picture to fetch: the entry + says `audioOnly: true`, the manifest says `render.preferLocalAudio: true`, + or the record's `platform` is a feed (`podcast`, `feed`, `rss`); + - **otherwise only when nothing could be fetched anyway**: `--no-network`, + `--skip-fetch`, or a record with no page (`webpageUrl`) to fetch from. + + Being last, it never beats a picture already on disk. Without the rule a cut + would quietly lose its pictures to it: every transcribed video keeps an + `audio.mp3`. Under `--no-network` an audio-only clip is satisfied — listed up + front as playing from audio only, not as a miss. + +Only a miss in all of them fetches. The channels tree is the one the clip bench reads for the project: `provenance.channelsDir`, else a `.shadow-channels/` beside the manifest, else `CHANNELS_DIR` / the checkout's `transcripts/channels`. A file reached through a link into `media/` (perhaps on another drive) is @@ -175,7 +190,20 @@ fetch would have produced — rather than decoding a fifteen-minute window or a three-hour container. A raw-cache window is measured whole, as before, so no cached cut moves. umtool's bench lists tiers 2 and 3 through the same module (`clipWindowDirs`), with a container's span from its cue doc rather than an -ffprobe, so what the bench calls fetched is what the build cuts from. +ffprobe, so what the bench calls fetched is what the build cuts from. (Not the +audio tier: the bench would call nearly every transcribed video fetched.) + +**A source with no picture plays under a poster.** Whatever a clip is cut from +— the audio tier, a `src` that is an mp3 (below), a window with no video +stream — the build `ffprobe`s it, and when it holds no video (an mp3's cover +art does not count) the segment's picture is a poster: the clip's own card — +channel, title and date, resolved as the header resolves them — rendered by +`renderCard` at exactly the picture box's size, with the sound's waveform +moving along its foot in the accent colour (`render.audioPoster.waveform`, +default `true`; `false` keeps the still). It replaces `[0:v]` and nothing +else, so the letterbox, the header, the footer and the deck's framing all +apply, and the encode is the same: size, fps, pixel format, SAR and audio +layout match every other segment's, and the concat cannot tell it from one. ## Driven from umtool @@ -253,6 +281,43 @@ window: Only the played window is in the segment, so a mark past `cutEnd` (inside the extent, so it validates) mutes nothing; the build says it will not be heard. +### A clip with its own media: `src` and `cues` + +A clip may name a FILE instead of a corpus recording — a podcast episode, a +recording no channel holds — with no stub channel built for it: + +```jsonc +{ "type": "clip", "id": "p01", + "src": "media/episode-12.mp3", // relative to the MANIFEST; video or audio-only + "cues": "media/episode-12.json", // optional: its cues, relative to the manifest + "start": 61.2, "end": 74.9, // optional: default the whole file + "title": "Episode 12", "channelTitle": "The Feed", "date": "2026-01-02", + "citeUrl": "https://…" } // optional: the ONLY thing that draws a QR +``` + +- **The window is the file**, `[0, its ffprobe duration]`; `start`/`end` cut + inside it and default to its two ends. Nothing is looked up or fetched, and + `--fetch-only` on one is a no-op. The cut still snaps to silence. +- **`cues`** is read in either shape — the corpus's `transcript.cues.json` + (`cues: [{start, end, text}]`, with `title`, `channel`, `uploadDate`, + `platform`) or the transcript `scripts/parakeet-stitch.mjs` writes + (`chunk_data: [{start_time, end_time, text}]`, `duration_seconds`) — through + one normaliser (`local-media.mjs` `normaliseCueDoc`), and feeds + `resolve-windows.mjs`'s widening and `--cut-to-quote` as a record's cues do. + Without `cues`, or without `start`/`end`, resolve-windows leaves the clip as + it is. The header, the chapter and the deck read the cue file's `title`, + `channel` and `uploadDate` (the file's name is the title when it has none); + the entry's own `title`, `channelTitle` and `date` win as on any clip. +- **No QR is derived**: the file has no page on the archive. `citeUrl` draws + one. (The legacy rail draws a code on every tile, so there a `src` clip + without `citeUrl` gets a card's — the sweep's link.) +- **Paths may not climb out of the manifest's directory**, and an absolute one + is refused unless the manifest sets `render.allowAbsoluteSrc: true` — media + too big to keep beside the manifest is a decision written down. A `src` beside + a `video`, a `cues` without a `src`, a file that is not there, a cue file in + neither shape, or an `end` past the file's end refuses the build before + anything runs, naming each entry (`timeline[<i>] <id>: …`). + `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 (all of it, when the segment is shorter), reaching both diff --git a/umtool/report-to-video/build-video.mjs b/umtool/report-to-video/build-video.mjs @@ -28,9 +28,11 @@ // // Before any fetch, a clip's source is looked for ON DISK (sources.mjs): this // build's out/clips-raw, then the corpus's channels/<slug>/data/<id>/clips/ -// windows the editor fetched, then a saved whole source. Only a miss in all -// three goes to the network, and --no-network refuses the build up front if -// any clip would. +// windows the editor fetched, then a saved whole source -- and, by +// sources.mjs's audioAllowed rule, the recording's sound alone, which plays +// under a poster. Only a miss in all of them goes to the network, and +// --no-network refuses the build up front if any clip would. A clip with its +// own `src` (a file beside the manifest, local-media.mjs) is its own source. // // In the app: not used. On the CLI: // node umtool/report-to-video/build-video.mjs <manifest.json> [options] diff --git a/umtool/report-to-video/package.json b/umtool/report-to-video/package.json @@ -26,6 +26,7 @@ "./deck": "./deck.mjs", "./factcheck": "./factcheck.mjs", "./ledger-totals": "./ledger-totals.mjs", + "./local-media": "./local-media.mjs", "./mute": "./mute.mjs", "./package.json": "./package.json", "./post-links": "./post-links.mjs",