← 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
M data/store-scripts.jsonA filmgen/approve-store-film.mjsM filmgen/heygen_batch.mjsM social/poster/caption.mjs
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 →