@hraness/slopcamera 3.2.4

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 (441) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +60 -0
  3. package/PRIVACY.md +46 -0
  4. package/README.md +311 -0
  5. package/SECURITY.md +21 -0
  6. package/apps/desktop/README.md +966 -0
  7. package/apps/desktop/analysis/protocol.ts +223 -0
  8. package/apps/desktop/application/capability-binding.ts +594 -0
  9. package/apps/desktop/application/context.ts +97 -0
  10. package/apps/desktop/application/creative-iteration.ts +1385 -0
  11. package/apps/desktop/application/default-registry.ts +146 -0
  12. package/apps/desktop/application/directing-contract.ts +218 -0
  13. package/apps/desktop/application/directing-plan.ts +178 -0
  14. package/apps/desktop/application/errors.ts +18 -0
  15. package/apps/desktop/application/gateway-port.ts +867 -0
  16. package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
  17. package/apps/desktop/application/html-overlay-integrity.ts +337 -0
  18. package/apps/desktop/application/html-overlay-renderer.ts +45 -0
  19. package/apps/desktop/application/index.ts +13 -0
  20. package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
  21. package/apps/desktop/application/operation-effects.ts +94 -0
  22. package/apps/desktop/application/operation.ts +224 -0
  23. package/apps/desktop/application/operations/analysis/faces.ts +244 -0
  24. package/apps/desktop/application/operations/analysis/music.ts +248 -0
  25. package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
  26. package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
  27. package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
  28. package/apps/desktop/application/operations/analysis/shared.ts +202 -0
  29. package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
  30. package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
  31. package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
  32. package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
  33. package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
  34. package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
  35. package/apps/desktop/application/operations/gateway/image.ts +41 -0
  36. package/apps/desktop/application/operations/gateway/index.ts +17 -0
  37. package/apps/desktop/application/operations/gateway/shared.ts +122 -0
  38. package/apps/desktop/application/operations/gateway/speech.ts +41 -0
  39. package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
  40. package/apps/desktop/application/operations/gateway/video.ts +41 -0
  41. package/apps/desktop/application/operations/index.ts +31 -0
  42. package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
  43. package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
  44. package/apps/desktop/application/operations/iteration/select.ts +155 -0
  45. package/apps/desktop/application/operations/iteration/shared.ts +250 -0
  46. package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
  47. package/apps/desktop/application/operations/media/binding.ts +79 -0
  48. package/apps/desktop/application/operations/media/capabilities.ts +112 -0
  49. package/apps/desktop/application/operations/media/color-grade.ts +343 -0
  50. package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
  51. package/apps/desktop/application/operations/media/index.ts +8 -0
  52. package/apps/desktop/application/operations/media/ingest.ts +303 -0
  53. package/apps/desktop/application/operations/media/overlay.ts +729 -0
  54. package/apps/desktop/application/operations/media/shared.ts +845 -0
  55. package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
  56. package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
  57. package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
  58. package/apps/desktop/application/operations/project/snapshot.ts +95 -0
  59. package/apps/desktop/application/operations/recording/index.ts +5 -0
  60. package/apps/desktop/application/operations/recording/pause.ts +34 -0
  61. package/apps/desktop/application/operations/recording/resume.ts +34 -0
  62. package/apps/desktop/application/operations/recording/shared.ts +199 -0
  63. package/apps/desktop/application/operations/recording/start.ts +169 -0
  64. package/apps/desktop/application/operations/recording/stop.ts +34 -0
  65. package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
  66. package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
  67. package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
  68. package/apps/desktop/application/operations/render/project-plan.ts +939 -0
  69. package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
  70. package/apps/desktop/application/operations/render/project.ts +1922 -0
  71. package/apps/desktop/application/operations/shared.ts +67 -0
  72. package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
  73. package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
  74. package/apps/desktop/application/operations/spatial-project.ts +209 -0
  75. package/apps/desktop/application/operations/spatial-render.ts +415 -0
  76. package/apps/desktop/application/operations/spatial-scene.ts +97 -0
  77. package/apps/desktop/application/operations/studio.ts +143 -0
  78. package/apps/desktop/application/output-publication-lease.ts +163 -0
  79. package/apps/desktop/application/project-publication-lease.ts +233 -0
  80. package/apps/desktop/application/project-store.ts +196 -0
  81. package/apps/desktop/application/receipts.ts +533 -0
  82. package/apps/desktop/application/registry.ts +182 -0
  83. package/apps/desktop/application/spatial-assets.ts +675 -0
  84. package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
  85. package/apps/desktop/application/spatial-project-authority.ts +35 -0
  86. package/apps/desktop/application/spatial-project-lease.ts +126 -0
  87. package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
  88. package/apps/desktop/application/spatial-project-store.ts +286 -0
  89. package/apps/desktop/application/spatial-render-projection.ts +201 -0
  90. package/apps/desktop/application/spatial-render.ts +677 -0
  91. package/apps/desktop/application/spatial-spz.ts +44 -0
  92. package/apps/desktop/application/spatial-world-import.ts +135 -0
  93. package/apps/desktop/application/spatial-world-provenance.ts +46 -0
  94. package/apps/desktop/application/studio-port.ts +58 -0
  95. package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
  96. package/apps/desktop/capture/protocol.ts +689 -0
  97. package/apps/desktop/cli/analyzer.ts +406 -0
  98. package/apps/desktop/cli/args.ts +3181 -0
  99. package/apps/desktop/cli/asset-ingest.ts +747 -0
  100. package/apps/desktop/cli/atomic-render-effects.ts +210 -0
  101. package/apps/desktop/cli/atomic-render-platform.ts +205 -0
  102. package/apps/desktop/cli/atomic-render.ts +50 -0
  103. package/apps/desktop/cli/audio-analysis.ts +252 -0
  104. package/apps/desktop/cli/bounded-map.ts +39 -0
  105. package/apps/desktop/cli/bundle-service.ts +256 -0
  106. package/apps/desktop/cli/capabilities.ts +212 -0
  107. package/apps/desktop/cli/capture-bundle.ts +1117 -0
  108. package/apps/desktop/cli/command-host-resources.ts +251 -0
  109. package/apps/desktop/cli/commands.ts +6836 -0
  110. package/apps/desktop/cli/directing-blob.ts +291 -0
  111. package/apps/desktop/cli/directing-media.ts +517 -0
  112. package/apps/desktop/cli/directing-quote.ts +89 -0
  113. package/apps/desktop/cli/directing-service.ts +288 -0
  114. package/apps/desktop/cli/emoji-assets.ts +538 -0
  115. package/apps/desktop/cli/errors.ts +29 -0
  116. package/apps/desktop/cli/face-analysis-service.ts +940 -0
  117. package/apps/desktop/cli/filter-script.ts +76 -0
  118. package/apps/desktop/cli/gateway-application-port.ts +1142 -0
  119. package/apps/desktop/cli/gateway-credential.ts +81 -0
  120. package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
  121. package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
  122. package/apps/desktop/cli/gateway-media-service.ts +3106 -0
  123. package/apps/desktop/cli/gateway-media-signature.ts +122 -0
  124. package/apps/desktop/cli/gateway-provider-options.ts +562 -0
  125. package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
  126. package/apps/desktop/cli/help.ts +501 -0
  127. package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
  128. package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
  129. package/apps/desktop/cli/io.ts +290 -0
  130. package/apps/desktop/cli/main.ts +123 -0
  131. package/apps/desktop/cli/media-effects-service.ts +885 -0
  132. package/apps/desktop/cli/media-ingest-model.ts +364 -0
  133. package/apps/desktop/cli/media-ingest-platform.ts +227 -0
  134. package/apps/desktop/cli/media-ingest-program.ts +200 -0
  135. package/apps/desktop/cli/media-ingest.ts +30 -0
  136. package/apps/desktop/cli/music-analysis-service.ts +434 -0
  137. package/apps/desktop/cli/mutation-lock.ts +530 -0
  138. package/apps/desktop/cli/native-assets.d.ts +12 -0
  139. package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
  140. package/apps/desktop/cli/overlay-playback.ts +177 -0
  141. package/apps/desktop/cli/paths.ts +265 -0
  142. package/apps/desktop/cli/portable-surface.ts +242 -0
  143. package/apps/desktop/cli/project-camera-receipt.ts +112 -0
  144. package/apps/desktop/cli/project-face-camera.ts +394 -0
  145. package/apps/desktop/cli/project-inactivity-service.ts +552 -0
  146. package/apps/desktop/cli/project-media-integrity.ts +121 -0
  147. package/apps/desktop/cli/project-renderer.ts +1526 -0
  148. package/apps/desktop/cli/project-service.ts +519 -0
  149. package/apps/desktop/cli/project-state-transaction.ts +520 -0
  150. package/apps/desktop/cli/query-limits.ts +1 -0
  151. package/apps/desktop/cli/recording-controller.ts +1567 -0
  152. package/apps/desktop/cli/recording-daemon.ts +931 -0
  153. package/apps/desktop/cli/recording-ref.ts +56 -0
  154. package/apps/desktop/cli/renamed-environment.ts +6 -0
  155. package/apps/desktop/cli/renderer.ts +1439 -0
  156. package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
  157. package/apps/desktop/cli/scene-analysis-service.ts +879 -0
  158. package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
  159. package/apps/desktop/cli/spatial-project-service.ts +90 -0
  160. package/apps/desktop/cli/spatial-scene-service.ts +105 -0
  161. package/apps/desktop/cli/spatial-world-service.ts +39 -0
  162. package/apps/desktop/cli/speech-analysis-service.ts +682 -0
  163. package/apps/desktop/cli/studio-assemble.ts +58 -0
  164. package/apps/desktop/cli/studio-command.ts +60 -0
  165. package/apps/desktop/cli/studio-custody.ts +60 -0
  166. package/apps/desktop/cli/studio-encode.ts +216 -0
  167. package/apps/desktop/cli/studio-exr.ts +52 -0
  168. package/apps/desktop/cli/studio-files.ts +179 -0
  169. package/apps/desktop/cli/studio-output-validation.ts +162 -0
  170. package/apps/desktop/cli/studio-process.ts +145 -0
  171. package/apps/desktop/cli/studio-runtime.ts +77 -0
  172. package/apps/desktop/cli/studio-scaffold.ts +70 -0
  173. package/apps/desktop/cli/studio-service.ts +301 -0
  174. package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
  175. package/apps/desktop/cli/studio-template-names.ts +3 -0
  176. package/apps/desktop/cli/studio-workflow.ts +19 -0
  177. package/apps/desktop/cli/time.ts +25 -0
  178. package/apps/desktop/cli/workflow-code.ts +584 -0
  179. package/apps/desktop/cli/workflow-runs.ts +576 -0
  180. package/apps/desktop/code/advanced.ts +139 -0
  181. package/apps/desktop/code/application-node-planner.ts +1288 -0
  182. package/apps/desktop/code/compiler.ts +329 -0
  183. package/apps/desktop/code/contracts.ts +13 -0
  184. package/apps/desktop/code/creative-recipes.ts +94 -0
  185. package/apps/desktop/code/define-workflow.ts +115 -0
  186. package/apps/desktop/code/editing.ts +284 -0
  187. package/apps/desktop/code/file-candidate-provenance.ts +420 -0
  188. package/apps/desktop/code/file-candidate.ts +69 -0
  189. package/apps/desktop/code/graph-builder.ts +10 -0
  190. package/apps/desktop/code/host-resource-policy.ts +55 -0
  191. package/apps/desktop/code/plan-contracts.ts +159 -0
  192. package/apps/desktop/code/planning.ts +361 -0
  193. package/apps/desktop/code/public.ts +134 -0
  194. package/apps/desktop/code/reference-projection.ts +62 -0
  195. package/apps/desktop/code/run-contracts.ts +466 -0
  196. package/apps/desktop/code/run-store.ts +2478 -0
  197. package/apps/desktop/code/runtime-identity.ts +328 -0
  198. package/apps/desktop/code/scheduler.ts +3067 -0
  199. package/apps/desktop/code/semantic-builder.ts +1436 -0
  200. package/apps/desktop/code/source-bundle.ts +1641 -0
  201. package/apps/desktop/code/source-typecheck.ts +211 -0
  202. package/apps/desktop/code/testing.ts +79 -0
  203. package/apps/desktop/code/worker-client.ts +1675 -0
  204. package/apps/desktop/code/worker-effects.ts +141 -0
  205. package/apps/desktop/code/worker-entry.ts +435 -0
  206. package/apps/desktop/code/worker-lease-guardian.ts +179 -0
  207. package/apps/desktop/code/worker-process-identity.ts +484 -0
  208. package/apps/desktop/code/worker-protocol.ts +211 -0
  209. package/apps/desktop/code/workflow-effects.ts +132 -0
  210. package/apps/desktop/contracts/analysis.ts +949 -0
  211. package/apps/desktop/contracts/edit.ts +576 -0
  212. package/apps/desktop/contracts/index.ts +9 -0
  213. package/apps/desktop/contracts/media-effects.ts +139 -0
  214. package/apps/desktop/contracts/project-render.ts +644 -0
  215. package/apps/desktop/contracts/project.ts +740 -0
  216. package/apps/desktop/contracts/recording.ts +1434 -0
  217. package/apps/desktop/contracts/render.ts +334 -0
  218. package/apps/desktop/contracts/runtime.ts +155 -0
  219. package/apps/desktop/contracts/spatial-compositor.ts +58 -0
  220. package/apps/desktop/contracts/spatial-project.ts +159 -0
  221. package/apps/desktop/contracts/spatial-world.ts +48 -0
  222. package/apps/desktop/contracts/video-effects.ts +120 -0
  223. package/apps/desktop/core/alignment-apply.ts +400 -0
  224. package/apps/desktop/core/audio-alignment.ts +466 -0
  225. package/apps/desktop/core/auto-zoom.ts +209 -0
  226. package/apps/desktop/core/canonical-json.ts +14 -0
  227. package/apps/desktop/core/emoji.ts +210 -0
  228. package/apps/desktop/core/events.ts +409 -0
  229. package/apps/desktop/core/face-framing.ts +533 -0
  230. package/apps/desktop/core/face-tracks.ts +326 -0
  231. package/apps/desktop/core/inactivity.ts +137 -0
  232. package/apps/desktop/core/index.ts +30 -0
  233. package/apps/desktop/core/intervals.ts +123 -0
  234. package/apps/desktop/core/montage.ts +236 -0
  235. package/apps/desktop/core/music-analysis.ts +518 -0
  236. package/apps/desktop/core/plan.ts +244 -0
  237. package/apps/desktop/core/project-auto-zoom.ts +75 -0
  238. package/apps/desktop/core/project-camera-spatial.ts +676 -0
  239. package/apps/desktop/core/project-camera.ts +403 -0
  240. package/apps/desktop/core/project-captions.ts +888 -0
  241. package/apps/desktop/core/project-inactivity.ts +125 -0
  242. package/apps/desktop/core/project-metadata.ts +927 -0
  243. package/apps/desktop/core/project-plan.ts +375 -0
  244. package/apps/desktop/core/project-render-plan.ts +303 -0
  245. package/apps/desktop/core/project-time.ts +469 -0
  246. package/apps/desktop/core/render-plan.ts +442 -0
  247. package/apps/desktop/core/scene-cache.ts +174 -0
  248. package/apps/desktop/core/scene-sampling.ts +448 -0
  249. package/apps/desktop/core/spatial-compositor.ts +22 -0
  250. package/apps/desktop/core/spatial-durability.ts +99 -0
  251. package/apps/desktop/core/spatial-project.ts +169 -0
  252. package/apps/desktop/core/spatial-shot-clock.ts +32 -0
  253. package/apps/desktop/core/speech-analysis.ts +333 -0
  254. package/apps/desktop/core/storage.ts +970 -0
  255. package/apps/desktop/core/time-map.ts +91 -0
  256. package/apps/desktop/core/time.ts +61 -0
  257. package/apps/desktop/core/video-effects.ts +660 -0
  258. package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
  259. package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  260. package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  261. package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
  262. package/apps/desktop/dist/cli/main.js +5086 -0
  263. package/apps/desktop/html-overlay/catalog.ts +171 -0
  264. package/apps/desktop/html-overlay/contracts.ts +372 -0
  265. package/apps/desktop/html-overlay/execution-profile.ts +77 -0
  266. package/apps/desktop/html-overlay/index.ts +98 -0
  267. package/apps/desktop/html-overlay/libraries.ts +263 -0
  268. package/apps/desktop/html-overlay/random.ts +49 -0
  269. package/apps/desktop/html-overlay/runtime.ts +813 -0
  270. package/apps/desktop/html-overlay/scaffolds.ts +792 -0
  271. package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
  272. package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
  273. package/apps/desktop/html-overlay/spatial.ts +720 -0
  274. package/apps/desktop/studio/assets/README.md +39 -0
  275. package/apps/desktop/studio/assets/contracts.ts +129 -0
  276. package/apps/desktop/studio/assets/network.ts +64 -0
  277. package/apps/desktop/studio/assets/poly-haven.ts +148 -0
  278. package/apps/desktop/studio/drivers/blender_driver.py +466 -0
  279. package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
  280. package/apps/desktop/studio/education/driver.py +308 -0
  281. package/apps/desktop/studio/education/lesson.py +180 -0
  282. package/apps/desktop/studio/education/toolkit.py +249 -0
  283. package/apps/desktop/workflows/chaptered-demo.ts +48 -0
  284. package/apps/desktop/workflows/creative-iteration.ts +107 -0
  285. package/apps/desktop/workflows/creative-selection.ts +92 -0
  286. package/apps/desktop/workflows/fragments.ts +486 -0
  287. package/apps/desktop/workflows/index.ts +93 -0
  288. package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
  289. package/apps/desktop/workflows/scene-direction.ts +12 -0
  290. package/apps/desktop/workflows/social-variants.ts +75 -0
  291. package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
  292. package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
  293. package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  294. package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  295. package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
  296. package/dist/cli.js +479 -0
  297. package/dist/code/advanced.js +270 -0
  298. package/dist/code/index.js +2240 -0
  299. package/dist/generate.js +24 -0
  300. package/dist/host-resources.js +26 -0
  301. package/dist/index-42zsesc1.js +2257 -0
  302. package/dist/index-7308egqr.js +48 -0
  303. package/dist/index-8txs6fkn.js +712 -0
  304. package/dist/index-mcy8z0br.js +192 -0
  305. package/dist/index-p63wavx0.js +2029 -0
  306. package/dist/index-r7gdhmsp.js +424 -0
  307. package/dist/index-sh6xbav6.js +1219 -0
  308. package/dist/index-z1w83f81.js +4 -0
  309. package/dist/index-z7239b4h.js +2363 -0
  310. package/dist/index-zfeg9hmw.js +2013 -0
  311. package/dist/index.js +241 -0
  312. package/dist/operations.js +29 -0
  313. package/dist/skill-install-dh0ntqba.js +10 -0
  314. package/dist/vectorize/worker.js +142 -0
  315. package/dist/workflow.js +16 -0
  316. package/docs/README.md +38 -0
  317. package/docs/studio.md +215 -0
  318. package/examples/studio/blender/README.md +26 -0
  319. package/examples/studio/blender/character.py +152 -0
  320. package/examples/studio/blender/cloth.py +101 -0
  321. package/examples/studio/blender/color_chart.py +29 -0
  322. package/examples/studio/blender/fluid.py +78 -0
  323. package/examples/studio/blender/import_model.py +74 -0
  324. package/examples/studio/blender/product.py +53 -0
  325. package/examples/studio/blender/shaded_street.py +481 -0
  326. package/examples/studio/blender/studio_scene.py +147 -0
  327. package/examples/studio/cadquery/README.md +13 -0
  328. package/examples/studio/cadquery/bracket.py +36 -0
  329. package/examples/studio/cadquery/import_step.py +12 -0
  330. package/examples/studio/education/README.md +46 -0
  331. package/examples/studio/education/lesson.json +99 -0
  332. package/examples/studio/education/scene.py +7 -0
  333. package/examples/studio/hybrid-scene.ts +96 -0
  334. package/examples/studio/native-workflow.ts +14 -0
  335. package/examples/studio/vgpu/README.md +64 -0
  336. package/examples/studio/vgpu/heat-field.mjs +290 -0
  337. package/package.json +304 -0
  338. package/packages/scene/src/index.ts +1 -0
  339. package/packages/scene/src/scene.ts +215 -0
  340. package/schema/diagram.schema.json +334 -0
  341. package/skills/slopcamera/SKILL.md +45 -0
  342. package/skills/slopcamera/agents/openai.yaml +4 -0
  343. package/skills/slopcamera/references/customization.md +77 -0
  344. package/skills/slopcamera/references/diagrams.md +153 -0
  345. package/skills/slopcamera/references/directed-scenes.md +42 -0
  346. package/skills/slopcamera/references/directing-video.md +18 -0
  347. package/skills/slopcamera/references/educational-video.md +26 -0
  348. package/skills/slopcamera/references/gateway-media.md +134 -0
  349. package/skills/slopcamera/references/install.md +62 -0
  350. package/skills/slopcamera/references/native-studio.md +31 -0
  351. package/skills/slopcamera/references/reference-led-3d.md +140 -0
  352. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
  353. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
  354. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
  355. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
  356. package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
  357. package/skills/slopcamera/references/vectorization.md +29 -0
  358. package/skills/slopcamera/references/video-projects.md +187 -0
  359. package/skills/slopcamera/references/visual-communication.md +120 -0
  360. package/skills/slopcamera/references/workflows-sdk.md +45 -0
  361. package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
  362. package/src/artifacts.ts +106 -0
  363. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  364. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
  365. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
  366. package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
  367. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  368. package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
  369. package/src/assets.d.ts +9 -0
  370. package/src/cli.ts +601 -0
  371. package/src/cloud-errors.ts +26 -0
  372. package/src/code/advanced.ts +10 -0
  373. package/src/code/boundary.ts +21 -0
  374. package/src/code/canonical-json.ts +129 -0
  375. package/src/code/compiler.ts +953 -0
  376. package/src/code/contracts.ts +576 -0
  377. package/src/code/define-workflow.ts +334 -0
  378. package/src/code/errors.ts +41 -0
  379. package/src/code/graph-builder.ts +945 -0
  380. package/src/code/index.ts +121 -0
  381. package/src/code/json-snapshot.ts +899 -0
  382. package/src/code/json-utf8.ts +88 -0
  383. package/src/code/portable-builder.ts +136 -0
  384. package/src/code/projection.ts +378 -0
  385. package/src/code/public-operations.ts +450 -0
  386. package/src/code/runtime.ts +580 -0
  387. package/src/code/sha256.ts +36 -0
  388. package/src/config.ts +180 -0
  389. package/src/desktop.ts +253 -0
  390. package/src/fs.ts +10 -0
  391. package/src/generate.ts +704 -0
  392. package/src/host-resource-posix.ts +161 -0
  393. package/src/host-resources.ts +1749 -0
  394. package/src/icons.ts +58 -0
  395. package/src/index.ts +200 -0
  396. package/src/label-layout.ts +130 -0
  397. package/src/layout.ts +413 -0
  398. package/src/lint.ts +127 -0
  399. package/src/mcp/boundary.ts +354 -0
  400. package/src/mcp/index.ts +33 -0
  401. package/src/mcp/server.ts +272 -0
  402. package/src/mcp/tools.ts +963 -0
  403. package/src/mcp/types.ts +71 -0
  404. package/src/operations.ts +775 -0
  405. package/src/parse.ts +861 -0
  406. package/src/process-environment.ts +17 -0
  407. package/src/render.ts +437 -0
  408. package/src/skill-install.ts +49 -0
  409. package/src/spatial-scene/authoring.ts +27 -0
  410. package/src/spatial-scene/camera-track.ts +83 -0
  411. package/src/spatial-scene/contracts.ts +277 -0
  412. package/src/spatial-scene/evaluate.ts +145 -0
  413. package/src/spatial-scene/gltf.ts +558 -0
  414. package/src/spatial-scene/identity.ts +260 -0
  415. package/src/spatial-scene/index.ts +10 -0
  416. package/src/spatial-scene/inspect.ts +68 -0
  417. package/src/spatial-scene/math.ts +280 -0
  418. package/src/spatial-scene/patch.ts +131 -0
  419. package/src/spatial-scene/time.ts +53 -0
  420. package/src/studio/contracts.ts +129 -0
  421. package/src/studio/index.ts +2 -0
  422. package/src/studio/plan.ts +156 -0
  423. package/src/studio/shared.ts +41 -0
  424. package/src/theme.ts +68 -0
  425. package/src/tldr.ts +474 -0
  426. package/src/types.ts +212 -0
  427. package/src/vectorize/archive.ts +101 -0
  428. package/src/vectorize/command.ts +797 -0
  429. package/src/vectorize/index.ts +27 -0
  430. package/src/vectorize/limits.ts +61 -0
  431. package/src/vectorize/metrics.ts +351 -0
  432. package/src/vectorize/pixels.ts +323 -0
  433. package/src/vectorize/supervisor.ts +339 -0
  434. package/src/vectorize/svg.ts +188 -0
  435. package/src/vectorize/tool.ts +542 -0
  436. package/src/vectorize/types.ts +113 -0
  437. package/src/vectorize/vectorize.ts +634 -0
  438. package/src/vectorize/worker-protocol.ts +40 -0
  439. package/src/vectorize/worker.ts +209 -0
  440. package/src/version.ts +1 -0
  441. package/src/workflow.ts +409 -0
