@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,323 @@
|
|
|
1
|
+
import { constants } from "node:fs"
|
|
2
|
+
import { open, realpath, type FileHandle } from "node:fs/promises"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
import sharp from "sharp"
|
|
5
|
+
import { type VectorizeDeadline } from "./limits.js"
|
|
6
|
+
import { sha256 } from "./metrics.js"
|
|
7
|
+
import { VectorizeError, type VectorizeInput, type VectorizeLimits } from "./types.js"
|
|
8
|
+
|
|
9
|
+
const allowedFormats = new Set(["avif", "gif", "heif", "jpeg", "png", "tiff", "webp"])
|
|
10
|
+
const METRIC_MAX_EDGE = 512
|
|
11
|
+
export const VECTORIZE_SHARP_CONCURRENCY = 1
|
|
12
|
+
|
|
13
|
+
export function configureVectorizeSharpConcurrency(): void {
|
|
14
|
+
// Each isolated vectorizer declares one host CPU unit. Bind libvips to the
|
|
15
|
+
// same budget inside that worker without mutating an SDK caller's Sharp pool.
|
|
16
|
+
const actual = sharp.concurrency(VECTORIZE_SHARP_CONCURRENCY)
|
|
17
|
+
if (actual !== VECTORIZE_SHARP_CONCURRENCY) {
|
|
18
|
+
throw new VectorizeError(
|
|
19
|
+
"trace_failed",
|
|
20
|
+
"The vectorization worker could not bind its Sharp CPU budget.",
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LoadedRaster {
|
|
26
|
+
readonly bytes: Uint8Array
|
|
27
|
+
readonly format: string
|
|
28
|
+
readonly height: number
|
|
29
|
+
readonly inputBytes: number
|
|
30
|
+
readonly pixels: Uint8Array
|
|
31
|
+
readonly scoreHeight: number
|
|
32
|
+
readonly scorePixels: Uint8Array
|
|
33
|
+
readonly scoreWidth: number
|
|
34
|
+
readonly sourceSha256: string
|
|
35
|
+
readonly width: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function loadRaster(
|
|
39
|
+
input: VectorizeInput,
|
|
40
|
+
limits: Readonly<VectorizeLimits>,
|
|
41
|
+
deadline: VectorizeDeadline,
|
|
42
|
+
): Promise<LoadedRaster> {
|
|
43
|
+
deadline.assert("input read")
|
|
44
|
+
const bytes = await readInputBytes(input, limits.maxInputBytes, deadline)
|
|
45
|
+
deadline.assert("input metadata")
|
|
46
|
+
try {
|
|
47
|
+
const metadata = await sharp(bytes, {
|
|
48
|
+
failOn: "error",
|
|
49
|
+
limitInputPixels: limits.maxDecodedPixels,
|
|
50
|
+
sequentialRead: true,
|
|
51
|
+
}).metadata()
|
|
52
|
+
const format = metadata.format
|
|
53
|
+
if (format === undefined || !allowedFormats.has(format)) {
|
|
54
|
+
throw new VectorizeError(
|
|
55
|
+
"invalid_input",
|
|
56
|
+
`Expected a supported raster image, received ${format ?? "an unknown format"}.`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
if ((metadata.pages ?? 1) !== 1) {
|
|
60
|
+
throw new VectorizeError("invalid_input", "Animated and multipage raster inputs are rejected.")
|
|
61
|
+
}
|
|
62
|
+
if (
|
|
63
|
+
metadata.width === undefined ||
|
|
64
|
+
metadata.height === undefined ||
|
|
65
|
+
metadata.width < 1 ||
|
|
66
|
+
metadata.height < 1
|
|
67
|
+
) {
|
|
68
|
+
throw new VectorizeError("invalid_input", "Raster dimensions are missing or invalid.")
|
|
69
|
+
}
|
|
70
|
+
assertDimensions(metadata.width, metadata.height, limits)
|
|
71
|
+
|
|
72
|
+
const decoded = await sharp(bytes, {
|
|
73
|
+
failOn: "error",
|
|
74
|
+
limitInputPixels: limits.maxDecodedPixels,
|
|
75
|
+
sequentialRead: true,
|
|
76
|
+
})
|
|
77
|
+
.rotate()
|
|
78
|
+
.ensureAlpha()
|
|
79
|
+
.raw()
|
|
80
|
+
.toBuffer({ resolveWithObject: true })
|
|
81
|
+
const { channels, height, width } = decoded.info
|
|
82
|
+
if (channels !== 4) {
|
|
83
|
+
throw new VectorizeError("invalid_input", "Raster decoding did not produce RGBA pixels.")
|
|
84
|
+
}
|
|
85
|
+
assertDimensions(width, height, limits)
|
|
86
|
+
const pixels = Uint8Array.from(decoded.data)
|
|
87
|
+
if (!containsVisiblePixel(pixels)) {
|
|
88
|
+
throw new VectorizeError("invalid_input", "A fully transparent image cannot be vectorized.")
|
|
89
|
+
}
|
|
90
|
+
deadline.assert("raster decode")
|
|
91
|
+
|
|
92
|
+
const scoreScale = Math.min(1, METRIC_MAX_EDGE / Math.max(width, height))
|
|
93
|
+
const scoreWidth = Math.max(1, Math.round(width * scoreScale))
|
|
94
|
+
const scoreHeight = Math.max(1, Math.round(height * scoreScale))
|
|
95
|
+
const scorePixels =
|
|
96
|
+
scoreWidth === width && scoreHeight === height
|
|
97
|
+
? pixels
|
|
98
|
+
: Uint8Array.from(
|
|
99
|
+
await sharp(pixels, { raw: { channels: 4, height, width } })
|
|
100
|
+
.resize(scoreWidth, scoreHeight, { fit: "fill", kernel: "lanczos3" })
|
|
101
|
+
.raw()
|
|
102
|
+
.toBuffer(),
|
|
103
|
+
)
|
|
104
|
+
deadline.assert("metric sample")
|
|
105
|
+
return {
|
|
106
|
+
bytes,
|
|
107
|
+
format,
|
|
108
|
+
height,
|
|
109
|
+
inputBytes: bytes.byteLength,
|
|
110
|
+
pixels,
|
|
111
|
+
scoreHeight,
|
|
112
|
+
scorePixels,
|
|
113
|
+
scoreWidth,
|
|
114
|
+
sourceSha256: sha256(bytes),
|
|
115
|
+
width,
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error instanceof VectorizeError) throw error
|
|
119
|
+
throw new VectorizeError("invalid_input", "Raster input could not be decoded safely.", {}, {
|
|
120
|
+
cause: error,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readInputBytes(
|
|
126
|
+
input: VectorizeInput,
|
|
127
|
+
maximumBytes: number,
|
|
128
|
+
deadline: VectorizeDeadline,
|
|
129
|
+
): Promise<Uint8Array> {
|
|
130
|
+
if (typeof input === "string") {
|
|
131
|
+
return readRegularInput(resolve(input), maximumBytes, deadline)
|
|
132
|
+
}
|
|
133
|
+
const view = input instanceof ArrayBuffer ? new Uint8Array(input) : input
|
|
134
|
+
if (view.byteLength === 0) {
|
|
135
|
+
throw new VectorizeError("invalid_input", "Raster input is empty.")
|
|
136
|
+
}
|
|
137
|
+
if (view.byteLength > maximumBytes) {
|
|
138
|
+
throw new VectorizeError(
|
|
139
|
+
"input_limit",
|
|
140
|
+
`Raster input exceeds the ${maximumBytes}-byte limit.`,
|
|
141
|
+
{ bytes: view.byteLength, maximumBytes },
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
deadline.assert("input read")
|
|
145
|
+
const bytes = Uint8Array.from(view)
|
|
146
|
+
deadline.assert("input read")
|
|
147
|
+
return bytes
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function readRegularInput(
|
|
151
|
+
path: string,
|
|
152
|
+
maximumBytes: number,
|
|
153
|
+
deadline: VectorizeDeadline,
|
|
154
|
+
): Promise<Uint8Array> {
|
|
155
|
+
let handle: FileHandle | undefined
|
|
156
|
+
try {
|
|
157
|
+
deadline.assert("input read")
|
|
158
|
+
const targetPath = await realpath(path)
|
|
159
|
+
deadline.assert("input read")
|
|
160
|
+
handle = await open(targetPath, boundedReadFlags())
|
|
161
|
+
const metadata = await handle.stat()
|
|
162
|
+
if (!metadata.isFile()) {
|
|
163
|
+
throw new VectorizeError("invalid_input", `Raster input is not a file: ${path}`)
|
|
164
|
+
}
|
|
165
|
+
if (metadata.size < 1) {
|
|
166
|
+
throw new VectorizeError("invalid_input", "Raster input is empty.")
|
|
167
|
+
}
|
|
168
|
+
if (metadata.size > maximumBytes) {
|
|
169
|
+
throw new VectorizeError(
|
|
170
|
+
"input_limit",
|
|
171
|
+
`Raster input exceeds the ${maximumBytes}-byte limit.`,
|
|
172
|
+
{ bytes: metadata.size, maximumBytes },
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1_024, maximumBytes + 1))
|
|
177
|
+
const chunks: Buffer[] = []
|
|
178
|
+
let bytes = 0
|
|
179
|
+
while (true) {
|
|
180
|
+
deadline.assert("input read")
|
|
181
|
+
const maximumRead = Math.min(chunk.byteLength, maximumBytes - bytes + 1)
|
|
182
|
+
const { bytesRead } = await handle.read(chunk, 0, maximumRead, null)
|
|
183
|
+
if (bytesRead === 0) break
|
|
184
|
+
bytes += bytesRead
|
|
185
|
+
if (bytes > maximumBytes) {
|
|
186
|
+
throw new VectorizeError(
|
|
187
|
+
"input_limit",
|
|
188
|
+
`Raster input exceeds the ${maximumBytes}-byte limit.`,
|
|
189
|
+
{ bytes, maximumBytes },
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
chunks.push(Buffer.from(chunk.subarray(0, bytesRead)))
|
|
193
|
+
}
|
|
194
|
+
if (bytes === 0) {
|
|
195
|
+
throw new VectorizeError("invalid_input", "Raster input is empty.")
|
|
196
|
+
}
|
|
197
|
+
deadline.assert("input read")
|
|
198
|
+
return Buffer.concat(chunks, bytes)
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error instanceof VectorizeError) throw error
|
|
201
|
+
throw new VectorizeError(
|
|
202
|
+
"invalid_input",
|
|
203
|
+
"Raster input could not be read safely.",
|
|
204
|
+
{},
|
|
205
|
+
{ cause: error },
|
|
206
|
+
)
|
|
207
|
+
} finally {
|
|
208
|
+
await handle?.close().catch(() => undefined)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function assertDimensions(
|
|
213
|
+
width: number,
|
|
214
|
+
height: number,
|
|
215
|
+
limits: Readonly<VectorizeLimits>,
|
|
216
|
+
): void {
|
|
217
|
+
if (
|
|
218
|
+
!Number.isInteger(width) ||
|
|
219
|
+
!Number.isInteger(height) ||
|
|
220
|
+
width < 1 ||
|
|
221
|
+
height < 1 ||
|
|
222
|
+
width > limits.maxDimension ||
|
|
223
|
+
height > limits.maxDimension ||
|
|
224
|
+
width * height > limits.maxDecodedPixels
|
|
225
|
+
) {
|
|
226
|
+
throw new VectorizeError(
|
|
227
|
+
"input_limit",
|
|
228
|
+
`Raster dimensions must fit ${limits.maxDimension}px and ${limits.maxDecodedPixels} decoded pixels.`,
|
|
229
|
+
{ height, width },
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function containsVisiblePixel(rgba: Uint8Array): boolean {
|
|
235
|
+
for (let index = 3; index < rgba.length; index += 4) {
|
|
236
|
+
if (rgba[index]! > 0) return true
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function boundedReadFlags(): number {
|
|
242
|
+
if (process.platform === "win32") return constants.O_RDONLY
|
|
243
|
+
return constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function encodeTracePng(
|
|
247
|
+
pixels: Uint8Array,
|
|
248
|
+
width: number,
|
|
249
|
+
height: number,
|
|
250
|
+
): Promise<Uint8Array> {
|
|
251
|
+
return Uint8Array.from(
|
|
252
|
+
await sharp(pixels, { raw: { channels: 4, height, width } })
|
|
253
|
+
.png({ adaptiveFiltering: false, compressionLevel: 9, palette: false })
|
|
254
|
+
.toBuffer(),
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function renderSvgRgba(
|
|
259
|
+
svg: string,
|
|
260
|
+
width: number,
|
|
261
|
+
height: number,
|
|
262
|
+
maxDecodedPixels: number,
|
|
263
|
+
): Promise<Uint8Array> {
|
|
264
|
+
try {
|
|
265
|
+
return Uint8Array.from(
|
|
266
|
+
await sharp(Buffer.from(svg), {
|
|
267
|
+
density: 72,
|
|
268
|
+
failOn: "error",
|
|
269
|
+
limitInputPixels: maxDecodedPixels,
|
|
270
|
+
})
|
|
271
|
+
.resize(width, height, { fit: "fill" })
|
|
272
|
+
.ensureAlpha()
|
|
273
|
+
.raw()
|
|
274
|
+
.toBuffer(),
|
|
275
|
+
)
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw new VectorizeError(
|
|
278
|
+
"trace_failed",
|
|
279
|
+
"Canonical SVG could not be rendered for quality measurement.",
|
|
280
|
+
{},
|
|
281
|
+
{ cause: error },
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function sharpProvenance(): Readonly<{
|
|
287
|
+
sharp: string
|
|
288
|
+
sharpVersions: Readonly<Record<string, string>>
|
|
289
|
+
vips: string
|
|
290
|
+
}> {
|
|
291
|
+
const sharpVersions = normalizedPixelToolchain(sharp.versions)
|
|
292
|
+
const sharpVersion = sharpVersions.sharp
|
|
293
|
+
const vipsVersion = sharpVersions.vips
|
|
294
|
+
if (sharpVersion === undefined || vipsVersion === undefined) {
|
|
295
|
+
throw new VectorizeError(
|
|
296
|
+
"tool_version",
|
|
297
|
+
"Sharp must report its own and libvips version metadata.",
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
sharp: sharpVersion,
|
|
302
|
+
sharpVersions,
|
|
303
|
+
vips: vipsVersion,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function normalizedPixelToolchain(
|
|
308
|
+
versions: Readonly<Record<string, string | undefined>>,
|
|
309
|
+
): Readonly<Record<string, string>> {
|
|
310
|
+
const entries = Object.entries(versions)
|
|
311
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
312
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
313
|
+
if (
|
|
314
|
+
entries.length === 0 ||
|
|
315
|
+
entries.some(([name, version]) => name.length === 0 || version.length === 0)
|
|
316
|
+
) {
|
|
317
|
+
throw new VectorizeError(
|
|
318
|
+
"tool_version",
|
|
319
|
+
"Pixel toolchain versions must be nonempty strings.",
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
return Object.freeze(Object.fromEntries(entries))
|
|
323
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url"
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { dirname, join } from "node:path"
|
|
5
|
+
import { runBoundedCommand } from "./command.js"
|
|
6
|
+
import { resolveVectorizeLimits } from "./limits.js"
|
|
7
|
+
import {
|
|
8
|
+
MAX_VECTORIZE_REQUEST_BYTES,
|
|
9
|
+
MAX_VECTORIZE_RESPONSE_BYTES,
|
|
10
|
+
VECTORIZE_WORKER_PROTOCOL,
|
|
11
|
+
type VectorizeWorkerRequest,
|
|
12
|
+
type VectorizeWorkerResponse,
|
|
13
|
+
} from "./worker-protocol.js"
|
|
14
|
+
import {
|
|
15
|
+
VectorizeError,
|
|
16
|
+
vectorizeProfileNames,
|
|
17
|
+
type VectorizeErrorCode,
|
|
18
|
+
type VectorizeInput,
|
|
19
|
+
type VectorizeOptions,
|
|
20
|
+
type VectorizeResult,
|
|
21
|
+
} from "./types.js"
|
|
22
|
+
|
|
23
|
+
const vectorizeErrorCodes = new Set<VectorizeErrorCode>([
|
|
24
|
+
"input_limit",
|
|
25
|
+
"invalid_input",
|
|
26
|
+
"output_limit",
|
|
27
|
+
"quality_limit",
|
|
28
|
+
"timeout",
|
|
29
|
+
"tool_download",
|
|
30
|
+
"tool_integrity",
|
|
31
|
+
"tool_platform",
|
|
32
|
+
"tool_version",
|
|
33
|
+
"trace_failed",
|
|
34
|
+
"unsafe_svg",
|
|
35
|
+
])
|
|
36
|
+
const WORKER_SHUTDOWN_RESERVE_MS = 250
|
|
37
|
+
const WORKER_RESPONSE_RESERVE_MS = 100
|
|
38
|
+
|
|
39
|
+
export async function runVectorizeWorker(
|
|
40
|
+
input: VectorizeInput,
|
|
41
|
+
options: VectorizeOptions,
|
|
42
|
+
): Promise<VectorizeResult> {
|
|
43
|
+
const startedAt = performance.now()
|
|
44
|
+
const limits = resolveVectorizeLimits(options.limits)
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
throw new VectorizeError(
|
|
47
|
+
"tool_platform",
|
|
48
|
+
"Bounded VTracer streaming is unavailable on Windows.",
|
|
49
|
+
{ platform: process.platform },
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
const workerInput = encodeInput(input, limits.maxInputBytes)
|
|
53
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), "slopcamera-vectorize-"))
|
|
54
|
+
let result: VectorizeResult
|
|
55
|
+
try {
|
|
56
|
+
result = await executeVectorizeWorker(
|
|
57
|
+
workerInput,
|
|
58
|
+
options,
|
|
59
|
+
limits,
|
|
60
|
+
startedAt,
|
|
61
|
+
temporaryRoot,
|
|
62
|
+
)
|
|
63
|
+
} finally {
|
|
64
|
+
await removeTemporaryRoot(temporaryRoot)
|
|
65
|
+
}
|
|
66
|
+
if (performance.now() - startedAt >= limits.maxDurationMs) {
|
|
67
|
+
throw new VectorizeError(
|
|
68
|
+
"timeout",
|
|
69
|
+
"Vectorization exceeded the conversion time limit during cleanup.",
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function executeVectorizeWorker(
|
|
76
|
+
workerInput: VectorizeWorkerRequest["input"],
|
|
77
|
+
options: VectorizeOptions,
|
|
78
|
+
limits: ReturnType<typeof resolveVectorizeLimits>,
|
|
79
|
+
startedAt: number,
|
|
80
|
+
temporaryRoot: string,
|
|
81
|
+
): Promise<VectorizeResult> {
|
|
82
|
+
const preparationRemainingMs =
|
|
83
|
+
limits.maxDurationMs - (performance.now() - startedAt)
|
|
84
|
+
if (
|
|
85
|
+
preparationRemainingMs <=
|
|
86
|
+
WORKER_SHUTDOWN_RESERVE_MS + WORKER_RESPONSE_RESERVE_MS
|
|
87
|
+
) {
|
|
88
|
+
throw new VectorizeError(
|
|
89
|
+
"timeout",
|
|
90
|
+
"Vectorization has no remaining budget for isolated worker execution.",
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
const workerDurationMs = Math.floor(
|
|
94
|
+
preparationRemainingMs -
|
|
95
|
+
WORKER_SHUTDOWN_RESERVE_MS -
|
|
96
|
+
WORKER_RESPONSE_RESERVE_MS,
|
|
97
|
+
)
|
|
98
|
+
const inheritedFileDescriptors = inheritedDescriptors(
|
|
99
|
+
options.inheritedFileDescriptors,
|
|
100
|
+
)
|
|
101
|
+
const workerInheritedFileDescriptors = inheritedFileDescriptors.map(
|
|
102
|
+
(_descriptor, index) => index + 3,
|
|
103
|
+
)
|
|
104
|
+
const request: VectorizeWorkerRequest = {
|
|
105
|
+
input: workerInput,
|
|
106
|
+
options: cloneOptions(
|
|
107
|
+
options,
|
|
108
|
+
workerDurationMs,
|
|
109
|
+
workerInheritedFileDescriptors,
|
|
110
|
+
),
|
|
111
|
+
protocol: VECTORIZE_WORKER_PROTOCOL,
|
|
112
|
+
temporaryRoot,
|
|
113
|
+
}
|
|
114
|
+
const requestBytes = Buffer.from(JSON.stringify(request))
|
|
115
|
+
if (requestBytes.byteLength > MAX_VECTORIZE_REQUEST_BYTES) {
|
|
116
|
+
throw new VectorizeError(
|
|
117
|
+
"input_limit",
|
|
118
|
+
"The vectorization worker request exceeds its IPC limit.",
|
|
119
|
+
{
|
|
120
|
+
bytes: requestBytes.byteLength,
|
|
121
|
+
maximumBytes: MAX_VECTORIZE_REQUEST_BYTES,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
const remainingMs = limits.maxDurationMs - (performance.now() - startedAt)
|
|
126
|
+
if (remainingMs <= WORKER_SHUTDOWN_RESERVE_MS) {
|
|
127
|
+
throw new VectorizeError(
|
|
128
|
+
"timeout",
|
|
129
|
+
"Vectorization has no remaining budget for isolated worker startup and cleanup.",
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { stdout } = await runBoundedCommand(
|
|
134
|
+
[process.execPath, workerEntryPath()],
|
|
135
|
+
Math.floor(remainingMs - WORKER_SHUTDOWN_RESERVE_MS),
|
|
136
|
+
"trace_failed",
|
|
137
|
+
{
|
|
138
|
+
...(inheritedFileDescriptors.length === 0
|
|
139
|
+
? {}
|
|
140
|
+
: { inheritedFileDescriptors }),
|
|
141
|
+
maxStdoutBytes: MAX_VECTORIZE_RESPONSE_BYTES,
|
|
142
|
+
stdin: requestBytes,
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
if (performance.now() - startedAt >= limits.maxDurationMs) {
|
|
146
|
+
throw new VectorizeError(
|
|
147
|
+
"timeout",
|
|
148
|
+
"Vectorization exceeded the conversion time limit.",
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
const response = parseResponse(stdout)
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new VectorizeError(
|
|
154
|
+
response.error.code,
|
|
155
|
+
response.error.message,
|
|
156
|
+
response.error.details,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
assertResult(response.result, limits.maxOutputBytes)
|
|
160
|
+
if (performance.now() - startedAt >= limits.maxDurationMs) {
|
|
161
|
+
throw new VectorizeError(
|
|
162
|
+
"timeout",
|
|
163
|
+
"Vectorization exceeded the conversion time limit.",
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
return response.result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function removeTemporaryRoot(temporaryRoot: string): Promise<void> {
|
|
170
|
+
try {
|
|
171
|
+
await rm(temporaryRoot, { force: true, recursive: true })
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new VectorizeError(
|
|
174
|
+
"trace_failed",
|
|
175
|
+
"The isolated vectorization directory could not be removed.",
|
|
176
|
+
{ temporaryRoot },
|
|
177
|
+
{ cause: error },
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function encodeInput(
|
|
183
|
+
input: VectorizeInput,
|
|
184
|
+
maximumInputBytes: number,
|
|
185
|
+
): VectorizeWorkerRequest["input"] {
|
|
186
|
+
return typeof input === "string"
|
|
187
|
+
? { kind: "path", value: input }
|
|
188
|
+
: encodeBytes(input, maximumInputBytes)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function encodeBytes(
|
|
192
|
+
input: Uint8Array | ArrayBuffer,
|
|
193
|
+
maximumInputBytes: number,
|
|
194
|
+
): Readonly<{ kind: "bytes"; value: string }> {
|
|
195
|
+
const view = input instanceof ArrayBuffer ? new Uint8Array(input) : input
|
|
196
|
+
if (view.byteLength < 1) {
|
|
197
|
+
throw new VectorizeError("invalid_input", "Raster input is empty.")
|
|
198
|
+
}
|
|
199
|
+
if (view.byteLength > maximumInputBytes) {
|
|
200
|
+
throw new VectorizeError(
|
|
201
|
+
"input_limit",
|
|
202
|
+
`Raster input exceeds the ${maximumInputBytes}-byte limit.`,
|
|
203
|
+
{ bytes: view.byteLength, maximumBytes: maximumInputBytes },
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
kind: "bytes",
|
|
208
|
+
value: Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString("base64"),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cloneOptions(
|
|
213
|
+
options: VectorizeOptions,
|
|
214
|
+
workerDurationMs: number,
|
|
215
|
+
inheritedFileDescriptors: readonly number[],
|
|
216
|
+
): VectorizeOptions {
|
|
217
|
+
return {
|
|
218
|
+
...(options.alphaCutoff === undefined ? {} : { alphaCutoff: options.alphaCutoff }),
|
|
219
|
+
...(options.cacheDirectory === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { cacheDirectory: options.cacheDirectory }),
|
|
222
|
+
...(options.duotone === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: { duotone: [options.duotone[0], options.duotone[1]] as const }),
|
|
225
|
+
...(inheritedFileDescriptors.length === 0
|
|
226
|
+
? {}
|
|
227
|
+
: { inheritedFileDescriptors }),
|
|
228
|
+
limits: {
|
|
229
|
+
...options.limits,
|
|
230
|
+
maxDurationMs: workerDurationMs,
|
|
231
|
+
},
|
|
232
|
+
...(options.outputPath === undefined ? {} : { outputPath: options.outputPath }),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function inheritedDescriptors(
|
|
237
|
+
value: readonly number[] | undefined,
|
|
238
|
+
): readonly number[] {
|
|
239
|
+
const descriptors = value ?? []
|
|
240
|
+
if (
|
|
241
|
+
descriptors.length > 16
|
|
242
|
+
|| descriptors.some((descriptor, index) => (
|
|
243
|
+
!Number.isSafeInteger(descriptor)
|
|
244
|
+
|| descriptor < 0
|
|
245
|
+
|| descriptor > 2_147_483_647
|
|
246
|
+
|| descriptors.indexOf(descriptor) !== index
|
|
247
|
+
))
|
|
248
|
+
) {
|
|
249
|
+
throw new VectorizeError(
|
|
250
|
+
"invalid_input",
|
|
251
|
+
"Inherited vectorizer descriptors must be unique bounded integers.",
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
return Object.freeze([...descriptors])
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function workerEntryPath(): string {
|
|
258
|
+
const modulePath = fileURLToPath(import.meta.url)
|
|
259
|
+
return modulePath.endsWith(".ts")
|
|
260
|
+
? join(dirname(modulePath), "worker.ts")
|
|
261
|
+
: join(dirname(modulePath), "vectorize", "worker.js")
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseResponse(stdout: string): VectorizeWorkerResponse {
|
|
265
|
+
let parsed: unknown
|
|
266
|
+
try {
|
|
267
|
+
parsed = JSON.parse(stdout)
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new VectorizeError(
|
|
270
|
+
"trace_failed",
|
|
271
|
+
"The vectorization worker returned malformed output.",
|
|
272
|
+
{},
|
|
273
|
+
{ cause: error },
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
if (!isRecord(parsed) || parsed.protocol !== VECTORIZE_WORKER_PROTOCOL) {
|
|
277
|
+
throw new VectorizeError(
|
|
278
|
+
"trace_failed",
|
|
279
|
+
"The vectorization worker returned an incompatible response.",
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
if (parsed.ok === true && isVectorizeResult(parsed.result)) {
|
|
283
|
+
return parsed as unknown as VectorizeWorkerResponse
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
parsed.ok === false &&
|
|
287
|
+
isRecord(parsed.error) &&
|
|
288
|
+
typeof parsed.error.code === "string" &&
|
|
289
|
+
vectorizeErrorCodes.has(parsed.error.code as VectorizeErrorCode) &&
|
|
290
|
+
typeof parsed.error.message === "string" &&
|
|
291
|
+
isRecord(parsed.error.details)
|
|
292
|
+
) {
|
|
293
|
+
return parsed as unknown as VectorizeWorkerResponse
|
|
294
|
+
}
|
|
295
|
+
throw new VectorizeError(
|
|
296
|
+
"trace_failed",
|
|
297
|
+
"The vectorization worker returned an invalid response.",
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function assertResult(result: VectorizeResult, maximumOutputBytes: number): void {
|
|
302
|
+
const bytes = Buffer.byteLength(result.svg)
|
|
303
|
+
if (
|
|
304
|
+
bytes < 1 ||
|
|
305
|
+
bytes > maximumOutputBytes ||
|
|
306
|
+
result.receipt.bytes !== bytes ||
|
|
307
|
+
result.receipt.svgSha256.length !== 64
|
|
308
|
+
) {
|
|
309
|
+
throw new VectorizeError(
|
|
310
|
+
"trace_failed",
|
|
311
|
+
"The vectorization worker response violates its output contract.",
|
|
312
|
+
{ bytes, maximumOutputBytes },
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isVectorizeResult(value: unknown): value is VectorizeResult {
|
|
318
|
+
if (
|
|
319
|
+
!isRecord(value) ||
|
|
320
|
+
(value.outputPath !== null && typeof value.outputPath !== "string") ||
|
|
321
|
+
typeof value.svg !== "string" ||
|
|
322
|
+
!isRecord(value.receipt)
|
|
323
|
+
) {
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
326
|
+
const profile = value.receipt.profile
|
|
327
|
+
return (
|
|
328
|
+
typeof value.receipt.bytes === "number" &&
|
|
329
|
+
typeof value.receipt.svgSha256 === "string" &&
|
|
330
|
+
typeof profile === "string" &&
|
|
331
|
+
vectorizeProfileNames.includes(
|
|
332
|
+
profile as (typeof vectorizeProfileNames)[number],
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
339
|
+
}
|