create-aura3d 1.0.8 → 1.0.10

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 (44) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +4 -2
  5. package/dist/index.js.map +1 -1
  6. package/package.json +8 -4
  7. package/templates/cartoon-channel/package.json +1 -1
  8. package/templates/cartoon-studio/README.md +46 -0
  9. package/templates/cartoon-studio/index.html +13 -0
  10. package/templates/cartoon-studio/package.json +21 -0
  11. package/templates/cartoon-studio/playwright.config.ts +13 -0
  12. package/templates/cartoon-studio/src/aura-assets.ts +14 -0
  13. package/templates/cartoon-studio/src/characters.ts +28 -0
  14. package/templates/cartoon-studio/src/contract.ts +1 -0
  15. package/templates/cartoon-studio/src/episode.ts +300 -0
  16. package/templates/cartoon-studio/src/main.ts +265 -0
  17. package/templates/cartoon-studio/src/render-plan.ts +405 -0
  18. package/templates/cartoon-studio/src/sets.ts +14 -0
  19. package/templates/cartoon-studio/src/studio.ts +101 -0
  20. package/templates/cartoon-studio/tests/route-health.spec.ts +7 -0
  21. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +91 -0
  22. package/templates/cartoon-studio/tsconfig.json +12 -0
  23. package/templates/cinematic-scene/package.json +1 -1
  24. package/templates/episode-builder/README.md +46 -0
  25. package/templates/episode-builder/index.html +13 -0
  26. package/templates/episode-builder/package.json +21 -0
  27. package/templates/episode-builder/playwright.config.ts +13 -0
  28. package/templates/episode-builder/src/aura-assets.ts +14 -0
  29. package/templates/episode-builder/src/builder.ts +78 -0
  30. package/templates/episode-builder/src/characters.ts +28 -0
  31. package/templates/episode-builder/src/contract.ts +1 -0
  32. package/templates/episode-builder/src/episode.ts +300 -0
  33. package/templates/episode-builder/src/main.ts +265 -0
  34. package/templates/episode-builder/src/render-plan.ts +405 -0
  35. package/templates/episode-builder/src/sets.ts +14 -0
  36. package/templates/episode-builder/tests/route-health.spec.ts +7 -0
  37. package/templates/episode-builder/tests/storyboard-playback.spec.ts +98 -0
  38. package/templates/episode-builder/tsconfig.json +12 -0
  39. package/templates/fighting-game/package.json +1 -1
  40. package/templates/fighting-game/src/game/stage.ts +18 -4
  41. package/templates/fighting-game/src/main.ts +3 -2
  42. package/templates/mini-game/package.json +1 -1
  43. package/templates/product-viewer/package.json +1 -1
  44. package/templates/prompt-cartoon-channel/package.json +1 -1
