create-aura3d 1.1.2 → 1.3.0

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.
Files changed (215) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -3
  4. package/dist/index.js.map +1 -1
  5. package/package.json +11 -9
  6. package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
  7. package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
  8. package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
  9. package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
  10. package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
  11. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
  12. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
  13. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
  14. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
  15. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
  16. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
  17. package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
  18. package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
  19. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
  20. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
  21. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
  22. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
  23. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
  24. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
  25. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
  26. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
  27. package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
  28. package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
  29. package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
  30. package/templates/animation-studio/AGENTS.md +71 -0
  31. package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
  32. package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
  33. package/templates/animation-studio/aura.assets.json +669 -0
  34. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  35. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  36. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  37. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  38. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  39. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  40. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  41. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
  42. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  43. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  44. package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
  45. package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
  46. package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
  47. package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
  48. package/templates/animation-studio/dist/scene/integ.document.json +1 -0
  49. package/templates/animation-studio/dist/scene/working.document.json +723 -0
  50. package/templates/animation-studio/dist/scene/working.history.json +1 -0
  51. package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
  52. package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
  53. package/templates/animation-studio/package.json +32 -0
  54. package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
  55. package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
  56. package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
  57. package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
  58. package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
  59. package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
  60. package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
  61. package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
  62. package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
  63. package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
  64. package/templates/animation-studio/public/clip-library/idle.json +23035 -0
  65. package/templates/animation-studio/public/clip-library/manifest.json +154 -0
  66. package/templates/animation-studio/public/clip-library/nod.json +173903 -0
  67. package/templates/animation-studio/public/clip-library/point.json +69879 -0
  68. package/templates/animation-studio/public/clip-library/react.json +66084 -0
  69. package/templates/animation-studio/public/clip-library/run.json +9648 -0
  70. package/templates/animation-studio/public/clip-library/talk.json +69879 -0
  71. package/templates/animation-studio/public/clip-library/walk.json +8283 -0
  72. package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
  73. package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
  74. package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
  75. package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
  76. package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
  77. package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
  78. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
  79. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
  80. package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
  81. package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
  82. package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
  83. package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
  84. package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
  85. package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
  86. package/templates/animation-studio/scripts/anim.ts +103 -0
  87. package/templates/animation-studio/scripts/animation-scene.ts +457 -0
  88. package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
  89. package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
  90. package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
  91. package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
  92. package/templates/animation-studio/scripts/build-characters.ts +1543 -0
  93. package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
  94. package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
  95. package/templates/animation-studio/scripts/determinism-check.ts +48 -0
  96. package/templates/animation-studio/scripts/generate-scene.ts +95 -0
  97. package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
  98. package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
  99. package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
  100. package/templates/animation-studio/scripts/preview-server.ts +52 -0
  101. package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
  102. package/templates/animation-studio/scripts/render-core.ts +360 -0
  103. package/templates/animation-studio/scripts/render-live.ts +813 -0
  104. package/templates/animation-studio/scripts/render-parallel.ts +70 -0
  105. package/templates/animation-studio/scripts/render-server.ts +119 -0
  106. package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
  107. package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
  108. package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
  109. package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
  110. package/templates/animation-studio/scripts/texture-bake.ts +297 -0
  111. package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
  112. package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
  113. package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
  114. package/templates/animation-studio/src/animation-performance.ts +556 -0
  115. package/templates/animation-studio/src/aura-assets.ts +71 -0
  116. package/templates/animation-studio/src/character.ts +40 -0
  117. package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
  118. package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
  119. package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
  120. package/templates/animation-studio/src/empty-document.ts +77 -0
  121. package/templates/animation-studio/src/episode-document.ts +442 -0
  122. package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
  123. package/templates/animation-studio/src/fidelity.ts +129 -0
  124. package/templates/animation-studio/src/graph.ts +14 -0
  125. package/templates/animation-studio/src/procedural-hdri.ts +243 -0
  126. package/templates/animation-studio/src/profile.ts +97 -0
  127. package/templates/animation-studio/src/render-live-route.ts +42 -0
  128. package/templates/animation-studio/src/render-modes.ts +137 -0
  129. package/templates/animation-studio/src/scene-player.ts +867 -0
  130. package/templates/animation-studio/src/set-templates.ts +361 -0
  131. package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
  132. package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
  133. package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
  134. package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
  135. package/templates/animation-studio/tsconfig.json +26 -0
  136. package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
  137. package/templates/character-controller/README.md +33 -0
  138. package/templates/character-controller/index.html +19 -0
  139. package/templates/character-controller/package.json +23 -0
  140. package/templates/character-controller/playwright.config.ts +14 -0
  141. package/templates/character-controller/src/controller.ts +44 -0
  142. package/templates/character-controller/src/main.ts +62 -0
  143. package/templates/character-controller/tests/route-health.spec.ts +26 -0
  144. package/templates/character-controller/tests/screenshot.spec.ts +9 -0
  145. package/templates/character-controller/tsconfig.json +15 -0
  146. package/templates/cinematic-scene/package.json +1 -1
  147. package/templates/episode-builder/README.md +4 -4
  148. package/templates/episode-builder/package.json +1 -1
  149. package/templates/episode-builder/src/aura-assets.ts +1 -1
  150. package/templates/episode-builder/src/builder.ts +4 -4
  151. package/templates/episode-builder/src/episode.ts +21 -21
  152. package/templates/episode-builder/src/main.ts +20 -20
  153. package/templates/episode-builder/src/render-plan.ts +13 -13
  154. package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
  155. package/templates/fighting-game/package.json +1 -1
  156. package/templates/mini-game/package.json +1 -1
  157. package/templates/product-viewer/package.json +1 -1
  158. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
  159. package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
  160. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
  161. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
  162. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
  163. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
  164. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
  165. package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
  166. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
  167. package/templates/cartoon-studio/aura.assets.json +0 -400
  168. package/templates/cartoon-studio/package.json +0 -33
  169. package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
  170. package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
  171. package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
  172. package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
  173. package/templates/cartoon-studio/scripts/episode.ts +0 -323
  174. package/templates/cartoon-studio/scripts/render-live.ts +0 -873
  175. package/templates/cartoon-studio/src/aura-assets.ts +0 -64
  176. package/templates/cartoon-studio/src/characters.ts +0 -65
  177. package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
  178. package/templates/cartoon-studio/src/episode.ts +0 -306
  179. package/templates/cartoon-studio/src/main.ts +0 -400
  180. package/templates/cartoon-studio/src/render-live-route.ts +0 -913
  181. package/templates/cartoon-studio/src/render-plan.ts +0 -567
  182. package/templates/cartoon-studio/src/review.ts +0 -99
  183. package/templates/cartoon-studio/src/studio.ts +0 -101
  184. package/templates/cartoon-studio/test-results/.last-run.json +0 -4
  185. package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
  186. package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
  187. package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
  188. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
  189. package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
  190. package/templates/cartoon-studio/tsconfig.json +0 -14
  191. package/templates/prompt-cartoon-channel/index.html +0 -13
  192. package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
  193. /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
  194. /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
  195. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
  196. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
  197. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
  198. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
  199. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
  200. /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
  201. /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
  202. /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
  203. /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
  204. /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
  205. /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  206. /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
  207. /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
  208. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
  209. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
  210. /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
  211. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
  212. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
  213. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
  214. /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
  215. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * regen-manifest.ts — regenerate the miko + luma entries in aura.assets.json from
