@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,2054 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
IsoTimestampSchema,
|
|
5
|
+
ProjectCameraMoveSchema,
|
|
6
|
+
ProjectPlacementIdSchema,
|
|
7
|
+
ProjectZoomOperationSchema,
|
|
8
|
+
RecordingIdSchema,
|
|
9
|
+
Sha256Schema,
|
|
10
|
+
ZoomIdSchema,
|
|
11
|
+
type ProjectEditPlanV1,
|
|
12
|
+
type VideoProjectV1,
|
|
13
|
+
} from "../../../contracts";
|
|
14
|
+
import {
|
|
15
|
+
assertProjectCameraMoveBindings,
|
|
16
|
+
assertStaticProjectZoomTargetVisible,
|
|
17
|
+
canonicalSlopcameraPersistenceDocument,
|
|
18
|
+
canonicalJsonSha256,
|
|
19
|
+
cutProjectPlan,
|
|
20
|
+
hashProjectCameraGeometry,
|
|
21
|
+
hashProjectCameraSync,
|
|
22
|
+
hashProjectEditPlan,
|
|
23
|
+
hashPlacementSync,
|
|
24
|
+
hashProjectStructure,
|
|
25
|
+
intersectIntervals,
|
|
26
|
+
normalizeProjectEditPlan,
|
|
27
|
+
projectIntervalHasCompleteSyncCoverage,
|
|
28
|
+
resolveProjectMetadataSourceLayer,
|
|
29
|
+
setProjectSpeed,
|
|
30
|
+
subtractIntervals,
|
|
31
|
+
trimProjectPlan,
|
|
32
|
+
type ResolvedProjectMetadataSourceLayer,
|
|
33
|
+
} from "../../../core";
|
|
34
|
+
import { openRecording } from "../../../cli/bundle-service";
|
|
35
|
+
import { CliError } from "../../../cli/errors";
|
|
36
|
+
import {
|
|
37
|
+
commitProjectStateTransaction,
|
|
38
|
+
} from "../../../cli/project-state-transaction";
|
|
39
|
+
import type { ApplicationContext } from "../../context";
|
|
40
|
+
import { ApplicationError } from "../../errors";
|
|
41
|
+
import type {
|
|
42
|
+
OperationDefinition,
|
|
43
|
+
OperationExecutionContext,
|
|
44
|
+
} from "../../operation";
|
|
45
|
+
import {
|
|
46
|
+
assertProjectEditBasis,
|
|
47
|
+
hashProjectGeneration,
|
|
48
|
+
openProjectSnapshot,
|
|
49
|
+
projectEditBasis,
|
|
50
|
+
ProjectEditBasisSchema,
|
|
51
|
+
type OpenProjectSnapshot,
|
|
52
|
+
type ProjectEditBasis,
|
|
53
|
+
} from "../../project-store";
|
|
54
|
+
import { ProjectEditCommitReceiptSchema } from "../../receipts";
|
|
55
|
+
import {
|
|
56
|
+
ProjectEditBatchV2Schema,
|
|
57
|
+
ProjectEditBatchV3Schema,
|
|
58
|
+
ProjectEditBatchSchema,
|
|
59
|
+
projectEditNormalizationPhaseV3,
|
|
60
|
+
type ManualProjectZoomInputV3,
|
|
61
|
+
type OrderedProjectEditV2,
|
|
62
|
+
type OrderedProjectEditV3,
|
|
63
|
+
type OrderedProjectEdit,
|
|
64
|
+
type ProjectEditNormalizationPhaseV3,
|
|
65
|
+
} from "../derive/edit-batch";
|
|
66
|
+
import {
|
|
67
|
+
ProjectReferenceSchema,
|
|
68
|
+
throwIfAborted,
|
|
69
|
+
} from "../shared";
|
|
70
|
+
|
|
71
|
+
export const CommitProjectEditsInputSchema = z.strictObject({
|
|
72
|
+
batch: ProjectEditBatchSchema,
|
|
73
|
+
basis: ProjectEditBasisSchema,
|
|
74
|
+
project: ProjectReferenceSchema,
|
|
75
|
+
updatedAt: IsoTimestampSchema.optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type CommitProjectEditsInput = z.infer<typeof CommitProjectEditsInputSchema>;
|
|
79
|
+
export type CommitProjectEditsOutput = z.infer<typeof ProjectEditCommitReceiptSchema>;
|
|
80
|
+
|
|
81
|
+
export const ProjectCommitMetadataBindingV2Schema = z.strictObject({
|
|
82
|
+
manifestSha256: Sha256Schema,
|
|
83
|
+
placementId: ProjectPlacementIdSchema,
|
|
84
|
+
recordingId: RecordingIdSchema,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const CommitProjectEditsInputV2Schema = z.strictObject({
|
|
88
|
+
batch: ProjectEditBatchV2Schema,
|
|
89
|
+
basis: ProjectEditBasisSchema,
|
|
90
|
+
metadataBinding: ProjectCommitMetadataBindingV2Schema.optional(),
|
|
91
|
+
project: ProjectReferenceSchema,
|
|
92
|
+
updatedAt: IsoTimestampSchema.optional(),
|
|
93
|
+
}).superRefine((input, context) => {
|
|
94
|
+
const requiresBinding = input.batch.ordered.some(edit => (
|
|
95
|
+
edit.kind === "set-metadata-effects" && metadataEffectsAreEnabled(edit)
|
|
96
|
+
));
|
|
97
|
+
if (!requiresBinding && input.metadataBinding !== undefined) {
|
|
98
|
+
context.addIssue({
|
|
99
|
+
code: "custom",
|
|
100
|
+
message: "Metadata binding is only valid when this batch enables metadata effects.",
|
|
101
|
+
path: ["metadataBinding"],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export type ProjectCommitMetadataBindingV2 = z.infer<
|
|
107
|
+
typeof ProjectCommitMetadataBindingV2Schema
|
|
108
|
+
>;
|
|
109
|
+
export type CommitProjectEditsInputV2 = z.infer<
|
|
110
|
+
typeof CommitProjectEditsInputV2Schema
|
|
111
|
+
>;
|
|
112
|
+
|
|
113
|
+
export const ProjectCommitManualZoomBindingV3Schema = z.strictObject({
|
|
114
|
+
displayId: z.string().min(1).max(256),
|
|
115
|
+
manifestSha256: Sha256Schema,
|
|
116
|
+
placementId: ProjectPlacementIdSchema,
|
|
117
|
+
recordingId: RecordingIdSchema,
|
|
118
|
+
syncSha256: Sha256Schema,
|
|
119
|
+
zoomId: ZoomIdSchema,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const CommitProjectEditsInputV3Schema = z.strictObject({
|
|
123
|
+
batch: ProjectEditBatchV3Schema,
|
|
124
|
+
basis: ProjectEditBasisSchema,
|
|
125
|
+
manualZoomBindings: z.array(
|
|
126
|
+
ProjectCommitManualZoomBindingV3Schema,
|
|
127
|
+
).max(10_000).optional(),
|
|
128
|
+
metadataBinding: ProjectCommitMetadataBindingV2Schema.optional(),
|
|
129
|
+
project: ProjectReferenceSchema,
|
|
130
|
+
updatedAt: IsoTimestampSchema.optional(),
|
|
131
|
+
}).superRefine((input, context) => {
|
|
132
|
+
const requiresMetadataBinding = input.batch.ordered.some(edit => (
|
|
133
|
+
edit.kind === "set-metadata-effects" && metadataEffectsAreEnabled(edit)
|
|
134
|
+
));
|
|
135
|
+
if (!requiresMetadataBinding && input.metadataBinding !== undefined) {
|
|
136
|
+
context.addIssue({
|
|
137
|
+
code: "custom",
|
|
138
|
+
message: "Metadata binding is only valid when this batch enables metadata effects.",
|
|
139
|
+
path: ["metadataBinding"],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const manualZoomIds = input.batch.ordered.flatMap(edit => (
|
|
143
|
+
edit.kind === "add-manual-zooms"
|
|
144
|
+
? edit.zooms.map(zoom => zoom.zoomId)
|
|
145
|
+
: []
|
|
146
|
+
));
|
|
147
|
+
if (manualZoomIds.length === 0 && input.manualZoomBindings !== undefined) {
|
|
148
|
+
context.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
message: "Manual zoom bindings are only valid for a batch that adds manual zooms.",
|
|
151
|
+
path: ["manualZoomBindings"],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
input.manualZoomBindings !== undefined
|
|
156
|
+
&& new Set(input.manualZoomBindings.map(binding => binding.zoomId)).size
|
|
157
|
+
!== input.manualZoomBindings.length
|
|
158
|
+
) {
|
|
159
|
+
context.addIssue({
|
|
160
|
+
code: "custom",
|
|
161
|
+
message: "Manual zoom bindings must have unique zoom IDs.",
|
|
162
|
+
path: ["manualZoomBindings"],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export type ProjectCommitManualZoomBindingV3 = z.infer<
|
|
168
|
+
typeof ProjectCommitManualZoomBindingV3Schema
|
|
169
|
+
>;
|
|
170
|
+
export type CommitProjectEditsInputV3 = z.infer<
|
|
171
|
+
typeof CommitProjectEditsInputV3Schema
|
|
172
|
+
>;
|
|
173
|
+
|
|
174
|
+
export function applyOrderedProjectEdit(
|
|
175
|
+
project: Awaited<ReturnType<typeof openProjectSnapshot>>["project"],
|
|
176
|
+
plan: Awaited<ReturnType<typeof openProjectSnapshot>>["plan"],
|
|
177
|
+
edit: OrderedProjectEdit,
|
|
178
|
+
updatedAt: string,
|
|
179
|
+
normalize = true,
|
|
180
|
+
) {
|
|
181
|
+
switch (edit.kind) {
|
|
182
|
+
case "cut":
|
|
183
|
+
return normalize
|
|
184
|
+
? cutProjectPlan(project, plan, edit.range, updatedAt)
|
|
185
|
+
: {
|
|
186
|
+
...plan,
|
|
187
|
+
keep: subtractIntervals(plan.keep, [edit.range]),
|
|
188
|
+
updatedAt,
|
|
189
|
+
};
|
|
190
|
+
case "trim":
|
|
191
|
+
return normalize
|
|
192
|
+
? trimProjectPlan(project, plan, edit.range, updatedAt)
|
|
193
|
+
: {
|
|
194
|
+
...plan,
|
|
195
|
+
keep: intersectIntervals(plan.keep, [edit.range]),
|
|
196
|
+
updatedAt,
|
|
197
|
+
};
|
|
198
|
+
case "speed":
|
|
199
|
+
return normalize
|
|
200
|
+
? setProjectSpeed(project, plan, edit.range, edit.rate, updatedAt)
|
|
201
|
+
: {
|
|
202
|
+
...plan,
|
|
203
|
+
speed: [
|
|
204
|
+
...plan.speed,
|
|
205
|
+
{ range: edit.range, rate: edit.rate },
|
|
206
|
+
],
|
|
207
|
+
updatedAt,
|
|
208
|
+
};
|
|
209
|
+
case "add-zooms":
|
|
210
|
+
return finalizeProjectEditDraft({
|
|
211
|
+
...plan,
|
|
212
|
+
updatedAt,
|
|
213
|
+
zooms: [...plan.zooms, ...edit.zooms],
|
|
214
|
+
}, normalize);
|
|
215
|
+
case "add-overlays": {
|
|
216
|
+
const existing = new Set(plan.overlays.map(overlay => overlay.overlayId));
|
|
217
|
+
const conflicts = edit.overlays
|
|
218
|
+
.map(overlay => overlay.overlayId)
|
|
219
|
+
.filter(overlayId => existing.has(overlayId))
|
|
220
|
+
.sort();
|
|
221
|
+
if (conflicts.length > 0) {
|
|
222
|
+
throw new ApplicationError(
|
|
223
|
+
"conflict",
|
|
224
|
+
`Overlay ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return finalizeProjectEditDraft({
|
|
228
|
+
...plan,
|
|
229
|
+
overlays: [...plan.overlays, ...edit.overlays],
|
|
230
|
+
updatedAt,
|
|
231
|
+
}, normalize);
|
|
232
|
+
}
|
|
233
|
+
case "remove-overlays": {
|
|
234
|
+
const requested = new Set(edit.overlayIds);
|
|
235
|
+
const overlays = plan.overlays.filter(
|
|
236
|
+
overlay => !requested.has(overlay.overlayId),
|
|
237
|
+
);
|
|
238
|
+
if (overlays.length !== plan.overlays.length - requested.size) {
|
|
239
|
+
const existing = new Set(plan.overlays.map(overlay => overlay.overlayId));
|
|
240
|
+
const missing = [...requested].filter(id => !existing.has(id)).sort();
|
|
241
|
+
throw new ApplicationError(
|
|
242
|
+
"not-found",
|
|
243
|
+
`Unknown overlay${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return finalizeProjectEditDraft({
|
|
247
|
+
...plan,
|
|
248
|
+
overlays,
|
|
249
|
+
updatedAt,
|
|
250
|
+
}, normalize);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function finalizeProjectEditDraft(
|
|
256
|
+
plan: ProjectEditPlanV1,
|
|
257
|
+
normalize: boolean,
|
|
258
|
+
): ProjectEditPlanV1 {
|
|
259
|
+
return normalize ? normalizeProjectEditPlan(plan) : plan;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function projectEditCommitReceipt(
|
|
263
|
+
input: {
|
|
264
|
+
readonly batch: {
|
|
265
|
+
readonly ordered: readonly { readonly kind: string }[];
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
project: Awaited<ReturnType<typeof openProjectSnapshot>>["project"],
|
|
269
|
+
plan: Awaited<ReturnType<typeof openProjectSnapshot>>["plan"],
|
|
270
|
+
): CommitProjectEditsOutput {
|
|
271
|
+
const onlyEdit = input.batch.ordered.length === 1
|
|
272
|
+
? input.batch.ordered[0]
|
|
273
|
+
: undefined;
|
|
274
|
+
return ProjectEditCommitReceiptSchema.parse({
|
|
275
|
+
editBasis: projectEditBasis(project, plan),
|
|
276
|
+
generation: hashProjectGeneration(project, plan),
|
|
277
|
+
operation:
|
|
278
|
+
onlyEdit?.kind === "cut"
|
|
279
|
+
|| onlyEdit?.kind === "speed"
|
|
280
|
+
|| onlyEdit?.kind === "trim"
|
|
281
|
+
? onlyEdit.kind
|
|
282
|
+
: "batch",
|
|
283
|
+
planHash: hashProjectEditPlan(plan),
|
|
284
|
+
planId: plan.planId,
|
|
285
|
+
projectId: project.projectId,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function projectEditTransactionId(nodePlanSha256: string): string {
|
|
290
|
+
return `transaction_${nodePlanSha256.slice(0, 32)}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function metadataEffectsAreEnabled(
|
|
294
|
+
effects: Extract<
|
|
295
|
+
OrderedProjectEditV2,
|
|
296
|
+
{ readonly kind: "set-metadata-effects" }
|
|
297
|
+
>,
|
|
298
|
+
): boolean {
|
|
299
|
+
return effects.clicks.enabled
|
|
300
|
+
|| effects.cursor.enabled
|
|
301
|
+
|| effects.keystrokes.enabled
|
|
302
|
+
|| effects.typedText.enabled;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function recordingMetadataLoadError(
|
|
306
|
+
error: unknown,
|
|
307
|
+
placementId: string,
|
|
308
|
+
): ApplicationError {
|
|
309
|
+
const message = `Recording metadata for placement ${placementId} could not be loaded.`;
|
|
310
|
+
if (!(error instanceof CliError)) {
|
|
311
|
+
return new ApplicationError("invalid-data", message);
|
|
312
|
+
}
|
|
313
|
+
switch (error.code) {
|
|
314
|
+
case "not-found":
|
|
315
|
+
return new ApplicationError("not-found", message);
|
|
316
|
+
case "unavailable":
|
|
317
|
+
return new ApplicationError("unavailable", message);
|
|
318
|
+
case "conflict":
|
|
319
|
+
case "invalid-data":
|
|
320
|
+
case "unsafe-path":
|
|
321
|
+
case "usage":
|
|
322
|
+
return new ApplicationError("invalid-data", message);
|
|
323
|
+
case "ambiguous":
|
|
324
|
+
case "authorization-required":
|
|
325
|
+
case "cancelled":
|
|
326
|
+
case "incompatible":
|
|
327
|
+
case "internal":
|
|
328
|
+
case "subprocess":
|
|
329
|
+
case "unsupported-plan":
|
|
330
|
+
return new ApplicationError("unavailable", message);
|
|
331
|
+
default: {
|
|
332
|
+
const exhaustive: never = error.code;
|
|
333
|
+
return exhaustive;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function assertMetadataEffectsAvailable(
|
|
339
|
+
application: ApplicationContext,
|
|
340
|
+
project: VideoProjectV1,
|
|
341
|
+
edit: Extract<
|
|
342
|
+
OrderedProjectEditV2,
|
|
343
|
+
{ readonly kind: "set-metadata-effects" }
|
|
344
|
+
>,
|
|
345
|
+
expectedBinding?: ProjectCommitMetadataBindingV2,
|
|
346
|
+
): Promise<ProjectCommitMetadataBindingV2 | undefined> {
|
|
347
|
+
if (!metadataEffectsAreEnabled(edit)) return undefined;
|
|
348
|
+
const placementId = edit.metadataPlacementId;
|
|
349
|
+
if (placementId === null) {
|
|
350
|
+
throw new ApplicationError(
|
|
351
|
+
"invalid-data",
|
|
352
|
+
"Enabled metadata effects require a non-null metadata placement.",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const placement = project.placements.find(
|
|
356
|
+
candidate => candidate.placementId === placementId,
|
|
357
|
+
);
|
|
358
|
+
if (placement === undefined) {
|
|
359
|
+
throw new ApplicationError(
|
|
360
|
+
"not-found",
|
|
361
|
+
`Unknown project metadata placement: ${placementId}`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (!placement.enabled) {
|
|
365
|
+
throw new ApplicationError(
|
|
366
|
+
"conflict",
|
|
367
|
+
`Project metadata placement is disabled: ${placementId}`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (placement.sync.provenance.kind === "unverified") {
|
|
371
|
+
throw new ApplicationError(
|
|
372
|
+
"conflict",
|
|
373
|
+
`Project metadata placement has unverified synchronization: ${placementId}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const asset = project.assets.find(
|
|
377
|
+
candidate => candidate.assetId === placement.assetId,
|
|
378
|
+
);
|
|
379
|
+
if (asset?.source.kind !== "recording") {
|
|
380
|
+
throw new ApplicationError(
|
|
381
|
+
"conflict",
|
|
382
|
+
`Metadata placement ${placementId} is not backed by a Slopcamera recording.`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
let manifest: Awaited<ReturnType<typeof openRecording>>["manifest"];
|
|
386
|
+
try {
|
|
387
|
+
manifest = (
|
|
388
|
+
await openRecording(
|
|
389
|
+
application.paths.artifactRoot,
|
|
390
|
+
asset.source.recordingId,
|
|
391
|
+
)
|
|
392
|
+
).manifest;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw recordingMetadataLoadError(error, placementId);
|
|
395
|
+
}
|
|
396
|
+
if (manifest.state !== "stopped") {
|
|
397
|
+
throw new ApplicationError(
|
|
398
|
+
"conflict",
|
|
399
|
+
`Metadata effects require an immutable stopped recording for placement ${placementId}.`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (manifest.capture.cursor !== "metadata") {
|
|
403
|
+
throw new ApplicationError(
|
|
404
|
+
"conflict",
|
|
405
|
+
`Interaction metadata capture was disabled for placement ${placementId}.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
(edit.clicks.enabled || edit.keystrokes.enabled || edit.typedText.enabled)
|
|
410
|
+
&& manifest.permissions.inputMonitoring !== "authorized"
|
|
411
|
+
) {
|
|
412
|
+
throw new ApplicationError(
|
|
413
|
+
"conflict",
|
|
414
|
+
`Input monitoring was not authorized for placement ${placementId}.`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
edit.typedText.enabled
|
|
419
|
+
&& manifest.capture.typedText !== "enabled"
|
|
420
|
+
) {
|
|
421
|
+
throw new ApplicationError(
|
|
422
|
+
"conflict",
|
|
423
|
+
`Typed-text capture was disabled for placement ${placementId}.`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (
|
|
427
|
+
edit.typedText.enabled
|
|
428
|
+
&& manifest.permissions.accessibility !== "authorized"
|
|
429
|
+
) {
|
|
430
|
+
throw new ApplicationError(
|
|
431
|
+
"conflict",
|
|
432
|
+
`Accessibility metadata was not authorized for placement ${placementId}.`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const actual = ProjectCommitMetadataBindingV2Schema.parse({
|
|
436
|
+
manifestSha256: canonicalJsonSha256(manifest),
|
|
437
|
+
placementId,
|
|
438
|
+
recordingId: asset.source.recordingId,
|
|
439
|
+
});
|
|
440
|
+
if (
|
|
441
|
+
expectedBinding !== undefined
|
|
442
|
+
&& (
|
|
443
|
+
expectedBinding.manifestSha256 !== actual.manifestSha256
|
|
444
|
+
|| expectedBinding.placementId !== actual.placementId
|
|
445
|
+
|| expectedBinding.recordingId !== actual.recordingId
|
|
446
|
+
)
|
|
447
|
+
) {
|
|
448
|
+
throw new ApplicationError(
|
|
449
|
+
"conflict",
|
|
450
|
+
"Recording metadata changed after the project edit batch was prepared.",
|
|
451
|
+
{
|
|
452
|
+
actualManifestSha256: actual.manifestSha256,
|
|
453
|
+
expectedManifestSha256: expectedBinding.manifestSha256,
|
|
454
|
+
placementId,
|
|
455
|
+
},
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return actual;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function bindProjectCommitEditsInputV2(
|
|
462
|
+
application: ApplicationContext,
|
|
463
|
+
input: unknown,
|
|
464
|
+
): Promise<CommitProjectEditsInputV2> {
|
|
465
|
+
const parsed = CommitProjectEditsInputV2Schema.parse(input);
|
|
466
|
+
const edit = parsed.batch.ordered.find(
|
|
467
|
+
(
|
|
468
|
+
candidate,
|
|
469
|
+
): candidate is Extract<
|
|
470
|
+
OrderedProjectEditV2,
|
|
471
|
+
{ readonly kind: "set-metadata-effects" }
|
|
472
|
+
> => candidate.kind === "set-metadata-effects",
|
|
473
|
+
);
|
|
474
|
+
if (edit === undefined || !metadataEffectsAreEnabled(edit)) return parsed;
|
|
475
|
+
const snapshot = await openProjectSnapshot(
|
|
476
|
+
application.paths.projectRoot,
|
|
477
|
+
parsed.project,
|
|
478
|
+
);
|
|
479
|
+
return await bindProjectCommitEditsInputV2FromSnapshot(
|
|
480
|
+
application,
|
|
481
|
+
parsed,
|
|
482
|
+
snapshot,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Host-binding path for callers that acquired a coherent project snapshot
|
|
488
|
+
* before entering this potentially long recording-evidence resolution.
|
|
489
|
+
*/
|
|
490
|
+
export async function bindProjectCommitEditsInputV2FromSnapshot(
|
|
491
|
+
application: ApplicationContext,
|
|
492
|
+
input: unknown,
|
|
493
|
+
snapshot: OpenProjectSnapshot,
|
|
494
|
+
): Promise<CommitProjectEditsInputV2> {
|
|
495
|
+
const parsed = CommitProjectEditsInputV2Schema.parse(input);
|
|
496
|
+
const edit = parsed.batch.ordered.find(
|
|
497
|
+
(
|
|
498
|
+
candidate,
|
|
499
|
+
): candidate is Extract<
|
|
500
|
+
OrderedProjectEditV2,
|
|
501
|
+
{ readonly kind: "set-metadata-effects" }
|
|
502
|
+
> => candidate.kind === "set-metadata-effects",
|
|
503
|
+
);
|
|
504
|
+
if (edit === undefined || !metadataEffectsAreEnabled(edit)) return parsed;
|
|
505
|
+
assertProjectEditBasis(parsed.basis, snapshot);
|
|
506
|
+
const metadataBinding = await assertMetadataEffectsAvailable(
|
|
507
|
+
application,
|
|
508
|
+
snapshot.project,
|
|
509
|
+
edit,
|
|
510
|
+
);
|
|
511
|
+
if (metadataBinding === undefined) {
|
|
512
|
+
throw new ApplicationError(
|
|
513
|
+
"internal",
|
|
514
|
+
"Enabled metadata effects did not produce a recording metadata binding.",
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return CommitProjectEditsInputV2Schema.parse({
|
|
518
|
+
...parsed,
|
|
519
|
+
metadataBinding,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function manualZoomInputs(
|
|
524
|
+
ordered: readonly OrderedProjectEditV3[],
|
|
525
|
+
): readonly ManualProjectZoomInputV3[] {
|
|
526
|
+
return ordered.flatMap(edit => (
|
|
527
|
+
edit.kind === "add-manual-zooms" ? edit.zooms : []
|
|
528
|
+
));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
type ProjectAsset = VideoProjectV1["assets"][number];
|
|
532
|
+
type ProjectPlacement = VideoProjectV1["placements"][number];
|
|
533
|
+
interface ManualZoomRecordingEvidence {
|
|
534
|
+
readonly manifest: Awaited<
|
|
535
|
+
ReturnType<typeof openRecording>
|
|
536
|
+
>["manifest"];
|
|
537
|
+
readonly manifestSha256: string;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
interface ManualZoomResolutionContext {
|
|
541
|
+
readonly assets: ReadonlyMap<string, ProjectAsset>;
|
|
542
|
+
readonly layers: Map<
|
|
543
|
+
string,
|
|
544
|
+
ResolvedProjectMetadataSourceLayer | null
|
|
545
|
+
>;
|
|
546
|
+
readonly placements: ReadonlyMap<string, ProjectPlacement>;
|
|
547
|
+
readonly recordings: Map<string, Promise<ManualZoomRecordingEvidence>>;
|
|
548
|
+
readonly syncSha256: Map<string, string>;
|
|
549
|
+
readonly onResolutionCacheMiss:
|
|
550
|
+
BindProjectCommitEditsInputV3Options["onResolutionCacheMiss"];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export type ManualZoomResolutionCacheKind =
|
|
554
|
+
| "display-layer"
|
|
555
|
+
| "recording"
|
|
556
|
+
| "synchronization";
|
|
557
|
+
|
|
558
|
+
export interface BindProjectCommitEditsInputV3Options {
|
|
559
|
+
/**
|
|
560
|
+
* Test/diagnostic hook. It fires only when host evidence is actually
|
|
561
|
+
* resolved, never for a cache hit.
|
|
562
|
+
*/
|
|
563
|
+
readonly onResolutionCacheMiss?: (
|
|
564
|
+
kind: ManualZoomResolutionCacheKind,
|
|
565
|
+
key: string,
|
|
566
|
+
) => void;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function manualZoomResolutionContext(
|
|
570
|
+
project: VideoProjectV1,
|
|
571
|
+
options: BindProjectCommitEditsInputV3Options = {},
|
|
572
|
+
): ManualZoomResolutionContext {
|
|
573
|
+
return {
|
|
574
|
+
assets: new Map(project.assets.map(asset => [asset.assetId, asset])),
|
|
575
|
+
layers: new Map(),
|
|
576
|
+
placements: new Map(project.placements.map(
|
|
577
|
+
placement => [placement.placementId, placement],
|
|
578
|
+
)),
|
|
579
|
+
recordings: new Map(),
|
|
580
|
+
syncSha256: new Map(),
|
|
581
|
+
onResolutionCacheMiss: options.onResolutionCacheMiss,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function indexManualZoomBindings(
|
|
586
|
+
bindings: readonly ProjectCommitManualZoomBindingV3[],
|
|
587
|
+
): ReadonlyMap<string, ProjectCommitManualZoomBindingV3> {
|
|
588
|
+
const indexed = new Map<string, ProjectCommitManualZoomBindingV3>();
|
|
589
|
+
for (const binding of bindings) {
|
|
590
|
+
if (indexed.has(binding.zoomId)) {
|
|
591
|
+
throw new ApplicationError(
|
|
592
|
+
"incompatible",
|
|
593
|
+
`Manual zoom binding is duplicated: ${binding.zoomId}`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
indexed.set(binding.zoomId, binding);
|
|
597
|
+
}
|
|
598
|
+
return indexed;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function manualZoomBindingFor(
|
|
602
|
+
bindings: ReadonlyMap<string, ProjectCommitManualZoomBindingV3>,
|
|
603
|
+
zoomId: string,
|
|
604
|
+
): ProjectCommitManualZoomBindingV3 {
|
|
605
|
+
const binding = bindings.get(zoomId);
|
|
606
|
+
if (binding === undefined) {
|
|
607
|
+
throw new ApplicationError(
|
|
608
|
+
"incompatible",
|
|
609
|
+
`Manual zoom ${zoomId} requires a host-owned recording binding.`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
return binding;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function resolveManualZoomBinding(
|
|
616
|
+
application: ApplicationContext,
|
|
617
|
+
project: VideoProjectV1,
|
|
618
|
+
resolution: ManualZoomResolutionContext,
|
|
619
|
+
input: ManualProjectZoomInputV3,
|
|
620
|
+
defaultPlacementId: VideoProjectV1["referencePlacementId"],
|
|
621
|
+
expectedBinding?: ProjectCommitManualZoomBindingV3,
|
|
622
|
+
): Promise<ProjectCommitManualZoomBindingV3> {
|
|
623
|
+
const placementId = input.placementId
|
|
624
|
+
?? expectedBinding?.placementId
|
|
625
|
+
?? defaultPlacementId;
|
|
626
|
+
const placement = resolution.placements.get(placementId);
|
|
627
|
+
if (placement === undefined) {
|
|
628
|
+
throw new ApplicationError(
|
|
629
|
+
"not-found",
|
|
630
|
+
`Unknown project zoom placement: ${placementId}`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (!placement.enabled) {
|
|
634
|
+
throw new ApplicationError(
|
|
635
|
+
"conflict",
|
|
636
|
+
`Project zoom placement is disabled: ${placementId}`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (placement.sync.provenance.kind === "unverified") {
|
|
640
|
+
throw new ApplicationError(
|
|
641
|
+
"conflict",
|
|
642
|
+
`Project zoom placement has unverified synchronization: ${placementId}`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (input.range.endUs > project.timeline.durationUs) {
|
|
646
|
+
throw new ApplicationError(
|
|
647
|
+
"invalid-data",
|
|
648
|
+
`Manual zoom ${input.zoomId} exceeds the project timeline.`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (!projectIntervalHasCompleteSyncCoverage(
|
|
652
|
+
placement.sync,
|
|
653
|
+
input.range,
|
|
654
|
+
)) {
|
|
655
|
+
throw new ApplicationError(
|
|
656
|
+
"conflict",
|
|
657
|
+
`Manual zoom ${input.zoomId} is not fully covered by placement ${placementId}.`,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const asset = resolution.assets.get(placement.assetId);
|
|
661
|
+
if (asset?.source.kind !== "recording") {
|
|
662
|
+
throw new ApplicationError(
|
|
663
|
+
"conflict",
|
|
664
|
+
`Zoom placement ${placementId} is not backed by a Slopcamera recording.`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
let recording: ManualZoomRecordingEvidence;
|
|
668
|
+
try {
|
|
669
|
+
let pending = resolution.recordings.get(asset.source.recordingId);
|
|
670
|
+
if (pending === undefined) {
|
|
671
|
+
resolution.onResolutionCacheMiss?.(
|
|
672
|
+
"recording",
|
|
673
|
+
asset.source.recordingId,
|
|
674
|
+
);
|
|
675
|
+
pending = openRecording(
|
|
676
|
+
application.paths.artifactRoot,
|
|
677
|
+
asset.source.recordingId,
|
|
678
|
+
).then(opened => ({
|
|
679
|
+
manifest: opened.manifest,
|
|
680
|
+
manifestSha256: canonicalJsonSha256(opened.manifest),
|
|
681
|
+
}));
|
|
682
|
+
resolution.recordings.set(asset.source.recordingId, pending);
|
|
683
|
+
}
|
|
684
|
+
recording = await pending;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
throw recordingMetadataLoadError(error, placementId);
|
|
687
|
+
}
|
|
688
|
+
const { manifest } = recording;
|
|
689
|
+
if (manifest.state !== "stopped") {
|
|
690
|
+
throw new ApplicationError(
|
|
691
|
+
"conflict",
|
|
692
|
+
`Manual zooms require an immutable stopped recording for placement ${placementId}.`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const displayId = input.displayId
|
|
696
|
+
?? expectedBinding?.displayId
|
|
697
|
+
?? manifest.sources.displays.find(display => display.isPrimary)?.displayId;
|
|
698
|
+
if (displayId === undefined) {
|
|
699
|
+
throw new ApplicationError(
|
|
700
|
+
"not-found",
|
|
701
|
+
`Manual zoom ${input.zoomId} requires a selected or primary display.`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const layerKey = `${placementId}\0${displayId}`;
|
|
705
|
+
let layer = resolution.layers.get(layerKey);
|
|
706
|
+
if (layer === undefined) {
|
|
707
|
+
resolution.onResolutionCacheMiss?.("display-layer", layerKey);
|
|
708
|
+
try {
|
|
709
|
+
layer = resolveProjectMetadataSourceLayer(
|
|
710
|
+
manifest,
|
|
711
|
+
asset,
|
|
712
|
+
placement,
|
|
713
|
+
displayId,
|
|
714
|
+
);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (!(error instanceof TypeError)) throw error;
|
|
717
|
+
throw new ApplicationError("conflict", error.message);
|
|
718
|
+
}
|
|
719
|
+
resolution.layers.set(layerKey, layer);
|
|
720
|
+
}
|
|
721
|
+
if (layer === null) {
|
|
722
|
+
throw new ApplicationError(
|
|
723
|
+
"conflict",
|
|
724
|
+
`Display ${displayId} is not an enabled video layer on placement ${placementId}.`,
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
if (input.target.kind === "point" || input.target.kind === "rect") {
|
|
728
|
+
try {
|
|
729
|
+
assertStaticProjectZoomTargetVisible(input.target, {
|
|
730
|
+
display: layer.display,
|
|
731
|
+
presentation: layer.presentation,
|
|
732
|
+
stream: layer.stream,
|
|
733
|
+
});
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (!(error instanceof TypeError)) throw error;
|
|
736
|
+
throw new ApplicationError(
|
|
737
|
+
"invalid-data",
|
|
738
|
+
`Manual zoom ${input.zoomId} has a target outside the visible display layer: ${error.message}`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (
|
|
743
|
+
input.target.kind === "cursor"
|
|
744
|
+
&& manifest.capture.cursor !== "metadata"
|
|
745
|
+
) {
|
|
746
|
+
throw new ApplicationError(
|
|
747
|
+
"conflict",
|
|
748
|
+
`Cursor metadata capture was disabled for placement ${placementId}.`,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (
|
|
752
|
+
input.target.kind === "window"
|
|
753
|
+
&& (
|
|
754
|
+
manifest.capture.windowMetadata === "disabled"
|
|
755
|
+
|| manifest.permissions.windowMetadata !== "authorized"
|
|
756
|
+
)
|
|
757
|
+
) {
|
|
758
|
+
throw new ApplicationError(
|
|
759
|
+
"conflict",
|
|
760
|
+
`Window metadata was unavailable for placement ${placementId}.`,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
if (
|
|
764
|
+
input.target.kind === "focused-input"
|
|
765
|
+
&& manifest.permissions.accessibility !== "authorized"
|
|
766
|
+
) {
|
|
767
|
+
throw new ApplicationError(
|
|
768
|
+
"conflict",
|
|
769
|
+
`Accessibility metadata was unavailable for placement ${placementId}.`,
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
let syncSha256 = resolution.syncSha256.get(placementId);
|
|
773
|
+
if (syncSha256 === undefined) {
|
|
774
|
+
resolution.onResolutionCacheMiss?.("synchronization", placementId);
|
|
775
|
+
syncSha256 = hashPlacementSync(placement);
|
|
776
|
+
resolution.syncSha256.set(placementId, syncSha256);
|
|
777
|
+
}
|
|
778
|
+
const actual = ProjectCommitManualZoomBindingV3Schema.parse({
|
|
779
|
+
displayId,
|
|
780
|
+
manifestSha256: recording.manifestSha256,
|
|
781
|
+
placementId,
|
|
782
|
+
recordingId: asset.source.recordingId,
|
|
783
|
+
syncSha256,
|
|
784
|
+
zoomId: input.zoomId,
|
|
785
|
+
});
|
|
786
|
+
if (
|
|
787
|
+
expectedBinding !== undefined
|
|
788
|
+
&& canonicalJsonSha256(actual) !== canonicalJsonSha256(expectedBinding)
|
|
789
|
+
) {
|
|
790
|
+
throw new ApplicationError(
|
|
791
|
+
"conflict",
|
|
792
|
+
`Manual zoom ${input.zoomId} bindings changed after the edit batch was prepared.`,
|
|
793
|
+
{
|
|
794
|
+
actualManifestSha256: actual.manifestSha256,
|
|
795
|
+
expectedManifestSha256: expectedBinding.manifestSha256,
|
|
796
|
+
placementId,
|
|
797
|
+
},
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return actual;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export async function bindProjectCommitEditsInputV3(
|
|
804
|
+
application: ApplicationContext,
|
|
805
|
+
input: unknown,
|
|
806
|
+
options: BindProjectCommitEditsInputV3Options = {},
|
|
807
|
+
): Promise<CommitProjectEditsInputV3> {
|
|
808
|
+
const parsed = CommitProjectEditsInputV3Schema.parse(input);
|
|
809
|
+
const metadataEdit = parsed.batch.ordered.find(
|
|
810
|
+
(
|
|
811
|
+
candidate,
|
|
812
|
+
): candidate is Extract<
|
|
813
|
+
OrderedProjectEditV3,
|
|
814
|
+
{ readonly kind: "set-metadata-effects" }
|
|
815
|
+
> => candidate.kind === "set-metadata-effects",
|
|
816
|
+
);
|
|
817
|
+
const zooms = manualZoomInputs(parsed.batch.ordered);
|
|
818
|
+
const requiresMetadataBinding = metadataEdit !== undefined
|
|
819
|
+
&& metadataEffectsAreEnabled(metadataEdit);
|
|
820
|
+
if (!requiresMetadataBinding && zooms.length === 0) return parsed;
|
|
821
|
+
const snapshot = await openProjectSnapshot(
|
|
822
|
+
application.paths.projectRoot,
|
|
823
|
+
parsed.project,
|
|
824
|
+
);
|
|
825
|
+
return await bindProjectCommitEditsInputV3FromSnapshot(
|
|
826
|
+
application,
|
|
827
|
+
parsed,
|
|
828
|
+
snapshot,
|
|
829
|
+
options,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Host-binding path for callers that acquired a coherent project snapshot
|
|
835
|
+
* before resolving recording and display evidence.
|
|
836
|
+
*/
|
|
837
|
+
export async function bindProjectCommitEditsInputV3FromSnapshot(
|
|
838
|
+
application: ApplicationContext,
|
|
839
|
+
input: unknown,
|
|
840
|
+
snapshot: OpenProjectSnapshot,
|
|
841
|
+
options: BindProjectCommitEditsInputV3Options = {},
|
|
842
|
+
): Promise<CommitProjectEditsInputV3> {
|
|
843
|
+
const parsed = CommitProjectEditsInputV3Schema.parse(input);
|
|
844
|
+
const metadataEdit = parsed.batch.ordered.find(
|
|
845
|
+
(
|
|
846
|
+
candidate,
|
|
847
|
+
): candidate is Extract<
|
|
848
|
+
OrderedProjectEditV3,
|
|
849
|
+
{ readonly kind: "set-metadata-effects" }
|
|
850
|
+
> => candidate.kind === "set-metadata-effects",
|
|
851
|
+
);
|
|
852
|
+
const zooms = manualZoomInputs(parsed.batch.ordered);
|
|
853
|
+
const requiresMetadataBinding = metadataEdit !== undefined
|
|
854
|
+
&& metadataEffectsAreEnabled(metadataEdit);
|
|
855
|
+
if (!requiresMetadataBinding && zooms.length === 0) return parsed;
|
|
856
|
+
assertProjectEditBasis(parsed.basis, snapshot);
|
|
857
|
+
const metadataBinding = requiresMetadataBinding
|
|
858
|
+
? await assertMetadataEffectsAvailable(
|
|
859
|
+
application,
|
|
860
|
+
snapshot.project,
|
|
861
|
+
metadataEdit,
|
|
862
|
+
)
|
|
863
|
+
: undefined;
|
|
864
|
+
const defaultPlacementId = snapshot.plan.effects.metadataPlacementId
|
|
865
|
+
?? snapshot.project.referencePlacementId;
|
|
866
|
+
const resolution = manualZoomResolutionContext(
|
|
867
|
+
snapshot.project,
|
|
868
|
+
options,
|
|
869
|
+
);
|
|
870
|
+
const manualZoomBindings: ProjectCommitManualZoomBindingV3[] = [];
|
|
871
|
+
for (const zoom of zooms) {
|
|
872
|
+
manualZoomBindings.push(await resolveManualZoomBinding(
|
|
873
|
+
application,
|
|
874
|
+
snapshot.project,
|
|
875
|
+
resolution,
|
|
876
|
+
zoom,
|
|
877
|
+
defaultPlacementId,
|
|
878
|
+
));
|
|
879
|
+
}
|
|
880
|
+
return CommitProjectEditsInputV3Schema.parse({
|
|
881
|
+
...parsed,
|
|
882
|
+
...(manualZoomBindings.length === 0 ? {} : { manualZoomBindings }),
|
|
883
|
+
...(metadataBinding === undefined ? {} : { metadataBinding }),
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function manualCameraMove(
|
|
888
|
+
project: VideoProjectV1,
|
|
889
|
+
input: Extract<
|
|
890
|
+
OrderedProjectEditV2,
|
|
891
|
+
{ readonly kind: "add-manual-camera-moves" }
|
|
892
|
+
>["cameraMoves"][number],
|
|
893
|
+
) {
|
|
894
|
+
const placement = project.placements.find(
|
|
895
|
+
candidate => candidate.placementId === input.placementId,
|
|
896
|
+
);
|
|
897
|
+
if (placement === undefined) {
|
|
898
|
+
throw new ApplicationError(
|
|
899
|
+
"not-found",
|
|
900
|
+
`Unknown camera placement: ${input.placementId}`,
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const asset = project.assets.find(
|
|
904
|
+
candidate => candidate.assetId === placement.assetId,
|
|
905
|
+
);
|
|
906
|
+
const stream = asset?.streams.find(
|
|
907
|
+
candidate => candidate.streamId === input.streamId,
|
|
908
|
+
);
|
|
909
|
+
if (stream?.kind !== "video") {
|
|
910
|
+
throw new ApplicationError(
|
|
911
|
+
"not-found",
|
|
912
|
+
`Unknown camera video stream: ${input.placementId}:${input.streamId}`,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
const configured = placement.video.find(
|
|
916
|
+
candidate => candidate.streamId === input.streamId,
|
|
917
|
+
);
|
|
918
|
+
if (configured === undefined) {
|
|
919
|
+
throw new ApplicationError(
|
|
920
|
+
"conflict",
|
|
921
|
+
`Camera video stream is not configured on placement ${input.placementId}.`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (!placement.enabled || !configured.presentation.enabled) {
|
|
925
|
+
throw new ApplicationError(
|
|
926
|
+
"conflict",
|
|
927
|
+
`Camera move ${input.cameraMoveId} targets a disabled video layer.`,
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
const move = ProjectCameraMoveSchema.parse({
|
|
932
|
+
...input,
|
|
933
|
+
binding: {
|
|
934
|
+
geometrySha256: hashProjectCameraGeometry(
|
|
935
|
+
project,
|
|
936
|
+
input.placementId,
|
|
937
|
+
input.streamId,
|
|
938
|
+
),
|
|
939
|
+
syncSha256: hashProjectCameraSync(placement),
|
|
940
|
+
},
|
|
941
|
+
origin: { kind: "manual" },
|
|
942
|
+
});
|
|
943
|
+
assertProjectCameraMoveBindings(project, move);
|
|
944
|
+
return move;
|
|
945
|
+
} catch (error) {
|
|
946
|
+
if (error instanceof ApplicationError) throw error;
|
|
947
|
+
throw new ApplicationError(
|
|
948
|
+
"invalid-data",
|
|
949
|
+
`Manual camera move ${input.cameraMoveId} is invalid for the current project.`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async function applyOrderedProjectEditV2(
|
|
955
|
+
application: ApplicationContext,
|
|
956
|
+
project: VideoProjectV1,
|
|
957
|
+
plan: ProjectEditPlanV1,
|
|
958
|
+
edit: OrderedProjectEditV2,
|
|
959
|
+
metadataBinding: ProjectCommitMetadataBindingV2 | undefined,
|
|
960
|
+
updatedAt: string,
|
|
961
|
+
normalize = true,
|
|
962
|
+
): Promise<ProjectEditPlanV1> {
|
|
963
|
+
switch (edit.kind) {
|
|
964
|
+
case "cut":
|
|
965
|
+
case "trim":
|
|
966
|
+
case "speed":
|
|
967
|
+
case "add-overlays":
|
|
968
|
+
case "remove-overlays":
|
|
969
|
+
return applyOrderedProjectEdit(
|
|
970
|
+
project,
|
|
971
|
+
plan,
|
|
972
|
+
edit,
|
|
973
|
+
updatedAt,
|
|
974
|
+
normalize,
|
|
975
|
+
);
|
|
976
|
+
case "add-zooms": {
|
|
977
|
+
const existing = new Set(
|
|
978
|
+
plan.zooms.map(zoom => zoom.operation.zoomId),
|
|
979
|
+
);
|
|
980
|
+
const conflicts = edit.zooms
|
|
981
|
+
.map(zoom => zoom.operation.zoomId)
|
|
982
|
+
.filter(zoomId => existing.has(zoomId))
|
|
983
|
+
.sort();
|
|
984
|
+
if (conflicts.length > 0) {
|
|
985
|
+
throw new ApplicationError(
|
|
986
|
+
"conflict",
|
|
987
|
+
`Zoom ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
return finalizeProjectEditDraft({
|
|
992
|
+
...plan,
|
|
993
|
+
updatedAt,
|
|
994
|
+
zooms: [...plan.zooms, ...edit.zooms],
|
|
995
|
+
}, normalize);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (error instanceof ApplicationError) throw error;
|
|
998
|
+
throw new ApplicationError(
|
|
999
|
+
"conflict",
|
|
1000
|
+
"Zooms are incompatible with the current project edit plan.",
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
case "remove-zooms": {
|
|
1005
|
+
const requested = new Set(edit.zoomIds);
|
|
1006
|
+
const existing = new Set(
|
|
1007
|
+
plan.zooms.map(zoom => zoom.operation.zoomId),
|
|
1008
|
+
);
|
|
1009
|
+
const missing = [...requested]
|
|
1010
|
+
.filter(zoomId => !existing.has(zoomId))
|
|
1011
|
+
.sort();
|
|
1012
|
+
if (missing.length > 0) {
|
|
1013
|
+
throw new ApplicationError(
|
|
1014
|
+
"not-found",
|
|
1015
|
+
`Unknown zoom${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return finalizeProjectEditDraft({
|
|
1019
|
+
...plan,
|
|
1020
|
+
updatedAt,
|
|
1021
|
+
zooms: plan.zooms.filter(
|
|
1022
|
+
zoom => !requested.has(zoom.operation.zoomId),
|
|
1023
|
+
),
|
|
1024
|
+
}, normalize);
|
|
1025
|
+
}
|
|
1026
|
+
case "set-metadata-effects":
|
|
1027
|
+
await assertMetadataEffectsAvailable(
|
|
1028
|
+
application,
|
|
1029
|
+
project,
|
|
1030
|
+
edit,
|
|
1031
|
+
metadataBinding,
|
|
1032
|
+
);
|
|
1033
|
+
return finalizeProjectEditDraft({
|
|
1034
|
+
...plan,
|
|
1035
|
+
effects: {
|
|
1036
|
+
clicks: edit.clicks,
|
|
1037
|
+
cursor: edit.cursor,
|
|
1038
|
+
keystrokes: edit.keystrokes,
|
|
1039
|
+
metadataPlacementId: edit.metadataPlacementId,
|
|
1040
|
+
typedText: edit.typedText,
|
|
1041
|
+
},
|
|
1042
|
+
updatedAt,
|
|
1043
|
+
}, normalize);
|
|
1044
|
+
case "add-manual-camera-moves": {
|
|
1045
|
+
const existing = new Set(
|
|
1046
|
+
plan.cameraMoves.map(move => move.cameraMoveId),
|
|
1047
|
+
);
|
|
1048
|
+
const conflicts = edit.cameraMoves
|
|
1049
|
+
.map(move => move.cameraMoveId)
|
|
1050
|
+
.filter(cameraMoveId => existing.has(cameraMoveId))
|
|
1051
|
+
.sort();
|
|
1052
|
+
if (conflicts.length > 0) {
|
|
1053
|
+
throw new ApplicationError(
|
|
1054
|
+
"conflict",
|
|
1055
|
+
`Camera move ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const additions = edit.cameraMoves.map(
|
|
1059
|
+
move => manualCameraMove(project, move),
|
|
1060
|
+
);
|
|
1061
|
+
try {
|
|
1062
|
+
return finalizeProjectEditDraft({
|
|
1063
|
+
...plan,
|
|
1064
|
+
cameraMoves: [...plan.cameraMoves, ...additions],
|
|
1065
|
+
updatedAt,
|
|
1066
|
+
}, normalize);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error instanceof ApplicationError) throw error;
|
|
1069
|
+
throw new ApplicationError(
|
|
1070
|
+
"conflict",
|
|
1071
|
+
"Manual camera moves are incompatible with the current project edit plan.",
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
case "remove-camera-moves": {
|
|
1076
|
+
const requested = new Set(edit.cameraMoveIds);
|
|
1077
|
+
const existing = new Set(
|
|
1078
|
+
plan.cameraMoves.map(move => move.cameraMoveId),
|
|
1079
|
+
);
|
|
1080
|
+
const missing = [...requested]
|
|
1081
|
+
.filter(cameraMoveId => !existing.has(cameraMoveId))
|
|
1082
|
+
.sort();
|
|
1083
|
+
if (missing.length > 0) {
|
|
1084
|
+
throw new ApplicationError(
|
|
1085
|
+
"not-found",
|
|
1086
|
+
`Unknown camera move${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
return finalizeProjectEditDraft({
|
|
1090
|
+
...plan,
|
|
1091
|
+
cameraMoves: plan.cameraMoves.filter(
|
|
1092
|
+
move => !requested.has(move.cameraMoveId),
|
|
1093
|
+
),
|
|
1094
|
+
updatedAt,
|
|
1095
|
+
}, normalize);
|
|
1096
|
+
}
|
|
1097
|
+
default: {
|
|
1098
|
+
const exhaustive: never = edit;
|
|
1099
|
+
return exhaustive;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function manualProjectZoom(
|
|
1105
|
+
input: ManualProjectZoomInputV3,
|
|
1106
|
+
binding: ProjectCommitManualZoomBindingV3,
|
|
1107
|
+
) {
|
|
1108
|
+
if (
|
|
1109
|
+
(input.placementId !== undefined && input.placementId !== binding.placementId)
|
|
1110
|
+
|| (input.displayId !== undefined && input.displayId !== binding.displayId)
|
|
1111
|
+
|| input.zoomId !== binding.zoomId
|
|
1112
|
+
) {
|
|
1113
|
+
throw new ApplicationError(
|
|
1114
|
+
"conflict",
|
|
1115
|
+
`Manual zoom ${input.zoomId} does not match its host-owned binding.`,
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
return ProjectZoomOperationSchema.parse({
|
|
1120
|
+
operation: {
|
|
1121
|
+
displayId: binding.displayId,
|
|
1122
|
+
easing: input.easing,
|
|
1123
|
+
enterDurationUs: input.enterDurationUs,
|
|
1124
|
+
exitDurationUs: input.exitDurationUs,
|
|
1125
|
+
kind: "manual",
|
|
1126
|
+
range: input.range,
|
|
1127
|
+
scale: input.scale,
|
|
1128
|
+
target: input.target,
|
|
1129
|
+
zoomId: input.zoomId,
|
|
1130
|
+
},
|
|
1131
|
+
placementId: binding.placementId,
|
|
1132
|
+
});
|
|
1133
|
+
} catch {
|
|
1134
|
+
throw new ApplicationError(
|
|
1135
|
+
"invalid-data",
|
|
1136
|
+
`Manual zoom ${input.zoomId} is invalid for the current project.`,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function removeKnownProjectEditIds<Id extends string>(
|
|
1142
|
+
available: Set<Id>,
|
|
1143
|
+
ids: readonly Id[],
|
|
1144
|
+
label: string,
|
|
1145
|
+
): void {
|
|
1146
|
+
const requested = new Set(ids);
|
|
1147
|
+
const missing = [...requested]
|
|
1148
|
+
.filter(id => !available.has(id))
|
|
1149
|
+
.sort();
|
|
1150
|
+
if (missing.length > 0) {
|
|
1151
|
+
throw new ApplicationError(
|
|
1152
|
+
"not-found",
|
|
1153
|
+
`Unknown ${label}${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`,
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
for (const id of requested) available.delete(id);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async function applyOrderedProjectEditV3(
|
|
1160
|
+
application: ApplicationContext,
|
|
1161
|
+
project: VideoProjectV1,
|
|
1162
|
+
plan: ProjectEditPlanV1,
|
|
1163
|
+
edit: OrderedProjectEditV3,
|
|
1164
|
+
metadataBinding: ProjectCommitMetadataBindingV2 | undefined,
|
|
1165
|
+
manualZoomBindings: ReadonlyMap<
|
|
1166
|
+
string,
|
|
1167
|
+
ProjectCommitManualZoomBindingV3
|
|
1168
|
+
>,
|
|
1169
|
+
updatedAt: string,
|
|
1170
|
+
normalize = true,
|
|
1171
|
+
): Promise<ProjectEditPlanV1> {
|
|
1172
|
+
if (edit.kind !== "add-manual-zooms") {
|
|
1173
|
+
return await applyOrderedProjectEditV2(
|
|
1174
|
+
application,
|
|
1175
|
+
project,
|
|
1176
|
+
plan,
|
|
1177
|
+
edit,
|
|
1178
|
+
metadataBinding,
|
|
1179
|
+
updatedAt,
|
|
1180
|
+
normalize,
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const existing = new Set(
|
|
1184
|
+
plan.zooms.map(zoom => zoom.operation.zoomId),
|
|
1185
|
+
);
|
|
1186
|
+
const conflicts = edit.zooms
|
|
1187
|
+
.map(zoom => zoom.zoomId)
|
|
1188
|
+
.filter(zoomId => existing.has(zoomId))
|
|
1189
|
+
.sort();
|
|
1190
|
+
if (conflicts.length > 0) {
|
|
1191
|
+
throw new ApplicationError(
|
|
1192
|
+
"conflict",
|
|
1193
|
+
`Zoom ID${conflicts.length === 1 ? "" : "s"} already exist${conflicts.length === 1 ? "s" : ""}: ${conflicts.join(", ")}`,
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
const additions = edit.zooms.map(zoom => manualProjectZoom(
|
|
1197
|
+
zoom,
|
|
1198
|
+
manualZoomBindingFor(manualZoomBindings, zoom.zoomId),
|
|
1199
|
+
));
|
|
1200
|
+
try {
|
|
1201
|
+
return finalizeProjectEditDraft({
|
|
1202
|
+
...plan,
|
|
1203
|
+
updatedAt,
|
|
1204
|
+
zooms: [...plan.zooms, ...additions],
|
|
1205
|
+
}, normalize);
|
|
1206
|
+
} catch {
|
|
1207
|
+
throw new ApplicationError(
|
|
1208
|
+
"conflict",
|
|
1209
|
+
"Manual zooms are incompatible with the current project edit plan.",
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function applyOrderedProjectEditPhaseV3(
|
|
1215
|
+
application: ApplicationContext,
|
|
1216
|
+
project: VideoProjectV1,
|
|
1217
|
+
plan: ProjectEditPlanV1,
|
|
1218
|
+
edits: readonly OrderedProjectEditV3[],
|
|
1219
|
+
metadataBinding: ProjectCommitMetadataBindingV2 | undefined,
|
|
1220
|
+
manualZoomBindings: ReadonlyMap<
|
|
1221
|
+
string,
|
|
1222
|
+
ProjectCommitManualZoomBindingV3
|
|
1223
|
+
>,
|
|
1224
|
+
updatedAt: string,
|
|
1225
|
+
abortSignal: AbortSignal,
|
|
1226
|
+
): Promise<ProjectEditPlanV1> {
|
|
1227
|
+
const first = edits[0];
|
|
1228
|
+
if (first === undefined) return plan;
|
|
1229
|
+
const phase: ProjectEditNormalizationPhaseV3 =
|
|
1230
|
+
projectEditNormalizationPhaseV3(first);
|
|
1231
|
+
if (edits.some(edit => projectEditNormalizationPhaseV3(edit) !== phase)) {
|
|
1232
|
+
throw new ApplicationError(
|
|
1233
|
+
"internal",
|
|
1234
|
+
"A project edit normalization phase contained incompatible edits.",
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
switch (phase) {
|
|
1239
|
+
case "structural": {
|
|
1240
|
+
const cuts: Extract<
|
|
1241
|
+
OrderedProjectEditV3,
|
|
1242
|
+
{ readonly kind: "cut" }
|
|
1243
|
+
>["range"][] = [];
|
|
1244
|
+
const speed: ProjectEditPlanV1["speed"][number][] = [];
|
|
1245
|
+
let trimStartUs = 0;
|
|
1246
|
+
let trimEndUs = project.timeline.durationUs;
|
|
1247
|
+
let hasTrim = false;
|
|
1248
|
+
for (const edit of edits) {
|
|
1249
|
+
throwIfAborted(abortSignal);
|
|
1250
|
+
switch (edit.kind) {
|
|
1251
|
+
case "cut":
|
|
1252
|
+
cuts.push(edit.range);
|
|
1253
|
+
break;
|
|
1254
|
+
case "speed":
|
|
1255
|
+
speed.push({ range: edit.range, rate: edit.rate });
|
|
1256
|
+
break;
|
|
1257
|
+
case "trim":
|
|
1258
|
+
hasTrim = true;
|
|
1259
|
+
trimStartUs = Math.max(trimStartUs, edit.range.startUs);
|
|
1260
|
+
trimEndUs = Math.min(trimEndUs, edit.range.endUs);
|
|
1261
|
+
break;
|
|
1262
|
+
case "add-manual-camera-moves":
|
|
1263
|
+
case "add-manual-zooms":
|
|
1264
|
+
case "add-overlays":
|
|
1265
|
+
case "add-zooms":
|
|
1266
|
+
case "remove-camera-moves":
|
|
1267
|
+
case "remove-overlays":
|
|
1268
|
+
case "remove-zooms":
|
|
1269
|
+
case "set-metadata-effects":
|
|
1270
|
+
throw new ApplicationError(
|
|
1271
|
+
"internal",
|
|
1272
|
+
"A structural project edit phase contained a non-structural edit.",
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const trimmedKeep = !hasTrim
|
|
1277
|
+
? plan.keep
|
|
1278
|
+
: trimStartUs >= trimEndUs
|
|
1279
|
+
? []
|
|
1280
|
+
: intersectIntervals(plan.keep, [{
|
|
1281
|
+
endUs: trimEndUs,
|
|
1282
|
+
startUs: trimStartUs,
|
|
1283
|
+
}]);
|
|
1284
|
+
return {
|
|
1285
|
+
...plan,
|
|
1286
|
+
keep: subtractIntervals(trimmedKeep, cuts),
|
|
1287
|
+
speed: [...plan.speed, ...speed],
|
|
1288
|
+
updatedAt,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
case "add": {
|
|
1292
|
+
const cameraMoveIds = new Set(
|
|
1293
|
+
plan.cameraMoves.map(move => move.cameraMoveId),
|
|
1294
|
+
);
|
|
1295
|
+
const overlayIds = new Set(
|
|
1296
|
+
plan.overlays.map(overlay => overlay.overlayId),
|
|
1297
|
+
);
|
|
1298
|
+
const zoomIds = new Set(
|
|
1299
|
+
plan.zooms.map(zoom => zoom.operation.zoomId),
|
|
1300
|
+
);
|
|
1301
|
+
const cameraMoves: ProjectEditPlanV1["cameraMoves"][number][] = [];
|
|
1302
|
+
const overlays: ProjectEditPlanV1["overlays"][number][] = [];
|
|
1303
|
+
const zooms: ProjectEditPlanV1["zooms"][number][] = [];
|
|
1304
|
+
for (const edit of edits) {
|
|
1305
|
+
throwIfAborted(abortSignal);
|
|
1306
|
+
switch (edit.kind) {
|
|
1307
|
+
case "add-manual-camera-moves":
|
|
1308
|
+
for (const input of edit.cameraMoves) {
|
|
1309
|
+
if (cameraMoveIds.has(input.cameraMoveId)) {
|
|
1310
|
+
throw new ApplicationError(
|
|
1311
|
+
"conflict",
|
|
1312
|
+
`Camera move ID already exists: ${input.cameraMoveId}`,
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
cameraMoveIds.add(input.cameraMoveId);
|
|
1316
|
+
cameraMoves.push(manualCameraMove(project, input));
|
|
1317
|
+
}
|
|
1318
|
+
break;
|
|
1319
|
+
case "add-manual-zooms":
|
|
1320
|
+
for (const input of edit.zooms) {
|
|
1321
|
+
if (zoomIds.has(input.zoomId)) {
|
|
1322
|
+
throw new ApplicationError(
|
|
1323
|
+
"conflict",
|
|
1324
|
+
`Zoom ID already exists: ${input.zoomId}`,
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
zoomIds.add(input.zoomId);
|
|
1328
|
+
zooms.push(manualProjectZoom(
|
|
1329
|
+
input,
|
|
1330
|
+
manualZoomBindingFor(manualZoomBindings, input.zoomId),
|
|
1331
|
+
));
|
|
1332
|
+
}
|
|
1333
|
+
break;
|
|
1334
|
+
case "add-overlays":
|
|
1335
|
+
for (const overlay of edit.overlays) {
|
|
1336
|
+
if (overlayIds.has(overlay.overlayId)) {
|
|
1337
|
+
throw new ApplicationError(
|
|
1338
|
+
"conflict",
|
|
1339
|
+
`Overlay ID already exists: ${overlay.overlayId}`,
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
overlayIds.add(overlay.overlayId);
|
|
1343
|
+
overlays.push(overlay);
|
|
1344
|
+
}
|
|
1345
|
+
break;
|
|
1346
|
+
case "add-zooms":
|
|
1347
|
+
for (const zoom of edit.zooms) {
|
|
1348
|
+
if (zoomIds.has(zoom.operation.zoomId)) {
|
|
1349
|
+
throw new ApplicationError(
|
|
1350
|
+
"conflict",
|
|
1351
|
+
`Zoom ID already exists: ${zoom.operation.zoomId}`,
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
zoomIds.add(zoom.operation.zoomId);
|
|
1355
|
+
zooms.push(zoom);
|
|
1356
|
+
}
|
|
1357
|
+
break;
|
|
1358
|
+
case "cut":
|
|
1359
|
+
case "remove-camera-moves":
|
|
1360
|
+
case "remove-overlays":
|
|
1361
|
+
case "remove-zooms":
|
|
1362
|
+
case "set-metadata-effects":
|
|
1363
|
+
case "speed":
|
|
1364
|
+
case "trim":
|
|
1365
|
+
throw new ApplicationError(
|
|
1366
|
+
"internal",
|
|
1367
|
+
"An additive project edit phase contained a non-additive edit.",
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
...plan,
|
|
1373
|
+
cameraMoves: [...plan.cameraMoves, ...cameraMoves],
|
|
1374
|
+
overlays: [...plan.overlays, ...overlays],
|
|
1375
|
+
updatedAt,
|
|
1376
|
+
zooms: [...plan.zooms, ...zooms],
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
case "remove": {
|
|
1380
|
+
const cameraMoveIds = new Set(
|
|
1381
|
+
plan.cameraMoves.map(move => move.cameraMoveId),
|
|
1382
|
+
);
|
|
1383
|
+
const overlayIds = new Set(
|
|
1384
|
+
plan.overlays.map(overlay => overlay.overlayId),
|
|
1385
|
+
);
|
|
1386
|
+
const zoomIds = new Set(
|
|
1387
|
+
plan.zooms.map(zoom => zoom.operation.zoomId),
|
|
1388
|
+
);
|
|
1389
|
+
for (const edit of edits) {
|
|
1390
|
+
throwIfAborted(abortSignal);
|
|
1391
|
+
switch (edit.kind) {
|
|
1392
|
+
case "remove-camera-moves":
|
|
1393
|
+
removeKnownProjectEditIds(
|
|
1394
|
+
cameraMoveIds,
|
|
1395
|
+
edit.cameraMoveIds,
|
|
1396
|
+
"camera move",
|
|
1397
|
+
);
|
|
1398
|
+
break;
|
|
1399
|
+
case "remove-overlays":
|
|
1400
|
+
removeKnownProjectEditIds(
|
|
1401
|
+
overlayIds,
|
|
1402
|
+
edit.overlayIds,
|
|
1403
|
+
"overlay",
|
|
1404
|
+
);
|
|
1405
|
+
break;
|
|
1406
|
+
case "remove-zooms":
|
|
1407
|
+
removeKnownProjectEditIds(
|
|
1408
|
+
zoomIds,
|
|
1409
|
+
edit.zoomIds,
|
|
1410
|
+
"zoom",
|
|
1411
|
+
);
|
|
1412
|
+
break;
|
|
1413
|
+
case "add-manual-camera-moves":
|
|
1414
|
+
case "add-manual-zooms":
|
|
1415
|
+
case "add-overlays":
|
|
1416
|
+
case "add-zooms":
|
|
1417
|
+
case "cut":
|
|
1418
|
+
case "set-metadata-effects":
|
|
1419
|
+
case "speed":
|
|
1420
|
+
case "trim":
|
|
1421
|
+
throw new ApplicationError(
|
|
1422
|
+
"internal",
|
|
1423
|
+
"A removal project edit phase contained a non-removal edit.",
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
...plan,
|
|
1429
|
+
cameraMoves: plan.cameraMoves.filter(
|
|
1430
|
+
move => cameraMoveIds.has(move.cameraMoveId),
|
|
1431
|
+
),
|
|
1432
|
+
overlays: plan.overlays.filter(
|
|
1433
|
+
overlay => overlayIds.has(overlay.overlayId),
|
|
1434
|
+
),
|
|
1435
|
+
updatedAt,
|
|
1436
|
+
zooms: plan.zooms.filter(
|
|
1437
|
+
zoom => zoomIds.has(zoom.operation.zoomId),
|
|
1438
|
+
),
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
case "metadata": {
|
|
1442
|
+
const edit = first.kind === "set-metadata-effects"
|
|
1443
|
+
? first
|
|
1444
|
+
: undefined;
|
|
1445
|
+
if (edit === undefined || edits.length !== 1) {
|
|
1446
|
+
throw new ApplicationError(
|
|
1447
|
+
"internal",
|
|
1448
|
+
"A metadata project edit phase must contain exactly one metadata edit.",
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
return await applyOrderedProjectEditV2(
|
|
1452
|
+
application,
|
|
1453
|
+
project,
|
|
1454
|
+
plan,
|
|
1455
|
+
edit,
|
|
1456
|
+
metadataBinding,
|
|
1457
|
+
updatedAt,
|
|
1458
|
+
false,
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
default: {
|
|
1462
|
+
const exhaustive: never = phase;
|
|
1463
|
+
return exhaustive;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
interface ProjectEditsTransactionInput<Edit> {
|
|
1469
|
+
readonly batch: {
|
|
1470
|
+
readonly ordered: readonly Edit[];
|
|
1471
|
+
};
|
|
1472
|
+
readonly basis: ProjectEditBasis;
|
|
1473
|
+
readonly project: string;
|
|
1474
|
+
readonly updatedAt?: string | undefined;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
interface CompleteProjectEditBindingsV3 {
|
|
1478
|
+
readonly manualZoomBindings: ReadonlyMap<
|
|
1479
|
+
string,
|
|
1480
|
+
ProjectCommitManualZoomBindingV3
|
|
1481
|
+
>;
|
|
1482
|
+
readonly metadataBinding: ProjectCommitMetadataBindingV2 | undefined;
|
|
1483
|
+
readonly metadataEdit: Extract<
|
|
1484
|
+
OrderedProjectEditV3,
|
|
1485
|
+
{ readonly kind: "set-metadata-effects" }
|
|
1486
|
+
> | undefined;
|
|
1487
|
+
readonly requiresExternalRevalidation: boolean;
|
|
1488
|
+
readonly zooms: readonly ManualProjectZoomInputV3[];
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function completeProjectEditBindingsV3(input: {
|
|
1492
|
+
readonly batch: CommitProjectEditsInputV3["batch"];
|
|
1493
|
+
readonly manualZoomBindings?: readonly ProjectCommitManualZoomBindingV3[];
|
|
1494
|
+
readonly metadataBinding?: ProjectCommitMetadataBindingV2;
|
|
1495
|
+
}): CompleteProjectEditBindingsV3 {
|
|
1496
|
+
const metadataEdit = input.batch.ordered.find(
|
|
1497
|
+
(
|
|
1498
|
+
candidate,
|
|
1499
|
+
): candidate is Extract<
|
|
1500
|
+
OrderedProjectEditV3,
|
|
1501
|
+
{ readonly kind: "set-metadata-effects" }
|
|
1502
|
+
> => candidate.kind === "set-metadata-effects",
|
|
1503
|
+
);
|
|
1504
|
+
const requiresMetadataBinding = metadataEdit !== undefined
|
|
1505
|
+
&& metadataEffectsAreEnabled(metadataEdit);
|
|
1506
|
+
if (requiresMetadataBinding && input.metadataBinding === undefined) {
|
|
1507
|
+
throw new ApplicationError(
|
|
1508
|
+
"incompatible",
|
|
1509
|
+
"Enabled metadata effects require a host-bound recording manifest.",
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
if (!requiresMetadataBinding && input.metadataBinding !== undefined) {
|
|
1513
|
+
throw new ApplicationError(
|
|
1514
|
+
"incompatible",
|
|
1515
|
+
"A metadata binding is valid only for enabled metadata effects.",
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
const zooms = manualZoomInputs(input.batch.ordered);
|
|
1519
|
+
const manualZoomBindings = indexManualZoomBindings(
|
|
1520
|
+
input.manualZoomBindings ?? [],
|
|
1521
|
+
);
|
|
1522
|
+
const expectedZoomIds = new Set(zooms.map(zoom => zoom.zoomId));
|
|
1523
|
+
const actualZoomIds = new Set(manualZoomBindings.keys());
|
|
1524
|
+
if (
|
|
1525
|
+
expectedZoomIds.size !== actualZoomIds.size
|
|
1526
|
+
|| [...expectedZoomIds].some(zoomId => !actualZoomIds.has(zoomId))
|
|
1527
|
+
) {
|
|
1528
|
+
throw new ApplicationError(
|
|
1529
|
+
"incompatible",
|
|
1530
|
+
"Manual zooms require exact host-owned recording bindings.",
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
return {
|
|
1534
|
+
manualZoomBindings,
|
|
1535
|
+
metadataBinding: input.metadataBinding,
|
|
1536
|
+
metadataEdit,
|
|
1537
|
+
requiresExternalRevalidation: requiresMetadataBinding || zooms.length > 0,
|
|
1538
|
+
zooms,
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
async function deriveProjectEdits<Edit extends { readonly kind: string }>(
|
|
1543
|
+
input: {
|
|
1544
|
+
readonly abortSignal: AbortSignal;
|
|
1545
|
+
readonly edits: readonly Edit[];
|
|
1546
|
+
readonly plan: ProjectEditPlanV1;
|
|
1547
|
+
readonly project: VideoProjectV1;
|
|
1548
|
+
readonly updatedAt: string;
|
|
1549
|
+
},
|
|
1550
|
+
apply: (
|
|
1551
|
+
project: VideoProjectV1,
|
|
1552
|
+
plan: ProjectEditPlanV1,
|
|
1553
|
+
edit: Edit,
|
|
1554
|
+
updatedAt: string,
|
|
1555
|
+
) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
|
|
1556
|
+
normalizationPhase?: (edit: Edit) => string,
|
|
1557
|
+
applyPhase?: (
|
|
1558
|
+
project: VideoProjectV1,
|
|
1559
|
+
plan: ProjectEditPlanV1,
|
|
1560
|
+
edits: readonly Edit[],
|
|
1561
|
+
updatedAt: string,
|
|
1562
|
+
abortSignal: AbortSignal,
|
|
1563
|
+
) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
|
|
1564
|
+
): Promise<ProjectEditPlanV1> {
|
|
1565
|
+
let next = input.plan;
|
|
1566
|
+
if (normalizationPhase !== undefined && applyPhase !== undefined) {
|
|
1567
|
+
let index = 0;
|
|
1568
|
+
while (index < input.edits.length) {
|
|
1569
|
+
throwIfAborted(input.abortSignal);
|
|
1570
|
+
const phase = normalizationPhase(input.edits[index]!);
|
|
1571
|
+
let endIndex = index + 1;
|
|
1572
|
+
while (
|
|
1573
|
+
endIndex < input.edits.length
|
|
1574
|
+
&& normalizationPhase(input.edits[endIndex]!) === phase
|
|
1575
|
+
) {
|
|
1576
|
+
endIndex += 1;
|
|
1577
|
+
}
|
|
1578
|
+
next = await applyPhase(
|
|
1579
|
+
input.project,
|
|
1580
|
+
next,
|
|
1581
|
+
input.edits.slice(index, endIndex),
|
|
1582
|
+
input.updatedAt,
|
|
1583
|
+
input.abortSignal,
|
|
1584
|
+
);
|
|
1585
|
+
next = normalizeCompleteEditDraft(next);
|
|
1586
|
+
throwIfAborted(input.abortSignal);
|
|
1587
|
+
index = endIndex;
|
|
1588
|
+
}
|
|
1589
|
+
return next;
|
|
1590
|
+
}
|
|
1591
|
+
for (const edit of input.edits) {
|
|
1592
|
+
throwIfAborted(input.abortSignal);
|
|
1593
|
+
next = await apply(input.project, next, edit, input.updatedAt);
|
|
1594
|
+
}
|
|
1595
|
+
return next;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Derive a complete V3 edit batch from exact frozen documents and complete
|
|
1600
|
+
* host bindings without publishing the mutable current-plan pointer.
|
|
1601
|
+
*/
|
|
1602
|
+
export async function deriveProjectEditPlanV3(input: {
|
|
1603
|
+
readonly abortSignal: AbortSignal;
|
|
1604
|
+
readonly application: ApplicationContext;
|
|
1605
|
+
readonly batch: CommitProjectEditsInputV3["batch"];
|
|
1606
|
+
readonly manualZoomBindings?: readonly ProjectCommitManualZoomBindingV3[];
|
|
1607
|
+
readonly metadataBinding?: ProjectCommitMetadataBindingV2;
|
|
1608
|
+
readonly plan: ProjectEditPlanV1;
|
|
1609
|
+
readonly project: VideoProjectV1;
|
|
1610
|
+
readonly updatedAt: string;
|
|
1611
|
+
}): Promise<ProjectEditPlanV1> {
|
|
1612
|
+
const bindings = completeProjectEditBindingsV3(input);
|
|
1613
|
+
return await deriveProjectEdits(
|
|
1614
|
+
{
|
|
1615
|
+
abortSignal: input.abortSignal,
|
|
1616
|
+
edits: input.batch.ordered,
|
|
1617
|
+
plan: input.plan,
|
|
1618
|
+
project: input.project,
|
|
1619
|
+
updatedAt: input.updatedAt,
|
|
1620
|
+
},
|
|
1621
|
+
async (project, plan, edit, updatedAt) => (
|
|
1622
|
+
await applyOrderedProjectEditV3(
|
|
1623
|
+
input.application,
|
|
1624
|
+
project,
|
|
1625
|
+
plan,
|
|
1626
|
+
edit,
|
|
1627
|
+
bindings.metadataBinding,
|
|
1628
|
+
bindings.manualZoomBindings,
|
|
1629
|
+
updatedAt,
|
|
1630
|
+
false,
|
|
1631
|
+
)
|
|
1632
|
+
),
|
|
1633
|
+
projectEditNormalizationPhaseV3,
|
|
1634
|
+
async (project, plan, edits, updatedAt, abortSignal) => (
|
|
1635
|
+
await applyOrderedProjectEditPhaseV3(
|
|
1636
|
+
input.application,
|
|
1637
|
+
project,
|
|
1638
|
+
plan,
|
|
1639
|
+
edits,
|
|
1640
|
+
bindings.metadataBinding,
|
|
1641
|
+
bindings.manualZoomBindings,
|
|
1642
|
+
updatedAt,
|
|
1643
|
+
abortSignal,
|
|
1644
|
+
)
|
|
1645
|
+
),
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/** Reverify every external recording fact bound into a complete V3 batch. */
|
|
1650
|
+
export async function revalidateProjectEditBindingsV3(input: {
|
|
1651
|
+
readonly abortSignal: AbortSignal;
|
|
1652
|
+
readonly application: ApplicationContext;
|
|
1653
|
+
readonly batch: CommitProjectEditsInputV3["batch"];
|
|
1654
|
+
readonly manualZoomBindings?: readonly ProjectCommitManualZoomBindingV3[];
|
|
1655
|
+
readonly metadataBinding?: ProjectCommitMetadataBindingV2;
|
|
1656
|
+
readonly project: VideoProjectV1;
|
|
1657
|
+
}): Promise<void> {
|
|
1658
|
+
const bindings = completeProjectEditBindingsV3(input);
|
|
1659
|
+
if (bindings.metadataEdit !== undefined && bindings.metadataBinding !== undefined) {
|
|
1660
|
+
await assertMetadataEffectsAvailable(
|
|
1661
|
+
input.application,
|
|
1662
|
+
input.project,
|
|
1663
|
+
bindings.metadataEdit,
|
|
1664
|
+
bindings.metadataBinding,
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
const resolution = manualZoomResolutionContext(input.project);
|
|
1668
|
+
for (const zoom of bindings.zooms) {
|
|
1669
|
+
throwIfAborted(input.abortSignal);
|
|
1670
|
+
const expected = manualZoomBindingFor(
|
|
1671
|
+
bindings.manualZoomBindings,
|
|
1672
|
+
zoom.zoomId,
|
|
1673
|
+
);
|
|
1674
|
+
await resolveManualZoomBinding(
|
|
1675
|
+
input.application,
|
|
1676
|
+
input.project,
|
|
1677
|
+
resolution,
|
|
1678
|
+
zoom,
|
|
1679
|
+
expected.placementId,
|
|
1680
|
+
expected,
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function executeProjectEditsTransaction<
|
|
1686
|
+
Edit extends { readonly kind: string },
|
|
1687
|
+
>(
|
|
1688
|
+
context: OperationExecutionContext,
|
|
1689
|
+
input: ProjectEditsTransactionInput<Edit>,
|
|
1690
|
+
apply: (
|
|
1691
|
+
project: VideoProjectV1,
|
|
1692
|
+
plan: ProjectEditPlanV1,
|
|
1693
|
+
edit: Edit,
|
|
1694
|
+
updatedAt: string,
|
|
1695
|
+
) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
|
|
1696
|
+
revalidateBeforePublication?: (
|
|
1697
|
+
project: VideoProjectV1,
|
|
1698
|
+
) => Promise<void>,
|
|
1699
|
+
normalizationPhase?: (edit: Edit) => string,
|
|
1700
|
+
applyPhase?: (
|
|
1701
|
+
project: VideoProjectV1,
|
|
1702
|
+
plan: ProjectEditPlanV1,
|
|
1703
|
+
edits: readonly Edit[],
|
|
1704
|
+
updatedAt: string,
|
|
1705
|
+
abortSignal: AbortSignal,
|
|
1706
|
+
) => ProjectEditPlanV1 | Promise<ProjectEditPlanV1>,
|
|
1707
|
+
): Promise<CommitProjectEditsOutput> {
|
|
1708
|
+
throwIfAborted(context.abortSignal);
|
|
1709
|
+
const snapshot = await openProjectSnapshot(
|
|
1710
|
+
context.application.paths.projectRoot,
|
|
1711
|
+
input.project,
|
|
1712
|
+
);
|
|
1713
|
+
assertProjectEditBasis(input.basis, snapshot);
|
|
1714
|
+
if (
|
|
1715
|
+
snapshot.plan.projectStructureSha256
|
|
1716
|
+
!== hashProjectStructure(snapshot.project)
|
|
1717
|
+
) {
|
|
1718
|
+
throw new ApplicationError(
|
|
1719
|
+
"conflict",
|
|
1720
|
+
"The current edit plan belongs to an earlier project structure.",
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
const updatedAt = input.updatedAt ?? IsoTimestampSchema.parse(
|
|
1724
|
+
context.application.clock.now().toISOString(),
|
|
1725
|
+
);
|
|
1726
|
+
const next = await deriveProjectEdits({
|
|
1727
|
+
abortSignal: context.abortSignal,
|
|
1728
|
+
edits: input.batch.ordered,
|
|
1729
|
+
plan: snapshot.plan,
|
|
1730
|
+
project: snapshot.project,
|
|
1731
|
+
updatedAt,
|
|
1732
|
+
}, apply, normalizationPhase, applyPhase);
|
|
1733
|
+
throwIfAborted(context.abortSignal);
|
|
1734
|
+
|
|
1735
|
+
// The scheduler owns the outer physical project lease. The transaction
|
|
1736
|
+
// marker makes process loss before/after the atomic current-plan
|
|
1737
|
+
// replacement explicitly recoverable on resume.
|
|
1738
|
+
// Reject already-stale external evidence before the workflow marks
|
|
1739
|
+
// publication as potentially authoritative. This avoids turning an
|
|
1740
|
+
// ordinary stale input into a reconciliation loop.
|
|
1741
|
+
await revalidateBeforePublication?.(snapshot.project);
|
|
1742
|
+
throwIfAborted(context.abortSignal);
|
|
1743
|
+
await context.workflow?.beforePublication();
|
|
1744
|
+
throwIfAborted(context.abortSignal);
|
|
1745
|
+
// External recording evidence can change while the scheduler fence is
|
|
1746
|
+
// checked. Reopen it after that awaited gate, then reassert the durable
|
|
1747
|
+
// workflow fence so cancellation/fence ownership is the final external
|
|
1748
|
+
// check before the project transaction publishes. Stopped recording
|
|
1749
|
+
// evidence is immutable by contract; the exact hash check still rejects
|
|
1750
|
+
// observable drift injected during the first gate.
|
|
1751
|
+
if (revalidateBeforePublication !== undefined) {
|
|
1752
|
+
await revalidateBeforePublication(snapshot.project);
|
|
1753
|
+
throwIfAborted(context.abortSignal);
|
|
1754
|
+
await context.workflow?.beforePublication();
|
|
1755
|
+
throwIfAborted(context.abortSignal);
|
|
1756
|
+
}
|
|
1757
|
+
const persistedProject = canonicalSlopcameraPersistenceDocument(snapshot.project);
|
|
1758
|
+
const persistedPriorPlan = canonicalSlopcameraPersistenceDocument(snapshot.plan);
|
|
1759
|
+
const persistedNextPlan = canonicalSlopcameraPersistenceDocument(next);
|
|
1760
|
+
const nodePlanSha256 = context.workflow?.nodePlanSha256
|
|
1761
|
+
?? canonicalJsonSha256({
|
|
1762
|
+
generation: snapshot.generation,
|
|
1763
|
+
input,
|
|
1764
|
+
nextPlanSha256: hashProjectEditPlan(next),
|
|
1765
|
+
});
|
|
1766
|
+
await commitProjectStateTransaction({
|
|
1767
|
+
after: { plan: persistedNextPlan, project: persistedProject },
|
|
1768
|
+
before: { plan: persistedPriorPlan, project: persistedProject },
|
|
1769
|
+
fileSystem: snapshot.openProject.fileSystem,
|
|
1770
|
+
transactionId: projectEditTransactionId(nodePlanSha256),
|
|
1771
|
+
});
|
|
1772
|
+
return projectEditCommitReceipt(input, persistedProject, persistedNextPlan);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function normalizeCompleteEditDraft(
|
|
1776
|
+
plan: ProjectEditPlanV1,
|
|
1777
|
+
): ProjectEditPlanV1 {
|
|
1778
|
+
try {
|
|
1779
|
+
return normalizeProjectEditPlan(plan);
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
if (error instanceof ApplicationError) throw error;
|
|
1782
|
+
throw new ApplicationError(
|
|
1783
|
+
"conflict",
|
|
1784
|
+
"Complete edits are incompatible with the current project edit plan.",
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
export const commitProjectEditsOperationDefinition = {
|
|
1790
|
+
inputSchema: CommitProjectEditsInputSchema,
|
|
1791
|
+
inputSchemaId: "slopcamera.operation.project.commit-edits.input/v1",
|
|
1792
|
+
kind: "project.commit-edits",
|
|
1793
|
+
lifecycle: {
|
|
1794
|
+
kind: "project-transaction",
|
|
1795
|
+
execute: async (context, input) => await executeProjectEditsTransaction(
|
|
1796
|
+
context,
|
|
1797
|
+
input,
|
|
1798
|
+
applyOrderedProjectEdit,
|
|
1799
|
+
),
|
|
1800
|
+
},
|
|
1801
|
+
outputSchema: ProjectEditCommitReceiptSchema,
|
|
1802
|
+
outputSchemaId: "slopcamera.operation.project.commit-edits.output/v1",
|
|
1803
|
+
policy: {
|
|
1804
|
+
cache: "none",
|
|
1805
|
+
cancellable: true,
|
|
1806
|
+
effect: "project-mutation",
|
|
1807
|
+
maxDurationMs: 30_000,
|
|
1808
|
+
maxFanOut: 0,
|
|
1809
|
+
maxInputBytes: 16 * 1024 * 1024,
|
|
1810
|
+
maxOutputBytes: 4_096,
|
|
1811
|
+
preparation: ["project-state"],
|
|
1812
|
+
resources: [
|
|
1813
|
+
{ amount: 1, resource: "cpu" },
|
|
1814
|
+
{ amount: 1, resource: "local-io" },
|
|
1815
|
+
{ amount: 1, resource: "project-publication" },
|
|
1816
|
+
],
|
|
1817
|
+
resume: "recoverable-transaction",
|
|
1818
|
+
},
|
|
1819
|
+
summarize: output => ({
|
|
1820
|
+
fields: {
|
|
1821
|
+
generationSha256: output.generation.generationSha256,
|
|
1822
|
+
operation: output.operation,
|
|
1823
|
+
planHash: output.planHash,
|
|
1824
|
+
planId: output.planId,
|
|
1825
|
+
projectId: output.projectId,
|
|
1826
|
+
},
|
|
1827
|
+
kind: "project.commit-edits",
|
|
1828
|
+
}),
|
|
1829
|
+
version: 1,
|
|
1830
|
+
} satisfies OperationDefinition<
|
|
1831
|
+
"project.commit-edits",
|
|
1832
|
+
CommitProjectEditsInput,
|
|
1833
|
+
CommitProjectEditsOutput
|
|
1834
|
+
>;
|
|
1835
|
+
|
|
1836
|
+
export const commitProjectEditsOperationDefinitionV2 = {
|
|
1837
|
+
inputSchema: CommitProjectEditsInputV2Schema,
|
|
1838
|
+
inputSchemaId: "slopcamera.operation.project.commit-edits.input/v2",
|
|
1839
|
+
kind: "project.commit-edits",
|
|
1840
|
+
lifecycle: {
|
|
1841
|
+
kind: "project-transaction",
|
|
1842
|
+
execute: async (context, input) => {
|
|
1843
|
+
const metadataEdit = input.batch.ordered.find(
|
|
1844
|
+
(
|
|
1845
|
+
candidate,
|
|
1846
|
+
): candidate is Extract<
|
|
1847
|
+
OrderedProjectEditV2,
|
|
1848
|
+
{ readonly kind: "set-metadata-effects" }
|
|
1849
|
+
> => candidate.kind === "set-metadata-effects",
|
|
1850
|
+
);
|
|
1851
|
+
const requiresMetadataBinding = metadataEdit !== undefined
|
|
1852
|
+
&& metadataEffectsAreEnabled(metadataEdit);
|
|
1853
|
+
if (requiresMetadataBinding && input.metadataBinding === undefined) {
|
|
1854
|
+
throw new ApplicationError(
|
|
1855
|
+
"incompatible",
|
|
1856
|
+
"Enabled metadata effects require a host-bound recording manifest.",
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
return await executeProjectEditsTransaction(
|
|
1860
|
+
context,
|
|
1861
|
+
input,
|
|
1862
|
+
async (project, plan, edit, updatedAt) => (
|
|
1863
|
+
await applyOrderedProjectEditV2(
|
|
1864
|
+
context.application,
|
|
1865
|
+
project,
|
|
1866
|
+
plan,
|
|
1867
|
+
edit,
|
|
1868
|
+
input.metadataBinding,
|
|
1869
|
+
updatedAt,
|
|
1870
|
+
)
|
|
1871
|
+
),
|
|
1872
|
+
requiresMetadataBinding
|
|
1873
|
+
? async (project) => {
|
|
1874
|
+
await assertMetadataEffectsAvailable(
|
|
1875
|
+
context.application,
|
|
1876
|
+
project,
|
|
1877
|
+
metadataEdit,
|
|
1878
|
+
input.metadataBinding,
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
: undefined,
|
|
1882
|
+
);
|
|
1883
|
+
},
|
|
1884
|
+
},
|
|
1885
|
+
outputSchema: ProjectEditCommitReceiptSchema,
|
|
1886
|
+
outputSchemaId: "slopcamera.operation.project.commit-edits.output/v2",
|
|
1887
|
+
policy: {
|
|
1888
|
+
cache: "none",
|
|
1889
|
+
cancellable: true,
|
|
1890
|
+
effect: "project-mutation",
|
|
1891
|
+
maxDurationMs: 30_000,
|
|
1892
|
+
maxFanOut: 0,
|
|
1893
|
+
maxInputBytes: 16 * 1024 * 1024,
|
|
1894
|
+
maxOutputBytes: 4_096,
|
|
1895
|
+
preparation: ["project-state", "recording-metadata"],
|
|
1896
|
+
resources: [
|
|
1897
|
+
{ amount: 1, resource: "cpu" },
|
|
1898
|
+
{ amount: 1, resource: "local-io" },
|
|
1899
|
+
{ amount: 1, resource: "project-publication" },
|
|
1900
|
+
],
|
|
1901
|
+
resume: "recoverable-transaction",
|
|
1902
|
+
},
|
|
1903
|
+
summarize: output => ({
|
|
1904
|
+
fields: {
|
|
1905
|
+
generationSha256: output.generation.generationSha256,
|
|
1906
|
+
operation: output.operation,
|
|
1907
|
+
planHash: output.planHash,
|
|
1908
|
+
planId: output.planId,
|
|
1909
|
+
projectId: output.projectId,
|
|
1910
|
+
},
|
|
1911
|
+
kind: "project.commit-edits",
|
|
1912
|
+
}),
|
|
1913
|
+
version: 2,
|
|
1914
|
+
} satisfies OperationDefinition<
|
|
1915
|
+
"project.commit-edits",
|
|
1916
|
+
CommitProjectEditsInputV2,
|
|
1917
|
+
CommitProjectEditsOutput
|
|
1918
|
+
>;
|
|
1919
|
+
|
|
1920
|
+
export const commitProjectEditsOperationDefinitionV3 = {
|
|
1921
|
+
inputSchema: CommitProjectEditsInputV3Schema,
|
|
1922
|
+
inputSchemaId: "slopcamera.operation.project.commit-edits.input/v3",
|
|
1923
|
+
kind: "project.commit-edits",
|
|
1924
|
+
lifecycle: {
|
|
1925
|
+
kind: "project-transaction",
|
|
1926
|
+
execute: async (context, input) => {
|
|
1927
|
+
const metadataEdit = input.batch.ordered.find(
|
|
1928
|
+
(
|
|
1929
|
+
candidate,
|
|
1930
|
+
): candidate is Extract<
|
|
1931
|
+
OrderedProjectEditV3,
|
|
1932
|
+
{ readonly kind: "set-metadata-effects" }
|
|
1933
|
+
> => candidate.kind === "set-metadata-effects",
|
|
1934
|
+
);
|
|
1935
|
+
const requiresMetadataBinding = metadataEdit !== undefined
|
|
1936
|
+
&& metadataEffectsAreEnabled(metadataEdit);
|
|
1937
|
+
if (requiresMetadataBinding && input.metadataBinding === undefined) {
|
|
1938
|
+
throw new ApplicationError(
|
|
1939
|
+
"incompatible",
|
|
1940
|
+
"Enabled metadata effects require a host-bound recording manifest.",
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
const zooms = manualZoomInputs(input.batch.ordered);
|
|
1944
|
+
const manualZoomBindings = indexManualZoomBindings(
|
|
1945
|
+
input.manualZoomBindings ?? [],
|
|
1946
|
+
);
|
|
1947
|
+
const expectedZoomIds = new Set(zooms.map(zoom => zoom.zoomId));
|
|
1948
|
+
const actualZoomIds = new Set(manualZoomBindings.keys());
|
|
1949
|
+
if (
|
|
1950
|
+
expectedZoomIds.size !== actualZoomIds.size
|
|
1951
|
+
|| [...expectedZoomIds].some(zoomId => !actualZoomIds.has(zoomId))
|
|
1952
|
+
) {
|
|
1953
|
+
throw new ApplicationError(
|
|
1954
|
+
"incompatible",
|
|
1955
|
+
"Manual zooms require exact host-owned recording bindings.",
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
return await executeProjectEditsTransaction(
|
|
1959
|
+
context,
|
|
1960
|
+
input,
|
|
1961
|
+
async (project, plan, edit, updatedAt) => (
|
|
1962
|
+
await applyOrderedProjectEditV3(
|
|
1963
|
+
context.application,
|
|
1964
|
+
project,
|
|
1965
|
+
plan,
|
|
1966
|
+
edit,
|
|
1967
|
+
input.metadataBinding,
|
|
1968
|
+
manualZoomBindings,
|
|
1969
|
+
updatedAt,
|
|
1970
|
+
false,
|
|
1971
|
+
)
|
|
1972
|
+
),
|
|
1973
|
+
requiresMetadataBinding || zooms.length > 0
|
|
1974
|
+
? async (project) => {
|
|
1975
|
+
if (requiresMetadataBinding) {
|
|
1976
|
+
await assertMetadataEffectsAvailable(
|
|
1977
|
+
context.application,
|
|
1978
|
+
project,
|
|
1979
|
+
metadataEdit,
|
|
1980
|
+
input.metadataBinding,
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
const resolution = manualZoomResolutionContext(project);
|
|
1984
|
+
for (const zoom of zooms) {
|
|
1985
|
+
throwIfAborted(context.abortSignal);
|
|
1986
|
+
const expected = manualZoomBindingFor(
|
|
1987
|
+
manualZoomBindings,
|
|
1988
|
+
zoom.zoomId,
|
|
1989
|
+
);
|
|
1990
|
+
await resolveManualZoomBinding(
|
|
1991
|
+
context.application,
|
|
1992
|
+
project,
|
|
1993
|
+
resolution,
|
|
1994
|
+
zoom,
|
|
1995
|
+
expected.placementId,
|
|
1996
|
+
expected,
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
: undefined,
|
|
2001
|
+
projectEditNormalizationPhaseV3,
|
|
2002
|
+
async (
|
|
2003
|
+
project,
|
|
2004
|
+
plan,
|
|
2005
|
+
edits,
|
|
2006
|
+
updatedAt,
|
|
2007
|
+
abortSignal,
|
|
2008
|
+
) => await applyOrderedProjectEditPhaseV3(
|
|
2009
|
+
context.application,
|
|
2010
|
+
project,
|
|
2011
|
+
plan,
|
|
2012
|
+
edits,
|
|
2013
|
+
input.metadataBinding,
|
|
2014
|
+
manualZoomBindings,
|
|
2015
|
+
updatedAt,
|
|
2016
|
+
abortSignal,
|
|
2017
|
+
),
|
|
2018
|
+
);
|
|
2019
|
+
},
|
|
2020
|
+
},
|
|
2021
|
+
outputSchema: ProjectEditCommitReceiptSchema,
|
|
2022
|
+
outputSchemaId: "slopcamera.operation.project.commit-edits.output/v3",
|
|
2023
|
+
policy: {
|
|
2024
|
+
cache: "none",
|
|
2025
|
+
cancellable: true,
|
|
2026
|
+
effect: "project-mutation",
|
|
2027
|
+
maxDurationMs: 90_000,
|
|
2028
|
+
maxFanOut: 0,
|
|
2029
|
+
maxInputBytes: 16 * 1024 * 1024,
|
|
2030
|
+
maxOutputBytes: 4_096,
|
|
2031
|
+
preparation: ["project-state", "recording-metadata"],
|
|
2032
|
+
resources: [
|
|
2033
|
+
{ amount: 1, resource: "cpu" },
|
|
2034
|
+
{ amount: 1, resource: "local-io" },
|
|
2035
|
+
{ amount: 1, resource: "project-publication" },
|
|
2036
|
+
],
|
|
2037
|
+
resume: "recoverable-transaction",
|
|
2038
|
+
},
|
|
2039
|
+
summarize: output => ({
|
|
2040
|
+
fields: {
|
|
2041
|
+
generationSha256: output.generation.generationSha256,
|
|
2042
|
+
operation: output.operation,
|
|
2043
|
+
planHash: output.planHash,
|
|
2044
|
+
planId: output.planId,
|
|
2045
|
+
projectId: output.projectId,
|
|
2046
|
+
},
|
|
2047
|
+
kind: "project.commit-edits",
|
|
2048
|
+
}),
|
|
2049
|
+
version: 3,
|
|
2050
|
+
} satisfies OperationDefinition<
|
|
2051
|
+
"project.commit-edits",
|
|
2052
|
+
CommitProjectEditsInputV3,
|
|
2053
|
+
CommitProjectEditsOutput
|
|
2054
|
+
>;
|