Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

recap/bin/tts.mjs: skip /api/say if narration hash matches cached recap.mp3

ElevenLabs (via the /api/say proxy) is the only real-money step in the
recap pipeline besides photo gen. Hashing the narration text + voice
config + checking out/recap.mp3.hash lets reruns skip the API hit when
nothing changed. --force overrides.

This unblocks $0 reruns of an episode (audio + photos already cached).
First step toward the broader ac-24 CLI cache layer (see memory note
feedback_recap_ac24_cli.md).

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

+36 -5
+36 -5
recap/bin/tts.mjs
··· 1 1 #!/usr/bin/env node 2 2 // tts.mjs — POST audience narration to /api/say, save MP3 to out/recap.mp3. 3 - // Usage: node bin/tts.mjs [audience-name] (default: fia) 3 + // 4 + // Caching: keyed on a content hash of (narration text + voice provider + 5 + // voice id). If `out/recap.mp3` exists AND `out/recap.mp3.hash` matches 6 + // the current input hash, skip the /api/say call entirely (which costs 7 + // real money via ElevenLabs proxying). Pass `--force` to bypass. 8 + // 9 + // Usage: 10 + // node bin/tts.mjs [audience-name] (default: fia) 11 + // node bin/tts.mjs jeffrey-73h-2026-05-02 --force 4 12 5 - import { writeFileSync } from "node:fs"; 13 + import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs"; 6 14 import { resolve, dirname } from "node:path"; 7 15 import { fileURLToPath } from "node:url"; 16 + import { createHash } from "node:crypto"; 8 17 9 18 const HERE = dirname(fileURLToPath(import.meta.url)); 10 19 const ROOT = resolve(HERE, ".."); 11 - const audienceName = process.argv[2] || "fia"; 20 + const argv = process.argv.slice(2); 21 + const force = argv.includes("--force"); 22 + const audienceName = argv.find((a) => !a.startsWith("--")) || "fia"; 12 23 13 24 const { audience } = await import(`${ROOT}/audience/${audienceName}.mjs`); 14 25 ··· 18 29 voice: audience.voice.voice, 19 30 }; 20 31 32 + const inputHash = createHash("sha256") 33 + .update(JSON.stringify(body)) 34 + .digest("hex") 35 + .slice(0, 16); 36 + 37 + const out = `${ROOT}/out/recap.mp3`; 38 + const hashFile = `${out}.hash`; 39 + mkdirSync(`${ROOT}/out`, { recursive: true }); 40 + 41 + // Skip if cached output matches the current input hash. ElevenLabs is 42 + // the only real-money step here, so skipping saves ~$ on every rerun. 43 + if (!force && existsSync(out) && existsSync(hashFile)) { 44 + const cached = readFileSync(hashFile, "utf8").trim(); 45 + if (cached === inputHash) { 46 + const size = (readFileSync(out).length / 1024).toFixed(0); 47 + console.log(`✓ ${out} cached (${size} KB · hash ${inputHash}) — skipping /api/say`); 48 + process.exit(0); 49 + } 50 + } 51 + 21 52 console.log(`→ POST /api/say · ${audience.narration.length} chars · ${audience.voice.provider}`); 22 53 const res = await fetch("https://aesthetic.computer/api/say", { 23 54 method: "POST", ··· 32 63 } 33 64 34 65 const buf = Buffer.from(await res.arrayBuffer()); 35 - const out = `${ROOT}/out/recap.mp3`; 36 66 writeFileSync(out, buf); 37 - console.log(`✓ ${out} (${(buf.length / 1024).toFixed(0)} KB)`); 67 + writeFileSync(hashFile, inputHash + "\n"); 68 + console.log(`✓ ${out} (${(buf.length / 1024).toFixed(0)} KB · hash ${inputHash})`);