davinci-resolve-mcp 2.57.4 → 2.58.0
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/AGENTS.md +20 -0
- package/CHANGELOG.md +197 -2
- package/LICENSE +1 -1
- package/README.md +73 -1
- package/bin/davinci-resolve-advanced-mcp.mjs +20 -0
- package/docs/README.md +19 -0
- package/docs/SKILL.md +62 -1
- package/docs/contributing.md +1 -1
- package/docs/guides/control-panel.md +9 -0
- package/docs/kernels/README.md +21 -0
- package/docs/kernels/audio-fairlight-kernel.md +22 -0
- package/docs/kernels/color-grade-kernel.md +51 -0
- package/docs/kernels/fusion-composition-kernel.md +17 -0
- package/docs/kernels/media-pool-ingest-kernel.md +20 -0
- package/docs/kernels/render-deliver-kernel.md +32 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +39 -0
- package/docs/kernels/timeline-edit-kernel.md +17 -0
- package/docs/process/release-process.md +29 -7
- package/docs/reference/api-coverage.md +1 -1
- package/docs/reference/api-limitations.md +38 -1
- package/install.py +27 -4
- package/package.json +23 -2
- package/resolve-advanced/README.md +194 -0
- package/resolve-advanced/package.json +50 -0
- package/resolve-advanced/server/black-balance.mjs +92 -0
- package/resolve-advanced/server/capabilities.mjs +86 -0
- package/resolve-advanced/server/cdl-io.mjs +76 -0
- package/resolve-advanced/server/contrast-normalize.mjs +180 -0
- package/resolve-advanced/server/db-patch.mjs +91 -0
- package/resolve-advanced/server/deliverable-entities.mjs +86 -0
- package/resolve-advanced/server/deliverable-qc.mjs +270 -0
- package/resolve-advanced/server/editorial.mjs +370 -0
- package/resolve-advanced/server/exposure-level.mjs +129 -0
- package/resolve-advanced/server/extract-frames.mjs +196 -0
- package/resolve-advanced/server/ffprobe-media.mjs +88 -0
- package/resolve-advanced/server/gamut-legal.mjs +112 -0
- package/resolve-advanced/server/grade-body-patch.mjs +246 -0
- package/resolve-advanced/server/grade-transfer.mjs +66 -0
- package/resolve-advanced/server/group-grade-read.mjs +137 -0
- package/resolve-advanced/server/index.mjs +105 -0
- package/resolve-advanced/server/lib.mjs +84 -0
- package/resolve-advanced/server/libs.mjs +65 -0
- package/resolve-advanced/server/lineage-db.mjs +479 -0
- package/resolve-advanced/server/lut-apply.mjs +61 -0
- package/resolve-advanced/server/match-to-reference.mjs +173 -0
- package/resolve-advanced/server/media-inventory.mjs +169 -0
- package/resolve-advanced/server/media-ops.mjs +243 -0
- package/resolve-advanced/server/node-meta-db.mjs +62 -0
- package/resolve-advanced/server/node-provenance.mjs +102 -0
- package/resolve-advanced/server/offline-ref-db.mjs +420 -0
- package/resolve-advanced/server/offline-ref.mjs +179 -0
- package/resolve-advanced/server/project-db.mjs +233 -0
- package/resolve-advanced/server/provenance-audit.mjs +210 -0
- package/resolve-advanced/server/qc-frame.mjs +224 -0
- package/resolve-advanced/server/qc-sampler.mjs +67 -0
- package/resolve-advanced/server/readback.mjs +77 -0
- package/resolve-advanced/server/render-manifest.mjs +93 -0
- package/resolve-advanced/server/reverse-clip-db.mjs +270 -0
- package/resolve-advanced/server/runner-apply-contract.mjs +64 -0
- package/resolve-advanced/server/runner.mjs +244 -0
- package/resolve-advanced/server/saturation-match.mjs +108 -0
- package/resolve-advanced/server/scope-read.mjs +257 -0
- package/resolve-advanced/server/season-look.mjs +96 -0
- package/resolve-advanced/server/shot-intent.mjs +82 -0
- package/resolve-advanced/server/shot-match.mjs +147 -0
- package/resolve-advanced/server/skin-match.mjs +205 -0
- package/resolve-advanced/server/spec-compile.mjs +217 -0
- package/resolve-advanced/server/tone-curve-transfer.mjs +184 -0
- package/resolve-advanced/server/tool-catalog.mjs +265 -0
- package/resolve-advanced/server/tools/audio.mjs +54 -0
- package/resolve-advanced/server/tools/audio_plan.mjs +58 -0
- package/resolve-advanced/server/tools/capabilities.mjs +16 -0
- package/resolve-advanced/server/tools/color_trace.mjs +165 -0
- package/resolve-advanced/server/tools/conform.mjs +291 -0
- package/resolve-advanced/server/tools/deliverable.mjs +105 -0
- package/resolve-advanced/server/tools/drp.mjs +232 -0
- package/resolve-advanced/server/tools/drt.mjs +200 -0
- package/resolve-advanced/server/tools/drx.mjs +943 -0
- package/resolve-advanced/server/tools/editorial.mjs +69 -0
- package/resolve-advanced/server/tools/fairlight.mjs +108 -0
- package/resolve-advanced/server/tools/fusion.mjs +64 -0
- package/resolve-advanced/server/tools/media.mjs +102 -0
- package/resolve-advanced/server/tools/offline_ref.mjs +123 -0
- package/resolve-advanced/server/tools/pipeline.mjs +137 -0
- package/resolve-advanced/server/tools/project_db.mjs +200 -0
- package/resolve-advanced/server/tools/project_read.mjs +212 -0
- package/resolve-advanced/server/tools/provenance.mjs +73 -0
- package/resolve-advanced/server/verify-grade.mjs +113 -0
- package/resolve-advanced/server/white-balance-match.mjs +122 -0
- package/resolve-advanced/vendor/audio/format-converter.js +330 -0
- package/resolve-advanced/vendor/audio/split.js +173 -0
- package/resolve-advanced/vendor/audio/trim.js +188 -0
- package/resolve-advanced/vendor/audio-fairlight/index.js +391 -0
- package/resolve-advanced/vendor/conform-qc/adapters/frame-sampler.js +56 -0
- package/resolve-advanced/vendor/conform-qc/adapters/local-ffmpeg-sampler.js +65 -0
- package/resolve-advanced/vendor/conform-qc/adapters/rendernode-ffmpeg-sampler.js +50 -0
- package/resolve-advanced/vendor/conform-qc/adapters/resolve/driver.py +176 -0
- package/resolve-advanced/vendor/conform-qc/adapters/resolve-driver.js +61 -0
- package/resolve-advanced/vendor/conform-qc/adapters/resolve-headless-driver.js +86 -0
- package/resolve-advanced/vendor/conform-qc/adapters/vision-validator.js +75 -0
- package/resolve-advanced/vendor/conform-qc/cli.js +63 -0
- package/resolve-advanced/vendor/conform-qc/compare/advisory.js +106 -0
- package/resolve-advanced/vendor/conform-qc/compare/decode.js +36 -0
- package/resolve-advanced/vendor/conform-qc/compare/index.js +61 -0
- package/resolve-advanced/vendor/conform-qc/compare/locate.js +70 -0
- package/resolve-advanced/vendor/conform-qc/compare/metrics.js +409 -0
- package/resolve-advanced/vendor/conform-qc/docs/runbook.md +72 -0
- package/resolve-advanced/vendor/conform-qc/index.js +49 -0
- package/resolve-advanced/vendor/conform-qc/knowledge/index.js +110 -0
- package/resolve-advanced/vendor/conform-qc/ops/conform-core.js +137 -0
- package/resolve-advanced/vendor/conform-qc/ops/conform-diff.js +109 -0
- package/resolve-advanced/vendor/conform-qc/ops/insert.js +108 -0
- package/resolve-advanced/vendor/conform-qc/ops/local-conform.js +61 -0
- package/resolve-advanced/vendor/conform-qc/ops/patch.js +80 -0
- package/resolve-advanced/vendor/conform-qc/ops/trigger.js +32 -0
- package/resolve-advanced/vendor/conform-qc/ops/verify.js +105 -0
- package/resolve-advanced/vendor/conform-qc/ops/workflow.js +30 -0
- package/resolve-advanced/vendor/conform-qc/oracle/emulate.js +155 -0
- package/resolve-advanced/vendor/conform-qc/oracle/index.js +38 -0
- package/resolve-advanced/vendor/conform-qc/oracle/resolve.js +196 -0
- package/resolve-advanced/vendor/conform-qc/package.json +16 -0
- package/resolve-advanced/vendor/conform-qc/packaging/emit-aaf.js +30 -0
- package/resolve-advanced/vendor/conform-qc/packaging/emit-fcp7.js +64 -0
- package/resolve-advanced/vendor/conform-qc/packaging/index.js +169 -0
- package/resolve-advanced/vendor/conform-qc/packaging/media-ops.js +56 -0
- package/resolve-advanced/vendor/conform-qc/packaging/otio.js +50 -0
- package/resolve-advanced/vendor/conform-qc/packaging/relink.js +50 -0
- package/resolve-advanced/vendor/conform-qc/packaging/surgical-relink.js +195 -0
- package/resolve-advanced/vendor/conform-qc/parse/index.js +54 -0
- package/resolve-advanced/vendor/conform-qc/parse/merge-dto.js +27 -0
- package/resolve-advanced/vendor/conform-qc/parse/xmeml-geometry.js +256 -0
- package/resolve-advanced/vendor/conform-qc/reference/burnin-ocr.js +100 -0
- package/resolve-advanced/vendor/conform-qc/reference/sidecar.js +37 -0
- package/resolve-advanced/vendor/conform-qc/repair/analysis.js +62 -0
- package/resolve-advanced/vendor/conform-qc/repair/index.js +78 -0
- package/resolve-advanced/vendor/conform-qc/repair/media-analysis.js +139 -0
- package/resolve-advanced/vendor/conform-qc/repair/media-index.js +135 -0
- package/resolve-advanced/vendor/conform-qc/repair/strategies.js +250 -0
- package/resolve-advanced/vendor/conform-qc/report/index.js +133 -0
- package/resolve-advanced/vendor/conform-qc/roles/index.js +96 -0
- package/resolve-advanced/vendor/conform-qc/synthetic/generate.js +233 -0
- package/resolve-advanced/vendor/conform-qc/test/advisory.test.js +85 -0
- package/resolve-advanced/vendor/conform-qc/test/cli.test.js +39 -0
- package/resolve-advanced/vendor/conform-qc/test/compare.test.js +152 -0
- package/resolve-advanced/vendor/conform-qc/test/conform-diff.test.js +70 -0
- package/resolve-advanced/vendor/conform-qc/test/emulate.test.js +51 -0
- package/resolve-advanced/vendor/conform-qc/test/frame-sampler.test.js +30 -0
- package/resolve-advanced/vendor/conform-qc/test/geometry.test.js +112 -0
- package/resolve-advanced/vendor/conform-qc/test/knowledge.test.js +56 -0
- package/resolve-advanced/vendor/conform-qc/test/lifecycle.test.js +110 -0
- package/resolve-advanced/vendor/conform-qc/test/local-ffmpeg-sampler.test.js +66 -0
- package/resolve-advanced/vendor/conform-qc/test/locate.test.js +63 -0
- package/resolve-advanced/vendor/conform-qc/test/media-analysis.test.js +63 -0
- package/resolve-advanced/vendor/conform-qc/test/media-index-normalized.test.js +65 -0
- package/resolve-advanced/vendor/conform-qc/test/media-index-origin.test.js +53 -0
- package/resolve-advanced/vendor/conform-qc/test/metrics-fast.test.js +91 -0
- package/resolve-advanced/vendor/conform-qc/test/oracle-reverse.test.js +86 -0
- package/resolve-advanced/vendor/conform-qc/test/oracle.test.js +136 -0
- package/resolve-advanced/vendor/conform-qc/test/p0-acceptance.test.js +60 -0
- package/resolve-advanced/vendor/conform-qc/test/p1-acceptance.test.js +75 -0
- package/resolve-advanced/vendor/conform-qc/test/p45-patch-acceptance.test.js +102 -0
- package/resolve-advanced/vendor/conform-qc/test/packaging.test.js +133 -0
- package/resolve-advanced/vendor/conform-qc/test/parse-stubs.test.js +28 -0
- package/resolve-advanced/vendor/conform-qc/test/repair-strategies.test.js +128 -0
- package/resolve-advanced/vendor/conform-qc/test/repair.test.js +84 -0
- package/resolve-advanced/vendor/conform-qc/test/report.test.js +52 -0
- package/resolve-advanced/vendor/conform-qc/test/resolve-readback.test.js +71 -0
- package/resolve-advanced/vendor/conform-qc/test/roles.test.js +107 -0
- package/resolve-advanced/vendor/conform-qc/test/runbook.test.js +30 -0
- package/resolve-advanced/vendor/conform-qc/test/scale-residual.test.js +40 -0
- package/resolve-advanced/vendor/conform-qc/test/sidecar.test.js +51 -0
- package/resolve-advanced/vendor/conform-qc/test/smoke.test.js +87 -0
- package/resolve-advanced/vendor/conform-qc/test/surgical-relink.test.js +73 -0
- package/resolve-advanced/vendor/conform-qc/test/synthetic.test.js +98 -0
- package/resolve-advanced/vendor/conform-qc/test/verify.test.js +93 -0
- package/resolve-advanced/vendor/conform-qc/test/vision-validator.test.js +43 -0
- package/resolve-advanced/vendor/conform-qc/test/workflow-driver.test.js +46 -0
- package/resolve-advanced/vendor/conform-qc/toolset/index.js +53 -0
- package/resolve-advanced/vendor/drp-format/README.md +116 -0
- package/resolve-advanced/vendor/drp-format/__tests__/_resolve-verify.js +39 -0
- package/resolve-advanced/vendor/drp-format/__tests__/add-media-clip.test.js +30 -0
- package/resolve-advanced/vendor/drp-format/__tests__/assemble-timeline.test.js +68 -0
- package/resolve-advanced/vendor/drp-format/__tests__/author-project.test.js +57 -0
- package/resolve-advanced/vendor/drp-format/__tests__/composition-text.test.js +98 -0
- package/resolve-advanced/vendor/drp-format/__tests__/diff.test.js +272 -0
- package/resolve-advanced/vendor/drp-format/__tests__/effect-filters-compressed.test.js +143 -0
- package/resolve-advanced/vendor/drp-format/__tests__/fixtures/p0-3-session30-empty-effectfilters.drp +0 -0
- package/resolve-advanced/vendor/drp-format/__tests__/fixtures/retimed-timemap-50pct.hex +1 -0
- package/resolve-advanced/vendor/drp-format/__tests__/fixtures/retimed-timemap-dynamic.hex +1 -0
- package/resolve-advanced/vendor/drp-format/__tests__/inject-grades.test.js +238 -0
- package/resolve-advanced/vendor/drp-format/__tests__/keyed-dict.test.js +85 -0
- package/resolve-advanced/vendor/drp-format/__tests__/lut-refs.test.js +119 -0
- package/resolve-advanced/vendor/drp-format/__tests__/media-blobs.test.js +25 -0
- package/resolve-advanced/vendor/drp-format/__tests__/media-timemap.test.js +119 -0
- package/resolve-advanced/vendor/drp-format/__tests__/place-fusion-title.test.js +52 -0
- package/resolve-advanced/vendor/drp-format/__tests__/place-generator.test.js +40 -0
- package/resolve-advanced/vendor/drp-format/__tests__/place-transition.test.js +48 -0
- package/resolve-advanced/vendor/drp-format/__tests__/protobuf-wire.test.js +64 -0
- package/resolve-advanced/vendor/drp-format/__tests__/relink-media.test.js +106 -0
- package/resolve-advanced/vendor/drp-format/__tests__/splice-clips.test.js +256 -0
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +65 -0
- package/resolve-advanced/vendor/drp-format/author-project.js +118 -0
- package/resolve-advanced/vendor/drp-format/composition-text.js +213 -0
- package/resolve-advanced/vendor/drp-format/curves-encoder.js +763 -0
- package/resolve-advanced/vendor/drp-format/diff.js +310 -0
- package/resolve-advanced/vendor/drp-format/drp-packager.js +201 -0
- package/resolve-advanced/vendor/drp-format/drp-validator.js +809 -0
- package/resolve-advanced/vendor/drp-format/effect-encoder.js +640 -0
- package/resolve-advanced/vendor/drp-format/extract-lut-refs.js +119 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.example.js +133 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.js +401 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.test.js +171 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.verify.js +249 -0
- package/resolve-advanced/vendor/drp-format/grade-node-extractor.js +495 -0
- package/resolve-advanced/vendor/drp-format/grade-parameter-decoder.js +774 -0
- package/resolve-advanced/vendor/drp-format/index.js +174 -0
- package/resolve-advanced/vendor/drp-format/inject-grades.js +219 -0
- package/resolve-advanced/vendor/drp-format/keyed-dict.js +227 -0
- package/resolve-advanced/vendor/drp-format/marker-encoder.js +532 -0
- package/resolve-advanced/vendor/drp-format/media-blobs.js +53 -0
- package/resolve-advanced/vendor/drp-format/media-timemap.js +164 -0
- package/resolve-advanced/vendor/drp-format/mp-folder-builder.js +293 -0
- package/resolve-advanced/vendor/drp-format/node-tree-encoder.js +1246 -0
- package/resolve-advanced/vendor/drp-format/package.json +18 -0
- package/resolve-advanced/vendor/drp-format/place-fusion-title.js +131 -0
- package/resolve-advanced/vendor/drp-format/place-generator.js +77 -0
- package/resolve-advanced/vendor/drp-format/place-transition.js +91 -0
- package/resolve-advanced/vendor/drp-format/power-window-encoder.js +776 -0
- package/resolve-advanced/vendor/drp-format/protobuf-wire.js +111 -0
- package/resolve-advanced/vendor/drp-format/qualifier-encoder.js +706 -0
- package/resolve-advanced/vendor/drp-format/relink-media.js +218 -0
- package/resolve-advanced/vendor/drp-format/rich-title-encoder.js +447 -0
- package/resolve-advanced/vendor/drp-format/seq-container-builder.js +572 -0
- package/resolve-advanced/vendor/drp-format/seq-surgery.js +155 -0
- package/resolve-advanced/vendor/drp-format/splice-clips.js +445 -0
- package/resolve-advanced/vendor/drp-format/templates/empty-project.drp +0 -0
- package/resolve-advanced/vendor/drp-format/templates/fusion-title.xml +56 -0
- package/resolve-advanced/vendor/drp-format/templates/generator-solid-color.xml +21 -0
- package/resolve-advanced/vendor/drp-format/templates/media-clip-h264.drp +0 -0
- package/resolve-advanced/vendor/drp-format/templates/transition-cross-dissolve.xml +22 -0
- package/resolve-advanced/vendor/drp-format/tool-registry.descriptor.js +80 -0
- package/resolve-advanced/vendor/drp-format/utils/safe-archive.js +54 -0
- package/resolve-advanced/vendor/drp-format/xml-builder.js +759 -0
- package/resolve-advanced/vendor/drt-format/__tests__/_resolve-verify.js +21 -0
- package/resolve-advanced/vendor/drt-format/__tests__/build.test.js +113 -0
- package/resolve-advanced/vendor/drt-format/__tests__/multi-format.test.js +93 -0
- package/resolve-advanced/vendor/drt-format/__tests__/parse-real-resolve21.test.js +51 -0
- package/resolve-advanced/vendor/drt-format/__tests__/parse.test.js +134 -0
- package/resolve-advanced/vendor/drt-format/__tests__/resolve-versions.test.js +120 -0
- package/resolve-advanced/vendor/drt-format/__tests__/schema-fingerprint.test.js +62 -0
- package/resolve-advanced/vendor/drt-format/__tests__/smoke.test.js +27 -0
- package/resolve-advanced/vendor/drt-format/__tests__/validate.test.js +109 -0
- package/resolve-advanced/vendor/drt-format/capabilities.js +69 -0
- package/resolve-advanced/vendor/drt-format/capability-domains.js +56 -0
- package/resolve-advanced/vendor/drt-format/drt-builder.js +42 -0
- package/resolve-advanced/vendor/drt-format/drt-parser.js +164 -0
- package/resolve-advanced/vendor/drt-format/drt-validator.js +98 -0
- package/resolve-advanced/vendor/drt-format/index.js +45 -0
- package/resolve-advanced/vendor/drt-format/package.json +17 -0
- package/resolve-advanced/vendor/drt-format/resolve-versions.js +153 -0
- package/resolve-advanced/vendor/drt-format/schema-fingerprint.js +74 -0
- package/resolve-advanced/vendor/drt-format/templates/drt-project-shell.xml +124 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/curves-roundtrip.test.js +148 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-face-refinement.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-filmgrain.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-glow.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-lens-flare.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-3-beauty-25.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-3-beauty-75.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p5-1-node-no-lut.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p5-1-node-with-lut.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/hsl-curves-roundtrip.test.js +117 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/lut-refs-roundtrip.test.js +112 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/matte-roundtrip.test.js +167 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/ofx-roundtrip.test.js +215 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/ofx-slugs-discovery.test.js +149 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/power-window-roundtrip.test.js +230 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/qualifier-roundtrip.test.js +226 -0
- package/resolve-advanced/vendor/drx-codec/cdl-exporter.js +470 -0
- package/resolve-advanced/vendor/drx-codec/color-nlp-parser.js +833 -0
- package/resolve-advanced/vendor/drx-codec/data/color-nlp-action-mappings.json +2232 -0
- package/resolve-advanced/vendor/drx-codec/drx-generator.js +4123 -0
- package/resolve-advanced/vendor/drx-codec/drx-merger.js +320 -0
- package/resolve-advanced/vendor/drx-codec/drx-parser.js +1808 -0
- package/resolve-advanced/vendor/drx-codec/extract-custom-curves.js +215 -0
- package/resolve-advanced/vendor/drx-codec/extract-hsl-curves.js +180 -0
- package/resolve-advanced/vendor/drx-codec/extract-lut-refs.js +158 -0
- package/resolve-advanced/vendor/drx-codec/extract-matte-finesse.js +83 -0
- package/resolve-advanced/vendor/drx-codec/extract-ofx-params.js +226 -0
- package/resolve-advanced/vendor/drx-codec/extract-power-window.js +184 -0
- package/resolve-advanced/vendor/drx-codec/extract-qualifier.js +99 -0
- package/resolve-advanced/vendor/drx-codec/index.js +109 -0
- package/resolve-advanced/vendor/drx-codec/node-layout.js +249 -0
- package/resolve-advanced/vendor/drx-codec/resolved-drx-generator.js +393 -0
- package/resolve-advanced/vendor/drx-codec/tool-resolver.js +974 -0
- package/resolve-advanced/vendor/drx-codec/training-examples.js +548 -0
- package/resolve-advanced/vendor/drx-codec/vocabulary-intents.js +539 -0
- package/resolve-advanced/vendor/drx-parameters/CALIBRATION-STATUS.md +319 -0
- package/resolve-advanced/vendor/drx-parameters/DRX-VALUE-SCALING.md +251 -0
- package/resolve-advanced/vendor/drx-parameters/calibrated-ranges.json +17737 -0
- package/resolve-advanced/vendor/drx-parameters/corrector-types.js +120 -0
- package/resolve-advanced/vendor/drx-parameters/index.js +193 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-codec.js +451 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-ids.js +1292 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-ranges.js +570 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-validator.js +392 -0
- package/resolve-advanced/vendor/drx-parameters/resolvefx-registry.json +14973 -0
- package/resolve-advanced/vendor/drx-parameters/tool-registry.descriptor.js +38 -0
- package/resolve-advanced/vendor/drx-parameters/tools/drx-analyzer.js +371 -0
- package/resolve-advanced/vendor/fairlight/index.js +833 -0
- package/resolve-advanced/vendor/fusion-codec/composition-generator.js +401 -0
- package/resolve-advanced/vendor/fusion-codec/templates/blur-region.js +103 -0
- package/resolve-advanced/vendor/fusion-codec/templates/color-correct.js +87 -0
- package/resolve-advanced/vendor/fusion-codec/templates/film-grain.js +106 -0
- package/resolve-advanced/vendor/fusion-codec/templates/lower-third.js +241 -0
- package/resolve-advanced/vendor/fusion-codec/templates/picture-in-picture.js +188 -0
- package/resolve-advanced/vendor/fusion-codec/templates/text-overlay.js +161 -0
- package/resolve-advanced/vendor/fusion-codec/templates/title-card.js +184 -0
- package/resolve-advanced/vendor/fusion-codec/templates/vignette.js +107 -0
- package/resolve-advanced/vendor/fusion-codec/templates/watermark.js +101 -0
- package/scripts/agent-rules/README.md +68 -0
- package/src/analysis_dashboard.py +233 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +479 -29
- package/src/utils/api_truth.py +117 -0
- package/src/utils/cut_ir.py +1 -1
- package/src/utils/failure_tracker.py +0 -2
- package/src/utils/timeline_xml.py +405 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* synthetic/generate.js — build a tiny, fully-synthetic, ANONYMIZED portable
|
|
5
|
+
* fixture with KNOWN golden answers (spec/harness: P0-synthetic-fixture).
|
|
6
|
+
*
|
|
7
|
+
* A real turnover.xml + frames are git-ignored client material, so on a
|
|
8
|
+
* fresh clone the parser/comparator tests skip. This generator emits a small
|
|
9
|
+
* crafted XMEML + test-pattern PNGs whose answers we compute by construction, so
|
|
10
|
+
* the WHOLE suite runs client-free. The output is committed under
|
|
11
|
+
* __fixtures__/synthetic/ (it contains no client data), and this generator makes
|
|
12
|
+
* it reproducible. No fabricated "expected" numbers — every golden here is
|
|
13
|
+
* derived from the same math the Oracle implements, encoded once.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const sharp = require('sharp');
|
|
19
|
+
|
|
20
|
+
const TPF = 10584000000; // ticks/frame @24
|
|
21
|
+
const SEQ_W = 1920;
|
|
22
|
+
const SEQ_H = 1080;
|
|
23
|
+
const SRC_W = 960; // so seqW/srcW = 2.0 (fit = 200%)
|
|
24
|
+
const SRC_H = 540;
|
|
25
|
+
|
|
26
|
+
// ── Clip definitions (inputs) + the answers they imply by construction ────────
|
|
27
|
+
// expected_source_start = startoffset + in ; ticks encode in (or 2*in for 50%).
|
|
28
|
+
// expected_scale_corrected = scale * srcW / seqW.
|
|
29
|
+
const CLIPS = [
|
|
30
|
+
{ key: 'normal', start: 0, end: 48, in: 1000, suboff: null, scale: 200, speed: 100,
|
|
31
|
+
center: { h: 0, v: 0 }, rotation: 0, crop: { left: 0, top: 0, right: 0, bottom: 0 } },
|
|
32
|
+
{ key: 'subclip', start: 48, end: 96, in: 500, suboff: 2000, scale: 200, speed: 100,
|
|
33
|
+
center: { h: 0, v: 0 }, rotation: 0, crop: { left: 0, top: 0, right: 0, bottom: 0 } },
|
|
34
|
+
{ key: 'slowmo', start: 96, end: 144, in: 3000, suboff: null, scale: 200, speed: 200, slow: true,
|
|
35
|
+
center: { h: 0, v: 0 }, rotation: 0, crop: { left: 0, top: 0, right: 0, bottom: 0 } },
|
|
36
|
+
{ key: 'reframe', start: 144, end: 192, in: 4000, suboff: null, scale: 250, speed: 100,
|
|
37
|
+
center: { h: 120, v: -60 }, rotation: 5, crop: { left: 10, top: 4, right: 8, bottom: 2 } },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function ticksFor(clip) {
|
|
41
|
+
// 50% slow-mo: ticks/tpf = 2*in (the Oracle detects retime here); else ticks/tpf = in.
|
|
42
|
+
const frames = clip.slow ? clip.in * 2 : clip.in;
|
|
43
|
+
return frames * TPF;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function expectedSourceStart(clip) {
|
|
47
|
+
return (clip.suboff || 0) + clip.in;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function expectedScaleCorrected(clip) {
|
|
51
|
+
return (clip.scale * SRC_W) / SEQ_W;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── XMEML emission ────────────────────────────────────────────────────────────
|
|
55
|
+
function fileDef(id, basename, full) {
|
|
56
|
+
if (!full) return `<file id="${id}"/>`;
|
|
57
|
+
return `<file id="${id}">
|
|
58
|
+
<name>${basename}</name>
|
|
59
|
+
<pathurl>file://localhost/SYNTH/${encodeURIComponent(basename)}</pathurl>
|
|
60
|
+
<media><video><samplecharacteristics>
|
|
61
|
+
<width>${SRC_W}</width><height>${SRC_H}</height>
|
|
62
|
+
</samplecharacteristics></video></media>
|
|
63
|
+
</file>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clipXml(clip, idx) {
|
|
67
|
+
const fileId = `file-${idx + 1}`;
|
|
68
|
+
const basename = `SYNTH_${clip.key}.mov`;
|
|
69
|
+
// Exercise file-id resolution: the 'normal' clip defines file-1 fully; later
|
|
70
|
+
// clips define their own; we ALSO make 'subclip' reference normal's file by id
|
|
71
|
+
// only when keys match — here each clip has its own file for clarity, but the
|
|
72
|
+
// 'reframe' clip references file-1 self-closing to prove resolution.
|
|
73
|
+
const usesSharedFile = clip.key === 'reframe';
|
|
74
|
+
const effectiveId = usesSharedFile ? 'file-1' : fileId;
|
|
75
|
+
const file = usesSharedFile ? fileDef('file-1', 'SYNTH_normal.mov', false) : fileDef(fileId, basename, true);
|
|
76
|
+
const sub = clip.suboff != null ? `<subclipinfo><startoffset>${clip.suboff}</startoffset></subclipinfo>` : '';
|
|
77
|
+
const timeRemap = clip.speed !== 100
|
|
78
|
+
? `<filter><effect><name>Time Remap</name><effectid>timeremap</effectid>
|
|
79
|
+
<parameter><parameterid>speed</parameterid><value>${clip.speed}</value></parameter>
|
|
80
|
+
<parameter><parameterid>variablespeed</parameterid><value>0</value></parameter>
|
|
81
|
+
</effect></filter>`
|
|
82
|
+
: '';
|
|
83
|
+
return `<clipitem id="clipitem-${idx + 1}">
|
|
84
|
+
<name>${clip.key}</name>
|
|
85
|
+
<start>${clip.start}</start>
|
|
86
|
+
<end>${clip.end}</end>
|
|
87
|
+
<in>${clip.in}</in>
|
|
88
|
+
<out>${clip.in + (clip.end - clip.start)}</out>
|
|
89
|
+
<pproTicksIn>${ticksFor(clip)}</pproTicksIn>
|
|
90
|
+
<pproTicksOut>${ticksFor(clip) + (clip.end - clip.start) * TPF}</pproTicksOut>
|
|
91
|
+
${effectiveId === 'file-1' ? file : file}
|
|
92
|
+
${sub}
|
|
93
|
+
<filter><effect><name>Basic Motion</name><effectid>basic</effectid>
|
|
94
|
+
<parameter><parameterid>scale</parameterid><value>${clip.scale}</value></parameter>
|
|
95
|
+
<parameter><parameterid>rotation</parameterid><value>${clip.rotation}</value></parameter>
|
|
96
|
+
<parameter><parameterid>center</parameterid><value><horiz>${clip.center.h}</horiz><vert>${clip.center.v}</vert></value></parameter>
|
|
97
|
+
<parameter><parameterid>leftcrop</parameterid><value>${clip.crop.left}</value></parameter>
|
|
98
|
+
<parameter><parameterid>topcrop</parameterid><value>${clip.crop.top}</value></parameter>
|
|
99
|
+
<parameter><parameterid>rightcrop</parameterid><value>${clip.crop.right}</value></parameter>
|
|
100
|
+
<parameter><parameterid>bottomcrop</parameterid><value>${clip.crop.bottom}</value></parameter>
|
|
101
|
+
</effect></filter>
|
|
102
|
+
${timeRemap}
|
|
103
|
+
</clipitem>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildXml() {
|
|
107
|
+
const clipitems = CLIPS.map(clipXml).join('\n');
|
|
108
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
109
|
+
<xmeml version="4">
|
|
110
|
+
<sequence id="seq-synth">
|
|
111
|
+
<name>SYNTH_REEL</name>
|
|
112
|
+
<media><video>
|
|
113
|
+
<format><samplecharacteristics>
|
|
114
|
+
<rate><timebase>24</timebase><ntsc>FALSE</ntsc></rate>
|
|
115
|
+
<width>${SEQ_W}</width><height>${SEQ_H}</height>
|
|
116
|
+
</samplecharacteristics></format>
|
|
117
|
+
<track>
|
|
118
|
+
${clipitems}
|
|
119
|
+
</track>
|
|
120
|
+
</video></media>
|
|
121
|
+
</sequence>
|
|
122
|
+
</xmeml>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Frame (PNG) emission ──────────────────────────────────────────────────────
|
|
126
|
+
const FW = 384;
|
|
127
|
+
const FH = 216;
|
|
128
|
+
|
|
129
|
+
function patternBase(x, y) {
|
|
130
|
+
// A structured "scene": gradients + edges + sinusoids. 0..1.
|
|
131
|
+
const g = 0.4 + 0.3 * (x / FW);
|
|
132
|
+
const s = 0.15 * Math.sin(x * 0.10) * Math.cos(y * 0.08);
|
|
133
|
+
const block = x > FW * 0.55 && x < FW * 0.8 && y > FH * 0.3 && y < FH * 0.75 ? 0.2 : 0;
|
|
134
|
+
return Math.max(0, Math.min(1, g + s + block));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function patternOther(x, y) {
|
|
138
|
+
// A clearly different scene (for the WRONG case).
|
|
139
|
+
const r = Math.hypot(x - FW * 0.5, y - FH * 0.5) / (FW * 0.5);
|
|
140
|
+
return Math.max(0, Math.min(1, 0.7 - 0.5 * r + 0.2 * Math.sin(y * 0.2)));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withBurnIn(val, x, y) {
|
|
144
|
+
// Simulated burn-ins: bright top-center TC box + bottom strip.
|
|
145
|
+
const fx = x / FW;
|
|
146
|
+
const fy = y / FH;
|
|
147
|
+
if (fy >= 0.92) return 0.95;
|
|
148
|
+
if (fy >= 0.07 && fy <= 0.17 && fx >= 0.36 && fx <= 0.64) return 0.95;
|
|
149
|
+
return val;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderRaw(fn) {
|
|
153
|
+
const buf = Buffer.alloc(FW * FH);
|
|
154
|
+
for (let y = 0; y < FH; y++) {
|
|
155
|
+
for (let x = 0; x < FW; x++) {
|
|
156
|
+
buf[y * FW + x] = Math.round(fn(x, y) * 255);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return buf;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function writePng(buf, file) {
|
|
163
|
+
await sharp(buf, { raw: { width: FW, height: FH, channels: 1 } }).png().toFile(file);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function generateAll(outDir) {
|
|
167
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
168
|
+
const framesDir = path.join(outDir, 'frames');
|
|
169
|
+
fs.mkdirSync(framesDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
// XML + oracle golden
|
|
172
|
+
fs.writeFileSync(path.join(outDir, 'turnover.synth.xml'), buildXml());
|
|
173
|
+
const oracle = {
|
|
174
|
+
sequence: { name: 'SYNTH_REEL', width: SEQ_W, height: SEQ_H, fps: 24 },
|
|
175
|
+
ticksPerFrame: TPF,
|
|
176
|
+
clipCount: CLIPS.length,
|
|
177
|
+
clips: CLIPS.map((c) => ({
|
|
178
|
+
key: c.key,
|
|
179
|
+
seqstart: c.start,
|
|
180
|
+
xml_in: c.in,
|
|
181
|
+
is_subclip: c.suboff != null,
|
|
182
|
+
subclip_startoffset: c.suboff || 0,
|
|
183
|
+
pproTicksIn: ticksFor(c),
|
|
184
|
+
scale_premiere: c.scale,
|
|
185
|
+
speed: c.speed,
|
|
186
|
+
center: c.center,
|
|
187
|
+
rotation: c.rotation,
|
|
188
|
+
crop: c.crop,
|
|
189
|
+
srcW: SRC_W,
|
|
190
|
+
srcH: SRC_H,
|
|
191
|
+
expected_source_start: expectedSourceStart(c),
|
|
192
|
+
expected_sample_frame: (c.suboff || 0) + (c.slow ? c.in * 2 : c.in),
|
|
193
|
+
expected_scale_corrected: expectedScaleCorrected(c),
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
fs.writeFileSync(path.join(outDir, 'golden_oracle.synth.json'), JSON.stringify(oracle, null, 1));
|
|
197
|
+
|
|
198
|
+
// Frames: match (same), dark-trap (ref darkened), wrong (different)
|
|
199
|
+
const base = renderRaw(patternBase);
|
|
200
|
+
const baseBurn = renderRaw((x, y) => withBurnIn(patternBase(x, y), x, y));
|
|
201
|
+
const baseDarkBurn = renderRaw((x, y) => withBurnIn(patternBase(x, y) * 0.22, x, y));
|
|
202
|
+
const other = renderRaw(patternOther);
|
|
203
|
+
const otherBurn = renderRaw((x, y) => withBurnIn(patternOther(x, y), x, y));
|
|
204
|
+
|
|
205
|
+
await writePng(baseBurn, path.join(framesDir, 'match__reference.png'));
|
|
206
|
+
await writePng(base, path.join(framesDir, 'match__derived.png'));
|
|
207
|
+
await writePng(baseDarkBurn, path.join(framesDir, 'darktrap__reference.png'));
|
|
208
|
+
await writePng(base, path.join(framesDir, 'darktrap__derived.png'));
|
|
209
|
+
await writePng(otherBurn, path.join(framesDir, 'wrong__reference.png'));
|
|
210
|
+
await writePng(base, path.join(framesDir, 'wrong__derived.png'));
|
|
211
|
+
|
|
212
|
+
const compare = {
|
|
213
|
+
note: 'Synthetic, client-free. darktrap reference is darkened 0.22x — brightness-robust metric must still MATCH.',
|
|
214
|
+
cases: [
|
|
215
|
+
{ label: 'match', expected_verdict: 'MATCH', reference: 'frames/match__reference.png', derived: 'frames/match__derived.png' },
|
|
216
|
+
{ label: 'darktrap', expected_verdict: 'MATCH', reference: 'frames/darktrap__reference.png', derived: 'frames/darktrap__derived.png' },
|
|
217
|
+
{ label: 'wrong', expected_verdict: 'WRONG', reference: 'frames/wrong__reference.png', derived: 'frames/wrong__derived.png' },
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
fs.writeFileSync(path.join(outDir, 'golden_compare.synth.json'), JSON.stringify(compare, null, 1));
|
|
221
|
+
|
|
222
|
+
return { outDir, clips: CLIPS.length };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = { generateAll, buildXml, CLIPS, TPF, SEQ_W, SRC_W };
|
|
226
|
+
|
|
227
|
+
if (require.main === module) {
|
|
228
|
+
const out = path.join(__dirname, '..', '__fixtures__', 'synthetic');
|
|
229
|
+
generateAll(out).then((r) => {
|
|
230
|
+
// eslint-disable-next-line no-console
|
|
231
|
+
console.log(`synthetic fixture written to ${r.outDir} (${r.clips} clips + frames)`);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advisory escalation policy tests — client-free. These encode the SAMPLE
|
|
5
|
+
* non-negotiable: vision may raise a flag but can NEVER override the math.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const test = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
|
|
11
|
+
const { shouldConsultVision, reconcile, actionFor } = require('../compare/advisory');
|
|
12
|
+
const { FakeVisionValidator } = require('../adapters/vision-validator');
|
|
13
|
+
|
|
14
|
+
test('advisory: cost gate — borderline / alignment / propose-only are consulted; clear MATCH is not', () => {
|
|
15
|
+
// Clear MATCH (above band): do NOT consult.
|
|
16
|
+
assert.equal(shouldConsultVision({ verdict: 'MATCH', structure: 0.99 }, { mode: 'content-identity' }), false);
|
|
17
|
+
// Clear WRONG (below band): do NOT consult.
|
|
18
|
+
assert.equal(shouldConsultVision({ verdict: 'WRONG', structure: 0.40 }, { mode: 'content-identity' }), false);
|
|
19
|
+
// Borderline: consult.
|
|
20
|
+
assert.equal(shouldConsultVision({ verdict: 'MATCH', structure: 0.90 }, { mode: 'content-identity' }), true);
|
|
21
|
+
// Alignment mode: always consult (metric doesn't apply).
|
|
22
|
+
assert.equal(shouldConsultVision({ verdict: 'MATCH', structure: 0.99 }, { mode: 'alignment' }), true);
|
|
23
|
+
// Stubborn propose-only repair: consult.
|
|
24
|
+
assert.equal(shouldConsultVision({ verdict: 'WRONG', structure: 0.40 }, { proposeOnlyRepair: true }), true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('advisory: corroboration does NOT change the deterministic verdict or force auto-apply', () => {
|
|
28
|
+
const det = { verdict: 'MATCH', structure: 0.90 };
|
|
29
|
+
const adv = { verdict: 'MATCH', confidence: 0.8, advisory: true, provenance: { model: 'fake' } };
|
|
30
|
+
const out = reconcile(det, adv);
|
|
31
|
+
assert.equal(out.verdict, 'MATCH');
|
|
32
|
+
assert.equal(out.finalAction, 'auto-apply-eligible'); // eligibility from the metric, not from vision
|
|
33
|
+
assert.equal(out.reason, 'corroborated');
|
|
34
|
+
assert.equal(out.escalated, false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('advisory: vision can NEVER flip a deterministic WRONG to MATCH — it escalates to human', () => {
|
|
38
|
+
const det = { verdict: 'WRONG', structure: 0.40 };
|
|
39
|
+
const adv = { verdict: 'MATCH', confidence: 0.9, advisory: true, provenance: { model: 'fake' } };
|
|
40
|
+
const out = reconcile(det, adv);
|
|
41
|
+
assert.notEqual(out.verdict, 'MATCH', 'must NOT become MATCH on vision say-so');
|
|
42
|
+
assert.equal(out.verdict, 'REVIEW');
|
|
43
|
+
assert.equal(out.finalAction, 'human-review');
|
|
44
|
+
assert.equal(out.escalated, true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('advisory: vision RAISES a flag on a deterministic MATCH it disputes → human REVIEW', () => {
|
|
48
|
+
const det = { verdict: 'MATCH', structure: 0.91 };
|
|
49
|
+
const adv = { verdict: 'WRONG', confidence: 0.7, advisory: true, provenance: { model: 'fake' } };
|
|
50
|
+
const out = reconcile(det, adv);
|
|
51
|
+
assert.equal(out.verdict, 'REVIEW');
|
|
52
|
+
assert.equal(out.escalated, true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('advisory: with no validator injected, the deterministic verdict stands unchanged', () => {
|
|
56
|
+
const det = { verdict: 'MATCH', structure: 0.99 };
|
|
57
|
+
const out = reconcile(det, null);
|
|
58
|
+
assert.equal(out.verdict, 'MATCH');
|
|
59
|
+
assert.equal(out.advisory, null);
|
|
60
|
+
assert.equal(out.finalAction, 'auto-apply-eligible');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('advisory: reconcile rejects a non-advisory result (cannot smuggle authority)', () => {
|
|
64
|
+
const det = { verdict: 'MATCH', structure: 0.90 };
|
|
65
|
+
assert.throws(() => reconcile(det, { verdict: 'WRONG', advisory: false }), /must be marked advisory/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('advisory: end-to-end with FakeVisionValidator on a borderline dark-grade-style cut', async () => {
|
|
69
|
+
const det = { verdict: 'MATCH', structure: 0.90 }; // borderline
|
|
70
|
+
const fake = new FakeVisionValidator({ dark_grade: { verdict: 'MATCH', confidence: 0.85, rationale: 'same scene' } });
|
|
71
|
+
// Only consult because it's borderline.
|
|
72
|
+
assert.equal(shouldConsultVision(det, { mode: 'content-identity' }), true);
|
|
73
|
+
const adv = await fake.validate({}, {}, { label: 'dark_grade' });
|
|
74
|
+
const out = reconcile(det, adv);
|
|
75
|
+
assert.equal(out.verdict, 'MATCH');
|
|
76
|
+
assert.equal(out.reason, 'corroborated');
|
|
77
|
+
assert.equal(fake.calls.length, 1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('advisory: actionFor maps verdicts to the §15 policy', () => {
|
|
81
|
+
assert.equal(actionFor('MATCH'), 'auto-apply-eligible');
|
|
82
|
+
assert.equal(actionFor('OFFSET'), 'propose');
|
|
83
|
+
assert.equal(actionFor('WRONG'), 'flag-v2');
|
|
84
|
+
assert.equal(actionFor('REVIEW'), 'human-review');
|
|
85
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** CLI entry tests — runCli programmatically + a real subprocess invocation. */
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { execFileSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const { runCli } = require('../cli');
|
|
11
|
+
|
|
12
|
+
const SYNTH_MODEL = path.join(__dirname, '..', '__fixtures__', 'synthetic', 'golden_oracle.synth.json');
|
|
13
|
+
const CLI = path.join(__dirname, '..', 'cli.js');
|
|
14
|
+
|
|
15
|
+
test('cli: verify on the synthetic model returns a Tier-C report (exit 0)', async () => {
|
|
16
|
+
const r = await runCli(['verify', SYNTH_MODEL]);
|
|
17
|
+
assert.equal(r.code, 0);
|
|
18
|
+
assert.equal(r.report.summary.mathVerified, r.report.perCut.length);
|
|
19
|
+
assert.match(r.output, /conform-qc verify/);
|
|
20
|
+
assert.match(r.output, /math-verified=/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('cli: bad usage and unreadable model exit non-zero', async () => {
|
|
24
|
+
assert.equal((await runCli([])).code, 2);
|
|
25
|
+
assert.equal((await runCli(['verify', '/no/such/model.json'])).code, 1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('cli: --json emits the per-cut report', async () => {
|
|
29
|
+
const r = await runCli(['verify', SYNTH_MODEL, '--json']);
|
|
30
|
+
const parsed = JSON.parse(r.output);
|
|
31
|
+
assert.ok(parsed.summary && Array.isArray(parsed.perCut));
|
|
32
|
+
assert.equal(parsed.perCut.length, 4);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('cli: runs as a real subprocess and exits 0', () => {
|
|
36
|
+
const out = execFileSync('node', [CLI, 'verify', SYNTH_MODEL], { encoding: 'utf8' });
|
|
37
|
+
assert.match(out, /conform-qc verify/);
|
|
38
|
+
assert.match(out, /cuts: 4/);
|
|
39
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Comparator tests — verified END-TO-END against the 4 committed golden verdicts
|
|
5
|
+
* (golden_compare.json) and their reference/derived PNG pairs.
|
|
6
|
+
*
|
|
7
|
+
* The PNG pairs live under frames/ which is git-ignored raw material, so the
|
|
8
|
+
* frame-dependent tests SKIP-IF-ABSENT. golden_compare.json itself is committed.
|
|
9
|
+
* The dark_grade_match case is THE TRAP: raw PSNR ~9 but it MUST classify MATCH.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const test = require('node:test');
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
const pkg = require('../index');
|
|
18
|
+
const compare = require('../compare');
|
|
19
|
+
|
|
20
|
+
const DIR = pkg.reelFixtureDir();
|
|
21
|
+
const FRAMES = path.join(DIR, 'frames');
|
|
22
|
+
const HAVE_FRAMES = fs.existsSync(FRAMES) && fs.readdirSync(FRAMES).some((f) => f.endsWith('.png'));
|
|
23
|
+
const SKIP = HAVE_FRAMES ? false : 'frames/ absent (git-ignored raw material) — skipping';
|
|
24
|
+
|
|
25
|
+
const COMPARE = JSON.parse(fs.readFileSync(path.join(DIR, 'golden_compare.json'), 'utf8'));
|
|
26
|
+
|
|
27
|
+
test('comparator: alignment mode is a distinct entry point (stub), content-identity is default', async () => {
|
|
28
|
+
// The alignment stub throws clearly (full impl is P4.5).
|
|
29
|
+
assert.throws(() => compare.alignmentVerify(), /alignment verification mode not implemented/);
|
|
30
|
+
// compareFrames routes by mode without needing to decode for the stub path.
|
|
31
|
+
await assert.rejects(
|
|
32
|
+
() => compare.compareFrames('ref', 'der', { mode: 'alignment' }),
|
|
33
|
+
/alignment verification mode not implemented/,
|
|
34
|
+
);
|
|
35
|
+
// An unknown mode errors clearly.
|
|
36
|
+
await assert.rejects(() => compare.compareFrames('ref', 'der', { mode: 'bogus' }), /unknown mode "bogus"/);
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.log('[compare] alignment mode: distinct stub entry point, content-identity is default');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('comparator: cross-correlation reports a correctable OFFSET, not WRONG (synthetic, client-free)', () => {
|
|
42
|
+
// A structured pattern with high-frequency detail so a shift decorrelates at
|
|
43
|
+
// zero offset but realigns at the true shift. Deterministic (no RNG).
|
|
44
|
+
const W = 96;
|
|
45
|
+
const H = 64;
|
|
46
|
+
const a = new Float64Array(W * H);
|
|
47
|
+
for (let y = 0; y < H; y++) {
|
|
48
|
+
for (let x = 0; x < W; x++) {
|
|
49
|
+
a[y * W + x] = 0.5 + 0.25 * Math.sin(x * 0.7) * Math.cos(y * 0.5) + 0.2 * Math.sin((x + y) * 1.3);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Displace content by (sx,sy); the correction offset is the inverse (-sx,-sy).
|
|
53
|
+
const sx = 5;
|
|
54
|
+
const sy = -3;
|
|
55
|
+
const b = new Float64Array(W * H);
|
|
56
|
+
for (let y = 0; y < H; y++) {
|
|
57
|
+
for (let x = 0; x < W; x++) {
|
|
58
|
+
const srcx = x - sx;
|
|
59
|
+
const srcy = y - sy;
|
|
60
|
+
b[y * W + x] = srcx >= 0 && srcx < W && srcy >= 0 && srcy < H ? a[srcy * W + srcx] : 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const out = compare.classify(a, b, { width: W, height: H });
|
|
64
|
+
assert.equal(out.verdict, 'OFFSET', `expected OFFSET, got ${out.verdict} (zero-shift structure ${out.structure?.toFixed(3)})`);
|
|
65
|
+
assert.equal(out.offset.dx, -sx, `dx ${out.offset.dx} should be ${-sx}`);
|
|
66
|
+
assert.equal(out.offset.dy, -sy, `dy ${out.offset.dy} should be ${-sy}`);
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log(`[compare] cross-corr offset: applied (${sx},${sy}) -> correction (${out.offset.dx},${out.offset.dy}), verdict OFFSET`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('comparator: classifies all 4 golden verdicts (dark-grade trap = MATCH)', { skip: SKIP }, async () => {
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const c of COMPARE.cases) {
|
|
74
|
+
const refPath = path.join(DIR, c.reference);
|
|
75
|
+
const derPath = path.join(DIR, c.derived);
|
|
76
|
+
const out = await compare.compareFrames(refPath, derPath);
|
|
77
|
+
results.push({ label: c.label, got: out.verdict, want: c.expected_verdict, structure: out.structure });
|
|
78
|
+
assert.equal(
|
|
79
|
+
out.verdict,
|
|
80
|
+
c.expected_verdict,
|
|
81
|
+
`${c.label}: got ${out.verdict} (structure ${out.structure?.toFixed(4)}), want ${c.expected_verdict}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
// Explicit trap assertion — the dark grade must MATCH despite the brightness gap.
|
|
85
|
+
const dark = results.find((r) => r.label === 'dark_grade_match');
|
|
86
|
+
assert.equal(dark.got, 'MATCH', 'dark_grade_match MUST classify MATCH (brightness trap)');
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.log('[compare] 4/4 golden verdicts: ' + results.map((r) => `${r.label}=${r.got}`).join(', '));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('comparator: brightness-robust — structure MATCHes the dark grade where full SSIM fails', { skip: SKIP }, async () => {
|
|
92
|
+
const c = COMPARE.cases.find((x) => x.label === 'dark_grade_match');
|
|
93
|
+
const size = compare.DEFAULT_SIZE;
|
|
94
|
+
const ref = await compare.decodeGrayNormalized(path.join(DIR, c.reference), size);
|
|
95
|
+
const der = await compare.decodeGrayNormalized(path.join(DIR, c.derived), size);
|
|
96
|
+
const mask = compare.buildBurnInMask(size.width, size.height);
|
|
97
|
+
const structure = compare.ssimStructure(ref.data, der.data, mask);
|
|
98
|
+
const full = compare.ssimFull(ref.data, der.data, mask);
|
|
99
|
+
// The brightness-robust structure score accepts the dark grade...
|
|
100
|
+
assert.ok(structure >= 0.9, `structure ${structure.toFixed(4)} should be >= 0.90`);
|
|
101
|
+
// ...while naive full SSIM (with the luminance term) FALSE-REJECTS it — the trap.
|
|
102
|
+
assert.ok(full < 0.9, `full SSIM ${full.toFixed(4)} should be < 0.90 (proves the trap)`);
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.log(`[compare] brightness trap: structure=${structure.toFixed(3)} (MATCH) vs full SSIM=${full.toFixed(3)} (would false-flag)`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('comparator: burn-in masking is applied and configurable, and raises the match score', { skip: SKIP }, async () => {
|
|
108
|
+
const c = COMPARE.cases.find((x) => x.label === 'clean_match_hermes');
|
|
109
|
+
const size = compare.DEFAULT_SIZE;
|
|
110
|
+
const ref = await compare.decodeGrayNormalized(path.join(DIR, c.reference), size);
|
|
111
|
+
const der = await compare.decodeGrayNormalized(path.join(DIR, c.derived), size);
|
|
112
|
+
const noMask = new Uint8Array(size.width * size.height);
|
|
113
|
+
const burnMask = compare.buildBurnInMask(size.width, size.height);
|
|
114
|
+
// The default mask covers some pixels (the burn-in regions exist).
|
|
115
|
+
let masked = 0;
|
|
116
|
+
for (let i = 0; i < burnMask.length; i++) masked += burnMask[i];
|
|
117
|
+
assert.ok(masked > 0, 'default burn-in mask must cover the TC/filename regions');
|
|
118
|
+
const sMasked = compare.ssimStructure(ref.data, der.data, burnMask);
|
|
119
|
+
const sUnmasked = compare.ssimStructure(ref.data, der.data, noMask);
|
|
120
|
+
// Masking the burn-in (present in ref, absent in derived) improves the score.
|
|
121
|
+
assert.ok(sMasked > sUnmasked, `masked ${sMasked.toFixed(4)} should beat unmasked ${sUnmasked.toFixed(4)}`);
|
|
122
|
+
// Regions are configurable: a different region set changes the masked count.
|
|
123
|
+
const custom = compare.buildBurnInMask(size.width, size.height, [{ x0: 0, y0: 0, x1: 1, y1: 0.1 }]);
|
|
124
|
+
let customCount = 0;
|
|
125
|
+
for (let i = 0; i < custom.length; i++) customCount += custom[i];
|
|
126
|
+
assert.notEqual(customCount, masked, 'custom regions should change the mask coverage');
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
console.log(`[compare] burn-in mask: ${masked} px masked; masked struct ${sMasked.toFixed(4)} > unmasked ${sUnmasked.toFixed(4)}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('comparator: SSIM>=0.90 / PSNR>=25 thresholds separate clean from wrong', { skip: SKIP }, async () => {
|
|
132
|
+
const size = compare.DEFAULT_SIZE;
|
|
133
|
+
const mask = compare.buildBurnInMask(size.width, size.height);
|
|
134
|
+
const t = compare.DEFAULT_THRESHOLDS;
|
|
135
|
+
assert.equal(t.structure, 0.9);
|
|
136
|
+
assert.equal(t.psnrNorm, 25);
|
|
137
|
+
const score = async (label) => {
|
|
138
|
+
const c = COMPARE.cases.find((x) => x.label === label);
|
|
139
|
+
const ref = await compare.decodeGrayNormalized(path.join(DIR, c.reference), size);
|
|
140
|
+
const der = await compare.decodeGrayNormalized(path.join(DIR, c.derived), size);
|
|
141
|
+
return {
|
|
142
|
+
structure: compare.ssimStructure(ref.data, der.data, mask),
|
|
143
|
+
psnr: compare.psnrNormalized(ref.data, der.data, mask),
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const clean = await score('clean_match_hermes');
|
|
147
|
+
const wrong = await score('genuine_wrong');
|
|
148
|
+
assert.ok(clean.structure >= t.structure && clean.psnr >= t.psnrNorm, `clean above thresholds: ${JSON.stringify(clean)}`);
|
|
149
|
+
assert.ok(wrong.structure < t.structure && wrong.psnr < t.psnrNorm, `wrong below thresholds: ${JSON.stringify(wrong)}`);
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
console.log(`[compare] thresholds: clean struct ${clean.structure.toFixed(3)}/psnr ${clean.psnr.toFixed(1)} vs wrong ${wrong.structure.toFixed(3)}/${wrong.psnr.toFixed(1)}`);
|
|
152
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const { diffConform } = require('../ops/conform-diff');
|
|
7
|
+
|
|
8
|
+
function truthClip(seqstart, zoom, sourceFrame, dur, { retimed = false, conf = 'high', mismatch = false } = {}) {
|
|
9
|
+
return {
|
|
10
|
+
seqstart, seqend: seqstart + dur,
|
|
11
|
+
timing: { duration: dur }, sourceFrame,
|
|
12
|
+
source: { basename: 'x.mov' },
|
|
13
|
+
retime: { retimed },
|
|
14
|
+
transform: { zoomX: zoom, zoomY: zoom },
|
|
15
|
+
flags: { scaleConfidence: conf, aspectMismatch: mismatch },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TRUTH = {
|
|
20
|
+
clips: [
|
|
21
|
+
truthClip(0, 1.06282, 100, 96),
|
|
22
|
+
truthClip(96, 1.11856, 200, 48),
|
|
23
|
+
truthClip(200, 1.06283, 300, 60, { conf: 'low', mismatch: true }), // LoCon
|
|
24
|
+
truthClip(300, 1.0, 400, 24), // generator-ish, dropped on import
|
|
25
|
+
],
|
|
26
|
+
transitions: [{}, {}],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
test('diffConform: clean clips pass; LoCon scaling mismatch + dropped clip surface', () => {
|
|
30
|
+
const db = {
|
|
31
|
+
startOffset: 86400,
|
|
32
|
+
clips: [
|
|
33
|
+
{ start: 86400, duration: 96, mediaStart: 100, name: 'x.mov', zoomX: 1.06282, retimePresent: false },
|
|
34
|
+
{ start: 86496, duration: 48, mediaStart: 200, name: 'x.mov', zoomX: 1.11856, retimePresent: false },
|
|
35
|
+
{ start: 86600, duration: 60, mediaStart: 300, name: 'x.mov', zoomX: 1.26671, retimePresent: false }, // wrong scale in DB
|
|
36
|
+
// seq300 clip absent (dropped on import)
|
|
37
|
+
],
|
|
38
|
+
transitions: [{}, {}],
|
|
39
|
+
};
|
|
40
|
+
const r = diffConform(TRUTH, db);
|
|
41
|
+
assert.equal(r.offset, 86400);
|
|
42
|
+
assert.equal(r.summary.matched, 3);
|
|
43
|
+
assert.equal(r.summary.clean, 2);
|
|
44
|
+
assert.equal(r.summary.withIssues, 1);
|
|
45
|
+
assert.equal(r.summary.missing, 1); // the seq300 clip
|
|
46
|
+
assert.equal(r.summary.byAttr.scaling, 1);
|
|
47
|
+
const loconIssue = r.issues[0].issues.find((i) => i.attr === 'scaling');
|
|
48
|
+
assert.equal(loconIssue.confidence, 'low');
|
|
49
|
+
assert.equal(loconIssue.aspectMismatch, true);
|
|
50
|
+
assert.equal(loconIssue.db, 1.26671);
|
|
51
|
+
assert.equal(r.summary.transitions.match, true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('diffConform: catches dropped retime + extra DB clip + transition count mismatch', () => {
|
|
55
|
+
const db = {
|
|
56
|
+
startOffset: 0,
|
|
57
|
+
clips: [
|
|
58
|
+
{ start: 0, duration: 96, mediaStart: 100, name: 'x.mov', zoomX: 1.06282, retimePresent: false },
|
|
59
|
+
{ start: 96, duration: 48, mediaStart: 200, name: 'x.mov', zoomX: 1.11856, retimePresent: false },
|
|
60
|
+
{ start: 200, duration: 60, mediaStart: 300, name: 'x.mov', zoomX: 1.06283, retimePresent: false },
|
|
61
|
+
{ start: 999, duration: 10, mediaStart: 5, name: 'extra.mov', zoomX: 1, retimePresent: false }, // extra
|
|
62
|
+
],
|
|
63
|
+
transitions: [{}], // only 1 vs truth 2
|
|
64
|
+
};
|
|
65
|
+
const t2 = { ...TRUTH, clips: [truthClip(0, 1.06282, 100, 96), truthClip(96, 1.11856, 200, 48, { retimed: true }), truthClip(200, 1.06283, 300, 60, { conf: 'low', mismatch: true })] };
|
|
66
|
+
const r = diffConform(t2, db);
|
|
67
|
+
assert.equal(r.summary.byAttr.retime, 1); // clip2 truth retimed but DB has none
|
|
68
|
+
assert.equal(r.summary.extra, 1);
|
|
69
|
+
assert.equal(r.summary.transitions.match, false);
|
|
70
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const { emulateClip, fitFactor, MODE } = require('../oracle/emulate');
|
|
7
|
+
|
|
8
|
+
const TPF = 254016000000 / 24;
|
|
9
|
+
const CTX = { ticksPerFrame: TPF, seqW: 3600, seqH: 2160, mode: MODE.FIT };
|
|
10
|
+
// an un-retimed clip at scale S with proxy dims; ticks consistent with in
|
|
11
|
+
const clip = (S, pw, ph, inF = 100) => ({
|
|
12
|
+
seqstart: 0, seqend: 96, xml_in: inF, pproTicksIn: inF * TPF, is_subclip: false,
|
|
13
|
+
scale_premiere: S, srcW: pw, srcH: ph, source_basename: 'x.mov', speed: 100,
|
|
14
|
+
});
|
|
15
|
+
const HR_1568 = { ok: true, width: 4096, height: 2612, codec: 'prores', bitDepth: 12 };
|
|
16
|
+
|
|
17
|
+
test('emulate: same-aspect clip -> proxy-aspect zoom 1.06282, high confidence', () => {
|
|
18
|
+
const t = emulateClip(clip(175.781, 2048, 1306), CTX, HR_1568);
|
|
19
|
+
assert.equal(t.transform.zoomX, 1.06282); // 175.781*1306/2160
|
|
20
|
+
assert.equal(t.transform.basis, 'proxy');
|
|
21
|
+
assert.equal(t.flags.aspectMismatch, false);
|
|
22
|
+
assert.equal(t.flags.scaleConfidence, 'high');
|
|
23
|
+
assert.equal(t.sourceFrame, 100);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('emulate: proxy/highres ASPECT MISMATCH keeps the proxy zoom but flags for reference review (LoCon)', () => {
|
|
27
|
+
const t = emulateClip(clip(253.343, 1421, 1080), CTX, HR_1568);
|
|
28
|
+
assert.equal(t.flags.aspectMismatch, true);
|
|
29
|
+
assert.equal(t.flags.scaleConfidence, 'review');
|
|
30
|
+
// the proxy is what the editor framed + what the reference renders: 253.343*1080/2160
|
|
31
|
+
assert.ok(Math.abs(t.transform.zoomX - 1.26671) < 0.001, `got ${t.transform.zoomX}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('emulate: zoom is the proxy-aspect correction with or without highres analysis', () => {
|
|
35
|
+
const withHr = emulateClip(clip(180, 2048, 1306), CTX, HR_1568).transform.zoomX;
|
|
36
|
+
const noHr = emulateClip(clip(180, 2048, 1306), CTX, null).transform.zoomX;
|
|
37
|
+
assert.equal(withHr, 1.08833); // 180*1306/2160
|
|
38
|
+
assert.equal(noHr, 1.08833);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('emulate: without highres analysis, basis is proxy and confidence stays high', () => {
|
|
42
|
+
const t = emulateClip(clip(175.781, 2048, 1306), CTX, null);
|
|
43
|
+
assert.equal(t.transform.basis, 'proxy');
|
|
44
|
+
assert.equal(t.flags.scaleConfidence, 'high');
|
|
45
|
+
assert.equal(t.transform.zoomX, 1.06282);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('fitFactor: fit vs fill pick the binding dimension', () => {
|
|
49
|
+
// narrower-than-timeline source: fit binds on height, fill binds on width
|
|
50
|
+
assert.ok(fitFactor(4096, 2612, 3600, 2160, MODE.FIT) < fitFactor(4096, 2612, 3600, 2160, MODE.FILL));
|
|
51
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** FrameSampler interface tests — client-free. */
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
|
|
8
|
+
const { FrameSampler, FakeFrameSampler, isFrameSampler } = require('../adapters/frame-sampler');
|
|
9
|
+
|
|
10
|
+
test('FrameSampler: base interface throws until implemented', async () => {
|
|
11
|
+
const base = new FrameSampler();
|
|
12
|
+
assert.equal(isFrameSampler(base), true);
|
|
13
|
+
await assert.rejects(() => base.sample('media', 0, { width: 4, height: 4 }), /must be implemented/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('FrameSampler: fake conforms — right dims, deterministic, content varies by frame', async () => {
|
|
17
|
+
const s = new FakeFrameSampler();
|
|
18
|
+
assert.equal(isFrameSampler(s), true);
|
|
19
|
+
const size = { width: 8, height: 6 };
|
|
20
|
+
const a = await s.sample('clipA.mov', 100, size);
|
|
21
|
+
assert.equal(a.width, 8);
|
|
22
|
+
assert.equal(a.height, 6);
|
|
23
|
+
assert.equal(a.data.length, 48);
|
|
24
|
+
const aAgain = await s.sample('clipA.mov', 100, size);
|
|
25
|
+
assert.deepEqual(Array.from(a.data), Array.from(aAgain.data), 'same request => same pixels');
|
|
26
|
+
const b = await s.sample('clipA.mov', 101, size);
|
|
27
|
+
assert.notDeepEqual(Array.from(a.data), Array.from(b.data), 'different frame => different pixels');
|
|
28
|
+
// A non-conforming object is rejected by the duck-typed check.
|
|
29
|
+
assert.equal(isFrameSampler({}), false);
|
|
30
|
+
});
|