[object Object]

← back to Beverlyhillsvideos

stores: bake IP/copyright compliance guardrails into store-film pipeline

58e6a6f60e45b79a2b1bfca40524c11ce9651a99 · 2026-08-06 14:37:05 -0700 · Steve Abrams

Per vp-compliance-policy review (GO-WITH-CONDITIONS) — 'text anti-prompts
aren't enforceable on a generative model, must visually check output frames':
- Guardrail A: hard negative visual constraints in every store HeyGen prompt
  (no logos/monograms/trademark patterns/branded storefront/products/faces).
- Guardrail B (blocker): per-frame review HOLD gate — store films extract 1fps
  frames to filmgen/review/<slug>/ and are HELD (reviewState:pending), never
  reframed/integrated/posted, until approve-store-film.mjs clears them.
- Guardrail C: trademark/no-affiliation disclaimer in every store caption +
  store-appropriate body copy & hashtags (caption.mjs branches on type).
- Pilot narration softened to avoid naming protected product designs; pilot set
  swapped to 8 fashion-first houses (jewelers/watchmakers deferred).
No spend — nothing generated. Restaurant pipeline unchanged (guards are store-only).

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

Files touched

Diff

commit 58e6a6f60e45b79a2b1bfca40524c11ce9651a99
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 14:37:05 2026 -0700

    stores: bake IP/copyright compliance guardrails into store-film pipeline
    
    Per vp-compliance-policy review (GO-WITH-CONDITIONS) — 'text anti-prompts
    aren't enforceable on a generative model, must visually check output frames':
    - Guardrail A: hard negative visual constraints in every store HeyGen prompt
      (no logos/monograms/trademark patterns/branded storefront/products/faces).
    - Guardrail B (blocker): per-frame review HOLD gate — store films extract 1fps
      frames to filmgen/review/<slug>/ and are HELD (reviewState:pending), never
      reframed/integrated/posted, until approve-store-film.mjs clears them.
    - Guardrail C: trademark/no-affiliation disclaimer in every store caption +
      store-appropriate body copy & hashtags (caption.mjs branches on type).
    - Pilot narration softened to avoid naming protected product designs; pilot set
      swapped to 8 fashion-first houses (jewelers/watchmakers deferred).
    No spend — nothing generated. Restaurant pipeline unchanged (guards are store-only).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 data/store-scripts.json        | 10 ++++----
 filmgen/approve-store-film.mjs | 58 ++++++++++++++++++++++++++++++++++++++++++
 filmgen/heygen_batch.mjs       | 31 +++++++++++++++++++++-
 social/poster/caption.mjs      | 20 +++++++++++++--
 4 files changed, 111 insertions(+), 8 deletions(-)

