@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,940 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, open, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AnalysisIdSchema,
|
|
7
|
+
AnalysisToolSchema,
|
|
8
|
+
FaceAnalysisConfigSchema,
|
|
9
|
+
FaceAnalysisV1Schema,
|
|
10
|
+
ProjectAnalysisReferenceSchema,
|
|
11
|
+
VideoProjectV1Schema,
|
|
12
|
+
type FaceAnalysisConfig,
|
|
13
|
+
type FaceAnalysisV1,
|
|
14
|
+
type FaceTrackId,
|
|
15
|
+
type NormalizedTopLeftRect,
|
|
16
|
+
type ProjectAssetV1,
|
|
17
|
+
type RawFaceDetectionFrame,
|
|
18
|
+
type VideoProjectV1,
|
|
19
|
+
} from "../contracts";
|
|
20
|
+
import {
|
|
21
|
+
associateFaceDetections,
|
|
22
|
+
canonicalSlopcameraPersistenceDocument,
|
|
23
|
+
canonicalJson,
|
|
24
|
+
canonicalJsonSha256,
|
|
25
|
+
saveAnalysisArtifact,
|
|
26
|
+
saveVideoProject,
|
|
27
|
+
sha256Hex,
|
|
28
|
+
} from "../core";
|
|
29
|
+
import {
|
|
30
|
+
MAXIMUM_FACE_ANALYZER_FRAMES,
|
|
31
|
+
MAXIMUM_FACE_ANALYZER_OUTPUT_BYTES,
|
|
32
|
+
parseFaceAnalyzerJsonLines,
|
|
33
|
+
type FaceAnalyzerBackend,
|
|
34
|
+
type FaceAnalyzerEvent,
|
|
35
|
+
type FaceAnalyzerFrameEvent,
|
|
36
|
+
} from "../analysis/protocol";
|
|
37
|
+
import { CliError } from "./errors";
|
|
38
|
+
import type { ProcessRunner } from "./io";
|
|
39
|
+
import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
|
|
40
|
+
import { resolveVerifiedProjectMedia } from "./project-media-integrity";
|
|
41
|
+
import { projectAnalysisPath, type OpenProject } from "./project-service";
|
|
42
|
+
import {
|
|
43
|
+
resolveVideoAnalysisSubject,
|
|
44
|
+
type ResolvedVideoAnalysisSubject,
|
|
45
|
+
} from "./scene-analysis-service";
|
|
46
|
+
|
|
47
|
+
type VideoStream = Extract<ProjectAssetV1["streams"][number], { readonly kind: "video" }>;
|
|
48
|
+
type VideoSegment = VideoStream["segments"][number];
|
|
49
|
+
export type FaceAnalysisReference = Extract<
|
|
50
|
+
VideoProjectV1["analyses"][number],
|
|
51
|
+
{ readonly kind: "faces" }
|
|
52
|
+
>;
|
|
53
|
+
type StartedEvent = Extract<FaceAnalyzerEvent, { readonly event: "started" }>;
|
|
54
|
+
type FrameEvent = FaceAnalyzerFrameEvent;
|
|
55
|
+
type CompletedEvent = Extract<FaceAnalyzerEvent, { readonly event: "completed" }>;
|
|
56
|
+
|
|
57
|
+
const MAXIMUM_FFPROBE_OUTPUT_BYTES = 1_048_576;
|
|
58
|
+
const MAXIMUM_ANALYSIS_FRAMES = 250_000;
|
|
59
|
+
const MAXIMUM_TRACK_LIST_LIMIT = 1_000;
|
|
60
|
+
|
|
61
|
+
export const DEFAULT_FACE_ANALYSIS_CONFIG = FaceAnalysisConfigSchema.parse({
|
|
62
|
+
sampleIntervalUs: 125_000,
|
|
63
|
+
tracking: {
|
|
64
|
+
iouWeight: 0.7,
|
|
65
|
+
maximumCenterDistance: 0.3,
|
|
66
|
+
maximumFacesPerFrame: 32,
|
|
67
|
+
maximumGapUs: 500_000,
|
|
68
|
+
minimumConfidence: 0.6,
|
|
69
|
+
minimumIou: 0.05,
|
|
70
|
+
},
|
|
71
|
+
}) satisfies FaceAnalysisConfig;
|
|
72
|
+
|
|
73
|
+
function isMissingFile(error: unknown): boolean {
|
|
74
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function requireUnusedAnalysisPath(project: OpenProject, path: string): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
await project.fileSystem.readText(path);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (isMissingFile(error)) return;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
throw new CliError("conflict", `Analysis artifact already exists and will not be overwritten: ${path}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseFfprobeVideoStreams(output: string): readonly number[] {
|
|
88
|
+
let input: unknown;
|
|
89
|
+
try {
|
|
90
|
+
input = JSON.parse(output) as unknown;
|
|
91
|
+
} catch {
|
|
92
|
+
throw new CliError("invalid-data", "FFprobe returned invalid JSON while resolving a face-analysis video track.");
|
|
93
|
+
}
|
|
94
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
95
|
+
throw new CliError("invalid-data", "FFprobe face-analysis output must be an object.");
|
|
96
|
+
}
|
|
97
|
+
const streams = (input as Readonly<Record<string, unknown>>).streams;
|
|
98
|
+
if (!Array.isArray(streams)) {
|
|
99
|
+
throw new CliError("invalid-data", "FFprobe face-analysis output omits its stream list.");
|
|
100
|
+
}
|
|
101
|
+
const indexes = new Set<number>();
|
|
102
|
+
const videos: number[] = [];
|
|
103
|
+
for (const value of streams) {
|
|
104
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
105
|
+
throw new CliError("invalid-data", "FFprobe face-analysis stream entry must be an object.");
|
|
106
|
+
}
|
|
107
|
+
const stream = value as Readonly<Record<string, unknown>>;
|
|
108
|
+
if (!Number.isSafeInteger(stream.index) || (stream.index as number) < 0) {
|
|
109
|
+
throw new CliError("invalid-data", "FFprobe face-analysis stream index is invalid.");
|
|
110
|
+
}
|
|
111
|
+
const index = stream.index as number;
|
|
112
|
+
if (indexes.has(index)) {
|
|
113
|
+
throw new CliError("invalid-data", `FFprobe repeated stream index ${index}.`);
|
|
114
|
+
}
|
|
115
|
+
indexes.add(index);
|
|
116
|
+
if (stream.codec_type === "video") videos.push(index);
|
|
117
|
+
else if (!["attachment", "audio", "data", "subtitle"].includes(String(stream.codec_type))) {
|
|
118
|
+
throw new CliError("invalid-data", `FFprobe stream ${index} has an invalid codec type.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return videos.sort((left, right) => left - right);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apple AVFoundation selects a video track by its ordinal among video tracks,
|
|
126
|
+
* while project segments retain FFmpeg's absolute stream index.
|
|
127
|
+
*/
|
|
128
|
+
export async function probeFaceAnalyzerVideoTrackOrdinal(options: {
|
|
129
|
+
readonly absolutePath: string;
|
|
130
|
+
readonly ffprobe: string;
|
|
131
|
+
readonly runner: ProcessRunner;
|
|
132
|
+
readonly streamIndex: number;
|
|
133
|
+
}): Promise<{ readonly ordinal: number; readonly totalVideoTracks: number }> {
|
|
134
|
+
const result = await options.runner.run([
|
|
135
|
+
options.ffprobe,
|
|
136
|
+
"-v", "error",
|
|
137
|
+
"-show_entries", "stream=index,codec_type",
|
|
138
|
+
"-of", "json",
|
|
139
|
+
options.absolutePath,
|
|
140
|
+
], { maxOutputBytes: MAXIMUM_FFPROBE_OUTPUT_BYTES });
|
|
141
|
+
if (result.exitCode !== 0) {
|
|
142
|
+
throw new CliError(
|
|
143
|
+
"subprocess",
|
|
144
|
+
`FFprobe could not resolve the face-analysis video track: ${
|
|
145
|
+
result.stderr.trim().slice(-4_000) || `exit ${result.exitCode}`
|
|
146
|
+
}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const videoStreams = parseFfprobeVideoStreams(result.stdout);
|
|
150
|
+
const ordinal = videoStreams.indexOf(options.streamIndex);
|
|
151
|
+
if (ordinal < 0) {
|
|
152
|
+
throw new CliError(
|
|
153
|
+
"invalid-data",
|
|
154
|
+
`Project stream index ${options.streamIndex} is not a video stream in ${options.absolutePath}.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return { ordinal, totalVideoTracks: videoStreams.length };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function coordinateSpace(started: StartedEvent): FaceAnalysisV1["coordinateSpace"] {
|
|
161
|
+
return {
|
|
162
|
+
encodedPixelHeight: started.orientation.encodedPixelHeight,
|
|
163
|
+
encodedPixelWidth: started.orientation.encodedPixelWidth,
|
|
164
|
+
mirroredHorizontally: started.orientation.mirroredHorizontally,
|
|
165
|
+
origin: "top-left",
|
|
166
|
+
pixelHeight: started.orientation.pixelHeight,
|
|
167
|
+
pixelWidth: started.orientation.pixelWidth,
|
|
168
|
+
rotationDegrees: started.orientation.rotationDegrees,
|
|
169
|
+
sampleAspectRatio: started.orientation.sampleAspectRatio,
|
|
170
|
+
units: "normalized",
|
|
171
|
+
xAxis: "right",
|
|
172
|
+
yAxis: "down",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function helperFailure(events: readonly FaceAnalyzerEvent[]): Extract<
|
|
177
|
+
FaceAnalyzerEvent,
|
|
178
|
+
{ readonly event: "error" }
|
|
179
|
+
> | undefined {
|
|
180
|
+
return events.find((event): event is Extract<FaceAnalyzerEvent, { readonly event: "error" }> =>
|
|
181
|
+
event.event === "error");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface ParsedFaceAnalyzerRun {
|
|
185
|
+
readonly completed: CompletedEvent;
|
|
186
|
+
readonly frames: readonly FrameEvent[];
|
|
187
|
+
readonly started: StartedEvent;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Accept only a complete helper transcript. In particular, a started/frame/error
|
|
192
|
+
* transcript never escapes as reusable partial evidence.
|
|
193
|
+
*/
|
|
194
|
+
export function parseCompletedFaceAnalyzerRun(
|
|
195
|
+
output: string,
|
|
196
|
+
expected: {
|
|
197
|
+
readonly endUs: number;
|
|
198
|
+
readonly maximumFacesPerFrame: number;
|
|
199
|
+
readonly maximumFrames: number;
|
|
200
|
+
readonly minimumConfidence: number;
|
|
201
|
+
readonly sampleIntervalUs: number;
|
|
202
|
+
readonly startUs: number;
|
|
203
|
+
readonly totalVideoTracks: number;
|
|
204
|
+
readonly videoTrackOrdinal: number;
|
|
205
|
+
},
|
|
206
|
+
): ParsedFaceAnalyzerRun {
|
|
207
|
+
let events: readonly FaceAnalyzerEvent[];
|
|
208
|
+
try {
|
|
209
|
+
events = parseFaceAnalyzerJsonLines(output);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new CliError(
|
|
212
|
+
"invalid-data",
|
|
213
|
+
`Face analyzer returned an invalid bounded JSONL transcript: ${
|
|
214
|
+
error instanceof Error ? error.message : String(error)
|
|
215
|
+
}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const failure = helperFailure(events);
|
|
219
|
+
if (failure !== undefined) {
|
|
220
|
+
throw new CliError(
|
|
221
|
+
"subprocess",
|
|
222
|
+
`Face analyzer failed (${failure.code}): ${failure.message}`,
|
|
223
|
+
{ analyzerCode: failure.code },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const first = events[0];
|
|
227
|
+
const last = events.at(-1);
|
|
228
|
+
if (first?.event !== "started" || last?.event !== "completed") {
|
|
229
|
+
throw new CliError(
|
|
230
|
+
"invalid-data",
|
|
231
|
+
"Face analyzer transcript must contain exactly started, frame*, completed.",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const middle = events.slice(1, -1);
|
|
235
|
+
if (middle.some(event => event.event !== "frame")) {
|
|
236
|
+
throw new CliError(
|
|
237
|
+
"invalid-data",
|
|
238
|
+
"Face analyzer transcript contains an event outside started, frame*, completed.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const frames = middle as readonly FrameEvent[];
|
|
242
|
+
const started = first;
|
|
243
|
+
const completed = last;
|
|
244
|
+
if (
|
|
245
|
+
started.limits.endUs !== expected.endUs
|
|
246
|
+
|| started.limits.maximumFacesPerFrame !== expected.maximumFacesPerFrame
|
|
247
|
+
|| started.limits.maximumFrames !== expected.maximumFrames
|
|
248
|
+
|| started.limits.maximumOutputBytes !== MAXIMUM_FACE_ANALYZER_OUTPUT_BYTES
|
|
249
|
+
|| started.limits.minimumConfidence !== expected.minimumConfidence
|
|
250
|
+
|| started.limits.sampleIntervalUs !== expected.sampleIntervalUs
|
|
251
|
+
|| started.limits.startUs !== expected.startUs
|
|
252
|
+
|| started.track.totalVideoTracks !== expected.totalVideoTracks
|
|
253
|
+
|| started.track.videoTrackOrdinal !== expected.videoTrackOrdinal
|
|
254
|
+
) {
|
|
255
|
+
throw new CliError("invalid-data", "Face analyzer did not honor its requested bounded invocation.");
|
|
256
|
+
}
|
|
257
|
+
let previousPtsUs = -1;
|
|
258
|
+
let faceDetections = 0;
|
|
259
|
+
for (const [index, frame] of frames.entries()) {
|
|
260
|
+
if (
|
|
261
|
+
frame.sampleIndex !== index
|
|
262
|
+
|| frame.ptsUs <= previousPtsUs
|
|
263
|
+
|| frame.ptsUs < expected.startUs
|
|
264
|
+
|| frame.ptsUs >= expected.endUs
|
|
265
|
+
) {
|
|
266
|
+
throw new CliError("invalid-data", "Face analyzer returned unordered or out-of-range frame evidence.");
|
|
267
|
+
}
|
|
268
|
+
if (frame.faces.some((face, faceIndex) => face.detectionIndex !== faceIndex)) {
|
|
269
|
+
throw new CliError("invalid-data", "Face analyzer detection indexes must be contiguous within a frame.");
|
|
270
|
+
}
|
|
271
|
+
previousPtsUs = frame.ptsUs;
|
|
272
|
+
faceDetections += frame.faces.length;
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
completed.framesAnalyzed !== frames.length
|
|
276
|
+
|| completed.framesRead < completed.framesAnalyzed
|
|
277
|
+
|| completed.faceDetections !== faceDetections
|
|
278
|
+
|| completed.firstPtsUs !== (frames[0]?.ptsUs ?? null)
|
|
279
|
+
|| completed.lastPtsUs !== (frames.at(-1)?.ptsUs ?? null)
|
|
280
|
+
) {
|
|
281
|
+
throw new CliError("invalid-data", "Face analyzer completion counts do not match its frame evidence.");
|
|
282
|
+
}
|
|
283
|
+
return { completed, frames, started };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function requestedMaximumFrames(segment: VideoSegment, sampleIntervalUs: number): number {
|
|
287
|
+
const durationUs = segment.fileRange.endUs - segment.fileRange.startUs;
|
|
288
|
+
const maximumFrames = Math.ceil(durationUs / sampleIntervalUs) + 1;
|
|
289
|
+
if (maximumFrames > MAXIMUM_FACE_ANALYZER_FRAMES) {
|
|
290
|
+
throw new CliError(
|
|
291
|
+
"usage",
|
|
292
|
+
`Face analysis segment ${segment.path} requires more than ${
|
|
293
|
+
MAXIMUM_FACE_ANALYZER_FRAMES
|
|
294
|
+
} bounded samples; lower --sample-fps or use shorter media segments.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return maximumFrames;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function analyzeSegment(options: {
|
|
301
|
+
readonly config: FaceAnalysisConfig;
|
|
302
|
+
readonly faceAnalyzer: string;
|
|
303
|
+
readonly ffprobe: string;
|
|
304
|
+
readonly repositoryRoot: string;
|
|
305
|
+
readonly runner: ProcessRunner;
|
|
306
|
+
readonly segment: VideoSegment;
|
|
307
|
+
}): Promise<{
|
|
308
|
+
readonly backend: FaceAnalyzerBackend;
|
|
309
|
+
readonly coordinateSpace: FaceAnalysisV1["coordinateSpace"];
|
|
310
|
+
readonly frames: readonly RawFaceDetectionFrame[];
|
|
311
|
+
}> {
|
|
312
|
+
const label = `Face-analysis segment ${options.segment.path}:${options.segment.streamIndex}`;
|
|
313
|
+
const firstVerifiedPath = await resolveVerifiedProjectMedia({
|
|
314
|
+
expected: { bytes: options.segment.bytes, sha256: options.segment.sha256 },
|
|
315
|
+
label,
|
|
316
|
+
path: options.segment.path,
|
|
317
|
+
repositoryRoot: options.repositoryRoot,
|
|
318
|
+
});
|
|
319
|
+
const track = await probeFaceAnalyzerVideoTrackOrdinal({
|
|
320
|
+
absolutePath: firstVerifiedPath,
|
|
321
|
+
ffprobe: options.ffprobe,
|
|
322
|
+
runner: options.runner,
|
|
323
|
+
streamIndex: options.segment.streamIndex,
|
|
324
|
+
});
|
|
325
|
+
// FFprobe is a separate reader. Re-hash the physical file directly before
|
|
326
|
+
// entrusting it to the signed helper, instead of reusing prior verification.
|
|
327
|
+
const source = await resolveVerifiedProjectMedia({
|
|
328
|
+
expected: { bytes: options.segment.bytes, sha256: options.segment.sha256 },
|
|
329
|
+
label,
|
|
330
|
+
path: options.segment.path,
|
|
331
|
+
repositoryRoot: options.repositoryRoot,
|
|
332
|
+
});
|
|
333
|
+
if (source !== firstVerifiedPath) {
|
|
334
|
+
throw new CliError("conflict", "Face-analysis media path changed between track probing and helper execution.");
|
|
335
|
+
}
|
|
336
|
+
const maximumFrames = requestedMaximumFrames(options.segment, options.config.sampleIntervalUs);
|
|
337
|
+
const result = await options.runner.run([
|
|
338
|
+
options.faceAnalyzer,
|
|
339
|
+
"--input", source,
|
|
340
|
+
"--video-track-ordinal", String(track.ordinal),
|
|
341
|
+
"--start-us", String(options.segment.fileRange.startUs),
|
|
342
|
+
"--end-us", String(options.segment.fileRange.endUs),
|
|
343
|
+
"--sample-interval-us", String(options.config.sampleIntervalUs),
|
|
344
|
+
"--max-faces-per-frame", String(options.config.tracking.maximumFacesPerFrame),
|
|
345
|
+
"--max-frames", String(maximumFrames),
|
|
346
|
+
"--max-output-bytes", String(MAXIMUM_FACE_ANALYZER_OUTPUT_BYTES),
|
|
347
|
+
"--minimum-confidence", String(options.config.tracking.minimumConfidence),
|
|
348
|
+
], { maxOutputBytes: MAXIMUM_FACE_ANALYZER_OUTPUT_BYTES, stdin: "ignore" });
|
|
349
|
+
let parsed: ParsedFaceAnalyzerRun;
|
|
350
|
+
try {
|
|
351
|
+
parsed = parseCompletedFaceAnalyzerRun(result.stdout, {
|
|
352
|
+
endUs: options.segment.fileRange.endUs,
|
|
353
|
+
maximumFacesPerFrame: options.config.tracking.maximumFacesPerFrame,
|
|
354
|
+
maximumFrames,
|
|
355
|
+
minimumConfidence: options.config.tracking.minimumConfidence,
|
|
356
|
+
sampleIntervalUs: options.config.sampleIntervalUs,
|
|
357
|
+
startUs: options.segment.fileRange.startUs,
|
|
358
|
+
totalVideoTracks: track.totalVideoTracks,
|
|
359
|
+
videoTrackOrdinal: track.ordinal,
|
|
360
|
+
});
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error instanceof CliError && error.code === "subprocess") throw error;
|
|
363
|
+
if (result.exitCode !== 0) {
|
|
364
|
+
throw new CliError(
|
|
365
|
+
"subprocess",
|
|
366
|
+
`Face analyzer exited ${result.exitCode}: ${
|
|
367
|
+
result.stderr.trim().slice(-4_000) || (error instanceof Error ? error.message : String(error))
|
|
368
|
+
}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
if (result.exitCode !== 0) {
|
|
374
|
+
throw new CliError(
|
|
375
|
+
"subprocess",
|
|
376
|
+
`Face analyzer exited ${result.exitCode} after emitting an otherwise complete transcript.`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
backend: parsed.started.backend,
|
|
381
|
+
coordinateSpace: coordinateSpace(parsed.started),
|
|
382
|
+
frames: parsed.frames.map(frame => {
|
|
383
|
+
const assetTimeUs = options.segment.assetRange.startUs
|
|
384
|
+
+ frame.ptsUs
|
|
385
|
+
- options.segment.fileRange.startUs;
|
|
386
|
+
if (
|
|
387
|
+
!Number.isSafeInteger(assetTimeUs)
|
|
388
|
+
|| assetTimeUs < options.segment.assetRange.startUs
|
|
389
|
+
|| assetTimeUs >= options.segment.assetRange.endUs
|
|
390
|
+
) {
|
|
391
|
+
throw new CliError("invalid-data", "Face analyzer frame PTS does not map inside its project asset segment.");
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
assetTimeUs,
|
|
395
|
+
detections: frame.faces.map(face => ({
|
|
396
|
+
confidence: face.confidence,
|
|
397
|
+
rect: face.bounds,
|
|
398
|
+
})),
|
|
399
|
+
};
|
|
400
|
+
}),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function combineFrames(runs: readonly (readonly RawFaceDetectionFrame[])[]): readonly RawFaceDetectionFrame[] {
|
|
405
|
+
const ordered = runs.flat().sort((left, right) => left.assetTimeUs - right.assetTimeUs);
|
|
406
|
+
const deduplicated: RawFaceDetectionFrame[] = [];
|
|
407
|
+
for (const frame of ordered) {
|
|
408
|
+
const prior = deduplicated.at(-1);
|
|
409
|
+
if (prior?.assetTimeUs !== frame.assetTimeUs) {
|
|
410
|
+
deduplicated.push(frame);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (canonicalJson(prior) !== canonicalJson(frame)) {
|
|
414
|
+
throw new CliError(
|
|
415
|
+
"invalid-data",
|
|
416
|
+
`Face analyzer returned conflicting evidence at asset time ${frame.assetTimeUs}.`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (deduplicated.length === 0) {
|
|
421
|
+
throw new CliError("invalid-data", "Face analyzer did not emit any sampled video frames.");
|
|
422
|
+
}
|
|
423
|
+
if (deduplicated.length > MAXIMUM_ANALYSIS_FRAMES) {
|
|
424
|
+
throw new CliError(
|
|
425
|
+
"invalid-data",
|
|
426
|
+
`Face analysis exceeds the ${MAXIMUM_ANALYSIS_FRAMES}-frame immutable evidence limit.`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
return deduplicated;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function sameCanonicalValue(left: unknown, right: unknown): boolean {
|
|
433
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface AnalyzeProjectFacesOptions {
|
|
437
|
+
readonly analysisId?: string;
|
|
438
|
+
readonly config?: FaceAnalysisConfig;
|
|
439
|
+
readonly faceAnalyzer: string;
|
|
440
|
+
readonly ffprobe: string;
|
|
441
|
+
readonly now: Date;
|
|
442
|
+
readonly project: OpenProject;
|
|
443
|
+
readonly repositoryRoot: string;
|
|
444
|
+
readonly runner: ProcessRunner;
|
|
445
|
+
readonly source: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface AnalyzedProjectFaces {
|
|
449
|
+
readonly analysis: FaceAnalysisV1;
|
|
450
|
+
readonly selected: ResolvedVideoAnalysisSubject;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Analyze every immutable segment locally without persisting partial results. */
|
|
454
|
+
export async function analyzeProjectFaces(
|
|
455
|
+
options: AnalyzeProjectFacesOptions,
|
|
456
|
+
): Promise<AnalyzedProjectFaces> {
|
|
457
|
+
const selected = resolveVideoAnalysisSubject(options.project.project, options.source);
|
|
458
|
+
const analysisId = AnalysisIdSchema.parse(
|
|
459
|
+
options.analysisId ?? `analysis_${randomUUID().replaceAll("-", "")}`,
|
|
460
|
+
);
|
|
461
|
+
if (options.project.project.analyses.some(existing => existing.analysisId === analysisId)) {
|
|
462
|
+
throw new CliError("conflict", `Project analysis already exists: ${analysisId}`);
|
|
463
|
+
}
|
|
464
|
+
const config = FaceAnalysisConfigSchema.parse(options.config ?? DEFAULT_FACE_ANALYSIS_CONFIG);
|
|
465
|
+
const estimatedFrames = selected.stream.segments.reduce(
|
|
466
|
+
(total, segment) => total + requestedMaximumFrames(segment, config.sampleIntervalUs),
|
|
467
|
+
0,
|
|
468
|
+
);
|
|
469
|
+
if (estimatedFrames > MAXIMUM_ANALYSIS_FRAMES) {
|
|
470
|
+
throw new CliError(
|
|
471
|
+
"usage",
|
|
472
|
+
`Requested face sampling may exceed ${MAXIMUM_ANALYSIS_FRAMES} frames; lower --sample-fps.`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const runs = [];
|
|
476
|
+
for (const segment of selected.stream.segments) {
|
|
477
|
+
runs.push(await analyzeSegment({
|
|
478
|
+
config,
|
|
479
|
+
faceAnalyzer: options.faceAnalyzer,
|
|
480
|
+
ffprobe: options.ffprobe,
|
|
481
|
+
repositoryRoot: options.repositoryRoot,
|
|
482
|
+
runner: options.runner,
|
|
483
|
+
segment,
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
const firstRun = runs[0];
|
|
487
|
+
if (firstRun === undefined) throw new CliError("invalid-data", "Video stream has no face-analysis segments.");
|
|
488
|
+
for (const run of runs.slice(1)) {
|
|
489
|
+
if (
|
|
490
|
+
!sameCanonicalValue(run.backend, firstRun.backend)
|
|
491
|
+
|| !sameCanonicalValue(run.coordinateSpace, firstRun.coordinateSpace)
|
|
492
|
+
) {
|
|
493
|
+
throw new CliError(
|
|
494
|
+
"invalid-data",
|
|
495
|
+
"Face-analysis segments disagree on their local backend or upright coordinate space.",
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const frames = combineFrames(runs.map(run => run.frames));
|
|
500
|
+
const association = associateFaceDetections(frames, config.tracking);
|
|
501
|
+
const coverageRange = {
|
|
502
|
+
endUs: selected.stream.segments.at(-1)!.assetRange.endUs,
|
|
503
|
+
startUs: selected.stream.segments[0]!.assetRange.startUs,
|
|
504
|
+
};
|
|
505
|
+
const createdAt = options.now.toISOString();
|
|
506
|
+
const mappedBackend = {
|
|
507
|
+
architecture: firstRun.backend.architecture,
|
|
508
|
+
kind: "apple-vision" as const,
|
|
509
|
+
osBuild: firstRun.backend.osBuild,
|
|
510
|
+
requestRevision: firstRun.backend.revision,
|
|
511
|
+
runtimeVersion: firstRun.backend.runtimeVersion,
|
|
512
|
+
};
|
|
513
|
+
const analysis = FaceAnalysisV1Schema.parse({
|
|
514
|
+
analysisId,
|
|
515
|
+
backend: mappedBackend,
|
|
516
|
+
config,
|
|
517
|
+
coordinateSpace: firstRun.coordinateSpace,
|
|
518
|
+
coverage: {
|
|
519
|
+
analyzedFrames: association.results.length,
|
|
520
|
+
failedFrames: 0,
|
|
521
|
+
range: coverageRange,
|
|
522
|
+
requestedFrames: association.results.length,
|
|
523
|
+
},
|
|
524
|
+
createdAt,
|
|
525
|
+
durationUs: selected.asset.durationUs,
|
|
526
|
+
inputDigest: canonicalJsonSha256({
|
|
527
|
+
analyzer: {
|
|
528
|
+
helperVersion: firstRun.backend.helperVersion,
|
|
529
|
+
protocolKind: "slopcamera.face-analysis",
|
|
530
|
+
schemaVersion: 1,
|
|
531
|
+
},
|
|
532
|
+
backend: mappedBackend,
|
|
533
|
+
config,
|
|
534
|
+
subject: selected.subject,
|
|
535
|
+
}),
|
|
536
|
+
kind: "slopcamera.face-analysis",
|
|
537
|
+
privacy: {
|
|
538
|
+
biometricIdentification: "not-performed",
|
|
539
|
+
execution: "local-only",
|
|
540
|
+
storedEvidence: "bounding-boxes-only",
|
|
541
|
+
tracking: "geometry-continuity-only",
|
|
542
|
+
},
|
|
543
|
+
results: association.results,
|
|
544
|
+
schemaVersion: 1,
|
|
545
|
+
subject: selected.subject,
|
|
546
|
+
tool: AnalysisToolSchema.parse({
|
|
547
|
+
name: "slopcamera-face-analyzer",
|
|
548
|
+
profile: "apple-vision-face-rectangles-v1",
|
|
549
|
+
version: firstRun.backend.helperVersion,
|
|
550
|
+
}),
|
|
551
|
+
tracks: association.tracks,
|
|
552
|
+
});
|
|
553
|
+
return { analysis, selected };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface BuildFaceProjectUpdateOptions {
|
|
557
|
+
readonly analysis: FaceAnalysisV1;
|
|
558
|
+
readonly analysisPath: string;
|
|
559
|
+
readonly project: VideoProjectV1;
|
|
560
|
+
readonly updatedAt: string;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function buildFaceAnalysisReference(
|
|
564
|
+
analysisInput: FaceAnalysisV1,
|
|
565
|
+
analysisPath: string,
|
|
566
|
+
): FaceAnalysisReference {
|
|
567
|
+
const analysis = FaceAnalysisV1Schema.parse(analysisInput);
|
|
568
|
+
const reference = ProjectAnalysisReferenceSchema.parse({
|
|
569
|
+
analysisId: analysis.analysisId,
|
|
570
|
+
analyzedFrames: analysis.coverage.analyzedFrames,
|
|
571
|
+
assetId: analysis.subject.assetId,
|
|
572
|
+
createdAt: analysis.createdAt,
|
|
573
|
+
kind: "faces",
|
|
574
|
+
localOnly: true,
|
|
575
|
+
path: analysisPath,
|
|
576
|
+
sha256: sha256Hex(`${canonicalJson(analysis)}\n`),
|
|
577
|
+
streamId: analysis.subject.streamId,
|
|
578
|
+
subjectIntegritySha256: analysis.subject.integritySha256,
|
|
579
|
+
trackCount: analysis.tracks.length,
|
|
580
|
+
});
|
|
581
|
+
if (reference.kind !== "faces") {
|
|
582
|
+
throw new CliError("internal", "Face reference parsing changed its kind.");
|
|
583
|
+
}
|
|
584
|
+
return reference;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Build the compact project pointer for an immutable, content-addressed face sidecar. */
|
|
588
|
+
export function buildFaceProjectUpdate(options: BuildFaceProjectUpdateOptions): {
|
|
589
|
+
readonly project: VideoProjectV1;
|
|
590
|
+
readonly reference: FaceAnalysisReference;
|
|
591
|
+
} {
|
|
592
|
+
const analysis = FaceAnalysisV1Schema.parse(options.analysis);
|
|
593
|
+
if (options.project.analyses.some(existing => existing.analysisId === analysis.analysisId)) {
|
|
594
|
+
throw new CliError("conflict", `Project analysis already exists: ${analysis.analysisId}`);
|
|
595
|
+
}
|
|
596
|
+
if (Date.parse(options.updatedAt) < Date.parse(options.project.updatedAt)) {
|
|
597
|
+
throw new CliError("conflict", "The face analysis update time cannot precede the project update time.");
|
|
598
|
+
}
|
|
599
|
+
const reference = buildFaceAnalysisReference(analysis, options.analysisPath);
|
|
600
|
+
const project = VideoProjectV1Schema.parse({
|
|
601
|
+
...options.project,
|
|
602
|
+
analyses: [...options.project.analyses, reference],
|
|
603
|
+
updatedAt: options.updatedAt,
|
|
604
|
+
});
|
|
605
|
+
return { project, reference };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export interface PersistedProjectFaceAnalysis extends AnalyzedProjectFaces {
|
|
609
|
+
readonly analysisPath: string;
|
|
610
|
+
readonly project: VideoProjectV1;
|
|
611
|
+
readonly reference: FaceAnalysisReference;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function syncDirectory(path: string): Promise<void> {
|
|
615
|
+
const handle = await open(path, "r");
|
|
616
|
+
try {
|
|
617
|
+
await handle.sync();
|
|
618
|
+
} finally {
|
|
619
|
+
await handle.close();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function saveFaceAnalysisArtifactOnce(
|
|
624
|
+
project: OpenProject,
|
|
625
|
+
analysis: FaceAnalysisV1,
|
|
626
|
+
path: string,
|
|
627
|
+
): Promise<void> {
|
|
628
|
+
const parentRelative = dirname(path);
|
|
629
|
+
const parent = await ensurePhysicalPrivateDirectoryWithin(project.directory.path, parentRelative);
|
|
630
|
+
const stagePath = `${path}.stage-${randomUUID()}`;
|
|
631
|
+
const absoluteStage = join(project.directory.path, stagePath);
|
|
632
|
+
const absoluteTarget = join(project.directory.path, path);
|
|
633
|
+
await saveAnalysisArtifact(project.fileSystem, analysis, stagePath);
|
|
634
|
+
try {
|
|
635
|
+
try {
|
|
636
|
+
// Hard-link installation is an atomic create-if-absent operation. Unlike
|
|
637
|
+
// rename, it cannot replace prior immutable evidence.
|
|
638
|
+
await link(absoluteStage, absoluteTarget);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") {
|
|
641
|
+
throw new CliError("conflict", `Analysis artifact already exists and will not be overwritten: ${path}`);
|
|
642
|
+
}
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
} finally {
|
|
646
|
+
await rm(absoluteStage, { force: true });
|
|
647
|
+
}
|
|
648
|
+
await syncDirectory(parent);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export interface PublishedProjectFaceAnalysisArtifact {
|
|
652
|
+
readonly analysisPath: string;
|
|
653
|
+
readonly reference: FaceAnalysisReference;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export async function publishProjectFaceAnalysisArtifact(options: {
|
|
657
|
+
readonly analysis: FaceAnalysisV1;
|
|
658
|
+
readonly project: OpenProject;
|
|
659
|
+
}): Promise<PublishedProjectFaceAnalysisArtifact> {
|
|
660
|
+
const analysis = FaceAnalysisV1Schema.parse(options.analysis);
|
|
661
|
+
const analysisPath = projectAnalysisPath("faces", analysis.analysisId);
|
|
662
|
+
await requireUnusedAnalysisPath(options.project, analysisPath);
|
|
663
|
+
const reference = buildFaceAnalysisReference(analysis, analysisPath);
|
|
664
|
+
await saveFaceAnalysisArtifactOnce(options.project, analysis, analysisPath);
|
|
665
|
+
const persistedText = await options.project.fileSystem.readText(analysisPath);
|
|
666
|
+
if (sha256Hex(persistedText) !== reference.sha256) {
|
|
667
|
+
throw new CliError(
|
|
668
|
+
"conflict",
|
|
669
|
+
"Persisted face-analysis sidecar failed its content-addressed verification.",
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return { analysisPath, reference };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Persist the complete sidecar before installing its project pointer. A failed
|
|
677
|
+
* project write can leave only an unreferenced immutable sidecar, never a
|
|
678
|
+
* project reference to partial evidence.
|
|
679
|
+
*/
|
|
680
|
+
export async function analyzeAndPersistProjectFaces(
|
|
681
|
+
options: AnalyzeProjectFacesOptions,
|
|
682
|
+
): Promise<PersistedProjectFaceAnalysis> {
|
|
683
|
+
const analyzed = await analyzeProjectFaces(options);
|
|
684
|
+
const published = await publishProjectFaceAnalysisArtifact({
|
|
685
|
+
analysis: analyzed.analysis,
|
|
686
|
+
project: options.project,
|
|
687
|
+
});
|
|
688
|
+
const update = buildFaceProjectUpdate({
|
|
689
|
+
analysis: analyzed.analysis,
|
|
690
|
+
analysisPath: published.analysisPath,
|
|
691
|
+
project: options.project.project,
|
|
692
|
+
updatedAt: analyzed.analysis.createdAt,
|
|
693
|
+
});
|
|
694
|
+
const project = canonicalSlopcameraPersistenceDocument(update.project);
|
|
695
|
+
await saveVideoProject(options.project.fileSystem, project);
|
|
696
|
+
return {
|
|
697
|
+
...analyzed,
|
|
698
|
+
analysisPath: published.analysisPath,
|
|
699
|
+
...update,
|
|
700
|
+
project,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export interface LoadedProjectFaceAnalysis {
|
|
705
|
+
readonly analysis: FaceAnalysisV1;
|
|
706
|
+
readonly reference: FaceAnalysisReference;
|
|
707
|
+
readonly selected: ResolvedVideoAnalysisSubject;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Load a face sidecar only after its project pointer, bytes, IDs, and current media subject agree. */
|
|
711
|
+
export async function loadVerifiedProjectFaceAnalysis(options: {
|
|
712
|
+
readonly analysisId: string;
|
|
713
|
+
readonly project: OpenProject;
|
|
714
|
+
}): Promise<LoadedProjectFaceAnalysis> {
|
|
715
|
+
const analysisId = AnalysisIdSchema.parse(options.analysisId);
|
|
716
|
+
const reference = options.project.project.analyses.find(candidate => candidate.analysisId === analysisId);
|
|
717
|
+
if (reference === undefined) throw new CliError("not-found", `Unknown project analysis: ${analysisId}`);
|
|
718
|
+
if (reference.kind !== "faces") {
|
|
719
|
+
throw new CliError("invalid-data", `Project analysis ${analysisId} is not face evidence.`);
|
|
720
|
+
}
|
|
721
|
+
const expectedPath = projectAnalysisPath("faces", analysisId);
|
|
722
|
+
if (reference.path !== expectedPath) {
|
|
723
|
+
throw new CliError("invalid-data", `Face analysis ${analysisId} does not use its canonical sidecar path.`);
|
|
724
|
+
}
|
|
725
|
+
let text: string;
|
|
726
|
+
try {
|
|
727
|
+
text = await options.project.fileSystem.readText(reference.path);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (isMissingFile(error)) {
|
|
730
|
+
throw new CliError("invalid-data", `Face-analysis sidecar is missing: ${reference.path}`);
|
|
731
|
+
}
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
if (sha256Hex(text) !== reference.sha256) {
|
|
735
|
+
throw new CliError("invalid-data", `Face-analysis sidecar ${analysisId} failed its SHA-256 check.`);
|
|
736
|
+
}
|
|
737
|
+
let input: unknown;
|
|
738
|
+
try {
|
|
739
|
+
input = JSON.parse(text) as unknown;
|
|
740
|
+
} catch {
|
|
741
|
+
throw new CliError("invalid-data", `Face-analysis sidecar ${analysisId} is not valid JSON.`);
|
|
742
|
+
}
|
|
743
|
+
const parsed = FaceAnalysisV1Schema.safeParse(input);
|
|
744
|
+
if (!parsed.success) {
|
|
745
|
+
throw new CliError("invalid-data", `Face-analysis sidecar ${analysisId} is invalid: ${parsed.error.message}`);
|
|
746
|
+
}
|
|
747
|
+
const analysis = parsed.data;
|
|
748
|
+
if (`${canonicalJson(analysis)}\n` !== text) {
|
|
749
|
+
throw new CliError("invalid-data", `Face-analysis sidecar ${analysisId} is not canonical immutable JSON.`);
|
|
750
|
+
}
|
|
751
|
+
if (
|
|
752
|
+
analysis.analysisId !== reference.analysisId
|
|
753
|
+
|| analysis.createdAt !== reference.createdAt
|
|
754
|
+
|| analysis.subject.assetId !== reference.assetId
|
|
755
|
+
|| analysis.subject.streamId !== reference.streamId
|
|
756
|
+
|| analysis.subject.integritySha256 !== reference.subjectIntegritySha256
|
|
757
|
+
|| analysis.coverage.analyzedFrames !== reference.analyzedFrames
|
|
758
|
+
|| analysis.tracks.length !== reference.trackCount
|
|
759
|
+
) {
|
|
760
|
+
throw new CliError("invalid-data", `Face-analysis sidecar ${analysisId} disagrees with its project reference.`);
|
|
761
|
+
}
|
|
762
|
+
const selected = resolveVideoAnalysisSubject(
|
|
763
|
+
options.project.project,
|
|
764
|
+
`${reference.assetId}:${reference.streamId}`,
|
|
765
|
+
);
|
|
766
|
+
if (
|
|
767
|
+
analysis.subject.integritySha256 !== selected.subject.integritySha256
|
|
768
|
+
|| analysis.durationUs !== selected.asset.durationUs
|
|
769
|
+
) {
|
|
770
|
+
throw new CliError("conflict", `Face analysis ${analysisId} is stale for the current project media subject.`);
|
|
771
|
+
}
|
|
772
|
+
const firstSegment = selected.stream.segments[0]!;
|
|
773
|
+
const lastSegment = selected.stream.segments.at(-1)!;
|
|
774
|
+
if (
|
|
775
|
+
analysis.coverage.range.startUs !== firstSegment.assetRange.startUs
|
|
776
|
+
|| analysis.coverage.range.endUs !== lastSegment.assetRange.endUs
|
|
777
|
+
|| analysis.results.some(result => !selected.stream.segments.some(segment =>
|
|
778
|
+
result.assetTimeUs >= segment.assetRange.startUs && result.assetTimeUs < segment.assetRange.endUs))
|
|
779
|
+
) {
|
|
780
|
+
throw new CliError("invalid-data", `Face analysis ${analysisId} does not match its stream segment coverage.`);
|
|
781
|
+
}
|
|
782
|
+
return { analysis, reference, selected };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export interface FaceTrackSummary {
|
|
786
|
+
readonly firstSeenAssetTimeUs: number;
|
|
787
|
+
readonly lastSeenAssetTimeUs: number;
|
|
788
|
+
readonly maximumArea: number;
|
|
789
|
+
readonly maximumConfidence: number;
|
|
790
|
+
readonly maximumObservedGapUs: number;
|
|
791
|
+
readonly meanArea: number;
|
|
792
|
+
readonly meanConfidence: number;
|
|
793
|
+
readonly observationCount: number;
|
|
794
|
+
readonly sample: {
|
|
795
|
+
readonly assetTimeUs: number;
|
|
796
|
+
readonly confidence: number;
|
|
797
|
+
readonly distanceUs: number;
|
|
798
|
+
readonly rect: NormalizedTopLeftRect;
|
|
799
|
+
} | null;
|
|
800
|
+
readonly trackId: FaceTrackId;
|
|
801
|
+
readonly visibleDurationUs: number;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export interface FaceTrackSummaryList {
|
|
805
|
+
readonly analysisId: string;
|
|
806
|
+
readonly atUs: number | null;
|
|
807
|
+
readonly returned: number;
|
|
808
|
+
readonly totalMatched: number;
|
|
809
|
+
readonly tracks: readonly FaceTrackSummary[];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
interface MutableTrackStats {
|
|
813
|
+
areaSum: number;
|
|
814
|
+
confidenceSum: number;
|
|
815
|
+
maximumArea: number;
|
|
816
|
+
maximumConfidence: number;
|
|
817
|
+
observations: number;
|
|
818
|
+
sample: FaceTrackSummary["sample"];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Produce a token-bounded track list. With `atUs`, only tracks having nearby
|
|
823
|
+
* sampled evidence within the configured continuity gap are returned.
|
|
824
|
+
*/
|
|
825
|
+
export function listFaceTrackSummaries(options: {
|
|
826
|
+
readonly analysis: FaceAnalysisV1;
|
|
827
|
+
readonly atUs?: number;
|
|
828
|
+
readonly limit: number;
|
|
829
|
+
readonly minimumConfidence: number;
|
|
830
|
+
readonly minimumDurationUs: number;
|
|
831
|
+
}): FaceTrackSummaryList {
|
|
832
|
+
const analysis = FaceAnalysisV1Schema.parse(options.analysis);
|
|
833
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > MAXIMUM_TRACK_LIST_LIMIT) {
|
|
834
|
+
throw new CliError("usage", `Face track list limit must be from 1 through ${MAXIMUM_TRACK_LIST_LIMIT}.`);
|
|
835
|
+
}
|
|
836
|
+
if (!Number.isFinite(options.minimumConfidence)
|
|
837
|
+
|| options.minimumConfidence < 0
|
|
838
|
+
|| options.minimumConfidence > 1) {
|
|
839
|
+
throw new CliError("usage", "Face track minimum confidence must be between zero and one.");
|
|
840
|
+
}
|
|
841
|
+
if (!Number.isSafeInteger(options.minimumDurationUs) || options.minimumDurationUs < 0) {
|
|
842
|
+
throw new CliError("usage", "Face track minimum duration must be nonnegative integer microseconds.");
|
|
843
|
+
}
|
|
844
|
+
if (
|
|
845
|
+
options.atUs !== undefined
|
|
846
|
+
&& (!Number.isSafeInteger(options.atUs) || options.atUs < 0 || options.atUs > analysis.durationUs)
|
|
847
|
+
) {
|
|
848
|
+
throw new CliError("usage", "Face track inspection time must lie inside the asset timeline.");
|
|
849
|
+
}
|
|
850
|
+
const eligibleTracks = new Map(analysis.tracks
|
|
851
|
+
.filter(track => track.lastSeenAssetTimeUs - track.firstSeenAssetTimeUs >= options.minimumDurationUs)
|
|
852
|
+
.map(track => [track.trackId, track]));
|
|
853
|
+
const stats = new Map<FaceTrackId, MutableTrackStats>();
|
|
854
|
+
for (const result of analysis.results) {
|
|
855
|
+
if (result.state !== "analyzed") continue;
|
|
856
|
+
for (const detection of result.detections) {
|
|
857
|
+
if (!eligibleTracks.has(detection.trackId)) continue;
|
|
858
|
+
const area = detection.rect.width * detection.rect.height;
|
|
859
|
+
const current = stats.get(detection.trackId) ?? {
|
|
860
|
+
areaSum: 0,
|
|
861
|
+
confidenceSum: 0,
|
|
862
|
+
maximumArea: 0,
|
|
863
|
+
maximumConfidence: 0,
|
|
864
|
+
observations: 0,
|
|
865
|
+
sample: null,
|
|
866
|
+
};
|
|
867
|
+
current.areaSum += area;
|
|
868
|
+
current.confidenceSum += detection.confidence;
|
|
869
|
+
current.maximumArea = Math.max(current.maximumArea, area);
|
|
870
|
+
current.maximumConfidence = Math.max(current.maximumConfidence, detection.confidence);
|
|
871
|
+
current.observations += 1;
|
|
872
|
+
if (options.atUs !== undefined) {
|
|
873
|
+
const distanceUs = Math.abs(result.assetTimeUs - options.atUs);
|
|
874
|
+
if (
|
|
875
|
+
distanceUs <= analysis.config.tracking.maximumGapUs
|
|
876
|
+
&& (
|
|
877
|
+
current.sample === null
|
|
878
|
+
|| distanceUs < current.sample.distanceUs
|
|
879
|
+
|| (distanceUs === current.sample.distanceUs && result.assetTimeUs < current.sample.assetTimeUs)
|
|
880
|
+
)
|
|
881
|
+
) {
|
|
882
|
+
current.sample = {
|
|
883
|
+
assetTimeUs: result.assetTimeUs,
|
|
884
|
+
confidence: detection.confidence,
|
|
885
|
+
distanceUs,
|
|
886
|
+
rect: detection.rect,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
stats.set(detection.trackId, current);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const summaries = [...eligibleTracks.values()].flatMap((track): readonly FaceTrackSummary[] => {
|
|
894
|
+
const trackStats = stats.get(track.trackId);
|
|
895
|
+
if (
|
|
896
|
+
trackStats === undefined
|
|
897
|
+
|| trackStats.maximumConfidence < options.minimumConfidence
|
|
898
|
+
|| (options.atUs !== undefined
|
|
899
|
+
&& (trackStats.sample === null || trackStats.sample.confidence < options.minimumConfidence))
|
|
900
|
+
) {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
return [{
|
|
904
|
+
firstSeenAssetTimeUs: track.firstSeenAssetTimeUs,
|
|
905
|
+
lastSeenAssetTimeUs: track.lastSeenAssetTimeUs,
|
|
906
|
+
maximumArea: trackStats.maximumArea,
|
|
907
|
+
maximumConfidence: trackStats.maximumConfidence,
|
|
908
|
+
maximumObservedGapUs: track.maximumObservedGapUs,
|
|
909
|
+
meanArea: trackStats.areaSum / trackStats.observations,
|
|
910
|
+
meanConfidence: trackStats.confidenceSum / trackStats.observations,
|
|
911
|
+
observationCount: track.observationCount,
|
|
912
|
+
sample: trackStats.sample,
|
|
913
|
+
trackId: track.trackId,
|
|
914
|
+
visibleDurationUs: track.lastSeenAssetTimeUs - track.firstSeenAssetTimeUs,
|
|
915
|
+
}];
|
|
916
|
+
}).sort((left, right) => {
|
|
917
|
+
if (options.atUs !== undefined) {
|
|
918
|
+
return (
|
|
919
|
+
(right.sample?.rect.width ?? 0) * (right.sample?.rect.height ?? 0)
|
|
920
|
+
- (left.sample?.rect.width ?? 0) * (left.sample?.rect.height ?? 0)
|
|
921
|
+
|| (right.sample?.confidence ?? 0) - (left.sample?.confidence ?? 0)
|
|
922
|
+
|| left.trackId.localeCompare(right.trackId)
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
return (
|
|
926
|
+
right.visibleDurationUs - left.visibleDurationUs
|
|
927
|
+
|| right.observationCount - left.observationCount
|
|
928
|
+
|| right.meanArea - left.meanArea
|
|
929
|
+
|| left.trackId.localeCompare(right.trackId)
|
|
930
|
+
);
|
|
931
|
+
});
|
|
932
|
+
const tracks = summaries.slice(0, options.limit);
|
|
933
|
+
return {
|
|
934
|
+
analysisId: analysis.analysisId,
|
|
935
|
+
atUs: options.atUs ?? null,
|
|
936
|
+
returned: tracks.length,
|
|
937
|
+
totalMatched: summaries.length,
|
|
938
|
+
tracks,
|
|
939
|
+
};
|
|
940
|
+
}
|