create-aura3d 1.0.10 → 1.1.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.
Files changed (61) hide show
  1. package/dist/cli.js +0 -0
  2. package/package.json +2 -2
  3. package/templates/cartoon-channel/README.md +38 -0
  4. package/templates/cartoon-channel/aura.assets.json +1364 -0
  5. package/templates/cartoon-channel/package.json +3 -2
  6. package/templates/cartoon-channel/public/aura-assets/luma.047f5e5f.glb +0 -0
  7. package/templates/cartoon-channel/public/aura-assets/luma.thumb.svg +1 -0
  8. package/templates/cartoon-channel/public/aura-assets/miko.047f5e5f.glb +0 -0
  9. package/templates/cartoon-channel/public/aura-assets/miko.thumb.svg +1 -0
  10. package/templates/cartoon-channel/public/aura-assets/moon-garden-feature-frame.png +0 -0
  11. package/templates/cartoon-channel/src/aura-assets.ts +5 -11
  12. package/templates/cartoon-channel/src/concept-episode-2-5d.css +159 -0
  13. package/templates/cartoon-channel/src/concept-episode-2-5d.ts +155 -0
  14. package/templates/cartoon-channel/src/image-puppet-episode.css +290 -0
  15. package/templates/cartoon-channel/src/image-puppet-episode.ts +139 -0
  16. package/templates/cartoon-channel/src/main.ts +89 -71
  17. package/templates/cartoon-channel/src/puppet-episode-2d.css +526 -0
  18. package/templates/cartoon-channel/src/puppet-episode-2d.ts +189 -0
  19. package/templates/cartoon-channel/src/render-plan.ts +1 -1
  20. package/templates/cartoon-channel/src/sample-episode-visual.css +457 -0
  21. package/templates/cartoon-channel/src/sample-episode-visual.ts +185 -0
  22. package/templates/cartoon-channel/test-results/.last-run.json +4 -0
  23. package/templates/cartoon-channel/tests/concept-2-5d-animation.spec.ts +18 -0
  24. package/templates/cartoon-channel/tests/concept-2-5d.spec.ts +30 -0
  25. package/templates/cartoon-channel/tests/image-puppet-animation.spec.ts +18 -0
  26. package/templates/cartoon-channel/tests/image-puppet.spec.ts +29 -0
  27. package/templates/cartoon-channel/tests/puppet-2d-animation.spec.ts +18 -0
  28. package/templates/cartoon-channel/tests/puppet-2d.spec.ts +26 -0
  29. package/templates/cartoon-channel/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  30. package/templates/cartoon-channel/tests/sample-episode-visual.spec.ts +45 -0
  31. package/templates/cartoon-channel/tests/storyboard-playback.spec.ts +1 -1
  32. package/templates/cartoon-studio/README.md +36 -0
  33. package/templates/cartoon-studio/aura.assets.json +157 -0
  34. package/templates/cartoon-studio/package-lock.json +2251 -0
  35. package/templates/cartoon-studio/package.json +13 -2
  36. package/templates/cartoon-studio/public/aura-assets/luma.047f5e5f.glb +0 -0
  37. package/templates/cartoon-studio/public/aura-assets/luma.humanoid-fixture.glb +0 -0
  38. package/templates/cartoon-studio/public/aura-assets/luma.thumb.svg +1 -0
  39. package/templates/cartoon-studio/public/aura-assets/miko.047f5e5f.glb +0 -0
  40. package/templates/cartoon-studio/public/aura-assets/miko.thumb.svg +1 -0
  41. package/templates/cartoon-studio/public/aura-assets/moonGarden.gltf +183 -0
  42. package/templates/cartoon-studio/scripts/episode.ts +276 -0
  43. package/templates/cartoon-studio/src/aura-assets.ts +55 -8
  44. package/templates/cartoon-studio/src/characters.ts +38 -1
  45. package/templates/cartoon-studio/src/episode-renderer.ts +605 -0
  46. package/templates/cartoon-studio/src/episode.ts +12 -6
  47. package/templates/cartoon-studio/src/main.ts +109 -41
  48. package/templates/cartoon-studio/src/render-plan.ts +162 -0
  49. package/templates/cartoon-studio/src/review.ts +99 -0
  50. package/templates/cartoon-studio/test-results/.last-run.json +4 -0
  51. package/templates/cartoon-studio/tests/episode-render.spec.ts +44 -0
  52. package/templates/cartoon-studio/tests/motion-quality.spec.ts +45 -0
  53. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +18 -10
  54. package/templates/cartoon-studio/tests/visual-review.spec.ts +21 -0
  55. package/templates/cinematic-scene/package.json +1 -1
  56. package/templates/episode-builder/package.json +1 -1
  57. package/templates/fighting-game/package.json +1 -1
  58. package/templates/mini-game/package.json +1 -1
  59. package/templates/product-viewer/package.json +1 -1
  60. package/templates/product-viewer/tests/screenshot.spec.ts +8 -16
  61. package/templates/prompt-cartoon-channel/package.json +1 -1
