@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,179 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, open, opendir, realpath } from "node:fs/promises";
4
+ import { dirname, join, parse, relative, resolve, sep } from "node:path";
5
+ import { canonicalJson } from "../../../src/code/canonical-json";
6
+ import {
7
+ parseStudioSourceBundle, StudioPathSchema, studioOutputPath, studioSourceBundleSha256,
8
+ type StudioJob, type StudioOutputArtifact, type StudioSourceBundle,
9
+ } from "../../../src/studio";
10
+ import { createNodeBundleFileSystem } from "../core/storage";
11
+ import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
12
+
13
+ export const studioBytesSha256 = (bytes: string | Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
14
+ export const studioJson = (value: unknown): string => `${canonicalJson(value)}\n`;
15
+
16
+ export async function readStudioFileEdges(path: string, expectedBytes: number, edgeBytes = 256): Promise<{ readonly first: Buffer; readonly last: Buffer }> {
17
+ if (!Number.isInteger(edgeBytes) || edgeBytes < 1 || edgeBytes > 1_048_576) throw new Error("Invalid studio header bound.");
18
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
19
+ try {
20
+ const before = await handle.stat({ bigint: true });
21
+ if (!before.isFile() || before.size !== BigInt(expectedBytes)) throw new Error("Studio output changed before header validation.");
22
+ const length = Math.min(edgeBytes, expectedBytes), first = Buffer.alloc(length), last = Buffer.alloc(length);
23
+ const a = await handle.read(first, 0, length, 0), b = await handle.read(last, 0, length, expectedBytes - length);
24
+ const after = await handle.stat({ bigint: true });
25
+ if (a.bytesRead !== length || b.bytesRead !== length || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs) throw new Error("Studio output changed during header validation.");
26
+ return { first, last };
27
+ } finally { await handle.close(); }
28
+ }
29
+
30
+ export async function readStudioBytes(path: string, expected: { readonly bytes: number; readonly sha256: string }, maximumBytes: number): Promise<Buffer> {
31
+ if (expected.bytes > maximumBytes) throw new Error("Studio binary exceeds its decode profile.");
32
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
33
+ try {
34
+ const before = await handle.stat({ bigint: true });
35
+ if (!before.isFile() || before.size !== BigInt(expected.bytes)) throw new Error("Studio binary changed before decode.");
36
+ const bytes = Buffer.alloc(expected.bytes);
37
+ let offset = 0;
38
+ while (offset < bytes.length) {
39
+ const result = await handle.read(bytes, offset, Math.min(256 * 1024, bytes.length - offset), offset);
40
+ if (result.bytesRead === 0) throw new Error("Studio binary ended during decode capture.");
41
+ offset += result.bytesRead;
42
+ }
43
+ const after = await handle.stat({ bigint: true });
44
+ if (before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || studioBytesSha256(bytes) !== expected.sha256) throw new Error("Studio binary changed during decode capture.");
45
+ return bytes;
46
+ } finally { await handle.close(); }
47
+ }
48
+
49
+ export async function studioPhysicalRoot(path: string): Promise<string> {
50
+ const root = resolve(path), stat = await lstat(root);
51
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error("Studio root must be a physical directory.");
52
+ return await realpath(root);
53
+ }
54
+
55
+ /** The source root is selected explicitly; only named files are read. */
56
+ export async function captureStudioSource(input: {
57
+ readonly sourceRoot: string; readonly engine: StudioSourceBundle["engine"];
58
+ readonly entrypoint: StudioSourceBundle["entrypoint"]; readonly files: readonly string[];
59
+ }): Promise<StudioSourceBundle> {
60
+ if (input.files.length < 1 || input.files.length > 512) throw new Error("Studio sources require 1–512 explicit files.");
61
+ const root = await studioPhysicalRoot(input.sourceRoot), fs = createNodeBundleFileSystem(root);
62
+ const files = [];
63
+ let bytes = 0;
64
+ for (const path of input.files) {
65
+ StudioPathSchema.parse(path);
66
+ const integrity = await fs.inspectFile!(path, 4_294_967_296 - bytes);
67
+ bytes += integrity.bytes;
68
+ files.push({ path, ...integrity });
69
+ }
70
+ return parseStudioSourceBundle({ kind: "slopcamera.studio-source-bundle", schemaVersion: 1, engine: input.engine, entrypoint: input.entrypoint, files });
71
+ }
72
+
73
+ export async function verifyStudioSource(sourceRoot: string, bundle: StudioSourceBundle): Promise<void> {
74
+ const fs = createNodeBundleFileSystem(await studioPhysicalRoot(sourceRoot));
75
+ const inventory = await inventoryStudioFiles(sourceRoot, 512, 4_294_967_296);
76
+ if (inventory.length !== bundle.files.length || inventory.some(item => !bundle.files.some(file => file.path === item.path && file.bytes === item.bytes))) throw new Error("Retained studio source contains an undeclared or changed file.");
77
+ for (const file of bundle.files) {
78
+ const found = await fs.inspectFile!(file.path, file.bytes);
79
+ if (found.bytes !== file.bytes || found.sha256 !== file.sha256) throw new Error(`Studio source changed: ${file.path}`);
80
+ }
81
+ }
82
+
83
+ /** Reuse the storage layer's inode-pinned exact copy and no-replace publication across two explicit roots. */
84
+ export async function copyStudioSource(sourceRoot: string, destinationRoot: string, bundle: StudioSourceBundle, fence: () => Promise<void>): Promise<void> {
85
+ const source = await studioPhysicalRoot(sourceRoot), destination = await studioPhysicalRoot(destinationRoot);
86
+ let ancestor = source;
87
+ while (relative(ancestor, destination) === ".." || relative(ancestor, destination).startsWith(`..${sep}`)) {
88
+ const parent = dirname(ancestor);
89
+ if (parent === ancestor) throw new Error("Studio source and destination do not share a filesystem root.");
90
+ ancestor = parent;
91
+ }
92
+ if (parse(source).root !== parse(destination).root) throw new Error("Studio copies require a shared filesystem root.");
93
+ const fs = createNodeBundleFileSystem(ancestor);
94
+ for (const file of bundle.files) {
95
+ await fs.copyFileNoReplace!(relative(ancestor, join(source, file.path)), relative(ancestor, join(destination, file.path)), file, fence);
96
+ }
97
+ }
98
+
99
+ export async function retainStudioSource(input: {
100
+ readonly studioRoot: string; readonly sourceRoot: string; readonly bundle: StudioSourceBundle; readonly fence: () => Promise<void>;
101
+ }): Promise<{ readonly bundle: StudioSourceBundle; readonly bundleSha256: string; readonly sourceRoot: string }> {
102
+ const bundle = parseStudioSourceBundle(input.bundle), bundleSha256 = studioSourceBundleSha256(bundle);
103
+ const directory = await ensurePhysicalPrivateDirectoryWithin(input.studioRoot, `bundles/${bundleSha256}`);
104
+ const sourceRoot = await ensurePhysicalPrivateDirectoryWithin(directory, "source");
105
+ await copyStudioSource(input.sourceRoot, sourceRoot, bundle, input.fence);
106
+ const fs = createNodeBundleFileSystem(directory), text = studioJson(bundle);
107
+ const disposition = await fs.writeTextNoReplace!("bundle.json", text, input.fence);
108
+ if (disposition !== "created" && await fs.readText("bundle.json") !== text) throw new Error("Retained studio bundle conflicts with its digest.");
109
+ await verifyStudioSource(sourceRoot, bundle);
110
+ return { bundle, bundleSha256, sourceRoot };
111
+ }
112
+
113
+ export interface StudioFileInventory { readonly path: string; readonly bytes: number }
114
+
115
+ /** Bounded traversal is restricted to the newly owned output/cache directory, never an asset source. */
116
+ export async function inventoryStudioFiles(rootInput: string, maximumFiles: number, maximumBytes: number, mode: "settled" | "live" = "settled"): Promise<readonly StudioFileInventory[]> {
117
+ const root = await studioPhysicalRoot(rootInput), found: StudioFileInventory[] = [];
118
+ const rootIdentity = await lstat(root, { bigint: true });
119
+ if (!rootIdentity.isDirectory() || rootIdentity.isSymbolicLink()) throw new Error("Studio output root must remain a physical directory.");
120
+ const pending = [""], names = new Set<string>();
121
+ // Native caches atomically rename and remove temporary descendants. A live
122
+ // observation may omit an already removed entry; settled publication may not.
123
+ const disappeared = (error: unknown): boolean => mode === "live" && error instanceof Error && "code" in error && error.code === "ENOENT";
124
+ let bytes = 0, entries = 0;
125
+ while (pending.length > 0) {
126
+ const current = pending.pop()!;
127
+ let directory;
128
+ try { directory = await opendir(join(root, current)); }
129
+ catch (error) { if (current !== "" && disappeared(error)) continue; throw error; }
130
+ for await (const entry of directory) {
131
+ if (++entries > maximumFiles * 8 + 1024) throw new Error("Studio output directory-entry budget exceeded.");
132
+ const path = current === "" ? entry.name : `${current}/${entry.name}`;
133
+ StudioPathSchema.parse(path);
134
+ if (path.split("/").length > 32 || names.has(path.toLowerCase())) throw new Error("Studio output paths exceed depth or collide.");
135
+ names.add(path.toLowerCase());
136
+ let stat;
137
+ try { stat = await lstat(join(root, path)); }
138
+ catch (error) { if (disappeared(error)) continue; throw error; }
139
+ // APFS can return the just-unlinked inode with nlink=0 instead of ENOENT.
140
+ // Reobserve once; only disappearance or a valid replacement can pass.
141
+ if (mode === "live" && stat.isFile() && stat.nlink === 0) {
142
+ try { stat = await lstat(join(root, path)); }
143
+ catch (error) { if (disappeared(error)) continue; throw error; }
144
+ }
145
+ if (stat.isSymbolicLink()) throw new Error("Studio outputs cannot contain symlinks.");
146
+ if (stat.isDirectory()) pending.push(path);
147
+ else {
148
+ if (!stat.isFile() || stat.nlink !== 1 || !Number.isSafeInteger(stat.size)) throw new Error("Studio output must be a singly linked regular file.");
149
+ bytes += stat.size;
150
+ if (found.length >= maximumFiles || bytes > maximumBytes) throw new Error("Studio output budget exceeded.");
151
+ found.push({ path, bytes: stat.size });
152
+ }
153
+ }
154
+ }
155
+ // Disappearance of the owned root itself is always a failure, including when
156
+ // an open directory iterator could otherwise finish after its unlink.
157
+ if (await studioPhysicalRoot(rootInput) !== root) throw new Error("Studio output root changed during inventory.");
158
+ const after = await lstat(root, { bigint: true });
159
+ if (!after.isDirectory() || after.isSymbolicLink() || after.dev !== rootIdentity.dev || after.ino !== rootIdentity.ino) throw new Error("Studio output root changed during inventory.");
160
+ return found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
161
+ }
162
+
163
+ export async function inspectStudioOutputs(root: string, job: StudioJob): Promise<readonly StudioOutputArtifact[]> {
164
+ const inventory = await inventoryStudioFiles(root, job.limits.maximumOutputFiles, job.limits.maximumOutputBytes);
165
+ const files = new Map<string, { readonly output: StudioJob["outputs"][number]; readonly frame?: number }>();
166
+ for (const output of job.outputs) {
167
+ if (output.kind === "file") files.set(output.path, { output });
168
+ else if (output.kind === "sequence") for (let frame = job.render!.startFrame; frame < job.render!.endFrameExclusive; frame++) files.set(studioOutputPath(output, frame), { output, frame });
169
+ }
170
+ const fs = createNodeBundleFileSystem(root), artifacts: StudioOutputArtifact[] = [];
171
+ for (const item of inventory) {
172
+ const match = files.get(item.path);
173
+ const output = match?.output ?? job.outputs.find(output => output.kind === "directory" && item.path.startsWith(`${output.path}/`));
174
+ if (output === undefined || item.bytes === 0) throw new Error(`Undeclared or empty studio output: ${item.path}`);
175
+ const integrity = await fs.inspectFile!(item.path, item.bytes);
176
+ artifacts.push({ outputId: output.id, path: item.path, ...integrity, role: output.role, format: output.format, ...(match?.frame === undefined ? {} : { frame: match.frame }) });
177
+ }
178
+ return artifacts;
179
+ }
@@ -0,0 +1,162 @@
1
+ import { join } from "node:path";
2
+ import sharp from "sharp";
3
+ import { z } from "zod";
4
+ import { STUDIO_LIMITS, type StudioJob, type StudioOutputArtifact } from "../../../src/studio";
5
+ import type { ApplicationContext } from "../application/context";
6
+ import { bindExactCapability } from "../application/capability-binding";
7
+ import { readStudioBytes, readStudioFileEdges } from "./studio-files";
8
+ import { inspectStudioExrHeader } from "./studio-exr";
9
+ import type { StudioProcessPort } from "./studio-process";
10
+
11
+ export interface StudioOutputValidation {
12
+ readonly outputId: string; readonly path: string;
13
+ readonly validation: "decoded-raster" | "decoded-video" | "decoded-audio" | "format-header" | "opaque-cache" | "inert-source";
14
+ readonly width?: number; readonly height?: number; readonly frameCount?: number;
15
+ }
16
+
17
+ const probeSchema = z.object({ streams: z.array(z.object({
18
+ codec_type: z.string(), codec_name: z.string().optional(), width: z.number().int().optional(), height: z.number().int().optional(),
19
+ pix_fmt: z.string().optional(), avg_frame_rate: z.string().optional(), nb_read_frames: z.string().optional(),
20
+ sample_rate: z.string().optional(), channels: z.number().int().optional(), time_base: z.string().max(32).optional(),
21
+ })).max(32), format: z.object({ format_name: z.string().min(1).max(128) }), frames: z.array(z.object({
22
+ best_effort_timestamp: z.number().int().safe(), duration: z.number().int().safe().positive().optional(), pkt_duration: z.number().int().safe().positive().optional(),
23
+ width: z.number().int().positive(), height: z.number().int().positive(), pix_fmt: z.string().min(1).max(64),
24
+ })).max(STUDIO_LIMITS.frames).optional() });
25
+
26
+ const mediaFormats = {
27
+ mp4: { demuxer: "mov", names: ["mov", "mp4", "m4a", "3gp", "3g2", "mj2"] },
28
+ mov: { demuxer: "mov", names: ["mov", "mp4", "m4a", "3gp", "3g2", "mj2"] },
29
+ webm: { demuxer: "matroska", names: ["matroska", "webm"] },
30
+ wav: { demuxer: "wav", names: ["wav"] }, flac: { demuxer: "flac", names: ["flac"] }, mp3: { demuxer: "mp3", names: ["mp3"] },
31
+ exr: { demuxer: "exr_pipe", names: ["exr_pipe"] },
32
+ } as const;
33
+ type ProbedFormat = keyof typeof mediaFormats;
34
+ // The portable contract can describe higher precision, but this verified video
35
+ // delivery profile is RGB(A)8 only. Preserve uint16 PNG and float EXR masters;
36
+ // 10/12/16-bit and floating video need separately qualified format profiles.
37
+ const rgb8VideoPixels = new Set(["rgb24", "bgr24", "gbrp", "yuv420p", "yuv422p", "yuv444p", "yuvj420p", "yuvj422p", "yuvj444p"]);
38
+ const rgba8VideoPixels = new Set(["rgba", "bgra", "argb", "abgr", "gbrap", "yuva420p", "yuva422p", "yuva444p"]);
39
+
40
+ function mediaInputArguments(format: ProbedFormat): readonly string[] {
41
+ const { demuxer } = mediaFormats[format];
42
+ return ["-protocol_whitelist", "file", "-format_whitelist", demuxer, "-f", demuxer,
43
+ ...(demuxer === "mov" ? ["-enable_drefs", "0", "-use_absolute_path", "0"] : []),
44
+ "-max_pixels", String(STUDIO_LIMITS.pixels), "-threads", "1"];
45
+ }
46
+
47
+ /** Each rational timestamp may be quantized by less than one container tick; drift cannot accumulate. */
48
+ function validateVideoClock(probe: z.infer<typeof probeSchema>, render: NonNullable<StudioJob["render"]>): void {
49
+ const stream = probe.streams[0]!, frames = probe.frames;
50
+ const base = /^([1-9]\d{0,8})\/([1-9]\d{0,8})$/u.exec(stream.time_base ?? "");
51
+ const count = render.endFrameExclusive - render.startFrame;
52
+ if (base === null || frames?.length !== count) throw new Error("Studio video requires every decoded timestamp and a bounded exact time base.");
53
+ const denominator = BigInt(render.frameRate.numerator) * BigInt(base[1]!);
54
+ const step = BigInt(render.frameRate.denominator) * BigInt(base[2]!);
55
+ // A clock coarser than a frame cannot establish every individual frame boundary.
56
+ if (step < denominator) throw new Error("Studio video time base cannot represent its declared frame cadence.");
57
+ const matches = (ticks: number, index: number, quantizedValues = 1n) => {
58
+ const delta = BigInt(ticks) * denominator - BigInt(index) * step;
59
+ return delta > -denominator * quantizedValues && delta < denominator * quantizedValues;
60
+ };
61
+ for (const [index, frame] of frames.entries()) {
62
+ if (index === 0 && frame.best_effort_timestamp !== 0 || index > 0 && frame.best_effort_timestamp <= frames[index - 1]!.best_effort_timestamp
63
+ || !matches(frame.best_effort_timestamp, index) || frame.width !== render.width || frame.height !== render.height || frame.pix_fmt !== stream.pix_fmt) {
64
+ throw new Error("Studio decoded frames differ from their zero-based rational clock or fixed pixel profile.");
65
+ }
66
+ const duration = frame.duration ?? frame.pkt_duration;
67
+ if (frame.duration !== undefined && frame.pkt_duration !== undefined && frame.duration !== frame.pkt_duration) throw new Error("Studio decoded frame durations disagree.");
68
+ // Matroska quantizes nominal duration independently of PTS. Their sum has two
69
+ // bounded quantization errors, while each PTS and duration must still match on its own.
70
+ if (duration !== undefined && (!matches(duration, 1) || !Number.isSafeInteger(frame.best_effort_timestamp + duration) || !matches(frame.best_effort_timestamp + duration, index + 1, 2n))) {
71
+ throw new Error("Studio decoded frame duration differs from its rational end boundary.");
72
+ }
73
+ if (index === count - 1 && duration === undefined) throw new Error("Studio video requires the final decoded frame duration to prove end coverage.");
74
+ }
75
+ }
76
+
77
+ /** Observed pixel/stream properties are checked; native units and cache meaning remain declared source semantics. */
78
+ export async function validateStudioOutputMedia(input: {
79
+ readonly application: ApplicationContext; readonly process: StudioProcessPort; readonly root: string;
80
+ readonly job: StudioJob; readonly outputs: readonly StudioOutputArtifact[]; readonly signal: AbortSignal;
81
+ readonly cwd: string; readonly env: Readonly<Record<string, string>>; readonly inheritedFileDescriptors?: readonly number[];
82
+ readonly fence: () => Promise<void>;
83
+ }): Promise<readonly StudioOutputValidation[]> {
84
+ const evidence: StudioOutputValidation[] = [];
85
+ for (const artifact of input.outputs) {
86
+ if (input.signal.aborted) throw new Error("Studio output validation cancelled.");
87
+ await input.fence();
88
+ const output = input.job.outputs.find(output => output.id === artifact.outputId)!;
89
+ const path = join(input.root, artifact.path);
90
+ if (artifact.format === "png") {
91
+ const bytes = await readStudioBytes(path, artifact, 272 * 1024 * 1024);
92
+ const metadata = await sharp(bytes, { failOn: "error", limitInputPixels: 33_554_432, animated: true }).metadata();
93
+ const interpretation = output.interpretation;
94
+ if (interpretation.kind !== "raster" || metadata.format !== "png" || (metadata.pages ?? 1) !== 1
95
+ || metadata.width !== input.job.render?.width || metadata.height !== input.job.render.height
96
+ || metadata.channels !== interpretation.channels.length
97
+ || Boolean(metadata.hasAlpha) !== ["straight", "premultiplied"].includes(interpretation.alpha)
98
+ || (interpretation.dataType === "uint16" ? metadata.depth !== "ushort" : metadata.depth !== "uchar")) throw new Error(`Studio PNG profile differs from its declaration: ${artifact.path}`);
99
+ await sharp(bytes, { failOn: "error", limitInputPixels: 33_554_432 }).raw().toBuffer();
100
+ evidence.push({ outputId: output.id, path: artifact.path, validation: "decoded-raster", width: metadata.width, height: metadata.height, frameCount: 1 });
101
+ } else if (["mp4", "mov", "webm", "wav", "flac", "mp3", "exr"].includes(artifact.format)) {
102
+ if (artifact.format === "exr") {
103
+ const header = inspectStudioExrHeader((await readStudioFileEdges(path, artifact.bytes, 1_048_576)).first);
104
+ const interpretation = output.interpretation;
105
+ if (interpretation.kind !== "raster" || header.width !== input.job.render?.width || header.height !== input.job.render.height
106
+ || header.channels.some(channel => channel.dataType !== interpretation.dataType)
107
+ || header.channels.map(channel => channel.name).sort().join("\0") !== [...interpretation.channels].sort().join("\0")
108
+ || header.channels.some(channel => channel.name === "A") !== ["straight", "premultiplied"].includes(interpretation.alpha)) throw new Error("Studio EXR channel/type/alpha profile differs from its declaration.");
109
+ }
110
+ const format = artifact.format as ProbedFormat, video = ["mp4", "mov", "webm"].includes(format);
111
+ const capability = await bindExactCapability(await input.application.capability("ffprobe"));
112
+ const entries = "stream=codec_type,codec_name,width,height,pix_fmt,avg_frame_rate,nb_read_frames,sample_rate,channels,time_base:format=format_name"
113
+ + (video ? ":frame=best_effort_timestamp,duration,pkt_duration,width,height,pix_fmt:frame_side_data=" : "");
114
+ const result = await input.process.run([capability.executablePath, "-v", "error", ...mediaInputArguments(format), "-count_frames", "-show_streams", "-show_format",
115
+ ...(video ? ["-show_frames"] : []), "-show_entries", entries, "-of", "json=compact=1", path], {
116
+ cwd: input.cwd, env: input.env, timeoutMs: Math.min(input.job.limits.timeoutSeconds * 1000, 300_000), maximumLogBytes: 4_194_304,
117
+ signal: input.signal, ...(input.inheritedFileDescriptors === undefined ? {} : { inheritedFileDescriptors: input.inheritedFileDescriptors }),
118
+ });
119
+ if (result.exitCode !== 0 || result.failure !== undefined || result.custody !== "closed" || result.stderr.trim() !== "") throw new Error(`Studio output does not fully decode: ${artifact.path}`);
120
+ const probe = probeSchema.parse(JSON.parse(result.stdout) as unknown);
121
+ const names = probe.format.format_name.split(","), allowed: readonly string[] = mediaFormats[format].names;
122
+ if (!names.includes(mediaFormats[format].demuxer) || names.some(name => !allowed.includes(name))) throw new Error("Studio media container differs from its declared format family.");
123
+ const interpretation = output.interpretation;
124
+ if (probe.streams.length !== 1) throw new Error("Studio outputs require exactly one declared media stream.");
125
+ const stream = probe.streams[0]!;
126
+ if (interpretation.kind === "audio") {
127
+ if (stream.codec_type !== "audio" || stream.sample_rate !== String(interpretation.sampleRate) || stream.channels !== interpretation.channels || !(Number(stream.nb_read_frames) > 0)) throw new Error("Studio audio differs from its declared profile.");
128
+ evidence.push({ outputId: output.id, path: artifact.path, validation: "decoded-audio" });
129
+ } else {
130
+ const render = input.job.render;
131
+ if (interpretation.kind !== "raster" || render === undefined || stream.codec_type !== "video" || stream.width !== render.width || stream.height !== render.height) throw new Error("Studio raster differs from its dimensions or type.");
132
+ const frameCount = artifact.format === "exr" ? 1 : render.endFrameExclusive - render.startFrame;
133
+ if (stream.nb_read_frames !== String(frameCount)) throw new Error("Studio decoded frame count differs from its exact interval.");
134
+ if (artifact.format !== "exr") {
135
+ const rate = /^([1-9]\d{0,8})\/([1-9]\d{0,8})$/u.exec(stream.avg_frame_rate ?? "");
136
+ if (rate === null || BigInt(rate[1]!) * BigInt(render.frameRate.denominator) !== BigInt(rate[2]!) * BigInt(render.frameRate.numerator)) throw new Error("Studio video cadence differs from its exact rational clock.");
137
+ const pixels = stream.pix_fmt ?? "", alpha = rgba8VideoPixels.has(pixels);
138
+ if (!rgb8VideoPixels.has(pixels) && !alpha || interpretation.dataType !== "uint8"
139
+ || interpretation.channels.join(",") !== (alpha ? "R,G,B,A" : "R,G,B")) {
140
+ throw new Error("Studio video requires a verified 8-bit RGB/RGBA delivery profile; retain higher precision in PNG or EXR masters.");
141
+ }
142
+ if (alpha !== ["straight", "premultiplied"].includes(interpretation.alpha)) throw new Error("Studio video alpha differs from its declaration.");
143
+ validateVideoClock(probe, render);
144
+ }
145
+ evidence.push({ outputId: output.id, path: artifact.path, validation: artifact.format === "exr" ? "decoded-raster" : "decoded-video", width: render.width, height: render.height, frameCount });
146
+ }
147
+ } else if (artifact.format === "cache") evidence.push({ outputId: output.id, path: artifact.path, validation: "opaque-cache" });
148
+ else if (artifact.format === "py") evidence.push({ outputId: output.id, path: artifact.path, validation: "inert-source" });
149
+ else {
150
+ // Native formats are retained as native authority. This checks framing, not a universal semantic round trip.
151
+ const { first: bytes, last } = await readStudioFileEdges(path, artifact.bytes);
152
+ const valid = artifact.format === "glb" ? bytes.length >= 20 && bytes.toString("ascii", 0, 4) === "glTF" && bytes.readUInt32LE(4) === 2 && bytes.readUInt32LE(8) === artifact.bytes
153
+ : artifact.format === "blend" ? bytes.toString("ascii", 0, 7) === "BLENDER" || bytes.subarray(0, 4).equals(Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))
154
+ : artifact.format === "step" ? bytes.toString("ascii").includes("ISO-10303-21;") && last.toString("ascii").includes("END-ISO-10303-21;")
155
+ : artifact.format === "usdc" ? bytes.toString("ascii", 0, 8) === "PXR-USDC" : artifact.format === "usda" ? bytes.toString("ascii", 0, 9) === "#usda 1.0"
156
+ : artifact.format === "usd" && (bytes.toString("ascii", 0, 8) === "PXR-USDC" || bytes.toString("ascii", 0, 9) === "#usda 1.0");
157
+ if (!valid) throw new Error(`Studio native output has invalid format framing: ${artifact.path}`);
158
+ evidence.push({ outputId: output.id, path: artifact.path, validation: "format-header" });
159
+ }
160
+ }
161
+ return evidence;
162
+ }
@@ -0,0 +1,145 @@
1
+ import { spawn } from "node:child_process";
2
+ import { dirname, join } from "node:path";
3
+
4
+ /** Trusted native code is not sandboxed. This port supervises its ordinary POSIX process group. */
5
+ export interface StudioProcessResult {
6
+ readonly exitCode: number | null;
7
+ readonly stdout: string;
8
+ readonly stderr: string;
9
+ readonly custody: "closed" | "unknown";
10
+ readonly failure?: "cancelled" | "timeout" | "output-limit" | "spawn" | "descendants";
11
+ }
12
+
13
+ export interface StudioProcessOptions {
14
+ readonly cwd: string;
15
+ readonly env: Readonly<Record<string, string>>;
16
+ readonly timeoutMs: number;
17
+ readonly maximumLogBytes: number;
18
+ readonly signal?: AbortSignal;
19
+ readonly inheritedFileDescriptors?: readonly number[];
20
+ readonly onSpawn?: (pid: number) => Promise<void>;
21
+ readonly assertBudget?: () => Promise<void>;
22
+ }
23
+
24
+ export interface StudioProcessPort {
25
+ run(argv: readonly [string, ...string[]], options: StudioProcessOptions): Promise<StudioProcessResult>;
26
+ }
27
+
28
+ export function studioChildEnvironment(executable: string, workingRoot: string, threads: number): Record<string, string> {
29
+ if (!Number.isInteger(threads) || threads < 1 || threads > 1024) throw new Error("Invalid native worker budget.");
30
+ return {
31
+ PATH: `${dirname(executable)}:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin`,
32
+ HOME: join(workingRoot, "home"), TMPDIR: join(workingRoot, "tmp"),
33
+ LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8", TZ: "UTC",
34
+ PYTHONNOUSERSITE: "1", PYTHONDONTWRITEBYTECODE: "1",
35
+ OMP_NUM_THREADS: String(threads), OPENBLAS_NUM_THREADS: String(threads),
36
+ MKL_NUM_THREADS: String(threads), NUMEXPR_NUM_THREADS: String(threads),
37
+ BLENDER_USER_RESOURCES: join(workingRoot, "blender-user"),
38
+ };
39
+ }
40
+
41
+ function groupState(pid: number): "present" | "absent" | "unknown" {
42
+ try { process.kill(-pid, 0); return "present"; }
43
+ catch (error) {
44
+ return error instanceof Error && "code" in error && error.code === "ESRCH" ? "absent" : "unknown";
45
+ }
46
+ }
47
+
48
+ /** Detached sessions created by authored code are unsupported; process groups are custody, not confinement. */
49
+ export class NativeStudioProcess implements StudioProcessPort {
50
+ async run(argv: readonly [string, ...string[]], options: StudioProcessOptions): Promise<StudioProcessResult> {
51
+ if (process.platform !== "darwin" && process.platform !== "linux") throw new Error("Native studio execution currently requires POSIX process-group supervision.");
52
+ if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 21_600_000
53
+ || !Number.isSafeInteger(options.maximumLogBytes) || options.maximumLogBytes < 1 || options.maximumLogBytes > 4_194_304) throw new Error("Invalid native process limits.");
54
+ if (options.signal?.aborted) return { exitCode: null, stdout: "", stderr: "", custody: "closed", failure: "cancelled" };
55
+ const child = spawn(argv[0], [...argv.slice(1)], {
56
+ cwd: options.cwd, env: { ...options.env }, detached: true,
57
+ stdio: ["ignore", "pipe", "pipe", ...(options.inheritedFileDescriptors ?? [])],
58
+ });
59
+ let failure: StudioProcessResult["failure"];
60
+ let total = 0;
61
+ const stdout: Buffer[] = [], stderr: Buffer[] = [];
62
+ let spawned = false;
63
+ let escalation: ReturnType<typeof setTimeout> | undefined;
64
+ let custodyDeadline: ReturnType<typeof setTimeout> | undefined;
65
+ let budgetTimer: ReturnType<typeof setInterval> | undefined;
66
+ let budgetCheck: Promise<void> | undefined;
67
+ let spawnCheckpoint: Promise<void> | undefined;
68
+ let leaderExited = false;
69
+ let resolveExit: (code: number | null) => void = () => {};
70
+ const signalGroup = (signal: NodeJS.Signals) => {
71
+ if (!spawned || child.pid === undefined) return;
72
+ try { process.kill(-child.pid, signal); }
73
+ catch (error) { if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) failure ??= "descendants"; }
74
+ };
75
+ const stop = (reason: NonNullable<StudioProcessResult["failure"]>) => {
76
+ failure ??= reason;
77
+ signalGroup("SIGTERM");
78
+ escalation ??= setTimeout(() => signalGroup("SIGKILL"), 500);
79
+ custodyDeadline ??= setTimeout(() => resolveExit(null), 5_500);
80
+ };
81
+ const capture = (target: Buffer[], chunk: Buffer) => {
82
+ const remaining = Math.max(0, options.maximumLogBytes - total);
83
+ if (remaining > 0) target.push(Buffer.from(chunk.subarray(0, remaining)));
84
+ total += chunk.length;
85
+ if (total > options.maximumLogBytes) stop("output-limit");
86
+ };
87
+ child.stdout?.on("data", (chunk: Buffer) => capture(stdout, chunk));
88
+ child.stderr?.on("data", (chunk: Buffer) => capture(stderr, chunk));
89
+ child.stdout?.on("error", () => stop("spawn"));
90
+ child.stderr?.on("error", () => stop("spawn"));
91
+ const abort = () => stop("cancelled");
92
+ const exit = new Promise<number | null>(resolve => {
93
+ resolveExit = resolve;
94
+ child.once("error", () => { failure ??= "spawn"; resolve(null); });
95
+ child.once("exit", code => { leaderExited = true; resolve(code); });
96
+ });
97
+ const closed = new Promise<void>(resolve => child.once("close", () => resolve()));
98
+ child.once("spawn", () => {
99
+ spawned = true;
100
+ if (options.signal?.aborted) stop("cancelled");
101
+ spawnCheckpoint = Promise.resolve().then(() => options.onSpawn?.(child.pid!)).catch(() => stop("spawn"));
102
+ });
103
+ options.signal?.addEventListener("abort", abort, { once: true });
104
+ const timer = setTimeout(() => stop("timeout"), options.timeoutMs);
105
+ if (options.assertBudget !== undefined) budgetTimer = setInterval(() => {
106
+ if (budgetCheck !== undefined) return;
107
+ budgetCheck = options.assertBudget!().catch(() => { stop("output-limit"); }).finally(() => { budgetCheck = undefined; });
108
+ }, 500);
109
+ try {
110
+ const exitCode = await exit;
111
+ if (budgetTimer !== undefined) { clearInterval(budgetTimer); budgetTimer = undefined; }
112
+ let callbacksSettled = false, callbackTimer: ReturnType<typeof setTimeout> | undefined;
113
+ try {
114
+ callbacksSettled = await Promise.race([
115
+ Promise.all([spawnCheckpoint, budgetCheck]).then(() => true),
116
+ new Promise<false>(resolve => { callbackTimer = setTimeout(() => resolve(false), 5_000); }),
117
+ ]);
118
+ if (!callbacksSettled) stop("descendants");
119
+ } finally { if (callbackTimer !== undefined) clearTimeout(callbackTimer); }
120
+ if (child.pid !== undefined && spawned && groupState(child.pid) !== "absent") {
121
+ stop("descendants");
122
+ const deadline = performance.now() + 5_000;
123
+ while (groupState(child.pid) === "present" && performance.now() < deadline) {
124
+ await new Promise(resolve => setTimeout(resolve, 50));
125
+ }
126
+ }
127
+ let custody: StudioProcessResult["custody"] = callbacksSettled && (!spawned || (child.pid !== undefined && leaderExited && groupState(child.pid) === "absent")) ? "closed" : "unknown";
128
+ if (custody === "closed") {
129
+ let drainTimer: ReturnType<typeof setTimeout> | undefined;
130
+ try {
131
+ const drained = await Promise.race([closed.then(() => true), new Promise<false>(resolve => { drainTimer = setTimeout(() => resolve(false), 5_000); })]);
132
+ if (!drained) { custody = "unknown"; failure ??= "descendants"; }
133
+ } finally { if (drainTimer !== undefined) clearTimeout(drainTimer); }
134
+ }
135
+ if (custody !== "closed") { child.stdout?.destroy(); child.stderr?.destroy(); }
136
+ return { exitCode, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8"), custody, ...(failure === undefined ? {} : { failure }) };
137
+ } finally {
138
+ clearTimeout(timer);
139
+ if (escalation !== undefined) clearTimeout(escalation);
140
+ if (custodyDeadline !== undefined) clearTimeout(custodyDeadline);
141
+ if (budgetTimer !== undefined) clearInterval(budgetTimer);
142
+ options.signal?.removeEventListener("abort", abort);
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,77 @@
1
+ import { constants } from "node:fs";
2
+ import { access, lstat, realpath } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { z } from "zod";
5
+ import { parseStudioRuntimeIdentity, StudioCapabilityNameSchema, type StudioSourceBundle, type StudioRuntimeIdentity } from "../../../src/studio";
6
+ import { createNodeBundleFileSystem } from "../core/storage";
7
+ import blenderDriver from "../studio/drivers/blender_driver.py" with { type: "text" };
8
+ import cadqueryDriver from "../studio/drivers/cadquery_driver.py" with { type: "text" };
9
+ import manimDriver from "../studio/education/driver.py" with { type: "text" };
10
+ import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
11
+ import { studioBytesSha256, studioJson } from "./studio-files";
12
+ import { studioChildEnvironment, type StudioProcessPort } from "./studio-process";
13
+
14
+ export interface StudioRuntimeSelection { readonly blender?: string; readonly python?: string; readonly threads: number }
15
+ export interface BoundStudioRuntime {
16
+ readonly identity: StudioRuntimeIdentity; readonly executable: string; readonly driver: string;
17
+ readonly environment: Readonly<Record<string, string>>; readonly workingRoot: string; readonly threads: number;
18
+ }
19
+ const drivers = { blender: blenderDriver, cadquery: cadqueryDriver, manim: manimDriver } as const;
20
+ const probeSchema = z.strictObject({
21
+ name: z.string().min(1).max(128), version: z.string().min(1).max(512),
22
+ packages: z.record(z.string().min(1).max(128), z.string().max(512)).refine(value => Object.keys(value).length <= 2048),
23
+ capabilities: z.array(StudioCapabilityNameSchema).max(12),
24
+ });
25
+
26
+ /** Engine commands are fixed. The host selects an installed runtime; serialized jobs never carry argv. */
27
+ export function studioDriverArgv(runtime: Pick<BoundStudioRuntime, "executable" | "driver" | "threads">, engine: StudioSourceBundle["engine"], action: { readonly probe: true } | { readonly request: string }): readonly [string, ...string[]] {
28
+ const tail = "probe" in action ? ["--probe"] : ["--request", action.request];
29
+ return engine === "blender"
30
+ ? [runtime.executable, "--factory-startup", "--background", "--disable-autoexec", "--python-exit-code", "23", "--threads", String(runtime.threads), "--python", runtime.driver, "--", ...tail]
31
+ : [runtime.executable, runtime.driver, ...tail];
32
+ }
33
+
34
+ async function executableIdentity(path: string): Promise<{ readonly executable: string; readonly sha256: string }> {
35
+ // Preserve the selected venv symlink in argv: resolving it changes Python's environment.
36
+ const executable = resolve(path), physical = await realpath(executable), stat = await lstat(physical);
37
+ if (!stat.isFile() || stat.size > 1024 * 1024 * 1024) throw new Error("Studio runtime must be a bounded installed executable.");
38
+ await access(executable, constants.X_OK);
39
+ const fs = createNodeBundleFileSystem(dirname(physical));
40
+ return { executable, sha256: (await fs.inspectFile!(physical.slice(dirname(physical).length + 1), stat.size)).sha256 };
41
+ }
42
+
43
+ export async function probeStudioRuntime(input: {
44
+ readonly engine: StudioSourceBundle["engine"]; readonly selection: StudioRuntimeSelection; readonly workingRoot: string;
45
+ readonly process: StudioProcessPort; readonly signal: AbortSignal; readonly fence: () => Promise<void>;
46
+ readonly inheritedFileDescriptors?: readonly number[];
47
+ }): Promise<BoundStudioRuntime> {
48
+ const selected = input.engine === "blender" ? input.selection.blender : input.selection.python;
49
+ if (selected === undefined) throw new Error(input.engine === "blender" ? "Select an installed Blender runtime with --blender-bin." : "Select an installed Python environment with --python.");
50
+ const executable = await executableIdentity(selected), text = drivers[input.engine], driverSha256 = studioBytesSha256(text);
51
+ for (const path of ["home", "tmp", "blender-user", "driver"]) await ensurePhysicalPrivateDirectoryWithin(input.workingRoot, path);
52
+ const fs = createNodeBundleFileSystem(input.workingRoot), driverPath = `driver/${input.engine}-${driverSha256}.py`;
53
+ const disposition = await fs.writeTextNoReplace!(driverPath, text, input.fence);
54
+ if (disposition !== "created" && await fs.readText(driverPath) !== text) throw new Error("Fixed studio driver bytes changed.");
55
+ const environment = studioChildEnvironment(executable.executable, input.workingRoot, input.selection.threads);
56
+ const bound = { executable: executable.executable, driver: join(input.workingRoot, driverPath), environment, workingRoot: input.workingRoot, threads: input.selection.threads };
57
+ await input.fence();
58
+ const result = await input.process.run(studioDriverArgv(bound, input.engine, { probe: true }), {
59
+ cwd: input.workingRoot, env: environment, timeoutMs: 120_000, maximumLogBytes: 1_048_576, signal: input.signal,
60
+ ...(input.inheritedFileDescriptors === undefined ? {} : { inheritedFileDescriptors: input.inheritedFileDescriptors }),
61
+ });
62
+ if (result.custody !== "closed" || result.failure !== undefined || result.exitCode !== 0) throw new Error("Fixed native runtime probe did not finish with closed process custody.");
63
+ const lines = result.stdout.split(/\r?\n/u).filter(line => line.startsWith("SLOPCAMERA_STUDIO_PROBE="));
64
+ if (lines.length !== 1) throw new Error("Native runtime probe must emit one bounded identity document.");
65
+ const probe = probeSchema.parse(JSON.parse(lines[0]!.slice("SLOPCAMERA_STUDIO_PROBE=".length)) as unknown);
66
+ if (new Set(probe.capabilities).size !== probe.capabilities.length) throw new Error("Native runtime probe repeats a capability.");
67
+ await input.fence();
68
+ const after = await executableIdentity(selected);
69
+ if (executable.sha256 !== after.sha256) throw new Error("Studio executable changed during probing.");
70
+ const identity = parseStudioRuntimeIdentity({
71
+ kind: "slopcamera.studio-runtime", schemaVersion: 1, engine: input.engine,
72
+ tool: { name: probe.name, version: probe.version, executableSha256: executable.sha256 }, driverSha256,
73
+ environment: { fingerprintSha256: studioBytesSha256(studioJson({ platform: process.platform, architecture: process.arch, packages: probe.packages, threads: input.selection.threads })), evidence: "observed-package-environment", hermetic: false },
74
+ capabilities: probe.capabilities.map(name => ({ name, support: "available", evidence: "probe" })),
75
+ });
76
+ return { ...bound, identity };
77
+ }