Archilyzer · Source

archilyzer

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

commit 1a24934070886f74cf086cbe82ef9184e5314838
parent 40c8de2f769444cafcc13c638748dcdbf2155a4f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun,  4 Oct 2026 16:31:08 -0400

umtool: fetch-via-editor --max-height; a whole-source fetch asks at the manifest's maxHeightSource

--max-height (144-2160) is checked before the request and sent with a window
or --full; a reported height rides on done, with a note when it is taller
than asked. editorFullSourceSteps passes the caller's height, else the
manifest's render.maxHeightSource; the bench's whole-source fetch hands it
the manifest.

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

Diffstat:
Mumtool/app/api/report/fetch/route.ts | 2+-
Mumtool/lib/report/driver.mjs | 23++++++++++++++++++++++-
Mumtool/lib/report/driver.test.mjs | 18+++++++++++++++++-
Mumtool/report-to-video/fetch-via-editor.mjs | 41++++++++++++++++++++++++++++++++++++++---
Aumtool/report-to-video/fetch-via-editor.test.mjs | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 196 insertions(+), 6 deletions(-)

diff --git a/umtool/app/api/report/fetch/route.ts b/umtool/app/api/report/fetch/route.ts @@ -122,7 +122,7 @@ export async function POST(request: Request) { // yt-dlp from here has none of that, which is why it is now the opt-out // (UMTOOL_LOCAL_FETCH=1) rather than the default. const steps = wantFull - ? editorFullSourceSteps(r.project, clipId) + ? editorFullSourceSteps(r.project, clipId, { manifest: r.manifest }) : localFetch() ? fetchSteps(r.project, clipId, { padBefore, padAfter }) : editorFetchSteps(r.project, clipId, { padBefore, padAfter }); diff --git a/umtool/lib/report/driver.mjs b/umtool/lib/report/driver.mjs @@ -277,11 +277,20 @@ export function editorFetchSteps(project, clipId, pad) { * media, and the job keeps running on the editor if we give up — nothing is * lost, and the next ask finds it cached. * + * THE HEIGHT. `--max-height` is the caller's `maxHeight`, else the manifest's + * `render.maxHeightSource` — the tallest source the report renders from, so a + * whole recording is not fetched taller than the cut will use. At 720 or less + * the editor saves its 720p H.264 preset, above it the original. A value that + * is not a whole number from 144 to 2160 falls through to the next, and with + * neither the flag is left off and the channel's own quality applies. + * * @param {{ dir: string }} project * @param {string} clipId + * @param {{ maxHeight?: unknown, manifest?: { render?: { maxHeightSource?: unknown } } | null }} [opts] * @returns {import("../trim").Step[]} */ -export function editorFullSourceSteps(project, clipId) { +export function editorFullSourceSteps(project, clipId, opts = {}) { + const maxHeight = [opts.maxHeight, opts.manifest?.render?.maxHeightSource].find(isFetchMaxHeight); return [ { cwd: PIPELINE_DIR, @@ -296,6 +305,7 @@ export function editorFullSourceSteps(project, clipId) { "--fetch-only", clipId, "--full", + ...(maxHeight !== undefined ? ["--max-height", String(maxHeight)] : []), "--progress", "ndjson", ], @@ -305,6 +315,17 @@ export function editorFullSourceSteps(project, clipId) { ]; } +/** + * A source height the editor accepts as a fetch cap: a whole number of pixels + * from 144 to 2160 (common/lib/clipWindow.ts isFetchMaxHeight, whose bounds + * fetch-via-editor.mjs checks too). + * @param {unknown} v + * @returns {v is number} + */ +function isFetchMaxHeight(v) { + return typeof v === "number" && Number.isInteger(v) && v >= 144 && v <= 2160; +} + /** Whether this instance fetches locally with yt-dlp instead of asking. */ export const localFetch = () => process.env.UMTOOL_LOCAL_FETCH === "1"; diff --git a/umtool/lib/report/driver.test.mjs b/umtool/lib/report/driver.test.mjs @@ -4,7 +4,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { buildSteps } from "./driver.mjs"; +import { buildSteps, editorFullSourceSteps } from "./driver.mjs"; const project = { id: "p", dir: "/r/p" }; @@ -33,3 +33,19 @@ test("the verify is told when the build joined without crossfades", () => { assert.ok(verifyOf(buildSteps(project, { preset: "final", options: { xfade: false } })).includes("--no-xfade")); assert.ok(!verifyOf(buildSteps(project, { preset: "final" })).includes("--no-xfade")); }); + +test("a whole-source fetch asks at the manifest's maxHeightSource, unless told a height", () => { + const argvOf = (opts) => editorFullSourceSteps(project, "c01", opts)[0].argv; + const heightOf = (argv) => (argv.includes("--max-height") ? argv[argv.indexOf("--max-height") + 1] : null); + const manifest = { render: { maxHeightSource: 720 } }; + assert.equal(heightOf(argvOf({ manifest })), "720"); + assert.ok(argvOf({ manifest }).includes("--full")); + // An explicit height beats the manifest's. + assert.equal(heightOf(argvOf({ manifest, maxHeight: 1080 })), "1080"); + // Neither, or neither valid: no flag, so the channel's own quality applies. + assert.equal(heightOf(argvOf()), null); + assert.equal(heightOf(argvOf({ manifest: { render: {} } })), null); + assert.equal(heightOf(argvOf({ manifest: { render: { maxHeightSource: 4320 } } })), null); + // A bad explicit height falls through to the manifest's. + assert.equal(heightOf(argvOf({ manifest, maxHeight: "tall" })), "720"); +}); diff --git a/umtool/report-to-video/fetch-via-editor.mjs b/umtool/report-to-video/fetch-via-editor.mjs @@ -57,7 +57,7 @@ function die(message) { const manifestPath = argv.find((a) => !a.startsWith("-") && a.endsWith(".json")); const clipId = flag("--fetch-only") ?? flag("--clip"); if (!manifestPath || !clipId) { - die("usage: fetch-via-editor.mjs <manifest.json> --fetch-only <clipId> [--full] [--pad-before N] [--pad-after N] [--progress ndjson]"); + die("usage: fetch-via-editor.mjs <manifest.json> --fetch-only <clipId> [--full] [--max-height N] [--pad-before N] [--pad-after N] [--progress ndjson]"); } // THE WHOLE RECORDING INSTEAD OF A WINDOW. For a clip whose windows would tile @@ -67,6 +67,26 @@ if (!manifestPath || !clipId) { // the pointer it writes beside the video is what `clipWindowDirs` reads back. const wantFull = argv.includes("--full"); +// THE TALLEST SOURCE TO FETCH, in pixels, when the caller names one. A window +// is fetched at or under it; a whole recording at 720 or less is saved as the +// editor's 720p H.264 preset, above it at the original quality. Absent, the +// editor's own default applies (720 for a window, the channel's source-video +// quality for a whole recording). Checked here with the editor's bounds, so a +// typo is refused before the request rather than as a 400 after it. +const MIN_MAX_HEIGHT = 144; +const MAX_MAX_HEIGHT = 2160; +const maxHeightArg = flag("--max-height"); +const maxHeight = maxHeightArg === undefined ? undefined : Number(maxHeightArg); +if ( + maxHeight !== undefined && + !(Number.isInteger(maxHeight) && maxHeight >= MIN_MAX_HEIGHT && maxHeight <= MAX_MAX_HEIGHT) +) { + die( + `--max-height ${maxHeightArg} must be a whole number of pixels from ` + + `${MIN_MAX_HEIGHT} to ${MAX_MAX_HEIGHT}`, + ); +} + const editorUrl = (process.env.ARCHILYZER_EDITOR_URL ?? DEFAULT_EDITOR).replace(/\/+$/, ""); const token = process.env.WORKER_TOKEN ?? ""; if (!token) { @@ -165,6 +185,7 @@ const res = await ask(`${editorUrl}/api/media/fetch-window`, { videoId: entry.video, webpageUrl: entry.webpageUrl ?? undefined, ...(wantFull ? { full: true } : { from, to, pad: Math.max(padBefore, padAfter) }), + ...(maxHeight !== undefined ? { maxHeight } : {}), requestedBy: "umtool", manifest: manifestId, clipId: entry.id, @@ -174,6 +195,20 @@ const res = await ask(`${editorUrl}/api/media/fetch-window`, { const body = await res.json().catch(() => ({})); +// The file's height, when the editor reports it: carried on `done`, and said +// out loud when it is taller than --max-height (a file already on disk is +// served as it is, never re-fetched for a different cap). +function heightOf(answer) { + const h = Number(answer.height); + if (!Number.isInteger(h) || h <= 0) return {}; + if (maxHeight !== undefined && h > maxHeight) { + EMIT("note", { + message: ` ${path.basename(String(answer.file ?? ""))} is ${h}p, taller than --max-height ${maxHeight}`, + }); + } + return { height: h }; +} + if (res.status === 200) { // Already on disk — possibly WIDER than asked for, which is the point of // containing-window reuse. `fetchStart` is the file's own start, because @@ -186,7 +221,7 @@ if (res.status === 200) { cached: true, reuse: path.basename(String(body.file ?? "")), }); - EMIT("done", { out: body.file, fetchStart: body.from, cached: true }); + EMIT("done", { out: body.file, fetchStart: body.from, cached: true, ...heightOf(body) }); process.exit(0); } @@ -229,7 +264,7 @@ for (;;) { } if (j.status === "done") { EMIT("fetch", { id: entry.id, video: entry.video, from, to, cached: true }); - EMIT("done", { out: j.file ?? null, fetchStart: j.from ?? from, cached: false }); + EMIT("done", { out: j.file ?? null, fetchStart: j.from ?? from, cached: false, ...heightOf(j) }); process.exit(0); } if (j.status === "failed" || j.status === "cancelled") { diff --git a/umtool/report-to-video/fetch-via-editor.test.mjs b/umtool/report-to-video/fetch-via-editor.test.mjs @@ -0,0 +1,118 @@ +// fetch-via-editor.mjs's --max-height: checked before any request, sent with a +// window and with --full, and a reported height comes back on `done`. +// +// The editor is a scripted HTTP server on a loopback port: no corpus, no +// yt-dlp, nothing fetched. +// +// Run with: pnpm test:scripts +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), "fetch-via-editor.mjs"); + +const MANIFEST = { + provenance: { channelSlug: "demo-channel", manifestId: "demo" }, + timeline: [{ id: "c01", type: "clip", video: "abc123", start: 10, end: 20, note: "why" }], +}; + +/** An editor that answers every POST with `answer` and records the bodies. */ +async function stubEditor(answer) { + const posts = []; + const server = http.createServer((req, res) => { + let raw = ""; + req.on("data", (c) => (raw += c)); + req.on("end", () => { + posts.push(JSON.parse(raw || "{}")); + res.writeHead(answer.status, { "content-type": "application/json" }); + res.end(JSON.stringify(answer.body)); + }); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + const { port } = /** @type {import("node:net").AddressInfo} */ (server.address()); + return { posts, url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(r)) }; +} + +/** Run the script; resolves with its exit code and NDJSON events. */ +async function run(editorUrl, args) { + const dir = await mkdtemp(path.join(os.tmpdir(), "fetch-via-editor-")); + try { + const manifest = path.join(dir, "video.manifest.json"); + await writeFile(manifest, JSON.stringify(MANIFEST)); + return await new Promise((resolve) => { + execFile( + process.execPath, + [SCRIPT, manifest, "--fetch-only", "c01", "--progress", "ndjson", ...args], + { env: { ...process.env, ARCHILYZER_EDITOR_URL: editorUrl, WORKER_TOKEN: "tok" } }, + (err, stdout) => { + const events = stdout + .split("\n") + .filter(Boolean) + .map((l) => JSON.parse(l)); + resolve({ code: err ? err.code : 0, events }); + }, + ); + }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const CACHED = (height) => ({ + status: 200, + body: { cached: true, file: "/corpus/clips/7.00-23.00.mp4", from: 7, to: 23, bytes: 1, height }, +}); + +test("--max-height rides on a window request; a taller cached file is noted and its height carried", async () => { + const ed = await stubEditor(CACHED(1080)); + try { + const { code, events } = await run(ed.url, ["--max-height", "480"]); + assert.equal(code, 0); + assert.equal(ed.posts.length, 1); + assert.equal(ed.posts[0].maxHeight, 480); + assert.equal(ed.posts[0].from, 7); + const done = events.find((e) => e.ev === "done"); + assert.equal(done.height, 1080); + assert.ok( + events.some((e) => e.ev === "note" && /is 1080p, taller than --max-height 480/.test(e.message)), + JSON.stringify(events), + ); + } finally { + await ed.close(); + } +}); + +test("--max-height rides on --full, which sends no span", async () => { + const ed = await stubEditor(CACHED(720)); + try { + const { code, events } = await run(ed.url, ["--full", "--max-height", "720"]); + assert.equal(code, 0); + assert.equal(ed.posts[0].full, true); + assert.equal(ed.posts[0].maxHeight, 720); + assert.equal("from" in ed.posts[0], false); + assert.ok(!events.some((e) => e.ev === "note" && /taller/.test(e.message))); + } finally { + await ed.close(); + } +}); + +test("no --max-height sends none; a bad one is refused before any request", async () => { + const ed = await stubEditor(CACHED(720)); + try { + assert.equal((await run(ed.url, [])).code, 0); + assert.equal("maxHeight" in ed.posts[0], false); + for (const bad of ["4320", "720.5", "tall"]) { + const { code, events } = await run(ed.url, ["--max-height", bad]); + assert.equal(code, 1, bad); + assert.ok(events.some((e) => e.ev === "note" && /must be a whole number of pixels from 144 to 2160/.test(e.message))); + } + assert.equal(ed.posts.length, 1); + } finally { + await ed.close(); + } +});