@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,888 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SpeechAnalysisV1Schema,
|
|
3
|
+
type SpeechAnalysisV1,
|
|
4
|
+
} from "../contracts/analysis";
|
|
5
|
+
import type { SourceInterval } from "../contracts/edit";
|
|
6
|
+
import {
|
|
7
|
+
ProjectEditPlanV1Schema,
|
|
8
|
+
VideoProjectV1Schema,
|
|
9
|
+
type ProjectEditPlanV1,
|
|
10
|
+
type VideoProjectV1,
|
|
11
|
+
} from "../contracts/project";
|
|
12
|
+
import {
|
|
13
|
+
canonicalJson,
|
|
14
|
+
canonicalJsonSha256,
|
|
15
|
+
sha256Hex,
|
|
16
|
+
} from "./canonical-json";
|
|
17
|
+
import { hashProjectStructure } from "./project-plan";
|
|
18
|
+
import {
|
|
19
|
+
buildProjectOutputTimeMap,
|
|
20
|
+
interpolateMicroseconds,
|
|
21
|
+
projectSyncSegments,
|
|
22
|
+
type ProjectOutputSegment,
|
|
23
|
+
type ProjectSyncSegment,
|
|
24
|
+
} from "./project-time";
|
|
25
|
+
|
|
26
|
+
export const PROJECT_CAPTION_LIMITS = Object.freeze({
|
|
27
|
+
maximumVisibleCues: 5_000,
|
|
28
|
+
maximumVisibleWords: 20_000,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export interface ProjectCaptionOutput {
|
|
32
|
+
/** When supplied, binds compilation to the already-planned output clock. */
|
|
33
|
+
readonly durationUs?: number;
|
|
34
|
+
readonly pixelHeight: number;
|
|
35
|
+
readonly pixelWidth: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ProjectCaptionLines =
|
|
39
|
+
| readonly [string]
|
|
40
|
+
| readonly [string, string];
|
|
41
|
+
|
|
42
|
+
export interface ProjectCaptionCue {
|
|
43
|
+
readonly lines: ProjectCaptionLines;
|
|
44
|
+
readonly outputRange: SourceInterval;
|
|
45
|
+
readonly projectRange: SourceInterval;
|
|
46
|
+
readonly sourceWordIndices: readonly number[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SocialCaptionSvg {
|
|
50
|
+
readonly bottomSafeMargin: number;
|
|
51
|
+
readonly intrinsicHeight: number;
|
|
52
|
+
readonly intrinsicWidth: number;
|
|
53
|
+
readonly svg: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CompileProjectCaptionCuesInput {
|
|
57
|
+
readonly analysis: SpeechAnalysisV1;
|
|
58
|
+
readonly output: ProjectCaptionOutput;
|
|
59
|
+
readonly placementId: VideoProjectV1["placements"][number]["placementId"] | string;
|
|
60
|
+
readonly plan: ProjectEditPlanV1;
|
|
61
|
+
readonly project: VideoProjectV1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface CaptionProfile {
|
|
65
|
+
readonly fontSize: number;
|
|
66
|
+
readonly maximumCueDurationUs: number;
|
|
67
|
+
readonly maximumGapUs: number;
|
|
68
|
+
readonly maximumLineUnits: number;
|
|
69
|
+
readonly maximumTokenFragmentsPerCue: number;
|
|
70
|
+
readonly maximumWordsPerCue: number;
|
|
71
|
+
readonly safeBottomFraction: number;
|
|
72
|
+
readonly targetCardWidth: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface MappedCaptionToken {
|
|
76
|
+
readonly assetRange: SourceInterval;
|
|
77
|
+
readonly mappingKey: string;
|
|
78
|
+
readonly outputRange: SourceInterval;
|
|
79
|
+
readonly projectRange: SourceInterval;
|
|
80
|
+
readonly sourceWordIndex: number;
|
|
81
|
+
readonly speaker: string | null;
|
|
82
|
+
readonly text: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter("und", {
|
|
86
|
+
granularity: "grapheme",
|
|
87
|
+
});
|
|
88
|
+
const CAPTION_WIDTH_UNITS_PER_EM = 1_000;
|
|
89
|
+
const EAST_ASIAN_WIDE_GRAPHEME = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\uff01-\uff60\uffe0-\uffe6]/u;
|
|
90
|
+
const EMOJI_OR_FLAG_GRAPHEME = /[\p{Extended_Pictographic}\p{Regional_Indicator}]/u;
|
|
91
|
+
const ASCII_WIDE_GRAPHEME = /^[%&@MWmw]$/u;
|
|
92
|
+
const ASCII_NARROW_GRAPHEME = /^[!"'(),.:;I\u005b\u005d`fijlt|]$/u;
|
|
93
|
+
const ASCII_UPPER_OR_DIGIT_GRAPHEME = /^[0-9A-Z]$/u;
|
|
94
|
+
const LETTER_OR_NUMBER = /[\p{Letter}\p{Number}]/gu;
|
|
95
|
+
const WHITESPACE_GRAPHEME = /^\s+$/u;
|
|
96
|
+
const CJK_WITHOUT_WORD_SPACES = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
|
97
|
+
const NO_SPACE_BEFORE = /^[,.;:!?%\u2026\u3001\u3002\uff0c\uff01\uff1a\uff1b\uff1f)\uff09\u3009\u300b\u3011\u300d\u300f\u2019\u201d\]}]/u;
|
|
98
|
+
const NO_SPACE_AFTER = /[(\uff08\u3008\u300a\u3010\u300c\u300e\u2018\u201c\u005b{]$/u;
|
|
99
|
+
const CONTRACTION_SUFFIX = /^(?:['\u2019](?:d|ll|m|re|s|t|ve))(?:\b|$)/iu;
|
|
100
|
+
const CUE_ENDING_PUNCTUATION = /[.!?;:\u2026\u3002\uff01\uff1a\uff1b\uff1f](?:['"\u2019\u201d)\]}\uff09\u3011\u300d\u300f\u300b]*)$/u;
|
|
101
|
+
const CLOSING_DELIMITER_TOKEN = /^["'\u2019\u201d)\]}\uff09\u3011\u300d\u300f\u300b]+$/u;
|
|
102
|
+
const PUNCTUATION_TOKEN = /^\p{Punctuation}+$/u;
|
|
103
|
+
const LETTER_OR_NUMBER_CHARACTER = /^[\p{Letter}\p{Number}]$/u;
|
|
104
|
+
const MAXIMUM_ATTACHED_PUNCTUATION_FRAGMENTS = 8;
|
|
105
|
+
|
|
106
|
+
interface CaptionGrapheme {
|
|
107
|
+
readonly endIndex: number;
|
|
108
|
+
readonly segment: string;
|
|
109
|
+
readonly startIndex: number;
|
|
110
|
+
readonly widthUnits: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requireOutput(output: ProjectCaptionOutput): void {
|
|
114
|
+
if (
|
|
115
|
+
!Number.isSafeInteger(output.pixelWidth)
|
|
116
|
+
|| output.pixelWidth <= 0
|
|
117
|
+
|| output.pixelWidth > 16_384
|
|
118
|
+
|| !Number.isSafeInteger(output.pixelHeight)
|
|
119
|
+
|| output.pixelHeight <= 0
|
|
120
|
+
|| output.pixelHeight > 16_384
|
|
121
|
+
|| output.pixelWidth * output.pixelHeight > 134_217_728
|
|
122
|
+
) {
|
|
123
|
+
throw new RangeError("Caption output dimensions are invalid or exceed the 128-megapixel bound.");
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
output.durationUs !== undefined
|
|
127
|
+
&& (!Number.isSafeInteger(output.durationUs) || output.durationUs < 0)
|
|
128
|
+
) {
|
|
129
|
+
throw new RangeError("Caption output duration must be a nonnegative safe integer.");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function captionProfile(output: ProjectCaptionOutput): CaptionProfile {
|
|
134
|
+
requireOutput(output);
|
|
135
|
+
const aspect = output.pixelWidth / output.pixelHeight;
|
|
136
|
+
const family = aspect >= 1.15
|
|
137
|
+
? "landscape"
|
|
138
|
+
: aspect <= 0.68
|
|
139
|
+
? "portrait"
|
|
140
|
+
: "feed";
|
|
141
|
+
const geometry = family === "landscape"
|
|
142
|
+
? {
|
|
143
|
+
fontScale: 0.052,
|
|
144
|
+
maximumCueDurationUs: 3_400_000,
|
|
145
|
+
maximumGapUs: 700_000,
|
|
146
|
+
maximumWordsPerCue: 12,
|
|
147
|
+
safeBottomFraction: 0.075,
|
|
148
|
+
widthFraction: 0.78,
|
|
149
|
+
}
|
|
150
|
+
: family === "portrait"
|
|
151
|
+
? {
|
|
152
|
+
fontScale: 0.06,
|
|
153
|
+
maximumCueDurationUs: 2_800_000,
|
|
154
|
+
maximumGapUs: 600_000,
|
|
155
|
+
maximumWordsPerCue: 7,
|
|
156
|
+
safeBottomFraction: 0.17,
|
|
157
|
+
widthFraction: 0.9,
|
|
158
|
+
}
|
|
159
|
+
: {
|
|
160
|
+
fontScale: 0.052,
|
|
161
|
+
maximumCueDurationUs: 3_100_000,
|
|
162
|
+
maximumGapUs: 650_000,
|
|
163
|
+
maximumWordsPerCue: 9,
|
|
164
|
+
safeBottomFraction: 0.105,
|
|
165
|
+
widthFraction: 0.9,
|
|
166
|
+
};
|
|
167
|
+
const fontSize = Math.max(1, Math.round(
|
|
168
|
+
Math.min(output.pixelWidth, output.pixelHeight) * geometry.fontScale,
|
|
169
|
+
));
|
|
170
|
+
const targetCardWidth = Math.max(1, Math.min(
|
|
171
|
+
output.pixelWidth,
|
|
172
|
+
Math.round(output.pixelWidth * geometry.widthFraction),
|
|
173
|
+
));
|
|
174
|
+
const horizontalPadding = Math.max(0, Math.round(fontSize * 0.65));
|
|
175
|
+
const usableWidth = Math.max(1, targetCardWidth - horizontalPadding * 2);
|
|
176
|
+
const maximumLineUnits = Math.max(1, Math.floor(
|
|
177
|
+
usableWidth * CAPTION_WIDTH_UNITS_PER_EM / fontSize,
|
|
178
|
+
));
|
|
179
|
+
return {
|
|
180
|
+
fontSize,
|
|
181
|
+
maximumCueDurationUs: geometry.maximumCueDurationUs,
|
|
182
|
+
maximumGapUs: geometry.maximumGapUs,
|
|
183
|
+
maximumLineUnits,
|
|
184
|
+
maximumTokenFragmentsPerCue:
|
|
185
|
+
geometry.maximumWordsPerCue + MAXIMUM_ATTACHED_PUNCTUATION_FRAGMENTS,
|
|
186
|
+
maximumWordsPerCue: geometry.maximumWordsPerCue,
|
|
187
|
+
safeBottomFraction: geometry.safeBottomFraction,
|
|
188
|
+
targetCardWidth,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function intersection(left: SourceInterval, right: SourceInterval): SourceInterval | null {
|
|
193
|
+
const startUs = Math.max(left.startUs, right.startUs);
|
|
194
|
+
const endUs = Math.min(left.endUs, right.endUs);
|
|
195
|
+
return endUs > startUs ? { endUs, startUs } : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function graphemeWidth(value: string): number {
|
|
199
|
+
if (WHITESPACE_GRAPHEME.test(value)) return 350;
|
|
200
|
+
if (EMOJI_OR_FLAG_GRAPHEME.test(value) || EAST_ASIAN_WIDE_GRAPHEME.test(value)) {
|
|
201
|
+
return 1_100;
|
|
202
|
+
}
|
|
203
|
+
if (ASCII_WIDE_GRAPHEME.test(value)) return 1_000;
|
|
204
|
+
if (ASCII_NARROW_GRAPHEME.test(value)) return 420;
|
|
205
|
+
if (ASCII_UPPER_OR_DIGIT_GRAPHEME.test(value)) return 720;
|
|
206
|
+
if (/^[\x20-\x7e]$/u.test(value)) return 620;
|
|
207
|
+
|
|
208
|
+
// A grapheme in a shaped script can contain several joined letters. Count
|
|
209
|
+
// those bases instead of assuming that every cluster occupies one Latin
|
|
210
|
+
// average advance. SVG textLength then makes this deterministic estimate the
|
|
211
|
+
// rendered advance, including when the renderer chooses a fallback font.
|
|
212
|
+
const baseCount = [...value.matchAll(LETTER_OR_NUMBER)].length;
|
|
213
|
+
return Math.max(850, Math.min(2_800, baseCount * 700));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function captionGraphemes(value: string): readonly CaptionGrapheme[] {
|
|
217
|
+
return [...GRAPHEME_SEGMENTER.segment(value)].map(item => ({
|
|
218
|
+
endIndex: item.index + item.segment.length,
|
|
219
|
+
segment: item.segment,
|
|
220
|
+
startIndex: item.index,
|
|
221
|
+
widthUnits: graphemeWidth(item.segment),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function displayWidth(value: string): number {
|
|
226
|
+
return captionGraphemes(value).reduce((width, grapheme) => width + grapheme.widthUnits, 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function lastGrapheme(value: string): string {
|
|
230
|
+
return captionGraphemes(value).at(-1)?.segment ?? "";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function firstGrapheme(value: string): string {
|
|
234
|
+
return captionGraphemes(value)[0]?.segment ?? "";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function straightQuoteCount(value: string, quote: "\"" | "'"): number {
|
|
238
|
+
const characters = [...value];
|
|
239
|
+
let count = 0;
|
|
240
|
+
for (let index = 0; index < characters.length; index += 1) {
|
|
241
|
+
if (characters[index] !== quote) continue;
|
|
242
|
+
if (
|
|
243
|
+
quote === "'"
|
|
244
|
+
&& LETTER_OR_NUMBER_CHARACTER.test(characters[index - 1] ?? "")
|
|
245
|
+
&& LETTER_OR_NUMBER_CHARACTER.test(characters[index + 1] ?? "")
|
|
246
|
+
) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
count += 1;
|
|
250
|
+
}
|
|
251
|
+
return count;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function straightQuoteCloses(previous: string, quote: "\"" | "'"): boolean {
|
|
255
|
+
return straightQuoteCount(previous, quote) % 2 === 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isClosingDelimiterToken(previous: string, current: string): boolean {
|
|
259
|
+
if (!CLOSING_DELIMITER_TOKEN.test(current)) return false;
|
|
260
|
+
let context = previous;
|
|
261
|
+
for (const character of current) {
|
|
262
|
+
if (
|
|
263
|
+
(character === '"' || character === "'")
|
|
264
|
+
&& !straightQuoteCloses(context, character)
|
|
265
|
+
) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
context += character;
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function needsTokenSpace(previous: string, current: string): boolean {
|
|
274
|
+
if (NO_SPACE_BEFORE.test(current) || NO_SPACE_AFTER.test(previous) || CONTRACTION_SUFFIX.test(current)) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const previousEnd = lastGrapheme(previous);
|
|
278
|
+
const currentStart = firstGrapheme(current);
|
|
279
|
+
if (
|
|
280
|
+
(currentStart === '"' || currentStart === "'")
|
|
281
|
+
&& straightQuoteCloses(previous, currentStart)
|
|
282
|
+
) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
(previousEnd === '"' || previousEnd === "'")
|
|
287
|
+
&& straightQuoteCount(previous, previousEnd) % 2 === 1
|
|
288
|
+
) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
return !(CJK_WITHOUT_WORD_SPACES.test(previousEnd) && CJK_WITHOUT_WORD_SPACES.test(currentStart));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function joinedTokens(tokens: readonly MappedCaptionToken[]): string {
|
|
295
|
+
let text = "";
|
|
296
|
+
for (const token of tokens) {
|
|
297
|
+
if (text.length === 0) {
|
|
298
|
+
text = token.text;
|
|
299
|
+
} else {
|
|
300
|
+
text += `${needsTokenSpace(text, token.text) ? " " : ""}${token.text}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return text;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isNaturalLineBreak(leftGrapheme: string, rightGrapheme: string): boolean {
|
|
307
|
+
return /\s$/u.test(leftGrapheme)
|
|
308
|
+
|| /^\s/u.test(rightGrapheme)
|
|
309
|
+
|| (
|
|
310
|
+
CJK_WITHOUT_WORD_SPACES.test(leftGrapheme)
|
|
311
|
+
&& CJK_WITHOUT_WORD_SPACES.test(rightGrapheme)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function wrapCaptionText(text: string, maximumLineUnits: number): ProjectCaptionLines | null {
|
|
316
|
+
const segments = captionGraphemes(text);
|
|
317
|
+
const prefixWidths = new Array<number>(segments.length + 1).fill(0);
|
|
318
|
+
const lastNonWhitespace = new Array<number>(segments.length + 1).fill(0);
|
|
319
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
320
|
+
const segment = segments[index]!;
|
|
321
|
+
prefixWidths[index + 1] = prefixWidths[index]! + segment.widthUnits;
|
|
322
|
+
lastNonWhitespace[index + 1] = WHITESPACE_GRAPHEME.test(segment.segment)
|
|
323
|
+
? lastNonWhitespace[index]!
|
|
324
|
+
: index + 1;
|
|
325
|
+
}
|
|
326
|
+
const totalWidth = prefixWidths.at(-1) ?? 0;
|
|
327
|
+
if (totalWidth <= maximumLineUnits) return [text];
|
|
328
|
+
if (totalWidth > maximumLineUnits * 2) return null;
|
|
329
|
+
|
|
330
|
+
const firstNonWhitespace = new Array<number>(segments.length + 1).fill(segments.length);
|
|
331
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
332
|
+
firstNonWhitespace[index] = WHITESPACE_GRAPHEME.test(segments[index]!.segment)
|
|
333
|
+
? firstNonWhitespace[index + 1]!
|
|
334
|
+
: index;
|
|
335
|
+
}
|
|
336
|
+
let best: {
|
|
337
|
+
readonly imbalance: number;
|
|
338
|
+
readonly leftEnd: number;
|
|
339
|
+
readonly natural: boolean;
|
|
340
|
+
readonly rightStart: number;
|
|
341
|
+
} | null = null;
|
|
342
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
343
|
+
const leftEnd = lastNonWhitespace[index]!;
|
|
344
|
+
const rightStart = firstNonWhitespace[index]!;
|
|
345
|
+
if (leftEnd === 0 || rightStart === segments.length) continue;
|
|
346
|
+
const leftWidth = prefixWidths[leftEnd]!;
|
|
347
|
+
const rightWidth = totalWidth - prefixWidths[rightStart]!;
|
|
348
|
+
if (leftWidth > maximumLineUnits || rightWidth > maximumLineUnits) continue;
|
|
349
|
+
const natural = isNaturalLineBreak(
|
|
350
|
+
segments[index - 1]!.segment,
|
|
351
|
+
segments[index]!.segment,
|
|
352
|
+
);
|
|
353
|
+
const imbalance = Math.abs(leftWidth - rightWidth);
|
|
354
|
+
if (
|
|
355
|
+
best === null
|
|
356
|
+
|| (natural && !best.natural)
|
|
357
|
+
|| (natural === best.natural && imbalance < best.imbalance)
|
|
358
|
+
) {
|
|
359
|
+
best = { imbalance, leftEnd, natural, rightStart };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (best === null) return null;
|
|
363
|
+
return [
|
|
364
|
+
text.slice(0, segments[best.leftEnd - 1]!.endIndex),
|
|
365
|
+
text.slice(segments[best.rightStart]!.startIndex),
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeToken(value: string): string {
|
|
370
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function splitTextToFit(value: string, maximumLineUnits: number): readonly string[] {
|
|
374
|
+
if (wrapCaptionText(value, maximumLineUnits) !== null) return [value];
|
|
375
|
+
const chunks: string[] = [];
|
|
376
|
+
let chunk = "";
|
|
377
|
+
let width = 0;
|
|
378
|
+
for (const grapheme of captionGraphemes(value)) {
|
|
379
|
+
const nextWidth = grapheme.widthUnits;
|
|
380
|
+
if (nextWidth > maximumLineUnits) {
|
|
381
|
+
throw new RangeError("A transcript grapheme cannot fit within the caption line bound.");
|
|
382
|
+
}
|
|
383
|
+
if (chunk.length > 0 && width + nextWidth > maximumLineUnits) {
|
|
384
|
+
const trimmed = chunk.trim();
|
|
385
|
+
if (trimmed.length > 0) chunks.push(trimmed);
|
|
386
|
+
chunk = "";
|
|
387
|
+
width = 0;
|
|
388
|
+
}
|
|
389
|
+
chunk += grapheme.segment;
|
|
390
|
+
width += nextWidth;
|
|
391
|
+
}
|
|
392
|
+
const trimmed = chunk.trim();
|
|
393
|
+
if (trimmed.length > 0) chunks.push(trimmed);
|
|
394
|
+
if (chunks.length === 0 || chunks.some(candidate => wrapCaptionText(candidate, maximumLineUnits) === null)) {
|
|
395
|
+
throw new RangeError("A transcript token cannot fit within the two-line caption bound.");
|
|
396
|
+
}
|
|
397
|
+
return chunks;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function splitMappedToken(
|
|
401
|
+
token: MappedCaptionToken,
|
|
402
|
+
maximumLineUnits: number,
|
|
403
|
+
): readonly MappedCaptionToken[] {
|
|
404
|
+
const chunks = splitTextToFit(token.text, maximumLineUnits);
|
|
405
|
+
if (chunks.length === 1) return [token];
|
|
406
|
+
const ranges = [token.assetRange, token.projectRange, token.outputRange];
|
|
407
|
+
if (ranges.some(range => range.endUs - range.startUs < chunks.length)) {
|
|
408
|
+
throw new RangeError(
|
|
409
|
+
`Transcript word ${token.sourceWordIndex} is too short to split into bounded caption lines.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const weights = chunks.map(chunk => Math.max(1, displayWidth(chunk)));
|
|
413
|
+
const totalWeight = weights.reduce((total, weight) => total + weight, 0);
|
|
414
|
+
let priorWeight = 0;
|
|
415
|
+
return chunks.map((text, index) => {
|
|
416
|
+
const nextWeight = priorWeight + weights[index]!;
|
|
417
|
+
const rangeAt = (range: SourceInterval): SourceInterval => ({
|
|
418
|
+
endUs: interpolateMicroseconds(
|
|
419
|
+
nextWeight,
|
|
420
|
+
0,
|
|
421
|
+
totalWeight,
|
|
422
|
+
range.startUs,
|
|
423
|
+
range.endUs,
|
|
424
|
+
),
|
|
425
|
+
startUs: interpolateMicroseconds(
|
|
426
|
+
priorWeight,
|
|
427
|
+
0,
|
|
428
|
+
totalWeight,
|
|
429
|
+
range.startUs,
|
|
430
|
+
range.endUs,
|
|
431
|
+
),
|
|
432
|
+
});
|
|
433
|
+
const output: MappedCaptionToken = {
|
|
434
|
+
...token,
|
|
435
|
+
assetRange: rangeAt(token.assetRange),
|
|
436
|
+
mappingKey: `${token.mappingKey}:token-part-${index}`,
|
|
437
|
+
outputRange: rangeAt(token.outputRange),
|
|
438
|
+
projectRange: rangeAt(token.projectRange),
|
|
439
|
+
text,
|
|
440
|
+
};
|
|
441
|
+
priorWeight = nextWeight;
|
|
442
|
+
if (
|
|
443
|
+
output.assetRange.endUs <= output.assetRange.startUs
|
|
444
|
+
|| output.projectRange.endUs <= output.projectRange.startUs
|
|
445
|
+
|| output.outputRange.endUs <= output.outputRange.startUs
|
|
446
|
+
) {
|
|
447
|
+
throw new RangeError(
|
|
448
|
+
`Transcript word ${token.sourceWordIndex} collapsed while splitting bounded caption lines.`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return output;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function firstSyncSegmentEndingAfter(
|
|
456
|
+
segments: readonly ProjectSyncSegment[],
|
|
457
|
+
assetTimeUs: number,
|
|
458
|
+
): number {
|
|
459
|
+
let low = 0;
|
|
460
|
+
let high = segments.length;
|
|
461
|
+
while (low < high) {
|
|
462
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
463
|
+
if (segments[middle]!.asset.endUs <= assetTimeUs) low = middle + 1;
|
|
464
|
+
else high = middle;
|
|
465
|
+
}
|
|
466
|
+
return low;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function firstOutputSegmentEndingAfter(
|
|
470
|
+
segments: readonly ProjectOutputSegment[],
|
|
471
|
+
projectTimeUs: number,
|
|
472
|
+
): number {
|
|
473
|
+
let low = 0;
|
|
474
|
+
let high = segments.length;
|
|
475
|
+
while (low < high) {
|
|
476
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
477
|
+
if (segments[middle]!.project.endUs <= projectTimeUs) low = middle + 1;
|
|
478
|
+
else high = middle;
|
|
479
|
+
}
|
|
480
|
+
return low;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function* mappedWordTokens(input: {
|
|
484
|
+
readonly maximumLineUnits: number;
|
|
485
|
+
readonly outputSegments: readonly ProjectOutputSegment[];
|
|
486
|
+
readonly syncSegments: readonly ProjectSyncSegment[];
|
|
487
|
+
readonly word: Extract<SpeechAnalysisV1["result"], { readonly status: "transcribed" }>["words"][number];
|
|
488
|
+
}): Generator<MappedCaptionToken, void> {
|
|
489
|
+
const text = normalizeToken(input.word.text);
|
|
490
|
+
if (text.length === 0) return;
|
|
491
|
+
for (
|
|
492
|
+
let syncIndex = firstSyncSegmentEndingAfter(input.syncSegments, input.word.range.startUs);
|
|
493
|
+
syncIndex < input.syncSegments.length;
|
|
494
|
+
syncIndex += 1
|
|
495
|
+
) {
|
|
496
|
+
const sync = input.syncSegments[syncIndex]!;
|
|
497
|
+
if (sync.asset.startUs >= input.word.range.endUs) break;
|
|
498
|
+
const assetRange = intersection(input.word.range, sync.asset);
|
|
499
|
+
if (assetRange === null) continue;
|
|
500
|
+
const mappedProjectRange = {
|
|
501
|
+
endUs: interpolateMicroseconds(
|
|
502
|
+
assetRange.endUs,
|
|
503
|
+
sync.asset.startUs,
|
|
504
|
+
sync.asset.endUs,
|
|
505
|
+
sync.project.startUs,
|
|
506
|
+
sync.project.endUs,
|
|
507
|
+
),
|
|
508
|
+
startUs: interpolateMicroseconds(
|
|
509
|
+
assetRange.startUs,
|
|
510
|
+
sync.asset.startUs,
|
|
511
|
+
sync.asset.endUs,
|
|
512
|
+
sync.project.startUs,
|
|
513
|
+
sync.project.endUs,
|
|
514
|
+
),
|
|
515
|
+
};
|
|
516
|
+
if (mappedProjectRange.endUs <= mappedProjectRange.startUs) continue;
|
|
517
|
+
for (
|
|
518
|
+
let outputIndex = firstOutputSegmentEndingAfter(
|
|
519
|
+
input.outputSegments,
|
|
520
|
+
mappedProjectRange.startUs,
|
|
521
|
+
);
|
|
522
|
+
outputIndex < input.outputSegments.length;
|
|
523
|
+
outputIndex += 1
|
|
524
|
+
) {
|
|
525
|
+
const outputSegment = input.outputSegments[outputIndex]!;
|
|
526
|
+
if (outputSegment.project.startUs >= mappedProjectRange.endUs) break;
|
|
527
|
+
const projectRange = intersection(mappedProjectRange, outputSegment.project);
|
|
528
|
+
if (projectRange === null) continue;
|
|
529
|
+
const fragmentAssetRange = {
|
|
530
|
+
endUs: interpolateMicroseconds(
|
|
531
|
+
projectRange.endUs,
|
|
532
|
+
sync.project.startUs,
|
|
533
|
+
sync.project.endUs,
|
|
534
|
+
sync.asset.startUs,
|
|
535
|
+
sync.asset.endUs,
|
|
536
|
+
),
|
|
537
|
+
startUs: interpolateMicroseconds(
|
|
538
|
+
projectRange.startUs,
|
|
539
|
+
sync.project.startUs,
|
|
540
|
+
sync.project.endUs,
|
|
541
|
+
sync.asset.startUs,
|
|
542
|
+
sync.asset.endUs,
|
|
543
|
+
),
|
|
544
|
+
};
|
|
545
|
+
const outputRange = {
|
|
546
|
+
endUs: interpolateMicroseconds(
|
|
547
|
+
projectRange.endUs,
|
|
548
|
+
outputSegment.project.startUs,
|
|
549
|
+
outputSegment.project.endUs,
|
|
550
|
+
outputSegment.output.startUs,
|
|
551
|
+
outputSegment.output.endUs,
|
|
552
|
+
),
|
|
553
|
+
startUs: interpolateMicroseconds(
|
|
554
|
+
projectRange.startUs,
|
|
555
|
+
outputSegment.project.startUs,
|
|
556
|
+
outputSegment.project.endUs,
|
|
557
|
+
outputSegment.output.startUs,
|
|
558
|
+
outputSegment.output.endUs,
|
|
559
|
+
),
|
|
560
|
+
};
|
|
561
|
+
if (
|
|
562
|
+
fragmentAssetRange.endUs <= fragmentAssetRange.startUs
|
|
563
|
+
|| outputRange.endUs <= outputRange.startUs
|
|
564
|
+
) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
yield* splitMappedToken({
|
|
568
|
+
assetRange: fragmentAssetRange,
|
|
569
|
+
mappingKey: `${syncIndex}:${outputIndex}`,
|
|
570
|
+
outputRange,
|
|
571
|
+
projectRange,
|
|
572
|
+
sourceWordIndex: input.word.wordIndex,
|
|
573
|
+
speaker: input.word.speaker,
|
|
574
|
+
text,
|
|
575
|
+
}, input.maximumLineUnits);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function assertCurrentCaptionInputs(
|
|
581
|
+
project: VideoProjectV1,
|
|
582
|
+
plan: ProjectEditPlanV1,
|
|
583
|
+
analysis: SpeechAnalysisV1,
|
|
584
|
+
placementId: string,
|
|
585
|
+
): {
|
|
586
|
+
readonly placement: VideoProjectV1["placements"][number];
|
|
587
|
+
readonly words: Extract<SpeechAnalysisV1["result"], { readonly status: "transcribed" }>["words"];
|
|
588
|
+
} {
|
|
589
|
+
if (plan.projectId !== project.projectId) {
|
|
590
|
+
throw new TypeError("Caption edit plan belongs to another project.");
|
|
591
|
+
}
|
|
592
|
+
if (
|
|
593
|
+
plan.timelineDurationUs !== project.timeline.durationUs
|
|
594
|
+
|| plan.projectStructureSha256 !== hashProjectStructure(project)
|
|
595
|
+
) {
|
|
596
|
+
throw new TypeError("Caption project and edit plan are stale or contradictory.");
|
|
597
|
+
}
|
|
598
|
+
const placement = project.placements.find(candidate => candidate.placementId === placementId);
|
|
599
|
+
if (placement === undefined) {
|
|
600
|
+
throw new TypeError(`Caption placement ${placementId} does not exist in the project.`);
|
|
601
|
+
}
|
|
602
|
+
if (!placement.enabled) {
|
|
603
|
+
throw new TypeError(`Caption placement ${placementId} is disabled.`);
|
|
604
|
+
}
|
|
605
|
+
if (analysis.subject.assetId !== placement.assetId) {
|
|
606
|
+
throw new TypeError("Caption speech analysis does not match the selected placement asset.");
|
|
607
|
+
}
|
|
608
|
+
const configured = placement.audio.find(candidate => candidate.streamId === analysis.subject.streamId);
|
|
609
|
+
if (configured?.presentation.enabled !== true) {
|
|
610
|
+
throw new TypeError("Caption speech stream is not audibly enabled on the selected placement.");
|
|
611
|
+
}
|
|
612
|
+
const asset = project.assets.find(candidate => candidate.assetId === placement.assetId);
|
|
613
|
+
const stream = asset?.streams.find(candidate => candidate.streamId === analysis.subject.streamId);
|
|
614
|
+
if (asset === undefined || stream?.kind !== "audio") {
|
|
615
|
+
throw new TypeError("Caption speech analysis does not identify a current project audio stream.");
|
|
616
|
+
}
|
|
617
|
+
const expectedSubjectIntegrity = canonicalJsonSha256({
|
|
618
|
+
assetDurationUs: asset.durationUs,
|
|
619
|
+
stream,
|
|
620
|
+
});
|
|
621
|
+
if (
|
|
622
|
+
analysis.durationUs !== asset.durationUs
|
|
623
|
+
|| analysis.subject.integritySha256 !== expectedSubjectIntegrity
|
|
624
|
+
) {
|
|
625
|
+
throw new TypeError("Caption speech analysis is stale for the current audio stream.");
|
|
626
|
+
}
|
|
627
|
+
const reference = project.analyses.find(candidate => candidate.analysisId === analysis.analysisId);
|
|
628
|
+
if (reference === undefined || reference.kind !== "speech") {
|
|
629
|
+
throw new TypeError("Caption speech analysis is not registered by the current project.");
|
|
630
|
+
}
|
|
631
|
+
const wordCount = analysis.result.status === "transcribed" ? analysis.result.words.length : 0;
|
|
632
|
+
const fillerCount = analysis.result.status === "transcribed" ? analysis.result.fillers.length : 0;
|
|
633
|
+
if (
|
|
634
|
+
reference.assetId !== analysis.subject.assetId
|
|
635
|
+
|| reference.streamId !== analysis.subject.streamId
|
|
636
|
+
|| reference.createdAt !== analysis.createdAt
|
|
637
|
+
|| reference.wordCount !== wordCount
|
|
638
|
+
|| reference.fillerCount !== fillerCount
|
|
639
|
+
|| reference.sha256 !== sha256Hex(`${canonicalJson(analysis)}\n`)
|
|
640
|
+
) {
|
|
641
|
+
throw new TypeError("Caption speech analysis contradicts its current project reference.");
|
|
642
|
+
}
|
|
643
|
+
if (analysis.result.status === "no-speech") {
|
|
644
|
+
throw new TypeError("Caption speech analysis contains no speech.");
|
|
645
|
+
}
|
|
646
|
+
return { placement, words: analysis.result.words };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function sourceIndices(tokens: readonly MappedCaptionToken[]): readonly number[] {
|
|
650
|
+
const output: number[] = [];
|
|
651
|
+
for (const token of tokens) {
|
|
652
|
+
if (output.at(-1) !== token.sourceWordIndex) output.push(token.sourceWordIndex);
|
|
653
|
+
}
|
|
654
|
+
return output;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function canAppendToken(
|
|
658
|
+
tokens: readonly MappedCaptionToken[],
|
|
659
|
+
next: MappedCaptionToken,
|
|
660
|
+
profile: CaptionProfile,
|
|
661
|
+
): boolean {
|
|
662
|
+
const previous = tokens.at(-1);
|
|
663
|
+
const first = tokens[0];
|
|
664
|
+
if (previous === undefined || first === undefined) return true;
|
|
665
|
+
if (tokens.length >= profile.maximumTokenFragmentsPerCue) return false;
|
|
666
|
+
if (
|
|
667
|
+
previous.mappingKey !== next.mappingKey
|
|
668
|
+
|| next.sourceWordIndex !== previous.sourceWordIndex + 1
|
|
669
|
+
|| previous.speaker !== next.speaker
|
|
670
|
+
|| next.outputRange.startUs < previous.outputRange.endUs
|
|
671
|
+
|| next.projectRange.startUs < previous.projectRange.endUs
|
|
672
|
+
|| next.outputRange.startUs - previous.outputRange.endUs > profile.maximumGapUs
|
|
673
|
+
) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
const pendingText = joinedTokens(tokens);
|
|
677
|
+
if (
|
|
678
|
+
CUE_ENDING_PUNCTUATION.test(pendingText)
|
|
679
|
+
&& !isClosingDelimiterToken(pendingText, next.text)
|
|
680
|
+
) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
const attachesPunctuation = PUNCTUATION_TOKEN.test(next.text)
|
|
684
|
+
&& !needsTokenSpace(pendingText, next.text);
|
|
685
|
+
if (
|
|
686
|
+
sourceIndices([...tokens, next]).length > profile.maximumWordsPerCue
|
|
687
|
+
&& !attachesPunctuation
|
|
688
|
+
) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
if (next.outputRange.endUs - first.outputRange.startUs > profile.maximumCueDurationUs) return false;
|
|
692
|
+
return wrapCaptionText(joinedTokens([...tokens, next]), profile.maximumLineUnits) !== null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function cueFromTokens(
|
|
696
|
+
tokens: readonly MappedCaptionToken[],
|
|
697
|
+
profile: CaptionProfile,
|
|
698
|
+
): ProjectCaptionCue {
|
|
699
|
+
const first = tokens[0];
|
|
700
|
+
const last = tokens.at(-1);
|
|
701
|
+
if (first === undefined || last === undefined) {
|
|
702
|
+
throw new TypeError("Cannot build an empty project caption cue.");
|
|
703
|
+
}
|
|
704
|
+
const lines = wrapCaptionText(joinedTokens(tokens), profile.maximumLineUnits);
|
|
705
|
+
if (lines === null) throw new RangeError("Project caption cue exceeds its two-line width bound.");
|
|
706
|
+
return {
|
|
707
|
+
lines,
|
|
708
|
+
outputRange: {
|
|
709
|
+
endUs: last.outputRange.endUs,
|
|
710
|
+
startUs: first.outputRange.startUs,
|
|
711
|
+
},
|
|
712
|
+
projectRange: {
|
|
713
|
+
endUs: last.projectRange.endUs,
|
|
714
|
+
startUs: first.projectRange.startUs,
|
|
715
|
+
},
|
|
716
|
+
sourceWordIndices: sourceIndices(tokens),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function assertCueLaws(cues: readonly ProjectCaptionCue[]): void {
|
|
721
|
+
for (let index = 0; index < cues.length; index += 1) {
|
|
722
|
+
const cue = cues[index]!;
|
|
723
|
+
if (
|
|
724
|
+
cue.outputRange.endUs <= cue.outputRange.startUs
|
|
725
|
+
|| cue.projectRange.endUs <= cue.projectRange.startUs
|
|
726
|
+
|| cue.lines.length < 1
|
|
727
|
+
|| cue.lines.length > 2
|
|
728
|
+
|| cue.lines.some(line => line.length === 0)
|
|
729
|
+
|| cue.sourceWordIndices.length === 0
|
|
730
|
+
) {
|
|
731
|
+
throw new TypeError(`Compiled caption cue ${index} is empty or inverted.`);
|
|
732
|
+
}
|
|
733
|
+
const previous = cues[index - 1];
|
|
734
|
+
if (previous !== undefined && (
|
|
735
|
+
cue.outputRange.startUs < previous.outputRange.endUs
|
|
736
|
+
|| cue.projectRange.startUs < previous.projectRange.endUs
|
|
737
|
+
)) {
|
|
738
|
+
throw new TypeError("Compiled caption cues must be ordered and non-overlapping.");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Compile one current word transcript through placement synchronization and
|
|
745
|
+
* the project's cut/speed output map. The result is pure and output-clock
|
|
746
|
+
* ready; each cue keeps exact project-clock and source-word provenance.
|
|
747
|
+
*/
|
|
748
|
+
export function compileProjectCaptionCues(
|
|
749
|
+
input: CompileProjectCaptionCuesInput,
|
|
750
|
+
): readonly ProjectCaptionCue[] {
|
|
751
|
+
const project = VideoProjectV1Schema.parse(input.project);
|
|
752
|
+
const plan = ProjectEditPlanV1Schema.parse(input.plan);
|
|
753
|
+
const analysis = SpeechAnalysisV1Schema.parse(input.analysis);
|
|
754
|
+
const profile = captionProfile(input.output);
|
|
755
|
+
const { placement, words } = assertCurrentCaptionInputs(
|
|
756
|
+
project,
|
|
757
|
+
plan,
|
|
758
|
+
analysis,
|
|
759
|
+
input.placementId,
|
|
760
|
+
);
|
|
761
|
+
const outputMap = buildProjectOutputTimeMap(plan);
|
|
762
|
+
if (
|
|
763
|
+
input.output.durationUs !== undefined
|
|
764
|
+
&& input.output.durationUs !== outputMap.durationUs
|
|
765
|
+
) {
|
|
766
|
+
throw new TypeError("Caption output duration contradicts the project cut/speed map.");
|
|
767
|
+
}
|
|
768
|
+
const syncSegments = projectSyncSegments(placement.sync);
|
|
769
|
+
const cues: ProjectCaptionCue[] = [];
|
|
770
|
+
let pending: MappedCaptionToken[] = [];
|
|
771
|
+
const flush = (): void => {
|
|
772
|
+
if (pending.length === 0) return;
|
|
773
|
+
cues.push(cueFromTokens(pending, profile));
|
|
774
|
+
pending = [];
|
|
775
|
+
if (cues.length > PROJECT_CAPTION_LIMITS.maximumVisibleCues) {
|
|
776
|
+
throw new RangeError(
|
|
777
|
+
`Visible transcript exceeds the ${PROJECT_CAPTION_LIMITS.maximumVisibleCues}-cue caption bound.`,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
let visibleWordCount = 0;
|
|
782
|
+
for (const word of words) {
|
|
783
|
+
let wordIsVisible = false;
|
|
784
|
+
for (const token of mappedWordTokens({
|
|
785
|
+
maximumLineUnits: profile.maximumLineUnits,
|
|
786
|
+
outputSegments: outputMap.segments,
|
|
787
|
+
syncSegments,
|
|
788
|
+
word,
|
|
789
|
+
})) {
|
|
790
|
+
if (!wordIsVisible) {
|
|
791
|
+
wordIsVisible = true;
|
|
792
|
+
visibleWordCount += 1;
|
|
793
|
+
if (visibleWordCount > PROJECT_CAPTION_LIMITS.maximumVisibleWords) {
|
|
794
|
+
throw new RangeError(
|
|
795
|
+
`Visible transcript exceeds the ${PROJECT_CAPTION_LIMITS.maximumVisibleWords}-word caption bound.`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!canAppendToken(pending, token, profile)) flush();
|
|
800
|
+
pending.push(token);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (visibleWordCount === 0) {
|
|
804
|
+
throw new TypeError("Caption compilation produced no visible transcript after project edits.");
|
|
805
|
+
}
|
|
806
|
+
flush();
|
|
807
|
+
assertCueLaws(cues);
|
|
808
|
+
return cues;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function xmlSafeText(value: string): string {
|
|
812
|
+
let safe = "";
|
|
813
|
+
for (const character of value) {
|
|
814
|
+
const codePoint = character.codePointAt(0)!;
|
|
815
|
+
const invalid = (
|
|
816
|
+
(codePoint < 0x20 && codePoint !== 0x09 && codePoint !== 0x0a && codePoint !== 0x0d)
|
|
817
|
+
|| (codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
818
|
+
|| (codePoint & 0xffff) === 0xfffe
|
|
819
|
+
|| (codePoint & 0xffff) === 0xffff
|
|
820
|
+
|| codePoint > 0x10ffff
|
|
821
|
+
);
|
|
822
|
+
safe += invalid ? "\ufffd" : character;
|
|
823
|
+
}
|
|
824
|
+
return safe
|
|
825
|
+
.replaceAll("&", "&")
|
|
826
|
+
.replaceAll("<", "<")
|
|
827
|
+
.replaceAll(">", ">")
|
|
828
|
+
.replaceAll('"', """)
|
|
829
|
+
.replaceAll("'", "'");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Render one already-wrapped cue as a transparent, intrinsic-size SVG card. */
|
|
833
|
+
export function renderSocialCaptionSvg(
|
|
834
|
+
cue: ProjectCaptionCue,
|
|
835
|
+
output: ProjectCaptionOutput,
|
|
836
|
+
): SocialCaptionSvg {
|
|
837
|
+
const profile = captionProfile(output);
|
|
838
|
+
if (
|
|
839
|
+
(cue.lines.length !== 1 && cue.lines.length !== 2)
|
|
840
|
+
|| cue.lines.some(line => typeof line !== "string" || line.length === 0)
|
|
841
|
+
) {
|
|
842
|
+
throw new TypeError("Social caption SVG requires one or two nonempty wrapped lines.");
|
|
843
|
+
}
|
|
844
|
+
if (cue.lines.some(line => displayWidth(line) > profile.maximumLineUnits)) {
|
|
845
|
+
throw new RangeError("Social caption SVG line exceeds the selected output width bound.");
|
|
846
|
+
}
|
|
847
|
+
const horizontalPadding = Math.max(0, Math.round(profile.fontSize * 0.65));
|
|
848
|
+
const verticalPadding = Math.max(0, Math.round(profile.fontSize * 0.38));
|
|
849
|
+
const lineHeight = Math.max(1, Math.round(profile.fontSize * 1.18));
|
|
850
|
+
const lineTextWidths = cue.lines.map(line => Math.max(
|
|
851
|
+
1,
|
|
852
|
+
Math.ceil(displayWidth(line) * profile.fontSize / CAPTION_WIDTH_UNITS_PER_EM),
|
|
853
|
+
));
|
|
854
|
+
const estimatedTextWidth = Math.max(...lineTextWidths);
|
|
855
|
+
const intrinsicWidth = Math.max(1, Math.min(
|
|
856
|
+
profile.targetCardWidth,
|
|
857
|
+
Math.max(
|
|
858
|
+
Math.min(profile.targetCardWidth, profile.fontSize * 4),
|
|
859
|
+
estimatedTextWidth + horizontalPadding * 2,
|
|
860
|
+
),
|
|
861
|
+
));
|
|
862
|
+
const intrinsicHeight = Math.max(1, Math.min(
|
|
863
|
+
output.pixelHeight,
|
|
864
|
+
verticalPadding * 2 + lineHeight * cue.lines.length,
|
|
865
|
+
));
|
|
866
|
+
const bottomSafeMargin = Math.max(0, Math.min(
|
|
867
|
+
output.pixelHeight - intrinsicHeight,
|
|
868
|
+
Math.round(output.pixelHeight * profile.safeBottomFraction),
|
|
869
|
+
));
|
|
870
|
+
const radius = Math.max(1, Math.round(profile.fontSize * 0.34));
|
|
871
|
+
const strokeWidth = Math.max(1, Math.round(profile.fontSize * 0.055));
|
|
872
|
+
const textStrokeWidth = Math.max(1, Math.round(profile.fontSize * 0.045));
|
|
873
|
+
const centerX = intrinsicWidth / 2;
|
|
874
|
+
const blockHeight = lineHeight * cue.lines.length;
|
|
875
|
+
const firstBaseline = Math.round(
|
|
876
|
+
(intrinsicHeight - blockHeight) / 2 + profile.fontSize * 0.84,
|
|
877
|
+
);
|
|
878
|
+
const text = cue.lines.map((line, index) => (
|
|
879
|
+
`<tspan x="${centerX}" y="${firstBaseline + index * lineHeight}" textLength="${lineTextWidths[index]}" lengthAdjust="spacingAndGlyphs">${xmlSafeText(line)}</tspan>`
|
|
880
|
+
)).join("");
|
|
881
|
+
const svg = [
|
|
882
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${intrinsicWidth}" height="${intrinsicHeight}" viewBox="0 0 ${intrinsicWidth} ${intrinsicHeight}" fill="none">`,
|
|
883
|
+
`<rect x="${strokeWidth / 2}" y="${strokeWidth / 2}" width="${Math.max(0, intrinsicWidth - strokeWidth)}" height="${Math.max(0, intrinsicHeight - strokeWidth)}" rx="${radius}" fill="#050505" fill-opacity="0.9" stroke="#ffffff" stroke-opacity="0.2" stroke-width="${strokeWidth}"/>`,
|
|
884
|
+
`<text fill="#ffffff" stroke="#000000" stroke-opacity="0.72" stroke-width="${textStrokeWidth}" paint-order="stroke fill" font-family="Nebula Sans" font-size="${profile.fontSize}" font-weight="700" text-anchor="middle">${text}</text>`,
|
|
885
|
+
"</svg>",
|
|
886
|
+
].join("");
|
|
887
|
+
return { bottomSafeMargin, intrinsicHeight, intrinsicWidth, svg };
|
|
888
|
+
}
|