create-aura3d 1.0.9 → 1.1.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.
- package/dist/cli.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- package/templates/cartoon-channel/README.md +38 -0
- package/templates/cartoon-channel/aura.assets.json +1364 -0
- package/templates/cartoon-channel/package.json +3 -2
- package/templates/cartoon-channel/public/aura-assets/luma.047f5e5f.glb +0 -0
- package/templates/cartoon-channel/public/aura-assets/luma.thumb.svg +1 -0
- package/templates/cartoon-channel/public/aura-assets/miko.047f5e5f.glb +0 -0
- package/templates/cartoon-channel/public/aura-assets/miko.thumb.svg +1 -0
- package/templates/cartoon-channel/public/aura-assets/moon-garden-feature-frame.png +0 -0
- package/templates/cartoon-channel/src/aura-assets.ts +5 -11
- package/templates/cartoon-channel/src/concept-episode-2-5d.css +159 -0
- package/templates/cartoon-channel/src/concept-episode-2-5d.ts +155 -0
- package/templates/cartoon-channel/src/image-puppet-episode.css +290 -0
- package/templates/cartoon-channel/src/image-puppet-episode.ts +139 -0
- package/templates/cartoon-channel/src/main.ts +89 -71
- package/templates/cartoon-channel/src/puppet-episode-2d.css +526 -0
- package/templates/cartoon-channel/src/puppet-episode-2d.ts +189 -0
- package/templates/cartoon-channel/src/render-plan.ts +1 -1
- package/templates/cartoon-channel/src/sample-episode-visual.css +457 -0
- package/templates/cartoon-channel/src/sample-episode-visual.ts +185 -0
- package/templates/cartoon-channel/test-results/.last-run.json +4 -0
- package/templates/cartoon-channel/tests/concept-2-5d-animation.spec.ts +18 -0
- package/templates/cartoon-channel/tests/concept-2-5d.spec.ts +30 -0
- package/templates/cartoon-channel/tests/image-puppet-animation.spec.ts +18 -0
- package/templates/cartoon-channel/tests/image-puppet.spec.ts +29 -0
- package/templates/cartoon-channel/tests/puppet-2d-animation.spec.ts +18 -0
- package/templates/cartoon-channel/tests/puppet-2d.spec.ts +26 -0
- package/templates/cartoon-channel/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
- package/templates/cartoon-channel/tests/sample-episode-visual.spec.ts +45 -0
- package/templates/cartoon-channel/tests/storyboard-playback.spec.ts +1 -1
- package/templates/cartoon-studio/README.md +82 -0
- package/templates/cartoon-studio/aura.assets.json +157 -0
- package/templates/cartoon-studio/index.html +13 -0
- package/templates/cartoon-studio/package.json +32 -0
- package/templates/cartoon-studio/playwright.config.ts +13 -0
- package/templates/cartoon-studio/public/aura-assets/luma.047f5e5f.glb +0 -0
- package/templates/cartoon-studio/public/aura-assets/luma.humanoid-fixture.glb +0 -0
- package/templates/cartoon-studio/public/aura-assets/luma.thumb.svg +1 -0
- package/templates/cartoon-studio/public/aura-assets/miko.047f5e5f.glb +0 -0
- package/templates/cartoon-studio/public/aura-assets/miko.thumb.svg +1 -0
- package/templates/cartoon-studio/public/aura-assets/moonGarden.gltf +183 -0
- package/templates/cartoon-studio/scripts/episode.ts +276 -0
- package/templates/cartoon-studio/src/aura-assets.ts +61 -0
- package/templates/cartoon-studio/src/characters.ts +65 -0
- package/templates/cartoon-studio/src/contract.ts +1 -0
- package/templates/cartoon-studio/src/episode-renderer.ts +605 -0
- package/templates/cartoon-studio/src/episode.ts +306 -0
- package/templates/cartoon-studio/src/main.ts +333 -0
- package/templates/cartoon-studio/src/render-plan.ts +567 -0
- package/templates/cartoon-studio/src/review.ts +99 -0
- package/templates/cartoon-studio/src/sets.ts +14 -0
- package/templates/cartoon-studio/src/studio.ts +101 -0
- package/templates/cartoon-studio/test-results/.last-run.json +4 -0
- package/templates/cartoon-studio/tests/episode-render.spec.ts +44 -0
- package/templates/cartoon-studio/tests/motion-quality.spec.ts +45 -0
- package/templates/cartoon-studio/tests/route-health.spec.ts +7 -0
- package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +99 -0
- package/templates/cartoon-studio/tests/visual-review.spec.ts +21 -0
- package/templates/cartoon-studio/tsconfig.json +12 -0
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/episode-builder/README.md +46 -0
- package/templates/episode-builder/index.html +13 -0
- package/templates/episode-builder/package.json +21 -0
- package/templates/episode-builder/playwright.config.ts +13 -0
- package/templates/episode-builder/src/aura-assets.ts +14 -0
- package/templates/episode-builder/src/builder.ts +78 -0
- package/templates/episode-builder/src/characters.ts +28 -0
- package/templates/episode-builder/src/contract.ts +1 -0
- package/templates/episode-builder/src/episode.ts +300 -0
- package/templates/episode-builder/src/main.ts +265 -0
- package/templates/episode-builder/src/render-plan.ts +405 -0
- package/templates/episode-builder/src/sets.ts +14 -0
- package/templates/episode-builder/tests/route-health.spec.ts +7 -0
- package/templates/episode-builder/tests/storyboard-playback.spec.ts +98 -0
- package/templates/episode-builder/tsconfig.json +12 -0
- package/templates/fighting-game/package.json +1 -1
- package/templates/fighting-game/src/game/stage.ts +18 -4
- package/templates/fighting-game/src/main.ts +3 -2
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/product-viewer/tests/screenshot.spec.ts +8 -16
- package/templates/prompt-cartoon-channel/package.json +1 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineAuraAssets } from "@aura3d/engine";
|
|
2
|
+
import { AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID } from "./contract";
|
|
3
|
+
|
|
4
|
+
export const assetContractId = AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID;
|
|
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
|
+
|
|
15
|
+
export const assets = defineAuraAssets({
|
|
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);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID } from "./contract";
|
|
2
|
+
|
|
3
|
+
export const characterContractId = AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID;
|
|
4
|
+
|
|
5
|
+
export const characters = [
|
|
6
|
+
{
|
|
7
|
+
id: "miko",
|
|
8
|
+
name: "Miko",
|
|
9
|
+
palette: ["#7de2ff", "#f7ffe8"],
|
|
10
|
+
primitiveFallback: "round robot with antenna",
|
|
11
|
+
typedAssetKey: "miko",
|
|
12
|
+
runtimeNodeId: "miko",
|
|
13
|
+
primitiveMouthNodeId: "miko:mouth",
|
|
14
|
+
clipRequirements: ["Idle", "Walking", "Wave", "Talk"],
|
|
15
|
+
performanceChannels: ["body", "facial", "gesture", "blocking", "gaze"],
|
|
16
|
+
glbUpgrade: "After assets add, use model(assets.miko) and keep viseme blendshape metadata."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "luma",
|
|
20
|
+
name: "Luma",
|
|
21
|
+
palette: ["#ffe18e", "#40ffbf"],
|
|
22
|
+
primitiveFallback: "small moon-garden helper",
|
|
23
|
+
typedAssetKey: "luma",
|
|
24
|
+
runtimeNodeId: "luma",
|
|
25
|
+
primitiveMouthNodeId: "luma:mouth",
|
|
26
|
+
clipRequirements: ["Idle", "Walking", "Wave", "Talk"],
|
|
27
|
+
performanceChannels: ["body", "facial", "gesture", "blocking", "gaze"],
|
|
28
|
+
glbUpgrade: "After assets add, use model(assets.luma) and keep viseme blendshape metadata."
|
|
29
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID = "auravoice-aura3d-prompt-animation/v1" as const;
|