Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

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 };