@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,517 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, open, realpath, unlink } from "node:fs/promises";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
+
6
+ import sharp from "sharp";
7
+ import { z } from "zod";
8
+
9
+ import type { ApplicationContext } from "../application/context";
10
+ import { DirectingEndpointSchema, directingPtsTimeUs, type DirectingEndpoint } from "../application/directing-contract";
11
+ import { GatewayMediaSourceReferenceSchema, type GatewayMediaSourceReference } from "../application/gateway-port";
12
+ import { loadRepositoryMedia } from "../application/operations/media/shared";
13
+ import { ProjectAssetV1Schema, ProjectEditPlanV1Schema, RepositoryRelativePathSchema, Sha256Schema, VideoProjectV1Schema, type ProjectAssetV1 } from "../contracts";
14
+ import { canonicalJson, sha256Hex } from "../core/canonical-json";
15
+ import { createDefaultProjectEditPlan } from "../core/project-plan";
16
+ import { createNodeBundleFileSystem } from "../core/storage";
17
+ import { CliError } from "./errors";
18
+ import { parseMediaProbe, type ProbedMedia } from "./media-ingest";
19
+ import { withMutationLock } from "./mutation-lock";
20
+ import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
21
+ import { resolveVerifiedProjectMedia } from "./project-media-integrity";
22
+
23
+ const LIMITS = { videoBytes: 512 * 1024 * 1024, imageBytes: 30 * 1024 * 1024, probeBytes: 8 * 1024 * 1024, dimension: 4096, frames: 4000, durationUs: 60_000_000 } as const;
24
+ const INPUT_ARGUMENTS = ["-protocol_whitelist", "file", "-format_whitelist", "mov", "-enable_drefs", "0", "-use_absolute_path", "0", "-max_pixels", String(LIMITS.dimension ** 2), "-threads", "2"] as const;
25
+ const artifactSchema = z.strictObject({ path: RepositoryRelativePathSchema, bytes: z.number().int().safe().positive(), sha256: Sha256Schema });
26
+ const endpointSchema = DirectingEndpointSchema.refine(endpoint => endpoint.frameIndex < LIMITS.frames, "Directing endpoint exceeds the 4000-frame media bound.");
27
+ export type { DirectingEndpoint } from "../application/directing-contract";
28
+ const colorSchema = z.strictObject({
29
+ policy: z.enum(["bt709-to-srgb-v1", "untagged-8bit-yuv-assume-bt709-v1", "rgb-srgb-v1"]),
30
+ observed: z.strictObject({ pixelFormat: z.string(), range: z.string().nullable(), transfer: z.string().nullable(), primaries: z.string().nullable(), matrix: z.string().nullable() }),
31
+ assumptions: z.array(z.enum(["bt709-transfer", "bt709-primaries", "bt709-matrix", "limited-range", "srgb-transfer"])).max(5),
32
+ });
33
+ type ColorInterpretation = z.infer<typeof colorSchema>;
34
+ const endpointReceiptSchema = z.strictObject({
35
+ kind: z.literal("slopcamera.directing-endpoint"), schemaVersion: z.literal(1), requestSha256: Sha256Schema,
36
+ position: z.enum(["first", "last"]), endpoint: endpointSchema,
37
+ color: colorSchema,
38
+ ffmpegVersion: z.string().min(1).max(512), ffprobeVersion: z.string().min(1).max(512),
39
+ });
40
+ const clipSchema = z.strictObject({
41
+ shotId: z.string().min(1).max(128), attemptId: z.string().min(1).max(128),
42
+ source: GatewayMediaSourceReferenceSchema, durationUs: z.number().int().positive().max(LIMITS.durationUs),
43
+ });
44
+ const assemblyInputSchema = z.strictObject({
45
+ id: z.string().min(1).max(128), title: z.string().trim().min(1).max(512),
46
+ clips: z.array(clipSchema).min(1).max(128), recipeSha256: Sha256Schema,
47
+ }).superRefine((input, context) => {
48
+ if (new Set(input.clips.map(clip => clip.shotId)).size !== input.clips.length) context.addIssue({ code: "custom", message: "Assembly shot IDs must be unique." });
49
+ });
50
+ export type DirectingAssemblyInput = z.input<typeof assemblyInputSchema>;
51
+ export interface DirectingAssemblyResult {
52
+ readonly projectId: string;
53
+ readonly projectPath: string;
54
+ readonly receipt: z.infer<typeof artifactSchema>;
55
+ }
56
+ const ASSEMBLY_TIMING_POLICY = "video-span-floor-cumulative-v1" as const;
57
+ const timingRangeSchema = z.strictObject({ startUs: z.number().int().safe().nonnegative(), endUs: z.number().int().safe().positive() }).refine(range => range.endUs > range.startUs);
58
+ const assemblyTimingSchema = z.strictObject({
59
+ shotId: z.string(), attemptId: z.string(), sourceSha256: Sha256Schema, sourceDurationUs: z.number().int().positive(),
60
+ videoAssetRange: timingRangeSchema, videoDurationSeconds: z.strictObject({ numerator: z.string().regex(/^[1-9]\d*$/u), denominator: z.number().int().positive() }),
61
+ projectRange: timingRangeSchema,
62
+ audio: z.array(z.strictObject({ streamIndex: z.number().int().nonnegative(), sourceAssetRange: timingRangeSchema, selectedAssetRange: timingRangeSchema.nullable(), trimmedBeforeUs: z.number().int().nonnegative(), trimmedAfterUs: z.number().int().nonnegative() })).max(7),
63
+ });
64
+ const assemblyIntentSchema = z.strictObject({
65
+ kind: z.literal("slopcamera.directing-assembly-intent"), schemaVersion: z.literal(2), requestSha256: Sha256Schema,
66
+ input: assemblyInputSchema, project: VideoProjectV1Schema, plan: ProjectEditPlanV1Schema,
67
+ colors: z.array(z.strictObject({ shotId: z.string(), sourceSha256: Sha256Schema, interpretation: colorSchema })).min(1).max(128),
68
+ normalizations: z.array(artifactSchema).max(128),
69
+ timingPolicy: z.literal(ASSEMBLY_TIMING_POLICY), timing: z.array(assemblyTimingSchema).min(1).max(128),
70
+ });
71
+ const normalizationSchema = z.strictObject({
72
+ kind: z.literal("slopcamera.directing-rgb-normalization"), schemaVersion: z.literal(1), requestSha256: Sha256Schema,
73
+ input: GatewayMediaSourceReferenceSchema, output: GatewayMediaSourceReferenceSchema, color: colorSchema,
74
+ codec: z.literal("libx264rgb-lossless-v1"), ffmpegVersion: z.string(), frameCount: z.number().int().positive().max(LIMITS.frames),
75
+ });
76
+ const frameSchema = z.object({
77
+ media_type: z.string(), stream_index: z.number().int().nonnegative(),
78
+ width: z.number().int().positive().optional(), height: z.number().int().positive().optional(),
79
+ best_effort_timestamp: z.number().int().safe().optional(),
80
+ duration: z.number().int().positive().optional(), pkt_duration: z.number().int().positive().optional(),
81
+ pix_fmt: z.string().optional(), color_range: z.string().optional(), color_transfer: z.string().optional(), color_primaries: z.string().optional(), color_space: z.string().optional(),
82
+ side_data_list: z.array(z.object({ side_data_type: z.string() })).max(16).optional(),
83
+ });
84
+ const streamSchema = z.object({
85
+ index: z.number().int().nonnegative(), codec_type: z.string(), codec_name: z.string(), width: z.number().int().positive().optional(), height: z.number().int().positive().optional(),
86
+ avg_frame_rate: z.string().optional(), r_frame_rate: z.string().optional(), duration: z.string().optional(), start_time: z.string().optional(),
87
+ channels: z.number().int().positive().optional(), sample_rate: z.string().optional(),
88
+ disposition: z.object({ attached_pic: z.number().int().min(0).max(1).optional(), still_image: z.number().int().min(0).max(1).optional(), timed_thumbnails: z.number().int().min(0).max(1).optional() }).optional(),
89
+ tags: z.object({ DURATION: z.string().optional(), duration: z.string().optional() }).optional(),
90
+ pix_fmt: z.string().optional(), time_base: z.string().optional(), color_range: z.string().optional(), color_transfer: z.string().optional(), color_primaries: z.string().optional(), color_space: z.string().optional(),
91
+ side_data_list: z.array(z.object({ side_data_type: z.string() })).max(16).optional(),
92
+ });
93
+ const probeSchema = z.object({ streams: z.array(streamSchema).min(1).max(8), frames: z.array(frameSchema).min(1).max(16_000), format: z.object({ duration: z.string().optional(), format_name: z.string(), start_time: z.string().optional() }) });
94
+
95
+ function fail(message: string): never { throw new CliError("invalid-data", message); }
96
+ async function active(application: ApplicationContext, signal: AbortSignal): Promise<void> {
97
+ if (signal.aborted) throw new CliError("cancelled", "Directing media operation was cancelled.");
98
+ await application.hostResourceLease?.assertOwned();
99
+ }
100
+ function repoPath(application: ApplicationContext, path: string): string {
101
+ return RepositoryRelativePathSchema.parse(relative(application.paths.repositoryRoot, path));
102
+ }
103
+ async function directory(application: ApplicationContext, requested: string): Promise<string> {
104
+ return await ensurePhysicalPrivateDirectoryWithin(await realpath(application.paths.repositoryRoot), requested);
105
+ }
106
+ async function mediaRoot(application: ApplicationContext): Promise<string> {
107
+ return await directory(application, repoPath(application, join(dirname(application.paths.artifactRoot), "generated", "directing-media")));
108
+ }
109
+ async function fileBytes(path: string, maximumBytes: number, signal: AbortSignal): Promise<Uint8Array> {
110
+ if (signal.aborted) throw new CliError("cancelled", "Directing media read was cancelled.");
111
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
112
+ try {
113
+ const before = await handle.stat({ bigint: true });
114
+ if (!before.isFile() || before.size < 1n || before.size > BigInt(maximumBytes)) fail("Directing media must be one bounded physical regular file.");
115
+ const bytes = new Uint8Array(Number(before.size));
116
+ let offset = 0;
117
+ while (offset < bytes.length) {
118
+ if (signal.aborted) throw new CliError("cancelled", "Directing media read was cancelled.");
119
+ const read = await handle.read(bytes, offset, Math.min(256 * 1024, bytes.length - offset), offset);
120
+ if (read.bytesRead === 0) throw new CliError("conflict", "Directing media changed during its read.");
121
+ offset += read.bytesRead;
122
+ }
123
+ const after = await handle.stat({ bigint: true });
124
+ const leaf = await lstat(path, { bigint: true });
125
+ if (!leaf.isFile() || leaf.isSymbolicLink() || leaf.dev !== before.dev || leaf.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) throw new CliError("conflict", "Directing media changed during its read.");
126
+ return bytes;
127
+ } finally { await handle.close(); }
128
+ }
129
+ async function removeOwned(path: string, identity: { readonly dev: number; readonly ino: number }): Promise<void> {
130
+ const current = await lstat(path);
131
+ if (!current.isFile() || current.isSymbolicLink() || current.dev !== identity.dev || current.ino !== identity.ino) throw new CliError("conflict", "Directing temporary file changed identity; retained for inspection.");
132
+ await unlink(path);
133
+ }
134
+ async function withNativeOutput<Native, T>(root: string, prefix: string, suffix: string, run: (path: string) => Promise<Native>, body: (path: string, native: Native) => Promise<T>): Promise<T> {
135
+ const path = join(root, `.${prefix}-${randomUUID()}.${suffix}`), parentIdentity = await lstat(root);
136
+ let identity: { readonly dev: number; readonly ino: number } | undefined;
137
+ let outcome: { success: true; value: T } | { success: false; error: unknown };
138
+ try {
139
+ let native: { success: true; value: Native } | { success: false; error: unknown };
140
+ try { native = { success: true, value: await run(path) }; }
141
+ catch (error) { native = { success: false, error }; }
142
+ // The native child has settled. Capture its output before any downstream read or publication.
143
+ try { identity = await lstat(path); }
144
+ catch (error) {
145
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
146
+ if (!native.success) throw new AggregateError([native.error, error], "Directing native output failed and its identity could not be inspected.", { cause: native.error });
147
+ throw error;
148
+ }
149
+ }
150
+ if (!native.success) throw native.error;
151
+ outcome = { success: true, value: await body(path, native.value) };
152
+ }
153
+ catch (error) { outcome = { success: false, error }; }
154
+ try {
155
+ const parent = await lstat(root);
156
+ if (!parent.isDirectory() || parent.isSymbolicLink() || parent.dev !== parentIdentity.dev || parent.ino !== parentIdentity.ino) throw new CliError("conflict", "Directing decoder output parent changed; retaining uncertain temporary output.");
157
+ if (identity !== undefined) await removeOwned(path, identity);
158
+ else {
159
+ let unexpected = false;
160
+ try { await lstat(path); unexpected = true; }
161
+ catch (error) { if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error; }
162
+ if (unexpected) throw new CliError("conflict", "Unowned directing temporary output appeared after native completion; retained for inspection.");
163
+ }
164
+ } catch (cleanupError) {
165
+ if (!outcome.success) throw new AggregateError([outcome.error, cleanupError], "Directing endpoint failed and temporary cleanup also failed.", { cause: outcome.error });
166
+ throw cleanupError;
167
+ }
168
+ if (!outcome.success) throw outcome.error;
169
+ return outcome.value;
170
+ }
171
+ async function publishBytes(application: ApplicationContext, bytes: Uint8Array, extension: string, signal: AbortSignal): Promise<z.infer<typeof artifactSchema>> {
172
+ await active(application, signal);
173
+ const root = await mediaRoot(application), sha256 = createHash("sha256").update(bytes).digest("hex");
174
+ const temporary = join(root, `.media-${randomUUID()}`), path = join(root, `${sha256}.${extension}`);
175
+ const handle = await open(temporary, "wx", 0o600);
176
+ const identity = await handle.stat();
177
+ try {
178
+ await handle.writeFile(bytes); await handle.sync(); await handle.close();
179
+ const artifact = artifactSchema.parse({ path: repoPath(application, path), bytes: bytes.length, sha256 });
180
+ const fs = createNodeBundleFileSystem(application.paths.repositoryRoot), beforeCopy = await lstat(temporary, { bigint: true });
181
+ try { await fs.copyFileNoReplace!(repoPath(application, temporary), artifact.path, artifact, async () => await active(application, signal)); }
182
+ catch (error) {
183
+ if (!(error instanceof Error) || !error.message.includes("Immutable bundle copy source changed or failed verification")) throw error;
184
+ let diagnostic: string;
185
+ try {
186
+ const observed = await fs.inspectFile!(repoPath(application, temporary), bytes.length), afterCopy = await lstat(temporary, { bigint: true });
187
+ diagnostic = `sourceBytes=${observed.bytes}/${artifact.bytes}, sourceSha256Matches=${observed.sha256 === artifact.sha256}, sourceInodeMatches=${beforeCopy.dev === afterCopy.dev && beforeCopy.ino === afterCopy.ino}, sourceMtimeMatches=${beforeCopy.mtimeNs === afterCopy.mtimeNs}, sourceCtimeMatches=${beforeCopy.ctimeNs === afterCopy.ctimeNs}`;
188
+ } catch (inspectionError) { throw new AggregateError([error, inspectionError], "Directing immutable publication and source inspection failed.", { cause: error }); }
189
+ const failure = new CliError("conflict", `Directing immutable publication failed: ${diagnostic}.`);
190
+ failure.cause = error;
191
+ throw failure;
192
+ }
193
+ return artifact;
194
+ } finally {
195
+ await handle.close();
196
+ await removeOwned(temporary, identity);
197
+ }
198
+ }
199
+ async function publishJson(application: ApplicationContext, path: string, value: unknown, signal: AbortSignal, assertCustody?: () => Promise<void>): Promise<z.infer<typeof artifactSchema>> {
200
+ const text = `${canonicalJson(value)}\n`, artifact = artifactSchema.parse({ path: repoPath(application, path), bytes: Buffer.byteLength(text), sha256: sha256Hex(text) });
201
+ const fs = createNodeBundleFileSystem(application.paths.repositoryRoot);
202
+ await fs.writeTextNoReplace!(artifact.path, text, async () => { await active(application, signal); await assertCustody?.(); });
203
+ await resolveVerifiedProjectMedia({ repositoryRoot: application.paths.repositoryRoot, path: artifact.path, expected: artifact, label: "Directing retained document" });
204
+ await active(application, signal); await assertCustody?.();
205
+ return artifact;
206
+ }
207
+ async function maybeJson(path: string): Promise<unknown | undefined> {
208
+ try { return JSON.parse(await createNodeBundleFileSystem(dirname(path)).readText(basename(path), 8 * 1024 * 1024)) as unknown; }
209
+ catch (error) { if (error instanceof Error && "code" in error && error.code === "ENOENT") return undefined; throw error; }
210
+ }
211
+ async function capability(application: ApplicationContext, name: "ffmpeg" | "ffprobe"): Promise<{ command: string; version: string }> {
212
+ const found = await application.capability(name);
213
+ if (!found.available || found.command === undefined || found.version === undefined) throw new CliError("unavailable", `Directing media requires a versioned ${name} capability.`);
214
+ return { command: found.command, version: found.version };
215
+ }
216
+ async function verifySource(application: ApplicationContext, source: GatewayMediaSourceReference, signal: AbortSignal): Promise<string> {
217
+ await active(application, signal);
218
+ return await resolveVerifiedProjectMedia({ repositoryRoot: application.paths.repositoryRoot, path: source.path, expected: source, label: "Directing source" });
219
+ }
220
+ async function retainVideo(application: ApplicationContext, input: GatewayMediaSourceReference, signal: AbortSignal): Promise<GatewayMediaSourceReference> {
221
+ const source = GatewayMediaSourceReferenceSchema.parse(input);
222
+ if (source.mediaType !== "video/mp4" && source.mediaType !== "video/quicktime") fail("Directing endpoints require self-contained MP4 or QuickTime video.");
223
+ const loaded = await loadRepositoryMedia(application, { path: source.path, bytes: source.bytes, sha256: source.sha256 }, signal, LIMITS.videoBytes);
224
+ if (loaded.data.length < 12 || Buffer.from(loaded.data.subarray(4, 8)).toString("ascii") !== "ftyp") fail("Directing video must be self-contained ISO-BMFF media.");
225
+ const artifact = await publishBytes(application, loaded.data, "mp4", signal);
226
+ return GatewayMediaSourceReferenceSchema.parse({ ...artifact, mediaType: source.mediaType });
227
+ }
228
+
229
+ /** Imports exactly the caller-named still; never uploads it or reads neighboring files. */
230
+ export async function importDirectingAnchor(application: ApplicationContext, path: string, signal: AbortSignal): Promise<GatewayMediaSourceReference> {
231
+ await active(application, signal);
232
+ const resolved = resolve(application.paths.repositoryRoot, path);
233
+ if (await realpath(dirname(resolved)) !== dirname(resolved)) throw new CliError("unsafe-path", "Directing anchors require physical parent directories.");
234
+ const bytes = await fileBytes(resolved, LIMITS.imageBytes, signal);
235
+ const metadata = await sharp(bytes, { limitInputPixels: LIMITS.dimension ** 2, failOn: "error", animated: true }).metadata();
236
+ const extension = metadata.format === "jpeg" ? "jpg" : metadata.format;
237
+ if (!extension || !["png", "jpg", "webp"].includes(extension) || metadata.pages !== undefined && metadata.pages !== 1 || !metadata.width || !metadata.height || metadata.width > LIMITS.dimension || metadata.height > LIMITS.dimension || metadata.orientation !== undefined && metadata.orientation !== 1) fail("Directing anchors require a single unrotated PNG, JPEG, or WebP image no larger than 4096 pixels per side.");
238
+ await sharp(bytes, { limitInputPixels: LIMITS.dimension ** 2, failOn: "error" }).raw().toBuffer();
239
+ const artifact = await publishBytes(application, bytes, extension, signal);
240
+ return GatewayMediaSourceReferenceSchema.parse({ ...artifact, mediaType: metadata.format === "jpeg" ? "image/jpeg" : `image/${metadata.format}`, facts: { width: metadata.width, height: metadata.height } });
241
+ }
242
+
243
+ interface ProbedVideo {
244
+ readonly media: ProbedMedia;
245
+ readonly stream: z.infer<typeof streamSchema>;
246
+ readonly frames: readonly z.infer<typeof frameSchema>[];
247
+ readonly numerator: number;
248
+ readonly denominator: number;
249
+ readonly colorFilter: string;
250
+ readonly color: ColorInterpretation;
251
+ }
252
+ async function probeVideo(application: ApplicationContext, source: GatewayMediaSourceReference, command: string, signal: AbortSignal): Promise<ProbedVideo> {
253
+ const path = await verifySource(application, source, signal);
254
+ const result = await application.runner.run([command, "-v", "error", ...INPUT_ARGUMENTS, "-show_streams", "-show_format", "-show_frames", "-show_entries", "frame=media_type,stream_index,width,height,pix_fmt,color_range,color_transfer,color_primaries,color_space,best_effort_timestamp,duration,pkt_duration:frame_side_data=side_data_type", "-of", "json", path], { abortSignal: signal, timeoutMs: 120_000, maxOutputBytes: LIMITS.probeBytes });
255
+ await verifySource(application, source, signal);
256
+ if (result.exitCode !== 0) throw new CliError("subprocess", "FFprobe could not inspect directing video.");
257
+ const parsed = probeSchema.parse(JSON.parse(result.stdout) as unknown);
258
+ const media = parseMediaProbe(JSON.stringify({ format: parsed.format, streams: parsed.streams.map(({ pix_fmt: _pixels, time_base: _base, color_range: _range, color_transfer: _transfer, color_primaries: _primaries, color_space: _matrix, side_data_list: _sideData, ...stream }) => stream) }));
259
+ const videos = media.streams.filter(stream => stream.codec_type === "video");
260
+ if (videos.length !== 1 || media.durationUs > LIMITS.durationUs) fail("Directing clips require exactly one playable video stream and at most 60 seconds.");
261
+ const stream = parsed.streams.find(candidate => candidate.index === videos[0]!.index)!;
262
+ if (!stream.width || !stream.height || stream.width > LIMITS.dimension || stream.height > LIMITS.dimension) fail("Directing video dimensions exceed 4096 pixels per side.");
263
+ const base = /^([1-9]\d{0,8})\/([1-9]\d{0,8})$/u.exec(stream.time_base ?? "");
264
+ if (base === null) fail("Directing video requires an exact bounded rational time base.");
265
+ const frames = parsed.frames.filter(frame => frame.media_type === "video" && frame.stream_index === stream.index);
266
+ if (frames.length < 1 || frames.length > LIMITS.frames || frames.some((frame, index) => frame.best_effort_timestamp === undefined || index > 0 && frame.best_effort_timestamp <= frames[index - 1]!.best_effort_timestamp!)) fail("Directing video requires 1–4000 strictly ordered presentation timestamps.");
267
+ if (frames.some(frame => frame.width !== stream.width || frame.height !== stream.height)) fail("Directing video must retain fixed dimensions for every decoded frame.");
268
+ const final = frames.at(-1)!, finalDuration = final.duration ?? final.pkt_duration;
269
+ if (finalDuration === undefined) fail("Directing video must retain the last frame's presentation duration.");
270
+ const coverage = (BigInt(final.best_effort_timestamp!) + BigInt(finalDuration) - BigInt(frames[0]!.best_effort_timestamp!)) * BigInt(base[1]!) * 1_000_000n;
271
+ const videoDurationUs = videos[0]!.assetRange.endUs - videos[0]!.assetRange.startUs;
272
+ if (coverage <= 0n || coverage > BigInt(LIMITS.durationUs) * BigInt(base[2]!) || Math.abs(Number(coverage) / Number(base[2]) - videoDurationUs) > 1) fail(`Directing video presentation coverage (${Number(coverage) / Number(base[2])}us) differs from its bounded media duration (${videoDurationUs}us).`);
273
+ if ([...(stream.side_data_list ?? []), ...frames.flatMap(frame => frame.side_data_list ?? [])].some(side => /mastering display|content light|dolby|dovi|hdr|ambient viewing environment/iu.test(side.side_data_type))) fail("Directing video with HDR metadata requires a separately qualified tone-mapping policy.");
274
+ const missing = (value: string | undefined) => value === undefined || value === "unknown" || value === "unspecified";
275
+ const observed = { pixelFormat: stream.pix_fmt ?? "unknown", range: stream.color_range ?? null, transfer: stream.color_transfer ?? null, primaries: stream.color_primaries ?? null, matrix: stream.color_space ?? null };
276
+ const assumptions: ColorInterpretation["assumptions"] = [];
277
+ let colorFilter: string, policy: ColorInterpretation["policy"];
278
+ if (["yuv420p", "yuv422p", "yuv444p"].includes(stream.pix_fmt ?? "")) {
279
+ if ([stream, ...frames].some(value => [value.color_transfer, value.color_primaries, value.color_space].some(tag => !missing(tag) && tag !== "bt709") || value.pix_fmt !== undefined && value.pix_fmt !== stream.pix_fmt)) fail("Directing YUV video requires 8-bit BT.709-compatible SDR metadata; conflicting, HDR, and changing formats are rejected.");
280
+ if (!missing(stream.color_range) && stream.color_range !== "tv" && stream.color_range !== "pc") fail("Directing YUV video has an unsupported color range.");
281
+ if (frames.some(frame => !missing(frame.color_range) && frame.color_range !== (stream.color_range === "pc" ? "pc" : "tv"))) fail("Directing YUV frame range conflicts with the retained source interpretation.");
282
+ if (missing(stream.color_transfer)) assumptions.push("bt709-transfer");
283
+ if (missing(stream.color_primaries)) assumptions.push("bt709-primaries");
284
+ if (missing(stream.color_space)) assumptions.push("bt709-matrix");
285
+ if (missing(stream.color_range)) assumptions.push("limited-range");
286
+ policy = assumptions.length === 0 ? "bt709-to-srgb-v1" : "untagged-8bit-yuv-assume-bt709-v1";
287
+ // Missing SDR tags are a retained interpretation, never evidence that HDR has been tone mapped.
288
+ colorFilter = `colorspace=ispace=bt709:itrc=bt709:iprimaries=bt709:irange=${stream.color_range === "pc" ? "pc" : "tv"}:space=bt709:trc=iec61966-2-1:primaries=bt709:range=pc:format=yuv444p,format=rgb24`;
289
+ } else {
290
+ if (!["rgb24", "bgr24", "gbrp"].includes(stream.pix_fmt ?? "") || [stream, ...frames].some(value => value.pix_fmt !== undefined && value.pix_fmt !== stream.pix_fmt || !missing(value.color_transfer) && value.color_transfer !== "iec61966-2-1" || !missing(value.color_primaries) && value.color_primaries !== "bt709" || !missing(value.color_space) && value.color_space !== "gbr")) fail("Directing RGB video requires fixed opaque qualified 8-bit SDR sRGB pixels.");
291
+ if ([stream, ...frames].some(value => !missing(value.color_range) && value.color_range !== "pc")) fail("Directing RGB video requires consistent full-range pixels.");
292
+ colorFilter = "format=rgb24";
293
+ policy = "rgb-srgb-v1";
294
+ if (missing(stream.color_transfer)) assumptions.push("srgb-transfer");
295
+ }
296
+ return { media, stream, frames, numerator: Number(base[1]), denominator: Number(base[2]), colorFilter, color: colorSchema.parse({ policy, observed, assumptions }) };
297
+ }
298
+
299
+ /** Reads exact retained bytes without publishing; container coverage can exceed the native video span. */
300
+ export async function inspectDirectingClipDuration(application: ApplicationContext, input: GatewayMediaSourceReference, signal: AbortSignal): Promise<number> {
301
+ const source = GatewayMediaSourceReferenceSchema.parse(input);
302
+ if (source.mediaType !== "video/mp4" && source.mediaType !== "video/quicktime") fail("Clip inspection requires self-contained MP4 or QuickTime video.");
303
+ await active(application, signal);
304
+ const ffprobe = await capability(application, "ffprobe");
305
+ return (await probeVideo(application, source, ffprobe.command, signal)).media.durationUs;
306
+ }
307
+
308
+ function assertNormalizedClock(original: ProbedVideo, normalized: ProbedVideo): void {
309
+ if (normalized.stream.codec_name !== "h264" || normalized.stream.pix_fmt !== "gbrp" || normalized.stream.color_transfer !== "iec61966-2-1" || normalized.stream.color_primaries !== "bt709" || normalized.stream.color_space !== "gbr" || normalized.stream.color_range !== "pc") fail("Lossless RGB derivative does not declare the required sRGB interpretation.");
310
+ if (normalized.frames.length !== original.frames.length || normalized.stream.width !== original.stream.width || normalized.stream.height !== original.stream.height || Math.abs(normalized.media.durationUs - original.media.durationUs) > 1) fail("Lossless RGB derivative changed media geometry, duration, or frame count.");
311
+ for (const [index, frame] of normalized.frames.entries()) {
312
+ if (BigInt(frame.best_effort_timestamp!) * BigInt(normalized.numerator) * BigInt(original.denominator) !== BigInt(original.frames[index]!.best_effort_timestamp!) * BigInt(original.numerator) * BigInt(normalized.denominator)) fail("Lossless RGB derivative changed a native presentation timestamp.");
313
+ }
314
+ const videoRange = (video: ProbedVideo) => video.media.streams.filter(stream => stream.codec_type === "video").map(stream => ({ assetRange: stream.assetRange, fileRange: stream.fileRange }));
315
+ if (canonicalJson(videoRange(normalized)) !== canonicalJson(videoRange(original))) fail("Lossless RGB derivative changed the video presentation range.");
316
+ const finalDuration = (video: ProbedVideo) => BigInt(video.frames.at(-1)!.duration ?? video.frames.at(-1)!.pkt_duration!);
317
+ if (finalDuration(normalized) * BigInt(normalized.numerator) * BigInt(original.denominator) !== finalDuration(original) * BigInt(original.numerator) * BigInt(normalized.denominator)) fail("Lossless RGB derivative changed the final video frame duration.");
318
+ const audio = (video: ProbedVideo) => video.media.streams.filter(stream => stream.codec_type === "audio").map(stream => ({ codec: stream.codec_name, channels: stream.channels, sampleRate: stream.sample_rate, assetRange: stream.assetRange, fileRange: stream.fileRange }));
319
+ if (canonicalJson(audio(normalized)) !== canonicalJson(audio(original))) fail("Lossless RGB derivative changed copied audio streams or their clock.");
320
+ }
321
+
322
+ async function normalizeForAssembly(application: ApplicationContext, source: GatewayMediaSourceReference, original: ProbedVideo, signal: AbortSignal, requireRetainedReceipt = false): Promise<{ source: GatewayMediaSourceReference; probe: ProbedVideo; receipt?: z.infer<typeof artifactSchema> }> {
323
+ if (original.color.policy === "rgb-srgb-v1") return { source, probe: original };
324
+ const root = await mediaRoot(application), ffmpeg = await capability(application, "ffmpeg"), ffprobe = await capability(application, "ffprobe");
325
+ const requestSha256 = sha256Hex(canonicalJson({ kind: "slopcamera.directing-rgb-normalization", schemaVersion: 1, source, color: original.color, ffmpegVersion: ffmpeg.version, codec: "libx264rgb-lossless-v1" }));
326
+ const receiptPath = join(root, `rgb-${requestSha256}.json`);
327
+ return await withMutationLock(await directory(application, repoPath(application, join(root, `rgb-${requestSha256}`))), { command: "directing RGB normalization", label: "Directing RGB normalization" }, async lease => {
328
+ const previous = await maybeJson(receiptPath);
329
+ if (previous !== undefined) {
330
+ const prior = normalizationSchema.parse(previous);
331
+ if (prior.requestSha256 !== requestSha256 || prior.ffmpegVersion !== ffmpeg.version || canonicalJson(prior.input) !== canonicalJson(source) || canonicalJson(prior.color) !== canonicalJson(original.color)) throw new CliError("conflict", "RGB normalization receipt differs from its source interpretation.");
332
+ const probe = await probeVideo(application, prior.output, ffprobe.command, signal);
333
+ if (prior.frameCount !== probe.frames.length) throw new CliError("conflict", "RGB normalization receipt differs from its decoded frame count.");
334
+ assertNormalizedClock(original, probe);
335
+ const receipt = await publishJson(application, receiptPath, prior, signal, lease.assertOwned);
336
+ return { source: prior.output, probe, receipt };
337
+ }
338
+ if (requireRetainedReceipt) throw new CliError("conflict", "Retained assembly normalization is unavailable for the current toolchain; recovery cannot re-encode media.");
339
+ return await withNativeOutput(root, "rgb", "mp4", async temporary => {
340
+ const path = await verifySource(application, source, signal);
341
+ return await application.runner.run([ffmpeg.command, "-v", "error", "-nostdin", "-copyts", "-filter_threads", "1", ...INPUT_ARGUMENTS, "-i", path,
342
+ "-map", `0:${original.stream.index}`, "-map", "0:a?", "-vf", original.colorFilter, "-fps_mode", "passthrough", "-enc_time_base", `${original.numerator}/${original.denominator}`,
343
+ "-c:v", "libx264rgb", "-preset", "veryfast", "-crf", "0", "-bf", "0", "-threads:v", "2", "-pix_fmt", "rgb24", "-color_range", "pc", "-color_primaries", "bt709", "-color_trc", "iec61966-2-1", "-colorspace", "rgb",
344
+ "-c:a", "copy", "-video_track_timescale", String(original.denominator), "-avoid_negative_ts", "disabled", "-fs", String(LIMITS.videoBytes - 1024 * 1024), "-f", "mp4", "-n", temporary], { abortSignal: signal, timeoutMs: 120_000, maxOutputBytes: 1024 * 1024 });
345
+ }, async (temporary, result) => {
346
+ await verifySource(application, source, signal);
347
+ if (result.exitCode !== 0) throw new CliError("subprocess", "Lossless RGB assembly normalization requires FFmpeg libx264rgb and compatible copied audio.");
348
+ const bytes = await fileBytes(temporary, LIMITS.videoBytes, signal), artifact = await publishBytes(application, bytes, "mp4", signal);
349
+ const output = GatewayMediaSourceReferenceSchema.parse({ ...artifact, mediaType: "video/mp4" });
350
+ const probe = await probeVideo(application, output, ffprobe.command, signal);
351
+ assertNormalizedClock(original, probe);
352
+ const measured = GatewayMediaSourceReferenceSchema.parse({ ...output, facts: { durationSeconds: probe.media.durationUs / 1_000_000, width: probe.stream.width, height: probe.stream.height } });
353
+ const receipt = await publishJson(application, receiptPath, normalizationSchema.parse({ kind: "slopcamera.directing-rgb-normalization", schemaVersion: 1, requestSha256, input: source, output: measured, color: original.color,
354
+ codec: "libx264rgb-lossless-v1", ffmpegVersion: ffmpeg.version, frameCount: probe.frames.length }), signal, lease.assertOwned);
355
+ return { source: measured, probe, receipt };
356
+ });
357
+ });
358
+ }
359
+
360
+ /** Uses decoded frame order and native PTS; never estimates an endpoint from nominal FPS. */
361
+ export async function extractDirectingEndpoint(application: ApplicationContext, input: GatewayMediaSourceReference, position: "first" | "last", signal: AbortSignal): Promise<DirectingEndpoint> {
362
+ const source = GatewayMediaSourceReferenceSchema.parse(input);
363
+ const requestedPosition = z.enum(["first", "last"]).parse(position);
364
+ await verifySource(application, source, signal);
365
+ const requestSha256 = sha256Hex(canonicalJson({ kind: "slopcamera.directing-endpoint-request", schemaVersion: 1, colorPolicy: "sdr-video-color-v1", source: { path: source.path, bytes: source.bytes, sha256: source.sha256, mediaType: source.mediaType }, position: requestedPosition }));
366
+ const root = await mediaRoot(application), receiptPath = join(root, `endpoint-${requestSha256}.json`);
367
+ return await withMutationLock(await directory(application, repoPath(application, join(root, `endpoint-${requestSha256}`))), { command: "directing endpoint", label: "Directing endpoint" }, async lease => {
368
+ const previous = await maybeJson(receiptPath);
369
+ if (previous !== undefined) {
370
+ const receipt = endpointReceiptSchema.parse(previous);
371
+ if (receipt.requestSha256 !== requestSha256 || receipt.position !== requestedPosition || receipt.endpoint.source.sha256 !== source.sha256 || receipt.endpoint.source.path !== source.path || receipt.endpoint.source.bytes !== source.bytes || receipt.endpoint.source.mediaType !== source.mediaType) throw new CliError("conflict", "Directing endpoint receipt differs from its request.");
372
+ await verifySource(application, receipt.endpoint.image, signal);
373
+ await active(application, signal); await lease.assertOwned();
374
+ return receipt.endpoint;
375
+ }
376
+ const retained = await retainVideo(application, source, signal);
377
+ const [ffmpeg, ffprobe] = await Promise.all([capability(application, "ffmpeg"), capability(application, "ffprobe")]);
378
+ const probe = await probeVideo(application, retained, ffprobe.command, signal);
379
+ const frameIndex = requestedPosition === "first" ? 0 : probe.frames.length - 1, value = probe.frames[frameIndex]!.best_effort_timestamp!;
380
+ const temporary = join(root, `.endpoint-${randomUUID()}.png`), sourcePath = await verifySource(application, retained, signal);
381
+ const parentIdentity = await lstat(root);
382
+ let identity: { readonly dev: number; readonly ino: number } | undefined;
383
+ let failed = false, failure: unknown, completed: DirectingEndpoint | undefined;
384
+ try {
385
+ // The runner must settle its native child before this scope releases output custody.
386
+ const result = await application.runner.run([ffmpeg.command, "-hide_banner", "-loglevel", "info", "-nostdin", "-copyts", "-filter_threads", "1", ...INPUT_ARGUMENTS, "-i", sourcePath,
387
+ "-map", `0:${probe.stream.index}`, "-vf", `select=eq(n\\,${frameIndex}),showinfo,${probe.colorFilter}`, "-fps_mode", "passthrough", "-frames:v", "1", "-threads:v", "1", "-an", "-sn", "-dn", "-f", "image2", "-n", temporary], { abortSignal: signal, timeoutMs: 120_000, maxOutputBytes: 1024 * 1024 });
388
+ await verifySource(application, retained, signal);
389
+ await verifySource(application, source, signal);
390
+ if (result.exitCode !== 0) throw new CliError("subprocess", "FFmpeg could not decode the directing endpoint.");
391
+ identity = await lstat(temporary);
392
+ const base = /Parsed_showinfo_[^\n]*config in time_base:\s*(\d+)\/(\d+)/u.exec(result.stderr);
393
+ const pts = /Parsed_showinfo_[^\n]*\bn:\s*0\s+pts:\s*(-?\d+)/u.exec(result.stderr);
394
+ if (!base || !pts || BigInt(pts[1]!) * BigInt(base[1]!) * BigInt(probe.denominator) !== BigInt(value) * BigInt(probe.numerator) * BigInt(base[2]!)) fail("FFmpeg did not decode the exact probed source PTS.");
395
+ const bytes = await fileBytes(temporary, LIMITS.imageBytes, signal);
396
+ const metadata = await sharp(bytes, { limitInputPixels: LIMITS.dimension ** 2, failOn: "error" }).metadata();
397
+ if (metadata.format !== "png" || metadata.width !== probe.stream.width || metadata.height !== probe.stream.height) fail("Directing endpoint dimensions differ from the source.");
398
+ await sharp(bytes, { limitInputPixels: LIMITS.dimension ** 2, failOn: "error" }).raw().toBuffer();
399
+ const artifact = await publishBytes(application, bytes, "png", signal);
400
+ const endpoint = endpointSchema.parse({
401
+ image: { ...artifact, mediaType: "image/png", facts: { width: metadata.width, height: metadata.height } },
402
+ source: { ...source, facts: { durationSeconds: probe.media.durationUs / 1_000_000, width: probe.stream.width, height: probe.stream.height } },
403
+ frameIndex, pts: { value: String(value), timeBaseNumerator: probe.numerator, timeBaseDenominator: probe.denominator },
404
+ timeUs: directingPtsTimeUs({ value: String(value), timeBaseNumerator: probe.numerator, timeBaseDenominator: probe.denominator }),
405
+ });
406
+ await publishJson(application, receiptPath, endpointReceiptSchema.parse({ kind: "slopcamera.directing-endpoint", schemaVersion: 1, requestSha256, position: requestedPosition, endpoint, color: probe.color, ffmpegVersion: ffmpeg.version, ffprobeVersion: ffprobe.version }), signal, lease.assertOwned);
407
+ completed = endpoint;
408
+ } catch (error) { failed = true; failure = error; }
409
+ // Settle cleanup separately so it cannot discard the primary native failure.
410
+ try {
411
+ const parent = await lstat(root);
412
+ if (!parent.isDirectory() || parent.isSymbolicLink() || parent.dev !== parentIdentity.dev || parent.ino !== parentIdentity.ino) throw new CliError("conflict", "Directing decoder output parent changed; retaining uncertain temporary output.");
413
+ if (identity === undefined) {
414
+ try { identity = await lstat(temporary); }
415
+ catch (error) { if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error; }
416
+ }
417
+ if (identity !== undefined) await removeOwned(temporary, identity);
418
+ } catch (cleanupError) {
419
+ if (failed) throw new AggregateError([failure, cleanupError], "Directing endpoint failed and temporary cleanup also failed.", { cause: failure });
420
+ throw cleanupError;
421
+ }
422
+ if (failed) throw failure;
423
+ return completed!;
424
+ });
425
+ }
426
+
427
+ function projectAsset(source: GatewayMediaSourceReference, probe: ProbedMedia, timestamp: string): ProjectAssetV1 {
428
+ const suffix = source.sha256.slice(0, 24);
429
+ const rate = (value: string | undefined) => {
430
+ const parts = /^(\d+)\/(\d+)$/u.exec(value ?? "");
431
+ return parts === null || Number(parts[1]) <= 0 || Number(parts[2]) <= 0 ? undefined : Number(parts[1]) / Number(parts[2]);
432
+ };
433
+ return ProjectAssetV1Schema.parse({
434
+ assetId: `asset_${suffix}`, createdAt: timestamp, durationUs: probe.durationUs, label: `Directed clip ${suffix}`, role: "b-roll",
435
+ source: { kind: "generated", generator: "slopcamera.directing", generatorVersion: "1", sourceSha256: source.sha256 },
436
+ streams: probe.streams.map(stream => ({
437
+ streamId: `stream_${suffix}_${stream.index}`, label: `${stream.codec_type} ${stream.index}`, kind: stream.codec_type,
438
+ ...(stream.codec_type === "video" ? { role: "b-roll", pixelWidth: stream.width, pixelHeight: stream.height,
439
+ frameRate: rate(stream.avg_frame_rate) ?? rate(stream.r_frame_rate) } : { role: "other", channels: stream.channels, sampleRateHz: Number(stream.sample_rate) }),
440
+ segments: [{ path: source.path, bytes: source.bytes, sha256: source.sha256, codec: stream.codec_name, container: probe.container, streamIndex: stream.index, assetRange: stream.assetRange, fileRange: stream.fileRange }],
441
+ })),
442
+ });
443
+ }
444
+
445
+ /** Publishes one ordinary editable project; its initial assembly remains bound to accepted bytes. */
446
+ export async function assembleDirectingClips(application: ApplicationContext, request: DirectingAssemblyInput, signal: AbortSignal): Promise<DirectingAssemblyResult> {
447
+ const input = assemblyInputSchema.parse(request);
448
+ await active(application, signal);
449
+ const requestSha256 = sha256Hex(canonicalJson({ kind: "slopcamera.directing-assembly", schemaVersion: 2, colorPolicy: "sdr-rgb-assembly-v1", timingPolicy: ASSEMBLY_TIMING_POLICY, input }));
450
+ const root = await mediaRoot(application), projectId = `project_directing_${requestSha256.slice(0, 40)}`;
451
+ const projectDirectory = join(application.paths.projectRoot, projectId), intentPath = join(root, `assembly-${requestSha256}.intent.json`);
452
+ return await withMutationLock(await directory(application, repoPath(application, join(root, `assembly-${requestSha256}`))), { command: "directing assembly", label: "Directing assembly" }, async lease => {
453
+ for (const clip of input.clips) await verifySource(application, clip.source, signal);
454
+ const previous = await maybeJson(intentPath);
455
+ const priorIntent = previous === undefined ? undefined : assemblyIntentSchema.parse(previous);
456
+ if (priorIntent !== undefined && (priorIntent.requestSha256 !== requestSha256 || canonicalJson(priorIntent.input) !== canonicalJson(input) || priorIntent.project.projectId !== projectId)) throw new CliError("conflict", "Directing assembly intent differs from the exact accepted selection.");
457
+ // A saved intent is a recovery draft, not authority for placement or receipt
458
+ // content. Derive every field again from accepted bytes and current policies.
459
+ const ffprobe = await capability(application, "ffprobe"), timestamp = priorIntent?.project.createdAt ?? application.clock.now().toISOString();
460
+ const assets: ProjectAssetV1[] = [], placements: unknown[] = [], colors: z.infer<typeof assemblyIntentSchema>["colors"] = [], normalizations: z.infer<typeof assemblyIntentSchema>["normalizations"] = [], timing: z.infer<typeof assemblyIntentSchema>["timing"] = [];
461
+ let offset = 0, cumulativeNumerator = 0n, cumulativeDenominator = 1n;
462
+ for (const [index, clip] of input.clips.entries()) {
463
+ const retained = await retainVideo(application, clip.source, signal), probe = await probeVideo(application, retained, ffprobe.command, signal);
464
+ if (Math.abs(probe.media.durationUs - clip.durationUs) > 1) fail("Accepted clip duration differs from its actual media duration.");
465
+ const normalized = await normalizeForAssembly(application, retained, probe, signal, priorIntent !== undefined);
466
+ const asset = projectAsset(normalized.source, normalized.probe.media, timestamp);
467
+ if (normalized.receipt !== undefined && !normalizations.some(receipt => receipt.sha256 === normalized.receipt!.sha256)) normalizations.push(normalized.receipt);
468
+ colors.push({ shotId: clip.shotId, sourceSha256: clip.source.sha256, interpretation: probe.color });
469
+ if (!assets.some(candidate => candidate.assetId === asset.assetId)) assets.push(asset);
470
+ const video = probe.media.streams.find(stream => stream.codec_type === "video")!, videoAssetRange = video.assetRange;
471
+ const finalFrame = probe.frames.at(-1)!;
472
+ const durationNumerator = (BigInt(finalFrame.best_effort_timestamp!) + BigInt(finalFrame.duration ?? finalFrame.pkt_duration!) - BigInt(probe.frames[0]!.best_effort_timestamp!)) * BigInt(probe.numerator);
473
+ const durationDenominator = BigInt(probe.denominator), start = offset;
474
+ cumulativeNumerator = cumulativeNumerator * durationDenominator + durationNumerator * cumulativeDenominator;
475
+ cumulativeDenominator *= durationDenominator;
476
+ let a = cumulativeNumerator, b = cumulativeDenominator;
477
+ while (b !== 0n) { const remainder = a % b; a = b; b = remainder; }
478
+ cumulativeNumerator /= a; cumulativeDenominator /= a;
479
+ // Never round a cut up past an exact CFR sample time. Floor the cumulative
480
+ // rational clock once per boundary, so rounding error cannot accumulate.
481
+ offset = Number(cumulativeNumerator * 1_000_000n / cumulativeDenominator);
482
+ if (!Number.isSafeInteger(offset) || offset <= start) fail("Directed video span cannot be represented in the project microsecond clock.");
483
+ const audio = probe.media.streams.filter(stream => stream.codec_type === "audio").map(stream => {
484
+ const startUs = Math.max(stream.assetRange.startUs, videoAssetRange.startUs), endUs = Math.min(stream.assetRange.endUs, videoAssetRange.endUs);
485
+ return { streamIndex: stream.index, sourceAssetRange: stream.assetRange, selectedAssetRange: endUs > startUs ? { startUs, endUs } : null,
486
+ trimmedBeforeUs: Math.max(0, Math.min(stream.assetRange.endUs, videoAssetRange.startUs) - stream.assetRange.startUs),
487
+ trimmedAfterUs: Math.max(0, stream.assetRange.endUs - Math.max(stream.assetRange.startUs, videoAssetRange.endUs)) };
488
+ });
489
+ timing.push(assemblyTimingSchema.parse({ shotId: clip.shotId, attemptId: clip.attemptId, sourceSha256: clip.source.sha256, sourceDurationUs: probe.media.durationUs,
490
+ videoAssetRange, videoDurationSeconds: { numerator: String(durationNumerator), denominator: probe.denominator }, projectRange: { startUs: start, endUs: offset }, audio }));
491
+ placements.push({
492
+ placementId: `placement_${requestSha256.slice(0, 24)}_${index}`, assetId: asset.assetId, assetRange: videoAssetRange, enabled: true,
493
+ sync: { anchors: [{ assetTimeUs: videoAssetRange.startUs, projectTimeUs: start }, { assetTimeUs: videoAssetRange.endUs, projectTimeUs: offset }], provenance: { kind: "manual", note: `Directed shot ${clip.shotId}; accepted attempt ${clip.attemptId}; video span cut with audio trimmed to the selected video span.` } },
494
+ video: asset.streams.filter(stream => stream.kind === "video").map(stream => ({ streamId: stream.streamId, presentation: { enabled: true, blendMode: "normal", crop: { kind: "none" }, fit: "contain", layer: 0, layout: { kind: "normalized", x: 0, y: 0, width: 1, height: 1 }, opacity: 1 } })),
495
+ audio: asset.streams.filter(stream => stream.kind === "audio" && stream.segments.some(segment => segment.assetRange.startUs < videoAssetRange.endUs && segment.assetRange.endUs > videoAssetRange.startUs)).map(stream => ({ streamId: stream.streamId, presentation: { enabled: true, gainDb: 0, pan: 0 } })),
496
+ });
497
+ }
498
+ const project = VideoProjectV1Schema.parse({ kind: "slopcamera.video-project", schemaVersion: 1, projectId, name: input.title, createdAt: timestamp, updatedAt: timestamp, analyses: [], assets, placements,
499
+ referencePlacementId: `placement_${requestSha256.slice(0, 24)}_0`, currentEditPlanPath: "edits/current.json", timeline: { durationUs: offset, timebase: "microseconds" } });
500
+ const plan = createDefaultProjectEditPlan(project, ProjectEditPlanV1Schema.shape.planId.parse(`plan_${requestSha256.slice(0, 40)}`), timestamp);
501
+ const intent = assemblyIntentSchema.parse({ kind: "slopcamera.directing-assembly-intent", schemaVersion: 2, requestSha256, input, project, plan, colors, normalizations, timingPolicy: ASSEMBLY_TIMING_POLICY, timing });
502
+ if (priorIntent !== undefined && canonicalJson(priorIntent) !== canonicalJson(intent)) throw new CliError("conflict", "Retained directing assembly intent differs from the verified canonical derivation.");
503
+ await publishJson(application, intentPath, intent, signal, lease.assertOwned);
504
+ // Resume only these exact retained documents; edited or unknown generations conflict.
505
+ for (const receipt of intent.normalizations) await resolveVerifiedProjectMedia({ repositoryRoot: application.paths.repositoryRoot, path: receipt.path, expected: receipt, label: "Assembled color normalization receipt" });
506
+ for (const asset of intent.project.assets) for (const stream of asset.streams) for (const segment of stream.segments) await resolveVerifiedProjectMedia({ repositoryRoot: application.paths.repositoryRoot, path: segment.path, expected: segment, label: "Assembled clip" });
507
+ await directory(application, repoPath(application, projectDirectory));
508
+ await directory(application, repoPath(application, join(projectDirectory, "edits")));
509
+ const publishedPlan = await publishJson(application, join(projectDirectory, "edits/current.json"), intent.plan, signal, lease.assertOwned);
510
+ const publishedProject = await publishJson(application, join(projectDirectory, "project.json"), intent.project, signal, lease.assertOwned);
511
+ const receipt = await publishJson(application, join(root, `assembly-${requestSha256}.json`), {
512
+ kind: "slopcamera.directing-assembly-receipt", schemaVersion: 2, requestSha256, recipeSha256: input.recipeSha256,
513
+ projectId, project: publishedProject, plan: publishedPlan, clips: input.clips, durationUs: intent.project.timeline.durationUs, colors: intent.colors, normalizations: intent.normalizations, timingPolicy: intent.timingPolicy, timing: intent.timing,
514
+ }, signal, lease.assertOwned);
515
+ return { projectId, projectPath: publishedProject.path, receipt };
516
+ });
517
+ }
@@ -0,0 +1,89 @@
1
+ import type { DirectingQuote, DirectingShot } from "../application/directing-contract";
2
+ import type { GatewayMediaCatalogView, GatewayJsonValue } from "./gateway-media-catalog";
3
+ import { CliError } from "./errors";
4
+
5
+ /** Integer microdollars avoid floating point drift across small paid attempts. */
6
+ export function parseDirectingUsd(value: string): number {
7
+ if (!/^(?:0|[1-9]\d{0,5})(?:\.\d{1,6})?$/u.test(value)) {
8
+ throw new CliError("usage", "Budget must be positive USD with at most six decimal places.");
9
+ }
10
+ const [whole = "0", fraction = ""] = value.split(".");
11
+ const result = Number(whole) * 1_000_000 + Number(fraction.padEnd(6, "0"));
12
+ if (!Number.isSafeInteger(result) || result <= 0) throw new CliError("usage", "Budget must be positive USD.");
13
+ return result;
14
+ }
15
+
16
+ function record(value: GatewayJsonValue | undefined): Readonly<Record<string, GatewayJsonValue>> | undefined {
17
+ return value !== null && typeof value === "object" && !Array.isArray(value)
18
+ ? value as Readonly<Record<string, GatewayJsonValue>> : undefined;
19
+ }
20
+
21
+ function unsupported(message: string): never {
22
+ throw new CliError("unavailable", `Directing preflight: ${message}`);
23
+ }
24
+
25
+ /** Only transparent duration pricing can admit a budgeted directing request. */
26
+ export function quoteDirectingShot(
27
+ catalog: GatewayMediaCatalogView,
28
+ shot: DirectingShot,
29
+ now: Date,
30
+ ): DirectingQuote {
31
+ if ((shot.fps ?? 24) * shot.durationSeconds > 4000) unsupported("this request exceeds the local 4,000 decoded-frame bound.");
32
+ const dimensions = /^(\d+)x(\d+)$/u.exec(shot.resolution);
33
+ if (dimensions && (Number(dimensions[1]) > 4096 || Number(dimensions[2]) > 4096)) unsupported("this request exceeds the local 4,096-pixel dimension bound.");
34
+ const vertical = /^(\d+)p$/iu.exec(shot.resolution);
35
+ if (vertical) {
36
+ const [width = 1, height = 1] = shot.aspectRatio.split(":").map(Number);
37
+ const shortSide = Number(vertical[1]);
38
+ if (Math.max(shortSide, shortSide * width / height) > 4096) unsupported("this request exceeds the local 4,096-pixel dimension bound.");
39
+ }
40
+ const age = now.getTime() - Date.parse(catalog.snapshot.validatedAt);
41
+ if (catalog.status !== "fresh" || !Number.isFinite(age) || age < 0 || age > 5 * 60_000) unsupported("a fresh live catalog is required.");
42
+ const model = catalog.snapshot.models.find(value => value.id === shot.model);
43
+ if (model?.kind !== "video" || model.executionMode !== "video-model") unsupported("the selected model is not a batch video model.");
44
+ const capability = model.capabilities;
45
+ const operation = shot.lastFrame !== undefined ? "first-last-frame" : shot.firstFrame !== undefined ? "image-to-video" : "text-to-video";
46
+ for (const [key, value] of [
47
+ ["supported_operations", operation],
48
+ ["supported_resolutions", shot.resolution],
49
+ ["supported_aspect_ratios", shot.aspectRatio],
50
+ ["supported_durations_seconds", shot.durationSeconds],
51
+ ...(shot.fps === undefined ? [] : [["supported_fps", shot.fps]]),
52
+ ] as const) {
53
+ const supported = capability?.[key];
54
+ if (!Array.isArray(supported) || !supported.includes(value)) unsupported(`the catalog does not confirm ${key}=${value}.`);
55
+ }
56
+ if (model.pricing.varies_by_provider === true) unsupported("provider-dependent pricing cannot establish a bounded reservation.");
57
+ const rows = model.pricing.video_duration_pricing;
58
+ if (!Array.isArray(rows) || rows.length === 0 || rows.length > 128) unsupported("duration pricing is unavailable.");
59
+ const parsed = rows.map(record);
60
+ if (parsed.some(row => row === undefined || Object.keys(row).some(key => !["resolution", "cost_per_second"].includes(key)))) {
61
+ unsupported("the model has unsupported pricing conditions; use the ordinary Gateway command with a separately reviewed quote.");
62
+ }
63
+ const exact = parsed.filter(row => row?.resolution === shot.resolution);
64
+ const matching = exact.length > 0 ? exact : parsed.filter(row => row?.resolution === undefined);
65
+ if (matching.length !== 1 || typeof matching[0]?.cost_per_second !== "string") unsupported("pricing is missing or ambiguous for this resolution.");
66
+ const rate = parseDirectingUsd(matching[0].cost_per_second);
67
+ const cost = rate * shot.durationSeconds;
68
+ if (!Number.isSafeInteger(cost)) unsupported("the quote exceeds the integer budget bound.");
69
+ return {
70
+ catalogSha256: catalog.snapshot.snapshotId.slice(7),
71
+ validatedAt: catalog.snapshot.validatedAt,
72
+ costMicroUsd: cost,
73
+ rateMicroUsdPerSecond: rate,
74
+ };
75
+ }
76
+
77
+ /** Determine transport from the same catalog used to price the shot. */
78
+ export function directingInputTransport(catalog: GatewayMediaCatalogView, shot: DirectingShot): "none" | "inline" | "url" {
79
+ if (shot.firstFrame === undefined && shot.lastFrame === undefined) return "none";
80
+ const model = catalog.snapshot.models.find(value => value.id === shot.model);
81
+ const inputLimits = record(model?.capabilities?.input_limits);
82
+ const images = record(inputLimits?.image);
83
+ const sources = images?.supported_sources;
84
+ if (sources === undefined || sources === null) return "inline";
85
+ if (!Array.isArray(sources) || sources.some(value => typeof value !== "string")) unsupported("invalid image transport capabilities.");
86
+ if (sources.includes("base64") || sources.includes("buffer")) return "inline";
87
+ if (sources.includes("url")) return "url";
88
+ return unsupported("the model has no supported image transport.");
89
+ }