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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clip exposure/balance LEVELING — compute a per-clip *general* offset (per-channel
|
|
3
|
+
* gain) toward a group hero from measured frame stats, and emit a round-trip-asserted
|
|
4
|
+
* Primary-Balance DRX (the series' node style, not flat CDL).
|
|
5
|
+
*
|
|
6
|
+
* LOCAL: ffmpeg-extracted PNG frames -> sharp pixel means -> arithmetic -> drx codec.
|
|
7
|
+
* No Resolve, no LLM vision. Frame extraction + grade APPLY are the caller's job
|
|
8
|
+
* (Resolve scripting API). This module is the deterministic core.
|
|
9
|
+
*
|
|
10
|
+
* Modes:
|
|
11
|
+
* within_camera_mean (default) — VALID only for one camera's drift over a record
|
|
12
|
+
* (same framing). Matches each clip's per-channel mean to the group hero.
|
|
13
|
+
* scene_aware / skin_match — NOT IMPLEMENTED (throws). overall-mean is wrong across
|
|
14
|
+
* different shots (b-roll) or different framings/cameras (use skin-match there).
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { drxTool } from './tools/drx.mjs';
|
|
19
|
+
import { provenanceLabel, gist } from './node-provenance.mjs';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
function loadSharp() {
|
|
23
|
+
try {
|
|
24
|
+
return require('sharp');
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error("exposure-level needs the optional dep 'sharp' (frame stats). Install: npm i sharp");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const luma = (m) => 0.2126 * m.r + 0.7152 * m.g + 0.0722 * m.b;
|
|
31
|
+
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
|
|
32
|
+
const mean = (a) => a.reduce((x, y) => x + y, 0) / a.length;
|
|
33
|
+
const std = (a) => {
|
|
34
|
+
const m = mean(a);
|
|
35
|
+
return Math.sqrt(mean(a.map((x) => (x - m) ** 2)));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Per-channel mean (0..max) of a PNG. */
|
|
39
|
+
export async function measureMeanRGB(pngPath) {
|
|
40
|
+
const s = await loadSharp()(pngPath).stats();
|
|
41
|
+
return { r: s.channels[0].mean, g: s.channels[1].mean, b: s.channels[2].mean };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Generate a Primary-Balance DRX for a per-channel gain, then decode it and THROW if
|
|
45
|
+
* a non-unity grade came back empty (the silent-empty bug guard). Returns the DRX path. */
|
|
46
|
+
export async function generateAssertedGainDRX(gain, label, outPath) {
|
|
47
|
+
// gain values are raw multipliers -> pin space:'drx' (UI happens to be 1:1 for gain,
|
|
48
|
+
// but the pin keeps this emitter immune to the space default).
|
|
49
|
+
//
|
|
50
|
+
// Resolve renders the RGB gain wheels as COLOR BALANCE only: the per-channel values
|
|
51
|
+
// are luma-renormalized at render time, so a uniform [1.3,1.3,1.3] with master 1 is a
|
|
52
|
+
// NO-OP (live-validated 2026-07-03, DaVinci YRGB/Rec709). The achromatic level must be
|
|
53
|
+
// carried by gain.master. Emit master = luma-weighted common gain; keep the channels
|
|
54
|
+
// absolute — Resolve's own renormalization then yields exactly the intended per-channel
|
|
55
|
+
// multipliers (master × ch/luma(ch) == ch).
|
|
56
|
+
const master = 0.2126 * gain.r + 0.7152 * gain.g + 0.0722 * gain.b;
|
|
57
|
+
await drxTool.handler({ action: 'generate', args: { gradeParams: { space: 'drx', gain: [gain.r, gain.g, gain.b, master] }, metadata: { label }, outputPath: outPath } });
|
|
58
|
+
const back = await drxTool.handler({ action: 'parse', args: { content: fs.readFileSync(outPath, 'utf8') } });
|
|
59
|
+
const got = (back.nodes || []).flatMap((n) => (n.correctors || []).flatMap((c) => c.parameters || [])).filter((p) => /gain\.(r|g|b)/i.test(p.name)).length;
|
|
60
|
+
const nonUnity = [gain.r, gain.g, gain.b].some((v) => Math.abs(v - 1) > 0.002);
|
|
61
|
+
if (nonUnity && got === 0) throw new Error(`round-trip assert FAILED: ${label} generated an EMPTY grade (gain ${JSON.stringify(gain)})`);
|
|
62
|
+
return outPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Array<{id:string|number, png:string, group:string}>} clips
|
|
67
|
+
* @param {{outDir:string, mode?:string, clampGain?:[number,number], maxCorrWarn?:number}} opts
|
|
68
|
+
* @returns {Promise<{grades:Array, report:Object, warnings:string[]}>}
|
|
69
|
+
*/
|
|
70
|
+
export async function computeLevels(clips, opts = {}) {
|
|
71
|
+
const mode = opts.mode || 'within_camera_mean';
|
|
72
|
+
if (mode !== 'within_camera_mean') {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`mode '${mode}' not implemented. within_camera_mean only (overall-mean is invalid across different shots/cameras — use skin-match/scene-cluster, TODO).`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const [lo, hi] = opts.clampGain || [0.5, 2];
|
|
78
|
+
const maxCorrWarn = opts.maxCorrWarn ?? 0.12; // flag >12% as likely over-correction
|
|
79
|
+
if (!opts.outDir) throw new Error('opts.outDir required');
|
|
80
|
+
fs.mkdirSync(opts.outDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const byGroup = new Map();
|
|
83
|
+
for (const c of clips) {
|
|
84
|
+
if (!byGroup.has(c.group)) byGroup.set(c.group, []);
|
|
85
|
+
byGroup.get(c.group).push(c);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const grades = [];
|
|
89
|
+
const report = {};
|
|
90
|
+
const warnings = [];
|
|
91
|
+
for (const [group, gclips] of byGroup) {
|
|
92
|
+
for (const c of gclips) {
|
|
93
|
+
try {
|
|
94
|
+
c.mean = await measureMeanRGB(c.png);
|
|
95
|
+
c.luma = luma(c.mean);
|
|
96
|
+
} catch {
|
|
97
|
+
c.mean = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const valid = gclips.filter((c) => c.mean);
|
|
101
|
+
if (!valid.length) {
|
|
102
|
+
report[group] = { error: 'no measurable frames' };
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const H = valid[0].mean; // hero = first clip in group order
|
|
106
|
+
const lr = valid.map((c) => c.luma);
|
|
107
|
+
let maxCorr = 0;
|
|
108
|
+
for (const c of valid) {
|
|
109
|
+
const gain = { r: clamp(H.r / c.mean.r, lo, hi), g: clamp(H.g / c.mean.g, lo, hi), b: clamp(H.b / c.mean.b, lo, hi) };
|
|
110
|
+
const corr = Math.max(...['r', 'g', 'b'].map((k) => Math.abs(gain[k] - 1)));
|
|
111
|
+
maxCorr = Math.max(maxCorr, corr);
|
|
112
|
+
const drxPath = `${opts.outDir}/${group}_${c.id}.drx`;
|
|
113
|
+
await generateAssertedGainDRX(gain, provenanceLabel('exposure_level', { source: `hero:${valid[0].id}`, gist: gist('gain', gain) }), drxPath);
|
|
114
|
+
grades.push({ id: c.id, group, gain, correctionPct: +(100 * corr).toFixed(2), drxPath, isHero: c === valid[0] });
|
|
115
|
+
}
|
|
116
|
+
report[group] = {
|
|
117
|
+
clips: valid.length,
|
|
118
|
+
hero_id: valid[0].id,
|
|
119
|
+
luma_pct_std: +((100 * std(lr)) / mean(lr)).toFixed(2),
|
|
120
|
+
luma_pct_range: +((100 * (Math.max(...lr) - Math.min(...lr))) / mean(lr)).toFixed(2),
|
|
121
|
+
max_correction_pct: +(100 * maxCorr).toFixed(2),
|
|
122
|
+
};
|
|
123
|
+
if (maxCorr > maxCorrWarn)
|
|
124
|
+
warnings.push(
|
|
125
|
+
`${group}: max correction ${(100 * maxCorr).toFixed(1)}% > ${(100 * maxCorrWarn).toFixed(0)}% — likely NOT one-camera drift (different shots?). Review before applying.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return { grades, report, warnings };
|
|
129
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract_frames (Phase-1a) — the missing front door. ffmpeg-extract DISPLAY-REFERRED
|
|
3
|
+
* frames at oracle/midpoint/TC positions so the whole grading catalog is usable standalone
|
|
4
|
+
* (today extraction is "the caller's job").
|
|
5
|
+
*
|
|
6
|
+
* THE CRUX is the working space (cross-craft review, hard rule): the matchers' skin/tone
|
|
7
|
+
* gates assume Rec.709/sRGB. SLog3 measured raw is garbage, and a caller-supplied-CDL
|
|
8
|
+
* SLog3→709 approximation is DANGEROUS (you'd match the approximation's artifacts). So this
|
|
9
|
+
* tool HARD-REFUSES a log/scene-referred source — it never approximates. Accepted:
|
|
10
|
+
* - a source ffprobe reports as a known DISPLAY transfer (bt709/601/sRGB/HLG/PQ), or
|
|
11
|
+
* - an explicit display-referred proxy/render path (opts.proxy / clip.proxy), or
|
|
12
|
+
* - an explicit caller assertion `displayReferred:true` when ffprobe can't tell (unknown).
|
|
13
|
+
* A known LOG transfer, or 'log' anywhere in the stream tags → THROW. Unknown + no assertion
|
|
14
|
+
* → THROW (skip-not-fake). Resolve against real camera-log footage with a post-ODT render.
|
|
15
|
+
*
|
|
16
|
+
* Silent-lie guard: assert the extracted PNG has bytes>0 (an empty file is the empty-green lie).
|
|
17
|
+
* Deps: ffmpeg + ffprobe on PATH (peer dep; not bundled — GPL). No Resolve, no LLM.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { requireFfmpeg } from './capabilities.mjs';
|
|
23
|
+
|
|
24
|
+
// color_transfer values that are DISPLAY-referred (SDR + HDR display). Camera log
|
|
25
|
+
// (SLog3/LogC/RedLog) is NOT in ffmpeg's enum — it shows as 'unknown' → we refuse unless asserted.
|
|
26
|
+
const DISPLAY_TRANSFERS = new Set([
|
|
27
|
+
'bt709',
|
|
28
|
+
'smpte170m',
|
|
29
|
+
'bt470m',
|
|
30
|
+
'bt470bg',
|
|
31
|
+
'gamma22',
|
|
32
|
+
'gamma28',
|
|
33
|
+
'iec61966-2-1', // sRGB
|
|
34
|
+
'srgb',
|
|
35
|
+
'bt2020-10',
|
|
36
|
+
'bt2020-12',
|
|
37
|
+
'smpte2084', // PQ (HDR display-referred)
|
|
38
|
+
'arib-std-b67', // HLG (HDR display-referred)
|
|
39
|
+
]);
|
|
40
|
+
// Explicitly scene-referred / camera-log transfers → hard refuse.
|
|
41
|
+
const LOG_TRANSFERS = new Set(['log100', 'log316']);
|
|
42
|
+
// Known DISPLAY matrices — a fallback display signal when the transfer tag was dropped by the
|
|
43
|
+
// muxer (common: libx264/mp4 keeps color_space but not color_transfer). A Rec.709/601 matrix
|
|
44
|
+
// strongly implies display-referred SDR. NOT bt2020nc (wide-gamut; ambiguous) — stay conservative.
|
|
45
|
+
const DISPLAY_MATRICES = new Set(['bt709', 'smpte170m', 'bt470bg', 'fcc', 'smpte240m']);
|
|
46
|
+
|
|
47
|
+
/** Probe a media file's video stream: colorspace tags + frame count + fps. */
|
|
48
|
+
export function probeStream(source, { ffprobe = 'ffprobe' } = {}) {
|
|
49
|
+
const r = spawnSync(
|
|
50
|
+
ffprobe,
|
|
51
|
+
[
|
|
52
|
+
'-v',
|
|
53
|
+
'error',
|
|
54
|
+
'-select_streams',
|
|
55
|
+
'v:0',
|
|
56
|
+
'-show_entries',
|
|
57
|
+
'stream=color_transfer,color_space,color_primaries,nb_frames,r_frame_rate,duration:stream_tags=color_transfer',
|
|
58
|
+
'-of',
|
|
59
|
+
'json',
|
|
60
|
+
source,
|
|
61
|
+
],
|
|
62
|
+
{ encoding: 'utf8', timeout: 15000 },
|
|
63
|
+
);
|
|
64
|
+
if (r.status !== 0) throw new Error(`ffprobe failed on ${source}: ${(r.stderr || '').slice(-300)}`);
|
|
65
|
+
const j = JSON.parse(r.stdout || '{}');
|
|
66
|
+
const s = (j.streams && j.streams[0]) || {};
|
|
67
|
+
const [num, den] = String(s.r_frame_rate || '0/1')
|
|
68
|
+
.split('/')
|
|
69
|
+
.map(Number);
|
|
70
|
+
const fps = den ? num / den : 0;
|
|
71
|
+
let frameCount = Number(s.nb_frames) || 0;
|
|
72
|
+
if (!frameCount && s.duration && fps) frameCount = Math.round(Number(s.duration) * fps);
|
|
73
|
+
return {
|
|
74
|
+
colorTransfer: s.color_transfer || (s.tags && s.tags.color_transfer) || 'unknown',
|
|
75
|
+
colorSpace: s.color_space || 'unknown',
|
|
76
|
+
colorPrimaries: s.color_primaries || 'unknown',
|
|
77
|
+
fps,
|
|
78
|
+
frameCount,
|
|
79
|
+
duration: Number(s.duration) || 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Decide if a source is safe (display-referred) to measure, or must be refused.
|
|
85
|
+
* @returns {{ok:true, transfer:string, reason:string} | {ok:false, transfer:string, reason:string}}
|
|
86
|
+
*/
|
|
87
|
+
export function assessColorspace(probe, opts = {}) {
|
|
88
|
+
const t = String(probe.colorTransfer || 'unknown').toLowerCase();
|
|
89
|
+
if (t.includes('log') || LOG_TRANSFERS.has(t)) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
transfer: t,
|
|
93
|
+
reason: `source transfer '${t}' is LOG/scene-referred — refuse (no approximation; use a post-ODT display-referred render/proxy)`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (DISPLAY_TRANSFERS.has(t)) return { ok: true, transfer: t, reason: `display-referred transfer '${t}'` };
|
|
97
|
+
// Transfer tag missing/unknown but a known display MATRIX survived → treat as display-referred SDR.
|
|
98
|
+
const cs = String(probe.colorSpace || 'unknown').toLowerCase();
|
|
99
|
+
if (DISPLAY_MATRICES.has(cs))
|
|
100
|
+
return { ok: true, transfer: t, matrix: cs, reason: `transfer '${t}' unknown but display matrix '${cs}' → display-referred SDR` };
|
|
101
|
+
// Unknown: camera log commonly probes as 'unknown'. Only proceed if the caller asserts.
|
|
102
|
+
if (opts.displayReferred) return { ok: true, transfer: t, reason: `transfer '${t}' unknown; caller asserts displayReferred` };
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
transfer: t,
|
|
106
|
+
reason: `source transfer '${t}' is UNKNOWN — camera log often probes as unknown. Point at a display-referred proxy/render, or pass displayReferred:true if you KNOW it's Rec.709/sRGB.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Resolve an `at` position to a 0-based frame index. */
|
|
111
|
+
export function resolvePosition(at, probe) {
|
|
112
|
+
if (typeof at === 'number') return Math.max(0, Math.round(at));
|
|
113
|
+
if (at == null || at === 'midpoint') return Math.max(0, Math.floor((probe.frameCount || 1) / 2));
|
|
114
|
+
if (typeof at === 'string' && at.includes(':')) {
|
|
115
|
+
// TC HH:MM:SS:FF → frame index via fps
|
|
116
|
+
const parts = at.split(':').map(Number);
|
|
117
|
+
if (parts.length === 4 && probe.fps) {
|
|
118
|
+
const [h, m, s, f] = parts;
|
|
119
|
+
return Math.max(0, Math.round((h * 3600 + m * 60 + s) * probe.fps) + f);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`extract_frames: cannot resolve position '${at}'`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function runFfmpegToFile(args, ffmpegPath) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const proc = spawn(ffmpegPath, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
128
|
+
const err = [];
|
|
129
|
+
proc.stderr.on('data', (d) => err.push(d));
|
|
130
|
+
proc.on('error', reject);
|
|
131
|
+
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`ffmpeg exited ${code}: ${Buffer.concat(err).toString().slice(-300)}`))));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {Array<{id:string|number, source:string, at?:any, proxy?:string, displayReferred?:boolean}>} clips
|
|
137
|
+
* @param {{outDir:string, ffmpeg?:string, ffprobe?:string, displayReferred?:boolean}} opts
|
|
138
|
+
* @returns {Promise<{frames:Object, skipped:Array, warnings:string[]}>} frames = { id → png path }
|
|
139
|
+
*/
|
|
140
|
+
export async function extractFrames(clips, opts = {}) {
|
|
141
|
+
requireFfmpeg();
|
|
142
|
+
if (!opts.outDir) throw new Error('opts.outDir required');
|
|
143
|
+
fs.mkdirSync(opts.outDir, { recursive: true });
|
|
144
|
+
const ffmpeg = opts.ffmpeg || 'ffmpeg';
|
|
145
|
+
const ffprobe = opts.ffprobe || 'ffprobe';
|
|
146
|
+
const frames = {};
|
|
147
|
+
const skipped = [];
|
|
148
|
+
const warnings = [];
|
|
149
|
+
|
|
150
|
+
for (const c of clips) {
|
|
151
|
+
const src = c.proxy || opts.proxy || c.source;
|
|
152
|
+
let probe;
|
|
153
|
+
try {
|
|
154
|
+
probe = probeStream(src, { ffprobe });
|
|
155
|
+
} catch (e) {
|
|
156
|
+
skipped.push({ id: c.id, reason: `probe failed: ${e.message}` });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const assess = assessColorspace(probe, { displayReferred: c.displayReferred ?? opts.displayReferred });
|
|
160
|
+
if (!assess.ok) {
|
|
161
|
+
// HARD refuse — never approximate a log source into a display-referred measurement.
|
|
162
|
+
skipped.push({ id: c.id, reason: assess.reason, transfer: assess.transfer });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
let frameIdx;
|
|
166
|
+
try {
|
|
167
|
+
frameIdx = resolvePosition(c.at, probe);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
skipped.push({ id: c.id, reason: e.message });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const outPng = path.join(opts.outDir, `${c.id}.png`);
|
|
173
|
+
const args = ['-v', 'error', '-i', src, '-vf', `select=eq(n\\,${frameIdx})`, '-vframes', '1', '-y', outPng];
|
|
174
|
+
try {
|
|
175
|
+
await runFfmpegToFile(args, ffmpeg);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
skipped.push({ id: c.id, reason: `extract failed: ${e.message}` });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Silent-lie guard: assert bytes were actually written.
|
|
181
|
+
let sz = 0;
|
|
182
|
+
try {
|
|
183
|
+
sz = fs.statSync(outPng).size;
|
|
184
|
+
} catch {
|
|
185
|
+
sz = 0;
|
|
186
|
+
}
|
|
187
|
+
if (!sz) {
|
|
188
|
+
skipped.push({ id: c.id, reason: `extracted 0 bytes (frame ${frameIdx} out of range?)` });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
frames[c.id] = outPng;
|
|
192
|
+
if (assess.transfer === 'smpte2084' || assess.transfer === 'arib-std-b67')
|
|
193
|
+
warnings.push(`${c.id}: HDR transfer '${assess.transfer}' — display-referred but the SDR matchers' 16–235 assumptions differ; verify.`);
|
|
194
|
+
}
|
|
195
|
+
return { frames, skipped, warnings };
|
|
196
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ffprobe-media — one normalized media-probe helper the deliverable/media QC tools share.
|
|
3
|
+
* Wraps `ffprobe -show_streams -show_format` into a compact { video, audio[], format } shape.
|
|
4
|
+
* Deps: ffprobe on PATH (peer; not bundled — GPL). Deterministic read; no Resolve, no LLM.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { requireFfmpeg } from './capabilities.mjs';
|
|
8
|
+
|
|
9
|
+
/** Parse an "a/b" rational string → number (0 on divide-by-zero). */
|
|
10
|
+
export function ratio(s) {
|
|
11
|
+
const [n, d] = String(s || '0/1')
|
|
12
|
+
.split('/')
|
|
13
|
+
.map(Number);
|
|
14
|
+
return d ? n / d : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Probe a media file. Returns null on failure (skip-not-fake at the caller).
|
|
19
|
+
* @param {string} file
|
|
20
|
+
* @param {{ffprobe?:string}} [opts]
|
|
21
|
+
*/
|
|
22
|
+
export function probeMedia(file, opts = {}) {
|
|
23
|
+
requireFfmpeg();
|
|
24
|
+
const ffprobe = opts.ffprobe || 'ffprobe';
|
|
25
|
+
const r = spawnSync(ffprobe, ['-v', 'error', '-show_streams', '-show_format', '-of', 'json', file], {
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
29
|
+
});
|
|
30
|
+
if (r.status !== 0) return null;
|
|
31
|
+
let j;
|
|
32
|
+
try {
|
|
33
|
+
j = JSON.parse(r.stdout || '{}');
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const streams = j.streams || [];
|
|
38
|
+
const vs = streams.find((s) => s.codec_type === 'video') || null;
|
|
39
|
+
const as = streams.filter((s) => s.codec_type === 'audio');
|
|
40
|
+
const fmt = j.format || {};
|
|
41
|
+
const fps = vs ? ratio(vs.r_frame_rate) : 0;
|
|
42
|
+
let frameCount = vs ? Number(vs.nb_frames) || 0 : 0;
|
|
43
|
+
const duration = Number(fmt.duration) || (vs ? Number(vs.duration) : 0) || 0;
|
|
44
|
+
if (!frameCount && duration && fps) frameCount = Math.round(duration * fps);
|
|
45
|
+
const video = vs
|
|
46
|
+
? {
|
|
47
|
+
codec: vs.codec_name || 'unknown',
|
|
48
|
+
profile: vs.profile != null ? String(vs.profile) : null,
|
|
49
|
+
width: vs.width || 0,
|
|
50
|
+
height: vs.height || 0,
|
|
51
|
+
fps: +fps.toFixed(4),
|
|
52
|
+
fpsExact: vs.r_frame_rate,
|
|
53
|
+
fieldOrder: vs.field_order || 'progressive',
|
|
54
|
+
scan: vs.field_order && vs.field_order !== 'progressive' ? 'interlaced' : 'progressive',
|
|
55
|
+
pixFmt: vs.pix_fmt || 'unknown',
|
|
56
|
+
colorPrimaries: vs.color_primaries || 'unknown',
|
|
57
|
+
colorTransfer: vs.color_transfer || 'unknown',
|
|
58
|
+
colorMatrix: vs.color_space || 'unknown',
|
|
59
|
+
colorRange: vs.color_range || 'unknown',
|
|
60
|
+
bitDepth: vs.bits_per_raw_sample ? Number(vs.bits_per_raw_sample) : null,
|
|
61
|
+
sampleAspect: vs.sample_aspect_ratio || '1:1',
|
|
62
|
+
displayAspect: vs.display_aspect_ratio || null,
|
|
63
|
+
frameCount,
|
|
64
|
+
}
|
|
65
|
+
: null;
|
|
66
|
+
const audio = as.map((a) => ({
|
|
67
|
+
codec: a.codec_name || 'unknown',
|
|
68
|
+
channels: a.channels || 0,
|
|
69
|
+
channelLayout: a.channel_layout || 'unknown',
|
|
70
|
+
sampleRate: Number(a.sample_rate) || 0,
|
|
71
|
+
bitDepth: a.bits_per_raw_sample ? Number(a.bits_per_raw_sample) : a.bits_per_sample || null,
|
|
72
|
+
}));
|
|
73
|
+
// Timecode: format tag, then any stream tag (camera TC usually rides a data/video stream tag).
|
|
74
|
+
const timecode = (fmt.tags && fmt.tags.timecode) || streams.map((s) => s.tags && s.tags.timecode).find(Boolean) || null;
|
|
75
|
+
return {
|
|
76
|
+
video,
|
|
77
|
+
audio,
|
|
78
|
+
format: {
|
|
79
|
+
container: (fmt.format_name || '').split(',')[0] || 'unknown',
|
|
80
|
+
duration: +duration.toFixed(4),
|
|
81
|
+
size: Number(fmt.size) || 0,
|
|
82
|
+
bitRate: Number(fmt.bit_rate) || 0,
|
|
83
|
+
nbStreams: streams.length,
|
|
84
|
+
tags: fmt.tags || {},
|
|
85
|
+
timecode,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcast-legal / clipping QC (C3 `gamut_legal`).
|
|
3
|
+
*
|
|
4
|
+
* Pure MEASUREMENT — no grade emitted. On display-referred frames it reports, per
|
|
5
|
+
* channel, how much of the picture sits outside video-legal range (default 8-bit
|
|
6
|
+
* 16–235) and how much is HARD-CLIPPED (==0 crushed / ==255 blown), then gives each
|
|
7
|
+
* clip a pass/fail against a tolerance. The delivery-QC signal before master render
|
|
8
|
+
* (a common master is Rec.709 Gamma 2.4 — many broadcasters want video-legal).
|
|
9
|
+
*
|
|
10
|
+
* Caveat: superwhite/sub-black detail already clipped INTO the PNG can't be recovered
|
|
11
|
+
* here — this flags what the frame shows. For true scope-legal QC use the timeline
|
|
12
|
+
* scopes; this is the cheap local pre-check. LOCAL & deterministic; no Resolve.
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
function loadSharp() {
|
|
18
|
+
try {
|
|
19
|
+
return require('sharp');
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error("gamut-legal needs the optional dep 'sharp'. Install: npm i sharp");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const pct = (n, d) => (d ? +((100 * n) / d).toFixed(3) : 0);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @returns {Promise<{channels:Object, illegalPct:number, clippedPct:number, nTotal:number}|null>}
|
|
28
|
+
*/
|
|
29
|
+
export async function measureLegal(pngPath, opts = {}) {
|
|
30
|
+
const sharp = loadSharp();
|
|
31
|
+
const low = opts.low ?? 16,
|
|
32
|
+
high = opts.high ?? 235,
|
|
33
|
+
maxSide = opts.maxSide ?? 480;
|
|
34
|
+
let data, ch;
|
|
35
|
+
try {
|
|
36
|
+
const out = await sharp(pngPath).resize(maxSide, maxSide, { fit: 'inside', kernel: 'nearest' }).raw().toBuffer({ resolveWithObject: true });
|
|
37
|
+
data = out.data;
|
|
38
|
+
ch = out.info.channels;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const nTotal = Math.floor(data.length / ch);
|
|
43
|
+
const names = ['r', 'g', 'b'];
|
|
44
|
+
const acc = names.map(() => ({ min: 255, max: 0, below: 0, above: 0, at0: 0, at255: 0 }));
|
|
45
|
+
let illegalPx = 0,
|
|
46
|
+
clippedPx = 0;
|
|
47
|
+
for (let i = 0; i < data.length; i += ch) {
|
|
48
|
+
let illegal = false,
|
|
49
|
+
clipped = false;
|
|
50
|
+
for (let c = 0; c < 3; c++) {
|
|
51
|
+
const v = data[i + c],
|
|
52
|
+
a = acc[c];
|
|
53
|
+
if (v < a.min) a.min = v;
|
|
54
|
+
if (v > a.max) a.max = v;
|
|
55
|
+
if (v < low) {
|
|
56
|
+
a.below++;
|
|
57
|
+
illegal = true;
|
|
58
|
+
} else if (v > high) {
|
|
59
|
+
a.above++;
|
|
60
|
+
illegal = true;
|
|
61
|
+
}
|
|
62
|
+
if (v === 0) {
|
|
63
|
+
a.at0++;
|
|
64
|
+
clipped = true;
|
|
65
|
+
} else if (v === 255) {
|
|
66
|
+
a.at255++;
|
|
67
|
+
clipped = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (illegal) illegalPx++;
|
|
71
|
+
if (clipped) clippedPx++;
|
|
72
|
+
}
|
|
73
|
+
const channels = {};
|
|
74
|
+
names.forEach((n, c) => {
|
|
75
|
+
const a = acc[c];
|
|
76
|
+
channels[n] = {
|
|
77
|
+
min: a.min,
|
|
78
|
+
max: a.max,
|
|
79
|
+
belowPct: pct(a.below, nTotal),
|
|
80
|
+
abovePct: pct(a.above, nTotal),
|
|
81
|
+
crushedPct: pct(a.at0, nTotal),
|
|
82
|
+
blownPct: pct(a.at255, nTotal),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return { channels, illegalPct: pct(illegalPx, nTotal), clippedPct: pct(clippedPx, nTotal), nTotal };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {Array<{id:string|number, png:string}>} clips
|
|
90
|
+
* @param {{low?:number, high?:number, maxIllegalPct?:number}} opts
|
|
91
|
+
* @returns {Promise<{report:Object, warnings:string[], skipped:Array}>}
|
|
92
|
+
*/
|
|
93
|
+
export async function computeGamutLegal(clips, opts = {}) {
|
|
94
|
+
const maxIllegalPct = opts.maxIllegalPct ?? 1.0; // tolerate up to 1% out-of-legal by default
|
|
95
|
+
const report = {};
|
|
96
|
+
const warnings = [];
|
|
97
|
+
const skipped = [];
|
|
98
|
+
for (const c of clips) {
|
|
99
|
+
const m = await measureLegal(c.png, opts);
|
|
100
|
+
if (!m) {
|
|
101
|
+
skipped.push({ id: c.id, reason: 'unreadable frame' });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const pass = m.illegalPct <= maxIllegalPct;
|
|
105
|
+
report[c.id] = { pass, illegalPct: m.illegalPct, clippedPct: m.clippedPct, channels: m.channels };
|
|
106
|
+
if (!pass)
|
|
107
|
+
warnings.push(
|
|
108
|
+
`${c.id}: ${m.illegalPct}% out-of-legal (>${maxIllegalPct}% tol) [low ${opts.low ?? 16}/high ${opts.high ?? 235}] — review before broadcast-legal delivery.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return { report, warnings, skipped };
|
|
112
|
+
}
|