@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,885 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants, type BigIntStats, type Stats } from "node:fs";
3
+ import {
4
+ type FileHandle,
5
+ lstat,
6
+ open,
7
+ realpath,
8
+ } from "node:fs/promises";
9
+ import { basename, dirname, extname, join, resolve } from "node:path";
10
+
11
+ import { Effect, Exit, Option, Ref } from "effect";
12
+
13
+ import {
14
+ AudioEffectsTransformV1Schema,
15
+ ColorGradeTransformV1Schema,
16
+ type AudioEffect,
17
+ type AudioEffectsTransformV1,
18
+ type ColorGradeControls,
19
+ type ColorGradeTransformV1,
20
+ } from "../contracts";
21
+ import {
22
+ operationBoundary,
23
+ operationResource,
24
+ operationValidation,
25
+ runStandaloneOperation,
26
+ type OperationEffectFailure,
27
+ } from "../application/operation-effects";
28
+ import { executeAtomicRenderEffect, type AtomicRenderOutput } from "./atomic-render-effects";
29
+ import { AtomicRenderPlatformLive } from "./atomic-render-platform";
30
+ import { CliError } from "./errors";
31
+ import type { ProcessRunner, RunOptions } from "./io";
32
+ import { SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS } from "./media-ingest";
33
+
34
+ const MAXIMUM_PATH_BYTES = 8_192;
35
+ const MAXIMUM_LOCAL_MEDIA_EFFECT_INPUT_BYTES = 512 * 1024 * 1024 * 1024;
36
+ export const MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
37
+ const LOCAL_MEDIA_EFFECT_TIMEOUT_MS = 12 * 60 * 60_000;
38
+ const PINNED_CHILD_INPUT_DESCRIPTOR = 3;
39
+ const PINNED_CHILD_INPUT_PATH = `/dev/fd/${PINNED_CHILD_INPUT_DESCRIPTOR}`;
40
+
41
+ const REVERB_TAPS = {
42
+ "small-room": [
43
+ { delayMs: 29, weight: 0.5 },
44
+ { delayMs: 47, weight: 0.3 },
45
+ { delayMs: 71, weight: 0.2 },
46
+ ],
47
+ "medium-room": [
48
+ { delayMs: 43, weight: 0.4 },
49
+ { delayMs: 67, weight: 0.3 },
50
+ { delayMs: 101, weight: 0.2 },
51
+ { delayMs: 149, weight: 0.1 },
52
+ ],
53
+ "large-hall": [
54
+ { delayMs: 61, weight: 0.35 },
55
+ { delayMs: 113, weight: 0.27 },
56
+ { delayMs: 173, weight: 0.2 },
57
+ { delayMs: 257, weight: 0.12 },
58
+ { delayMs: 347, weight: 0.06 },
59
+ ],
60
+ plate: [
61
+ { delayMs: 23, weight: 0.28 },
62
+ { delayMs: 37, weight: 0.24 },
63
+ { delayMs: 53, weight: 0.2 },
64
+ { delayMs: 79, weight: 0.16 },
65
+ { delayMs: 113, weight: 0.12 },
66
+ ],
67
+ } as const satisfies Readonly<Record<
68
+ Extract<AudioEffect, { readonly kind: "reverb" }>["preset"],
69
+ readonly { readonly delayMs: number; readonly weight: number }[]
70
+ >>;
71
+
72
+ export interface ResolvedColorGradeControls {
73
+ readonly brightness: number;
74
+ readonly contrast: number;
75
+ readonly gamma: number;
76
+ readonly hue: number;
77
+ readonly saturation: number;
78
+ readonly temperature: number;
79
+ readonly tint: number;
80
+ }
81
+
82
+ const NEUTRAL_GRADE: ResolvedColorGradeControls = {
83
+ brightness: 0,
84
+ contrast: 1,
85
+ gamma: 1,
86
+ hue: 0,
87
+ saturation: 1,
88
+ temperature: 0,
89
+ tint: 0,
90
+ };
91
+
92
+ const COLOR_PRESETS = {
93
+ cinematic: {
94
+ brightness: -0.02,
95
+ contrast: 1.14,
96
+ gamma: 0.96,
97
+ hue: 0,
98
+ saturation: 0.88,
99
+ temperature: 0.12,
100
+ tint: 0.06,
101
+ },
102
+ clean: {
103
+ brightness: 0.02,
104
+ contrast: 1.04,
105
+ gamma: 1,
106
+ hue: 0,
107
+ saturation: 1.02,
108
+ temperature: 0,
109
+ tint: 0,
110
+ },
111
+ cool: {
112
+ brightness: 0,
113
+ contrast: 1.05,
114
+ gamma: 1,
115
+ hue: 0,
116
+ saturation: 0.96,
117
+ temperature: -0.35,
118
+ tint: 0,
119
+ },
120
+ flat: {
121
+ brightness: 0.02,
122
+ contrast: 0.86,
123
+ gamma: 1.04,
124
+ hue: 0,
125
+ saturation: 0.85,
126
+ temperature: 0,
127
+ tint: 0,
128
+ },
129
+ monochrome: {
130
+ brightness: 0,
131
+ contrast: 1.1,
132
+ gamma: 1,
133
+ hue: 0,
134
+ saturation: 0,
135
+ temperature: 0,
136
+ tint: 0,
137
+ },
138
+ vivid: {
139
+ brightness: 0.01,
140
+ contrast: 1.12,
141
+ gamma: 1,
142
+ hue: 0,
143
+ saturation: 1.22,
144
+ temperature: 0,
145
+ tint: 0,
146
+ },
147
+ warm: {
148
+ brightness: 0,
149
+ contrast: 1.04,
150
+ gamma: 1,
151
+ hue: 0,
152
+ saturation: 1.08,
153
+ temperature: 0.35,
154
+ tint: 0,
155
+ },
156
+ } as const satisfies Readonly<Record<
157
+ Extract<ColorGradeTransformV1["grade"], { readonly kind: "preset" }>["preset"],
158
+ ResolvedColorGradeControls
159
+ >>;
160
+
161
+ export interface BuiltAudioEffectsGraph {
162
+ readonly filterGraph: string;
163
+ readonly outputLabel: string;
164
+ readonly transform: AudioEffectsTransformV1;
165
+ }
166
+
167
+ export interface BuiltColorGradeFilter {
168
+ readonly controls: ResolvedColorGradeControls;
169
+ readonly filter: string;
170
+ readonly transform: ColorGradeTransformV1;
171
+ }
172
+
173
+ export interface BuiltAudioEffectsInvocation extends BuiltAudioEffectsGraph {
174
+ readonly argv: readonly [string, ...string[]];
175
+ }
176
+
177
+ export interface BuiltColorGradeInvocation extends BuiltColorGradeFilter {
178
+ readonly argv: readonly [string, ...string[]];
179
+ }
180
+
181
+ export interface LocalMediaTransformResult<Transform> extends AtomicRenderOutput {
182
+ readonly filterGraph: string;
183
+ readonly outputPath: string;
184
+ readonly transform: Transform;
185
+ }
186
+
187
+ export interface ExpectedLocalMediaInput {
188
+ readonly bytes: number;
189
+ readonly device: number;
190
+ readonly inode: number;
191
+ readonly modifiedAtMs: number;
192
+ readonly sha256: string;
193
+ }
194
+
195
+ interface PinnedLocalMediaInput {
196
+ readonly assertUnchanged: () => Promise<void>;
197
+ readonly processPath: string;
198
+ readonly runner: ProcessRunner;
199
+ }
200
+
201
+ function validateExpectedInput(input: ExpectedLocalMediaInput): void {
202
+ if (
203
+ !Number.isSafeInteger(input.bytes)
204
+ || input.bytes <= 0
205
+ || input.bytes > MAXIMUM_LOCAL_MEDIA_EFFECT_INPUT_BYTES
206
+ || !Number.isSafeInteger(input.device)
207
+ || input.device < 0
208
+ || !Number.isSafeInteger(input.inode)
209
+ || input.inode < 0
210
+ || !Number.isFinite(input.modifiedAtMs)
211
+ || !/^[a-f0-9]{64}$/u.test(input.sha256)
212
+ ) {
213
+ throw new CliError("internal", "Expected local-media input identity is invalid.");
214
+ }
215
+ }
216
+
217
+ function sameExactFile(left: BigIntStats, right: BigIntStats): boolean {
218
+ return left.dev === right.dev
219
+ && left.ino === right.ino
220
+ && left.size === right.size
221
+ && left.nlink === right.nlink
222
+ && left.mtimeNs === right.mtimeNs
223
+ && left.ctimeNs === right.ctimeNs;
224
+ }
225
+
226
+ async function hashFileHandle(handle: FileHandle, bytes: number): Promise<string> {
227
+ const hash = createHash("sha256");
228
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
229
+ let offset = 0;
230
+ while (offset < bytes) {
231
+ const read = await handle.read(
232
+ buffer,
233
+ 0,
234
+ Math.min(buffer.byteLength, bytes - offset),
235
+ offset,
236
+ );
237
+ if (read.bytesRead === 0) break;
238
+ hash.update(buffer.subarray(0, read.bytesRead));
239
+ offset += read.bytesRead;
240
+ }
241
+ if (offset !== bytes) {
242
+ throw new CliError("conflict", "Media input ended while it was being pinned.");
243
+ }
244
+ return hash.digest("hex");
245
+ }
246
+
247
+ class InheritedInputRunner implements ProcessRunner {
248
+ readonly #descriptor: number;
249
+ readonly #runner: ProcessRunner;
250
+
251
+ constructor(runner: ProcessRunner, descriptor: number) {
252
+ this.#descriptor = descriptor;
253
+ this.#runner = runner;
254
+ }
255
+
256
+ run(
257
+ argv: readonly [string, ...string[]],
258
+ options: RunOptions = {},
259
+ ): ReturnType<ProcessRunner["run"]> {
260
+ if ((options.inheritedFileDescriptors?.length ?? 0) !== 0) {
261
+ throw new CliError("internal", "Pinned media execution cannot inherit unrelated file descriptors.");
262
+ }
263
+ return this.#runner.run(argv, {
264
+ ...options,
265
+ inheritedFileDescriptors: [this.#descriptor],
266
+ });
267
+ }
268
+ }
269
+
270
+ interface OpenedLocalMediaInput {
271
+ readonly handle: FileHandle;
272
+ readonly lexical: Stats;
273
+ }
274
+
275
+ async function openLocalMediaInput(
276
+ path: string,
277
+ expected: ExpectedLocalMediaInput,
278
+ ): Promise<OpenedLocalMediaInput> {
279
+ validateExpectedInput(expected);
280
+ if (process.platform === "win32") {
281
+ throw new CliError(
282
+ "unavailable",
283
+ "Descriptor-pinned local media effects are supported on macOS and other POSIX systems.",
284
+ );
285
+ }
286
+ const lexical = await lstat(path).catch((error: unknown) => {
287
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
288
+ throw new CliError("not-found", `Media input does not exist: ${path}`);
289
+ }
290
+ throw error;
291
+ });
292
+ if (
293
+ lexical.isSymbolicLink()
294
+ || !lexical.isFile()
295
+ || lexical.size !== expected.bytes
296
+ || lexical.dev !== expected.device
297
+ || lexical.ino !== expected.inode
298
+ || lexical.mtimeMs !== expected.modifiedAtMs
299
+ ) {
300
+ throw new CliError("conflict", "Media input changed after it was inspected.");
301
+ }
302
+ const handle = await open(
303
+ path,
304
+ constants.O_RDONLY
305
+ | (constants.O_NOFOLLOW ?? 0)
306
+ | (constants.O_NONBLOCK ?? 0),
307
+ ).catch((error: unknown) => {
308
+ if (error instanceof Error && "code" in error && error.code === "ELOOP") {
309
+ throw new CliError("unsafe-path", `Media input must not be a symlink: ${path}`);
310
+ }
311
+ throw error;
312
+ });
313
+ return { handle, lexical };
314
+ }
315
+
316
+ /** First-observed failure wins, but both admitted native reads settle before close. */
317
+ function pinOpenedLocalMediaInput(
318
+ { handle, lexical }: OpenedLocalMediaInput,
319
+ expected: ExpectedLocalMediaInput,
320
+ runner: ProcessRunner,
321
+ ): Effect.Effect<PinnedLocalMediaInput, OperationEffectFailure> {
322
+ return Effect.uninterruptible(Effect.gen(function*() {
323
+ const firstFailure = yield* Ref.make(Option.none<OperationEffectFailure>());
324
+ const capture = <A>(read: Effect.Effect<A, OperationEffectFailure>) => Effect.exit(Effect.tapError(
325
+ read, failure => Ref.update(firstFailure, first => Option.isNone(first) ? Option.some(failure) : first),
326
+ ));
327
+ const inspected = yield* Effect.all({
328
+ opened: capture(operationBoundary("media", () => handle.stat())),
329
+ exact: capture(operationBoundary("media", () => handle.stat({ bigint: true }))),
330
+ }, { concurrency: 2 });
331
+ const joined = Exit.zip(inspected.opened, inspected.exact);
332
+ if (Exit.isFailure(joined)) {
333
+ const first = yield* Ref.get(firstFailure);
334
+ if (Option.isSome(first)) return yield* Effect.fail({ ...first.value, priorCause: joined.cause });
335
+ return yield* Effect.failCause(joined.cause);
336
+ }
337
+ const [opened, exactBefore] = joined.value;
338
+ yield* operationValidation("media", () => {
339
+ if (
340
+ !opened.isFile()
341
+ || !exactBefore.isFile()
342
+ || opened.dev !== lexical.dev
343
+ || opened.ino !== lexical.ino
344
+ || opened.size !== lexical.size
345
+ || opened.dev !== expected.device
346
+ || opened.ino !== expected.inode
347
+ || opened.size !== expected.bytes
348
+ || opened.mtimeMs !== expected.modifiedAtMs
349
+ ) {
350
+ throw new CliError("conflict", "Media input changed while its descriptor was being pinned.");
351
+ }
352
+ });
353
+ const sha256 = yield* operationBoundary("media", () => hashFileHandle(handle, expected.bytes));
354
+ const exactAfter = yield* operationBoundary("media", () => handle.stat({ bigint: true }));
355
+ yield* operationValidation("media", () => {
356
+ if (!sameExactFile(exactBefore, exactAfter) || sha256 !== expected.sha256) {
357
+ throw new CliError("conflict", "Media input bytes changed before rendering.");
358
+ }
359
+ });
360
+ return {
361
+ assertUnchanged: async () => {
362
+ const afterRender = await handle.stat({ bigint: true });
363
+ if (!sameExactFile(exactAfter, afterRender)) {
364
+ throw new CliError("conflict", "Media input changed while effects were rendering.");
365
+ }
366
+ },
367
+ processPath: PINNED_CHILD_INPUT_PATH,
368
+ runner: new InheritedInputRunner(runner, handle.fd),
369
+ };
370
+ }));
371
+ }
372
+
373
+ function decimal(value: number): string {
374
+ if (!Number.isFinite(value)) throw new CliError("internal", "FFmpeg filter value must be finite.");
375
+ const normalized = Math.abs(value) < 0.000_000_000_1 ? 0 : value;
376
+ return normalized.toFixed(10).replace(/0+$/u, "").replace(/\.$/u, "");
377
+ }
378
+
379
+ function amplitude(decibels: number): number {
380
+ return 10 ** (decibels / 20);
381
+ }
382
+
383
+ function parseAudioTransform(input: unknown): AudioEffectsTransformV1 {
384
+ const parsed = AudioEffectsTransformV1Schema.safeParse(input);
385
+ if (!parsed.success) {
386
+ throw new CliError(
387
+ "usage",
388
+ `Invalid audio-effects transform: ${parsed.error.issues[0]?.message ?? "unknown error"}`,
389
+ );
390
+ }
391
+ return parsed.data;
392
+ }
393
+
394
+ function parseColorTransform(input: unknown): ColorGradeTransformV1 {
395
+ const parsed = ColorGradeTransformV1Schema.safeParse(input);
396
+ if (!parsed.success) {
397
+ throw new CliError(
398
+ "usage",
399
+ `Invalid color-grade transform: ${parsed.error.issues[0]?.message ?? "unknown error"}`,
400
+ );
401
+ }
402
+ return parsed.data;
403
+ }
404
+
405
+ function effectOutputLabel(index: number): string {
406
+ return `audio_fx_${index}`;
407
+ }
408
+
409
+ function buildDelayGraph(
410
+ inputLabel: string,
411
+ outputLabel: string,
412
+ effect: Extract<AudioEffect, { readonly kind: "delay" }>,
413
+ serial: number,
414
+ ): readonly string[] {
415
+ const dry = `delay_${serial}_dry`;
416
+ const dryGain = `delay_${serial}_dry_gain`;
417
+ const maximumTaps = Math.max(
418
+ 1,
419
+ Math.min(16, Math.floor(30_000 / effect.delayMs)),
420
+ );
421
+ const tapCount = effect.decay === 0
422
+ ? 1
423
+ : effect.decay >= 1
424
+ ? maximumTaps
425
+ : Math.min(
426
+ maximumTaps,
427
+ Math.max(
428
+ 1,
429
+ Math.ceil(Math.log(0.01) / Math.log(effect.decay)),
430
+ ),
431
+ );
432
+ const taps = Array.from(
433
+ { length: tapCount },
434
+ (_, index) => ({
435
+ delayMs: effect.delayMs * (index + 1),
436
+ gain: effect.mix * effect.decay ** index,
437
+ input: `delay_${serial}_tap_${index}`,
438
+ output: `delay_${serial}_tap_${index}_gain`,
439
+ }),
440
+ );
441
+ return [
442
+ `[${inputLabel}]asplit=${taps.length + 1}[${dry}]${taps.map(tap => `[${tap.input}]`).join("")}`,
443
+ `[${dry}]volume=volume=${decimal(1 - effect.mix)}[${dryGain}]`,
444
+ ...taps.map(tap => (
445
+ `[${tap.input}]adelay=delays=${tap.delayMs}:all=1,volume=volume=${decimal(tap.gain)}[${tap.output}]`
446
+ )),
447
+ `[${dryGain}]${taps.map(tap => `[${tap.output}]`).join("")}amix=inputs=${taps.length + 1}:duration=longest:dropout_transition=0:normalize=0[${outputLabel}]`,
448
+ ];
449
+ }
450
+
451
+ function buildReverbGraph(
452
+ inputLabel: string,
453
+ outputLabel: string,
454
+ effect: Extract<AudioEffect, { readonly kind: "reverb" }>,
455
+ serial: number,
456
+ ): readonly string[] {
457
+ const taps = REVERB_TAPS[effect.preset];
458
+ const dry = `reverb_${serial}_dry`;
459
+ const dryGain = `reverb_${serial}_dry_gain`;
460
+ const wetInputs = taps.map((_, index) => `reverb_${serial}_tap_${index}`);
461
+ const wetGains = taps.map((_, index) => `reverb_${serial}_tap_${index}_gain`);
462
+ return [
463
+ `[${inputLabel}]asplit=${taps.length + 1}[${dry}]${wetInputs.map(label => `[${label}]`).join("")}`,
464
+ `[${dry}]volume=volume=${decimal(1 - effect.mix)}[${dryGain}]`,
465
+ ...taps.map((tap, index) =>
466
+ `[${wetInputs[index]}]adelay=delays=${tap.delayMs}:all=1,volume=volume=${decimal(effect.mix * tap.weight)}[${wetGains[index]}]`
467
+ ),
468
+ `[${dryGain}]${wetGains.map(label => `[${label}]`).join("")}amix=inputs=${taps.length + 1}:duration=longest:dropout_transition=0:normalize=0[${outputLabel}]`,
469
+ ];
470
+ }
471
+
472
+ /** Build a bounded, numeric-only FFmpeg graph. No caller text reaches the graph. */
473
+ export function buildAudioEffectsFilterGraph(input: unknown): BuiltAudioEffectsGraph {
474
+ const transform = parseAudioTransform(input);
475
+ const filters: string[] = [];
476
+ let currentLabel = `0:a:${transform.audioStreamIndex}`;
477
+ for (const [index, effect] of transform.effects.entries()) {
478
+ const nextLabel = effectOutputLabel(index);
479
+ if (effect.kind === "volume") {
480
+ filters.push(`[${currentLabel}]volume=volume=${decimal(effect.gainDb)}dB[${nextLabel}]`);
481
+ } else if (effect.kind === "compressor") {
482
+ filters.push(
483
+ `[${currentLabel}]acompressor=threshold=${decimal(amplitude(effect.thresholdDb))}:ratio=${decimal(effect.ratio)}:attack=${decimal(effect.attackMs)}:release=${decimal(effect.releaseMs)}:makeup=${decimal(amplitude(effect.makeupGainDb))}:knee=${decimal(effect.knee)}:detection=rms:link=average[${nextLabel}]`,
484
+ );
485
+ } else if (effect.kind === "delay") {
486
+ filters.push(...buildDelayGraph(currentLabel, nextLabel, effect, index));
487
+ } else if (effect.kind === "reverb") {
488
+ filters.push(...buildReverbGraph(currentLabel, nextLabel, effect, index));
489
+ } else if (effect.kind === "denoise") {
490
+ filters.push(
491
+ `[${currentLabel}]afftdn=nf=${decimal(effect.noiseFloorDb)}:nr=${decimal(effect.noiseReductionDb)}:tn=${effect.trackNoise ? "1" : "0"}[${nextLabel}]`,
492
+ );
493
+ } else {
494
+ effect satisfies never;
495
+ }
496
+ currentLabel = nextLabel;
497
+ }
498
+ return {
499
+ filterGraph: filters.join(";"),
500
+ outputLabel: currentLabel,
501
+ transform,
502
+ };
503
+ }
504
+
505
+ function applyControls(
506
+ base: ResolvedColorGradeControls,
507
+ overrides: ColorGradeControls | undefined,
508
+ ): ResolvedColorGradeControls {
509
+ return {
510
+ brightness: overrides?.brightness ?? base.brightness,
511
+ contrast: overrides?.contrast ?? base.contrast,
512
+ gamma: overrides?.gamma ?? base.gamma,
513
+ hue: overrides?.hue ?? base.hue,
514
+ saturation: overrides?.saturation ?? base.saturation,
515
+ temperature: overrides?.temperature ?? base.temperature,
516
+ tint: overrides?.tint ?? base.tint,
517
+ };
518
+ }
519
+
520
+ function clampBalance(value: number): number {
521
+ return Math.max(-1, Math.min(1, value));
522
+ }
523
+
524
+ function balanceBand(
525
+ temperature: number,
526
+ tint: number,
527
+ strength: number,
528
+ ): { readonly blue: number; readonly green: number; readonly red: number } {
529
+ return {
530
+ blue: clampBalance((-temperature + tint * 0.5) * strength),
531
+ green: clampBalance(-tint * strength),
532
+ red: clampBalance((temperature + tint * 0.5) * strength),
533
+ };
534
+ }
535
+
536
+ export function resolveColorGradeControls(input: unknown): ResolvedColorGradeControls {
537
+ const transform = parseColorTransform(input);
538
+ return transform.grade.kind === "custom"
539
+ ? applyControls(NEUTRAL_GRADE, transform.grade.controls)
540
+ : applyControls(COLOR_PRESETS[transform.grade.preset], transform.grade.overrides);
541
+ }
542
+
543
+ /** Resolve a preset plus overrides into an explicit, deterministic FFmpeg chain. */
544
+ export function buildColorGradeFilter(input: unknown): BuiltColorGradeFilter {
545
+ const transform = parseColorTransform(input);
546
+ const controls = transform.grade.kind === "custom"
547
+ ? applyControls(NEUTRAL_GRADE, transform.grade.controls)
548
+ : applyControls(COLOR_PRESETS[transform.grade.preset], transform.grade.overrides);
549
+ const filters = [
550
+ `eq=brightness=${decimal(controls.brightness)}:contrast=${decimal(controls.contrast)}:saturation=${decimal(controls.saturation)}:gamma=${decimal(controls.gamma)}`,
551
+ ];
552
+ if (controls.temperature !== 0 || controls.tint !== 0) {
553
+ const shadows = balanceBand(controls.temperature, controls.tint, 0.12);
554
+ const midtones = balanceBand(controls.temperature, controls.tint, 0.2);
555
+ const highlights = balanceBand(controls.temperature, controls.tint, 0.1);
556
+ filters.push(
557
+ [
558
+ `colorbalance=rs=${decimal(shadows.red)}`,
559
+ `gs=${decimal(shadows.green)}`,
560
+ `bs=${decimal(shadows.blue)}`,
561
+ `rm=${decimal(midtones.red)}`,
562
+ `gm=${decimal(midtones.green)}`,
563
+ `bm=${decimal(midtones.blue)}`,
564
+ `rh=${decimal(highlights.red)}`,
565
+ `gh=${decimal(highlights.green)}`,
566
+ `bh=${decimal(highlights.blue)}`,
567
+ ].join(":"),
568
+ );
569
+ }
570
+ if (controls.hue !== 0) filters.push(`hue=h=${decimal(controls.hue)}`);
571
+ return { controls, filter: filters.join(","), transform };
572
+ }
573
+
574
+ function validateArgument(value: string, label: string): void {
575
+ if (
576
+ value.length === 0
577
+ || value.includes("\0")
578
+ || Buffer.byteLength(value) > MAXIMUM_PATH_BYTES
579
+ ) {
580
+ throw new CliError("usage", `${label} must be a bounded, nonempty, NUL-free string.`);
581
+ }
582
+ }
583
+
584
+ function assertExtension(path: string, expected: readonly string[], label: string): void {
585
+ const extension = extname(path).toLocaleLowerCase("en-US");
586
+ if (!expected.includes(extension)) {
587
+ throw new CliError("usage", `${label} output must use ${expected.join(", ")}.`);
588
+ }
589
+ }
590
+
591
+ function audioEncodingArguments(transform: AudioEffectsTransformV1, outputPath: string): readonly string[] {
592
+ if (transform.output.kind === "preserve-video") {
593
+ if (transform.output.profile === "aac") {
594
+ assertExtension(outputPath, [".mp4", ".mov", ".m4v"], "AAC video");
595
+ return ["-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart"];
596
+ }
597
+ assertExtension(outputPath, [".mkv", ".webm"], "Opus video");
598
+ return ["-c:v", "copy", "-c:a", "libopus", "-b:a", "160k"];
599
+ }
600
+ if (transform.output.profile === "wav-pcm-s16le") {
601
+ assertExtension(outputPath, [".wav"], "PCM");
602
+ return ["-c:a", "pcm_s16le"];
603
+ }
604
+ if (transform.output.profile === "flac") {
605
+ assertExtension(outputPath, [".flac"], "FLAC");
606
+ return ["-c:a", "flac"];
607
+ }
608
+ if (transform.output.profile === "mp3") {
609
+ assertExtension(outputPath, [".mp3"], "MP3");
610
+ return ["-c:a", "libmp3lame", "-q:a", "2"];
611
+ }
612
+ if (transform.output.profile === "aac") {
613
+ assertExtension(outputPath, [".aac", ".m4a", ".mp4"], "AAC");
614
+ return ["-c:a", "aac", "-b:a", "192k"];
615
+ }
616
+ assertExtension(outputPath, [".ogg", ".opus", ".webm"], "Opus");
617
+ return ["-c:a", "libopus", "-b:a", "160k"];
618
+ }
619
+
620
+ function colorEncodingArguments(transform: ColorGradeTransformV1, outputPath: string): readonly string[] {
621
+ if (transform.outputProfile === "h264-mp4") {
622
+ assertExtension(outputPath, [".mp4"], "H.264");
623
+ return [
624
+ "-c:v", "libx264",
625
+ "-preset", "medium",
626
+ "-crf", "18",
627
+ "-pix_fmt", "yuv420p",
628
+ "-c:a", "aac",
629
+ "-b:a", "192k",
630
+ "-movflags", "+faststart",
631
+ ];
632
+ }
633
+ if (transform.outputProfile === "prores-mov") {
634
+ assertExtension(outputPath, [".mov"], "ProRes");
635
+ return [
636
+ "-c:v", "prores_ks",
637
+ "-profile:v", "3",
638
+ "-pix_fmt", "yuv422p10le",
639
+ "-c:a", "pcm_s16le",
640
+ ];
641
+ }
642
+ assertExtension(outputPath, [".webm"], "VP9");
643
+ return [
644
+ "-c:v", "libvpx-vp9",
645
+ "-crf", "30",
646
+ "-b:v", "0",
647
+ "-c:a", "libopus",
648
+ "-b:a", "160k",
649
+ ];
650
+ }
651
+
652
+ export function buildAudioEffectsInvocation(options: {
653
+ readonly ffmpeg: string;
654
+ readonly inputPath: string;
655
+ readonly outputPath: string;
656
+ readonly transform: unknown;
657
+ }): BuiltAudioEffectsInvocation {
658
+ validateArgument(options.ffmpeg, "FFmpeg executable");
659
+ validateArgument(options.inputPath, "Input path");
660
+ validateArgument(options.outputPath, "Output path");
661
+ if (resolve(options.inputPath) === resolve(options.outputPath)) {
662
+ throw new CliError("unsafe-path", "Audio-effects output must differ from its immutable input.");
663
+ }
664
+ const built = buildAudioEffectsFilterGraph(options.transform);
665
+ const arguments_: string[] = [
666
+ "-hide_banner", "-nostdin", "-xerror", "-y",
667
+ ...SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS,
668
+ "-i", options.inputPath,
669
+ "-filter_complex", built.filterGraph,
670
+ ...(built.transform.output.kind === "preserve-video"
671
+ ? [
672
+ "-map",
673
+ built.transform.output.inputVideoStreamIndex === undefined
674
+ ? `0:V:${built.transform.output.videoStreamIndex}`
675
+ : `0:${built.transform.output.inputVideoStreamIndex}`,
676
+ ]
677
+ : []),
678
+ "-map", `[${built.outputLabel}]`,
679
+ "-map_metadata", "0",
680
+ "-sn", "-dn",
681
+ "-fs", String(MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES),
682
+ ...audioEncodingArguments(built.transform, options.outputPath),
683
+ options.outputPath,
684
+ ];
685
+ return {
686
+ ...built,
687
+ argv: [options.ffmpeg, ...arguments_],
688
+ };
689
+ }
690
+
691
+ export function buildColorGradeInvocation(options: {
692
+ readonly ffmpeg: string;
693
+ readonly inputPath: string;
694
+ readonly outputPath: string;
695
+ readonly transform: unknown;
696
+ }): BuiltColorGradeInvocation {
697
+ validateArgument(options.ffmpeg, "FFmpeg executable");
698
+ validateArgument(options.inputPath, "Input path");
699
+ validateArgument(options.outputPath, "Output path");
700
+ if (resolve(options.inputPath) === resolve(options.outputPath)) {
701
+ throw new CliError("unsafe-path", "Color-grade output must differ from its immutable input.");
702
+ }
703
+ const built = buildColorGradeFilter(options.transform);
704
+ const outputLabel = "graded_video";
705
+ const arguments_: string[] = [
706
+ "-hide_banner", "-nostdin", "-xerror", "-y",
707
+ ...SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS,
708
+ "-i", options.inputPath,
709
+ "-filter_complex", `[${
710
+ built.transform.inputStreamIndex === undefined
711
+ ? `0:V:${built.transform.videoStreamIndex}`
712
+ : `0:${built.transform.inputStreamIndex}`
713
+ }]${built.filter}[${outputLabel}]`,
714
+ "-map", `[${outputLabel}]`,
715
+ "-map", "0:a?",
716
+ "-map_metadata", "0",
717
+ "-sn", "-dn",
718
+ "-fs", String(MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES),
719
+ ...colorEncodingArguments(built.transform, options.outputPath),
720
+ options.outputPath,
721
+ ];
722
+ return {
723
+ ...built,
724
+ argv: [options.ffmpeg, ...arguments_],
725
+ };
726
+ }
727
+
728
+ async function assertSafeTransformPaths(inputPath: string, outputPath: string): Promise<void> {
729
+ if (inputPath === outputPath) {
730
+ throw new CliError("unsafe-path", "Derived media output must differ from its immutable input.");
731
+ }
732
+ const input = await lstat(inputPath).catch((error: unknown) => {
733
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
734
+ throw new CliError("not-found", `Media input does not exist: ${inputPath}`);
735
+ }
736
+ throw error;
737
+ });
738
+ if (input.isSymbolicLink() || !input.isFile() || input.size <= 0) {
739
+ throw new CliError("unsafe-path", "Media input must be a physical, nonempty regular file.");
740
+ }
741
+ const parentPath = dirname(outputPath);
742
+ const parent = await lstat(parentPath).catch((error: unknown) => {
743
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
744
+ throw new CliError("not-found", `Media output directory does not exist: ${parentPath}`);
745
+ }
746
+ throw error;
747
+ });
748
+ if (parent.isSymbolicLink() || !parent.isDirectory()) {
749
+ throw new CliError("unsafe-path", "Media output directory must be a physical directory.");
750
+ }
751
+ const [physicalInput, physicalOutputParent] = await Promise.all([
752
+ realpath(inputPath),
753
+ realpath(parentPath),
754
+ ]);
755
+ if (physicalInput === join(physicalOutputParent, basename(outputPath))) {
756
+ throw new CliError("unsafe-path", "Derived media output resolves to its immutable input.");
757
+ }
758
+ try {
759
+ const output = await lstat(outputPath);
760
+ if (output.dev === input.dev && output.ino === input.ino) {
761
+ throw new CliError("unsafe-path", "Derived media output aliases its immutable input.");
762
+ }
763
+ throw new CliError("conflict", `Derived media output already exists: ${outputPath}`);
764
+ } catch (error) {
765
+ if (error instanceof CliError) throw error;
766
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
767
+ }
768
+ }
769
+
770
+ export class LocalMediaEffectsService {
771
+ readonly #ffmpeg: string;
772
+ readonly #runner: ProcessRunner;
773
+
774
+ constructor(options: { readonly ffmpeg: string; readonly runner: ProcessRunner }) {
775
+ validateArgument(options.ffmpeg, "FFmpeg executable");
776
+ this.#ffmpeg = options.ffmpeg;
777
+ this.#runner = options.runner;
778
+ }
779
+
780
+ buildAudio(options: {
781
+ readonly inputPath: string;
782
+ readonly outputPath: string;
783
+ readonly transform: unknown;
784
+ }): BuiltAudioEffectsInvocation {
785
+ return buildAudioEffectsInvocation({ ...options, ffmpeg: this.#ffmpeg });
786
+ }
787
+
788
+ buildColor(options: {
789
+ readonly inputPath: string;
790
+ readonly outputPath: string;
791
+ readonly transform: unknown;
792
+ }): BuiltColorGradeInvocation {
793
+ return buildColorGradeInvocation({ ...options, ffmpeg: this.#ffmpeg });
794
+ }
795
+
796
+ async renderAudio(options: {
797
+ readonly expectedInput: ExpectedLocalMediaInput;
798
+ readonly inputPath: string;
799
+ readonly outputPath: string;
800
+ readonly transform: unknown;
801
+ }): Promise<LocalMediaTransformResult<AudioEffectsTransformV1>> {
802
+ return await runStandaloneOperation(this.renderAudioEffect(options));
803
+ }
804
+
805
+ renderAudioEffect(options: {
806
+ readonly expectedInput: ExpectedLocalMediaInput;
807
+ readonly inputPath: string;
808
+ readonly outputPath: string;
809
+ readonly transform: unknown;
810
+ }): Effect.Effect<LocalMediaTransformResult<AudioEffectsTransformV1>, OperationEffectFailure> {
811
+ return this.#renderEffect(options, paths => {
812
+ const built = this.buildAudio({ ...options, ...paths });
813
+ return { ...built, failureLabel: "FFmpeg audio-effects render failed" };
814
+ });
815
+ }
816
+
817
+ async renderColor(options: {
818
+ readonly expectedInput: ExpectedLocalMediaInput;
819
+ readonly inputPath: string;
820
+ readonly outputPath: string;
821
+ readonly transform: unknown;
822
+ }): Promise<LocalMediaTransformResult<ColorGradeTransformV1>> {
823
+ return await runStandaloneOperation(this.renderColorEffect(options));
824
+ }
825
+
826
+ renderColorEffect(options: {
827
+ readonly expectedInput: ExpectedLocalMediaInput;
828
+ readonly inputPath: string;
829
+ readonly outputPath: string;
830
+ readonly transform: unknown;
831
+ }): Effect.Effect<LocalMediaTransformResult<ColorGradeTransformV1>, OperationEffectFailure> {
832
+ return this.#renderEffect(options, paths => {
833
+ const built = this.buildColor({ ...options, ...paths });
834
+ return { ...built, filterGraph: built.filter, failureLabel: "FFmpeg color-grade render failed" };
835
+ });
836
+ }
837
+
838
+ #renderEffect<Transform>(
839
+ options: {
840
+ readonly expectedInput: ExpectedLocalMediaInput;
841
+ readonly inputPath: string;
842
+ readonly outputPath: string;
843
+ },
844
+ build: (paths: { readonly inputPath: string; readonly outputPath: string }) => {
845
+ readonly argv: readonly [string, ...string[]];
846
+ readonly failureLabel: string;
847
+ readonly filterGraph: string;
848
+ readonly transform: Transform;
849
+ },
850
+ ): Effect.Effect<LocalMediaTransformResult<Transform>, OperationEffectFailure> {
851
+ const runner = this.#runner;
852
+ return Effect.gen(function*() {
853
+ const { inputPath, outputPath } = yield* operationValidation("input", () => ({
854
+ inputPath: resolve(options.inputPath),
855
+ outputPath: resolve(options.outputPath),
856
+ }));
857
+ yield* Effect.uninterruptible(operationBoundary("media", () => assertSafeTransformPaths(inputPath, outputPath)));
858
+ return yield* operationResource(
859
+ operationBoundary("media", () => openLocalMediaInput(inputPath, options.expectedInput)),
860
+ opened => Effect.gen(function*() {
861
+ const pinned = yield* pinOpenedLocalMediaInput(opened, options.expectedInput, runner);
862
+ const built = yield* operationValidation("input", () => build({ inputPath: pinned.processPath, outputPath }));
863
+ const rendered = yield* executeAtomicRenderEffect({
864
+ argv: built.argv,
865
+ failureLabel: built.failureLabel,
866
+ finalOutputPath: outputPath,
867
+ maximumOutputBytes: MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES,
868
+ requireFreshOutput: true,
869
+ runner: pinned.runner,
870
+ timeoutMs: LOCAL_MEDIA_EFFECT_TIMEOUT_MS,
871
+ }, {
872
+ prepare: () => operationBoundary("publication", pinned.assertUnchanged),
873
+ });
874
+ return {
875
+ ...rendered.output,
876
+ filterGraph: built.filterGraph,
877
+ outputPath,
878
+ transform: built.transform,
879
+ };
880
+ }),
881
+ opened => operationBoundary("cleanup", () => opened.handle.close()),
882
+ );
883
+ }).pipe(Effect.provide(AtomicRenderPlatformLive));
884
+ }
885
+ }