@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,308 @@
|
|
|
1
|
+
"""Fixed Manim Community render adapter. Source executes only with --request.
|
|
2
|
+
|
|
3
|
+
This is trusted current-user Python, not an operating-system security sandbox.
|
|
4
|
+
The SLOPCAMERA host owns consent, source hashes, process custody and authoritative receipts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
from fractions import Fraction
|
|
9
|
+
import importlib.metadata
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
import re
|
|
14
|
+
import runpy
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bounded_json(path):
|
|
19
|
+
path = Path(path)
|
|
20
|
+
if path.is_symlink() or not path.is_file():
|
|
21
|
+
raise ValueError("Driver request must be a physical regular file.")
|
|
22
|
+
with path.open("rb") as source:
|
|
23
|
+
data = source.read(2*1024*1024+1)
|
|
24
|
+
if len(data) > 2*1024*1024:
|
|
25
|
+
raise ValueError("Driver request exceeds its byte bound.")
|
|
26
|
+
return json.loads(data)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def relative_path(value):
|
|
30
|
+
if not isinstance(value, str) or not value or "\\" in value or "\x00" in value:
|
|
31
|
+
raise ValueError("Expected a normalized relative output path.")
|
|
32
|
+
path = PurePosixPath(value)
|
|
33
|
+
if path.is_absolute() or any(part in (".", "..", "") for part in value.split("/")):
|
|
34
|
+
raise ValueError("Driver path escapes its declared root.")
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def integer(value, minimum, maximum):
|
|
39
|
+
if type(value) is not int or not minimum <= value <= maximum:
|
|
40
|
+
raise ValueError("Driver integer exceeds the supported render profile.")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_request(request):
|
|
45
|
+
if type(request) is not dict or set(request) != {"bundle", "job", "sourceRoot", "outputRoot", "workingRoot"}:
|
|
46
|
+
raise ValueError("Invalid fixed driver request envelope.")
|
|
47
|
+
job, bundle = request["job"], request["bundle"]
|
|
48
|
+
if job["kind"] != "slopcamera.studio-job" or type(job["schemaVersion"]) is not int or job["schemaVersion"] != 1 or bundle["kind"] != "slopcamera.studio-source-bundle" or type(bundle["schemaVersion"]) is not int or bundle["schemaVersion"] != 1:
|
|
49
|
+
raise ValueError("Unsupported studio protocol.")
|
|
50
|
+
if job["stage"] != "render" or bundle["engine"] != "manim" or bundle["entrypoint"]["kind"] != "python":
|
|
51
|
+
raise ValueError("The Manim adapter supports explicit Python render jobs only.")
|
|
52
|
+
if job["execution"] != {"trust": "trusted-current-user", "isolation": "none", "hermetic": False}:
|
|
53
|
+
raise ValueError("Manim source requires the declared trusted current-user execution model.")
|
|
54
|
+
engine = job["engine"]
|
|
55
|
+
if set(engine) != {"engine", "scene", "renderer", "transparent"} or engine["engine"] != "manim" or engine["renderer"] != "cairo" or type(engine["transparent"]) is not bool or re.fullmatch(r"[A-Za-z_]\w*", engine["scene"], flags=re.ASCII) is None:
|
|
56
|
+
raise ValueError("The Manim profile requires Cairo and an exact Scene class name.")
|
|
57
|
+
render = job["render"]
|
|
58
|
+
width, height = integer(render["width"], 1, 2048), integer(render["height"], 1, 2048)
|
|
59
|
+
if width*height > 4_194_304:
|
|
60
|
+
raise ValueError("The Manim profile permits at most four megapixels.")
|
|
61
|
+
fps = Fraction(integer(render["frameRate"]["numerator"], 1, 240_000), integer(render["frameRate"]["denominator"], 1, 100_000))
|
|
62
|
+
start, end = integer(render["startFrame"], 0, 14_399), integer(render["endFrameExclusive"], 1, 14_400)
|
|
63
|
+
if fps > 60 or start >= end:
|
|
64
|
+
raise ValueError("The Manim profile requires <=60fps and a positive interval ending by frame14400.")
|
|
65
|
+
limits = job["limits"]
|
|
66
|
+
integer(limits["maximumOutputBytes"], 1, 64*1024**3)
|
|
67
|
+
maximum_files = integer(limits["maximumOutputFiles"], 1, 25_000)
|
|
68
|
+
outputs = job["outputs"]
|
|
69
|
+
if type(outputs) is not list or not 1 <= len(outputs) <= 16:
|
|
70
|
+
raise ValueError("Manim requires one to sixteen declared raster outputs.")
|
|
71
|
+
movies, count = 0, 0
|
|
72
|
+
for output in outputs:
|
|
73
|
+
kind, format_name = output["kind"], output["format"]
|
|
74
|
+
if output["role"] not in ("beauty", "auxiliary") or (kind, format_name) not in (("sequence", "png"), ("file", "png"), ("file", "mp4")):
|
|
75
|
+
raise ValueError("Manim supports PNG sequences/stills and opaque lossless RGB MP4 output.")
|
|
76
|
+
value = relative_path(output["pathPattern"] if kind == "sequence" else output["path"])
|
|
77
|
+
if kind == "sequence" and (value.count("%06d") != 1 or "%" in value.replace("%06d", "")):
|
|
78
|
+
raise ValueError("PNG sequences require one exact %06d token.")
|
|
79
|
+
if not value.endswith("."+format_name):
|
|
80
|
+
raise ValueError("Raster output path and format differ.")
|
|
81
|
+
interpretation = output["interpretation"]
|
|
82
|
+
alpha = "straight" if engine["transparent"] else "opaque"
|
|
83
|
+
channels = ["R", "G", "B", "A"] if engine["transparent"] else ["R", "G", "B"]
|
|
84
|
+
if interpretation != {"kind": "raster", "colorSpace": "srgb", "alpha": alpha, "dataType": "uint8", "channels": channels, "semantic": "color", "unit": "unitless"}:
|
|
85
|
+
raise ValueError("Manim output must explicitly describe its sRGB uint8 color pixels and alpha.")
|
|
86
|
+
if format_name == "mp4":
|
|
87
|
+
movies += 1
|
|
88
|
+
if engine["transparent"] or width % 2 or height % 2:
|
|
89
|
+
raise ValueError("RGB MP4 requires opaque, even-sized frames; use PNG for alpha.")
|
|
90
|
+
count += end-start if kind == "sequence" else 1
|
|
91
|
+
if movies > 1 or count > maximum_files:
|
|
92
|
+
raise ValueError("Manim permits one movie and must fit the declared output-file budget.")
|
|
93
|
+
entry = relative_path(bundle["entrypoint"]["path"])
|
|
94
|
+
if not any(file["path"] == entry and file["bytes"] > 0 for file in bundle["files"]):
|
|
95
|
+
raise ValueError("The Scene entrypoint is not a declared nonempty source file.")
|
|
96
|
+
return fps, start, end
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def physical_directory(value):
|
|
100
|
+
path = Path(value)
|
|
101
|
+
if not path.is_absolute() or not path.is_dir() or path.is_symlink() or path.resolve() != path:
|
|
102
|
+
raise ValueError("Driver roots must be existing absolute physical directories.")
|
|
103
|
+
return path
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_output(root, relative):
|
|
107
|
+
relative_path(relative)
|
|
108
|
+
target = root / relative
|
|
109
|
+
parent = root
|
|
110
|
+
for part in PurePosixPath(relative).parts[:-1]:
|
|
111
|
+
parent = parent / part
|
|
112
|
+
parent.mkdir(mode=0o700, exist_ok=True)
|
|
113
|
+
if parent.is_symlink() or parent.resolve() != parent:
|
|
114
|
+
raise ValueError("Output parent changed to an indirect path.")
|
|
115
|
+
return target.open("xb")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def source_context(request):
|
|
119
|
+
"""Author-visible data cannot mutate the already admitted driver request."""
|
|
120
|
+
context = {key: request[key] for key in ("sourceRoot", "outputRoot", "workingRoot")}
|
|
121
|
+
context.update({key: request["job"].get(key) for key in ("parameters", "stage", "render")})
|
|
122
|
+
return json.loads(json.dumps(context, allow_nan=False))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def probe():
|
|
126
|
+
import manim
|
|
127
|
+
import av
|
|
128
|
+
from PIL import Image
|
|
129
|
+
from manim.renderer.cairo_renderer import CairoRenderer
|
|
130
|
+
versions = {}
|
|
131
|
+
for distribution in ("manim", "av", "numpy", "Pillow", "manimpango", "typst"):
|
|
132
|
+
try:
|
|
133
|
+
versions[distribution] = importlib.metadata.version(distribution)
|
|
134
|
+
except importlib.metadata.PackageNotFoundError:
|
|
135
|
+
pass
|
|
136
|
+
capabilities = ["python-authoring", "render", "image-sequence"] if manim.__version__.startswith("0.21.") else []
|
|
137
|
+
try:
|
|
138
|
+
av.codec.Codec("libx264rgb", "w")
|
|
139
|
+
if capabilities:
|
|
140
|
+
capabilities.append("beauty-video")
|
|
141
|
+
except av.error.FFmpegError:
|
|
142
|
+
pass
|
|
143
|
+
return {"name": "Manim Community", "version": manim.__version__, "packages": versions, "capabilities": capabilities}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def render_request(request):
|
|
147
|
+
fps, start, end = validate_request(request)
|
|
148
|
+
job = request["job"]
|
|
149
|
+
source_root, output_root, working_root = (physical_directory(request[key]) for key in ("sourceRoot", "outputRoot", "workingRoot"))
|
|
150
|
+
roots = (source_root, output_root, working_root)
|
|
151
|
+
if any(a == b or a in b.parents or b in a.parents for index, a in enumerate(roots) for b in roots[index+1:]):
|
|
152
|
+
raise ValueError("Source, output and scratch roots must be disjoint.")
|
|
153
|
+
if any(output_root.iterdir()):
|
|
154
|
+
raise ValueError("Native rendering requires a fresh owned output directory.")
|
|
155
|
+
entrypoint = source_root / request["bundle"]["entrypoint"]["path"]
|
|
156
|
+
if entrypoint.is_symlink() or entrypoint.resolve() != entrypoint or not entrypoint.is_file():
|
|
157
|
+
raise ValueError("Declared Python entrypoint is not a physical regular file.")
|
|
158
|
+
os.chdir(working_root)
|
|
159
|
+
sys.dont_write_bytecode = True
|
|
160
|
+
import manim
|
|
161
|
+
import numpy as np
|
|
162
|
+
from PIL import Image
|
|
163
|
+
from manim import Scene, tempconfig
|
|
164
|
+
from manim.renderer.cairo_renderer import CairoRenderer
|
|
165
|
+
from manim.scene.scene_file_writer import SceneFileWriter
|
|
166
|
+
from manim.utils.exceptions import EndSceneEarlyException
|
|
167
|
+
if not manim.__version__.startswith("0.21."):
|
|
168
|
+
raise ValueError("This driver is qualified for the Manim Community0.21 profile only.")
|
|
169
|
+
dimensions = job["render"]
|
|
170
|
+
transparent = job["engine"]["transparent"]
|
|
171
|
+
maximum_bytes = job["limits"]["maximumOutputBytes"]
|
|
172
|
+
written_bytes = 0
|
|
173
|
+
selected = 0
|
|
174
|
+
movie_handle = container = stream = None
|
|
175
|
+
movie_output = next((output for output in job["outputs"] if output["format"] == "mp4"), None)
|
|
176
|
+
def budget():
|
|
177
|
+
movie_bytes = max(movie_handle.tell(), os.fstat(movie_handle.fileno()).st_size) if movie_handle is not None else 0
|
|
178
|
+
if written_bytes + movie_bytes > maximum_bytes:
|
|
179
|
+
raise ValueError("Manim exceeded the declared output-byte budget.")
|
|
180
|
+
|
|
181
|
+
class RetainedFileWriter(SceneFileWriter):
|
|
182
|
+
# The owned sink below handles every frame and codec. Starting an upstream
|
|
183
|
+
# partial encoder would leave a worker waiting when an exact frame stop
|
|
184
|
+
# interrupts an animation, even with write_to_movie=False in CE0.21.
|
|
185
|
+
def begin_animation(self, *args, **kwargs):
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
def end_animation(self, *args, **kwargs):
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
def finish(self):
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
def add_sound(self, *args, **kwargs):
|
|
195
|
+
raise ValueError("Compose retained narration and sound effects in the SLOPCAMERA project audio layer.")
|
|
196
|
+
|
|
197
|
+
def add_audio_segment(self, *args, **kwargs):
|
|
198
|
+
raise ValueError("Compose retained audio in the SLOPCAMERA project audio layer.")
|
|
199
|
+
|
|
200
|
+
class RetainedCairoRenderer(CairoRenderer):
|
|
201
|
+
raw_frames = 0
|
|
202
|
+
|
|
203
|
+
def add_frame(self, frame, num_frames=1):
|
|
204
|
+
nonlocal selected, written_bytes
|
|
205
|
+
if self.skip_animations:
|
|
206
|
+
return
|
|
207
|
+
if not isinstance(frame, np.ndarray) or frame.dtype != np.uint8 or frame.shape[:2] != (dimensions["height"], dimensions["width"]) or frame.shape[2] not in (3, 4):
|
|
208
|
+
raise ValueError("Manim returned pixels outside the declared uint8 frame geometry.")
|
|
209
|
+
if self.camera.frame_rate != float(fps):
|
|
210
|
+
raise ValueError("Source changed the admitted frame rate.")
|
|
211
|
+
for _ in range(num_frames):
|
|
212
|
+
index = self.raw_frames
|
|
213
|
+
if start <= index < end:
|
|
214
|
+
image = Image.fromarray(frame).convert("RGBA" if transparent else "RGB")
|
|
215
|
+
for output in job["outputs"]:
|
|
216
|
+
if output["format"] != "png" or output["kind"] == "file" and index != end-1:
|
|
217
|
+
continue
|
|
218
|
+
target = output["pathPattern"].replace("%06d", f"{index:06d}") if output["kind"] == "sequence" else output["path"]
|
|
219
|
+
with create_output(output_root, target) as destination:
|
|
220
|
+
image.save(destination, format="PNG")
|
|
221
|
+
written_bytes += destination.tell()
|
|
222
|
+
budget()
|
|
223
|
+
if stream is not None:
|
|
224
|
+
video_frame = av.VideoFrame.from_ndarray(np.asarray(image), format="rgb24")
|
|
225
|
+
video_frame.pts, video_frame.time_base = selected, Fraction(fps.denominator, fps.numerator)
|
|
226
|
+
for packet in stream.encode(video_frame):
|
|
227
|
+
container.mux(packet)
|
|
228
|
+
budget()
|
|
229
|
+
selected += 1
|
|
230
|
+
self.raw_frames += 1
|
|
231
|
+
self.time = self.raw_frames/float(fps)
|
|
232
|
+
if self.raw_frames >= end:
|
|
233
|
+
raise EndSceneEarlyException()
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
if movie_output is not None:
|
|
237
|
+
import av
|
|
238
|
+
movie_handle = create_output(output_root, movie_output["path"])
|
|
239
|
+
container = av.open(movie_handle, mode="w", format="mp4")
|
|
240
|
+
stream = container.add_stream("libx264rgb", rate=fps)
|
|
241
|
+
stream.width, stream.height = dimensions["width"], dimensions["height"]
|
|
242
|
+
stream.pix_fmt = "rgb24"
|
|
243
|
+
stream.options = {"crf": "0", "preset": "ultrafast", "threads": "1"}
|
|
244
|
+
stream.time_base = Fraction(fps.denominator, fps.numerator)
|
|
245
|
+
scratch = working_root / "manim"
|
|
246
|
+
scratch.mkdir(mode=0o700)
|
|
247
|
+
settings = {"renderer": "cairo", "pixel_width": dimensions["width"], "pixel_height": dimensions["height"],
|
|
248
|
+
"frame_width": 8.0, "frame_height": 8.0*dimensions["height"]/dimensions["width"], "frame_rate": float(fps),
|
|
249
|
+
"transparent": transparent, "write_to_movie": False, "save_last_frame": False, "save_pngs": False,
|
|
250
|
+
"format": "png", "disable_caching": True, "disable_caching_warning": True,
|
|
251
|
+
"media_dir": str(scratch), "text_dir": str(scratch/"texts"), "tex_dir": str(scratch/"tex"),
|
|
252
|
+
"video_dir": str(scratch/"videos"), "images_dir": str(scratch/"images"),
|
|
253
|
+
"partial_movie_dir": str(scratch/"partial"), "log_dir": str(scratch/"logs"),
|
|
254
|
+
"preview": False, "show_in_file_browser": False, "notify_outdated_version": False,
|
|
255
|
+
"progress_bar": "none", "verbosity": "ERROR", "seed": 0}
|
|
256
|
+
with tempconfig(settings):
|
|
257
|
+
context = source_context(request)
|
|
258
|
+
sys.path.insert(0, str(source_root))
|
|
259
|
+
namespace = runpy.run_path(str(entrypoint), init_globals={"SLOPCAMERA_CONTEXT": context}, run_name="__slopcamera_studio_source__")
|
|
260
|
+
scene_class = namespace.get(job["engine"]["scene"])
|
|
261
|
+
if not isinstance(scene_class, type) or not issubclass(scene_class, Scene):
|
|
262
|
+
raise ValueError("The selected source entrypoint did not declare a Manim Scene.")
|
|
263
|
+
renderer = RetainedCairoRenderer(file_writer_class=RetainedFileWriter)
|
|
264
|
+
scene_class(renderer=renderer).render()
|
|
265
|
+
if selected != end-start:
|
|
266
|
+
raise ValueError("The Scene ended before covering the exact requested frame interval.")
|
|
267
|
+
if stream is not None:
|
|
268
|
+
for packet in stream.encode():
|
|
269
|
+
container.mux(packet)
|
|
270
|
+
finished_container = container
|
|
271
|
+
container = None
|
|
272
|
+
finished_container.close()
|
|
273
|
+
budget()
|
|
274
|
+
report = {"engine": "manim", "version": manim.__version__, "renderer": "cairo", "scene": job["engine"]["scene"],
|
|
275
|
+
"frameRate": dimensions["frameRate"], "width": dimensions["width"], "height": dimensions["height"],
|
|
276
|
+
"startFrame": start, "endFrameExclusive": end, "selectedFrames": selected,
|
|
277
|
+
"color": "srgb-uint8", "alpha": "straight" if transparent else "opaque",
|
|
278
|
+
"movieEncoding": "lossless-libx264rgb" if movie_output else None, "audio": "host-owned"}
|
|
279
|
+
with (working_root/"result.json").open("x", encoding="utf8") as target:
|
|
280
|
+
json.dump(report, target, separators=(",", ":"))
|
|
281
|
+
target.write("\n")
|
|
282
|
+
finally:
|
|
283
|
+
primary_error = sys.exception()
|
|
284
|
+
cleanup_errors = []
|
|
285
|
+
for resource in (container, movie_handle):
|
|
286
|
+
if resource is not None:
|
|
287
|
+
try:
|
|
288
|
+
resource.close()
|
|
289
|
+
except BaseException as error:
|
|
290
|
+
cleanup_errors.append(error)
|
|
291
|
+
if cleanup_errors:
|
|
292
|
+
raise BaseExceptionGroup("Manim rendering and output cleanup failed.", ([primary_error] if primary_error is not None else []) + cleanup_errors)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def main():
|
|
296
|
+
parser = argparse.ArgumentParser(description="SLOPCAMERA fixed Manim Community driver")
|
|
297
|
+
mode = parser.add_mutually_exclusive_group(required=True)
|
|
298
|
+
mode.add_argument("--probe", action="store_true")
|
|
299
|
+
mode.add_argument("--request")
|
|
300
|
+
arguments = parser.parse_args()
|
|
301
|
+
if arguments.probe:
|
|
302
|
+
print("SLOPCAMERA_STUDIO_PROBE="+json.dumps(probe(), separators=(",", ":")))
|
|
303
|
+
else:
|
|
304
|
+
render_request(bounded_json(arguments.request))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
main()
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Pure retained lesson data and timing. Importing this module renders nothing."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
5
|
+
from math import isqrt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LessonError(ValueError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _object(value, required, optional=()):
|
|
13
|
+
if type(value) is not dict or not set(required) <= value.keys() or value.keys() - set(required) - set(optional):
|
|
14
|
+
raise LessonError("Lesson object has missing or unsupported fields.")
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _integer(value, minimum, maximum):
|
|
19
|
+
if type(value) is not int or not minimum <= value <= maximum:
|
|
20
|
+
raise LessonError("Lesson timing or count is outside its integer bound.")
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _text(value, maximum=80):
|
|
25
|
+
if not isinstance(value, str) or not value.strip() or len(value) > maximum or any(ord(c) < 32 for c in value):
|
|
26
|
+
raise LessonError("Lesson text must be bounded, nonempty and contain no control characters.")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _array(value, maximum):
|
|
31
|
+
if type(value) is not list or len(value) > maximum:
|
|
32
|
+
raise LessonError("Lesson cue list exceeds its bound.")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _interval(value, duration):
|
|
37
|
+
start = _integer(value["startUs"], 0, duration - 1)
|
|
38
|
+
end = _integer(value["endUs"], 1, duration)
|
|
39
|
+
if start >= end:
|
|
40
|
+
raise LessonError("Cue intervals must have positive duration.")
|
|
41
|
+
return start, end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ordered(cues, duration, fields):
|
|
45
|
+
previous = 0
|
|
46
|
+
for cue in cues:
|
|
47
|
+
_object(cue, ("startUs", "endUs", *fields))
|
|
48
|
+
start, previous_end = _interval(cue, duration)
|
|
49
|
+
if start < previous:
|
|
50
|
+
raise LessonError("Cue intervals must be ordered and nonoverlapping.")
|
|
51
|
+
previous = previous_end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def triangle_facts(legs):
|
|
55
|
+
if type(legs) is not list or len(legs) != 2:
|
|
56
|
+
raise LessonError("Pythagorean tiles require two integer legs.")
|
|
57
|
+
a, b = (_integer(side, 1, 12) for side in legs)
|
|
58
|
+
squared = a * a + b * b
|
|
59
|
+
c = isqrt(squared)
|
|
60
|
+
if c * c != squared or c > 15:
|
|
61
|
+
raise LessonError("This tile template requires an integer hypotenuse no larger than fifteen.")
|
|
62
|
+
return {"a": a, "b": b, "c": c, "areaA": a * a, "areaB": b * b,
|
|
63
|
+
"areaC": squared, "vertices": ((0, 0), (b, 0), (0, a))}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_lesson(value):
|
|
67
|
+
"""Validate authored JSON. No formulas, paths, providers or source are evaluated."""
|
|
68
|
+
value = _object(value, ("kind", "schemaVersion", "title", "subtitle", "durationUs", "topic",
|
|
69
|
+
"beats", "presenter", "captions", "captionTiming", "mouthCues", "mouthTiming", "gestures"),
|
|
70
|
+
("narration", "sfx", "showCaptions"))
|
|
71
|
+
if value["kind"] != "slopcamera.education-lesson" or type(value["schemaVersion"]) is not int or value["schemaVersion"] != 1:
|
|
72
|
+
raise LessonError("Unsupported educational lesson version.")
|
|
73
|
+
_text(value["title"], 48)
|
|
74
|
+
_text(value["subtitle"], 72)
|
|
75
|
+
duration = _integer(value["durationUs"], 1_000_000, 60_000_000)
|
|
76
|
+
topic = _object(value["topic"], ("kind", "legs"))
|
|
77
|
+
if topic["kind"] != "pythagorean-tiles":
|
|
78
|
+
raise LessonError("Unsupported lesson template; use trusted Python for other explanations.")
|
|
79
|
+
triangle_facts(topic["legs"])
|
|
80
|
+
beats = _object(value["beats"], ("squaresUs", "rearrangeUs", "resultUs"))
|
|
81
|
+
times = [_integer(beats[key], 1, duration - 1) for key in ("squaresUs", "rearrangeUs", "resultUs")]
|
|
82
|
+
if not 500_000 <= times[0] < times[1] < times[2] <= duration - 500_000:
|
|
83
|
+
raise LessonError("Lesson beats must increase with room for the opening and result.")
|
|
84
|
+
presenter = _object(value["presenter"], ("name",))
|
|
85
|
+
_text(presenter["name"], 16)
|
|
86
|
+
if value["captionTiming"] not in ("authored", "aligned") or value["mouthTiming"] not in ("authored", "rhubarb", "none"):
|
|
87
|
+
raise LessonError("Timing provenance must be explicit.")
|
|
88
|
+
if "showCaptions" in value and type(value["showCaptions"]) is not bool:
|
|
89
|
+
raise LessonError("showCaptions must be boolean.")
|
|
90
|
+
previous = 0
|
|
91
|
+
for caption in _array(value["captions"], 80):
|
|
92
|
+
_object(caption, ("words",))
|
|
93
|
+
words = _array(caption["words"], 12)
|
|
94
|
+
if not words or sum(len(_text(word.get("text"), 32)) + 1 for word in words if type(word) is dict) > 96:
|
|
95
|
+
raise LessonError("Caption phrases must have one to twelve short words.")
|
|
96
|
+
_ordered(words, duration, ("text",))
|
|
97
|
+
for word in words:
|
|
98
|
+
_text(word["text"], 32)
|
|
99
|
+
if words[0]["startUs"] < previous:
|
|
100
|
+
raise LessonError("Caption phrases must be ordered and nonoverlapping.")
|
|
101
|
+
previous = words[-1]["endUs"]
|
|
102
|
+
mouths = _array(value["mouthCues"], 2400)
|
|
103
|
+
_ordered(mouths, duration, ("value",))
|
|
104
|
+
for cue in mouths:
|
|
105
|
+
if cue["value"] not in tuple("ABCDEFGHX"):
|
|
106
|
+
raise LessonError("Mouth cues must use Rhubarb A–H or X shapes.")
|
|
107
|
+
if value["mouthTiming"] == "none" and mouths:
|
|
108
|
+
raise LessonError("A resting mouth cannot contain hidden timing cues.")
|
|
109
|
+
gestures = _array(value["gestures"], 160)
|
|
110
|
+
_ordered(gestures, duration, ("kind",))
|
|
111
|
+
if any(cue["kind"] not in ("point", "open", "nod", "rest") for cue in gestures):
|
|
112
|
+
raise LessonError("Unsupported presenter gesture.")
|
|
113
|
+
if "narration" in value:
|
|
114
|
+
narration = _object(value["narration"], ("assetId", "transcript"))
|
|
115
|
+
_text(narration["assetId"], 64)
|
|
116
|
+
_text(narration["transcript"], 4000)
|
|
117
|
+
for sound in _array(value.get("sfx", []), 80):
|
|
118
|
+
_object(sound, ("assetId", "atUs", "gainDb"))
|
|
119
|
+
_text(sound["assetId"], 64)
|
|
120
|
+
_integer(sound["atUs"], 0, duration - 1)
|
|
121
|
+
_integer(sound["gainDb"], -60, 0)
|
|
122
|
+
return deepcopy(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cue_at(cues, time_us):
|
|
126
|
+
"""Half-open intervals make boundaries independent of sample order."""
|
|
127
|
+
return next((cue for cue in cues if cue["startUs"] <= time_us < cue["endUs"]), None)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def caption_at(lesson, time_us):
|
|
131
|
+
for index, caption in enumerate(lesson["captions"]):
|
|
132
|
+
words = caption["words"]
|
|
133
|
+
if words[0]["startUs"] <= time_us < words[-1]["endUs"]:
|
|
134
|
+
active = next((i for i, word in enumerate(words) if word["startUs"] <= time_us < word["endUs"]), None)
|
|
135
|
+
return index, active
|
|
136
|
+
return None, None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def mouth_at(lesson, time_us):
|
|
140
|
+
cue = cue_at(lesson["mouthCues"], time_us)
|
|
141
|
+
return "X" if cue is None else cue["value"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def rhubarb_mouth_cues(value, duration_us, offset_us=0):
|
|
145
|
+
"""Import measured mouth timing; absolute metadata paths are never retained."""
|
|
146
|
+
_integer(duration_us, 1, 60_000_000)
|
|
147
|
+
_integer(offset_us, 0, duration_us - 1)
|
|
148
|
+
_object(value, ("metadata", "mouthCues"))
|
|
149
|
+
cues = []
|
|
150
|
+
for raw in _array(value["mouthCues"], 2400):
|
|
151
|
+
_object(raw, ("start", "end", "value"))
|
|
152
|
+
converted = {"value": raw["value"]}
|
|
153
|
+
for source, target in (("start", "startUs"), ("end", "endUs")):
|
|
154
|
+
if type(raw[source]) not in (int, float):
|
|
155
|
+
raise LessonError("Rhubarb timestamps must be finite numbers.")
|
|
156
|
+
try:
|
|
157
|
+
number = Decimal(str(raw[source]))
|
|
158
|
+
if not number.is_finite():
|
|
159
|
+
raise LessonError("Rhubarb timestamps must be finite numbers.")
|
|
160
|
+
converted[target] = offset_us + int((number * 1_000_000).to_integral_value(rounding=ROUND_HALF_UP))
|
|
161
|
+
except InvalidOperation as error:
|
|
162
|
+
raise LessonError("Invalid Rhubarb timestamp.") from error
|
|
163
|
+
cues.append(converted)
|
|
164
|
+
_ordered(cues, duration_us, ("value",))
|
|
165
|
+
if any(cue["value"] not in tuple("ABCDEFGHX") for cue in cues):
|
|
166
|
+
raise LessonError("Unsupported Rhubarb mouth shape.")
|
|
167
|
+
return cues
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def tile_layout(legs):
|
|
171
|
+
"""Stable tile identity and equal-sized source/target unit cells."""
|
|
172
|
+
facts = triangle_facts(legs)
|
|
173
|
+
cells = []
|
|
174
|
+
for group, side in (("a", facts["a"]), ("b", facts["b"])):
|
|
175
|
+
for row in range(side):
|
|
176
|
+
for column in range(side):
|
|
177
|
+
index = len(cells)
|
|
178
|
+
cells.append({"id": f"{group}_{row}_{column}", "group": group,
|
|
179
|
+
"source": (column, row), "target": (index % facts["c"], index // facts["c"])})
|
|
180
|
+
return cells
|