@cueframe/kernel 0.0.1 → 0.1.1

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 (432) hide show
  1. package/LICENSE +661 -0
  2. package/dist/_members/account-model/testing/conversationStoreContract.d.ts +1 -1
  3. package/dist/_members/authoring/providers/resourceApi.d.ts +1 -1
  4. package/dist/_members/authoring/transpileComponent.d.ts +1 -1
  5. package/dist/_members/cloud-import/CloudImportService.d.ts +35 -0
  6. package/dist/_members/cloud-import/ImportProvider.d.ts +73 -0
  7. package/dist/_members/cloud-import/MediaItemPreprocessor.d.ts +27 -0
  8. package/dist/_members/cloud-import/cloudConfig.d.ts +23 -0
  9. package/dist/_members/cloud-import/index.d.ts +9 -0
  10. package/dist/_members/cloud-import/multipartUpload.d.ts +12 -0
  11. package/dist/_members/composition/ops/compositionOps.d.ts +2 -1
  12. package/dist/_members/composition/ops/types.d.ts +1 -1
  13. package/dist/_members/composition/projection/behind-readiness.d.ts +127 -0
  14. package/dist/_members/composition/projection/index.d.ts +2 -0
  15. package/dist/_members/composition/projection/reframe-readiness.d.ts +156 -0
  16. package/dist/_members/composition/timeline/placement.d.ts +1 -1
  17. package/dist/_members/composition/validators/composition.d.ts +43 -26
  18. package/dist/_members/composition/validators/render.d.ts +32 -0
  19. package/dist/_members/composition-ops/index.d.ts +1 -1
  20. package/dist/_members/composition-ops/opRecorder.d.ts +1 -1
  21. package/dist/_members/convex-api/api.d.ts +5 -0
  22. package/dist/_members/convex-api/dataModel.d.ts +6 -0
  23. package/dist/_members/core/render/compositionRender.d.ts +17 -1
  24. package/dist/_members/core/render/exportSettings.d.ts +1 -1
  25. package/dist/_members/core/render/types.d.ts +31 -0
  26. package/dist/_members/mcp/curation.d.ts +1 -1
  27. package/dist/_members/plane/conformance.d.ts +1 -1
  28. package/dist/_members/render-harness/localRender.d.ts +1 -1
  29. package/dist/animate.js +828 -79187
  30. package/dist/authoring/build-tools.js +8 -11
  31. package/dist/authoring/context.js +5 -5
  32. package/dist/authoring/models.js +1 -2
  33. package/dist/authoring/prompts.js +1 -1
  34. package/dist/authoring/tools.js +7 -7
  35. package/dist/authoring/transpileComponent.js +3 -2220
  36. package/dist/authoring.js +30 -30
  37. package/dist/brand-derivation.js +1 -1
  38. package/dist/chunk-6IU2UQI5.js +107 -0
  39. package/dist/{chunk-5I7VDCAC.js → chunk-7FO3ZJ43.js} +4 -1
  40. package/dist/{chunk-N67PDTKQ.js → chunk-ABL557OT.js} +1 -1
  41. package/dist/{chunk-MC5DLSEV.js → chunk-AP6ZIVAV.js} +15 -15
  42. package/dist/{chunk-QLAQHR3L.js → chunk-AQBREB5O.js} +1 -1
  43. package/dist/{chunk-DSNG6WLU.js → chunk-AYVJXDLB.js} +1 -4
  44. package/dist/{chunk-6CMVBTTP.js → chunk-DK2ODBNT.js} +19 -9
  45. package/dist/chunk-DSMLHYJ4.js +82 -0
  46. package/dist/{chunk-BRKJKB33.js → chunk-FYZVH3MC.js} +1 -1
  47. package/dist/{chunk-MVEE6HEC.js → chunk-GA23TRNR.js} +5 -7
  48. package/dist/chunk-HCCBDJLW.js +68 -0
  49. package/dist/{chunk-JRQVPFLF.js → chunk-HY3RIEZW.js} +1 -1
  50. package/dist/{chunk-OIRUO5P7.js → chunk-JJ6XD7NG.js} +152 -2
  51. package/dist/{_members/primitives/chunk-IDAL6G3Y.js → chunk-JQMPHKLU.js} +19 -27
  52. package/dist/{chunk-G3PBH53P.js → chunk-MBCXCJOE.js} +1 -1
  53. package/dist/{chunk-UEI3NLNK.js → chunk-MHOQTPOB.js} +1 -4
  54. package/dist/{chunk-37NJ27UU.js → chunk-OCFDUCF4.js} +3 -0
  55. package/dist/{chunk-WZTTZHNR.js → chunk-OLFOPQCA.js} +12 -9
  56. package/dist/{chunk-P43IE4WB.js → chunk-P727GZVO.js} +2 -4
  57. package/dist/{chunk-52AHXNKM.js → chunk-PDYO3DOJ.js} +5 -5
  58. package/dist/{chunk-S3P5BFFL.js → chunk-PMY24FUD.js} +2 -4
  59. package/dist/{chunk-ATAF24QG.js → chunk-PODIYN2K.js} +10 -3
  60. package/dist/chunk-RF7U3SCL.js +606 -0
  61. package/dist/{chunk-JD77Y2WM.js → chunk-SW336CJP.js} +1 -4
  62. package/dist/{chunk-TULRCY6U.js → chunk-TOJEXFXL.js} +1 -1
  63. package/dist/{chunk-ELLSET2N.js → chunk-UHGSZZDI.js} +3 -6
  64. package/dist/{chunk-M3I5LMAB.js → chunk-UY4SWJVE.js} +1 -1
  65. package/dist/{chunk-6QJ3V3VS.js → chunk-YX2GSJK7.js} +1 -1
  66. package/dist/chunk-ZT7MHRQU.js +104 -0
  67. package/dist/{chunk-NV4I6GJV.js → chunk-ZWMV6DD2.js} +27 -4
  68. package/dist/cloud-import/multipart.d.ts +1 -0
  69. package/dist/cloud-import/multipart.js +8 -0
  70. package/dist/cloud-import.d.ts +1 -0
  71. package/dist/cloud-import.js +234 -0
  72. package/dist/compose/renderer.js +19 -22
  73. package/dist/compose.js +19 -22
  74. package/dist/composition/graphics-plan.js +3 -3
  75. package/dist/composition/ops.js +4 -4
  76. package/dist/composition/primitive-catalog.js +1 -1
  77. package/dist/composition/projection.js +21 -5
  78. package/dist/composition/validators.js +6 -4
  79. package/dist/composition-ops.js +15 -13
  80. package/dist/composition.js +26 -8
  81. package/dist/{context-CEAKQ5T7.js → context-YWUZDG5X.js} +5 -5
  82. package/dist/convex-api/dataModel.d.ts +1 -0
  83. package/dist/convex-api/dataModel.js +0 -0
  84. package/dist/convex-api.d.ts +1 -0
  85. package/dist/convex-api.js +8 -0
  86. package/dist/core/render.js +3 -3
  87. package/dist/core/select.js +2 -2
  88. package/dist/core.js +3 -3
  89. package/dist/errors.js +1 -1
  90. package/dist/mcp/adapter.js +2 -5
  91. package/dist/mcp/server.js +1 -2
  92. package/dist/mcp.js +2 -95
  93. package/dist/media-model/media-kind.js +9 -98
  94. package/dist/media-model/testing.js +1071 -718
  95. package/dist/nle-export/server.js +328 -130
  96. package/dist/nle-export/xml.js +5 -22799
  97. package/dist/nle-export.js +20 -22
  98. package/dist/primitives/backgrounds.js +2 -3
  99. package/dist/primitives/captions.js +4 -243
  100. package/dist/primitives/effects.js +2 -3
  101. package/dist/primitives/hero-text.js +2 -3
  102. package/dist/primitives/layouts.js +2 -3
  103. package/dist/primitives/lib/index.js +8 -10
  104. package/dist/primitives/lib/preview.js +1 -2
  105. package/dist/primitives/lib/schema.js +1 -2
  106. package/dist/primitives/lib/text/fitText.js +1 -2
  107. package/dist/primitives/lib.js +8 -10
  108. package/dist/primitives/scenes.js +1 -2
  109. package/dist/primitives/text.js +2 -3
  110. package/dist/primitives/ui-blocks.js +4 -5
  111. package/dist/primitives.js +19 -21
  112. package/package.json +125 -16
  113. package/dist/TextureLoader-UQOFUSAP.js +0 -422
  114. package/dist/VideoTexture-AQWSPIJG.js +0 -62
  115. package/dist/_members/account-model/index.js +0 -4
  116. package/dist/_members/account-model/testing/conversationStoreContract.js +0 -168
  117. package/dist/_members/account-model/testing/index.js +0 -7
  118. package/dist/_members/account-model/testing/restorePointStoreContract.js +0 -69
  119. package/dist/_members/account-model/types/index.js +0 -1
  120. package/dist/_members/account-model/validators/contextFile.js +0 -18
  121. package/dist/_members/account-model/validators/conversation.js +0 -95
  122. package/dist/_members/account-model/validators/dashboardInsight.js +0 -16
  123. package/dist/_members/account-model/validators/index.js +0 -7
  124. package/dist/_members/account-model/validators/sessionToken.js +0 -49
  125. package/dist/_members/account-model/validators/uploadToken.js +0 -16
  126. package/dist/_members/account-model/validators/userSettings.js +0 -12
  127. package/dist/_members/account-model/validators/workingComposition.js +0 -20
  128. package/dist/_members/brand-derivation/constants.js +0 -20
  129. package/dist/_members/brand-derivation/contrastSafe.js +0 -114
  130. package/dist/_members/brand-derivation/index.js +0 -1
  131. package/dist/_members/composition/audio/audioMix.js +0 -61
  132. package/dist/_members/composition/audio/index.js +0 -5
  133. package/dist/_members/composition/audio/mixStaging.js +0 -195
  134. package/dist/_members/composition/audio/musicPack.generated.js +0 -136
  135. package/dist/_members/composition/audio/musicPack.js +0 -36
  136. package/dist/_members/composition/audio/pack.generated.js +0 -603
  137. package/dist/_members/composition/audio/pack.js +0 -48
  138. package/dist/_members/composition/audio/palette.js +0 -134
  139. package/dist/_members/composition/autozoom/index.js +0 -83
  140. package/dist/_members/composition/brand-kit.js +0 -45
  141. package/dist/_members/composition/brief/contract.js +0 -209
  142. package/dist/_members/composition/color-grade.js +0 -80
  143. package/dist/_members/composition/depth.js +0 -8
  144. package/dist/_members/composition/fixtures/index.js +0 -143
  145. package/dist/_members/composition/fixtures/vaayaErrored.d.ts +0 -25
  146. package/dist/_members/composition/fixtures/vaayaErrored.js +0 -203
  147. package/dist/_members/composition/fonts.js +0 -162
  148. package/dist/_members/composition/graphics-plan/compile.js +0 -221
  149. package/dist/_members/composition/graphics-plan/index.js +0 -10
  150. package/dist/_members/composition/graphics-plan/instantiateMotif.js +0 -44
  151. package/dist/_members/composition/graphics-plan/motion.js +0 -66
  152. package/dist/_members/composition/graphics-plan/schemas.js +0 -261
  153. package/dist/_members/composition/graphics-plan/solve.js +0 -145
  154. package/dist/_members/composition/index.js +0 -5
  155. package/dist/_members/composition/invariants/clipPlacement.js +0 -69
  156. package/dist/_members/composition/invariants/composition.js +0 -28
  157. package/dist/_members/composition/invariants/errors.js +0 -10
  158. package/dist/_members/composition/invariants/etag.js +0 -38
  159. package/dist/_members/composition/invariants/ids.js +0 -33
  160. package/dist/_members/composition/invariants/index.js +0 -9
  161. package/dist/_members/composition/invariants/keyframes.js +0 -45
  162. package/dist/_members/composition/invariants/markers.js +0 -29
  163. package/dist/_members/composition/invariants/time.js +0 -20
  164. package/dist/_members/composition/invariants/tracks.js +0 -304
  165. package/dist/_members/composition/ops/compositionOps.js +0 -1056
  166. package/dist/_members/composition/ops/headlessStore.js +0 -94
  167. package/dist/_members/composition/ops/index.js +0 -4
  168. package/dist/_members/composition/ops/selectors.js +0 -118
  169. package/dist/_members/composition/ops/timeline-word.js +0 -1
  170. package/dist/_members/composition/ops/transcript.js +0 -77
  171. package/dist/_members/composition/ops/types.js +0 -16
  172. package/dist/_members/composition/primitive-catalog/catalogQuery.js +0 -150
  173. package/dist/_members/composition/primitive-catalog/disabled.js +0 -32
  174. package/dist/_members/composition/primitive-catalog/generated/meta.js +0 -15161
  175. package/dist/_members/composition/primitive-catalog/index.js +0 -46
  176. package/dist/_members/composition/primitive-catalog/paramValidation.js +0 -231
  177. package/dist/_members/composition/primitive-catalog/projection.js +0 -164
  178. package/dist/_members/composition/primitive-catalog/types.js +0 -14
  179. package/dist/_members/composition/projection/behind-placement.js +0 -62
  180. package/dist/_members/composition/projection/brandKit.js +0 -482
  181. package/dist/_members/composition/projection/cardTokens.js +0 -209
  182. package/dist/_members/composition/projection/composition-floor.js +0 -582
  183. package/dist/_members/composition/projection/cropProjection.js +0 -257
  184. package/dist/_members/composition/projection/index.js +0 -7
  185. package/dist/_members/composition/projection/occlusion-budget.js +0 -104
  186. package/dist/_members/composition/projection/safe-zones.js +0 -99
  187. package/dist/_members/composition/reframe/aspectLoss.js +0 -115
  188. package/dist/_members/composition/reframe/coverage.js +0 -62
  189. package/dist/_members/composition/reframe/derive.js +0 -73
  190. package/dist/_members/composition/reframe/index.js +0 -109
  191. package/dist/_members/composition/reframe/kenBurns.js +0 -64
  192. package/dist/_members/composition/reframe/shotScale.js +0 -176
  193. package/dist/_members/composition/reframe/trajectory.js +0 -124
  194. package/dist/_members/composition/timeline/compound.js +0 -134
  195. package/dist/_members/composition/timeline/index.js +0 -9
  196. package/dist/_members/composition/timeline/intent.js +0 -228
  197. package/dist/_members/composition/timeline/keyframes.js +0 -121
  198. package/dist/_members/composition/timeline/link.js +0 -59
  199. package/dist/_members/composition/timeline/occupancy.js +0 -36
  200. package/dist/_members/composition/timeline/placement.js +0 -74
  201. package/dist/_members/composition/timeline/plan.js +0 -31
  202. package/dist/_members/composition/timeline/resolve.js +0 -170
  203. package/dist/_members/composition/timeline/trims.js +0 -278
  204. package/dist/_members/composition/types/index.js +0 -1
  205. package/dist/_members/composition/validators/brandKit.js +0 -295
  206. package/dist/_members/composition/validators/composePlan.js +0 -113
  207. package/dist/_members/composition/validators/composition.js +0 -703
  208. package/dist/_members/composition/validators/font.js +0 -34
  209. package/dist/_members/composition/validators/graphicKinds.js +0 -30
  210. package/dist/_members/composition/validators/index.js +0 -9
  211. package/dist/_members/composition/validators/project.js +0 -29
  212. package/dist/_members/composition/validators/registry.js +0 -41
  213. package/dist/_members/composition/validators/render.js +0 -139
  214. package/dist/_members/composition/validators/session.js +0 -37
  215. package/dist/_members/composition/validators/webhook.js +0 -72
  216. package/dist/_members/composition-ops/alphaLayers.js +0 -141
  217. package/dist/_members/composition-ops/index.js +0 -637
  218. package/dist/_members/composition-ops/materializeGrid.js +0 -99
  219. package/dist/_members/composition-ops/opRecorder.js +0 -196
  220. package/dist/_members/composition-ops/recordingStore.js +0 -14
  221. package/dist/_members/composition-ops/replaceRegion.js +0 -102
  222. package/dist/_members/composition-ops/testFixtures.js +0 -19
  223. package/dist/_members/core/index.js +0 -5
  224. package/dist/_members/core/render/bookends.js +0 -53
  225. package/dist/_members/core/render/captions.js +0 -127
  226. package/dist/_members/core/render/compositionRender.js +0 -1249
  227. package/dist/_members/core/render/deriveTrajectory.js +0 -889
  228. package/dist/_members/core/render/emphasis.js +0 -16
  229. package/dist/_members/core/render/eval/cropBox.js +0 -139
  230. package/dist/_members/core/render/eval/inclusion.js +0 -96
  231. package/dist/_members/core/render/eval/index.js +0 -146
  232. package/dist/_members/core/render/eval/metrics.js +0 -94
  233. package/dist/_members/core/render/exportSettings.js +0 -65
  234. package/dist/_members/core/render/index.js +0 -20
  235. package/dist/_members/core/render/mapFaceToPerson.js +0 -73
  236. package/dist/_members/core/render/types.js +0 -8
  237. package/dist/_members/core/render/vcam/cameraPath.js +0 -248
  238. package/dist/_members/core/render/vcam/constants.js +0 -115
  239. package/dist/_members/core/render/vcam/errorCodes.js +0 -39
  240. package/dist/_members/core/render/vcam/framing.js +0 -136
  241. package/dist/_members/core/render/vcam/requiredRegion.js +0 -137
  242. package/dist/_members/core/render/vcam/shots.js +0 -53
  243. package/dist/_members/core/render/vcam/subjectSet.js +0 -388
  244. package/dist/_members/core/select/errorCodes.js +0 -38
  245. package/dist/_members/core/select/index.js +0 -26
  246. package/dist/_members/core/select/model.js +0 -18
  247. package/dist/_members/core/select/select.js +0 -244
  248. package/dist/_members/core/select/stabilize.js +0 -242
  249. package/dist/_members/core/types.js +0 -7
  250. package/dist/_members/errors/index.js +0 -1
  251. package/dist/_members/errors/registry.js +0 -560
  252. package/dist/_members/foundation/codes/index.js +0 -31
  253. package/dist/_members/foundation/index.js +0 -2
  254. package/dist/_members/foundation/primitives/index.js +0 -5
  255. package/dist/_members/mcp/adapter.js +0 -771
  256. package/dist/_members/mcp/burnedNames.js +0 -73
  257. package/dist/_members/mcp/core.js +0 -390
  258. package/dist/_members/mcp/curation.js +0 -792
  259. package/dist/_members/mcp/executor.js +0 -142
  260. package/dist/_members/mcp/format.js +0 -9
  261. package/dist/_members/mcp/generated/catalogPreviews.js +0 -95
  262. package/dist/_members/mcp/generated/harness.js +0 -81
  263. package/dist/_members/mcp/generated/operations.js +0 -131
  264. package/dist/_members/mcp/index.js +0 -50
  265. package/dist/_members/mcp/internalLoopTools.js +0 -109
  266. package/dist/_members/mcp/prompts.js +0 -45
  267. package/dist/_members/mcp/resources.js +0 -166
  268. package/dist/_members/mcp/server.js +0 -340
  269. package/dist/_members/mcp/wait.js +0 -378
  270. package/dist/_members/mcp/x402Meta.js +0 -66
  271. package/dist/_members/media-model/api.js +0 -175
  272. package/dist/_members/media-model/index.js +0 -1
  273. package/dist/_members/media-model/media-kind.js +0 -116
  274. package/dist/_members/media-model/testing/index.js +0 -1
  275. package/dist/_members/media-model/testing/mediaStoreContract.js +0 -82
  276. package/dist/_members/media-model/transcriptSearch.js +0 -70
  277. package/dist/_members/media-model/transcriptSearchCases.json +0 -79
  278. package/dist/_members/nle-export/compact-timeline.js +0 -73
  279. package/dist/_members/nle-export/compact-timeline.js.map +0 -1
  280. package/dist/_members/nle-export/composition-to-input.js +0 -147
  281. package/dist/_members/nle-export/composition-to-input.js.map +0 -1
  282. package/dist/_members/nle-export/fcpxml.js +0 -159
  283. package/dist/_members/nle-export/fcpxml.js.map +0 -1
  284. package/dist/_members/nle-export/index.js +0 -12
  285. package/dist/_members/nle-export/index.js.map +0 -1
  286. package/dist/_members/nle-export/premiere.js +0 -161
  287. package/dist/_members/nle-export/premiere.js.map +0 -1
  288. package/dist/_members/nle-export/server/index.js +0 -2
  289. package/dist/_members/nle-export/server/index.js.map +0 -1
  290. package/dist/_members/nle-export/server/pack-zip.js +0 -38
  291. package/dist/_members/nle-export/server/pack-zip.js.map +0 -1
  292. package/dist/_members/nle-export/srt.js +0 -50
  293. package/dist/_members/nle-export/srt.js.map +0 -1
  294. package/dist/_members/nle-export/types.js +0 -9
  295. package/dist/_members/nle-export/types.js.map +0 -1
  296. package/dist/_members/nle-export/xml/index.js +0 -11
  297. package/dist/_members/nle-export/xml/index.js.map +0 -1
  298. package/dist/_members/nle-export/xml-utils.js +0 -48
  299. package/dist/_members/nle-export/xml-utils.js.map +0 -1
  300. package/dist/_members/perception/activeSpeaker.js +0 -103
  301. package/dist/_members/perception/audioQuality.js +0 -76
  302. package/dist/_members/perception/energyBeat.js +0 -99
  303. package/dist/_members/perception/identityMatch.js +0 -91
  304. package/dist/_members/perception/index.js +0 -48
  305. package/dist/_members/perception/visualQuality.js +0 -95
  306. package/dist/_members/plane/conformance.js +0 -90
  307. package/dist/_members/plane/contract.js +0 -49
  308. package/dist/_members/plane/index.js +0 -16
  309. package/dist/_members/primitives/backgrounds/index.js +0 -32
  310. package/dist/_members/primitives/backgrounds/index.js.map +0 -1
  311. package/dist/_members/primitives/captions/index.js +0 -398
  312. package/dist/_members/primitives/captions/index.js.map +0 -1
  313. package/dist/_members/primitives/chunk-2LVE4UIT.js +0 -139
  314. package/dist/_members/primitives/chunk-2LVE4UIT.js.map +0 -1
  315. package/dist/_members/primitives/chunk-3IQUH6KK.js +0 -20
  316. package/dist/_members/primitives/chunk-3IQUH6KK.js.map +0 -1
  317. package/dist/_members/primitives/chunk-3OMURV6Q.js +0 -112
  318. package/dist/_members/primitives/chunk-3OMURV6Q.js.map +0 -1
  319. package/dist/_members/primitives/chunk-4IS6TMEG.js +0 -21
  320. package/dist/_members/primitives/chunk-4IS6TMEG.js.map +0 -1
  321. package/dist/_members/primitives/chunk-5MWGAEGQ.js +0 -1644
  322. package/dist/_members/primitives/chunk-5MWGAEGQ.js.map +0 -1
  323. package/dist/_members/primitives/chunk-7RRPFPQK.js +0 -15
  324. package/dist/_members/primitives/chunk-7RRPFPQK.js.map +0 -1
  325. package/dist/_members/primitives/chunk-DH5TRZK2.js +0 -125
  326. package/dist/_members/primitives/chunk-DH5TRZK2.js.map +0 -1
  327. package/dist/_members/primitives/chunk-E3KNUSFG.js +0 -2337
  328. package/dist/_members/primitives/chunk-E3KNUSFG.js.map +0 -1
  329. package/dist/_members/primitives/chunk-G2B3G4HK.js +0 -47
  330. package/dist/_members/primitives/chunk-G2B3G4HK.js.map +0 -1
  331. package/dist/_members/primitives/chunk-HFPFGZXT.js +0 -1494
  332. package/dist/_members/primitives/chunk-HFPFGZXT.js.map +0 -1
  333. package/dist/_members/primitives/chunk-HIWVFZVH.js +0 -19
  334. package/dist/_members/primitives/chunk-HIWVFZVH.js.map +0 -1
  335. package/dist/_members/primitives/chunk-IDAL6G3Y.js.map +0 -1
  336. package/dist/_members/primitives/chunk-KNC6JP5F.js +0 -34
  337. package/dist/_members/primitives/chunk-KNC6JP5F.js.map +0 -1
  338. package/dist/_members/primitives/chunk-KYRDVZST.js +0 -459
  339. package/dist/_members/primitives/chunk-KYRDVZST.js.map +0 -1
  340. package/dist/_members/primitives/chunk-LWLHS2NS.js +0 -3410
  341. package/dist/_members/primitives/chunk-LWLHS2NS.js.map +0 -1
  342. package/dist/_members/primitives/chunk-QAQ3P7DR.js +0 -8855
  343. package/dist/_members/primitives/chunk-QAQ3P7DR.js.map +0 -1
  344. package/dist/_members/primitives/chunk-ROV2ODWC.js +0 -81
  345. package/dist/_members/primitives/chunk-ROV2ODWC.js.map +0 -1
  346. package/dist/_members/primitives/chunk-TCRC2FH3.js +0 -183
  347. package/dist/_members/primitives/chunk-TCRC2FH3.js.map +0 -1
  348. package/dist/_members/primitives/chunk-US4YEDAA.js +0 -96
  349. package/dist/_members/primitives/chunk-US4YEDAA.js.map +0 -1
  350. package/dist/_members/primitives/chunk-UVKRO5ER.js +0 -13
  351. package/dist/_members/primitives/chunk-UVKRO5ER.js.map +0 -1
  352. package/dist/_members/primitives/chunk-W4EIIF2I.js +0 -10
  353. package/dist/_members/primitives/chunk-W4EIIF2I.js.map +0 -1
  354. package/dist/_members/primitives/chunk-WPGKIPCO.js +0 -3563
  355. package/dist/_members/primitives/chunk-WPGKIPCO.js.map +0 -1
  356. package/dist/_members/primitives/chunk-WZFLEGMD.js +0 -1435
  357. package/dist/_members/primitives/chunk-WZFLEGMD.js.map +0 -1
  358. package/dist/_members/primitives/chunk-XHU3WBEI.js +0 -33
  359. package/dist/_members/primitives/chunk-XHU3WBEI.js.map +0 -1
  360. package/dist/_members/primitives/chunk-YN7WTZZ7.js +0 -46
  361. package/dist/_members/primitives/chunk-YN7WTZZ7.js.map +0 -1
  362. package/dist/_members/primitives/effects/index.js +0 -30
  363. package/dist/_members/primitives/effects/index.js.map +0 -1
  364. package/dist/_members/primitives/hero-text/index.js +0 -14
  365. package/dist/_members/primitives/hero-text/index.js.map +0 -1
  366. package/dist/_members/primitives/index.js +0 -64
  367. package/dist/_members/primitives/index.js.map +0 -1
  368. package/dist/_members/primitives/layouts/index.js +0 -24
  369. package/dist/_members/primitives/layouts/index.js.map +0 -1
  370. package/dist/_members/primitives/lib/easings.js +0 -26
  371. package/dist/_members/primitives/lib/easings.js.map +0 -1
  372. package/dist/_members/primitives/lib/index.js +0 -74
  373. package/dist/_members/primitives/lib/index.js.map +0 -1
  374. package/dist/_members/primitives/lib/palette.js +0 -72
  375. package/dist/_members/primitives/lib/palette.js.map +0 -1
  376. package/dist/_members/primitives/lib/preview.js +0 -8
  377. package/dist/_members/primitives/lib/preview.js.map +0 -1
  378. package/dist/_members/primitives/lib/schema.js +0 -12
  379. package/dist/_members/primitives/lib/schema.js.map +0 -1
  380. package/dist/_members/primitives/lib/seeded-random.js +0 -14
  381. package/dist/_members/primitives/lib/seeded-random.js.map +0 -1
  382. package/dist/_members/primitives/lib/text/contrast.js +0 -12
  383. package/dist/_members/primitives/lib/text/contrast.js.map +0 -1
  384. package/dist/_members/primitives/lib/text/fill.js +0 -8
  385. package/dist/_members/primitives/lib/text/fill.js.map +0 -1
  386. package/dist/_members/primitives/lib/text/fitText.js +0 -10
  387. package/dist/_members/primitives/lib/text/fitText.js.map +0 -1
  388. package/dist/_members/primitives/lib/text/marker.js +0 -8
  389. package/dist/_members/primitives/lib/text/marker.js.map +0 -1
  390. package/dist/_members/primitives/lib/text/motion.js +0 -16
  391. package/dist/_members/primitives/lib/text/motion.js.map +0 -1
  392. package/dist/_members/primitives/lib/text/recolor.js +0 -8
  393. package/dist/_members/primitives/lib/text/recolor.js.map +0 -1
  394. package/dist/_members/primitives/lib/text/spec.js +0 -1
  395. package/dist/_members/primitives/lib/text/spec.js.map +0 -1
  396. package/dist/_members/primitives/lib/text/stage.js +0 -8
  397. package/dist/_members/primitives/lib/text/stage.js.map +0 -1
  398. package/dist/_members/primitives/lib/text/typeface.js +0 -8
  399. package/dist/_members/primitives/lib/text/typeface.js.map +0 -1
  400. package/dist/_members/primitives/lib/tokens.js +0 -24
  401. package/dist/_members/primitives/lib/tokens.js.map +0 -1
  402. package/dist/_members/primitives/lib/types.js +0 -1
  403. package/dist/_members/primitives/lib/types.js.map +0 -1
  404. package/dist/_members/primitives/params-required.json +0 -153
  405. package/dist/_members/primitives/scenes/index.js +0 -25
  406. package/dist/_members/primitives/scenes/index.js.map +0 -1
  407. package/dist/_members/primitives/subject-foreground/index.js +0 -30
  408. package/dist/_members/primitives/subject-foreground/index.js.map +0 -1
  409. package/dist/_members/primitives/text/index.js +0 -57
  410. package/dist/_members/primitives/text/index.js.map +0 -1
  411. package/dist/_members/primitives/transitions/index.js +0 -67
  412. package/dist/_members/primitives/transitions/index.js.map +0 -1
  413. package/dist/_members/primitives/ui-blocks/index.js +0 -76
  414. package/dist/_members/primitives/ui-blocks/index.js.map +0 -1
  415. package/dist/_members/primitives/version-map.json +0 -105
  416. package/dist/_members/primitives/workspace.js +0 -60
  417. package/dist/_members/primitives/workspace.js.map +0 -1
  418. package/dist/chunk-7GISRVON.js +0 -14176
  419. package/dist/chunk-DIBOJJMU.js +0 -16838
  420. package/dist/chunk-EAUSEGB7.js +0 -420
  421. package/dist/chunk-FUWJOVVK.js +0 -3633
  422. package/dist/chunk-G75E7YA4.js +0 -11883
  423. package/dist/chunk-JCZQT7K3.js +0 -3266
  424. package/dist/chunk-KF5ETMKB.js +0 -544
  425. package/dist/chunk-NDKERSXY.js +0 -1296
  426. package/dist/chunk-V43Y7IE4.js +0 -3621
  427. package/dist/chunk-VIVQHIGZ.js +0 -4319
  428. package/dist/chunk-YA3OR2KK.js +0 -249
  429. package/dist/hls-HD4C5G6W.js +0 -33000
  430. package/dist/token-RGFIT24D.js +0 -69
  431. package/dist/token-util-GPLSGYVO.js +0 -5
  432. package/dist/vision_bundle-5Y376MAR.js +0 -4084
