create-aura3d 1.0.10 → 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 (53) hide show
  1. package/package.json +1 -1
  2. package/templates/cartoon-channel/README.md +38 -0
  3. package/templates/cartoon-channel/aura.assets.json +1364 -0
  4. package/templates/cartoon-channel/package.json +2 -1
  5. package/templates/cartoon-channel/public/aura-assets/luma.047f5e5f.glb +0 -0
  6. package/templates/cartoon-channel/public/aura-assets/luma.thumb.svg +1 -0
  7. package/templates/cartoon-channel/public/aura-assets/miko.047f5e5f.glb +0 -0
  8. package/templates/cartoon-channel/public/aura-assets/miko.thumb.svg +1 -0
  9. package/templates/cartoon-channel/public/aura-assets/moon-garden-feature-frame.png +0 -0
  10. package/templates/cartoon-channel/src/aura-assets.ts +5 -11
  11. package/templates/cartoon-channel/src/concept-episode-2-5d.css +159 -0
  12. package/templates/cartoon-channel/src/concept-episode-2-5d.ts +155 -0
  13. package/templates/cartoon-channel/src/image-puppet-episode.css +290 -0
  14. package/templates/cartoon-channel/src/image-puppet-episode.ts +139 -0
  15. package/templates/cartoon-channel/src/main.ts +89 -71
  16. package/templates/cartoon-channel/src/puppet-episode-2d.css +526 -0
  17. package/templates/cartoon-channel/src/puppet-episode-2d.ts +189 -0
  18. package/templates/cartoon-channel/src/render-plan.ts +1 -1
  19. package/templates/cartoon-channel/src/sample-episode-visual.css +457 -0
  20. package/templates/cartoon-channel/src/sample-episode-visual.ts +185 -0
  21. package/templates/cartoon-channel/test-results/.last-run.json +4 -0
  22. package/templates/cartoon-channel/tests/concept-2-5d-animation.spec.ts +18 -0
  23. package/templates/cartoon-channel/tests/concept-2-5d.spec.ts +30 -0
  24. package/templates/cartoon-channel/tests/image-puppet-animation.spec.ts +18 -0
  25. package/templates/cartoon-channel/tests/image-puppet.spec.ts +29 -0
  26. package/templates/cartoon-channel/tests/puppet-2d-animation.spec.ts +18 -0
  27. package/templates/cartoon-channel/tests/puppet-2d.spec.ts +26 -0
  28. package/templates/cartoon-channel/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  29. package/templates/cartoon-channel/tests/sample-episode-visual.spec.ts +45 -0
  30. package/templates/cartoon-channel/tests/storyboard-playback.spec.ts +1 -1
  31. package/templates/cartoon-studio/README.md +36 -0
  32. package/templates/cartoon-studio/aura.assets.json +157 -0
  33. package/templates/cartoon-studio/package.json +12 -1
  34. package/templates/cartoon-studio/public/aura-assets/luma.047f5e5f.glb +0 -0
  35. package/templates/cartoon-studio/public/aura-assets/luma.humanoid-fixture.glb +0 -0
  36. package/templates/cartoon-studio/public/aura-assets/luma.thumb.svg +1 -0
  37. package/templates/cartoon-studio/public/aura-assets/miko.047f5e5f.glb +0 -0
  38. package/templates/cartoon-studio/public/aura-assets/miko.thumb.svg +1 -0
  39. package/templates/cartoon-studio/public/aura-assets/moonGarden.gltf +183 -0
  40. package/templates/cartoon-studio/scripts/episode.ts +276 -0
  41. package/templates/cartoon-studio/src/aura-assets.ts +55 -8
  42. package/templates/cartoon-studio/src/characters.ts +38 -1
  43. package/templates/cartoon-studio/src/episode-renderer.ts +605 -0
  44. package/templates/cartoon-studio/src/episode.ts +12 -6
  45. package/templates/cartoon-studio/src/main.ts +109 -41
  46. package/templates/cartoon-studio/src/render-plan.ts +162 -0
  47. package/templates/cartoon-studio/src/review.ts +99 -0
  48. package/templates/cartoon-studio/test-results/.last-run.json +4 -0
  49. package/templates/cartoon-studio/tests/episode-render.spec.ts +44 -0
  50. package/templates/cartoon-studio/tests/motion-quality.spec.ts +45 -0
  51. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +18 -10
  52. package/templates/cartoon-studio/tests/visual-review.spec.ts +21 -0
  53. package/templates/product-viewer/tests/screenshot.spec.ts +8 -16
