@koda-sl/baker-cli 0.39.29 → 0.66.2

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 (315) hide show
  1. package/README.md +1754 -7
  2. package/canvas/avocado-tutorial.json +89 -0
  3. package/canvas/hello-world-composition/index.html +83 -0
  4. package/canvas/hello-world-composition/meta.json +41 -0
  5. package/canvas/hello-world-overlay.json +37 -0
  6. package/canvas/phone-scroll-composition/index.html +141 -0
  7. package/canvas/phone-scroll-composition/meta.json +39 -0
  8. package/canvas/tiktok-captions-composition/index.html +126 -0
  9. package/canvas/tiktok-captions-composition/meta.json +46 -0
  10. package/canvas/video-overlay-composition/index.html +239 -0
  11. package/canvas/video-overlay-composition/meta.json +29 -0
  12. package/dist/cli.js +2 -0
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/ads/meta/creatives.js +1 -1
  15. package/dist/commands/canvas/catalog.d.ts +2 -0
  16. package/dist/commands/canvas/catalog.d.ts.map +1 -0
  17. package/dist/commands/canvas/catalog.js +13 -0
  18. package/dist/commands/canvas/catalog.js.map +1 -0
  19. package/dist/commands/canvas/index.d.ts +2 -0
  20. package/dist/commands/canvas/index.d.ts.map +1 -0
  21. package/dist/commands/canvas/index.js +32 -0
  22. package/dist/commands/canvas/index.js.map +1 -0
  23. package/dist/commands/canvas/inspect.d.ts +16 -0
  24. package/dist/commands/canvas/inspect.d.ts.map +1 -0
  25. package/dist/commands/canvas/inspect.js +115 -0
  26. package/dist/commands/canvas/inspect.js.map +1 -0
  27. package/dist/commands/canvas/run.d.ts +24 -0
  28. package/dist/commands/canvas/run.d.ts.map +1 -0
  29. package/dist/commands/canvas/run.js +56 -0
  30. package/dist/commands/canvas/run.js.map +1 -0
  31. package/dist/commands/canvas/scaffold-static-ad.d.ts +40 -0
  32. package/dist/commands/canvas/scaffold-static-ad.d.ts.map +1 -0
  33. package/dist/commands/canvas/scaffold-static-ad.js +265 -0
  34. package/dist/commands/canvas/scaffold-static-ad.js.map +1 -0
  35. package/dist/commands/canvas/scaffold-video.d.ts +44 -0
  36. package/dist/commands/canvas/scaffold-video.d.ts.map +1 -0
  37. package/dist/commands/canvas/scaffold-video.js +235 -0
  38. package/dist/commands/canvas/scaffold-video.js.map +1 -0
  39. package/dist/commands/canvas/validate.d.ts +8 -0
  40. package/dist/commands/canvas/validate.d.ts.map +1 -0
  41. package/dist/commands/canvas/validate.js +37 -0
  42. package/dist/commands/canvas/validate.js.map +1 -0
  43. package/dist/commands/images/find.d.ts +1 -1
  44. package/dist/commands/images/find.d.ts.map +1 -1
  45. package/dist/commands/images/find.js +12 -3
  46. package/dist/commands/images/find.js.map +1 -1
  47. package/dist/commands/images/google.d.ts +1 -1
  48. package/dist/commands/images/google.d.ts.map +1 -1
  49. package/dist/commands/images/google.js +12 -3
  50. package/dist/commands/images/google.js.map +1 -1
  51. package/dist/commands/images/stock.d.ts +1 -1
  52. package/dist/commands/images/stock.d.ts.map +1 -1
  53. package/dist/commands/images/stock.js +7 -3
  54. package/dist/commands/images/stock.js.map +1 -1
  55. package/dist/engine/client/backend-client.d.ts +50 -0
  56. package/dist/engine/client/backend-client.d.ts.map +1 -0
  57. package/dist/engine/client/backend-client.js +20 -0
  58. package/dist/engine/client/backend-client.js.map +1 -0
  59. package/dist/engine/client/env.d.ts +7 -0
  60. package/dist/engine/client/env.d.ts.map +1 -0
  61. package/dist/engine/client/env.js +18 -0
  62. package/dist/engine/client/env.js.map +1 -0
  63. package/dist/engine/client/http.d.ts +56 -0
  64. package/dist/engine/client/http.d.ts.map +1 -0
  65. package/dist/engine/client/http.js +168 -0
  66. package/dist/engine/client/http.js.map +1 -0
  67. package/dist/engine/engine/cache-key.d.ts +19 -0
  68. package/dist/engine/engine/cache-key.d.ts.map +1 -0
  69. package/dist/engine/engine/cache-key.js +33 -0
  70. package/dist/engine/engine/cache-key.js.map +1 -0
  71. package/dist/engine/engine/canonical.d.ts +14 -0
  72. package/dist/engine/engine/canonical.d.ts.map +1 -0
  73. package/dist/engine/engine/canonical.js +53 -0
  74. package/dist/engine/engine/canonical.js.map +1 -0
  75. package/dist/engine/engine/composition-hash.d.ts +14 -0
  76. package/dist/engine/engine/composition-hash.d.ts.map +1 -0
  77. package/dist/engine/engine/composition-hash.js +51 -0
  78. package/dist/engine/engine/composition-hash.js.map +1 -0
  79. package/dist/engine/engine/composition-meta.d.ts +77 -0
  80. package/dist/engine/engine/composition-meta.d.ts.map +1 -0
  81. package/dist/engine/engine/composition-meta.js +199 -0
  82. package/dist/engine/engine/composition-meta.js.map +1 -0
  83. package/dist/engine/engine/context.d.ts +16 -0
  84. package/dist/engine/engine/context.d.ts.map +1 -0
  85. package/dist/engine/engine/context.js +2 -0
  86. package/dist/engine/engine/context.js.map +1 -0
  87. package/dist/engine/engine/define.d.ts +69 -0
  88. package/dist/engine/engine/define.d.ts.map +1 -0
  89. package/dist/engine/engine/define.js +9 -0
  90. package/dist/engine/engine/define.js.map +1 -0
  91. package/dist/engine/engine/didyoumean.d.ts +2 -0
  92. package/dist/engine/engine/didyoumean.d.ts.map +1 -0
  93. package/dist/engine/engine/didyoumean.js +58 -0
  94. package/dist/engine/engine/didyoumean.js.map +1 -0
  95. package/dist/engine/engine/errors.d.ts +41 -0
  96. package/dist/engine/engine/errors.d.ts.map +1 -0
  97. package/dist/engine/engine/errors.js +45 -0
  98. package/dist/engine/engine/errors.js.map +1 -0
  99. package/dist/engine/engine/executor.d.ts +72 -0
  100. package/dist/engine/engine/executor.d.ts.map +1 -0
  101. package/dist/engine/engine/executor.js +445 -0
  102. package/dist/engine/engine/executor.js.map +1 -0
  103. package/dist/engine/engine/refs.d.ts +12 -0
  104. package/dist/engine/engine/refs.d.ts.map +1 -0
  105. package/dist/engine/engine/refs.js +48 -0
  106. package/dist/engine/engine/refs.js.map +1 -0
  107. package/dist/engine/engine/registry.d.ts +11 -0
  108. package/dist/engine/engine/registry.d.ts.map +1 -0
  109. package/dist/engine/engine/registry.js +26 -0
  110. package/dist/engine/engine/registry.js.map +1 -0
  111. package/dist/engine/engine/scheduler.d.ts +10 -0
  112. package/dist/engine/engine/scheduler.d.ts.map +1 -0
  113. package/dist/engine/engine/scheduler.js +89 -0
  114. package/dist/engine/engine/scheduler.js.map +1 -0
  115. package/dist/engine/engine/validator.d.ts +31 -0
  116. package/dist/engine/engine/validator.d.ts.map +1 -0
  117. package/dist/engine/engine/validator.js +501 -0
  118. package/dist/engine/engine/validator.js.map +1 -0
  119. package/dist/engine/index.d.ts +33 -0
  120. package/dist/engine/index.d.ts.map +1 -0
  121. package/dist/engine/index.js +104 -0
  122. package/dist/engine/index.js.map +1 -0
  123. package/dist/engine/lib/ulid.d.ts +3 -0
  124. package/dist/engine/lib/ulid.d.ts.map +1 -0
  125. package/dist/engine/lib/ulid.js +36 -0
  126. package/dist/engine/lib/ulid.js.map +1 -0
  127. package/dist/engine/models/canvas-ad-params.test.d.ts +2 -0
  128. package/dist/engine/models/canvas-ad-params.test.d.ts.map +1 -0
  129. package/dist/engine/models/canvas-ad-params.test.js +60 -0
  130. package/dist/engine/models/canvas-ad-params.test.js.map +1 -0
  131. package/dist/engine/models/registry.d.ts +63 -0
  132. package/dist/engine/models/registry.d.ts.map +1 -0
  133. package/dist/engine/models/registry.js +432 -0
  134. package/dist/engine/models/registry.js.map +1 -0
  135. package/dist/engine/models/validateParams.d.ts +38 -0
  136. package/dist/engine/models/validateParams.d.ts.map +1 -0
  137. package/dist/engine/models/validateParams.js +166 -0
  138. package/dist/engine/models/validateParams.js.map +1 -0
  139. package/dist/engine/nodes/ingest.d.ts +113 -0
  140. package/dist/engine/nodes/ingest.d.ts.map +1 -0
  141. package/dist/engine/nodes/ingest.js +555 -0
  142. package/dist/engine/nodes/ingest.js.map +1 -0
  143. package/dist/engine/nodes/ingest.svg.test.d.ts +2 -0
  144. package/dist/engine/nodes/ingest.svg.test.d.ts.map +1 -0
  145. package/dist/engine/nodes/ingest.svg.test.js +30 -0
  146. package/dist/engine/nodes/ingest.svg.test.js.map +1 -0
  147. package/dist/engine/nodes/local/audioTimeline.d.ts +82 -0
  148. package/dist/engine/nodes/local/audioTimeline.d.ts.map +1 -0
  149. package/dist/engine/nodes/local/audioTimeline.js +97 -0
  150. package/dist/engine/nodes/local/audioTimeline.js.map +1 -0
  151. package/dist/engine/nodes/local/ffmpeg.d.ts +56 -0
  152. package/dist/engine/nodes/local/ffmpeg.d.ts.map +1 -0
  153. package/dist/engine/nodes/local/ffmpeg.js +50 -0
  154. package/dist/engine/nodes/local/ffmpeg.js.map +1 -0
  155. package/dist/engine/nodes/local/fontSpecimen.d.ts +50 -0
  156. package/dist/engine/nodes/local/fontSpecimen.d.ts.map +1 -0
  157. package/dist/engine/nodes/local/fontSpecimen.js +198 -0
  158. package/dist/engine/nodes/local/fontSpecimen.js.map +1 -0
  159. package/dist/engine/nodes/local/hyperframe-snapshot.d.ts +116 -0
  160. package/dist/engine/nodes/local/hyperframe-snapshot.d.ts.map +1 -0
  161. package/dist/engine/nodes/local/hyperframe-snapshot.js +230 -0
  162. package/dist/engine/nodes/local/hyperframe-snapshot.js.map +1 -0
  163. package/dist/engine/nodes/local/hyperframe.d.ts +123 -0
  164. package/dist/engine/nodes/local/hyperframe.d.ts.map +1 -0
  165. package/dist/engine/nodes/local/hyperframe.js +367 -0
  166. package/dist/engine/nodes/local/hyperframe.js.map +1 -0
  167. package/dist/engine/nodes/local/imagemagick.d.ts +56 -0
  168. package/dist/engine/nodes/local/imagemagick.d.ts.map +1 -0
  169. package/dist/engine/nodes/local/imagemagick.js +71 -0
  170. package/dist/engine/nodes/local/imagemagick.js.map +1 -0
  171. package/dist/engine/nodes/local/lib/assets.d.ts +20 -0
  172. package/dist/engine/nodes/local/lib/assets.d.ts.map +1 -0
  173. package/dist/engine/nodes/local/lib/assets.js +40 -0
  174. package/dist/engine/nodes/local/lib/assets.js.map +1 -0
  175. package/dist/engine/nodes/local/lib/cli-runner.d.ts +78 -0
  176. package/dist/engine/nodes/local/lib/cli-runner.d.ts.map +1 -0
  177. package/dist/engine/nodes/local/lib/cli-runner.js +254 -0
  178. package/dist/engine/nodes/local/lib/cli-runner.js.map +1 -0
  179. package/dist/engine/nodes/local/lib/ffmpeg.d.ts +23 -0
  180. package/dist/engine/nodes/local/lib/ffmpeg.d.ts.map +1 -0
  181. package/dist/engine/nodes/local/lib/ffmpeg.js +75 -0
  182. package/dist/engine/nodes/local/lib/ffmpeg.js.map +1 -0
  183. package/dist/engine/nodes/local/lib/hyperframe-errors.d.ts +2 -0
  184. package/dist/engine/nodes/local/lib/hyperframe-errors.d.ts.map +1 -0
  185. package/dist/engine/nodes/local/lib/hyperframe-errors.js +47 -0
  186. package/dist/engine/nodes/local/lib/hyperframe-errors.js.map +1 -0
  187. package/dist/engine/nodes/local/lib/templating.d.ts +22 -0
  188. package/dist/engine/nodes/local/lib/templating.d.ts.map +1 -0
  189. package/dist/engine/nodes/local/lib/templating.js +85 -0
  190. package/dist/engine/nodes/local/lib/templating.js.map +1 -0
  191. package/dist/engine/nodes/local/text.d.ts +6 -0
  192. package/dist/engine/nodes/local/text.d.ts.map +1 -0
  193. package/dist/engine/nodes/local/text.js +15 -0
  194. package/dist/engine/nodes/local/text.js.map +1 -0
  195. package/dist/engine/nodes/remote/delegate.d.ts +25 -0
  196. package/dist/engine/nodes/remote/delegate.d.ts.map +1 -0
  197. package/dist/engine/nodes/remote/delegate.js +160 -0
  198. package/dist/engine/nodes/remote/delegate.js.map +1 -0
  199. package/dist/engine/nodes/remote/dialogue.d.ts +34 -0
  200. package/dist/engine/nodes/remote/dialogue.d.ts.map +1 -0
  201. package/dist/engine/nodes/remote/dialogue.js +54 -0
  202. package/dist/engine/nodes/remote/dialogue.js.map +1 -0
  203. package/dist/engine/nodes/remote/image.d.ts +42 -0
  204. package/dist/engine/nodes/remote/image.d.ts.map +1 -0
  205. package/dist/engine/nodes/remote/image.js +43 -0
  206. package/dist/engine/nodes/remote/image.js.map +1 -0
  207. package/dist/engine/nodes/remote/imageAspectAdapt.d.ts +30 -0
  208. package/dist/engine/nodes/remote/imageAspectAdapt.d.ts.map +1 -0
  209. package/dist/engine/nodes/remote/imageAspectAdapt.js +42 -0
  210. package/dist/engine/nodes/remote/imageAspectAdapt.js.map +1 -0
  211. package/dist/engine/nodes/remote/imageBackgroundRemove.d.ts +39 -0
  212. package/dist/engine/nodes/remote/imageBackgroundRemove.d.ts.map +1 -0
  213. package/dist/engine/nodes/remote/imageBackgroundRemove.js +37 -0
  214. package/dist/engine/nodes/remote/imageBackgroundRemove.js.map +1 -0
  215. package/dist/engine/nodes/remote/imageDescribe.d.ts +29 -0
  216. package/dist/engine/nodes/remote/imageDescribe.d.ts.map +1 -0
  217. package/dist/engine/nodes/remote/imageDescribe.js +25 -0
  218. package/dist/engine/nodes/remote/imageDescribe.js.map +1 -0
  219. package/dist/engine/nodes/remote/imageReferenceSheet.d.ts +34 -0
  220. package/dist/engine/nodes/remote/imageReferenceSheet.d.ts.map +1 -0
  221. package/dist/engine/nodes/remote/imageReferenceSheet.js +38 -0
  222. package/dist/engine/nodes/remote/imageReferenceSheet.js.map +1 -0
  223. package/dist/engine/nodes/remote/imageSearch.d.ts +18 -0
  224. package/dist/engine/nodes/remote/imageSearch.d.ts.map +1 -0
  225. package/dist/engine/nodes/remote/imageSearch.js +22 -0
  226. package/dist/engine/nodes/remote/imageSearch.js.map +1 -0
  227. package/dist/engine/nodes/remote/imageSelect.d.ts +39 -0
  228. package/dist/engine/nodes/remote/imageSelect.d.ts.map +1 -0
  229. package/dist/engine/nodes/remote/imageSelect.js +45 -0
  230. package/dist/engine/nodes/remote/imageSelect.js.map +1 -0
  231. package/dist/engine/nodes/remote/music.d.ts +45 -0
  232. package/dist/engine/nodes/remote/music.d.ts.map +1 -0
  233. package/dist/engine/nodes/remote/music.js +73 -0
  234. package/dist/engine/nodes/remote/music.js.map +1 -0
  235. package/dist/engine/nodes/remote/soundEffect.d.ts +21 -0
  236. package/dist/engine/nodes/remote/soundEffect.d.ts.map +1 -0
  237. package/dist/engine/nodes/remote/soundEffect.js +41 -0
  238. package/dist/engine/nodes/remote/soundEffect.js.map +1 -0
  239. package/dist/engine/nodes/remote/textGenerate.d.ts +21 -0
  240. package/dist/engine/nodes/remote/textGenerate.d.ts.map +1 -0
  241. package/dist/engine/nodes/remote/textGenerate.js +27 -0
  242. package/dist/engine/nodes/remote/textGenerate.js.map +1 -0
  243. package/dist/engine/nodes/remote/tts.d.ts +45 -0
  244. package/dist/engine/nodes/remote/tts.d.ts.map +1 -0
  245. package/dist/engine/nodes/remote/tts.js +66 -0
  246. package/dist/engine/nodes/remote/tts.js.map +1 -0
  247. package/dist/engine/nodes/remote/video.d.ts +58 -0
  248. package/dist/engine/nodes/remote/video.d.ts.map +1 -0
  249. package/dist/engine/nodes/remote/video.js +44 -0
  250. package/dist/engine/nodes/remote/video.js.map +1 -0
  251. package/dist/engine/nodes/remote/videoBackgroundRemove.d.ts +30 -0
  252. package/dist/engine/nodes/remote/videoBackgroundRemove.d.ts.map +1 -0
  253. package/dist/engine/nodes/remote/videoBackgroundRemove.js +29 -0
  254. package/dist/engine/nodes/remote/videoBackgroundRemove.js.map +1 -0
  255. package/dist/engine/nodes/remote/videoDeconstruct.d.ts +61 -0
  256. package/dist/engine/nodes/remote/videoDeconstruct.d.ts.map +1 -0
  257. package/dist/engine/nodes/remote/videoDeconstruct.js +40 -0
  258. package/dist/engine/nodes/remote/videoDeconstruct.js.map +1 -0
  259. package/dist/engine/nodes/remote/videoLipsync.d.ts +37 -0
  260. package/dist/engine/nodes/remote/videoLipsync.d.ts.map +1 -0
  261. package/dist/engine/nodes/remote/videoLipsync.js +26 -0
  262. package/dist/engine/nodes/remote/videoLipsync.js.map +1 -0
  263. package/dist/engine/nodes/remote/videoTranscribe.d.ts +116 -0
  264. package/dist/engine/nodes/remote/videoTranscribe.d.ts.map +1 -0
  265. package/dist/engine/nodes/remote/videoTranscribe.js +123 -0
  266. package/dist/engine/nodes/remote/videoTranscribe.js.map +1 -0
  267. package/dist/engine/nodes/remote/voiceSelect.d.ts +28 -0
  268. package/dist/engine/nodes/remote/voiceSelect.d.ts.map +1 -0
  269. package/dist/engine/nodes/remote/voiceSelect.js +25 -0
  270. package/dist/engine/nodes/remote/voiceSelect.js.map +1 -0
  271. package/dist/engine/scaffold/staticAd.d.ts +44 -0
  272. package/dist/engine/scaffold/staticAd.d.ts.map +1 -0
  273. package/dist/engine/scaffold/staticAd.js +243 -0
  274. package/dist/engine/scaffold/staticAd.js.map +1 -0
  275. package/dist/engine/scaffold/video.d.ts +56 -0
  276. package/dist/engine/scaffold/video.d.ts.map +1 -0
  277. package/dist/engine/scaffold/video.js +709 -0
  278. package/dist/engine/scaffold/video.js.map +1 -0
  279. package/dist/engine/schema/canvas.d.ts +41 -0
  280. package/dist/engine/schema/canvas.d.ts.map +1 -0
  281. package/dist/engine/schema/canvas.js +67 -0
  282. package/dist/engine/schema/canvas.js.map +1 -0
  283. package/dist/engine/schema/catalog.d.ts +25 -0
  284. package/dist/engine/schema/catalog.d.ts.map +1 -0
  285. package/dist/engine/schema/catalog.js +48 -0
  286. package/dist/engine/schema/catalog.js.map +1 -0
  287. package/dist/engine/schema/primitives.d.ts +6 -0
  288. package/dist/engine/schema/primitives.d.ts.map +1 -0
  289. package/dist/engine/schema/primitives.js +4 -0
  290. package/dist/engine/schema/primitives.js.map +1 -0
  291. package/dist/engine/schema/prompts.d.ts +4 -0
  292. package/dist/engine/schema/prompts.d.ts.map +1 -0
  293. package/dist/engine/schema/prompts.js +23 -0
  294. package/dist/engine/schema/prompts.js.map +1 -0
  295. package/dist/engine/schema/refs.d.ts +113 -0
  296. package/dist/engine/schema/refs.d.ts.map +1 -0
  297. package/dist/engine/schema/refs.js +35 -0
  298. package/dist/engine/schema/refs.js.map +1 -0
  299. package/dist/engine/storage/asset-store.d.ts +48 -0
  300. package/dist/engine/storage/asset-store.d.ts.map +1 -0
  301. package/dist/engine/storage/asset-store.js +166 -0
  302. package/dist/engine/storage/asset-store.js.map +1 -0
  303. package/dist/engine/storage/cache-store.d.ts +21 -0
  304. package/dist/engine/storage/cache-store.d.ts.map +1 -0
  305. package/dist/engine/storage/cache-store.js +31 -0
  306. package/dist/engine/storage/cache-store.js.map +1 -0
  307. package/dist/engine/storage/output-writer.d.ts +18 -0
  308. package/dist/engine/storage/output-writer.d.ts.map +1 -0
  309. package/dist/engine/storage/output-writer.js +52 -0
  310. package/dist/engine/storage/output-writer.js.map +1 -0
  311. package/dist/engine/storage/sha256.d.ts +2 -0
  312. package/dist/engine/storage/sha256.d.ts.map +1 -0
  313. package/dist/engine/storage/sha256.js +7 -0
  314. package/dist/engine/storage/sha256.js.map +1 -0
  315. package/package.json +15 -3
