[object Object]

← back to Beverlyhillsvideos

auto-save: 2026-08-05T15:13:13 (5 files) — build.mjs data/heygen-batch.jsonl data/film-scripts.json data/films.json filmgen/heygen_batch.mjs

b1d42b77288450737ffc5094ac7fb9f16b184816 · 2026-08-05 15:13:18 -0700 · Steve Abrams

Files touched

Diff

commit b1d42b77288450737ffc5094ac7fb9f16b184816
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 5 15:13:18 2026 -0700

    auto-save: 2026-08-05T15:13:13 (5 files) — build.mjs data/heygen-batch.jsonl data/film-scripts.json data/films.json filmgen/heygen_batch.mjs
---
 build.mjs                |   7 ++-
 data/film-scripts.json   |  18 ++++++++
 data/films.json          |   9 ++++
 data/heygen-batch.jsonl  |   1 +
 filmgen/heygen_batch.mjs | 117 +++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 148 insertions(+), 4 deletions(-)

diff --git a/build.mjs b/build.mjs
index a9d2136..481c2d5 100644
--- a/build.mjs
+++ b/build.mjs
@@ -281,10 +281,9 @@ for (const p of POSTS) {
 }
 
 // ---- original restaurant films (HeyGen-produced presenter videos, self-hosted) ----
