@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,39 @@
|
|
|
1
|
+
# Retained Poly Haven assets
|
|
2
|
+
|
|
3
|
+
This adapter finds free Poly Haven assets and imports an explicitly selected file set into a local studio source directory. Search and import results identify Poly Haven, retain the artists' credits, and distinguish the CC0 asset license from the API service terms.
|
|
4
|
+
|
|
5
|
+
Poly Haven's current API terms permit personal and commercial use without a key or payment. API clients must identify their application and make the source of surfaced content clear. SLOPCAMERA sends `SLOPCAMERA/StudioAssets (+https://slopcamera.com)` and returns `Powered by Poly Haven`. The assets themselves remain CC0 without an attribution requirement. These terms were checked on September 9, 2026 against the [current API terms](https://raw.githubusercontent.com/Poly-Haven/Public-API/master/ToS.md), the [July 18 API announcement](https://polyhaven.com/our-api), and the [asset license](https://polyhaven.com/license).
|
|
6
|
+
|
|
7
|
+
## Service interface
|
|
8
|
+
|
|
9
|
+
`createPolyHavenAssetService({storageRoot, fetch?, signal?, beforePublication?})` exposes `search(input)`, `describe(assetId)`, `plan(selection)`, and `importAsset(plan)`. `storageRoot` must be an existing physical private directory, normally below `artifacts/slopcamera/generated/studio-assets`. The optional fetch and publication hooks are host-owned runtime dependencies; they never enter a retained plan.
|
|
10
|
+
|
|
11
|
+
Search input is `{provider:"poly-haven",query:"warm studio",type:"hdris",limit:8}`. Types are `all`, `hdris`, `textures`, and `models`; limits are one through 20. The adapter calls the documented `/search` endpoint and fetches metadata only for those bounded results. It preserves provider result order. Similarity scores do not independently explain the hybrid ranking or prove suitability. [Official API specification](https://raw.githubusercontent.com/Poly-Haven/Public-API/master/swagger.yml)
|
|
12
|
+
|
|
13
|
+
Selections require `provider`, `assetId`, `resolution`, `kind`, and `format`. `maximumTotalBytes` defaults to 50 MiB and cannot exceed 512 MiB. Supported resolutions are `1k`, `2k`, and `4k`.
|
|
14
|
+
|
|
15
|
+
| Kind | Formats | Selection |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `hdri` | `hdr`, `exr` | One exact environment file |
|
|
18
|
+
| `texture` | `png`, `jpg`, `exr` | An explicit `maps` array using catalog names such as `Diffuse`, `Rough`, and `nor_gl` |
|
|
19
|
+
| `model` | `gltf` | One glTF 2.0 file and its complete catalog `include` dependencies |
|
|
20
|
+
|
|
21
|
+
`describe` returns asset/author metadata and bounded provider snapshots, so an agent can inspect available variants before selecting one. Plans retain canonical snapshots, exact selected URLs, MD5 checksums, byte counts, relative paths, credits, and a SHA-256 plan identity. `parsePolyHavenAssetPlan` recomputes the entire plan from its snapshots and selection. It rejects changed counts, omitted dependencies, conflicting paths, unexpected fields, and altered provenance.
|
|
22
|
+
|
|
23
|
+
## Acquisition and replay
|
|
24
|
+
|
|
25
|
+
Downloads use only HTTPS `api.polyhaven.com` metadata endpoints and `dl.polyhaven.org/file/ph-assets/` asset paths. Credentials, URL fragments, query strings on asset downloads, alternate ports, redirects, and arbitrary remote URLs reject. Each file is at most 128 MiB, each complete selection is at most 64 files, and response capture checks actual streamed bytes. Requests have a bounded deadline and honor host cancellation. There are no automatic retries, uploads, credential reads, or provider charges.
|
|
26
|
+
|
|
27
|
+
Import verifies catalog size and MD5, checks supported file signatures, and computes local SHA-256 identities. The shared physical storage adapter publishes each exact source file with atomic no-replace semantics. Completion is published only after the entire source closure verifies. An interrupted import can reuse already verified files; a completed import rechecks all retained files and its receipt without network access. Conflicting bytes or undeclared files reject rather than being replaced.
|
|
28
|
+
|
|
29
|
+
Exact download stages remain retained as recovery evidence, so disk usage can exceed the network byte budget. Source files live in `imports/<planSha256>/source`; the canonical plan and receipt live beside that directory. The import result returns the private absolute `sourceRoot`, the relative `receiptPath`, and its receipt identity. Agents explicitly include the selected source files in the ordinary studio bundle before rendering. Acquisition does not execute a renderer or append objects to a scene.
|
|
30
|
+
|
|
31
|
+
The glTF subset accepts explicit local `.bin`, PNG, and JPEG dependencies. It verifies every buffer/image URI against the catalog closure and checks declared buffer byte lengths. Remote/data URIs, hidden resource references, unsupported extensions, omitted dependencies, and unreferenced catalog includes reject. Common material and texture-transform extensions are supported; compressed geometry and additional image formats require a separately qualified profile. Native `.blend`, scripts, archives, FBX, USD, and GLB are not acquired by this subset. Existing explicit native-source import paths retain such source as inert bytes until a separately authorized render.
|
|
32
|
+
|
|
33
|
+
Provider metadata and transport checksums are evidence of what was fetched. They do not establish cryptographically authenticated authorship, renderer compatibility, physical calibration, or absence of hostile native decoder inputs. API thumbnail metadata is not assigned the CC0 asset-file license, and the importer does not fetch preview images.
|
|
34
|
+
|
|
35
|
+
## Validation
|
|
36
|
+
|
|
37
|
+
`bun test apps/desktop/studio/assets/poly-haven.test.ts` runs network-free regressions for canonical plans, foreign accessors, source URLs, budgets, glTF dependencies, cancellation, publication custody, interrupted acquisition, and offline replay.
|
|
38
|
+
|
|
39
|
+
The opt-in `qualify-live.ts <fresh-absolute-ignored-directory>` imports `venice_sunset` at 1k HDR and `dirty_football` at 1k glTF. It enforces a combined 50 MiB free download ceiling and requires both complete imports to replay with networking disabled. The September 9 qualification retained six exact files totaling 3,435,401 bytes: 1,440,400 bytes of HDRI and 1,995,001 bytes of model/source dependencies. The qualification writes its evidence into the requested ignored directory. This verifies acquisition and local dependency closure, not a native render of the imported asset.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canonicalJson, boundedCanonicalJsonSha256 } from "../../../../src/code/canonical-json";
|
|
3
|
+
import { createBoundedJsonValueSnapshot, deepFreezeJson } from "../../../../src/code/json-snapshot";
|
|
4
|
+
import { StudioPathSchema } from "../../../../src/studio";
|
|
5
|
+
|
|
6
|
+
export const POLY_HAVEN = Object.freeze({ provider: "poly-haven" as const, api: "https://api.polyhaven.com", download: "https://dl.polyhaven.org", credit: "Powered by Poly Haven", homepage: "https://polyhaven.com", license: "CC0-1.0", licenseUrl: "https://creativecommons.org/publicdomain/zero/1.0/", termsUrl: "https://github.com/Poly-Haven/Public-API/blob/master/ToS.md", userAgent: "SLOPCAMERA/StudioAssets (+https://slopcamera.com)" });
|
|
7
|
+
export const ASSET_LIMITS = Object.freeze({ jsonBytes: 4 * 1024 * 1024, fileBytes: 128 * 1024 * 1024, totalBytes: 512 * 1024 * 1024, files: 64, requestMs: 120_000 });
|
|
8
|
+
export const AssetIdSchema = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,127}$/u);
|
|
9
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
10
|
+
const text = z.string().min(1).max(512).refine(value => !/[\u0000-\u001f\u007f]/u.test(value));
|
|
11
|
+
const resolution = z.enum(["1k", "2k", "4k"]);
|
|
12
|
+
const common = { provider: z.literal("poly-haven"), assetId: AssetIdSchema, resolution, maximumTotalBytes: z.number().int().min(1).max(ASSET_LIMITS.totalBytes).default(50 * 1024 * 1024) };
|
|
13
|
+
export const AssetSelectionSchema = z.discriminatedUnion("kind", [
|
|
14
|
+
z.strictObject({ ...common, kind: z.literal("hdri"), format: z.enum(["hdr", "exr"]) }),
|
|
15
|
+
z.strictObject({ ...common, kind: z.literal("texture"), format: z.enum(["png", "jpg", "exr"]), maps: z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,63}$/u)).min(1).max(16) }).refine(value => new Set(value.maps).size === value.maps.length, "Texture maps must be unique."),
|
|
16
|
+
z.strictObject({ ...common, kind: z.literal("model"), format: z.literal("gltf") }),
|
|
17
|
+
]);
|
|
18
|
+
export type AssetSelection = z.infer<typeof AssetSelectionSchema>;
|
|
19
|
+
export const AssetSearchSchema = z.strictObject({ provider: z.literal("poly-haven"), query: z.string().trim().min(1).max(100).refine(value => !/[\u0000-\u001f\u007f]/u.test(value)).transform(value => value.toLowerCase()), type: z.enum(["all", "hdris", "textures", "models"]).default("all"), limit: z.number().int().min(1).max(20).default(8) });
|
|
20
|
+
export type AssetSearch = z.infer<typeof AssetSearchSchema>;
|
|
21
|
+
export function captureAssetJson(value: unknown): unknown {
|
|
22
|
+
return createBoundedJsonValueSnapshot(value, ASSET_LIMITS.jsonBytes, "studio asset data", { maximumDepth: 24, maximumValues: 150_000 }).value;
|
|
23
|
+
}
|
|
24
|
+
export function parseAssetSelection(value: unknown): AssetSelection { return deepFreezeJson(AssetSelectionSchema.parse(captureAssetJson(value))); }
|
|
25
|
+
export function parseAssetSearch(value: unknown): AssetSearch { return deepFreezeJson(AssetSearchSchema.parse(captureAssetJson(value))); }
|
|
26
|
+
export const assetJson = (value: unknown): string => `${canonicalJson(value)}\n`;
|
|
27
|
+
export const assetHash = (value: unknown): string => boundedCanonicalJsonSha256(value, { maximumBytes: ASSET_LIMITS.jsonBytes });
|
|
28
|
+
|
|
29
|
+
export function downloadUrl(value: unknown): string {
|
|
30
|
+
if (typeof value !== "string" || value.length > 2048) throw new Error("Invalid Poly Haven download URL.");
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (url.origin !== POLY_HAVEN.download || url.username || url.password || url.port || url.hash || url.search || !url.pathname.startsWith("/file/ph-assets/") || /%2f|%5c/iu.test(url.pathname)) throw new Error("Poly Haven downloads require the fixed HTTPS asset origin and path.");
|
|
33
|
+
const path = decodeURIComponent(url.pathname.slice("/file/ph-assets/".length));
|
|
34
|
+
StudioPathSchema.parse(path);
|
|
35
|
+
if (url.href !== value) throw new Error("Poly Haven download URLs must be canonical.");
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function record(value: unknown): Record<string, unknown> {
|
|
39
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected Poly Haven object metadata.");
|
|
40
|
+
return value as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
function own(value: unknown, key: string): unknown {
|
|
43
|
+
const object = record(value);
|
|
44
|
+
if (!Object.hasOwn(object, key)) throw new Error("The selected Poly Haven variant is unavailable.");
|
|
45
|
+
return object[key];
|
|
46
|
+
}
|
|
47
|
+
function validateMetadataUrls(value: unknown): void {
|
|
48
|
+
if (Array.isArray(value)) { value.forEach(validateMetadataUrls); return; }
|
|
49
|
+
if (value === null || typeof value !== "object") return;
|
|
50
|
+
for (const [key, item] of Object.entries(value)) {
|
|
51
|
+
if (/^(url|thumbnail_url)$/u.test(key) && item !== null) {
|
|
52
|
+
if (key === "url") downloadUrl(item);
|
|
53
|
+
else {
|
|
54
|
+
const url = new URL(z.string().max(2048).parse(item));
|
|
55
|
+
if (url.origin !== "https://cdn.polyhaven.com" || url.username || url.password || url.port || url.hash || !url.pathname.startsWith("/asset_img/") || [...url.searchParams.keys()].some(key => !["width", "height", "v"].includes(key))) throw new Error("Unexpected Poly Haven thumbnail URL.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
validateMetadataUrls(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function capturePolyHavenSnapshots(input: unknown): { readonly info: unknown; readonly files: unknown } {
|
|
62
|
+
const captured = z.strictObject({ info: z.unknown(), files: z.unknown() }).parse(captureAssetJson(input));
|
|
63
|
+
const snapshots = { info: record(captured.info), files: record(captured.files) };
|
|
64
|
+
validateMetadataUrls(snapshots);
|
|
65
|
+
return deepFreezeJson(snapshots);
|
|
66
|
+
}
|
|
67
|
+
export interface PolyHavenAsset { readonly assetId: string; readonly name: string; readonly type: "hdris" | "textures" | "models"; readonly authors: readonly { readonly name: string; readonly role: string }[]; readonly assetPage: string }
|
|
68
|
+
export function describePolyHavenAsset(assetIdInput: unknown, infoInput: unknown): PolyHavenAsset {
|
|
69
|
+
const assetId = AssetIdSchema.parse(assetIdInput), info = record(captureAssetJson(infoInput));
|
|
70
|
+
validateMetadataUrls(info);
|
|
71
|
+
const type = z.number().int().min(0).max(2).parse(info.type), authors = record(info.authors);
|
|
72
|
+
if (Object.keys(authors).length < 1 || Object.keys(authors).length > 64) throw new Error("Poly Haven author metadata is missing or exceeds its bound.");
|
|
73
|
+
return deepFreezeJson({ assetId, name: text.parse(info.name), type: (["hdris", "textures", "models"] as const)[type]!, authors: Object.entries(authors).map(([name, role]) => ({ name: text.parse(name), role: text.parse(role) })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0), assetPage: `${POLY_HAVEN.homepage}/a/${assetId}` });
|
|
74
|
+
}
|
|
75
|
+
export interface AssetPlannedFile { readonly path: string; readonly url: string; readonly bytes: number; readonly md5: string; readonly role: "environment" | "texture" | "model" | "dependency"; readonly map?: string }
|
|
76
|
+
const sourceFile = z.object({ url: z.string(), size: z.number().int().positive().max(ASSET_LIMITS.fileBytes), md5: z.string().regex(/^[a-f0-9]{32}$/u), include: z.record(z.string(), z.unknown()).optional() });
|
|
77
|
+
function selectedFile(value: unknown, role: AssetPlannedFile["role"], pathInput?: string, map?: string): { file: AssetPlannedFile; includes: Record<string, unknown> } {
|
|
78
|
+
const source = sourceFile.parse(value), url = downloadUrl(source.url);
|
|
79
|
+
if (role === "model" && source.size > ASSET_LIMITS.jsonBytes) throw new Error("Selected glTF JSON exceeds its four MiB decode profile.");
|
|
80
|
+
const basename = decodeURIComponent(new URL(url).pathname.split("/").at(-1)!);
|
|
81
|
+
const path = StudioPathSchema.parse(pathInput ?? basename);
|
|
82
|
+
const allowed = role === "model" ? /\.gltf$/u : role === "dependency" ? /\.(?:bin|png|jpg|jpeg)$/u : /\.(?:hdr|exr|png|jpg)$/u;
|
|
83
|
+
if (!allowed.test(path) || !allowed.test(basename)) throw new Error("Unsupported asset file; archives and executable sources are not imported by this adapter.");
|
|
84
|
+
return { file: { path, url, bytes: source.size, md5: source.md5, role, ...(map === undefined ? {} : { map }) }, includes: source.include ?? {} };
|
|
85
|
+
}
|
|
86
|
+
export interface PolyHavenAssetPlan {
|
|
87
|
+
readonly kind: "slopcamera.studio-asset-plan"; readonly schemaVersion: 1; readonly provider: "poly-haven"; readonly selection: AssetSelection;
|
|
88
|
+
readonly asset: PolyHavenAsset; readonly license: { readonly spdx: "CC0-1.0"; readonly url: string; readonly scope: "asset-files" };
|
|
89
|
+
readonly credit: string; readonly apiTermsUrl: string; readonly snapshots: { readonly info: unknown; readonly files: unknown };
|
|
90
|
+
readonly files: readonly AssetPlannedFile[]; readonly totalBytes: number; readonly planSha256: string;
|
|
91
|
+
}
|
|
92
|
+
export function planPolyHavenAsset(selectionInput: unknown, snapshotsInput: { readonly info: unknown; readonly files: unknown }): PolyHavenAssetPlan {
|
|
93
|
+
const selection = parseAssetSelection(selectionInput), snapshots = capturePolyHavenSnapshots(snapshotsInput);
|
|
94
|
+
const asset = describePolyHavenAsset(selection.assetId, snapshots.info);
|
|
95
|
+
if (asset.type !== ({ hdri: "hdris", texture: "textures", model: "models" } as const)[selection.kind]) throw new Error("Selected asset kind differs from its provider metadata.");
|
|
96
|
+
const files: AssetPlannedFile[] = [];
|
|
97
|
+
for (const map of selection.kind === "texture" ? [...selection.maps].sort() : [selection.kind === "hdri" ? "hdri" : "gltf"]) {
|
|
98
|
+
const variant = own(own(own(snapshots.files, map), selection.resolution), selection.format);
|
|
99
|
+
const selected = selectedFile(variant, selection.kind === "hdri" ? "environment" : selection.kind, undefined, selection.kind === "texture" ? map : undefined);
|
|
100
|
+
if (!selected.file.path.endsWith(`.${selection.format}`)) throw new Error("Selected asset format differs from its download filename.");
|
|
101
|
+
files.push(selected.file);
|
|
102
|
+
const includes = Object.entries(selected.includes);
|
|
103
|
+
if (selection.kind !== "model" && includes.length > 0) throw new Error("The selected standalone map unexpectedly requires dependencies.");
|
|
104
|
+
for (const [path, value] of includes) {
|
|
105
|
+
const dependency = selectedFile(value, "dependency", path);
|
|
106
|
+
if (Object.keys(dependency.includes).length > 0) throw new Error("Nested native dependencies require a separately supported import profile.");
|
|
107
|
+
files.push(dependency.file);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const paths = new Set<string>();
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
const key = file.path.toLowerCase();
|
|
113
|
+
if (paths.has(key)) throw new Error("Selected asset paths collide.");
|
|
114
|
+
paths.add(key);
|
|
115
|
+
}
|
|
116
|
+
for (const path of paths) for (let offset = path.indexOf("/"); offset !== -1; offset = path.indexOf("/", offset + 1)) if (paths.has(path.slice(0, offset))) throw new Error("Selected asset uses a file as a directory.");
|
|
117
|
+
const totalBytes = files.reduce((total, file) => total + file.bytes, 0);
|
|
118
|
+
if (files.length > ASSET_LIMITS.files || totalBytes > selection.maximumTotalBytes) throw new Error("Selected complete asset exceeds its explicit file or byte budget.");
|
|
119
|
+
const body = { kind: "slopcamera.studio-asset-plan" as const, schemaVersion: 1 as const, provider: "poly-haven" as const, selection, asset, license: { spdx: "CC0-1.0" as const, url: POLY_HAVEN.licenseUrl, scope: "asset-files" as const }, credit: POLY_HAVEN.credit, apiTermsUrl: POLY_HAVEN.termsUrl, snapshots, files: files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0), totalBytes };
|
|
120
|
+
return deepFreezeJson({ ...body, planSha256: assetHash(body) });
|
|
121
|
+
}
|
|
122
|
+
export function parsePolyHavenAssetPlan(input: unknown): PolyHavenAssetPlan {
|
|
123
|
+
const value = record(captureAssetJson(input));
|
|
124
|
+
const snapshots = z.strictObject({ info: z.unknown(), files: z.unknown() }).parse(value.snapshots);
|
|
125
|
+
digest.parse(value.planSha256);
|
|
126
|
+
const derived = planPolyHavenAsset(value.selection, { info: snapshots.info, files: snapshots.files });
|
|
127
|
+
if (canonicalJson(value) !== canonicalJson(derived)) throw new Error("Asset plan differs from its exact canonical provider snapshots and selection.");
|
|
128
|
+
return derived;
|
|
129
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ASSET_LIMITS, POLY_HAVEN, downloadUrl } from "./contracts";
|
|
2
|
+
|
|
3
|
+
export type AssetFetch = (input: string, init: RequestInit) => Promise<Response>;
|
|
4
|
+
function aborted(signal: AbortSignal): void { if (signal.aborted) throw new Error("Poly Haven asset operation was cancelled or timed out."); }
|
|
5
|
+
async function withCancellation<Value>(pending: Promise<Value>, signal: AbortSignal): Promise<Value> {
|
|
6
|
+
let onAbort: (() => void) | undefined;
|
|
7
|
+
try {
|
|
8
|
+
return await Promise.race([pending, new Promise<never>((_, reject) => {
|
|
9
|
+
onAbort = () => reject(new Error("Poly Haven response was cancelled or timed out."));
|
|
10
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
11
|
+
if (signal.aborted) onAbort();
|
|
12
|
+
})]);
|
|
13
|
+
} finally { if (onAbort !== undefined) signal.removeEventListener("abort", onAbort); }
|
|
14
|
+
}
|
|
15
|
+
export function createPolyHavenNetwork(options: { readonly fetch?: AssetFetch; readonly signal?: AbortSignal }) {
|
|
16
|
+
const request = options.fetch ?? globalThis.fetch;
|
|
17
|
+
const cancellation = options.signal;
|
|
18
|
+
async function read(url: string, maximumBytes: number, expectedBytes?: number): Promise<Buffer> {
|
|
19
|
+
const signal = AbortSignal.any([AbortSignal.timeout(ASSET_LIMITS.requestMs), ...(cancellation === undefined ? [] : [cancellation])]);
|
|
20
|
+
aborted(signal);
|
|
21
|
+
const response = await withCancellation(request(url, { method: "GET", redirect: "error", credentials: "omit", signal, headers: { "user-agent": POLY_HAVEN.userAgent, accept: "application/json, application/octet-stream;q=0.9" } }), signal);
|
|
22
|
+
if (signal.aborted) { void response.body?.cancel().catch(() => undefined); aborted(signal); }
|
|
23
|
+
if (response.status !== 200 || response.redirected || response.url !== "" && response.url !== url || response.body === null) {
|
|
24
|
+
void response.body?.cancel().catch(() => undefined);
|
|
25
|
+
throw new Error(`Poly Haven returned an unsupported response (HTTP ${response.status}).`);
|
|
26
|
+
}
|
|
27
|
+
const declared = response.headers.get("content-length");
|
|
28
|
+
if (declared !== null && (!/^\d+$/u.test(declared) || !Number.isSafeInteger(Number(declared)) || Number(declared) > maximumBytes)) {
|
|
29
|
+
void response.body.cancel().catch(() => undefined);
|
|
30
|
+
throw new Error("Poly Haven response exceeds its byte bound.");
|
|
31
|
+
}
|
|
32
|
+
const reader = response.body.getReader(), chunks: Buffer[] = [];
|
|
33
|
+
let length = 0;
|
|
34
|
+
try {
|
|
35
|
+
for (;;) {
|
|
36
|
+
aborted(signal);
|
|
37
|
+
const next = await withCancellation(reader.read(), signal);
|
|
38
|
+
if (next.done) break;
|
|
39
|
+
length += next.value.byteLength;
|
|
40
|
+
if (length > maximumBytes) throw new Error("Poly Haven response exceeds its byte bound.");
|
|
41
|
+
chunks.push(Buffer.from(next.value));
|
|
42
|
+
}
|
|
43
|
+
aborted(signal);
|
|
44
|
+
if (expectedBytes !== undefined && length !== expectedBytes) throw new Error("Poly Haven file length differs from its selected catalog entry.");
|
|
45
|
+
return Buffer.concat(chunks, length);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Cancellation of an injected or broken stream must not postpone failure.
|
|
48
|
+
void reader.cancel().catch(() => undefined);
|
|
49
|
+
throw error;
|
|
50
|
+
} finally { reader.releaseLock(); }
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async json(path: string, query?: URLSearchParams): Promise<unknown> {
|
|
54
|
+
if (!/^\/(?:search|info\/[a-z0-9][a-z0-9_-]{0,127}|files\/[a-z0-9][a-z0-9_-]{0,127})$/u.test(path)) throw new Error("Unsupported Poly Haven API endpoint.");
|
|
55
|
+
const url = `${POLY_HAVEN.api}${path}${query === undefined ? "" : `?${query.toString()}`}`;
|
|
56
|
+
const bytes = await read(url, ASSET_LIMITS.jsonBytes);
|
|
57
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown;
|
|
58
|
+
},
|
|
59
|
+
async file(url: string, bytes: number): Promise<Buffer> {
|
|
60
|
+
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > ASSET_LIMITS.fileBytes) throw new Error("Asset file exceeds its byte bound.");
|
|
61
|
+
return read(downloadUrl(url), bytes, bytes);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { StudioPathSchema } from "../../../../src/studio";
|
|
7
|
+
import { deepFreezeJson } from "../../../../src/code/json-snapshot";
|
|
8
|
+
import { createNodeBundleFileSystem } from "../../core/storage";
|
|
9
|
+
import { ensurePhysicalPrivateDirectoryWithin } from "../../cli/paths";
|
|
10
|
+
import { inventoryStudioFiles, readStudioBytes } from "../../cli/studio-files";
|
|
11
|
+
import { ASSET_LIMITS, AssetIdSchema, POLY_HAVEN, assetJson, captureAssetJson, capturePolyHavenSnapshots, describePolyHavenAsset, parseAssetSearch, parseAssetSelection, parsePolyHavenAssetPlan, planPolyHavenAsset, type AssetPlannedFile, type PolyHavenAssetPlan } from "./contracts";
|
|
12
|
+
import { createPolyHavenNetwork, type AssetFetch } from "./network";
|
|
13
|
+
|
|
14
|
+
const hash = (algorithm: "md5" | "sha256", bytes: Uint8Array): string => createHash(algorithm).update(bytes).digest("hex");
|
|
15
|
+
const absent = (error: unknown): boolean => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
16
|
+
export interface ImportedAssetFile extends AssetPlannedFile { readonly sha256: string }
|
|
17
|
+
export interface PolyHavenAssetReceipt {
|
|
18
|
+
readonly kind: "slopcamera.studio-asset-import"; readonly schemaVersion: 1; readonly provider: "poly-haven"; readonly planSha256: string;
|
|
19
|
+
readonly asset: PolyHavenAssetPlan["asset"]; readonly license: PolyHavenAssetPlan["license"]; readonly credit: string; readonly apiTermsUrl: string;
|
|
20
|
+
readonly files: readonly ImportedAssetFile[]; readonly totalBytes: number; readonly sourceDirectory: "source"; readonly verification: "catalog-md5-and-local-sha256";
|
|
21
|
+
}
|
|
22
|
+
function fileChecks(file: AssetPlannedFile, bytes: Buffer): void {
|
|
23
|
+
if (bytes.length !== file.bytes || hash("md5", bytes) !== file.md5) throw new Error("Downloaded asset differs from its selected catalog size or MD5.");
|
|
24
|
+
const extension = file.path.split(".").at(-1);
|
|
25
|
+
const valid = extension === "bin" || extension === "gltf" || extension === "png" && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
|
|
26
|
+
|| (extension === "jpg" || extension === "jpeg") && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255
|
|
27
|
+
|| extension === "exr" && bytes.subarray(0, 4).equals(Buffer.from([0x76, 0x2f, 0x31, 0x01]))
|
|
28
|
+
|| extension === "hdr" && /^(?:#\?RADIANCE|#\?RGBE)\r?\n/u.test(bytes.subarray(0, 32).toString("ascii"));
|
|
29
|
+
if (!valid) throw new Error("Asset file signature differs from its selected format.");
|
|
30
|
+
}
|
|
31
|
+
export function validateGltfAssetClosure(bytes: Uint8Array, planInput: unknown): void {
|
|
32
|
+
const plan = parsePolyHavenAssetPlan(planInput);
|
|
33
|
+
if (plan.selection.kind !== "model" || bytes.byteLength > ASSET_LIMITS.jsonBytes) throw new Error("glTF source exceeds the supported JSON profile.");
|
|
34
|
+
const raw = captureAssetJson(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
|
|
35
|
+
const document = z.object({ asset: z.object({ version: z.literal("2.0") }), buffers: z.array(z.object({ uri: z.string(), byteLength: z.number().int().positive() })).max(64).default([]), images: z.array(z.object({ uri: z.string() })).max(64).default([]), extensionsUsed: z.array(z.string()).max(32).default([]), extensionsRequired: z.array(z.string()).max(32).default([]) }).parse(raw);
|
|
36
|
+
const allowed = new Set(["KHR_materials_ior", "KHR_materials_specular", "KHR_materials_transmission", "KHR_materials_volume", "KHR_materials_clearcoat", "KHR_materials_sheen", "KHR_materials_emissive_strength", "KHR_materials_unlit", "KHR_texture_transform", "KHR_mesh_quantization"]);
|
|
37
|
+
if ([...document.extensionsUsed, ...document.extensionsRequired].some(name => !allowed.has(name))) throw new Error("glTF uses an extension outside the supported asset profile.");
|
|
38
|
+
function inspect(value: unknown, path: string): void {
|
|
39
|
+
if (Array.isArray(value)) { value.forEach((item, index) => inspect(item, `${path}/${index}`)); return; }
|
|
40
|
+
if (value === null || typeof value !== "object") return;
|
|
41
|
+
for (const [key, item] of Object.entries(value)) {
|
|
42
|
+
if (key === "uri" && !/^\/(?:images|buffers)\/\d+$/u.test(path)) throw new Error("glTF contains a resource outside its supported buffer/image closure.");
|
|
43
|
+
if (key === "extensions" && (item === null || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some(name => !allowed.has(name)))) throw new Error("glTF contains an undeclared unsupported extension.");
|
|
44
|
+
inspect(item, `${path}/${key}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
inspect(raw, "");
|
|
48
|
+
const actual = new Set<string>(), expected = plan.files.filter(file => file.role === "dependency");
|
|
49
|
+
for (const item of [...document.buffers, ...document.images]) {
|
|
50
|
+
if (/[?#\\:]/u.test(item.uri) || /%2f|%5c/iu.test(item.uri)) throw new Error("glTF resource references must be local explicit paths.");
|
|
51
|
+
const path = StudioPathSchema.parse(decodeURIComponent(item.uri));
|
|
52
|
+
const file = expected.find(file => file.path === path);
|
|
53
|
+
if (file === undefined || "byteLength" in item && item.byteLength !== file.bytes) throw new Error("glTF resource differs from its complete selected dependency closure.");
|
|
54
|
+
actual.add(path);
|
|
55
|
+
}
|
|
56
|
+
if (expected.some(file => !actual.has(file.path))) throw new Error("glTF catalog contains an unreferenced dependency outside the source closure.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createPolyHavenAssetService(options: { readonly storageRoot: string; readonly fetch?: AssetFetch; readonly signal?: AbortSignal; readonly beforePublication?: () => Promise<void> }) {
|
|
60
|
+
const network = createPolyHavenNetwork(options);
|
|
61
|
+
const storageRoot = options.storageRoot, cancellation = options.signal, beforePublication = options.beforePublication;
|
|
62
|
+
async function fence(): Promise<void> {
|
|
63
|
+
if (cancellation?.aborted) throw new Error("Poly Haven asset operation was cancelled.");
|
|
64
|
+
await beforePublication?.();
|
|
65
|
+
if (cancellation?.aborted) throw new Error("Poly Haven asset operation was cancelled.");
|
|
66
|
+
}
|
|
67
|
+
async function describe(assetIdInput: unknown) {
|
|
68
|
+
const assetId = AssetIdSchema.parse(captureAssetJson(assetIdInput));
|
|
69
|
+
const snapshots = capturePolyHavenSnapshots({ info: await network.json(`/info/${assetId}`), files: await network.json(`/files/${assetId}`) });
|
|
70
|
+
const asset = describePolyHavenAsset(assetId, snapshots.info);
|
|
71
|
+
return deepFreezeJson({ provider: POLY_HAVEN.provider, credit: POLY_HAVEN.credit, apiTermsUrl: POLY_HAVEN.termsUrl, asset, snapshots });
|
|
72
|
+
}
|
|
73
|
+
async function verify(directory: string, plan: PolyHavenAssetPlan, receiptInput?: unknown): Promise<PolyHavenAssetReceipt> {
|
|
74
|
+
const fs = createNodeBundleFileSystem(directory), files: ImportedAssetFile[] = [];
|
|
75
|
+
const inventory = await inventoryStudioFiles(join(directory, "source"), ASSET_LIMITS.files, plan.selection.maximumTotalBytes);
|
|
76
|
+
if (inventory.length !== plan.files.length) throw new Error("Retained asset source closure contains missing or undeclared files.");
|
|
77
|
+
for (const file of plan.files) {
|
|
78
|
+
const integrity = await fs.inspectFile!(`source/${file.path}`, file.bytes);
|
|
79
|
+
const bytes = await readStudioBytes(join(directory, "source", file.path), integrity, ASSET_LIMITS.fileBytes);
|
|
80
|
+
fileChecks(file, bytes);
|
|
81
|
+
if (file.role === "model") validateGltfAssetClosure(bytes, plan);
|
|
82
|
+
files.push({ ...file, sha256: integrity.sha256 });
|
|
83
|
+
}
|
|
84
|
+
const receipt: PolyHavenAssetReceipt = { kind: "slopcamera.studio-asset-import", schemaVersion: 1, provider: "poly-haven", planSha256: plan.planSha256, asset: plan.asset, license: plan.license, credit: plan.credit, apiTermsUrl: plan.apiTermsUrl, files, totalBytes: plan.totalBytes, sourceDirectory: "source", verification: "catalog-md5-and-local-sha256" };
|
|
85
|
+
if (receiptInput !== undefined && assetJson(captureAssetJson(receiptInput)) !== assetJson(receipt)) throw new Error("Retained asset receipt conflicts with its verified source files.");
|
|
86
|
+
await fence();
|
|
87
|
+
return deepFreezeJson(receipt);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
async search(input: unknown) {
|
|
91
|
+
const query = parseAssetSearch(input);
|
|
92
|
+
const result = z.object({ query: z.string().max(100), type: z.string(), total: z.number().int().nonnegative(), results: z.array(z.object({ slug: AssetIdSchema, score: z.number().finite().min(-1).max(1) })).max(20) }).parse(captureAssetJson(await network.json("/search", new URLSearchParams({ q: query.query, t: query.type, limit: String(query.limit) }))));
|
|
93
|
+
if (result.results.length > query.limit || result.query !== query.query || result.type !== query.type || new Set(result.results.map(item => item.slug)).size !== result.results.length) throw new Error("Poly Haven search differs from its bounded request.");
|
|
94
|
+
const items = [];
|
|
95
|
+
for (const resultItem of result.results) {
|
|
96
|
+
const asset = describePolyHavenAsset(resultItem.slug, await network.json(`/info/${resultItem.slug}`));
|
|
97
|
+
if (query.type !== "all" && asset.type !== query.type) throw new Error("Poly Haven search returned an asset outside the requested type.");
|
|
98
|
+
items.push({ ...asset, score: resultItem.score });
|
|
99
|
+
}
|
|
100
|
+
return deepFreezeJson({ provider: POLY_HAVEN.provider, credit: POLY_HAVEN.credit, apiTermsUrl: POLY_HAVEN.termsUrl, query, total: result.total, items });
|
|
101
|
+
},
|
|
102
|
+
describe,
|
|
103
|
+
async plan(input: unknown): Promise<PolyHavenAssetPlan> {
|
|
104
|
+
const selection = parseAssetSelection(input), found = await describe(selection.assetId);
|
|
105
|
+
return planPolyHavenAsset(selection, found.snapshots);
|
|
106
|
+
},
|
|
107
|
+
async importAsset(input: unknown) {
|
|
108
|
+
const plan = parsePolyHavenAssetPlan(input);
|
|
109
|
+
await fence();
|
|
110
|
+
const root = resolve(storageRoot), details = await lstat(root);
|
|
111
|
+
if (!details.isDirectory() || details.isSymbolicLink() || await realpath(root) !== root) throw new Error("Asset storage requires an existing physical root.");
|
|
112
|
+
const relative = `imports/${plan.planSha256}`, directory = await ensurePhysicalPrivateDirectoryWithin(root, relative), fs = createNodeBundleFileSystem(root);
|
|
113
|
+
await ensurePhysicalPrivateDirectoryWithin(directory, "source");
|
|
114
|
+
await fs.writeTextNoReplace!(`${relative}/plan.json`, assetJson(plan), fence);
|
|
115
|
+
if (await fs.readText(`${relative}/plan.json`) !== assetJson(plan)) throw new Error("Retained asset plan conflicts with the requested plan.");
|
|
116
|
+
const retained = await fs.readText(`${relative}/receipt.json`).catch(error => { if (absent(error)) return undefined; throw error; });
|
|
117
|
+
if (retained !== undefined) {
|
|
118
|
+
const receipt = await verify(directory, plan, JSON.parse(retained));
|
|
119
|
+
return deepFreezeJson({ disposition: "reused" as const, receipt, receiptPath: `${relative}/receipt.json`, sourceRoot: join(directory, "source"), receiptSha256: hash("sha256", Buffer.from(assetJson(receipt))) });
|
|
120
|
+
}
|
|
121
|
+
for (const file of [...plan.files].sort((a, b) => Number(b.role === "model") - Number(a.role === "model"))) {
|
|
122
|
+
await fence();
|
|
123
|
+
const destination = `${relative}/source/${file.path}`;
|
|
124
|
+
const existing = await fs.inspectFile!(destination, file.bytes).catch(error => { if (absent(error)) return undefined; throw error; });
|
|
125
|
+
if (existing !== undefined) {
|
|
126
|
+
const bytes = await readStudioBytes(join(root, destination), existing, ASSET_LIMITS.fileBytes);
|
|
127
|
+
fileChecks(file, bytes);
|
|
128
|
+
if (file.role === "model") validateGltfAssetClosure(bytes, plan);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const bytes = await network.file(file.url, file.bytes);
|
|
132
|
+
fileChecks(file, bytes);
|
|
133
|
+
if (file.role === "model") validateGltfAssetClosure(bytes, plan);
|
|
134
|
+
const staging = `staging/${randomUUID()}`, stage = await ensurePhysicalPrivateDirectoryWithin(root, staging);
|
|
135
|
+
const handle = await open(join(stage, "download"), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
136
|
+
try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
|
|
137
|
+
// Keep the exact downloaded stage as recoverable evidence. The source copy
|
|
138
|
+
// uses shared inode-pinned, guarded, atomic no-replace publication.
|
|
139
|
+
await fs.copyFileNoReplace!(`${staging}/download`, destination, { bytes: bytes.length, sha256: hash("sha256", bytes) }, fence);
|
|
140
|
+
}
|
|
141
|
+
const receipt = await verify(directory, plan);
|
|
142
|
+
await fs.writeTextNoReplace!(`${relative}/receipt.json`, assetJson(receipt), fence);
|
|
143
|
+
const winner = JSON.parse(await fs.readText(`${relative}/receipt.json`)) as unknown;
|
|
144
|
+
const verified = await verify(directory, plan, winner);
|
|
145
|
+
return deepFreezeJson({ disposition: "created" as const, receipt: verified, receiptPath: `${relative}/receipt.json`, sourceRoot: join(directory, "source"), receiptSha256: hash("sha256", Buffer.from(assetJson(verified))) });
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|