diff --git a/data/store-scripts.json b/data/store-scripts.json
index 4687ce7..44d19f6 100644
--- a/data/store-scripts.json
+++ b/data/store-scripts.json
@@ -1,10 +1,10 @@
 {
   "Gucci": "On Rodeo Drive, the Gucci flagship is one of Beverly Hills' great fashion houses — Florentine heritage, bold Italian design, and, above the store, the Michelin-starred Gucci Osteria. As much a destination as a boutique.",
   "Chanel": "Chanel on Rodeo Drive — the house of Coco Chanel. Timeless tweed, the little black dress, and No. 5. One of the most storied names in fashion, in one of Beverly Hills' most photographed boutiques.",
-  "Louis Vuitton": "Louis Vuitton on Rodeo Drive — the French maison known the world over for its monogram, its trunks, and its savoir-faire. A flagship that anchors Beverly Hills' luxury shopping like few others.",
+  "Louis Vuitton": "Louis Vuitton on Rodeo Drive — the French maison known the world over for its trunks and its savoir-faire. A flagship that anchors Beverly Hills' luxury shopping like few others.",
   "Dior": "Christian Dior on Rodeo Drive — Parisian couture, the New Look, and a boutique as elegant as the house itself. A cornerstone of Beverly Hills style, season after season.",
-  "Cartier": "Cartier on Rodeo Drive — the jeweler of kings. Known for the Love bracelet, the Tank watch, and more than a century of high jewelry. One of Beverly Hills' most enduring addresses.",
-  "Tiffany & Co.": "Tiffany & Co. on Rodeo Drive — the American jeweler behind the robin's-egg blue box, the diamond, and a legacy of romance. A Beverly Hills icon for generations.",
-  "Hermès": "Hermès on Rodeo Drive — the French house of the Birkin and the Kelly, of silk scarves and saddle-stitched leather. Craftsmanship that quietly defines luxury.",
-  "Prada": "Prada on Rodeo Drive — Milanese minimalism, sharp tailoring, and a boutique that reads like a gallery. A modern pillar of Beverly Hills fashion."
+  "Prada": "Prada on Rodeo Drive — Milanese minimalism, sharp tailoring, and a boutique that reads like a gallery. A modern pillar of Beverly Hills fashion.",
+  "Hermès": "Hermès on Rodeo Drive — the French house of fine leather and silk, of quiet, exacting craftsmanship. A name that has come to define understated luxury.",
+  "Saint Laurent": "Saint Laurent on Rodeo Drive — the house of Yves Saint Laurent, known for sharp tailoring, Le Smoking, and a rock-and-roll Parisian edge. A defining name in Beverly Hills fashion.",
+  "Ralph Lauren": "Ralph Lauren on Rodeo Drive — American style at its most polished. The flagship brings the house's world of refined, heritage-minded tailoring to Beverly Hills."
 }