@@ -5,16 +5,27 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite --host 0.0.0.0",
8
+ "episode:plan": "tsx scripts/episode.ts plan",
9
+ "episode:preview": "tsx scripts/episode.ts preview",
10
+ "episode:render": "tsx scripts/episode.ts render",
11
+ "episode:package": "tsx scripts/episode.ts package",
12
+ "episode:review": "tsx scripts/episode.ts review",
13
+ "episode:verify": "npm run episode:render && npm run episode:package && npm run episode:review && tsx scripts/episode.ts verify",
8
14
  "typecheck": "tsc --noEmit",
9
15
  "build": "npm run typecheck && vite build",
10
16
  "preview": "vite preview --host 0.0.0.0",
11
- "test": "playwright test tests/route-health.spec.ts tests/storyboard-playback.spec.ts --workers=1"
17
+ "test": "playwright test tests/route-health.spec.ts tests/storyboard-playback.spec.ts tests/episode-render.spec.ts tests/motion-quality.spec.ts tests/visual-review.spec.ts --workers=1"
12
18
  },
13
19
  "dependencies": {
14
- "@aura3d/engine": "1.0.10"
20
+ "@aura3d/animation": "1.1.0",
21
+ "@aura3d/assets": "1.1.0",
22
+ "@aura3d/core": "1.1.0",
23
+ "@aura3d/physics": "1.1.0",
24
+ "@aura3d/engine": "1.1.0"
15
25
  },
