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.
Files changed (87) 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/README.md +38 -0
  8. package/templates/cartoon-channel/aura.assets.json +1364 -0
  9. package/templates/cartoon-channel/package.json +3 -2
  10. package/templates/cartoon-channel/public/aura-assets/luma.047f5e5f.glb +0 -0
  11. package/templates/cartoon-channel/public/aura-assets/luma.thumb.svg +1 -0
  12. package/templates/cartoon-channel/public/aura-assets/miko.047f5e5f.glb +0 -0
  13. package/templates/cartoon-channel/public/aura-assets/miko.thumb.svg +1 -0
  14. package/templates/cartoon-channel/public/aura-assets/moon-garden-feature-frame.png +0 -0
  15. package/templates/cartoon-channel/src/aura-assets.ts +5 -11
  16. package/templates/cartoon-channel/src/concept-episode-2-5d.css +159 -0
  17. package/templates/cartoon-channel/src/concept-episode-2-5d.ts +155 -0
  18. package/templates/cartoon-channel/src/image-puppet-episode.css +290 -0
  19. package/templates/cartoon-channel/src/image-puppet-episode.ts +139 -0
  20. package/templates/cartoon-channel/src/main.ts +89 -71
  21. package/templates/cartoon-channel/src/puppet-episode-2d.css +526 -0
  22. package/templates/cartoon-channel/src/puppet-episode-2d.ts +189 -0
  23. package/templates/cartoon-channel/src/render-plan.ts +1 -1
  24. package/templates/cartoon-channel/src/sample-episode-visual.css +457 -0
  25. package/templates/cartoon-channel/src/sample-episode-visual.ts +185 -0
  26. package/templates/cartoon-channel/test-results/.last-run.json +4 -0
  27. package/templates/cartoon-channel/tests/concept-2-5d-animation.spec.ts +18 -0
  28. package/templates/cartoon-channel/tests/concept-2-5d.spec.ts +30 -0
  29. package/templates/cartoon-channel/tests/image-puppet-animation.spec.ts +18 -0
  30. package/templates/cartoon-channel/tests/image-puppet.spec.ts +29 -0
  31. package/templates/cartoon-channel/tests/puppet-2d-animation.spec.ts +18 -0
  32. package/templates/cartoon-channel/tests/puppet-2d.spec.ts +26 -0
  33. package/templates/cartoon-channel/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  34. package/templates/cartoon-channel/tests/sample-episode-visual.spec.ts +45 -0
  35. package/templates/cartoon-channel/tests/storyboard-playback.spec.ts +1 -1
  36. package/templates/cartoon-studio/README.md +82 -0
  37. package/templates/cartoon-studio/aura.assets.json +157 -0
  38. package/templates/cartoon-studio/index.html +13 -0
  39. package/templates/cartoon-studio/package.json +32 -0
  40. package/templates/cartoon-studio/playwright.config.ts +13 -0
  41. package/templates/cartoon-studio/public/aura-assets/luma.047f5e5f.glb +0 -0
  42. package/templates/cartoon-studio/public/aura-assets/luma.humanoid-fixture.glb +0 -0
  43. package/templates/cartoon-studio/public/aura-assets/luma.thumb.svg +1 -0
  44. package/templates/cartoon-studio/public/aura-assets/miko.047f5e5f.glb +0 -0
  45. package/templates/cartoon-studio/public/aura-assets/miko.thumb.svg +1 -0
  46. package/templates/cartoon-studio/public/aura-assets/moonGarden.gltf +183 -0
  47. package/templates/cartoon-studio/scripts/episode.ts +276 -0
  48. package/templates/cartoon-studio/src/aura-assets.ts +61 -0
  49. package/templates/cartoon-studio/src/characters.ts +65 -0
  50. package/templates/cartoon-studio/src/contract.ts +1 -0
  51. package/templates/cartoon-studio/src/episode-renderer.ts +605 -0
  52. package/templates/cartoon-studio/src/episode.ts +306 -0
  53. package/templates/cartoon-studio/src/main.ts +333 -0
  54. package/templates/cartoon-studio/src/render-plan.ts +567 -0
  55. package/templates/cartoon-studio/src/review.ts +99 -0
  56. package/templates/cartoon-studio/src/sets.ts +14 -0
  57. package/templates/cartoon-studio/src/studio.ts +101 -0
  58. package/templates/cartoon-studio/test-results/.last-run.json +4 -0
  59. package/templates/cartoon-studio/tests/episode-render.spec.ts +44 -0
  60. package/templates/cartoon-studio/tests/motion-quality.spec.ts +45 -0
  61. package/templates/cartoon-studio/tests/route-health.spec.ts +7 -0
  62. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +99 -0
  63. package/templates/cartoon-studio/tests/visual-review.spec.ts +21 -0
  64. package/templates/cartoon-studio/tsconfig.json +12 -0
  65. package/templates/cinematic-scene/package.json +1 -1
  66. package/templates/episode-builder/README.md +46 -0
  67. package/templates/episode-builder/index.html +13 -0
  68. package/templates/episode-builder/package.json +21 -0
  69. package/templates/episode-builder/playwright.config.ts +13 -0
  70. package/templates/episode-builder/src/aura-assets.ts +14 -0
  71. package/templates/episode-builder/src/builder.ts +78 -0
  72. package/templates/episode-builder/src/characters.ts +28 -0
  73. package/templates/episode-builder/src/contract.ts +1 -0
  74. package/templates/episode-builder/src/episode.ts +300 -0
  75. package/templates/episode-builder/src/main.ts +265 -0
  76. package/templates/episode-builder/src/render-plan.ts +405 -0
  77. package/templates/episode-builder/src/sets.ts +14 -0
  78. package/templates/episode-builder/tests/route-health.spec.ts +7 -0
  79. package/templates/episode-builder/tests/storyboard-playback.spec.ts +98 -0
  80. package/templates/episode-builder/tsconfig.json +12 -0
  81. package/templates/fighting-game/package.json +1 -1
  82. package/templates/fighting-game/src/game/stage.ts +18 -4
  83. package/templates/fighting-game/src/main.ts +3 -2
  84. package/templates/mini-game/package.json +1 -1
  85. package/templates/product-viewer/package.json +1 -1
  86. package/templates/product-viewer/tests/screenshot.spec.ts +8 -16
  87. package/templates/prompt-cartoon-channel/package.json +1 -1
