@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,682 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { lstat, mkdtemp, open, rm } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+
6
+ import {
7
+ MusicAnalysisV1Schema,
8
+ ProjectAnalysisReferenceSchema,
9
+ SpeechAnalysisV1Schema,
10
+ VideoProjectV1Schema,
11
+ type AnalysisSubject,
12
+ type MusicAnalysisV1,
13
+ type ProjectAssetV1,
14
+ type SpeechAnalysisV1,
15
+ type VideoProjectV1,
16
+ } from "../contracts";
17
+ import {
18
+ analyzeSpeech,
19
+ canonicalSlopcameraPersistenceDocument,
20
+ canonicalJson,
21
+ canonicalJsonSha256,
22
+ loadAnalysisArtifact,
23
+ saveAnalysisArtifact,
24
+ saveVideoProject,
25
+ sha256Hex,
26
+ type BundleFileSystem,
27
+ type SpeechWordInput,
28
+ } from "../core";
29
+ import { CliError } from "./errors";
30
+ import type { ProcessRunner } from "./io";
31
+ import { ensurePrivateDirectory } from "./paths";
32
+ import { resolveVerifiedProjectMedia } from "./project-media-integrity";
33
+ import { projectAnalysisPath } from "./project-service";
34
+ import { resolveAudioAnalysisSubject } from "./audio-analysis";
35
+ import { assertCompleteMusicAnalysis } from "./music-analysis-service";
36
+
37
+ export const SLOPCAMERA_WHISPER_CPP_PROFILE = "slopcamera-whisper-cpp-word-timestamps-v1";
38
+ export const SLOPCAMERA_SPEECH_PCM_PROFILE = "pcm-s16le-16000hz-mono-wav-v1";
39
+
40
+ const SPEECH_SAMPLE_RATE_HZ = 16_000;
41
+ const MAXIMUM_PCM_BYTES = 8 * 1024 * 1024 * 1024;
42
+ const MAXIMUM_JSON_BYTES = 64 * 1024 * 1024;
43
+ const MAXIMUM_WORDS = 1_000_000;
44
+ const PROCESS_OUTPUT_BYTES = 1024 * 1024;
45
+
46
+ type AudioStream = Extract<ProjectAssetV1["streams"][number], { readonly kind: "audio" }>;
47
+ export type SpeechAnalysisReference = Extract<
48
+ VideoProjectV1["analyses"][number],
49
+ { readonly kind: "speech" }
50
+ >;
51
+
52
+ export interface WhisperCppRuntime {
53
+ /** A literal executable pathname or command passed directly to the process runner. */
54
+ readonly executable: string;
55
+ readonly modelPath: string;
56
+ /** A caller-probed version string stored in analysis provenance. */
57
+ readonly version: string;
58
+ }
59
+
60
+ export interface LocalSpeechAnalysisConfig {
61
+ readonly language: string;
62
+ readonly minimumFillerConfidence: number;
63
+ readonly processors: number;
64
+ readonly speechHandleUs: number;
65
+ readonly threads: number;
66
+ readonly useGpu: boolean;
67
+ }
68
+
69
+ export interface ParsedWhisperWordTranscript {
70
+ readonly detectedLanguage: string | null;
71
+ readonly words: readonly SpeechWordInput[];
72
+ }
73
+
74
+ export interface LoadedMusicProtection {
75
+ readonly analysis: MusicAnalysisV1;
76
+ readonly reference: Extract<VideoProjectV1["analyses"][number], { readonly kind: "music" }>;
77
+ }
78
+
79
+ export interface LocalSpeechAnalysisResult {
80
+ readonly analysis: SpeechAnalysisV1;
81
+ readonly analysisPath: string;
82
+ readonly musicAnalysisId: string | null;
83
+ readonly reference: SpeechAnalysisReference;
84
+ }
85
+
86
+ export interface RunLocalSpeechAnalysisOptions {
87
+ readonly analysisId?: string;
88
+ readonly config: LocalSpeechAnalysisConfig;
89
+ readonly ffmpeg: string;
90
+ readonly fileSystem?: BundleFileSystem;
91
+ readonly now: Date;
92
+ readonly project: VideoProjectV1;
93
+ readonly projectDirectory: string;
94
+ readonly repositoryRoot: string;
95
+ readonly runner: ProcessRunner;
96
+ readonly runtime: WhisperCppRuntime;
97
+ /** `<asset-id>:<stream-id>` selected by the CLI. */
98
+ readonly source: string;
99
+ readonly useLatestMusicAnalysis?: boolean;
100
+ }
101
+
102
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
104
+ }
105
+
106
+ function nonemptyNulFree(value: unknown): value is string {
107
+ return typeof value === "string" && value.length > 0 && !value.includes("\0");
108
+ }
109
+
110
+ function normalizedLanguage(value: unknown): string | null {
111
+ if (typeof value !== "string") return null;
112
+ const normalized = value.trim().replaceAll("_", "-").toLocaleLowerCase("en-US");
113
+ if (normalized === "auto") return normalized;
114
+ if (!/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/u.test(normalized)) return null;
115
+ return normalized;
116
+ }
117
+
118
+ function whisperLanguage(value: unknown): string | null {
119
+ const normalized = normalizedLanguage(value);
120
+ return normalized === null || normalized === "auto" ? normalized : normalized.split("-")[0] ?? null;
121
+ }
122
+
123
+ function validateConfig(config: LocalSpeechAnalysisConfig): void {
124
+ if (normalizedLanguage(config.language) === null) {
125
+ throw new CliError("usage", "Speech language must be auto or a BCP-47-style language tag.");
126
+ }
127
+ if (!Number.isSafeInteger(config.threads) || config.threads < 1 || config.threads > 256) {
128
+ throw new CliError("usage", "whisper.cpp threads must be an integer from 1 through 256.");
129
+ }
130
+ if (!Number.isSafeInteger(config.processors) || config.processors < 1 || config.processors > 64) {
131
+ throw new CliError("usage", "whisper.cpp processors must be an integer from 1 through 64.");
132
+ }
133
+ if (!Number.isFinite(config.minimumFillerConfidence)
134
+ || config.minimumFillerConfidence < 0
135
+ || config.minimumFillerConfidence > 1) {
136
+ throw new CliError("usage", "Minimum filler confidence must be between zero and one.");
137
+ }
138
+ if (!Number.isSafeInteger(config.speechHandleUs) || config.speechHandleUs < 0) {
139
+ throw new CliError("usage", "Speech handle must be a nonnegative integer number of microseconds.");
140
+ }
141
+ }
142
+
143
+ /** Build the complete whisper.cpp argv used for word-level JSON. No shell string is constructed. */
144
+ export function buildWhisperCppSpeechArgv(options: {
145
+ readonly config: LocalSpeechAnalysisConfig;
146
+ readonly inputWavPath: string;
147
+ readonly outputPrefix: string;
148
+ readonly runtime: WhisperCppRuntime;
149
+ }): readonly [string, ...string[]] {
150
+ validateConfig(options.config);
151
+ for (const value of [
152
+ options.runtime.executable,
153
+ options.runtime.modelPath,
154
+ options.inputWavPath,
155
+ options.outputPrefix,
156
+ ]) {
157
+ if (!nonemptyNulFree(value) || value.length > 8_192) {
158
+ throw new CliError("usage", "whisper.cpp executable, model, and media paths must be bounded, nonempty, and NUL-free.");
159
+ }
160
+ }
161
+ if (!nonemptyNulFree(options.runtime.version) || options.runtime.version.length > 128) {
162
+ throw new CliError("usage", "whisper.cpp version must be a nonempty, NUL-free string of at most 128 characters.");
163
+ }
164
+ const language = whisperLanguage(options.config.language);
165
+ if (language === null) throw new CliError("usage", "Speech language is invalid.");
166
+ return [
167
+ options.runtime.executable,
168
+ "--model", options.runtime.modelPath,
169
+ "--file", options.inputWavPath,
170
+ "--language", language,
171
+ "--threads", String(options.config.threads),
172
+ "--processors", String(options.config.processors),
173
+ ...(options.config.useGpu ? [] : ["--no-gpu"]),
174
+ "--split-on-word",
175
+ "--max-len", "1",
176
+ "--output-json-full",
177
+ "--output-file", options.outputPrefix,
178
+ "--no-prints",
179
+ ];
180
+ }
181
+
182
+ function wordTimestampFailure(reason: string): CliError {
183
+ return new CliError(
184
+ "invalid-data",
185
+ `whisper.cpp word timestamps are unavailable: ${reason}. Use a whisper.cpp build that supports full JSON word output.`,
186
+ );
187
+ }
188
+
189
+ function boundedWordText(value: unknown): string | null {
190
+ if (typeof value !== "string") return null;
191
+ const normalized = value.normalize("NFKC").trim();
192
+ if (normalized.length === 0 || normalized.length > 256 || /[\r\n\0]/u.test(normalized)) return null;
193
+ return normalized;
194
+ }
195
+
196
+ function isSpecialWhisperToken(text: string): boolean {
197
+ const trimmed = text.trim();
198
+ return /^\[_.*_\]$/u.test(trimmed) || /^<\|.*\|>$/u.test(trimmed);
199
+ }
200
+
201
+ function confidenceFromTokens(value: unknown, segmentText: string): number | null {
202
+ if (!Array.isArray(value) || value.length === 0 || value.length > 4_096) return null;
203
+ const lexical: { readonly confidence: number; readonly text: string }[] = [];
204
+ for (const token of value) {
205
+ if (!isRecord(token) || typeof token.text !== "string") return null;
206
+ if (isSpecialWhisperToken(token.text) || token.text.trim() === "") continue;
207
+ if (typeof token.p !== "number" || !Number.isFinite(token.p) || token.p < 0 || token.p > 1) return null;
208
+ lexical.push({ confidence: token.p, text: token.text });
209
+ }
210
+ if (lexical.length === 0) return null;
211
+ const tokenText = lexical.map(token => token.text).join("").normalize("NFKC").replace(/\s+/gu, "");
212
+ if (tokenText !== segmentText.replace(/\s+/gu, "")) return null;
213
+ return Math.min(...lexical.map(token => token.confidence));
214
+ }
215
+
216
+ /**
217
+ * Parse only the `--max-len 1 --split-on-word --output-json-full` shape.
218
+ * Segment-level text containing more than one whitespace-delimited word is
219
+ * rejected instead of being assigned a misleading coarse timestamp.
220
+ */
221
+ export function parseWhisperCppWordJson(
222
+ input: unknown,
223
+ durationUs: number,
224
+ ): ParsedWhisperWordTranscript {
225
+ if (!Number.isSafeInteger(durationUs) || durationUs <= 0) {
226
+ throw new CliError("invalid-data", "Speech analysis duration must be a positive integer number of microseconds.");
227
+ }
228
+ let value = input;
229
+ if (typeof input === "string") {
230
+ if (new TextEncoder().encode(input).byteLength > MAXIMUM_JSON_BYTES) {
231
+ throw wordTimestampFailure("full JSON output exceeds the 64 MiB limit");
232
+ }
233
+ try {
234
+ value = JSON.parse(input) as unknown;
235
+ } catch {
236
+ throw wordTimestampFailure("full JSON output is malformed");
237
+ }
238
+ }
239
+ if (!isRecord(value) || !Array.isArray(value.transcription)) {
240
+ throw wordTimestampFailure("full JSON output has no transcription array");
241
+ }
242
+ if (value.transcription.length > MAXIMUM_WORDS) {
243
+ throw wordTimestampFailure("full JSON output exceeds the one-million-word limit");
244
+ }
245
+ const languageCandidates = [
246
+ value.language,
247
+ isRecord(value.result) ? value.result.language : undefined,
248
+ isRecord(value.transcription_info) ? value.transcription_info.language : undefined,
249
+ ].filter(candidate => candidate !== undefined);
250
+ const languages = languageCandidates.map(candidate => normalizedLanguage(candidate));
251
+ if (languages.some(language => language === null || language === "auto")) {
252
+ throw wordTimestampFailure("detected language is invalid");
253
+ }
254
+ const uniqueLanguages = [...new Set(languages as string[])];
255
+ if (uniqueLanguages.length > 1) throw wordTimestampFailure("detected language fields disagree");
256
+ const words: SpeechWordInput[] = [];
257
+ let priorEndUs = 0;
258
+ for (const segment of value.transcription) {
259
+ if (!isRecord(segment) || !isRecord(segment.offsets)) {
260
+ throw wordTimestampFailure("a transcription item has no numeric offsets");
261
+ }
262
+ const text = boundedWordText(segment.text);
263
+ if (text === null || /\s/u.test(text)) {
264
+ throw wordTimestampFailure("the output contains a segment that is not one word");
265
+ }
266
+ const fromMs = segment.offsets.from;
267
+ const toMs = segment.offsets.to;
268
+ if (!Number.isSafeInteger(fromMs) || !Number.isSafeInteger(toMs)
269
+ || (fromMs as number) < 0 || (toMs as number) <= (fromMs as number)) {
270
+ throw wordTimestampFailure("a word has invalid millisecond offsets");
271
+ }
272
+ const startUs = (fromMs as number) * 1_000;
273
+ const endUs = (toMs as number) * 1_000;
274
+ if (!Number.isSafeInteger(startUs) || !Number.isSafeInteger(endUs)
275
+ || startUs < priorEndUs || endUs > durationUs) {
276
+ throw wordTimestampFailure("word offsets overlap, exceed the asset, or are not safely representable");
277
+ }
278
+ const confidence = confidenceFromTokens(segment.tokens, text);
279
+ if (confidence === null) {
280
+ throw wordTimestampFailure("a word has no matching token probability");
281
+ }
282
+ words.push({ confidence, range: { endUs, startUs }, speaker: null, text });
283
+ priorEndUs = endUs;
284
+ }
285
+ if (words.length > 0 && uniqueLanguages[0] === undefined) {
286
+ throw wordTimestampFailure("full JSON output does not identify the detected language");
287
+ }
288
+ return { detectedLanguage: uniqueLanguages[0] ?? null, words };
289
+ }
290
+
291
+ function seconds(microseconds: number): string {
292
+ return (microseconds / 1_000_000).toFixed(6).replace(/0+$/u, "").replace(/\.$/u, "");
293
+ }
294
+
295
+ async function verifySpeechWavCoverage(
296
+ path: string,
297
+ assetDurationUs: number,
298
+ segmentCount: number,
299
+ maximumBytes: number,
300
+ ): Promise<void> {
301
+ let lexical;
302
+ try {
303
+ lexical = await lstat(path);
304
+ } catch {
305
+ throw new CliError("subprocess", "FFmpeg speech decode did not create its WAV output.");
306
+ }
307
+ if (
308
+ !lexical.isFile()
309
+ || lexical.isSymbolicLink()
310
+ || lexical.size < 44
311
+ || lexical.size > maximumBytes
312
+ || !Number.isSafeInteger(lexical.size)
313
+ ) {
314
+ throw new CliError("invalid-data", "FFmpeg speech decode produced an unsafe or unexpectedly sized WAV output.");
315
+ }
316
+ const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
317
+ try {
318
+ const opened = await handle.stat();
319
+ if (
320
+ !opened.isFile()
321
+ || opened.dev !== lexical.dev
322
+ || opened.ino !== lexical.ino
323
+ || opened.size !== lexical.size
324
+ ) {
325
+ throw new CliError("conflict", "Decoded speech WAV changed before validation.");
326
+ }
327
+ const readExactly = async (length: number, position: number): Promise<Buffer> => {
328
+ const bytes = Buffer.allocUnsafe(length);
329
+ const result = await handle.read(bytes, 0, length, position);
330
+ if (result.bytesRead !== length) {
331
+ throw new CliError("invalid-data", "Decoded speech WAV has a truncated chunk header.");
332
+ }
333
+ return bytes;
334
+ };
335
+ const header = await readExactly(12, 0);
336
+ if (header.toString("ascii", 0, 4) !== "RIFF" || header.toString("ascii", 8, 12) !== "WAVE") {
337
+ throw new CliError("invalid-data", "FFmpeg speech decode did not produce a RIFF/WAVE file.");
338
+ }
339
+ if (header.readUInt32LE(4) + 8 !== opened.size) {
340
+ throw new CliError("invalid-data", "Decoded speech WAV declares an incomplete or trailing RIFF payload.");
341
+ }
342
+ let dataBytes: number | null = null;
343
+ let formatValid = false;
344
+ let position = 12;
345
+ let chunks = 0;
346
+ while (position + 8 <= opened.size && chunks < 256) {
347
+ const chunk = await readExactly(8, position);
348
+ const identifier = chunk.toString("ascii", 0, 4);
349
+ const size = chunk.readUInt32LE(4);
350
+ const payload = position + 8;
351
+ const paddedEnd = payload + size + (size % 2);
352
+ if (!Number.isSafeInteger(paddedEnd) || paddedEnd > opened.size) {
353
+ throw new CliError("invalid-data", "Decoded speech WAV contains a truncated chunk.");
354
+ }
355
+ if (identifier === "fmt ") {
356
+ if (size < 16) throw new CliError("invalid-data", "Decoded speech WAV has an incomplete format chunk.");
357
+ const format = await readExactly(16, payload);
358
+ formatValid = format.readUInt16LE(0) === 1
359
+ && format.readUInt16LE(2) === 1
360
+ && format.readUInt32LE(4) === SPEECH_SAMPLE_RATE_HZ
361
+ && format.readUInt16LE(12) === 2
362
+ && format.readUInt16LE(14) === 16;
363
+ } else if (identifier === "data") {
364
+ dataBytes = size;
365
+ }
366
+ position = paddedEnd;
367
+ chunks += 1;
368
+ }
369
+ if (!formatValid || dataBytes === null || dataBytes % 2 !== 0) {
370
+ throw new CliError("invalid-data", "Decoded speech WAV does not contain 16 kHz mono signed-16 PCM data.");
371
+ }
372
+ const expectedSamples = Math.round(assetDurationUs * SPEECH_SAMPLE_RATE_HZ / 1_000_000);
373
+ const actualSamples = dataBytes / 2;
374
+ const sampleTolerance = Math.max(2, segmentCount * 2);
375
+ if (Math.abs(actualSamples - expectedSamples) > sampleTolerance) {
376
+ throw new CliError(
377
+ "invalid-data",
378
+ "FFmpeg speech decode produced incomplete PCM timeline coverage.",
379
+ { actualSamples, expectedSamples, sampleTolerance },
380
+ );
381
+ }
382
+ const after = await handle.stat();
383
+ if (
384
+ after.dev !== opened.dev
385
+ || after.ino !== opened.ino
386
+ || after.size !== opened.size
387
+ || after.mtimeMs !== opened.mtimeMs
388
+ || after.ctimeMs !== opened.ctimeMs
389
+ ) {
390
+ throw new CliError("conflict", "Decoded speech WAV changed during validation.");
391
+ }
392
+ } finally {
393
+ await handle.close();
394
+ }
395
+ }
396
+
397
+ /** Decode one logical project audio stream, preserving asset-clock gaps. */
398
+ export async function decodeSpeechWav(options: {
399
+ readonly assetDurationUs: number;
400
+ readonly ffmpeg: string;
401
+ readonly outputPath: string;
402
+ readonly repositoryRoot: string;
403
+ readonly runner: ProcessRunner;
404
+ readonly stream: AudioStream;
405
+ }): Promise<void> {
406
+ if (!nonemptyNulFree(options.ffmpeg) || !nonemptyNulFree(options.outputPath)) {
407
+ throw new CliError("usage", "FFmpeg and decoded audio paths must be nonempty and NUL-free.");
408
+ }
409
+ const expectedBytes = Math.ceil(options.assetDurationUs * SPEECH_SAMPLE_RATE_HZ * 2 / 1_000_000) + 1024 * 1024;
410
+ if (!Number.isSafeInteger(expectedBytes) || expectedBytes > MAXIMUM_PCM_BYTES) {
411
+ throw new CliError("invalid-data", "Decoded speech PCM would exceed its 8 GiB limit.");
412
+ }
413
+ const inputArguments: string[] = [];
414
+ const filters: string[] = [];
415
+ const labels: string[] = [];
416
+ let priorEndUs = 0;
417
+ for (const [index, segment] of options.stream.segments.entries()) {
418
+ if (segment.assetRange.startUs > priorEndUs) {
419
+ const gap = `gap_${index}`;
420
+ filters.push(`anullsrc=r=${SPEECH_SAMPLE_RATE_HZ}:cl=mono:d=${seconds(segment.assetRange.startUs - priorEndUs)}[${gap}]`);
421
+ labels.push(gap);
422
+ }
423
+ inputArguments.push("-i", await resolveVerifiedProjectMedia({
424
+ expected: { bytes: segment.bytes, sha256: segment.sha256 },
425
+ label: `Speech segment ${segment.path}:${segment.streamIndex}`,
426
+ path: segment.path,
427
+ repositoryRoot: options.repositoryRoot,
428
+ }));
429
+ const label = `speech_${index}`;
430
+ filters.push(
431
+ `[${index}:${segment.streamIndex}]atrim=start=${seconds(segment.fileRange.startUs)}:end=${seconds(segment.fileRange.endUs)},asetpts=PTS-STARTPTS,aresample=${SPEECH_SAMPLE_RATE_HZ},aformat=sample_fmts=s16:channel_layouts=mono[${label}]`,
432
+ );
433
+ labels.push(label);
434
+ priorEndUs = segment.assetRange.endUs;
435
+ }
436
+ if (priorEndUs < options.assetDurationUs) {
437
+ const gap = "gap_tail";
438
+ filters.push(`anullsrc=r=${SPEECH_SAMPLE_RATE_HZ}:cl=mono:d=${seconds(options.assetDurationUs - priorEndUs)}[${gap}]`);
439
+ labels.push(gap);
440
+ }
441
+ if (labels.length === 0) throw new CliError("invalid-data", `Audio stream ${options.stream.streamId} has no media segments.`);
442
+ filters.push(`${labels.map(label => `[${label}]`).join("")}concat=n=${labels.length}:v=0:a=1[decoded]`);
443
+ const result = await options.runner.run([
444
+ options.ffmpeg,
445
+ "-hide_banner", "-nostdin", "-y",
446
+ ...inputArguments,
447
+ "-filter_complex", filters.join(";"),
448
+ "-map", "[decoded]",
449
+ "-vn", "-c:a", "pcm_s16le", "-ac", "1", "-ar", String(SPEECH_SAMPLE_RATE_HZ),
450
+ "-t", seconds(options.assetDurationUs),
451
+ "-fs", String(expectedBytes),
452
+ options.outputPath,
453
+ ], { maxOutputBytes: PROCESS_OUTPUT_BYTES });
454
+ if (result.exitCode !== 0) {
455
+ throw new CliError("subprocess", `FFmpeg speech decode failed: ${result.stderr.trim().slice(-4_000) || `exit ${result.exitCode}`}`);
456
+ }
457
+ await verifySpeechWavCoverage(
458
+ options.outputPath,
459
+ options.assetDurationUs,
460
+ options.stream.segments.length,
461
+ expectedBytes,
462
+ );
463
+ }
464
+
465
+ async function readBoundedOwnedText(path: string, maximumBytes: number): Promise<string> {
466
+ try {
467
+ const before = await lstat(path);
468
+ if (!before.isFile() || before.isSymbolicLink() || before.size <= 0) {
469
+ throw wordTimestampFailure("full JSON output is missing or unsafe");
470
+ }
471
+ if (before.size > maximumBytes) throw wordTimestampFailure("full JSON output exceeds the 64 MiB limit");
472
+ const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
473
+ try {
474
+ const opened = await handle.stat();
475
+ if (!opened.isFile() || opened.size !== before.size || opened.dev !== before.dev || opened.ino !== before.ino) {
476
+ throw wordTimestampFailure("full JSON output changed before it could be read");
477
+ }
478
+ const bytes = await handle.readFile();
479
+ const after = await handle.stat();
480
+ if (bytes.byteLength !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) {
481
+ throw wordTimestampFailure("full JSON output changed while it was being read");
482
+ }
483
+ try {
484
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
485
+ } catch {
486
+ throw wordTimestampFailure("full JSON output is not valid UTF-8");
487
+ }
488
+ } finally {
489
+ await handle.close();
490
+ }
491
+ } catch (error) {
492
+ if (error instanceof CliError) throw error;
493
+ throw wordTimestampFailure("full JSON output could not be read safely");
494
+ }
495
+ }
496
+
497
+ /** Load and integrity-check the newest music artifact for the exact current stream. */
498
+ export async function loadLatestMusicProtectionAnalysis(options: {
499
+ readonly fileSystem: BundleFileSystem;
500
+ readonly project: VideoProjectV1;
501
+ readonly subject: AnalysisSubject;
502
+ }): Promise<LoadedMusicProtection | null> {
503
+ const reference = [...options.project.analyses]
504
+ .filter((candidate): candidate is Extract<VideoProjectV1["analyses"][number], { readonly kind: "music" }> =>
505
+ candidate.kind === "music"
506
+ && candidate.assetId === options.subject.assetId
507
+ && candidate.streamId === options.subject.streamId)
508
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt)
509
+ || String(right.analysisId).localeCompare(String(left.analysisId)))[0];
510
+ if (reference === undefined) return null;
511
+ let analysis: MusicAnalysisV1;
512
+ try {
513
+ analysis = MusicAnalysisV1Schema.parse(await loadAnalysisArtifact(options.fileSystem, reference.path));
514
+ } catch (error) {
515
+ throw new CliError("invalid-data", `Latest music analysis ${reference.analysisId} cannot be loaded: ${String(error)}`);
516
+ }
517
+ if (analysis.analysisId !== reference.analysisId
518
+ || analysis.subject.assetId !== options.subject.assetId
519
+ || analysis.subject.streamId !== options.subject.streamId
520
+ || analysis.subject.integritySha256 !== options.subject.integritySha256) {
521
+ throw new CliError("conflict", `Latest music analysis ${reference.analysisId} is stale for the selected audio stream.`);
522
+ }
523
+ const asset = options.project.assets.find(candidate => candidate.assetId === options.subject.assetId);
524
+ const stream = asset?.streams.find(candidate => candidate.streamId === options.subject.streamId);
525
+ if (stream?.kind !== "audio") {
526
+ throw new CliError("invalid-data", `Music analysis ${reference.analysisId} has no current audio stream.`);
527
+ }
528
+ assertCompleteMusicAnalysis(analysis, stream);
529
+ const artifactSha256 = sha256Hex(`${canonicalJson(analysis)}\n`);
530
+ if (artifactSha256 !== reference.sha256) {
531
+ throw new CliError("invalid-data", `Latest music analysis ${reference.analysisId} failed its project-reference integrity check.`);
532
+ }
533
+ return { analysis, reference };
534
+ }
535
+
536
+ export function createSpeechAnalysisReference(
537
+ analysis: SpeechAnalysisV1,
538
+ path = projectAnalysisPath("speech", analysis.analysisId),
539
+ ): SpeechAnalysisReference {
540
+ const validated = SpeechAnalysisV1Schema.parse(analysis);
541
+ return ProjectAnalysisReferenceSchema.parse({
542
+ analysisId: validated.analysisId,
543
+ assetId: validated.subject.assetId,
544
+ createdAt: validated.createdAt,
545
+ fillerCount: validated.result.status === "transcribed" ? validated.result.fillers.length : 0,
546
+ kind: "speech",
547
+ path,
548
+ sha256: sha256Hex(`${canonicalJson(validated)}\n`),
549
+ streamId: validated.subject.streamId,
550
+ wordCount: validated.result.status === "transcribed" ? validated.result.words.length : 0,
551
+ }) as SpeechAnalysisReference;
552
+ }
553
+
554
+ /** Decode, transcribe, parse, and analyze speech while always deleting temporary media. */
555
+ export async function runLocalSpeechAnalysis(
556
+ options: RunLocalSpeechAnalysisOptions,
557
+ ): Promise<LocalSpeechAnalysisResult> {
558
+ validateConfig(options.config);
559
+ const selected = resolveAudioAnalysisSubject(options.project, options.source);
560
+ if (options.useLatestMusicAnalysis && options.fileSystem === undefined) {
561
+ throw new CliError("internal", "A bundle filesystem is required to load music protection analysis.");
562
+ }
563
+ const music = options.useLatestMusicAnalysis
564
+ ? await loadLatestMusicProtectionAnalysis({
565
+ fileSystem: options.fileSystem!,
566
+ project: options.project,
567
+ subject: selected.subject,
568
+ })
569
+ : null;
570
+ const cacheRoot = join(options.projectDirectory, "analysis", "cache");
571
+ await ensurePrivateDirectory(join(options.projectDirectory, "analysis"));
572
+ await ensurePrivateDirectory(cacheRoot);
573
+ const workDirectory = await mkdtemp(join(cacheRoot, "speech-"));
574
+ const pcmPath = join(workDirectory, "input.wav");
575
+ const outputPrefix = join(workDirectory, "transcript");
576
+ try {
577
+ await decodeSpeechWav({
578
+ assetDurationUs: selected.asset.durationUs,
579
+ ffmpeg: options.ffmpeg,
580
+ outputPath: pcmPath,
581
+ repositoryRoot: options.repositoryRoot,
582
+ runner: options.runner,
583
+ stream: selected.stream,
584
+ });
585
+ const argv = buildWhisperCppSpeechArgv({
586
+ config: options.config,
587
+ inputWavPath: pcmPath,
588
+ outputPrefix,
589
+ runtime: options.runtime,
590
+ });
591
+ const process = await options.runner.run(argv, { maxOutputBytes: PROCESS_OUTPUT_BYTES });
592
+ if (process.exitCode !== 0) {
593
+ throw new CliError(
594
+ "subprocess",
595
+ `whisper.cpp speech analysis failed: ${process.stderr.trim().slice(-4_000) || `exit ${process.exitCode}`}`,
596
+ );
597
+ }
598
+ const transcript = parseWhisperCppWordJson(
599
+ await readBoundedOwnedText(`${outputPrefix}.json`, MAXIMUM_JSON_BYTES),
600
+ selected.asset.durationUs,
601
+ );
602
+ const analysisId = options.analysisId ?? `analysis_${randomUUID().replaceAll("-", "")}`;
603
+ const createdAt = options.now.toISOString();
604
+ const requestedLanguage = normalizedLanguage(options.config.language)!;
605
+ if (requestedLanguage !== "auto" && transcript.detectedLanguage !== null
606
+ && whisperLanguage(requestedLanguage) !== whisperLanguage(transcript.detectedLanguage)) {
607
+ throw wordTimestampFailure("detected language does not match the requested language");
608
+ }
609
+ const analysis = analyzeSpeech({
610
+ analysisId,
611
+ config: {
612
+ language: requestedLanguage,
613
+ minimumFillerConfidence: options.config.minimumFillerConfidence,
614
+ speechHandleUs: options.config.speechHandleUs,
615
+ },
616
+ createdAt,
617
+ detectedLanguage: transcript.detectedLanguage
618
+ ?? (requestedLanguage === "auto" ? "" : requestedLanguage),
619
+ durationUs: selected.asset.durationUs,
620
+ inputDigest: canonicalJsonSha256({
621
+ config: options.config,
622
+ musicAnalysisId: music?.analysis.analysisId ?? null,
623
+ pcmProfile: SLOPCAMERA_SPEECH_PCM_PROFILE,
624
+ runtime: {
625
+ executable: canonicalJsonSha256(options.runtime.executable),
626
+ model: canonicalJsonSha256(options.runtime.modelPath),
627
+ version: options.runtime.version,
628
+ },
629
+ subject: selected.subject,
630
+ transcript,
631
+ whisperProfile: SLOPCAMERA_WHISPER_CPP_PROFILE,
632
+ }),
633
+ musicRegions: music?.analysis.musicRegions ?? [],
634
+ subject: selected.subject,
635
+ tool: {
636
+ name: "whisper.cpp",
637
+ profile: SLOPCAMERA_WHISPER_CPP_PROFILE,
638
+ version: options.runtime.version,
639
+ },
640
+ words: transcript.words,
641
+ });
642
+ const validated = SpeechAnalysisV1Schema.parse(analysis);
643
+ const analysisPath = projectAnalysisPath("speech", validated.analysisId);
644
+ return {
645
+ analysis: validated,
646
+ analysisPath,
647
+ musicAnalysisId: music?.analysis.analysisId ?? null,
648
+ reference: createSpeechAnalysisReference(validated, analysisPath),
649
+ };
650
+ } finally {
651
+ await rm(workDirectory, { force: true, recursive: true });
652
+ }
653
+ }
654
+
655
+ /** Persist an already validated result and return the next validated project. */
656
+ export async function persistSpeechAnalysis(options: {
657
+ readonly fileSystem: BundleFileSystem;
658
+ readonly project: VideoProjectV1;
659
+ readonly result: LocalSpeechAnalysisResult;
660
+ readonly updatedAt?: Date;
661
+ }): Promise<VideoProjectV1> {
662
+ const analysis = SpeechAnalysisV1Schema.parse(options.result.analysis);
663
+ const reference = createSpeechAnalysisReference(analysis, options.result.analysisPath);
664
+ if (canonicalJson(reference) !== canonicalJson(options.result.reference)) {
665
+ throw new CliError("invalid-data", "Speech analysis reference does not match its validated artifact.");
666
+ }
667
+ const requestedTimestamp = (options.updatedAt ?? new Date(analysis.createdAt)).toISOString();
668
+ const timestamp = requestedTimestamp.localeCompare(options.project.updatedAt) < 0
669
+ ? options.project.updatedAt
670
+ : requestedTimestamp;
671
+ const nextProject = canonicalSlopcameraPersistenceDocument(VideoProjectV1Schema.parse({
672
+ ...options.project,
673
+ analyses: [
674
+ ...options.project.analyses.filter(existing => existing.analysisId !== analysis.analysisId),
675
+ reference,
676
+ ],
677
+ updatedAt: timestamp,
678
+ }));
679
+ await saveAnalysisArtifact(options.fileSystem, analysis, options.result.analysisPath);
680
+ await saveVideoProject(options.fileSystem, nextProject);
681
+ return nextProject;
682
+ }