From b6d9424b51e6b2232ee80a86636a79ffdcfb60ff Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 23 Sep 2026 16:00:17 -0700 Subject: [PATCH] AC reels: GPU encode, chrome-free stamps, most-played clock recipe - pop/lib/video-codec.mjs: one h264 arg set for capture-av + spawnFFmpegEncode; AC_VIDEO_ENCODER=nvenc swaps libx264 for h264_nvenc (p7/hq, capped 8M/12M) on jastow's 3070. Macs keep libx264. - stamp-reel --chrome none: skip the pals side stamps + title columns. - aesthetic lane: chrome none, 56px cursor, and no cursor at all when a recipe has no scripted performance (it was parked mid-frame from the audio-unlock click). - new recipe clock-wibe: *wibe, the most-hit stored clock (110 hits). Co-Authored-By: Claude Opus 5.5 --- marketing/av-reels/bin/capture-av.mjs | 3 ++- marketing/av-reels/bin/stamp-reel.mjs | 8 +++++--- pop/lib/preview-shared.mjs | 4 ++-- pop/lib/video-codec.mjs | 23 +++++++++++++++++++++++ toolchain/instagram/reel-app-config.mjs | 13 +++++++++++++ toolchain/instagram/reel-app.mjs | 8 +++++++- 6 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 pop/lib/video-codec.mjs diff --git a/marketing/av-reels/bin/capture-av.mjs b/marketing/av-reels/bin/capture-av.mjs index 35b0dea624..5473dc57a9 100644 --- a/marketing/av-reels/bin/capture-av.mjs +++ b/marketing/av-reels/bin/capture-av.mjs @@ -27,6 +27,7 @@ import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; import { PERFORMANCES } from "./performances.mjs"; +import { h264Args } from "../../../pop/lib/video-codec.mjs"; const HERE = dirname(fileURLToPath(import.meta.url)); const LANE = resolve(HERE, ".."); @@ -347,7 +348,7 @@ const mapArgs = haveAudio ? ["-map", "0:v", "-map", "1:a", "-c:a", "aac", "-b:a", "192k", "-shortest"] : ["-map", "0:v"]; const ff = spawnSync("ffmpeg", [...vArgs, ...aArgs, - "-vsync", "cfr", "-r", String(FPS), "-c:v", "libx264", "-preset", "medium", "-crf", "18", + "-vsync", "cfr", "-r", String(FPS), ...h264Args({ crf: 18, preset: "medium" }), "-pix_fmt", "yuv420p", ...mapArgs, "-movflags", "+faststart", OUTMP4], { stdio: ["ignore", "ignore", "inherit"] }); if (ff.status !== 0) { console.error("✗ ffmpeg assemble failed"); process.exit(1); } diff --git a/marketing/av-reels/bin/stamp-reel.mjs b/marketing/av-reels/bin/stamp-reel.mjs index b2b9d35831..2fee67d2b3 100644 --- a/marketing/av-reels/bin/stamp-reel.mjs +++ b/marketing/av-reels/bin/stamp-reel.mjs @@ -63,6 +63,8 @@ const FINGER_TINT = flags["finger-tint"] // shows it); lead the marker by CURSOR_LEAD seconds to line it up. const CURSOR_LEAD = parseFloat(flags["cursor-lead"] ?? 0.12); const CURSOR_SIZE = parseInt(flags["cursor-size"] || 104, 10); +// --chrome none: skip the pals stamps + title columns; the cursor stays. +const CHROME = flags.chrome !== "none"; let PRESSES = []; let CURSOR_TRACK = null; // per-frame [{t,x,y,down}] — exact, no interpolation { @@ -163,7 +165,7 @@ let palsImg = null, palsBlur = null; palsBlur = (rb.status === 0 && existsSync(blurPng)) ? await loadImage(blurPng) : palsImg; } } -if (!palsImg) throw new Error("pals watermark failed to rasterize (need rsvg-convert)"); +if (!palsImg && CHROME) throw new Error("pals watermark failed to rasterize (need rsvg-convert)"); // AC's real cursors for the "cursor" pointer style (precise=up, active=down). let cursorImgs = null; @@ -277,7 +279,7 @@ function drawTitleChars(audioT) { } // ── frame pump: base → canvas → chrome → encoder (re-muxes base audio) ────── -console.log(`▸ chroming ${FRAMES} frames · "${TITLE}" columns + pals seep${SHARPEN ? ` · substrate sharpen ×${SHARPEN}` : ""}${PRESSES.length ? ` · ${PRESSES.length} finger presses` : ""}`); +console.log(`▸ chroming ${FRAMES} frames · ${CHROME ? `"${TITLE}" columns + pals seep` : "no chrome"}${SHARPEN ? ` · substrate sharpen ×${SHARPEN}` : ""}${PRESSES.length ? ` · ${PRESSES.length} finger presses` : ""}`); // substrate: area-downscale to the buffer grid then nearest-upscale = crisp fat // pixels, then unsharp to pop the block edges. Runs on the piece only (chrome + // fingers are drawn full-res afterward). @@ -313,7 +315,7 @@ for await (const chunk of dec.stdout) { drawFingers(ctx, PRESSES, fi / FPS + CURSOR_LEAD, W, H, { style: EFF_POINTER, tint: FINGER_TINT, cursorImgs }); } } - drawWatermark(fi / FPS); + if (CHROME) drawWatermark(fi / FPS); if (!enc.stdin.write(canvas.toBuffer("raw"))) await once(enc.stdin, "drain"); fi++; if (fi % 120 === 0) console.log(` ${fi}/${FRAMES} · ${((Date.now() - t0) / 1000).toFixed(0)}s`); diff --git a/pop/lib/preview-shared.mjs b/pop/lib/preview-shared.mjs index 54be99e4cf..b054f948b9 100644 --- a/pop/lib/preview-shared.mjs +++ b/pop/lib/preview-shared.mjs @@ -16,6 +16,7 @@ import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { loadImage } from "canvas"; +import { h264Args } from "./video-codec.mjs"; // The typeface is installed on the machines that render regularly, but it // also ships in-repo as a Menu Band resource. Prefer the installed copy @@ -518,8 +519,7 @@ export function spawnFFmpegEncode({ audioPath, w, h, fps, outPath, crf = 20, vid "-i", "-", "-i", audioPath, ...(videoFilter ? ["-vf", videoFilter] : []), - "-c:v", "libx264", "-preset", "faster", "-crf", String(crf), - "-threads", "0", + ...h264Args({ crf }), "-c:a", "aac", "-b:a", "192k", "-pix_fmt", "yuv420p", "-shortest", diff --git a/pop/lib/video-codec.mjs b/pop/lib/video-codec.mjs new file mode 100644 index 0000000000..8310cc836e --- /dev/null +++ b/pop/lib/video-codec.mjs @@ -0,0 +1,23 @@ +// video-codec.mjs — the H.264 encoder args every reel/preview encode shares. +// +// Default is libx264 (every Mac in the fleet). A box with an NVIDIA card sets +// AC_VIDEO_ENCODER=nvenc (jastow's reel farm) and gets h264_nvenc instead, +// tuned so `crf` means roughly the same quality on both paths. + +export function videoEncoder() { + return process.env.AC_VIDEO_ENCODER === "nvenc" ? "nvenc" : "x264"; +} + +// crf: libx264 constant-rate factor; nvenc's constant-quality `cq` tracks it +// about one step higher for the same look. preset: libx264 speed preset. +export function h264Args({ crf = 20, preset = "faster" } = {}) { + if (videoEncoder() === "nvenc") { + return ["-c:v", "h264_nvenc", "-preset", "p7", "-tune", "hq", "-rc", "vbr", + // Capped: Instagram re-encodes everything, so bits past ~8–10 Mbps are + // thrown away (uncapped cq ran ~11 Mbps on a clock reel). + "-cq", String(crf + 1), "-b:v", "8M", "-maxrate", "12M", "-bufsize", "16M", + "-spatial-aq", "1", "-temporal-aq", "1", "-bf", "3", "-g", "120", + "-profile:v", "high"]; + } + return ["-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-threads", "0"]; +} diff --git a/toolchain/instagram/reel-app-config.mjs b/toolchain/instagram/reel-app-config.mjs index 5149a463ee..fd006e0501 100644 --- a/toolchain/instagram/reel-app-config.mjs +++ b/toolchain/instagram/reel-app-config.mjs @@ -18,6 +18,9 @@ export const REEL_APPS = { // 60 because the shared media gate wants reels authored at 60fps, and the // CDP screencast delivers ~60 anyway (30 dropped every other frame). capture: { width: 1080, height: 1920, fps: 60 }, + // The piece alone: no pals side stamps or title columns, and a cursor + // small enough to read as a pointer rather than a mascot. + stamp: { chrome: "none", cursorSize: 56 }, segments: ["instrument", "draw", "instrument", "code"], tags: ["aestheticcomputer", "creativecoding", "digitalart", "generativeart", "webart"], recipes: [ @@ -39,6 +42,16 @@ export const REEL_APPS = { segment: "instrument", line: "Ten touches turn into a bubble instrument.", }, + { + // The most-hit stored clock (110 hits on /api/store-clock, 2026-09-23): + // four saw voices; the lead loops every ~7.75 s, so 16 s is two loops. + id: "clock-wibe", + piece: "*wibe", + duration: 16, + title: "clock", + segment: "code", + line: "The most-played clock on aesthetic.computer: four saw voices, one tiny score.", + }, { id: "clock-square", piece: "clock:0.5~{square}cegcdfdefgec", diff --git a/toolchain/instagram/reel-app.mjs b/toolchain/instagram/reel-app.mjs index 3a70c6c2fb..f5a412632f 100644 --- a/toolchain/instagram/reel-app.mjs +++ b/toolchain/instagram/reel-app.mjs @@ -170,8 +170,14 @@ async function buildAesthetic({ config, day, index, slot, dir, source }) { run(process.execPath, captureArgs, { inherit: true }); const base = join(capture, `base-${source.title}.mp4`); const reel = join(dir, "reel.mp4"); + const stamp = config.stamp || {}; run(process.execPath, [STAMP_AV, base, "--title", source.title, "--out", reel, - "--fps", String(config.capture.fps)], { inherit: true }); + "--fps", String(config.capture.fps), + ...(stamp.chrome ? ["--chrome", stamp.chrome] : []), + ...(stamp.cursorSize ? ["--cursor-size", String(stamp.cursorSize)] : []), + // No scripted performance means the only "input" was capture-av's + // audio-unlock click — don't draw a cursor parked mid-frame for it. + ...(source.performance ? [] : ["--pointer", "none"])], { inherit: true }); return { format: "ac.instagram.reel", version: 1, account: config.account, id: `${day}-s${index}-${source.id}`, day, index, slot, sourceId: source.id, -- 2.51.2