@@ -0,0 +1,1526 @@
1
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference -- packed source types must include Bun asset modules
2
+ /// <reference path="../../../src/assets.d.ts" />
3
+
4
+ import { randomUUID } from "node:crypto";
5
+ import { constants } from "node:fs";
6
+ import {
7
+ chmod,
8
+ link,
9
+ lstat,
10
+ open,
11
+ readFile,
12
+ realpath,
13
+ rename,
14
+ rm,
15
+ writeFile,
16
+ } from "node:fs/promises";
17
+ import { isAbsolute, join, relative, resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ import { Resvg } from "@resvg/resvg-js";
21
+ import { z } from "zod";
22
+ import nebulaSansBoldAsset from "../../../src/assets/fonts/nebula-sans/NebulaSans-Bold.otf" with { type: "file" };
23
+
24
+ import {
25
+ ProjectRenderInvocationSchema,
26
+ resolveProjectRenderEncoderRecipe,
27
+ type OverlayOperation,
28
+ type ProjectRenderEncoderRecipe,
29
+ type ProjectRenderInvocation,
30
+ type ProjectRenderPlanV1,
31
+ type ProjectRenderTier,
32
+ type ResolvedProjectOverlay,
33
+ type ResolvedProjectVideoSlice,
34
+ } from "../contracts";
35
+ import {
36
+ assertProjectCameraSpatialLayerGeometry,
37
+ buildProjectCameraSpatialIndex,
38
+ canonicalJson,
39
+ canonicalJsonSha256,
40
+ evaluateProjectCameraSpatialViewport,
41
+ projectCameraSegmentsOverlapping,
42
+ projectCameraSpatialLayer,
43
+ type ProjectCameraSpatialAlgebra,
44
+ type ProjectCameraSpatialIndex,
45
+ } from "../core";
46
+ import { probeVisualMediaSummary } from "./analyzer";
47
+ import { inspectPngIntrinsicSize, inspectSvgIntrinsicSize } from "./asset-ingest";
48
+ import { CliError } from "./errors";
49
+ import { materializeFilterScript } from "./filter-script";
50
+ import type { ProcessRunner } from "./io";
51
+ import {
52
+ assertAudioLoopBufferWithinLimit,
53
+ assertVideoLoopBufferWithinLimit,
54
+ resolveAudibleSourceRange,
55
+ } from "./overlay-playback";
56
+ import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
57
+ import {
58
+ fingerprintPhysicalProjectMedia,
59
+ resolvePhysicalProjectMedia,
60
+ resolveVerifiedProjectMedia,
61
+ verifyPhysicalProjectMedia,
62
+ type ExpectedProjectMediaIntegrity,
63
+ } from "./project-media-integrity";
64
+ import { applyMetadataEffects } from "./renderer";
65
+ import type { SpatialCompositorCadenceBindingV1 } from "../contracts/spatial-compositor";
66
+ import { assertSpatialCompositorCadence } from "../core/spatial-compositor";
67
+ import { spatialFrameCount } from "../../../src/spatial-scene/time";
68
+
69
+ const MAXIMUM_SVG_CACHE_MANIFEST_BYTES = 64 * 1_024;
70
+ const MAXIMUM_SVG_DERIVATIVE_BYTES = 512 * 1_024 * 1_024;
71
+ const SVG_RASTER_RECIPE_VERSION = "slopcamera-rsvg-convert-v1";
72
+ const CAPTION_SVG_RASTER_RECIPE_VERSION = "slopcamera-caption-resvg-v1";
73
+ const CAPTION_RESVG_RENDERER_VERSION = "2.6.2";
74
+ const CAPTION_FONT = Object.freeze({
75
+ bytes: 145_348,
76
+ family: "Nebula Sans",
77
+ sha256: "91617d3e2281e8213f64f6bf359f387022d3149b35000b38365c32130a25bfa8",
78
+ weight: 700,
79
+ });
80
+
81
+ const SvgRasterSourceSchema = z.strictObject({
82
+ bytes: z.number().int().safe().positive(),
83
+ mediaType: z.literal("image/svg+xml"),
84
+ pixelHeight: z.number().int().safe().positive().max(16_384),
85
+ pixelWidth: z.number().int().safe().positive().max(16_384),
86
+ sha256: z.string().regex(/^[a-f0-9]{64}$/u),
87
+ });
88
+
89
+ const SvgRasterRecipeSchema = z.discriminatedUnion("renderer", [
90
+ z.strictObject({
91
+ arguments: z.tuple([
92
+ z.literal("--keep-aspect-ratio"),
93
+ z.literal("--width"),
94
+ z.string().regex(/^[1-9][0-9]{0,4}$/u),
95
+ z.literal("--height"),
96
+ z.string().regex(/^[1-9][0-9]{0,4}$/u),
97
+ ]),
98
+ renderer: z.literal("rsvg-convert"),
99
+ rendererVersion: z.string().trim().min(1).max(300),
100
+ source: SvgRasterSourceSchema,
101
+ version: z.literal(SVG_RASTER_RECIPE_VERSION),
102
+ }),
103
+ z.strictObject({
104
+ arguments: z.strictObject({
105
+ defaultFontFamily: z.literal(CAPTION_FONT.family),
106
+ fontBytes: z.literal(CAPTION_FONT.bytes),
107
+ fontSha256: z.literal(CAPTION_FONT.sha256),
108
+ fontWeight: z.literal(CAPTION_FONT.weight),
109
+ loadSystemFonts: z.literal(false),
110
+ }),
111
+ renderer: z.literal("@resvg/resvg-js"),
112
+ rendererVersion: z.literal(CAPTION_RESVG_RENDERER_VERSION),
113
+ source: SvgRasterSourceSchema,
114
+ version: z.literal(CAPTION_SVG_RASTER_RECIPE_VERSION),
115
+ }),
116
+ ]);
117
+
118
+ const SvgCacheManifestSchema = z.strictObject({
119
+ derivative: z.strictObject({
120
+ bytes: z.number().int().safe().positive().max(MAXIMUM_SVG_DERIVATIVE_BYTES),
121
+ path: z.string().regex(/^[a-f0-9]{64}\.png$/u),
122
+ sha256: z.string().regex(/^[a-f0-9]{64}$/u),
123
+ }),
124
+ kind: z.union([
125
+ z.literal("slopcamera.svg-raster-cache"),
126
+ z.literal("studio.svg-raster-cache"),
127
+ ]),
128
+ recipe: SvgRasterRecipeSchema,
129
+ recipeSha256: z.string().regex(/^[a-f0-9]{64}$/u),
130
+ schemaVersion: z.literal(2),
131
+ });
132
+
133
+ function seconds(microseconds: number): string {
134
+ return (microseconds / 1_000_000).toFixed(6).replace(/0+$/u, "").replace(/\.$/u, "");
135
+ }
136
+
137
+ function decimal(value: number): string {
138
+ return Number.isInteger(value) ? String(value) : value.toFixed(10).replace(/0+$/u, "").replace(/\.$/u, "");
139
+ }
140
+
141
+ function audioPlacementFilters(startUs: number): string[] {
142
+ // amix consumes each input from its first sample; shifting timestamps alone
143
+ // does not place that input in the program. Insert silence at the output rate.
144
+ const samples = (BigInt(startUs) * 48_000n + 500_000n) / 1_000_000n;
145
+ return ["asetpts=PTS-STARTPTS", ...(samples === 0n ? [] : [`adelay=${samples}S:all=1`])];
146
+ }
147
+
148
+ function atempo(rate: number): string {
149
+ if (!Number.isFinite(rate) || rate <= 0) throw new CliError("unsupported-plan", "Audio tempo rate must be positive.");
150
+ const filters: string[] = [];
151
+ let remaining = rate;
152
+ while (remaining > 100) {
153
+ filters.push("atempo=100");
154
+ remaining /= 100;
155
+ }
156
+ while (remaining < 0.5) {
157
+ filters.push("atempo=0.5");
158
+ remaining /= 0.5;
159
+ }
160
+ filters.push(`atempo=${decimal(remaining)}`);
161
+ return filters.join(",");
162
+ }
163
+
164
+ function isWithin(root: string, candidate: string): boolean {
165
+ const pathFromRoot = relative(root, candidate);
166
+ return pathFromRoot === "" || (!pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot));
167
+ }
168
+
169
+ async function resolveProjectOverlayMedia(
170
+ projectDirectory: string,
171
+ repositoryRoot: string,
172
+ path: string,
173
+ ): Promise<string> {
174
+ const projectRoot = await realpath(projectDirectory);
175
+ try {
176
+ return await resolvePhysicalProjectMedia(projectRoot, path);
177
+ } catch (error) {
178
+ if (error instanceof CliError) throw error;
179
+ if (!isFileSystemError(error, "ENOENT")) throw error;
180
+ }
181
+ // Backward compatibility for plans that intentionally stored a repository-relative overlay path.
182
+ return await resolvePhysicalProjectMedia(repositoryRoot, path);
183
+ }
184
+
185
+ function isFileSystemError(error: unknown, code: string): boolean {
186
+ return error instanceof Error && "code" in error && error.code === code;
187
+ }
188
+
189
+ async function readBoundedPhysicalText(path: string, maximumBytes: number): Promise<string> {
190
+ const lexical = await lstat(path);
191
+ if (lexical.isSymbolicLink() || !lexical.isFile() || lexical.size <= 0 || lexical.size > maximumBytes) {
192
+ throw new CliError("invalid-data", `Cache manifest must be a bounded physical file: ${path}`);
193
+ }
194
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
195
+ try {
196
+ const before = await handle.stat();
197
+ if (!before.isFile() || before.dev !== lexical.dev || before.ino !== lexical.ino || before.size !== lexical.size) {
198
+ throw new CliError("conflict", `Cache manifest changed before validation: ${path}`);
199
+ }
200
+ const bytes = await handle.readFile();
201
+ const after = await handle.stat();
202
+ if (
203
+ bytes.byteLength !== before.size
204
+ || after.dev !== before.dev
205
+ || after.ino !== before.ino
206
+ || after.size !== before.size
207
+ || after.mtimeMs !== before.mtimeMs
208
+ || after.ctimeMs !== before.ctimeMs
209
+ ) {
210
+ throw new CliError("conflict", `Cache manifest changed during validation: ${path}`);
211
+ }
212
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
213
+ } finally {
214
+ await handle.close();
215
+ }
216
+ }
217
+
218
+ async function physicalOverlayCacheDirectory(
219
+ projectDirectory: string,
220
+ workspaceDirectory: string | undefined,
221
+ ): Promise<string> {
222
+ return await ensurePhysicalPrivateDirectoryWithin(
223
+ workspaceDirectory ?? projectDirectory,
224
+ workspaceDirectory === undefined ? "renders/.overlay-cache" : "overlay-cache",
225
+ );
226
+ }
227
+
228
+ function svgRasterRecipe(overlay: OverlayOperation, rendererVersion: string) {
229
+ const asset = overlay.source.asset;
230
+ return SvgCacheManifestSchema.shape.recipe.parse({
231
+ arguments: [
232
+ "--keep-aspect-ratio",
233
+ "--width", String(overlay.intrinsicSize.width),
234
+ "--height", String(overlay.intrinsicSize.height),
235
+ ],
236
+ renderer: "rsvg-convert",
237
+ rendererVersion,
238
+ source: {
239
+ bytes: asset.bytes,
240
+ mediaType: "image/svg+xml",
241
+ pixelHeight: overlay.intrinsicSize.height,
242
+ pixelWidth: overlay.intrinsicSize.width,
243
+ sha256: asset.sha256,
244
+ },
245
+ version: SVG_RASTER_RECIPE_VERSION,
246
+ });
247
+ }
248
+
249
+ function isOwnedCaptionSprite(overlay: OverlayOperation): boolean {
250
+ const asset = overlay.source.asset;
251
+ const provenance = asset.provenance;
252
+ return overlay.source.kind === "svg"
253
+ && asset.path === `renders/caption-assets/${asset.sha256}.svg`
254
+ && provenance.kind === "generated"
255
+ && provenance.generator === "slopcamera-social-caption-sprite"
256
+ && provenance.generatorVersion === "1"
257
+ && provenance.command.length === 3
258
+ && provenance.command[0] === "slopcamera"
259
+ && provenance.command[1] === "caption"
260
+ && provenance.command[2] === "social-block-v1";
261
+ }
262
+
263
+ function captionSvgRasterRecipe(overlay: OverlayOperation) {
264
+ const asset = overlay.source.asset;
265
+ return SvgCacheManifestSchema.shape.recipe.parse({
266
+ arguments: {
267
+ defaultFontFamily: CAPTION_FONT.family,
268
+ fontBytes: CAPTION_FONT.bytes,
269
+ fontSha256: CAPTION_FONT.sha256,
270
+ fontWeight: CAPTION_FONT.weight,
271
+ loadSystemFonts: false,
272
+ },
273
+ renderer: "@resvg/resvg-js",
274
+ rendererVersion: CAPTION_RESVG_RENDERER_VERSION,
275
+ source: {
276
+ bytes: asset.bytes,
277
+ mediaType: "image/svg+xml",
278
+ pixelHeight: overlay.intrinsicSize.height,
279
+ pixelWidth: overlay.intrinsicSize.width,
280
+ sha256: asset.sha256,
281
+ },
282
+ version: CAPTION_SVG_RASTER_RECIPE_VERSION,
283
+ });
284
+ }
285
+
286
+ function bundledCaptionFontPath(): string {
287
+ return isAbsolute(nebulaSansBoldAsset)
288
+ ? nebulaSansBoldAsset
289
+ : fileURLToPath(new URL(nebulaSansBoldAsset, import.meta.url));
290
+ }
291
+
292
+ async function reusableSvgDerivative(
293
+ cacheDirectory: string,
294
+ recipeSha256: string,
295
+ expectedSize: OverlayOperation["intrinsicSize"],
296
+ ): Promise<string | null> {
297
+ const manifestPath = join(cacheDirectory, `${recipeSha256}.json`);
298
+ try {
299
+ const parsed = SvgCacheManifestSchema.safeParse(
300
+ JSON.parse(await readBoundedPhysicalText(manifestPath, MAXIMUM_SVG_CACHE_MANIFEST_BYTES)) as unknown,
301
+ );
302
+ if (
303
+ !parsed.success
304
+ || parsed.data.recipeSha256 !== recipeSha256
305
+ || canonicalJsonSha256(parsed.data.recipe) !== recipeSha256
306
+ ) return null;
307
+ const derivative = join(cacheDirectory, parsed.data.derivative.path);
308
+ try {
309
+ const verified = await verifyPhysicalProjectMedia(derivative, parsed.data.derivative, "SVG cache derivative");
310
+ const size = await inspectPngIntrinsicSize(verified);
311
+ return size.width === expectedSize.width && size.height === expectedSize.height ? verified : null;
312
+ } catch (error) {
313
+ if (error instanceof CliError && (error.code === "invalid-data" || error.code === "conflict")) return null;
314
+ throw error;
315
+ }
316
+ } catch (error) {
317
+ if (isFileSystemError(error, "ENOENT") || error instanceof SyntaxError) return null;
318
+ throw error;
319
+ }
320
+ }
321
+
322
+ async function createSvgDerivative(
323
+ input: string,
324
+ cacheDirectory: string,
325
+ recipe: z.infer<typeof SvgCacheManifestSchema>["recipe"],
326
+ recipeSha256: string,
327
+ options: ProjectRenderBuildOptions,
328
+ ): Promise<string> {
329
+ if (
330
+ recipe.renderer === "rsvg-convert"
331
+ && (options.rsvgConvert === undefined || options.runner === undefined)
332
+ ) {
333
+ throw new CliError("unavailable", "SVG overlays require rsvg-convert.");
334
+ }
335
+ const temporaryOutput = join(cacheDirectory, `.raster-${randomUUID()}.png`);
336
+ const temporaryManifest = join(cacheDirectory, `.manifest-${randomUUID()}.json`);
337
+ try {
338
+ if (recipe.renderer === "rsvg-convert") {
339
+ const result = await options.runner!.run([
340
+ options.rsvgConvert!,
341
+ ...recipe.arguments,
342
+ "-o", temporaryOutput,
343
+ input,
344
+ ], { maxOutputBytes: 1_000_000 });
345
+ if (result.exitCode !== 0) {
346
+ throw new CliError("subprocess", `rsvg-convert failed: ${result.stderr.trim().slice(-4_000) || `exit ${result.exitCode}`}`);
347
+ }
348
+ } else {
349
+ const fontPath = await verifyPhysicalProjectMedia(
350
+ bundledCaptionFontPath(),
351
+ CAPTION_FONT,
352
+ "Bundled Nebula Sans Bold caption font",
353
+ );
354
+ try {
355
+ const rendered = new Resvg(await readFile(input), {
356
+ font: {
357
+ defaultFontFamily: recipe.arguments.defaultFontFamily,
358
+ fontFiles: [fontPath],
359
+ loadSystemFonts: recipe.arguments.loadSystemFonts,
360
+ },
361
+ }).render().asPng();
362
+ if (rendered.byteLength <= 0 || rendered.byteLength > MAXIMUM_SVG_DERIVATIVE_BYTES) {
363
+ throw new CliError("invalid-data", "Caption SVG derivative exceeds its byte bound.");
364
+ }
365
+ await writeFile(temporaryOutput, rendered, { flag: "wx", mode: 0o600 });
366
+ } catch (error) {
367
+ if (error instanceof CliError) throw error;
368
+ throw new CliError("subprocess", "Resvg could not rasterize the generated caption sprite.", {
369
+ cause: error instanceof Error ? error.message : String(error),
370
+ });
371
+ }
372
+ }
373
+ const generated = await fingerprintPhysicalProjectMedia(temporaryOutput, MAXIMUM_SVG_DERIVATIVE_BYTES);
374
+ const generatedSize = await inspectPngIntrinsicSize(temporaryOutput);
375
+ if (
376
+ generatedSize.width !== recipe.source.pixelWidth
377
+ || generatedSize.height !== recipe.source.pixelHeight
378
+ ) {
379
+ throw new CliError("invalid-data", "SVG derivative dimensions do not match its bounded raster recipe.");
380
+ }
381
+ const derivativeName = `${generated.sha256}.png`;
382
+ const derivative = join(cacheDirectory, derivativeName);
383
+ await chmod(temporaryOutput, 0o600);
384
+ try {
385
+ await link(temporaryOutput, derivative);
386
+ await rm(temporaryOutput);
387
+ } catch (error) {
388
+ if (!isFileSystemError(error, "EEXIST")) throw error;
389
+ try {
390
+ await verifyPhysicalProjectMedia(derivative, generated, "SVG cache derivative");
391
+ await rm(temporaryOutput);
392
+ } catch (verificationError) {
393
+ const existing = await lstat(derivative);
394
+ if (existing.isSymbolicLink() || !existing.isFile() || existing.nlink !== 1) {
395
+ throw new CliError("unsafe-path", `SVG cache derivative is unsafe: ${derivative}`);
396
+ }
397
+ if (!(verificationError instanceof CliError)) throw verificationError;
398
+ await rename(temporaryOutput, derivative);
399
+ }
400
+ }
401
+ const manifest = SvgCacheManifestSchema.parse({
402
+ derivative: { bytes: generated.bytes, path: derivativeName, sha256: generated.sha256 },
403
+ kind: "slopcamera.svg-raster-cache",
404
+ recipe,
405
+ recipeSha256,
406
+ schemaVersion: 2,
407
+ });
408
+ await writeFile(temporaryManifest, `${canonicalJson(manifest)}\n`, { flag: "wx", mode: 0o600 });
409
+ await rename(temporaryManifest, join(cacheDirectory, `${recipeSha256}.json`));
410
+ return await verifyPhysicalProjectMedia(derivative, manifest.derivative, "SVG cache derivative");
411
+ } finally {
412
+ await Promise.all([
413
+ rm(temporaryOutput, { force: true }),
414
+ rm(temporaryManifest, { force: true }),
415
+ ]);
416
+ }
417
+ }
418
+
419
+ interface PreparedProjectOverlayMedia {
420
+ readonly audioStreamIndex: number | null;
421
+ readonly path: string;
422
+ readonly sourcePath: string;
423
+ readonly videoStreamIndex: number | null;
424
+ }
425
+
426
+ async function prepareProjectOverlayMedia(
427
+ overlay: OverlayOperation,
428
+ options: ProjectRenderBuildOptions,
429
+ ): Promise<PreparedProjectOverlayMedia> {
430
+ const input = await resolveProjectOverlayMedia(
431
+ options.projectDirectory,
432
+ options.repositoryRoot,
433
+ overlay.source.asset.path,
434
+ );
435
+ await verifyPhysicalProjectMedia(input, overlay.source.asset, `Overlay ${overlay.overlayId}`);
436
+ if (overlay.source.asset.mediaType !== "image/svg+xml") {
437
+ if (options.ffprobe === undefined || options.runner === undefined) {
438
+ throw new CliError("unavailable", `Overlay ${overlay.overlayId} requires FFprobe dimension verification.`);
439
+ }
440
+ const probed = await probeVisualMediaSummary(options.ffprobe, options.runner, input);
441
+ if (
442
+ probed.pixelWidth !== overlay.intrinsicSize.width
443
+ || probed.pixelHeight !== overlay.intrinsicSize.height
444
+ ) {
445
+ throw new CliError("invalid-data", `Overlay ${overlay.overlayId} intrinsic dimensions do not match its media.`);
446
+ }
447
+ if (overlay.source.kind === "gif" || overlay.source.kind === "video") {
448
+ const playback = overlay.source.playback;
449
+ if (
450
+ playback.videoStreamIndex !== null
451
+ && playback.videoStreamIndex !== undefined
452
+ && playback.videoStreamIndex !== probed.videoStreamIndex
453
+ ) {
454
+ throw new CliError("invalid-data", `Overlay ${overlay.overlayId} selected video stream does not match its media.`);
455
+ }
456
+ if (
457
+ playback.audioStreamIndex !== null
458
+ && playback.audioStreamIndex !== undefined
459
+ && playback.audioStreamIndex !== probed.audioStreamIndex
460
+ ) {
461
+ throw new CliError("invalid-data", `Overlay ${overlay.overlayId} selected audio stream does not match its media.`);
462
+ }
463
+ if (
464
+ overlay.source.kind === "video"
465
+ && overlay.source.audioPolicy.kind !== "mute"
466
+ && probed.audioStreamIndex === null
467
+ ) {
468
+ throw new CliError("invalid-data", `Overlay ${overlay.overlayId} requires an audio stream that is absent from its media.`);
469
+ }
470
+ return {
471
+ audioStreamIndex: probed.audioStreamIndex,
472
+ path: input,
473
+ sourcePath: input,
474
+ videoStreamIndex: probed.videoStreamIndex,
475
+ };
476
+ }
477
+ return {
478
+ audioStreamIndex: null,
479
+ path: input,
480
+ sourcePath: input,
481
+ videoStreamIndex: null,
482
+ };
483
+ }
484
+ const svgSize = await inspectSvgIntrinsicSize(input);
485
+ if (svgSize.width !== overlay.intrinsicSize.width || svgSize.height !== overlay.intrinsicSize.height) {
486
+ throw new CliError("invalid-data", `Overlay ${overlay.overlayId} intrinsic dimensions do not match its SVG.`);
487
+ }
488
+ const captionSprite = isOwnedCaptionSprite(overlay);
489
+ if (!captionSprite && (
490
+ options.rsvgConvert === undefined
491
+ || options.rsvgConvertVersion === undefined
492
+ || options.runner === undefined
493
+ )) {
494
+ throw new CliError("unavailable", `SVG overlay ${overlay.overlayId} requires rsvg-convert with a probed version.`);
495
+ }
496
+ const cacheDirectory = await physicalOverlayCacheDirectory(
497
+ options.projectDirectory,
498
+ options.workspaceDirectory,
499
+ );
500
+ const recipe = captionSprite
501
+ ? captionSvgRasterRecipe(overlay)
502
+ : svgRasterRecipe(overlay, options.rsvgConvertVersion!);
503
+ const recipeSha256 = canonicalJsonSha256(recipe);
504
+ const cached = await reusableSvgDerivative(cacheDirectory, recipeSha256, overlay.intrinsicSize);
505
+ if (cached !== null) {
506
+ return {
507
+ audioStreamIndex: null,
508
+ path: cached,
509
+ sourcePath: input,
510
+ videoStreamIndex: null,
511
+ };
512
+ }
513
+ if (options.dryRun) {
514
+ return {
515
+ audioStreamIndex: null,
516
+ path: join(cacheDirectory, `pending-${recipeSha256}.png`),
517
+ sourcePath: input,
518
+ videoStreamIndex: null,
519
+ };
520
+ }
521
+ return {
522
+ audioStreamIndex: null,
523
+ path: await createSvgDerivative(input, cacheDirectory, recipe, recipeSha256, options),
524
+ sourcePath: input,
525
+ videoStreamIndex: null,
526
+ };
527
+ }
528
+
529
+ function inputSpecifier(index: number, streamIndex: number): string {
530
+ return `${index}:${streamIndex}`;
531
+ }
532
+
533
+ function animatedStreamSpecifier(
534
+ inputIndex: number,
535
+ streamType: "a" | "v",
536
+ absoluteStreamIndex: number | null | undefined,
537
+ ): string {
538
+ return absoluteStreamIndex === null || absoluteStreamIndex === undefined
539
+ ? `${inputIndex}:${streamType}:0`
540
+ : inputSpecifier(inputIndex, absoluteStreamIndex);
541
+ }
542
+
543
+ function layoutPixels(
544
+ presentation: Extract<ResolvedProjectVideoSlice["presentation"], { readonly enabled: true }>,
545
+ outputWidth: number,
546
+ outputHeight: number,
547
+ ): { readonly height: number; readonly width: number; readonly x: number; readonly y: number } {
548
+ if (presentation.layout.kind === "output-pixels") {
549
+ return {
550
+ height: Math.max(1, Math.round(presentation.layout.height)),
551
+ width: Math.max(1, Math.round(presentation.layout.width)),
552
+ x: Math.round(presentation.layout.x),
553
+ y: Math.round(presentation.layout.y),
554
+ };
555
+ }
556
+ return {
557
+ height: Math.max(1, Math.round(presentation.layout.height * outputHeight)),
558
+ width: Math.max(1, Math.round(presentation.layout.width * outputWidth)),
559
+ x: Math.round(presentation.layout.x * outputWidth),
560
+ y: Math.round(presentation.layout.y * outputHeight),
561
+ };
562
+ }
563
+
564
+ function videoTransform(
565
+ slice: ResolvedProjectVideoSlice,
566
+ outputWidth: number,
567
+ outputHeight: number,
568
+ ): { readonly filters: readonly string[]; readonly x: number; readonly y: number } {
569
+ if (!slice.presentation.enabled) throw new CliError("invalid-data", "Disabled video slice reached the renderer.");
570
+ const presentation = slice.presentation;
571
+ const filters: string[] = [];
572
+ if (presentation.crop.kind === "normalized-insets") {
573
+ const crop = presentation.crop;
574
+ filters.push(
575
+ `crop=w='iw*${decimal(1 - crop.left - crop.right)}':h='ih*${decimal(1 - crop.top - crop.bottom)}':x='iw*${decimal(crop.left)}':y='ih*${decimal(crop.top)}'`,
576
+ );
577
+ }
578
+ const layout = layoutPixels(presentation, outputWidth, outputHeight);
579
+ if (presentation.fit === "fill") {
580
+ filters.push(`scale=${layout.width}:${layout.height}`);
581
+ } else if (presentation.fit === "contain") {
582
+ filters.push(
583
+ `scale=${layout.width}:${layout.height}:force_original_aspect_ratio=decrease`,
584
+ `pad=${layout.width}:${layout.height}:(ow-iw)/2:(oh-ih)/2:color=0x00000000`,
585
+ );
586
+ } else {
587
+ filters.push(
588
+ `scale=${layout.width}:${layout.height}:force_original_aspect_ratio=increase`,
589
+ `crop=${layout.width}:${layout.height}`,
590
+ );
591
+ }
592
+ filters.push("format=rgba");
593
+ if (presentation.opacity !== 1) filters.push(`colorchannelmixer=aa=${decimal(presentation.opacity)}`);
594
+ return { filters, x: layout.x, y: layout.y };
595
+ }
596
+
597
+ function overlayAnimationDuration(animation: OverlayOperation["entrance"]): number {
598
+ return animation.kind === "none" ? 0 : animation.durationUs;
599
+ }
600
+
601
+ function effectiveAnimationBounds(
602
+ overlay: OverlayOperation,
603
+ visibleDurationUs: number,
604
+ ): { readonly entranceUs: number; readonly exitUs: number } {
605
+ const entranceUs = overlayAnimationDuration(overlay.entrance);
606
+ const exitUs = overlayAnimationDuration(overlay.exit);
607
+ if (entranceUs + exitUs <= visibleDurationUs || entranceUs + exitUs === 0) return { entranceUs, exitUs };
608
+ const first = Math.floor(visibleDurationUs * entranceUs / (entranceUs + exitUs));
609
+ return { entranceUs: first, exitUs: visibleDurationUs - first };
610
+ }
611
+
612
+ type OverlayEasing = Extract<OverlayOperation["entrance"], { readonly easing: unknown }>["easing"];
613
+
614
+ function eased(easing: OverlayEasing, progress: string): string {
615
+ if (easing.kind === "linear") return progress;
616
+ if (easing.kind === "ease-in") return `pow(${progress},2)`;
617
+ if (easing.kind === "ease-out") return `1-pow(1-(${progress}),2)`;
618
+ if (easing.kind === "ease-in-out") return `if(lt(${progress},0.5),2*pow(${progress},2),1-pow(-2*(${progress})+2,2)/2)`;
619
+ if (easing.kind === "spring") return `min(1,max(0,1-exp(-6*(${progress}))*cos(8*(${progress}))))`;
620
+ if (easing.kind === "cubic-bezier") {
621
+ const bounded = `min(1,max(0,${progress}))`;
622
+ const parameter = `root(${cubicBezierCoordinateExpression("ld(0)", easing.x1, easing.x2)}-(${bounded}),1)`;
623
+ return cubicBezierCoordinateExpression(parameter, easing.y1, easing.y2);
624
+ }
625
+ return progress;
626
+ }
627
+
628
+ function cubicBezierCoordinateExpression(parameter: string, first: number, second: number): string {
629
+ const inverse = `(1-(${parameter}))`;
630
+ return `3*pow(${inverse},2)*(${parameter})*${decimal(first)}+3*(${inverse})*pow(${parameter},2)*${decimal(second)}+pow(${parameter},3)`;
631
+ }
632
+
633
+ const CAMERA_EXPRESSION_ALGEBRA = {
634
+ add: (left: string, right: string) => `((${left})+(${right}))`,
635
+ constant: decimal,
636
+ divide: (left: string, right: string) => `((${left})/(${right}))`,
637
+ easing: (easing: OverlayEasing, progress: string) => (
638
+ eased(easing, progress)
639
+ ),
640
+ exponential: (value: string) => `exp(${value})`,
641
+ maximum: (left: string, right: string) => `max(${left},${right})`,
642
+ minimum: (left: string, right: string) => `min(${left},${right})`,
643
+ multiply: (left: string, right: string) => `((${left})*(${right}))`,
644
+ selectRange: (
645
+ clock: string,
646
+ range: { readonly endUs: number; readonly startUs: number },
647
+ end: "inclusive" | "exclusive",
648
+ active: string,
649
+ fallback: string,
650
+ ) => `if(gte(${clock},${decimal(range.startUs)})*${
651
+ end === "inclusive" ? "lte" : "lt"
652
+ }(${clock},${decimal(range.endUs)}),${active},${fallback})`,
653
+ subtract: (left: string, right: string) => `((${left})-(${right}))`,
654
+ } satisfies ProjectCameraSpatialAlgebra<string>;
655
+
656
+ function projectCameraFilters(
657
+ plan: ProjectRenderPlanV1,
658
+ slice: ResolvedProjectVideoSlice,
659
+ cameraIndex: ProjectCameraSpatialIndex,
660
+ rationalRate?: string,
661
+ ): readonly string[] {
662
+ const layerCamera = projectCameraSpatialLayer(
663
+ cameraIndex,
664
+ slice.placementId,
665
+ slice.streamId,
666
+ );
667
+ if (layerCamera === null) return [];
668
+ const segments = projectCameraSegmentsOverlapping(
669
+ layerCamera.segments,
670
+ slice.outputRange,
671
+ );
672
+ if (layerCamera.keyframeGroups.length === 0 && segments.length === 0) {
673
+ return [];
674
+ }
675
+ const layout = layoutPixels(slice.presentation as Extract<ResolvedProjectVideoSlice["presentation"], { readonly enabled: true }>, plan.output.pixelWidth, plan.output.pixelHeight);
676
+ const activeCamera = {
677
+ keyframeGroups: layerCamera.keyframeGroups,
678
+ segments,
679
+ };
680
+ try {
681
+ assertProjectCameraSpatialLayerGeometry(
682
+ activeCamera,
683
+ layout.width,
684
+ layout.height,
685
+ `${slice.placementId}:${slice.streamId}`,
686
+ );
687
+ } catch {
688
+ throw new CliError("invalid-data", `Camera geometry no longer matches layer ${slice.placementId}:${slice.streamId}.`);
689
+ }
690
+ const globalTimeUs = `(${decimal(slice.outputRange.startUs)}+on*1000000/${
691
+ rationalRate === undefined ? decimal(plan.output.frameRate) : `(${rationalRate})`
692
+ })`;
693
+ const viewport = evaluateProjectCameraSpatialViewport(
694
+ activeCamera,
695
+ {
696
+ outputTimeUs: globalTimeUs,
697
+ pixelHeight: layout.height,
698
+ pixelWidth: layout.width,
699
+ },
700
+ CAMERA_EXPRESSION_ALGEBRA,
701
+ );
702
+ return [
703
+ // zoompan's d=1 clock is frame-count based. Normalize the already
704
+ // speed-adjusted slice to the output cadence first so it cannot restore
705
+ // source cadence or discard the project's speed map.
706
+ `fps=${rationalRate ?? decimal(plan.output.frameRate)}`,
707
+ `zoompan=z='${layout.width}/(${viewport.width})':x='${viewport.x}':y='${viewport.y}':d=1:s=${layout.width}x${layout.height}:fps=${rationalRate ?? decimal(plan.output.frameRate)}`,
708
+ "settb=AVTB",
709
+ `setpts=PTS-STARTPTS+${seconds(slice.outputRange.startUs)}/TB`,
710
+ "format=rgba",
711
+ ];
712
+ }
713
+
714
+ function scaleExpression(
715
+ overlay: OverlayOperation,
716
+ fullStartUs: number,
717
+ fullEndUs: number,
718
+ entranceUs: number,
719
+ exitUs: number,
720
+ ): string {
721
+ let expression = decimal(overlay.scale);
722
+ if (overlay.entrance.kind === "scale" && entranceUs > 0) {
723
+ const start = seconds(fullStartUs);
724
+ const end = seconds(fullStartUs + entranceUs);
725
+ const from = overlay.entrance.fromScale * overlay.scale;
726
+ const progress = `(t-${start})/(${end}-${start})`;
727
+ expression = `if(between(t,${start},${end}),${decimal(from)}+${decimal(overlay.scale - from)}*(${eased(overlay.entrance.easing, progress)}),${expression})`;
728
+ }
729
+ if (overlay.exit.kind === "scale" && exitUs > 0) {
730
+ const start = seconds(fullEndUs - exitUs);
731
+ const end = seconds(fullEndUs);
732
+ const to = overlay.exit.fromScale * overlay.scale;
733
+ const progress = `(t-${start})/(${end}-${start})`;
734
+ expression = `if(between(t,${start},${end}),${decimal(overlay.scale)}+${decimal(to - overlay.scale)}*(${eased(overlay.exit.easing, progress)}),${expression})`;
735
+ }
736
+ const motionScale = overlayMotionExpression(overlay, "scaleMultiplier", fullStartUs, fullEndUs, 1);
737
+ return motionScale === "1" ? expression : `(${expression})*(${motionScale})`;
738
+ }
739
+
740
+ type OverlayMotionField = "opacityMultiplier" | "positionX" | "positionY" | "rotationOffsetDegrees" | "scaleMultiplier";
741
+
742
+ function overlayMotionValue(
743
+ keyframe: Extract<NonNullable<OverlayOperation["motion"]>, { readonly kind: "keyframes" }>["keyframes"][number],
744
+ field: OverlayMotionField,
745
+ ): number {
746
+ if (field === "positionX") return keyframe.positionOffset.x;
747
+ if (field === "positionY") return keyframe.positionOffset.y;
748
+ return keyframe[field];
749
+ }
750
+
751
+ function overlayMotionExpression(
752
+ overlay: OverlayOperation,
753
+ field: OverlayMotionField,
754
+ startUs: number,
755
+ endUs: number,
756
+ fallback: number,
757
+ timeExpression = "t",
758
+ ): string {
759
+ const motion = overlay.motion ?? { kind: "none" as const };
760
+ if (motion.kind === "none" || endUs <= startUs) return decimal(fallback);
761
+ const keyframes = motion.keyframes;
762
+ let expression = decimal(overlayMotionValue(keyframes.at(-1)!, field));
763
+ for (let index = keyframes.length - 1; index > 0; index -= 1) {
764
+ const left = keyframes[index - 1]!;
765
+ const right = keyframes[index]!;
766
+ const leftTimeUs = startUs + Math.round((endUs - startUs) * left.offset);
767
+ const rightTimeUs = startUs + Math.round((endUs - startUs) * right.offset);
768
+ if (rightTimeUs <= leftTimeUs) continue;
769
+ const leftValue = overlayMotionValue(left, field);
770
+ const rightValue = overlayMotionValue(right, field);
771
+ const progress = `(${timeExpression}-${seconds(leftTimeUs)})/(${seconds(rightTimeUs)}-${seconds(leftTimeUs)})`;
772
+ const value = leftValue === rightValue
773
+ ? decimal(leftValue)
774
+ : `${decimal(leftValue)}+${decimal(rightValue - leftValue)}*(${eased(left.easing, progress)})`;
775
+ expression = `if(between(${timeExpression},${seconds(leftTimeUs)},${seconds(rightTimeUs)}),${value},${expression})`;
776
+ }
777
+ return expression;
778
+ }
779
+
780
+ function overlayGeometryFilters(
781
+ overlay: OverlayOperation,
782
+ fullStartUs: number,
783
+ fullEndUs: number,
784
+ entranceUs: number,
785
+ exitUs: number,
786
+ ): readonly string[] {
787
+ const filters: string[] = [];
788
+ const crop = overlay.crop ?? { kind: "none" as const };
789
+ if (crop.kind === "normalized-insets") {
790
+ filters.push(
791
+ `crop=w='iw*${decimal(1 - crop.left - crop.right)}':h='ih*${decimal(1 - crop.top - crop.bottom)}':x='iw*${decimal(crop.left)}':y='ih*${decimal(crop.top)}'`,
792
+ );
793
+ }
794
+ filters.push("format=rgba");
795
+ if (overlay.size.kind === "pixels") {
796
+ const width = decimal(overlay.size.width);
797
+ const height = decimal(overlay.size.height);
798
+ const fit = overlay.fit ?? "fill";
799
+ if (fit === "fill") {
800
+ filters.push(`scale=${width}:${height}`);
801
+ } else if (fit === "contain") {
802
+ filters.push(
803
+ `scale=${width}:${height}:force_original_aspect_ratio=decrease`,
804
+ `pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:color=0x00000000`,
805
+ );
806
+ } else {
807
+ filters.push(
808
+ `scale=${width}:${height}:force_original_aspect_ratio=increase`,
809
+ `crop=${width}:${height}`,
810
+ );
811
+ }
812
+ }
813
+ const scale = scaleExpression(overlay, fullStartUs, fullEndUs, entranceUs, exitUs);
814
+ filters.push(`scale=w='iw*(${scale})':h='ih*(${scale})':eval=frame`);
815
+ const opacity = overlayMotionExpression(overlay, "opacityMultiplier", fullStartUs, fullEndUs, 1, "T");
816
+ const mask = overlay.mask ?? { kind: "none" as const };
817
+ if (mask.kind === "none" && opacity === "1") {
818
+ filters.push(`colorchannelmixer=aa=${decimal(overlay.opacity)}`);
819
+ } else {
820
+ const radius = mask.kind === "rounded-rectangle"
821
+ ? `min(${decimal(mask.radiusPx)},min(W/2,H/2))`
822
+ : "0";
823
+ const maskExpression = mask.kind === "rounded-rectangle"
824
+ ? `gt(between(X,${radius},W-${radius})+between(Y,${radius},H-${radius})+lte(pow(X-min(max(X,${radius}),W-${radius}),2)+pow(Y-min(max(Y,${radius}),H-${radius}),2),pow(${radius},2)),0)`
825
+ : "1";
826
+ filters.push(
827
+ `geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='alpha(X,Y)*${decimal(overlay.opacity)}*(${opacity})*(${maskExpression})'`,
828
+ );
829
+ }
830
+ if (overlay.entrance.kind === "fade" && entranceUs > 0) {
831
+ filters.push(`fade=t=in:st=${seconds(fullStartUs)}:d=${seconds(entranceUs)}:alpha=1`);
832
+ }
833
+ if (overlay.exit.kind === "fade" && exitUs > 0) {
834
+ filters.push(`fade=t=out:st=${seconds(fullEndUs - exitUs)}:d=${seconds(exitUs)}:alpha=1`);
835
+ }
836
+ const rotationOffset = overlayMotionExpression(overlay, "rotationOffsetDegrees", fullStartUs, fullEndUs, 0);
837
+ const rotation = rotationOffset === "0"
838
+ ? decimal(overlay.rotationDegrees)
839
+ : `${decimal(overlay.rotationDegrees)}+(${rotationOffset})`;
840
+ if (rotation !== "0") {
841
+ filters.push(`rotate='(${rotation})*PI/180':c=none:ow=rotw(iw):oh=roth(ih)`);
842
+ }
843
+ return filters;
844
+ }
845
+
846
+ function anchorPosition(anchor: OverlayOperation["anchor"], axis: "x" | "y"): string {
847
+ if (axis === "x") {
848
+ if (anchor.endsWith("left") || anchor === "left") return "0";
849
+ if (anchor.endsWith("right") || anchor === "right") return "W-w";
850
+ return "(W-w)/2";
851
+ }
852
+ if (anchor.startsWith("top") || anchor === "top") return "0";
853
+ if (anchor.startsWith("bottom") || anchor === "bottom") return "H-h";
854
+ return "(H-h)/2";
855
+ }
856
+
857
+ function slideExpression(
858
+ overlay: OverlayOperation,
859
+ axis: "x" | "y",
860
+ fullStartUs: number,
861
+ fullEndUs: number,
862
+ entranceUs: number,
863
+ exitUs: number,
864
+ ): string {
865
+ const expressions: string[] = [];
866
+ for (const [animation, startUs, durationUs, entering] of [
867
+ [overlay.entrance, fullStartUs, entranceUs, true],
868
+ [overlay.exit, fullEndUs - exitUs, exitUs, false],
869
+ ] as const) {
870
+ if (animation.kind !== "slide" || durationUs === 0) continue;
871
+ const onAxis = axis === "x"
872
+ ? animation.direction === "left" || animation.direction === "right"
873
+ : animation.direction === "up" || animation.direction === "down";
874
+ if (!onAxis) continue;
875
+ const sign = animation.direction === "left" || animation.direction === "up" ? -1 : 1;
876
+ const start = seconds(startUs);
877
+ const end = seconds(startUs + durationUs);
878
+ const from = entering ? sign * animation.distancePx : 0;
879
+ const delta = entering ? -sign * animation.distancePx : sign * animation.distancePx;
880
+ const progress = `(t-${start})/(${end}-${start})`;
881
+ expressions.push(`if(between(t,${start},${end}),${decimal(from)}+${decimal(delta)}*(${eased(animation.easing, progress)}),0)`);
882
+ }
883
+ return expressions.length === 0 ? "0" : expressions.join("+");
884
+ }
885
+
886
+ function overlayPositionExpression(
887
+ overlay: OverlayOperation,
888
+ axis: "x" | "y",
889
+ fullStartUs: number,
890
+ fullEndUs: number,
891
+ entranceUs: number,
892
+ exitUs: number,
893
+ ): string {
894
+ const field = axis === "x" ? "positionX" : "positionY";
895
+ return `${anchorPosition(overlay.anchor, axis)}+${decimal(overlay.position[axis])}+${overlayMotionExpression(overlay, field, fullStartUs, fullEndUs, 0)}+${slideExpression(overlay, axis, fullStartUs, fullEndUs, entranceUs, exitUs)}`;
896
+ }
897
+
898
+ function overlayVideoChain(
899
+ resolved: ResolvedProjectOverlay,
900
+ fullStartUs: number,
901
+ fullEndUs: number,
902
+ frameRate: number,
903
+ spatialCadence?: SpatialCompositorCadenceBindingV1,
904
+ ): { readonly filters: readonly string[]; readonly visibleEndUs: number } | null {
905
+ const overlay = resolved.operation;
906
+ const sliceDurationUs = resolved.outputRange.endUs - resolved.outputRange.startUs;
907
+ const filters: string[] = [];
908
+ let visibleDurationUs = sliceDurationUs;
909
+ if (overlay.source.kind === "gif" || overlay.source.kind === "video") {
910
+ const playback = overlay.source.playback;
911
+ const streamStartUs = playback.streamStartUs ?? 0;
912
+ const sourceWindowUs = playback.sourceOutUs - playback.sourceInUs;
913
+ const offsetSourceUs = Math.round(resolved.playbackOffsetUs * playback.playbackRate);
914
+ const requiredSourceUs = Math.ceil(sliceDurationUs * playback.playbackRate);
915
+ const requestedEndSourceUs = offsetSourceUs + requiredSourceUs;
916
+ if (
917
+ !Number.isSafeInteger(offsetSourceUs)
918
+ || !Number.isSafeInteger(requiredSourceUs)
919
+ || !Number.isSafeInteger(requestedEndSourceUs)
920
+ || offsetSourceUs < 0
921
+ || requiredSourceUs <= 0
922
+ ) {
923
+ throw new CliError("unsupported-plan", `Overlay ${overlay.overlayId} has an unsupported playback duration.`);
924
+ }
925
+ const needsLoop = playback.endBehavior === "loop" && requestedEndSourceUs > sourceWindowUs;
926
+ const decodedSourceUs = Math.min(sourceWindowUs, requestedEndSourceUs);
927
+ const effectiveSourceOutUs = streamStartUs + playback.sourceInUs + decodedSourceUs;
928
+ if (!Number.isSafeInteger(effectiveSourceOutUs)) {
929
+ throw new CliError("unsupported-plan", `Overlay ${overlay.overlayId} has an unsupported source window.`);
930
+ }
931
+ filters.push(
932
+ `trim=start=${seconds(streamStartUs + playback.sourceInUs)}:end=${seconds(effectiveSourceOutUs)}`,
933
+ "setpts=PTS-STARTPTS",
934
+ `fps=${spatialCadence === undefined ? decimal(frameRate) : `${spatialCadence.cadence.frameRate.numerator}/${spatialCadence.cadence.frameRate.denominator}`}`,
935
+ );
936
+ if (needsLoop) {
937
+ const frameCount = spatialCadence === undefined
938
+ ? Math.max(1, Math.ceil(sourceWindowUs * frameRate / 1_000_000))
939
+ : spatialFrameCount(sourceWindowUs, spatialCadence.cadence.frameRate);
940
+ if (frameCount > 1_000_000) throw new CliError("unsupported-plan", "Animated overlay loop exceeds the frame buffer bound.");
941
+ assertVideoLoopBufferWithinLimit({
942
+ frameCount,
943
+ label: `Overlay ${overlay.overlayId}`,
944
+ pixelHeight: overlay.intrinsicSize.height,
945
+ pixelWidth: overlay.intrinsicSize.width,
946
+ });
947
+ filters.push("format=rgba");
948
+ filters.push(`loop=loop=-1:size=${frameCount}:start=0`);
949
+ } else if (playback.endBehavior === "freeze-end") {
950
+ filters.push(`tpad=stop_mode=clone:stop_duration=${seconds(offsetSourceUs + requiredSourceUs)}`);
951
+ } else {
952
+ if (offsetSourceUs >= sourceWindowUs) return null;
953
+ visibleDurationUs = Math.min(sliceDurationUs, (sourceWindowUs - offsetSourceUs) / playback.playbackRate);
954
+ }
955
+ filters.push(
956
+ `trim=start=${seconds(offsetSourceUs)}:duration=${seconds(Math.min(requiredSourceUs, playback.endBehavior === "hide" ? sourceWindowUs - offsetSourceUs : requiredSourceUs))}`,
957
+ `setpts=(PTS-STARTPTS)/${decimal(playback.playbackRate)}+${seconds(resolved.outputRange.startUs)}/TB`,
958
+ );
959
+ } else {
960
+ filters.push(
961
+ `trim=duration=${seconds(sliceDurationUs)}`,
962
+ `setpts=PTS-STARTPTS+${seconds(resolved.outputRange.startUs)}/TB`,
963
+ );
964
+ }
965
+ const { entranceUs, exitUs } = effectiveAnimationBounds(overlay, fullEndUs - fullStartUs);
966
+ filters.push(...overlayGeometryFilters(overlay, fullStartUs, fullEndUs, entranceUs, exitUs));
967
+ return { filters, visibleEndUs: resolved.outputRange.startUs + Math.round(visibleDurationUs) };
968
+ }
969
+
970
+ function overlayAudioChain(resolved: ResolvedProjectOverlay): {
971
+ readonly audibleEndUs: number;
972
+ readonly audibleStartUs: number;
973
+ readonly filters: readonly string[];
974
+ readonly loopDuck: {
975
+ readonly audioDelayUs: number;
976
+ readonly audioEndUs: number;
977
+ readonly sourceOffsetUs: number;
978
+ readonly sourceWindowUs: number;
979
+ } | null;
980
+ } | null {
981
+ const source = resolved.operation.source;
982
+ if (source.kind !== "video" || source.audioPolicy.kind === "mute") return null;
983
+ const playback = source.playback;
984
+ const streamStartUs = playback.streamStartUs ?? 0;
985
+ const audioDelayUs = Math.max(0, (playback.audioStartUs ?? streamStartUs) - (streamStartUs + playback.sourceInUs));
986
+ const sliceDurationUs = resolved.outputRange.endUs - resolved.outputRange.startUs;
987
+ const sourceWindowUs = playback.sourceOutUs - playback.sourceInUs;
988
+ const selectedSourceStartUs = streamStartUs + playback.sourceInUs;
989
+ const audioEndUs = Math.min(
990
+ sourceWindowUs,
991
+ Math.max(
992
+ 0,
993
+ (playback.audioEndUs ?? selectedSourceStartUs + sourceWindowUs) - selectedSourceStartUs,
994
+ ),
995
+ );
996
+ const offsetSourceUs = Math.round(resolved.playbackOffsetUs * playback.playbackRate);
997
+ if (playback.endBehavior === "hide" && offsetSourceUs >= sourceWindowUs) return null;
998
+ const requiredSourceUs = Math.ceil(sliceDurationUs * playback.playbackRate);
999
+ const requestedEndSourceUs = offsetSourceUs + requiredSourceUs;
1000
+ if (
1001
+ !Number.isSafeInteger(offsetSourceUs)
1002
+ || !Number.isSafeInteger(requiredSourceUs)
1003
+ || !Number.isSafeInteger(requestedEndSourceUs)
1004
+ || offsetSourceUs < 0
1005
+ || requiredSourceUs <= 0
1006
+ ) {
1007
+ throw new CliError("unsupported-plan", `Overlay ${resolved.operation.overlayId} has an unsupported audio duration.`);
1008
+ }
1009
+ const needsLoop = playback.endBehavior === "loop" && requestedEndSourceUs > sourceWindowUs;
1010
+ const audibleSourceRange = resolveAudibleSourceRange({
1011
+ audioDelayUs,
1012
+ audioEndUs,
1013
+ endBehavior: playback.endBehavior,
1014
+ requestedDurationUs: requiredSourceUs,
1015
+ requestedStartUs: offsetSourceUs,
1016
+ sourceWindowUs,
1017
+ });
1018
+ if (audibleSourceRange === null) return null;
1019
+ const audibleStartUs = resolved.outputRange.startUs + Math.ceil(
1020
+ (audibleSourceRange.startUs - offsetSourceUs) / playback.playbackRate,
1021
+ );
1022
+ const audibleEndUs = Math.min(
1023
+ resolved.outputRange.endUs,
1024
+ resolved.outputRange.startUs + Math.floor(
1025
+ (audibleSourceRange.endUs - offsetSourceUs) / playback.playbackRate,
1026
+ ),
1027
+ );
1028
+ if (audibleEndUs <= audibleStartUs) return null;
1029
+ const decodedSourceUs = Math.min(sourceWindowUs, requestedEndSourceUs);
1030
+ const effectiveSourceOutUs = streamStartUs + playback.sourceInUs + decodedSourceUs;
1031
+ if (!Number.isSafeInteger(effectiveSourceOutUs)) {
1032
+ throw new CliError("unsupported-plan", `Overlay ${resolved.operation.overlayId} has an unsupported audio source window.`);
1033
+ }
1034
+ const filters = [
1035
+ `atrim=start=${seconds(streamStartUs + playback.sourceInUs)}:end=${seconds(effectiveSourceOutUs)}`,
1036
+ "aresample=48000",
1037
+ "asetpts=PTS-STARTPTS",
1038
+ ...(audioDelayUs === 0 ? [] : [`adelay=${decimal(audioDelayUs / 1_000)}:all=1`]),
1039
+ ];
1040
+ if (needsLoop) {
1041
+ const samples = Math.max(1, Math.ceil(sourceWindowUs * 48_000 / 1_000_000));
1042
+ if (samples > 10_000_000) throw new CliError("unsupported-plan", "Animated overlay audio loop exceeds its sample buffer bound.");
1043
+ assertAudioLoopBufferWithinLimit({ label: `Overlay ${resolved.operation.overlayId}`, sampleCount: samples });
1044
+ if (audioEndUs < sourceWindowUs) {
1045
+ filters.push(`apad=pad_dur=${seconds(sourceWindowUs - audioEndUs)}`);
1046
+ }
1047
+ filters.push(`atrim=duration=${seconds(sourceWindowUs)}`);
1048
+ filters.push("aformat=sample_fmts=flt:channel_layouts=stereo");
1049
+ filters.push(`aloop=loop=-1:size=${samples}`);
1050
+ }
1051
+ const usableSourceUs = playback.endBehavior === "loop"
1052
+ ? requiredSourceUs
1053
+ : Math.max(0, Math.min(requiredSourceUs, sourceWindowUs - offsetSourceUs));
1054
+ if (usableSourceUs === 0) return null;
1055
+ filters.push(
1056
+ `atrim=start=${seconds(offsetSourceUs)}:duration=${seconds(usableSourceUs)}`,
1057
+ atempo(playback.playbackRate),
1058
+ ...audioPlacementFilters(resolved.outputRange.startUs),
1059
+ `volume=${decimal(source.audioPolicy.volume)}`,
1060
+ );
1061
+ return {
1062
+ audibleEndUs,
1063
+ audibleStartUs,
1064
+ filters,
1065
+ loopDuck: needsLoop && (audioDelayUs > 0 || audioEndUs < sourceWindowUs)
1066
+ ? { audioDelayUs, audioEndUs, sourceOffsetUs: offsetSourceUs, sourceWindowUs }
1067
+ : null,
1068
+ };
1069
+ }
1070
+
1071
+ function overlayVisibleEndUs(overlay: OverlayOperation, startUs: number, endUs: number): number {
1072
+ const source = overlay.source;
1073
+ if ((source.kind !== "gif" && source.kind !== "video") || source.playback.endBehavior !== "hide") return endUs;
1074
+ const sourceWindowUs = source.playback.sourceOutUs - source.playback.sourceInUs;
1075
+ return Math.min(endUs, startUs + Math.round(sourceWindowUs / source.playback.playbackRate));
1076
+ }
1077
+
1078
+ type AudioSlice = ProjectRenderPlanV1["audioSlices"][number];
1079
+
1080
+ function audioSlicesAreContinuous(left: AudioSlice, right: AudioSlice): boolean {
1081
+ return left.placementId === right.placementId
1082
+ && left.streamId === right.streamId
1083
+ && left.outputRange.endUs === right.outputRange.startUs
1084
+ && left.projectRange.endUs === right.projectRange.startUs
1085
+ && left.assetRange.endUs === right.assetRange.startUs
1086
+ && left.presentation.enabled
1087
+ && right.presentation.enabled
1088
+ && left.presentation.gainDb === right.presentation.gainDb
1089
+ && left.presentation.pan === right.presentation.pan;
1090
+ }
1091
+
1092
+ function audioFadeBoundaries(
1093
+ slices: readonly AudioSlice[],
1094
+ ): ReadonlyMap<AudioSlice, Readonly<{ readonly fadeIn: boolean; readonly fadeOut: boolean }>> {
1095
+ const groups = new Map<string, AudioSlice[]>();
1096
+ for (const slice of slices) {
1097
+ const key = `${slice.placementId}\u0000${slice.streamId}`;
1098
+ const group = groups.get(key) ?? [];
1099
+ group.push(slice);
1100
+ groups.set(key, group);
1101
+ }
1102
+ const boundaries = new Map<AudioSlice, { readonly fadeIn: boolean; readonly fadeOut: boolean }>();
1103
+ for (const group of groups.values()) {
1104
+ group.sort((left, right) => (
1105
+ left.outputRange.startUs - right.outputRange.startUs
1106
+ || left.projectRange.startUs - right.projectRange.startUs
1107
+ || left.assetRange.startUs - right.assetRange.startUs
1108
+ ));
1109
+ for (const [index, slice] of group.entries()) {
1110
+ const previous = group[index - 1];
1111
+ const next = group[index + 1];
1112
+ boundaries.set(slice, {
1113
+ fadeIn: previous === undefined || !audioSlicesAreContinuous(previous, slice),
1114
+ fadeOut: next === undefined || !audioSlicesAreContinuous(slice, next),
1115
+ });
1116
+ }
1117
+ }
1118
+ return boundaries;
1119
+ }
1120
+
1121
+ export interface ProjectRenderBuildOptions {
1122
+ readonly dryRun?: boolean;
1123
+ readonly ffmpeg: string;
1124
+ readonly ffprobe?: string;
1125
+ readonly outputPath: string;
1126
+ readonly projectDirectory: string;
1127
+ /** Versioned host-owned encoder recipe. Omitted only for v1 compatibility. */
1128
+ readonly renderTier?: ProjectRenderTier;
1129
+ readonly repositoryRoot: string;
1130
+ readonly rsvgConvert?: string;
1131
+ readonly rsvgConvertVersion?: string;
1132
+ readonly runner?: ProcessRunner;
1133
+ /** Host-owned private root for filter graphs and SVG derivatives. */
1134
+ readonly workspaceDirectory?: string;
1135
+ /** V2 projection's versioned exact cadence; V1 numeric planning hashes retain their meaning. */
1136
+ readonly spatialCadence?: SpatialCompositorCadenceBindingV1;
1137
+ }
1138
+
1139
+ function projectRenderEncoderArguments(
1140
+ recipe: ProjectRenderEncoderRecipe | undefined,
1141
+ ): readonly string[] {
1142
+ return recipe === undefined
1143
+ ? []
1144
+ : [
1145
+ "-threads:v", String(recipe.video.threads),
1146
+ "-preset", recipe.video.preset,
1147
+ "-crf", String(recipe.video.crf),
1148
+ "-b:a", recipe.audio.bitrate,
1149
+ ];
1150
+ }
1151
+
1152
+ function projectRenderFilterThreadArguments(
1153
+ recipe: ProjectRenderEncoderRecipe | undefined,
1154
+ ): readonly string[] {
1155
+ return recipe === undefined
1156
+ ? []
1157
+ : [
1158
+ "-filter_threads", String(recipe.filterThreads),
1159
+ "-filter_complex_threads", String(recipe.filterComplexThreads),
1160
+ ];
1161
+ }
1162
+
1163
+ function projectRenderDecoderThreadArguments(
1164
+ recipe: ProjectRenderEncoderRecipe | undefined,
1165
+ ): readonly string[] {
1166
+ return recipe === undefined
1167
+ ? []
1168
+ : ["-threads", String(recipe.decoderThreads)];
1169
+ }
1170
+
1171
+ export interface ProjectRenderPinnedInput extends ExpectedProjectMediaIntegrity {
1172
+ readonly label: string;
1173
+ readonly path: string;
1174
+ }
1175
+
1176
+ export interface BuiltProjectRenderInvocation {
1177
+ readonly argv: readonly [string, ...string[]];
1178
+ readonly invocation: ProjectRenderInvocation;
1179
+ readonly pinnedInputs: readonly ProjectRenderPinnedInput[];
1180
+ }
1181
+
1182
+ export async function reverifyProjectRenderInputs(
1183
+ inputs: readonly ProjectRenderPinnedInput[],
1184
+ ): Promise<void> {
1185
+ await Promise.all(inputs.map(async input => {
1186
+ await verifyPhysicalProjectMedia(input.path, input, input.label);
1187
+ }));
1188
+ }
1189
+
1190
+ export async function buildProjectFfmpegInvocation(
1191
+ plan: ProjectRenderPlanV1,
1192
+ options: ProjectRenderBuildOptions,
1193
+ ): Promise<BuiltProjectRenderInvocation> {
1194
+ if (plan.output.durationUs <= 0) throw new CliError("unsupported-plan", "A zero-duration project cannot render.");
1195
+ const spatialCadence = options.spatialCadence === undefined ? undefined : assertSpatialCompositorCadence(options.spatialCadence, plan);
1196
+ const rationalRate = spatialCadence === undefined ? undefined : `${spatialCadence.cadence.frameRate.numerator}/${spatialCadence.cadence.frameRate.denominator}`;
1197
+ const emittedRate = rationalRate ?? decimal(plan.output.frameRate);
1198
+ const projectRoot = await realpath(options.projectDirectory);
1199
+ const output = resolve(options.outputPath);
1200
+ if (!isWithin(projectRoot, output)) throw new CliError("unsafe-path", "Project render output must remain in its project directory.");
1201
+ const outputRelative = relative(projectRoot, output);
1202
+ if (!outputRelative.startsWith("renders/") || isAbsolute(outputRelative)) {
1203
+ throw new CliError("unsafe-path", "Project render output must remain under renders/.");
1204
+ }
1205
+ const encoderRecipe = options.renderTier === undefined
1206
+ ? undefined
1207
+ : resolveProjectRenderEncoderRecipe(options.renderTier);
1208
+
1209
+ const mediaIntegrity = new Map<string, ExpectedProjectMediaIntegrity>();
1210
+ for (const slice of [...plan.videoSlices, ...plan.audioSlices]) {
1211
+ const expected = { bytes: slice.bytes, sha256: slice.sha256 };
1212
+ const prior = mediaIntegrity.get(slice.path);
1213
+ if (prior !== undefined && (prior.bytes !== expected.bytes || prior.sha256 !== expected.sha256)) {
1214
+ throw new CliError("invalid-data", `Render slices disagree about media integrity for ${slice.path}.`);
1215
+ }
1216
+ mediaIntegrity.set(slice.path, expected);
1217
+ }
1218
+ const mediaInputIndex = new Map<string, number>();
1219
+ const inputArguments: string[] = [];
1220
+ const pinnedInputs: ProjectRenderPinnedInput[] = [];
1221
+ for (const [path, expected] of mediaIntegrity) {
1222
+ mediaInputIndex.set(path, mediaInputIndex.size);
1223
+ const spatialOutput = spatialCadence === undefined ? null : /^spatial\/outputs\/([a-f0-9]{64})\.(?:mov|mp4|webm)$/u.exec(path);
1224
+ if (spatialOutput !== null && spatialOutput[1] !== expected.sha256) throw new CliError("invalid-data", "Spatial materialized media filename does not match its exact payload digest.");
1225
+ const physical = await resolveVerifiedProjectMedia({
1226
+ expected,
1227
+ label: `Project media ${path}`,
1228
+ path,
1229
+ repositoryRoot: spatialOutput === null ? options.repositoryRoot : projectRoot,
1230
+ });
1231
+ inputArguments.push(
1232
+ ...projectRenderDecoderThreadArguments(encoderRecipe),
1233
+ "-i",
1234
+ physical,
1235
+ );
1236
+ pinnedInputs.push({
1237
+ ...expected,
1238
+ label: `Project media ${path}`,
1239
+ path: physical,
1240
+ });
1241
+ }
1242
+ const overlayInputIndex = new Map<string, number>();
1243
+ const preparedOverlayMedia = new Map<string, PreparedProjectOverlayMedia>();
1244
+ for (const resolvedOverlay of plan.overlays) {
1245
+ const overlay = resolvedOverlay.operation;
1246
+ if (overlayInputIndex.has(overlay.overlayId)) continue;
1247
+ overlayInputIndex.set(overlay.overlayId, mediaInputIndex.size + overlayInputIndex.size);
1248
+ if (overlay.source.kind === "image" || overlay.source.kind === "svg" || overlay.source.kind === "emoji") {
1249
+ inputArguments.push("-loop", "1");
1250
+ }
1251
+ const prepared = await prepareProjectOverlayMedia(overlay, options);
1252
+ preparedOverlayMedia.set(overlay.overlayId, prepared);
1253
+ inputArguments.push(
1254
+ ...projectRenderDecoderThreadArguments(encoderRecipe),
1255
+ "-i",
1256
+ prepared.path,
1257
+ );
1258
+ pinnedInputs.push({
1259
+ bytes: overlay.source.asset.bytes,
1260
+ label: `Overlay ${overlay.overlayId}`,
1261
+ path: prepared.sourcePath,
1262
+ sha256: overlay.source.asset.sha256,
1263
+ });
1264
+ if (prepared.path !== prepared.sourcePath && !options.dryRun) {
1265
+ const derivative = await fingerprintPhysicalProjectMedia(
1266
+ prepared.path,
1267
+ MAXIMUM_SVG_DERIVATIVE_BYTES,
1268
+ );
1269
+ pinnedInputs.push({
1270
+ bytes: derivative.bytes,
1271
+ label: `Prepared overlay ${overlay.overlayId}`,
1272
+ path: derivative.path,
1273
+ sha256: derivative.sha256,
1274
+ });
1275
+ }
1276
+ }
1277
+
1278
+ const filters: string[] = [
1279
+ `color=c=${plan.output.background}:s=${plan.output.pixelWidth}x${plan.output.pixelHeight}:r=${emittedRate}:d=${seconds(plan.output.durationUs)},format=rgba[canvas_0]`,
1280
+ ];
1281
+ const cameraIndex = buildProjectCameraSpatialIndex(plan);
1282
+ let currentVideo = "canvas_0";
1283
+ let serial = 0;
1284
+ for (const slice of plan.videoSlices) {
1285
+ if (!slice.presentation.enabled) continue;
1286
+ const input = mediaInputIndex.get(slice.path)!;
1287
+ const label = `project_video_${serial++}`;
1288
+ const inputDurationUs = slice.fileRange.endUs - slice.fileRange.startUs;
1289
+ const outputDurationUs = slice.outputRange.endUs - slice.outputRange.startUs;
1290
+ const transform = videoTransform(slice, plan.output.pixelWidth, plan.output.pixelHeight);
1291
+ const chain = [
1292
+ `trim=start=${seconds(slice.fileRange.startUs)}:end=${seconds(slice.fileRange.endUs)}`,
1293
+ // A source may use a coarse clock such as 1/24. Apply project offsets in
1294
+ // microseconds so a fractional cut cannot truncate away a whole frame.
1295
+ "settb=AVTB",
1296
+ `setpts=(PTS-STARTPTS)*${decimal(outputDurationUs / inputDurationUs)}+${seconds(slice.outputRange.startUs)}/TB`,
1297
+ ...transform.filters,
1298
+ ...projectCameraFilters(plan, slice, cameraIndex, rationalRate),
1299
+ ];
1300
+ filters.push(`[${inputSpecifier(input, slice.streamIndex)}]${chain.join(",")}[${label}]`);
1301
+ const next = `canvas_${serial++}`;
1302
+ // EOF is the last decoded PTS, not the end of that frame's presentation.
1303
+ // Hold its pixels only inside the compiler's declared half-open interval.
1304
+ const enable = `gte(t,${seconds(slice.outputRange.startUs)})*lt(t,${seconds(slice.outputRange.endUs)})`;
1305
+ if (slice.presentation.blendMode === "normal") {
1306
+ filters.push(
1307
+ `[${currentVideo}][${label}]overlay=x=${transform.x}:y=${transform.y}:eof_action=repeat:repeatlast=1:enable='${enable}'[${next}]`,
1308
+ );
1309
+ } else {
1310
+ const transparent = `video_blend_canvas_${serial++}`;
1311
+ const positioned = `video_blend_positioned_${serial++}`;
1312
+ const layerColor = `video_blend_color_${serial++}`;
1313
+ const layerAlphaSource = `video_blend_alpha_source_${serial++}`;
1314
+ const layerMask = `video_blend_mask_${serial++}`;
1315
+ const baseBlend = `video_blend_base_${serial++}`;
1316
+ const baseMerge = `video_blend_merge_${serial++}`;
1317
+ const blended = `video_blend_result_${serial++}`;
1318
+ filters.push(
1319
+ `color=c=black@0:s=${plan.output.pixelWidth}x${plan.output.pixelHeight}:r=${emittedRate}:d=${seconds(plan.output.durationUs)},format=rgba[${transparent}]`,
1320
+ `[${transparent}][${label}]overlay=x=${transform.x}:y=${transform.y}:eof_action=repeat:repeatlast=1:enable='${enable}'[${positioned}]`,
1321
+ `[${positioned}]format=rgba,split=2[${layerColor}][${layerAlphaSource}]`,
1322
+ `[${layerAlphaSource}]alphaextract[${layerMask}]`,
1323
+ `[${currentVideo}]split=2[${baseBlend}][${baseMerge}]`,
1324
+ `[${baseBlend}][${layerColor}]blend=all_mode=${slice.presentation.blendMode}[${blended}]`,
1325
+ `[${baseMerge}][${blended}][${layerMask}]maskedmerge[${next}]`,
1326
+ );
1327
+ }
1328
+ currentVideo = next;
1329
+ }
1330
+
1331
+ ({ currentVideo, serial } = applyMetadataEffects({
1332
+ cameraKeyframes: [],
1333
+ effects: plan.effects,
1334
+ output: plan.output,
1335
+ }, filters, currentVideo, serial, spatialCadence?.cadence.frameRate));
1336
+
1337
+ const orderedOverlays = [...plan.overlays].sort((left, right) => (
1338
+ left.operation.zIndex - right.operation.zIndex
1339
+ || left.outputRange.startUs - right.outputRange.startUs
1340
+ || left.operation.overlayId.localeCompare(right.operation.overlayId)
1341
+ ));
1342
+ const overlayBounds = new Map<string, { readonly endUs: number; readonly startUs: number }>();
1343
+ for (const resolvedOverlay of orderedOverlays) {
1344
+ const prior = overlayBounds.get(resolvedOverlay.operation.overlayId);
1345
+ overlayBounds.set(resolvedOverlay.operation.overlayId, {
1346
+ endUs: Math.max(prior?.endUs ?? 0, resolvedOverlay.outputRange.endUs),
1347
+ startUs: Math.min(prior?.startUs ?? Number.MAX_SAFE_INTEGER, resolvedOverlay.outputRange.startUs),
1348
+ });
1349
+ }
1350
+ for (const resolvedOverlay of orderedOverlays) {
1351
+ const bounds = overlayBounds.get(resolvedOverlay.operation.overlayId)!;
1352
+ const visibleFullEndUs = overlayVisibleEndUs(resolvedOverlay.operation, bounds.startUs, bounds.endUs);
1353
+ const chain = overlayVideoChain(resolvedOverlay, bounds.startUs, visibleFullEndUs, plan.output.frameRate, spatialCadence);
1354
+ if (chain === null) continue;
1355
+ const input = overlayInputIndex.get(resolvedOverlay.operation.overlayId)!;
1356
+ const label = `graphic_${serial++}`;
1357
+ const source = resolvedOverlay.operation.source;
1358
+ const prepared = preparedOverlayMedia.get(resolvedOverlay.operation.overlayId)!;
1359
+ const videoStreamIndex = source.kind === "gif" || source.kind === "video"
1360
+ ? prepared.videoStreamIndex ?? source.playback.videoStreamIndex
1361
+ : null;
1362
+ filters.push(`[${animatedStreamSpecifier(input, "v", videoStreamIndex)}]${chain.filters.join(",")}[${label}]`);
1363
+ const overlay = resolvedOverlay.operation;
1364
+ const animations = effectiveAnimationBounds(overlay, visibleFullEndUs - bounds.startUs);
1365
+ const x = overlayPositionExpression(overlay, "x", bounds.startUs, visibleFullEndUs, animations.entranceUs, animations.exitUs);
1366
+ const y = overlayPositionExpression(overlay, "y", bounds.startUs, visibleFullEndUs, animations.entranceUs, animations.exitUs);
1367
+ const next = `canvas_${serial++}`;
1368
+ const enable = `between(t,${seconds(resolvedOverlay.outputRange.startUs)},${seconds(chain.visibleEndUs)})`;
1369
+ const blendMode = overlay.blendMode ?? "normal";
1370
+ if (blendMode === "normal") {
1371
+ filters.push(
1372
+ `[${currentVideo}][${label}]overlay=x='${x}':y='${y}':eof_action=pass:repeatlast=0:enable='${enable}'[${next}]`,
1373
+ );
1374
+ } else {
1375
+ const transparent = `graphic_blend_canvas_${serial++}`;
1376
+ const positioned = `graphic_blend_positioned_${serial++}`;
1377
+ const layerColor = `graphic_blend_color_${serial++}`;
1378
+ const layerAlphaSource = `graphic_blend_alpha_source_${serial++}`;
1379
+ const layerMask = `graphic_blend_mask_${serial++}`;
1380
+ const baseBlend = `graphic_blend_base_${serial++}`;
1381
+ const baseMerge = `graphic_blend_merge_${serial++}`;
1382
+ const blended = `graphic_blend_result_${serial++}`;
1383
+ filters.push(
1384
+ `color=c=black@0:s=${plan.output.pixelWidth}x${plan.output.pixelHeight}:r=${emittedRate}:d=${seconds(plan.output.durationUs)},format=rgba[${transparent}]`,
1385
+ `[${transparent}][${label}]overlay=x='${x}':y='${y}':eof_action=pass:repeatlast=0:enable='${enable}'[${positioned}]`,
1386
+ `[${positioned}]format=rgba,split=2[${layerColor}][${layerAlphaSource}]`,
1387
+ `[${layerAlphaSource}]alphaextract[${layerMask}]`,
1388
+ `[${currentVideo}]split=2[${baseBlend}][${baseMerge}]`,
1389
+ `[${baseBlend}][${layerColor}]blend=all_mode=${blendMode}[${blended}]`,
1390
+ `[${baseMerge}][${blended}][${layerMask}]maskedmerge[${next}]`,
1391
+ );
1392
+ }
1393
+ currentVideo = next;
1394
+ }
1395
+ filters.push(spatialCadence === undefined
1396
+ ? `[${currentVideo}]format=yuv420p[video_out]`
1397
+ : `[${currentVideo}]trim=end_frame=${spatialCadence.cadence.frameCount},settb=expr=${spatialCadence.cadence.frameRate.denominator}/${spatialCadence.cadence.frameRate.numerator},setpts=N,format=yuv420p[video_out]`);
1398
+
1399
+ const audioLabels: string[] = [];
1400
+ const fadeBoundaries = audioFadeBoundaries(plan.audioSlices);
1401
+ for (const slice of plan.audioSlices) {
1402
+ if (!slice.presentation.enabled) continue;
1403
+ const input = mediaInputIndex.get(slice.path)!;
1404
+ const label = `project_audio_${serial++}`;
1405
+ const inputDurationUs = slice.fileRange.endUs - slice.fileRange.startUs;
1406
+ const outputDurationUs = slice.outputRange.endUs - slice.outputRange.startUs;
1407
+ const playbackRate = inputDurationUs / outputDurationUs;
1408
+ const fadeUs = Math.min(5_000, Math.floor(outputDurationUs / 4));
1409
+ const boundary = fadeBoundaries.get(slice) ?? { fadeIn: true, fadeOut: true };
1410
+ const chain = [
1411
+ `atrim=start=${seconds(slice.fileRange.startUs)}:end=${seconds(slice.fileRange.endUs)}`,
1412
+ "asetpts=PTS-STARTPTS",
1413
+ "aresample=48000",
1414
+ atempo(playbackRate),
1415
+ `atrim=duration=${seconds(outputDurationUs)}`,
1416
+ ...(fadeUs > 0 && boundary.fadeIn ? [`afade=t=in:st=0:d=${seconds(fadeUs)}`] : []),
1417
+ ...(fadeUs > 0 && boundary.fadeOut
1418
+ ? [`afade=t=out:st=${seconds(outputDurationUs - fadeUs)}:d=${seconds(fadeUs)}`]
1419
+ : []),
1420
+ `volume=${decimal(slice.presentation.gainDb)}dB`,
1421
+ "aformat=channel_layouts=stereo",
1422
+ ...(slice.presentation.pan === 0 ? [] : [`stereotools=balance_out=${decimal(slice.presentation.pan)}`]),
1423
+ ...audioPlacementFilters(slice.outputRange.startUs),
1424
+ ];
1425
+ filters.push(`[${inputSpecifier(input, slice.streamIndex)}]${chain.join(",")}[${label}]`);
1426
+ audioLabels.push(label);
1427
+ }
1428
+ filters.push(`anullsrc=r=48000:cl=stereo:d=${seconds(plan.output.durationUs)}[silence]`);
1429
+ const primaryAudio = "primary_audio";
1430
+ filters.push(
1431
+ `[silence]${audioLabels.map(label => `[${label}]`).join("")}amix=inputs=${audioLabels.length + 1}:duration=longest:dropout_transition=0:normalize=0,atrim=duration=${seconds(plan.output.durationUs)}[${primaryAudio}]`,
1432
+ );
1433
+ const overlayAudioLabels: string[] = [];
1434
+ const duckEnvelopes: { readonly condition: string; readonly target: number }[] = [];
1435
+ for (const resolvedOverlay of orderedOverlays) {
1436
+ const overlayAudio = overlayAudioChain(resolvedOverlay);
1437
+ if (overlayAudio === null) continue;
1438
+ const input = overlayInputIndex.get(resolvedOverlay.operation.overlayId)!;
1439
+ const label = `graphic_audio_${serial++}`;
1440
+ const source = resolvedOverlay.operation.source;
1441
+ const prepared = preparedOverlayMedia.get(resolvedOverlay.operation.overlayId)!;
1442
+ const audioStreamIndex = source.kind === "video"
1443
+ ? prepared.audioStreamIndex ?? source.playback.audioStreamIndex
1444
+ : null;
1445
+ filters.push(`[${animatedStreamSpecifier(input, "a", audioStreamIndex)}]${overlayAudio.filters.join(",")}[${label}]`);
1446
+ if (source.kind === "video" && source.audioPolicy.kind === "duck-primary") {
1447
+ const duckCondition = overlayAudio.loopDuck === null
1448
+ ? `between(t,${seconds(overlayAudio.audibleStartUs)},${seconds(overlayAudio.audibleEndUs)})`
1449
+ : [
1450
+ `between(t,${seconds(resolvedOverlay.outputRange.startUs)},${seconds(resolvedOverlay.outputRange.endUs)})`,
1451
+ `gte(mod(${seconds(overlayAudio.loopDuck.sourceOffsetUs)}+(t-${seconds(resolvedOverlay.outputRange.startUs)})*${decimal(source.playback.playbackRate)},${seconds(overlayAudio.loopDuck.sourceWindowUs)}),${seconds(overlayAudio.loopDuck.audioDelayUs)})`,
1452
+ `lt(mod(${seconds(overlayAudio.loopDuck.sourceOffsetUs)}+(t-${seconds(resolvedOverlay.outputRange.startUs)})*${decimal(source.playback.playbackRate)},${seconds(overlayAudio.loopDuck.sourceWindowUs)}),${seconds(overlayAudio.loopDuck.audioEndUs)})`,
1453
+ ].join("*");
1454
+ duckEnvelopes.push({ condition: duckCondition, target: source.audioPolicy.duckPrimaryTo });
1455
+ }
1456
+ overlayAudioLabels.push(label);
1457
+ }
1458
+ let currentPrimaryAudio = primaryAudio;
1459
+ if (duckEnvelopes.length > 0) {
1460
+ const ducked = `ducked_${serial++}`;
1461
+ const duckVolume = duckEnvelopes
1462
+ .map(envelope => `if(${envelope.condition},${decimal(envelope.target)},1)`)
1463
+ .reduce((left, right) => `min(${left},${right})`);
1464
+ filters.push(`[${primaryAudio}]volume='${duckVolume}':eval=frame[${ducked}]`);
1465
+ currentPrimaryAudio = ducked;
1466
+ }
1467
+ let currentAudio = currentPrimaryAudio;
1468
+ if (overlayAudioLabels.length > 0) {
1469
+ const mixed = `mixed_${serial++}`;
1470
+ filters.push(
1471
+ `[${currentPrimaryAudio}]${overlayAudioLabels.map(label => `[${label}]`).join("")}amix=inputs=${overlayAudioLabels.length + 1}:duration=longest:dropout_transition=0:normalize=0[${mixed}]`,
1472
+ );
1473
+ currentAudio = mixed;
1474
+ }
1475
+
1476
+ if (spatialCadence !== undefined) {
1477
+ const boundedAudio = `spatial_audio_${serial++}`;
1478
+ filters.push(`[${currentAudio}]atrim=duration=${seconds(plan.output.durationUs)}[${boundedAudio}]`);
1479
+ currentAudio = boundedAudio;
1480
+ }
1481
+ const filterGraph = await materializeFilterScript({
1482
+ graph: filters.join(";"),
1483
+ relativeDirectory: options.workspaceDirectory === undefined
1484
+ ? "renders/.filter-graphs"
1485
+ : "filter-graphs",
1486
+ root: options.workspaceDirectory ?? projectRoot,
1487
+ });
1488
+ const arguments_: string[] = [
1489
+ "-hide_banner", "-nostdin", "-y",
1490
+ ...projectRenderFilterThreadArguments(encoderRecipe),
1491
+ ...inputArguments,
1492
+ "-filter_complex_script", filterGraph.path,
1493
+ "-map", "[video_out]",
1494
+ "-map", `[${currentAudio}]`,
1495
+ "-c:v", encoderRecipe?.video.codec ?? "libx264",
1496
+ ...projectRenderEncoderArguments(encoderRecipe),
1497
+ "-pix_fmt", encoderRecipe?.video.pixelFormat ?? "yuv420p",
1498
+ "-c:a", encoderRecipe?.audio.codec ?? "aac",
1499
+ ...(spatialCadence === undefined ? [] : [
1500
+ "-r:v", rationalRate!, "-fps_mode:v", "cfr",
1501
+ "-enc_time_base:v", `${spatialCadence.cadence.frameRate.denominator}:${spatialCadence.cadence.frameRate.numerator}`,
1502
+ "-video_track_timescale", String(spatialCadence.cadence.frameRate.numerator),
1503
+ // The default 1-kHz MOV edit-list clock would truncate sub-ms audio tails.
1504
+ "-movie_timescale", "1000000",
1505
+ ]),
1506
+ ...(spatialCadence === undefined ? ["-t", seconds(plan.output.durationUs)] : []),
1507
+ "-movflags", encoderRecipe?.container.movflags ?? "+faststart",
1508
+ output,
1509
+ ];
1510
+ const invocation = ProjectRenderInvocationSchema.parse({
1511
+ arguments: arguments_,
1512
+ executable: "ffmpeg",
1513
+ filterGraph: {
1514
+ bytes: filterGraph.bytes,
1515
+ path: filterGraph.repositoryPath,
1516
+ sha256: filterGraph.sha256,
1517
+ },
1518
+ outputPath: outputRelative,
1519
+ renderPlanSha256: canonicalJsonSha256(plan),
1520
+ });
1521
+ return {
1522
+ argv: [options.ffmpeg, ...arguments_],
1523
+ invocation,
1524
+ pinnedInputs,
1525
+ };
1526
+ }