368 lines
16 KiB
JavaScript
368 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Build the durable human-review record for the Batch B evaluated-pose audit.
|
|
*
|
|
* This script deliberately does not alter GLBs, manifests, clip labels, or runtime
|
|
* code. The subjective decisions below correspond to the contact sheets reviewed
|
|
* on 2026-08-01 and make the resulting JSON/Markdown reproducible from summary.json.
|
|
*/
|
|
|
|
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
const projectRoot = path.resolve(import.meta.dirname, "..", "..");
|
|
const batchDirectory = path.join(
|
|
projectRoot,
|
|
"artifacts",
|
|
"animation-audit",
|
|
"batches",
|
|
"batch-b",
|
|
);
|
|
const summaryPath = path.join(batchDirectory, "summary.json");
|
|
const jsonPath = path.join(batchDirectory, "batch-b-review.json");
|
|
const markdownPath = path.join(batchDirectory, "batch-b-review.md");
|
|
|
|
const exactCommand =
|
|
"node scripts/runewaker-pipeline/audit-animated-poses.mjs --root public/assets/creatures --input-list artifacts/animation-audit/batches/batch-b-inputs.json --output-dir artifacts/animation-audit/batches/batch-b --summary artifacts/animation-audit/batches/batch-b/summary.json --sample-mode all --render-mode families --render-limit 24 --contact-sheets --jobs 2";
|
|
|
|
const nonNeutralBindActors = new Set([
|
|
"raksha-temple/angaren-female-paperdoll-image-583960.glb",
|
|
"raksha-temple/angaren-female-paperdoll-image-583961.glb",
|
|
"raksha-temple/angaren-female-paperdoll-image-583962.glb",
|
|
"raksha-temple/angaren-female-paperdoll-image-583963.glb",
|
|
"raksha-temple/angaren-male-image-583958.glb",
|
|
"raksha-temple/angaren-male-image-583959.glb",
|
|
"raksha-temple/angaren-male-image-584146.glb",
|
|
"the-origin/hm-female-image-578588.glb",
|
|
"the-origin/hm-male-image-572550.glb",
|
|
"the-origin/hm-male-image-578583.glb",
|
|
"the-origin/hm-male-image-578585.glb",
|
|
"tomb-seven-heroes/angaren-female-paperdoll-image-588427.glb",
|
|
"tomb-seven-heroes/angaren-female-paperdoll-image-588487.glb",
|
|
"tomb-seven-heroes/angaren-male-image-588426.glb",
|
|
"tomb-seven-heroes/hm-female-image-588422.glb",
|
|
"tomb-seven-heroes/hm-female-image-588424.glb",
|
|
"tomb-seven-heroes/hm-female-image-588477.glb",
|
|
"tomb-seven-heroes/hm-female-image-588488.glb",
|
|
"tomb-seven-heroes/hm-male-image-588423.glb",
|
|
"tomb-seven-heroes/hm-male-image-588425.glb",
|
|
"tomb-seven-heroes/hm-male-image-588428.glb",
|
|
"tomb-seven-heroes/hm-male-image-588485.glb",
|
|
"tomb-seven-heroes/hm-male-image-588486.glb",
|
|
"tomb-seven-heroes/lamuren-male-image-587433.glb",
|
|
]);
|
|
|
|
const lowContrastActors = new Set([
|
|
"hoto/skate-fish-02.glb",
|
|
"hoto/skate-fish-03.glb",
|
|
"raksha-temple/treat-gold-01.glb",
|
|
"sand-vortex/wind-element-sand.glb",
|
|
"the-origin/treat-falynor.glb",
|
|
"the-origin/treat-gold-01.glb",
|
|
"the-origin/treat-silver-01.glb",
|
|
"the-origin/wind-element-warrior-04.glb",
|
|
]);
|
|
|
|
const warningDispositions = new Map([
|
|
[
|
|
"hoto/darkreef-fang.glb",
|
|
"Accepted expected component motion: the only flag is one extreme fin/tendril topology component at spawn start; all rendered families remain coherent.",
|
|
],
|
|
[
|
|
"hoto/myrma-man-gabba.glb",
|
|
"Accepted expected component motion: five buff/cast samples move small tail, ornament, or effect-card components; no detached anatomy or skin deformation is visible.",
|
|
],
|
|
[
|
|
"the-origin/bomrow-captain.glb",
|
|
"Accepted expected component motion: the actor is constructed from many independently moving stone chunks, so component-distance heuristics flag 60 samples; every rendered family retains a coherent golem silhouette.",
|
|
],
|
|
[
|
|
"the-origin/bomrow-soldier.glb",
|
|
"Accepted expected component motion: the actor is constructed from many independently moving stone chunks, so component-distance heuristics flag 59 samples; every rendered family retains a coherent golem silhouette.",
|
|
],
|
|
[
|
|
"the-origin/poto-golem.glb",
|
|
"Accepted expected component motion: 15 small stone components move far relative to their local size during the middle of unarmed_attack02; the reviewed attack pose is coherent.",
|
|
],
|
|
[
|
|
"tomb-seven-heroes/evilye-olive.glb",
|
|
"Accepted expected component motion: five cast/death samples move long independent tentacle components; all reviewed silhouettes remain anatomically coherent.",
|
|
],
|
|
[
|
|
"tomb-seven-heroes/evilye.glb",
|
|
"Accepted expected component motion: five cast/death samples move long independent tentacle components; all reviewed silhouettes remain anatomically coherent.",
|
|
],
|
|
[
|
|
"tomb-seven-heroes/sample-template.glb",
|
|
"Accepted expected component motion: 24 small hard-surface wing/claw components move far relative to local size during the middle of unarmed_attack02; the reviewed attack pose remains coherent.",
|
|
],
|
|
]);
|
|
|
|
const summary = JSON.parse(await readFile(summaryPath, "utf8"));
|
|
const reviewPageNames = (await readdir(path.join(batchDirectory, "review-pages")))
|
|
.filter((name) => name.toLowerCase().endsWith(".png"))
|
|
.sort();
|
|
|
|
const actors = [];
|
|
const issueRecords = [];
|
|
|
|
for (const actor of summary.actors) {
|
|
const report = JSON.parse(
|
|
await readFile(path.join(batchDirectory, actor.report), "utf8"),
|
|
);
|
|
const actorIssues = [];
|
|
|
|
for (const sample of report.samples ?? []) {
|
|
for (const issue of sample.issues ?? []) {
|
|
const record = {
|
|
actor: actor.input,
|
|
report: actor.report,
|
|
sampleId: sample.sampleId,
|
|
action: sample.action,
|
|
semanticFamily: sample.semanticFamily,
|
|
frameLabel: sample.frameLabel,
|
|
frame: sample.frame,
|
|
severity: issue.severity,
|
|
code: issue.code,
|
|
value: issue.value,
|
|
metrics: {
|
|
diagonalRatio: sample.bindComparison?.diagonalRatio ?? null,
|
|
maximumRadiusRatio: sample.bindComparison?.maximumRadiusRatio ?? null,
|
|
rmsRadiusRatio: sample.bindComparison?.rmsRadiusRatio ?? null,
|
|
centroidShiftRatio: sample.bindComparison?.centroidShiftRatio ?? null,
|
|
axisExtentRatios: sample.bindComparison?.axisExtentRatios ?? null,
|
|
componentCount: sample.bindComparison?.componentCount ?? null,
|
|
extremeComponentCount:
|
|
sample.bindComparison?.extremeComponentCount ?? null,
|
|
disconnectedComponentCount:
|
|
sample.bindComparison?.disconnectedComponentCount ?? null,
|
|
},
|
|
};
|
|
actorIssues.push(record);
|
|
issueRecords.push(record);
|
|
}
|
|
}
|
|
|
|
const observations = [];
|
|
if (nonNeutralBindActors.has(actor.input)) {
|
|
observations.push(
|
|
"The audit-only bind frame is a source-authored raised-knee or prone pose rather than a neutral stance. All sampled runtime action families are coherent; this could at most produce a brief pre-mixer load flash.",
|
|
);
|
|
}
|
|
if (lowContrastActors.has(actor.input)) {
|
|
observations.push(
|
|
"The contact sheet is low-contrast in the neutral audit renderer. The silhouette remains readable and no deformation is visible; verify final material appearance under dungeon lighting separately.",
|
|
);
|
|
}
|
|
if (warningDispositions.has(actor.input)) {
|
|
observations.push(warningDispositions.get(actor.input));
|
|
}
|
|
|
|
actors.push({
|
|
actorId: actor.actorId,
|
|
input: actor.input,
|
|
dungeon: actor.input.split("/")[0],
|
|
sourceStatus: actor.status,
|
|
actions: actor.actions,
|
|
sampledPoses: actor.poses,
|
|
flaggedSamples: actor.flaggedSamples,
|
|
warnings: actor.warnings,
|
|
errors: actor.errors,
|
|
visualVerdict: "pass",
|
|
overallVerdict: actor.status === "warning" ? "accepted-warning" : "pass",
|
|
report: actor.report,
|
|
contactSheet: actor.report.replace(
|
|
/\.pose-audit\.json$/,
|
|
".review/contact-sheet.png",
|
|
),
|
|
observations,
|
|
issues: actorIssues,
|
|
});
|
|
}
|
|
|
|
const dungeonTotals = Object.values(
|
|
actors.reduce((groups, actor) => {
|
|
groups[actor.dungeon] ??= {
|
|
dungeon: actor.dungeon,
|
|
actors: 0,
|
|
sourcePass: 0,
|
|
acceptedWarnings: 0,
|
|
visualPass: 0,
|
|
visualErrors: 0,
|
|
};
|
|
const group = groups[actor.dungeon];
|
|
group.actors += 1;
|
|
group.sourcePass += actor.sourceStatus === "pass" ? 1 : 0;
|
|
group.acceptedWarnings += actor.sourceStatus === "warning" ? 1 : 0;
|
|
group.visualPass += actor.visualVerdict === "pass" ? 1 : 0;
|
|
group.visualErrors += actor.visualVerdict === "error" ? 1 : 0;
|
|
return groups;
|
|
}, {}),
|
|
).sort((left, right) => left.dungeon.localeCompare(right.dungeon));
|
|
|
|
const issueCodes = Object.entries(
|
|
issueRecords.reduce((counts, issue) => {
|
|
counts[issue.code] = (counts[issue.code] ?? 0) + 1;
|
|
return counts;
|
|
}, {}),
|
|
).map(([code, count]) => ({ code, count }));
|
|
|
|
const review = {
|
|
schemaVersion: 1,
|
|
reviewId: "runewaker-pose-audit-batch-b-2026-08-01",
|
|
reviewedAt: new Date().toISOString(),
|
|
reviewer: "Codex visual review",
|
|
scope: {
|
|
dungeons: [
|
|
"tomb-seven-heroes",
|
|
"raksha-temple",
|
|
"the-origin",
|
|
"hoto",
|
|
"paspers-shrine",
|
|
"sand-vortex",
|
|
],
|
|
inputManifest:
|
|
"artifacts/animation-audit/batches/batch-b-inputs.json",
|
|
sourceSummary: "artifacts/animation-audit/batches/batch-b/summary.json",
|
|
exactCommand,
|
|
sampleMode: "all",
|
|
renderMode: "families",
|
|
renderLimit: 24,
|
|
contactSheets: true,
|
|
jobs: 2,
|
|
},
|
|
totals: {
|
|
...summary.totals,
|
|
numericIssueRecords: issueRecords.length,
|
|
visualPass: actors.filter((actor) => actor.visualVerdict === "pass").length,
|
|
visualWarnings: actors.filter((actor) => actor.visualVerdict === "warning")
|
|
.length,
|
|
visualErrors: actors.filter((actor) => actor.visualVerdict === "error").length,
|
|
acceptedNumericWarningActors: actors.filter(
|
|
(actor) => actor.overallVerdict === "accepted-warning",
|
|
).length,
|
|
overallAccepted: actors.filter((actor) =>
|
|
["pass", "accepted-warning"].includes(actor.overallVerdict),
|
|
).length,
|
|
},
|
|
conclusions: {
|
|
verdict: "accepted-with-numeric-warnings",
|
|
visualRigBreakageFound: false,
|
|
numericalDefectsFound: false,
|
|
warningDisposition:
|
|
"All 137 warnings are extreme-components heuristic flags on eight actors. Contact-sheet review found them consistent with intentional motion of segmented stone, tentacle, fin, tail, wing, claw, ornament, or effect-card components.",
|
|
castLabelChangesMade: false,
|
|
castLabelNote:
|
|
"No clip labels were changed in this review. Action-named cast clips remain documented separately from runtime clip-family reachability.",
|
|
limitations: [
|
|
"Family contact sheets show the middle frame of one representative clip per semantic family; the numeric audit separately sampled start, middle, and end for every action.",
|
|
"The audit detects gross deformation and supports visual disposition; it does not prove timing, gameplay semantics, effects, sound, or dungeon lighting.",
|
|
"Non-neutral source bind frames and low-contrast neutral-renderer materials are informational observations, not animation failures.",
|
|
],
|
|
},
|
|
reviewPages: reviewPageNames.map((name) => `review-pages/${name}`),
|
|
dungeonTotals,
|
|
issueCodes,
|
|
warningDispositions: [...warningDispositions].map(([actor, disposition]) => ({
|
|
actor,
|
|
disposition,
|
|
})),
|
|
issues: issueRecords,
|
|
actors,
|
|
};
|
|
|
|
await writeFile(jsonPath, `${JSON.stringify(review, null, 2)}\n`, "utf8");
|
|
|
|
const warningRows = actors
|
|
.filter((actor) => actor.sourceStatus === "warning")
|
|
.map((actor) => {
|
|
const actions = [...new Set(actor.issues.map((issue) => issue.action))];
|
|
const maximum = Math.max(...actor.issues.map((issue) => issue.value));
|
|
return `| \`${actor.input}\` | ${actor.flaggedSamples} | ${actor.issues.length} | \`extreme-components\` (max ${maximum}) | ${actions.join(", ")} | ${warningDispositions.get(actor.input)} |`;
|
|
});
|
|
|
|
const actorRows = actors.map((actor) => {
|
|
const notes = actor.observations.length > 0 ? actor.observations.join(" ") : "-";
|
|
return `| \`${actor.input}\` | ${actor.sourceStatus} | ${actor.visualVerdict} | ${actor.overallVerdict} | ${actor.actions} | ${actor.sampledPoses} | ${notes} |`;
|
|
});
|
|
|
|
const dungeonRows = dungeonTotals.map(
|
|
(dungeon) =>
|
|
`| ${dungeon.dungeon} | ${dungeon.actors} | ${dungeon.sourcePass} | ${dungeon.acceptedWarnings} | ${dungeon.visualPass} | ${dungeon.visualErrors} |`,
|
|
);
|
|
|
|
const markdown = `# Batch B animated-pose review
|
|
|
|
## Verdict
|
|
|
|
Accepted with numeric warnings. All 86 configured shipping GLB variants were audited, every individual actor contact sheet and all 10 paged review sheets were visually inspected, and no rig explosion, imploded skin, detached paperdoll equipment, non-finite geometry, or other visual animation failure was found.
|
|
|
|
The automated audit produced 137 warning records on eight actors and no errors or process failures. Every warning is \`extreme-components\`; visual review classifies them as intentional movement of small or segmented components, not a numerical deformation defect. No cast labels or shipping assets were changed.
|
|
|
|
## Exact command
|
|
|
|
\`\`\`powershell
|
|
${exactCommand}
|
|
\`\`\`
|
|
|
|
## Totals
|
|
|
|
| Measure | Count |
|
|
| --- | ---: |
|
|
| Configured actors | ${review.totals.actors} |
|
|
| Numeric pass | ${review.totals.pass} |
|
|
| Numeric warning | ${review.totals.warning} |
|
|
| Numeric error | ${review.totals.error} |
|
|
| Process failure | ${review.totals.failed} |
|
|
| Sampled poses | ${review.totals.poses.toLocaleString("en-US")} |
|
|
| Flagged samples / issue records | ${review.totals.flaggedSamples} / ${review.totals.numericIssueRecords} |
|
|
| Visual pass | ${review.totals.visualPass} |
|
|
| Visual warning / error | ${review.totals.visualWarnings} / ${review.totals.visualErrors} |
|
|
|
|
## Dungeon totals
|
|
|
|
| Dungeon | Actors | Numeric pass | Accepted numeric warnings | Visual pass | Visual errors |
|
|
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
${dungeonRows.join("\n")}
|
|
|
|
## Numeric warning dispositions
|
|
|
|
| Actor | Flagged samples | Warning records | Code / maximum components | Affected actions | Disposition |
|
|
| --- | ---: | ---: | --- | --- | --- |
|
|
${warningRows.join("\n")}
|
|
|
|
All 137 warning records, including action, semantic family, sampled frame, value, and bind-relative metrics, are preserved in [batch-b-review.json](./batch-b-review.json). The authoritative per-actor reports retain each named component outlier.
|
|
|
|
## Visual observations
|
|
|
|
- Twenty-four humanoid/paperdoll variants show a source-authored raised-knee or prone audit bind frame. Their sampled attack, cast, death, wound, idle, movement, and special-family frames are coherent. This is not a rig failure, though it may permit a brief pre-mixer pose flash during first load.
|
|
- Eight dark elemental/tree/fish assets are low-contrast under the neutral audit renderer. Their silhouettes remain intact; final material appearance should be judged under dungeon lighting.
|
|
- Paperdoll equipment remained attached in all reviewed animation-family poses.
|
|
- The paged review images are in [review-pages](./review-pages/); actor-level contact sheets sit beside each per-actor JSON report.
|
|
|
|
## Cast-label note
|
|
|
|
This review did not change cast labels. Some source clips are named \`Action - cast...\`, while the runtime coverage audit remains authoritative for which clips the shipping selector can reach. Label cleanup is intentionally outside this Batch B visual disposition.
|
|
|
|
## Per-actor verdicts
|
|
|
|
| Actor | Numeric | Visual | Overall | Actions | Poses | Observations |
|
|
| --- | --- | --- | --- | ---: | ---: | --- |
|
|
${actorRows.join("\n")}
|
|
|
|
## Limitations
|
|
|
|
- Family contact sheets show one middle frame per semantic family; the numeric audit sampled start, middle, and end for every action.
|
|
- This review assesses evaluated geometry and representative poses. It does not prove combat timing, gameplay semantics, effects, sound, or dungeon-lighting fidelity.
|
|
- Informational bind-frame and neutral-renderer material observations are not counted as animation failures.
|
|
`;
|
|
|
|
await writeFile(markdownPath, markdown, "utf8");
|
|
|
|
console.log(`Wrote ${path.relative(projectRoot, jsonPath)}`);
|
|
console.log(`Wrote ${path.relative(projectRoot, markdownPath)}`);
|
|
console.log(
|
|
`Reviewed ${actors.length} actors; ${issueRecords.length} warning/error records; visual errors 0.`,
|
|
);
|