diff --git a/filmgen/approve-store-film.mjs b/filmgen/approve-store-film.mjs
new file mode 100644
index 0000000..b29a75d
--- /dev/null
+++ b/filmgen/approve-store-film.mjs
@@ -0,0 +1,58 @@
+#!/usr/bin/env node
+// approve-store-film.mjs — compliance review gate for store films.
+// Store films are HELD after generation (reviewState:'pending') with frames extracted to
+// filmgen/review/<slug>/. A human (or vision model) inspects those frames for any brand
+// logo/monogram/branded storefront/product/foreign face, then:
+//
+//   node filmgen/approve-store-film.mjs                 # list pending + their frame dirs
+//   node filmgen/approve-store-film.mjs approve <slug>  # clear to publish (next store batch reframes+integrates)
+//   node filmgen/approve-store-film.mjs approve all      # approve every pending (use only after reviewing all)
+//   node filmgen/approve-store-film.mjs reject  <slug>   # kill it: delete assets, mark rejected
+import { readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
+
+const ROOT = new URL('..', import.meta.url).pathname;
+const LEDGER = ROOT + 'data/heygen-batch-store.jsonl';
+const VIDEO_DIR = ROOT + 'public/video';
+const REVIEW_DIR = ROOT + 'filmgen/review';
+
+const jget = (s) => { try { return JSON.parse(s); } catch { return null; } };
+function load() {
+  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 save(rows) { writeFileSync(LEDGER, Object.values(rows).map((r) => JSON.stringify(r)).join('\n') + '\n'); }
+
+const [cmd, arg] = process.argv.slice(2);
+const rows = load();
+const pending = Object.values(rows).filter((r) => r.reviewState === 'pending');
+
+if (!cmd || cmd === 'list') {
+  if (!pending.length) { console.log('No store films pending review.'); process.exit(0); }
+  console.log(`${pending.length} store film(s) pending compliance review:\n`);
+  for (const r of pending) console.log(`  ${r.slug.padEnd(24)} frames → filmgen/review/${r.slug}/`);
+  console.log('\nInspect frames for brand logos/monograms/branded storefront/product/foreign faces.');
+  console.log('Then: node filmgen/approve-store-film.mjs approve <slug>   (or reject <slug>)');
+  process.exit(0);
+}
+
+const targets = arg === 'all' ? pending.map((r) => r.slug) : [arg];
+if (!arg) { console.error('Usage: approve|reject <slug|all>'); process.exit(1); }
+
+for (const sl of targets) {
+  const r = Object.values(rows).find((x) => x.slug === sl);
+  if (!r) { console.error(`  ✗ no ledger row for slug: ${sl}`); continue; }
+  if (cmd === 'approve') {
+    r.reviewState = 'approved';
+    console.log(`  ✓ approved ${sl} — next store batch run will reframe + integrate it`);
+  } else if (cmd === 'reject') {
+    r.reviewState = 'rejected';
+    for (const p of [`${VIDEO_DIR}/${sl}.mp4`, `${VIDEO_DIR}/${sl}.jpg`, `${REVIEW_DIR}/${sl}`]) {
+      try { rmSync(p, { recursive: true, force: true }); } catch {}
+    }
+    console.log(`  ✗ rejected ${sl} — assets deleted, will not publish`);
+  } else {
+    console.error(`unknown command: ${cmd}`); process.exit(1);
+  }
+}
+save(rows);
diff --git a/filmgen/heygen_batch.mjs b/filmgen/heygen_batch.mjs
index c9e591e..c0bf05c 100644
--- a/filmgen/heygen_batch.mjs
+++ b/filmgen/heygen_batch.mjs
@@ -86,6 +86,16 @@ function leadStart(raw) {
   } catch {}
   return 0;
 }
+const REVIEW_DIR = ROOT + 'filmgen/review';
+// Extract 1 fps of frames so a human (or vision model) can confirm no brand logo,
+// monogram, branded storefront/product, or foreign face was rendered before publish.
+function extractReviewFrames(mp4, slug) {
+  const dir = `${REVIEW_DIR}/${slug}`;
+  try { mkdirSync(dir, { recursive: true }); } catch {}
+  try { sh(`ffmpeg -y -i ${mp4} -vf fps=1 -q:v 3 ${dir}/frame-%02d.jpg 2>/dev/null`); } catch {}
+  return dir;
+}
+
 function download(r) {
   if (!r.slug) r.slug = slug(r.name);          // ledger rows predating the driver lack a slug
   const raw = `${VIDEO_DIR}/${r.slug}-raw.mp4`;
@@ -96,6 +106,18 @@ function download(r) {
   sh(`ffmpeg -y -ss ${start} -i ${raw} -t 29 -c:v libx264 -preset medium -crf 20 -c:a aac -b:a 128k -movflags +faststart ${mp4} 2>/dev/null`);
   try { sh(`rm -f ${raw}`); } catch {}
   try { sh(`ffmpeg -y -ss 1 -i ${mp4} -vframes 1 -q:v 3 ${VIDEO_DIR}/${r.slug}.jpg 2>/dev/null`); } catch {}
+
+  // COMPLIANCE GATE (store kind): hold the film for a per-frame logo/mark review before
+  // it reaches the social ratios or store-films.json. Reframe + integrate only after a
+  // human/vision approval flips reviewState to 'approved' (filmgen/approve-store-film.mjs).
+  if (KIND === 'store' && r.reviewState !== 'approved') {
+    const dir = extractReviewFrames(mp4, r.slug);
+    r.reviewState = 'pending';
+    r.mp4Ready = true;      // mp4 exists; NOT reframed, NOT downloaded, NOT published
+    console.log(`  ⏸ HELD for review: ${r.name} — frames in ${dir.replace(ROOT,'')}`);
+    return;
+  }
+
   // social-media aspect ratios (vertical 9:16, portrait 4:5, square 1:1), from the trimmed film
   for (const s of SOCIAL) {
     try { reframe(mp4, s.w, s.h, `${VIDEO_DIR}/${s.dir}/${r.slug}.mp4`); } catch {}
@@ -113,6 +135,7 @@ function main() {
     const r = rows[name];
     if (!r || !r.video_id) continue;
     if (r.status === 'completed' && r.downloaded) continue;
+    if (r.reviewState === 'pending') continue;   // held for compliance review — don't reprocess
     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;
@@ -131,7 +154,13 @@ function main() {
     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 ${KIND_LABEL} ${name}. Use EXACTLY this narration and do not lengthen it: "${script}". Elegant, warm, upscale editorial tone. Landscape 16:9.`;
+    // Store films name real brands → hard visual constraints so HeyGen doesn't render
+    // trademarked marks/trade-dress into b-roll. NOTE: these are advisory to the model,
+    // NOT enforceable — the per-frame review gate in download() is the real backstop.
+    const NEG = KIND === 'store'
+      ? ' Show the store name ONLY as plain elegant serif typography — never a stylized brand logotype. Do NOT render any brand logo, monogram, wordmark, emblem, crest, crown, or trademarked repeating pattern (no LV monogram, no interlocking-C, no GG, no Burberry check). Do NOT render recognizable brand products, packaging, or the real branded storefront/facade/interior. Backgrounds: GENERIC upscale Beverly Hills retail street and neutral studio only. No third-party faces or celebrity likenesses.'
+      : '';
+    const prompt = `Create a SHORT ~25 second editorial spotlight video for a Beverly Hills city-guide website about the ${KIND_LABEL} ${name}. Use EXACTLY this narration and do not lengthen it: "${script}". Elegant, warm, upscale editorial tone. Landscape 16:9.${NEG}`;
     try {
       const out = jget(sh(`heygen video-agent create --orientation landscape --prompt ${JSON.stringify(prompt)} 2>/dev/null`));
       const d = out.data || out;
diff --git a/social/poster/caption.mjs b/social/poster/caption.mjs
index 0449b94..3b5f4c7 100644
--- a/social/poster/caption.mjs
+++ b/social/poster/caption.mjs
@@ -22,14 +22,30 @@ function rotate(pool, n, seed) {
   return out;
 }
 
+// Store films name real brands, so every store caption carries a trademark /
+// no-affiliation disclaimer (compliance guardrail C, TK-10309).
+const STORE_DISCLAIMER =
+  'Beverly Hills Videos is an independent editorial guide — not affiliated with, sponsored by, or endorsed by the brands shown. Trademarks belong to their respective owners.';
+const SHOPPING = [
+  '#luxuryshopping', '#rodeodrivestyle', '#designerfashion', '#luxurylifestyle',
+  '#beverlyhillsshopping', '#hautecouture', '#luxurybrands', '#shoppingdistrict',
+  '#luxurytravel', '#cityguide', '#style', '#fashion', '#visitbeverlyhills', '#cinematic',
+];
+
 export function buildCaption(film, seedIndex = 0) {
+  const isStore = film.type === 'store';
   const blurb = (film.blurb || '').trim().replace(/\s+/g, ' ');
   const hook = blurb ? `${film.name}. ${blurb}` : `${film.name} — Beverly Hills, in film.`;
-  const tags = [...CORE, ...rotate(DINING, 11, seedIndex)];
+  const pool = isStore ? SHOPPING : DINING;
+  const tags = [...CORE, ...rotate(pool, 11, seedIndex)];
+  const line = isStore
+    ? 'A short film from Beverly Hills Videos — the insider’s guide to Rodeo Drive & Beverly Hills style. Explore the guide → link in bio.'
+    : 'A short film from Beverly Hills Videos — the insider’s guide to the city’s finest dining. Full guide, menu & interactive map → link in bio.';
   const body = [
     hook,
     '',
-    'A short film from Beverly Hills Videos — the insider’s guide to the city’s finest dining. Full guide, menu & interactive map → link in bio.',
+    line,
+    ...(isStore ? ['', STORE_DISCLAIMER] : []),
     '',
     tags.join(' '),
   ].join('\n');

← fbefcdb stores: Rodeo Drive store-film pipeline (typed stream, resta  ·  back to Beverlyhillsvideos  ·  stores: animated store-card renderer (staggered reveals + em c5e7566 →