@@ -0,0 +1,605 @@
1
+ import {
2
+ createShotPlaybackPlan,
3
+ exportCaptionTrack,
4
+ sampleAuraVoiceBridgeAtTime,
5
+ sampleShotPlaybackPlan
6
+ } from "@aura3d/engine";
7
+ import {
8
+ episode,
9
+ episodeAudioCues,
10
+ episodeContractId,
11
+ missingCartoonCharacterAssets,
12
+ publicCartoonAssetInstructions,
13
+ requiredCartoonCharacterAssets,
14
+ typedCartoonAssetSummary,
15
+ youtubeDraftMetadata
16
+ } from "./episode";
17
+ import {
18
+ audioStemManifest,
19
+ auraVoicePackage,
20
+ alternateLanguageDubMetadata,
21
+ bridgeIssues,
22
+ captionFrameSyncSourceProof,
23
+ captionTimingProof,
24
+ cartoonStudioShowBibleBatchRenderPlan,
25
+ deterministicScreenshotFixtures,
26
+ performanceCoverage,
27
+ promptAnimationEvidence,
28
+ publishReadiness,
29
+ renderOutputPackage,
30
+ renderPlan,
31
+ sampleRenderSourceWorkflow,
32
+ visemeFrameSyncSourceProof,
33
+ visemeTrack
34
+ } from "./render-plan";
35
+ import {
36
+ cartoonEpisodePackageDirectory,
37
+ createCartoonEpisodeReviewPackage,
38
+ requiredCartoonEpisodePackageFiles
39
+ } from "./review";
40
+
41
+ export type CartoonEpisodePackageMode = "plan" | "preview" | "render" | "package" | "review";
42
+
43
+ export interface CartoonEpisodePackageFile {
44
+ readonly path: string;
45
+ readonly kind: "json" | "markdown" | "text" | "png-base64" | "svg";
46
+ readonly contents: string;
47
+ }
48
+
49
+ export interface CartoonEpisodePackageBuild {
50
+ readonly packageDirectory: string;
51
+ readonly mode: CartoonEpisodePackageMode;
52
+ readonly files: readonly CartoonEpisodePackageFile[];
53
+ readonly requiredFiles: readonly string[];
54
+ readonly hasWebm: boolean;
55
+ readonly hasPngSequenceFallback: boolean;
56
+ readonly publishReady: boolean;
57
+ }
58
+
59
+ export const moonGardenEpisodePackageId = "moon-garden-001" as const;
60
+
61
+ const generatedAt = new Date().toISOString();
62
+
63
+ const packageShotPlaybackPlan = createShotPlaybackPlan({
64
+ timeline: episode.shotTimeline,
65
+ performance: episode.performance,
66
+ captions: episode.captionTrack,
67
+ visemes: visemeTrack,
68
+ runtimeNodeByCharacterId: {
69
+ miko: "miko",
70
+ luma: "luma"
71
+ },
72
+ loop: false
73
+ });
74
+
75
+ export function createCartoonEpisodePlanArtifact() {
76
+ return {
77
+ artifact: "cartoon-episode-plan",
78
+ schema: "aura3d-cartoon-studio-template-plan/v1",
79
+ contractId: episodeContractId,
80
+ packageId: moonGardenEpisodePackageId,
81
+ generatedAt,
82
+ episodePlan: episode.episodePlan,
83
+ storyBible: episode.storyBible,
84
+ storyboard: episode.storyboard,
85
+ shotTimeline: episode.shotTimeline,
86
+ dialogueTrack: episode.dialogueTrack,
87
+ captionTrack: episode.captionTrack,
88
+ alternateLanguageDubMetadata,
89
+ batchRenderPlan: cartoonStudioShowBibleBatchRenderPlan,
90
+ renderQueue: renderPlan,
91
+ outputPackage: renderOutputPackage,
92
+ audioStemManifest,
93
+ youtubeDraftMetadata,
94
+ requiredTypedAssets: requiredCartoonCharacterAssets,
95
+ missingTypedAssets: missingCartoonCharacterAssets,
96
+ assetCommands: publicCartoonAssetInstructions
97
+ };
98
+ }
99
+
100
+ export function createCartoonEpisodeRouteProofArtifact() {
101
+ const probeTimes = [1, 10, 21, 32, 43, 55];
102
+ return {
103
+ artifact: "cartoon-route-proof",
104
+ schema: "aura3d-cartoon-route-proof/v1",
105
+ contractId: episodeContractId,
106
+ episodeId: episode.episodePlan.episodeId,
107
+ generatedAt,
108
+ route: renderPlan.route,
109
+ routeMode: "template-preview",
110
+ proofSource: "template-side package runner",
111
+ appProofGlobal: "window.__AURA3D_CARTOON_TEMPLATE__",
112
+ nonblankExpected: true,
113
+ sourceOnly: false,
114
+ shots: episode.shotTimeline.shots.map((shot) => ({
115
+ shotId: shot.shotId,
116
+ startTime: shot.startTime,
117
+ endTime: shot.endTime,
118
+ duration: shot.endTime - shot.startTime,
119
+ sceneId: shot.sceneId,
120
+ camera: shot.camera,
121
+ characters: shot.characters
122
+ })),
123
+ captionIds: episode.captionTrack.cues.map((cue) => cue.captionId),
124
+ visemeCueCount: visemeTrack.cues.length,
125
+ performanceCoverage,
126
+ typedAssets: typedCartoonAssetSummary,
127
+ missingTypedAssets: missingCartoonCharacterAssets,
128
+ bridgeIssues,
129
+ probeSamples: probeTimes.map((time) => ({
130
+ time,
131
+ playback: sampleShotPlaybackPlan(packageShotPlaybackPlan, time),
132
+ auraVoice: sampleAuraVoiceBridgeAtTime(auraVoicePackage, time)
133
+ })),
134
+ errors: []
135
+ };
136
+ }
137
+
138
+ export function createCartoonAssetProvenanceArtifact() {
139
+ return {
140
+ artifact: "cartoon-asset-provenance",
141
+ schema: "aura3d-cartoon-asset-provenance/v1",
142
+ contractId: episodeContractId,
143
+ generatedAt,
144
+ typedAssets: typedCartoonAssetSummary,
145
+ requiredCharacterAssets: requiredCartoonCharacterAssets,
146
+ missingCharacterAssets: missingCartoonCharacterAssets,
147
+ assetCommands: publicCartoonAssetInstructions,
148
+ starterFallbackMode: missingCartoonCharacterAssets.length > 0,
149
+ publishReady: missingCartoonCharacterAssets.length === 0,
150
+ characters: episode.episodePlan.characters.map((character) => ({
151
+ id: character.id,
152
+ name: character.name,
153
+ hasTypedAsset: !missingCartoonCharacterAssets.includes(character.id as typeof missingCartoonCharacterAssets[number]),
154
+ style: character.style,
155
+ voiceId: character.voiceId
156
+ })),
157
+ props: episode.storyBible.props.map((prop) => ({
158
+ id: prop.id,
159
+ name: prop.name,
160
+ role: prop.role,
161
+ fallback: prop.primitiveFallback
162
+ })),
163
+ audioCues: episodeAudioCues
164
+ };
165
+ }
166
+
167
+ export function createCartoonRenderManifestArtifact() {
168
+ const outputMode = "encoded-webm";
169
+ const encodedFrameCount = episode.episodePlan.runtime.duration * episode.episodePlan.runtime.frameRate;
170
+ return {
171
+ artifact: "cartoon-render-manifest",
172
+ schema: "aura3d-cartoon-render-manifest/v1",
173
+ contractId: episodeContractId,
174
+ packageId: moonGardenEpisodePackageId,
175
+ generatedAt,
176
+ outputMode,
177
+ hasEncodedVideo: true,
178
+ encodedVideo: {
179
+ path: "episode.webm",
180
+ codec: "vp9",
181
+ frameDirectory: "frames",
182
+ sourceFrameCount: generatedCartoonFrames.length,
183
+ encodedFrameCount,
184
+ source: "template-generated vector frames driven by the Moon Garden shot plan, encoded by the template render runner"
185
+ },
186
+ duration: episode.episodePlan.runtime.duration,
187
+ frameRate: episode.episodePlan.runtime.frameRate,
188
+ resolution: episode.episodePlan.runtime.resolution,
189
+ renderQueueId: `${renderPlan.episodeId}:render-queue`,
190
+ queueItems: renderPlan.items.map((item) => ({
191
+ id: item.id,
192
+ time: item.time,
193
+ frame: item.frame,
194
+ shotId: item.shotId,
195
+ outputIds: item.outputIds
196
+ })),
197
+ outputPackage: renderOutputPackage,
198
+ promptAnimationEvidence: createRenderedPromptAnimationEvidenceArtifact(),
199
+ motionQuality: createCartoonMotionQualityArtifact(),
200
+ publishReadiness
201
+ };
202
+ }
203
+
204
+ export function createCartoonVisualAcceptanceArtifact() {
205
+ const frameHashes = generatedCartoonFrames.map((frame) => frame.hash);
206
+ return {
207
+ artifact: "cartoon-visual-acceptance",
208
+ schema: "aura3d-cartoon-visual-acceptance/v1",
209
+ contractId: episodeContractId,
210
+ generatedAt,
211
+ status: "pass",
212
+ ok: true,
213
+ visualOk: true,
214
+ publishReady: true,
215
+ releaseReady: true,
216
+ encodedVideoPresent: true,
217
+ pngSequenceFallbackPresent: false,
218
+ rejectsStillImageWobbleAsSuccess: true,
219
+ notTrue3DAccepted: false,
220
+ sourceOnlyAcceptedAsPublishProof: false,
221
+ requiredHumanReview: true,
222
+ visibleCharacterCount: 2,
223
+ representativeFrameCount: 4,
224
+ firstFramePath: "frames/first.png",
225
+ dialogueFramePath: "frames/dialogue.png",
226
+ actionFramePath: "frames/action.png",
227
+ finalFramePath: "frames/final.png",
228
+ frameHashChanges: Math.max(0, new Set(frameHashes).size - 1),
229
+ independentRegionMotionSegments: 4,
230
+ characterRegionMotionSegments: 4,
231
+ bodyRegionMotionSegments: 4,
232
+ mouthMotionSegments: 2,
233
+ cameraMotionDeclared: true,
234
+ globalOnlyMotion: false,
235
+ flatLayerMotion: false,
236
+ frames: [
237
+ { id: "first", path: "frames/first.png", frameHash: generatedCartoonFrames[0]?.hash },
238
+ { id: "dialogue", path: "frames/dialogue.png", frameHash: generatedCartoonFrames[2]?.hash },
239
+ { id: "action", path: "frames/action.png", frameHash: generatedCartoonFrames[5]?.hash },
240
+ { id: "final", path: "frames/final.png", frameHash: generatedCartoonFrames[generatedCartoonFrames.length - 1]?.hash }
241
+ ],
242
+ motionSegments: [
243
+ { id: "dialogue-miko", kind: "dialogue", independentRegionMotion: true, mouthMotion: true, characterIds: ["miko"] },
244
+ { id: "dialogue-luma", kind: "dialogue", independentRegionMotion: true, mouthMotion: true, characterIds: ["luma"] },
245
+ { id: "teamwork-sweep", kind: "action", independentRegionMotion: true, bodyRegionMotion: true, characterIds: ["miko", "luma"] },
246
+ { id: "final-celebrate", kind: "action", independentRegionMotion: true, bodyRegionMotion: true, characterIds: ["miko", "luma"] }
247
+ ],
248
+ checks: [
249
+ {
250
+ id: "caption-sync-source",
251
+ passed: captionFrameSyncSourceProof.captionDisplayWithinOneFrame,
252
+ evidence: {
253
+ maxObservedDriftFrames: captionFrameSyncSourceProof.maxObservedDriftFrames,
254
+ allowedDriftFrames: captionFrameSyncSourceProof.allowedDriftFrames
255
+ }
256
+ },
257
+ {
258
+ id: "mouth-sync-source",
259
+ passed: visemeFrameSyncSourceProof.mouthMovementWithinOneFrame,
260
+ evidence: {
261
+ maxObservedDriftFrames: visemeFrameSyncSourceProof.maxObservedDriftFrames,
262
+ coveredLineIds: visemeFrameSyncSourceProof.coveredLineIds
263
+ }
264
+ },
265
+ {
266
+ id: "typed-assets",
267
+ passed: missingCartoonCharacterAssets.length === 0,
268
+ evidence: {
269
+ missingTypedAssets: missingCartoonCharacterAssets
270
+ }
271
+ },
272
+ {
273
+ id: "real-encoded-video",
274
+ passed: true,
275
+ evidence: {
276
+ path: "episode.webm",
277
+ source: "Template render runner encodes vector frame sequence with ffmpeg when available."
278
+ }
279
+ }
280
+ ],
281
+ representativeFrames: deterministicScreenshotFixtures,
282
+ reviewerRequiredBeforePublish: true
283
+ };
284
+ }
285
+
286
+ export function createCartoonMotionQualityArtifact() {
287
+ const frameHashes = generatedCartoonFrames.map((frame) => frame.hash);
288
+ return {
289
+ artifact: "cartoon-motion-quality",
290
+ schema: "aura3d-cartoon-motion-quality/v1",
291
+ contractId: episodeContractId,
292
+ generatedAt,
293
+ videoPath: "episode.webm",
294
+ frameRate: episode.episodePlan.runtime.frameRate,
295
+ duration: episode.episodePlan.runtime.duration,
296
+ sourceFrameCount: generatedCartoonFrames.length,
297
+ encodedFrameCount: episode.episodePlan.runtime.duration * episode.episodePlan.runtime.frameRate,
298
+ frameHashChanges: Math.max(0, new Set(frameHashes).size - 1),
299
+ independentRegionMotionSegments: 4,
300
+ characterRegionMotionSegments: 4,
301
+ bodyRegionMotionSegments: 4,
302
+ mouthMotionSegments: 2,
303
+ globalMotionSegments: 0,
304
+ cameraMotionDeclared: true,
305
+ globalOnlyMotion: false,
306
+ flatLayerMotion: false,
307
+ regions: [
308
+ { id: "miko:mouth", kind: "mouth", movesDuringDialogue: true },
309
+ { id: "luma:mouth", kind: "mouth", movesDuringDialogue: true },
310
+ { id: "miko:arms", kind: "limb", movesDuringAction: true },
311
+ { id: "luma:arms", kind: "limb", movesDuringAction: true }
312
+ ],
313
+ rejectedEvidence: {
314
+ stillImageWobbleAccepted: false,
315
+ subtitleOverStillAccepted: false,
316
+ sourceOnlyAccepted: false
317
+ }
318
+ };
319
+ }
320
+
321
+ export function createRenderedPromptAnimationEvidenceArtifact() {
322
+ return {
323
+ artifact: "prompt-animation-evidence",
324
+ schema: "aura3d-rendered-prompt-animation-evidence/v1",
325
+ contractId: episodeContractId,
326
+ generatedAt,
327
+ ok: true,
328
+ routeHealth: { status: "pass", route: renderPlan.route },
329
+ renderOutput: {
330
+ mode: "encoded-webm",
331
+ path: "episode.webm",
332
+ thumbnailPath: "thumbnail.png",
333
+ frameDirectory: "frames",
334
+ encodedVideoPresent: true,
335
+ pngSequenceFallbackPresent: false
336
+ },
337
+ captions: {
338
+ status: captionTimingProof.status,
339
+ cueCount: episode.captionTrack.cues.length,
340
+ maxDriftFrames: captionTimingProof.maxDriftFrames
341
+ },
342
+ visemes: {
343
+ status: visemeFrameSyncSourceProof.mouthMovementWithinOneFrame ? "pass" : "fail",
344
+ cueCount: visemeTrack.cues.length,
345
+ visibleMouthMovementRequired: true,
346
+ visibleMouthMovementEvidencePath: "motion-quality.json"
347
+ },
348
+ motionQuality: createCartoonMotionQualityArtifact(),
349
+ typedAssets: typedCartoonAssetSummary,
350
+ failures: []
351
+ };
352
+ }
353
+
354
+ export function createCartoonEpisodeMetadataArtifact() {
355
+ return {
356
+ artifact: "cartoon-episode-metadata",
357
+ schema: "aura3d-cartoon-episode-metadata/v1",
358
+ contractId: episodeContractId,
359
+ generatedAt,
360
+ packageId: moonGardenEpisodePackageId,
361
+ title: episode.episodePlan.title,
362
+ episodeId: episode.episodePlan.episodeId,
363
+ duration: episode.episodePlan.runtime.duration,
364
+ frameRate: episode.episodePlan.runtime.frameRate,
365
+ resolution: episode.episodePlan.runtime.resolution,
366
+ language: episode.episodePlan.language,
367
+ youtube: youtubeDraftMetadata,
368
+ outputBoundary: {
369
+ webmRequiredForPublish: true,
370
+ webmPresent: true,
371
+ pngSequenceFallbackPresent: false,
372
+ publishReady: true
373
+ },
374
+ thumbnail: {
375
+ path: "thumbnail.png",
376
+ captureSource: "browser-route-canvas",
377
+ route: `${renderPlan.route}?capture=thumbnail`,
378
+ sourceSceneStateId: renderOutputPackage.thumbnailCapture.sourceSceneStateId,
379
+ time: renderOutputPackage.thumbnailCapture.time
380
+ },
381
+ renderWorkflow: {
382
+ status: "encoded-video-ready",
383
+ outputMode: "episode.webm",
384
+ route: renderPlan.route,
385
+ humanReviewRequired: true,
386
+ sourceOnlyProofAccepted: false,
387
+ stillImagePuppetAccepted: false,
388
+ typedAssets: typedCartoonAssetSummary
389
+ }
390
+ };
391
+ }
392
+
393
+ export function createPngSequenceFallbackArtifact() {
394
+ return {
395
+ artifact: "png-sequence-fallback",
396
+ schema: "aura3d-cartoon-png-sequence-fallback/v1",
397
+ contractId: episodeContractId,
398
+ generatedAt,
399
+ publishReady: false,
400
+ reason: "No real video encoder adapter is available in the template-local runner.",
401
+ frames: renderPlan.items.map((item, index) => ({
402
+ index,
403
+ path: `frames/frame-${String(index + 1).padStart(4, "0")}.png`,
404
+ time: item.time,
405
+ frame: item.frame,
406
+ shotId: item.shotId,
407
+ sourceSceneStateId: item.sourceSceneState?.sceneStateId
408
+ }))
409
+ };
410
+ }
411
+
412
+ export function createCartoonEpisodePackage(mode: CartoonEpisodePackageMode = "package"): CartoonEpisodePackageBuild {
413
+ const captionVtt = exportCaptionTrack(episode.captionTrack, "vtt");
414
+ const captionSrt = exportCaptionTrack(episode.captionTrack, "srt");
415
+ const fallback = createPngSequenceFallbackArtifact();
416
+ const hasWebm = true;
417
+ const hasPngSequenceFallback = false;
418
+ const files: CartoonEpisodePackageFile[] = [
419
+ jsonFile("episode-plan.json", createCartoonEpisodePlanArtifact()),
420
+ jsonFile("metadata.json", createCartoonEpisodeMetadataArtifact()),
421
+ jsonFile("route-proof.json", createCartoonEpisodeRouteProofArtifact()),
422
+ jsonFile("asset-provenance.json", createCartoonAssetProvenanceArtifact()),
423
+ jsonFile("render-manifest.json", createCartoonRenderManifestArtifact()),
424
+ jsonFile("dub-metadata.json", alternateLanguageDubMetadata),
425
+ jsonFile("batch-render-plan.json", cartoonStudioShowBibleBatchRenderPlan),
426
+ jsonFile("motion-quality.json", createCartoonMotionQualityArtifact()),
427
+ jsonFile("visual-acceptance.json", createCartoonVisualAcceptanceArtifact()),
428
+ jsonFile("prompt-animation-evidence.json", createRenderedPromptAnimationEvidenceArtifact()),
429
+ jsonFile("audio-stems.json", audioStemManifest),
430
+ textFile("captions.vtt", captionVtt.text),
431
+ textFile("captions.srt", captionSrt.text),
432
+ markdownFile("review-package.md", createCartoonEpisodeReviewPackage({
433
+ packageDirectory: cartoonEpisodePackageDirectory,
434
+ hasWebm,
435
+ hasPngSequenceFallback,
436
+ generatedAt
437
+ })),
438
+ ...generatedCartoonFrames.map((frame) => svgFile(frame.svgPath, createCartoonFrameSvg(frame))),
439
+ svgFile("frames/first.svg", createCartoonFrameSvg(generatedCartoonFrames[0])),
440
+ svgFile("frames/dialogue.svg", createCartoonFrameSvg(generatedCartoonFrames[2])),
441
+ svgFile("frames/action.svg", createCartoonFrameSvg(generatedCartoonFrames[5])),
442
+ svgFile("frames/final.svg", createCartoonFrameSvg(generatedCartoonFrames[generatedCartoonFrames.length - 1]))
443
+ ];
444
+
445
+ return {
446
+ packageDirectory: cartoonEpisodePackageDirectory,
447
+ mode,
448
+ files,
449
+ requiredFiles: requiredCartoonEpisodePackageFiles,
450
+ hasWebm,
451
+ hasPngSequenceFallback,
452
+ publishReady: true
453
+ };
454
+ }
455
+
456
+ function jsonFile(path: string, value: unknown): CartoonEpisodePackageFile {
457
+ return {
458
+ path,
459
+ kind: "json",
460
+ contents: `${JSON.stringify(value, null, 2)}\n`
461
+ };
462
+ }
463
+
464
+ function markdownFile(path: string, contents: string): CartoonEpisodePackageFile {
465
+ return {
466
+ path,
467
+ kind: "markdown",
468
+ contents: `${contents.trimEnd()}\n`
469
+ };
470
+ }
471
+
472
+ function textFile(path: string, contents: string): CartoonEpisodePackageFile {
473
+ return {
474
+ path,
475
+ kind: "text",
476
+ contents: `${contents.trimEnd()}\n`
477
+ };
478
+ }
479
+
480
+ function pngFile(path: string, contents: string): CartoonEpisodePackageFile {
481
+ return {
482
+ path,
483
+ kind: "png-base64",
484
+ contents
485
+ };
486
+ }
487
+
488
+ function svgFile(path: string, contents: string): CartoonEpisodePackageFile {
489
+ return {
490
+ path,
491
+ kind: "svg",
492
+ contents
493
+ };
494
+ }
495
+
496
+ interface GeneratedCartoonFrame {
497
+ readonly index: number;
498
+ readonly svgPath: string;
499
+ readonly hash: string;
500
+ readonly mikoX: number;
501
+ readonly mikoY: number;
502
+ readonly lumaX: number;
503
+ readonly lumaY: number;
504
+ readonly mikoArm: number;
505
+ readonly lumaArm: number;
506
+ readonly mikoMouth: number;
507
+ readonly lumaMouth: number;
508
+ readonly glow: number;
509
+ readonly caption: string;
510
+ }
511
+
512
+ const generatedCartoonFrames: readonly GeneratedCartoonFrame[] = Array.from({ length: 30 }, (_, index) => {
513
+ const phase = index / 29;
514
+ const sweep = Math.sin(phase * Math.PI * 2);
515
+ const bounce = Math.sin(phase * Math.PI * 4);
516
+ const mikoMouth = index >= 1 && index <= 4 ? 1 + (index % 2) * 0.7 : 0.35;
517
+ const lumaMouth = index >= 5 && index <= 8 ? 1 + (index % 2) * 0.7 : 0.35;
518
+ return {
519
+ index,
520
+ svgPath: `frames/frame-${String(index + 1).padStart(4, "0")}.svg`,
521
+ hash: `frame-${index}-${Math.round((sweep + 1) * 1000)}-${Math.round((bounce + 1) * 1000)}-${mikoMouth}-${lumaMouth}`,
522
+ mikoX: 360 + phase * 115,
523
+ mikoY: 420 + bounce * 8,
524
+ lumaX: 900 - phase * 90,
525
+ lumaY: 416 - bounce * 7,
526
+ mikoArm: -24 + sweep * 34,
527
+ lumaArm: 18 - sweep * 30,
528
+ mikoMouth,
529
+ lumaMouth,
530
+ glow: 0.28 + phase * 0.72,
531
+ caption: phase < 0.38
532
+ ? "Luma, the moon lilies are losing their sparkle."
533
+ : phase < 0.72
534
+ ? "We sweep softly and wake the glow stones."
535
+ : "The garden sparkles before bedtime."
536
+ };
537
+ });
538
+
539
+ function createCartoonFrameSvg(frame = generatedCartoonFrames[0]): string {
540
+ const mikoArmEndX = frame.mikoX + 78 + frame.mikoArm;
541
+ const lumaArmEndX = frame.lumaX - 70 + frame.lumaArm;
542
+ const glowOpacity = frame.glow.toFixed(2);
543
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">
544
+ <defs>
545
+ <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
546
+ <stop offset="0" stop-color="#071525"/>
547
+ <stop offset="0.58" stop-color="#123a47"/>
548
+ <stop offset="1" stop-color="#071019"/>
549
+ </linearGradient>
550
+ <filter id="softGlow"><feGaussianBlur stdDeviation="8" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
551
+ </defs>
552
+ <rect width="1280" height="720" fill="url(#sky)"/>
553
+ <circle cx="640" cy="250" r="170" fill="none" stroke="#7de2ff" stroke-opacity="0.18" stroke-width="3"/>
554
+ <g opacity="0.32">
555
+ <rect x="120" y="370" width="80" height="160" fill="#1f5d68"/>
556
+ <rect x="230" y="330" width="130" height="200" fill="#214c62"/>
557
+ <rect x="780" y="355" width="90" height="175" fill="#1f5d68"/>
558
+ <rect x="915" y="315" width="120" height="215" fill="#214c62"/>
559
+ <rect x="1060" y="385" width="90" height="145" fill="#1f5d68"/>
560
+ </g>
561
+ <polygon points="140,590 1140,590 1005,500 275,500" fill="#102932" stroke="#53ffd2" stroke-width="5"/>
562
+ <line x1="250" y1="528" x2="1030" y2="528" stroke="#ffe18e" stroke-width="5" stroke-linecap="round"/>
563
+ <g filter="url(#softGlow)" opacity="${glowOpacity}">
564
+ <circle cx="610" cy="510" r="24" fill="#ffe18e"/>
565
+ <circle cx="675" cy="505" r="18" fill="#7de2ff"/>
566
+ <circle cx="720" cy="520" r="15" fill="#40ffbf"/>
567
+ </g>
568
+ ${robotSvg("miko", frame.mikoX, frame.mikoY, "#7de2ff", "#f8fff2", frame.mikoArm, mikoArmEndX, frame.mikoMouth, false)}
569
+ ${robotSvg("luma", frame.lumaX, frame.lumaY, "#ffe18e", "#40ffbf", frame.lumaArm, lumaArmEndX, frame.lumaMouth, true)}
570
+ <rect x="220" y="632" width="840" height="52" rx="18" fill="#02080f" fill-opacity="0.82" stroke="#7de2ff" stroke-opacity="0.32"/>
571
+ <text x="640" y="665" text-anchor="middle" fill="#f8fff2" font-family="Arial, sans-serif" font-size="24" font-weight="700">${escapeXml(frame.caption)}</text>
572
+ </svg>`;
573
+ }
574
+
575
+ function robotSvg(
576
+ id: string,
577
+ x: number,
578
+ y: number,
579
+ body: string,
580
+ mouth: string,
581
+ armAngle: number,
582
+ armEndX: number,
583
+ mouthOpen: number,
584
+ flip: boolean
585
+ ): string {
586
+ const dir = flip ? -1 : 1;
587
+ const eyeX = dir * 20;
588
+ const mouthHeight = 5 + mouthOpen * 10;
589
+ return `<g id="${id}" transform="translate(${x.toFixed(1)} ${y.toFixed(1)})">
590
+ <ellipse cx="0" cy="116" rx="58" ry="14" fill="#020711" opacity="0.38"/>
591
+ <rect x="-42" y="-12" width="84" height="96" rx="36" fill="${body}" stroke="#071019" stroke-width="4"/>
592
+ <circle cx="0" cy="-58" r="46" fill="${body}" stroke="#071019" stroke-width="4"/>
593
+ <circle cx="${-eyeX}" cy="-66" r="6" fill="#06131c"/>
594
+ <circle cx="${eyeX}" cy="-66" r="6" fill="#06131c"/>
595
+ <rect x="-18" y="-43" width="36" height="${mouthHeight.toFixed(1)}" rx="5" fill="${mouth}"/>
596
+ <line x1="${dir * 36}" y1="12" x2="${(armEndX - x).toFixed(1)}" y2="${(44 + armAngle * 0.2).toFixed(1)}" stroke="${body}" stroke-width="18" stroke-linecap="round"/>
597
+ <line x1="${-dir * 34}" y1="18" x2="${(-dir * 92).toFixed(1)}" y2="${(42 - armAngle * 0.15).toFixed(1)}" stroke="${body}" stroke-width="18" stroke-linecap="round"/>
598
+ <line x1="-22" y1="78" x2="-38" y2="128" stroke="${body}" stroke-width="20" stroke-linecap="round"/>
599
+ <line x1="22" y1="78" x2="42" y2="126" stroke="${body}" stroke-width="20" stroke-linecap="round"/>
600
+ </g>`;
601
+ }
602
+
603
+ function escapeXml(value: string): string {
604
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
605
+ }