@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,885 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants, type BigIntStats, type Stats } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
type FileHandle,
|
|
5
|
+
lstat,
|
|
6
|
+
open,
|
|
7
|
+
realpath,
|
|
8
|
+
} from "node:fs/promises";
|
|
9
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { Effect, Exit, Option, Ref } from "effect";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AudioEffectsTransformV1Schema,
|
|
15
|
+
ColorGradeTransformV1Schema,
|
|
16
|
+
type AudioEffect,
|
|
17
|
+
type AudioEffectsTransformV1,
|
|
18
|
+
type ColorGradeControls,
|
|
19
|
+
type ColorGradeTransformV1,
|
|
20
|
+
} from "../contracts";
|
|
21
|
+
import {
|
|
22
|
+
operationBoundary,
|
|
23
|
+
operationResource,
|
|
24
|
+
operationValidation,
|
|
25
|
+
runStandaloneOperation,
|
|
26
|
+
type OperationEffectFailure,
|
|
27
|
+
} from "../application/operation-effects";
|
|
28
|
+
import { executeAtomicRenderEffect, type AtomicRenderOutput } from "./atomic-render-effects";
|
|
29
|
+
import { AtomicRenderPlatformLive } from "./atomic-render-platform";
|
|
30
|
+
import { CliError } from "./errors";
|
|
31
|
+
import type { ProcessRunner, RunOptions } from "./io";
|
|
32
|
+
import { SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS } from "./media-ingest";
|
|
33
|
+
|
|
34
|
+
const MAXIMUM_PATH_BYTES = 8_192;
|
|
35
|
+
const MAXIMUM_LOCAL_MEDIA_EFFECT_INPUT_BYTES = 512 * 1024 * 1024 * 1024;
|
|
36
|
+
export const MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
|
|
37
|
+
const LOCAL_MEDIA_EFFECT_TIMEOUT_MS = 12 * 60 * 60_000;
|
|
38
|
+
const PINNED_CHILD_INPUT_DESCRIPTOR = 3;
|
|
39
|
+
const PINNED_CHILD_INPUT_PATH = `/dev/fd/${PINNED_CHILD_INPUT_DESCRIPTOR}`;
|
|
40
|
+
|
|
41
|
+
const REVERB_TAPS = {
|
|
42
|
+
"small-room": [
|
|
43
|
+
{ delayMs: 29, weight: 0.5 },
|
|
44
|
+
{ delayMs: 47, weight: 0.3 },
|
|
45
|
+
{ delayMs: 71, weight: 0.2 },
|
|
46
|
+
],
|
|
47
|
+
"medium-room": [
|
|
48
|
+
{ delayMs: 43, weight: 0.4 },
|
|
49
|
+
{ delayMs: 67, weight: 0.3 },
|
|
50
|
+
{ delayMs: 101, weight: 0.2 },
|
|
51
|
+
{ delayMs: 149, weight: 0.1 },
|
|
52
|
+
],
|
|
53
|
+
"large-hall": [
|
|
54
|
+
{ delayMs: 61, weight: 0.35 },
|
|
55
|
+
{ delayMs: 113, weight: 0.27 },
|
|
56
|
+
{ delayMs: 173, weight: 0.2 },
|
|
57
|
+
{ delayMs: 257, weight: 0.12 },
|
|
58
|
+
{ delayMs: 347, weight: 0.06 },
|
|
59
|
+
],
|
|
60
|
+
plate: [
|
|
61
|
+
{ delayMs: 23, weight: 0.28 },
|
|
62
|
+
{ delayMs: 37, weight: 0.24 },
|
|
63
|
+
{ delayMs: 53, weight: 0.2 },
|
|
64
|
+
{ delayMs: 79, weight: 0.16 },
|
|
65
|
+
{ delayMs: 113, weight: 0.12 },
|
|
66
|
+
],
|
|
67
|
+
} as const satisfies Readonly<Record<
|
|
68
|
+
Extract<AudioEffect, { readonly kind: "reverb" }>["preset"],
|
|
69
|
+
readonly { readonly delayMs: number; readonly weight: number }[]
|
|
70
|
+
>>;
|
|
71
|
+
|
|
72
|
+
export interface ResolvedColorGradeControls {
|
|
73
|
+
readonly brightness: number;
|
|
74
|
+
readonly contrast: number;
|
|
75
|
+
readonly gamma: number;
|
|
76
|
+
readonly hue: number;
|
|
77
|
+
readonly saturation: number;
|
|
78
|
+
readonly temperature: number;
|
|
79
|
+
readonly tint: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const NEUTRAL_GRADE: ResolvedColorGradeControls = {
|
|
83
|
+
brightness: 0,
|
|
84
|
+
contrast: 1,
|
|
85
|
+
gamma: 1,
|
|
86
|
+
hue: 0,
|
|
87
|
+
saturation: 1,
|
|
88
|
+
temperature: 0,
|
|
89
|
+
tint: 0,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const COLOR_PRESETS = {
|
|
93
|
+
cinematic: {
|
|
94
|
+
brightness: -0.02,
|
|
95
|
+
contrast: 1.14,
|
|
96
|
+
gamma: 0.96,
|
|
97
|
+
hue: 0,
|
|
98
|
+
saturation: 0.88,
|
|
99
|
+
temperature: 0.12,
|
|
100
|
+
tint: 0.06,
|
|
101
|
+
},
|
|
102
|
+
clean: {
|
|
103
|
+
brightness: 0.02,
|
|
104
|
+
contrast: 1.04,
|
|
105
|
+
gamma: 1,
|
|
106
|
+
hue: 0,
|
|
107
|
+
saturation: 1.02,
|
|
108
|
+
temperature: 0,
|
|
109
|
+
tint: 0,
|
|
110
|
+
},
|
|
111
|
+
cool: {
|
|
112
|
+
brightness: 0,
|
|
113
|
+
contrast: 1.05,
|
|
114
|
+
gamma: 1,
|
|
115
|
+
hue: 0,
|
|
116
|
+
saturation: 0.96,
|
|
117
|
+
temperature: -0.35,
|
|
118
|
+
tint: 0,
|
|
119
|
+
},
|
|
120
|
+
flat: {
|
|
121
|
+
brightness: 0.02,
|
|
122
|
+
contrast: 0.86,
|
|
123
|
+
gamma: 1.04,
|
|
124
|
+
hue: 0,
|
|
125
|
+
saturation: 0.85,
|
|
126
|
+
temperature: 0,
|
|
127
|
+
tint: 0,
|
|
128
|
+
},
|
|
129
|
+
monochrome: {
|
|
130
|
+
brightness: 0,
|
|
131
|
+
contrast: 1.1,
|
|
132
|
+
gamma: 1,
|
|
133
|
+
hue: 0,
|
|
134
|
+
saturation: 0,
|
|
135
|
+
temperature: 0,
|
|
136
|
+
tint: 0,
|
|
137
|
+
},
|
|
138
|
+
vivid: {
|
|
139
|
+
brightness: 0.01,
|
|
140
|
+
contrast: 1.12,
|
|
141
|
+
gamma: 1,
|
|
142
|
+
hue: 0,
|
|
143
|
+
saturation: 1.22,
|
|
144
|
+
temperature: 0,
|
|
145
|
+
tint: 0,
|
|
146
|
+
},
|
|
147
|
+
warm: {
|
|
148
|
+
brightness: 0,
|
|
149
|
+
contrast: 1.04,
|
|
150
|
+
gamma: 1,
|
|
151
|
+
hue: 0,
|
|
152
|
+
saturation: 1.08,
|
|
153
|
+
temperature: 0.35,
|
|
154
|
+
tint: 0,
|
|
155
|
+
},
|
|
156
|
+
} as const satisfies Readonly<Record<
|
|
157
|
+
Extract<ColorGradeTransformV1["grade"], { readonly kind: "preset" }>["preset"],
|
|
158
|
+
ResolvedColorGradeControls
|
|
159
|
+
>>;
|
|
160
|
+
|
|
161
|
+
export interface BuiltAudioEffectsGraph {
|
|
162
|
+
readonly filterGraph: string;
|
|
163
|
+
readonly outputLabel: string;
|
|
164
|
+
readonly transform: AudioEffectsTransformV1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface BuiltColorGradeFilter {
|
|
168
|
+
readonly controls: ResolvedColorGradeControls;
|
|
169
|
+
readonly filter: string;
|
|
170
|
+
readonly transform: ColorGradeTransformV1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface BuiltAudioEffectsInvocation extends BuiltAudioEffectsGraph {
|
|
174
|
+
readonly argv: readonly [string, ...string[]];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface BuiltColorGradeInvocation extends BuiltColorGradeFilter {
|
|
178
|
+
readonly argv: readonly [string, ...string[]];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface LocalMediaTransformResult<Transform> extends AtomicRenderOutput {
|
|
182
|
+
readonly filterGraph: string;
|
|
183
|
+
readonly outputPath: string;
|
|
184
|
+
readonly transform: Transform;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface ExpectedLocalMediaInput {
|
|
188
|
+
readonly bytes: number;
|
|
189
|
+
readonly device: number;
|
|
190
|
+
readonly inode: number;
|
|
191
|
+
readonly modifiedAtMs: number;
|
|
192
|
+
readonly sha256: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface PinnedLocalMediaInput {
|
|
196
|
+
readonly assertUnchanged: () => Promise<void>;
|
|
197
|
+
readonly processPath: string;
|
|
198
|
+
readonly runner: ProcessRunner;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateExpectedInput(input: ExpectedLocalMediaInput): void {
|
|
202
|
+
if (
|
|
203
|
+
!Number.isSafeInteger(input.bytes)
|
|
204
|
+
|| input.bytes <= 0
|
|
205
|
+
|| input.bytes > MAXIMUM_LOCAL_MEDIA_EFFECT_INPUT_BYTES
|
|
206
|
+
|| !Number.isSafeInteger(input.device)
|
|
207
|
+
|| input.device < 0
|
|
208
|
+
|| !Number.isSafeInteger(input.inode)
|
|
209
|
+
|| input.inode < 0
|
|
210
|
+
|| !Number.isFinite(input.modifiedAtMs)
|
|
211
|
+
|| !/^[a-f0-9]{64}$/u.test(input.sha256)
|
|
212
|
+
) {
|
|
213
|
+
throw new CliError("internal", "Expected local-media input identity is invalid.");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function sameExactFile(left: BigIntStats, right: BigIntStats): boolean {
|
|
218
|
+
return left.dev === right.dev
|
|
219
|
+
&& left.ino === right.ino
|
|
220
|
+
&& left.size === right.size
|
|
221
|
+
&& left.nlink === right.nlink
|
|
222
|
+
&& left.mtimeNs === right.mtimeNs
|
|
223
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function hashFileHandle(handle: FileHandle, bytes: number): Promise<string> {
|
|
227
|
+
const hash = createHash("sha256");
|
|
228
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
229
|
+
let offset = 0;
|
|
230
|
+
while (offset < bytes) {
|
|
231
|
+
const read = await handle.read(
|
|
232
|
+
buffer,
|
|
233
|
+
0,
|
|
234
|
+
Math.min(buffer.byteLength, bytes - offset),
|
|
235
|
+
offset,
|
|
236
|
+
);
|
|
237
|
+
if (read.bytesRead === 0) break;
|
|
238
|
+
hash.update(buffer.subarray(0, read.bytesRead));
|
|
239
|
+
offset += read.bytesRead;
|
|
240
|
+
}
|
|
241
|
+
if (offset !== bytes) {
|
|
242
|
+
throw new CliError("conflict", "Media input ended while it was being pinned.");
|
|
243
|
+
}
|
|
244
|
+
return hash.digest("hex");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
class InheritedInputRunner implements ProcessRunner {
|
|
248
|
+
readonly #descriptor: number;
|
|
249
|
+
readonly #runner: ProcessRunner;
|
|
250
|
+
|
|
251
|
+
constructor(runner: ProcessRunner, descriptor: number) {
|
|
252
|
+
this.#descriptor = descriptor;
|
|
253
|
+
this.#runner = runner;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
run(
|
|
257
|
+
argv: readonly [string, ...string[]],
|
|
258
|
+
options: RunOptions = {},
|
|
259
|
+
): ReturnType<ProcessRunner["run"]> {
|
|
260
|
+
if ((options.inheritedFileDescriptors?.length ?? 0) !== 0) {
|
|
261
|
+
throw new CliError("internal", "Pinned media execution cannot inherit unrelated file descriptors.");
|
|
262
|
+
}
|
|
263
|
+
return this.#runner.run(argv, {
|
|
264
|
+
...options,
|
|
265
|
+
inheritedFileDescriptors: [this.#descriptor],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
interface OpenedLocalMediaInput {
|
|
271
|
+
readonly handle: FileHandle;
|
|
272
|
+
readonly lexical: Stats;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function openLocalMediaInput(
|
|
276
|
+
path: string,
|
|
277
|
+
expected: ExpectedLocalMediaInput,
|
|
278
|
+
): Promise<OpenedLocalMediaInput> {
|
|
279
|
+
validateExpectedInput(expected);
|
|
280
|
+
if (process.platform === "win32") {
|
|
281
|
+
throw new CliError(
|
|
282
|
+
"unavailable",
|
|
283
|
+
"Descriptor-pinned local media effects are supported on macOS and other POSIX systems.",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const lexical = await lstat(path).catch((error: unknown) => {
|
|
287
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
288
|
+
throw new CliError("not-found", `Media input does not exist: ${path}`);
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
});
|
|
292
|
+
if (
|
|
293
|
+
lexical.isSymbolicLink()
|
|
294
|
+
|| !lexical.isFile()
|
|
295
|
+
|| lexical.size !== expected.bytes
|
|
296
|
+
|| lexical.dev !== expected.device
|
|
297
|
+
|| lexical.ino !== expected.inode
|
|
298
|
+
|| lexical.mtimeMs !== expected.modifiedAtMs
|
|
299
|
+
) {
|
|
300
|
+
throw new CliError("conflict", "Media input changed after it was inspected.");
|
|
301
|
+
}
|
|
302
|
+
const handle = await open(
|
|
303
|
+
path,
|
|
304
|
+
constants.O_RDONLY
|
|
305
|
+
| (constants.O_NOFOLLOW ?? 0)
|
|
306
|
+
| (constants.O_NONBLOCK ?? 0),
|
|
307
|
+
).catch((error: unknown) => {
|
|
308
|
+
if (error instanceof Error && "code" in error && error.code === "ELOOP") {
|
|
309
|
+
throw new CliError("unsafe-path", `Media input must not be a symlink: ${path}`);
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
});
|
|
313
|
+
return { handle, lexical };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** First-observed failure wins, but both admitted native reads settle before close. */
|
|
317
|
+
function pinOpenedLocalMediaInput(
|
|
318
|
+
{ handle, lexical }: OpenedLocalMediaInput,
|
|
319
|
+
expected: ExpectedLocalMediaInput,
|
|
320
|
+
runner: ProcessRunner,
|
|
321
|
+
): Effect.Effect<PinnedLocalMediaInput, OperationEffectFailure> {
|
|
322
|
+
return Effect.uninterruptible(Effect.gen(function*() {
|
|
323
|
+
const firstFailure = yield* Ref.make(Option.none<OperationEffectFailure>());
|
|
324
|
+
const capture = <A>(read: Effect.Effect<A, OperationEffectFailure>) => Effect.exit(Effect.tapError(
|
|
325
|
+
read, failure => Ref.update(firstFailure, first => Option.isNone(first) ? Option.some(failure) : first),
|
|
326
|
+
));
|
|
327
|
+
const inspected = yield* Effect.all({
|
|
328
|
+
opened: capture(operationBoundary("media", () => handle.stat())),
|
|
329
|
+
exact: capture(operationBoundary("media", () => handle.stat({ bigint: true }))),
|
|
330
|
+
}, { concurrency: 2 });
|
|
331
|
+
const joined = Exit.zip(inspected.opened, inspected.exact);
|
|
332
|
+
if (Exit.isFailure(joined)) {
|
|
333
|
+
const first = yield* Ref.get(firstFailure);
|
|
334
|
+
if (Option.isSome(first)) return yield* Effect.fail({ ...first.value, priorCause: joined.cause });
|
|
335
|
+
return yield* Effect.failCause(joined.cause);
|
|
336
|
+
}
|
|
337
|
+
const [opened, exactBefore] = joined.value;
|
|
338
|
+
yield* operationValidation("media", () => {
|
|
339
|
+
if (
|
|
340
|
+
!opened.isFile()
|
|
341
|
+
|| !exactBefore.isFile()
|
|
342
|
+
|| opened.dev !== lexical.dev
|
|
343
|
+
|| opened.ino !== lexical.ino
|
|
344
|
+
|| opened.size !== lexical.size
|
|
345
|
+
|| opened.dev !== expected.device
|
|
346
|
+
|| opened.ino !== expected.inode
|
|
347
|
+
|| opened.size !== expected.bytes
|
|
348
|
+
|| opened.mtimeMs !== expected.modifiedAtMs
|
|
349
|
+
) {
|
|
350
|
+
throw new CliError("conflict", "Media input changed while its descriptor was being pinned.");
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
const sha256 = yield* operationBoundary("media", () => hashFileHandle(handle, expected.bytes));
|
|
354
|
+
const exactAfter = yield* operationBoundary("media", () => handle.stat({ bigint: true }));
|
|
355
|
+
yield* operationValidation("media", () => {
|
|
356
|
+
if (!sameExactFile(exactBefore, exactAfter) || sha256 !== expected.sha256) {
|
|
357
|
+
throw new CliError("conflict", "Media input bytes changed before rendering.");
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return {
|
|
361
|
+
assertUnchanged: async () => {
|
|
362
|
+
const afterRender = await handle.stat({ bigint: true });
|
|
363
|
+
if (!sameExactFile(exactAfter, afterRender)) {
|
|
364
|
+
throw new CliError("conflict", "Media input changed while effects were rendering.");
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
processPath: PINNED_CHILD_INPUT_PATH,
|
|
368
|
+
runner: new InheritedInputRunner(runner, handle.fd),
|
|
369
|
+
};
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function decimal(value: number): string {
|
|
374
|
+
if (!Number.isFinite(value)) throw new CliError("internal", "FFmpeg filter value must be finite.");
|
|
375
|
+
const normalized = Math.abs(value) < 0.000_000_000_1 ? 0 : value;
|
|
376
|
+
return normalized.toFixed(10).replace(/0+$/u, "").replace(/\.$/u, "");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function amplitude(decibels: number): number {
|
|
380
|
+
return 10 ** (decibels / 20);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function parseAudioTransform(input: unknown): AudioEffectsTransformV1 {
|
|
384
|
+
const parsed = AudioEffectsTransformV1Schema.safeParse(input);
|
|
385
|
+
if (!parsed.success) {
|
|
386
|
+
throw new CliError(
|
|
387
|
+
"usage",
|
|
388
|
+
`Invalid audio-effects transform: ${parsed.error.issues[0]?.message ?? "unknown error"}`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
return parsed.data;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function parseColorTransform(input: unknown): ColorGradeTransformV1 {
|
|
395
|
+
const parsed = ColorGradeTransformV1Schema.safeParse(input);
|
|
396
|
+
if (!parsed.success) {
|
|
397
|
+
throw new CliError(
|
|
398
|
+
"usage",
|
|
399
|
+
`Invalid color-grade transform: ${parsed.error.issues[0]?.message ?? "unknown error"}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return parsed.data;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function effectOutputLabel(index: number): string {
|
|
406
|
+
return `audio_fx_${index}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildDelayGraph(
|
|
410
|
+
inputLabel: string,
|
|
411
|
+
outputLabel: string,
|
|
412
|
+
effect: Extract<AudioEffect, { readonly kind: "delay" }>,
|
|
413
|
+
serial: number,
|
|
414
|
+
): readonly string[] {
|
|
415
|
+
const dry = `delay_${serial}_dry`;
|
|
416
|
+
const dryGain = `delay_${serial}_dry_gain`;
|
|
417
|
+
const maximumTaps = Math.max(
|
|
418
|
+
1,
|
|
419
|
+
Math.min(16, Math.floor(30_000 / effect.delayMs)),
|
|
420
|
+
);
|
|
421
|
+
const tapCount = effect.decay === 0
|
|
422
|
+
? 1
|
|
423
|
+
: effect.decay >= 1
|
|
424
|
+
? maximumTaps
|
|
425
|
+
: Math.min(
|
|
426
|
+
maximumTaps,
|
|
427
|
+
Math.max(
|
|
428
|
+
1,
|
|
429
|
+
Math.ceil(Math.log(0.01) / Math.log(effect.decay)),
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
const taps = Array.from(
|
|
433
|
+
{ length: tapCount },
|
|
434
|
+
(_, index) => ({
|
|
435
|
+
delayMs: effect.delayMs * (index + 1),
|
|
436
|
+
gain: effect.mix * effect.decay ** index,
|
|
437
|
+
input: `delay_${serial}_tap_${index}`,
|
|
438
|
+
output: `delay_${serial}_tap_${index}_gain`,
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
441
|
+
return [
|
|
442
|
+
`[${inputLabel}]asplit=${taps.length + 1}[${dry}]${taps.map(tap => `[${tap.input}]`).join("")}`,
|
|
443
|
+
`[${dry}]volume=volume=${decimal(1 - effect.mix)}[${dryGain}]`,
|
|
444
|
+
...taps.map(tap => (
|
|
445
|
+
`[${tap.input}]adelay=delays=${tap.delayMs}:all=1,volume=volume=${decimal(tap.gain)}[${tap.output}]`
|
|
446
|
+
)),
|
|
447
|
+
`[${dryGain}]${taps.map(tap => `[${tap.output}]`).join("")}amix=inputs=${taps.length + 1}:duration=longest:dropout_transition=0:normalize=0[${outputLabel}]`,
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function buildReverbGraph(
|
|
452
|
+
inputLabel: string,
|
|
453
|
+
outputLabel: string,
|
|
454
|
+
effect: Extract<AudioEffect, { readonly kind: "reverb" }>,
|
|
455
|
+
serial: number,
|
|
456
|
+
): readonly string[] {
|
|
457
|
+
const taps = REVERB_TAPS[effect.preset];
|
|
458
|
+
const dry = `reverb_${serial}_dry`;
|
|
459
|
+
const dryGain = `reverb_${serial}_dry_gain`;
|
|
460
|
+
const wetInputs = taps.map((_, index) => `reverb_${serial}_tap_${index}`);
|
|
461
|
+
const wetGains = taps.map((_, index) => `reverb_${serial}_tap_${index}_gain`);
|
|
462
|
+
return [
|
|
463
|
+
`[${inputLabel}]asplit=${taps.length + 1}[${dry}]${wetInputs.map(label => `[${label}]`).join("")}`,
|
|
464
|
+
`[${dry}]volume=volume=${decimal(1 - effect.mix)}[${dryGain}]`,
|
|
465
|
+
...taps.map((tap, index) =>
|
|
466
|
+
`[${wetInputs[index]}]adelay=delays=${tap.delayMs}:all=1,volume=volume=${decimal(effect.mix * tap.weight)}[${wetGains[index]}]`
|
|
467
|
+
),
|
|
468
|
+
`[${dryGain}]${wetGains.map(label => `[${label}]`).join("")}amix=inputs=${taps.length + 1}:duration=longest:dropout_transition=0:normalize=0[${outputLabel}]`,
|
|
469
|
+
];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Build a bounded, numeric-only FFmpeg graph. No caller text reaches the graph. */
|
|
473
|
+
export function buildAudioEffectsFilterGraph(input: unknown): BuiltAudioEffectsGraph {
|
|
474
|
+
const transform = parseAudioTransform(input);
|
|
475
|
+
const filters: string[] = [];
|
|
476
|
+
let currentLabel = `0:a:${transform.audioStreamIndex}`;
|
|
477
|
+
for (const [index, effect] of transform.effects.entries()) {
|
|
478
|
+
const nextLabel = effectOutputLabel(index);
|
|
479
|
+
if (effect.kind === "volume") {
|
|
480
|
+
filters.push(`[${currentLabel}]volume=volume=${decimal(effect.gainDb)}dB[${nextLabel}]`);
|
|
481
|
+
} else if (effect.kind === "compressor") {
|
|
482
|
+
filters.push(
|
|
483
|
+
`[${currentLabel}]acompressor=threshold=${decimal(amplitude(effect.thresholdDb))}:ratio=${decimal(effect.ratio)}:attack=${decimal(effect.attackMs)}:release=${decimal(effect.releaseMs)}:makeup=${decimal(amplitude(effect.makeupGainDb))}:knee=${decimal(effect.knee)}:detection=rms:link=average[${nextLabel}]`,
|
|
484
|
+
);
|
|
485
|
+
} else if (effect.kind === "delay") {
|
|
486
|
+
filters.push(...buildDelayGraph(currentLabel, nextLabel, effect, index));
|
|
487
|
+
} else if (effect.kind === "reverb") {
|
|
488
|
+
filters.push(...buildReverbGraph(currentLabel, nextLabel, effect, index));
|
|
489
|
+
} else if (effect.kind === "denoise") {
|
|
490
|
+
filters.push(
|
|
491
|
+
`[${currentLabel}]afftdn=nf=${decimal(effect.noiseFloorDb)}:nr=${decimal(effect.noiseReductionDb)}:tn=${effect.trackNoise ? "1" : "0"}[${nextLabel}]`,
|
|
492
|
+
);
|
|
493
|
+
} else {
|
|
494
|
+
effect satisfies never;
|
|
495
|
+
}
|
|
496
|
+
currentLabel = nextLabel;
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
filterGraph: filters.join(";"),
|
|
500
|
+
outputLabel: currentLabel,
|
|
501
|
+
transform,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function applyControls(
|
|
506
|
+
base: ResolvedColorGradeControls,
|
|
507
|
+
overrides: ColorGradeControls | undefined,
|
|
508
|
+
): ResolvedColorGradeControls {
|
|
509
|
+
return {
|
|
510
|
+
brightness: overrides?.brightness ?? base.brightness,
|
|
511
|
+
contrast: overrides?.contrast ?? base.contrast,
|
|
512
|
+
gamma: overrides?.gamma ?? base.gamma,
|
|
513
|
+
hue: overrides?.hue ?? base.hue,
|
|
514
|
+
saturation: overrides?.saturation ?? base.saturation,
|
|
515
|
+
temperature: overrides?.temperature ?? base.temperature,
|
|
516
|
+
tint: overrides?.tint ?? base.tint,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function clampBalance(value: number): number {
|
|
521
|
+
return Math.max(-1, Math.min(1, value));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function balanceBand(
|
|
525
|
+
temperature: number,
|
|
526
|
+
tint: number,
|
|
527
|
+
strength: number,
|
|
528
|
+
): { readonly blue: number; readonly green: number; readonly red: number } {
|
|
529
|
+
return {
|
|
530
|
+
blue: clampBalance((-temperature + tint * 0.5) * strength),
|
|
531
|
+
green: clampBalance(-tint * strength),
|
|
532
|
+
red: clampBalance((temperature + tint * 0.5) * strength),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function resolveColorGradeControls(input: unknown): ResolvedColorGradeControls {
|
|
537
|
+
const transform = parseColorTransform(input);
|
|
538
|
+
return transform.grade.kind === "custom"
|
|
539
|
+
? applyControls(NEUTRAL_GRADE, transform.grade.controls)
|
|
540
|
+
: applyControls(COLOR_PRESETS[transform.grade.preset], transform.grade.overrides);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Resolve a preset plus overrides into an explicit, deterministic FFmpeg chain. */
|
|
544
|
+
export function buildColorGradeFilter(input: unknown): BuiltColorGradeFilter {
|
|
545
|
+
const transform = parseColorTransform(input);
|
|
546
|
+
const controls = transform.grade.kind === "custom"
|
|
547
|
+
? applyControls(NEUTRAL_GRADE, transform.grade.controls)
|
|
548
|
+
: applyControls(COLOR_PRESETS[transform.grade.preset], transform.grade.overrides);
|
|
549
|
+
const filters = [
|
|
550
|
+
`eq=brightness=${decimal(controls.brightness)}:contrast=${decimal(controls.contrast)}:saturation=${decimal(controls.saturation)}:gamma=${decimal(controls.gamma)}`,
|
|
551
|
+
];
|
|
552
|
+
if (controls.temperature !== 0 || controls.tint !== 0) {
|
|
553
|
+
const shadows = balanceBand(controls.temperature, controls.tint, 0.12);
|
|
554
|
+
const midtones = balanceBand(controls.temperature, controls.tint, 0.2);
|
|
555
|
+
const highlights = balanceBand(controls.temperature, controls.tint, 0.1);
|
|
556
|
+
filters.push(
|
|
557
|
+
[
|
|
558
|
+
`colorbalance=rs=${decimal(shadows.red)}`,
|
|
559
|
+
`gs=${decimal(shadows.green)}`,
|
|
560
|
+
`bs=${decimal(shadows.blue)}`,
|
|
561
|
+
`rm=${decimal(midtones.red)}`,
|
|
562
|
+
`gm=${decimal(midtones.green)}`,
|
|
563
|
+
`bm=${decimal(midtones.blue)}`,
|
|
564
|
+
`rh=${decimal(highlights.red)}`,
|
|
565
|
+
`gh=${decimal(highlights.green)}`,
|
|
566
|
+
`bh=${decimal(highlights.blue)}`,
|
|
567
|
+
].join(":"),
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (controls.hue !== 0) filters.push(`hue=h=${decimal(controls.hue)}`);
|
|
571
|
+
return { controls, filter: filters.join(","), transform };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function validateArgument(value: string, label: string): void {
|
|
575
|
+
if (
|
|
576
|
+
value.length === 0
|
|
577
|
+
|| value.includes("\0")
|
|
578
|
+
|| Buffer.byteLength(value) > MAXIMUM_PATH_BYTES
|
|
579
|
+
) {
|
|
580
|
+
throw new CliError("usage", `${label} must be a bounded, nonempty, NUL-free string.`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function assertExtension(path: string, expected: readonly string[], label: string): void {
|
|
585
|
+
const extension = extname(path).toLocaleLowerCase("en-US");
|
|
586
|
+
if (!expected.includes(extension)) {
|
|
587
|
+
throw new CliError("usage", `${label} output must use ${expected.join(", ")}.`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function audioEncodingArguments(transform: AudioEffectsTransformV1, outputPath: string): readonly string[] {
|
|
592
|
+
if (transform.output.kind === "preserve-video") {
|
|
593
|
+
if (transform.output.profile === "aac") {
|
|
594
|
+
assertExtension(outputPath, [".mp4", ".mov", ".m4v"], "AAC video");
|
|
595
|
+
return ["-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart"];
|
|
596
|
+
}
|
|
597
|
+
assertExtension(outputPath, [".mkv", ".webm"], "Opus video");
|
|
598
|
+
return ["-c:v", "copy", "-c:a", "libopus", "-b:a", "160k"];
|
|
599
|
+
}
|
|
600
|
+
if (transform.output.profile === "wav-pcm-s16le") {
|
|
601
|
+
assertExtension(outputPath, [".wav"], "PCM");
|
|
602
|
+
return ["-c:a", "pcm_s16le"];
|
|
603
|
+
}
|
|
604
|
+
if (transform.output.profile === "flac") {
|
|
605
|
+
assertExtension(outputPath, [".flac"], "FLAC");
|
|
606
|
+
return ["-c:a", "flac"];
|
|
607
|
+
}
|
|
608
|
+
if (transform.output.profile === "mp3") {
|
|
609
|
+
assertExtension(outputPath, [".mp3"], "MP3");
|
|
610
|
+
return ["-c:a", "libmp3lame", "-q:a", "2"];
|
|
611
|
+
}
|
|
612
|
+
if (transform.output.profile === "aac") {
|
|
613
|
+
assertExtension(outputPath, [".aac", ".m4a", ".mp4"], "AAC");
|
|
614
|
+
return ["-c:a", "aac", "-b:a", "192k"];
|
|
615
|
+
}
|
|
616
|
+
assertExtension(outputPath, [".ogg", ".opus", ".webm"], "Opus");
|
|
617
|
+
return ["-c:a", "libopus", "-b:a", "160k"];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function colorEncodingArguments(transform: ColorGradeTransformV1, outputPath: string): readonly string[] {
|
|
621
|
+
if (transform.outputProfile === "h264-mp4") {
|
|
622
|
+
assertExtension(outputPath, [".mp4"], "H.264");
|
|
623
|
+
return [
|
|
624
|
+
"-c:v", "libx264",
|
|
625
|
+
"-preset", "medium",
|
|
626
|
+
"-crf", "18",
|
|
627
|
+
"-pix_fmt", "yuv420p",
|
|
628
|
+
"-c:a", "aac",
|
|
629
|
+
"-b:a", "192k",
|
|
630
|
+
"-movflags", "+faststart",
|
|
631
|
+
];
|
|
632
|
+
}
|
|
633
|
+
if (transform.outputProfile === "prores-mov") {
|
|
634
|
+
assertExtension(outputPath, [".mov"], "ProRes");
|
|
635
|
+
return [
|
|
636
|
+
"-c:v", "prores_ks",
|
|
637
|
+
"-profile:v", "3",
|
|
638
|
+
"-pix_fmt", "yuv422p10le",
|
|
639
|
+
"-c:a", "pcm_s16le",
|
|
640
|
+
];
|
|
641
|
+
}
|
|
642
|
+
assertExtension(outputPath, [".webm"], "VP9");
|
|
643
|
+
return [
|
|
644
|
+
"-c:v", "libvpx-vp9",
|
|
645
|
+
"-crf", "30",
|
|
646
|
+
"-b:v", "0",
|
|
647
|
+
"-c:a", "libopus",
|
|
648
|
+
"-b:a", "160k",
|
|
649
|
+
];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function buildAudioEffectsInvocation(options: {
|
|
653
|
+
readonly ffmpeg: string;
|
|
654
|
+
readonly inputPath: string;
|
|
655
|
+
readonly outputPath: string;
|
|
656
|
+
readonly transform: unknown;
|
|
657
|
+
}): BuiltAudioEffectsInvocation {
|
|
658
|
+
validateArgument(options.ffmpeg, "FFmpeg executable");
|
|
659
|
+
validateArgument(options.inputPath, "Input path");
|
|
660
|
+
validateArgument(options.outputPath, "Output path");
|
|
661
|
+
if (resolve(options.inputPath) === resolve(options.outputPath)) {
|
|
662
|
+
throw new CliError("unsafe-path", "Audio-effects output must differ from its immutable input.");
|
|
663
|
+
}
|
|
664
|
+
const built = buildAudioEffectsFilterGraph(options.transform);
|
|
665
|
+
const arguments_: string[] = [
|
|
666
|
+
"-hide_banner", "-nostdin", "-xerror", "-y",
|
|
667
|
+
...SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS,
|
|
668
|
+
"-i", options.inputPath,
|
|
669
|
+
"-filter_complex", built.filterGraph,
|
|
670
|
+
...(built.transform.output.kind === "preserve-video"
|
|
671
|
+
? [
|
|
672
|
+
"-map",
|
|
673
|
+
built.transform.output.inputVideoStreamIndex === undefined
|
|
674
|
+
? `0:V:${built.transform.output.videoStreamIndex}`
|
|
675
|
+
: `0:${built.transform.output.inputVideoStreamIndex}`,
|
|
676
|
+
]
|
|
677
|
+
: []),
|
|
678
|
+
"-map", `[${built.outputLabel}]`,
|
|
679
|
+
"-map_metadata", "0",
|
|
680
|
+
"-sn", "-dn",
|
|
681
|
+
"-fs", String(MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES),
|
|
682
|
+
...audioEncodingArguments(built.transform, options.outputPath),
|
|
683
|
+
options.outputPath,
|
|
684
|
+
];
|
|
685
|
+
return {
|
|
686
|
+
...built,
|
|
687
|
+
argv: [options.ffmpeg, ...arguments_],
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function buildColorGradeInvocation(options: {
|
|
692
|
+
readonly ffmpeg: string;
|
|
693
|
+
readonly inputPath: string;
|
|
694
|
+
readonly outputPath: string;
|
|
695
|
+
readonly transform: unknown;
|
|
696
|
+
}): BuiltColorGradeInvocation {
|
|
697
|
+
validateArgument(options.ffmpeg, "FFmpeg executable");
|
|
698
|
+
validateArgument(options.inputPath, "Input path");
|
|
699
|
+
validateArgument(options.outputPath, "Output path");
|
|
700
|
+
if (resolve(options.inputPath) === resolve(options.outputPath)) {
|
|
701
|
+
throw new CliError("unsafe-path", "Color-grade output must differ from its immutable input.");
|
|
702
|
+
}
|
|
703
|
+
const built = buildColorGradeFilter(options.transform);
|
|
704
|
+
const outputLabel = "graded_video";
|
|
705
|
+
const arguments_: string[] = [
|
|
706
|
+
"-hide_banner", "-nostdin", "-xerror", "-y",
|
|
707
|
+
...SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS,
|
|
708
|
+
"-i", options.inputPath,
|
|
709
|
+
"-filter_complex", `[${
|
|
710
|
+
built.transform.inputStreamIndex === undefined
|
|
711
|
+
? `0:V:${built.transform.videoStreamIndex}`
|
|
712
|
+
: `0:${built.transform.inputStreamIndex}`
|
|
713
|
+
}]${built.filter}[${outputLabel}]`,
|
|
714
|
+
"-map", `[${outputLabel}]`,
|
|
715
|
+
"-map", "0:a?",
|
|
716
|
+
"-map_metadata", "0",
|
|
717
|
+
"-sn", "-dn",
|
|
718
|
+
"-fs", String(MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES),
|
|
719
|
+
...colorEncodingArguments(built.transform, options.outputPath),
|
|
720
|
+
options.outputPath,
|
|
721
|
+
];
|
|
722
|
+
return {
|
|
723
|
+
...built,
|
|
724
|
+
argv: [options.ffmpeg, ...arguments_],
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function assertSafeTransformPaths(inputPath: string, outputPath: string): Promise<void> {
|
|
729
|
+
if (inputPath === outputPath) {
|
|
730
|
+
throw new CliError("unsafe-path", "Derived media output must differ from its immutable input.");
|
|
731
|
+
}
|
|
732
|
+
const input = await lstat(inputPath).catch((error: unknown) => {
|
|
733
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
734
|
+
throw new CliError("not-found", `Media input does not exist: ${inputPath}`);
|
|
735
|
+
}
|
|
736
|
+
throw error;
|
|
737
|
+
});
|
|
738
|
+
if (input.isSymbolicLink() || !input.isFile() || input.size <= 0) {
|
|
739
|
+
throw new CliError("unsafe-path", "Media input must be a physical, nonempty regular file.");
|
|
740
|
+
}
|
|
741
|
+
const parentPath = dirname(outputPath);
|
|
742
|
+
const parent = await lstat(parentPath).catch((error: unknown) => {
|
|
743
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
744
|
+
throw new CliError("not-found", `Media output directory does not exist: ${parentPath}`);
|
|
745
|
+
}
|
|
746
|
+
throw error;
|
|
747
|
+
});
|
|
748
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) {
|
|
749
|
+
throw new CliError("unsafe-path", "Media output directory must be a physical directory.");
|
|
750
|
+
}
|
|
751
|
+
const [physicalInput, physicalOutputParent] = await Promise.all([
|
|
752
|
+
realpath(inputPath),
|
|
753
|
+
realpath(parentPath),
|
|
754
|
+
]);
|
|
755
|
+
if (physicalInput === join(physicalOutputParent, basename(outputPath))) {
|
|
756
|
+
throw new CliError("unsafe-path", "Derived media output resolves to its immutable input.");
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
const output = await lstat(outputPath);
|
|
760
|
+
if (output.dev === input.dev && output.ino === input.ino) {
|
|
761
|
+
throw new CliError("unsafe-path", "Derived media output aliases its immutable input.");
|
|
762
|
+
}
|
|
763
|
+
throw new CliError("conflict", `Derived media output already exists: ${outputPath}`);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
if (error instanceof CliError) throw error;
|
|
766
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export class LocalMediaEffectsService {
|
|
771
|
+
readonly #ffmpeg: string;
|
|
772
|
+
readonly #runner: ProcessRunner;
|
|
773
|
+
|
|
774
|
+
constructor(options: { readonly ffmpeg: string; readonly runner: ProcessRunner }) {
|
|
775
|
+
validateArgument(options.ffmpeg, "FFmpeg executable");
|
|
776
|
+
this.#ffmpeg = options.ffmpeg;
|
|
777
|
+
this.#runner = options.runner;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
buildAudio(options: {
|
|
781
|
+
readonly inputPath: string;
|
|
782
|
+
readonly outputPath: string;
|
|
783
|
+
readonly transform: unknown;
|
|
784
|
+
}): BuiltAudioEffectsInvocation {
|
|
785
|
+
return buildAudioEffectsInvocation({ ...options, ffmpeg: this.#ffmpeg });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
buildColor(options: {
|
|
789
|
+
readonly inputPath: string;
|
|
790
|
+
readonly outputPath: string;
|
|
791
|
+
readonly transform: unknown;
|
|
792
|
+
}): BuiltColorGradeInvocation {
|
|
793
|
+
return buildColorGradeInvocation({ ...options, ffmpeg: this.#ffmpeg });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async renderAudio(options: {
|
|
797
|
+
readonly expectedInput: ExpectedLocalMediaInput;
|
|
798
|
+
readonly inputPath: string;
|
|
799
|
+
readonly outputPath: string;
|
|
800
|
+
readonly transform: unknown;
|
|
801
|
+
}): Promise<LocalMediaTransformResult<AudioEffectsTransformV1>> {
|
|
802
|
+
return await runStandaloneOperation(this.renderAudioEffect(options));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
renderAudioEffect(options: {
|
|
806
|
+
readonly expectedInput: ExpectedLocalMediaInput;
|
|
807
|
+
readonly inputPath: string;
|
|
808
|
+
readonly outputPath: string;
|
|
809
|
+
readonly transform: unknown;
|
|
810
|
+
}): Effect.Effect<LocalMediaTransformResult<AudioEffectsTransformV1>, OperationEffectFailure> {
|
|
811
|
+
return this.#renderEffect(options, paths => {
|
|
812
|
+
const built = this.buildAudio({ ...options, ...paths });
|
|
813
|
+
return { ...built, failureLabel: "FFmpeg audio-effects render failed" };
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async renderColor(options: {
|
|
818
|
+
readonly expectedInput: ExpectedLocalMediaInput;
|
|
819
|
+
readonly inputPath: string;
|
|
820
|
+
readonly outputPath: string;
|
|
821
|
+
readonly transform: unknown;
|
|
822
|
+
}): Promise<LocalMediaTransformResult<ColorGradeTransformV1>> {
|
|
823
|
+
return await runStandaloneOperation(this.renderColorEffect(options));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
renderColorEffect(options: {
|
|
827
|
+
readonly expectedInput: ExpectedLocalMediaInput;
|
|
828
|
+
readonly inputPath: string;
|
|
829
|
+
readonly outputPath: string;
|
|
830
|
+
readonly transform: unknown;
|
|
831
|
+
}): Effect.Effect<LocalMediaTransformResult<ColorGradeTransformV1>, OperationEffectFailure> {
|
|
832
|
+
return this.#renderEffect(options, paths => {
|
|
833
|
+
const built = this.buildColor({ ...options, ...paths });
|
|
834
|
+
return { ...built, filterGraph: built.filter, failureLabel: "FFmpeg color-grade render failed" };
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
#renderEffect<Transform>(
|
|
839
|
+
options: {
|
|
840
|
+
readonly expectedInput: ExpectedLocalMediaInput;
|
|
841
|
+
readonly inputPath: string;
|
|
842
|
+
readonly outputPath: string;
|
|
843
|
+
},
|
|
844
|
+
build: (paths: { readonly inputPath: string; readonly outputPath: string }) => {
|
|
845
|
+
readonly argv: readonly [string, ...string[]];
|
|
846
|
+
readonly failureLabel: string;
|
|
847
|
+
readonly filterGraph: string;
|
|
848
|
+
readonly transform: Transform;
|
|
849
|
+
},
|
|
850
|
+
): Effect.Effect<LocalMediaTransformResult<Transform>, OperationEffectFailure> {
|
|
851
|
+
const runner = this.#runner;
|
|
852
|
+
return Effect.gen(function*() {
|
|
853
|
+
const { inputPath, outputPath } = yield* operationValidation("input", () => ({
|
|
854
|
+
inputPath: resolve(options.inputPath),
|
|
855
|
+
outputPath: resolve(options.outputPath),
|
|
856
|
+
}));
|
|
857
|
+
yield* Effect.uninterruptible(operationBoundary("media", () => assertSafeTransformPaths(inputPath, outputPath)));
|
|
858
|
+
return yield* operationResource(
|
|
859
|
+
operationBoundary("media", () => openLocalMediaInput(inputPath, options.expectedInput)),
|
|
860
|
+
opened => Effect.gen(function*() {
|
|
861
|
+
const pinned = yield* pinOpenedLocalMediaInput(opened, options.expectedInput, runner);
|
|
862
|
+
const built = yield* operationValidation("input", () => build({ inputPath: pinned.processPath, outputPath }));
|
|
863
|
+
const rendered = yield* executeAtomicRenderEffect({
|
|
864
|
+
argv: built.argv,
|
|
865
|
+
failureLabel: built.failureLabel,
|
|
866
|
+
finalOutputPath: outputPath,
|
|
867
|
+
maximumOutputBytes: MAXIMUM_LOCAL_MEDIA_EFFECT_OUTPUT_BYTES,
|
|
868
|
+
requireFreshOutput: true,
|
|
869
|
+
runner: pinned.runner,
|
|
870
|
+
timeoutMs: LOCAL_MEDIA_EFFECT_TIMEOUT_MS,
|
|
871
|
+
}, {
|
|
872
|
+
prepare: () => operationBoundary("publication", pinned.assertUnchanged),
|
|
873
|
+
});
|
|
874
|
+
return {
|
|
875
|
+
...rendered.output,
|
|
876
|
+
filterGraph: built.filterGraph,
|
|
877
|
+
outputPath,
|
|
878
|
+
transform: built.transform,
|
|
879
|
+
};
|
|
880
|
+
}),
|
|
881
|
+
opened => operationBoundary("cleanup", () => opened.handle.close()),
|
|
882
|
+
);
|
|
883
|
+
}).pipe(Effect.provide(AtomicRenderPlatformLive));
|
|
884
|
+
}
|
|
885
|
+
}
|