Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 134 lines 5.8 kB view raw
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 24import { readFileSync, existsSync, statSync } from "node:fs"; 25import { spawn } from "node:child_process"; 26import { dirname } from "node:path"; 27import { fileURLToPath } from "node:url"; 28 29const HERE = dirname(fileURLToPath(import.meta.url)); 30const ROOT = HERE; // recap/ 31 32const PKG = (() => { 33 try { return JSON.parse(readFileSync(`${ROOT}/package.json`, "utf8")); } 34 catch { return { name: "ac-24", version: "0.0.0" }; } 35})(); 36 37const argv = process.argv.slice(2); 38const cmd = argv[0]; 39 40// ── help ────────────────────────────────────────────────────────────── 41function 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 ───────────────────────────────────────────────────────────── 52async 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) ─────────────────────────────────────── 80function 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 ────────────────────────────────────────────────────────── 111const positional = argv.filter((a) => !a.startsWith("--")); 112const flags = argv.filter((a) => a.startsWith("--")); 113 114switch (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}