@@ -0,0 +1,405 @@
1
+ import {
2
+ collectPromptAnimationEvidence,
3
+ createAudioStemManifest,
4
+ createAuraVoiceBridgePackage,
5
+ createAuraVoiceVisemeTrack,
6
+ createCaptionTimingProof,
7
+ createGlbBlendshapeVisemeCue,
8
+ createCartoonPerformanceCoverage,
9
+ createCartoonRenderOutputPackageMetadata,
10
+ createPrimitiveMouthVisemeCues,
11
+ createPromptAnimationDeterministicScreenshotFixtureMetadata,
12
+ evaluatePromptAnimationPublishReadiness,
13
+ defineDubMap,
14
+ glbVisemeBlendshapeExample,
15
+ primitiveMouthVisemeExample,
16
+ sampleAuraVoiceBridgeAtTime,
17
+ sampleVisemeTrack,
18
+ validateAuraVoiceBridgePackage
19
+ } from "@aura3d/engine";
20
+ import {
21
+ episode,
22
+ episodeAudioCues,
23
+ episodeContractId,
24
+ missingCartoonCharacterAssets,
25
+ publicCartoonAssetInstructions,
26
+ requiredCartoonCharacterAssets,
27
+ typedCartoonAssetSummary,
28
+ youtubeDraftMetadata
29
+ } from "./episode";
30
+
31
+ const frameRate = episode.episodePlan.runtime.frameRate;
32
+
33
+ export const renderContractId = episodeContractId;
34
+
35
+ export const renderPlan = episode.renderQueue;
36
+
37
+ export const renderOutputPackage = createCartoonRenderOutputPackageMetadata({
38
+ episodePlan: episode.episodePlan,
39
+ shotTimeline: episode.shotTimeline,
40
+ renderQueue: renderPlan,
41
+ youtube: youtubeDraftMetadata,
42
+ generatedAt: episode.episodePlan.generatedAt
43
+ });
44
+
45
+ export const audioStemManifest = createAudioStemManifest({
46
+ episodeId: episode.episodePlan.episodeId,
47
+ duration: episode.dialogueTrack.duration,
48
+ stems: [
49
+ ...episode.dialogueTrack.lines.map((line) => ({
50
+ id: `audio:${line.lineId}`,
51
+ role: "dialogue" as const,
52
+ path: line.audioFile ?? `assets/audio/en/${line.lineId}.wav`,
53
+ startTime: line.startTime,
54
+ duration: line.endTime - line.startTime,
55
+ language: line.language
56
+ })),
57
+ ...episodeAudioCues
58
+ ],
59
+ generatedAt: episode.episodePlan.generatedAt
60
+ });
61
+
62
+ export const visemeTrack = createAuraVoiceVisemeTrack({
63
+ episodeId: episode.episodePlan.episodeId,
64
+ language: episode.episodePlan.language,
65
+ frameRate,
66
+ cues: episode.dialogueTrack.lines.flatMap((line) =>
67
+ createPrimitiveMouthVisemeCues({
68
+ characterId: line.speakerId,
69
+ speakerId: line.speakerId,
70
+ lineId: line.lineId,
71
+ startTime: line.startTime,
72
+ endTime: line.endTime
73
+ }).map((cue, cueIndex) => {
74
+ const words = line.text.split(/\s+/).filter(Boolean);
75
+ const wordIndex = Math.min(cueIndex, Math.max(0, words.length - 1));
76
+ return createGlbBlendshapeVisemeCue({
77
+ ...cue,
78
+ phoneme: cue.visemeId === "m" ? "M" : "AH",
79
+ phonemeId: `${line.lineId}:phoneme-${cueIndex + 1}`,
80
+ word: words[wordIndex] ?? line.text,
81
+ wordIndex,
82
+ wordStartTime: cue.startTime,
83
+ wordEndTime: cue.endTime
84
+ });
85
+ })
86
+ ),
87
+ generatedAt: episode.episodePlan.generatedAt
88
+ });
89
+
90
+ export const primitiveMouthRuntimeExample = {
91
+ ...primitiveMouthVisemeExample,
92
+ runtimeNodes: {
93
+ miko: "miko:mouth",
94
+ luma: "luma:mouth"
95
+ }
96
+ } as const;
97
+
98
+ export const glbVisemeRuntimeExample = {
99
+ ...glbVisemeBlendshapeExample,
100
+ typedAssetExample: "import { assets } from './aura-assets'; model(assets.character)",
101
+ sampleBlendshapeNames: Object.values(glbVisemeBlendshapeExample.blendshapeMap).slice(0, 6)
102
+ } as const;
103
+
104
+ export const captionTimingProof = createCaptionTimingProof(episode.dialogueTrack, episode.captionTrack, {
105
+ frameRate,
106
+ maxAllowedDriftFrames: 1
107
+ });
108
+
109
+ function frameDriftFrames(actual: number, expected: number) {
110
+ return Math.round(Math.abs(actual - expected) * frameRate);
111
+ }
112
+
113
+ const visemeLineSyncChecks = episode.dialogueTrack.lines.map((line) => {
114
+ const cues = visemeTrack.cues.filter((cue) => cue.lineId === line.lineId && cue.speakerId === line.speakerId);
115
+ const firstCue = cues[0];
116
+ const lastCue = cues[cues.length - 1];
117
+ const startDriftFrames = firstCue ? frameDriftFrames(firstCue.startTime, line.startTime) : 999;
118
+ const endDriftFrames = lastCue ? frameDriftFrames(lastCue.endTime, line.endTime) : 999;
119
+ const sampleTime = Math.min(line.endTime - 1 / frameRate, line.startTime + 1 / frameRate);
120
+ const sampledMouth = sampleVisemeTrack(visemeTrack, sampleTime, line.speakerId);
121
+ return {
122
+ lineId: line.lineId,
123
+ speakerId: line.speakerId,
124
+ characterId: line.speakerId,
125
+ cueCount: cues.length,
126
+ startDriftFrames,
127
+ endDriftFrames,
128
+ maxDriftFrames: Math.max(startDriftFrames, endDriftFrames),
129
+ sampledMouthState: {
130
+ time: sampleTime,
131
+ frame: Math.round(sampleTime * frameRate),
132
+ visemeId: sampledMouth.visemeId,
133
+ mouthOpenness: sampledMouth.mouthOpenness,
134
+ primitiveMouthCard: sampledMouth.primitiveMouthCard,
135
+ blendshapeWeights: sampledMouth.blendshapeWeights
136
+ }
137
+ };
138
+ });
139
+
140
+ const maxVisemeLineDriftFrames = Math.max(0, ...visemeLineSyncChecks.map((check) => check.maxDriftFrames));
141
+ const missingVisemeLineIds = visemeLineSyncChecks
142
+ .filter((check) => check.cueCount === 0 || check.maxDriftFrames > 1)
143
+ .map((check) => check.lineId);
144
+
145
+ export const captionFrameSyncSourceProof = {
146
+ contractId: renderContractId,
147
+ sourceOnly: true,
148
+ proofType: "caption-display-one-frame-source-harness",
149
+ frameRate,
150
+ allowedDriftFrames: 1,
151
+ allowedFrameDurationSeconds: 1 / frameRate,
152
+ captionDisplayWithinOneFrame: captionTimingProof.status === "pass" && captionTimingProof.maxDriftFrames <= 1,
153
+ maxObservedDriftFrames: captionTimingProof.maxDriftFrames,
154
+ coveredLineIds: captionTimingProof.coveredLineIds,
155
+ missingLineIds: captionTimingProof.missingLineIds,
156
+ sampledCaptionCues: captionTimingProof.lines.map((line) => ({
157
+ lineId: line.lineId,
158
+ captionCueId: line.captionId,
159
+ startDriftFrames: line.startDriftFrames,
160
+ endDriftFrames: line.endDriftFrames,
161
+ maxDriftFrames: line.maxDriftFrames
162
+ })),
163
+ executionBoundary: "Source harness only; actual caption display proof requires rendered frame/video evidence."
164
+ } as const;
165
+
166
+ export const visemeFrameSyncSourceProof = {
167
+ contractId: renderContractId,
168
+ sourceOnly: true,
169
+ proofType: "mouth-movement-one-frame-source-harness",
170
+ frameRate,
171
+ allowedDriftFrames: 1,
172
+ allowedFrameDurationSeconds: 1 / frameRate,
173
+ mouthMovementWithinOneFrame: missingVisemeLineIds.length === 0 && maxVisemeLineDriftFrames <= 1,
174
+ maxObservedDriftFrames: maxVisemeLineDriftFrames,
175
+ coveredLineIds: visemeLineSyncChecks.filter((check) => check.cueCount > 0).map((check) => check.lineId),
176
+ missingLineIds: missingVisemeLineIds,
177
+ sampledMouthStates: visemeLineSyncChecks.map((check) => ({
178
+ lineId: check.lineId,
179
+ speakerId: check.speakerId,
180
+ characterId: check.characterId,
181
+ ...check.sampledMouthState
182
+ })),
183
+ primitiveMouthRuntimeNodeIds: primitiveMouthRuntimeExample.runtimeNodes,
184
+ glbBlendshapeNames: glbVisemeRuntimeExample.sampleBlendshapeNames,
185
+ executionBoundary: "Source harness only; actual mouth movement proof requires rendered frame/video evidence."
186
+ } as const;
187
+
188
+ function shotForLine(lineId: string) {
189
+ return episode.shotTimeline.shots.find((shot) => lineId.startsWith(`${shot.shotId}:line-`));
190
+ }
191
+
192
+ export const spanishDubMap = defineDubMap({
193
+ artifact: "dub-map",
194
+ contractId: episode.dialogueTrack.contractId,
195
+ episodeId: episode.episodePlan.episodeId,
196
+ sourceLanguage: "en",
197
+ targetLanguage: "es",
198
+ entries: episode.dialogueTrack.lines.map((line) => {
199
+ const shot = shotForLine(line.lineId);
200
+ return {
201
+ originalStoryboardId: shot?.sceneId,
202
+ dubbedStoryboardId: shot?.sceneId,
203
+ originalStoryboardSceneId: shot?.sceneId,
204
+ dubbedStoryboardSceneId: shot?.sceneId,
205
+ originalLineId: line.lineId,
206
+ dubbedLineId: line.lineId,
207
+ originalCaptionId: `${line.lineId}:caption`,
208
+ dubbedCaptionId: `${line.lineId}:caption`,
209
+ originalCharacterId: line.speakerId,
210
+ dubbedCharacterId: line.speakerId,
211
+ originalSpeakerId: line.speakerId,
212
+ dubbedSpeakerId: line.speakerId,
213
+ originalLanguage: line.language,
214
+ dubbedLanguage: "es",
215
+ originalShotId: shot?.shotId,
216
+ dubbedShotId: shot?.shotId,
217
+ originalSceneId: shot?.sceneId,
218
+ dubbedSceneId: shot?.sceneId
219
+ };
220
+ }),
221
+ generatedAt: episode.episodePlan.generatedAt
222
+ });
223
+
224
+ export const phonemeVisemeDubSyncSourceProof = {
225
+ contractId: renderContractId,
226
+ sourceOnly: true,
227
+ proofType: "phoneme-viseme-dub-sync-source-harness",
228
+ visemeFormat: visemeTrack.format,
229
+ frameRate,
230
+ allowedDriftFrames: 1,
231
+ phonemeFields: ["phoneme", "phonemeId", "word", "wordIndex", "wordStartTime", "wordEndTime"],
232
+ sampledPhonemeVisemeCues: visemeTrack.cues.slice(0, 8).map((cue) => ({
233
+ cueId: cue.id,
234
+ lineId: cue.lineId,
235
+ speakerId: cue.speakerId,
236
+ characterId: cue.characterId,
237
+ visemeId: cue.visemeId,
238
+ phoneme: cue.phoneme,
239
+ phonemeId: cue.phonemeId,
240
+ word: cue.word,
241
+ wordIndex: cue.wordIndex,
242
+ wordStartTime: cue.wordStartTime,
243
+ wordEndTime: cue.wordEndTime,
244
+ startTime: cue.startTime,
245
+ endTime: cue.endTime
246
+ })),
247
+ dubContinuity: {
248
+ sourceLanguage: spanishDubMap.sourceLanguage,
249
+ targetLanguage: spanishDubMap.targetLanguage,
250
+ stableShotIds: spanishDubMap.entries.every((entry) => entry.originalShotId === entry.dubbedShotId),
251
+ stableStoryboardIds: spanishDubMap.entries.every((entry) => entry.originalStoryboardId === entry.dubbedStoryboardId),
252
+ stableCaptionIds: spanishDubMap.entries.every((entry) => entry.originalCaptionId === entry.dubbedCaptionId),
253
+ stableLineIds: spanishDubMap.entries.every((entry) => entry.originalLineId === entry.dubbedLineId)
254
+ },
255
+ executionBoundary: "Source harness only; actual phoneme, viseme, and dub playback proof requires rendered frame/video evidence."
256
+ } as const;
257
+
258
+ export const performanceCoverage = createCartoonPerformanceCoverage(episode.performance, episode.dialogueTrack);
259
+
260
+ export const reviewPackagePaths = renderOutputPackage.reviewPackagePaths;
261
+
262
+ export const thumbnailCapture = renderOutputPackage.thumbnailCapture;
263
+
264
+ export const plannedDeterministicCaptureSources = renderPlan.items.map((item) => ({
265
+ renderQueueItemId: item.id,
266
+ time: item.time,
267
+ frame: item.frame,
268
+ ...(item.shotId ? { shotId: item.shotId } : {}),
269
+ sourceSceneStateId: item.sourceSceneState?.sceneStateId,
270
+ auraVoiceTimestamp: item.sourceSceneState?.auraVoiceTimestamp ?? item.time,
271
+ deterministicSeed: item.sourceSceneState?.deterministicSeed
272
+ }));
273
+
274
+ export const sampleRenderSourceWorkflow = {
275
+ contractId: renderContractId,
276
+ sourceOnly: true,
277
+ status: "source-workflow-ready",
278
+ promptToAudioToAura3DAnimation: true,
279
+ sampleEpisodeId: episode.episodePlan.episodeId,
280
+ runtimeSeconds: episode.episodePlan.runtime.duration,
281
+ minimumRuntimeSeconds: 60,
282
+ frameRate,
283
+ resolution: episode.episodePlan.runtime.resolution,
284
+ shotCount: episode.shotTimeline.shots.length,
285
+ speakingCharacterIds: Array.from(new Set(episode.dialogueTrack.lines.map((line) => line.speakerId))),
286
+ requiredTypedAssets: requiredCartoonCharacterAssets,
287
+ typedAssetSummary: typedCartoonAssetSummary,
288
+ missingTypedCharacterAssets: missingCartoonCharacterAssets,
289
+ assetCommands: publicCartoonAssetInstructions,
290
+ audioStemRoles: audioStemManifest.stems.map((stem) => stem.role),
291
+ requiresNoManualTimingEdits: true,
292
+ doesNotClaimRenderedArtifacts: true,
293
+ humanReviewRequired: true,
294
+ captionFrameSyncSourceProof,
295
+ visemeFrameSyncSourceProof,
296
+ phonemeVisemeDubSyncSourceProof,
297
+ artifactManifestExpectations: {
298
+ video: {
299
+ required: true,
300
+ outputId: renderOutputPackage.outputs.mp4?.id,
301
+ path: renderOutputPackage.outputs.mp4?.path,
302
+ evidenceJsonFields: ["path", "sha256", "byteSize", "duration", "frameRate", "resolution", "renderQueueId"]
303
+ },
304
+ thumbnail: {
305
+ required: true,
306
+ outputId: renderOutputPackage.outputs.thumbnail?.id,
307
+ path: renderOutputPackage.outputs.thumbnail?.path,
308
+ sourceSceneStateId: renderOutputPackage.thumbnailCapture.sourceSceneStateId,
309
+ evidenceJsonFields: ["path", "sha256", "byteSize", "width", "height", "captureTime", "shotId", "sourceSceneStateId"]
310
+ },
311
+ captions: {
312
+ required: true,
313
+ outputIds: renderOutputPackage.outputs.captions.map((output) => output.id),
314
+ paths: renderOutputPackage.outputs.captions.map((output) => output.path),
315
+ evidenceJsonFields: ["path", "sha256", "byteSize", "language", "cueCount", "captionTrackId"]
316
+ },
317
+ timeline: {
318
+ required: true,
319
+ outputId: renderOutputPackage.outputs.timelineJson?.id,
320
+ path: renderOutputPackage.outputs.timelineJson?.path,
321
+ evidenceJsonFields: ["path", "sha256", "byteSize", "shotTimelineId", "frameRate", "duration"]
322
+ },
323
+ audioStems: {
324
+ required: true,
325
+ artifact: audioStemManifest.artifact,
326
+ path: "dist/render/audio-stems.json",
327
+ stemCount: audioStemManifest.stems.length,
328
+ evidenceJsonFields: ["path", "sha256", "byteSize", "stemCount", "dialogueStemCount", "sfxStemCount"]
329
+ },
330
+ evidenceJson: {
331
+ required: true,
332
+ outputId: renderOutputPackage.outputs.evidenceJson?.id,
333
+ path: renderOutputPackage.outputs.evidenceJson?.path,
334
+ evidenceJsonFields: ["path", "sha256", "byteSize", "schema", "ok", "gates", "proofs"]
335
+ },
336
+ youtubeDraftMetadata: {
337
+ required: true,
338
+ outputId: renderOutputPackage.outputs.youtubeMetadata?.id,
339
+ path: renderOutputPackage.outputs.youtubeMetadata?.path,
340
+ evidenceJsonFields: ["title", "description", "language", "videoPath", "thumbnailPath", "captionsPath"]
341
+ }
342
+ },
343
+ requiredEvidenceJsonFields: [
344
+ "artifacts.video",
345
+ "artifacts.thumbnail",
346
+ "artifacts.captions",
347
+ "artifacts.timeline",
348
+ "artifacts.audioStems",
349
+ "artifacts.evidenceJson",
350
+ "captionFrameSyncSourceProof.captionDisplayWithinOneFrame",
351
+ "visemeFrameSyncSourceProof.mouthMovementWithinOneFrame",
352
+ "phonemeVisemeDubSyncSourceProof.sampledPhonemeVisemeCues",
353
+ "phonemeVisemeDubSyncSourceProof.dubContinuity.stableShotIds",
354
+ "humanReview.status"
355
+ ],
356
+ remainingProofGates: [
357
+ "Fresh scaffold npm run build output",
358
+ "Browser route health and storyboard playback output",
359
+ "Typed character GLBs added through src/aura-assets.ts",
360
+ "npx @aura3d/cli@latest assets validate-cartoon output",
361
+ "Actual 60-second MP4/WebM render artifact",
362
+ "Actual thumbnail artifact captured from the same Aura3D scene state",
363
+ "Actual captions/timeline/audio-stems/evidence JSON files with hashes",
364
+ "Human visual review approval"
365
+ ]
366
+ } as const;
367
+
368
+ export const auraVoicePackage = createAuraVoiceBridgePackage({
369
+ episodePlan: episode.episodePlan,
370
+ storyboard: episode.storyboard,
371
+ shotTimeline: episode.shotTimeline,
372
+ dialogueTrack: episode.dialogueTrack,
373
+ captionTrack: episode.captionTrack,
374
+ visemes: visemeTrack,
375
+ audioStems: audioStemManifest,
376
+ dubMap: spanishDubMap,
377
+ renderQueue: renderPlan,
378
+ renderOutputPackage
379
+ }, {
380
+ route: "/",
381
+ frameRate,
382
+ maxTimingDriftFrames: 1,
383
+ youtube: youtubeDraftMetadata
384
+ });
385
+
386
+ export const deterministicScreenshotFixtures = createPromptAnimationDeterministicScreenshotFixtureMetadata({
387
+ bridgePackage: auraVoicePackage,
388
+ count: 3,
389
+ pathPrefix: "artifacts/screenshots/moon-garden"
390
+ });
391
+
392
+ export const accessibilityProofMetadata = auraVoicePackage.artifacts.episodePlan.accessibilityProof;
393
+
394
+ export const bridgeIssues = validateAuraVoiceBridgePackage(auraVoicePackage);
395
+
396
+ export const bridgeSampleAtThumbnail = sampleAuraVoiceBridgeAtTime(auraVoicePackage, renderOutputPackage.thumbnailCapture.time);
397
+
398
+ export const promptAnimationEvidence = collectPromptAnimationEvidence({
399
+ bridgePackage: auraVoicePackage,
400
+ performanceCoverage,
401
+ routeHealth: { status: "missing" },
402
+ generatedAt: episode.episodePlan.generatedAt
403
+ });
404
+
405
+ export const publishReadiness = evaluatePromptAnimationPublishReadiness(promptAnimationEvidence);
@@ -0,0 +1,14 @@
1
+ import { AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID } from "./contract";
2
+
3
+ export const setContractId = AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID;
4
+
5
+ export const sets = [
6
+ {
7
+ id: "moon-garden",
8
+ name: "Moon Garden",
9
+ layers: ["foreground flowers", "midground robot path", "background planet skyline"],
10
+ primitiveFallbackProps: ["glow-broom", "glow-stones", "moon-lilies"],
11
+ optionalTypedAssetKeys: ["glowBroom", "glowStones", "moonLilies"],
12
+ safety: "soft colors, readable captions, reduced flash by default"
13
+ }
14
+ ];
@@ -0,0 +1,7 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("episode builder route loads", async ({ page }) => {
4
+ await page.goto("/");
5
+ await expect(page.locator("#app")).toBeVisible();
6
+ });
7
+
@@ -0,0 +1,98 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("episode builder storyboard caption renders", async ({ page }) => {
4
+ await page.goto("/");
5
+ await expect(page.getByText(/Aura3D episode builder|moon|robot/i)).toBeVisible();
6
+ });
7
+
8
+ test("storyboard playback, character performance, caption timing, cuts, and nonblank cartoon frames are sourced", async ({
9
+ page
10
+ }) => {
11
+ await page.goto("/");
12
+
13
+ const routeProof = (await page.evaluate(() => {
14
+ const template = (window as unknown as {
15
+ __AURA3D_CARTOON_TEMPLATE__?: {
16
+ shotIds: readonly string[];
17
+ captionIds: readonly string[];
18
+ storyBible?: unknown;
19
+ builder?: unknown;
20
+ playbackProbeSamples: readonly unknown[];
21
+ sampleAt(time: number): unknown;
22
+ };
23
+ }).__AURA3D_CARTOON_TEMPLATE__;
24
+ return {
25
+ shotIds: template?.shotIds ?? [],
26
+ captionIds: template?.captionIds ?? [],
27
+ storyBible: template?.storyBible,
28
+ builder: template?.builder,
29
+ samples: [1, 21, 43].map((time) => template?.sampleAt(time)),
30
+ playbackProbeSamples: template?.playbackProbeSamples ?? [],
31
+ bodyShotCount: document.body.dataset.cartoonShotCount,
32
+ bodyCaptionCount: document.body.dataset.cartoonCaptionCount,
33
+ bodyFormats: document.body.dataset.episodeBuilderFormats
34
+ };
35
+ })) as {
36
+ shotIds: string[];
37
+ captionIds: string[];
38
+ storyBible?: {
39
+ props?: unknown[];
40
+ styleGuide?: { visualStyle?: string };
41
+ shotList?: unknown[];
42
+ };
43
+ builder?: {
44
+ formats?: readonly { id: string }[];
45
+ wizardSteps?: readonly { id: string; complete: boolean }[];
46
+ compiledEpisode?: { shotCount?: number; captionCount?: number; renderQueueItems?: number };
47
+ typedAssets?: { commands?: readonly string[] };
48
+ };
49
+ samples: Array<{
50
+ shotId?: string;
51
+ captionId?: string;
52
+ captionText?: string;
53
+ cameraMove?: string;
54
+ transitionOut?: string;
55
+ nodeUpdates?: Array<{ characterId?: string; action?: string; emotion?: string }>;
56
+ }>;
57
+ playbackProbeSamples: unknown[];
58
+ bodyShotCount?: string;
59
+ bodyCaptionCount?: string;
60
+ bodyFormats?: string;
61
+ };
62
+
63
+ expect(routeProof.shotIds).toEqual([
64
+ "shot-moon-garden-open",
65
+ "shot-glow-stone-teamwork",
66
+ "shot-moon-garden-finish"
67
+ ]);
68
+ expect(new Set(routeProof.samples.map((sample) => sample.shotId)).size).toBe(3);
69
+ expect(routeProof.captionIds).toHaveLength(6);
70
+ expect(routeProof.samples.every((sample) => sample.captionId && sample.captionText)).toBe(true);
71
+ expect(routeProof.samples.some((sample) => sample.cameraMove === "push-in" || sample.transitionOut === "cut")).toBe(true);
72
+ expect(routeProof.samples.flatMap((sample) => sample.nodeUpdates ?? []).some((update) => update.action === "speak")).toBe(true);
73
+ expect(routeProof.samples.flatMap((sample) => sample.nodeUpdates ?? []).some((update) => update.characterId === "miko")).toBe(true);
74
+ expect(routeProof.samples.flatMap((sample) => sample.nodeUpdates ?? []).some((update) => update.characterId === "luma")).toBe(true);
75
+ expect(routeProof.storyBible?.props?.length).toBeGreaterThanOrEqual(3);
76
+ expect(routeProof.storyBible?.styleGuide?.visualStyle).toMatch(/cartoon/i);
77
+ expect(routeProof.storyBible?.shotList).toHaveLength(3);
78
+ expect(routeProof.playbackProbeSamples).toHaveLength(3);
79
+ expect(routeProof.bodyShotCount).toBe("3");
80
+ expect(routeProof.bodyCaptionCount).toBe("6");
81
+ expect(routeProof.builder?.formats?.map((format) => format.id)).toEqual([
82
+ "short-form",
83
+ "standard",
84
+ "series-pilot",
85
+ "educational",
86
+ "music-video"
87
+ ]);
88
+ expect(routeProof.builder?.wizardSteps?.some((step) => step.id === "beats" && step.complete)).toBe(true);
89
+ expect(routeProof.builder?.compiledEpisode?.shotCount).toBe(3);
90
+ expect(routeProof.builder?.compiledEpisode?.captionCount).toBe(6);
91
+ expect(routeProof.builder?.compiledEpisode?.renderQueueItems).toBeGreaterThan(0);
92
+ expect(routeProof.builder?.typedAssets?.commands?.some((command) => command.includes("assets validate-cartoon"))).toBe(true);
93
+ expect(routeProof.bodyFormats).toBe("short-form,standard,series-pilot,educational,music-video");
94
+ await expect(page.locator("#episode-builder-panel")).toBeVisible();
95
+
96
+ const screenshot = await page.screenshot();
97
+ expect(screenshot.byteLength).toBeGreaterThan(2048);
98
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "jsx": "react-jsx",
8
+ "types": ["vite/client"]
9
+ },
10
+ "include": ["src"]
11
+ }
12
+
@@ -11,7 +11,7 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/gameplay-smoke.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "1.0.6"
14
+ "@aura3d/engine": "1.0.10"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@playwright/test": "^1.52.0",
@@ -1,5 +1,5 @@
1
1
  import { game, games } from "@aura3d/engine";