3
+ * the ACTUAL bytes of the authored GLBs (hash, bounds, clips, skeleton, morphs),
4
+ * using the same CLI inspector the rest of the toolchain uses. No fabricated values:
5
+ * every field below is read from the file or copied verbatim from the existing
6
+ * moonGarden entry (whose hash is already in sync).
7
+ *
8
+ * Run from repo root:
9
+ * pnpm exec tsx --tsconfig tsconfig.base.json \
10
+ * packages/create-aura3d/templates/animation-studio/scripts/regen-manifest.ts
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ import { readFileSync, writeFileSync } from "node:fs";
14
+ import { dirname, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { inspectAsset } from "../../../../aura3d-cli/src/index.ts";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const TEMPLATE_ROOT = resolve(__dirname, "..");
20
+ const MANIFEST_PATH = resolve(TEMPLATE_ROOT, "aura.assets.json");
21
+
22
+ interface LeadSpec {
23
+ readonly id: string;
24
+ readonly file: string; // relative to template root
25
+ readonly name: string;
26
+ readonly objaverseId: string;
27
+ readonly authorLabel: string;
28
+ }
29
+
30
+ // The two authored procedural mascots that make up the guaranteed-good cast.
31
+ const LEADS: readonly LeadSpec[] = [
32
+ {
33
+ id: "miko",
34
+ file: "public/aura-assets/miko.catalog.glb",
35
+ name: "Miko",
36
+ objaverseId: "aura3d:miko-authored-mascot",
37
+ authorLabel: "Aura3D"
38
+ },
39
+ {
40
+ id: "luma",
41
+ file: "public/aura-assets/luma2.catalog.glb",
42
+ name: "Luma",
43
+ objaverseId: "aura3d:luma-authored-mascot",
44
+ authorLabel: "Aura3D"
45
+ }
46
+ ];
47
+
48
+ function sha256(path: string): string {
49
+ return `sha256-${createHash("sha256").update(readFileSync(path)).digest("hex")}`;
50
+ }
51
+
52
+ function buildLeadEntry(spec: LeadSpec): Record<string, unknown> {
53
+ const r = inspectAsset({
54
+ projectDir: TEMPLATE_ROOT,
55
+ file: spec.file,
56
+ animation: true,
57
+ humanoid: true,
58
+ skeleton: true,
59
+ morphs: true,
60
+ license: true
61
+ });
62
+ const filePath = resolve(TEMPLATE_ROOT, spec.file);
63
+ const hash = sha256(filePath);
64
+ const url = `/${spec.file.replace(/^public\//, "")}`;
65
+ const morphNames = r.morphTargets?.targetNames ?? [];
66
+ const hasMouthMorph = morphNames.length > 0;
67
+ const attribution =
68
+ `Aura3D-authored procedural rigged mascot "${spec.name}" (CC0). ` +
69
+ `Skinned ${r.skeleton?.jointCount ?? 0}-joint rig; embedded clips ${r.animations.join(", ")}. ` +
70
+ `Lip-sync is driven by the facial mouth blendshape morph target ` +
71
+ `(${morphNames.join(", ") || "none"}).`;
72
+
73
+ return {
74
+ id: spec.id,
75
+ type: "model",
76
+ format: "glb",
77
+ source: spec.file,
78
+ outputPath: spec.file,
79
+ url,
80
+ hash,
81
+ sizeBytes: r.sizeBytes,
82
+ bounds: r.bounds,
83
+ boundsMetadata: r.boundsMetadata,
84
+ materials: r.materials,
85
+ materialMetadata: r.materialMetadata,
86
+ animations: r.animations,
87
+ animationMetadata: r.animation,
88
+ humanoid: r.humanoid,
89
+ skeleton: r.skeleton,
90
+ morphTargets: r.morphTargets,
91
+ textures: r.textures,
92
+ dependencies: r.dependencies ?? [],
93
+ nodeNames: r.nodeNames,
94
+ provenance: {
95
+ license: "CC0-1.0",
96
+ author: spec.authorLabel,
97
+ sourceFamily: "Aura3D authored cast (build-characters.ts, CC0 procedural mascot)",
98
+ sourcePath: spec.file,
99
+ sourceUrl: `https://aura3d.auraone.ai${url}`,
100
+ attribution,
101
+ evidence: [
102
+ "animation-character",
103
+ "rigged",
104
+ "animated",
105
+ ...(hasMouthMorph ? ["mouth blendshape morph", "facial morph target"] : []),
106
+ "authored",
107
+ "cc0"
108
+ ],
109
+ sha256: hash,
110
+ retrievedAt: "2026-06-07"
111
+ },
112
+ thumbnailUrl: `/aura-assets/${spec.id}.thumb.svg`,
113
+ warnings: r.warnings ?? []
114
+ };
115
+ }
116
+
117
+ function main(): void {
118
+ const manifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8")) as {
119
+ assets: Record<string, unknown>[];
120
+ } & Record<string, unknown>;
121
+
122
+ const byId = new Map<string, Record<string, unknown>>();
123
+ for (const asset of manifest.assets) byId.set(asset.id as string, asset);
124
+
125
+ for (const spec of LEADS) {
126
+ byId.set(spec.id, buildLeadEntry(spec));
127
+ }
128
+
129
+ // Re-sync the moonGarden hash + sizeBytes from disk too (do not change its shape).
130
+ const moon = byId.get("moonGarden");
131
+ if (moon && typeof moon.outputPath === "string") {
132
+ const moonPath = resolve(TEMPLATE_ROOT, moon.outputPath);
133
+ moon.hash = sha256(moonPath);
134
+ moon.sizeBytes = readFileSync(moonPath).byteLength;
135
+ if (moon.provenance && typeof moon.provenance === "object") {
136
+ (moon.provenance as Record<string, unknown>).sha256 = moon.hash;
137
+ }
138
+ }
139
+
140
+ // Preserve manifest ordering: luma, miko, moonGarden (as it was), refreshed.
141
+ const order = ["luma", "miko", "moonGarden"];
142
+ const ordered = order.filter((id) => byId.has(id)).map((id) => byId.get(id)!);
143
+ for (const asset of manifest.assets) {
144
+ const id = asset.id as string;
145
+ if (!order.includes(id) && byId.has(id)) ordered.push(byId.get(id)!);
146
+ }
147
+
148
+ manifest.assets = ordered;
149
+ writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`);
150
+ console.log(`Wrote ${MANIFEST_PATH}`);
151
+ for (const asset of ordered) {
152
+ console.log(` ${asset.id}: ${asset.hash} (${asset.sizeBytes} bytes)`);
153
+ }
154
+ }
155
+
156
+ main();
@@ -0,0 +1,360 @@
1
+ /**
2
+ * render-core.ts — the REUSABLE capture + toon-post + encode pipeline, factored out of
3
+ * render-live.ts so the warm render server (#7, render-server.ts) and any future caller
4
+ * can drive it WITHOUT re-launching Vite/Chromium per render.
5
+ *
6
+ * render-live.ts remains the full-fidelity CLI (it keeps the per-frame proof
7
+ * instrumentation: lip-sync A/B, staged-position pixel diffs, AuraVoice handoff record).
8
+ * This module is the lean, parameterized path: given an already-ready Playwright page +
9
+ * a document, it captures a frame range, applies the exact same toon treatment, burns
10
+ * captions, and encodes a webm. Width/height/fps/low-fi are PARAMETERS here (not module
11
+ * env constants), so one warm page can serve preview and full renders interchangeably.
12
+ */
13
+
14
+ import { createRequire } from "node:module";
15
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
16
+ import { resolve, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { createServer, type ViteDevServer } from "vite";
19
+ import { quantizeToonBand } from "@aura3d/rendering";
20
+ import { createFfmpegFrameEncoderAdapter } from "@aura3d/engine";
21
+ import type { Page } from "@playwright/test";
22
+ import type { EpisodeDocument } from "../src/episode-document.js";
23
+
24
+ export interface RenderProfile {
25
+ readonly width: number;
26
+ readonly height: number;
27
+ readonly frameRate: number;
28
+ readonly lowFi: boolean;
29
+ }
30
+
31
+ /** Standard fidelity profiles (mirrors render-live.ts: full = 960×540@12, low = 480×270@8). */
32
+ export function profileFor(lowFi: boolean): RenderProfile {
33
+ return lowFi ? { width: 480, height: 270, frameRate: 8, lowFi: true } : { width: 960, height: 540, frameRate: 12, lowFi: false };
34
+ }
35
+
36
+ // --- sharp (PNG encode/decode) ---
37
+ export interface SharpModule {
38
+ (input: Buffer | Uint8Array, options?: { raw: { width: number; height: number; channels: number } }): {
39
+ png(): { toBuffer(): Promise<Buffer> };
40
+ };
41
+ }
42
+ export function loadSharp(): SharpModule {
43
+ try {
44
+ const require = createRequire(import.meta.url);
45
+ return require("sharp") as SharpModule;
46
+ } catch {
47
+ // Walk up from the render cwd AND from this module's location to find the pnpm
48
+ // store copy of sharp — in the monorepo the template dir has no local sharp, but a
49
+ // parent (repo root) does. This keeps the render working without a per-template install.
50
+ const seeds = [process.cwd(), dirname(fileURLToPath(import.meta.url))];
51
+ for (const seed of seeds) {
52
+ let dir = seed;
53
+ for (let i = 0; i < 8; i++) {
54
+ const storePkg = resolve(dir, "node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/package.json");
55
+ if (existsSync(storePkg)) return createRequire(storePkg)("sharp") as SharpModule;
56
+ const direct = resolve(dir, "node_modules/sharp/package.json");
57
+ if (existsSync(direct)) return createRequire(direct)("sharp") as SharpModule;
58
+ const parent = resolve(dir, "..");
59
+ if (parent === dir) break;
60
+ dir = parent;
61
+ }
62
+ }
63
+ throw new Error("Could not load `sharp`. Install it: pnpm add -D sharp");
64
+ }
65
+ }
66
+ export const sharp = loadSharp();
67
+
68
+ export async function rawRgbaToPng(pixels: Uint8Array, width: number, height: number): Promise<Uint8Array> {
69
+ const buffer = await sharp(Buffer.from(pixels.buffer, pixels.byteOffset, pixels.byteLength), {
70
+ raw: { width, height, channels: 4 }
71
+ })
72
+ .png()
73
+ .toBuffer();
74
+ return new Uint8Array(buffer);
75
+ }
76
+
77
+ // --- toon post-pass (the single canonical home; render-live.ts imports these) ---
78
+ export function clampByte(value: number): number {
79
+ return value < 0 ? 0 : value > 255 ? 255 : Math.round(value);
80
+ }
81
+ export function aces(x: number): number {
82
+ const a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
83
+ const y = (x * (a * x + b)) / (x * (c * x + d) + e);
84
+ return y < 0 ? 0 : y > 1 ? 1 : y;
85
+ }
86
+ export function inkOutline(pixels: Uint8Array, width: number, height: number, threshold: number): Uint8Array {
87
+ const out = new Uint8Array(pixels);
88
+ const lumaAt = (x: number, y: number): number => {
89
+ const i = (y * width + x) * 4;
90
+ return (0.2126 * pixels[i]! + 0.7152 * pixels[i + 1]! + 0.0722 * pixels[i + 2]!) / 255;
91
+ };
92
+ for (let y = 1; y < height - 1; y += 1) {
93
+ for (let x = 1; x < width - 1; x += 1) {
94
+ const c = lumaAt(x, y);
95
+ const edge = Math.max(
96
+ Math.abs(c - lumaAt(x - 1, y)), Math.abs(c - lumaAt(x + 1, y)),
97
+ Math.abs(c - lumaAt(x, y - 1)), Math.abs(c - lumaAt(x, y + 1))
98
+ );
99
+ if (edge > threshold) {
100
+ const k = Math.min(1, (edge - threshold) / 0.25);
101
+ const f = 1 - 0.75 * k;
102
+ const i = (y * width + x) * 4;
103
+ out[i] = clampByte(pixels[i]! * f);
104
+ out[i + 1] = clampByte(pixels[i + 1]! * f);
105
+ out[i + 2] = clampByte(pixels[i + 2]! * f);
106
+ }
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+ export function boxBlur(src: Float32Array, dst: Float32Array, width: number, height: number, radius: number): void {
112
+ const norm = 1 / (radius * 2 + 1);
113
+ for (let y = 0; y < height; y += 1) {
114
+ const row = y * width;
115
+ let sum = 0;
116
+ for (let x = -radius; x <= radius; x += 1) sum += src[row + Math.min(width - 1, Math.max(0, x))]!;
117
+ for (let x = 0; x < width; x += 1) {
118
+ dst[row + x] = sum * norm;
119
+ sum += src[row + Math.min(width - 1, x + radius + 1)]! - src[row + Math.max(0, x - radius)]!;
120
+ }
121
+ }
122
+ for (let x = 0; x < width; x += 1) {
123
+ let sum = 0;
124
+ for (let y = -radius; y <= radius; y += 1) sum += dst[Math.min(height - 1, Math.max(0, y)) * width + x]!;
125
+ for (let y = 0; y < height; y += 1) {
126
+ src[y * width + x] = sum * norm;
127
+ sum += dst[Math.min(height - 1, y + radius + 1) * width + x]! - dst[Math.max(0, y - radius) * width + x]!;
128
+ }
129
+ }
130
+ }
131
+ export function applyBloom(pixels: Uint8Array, width: number, height: number, options: { threshold: number; intensity: number }): Uint8Array {
132
+ const count = width * height;
133
+ const br = new Float32Array(count), bg = new Float32Array(count), bb = new Float32Array(count);
134
+ const inv = 1 / Math.max(1e-3, 1 - options.threshold);
135
+ for (let p = 0, i = 0; p < count; p += 1, i += 4) {
136
+ const r = pixels[i]! / 255, g = pixels[i + 1]! / 255, b = pixels[i + 2]! / 255;
137
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
138
+ const w = luma > options.threshold ? (luma - options.threshold) * inv : 0;
139
+ br[p] = r * w; bg[p] = g * w; bb[p] = b * w;
140
+ }
141
+ const accR = new Float32Array(count), accG = new Float32Array(count), accB = new Float32Array(count);
142
+ const scratch = new Float32Array(count);
143
+ for (const radius of [3, 7, 15]) {
144
+ const tr = br.slice(), tg = bg.slice(), tb = bb.slice();
145
+ boxBlur(tr, scratch, width, height, radius);
146
+ boxBlur(tg, scratch, width, height, radius);
147
+ boxBlur(tb, scratch, width, height, radius);
148
+ for (let p = 0; p < count; p += 1) { accR[p] += tr[p]!; accG[p] += tg[p]!; accB[p] += tb[p]!; }
149
+ }
150
+ const k = (options.intensity / 3) * 1.6;
151
+ const out = new Uint8Array(pixels.length);
152
+ const base = (c: number, add: number): number => {
153
+ const bn = c / 255, an = Math.min(1, add * k);
154
+ return clampByte((1 - (1 - bn) * (1 - an)) * 255);
155
+ };
156
+ for (let p = 0, i = 0; p < count; p += 1, i += 4) {
157
+ out[i] = base(pixels[i]!, accR[p]!);
158
+ out[i + 1] = base(pixels[i + 1]!, accG[p]!);
159
+ out[i + 2] = base(pixels[i + 2]!, accB[p]!);
160
+ out[i + 3] = pixels[i + 3]!;
161
+ }
162
+ return out;
163
+ }
164
+ export function applyVignette(pixels: Uint8Array, width: number, height: number, strength: number): Uint8Array {
165
+ const out = new Uint8Array(pixels);
166
+ const cx = width / 2, cy = height / 2, maxD = Math.hypot(cx, cy);
167
+ for (let y = 0; y < height; y += 1) {
168
+ for (let x = 0; x < width; x += 1) {
169
+ const d = Math.hypot(x - cx, y - cy) / maxD;
170
+ const t = Math.max(0, (d - 0.6) / 0.4);
171
+ const factor = 1 - strength * (t * t * (3 - 2 * t));
172
+ const i = (y * width + x) * 4;
173
+ out[i] = clampByte(pixels[i]! * factor);
174
+ out[i + 1] = clampByte(pixels[i + 1]! * factor);
175
+ out[i + 2] = clampByte(pixels[i + 2]! * factor);
176
+ }
177
+ }
178
+ return out;
179
+ }
180
+ /** Render style: "toon" = the (tuned) cel look; "pbr" = realistic (skip cel quantize + ink outline). */
181
+ export type RenderStyle = "toon" | "pbr";
182
+
183
+ /** Resolve the effective style: explicit arg wins, else AURA_RENDER_STYLE env, else "toon". */
184
+ export function resolveRenderStyle(style?: RenderStyle): RenderStyle {
185
+ if (style === "toon" || style === "pbr") return style;
186
+ const env = (process.env.AURA_RENDER_STYLE ?? "").trim().toLowerCase();
187
+ return env === "pbr" ? "pbr" : "toon";
188
+ }
189
+
190
+ export function applyToonTreatment(
191
+ pixels: Uint8Array,
192
+ width: number,
193
+ height: number,
194
+ lowFi: boolean,
195
+ style?: RenderStyle
196
+ ): Uint8Array {
197
+ const effectiveStyle = resolveRenderStyle(style);
198
+ // --- tone map + (optional) cel banding + saturation grade ---
199
+ // toon: more bands + a lighter cel-mix so the asset's real texture survives the quantize;
200
+ // pbr: no quantize at all — just the ACES grade so realistic assets read realistically.
201
+ const bands = 8, celMix = 0.32;
202
+ const useCel = effectiveStyle === "toon";
203
+ const toned = new Uint8Array(pixels.length);
204
+ for (let i = 0; i < pixels.length; i += 4) {
205
+ const r = aces(pixels[i]! / 255), g = aces(pixels[i + 1]! / 255), b = aces(pixels[i + 2]! / 255);
206
+ let cr = r, cg = g, cb = b;
207
+ if (useCel) {
208
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
209
+ const q = quantizeToonBand(luma, bands);
210
+ const scale = luma > 1e-4 ? q / luma : q;
211
+ cr = (1 - celMix) * r + celMix * r * scale;
212
+ cg = (1 - celMix) * g + celMix * g * scale;
213
+ cb = (1 - celMix) * b + celMix * b * scale;
214
+ }
215
+ const l = 0.2126 * cr + 0.7152 * cg + 0.0722 * cb;
216
+ // gentle saturation lift for both styles (toon a touch warmer than pbr)
217
+ const sat = useCel ? 1.08 : 1.04;
218
+ cr = l + (cr - l) * sat; cg = l + (cg - l) * sat; cb = l + (cb - l) * sat;
219
+ toned[i] = clampByte(cr * 255); toned[i + 1] = clampByte(cg * 255); toned[i + 2] = clampByte(cb * 255);
220
+ toned[i + 3] = pixels[i + 3]!;
221
+ }
222
+ // ink outline only for the cel look; pbr keeps clean edges.
223
+ const outlined = useCel ? inkOutline(toned, width, height, 0.42) : toned;
224
+ // bloom that halos genuine highlights without flattening color (higher threshold, lower intensity).
225
+ const bloomed = lowFi ? outlined : applyBloom(outlined, width, height, { threshold: 0.82, intensity: 0.45 });
226
+ // softer vignette so corners aren't crushed.
227
+ return applyVignette(bloomed, width, height, 0.18);
228
+ }
229
+
230
+ export async function compositeCaptionPng(png: Uint8Array, text: string, width: number, height: number): Promise<Uint8Array> {
231
+ const t = (text ?? "").trim();
232
+ if (!t) return png;
233
+ const wrap = (s: string, max = 38): string[] => {
234
+ const words = s.split(/\s+/); const out: string[] = []; let cur = "";
235
+ for (const w of words) { if ((`${cur} ${w}`).trim().length > max && cur) { out.push(cur); cur = w; } else cur = `${cur} ${w}`.trim(); }
236
+ if (cur) out.push(cur); return out;
237
+ };
238
+ const lines = t.split(/\r?\n/).flatMap((s) => wrap(s.trim())).filter(Boolean);
239
+ const fontPx = Math.round(height * 0.05);
240
+ const lineH = Math.round(fontPx * 1.22);
241
+ const bottomMargin = Math.round(height * 0.06);
242
+ const totalH = lines.length * lineH;
243
+ const cx = width / 2;
244
+ const firstBaseline = height - bottomMargin - totalH + fontPx;
245
+ const esc = (s: string): string => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
246
+ const tspans = lines.map((ln, i) => `<tspan x="${cx}" y="${firstBaseline + i * lineH}">${esc(ln)}</tspan>`).join("");
247
+ const stroke = Math.max(3, Math.round(fontPx * 0.18));
248
+ const svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
249
+ <text text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="${fontPx}" font-weight="700" fill="#ffffff" stroke="rgba(8,10,18,0.92)" stroke-width="${stroke}" stroke-linejoin="round" paint-order="stroke">${tspans}</text>
250
+ </svg>`;
251
+ const sharpAny = sharp as unknown as (input: Buffer) => { composite(items: { input: Buffer }[]): { png(): { toBuffer(): Promise<Buffer> } } };
252
+ const out = await sharpAny(Buffer.from(png)).composite([{ input: Buffer.from(svg) }]).png().toBuffer();
253
+ return new Uint8Array(out);
254
+ }
255
+
256
+ /** Seek the route at `time` and read back the canvas as raw RGBA, plus the seek proof. */
257
+ export async function seekAndReadPixels(page: Page, time: number, width: number, height: number): Promise<{ proof: { caption?: { text?: string }; shot?: { shotId?: string } }; raw: Uint8Array }> {
258
+ const result = await page.evaluate(
259
+ ({ t, w, h }) => {
260
+ const win = window as unknown as { __auraSeek__: (time: number) => unknown };
261
+ const proof = win.__auraSeek__(t);
262
+ const canvas = document.querySelector("#live-canvas") as HTMLCanvasElement;
263
+ const copy = document.createElement("canvas");
264
+ copy.width = w; copy.height = h;
265
+ copy.getContext("2d")!.drawImage(canvas, 0, 0, w, h);
266
+ return { proof, dataUrl: copy.toDataURL("image/png") };
267
+ },
268
+ { t: time, w: width, h: height }
269
+ );
270
+ const b64 = result.dataUrl.slice(result.dataUrl.indexOf(",") + 1);
271
+ const decoded = await (sharp as unknown as (b: Buffer) => { ensureAlpha(): { raw(): { toBuffer(o: { resolveWithObject: true }): Promise<{ data: Buffer }> } } })(Buffer.from(b64, "base64"))
272
+ .ensureAlpha().raw().toBuffer({ resolveWithObject: true });
273
+ return {
274
+ proof: result.proof as { caption?: { text?: string }; shot?: { shotId?: string } },
275
+ raw: new Uint8Array(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength)
276
+ };
277
+ }
278
+
279
+ export interface RangeResult {
280
+ readonly frameCount: number;
281
+ readonly video: string;
282
+ readonly framesDir: string;
283
+ readonly firstSec: number;
284
+ readonly lastSec: number;
285
+ readonly profile: RenderProfile;
286
+ }
287
+
288
+ /**
289
+ * Capture [firstSec, lastSec) of an already-ready page, toon-treat + caption every frame,
290
+ * and encode a webm into `outDir`. The page must already have the target document mounted
291
+ * (the route's __auraSeek__ hook ready). Returns the output paths + frame count.
292
+ */
293
+ export async function renderRange(
294
+ page: Page,
295
+ opts: { document: EpisodeDocument; firstSec: number; lastSec: number; profile: RenderProfile; outDir: string }
296
+ ): Promise<RangeResult> {
297
+ const { width, height, frameRate, lowFi } = opts.profile;
298
+ const framesDir = resolve(opts.outDir, "frames");
299
+ mkdirSync(framesDir, { recursive: true });
300
+ const firstFrame = Math.max(0, Math.round(opts.firstSec * frameRate));
301
+ const lastFrame = Math.min(Math.round(opts.document.duration * frameRate), Math.round(opts.lastSec * frameRate));
302
+
303
+ const frames: { time: number; png: Uint8Array }[] = [];
304
+ for (let i = firstFrame; i < lastFrame; i += 1) {
305
+ const t = i / frameRate;
306
+ const { proof, raw } = await seekAndReadPixels(page, t, width, height);
307
+ const capText = (proof.caption?.text ?? "").trim();
308
+ const treated = applyToonTreatment(raw, width, height, lowFi);
309
+ const basePng = await rawRgbaToPng(treated, width, height);
310
+ const png = await compositeCaptionPng(basePng, capText, width, height);
311
+ frames.push({ time: t, png });
312
+ }
313
+ if (frames.length === 0) throw new Error("renderRange captured no frames.");
314
+ writeFileSync(resolve(framesDir, "first.png"), frames[0]!.png);
315
+ writeFileSync(resolve(framesDir, "final.png"), frames[frames.length - 1]!.png);
316
+
317
+ const encoder = await createFfmpegFrameEncoderAdapter({ codec: "vp9", container: "webm", frameRate });
318
+ for (let index = 0; index < frames.length; index += 1) {
319
+ encoder.encode({ frame: index, time: frames[index]!.time, viewport: { width, height }, image: frames[index]!.png });
320
+ }
321
+ const finalized = await encoder.finalize({
322
+ codec: "vp9", container: "webm", mimeType: "video/webm; codecs=vp9", frameRate,
323
+ viewport: { width, height }, frameCount: frames.length, duration: frames.length / frameRate, byteLength: 0, chunks: []
324
+ });
325
+ if (!(finalized instanceof Uint8Array) || finalized.byteLength === 0) throw new Error("ffmpeg produced no video bytes.");
326
+ const videoPath = resolve(opts.outDir, "episode-3d.webm");
327
+ writeFileSync(videoPath, finalized);
328
+ return { frameCount: frames.length, video: videoPath, framesDir, firstSec: opts.firstSec, lastSec: opts.lastSec, profile: opts.profile };
329
+ }
330
+
331
+ /** Start a Vite dev server rooted at the template, aliasing @aura3d/* to the in-repo dist build. */
332
+ export async function startWarmVite(templateRoot: string, monorepoRoot: string): Promise<ViteDevServer> {
333
+ const DIST = (p: string): string => resolve(monorepoRoot, "dist", p);
334
+ const distBuildExists = existsSync(DIST("engine/advanced-runtime/index.js"));
335
+ const server = await createServer({
336
+ root: templateRoot,
337
+ configFile: false,
338
+ ...(distBuildExists
339
+ ? {
340
+ resolve: {
341
+ alias: [
342
+ { find: /^@aura3d\/engine$/, replacement: DIST("engine/agent-api/index.js") },
343
+ { find: /^@aura3d\/engine\/advanced-runtime$/, replacement: DIST("engine/advanced-runtime/index.js") },
344
+ { find: /^@aura3d\/engine\/production-runtime$/, replacement: DIST("engine/production-runtime/index.js") },
345
+ { find: /^@aura3d\/engine\/assets\/browser$/, replacement: DIST("assets/browser-index.js") },
346
+ { find: /^@aura3d\/engine\/rendering$/, replacement: DIST("rendering/index.js") },
347
+ { find: /^@aura3d\/engine\/scene$/, replacement: DIST("scene/index.js") },
348
+ { find: /^@aura3d\/rendering$/, replacement: DIST("rendering/index.js") },
349
+ { find: /^@aura3d\/scene$/, replacement: DIST("scene/index.js") },
350
+ { find: /^@aura3d\/assets\/browser$/, replacement: DIST("assets/browser-index.js") }
351
+ ]
352
+ }
353
+ }
354
+ : {}),
355
+ server: { host: "127.0.0.1", port: 0, hmr: false },
356
+ logLevel: "warn"
357
+ });
358
+ await server.listen();
359
+ return server;
360
+ }