@1agh/maude 0.42.0 → 0.44.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 (332) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/acp/bridge.ts +44 -10
  4. package/apps/studio/acp/env.ts +23 -0
  5. package/apps/studio/acp/login-state.ts +263 -0
  6. package/apps/studio/acp/plugin-bootstrap.ts +31 -25
  7. package/apps/studio/acp/probe.ts +152 -1
  8. package/apps/studio/annotations-layer.tsx +1451 -324
  9. package/apps/studio/annotations-model.ts +58 -0
  10. package/apps/studio/api.ts +568 -24
  11. package/apps/studio/bin/_audio-search.mjs +140 -0
  12. package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
  13. package/apps/studio/bin/_import-asset.mjs +815 -0
  14. package/apps/studio/bin/_import-brand.mjs +635 -0
  15. package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
  16. package/apps/studio/bin/_import-tokens.mjs +1201 -0
  17. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  18. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  19. package/apps/studio/bin/_transcribe.mjs +419 -0
  20. package/apps/studio/bin/_transcribe.test.mjs +80 -0
  21. package/apps/studio/bin/_video-playwright.mjs +141 -71
  22. package/apps/studio/bin/audio-search.sh +28 -0
  23. package/apps/studio/bin/generate.sh +154 -0
  24. package/apps/studio/bin/import-asset.sh +34 -0
  25. package/apps/studio/bin/import-brand.sh +29 -0
  26. package/apps/studio/bin/import-tokens.sh +33 -0
  27. package/apps/studio/bin/ingest-footage.sh +27 -0
  28. package/apps/studio/bin/photo-adjust.sh +163 -0
  29. package/apps/studio/bin/photo-bg-remove.sh +258 -0
  30. package/apps/studio/bin/probe-footage.sh +28 -0
  31. package/apps/studio/bin/read-annotations.mjs +125 -2
  32. package/apps/studio/bin/transcribe.sh +39 -0
  33. package/apps/studio/canvas-edit.ts +656 -12
  34. package/apps/studio/canvas-lib.tsx +637 -10
  35. package/apps/studio/canvas-pipeline.ts +65 -7
  36. package/apps/studio/canvas-shell.tsx +156 -16
  37. package/apps/studio/client/app.jsx +2385 -699
  38. package/apps/studio/client/export-center.jsx +35 -1
  39. package/apps/studio/client/generate-dialog.jsx +352 -0
  40. package/apps/studio/client/github.js +18 -0
  41. package/apps/studio/client/inspector-controls.jsx +781 -0
  42. package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
  43. package/apps/studio/client/panels/ChatPanel.jsx +15 -33
  44. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  45. package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
  46. package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
  47. package/apps/studio/client/panels/ReadinessList.jsx +227 -7
  48. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  49. package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
  50. package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
  51. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  52. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  53. package/apps/studio/client/photo-knobs.jsx +432 -0
  54. package/apps/studio/client/styles/3-shell-maude.css +376 -28
  55. package/apps/studio/client/styles/4-components.css +223 -0
  56. package/apps/studio/client/styles/6-acp-chat.css +86 -0
  57. package/apps/studio/client/tour/quick-setup-tour.js +39 -0
  58. package/apps/studio/commands/annotation-strokes-command.ts +13 -3
  59. package/apps/studio/commands/edit-source-command.ts +31 -4
  60. package/apps/studio/config.schema.json +70 -0
  61. package/apps/studio/context-menu.tsx +42 -8
  62. package/apps/studio/context.ts +16 -0
  63. package/apps/studio/design-setup-readiness.ts +115 -0
  64. package/apps/studio/dist/client.bundle.js +5572 -21
  65. package/apps/studio/dist/comment-mount.js +2 -2
  66. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  67. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  68. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  69. package/apps/studio/dist/styles.css +1 -1
  70. package/apps/studio/dom-selection.ts +25 -0
  71. package/apps/studio/draw/palette.ts +34 -0
  72. package/apps/studio/exporters/jobs.ts +47 -5
  73. package/apps/studio/exporters/video.ts +24 -4
  74. package/apps/studio/footage/schema.test.ts +287 -0
  75. package/apps/studio/footage/schema.ts +662 -0
  76. package/apps/studio/footage-store.ts +235 -0
  77. package/apps/studio/fs-watch.ts +10 -0
  78. package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
  79. package/apps/studio/generation/adapters/gemini.ts +542 -0
  80. package/apps/studio/generation/adapters/groq.test.ts +80 -0
  81. package/apps/studio/generation/adapters/groq.ts +247 -0
  82. package/apps/studio/generation/audio-library.test.ts +95 -0
  83. package/apps/studio/generation/audio-library.ts +156 -0
  84. package/apps/studio/generation/captions.test.ts +78 -0
  85. package/apps/studio/generation/captions.ts +132 -0
  86. package/apps/studio/generation/download.ts +76 -0
  87. package/apps/studio/generation/elevenlabs.test.ts +223 -0
  88. package/apps/studio/generation/gemini.test.ts +350 -0
  89. package/apps/studio/generation/jobs.test.ts +128 -0
  90. package/apps/studio/generation/jobs.ts +243 -0
  91. package/apps/studio/generation/keys.test.ts +75 -0
  92. package/apps/studio/generation/keys.ts +162 -0
  93. package/apps/studio/generation/prefs.test.ts +95 -0
  94. package/apps/studio/generation/prefs.ts +82 -0
  95. package/apps/studio/generation/registry.ts +88 -0
  96. package/apps/studio/generation/types.test.ts +123 -0
  97. package/apps/studio/generation/types.ts +266 -0
  98. package/apps/studio/generation/whisper-models.test.ts +152 -0
  99. package/apps/studio/generation/whisper-models.ts +257 -0
  100. package/apps/studio/generation/whisper-spike-results.md +61 -0
  101. package/apps/studio/git/endpoints.ts +68 -7
  102. package/apps/studio/git/service.ts +199 -21
  103. package/apps/studio/github/service.ts +67 -0
  104. package/apps/studio/handoff.ts +3 -2
  105. package/apps/studio/http.ts +1039 -22
  106. package/apps/studio/input-router.tsx +10 -0
  107. package/apps/studio/inspect.ts +53 -1
  108. package/apps/studio/media/intro.mp4 +0 -0
  109. package/apps/studio/media-commit-chain.ts +119 -0
  110. package/apps/studio/paths.ts +20 -0
  111. package/apps/studio/photo/filters.ts +285 -0
  112. package/apps/studio/photo/pipeline.ts +607 -0
  113. package/apps/studio/photo/schema.ts +367 -0
  114. package/apps/studio/photo-store.ts +154 -0
  115. package/apps/studio/readiness.ts +108 -20
  116. package/apps/studio/runtime-bundle.ts +29 -6
  117. package/apps/studio/scaffold-design.ts +1145 -0
  118. package/apps/studio/server.ts +18 -2
  119. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  120. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  121. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  122. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  123. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  124. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  125. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  126. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  127. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  128. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  129. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  130. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  131. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  132. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  133. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  134. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  135. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  136. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  137. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  138. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  139. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  140. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  141. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  142. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  143. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  144. package/apps/studio/stickers/life-style/best.png +0 -0
  145. package/apps/studio/stickers/life-style/come-on.png +0 -0
  146. package/apps/studio/stickers/life-style/cut.png +0 -0
  147. package/apps/studio/stickers/life-style/detox.png +0 -0
  148. package/apps/studio/stickers/life-style/just.png +0 -0
  149. package/apps/studio/stickers/life-style/manifest.json +68 -0
  150. package/apps/studio/stickers/life-style/mirror.png +0 -0
  151. package/apps/studio/stickers/life-style/nice.png +0 -0
  152. package/apps/studio/stickers/life-style/objects.png +0 -0
  153. package/apps/studio/stickers/life-style/ok.png +0 -0
  154. package/apps/studio/stickers/life-style/queen.png +0 -0
  155. package/apps/studio/stickers/life-style/star.png +0 -0
  156. package/apps/studio/stickers/life-style/sun.png +0 -0
  157. package/apps/studio/stickers/life-style/water.png +0 -0
  158. package/apps/studio/stickers/life-style/yeah.png +0 -0
  159. package/apps/studio/stickers/life-style/you-can.png +0 -0
  160. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  161. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  162. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  163. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  164. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  165. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  166. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  167. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  168. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  169. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  170. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  171. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  172. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  173. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  174. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  175. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  176. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  177. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  178. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  179. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  180. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  181. package/apps/studio/stickers/project-status/group-100.png +0 -0
  182. package/apps/studio/stickers/project-status/group-101.png +0 -0
  183. package/apps/studio/stickers/project-status/group-102.png +0 -0
  184. package/apps/studio/stickers/project-status/group-103.png +0 -0
  185. package/apps/studio/stickers/project-status/group-104.png +0 -0
  186. package/apps/studio/stickers/project-status/group-105.png +0 -0
  187. package/apps/studio/stickers/project-status/group-106.png +0 -0
  188. package/apps/studio/stickers/project-status/group-107.png +0 -0
  189. package/apps/studio/stickers/project-status/group-108.png +0 -0
  190. package/apps/studio/stickers/project-status/group-109.png +0 -0
  191. package/apps/studio/stickers/project-status/group-110.png +0 -0
  192. package/apps/studio/stickers/project-status/group-111.png +0 -0
  193. package/apps/studio/stickers/project-status/group-112.png +0 -0
  194. package/apps/studio/stickers/project-status/group-113.png +0 -0
  195. package/apps/studio/stickers/project-status/group-114.png +0 -0
  196. package/apps/studio/stickers/project-status/group-115.png +0 -0
  197. package/apps/studio/stickers/project-status/group-117.png +0 -0
  198. package/apps/studio/stickers/project-status/group-118.png +0 -0
  199. package/apps/studio/stickers/project-status/group-119.png +0 -0
  200. package/apps/studio/stickers/project-status/group-120.png +0 -0
  201. package/apps/studio/stickers/project-status/group-121.png +0 -0
  202. package/apps/studio/stickers/project-status/group-122.png +0 -0
  203. package/apps/studio/stickers/project-status/group-41.png +0 -0
  204. package/apps/studio/stickers/project-status/group-42.png +0 -0
  205. package/apps/studio/stickers/project-status/group-43.png +0 -0
  206. package/apps/studio/stickers/project-status/group-44.png +0 -0
  207. package/apps/studio/stickers/project-status/group-45.png +0 -0
  208. package/apps/studio/stickers/project-status/group-46.png +0 -0
  209. package/apps/studio/stickers/project-status/group-47.png +0 -0
  210. package/apps/studio/stickers/project-status/group-48.png +0 -0
  211. package/apps/studio/stickers/project-status/group-49.png +0 -0
  212. package/apps/studio/stickers/project-status/group-50.png +0 -0
  213. package/apps/studio/stickers/project-status/group-51.png +0 -0
  214. package/apps/studio/stickers/project-status/group-52.png +0 -0
  215. package/apps/studio/stickers/project-status/group-53.png +0 -0
  216. package/apps/studio/stickers/project-status/group-54.png +0 -0
  217. package/apps/studio/stickers/project-status/group-55.png +0 -0
  218. package/apps/studio/stickers/project-status/group-56.png +0 -0
  219. package/apps/studio/stickers/project-status/group-57.png +0 -0
  220. package/apps/studio/stickers/project-status/group-58.png +0 -0
  221. package/apps/studio/stickers/project-status/group-59.png +0 -0
  222. package/apps/studio/stickers/project-status/group-60.png +0 -0
  223. package/apps/studio/stickers/project-status/group-61.png +0 -0
  224. package/apps/studio/stickers/project-status/group-62.png +0 -0
  225. package/apps/studio/stickers/project-status/group-63.png +0 -0
  226. package/apps/studio/stickers/project-status/group-64.png +0 -0
  227. package/apps/studio/stickers/project-status/group-65.png +0 -0
  228. package/apps/studio/stickers/project-status/group-66.png +0 -0
  229. package/apps/studio/stickers/project-status/group-67.png +0 -0
  230. package/apps/studio/stickers/project-status/group-68.png +0 -0
  231. package/apps/studio/stickers/project-status/group-69.png +0 -0
  232. package/apps/studio/stickers/project-status/group-70.png +0 -0
  233. package/apps/studio/stickers/project-status/group-71.png +0 -0
  234. package/apps/studio/stickers/project-status/group-72.png +0 -0
  235. package/apps/studio/stickers/project-status/group-73.png +0 -0
  236. package/apps/studio/stickers/project-status/group-74.png +0 -0
  237. package/apps/studio/stickers/project-status/group-75.png +0 -0
  238. package/apps/studio/stickers/project-status/group-76.png +0 -0
  239. package/apps/studio/stickers/project-status/group-77.png +0 -0
  240. package/apps/studio/stickers/project-status/group-78.png +0 -0
  241. package/apps/studio/stickers/project-status/group-79.png +0 -0
  242. package/apps/studio/stickers/project-status/group-80.png +0 -0
  243. package/apps/studio/stickers/project-status/group-81.png +0 -0
  244. package/apps/studio/stickers/project-status/group-82.png +0 -0
  245. package/apps/studio/stickers/project-status/group-83.png +0 -0
  246. package/apps/studio/stickers/project-status/group-84.png +0 -0
  247. package/apps/studio/stickers/project-status/group-85.png +0 -0
  248. package/apps/studio/stickers/project-status/group-86.png +0 -0
  249. package/apps/studio/stickers/project-status/group-87.png +0 -0
  250. package/apps/studio/stickers/project-status/group-88.png +0 -0
  251. package/apps/studio/stickers/project-status/group-89.png +0 -0
  252. package/apps/studio/stickers/project-status/group-90.png +0 -0
  253. package/apps/studio/stickers/project-status/group-91.png +0 -0
  254. package/apps/studio/stickers/project-status/group-92.png +0 -0
  255. package/apps/studio/stickers/project-status/group-93.png +0 -0
  256. package/apps/studio/stickers/project-status/group-94.png +0 -0
  257. package/apps/studio/stickers/project-status/group-96.png +0 -0
  258. package/apps/studio/stickers/project-status/group-97.png +0 -0
  259. package/apps/studio/stickers/project-status/group-98.png +0 -0
  260. package/apps/studio/stickers/project-status/group-99.png +0 -0
  261. package/apps/studio/stickers/project-status/manifest.json +328 -0
  262. package/apps/studio/test/_helpers.ts +15 -2
  263. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  264. package/apps/studio/test/acp-bridge.test.ts +34 -1
  265. package/apps/studio/test/acp-commands.test.ts +2 -1
  266. package/apps/studio/test/acp-env.test.ts +30 -0
  267. package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
  268. package/apps/studio/test/acp-session-plugins.test.ts +72 -1
  269. package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
  270. package/apps/studio/test/annotations-layer.test.ts +174 -0
  271. package/apps/studio/test/asset-api.test.ts +3 -3
  272. package/apps/studio/test/canvas-edit.test.ts +17 -6
  273. package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
  274. package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
  275. package/apps/studio/test/csrf-write-guard.test.ts +23 -1
  276. package/apps/studio/test/design-setup-readiness.test.ts +155 -0
  277. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  278. package/apps/studio/test/edit-source-command.test.ts +23 -0
  279. package/apps/studio/test/element-structural-edit.test.ts +154 -0
  280. package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
  281. package/apps/studio/test/footage-store.test.ts +100 -0
  282. package/apps/studio/test/generate-route.test.ts +106 -0
  283. package/apps/studio/test/git-branches.test.ts +97 -0
  284. package/apps/studio/test/github-api.test.ts +56 -1
  285. package/apps/studio/test/import-asset-browser.test.ts +64 -0
  286. package/apps/studio/test/import-asset-route.test.ts +71 -0
  287. package/apps/studio/test/import-asset.test.ts +399 -0
  288. package/apps/studio/test/import-brand-browser.test.ts +88 -0
  289. package/apps/studio/test/import-brand-route.test.ts +120 -0
  290. package/apps/studio/test/import-brand.test.ts +206 -0
  291. package/apps/studio/test/import-tokens.test.ts +722 -0
  292. package/apps/studio/test/input-router.test.ts +33 -0
  293. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  294. package/apps/studio/test/media-commit-chain.test.ts +210 -0
  295. package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
  296. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  297. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  298. package/apps/studio/test/photo-filters.test.ts +149 -0
  299. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  300. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  301. package/apps/studio/test/read-annotations.test.ts +164 -0
  302. package/apps/studio/test/readiness.test.ts +13 -11
  303. package/apps/studio/test/scaffold-design.test.ts +122 -0
  304. package/apps/studio/test/shell-importmap.test.ts +49 -0
  305. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  306. package/apps/studio/test/timeline-parse.test.ts +57 -0
  307. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  308. package/apps/studio/test/tour-overlay.test.tsx +16 -0
  309. package/apps/studio/test/undo-stack.test.ts +33 -0
  310. package/apps/studio/test/video-asset.test.ts +5 -5
  311. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  312. package/apps/studio/text-caret.ts +239 -0
  313. package/apps/studio/tool-palette.tsx +49 -21
  314. package/apps/studio/ui-prefs.ts +130 -0
  315. package/apps/studio/undo-stack.ts +94 -2
  316. package/apps/studio/use-annotation-resize.tsx +4 -4
  317. package/apps/studio/use-canvas-media-drop.tsx +40 -1
  318. package/apps/studio/use-chrome-visibility.tsx +8 -2
  319. package/apps/studio/use-selection-set.tsx +21 -0
  320. package/apps/studio/video-comp.tsx +48 -7
  321. package/apps/studio/whats-new.json +149 -0
  322. package/apps/studio/ws.ts +5 -0
  323. package/cli/bin/maude.mjs +6 -6
  324. package/cli/commands/design.mjs +67 -3
  325. package/cli/lib/gitignore-block.mjs +2 -0
  326. package/cli/lib/pkg-root.mjs +107 -0
  327. package/cli/lib/pkg-root.test.mjs +79 -0
  328. package/cli/lib/reconstruct-toolset.test.mjs +194 -0
  329. package/cli/lib/update-check.mjs +8 -0
  330. package/package.json +13 -11
  331. package/plugins/design/dependencies.json +18 -0
  332. package/plugins/design/templates/_shell.html +1 -0
