@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.
- package/LICENSE +21 -0
- package/NOTICE.md +60 -0
- package/PRIVACY.md +46 -0
- package/README.md +311 -0
- package/SECURITY.md +21 -0
- package/apps/desktop/README.md +966 -0
- package/apps/desktop/analysis/protocol.ts +223 -0
- package/apps/desktop/application/capability-binding.ts +594 -0
- package/apps/desktop/application/context.ts +97 -0
- package/apps/desktop/application/creative-iteration.ts +1385 -0
- package/apps/desktop/application/default-registry.ts +146 -0
- package/apps/desktop/application/directing-contract.ts +218 -0
- package/apps/desktop/application/directing-plan.ts +178 -0
- package/apps/desktop/application/errors.ts +18 -0
- package/apps/desktop/application/gateway-port.ts +867 -0
- package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
- package/apps/desktop/application/html-overlay-integrity.ts +337 -0
- package/apps/desktop/application/html-overlay-renderer.ts +45 -0
- package/apps/desktop/application/index.ts +13 -0
- package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
- package/apps/desktop/application/operation-effects.ts +94 -0
- package/apps/desktop/application/operation.ts +224 -0
- package/apps/desktop/application/operations/analysis/faces.ts +244 -0
- package/apps/desktop/application/operations/analysis/music.ts +248 -0
- package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
- package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
- package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
- package/apps/desktop/application/operations/analysis/shared.ts +202 -0
- package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
- package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
- package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
- package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
- package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
- package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
- package/apps/desktop/application/operations/gateway/image.ts +41 -0
- package/apps/desktop/application/operations/gateway/index.ts +17 -0
- package/apps/desktop/application/operations/gateway/shared.ts +122 -0
- package/apps/desktop/application/operations/gateway/speech.ts +41 -0
- package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
- package/apps/desktop/application/operations/gateway/video.ts +41 -0
- package/apps/desktop/application/operations/index.ts +31 -0
- package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
- package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
- package/apps/desktop/application/operations/iteration/select.ts +155 -0
- package/apps/desktop/application/operations/iteration/shared.ts +250 -0
- package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
- package/apps/desktop/application/operations/media/binding.ts +79 -0
- package/apps/desktop/application/operations/media/capabilities.ts +112 -0
- package/apps/desktop/application/operations/media/color-grade.ts +343 -0
- package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
- package/apps/desktop/application/operations/media/index.ts +8 -0
- package/apps/desktop/application/operations/media/ingest.ts +303 -0
- package/apps/desktop/application/operations/media/overlay.ts +729 -0
- package/apps/desktop/application/operations/media/shared.ts +845 -0
- package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
- package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
- package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
- package/apps/desktop/application/operations/project/snapshot.ts +95 -0
- package/apps/desktop/application/operations/recording/index.ts +5 -0
- package/apps/desktop/application/operations/recording/pause.ts +34 -0
- package/apps/desktop/application/operations/recording/resume.ts +34 -0
- package/apps/desktop/application/operations/recording/shared.ts +199 -0
- package/apps/desktop/application/operations/recording/start.ts +169 -0
- package/apps/desktop/application/operations/recording/stop.ts +34 -0
- package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
- package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
- package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
- package/apps/desktop/application/operations/render/project-plan.ts +939 -0
- package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
- package/apps/desktop/application/operations/render/project.ts +1922 -0
- package/apps/desktop/application/operations/shared.ts +67 -0
- package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
- package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
- package/apps/desktop/application/operations/spatial-project.ts +209 -0
- package/apps/desktop/application/operations/spatial-render.ts +415 -0
- package/apps/desktop/application/operations/spatial-scene.ts +97 -0
- package/apps/desktop/application/operations/studio.ts +143 -0
- package/apps/desktop/application/output-publication-lease.ts +163 -0
- package/apps/desktop/application/project-publication-lease.ts +233 -0
- package/apps/desktop/application/project-store.ts +196 -0
- package/apps/desktop/application/receipts.ts +533 -0
- package/apps/desktop/application/registry.ts +182 -0
- package/apps/desktop/application/spatial-assets.ts +675 -0
- package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
- package/apps/desktop/application/spatial-project-authority.ts +35 -0
- package/apps/desktop/application/spatial-project-lease.ts +126 -0
- package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
- package/apps/desktop/application/spatial-project-store.ts +286 -0
- package/apps/desktop/application/spatial-render-projection.ts +201 -0
- package/apps/desktop/application/spatial-render.ts +677 -0
- package/apps/desktop/application/spatial-spz.ts +44 -0
- package/apps/desktop/application/spatial-world-import.ts +135 -0
- package/apps/desktop/application/spatial-world-provenance.ts +46 -0
- package/apps/desktop/application/studio-port.ts +58 -0
- package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
- package/apps/desktop/capture/protocol.ts +689 -0
- package/apps/desktop/cli/analyzer.ts +406 -0
- package/apps/desktop/cli/args.ts +3181 -0
- package/apps/desktop/cli/asset-ingest.ts +747 -0
- package/apps/desktop/cli/atomic-render-effects.ts +210 -0
- package/apps/desktop/cli/atomic-render-platform.ts +205 -0
- package/apps/desktop/cli/atomic-render.ts +50 -0
- package/apps/desktop/cli/audio-analysis.ts +252 -0
- package/apps/desktop/cli/bounded-map.ts +39 -0
- package/apps/desktop/cli/bundle-service.ts +256 -0
- package/apps/desktop/cli/capabilities.ts +212 -0
- package/apps/desktop/cli/capture-bundle.ts +1117 -0
- package/apps/desktop/cli/command-host-resources.ts +251 -0
- package/apps/desktop/cli/commands.ts +6836 -0
- package/apps/desktop/cli/directing-blob.ts +291 -0
- package/apps/desktop/cli/directing-media.ts +517 -0
- package/apps/desktop/cli/directing-quote.ts +89 -0
- package/apps/desktop/cli/directing-service.ts +288 -0
- package/apps/desktop/cli/emoji-assets.ts +538 -0
- package/apps/desktop/cli/errors.ts +29 -0
- package/apps/desktop/cli/face-analysis-service.ts +940 -0
- package/apps/desktop/cli/filter-script.ts +76 -0
- package/apps/desktop/cli/gateway-application-port.ts +1142 -0
- package/apps/desktop/cli/gateway-credential.ts +81 -0
- package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
- package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
- package/apps/desktop/cli/gateway-media-service.ts +3106 -0
- package/apps/desktop/cli/gateway-media-signature.ts +122 -0
- package/apps/desktop/cli/gateway-provider-options.ts +562 -0
- package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
- package/apps/desktop/cli/help.ts +501 -0
- package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
- package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
- package/apps/desktop/cli/io.ts +290 -0
- package/apps/desktop/cli/main.ts +123 -0
- package/apps/desktop/cli/media-effects-service.ts +885 -0
- package/apps/desktop/cli/media-ingest-model.ts +364 -0
- package/apps/desktop/cli/media-ingest-platform.ts +227 -0
- package/apps/desktop/cli/media-ingest-program.ts +200 -0
- package/apps/desktop/cli/media-ingest.ts +30 -0
- package/apps/desktop/cli/music-analysis-service.ts +434 -0
- package/apps/desktop/cli/mutation-lock.ts +530 -0
- package/apps/desktop/cli/native-assets.d.ts +12 -0
- package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
- package/apps/desktop/cli/overlay-playback.ts +177 -0
- package/apps/desktop/cli/paths.ts +265 -0
- package/apps/desktop/cli/portable-surface.ts +242 -0
- package/apps/desktop/cli/project-camera-receipt.ts +112 -0
- package/apps/desktop/cli/project-face-camera.ts +394 -0
- package/apps/desktop/cli/project-inactivity-service.ts +552 -0
- package/apps/desktop/cli/project-media-integrity.ts +121 -0
- package/apps/desktop/cli/project-renderer.ts +1526 -0
- package/apps/desktop/cli/project-service.ts +519 -0
- package/apps/desktop/cli/project-state-transaction.ts +520 -0
- package/apps/desktop/cli/query-limits.ts +1 -0
- package/apps/desktop/cli/recording-controller.ts +1567 -0
- package/apps/desktop/cli/recording-daemon.ts +931 -0
- package/apps/desktop/cli/recording-ref.ts +56 -0
- package/apps/desktop/cli/renamed-environment.ts +6 -0
- package/apps/desktop/cli/renderer.ts +1439 -0
- package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
- package/apps/desktop/cli/scene-analysis-service.ts +879 -0
- package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
- package/apps/desktop/cli/spatial-project-service.ts +90 -0
- package/apps/desktop/cli/spatial-scene-service.ts +105 -0
- package/apps/desktop/cli/spatial-world-service.ts +39 -0
- package/apps/desktop/cli/speech-analysis-service.ts +682 -0
- package/apps/desktop/cli/studio-assemble.ts +58 -0
- package/apps/desktop/cli/studio-command.ts +60 -0
- package/apps/desktop/cli/studio-custody.ts +60 -0
- package/apps/desktop/cli/studio-encode.ts +216 -0
- package/apps/desktop/cli/studio-exr.ts +52 -0
- package/apps/desktop/cli/studio-files.ts +179 -0
- package/apps/desktop/cli/studio-output-validation.ts +162 -0
- package/apps/desktop/cli/studio-process.ts +145 -0
- package/apps/desktop/cli/studio-runtime.ts +77 -0
- package/apps/desktop/cli/studio-scaffold.ts +70 -0
- package/apps/desktop/cli/studio-service.ts +301 -0
- package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
- package/apps/desktop/cli/studio-template-names.ts +3 -0
- package/apps/desktop/cli/studio-workflow.ts +19 -0
- package/apps/desktop/cli/time.ts +25 -0
- package/apps/desktop/cli/workflow-code.ts +584 -0
- package/apps/desktop/cli/workflow-runs.ts +576 -0
- package/apps/desktop/code/advanced.ts +139 -0
- package/apps/desktop/code/application-node-planner.ts +1288 -0
- package/apps/desktop/code/compiler.ts +329 -0
- package/apps/desktop/code/contracts.ts +13 -0
- package/apps/desktop/code/creative-recipes.ts +94 -0
- package/apps/desktop/code/define-workflow.ts +115 -0
- package/apps/desktop/code/editing.ts +284 -0
- package/apps/desktop/code/file-candidate-provenance.ts +420 -0
- package/apps/desktop/code/file-candidate.ts +69 -0
- package/apps/desktop/code/graph-builder.ts +10 -0
- package/apps/desktop/code/host-resource-policy.ts +55 -0
- package/apps/desktop/code/plan-contracts.ts +159 -0
- package/apps/desktop/code/planning.ts +361 -0
- package/apps/desktop/code/public.ts +134 -0
- package/apps/desktop/code/reference-projection.ts +62 -0
- package/apps/desktop/code/run-contracts.ts +466 -0
- package/apps/desktop/code/run-store.ts +2478 -0
- package/apps/desktop/code/runtime-identity.ts +328 -0
- package/apps/desktop/code/scheduler.ts +3067 -0
- package/apps/desktop/code/semantic-builder.ts +1436 -0
- package/apps/desktop/code/source-bundle.ts +1641 -0
- package/apps/desktop/code/source-typecheck.ts +211 -0
- package/apps/desktop/code/testing.ts +79 -0
- package/apps/desktop/code/worker-client.ts +1675 -0
- package/apps/desktop/code/worker-effects.ts +141 -0
- package/apps/desktop/code/worker-entry.ts +435 -0
- package/apps/desktop/code/worker-lease-guardian.ts +179 -0
- package/apps/desktop/code/worker-process-identity.ts +484 -0
- package/apps/desktop/code/worker-protocol.ts +211 -0
- package/apps/desktop/code/workflow-effects.ts +132 -0
- package/apps/desktop/contracts/analysis.ts +949 -0
- package/apps/desktop/contracts/edit.ts +576 -0
- package/apps/desktop/contracts/index.ts +9 -0
- package/apps/desktop/contracts/media-effects.ts +139 -0
- package/apps/desktop/contracts/project-render.ts +644 -0
- package/apps/desktop/contracts/project.ts +740 -0
- package/apps/desktop/contracts/recording.ts +1434 -0
- package/apps/desktop/contracts/render.ts +334 -0
- package/apps/desktop/contracts/runtime.ts +155 -0
- package/apps/desktop/contracts/spatial-compositor.ts +58 -0
- package/apps/desktop/contracts/spatial-project.ts +159 -0
- package/apps/desktop/contracts/spatial-world.ts +48 -0
- package/apps/desktop/contracts/video-effects.ts +120 -0
- package/apps/desktop/core/alignment-apply.ts +400 -0
- package/apps/desktop/core/audio-alignment.ts +466 -0
- package/apps/desktop/core/auto-zoom.ts +209 -0
- package/apps/desktop/core/canonical-json.ts +14 -0
- package/apps/desktop/core/emoji.ts +210 -0
- package/apps/desktop/core/events.ts +409 -0
- package/apps/desktop/core/face-framing.ts +533 -0
- package/apps/desktop/core/face-tracks.ts +326 -0
- package/apps/desktop/core/inactivity.ts +137 -0
- package/apps/desktop/core/index.ts +30 -0
- package/apps/desktop/core/intervals.ts +123 -0
- package/apps/desktop/core/montage.ts +236 -0
- package/apps/desktop/core/music-analysis.ts +518 -0
- package/apps/desktop/core/plan.ts +244 -0
- package/apps/desktop/core/project-auto-zoom.ts +75 -0
- package/apps/desktop/core/project-camera-spatial.ts +676 -0
- package/apps/desktop/core/project-camera.ts +403 -0
- package/apps/desktop/core/project-captions.ts +888 -0
- package/apps/desktop/core/project-inactivity.ts +125 -0
- package/apps/desktop/core/project-metadata.ts +927 -0
- package/apps/desktop/core/project-plan.ts +375 -0
- package/apps/desktop/core/project-render-plan.ts +303 -0
- package/apps/desktop/core/project-time.ts +469 -0
- package/apps/desktop/core/render-plan.ts +442 -0
- package/apps/desktop/core/scene-cache.ts +174 -0
- package/apps/desktop/core/scene-sampling.ts +448 -0
- package/apps/desktop/core/spatial-compositor.ts +22 -0
- package/apps/desktop/core/spatial-durability.ts +99 -0
- package/apps/desktop/core/spatial-project.ts +169 -0
- package/apps/desktop/core/spatial-shot-clock.ts +32 -0
- package/apps/desktop/core/speech-analysis.ts +333 -0
- package/apps/desktop/core/storage.ts +970 -0
- package/apps/desktop/core/time-map.ts +91 -0
- package/apps/desktop/core/time.ts +61 -0
- package/apps/desktop/core/video-effects.ts +660 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
- package/apps/desktop/dist/cli/main.js +5086 -0
- package/apps/desktop/html-overlay/catalog.ts +171 -0
- package/apps/desktop/html-overlay/contracts.ts +372 -0
- package/apps/desktop/html-overlay/execution-profile.ts +77 -0
- package/apps/desktop/html-overlay/index.ts +98 -0
- package/apps/desktop/html-overlay/libraries.ts +263 -0
- package/apps/desktop/html-overlay/random.ts +49 -0
- package/apps/desktop/html-overlay/runtime.ts +813 -0
- package/apps/desktop/html-overlay/scaffolds.ts +792 -0
- package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
- package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
- package/apps/desktop/html-overlay/spatial.ts +720 -0
- package/apps/desktop/studio/assets/README.md +39 -0
- package/apps/desktop/studio/assets/contracts.ts +129 -0
- package/apps/desktop/studio/assets/network.ts +64 -0
- package/apps/desktop/studio/assets/poly-haven.ts +148 -0
- package/apps/desktop/studio/drivers/blender_driver.py +466 -0
- package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
- package/apps/desktop/studio/education/driver.py +308 -0
- package/apps/desktop/studio/education/lesson.py +180 -0
- package/apps/desktop/studio/education/toolkit.py +249 -0
- package/apps/desktop/workflows/chaptered-demo.ts +48 -0
- package/apps/desktop/workflows/creative-iteration.ts +107 -0
- package/apps/desktop/workflows/creative-selection.ts +92 -0
- package/apps/desktop/workflows/fragments.ts +486 -0
- package/apps/desktop/workflows/index.ts +93 -0
- package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
- package/apps/desktop/workflows/scene-direction.ts +12 -0
- package/apps/desktop/workflows/social-variants.ts +75 -0
- package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
- package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
- package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
- package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
- package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
- package/dist/cli.js +479 -0
- package/dist/code/advanced.js +270 -0
- package/dist/code/index.js +2240 -0
- package/dist/generate.js +24 -0
- package/dist/host-resources.js +26 -0
- package/dist/index-42zsesc1.js +2257 -0
- package/dist/index-7308egqr.js +48 -0
- package/dist/index-8txs6fkn.js +712 -0
- package/dist/index-mcy8z0br.js +192 -0
- package/dist/index-p63wavx0.js +2029 -0
- package/dist/index-r7gdhmsp.js +424 -0
- package/dist/index-sh6xbav6.js +1219 -0
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index-z7239b4h.js +2363 -0
- package/dist/index-zfeg9hmw.js +2013 -0
- package/dist/index.js +241 -0
- package/dist/operations.js +29 -0
- package/dist/skill-install-dh0ntqba.js +10 -0
- package/dist/vectorize/worker.js +142 -0
- package/dist/workflow.js +16 -0
- package/docs/README.md +38 -0
- package/docs/studio.md +215 -0
- package/examples/studio/blender/README.md +26 -0
- package/examples/studio/blender/character.py +152 -0
- package/examples/studio/blender/cloth.py +101 -0
- package/examples/studio/blender/color_chart.py +29 -0
- package/examples/studio/blender/fluid.py +78 -0
- package/examples/studio/blender/import_model.py +74 -0
- package/examples/studio/blender/product.py +53 -0
- package/examples/studio/blender/shaded_street.py +481 -0
- package/examples/studio/blender/studio_scene.py +147 -0
- package/examples/studio/cadquery/README.md +13 -0
- package/examples/studio/cadquery/bracket.py +36 -0
- package/examples/studio/cadquery/import_step.py +12 -0
- package/examples/studio/education/README.md +46 -0
- package/examples/studio/education/lesson.json +99 -0
- package/examples/studio/education/scene.py +7 -0
- package/examples/studio/hybrid-scene.ts +96 -0
- package/examples/studio/native-workflow.ts +14 -0
- package/examples/studio/vgpu/README.md +64 -0
- package/examples/studio/vgpu/heat-field.mjs +290 -0
- package/package.json +304 -0
- package/packages/scene/src/index.ts +1 -0
- package/packages/scene/src/scene.ts +215 -0
- package/schema/diagram.schema.json +334 -0
- package/skills/slopcamera/SKILL.md +45 -0
- package/skills/slopcamera/agents/openai.yaml +4 -0
- package/skills/slopcamera/references/customization.md +77 -0
- package/skills/slopcamera/references/diagrams.md +153 -0
- package/skills/slopcamera/references/directed-scenes.md +42 -0
- package/skills/slopcamera/references/directing-video.md +18 -0
- package/skills/slopcamera/references/educational-video.md +26 -0
- package/skills/slopcamera/references/gateway-media.md +134 -0
- package/skills/slopcamera/references/install.md +62 -0
- package/skills/slopcamera/references/native-studio.md +31 -0
- package/skills/slopcamera/references/reference-led-3d.md +140 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
- package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
- package/skills/slopcamera/references/vectorization.md +29 -0
- package/skills/slopcamera/references/video-projects.md +187 -0
- package/skills/slopcamera/references/visual-communication.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +45 -0
- package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
- package/src/artifacts.ts +106 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
- package/src/assets.d.ts +9 -0
- package/src/cli.ts +601 -0
- package/src/cloud-errors.ts +26 -0
- package/src/code/advanced.ts +10 -0
- package/src/code/boundary.ts +21 -0
- package/src/code/canonical-json.ts +129 -0
- package/src/code/compiler.ts +953 -0
- package/src/code/contracts.ts +576 -0
- package/src/code/define-workflow.ts +334 -0
- package/src/code/errors.ts +41 -0
- package/src/code/graph-builder.ts +945 -0
- package/src/code/index.ts +121 -0
- package/src/code/json-snapshot.ts +899 -0
- package/src/code/json-utf8.ts +88 -0
- package/src/code/portable-builder.ts +136 -0
- package/src/code/projection.ts +378 -0
- package/src/code/public-operations.ts +450 -0
- package/src/code/runtime.ts +580 -0
- package/src/code/sha256.ts +36 -0
- package/src/config.ts +180 -0
- package/src/desktop.ts +253 -0
- package/src/fs.ts +10 -0
- package/src/generate.ts +704 -0
- package/src/host-resource-posix.ts +161 -0
- package/src/host-resources.ts +1749 -0
- package/src/icons.ts +58 -0
- package/src/index.ts +200 -0
- package/src/label-layout.ts +130 -0
- package/src/layout.ts +413 -0
- package/src/lint.ts +127 -0
- package/src/mcp/boundary.ts +354 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/server.ts +272 -0
- package/src/mcp/tools.ts +963 -0
- package/src/mcp/types.ts +71 -0
- package/src/operations.ts +775 -0
- package/src/parse.ts +861 -0
- package/src/process-environment.ts +17 -0
- package/src/render.ts +437 -0
- package/src/skill-install.ts +49 -0
- package/src/spatial-scene/authoring.ts +27 -0
- package/src/spatial-scene/camera-track.ts +83 -0
- package/src/spatial-scene/contracts.ts +277 -0
- package/src/spatial-scene/evaluate.ts +145 -0
- package/src/spatial-scene/gltf.ts +558 -0
- package/src/spatial-scene/identity.ts +260 -0
- package/src/spatial-scene/index.ts +10 -0
- package/src/spatial-scene/inspect.ts +68 -0
- package/src/spatial-scene/math.ts +280 -0
- package/src/spatial-scene/patch.ts +131 -0
- package/src/spatial-scene/time.ts +53 -0
- package/src/studio/contracts.ts +129 -0
- package/src/studio/index.ts +2 -0
- package/src/studio/plan.ts +156 -0
- package/src/studio/shared.ts +41 -0
- package/src/theme.ts +68 -0
- package/src/tldr.ts +474 -0
- package/src/types.ts +212 -0
- package/src/vectorize/archive.ts +101 -0
- package/src/vectorize/command.ts +797 -0
- package/src/vectorize/index.ts +27 -0
- package/src/vectorize/limits.ts +61 -0
- package/src/vectorize/metrics.ts +351 -0
- package/src/vectorize/pixels.ts +323 -0
- package/src/vectorize/supervisor.ts +339 -0
- package/src/vectorize/svg.ts +188 -0
- package/src/vectorize/tool.ts +542 -0
- package/src/vectorize/types.ts +113 -0
- package/src/vectorize/vectorize.ts +634 -0
- package/src/vectorize/worker-protocol.ts +40 -0
- package/src/vectorize/worker.ts +209 -0
- package/src/version.ts +1 -0
- package/src/workflow.ts +409 -0
|
@@ -0,0 +1,3181 @@
|
|
|
1
|
+
import { CliError } from "./errors";
|
|
2
|
+
import { MAX_EVENT_QUERY_LIMIT } from "./query-limits";
|
|
3
|
+
import { STUDIO_TEMPLATES, type StudioTemplate } from "./studio-template-names";
|
|
4
|
+
|
|
5
|
+
export type OverlayKind = "image" | "svg" | "gif" | "video" | "emoji";
|
|
6
|
+
export type ZoomTargetKind = "rect" | "point" | "cursor" | "window" | "focused-input";
|
|
7
|
+
export type EmojiProvider = "apple-emoji-pack" | "brand-catalog";
|
|
8
|
+
export type ProjectMediaRole =
|
|
9
|
+
| "screen"
|
|
10
|
+
| "camera"
|
|
11
|
+
| "b-roll"
|
|
12
|
+
| "system-audio"
|
|
13
|
+
| "microphone"
|
|
14
|
+
| "portable-audio"
|
|
15
|
+
| "music"
|
|
16
|
+
| "dialogue"
|
|
17
|
+
| "other";
|
|
18
|
+
export type CameraFraming = "tight" | "medium" | "wide" | "group";
|
|
19
|
+
export type CameraGapPolicy = "hold" | "fallback" | "fail";
|
|
20
|
+
export type CameraFrame = readonly [centerX: number, centerY: number, zoom: number];
|
|
21
|
+
export interface CameraPathKeyframe {
|
|
22
|
+
readonly at: string;
|
|
23
|
+
readonly frame: CameraFrame;
|
|
24
|
+
}
|
|
25
|
+
export type GatewayMediaModelType = "image" | "video" | "speech" | "transcription";
|
|
26
|
+
export type GatewayFrameType = "first" | "last";
|
|
27
|
+
export type TranscriptOutputFormat = "all" | "json" | "srt" | "text" | "vtt";
|
|
28
|
+
export type AudioReverbPreset = "room" | "hall" | "plate";
|
|
29
|
+
export type VideoColorPreset = "clean" | "cool" | "cinematic" | "flat" | "mono" | "vivid" | "warm";
|
|
30
|
+
|
|
31
|
+
export interface GatewayFrameInput {
|
|
32
|
+
readonly frameType: GatewayFrameType;
|
|
33
|
+
readonly path: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface JsonOption {
|
|
37
|
+
readonly json: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface StudioWorkflowOptions {
|
|
40
|
+
readonly studio?: { readonly blender?: string; readonly python?: string; readonly allowTrustedCode: boolean };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type StudioCommand = JsonOption & { readonly kind: "studio" } & (
|
|
44
|
+
| { readonly action: "assets"; readonly operation: "search" | "describe" | "plan" | "import"; readonly path: string }
|
|
45
|
+
| { readonly action: "init"; readonly path: string; readonly template: StudioTemplate }
|
|
46
|
+
| { readonly action: "bundle"; readonly path: string; readonly sourceRoot?: string }
|
|
47
|
+
| { readonly action: "plan"; readonly path: string }
|
|
48
|
+
| { readonly action: "assemble"; readonly id: string; readonly outputId: string; readonly title?: string }
|
|
49
|
+
| { readonly action: "encode"; readonly id: string; readonly outputId: string }
|
|
50
|
+
| { readonly action: "asset"; readonly id: string; readonly outputId: string; readonly assetId: string; readonly representation: "native" | "encoded-video"; readonly frame?: number }
|
|
51
|
+
| { readonly action: "inspect"; readonly id: string }
|
|
52
|
+
| { readonly action: "reconcile"; readonly id: string }
|
|
53
|
+
| { readonly action: "probe" | "run"; readonly path: string; readonly blender?: string; readonly python?: string; readonly allowTrustedCode: boolean }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export type SpatialCliExecutionProfile = "three-webgl2-hardware-v1" | "three-spark-webgl2-hardware-v1";
|
|
57
|
+
export type SpatialWorldCommand = JsonOption & {
|
|
58
|
+
readonly kind: "spatial-world"; readonly action: "import";
|
|
59
|
+
readonly input: string; readonly sourceRoot: string; readonly outputRoot: string;
|
|
60
|
+
};
|
|
61
|
+
export type DirectingCommand = JsonOption & { readonly kind: "directing" } & (
|
|
62
|
+
| { readonly action: "init"; readonly path: string }
|
|
63
|
+
| { readonly action: "anchor"; readonly input: string }
|
|
64
|
+
| { readonly action: "plan"; readonly recipe: string }
|
|
65
|
+
| { readonly action: "start"; readonly recipe: string; readonly budgetUsd: string }
|
|
66
|
+
| { readonly action: "inspect" | "assemble"; readonly id: string }
|
|
67
|
+
| { readonly action: "revise"; readonly id: string; readonly recipe: string }
|
|
68
|
+
| { readonly action: "generate"; readonly id: string; readonly shot: string; readonly attempt: string; readonly allowPaidGeneration: true; readonly allowCloudUpload: boolean; readonly allowReferenceHosting: boolean }
|
|
69
|
+
| { readonly action: "resume" | "cleanup"; readonly id: string; readonly attempt: string }
|
|
70
|
+
| { readonly action: "review"; readonly id: string; readonly attempt: string; readonly decision: "accepted" | "rejected"; readonly note: string }
|
|
71
|
+
);
|
|
72
|
+
export type SpatialSceneCommand = JsonOption & { readonly kind: "spatial-scene"; readonly path: string } & (
|
|
73
|
+
| { readonly action: "init" | "inspect" }
|
|
74
|
+
| { readonly action: "patch"; readonly patch: string; readonly output: string }
|
|
75
|
+
| { readonly action: "evaluate"; readonly camera: string; readonly timeUs: number }
|
|
76
|
+
| { readonly action: "camera-track"; readonly request: string; readonly output: string }
|
|
77
|
+
| { readonly action: "plan" | "render"; readonly request: string; readonly assets?: string; readonly executionProfile?: SpatialCliExecutionProfile }
|
|
78
|
+
);
|
|
79
|
+
export type SpatialProjectCommand = JsonOption & {
|
|
80
|
+
readonly kind: "spatial-project"; readonly project: string;
|
|
81
|
+
} & (
|
|
82
|
+
| { readonly action: "snapshot" }
|
|
83
|
+
| { readonly action: "prepare-render"; readonly input: string; readonly output: string; readonly executionProfile?: SpatialCliExecutionProfile }
|
|
84
|
+
| { readonly action: "migrate" | "patch" | "restore" | "add-shot" | "add-candidate" | "select-candidate" | "reconcile"; readonly input: string }
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export type CliCommand =
|
|
88
|
+
| StudioCommand
|
|
89
|
+
| DirectingCommand
|
|
90
|
+
| SpatialWorldCommand
|
|
91
|
+
| SpatialSceneCommand
|
|
92
|
+
| SpatialProjectCommand
|
|
93
|
+
| { readonly kind: "help"; readonly topic: readonly string[] }
|
|
94
|
+
| { readonly kind: "version" }
|
|
95
|
+
| ({ readonly kind: "operations-list" } & JsonOption)
|
|
96
|
+
| ({ readonly kind: "operations-show"; readonly operation: string } & JsonOption)
|
|
97
|
+
| ({ readonly kind: "diagram-check"; readonly path: string } & JsonOption)
|
|
98
|
+
| ({ readonly kind: "diagram-render"; readonly path: string; readonly scale: number | undefined } & JsonOption)
|
|
99
|
+
| ({
|
|
100
|
+
readonly alphaCutoff: number | undefined;
|
|
101
|
+
readonly duotone: readonly [string, string] | undefined;
|
|
102
|
+
readonly inputPath: string;
|
|
103
|
+
readonly kind: "image-vectorize";
|
|
104
|
+
readonly timeoutMs: number | undefined;
|
|
105
|
+
} & JsonOption)
|
|
106
|
+
| ({ readonly kind: "workflows-list" } & JsonOption)
|
|
107
|
+
| ({ readonly kind: "workflows-show"; readonly workflow: string } & JsonOption)
|
|
108
|
+
| ({
|
|
109
|
+
readonly input: string;
|
|
110
|
+
readonly kind: "workflows-plan";
|
|
111
|
+
readonly workflow: string;
|
|
112
|
+
} & JsonOption)
|
|
113
|
+
| ({
|
|
114
|
+
readonly input: string;
|
|
115
|
+
readonly jobs: number;
|
|
116
|
+
readonly jsonl: boolean;
|
|
117
|
+
readonly kind: "workflows-run";
|
|
118
|
+
readonly providerOptions: string | undefined;
|
|
119
|
+
readonly workflow: string;
|
|
120
|
+
} & JsonOption & StudioWorkflowOptions)
|
|
121
|
+
| { readonly kind: "code-init"; readonly path: string }
|
|
122
|
+
| ({ readonly kind: "code-check"; readonly path: string } & JsonOption)
|
|
123
|
+
| ({
|
|
124
|
+
readonly input: string;
|
|
125
|
+
readonly kind: "code-plan";
|
|
126
|
+
readonly path: string;
|
|
127
|
+
} & JsonOption)
|
|
128
|
+
| ({
|
|
129
|
+
readonly input: string;
|
|
130
|
+
readonly jobs: number;
|
|
131
|
+
readonly jsonl: boolean;
|
|
132
|
+
readonly kind: "code-run";
|
|
133
|
+
readonly path: string;
|
|
134
|
+
readonly plan: string | undefined;
|
|
135
|
+
readonly providerOptions: string | undefined;
|
|
136
|
+
} & JsonOption & StudioWorkflowOptions)
|
|
137
|
+
| ({ readonly kind: "runs-list"; readonly limit: number } & JsonOption)
|
|
138
|
+
| ({
|
|
139
|
+
readonly kind: "runs-show";
|
|
140
|
+
readonly nodes: "all" | "failed";
|
|
141
|
+
readonly runId: string;
|
|
142
|
+
} & JsonOption)
|
|
143
|
+
| ({
|
|
144
|
+
readonly jobs: number;
|
|
145
|
+
readonly jsonl: boolean;
|
|
146
|
+
readonly kind: "runs-resume";
|
|
147
|
+
readonly providerOptions: string | undefined;
|
|
148
|
+
readonly replayAmbiguousCode: readonly string[];
|
|
149
|
+
readonly runId: string;
|
|
150
|
+
} & JsonOption & StudioWorkflowOptions)
|
|
151
|
+
| ({
|
|
152
|
+
readonly kind: "runs-approve";
|
|
153
|
+
readonly nodeKey: string;
|
|
154
|
+
readonly planHash: string;
|
|
155
|
+
readonly planKind: "effect" | "preparation";
|
|
156
|
+
readonly runId: string;
|
|
157
|
+
} & JsonOption)
|
|
158
|
+
| ({ readonly kind: "runs-cancel"; readonly runId: string } & JsonOption)
|
|
159
|
+
| ({ readonly kind: "doctor" } & JsonOption)
|
|
160
|
+
| ({
|
|
161
|
+
readonly kind: "ai-provider-options-inspect";
|
|
162
|
+
readonly path: string;
|
|
163
|
+
} & JsonOption)
|
|
164
|
+
| ({
|
|
165
|
+
readonly kind: "ai-models-list";
|
|
166
|
+
readonly limit: number;
|
|
167
|
+
readonly modelType: GatewayMediaModelType | "all";
|
|
168
|
+
readonly provider: string | undefined;
|
|
169
|
+
readonly query: string | undefined;
|
|
170
|
+
readonly refresh: boolean;
|
|
171
|
+
} & JsonOption)
|
|
172
|
+
| ({
|
|
173
|
+
readonly kind: "ai-models-show";
|
|
174
|
+
readonly model: string;
|
|
175
|
+
readonly refresh: boolean;
|
|
176
|
+
} & JsonOption)
|
|
177
|
+
| ({
|
|
178
|
+
readonly allowCloudUpload: boolean;
|
|
179
|
+
readonly aspectRatio: string | undefined;
|
|
180
|
+
readonly count: number;
|
|
181
|
+
readonly images: readonly string[];
|
|
182
|
+
readonly kind: "ai-image-generate";
|
|
183
|
+
readonly mask: string | undefined;
|
|
184
|
+
readonly maxPerCall: number | undefined;
|
|
185
|
+
readonly maxOutputTokens: number | undefined;
|
|
186
|
+
readonly model: string;
|
|
187
|
+
readonly prompt: string | undefined;
|
|
188
|
+
readonly promptFile: string | undefined;
|
|
189
|
+
readonly providerOptions: string | undefined;
|
|
190
|
+
readonly seed: number | undefined;
|
|
191
|
+
readonly size: string | undefined;
|
|
192
|
+
readonly stopSequences: readonly string[];
|
|
193
|
+
readonly temperature: number | undefined;
|
|
194
|
+
readonly timeout: string;
|
|
195
|
+
} & JsonOption)
|
|
196
|
+
| ({
|
|
197
|
+
readonly allowCloudUpload: boolean;
|
|
198
|
+
readonly aspectRatio: string | undefined;
|
|
199
|
+
readonly count: number;
|
|
200
|
+
readonly durationSeconds: number | undefined;
|
|
201
|
+
readonly fps: number | undefined;
|
|
202
|
+
readonly frameImages: readonly GatewayFrameInput[];
|
|
203
|
+
readonly generateAudio: boolean | undefined;
|
|
204
|
+
readonly image: string | undefined;
|
|
205
|
+
readonly inputReferences: readonly string[];
|
|
206
|
+
readonly kind: "ai-video-generate";
|
|
207
|
+
readonly maxPerCall: number | undefined;
|
|
208
|
+
readonly model: string;
|
|
209
|
+
readonly prompt: string | undefined;
|
|
210
|
+
readonly promptFile: string | undefined;
|
|
211
|
+
readonly providerOptions: string | undefined;
|
|
212
|
+
readonly resolution: string | undefined;
|
|
213
|
+
readonly seed: number | undefined;
|
|
214
|
+
readonly timeout: string;
|
|
215
|
+
} & JsonOption)
|
|
216
|
+
| ({
|
|
217
|
+
readonly instructions: string | undefined;
|
|
218
|
+
readonly instructionsFile: string | undefined;
|
|
219
|
+
readonly kind: "ai-speech-generate";
|
|
220
|
+
readonly language: string | undefined;
|
|
221
|
+
readonly model: string;
|
|
222
|
+
readonly outputFormat: string | undefined;
|
|
223
|
+
readonly providerOptions: string | undefined;
|
|
224
|
+
readonly speed: number | undefined;
|
|
225
|
+
readonly text: string | undefined;
|
|
226
|
+
readonly textFile: string | undefined;
|
|
227
|
+
readonly timeout: string;
|
|
228
|
+
readonly voice: string | undefined;
|
|
229
|
+
} & JsonOption)
|
|
230
|
+
| ({
|
|
231
|
+
readonly allowCloudAudioUpload: boolean;
|
|
232
|
+
readonly format: TranscriptOutputFormat;
|
|
233
|
+
readonly input: string;
|
|
234
|
+
readonly kind: "ai-transcribe";
|
|
235
|
+
readonly model: string;
|
|
236
|
+
readonly providerOptions: string | undefined;
|
|
237
|
+
readonly timeout: string;
|
|
238
|
+
} & JsonOption)
|
|
239
|
+
| ({
|
|
240
|
+
readonly audioStreamIndex: number;
|
|
241
|
+
readonly compressor: boolean;
|
|
242
|
+
readonly compressorAttackMs: number;
|
|
243
|
+
readonly compressorMakeupDb: number;
|
|
244
|
+
readonly compressorRatio: number;
|
|
245
|
+
readonly compressorReleaseMs: number;
|
|
246
|
+
readonly compressorThresholdDb: number;
|
|
247
|
+
readonly delayFeedback: number;
|
|
248
|
+
readonly delayMix: number;
|
|
249
|
+
readonly delayMs: number | undefined;
|
|
250
|
+
readonly denoise: boolean;
|
|
251
|
+
readonly denoiseReductionDb: number;
|
|
252
|
+
readonly input: string;
|
|
253
|
+
readonly kind: "media-audio";
|
|
254
|
+
readonly output: string | undefined;
|
|
255
|
+
readonly reverb: AudioReverbPreset | undefined;
|
|
256
|
+
readonly reverbWet: number;
|
|
257
|
+
readonly volumeDb: number | undefined;
|
|
258
|
+
} & JsonOption)
|
|
259
|
+
| ({
|
|
260
|
+
readonly brightness: number | undefined;
|
|
261
|
+
readonly contrast: number | undefined;
|
|
262
|
+
readonly gamma: number | undefined;
|
|
263
|
+
readonly hueDegrees: number | undefined;
|
|
264
|
+
readonly input: string;
|
|
265
|
+
readonly kind: "media-color";
|
|
266
|
+
readonly output: string | undefined;
|
|
267
|
+
readonly preset: VideoColorPreset | undefined;
|
|
268
|
+
readonly saturation: number | undefined;
|
|
269
|
+
readonly temperature: number | undefined;
|
|
270
|
+
readonly tint: number | undefined;
|
|
271
|
+
readonly videoStreamIndex: number;
|
|
272
|
+
} & JsonOption)
|
|
273
|
+
| ({ readonly kind: "recordings-list"; readonly limit: number } & JsonOption)
|
|
274
|
+
| ({ readonly kind: "projects-list"; readonly limit: number } & JsonOption)
|
|
275
|
+
| ({
|
|
276
|
+
readonly kind: "projects-create";
|
|
277
|
+
readonly name: string | undefined;
|
|
278
|
+
readonly recording: string;
|
|
279
|
+
} & JsonOption)
|
|
280
|
+
| ({ readonly kind: "project-inspect"; readonly project: string } & JsonOption)
|
|
281
|
+
| ({
|
|
282
|
+
readonly at: string;
|
|
283
|
+
readonly kind: "project-add";
|
|
284
|
+
readonly path: string;
|
|
285
|
+
readonly project: string;
|
|
286
|
+
readonly role: ProjectMediaRole;
|
|
287
|
+
} & JsonOption)
|
|
288
|
+
| ({
|
|
289
|
+
readonly action: "push";
|
|
290
|
+
readonly center: readonly [number, number];
|
|
291
|
+
readonly easing: string;
|
|
292
|
+
readonly endZoom: number;
|
|
293
|
+
readonly from: string;
|
|
294
|
+
readonly kind: "project-camera-edit";
|
|
295
|
+
readonly placement: string;
|
|
296
|
+
readonly project: string;
|
|
297
|
+
readonly startZoom: number;
|
|
298
|
+
readonly stream: string;
|
|
299
|
+
readonly to: string;
|
|
300
|
+
} & JsonOption)
|
|
301
|
+
| ({
|
|
302
|
+
readonly action: "reframe";
|
|
303
|
+
readonly easing: string;
|
|
304
|
+
readonly from: string;
|
|
305
|
+
readonly fromFrame: CameraFrame;
|
|
306
|
+
readonly kind: "project-camera-edit";
|
|
307
|
+
readonly placement: string;
|
|
308
|
+
readonly project: string;
|
|
309
|
+
readonly stream: string;
|
|
310
|
+
readonly to: string;
|
|
311
|
+
readonly toFrame: CameraFrame;
|
|
312
|
+
} & JsonOption)
|
|
313
|
+
| ({
|
|
314
|
+
readonly action: "path";
|
|
315
|
+
readonly easing: string;
|
|
316
|
+
readonly keyframes: readonly CameraPathKeyframe[];
|
|
317
|
+
readonly kind: "project-camera-edit";
|
|
318
|
+
readonly placement: string;
|
|
319
|
+
readonly project: string;
|
|
320
|
+
readonly stream: string;
|
|
321
|
+
} & JsonOption)
|
|
322
|
+
| ({
|
|
323
|
+
readonly action: "follow-faces";
|
|
324
|
+
readonly analysis: string;
|
|
325
|
+
readonly easing: string;
|
|
326
|
+
readonly framing: CameraFraming;
|
|
327
|
+
readonly from: string;
|
|
328
|
+
readonly gapPolicy: CameraGapPolicy;
|
|
329
|
+
readonly headroom: number;
|
|
330
|
+
readonly kind: "project-camera-edit";
|
|
331
|
+
readonly maxZoom: number;
|
|
332
|
+
readonly minZoom: number;
|
|
333
|
+
readonly outputHeight: number;
|
|
334
|
+
readonly outputWidth: number;
|
|
335
|
+
readonly placement: string;
|
|
336
|
+
readonly project: string;
|
|
337
|
+
readonly requireAllSelected: boolean;
|
|
338
|
+
readonly select: "all" | "largest" | undefined;
|
|
339
|
+
readonly smoothing: number;
|
|
340
|
+
readonly to: string;
|
|
341
|
+
readonly tracks: readonly string[];
|
|
342
|
+
} & JsonOption)
|
|
343
|
+
| ({
|
|
344
|
+
readonly action: "show";
|
|
345
|
+
readonly kind: "project-camera-edit";
|
|
346
|
+
readonly project: string;
|
|
347
|
+
} & JsonOption)
|
|
348
|
+
| ({
|
|
349
|
+
readonly action: "remove";
|
|
350
|
+
readonly cameraMoveId: string;
|
|
351
|
+
readonly kind: "project-camera-edit";
|
|
352
|
+
readonly project: string;
|
|
353
|
+
} & JsonOption)
|
|
354
|
+
| ({
|
|
355
|
+
readonly from: string;
|
|
356
|
+
readonly kind: "project-edit";
|
|
357
|
+
readonly operation: "cut" | "trim";
|
|
358
|
+
readonly project: string;
|
|
359
|
+
readonly to: string;
|
|
360
|
+
} & JsonOption)
|
|
361
|
+
| ({
|
|
362
|
+
readonly edit: Extract<EditCommand, { readonly operation: "overlay-add" | "overlay-remove" }>;
|
|
363
|
+
readonly fps: number | undefined;
|
|
364
|
+
readonly kind: "project-overlay-edit";
|
|
365
|
+
readonly project: string;
|
|
366
|
+
} & JsonOption)
|
|
367
|
+
| ({
|
|
368
|
+
readonly edit: Extract<EditCommand, {
|
|
369
|
+
readonly operation: "zoom-add" | "zoom-remove" | "cursor" | "clicks" | "keystrokes" | "typed-text";
|
|
370
|
+
}>;
|
|
371
|
+
readonly fps: number | undefined;
|
|
372
|
+
readonly kind: "project-metadata-edit";
|
|
373
|
+
readonly project: string;
|
|
374
|
+
readonly sourcePlacement: string | undefined;
|
|
375
|
+
} & JsonOption)
|
|
376
|
+
| ({
|
|
377
|
+
readonly action: "plan" | "run";
|
|
378
|
+
readonly allowUnverifiedSync: boolean;
|
|
379
|
+
readonly dryRun: boolean;
|
|
380
|
+
readonly fps: number;
|
|
381
|
+
readonly height: number;
|
|
382
|
+
readonly kind: "project-render";
|
|
383
|
+
readonly output: string | undefined;
|
|
384
|
+
readonly project: string;
|
|
385
|
+
readonly width: number;
|
|
386
|
+
} & JsonOption)
|
|
387
|
+
| ({
|
|
388
|
+
readonly apply: boolean;
|
|
389
|
+
readonly candidate: string | undefined;
|
|
390
|
+
readonly kind: "align-analyze";
|
|
391
|
+
readonly maxOffset: string | undefined;
|
|
392
|
+
readonly project: string;
|
|
393
|
+
readonly reference: string;
|
|
394
|
+
readonly referencePlacement: string | undefined;
|
|
395
|
+
readonly target: string;
|
|
396
|
+
readonly targetPlacement: string | undefined;
|
|
397
|
+
} & JsonOption)
|
|
398
|
+
| ({
|
|
399
|
+
readonly analysis: string;
|
|
400
|
+
readonly candidate: string;
|
|
401
|
+
readonly kind: "align-apply";
|
|
402
|
+
readonly project: string;
|
|
403
|
+
readonly referencePlacement: string | undefined;
|
|
404
|
+
readonly targetPlacement: string | undefined;
|
|
405
|
+
} & JsonOption)
|
|
406
|
+
| ({
|
|
407
|
+
readonly from: string;
|
|
408
|
+
readonly kind: "project-edit";
|
|
409
|
+
readonly operation: "speed";
|
|
410
|
+
readonly project: string;
|
|
411
|
+
readonly rate: number;
|
|
412
|
+
readonly to: string;
|
|
413
|
+
} & JsonOption)
|
|
414
|
+
| ({
|
|
415
|
+
readonly fields: readonly string[] | undefined;
|
|
416
|
+
readonly kind: "inspect";
|
|
417
|
+
readonly recording: string;
|
|
418
|
+
} & JsonOption)
|
|
419
|
+
| ({
|
|
420
|
+
readonly kind: "analyze-music";
|
|
421
|
+
readonly project: string;
|
|
422
|
+
readonly source: string;
|
|
423
|
+
readonly window: string;
|
|
424
|
+
} & JsonOption)
|
|
425
|
+
| ({
|
|
426
|
+
readonly backend: "auto" | "vision";
|
|
427
|
+
readonly kind: "analyze-faces";
|
|
428
|
+
readonly maxFaces: number;
|
|
429
|
+
readonly maxTrackGap: string;
|
|
430
|
+
readonly minConfidence: number;
|
|
431
|
+
readonly project: string;
|
|
432
|
+
readonly sampleFps: number;
|
|
433
|
+
readonly source: string;
|
|
434
|
+
} & JsonOption)
|
|
435
|
+
| ({
|
|
436
|
+
readonly allowCloudUpload: boolean;
|
|
437
|
+
readonly execute: boolean;
|
|
438
|
+
readonly kind: "analyze-scenes";
|
|
439
|
+
readonly maximumSceneDuration: string;
|
|
440
|
+
readonly model: string;
|
|
441
|
+
readonly project: string;
|
|
442
|
+
readonly sceneThreshold: number;
|
|
443
|
+
readonly source: string;
|
|
444
|
+
} & JsonOption)
|
|
445
|
+
| ({
|
|
446
|
+
readonly kind: "analyze-speech";
|
|
447
|
+
readonly language: string;
|
|
448
|
+
readonly minimumFillerConfidence: number;
|
|
449
|
+
readonly model: string | undefined;
|
|
450
|
+
readonly noGpu: boolean;
|
|
451
|
+
readonly processors: number;
|
|
452
|
+
readonly protectMusic: boolean;
|
|
453
|
+
readonly project: string;
|
|
454
|
+
readonly source: string;
|
|
455
|
+
readonly speechHandle: string;
|
|
456
|
+
readonly threads: number;
|
|
457
|
+
readonly whisper: string | undefined;
|
|
458
|
+
} & JsonOption)
|
|
459
|
+
| ({
|
|
460
|
+
readonly analysis: string;
|
|
461
|
+
readonly autoOnly: boolean;
|
|
462
|
+
readonly kind: "fillers-list";
|
|
463
|
+
readonly project: string;
|
|
464
|
+
} & JsonOption)
|
|
465
|
+
| ({
|
|
466
|
+
readonly analysis: string;
|
|
467
|
+
readonly at: string | undefined;
|
|
468
|
+
readonly kind: "faces-list";
|
|
469
|
+
readonly limit: number;
|
|
470
|
+
readonly minConfidence: number;
|
|
471
|
+
readonly minDuration: string;
|
|
472
|
+
readonly project: string;
|
|
473
|
+
} & JsonOption)
|
|
474
|
+
| ({
|
|
475
|
+
readonly analysis: string;
|
|
476
|
+
readonly candidate: string;
|
|
477
|
+
readonly kind: "fillers-apply";
|
|
478
|
+
readonly placement: string | undefined;
|
|
479
|
+
readonly project: string;
|
|
480
|
+
} & JsonOption)
|
|
481
|
+
| ({
|
|
482
|
+
readonly around: string | undefined;
|
|
483
|
+
readonly eventKinds: readonly string[];
|
|
484
|
+
readonly fps: number | undefined;
|
|
485
|
+
readonly format: "human" | "json" | "jsonl";
|
|
486
|
+
readonly from: string | undefined;
|
|
487
|
+
readonly kind: "events";
|
|
488
|
+
readonly limit: number;
|
|
489
|
+
readonly recording: string;
|
|
490
|
+
readonly to: string | undefined;
|
|
491
|
+
})
|
|
492
|
+
| ({
|
|
493
|
+
readonly action: "start";
|
|
494
|
+
readonly cameraDeviceId: string | undefined;
|
|
495
|
+
readonly displays: readonly string[];
|
|
496
|
+
readonly microphone: boolean;
|
|
497
|
+
readonly microphoneDeviceId: string | undefined;
|
|
498
|
+
readonly strictInputs: boolean;
|
|
499
|
+
readonly systemAudio: boolean;
|
|
500
|
+
readonly typedText: boolean;
|
|
501
|
+
readonly webcam: boolean;
|
|
502
|
+
} & JsonOption & { readonly kind: "record" })
|
|
503
|
+
| ({ readonly action: "pause" | "resume" | "stop" | "status" } & JsonOption & {
|
|
504
|
+
readonly kind: "record";
|
|
505
|
+
})
|
|
506
|
+
| ({
|
|
507
|
+
readonly edit: EditCommand;
|
|
508
|
+
readonly fps: number | undefined;
|
|
509
|
+
readonly kind: "edit";
|
|
510
|
+
readonly recording: string;
|
|
511
|
+
} & JsonOption)
|
|
512
|
+
| ({
|
|
513
|
+
readonly apply: boolean;
|
|
514
|
+
readonly handle: "cut" | "speed" | "keep";
|
|
515
|
+
readonly kind: "analyze-inactivity";
|
|
516
|
+
readonly minDuration: string;
|
|
517
|
+
readonly motionThreshold: number;
|
|
518
|
+
readonly protectAudio: boolean;
|
|
519
|
+
readonly recording: string;
|
|
520
|
+
readonly speedRate: number;
|
|
521
|
+
} & JsonOption)
|
|
522
|
+
| ({
|
|
523
|
+
readonly apply: boolean;
|
|
524
|
+
readonly kind: "analyze-zooms";
|
|
525
|
+
readonly plan: string | undefined;
|
|
526
|
+
readonly recording: string;
|
|
527
|
+
} & JsonOption)
|
|
528
|
+
| ({
|
|
529
|
+
readonly autoInactivity: boolean;
|
|
530
|
+
readonly display: string;
|
|
531
|
+
readonly kind: "render-plan";
|
|
532
|
+
readonly output: string | undefined;
|
|
533
|
+
readonly recording: string;
|
|
534
|
+
} & JsonOption)
|
|
535
|
+
| ({
|
|
536
|
+
readonly autoInactivity: boolean;
|
|
537
|
+
readonly display: string;
|
|
538
|
+
readonly dryRun: boolean;
|
|
539
|
+
readonly kind: "render-run";
|
|
540
|
+
readonly output: string | undefined;
|
|
541
|
+
readonly recording: string;
|
|
542
|
+
} & JsonOption)
|
|
543
|
+
| ({
|
|
544
|
+
readonly kind: "emoji-search";
|
|
545
|
+
readonly limit: number;
|
|
546
|
+
readonly provider: EmojiProvider | "all";
|
|
547
|
+
readonly query: string;
|
|
548
|
+
readonly variant: "color" | "duotone" | undefined;
|
|
549
|
+
} & JsonOption)
|
|
550
|
+
| ({
|
|
551
|
+
readonly kind: "emoji-resolve";
|
|
552
|
+
readonly provider: EmojiProvider | "auto";
|
|
553
|
+
readonly query: string;
|
|
554
|
+
readonly variant: "color" | "duotone" | undefined;
|
|
555
|
+
} & JsonOption)
|
|
556
|
+
| { readonly kind: "complete"; readonly words: readonly string[] };
|
|
557
|
+
|
|
558
|
+
export type EditCommand =
|
|
559
|
+
| { readonly operation: "init" }
|
|
560
|
+
| { readonly operation: "show" }
|
|
561
|
+
| { readonly from: string; readonly operation: "trim"; readonly to: string }
|
|
562
|
+
| { readonly from: string; readonly operation: "cut"; readonly to: string }
|
|
563
|
+
| { readonly from: string; readonly operation: "speed"; readonly rate: number; readonly to: string }
|
|
564
|
+
| {
|
|
565
|
+
readonly display: string | undefined;
|
|
566
|
+
readonly easing: string;
|
|
567
|
+
readonly enterDuration: string | undefined;
|
|
568
|
+
readonly exitDuration: string | undefined;
|
|
569
|
+
readonly from: string;
|
|
570
|
+
readonly operation: "zoom-add";
|
|
571
|
+
readonly point: readonly [number, number] | undefined;
|
|
572
|
+
readonly rect: readonly [number, number, number, number] | undefined;
|
|
573
|
+
readonly scale: number;
|
|
574
|
+
readonly target: ZoomTargetKind;
|
|
575
|
+
readonly to: string;
|
|
576
|
+
readonly window: string | undefined;
|
|
577
|
+
}
|
|
578
|
+
| { readonly id: string; readonly operation: "zoom-remove" }
|
|
579
|
+
| ({ readonly operation: "overlay-add" } & OverlayAddOptions)
|
|
580
|
+
| { readonly id: string; readonly operation: "overlay-remove" }
|
|
581
|
+
| {
|
|
582
|
+
readonly clickHighlight: boolean | undefined;
|
|
583
|
+
readonly enabled: boolean;
|
|
584
|
+
readonly operation: "cursor";
|
|
585
|
+
readonly smoothing: number | undefined;
|
|
586
|
+
}
|
|
587
|
+
| {
|
|
588
|
+
readonly enabled: boolean;
|
|
589
|
+
readonly operation: "keystrokes";
|
|
590
|
+
readonly stopAfter: string | undefined;
|
|
591
|
+
}
|
|
592
|
+
| {
|
|
593
|
+
readonly color: string;
|
|
594
|
+
readonly duration: string;
|
|
595
|
+
readonly enabled: boolean;
|
|
596
|
+
readonly operation: "clicks";
|
|
597
|
+
readonly radius: number;
|
|
598
|
+
readonly style: "pulse" | "ring" | "fill";
|
|
599
|
+
}
|
|
600
|
+
| {
|
|
601
|
+
readonly enabled: boolean;
|
|
602
|
+
readonly idleTimeout: string;
|
|
603
|
+
readonly maxCharacters: number;
|
|
604
|
+
readonly operation: "typed-text";
|
|
605
|
+
readonly placement: "input" | "caption";
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
export interface OverlayAddOptions {
|
|
609
|
+
readonly anchor: "top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right";
|
|
610
|
+
readonly animatedAudio: "mute" | "mix" | "duck";
|
|
611
|
+
readonly audioVolume: number;
|
|
612
|
+
readonly blendMode: "normal" | "addition" | "darken" | "lighten" | "multiply" | "overlay" | "screen";
|
|
613
|
+
readonly cornerRadius: number | undefined;
|
|
614
|
+
readonly crop: readonly [number, number, number, number] | undefined;
|
|
615
|
+
readonly duckPrimaryTo: number;
|
|
616
|
+
readonly entrance: string | undefined;
|
|
617
|
+
readonly entranceDuration: string | undefined;
|
|
618
|
+
readonly entranceFromScale: number;
|
|
619
|
+
readonly easing: string;
|
|
620
|
+
readonly exit: string | undefined;
|
|
621
|
+
readonly exitDuration: string | undefined;
|
|
622
|
+
readonly exitToScale: number;
|
|
623
|
+
readonly fit: "contain" | "cover" | "fill";
|
|
624
|
+
readonly freezeEnd: boolean;
|
|
625
|
+
readonly from: string;
|
|
626
|
+
readonly height: number | undefined;
|
|
627
|
+
readonly loop: boolean;
|
|
628
|
+
readonly motionKeyframes: readonly Readonly<{
|
|
629
|
+
offset: number;
|
|
630
|
+
opacity: number;
|
|
631
|
+
position: readonly [number, number];
|
|
632
|
+
rotation: number;
|
|
633
|
+
scale: number;
|
|
634
|
+
}>[];
|
|
635
|
+
readonly opacity: number;
|
|
636
|
+
readonly overlayKind: OverlayKind;
|
|
637
|
+
readonly provider: EmojiProvider | "auto";
|
|
638
|
+
readonly playbackRate: number;
|
|
639
|
+
readonly position: readonly [number, number];
|
|
640
|
+
readonly rotation: number;
|
|
641
|
+
readonly scale: number;
|
|
642
|
+
readonly source: string;
|
|
643
|
+
readonly sourceIn: string | undefined;
|
|
644
|
+
readonly sourceOut: string | undefined;
|
|
645
|
+
readonly slideDistance: number;
|
|
646
|
+
readonly to: string;
|
|
647
|
+
readonly variant: "color" | "duotone" | undefined;
|
|
648
|
+
readonly width: number | undefined;
|
|
649
|
+
readonly zIndex: number;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
type OptionKind = "flag" | "value" | "repeat";
|
|
653
|
+
type OptionSpec = Readonly<Record<string, OptionKind>>;
|
|
654
|
+
type OptionValue = boolean | string | readonly string[];
|
|
655
|
+
|
|
656
|
+
interface ParsedOptions {
|
|
657
|
+
readonly options: Readonly<Record<string, OptionValue>>;
|
|
658
|
+
readonly positionals: readonly string[];
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function fail(message: string): never {
|
|
662
|
+
throw new CliError("usage", message);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function parseOptions(argv: readonly string[], spec: OptionSpec): ParsedOptions {
|
|
666
|
+
const options: Record<string, OptionValue> = {};
|
|
667
|
+
const positionals: string[] = [];
|
|
668
|
+
let optionsEnded = false;
|
|
669
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
670
|
+
const argument = argv[index]!;
|
|
671
|
+
if (optionsEnded || !argument.startsWith("-")) {
|
|
672
|
+
positionals.push(argument);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (argument === "--") {
|
|
676
|
+
optionsEnded = true;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const equalsIndex = argument.indexOf("=");
|
|
680
|
+
const name = equalsIndex === -1 ? argument : argument.slice(0, equalsIndex);
|
|
681
|
+
const inlineValue = equalsIndex === -1 ? undefined : argument.slice(equalsIndex + 1);
|
|
682
|
+
const kind = spec[name];
|
|
683
|
+
if (kind === undefined) fail(`Unknown option: ${name}`);
|
|
684
|
+
if (kind === "flag") {
|
|
685
|
+
if (inlineValue !== undefined) fail(`${name} does not take a value.`);
|
|
686
|
+
if (options[name] !== undefined) fail(`Option may be specified only once: ${name}`);
|
|
687
|
+
options[name] = true;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
const nextValue = inlineValue ?? argv[index + 1];
|
|
691
|
+
if (nextValue === undefined || (inlineValue === undefined && nextValue.startsWith("--"))) {
|
|
692
|
+
fail(`Option requires a value: ${name}`);
|
|
693
|
+
}
|
|
694
|
+
if (inlineValue === undefined) index += 1;
|
|
695
|
+
if (kind === "value") {
|
|
696
|
+
if (options[name] !== undefined) fail(`Option may be specified only once: ${name}`);
|
|
697
|
+
options[name] = nextValue;
|
|
698
|
+
} else {
|
|
699
|
+
const previous = options[name];
|
|
700
|
+
options[name] = previous === undefined
|
|
701
|
+
? [nextValue]
|
|
702
|
+
: [...requireStringArray(previous, name), nextValue];
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return { options, positionals };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function requireStringArray(value: OptionValue, option: string): readonly string[] {
|
|
709
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
710
|
+
fail(`Internal parser error for ${option}.`);
|
|
711
|
+
}
|
|
712
|
+
return value;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function optionString(parsed: ParsedOptions, name: string): string | undefined {
|
|
716
|
+
const value = parsed.options[name];
|
|
717
|
+
if (value === undefined) return undefined;
|
|
718
|
+
if (typeof value !== "string") fail(`Internal parser error for ${name}.`);
|
|
719
|
+
return value;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function optionStrings(parsed: ParsedOptions, name: string): readonly string[] {
|
|
723
|
+
const value = parsed.options[name];
|
|
724
|
+
if (value === undefined) return [];
|
|
725
|
+
return requireStringArray(value, name);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function optionFlag(parsed: ParsedOptions, name: string): boolean {
|
|
729
|
+
return parsed.options[name] === true;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function strictInteger(value: string | undefined, name: string, defaults: number): number {
|
|
733
|
+
if (value === undefined) return defaults;
|
|
734
|
+
if (!/^[1-9][0-9]*$/u.test(value)) fail(`${name} must be a positive integer.`);
|
|
735
|
+
const parsed = Number(value);
|
|
736
|
+
if (!Number.isSafeInteger(parsed)) fail(`${name} exceeds the safe integer range.`);
|
|
737
|
+
return parsed;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function optionalStrictInteger(value: string | undefined, name: string): number | undefined {
|
|
741
|
+
return value === undefined ? undefined : strictInteger(value, name, 1);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function optionalNonNegativeInteger(value: string | undefined, name: string): number | undefined {
|
|
745
|
+
if (value === undefined) return undefined;
|
|
746
|
+
if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) fail(`${name} must be a non-negative integer.`);
|
|
747
|
+
const parsed = Number(value);
|
|
748
|
+
if (!Number.isSafeInteger(parsed)) fail(`${name} exceeds the safe integer range.`);
|
|
749
|
+
return parsed;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function strictEvenPositiveInteger(value: string | undefined, name: string, defaults: number): number {
|
|
753
|
+
const parsed = strictInteger(value, name, defaults);
|
|
754
|
+
if (parsed % 2 !== 0) fail(`${name} must be an even positive integer.`);
|
|
755
|
+
return parsed;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function strictNumber(
|
|
759
|
+
value: string | undefined,
|
|
760
|
+
name: string,
|
|
761
|
+
defaults?: number,
|
|
762
|
+
): number {
|
|
763
|
+
if (value === undefined) {
|
|
764
|
+
if (defaults === undefined) fail(`${name} is required.`);
|
|
765
|
+
return defaults;
|
|
766
|
+
}
|
|
767
|
+
if (value.trim() === "" || !Number.isFinite(Number(value))) fail(`${name} must be a finite number.`);
|
|
768
|
+
return Number(value);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function boundedNumber(
|
|
772
|
+
value: string | undefined,
|
|
773
|
+
name: string,
|
|
774
|
+
minimum: number,
|
|
775
|
+
maximum: number,
|
|
776
|
+
defaults?: number,
|
|
777
|
+
): number {
|
|
778
|
+
const parsed = strictNumber(value, name, defaults);
|
|
779
|
+
if (parsed < minimum || parsed > maximum) {
|
|
780
|
+
fail(`${name} must be between ${minimum} and ${maximum}.`);
|
|
781
|
+
}
|
|
782
|
+
return parsed;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function optionalBoundedNumber(
|
|
786
|
+
value: string | undefined,
|
|
787
|
+
name: string,
|
|
788
|
+
minimum: number,
|
|
789
|
+
maximum: number,
|
|
790
|
+
): number | undefined {
|
|
791
|
+
return value === undefined ? undefined : boundedNumber(value, name, minimum, maximum);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function strictBoolean(value: string | undefined, name: string, defaults: boolean): boolean {
|
|
795
|
+
if (value === undefined) return defaults;
|
|
796
|
+
if (value === "true") return true;
|
|
797
|
+
if (value === "false") return false;
|
|
798
|
+
fail(`${name} must be true or false.`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function captureSourceId(
|
|
802
|
+
value: string | undefined,
|
|
803
|
+
name: string,
|
|
804
|
+
maximumUTF8Bytes: number,
|
|
805
|
+
): string | undefined {
|
|
806
|
+
if (value === undefined) return undefined;
|
|
807
|
+
if (
|
|
808
|
+
value.length === 0
|
|
809
|
+
|| value.includes("\0")
|
|
810
|
+
|| new TextEncoder().encode(value).byteLength > maximumUTF8Bytes
|
|
811
|
+
) {
|
|
812
|
+
fail(`${name} must be a non-empty, non-NUL source ID of at most ${maximumUTF8Bytes} UTF-8 bytes.`);
|
|
813
|
+
}
|
|
814
|
+
return value;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function oneOf<T extends string>(
|
|
818
|
+
value: string | undefined,
|
|
819
|
+
name: string,
|
|
820
|
+
allowed: readonly T[],
|
|
821
|
+
defaults?: T,
|
|
822
|
+
): T {
|
|
823
|
+
if (value === undefined) {
|
|
824
|
+
if (defaults === undefined) fail(`${name} is required.`);
|
|
825
|
+
return defaults;
|
|
826
|
+
}
|
|
827
|
+
const match = allowed.find((candidate) => candidate === value);
|
|
828
|
+
if (match === undefined) fail(`${name} must be one of: ${allowed.join(", ")}.`);
|
|
829
|
+
return match;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function exactPositionals(parsed: ParsedOptions, count: number, usage: string): readonly string[] {
|
|
833
|
+
if (parsed.positionals.length !== count) fail(`Usage: ${usage}`);
|
|
834
|
+
return parsed.positionals;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const JSON_SPEC = { "--json": "flag" } as const;
|
|
838
|
+
|
|
839
|
+
function parseDoctor(argv: readonly string[]): CliCommand {
|
|
840
|
+
const parsed = parseOptions(argv, JSON_SPEC);
|
|
841
|
+
exactPositionals(parsed, 0, "slopcamera doctor [--json]");
|
|
842
|
+
return { json: optionFlag(parsed, "--json"), kind: "doctor" };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function oneTextSource(
|
|
846
|
+
parsed: ParsedOptions,
|
|
847
|
+
inlineName: string,
|
|
848
|
+
fileName: string,
|
|
849
|
+
options: Readonly<{ required: boolean }>,
|
|
850
|
+
): Readonly<{ inline: string | undefined; path: string | undefined }> {
|
|
851
|
+
const inline = optionString(parsed, inlineName);
|
|
852
|
+
const path = optionString(parsed, fileName);
|
|
853
|
+
if (inline !== undefined && path !== undefined) {
|
|
854
|
+
fail(`${inlineName} and ${fileName} are mutually exclusive.`);
|
|
855
|
+
}
|
|
856
|
+
if (options.required && inline === undefined && path === undefined) {
|
|
857
|
+
fail(`Exactly one of ${inlineName} or ${fileName} is required.`);
|
|
858
|
+
}
|
|
859
|
+
return { inline, path };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function parseGatewayFrameInputs(values: readonly string[]): readonly GatewayFrameInput[] {
|
|
863
|
+
const frames: GatewayFrameInput[] = [];
|
|
864
|
+
const seen = new Set<GatewayFrameType>();
|
|
865
|
+
for (const value of values) {
|
|
866
|
+
const separator = value.indexOf("=");
|
|
867
|
+
const frameType = separator === -1 ? "" : value.slice(0, separator);
|
|
868
|
+
const path = separator === -1 ? "" : value.slice(separator + 1);
|
|
869
|
+
if ((frameType !== "first" && frameType !== "last") || path.trim() === "") {
|
|
870
|
+
fail("--frame must use first=<path-or-https-url> or last=<path-or-https-url>.");
|
|
871
|
+
}
|
|
872
|
+
if (seen.has(frameType)) fail(`--frame ${frameType} may be specified only once.`);
|
|
873
|
+
seen.add(frameType);
|
|
874
|
+
frames.push({ frameType, path });
|
|
875
|
+
}
|
|
876
|
+
return frames;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function parseAiModels(argv: readonly string[]): CliCommand {
|
|
880
|
+
const action = argv[0];
|
|
881
|
+
if (action === "list") {
|
|
882
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
883
|
+
...JSON_SPEC,
|
|
884
|
+
"--limit": "value",
|
|
885
|
+
"--provider": "value",
|
|
886
|
+
"--query": "value",
|
|
887
|
+
"--refresh": "flag",
|
|
888
|
+
"--type": "value",
|
|
889
|
+
});
|
|
890
|
+
exactPositionals(parsed, 0, "slopcamera ai models list [options]");
|
|
891
|
+
return {
|
|
892
|
+
json: optionFlag(parsed, "--json"),
|
|
893
|
+
kind: "ai-models-list",
|
|
894
|
+
limit: (() => {
|
|
895
|
+
const limit = strictInteger(optionString(parsed, "--limit"), "--limit", 100);
|
|
896
|
+
if (limit > 500) fail("--limit must be at most 500.");
|
|
897
|
+
return limit;
|
|
898
|
+
})(),
|
|
899
|
+
modelType: oneOf(optionString(parsed, "--type"), "--type", [
|
|
900
|
+
"all", "image", "video", "speech", "transcription",
|
|
901
|
+
] as const, "all"),
|
|
902
|
+
provider: optionString(parsed, "--provider"),
|
|
903
|
+
query: optionString(parsed, "--query"),
|
|
904
|
+
refresh: optionFlag(parsed, "--refresh"),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
if (action === "show") {
|
|
908
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
909
|
+
...JSON_SPEC,
|
|
910
|
+
"--refresh": "flag",
|
|
911
|
+
});
|
|
912
|
+
const [model] = exactPositionals(parsed, 1, "slopcamera ai models show <model> [--refresh] [--json]");
|
|
913
|
+
return {
|
|
914
|
+
json: optionFlag(parsed, "--json"),
|
|
915
|
+
kind: "ai-models-show",
|
|
916
|
+
model: model!,
|
|
917
|
+
refresh: optionFlag(parsed, "--refresh"),
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
fail("Usage: slopcamera ai models <list|show> [options]");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function parseAiProviderOptions(argv: readonly string[]): CliCommand {
|
|
924
|
+
if (argv[0] !== "inspect") {
|
|
925
|
+
fail("Usage: slopcamera ai provider-options inspect <json-file> [--json]");
|
|
926
|
+
}
|
|
927
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
928
|
+
const [path] = exactPositionals(
|
|
929
|
+
parsed,
|
|
930
|
+
1,
|
|
931
|
+
"slopcamera ai provider-options inspect <json-file> [--json]",
|
|
932
|
+
);
|
|
933
|
+
return {
|
|
934
|
+
json: optionFlag(parsed, "--json"),
|
|
935
|
+
kind: "ai-provider-options-inspect",
|
|
936
|
+
path: path!,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function parseAiImage(argv: readonly string[]): CliCommand {
|
|
941
|
+
if (argv[0] !== "generate") fail("Usage: slopcamera ai image generate --model <id> [options]");
|
|
942
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
943
|
+
...JSON_SPEC,
|
|
944
|
+
"--allow-cloud-upload": "flag",
|
|
945
|
+
"--aspect-ratio": "value",
|
|
946
|
+
"--count": "value",
|
|
947
|
+
"--image": "repeat",
|
|
948
|
+
"--mask": "value",
|
|
949
|
+
"--max-per-call": "value",
|
|
950
|
+
"--max-output-tokens": "value",
|
|
951
|
+
"--model": "value",
|
|
952
|
+
"--prompt": "value",
|
|
953
|
+
"--prompt-file": "value",
|
|
954
|
+
"--provider-options": "value",
|
|
955
|
+
"--seed": "value",
|
|
956
|
+
"--size": "value",
|
|
957
|
+
"--stop": "repeat",
|
|
958
|
+
"--temperature": "value",
|
|
959
|
+
"--timeout": "value",
|
|
960
|
+
});
|
|
961
|
+
exactPositionals(parsed, 0, "slopcamera ai image generate --model <id> [options]");
|
|
962
|
+
const prompt = oneTextSource(parsed, "--prompt", "--prompt-file", { required: false });
|
|
963
|
+
const images = optionStrings(parsed, "--image");
|
|
964
|
+
if (images.length > 16) fail("--image may be specified at most 16 times.");
|
|
965
|
+
if (prompt.inline === undefined && prompt.path === undefined && images.length === 0) {
|
|
966
|
+
fail("Image generation requires a prompt or at least one --image reference.");
|
|
967
|
+
}
|
|
968
|
+
const model = optionString(parsed, "--model");
|
|
969
|
+
if (model === undefined) fail("--model is required.");
|
|
970
|
+
const count = strictInteger(optionString(parsed, "--count"), "--count", 1);
|
|
971
|
+
if (count > 16) fail("--count must be at most 16.");
|
|
972
|
+
const maxPerCall = optionalStrictInteger(optionString(parsed, "--max-per-call"), "--max-per-call");
|
|
973
|
+
if (maxPerCall !== undefined && maxPerCall > 16) fail("--max-per-call must be at most 16.");
|
|
974
|
+
if (maxPerCall !== undefined && maxPerCall < count) {
|
|
975
|
+
fail("--max-per-call must be at least --count; one CLI job maps to one paid Gateway call.");
|
|
976
|
+
}
|
|
977
|
+
return {
|
|
978
|
+
allowCloudUpload: optionFlag(parsed, "--allow-cloud-upload"),
|
|
979
|
+
aspectRatio: optionString(parsed, "--aspect-ratio"),
|
|
980
|
+
count,
|
|
981
|
+
images,
|
|
982
|
+
json: optionFlag(parsed, "--json"),
|
|
983
|
+
kind: "ai-image-generate",
|
|
984
|
+
mask: optionString(parsed, "--mask"),
|
|
985
|
+
maxPerCall,
|
|
986
|
+
maxOutputTokens: optionalStrictInteger(optionString(parsed, "--max-output-tokens"), "--max-output-tokens"),
|
|
987
|
+
model,
|
|
988
|
+
prompt: prompt.inline,
|
|
989
|
+
promptFile: prompt.path,
|
|
990
|
+
providerOptions: optionString(parsed, "--provider-options"),
|
|
991
|
+
seed: optionalNonNegativeInteger(optionString(parsed, "--seed"), "--seed"),
|
|
992
|
+
size: optionString(parsed, "--size"),
|
|
993
|
+
stopSequences: optionStrings(parsed, "--stop"),
|
|
994
|
+
temperature: optionalBoundedNumber(optionString(parsed, "--temperature"), "--temperature", 0, 100),
|
|
995
|
+
timeout: optionString(parsed, "--timeout") ?? "10m",
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function parseAiVideo(argv: readonly string[]): CliCommand {
|
|
1000
|
+
if (argv[0] !== "generate") fail("Usage: slopcamera ai video generate --model <id> [options]");
|
|
1001
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1002
|
+
...JSON_SPEC,
|
|
1003
|
+
"--allow-cloud-upload": "flag",
|
|
1004
|
+
"--aspect-ratio": "value",
|
|
1005
|
+
"--count": "value",
|
|
1006
|
+
"--duration": "value",
|
|
1007
|
+
"--fps": "value",
|
|
1008
|
+
"--frame": "repeat",
|
|
1009
|
+
"--generate-audio": "value",
|
|
1010
|
+
"--image": "value",
|
|
1011
|
+
"--max-per-call": "value",
|
|
1012
|
+
"--model": "value",
|
|
1013
|
+
"--prompt": "value",
|
|
1014
|
+
"--prompt-file": "value",
|
|
1015
|
+
"--provider-options": "value",
|
|
1016
|
+
"--reference": "repeat",
|
|
1017
|
+
"--resolution": "value",
|
|
1018
|
+
"--seed": "value",
|
|
1019
|
+
"--timeout": "value",
|
|
1020
|
+
});
|
|
1021
|
+
exactPositionals(parsed, 0, "slopcamera ai video generate --model <id> [options]");
|
|
1022
|
+
const prompt = oneTextSource(parsed, "--prompt", "--prompt-file", { required: false });
|
|
1023
|
+
const image = optionString(parsed, "--image");
|
|
1024
|
+
const frameImages = parseGatewayFrameInputs(optionStrings(parsed, "--frame"));
|
|
1025
|
+
const inputReferences = optionStrings(parsed, "--reference");
|
|
1026
|
+
if (inputReferences.length > 32) {
|
|
1027
|
+
fail("--reference may be specified at most 32 times.");
|
|
1028
|
+
}
|
|
1029
|
+
if (frameImages.length > 0 && inputReferences.length > 0) {
|
|
1030
|
+
fail("--frame and --reference are mutually exclusive because frame images take precedence in AI SDK video requests.");
|
|
1031
|
+
}
|
|
1032
|
+
if (
|
|
1033
|
+
image !== undefined
|
|
1034
|
+
&& frameImages.some(frame => frame.frameType === "first")
|
|
1035
|
+
) {
|
|
1036
|
+
fail("--image and --frame first are mutually exclusive because the first frame takes precedence.");
|
|
1037
|
+
}
|
|
1038
|
+
if (
|
|
1039
|
+
frameImages.some(frame => frame.frameType === "last")
|
|
1040
|
+
&& image === undefined
|
|
1041
|
+
&& !frameImages.some(frame => frame.frameType === "first")
|
|
1042
|
+
) {
|
|
1043
|
+
fail("--frame last requires --image or --frame first.");
|
|
1044
|
+
}
|
|
1045
|
+
if (
|
|
1046
|
+
prompt.inline === undefined
|
|
1047
|
+
&& prompt.path === undefined
|
|
1048
|
+
&& image === undefined
|
|
1049
|
+
&& frameImages.length === 0
|
|
1050
|
+
&& inputReferences.length === 0
|
|
1051
|
+
) {
|
|
1052
|
+
fail("Video generation requires a prompt, --image, --frame, or --reference.");
|
|
1053
|
+
}
|
|
1054
|
+
const model = optionString(parsed, "--model");
|
|
1055
|
+
if (model === undefined) fail("--model is required.");
|
|
1056
|
+
const count = strictInteger(optionString(parsed, "--count"), "--count", 1);
|
|
1057
|
+
if (count > 8) fail("--count must be at most 8.");
|
|
1058
|
+
const maxPerCall = optionalStrictInteger(optionString(parsed, "--max-per-call"), "--max-per-call");
|
|
1059
|
+
if (maxPerCall !== undefined && maxPerCall > 8) fail("--max-per-call must be at most 8.");
|
|
1060
|
+
if (maxPerCall !== undefined && maxPerCall < count) {
|
|
1061
|
+
fail("--max-per-call must be at least --count; one CLI job maps to one paid Gateway call.");
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
allowCloudUpload: optionFlag(parsed, "--allow-cloud-upload"),
|
|
1065
|
+
aspectRatio: optionString(parsed, "--aspect-ratio"),
|
|
1066
|
+
count,
|
|
1067
|
+
durationSeconds: optionalBoundedNumber(optionString(parsed, "--duration"), "--duration", 0.1, 3_600),
|
|
1068
|
+
fps: optionalStrictInteger(optionString(parsed, "--fps"), "--fps"),
|
|
1069
|
+
frameImages,
|
|
1070
|
+
generateAudio: optionString(parsed, "--generate-audio") === undefined
|
|
1071
|
+
? undefined
|
|
1072
|
+
: strictBoolean(optionString(parsed, "--generate-audio"), "--generate-audio", false),
|
|
1073
|
+
image,
|
|
1074
|
+
inputReferences,
|
|
1075
|
+
json: optionFlag(parsed, "--json"),
|
|
1076
|
+
kind: "ai-video-generate",
|
|
1077
|
+
maxPerCall,
|
|
1078
|
+
model,
|
|
1079
|
+
prompt: prompt.inline,
|
|
1080
|
+
promptFile: prompt.path,
|
|
1081
|
+
providerOptions: optionString(parsed, "--provider-options"),
|
|
1082
|
+
resolution: optionString(parsed, "--resolution"),
|
|
1083
|
+
seed: optionalNonNegativeInteger(optionString(parsed, "--seed"), "--seed"),
|
|
1084
|
+
timeout: optionString(parsed, "--timeout") ?? "30m",
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function parseAiSpeech(argv: readonly string[]): CliCommand {
|
|
1089
|
+
if (argv[0] !== "generate") fail("Usage: slopcamera ai speech generate --model <id> [options]");
|
|
1090
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1091
|
+
...JSON_SPEC,
|
|
1092
|
+
"--format": "value",
|
|
1093
|
+
"--instructions": "value",
|
|
1094
|
+
"--instructions-file": "value",
|
|
1095
|
+
"--language": "value",
|
|
1096
|
+
"--model": "value",
|
|
1097
|
+
"--provider-options": "value",
|
|
1098
|
+
"--speed": "value",
|
|
1099
|
+
"--text": "value",
|
|
1100
|
+
"--text-file": "value",
|
|
1101
|
+
"--timeout": "value",
|
|
1102
|
+
"--voice": "value",
|
|
1103
|
+
});
|
|
1104
|
+
exactPositionals(parsed, 0, "slopcamera ai speech generate --model <id> [options]");
|
|
1105
|
+
const text = oneTextSource(parsed, "--text", "--text-file", { required: true });
|
|
1106
|
+
const instructions = oneTextSource(parsed, "--instructions", "--instructions-file", { required: false });
|
|
1107
|
+
const model = optionString(parsed, "--model");
|
|
1108
|
+
if (model === undefined) fail("--model is required.");
|
|
1109
|
+
return {
|
|
1110
|
+
instructions: instructions.inline,
|
|
1111
|
+
instructionsFile: instructions.path,
|
|
1112
|
+
json: optionFlag(parsed, "--json"),
|
|
1113
|
+
kind: "ai-speech-generate",
|
|
1114
|
+
language: optionString(parsed, "--language"),
|
|
1115
|
+
model,
|
|
1116
|
+
outputFormat: optionString(parsed, "--format"),
|
|
1117
|
+
providerOptions: optionString(parsed, "--provider-options"),
|
|
1118
|
+
speed: optionalBoundedNumber(optionString(parsed, "--speed"), "--speed", 0.25, 4),
|
|
1119
|
+
text: text.inline,
|
|
1120
|
+
textFile: text.path,
|
|
1121
|
+
timeout: optionString(parsed, "--timeout") ?? "5m",
|
|
1122
|
+
voice: optionString(parsed, "--voice"),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function parseAiTranscribe(argv: readonly string[]): CliCommand {
|
|
1127
|
+
const parsed = parseOptions(argv, {
|
|
1128
|
+
...JSON_SPEC,
|
|
1129
|
+
"--allow-cloud-audio-upload": "flag",
|
|
1130
|
+
"--format": "value",
|
|
1131
|
+
"--model": "value",
|
|
1132
|
+
"--provider-options": "value",
|
|
1133
|
+
"--timeout": "value",
|
|
1134
|
+
});
|
|
1135
|
+
const [input] = exactPositionals(
|
|
1136
|
+
parsed,
|
|
1137
|
+
1,
|
|
1138
|
+
"slopcamera ai transcribe <audio-path> --model <id> --allow-cloud-audio-upload [options]",
|
|
1139
|
+
);
|
|
1140
|
+
const model = optionString(parsed, "--model");
|
|
1141
|
+
if (model === undefined) fail("--model is required.");
|
|
1142
|
+
return {
|
|
1143
|
+
allowCloudAudioUpload: optionFlag(parsed, "--allow-cloud-audio-upload"),
|
|
1144
|
+
format: oneOf(optionString(parsed, "--format"), "--format", [
|
|
1145
|
+
"all", "json", "srt", "text", "vtt",
|
|
1146
|
+
] as const, "all"),
|
|
1147
|
+
input: input!,
|
|
1148
|
+
json: optionFlag(parsed, "--json"),
|
|
1149
|
+
kind: "ai-transcribe",
|
|
1150
|
+
model,
|
|
1151
|
+
providerOptions: optionString(parsed, "--provider-options"),
|
|
1152
|
+
timeout: optionString(parsed, "--timeout") ?? "30m",
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function parseAi(argv: readonly string[]): CliCommand {
|
|
1157
|
+
switch (argv[0]) {
|
|
1158
|
+
case "models": return parseAiModels(argv.slice(1));
|
|
1159
|
+
case "provider-options": return parseAiProviderOptions(argv.slice(1));
|
|
1160
|
+
case "image": return parseAiImage(argv.slice(1));
|
|
1161
|
+
case "video": return parseAiVideo(argv.slice(1));
|
|
1162
|
+
case "speech": return parseAiSpeech(argv.slice(1));
|
|
1163
|
+
case "transcribe": return parseAiTranscribe(argv.slice(1));
|
|
1164
|
+
case undefined:
|
|
1165
|
+
default: fail("Usage: slopcamera ai <models|provider-options|image|video|speech|transcribe> [options]");
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function parseMediaAudio(argv: readonly string[]): CliCommand {
|
|
1170
|
+
const parsed = parseOptions(argv, {
|
|
1171
|
+
...JSON_SPEC,
|
|
1172
|
+
"--audio-stream": "value",
|
|
1173
|
+
"--compressor": "flag",
|
|
1174
|
+
"--compressor-attack-ms": "value",
|
|
1175
|
+
"--compressor-makeup-db": "value",
|
|
1176
|
+
"--compressor-ratio": "value",
|
|
1177
|
+
"--compressor-release-ms": "value",
|
|
1178
|
+
"--compressor-threshold-db": "value",
|
|
1179
|
+
"--delay-feedback": "value",
|
|
1180
|
+
"--delay-mix": "value",
|
|
1181
|
+
"--delay-ms": "value",
|
|
1182
|
+
"--denoise": "flag",
|
|
1183
|
+
"--denoise-reduction-db": "value",
|
|
1184
|
+
"--output": "value",
|
|
1185
|
+
"--reverb": "value",
|
|
1186
|
+
"--reverb-wet": "value",
|
|
1187
|
+
"--volume-db": "value",
|
|
1188
|
+
});
|
|
1189
|
+
const [input] = exactPositionals(parsed, 1, "slopcamera media audio <media-path> [effects] [--output <path>] [--json]");
|
|
1190
|
+
const compressorConfigured = [
|
|
1191
|
+
"--compressor-attack-ms",
|
|
1192
|
+
"--compressor-makeup-db",
|
|
1193
|
+
"--compressor-ratio",
|
|
1194
|
+
"--compressor-release-ms",
|
|
1195
|
+
"--compressor-threshold-db",
|
|
1196
|
+
].some(name => optionString(parsed, name) !== undefined);
|
|
1197
|
+
const denoiseConfigured = optionString(parsed, "--denoise-reduction-db") !== undefined;
|
|
1198
|
+
const volumeDb = optionalBoundedNumber(optionString(parsed, "--volume-db"), "--volume-db", -60, 24);
|
|
1199
|
+
const delayMs = optionalBoundedNumber(optionString(parsed, "--delay-ms"), "--delay-ms", 1, 10_000);
|
|
1200
|
+
if (delayMs !== undefined && !Number.isSafeInteger(delayMs)) {
|
|
1201
|
+
fail("--delay-ms must be a whole number of milliseconds.");
|
|
1202
|
+
}
|
|
1203
|
+
if (
|
|
1204
|
+
delayMs === undefined
|
|
1205
|
+
&& (
|
|
1206
|
+
optionString(parsed, "--delay-feedback") !== undefined
|
|
1207
|
+
|| optionString(parsed, "--delay-mix") !== undefined
|
|
1208
|
+
)
|
|
1209
|
+
) {
|
|
1210
|
+
fail("--delay-mix and --delay-feedback require --delay-ms.");
|
|
1211
|
+
}
|
|
1212
|
+
const reverb = optionString(parsed, "--reverb") === undefined
|
|
1213
|
+
? undefined
|
|
1214
|
+
: oneOf(optionString(parsed, "--reverb"), "--reverb", ["room", "hall", "plate"] as const);
|
|
1215
|
+
if (
|
|
1216
|
+
reverb === undefined
|
|
1217
|
+
&& optionString(parsed, "--reverb-wet") !== undefined
|
|
1218
|
+
) {
|
|
1219
|
+
fail("--reverb-wet requires --reverb.");
|
|
1220
|
+
}
|
|
1221
|
+
if (
|
|
1222
|
+
volumeDb === undefined
|
|
1223
|
+
&& !optionFlag(parsed, "--compressor")
|
|
1224
|
+
&& !compressorConfigured
|
|
1225
|
+
&& delayMs === undefined
|
|
1226
|
+
&& reverb === undefined
|
|
1227
|
+
&& !optionFlag(parsed, "--denoise")
|
|
1228
|
+
&& !denoiseConfigured
|
|
1229
|
+
) {
|
|
1230
|
+
fail("At least one audio effect is required.");
|
|
1231
|
+
}
|
|
1232
|
+
const audioStreamIndex = optionalNonNegativeInteger(
|
|
1233
|
+
optionString(parsed, "--audio-stream"),
|
|
1234
|
+
"--audio-stream",
|
|
1235
|
+
) ?? 0;
|
|
1236
|
+
if (audioStreamIndex > 255) fail("--audio-stream must be at most 255.");
|
|
1237
|
+
return {
|
|
1238
|
+
audioStreamIndex,
|
|
1239
|
+
compressor: optionFlag(parsed, "--compressor") || compressorConfigured,
|
|
1240
|
+
compressorAttackMs: boundedNumber(optionString(parsed, "--compressor-attack-ms"), "--compressor-attack-ms", 0.1, 2_000, 20),
|
|
1241
|
+
compressorMakeupDb: boundedNumber(optionString(parsed, "--compressor-makeup-db"), "--compressor-makeup-db", 0, 36, 0),
|
|
1242
|
+
compressorRatio: boundedNumber(optionString(parsed, "--compressor-ratio"), "--compressor-ratio", 1, 20, 4),
|
|
1243
|
+
compressorReleaseMs: boundedNumber(optionString(parsed, "--compressor-release-ms"), "--compressor-release-ms", 1, 9_000, 250),
|
|
1244
|
+
compressorThresholdDb: boundedNumber(optionString(parsed, "--compressor-threshold-db"), "--compressor-threshold-db", -60, 0, -18),
|
|
1245
|
+
delayFeedback: boundedNumber(optionString(parsed, "--delay-feedback"), "--delay-feedback", 0, 0.95, 0.35),
|
|
1246
|
+
delayMix: boundedNumber(optionString(parsed, "--delay-mix"), "--delay-mix", 0, 1, 0.25),
|
|
1247
|
+
delayMs,
|
|
1248
|
+
denoise: optionFlag(parsed, "--denoise") || denoiseConfigured,
|
|
1249
|
+
denoiseReductionDb: boundedNumber(optionString(parsed, "--denoise-reduction-db"), "--denoise-reduction-db", 0.01, 97, 12),
|
|
1250
|
+
input: input!,
|
|
1251
|
+
json: optionFlag(parsed, "--json"),
|
|
1252
|
+
kind: "media-audio",
|
|
1253
|
+
output: optionString(parsed, "--output"),
|
|
1254
|
+
reverb,
|
|
1255
|
+
reverbWet: boundedNumber(optionString(parsed, "--reverb-wet"), "--reverb-wet", 0, 1, 0.3),
|
|
1256
|
+
volumeDb,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function parseMediaColor(argv: readonly string[]): CliCommand {
|
|
1261
|
+
const parsed = parseOptions(argv, {
|
|
1262
|
+
...JSON_SPEC,
|
|
1263
|
+
"--brightness": "value",
|
|
1264
|
+
"--contrast": "value",
|
|
1265
|
+
"--gamma": "value",
|
|
1266
|
+
"--hue-degrees": "value",
|
|
1267
|
+
"--output": "value",
|
|
1268
|
+
"--preset": "value",
|
|
1269
|
+
"--saturation": "value",
|
|
1270
|
+
"--temperature": "value",
|
|
1271
|
+
"--tint": "value",
|
|
1272
|
+
"--video-stream": "value",
|
|
1273
|
+
});
|
|
1274
|
+
const [input] = exactPositionals(parsed, 1, "slopcamera media color <video-path> [grade] [--output <path>] [--json]");
|
|
1275
|
+
const preset = optionString(parsed, "--preset") === undefined
|
|
1276
|
+
? undefined
|
|
1277
|
+
: oneOf(optionString(parsed, "--preset"), "--preset", [
|
|
1278
|
+
"clean", "cool", "cinematic", "flat", "mono", "vivid", "warm",
|
|
1279
|
+
] as const);
|
|
1280
|
+
const explicitlyConfigured = [
|
|
1281
|
+
"--brightness", "--contrast", "--gamma", "--hue-degrees", "--saturation", "--temperature", "--tint",
|
|
1282
|
+
].some(name => optionString(parsed, name) !== undefined);
|
|
1283
|
+
if (preset === undefined && !explicitlyConfigured) fail("A color preset or at least one grade control is required.");
|
|
1284
|
+
const videoStreamIndex = optionalNonNegativeInteger(
|
|
1285
|
+
optionString(parsed, "--video-stream"),
|
|
1286
|
+
"--video-stream",
|
|
1287
|
+
) ?? 0;
|
|
1288
|
+
if (videoStreamIndex > 255) fail("--video-stream must be at most 255.");
|
|
1289
|
+
return {
|
|
1290
|
+
brightness: optionalBoundedNumber(optionString(parsed, "--brightness"), "--brightness", -0.5, 0.5),
|
|
1291
|
+
contrast: optionalBoundedNumber(optionString(parsed, "--contrast"), "--contrast", 0, 2),
|
|
1292
|
+
gamma: optionalBoundedNumber(optionString(parsed, "--gamma"), "--gamma", 0.1, 3),
|
|
1293
|
+
hueDegrees: optionalBoundedNumber(optionString(parsed, "--hue-degrees"), "--hue-degrees", -180, 180),
|
|
1294
|
+
input: input!,
|
|
1295
|
+
json: optionFlag(parsed, "--json"),
|
|
1296
|
+
kind: "media-color",
|
|
1297
|
+
output: optionString(parsed, "--output"),
|
|
1298
|
+
preset,
|
|
1299
|
+
saturation: optionalBoundedNumber(optionString(parsed, "--saturation"), "--saturation", 0, 3),
|
|
1300
|
+
temperature: optionalBoundedNumber(optionString(parsed, "--temperature"), "--temperature", -1, 1),
|
|
1301
|
+
tint: optionalBoundedNumber(optionString(parsed, "--tint"), "--tint", -1, 1),
|
|
1302
|
+
videoStreamIndex,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function parseMedia(argv: readonly string[]): CliCommand {
|
|
1307
|
+
switch (argv[0]) {
|
|
1308
|
+
case "audio": return parseMediaAudio(argv.slice(1));
|
|
1309
|
+
case "color": return parseMediaColor(argv.slice(1));
|
|
1310
|
+
case undefined:
|
|
1311
|
+
default: fail("Usage: slopcamera media <audio|color> <media-path> [options]");
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function parseRecordings(argv: readonly string[]): CliCommand {
|
|
1316
|
+
if (argv[0] !== "list") fail("Usage: slopcamera recordings list [--json] [--limit n]");
|
|
1317
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--limit": "value" });
|
|
1318
|
+
exactPositionals(parsed, 0, "slopcamera recordings list [--json] [--limit n]");
|
|
1319
|
+
return {
|
|
1320
|
+
json: optionFlag(parsed, "--json"),
|
|
1321
|
+
kind: "recordings-list",
|
|
1322
|
+
limit: strictInteger(optionString(parsed, "--limit"), "--limit", 50),
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function parseProjects(argv: readonly string[]): CliCommand {
|
|
1327
|
+
const action = argv[0];
|
|
1328
|
+
if (action === "list") {
|
|
1329
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--limit": "value" });
|
|
1330
|
+
exactPositionals(parsed, 0, "slopcamera projects list [--json] [--limit n]");
|
|
1331
|
+
return {
|
|
1332
|
+
json: optionFlag(parsed, "--json"),
|
|
1333
|
+
kind: "projects-list",
|
|
1334
|
+
limit: strictInteger(optionString(parsed, "--limit"), "--limit", 50),
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
if (action === "create") {
|
|
1338
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1339
|
+
...JSON_SPEC,
|
|
1340
|
+
"--from-recording": "value",
|
|
1341
|
+
"--name": "value",
|
|
1342
|
+
});
|
|
1343
|
+
exactPositionals(parsed, 0, "slopcamera projects create --from-recording <recording> [--name <name>] [--json]");
|
|
1344
|
+
const recording = optionString(parsed, "--from-recording");
|
|
1345
|
+
if (recording === undefined) fail("projects create requires --from-recording.");
|
|
1346
|
+
return {
|
|
1347
|
+
json: optionFlag(parsed, "--json"),
|
|
1348
|
+
kind: "projects-create",
|
|
1349
|
+
name: optionString(parsed, "--name"),
|
|
1350
|
+
recording,
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
fail("Usage: slopcamera projects <list|create> ...");
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function cameraFrame(value: string | undefined, name: string): CameraFrame {
|
|
1357
|
+
const values = tupleNumbers(value, name, 3);
|
|
1358
|
+
if (values === undefined) fail(`${name} is required.`);
|
|
1359
|
+
return [values[0]!, values[1]!, values[2]!] as const;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function cameraCenter(value: string | undefined): readonly [number, number] {
|
|
1363
|
+
const values = tupleNumbers(value, "--center", 2);
|
|
1364
|
+
if (values === undefined) fail("--center is required.");
|
|
1365
|
+
return [values[0]!, values[1]!] as const;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function cameraPathKeyframe(value: string): CameraPathKeyframe {
|
|
1369
|
+
const fields = value.split(",").map(field => field.trim());
|
|
1370
|
+
if (fields.length !== 4 || fields.some(field => field.length === 0)) {
|
|
1371
|
+
fail("--keyframe must be <time,center-x,center-y,zoom>.");
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
at: fields[0]!,
|
|
1375
|
+
frame: [
|
|
1376
|
+
strictNumber(fields[1], "--keyframe center-x"),
|
|
1377
|
+
strictNumber(fields[2], "--keyframe center-y"),
|
|
1378
|
+
strictNumber(fields[3], "--keyframe zoom"),
|
|
1379
|
+
],
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function parseProjectCamera(project: string, argv: readonly string[]): CliCommand {
|
|
1384
|
+
const action = argv[0];
|
|
1385
|
+
if (action === "show") {
|
|
1386
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
1387
|
+
exactPositionals(parsed, 0, "slopcamera project edit <project> camera show [--json]");
|
|
1388
|
+
return { action, json: optionFlag(parsed, "--json"), kind: "project-camera-edit", project };
|
|
1389
|
+
}
|
|
1390
|
+
if (action === "remove") {
|
|
1391
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--id": "value" });
|
|
1392
|
+
if (parsed.positionals.length > 1) {
|
|
1393
|
+
fail("Usage: slopcamera project edit <project> camera remove <camera-move-id> [--json]");
|
|
1394
|
+
}
|
|
1395
|
+
const cameraMoveId = parsed.positionals[0] ?? optionString(parsed, "--id");
|
|
1396
|
+
if (cameraMoveId === undefined) fail("camera remove requires a camera-move ID.");
|
|
1397
|
+
return {
|
|
1398
|
+
action,
|
|
1399
|
+
cameraMoveId,
|
|
1400
|
+
json: optionFlag(parsed, "--json"),
|
|
1401
|
+
kind: "project-camera-edit",
|
|
1402
|
+
project,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
if (action === "push") {
|
|
1406
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1407
|
+
...JSON_SPEC,
|
|
1408
|
+
...TIME_RANGE_SPEC,
|
|
1409
|
+
"--center": "value",
|
|
1410
|
+
"--easing": "value",
|
|
1411
|
+
"--end-zoom": "value",
|
|
1412
|
+
"--placement": "value",
|
|
1413
|
+
"--start-zoom": "value",
|
|
1414
|
+
"--stream": "value",
|
|
1415
|
+
});
|
|
1416
|
+
const [from, to] = timeRange(
|
|
1417
|
+
parsed,
|
|
1418
|
+
"slopcamera project edit <project> camera push --placement <id> --stream <id> --from <time> --to <time> --center <x,y> --end-zoom <z>",
|
|
1419
|
+
);
|
|
1420
|
+
const placement = optionString(parsed, "--placement");
|
|
1421
|
+
const stream = optionString(parsed, "--stream");
|
|
1422
|
+
if (placement === undefined || stream === undefined) {
|
|
1423
|
+
fail("camera push requires --placement and --stream.");
|
|
1424
|
+
}
|
|
1425
|
+
return {
|
|
1426
|
+
action,
|
|
1427
|
+
center: cameraCenter(optionString(parsed, "--center")),
|
|
1428
|
+
easing: optionString(parsed, "--easing") ?? "ease-in-out",
|
|
1429
|
+
endZoom: strictNumber(optionString(parsed, "--end-zoom"), "--end-zoom"),
|
|
1430
|
+
from,
|
|
1431
|
+
json: optionFlag(parsed, "--json"),
|
|
1432
|
+
kind: "project-camera-edit",
|
|
1433
|
+
placement,
|
|
1434
|
+
project,
|
|
1435
|
+
startZoom: strictNumber(optionString(parsed, "--start-zoom"), "--start-zoom", 1),
|
|
1436
|
+
stream,
|
|
1437
|
+
to,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
if (action === "reframe") {
|
|
1441
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1442
|
+
...JSON_SPEC,
|
|
1443
|
+
...TIME_RANGE_SPEC,
|
|
1444
|
+
"--easing": "value",
|
|
1445
|
+
"--from-frame": "value",
|
|
1446
|
+
"--placement": "value",
|
|
1447
|
+
"--stream": "value",
|
|
1448
|
+
"--to-frame": "value",
|
|
1449
|
+
});
|
|
1450
|
+
const [from, to] = timeRange(
|
|
1451
|
+
parsed,
|
|
1452
|
+
"slopcamera project edit <project> camera reframe --placement <id> --stream <id> --from <time> --to <time> --from-frame <x,y,z> --to-frame <x,y,z>",
|
|
1453
|
+
);
|
|
1454
|
+
const placement = optionString(parsed, "--placement");
|
|
1455
|
+
const stream = optionString(parsed, "--stream");
|
|
1456
|
+
if (placement === undefined || stream === undefined) {
|
|
1457
|
+
fail("camera reframe requires --placement and --stream.");
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
action,
|
|
1461
|
+
easing: optionString(parsed, "--easing") ?? "ease-in-out",
|
|
1462
|
+
from,
|
|
1463
|
+
fromFrame: cameraFrame(optionString(parsed, "--from-frame"), "--from-frame"),
|
|
1464
|
+
json: optionFlag(parsed, "--json"),
|
|
1465
|
+
kind: "project-camera-edit",
|
|
1466
|
+
placement,
|
|
1467
|
+
project,
|
|
1468
|
+
stream,
|
|
1469
|
+
to,
|
|
1470
|
+
toFrame: cameraFrame(optionString(parsed, "--to-frame"), "--to-frame"),
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
if (action === "path") {
|
|
1474
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1475
|
+
...JSON_SPEC,
|
|
1476
|
+
"--easing": "value",
|
|
1477
|
+
"--keyframe": "repeat",
|
|
1478
|
+
"--placement": "value",
|
|
1479
|
+
"--stream": "value",
|
|
1480
|
+
});
|
|
1481
|
+
exactPositionals(
|
|
1482
|
+
parsed,
|
|
1483
|
+
0,
|
|
1484
|
+
"slopcamera project edit <project> camera path --placement <id> --stream <id> --keyframe <time,x,y,zoom> --keyframe <time,x,y,zoom> ...",
|
|
1485
|
+
);
|
|
1486
|
+
const placement = optionString(parsed, "--placement");
|
|
1487
|
+
const stream = optionString(parsed, "--stream");
|
|
1488
|
+
if (placement === undefined || stream === undefined) {
|
|
1489
|
+
fail("camera path requires --placement and --stream.");
|
|
1490
|
+
}
|
|
1491
|
+
const values = optionStrings(parsed, "--keyframe");
|
|
1492
|
+
if (values.length < 2 || values.length > 4_096) {
|
|
1493
|
+
fail("camera path requires between 2 and 4096 --keyframe values.");
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
action,
|
|
1497
|
+
easing: optionString(parsed, "--easing") ?? "ease-in-out",
|
|
1498
|
+
json: optionFlag(parsed, "--json"),
|
|
1499
|
+
keyframes: values.map(cameraPathKeyframe),
|
|
1500
|
+
kind: "project-camera-edit",
|
|
1501
|
+
placement,
|
|
1502
|
+
project,
|
|
1503
|
+
stream,
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
if (action === "follow-faces") {
|
|
1507
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1508
|
+
...JSON_SPEC,
|
|
1509
|
+
...TIME_RANGE_SPEC,
|
|
1510
|
+
"--analysis": "value",
|
|
1511
|
+
"--easing": "value",
|
|
1512
|
+
"--framing": "value",
|
|
1513
|
+
"--gap-policy": "value",
|
|
1514
|
+
"--headroom": "value",
|
|
1515
|
+
"--max-zoom": "value",
|
|
1516
|
+
"--min-zoom": "value",
|
|
1517
|
+
"--output-height": "value",
|
|
1518
|
+
"--output-width": "value",
|
|
1519
|
+
"--placement": "value",
|
|
1520
|
+
"--require-all-selected": "flag",
|
|
1521
|
+
"--select": "value",
|
|
1522
|
+
"--smoothing": "value",
|
|
1523
|
+
"--track": "repeat",
|
|
1524
|
+
});
|
|
1525
|
+
const [from, to] = timeRange(
|
|
1526
|
+
parsed,
|
|
1527
|
+
"slopcamera project edit <project> camera follow-faces --placement <id> --analysis <id> --from <time> --to <time> (--track <id> ... | --select <largest|all>) [--require-all-selected]",
|
|
1528
|
+
);
|
|
1529
|
+
const placement = optionString(parsed, "--placement");
|
|
1530
|
+
const analysis = optionString(parsed, "--analysis");
|
|
1531
|
+
if (placement === undefined || analysis === undefined) {
|
|
1532
|
+
fail("camera follow-faces requires --placement and --analysis.");
|
|
1533
|
+
}
|
|
1534
|
+
const tracks = optionStrings(parsed, "--track");
|
|
1535
|
+
const selectValue = optionString(parsed, "--select");
|
|
1536
|
+
const select = selectValue === undefined
|
|
1537
|
+
? undefined
|
|
1538
|
+
: oneOf(selectValue, "--select", ["all", "largest"] as const);
|
|
1539
|
+
if ((tracks.length === 0) === (select === undefined)) {
|
|
1540
|
+
fail("camera follow-faces requires either one or more --track values or one --select mode.");
|
|
1541
|
+
}
|
|
1542
|
+
const requireAllSelected = optionFlag(parsed, "--require-all-selected");
|
|
1543
|
+
if (select === "largest" && requireAllSelected) {
|
|
1544
|
+
fail("--require-all-selected cannot be combined with dynamic --select largest.");
|
|
1545
|
+
}
|
|
1546
|
+
const minZoom = strictNumber(optionString(parsed, "--min-zoom"), "--min-zoom", 1);
|
|
1547
|
+
const maxZoom = strictNumber(optionString(parsed, "--max-zoom"), "--max-zoom", 2.2);
|
|
1548
|
+
if (minZoom < 1 || maxZoom < minZoom || maxZoom > 10) {
|
|
1549
|
+
fail("Face-follow zooms must satisfy 1 <= --min-zoom <= --max-zoom <= 10.");
|
|
1550
|
+
}
|
|
1551
|
+
const smoothing = strictNumber(optionString(parsed, "--smoothing"), "--smoothing", 0.75);
|
|
1552
|
+
const headroom = strictNumber(optionString(parsed, "--headroom"), "--headroom", 0.18);
|
|
1553
|
+
if (smoothing < 0 || smoothing > 60) fail("--smoothing must be between zero and 60 seconds.");
|
|
1554
|
+
if (headroom < 0 || headroom > 1) fail("--headroom must be between zero and one.");
|
|
1555
|
+
return {
|
|
1556
|
+
action,
|
|
1557
|
+
analysis,
|
|
1558
|
+
easing: optionString(parsed, "--easing") ?? "ease-in-out",
|
|
1559
|
+
framing: oneOf(
|
|
1560
|
+
optionString(parsed, "--framing"),
|
|
1561
|
+
"--framing",
|
|
1562
|
+
["tight", "medium", "wide", "group"] as const,
|
|
1563
|
+
"medium",
|
|
1564
|
+
),
|
|
1565
|
+
from,
|
|
1566
|
+
gapPolicy: oneOf(
|
|
1567
|
+
optionString(parsed, "--gap-policy"),
|
|
1568
|
+
"--gap-policy",
|
|
1569
|
+
["hold", "fallback", "fail"] as const,
|
|
1570
|
+
"hold",
|
|
1571
|
+
),
|
|
1572
|
+
headroom,
|
|
1573
|
+
json: optionFlag(parsed, "--json"),
|
|
1574
|
+
kind: "project-camera-edit",
|
|
1575
|
+
maxZoom,
|
|
1576
|
+
minZoom,
|
|
1577
|
+
outputHeight: strictEvenPositiveInteger(
|
|
1578
|
+
optionString(parsed, "--output-height"),
|
|
1579
|
+
"--output-height",
|
|
1580
|
+
1_080,
|
|
1581
|
+
),
|
|
1582
|
+
outputWidth: strictEvenPositiveInteger(
|
|
1583
|
+
optionString(parsed, "--output-width"),
|
|
1584
|
+
"--output-width",
|
|
1585
|
+
1_920,
|
|
1586
|
+
),
|
|
1587
|
+
placement,
|
|
1588
|
+
project,
|
|
1589
|
+
requireAllSelected,
|
|
1590
|
+
select,
|
|
1591
|
+
smoothing,
|
|
1592
|
+
to,
|
|
1593
|
+
tracks,
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
fail("Usage: slopcamera project edit <project> camera <push|reframe|path|follow-faces|show|remove> ...");
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function parseProject(argv: readonly string[]): CliCommand {
|
|
1600
|
+
const action = argv[0];
|
|
1601
|
+
if (action === "inspect") {
|
|
1602
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
1603
|
+
const [project] = exactPositionals(parsed, 1, "slopcamera project inspect <project> [--json]");
|
|
1604
|
+
return { json: optionFlag(parsed, "--json"), kind: "project-inspect", project: project! };
|
|
1605
|
+
}
|
|
1606
|
+
if (action === "add") {
|
|
1607
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1608
|
+
...JSON_SPEC,
|
|
1609
|
+
"--at": "value",
|
|
1610
|
+
"--role": "value",
|
|
1611
|
+
});
|
|
1612
|
+
const [project, path] = exactPositionals(
|
|
1613
|
+
parsed,
|
|
1614
|
+
2,
|
|
1615
|
+
"slopcamera project add <project> <path> --role <role> [--at <project-time>] [--json]",
|
|
1616
|
+
);
|
|
1617
|
+
return {
|
|
1618
|
+
at: optionString(parsed, "--at") ?? "0s",
|
|
1619
|
+
json: optionFlag(parsed, "--json"),
|
|
1620
|
+
kind: "project-add",
|
|
1621
|
+
path: path!,
|
|
1622
|
+
project: project!,
|
|
1623
|
+
role: oneOf(optionString(parsed, "--role"), "--role", [
|
|
1624
|
+
"screen", "camera", "b-roll", "system-audio", "microphone", "portable-audio", "music", "dialogue", "other",
|
|
1625
|
+
] as const),
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
if (action === "edit") {
|
|
1629
|
+
const project = argv[1];
|
|
1630
|
+
const operation = argv[2];
|
|
1631
|
+
if (project !== undefined && operation === "camera") {
|
|
1632
|
+
return parseProjectCamera(project, argv.slice(3));
|
|
1633
|
+
}
|
|
1634
|
+
if (project !== undefined && operation === "overlay") {
|
|
1635
|
+
const cleanArgs: string[] = [];
|
|
1636
|
+
let json = false;
|
|
1637
|
+
let fps: number | undefined;
|
|
1638
|
+
const operationArgs = argv.slice(3);
|
|
1639
|
+
for (let index = 0; index < operationArgs.length; index += 1) {
|
|
1640
|
+
const argument = operationArgs[index]!;
|
|
1641
|
+
if (argument === "--json") {
|
|
1642
|
+
if (json) fail("Option may be specified only once: --json");
|
|
1643
|
+
json = true;
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
if (argument === "--fps" || argument.startsWith("--fps=")) {
|
|
1647
|
+
if (fps !== undefined) fail("Option may be specified only once: --fps");
|
|
1648
|
+
const value = argument === "--fps" ? operationArgs[index + 1] : argument.slice("--fps=".length);
|
|
1649
|
+
if (value === undefined || value.startsWith("--")) fail("Option requires a value: --fps");
|
|
1650
|
+
if (argument === "--fps") index += 1;
|
|
1651
|
+
fps = strictNumber(value, "--fps");
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
cleanArgs.push(argument);
|
|
1655
|
+
}
|
|
1656
|
+
const edit = parseOverlay(cleanArgs);
|
|
1657
|
+
if (edit.operation !== "overlay-add" && edit.operation !== "overlay-remove") {
|
|
1658
|
+
fail("Project overlay edit did not produce an overlay operation.");
|
|
1659
|
+
}
|
|
1660
|
+
return { edit, fps, json, kind: "project-overlay-edit", project };
|
|
1661
|
+
}
|
|
1662
|
+
if (
|
|
1663
|
+
project !== undefined
|
|
1664
|
+
&& (operation === "zoom"
|
|
1665
|
+
|| operation === "cursor"
|
|
1666
|
+
|| operation === "clicks"
|
|
1667
|
+
|| operation === "keystrokes"
|
|
1668
|
+
|| operation === "typed-text")
|
|
1669
|
+
) {
|
|
1670
|
+
const operationArgs = argv.slice(3);
|
|
1671
|
+
const cleanArgs: string[] = [];
|
|
1672
|
+
let sourcePlacement: string | undefined;
|
|
1673
|
+
for (let index = 0; index < operationArgs.length; index += 1) {
|
|
1674
|
+
const argument = operationArgs[index]!;
|
|
1675
|
+
if (argument === "--source-placement" || argument.startsWith("--source-placement=")) {
|
|
1676
|
+
if (sourcePlacement !== undefined) fail("Option may be specified only once: --source-placement");
|
|
1677
|
+
const value = argument === "--source-placement"
|
|
1678
|
+
? operationArgs[index + 1]
|
|
1679
|
+
: argument.slice("--source-placement=".length);
|
|
1680
|
+
if (value === undefined || value === "" || value.startsWith("--")) {
|
|
1681
|
+
fail("Option requires a value: --source-placement");
|
|
1682
|
+
}
|
|
1683
|
+
if (argument === "--source-placement") index += 1;
|
|
1684
|
+
sourcePlacement = value;
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
cleanArgs.push(argument);
|
|
1688
|
+
}
|
|
1689
|
+
const parsedEdit = parseEdit([project, operation, ...cleanArgs]);
|
|
1690
|
+
if (parsedEdit.kind !== "edit" || (
|
|
1691
|
+
parsedEdit.edit.operation !== "zoom-add"
|
|
1692
|
+
&& parsedEdit.edit.operation !== "zoom-remove"
|
|
1693
|
+
&& parsedEdit.edit.operation !== "cursor"
|
|
1694
|
+
&& parsedEdit.edit.operation !== "clicks"
|
|
1695
|
+
&& parsedEdit.edit.operation !== "keystrokes"
|
|
1696
|
+
&& parsedEdit.edit.operation !== "typed-text"
|
|
1697
|
+
)) {
|
|
1698
|
+
fail("Project metadata edit did not produce a supported operation.");
|
|
1699
|
+
}
|
|
1700
|
+
return {
|
|
1701
|
+
edit: parsedEdit.edit,
|
|
1702
|
+
fps: parsedEdit.fps,
|
|
1703
|
+
json: parsedEdit.json,
|
|
1704
|
+
kind: "project-metadata-edit",
|
|
1705
|
+
project,
|
|
1706
|
+
sourcePlacement,
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
if (project === undefined || (operation !== "cut" && operation !== "trim" && operation !== "speed")) {
|
|
1710
|
+
fail("Usage: slopcamera project edit <project> <cut|trim|speed|camera|zoom|overlay|cursor|clicks|keystrokes|typed-text> ...");
|
|
1711
|
+
}
|
|
1712
|
+
const parsed = parseOptions(argv.slice(3), JSON_SPEC);
|
|
1713
|
+
if (operation === "speed") {
|
|
1714
|
+
const [from, to, rate] = exactPositionals(
|
|
1715
|
+
parsed,
|
|
1716
|
+
3,
|
|
1717
|
+
"slopcamera project edit <project> speed <from> <to> <rate> [--json]",
|
|
1718
|
+
);
|
|
1719
|
+
return {
|
|
1720
|
+
from: from!,
|
|
1721
|
+
json: optionFlag(parsed, "--json"),
|
|
1722
|
+
kind: "project-edit",
|
|
1723
|
+
operation,
|
|
1724
|
+
project,
|
|
1725
|
+
rate: strictNumber(rate, "rate"),
|
|
1726
|
+
to: to!,
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
const [from, to] = exactPositionals(
|
|
1730
|
+
parsed,
|
|
1731
|
+
2,
|
|
1732
|
+
`slopcamera project edit <project> ${operation} <from> <to> [--json]`,
|
|
1733
|
+
);
|
|
1734
|
+
return {
|
|
1735
|
+
from: from!,
|
|
1736
|
+
json: optionFlag(parsed, "--json"),
|
|
1737
|
+
kind: "project-edit",
|
|
1738
|
+
operation,
|
|
1739
|
+
project,
|
|
1740
|
+
to: to!,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
if (action === "render") {
|
|
1744
|
+
const renderAction = argv[1];
|
|
1745
|
+
if (renderAction !== "plan" && renderAction !== "run") {
|
|
1746
|
+
fail("Usage: slopcamera project render <plan|run> <project> [options]");
|
|
1747
|
+
}
|
|
1748
|
+
const parsed = parseOptions(argv.slice(2), {
|
|
1749
|
+
...JSON_SPEC,
|
|
1750
|
+
"--allow-unverified-sync": "flag",
|
|
1751
|
+
"--dry-run": "flag",
|
|
1752
|
+
"--fps": "value",
|
|
1753
|
+
"--height": "value",
|
|
1754
|
+
"--output": "value",
|
|
1755
|
+
"--width": "value",
|
|
1756
|
+
});
|
|
1757
|
+
const [project] = exactPositionals(parsed, 1, `slopcamera project render ${renderAction} <project> [options]`);
|
|
1758
|
+
if (renderAction === "plan" && optionFlag(parsed, "--dry-run")) fail("--dry-run is valid only for project render run.");
|
|
1759
|
+
return {
|
|
1760
|
+
action: renderAction,
|
|
1761
|
+
allowUnverifiedSync: optionFlag(parsed, "--allow-unverified-sync"),
|
|
1762
|
+
dryRun: optionFlag(parsed, "--dry-run"),
|
|
1763
|
+
fps: strictNumber(optionString(parsed, "--fps"), "--fps", 60),
|
|
1764
|
+
height: strictEvenPositiveInteger(optionString(parsed, "--height"), "--height", 1_080),
|
|
1765
|
+
json: optionFlag(parsed, "--json"),
|
|
1766
|
+
kind: "project-render",
|
|
1767
|
+
output: optionString(parsed, "--output"),
|
|
1768
|
+
project: project!,
|
|
1769
|
+
width: strictEvenPositiveInteger(optionString(parsed, "--width"), "--width", 1_920),
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
fail("Usage: slopcamera project <inspect|add|edit|render> ...");
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function parseInspect(argv: readonly string[]): CliCommand {
|
|
1776
|
+
const parsed = parseOptions(argv, { ...JSON_SPEC, "--fields": "value" });
|
|
1777
|
+
const [recording] = exactPositionals(parsed, 1, "slopcamera inspect <recording> [--json] [--fields csv]");
|
|
1778
|
+
const fieldsValue = optionString(parsed, "--fields");
|
|
1779
|
+
const fields = fieldsValue === undefined
|
|
1780
|
+
? undefined
|
|
1781
|
+
: fieldsValue.split(",").map((field) => field.trim()).filter((field) => field !== "");
|
|
1782
|
+
if (fields !== undefined && fields.length === 0) fail("--fields must name at least one field.");
|
|
1783
|
+
return { fields, json: optionFlag(parsed, "--json"), kind: "inspect", recording: recording! };
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function parseEvents(argv: readonly string[]): CliCommand {
|
|
1787
|
+
const parsed = parseOptions(argv, {
|
|
1788
|
+
"--around": "value",
|
|
1789
|
+
"--from": "value",
|
|
1790
|
+
"--fps": "value",
|
|
1791
|
+
"--json": "flag",
|
|
1792
|
+
"--jsonl": "flag",
|
|
1793
|
+
"--kind": "repeat",
|
|
1794
|
+
"--limit": "value",
|
|
1795
|
+
"--to": "value",
|
|
1796
|
+
});
|
|
1797
|
+
const [recording] = exactPositionals(
|
|
1798
|
+
parsed,
|
|
1799
|
+
1,
|
|
1800
|
+
"slopcamera events <recording> --kind <kind> [--from time] [--to time] [--around time] [--limit n] [--json|--jsonl]",
|
|
1801
|
+
);
|
|
1802
|
+
const eventKinds = optionStrings(parsed, "--kind").flatMap((value) => value.split(","))
|
|
1803
|
+
.map((value) => value.trim()).filter((value) => value !== "");
|
|
1804
|
+
if (eventKinds.length === 0) fail("events requires at least one --kind.");
|
|
1805
|
+
const json = optionFlag(parsed, "--json");
|
|
1806
|
+
const jsonl = optionFlag(parsed, "--jsonl");
|
|
1807
|
+
if (json && jsonl) fail("--json and --jsonl are mutually exclusive.");
|
|
1808
|
+
const limit = strictInteger(optionString(parsed, "--limit"), "--limit", 100);
|
|
1809
|
+
if (limit > MAX_EVENT_QUERY_LIMIT) {
|
|
1810
|
+
fail(`--limit cannot exceed ${MAX_EVENT_QUERY_LIMIT}.`);
|
|
1811
|
+
}
|
|
1812
|
+
return {
|
|
1813
|
+
around: optionString(parsed, "--around"),
|
|
1814
|
+
eventKinds,
|
|
1815
|
+
fps: optionString(parsed, "--fps") === undefined
|
|
1816
|
+
? undefined
|
|
1817
|
+
: strictNumber(optionString(parsed, "--fps"), "--fps"),
|
|
1818
|
+
format: json ? "json" : jsonl ? "jsonl" : "human",
|
|
1819
|
+
from: optionString(parsed, "--from"),
|
|
1820
|
+
kind: "events",
|
|
1821
|
+
limit,
|
|
1822
|
+
recording: recording!,
|
|
1823
|
+
to: optionString(parsed, "--to"),
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function parseRecord(argv: readonly string[]): CliCommand {
|
|
1828
|
+
const action = argv[0];
|
|
1829
|
+
if (action === "start") {
|
|
1830
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1831
|
+
...JSON_SPEC,
|
|
1832
|
+
"--camera-device": "value",
|
|
1833
|
+
"--display": "repeat",
|
|
1834
|
+
"--microphone": "value",
|
|
1835
|
+
"--microphone-device": "value",
|
|
1836
|
+
"--strict-inputs": "flag",
|
|
1837
|
+
"--system-audio": "value",
|
|
1838
|
+
"--typed-text": "value",
|
|
1839
|
+
"--webcam": "value",
|
|
1840
|
+
});
|
|
1841
|
+
exactPositionals(parsed, 0, "slopcamera record start [options]");
|
|
1842
|
+
const displays = optionStrings(parsed, "--display").map((displayId) =>
|
|
1843
|
+
captureSourceId(displayId, "--display", 64)!
|
|
1844
|
+
);
|
|
1845
|
+
if (displays.length > 16) fail("--display may be specified at most 16 times.");
|
|
1846
|
+
if (new Set(displays).size !== displays.length) fail("--display IDs must be unique.");
|
|
1847
|
+
const microphone = strictBoolean(optionString(parsed, "--microphone"), "--microphone", true);
|
|
1848
|
+
const webcam = strictBoolean(optionString(parsed, "--webcam"), "--webcam", true);
|
|
1849
|
+
const cameraDeviceId = captureSourceId(optionString(parsed, "--camera-device"), "--camera-device", 256);
|
|
1850
|
+
const microphoneDeviceId = captureSourceId(
|
|
1851
|
+
optionString(parsed, "--microphone-device"),
|
|
1852
|
+
"--microphone-device",
|
|
1853
|
+
256,
|
|
1854
|
+
);
|
|
1855
|
+
if (!webcam && cameraDeviceId !== undefined) {
|
|
1856
|
+
fail("--camera-device cannot be combined with --webcam false.");
|
|
1857
|
+
}
|
|
1858
|
+
if (!microphone && microphoneDeviceId !== undefined) {
|
|
1859
|
+
fail("--microphone-device cannot be combined with --microphone false.");
|
|
1860
|
+
}
|
|
1861
|
+
return {
|
|
1862
|
+
action,
|
|
1863
|
+
cameraDeviceId,
|
|
1864
|
+
displays,
|
|
1865
|
+
json: optionFlag(parsed, "--json"),
|
|
1866
|
+
kind: "record",
|
|
1867
|
+
microphone,
|
|
1868
|
+
microphoneDeviceId,
|
|
1869
|
+
strictInputs: optionFlag(parsed, "--strict-inputs"),
|
|
1870
|
+
systemAudio: strictBoolean(optionString(parsed, "--system-audio"), "--system-audio", true),
|
|
1871
|
+
typedText: strictBoolean(optionString(parsed, "--typed-text"), "--typed-text", false),
|
|
1872
|
+
webcam,
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
if (action === "pause" || action === "resume" || action === "stop" || action === "status") {
|
|
1876
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
1877
|
+
exactPositionals(parsed, 0, `slopcamera record ${action} [--json]`);
|
|
1878
|
+
return { action, json: optionFlag(parsed, "--json"), kind: "record" };
|
|
1879
|
+
}
|
|
1880
|
+
fail("Usage: slopcamera record <start|pause|resume|stop|status> [options]");
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const TIME_RANGE_SPEC = {
|
|
1884
|
+
"--end": "value",
|
|
1885
|
+
"--from": "value",
|
|
1886
|
+
"--start": "value",
|
|
1887
|
+
"--to": "value",
|
|
1888
|
+
} as const;
|
|
1889
|
+
|
|
1890
|
+
function timeRange(parsed: ParsedOptions, positionalUsage: string): readonly [string, string] {
|
|
1891
|
+
const flaggedFrom = optionString(parsed, "--from") ?? optionString(parsed, "--start");
|
|
1892
|
+
const flaggedTo = optionString(parsed, "--to") ?? optionString(parsed, "--end");
|
|
1893
|
+
if (optionString(parsed, "--from") !== undefined && optionString(parsed, "--start") !== undefined) {
|
|
1894
|
+
fail("Use only one of --from and --start.");
|
|
1895
|
+
}
|
|
1896
|
+
if (optionString(parsed, "--to") !== undefined && optionString(parsed, "--end") !== undefined) {
|
|
1897
|
+
fail("Use only one of --to and --end.");
|
|
1898
|
+
}
|
|
1899
|
+
if (flaggedFrom !== undefined || flaggedTo !== undefined) {
|
|
1900
|
+
if (parsed.positionals.length !== 0 || flaggedFrom === undefined || flaggedTo === undefined) {
|
|
1901
|
+
fail(`Usage: ${positionalUsage}`);
|
|
1902
|
+
}
|
|
1903
|
+
return [flaggedFrom, flaggedTo];
|
|
1904
|
+
}
|
|
1905
|
+
const [from, to] = exactPositionals(parsed, 2, positionalUsage);
|
|
1906
|
+
return [from!, to!];
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function tupleNumbers(value: string | undefined, name: string, size: number): readonly number[] | undefined {
|
|
1910
|
+
if (value === undefined) return undefined;
|
|
1911
|
+
const parts = value.split(",").map((part) => strictNumber(part.trim(), name));
|
|
1912
|
+
if (parts.length !== size) fail(`${name} requires ${size} comma-separated numbers.`);
|
|
1913
|
+
return parts;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function parseZoom(argv: readonly string[]): EditCommand {
|
|
1917
|
+
const verb = argv[0] === "add" || argv[0] === "remove" ? argv[0] : "add";
|
|
1918
|
+
const remaining = verb === "add" && argv[0] !== "add" ? argv : argv.slice(1);
|
|
1919
|
+
if (verb === "remove") {
|
|
1920
|
+
const parsed = parseOptions(remaining, { "--id": "value" });
|
|
1921
|
+
const positionalId = parsed.positionals[0];
|
|
1922
|
+
if (parsed.positionals.length > 1) fail("Usage: slopcamera edit <recording> zoom remove <id>");
|
|
1923
|
+
const id = positionalId ?? optionString(parsed, "--id");
|
|
1924
|
+
if (id === undefined) fail("zoom remove requires an ID.");
|
|
1925
|
+
return { id, operation: "zoom-remove" };
|
|
1926
|
+
}
|
|
1927
|
+
const parsed = parseOptions(remaining, {
|
|
1928
|
+
...TIME_RANGE_SPEC,
|
|
1929
|
+
"--display": "value",
|
|
1930
|
+
"--easing": "value",
|
|
1931
|
+
"--enter-duration": "value",
|
|
1932
|
+
"--exit-duration": "value",
|
|
1933
|
+
"--point": "value",
|
|
1934
|
+
"--rect": "value",
|
|
1935
|
+
"--scale": "value",
|
|
1936
|
+
"--target": "value",
|
|
1937
|
+
"--window": "value",
|
|
1938
|
+
});
|
|
1939
|
+
const [from, to] = timeRange(parsed, "slopcamera edit <recording> zoom [add] --from <time> --to <time> --target <target>");
|
|
1940
|
+
const target = oneOf(optionString(parsed, "--target"), "--target", [
|
|
1941
|
+
"rect", "point", "cursor", "window", "focused-input",
|
|
1942
|
+
] as const);
|
|
1943
|
+
const rectValues = tupleNumbers(optionString(parsed, "--rect"), "--rect", 4);
|
|
1944
|
+
const pointValues = tupleNumbers(optionString(parsed, "--point"), "--point", 2);
|
|
1945
|
+
const rect = rectValues === undefined
|
|
1946
|
+
? undefined
|
|
1947
|
+
: [rectValues[0]!, rectValues[1]!, rectValues[2]!, rectValues[3]!] as const;
|
|
1948
|
+
const point = pointValues === undefined ? undefined : [pointValues[0]!, pointValues[1]!] as const;
|
|
1949
|
+
if (target === "rect" && rect === undefined) fail("A rect zoom requires --rect x,y,width,height.");
|
|
1950
|
+
if (target === "point" && point === undefined) fail("A point zoom requires --point x,y.");
|
|
1951
|
+
if (target === "window" && optionString(parsed, "--window") === undefined) {
|
|
1952
|
+
fail("A window zoom requires --window <window-id-or-title>.");
|
|
1953
|
+
}
|
|
1954
|
+
return {
|
|
1955
|
+
display: optionString(parsed, "--display"),
|
|
1956
|
+
easing: optionString(parsed, "--easing") ?? "ease-in-out",
|
|
1957
|
+
enterDuration: optionString(parsed, "--enter-duration"),
|
|
1958
|
+
exitDuration: optionString(parsed, "--exit-duration"),
|
|
1959
|
+
from,
|
|
1960
|
+
operation: "zoom-add",
|
|
1961
|
+
point,
|
|
1962
|
+
rect,
|
|
1963
|
+
scale: strictNumber(optionString(parsed, "--scale"), "--scale", 2),
|
|
1964
|
+
target,
|
|
1965
|
+
to,
|
|
1966
|
+
window: optionString(parsed, "--window"),
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function parseOverlay(argv: readonly string[]): EditCommand {
|
|
1971
|
+
const verb = argv[0];
|
|
1972
|
+
if (verb === "remove") {
|
|
1973
|
+
const parsed = parseOptions(argv.slice(1), { "--id": "value" });
|
|
1974
|
+
if (parsed.positionals.length > 1) fail("Usage: slopcamera edit <recording> overlay remove <id>");
|
|
1975
|
+
const id = parsed.positionals[0] ?? optionString(parsed, "--id");
|
|
1976
|
+
if (id === undefined) fail("overlay remove requires an ID.");
|
|
1977
|
+
return { id, operation: "overlay-remove" };
|
|
1978
|
+
}
|
|
1979
|
+
if (verb !== "add") fail("Usage: slopcamera edit <recording> overlay <add|remove> ...");
|
|
1980
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
1981
|
+
...TIME_RANGE_SPEC,
|
|
1982
|
+
"--anchor": "value",
|
|
1983
|
+
"--animated-audio": "value",
|
|
1984
|
+
"--audio-volume": "value",
|
|
1985
|
+
"--blend-mode": "value",
|
|
1986
|
+
"--corner-radius": "value",
|
|
1987
|
+
"--crop": "value",
|
|
1988
|
+
"--duck-primary-to": "value",
|
|
1989
|
+
"--entrance": "value",
|
|
1990
|
+
"--entrance-duration": "value",
|
|
1991
|
+
"--entrance-from-scale": "value",
|
|
1992
|
+
"--easing": "value",
|
|
1993
|
+
"--exit": "value",
|
|
1994
|
+
"--exit-duration": "value",
|
|
1995
|
+
"--exit-to-scale": "value",
|
|
1996
|
+
"--fit": "value",
|
|
1997
|
+
"--freeze-end": "value",
|
|
1998
|
+
"--height": "value",
|
|
1999
|
+
"--kind": "value",
|
|
2000
|
+
"--loop": "value",
|
|
2001
|
+
"--keyframe": "repeat",
|
|
2002
|
+
"--opacity": "value",
|
|
2003
|
+
"--playback-rate": "value",
|
|
2004
|
+
"--position": "value",
|
|
2005
|
+
"--provider": "value",
|
|
2006
|
+
"--rotation": "value",
|
|
2007
|
+
"--scale": "value",
|
|
2008
|
+
"--source": "value",
|
|
2009
|
+
"--source-in": "value",
|
|
2010
|
+
"--source-out": "value",
|
|
2011
|
+
"--slide-distance": "value",
|
|
2012
|
+
"--variant": "value",
|
|
2013
|
+
"--width": "value",
|
|
2014
|
+
"--z-index": "value",
|
|
2015
|
+
});
|
|
2016
|
+
const [from, to] = timeRange(parsed, "slopcamera edit <recording> overlay add --kind <kind> --source <path-or-emoji> --from <time> --to <time>");
|
|
2017
|
+
const overlayKind = oneOf(optionString(parsed, "--kind"), "--kind", [
|
|
2018
|
+
"image", "svg", "gif", "video", "emoji",
|
|
2019
|
+
] as const);
|
|
2020
|
+
const source = optionString(parsed, "--source");
|
|
2021
|
+
if (source === undefined) fail("overlay add requires --source.");
|
|
2022
|
+
const positionValues = tupleNumbers(optionString(parsed, "--position"), "--position", 2) ?? [0, 0];
|
|
2023
|
+
const cropValues = tupleNumbers(optionString(parsed, "--crop"), "--crop", 4);
|
|
2024
|
+
const motionKeyframes = optionStrings(parsed, "--keyframe").map((value) => {
|
|
2025
|
+
const values = tupleNumbers(value, "--keyframe", 6)!;
|
|
2026
|
+
return {
|
|
2027
|
+
offset: values[0]!,
|
|
2028
|
+
opacity: values[5]!,
|
|
2029
|
+
position: [values[1]!, values[2]!] as const,
|
|
2030
|
+
rotation: values[4]!,
|
|
2031
|
+
scale: values[3]!,
|
|
2032
|
+
};
|
|
2033
|
+
});
|
|
2034
|
+
if (motionKeyframes.length === 1) fail("Overlay motion requires either zero or at least two --keyframe values.");
|
|
2035
|
+
return {
|
|
2036
|
+
anchor: oneOf(optionString(parsed, "--anchor"), "--anchor", [
|
|
2037
|
+
"top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right",
|
|
2038
|
+
] as const, "center"),
|
|
2039
|
+
animatedAudio: oneOf(optionString(parsed, "--animated-audio"), "--animated-audio", [
|
|
2040
|
+
"mute", "mix", "duck",
|
|
2041
|
+
] as const, "mute"),
|
|
2042
|
+
audioVolume: strictNumber(optionString(parsed, "--audio-volume"), "--audio-volume", 1),
|
|
2043
|
+
blendMode: oneOf(optionString(parsed, "--blend-mode"), "--blend-mode", [
|
|
2044
|
+
"normal", "addition", "darken", "lighten", "multiply", "overlay", "screen",
|
|
2045
|
+
] as const, "normal"),
|
|
2046
|
+
cornerRadius: optionString(parsed, "--corner-radius") === undefined
|
|
2047
|
+
? undefined
|
|
2048
|
+
: strictNumber(optionString(parsed, "--corner-radius"), "--corner-radius"),
|
|
2049
|
+
crop: cropValues === undefined
|
|
2050
|
+
? undefined
|
|
2051
|
+
: [cropValues[0]!, cropValues[1]!, cropValues[2]!, cropValues[3]!] as const,
|
|
2052
|
+
duckPrimaryTo: strictNumber(optionString(parsed, "--duck-primary-to"), "--duck-primary-to", 0.25),
|
|
2053
|
+
entrance: optionString(parsed, "--entrance"),
|
|
2054
|
+
entranceDuration: optionString(parsed, "--entrance-duration"),
|
|
2055
|
+
entranceFromScale: strictNumber(optionString(parsed, "--entrance-from-scale"), "--entrance-from-scale", 0.8),
|
|
2056
|
+
easing: optionString(parsed, "--easing") ?? "ease-in-out",
|
|
2057
|
+
exit: optionString(parsed, "--exit"),
|
|
2058
|
+
exitDuration: optionString(parsed, "--exit-duration"),
|
|
2059
|
+
exitToScale: strictNumber(optionString(parsed, "--exit-to-scale"), "--exit-to-scale", 0.8),
|
|
2060
|
+
fit: oneOf(optionString(parsed, "--fit"), "--fit", ["contain", "cover", "fill"] as const, "fill"),
|
|
2061
|
+
freezeEnd: strictBoolean(optionString(parsed, "--freeze-end"), "--freeze-end", false),
|
|
2062
|
+
from,
|
|
2063
|
+
height: optionString(parsed, "--height") === undefined
|
|
2064
|
+
? undefined
|
|
2065
|
+
: strictNumber(optionString(parsed, "--height"), "--height"),
|
|
2066
|
+
loop: strictBoolean(optionString(parsed, "--loop"), "--loop", false),
|
|
2067
|
+
motionKeyframes,
|
|
2068
|
+
opacity: strictNumber(optionString(parsed, "--opacity"), "--opacity", 1),
|
|
2069
|
+
operation: "overlay-add",
|
|
2070
|
+
overlayKind,
|
|
2071
|
+
playbackRate: strictNumber(optionString(parsed, "--playback-rate"), "--playback-rate", 1),
|
|
2072
|
+
position: [positionValues[0]!, positionValues[1]!],
|
|
2073
|
+
provider: oneOf(optionString(parsed, "--provider"), "--provider", [
|
|
2074
|
+
"auto", "apple-emoji-pack", "brand-catalog",
|
|
2075
|
+
] as const, "auto"),
|
|
2076
|
+
rotation: strictNumber(optionString(parsed, "--rotation"), "--rotation", 0),
|
|
2077
|
+
scale: strictNumber(optionString(parsed, "--scale"), "--scale", 1),
|
|
2078
|
+
source,
|
|
2079
|
+
sourceIn: optionString(parsed, "--source-in"),
|
|
2080
|
+
sourceOut: optionString(parsed, "--source-out"),
|
|
2081
|
+
slideDistance: strictNumber(optionString(parsed, "--slide-distance"), "--slide-distance", 48),
|
|
2082
|
+
to,
|
|
2083
|
+
variant: optionString(parsed, "--variant") === undefined
|
|
2084
|
+
? undefined
|
|
2085
|
+
: oneOf(optionString(parsed, "--variant"), "--variant", ["color", "duotone"] as const),
|
|
2086
|
+
width: optionString(parsed, "--width") === undefined
|
|
2087
|
+
? undefined
|
|
2088
|
+
: strictNumber(optionString(parsed, "--width"), "--width"),
|
|
2089
|
+
zIndex: strictNumber(optionString(parsed, "--z-index"), "--z-index", 0),
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function parseEdit(argv: readonly string[]): CliCommand {
|
|
2094
|
+
const recording = argv[0];
|
|
2095
|
+
const operation = argv[1];
|
|
2096
|
+
if (recording === undefined || operation === undefined) {
|
|
2097
|
+
fail("Usage: slopcamera edit <recording> <init|show|trim|cut|speed|zoom|overlay|cursor|clicks|keystrokes|typed-text> ...");
|
|
2098
|
+
}
|
|
2099
|
+
const operationArgs = argv.slice(2);
|
|
2100
|
+
let json = false;
|
|
2101
|
+
let fps: number | undefined;
|
|
2102
|
+
let edit: EditCommand;
|
|
2103
|
+
if (operation === "init" || operation === "show") {
|
|
2104
|
+
const parsed = parseOptions(operationArgs, { "--fps": "value", "--json": "flag" });
|
|
2105
|
+
exactPositionals(parsed, 0, `slopcamera edit <recording> ${operation} [--json]`);
|
|
2106
|
+
json = optionFlag(parsed, "--json");
|
|
2107
|
+
fps = optionString(parsed, "--fps") === undefined
|
|
2108
|
+
? undefined
|
|
2109
|
+
: strictNumber(optionString(parsed, "--fps"), "--fps");
|
|
2110
|
+
edit = { operation };
|
|
2111
|
+
} else {
|
|
2112
|
+
const cleanArgs: string[] = [];
|
|
2113
|
+
for (let index = 0; index < operationArgs.length; index += 1) {
|
|
2114
|
+
const argument = operationArgs[index]!;
|
|
2115
|
+
if (argument === "--json") {
|
|
2116
|
+
if (json) fail("Option may be specified only once: --json");
|
|
2117
|
+
json = true;
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
if (argument === "--fps" || argument.startsWith("--fps=")) {
|
|
2121
|
+
if (fps !== undefined) fail("Option may be specified only once: --fps");
|
|
2122
|
+
const value = argument === "--fps" ? operationArgs[index + 1] : argument.slice("--fps=".length);
|
|
2123
|
+
if (value === undefined || value.startsWith("--")) fail("Option requires a value: --fps");
|
|
2124
|
+
if (argument === "--fps") index += 1;
|
|
2125
|
+
fps = strictNumber(value, "--fps");
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
cleanArgs.push(argument);
|
|
2129
|
+
}
|
|
2130
|
+
if (operation === "trim" || operation === "cut") {
|
|
2131
|
+
const parsed = parseOptions(cleanArgs, TIME_RANGE_SPEC);
|
|
2132
|
+
const [from, to] = timeRange(parsed, `slopcamera edit <recording> ${operation} <from> <to>`);
|
|
2133
|
+
edit = { from, operation, to };
|
|
2134
|
+
} else if (operation === "speed") {
|
|
2135
|
+
const parsed = parseOptions(cleanArgs, { ...TIME_RANGE_SPEC, "--rate": "value" });
|
|
2136
|
+
let from: string;
|
|
2137
|
+
let to: string;
|
|
2138
|
+
let rate: number;
|
|
2139
|
+
if (parsed.positionals.length === 3 && optionString(parsed, "--rate") === undefined) {
|
|
2140
|
+
from = parsed.positionals[0]!;
|
|
2141
|
+
to = parsed.positionals[1]!;
|
|
2142
|
+
rate = strictNumber(parsed.positionals[2], "rate");
|
|
2143
|
+
} else {
|
|
2144
|
+
[from, to] = timeRange(parsed, "slopcamera edit <recording> speed --from <time> --to <time> --rate <number>");
|
|
2145
|
+
rate = strictNumber(optionString(parsed, "--rate"), "--rate");
|
|
2146
|
+
}
|
|
2147
|
+
edit = { from, operation, rate, to };
|
|
2148
|
+
} else if (operation === "zoom") {
|
|
2149
|
+
edit = parseZoom(cleanArgs);
|
|
2150
|
+
} else if (operation === "overlay") {
|
|
2151
|
+
edit = parseOverlay(cleanArgs);
|
|
2152
|
+
} else if (operation === "cursor") {
|
|
2153
|
+
const parsed = parseOptions(cleanArgs, {
|
|
2154
|
+
"--click-highlight": "value",
|
|
2155
|
+
"--enabled": "value",
|
|
2156
|
+
"--smoothing": "value",
|
|
2157
|
+
});
|
|
2158
|
+
if (parsed.positionals.length > 1) fail("Usage: slopcamera edit <recording> cursor <on|off> [options]");
|
|
2159
|
+
const toggle = parsed.positionals[0];
|
|
2160
|
+
if (toggle !== undefined && toggle !== "on" && toggle !== "off") fail("cursor state must be on or off.");
|
|
2161
|
+
edit = {
|
|
2162
|
+
clickHighlight: optionString(parsed, "--click-highlight") === undefined
|
|
2163
|
+
? undefined
|
|
2164
|
+
: strictBoolean(optionString(parsed, "--click-highlight"), "--click-highlight", true),
|
|
2165
|
+
enabled: toggle === undefined
|
|
2166
|
+
? strictBoolean(optionString(parsed, "--enabled"), "--enabled", true)
|
|
2167
|
+
: toggle === "on",
|
|
2168
|
+
operation: "cursor",
|
|
2169
|
+
smoothing: optionString(parsed, "--smoothing") === undefined
|
|
2170
|
+
? undefined
|
|
2171
|
+
: strictNumber(optionString(parsed, "--smoothing"), "--smoothing"),
|
|
2172
|
+
};
|
|
2173
|
+
} else if (operation === "keystrokes") {
|
|
2174
|
+
const parsed = parseOptions(cleanArgs, { "--enabled": "value", "--stop-after": "value" });
|
|
2175
|
+
if (parsed.positionals.length > 1) fail("Usage: slopcamera edit <recording> keystrokes <on|off> [--stop-after time]");
|
|
2176
|
+
const toggle = parsed.positionals[0];
|
|
2177
|
+
if (toggle !== undefined && toggle !== "on" && toggle !== "off") fail("keystrokes state must be on or off.");
|
|
2178
|
+
edit = {
|
|
2179
|
+
enabled: toggle === undefined
|
|
2180
|
+
? strictBoolean(optionString(parsed, "--enabled"), "--enabled", true)
|
|
2181
|
+
: toggle === "on",
|
|
2182
|
+
operation: "keystrokes",
|
|
2183
|
+
stopAfter: optionString(parsed, "--stop-after"),
|
|
2184
|
+
};
|
|
2185
|
+
} else if (operation === "clicks") {
|
|
2186
|
+
const parsed = parseOptions(cleanArgs, {
|
|
2187
|
+
"--color": "value",
|
|
2188
|
+
"--duration": "value",
|
|
2189
|
+
"--radius": "value",
|
|
2190
|
+
"--style": "value",
|
|
2191
|
+
});
|
|
2192
|
+
const [toggle] = exactPositionals(parsed, 1, "slopcamera edit <recording> clicks <on|off> [options]");
|
|
2193
|
+
if (toggle !== "on" && toggle !== "off") fail("clicks state must be on or off.");
|
|
2194
|
+
edit = {
|
|
2195
|
+
color: optionString(parsed, "--color") ?? "#ffcc00cc",
|
|
2196
|
+
duration: optionString(parsed, "--duration") ?? "350ms",
|
|
2197
|
+
enabled: toggle === "on",
|
|
2198
|
+
operation: "clicks",
|
|
2199
|
+
radius: strictNumber(optionString(parsed, "--radius"), "--radius", 28),
|
|
2200
|
+
style: oneOf(optionString(parsed, "--style"), "--style", ["pulse", "ring", "fill"] as const, "pulse"),
|
|
2201
|
+
};
|
|
2202
|
+
} else if (operation === "typed-text") {
|
|
2203
|
+
const parsed = parseOptions(cleanArgs, {
|
|
2204
|
+
"--idle-timeout": "value",
|
|
2205
|
+
"--max-characters": "value",
|
|
2206
|
+
"--placement": "value",
|
|
2207
|
+
});
|
|
2208
|
+
const [toggle] = exactPositionals(parsed, 1, "slopcamera edit <recording> typed-text <on|off> [options]");
|
|
2209
|
+
if (toggle !== "on" && toggle !== "off") fail("typed-text state must be on or off.");
|
|
2210
|
+
edit = {
|
|
2211
|
+
enabled: toggle === "on",
|
|
2212
|
+
idleTimeout: optionString(parsed, "--idle-timeout") ?? "1200ms",
|
|
2213
|
+
maxCharacters: strictInteger(optionString(parsed, "--max-characters"), "--max-characters", 160),
|
|
2214
|
+
operation: "typed-text",
|
|
2215
|
+
placement: oneOf(optionString(parsed, "--placement"), "--placement", ["input", "caption"] as const, "input"),
|
|
2216
|
+
};
|
|
2217
|
+
} else {
|
|
2218
|
+
fail(`Unknown edit operation: ${operation}`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return {
|
|
2222
|
+
edit,
|
|
2223
|
+
fps,
|
|
2224
|
+
json,
|
|
2225
|
+
kind: "edit",
|
|
2226
|
+
recording,
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
function parseAnalyze(argv: readonly string[]): CliCommand {
|
|
2231
|
+
if (argv[0] === "faces") {
|
|
2232
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2233
|
+
...JSON_SPEC,
|
|
2234
|
+
"--backend": "value",
|
|
2235
|
+
"--max-faces": "value",
|
|
2236
|
+
"--max-track-gap": "value",
|
|
2237
|
+
"--min-confidence": "value",
|
|
2238
|
+
"--sample-fps": "value",
|
|
2239
|
+
"--source": "value",
|
|
2240
|
+
});
|
|
2241
|
+
const [project] = exactPositionals(
|
|
2242
|
+
parsed,
|
|
2243
|
+
1,
|
|
2244
|
+
"slopcamera analyze faces <project> --source <asset:video-stream> [options] [--json]",
|
|
2245
|
+
);
|
|
2246
|
+
const source = optionString(parsed, "--source");
|
|
2247
|
+
if (source === undefined) fail("analyze faces requires --source.");
|
|
2248
|
+
const sampleFps = strictNumber(optionString(parsed, "--sample-fps"), "--sample-fps", 8);
|
|
2249
|
+
const minConfidence = strictNumber(
|
|
2250
|
+
optionString(parsed, "--min-confidence"),
|
|
2251
|
+
"--min-confidence",
|
|
2252
|
+
0.6,
|
|
2253
|
+
);
|
|
2254
|
+
const maxFaces = strictInteger(optionString(parsed, "--max-faces"), "--max-faces", 32);
|
|
2255
|
+
if (sampleFps <= 0 || sampleFps > 60) fail("--sample-fps must be greater than zero and at most 60.");
|
|
2256
|
+
if (minConfidence < 0 || minConfidence > 1) {
|
|
2257
|
+
fail("--min-confidence must be between zero and one.");
|
|
2258
|
+
}
|
|
2259
|
+
if (maxFaces > 128) fail("--max-faces must be at most 128.");
|
|
2260
|
+
return {
|
|
2261
|
+
backend: oneOf(
|
|
2262
|
+
optionString(parsed, "--backend"),
|
|
2263
|
+
"--backend",
|
|
2264
|
+
["auto", "vision"] as const,
|
|
2265
|
+
"auto",
|
|
2266
|
+
),
|
|
2267
|
+
json: optionFlag(parsed, "--json"),
|
|
2268
|
+
kind: "analyze-faces",
|
|
2269
|
+
maxFaces,
|
|
2270
|
+
maxTrackGap: optionString(parsed, "--max-track-gap") ?? "500ms",
|
|
2271
|
+
minConfidence,
|
|
2272
|
+
project: project!,
|
|
2273
|
+
sampleFps,
|
|
2274
|
+
source,
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
if (argv[0] === "music") {
|
|
2278
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2279
|
+
...JSON_SPEC,
|
|
2280
|
+
"--source": "value",
|
|
2281
|
+
"--window": "value",
|
|
2282
|
+
});
|
|
2283
|
+
const [project] = exactPositionals(parsed, 1, "slopcamera analyze music <project> --source <asset:stream> [--window <time>] [--json]");
|
|
2284
|
+
const source = optionString(parsed, "--source");
|
|
2285
|
+
if (source === undefined) fail("analyze music requires --source.");
|
|
2286
|
+
return {
|
|
2287
|
+
json: optionFlag(parsed, "--json"),
|
|
2288
|
+
kind: "analyze-music",
|
|
2289
|
+
project: project!,
|
|
2290
|
+
source,
|
|
2291
|
+
window: optionString(parsed, "--window") ?? "20s",
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
if (argv[0] === "scenes") {
|
|
2295
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2296
|
+
...JSON_SPEC,
|
|
2297
|
+
"--allow-cloud-upload": "flag",
|
|
2298
|
+
"--execute": "flag",
|
|
2299
|
+
"--max-scene-duration": "value",
|
|
2300
|
+
"--model": "value",
|
|
2301
|
+
"--scene-threshold": "value",
|
|
2302
|
+
"--source": "value",
|
|
2303
|
+
});
|
|
2304
|
+
const [project] = exactPositionals(
|
|
2305
|
+
parsed,
|
|
2306
|
+
1,
|
|
2307
|
+
"slopcamera analyze scenes <project> --source <asset:stream> [--execute --allow-cloud-upload] [options]",
|
|
2308
|
+
);
|
|
2309
|
+
const source = optionString(parsed, "--source");
|
|
2310
|
+
if (source === undefined) fail("analyze scenes requires --source.");
|
|
2311
|
+
const execute = optionFlag(parsed, "--execute");
|
|
2312
|
+
const allowCloudUpload = optionFlag(parsed, "--allow-cloud-upload");
|
|
2313
|
+
if (execute && !allowCloudUpload) {
|
|
2314
|
+
fail("Scene execution requires explicit --allow-cloud-upload acknowledgement.");
|
|
2315
|
+
}
|
|
2316
|
+
if (allowCloudUpload && !execute) {
|
|
2317
|
+
fail("--allow-cloud-upload is meaningful only with --execute.");
|
|
2318
|
+
}
|
|
2319
|
+
const sceneThreshold = strictNumber(optionString(parsed, "--scene-threshold"), "--scene-threshold", 0.35);
|
|
2320
|
+
if (sceneThreshold <= 0 || sceneThreshold > 1) fail("--scene-threshold must be greater than zero and at most one.");
|
|
2321
|
+
return {
|
|
2322
|
+
allowCloudUpload,
|
|
2323
|
+
execute,
|
|
2324
|
+
json: optionFlag(parsed, "--json"),
|
|
2325
|
+
kind: "analyze-scenes",
|
|
2326
|
+
maximumSceneDuration: optionString(parsed, "--max-scene-duration") ?? "20s",
|
|
2327
|
+
model: optionString(parsed, "--model") ?? "google/gemini-3-flash",
|
|
2328
|
+
project: project!,
|
|
2329
|
+
sceneThreshold,
|
|
2330
|
+
source,
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
if (argv[0] === "speech") {
|
|
2334
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2335
|
+
...JSON_SPEC,
|
|
2336
|
+
"--language": "value",
|
|
2337
|
+
"--min-filler-confidence": "value",
|
|
2338
|
+
"--model": "value",
|
|
2339
|
+
"--no-gpu": "flag",
|
|
2340
|
+
"--processors": "value",
|
|
2341
|
+
"--protect-music": "value",
|
|
2342
|
+
"--source": "value",
|
|
2343
|
+
"--speech-handle": "value",
|
|
2344
|
+
"--threads": "value",
|
|
2345
|
+
"--whisper": "value",
|
|
2346
|
+
});
|
|
2347
|
+
const [project] = exactPositionals(parsed, 1, "slopcamera analyze speech <project> --source <asset:stream> --model <path> [options]");
|
|
2348
|
+
const source = optionString(parsed, "--source");
|
|
2349
|
+
if (source === undefined) fail("analyze speech requires --source.");
|
|
2350
|
+
const minimumFillerConfidence = strictNumber(
|
|
2351
|
+
optionString(parsed, "--min-filler-confidence"),
|
|
2352
|
+
"--min-filler-confidence",
|
|
2353
|
+
0.82,
|
|
2354
|
+
);
|
|
2355
|
+
if (minimumFillerConfidence < 0 || minimumFillerConfidence > 1) {
|
|
2356
|
+
fail("--min-filler-confidence must be between zero and one.");
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
json: optionFlag(parsed, "--json"),
|
|
2360
|
+
kind: "analyze-speech",
|
|
2361
|
+
language: optionString(parsed, "--language") ?? "auto",
|
|
2362
|
+
minimumFillerConfidence,
|
|
2363
|
+
model: optionString(parsed, "--model"),
|
|
2364
|
+
noGpu: optionFlag(parsed, "--no-gpu"),
|
|
2365
|
+
processors: strictInteger(optionString(parsed, "--processors"), "--processors", 1),
|
|
2366
|
+
protectMusic: strictBoolean(optionString(parsed, "--protect-music"), "--protect-music", true),
|
|
2367
|
+
project: project!,
|
|
2368
|
+
source,
|
|
2369
|
+
speechHandle: optionString(parsed, "--speech-handle") ?? "120ms",
|
|
2370
|
+
threads: strictInteger(optionString(parsed, "--threads"), "--threads", 4),
|
|
2371
|
+
whisper: optionString(parsed, "--whisper"),
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
if (argv[0] === "zooms") {
|
|
2375
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2376
|
+
...JSON_SPEC,
|
|
2377
|
+
"--apply": "flag",
|
|
2378
|
+
"--plan": "value",
|
|
2379
|
+
});
|
|
2380
|
+
const [recording] = exactPositionals(parsed, 1, "slopcamera analyze zooms <recording> [--apply] [--plan <id>] [--json]");
|
|
2381
|
+
return {
|
|
2382
|
+
apply: optionFlag(parsed, "--apply"),
|
|
2383
|
+
json: optionFlag(parsed, "--json"),
|
|
2384
|
+
kind: "analyze-zooms",
|
|
2385
|
+
plan: optionString(parsed, "--plan"),
|
|
2386
|
+
recording: recording!,
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
if (argv[0] !== "inactivity") {
|
|
2390
|
+
fail("Usage: slopcamera analyze <faces|inactivity|zooms|music|scenes|speech> ...");
|
|
2391
|
+
}
|
|
2392
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2393
|
+
...JSON_SPEC,
|
|
2394
|
+
"--apply": "flag",
|
|
2395
|
+
"--handle": "value",
|
|
2396
|
+
"--min-duration": "value",
|
|
2397
|
+
"--motion-threshold": "value",
|
|
2398
|
+
"--protect-audio": "value",
|
|
2399
|
+
"--speed-rate": "value",
|
|
2400
|
+
});
|
|
2401
|
+
const [recording] = exactPositionals(parsed, 1, "slopcamera analyze inactivity <recording|project> [options]");
|
|
2402
|
+
const threshold = strictNumber(optionString(parsed, "--motion-threshold"), "--motion-threshold", 0.003);
|
|
2403
|
+
if (threshold < 0 || threshold > 1) fail("--motion-threshold must be between 0 and 1.");
|
|
2404
|
+
const speedRate = strictNumber(optionString(parsed, "--speed-rate"), "--speed-rate", 8);
|
|
2405
|
+
if (speedRate <= 1 || speedRate > 64) fail("--speed-rate must be greater than 1 and at most 64.");
|
|
2406
|
+
return {
|
|
2407
|
+
apply: optionFlag(parsed, "--apply"),
|
|
2408
|
+
handle: oneOf(optionString(parsed, "--handle"), "--handle", ["cut", "speed", "keep"] as const, "cut"),
|
|
2409
|
+
json: optionFlag(parsed, "--json"),
|
|
2410
|
+
kind: "analyze-inactivity",
|
|
2411
|
+
minDuration: optionString(parsed, "--min-duration") ?? "3s",
|
|
2412
|
+
motionThreshold: threshold,
|
|
2413
|
+
protectAudio: strictBoolean(optionString(parsed, "--protect-audio"), "--protect-audio", true),
|
|
2414
|
+
recording: recording!,
|
|
2415
|
+
speedRate,
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function parseFaces(argv: readonly string[]): CliCommand {
|
|
2420
|
+
if (argv[0] !== "list") {
|
|
2421
|
+
fail("Usage: slopcamera faces list <project> <analysis-id> [options] [--json]");
|
|
2422
|
+
}
|
|
2423
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2424
|
+
...JSON_SPEC,
|
|
2425
|
+
"--at": "value",
|
|
2426
|
+
"--limit": "value",
|
|
2427
|
+
"--min-confidence": "value",
|
|
2428
|
+
"--min-duration": "value",
|
|
2429
|
+
});
|
|
2430
|
+
const [project, analysis] = exactPositionals(
|
|
2431
|
+
parsed,
|
|
2432
|
+
2,
|
|
2433
|
+
"slopcamera faces list <project> <analysis-id> [options] [--json]",
|
|
2434
|
+
);
|
|
2435
|
+
const minConfidence = strictNumber(
|
|
2436
|
+
optionString(parsed, "--min-confidence"),
|
|
2437
|
+
"--min-confidence",
|
|
2438
|
+
0.6,
|
|
2439
|
+
);
|
|
2440
|
+
if (minConfidence < 0 || minConfidence > 1) {
|
|
2441
|
+
fail("--min-confidence must be between zero and one.");
|
|
2442
|
+
}
|
|
2443
|
+
const limit = strictInteger(optionString(parsed, "--limit"), "--limit", 20);
|
|
2444
|
+
if (limit > 1_000) fail("--limit must be at most 1000.");
|
|
2445
|
+
return {
|
|
2446
|
+
analysis: analysis!,
|
|
2447
|
+
at: optionString(parsed, "--at"),
|
|
2448
|
+
json: optionFlag(parsed, "--json"),
|
|
2449
|
+
kind: "faces-list",
|
|
2450
|
+
limit,
|
|
2451
|
+
minConfidence,
|
|
2452
|
+
minDuration: optionString(parsed, "--min-duration") ?? "0s",
|
|
2453
|
+
project: project!,
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function parseFillers(argv: readonly string[]): CliCommand {
|
|
2458
|
+
const action = argv[0];
|
|
2459
|
+
if (action === "list") {
|
|
2460
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--auto-only": "flag" });
|
|
2461
|
+
const [project, analysis] = exactPositionals(
|
|
2462
|
+
parsed,
|
|
2463
|
+
2,
|
|
2464
|
+
"slopcamera fillers list <project> <speech-analysis-id> [--auto-only] [--json]",
|
|
2465
|
+
);
|
|
2466
|
+
return {
|
|
2467
|
+
analysis: analysis!,
|
|
2468
|
+
autoOnly: optionFlag(parsed, "--auto-only"),
|
|
2469
|
+
json: optionFlag(parsed, "--json"),
|
|
2470
|
+
kind: "fillers-list",
|
|
2471
|
+
project: project!,
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
if (action === "apply") {
|
|
2475
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--placement": "value" });
|
|
2476
|
+
const [project, analysis, candidate] = exactPositionals(
|
|
2477
|
+
parsed,
|
|
2478
|
+
3,
|
|
2479
|
+
"slopcamera fillers apply <project> <speech-analysis-id> <candidate-id> [--placement <id>] [--json]",
|
|
2480
|
+
);
|
|
2481
|
+
return {
|
|
2482
|
+
analysis: analysis!,
|
|
2483
|
+
candidate: candidate!,
|
|
2484
|
+
json: optionFlag(parsed, "--json"),
|
|
2485
|
+
kind: "fillers-apply",
|
|
2486
|
+
placement: optionString(parsed, "--placement"),
|
|
2487
|
+
project: project!,
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
fail("Usage: slopcamera fillers <list|apply> ...");
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
function parseAlign(argv: readonly string[]): CliCommand {
|
|
2494
|
+
const action = argv[0];
|
|
2495
|
+
if (action === "analyze") {
|
|
2496
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2497
|
+
...JSON_SPEC,
|
|
2498
|
+
"--apply": "flag",
|
|
2499
|
+
"--candidate": "value",
|
|
2500
|
+
"--max-offset": "value",
|
|
2501
|
+
"--reference": "value",
|
|
2502
|
+
"--reference-placement": "value",
|
|
2503
|
+
"--target": "value",
|
|
2504
|
+
"--target-placement": "value",
|
|
2505
|
+
});
|
|
2506
|
+
const [project] = exactPositionals(parsed, 1, "slopcamera align analyze <project> --reference <asset:stream> --target <asset:stream> [options]");
|
|
2507
|
+
const reference = optionString(parsed, "--reference");
|
|
2508
|
+
const target = optionString(parsed, "--target");
|
|
2509
|
+
if (reference === undefined || target === undefined) fail("align analyze requires --reference and --target.");
|
|
2510
|
+
if (optionString(parsed, "--candidate") !== undefined && !optionFlag(parsed, "--apply")) {
|
|
2511
|
+
fail("--candidate is meaningful only with --apply.");
|
|
2512
|
+
}
|
|
2513
|
+
return {
|
|
2514
|
+
apply: optionFlag(parsed, "--apply"),
|
|
2515
|
+
candidate: optionString(parsed, "--candidate"),
|
|
2516
|
+
json: optionFlag(parsed, "--json"),
|
|
2517
|
+
kind: "align-analyze",
|
|
2518
|
+
maxOffset: optionString(parsed, "--max-offset"),
|
|
2519
|
+
project: project!,
|
|
2520
|
+
reference,
|
|
2521
|
+
referencePlacement: optionString(parsed, "--reference-placement"),
|
|
2522
|
+
target,
|
|
2523
|
+
targetPlacement: optionString(parsed, "--target-placement"),
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
if (action === "apply") {
|
|
2527
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2528
|
+
...JSON_SPEC,
|
|
2529
|
+
"--candidate": "value",
|
|
2530
|
+
"--reference-placement": "value",
|
|
2531
|
+
"--target-placement": "value",
|
|
2532
|
+
});
|
|
2533
|
+
const [project, analysis] = exactPositionals(
|
|
2534
|
+
parsed,
|
|
2535
|
+
2,
|
|
2536
|
+
"slopcamera align apply <project> <analysis-id> --candidate <candidate-id> [options]",
|
|
2537
|
+
);
|
|
2538
|
+
const candidate = optionString(parsed, "--candidate");
|
|
2539
|
+
if (candidate === undefined) fail("align apply requires --candidate.");
|
|
2540
|
+
return {
|
|
2541
|
+
analysis: analysis!,
|
|
2542
|
+
candidate,
|
|
2543
|
+
json: optionFlag(parsed, "--json"),
|
|
2544
|
+
kind: "align-apply",
|
|
2545
|
+
project: project!,
|
|
2546
|
+
referencePlacement: optionString(parsed, "--reference-placement"),
|
|
2547
|
+
targetPlacement: optionString(parsed, "--target-placement"),
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
fail("Usage: slopcamera align <analyze|apply> ...");
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function parseRender(argv: readonly string[]): CliCommand {
|
|
2554
|
+
const action = argv[0];
|
|
2555
|
+
if (action !== "plan" && action !== "run") {
|
|
2556
|
+
fail("Usage: slopcamera render <plan|run> <recording> [options]");
|
|
2557
|
+
}
|
|
2558
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2559
|
+
...JSON_SPEC,
|
|
2560
|
+
"--dry-run": "flag",
|
|
2561
|
+
"--display": "value",
|
|
2562
|
+
"--keep-inactivity": "flag",
|
|
2563
|
+
"--no-auto-inactivity": "flag",
|
|
2564
|
+
"--output": "value",
|
|
2565
|
+
});
|
|
2566
|
+
const [recording] = exactPositionals(parsed, 1, `slopcamera render ${action} <recording> [options]`);
|
|
2567
|
+
const keepInactivity = optionFlag(parsed, "--keep-inactivity");
|
|
2568
|
+
const noAutoInactivity = optionFlag(parsed, "--no-auto-inactivity");
|
|
2569
|
+
if (keepInactivity && noAutoInactivity) fail("Use only one inactivity opt-out flag.");
|
|
2570
|
+
const autoInactivity = !keepInactivity && !noAutoInactivity;
|
|
2571
|
+
if (action === "plan") {
|
|
2572
|
+
if (optionFlag(parsed, "--dry-run")) fail("--dry-run is valid only for render run.");
|
|
2573
|
+
return {
|
|
2574
|
+
autoInactivity,
|
|
2575
|
+
display: optionString(parsed, "--display") ?? "primary",
|
|
2576
|
+
json: optionFlag(parsed, "--json"),
|
|
2577
|
+
kind: "render-plan",
|
|
2578
|
+
output: optionString(parsed, "--output"),
|
|
2579
|
+
recording: recording!,
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
return {
|
|
2583
|
+
autoInactivity,
|
|
2584
|
+
display: optionString(parsed, "--display") ?? "primary",
|
|
2585
|
+
dryRun: optionFlag(parsed, "--dry-run"),
|
|
2586
|
+
json: optionFlag(parsed, "--json"),
|
|
2587
|
+
kind: "render-run",
|
|
2588
|
+
output: optionString(parsed, "--output"),
|
|
2589
|
+
recording: recording!,
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
function parseAssets(argv: readonly string[]): CliCommand {
|
|
2594
|
+
if (argv[0] !== "emoji" || (argv[1] !== "search" && argv[1] !== "resolve")) {
|
|
2595
|
+
fail("Usage: slopcamera assets emoji <search|resolve> <glyph|name|hex-id> [options]");
|
|
2596
|
+
}
|
|
2597
|
+
const action = argv[1];
|
|
2598
|
+
const parsed = parseOptions(argv.slice(2), {
|
|
2599
|
+
...JSON_SPEC,
|
|
2600
|
+
"--limit": "value",
|
|
2601
|
+
"--provider": "value",
|
|
2602
|
+
"--variant": "value",
|
|
2603
|
+
});
|
|
2604
|
+
const [query] = exactPositionals(parsed, 1, `slopcamera assets emoji ${action} <glyph|name|hex-id> [options]`);
|
|
2605
|
+
const variant = optionString(parsed, "--variant") === undefined
|
|
2606
|
+
? undefined
|
|
2607
|
+
: oneOf(optionString(parsed, "--variant"), "--variant", ["color", "duotone"] as const);
|
|
2608
|
+
const provider = oneOf(optionString(parsed, "--provider"), "--provider", [
|
|
2609
|
+
"auto", "all", "apple-emoji-pack", "brand-catalog",
|
|
2610
|
+
] as const, action === "search" ? "all" : "auto");
|
|
2611
|
+
if (action === "search") {
|
|
2612
|
+
if (provider === "auto") fail("Emoji search provider must be all, apple-emoji-pack, or brand-catalog.");
|
|
2613
|
+
return {
|
|
2614
|
+
json: optionFlag(parsed, "--json"),
|
|
2615
|
+
kind: "emoji-search",
|
|
2616
|
+
limit: strictInteger(optionString(parsed, "--limit"), "--limit", 20),
|
|
2617
|
+
provider,
|
|
2618
|
+
query: query!,
|
|
2619
|
+
variant,
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
if (provider === "all") fail("Emoji resolve provider must be auto, apple-emoji-pack, or brand-catalog.");
|
|
2623
|
+
return {
|
|
2624
|
+
json: optionFlag(parsed, "--json"),
|
|
2625
|
+
kind: "emoji-resolve",
|
|
2626
|
+
provider,
|
|
2627
|
+
query: query!,
|
|
2628
|
+
variant,
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
const NATIVE_STUDIO_SPEC = { "--studio-python": "value", "--studio-blender-bin": "value", "--allow-trusted-code": "flag" } as const;
|
|
2633
|
+
function studioWorkflowOptions(parsed: ParsedOptions): StudioWorkflowOptions {
|
|
2634
|
+
const python = optionString(parsed, "--studio-python"), blender = optionString(parsed, "--studio-blender-bin"), allowTrustedCode = optionFlag(parsed, "--allow-trusted-code");
|
|
2635
|
+
return python === undefined && blender === undefined && !allowTrustedCode ? {} : { studio: { allowTrustedCode, ...(python === undefined ? {} : { python }), ...(blender === undefined ? {} : { blender }) } };
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
const RUN_OUTPUT_SPEC = {
|
|
2639
|
+
...JSON_SPEC,
|
|
2640
|
+
"--jobs": "value",
|
|
2641
|
+
"--jsonl": "flag",
|
|
2642
|
+
} as const;
|
|
2643
|
+
|
|
2644
|
+
function parseJobs(parsed: ParsedOptions): number {
|
|
2645
|
+
const jobs = strictInteger(optionString(parsed, "--jobs"), "--jobs", 4);
|
|
2646
|
+
if (jobs < 1 || jobs > 64) fail("--jobs must be from 1 through 64.");
|
|
2647
|
+
return jobs;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
function runOutput(parsed: ParsedOptions): {
|
|
2651
|
+
readonly jobs: number;
|
|
2652
|
+
readonly json: boolean;
|
|
2653
|
+
readonly jsonl: boolean;
|
|
2654
|
+
} {
|
|
2655
|
+
const json = optionFlag(parsed, "--json");
|
|
2656
|
+
const jsonl = optionFlag(parsed, "--jsonl");
|
|
2657
|
+
if (json && jsonl) fail("Use only one of --json or --jsonl.");
|
|
2658
|
+
return { jobs: parseJobs(parsed), json: json || jsonl, jsonl };
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
function checkedSha256(value: string | undefined, name: string): string | undefined {
|
|
2662
|
+
if (value === undefined) return undefined;
|
|
2663
|
+
if (!/^[a-f0-9]{64}$/u.test(value)) fail(`${name} must be a lowercase SHA-256 digest.`);
|
|
2664
|
+
return value;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function checkedRunId(value: string): string {
|
|
2668
|
+
if (!/^run_[a-z0-9][a-z0-9_-]{5,95}$/u.test(value)) {
|
|
2669
|
+
fail("Run IDs must use the run_<safe-id> form.");
|
|
2670
|
+
}
|
|
2671
|
+
return value;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
function checkedNodeKey(value: string): string {
|
|
2675
|
+
if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*(?:\/[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*)*$/u.test(value)) {
|
|
2676
|
+
fail("Node keys must use slash-separated lowercase safe identifiers.");
|
|
2677
|
+
}
|
|
2678
|
+
return value;
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function parseOperations(argv: readonly string[]): CliCommand {
|
|
2682
|
+
const action = argv[0];
|
|
2683
|
+
if (action === "list") {
|
|
2684
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
2685
|
+
exactPositionals(parsed, 0, "slopcamera operations list [--json]");
|
|
2686
|
+
return { json: optionFlag(parsed, "--json"), kind: "operations-list" };
|
|
2687
|
+
}
|
|
2688
|
+
if (action === "show") {
|
|
2689
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
2690
|
+
const [operation] = exactPositionals(
|
|
2691
|
+
parsed,
|
|
2692
|
+
1,
|
|
2693
|
+
"slopcamera operations show <kind>[@<version>] [--json]",
|
|
2694
|
+
);
|
|
2695
|
+
return { json: optionFlag(parsed, "--json"), kind: "operations-show", operation: operation! };
|
|
2696
|
+
}
|
|
2697
|
+
fail("Usage: slopcamera operations <list|show> ...");
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function parseDiagram(argv: readonly string[]): CliCommand {
|
|
2701
|
+
const action = argv[0];
|
|
2702
|
+
if (action === "check") {
|
|
2703
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
2704
|
+
const [path] = exactPositionals(parsed, 1, "slopcamera diagram check <diagram.json> [--json]");
|
|
2705
|
+
return { json: optionFlag(parsed, "--json"), kind: "diagram-check", path: path! };
|
|
2706
|
+
}
|
|
2707
|
+
if (action === "render") {
|
|
2708
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2709
|
+
...JSON_SPEC,
|
|
2710
|
+
"--scale": "value",
|
|
2711
|
+
});
|
|
2712
|
+
const [path] = exactPositionals(
|
|
2713
|
+
parsed,
|
|
2714
|
+
1,
|
|
2715
|
+
"slopcamera diagram render <diagram.json> [--scale <0..4>] [--json]",
|
|
2716
|
+
);
|
|
2717
|
+
return {
|
|
2718
|
+
json: optionFlag(parsed, "--json"),
|
|
2719
|
+
kind: "diagram-render",
|
|
2720
|
+
path: path!,
|
|
2721
|
+
scale: optionalBoundedNumber(optionString(parsed, "--scale"), "--scale", Number.EPSILON, 4),
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
fail("Usage: slopcamera diagram <check|render> <diagram.json> [options]");
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function parseDuotone(value: string | undefined): readonly [string, string] | undefined {
|
|
2728
|
+
if (value === undefined) return undefined;
|
|
2729
|
+
const colors = value.split(",");
|
|
2730
|
+
if (
|
|
2731
|
+
colors.length !== 2
|
|
2732
|
+
|| !colors.every(color => /^#[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$/u.test(color))
|
|
2733
|
+
) {
|
|
2734
|
+
fail("--duotone must be two comma-separated #RGB or #RRGGBB colors.");
|
|
2735
|
+
}
|
|
2736
|
+
return [colors[0]!, colors[1]!];
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function parseImage(argv: readonly string[]): CliCommand {
|
|
2740
|
+
if (argv[0] !== "vectorize") {
|
|
2741
|
+
fail("Usage: slopcamera image vectorize <raster-path> [options]");
|
|
2742
|
+
}
|
|
2743
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2744
|
+
...JSON_SPEC,
|
|
2745
|
+
"--alpha-cutoff": "value",
|
|
2746
|
+
"--duotone": "value",
|
|
2747
|
+
"--timeout-ms": "value",
|
|
2748
|
+
});
|
|
2749
|
+
const [inputPath] = exactPositionals(
|
|
2750
|
+
parsed,
|
|
2751
|
+
1,
|
|
2752
|
+
"slopcamera image vectorize <raster-path> [--duotone <#primary,#secondary>] [--alpha-cutoff <1..64>] [--timeout-ms <1..300000>] [--json]",
|
|
2753
|
+
);
|
|
2754
|
+
const alphaCutoff = optionalStrictInteger(
|
|
2755
|
+
optionString(parsed, "--alpha-cutoff"),
|
|
2756
|
+
"--alpha-cutoff",
|
|
2757
|
+
);
|
|
2758
|
+
if (alphaCutoff !== undefined && alphaCutoff > 64) {
|
|
2759
|
+
fail("--alpha-cutoff must be between 1 and 64.");
|
|
2760
|
+
}
|
|
2761
|
+
const timeoutMs = optionalStrictInteger(
|
|
2762
|
+
optionString(parsed, "--timeout-ms"),
|
|
2763
|
+
"--timeout-ms",
|
|
2764
|
+
);
|
|
2765
|
+
if (timeoutMs !== undefined && timeoutMs > 300_000) {
|
|
2766
|
+
fail("--timeout-ms must be between 1 and 300000.");
|
|
2767
|
+
}
|
|
2768
|
+
return {
|
|
2769
|
+
alphaCutoff,
|
|
2770
|
+
duotone: parseDuotone(optionString(parsed, "--duotone")),
|
|
2771
|
+
inputPath: inputPath!,
|
|
2772
|
+
json: optionFlag(parsed, "--json"),
|
|
2773
|
+
kind: "image-vectorize",
|
|
2774
|
+
timeoutMs,
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
function parseWorkflows(argv: readonly string[]): CliCommand {
|
|
2779
|
+
const action = argv[0];
|
|
2780
|
+
if (action === "list") {
|
|
2781
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
2782
|
+
exactPositionals(parsed, 0, "slopcamera workflows list [--json]");
|
|
2783
|
+
return { json: optionFlag(parsed, "--json"), kind: "workflows-list" };
|
|
2784
|
+
}
|
|
2785
|
+
if (action === "show") {
|
|
2786
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
2787
|
+
const [workflow] = exactPositionals(parsed, 1, "slopcamera workflows show <id> [--json]");
|
|
2788
|
+
return { json: optionFlag(parsed, "--json"), kind: "workflows-show", workflow: workflow! };
|
|
2789
|
+
}
|
|
2790
|
+
if (action === "plan") {
|
|
2791
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--input": "value" });
|
|
2792
|
+
const [workflow] = exactPositionals(
|
|
2793
|
+
parsed,
|
|
2794
|
+
1,
|
|
2795
|
+
"slopcamera workflows plan <id> --input <json-file> [--json]",
|
|
2796
|
+
);
|
|
2797
|
+
const input = optionString(parsed, "--input");
|
|
2798
|
+
if (input === undefined) fail("workflows plan requires --input.");
|
|
2799
|
+
return {
|
|
2800
|
+
input,
|
|
2801
|
+
json: optionFlag(parsed, "--json"),
|
|
2802
|
+
kind: "workflows-plan",
|
|
2803
|
+
workflow: workflow!,
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
if (action === "run") {
|
|
2807
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2808
|
+
...RUN_OUTPUT_SPEC,
|
|
2809
|
+
...NATIVE_STUDIO_SPEC,
|
|
2810
|
+
"--input": "value",
|
|
2811
|
+
"--provider-options": "value",
|
|
2812
|
+
});
|
|
2813
|
+
const [workflow] = exactPositionals(
|
|
2814
|
+
parsed,
|
|
2815
|
+
1,
|
|
2816
|
+
"slopcamera workflows run <id> --input <json-file> [--provider-options <json-file>] [--jobs <n>] [--json|--jsonl]",
|
|
2817
|
+
);
|
|
2818
|
+
const input = optionString(parsed, "--input");
|
|
2819
|
+
if (input === undefined) fail("workflows run requires --input.");
|
|
2820
|
+
return {
|
|
2821
|
+
input,
|
|
2822
|
+
kind: "workflows-run",
|
|
2823
|
+
...studioWorkflowOptions(parsed),
|
|
2824
|
+
providerOptions: optionString(parsed, "--provider-options"),
|
|
2825
|
+
workflow: workflow!,
|
|
2826
|
+
...runOutput(parsed),
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
fail("Usage: slopcamera workflows <list|show|plan|run> ...");
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function parseCode(argv: readonly string[]): CliCommand {
|
|
2833
|
+
const action = argv[0];
|
|
2834
|
+
if (action === "init") {
|
|
2835
|
+
const parsed = parseOptions(argv.slice(1), {});
|
|
2836
|
+
const [path] = exactPositionals(parsed, 1, "slopcamera code init <path>");
|
|
2837
|
+
return { kind: "code-init", path: path! };
|
|
2838
|
+
}
|
|
2839
|
+
if (action === "check") {
|
|
2840
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
2841
|
+
const [path] = exactPositionals(parsed, 1, "slopcamera code check <path> [--json]");
|
|
2842
|
+
return { json: optionFlag(parsed, "--json"), kind: "code-check", path: path! };
|
|
2843
|
+
}
|
|
2844
|
+
if (action === "plan") {
|
|
2845
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--input": "value" });
|
|
2846
|
+
const [path] = exactPositionals(
|
|
2847
|
+
parsed,
|
|
2848
|
+
1,
|
|
2849
|
+
"slopcamera code plan <path> --input <json-file> [--json]",
|
|
2850
|
+
);
|
|
2851
|
+
const input = optionString(parsed, "--input");
|
|
2852
|
+
if (input === undefined) fail("code plan requires --input.");
|
|
2853
|
+
return { input, json: optionFlag(parsed, "--json"), kind: "code-plan", path: path! };
|
|
2854
|
+
}
|
|
2855
|
+
if (action === "run") {
|
|
2856
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2857
|
+
...RUN_OUTPUT_SPEC,
|
|
2858
|
+
...NATIVE_STUDIO_SPEC,
|
|
2859
|
+
"--input": "value",
|
|
2860
|
+
"--plan": "value",
|
|
2861
|
+
"--provider-options": "value",
|
|
2862
|
+
});
|
|
2863
|
+
const [path] = exactPositionals(
|
|
2864
|
+
parsed,
|
|
2865
|
+
1,
|
|
2866
|
+
"slopcamera code run <path> --input <json-file> [--plan <sha256>] [--provider-options <json-file>] [--jobs <n>] [--json|--jsonl]",
|
|
2867
|
+
);
|
|
2868
|
+
const input = optionString(parsed, "--input");
|
|
2869
|
+
if (input === undefined) fail("code run requires --input.");
|
|
2870
|
+
return {
|
|
2871
|
+
input,
|
|
2872
|
+
kind: "code-run",
|
|
2873
|
+
...studioWorkflowOptions(parsed),
|
|
2874
|
+
path: path!,
|
|
2875
|
+
plan: checkedSha256(optionString(parsed, "--plan"), "--plan"),
|
|
2876
|
+
providerOptions: optionString(parsed, "--provider-options"),
|
|
2877
|
+
...runOutput(parsed),
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
fail("Usage: slopcamera code <init|check|plan|run> ...");
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function parseRuns(argv: readonly string[]): CliCommand {
|
|
2884
|
+
const action = argv[0];
|
|
2885
|
+
if (action === "list") {
|
|
2886
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--limit": "value" });
|
|
2887
|
+
exactPositionals(parsed, 0, "slopcamera runs list [--limit <n>] [--json]");
|
|
2888
|
+
const limit = strictInteger(optionString(parsed, "--limit"), "--limit", 20);
|
|
2889
|
+
if (limit < 1 || limit > 1_000) fail("--limit must be from 1 through 1000.");
|
|
2890
|
+
return { json: optionFlag(parsed, "--json"), kind: "runs-list", limit };
|
|
2891
|
+
}
|
|
2892
|
+
if (action === "show") {
|
|
2893
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--nodes": "value" });
|
|
2894
|
+
const [runId] = exactPositionals(
|
|
2895
|
+
parsed,
|
|
2896
|
+
1,
|
|
2897
|
+
"slopcamera runs show <run-id> [--nodes failed|all] [--json]",
|
|
2898
|
+
);
|
|
2899
|
+
return {
|
|
2900
|
+
json: optionFlag(parsed, "--json"),
|
|
2901
|
+
kind: "runs-show",
|
|
2902
|
+
nodes: oneOf(optionString(parsed, "--nodes"), "--nodes", ["failed", "all"] as const, "failed"),
|
|
2903
|
+
runId: checkedRunId(runId!),
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
if (action === "resume") {
|
|
2907
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2908
|
+
...RUN_OUTPUT_SPEC,
|
|
2909
|
+
...NATIVE_STUDIO_SPEC,
|
|
2910
|
+
"--provider-options": "value",
|
|
2911
|
+
"--replay-ambiguous-code": "repeat",
|
|
2912
|
+
});
|
|
2913
|
+
const [runId] = exactPositionals(
|
|
2914
|
+
parsed,
|
|
2915
|
+
1,
|
|
2916
|
+
"slopcamera runs resume <run-id> [--provider-options <json-file>] [--replay-ambiguous-code <node-key> ...] [--jobs <n>] [--json|--jsonl]",
|
|
2917
|
+
);
|
|
2918
|
+
return {
|
|
2919
|
+
kind: "runs-resume",
|
|
2920
|
+
...studioWorkflowOptions(parsed),
|
|
2921
|
+
providerOptions: optionString(parsed, "--provider-options"),
|
|
2922
|
+
replayAmbiguousCode: optionStrings(parsed, "--replay-ambiguous-code")
|
|
2923
|
+
.map(checkedNodeKey),
|
|
2924
|
+
runId: checkedRunId(runId!),
|
|
2925
|
+
...runOutput(parsed),
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
if (action === "approve") {
|
|
2929
|
+
const parsed = parseOptions(argv.slice(1), {
|
|
2930
|
+
...JSON_SPEC,
|
|
2931
|
+
"--node-plan": "value",
|
|
2932
|
+
"--preparation-plan": "value",
|
|
2933
|
+
});
|
|
2934
|
+
const [runId, nodeKey] = exactPositionals(
|
|
2935
|
+
parsed,
|
|
2936
|
+
2,
|
|
2937
|
+
"slopcamera runs approve <run-id> <node-key> (--preparation-plan <sha256>|--node-plan <sha256>) [--json]",
|
|
2938
|
+
);
|
|
2939
|
+
const preparation = checkedSha256(optionString(parsed, "--preparation-plan"), "--preparation-plan");
|
|
2940
|
+
const effect = checkedSha256(optionString(parsed, "--node-plan"), "--node-plan");
|
|
2941
|
+
if ((preparation === undefined) === (effect === undefined)) {
|
|
2942
|
+
fail("runs approve requires exactly one of --preparation-plan or --node-plan.");
|
|
2943
|
+
}
|
|
2944
|
+
return {
|
|
2945
|
+
json: optionFlag(parsed, "--json"),
|
|
2946
|
+
kind: "runs-approve",
|
|
2947
|
+
nodeKey: checkedNodeKey(nodeKey!),
|
|
2948
|
+
planHash: (preparation ?? effect)!,
|
|
2949
|
+
planKind: preparation === undefined ? "effect" : "preparation",
|
|
2950
|
+
runId: checkedRunId(runId!),
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
if (action === "cancel") {
|
|
2954
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
2955
|
+
const [runId] = exactPositionals(parsed, 1, "slopcamera runs cancel <run-id> [--json]");
|
|
2956
|
+
return { json: optionFlag(parsed, "--json"), kind: "runs-cancel", runId: checkedRunId(runId!) };
|
|
2957
|
+
}
|
|
2958
|
+
fail("Usage: slopcamera runs <list|show|resume|approve|cancel> ...");
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
function helpTopic(argv: readonly string[], index: number): readonly string[] {
|
|
2962
|
+
return argv.slice(0, index).filter((argument) => !argument.startsWith("-"));
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function spatialCliExecutionProfile(value: string | undefined): SpatialCliExecutionProfile | undefined {
|
|
2966
|
+
if (value === undefined || value === "three-webgl2-hardware-v1" || value === "three-spark-webgl2-hardware-v1") return value;
|
|
2967
|
+
return fail("--profile requires three-webgl2-hardware-v1 or three-spark-webgl2-hardware-v1.");
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
function parseStudioArgs(argv: readonly string[]): StudioCommand {
|
|
2971
|
+
const action = argv[0];
|
|
2972
|
+
if (action === "assets") {
|
|
2973
|
+
const operation = argv[1];
|
|
2974
|
+
if (operation !== "search" && operation !== "describe" && operation !== "plan" && operation !== "import") return fail("Usage: slopcamera studio assets <search|describe|plan|import> <json-file-or-asset-id> [--json]");
|
|
2975
|
+
const parsed = parseOptions(argv.slice(2), JSON_SPEC);
|
|
2976
|
+
const [path] = exactPositionals(parsed, 1, `slopcamera studio assets ${operation} <${operation === "describe" ? "asset-id" : "json-file"}>`);
|
|
2977
|
+
return { kind: "studio", action, operation, path: path!, json: optionFlag(parsed, "--json") };
|
|
2978
|
+
}
|
|
2979
|
+
const specs: Record<string, Readonly<Record<string, "value" | "flag">>> = {
|
|
2980
|
+
asset: { ...JSON_SPEC, "--output-id": "value", "--asset-id": "value", "--representation": "value", "--frame": "value" },
|
|
2981
|
+
init: { ...JSON_SPEC, "--template": "value" }, bundle: { ...JSON_SPEC, "--source-root": "value" }, plan: JSON_SPEC, inspect: JSON_SPEC, reconcile: JSON_SPEC, encode: { ...JSON_SPEC, "--output-id": "value" }, assemble: { ...JSON_SPEC, "--output-id": "value", "--name": "value" },
|
|
2982
|
+
probe: { ...JSON_SPEC, "--blender-bin": "value", "--python": "value" },
|
|
2983
|
+
run: { ...JSON_SPEC, "--blender-bin": "value", "--python": "value", "--allow-trusted-code": "flag" },
|
|
2984
|
+
};
|
|
2985
|
+
if (action === undefined || specs[action] === undefined) return fail("Usage: slopcamera studio <init|bundle|plan|probe|run|encode|asset|assemble|inspect|reconcile> ...");
|
|
2986
|
+
const parsed = parseOptions(argv.slice(1), specs[action]!);
|
|
2987
|
+
const [path] = exactPositionals(parsed, 1, `slopcamera studio ${action} <${action === "inspect" ? "studio-id" : action === "init" ? "directory" : "json-file"}>`);
|
|
2988
|
+
const common = { kind: "studio" as const, json: optionFlag(parsed, "--json") };
|
|
2989
|
+
if (action === "asset") {
|
|
2990
|
+
const outputId = optionString(parsed, "--output-id"), assetId = optionString(parsed, "--asset-id");
|
|
2991
|
+
const representation = optionString(parsed, "--representation"), frame = optionString(parsed, "--frame");
|
|
2992
|
+
if (!outputId || !assetId || (representation !== "native" && representation !== "encoded-video")) return fail("studio asset requires --output-id, --asset-id and --representation native|encoded-video.");
|
|
2993
|
+
if (frame !== undefined && (!/^(0|[1-9][0-9]*)$/u.test(frame) || !Number.isSafeInteger(Number(frame)) || representation !== "native")) return fail("--frame requires a nonnegative integer and native representation.");
|
|
2994
|
+
return { ...common, action, id: path!, outputId, assetId, representation, ...(frame === undefined ? {} : { frame: Number(frame) }) };
|
|
2995
|
+
}
|
|
2996
|
+
if (action === "init") {
|
|
2997
|
+
const template = optionString(parsed, "--template") ?? "blender-product";
|
|
2998
|
+
if (!STUDIO_TEMPLATES.some(value => value === template)) return fail(`Studio template must be one of: ${STUDIO_TEMPLATES.join(", ")}.`);
|
|
2999
|
+
return { ...common, action, path: path!, template: template as StudioTemplate };
|
|
3000
|
+
}
|
|
3001
|
+
if (action === "bundle") {
|
|
3002
|
+
const sourceRoot = optionString(parsed, "--source-root");
|
|
3003
|
+
return { ...common, action, path: path!, ...(sourceRoot === undefined ? {} : { sourceRoot }) };
|
|
3004
|
+
}
|
|
3005
|
+
if (action === "encode" || action === "assemble") {
|
|
3006
|
+
const outputId = optionString(parsed, "--output-id") ?? fail(`studio ${action} requires --output-id naming a declared PNG sequence.`);
|
|
3007
|
+
const title = action === "assemble" ? optionString(parsed, "--name") : undefined;
|
|
3008
|
+
return { ...common, action, id: path!, outputId, ...(title === undefined ? {} : { title }) };
|
|
3009
|
+
}
|
|
3010
|
+
if (action === "plan") return { ...common, action, path: path! };
|
|
3011
|
+
if (action === "inspect" || action === "reconcile") return { ...common, action, id: path! };
|
|
3012
|
+
const blender = optionString(parsed, "--blender-bin"), python = optionString(parsed, "--python");
|
|
3013
|
+
if (action !== "probe" && action !== "run") return fail("Unsupported studio action.");
|
|
3014
|
+
if (action === "run" && !optionFlag(parsed, "--allow-trusted-code")) return fail("studio run requires --allow-trusted-code: authored Python runs as the current user without an OS sandbox.");
|
|
3015
|
+
return { ...common, action, path: path!, ...(blender === undefined ? {} : { blender }), ...(python === undefined ? {} : { python }), allowTrustedCode: action === "run" };
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
function parseDirectingArgs(argv: readonly string[]): DirectingCommand {
|
|
3019
|
+
const action = argv[0];
|
|
3020
|
+
const specs: Record<string, Readonly<Record<string, "value" | "flag">>> = {
|
|
3021
|
+
init: JSON_SPEC,
|
|
3022
|
+
anchor: { ...JSON_SPEC, "--input": "value" },
|
|
3023
|
+
plan: JSON_SPEC,
|
|
3024
|
+
start: { ...JSON_SPEC, "--budget-usd": "value" },
|
|
3025
|
+
inspect: JSON_SPEC,
|
|
3026
|
+
assemble: JSON_SPEC,
|
|
3027
|
+
revise: { ...JSON_SPEC, "--recipe": "value" },
|
|
3028
|
+
generate: { ...JSON_SPEC, "--shot": "value", "--attempt": "value", "--allow-paid-generation": "flag", "--allow-cloud-upload": "flag", "--allow-reference-hosting": "flag" },
|
|
3029
|
+
resume: { ...JSON_SPEC, "--attempt": "value" },
|
|
3030
|
+
cleanup: { ...JSON_SPEC, "--attempt": "value" },
|
|
3031
|
+
review: { ...JSON_SPEC, "--attempt": "value", "--decision": "value", "--note": "value" },
|
|
3032
|
+
};
|
|
3033
|
+
if (action === undefined || specs[action] === undefined) return fail("Usage: slopcamera direct <init|anchor|plan|start|inspect|revise|generate|resume|review|assemble|cleanup> ...");
|
|
3034
|
+
const parsed = parseOptions(argv.slice(1), specs[action]!);
|
|
3035
|
+
const required = (name: string): string => optionString(parsed, name) ?? fail(`direct ${action} requires ${name}.`);
|
|
3036
|
+
const common = { kind: "directing" as const, json: optionFlag(parsed, "--json") };
|
|
3037
|
+
if (action === "anchor") {
|
|
3038
|
+
exactPositionals(parsed, 0, "slopcamera direct anchor --input <image>");
|
|
3039
|
+
return { ...common, action, input: required("--input") };
|
|
3040
|
+
}
|
|
3041
|
+
const [value] = exactPositionals(parsed, 1, `slopcamera direct ${action} <${["init", "plan", "start"].includes(action) ? "recipe.json" : "direct-id"}>`);
|
|
3042
|
+
const id = value!;
|
|
3043
|
+
switch (action) {
|
|
3044
|
+
case "init": return { ...common, action, path: id };
|
|
3045
|
+
case "plan": return { ...common, action, recipe: id };
|
|
3046
|
+
case "start": return { ...common, action, recipe: id, budgetUsd: required("--budget-usd") };
|
|
3047
|
+
case "inspect": case "assemble": return { ...common, action, id };
|
|
3048
|
+
case "revise": return { ...common, action, id, recipe: required("--recipe") };
|
|
3049
|
+
case "resume": case "cleanup": return { ...common, action, id, attempt: required("--attempt") };
|
|
3050
|
+
case "review": {
|
|
3051
|
+
const decision = required("--decision");
|
|
3052
|
+
if (decision !== "accepted" && decision !== "rejected") return fail("Review decision must be accepted or rejected.");
|
|
3053
|
+
return { ...common, action, id, attempt: required("--attempt"), decision, note: required("--note") };
|
|
3054
|
+
}
|
|
3055
|
+
case "generate": {
|
|
3056
|
+
if (!optionFlag(parsed, "--allow-paid-generation")) return fail("direct generate requires --allow-paid-generation for this exact take.");
|
|
3057
|
+
return { ...common, action, id, shot: required("--shot"), attempt: required("--attempt"), allowPaidGeneration: true, allowCloudUpload: optionFlag(parsed, "--allow-cloud-upload"), allowReferenceHosting: optionFlag(parsed, "--allow-reference-hosting") };
|
|
3058
|
+
}
|
|
3059
|
+
default: return fail("Unsupported directing command.");
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
function parseSpatialWorldArgs(argv: readonly string[]): SpatialWorldCommand {
|
|
3064
|
+
const action = argv[0];
|
|
3065
|
+
if (action === "import") {
|
|
3066
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--input": "value", "--source-root": "value", "--output-root": "value" });
|
|
3067
|
+
exactPositionals(parsed, 0, "slopcamera scene world import --input <request.json> --source-root <directory> --output-root <artifact-directory>");
|
|
3068
|
+
const input = optionString(parsed, "--input"), sourceRoot = optionString(parsed, "--source-root"), outputRoot = optionString(parsed, "--output-root");
|
|
3069
|
+
if (input === undefined || sourceRoot === undefined || outputRoot === undefined) fail("World import requires --input, --source-root, and --output-root.");
|
|
3070
|
+
return { kind: "spatial-world", action, input, sourceRoot, outputRoot, json: optionFlag(parsed, "--json") };
|
|
3071
|
+
}
|
|
3072
|
+
return fail("Usage: slopcamera scene world import --input <request.json> --source-root <directory> --output-root <artifact-directory>");
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
function parseSpatialSceneArgs(argv: readonly string[]): SpatialSceneCommand | SpatialProjectCommand | SpatialWorldCommand {
|
|
3076
|
+
const action = argv[0];
|
|
3077
|
+
if (action === "world") return parseSpatialWorldArgs(argv.slice(1));
|
|
3078
|
+
if (action === "project") {
|
|
3079
|
+
const projectAction = argv[1];
|
|
3080
|
+
if (projectAction === "snapshot") {
|
|
3081
|
+
const parsed = parseOptions(argv.slice(2), JSON_SPEC);
|
|
3082
|
+
const [project] = exactPositionals(parsed, 1, "slopcamera scene project snapshot <project-id> [--json]");
|
|
3083
|
+
return { kind: "spatial-project", action: projectAction, project: project!, json: optionFlag(parsed, "--json") };
|
|
3084
|
+
}
|
|
3085
|
+
if (projectAction === "prepare-render") {
|
|
3086
|
+
const parsed = parseOptions(argv.slice(2), { ...JSON_SPEC, "--input": "value", "--output": "value", "--profile": "value" });
|
|
3087
|
+
const [project] = exactPositionals(parsed, 1, "slopcamera scene project prepare-render <project-id> --input <request.json> --output <prepared-render.json> [--json]");
|
|
3088
|
+
const input = optionString(parsed, "--input"), output = optionString(parsed, "--output");
|
|
3089
|
+
if (input === undefined || output === undefined) fail("scene project prepare-render requires --input and --output.");
|
|
3090
|
+
const executionProfile = spatialCliExecutionProfile(optionString(parsed, "--profile"));
|
|
3091
|
+
return { kind: "spatial-project", action: projectAction, project: project!, input, output, ...(executionProfile === undefined ? {} : { executionProfile }), json: optionFlag(parsed, "--json") };
|
|
3092
|
+
}
|
|
3093
|
+
if (projectAction === "migrate" || projectAction === "patch" || projectAction === "restore" || projectAction === "add-shot" || projectAction === "add-candidate" || projectAction === "select-candidate" || projectAction === "reconcile") {
|
|
3094
|
+
const parsed = parseOptions(argv.slice(2), { ...JSON_SPEC, "--input": "value" });
|
|
3095
|
+
const [project] = exactPositionals(parsed, 1, `slopcamera scene project ${projectAction} <project-id> --input <request.json> [--json]`);
|
|
3096
|
+
const input = optionString(parsed, "--input");
|
|
3097
|
+
if (input === undefined) fail(`scene project ${projectAction} requires --input.`);
|
|
3098
|
+
return { kind: "spatial-project", action: projectAction, project: project!, input, json: optionFlag(parsed, "--json") };
|
|
3099
|
+
}
|
|
3100
|
+
fail("Usage: slopcamera scene project <snapshot|migrate|patch|restore|add-shot|add-candidate|select-candidate|reconcile|prepare-render> ...");
|
|
3101
|
+
}
|
|
3102
|
+
if (action === "init" || action === "inspect" || action === "check") {
|
|
3103
|
+
const parsed = parseOptions(argv.slice(1), JSON_SPEC);
|
|
3104
|
+
const [path] = exactPositionals(parsed, 1, `slopcamera scene ${action} <scene.json> [--json]`);
|
|
3105
|
+
return { kind: "spatial-scene", action: action === "check" ? "inspect" : action, path: path!, json: optionFlag(parsed, "--json") };
|
|
3106
|
+
}
|
|
3107
|
+
if (action === "patch") {
|
|
3108
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--patch": "value", "--output": "value" });
|
|
3109
|
+
const [path] = exactPositionals(parsed, 1, "slopcamera scene patch <scene.json> --patch <patch.json> --output <new-scene.json>");
|
|
3110
|
+
const patch = optionString(parsed, "--patch"), output = optionString(parsed, "--output");
|
|
3111
|
+
if (patch === undefined || output === undefined) fail("scene patch requires --patch and --output.");
|
|
3112
|
+
return { kind: "spatial-scene", action, path: path!, patch, output, json: optionFlag(parsed, "--json") };
|
|
3113
|
+
}
|
|
3114
|
+
if (action === "camera-track") {
|
|
3115
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--request": "value", "--output": "value" });
|
|
3116
|
+
const [path] = exactPositionals(parsed, 1, "slopcamera scene camera-track <scene.json> --request <sampling.json> --output <new-track.json>");
|
|
3117
|
+
const request = optionString(parsed, "--request"), output = optionString(parsed, "--output");
|
|
3118
|
+
if (request === undefined || output === undefined) return fail("scene camera-track requires --request and --output.");
|
|
3119
|
+
return { kind: "spatial-scene", action, path: path!, request, output, json: optionFlag(parsed, "--json") };
|
|
3120
|
+
}
|
|
3121
|
+
if (action === "evaluate") {
|
|
3122
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--camera": "value", "--time-us": "value" });
|
|
3123
|
+
const [path] = exactPositionals(parsed, 1, "slopcamera scene evaluate <scene.json> --camera <camera-id> --time-us <integer>");
|
|
3124
|
+
const camera = optionString(parsed, "--camera"), time = optionString(parsed, "--time-us");
|
|
3125
|
+
if (camera === undefined || time === undefined || !/^\d+$/u.test(time) || !Number.isSafeInteger(Number(time))) fail("scene evaluate requires --camera and integer --time-us.");
|
|
3126
|
+
return { kind: "spatial-scene", action, path: path!, camera, timeUs: Number(time), json: optionFlag(parsed, "--json") };
|
|
3127
|
+
}
|
|
3128
|
+
if (action === "plan" || action === "render") {
|
|
3129
|
+
const parsed = parseOptions(argv.slice(1), { ...JSON_SPEC, "--request": "value", "--assets": "value", "--profile": "value" });
|
|
3130
|
+
const [path] = exactPositionals(parsed, 1, `slopcamera scene ${action} <scene.json> --request <request.json> [--assets <bindings.json>]`);
|
|
3131
|
+
const request = optionString(parsed, "--request"), assets = optionString(parsed, "--assets");
|
|
3132
|
+
if (request === undefined) fail(`scene ${action} requires --request.`);
|
|
3133
|
+
const executionProfile = spatialCliExecutionProfile(optionString(parsed, "--profile"));
|
|
3134
|
+
return { kind: "spatial-scene", action, path: path!, request, ...(assets === undefined ? {} : { assets }), ...(executionProfile === undefined ? {} : { executionProfile }), json: optionFlag(parsed, "--json") };
|
|
3135
|
+
}
|
|
3136
|
+
fail("Usage: slopcamera scene <init|check|inspect|patch|evaluate|camera-track|plan|render> ...");
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
export function parseCliArgs(argv: readonly string[]): CliCommand {
|
|
3140
|
+
if (argv.length === 0) return { kind: "help", topic: [] };
|
|
3141
|
+
const helpIndex = argv.findIndex((argument) => argument === "--help" || argument === "-h");
|
|
3142
|
+
if (helpIndex !== -1) return { kind: "help", topic: helpTopic(argv, helpIndex) };
|
|
3143
|
+
if (argv[0] === "help") {
|
|
3144
|
+
if (argv.slice(1).some((argument) => argument.startsWith("-"))) fail("help takes only a command topic.");
|
|
3145
|
+
return { kind: "help", topic: argv.slice(1) };
|
|
3146
|
+
}
|
|
3147
|
+
if (argv[0] === "--version" || argv[0] === "version") {
|
|
3148
|
+
if (argv.length !== 1) fail("--version does not take arguments.");
|
|
3149
|
+
return { kind: "version" };
|
|
3150
|
+
}
|
|
3151
|
+
const command = argv[0]!;
|
|
3152
|
+
switch (command) {
|
|
3153
|
+
case "studio": return parseStudioArgs(argv.slice(1));
|
|
3154
|
+
case "direct": return parseDirectingArgs(argv.slice(1));
|
|
3155
|
+
case "scene": return parseSpatialSceneArgs(argv.slice(1));
|
|
3156
|
+
case "operations": return parseOperations(argv.slice(1));
|
|
3157
|
+
case "diagram": return parseDiagram(argv.slice(1));
|
|
3158
|
+
case "image": return parseImage(argv.slice(1));
|
|
3159
|
+
case "workflows": return parseWorkflows(argv.slice(1));
|
|
3160
|
+
case "code": return parseCode(argv.slice(1));
|
|
3161
|
+
case "runs": return parseRuns(argv.slice(1));
|
|
3162
|
+
case "doctor": return parseDoctor(argv.slice(1));
|
|
3163
|
+
case "ai": return parseAi(argv.slice(1));
|
|
3164
|
+
case "media": return parseMedia(argv.slice(1));
|
|
3165
|
+
case "recordings": return parseRecordings(argv.slice(1));
|
|
3166
|
+
case "projects": return parseProjects(argv.slice(1));
|
|
3167
|
+
case "project": return parseProject(argv.slice(1));
|
|
3168
|
+
case "inspect": return parseInspect(argv.slice(1));
|
|
3169
|
+
case "events": return parseEvents(argv.slice(1));
|
|
3170
|
+
case "record": return parseRecord(argv.slice(1));
|
|
3171
|
+
case "edit": return parseEdit(argv.slice(1));
|
|
3172
|
+
case "analyze": return parseAnalyze(argv.slice(1));
|
|
3173
|
+
case "faces": return parseFaces(argv.slice(1));
|
|
3174
|
+
case "align": return parseAlign(argv.slice(1));
|
|
3175
|
+
case "fillers": return parseFillers(argv.slice(1));
|
|
3176
|
+
case "render": return parseRender(argv.slice(1));
|
|
3177
|
+
case "assets": return parseAssets(argv.slice(1));
|
|
3178
|
+
case "__complete": return { kind: "complete", words: argv.slice(1) };
|
|
3179
|
+
default: fail(`Unknown command: ${command}. Run slopcamera help.`);
|
|
3180
|
+
}
|
|
3181
|
+
}
|