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:
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();
+ }
+});