@@ -0,0 +1,662 @@
1
+ /**
2
+ * @file footage/schema.ts — Footage-analysis + EDL sidecar data model
3
+ * (feature-footage-analysis-director, Task 1).
4
+ * @scope apps/studio/footage/schema.ts
5
+ * @purpose The single source of truth for the two artifacts the footage
6
+ * "director" pipeline persists (DDR-115 taxonomy: both VERSIONED,
7
+ * alongside the footage they describe — same bucket as `.photo.json`
8
+ * / `.meta.json` / `.annotations.svg`):
9
+ *
10
+ * 1. `assets/<sha8>.footage.json` — a `FootageAnalysis`: what the
11
+ * `footage-analyst` agent SAW when it watched the keyframes of
12
+ * one clip (shots, good-moment ranges, tags, a summary). Per-clip.
13
+ * 2. `<designRoot>/<slug>.edl.json` — an `Edl`: what the
14
+ * `footage-director` agent decided (an ordered beat list — the
15
+ * cut). Per-project-cut.
16
+ *
17
+ * Both are *metadata only* — never pixels, never re-encoded video.
18
+ * The `/design:reel` codegen reads the `Edl` and emits a
19
+ * Timeline-parseable `<TransitionSeries>` video-comp TSX (video-comp
20
+ * SKILL.md is the authoring contract).
21
+ *
22
+ * @invariant DEPENDENCY-FREE. Imported by BOTH the server (`footage-store.ts`,
23
+ * the `/_api/footage` route) and any client/agent surface. MUST NOT
24
+ * import any browser / native / heavy lib. Pure TS types + plain
25
+ * helpers + a hand-rolled structural validator (no Ajv) — the exact
26
+ * house style of `photo/schema.ts`.
27
+ *
28
+ * @time-model Footage time is in **SECONDS** (source-relative), NOT frames: an
29
+ * HTML5 `<video>` (the probe surface) can report `duration` but not a
30
+ * reliable native fps, and the analyst reasons about seconds. The
31
+ * `Edl` composes at a chosen OUTPUT fps and lengths there ARE in
32
+ * frames (Remotion's clock); the director converts source-seconds →
33
+ * output-frames at codegen. Keeping the two clocks separate is
34
+ * deliberate — see the plan's Task-1 note.
35
+ */
36
+
37
+ /** Schema version of a `FootageAnalysis`. Bump on an incompatible shape change. */
38
+ export const FOOTAGE_ANALYSIS_VERSION = 1 as const;
39
+ /** Schema version of an `Edl`. */
40
+ export const EDL_VERSION = 1 as const;
41
+
42
+ /**
43
+ * The six transition presentations Maude bundles from `@remotion/transitions`
44
+ * (video-comp SKILL.md — the ONLY ones that resolve on an end-user install).
45
+ * `none` is a hard cut. The director may only choose from these.
46
+ */
47
+ export type TransitionPresentation = 'none' | 'fade' | 'slide' | 'wipe' | 'flip' | 'clock-wipe';
48
+ export const TRANSITION_PRESENTATIONS: readonly TransitionPresentation[] = [
49
+ 'none',
50
+ 'fade',
51
+ 'slide',
52
+ 'wipe',
53
+ 'flip',
54
+ 'clock-wipe',
55
+ ];
56
+
57
+ /** Coarse shot vocabulary the analyst tags each segment with (advisory). */
58
+ export type ShotKind =
59
+ | 'wide'
60
+ | 'medium'
61
+ | 'close'
62
+ | 'detail'
63
+ | 'establishing'
64
+ | 'action'
65
+ | 'portrait'
66
+ | 'product'
67
+ | 'transition'
68
+ | 'other';
69
+ export const SHOT_KINDS: readonly ShotKind[] = [
70
+ 'wide',
71
+ 'medium',
72
+ 'close',
73
+ 'detail',
74
+ 'establishing',
75
+ 'action',
76
+ 'portrait',
77
+ 'product',
78
+ 'transition',
79
+ 'other',
80
+ ];
81
+
82
+ /** Dominant camera/subject motion the analyst reads across the keyframe run. */
83
+ export type MotionKind =
84
+ | 'static'
85
+ | 'pan'
86
+ | 'tilt'
87
+ | 'push-in'
88
+ | 'pull-out'
89
+ | 'handheld'
90
+ | 'tracking'
91
+ | 'fast'
92
+ | 'other';
93
+ export const MOTION_KINDS: readonly MotionKind[] = [
94
+ 'static',
95
+ 'pan',
96
+ 'tilt',
97
+ 'push-in',
98
+ 'pull-out',
99
+ 'handheld',
100
+ 'tracking',
101
+ 'fast',
102
+ 'other',
103
+ ];
104
+
105
+ /**
106
+ * One shot the analyst found inside a clip. `start`/`end` are SECONDS in the
107
+ * SOURCE clip (0 ≤ start < end ≤ clip duration). `quality` is the analyst's
108
+ * 0..1 confidence that this is a *usable, good* moment; `usable` is the boolean
109
+ * gate the director filters on first.
110
+ */
111
+ export interface FootageShot {
112
+ /** Source in-point, seconds. */
113
+ start: number;
114
+ /** Source out-point, seconds (> start). */
115
+ end: number;
116
+ kind?: ShotKind;
117
+ motion?: MotionKind;
118
+ /** Free-text: who/what is on screen ("drone push over the river"). */
119
+ subject?: string;
120
+ /** Free-text lighting note ("golden hour, backlit"). */
121
+ lighting?: string;
122
+ /** Free-text mood ("calm, aspirational"). */
123
+ mood?: string;
124
+ /** 0..1 — how strong a shot this is for a reel. */
125
+ quality?: number;
126
+ /** The director only considers `usable !== false` shots. */
127
+ usable?: boolean;
128
+ /** One-line director-facing note ("hero shot; hold on the logo reveal"). */
129
+ note?: string;
130
+ }
131
+
132
+ /**
133
+ * The full analysis of one clip. `asset` is the `assets/<sha8>.<ext>` it
134
+ * describes (redundant with the sidecar filename, but self-describing for the
135
+ * director + audit). `durationSec` / `width` / `height` come from the probe
136
+ * (`<video>` metadata). `keyframes` is the count of frames the analyst actually
137
+ * watched (provenance — so a downstream reader knows the sampling density).
138
+ */
139
+ export interface FootageAnalysis {
140
+ version?: number;
141
+ /** `assets/<sha8>.<ext>` — the source this analysis targets. */
142
+ asset?: string;
143
+ durationSec?: number;
144
+ width?: number;
145
+ height?: number;
146
+ /** How many keyframes were sampled to produce this analysis. */
147
+ keyframes?: number;
148
+ shots?: FootageShot[];
149
+ /** One-paragraph character-of-the-clip summary. */
150
+ summary?: string;
151
+ /** Cross-cutting tags ("rebrand", "exterior", "people"). */
152
+ tags?: string[];
153
+ }
154
+
155
+ /** An optional graphic overlay laid over a beat (title card / lower-third / logo). */
156
+ export interface EdlOverlay {
157
+ kind: 'title' | 'lower-third' | 'caption' | 'logo';
158
+ /** Display text (absent for a bare logo). */
159
+ text?: string;
160
+ }
161
+
162
+ /** The transition BETWEEN this beat and the previous one (null on beat 0 / hard cut). */
163
+ export interface EdlTransition {
164
+ presentation: TransitionPresentation;
165
+ /** Overlap length in OUTPUT frames. */
166
+ frames: number;
167
+ }
168
+
169
+ /**
170
+ * One beat of the cut. `clip` is the `assets/<sha8>.<ext>` it draws from;
171
+ * `startSec` is the SOURCE in-point (seconds), `durationFrames` the beat length
172
+ * in OUTPUT frames (at the Edl's `fps`). Two beats may share one `clip` with
173
+ * different `startSec` — pulling multiple shots from one clip is first-class.
174
+ */
175
+ export interface EdlBeat {
176
+ /** `assets/<sha8>.<ext>` — the clip this beat draws from. */
177
+ clip: string;
178
+ /** Source in-point, seconds. */
179
+ startSec: number;
180
+ /** Beat length in OUTPUT frames. */
181
+ durationFrames: number;
182
+ /** Why this shot, here — the director's reasoning (audit + regen). */
183
+ why?: string;
184
+ /** Transition INTO this beat from the previous one (null ⇒ hard cut). */
185
+ transition?: EdlTransition | null;
186
+ /** Optional graphic over the beat. */
187
+ overlay?: EdlOverlay | null;
188
+ /** Stable Timeline identity ("beat-1", "hero", "logo"). */
189
+ name?: string;
190
+ }
191
+
192
+ /** A plain user-dropped music bed laid under the whole cut (NOT analysis — see the plan's scope note). */
193
+ export interface EdlMusic {
194
+ /** `assets/<sha8>.<ext>` audio track. */
195
+ asset: string;
196
+ /** Fade the bed out over the last N output frames. */
197
+ fadeOutFrames?: number;
198
+ }
199
+
200
+ /** feature-ai-media-generation Phase 2 — the role an audio track plays in the cut. */
201
+ export type EdlAudioKind = 'music' | 'voiceover' | 'sfx';
202
+
203
+ /**
204
+ * One audio track in the cut (DDR-164 Phase 2). Generalizes the single `music`
205
+ * bed into a LIST so a reel can layer generated music + voiceover + per-beat
206
+ * SFX. `startFrame`/`durationFrames` place + trim it on the OUTPUT timeline (at
207
+ * the Edl's `fps`); omit both for a whole-reel bed. The codegen emits an
208
+ * `<Audio src="assets/…">` inside a `<Sequence from={startFrame}>`.
209
+ */
210
+ export interface EdlAudioTrack {
211
+ /** `assets/<sha8>.<ext>` audio asset. */
212
+ asset: string;
213
+ /** Role — drives default gain/ducking guidance in the codegen. Default 'music'. */
214
+ kind?: EdlAudioKind;
215
+ /** Where the track enters, OUTPUT frames from the start. Default 0. */
216
+ startFrame?: number;
217
+ /** Trim length in OUTPUT frames. Omit ⇒ play to the end of the source. */
218
+ durationFrames?: number;
219
+ /** Volume adjustment in dB (negative = quieter; e.g. a −12 dB music bed under VO). */
220
+ gainDb?: number;
221
+ fadeInFrames?: number;
222
+ fadeOutFrames?: number;
223
+ /** Stable Timeline identity ("vo", "bed", "whoosh-3"). */
224
+ name?: string;
225
+ }
226
+
227
+ /** One rendered caption cue on the OUTPUT timeline — SECONDS (matches captions.ts). */
228
+ export interface EdlCaptionCue {
229
+ startSec: number;
230
+ endSec: number;
231
+ text: string;
232
+ }
233
+
234
+ /**
235
+ * The caption/subtitle track (DDR-164 Phase 2), fed by `generation/captions.ts`
236
+ * (whisper.cpp local or ElevenLabs Scribe / Groq cloud). Stored as parsed cues
237
+ * (seconds) rather than a raw SRT file so the codegen can render them as overlays
238
+ * without re-parsing; the codegen converts seconds → frames at the Edl's `fps`.
239
+ */
240
+ export interface EdlCaptions {
241
+ cues: EdlCaptionCue[];
242
+ /** Where the caption sits on screen. Default 'lower-third'. */
243
+ style?: 'lower-third' | 'centered' | 'top';
244
+ }
245
+
246
+ /**
247
+ * The edit decision list — the director's output, one per cut. `beats` is the
248
+ * ordered timeline; the composition's total length = sum(durationFrames) −
249
+ * sum(transition overlaps).
250
+ */
251
+ export interface Edl {
252
+ version?: number;
253
+ title?: string;
254
+ /** Output composition fps (default 30). */
255
+ fps?: number;
256
+ /** Output composition dimensions. */
257
+ width?: number;
258
+ height?: number;
259
+ beats?: EdlBeat[];
260
+ /** A single whole-reel music bed (kept for backwards compatibility). */
261
+ music?: EdlMusic | null;
262
+ /** feature-ai-media-generation Phase 2 — layered audio (music/VO/SFX). */
263
+ audioTracks?: EdlAudioTrack[];
264
+ /** feature-ai-media-generation Phase 2 — the caption/subtitle track. */
265
+ captions?: EdlCaptions | null;
266
+ }
267
+
268
+ // ── shared helpers ───────────────────────────────────────────────────────────
269
+
270
+ const ASSET_REL_RE = /^assets\/[0-9a-f]{8,}[A-Za-z0-9._-]*$/;
271
+ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
272
+ typeof v === 'object' && v !== null && !Array.isArray(v);
273
+
274
+ /** True when the analysis has nothing usable in it (no shots, or none usable). */
275
+ export function isEmptyAnalysis(a?: FootageAnalysis | null): boolean {
276
+ if (!a || !a.shots || a.shots.length === 0) return true;
277
+ return !a.shots.some((s) => s.usable !== false);
278
+ }
279
+
280
+ /** A fresh, empty analysis for `asset`. */
281
+ export function emptyFootageAnalysis(asset?: string): FootageAnalysis {
282
+ return { version: FOOTAGE_ANALYSIS_VERSION, ...(asset ? { asset } : {}), shots: [] };
283
+ }
284
+
285
+ /** The provenance tag every AI-generated clip's sidecar carries. The
286
+ * footage-analyst preserves it when it enriches the stub with real shots. It is
287
+ * an ADVISORY hint, not a trust boundary — the sidecar is versioned + peer-
288
+ * editable (DDR-054), so a peer can add/strip it; nothing security-relevant keys
289
+ * off it (it only labels a beat in the UI). */
290
+ export const AI_GENERATED_TAG = 'ai-generated';
291
+
292
+ /**
293
+ * A provenance `FootageAnalysis` stub for an AI-GENERATED clip (DDR-164 Phase 3,
294
+ * Task 3.2). Written by the `/_api/generate-jobs` route the moment a video job
295
+ * lands, so the generated clip is immediately KNOWN to the footage pipeline —
296
+ * `getAnalysis` returns non-null and the reel director can see the clip is
297
+ * synthetic (the `ai-generated` tag) and read what it was generated FOR (the
298
+ * prompt, as the summary) without re-watching.
299
+ *
300
+ * It carries NO shots on purpose: the `footage-analyst` still probes the
301
+ * keyframes and fills the real shot list (so `isEmptyAnalysis` is true and the
302
+ * reel's shot-aware cache re-analyzes it), but the provenance tag + summary
303
+ * survive. Duration/dimensions are unknown until that probe, so they are omitted.
304
+ */
305
+ export function generatedClipAnalysis(
306
+ asset: string,
307
+ provenance: { provider?: string; model?: string; prompt?: string } = {}
308
+ ): FootageAnalysis {
309
+ const tags = [AI_GENERATED_TAG];
310
+ if (provenance.provider) tags.push(provenance.provider);
311
+ const label = [provenance.provider, provenance.model].filter(Boolean).join(' · ');
312
+ // This is the LOCAL generation request's prompt (not a peer file), but its text
313
+ // becomes the sidecar `summary`, which IS versioned + peer-syncable (DDR-054)
314
+ // and read by the footage agents downstream. Map C0/C1 control chars to spaces
315
+ // (matching generation's sanitizeReuseText) so it can't fabricate multi-line
316
+ // instructions, then collapse whitespace; bounded to the summary cap below. The
317
+ // agents' own "treat sidecar text as data" posture is the mitigation for
318
+ // arbitrary PEER-authored summaries — this only cleans what we write.
319
+ const prompt = [...(provenance.prompt ?? '')]
320
+ .map((ch) => {
321
+ const c = ch.charCodeAt(0);
322
+ return (c >= 0x00 && c <= 0x1f) || (c >= 0x7f && c <= 0x9f) ? ' ' : ch;
323
+ })
324
+ .join('')
325
+ .replace(/\s+/g, ' ')
326
+ .trim();
327
+ let summary = `AI-generated clip${label ? ` (${label})` : ''}${prompt ? ` — ${prompt}` : ''}`;
328
+ if (summary.length > 3900) summary = `${summary.slice(0, 3900)}…`;
329
+ return {
330
+ version: FOOTAGE_ANALYSIS_VERSION,
331
+ asset,
332
+ keyframes: 0,
333
+ shots: [],
334
+ summary,
335
+ tags,
336
+ };
337
+ }
338
+
339
+ // ── structural validation (dependency-free — no Ajv) ─────────────────────────
340
+ // Mirrors photo/schema.ts: the `/_api/footage` route validates untrusted JSON
341
+ // with these before persisting. Unknown keys / wrong types / out-of-range
342
+ // numbers / non-relative asset paths all fail — no crafted field round-trips.
343
+
344
+ export interface ValidationResult {
345
+ ok: boolean;
346
+ errors: string[];
347
+ }
348
+
349
+ function num(
350
+ errors: string[],
351
+ obj: Record<string, unknown>,
352
+ key: string,
353
+ lo: number,
354
+ hi: number,
355
+ where: string
356
+ ) {
357
+ if (!(key in obj) || obj[key] == null) return;
358
+ const v = obj[key];
359
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
360
+ errors.push(`${where}.${key}: must be a finite number`);
361
+ return;
362
+ }
363
+ if (v < lo || v > hi) errors.push(`${where}.${key}: ${v} out of range [${lo}, ${hi}]`);
364
+ }
365
+
366
+ function intGe(
367
+ errors: string[],
368
+ obj: Record<string, unknown>,
369
+ key: string,
370
+ lo: number,
371
+ where: string
372
+ ) {
373
+ if (!(key in obj) || obj[key] == null) return;
374
+ const v = obj[key];
375
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < lo)
376
+ errors.push(`${where}.${key}: must be an integer ≥ ${lo}`);
377
+ }
378
+
379
+ function bool(errors: string[], obj: Record<string, unknown>, key: string, where: string) {
380
+ if (key in obj && obj[key] != null && typeof obj[key] !== 'boolean')
381
+ errors.push(`${where}.${key}: must be a boolean`);
382
+ }
383
+
384
+ function str(
385
+ errors: string[],
386
+ obj: Record<string, unknown>,
387
+ key: string,
388
+ maxLen: number,
389
+ where: string
390
+ ) {
391
+ if (!(key in obj) || obj[key] == null) return;
392
+ const v = obj[key];
393
+ if (typeof v !== 'string') errors.push(`${where}.${key}: must be a string`);
394
+ else if (v.length > maxLen) errors.push(`${where}.${key}: exceeds ${maxLen} chars`);
395
+ }
396
+
397
+ function assetRel(errors: string[], obj: Record<string, unknown>, key: string, where: string) {
398
+ if (!(key in obj) || obj[key] == null) return;
399
+ const v = obj[key];
400
+ if (typeof v !== 'string' || !ASSET_REL_RE.test(v))
401
+ errors.push(`${where}.${key}: must be a relative assets/<sha8>.<ext> path`);
402
+ }
403
+
404
+ function assertKeys(
405
+ errors: string[],
406
+ obj: Record<string, unknown>,
407
+ allowed: string[],
408
+ where: string
409
+ ) {
410
+ for (const k of Object.keys(obj))
411
+ if (!allowed.includes(k)) errors.push(`${where}: unknown key "${k}"`);
412
+ }
413
+
414
+ function strArray(
415
+ errors: string[],
416
+ obj: Record<string, unknown>,
417
+ key: string,
418
+ maxItems: number,
419
+ where: string
420
+ ) {
421
+ if (!(key in obj) || obj[key] == null) return;
422
+ const v = obj[key];
423
+ if (!Array.isArray(v)) {
424
+ errors.push(`${where}.${key}: must be an array`);
425
+ return;
426
+ }
427
+ if (v.length > maxItems) errors.push(`${where}.${key}: more than ${maxItems} items`);
428
+ for (const s of v)
429
+ if (typeof s !== 'string') errors.push(`${where}.${key}: items must be strings`);
430
+ }
431
+
432
+ /** Structurally validate an untrusted value as a `FootageAnalysis`. */
433
+ export function validateFootageAnalysis(input: unknown): ValidationResult {
434
+ const errors: string[] = [];
435
+ if (!isPlainObject(input)) return { ok: false, errors: ['root: must be an object'] };
436
+
437
+ assertKeys(
438
+ errors,
439
+ input,
440
+ ['version', 'asset', 'durationSec', 'width', 'height', 'keyframes', 'shots', 'summary', 'tags'],
441
+ 'root'
442
+ );
443
+ if ('version' in input && input.version != null && typeof input.version !== 'number')
444
+ errors.push('version: must be a number');
445
+ assetRel(errors, input, 'asset', 'root');
446
+ num(errors, input, 'durationSec', 0, 24 * 3600, 'root');
447
+ num(errors, input, 'width', 0, 100000, 'root');
448
+ num(errors, input, 'height', 0, 100000, 'root');
449
+ intGe(errors, input, 'keyframes', 0, 'root');
450
+ str(errors, input, 'summary', 4000, 'root');
451
+ strArray(errors, input, 'tags', 64, 'root');
452
+
453
+ const dur = typeof input.durationSec === 'number' ? input.durationSec : Infinity;
454
+ if ('shots' in input && input.shots != null) {
455
+ if (!Array.isArray(input.shots)) errors.push('shots: must be an array');
456
+ else if (input.shots.length > 512) errors.push('shots: more than 512 items');
457
+ else
458
+ input.shots.forEach((raw, i) => {
459
+ const where = `shots[${i}]`;
460
+ if (!isPlainObject(raw)) {
461
+ errors.push(`${where}: must be an object`);
462
+ return;
463
+ }
464
+ assertKeys(
465
+ errors,
466
+ raw,
467
+ [
468
+ 'start',
469
+ 'end',
470
+ 'kind',
471
+ 'motion',
472
+ 'subject',
473
+ 'lighting',
474
+ 'mood',
475
+ 'quality',
476
+ 'usable',
477
+ 'note',
478
+ ],
479
+ where
480
+ );
481
+ num(errors, raw, 'start', 0, 24 * 3600, where);
482
+ num(errors, raw, 'end', 0, 24 * 3600, where);
483
+ if (typeof raw.start === 'number' && typeof raw.end === 'number' && !(raw.end > raw.start))
484
+ errors.push(`${where}: end must be > start`);
485
+ if (typeof raw.end === 'number' && raw.end > dur + 0.5)
486
+ errors.push(`${where}: end ${raw.end}s exceeds clip duration ${dur}s`);
487
+ if ('kind' in raw && raw.kind != null && !SHOT_KINDS.includes(raw.kind as ShotKind))
488
+ errors.push(`${where}.kind: must be one of ${SHOT_KINDS.join(', ')}`);
489
+ if (
490
+ 'motion' in raw &&
491
+ raw.motion != null &&
492
+ !MOTION_KINDS.includes(raw.motion as MotionKind)
493
+ )
494
+ errors.push(`${where}.motion: must be one of ${MOTION_KINDS.join(', ')}`);
495
+ str(errors, raw, 'subject', 500, where);
496
+ str(errors, raw, 'lighting', 500, where);
497
+ str(errors, raw, 'mood', 500, where);
498
+ str(errors, raw, 'note', 1000, where);
499
+ num(errors, raw, 'quality', 0, 1, where);
500
+ bool(errors, raw, 'usable', where);
501
+ });
502
+ }
503
+
504
+ return { ok: errors.length === 0, errors };
505
+ }
506
+
507
+ function validateOverlay(errors: string[], raw: unknown, where: string) {
508
+ if (raw == null) return;
509
+ if (!isPlainObject(raw)) {
510
+ errors.push(`${where}: must be an object`);
511
+ return;
512
+ }
513
+ assertKeys(errors, raw, ['kind', 'text'], where);
514
+ const kinds = ['title', 'lower-third', 'caption', 'logo'];
515
+ if (!kinds.includes(raw.kind as string))
516
+ errors.push(`${where}.kind: must be one of ${kinds.join(', ')}`);
517
+ str(errors, raw, 'text', 500, where);
518
+ }
519
+
520
+ function validateTransition(errors: string[], raw: unknown, where: string) {
521
+ if (raw == null) return;
522
+ if (!isPlainObject(raw)) {
523
+ errors.push(`${where}: must be an object`);
524
+ return;
525
+ }
526
+ assertKeys(errors, raw, ['presentation', 'frames'], where);
527
+ if (!TRANSITION_PRESENTATIONS.includes(raw.presentation as TransitionPresentation))
528
+ errors.push(`${where}.presentation: must be one of ${TRANSITION_PRESENTATIONS.join(', ')}`);
529
+ intGe(errors, raw, 'frames', 0, where);
530
+ }
531
+
532
+ /** Structurally validate an untrusted value as an `Edl`. */
533
+ export function validateEdl(input: unknown): ValidationResult {
534
+ const errors: string[] = [];
535
+ if (!isPlainObject(input)) return { ok: false, errors: ['root: must be an object'] };
536
+
537
+ assertKeys(
538
+ errors,
539
+ input,
540
+ ['version', 'title', 'fps', 'width', 'height', 'beats', 'music', 'audioTracks', 'captions'],
541
+ 'root'
542
+ );
543
+ if ('version' in input && input.version != null && typeof input.version !== 'number')
544
+ errors.push('version: must be a number');
545
+ str(errors, input, 'title', 300, 'root');
546
+ num(errors, input, 'fps', 1, 120, 'root');
547
+ num(errors, input, 'width', 1, 100000, 'root');
548
+ num(errors, input, 'height', 1, 100000, 'root');
549
+
550
+ if ('beats' in input && input.beats != null) {
551
+ if (!Array.isArray(input.beats)) errors.push('beats: must be an array');
552
+ else if (input.beats.length > 512) errors.push('beats: more than 512 items');
553
+ else
554
+ input.beats.forEach((raw, i) => {
555
+ const where = `beats[${i}]`;
556
+ if (!isPlainObject(raw)) {
557
+ errors.push(`${where}: must be an object`);
558
+ return;
559
+ }
560
+ assertKeys(
561
+ errors,
562
+ raw,
563
+ ['clip', 'startSec', 'durationFrames', 'why', 'transition', 'overlay', 'name'],
564
+ where
565
+ );
566
+ assetRel(errors, raw, 'clip', where);
567
+ if (!('clip' in raw) || raw.clip == null) errors.push(`${where}.clip: required`);
568
+ num(errors, raw, 'startSec', 0, 24 * 3600, where);
569
+ intGe(errors, raw, 'durationFrames', 1, where);
570
+ str(errors, raw, 'why', 1000, where);
571
+ str(errors, raw, 'name', 120, where);
572
+ if ('transition' in raw) validateTransition(errors, raw.transition, `${where}.transition`);
573
+ if ('overlay' in raw) validateOverlay(errors, raw.overlay, `${where}.overlay`);
574
+ });
575
+ }
576
+
577
+ if ('music' in input && input.music != null) {
578
+ const m = input.music;
579
+ if (!isPlainObject(m)) errors.push('music: must be an object');
580
+ else {
581
+ assertKeys(errors, m, ['asset', 'fadeOutFrames'], 'music');
582
+ assetRel(errors, m, 'asset', 'music');
583
+ if (!('asset' in m) || m.asset == null) errors.push('music.asset: required');
584
+ intGe(errors, m, 'fadeOutFrames', 0, 'music');
585
+ }
586
+ }
587
+
588
+ // feature-ai-media-generation Phase 2 — layered audio tracks (music/VO/SFX).
589
+ if ('audioTracks' in input && input.audioTracks != null) {
590
+ if (!Array.isArray(input.audioTracks)) errors.push('audioTracks: must be an array');
591
+ else if (input.audioTracks.length > 64) errors.push('audioTracks: more than 64 items');
592
+ else
593
+ input.audioTracks.forEach((raw, i) => {
594
+ const where = `audioTracks[${i}]`;
595
+ if (!isPlainObject(raw)) {
596
+ errors.push(`${where}: must be an object`);
597
+ return;
598
+ }
599
+ assertKeys(
600
+ errors,
601
+ raw,
602
+ [
603
+ 'asset',
604
+ 'kind',
605
+ 'startFrame',
606
+ 'durationFrames',
607
+ 'gainDb',
608
+ 'fadeInFrames',
609
+ 'fadeOutFrames',
610
+ 'name',
611
+ ],
612
+ where
613
+ );
614
+ assetRel(errors, raw, 'asset', where);
615
+ if (!('asset' in raw) || raw.asset == null) errors.push(`${where}.asset: required`);
616
+ if (
617
+ 'kind' in raw &&
618
+ raw.kind != null &&
619
+ !['music', 'voiceover', 'sfx'].includes(raw.kind as string)
620
+ )
621
+ errors.push(`${where}.kind: must be music|voiceover|sfx`);
622
+ intGe(errors, raw, 'startFrame', 0, where);
623
+ intGe(errors, raw, 'durationFrames', 1, where);
624
+ num(errors, raw, 'gainDb', -60, 24, where);
625
+ intGe(errors, raw, 'fadeInFrames', 0, where);
626
+ intGe(errors, raw, 'fadeOutFrames', 0, where);
627
+ str(errors, raw, 'name', 120, where);
628
+ });
629
+ }
630
+
631
+ // feature-ai-media-generation Phase 2 — the caption/subtitle track.
632
+ if ('captions' in input && input.captions != null) {
633
+ const c = input.captions;
634
+ if (!isPlainObject(c)) errors.push('captions: must be an object');
635
+ else {
636
+ assertKeys(errors, c, ['cues', 'style'], 'captions');
637
+ if (
638
+ 'style' in c &&
639
+ c.style != null &&
640
+ !['lower-third', 'centered', 'top'].includes(c.style as string)
641
+ )
642
+ errors.push('captions.style: must be lower-third|centered|top');
643
+ if (!('cues' in c) || !Array.isArray(c.cues)) errors.push('captions.cues: must be an array');
644
+ else if (c.cues.length > 5000) errors.push('captions.cues: more than 5000 items');
645
+ else
646
+ c.cues.forEach((raw, i) => {
647
+ const where = `captions.cues[${i}]`;
648
+ if (!isPlainObject(raw)) {
649
+ errors.push(`${where}: must be an object`);
650
+ return;
651
+ }
652
+ assertKeys(errors, raw, ['startSec', 'endSec', 'text'], where);
653
+ num(errors, raw, 'startSec', 0, 24 * 3600, where);
654
+ num(errors, raw, 'endSec', 0, 24 * 3600, where);
655
+ str(errors, raw, 'text', 2000, where);
656
+ if (!('text' in raw) || raw.text == null) errors.push(`${where}.text: required`);
657
+ });
658
+ }
659
+ }
660
+
661
+ return { ok: errors.length === 0, errors };
662
+ }