-const FILMS = [
-  { name: 'Spago Beverly Hills', src: '/video/spago.mp4', poster: '/video/spago.jpg',
-    blurb: 'Wolfgang Puck’s flagship on Canon Drive — the restaurant that helped define California fine dining.' },
-];
+// Manifest is written by filmgen/heygen_batch.mjs as each film completes.
+let FILMS = [];
+try { FILMS = JSON.parse(readFileSync('data/films.json', 'utf8')); } catch {}
 
 // ---- videos page (real, verified BH local-business embeds) ----
 const videosBody = `
diff --git a/data/film-scripts.json b/data/film-scripts.json
new file mode 100644
index 0000000..56be03d
--- /dev/null
+++ b/data/film-scripts.json
@@ -0,0 +1,18 @@
+{
+  "Funke": "Tucked along South Santa Monica Boulevard, Funke is chef Evan Funke's temple to handmade pasta. Behind glass, cooks shape every noodle by hand, the old way, no machines. The room is warm, buzzing, unmistakably Beverly Hills. Come for the pasta tasting, and one of the city's most coveted tables.",
+  "Mr. Chow": "On North Camden Drive, Mr. Chow has been Beverly Hills' theater of Beijing cuisine since 1974. Green-prawn dumplings, noodles pulled by hand tableside, and a celebrity crowd beneath a stark black-and-white room. It's dinner as spectacle, glamorous and timeless, unmistakably Beverly Hills.",
+  "Il Pastaio": "At the corner of Canon and Brighton, Il Pastaio is Beverly Hills' beloved sidewalk trattoria. Celestino Drago's risottos and handmade pastas draw a devoted crowd to its sunny patio. Come early, the tables fill fast, and the people-watching is as rich as the food.",
+  "Matsuhisa": "On La Cienega, Matsuhisa is where Nobu Matsuhisa changed sushi forever. His Peruvian-inflected Japanese cooking, black cod miso, yellowtail jalapeño, launched a global empire from this modest Beverly Hills room. Sit at the bar, order omakase; it remains one of the great meals in Los Angeles.",
+  "Lawry's The Prime Rib": "Since 1938, Lawry's The Prime Rib has carved its legend tableside from gleaming silver carts. On La Cienega, this Beverly Hills institution serves one thing exquisitely, roasted prime rib, spinning salad and all. It's old-Hollywood ceremony, unchanged and unbeatable, for generations of Angelenos.",
+  "Avra Beverly Hills": "On North Beverly Drive, Avra brings the Greek islands to Beverly Hills. Whole fresh fish displayed on ice, grilled simply over coals, served in a sun-washed, taverna-elegant room. It's coastal Mediterranean dining at its most glamorous, a favorite of the Golden Triangle.",
+  "The Grill on the Alley": "Tucked off Dayton Way, The Grill on the Alley is Beverly Hills' quintessential power-lunch table. Since 1984, agents and executives have sealed deals over its martinis, Cobb salads, and classic American chops. Clubby, timeless, and always buzzing, this is where Hollywood does business.",
+  "E. Baldi": "On Canon Drive, E. Baldi is Beverly Hills' insider Italian, run by Giorgio Baldi's son Edoardo. An intimate room of white tablecloths and simple, perfect pastas, the lemon spaghetti is legendary. Reservations are gold, the regulars famous. It's understated luxury, done the Italian way.",
+  "La Scala": "A Beverly Hills institution since 1956, La Scala on Canon Drive is home of the original chopped salad. Red leather booths, a see-and-be-seen crowd, and generations of regulars who order the same beloved dishes. It's classic Hollywood Italian, nostalgic, buzzy, and endlessly charming.",
+  "Nate 'n Al's": "On Beverly Drive since 1945, Nate 'n Al's is the beloved deli of Beverly Hills. Towering pastrami, matzo ball soup, and a counter where studio moguls and locals have shared booths for eighty years. It's unpretentious, iconic, and pure old Beverly Hills comfort.",
+  "Wally's": "On Canon Drive, Wally's pairs a world-class wine list with polished market cuisine. What began as a legendary wine shop is now Beverly Hills' favorite bar and restaurant, thousands of bottles, a buzzing patio, and a crowd that knows its Burgundy. Come for the wine, stay for everything.",
+  "The Polo Lounge": "Inside the pink-and-green Beverly Hills Hotel, the Polo Lounge is old Hollywood itself. Since 1941, deals and romances have unfolded on its garden patio over the famous McCarthy salad. It's the most storied room in Beverly Hills, timeless glamour, still humming day and night.",
+  "The Belvedere": "At the Peninsula Beverly Hills, The Belvedere is refined hotel dining at its finest. A jewel-box room and a garden terrace set the stage for polished California cuisine and one of the city's great power breakfasts. Elegant, discreet, and quietly perfect, Beverly Hills hospitality distilled.",
+  "Steak 48": "On Canon Drive, Steak 48 brings modern steakhouse energy to Beverly Hills. An open kitchen, seafood towers on ice, and prime cuts in a dark, glamorous, always-packed room. It's a see-and-be-seen dinner destination, polished and buzzing, built for a night in the Golden Triangle.",
+  "Cipriani": "The Venetian legend arrives in Beverly Hills. Cipriani brings its Bellinis, its carpaccio, and its white-jacketed service to the Golden Triangle. Impossibly chic, effortlessly glamorous, the same timeless menu that made Harry's Bar famous, now one of the city's hardest tables to land.",
+  "Il Cielo": "On Burton Way, Il Cielo is Beverly Hills' most romantic dinner. A twinkling garden of fairy lights and blooming vines sets the stage for Northern Italian cooking. It's the city's favorite proposal spot, enchanting and intimate, unlike anywhere else in the Golden Triangle."
+}
diff --git a/data/films.json b/data/films.json
new file mode 100644
index 0000000..d95d6c0
--- /dev/null
+++ b/data/films.json
@@ -0,0 +1,9 @@
+[
+ {
+  "name": "Spago Beverly Hills",
+  "slug": "spago",
+  "blurb": "Wolfgang Puck’s flagship on Canon Drive — the restaurant that helped define California fine dining.",
+  "src": "/video/spago.mp4",
+  "poster": "/video/spago.jpg"
+ }
+]
\ No newline at end of file
diff --git a/data/heygen-batch.jsonl b/data/heygen-batch.jsonl
index 1b6e24f..5f0de75 100644
--- a/data/heygen-batch.jsonl
+++ b/data/heygen-batch.jsonl
@@ -1,3 +1,4 @@
 {"name":"Spago Beverly Hills","session_id":"27ab0fe429764112a47decaf1e42f22e","video_id":"eb53be3269ed4579865a0a38b5f2335c","status":"completed","integrated":true}
 {"name":"CUT by Wolfgang Puck","session_id":"df26bc4723474c11a2b3ec08d74909d2","video_id":"7b5ca4de01d44f9ea156c46c8deb1d13","status":"generating"}
 {"name":"Mastro's Steakhouse Beverly Hills","session_id":"fa9ff2f7f5444adb98e765d18d1056f5","video_id":"e923ad16427345e7ad84f0826c3544b6","status":"generating"}
+{"name":"Funke","slug":"funke","session_id":"05de31a9968241ef904cf38a9a082ce7","video_id":"32d03f1f16eb462aab10acdc3e96c4e4","status":"pending","downloaded":false}
diff --git a/filmgen/heygen_batch.mjs b/filmgen/heygen_batch.mjs
new file mode 100644
index 0000000..c6f4b0a
--- /dev/null
+++ b/filmgen/heygen_batch.mjs
@@ -0,0 +1,117 @@
+#!/usr/bin/env node
+// Self-driving HeyGen restaurant-film batch.
+// Idempotent: reconciles data/heygen-batch.jsonl each run. Keeps <=CONCURRENCY in flight,
+// downloads completed videos to public/video/, updates data/films.json for the site build.
+// Cost guard: never submits if wallet < FLOOR. Every script is ~22s -> <$1/video.
+import { execSync } from 'child_process';
+import { readFileSync, writeFileSync, existsSync } from 'fs';
+
+const ROOT = new URL('..', import.meta.url).pathname;
+const LEDGER = ROOT + 'data/heygen-batch.jsonl';
+const SCRIPTS = ROOT + 'data/film-scripts.json';
+const FILMS = ROOT + 'data/films.json';
+const LOCS = ROOT + 'data/map-locations.json';
+const CONCURRENCY = 3;
+const FLOOR = 150;              // stop submitting if wallet drops below this
+const RATE = 0.0337;           // $/sec, measured from wallet delta
+const VIDEO_DIR = ROOT + 'public/video';
+
+const sh = (c) => execSync(c, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
+const slug = (n) => n.toLowerCase().replace(/&/g,'and').replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
+const jget = (s) => { try { return JSON.parse(s); } catch { return null; } };
+
+function wallet() {
+  try { return jget(sh('heygen auth status 2>/dev/null')).data.wallet.remaining_balance; }
+  catch { return null; }
+}
+function videoStatus(vid) {
+  try { const d = jget(sh(`heygen video get ${vid} 2>/dev/null`)).data; return { status: d.status, duration: d.duration }; }
+  catch { return { status: 'unknown' }; }
+}
+function loadLedger() {
+  const rows = {};
+  if (existsSync(LEDGER)) for (const ln of readFileSync(LEDGER,'utf8').trim().split('\n')) { const r = jget(ln); if (r) rows[r.name] = r; }
+  return rows;
+}
+function saveLedger(rows) {
+  writeFileSync(LEDGER, Object.values(rows).map(r => JSON.stringify(r)).join('\n') + '\n');
+}
+const cats = existsSync(LOCS) ? Object.fromEntries(jget(readFileSync(LOCS,'utf8')).locations.filter(l=>l.type==='restaurant').map(l=>[l.name.replace(' Beverly Hills',''), l.category])) : {};
+function catFor(name){ return cats[name] || cats[name.replace(' Beverly Hills','')] || 'Beverly Hills'; }
+
+function updateFilms(rows, scripts) {
+  const films = existsSync(FILMS) ? jget(readFileSync(FILMS,'utf8')) : [];
+  const bySlug = Object.fromEntries(films.map(f=>[f.slug,f]));
+  for (const r of Object.values(rows)) {
+    if (r.status === 'completed' && r.downloaded && !bySlug[r.slug]) {
+      const first = (scripts[r.name]||'').split('. ')[0];
+      const blurb = first ? (first.endsWith('.')?first:first+'.') : catFor(r.name);
+      films.push({ name: r.name, slug: r.slug, blurb, src: `/video/${r.slug}.mp4`, poster: `/video/${r.slug}.jpg` });
+    }
+  }
+  writeFileSync(FILMS, JSON.stringify(films, null, 1));
+  return films.length;
+}
+
+function download(r) {
+  const mp4 = `${VIDEO_DIR}/${r.slug}.mp4`;
+  sh(`heygen video download ${r.video_id} --output-path ${mp4} --force`);
+  try { sh(`ffmpeg -y -ss 2 -i ${mp4} -vframes 1 -q:v 3 ${VIDEO_DIR}/${r.slug}.jpg 2>/dev/null`); } catch {}
+  r.downloaded = true;
+}
+
+function main() {
+  const scripts = jget(readFileSync(SCRIPTS,'utf8'));
+  const rows = loadLedger();
+  const w0 = wallet();
+
+  // 1) POLL: advance generating rows; download completed
+  for (const name of Object.keys(scripts)) {
+    const r = rows[name];
+    if (!r || !r.video_id) continue;
+    if (r.status === 'completed' && r.downloaded) continue;
+    const s = videoStatus(r.video_id);
+    if (s.status && s.status !== 'unknown') r.status = s.status;
+    if (s.duration) r.duration = Math.round(s.duration*10)/10;
+    if (r.status === 'completed' && !r.downloaded) {
+      try { download(r); r.est_cost = r.duration ? +(r.duration*RATE).toFixed(2) : null; console.log(`  ⬇ downloaded ${name} (${r.duration}s ~$${r.est_cost})`); }
+      catch(e){ console.log(`  ⚠ download failed ${name}: ${String(e).slice(0,80)}`); }
+    }
+  }
+
+  // 2) SUBMIT: keep CONCURRENCY in flight
+  const inflight = () => Object.values(rows).filter(r => ['generating','pending','processing','waiting'].includes(r.status)).length;
+  const queue = Object.keys(scripts).filter(n => !rows[n] || !rows[n].video_id);
+  let submitted = 0;
+  const wnow = wallet();
+  for (const name of queue) {
+    if (inflight() >= CONCURRENCY) break;
+    if (wnow !== null && wnow < FLOOR) { console.log(`  ⛔ wallet $${wnow} < floor $${FLOOR} — not submitting`); break; }
+    const script = scripts[name];
+    const prompt = `Create a SHORT ~25 second editorial spotlight video for a Beverly Hills city-guide website about the restaurant ${name}. Use EXACTLY this narration and do not lengthen it: "${script}". Elegant, warm, upscale editorial tone. Landscape 16:9.`;
+    try {
+      const out = jget(sh(`heygen video-agent create --orientation landscape --prompt ${JSON.stringify(prompt)} 2>/dev/null`));
+      const d = out.data || out;
+      rows[name] = { name, slug: slug(name), session_id: d.session_id || d.id, video_id: d.video_id, status: d.status || 'generating', downloaded: false };
+      submitted++; console.log(`  ▶ submitted ${name} (video ${rows[name].video_id})`);
+    } catch(e){ console.log(`  ✗ submit failed ${name}: ${String(e).slice(0,100)}`); }
+  }
+
+  saveLedger(rows);
+  const filmCount = updateFilms(rows, scripts);
+
+  // 3) SUMMARY
+  const by = (s) => Object.values(rows).filter(r => scripts[r.name] && r.status===s).length;
+  const done = Object.values(rows).filter(r => scripts[r.name] && r.status==='completed').length;
+  const total = Object.keys(scripts).length;
+  console.log('─'.repeat(48));
+  console.log(`films batch: ${done}/${total} completed · ${inflight()} in flight · ${queue.length-submitted} queued`);
+  console.log(`downloaded+integrated: ${Object.values(rows).filter(r=>scripts[r.name]&&r.downloaded).length} · films.json entries: ${filmCount}`);
+  console.log(`wallet: $${wnow}  (pre-batch marker in filmgen/work/wallet_pre.txt)`);
+  const pre = existsSync(ROOT+'filmgen/work/wallet_pre.txt') ? +readFileSync(ROOT+'filmgen/work/wallet_pre.txt','utf8').trim() : null;
+  if (pre && wnow!==null) console.log(`measured batch spend so far: $${(pre-wnow).toFixed(2)}`);
+  const allDone = done === total;
+  console.log(allDone ? 'STATUS: ALL DONE ✅' : 'STATUS: in progress — re-run to advance');
+  process.exit(allDone ? 0 : 2);
+}
+main();

← 94d6025 map: add verified current-menu links to 19 restaurants (View  ·  back to Beverlyhillsvideos  ·  films: self-driving HeyGen restaurant-film batch (16 short s 26ef052 →