@@ -1,1249 +0,0 @@
1
- /**
2
- * Wire-shape `Composition` → `RenderInput` translator.
3
- *
4
- * `Composition` is the canonical multi-track wire shape from
5
- * `@cueframe/composition`. It crosses the API boundary
6
- * (PUT /v1/projects/:id/composition), gets snapshotted onto
7
- * `renderJobs.composition`, and is what the Remotion worker reads
8
- * before kicking off a render.
9
- *
10
- * This module is the single bridge from that wire shape to the
11
- * frame-based `RenderInput` Remotion consumes:
12
- *
13
- * {format, tracks[]: [{kind, contents[]: TimelineClip[]}]}
14
- * ↓
15
- * compositionToRenderInput(composition, srcUrls)
16
- * ↓
17
- * { clips, overlays, audioTracks, effects, width, height, fps,
18
- * durationInFrames }
19
- *
20
- * Track-kind handling:
21
- * - `video` → `CompositionClip[]`. Each clip's wire startTime/duration
22
- * map to timelineStartFrames/durationInFrames; `source.trim`'s
23
- * source-relative seconds map to sourceInFrames/sourceOutFrames.
24
- * Clips on multiple video tracks layer in track order (z-order
25
- * follows the wire array).
26
- * - `audio` → `CompositionAudioTrack[]`. `source.volumeDb` (if set)
27
- * converts to linear gain via `10^(dB/20)`; `source.fadeInMs` /
28
- * `source.fadeOutMs` convert to frames.
29
- * - `image` → `CompositionClip[]` with `mediaType: 'image'`. Stills
30
- * have no source timeline (sourceIn/Out are zero); `source.kenBurns`
31
- * flows through as a convenience that the renderer's <StillCrop> lowers
32
- * to a `reframe` (kenBurnsToReframe) and runs through the UNIFIED crop
33
- * renderer, so a still gets the same per-frame point-zoom video does.
34
- * Sub-frame durations are dropped (would crash Remotion's <Sequence>).
35
- * - `overlay` → `CompositionOverlay[]`. Wire `source.primitiveId`
36
- * becomes `template`; `source.params` flow through to the renderer.
37
- * - `effect` → `CompositionEffect[]` with `target.kind: 'global'`,
38
- * scoped to the effect clip's wire start/duration.
39
- *
40
- * `srcUrls` is a `mediaId → presigned URL` map. The caller (Temporal
41
- * activity) pre-signs every unique mediaId before calling this. A
42
- * referenced mediaId missing from `srcUrls` throws — silent fallbacks
43
- * are forbidden (the v0 path's silent-fallback bug class).
44
- */
45
- import { compositionHasVo, roleDefaultGainDb } from "@cueframe/composition/audio";
46
- import { flattenCompoundClips, hasCompoundClips } from "@cueframe/composition/timeline";
47
- import { resolveBookendTimeline } from "./bookends";
48
- /**
49
- * Project a wire clip's per-clip `effects` (authorable on `TimelineClip.effects`)
50
- * into clip-targeted render effects. The renderer's `EffectLayer` resolves
51
- * `target.kind==='clip'` by looking the `clipId` up in the clipFrames map (keyed
52
- * by render-clip id) and anchoring the effect to that clip's timeline window. We
53
- * target the FIRST emitted sub-span's render id (`renderClipId` — equals the wire
54
- * `c.id` for a single-span clip, `${c.id}#0` when excludedRanges fan it out) and
55
- * leave start/duration unset so `EffectLayer` spans the clip's whole window.
56
- *
57
- * Uses the SAME shape the global effect path (`projectEffectTrack`) emits — only
58
- * the `target` differs (`clip` vs `global`). When a clip carries no effects this
59
- * returns `[]`, so behavior is unchanged for the common case.
60
- */
61
- function projectClipEffects(clip, renderClipId) {
62
- const effects = clip.effects;
63
- if (!effects || effects.length === 0)
64
- return [];
65
- return effects.map((e, idx) => ({
66
- // Deterministic, unique within the RenderInput: derives from the render-clip
67
- // id + the effect's index on the clip (so React keys / clipFrames don't collide).
68
- id: `${renderClipId}::effect:${idx}`,
69
- primitiveId: e.primitiveId,
70
- params: (e.params ?? {}),
71
- target: { kind: "clip", clipId: renderClipId },
72
- }));
73
- }
74
- /**
75
- * Resolve `Format` (`aspectRatio`, optional `fps` + `resolution`) to
76
- * concrete pixel dimensions. Mirrors the public-composition path's
77
- * `publicFormatDimensions` so a Composition with `format: { aspectRatio:
78
- * "9:16", fps: 30, resolution: "fhd" }` lands at the same 1080×1920
79
- * regardless of which translator the worker traversed.
80
- */
81
- export function compositionFormatDimensions(format) {
82
- const fps = format.fps ?? 30;
83
- const resolution = format.resolution ?? "fhd";
84
- const dims = {
85
- "9:16": {
86
- hd: { width: 720, height: 1280 },
87
- fhd: { width: 1080, height: 1920 },
88
- "4k": { width: 2160, height: 3840 },
89
- },
90
- "1:1": {
91
- hd: { width: 720, height: 720 },
92
- fhd: { width: 1080, height: 1080 },
93
- "4k": { width: 2160, height: 2160 },
94
- },
95
- "4:5": {
96
- hd: { width: 720, height: 900 },
97
- fhd: { width: 1080, height: 1350 },
98
- "4k": { width: 2160, height: 2700 },
99
- },
100
- "16:9": {
101
- hd: { width: 1280, height: 720 },
102
- fhd: { width: 1920, height: 1080 },
103
- "4k": { width: 3840, height: 2160 },
104
- },
105
- };
106
- return { ...dims[format.aspectRatio][resolution], fps };
107
- }
108
- /**
109
- * Runtime-validate an untrusted value as a `Composition`. The Temporal
110
- * activity reads `renderJobs.composition` which is stored as
111
- * `v.optional(v.any())` — Convex doesn't enforce shape at read time. A
112
- * schema drift or a manual DB edit could leave a malformed value here,
113
- * and the renderer would crash later in the pipeline with a less
114
- * actionable error.
115
- *
116
- * Returns the validated composition or throws with a specific error
117
- * message. Call this at the activity boundary before passing the
118
- * composition to `compositionToRenderInput`. Pure JS validation — no
119
- * Zod runtime — so this module ships into the Temporal worker without
120
- * dragging the package's Zod surface along.
121
- */
122
- export function validateComposition(raw) {
123
- if (!raw || typeof raw !== "object") {
124
- throw new Error("composition: expected object, got " + typeof raw);
125
- }
126
- const obj = raw;
127
- if (obj.v !== 1) {
128
- throw new Error("composition.v: expected literal 1");
129
- }
130
- const fmt = obj.format;
131
- if (!fmt || typeof fmt.aspectRatio !== "string") {
132
- throw new Error("composition.format.aspectRatio: required string");
133
- }
134
- if (!Array.isArray(obj.tracks)) {
135
- throw new Error("composition.tracks: expected array");
136
- }
137
- for (let i = 0; i < obj.tracks.length; i++) {
138
- const t = obj.tracks[i];
139
- if (!t || typeof t !== "object") {
140
- throw new Error(`composition.tracks[${i}]: expected object`);
141
- }
142
- if (typeof t.id !== "string") {
143
- throw new Error(`composition.tracks[${i}].id: expected string`);
144
- }
145
- if (!["video", "audio", "image", "overlay", "effect"].includes(t.kind)) {
146
- throw new Error(`composition.tracks[${i}].kind: invalid value`);
147
- }
148
- if (!Array.isArray(t.contents)) {
149
- throw new Error(`composition.tracks[${i}].contents: expected array`);
150
- }
151
- }
152
- if (obj.markers !== undefined && !Array.isArray(obj.markers)) {
153
- throw new Error("composition.markers: expected array when present");
154
- }
155
- if (obj.captions !== undefined) {
156
- const c = obj.captions;
157
- if (!c || typeof c !== "object") {
158
- throw new Error("composition.captions: expected object when present");
159
- }
160
- // Captions are segment-structured ({ segments: [{ words }] }); the legacy flat
161
- // `words` shape is gone. Validate the segments array the flatten below consumes.
162
- if (!Array.isArray(c.segments)) {
163
- throw new Error("composition.captions.segments: expected array");
164
- }
165
- if (c.style !== undefined && (typeof c.style !== "object" || c.style === null)) {
166
- throw new Error("composition.captions.style: expected object when present");
167
- }
168
- // fontSize UNITS backstop — PERCENT of composition height, bounded [1,15]
169
- // (the wire zod enforces the same bound at author time; this catches
170
- // STORED/legacy compositions read back from `renderJobs.composition`,
171
- // which is v.any()). A px-intent value (e.g. 31 — memoir/P2surf E2E
172
- // 2026-07-22) would render 31% of the frame height per token: a giant
173
- // multi-line page overflowing the caption band into the upper half. Fail
174
- // loud at the render boundary rather than ship garbage pixels; this runs
175
- // in resolveRenderEnvironment BEFORE the render cache is consulted, so a
176
- // stale mis-sized MP4 is never served either.
177
- if (c.style !== undefined) {
178
- const fs = c.style.fontSize;
179
- if (fs !== undefined && (typeof fs !== "number" || fs < 1 || fs > 15)) {
180
- throw new Error("composition.captions.style.fontSize: PERCENT of composition height (1-15, default 5) — " +
181
- `got ${String(fs)}. px values are not accepted; a px-intent value renders giant ` +
182
- "band-overflowing captions. Re-author the style with a percent value.");
183
- }
184
- }
185
- }
186
- return obj;
187
- }
188
- /** Round non-negative seconds to a frame index. */
189
- function frames(seconds, fps) {
190
- return Math.max(0, Math.round(seconds * fps));
191
- }
192
- /**
193
- * The frame span of a TIMELINE placement, ANCHORED AT BOTH ENDS.
194
- *
195
- * THE BUG THIS EXISTS FOR. Rounding `startTime` and `duration` INDEPENDENTLY —
196
- * `frames(start)` and `frames(duration)` — does not compose:
197
- *
198
- * round(a·f) + round(d·f) !== round((a+d)·f)
199
- *
200
- * At 30fps a clip at 0.49s lasting 0.02s occupies frames 15..16, while the clip
201
- * authored to start exactly at 0.51s begins at frame 15 — a one-frame OVERLAP,
202
- * out of arithmetic alone. Shift the numbers slightly and it is a one-frame GAP
203
- * (a black flash). Two clips that are perfectly contiguous in seconds could
204
- * render as neither. The renderer already knew: the warm-decode grouping below
205
- * tolerates exactly this, "within one frame of rounding noise".
206
- *
207
- * Anchoring on the END TIME instead makes it compose by construction: clip A's
208
- * last frame is `frames(Aend)` and clip B starts at `frames(Bstart)`, so when
209
- * `Bstart === Aend` in seconds the two are the SAME integer and the boundary is
210
- * exact — no gap, no overlap, no tolerance needed.
211
- *
212
- * Every timeline placement goes through here. Source-time quantities
213
- * (`sourceInFrames`, trims, fades) keep using `frames()`: they are not spans on
214
- * the timeline and have no neighbour to meet.
215
- */
216
- function timelineSpanFrames(startSec, endSec, fps) {
217
- const startFrame = frames(startSec, fps);
218
- return { startFrame, durationInFrames: Math.max(0, frames(endSec, fps) - startFrame) };
219
- }
220
- /**
221
- * Map a canonical `Transition.type` (a free string the agent authors, e.g.
222
- * 'fade' | 'dissolve' | 'wipe' | 'cut' | 'push') to the render input's
223
- * supported presentation enum. 'cut' → 'none' (a hard cut needs no
224
- * presentation); anything the renderer can't present ('push'/'slide'/unknown)
225
- * falls back to 'fade' so the transition still appears rather than silently
226
- * dropping to dead pixels.
227
- */
228
- function mapTransitionType(type) {
229
- switch (type) {
230
- case 'fade':
231
- case 'dissolve':
232
- case 'wipe':
233
- case 'none':
234
- return type;
235
- case 'cut':
236
- return 'none';
237
- default:
238
- return 'fade';
239
- }
240
- }
241
- /**
242
- * Overlay entrance-transition projection — mirrors the video-clip mapping at the
243
- * `s === 0` site in projectMediaTrack so an authored `transitionIn` on an
244
- * OVERLAY-family clip (alpha-layer / card / custom / primitive) is honored at
245
- * render. It was silently dropped: only the video lane projected these two
246
- * fields, so an authored overlay transition rendered as a dead hard cut (the v5
247
- * memoir "silent transitionIn drop" Class-B failure). Same field names/shape/enum
248
- * as the clip lane; the render host wraps the mounted overlay content in a
249
- * frame-pure entrance envelope when they are present, and emits nothing (=
250
- * byte-identical behavior) when absent.
251
- */
252
- function overlayTransitionFields(c, fps) {
253
- return c.transitionIn
254
- ? {
255
- transitionIn: mapTransitionType(c.transitionIn.type),
256
- transitionDurationFrames: Math.max(1, Math.round(c.transitionIn.duration * fps)),
257
- }
258
- : {};
259
- }
260
- /**
261
- * A clip that produces no frames, and therefore must not be able to fail a
262
- * render by demanding a URL it will never use.
263
- *
264
- * Five duplicate `duration: 0` clips of a cloud-only SFX took the whole app
265
- * down through the ErrorBoundary with "no presigned URL was supplied" — for
266
- * media that could never appear in the output. `durationInFrames` rounds to
267
- * zero and Remotion draws nothing either way; the only difference is whether
268
- * the render happens at all.
269
- *
270
- * ONE definition, because the video and image loops both need it and two
271
- * copies of one rule is how they drift.
272
- */
273
- function rendersNoFrames(c) {
274
- return !(c.duration > 0);
275
- }
276
- function resolveSrcUrl(mediaId, srcUrls) {
277
- const url = srcUrls[mediaId];
278
- if (!url) {
279
- throw new Error(`Composition references mediaId '${mediaId}' but no presigned URL was supplied. ` +
280
- `Pre-sign every unique mediaId before calling compositionToRenderInput.`);
281
- }
282
- return url;
283
- }
284
- /**
285
- * Given a clip's effective trim window [trimStart, trimEnd] and an
286
- * optional sorted+non-overlapping list of excluded ranges (all in source
287
- * coordinates), return the playable spans in order. Each span is a
288
- * contiguous source range that should render as a sub-clip.
289
- *
290
- * Invariant (enforced upstream): Σ(span.end - span.start) === clip.duration.
291
- */
292
- /** The clip's playable spans (seconds) = [trimStart, trimEnd] minus
293
- * excludedRanges. SSOT for section splitting — exported so the Director derives
294
- * the SAME sections the render uses when it builds structure-first keep-outs. */
295
- export function playableSpans(trimStart, trimEnd, excluded) {
296
- if (!excluded || excluded.length === 0) {
297
- return [{ start: trimStart, end: trimEnd }];
298
- }
299
- const out = [];
300
- let cursor = trimStart;
301
- for (const r of excluded) {
302
- if (r.start > cursor)
303
- out.push({ start: cursor, end: r.start });
304
- cursor = r.end;
305
- }
306
- if (cursor < trimEnd)
307
- out.push({ start: cursor, end: trimEnd });
308
- return out;
309
- }
310
- /**
311
- * Project a video/audio track's contents into `CompositionClip[]` /
312
- * `CompositionAudioTrack[]`. Clips on the wire reference media via
313
- * `source: { kind: 'media', mediaId, trim? }`; sources of any other
314
- * kind on a video/audio track are a wire violation and are dropped
315
- * with a thrown error.
316
- */
317
- function projectMediaTrack(track, fps, srcUrls, trajectoryUrls, coViewUrls) {
318
- const clips = [];
319
- const audio = [];
320
- const effects = [];
321
- for (let i = 0; i < track.contents.length; i++) {
322
- const c = track.contents[i];
323
- // Wire `Gap` entries omit `source`; render path drops them — gaps
324
- // on the timeline are produced naturally by clip start/duration
325
- // gaps. `Gap` only carries explicit non-contiguous semantics that
326
- // matter to OTIO export, not to Remotion.
327
- if (!("source" in c) || !c.source)
328
- continue;
329
- if (c.source.kind !== "media") {
330
- throw new Error(`composition.tracks[${track.id}].contents[${i}]: kind=${c.source.kind} is invalid on a ${track.kind} track (expected 'media')`);
331
- }
332
- if (rendersNoFrames(c))
333
- continue;
334
- const src = resolveSrcUrl(c.source.mediaId, srcUrls);
335
- // FAIL LOUD: a co-view clip (captions-behind-subject matte → packed scene)
336
- // placed in a sub-frame `region` is unsupported — MainComposition's co-view
337
- // z-split path renders the packed file full-frame and does NOT honor region,
338
- // so the placement would be silently dropped (the card paints over the whole
339
- // canvas). Reject at projection time rather than ship a mis-placed render.
340
- if (coViewUrls?.[c.source.mediaId] !== undefined &&
341
- c.region !== undefined &&
342
- !(c.region.x === 0 && c.region.y === 0 && c.region.w === 1 && c.region.h === 1)) {
343
- throw new Error(`composition.tracks[${track.id}].contents[${i}] (clip ${c.id}): a co-view ` +
344
- `clip (captions-behind-subject) cannot also carry a sub-frame region — the ` +
345
- `co-view render path does not honor region. Use one or the other.`);
346
- }
347
- if (track.kind === "audio") {
348
- const trimStart = c.source.trim?.start ?? 0;
349
- const trimEnd = c.source.trim?.end ?? trimStart + c.duration;
350
- const spans = playableSpans(trimStart, trimEnd, c.source.excludedRanges);
351
- // Audio mix: `volumeDb` (new for SFX beds) wins over the legacy
352
- // linear `volume` field. dB → linear via 10^(dB/20). When neither
353
- // is set, fall back to 1.0 (unchanged from source). The renderer
354
- // separately defaults -12 dB for stock SFX selections; that's a
355
- // policy decision applied upstream in the brief writer, not here.
356
- // TRACK GAIN multiplies the clip's own level. It is a fader above the
357
- // clips, not a replacement for them, so "turn the music down" is one
358
- // control rather than an edit per clip — and an absent `track.volume`
359
- // leaves the result bit-identical to before the field existed.
360
- const resolvedVolume = (c.source.volumeDb !== undefined
361
- ? Math.pow(10, c.source.volumeDb / 20)
362
- : (c.source.volume ?? 1)) * (track.volume ?? 1);
363
- // ms → frames. Round so fade-in + fade-out together never exceed
364
- // the clip's duration (renderer's volume function would clamp, but
365
- // we'd rather not rely on that).
366
- const fadeInFrames = c.source.fadeInMs !== undefined
367
- ? Math.max(0, Math.round((c.source.fadeInMs / 1000) * fps))
368
- : undefined;
369
- const fadeOutFrames = c.source.fadeOutMs !== undefined
370
- ? Math.max(0, Math.round((c.source.fadeOutMs / 1000) * fps))
371
- : undefined;
372
- // Walk in accumulated SECONDS and derive each boundary end-anchored, so a
373
- // sub-frame span that gets dropped cannot shift the ones after it (the old
374
- // frame-cursor accumulation had to absorb that drift deliberately).
375
- let playedSec = 0;
376
- for (let s = 0; s < spans.length; s++) {
377
- const span = spans[s];
378
- const spanStartSec = c.startTime + playedSec;
379
- playedSec += span.end - span.start;
380
- const { startFrame, durationInFrames: subDurationFrames } = timelineSpanFrames(spanStartSec, c.startTime + playedSec, fps);
381
- // Sub-frame spans (e.g. two excluded ranges leaving a 10ms gap at 30fps
382
- // round to 0 frames) would crash Remotion's <Sequence>, which requires
383
- // positive duration. Drop them — the dropped time is absorbed into the
384
- // surrounding excluded zone. `playedSec` still advanced, so every LATER
385
- // span keeps its true absolute position instead of sliding earlier.
386
- if (subDurationFrames === 0)
387
- continue;
388
- const sourceInFrames = frames(span.start, fps);
389
- audio.push({
390
- id: spans.length === 1 ? c.id : `${c.id}#${s}`,
391
- src,
392
- sourceInFrames,
393
- sourceOutFrames: sourceInFrames + subDurationFrames,
394
- startFrame,
395
- durationInFrames: subDurationFrames,
396
- volume: resolvedVolume,
397
- ...(fadeInFrames !== undefined ? { fadeInFrames } : {}),
398
- ...(fadeOutFrames !== undefined ? { fadeOutFrames } : {}),
399
- ...(c.source.loop !== undefined ? { loop: c.source.loop } : {}),
400
- });
401
- }
402
- continue;
403
- }
404
- const trimStart = c.source.trim?.start ?? 0;
405
- // Fallback is in source coords — mirrors trimStart's scope. Don't replace
406
- // with c.startTime + c.duration (timeline coords, dimensional mismatch).
407
- const trimEnd = c.source.trim?.end ?? trimStart + c.duration;
408
- const spans = playableSpans(trimStart, trimEnd, c.source.excludedRanges);
409
- // First emitted sub-clip render id — the clip-targeted effect anchors to it
410
- // (it's a key in EffectLayer's clipFrames map; the wire `c.id` itself is NOT
411
- // when excludedRanges fan the clip into `${c.id}#N` sub-spans).
412
- let firstEmittedSubId;
413
- // Accumulated SECONDS, end-anchored per span — see the audio branch.
414
- let playedSec = 0;
415
- for (let s = 0; s < spans.length; s++) {
416
- const span = spans[s];
417
- const spanStartSec = c.startTime + playedSec;
418
- playedSec += span.end - span.start;
419
- const { startFrame: timelineCursorFrames, durationInFrames: subDurationFrames } = timelineSpanFrames(spanStartSec, c.startTime + playedSec, fps);
420
- // See the audio branch above for the rationale: sub-frame spans
421
- // would crash Remotion's <Sequence>. Skip them; `playedSec` has already
422
- // advanced, so later spans keep their true absolute position.
423
- if (subDurationFrames === 0)
424
- continue;
425
- const sourceInFrames = frames(span.start, fps);
426
- // Stable sub-clip ID: deterministic across renders of the same composition,
427
- // unique within the RenderInput so React keys don't collide. Single-span
428
- // clips keep the original ID for backward compat with consumers that key
429
- // on it (selection, hover, etc.).
430
- const subId = spans.length === 1 ? c.id : `${c.id}#${s}`;
431
- if (firstEmittedSubId === undefined)
432
- firstEmittedSubId = subId;
433
- clips.push({
434
- id: subId,
435
- src,
436
- sourceInFrames,
437
- sourceOutFrames: sourceInFrames + subDurationFrames,
438
- timelineStartFrames: timelineCursorFrames,
439
- durationInFrames: subDurationFrames,
440
- // Clip-relative trajectory origin: every sub-span of this clip shares the
441
- // clip's trim.start, so the crop can rebase source-absolute span frames
442
- // to the clip-relative (trajectory) frame across the excludedRanges gaps.
443
- trimStartFrames: frames(trimStart, fps),
444
- ...(c.source.playbackRate !== undefined ? { playbackRate: c.source.playbackRate } : {}),
445
- ...(c.source.volume !== undefined ? { volume: c.source.volume } : {}),
446
- ...(c.source.muted !== undefined ? { muted: c.source.muted } : {}),
447
- ...(c.source.loop !== undefined ? { loop: c.source.loop } : {}),
448
- ...(c.opacity !== undefined ? { opacity: c.opacity } : {}),
449
- ...(c.scale !== undefined ? { scale: c.scale } : {}),
450
- ...(c.rotation !== undefined ? { rotation: c.rotation } : {}),
451
- ...(c.animate !== undefined ? { animate: c.animate } : {}),
452
- // Output-space placement. A sub-frame `region` makes the clip occupy
453
- // only that rect of the output (PiP / split-screen / inset) — lower
454
- // tracks show through the uncovered area. Carried per-clip (shared by
455
- // every sub-span split from one wire clip). `fit` selects cover/contain.
456
- ...(c.region !== undefined ? { region: c.region } : {}),
457
- ...(c.fit !== undefined ? { fit: c.fit } : {}),
458
- // Clip transition-in. Attached only to the FIRST span of a clip
459
- // (transitions apply between original clips, not split sub-clips).
460
- // The canonical `{type,duration}` maps to the render-input string enum
461
- // + a frame count; without this the authored transition was dropped
462
- // and rendered as a dead hard cut (mirror of tracksToRenderInput.ts).
463
- ...(s === 0 && c.transitionIn
464
- ? {
465
- transitionIn: mapTransitionType(c.transitionIn.type),
466
- transitionDurationFrames: Math.max(1, Math.round(c.transitionIn.duration * fps)),
467
- }
468
- : {}),
469
- // Per-frame crop trajectory URL. When present, the renderer
470
- // applies the trajectory's per-frame normalized centers as a
471
- // CSS transform over the raw video to produce the cropped
472
- // output. Keyed by the PARENT wire-clip id `c.id` (NOT the
473
- // sub-span `subId`): the trajectory map is per-clip (Phase 2), and
474
- // every sub-span split from a clip shares that clip's trajectory.
475
- // Resolved at the caller layer (Temporal activity), which looks up
476
- // mediaItems.variants[intentHash][aspect].trajectoryR2Key per clip
477
- // and presigns it.
478
- ...(trajectoryUrls?.[c.id] !== undefined
479
- ? { cropTrajectoryUrl: trajectoryUrls[c.id] }
480
- : {}),
481
- // Captions-behind CO-VIEW. Content-driven (Phase 6): the matte gate baked
482
- // a crop-independent frame-packed co-view (scene over matte, one opaque
483
- // H.264) for any clip whose timeline range overlaps a behind-subject
484
- // caption window, surfaced here as coViewUrls[mediaId]. MATTE PRESENCE is
485
- // the gate — no per-clip enable flag. The renderer splits the packed
486
- // decode into the scene background + the subject cut-out foreground, and
487
- // the global caption layer's behind tokens render between them (the
488
- // output-space z-split in MainComposition).
489
- ...(coViewUrls?.[c.source.mediaId] !== undefined
490
- ? { packedUrl: coViewUrls[c.source.mediaId] }
491
- : {}),
492
- });
493
- }
494
- // Per-clip authored effects (target:'clip'). Anchored to the first emitted
495
- // sub-clip render id (a clipFrames key). Skipped when every span dropped
496
- // (sub-frame) so no effect targets a clip the renderer never mounted.
497
- if (firstEmittedSubId !== undefined) {
498
- effects.push(...projectClipEffects(c, firstEmittedSubId));
499
- }
500
- }
501
- // Group consecutive runs sharing the same src + playbackRate on this
502
- // track. Two distinct wire clips referencing the same mediaId — produced
503
- // either by `cutAndRipple` splitting a clip into left/right fragments or
504
- // by `excludedRanges` fanning one wire clip into N+1 sub-clips — share a
505
- // parentId so MainComposition.groupClips collapses them into ONE
506
- // <JumpCutVideo> driven by `sections`. Without this, each fragment
507
- // mounts its own <Video> element and the user hits a cold network +
508
- // decode warmup on every cut boundary.
509
- for (let i = 1; i < clips.length; i++) {
510
- const prev = clips[i - 1];
511
- const cur = clips[i];
512
- if (prev.src !== cur.src)
513
- continue;
514
- if ((prev.playbackRate ?? 1) !== (cur.playbackRate ?? 1))
515
- continue;
516
- // Different output placement → distinct on-screen rects; collapsing them
517
- // into one <JumpCutVideo> would render both spans through clips[0]'s region.
518
- // Sub-spans split from ONE wire clip copy the same region, so they still
519
- // group (identical region → equal); only genuinely different placements split.
520
- if (JSON.stringify(prev.region) !== JSON.stringify(cur.region))
521
- continue;
522
- if ((prev.fit ?? "cover") !== (cur.fit ?? "cover"))
523
- continue;
524
- // Per-clip crop trajectory + clip-relative origin must match. The merged
525
- // group renders ALL its sections through ONE <CropFromTrajectory> driven by
526
- // the FIRST clip's trajectory URL + trimStartFrames (MainComposition.GroupContent
527
- // reads `first.cropTrajectoryUrl` / `first.trimStartFrames`). Two distinct wire
528
- // reframe clips of the same media each carry their OWN trajectory (covering only
529
- // their own clip-relative window) and their OWN trim.start — collapsing them
530
- // would play the later clip's frames against the earlier clip's trajectory,
531
- // overrunning its coverage by a few frames at the seam (the
532
- // `trajectory_coverage_gap` bug: srcFrame past every segment). Sub-spans split
533
- // from ONE wire clip (excludedRanges) share `c.id` → same trajectory URL → same
534
- // trim.start, so they still merge into the warm-decode JumpCutVideo.
535
- if (prev.cropTrajectoryUrl !== cur.cropTrajectoryUrl)
536
- continue;
537
- if ((prev.trimStartFrames ?? 0) !== (cur.trimStartFrames ?? 0))
538
- continue;
539
- // Consecutive on the timeline (within one frame of rounding noise).
540
- const prevEnd = prev.timelineStartFrames + prev.durationInFrames;
541
- if (Math.abs(prevEnd - cur.timelineStartFrames) > 1)
542
- continue;
543
- const parent = prev.parentId ?? prev.id;
544
- cur.parentId = parent;
545
- if (prev.parentId === undefined)
546
- prev.parentId = parent;
547
- }
548
- return { clips, audio, effects };
549
- }
550
- /**
551
- * Project an image track's contents into `CompositionClip[]` with
552
- * `mediaType: 'image'`. Stills don't have a source timeline — `trim` /
553
- * `excludedRanges` are ignored. Each clip occupies the full duration on
554
- * the timeline; `kenBurns` flows through and the renderer's <StillCrop>
555
- * lowers it to a `reframe` for the unified per-frame crop.
556
- */
557
- function projectImageTrack(track, fps, srcUrls) {
558
- const clips = [];
559
- const effects = [];
560
- for (let i = 0; i < track.contents.length; i++) {
561
- const c = track.contents[i];
562
- if (!("source" in c) || !c.source)
563
- continue;
564
- if (c.source.kind !== "media") {
565
- throw new Error(`composition.tracks[${track.id}].contents[${i}]: kind=${c.source.kind} is invalid on an image track (expected 'media')`);
566
- }
567
- if (rendersNoFrames(c))
568
- continue;
569
- const src = resolveSrcUrl(c.source.mediaId, srcUrls);
570
- const { startFrame: timelineStartFrames, durationInFrames } = timelineSpanFrames(c.startTime, c.startTime + c.duration, fps);
571
- // Sub-frame durations would crash Remotion's <Sequence>. Drop them
572
- // the same way overlay/effect tracks do — the dropped time is
573
- // implicit in the surrounding wire shape.
574
- if (durationInFrames === 0)
575
- continue;
576
- clips.push({
577
- id: c.id,
578
- src,
579
- // Stills have no source timeline — sourceIn/Out are zero. The
580
- // renderer keys on mediaType==='image' to skip source-relative
581
- // seeking entirely.
582
- sourceInFrames: 0,
583
- sourceOutFrames: 0,
584
- timelineStartFrames,
585
- durationInFrames,
586
- mediaType: 'image',
587
- kenBurns: c.source.kenBurns ?? 'in',
588
- // The agent's authored camera channel for the still. When present it
589
- // DRIVES the point-zoom (StillCrop derives the still trajectory from it);
590
- // `kenBurns` is the fallback when no reframe was authored. This is how an
591
- // image gets an arbitrary point-zoom (zoom into a UI element), not just
592
- // the centered kenBurns presets.
593
- ...(c.source.reframe !== undefined && c.source.reframe.segments.length > 0
594
- ? { reframe: c.source.reframe }
595
- : {}),
596
- ...(c.opacity !== undefined ? { opacity: c.opacity } : {}),
597
- ...(c.scale !== undefined ? { scale: c.scale } : {}),
598
- ...(c.rotation !== undefined ? { rotation: c.rotation } : {}),
599
- ...(c.animate !== undefined ? { animate: c.animate } : {}),
600
- // Output-space placement (PiP / inset still). See projectMediaTrack.
601
- ...(c.region !== undefined ? { region: c.region } : {}),
602
- ...(c.fit !== undefined ? { fit: c.fit } : {}),
603
- });
604
- // Per-clip authored effects (target:'clip'). Image clips don't fan out, so
605
- // the render-clip id is the wire `c.id`.
606
- effects.push(...projectClipEffects(c, c.id));
607
- }
608
- return { clips, effects };
609
- }
610
- /** Clip-level transform channel (position/opacity/scale/rotation — the wire's
611
- * "animation channel", distinct from the `region` placement primitive). One
612
- * helper so all three overlay-family branches (overlay / card / alpha-layer)
613
- * carry it identically — these fields were silently dropped here until
614
- * 2026-07-20 (spec §2.1.1). Absent stays absent: no defaults minted. */
615
- function clipTransformChannel(c) {
616
- if (!("source" in c))
617
- return {};
618
- return {
619
- ...(c.position !== undefined ? { position: c.position } : {}),
620
- ...(c.opacity !== undefined ? { opacity: c.opacity } : {}),
621
- ...(c.scale !== undefined ? { scale: c.scale } : {}),
622
- ...(c.rotation !== undefined ? { rotation: c.rotation } : {}),
623
- ...(c.animate !== undefined ? { animate: c.animate } : {}),
624
- };
625
- }
626
- function projectOverlayTrack(track, fps,
627
- /** clipId → baked-alpha file URL/path for alpha-layer clips (2.6b). The box
628
- * bake writes the file + the daemon threads this map; absent for non-alpha. */
629
- alphaUrls = {},
630
- /** clipId → FRESH transpiled jsSource for component-backed `custom` overlays
631
- * (bakeless live lane). Materialized per render from `installedComponents`
632
- * (renders.ts) — the live-lane peer of `alphaUrls`, so a component edit is
633
- * reflected on the next render with no re-save. Preferred over any stored
634
- * `params.sourceCode` (which only desktop-INLINE customs carry). */
635
- jsSourceByClipId = {}) {
636
- const overlays = [];
637
- for (let i = 0; i < track.contents.length; i++) {
638
- const c = track.contents[i];
639
- if (!("source" in c) || !c.source)
640
- continue;
641
- // Alpha-layer (2.6b): an OWNED .cueframe component baked to a TRANSPARENT
642
- // video (the component's props baked INTO the pixels — never run in the
643
- // shared render). The render only needs the baked file + its zPlane. The
644
- // file is resolved PER CLIP (alphaUrls[clipId]); the box bake produces it
645
- // and the daemon threads the map. Missing ⇒ fail loud (never a silent
646
- // empty layer): the bake must run before the render.
647
- if (c.source.kind === "alpha-layer") {
648
- const { startFrame, durationInFrames } = timelineSpanFrames(c.startTime, c.startTime + c.duration, fps);
649
- if (durationInFrames === 0)
650
- continue;
651
- const src = alphaUrls[c.id];
652
- if (!src) {
653
- throw new Error(`composition.tracks[${track.id}].contents[${i}]: alpha-layer clip ${c.id} has no baked alpha (alphaUrls[${c.id}] missing) — bake before render`);
654
- }
655
- overlays.push({
656
- id: c.id,
657
- template: "alpha-layer",
658
- startFrame,
659
- durationInFrames,
660
- params: { src },
661
- ...(c.source.zPlane !== undefined ? { zPlane: c.source.zPlane } : {}),
662
- ...(c.region !== undefined ? { region: c.region } : {}),
663
- ...(c.fit !== undefined ? { fit: c.fit } : {}),
664
- ...overlayTransitionFields(c, fps),
665
- ...clipTransformChannel(c),
666
- });
667
- continue;
668
- }
669
- // card: an authored HTML/CSS graphic drawn VERBATIM by the wrapper
670
- // (OverlayContent case 'card'). Carry the markup + brand-token map
671
- // through untouched; brand refs resolve at the render boundary (like every
672
- // overlay). The clip's `region` is the keep-out box. Front only this slice.
673
- if (c.source.kind === "card") {
674
- const { startFrame, durationInFrames } = timelineSpanFrames(c.startTime, c.startTime + c.duration, fps);
675
- if (durationInFrames === 0)
676
- continue;
677
- overlays.push({
678
- id: c.id,
679
- template: "card",
680
- startFrame,
681
- durationInFrames,
682
- params: {
683
- html: c.source.html,
684
- ...(c.source.tokens ? { cssTokens: c.source.tokens } : {}),
685
- },
686
- ...(c.source.zPlane !== undefined ? { zPlane: c.source.zPlane } : {}),
687
- ...(c.region !== undefined ? { region: c.region } : {}),
688
- ...(c.fit !== undefined ? { fit: c.fit } : {}),
689
- ...overlayTransitionFields(c, fps),
690
- ...clipTransformChannel(c),
691
- });
692
- continue;
693
- }
694
- if (c.source.kind !== "overlay") {
695
- throw new Error(`composition.tracks[${track.id}].contents[${i}]: kind=${c.source.kind} is invalid on an overlay track (expected 'overlay', 'alpha-layer', or 'card')`);
696
- }
697
- const { startFrame, durationInFrames } = timelineSpanFrames(c.startTime, c.startTime + c.duration, fps);
698
- // Sub-frame durations would crash Remotion's <Sequence>. Drop the overlay.
699
- if (durationInFrames === 0)
700
- continue;
701
- let overlayParams = (c.source.params ?? {});
702
- // primitiveId="custom" — the DynamicOverlay/new Function sandbox lane. Its
703
- // source resolves in one of two ways:
704
- // - component-backed (bakeless live lane): a FRESH per-clip jsSource from
705
- // `jsSourceByClipId` (materialized this render from installedComponents),
706
- // which WINS over any stored value so a component edit shows next render.
707
- // - desktop-INLINE: the transpiled code the author stored on
708
- // `params.sourceCode`.
709
- // A custom with NEITHER would otherwise render "Custom graphic error: No
710
- // sourceCode in params" into the frame (the in-loop judge scores that as
711
- // editorial copy). This is the single render chokepoint every path passes
712
- // through — fail loud, not a fallback. The component-reference marker
713
- // (`params.componentId`) + any superseded stored source are stripped from the
714
- // params handed to the sandbox, so the component sees only its own props.
715
- if (c.source.primitiveId === "custom") {
716
- const fresh = jsSourceByClipId[c.id];
717
- const sc = typeof fresh === "string" && fresh.length > 0 ? fresh : overlayParams.sourceCode;
718
- if (typeof sc !== "string" || sc.length === 0) {
719
- throw new Error(`composition.tracks[${track.id}].contents[${i}].source: custom overlay has no sourceCode ` +
720
- `(no fresh jsSource for clip '${c.id}' and no inline params.sourceCode) — mirror code 'custom_missing_sourcecode'`);
721
- }
722
- const { componentId: _cid, sourceCode: _sc, ...props } = overlayParams;
723
- void _cid;
724
- void _sc;
725
- overlayParams = { sourceCode: sc, ...props };
726
- }
727
- overlays.push({
728
- id: c.id,
729
- template: c.source.primitiveId,
730
- startFrame,
731
- durationInFrames,
732
- params: overlayParams,
733
- ...(c.source.zPlane !== undefined ? { zPlane: c.source.zPlane } : {}),
734
- ...(c.source.anchor !== undefined ? { anchor: c.source.anchor } : {}),
735
- ...(c.source.parallax !== undefined ? { parallax: c.source.parallax } : {}),
736
- // Output-space placement (clip-level, like media/image): the overlay
737
- // primitive occupies only this sub-rect of the output (PiP / inset card).
738
- // MainComposition wraps it in the same RegionBox; absent = full frame.
739
- ...(c.region !== undefined ? { region: c.region } : {}),
740
- ...(c.fit !== undefined ? { fit: c.fit } : {}),
741
- ...overlayTransitionFields(c, fps),
742
- ...clipTransformChannel(c),
743
- });
744
- }
745
- return overlays;
746
- }
747
- function projectEffectTrack(track, fps) {
748
- const effects = [];
749
- for (let i = 0; i < track.contents.length; i++) {
750
- const c = track.contents[i];
751
- if (!("source" in c) || !c.source)
752
- continue;
753
- if (c.source.kind !== "effect") {
754
- throw new Error(`composition.tracks[${track.id}].contents[${i}]: kind=${c.source.kind} is invalid on an effect track (expected 'effect')`);
755
- }
756
- const { startFrame, durationInFrames } = timelineSpanFrames(c.startTime, c.startTime + c.duration, fps);
757
- // Sub-frame durations would crash Remotion's <Sequence>. Drop the effect.
758
- if (durationInFrames === 0)
759
- continue;
760
- effects.push({
761
- id: c.id,
762
- primitiveId: c.source.primitiveId,
763
- params: (c.source.params ?? {}),
764
- target: { kind: "global" },
765
- startFrame,
766
- durationInFrames,
767
- });
768
- }
769
- return effects;
770
- }
771
- /**
772
- * Merge the composition's caption words into speech spans (gap < 300ms merges
773
- * adjacent words; spans < 200ms are dropped as noise), returned as ABSOLUTE
774
- * timeline frame ranges. This is the speech SSOT the duck envelope reads — it
775
- * ripples with every cut/ripple because caption words do.
776
- */
777
- function buildCaptionSpeechSpans(composition, fps) {
778
- const words = (composition.captions?.segments ?? [])
779
- .flatMap((seg) => seg.words)
780
- .filter((w) => w.endMs > w.startMs)
781
- .sort((a, b) => a.startMs - b.startMs);
782
- if (words.length === 0)
783
- return [];
784
- const MERGE_GAP_MS = 300;
785
- const MIN_SPAN_MS = 200;
786
- const spans = [];
787
- let cur = { startMs: words[0].startMs, endMs: words[0].endMs };
788
- for (let i = 1; i < words.length; i++) {
789
- const w = words[i];
790
- if (w.startMs - cur.endMs < MERGE_GAP_MS) {
791
- if (w.endMs > cur.endMs)
792
- cur.endMs = w.endMs;
793
- }
794
- else {
795
- spans.push(cur);
796
- cur = { startMs: w.startMs, endMs: w.endMs };
797
- }
798
- }
799
- spans.push(cur);
800
- return spans
801
- .filter((s) => s.endMs - s.startMs >= MIN_SPAN_MS)
802
- .map((s) => ({
803
- startFrame: Math.round((s.startMs / 1000) * fps),
804
- endFrame: Math.round((s.endMs / 1000) * fps),
805
- }));
806
- }
807
- /**
808
- * Role-based DEFAULT gain staging — a post-projection pass over the already-built
809
- * `audioTracks` (audio-substrate P0, Phase 3: "the engine owns the default mix").
810
- * It lives HERE, not in `projectMediaTrack`, because the music default is
811
- * CONDITIONAL on whether the WHOLE composition contains VO — track scope isn't
812
- * enough (same reason `applyDuckEnvelopes` is a post-projection pass).
813
- *
814
- * Rule: an authored `volumeDb`/`volume` ALWAYS wins (the projection already baked
815
- * it into `track.volume` at line ~362), so this only touches audio-track clips
816
- * whose source is role-tagged AND carries NEITHER authored level — it stamps the
817
- * calibrated ROLE_DEFAULT_GAIN_DB in its place. A clip with NO `audioRole` keeps
818
- * today's unity (the engine never guesses a role). This is the SINGLE default
819
- * point every render lane flows through (baked + live both call
820
- * `compositionToRenderInput`), so it can never diverge across lanes. AUDIO-only —
821
- * video-clip embedded audio is untouched (a VO video clip's default is 0 dB =
822
- * unity = the renderer's existing default, so there is nothing to change).
823
- */
824
- function applyRoleDefaultGains(audioTracks, composition) {
825
- let voPresent;
826
- for (const track of composition.tracks) {
827
- if (track.kind !== "audio" || track.enabled === false)
828
- continue;
829
- for (const c of track.contents) {
830
- if (!("source" in c))
831
- continue; // skip Gaps
832
- const src = c.source;
833
- if (src.kind !== "media")
834
- continue;
835
- const role = src.audioRole;
836
- if (!role)
837
- continue; // no role → unity preserved (today's behaviour)
838
- if (src.volumeDb !== undefined || src.volume !== undefined)
839
- continue; // authored wins
840
- // Resolve VO presence once, lazily (only when a role-tagged unauthored clip
841
- // actually needs the music-conditional).
842
- if (voPresent === undefined)
843
- voPresent = compositionHasVo(composition);
844
- const gain = Math.pow(10, roleDefaultGainDb(role, voPresent) / 20);
845
- for (const t of audioTracks) {
846
- if (t.id === c.id || t.id.startsWith(`${c.id}#`))
847
- t.volume = gain;
848
- }
849
- }
850
- }
851
- }
852
- /**
853
- * Speech-window duck resolution — a post-projection pass over the already-built
854
- * `audioTracks`. It lives HERE (not in `projectMediaTrack`, which has no
855
- * `composition`/`captions` in scope — review correction A). For each audio clip
856
- * whose source carries `duck` and a `music`/`vo` role, resolve the speech windows
857
- * (from caption words, else the source's explicit `duck.windows`) into
858
- * SEQUENCE-RELATIVE frame ranges and stamp them onto the matching projected
859
- * track(s). Only `music`/`vo` roles duck; SFX (`audioRole:'sfx'`) and AMBIENCE
860
- * (`audioRole:'ambience'`) are NEVER ducked — ambience HOLDS under VO (founder
861
- * ruling, audio-substrate P0), so a stray `duck` on an ambience source is inert
862
- * here. Pure data → data.
863
- */
864
- function applyDuckEnvelopes(audioTracks, composition, fps) {
865
- const captionSpansFrames = buildCaptionSpeechSpans(composition, fps);
866
- for (const track of composition.tracks) {
867
- if (track.kind !== "audio" || track.enabled === false)
868
- continue;
869
- for (const c of track.contents) {
870
- if (!("source" in c))
871
- continue; // skip Gaps (no source), same discrimination as the projection
872
- const src = c.source;
873
- if (src.kind !== "media")
874
- continue;
875
- const duck = src.duck;
876
- const role = src.audioRole;
877
- if (!duck || (role !== "music" && role !== "vo"))
878
- continue;
879
- // Explicit windows win when present; otherwise the caption-derived spans.
880
- // The wire invariant forbids setting both (one SSOT) — enforced upstream.
881
- const absWindows = duck.windows && duck.windows.length > 0
882
- ? duck.windows.map((w) => ({
883
- startFrame: Math.round((w.startMs / 1000) * fps),
884
- endFrame: Math.round((w.endMs / 1000) * fps),
885
- }))
886
- : captionSpansFrames;
887
- if (absWindows.length === 0)
888
- continue;
889
- const duckVolume = Math.pow(10, duck.duckDb / 20);
890
- const attackFrames = Math.max(0, Math.round((duck.attackMs / 1000) * fps));
891
- const releaseFrames = Math.max(0, Math.round((duck.releaseMs / 1000) * fps));
892
- // Stamp every projected track derived from this clip (single span = `c.id`;
893
- // excludedRange splits = `${c.id}#${s}`), clamping each window into the
894
- // track's [0, durationInFrames] sequence-relative domain.
895
- for (const t of audioTracks) {
896
- if (t.id !== c.id && !t.id.startsWith(`${c.id}#`))
897
- continue;
898
- const rel = [];
899
- for (const w of absWindows) {
900
- const s = Math.max(0, w.startFrame - t.startFrame);
901
- const e = Math.min(t.durationInFrames, w.endFrame - t.startFrame);
902
- if (e > s)
903
- rel.push({ startFrame: s, endFrame: e });
904
- }
905
- if (rel.length === 0)
906
- continue;
907
- t.duckWindows = rel;
908
- t.duckVolume = duckVolume;
909
- t.duckAttackFrames = attackFrames;
910
- t.duckReleaseFrames = releaseFrames;
911
- }
912
- }
913
- }
914
- }
915
- /**
916
- * Every unique mediaId the composition references — the SAME references
917
- * `compositionToRenderInput` resolves through `srcUrls` (any clip whose
918
- * `source.kind === 'media'`, on any track family). Interactive hosts (the
919
- * editor preview) gate on this BEFORE converting: a referenced id without a
920
- * URL yet is a LOADING state there, while the converter's fail-loud throw
921
- * stays correct for export/server paths where every URL must already exist.
922
- * Living next to the converter keeps the two walks from drifting.
923
- */
924
- export function referencedMediaIds(composition) {
925
- const ids = new Set();
926
- for (const track of composition.tracks) {
927
- for (const c of track.contents) {
928
- if (!("source" in c) || !c.source)
929
- continue;
930
- if (c.source.kind === "media" && typeof c.source.mediaId === "string") {
931
- ids.add(c.source.mediaId);
932
- }
933
- }
934
- }
935
- return [...ids];
936
- }
937
- /**
938
- * Every component-REFERENCE `custom` overlay clip, as `clipId → componentId` —
939
- * the SAME references `compositionToRenderInput` resolves through
940
- * `jsSourceByClipId` (bakeless live lane). Clips carrying their own inline
941
- * `params.sourceCode` (desktop-authored customs) need no materialization and
942
- * are excluded. Interactive hosts (the editor preview) gate on this BEFORE
943
- * converting and feed the fetched live `jsSource`s in — the converter's
944
- * fail-loud `custom_missing_sourcecode` throw stays correct for export/server
945
- * paths. Living next to the converter keeps the walks from drifting.
946
- */
947
- export function componentRefsByClipId(composition) {
948
- const refs = {};
949
- for (const track of composition.tracks) {
950
- if (track.kind !== "overlay")
951
- continue;
952
- for (const c of track.contents) {
953
- if (!("source" in c) || !c.source)
954
- continue;
955
- const src = c.source;
956
- if (src.kind !== "overlay" || src.primitiveId !== "custom")
957
- continue;
958
- const params = src.params ?? {};
959
- if (typeof params.sourceCode === "string" && params.sourceCode.length > 0)
960
- continue;
961
- if (typeof params.componentId === "string" && params.componentId.length > 0) {
962
- refs[c.id] = params.componentId;
963
- }
964
- }
965
- }
966
- return refs;
967
- }
968
- /**
969
- * Translate a wire-shape `Composition` to a frame-based `RenderInput`.
970
- *
971
- * The output's `durationInFrames` is the max content end across all
972
- * tracks (clips + overlays + effects). An empty composition produces
973
- * an empty RenderInput with duration 0 — caller decides whether that
974
- * should error or render a black frame; we don't make a policy call
975
- * here.
976
- */
977
- export function compositionToRenderInput(composition, srcUrls,
978
- /**
979
- * Optional parallel map: `clipId → presigned crop-trajectory JSON URL`
980
- * (keyed PER CLIP since Phase 2 — two clips of one media with different
981
- * framing each carry their own trajectory). Populated by the caller
982
- * (Temporal activity) when the mediaItem has a baked variant for the
983
- * clip's window-qualified (intentHash, aspect). The renderer fetches the
984
- * JSON at composition time and applies a per-frame CSS transform on the
985
- * raw video to produce the cropped output. When unset for a given clipId,
986
- * the renderer plays the raw source un-cropped.
987
- */
988
- trajectoryUrls,
989
- /**
990
- * Optional parallel map: `mediaId → presigned frame-packed co-view URL`
991
- * (scene over matte, one opaque H.264). Populated by the caller for any clip
992
- * whose timeline range overlaps a behind-subject caption window and whose
993
- * matte the gate baked (content-driven, Phase 6). Matte PRESENCE is the gate —
994
- * the render splits it into scene background + subject cut-out for the z-split.
995
- */
996
- coViewUrls,
997
- /**
998
- * Optional parallel map: `clipId → baked-alpha file URL/path` for alpha-layer
999
- * clips (2.6b). The box bake (renderComponentAlpha) writes the transparent
1000
- * video; the caller (session daemon / director) threads the path here. The
1001
- * renderer mounts it as an alpha overlay at the clip's zPlane.
1002
- */
1003
- alphaUrls,
1004
- /**
1005
- * Optional parallel map: `clipId → FRESH transpiled jsSource` for
1006
- * component-backed `custom` overlays (bakeless live lane). Materialized per
1007
- * render from `installedComponents` (renders.ts) — the live-lane peer of
1008
- * `alphaUrls`. The overlay projection prefers it over any stored
1009
- * `params.sourceCode`, so a component edit is reflected on the next render
1010
- * with no composition re-save.
1011
- */
1012
- jsSourceByClipId,
1013
- /**
1014
- * Compound-clip resolution: `compositionId → Composition` for every
1015
- * `{kind:'composition'}` source the composition (transitively) embeds.
1016
- * Flattening happens HERE, at the one translator every render lane flows
1017
- * through, so an embedded sequence renders identically on desktop, cloud
1018
- * and still lanes. A compound clip with no entry FAILS LOUD
1019
- * (`compound_unresolved`) rather than rendering a deliverable that is
1020
- * silently missing a sequence.
1021
- */
1022
- subCompositions) {
1023
- const flattened = hasCompoundClips(composition)
1024
- ? flattenCompoundClips(composition, (id) => subCompositions?.[id])
1025
- : composition;
1026
- composition = flattened;
1027
- const { width, height, fps } = compositionFormatDimensions(composition.format);
1028
- const clips = [];
1029
- const overlays = [];
1030
- const audioTracks = [];
1031
- const effects = [];
1032
- for (const track of composition.tracks) {
1033
- if (track.enabled === false)
1034
- continue;
1035
- if (track.kind === "video" || track.kind === "audio") {
1036
- const { clips: c, audio, effects: clipEffects } = projectMediaTrack(track, fps, srcUrls, trajectoryUrls, coViewUrls);
1037
- clips.push(...c);
1038
- audioTracks.push(...audio);
1039
- // Per-clip authored effects (target:'clip'), alongside the dedicated
1040
- // effect-track path (target:'global'). Both feed the one render effects[].
1041
- effects.push(...clipEffects);
1042
- }
1043
- else if (track.kind === "image") {
1044
- const { clips: c, effects: clipEffects } = projectImageTrack(track, fps, srcUrls);
1045
- clips.push(...c);
1046
- effects.push(...clipEffects);
1047
- }
1048
- else if (track.kind === "overlay") {
1049
- overlays.push(...projectOverlayTrack(track, fps, alphaUrls ?? {}, jsSourceByClipId ?? {}));
1050
- }
1051
- else if (track.kind === "effect") {
1052
- effects.push(...projectEffectTrack(track, fps));
1053
- }
1054
- }
1055
- // Role-based DEFAULT gain staging (Phase 3): stamp the calibrated engine
1056
- // default onto role-tagged audio clips that carry no authored level, BEFORE the
1057
- // duck pass (which layers a speech envelope on top of the base volume). Runs
1058
- // first so ducking operates on the correctly-staged base level. No-op unless a
1059
- // role-tagged clip lacks an authored volume.
1060
- if (audioTracks.length > 0) {
1061
- applyRoleDefaultGains(audioTracks, composition);
1062
- }
1063
- // Speech-aware ducking: resolve duck windows from caption words (SSOT) and
1064
- // stamp frame-range envelopes onto music/vo audio tracks. Post-projection so
1065
- // it has `composition.captions` in scope (correction A). No-op when no audio
1066
- // clip carries `duck`.
1067
- if (audioTracks.length > 0) {
1068
- applyDuckEnvelopes(audioTracks, composition, fps);
1069
- }
1070
- // durationInFrames covers the latest content edge across the whole
1071
- // timeline. Computed from the wire (not summed) so concurrent clips
1072
- // on stacked video tracks are handled correctly.
1073
- let maxEndFrames = 0;
1074
- const consider = (start, dur) => {
1075
- const end = start + dur;
1076
- if (end > maxEndFrames)
1077
- maxEndFrames = end;
1078
- };
1079
- for (const c of clips)
1080
- consider(c.timelineStartFrames, c.durationInFrames);
1081
- for (const o of overlays)
1082
- consider(o.startFrame, o.durationInFrames);
1083
- for (const a of audioTracks)
1084
- consider(a.startFrame, a.durationInFrames);
1085
- for (const e of effects) {
1086
- consider(e.startFrame ?? 0, e.durationInFrames ?? 0);
1087
- }
1088
- // VISUAL-DURATION CLAMP: caption lingers, decor-primitive overlays, audio
1089
- // tails, and effects must never extend the timeline past the last
1090
- // BACKGROUND-producing frame — frames beyond it have no source pixel and
1091
- // render solid black (waverly-final.mp4 2026-07-17: a ~2-frame black tail at
1092
- // 28.83-28.90s). Background-producing = video/image CLIPS **and SCENE-CLASS
1093
- // overlays** (alpha-layer / custom / card): a component world-layer or card IS
1094
- // the film's background in graphics-led and hybrid films. Without the
1095
- // scene-class term, a hybrid film whose background is a component overlay and
1096
- // whose only video clips are inset b-roll clamps to the b-roll's end and
1097
- // silently truncates (memoir-launch 41.5s → 6.9s, 2026-07-21). Decor
1098
- // primitive overlays (lower-thirds, badges) and caption pages still clamp —
1099
- // the original black-tail fix is preserved. When nothing background-producing
1100
- // exists, the overlay/effect edge legitimately defines the duration.
1101
- const SCENE_OVERLAY_TEMPLATES = new Set(["alpha-layer", "custom", "card"]);
1102
- let maxBackgroundEndFrames = 0;
1103
- for (const c of clips) {
1104
- const end = c.timelineStartFrames + c.durationInFrames;
1105
- if (end > maxBackgroundEndFrames)
1106
- maxBackgroundEndFrames = end;
1107
- }
1108
- for (const o of overlays) {
1109
- if (!SCENE_OVERLAY_TEMPLATES.has(o.template))
1110
- continue;
1111
- const end = o.startFrame + o.durationInFrames;
1112
- if (end > maxBackgroundEndFrames)
1113
- maxBackgroundEndFrames = end;
1114
- }
1115
- const bodyDurationInFrames = maxBackgroundEndFrames > 0 ? Math.min(maxEndFrames, maxBackgroundEndFrames) : maxEndFrames;
1116
- // Carry the authored caption SEGMENTS through (dropping empty ones); each
1117
- // segment is a hard caption-page boundary at render — the renderer paginates
1118
- // within a segment, never across — so sentence-bounded segments cut captions
1119
- // at sentence ends. Depth is overlay-only now (captions are FRONT-ONLY).
1120
- const captionSegments = composition.captions?.segments
1121
- .map((seg) => seg.words)
1122
- .filter((words) => words.length > 0);
1123
- // Frozen brand kit pass-through. The render snapshot
1124
- // (renderJobs.composition, see services/convex/api/internal/renders.ts)
1125
- // attaches the already-resolved brand kit at `composition.brandKit`.
1126
- // `Composition` is a `.passthrough()` schema so the field rides through
1127
- // untyped — read it back here so the renderer's primitive brand-resolution
1128
- // path is actually fed. Without this, RenderInput.brandKit is always
1129
- // undefined and brand-bound primitive defaults fall to their hardcoded
1130
- // `fallback`, producing off-brand output.
1131
- const brandKit = composition.brandKit;
1132
- // Brand BOOKENDS — when the resolved kit carries an intro/outro bumper, the
1133
- // render prepends the intro and appends the outro (MainComposition places +
1134
- // shifts the body); the composition total extends by intro+outro. fps-driven
1135
- // and SOFT (a missing/odd/srcless bumper reserves 0 frames → no extension).
1136
- const { totalFrames: durationInFrames } = resolveBookendTimeline(brandKit, bodyDurationInFrames, fps);
1137
- // Caption identity from the brand kit. Per-creator caption look is BRAND DATA:
1138
- // the resolved kit carries `caption.{ fg, fontFamily, emphasisFontFamily,
1139
- // emphasisScale, emphasisFontWeight, emphasisFontStyle }` (renders.ts →
1140
- // resolveBrandKit, derived from the dashboard / a creator's references). Here
1141
- // we build the caption style from that identity. Base GEOMETRY (size, screen
1142
- // position, safe-area) is NOT brand — it's the Captions primitive's calibrated
1143
- // DEFAULT_STYLE, which unset fields fall through to. So the kit selects the
1144
- // LOOK (serif-vs-sans emphasis, scale, weight, italic, accent); the primitive
1145
- // owns the legible realization.
1146
- //
1147
- // There is no caption VARIANT anymore — the old free-text-hint → variant
1148
- // picker is deleted. A clip with no kit rides the primitive default; a clip
1149
- // with a kit but no caption identity gets only the brand accent on the
1150
- // active/emphasis word (today's emphasisColor-only behavior, preserved).
1151
- //
1152
- // RenderInput's `brandKit` is typed against core's sparse BrandKit (no caption
1153
- // token), so the resolved-kit fields ride through untyped — read via a narrow
1154
- // accessor. An explicit per-clip `captions.style` override (rare) is the base
1155
- // the identity is applied over.
1156
- const cap = brandKit?.caption;
1157
- const brandAccent = cap?.fg ??
1158
- brandKit?.colors?.accent;
1159
- // Brand MOTION → caption entrance. Applied when the brand authored one of the
1160
- // caption-supported animated entrances ('fade' | 'word-pop' | 'stagger-up' —
1161
- // the shared TextMotion vocabulary the Captions primitive renders). A
1162
- // default/none/absent/unknown brand entrance leaves captions on the
1163
- // primitive's calibrated default (SOFT — never throws), so existing kits
1164
- // render identically. duration/staggerMs/exitMs ride the brand's values.
1165
- const mot = brandKit?.motion;
1166
- const ANIMATED_CAPTION_ENTRANCES = ["fade", "word-pop", "stagger-up"];
1167
- const captionEntrance = ANIMATED_CAPTION_ENTRANCES.includes(mot?.entrance)
1168
- ? mot.entrance
1169
- : undefined;
1170
- const captionMotion = captionEntrance
1171
- ? {
1172
- entrance: captionEntrance,
1173
- ...(mot.duration != null ? { entranceMs: mot.duration } : {}),
1174
- ...(mot.staggerMs != null ? { entranceStaggerMs: mot.staggerMs } : {}),
1175
- ...(mot.exitMs != null ? { exitMs: mot.exitMs } : {}),
1176
- }
1177
- : undefined;
1178
- const baseStyle = composition.captions?.style;
1179
- const hasCaptions = captionSegments != null && captionSegments.length > 0;
1180
- const captionStyle = hasCaptions && (cap || baseStyle || captionMotion)
1181
- ? {
1182
- ...(baseStyle ?? {}),
1183
- ...(cap?.fontFamily ? { fontFamily: cap.fontFamily } : {}),
1184
- ...(cap?.color ? { color: cap.color } : {}),
1185
- ...(cap?.plate ? { plate: cap.plate } : {}),
1186
- ...(cap?.emphasisFontFamily ? { emphasisFontFamily: cap.emphasisFontFamily } : {}),
1187
- ...(cap?.emphasisScale != null ? { emphasisScale: cap.emphasisScale } : {}),
1188
- ...(cap?.emphasisFontWeight != null ? { emphasisFontWeight: cap.emphasisFontWeight } : {}),
1189
- ...(cap?.emphasisFontStyle ? { emphasisFontStyle: cap.emphasisFontStyle } : {}),
1190
- ...(cap?.emphasisTextTransform ? { emphasisTextTransform: cap.emphasisTextTransform } : {}),
1191
- ...(cap?.emphasisLetterSpacing != null ? { emphasisLetterSpacing: cap.emphasisLetterSpacing } : {}),
1192
- ...(cap?.emphasisPlate != null ? { emphasisPlate: cap.emphasisPlate } : {}),
1193
- ...(cap?.emphasisPlateColor ? { emphasisPlateColor: cap.emphasisPlateColor } : {}),
1194
- ...(cap?.emphasisPlateTextColor ? { emphasisPlateTextColor: cap.emphasisPlateTextColor } : {}),
1195
- // Brand accent → active/emphasis word color (today's behavior); a
1196
- // creator-set caption highlightColor is caption-specific identity and
1197
- // WINS over the generic accent.
1198
- ...(brandAccent ? { highlightColor: brandAccent, emphasisColor: brandAccent } : {}),
1199
- ...(cap?.highlightColor ? { highlightColor: cap.highlightColor } : {}),
1200
- // Brand-motion-derived caption entrance is the generic fallback; a
1201
- // caption-specific entrance (cap.*) is identity and WINS, so it spreads
1202
- // AFTER captionMotion.
1203
- ...(captionMotion ?? {}),
1204
- ...(cap?.entrance ? { entrance: cap.entrance } : {}),
1205
- ...(cap?.entranceMs != null ? { entranceMs: cap.entranceMs } : {}),
1206
- ...(cap?.entranceStaggerMs != null ? { entranceStaggerMs: cap.entranceStaggerMs } : {}),
1207
- ...(cap?.exitMs != null ? { exitMs: cap.exitMs } : {}),
1208
- }
1209
- : undefined;
1210
- return {
1211
- clips,
1212
- overlays,
1213
- width,
1214
- height,
1215
- fps,
1216
- durationInFrames,
1217
- // Target platform (from the wire `Format`) frozen onto the render input so the
1218
- // composition floor's safe-zone placement clears that surface's UI chrome.
1219
- // Absent ⇒ the floor's 'unknown' (broadcast) default. Output-determining, so
1220
- // it's part of the render cache key (RENDER_CACHE_VERSION bumped alongside).
1221
- ...(composition.format.platform !== undefined ? { platform: composition.format.platform } : {}),
1222
- ...(audioTracks.length > 0 ? { audioTracks } : {}),
1223
- ...(effects.length > 0 ? { effects } : {}),
1224
- ...(hasCaptions ? { captionSegments } : {}),
1225
- ...(captionStyle ? { captionStyle } : {}),
1226
- ...(brandKit ? { brandKit } : {}),
1227
- // (fitMode deleted 2026-05-25 — baked variants replace render-time fit
1228
- // decisions; the chosen variant URL IS the framing.)
1229
- };
1230
- }
1231
- /**
1232
- * Assemble a RenderInput from an ALREADY-validated (and brand-ref-resolved)
1233
- * composition plus PRE-RESOLVED asset URL maps. This is the pure core shared by
1234
- * the director's `resolveRenderEnvironment` (which resolves the URLs with R2/S3
1235
- * creds, host-side) AND the in-container session daemon (which is handed the URLs
1236
- * and holds no creds). One builder ⇒ render and capture cannot diverge on the
1237
- * composition→RenderInput translation.
1238
- *
1239
- * `fontAssets` (when present) are the host-presigned brand font faces; they are
1240
- * attached to the RenderInput's brandKit so the renderer can `@font-face` them.
1241
- * The caller does the cred-bound URL/font resolution; this only assembles.
1242
- */
1243
- export function assembleRenderInput(composition, resolved) {
1244
- const renderInput = compositionToRenderInput(composition, resolved.srcUrls, resolved.trajectoryUrls ?? {}, resolved.coViewUrls ?? {}, resolved.alphaUrls ?? {}, resolved.jsSourceByClipId ?? {});
1245
- if (renderInput.brandKit && resolved.fontAssets && resolved.fontAssets.length > 0) {
1246
- renderInput.brandKit.fontAssets = resolved.fontAssets;
1247
- }
1248
- return renderInput;
1249
- }