16
26
  "devDependencies": {
17
27
  "@playwright/test": "^1.52.0",
28
+ "tsx": "^4.20.6",
18
29
  "typescript": "^5.9.3",
19
30
  "vite": "^7.2.4"
20
31
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">luma 0.066x0.026x0.017</text></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">miko 0.066x0.026x0.017</text></svg>
@@ -0,0 +1,183 @@
1
+ {
2
+ "asset": {
3
+ "version": "2.0",
4
+ "generator": "Aura3D cartoon-studio local starter set anchor"
5
+ },
6
+ "scene": 0,
7
+ "scenes": [
8
+ {
9
+ "name": "Moon Garden starter set anchor",
10
+ "nodes": [0, 1, 2]
11
+ }
12
+ ],
13
+ "nodes": [
14
+ {
15
+ "name": "moon garden floor anchor",
16
+ "mesh": 0,
17
+ "translation": [0, 0, 0]
18
+ },
19
+ {
20
+ "name": "moon garden glow stone anchor",
21
+ "mesh": 1,
22
+ "translation": [0.45, 0.04, 0.25]
23
+ },
24
+ {
25
+ "name": "moon garden skyline anchor",
26
+ "mesh": 2,
27
+ "translation": [0, 0, -0.65]
28
+ }
29
+ ],
30
+ "meshes": [
31
+ {
32
+ "name": "moon garden floor",
33
+ "primitives": [
34
+ {
35
+ "attributes": {
36
+ "POSITION": 0
37
+ },
38
+ "indices": 1,
39
+ "material": 0
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "name": "glow stone marker",
45
+ "primitives": [
46
+ {
47
+ "attributes": {
48
+ "POSITION": 2
49
+ },
50
+ "indices": 3,
51
+ "material": 1
52
+ }
53
+ ]
54
+ },
55
+ {
56
+ "name": "skyline marker",
57
+ "primitives": [
58
+ {
59
+ "attributes": {
60
+ "POSITION": 4
61
+ },
62
+ "indices": 5,
63
+ "material": 2
64
+ }
65
+ ]
66
+ }
67
+ ],
68
+ "materials": [
69
+ {
70
+ "name": "soft blue moon path",
71
+ "pbrMetallicRoughness": {
72
+ "baseColorFactor": [0.06, 0.18, 0.28, 1],
73
+ "roughnessFactor": 0.82,
74
+ "metallicFactor": 0
75
+ }
76
+ },
77
+ {
78
+ "name": "warm glow stone",
79
+ "pbrMetallicRoughness": {
80
+ "baseColorFactor": [1, 0.82, 0.34, 1],
81
+ "roughnessFactor": 0.42,
82
+ "metallicFactor": 0
83
+ },
84
+ "emissiveFactor": [0.8, 0.55, 0.18]
85
+ },
86
+ {
87
+ "name": "distant teal skyline",
88
+ "pbrMetallicRoughness": {
89
+ "baseColorFactor": [0.12, 0.38, 0.42, 1],
90
+ "roughnessFactor": 0.9,
91
+ "metallicFactor": 0
92
+ }
93
+ }
94
+ ],
95
+ "accessors": [
96
+ {
97
+ "bufferView": 0,
98
+ "componentType": 5126,
99
+ "count": 4,
100
+ "type": "VEC3",
101
+ "min": [-1.6, 0, -0.6],
102
+ "max": [1.6, 0, 0.6]
103
+ },
104
+ {
105
+ "bufferView": 1,
106
+ "componentType": 5123,
107
+ "count": 6,
108
+ "type": "SCALAR"
109
+ },
110
+ {
111
+ "bufferView": 2,
112
+ "componentType": 5126,
113
+ "count": 4,
114
+ "type": "VEC3",
115
+ "min": [-0.14, 0, -0.14],
116
+ "max": [0.14, 0.2, 0.14]
117
+ },
118
+ {
119
+ "bufferView": 3,
120
+ "componentType": 5123,
121
+ "count": 6,
122
+ "type": "SCALAR"
123
+ },
124
+ {
125
+ "bufferView": 4,
126
+ "componentType": 5126,
127
+ "count": 4,
128
+ "type": "VEC3",
129
+ "min": [-1.4, 0, 0],
130
+ "max": [1.4, 0.5, 0]
131
+ },
132
+ {
133
+ "bufferView": 5,
134
+ "componentType": 5123,
135
+ "count": 6,
136
+ "type": "SCALAR"
137
+ }
138
+ ],
139
+ "bufferViews": [
140
+ {
141
+ "buffer": 0,
142
+ "byteOffset": 0,
143
+ "byteLength": 48,
144
+ "target": 34962
145
+ },
146
+ {
147
+ "buffer": 0,
148
+ "byteOffset": 48,
149
+ "byteLength": 12,
150
+ "target": 34963
151
+ },
152
+ {
153
+ "buffer": 0,
154
+ "byteOffset": 60,
155
+ "byteLength": 48,
156
+ "target": 34962
157
+ },
158
+ {
159
+ "buffer": 0,
160
+ "byteOffset": 108,
161
+ "byteLength": 12,
162
+ "target": 34963
163
+ },
164
+ {
165
+ "buffer": 0,
166
+ "byteOffset": 120,
167
+ "byteLength": 48,
168
+ "target": 34962
169
+ },
170
+ {
171
+ "buffer": 0,
172
+ "byteOffset": 168,
173
+ "byteLength": 12,
174
+ "target": 34963
175
+ }
176
+ ],
177
+ "buffers": [
178
+ {
179
+ "byteLength": 180,
180
+ "uri": "data:application/octet-stream;base64,zczMvZqZmb7MzMy+zczMPwAAAAAAAAAAAAAAAAAAAD8AAAAAAAAAAAAAAAAAAAAAzczMP5qZmb7MzMy+zcwMvQAAAAAFAAAAAQAAAAEAAAACAAAACtejvT0KF7U9Che1PQoXtT0KF7U9CtejvT0KFzU+AAAAAAAAPk0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAUABAAc2Mz+zsxMPs3MzD4AAAAAZmbmPwAAAAAAAAAAAAAAAAAAAAAAAAAAAM3MzD4AAAAAZmbmPwAAAAAAAAAABAUA"
181
+ }
182
+ ]
183
+ }
@@ -0,0 +1,276 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFileSync, spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import {
7
+ createCartoonEpisodePackage,
8
+ type CartoonEpisodePackageBuild,
9
+ type CartoonEpisodePackageMode
10
+ } from "../src/episode-renderer";
11
+
12
+ const command = process.argv[2] ?? "package";
13
+
14
+ async function main() {
15
+ if (command === "plan") {
16
+ await writePackage("plan", { only: new Set(["episode-plan.json", "metadata.json"]) });
17
+ return;
18
+ }
19
+ if (command === "preview") {
20
+ await writePackage("preview", { only: new Set(["route-proof.json", "metadata.json"]) });
21
+ return;
22
+ }
23
+ if (command === "render") {
24
+ await writePackage("render");
25
+ return;
26
+ }
27
+ if (command === "package") {
28
+ await writePackage("package");
29
+ return;
30
+ }
31
+ if (command === "review") {
32
+ await writePackage("review", { only: new Set(["review-package.md", "visual-acceptance.json", "render-manifest.json"]) });
33
+ return;
34
+ }
35
+ if (command === "verify") {
36
+ await verifyPackage(createCartoonEpisodePackage("package"));
37
+ return;
38
+ }
39
+ throw new Error(`Unknown episode command "${command}". Use plan, preview, render, package, review, or verify.`);
40
+ }
41
+
42
+ async function writePackage(mode: CartoonEpisodePackageMode, options: { only?: ReadonlySet<string> } = {}) {
43
+ const pkg = createCartoonEpisodePackage(mode);
44
+ await mkdir(pkg.packageDirectory, { recursive: true });
45
+ if (mode === "render" || mode === "package") {
46
+ await rm(path.join(pkg.packageDirectory, "frames"), { recursive: true, force: true });
47
+ await rm(path.join(pkg.packageDirectory, "episode.png-sequence-fallback.json"), { force: true });
48
+ }
49
+
50
+ for (const file of pkg.files) {
51
+ if (options.only && !options.only.has(file.path)) continue;
52
+ const target = path.join(pkg.packageDirectory, file.path);
53
+ await mkdir(path.dirname(target), { recursive: true });
54
+ if (file.kind === "png-base64") {
55
+ await writeFile(target, Buffer.from(file.contents, "base64"));
56
+ } else {
57
+ await writeFile(target, file.contents, "utf8");
58
+ }
59
+ }
60
+
61
+ if (!options.only && (mode === "render" || mode === "package")) await renderEpisodeMedia(pkg);
62
+ if (!options.only && mode === "package") await captureRouteThumbnail(pkg);
63
+ if (!options.only) await writeChecksumManifest(pkg);
64
+ console.log(JSON.stringify({
65
+ ok: true,
66
+ command: mode,
67
+ packageDirectory: pkg.packageDirectory,
68
+ hasWebm: pkg.hasWebm,
69
+ hasPngSequenceFallback: pkg.hasPngSequenceFallback,
70
+ publishReady: pkg.publishReady,
71
+ fileCount: options.only ? options.only.size : pkg.files.length
72
+ }, null, 2));
73
+ }
74
+
75
+ async function writeChecksumManifest(pkg: CartoonEpisodePackageBuild) {
76
+ const artifactEntries = [];
77
+ const packageArtifacts = [
78
+ ...pkg.files.map((file) => ({ path: file.path, kind: file.kind })),
79
+ { path: "episode.webm", kind: "video/webm" },
80
+ { path: "thumbnail.png", kind: "png" },
81
+ { path: "frames/first.png", kind: "png" },
82
+ { path: "frames/dialogue.png", kind: "png" },
83
+ { path: "frames/action.png", kind: "png" },
84
+ { path: "frames/final.png", kind: "png" }
85
+ ];
86
+ for (const file of packageArtifacts) {
87
+ const target = path.join(pkg.packageDirectory, file.path);
88
+ if (!existsSync(target)) continue;
89
+ const bytes = await readFile(target);
90
+ artifactEntries.push({
91
+ path: file.path,
92
+ kind: file.kind,
93
+ byteSize: bytes.length,
94
+ sha256: createHash("sha256").update(bytes).digest("hex")
95
+ });
96
+ }
97
+ const manifestPath = path.join(pkg.packageDirectory, "render-manifest.json");
98
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
99
+ manifest.artifacts = artifactEntries;
100
+ manifest.requiredFiles = pkg.requiredFiles;
101
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
102
+ }
103
+
104
+ async function renderEpisodeMedia(pkg: CartoonEpisodePackageBuild) {
105
+ const frameDirectory = path.join(pkg.packageDirectory, "frames");
106
+ const imageTool = executable("sips") ?? executable("magick") ?? executable("convert");
107
+ const ffmpeg = executable("ffmpeg");
108
+ if (!imageTool) throw new Error("Cannot render cartoon episode frames: ImageMagick `magick` or `convert` is required.");
109
+ if (!ffmpeg) throw new Error("Cannot encode cartoon episode: `ffmpeg` is required.");
110
+
111
+ const sequenceSvgFiles = pkg.files
112
+ .filter((file) => file.kind === "svg" && /^frames\/frame-\d+\.svg$/.test(file.path))
113
+ .map((file) => file.path)
114
+ .sort();
115
+
116
+ for (const svgPath of sequenceSvgFiles) {
117
+ const input = path.join(pkg.packageDirectory, svgPath);
118
+ const output = input.replace(/\.svg$/, ".png");
119
+ renderSvgToPng(imageTool, input, output);
120
+ }
121
+
122
+ const representativePairs = [
123
+ ["frames/first.svg", "frames/first.png"],
124
+ ["frames/dialogue.svg", "frames/dialogue.png"],
125
+ ["frames/action.svg", "frames/action.png"],
126
+ ["frames/final.svg", "frames/final.png"]
127
+ ] as const;
128
+ for (const [svgPath, pngPath] of representativePairs) {
129
+ renderSvgToPng(imageTool, path.join(pkg.packageDirectory, svgPath), path.join(pkg.packageDirectory, pngPath));
130
+ }
131
+
132
+ await copyFile(path.join(pkg.packageDirectory, "frames", "action.png"), path.join(pkg.packageDirectory, "thumbnail.png"));
133
+ encodeWebm(ffmpeg, frameDirectory, path.join(pkg.packageDirectory, "episode.webm"));
134
+ }
135
+
136
+ async function captureRouteThumbnail(pkg: CartoonEpisodePackageBuild) {
137
+ const vite = viteExecutable();
138
+ if (!vite) throw new Error("Cannot capture route thumbnail: Vite executable was not found.");
139
+ const port = 4387;
140
+ const origin = `http://127.0.0.1:${port}`;
141
+ const server = spawn(vite, ["--host", "127.0.0.1", "--port", String(port)], {
142
+ cwd: process.cwd(),
143
+ env: { ...process.env, BROWSER: "none" },
144
+ stdio: "pipe"
145
+ });
146
+ let startupLog = "";
147
+ server.stdout.on("data", (chunk) => {
148
+ startupLog += String(chunk);
149
+ });
150
+ server.stderr.on("data", (chunk) => {
151
+ startupLog += String(chunk);
152
+ });
153
+ try {
154
+ await waitForServer(origin, server, () => startupLog);
155
+ const { chromium } = await import("playwright");
156
+ const browser = await chromium.launch();
157
+ try {
158
+ const page = await browser.newPage({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 });
159
+ await page.goto(`${origin}/?capture=thumbnail`, { waitUntil: "networkidle" });
160
+ await page.waitForSelector("canvas", { timeout: 10_000 });
161
+ await page.waitForTimeout(500);
162
+ await page.locator("canvas").first().screenshot({
163
+ path: path.join(pkg.packageDirectory, "thumbnail.png")
164
+ });
165
+ } finally {
166
+ await browser.close();
167
+ }
168
+ } finally {
169
+ stopServer(server);
170
+ }
171
+ }
172
+
173
+ function renderSvgToPng(command: string, input: string, output: string) {
174
+ if (path.basename(command) === "sips") {
175
+ execFileSync(command, ["-s", "format", "png", input, "--out", output], { stdio: "pipe" });
176
+ return;
177
+ }
178
+ const args = path.basename(command) === "magick"
179
+ ? [input, "-background", "none", "-alpha", "remove", "-alpha", "off", output]
180
+ : [input, "-background", "none", "-alpha", "remove", "-alpha", "off", output];
181
+ execFileSync(command, args, { stdio: "pipe" });
182
+ }
183
+
184
+ function encodeWebm(ffmpeg: string, frameDirectory: string, output: string) {
185
+ execFileSync(ffmpeg, [
186
+ "-y",
187
+ "-framerate",
188
+ "30",
189
+ "-stream_loop",
190
+ "59",
191
+ "-i",
192
+ path.join(frameDirectory, "frame-%04d.png"),
193
+ "-t",
194
+ "60",
195
+ "-c:v",
196
+ "libvpx-vp9",
197
+ "-pix_fmt",
198
+ "yuv420p",
199
+ "-b:v",
200
+ "0",
201
+ "-crf",
202
+ "34",
203
+ output
204
+ ], { stdio: "pipe" });
205
+ }
206
+
207
+ function executable(command: string): string | undefined {
208
+ try {
209
+ return execFileSync("which", [command], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
210
+ } catch {
211
+ return undefined;
212
+ }
213
+ }
214
+
215
+ function viteExecutable(): string | undefined {
216
+ const local = path.join(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "vite.cmd" : "vite");
217
+ return existsSync(local) ? local : executable("vite");
218
+ }
219
+
220
+ async function waitForServer(origin: string, server: ChildProcessWithoutNullStreams, log: () => string) {
221
+ const startedAt = Date.now();
222
+ while (Date.now() - startedAt < 30_000) {
223
+ if (server.exitCode !== null) {
224
+ throw new Error(`Vite exited before route thumbnail capture. Log:\n${log()}`);
225
+ }
226
+ try {
227
+ const response = await fetch(origin);
228
+ if (response.ok) return;
229
+ } catch {
230
+ // Keep polling until Vite is ready.
231
+ }
232
+ await new Promise((resolve) => setTimeout(resolve, 250));
233
+ }
234
+ throw new Error(`Timed out waiting for Vite route thumbnail capture. Log:\n${log()}`);
235
+ }
236
+
237
+ function stopServer(server: ChildProcessWithoutNullStreams) {
238
+ if (server.exitCode === null) server.kill("SIGTERM");
239
+ }
240
+
241
+ async function verifyPackage(pkg: CartoonEpisodePackageBuild) {
242
+ const missing = [];
243
+ for (const requiredFile of pkg.requiredFiles) {
244
+ const target = path.join(pkg.packageDirectory, requiredFile);
245
+ if (!existsSync(target)) missing.push(requiredFile);
246
+ }
247
+
248
+ const hasWebm = existsSync(path.join(pkg.packageDirectory, "episode.webm"));
249
+ const hasFallback = existsSync(path.join(pkg.packageDirectory, "episode.png-sequence-fallback.json"));
250
+ if (!hasWebm && !hasFallback) missing.push("episode.webm or episode.png-sequence-fallback.json");
251
+
252
+ const byteSizes = [];
253
+ for (const requiredFile of pkg.requiredFiles) {
254
+ const target = path.join(pkg.packageDirectory, requiredFile);
255
+ if (!existsSync(target)) continue;
256
+ const info = await stat(target);
257
+ byteSizes.push({ path: requiredFile, byteSize: info.size });
258
+ if (info.size <= 0) missing.push(`${requiredFile} is empty`);
259
+ }
260
+
261
+ const ok = missing.length === 0;
262
+ console.log(JSON.stringify({
263
+ ok,
264
+ packageDirectory: pkg.packageDirectory,
265
+ hasWebm,
266
+ hasPngSequenceFallback: hasFallback,
267
+ checkedFiles: byteSizes,
268
+ missing
269
+ }, null, 2));
270
+ if (!ok) process.exitCode = 1;
271
+ }
272
+
273
+ main().catch((error) => {
274
+ console.error(error instanceof Error ? error.message : error);
275
+ process.exitCode = 1;
276
+ });
@@ -3,12 +3,59 @@ import { AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID } from "./contract";
3
3
 
4
4
  export const assetContractId = AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID;
5
5
 
6
+ export const cartoonStudioRequiredAssetKeys = ["miko", "luma", "moonGarden"] as const;
7
+
8
+ export const cartoonStudioOptionalAudioAssetKeys = [
9
+ "mikoDialogueStem",
10
+ "lumaDialogueStem",
11
+ "moonGardenMusic",
12
+ "moonGardenChimeSfx"
13
+ ] as const;
14
+
6
15
  export const assets = defineAuraAssets({
7
- /*
8
- * Add character GLBs with the Aura3D CLI, then keep these generated keys:
9
- *
10
- * npx @aura3d/cli@latest assets add ./assets/miko.glb --name miko
11
- * npx @aura3d/cli@latest assets add ./assets/luma.glb --name luma
12
- * npx @aura3d/cli@latest assets validate-cartoon
13
- */
14
- });
16
+ miko: {
17
+ type: "model",
18
+ format: "glb",
19
+ url: "/aura-assets/miko.047f5e5f.glb",
20
+ bounds: [0.066, 0.026, 0.017],
21
+ hash: "sha256-047f5e5fb3bb6d378bd1df16ca6137f2a596c99b3a1b5690b4020c05aaf6f319",
22
+ metadata: {
23
+ contractId: assetContractId,
24
+ role: "cartoon-character",
25
+ license: "CC0",
26
+ source: "Aura3D bundled humanoid starter fixture",
27
+ animationClips: ["Idle", "Walking", "Wave", "Punch", "Jump"],
28
+ mouthReadiness: "primitive-mouth-card"
29
+ }
30
+ },
31
+ luma: {
32
+ type: "model",
33
+ format: "glb",
34
+ url: "/aura-assets/luma.humanoid-fixture.glb",
35
+ bounds: [1.1, 1.8, 0.8],
36
+ hash: "sha256-dfb230fc1f942f259dd00281a1186953ad602fc5d69067ce63e24b2aa439736b",
37
+ metadata: {
38
+ contractId: assetContractId,
39
+ role: "cartoon-character",
40
+ license: "CC0",
41
+ source: "Aura3D bundled humanoid starter fixture",
42
+ animationClips: ["Idle", "Walking", "Wave", "Punch", "Jump"],
43
+ mouthReadiness: "primitive-mouth-card"
44
+ }
45
+ },
46
+ moonGarden: {
47
+ type: "model",
48
+ format: "gltf",
49
+ url: "/aura-assets/moonGarden.gltf",
50
+ bounds: [3.2, 0.6, 1.2],
51
+ hash: "sha256-85a288d6d434cc5c56ac154c3e996c2ad7612791622fc20a339fb647a6d426bd",
52
+ metadata: {
53
+ contractId: assetContractId,
54
+ role: "cartoon-set",
55
+ license: "MIT",
56
+ source: "Aura3D cartoon-studio local generated starter set anchor",
57
+ walkable: true,
58
+ framingReady: true
59
+ }
60
+ }
61
+ } as const);
@@ -11,6 +11,7 @@ export const characters = [
11
11
  typedAssetKey: "miko",
12
12
  runtimeNodeId: "miko",
13
13
  primitiveMouthNodeId: "miko:mouth",
14
+ clipRequirements: ["Idle", "Walking", "Wave", "Talk"],
14
15
  performanceChannels: ["body", "facial", "gesture", "blocking", "gaze"],
15
16
  glbUpgrade: "After assets add, use model(assets.miko) and keep viseme blendshape metadata."
16
17
  },
@@ -22,7 +23,43 @@ export const characters = [
22
23
  typedAssetKey: "luma",
23
24
  runtimeNodeId: "luma",
24
25
  primitiveMouthNodeId: "luma:mouth",
26
+ clipRequirements: ["Idle", "Walking", "Wave", "Talk"],
25
27
  performanceChannels: ["body", "facial", "gesture", "blocking", "gaze"],
26
28
  glbUpgrade: "After assets add, use model(assets.luma) and keep viseme blendshape metadata."
27
29
  }
28
- ];
30
+ ] as const;
31
+
32
+ export interface CartoonStudioCharacterReadiness {
33
+ readonly id: string;
34
+ readonly ok: boolean;
35
+ readonly missingClips: readonly string[];
36
+ readonly mouthReady: boolean;
37
+ readonly gestureReady: boolean;
38
+ readonly diagnostics: readonly string[];
39
+ }
40
+
41
+ export function validateCartoonStudioCharacters(
42
+ entries: readonly typeof characters[number][] = characters,
43
+ options: { readonly requiredClips?: readonly string[] } = {}
44
+ ): readonly CartoonStudioCharacterReadiness[] {
45
+ const requiredClips = options.requiredClips ?? ["Idle", "Walking", "Wave"];
46
+ return entries.map((character) => {
47
+ const declaredClips = new Set(character.clipRequirements.map((clip) => clip.toLowerCase()));
48
+ const missingClips = requiredClips.filter((clip) => !declaredClips.has(clip.toLowerCase()));
49
+ const mouthReady = Boolean(character.primitiveMouthNodeId);
50
+ const gestureReady = character.performanceChannels.includes("gesture");
51
+ const diagnostics = [
52
+ ...missingClips.map((clip) => `${character.id} missing required clip metadata: ${clip}`),
53
+ ...(mouthReady ? [] : [`${character.id} missing primitive mouth or blendshape metadata`]),
54
+ ...(gestureReady ? [] : [`${character.id} missing gesture performance channel`])
55
+ ];
56
+ return {
57
+ id: character.id,
58
+ ok: diagnostics.length === 0,
59
+ missingClips,
60
+ mouthReady,
61
+ gestureReady,
62
+ diagnostics
63
+ };
64
+ });
65
+ }