create-aura3d 1.1.2 → 1.3.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 (215) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -3
  4. package/dist/index.js.map +1 -1
  5. package/package.json +11 -9
  6. package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
  7. package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
  8. package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
  9. package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
  10. package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
  11. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
  12. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
  13. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
  14. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
  15. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
  16. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
  17. package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
  18. package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
  19. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
  20. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
  21. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
  22. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
  23. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
  24. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
  25. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
  26. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
  27. package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
  28. package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
  29. package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
  30. package/templates/animation-studio/AGENTS.md +71 -0
  31. package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
  32. package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
  33. package/templates/animation-studio/aura.assets.json +669 -0
  34. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  35. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  36. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  37. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  38. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  39. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  40. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  41. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
  42. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  43. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  44. package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
  45. package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
  46. package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
  47. package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
  48. package/templates/animation-studio/dist/scene/integ.document.json +1 -0
  49. package/templates/animation-studio/dist/scene/working.document.json +723 -0
  50. package/templates/animation-studio/dist/scene/working.history.json +1 -0
  51. package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
  52. package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
  53. package/templates/animation-studio/package.json +32 -0
  54. package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
  55. package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
  56. package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
  57. package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
  58. package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
  59. package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
  60. package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
  61. package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
  62. package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
  63. package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
  64. package/templates/animation-studio/public/clip-library/idle.json +23035 -0
  65. package/templates/animation-studio/public/clip-library/manifest.json +154 -0
  66. package/templates/animation-studio/public/clip-library/nod.json +173903 -0
  67. package/templates/animation-studio/public/clip-library/point.json +69879 -0
  68. package/templates/animation-studio/public/clip-library/react.json +66084 -0
  69. package/templates/animation-studio/public/clip-library/run.json +9648 -0
  70. package/templates/animation-studio/public/clip-library/talk.json +69879 -0
  71. package/templates/animation-studio/public/clip-library/walk.json +8283 -0
  72. package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
  73. package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
  74. package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
  75. package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
  76. package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
  77. package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
  78. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
  79. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
  80. package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
  81. package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
  82. package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
  83. package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
  84. package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
  85. package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
  86. package/templates/animation-studio/scripts/anim.ts +103 -0
  87. package/templates/animation-studio/scripts/animation-scene.ts +457 -0
  88. package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
  89. package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
  90. package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
  91. package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
  92. package/templates/animation-studio/scripts/build-characters.ts +1543 -0
  93. package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
  94. package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
  95. package/templates/animation-studio/scripts/determinism-check.ts +48 -0
  96. package/templates/animation-studio/scripts/generate-scene.ts +95 -0
  97. package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
  98. package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
  99. package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
  100. package/templates/animation-studio/scripts/preview-server.ts +52 -0
  101. package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
  102. package/templates/animation-studio/scripts/render-core.ts +360 -0
  103. package/templates/animation-studio/scripts/render-live.ts +813 -0
  104. package/templates/animation-studio/scripts/render-parallel.ts +70 -0
  105. package/templates/animation-studio/scripts/render-server.ts +119 -0
  106. package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
  107. package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
  108. package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
  109. package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
  110. package/templates/animation-studio/scripts/texture-bake.ts +297 -0
  111. package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
  112. package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
  113. package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
  114. package/templates/animation-studio/src/animation-performance.ts +556 -0
  115. package/templates/animation-studio/src/aura-assets.ts +71 -0
  116. package/templates/animation-studio/src/character.ts +40 -0
  117. package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
  118. package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
  119. package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
  120. package/templates/animation-studio/src/empty-document.ts +77 -0
  121. package/templates/animation-studio/src/episode-document.ts +442 -0
  122. package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
  123. package/templates/animation-studio/src/fidelity.ts +129 -0
  124. package/templates/animation-studio/src/graph.ts +14 -0
  125. package/templates/animation-studio/src/procedural-hdri.ts +243 -0
  126. package/templates/animation-studio/src/profile.ts +97 -0
  127. package/templates/animation-studio/src/render-live-route.ts +42 -0
  128. package/templates/animation-studio/src/render-modes.ts +137 -0
  129. package/templates/animation-studio/src/scene-player.ts +867 -0
  130. package/templates/animation-studio/src/set-templates.ts +361 -0
  131. package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
  132. package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
  133. package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
  134. package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
  135. package/templates/animation-studio/tsconfig.json +26 -0
  136. package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
  137. package/templates/character-controller/README.md +33 -0
  138. package/templates/character-controller/index.html +19 -0
  139. package/templates/character-controller/package.json +23 -0
  140. package/templates/character-controller/playwright.config.ts +14 -0
  141. package/templates/character-controller/src/controller.ts +44 -0
  142. package/templates/character-controller/src/main.ts +62 -0
  143. package/templates/character-controller/tests/route-health.spec.ts +26 -0
  144. package/templates/character-controller/tests/screenshot.spec.ts +9 -0
  145. package/templates/character-controller/tsconfig.json +15 -0
  146. package/templates/cinematic-scene/package.json +1 -1
  147. package/templates/episode-builder/README.md +4 -4
  148. package/templates/episode-builder/package.json +1 -1
  149. package/templates/episode-builder/src/aura-assets.ts +1 -1
  150. package/templates/episode-builder/src/builder.ts +4 -4
  151. package/templates/episode-builder/src/episode.ts +21 -21
  152. package/templates/episode-builder/src/main.ts +20 -20
  153. package/templates/episode-builder/src/render-plan.ts +13 -13
  154. package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
  155. package/templates/fighting-game/package.json +1 -1
  156. package/templates/mini-game/package.json +1 -1
  157. package/templates/product-viewer/package.json +1 -1
  158. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
  159. package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
  160. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
  161. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
  162. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
  163. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
  164. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
  165. package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
  166. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
  167. package/templates/cartoon-studio/aura.assets.json +0 -400
  168. package/templates/cartoon-studio/package.json +0 -33
  169. package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
  170. package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
  171. package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
  172. package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
  173. package/templates/cartoon-studio/scripts/episode.ts +0 -323
  174. package/templates/cartoon-studio/scripts/render-live.ts +0 -873
  175. package/templates/cartoon-studio/src/aura-assets.ts +0 -64
  176. package/templates/cartoon-studio/src/characters.ts +0 -65
  177. package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
  178. package/templates/cartoon-studio/src/episode.ts +0 -306
  179. package/templates/cartoon-studio/src/main.ts +0 -400
  180. package/templates/cartoon-studio/src/render-live-route.ts +0 -913
  181. package/templates/cartoon-studio/src/render-plan.ts +0 -567
  182. package/templates/cartoon-studio/src/review.ts +0 -99
  183. package/templates/cartoon-studio/src/studio.ts +0 -101
  184. package/templates/cartoon-studio/test-results/.last-run.json +0 -4
  185. package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
  186. package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
  187. package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
  188. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
  189. package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
  190. package/templates/cartoon-studio/tsconfig.json +0 -14
  191. package/templates/prompt-cartoon-channel/index.html +0 -13
  192. package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
  193. /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
  194. /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
  195. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
  196. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
  197. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
  198. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
  199. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
  200. /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
  201. /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
  202. /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
  203. /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
  204. /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
  205. /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  206. /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
  207. /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
  208. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
  209. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
  210. /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
  211. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
  212. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
  213. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
  214. /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
  215. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
