feat(remotion): asset-library catalog + Phase 0 (license firewall, @remotion/lottie, 30 CC0 characters)

- docs/ASSET_LIBRARY.md: curated catalog from the asset sweep (91 sources -> 62
  usable) + completeness-critic reality check; clean CC0/MIT tier, license/geo
  traps, and the 2.5D layered-scene plan (sky->room->furniture->device->character
  ->grain) to fix the "naked scene".
- deps: add @remotion/lottie@4.0.290 (runtime) + DiceBear (build-time devDep).
- scripts/gen-dicebear.mjs: generate 30 CC0 Open-Peeps characters OFFLINE (no
  runtime CDN) into public/illustrations/dicebear/ + a per-file assets.json ledger.
- scripts/check-assets.mjs: license-firewall CI guard — fails on any un-ledgered
  vendored asset.
- AssetSheet dev composition: proves vendored SVG -> staticFile() -> Remotion render
  (30 real characters render cleanly).
- NOTE: GitHub (Open Peeps/IRA/Notion git clones) + Gumroad (Lukasz) are geo-blocked
  headless here; those + Humaaans (Figma export) need a manual/mirror fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-22 18:59:03 +03:30
parent a3152ee84f
commit cb6512fee3
38 changed files with 1298 additions and 1 deletions
@@ -0,0 +1,41 @@
// License-firewall CI guard.
//
// Every vendored asset under public/illustrations/ (and public/lottie/) MUST have a
// row in public/illustrations/assets.json. No row -> the asset may not ship. Fails
// the build (exit 1) on any un-ledgered file. Run: npm run check:assets
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
import { join, dirname, relative } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const PUBLIC = join(__dirname, "..", "public");
const ILLUS = join(PUBLIC, "illustrations");
const ledgerPath = join(ILLUS, "assets.json");
if (!existsSync(ledgerPath)) {
console.error(`MISSING license ledger: ${ledgerPath}`);
process.exit(1);
}
const ledger = JSON.parse(readFileSync(ledgerPath, "utf8"));
function walk(dir) {
if (!existsSync(dir)) return [];
let out = [];
for (const e of readdirSync(dir)) {
const p = join(dir, e);
if (statSync(p).isDirectory()) out = out.concat(walk(p));
else if (/\.(svg|json)$/i.test(e) && e !== "assets.json") out.push(p);
}
return out;
}
// Asset files live under illustrations/<source>/ and lottie/.
const files = [...walk(ILLUS), ...walk(join(PUBLIC, "lottie"))]
.map((p) => relative(ILLUS, p).split("\\").join("/"));
const missing = files.filter((f) => !ledger[f]);
if (missing.length) {
console.error(`Un-ledgered assets (add a row to assets.json):\n ${missing.join("\n ")}`);
process.exit(1);
}
console.log(`assets.json OK — ${files.length} vendored asset(s), all ledgered.`);
@@ -0,0 +1,53 @@
// Build-time character generator.
//
// DiceBear's `open-peeps` style IS Pablo Stanley's Open Peeps artwork (CC0-1.0).
// We generate deterministic per-seed SVGs OFFLINE (no runtime CDN / HTTP call) and
// vendor them into public/illustrations/dicebear/, then write a license-ledger row
// per file into public/illustrations/assets.json. Run: npm run gen:dicebear
import { createAvatar } from "@dicebear/core";
import { openPeeps } from "@dicebear/collection";
import { writeFileSync, readFileSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ILLUS = join(__dirname, "..", "public", "illustrations");
const OUT = join(ILLUS, "dicebear");
mkdirSync(OUT, { recursive: true });
const COUNT = 30;
const VENDORED = "2026-06-22";
const ledger = {};
for (let i = 1; i <= COUNT; i++) {
const seed = `flatrender-peep-${i}`;
// idRandomizer keeps SVG <defs> ids unique so multiple avatars can be inlined
// in one document without clashing (Remotion composites many in one frame).
const svg = createAvatar(openPeeps, { seed, size: 400, randomizeIds: true }).toString();
const file = `openpeeps-${String(i).padStart(2, "0")}.svg`;
writeFileSync(join(OUT, file), svg, "utf8");
ledger[`dicebear/${file}`] = {
source: "DiceBear open-peeps (Pablo Stanley Open Peeps)",
license: "CC0-1.0",
license_class: "CC0",
commercial_ok: true,
attribution_required: false,
ai_training_allowed: true,
animation_fit: "swap-transform (bust/half-body micro-rig)",
generator: `createAvatar(openPeeps,{seed:'${seed}'})`,
url: "https://www.dicebear.com/styles/open-peeps/",
vendored: VENDORED,
};
}
// Merge into the assets.json license ledger (preserve any existing rows).
const ledgerPath = join(ILLUS, "assets.json");
let existing = {};
try {
existing = JSON.parse(readFileSync(ledgerPath, "utf8"));
} catch {
existing = {};
}
writeFileSync(ledgerPath, JSON.stringify({ ...existing, ...ledger }, null, 2) + "\n", "utf8");
console.log(`generated ${COUNT} open-peeps SVGs in ${OUT}`);
console.log(`ledger now has ${Object.keys({ ...existing, ...ledger }).length} rows`);