85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
202 lines
7.2 KiB
JavaScript
202 lines
7.2 KiB
JavaScript
/*
|
|
* lib-dna.cjs — DNA registry loader + scene-token resolver.
|
|
*
|
|
* A DNA is a complete visual language (type, palette logic, motion grammar, hero
|
|
* orchestration) that PARAMETERIZES per scene instead of shipping fixed look files.
|
|
* The resolver folds in safe-zones.json's v2 scene measurements (palette / optics /
|
|
* lighting) and envelope.json's loudness so the compiled tokens fit THIS clip:
|
|
*
|
|
* accent — scene-sampled when the DNA says "scene" (clamped readable)
|
|
* text shadow — contact shadow cast along the measured light direction
|
|
* depth match — embed text blur follows the scene's depth-of-field
|
|
* hero amp — entrance amplitude coupled to the spoken word's loudness (RMS)
|
|
*
|
|
* Everything is read from files written by deterministic scripts — no randomness.
|
|
*/
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
|
|
const SKILL_ROOT = path.resolve(__dirname, "..");
|
|
const DNA_DIR = path.join(SKILL_ROOT, "dna");
|
|
|
|
// legacy template names → DNA (back-compat for existing plan.json files)
|
|
const LEGACY = { "cinematic-cream": "cream" };
|
|
|
|
function list() {
|
|
try {
|
|
return fs
|
|
.readdirSync(DNA_DIR)
|
|
.filter((f) => f.endsWith(".json"))
|
|
.map((f) => f.replace(/\.json$/, ""))
|
|
.sort();
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function load(name) {
|
|
const p = path.join(DNA_DIR, `${name}.json`);
|
|
if (!fs.existsSync(p)) {
|
|
throw new Error(`unknown DNA "${name}". Available: ${list().join(", ")}`);
|
|
}
|
|
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
}
|
|
|
|
function readJson(p) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ── audio impact: where does this window's loudness rank within the clip? ──────
|
|
// envelope.json = { hop, rms: [linear 0..1 per hop] } (audio-envelope.cjs)
|
|
function heroImpact(project, t0, t1) {
|
|
const env = readJson(path.join(project, "envelope.json"));
|
|
if (!env || !Array.isArray(env.rms) || !env.rms.length) return 0.6; // neutral default
|
|
const hop = env.hop || 0.05;
|
|
const i0 = Math.max(0, Math.floor(t0 / hop)),
|
|
i1 = Math.min(env.rms.length, Math.ceil(t1 / hop));
|
|
if (i1 <= i0) return 0.6;
|
|
const span = i1 - i0;
|
|
const mean = (arr, a, b) => {
|
|
let s = 0;
|
|
for (let i = a; i < b; i++) s += arr[i];
|
|
return s / (b - a);
|
|
};
|
|
const target = mean(env.rms, i0, i1);
|
|
// percentile of this window vs all same-length windows across the clip
|
|
let below = 0,
|
|
total = 0;
|
|
for (let s = 0; s + span <= env.rms.length; s += Math.max(1, Math.floor(span / 2))) {
|
|
total++;
|
|
if (mean(env.rms, s, s + span) <= target) below++;
|
|
}
|
|
return total ? below / total : 0.6;
|
|
}
|
|
|
|
// ── scene-aware token resolution ───────────────────────────────────────────────
|
|
function resolveTokens(dna, project, opts) {
|
|
const sz = readJson(path.join(project, "safe-zones.json")) || {};
|
|
const palette = sz.palette || {};
|
|
const optics = sz.optics || {};
|
|
const lighting = sz.lighting || {};
|
|
|
|
// accent: "scene" → sampled suggestion, else literal.
|
|
// accentMode "counter" (loud registers): the accent must FIGHT the scene's
|
|
// temperature, not harmonize with it — a sampled warm sienna inside a tungsten room
|
|
// camouflages (the warm-on-warm failure a blind review caught across three DNAs).
|
|
// Counter-pole = rotate the sampled hue 180° and push saturation/lightness hot.
|
|
let accent =
|
|
dna.palette.accent === "scene"
|
|
? palette.accentSuggestion || dna.palette.accent_fallback || "#e3c06a"
|
|
: dna.palette.accent;
|
|
if (
|
|
dna.palette.accentMode === "counter" &&
|
|
dna.palette.accent === "scene" &&
|
|
/^#([0-9a-f]{6})$/i.test(accent || "")
|
|
) {
|
|
const n = parseInt(accent.slice(1), 16);
|
|
let r = (n >> 16) / 255,
|
|
g2 = ((n >> 8) & 255) / 255,
|
|
b = (n & 255) / 255;
|
|
const mx = Math.max(r, g2, b),
|
|
mn = Math.min(r, g2, b),
|
|
d = mx - mn;
|
|
let h = 0;
|
|
if (d > 0) {
|
|
h = mx === r ? ((g2 - b) / d) % 6 : mx === g2 ? (b - r) / d + 2 : (r - g2) / d + 4;
|
|
h *= 60;
|
|
if (h < 0) h += 360;
|
|
}
|
|
h = (h + 180) % 360;
|
|
const s = 0.85,
|
|
v = 0.92; // hot, acid, readable
|
|
const c = v * s,
|
|
x = c * (1 - Math.abs(((h / 60) % 2) - 1)),
|
|
m = v - c;
|
|
const [r2, g3, b2] =
|
|
h < 60
|
|
? [c, x, 0]
|
|
: h < 120
|
|
? [x, c, 0]
|
|
: h < 180
|
|
? [0, c, x]
|
|
: h < 240
|
|
? [0, x, c]
|
|
: h < 300
|
|
? [x, 0, c]
|
|
: [c, 0, x];
|
|
const f = (q) =>
|
|
Math.round((q + m) * 255)
|
|
.toString(16)
|
|
.padStart(2, "0");
|
|
accent = `#${f(r2)}${f(g3)}${f(b2)}`;
|
|
}
|
|
|
|
const dark = dna.palette.scheme === "dark-on-light";
|
|
|
|
// contact shadow along the measured light direction + a soft ambient.
|
|
// light-on-dark also gets the warm bloom layer when the DNA asks for it.
|
|
let shadowParts = [];
|
|
if (dna.optics && dna.optics.contactShadow) {
|
|
const sd = lighting.shadow || { dx: 0, dy: 3 };
|
|
shadowParts.push(`${sd.dx}px ${sd.dy + 1}px 10px rgba(0,0,0,${dark ? 0.22 : 0.32})`);
|
|
}
|
|
shadowParts.push(dark ? "0 1px 14px rgba(0,0,0,0.10)" : "0 2px 22px rgba(0,0,0,0.28)");
|
|
if (dna.optics && dna.optics.warmBloom) shadowParts.push("0 0 26px rgba(255,214,160,0.18)");
|
|
const textShadow = shadowParts.join(", ");
|
|
|
|
// depth match: embed (bg) text picks up the scene's bokeh; fg/rail stay sharp
|
|
const blurPx = (dna.optics && dna.optics.depthMatch && optics.suggestedTextBlurPx) || 0;
|
|
const baseFilter = dna.filter && dna.filter !== "none" ? dna.filter : "";
|
|
const filterBg = `${baseFilter}${blurPx ? ` blur(${blurPx}px)` : ""}`.trim() || "none";
|
|
const filterFg = baseFilter || "none";
|
|
|
|
// hero orchestration + loudness coupling — one config PER hero (scarcity is per
|
|
// beat/block, not per clip; each hero's amplitude follows ITS spoken loudness)
|
|
const heroGroups = (opts && (opts.heroGroups || (opts.heroGroup ? [opts.heroGroup] : []))) || [];
|
|
const heroes = heroGroups.map((hg) => {
|
|
const impact = heroImpact(project, hg.in, Math.min(hg.in + 0.7, hg.out));
|
|
const amp = +((0.75 + impact * 0.5) * (hg.minor ? 0.7 : 1)).toFixed(3); // 0.75…1.25, minors damped
|
|
return {
|
|
// spread the DNA's full hero block (entrance/perLetter/glow/breathe/ripple/loom/
|
|
// letterBlur/sheen/echoes/… — new fx fields flow through with no plumbing),
|
|
// then the per-hero computed values
|
|
...dna.hero,
|
|
entrance: dna.hero.entrance || "emergence",
|
|
dimOthers: dna.hero.dimOthers != null ? dna.hero.dimOthers : 0.45,
|
|
id: hg.id,
|
|
minor: hg.minor === true,
|
|
in: hg.in,
|
|
out: hg.out,
|
|
amp,
|
|
heroColor: dna.palette.heroColor === "accent" ? accent : null,
|
|
};
|
|
});
|
|
const hero = heroes[0] || null; // back-compat single accessor
|
|
|
|
return {
|
|
fontFamily: dna.font.family,
|
|
capColor: dna.palette.cap_color,
|
|
blend: dna.palette.blend,
|
|
accent,
|
|
textShadow,
|
|
filterBg,
|
|
filterFg,
|
|
blurPx,
|
|
motion: dna.motion,
|
|
hero,
|
|
heroes,
|
|
heroCss: (dna.hero && dna.hero.css) || "",
|
|
heroCase: (dna.hero && dna.hero.case) || "none",
|
|
heroTracking: (dna.hero && dna.hero.tracking) || "0",
|
|
dnaCss: dna.css || "",
|
|
register: dna.register,
|
|
name: dna.name,
|
|
};
|
|
}
|
|
|
|
module.exports = { LEGACY, list, load, resolveTokens, heroImpact };
|