@cueframe/kernel 0.1.0 → 0.1.1
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 +661 -0
- package/dist/_members/composition/ops/compositionOps.d.ts +1 -0
- package/dist/_members/composition/projection/behind-readiness.d.ts +127 -0
- package/dist/_members/composition/projection/index.d.ts +2 -0
- package/dist/_members/composition/projection/reframe-readiness.d.ts +156 -0
- package/dist/_members/composition/validators/composition.d.ts +42 -25
- package/dist/_members/composition/validators/render.d.ts +32 -0
- package/dist/_members/core/render/compositionRender.d.ts +17 -1
- package/dist/_members/core/render/types.d.ts +31 -0
- package/dist/authoring/build-tools.js +6 -6
- package/dist/authoring/context.js +4 -4
- package/dist/authoring/tools.js +5 -5
- package/dist/authoring.js +11 -9
- package/dist/{chunk-ZB3C3HM7.js → chunk-7FO3ZJ43.js} +3 -0
- package/dist/{chunk-ODL7GQJG.js → chunk-AP6ZIVAV.js} +2 -2
- package/dist/{chunk-IBYQG46O.js → chunk-AQBREB5O.js} +1 -1
- package/dist/{chunk-YIOY2XEO.js → chunk-DK2ODBNT.js} +15 -5
- package/dist/{chunk-JRQVPFLF.js → chunk-HY3RIEZW.js} +1 -1
- package/dist/{chunk-GEA7E2FL.js → chunk-JJ6XD7NG.js} +151 -1
- package/dist/{chunk-COYMZ76S.js → chunk-MBCXCJOE.js} +1 -1
- package/dist/{chunk-37NJ27UU.js → chunk-OCFDUCF4.js} +3 -0
- package/dist/{chunk-VOO5FQSS.js → chunk-PDYO3DOJ.js} +5 -5
- package/dist/{chunk-MXFAADCQ.js → chunk-PODIYN2K.js} +9 -2
- package/dist/{chunk-NFEGPWGS.js → chunk-TOJEXFXL.js} +1 -1
- package/dist/{chunk-LM4WDUQJ.js → chunk-UHGSZZDI.js} +1 -1
- package/dist/{chunk-XWKZB5U5.js → chunk-ZWMV6DD2.js} +24 -1
- package/dist/compose/renderer.js +3 -3
- package/dist/compose.js +6 -6
- package/dist/composition/graphics-plan.js +1 -1
- package/dist/composition/ops.js +3 -3
- package/dist/composition/primitive-catalog.js +1 -1
- package/dist/composition/projection.js +19 -3
- package/dist/composition/validators.js +6 -4
- package/dist/composition-ops.js +8 -6
- package/dist/composition.js +24 -6
- package/dist/{context-OKX3HHFS.js → context-YWUZDG5X.js} +4 -4
- package/dist/core/render.js +3 -3
- package/dist/core/select.js +2 -2
- package/dist/core.js +3 -3
- package/dist/errors.js +1 -1
- package/dist/mcp/adapter.js +1 -0
- package/dist/media-model/testing.js +1071 -718
- package/dist/nle-export/server.js +328 -130
- package/dist/nle-export.js +7 -6
- package/package.json +13 -14
- package/dist/_members/account-model/index.js +0 -4
- package/dist/_members/account-model/testing/conversationStoreContract.js +0 -168
- package/dist/_members/account-model/testing/index.js +0 -7
- package/dist/_members/account-model/testing/restorePointStoreContract.js +0 -69
- package/dist/_members/account-model/types/index.js +0 -1
- package/dist/_members/account-model/validators/contextFile.js +0 -18
- package/dist/_members/account-model/validators/conversation.js +0 -95
- package/dist/_members/account-model/validators/dashboardInsight.js +0 -16
- package/dist/_members/account-model/validators/index.js +0 -7
- package/dist/_members/account-model/validators/sessionToken.js +0 -49
- package/dist/_members/account-model/validators/uploadToken.js +0 -16
- package/dist/_members/account-model/validators/userSettings.js +0 -12
- package/dist/_members/account-model/validators/workingComposition.js +0 -20
- package/dist/_members/brand-derivation/constants.js +0 -20
- package/dist/_members/brand-derivation/contrastSafe.js +0 -114
- package/dist/_members/brand-derivation/index.js +0 -1
- package/dist/_members/cloud-import/chunk-XKO6ZSWI.js +0 -162
- package/dist/_members/cloud-import/chunk-XKO6ZSWI.js.map +0 -1
- package/dist/_members/cloud-import/index.js +0 -241
- package/dist/_members/cloud-import/index.js.map +0 -1
- package/dist/_members/cloud-import/multipartUpload.js +0 -7
- package/dist/_members/cloud-import/multipartUpload.js.map +0 -1
- package/dist/_members/composition/audio/audioMix.js +0 -61
- package/dist/_members/composition/audio/index.js +0 -5
- package/dist/_members/composition/audio/mixStaging.js +0 -195
- package/dist/_members/composition/audio/musicPack.generated.js +0 -136
- package/dist/_members/composition/audio/musicPack.js +0 -36
- package/dist/_members/composition/audio/pack.generated.js +0 -603
- package/dist/_members/composition/audio/pack.js +0 -48
- package/dist/_members/composition/audio/palette.js +0 -134
- package/dist/_members/composition/autozoom/index.js +0 -83
- package/dist/_members/composition/brand-kit.js +0 -45
- package/dist/_members/composition/brief/contract.js +0 -209
- package/dist/_members/composition/color-grade.js +0 -80
- package/dist/_members/composition/depth.js +0 -8
- package/dist/_members/composition/fixtures/index.js +0 -143
- package/dist/_members/composition/fixtures/vaayaErrored.d.ts +0 -25
- package/dist/_members/composition/fixtures/vaayaErrored.js +0 -203
- package/dist/_members/composition/fonts.js +0 -162
- package/dist/_members/composition/graphics-plan/compile.js +0 -221
- package/dist/_members/composition/graphics-plan/index.js +0 -10
- package/dist/_members/composition/graphics-plan/instantiateMotif.js +0 -44
- package/dist/_members/composition/graphics-plan/motion.js +0 -66
- package/dist/_members/composition/graphics-plan/schemas.js +0 -261
- package/dist/_members/composition/graphics-plan/solve.js +0 -145
- package/dist/_members/composition/index.js +0 -5
- package/dist/_members/composition/invariants/clipPlacement.js +0 -69
- package/dist/_members/composition/invariants/composition.js +0 -28
- package/dist/_members/composition/invariants/errors.js +0 -10
- package/dist/_members/composition/invariants/etag.js +0 -38
- package/dist/_members/composition/invariants/ids.js +0 -33
- package/dist/_members/composition/invariants/index.js +0 -9
- package/dist/_members/composition/invariants/keyframes.js +0 -45
- package/dist/_members/composition/invariants/markers.js +0 -29
- package/dist/_members/composition/invariants/time.js +0 -20
- package/dist/_members/composition/invariants/tracks.js +0 -304
- package/dist/_members/composition/ops/compositionOps.js +0 -1056
- package/dist/_members/composition/ops/headlessStore.js +0 -94
- package/dist/_members/composition/ops/index.js +0 -4
- package/dist/_members/composition/ops/selectors.js +0 -118
- package/dist/_members/composition/ops/timeline-word.js +0 -1
- package/dist/_members/composition/ops/transcript.js +0 -77
- package/dist/_members/composition/ops/types.js +0 -16
- package/dist/_members/composition/primitive-catalog/catalogQuery.js +0 -150
- package/dist/_members/composition/primitive-catalog/disabled.js +0 -32
- package/dist/_members/composition/primitive-catalog/generated/meta.js +0 -15161
- package/dist/_members/composition/primitive-catalog/index.js +0 -46
- package/dist/_members/composition/primitive-catalog/paramValidation.js +0 -231
- package/dist/_members/composition/primitive-catalog/projection.js +0 -164
- package/dist/_members/composition/primitive-catalog/types.js +0 -14
- package/dist/_members/composition/projection/behind-placement.js +0 -62
- package/dist/_members/composition/projection/brandKit.js +0 -482
- package/dist/_members/composition/projection/cardTokens.js +0 -209
- package/dist/_members/composition/projection/composition-floor.js +0 -582
- package/dist/_members/composition/projection/cropProjection.js +0 -257
- package/dist/_members/composition/projection/index.js +0 -7
- package/dist/_members/composition/projection/occlusion-budget.js +0 -104
- package/dist/_members/composition/projection/safe-zones.js +0 -99
- package/dist/_members/composition/reframe/aspectLoss.js +0 -115
- package/dist/_members/composition/reframe/coverage.js +0 -62
- package/dist/_members/composition/reframe/derive.js +0 -73
- package/dist/_members/composition/reframe/index.js +0 -109
- package/dist/_members/composition/reframe/kenBurns.js +0 -64
- package/dist/_members/composition/reframe/shotScale.js +0 -176
- package/dist/_members/composition/reframe/trajectory.js +0 -124
- package/dist/_members/composition/timeline/compound.js +0 -134
- package/dist/_members/composition/timeline/index.js +0 -9
- package/dist/_members/composition/timeline/intent.js +0 -228
- package/dist/_members/composition/timeline/keyframes.js +0 -121
- package/dist/_members/composition/timeline/link.js +0 -59
- package/dist/_members/composition/timeline/occupancy.js +0 -36
- package/dist/_members/composition/timeline/placement.js +0 -74
- package/dist/_members/composition/timeline/plan.js +0 -31
- package/dist/_members/composition/timeline/resolve.js +0 -170
- package/dist/_members/composition/timeline/trims.js +0 -278
- package/dist/_members/composition/types/index.js +0 -1
- package/dist/_members/composition/validators/brandKit.js +0 -295
- package/dist/_members/composition/validators/composePlan.js +0 -113
- package/dist/_members/composition/validators/composition.js +0 -703
- package/dist/_members/composition/validators/font.js +0 -34
- package/dist/_members/composition/validators/graphicKinds.js +0 -30
- package/dist/_members/composition/validators/index.js +0 -9
- package/dist/_members/composition/validators/project.js +0 -29
- package/dist/_members/composition/validators/registry.js +0 -41
- package/dist/_members/composition/validators/render.js +0 -139
- package/dist/_members/composition/validators/session.js +0 -37
- package/dist/_members/composition/validators/webhook.js +0 -72
- package/dist/_members/composition-ops/alphaLayers.js +0 -141
- package/dist/_members/composition-ops/index.js +0 -637
- package/dist/_members/composition-ops/materializeGrid.js +0 -99
- package/dist/_members/composition-ops/opRecorder.js +0 -196
- package/dist/_members/composition-ops/recordingStore.js +0 -14
- package/dist/_members/composition-ops/replaceRegion.js +0 -102
- package/dist/_members/composition-ops/testFixtures.js +0 -19
- package/dist/_members/core/index.js +0 -5
- package/dist/_members/core/render/bookends.js +0 -53
- package/dist/_members/core/render/captions.js +0 -127
- package/dist/_members/core/render/compositionRender.js +0 -1249
- package/dist/_members/core/render/deriveTrajectory.js +0 -889
- package/dist/_members/core/render/emphasis.js +0 -16
- package/dist/_members/core/render/eval/cropBox.js +0 -139
- package/dist/_members/core/render/eval/inclusion.js +0 -96
- package/dist/_members/core/render/eval/index.js +0 -146
- package/dist/_members/core/render/eval/metrics.js +0 -94
- package/dist/_members/core/render/exportSettings.js +0 -65
- package/dist/_members/core/render/index.js +0 -20
- package/dist/_members/core/render/mapFaceToPerson.js +0 -73
- package/dist/_members/core/render/types.js +0 -8
- package/dist/_members/core/render/vcam/cameraPath.js +0 -248
- package/dist/_members/core/render/vcam/constants.js +0 -115
- package/dist/_members/core/render/vcam/errorCodes.js +0 -39
- package/dist/_members/core/render/vcam/framing.js +0 -136
- package/dist/_members/core/render/vcam/requiredRegion.js +0 -137
- package/dist/_members/core/render/vcam/shots.js +0 -53
- package/dist/_members/core/render/vcam/subjectSet.js +0 -388
- package/dist/_members/core/select/errorCodes.js +0 -38
- package/dist/_members/core/select/index.js +0 -26
- package/dist/_members/core/select/model.js +0 -18
- package/dist/_members/core/select/select.js +0 -244
- package/dist/_members/core/select/stabilize.js +0 -242
- package/dist/_members/core/types.js +0 -7
- package/dist/_members/errors/index.js +0 -1
- package/dist/_members/errors/registry.js +0 -560
- package/dist/_members/foundation/codes/index.js +0 -31
- package/dist/_members/foundation/index.js +0 -2
- package/dist/_members/foundation/primitives/index.js +0 -5
- package/dist/_members/mcp/adapter.js +0 -771
- package/dist/_members/mcp/burnedNames.js +0 -73
- package/dist/_members/mcp/core.js +0 -390
- package/dist/_members/mcp/curation.js +0 -792
- package/dist/_members/mcp/executor.js +0 -142
- package/dist/_members/mcp/format.js +0 -9
- package/dist/_members/mcp/generated/catalogPreviews.js +0 -95
- package/dist/_members/mcp/generated/harness.js +0 -81
- package/dist/_members/mcp/generated/operations.js +0 -131
- package/dist/_members/mcp/index.js +0 -50
- package/dist/_members/mcp/internalLoopTools.js +0 -109
- package/dist/_members/mcp/prompts.js +0 -45
- package/dist/_members/mcp/resources.js +0 -166
- package/dist/_members/mcp/server.js +0 -340
- package/dist/_members/mcp/wait.js +0 -378
- package/dist/_members/mcp/x402Meta.js +0 -66
- package/dist/_members/media-model/api.js +0 -175
- package/dist/_members/media-model/index.js +0 -1
- package/dist/_members/media-model/media-kind.js +0 -116
- package/dist/_members/media-model/testing/index.js +0 -1
- package/dist/_members/media-model/testing/mediaStoreContract.js +0 -82
- package/dist/_members/media-model/transcriptSearch.js +0 -70
- package/dist/_members/media-model/transcriptSearchCases.json +0 -79
- package/dist/_members/nle-export/compact-timeline.js +0 -73
- package/dist/_members/nle-export/compact-timeline.js.map +0 -1
- package/dist/_members/nle-export/composition-to-input.js +0 -147
- package/dist/_members/nle-export/composition-to-input.js.map +0 -1
- package/dist/_members/nle-export/fcpxml.js +0 -159
- package/dist/_members/nle-export/fcpxml.js.map +0 -1
- package/dist/_members/nle-export/index.js +0 -12
- package/dist/_members/nle-export/index.js.map +0 -1
- package/dist/_members/nle-export/premiere.js +0 -161
- package/dist/_members/nle-export/premiere.js.map +0 -1
- package/dist/_members/nle-export/server/index.js +0 -2
- package/dist/_members/nle-export/server/index.js.map +0 -1
- package/dist/_members/nle-export/server/pack-zip.js +0 -38
- package/dist/_members/nle-export/server/pack-zip.js.map +0 -1
- package/dist/_members/nle-export/srt.js +0 -50
- package/dist/_members/nle-export/srt.js.map +0 -1
- package/dist/_members/nle-export/types.js +0 -9
- package/dist/_members/nle-export/types.js.map +0 -1
- package/dist/_members/nle-export/xml/index.js +0 -11
- package/dist/_members/nle-export/xml/index.js.map +0 -1
- package/dist/_members/nle-export/xml-utils.js +0 -48
- package/dist/_members/nle-export/xml-utils.js.map +0 -1
- package/dist/_members/perception/activeSpeaker.js +0 -103
- package/dist/_members/perception/audioQuality.js +0 -76
- package/dist/_members/perception/energyBeat.js +0 -99
- package/dist/_members/perception/identityMatch.js +0 -91
- package/dist/_members/perception/index.js +0 -48
- package/dist/_members/perception/visualQuality.js +0 -95
- package/dist/_members/plane/conformance.js +0 -90
- package/dist/_members/plane/contract.js +0 -49
- package/dist/_members/plane/index.js +0 -16
- package/dist/_members/primitives/backgrounds/index.js +0 -32
- package/dist/_members/primitives/backgrounds/index.js.map +0 -1
- package/dist/_members/primitives/captions/index.js +0 -398
- package/dist/_members/primitives/captions/index.js.map +0 -1
- package/dist/_members/primitives/chunk-2LVE4UIT.js +0 -139
- package/dist/_members/primitives/chunk-2LVE4UIT.js.map +0 -1
- package/dist/_members/primitives/chunk-3IQUH6KK.js +0 -20
- package/dist/_members/primitives/chunk-3IQUH6KK.js.map +0 -1
- package/dist/_members/primitives/chunk-3OMURV6Q.js +0 -112
- package/dist/_members/primitives/chunk-3OMURV6Q.js.map +0 -1
- package/dist/_members/primitives/chunk-4IS6TMEG.js +0 -21
- package/dist/_members/primitives/chunk-4IS6TMEG.js.map +0 -1
- package/dist/_members/primitives/chunk-5MWGAEGQ.js +0 -1644
- package/dist/_members/primitives/chunk-5MWGAEGQ.js.map +0 -1
- package/dist/_members/primitives/chunk-7RRPFPQK.js +0 -15
- package/dist/_members/primitives/chunk-7RRPFPQK.js.map +0 -1
- package/dist/_members/primitives/chunk-DH5TRZK2.js +0 -125
- package/dist/_members/primitives/chunk-DH5TRZK2.js.map +0 -1
- package/dist/_members/primitives/chunk-E3KNUSFG.js +0 -2337
- package/dist/_members/primitives/chunk-E3KNUSFG.js.map +0 -1
- package/dist/_members/primitives/chunk-G2B3G4HK.js +0 -47
- package/dist/_members/primitives/chunk-G2B3G4HK.js.map +0 -1
- package/dist/_members/primitives/chunk-HFPFGZXT.js +0 -1494
- package/dist/_members/primitives/chunk-HFPFGZXT.js.map +0 -1
- package/dist/_members/primitives/chunk-HIWVFZVH.js +0 -19
- package/dist/_members/primitives/chunk-HIWVFZVH.js.map +0 -1
- package/dist/_members/primitives/chunk-IDAL6G3Y.js +0 -134
- package/dist/_members/primitives/chunk-IDAL6G3Y.js.map +0 -1
- package/dist/_members/primitives/chunk-KNC6JP5F.js +0 -34
- package/dist/_members/primitives/chunk-KNC6JP5F.js.map +0 -1
- package/dist/_members/primitives/chunk-KYRDVZST.js +0 -459
- package/dist/_members/primitives/chunk-KYRDVZST.js.map +0 -1
- package/dist/_members/primitives/chunk-LWLHS2NS.js +0 -3410
- package/dist/_members/primitives/chunk-LWLHS2NS.js.map +0 -1
- package/dist/_members/primitives/chunk-QAQ3P7DR.js +0 -8855
- package/dist/_members/primitives/chunk-QAQ3P7DR.js.map +0 -1
- package/dist/_members/primitives/chunk-ROV2ODWC.js +0 -81
- package/dist/_members/primitives/chunk-ROV2ODWC.js.map +0 -1
- package/dist/_members/primitives/chunk-TCRC2FH3.js +0 -183
- package/dist/_members/primitives/chunk-TCRC2FH3.js.map +0 -1
- package/dist/_members/primitives/chunk-US4YEDAA.js +0 -96
- package/dist/_members/primitives/chunk-US4YEDAA.js.map +0 -1
- package/dist/_members/primitives/chunk-UVKRO5ER.js +0 -13
- package/dist/_members/primitives/chunk-UVKRO5ER.js.map +0 -1
- package/dist/_members/primitives/chunk-W4EIIF2I.js +0 -10
- package/dist/_members/primitives/chunk-W4EIIF2I.js.map +0 -1
- package/dist/_members/primitives/chunk-WPGKIPCO.js +0 -3563
- package/dist/_members/primitives/chunk-WPGKIPCO.js.map +0 -1
- package/dist/_members/primitives/chunk-WZFLEGMD.js +0 -1435
- package/dist/_members/primitives/chunk-WZFLEGMD.js.map +0 -1
- package/dist/_members/primitives/chunk-XHU3WBEI.js +0 -33
- package/dist/_members/primitives/chunk-XHU3WBEI.js.map +0 -1
- package/dist/_members/primitives/chunk-YN7WTZZ7.js +0 -46
- package/dist/_members/primitives/chunk-YN7WTZZ7.js.map +0 -1
- package/dist/_members/primitives/effects/index.js +0 -30
- package/dist/_members/primitives/effects/index.js.map +0 -1
- package/dist/_members/primitives/hero-text/index.js +0 -14
- package/dist/_members/primitives/hero-text/index.js.map +0 -1
- package/dist/_members/primitives/index.js +0 -64
- package/dist/_members/primitives/index.js.map +0 -1
- package/dist/_members/primitives/layouts/index.js +0 -24
- package/dist/_members/primitives/layouts/index.js.map +0 -1
- package/dist/_members/primitives/lib/easings.js +0 -26
- package/dist/_members/primitives/lib/easings.js.map +0 -1
- package/dist/_members/primitives/lib/index.js +0 -74
- package/dist/_members/primitives/lib/index.js.map +0 -1
- package/dist/_members/primitives/lib/palette.js +0 -72
- package/dist/_members/primitives/lib/palette.js.map +0 -1
- package/dist/_members/primitives/lib/preview.js +0 -8
- package/dist/_members/primitives/lib/preview.js.map +0 -1
- package/dist/_members/primitives/lib/schema.js +0 -12
- package/dist/_members/primitives/lib/schema.js.map +0 -1
- package/dist/_members/primitives/lib/seeded-random.js +0 -14
- package/dist/_members/primitives/lib/seeded-random.js.map +0 -1
- package/dist/_members/primitives/lib/text/contrast.js +0 -12
- package/dist/_members/primitives/lib/text/contrast.js.map +0 -1
- package/dist/_members/primitives/lib/text/fill.js +0 -8
- package/dist/_members/primitives/lib/text/fill.js.map +0 -1
- package/dist/_members/primitives/lib/text/fitText.js +0 -10
- package/dist/_members/primitives/lib/text/fitText.js.map +0 -1
- package/dist/_members/primitives/lib/text/marker.js +0 -8
- package/dist/_members/primitives/lib/text/marker.js.map +0 -1
- package/dist/_members/primitives/lib/text/motion.js +0 -16
- package/dist/_members/primitives/lib/text/motion.js.map +0 -1
- package/dist/_members/primitives/lib/text/recolor.js +0 -8
- package/dist/_members/primitives/lib/text/recolor.js.map +0 -1
- package/dist/_members/primitives/lib/text/spec.js +0 -1
- package/dist/_members/primitives/lib/text/spec.js.map +0 -1
- package/dist/_members/primitives/lib/text/stage.js +0 -8
- package/dist/_members/primitives/lib/text/stage.js.map +0 -1
- package/dist/_members/primitives/lib/text/typeface.js +0 -8
- package/dist/_members/primitives/lib/text/typeface.js.map +0 -1
- package/dist/_members/primitives/lib/tokens.js +0 -24
- package/dist/_members/primitives/lib/tokens.js.map +0 -1
- package/dist/_members/primitives/lib/types.js +0 -1
- package/dist/_members/primitives/lib/types.js.map +0 -1
- package/dist/_members/primitives/params-required.json +0 -153
- package/dist/_members/primitives/scenes/index.js +0 -25
- package/dist/_members/primitives/scenes/index.js.map +0 -1
- package/dist/_members/primitives/subject-foreground/index.js +0 -30
- package/dist/_members/primitives/subject-foreground/index.js.map +0 -1
- package/dist/_members/primitives/text/index.js +0 -57
- package/dist/_members/primitives/text/index.js.map +0 -1
- package/dist/_members/primitives/transitions/index.js +0 -67
- package/dist/_members/primitives/transitions/index.js.map +0 -1
- package/dist/_members/primitives/ui-blocks/index.js +0 -76
- package/dist/_members/primitives/ui-blocks/index.js.map +0 -1
- package/dist/_members/primitives/version-map.json +0 -105
- package/dist/_members/primitives/workspace.js +0 -60
- package/dist/_members/primitives/workspace.js.map +0 -1
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vcam/cameraPath.ts — the SINGLE virtual-camera stability stage (Phase 3).
|
|
3
|
-
*
|
|
4
|
-
* Replaces both `emaSmooth` (for the centers) and the render-side
|
|
5
|
-
* `smoothTrajectory` Gaussian with ONE pass per shot. Per the spec
|
|
6
|
-
* (2026-06-21-virtual-camera-reframe-design.md, "Virtual-camera stability — one
|
|
7
|
-
* stage, no LP"):
|
|
8
|
-
*
|
|
9
|
-
* 1. Deadzone INCLUSION gate (output-crop-normalized): the camera HOLDS while
|
|
10
|
-
* the padded RequiredRegion stays inside a safe region of the current crop
|
|
11
|
-
* window; it moves ONLY when the region would exit. This is the inclusion
|
|
12
|
-
* INVARIANT — the crop always contains the RequiredRegion ⇒ the subject
|
|
13
|
-
* never walks out of frame. This is what turns Phase 2's "static hold" into
|
|
14
|
-
* "hold, then glide to re-contain" — a moving subject is TRACKED.
|
|
15
|
-
* 2. One Hamming-window weighted mean over each shot run's gated centers
|
|
16
|
-
* (acausal, single pass, no lag). Forward-fill at the start; the window
|
|
17
|
-
* AUTO-SHRINKS near a shot boundary so it never blends across a cut.
|
|
18
|
-
* 3. Null/occluded regions are forward-filled (the last in-shot region) before
|
|
19
|
-
* the gate, minus the deleted jump heuristic — gaps longer than the window
|
|
20
|
-
* hold; re-emergence outside the pad glides over the window.
|
|
21
|
-
*
|
|
22
|
-
* No second smoothing: render samples this base path verbatim. The only reset is
|
|
23
|
-
* a detector `shotBoundary` — a center jump is NEVER inferred as a cut (the
|
|
24
|
-
* deleted `isJump`/`JUMP_THRESHOLD` false-cut teleport).
|
|
25
|
-
*
|
|
26
|
-
* Coordinate systems: RequiredRegions + centers are SOURCE-NORMALIZED. The
|
|
27
|
-
* deadzone is OUTPUT-CROP-NORMALIZED (a fraction of the crop window), so the
|
|
28
|
-
* held feel is zoom-invariant — it is converted to source units here using the
|
|
29
|
-
* crop-window size. Pure, deterministic, no I/O.
|
|
30
|
-
*/
|
|
31
|
-
import { segmentShots } from './shots';
|
|
32
|
-
import { DEADZONE, SMOOTH_WINDOW_SEC, REACQUIRE_GLIDE_PER_SEC } from './constants';
|
|
33
|
-
/** Clamp a window center so the crop window stays fully inside the source (the
|
|
34
|
-
* same coverage invariant the renderer enforces). A window wider than the axis
|
|
35
|
-
* centers. */
|
|
36
|
-
function clampCenter(c, sizeFrac) {
|
|
37
|
-
if (sizeFrac >= 1)
|
|
38
|
-
return 0.5;
|
|
39
|
-
const half = sizeFrac / 2;
|
|
40
|
-
return Math.max(half, Math.min(1 - half, c));
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Minimal center that contains a 1-D interval `[lo, hi]` inside a window of size
|
|
44
|
-
* `win` whose interior is shrunk by the deadzone margin `dz` on each side (the
|
|
45
|
-
* "safe region"). Returns the current `held` center UNCHANGED when the interval
|
|
46
|
-
* already fits in the held window's safe region; otherwise the nearest center
|
|
47
|
-
* that just re-contains it (push the window the smallest distance). The deadzone
|
|
48
|
-
* is the hold band: motion inside it demands zero camera motion.
|
|
49
|
-
*/
|
|
50
|
-
function gate1D(held, lo, hi, win, dz) {
|
|
51
|
-
const safe = Math.max(0, win - 2 * dz); // safe-region width (window minus margins)
|
|
52
|
-
const ext = hi - lo;
|
|
53
|
-
const mid = (lo + hi) / 2;
|
|
54
|
-
// Region wider than the safe band: can't be fully contained at this zoom (an
|
|
55
|
-
// OVER-WIDE group — surfaced upstream as `overframed` in a later phase). HOLD
|
|
56
|
-
// on the midpoint within the SAME deadzone so the camera doesn't wobble as
|
|
57
|
-
// members enter/exit the union box; only re-centre when the midpoint itself
|
|
58
|
-
// drifts beyond the deadzone (a real group shift). Without this hold an over-
|
|
59
|
-
// wide held group jitters every frame on the union-box breathing.
|
|
60
|
-
if (ext >= safe) {
|
|
61
|
-
if (Math.abs(mid - held) <= dz)
|
|
62
|
-
return held;
|
|
63
|
-
return mid > held ? mid - dz : mid + dz;
|
|
64
|
-
}
|
|
65
|
-
const safeLo = held - safe / 2;
|
|
66
|
-
const safeHi = held + safe / 2;
|
|
67
|
-
if (lo >= safeLo && hi <= safeHi)
|
|
68
|
-
return held; // already contained → HOLD
|
|
69
|
-
// Push minimally: if the region exits left, align its left to the safe edge;
|
|
70
|
-
// if it exits right, align its right. (Both can't exit — ext < safe.)
|
|
71
|
-
if (lo < safeLo)
|
|
72
|
-
return lo + safe / 2;
|
|
73
|
-
return hi - safe / 2;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Inclusion clamp with a per-frame GLIDE cap: re-contain the FULL region
|
|
77
|
-
* `[lo,hi]` inside a window of size `win` centered near `c`, but move no more
|
|
78
|
-
* than `maxStep` (source units) from the PREVIOUS output center `prev` in one
|
|
79
|
-
* frame. This preserves the inclusion invariant during CONTINUOUS tracking (the
|
|
80
|
-
* needed correction is sub-`maxStep`, so it's applied in full) while letting a
|
|
81
|
-
* far RE-ACQUISITION (subject re-emerges across the frame after a gap) GLIDE
|
|
82
|
-
* over several frames instead of teleporting — the spec's "re-emergence glides
|
|
83
|
-
* over the window" (the parabolic re-acquire ease is the deferred L1 slot; this
|
|
84
|
-
* is the linear floor). A region wider than the window can't be contained →
|
|
85
|
-
* ease toward its midpoint.
|
|
86
|
-
*/
|
|
87
|
-
function includeGlide1D(c, prev, lo, hi, win, maxStep) {
|
|
88
|
-
const ext = hi - lo;
|
|
89
|
-
let target;
|
|
90
|
-
if (ext >= win) {
|
|
91
|
-
// Over-wide: can't be contained at this zoom. Keep the smoothed (held) center
|
|
92
|
-
// — the gate already held it on the union midpoint within the deadzone, so
|
|
93
|
-
// re-snapping to the raw midpoint here would re-introduce the wobble.
|
|
94
|
-
target = c;
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
const winLo = c - win / 2;
|
|
98
|
-
const winHi = c + win / 2;
|
|
99
|
-
if (lo >= winLo && hi <= winHi)
|
|
100
|
-
target = c; // already contains the region
|
|
101
|
-
else if (lo < winLo)
|
|
102
|
-
target = lo + win / 2;
|
|
103
|
-
else
|
|
104
|
-
target = hi - win / 2;
|
|
105
|
-
}
|
|
106
|
-
// Glide cap: never jump more than maxStep from the previous output center.
|
|
107
|
-
const d = target - prev;
|
|
108
|
-
if (d > maxStep)
|
|
109
|
-
return prev + maxStep;
|
|
110
|
-
if (d < -maxStep)
|
|
111
|
-
return prev - maxStep;
|
|
112
|
-
return target;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* The deadzone INCLUSION gate over one shot's regions. Walks frames in order,
|
|
116
|
-
* holding the camera while the padded region stays inside the safe region of the
|
|
117
|
-
* current crop window, stepping minimally to re-contain it when it exits. The
|
|
118
|
-
* gate is seeded from the first region (so the shot opens framed on its subject,
|
|
119
|
-
* not at a stale center). Returns the per-frame GATED center (source-normalized),
|
|
120
|
-
* the input to the Hamming smoother.
|
|
121
|
-
*/
|
|
122
|
-
function deadzoneGate(regions, crop, dzX, dzY) {
|
|
123
|
-
const out = [];
|
|
124
|
-
// Seed the held center on the first region, coverage-clamped.
|
|
125
|
-
let hx = clampCenter(regions[0].cx, crop.w);
|
|
126
|
-
let hy = clampCenter(regions[0].cy, crop.h);
|
|
127
|
-
for (const r of regions) {
|
|
128
|
-
const loX = r.cx - r.w / 2;
|
|
129
|
-
const hiX = r.cx + r.w / 2;
|
|
130
|
-
const loY = r.cy - r.h / 2;
|
|
131
|
-
const hiY = r.cy + r.h / 2;
|
|
132
|
-
hx = clampCenter(gate1D(hx, loX, hiX, crop.w, dzX), crop.w);
|
|
133
|
-
hy = clampCenter(gate1D(hy, loY, hiY, crop.h, dzY), crop.h);
|
|
134
|
-
out.push([hx, hy]);
|
|
135
|
-
}
|
|
136
|
-
return out;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Acausal Hamming-weighted moving average over a shot run's gated centers. The
|
|
140
|
-
* half-window auto-shrinks at the shot ENDS (it never reads past `[0, n)`), so a
|
|
141
|
-
* shot shorter than the window smooths over its own length and never blends
|
|
142
|
-
* across the cut. Single pass, symmetric weights → no lag.
|
|
143
|
-
*/
|
|
144
|
-
function hammingSmooth(centers, halfWindow) {
|
|
145
|
-
const n = centers.length;
|
|
146
|
-
if (n <= 1 || halfWindow < 1)
|
|
147
|
-
return centers.map((c) => [c[0], c[1]]);
|
|
148
|
-
// Hamming weight at offset d (|d| ≤ halfWindow): 0.54 + 0.46·cos(π·d/halfWindow).
|
|
149
|
-
const weight = (d) => 0.54 + 0.46 * Math.cos((Math.PI * d) / halfWindow);
|
|
150
|
-
const out = new Array(n);
|
|
151
|
-
for (let i = 0; i < n; i++) {
|
|
152
|
-
const lo = Math.max(0, i - halfWindow);
|
|
153
|
-
const hi = Math.min(n - 1, i + halfWindow);
|
|
154
|
-
let sx = 0;
|
|
155
|
-
let sy = 0;
|
|
156
|
-
let sw = 0;
|
|
157
|
-
for (let j = lo; j <= hi; j++) {
|
|
158
|
-
const w = weight(j - i);
|
|
159
|
-
sx += centers[j][0] * w;
|
|
160
|
-
sy += centers[j][1] * w;
|
|
161
|
-
sw += w;
|
|
162
|
-
}
|
|
163
|
-
out[i] = sw > 0 ? [sx / sw, sy / sw] : [centers[i][0], centers[i][1]];
|
|
164
|
-
}
|
|
165
|
-
return out;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Forward-fill null regions within a shot slice. A gap (subject occluded) holds
|
|
169
|
-
* the LAST in-shot region; a leading gap (no region yet) borrows the first known
|
|
170
|
-
* region in the shot. A shot with no region at all (never resolved) → null,
|
|
171
|
-
* handled by the caller (center-hold). NO jump back-fill (the deleted heuristic).
|
|
172
|
-
*/
|
|
173
|
-
function fillShot(regions) {
|
|
174
|
-
const first = regions.find((r) => r !== null) ?? null;
|
|
175
|
-
if (first === null)
|
|
176
|
-
return null;
|
|
177
|
-
const out = new Array(regions.length);
|
|
178
|
-
let last = first;
|
|
179
|
-
for (let i = 0; i < regions.length; i++) {
|
|
180
|
-
if (regions[i] !== null)
|
|
181
|
-
last = regions[i];
|
|
182
|
-
out[i] = last;
|
|
183
|
-
}
|
|
184
|
-
return out;
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* The single stability stage: per-frame RequiredRegions → smoothed, inclusion-
|
|
188
|
-
* satisfying per-frame crop centers (source-normalized). `cropFor(frame)` gives
|
|
189
|
-
* the crop window for that frame (it can vary per frame when zoom is eased).
|
|
190
|
-
*
|
|
191
|
-
* `interiorCuts` are segment-local frame indices where a new shot begins (the
|
|
192
|
-
* SAME set the derive already computes). Each shot is gated + smoothed
|
|
193
|
-
* independently (deadzone, window clamp, forward-fill all reset at the cut).
|
|
194
|
-
* Frames whose shot never resolved a region hold `centerHold` (source center).
|
|
195
|
-
*/
|
|
196
|
-
export function cameraPath(regions, cropFor, fps, interiorCuts, centerHold = [0.5, 0.5]) {
|
|
197
|
-
const n = regions.length;
|
|
198
|
-
if (n === 0)
|
|
199
|
-
return [];
|
|
200
|
-
const halfWindow = Math.max(1, Math.round(SMOOTH_WINDOW_SEC * fps));
|
|
201
|
-
const out = new Array(n);
|
|
202
|
-
const shots = segmentShots(interiorCuts, n);
|
|
203
|
-
for (const shot of shots) {
|
|
204
|
-
const slice = regions.slice(shot.start, shot.end);
|
|
205
|
-
// `regions` are already the outlier-robust RequiredRegions
|
|
206
|
-
// (`buildRequiredRegions`): single-frame detector/ReID spikes rejected, so
|
|
207
|
-
// the gate frames the temporally-coherent subject, not a flicker.
|
|
208
|
-
const filled = fillShot(slice);
|
|
209
|
-
if (filled === null) {
|
|
210
|
-
// No region resolved anywhere in this shot → hold source center (the
|
|
211
|
-
// documented faceless/occluded outcome).
|
|
212
|
-
for (let i = shot.start; i < shot.end; i++)
|
|
213
|
-
out[i] = [centerHold[0], centerHold[1]];
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
// Use the shot's first crop window for the deadzone margins (zoom is per-shot
|
|
217
|
-
// constant for the non-eased path; eased zoom is rare on all-faces and the
|
|
218
|
-
// gate margin is robust to small per-frame zoom drift).
|
|
219
|
-
const crop0 = cropFor(shot.start);
|
|
220
|
-
const dzX = DEADZONE * crop0.w;
|
|
221
|
-
const dzY = DEADZONE * crop0.h;
|
|
222
|
-
const gated = deadzoneGate(filled, crop0, dzX, dzY);
|
|
223
|
-
// Auto-shrink the window to the shot length so it never blends across a cut.
|
|
224
|
-
const hw = Math.min(halfWindow, Math.max(1, gated.length - 1));
|
|
225
|
-
const smoothed = hammingSmooth(gated, hw);
|
|
226
|
-
// Post-smooth: re-establish the INCLUSION INVARIANT with a per-frame GLIDE
|
|
227
|
-
// cap. Continuous tracking needs a sub-cap correction → applied in full (the
|
|
228
|
-
// crop contains the RequiredRegion ⇒ the subject never walks out of frame).
|
|
229
|
-
// A far RE-ACQUISITION after a gap (subject re-emerges across the frame)
|
|
230
|
-
// exceeds the cap → it GLIDES over several frames instead of teleporting (the
|
|
231
|
-
// spec's "re-emergence glides over the window"). Then the coverage clamp
|
|
232
|
-
// keeps the window inside the source.
|
|
233
|
-
const maxStepX = (REACQUIRE_GLIDE_PER_SEC * crop0.w) / fps;
|
|
234
|
-
const maxStepY = (REACQUIRE_GLIDE_PER_SEC * crop0.h) / fps;
|
|
235
|
-
let prevX = clampCenter(smoothed[0][0], crop0.w);
|
|
236
|
-
let prevY = clampCenter(smoothed[0][1], crop0.h);
|
|
237
|
-
for (let i = 0; i < smoothed.length; i++) {
|
|
238
|
-
const crop = cropFor(shot.start + i);
|
|
239
|
-
const r = filled[i];
|
|
240
|
-
const incX = includeGlide1D(smoothed[i][0], prevX, r.cx - r.w / 2, r.cx + r.w / 2, crop.w, maxStepX);
|
|
241
|
-
const incY = includeGlide1D(smoothed[i][1], prevY, r.cy - r.h / 2, r.cy + r.h / 2, crop.h, maxStepY);
|
|
242
|
-
prevX = clampCenter(incX, crop.w);
|
|
243
|
-
prevY = clampCenter(incY, crop.h);
|
|
244
|
-
out[shot.start + i] = [prevX, prevY];
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return out;
|
|
248
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vcam/constants.ts — all virtual-camera selection params, in one place.
|
|
3
|
-
*
|
|
4
|
-
* Phase 2 (subject-of-interest selection). The deadzone / Hamming-window /
|
|
5
|
-
* zoom-fit params land alongside their modules in Phase 3/5; only the
|
|
6
|
-
* Phase-2 selection params live here today.
|
|
7
|
-
*
|
|
8
|
-
* Sizes are FRAME-HEIGHT fractions (a face box's height ÷ source height), the
|
|
9
|
-
* same units the detector emits. Persistence is a fraction of the shot's frames.
|
|
10
|
-
*/
|
|
11
|
-
/** Locked-set gate — drop a track iff
|
|
12
|
-
* `(persistence < P_MIN AND size_med < SIZE_DOMINANT) OR (size_med < SIZE_MIN)`.
|
|
13
|
-
* A transient small/brief face is dropped (can't re-weight anything); a
|
|
14
|
-
* DOMINANT late entrant (low persistence, large size) is KEPT — the flat
|
|
15
|
-
* `persistence < 0.5` gate would have silently dropped the intended subject. */
|
|
16
|
-
export const P_MIN = 0.5;
|
|
17
|
-
/** Minimum face-height fraction to survive the gate at all. */
|
|
18
|
-
export const SIZE_MIN = 0.06;
|
|
19
|
-
/** Face-height fraction at/above which a brief track is kept regardless of
|
|
20
|
-
* persistence (a dominant late entrant). */
|
|
21
|
-
export const SIZE_DOMINANT = 0.18;
|
|
22
|
-
/** Max members framed for `all-faces` (top-K by dominance). */
|
|
23
|
-
export const MAX_SUBJECTS = 3;
|
|
24
|
-
/** Dominance weights — `0.4·size + 0.2·centrality + 0.2·persistence + 0.2·speaking`.
|
|
25
|
-
* `speaking`/`centrality` feed dominance, NEVER the gate. */
|
|
26
|
-
export const DOMINANCE_WEIGHTS = {
|
|
27
|
-
size: 0.4,
|
|
28
|
-
centrality: 0.2,
|
|
29
|
-
persistence: 0.2,
|
|
30
|
-
speaking: 0.2,
|
|
31
|
-
};
|
|
32
|
-
/** Per-frame set-MEMBERSHIP hysteresis (in seconds; converted to frames with
|
|
33
|
-
* the source fps). The locked set is the candidate pool; whether a member is
|
|
34
|
-
* IN the framed region this frame is decided with add/drop hysteresis so a
|
|
35
|
-
* member flickering at the gate boundary doesn't toggle the framing. The
|
|
36
|
-
* deadzone (Phase 3) stabilizes PAN; hysteresis stabilizes set MEMBERSHIP. */
|
|
37
|
-
export const ENTER_SEC = 0.3;
|
|
38
|
-
export const LEAVE_SEC = 0.5;
|
|
39
|
-
// ── Phase 3: virtual-camera stability (requiredRegion + cameraPath) ──────────
|
|
40
|
-
/**
|
|
41
|
-
* Headroom pad added around a RequiredRegion's subject/group extent (a fraction
|
|
42
|
-
* of the SOURCE dimension), BEFORE the crop must contain it. This is the
|
|
43
|
-
* "frame the head, not just the box" margin — the region the crop is obliged to
|
|
44
|
-
* keep in view is the subject box grown by this much on every side. Source-
|
|
45
|
-
* normalized (the region geometry is source-normalized; the deadzone below is
|
|
46
|
-
* the output-crop-normalized hold band). ~6% of the source on each side gives a
|
|
47
|
-
* comfortable margin around a face/group without over-zooming.
|
|
48
|
-
*/
|
|
49
|
-
export const REGION_PAD = 0.06;
|
|
50
|
-
/**
|
|
51
|
-
* Deadzone inclusion gate (OUTPUT-CROP-NORMALIZED — a fraction of the crop
|
|
52
|
-
* WINDOW, the same units as the render's old `DEADZONE`). The camera HOLDS while
|
|
53
|
-
* the padded RequiredRegion stays inside the crop window shrunk by this margin
|
|
54
|
-
* on each side (the "safe region"); it only moves when the region would exit
|
|
55
|
-
* that safe region. Calibrated ≥ the render's old `DEADZONE=0.018` hold effect
|
|
56
|
-
* on a static head, so the relocated deadzone is no looser than before. 0.10 of
|
|
57
|
-
* the crop width is the spec's `REGION_PAD ≈ 0.10 of crop width` value — the
|
|
58
|
-
* band inside which subject motion demands ZERO camera motion (the locked feel).
|
|
59
|
-
*/
|
|
60
|
-
export const DEADZONE = 0.1;
|
|
61
|
-
/**
|
|
62
|
-
* Hamming smoothing window half-length, in SECONDS (converted to frames with the
|
|
63
|
-
* source fps; `round(0.5·fps)` ≈ 15 frames at 30fps). The single acausal
|
|
64
|
-
* Hamming-weighted mean over each shot run's deadzone-gated centers — replaces
|
|
65
|
-
* the old forward+backward EMA AND the render Gaussian. Clamped to the shot run
|
|
66
|
-
* (forward-fill at the start, window auto-shrinks near a shot boundary) so it
|
|
67
|
-
* never blends across a cut. A shorter window = snappier tracking; longer =
|
|
68
|
-
* more damped glide. 0.5s matches the spec.
|
|
69
|
-
*/
|
|
70
|
-
export const SMOOTH_WINDOW_SEC = 0.5;
|
|
71
|
-
/**
|
|
72
|
-
* Outlier-reject median-filter radius on the RequiredRegion series, in SECONDS
|
|
73
|
-
* (converted to frames with the source fps). A single-frame detector/ReID spike
|
|
74
|
-
* — the box teleporting across the frame for one frame, then snapping back —
|
|
75
|
-
* would yank the deadzone gate (the chase, re-skinned). A short median rejects
|
|
76
|
-
* it while tracking a clean multi-frame walk exactly (an order statistic, not a
|
|
77
|
-
* low-pass — lag-free on a monotone pan). ~0.12s (≈4 frames at 30fps, ≈7 at
|
|
78
|
-
* 60fps) outvotes a short multi-frame ReID swap without smearing a real walk.
|
|
79
|
-
*/
|
|
80
|
-
export const REGION_MEDIAN_SEC = 0.12;
|
|
81
|
-
/**
|
|
82
|
-
* Maximum per-frame camera-center move during the inclusion clamp, as a fraction
|
|
83
|
-
* of the CROP WINDOW per second (converted to source-units-per-frame with the
|
|
84
|
-
* crop size + fps). Sized GENEROUSLY: a real walking subject (≈3–5 crop-widths/s)
|
|
85
|
-
* tracks unthrottled, so the inclusion invariant holds frame-to-frame and the
|
|
86
|
-
* subject stays in frame (inclusionRate ≈ 1.0). The cap exists only to convert a
|
|
87
|
-
* single-frame TELEPORT — a detector ReID swap or a far re-emergence after a gap
|
|
88
|
-
* that would otherwise snap the camera a full frame-width in one frame — into a
|
|
89
|
-
* bounded ≤`cap/fps` step (the spec's "re-emergence glides over the window"; the
|
|
90
|
-
* parabolic re-acquire ease is the deferred L1 slot). At the cap the worst step
|
|
91
|
-
* is bounded well below the raw-centroid chase, while continuous tracking is
|
|
92
|
-
* unaffected. 8 crop-widths/sec ≈ 0.13 crop-widths/frame at 60fps.
|
|
93
|
-
*/
|
|
94
|
-
export const REACQUIRE_GLIDE_PER_SEC = 8;
|
|
95
|
-
// ── Phase 5: zoom-to-fit framing (framing.ts) ────────────────────────────────
|
|
96
|
-
/**
|
|
97
|
-
* The bake's crop-window zoom clamp, mirrored here so `fitZoom` produces a zoom
|
|
98
|
-
* inside the SAME envelope the renderer (`computeCropTransform` `bakedZoom`) and
|
|
99
|
-
* `cropWindowSizeFor` enforce. ZOOM_MAX = 1.0 is the widest honest frame (the
|
|
100
|
-
* largest source-fitting output-aspect rect — can't widen past the source);
|
|
101
|
-
* ZOOM_MIN = 0.4 is the tightest crop the bake allows. A fitting group is framed
|
|
102
|
-
* at the contained zoom clamped into [ZOOM_MIN, ZOOM_MAX]; an over-wide group
|
|
103
|
-
* pads-and-holds at ZOOM_MAX and is flagged `overframed`.
|
|
104
|
-
*/
|
|
105
|
-
export const ZOOM_MIN = 0.4;
|
|
106
|
-
export const ZOOM_MAX = 1.0;
|
|
107
|
-
/**
|
|
108
|
-
* Min-shot-length floor for ZOOM recalibration, in SECONDS (converted to frames
|
|
109
|
-
* with the source fps; round(0.4*fps) is ~12 frames at 30fps). A shot SHORTER
|
|
110
|
-
* than this inherits the PREVIOUS shot's zoom rather than recalibrating, so a
|
|
111
|
-
* sub-second montage (3-15-frame sub-shots, each independently zoom-fit) can't
|
|
112
|
-
* STROBE the zoom channel (the named regression risk this design introduces).
|
|
113
|
-
* Pan still resets at every cut - only the zoom is held; the floor is zoom-only.
|
|
114
|
-
*/
|
|
115
|
-
export const MIN_ZOOM_SHOT_FRAMES = 0.4;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vcam/errorCodes.ts — the virtual-camera error vocabulary, as VALUES.
|
|
3
|
-
*
|
|
4
|
-
* Every code the vcam layer can throw (now or in a later phase) is listed here
|
|
5
|
-
* as a literal value, and the registry-membership CI test
|
|
6
|
-
* (`__tests__/vcamErrorRegistry.test.ts`) imports THIS list and asserts each one
|
|
7
|
-
* exists in `@cueframe/errors`'s `ERROR_CODES`. Importing the codes as values
|
|
8
|
-
* (not grepping string literals) means a template/variable code can't slip past
|
|
9
|
-
* the gate — an unregistered code would make the CFERR decode fall back to
|
|
10
|
-
* `internal_error` and 500 (the `clip_source_track_mismatch` landmine).
|
|
11
|
-
*
|
|
12
|
-
* Phase 1 registers all of these in `packages/errors/src/registry.ts` even
|
|
13
|
-
* though some are thrown only in later phases (`subject_uncontainable`,
|
|
14
|
-
* `trajectory_internal_gap`, `clip_trajectory_merge_conflict`) — registered now
|
|
15
|
-
* so they can never 500 when the phase that throws them lands.
|
|
16
|
-
*
|
|
17
|
-
* `subject_not_tracked` is the EXISTING segment-level resolution gate (kept,
|
|
18
|
-
* registered already); it's listed so the membership test covers the full vcam
|
|
19
|
-
* throw surface in one place.
|
|
20
|
-
*
|
|
21
|
-
* NOT codes (documented emitted outcomes / legal inputs, never throws):
|
|
22
|
-
* - `overframed` — an emitted boolean OUTCOME on an `all-faces` segment whose
|
|
23
|
-
* group is wider than the aspect (the widest honest frame), NOT an error.
|
|
24
|
-
* - a legacy track with no `shotBoundaries` → ONE shot (no
|
|
25
|
-
* `shot_boundaries_missing` throw); an empty locked set → center-hold.
|
|
26
|
-
*/
|
|
27
|
-
/** The error codes the vcam (virtual-camera) layer is allowed to throw. Each
|
|
28
|
-
* MUST be a row in `@cueframe/errors` `ERROR_CODES` (asserted in CI). */
|
|
29
|
-
export const VCAM_ERROR_CODES = [
|
|
30
|
-
'subject_not_tracked',
|
|
31
|
-
// D10 (P5 4b): generated-footage sibling of `subject_not_tracked`, thrown by the
|
|
32
|
-
// same `deriveTrajectory` resolution gate when `isGenerated` is true. Listed so
|
|
33
|
-
// the membership test covers it (it would otherwise CFERR-decode to
|
|
34
|
-
// internal_error → 500, the clip_source_track_mismatch landmine).
|
|
35
|
-
'subject_detection_failed_on_generated',
|
|
36
|
-
'subject_uncontainable',
|
|
37
|
-
'trajectory_internal_gap',
|
|
38
|
-
'clip_trajectory_merge_conflict',
|
|
39
|
-
];
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vcam/framing.ts — zoom-to-fit for ALL modes (Phase 5).
|
|
3
|
-
*
|
|
4
|
-
* Generalizes today's face-only `calibrateShotScale` (which sizes ZOOM to a
|
|
5
|
-
* single detected face's median height for an editorial `shotScale`) into a
|
|
6
|
-
* mode-agnostic `fitZoom` that sizes the crop to CONTAIN a locked subject set's
|
|
7
|
-
* union extent — the automatic fit the spec mandates (brain spec
|
|
8
|
-
* 2026-06-21-virtual-camera-reframe-design.md, "Framing & zoom-to-fit (ALL
|
|
9
|
-
* modes)" + RESOLVED #2 "automatic-fit only for the ship").
|
|
10
|
-
*
|
|
11
|
-
* The headline consumer is `all-faces`: today every all-faces segment ships at
|
|
12
|
-
* zoom 1.0 (the widest honest frame) because `calibrateShotScale` has no
|
|
13
|
-
* `shotScale` target for it and returns the fallback. A group that FITS 9:16 is
|
|
14
|
-
* then left with wasted margin instead of being framed tight. `fitZoom` sizes the
|
|
15
|
-
* crop to the group's union so a fitting group is zoom-fit (zoom < 1.0), while an
|
|
16
|
-
* over-wide group (members at opposite edges of a 16:9 frame) is the documented
|
|
17
|
-
* `overframed` outcome: pad-and-hold at zoom 1.0, emit the flag, NEVER silently
|
|
18
|
-
* clip a wing and NEVER auto-drop a member (the editorial drop/cut decision
|
|
19
|
-
* belongs to the driving agent — RESOLVED #1).
|
|
20
|
-
*
|
|
21
|
-
* Hold scope is explicit (resolves the per-frame-vs-per-shot contradiction):
|
|
22
|
-
* zoom re-fits PER SHOT (a punch-in at every detector cut), but WITHIN a shot the
|
|
23
|
-
* fit is sized ONCE over the shot-wide union extent and HELD — no per-frame
|
|
24
|
-
* breathing. A pre-pass here computes the shot-max union BEFORE the per-shot zoom
|
|
25
|
-
* split slices the series.
|
|
26
|
-
*
|
|
27
|
-
* Sub-second montage cuts are a named zoom-strobe regression risk (3–15-frame
|
|
28
|
-
* sub-shots each independently calibrated → strobe). Mitigation: a min-shot-
|
|
29
|
-
* length floor — a shot shorter than `MIN_ZOOM_SHOT_FRAMES` INHERITS the previous
|
|
30
|
-
* shot's zoom (pan still resets at the cut; pan is cameraPath's job). So a 1-frame
|
|
31
|
-
* shot can't spike the zoom channel.
|
|
32
|
-
*
|
|
33
|
-
* Pure, deterministic, no I/O. Boxes are source-normalized `[x1,y1,x2,y2]` ∈
|
|
34
|
-
* [0,1]; `pad` is a fraction of the source dimension (the SAME `REGION_PAD` the
|
|
35
|
-
* RequiredRegion uses, so the crop is sized to contain exactly the padded region
|
|
36
|
-
* the camera is obliged to keep in frame).
|
|
37
|
-
*/
|
|
38
|
-
import { REGION_PAD, MIN_ZOOM_SHOT_FRAMES, ZOOM_MIN, ZOOM_MAX } from './constants';
|
|
39
|
-
import { segmentShots } from './shots';
|
|
40
|
-
import { cropWindowSizeFor } from '../deriveTrajectory';
|
|
41
|
-
/**
|
|
42
|
-
* The shot-wide padded union extent of a per-frame box series, over `[start,
|
|
43
|
-
* end)`. The union of every present box in the shot, grown by `pad` on each side
|
|
44
|
-
* (clamped to [0,1]). Returns null when no box is present anywhere in the shot
|
|
45
|
-
* (the camera center-holds; there is nothing to fit). Source-normalized.
|
|
46
|
-
*/
|
|
47
|
-
export function shotUnionExtent(boxes, start, end, pad = REGION_PAD) {
|
|
48
|
-
let x1 = Infinity;
|
|
49
|
-
let y1 = Infinity;
|
|
50
|
-
let x2 = -Infinity;
|
|
51
|
-
let y2 = -Infinity;
|
|
52
|
-
let any = false;
|
|
53
|
-
for (let i = start; i < end; i++) {
|
|
54
|
-
const b = i >= 0 && i < boxes.length ? boxes[i] : null;
|
|
55
|
-
if (!b)
|
|
56
|
-
continue;
|
|
57
|
-
any = true;
|
|
58
|
-
if (b[0] < x1)
|
|
59
|
-
x1 = b[0];
|
|
60
|
-
if (b[1] < y1)
|
|
61
|
-
y1 = b[1];
|
|
62
|
-
if (b[2] > x2)
|
|
63
|
-
x2 = b[2];
|
|
64
|
-
if (b[3] > y2)
|
|
65
|
-
y2 = b[3];
|
|
66
|
-
}
|
|
67
|
-
if (!any)
|
|
68
|
-
return null;
|
|
69
|
-
const px1 = Math.max(0, x1 - pad);
|
|
70
|
-
const py1 = Math.max(0, y1 - pad);
|
|
71
|
-
const px2 = Math.min(1, x2 + pad);
|
|
72
|
-
const py2 = Math.min(1, y2 + pad);
|
|
73
|
-
return { w: Math.max(0, px2 - px1), h: Math.max(0, py2 - py1) };
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* The automatic zoom-to-fit for ONE shot's union extent. Sizes the crop so the
|
|
77
|
-
* largest output-aspect rect that fits the source, scaled by this zoom, CONTAINS
|
|
78
|
-
* the padded union on BOTH axes. The crop window scales linearly with zoom
|
|
79
|
-
* (`cropWindowSizeFor`: `cropW *= zoom`), so the smallest zoom that contains a
|
|
80
|
-
* source-normalized union of size `(uw, uh)` is `max(uw/Wb, uh/Hb)` where
|
|
81
|
-
* `(Wb, Hb)` is the zoom-1.0 window.
|
|
82
|
-
*
|
|
83
|
-
* Fits (≤ 1.0) → that zoom, clamped to the floor. Doesn't fit (> 1.0 needed, the
|
|
84
|
-
* group is wider than the output aspect) → `overframed`, pad-and-hold at 1.0. A
|
|
85
|
-
* null/empty extent → no subject to fit → zoom 1.0, not overframed (center-hold).
|
|
86
|
-
*/
|
|
87
|
-
export function fitZoom(extent, srcW, srcH, aspect) {
|
|
88
|
-
if (extent === null || extent.w <= 0 || extent.h <= 0) {
|
|
89
|
-
return { zoom: ZOOM_MAX, overframed: false };
|
|
90
|
-
}
|
|
91
|
-
const base = cropWindowSizeFor(srcW, srcH, ZOOM_MAX, aspect); // zoom-1.0 window
|
|
92
|
-
if (base.w <= 0 || base.h <= 0)
|
|
93
|
-
return { zoom: ZOOM_MAX, overframed: false };
|
|
94
|
-
const needed = Math.max(extent.w / base.w, extent.h / base.h);
|
|
95
|
-
if (needed > ZOOM_MAX) {
|
|
96
|
-
// Wider than the output aspect at zoom 1.0 — the documented `overframed`
|
|
97
|
-
// outcome. Pad-and-hold at the widest honest frame.
|
|
98
|
-
return { zoom: ZOOM_MAX, overframed: true };
|
|
99
|
-
}
|
|
100
|
-
// Fits: take the contained zoom, clamped to the zoom floor (so an extremely
|
|
101
|
-
// small group can't over-zoom past ZOOM_MIN — the bake's tightest crop).
|
|
102
|
-
return { zoom: Math.max(ZOOM_MIN, needed), overframed: false };
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Per-shot automatic zoom-to-fit over a segment's per-frame union-box series.
|
|
106
|
-
* Computes the shot-wide union extent per shot, then `fitZoom`, applying the
|
|
107
|
-
* min-shot-length floor: a shot shorter than `MIN_ZOOM_SHOT_FRAMES` INHERITS the
|
|
108
|
-
* previous shot's fit (zoom + overframed) so a sub-second montage cut can't strobe
|
|
109
|
-
* the zoom channel; pan still resets at the cut (cameraPath owns pan). The first
|
|
110
|
-
* shot has no predecessor → it fits on its own extent regardless of length (a
|
|
111
|
-
* single short clip must still frame its subject).
|
|
112
|
-
*
|
|
113
|
-
* Returns one `ShotFit` per shot, in shot order — the same shot order
|
|
114
|
-
* `segmentShots(interiorCuts, segLen)` produces. The caller (deriveTrajectory)
|
|
115
|
-
* applies the per-shot zoom to the matching sub-shot it already slices.
|
|
116
|
-
*/
|
|
117
|
-
export function fitZoomPerShot(boxes, interiorCuts, srcW, srcH, aspect, fps, segLen, pad = REGION_PAD) {
|
|
118
|
-
const shots = segmentShots(interiorCuts, segLen);
|
|
119
|
-
const minLen = Math.max(1, Math.round(MIN_ZOOM_SHOT_FRAMES * fps));
|
|
120
|
-
const out = [];
|
|
121
|
-
let prev = null;
|
|
122
|
-
for (const shot of shots) {
|
|
123
|
-
const shotLen = shot.end - shot.start;
|
|
124
|
-
if (prev !== null && shotLen < minLen) {
|
|
125
|
-
// Sub-MIN_ZOOM_SHOT_FRAMES → inherit the previous shot's zoom (no recalibrate
|
|
126
|
-
// → no strobe). Pan still resets at the cut in cameraPath.
|
|
127
|
-
out.push({ zoom: prev.zoom, overframed: prev.overframed });
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
const extent = shotUnionExtent(boxes, shot.start, shot.end, pad);
|
|
131
|
-
const fit = fitZoom(extent, srcW, srcH, aspect);
|
|
132
|
-
out.push(fit);
|
|
133
|
-
prev = fit;
|
|
134
|
-
}
|
|
135
|
-
return out;
|
|
136
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vcam/requiredRegion.ts — every focus mode's per-frame target, lowered into a
|
|
3
|
-
* `RequiredRegion`: the SOURCE-NORMALIZED box the crop window MUST contain.
|
|
4
|
-
*
|
|
5
|
-
* Phase 3 of the virtual-camera redesign (brain spec
|
|
6
|
-
* 2026-06-21-virtual-camera-reframe-design.md, "Every focus mode lowers to a
|
|
7
|
-
* RequiredRegion" + "Virtual-camera stability"). A RequiredRegion is the padded
|
|
8
|
-
* subject/group extent (subject box + headroom pad). `cameraPath` then enforces
|
|
9
|
-
* the INCLUSION INVARIANT: the crop always contains the RequiredRegion ⇒ the
|
|
10
|
-
* subject can never walk out of frame.
|
|
11
|
-
*
|
|
12
|
-
* This is a REPRESENTATION, not a behavior change for the modes that already
|
|
13
|
-
* work: `face` lowers its single per-frame subject box; `point` a zero-size rect
|
|
14
|
-
* at the authored location; `frame-center` the center rect. Only `all-faces`
|
|
15
|
-
* gains tracking — its region is the per-frame framed-group union box from
|
|
16
|
-
* `subjectSet` (hysteresis-filtered), so the camera follows a moving group.
|
|
17
|
-
*
|
|
18
|
-
* Pure, deterministic, no I/O. All boxes are source-normalized `[x1,y1,x2,y2]`
|
|
19
|
-
* ∈ [0,1]; `REGION_PAD` is a fraction of the source dimension (the geometry is
|
|
20
|
-
* source-normalized — the deadzone in `cameraPath` is the output-crop-normalized
|
|
21
|
-
* hold band, a separate stage).
|
|
22
|
-
*/
|
|
23
|
-
import { REGION_PAD, REGION_MEDIAN_SEC } from './constants';
|
|
24
|
-
import { segmentShots } from './shots';
|
|
25
|
-
/** Pad a source-normalized box on every side by `pad` (clamped to [0,1]) and
|
|
26
|
-
* express it as a `RequiredRegion` (center + padded size). A zero-area box (a
|
|
27
|
-
* `point` target) yields a zero-size padded region at the point. */
|
|
28
|
-
export function padBox(box, pad = REGION_PAD) {
|
|
29
|
-
const [x1, y1, x2, y2] = box;
|
|
30
|
-
const px1 = Math.max(0, Math.min(1, x1 - pad));
|
|
31
|
-
const py1 = Math.max(0, Math.min(1, y1 - pad));
|
|
32
|
-
const px2 = Math.max(0, Math.min(1, x2 + pad));
|
|
33
|
-
const py2 = Math.max(0, Math.min(1, y2 + pad));
|
|
34
|
-
return {
|
|
35
|
-
cx: (px1 + px2) / 2,
|
|
36
|
-
cy: (py1 + py2) / 2,
|
|
37
|
-
w: Math.max(0, px2 - px1),
|
|
38
|
-
h: Math.max(0, py2 - py1),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
/** A zero-size RequiredRegion at a single point (the `point` mode target, or any
|
|
42
|
-
* centroid-only target with no extent). Padded so a point still carries a small
|
|
43
|
-
* margin the crop must contain. */
|
|
44
|
-
export function pointRegion(c, pad = REGION_PAD) {
|
|
45
|
-
return padBox([c[0], c[1], c[0], c[1]], pad);
|
|
46
|
-
}
|
|
47
|
-
/** The frame-center RequiredRegion — a zero-size region at source center. The
|
|
48
|
-
* crop centering on it is a no-op (the window is already centerable), so this
|
|
49
|
-
* preserves `frame-center`'s `[0.5,0.5]` behavior exactly. */
|
|
50
|
-
export function centerRegion() {
|
|
51
|
-
return { cx: 0.5, cy: 0.5, w: 0, h: 0 };
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Lower a per-frame series of source-normalized union boxes (e.g. `all-faces`'s
|
|
55
|
-
* per-frame framed-group box from `subjectSet.perFrameBoxes`) into per-frame
|
|
56
|
-
* RequiredRegions. A null box (no framed member present — occlusion / gap) →
|
|
57
|
-
* null region; `cameraPath` forward-fills the last in-shot region across the
|
|
58
|
-
* gap (so the camera holds, not snaps to center, during a brief occlusion).
|
|
59
|
-
*/
|
|
60
|
-
export function regionsFromBoxes(boxes, pad = REGION_PAD) {
|
|
61
|
-
return boxes.map((b) => (b === null ? null : padBox(b, pad)));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Lower a per-frame series of centroid-only targets (the `face` /
|
|
65
|
-
* `active-speaker` per-frame center) into zero-size RequiredRegions. These modes
|
|
66
|
-
* carry a single subject point, not a measured box, in the crop-center path —
|
|
67
|
-
* lowering to a padded point keeps their framing (the camera points at the
|
|
68
|
-
* subject) while giving `cameraPath` a region to include. A null center (subject
|
|
69
|
-
* absent this frame) → null region (forward-filled by `cameraPath`).
|
|
70
|
-
*/
|
|
71
|
-
export function regionsFromCenters(centers, pad = REGION_PAD) {
|
|
72
|
-
return centers.map((c) => (c === null ? null : pointRegion(c, pad)));
|
|
73
|
-
}
|
|
74
|
-
/** Median of a numeric window (lower-median for even length). */
|
|
75
|
-
function med(xs) {
|
|
76
|
-
const s = [...xs].sort((a, b) => a - b);
|
|
77
|
-
return s[Math.floor(s.length / 2)];
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Outlier-robust median filter on a region series within a SHOT (radius `r`
|
|
81
|
-
* frames). Single-frame detector/ReID spikes — a crowd ReID flicker that
|
|
82
|
-
* teleports the box across the frame for one frame, then snaps back — would
|
|
83
|
-
* otherwise yank the camera (the chase, re-skinned). A short median on
|
|
84
|
-
* cx/cy/w/h rejects them while tracking a clean multi-frame walk (an order
|
|
85
|
-
* statistic, lag-free on a monotone pan). The window is clamped to the shot
|
|
86
|
-
* (never reads past a cut). Null entries are skipped in the window (a present
|
|
87
|
-
* spike isn't outvoted by absent frames). Identity for r < 1.
|
|
88
|
-
*/
|
|
89
|
-
export function medianFilterRegions(regions, r, shotStart, shotEnd) {
|
|
90
|
-
const out = [...regions];
|
|
91
|
-
if (r < 1)
|
|
92
|
-
return out;
|
|
93
|
-
for (let i = shotStart; i < shotEnd; i++) {
|
|
94
|
-
if (regions[i] === null)
|
|
95
|
-
continue;
|
|
96
|
-
const lo = Math.max(shotStart, i - r);
|
|
97
|
-
const hi = Math.min(shotEnd - 1, i + r);
|
|
98
|
-
const cx = [];
|
|
99
|
-
const cy = [];
|
|
100
|
-
const w = [];
|
|
101
|
-
const h = [];
|
|
102
|
-
for (let j = lo; j <= hi; j++) {
|
|
103
|
-
const rj = regions[j];
|
|
104
|
-
if (rj === null)
|
|
105
|
-
continue;
|
|
106
|
-
cx.push(rj.cx);
|
|
107
|
-
cy.push(rj.cy);
|
|
108
|
-
w.push(rj.w);
|
|
109
|
-
h.push(rj.h);
|
|
110
|
-
}
|
|
111
|
-
if (cx.length === 0)
|
|
112
|
-
continue;
|
|
113
|
-
out[i] = { cx: med(cx), cy: med(cy), w: med(w), h: med(h) };
|
|
114
|
-
}
|
|
115
|
-
return out;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Build the canonical per-frame RequiredRegions for an `all-faces` segment from
|
|
119
|
-
* the per-frame framed-group union boxes (`subjectSet.perFrameBoxes`): pad each
|
|
120
|
-
* box, then outlier-reject single-frame spikes per shot. This is THE region the
|
|
121
|
-
* camera is obliged to contain AND the region the eval measures inclusion
|
|
122
|
-
* against — one source of truth, so the gate is honest (the camera is judged on
|
|
123
|
-
* the same temporally-coherent region it frames). `interiorCuts` are the segment-
|
|
124
|
-
* local shot boundaries (median windows never cross a cut). `fps` sizes the
|
|
125
|
-
* median radius. Null boxes (occlusion) stay null (forward-filled by cameraPath).
|
|
126
|
-
*/
|
|
127
|
-
export function buildRequiredRegions(boxes, fps, interiorCuts, pad = REGION_PAD) {
|
|
128
|
-
const padded = regionsFromBoxes(boxes, pad);
|
|
129
|
-
const r = Math.round(REGION_MEDIAN_SEC * fps);
|
|
130
|
-
if (r < 1)
|
|
131
|
-
return padded;
|
|
132
|
-
let out = padded;
|
|
133
|
-
for (const shot of segmentShots(interiorCuts, padded.length)) {
|
|
134
|
-
out = medianFilterRegions(out, r, shot.start, shot.end);
|
|
135
|
-
}
|
|
136
|
-
return out;
|
|
137
|
-
}
|