2
- import { publicAssetInstructions, REQUIRED_FIGHTER_ASSETS, REQUIRED_FIGHTER_CLIPS } from "./fighters";
2
+ import { publicAssetInstructions, REQUIRED_FIGHTER_ASSETS, REQUIRED_FIGHTER_CLIPS, type FighterAssetKey } from "./fighters";
3
3
 
4
4
  type GameVec3 = readonly [number, number, number];
5
5
 
@@ -21,9 +21,17 @@ export const fightingStageBounds = {
21
21
  maxZ: fightingStage.combatBounds.maxZ
22
22
  };
23
23
 
24
- export const fightingRouteReadiness = {
24
+ export interface FightingRouteReadinessOptions {
25
+ readonly missingFighterAssets: readonly FighterAssetKey[];
26
+ }
27
+
28
+ export function createFightingRouteReadiness(options: FightingRouteReadinessOptions) {
29
+ const placeholderMode = options.missingFighterAssets.length > 0;
30
+ return {
25
31
  kind: "aura3d-fighting-game-route-readiness",
26
- sourceOnly: true,
32
+ sourceOnly: placeholderMode,
33
+ placeholderMode,
34
+ proofMode: placeholderMode ? "source-placeholders" : "typed-assets",
27
35
  template: "fighting-game",
28
36
  route: "/",
29
37
  packageName: "aura3d-fighting-game",
@@ -50,6 +58,7 @@ export const fightingRouteReadiness = {
50
58
  "game.debug.overlay"
51
59
  ],
52
60
  requiredTypedAssets: REQUIRED_FIGHTER_ASSETS,
61
+ missingTypedAssets: options.missingFighterAssets,
53
62
  requiredAnimationClips: REQUIRED_FIGHTER_CLIPS,
54
63
  assetCommands: publicAssetInstructions,
55
64
  buildDeclarations: {
@@ -74,7 +83,12 @@ export const fightingRouteReadiness = {
74
83
  message: issue.message
75
84
  }))
76
85
  }
77
- } as const;
86
+ } as const;
87
+ }
88
+
89
+ export const fightingRouteReadiness = createFightingRouteReadiness({
90
+ missingFighterAssets: REQUIRED_FIGHTER_ASSETS
91
+ });
78
92
 
