create-aura3d 1.1.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
- package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
- package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
- package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
- package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
- package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
- package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
- package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
- package/templates/animation-studio/AGENTS.md +71 -0
- package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
- package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
- package/templates/animation-studio/aura.assets.json +669 -0
- package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
- package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
- package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
- package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
- package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
- package/templates/animation-studio/dist/scene/integ.document.json +1 -0
- package/templates/animation-studio/dist/scene/working.document.json +723 -0
- package/templates/animation-studio/dist/scene/working.history.json +1 -0
- package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
- package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
- package/templates/animation-studio/package.json +32 -0
- package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
- package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
- package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
- package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
- package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
- package/templates/animation-studio/public/clip-library/idle.json +23035 -0
- package/templates/animation-studio/public/clip-library/manifest.json +154 -0
- package/templates/animation-studio/public/clip-library/nod.json +173903 -0
- package/templates/animation-studio/public/clip-library/point.json +69879 -0
- package/templates/animation-studio/public/clip-library/react.json +66084 -0
- package/templates/animation-studio/public/clip-library/run.json +9648 -0
- package/templates/animation-studio/public/clip-library/talk.json +69879 -0
- package/templates/animation-studio/public/clip-library/walk.json +8283 -0
- package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
- package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
- package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
- package/templates/animation-studio/scripts/anim.ts +103 -0
- package/templates/animation-studio/scripts/animation-scene.ts +457 -0
- package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
- package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
- package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
- package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
- package/templates/animation-studio/scripts/build-characters.ts +1543 -0
- package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
- package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
- package/templates/animation-studio/scripts/determinism-check.ts +48 -0
- package/templates/animation-studio/scripts/generate-scene.ts +95 -0
- package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
- package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
- package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
- package/templates/animation-studio/scripts/preview-server.ts +52 -0
- package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
- package/templates/animation-studio/scripts/render-core.ts +360 -0
- package/templates/animation-studio/scripts/render-live.ts +813 -0
- package/templates/animation-studio/scripts/render-parallel.ts +70 -0
- package/templates/animation-studio/scripts/render-server.ts +119 -0
- package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
- package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
- package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
- package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
- package/templates/animation-studio/scripts/texture-bake.ts +297 -0
- package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
- package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
- package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
- package/templates/animation-studio/src/animation-performance.ts +556 -0
- package/templates/animation-studio/src/aura-assets.ts +71 -0
- package/templates/animation-studio/src/character.ts +40 -0
- package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
- package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
- package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
- package/templates/animation-studio/src/empty-document.ts +77 -0
- package/templates/animation-studio/src/episode-document.ts +442 -0
- package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
- package/templates/animation-studio/src/fidelity.ts +129 -0
- package/templates/animation-studio/src/graph.ts +14 -0
- package/templates/animation-studio/src/procedural-hdri.ts +243 -0
- package/templates/animation-studio/src/profile.ts +97 -0
- package/templates/animation-studio/src/render-live-route.ts +42 -0
- package/templates/animation-studio/src/render-modes.ts +137 -0
- package/templates/animation-studio/src/scene-player.ts +867 -0
- package/templates/animation-studio/src/set-templates.ts +361 -0
- package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
- package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
- package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
- package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
- package/templates/animation-studio/tsconfig.json +26 -0
- package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
- package/templates/character-controller/README.md +33 -0
- package/templates/character-controller/index.html +19 -0
- package/templates/character-controller/package.json +23 -0
- package/templates/character-controller/playwright.config.ts +14 -0
- package/templates/character-controller/src/controller.ts +44 -0
- package/templates/character-controller/src/main.ts +62 -0
- package/templates/character-controller/tests/route-health.spec.ts +26 -0
- package/templates/character-controller/tests/screenshot.spec.ts +9 -0
- package/templates/character-controller/tsconfig.json +15 -0
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/episode-builder/README.md +4 -4
- package/templates/episode-builder/package.json +1 -1
- package/templates/episode-builder/src/aura-assets.ts +1 -1
- package/templates/episode-builder/src/builder.ts +4 -4
- package/templates/episode-builder/src/episode.ts +21 -21
- package/templates/episode-builder/src/main.ts +20 -20
- package/templates/episode-builder/src/render-plan.ts +13 -13
- package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
- package/templates/fighting-game/package.json +1 -1
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
- package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
- package/templates/cartoon-studio/aura.assets.json +0 -400
- package/templates/cartoon-studio/package.json +0 -33
- package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
- package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
- package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
- package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
- package/templates/cartoon-studio/scripts/episode.ts +0 -323
- package/templates/cartoon-studio/scripts/render-live.ts +0 -873
- package/templates/cartoon-studio/src/aura-assets.ts +0 -64
- package/templates/cartoon-studio/src/characters.ts +0 -65
- package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
- package/templates/cartoon-studio/src/episode.ts +0 -306
- package/templates/cartoon-studio/src/main.ts +0 -400
- package/templates/cartoon-studio/src/render-live-route.ts +0 -913
- package/templates/cartoon-studio/src/render-plan.ts +0 -567
- package/templates/cartoon-studio/src/review.ts +0 -99
- package/templates/cartoon-studio/src/studio.ts +0 -101
- package/templates/cartoon-studio/test-results/.last-run.json +0 -4
- package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
- package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
- package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
- package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
- package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
- package/templates/cartoon-studio/tsconfig.json +0 -14
- package/templates/prompt-cartoon-channel/index.html +0 -13
- package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
- /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
- /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motion-evidence-cli.ts — write the Phase B4/B5 motion-evidence artifacts to disk.
|
|
3
|
+
*
|
|
4
|
+
* Produces:
|
|
5
|
+
* B4 tests/reports/standard-clips/<intent>.png (8 strips, one per standard intent)
|
|
6
|
+
* tests/reports/standard-clips/summary.json (per-intent bones/maxRot/firstFinalDiff)
|
|
7
|
+
* B5 tests/reports/rig-overlays/<rigName>.png (5 retargeting-overlay strips)
|
|
8
|
+
* tests/reports/rig-overlays/summary.json (per-rig grade + coverage + refusal flag)
|
|
9
|
+
*
|
|
10
|
+
* Standalone (no Playwright/GPU): reuses `buildSkeletonStrip`'s FK projection and `sharp` for PNG
|
|
11
|
+
* encoding only. Deterministic. Emits a final machine-readable JSON line the test parses.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* tsx scripts/motion-evidence-cli.ts --out tests/reports [--clip gesture]
|
|
15
|
+
*/
|
|
16
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
17
|
+
import { dirname, resolve, join } from "node:path";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import {
|
|
21
|
+
buildAllIntentEvidence,
|
|
22
|
+
buildAllRigEvidence,
|
|
23
|
+
type IntentEvidence,
|
|
24
|
+
type RigEvidence
|
|
25
|
+
} from "./motion-evidence.js";
|
|
26
|
+
|
|
27
|
+
interface SharpModule {
|
|
28
|
+
(input: Buffer | Uint8Array, options?: { raw: { width: number; height: number; channels: number } }): {
|
|
29
|
+
png(): { toBuffer(): Promise<Buffer> };
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function loadSharp(): SharpModule {
|
|
33
|
+
try {
|
|
34
|
+
return createRequire(import.meta.url)("sharp") as SharpModule;
|
|
35
|
+
} catch {
|
|
36
|
+
const roots = [process.cwd(), dirname(fileURLToPath(import.meta.url))];
|
|
37
|
+
for (const start of roots) {
|
|
38
|
+
let dir = start;
|
|
39
|
+
for (let up = 0; up < 8; up += 1) {
|
|
40
|
+
const storePkg = resolve(dir, "node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/package.json");
|
|
41
|
+
if (existsSync(storePkg)) return createRequire(storePkg)("sharp") as SharpModule;
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Could not load `sharp`. Install it: pnpm add -D sharp");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function rawRgbaToPng(pixels: Uint8Array, width: number, height: number): Promise<Uint8Array> {
|
|
51
|
+
const sharp = loadSharp();
|
|
52
|
+
const buffer = await sharp(Buffer.from(pixels.buffer, pixels.byteOffset, pixels.byteLength), {
|
|
53
|
+
raw: { width, height, channels: 4 }
|
|
54
|
+
})
|
|
55
|
+
.png()
|
|
56
|
+
.toBuffer();
|
|
57
|
+
return new Uint8Array(buffer);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function arg(name: string, fallback: string): string {
|
|
61
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
62
|
+
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function round(n: number): number {
|
|
66
|
+
return Math.round(n * 1000) / 1000;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main(): Promise<void> {
|
|
70
|
+
const outRoot = resolve(arg("out", "tests/reports"));
|
|
71
|
+
const clipIntent = arg("clip", "gesture");
|
|
72
|
+
|
|
73
|
+
// --- B4: per-intent strips + summary ----------------------------------------------------------
|
|
74
|
+
const intentDir = join(outRoot, "standard-clips");
|
|
75
|
+
mkdirSync(intentDir, { recursive: true });
|
|
76
|
+
const intents: IntentEvidence[] = buildAllIntentEvidence();
|
|
77
|
+
const intentSummary: Record<string, unknown> = {};
|
|
78
|
+
for (const ev of intents) {
|
|
79
|
+
const png = await rawRgbaToPng(ev.strip.rgba, ev.strip.width, ev.strip.height);
|
|
80
|
+
const file = join(intentDir, `${ev.intent}.png`);
|
|
81
|
+
writeFileSync(file, png);
|
|
82
|
+
intentSummary[ev.intent] = {
|
|
83
|
+
png: file,
|
|
84
|
+
bonesTouched: ev.bonesTouched,
|
|
85
|
+
bonesTouchedCount: ev.bonesTouched.length,
|
|
86
|
+
maxRotAmplitudeRad: round(ev.maxRotAmplitudeRad),
|
|
87
|
+
firstFinalDiff: ev.firstFinalDiff,
|
|
88
|
+
panelJointCounts: ev.panelJointCounts,
|
|
89
|
+
pngBytes: png.byteLength
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
writeFileSync(
|
|
93
|
+
join(intentDir, "summary.json"),
|
|
94
|
+
JSON.stringify({ generated: "B4 standard-clip visual regression", intents: intentSummary }, null, 2)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// --- B5: per-rig retargeting overlays + summary -----------------------------------------------
|
|
98
|
+
const rigDir = join(outRoot, "rig-overlays");
|
|
99
|
+
mkdirSync(rigDir, { recursive: true });
|
|
100
|
+
const rigs: RigEvidence[] = buildAllRigEvidence(clipIntent);
|
|
101
|
+
const rigSummary: Record<string, unknown> = {};
|
|
102
|
+
for (const ev of rigs) {
|
|
103
|
+
const png = await rawRgbaToPng(ev.strip.rgba, ev.strip.width, ev.strip.height);
|
|
104
|
+
const file = join(rigDir, `${ev.name}.png`);
|
|
105
|
+
writeFileSync(file, png);
|
|
106
|
+
rigSummary[ev.name] = {
|
|
107
|
+
png: file,
|
|
108
|
+
convention: ev.convention,
|
|
109
|
+
grade: ev.grade,
|
|
110
|
+
mappedBoneCount: ev.mappedBoneCount,
|
|
111
|
+
retargetCoverage: round(ev.retargetCoverage),
|
|
112
|
+
retargetedBoneCount: ev.retargetedBones.length,
|
|
113
|
+
refusesBodyActing: ev.refusesBodyActing,
|
|
114
|
+
clip: ev.clip,
|
|
115
|
+
firstFinalDiff: ev.firstFinalDiff,
|
|
116
|
+
gradeReasons: ev.gradeReasons,
|
|
117
|
+
pngBytes: png.byteLength
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
writeFileSync(
|
|
121
|
+
join(rigDir, "summary.json"),
|
|
122
|
+
JSON.stringify({ generated: "B5 5-rig retargeting overlays", clip: clipIntent, rigs: rigSummary }, null, 2)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
console.log(
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
ok: true,
|
|
128
|
+
outRoot,
|
|
129
|
+
clip: clipIntent,
|
|
130
|
+
intents: intents.map((ev) => ({
|
|
131
|
+
intent: ev.intent,
|
|
132
|
+
bonesTouched: ev.bonesTouched.length,
|
|
133
|
+
maxRotAmplitudeRad: round(ev.maxRotAmplitudeRad),
|
|
134
|
+
firstFinalDiff: ev.firstFinalDiff
|
|
135
|
+
})),
|
|
136
|
+
rigs: rigs.map((ev) => ({
|
|
137
|
+
name: ev.name,
|
|
138
|
+
grade: ev.grade,
|
|
139
|
+
mappedBoneCount: ev.mappedBoneCount,
|
|
140
|
+
refusesBodyActing: ev.refusesBodyActing,
|
|
141
|
+
firstFinalDiff: ev.firstFinalDiff
|
|
142
|
+
}))
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
main().catch((err) => {
|
|
148
|
+
console.error(JSON.stringify({ ok: false, error: (err as Error).message, stack: (err as Error).stack }));
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motion-evidence.ts — Phase B4/B5 motion-evidence artifact generator (GPU-free, deterministic).
|
|
3
|
+
*
|
|
4
|
+
* This is the visual-regression + retargeting-overlay producer the PRD's B4/B5 acceptance asks for.
|
|
5
|
+
* It is intentionally browser-FREE and GPU-FREE: it reuses the FK bone-projection in
|
|
6
|
+
* `skeleton-overlay.ts` (which drives the canonical rest skeleton with the SHARED standard-library
|
|
7
|
+
* clip and rasterizes a first/mid/final PNG strip) and adds:
|
|
8
|
+
*
|
|
9
|
+
* B4 — one skeleton-overlay strip PER standard intent (idle/talk/gesture/point/nod/walk/run/react)
|
|
10
|
+
* onto the STANDARD reference rig, plus a per-intent motion summary (bones touched, max
|
|
11
|
+
* rotation amplitude in rad, first-vs-final pixel diff).
|
|
12
|
+
*
|
|
13
|
+
* B5 — five materially-different synthetic rigs (Mixamo / Blender / UE / sparse 8-bone mascot /
|
|
14
|
+
* rich humanoid) inferred from node-name lists, GRADED with `gradeRig`, with the SAME clip
|
|
15
|
+
* retargeted onto each. A per-rig skeleton-overlay strip is produced that draws ONLY the bones
|
|
16
|
+
* the rig actually maps (so a sparse rig visibly acts with fewer bones), and each rig's grade
|
|
17
|
+
* is recorded. A D-grade rig is reported as refusing body-acting.
|
|
18
|
+
*
|
|
19
|
+
* Everything here is pure data + raw RGBA; PNG encoding is delegated to the caller (the CLI uses
|
|
20
|
+
* `sharp`). Deterministic — same inputs, same bytes — so a vitest test can assert the artifacts exist
|
|
21
|
+
* and that every strip shows real motion.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
25
|
+
// @ts-ignore — resolved at runtime to the freshly-built monorepo dist (has co-located .d.ts).
|
|
26
|
+
import {
|
|
27
|
+
createStandardHumanoidClipRegistry,
|
|
28
|
+
STANDARD_LIBRARY_RIG,
|
|
29
|
+
inferHumanoidRig,
|
|
30
|
+
gradeRig,
|
|
31
|
+
createHumanoidRetargetingMap,
|
|
32
|
+
type AnimationClipRegistry,
|
|
33
|
+
type HumanoidRigDefinition,
|
|
34
|
+
type RigGrade
|
|
35
|
+
} from "@aura3d/animation";
|
|
36
|
+
import { buildSkeletonStrip, type SkeletonStripResult } from "./skeleton-overlay.js";
|
|
37
|
+
|
|
38
|
+
/** The 8 shared standard intents (mirrors STANDARD_CLIP_IDS in packages/animation). */
|
|
39
|
+
export const STANDARD_INTENTS = [
|
|
40
|
+
"idle",
|
|
41
|
+
"talk",
|
|
42
|
+
"gesture",
|
|
43
|
+
"point",
|
|
44
|
+
"nod",
|
|
45
|
+
"walk",
|
|
46
|
+
"run",
|
|
47
|
+
"react"
|
|
48
|
+
] as const;
|
|
49
|
+
export type StandardIntent = (typeof STANDARD_INTENTS)[number];
|
|
50
|
+
|
|
51
|
+
type Quat = readonly [number, number, number, number];
|
|
52
|
+
|
|
53
|
+
interface SampledTrack {
|
|
54
|
+
readonly target: string;
|
|
55
|
+
readonly valueType: string;
|
|
56
|
+
sample(t: number): readonly number[];
|
|
57
|
+
}
|
|
58
|
+
interface SampledClip {
|
|
59
|
+
readonly duration: number;
|
|
60
|
+
readonly loop: boolean;
|
|
61
|
+
readonly tracks: readonly SampledTrack[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Angular distance (rad) between two unit quaternions. */
|
|
65
|
+
function quatAngle(a: Quat, b: Quat): number {
|
|
66
|
+
let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
|
|
67
|
+
dot = Math.min(1, Math.max(-1, Math.abs(dot)));
|
|
68
|
+
return 2 * Math.acos(dot);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Measure a clip's motion semantics directly off its tracks (independent of the projection):
|
|
73
|
+
* - bonesTouched: distinct bone names with a rotation OR translation track.
|
|
74
|
+
* - maxRotAmplitudeRad: the largest angular deviation any rotation track travels from its t=0 pose,
|
|
75
|
+
* sampled densely across the clip duration. A static (offset-only) clip reports ~0 here.
|
|
76
|
+
*/
|
|
77
|
+
export function measureClipMotion(clip: SampledClip): {
|
|
78
|
+
bonesTouched: string[];
|
|
79
|
+
maxRotAmplitudeRad: number;
|
|
80
|
+
rotationTracks: number;
|
|
81
|
+
} {
|
|
82
|
+
const duration = clip.duration > 0 ? clip.duration : 1;
|
|
83
|
+
const SAMPLES = 48;
|
|
84
|
+
const bones = new Set<string>();
|
|
85
|
+
let maxRot = 0;
|
|
86
|
+
let rotationTracks = 0;
|
|
87
|
+
for (const track of clip.tracks) {
|
|
88
|
+
const dot = track.target.lastIndexOf(".");
|
|
89
|
+
if (dot < 0) continue;
|
|
90
|
+
const bone = track.target.slice(0, dot);
|
|
91
|
+
const path = track.target.slice(dot + 1);
|
|
92
|
+
bones.add(bone);
|
|
93
|
+
if (path !== "rotation") continue;
|
|
94
|
+
rotationTracks += 1;
|
|
95
|
+
const first = track.sample(0);
|
|
96
|
+
if (first.length < 4) continue;
|
|
97
|
+
const q0: Quat = [first[0]!, first[1]!, first[2]!, first[3]!];
|
|
98
|
+
for (let i = 1; i <= SAMPLES; i += 1) {
|
|
99
|
+
const t = (duration * i) / SAMPLES;
|
|
100
|
+
const v = track.sample(clip.loop ? t % duration : Math.min(t, duration));
|
|
101
|
+
if (v.length < 4) continue;
|
|
102
|
+
const q: Quat = [v[0]!, v[1]!, v[2]!, v[3]!];
|
|
103
|
+
const angle = quatAngle(q0, q);
|
|
104
|
+
if (angle > maxRot) maxRot = angle;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { bonesTouched: [...bones].sort(), maxRotAmplitudeRad: maxRot, rotationTracks };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface IntentEvidence {
|
|
111
|
+
readonly intent: StandardIntent;
|
|
112
|
+
readonly bonesTouched: string[];
|
|
113
|
+
readonly maxRotAmplitudeRad: number;
|
|
114
|
+
/** first-vs-final pixel diff from the rendered strip (>0 ⇒ the figure visibly moved). */
|
|
115
|
+
readonly firstFinalDiff: number;
|
|
116
|
+
readonly panelJointCounts: readonly number[];
|
|
117
|
+
readonly strip: SkeletonStripResult;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** B4 — build the per-intent strip + measured motion for one standard intent. */
|
|
121
|
+
export function buildIntentEvidence(
|
|
122
|
+
intent: StandardIntent,
|
|
123
|
+
registry: AnimationClipRegistry
|
|
124
|
+
): IntentEvidence {
|
|
125
|
+
const clip = (registry.get?.(intent) ?? registry.require?.(intent)) as SampledClip | undefined;
|
|
126
|
+
if (!clip) throw new Error(`motion-evidence: no standard clip for intent "${intent}".`);
|
|
127
|
+
const motion = measureClipMotion(clip);
|
|
128
|
+
const strip = buildSkeletonStrip({ intent, registry });
|
|
129
|
+
return {
|
|
130
|
+
intent,
|
|
131
|
+
bonesTouched: motion.bonesTouched,
|
|
132
|
+
maxRotAmplitudeRad: motion.maxRotAmplitudeRad,
|
|
133
|
+
firstFinalDiff: strip.firstFinalMaxDiff,
|
|
134
|
+
panelJointCounts: strip.panelJointCounts,
|
|
135
|
+
strip
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Build evidence for every standard intent (B4). Shares one registry for determinism + speed. */
|
|
140
|
+
export function buildAllIntentEvidence(): IntentEvidence[] {
|
|
141
|
+
const registry = createStandardHumanoidClipRegistry();
|
|
142
|
+
return STANDARD_INTENTS.map((intent) => buildIntentEvidence(intent, registry));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --------------------------------------------------------------------------------------------------
|
|
146
|
+
// B5 — five materially-different synthetic rigs.
|
|
147
|
+
// --------------------------------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export interface SyntheticRigSpec {
|
|
150
|
+
readonly name: string;
|
|
151
|
+
/** Convention label for the report. */
|
|
152
|
+
readonly convention: string;
|
|
153
|
+
/** Node names a GLB of this convention would expose (drives `inferHumanoidRig`). */
|
|
154
|
+
readonly nodeNames: readonly string[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Mixamo joints (`mixamorig:` namespace, full humanoid → expect grade A). */
|
|
158
|
+
const MIXAMO_NODES: readonly string[] = [
|
|
159
|
+
"mixamorig:Hips",
|
|
160
|
+
"mixamorig:Spine",
|
|
161
|
+
"mixamorig:Spine1",
|
|
162
|
+
"mixamorig:Spine2",
|
|
163
|
+
"mixamorig:Neck",
|
|
164
|
+
"mixamorig:Head",
|
|
165
|
+
"mixamorig:LeftShoulder",
|
|
166
|
+
"mixamorig:LeftArm",
|
|
167
|
+
"mixamorig:LeftForeArm",
|
|
168
|
+
"mixamorig:LeftHand",
|
|
169
|
+
"mixamorig:RightShoulder",
|
|
170
|
+
"mixamorig:RightArm",
|
|
171
|
+
"mixamorig:RightForeArm",
|
|
172
|
+
"mixamorig:RightHand",
|
|
173
|
+
"mixamorig:LeftUpLeg",
|
|
174
|
+
"mixamorig:LeftLeg",
|
|
175
|
+
"mixamorig:LeftFoot",
|
|
176
|
+
"mixamorig:LeftToeBase",
|
|
177
|
+
"mixamorig:RightUpLeg",
|
|
178
|
+
"mixamorig:RightLeg",
|
|
179
|
+
"mixamorig:RightFoot",
|
|
180
|
+
"mixamorig:RightToeBase"
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
/** Blender / Rigify-ish names (full humanoid → expect grade A). */
|
|
184
|
+
const BLENDER_NODES: readonly string[] = [
|
|
185
|
+
"pelvis",
|
|
186
|
+
"spine",
|
|
187
|
+
"chest",
|
|
188
|
+
"upper_chest",
|
|
189
|
+
"neck",
|
|
190
|
+
"head",
|
|
191
|
+
"shoulder.L",
|
|
192
|
+
"upper_arm.L",
|
|
193
|
+
"forearm.L",
|
|
194
|
+
"hand.L",
|
|
195
|
+
"shoulder.R",
|
|
196
|
+
"upper_arm.R",
|
|
197
|
+
"forearm.R",
|
|
198
|
+
"hand.R",
|
|
199
|
+
"thigh.L",
|
|
200
|
+
"shin.L",
|
|
201
|
+
"foot.L",
|
|
202
|
+
"toe.L",
|
|
203
|
+
"thigh.R",
|
|
204
|
+
"shin.R",
|
|
205
|
+
"foot.R",
|
|
206
|
+
"toe.R"
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
/** Unreal Engine mannequin names (full humanoid → expect grade A). */
|
|
210
|
+
const UE_NODES: readonly string[] = [
|
|
211
|
+
"pelvis",
|
|
212
|
+
"spine_01",
|
|
213
|
+
"spine_02",
|
|
214
|
+
"spine_03",
|
|
215
|
+
"neck_01",
|
|
216
|
+
"head",
|
|
217
|
+
"clavicle_l",
|
|
218
|
+
"upperarm_l",
|
|
219
|
+
"lowerarm_l",
|
|
220
|
+
"hand_l",
|
|
221
|
+
"clavicle_r",
|
|
222
|
+
"upperarm_r",
|
|
223
|
+
"lowerarm_r",
|
|
224
|
+
"hand_r",
|
|
225
|
+
"thigh_l",
|
|
226
|
+
"calf_l",
|
|
227
|
+
"foot_l",
|
|
228
|
+
"ball_l",
|
|
229
|
+
"thigh_r",
|
|
230
|
+
"calf_r",
|
|
231
|
+
"foot_r",
|
|
232
|
+
"ball_r"
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
/** Sparse 8-node mascot: torso + head + stub arms, NO forearms/hands, NO legs → expect grade C. */
|
|
236
|
+
const MASCOT_NODES: readonly string[] = [
|
|
237
|
+
"root",
|
|
238
|
+
"spine",
|
|
239
|
+
"neck",
|
|
240
|
+
"head",
|
|
241
|
+
"arm_L",
|
|
242
|
+
"arm_R",
|
|
243
|
+
"eye_L",
|
|
244
|
+
"eye_R"
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
/** Rich humanoid (descriptive anatomical names, full chains → expect grade A). */
|
|
248
|
+
const RICH_NODES: readonly string[] = [
|
|
249
|
+
"Hips",
|
|
250
|
+
"LowerSpine",
|
|
251
|
+
"Ribcage",
|
|
252
|
+
"UpperChest",
|
|
253
|
+
"Neck",
|
|
254
|
+
"Head",
|
|
255
|
+
"Left Clavicle",
|
|
256
|
+
"Left Humerus",
|
|
257
|
+
"Left Ulna",
|
|
258
|
+
"Left Wrist",
|
|
259
|
+
"Right Clavicle",
|
|
260
|
+
"Right Humerus",
|
|
261
|
+
"Right Ulna",
|
|
262
|
+
"Right Wrist",
|
|
263
|
+
"Left Femur",
|
|
264
|
+
"Left Shin",
|
|
265
|
+
"Left Ankle",
|
|
266
|
+
"Left Ball",
|
|
267
|
+
"Right Femur",
|
|
268
|
+
"Right Shin",
|
|
269
|
+
"Right Ankle",
|
|
270
|
+
"Right Ball"
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Props-only / no usable skeleton (root + decorative nodes, no spine/head/limbs) → expect grade D.
|
|
275
|
+
* This is the honest "this asset cannot body-act" case; it is reported as refusing body acting.
|
|
276
|
+
*/
|
|
277
|
+
const PROPS_ONLY_NODES: readonly string[] = [
|
|
278
|
+
"root",
|
|
279
|
+
"Armature",
|
|
280
|
+
"Camera",
|
|
281
|
+
"Light",
|
|
282
|
+
"GroundPlane",
|
|
283
|
+
"Cube.001"
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
export const SYNTHETIC_RIGS: readonly SyntheticRigSpec[] = [
|
|
287
|
+
{ name: "mixamo", convention: "Mixamo (mixamorig:)", nodeNames: MIXAMO_NODES },
|
|
288
|
+
{ name: "blender", convention: "Blender / Rigify (.L/.R)", nodeNames: BLENDER_NODES },
|
|
289
|
+
{ name: "ue", convention: "Unreal Engine mannequin (_l/_r)", nodeNames: UE_NODES },
|
|
290
|
+
{ name: "mascot8", convention: "Sparse 8-node mascot", nodeNames: MASCOT_NODES },
|
|
291
|
+
{ name: "rich", convention: "Rich anatomical humanoid", nodeNames: RICH_NODES },
|
|
292
|
+
{ name: "props-only", convention: "Props-only / no usable skeleton", nodeNames: PROPS_ONLY_NODES }
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
export interface RigEvidence {
|
|
296
|
+
readonly name: string;
|
|
297
|
+
readonly convention: string;
|
|
298
|
+
readonly grade: RigGrade;
|
|
299
|
+
readonly gradeReasons: readonly string[];
|
|
300
|
+
readonly mappedBoneCount: number;
|
|
301
|
+
/** Canonical bones the standard clip's pose actually retargeted onto this rig. */
|
|
302
|
+
readonly retargetedBones: string[];
|
|
303
|
+
readonly retargetCoverage: number;
|
|
304
|
+
/** Honest: true ⇒ the rig is too sparse to drive body acting (grade D). */
|
|
305
|
+
readonly refusesBodyActing: boolean;
|
|
306
|
+
readonly clip: string;
|
|
307
|
+
readonly firstFinalDiff: number;
|
|
308
|
+
readonly strip: SkeletonStripResult;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Retarget one standard clip onto a synthetic rig and produce a skeleton-overlay strip restricted to
|
|
313
|
+
* the bones that rig actually maps. The strip therefore visualizes *this rig's* coverage of the clip:
|
|
314
|
+
* a full humanoid draws the whole figure, a sparse mascot draws only its torso/head.
|
|
315
|
+
*/
|
|
316
|
+
export function buildRigEvidence(spec: SyntheticRigSpec, clipIntent: string): RigEvidence {
|
|
317
|
+
const rig: HumanoidRigDefinition = inferHumanoidRig(spec.nodeNames, {
|
|
318
|
+
id: `synthetic-${spec.name}`,
|
|
319
|
+
name: spec.convention
|
|
320
|
+
});
|
|
321
|
+
const report = gradeRig(rig);
|
|
322
|
+
|
|
323
|
+
// Which canonical bones map on this rig — used to mask the projection (and to retarget).
|
|
324
|
+
const mappedBones = Object.keys(rig.bones).filter((b) => {
|
|
325
|
+
const binding = (rig.bones as Record<string, { name?: string } | undefined>)[b];
|
|
326
|
+
return Boolean(binding && typeof binding.name === "string" && binding.name.length > 0);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Build the retargeting map standard-library-rig → this rig and retarget the clip's first pose so
|
|
330
|
+
// we can report the bones that actually carried motion (coverage), even for D rigs.
|
|
331
|
+
const map = createHumanoidRetargetingMap(STANDARD_LIBRARY_RIG, rig, { minRequiredCoverage: 0 });
|
|
332
|
+
const retargetedBones = Object.keys(map.bindings).sort();
|
|
333
|
+
|
|
334
|
+
const refusesBodyActing = report.grade === "D";
|
|
335
|
+
|
|
336
|
+
// Mask the strip to the rig's mapped bones (a sparse rig draws a sparser figure). For D rigs we
|
|
337
|
+
// still render whatever (near-empty) skeleton it has — the strip + the refusal message together
|
|
338
|
+
// are the honest evidence.
|
|
339
|
+
const strip = buildSkeletonStrip({
|
|
340
|
+
intent: clipIntent,
|
|
341
|
+
bonesAllow: mappedBones
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
name: spec.name,
|
|
346
|
+
convention: spec.convention,
|
|
347
|
+
grade: report.grade,
|
|
348
|
+
gradeReasons: report.reasons,
|
|
349
|
+
mappedBoneCount: report.mappedBoneCount,
|
|
350
|
+
retargetedBones,
|
|
351
|
+
retargetCoverage: map.requiredCoverage,
|
|
352
|
+
refusesBodyActing,
|
|
353
|
+
clip: clipIntent,
|
|
354
|
+
firstFinalDiff: strip.firstFinalMaxDiff,
|
|
355
|
+
strip
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Build evidence for all five synthetic rigs (B5) against one shared clip. */
|
|
360
|
+
export function buildAllRigEvidence(clipIntent = "gesture"): RigEvidence[] {
|
|
361
|
+
return SYNTHETIC_RIGS.map((spec) => buildRigEvidence(spec, clipIntent));
|
|
362
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preview-server.ts — a watch-and-preview server for the studio loop: it watches a
|
|
3
|
+
* document JSON and renders a short PREVIEW RANGE to PNGs on every edit, so the agent/creator
|
|
4
|
+
* sees changes quickly instead of re-rendering the whole 60s.
|
|
5
|
+
*
|
|
6
|
+
* HONEST: this renders the range per change (windowed, fast-ish via the `#1` readback). The
|
|
7
|
+
* further optimization — keeping the browser/page WARM across edits (no per-render startup) +
|
|
8
|
+
* parallel pages — is the productionization noted in the design doc.
|
|
9
|
+
*
|
|
10
|
+
* Run: AURA_DOCUMENT=<doc.json> AURA_PREVIEW_RANGE=0-6 \
|
|
11
|
+
* pnpm exec tsx --tsconfig tsconfig.base.json packages/create-aura3d/templates/animation-studio/scripts/preview-server.ts
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, watch } from "node:fs";
|
|
16
|
+
import { dirname, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const REPO = resolve(__dirname, "..", "..", "..", "..", "..");
|
|
21
|
+
const DOC = process.env.AURA_DOCUMENT
|
|
22
|
+
? resolve(process.env.AURA_DOCUMENT)
|
|
23
|
+
: resolve(__dirname, "..", "dist", "generated", "studio-edited.document.json");
|
|
24
|
+
const RANGE = process.env.AURA_PREVIEW_RANGE ?? "0-6";
|
|
25
|
+
const OUT = process.env.AURA_OUTPUT_DIR ?? "dist/episodes/studio-preview";
|
|
26
|
+
|
|
27
|
+
function renderPreview(): void {
|
|
28
|
+
const t = Date.now();
|
|
29
|
+
const r = spawnSync(
|
|
30
|
+
"pnpm",
|
|
31
|
+
["exec", "tsx", "--tsconfig", "tsconfig.base.json", "packages/create-aura3d/templates/animation-studio/scripts/render-live.ts"],
|
|
32
|
+
{ cwd: REPO, stdio: "ignore", env: { ...process.env, AURA_DOCUMENT: DOC, AURA_OUTPUT_DIR: OUT, AURA_PREVIEW_RANGE: RANGE } }
|
|
33
|
+
);
|
|
34
|
+
console.log(`preview ${r.status === 0 ? "ok" : "FAILED"} (range ${RANGE}) in ${((Date.now() - t) / 1000).toFixed(1)}s → ${OUT}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function main(): void {
|
|
38
|
+
if (!existsSync(DOC)) {
|
|
39
|
+
console.error(`document not found: ${DOC}\nGenerate one first (studio.ts / generate-scene.ts).`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log(`preview-server watching ${DOC} (range ${RANGE}) ...`);
|
|
44
|
+
renderPreview();
|
|
45
|
+
let pending: NodeJS.Timeout | null = null;
|
|
46
|
+
watch(DOC, () => {
|
|
47
|
+
if (pending) clearTimeout(pending);
|
|
48
|
+
pending = setTimeout(renderPreview, 250); // debounce rapid writes
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main();
|