Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

at main 200 lines 8.0 kB view raw
1#!/usr/bin/env node 2// jeffrey-photos.mjs — pre-pipeline step. For each segment in the audience 3// config that has a `metaphor` field, call OpenAI gpt-image-2 (images.edit) 4// with the platter SHOOT_REFS + SELFIE_REFS for identity grounding and save 5// the result to recap/out/jeffrey-photos/<segment>.png. 6// 7// Each successful generation is also archived into the jeffrey platter as a 8// new entry in the `gens` bucket: a dated copy of the PNG goes to 9// system/public/assets/jeffreys/gens/<context>-<segment>-<ts>.png (synced to 10// the CDN by `npm run assets:sync:up`), and an item with full provenance 11// (model, refs, prompt, context, segment, timestamp) is appended to 12// papers/jeffrey-platter/manifest.json under buckets.gens.items. 13// 14// Caching: skip if the working pipeline cache already exists; pass --force to 15// regen. Run a single segment by name: --only 02_menuband. 16// 17// Usage: 18// node bin/jeffrey-photos.mjs jeffrey-24h 19// node bin/jeffrey-photos.mjs jeffrey-24h --force 20// node bin/jeffrey-photos.mjs jeffrey-24h --only 04_platter 21 22import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from "node:fs"; 23import { resolve, dirname } from "node:path"; 24import { fileURLToPath } from "node:url"; 25 26const HERE = dirname(fileURLToPath(import.meta.url)); 27const ROOT = resolve(HERE, ".."); 28const REPO = resolve(ROOT, ".."); 29 30const audienceName = process.argv[2] || "jeffrey-24h"; 31const force = process.argv.includes("--force"); 32const onlyIdx = process.argv.indexOf("--only"); 33const only = onlyIdx >= 0 ? process.argv[onlyIdx + 1] : null; 34 35const { audience } = await import(`${ROOT}/audience/${audienceName}.mjs`); 36 37const PHOTOS_DIR = `${ROOT}/out/jeffrey-photos`; 38mkdirSync(PHOTOS_DIR, { recursive: true }); 39 40// Platter archive — a copy of every successful gen goes here, plus a manifest 41// entry. The dir is synced to assets.aesthetic.computer/jeffreys/gens/ via 42// `npm run assets:sync:up`. 43const PLATTER_GENS_DIR = `${REPO}/system/public/assets/jeffreys/gens`; 44const PLATTER_MANIFEST = `${REPO}/papers/jeffrey-platter/manifest.json`; 45mkdirSync(PLATTER_GENS_DIR, { recursive: true }); 46 47// Mirrors generate-neo.py refs. 48const SHOOT_DIR = `${REPO}/portraits/jeffrey/corpus/shoot`; 49const ARCHIVE_DIR = `${REPO}/portraits/jeffrey/ig-archive/whistlegraph`; 50const SHOOT_REFS = [ 51 `${SHOOT_DIR}/jeffery-av--07.jpg`, 52 `${SHOOT_DIR}/jeffery-av--01.jpg`, 53 `${SHOOT_DIR}/jeffery-av--04.jpg`, 54]; 55const SELFIE_REFS = [ 56 `${ARCHIVE_DIR}/2018-12-02_Bq4ckGFFNtW.jpg`, 57 `${ARCHIVE_DIR}/2020-09-02_CEpxlO2FOvD.jpg`, 58 `${ARCHIVE_DIR}/2021-07-10_CRI095Vl7AO_1.jpg`, 59 `${ARCHIVE_DIR}/2025-01-25_DFQ2lHPzN_W.jpg`, 60 `${ARCHIVE_DIR}/2017-04-10_BStid5yjTHq.jpg`, 61]; 62const REFS = [...SHOOT_REFS, ...SELFIE_REFS].filter((p) => { 63 if (existsSync(p)) return true; 64 console.warn(` ⚠ ref missing, dropping: ${p}`); 65 return false; 66}); 67 68function loadOpenAIKey() { 69 if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY; 70 const vault = `${REPO}/aesthetic-computer-vault/.devcontainer/envs/devcontainer.env`; 71 if (existsSync(vault)) { 72 for (const line of readFileSync(vault, "utf8").split("\n")) { 73 if (line.startsWith("OPENAI_API_KEY=")) { 74 return line.slice("OPENAI_API_KEY=".length).trim().replace(/^['"]|['"]$/g, ""); 75 } 76 } 77 } 78 throw new Error("OPENAI_API_KEY not set and not found in vault devcontainer.env"); 79} 80 81const apiKey = loadOpenAIKey(); 82 83const MODEL = "gpt-image-2"; 84const SIZE = "1024x1536"; 85const QUALITY = "high"; 86 87async function generate(metaphor, outPath) { 88 const fd = new FormData(); 89 fd.append("model", MODEL); 90 fd.append("prompt", metaphor); 91 fd.append("size", SIZE); 92 fd.append("quality", QUALITY); 93 fd.append("n", "1"); 94 for (const ref of REFS) { 95 const buf = readFileSync(ref); 96 const ext = ref.toLowerCase().endsWith(".png") ? "png" : "jpeg"; 97 fd.append("image[]", new Blob([buf], { type: `image/${ext}` }), ref.split("/").pop()); 98 } 99 const res = await fetch("https://api.openai.com/v1/images/edits", { 100 method: "POST", 101 headers: { Authorization: `Bearer ${apiKey}` }, 102 body: fd, 103 }); 104 if (!res.ok) { 105 const err = await res.text(); 106 throw new Error(`OpenAI ${res.status}: ${err.slice(0, 500)}`); 107 } 108 const json = await res.json(); 109 const b64 = json.data?.[0]?.b64_json; 110 if (!b64) throw new Error(`no image returned: ${JSON.stringify(json).slice(0, 200)}`); 111 writeFileSync(outPath, Buffer.from(b64, "base64")); 112 const usage = json.usage || {}; 113 return { tokens_in: usage.input_tokens, tokens_out: usage.output_tokens }; 114} 115 116function isoStamp() { 117 const d = new Date(); 118 const pad = (n) => String(n).padStart(2, "0"); 119 return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}${pad(d.getMinutes())}`; 120} 121 122// Archive a successful gen into the platter: copy PNG to the assets/gens dir 123// + append a metadata entry to papers/jeffrey-platter/manifest.json. Lazy- 124// initializes the `gens` bucket if it doesn't exist yet. 125function archiveToPlatter({ segName, metaphor, sourcePath, context }) { 126 const stamp = isoStamp(); 127 const archiveName = `${context}-${segName}-${stamp}.png`; 128 const archivePath = `${PLATTER_GENS_DIR}/${archiveName}`; 129 writeFileSync(archivePath, readFileSync(sourcePath)); 130 131 const manifest = JSON.parse(readFileSync(PLATTER_MANIFEST, "utf8")); 132 manifest.buckets.gens ??= { 133 label: "Generated images — gpt-image-2 with platter-grounded identity (real+goofy default tone). One PNG per successful gen, dated. Synced to assets CDN via `npm run assets:sync:up`.", 134 url_pattern: "https://assets.aesthetic.computer/jeffreys/gens/{name}", 135 key_includes_extension: true, 136 items: {}, 137 }; 138 manifest.buckets.gens.items[archiveName] = { 139 model: MODEL, 140 size: SIZE, 141 quality: QUALITY, 142 refs: REFS.map((r) => r.replace(REPO + "/", "")), 143 context, 144 segment: segName, 145 generated: new Date().toISOString(), 146 bytes: statSync(archivePath).size, 147 prompt: metaphor, 148 }; 149 writeFileSync(PLATTER_MANIFEST, JSON.stringify(manifest, null, 2) + "\n"); 150 return { archiveName, archivePath }; 151} 152 153const context = `recap-${audienceName}`; 154 155console.log(`refs: ${REFS.length} (${REFS.length} found)`); 156console.log(`out: ${PHOTOS_DIR}`); 157console.log(`platter archive: ${PLATTER_GENS_DIR.replace(REPO + "/", "")}`); 158 159let generated = 0, cached = 0, failed = 0; 160for (const seg of audience.segments) { 161 if (only && seg.name !== only) continue; 162 const slide = audience.slides[seg.name]; 163 const metaphor = slide && typeof slide === "object" ? slide.metaphor : null; 164 if (!metaphor) { 165 console.log(` · ${seg.name}: no metaphor, skipping`); 166 continue; 167 } 168 const outPath = `${PHOTOS_DIR}/${seg.name}.png`; 169 if (existsSync(outPath) && !force) { 170 console.log(`${seg.name}.png (cached)`); 171 cached++; 172 continue; 173 } 174 process.stdout.write(`${seg.name}`); 175 const t0 = Date.now(); 176 try { 177 const usage = await generate(metaphor, outPath); 178 const archive = archiveToPlatter({ segName: seg.name, metaphor, sourcePath: outPath, context }); 179 const elapsed = ((Date.now() - t0) / 1000).toFixed(1); 180 const tok = usage.tokens_in 181 ? ` · tokens in=${usage.tokens_in} out=${usage.tokens_out}` 182 : ""; 183 console.log(`${elapsed}s${tok}${archive.archiveName}`); 184 generated++; 185 } catch (e) { 186 console.log(``); 187 console.error(` ${e.message}`); 188 failed++; 189 } 190} 191 192console.log(`✓ photos: ${generated} new, ${cached} cached, ${failed} failed`); 193if (generated > 0) { 194 console.log(` · platter manifest updated: papers/jeffrey-platter/manifest.json`); 195 console.log(` · run \`node papers/jeffrey-platter/sync.mjs\` to refresh consumer copy`); 196 console.log(` · run \`npm run assets:sync:up\` to push gens/ to the CDN`); 197} 198// Don't fail the pipeline on per-segment gen failures — slides fall back to a 199// dark-bg placeholder when the glob matches nothing. Re-run later to retry 200// just the missing photos.