@@ -5,6 +5,7 @@ import {
5
5
  createShotPlaybackPlan,
6
6
  effects,
7
7
  game,
8
+ group,
8
9
  installShotPlayback,
9
10
  labels,
10
11
  lights,
@@ -13,8 +14,7 @@ import {
13
14
  primitives,
14
15
  sampleAuraVoiceBridgeAtTime,
15
16
  sampleShotPlaybackPlan,
16
- scene,
17
- type AuraAssetRef
17
+ scene
18
18
  } from "@aura3d/engine";
19
19
  import { assets } from "./aura-assets";
20
20
  import {
@@ -23,8 +23,7 @@ import {
23
23
  missingCartoonCharacterAssets,
24
24
  publicCartoonAssetInstructions,
25
25
  requiredCartoonCharacterAssets,
26
- typedCartoonAssetSummary,
27
- type CartoonAssetKey
26
+ typedCartoonAssetSummary
28
27
  } from "./episode";
29
28
  import {
30
29
  accessibilityProofMetadata,
@@ -49,15 +48,31 @@ import { cartoonStudioSupport, installCartoonStudioPanel } from "./studio";
49
48
 
50
49
  declare global {
51
50
  interface Window {
52
- __AURA3D_CARTOON_TEMPLATE__?: {
51
+ __AURA3D_CARTOON_EPISODE_SEEK__?: (time: number) => void;
52
+ __AURA3D_CARTOON_EPISODE_PROOF__?: CartoonEpisodeRouteProof;
53
+ __AURA3D_CARTOON_TEMPLATE__?: CartoonEpisodeRouteProof;
54
+ }
55
+ }
56
+
57
+ interface CartoonEpisodeRouteProof {
58
+ readonly proofKind: "aura3d-cartoon-episode-proof";
53
59
  readonly contractId: string;
54
60
  readonly template: string;
55
61
  readonly storyBible: unknown;
62
+ readonly shots: readonly unknown[];
63
+ readonly captions: readonly unknown[];
64
+ readonly visemes: readonly unknown[];
65
+ readonly gestures: readonly unknown[];
66
+ readonly assets: unknown;
67
+ readonly renderStatus: unknown;
68
+ readonly errors: readonly string[];
69
+ readonly sourceOnlyAcceptedAsPublishProof: false;
56
70
  readonly shotIds: readonly string[];
57
71
  readonly captionIds: readonly string[];
58
72
  readonly playbackProbeTimes: readonly number[];
59
73
  readonly playbackProbeSamples: readonly unknown[];
60
74
  readonly renderQueueItems: number;
75
+ readonly renderPreset: unknown;
61
76
  readonly typedAssets: unknown;
62
77
  readonly requiredTypedAssets: readonly string[];
63
78
  readonly missingTypedAssets: readonly string[];
@@ -69,8 +84,6 @@ declare global {
69
84
  readonly sourceProofs: unknown;
70
85
  readonly sampleRenderSourceWorkflow: unknown;
71
86
  sampleAt(time: number): unknown;
72
- };
73
- }
74
87
  }
75
88
 
76
89
  const firstStoryboardShot = episode.storyboard.scenes[0]?.shots[0];
@@ -78,13 +91,21 @@ const firstCaption = episode.captionTrack.cues[0];
78
91
  const thumbnailCapture = renderOutputPackage.thumbnailCapture;
79
92
  const thumbnailCaption = captionCueAtTime(episode.captionTrack, thumbnailCapture.time);
80
93
  const storyBible = episode.storyBible;
81
- const typedCartoonAssets = assets as Partial<Record<CartoonAssetKey, AuraAssetRef<"model">>>;
94
+ const captureMode = new URLSearchParams(window.location.search).get("capture") === "thumbnail";
95
+ const cartoonRenderPreset = createCartoonStudioRouteRenderPreset({
96
+ name: "moon-garden-cartoon-studio",
97
+ resolution: episode.episodePlan.runtime.resolution,
98
+ materialStyle: { treatment: "cel" },
99
+ reducedMotion: episode.episodePlan.runtime.reducedMotion,
100
+ reducedFlash: true
101
+ });
82
102
 
83
103
  const captionOverlay = document.createElement("div");
84
104
  captionOverlay.id = "caption-overlay";
85
105
  captionOverlay.textContent = firstCaption?.text ?? firstStoryboardShot?.storyBeat ?? "Aura3D cartoon studio";
86
106
  captionOverlay.style.cssText = [
87
107
  "position:fixed",
108
+ captureMode ? "display:none" : "",
88
109
  "left:50%",
89
110
  "bottom:28px",
90
111
  "transform:translateX(-50%)",
@@ -121,14 +142,30 @@ document.body.dataset.cartoonShotCount = String(episode.shotTimeline.shots.lengt
121
142
  document.body.dataset.cartoonCaptionCount = String(episode.captionTrack.cues.length);
122
143
  document.body.dataset.cartoonStoryBibleProps = String(storyBible.props.length);
123
144
  document.body.dataset.cartoonStudioPanels = cartoonStudioSupport.panels.join(",");
124
- installCartoonStudioPanel(document.body);
145
+ if (!captureMode) installCartoonStudioPanel(document.body);
125
146
 
126
147
  const app = createAuraApp("#app", {
127
148
  scene: scene()
128
149
  .background("#081b2a")
129
- .add(createCharacterBody("miko", "Miko", "miko", [-0.8, 0.75, 0], [0.32, 0.42, 0.32], 0.72, "#7de2ff"))
150
+ .add(
151
+ model(assets.moonGarden, { name: "Moon Garden typed set asset" })
152
+ .position(0, 0, -0.02)
153
+ .scale(1)
154
+ .runtime(game.runtimeNode("moonGarden", { tags: ["set", "typed-asset", "moon-garden"] }))
155
+ )
156
+ .add(
157
+ model(assets.miko, { name: "Miko typed cartoon character" })
158
+ .position(-0.8, 0.75, 0)
159
+ .scale(0.72)
160
+ .runtime(game.runtimeNode("miko", { tags: ["character", "miko", "typed-asset"] }))
161
+ )
130
162
  .add(createMouthCard("miko:mouth", "Miko", [-0.8, 0.8, 0.32], "#f8fff2"))
131
- .add(createCharacterBody("luma", "Luma", "luma", [0.8, 0.72, 0], [0.3, 0.38, 0.3], 0.68, "#ffe18e"))
163
+ .add(
164
+ model(assets.luma, { name: "Luma typed cartoon character" })
165
+ .position(0.8, 0.72, 0)
166
+ .scale(0.68)
167
+ .runtime(game.runtimeNode("luma", { tags: ["character", "luma", "typed-asset"] }))
168
+ )
132
169
  .add(createMouthCard("luma:mouth", "Luma", [0.8, 0.77, 0.32], "#40ffbf"))
133
170
  .add(
134
171
  primitives
@@ -148,22 +185,40 @@ const app = createAuraApp("#app", {
148
185
  .position(-0.38, 0.2, 0.18)
149
186
  .scale([0.12, 0.24, 0.04])
150
187
  )
151
- .add(labels.hud(firstCaption?.text ?? firstStoryboardShot?.storyBeat ?? "Aura3D cartoon studio"))
188
+ .add(captureMode ? group("thumbnail capture label placeholder") : labels.hud(firstCaption?.text ?? firstStoryboardShot?.storyBeat ?? "Aura3D cartoon studio"))
152
189
  .add(lights.studio({ intensity: 1.2 }))
153
190
  .add(effects.bloom({ intensity: 0.18, color: "#7de2ff" }))
154
191
  .camera(camera.perspective({ position: [0, 1.4, 4.2], target: [0, 0.7, 0], fov: 42 })),
155
- diagnostics: true
192
+ diagnostics: !captureMode
156
193
  });
157
194
 
158
- window.__AURA3D_CARTOON_TEMPLATE__ = {
195
+ const cartoonEpisodeProof: CartoonEpisodeRouteProof = {
196
+ proofKind: "aura3d-cartoon-episode-proof",
159
197
  contractId: episodeContractId,
160
198
  template: "cartoon-studio",
161
199
  storyBible,
200
+ shots: episode.shotTimeline.shots,
201
+ captions: episode.captionTrack.cues,
202
+ visemes: visemeTrack.cues,
203
+ gestures: episode.performance.cues,
204
+ assets: typedCartoonAssetSummary,
205
+ renderStatus: {
206
+ route: renderPlan.route,
207
+ renderQueueItems: renderPlan.items.length,
208
+ outputs: renderPlan.outputs.map((output) => output.kind),
209
+ preset: cartoonRenderPreset,
210
+ sourceOnlyAcceptedAsPublishProof: false,
211
+ publishReadyFromCurrentEvidence: publishReadiness.ready,
212
+ issueCount: publishReadiness.issues.length
213
+ },
214
+ errors: bridgeIssues.map((issue) => String((issue as { message?: unknown }).message ?? issue)),
215
+ sourceOnlyAcceptedAsPublishProof: false,
162
216
  shotIds: episode.shotTimeline.shots.map((shot) => shot.shotId),
163
217
  captionIds: episode.captionTrack.cues.map((caption) => caption.captionId),
164
218
  playbackProbeTimes,
165
219
  playbackProbeSamples,
166
220
  renderQueueItems: renderPlan.items.length,
221
+ renderPreset: cartoonRenderPreset,
167
222
  typedAssets: typedCartoonAssetSummary,
168
223
  requiredTypedAssets: requiredCartoonCharacterAssets,
169
224
  missingTypedAssets: missingCartoonCharacterAssets,
@@ -182,6 +237,14 @@ window.__AURA3D_CARTOON_TEMPLATE__ = {
182
237
  return sampleCartoonRouteAt(time);
183
238
  }
184
239
  };
240
+ window.__AURA3D_CARTOON_EPISODE_PROOF__ = cartoonEpisodeProof;
241
+ window.__AURA3D_CARTOON_TEMPLATE__ = cartoonEpisodeProof;
242
+ window.__AURA3D_CARTOON_EPISODE_SEEK__ = (time: number) => {
243
+ const sample = sampleCartoonRouteAt(time);
244
+ captionOverlay.textContent = sample.captionText ?? firstCaption?.text ?? "";
245
+ captionOverlay.dataset.shotId = sample.shotId ?? "";
246
+ captionOverlay.dataset.captionId = sample.captionId ?? "";
247
+ };
185
248
 
186
249
  installShotPlayback(app, shotPlaybackPlan, {
187
250
  primitiveMouthNodeByCharacterId: {
@@ -195,33 +258,6 @@ installShotPlayback(app, shotPlaybackPlan, {
195
258
  }
196
259
  });
197
260
 
198
- function createCharacterBody(
199
- id: "miko" | "luma",
200
- name: string,
201
- assetKey: CartoonAssetKey,
202
- position: readonly [number, number, number],
203
- fallbackScale: readonly [number, number, number],
204
- modelScale: number,
205
- color: string
206
- ) {
207
- const asset = typedCartoonAssets[assetKey];
208
- const runtime = game.runtimeNode(id, { tags: ["character", id, asset ? "typed-asset" : "primitive-fallback"] });
209
- if (asset) {
210
- return model(asset, { name: `${name} typed character asset` })
211
- .position(position[0], position[1], position[2])
212
- .scale(modelScale)
213
- .runtime(runtime);
214
- }
215
- return primitives
216
- .sphere({
217
- name: `${name} primitive fallback body - add ${assetKey} with the Aura3D CLI`,
218
- material: material.pbr({ color, roughness: 0.54 })
219
- })
220
- .position(position[0], position[1], position[2])
221
- .scale(fallbackScale)
222
- .runtime(runtime);
223
- }
224
-
225
261
  function createMouthCard(id: "miko:mouth" | "luma:mouth", name: string, position: readonly [number, number, number], color: string) {
226
262
  return primitives
227
263
  .box({
@@ -233,6 +269,38 @@ function createMouthCard(id: "miko:mouth" | "luma:mouth", name: string, position
233
269
  .runtime(game.runtimeNode(id, { tags: ["mouth", "primitive", name.toLowerCase()] }));
234
270
  }
235
271
 
272
+ function createCartoonStudioRouteRenderPreset(input: {
273
+ readonly name: string;
274
+ readonly resolution: { readonly width: number; readonly height: number };
275
+ readonly materialStyle: { readonly treatment: "cel" | "preserve-pbr" };
276
+ readonly reducedMotion?: boolean | undefined;
277
+ readonly reducedFlash?: boolean | undefined;
278
+ }) {
279
+ return {
280
+ kind: "cartoon-render-preset-evidence",
281
+ name: input.name,
282
+ resolution: input.resolution,
283
+ lights: ["soft-key", "cool-rim", "set-fill", "emissive-practicals"],
284
+ shadows: { soft: true, contact: true },
285
+ postprocess: {
286
+ bloom: input.reducedFlash ? 0.08 : 0.18,
287
+ colorGrade: "storybook-night",
288
+ fogDepthCue: true
289
+ },
290
+ materialStyle: {
291
+ treatment: input.materialStyle.treatment,
292
+ outline: input.materialStyle.treatment === "cel",
293
+ rampSteps: input.materialStyle.treatment === "cel" ? 4 : 7,
294
+ saturation: 1.08,
295
+ blackLineWeight: input.materialStyle.treatment === "cel" ? 0.9 : 0
296
+ },
297
+ frameBudgetMs: 16.7,
298
+ debugOverlaysAllowedInExport: false,
299
+ reducedMotion: input.reducedMotion ?? false,
300
+ reducedFlash: input.reducedFlash ?? false
301
+ } as const;
302
+ }
303
+
236
304
  function sampleCartoonRouteAt(time: number) {
237
305
  const bridgeSample = sampleAuraVoiceBridgeAtTime(auraVoicePackage, time);
238
306
  const playbackSample = sampleShotPlaybackPlan(shotPlaybackPlan, time);
@@ -221,6 +221,87 @@ export const spanishDubMap = defineDubMap({
221
221
  generatedAt: episode.episodePlan.generatedAt
222
222
  });
223
223
 
224
+ const spanishLineTextBySourceText = new Map<string, string>([
225
+ ["Luma, the moon lilies are losing their sparkle.", "Luma, los lirios lunares estan perdiendo su brillo."],
226
+ ["Then we sweep softly and wake the glow stones.", "Entonces barremos con suavidad y despertamos las piedras brillantes."],
227
+ ["I will polish the blue stones one tiny circle at a time.", "Pulire las piedras azules con circulos pequenitos."],
228
+ ["And I will hum the garden's sleepy cleanup song.", "Y yo tarareare la cancion tranquila del jardin."],
229
+ ["The moon lilies are twinkling again.", "Los lirios lunares vuelven a centellear."],
230
+ ["Goodnight, little garden. Keep glowing.", "Buenas noches, jardin pequeno. Sigue brillando."]
231
+ ]);
232
+
233
+ function spanishTextForLine(text: string): string {
234
+ return spanishLineTextBySourceText.get(text) ?? text;
235
+ }
236
+
237
+ export const spanishDubCaptionMetadata = {
238
+ artifact: "alternate-language-caption-metadata",
239
+ schema: "aura3d-cartoon-studio-alternate-language-captions/v1",
240
+ contractId: renderContractId,
241
+ episodeId: episode.episodePlan.episodeId,
242
+ sourceLanguage: episode.dialogueTrack.language,
243
+ targetLanguage: spanishDubMap.targetLanguage,
244
+ cues: episode.captionTrack.cues.map((cue) => {
245
+ const sourceLine = episode.dialogueTrack.lines.find((line) => line.lineId === cue.lineId);
246
+ return {
247
+ captionId: cue.captionId,
248
+ lineId: cue.lineId,
249
+ shotId: cue.shotId,
250
+ sceneId: cue.sceneId,
251
+ speakerId: cue.speakerId,
252
+ sourceText: cue.text,
253
+ text: spanishTextForLine(sourceLine?.text ?? cue.text),
254
+ language: "es",
255
+ startTime: cue.startTime,
256
+ endTime: cue.endTime,
257
+ lineSafe: cue.lineSafe
258
+ };
259
+ }),
260
+ generatedAt: episode.episodePlan.generatedAt
261
+ } as const;
262
+
263
+ export const alternateLanguageDubMetadata = {
264
+ artifact: "cartoon-alternate-language-dub-proof",
265
+ schema: "aura3d-cartoon-studio-alternate-language-dub-proof/v1",
266
+ contractId: renderContractId,
267
+ episodeId: episode.episodePlan.episodeId,
268
+ sourceLanguage: episode.dialogueTrack.language,
269
+ alternateLanguageCount: 1,
270
+ alternateLanguages: [
271
+ {
272
+ language: spanishDubMap.targetLanguage,
273
+ label: "Spanish",
274
+ title: "Limpieza del Jardin Lunar",
275
+ dubMapArtifact: spanishDubMap.artifact,
276
+ lineCount: spanishDubMap.entries.length,
277
+ captionCueCount: spanishDubCaptionMetadata.cues.length,
278
+ captionMetadata: spanishDubCaptionMetadata,
279
+ audioStemSlots: episode.dialogueTrack.lines.map((line) => ({
280
+ id: `dub-slot:es:${line.lineId}`,
281
+ lineId: line.lineId,
282
+ speakerId: line.speakerId,
283
+ language: "es",
284
+ assetRequiredBeforePublish: true
285
+ })),
286
+ stableIds: {
287
+ shotIds: spanishDubMap.entries.every((entry) => entry.originalShotId === entry.dubbedShotId),
288
+ storyboardIds: spanishDubMap.entries.every((entry) => entry.originalStoryboardId === entry.dubbedStoryboardId),
289
+ sceneIds: spanishDubMap.entries.every((entry) => entry.originalSceneId === entry.dubbedSceneId),
290
+ lineIds: spanishDubMap.entries.every((entry) => entry.originalLineId === entry.dubbedLineId),
291
+ captionIds: spanishDubMap.entries.every((entry) => entry.originalCaptionId === entry.dubbedCaptionId),
292
+ characterIds: spanishDubMap.entries.every((entry) => entry.originalCharacterId === entry.dubbedCharacterId)
293
+ },
294
+ publishBoundary: "Metadata-only alternate-language proof. Add licensed/typed Spanish dialogue stems before claiming a rendered Spanish dub."
295
+ }
296
+ ],
297
+ assetPolicy: {
298
+ inventsAudioAssets: false,
299
+ inventsModelAssets: false,
300
+ typedAssetSource: "./src/aura-assets"
301
+ },
302
+ generatedAt: episode.episodePlan.generatedAt
303
+ } as const;
304
+
224
305
  export const phonemeVisemeDubSyncSourceProof = {
225
306
  contractId: renderContractId,
226
307
  sourceOnly: true,
@@ -255,6 +336,59 @@ export const phonemeVisemeDubSyncSourceProof = {
255
336
  executionBoundary: "Source harness only; actual phoneme, viseme, and dub playback proof requires rendered frame/video evidence."
256
337
  } as const;
257
338
 
339
+ export const cartoonStudioShowBibleBatchRenderPlan = {
340
+ artifact: "cartoon-show-bible-batch-render-plan",
341
+ schema: "aura3d-cartoon-studio-show-bible-batch-render-plan/v1",
342
+ contractId: renderContractId,
343
+ sourceStoryBible: {
344
+ episodeId: episode.storyBible.episodeId,
345
+ title: episode.storyBible.title,
346
+ characterIds: episode.storyBible.characters.map((character) => character.id),
347
+ locationIds: episode.storyBible.locations.map((location) => location.id),
348
+ propIds: episode.storyBible.props.map((prop) => prop.id),
349
+ continuityRules: episode.storyBible.continuityRules ?? []
350
+ },
351
+ renderJobs: [
352
+ {
353
+ id: "batch:moon-garden-cleanup-001",
354
+ episodeId: episode.episodePlan.episodeId,
355
+ title: episode.episodePlan.title,
356
+ sourceStoryBibleEpisodeId: episode.storyBible.episodeId,
357
+ route: renderPlan.route,
358
+ outputDirectory: "dist/episodes/moon-garden-001",
359
+ language: episode.episodePlan.language,
360
+ shotIds: episode.shotTimeline.shots.map((shot) => shot.shotId),
361
+ requiredTypedAssetKeys: typedCartoonAssetSummary.requiredCharacterAssets,
362
+ command: "npm run episode:render -- --episode moon-garden-cleanup-001"
363
+ },
364
+ {
365
+ id: "batch:moon-garden-glow-check-002",
366
+ episodeId: "moon-garden-glow-check-002",
367
+ title: "Moon Garden Glow Check",
368
+ sourceStoryBibleEpisodeId: episode.storyBible.episodeId,
369
+ route: renderPlan.route,
370
+ outputDirectory: "dist/episodes/moon-garden-002",
371
+ language: episode.episodePlan.language,
372
+ reusedCharacterIds: episode.storyBible.characters.map((character) => character.id),
373
+ reusedLocationIds: episode.storyBible.locations.map((location) => location.id),
374
+ reusedPropIds: episode.storyBible.props.map((prop) => prop.id),
375
+ requiredTypedAssetKeys: typedCartoonAssetSummary.requiredCharacterAssets,
376
+ command: "npm run episode:render -- --episode moon-garden-glow-check-002",
377
+ metadataOnly: true,
378
+ publishBoundary: "Second episode job reuses the same show bible and asset slots; rendered media is produced only when the template runner is executed with real typed assets."
379
+ }
380
+ ],
381
+ guarantees: {
382
+ oneShowBible: true,
383
+ multipleEpisodes: true,
384
+ noInventedAssets: true,
385
+ stableCharacterIds: true,
386
+ stableLocationIds: true,
387
+ stablePropIds: true
388
+ },
389
+ generatedAt: episode.episodePlan.generatedAt
390
+ } as const;
391
+
258
392
  export const performanceCoverage = createCartoonPerformanceCoverage(episode.performance, episode.dialogueTrack);
259
393
 
260
394
  export const reviewPackagePaths = renderOutputPackage.reviewPackagePaths;
@@ -365,6 +499,34 @@ export const sampleRenderSourceWorkflow = {
365
499
  ]
366
500
  } as const;
367
501
 
502
+ export const templateEpisodePackageWorkflow = {
503
+ contractId: renderContractId,
504
+ sourceOnly: false,
505
+ status: "template-package-runner-ready",
506
+ packageDirectory: "dist/episodes/moon-garden-001",
507
+ commands: [
508
+ "npm run episode:plan",
509
+ "npm run episode:preview",
510
+ "npm run episode:render",
511
+ "npm run episode:package",
512
+ "npm run episode:review",
513
+ "npm run episode:verify"
514
+ ],
515
+ requiredArtifacts: [
516
+ "thumbnail.png",
517
+ "captions.vtt",
518
+ "captions.srt",
519
+ "metadata.json",
520
+ "route-proof.json",
521
+ "asset-provenance.json",
522
+ "render-manifest.json",
523
+ "visual-acceptance.json",
524
+ "review-package.md"
525
+ ],
526
+ renderOutputMode: "episode.webm-or-png-sequence-fallback-marker",
527
+ publishBoundary: "PNG sequence fallback proves template packaging only; publish-ready claims require a real encoded WebM or MP4 plus human review."
528
+ } as const;
529
+
368
530
  export const auraVoicePackage = createAuraVoiceBridgePackage({
369
531
  episodePlan: episode.episodePlan,
370
532
  storyboard: episode.storyboard,
@@ -0,0 +1,99 @@
1
+ import { captionTimingProof, renderOutputPackage, visemeFrameSyncSourceProof } from "./render-plan";
2
+ import { episode, missingCartoonCharacterAssets, publicCartoonAssetInstructions, typedCartoonAssetSummary, youtubeDraftMetadata } from "./episode";
3
+
4
+ export const cartoonEpisodePackageDirectory = "dist/episodes/moon-garden-001" as const;
5
+
6
+ export const requiredCartoonEpisodePackageFiles = [
7
+ "episode.webm",
8
+ "thumbnail.png",
9
+ "captions.vtt",
10
+ "captions.srt",
11
+ "metadata.json",
12
+ "prompt-animation-evidence.json",
13
+ "route-proof.json",
14
+ "asset-provenance.json",
15
+ "render-manifest.json",
16
+ "dub-metadata.json",
17
+ "batch-render-plan.json",
18
+ "motion-quality.json",
19
+ "visual-acceptance.json",
20
+ "review-package.md"
21
+ ] as const;
22
+
23
+ export const optionalCartoonEpisodePackageFiles = [
24
+ "episode.mp4",
25
+ "episode.png-sequence-fallback.json"
26
+ ] as const;
27
+
28
+ export interface CartoonEpisodeReviewInput {
29
+ readonly packageDirectory?: string;
30
+ readonly hasWebm: boolean;
31
+ readonly hasPngSequenceFallback: boolean;
32
+ readonly generatedAt: string;
33
+ }
34
+
35
+ export function createCartoonEpisodeReviewPackage(input: CartoonEpisodeReviewInput): string {
36
+ const packageDirectory = input.packageDirectory ?? cartoonEpisodePackageDirectory;
37
+ const outputMode = input.hasWebm ? "encoded-webm" : input.hasPngSequenceFallback ? "png-sequence-fallback" : "missing-render-output";
38
+ const typedAssetStatus = missingCartoonCharacterAssets.length === 0 ? "typed character assets present" : "starter fallback mode";
39
+ const reviewStatus = input.hasWebm && missingCartoonCharacterAssets.length === 0 ? "ready-for-human-review" : "needs-render-upgrade-before-publish";
40
+
41
+ return [
42
+ "# Moon Garden 001 Review Package",
43
+ "",
44
+ `Generated: ${input.generatedAt}`,
45
+ `Episode: ${episode.episodePlan.title}`,
46
+ `Package: \`${packageDirectory}\``,
47
+ `Output mode: \`${outputMode}\``,
48
+ `Review status: \`${reviewStatus}\``,
49
+ "",
50
+ "## Required Files",
51
+ "",
52
+ ...requiredCartoonEpisodePackageFiles.map((file) => `- [x] \`${file}\``),
53
+ input.hasPngSequenceFallback ? "- [x] `episode.png-sequence-fallback.json`" : "- [ ] `episode.png-sequence-fallback.json`",
54
+ "",
55
+ "## Episode Summary",
56
+ "",
57
+ `- Runtime: ${episode.episodePlan.runtime.duration}s at ${episode.episodePlan.runtime.frameRate}fps`,
58
+ `- Resolution: ${episode.episodePlan.runtime.resolution.width}x${episode.episodePlan.runtime.resolution.height}`,
59
+ `- Shots: ${episode.shotTimeline.shots.length}`,
60
+ `- Captions: ${episode.captionTrack.cues.length}`,
61
+ `- Characters: ${episode.episodePlan.characters.map((character) => character.name).join(", ")}`,
62
+ `- YouTube draft title: ${youtubeDraftMetadata.title}`,
63
+ "",
64
+ "## Asset Status",
65
+ "",
66
+ `- Status: ${typedAssetStatus}`,
67
+ `- Required typed assets: ${typedCartoonAssetSummary.requiredCharacterAssets.join(", ")}`,
68
+ `- Missing typed assets: ${missingCartoonCharacterAssets.length ? missingCartoonCharacterAssets.join(", ") : "none"}`,
69
+ "",
70
+ "Typed asset commands:",
71
+ "",
72
+ "```bash",
73
+ ...publicCartoonAssetInstructions,
74
+ "```",
75
+ "",
76
+ "## Timing Proof",
77
+ "",
78
+ `- Caption timing: ${captionTimingProof.status}`,
79
+ `- Max caption drift frames: ${captionTimingProof.maxDriftFrames}`,
80
+ `- Mouth source sync: ${visemeFrameSyncSourceProof.mouthMovementWithinOneFrame ? "pass" : "fail"}`,
81
+ "",
82
+ "## Human Review Checklist",
83
+ "",
84
+ "- [ ] Both characters are readable in representative frames.",
85
+ "- [ ] Captions do not cover important action.",
86
+ "- [ ] Mouth movement is visible during dialogue in the browser route.",
87
+ "- [ ] Character body motion is more than still-image wobble.",
88
+ "- [ ] No route chrome, proof panels, or debug overlays appear in encoded output.",
89
+ "- [ ] Reviewer name and approval date are recorded before publish-ready claims.",
90
+ "",
91
+ "## Boundary",
92
+ "",
93
+ input.hasWebm
94
+ ? "This package includes an encoded WebM output. Human review and asset readiness are still required before public publish-ready claims."
95
+ : "This package uses the PNG-sequence fallback marker because no real browser video encoder was attached. It is useful for package workflow proof, but it is not publish-ready video evidence.",
96
+ "",
97
+ `Render package metadata source: \`${renderOutputPackage.packageId}\``
98
+ ].join("\n");
99
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }
@@ -0,0 +1,44 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import {
3
+ createCartoonEpisodePackage,
4
+ createCartoonRenderManifestArtifact,
5
+ createRenderedPromptAnimationEvidenceArtifact
6
+ } from "../src/episode-renderer";
7
+ import { requiredCartoonEpisodePackageFiles } from "../src/review";
8
+
9
+ test("episode renderer package declares encoded video and required package files", () => {
10
+ const pkg = createCartoonEpisodePackage("package");
11
+ const filePaths = new Set(pkg.files.map((file) => file.path));
12
+ const manifest = createCartoonRenderManifestArtifact();
13
+ const evidence = createRenderedPromptAnimationEvidenceArtifact();
14
+
15
+ expect(pkg.packageDirectory).toBe("dist/episodes/moon-garden-001");
16
+ expect(pkg.hasWebm).toBe(true);
17
+ expect(pkg.hasPngSequenceFallback).toBe(false);
18
+ expect(pkg.requiredFiles).toEqual([...requiredCartoonEpisodePackageFiles]);
19
+ expect([...requiredCartoonEpisodePackageFiles]
20
+ .filter((file) => file !== "episode.webm" && file !== "thumbnail.png")
21
+ .every((file) => filePaths.has(file))).toBe(true);
22
+ expect(filePaths.has("episode.webm")).toBe(false);
23
+ expect(filePaths.has("thumbnail.png")).toBe(false);
24
+ expect(pkg.files.filter((file) => /^frames\/frame-\d+\.svg$/.test(file.path)).length).toBeGreaterThan(20);
25
+
26
+ expect(manifest).toMatchObject({
27
+ artifact: "cartoon-render-manifest",
28
+ outputMode: "encoded-webm",
29
+ hasEncodedVideo: true,
30
+ encodedVideo: {
31
+ path: "episode.webm",
32
+ codec: "vp9"
33
+ }
34
+ });
35
+ expect(evidence).toMatchObject({
36
+ artifact: "prompt-animation-evidence",
37
+ ok: true,
38
+ renderOutput: {
39
+ mode: "encoded-webm",
40
+ encodedVideoPresent: true,
41
+ pngSequenceFallbackPresent: false
42
+ }
43
+ });
44
+ });
@@ -0,0 +1,45 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { createCartoonMotionQualityArtifact, createCartoonVisualAcceptanceArtifact } from "../src/episode-renderer";
3
+
4
+ test("motion quality rejects still-image wobble and requires regional character motion", () => {
5
+ const motion = createCartoonMotionQualityArtifact();
6
+
7
+ expect(motion).toMatchObject({
8
+ artifact: "cartoon-motion-quality",
9
+ videoPath: "episode.webm",
10
+ globalOnlyMotion: false,
11
+ flatLayerMotion: false,
12
+ cameraMotionDeclared: true,
13
+ rejectedEvidence: {
14
+ stillImageWobbleAccepted: false,
15
+ subtitleOverStillAccepted: false,
16
+ sourceOnlyAccepted: false
17
+ }
18
+ });
19
+ expect(motion.independentRegionMotionSegments).toBeGreaterThanOrEqual(2);
20
+ expect(motion.characterRegionMotionSegments).toBeGreaterThanOrEqual(2);
21
+ expect(motion.bodyRegionMotionSegments).toBeGreaterThanOrEqual(2);
22
+ expect(motion.mouthMotionSegments).toBeGreaterThanOrEqual(2);
23
+ expect(motion.regions.some((region) => region.kind === "mouth" && region.movesDuringDialogue)).toBe(true);
24
+ expect(motion.regions.some((region) => region.kind === "limb" && region.movesDuringAction)).toBe(true);
25
+ });
26
+
27
+ test("visual acceptance requires both characters and refuses source-only publish evidence", () => {
28
+ const visual = createCartoonVisualAcceptanceArtifact();
29
+
30
+ expect(visual).toMatchObject({
31
+ artifact: "cartoon-visual-acceptance",
32
+ ok: true,
33
+ visualOk: true,
34
+ encodedVideoPresent: true,
35
+ notTrue3DAccepted: false,
36
+ sourceOnlyAcceptedAsPublishProof: false,
37
+ requiredHumanReview: true,
38
+ visibleCharacterCount: 2,
39
+ globalOnlyMotion: false,
40
+ flatLayerMotion: false,
41
+ reviewerRequiredBeforePublish: true
42
+ });
43
+ expect(visual.frameHashChanges).toBeGreaterThan(1);
44
+ expect(visual.motionSegments.every((segment) => segment.independentRegionMotion)).toBe(true);
45
+ });