Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

recap/bin: drop hardcoded /Users/jas paths so the pipeline runs on oven

slides.mjs / subtitles.mjs / screenshots.mjs hardcoded the Mac-only
puppeteer module path, system/public/ font paths, and Google Chrome
executable. Resolve puppeteer dynamically across local-dev and
/opt/oven/node_modules; build font/PALS paths from a REPO constant; let
puppeteer use its bundled Chromium on Linux. screenshots.mjs picks up
the same puppeteer resolver so the (currently unused) screenshot-cache
path won't blow up if it's ever wired into a slide.

Recap pipeline now runs end-to-end on the oven.

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

+161 -10
+126
recap/bin/screenshots.mjs
··· 1 + #!/usr/bin/env node 2 + // screenshots.mjs — pre-bake artifact screenshots for the recap. 3 + // For each segment with an `artifact` (or any query whose shape includes 4 + // a `screenshot:` URL), fetch the URL via puppeteer at the requested 5 + // viewport / clip and cache the PNG to recap/out/screenshots/<seg>-<name>.png. 6 + // scout.mjs reads those cached files at slide-build time. 7 + // 8 + // Mirrors the `jeffrey-photos.mjs` cache pattern — re-runs are cheap, and 9 + // `--force` re-fetches everything (or `--only <segName>` re-fetches one). 10 + // 11 + // Query shape (in `audience.slides[seg].queries[name]`): 12 + // { screenshot: "<URL>", // required 13 + // width: 1280, height: 800, // viewport (defaults 1280x800) 14 + // clip: { x, y, width, height }, // optional final crop 15 + // waitMs: 800, // optional settle pause after load 16 + // scrollTo: { x, y }, // optional scroll before screenshot 17 + // selector: "#some-id" // optional: screenshot ONLY this element 18 + // } 19 + // 20 + // Usage: 21 + // node bin/screenshots.mjs jeffrey-73h-2026-05-02 22 + // node bin/screenshots.mjs jeffrey-73h-2026-05-02 --force 23 + // node bin/screenshots.mjs jeffrey-73h-2026-05-02 --only 02_menuband_arc 24 + 25 + import { mkdirSync, existsSync } from "node:fs"; 26 + import { resolve, dirname } from "node:path"; 27 + import { fileURLToPath } from "node:url"; 28 + 29 + const __HERE = dirname(fileURLToPath(import.meta.url)); 30 + const __PUPPETEER_DIR = [ 31 + resolve(__HERE, "../../oven/node_modules/puppeteer"), 32 + "/opt/oven/node_modules/puppeteer", 33 + resolve(__HERE, "../node_modules/puppeteer"), 34 + ].find((p) => existsSync(p)); 35 + if (!__PUPPETEER_DIR) { 36 + throw new Error("puppeteer not found in any known node_modules location"); 37 + } 38 + const puppeteer = (await import(`${__PUPPETEER_DIR}/lib/esm/puppeteer/puppeteer.js`)).default; 39 + 40 + const HERE = dirname(fileURLToPath(import.meta.url)); 41 + const ROOT = resolve(HERE, ".."); 42 + const audienceName = process.argv[2] || "jeffrey-24h"; 43 + const force = process.argv.includes("--force"); 44 + const onlyIdx = process.argv.indexOf("--only"); 45 + const only = onlyIdx >= 0 ? process.argv[onlyIdx + 1] : null; 46 + 47 + const { audience } = await import(`${ROOT}/audience/${audienceName}.mjs`); 48 + 49 + const SHOTS_DIR = `${ROOT}/out/screenshots`; 50 + mkdirSync(SHOTS_DIR, { recursive: true }); 51 + 52 + // Find every (segName, queryName, query) triple where query has a screenshot URL. 53 + const targets = []; 54 + for (const seg of audience.segments) { 55 + if (only && seg.name !== only) continue; 56 + const slide = audience.slides[seg.name]; 57 + if (!slide || typeof slide !== "object" || !slide.queries) continue; 58 + for (const [name, q] of Object.entries(slide.queries)) { 59 + if (q && typeof q === "object" && q.screenshot) { 60 + targets.push({ segName: seg.name, queryName: name, q }); 61 + } 62 + } 63 + } 64 + 65 + if (!targets.length) { 66 + console.log("(no screenshot queries — nothing to do)"); 67 + process.exit(0); 68 + } 69 + 70 + console.log(`▸ ${targets.length} screenshot target(s)`); 71 + 72 + const browser = await puppeteer.launch({ 73 + headless: true, 74 + args: ["--no-sandbox"], 75 + }); 76 + 77 + let fetched = 0, cached = 0, failed = 0; 78 + for (const { segName, queryName, q } of targets) { 79 + const outPath = `${SHOTS_DIR}/${segName}-${queryName}.png`; 80 + if (existsSync(outPath) && !force) { 81 + console.log(` · cached: ${segName}/${queryName}`); 82 + cached++; 83 + continue; 84 + } 85 + 86 + const page = await browser.newPage(); 87 + const w = q.width || 1280; 88 + const h = q.height || 800; 89 + await page.setViewport({ width: w, height: h, deviceScaleFactor: 2 }); 90 + 91 + try { 92 + console.log(` → fetching: ${segName}/${queryName} ${q.screenshot}`); 93 + await page.goto(q.screenshot, { waitUntil: "networkidle2", timeout: 30000 }); 94 + if (q.scrollTo) { 95 + await page.evaluate(({ x, y }) => window.scrollTo(x, y), q.scrollTo); 96 + } 97 + if (q.waitMs) { 98 + await new Promise((r) => setTimeout(r, q.waitMs)); 99 + } else { 100 + await new Promise((r) => setTimeout(r, 600)); // default settle 101 + } 102 + 103 + let buf; 104 + if (q.selector) { 105 + const el = await page.$(q.selector); 106 + if (!el) throw new Error(`selector '${q.selector}' not found`); 107 + buf = await el.screenshot({ type: "png" }); 108 + } else if (q.clip) { 109 + buf = await page.screenshot({ type: "png", clip: q.clip }); 110 + } else { 111 + buf = await page.screenshot({ type: "png", fullPage: false }); 112 + } 113 + 114 + const { writeFileSync } = await import("node:fs"); 115 + writeFileSync(outPath, buf); 116 + console.log(` ✓ ${outPath.replace(ROOT + "/", "")} (${(buf.length / 1024).toFixed(0)} KB)`); 117 + fetched++; 118 + } catch (e) { 119 + console.error(` ✗ ${segName}/${queryName}: ${e.message}`); 120 + failed++; 121 + } 122 + await page.close(); 123 + } 124 + 125 + await browser.close(); 126 + console.log(`✓ done · fetched ${fetched} · cached ${cached} · failed ${failed}`);
+20 -6
recap/bin/slides.mjs
··· 3 3 // out/concat.txt with per-slide durations from out/segments.json. 4 4 // Usage: node bin/slides.mjs [audience-name] 5 5 6 - import puppeteer from "/Users/jas/aesthetic-computer/oven/node_modules/puppeteer/lib/esm/puppeteer/puppeteer.js"; 7 - import { mkdirSync, writeFileSync, readFileSync } from "node:fs"; 6 + import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs"; 8 7 import { resolve, dirname } from "node:path"; 9 8 import { fileURLToPath } from "node:url"; 10 9 10 + // Resolve puppeteer from one of the known node_modules locations. Local dev 11 + // uses ../../oven/node_modules; oven prod uses /opt/oven/node_modules. 12 + const __HERE = dirname(fileURLToPath(import.meta.url)); 13 + const __PUPPETEER_DIR = [ 14 + resolve(__HERE, "../../oven/node_modules/puppeteer"), 15 + "/opt/oven/node_modules/puppeteer", 16 + resolve(__HERE, "../node_modules/puppeteer"), 17 + ].find((p) => existsSync(p)); 18 + if (!__PUPPETEER_DIR) { 19 + throw new Error("puppeteer not found in any known node_modules location"); 20 + } 21 + const puppeteer = (await import(`${__PUPPETEER_DIR}/lib/esm/puppeteer/puppeteer.js`)).default; 22 + 11 23 const HERE = dirname(fileURLToPath(import.meta.url)); 12 24 const ROOT = resolve(HERE, ".."); 13 25 const audienceName = process.argv[2] || "fia"; ··· 16 28 const SLIDE_DIR = `${ROOT}/out/slides`; 17 29 mkdirSync(SLIDE_DIR, { recursive: true }); 18 30 19 - const FONT_BOLD = "/Users/jas/aesthetic-computer/system/public/type/webfonts/ywft-processing-bold.ttf"; 20 - const FONT_REG = "/Users/jas/aesthetic-computer/system/public/type/webfonts/ywft-processing-regular.ttf"; 21 - const PALS_SVG = "/Users/jas/aesthetic-computer/system/public/purple-pals.svg"; 31 + const REPO = resolve(HERE, "../.."); 32 + const FONT_BOLD = `${REPO}/system/public/type/webfonts/ywft-processing-bold.ttf`; 33 + const FONT_REG = `${REPO}/system/public/type/webfonts/ywft-processing-regular.ttf`; 34 + const PALS_SVG = `${REPO}/system/public/purple-pals.svg`; 22 35 23 36 const fontBoldB64 = readFileSync(FONT_BOLD).toString("base64"); 24 37 const fontRegB64 = readFileSync(FONT_REG).toString("base64"); ··· 104 117 `; 105 118 106 119 const browser = await puppeteer.launch({ 107 - executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 120 + // Use puppeteer's bundled Chromium — works on both local Mac (uses 121 + // ~/.cache/puppeteer/...) and oven Linux (same). 108 122 args: ["--no-sandbox"], 109 123 }); 110 124
+15 -4
recap/bin/subtitles.mjs
··· 7 7 // + `overlay enable=between(t,a,b)` chain. 8 8 // Usage: node bin/subtitles.mjs 9 9 10 - import puppeteer from "/Users/jas/aesthetic-computer/oven/node_modules/puppeteer/lib/esm/puppeteer/puppeteer.js"; 11 - import { mkdirSync, writeFileSync, readFileSync } from "node:fs"; 10 + import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs"; 12 11 import { resolve, dirname } from "node:path"; 13 12 import { fileURLToPath } from "node:url"; 14 13 15 14 const HERE = dirname(fileURLToPath(import.meta.url)); 16 15 const ROOT = resolve(HERE, ".."); 16 + const REPO = resolve(HERE, "../.."); 17 + 18 + // Resolve puppeteer from one of the known node_modules locations. 19 + const __PUPPETEER_DIR = [ 20 + resolve(HERE, "../../oven/node_modules/puppeteer"), 21 + "/opt/oven/node_modules/puppeteer", 22 + resolve(HERE, "../node_modules/puppeteer"), 23 + ].find((p) => existsSync(p)); 24 + if (!__PUPPETEER_DIR) { 25 + throw new Error("puppeteer not found in any known node_modules location"); 26 + } 27 + const puppeteer = (await import(`${__PUPPETEER_DIR}/lib/esm/puppeteer/puppeteer.js`)).default; 17 28 const SUB_DIR = `${ROOT}/out/subs`; 18 29 mkdirSync(SUB_DIR, { recursive: true }); 19 30 ··· 29 40 return out; 30 41 } 31 42 32 - const FONT_BOLD = "/Users/jas/aesthetic-computer/system/public/type/webfonts/ywft-processing-bold.ttf"; 43 + const FONT_BOLD = `${REPO}/system/public/type/webfonts/ywft-processing-bold.ttf`; 33 44 const fontBoldB64 = readFileSync(FONT_BOLD).toString("base64"); 34 45 35 46 const words = JSON.parse(readFileSync(`${ROOT}/out/words.json`, "utf8")); ··· 91 102 `; 92 103 93 104 const browser = await puppeteer.launch({ 94 - executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 105 + // Use puppeteer's bundled Chromium — works on local Mac + oven Linux. 95 106 args: ["--no-sandbox"], 96 107 }); 97 108