79
93
  export function createFighterColliders(playerPosition: GameVec3, rivalPosition: GameVec3) {
80
94
  return [
@@ -13,9 +13,9 @@ import {
13
13
  import { heavyMove, lightMove, specialMove } from "./game/moves";
14
14
  import {
15
15
  createFighterColliders,
16
+ createFightingRouteReadiness,
16
17
  createTouchLayout,
17
18
  fightingControls,
18
- fightingRouteReadiness,
19
19
  fightingStage,
20
20
  fightingStageBounds,
21
21
  fightingStageIssues
@@ -41,6 +41,7 @@ const playerStart = [-0.9, 0, 0] as const;
41
41
  const rivalStart = [0.9, 0, 0] as const;
42
42
  const stageWarnings = fightingStageIssues.map((issue) => issue.message);
43
43
  const touchLayout = createTouchLayout(window.innerWidth, window.innerHeight);
44
+ const routeReadiness = createFightingRouteReadiness({ missingFighterAssets });
44
45
 
45
46
  ui.html(
46
47
  "#hud",
@@ -210,7 +211,7 @@ gameWindow.__AURA3D_GAME_SOURCE__ = {
210
211
  missingAssets: missingFighterAssets,
211
212
  addAssets: publicAssetInstructions,
212
213
  touchControls: touchLayout,
213
- readiness: fightingRouteReadiness
214
+ readiness: routeReadiness
214
215
  };
215
216
 
216
217
  let aiCooldown = 0;
@@ -11,7 +11,7 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "1.0.5"
14
+ "@aura3d/engine": "1.0.10"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,7 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/screenshot.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "1.0.5"
14
+ "@aura3d/engine": "1.0.10"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@playwright/test": "^1.52.0",
@@ -11,7 +11,7 @@
11
11
  "test": "playwright test tests/route-health.spec.ts tests/storyboard-playback.spec.ts --workers=1"
12
12
  },
13
13
  "dependencies": {
14
- "@aura3d/engine": "1.0.5"
14
+ "@aura3d/engine": "1.0.10"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@playwright/test": "^1.52.0",