@@ -0,0 +1,709 @@
1
+ // Pure transform: a `video-deconstruct/1` blueprint + an AI-selected list of the
2
+ // video's RECURRING identity elements → a runnable `baker-canvas/1` reproduction
3
+ // canvas. No IO — the command layer runs the deconstruct + element-selection
4
+ // passes, bakes the blueprint to `prompt.json`, calls this, validates, and writes.
5
+ //
6
+ // This is the video analogue of `staticAd.ts`, extended for time: every scene
7
+ // boundary is a static-ad-grade `image_generate` (the baked blueprint as
8
+ // `target_blueprint`, a dynamic reference legend, the real extracted frame as a
9
+ // composition anchor), and any recurring element — a person, animal, product, or
10
+ // logo — gets ONE shared `ingest` slot wired into every frame it appears in, so
11
+ // the same actor is grounded by the same source image across the whole video.
12
+ // The per-scene clip (`video_generate`) is fed an ultra-detailed motion brief
13
+ // composed from the scene's action, camera, dialogue, and transcript.
14
+ import { z } from "zod";
15
+ import { ELEVENLABS_MAX_MUSIC_LENGTH_MS } from "../models/registry.js";
16
+ const SEEDANCE_DURATIONS = [4, 5, 6, 8, 10, 12, 15];
17
+ const FIXED_TTS_MODEL = "elevenlabs/eleven_v3";
18
+ const FIXED_SFX_MODEL = "elevenlabs/eleven_text_to_sound_v2";
19
+ const FIXED_MUSIC_MODEL = "elevenlabs/music-v1";
20
+ const MUSIC_BED_GAIN_DB = -12;
21
+ // Aspect ratios both image_generate and the default video model (Seedance) accept.
22
+ const SHARED_ASPECT_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"]);
23
+ const EDGES = ["start", "end"];
24
+ /** Snap an arbitrary scene length to the nearest Seedance-allowed clip duration (≤15s). */
25
+ export function snapToSeedance(durationS) {
26
+ if (!Number.isFinite(durationS) || durationS <= 0)
27
+ return SEEDANCE_DURATIONS[0];
28
+ let best = SEEDANCE_DURATIONS[0];
29
+ let bestDelta = Math.abs(durationS - best);
30
+ for (const d of SEEDANCE_DURATIONS) {
31
+ const delta = Math.abs(durationS - d);
32
+ // On a tie, prefer the longer clip so we don't truncate scene content.
33
+ if (delta < bestDelta || (delta === bestDelta && d > best)) {
34
+ best = d;
35
+ bestDelta = delta;
36
+ }
37
+ }
38
+ return best;
39
+ }
40
+ // --- Tolerant blueprint schema: pick only the fields the scaffold consumes. ---
41
+ const FrameAsset = z.object({ url: z.string().optional() }).loose().optional();
42
+ const DialogueLine = z
43
+ .object({
44
+ speaker: z.string().optional(),
45
+ line: z.string().optional(),
46
+ start_s: z.number().optional(),
47
+ voice_description: z.string().optional(),
48
+ })
49
+ .loose();
50
+ const Sfx = z
51
+ .object({
52
+ at_s: z.number().optional(),
53
+ duration_s: z.number().optional(),
54
+ sound_effect_prompt: z.string().optional(),
55
+ description: z.string().optional(),
56
+ })
57
+ .loose();
58
+ const CameraMotion = z.object({ movement: z.string().optional(), detail: z.string().optional() }).loose();
59
+ const TranscriptWord = z.object({ text: z.string().optional() }).loose();
60
+ const Scene = z
61
+ .object({
62
+ start_s: z.number().optional(),
63
+ end_s: z.number().optional(),
64
+ duration_s: z.number().optional(),
65
+ summary: z.string().optional(),
66
+ action_detail: z.string().optional(),
67
+ camera_motion: CameraMotion.optional(),
68
+ start_frame_prompt: z.string().optional(),
69
+ end_frame_prompt: z.string().optional(),
70
+ motion_prompt: z.string().optional(),
71
+ dialogue: z.array(DialogueLine).optional(),
72
+ sfx: z.array(Sfx).optional(),
73
+ overlays: z.array(z.unknown()).optional(),
74
+ floating_elements: z.array(z.unknown()).optional(),
75
+ transcript_slice: z.array(TranscriptWord).optional(),
76
+ start_frame_asset: FrameAsset,
77
+ end_frame_asset: FrameAsset,
78
+ })
79
+ .loose();
80
+ const VideoBlueprint = z
81
+ .object({
82
+ source: z.object({ aspect_ratio: z.string().optional(), duration_s: z.number().optional() }).loose().optional(),
83
+ global: z
84
+ .object({
85
+ music: z
86
+ .object({
87
+ present: z.boolean().optional(),
88
+ music_prompt: z.string().optional(),
89
+ // Populated by the deconstruct when AudD (Shazam-style) recognizes the
90
+ // reference track. We never reuse it — only style the regenerated bed.
91
+ identified_track: z
92
+ .object({ title: z.string().optional(), artist: z.string().optional() })
93
+ .loose()
94
+ .nullish(),
95
+ })
96
+ .loose()
97
+ .optional(),
98
+ cast: z.array(z.object({ id: z.string().optional(), description: z.string().optional() }).loose()).optional(),
99
+ voiceover: z.object({ voice_description: z.string().optional() }).loose().optional(),
100
+ })
101
+ .loose()
102
+ .optional(),
103
+ scenes: z.array(Scene).min(1),
104
+ })
105
+ .loose();
106
+ // --- A recurring identity element chosen by the selection pass. ---
107
+ // `appears_in`/`scenes` say which scene(s) — and optionally which edge — it is
108
+ // visually present in; that drives which frames reference its shared source image.
109
+ const AppearsItem = z.union([z.number(), z.object({ scene: z.number(), edge: z.string().optional() }).loose()]);
110
+ const RecurringElement = z
111
+ .object({
112
+ // person | animal | product | logo | badge | other
113
+ type: z.string(),
114
+ label: z.string().optional(),
115
+ description: z.string().optional(),
116
+ expression: z.string().nullable().optional(),
117
+ // When the element maps to a global cast entry, its stable id (for annotation).
118
+ cast_id: z.string().nullable().optional(),
119
+ // Scenes the element appears in. Either a bare list of scene indices (both
120
+ // edges) or per-{scene,edge} entries. Both forms are accepted and merged.
121
+ scenes: z.array(z.number()).optional(),
122
+ appears_in: z.array(AppearsItem).optional(),
123
+ })
124
+ .loose();
125
+ const RecurringElements = z.array(RecurringElement);
126
+ /** lower_snake_case id that satisfies the canvas NodeIdRe; falls back to a stable default. */
127
+ function sanitizeId(raw, fallback) {
128
+ const id = raw
129
+ .toLowerCase()
130
+ .replace(/[^a-z0-9]+/g, "_")
131
+ .replace(/^_+|_+$/g, "");
132
+ return /^[a-z]/.test(id) ? id : `${fallback}_${id}`.replace(/_+$/g, "") || fallback;
133
+ }
134
+ /** UPPER_SNAKE reference label, deduped across elements (HERO, HERO_2, ...). */
135
+ function labelFor(el, used) {
136
+ const base = (el.label ?? el.type ?? "ELEMENT")
137
+ .toUpperCase()
138
+ .replace(/[^A-Z0-9]+/g, "_")
139
+ .replace(/^_+|_+$/g, "") || "ELEMENT";
140
+ let label = base;
141
+ let n = 2;
142
+ while (used.has(label))
143
+ label = `${base}_${n++}`;
144
+ used.add(label);
145
+ return label;
146
+ }
147
+ /** Assign each element its deduped reference label, in order — one source of truth shared by every consumer. */
148
+ function assignElementLabels(elements) {
149
+ const used = new Set();
150
+ return elements.map((el) => ({ el, label: labelFor(el, used) }));
151
+ }
152
+ /** Normalize an element's scene/edge presence into scene index → set of edges it appears on. */
153
+ function presenceOf(el) {
154
+ const map = new Map();
155
+ const add = (scene, edge) => {
156
+ if (!Number.isInteger(scene) || scene < 0)
157
+ return;
158
+ let set = map.get(scene);
159
+ if (!set) {
160
+ set = new Set();
161
+ map.set(scene, set);
162
+ }
163
+ if (edge)
164
+ set.add(edge);
165
+ else
166
+ for (const e of EDGES)
167
+ set.add(e);
168
+ };
169
+ for (const s of el.scenes ?? [])
170
+ add(s, null);
171
+ for (const a of el.appears_in ?? []) {
172
+ if (typeof a === "number")
173
+ add(a, null);
174
+ else if (a && typeof a === "object") {
175
+ const edge = a.edge === "start" || a.edge === "end" ? a.edge : null;
176
+ add(a.scene, edge);
177
+ }
178
+ }
179
+ return map;
180
+ }
181
+ function aspectRatioParam(blueprint) {
182
+ const ar = blueprint.source?.aspect_ratio;
183
+ return ar && SHARED_ASPECT_RATIOS.has(ar) ? ar : undefined;
184
+ }
185
+ /**
186
+ * Document the recurring elements in the baked prompt.json: add a top-level
187
+ * `reference_elements` summary (label/type/description/scenes), and stamp
188
+ * `reference_image: LABEL` onto the matching `global.cast[*]` entry by id — so
189
+ * the human editing the JSON sees which wired slot grounds which persona. Clones
190
+ * the input (never mutates the caller's object).
191
+ */
192
+ export function annotateBlueprintWithElements(blueprintInput, elementsInput) {
193
+ if (!blueprintInput || typeof blueprintInput !== "object")
194
+ return blueprintInput;
195
+ const elements = RecurringElements.parse(elementsInput);
196
+ const clone = JSON.parse(JSON.stringify(blueprintInput));
197
+ const cast = (clone.global?.cast ?? null);
198
+ const summary = [];
199
+ for (const { el, label } of assignElementLabels(elements)) {
200
+ const scenes = [...presenceOf(el).keys()].sort((a, b) => a - b);
201
+ summary.push({ reference_image: label, type: el.type, description: el.description ?? null, scenes });
202
+ if (el.cast_id && Array.isArray(cast)) {
203
+ const entry = cast.find((c) => c?.id === el.cast_id);
204
+ if (entry && typeof entry === "object")
205
+ entry.reference_image = label;
206
+ }
207
+ }
208
+ clone.reference_elements = summary;
209
+ return clone;
210
+ }
211
+ /** What the generator should do with each reference, by element type — emphasizes cross-frame consistency. */
212
+ function roleForType(type) {
213
+ switch (type.toLowerCase()) {
214
+ case "logo":
215
+ return "the brand wordmark/logo; reproduce it exactly, do not redraw or restyle it.";
216
+ case "badge":
217
+ return "a trust/rating badge; reproduce it exactly, do not invent ratings or seals.";
218
+ case "product":
219
+ return "the showcased product; keep this exact product identity consistent across every frame. Ignore any caption text printed on this reference.";
220
+ case "person":
221
+ case "animal":
222
+ return "a recurring hero subject; keep this exact identity (face, hair, wardrobe, markings) consistent across EVERY frame of the video. Ignore any caption text printed on this reference.";
223
+ default:
224
+ return "a recurring identity element; reproduce it faithfully and keep it consistent across every frame. Ignore any caption text printed on it.";
225
+ }
226
+ }
227
+ function todoPath(el, label) {
228
+ const desc = el.description ? ` — ${el.description}` : "";
229
+ const expr = el.expression ? `, with a ${el.expression} expression` : "";
230
+ return `[TODO: drop one real source image for ${label} (${el.type})${desc}${expr} — reused across every frame it appears in]`;
231
+ }
232
+ /** Compute one shared ingest slot per recurring element (id, label, presence map). */
233
+ function buildElementSlots(elements) {
234
+ const usedIds = new Set(["prompt", "spine", "overlaid", "audio_mix", "final", "music_bed"]);
235
+ const slots = [];
236
+ assignElementLabels(elements).forEach(({ el, label }, i) => {
237
+ let id = sanitizeId(`el_${label}`, `el_${i}`);
238
+ while (usedIds.has(id))
239
+ id = `${id}_${i}`;
240
+ usedIds.add(id);
241
+ slots.push({
242
+ id,
243
+ ref: `$ref:${id}.asset`,
244
+ label,
245
+ type: el.type,
246
+ description: el.description,
247
+ presence: presenceOf(el),
248
+ });
249
+ });
250
+ return slots;
251
+ }
252
+ /** The element slots whose presence includes this scene/edge. */
253
+ function slotsForFrame(slots, sceneIndex, edge) {
254
+ return slots.filter((s) => s.presence.get(sceneIndex)?.has(edge));
255
+ }
256
+ /** The element slots present anywhere in this scene (either edge) — for the motion brief. */
257
+ function slotsForScene(slots, sceneIndex) {
258
+ return slots.filter((s) => s.presence.has(sceneIndex));
259
+ }
260
+ /**
261
+ * Build the static-ad-grade prompt for one boundary frame.
262
+ *
263
+ * The prompt is SELF-CONTAINED and is the per-frame editable surface: the FRAME
264
+ * DESCRIPTION is inlined verbatim from the blueprint's `{edge}_frame_prompt`, so
265
+ * editing this one node's `params.prompt` changes ONLY this frame — no other
266
+ * frame and no shared file is touched. The blueprint (`{{target_blueprint}}`) is
267
+ * kept ONLY as a demoted, shared style reference (global cast identity, palette,
268
+ * typography mood, aspect ratio) — never as the source of this frame's content.
269
+ */
270
+ function buildFramePrompt(edge, sceneIndex, framePrompt, present, hasAnchor) {
271
+ const EDGE = edge.toUpperCase();
272
+ const legend = [
273
+ ...present.map((s) => `- ${s.label} — ${roleForType(s.type)}`),
274
+ ...(hasAnchor
275
+ ? [
276
+ "- ORIGINAL_FRAME — use ONLY for composition, framing, pose, and proportions of THIS frame. IGNORE its overlay text, captions, and any brand that is being swapped.",
277
+ ]
278
+ : []),
279
+ ].join("\n");
280
+ const description = framePrompt?.trim() ||
281
+ `the ${edge} frame of scene ${sceneIndex + 1} — describe the full composition, subjects, setting, action, lighting, and palette here. (Edit this line to change ONLY this frame.)`;
282
+ return [
283
+ `Render the ${EDGE} frame of scene ${sceneIndex + 1} as a single still image. This prompt is self-contained and edit-per-frame: change the FRAME DESCRIPTION below to alter ONLY this frame. EXCLUDE all overlay text, captions, stickers, and watermarks — they are composited on top later.`,
284
+ "",
285
+ "REFERENCE IMAGES (in the order provided):",
286
+ legend,
287
+ "",
288
+ "FRAME DESCRIPTION (this frame's editable prompt):",
289
+ description,
290
+ "",
291
+ "Keep every recurring element identical to its reference image across all frames. Use the GLOBAL STYLE REFERENCE only for shared cast identity, palette, typography mood, and aspect ratio — do NOT copy another scene's composition from it; this frame's content is the FRAME DESCRIPTION above.",
292
+ "",
293
+ "GLOBAL STYLE REFERENCE (shared across frames; not this frame's content):",
294
+ "{{target_blueprint}}",
295
+ ].join("\n");
296
+ }
297
+ /**
298
+ * Emit the node(s) for one scene boundary frame and return the ref to wire into
299
+ * video_generate: ingest the real frame (when present), then either reuse it
300
+ * directly (faithful mode) or regenerate it static-ad-style — the baked blueprint
301
+ * as target_blueprint, the shared recurring-element images + the real frame as
302
+ * references, and a per-element legend.
303
+ */
304
+ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
305
+ const refId = `s${ctx.sceneIndex}_${edge}_ref`;
306
+ if (url)
307
+ nodes.push({ id: refId, type: "ingest", params: { source: "url", url, expect: "image" } });
308
+ if (ctx.reuse && url)
309
+ return `$ref:${refId}.asset`;
310
+ const reference = [...present.map((s) => s.ref), ...(url ? [`$ref:${refId}.asset`] : [])];
311
+ const genParams = {
312
+ model: ctx.imageModel,
313
+ image_size: "2K",
314
+ prompt: buildFramePrompt(edge, ctx.sceneIndex, framePrompt, present, Boolean(url)),
315
+ };
316
+ if (ctx.ar)
317
+ genParams.aspect_ratio = ctx.ar;
318
+ const genNode = {
319
+ id: `s${ctx.sceneIndex}_${edge}`,
320
+ type: "image_generate",
321
+ // `params.prompt` is this frame's authoritative, edit-per-frame description.
322
+ // `target_blueprint` is kept only as a demoted shared style reference (global
323
+ // cast/palette/typography), so editing one frame never touches another.
324
+ inputs: { target_blueprint: "$ref:prompt.asset", ...(reference.length > 0 ? { reference } : {}) },
325
+ params: genParams,
326
+ };
327
+ nodes.push(genNode);
328
+ return `$ref:s${ctx.sceneIndex}_${edge}.images#0`;
329
+ }
330
+ /** Compose the ultra-detailed motion brief handed to the video model for one scene. */
331
+ function buildSeedancePrompt(scene, sceneIndex, present) {
332
+ const parts = [];
333
+ const summary = scene.summary?.trim();
334
+ parts.push(summary ? `Scene ${sceneIndex + 1}: ${summary}` : `Scene ${sceneIndex + 1}`);
335
+ if (scene.action_detail)
336
+ parts.push(`Action: ${scene.action_detail}`);
337
+ const cm = scene.camera_motion;
338
+ if (cm) {
339
+ const camera = [cm.movement, cm.detail].filter(Boolean).join(" — ");
340
+ if (camera)
341
+ parts.push(`Camera: ${camera}`);
342
+ }
343
+ if (scene.motion_prompt)
344
+ parts.push(`Motion: ${scene.motion_prompt}`);
345
+ if (present.length > 0) {
346
+ parts.push(`Keep these consistent with their references: ${present.map((s) => `${s.label} (${s.description ?? s.type})`).join("; ")}`);
347
+ }
348
+ const lines = (scene.dialogue ?? []).map((d) => d.line?.trim()).filter((l) => Boolean(l));
349
+ if (lines.length > 0)
350
+ parts.push(`Spoken: ${lines.map((l) => `"${l}"`).join(" ")}`);
351
+ const transcript = (scene.transcript_slice ?? [])
352
+ .map((w) => w.text?.trim())
353
+ .filter(Boolean)
354
+ .join(" ")
355
+ .trim();
356
+ if (transcript)
357
+ parts.push(`Transcript: ${transcript}`);
358
+ return parts.join("\n");
359
+ }
360
+ /** Build the per-scene visual chain (frames + clip) and return the clip ref ids. */
361
+ function buildSceneVisuals(blueprint, slots, opts, nodes) {
362
+ const ar = aspectRatioParam(blueprint);
363
+ const reuse = opts.frames === "reuse";
364
+ const clipRefs = [];
365
+ blueprint.scenes.forEach((scene, i) => {
366
+ const ctx = { sceneIndex: i, ar, reuse, imageModel: opts.imageModel };
367
+ const firstFrame = buildFrameRef("start", scene.start_frame_asset?.url, scene.start_frame_prompt, slotsForFrame(slots, i, "start"), ctx, nodes);
368
+ const lastFrame = buildFrameRef("end", scene.end_frame_asset?.url, scene.end_frame_prompt, slotsForFrame(slots, i, "end"), ctx, nodes);
369
+ const clipParams = {
370
+ model: opts.videoModel,
371
+ prompt: buildSeedancePrompt(scene, i, slotsForScene(slots, i)),
372
+ duration: snapToSeedance(scene.duration_s ?? 5),
373
+ };
374
+ if (ar)
375
+ clipParams.aspect_ratio = ar;
376
+ nodes.push({
377
+ id: `s${i}_clip`,
378
+ type: "video_generate",
379
+ inputs: { first_frame: firstFrame, last_frame: lastFrame },
380
+ params: clipParams,
381
+ });
382
+ clipRefs.push(`$ref:s${i}_clip.video`);
383
+ });
384
+ return clipRefs;
385
+ }
386
+ /**
387
+ * The music-bed generation prompt. When the deconstruct identified the real
388
+ * reference track (via AudD), style the regenerated bed after its vibe — but
389
+ * NEVER reproduce it (that would be a copyright lift); we generate original music
390
+ * that matches the mood/tempo/energy.
391
+ */
392
+ function musicBedPrompt(blueprint, musicPrompt) {
393
+ const track = blueprint.global?.music?.identified_track;
394
+ const title = track?.title?.trim();
395
+ if (!title)
396
+ return musicPrompt;
397
+ const by = track?.artist?.trim() ? ` by ${track.artist.trim()}` : "";
398
+ return `${musicPrompt}\n\nReference vibe: the original used "${title}"${by} (identified via AudD). Match its mood, tempo, and energy with ORIGINAL music — do not reproduce the track.`;
399
+ }
400
+ /** Build voiceover (tts), SFX, and music nodes; return the audio tracks to mix. */
401
+ function buildAudio(blueprint, nodes) {
402
+ const tracks = [];
403
+ // One voice_select per distinct speaker, cast from its description.
404
+ const voiceNodeBySpeaker = new Map();
405
+ const speakerDescription = (speaker) => {
406
+ for (const scene of blueprint.scenes) {
407
+ for (const line of scene.dialogue ?? []) {
408
+ if ((line.speaker ?? "voiceover") === speaker && line.voice_description)
409
+ return line.voice_description;
410
+ }
411
+ }
412
+ const cast = blueprint.global?.cast?.find((c) => c.id === speaker);
413
+ return cast?.description ?? blueprint.global?.voiceover?.voice_description ?? `${speaker} voice`;
414
+ };
415
+ const ensureVoiceNode = (speaker) => {
416
+ const existing = voiceNodeBySpeaker.get(speaker);
417
+ if (existing)
418
+ return existing;
419
+ const id = sanitizeId(`voice_${speaker}`, `voice_${voiceNodeBySpeaker.size}`);
420
+ nodes.push({ id, type: "voice_select", params: { description: speakerDescription(speaker) } });
421
+ voiceNodeBySpeaker.set(speaker, id);
422
+ return id;
423
+ };
424
+ const scriptBySpeaker = new Map();
425
+ const orderedSpeakers = [];
426
+ for (const scene of blueprint.scenes) {
427
+ for (const line of scene.dialogue ?? []) {
428
+ if (!line.line)
429
+ continue;
430
+ const speaker = line.speaker ?? "voiceover";
431
+ const start = line.start_s ?? scene.start_s ?? 0;
432
+ const existing = scriptBySpeaker.get(speaker);
433
+ if (existing) {
434
+ existing.lines.push(line.line);
435
+ existing.start = Math.min(existing.start, start);
436
+ }
437
+ else {
438
+ scriptBySpeaker.set(speaker, { lines: [line.line], start });
439
+ orderedSpeakers.push(speaker);
440
+ }
441
+ }
442
+ }
443
+ const usedVoIds = new Set();
444
+ orderedSpeakers.forEach((speaker, idx) => {
445
+ const script = scriptBySpeaker.get(speaker);
446
+ if (!script)
447
+ return;
448
+ const voiceNode = ensureVoiceNode(speaker);
449
+ let id = sanitizeId(`vo_${speaker}`, `vo_${idx}`);
450
+ while (usedVoIds.has(id))
451
+ id = `${id}_${idx}`;
452
+ usedVoIds.add(id);
453
+ nodes.push({
454
+ id,
455
+ type: "tts",
456
+ inputs: { voice_ref: `$ref:${voiceNode}.voice_id` },
457
+ // Join lines with a space: each line keeps its own terminal punctuation, so
458
+ // sentence boundaries (and the pauses they imply) survive into one read.
459
+ params: { model: FIXED_TTS_MODEL, text: script.lines.join(" "), voice: "{{voice_ref}}" },
460
+ });
461
+ tracks.push({ slot: id, ref: `$ref:${id}.audio`, start_s: script.start });
462
+ });
463
+ // SFX: one node per scene sound-effect event.
464
+ blueprint.scenes.forEach((scene, i) => {
465
+ (scene.sfx ?? []).forEach((sfx, k) => {
466
+ const text = sfx.sound_effect_prompt ?? sfx.description;
467
+ if (!text)
468
+ return;
469
+ const id = `s${i}_sfx${k}`;
470
+ const params = { model: FIXED_SFX_MODEL, text };
471
+ // Clamp to the provider range so the emitted canvas self-validates.
472
+ if (typeof sfx.duration_s === "number")
473
+ params.duration_seconds = Math.min(Math.max(sfx.duration_s, 0.5), 30);
474
+ nodes.push({ id, type: "sound_effect", params });
475
+ tracks.push({ slot: `sfx_s${i}_${k}`, ref: `$ref:${id}.audio`, start_s: sfx.at_s ?? scene.start_s ?? 0 });
476
+ });
477
+ });
478
+ const musicPrompt = blueprint.global?.music?.music_prompt;
479
+ if (musicPrompt) {
480
+ const totalMs = Math.round((blueprint.source?.duration_s ?? lastSceneEnd(blueprint)) * 1000);
481
+ // Clamp to the validated range so very long sources still scaffold; the
482
+ // audio_timeline pads/trims the bed to the real length anyway.
483
+ const musicMs = Math.min(Math.max(totalMs, 3000), ELEVENLABS_MAX_MUSIC_LENGTH_MS);
484
+ nodes.push({
485
+ id: "music_bed",
486
+ type: "music",
487
+ params: { model: FIXED_MUSIC_MODEL, prompt: musicBedPrompt(blueprint, musicPrompt), music_length_ms: musicMs },
488
+ });
489
+ // Music first so it sits under the voices in the mix.
490
+ tracks.unshift({ slot: "music", ref: "$ref:music_bed.audio", start_s: 0, gain_db: MUSIC_BED_GAIN_DB });
491
+ }
492
+ return tracks;
493
+ }
494
+ function lastSceneEnd(blueprint) {
495
+ let end = 0;
496
+ for (const s of blueprint.scenes)
497
+ end = Math.max(end, s.end_s ?? 0);
498
+ return end > 0 ? end : 8;
499
+ }
500
+ /** ffmpeg argv: concatenate N video-only clips into one. */
501
+ function concatArgs(count) {
502
+ const inputs = [];
503
+ let labels = "";
504
+ for (let i = 0; i < count; i++) {
505
+ inputs.push("-i", `{{in.c${i}}}`);
506
+ labels += `[${i}:v]`;
507
+ }
508
+ return [...inputs, "-filter_complex", `${labels}concat=n=${count}:v=1:a=0[v]`, "-map", "[v]", "{{out.video}}"];
509
+ }
510
+ /**
511
+ * Assemble a full video reproduction canvas. The baked blueprint as the editable
512
+ * `prompt`, one shared `ingest` per recurring element, then the visual spine
513
+ * (per-scene boundary frames → clips → concat), an optional burned-in overlay
514
+ * pass, and an optional audio mix (music + voiceover + SFX) muxed under the video.
515
+ */
516
+ export function scaffoldVideoCanvas(input, elementsInput, opts) {
517
+ const blueprint = VideoBlueprint.parse(input);
518
+ const elements = RecurringElements.parse(elementsInput);
519
+ const nodes = [];
520
+ // The editable "prompt": the baked deconstruct blueprint, ingested as JSON and
521
+ // fed into every frame generator via target_blueprint.
522
+ nodes.push({
523
+ id: "prompt",
524
+ type: "ingest",
525
+ params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" },
526
+ });
527
+ // One shared ingest per recurring identity element, wired into every frame it appears in.
528
+ // buildElementSlots preserves input order, so slots[i] corresponds to elements[i].
529
+ const slots = buildElementSlots(elements);
530
+ slots.forEach((slot, i) => {
531
+ nodes.push({
532
+ id: slot.id,
533
+ type: "ingest",
534
+ params: { source: "path", path: todoPath(elements[i], slot.label), expect: "image" },
535
+ });
536
+ });
537
+ const clipRefs = buildSceneVisuals(blueprint, slots, opts, nodes);
538
+ // Concatenate the per-scene clips into the visual spine.
539
+ const concatInputs = {};
540
+ clipRefs.forEach((ref, i) => {
541
+ concatInputs[`c${i}`] = ref;
542
+ });
543
+ nodes.push({
544
+ id: "spine",
545
+ type: "ffmpeg",
546
+ inputs: concatInputs,
547
+ params: { args: concatArgs(clipRefs.length), outputs: { video: { kind: "video", ext: "mp4" } } },
548
+ });
549
+ let videoRef = "$ref:spine.video";
550
+ let videoNode = "spine";
551
+ // Burned-in overlay pass (text overlays + floating-element placeholders).
552
+ const overlays = blueprint.scenes.flatMap((s) => s.overlays ?? []);
553
+ const floating = blueprint.scenes.flatMap((s) => s.floating_elements ?? []);
554
+ if (overlays.length > 0 || floating.length > 0) {
555
+ nodes.push({
556
+ id: "overlaid",
557
+ type: "hyperframe_render",
558
+ inputs: { background: videoRef },
559
+ params: { composition: opts.overlayCompositionPath, overlays, floating_elements: floating },
560
+ });
561
+ videoRef = "$ref:overlaid.video";
562
+ videoNode = "overlaid";
563
+ }
564
+ // Audio: mix music + voiceover + SFX onto the timeline, then mux under video.
565
+ const tracks = buildAudio(blueprint, nodes);
566
+ if (tracks.length > 0) {
567
+ const mixInputs = {};
568
+ for (const t of tracks)
569
+ mixInputs[t.slot] = t.ref;
570
+ nodes.push({
571
+ id: "audio_mix",
572
+ type: "audio_timeline",
573
+ inputs: mixInputs,
574
+ params: {
575
+ tracks: tracks.map((t) => ({
576
+ slot: t.slot,
577
+ start_s: t.start_s,
578
+ ...(t.gain_db !== undefined ? { gain_db: t.gain_db } : {}),
579
+ })),
580
+ total_ms: Math.round((blueprint.source?.duration_s ?? lastSceneEnd(blueprint)) * 1000),
581
+ },
582
+ });
583
+ nodes.push({
584
+ id: "final",
585
+ type: "ffmpeg",
586
+ inputs: { video: videoRef, audio: "$ref:audio_mix.audio" },
587
+ params: {
588
+ args: [
589
+ "-i",
590
+ "{{in.video}}",
591
+ "-i",
592
+ "{{in.audio}}",
593
+ "-map",
594
+ "0:v:0",
595
+ "-map",
596
+ "1:a:0",
597
+ "-c:v",
598
+ "copy",
599
+ "-c:a",
600
+ "aac",
601
+ "-shortest",
602
+ "{{out.video}}",
603
+ ],
604
+ outputs: { video: { kind: "video", ext: "mp4" } },
605
+ },
606
+ });
607
+ videoNode = "final";
608
+ }
609
+ return {
610
+ schema: "baker-canvas/1",
611
+ metadata: {
612
+ name: "video reproduction",
613
+ description: VIDEO_GUIDE,
614
+ todo: buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts),
615
+ },
616
+ nodes,
617
+ output: { node: videoNode, output: "video" },
618
+ };
619
+ }
620
+ /** The step-by-step next-steps guide embedded in the canvas (and printed by the command). */
621
+ const VIDEO_GUIDE = [
622
+ "Scaffolded by `baker canvas scaffold-video` — a runnable reproduction of your reference video. Each scene is two AI-generated boundary frames (start/end) animated into a clip, concatenated, overlaid with timed text, and mixed with voiceover + SFX + music. Edit it, supply the real assets, then run.",
623
+ "",
624
+ "WHAT TO DO NEXT:",
625
+ "1. Edit each frame's prompt IN PLACE. Every `s<i>_start` / `s<i>_end` node has its OWN self-contained `params.prompt` (the FRAME DESCRIPTION) — editing one changes only that frame. Rewrite the cast, product, claims, palette into the ad you want.",
626
+ "2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent.",
627
+ "3. Confirm the `voice_select` casting (one per speaker). The voiceover is ONE continuous `tts` per speaker — punctuation, ALL-CAPS, and spacing are read verbatim by eleven_v3 for emphasis/pauses, so edit `params.text` to shape delivery.",
628
+ "4. Text overlays are composited on top (not baked into frames) by the `overlaid` node — edit the `overlays` array there. For on-brand type, drop `brand-bold.otf` / `brand-regular.otf` into the `video-overlay-composition/` dir (referenced via @font-face); otherwise a system font is used. You don't always need text — it's often cleaner to overlay it than bake it in.",
629
+ "5. `baker canvas validate` then `baker canvas run`. Running generates many billed image/video/audio assets — it is not free.",
630
+ "",
631
+ "Tip: `prompt.json` is the deconstruction provenance + the demoted GLOBAL STYLE REFERENCE each frame reads for shared palette/cast cohesion. It is NOT the per-frame editing surface — the frame nodes are.",
632
+ ].join("\n");
633
+ /** Build the structured, in-canvas checklist mirroring the command's stdout report. */
634
+ function buildVideoTodo(report, overlayCount, floatingCount, opts) {
635
+ return {
636
+ edit_frames_in_place: "Each s<i>_start / s<i>_end node has its own editable params.prompt (FRAME DESCRIPTION). Edit per frame; the blueprint is only a shared style reference.",
637
+ frames_mode: opts.frames ?? "generate",
638
+ recurring_elements_to_supply: report.elements,
639
+ voices_to_confirm: report.dialogue.map((d) => ({
640
+ scene: d.scene,
641
+ speaker: d.speaker,
642
+ voice_description: d.voice_description,
643
+ line: d.line,
644
+ })),
645
+ voiceover_note: "One continuous tts per speaker; same voice locked via voice_select.voice_id. Use punctuation / ALL CAPS / line breaks in params.text for emphasis and pacing (read verbatim).",
646
+ text_overlays: {
647
+ count: overlayCount,
648
+ note: "Composited by the `overlaid` node, animated per the blueprint (fade/pop/slide/typewriter/karaoke). Edit the `overlays` array. Drop brand-*.otf into video-overlay-composition/ for on-brand type.",
649
+ },
650
+ floating_elements: {
651
+ count: floatingCount,
652
+ note: floatingCount > 0
653
+ ? "Rendered as labeled placeholders. Replace with the real logo/sticker/cutout art (recurring logos are better handled as an el_* element baked into frames)."
654
+ : "none detected",
655
+ },
656
+ sound_effects: { count: report.sfx_count },
657
+ music: {
658
+ present: report.has_music,
659
+ note: report.has_music
660
+ ? "Original bed regenerated from the deconstruct prompt (styled after the AudD-identified track when available); ducked under the voices."
661
+ : "no music bed scaffolded",
662
+ },
663
+ scenes_clamped_to_15s: report.clamped_scenes,
664
+ run_warning: "`baker canvas run` generates many billed image/video/audio assets — validate first, it is not free.",
665
+ };
666
+ }
667
+ /** Human-facing checklist the command prints — what needs review/assets after scaffolding. */
668
+ export function videoReport(input, elementsInput) {
669
+ const blueprint = VideoBlueprint.parse(input);
670
+ const elements = RecurringElements.parse(elementsInput);
671
+ const dialogue = [];
672
+ const clamped = [];
673
+ let sfxCount = 0;
674
+ let overlayCount = 0;
675
+ blueprint.scenes.forEach((scene, i) => {
676
+ for (const line of scene.dialogue ?? []) {
677
+ if (line.line) {
678
+ dialogue.push({
679
+ scene: i,
680
+ speaker: line.speaker ?? "voiceover",
681
+ line: line.line,
682
+ voice_description: line.voice_description ?? null,
683
+ });
684
+ }
685
+ }
686
+ sfxCount += (scene.sfx ?? []).length;
687
+ overlayCount += (scene.overlays ?? []).length;
688
+ const original = scene.duration_s ?? 5;
689
+ const clip = snapToSeedance(original);
690
+ if (original > 15)
691
+ clamped.push({ scene: i, original_s: original, clip_s: clip });
692
+ });
693
+ return {
694
+ scene_count: blueprint.scenes.length,
695
+ elements: assignElementLabels(elements).map(({ el, label }) => ({
696
+ label,
697
+ type: el.type,
698
+ description: el.description ?? null,
699
+ scenes: [...presenceOf(el).keys()].sort((a, b) => a - b),
700
+ asset_todo: todoPath(el, label),
701
+ })),
702
+ dialogue,
703
+ sfx_count: sfxCount,
704
+ overlay_count: overlayCount,
705
+ clamped_scenes: clamped,
706
+ has_music: Boolean(blueprint.global?.music?.music_prompt),
707
+ };
708
+ }
709
+ //# sourceMappingURL=video.js.map