← Files JuicyLucy AdsARCHIVED FILE
skills/video-ad-production/scripts/check-performance.mjs
11.4 KB · Oct 3, 2026 · 06:35 UTC
#!/usr/bin/env node
// The generation gate for `clapping-reaction`: does this clip actually PERFORM?
//
// node check-performance.mjs <clip.mp4> [more.mp4 ...]
// node check-performance.mjs --json <clip.mp4>
// node check-performance.mjs --face 18,48 --hands 50,85 <clip.mp4>
//
// A reaction ad is bought for the performer's face. The expensive failure is a
// generation that claps on schedule with a held, empty expression — it passes
// every other check in the skill, looks fine in a contact sheet if you are not
// looking for it, and quietly ships an ad with no emotional payoff. One clip in
// the six that calibrated this blueprint failed exactly that way.
//
// Three measurements, none of which need a model:
//
// faceIndex mean temporal motion in the face band, divided by the busiest
// decile of the same clip. It asks "is the face performing as much
// as the body is", so it is immune to overall clip brightness,
// contrast and shot scale. Flat faces score far below moving ones:
// 0.19 against 0.48-0.62 on the calibration set.
// tempo dominant period of hand-band motion, by autocorrelation. Counting
// motion peaks double-counts (a clap is an approach AND a retreat);
// autocorrelation reads the period directly. `r` is how periodic the
// clip actually is, which is the number that matters — the models
// agree to clap on a beat far more often than they deliver one.
// cuts scene changes. The blueprint is one continuous take; a generated
// cut is a re-roll, not something to edit around.
//
// Dependency-free by design: node + ffmpeg/ffprobe, like the other scripts here.
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { pathToFileURL } from "node:url";
/** Analysis resolution. Motion is a where-and-when question, not a detail one. */
export const GRID = { w: 60, h: 107 };
/** Band defaults as fractions of frame height, from the calibration set. */
export const DEFAULT_BANDS = { face: [0.2, 0.5], hands: [0.5, 0.85] };
export const THRESHOLDS = {
/** Below this the face is holding still — re-roll. */
faceReroll: 0.25,
/** Between reroll and this, look at the frames yourself before shipping. */
faceReview: 0.4,
/** Autocorrelation r below this means there is no beat to cut music to. */
tempoWeak: 0.35,
};
/**
* Per-row mean |frame(t) - frame(t-1)| over the whole clip.
* @param {Uint8Array|Buffer} buf packed 8-bit luma, `frames` frames of w*h
*/
export function rowMotion(buf, frames, { w, h } = GRID) {
const rows = new Float64Array(h);
if (frames < 2) return rows;
for (let f = 1; f < frames; f++) {
const a = (f - 1) * w * h;
const b = f * w * h;
for (let y = 0; y < h; y++) {
let s = 0;
for (let x = 0; x < w; x++) s += Math.abs(buf[b + y * w + x] - buf[a + y * w + x]);
rows[y] += s / w;
}
}
for (let y = 0; y < h; y++) rows[y] /= frames - 1;
return rows;
}
/** Mean of `rows` over a fractional band, e.g. [0.2, 0.5]. */
export function bandMean(rows, [from, to]) {
const h = rows.length;
const y0 = Math.max(0, Math.round(from * h));
const y1 = Math.min(h, Math.round(to * h));
if (y1 <= y0) return 0;
let s = 0;
for (let y = y0; y < y1; y++) s += rows[y];
return s / (y1 - y0);
}
/** Motion per decile of frame height — the shape that says where the ad's action is. */
export function deciles(rows) {
return Array.from({ length: 10 }, (_, d) => bandMean(rows, [d / 10, (d + 1) / 10]));
}
/**
* Face-band motion relative to the busiest decile of the same clip.
* Self-normalising, so it compares across clips with different grades and framing.
*/
export function faceIndex(rows, band = DEFAULT_BANDS.face) {
const peak = Math.max(...deciles(rows));
return peak === 0 ? 0 : bandMean(rows, band) / peak;
}
/** Frame-to-frame motion inside a band, as a time series. */
export function bandSignal(buf, frames, band, { w, h } = GRID) {
const y0 = Math.max(0, Math.round(band[0] * h));
const y1 = Math.min(h, Math.round(band[1] * h));
const sig = new Float64Array(Math.max(0, frames - 1));
for (let f = 1; f < frames; f++) {
const a = (f - 1) * w * h;
const b = f * w * h;
let s = 0;
for (let y = y0; y < y1; y++) {
for (let x = 0; x < w; x++) s += Math.abs(buf[b + y * w + x] - buf[a + y * w + x]);
}
sig[f - 1] = s / (w * Math.max(1, y1 - y0));
}
return sig;
}
/**
* Dominant period of a signal by autocorrelation, searched over 0.25s-1.5s
* (240-40 BPM). Returns the period in seconds and the correlation at it, which
* is the confidence — a clip with no steady rhythm still has a best lag, and `r`
* is the only thing that tells you not to trust it.
*/
export function dominantPeriod(signal, fps, { minLag = 6, maxLag = 36 } = {}) {
const n = signal.length;
if (n < maxLag + 2) return { seconds: null, r: 0 };
const mean = Array.prototype.reduce.call(signal, (a, b) => a + b, 0) / n;
const c = Array.from(signal, (v) => v - mean);
let best = { lag: 0, r: -Infinity };
for (let lag = minLag; lag <= maxLag; lag++) {
let num = 0;
let d1 = 0;
let d2 = 0;
for (let i = 0; i + lag < n; i++) {
num += c[i] * c[i + lag];
d1 += c[i] * c[i];
d2 += c[i + lag] * c[i + lag];
}
const r = d1 === 0 || d2 === 0 ? 0 : num / Math.sqrt(d1 * d2);
if (r > best.r) best = { lag, r };
}
return { seconds: best.lag / fps, r: best.r };
}
/**
* The verdict. `reroll` means regenerate; `review` means put your eyes on the
* frames before shipping it; `pass` means the performance is there.
*/
export function verdict({ cuts, faceIndex: face, tempoR }, t = THRESHOLDS) {
const reasons = [];
let level = "pass";
const raise = (next) => {
const rank = { pass: 0, review: 1, reroll: 2 };
if (rank[next] > rank[level]) level = next;
};
if (cuts > 0) {
reasons.push(`${cuts} scene change(s) — the blueprint is one continuous take`);
raise("reroll");
}
if (face < t.faceReroll) {
reasons.push(`face index ${face.toFixed(2)} < ${t.faceReroll} — the face is holding still`);
raise("reroll");
} else if (face < t.faceReview) {
reasons.push(`face index ${face.toFixed(2)} < ${t.faceReview} — thin expression arc`);
raise("review");
}
if (tempoR < t.tempoWeak) {
reasons.push(`tempo r=${tempoR.toFixed(2)} — no steady beat; do not cut music to this clip`);
raise("review");
}
return { level, reasons };
}
// ── I/O ─────────────────────────────────────────────────────────────────────
function lumaFrames(path, grid = GRID) {
const dir = mkdtempSync(join(tmpdir(), "perf-"));
try {
const raw = join(dir, "y.gray");
execFileSync("ffmpeg", [
"-v",
"error",
"-i",
path,
"-vf",
`scale=${grid.w}:${grid.h}:flags=area,format=gray`,
"-f",
"rawvideo",
raw,
"-y",
]);
const buf = readFileSync(raw);
return { buf, frames: Math.floor(buf.length / (grid.w * grid.h)) };
} finally {
rmSync(dir, { recursive: true, force: true });
}
}
export function probeFps(path) {
const out = execFileSync(
"ffprobe",
[
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=r_frame_rate",
"-of",
"csv=p=0",
path,
],
{ encoding: "utf8" },
).trim();
const [num, den] = out.split("/").map(Number);
const fps = den ? num / den : num;
// A stream with no frame rate reports `0/0`. Left alone that zero divides
// through `dominantPeriod` and reports a tempo of Infinity seconds, which
// reads as a plausible-looking "no beat" rather than as broken input.
if (!Number.isFinite(fps) || fps <= 0) {
throw new Error(`${basename(path)}: ffprobe reports no frame rate (r_frame_rate=${out || "?"})`);
}
return fps;
}
export function countCuts(path) {
// `metadata=print` writes to the LOG at info level, not to stdout. With
// `-v error` — which we want, so a real ffmpeg failure is not buried — the
// filter's lines were suppressed before they reached either stream, so the
// previous form matched nothing and silently returned 0 for every clip,
// making the blueprint's "any cut → reroll" gate a no-op.
//
// `file=-` sends the filter's own output to stdout instead of the log, so it
// survives `-v error`. It is portable across ffmpeg builds (verified on 6.0
// and 9.0) in a way `file=/dev/stdout` is not.
const out = execFileSync(
"ffmpeg",
[
"-v",
"error",
"-i",
path,
"-vf",
"select='gt(scene,0.15)',metadata=print:file=-",
"-f",
"null",
"-",
],
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
return (out.match(/lavfi\.scene_score/g) ?? []).length;
}
export function analyze(path, bands = DEFAULT_BANDS) {
const fps = probeFps(path);
const { buf, frames } = lumaFrames(path);
const rows = rowMotion(buf, frames);
const tempo = dominantPeriod(bandSignal(buf, frames, bands.hands), fps);
const face = faceIndex(rows, bands.face);
const cuts = countCuts(path);
return {
clip: basename(path),
frames,
fps,
cuts,
faceIndex: face,
tempo: {
seconds: tempo.seconds,
clapsPerSecond: tempo.seconds ? 1 / tempo.seconds : null,
bpm: tempo.seconds ? Math.round(60 / tempo.seconds) : null,
r: tempo.r,
},
deciles: deciles(rows),
...verdict({ cuts, faceIndex: face, tempoR: tempo.r }),
};
}
function parseBand(value, fallback) {
if (value === undefined) return fallback;
const parts = value.split(",").map((v) => Number(v) / 100);
return parts.length === 2 && parts.every((n) => Number.isFinite(n)) ? parts : fallback;
}
function run(argv) {
const asJson = argv.includes("--json");
const flagValue = (flag) => {
const i = argv.indexOf(flag);
return i === -1 ? undefined : argv[i + 1];
};
const bands = {
face: parseBand(flagValue("--face"), DEFAULT_BANDS.face),
hands: parseBand(flagValue("--hands"), DEFAULT_BANDS.hands),
};
const skip = new Set(["--json", "--face", "--hands", flagValue("--face"), flagValue("--hands")]);
const clips = argv.slice(2).filter((a) => !skip.has(a) && !a.startsWith("--"));
if (clips.length === 0) {
console.error(
"usage: check-performance.mjs [--json] [--face a,b] [--hands a,b] <clip.mp4> ...",
);
return 1;
}
const results = clips.map((c) => analyze(c, bands));
if (asJson) {
console.log(JSON.stringify(results, null, 2));
} else {
console.log("clip cuts faceIndex claps/s BPM r verdict");
for (const r of results) {
console.log(
`${r.clip.slice(0, 24).padEnd(24)} ${String(r.cuts).padStart(4)} ` +
`${r.faceIndex.toFixed(2).padStart(9)} ${(r.tempo.clapsPerSecond?.toFixed(2) ?? "—").padStart(7)} ` +
`${String(r.tempo.bpm ?? "—").padStart(4)} ${r.tempo.r.toFixed(2).padStart(5)} ${r.level}`,
);
for (const reason of r.reasons) console.log(` └ ${reason}`);
}
}
return results.some((r) => r.level === "reroll") ? 1 : 0;
}
const invokedDirectly =
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
if (invokedDirectly) {
process.exitCode = run(process.argv);
}
SHA-256: 2e57e2b36f9842db5ce1dae101e8901aaeeec52f00d0801fa3b04a74ac969648