chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env node
|
||||
/* check-occlusion.cjs — pixel-perfect occlusion gate (Node port of check-occlusion-v2.py).
|
||||
* Runs measure-layout.cjs (real Chromium DOM rects), reads the subject-matte alpha via sharp,
|
||||
* computes per-word / per-cap occlusion. No Python.
|
||||
* node check-occlusion.cjs <project-dir> [--strict] [--word-fail F] [--word-warn F] [--cap-fail F] [--remeasure]
|
||||
*/
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const cp = require("child_process");
|
||||
|
||||
function hfResolve(pkg) {
|
||||
const roots = [
|
||||
process.env.HYPERFRAMES_ROOT,
|
||||
path.resolve(__dirname, "..", "..", ".."),
|
||||
path.join(os.homedir(), "Downloads", "hyperframes"),
|
||||
].filter(Boolean);
|
||||
for (const root of roots) {
|
||||
const cands = [path.join(root, "node_modules", pkg)];
|
||||
const bun = path.join(root, "node_modules", ".bun");
|
||||
try {
|
||||
if (fs.existsSync(bun))
|
||||
for (const d of fs.readdirSync(bun))
|
||||
if (d.startsWith(pkg + "@")) cands.push(path.join(bun, d, "node_modules", pkg));
|
||||
} catch {}
|
||||
for (const c of cands) {
|
||||
try {
|
||||
if (fs.existsSync(c)) return require(c);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
console.error(`[v2] cannot find ${pkg} — set HYPERFRAMES_ROOT`);
|
||||
process.exit(3);
|
||||
}
|
||||
const sharp = hfResolve("sharp");
|
||||
|
||||
function ensureLayoutMeasured(project, force) {
|
||||
const lp = path.join(project, "_layout.json"),
|
||||
idx = path.join(project, "index.html");
|
||||
let stale = force || !fs.existsSync(lp);
|
||||
if (!stale && fs.existsSync(idx) && fs.statSync(idx).mtimeMs > fs.statSync(lp).mtimeMs)
|
||||
stale = true;
|
||||
if (stale)
|
||||
cp.execFileSync("node", [path.join(__dirname, "measure-layout.cjs"), project], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
return JSON.parse(fs.readFileSync(lp, "utf8"));
|
||||
}
|
||||
async function loadAlphaMask(png) {
|
||||
if (!fs.existsSync(png)) return null;
|
||||
const { data, info } = await sharp(png)
|
||||
.ensureAlpha()
|
||||
.extractChannel(3)
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
return { data, W: info.width, H: info.height };
|
||||
}
|
||||
function occlusionForRect(m, x, y, w, h) {
|
||||
if (!m) return 0;
|
||||
const x0 = Math.max(0, Math.round(x)),
|
||||
y0 = Math.max(0, Math.round(y));
|
||||
const x1 = Math.min(m.W, Math.round(x + w)),
|
||||
y1 = Math.min(m.H, Math.round(y + h));
|
||||
if (x1 <= x0 || y1 <= y0) return 0;
|
||||
let cnt = 0,
|
||||
tot = 0;
|
||||
for (let yy = y0; yy < y1; yy++) {
|
||||
const row = yy * m.W;
|
||||
for (let xx = x0; xx < x1; xx++) {
|
||||
tot++;
|
||||
if (m.data[row + xx] > 128) cnt++;
|
||||
}
|
||||
}
|
||||
return tot ? cnt / tot : 0;
|
||||
}
|
||||
function argf(name, d) {
|
||||
const i = process.argv.indexOf(name);
|
||||
return i >= 0 ? parseFloat(process.argv[i + 1]) : d;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const project = path.resolve(process.argv[2] || "");
|
||||
if (!process.argv[2]) {
|
||||
console.error("usage: check-occlusion.cjs <project-dir> [--strict]");
|
||||
process.exit(1);
|
||||
}
|
||||
const strict = process.argv.includes("--strict");
|
||||
const wordFail = argf("--word-fail", 0.65),
|
||||
wordWarn = argf("--word-warn", 0.35),
|
||||
capFail = argf("--cap-fail", 0.5);
|
||||
const layout = ensureLayoutMeasured(project, process.argv.includes("--remeasure"));
|
||||
const framesDir = path.join(project, "frames_fg");
|
||||
if (!fs.existsSync(framesDir)) {
|
||||
console.error(`[v2] missing ${framesDir}`);
|
||||
process.exit(2);
|
||||
}
|
||||
const planPath = path.join(project, "plan.json");
|
||||
const plan = fs.existsSync(planPath) ? JSON.parse(fs.readFileSync(planPath, "utf8")) : {};
|
||||
const planLayer = plan.caption_layer || "bg";
|
||||
// Hero groups (the ONE big promoted word) are SUPPOSED to sit ON the subject — for them
|
||||
// occlusion is a TARGET (~30–55%), not "minimize". Collect their ids for the advisory below.
|
||||
const heroIds = new Set();
|
||||
const heroIn = {};
|
||||
for (const g of plan.groups || [])
|
||||
if (g && (g.hero === true || /^(hero|crown)$/i.test(g.plane || ""))) {
|
||||
heroIds.add(g.id);
|
||||
heroIn[g.id] = g.in;
|
||||
}
|
||||
if (plan.crown_group && plan.crown_group.id) {
|
||||
heroIds.add(plan.crown_group.id);
|
||||
heroIn[plan.crown_group.id] = plan.crown_group.in;
|
||||
}
|
||||
const M = 2; // frame-edge tolerance (px) — matches check-overflow.cjs
|
||||
const frameW = layout.width,
|
||||
frameH = layout.height;
|
||||
|
||||
const capStats = {};
|
||||
for (const sample of layout.samples) {
|
||||
const png = path.join(framesDir, `f_${String(sample.frame_idx).padStart(4, "0")}.png`);
|
||||
const mask = await loadAlphaMask(png);
|
||||
if (!mask) continue;
|
||||
for (const cap of sample.caps) {
|
||||
const entry = (capStats[cap.id] ||= { layer: cap.layer || planLayer, samples: [] });
|
||||
const wordsData = [];
|
||||
for (const w of cap.words || []) {
|
||||
if ((w.opacity ?? 1) < 0.3) continue;
|
||||
wordsData.push({ text: w.text, occlusion: occlusionForRect(mask, w.x, w.y, w.w, w.h) });
|
||||
// Frame-edge overflow — clipped SETTLED text is always wrong; a hero's first
|
||||
// 0.5s is its entrance TRANSIENT (slam over-scale, streak fly-in pass through
|
||||
// off-frame states by design) — judge overflow on the hold, not mid-flight.
|
||||
if (heroIds.has(cap.id) && heroIn[cap.id] != null && sample.t < heroIn[cap.id] + 0.5)
|
||||
continue;
|
||||
const off = {
|
||||
left: Math.max(0, Math.round(-w.x - M)),
|
||||
right: Math.max(0, Math.round(w.x + w.w - frameW - M)),
|
||||
top: Math.max(0, Math.round(-w.y - M)),
|
||||
bottom: Math.max(0, Math.round(w.y + w.h - frameH - M)),
|
||||
};
|
||||
const score = off.left + off.right + off.top + off.bottom;
|
||||
if (score > 0 && (!entry.overflow || score > entry.overflow.score))
|
||||
entry.overflow = { text: w.text, off, score, t: sample.t };
|
||||
}
|
||||
const capOccl = occlusionForRect(
|
||||
mask,
|
||||
cap.cap_bbox.x,
|
||||
cap.cap_bbox.y,
|
||||
cap.cap_bbox.w,
|
||||
cap.cap_bbox.h,
|
||||
);
|
||||
entry.samples.push({
|
||||
t: sample.t,
|
||||
cap_occl: capOccl,
|
||||
cap_bbox: cap.cap_bbox,
|
||||
words: wordsData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
console.log(
|
||||
`[v2] ${path.basename(project)} word-fail≥${(wordFail * 100).toFixed(0)}% cap-fail≥${(capFail * 100).toFixed(0)}%`,
|
||||
);
|
||||
for (const [gid, entry] of Object.entries(capStats)) {
|
||||
// Frame-edge overflow applies to every layer (fg too). The skill allows the
|
||||
// climax a few-px graze on the first/last letter, so only a clear glyph clip
|
||||
// (>8px past an edge) is a hard FAIL; a sub-glyph graze is a WARN.
|
||||
if (entry.overflow) {
|
||||
const o = entry.overflow;
|
||||
const maxOff = Math.max(o.off.left, o.off.right, o.off.top, o.off.bottom);
|
||||
const sides = Object.entries(o.off)
|
||||
.filter(([, v]) => v > 0)
|
||||
.map(([s, v]) => `${s} ${v}px`)
|
||||
.join(", ");
|
||||
const hard = maxOff >= 8;
|
||||
console.log(
|
||||
` ${gid} [overflow${hard ? "" : "-warn"}] "${o.text}" off-frame: ${sides} (@${o.t}s)` +
|
||||
(hard ? " — cropped text is always wrong" : " (graze — within climax tolerance)"),
|
||||
);
|
||||
if (hard && !failures.includes(gid)) failures.push(gid);
|
||||
}
|
||||
if (entry.layer === "fg") {
|
||||
console.log(` ${gid} fg (occlusion skipped — fg renders above matte)`);
|
||||
continue;
|
||||
}
|
||||
const capOccls = entry.samples.map((s) => s.cap_occl);
|
||||
const avgCap = capOccls.length ? capOccls.reduce((a, b) => a + b, 0) / capOccls.length : 0;
|
||||
const peakCap = capOccls.length ? Math.max(...capOccls) : 0;
|
||||
const wordPeaks = {};
|
||||
for (const s of entry.samples)
|
||||
for (const wd of s.words)
|
||||
wordPeaks[wd.text] = Math.max(wordPeaks[wd.text] || 0, wd.occlusion);
|
||||
const oblit = Object.entries(wordPeaks).filter(([, p]) => p >= wordFail);
|
||||
const warn = Object.entries(wordPeaks).filter(([, p]) => p >= wordWarn && p < wordFail);
|
||||
let status = "OK";
|
||||
// HERO caps WANT occlusion (~30–55% is the embed); the generic cap threshold would
|
||||
// fail a working hero. Heroes fail only past the feasibility ceiling (68%).
|
||||
const capLimit = heroIds.has(gid) ? Math.max(capFail, 0.68) : capFail;
|
||||
if (oblit.length || peakCap >= capLimit) {
|
||||
status = "FAIL";
|
||||
failures.push(gid);
|
||||
} else if (warn.length) status = "WARN";
|
||||
let s = "";
|
||||
if (oblit.length)
|
||||
s =
|
||||
oblit
|
||||
.slice(0, 5)
|
||||
.map(([t, p]) => `${t}(${(p * 100).toFixed(0)}%)`)
|
||||
.join(" ") + (oblit.length > 5 ? ` …+${oblit.length - 5}` : "");
|
||||
else if (warn.length)
|
||||
s =
|
||||
"[warn] " +
|
||||
warn
|
||||
.slice(0, 3)
|
||||
.map(([t, p]) => `${t}(${(p * 100).toFixed(0)}%)`)
|
||||
.join(" ");
|
||||
console.log(
|
||||
` ${gid} ${entry.layer} avg ${(avgCap * 100).toFixed(0)}% peak ${(peakCap * 100).toFixed(0)}% ${status} ${s}`,
|
||||
);
|
||||
// HERO target-occlusion advisory (not a failure): a hero should sit ON the subject
|
||||
// (~30–55%). If it barely grazes, it reads as a small floating word, not an embed.
|
||||
if (heroIds.has(gid) && peakCap < 0.15) {
|
||||
// METRIC HONESTY: this advisory uses CAP-AREA occlusion, which saturates ~15%
|
||||
// for a width-filled hero over a narrow subject (the 30–55% figure elsewhere is
|
||||
// the safe-zones BAND metric — different denominator). If the hero already owns
|
||||
// the width, "center it + make it BIG" is unactionable — stay quiet.
|
||||
const widest = Math.max(...entry.samples.map((sm) => (sm.cap_bbox && sm.cap_bbox.w) || 0), 0);
|
||||
if (widest >= frameW * 0.8) {
|
||||
console.log(
|
||||
` ${gid} [hero-ok] peak ${(peakCap * 100).toFixed(0)}% cap-area — width-saturated hero over a narrow subject; cap-area can't reach the band target (this is the geometry, not a layout fault).`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
` ${gid} [hero-weak] peak ${(peakCap * 100).toFixed(0)}% — hero barely crosses the subject; it should sit ON the subject (~30–55% by the safe-zones BAND metric = the embed effect). Center it (safe-zones heroAnchor) + make it BIG; don't park it in a clean margin.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failures.length) {
|
||||
const uniq = [...new Set(failures)];
|
||||
console.error(`\n[v2] ${uniq.length} cap(s) FAIL: ${uniq.join(", ")}`);
|
||||
console.error(
|
||||
" → occlusion: set that cap's layer to fg, OR shrink/reposition; overflow: shrink/reposition (fg won't help)",
|
||||
);
|
||||
}
|
||||
process.exit(strict && failures.length ? 2 : 0);
|
||||
}
|
||||
main().catch((e) => {
|
||||
console.error("[v2]", e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user