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.
- package/dist/cli.js +0 -0
- package/package.json +2 -2
- 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 +36 -0
- package/templates/cartoon-studio/aura.assets.json +157 -0
- package/templates/cartoon-studio/package-lock.json +2251 -0
- package/templates/cartoon-studio/package.json +13 -2
- 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 +55 -8
- package/templates/cartoon-studio/src/characters.ts +38 -1
- package/templates/cartoon-studio/src/episode-renderer.ts +605 -0
- package/templates/cartoon-studio/src/episode.ts +12 -6
- package/templates/cartoon-studio/src/main.ts +109 -41
- package/templates/cartoon-studio/src/render-plan.ts +162 -0
- package/templates/cartoon-studio/src/review.ts +99 -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/storyboard-playback.spec.ts +18 -10
- package/templates/cartoon-studio/tests/visual-review.spec.ts +21 -0
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/episode-builder/package.json +1 -1
- package/templates/fighting-game/package.json +1 -1
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/product-viewer/tests/screenshot.spec.ts +8 -16
- 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/
|
|
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
|
}
|
|
Binary file
|
|
@@ -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>
|
|
Binary file
|
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|