← back to Beverlyhillsvideos
filmgen/approve-store-film.mjs
59 lines
#!/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);