@@ -1,262 +0,0 @@
1
- /**
2
- * build-dialogue-audio.ts — REAL synthesized dialogue audio for the Cartoon Studio
3
- * episode using the macOS built-in `say` TTS engine.
4
- *
5
- * HONESTY: `say` is a real, on-device text-to-speech synthesizer (this is a darwin
6
- * machine). The spoken bytes below are GENUINELY synthesized from the episode's
7
- * dialogue lines — not silence, not a placeholder tone. They ARE, however, robotic
8
- * "system voice" quality (placeholder-grade VO), NOT studio voice acting. We label
9
- * the result `voiceSource: "macos-say-tts"` so nobody mistakes it for finished VO.
10
- *
11
- * What this does, per dialogue line (read from the compiled episode dialogueTrack):
12
- * 1. `say -v <voice> -o line.aiff "<text>"` — a distinct voice per character
13
- * (miko = a higher/youthful voice, luma = a different adult voice).
14
- * 2. Place each line's audio at its real dialogue `startTime` on an episode-length
15
- * timeline via ffmpeg `adelay` (+ `apad` to the full episode duration).
16
- * 3. Mix all delayed lines together with a faint, steady (non-flashing) ambient
17
- * bed underneath, producing one episode-length dialogue track.
18
- *
19
- * GRACEFUL DEGRADE: if `say` is unavailable (non-mac / CI), this returns
20
- * `{ available: false }` and the caller falls back to the placeholder ambient bed.
21
- *
22
- * This module is imported by `render-live.ts`; it can also be run standalone
23
- * (`tsx scripts/build-dialogue-audio.ts`) to (re)build just the audio track.
24
- */
25
-
26
- import { spawnSync } from "node:child_process";
27
- import { createRequire } from "node:module";
28
- import { existsSync, mkdirSync, rmSync } from "node:fs";
29
- import { dirname, resolve } from "node:path";
30
- import { fileURLToPath } from "node:url";
31
- import { episode } from "../src/episode.js";
32
-
33
- const __dirname = dirname(fileURLToPath(import.meta.url));
34
- const TEMPLATE_ROOT = resolve(__dirname, "..");
35
-
36
- /** Distinct macOS `say` voices per character. Picked from `say -v '?'` (en_US):
37
- * - miko: "Junior" — a younger, higher system voice (matches the hero kid robot).
38
- * - luma: "Samantha" — a clearer adult system voice (the helper robot).
39
- * Both are standard en_US voices present on a default macOS install. */
40
- export const DIALOGUE_VOICE_BY_SPEAKER: Record<string, string> = {
41
- miko: "Junior",
42
- luma: "Samantha"
43
- };
44
- const FALLBACK_VOICE = "Samantha";
45
-
46
- export interface DialogueLineAudio {
47
- readonly lineId: string;
48
- readonly speakerId: string;
49
- readonly voice: string;
50
- readonly text: string;
51
- readonly startTime: number;
52
- readonly endTime: number;
53
- /** Actual synthesized clip duration (seconds), measured from the rendered file. */
54
- readonly spokenDuration: number;
55
- }
56
-
57
- export interface DialogueAudioResult {
58
- readonly available: boolean;
59
- /** Absolute path to the assembled episode-length dialogue track (WAV), if built. */
60
- readonly trackPath?: string;
61
- readonly durationSeconds: number;
62
- readonly sampleRate: number;
63
- readonly voiceSource: "macos-say-tts";
64
- readonly voices: Record<string, string>;
65
- readonly lines: DialogueLineAudio[];
66
- readonly note: string;
67
- }
68
-
69
- const SAMPLE_RATE = 48_000;
70
-
71
- function resolveFfmpeg(): string {
72
- const require = createRequire(import.meta.url);
73
- try {
74
- const installer = require("@ffmpeg-installer/ffmpeg") as { path?: string };
75
- if (installer.path && existsSync(installer.path)) return installer.path;
76
- } catch {
77
- /* fall through */
78
- }
79
- try {
80
- const ffmpegStatic = require("ffmpeg-static") as string | { default?: string };
81
- const p = typeof ffmpegStatic === "string" ? ffmpegStatic : ffmpegStatic.default;
82
- if (p && existsSync(p)) return p;
83
- } catch {
84
- /* fall through */
85
- }
86
- return "ffmpeg";
87
- }
88
-
89
- function resolveFfprobe(): string {
90
- return "ffprobe";
91
- }
92
-
93
- /** Is the macOS `say` TTS binary usable on this host? */
94
- export function isSayAvailable(): boolean {
95
- if (process.platform !== "darwin") return false;
96
- const probe = spawnSync("say", ["-v", "?"], { encoding: "utf8" });
97
- return probe.status === 0 && (probe.stdout ?? "").length > 0;
98
- }
99
-
100
- function probeDuration(ffprobe: string, file: string): number {
101
- const run = spawnSync(
102
- ffprobe,
103
- ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file],
104
- { encoding: "utf8" }
105
- );
106
- const d = Number((run.stdout ?? "").trim());
107
- return Number.isFinite(d) ? d : 0;
108
- }
109
-
110
- /**
111
- * Synthesize every dialogue line with `say` and assemble one episode-length WAV with
112
- * each line placed at its real `startTime`, plus a faint ambient bed underneath.
113
- * Returns `{ available: false }` (no throw) when `say` is missing so callers degrade.
114
- */
115
- export function buildDialogueAudioTrack(outputDir: string): DialogueAudioResult {
116
- const voices = DIALOGUE_VOICE_BY_SPEAKER;
117
- const durationSeconds = episode.dialogueTrack.duration;
118
- const trackLines = episode.dialogueTrack.lines;
119
-
120
- if (!isSayAvailable()) {
121
- return {
122
- available: false,
123
- durationSeconds,
124
- sampleRate: SAMPLE_RATE,
125
- voiceSource: "macos-say-tts",
126
- voices,
127
- lines: [],
128
- note: "macOS `say` TTS not available on this host; caller should fall back to placeholder ambient."
129
- };
130
- }
131
-
132
- const ffmpeg = resolveFfmpeg();
133
- const ffprobe = resolveFfprobe();
134
- const workDir = resolve(outputDir, "dialogue-audio");
135
- mkdirSync(workDir, { recursive: true });
136
-
137
- const lineResults: DialogueLineAudio[] = [];
138
- const wavSegments: string[] = [];
139
-
140
- for (const line of trackLines) {
141
- const voice = voices[line.speakerId] ?? FALLBACK_VOICE;
142
- const aiff = resolve(workDir, `${line.lineId.replace(/[^a-zA-Z0-9_-]/g, "_")}.aiff`);
143
- const wav = resolve(workDir, `${line.lineId.replace(/[^a-zA-Z0-9_-]/g, "_")}.wav`);
144
-
145
- // 1. REAL TTS: synthesize the line to AIFF with the character's distinct voice.
146
- const say = spawnSync("say", ["-v", voice, "-o", aiff, line.text], { encoding: "utf8" });
147
- if (say.status !== 0 || !existsSync(aiff)) {
148
- throw new Error(`say failed for ${line.lineId} (voice=${voice}): ${(say.stderr ?? "").slice(-300)}`);
149
- }
150
- const spokenDuration = probeDuration(ffprobe, aiff);
151
-
152
- // 2. Normalize each clip to a stereo 48k WAV at the episode sample rate, gained up
153
- // a touch so dialogue sits clearly above the ambient bed.
154
- const toWav = spawnSync(
155
- ffmpeg,
156
- [
157
- "-y",
158
- "-i", aiff,
159
- "-ar", String(SAMPLE_RATE),
160
- "-ac", "2",
161
- "-af", "volume=1.6,aformat=sample_fmts=s16:channel_layouts=stereo",
162
- wav
163
- ],
164
- { encoding: "utf8" }
165
- );
166
- if (toWav.status !== 0 || !existsSync(wav)) {
167
- throw new Error(`ffmpeg AIFF->WAV failed for ${line.lineId}: ${(toWav.stderr ?? "").slice(-300)}`);
168
- }
169
-
170
- wavSegments.push(wav);
171
- lineResults.push({
172
- lineId: line.lineId,
173
- speakerId: line.speakerId,
174
- voice,
175
- text: line.text,
176
- startTime: line.startTime,
177
- endTime: line.endTime,
178
- spokenDuration
179
- });
180
- }
181
-
182
- // 3. Build one episode-length mix: each line delayed to its startTime (adelay, ms),
183
- // padded to the full episode duration (apad), then amix'd with a faint, STEADY
184
- // ambient bed (two low sines + low pink noise, volume far below dialogue). The bed
185
- // is non-tremolo so it stays reduced-flash / sensory-safe.
186
- const trackPath = resolve(outputDir, "episode-dialogue.wav");
187
- const inputs: string[] = [];
188
- const filterParts: string[] = [];
189
- const mixLabels: string[] = [];
190
-
191
- wavSegments.forEach((seg, i) => {
192
- inputs.push("-i", seg);
193
- const delayMs = Math.round(lineResults[i]!.startTime * 1000);
194
- // adelay both channels; apad to full length so amix keeps the full timeline.
195
- filterParts.push(
196
- `[${i}:a]adelay=${delayMs}|${delayMs},apad=whole_dur=${durationSeconds},aformat=sample_fmts=fltp:channel_layouts=stereo[d${i}]`
197
- );
198
- mixLabels.push(`[d${i}]`);
199
- });
200
-
201
- // Faint steady ambient bed generated inline (no extra input files).
202
- const bedIndex = wavSegments.length;
203
- filterParts.push(
204
- `sine=frequency=174:sample_rate=${SAMPLE_RATE}:duration=${durationSeconds}[bedA]`,
205
- `sine=frequency=220:sample_rate=${SAMPLE_RATE}:duration=${durationSeconds}[bedB]`,
206
- `anoisesrc=color=pink:sample_rate=${SAMPLE_RATE}:amplitude=0.03:duration=${durationSeconds}[bedN]`,
207
- `[bedA][bedB]amix=inputs=2:weights=0.6 0.4[bedTones]`,
208
- `[bedTones][bedN]amix=inputs=2:weights=0.85 0.15,volume=0.05,aformat=sample_fmts=fltp:channel_layouts=stereo[bed]`
209
- );
210
- mixLabels.push("[bed]");
211
-
212
- // Dialogue lines at full weight, ambient bed faint underneath. normalize=0 keeps
213
- // dialogue from being attenuated when a line overlaps the bed.
214
- const mixCount = mixLabels.length;
215
- const dialogueWeights = lineResults.map(() => "1").join(" ");
216
- const filter =
217
- filterParts.join(";") +
218
- `;${mixLabels.join("")}amix=inputs=${mixCount}:weights=${dialogueWeights} 0.4:normalize=0:duration=longest,` +
219
- `volume=1.0,aformat=sample_fmts=s16:channel_layouts=stereo[out]`;
220
-
221
- const assemble = spawnSync(
222
- ffmpeg,
223
- [
224
- "-y",
225
- ...inputs,
226
- "-filter_complex", filter,
227
- "-map", "[out]",
228
- "-ar", String(SAMPLE_RATE),
229
- "-t", durationSeconds.toFixed(3),
230
- trackPath
231
- ],
232
- { encoding: "utf8" }
233
- );
234
- if (assemble.status !== 0 || !existsSync(trackPath)) {
235
- throw new Error(`ffmpeg dialogue assembly failed (status=${assemble.status}): ${(assemble.stderr ?? "").slice(-600)}`);
236
- }
237
-
238
- // Clean up per-line temp files; keep the assembled track.
239
- rmSync(workDir, { recursive: true, force: true });
240
-
241
- return {
242
- available: true,
243
- trackPath,
244
- durationSeconds,
245
- sampleRate: SAMPLE_RATE,
246
- voiceSource: "macos-say-tts",
247
- voices,
248
- lines: lineResults,
249
- note:
250
- "REAL synthesized dialogue via macOS `say` TTS, one distinct voice per character, " +
251
- "each line placed at its episode dialogue startTime over a faint steady ambient bed. " +
252
- "Honest caveat: `say` is robotic system-voice (placeholder-grade VO), not studio voice acting."
253
- };
254
- }
255
-
256
- // Allow standalone invocation: `tsx scripts/build-dialogue-audio.ts [outDir]`.
257
- if (import.meta.url === `file://${process.argv[1]}`) {
258
- const outDir = resolve(process.argv[2] ?? resolve(TEMPLATE_ROOT, "dist/episodes/live-3d"));
259
- mkdirSync(outDir, { recursive: true });
260
- const result = buildDialogueAudioTrack(outDir);
261
- console.log(JSON.stringify(result, null, 2));
262
- }
@@ -1,323 +0,0 @@
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")) {
62
- await renderEpisodeMedia(pkg);
63
- await verifyEncodedOutputs(pkg);
64
- }
65
- if (!options.only && mode === "package") await captureRouteThumbnail(pkg);
66
- if (!options.only) await writeChecksumManifest(pkg);
67
- console.log(JSON.stringify({
68
- ok: true,
69
- command: mode,
70
- packageDirectory: pkg.packageDirectory,
71
- hasWebm: pkg.hasWebm,
72
- hasPngSequenceFallback: pkg.hasPngSequenceFallback,
73
- publishReady: pkg.publishReady,
74
- fileCount: options.only ? options.only.size : pkg.files.length
75
- }, null, 2));
76
- }
77
-
78
- async function writeChecksumManifest(pkg: CartoonEpisodePackageBuild) {
79
- const artifactEntries = [];
80
- const packageArtifacts = [
81
- ...pkg.files.map((file) => ({ path: file.path, kind: file.kind })),
82
- { path: "episode.webm", kind: "video/webm" },
83
- { path: "thumbnail.png", kind: "png" },
84
- { path: "frames/first.png", kind: "png" },
85
- { path: "frames/dialogue.png", kind: "png" },
86
- { path: "frames/action.png", kind: "png" },
87
- { path: "frames/final.png", kind: "png" }
88
- ];
89
- for (const file of packageArtifacts) {
90
- const target = path.join(pkg.packageDirectory, file.path);
91
- if (!existsSync(target)) continue;
92
- const bytes = await readFile(target);
93
- artifactEntries.push({
94
- path: file.path,
95
- kind: file.kind,
96
- byteSize: bytes.length,
97
- sha256: createHash("sha256").update(bytes).digest("hex")
98
- });
99
- }
100
- const manifestPath = path.join(pkg.packageDirectory, "render-manifest.json");
101
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
102
- manifest.artifacts = artifactEntries;
103
- manifest.requiredFiles = pkg.requiredFiles;
104
- await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
105
- }
106
-
107
- // After encoding, rewrite the encoded-video claims in the package JSON so they are
108
- // VERIFIED facts (true only when episode.webm actually exists at a real size) instead
109
- // of the hardcoded `true` the artifact builder emits before encoding runs.
110
- async function verifyEncodedOutputs(pkg: CartoonEpisodePackageBuild) {
111
- const webmPath = path.join(pkg.packageDirectory, "episode.webm");
112
- const webmExists = existsSync(webmPath);
113
- const webmBytes = webmExists ? (await stat(webmPath)).size : 0;
114
- const realWebm = webmExists && webmBytes > 32_768;
115
-
116
- await patchPackageJson(path.join(pkg.packageDirectory, "render-manifest.json"), (m) => {
117
- m.hasEncodedVideo = realWebm;
118
- if (m.encodedVideo && typeof m.encodedVideo === "object") {
119
- (m.encodedVideo as Record<string, unknown>).verified = realWebm;
120
- (m.encodedVideo as Record<string, unknown>).byteLength = webmBytes;
121
- }
122
- });
123
- await patchPackageJson(path.join(pkg.packageDirectory, "visual-acceptance.json"), (m) => {
124
- m.encodedVideoPresent = realWebm;
125
- const checks = m.checks;
126
- if (Array.isArray(checks)) {
127
- const check = checks.find((c) => (c as Record<string, unknown>)?.id === "real-encoded-video") as Record<string, unknown> | undefined;
128
- if (check) {
129
- check.passed = realWebm;
130
- check.evidence = { ...(check.evidence as Record<string, unknown>), byteLength: webmBytes, verified: realWebm };
131
- }
132
- }
133
- });
134
- await patchPackageJson(path.join(pkg.packageDirectory, "metadata.json"), (m) => {
135
- const boundary = m.outputBoundary as Record<string, unknown> | undefined;
136
- if (boundary) boundary.webmPresent = realWebm;
137
- });
138
- await patchPackageJson(path.join(pkg.packageDirectory, "prompt-animation-evidence.json"), (m) => {
139
- const renderOutput = m.renderOutput as Record<string, unknown> | undefined;
140
- if (renderOutput) renderOutput.encodedVideoPresent = realWebm;
141
- });
142
- }
143
-
144
- async function patchPackageJson(filePath: string, patch: (data: Record<string, unknown>) => void) {
145
- if (!existsSync(filePath)) return;
146
- const data = JSON.parse(await readFile(filePath, "utf8")) as Record<string, unknown>;
147
- patch(data);
148
- await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
149
- }
150
-
151
- async function renderEpisodeMedia(pkg: CartoonEpisodePackageBuild) {
152
- const frameDirectory = path.join(pkg.packageDirectory, "frames");
153
- const imageTool = executable("sips") ?? executable("magick") ?? executable("convert");
154
- const ffmpeg = executable("ffmpeg");
155
- if (!imageTool) throw new Error("Cannot render cartoon episode frames: ImageMagick `magick` or `convert` is required.");
156
- if (!ffmpeg) throw new Error("Cannot encode cartoon episode: `ffmpeg` is required.");
157
-
158
- const sequenceSvgFiles = pkg.files
159
- .filter((file) => file.kind === "svg" && /^frames\/frame-\d+\.svg$/.test(file.path))
160
- .map((file) => file.path)
161
- .sort();
162
-
163
- for (const svgPath of sequenceSvgFiles) {
164
- const input = path.join(pkg.packageDirectory, svgPath);
165
- const output = input.replace(/\.svg$/, ".png");
166
- renderSvgToPng(imageTool, input, output);
167
- }
168
-
169
- const representativePairs = [
170
- ["frames/first.svg", "frames/first.png"],
171
- ["frames/dialogue.svg", "frames/dialogue.png"],
172
- ["frames/action.svg", "frames/action.png"],
173
- ["frames/final.svg", "frames/final.png"]
174
- ] as const;
175
- for (const [svgPath, pngPath] of representativePairs) {
176
- renderSvgToPng(imageTool, path.join(pkg.packageDirectory, svgPath), path.join(pkg.packageDirectory, pngPath));
177
- }
178
-
179
- await copyFile(path.join(pkg.packageDirectory, "frames", "action.png"), path.join(pkg.packageDirectory, "thumbnail.png"));
180
- encodeWebm(ffmpeg, frameDirectory, path.join(pkg.packageDirectory, "episode.webm"));
181
- }
182
-
183
- async function captureRouteThumbnail(pkg: CartoonEpisodePackageBuild) {
184
- const vite = viteExecutable();
185
- if (!vite) throw new Error("Cannot capture route thumbnail: Vite executable was not found.");
186
- const port = 4387;
187
- const origin = `http://127.0.0.1:${port}`;
188
- const server = spawn(vite, ["--host", "127.0.0.1", "--port", String(port)], {
189
- cwd: process.cwd(),
190
- env: { ...process.env, BROWSER: "none" },
191
- stdio: "pipe"
192
- });
193
- let startupLog = "";
194
- server.stdout.on("data", (chunk) => {
195
- startupLog += String(chunk);
196
- });
197
- server.stderr.on("data", (chunk) => {
198
- startupLog += String(chunk);
199
- });
200
- try {
201
- await waitForServer(origin, server, () => startupLog);
202
- const { chromium } = await import("playwright");
203
- const browser = await chromium.launch();
204
- try {
205
- const page = await browser.newPage({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 });
206
- await page.goto(`${origin}/?capture=thumbnail`, { waitUntil: "networkidle" });
207
- await page.waitForSelector("canvas", { timeout: 10_000 });
208
- await page.waitForTimeout(500);
209
- await page.locator("canvas").first().screenshot({
210
- path: path.join(pkg.packageDirectory, "thumbnail.png")
211
- });
212
- } finally {
213
- await browser.close();
214
- }
215
- } finally {
216
- stopServer(server);
217
- }
218
- }
219
-
220
- function renderSvgToPng(command: string, input: string, output: string) {
221
- if (path.basename(command) === "sips") {
222
- execFileSync(command, ["-s", "format", "png", input, "--out", output], { stdio: "pipe" });
223
- return;
224
- }
225
- const args = path.basename(command) === "magick"
226
- ? [input, "-background", "none", "-alpha", "remove", "-alpha", "off", output]
227
- : [input, "-background", "none", "-alpha", "remove", "-alpha", "off", output];
228
- execFileSync(command, args, { stdio: "pipe" });
229
- }
230
-
231
- function encodeWebm(ffmpeg: string, frameDirectory: string, output: string) {
232
- execFileSync(ffmpeg, [
233
- "-y",
234
- "-framerate",
235
- "30",
236
- "-stream_loop",
237
- "59",
238
- "-i",
239
- path.join(frameDirectory, "frame-%04d.png"),
240
- "-t",
241
- "60",
242
- "-c:v",
243
- "libvpx-vp9",
244
- "-pix_fmt",
245
- "yuv420p",
246
- "-b:v",
247
- "0",
248
- "-crf",
249
- "34",
250
- output
251
- ], { stdio: "pipe" });
252
- }
253
-
254
- function executable(command: string): string | undefined {
255
- try {
256
- return execFileSync("which", [command], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
257
- } catch {
258
- return undefined;
259
- }
260
- }
261
-
262
- function viteExecutable(): string | undefined {
263
- const local = path.join(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "vite.cmd" : "vite");
264
- return existsSync(local) ? local : executable("vite");
265
- }
266
-
267
- async function waitForServer(origin: string, server: ChildProcessWithoutNullStreams, log: () => string) {
268
- const startedAt = Date.now();
269
- while (Date.now() - startedAt < 30_000) {
270
- if (server.exitCode !== null) {
271
- throw new Error(`Vite exited before route thumbnail capture. Log:\n${log()}`);
272
- }
273
- try {
274
- const response = await fetch(origin);
275
- if (response.ok) return;
276
- } catch {
277
- // Keep polling until Vite is ready.
278
- }
279
- await new Promise((resolve) => setTimeout(resolve, 250));
280
- }
281
- throw new Error(`Timed out waiting for Vite route thumbnail capture. Log:\n${log()}`);
282
- }
283
-
284
- function stopServer(server: ChildProcessWithoutNullStreams) {
285
- if (server.exitCode === null) server.kill("SIGTERM");
286
- }
287
-
288
- async function verifyPackage(pkg: CartoonEpisodePackageBuild) {
289
- const missing = [];
290
- for (const requiredFile of pkg.requiredFiles) {
291
- const target = path.join(pkg.packageDirectory, requiredFile);
292
- if (!existsSync(target)) missing.push(requiredFile);
293
- }
294
-
295
- const hasWebm = existsSync(path.join(pkg.packageDirectory, "episode.webm"));
296
- const hasFallback = existsSync(path.join(pkg.packageDirectory, "episode.png-sequence-fallback.json"));
297
- if (!hasWebm && !hasFallback) missing.push("episode.webm or episode.png-sequence-fallback.json");
298
-
299
- const byteSizes = [];
300
- for (const requiredFile of pkg.requiredFiles) {
301
- const target = path.join(pkg.packageDirectory, requiredFile);
302
- if (!existsSync(target)) continue;
303
- const info = await stat(target);
304
- byteSizes.push({ path: requiredFile, byteSize: info.size });
305
- if (info.size <= 0) missing.push(`${requiredFile} is empty`);
306
- }
307
-
308
- const ok = missing.length === 0;
309
- console.log(JSON.stringify({
310
- ok,
311
- packageDirectory: pkg.packageDirectory,
312
- hasWebm,
313
- hasPngSequenceFallback: hasFallback,
314
- checkedFiles: byteSizes,
315
- missing
316
- }, null, 2));
317
- if (!ok) process.exitCode = 1;
318
- }
319
-
320
- main().catch((error) => {
321
- console.error(error instanceof Error ? error.message : error);
322
- process.exitCode = 1;
323
- });