Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

recap: add ac-24 cli scaffold (recap/cli.mjs + package.json)

Versioned entrypoint mirroring papers/cli.mjs. Today it's a thin
wrapper around pipeline.fish with a `build` subcommand and a read-only
`cache` inspector. The cache work for individual steps still lives
inside each bin/* script (tts.mjs / transcribe.mjs / align.mjs already
hash their inputs and skip when valid).

Future iterations move the per-step orchestration here and let the
oven's recap-builder.mjs call \`node recap/cli.mjs build <audience>\`
instead of \`fish pipeline.fish\`. For now both work — pipeline.fish is
still the source of truth for ordering.

ac-24 build <audience>
ac-24 cache <audience>
ac-24 version

See feedback memory: feedback_recap_ac24_cli.md.

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

+149
+134
recap/cli.mjs
··· 1 + #!/usr/bin/env node 2 + // recap/cli.mjs — `ac-24` CLI for the aesthetic 24 recap pipeline. 3 + // 4 + // The minimum-viable scaffold mirrors `papers/cli.mjs`: a single 5 + // versioned entrypoint that owns the surface area used by the oven's 6 + // recap-builder.mjs and (later) by anyone running the pipeline locally. 7 + // For now it's a thin wrapper around the existing `bin/*` scripts + 8 + // `pipeline.fish` — every step's caching lives inside its own script 9 + // (tts.mjs, transcribe.mjs, align.mjs, jeffrey-photos.mjs, etc.) and 10 + // will keep getting tightened. See feedback memory: 11 + // `feedback_recap_ac24_cli.md` for the full vision. 12 + // 13 + // Subcommands: 14 + // ac24 build <audience> — run the full pipeline for an audience 15 + // ac24 version — print CLI + pipeline version 16 + // ac24 cache — show per-episode cache state (stub) 17 + // 18 + // Usage from the repo: 19 + // node recap/cli.mjs build jeffrey-73h-2026-05-02 20 + // 21 + // On the oven, recap-builder.mjs can switch from `fish pipeline.fish` 22 + // to `node cli.mjs build <audience>` — same behavior, versioned shell. 23 + 24 + import { readFileSync, existsSync, statSync } from "node:fs"; 25 + import { spawn } from "node:child_process"; 26 + import { dirname } from "node:path"; 27 + import { fileURLToPath } from "node:url"; 28 + 29 + const HERE = dirname(fileURLToPath(import.meta.url)); 30 + const ROOT = HERE; // recap/ 31 + 32 + const PKG = (() => { 33 + try { return JSON.parse(readFileSync(`${ROOT}/package.json`, "utf8")); } 34 + catch { return { name: "ac-24", version: "0.0.0" }; } 35 + })(); 36 + 37 + const argv = process.argv.slice(2); 38 + const cmd = argv[0]; 39 + 40 + // ── help ────────────────────────────────────────────────────────────── 41 + function usage() { 42 + console.log(`ac-24 ${PKG.version} — aesthetic 24 recap pipeline\n`); 43 + console.log(`Subcommands:`); 44 + console.log(` build <audience> run the full pipeline for an audience`); 45 + console.log(` build <audience> --skip-tts reuse existing recap.mp3 (now redundant — tts.mjs auto-skips on hash match)`); 46 + console.log(` cache <audience> show per-step cache state for an audience`); 47 + console.log(` version print version`); 48 + console.log(` help show this`); 49 + } 50 + 51 + // ── build ───────────────────────────────────────────────────────────── 52 + async function build(audience, extraArgs) { 53 + if (!audience) { console.error("✗ build requires an audience name"); process.exit(2); } 54 + if (!/^[\w.-]+$/.test(audience)) { console.error(`✗ invalid audience name: ${audience}`); process.exit(2); } 55 + const audiencePath = `${ROOT}/audience/${audience}.mjs`; 56 + if (!existsSync(audiencePath)) { console.error(`✗ audience config not found: ${audiencePath}`); process.exit(2); } 57 + 58 + // pipeline.fish carries the source of truth for ordering today; this 59 + // is a pass-through. When the cache layer lands here, this function 60 + // will iterate the steps directly with hashed skip/run decisions. 61 + return await new Promise((resolveProm) => { 62 + const proc = spawn("fish", ["./pipeline.fish", audience, ...extraArgs], { 63 + cwd: ROOT, 64 + stdio: "inherit", 65 + env: process.env, 66 + }); 67 + proc.on("close", (code) => { 68 + process.exitCode = code; 69 + resolveProm(); 70 + }); 71 + proc.on("error", (err) => { 72 + console.error(`✗ failed to spawn pipeline.fish: ${err.message}`); 73 + process.exitCode = 1; 74 + resolveProm(); 75 + }); 76 + }); 77 + } 78 + 79 + // ── cache (read-only inspector) ─────────────────────────────────────── 80 + function cacheStatus(audience) { 81 + const out = `${ROOT}/out`; 82 + const fmt = (p) => { 83 + if (!existsSync(p)) return "MISSING"; 84 + const stat = statSync(p); 85 + return `${(stat.size / 1024).toFixed(0)} KB · ${stat.mtime.toISOString()}`; 86 + }; 87 + const items = [ 88 + [`tts (recap.mp3)`, `${out}/recap.mp3`, `${out}/recap.mp3.hash`], 89 + [`transcribe (words.json)`, `${out}/words.json`, `${out}/words.json.hash`], 90 + [`align (segments.json)`, `${out}/segments.json`, `${out}/segments.json.hash`], 91 + [`subs.json`, `${out}/subs.json`, null], 92 + [`subtitle-track.txt`, `${out}/subtitle-track.txt`, null], 93 + [`waltz.mp3`, `${out}/waltz.mp3`, null], 94 + [`recap.mp4`, `${out}/recap.mp4`, null], 95 + ]; 96 + console.log(`ac-24 cache · ${audience || "(no audience filter)"}`); 97 + for (const [label, file, hashFile] of items) { 98 + const hash = hashFile && existsSync(hashFile) 99 + ? readFileSync(hashFile, "utf8").trim().slice(0, 16) 100 + : null; 101 + console.log(` ${label.padEnd(26)} ${fmt(file)}${hash ? ` · hash ${hash}` : ""}`); 102 + } 103 + console.log(``); 104 + console.log(` jeffrey-photos: ${(() => { 105 + try { return require("node:fs").readdirSync(`${out}/jeffrey-photos`).length + " files"; } 106 + catch { return "MISSING"; } 107 + })()}`); 108 + } 109 + 110 + // ── dispatch ────────────────────────────────────────────────────────── 111 + const positional = argv.filter((a) => !a.startsWith("--")); 112 + const flags = argv.filter((a) => a.startsWith("--")); 113 + 114 + switch (cmd) { 115 + case "build": 116 + await build(positional[1], flags); 117 + break; 118 + case "cache": 119 + cacheStatus(positional[1] || ""); 120 + break; 121 + case "version": 122 + console.log(`ac-24 ${PKG.version}`); 123 + break; 124 + case "help": 125 + case "--help": 126 + case "-h": 127 + case undefined: 128 + usage(); 129 + break; 130 + default: 131 + console.error(`✗ unknown subcommand: ${cmd}`); 132 + usage(); 133 + process.exit(2); 134 + }
+15
recap/package.json
··· 1 + { 2 + "name": "ac-24", 3 + "version": "0.1.0", 4 + "description": "aesthetic 24 — daily recap video pipeline", 5 + "type": "module", 6 + "private": true, 7 + "bin": { 8 + "ac-24": "./cli.mjs" 9 + }, 10 + "scripts": { 11 + "build": "node cli.mjs build", 12 + "cache": "node cli.mjs cache", 13 + "version-check": "node cli.mjs version" 14 + } 15 + }