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,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* match_to_reference (P1c) — the headline general-case matcher. Move a target frame toward a
|
|
3
|
+
* REFERENCE image (approved still / client "make it look like this" / hero render) via affine
|
|
4
|
+
* mean-std transfer ("Reinhard-lite"): per channel gain = std_ref/std_tgt, offset = mean_ref −
|
|
5
|
+
* gain·mean_tgt. This is exactly the gain+offset affine the codec already emits (reuse
|
|
6
|
+
* generateAssertedAffineDRX) — no new DRX write path.
|
|
7
|
+
*
|
|
8
|
+
* CRAFT AMENDMENTS (cross-craft review, non-negotiable):
|
|
9
|
+
* - `lumaPreserve` default ON — apply the CHROMA shift but renormalize to the target's own luma
|
|
10
|
+
* (a colour/look match, not an exposure move).
|
|
11
|
+
* - SKIN-region-gated by default when skin is present — and match hue toward the vectorscope
|
|
12
|
+
* skin-LINE, never a single RGB mean (the calibrated-for-white-skin trap).
|
|
13
|
+
* - It is a starting NUDGE a human ratifies (gate: review), a TRIM — never a cross-IDT reconciler.
|
|
14
|
+
*
|
|
15
|
+
* Guards: reference unreadable → skip+warn; clamp gain/offset; over-correction warning. LOCAL &
|
|
16
|
+
* deterministic; no Resolve, no LLM. Frames must be display-referred (like every matcher).
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { generateAssertedAffineDRX } from './contrast-normalize.mjs';
|
|
21
|
+
import { provenanceLabel, gist } from './node-provenance.mjs';
|
|
22
|
+
import { isSkin } from './skin-match.mjs';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
function loadSharp() {
|
|
26
|
+
try {
|
|
27
|
+
return require('sharp');
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error("match-to-reference needs the optional dep 'sharp'. Install: npm i sharp");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
|
|
33
|
+
const lumaW = { r: 0.2126, g: 0.7152, b: 0.0722 };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Per-channel mean+std (0..1) over the whole frame OR skin-gated pixels.
|
|
37
|
+
* @returns {Promise<{mean:{r,g,b}, std:{r,g,b}, skinFrac:number}|null>}
|
|
38
|
+
*/
|
|
39
|
+
export async function measureMeanStd(pngPath, opts = {}) {
|
|
40
|
+
const sharp = loadSharp();
|
|
41
|
+
const maxSide = opts.maxSide ?? 480;
|
|
42
|
+
let data, ch, n;
|
|
43
|
+
try {
|
|
44
|
+
const out = await sharp(pngPath).resize(maxSide, maxSide, { fit: 'inside', kernel: 'nearest' }).raw().toBuffer({ resolveWithObject: true });
|
|
45
|
+
data = out.data;
|
|
46
|
+
ch = out.info.channels;
|
|
47
|
+
n = Math.floor(data.length / ch);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (!n) return null;
|
|
52
|
+
const skinGate = opts.skinGate;
|
|
53
|
+
let sr = 0,
|
|
54
|
+
sg = 0,
|
|
55
|
+
sb = 0,
|
|
56
|
+
srr = 0,
|
|
57
|
+
sgg = 0,
|
|
58
|
+
sbb = 0,
|
|
59
|
+
cnt = 0,
|
|
60
|
+
nSkin = 0;
|
|
61
|
+
for (let i = 0; i < data.length; i += ch) {
|
|
62
|
+
const r = data[i],
|
|
63
|
+
g = data[i + 1],
|
|
64
|
+
b = data[i + 2];
|
|
65
|
+
const skin = isSkin(r, g, b, opts.skinOpts || {});
|
|
66
|
+
if (skin) nSkin++;
|
|
67
|
+
if (skinGate && !skin) continue;
|
|
68
|
+
sr += r;
|
|
69
|
+
sg += g;
|
|
70
|
+
sb += b;
|
|
71
|
+
srr += r * r;
|
|
72
|
+
sgg += g * g;
|
|
73
|
+
sbb += b * b;
|
|
74
|
+
cnt++;
|
|
75
|
+
}
|
|
76
|
+
if (!cnt) return null;
|
|
77
|
+
const mk = (s, ss) => {
|
|
78
|
+
const m = s / cnt;
|
|
79
|
+
return { mean: m / 255, std: Math.sqrt(Math.max(0, ss / cnt - m * m)) / 255 };
|
|
80
|
+
};
|
|
81
|
+
const R = mk(sr, srr),
|
|
82
|
+
G = mk(sg, sgg),
|
|
83
|
+
B = mk(sb, sbb);
|
|
84
|
+
return { mean: { r: R.mean, g: G.mean, b: B.mean }, std: { r: R.std, g: G.std, b: B.std }, skinFrac: nSkin / n };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {Array<{id, png, reference?}>} clips per-clip reference OR shared opts.reference
|
|
89
|
+
* @param {{outDir, reference?, lumaPreserve?, skinGate?, clampGain?, clampOffset?, minSkinFrac?}} opts
|
|
90
|
+
*/
|
|
91
|
+
export async function computeMatchToReference(clips, opts = {}) {
|
|
92
|
+
if (!opts.outDir) throw new Error('opts.outDir required');
|
|
93
|
+
fs.mkdirSync(opts.outDir, { recursive: true });
|
|
94
|
+
const lumaPreserve = opts.lumaPreserve !== false; // default ON
|
|
95
|
+
const [glo, ghi] = opts.clampGain || [0.5, 2];
|
|
96
|
+
const [olo, ohi] = opts.clampOffset || [-0.5, 0.5];
|
|
97
|
+
const minSkinFrac = opts.minSkinFrac ?? 0.02;
|
|
98
|
+
|
|
99
|
+
const grades = [];
|
|
100
|
+
const warnings = [];
|
|
101
|
+
const skipped = [];
|
|
102
|
+
const report = {};
|
|
103
|
+
|
|
104
|
+
// Shared reference stats (measured once) — skin-gated when the reference itself has skin.
|
|
105
|
+
const refCache = new Map();
|
|
106
|
+
async function refStats(refPath) {
|
|
107
|
+
if (refCache.has(refPath)) return refCache.get(refPath);
|
|
108
|
+
// Probe skin fraction with a whole-frame pass, then decide gating.
|
|
109
|
+
const whole = await measureMeanStd(refPath, { skinGate: false });
|
|
110
|
+
let stats = whole;
|
|
111
|
+
let gated = false;
|
|
112
|
+
if (whole && opts.skinGate !== false && whole.skinFrac >= minSkinFrac) {
|
|
113
|
+
const skin = await measureMeanStd(refPath, { skinGate: true });
|
|
114
|
+
if (skin) {
|
|
115
|
+
stats = skin;
|
|
116
|
+
gated = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const val = stats ? { ...stats, gated } : null;
|
|
120
|
+
refCache.set(refPath, val);
|
|
121
|
+
return val;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const c of clips) {
|
|
125
|
+
const refPath = c.reference || opts.reference;
|
|
126
|
+
if (!refPath) {
|
|
127
|
+
skipped.push({ id: c.id, reason: 'no reference provided' });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const ref = await refStats(refPath);
|
|
131
|
+
if (!ref) {
|
|
132
|
+
skipped.push({ id: c.id, reason: 'reference unreadable' });
|
|
133
|
+
warnings.push(`${c.id}: reference '${refPath}' unreadable — skipped, not faked.`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Target: gate to skin iff the reference was skin-gated (compare like with like).
|
|
137
|
+
const tgt = await measureMeanStd(c.png, { skinGate: ref.gated });
|
|
138
|
+
if (!tgt) {
|
|
139
|
+
skipped.push({ id: c.id, reason: ref.gated ? 'target has no skin pixels to match' : 'target unreadable' });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const gain = {},
|
|
143
|
+
offset = {};
|
|
144
|
+
for (const k of ['r', 'g', 'b']) {
|
|
145
|
+
const st = tgt.std[k] || 1e-6;
|
|
146
|
+
gain[k] = clamp(ref.std[k] / st, glo, ghi);
|
|
147
|
+
offset[k] = clamp(ref.mean[k] - gain[k] * tgt.mean[k], olo, ohi);
|
|
148
|
+
}
|
|
149
|
+
if (lumaPreserve) {
|
|
150
|
+
// Keep the TARGET's own luma: shift all channels uniformly so transformed luma == target luma.
|
|
151
|
+
const tgtLuma = lumaW.r * tgt.mean.r + lumaW.g * tgt.mean.g + lumaW.b * tgt.mean.b;
|
|
152
|
+
const outLuma = ['r', 'g', 'b'].reduce((s, k) => s + lumaW[k] * (gain[k] * tgt.mean[k] + offset[k]), 0);
|
|
153
|
+
const delta = tgtLuma - outLuma;
|
|
154
|
+
for (const k of ['r', 'g', 'b']) offset[k] = clamp(offset[k] + delta, olo, ohi);
|
|
155
|
+
}
|
|
156
|
+
const corr = Math.max(...['r', 'g', 'b'].map((k) => Math.max(Math.abs(gain[k] - 1), Math.abs(offset[k]))));
|
|
157
|
+
const drxPath = `${opts.outDir}/${c.id}_matchref.drx`;
|
|
158
|
+
await generateAssertedAffineDRX(
|
|
159
|
+
gain,
|
|
160
|
+
offset,
|
|
161
|
+
provenanceLabel('match_to_reference', { source: `ref:${refPath.split('/').pop()}`, gist: gist('gain', gain) }),
|
|
162
|
+
drxPath,
|
|
163
|
+
warnings,
|
|
164
|
+
);
|
|
165
|
+
grades.push({ id: c.id, gain, offset, correctionPct: +(100 * corr).toFixed(2), skinGated: ref.gated, lumaPreserve, drxPath });
|
|
166
|
+
report[c.id] = { correctionPct: +(100 * corr).toFixed(2), skinGated: ref.gated };
|
|
167
|
+
if (corr > 0.35)
|
|
168
|
+
warnings.push(
|
|
169
|
+
`${c.id}: large correction ${(100 * corr).toFixed(0)}% — target/reference content may differ too much; review (a trim, not a cross-IDT reconciler).`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return { grades, report, warnings, skipped };
|
|
173
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster M — media_inventory (ffprobe) + sync (TC/waveform). Turns cards/turnovers into a
|
|
3
|
+
* manifest and a consistency report; pairs picture↔sound by timecode with drift + MOS flags.
|
|
4
|
+
*
|
|
5
|
+
* media_inventory: fps/codec/colorspace/TC/audio per file + consistency (mixed fps, wrong-space
|
|
6
|
+
* metadata, card-sequence gaps). Deps: ffprobe (peer).
|
|
7
|
+
* sync: TC-based picture↔sound pairing with per-take offset, long-take drift, and MOS flags.
|
|
8
|
+
* PURE over a supplied metadata list (fps + TC in/out); waveform-correlation refinement is a
|
|
9
|
+
* documented follow-up (offset from TC is the deterministic core, honest about its scope).
|
|
10
|
+
*
|
|
11
|
+
* No Resolve, no LLM.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { probeMedia } from './ffprobe-media.mjs';
|
|
15
|
+
|
|
16
|
+
// ── timecode ───────────────────────────────────────────────────────────
|
|
17
|
+
/** "HH:MM:SS:FF" (or ';' DF) → integer frames at fps. Returns null if unparseable. */
|
|
18
|
+
export function tcToFrames(tc, fps) {
|
|
19
|
+
if (typeof tc !== 'string' || !fps) return null;
|
|
20
|
+
const m = /^(\d{1,2}):(\d{2}):(\d{2})[:;](\d{2,3})$/.exec(tc.trim());
|
|
21
|
+
if (!m) return null;
|
|
22
|
+
const [, h, mm, s, f] = m.map(Number);
|
|
23
|
+
return Math.round((h * 3600 + mm * 60 + s) * fps) + f;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function framesToTc(frames, fps) {
|
|
27
|
+
if (!fps) return null;
|
|
28
|
+
const fpsI = Math.round(fps);
|
|
29
|
+
const f = frames % fpsI;
|
|
30
|
+
const totalSec = Math.floor(frames / fpsI);
|
|
31
|
+
const s = totalSec % 60;
|
|
32
|
+
const mm = Math.floor(totalSec / 60) % 60;
|
|
33
|
+
const h = Math.floor(totalSec / 3600);
|
|
34
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
35
|
+
return `${p2(h)}:${p2(mm)}:${p2(s)}:${p2(f)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── media_inventory ────────────────────────────────────────────────────
|
|
39
|
+
// Reel/clip parse for card-gap detection, e.g. A001C012, A001_C012, A001 C012.
|
|
40
|
+
const REEL_CLIP_RE = /([A-Z]\d{2,3})[_ ]?C(\d{2,4})/i;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {Array<{id?:string, path:string, expectedColorspace?:string}>} files
|
|
44
|
+
* @param {{ffprobe?:string}} [opts]
|
|
45
|
+
*/
|
|
46
|
+
export function mediaInventory(files, opts = {}) {
|
|
47
|
+
const items = [];
|
|
48
|
+
const skipped = [];
|
|
49
|
+
for (const f of files) {
|
|
50
|
+
const p = typeof f === 'string' ? f : f.path;
|
|
51
|
+
const probe = probeMedia(p, opts);
|
|
52
|
+
if (!probe) {
|
|
53
|
+
skipped.push({ path: p, reason: 'unreadable / not media' });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const v = probe.video || {};
|
|
57
|
+
const a0 = (probe.audio || [])[0] || {};
|
|
58
|
+
items.push({
|
|
59
|
+
id: (typeof f === 'object' && f.id) || path.basename(p),
|
|
60
|
+
name: path.basename(p),
|
|
61
|
+
fps: v.fps || null,
|
|
62
|
+
codec: v.codec || null,
|
|
63
|
+
width: v.width || null,
|
|
64
|
+
height: v.height || null,
|
|
65
|
+
colorTransfer: v.colorTransfer || null,
|
|
66
|
+
colorPrimaries: v.colorPrimaries || null,
|
|
67
|
+
colorMatrix: v.colorMatrix || null,
|
|
68
|
+
timecode: probe.format.timecode || null,
|
|
69
|
+
audioChannels: (probe.audio || []).reduce((n, a) => n + (a.channels || 0), 0),
|
|
70
|
+
audioSampleRate: a0.sampleRate || null,
|
|
71
|
+
duration: probe.format.duration,
|
|
72
|
+
expectedColorspace: (typeof f === 'object' && f.expectedColorspace) || null,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Consistency.
|
|
76
|
+
const distinct = (key) => [...new Set(items.map((i) => i[key]).filter((x) => x != null))];
|
|
77
|
+
const fpsSet = distinct('fps');
|
|
78
|
+
const warnings = [];
|
|
79
|
+
if (fpsSet.length > 1) warnings.push(`mixed frame rates across cards: ${fpsSet.join(', ')}`);
|
|
80
|
+
for (const it of items) {
|
|
81
|
+
if (it.expectedColorspace && it.colorTransfer && it.colorTransfer !== 'unknown' && it.colorTransfer.toLowerCase() !== it.expectedColorspace.toLowerCase())
|
|
82
|
+
warnings.push(`${it.name}: color transfer '${it.colorTransfer}' ≠ expected '${it.expectedColorspace}'`);
|
|
83
|
+
if (it.audioChannels === 0) warnings.push(`${it.name}: no audio channels (MOS?)`);
|
|
84
|
+
}
|
|
85
|
+
// Card-sequence gaps.
|
|
86
|
+
const byReel = new Map();
|
|
87
|
+
for (const it of items) {
|
|
88
|
+
const m = REEL_CLIP_RE.exec(it.name);
|
|
89
|
+
if (!m) continue;
|
|
90
|
+
const reel = m[1].toUpperCase();
|
|
91
|
+
const clip = Number(m[2]);
|
|
92
|
+
if (!byReel.has(reel)) byReel.set(reel, []);
|
|
93
|
+
byReel.get(reel).push(clip);
|
|
94
|
+
}
|
|
95
|
+
const cardGaps = [];
|
|
96
|
+
for (const [reel, nums] of byReel) {
|
|
97
|
+
nums.sort((a, b) => a - b);
|
|
98
|
+
const missing = [];
|
|
99
|
+
for (let n = nums[0]; n <= nums[nums.length - 1]; n++) if (!nums.includes(n)) missing.push(n);
|
|
100
|
+
if (missing.length) cardGaps.push({ reel, present: nums, missing });
|
|
101
|
+
}
|
|
102
|
+
if (cardGaps.length) warnings.push(`card-sequence gaps: ${cardGaps.map((g) => `${g.reel}[${g.missing.join(',')}]`).join(' ')}`);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
count: items.length,
|
|
106
|
+
items,
|
|
107
|
+
consistency: { frameRates: fpsSet, codecs: distinct('codec'), colorTransfers: distinct('colorTransfer'), mixedFps: fpsSet.length > 1, cardGaps },
|
|
108
|
+
warnings,
|
|
109
|
+
skipped,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── sync (TC-based) ────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Pair picture↔sound by timecode overlap; per-take offset + long-take drift + MOS flags. PURE.
|
|
116
|
+
* @param {Array<{id, type:'picture'|'sound', tcStart:string, tcEnd?:string, fps:number, hasAudio?:boolean, durationFrames?:number}>} clips
|
|
117
|
+
* @param {{driftToleranceFrames?:number, longTakeFrames?:number}} [opts]
|
|
118
|
+
*/
|
|
119
|
+
export function syncByTC(clips, opts = {}) {
|
|
120
|
+
const driftTol = opts.driftToleranceFrames ?? 2;
|
|
121
|
+
const longTake = opts.longTakeFrames ?? 3600; // ~2min @30 → "long take" where drift matters
|
|
122
|
+
const pics = clips.filter((c) => c.type === 'picture');
|
|
123
|
+
const sounds = clips.filter((c) => c.type === 'sound');
|
|
124
|
+
const spanOf = (c) => {
|
|
125
|
+
const start = tcToFrames(c.tcStart, c.fps);
|
|
126
|
+
if (start == null) return null;
|
|
127
|
+
const len = c.durationFrames ?? (c.tcEnd ? tcToFrames(c.tcEnd, c.fps) - start : null);
|
|
128
|
+
return { start, end: len != null ? start + len : null, len };
|
|
129
|
+
};
|
|
130
|
+
const pairs = [];
|
|
131
|
+
const mos = [];
|
|
132
|
+
const usedSound = new Set();
|
|
133
|
+
for (const pic of pics) {
|
|
134
|
+
const ps = spanOf(pic);
|
|
135
|
+
if (pic.hasAudio === false || !sounds.length) {
|
|
136
|
+
if (pic.hasAudio === false) mos.push({ id: pic.id, reason: 'flagged MOS (no sync audio)' });
|
|
137
|
+
}
|
|
138
|
+
// Find the sound whose TC span overlaps the most.
|
|
139
|
+
let best = null,
|
|
140
|
+
bestOverlap = -1;
|
|
141
|
+
for (const snd of sounds) {
|
|
142
|
+
if (usedSound.has(snd.id)) continue;
|
|
143
|
+
const ss = spanOf(snd);
|
|
144
|
+
if (!ps || !ss || ps.end == null || ss.end == null) continue;
|
|
145
|
+
const overlap = Math.min(ps.end, ss.end) - Math.max(ps.start, ss.start);
|
|
146
|
+
if (overlap > bestOverlap) {
|
|
147
|
+
bestOverlap = overlap;
|
|
148
|
+
best = { snd, ss };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (best && bestOverlap > 0) {
|
|
152
|
+
usedSound.add(best.snd.id);
|
|
153
|
+
const offsetFrames = best.ss.start - ps.start;
|
|
154
|
+
// Drift: on a long take, a length mismatch beyond tol suggests clock drift / pulldown slip.
|
|
155
|
+
let drift = null;
|
|
156
|
+
if (ps.len != null && best.ss.len != null) {
|
|
157
|
+
const lenDelta = best.ss.len - ps.len;
|
|
158
|
+
const isLong = Math.max(ps.len, best.ss.len) >= longTake;
|
|
159
|
+
drift = { lenDeltaFrames: lenDelta, longTake: isLong, flagged: Math.abs(lenDelta) > driftTol && isLong };
|
|
160
|
+
}
|
|
161
|
+
pairs.push({ picture: pic.id, sound: best.snd.id, offsetFrames, drift });
|
|
162
|
+
} else if (pic.hasAudio !== false) {
|
|
163
|
+
mos.push({ id: pic.id, reason: 'no overlapping sound found (guide-track only / MOS?)' });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const unmatchedSound = sounds.filter((s) => !usedSound.has(s.id)).map((s) => s.id);
|
|
167
|
+
const driftFlags = pairs.filter((p) => p.drift && p.drift.flagged).map((p) => p.picture);
|
|
168
|
+
return { pairs, mos, unmatchedSound, driftFlags, note: 'TC-based pairing (deterministic). Waveform cross-correlation is a follow-up refinement.' };
|
|
169
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cluster M — media front-end / AE ops (the recurring-show time sink). fs-based + deterministic;
|
|
3
|
+
* the ffprobe-backed inventory/sync live in media-inventory.mjs.
|
|
4
|
+
*
|
|
5
|
+
* Silent-lie discipline extends to MEDIA here (cross-craft review): assert bytes-read>0 and
|
|
6
|
+
* file-count matches (empty-green is the lie); dupe by HASH never by name; NEVER rewrite a
|
|
7
|
+
* camera-original filename (sidecar/DB only); path-maps scoped per episode (never relink to the
|
|
8
|
+
* wrong season). Everything is a dry-run PLAN or a VERIFY report — the AE approves the apply.
|
|
9
|
+
*
|
|
10
|
+
* ingest_verify — seal (hash manifest / MHL-like) / verify / dupes-by-hash
|
|
11
|
+
* relink_manifest — offline-media + path-map PREFLIGHT (dry-run)
|
|
12
|
+
* rename_plan — dry-run rename plan; refuses camera originals
|
|
13
|
+
* reel_normalize — normalize reel/card names to a convention (plan + collisions)
|
|
14
|
+
* project_hygiene — dupes, orphan/offline clips, mixed-fps timelines, empty bins, unlabeled versions
|
|
15
|
+
* turnover_package— assemble + checksum a dated color/sound/VFX folder manifest (dry-run)
|
|
16
|
+
*
|
|
17
|
+
* No Resolve, no LLM.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { checksumFile } from './render-manifest.mjs';
|
|
22
|
+
|
|
23
|
+
// ── ingest_verify ──────────────────────────────────────────────────────
|
|
24
|
+
/** Seal a set of files into a hash manifest (MHL-like). Asserts every file has bytes>0. */
|
|
25
|
+
export function sealFiles(files, opts = {}) {
|
|
26
|
+
const entries = [];
|
|
27
|
+
const missing = [];
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
const p = typeof f === 'string' ? f : f.path;
|
|
30
|
+
if (!fs.existsSync(p)) {
|
|
31
|
+
missing.push(p);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const { sha256, size } = checksumFile(p);
|
|
35
|
+
entries.push({ name: path.basename(p), path: p, sha256, size });
|
|
36
|
+
}
|
|
37
|
+
const totalBytes = entries.reduce((n, e) => n + e.size, 0);
|
|
38
|
+
return { version: 1, hashType: 'sha256', count: entries.length, totalBytes, files: entries, missing, sealedFrom: opts.label || null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Verify files against a prior seal. Reports ok/missing/changed per file + copy-completeness. */
|
|
42
|
+
export function verifyFiles(manifest, opts = {}) {
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const e of manifest.files || []) {
|
|
45
|
+
const p = opts.baseDir ? path.join(opts.baseDir, e.name) : e.path;
|
|
46
|
+
if (!fs.existsSync(p)) {
|
|
47
|
+
results.push({ name: e.name, status: 'missing', pass: false });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const { sha256, size } = checksumFile(p);
|
|
51
|
+
const ok = sha256 === e.sha256 && size === e.size;
|
|
52
|
+
results.push({ name: e.name, status: ok ? 'ok' : 'changed', pass: ok, ...(ok ? {} : { expectedSha: e.sha256, actualSha: sha256 }) });
|
|
53
|
+
}
|
|
54
|
+
const failed = results.filter((r) => !r.pass);
|
|
55
|
+
return {
|
|
56
|
+
pass: failed.length === 0,
|
|
57
|
+
verified: results.length,
|
|
58
|
+
expected: (manifest.files || []).length,
|
|
59
|
+
complete: results.length === (manifest.files || []).length,
|
|
60
|
+
results,
|
|
61
|
+
failed: failed.map((r) => r.name),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Find duplicate CONTENT (same hash, any name) across a file set. */
|
|
66
|
+
export function findDupesByHash(files) {
|
|
67
|
+
const byHash = new Map();
|
|
68
|
+
for (const f of files) {
|
|
69
|
+
const p = typeof f === 'string' ? f : f.path;
|
|
70
|
+
if (!fs.existsSync(p)) continue;
|
|
71
|
+
const { sha256 } = checksumFile(p);
|
|
72
|
+
if (!byHash.has(sha256)) byHash.set(sha256, []);
|
|
73
|
+
byHash.get(sha256).push(path.basename(p));
|
|
74
|
+
}
|
|
75
|
+
const dupes = [...byHash.entries()].filter(([, names]) => names.length > 1).map(([sha256, names]) => ({ sha256, names }));
|
|
76
|
+
return { dupeGroups: dupes.length, dupes };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function ingestVerify(mode, args = {}) {
|
|
80
|
+
if (mode === 'seal') return sealFiles(args.files || [], { label: args.label });
|
|
81
|
+
if (mode === 'verify') return verifyFiles(args.manifest, { baseDir: args.baseDir });
|
|
82
|
+
if (mode === 'dupes') return findDupesByHash(args.files || []);
|
|
83
|
+
throw new Error(`ingest_verify: unknown mode '${mode}' (seal|verify|dupes)`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── relink_manifest ────────────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Preflight offline media against a path-map (dry-run). path-maps are SCOPED PER EPISODE by
|
|
89
|
+
* the caller — never a global relink. Longest-prefix wins; multiple matches → ambiguous.
|
|
90
|
+
* @param {string[]} offlinePaths
|
|
91
|
+
* @param {Array<{from:string, to:string}>} pathMap prefix rewrites
|
|
92
|
+
*/
|
|
93
|
+
export function relinkManifest(offlinePaths, pathMap = [], opts = {}) {
|
|
94
|
+
const maps = [...pathMap].sort((a, b) => b.from.length - a.from.length);
|
|
95
|
+
const relinkable = [];
|
|
96
|
+
const stillOffline = [];
|
|
97
|
+
const ambiguous = [];
|
|
98
|
+
const unmapped = [];
|
|
99
|
+
for (const src of offlinePaths) {
|
|
100
|
+
const matches = maps.filter((m) => src.startsWith(m.from));
|
|
101
|
+
if (!matches.length) {
|
|
102
|
+
unmapped.push(src);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (matches.length > 1 && matches[0].from.length === matches[1].from.length) {
|
|
106
|
+
ambiguous.push({ from: src, candidates: matches.map((m) => m.from) });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const m = matches[0];
|
|
110
|
+
const target = src.replace(m.from, m.to);
|
|
111
|
+
if (opts.checkExists === false || fs.existsSync(target)) relinkable.push({ from: src, to: target, verified: opts.checkExists !== false });
|
|
112
|
+
else stillOffline.push({ from: src, to: target, reason: 'target does not exist' });
|
|
113
|
+
}
|
|
114
|
+
return { total: offlinePaths.length, relinkableCount: relinkable.length, relinkable, stillOffline, ambiguous, unmapped };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── rename_plan / reel_normalize ───────────────────────────────────────
|
|
118
|
+
// A camera-original if its name matches common camera patterns, unless the caller says otherwise.
|
|
119
|
+
const CAMERA_ORIGINAL_RE = /^([A-Z]\d{3}[_ ]?[CR]\d{3,4}|[A-Z]\d{3}_\d{8}_C\d+|MVI_\d+|DSC\d+|[A-Z]{2,4}_?\d{4,})/i;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Dry-run rename plan (regex find/replace). REFUSES camera originals (sidecar/DB only).
|
|
123
|
+
* @param {string[]} names basenames to rename
|
|
124
|
+
* @param {{find:string, replace:string, allowCameraOriginals?:boolean, cameraOriginalRe?:RegExp}} rule
|
|
125
|
+
*/
|
|
126
|
+
export function renamePlan(names, rule = {}) {
|
|
127
|
+
let re;
|
|
128
|
+
try {
|
|
129
|
+
re = new RegExp(rule.find, 'g');
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error(`rename_plan: invalid find regex '${rule.find}'`);
|
|
132
|
+
}
|
|
133
|
+
const camRe = rule.cameraOriginalRe || CAMERA_ORIGINAL_RE;
|
|
134
|
+
const plan = [];
|
|
135
|
+
const targets = new Map();
|
|
136
|
+
for (const name of names) {
|
|
137
|
+
const isCam = camRe.test(name);
|
|
138
|
+
if (isCam && !rule.allowCameraOriginals) {
|
|
139
|
+
plan.push({ from: name, action: 'refuse-camera-original', note: 'camera original — rename via sidecar/DB only, never the file' });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const to = name.replace(re, rule.replace ?? '');
|
|
143
|
+
if (to === name) {
|
|
144
|
+
plan.push({ from: name, to, action: 'noop' });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
targets.set(to, (targets.get(to) || 0) + 1);
|
|
148
|
+
plan.push({ from: name, to, action: 'rename' });
|
|
149
|
+
}
|
|
150
|
+
// Flag collisions (two sources → one target).
|
|
151
|
+
for (const p of plan) if (p.action === 'rename' && targets.get(p.to) > 1) p.action = 'collision';
|
|
152
|
+
const collisions = plan.filter((p) => p.action === 'collision');
|
|
153
|
+
return {
|
|
154
|
+
plan,
|
|
155
|
+
renameCount: plan.filter((p) => p.action === 'rename').length,
|
|
156
|
+
refused: plan.filter((p) => p.action === 'refuse-camera-original').length,
|
|
157
|
+
collisions: collisions.length,
|
|
158
|
+
dryRun: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Normalize reel/card names to a convention (uppercase + zero-pad the trailing number). */
|
|
163
|
+
export function reelNormalize(reels, opts = {}) {
|
|
164
|
+
const pad = opts.pad ?? 3;
|
|
165
|
+
const plan = [];
|
|
166
|
+
const targets = new Map();
|
|
167
|
+
for (const r of reels) {
|
|
168
|
+
const to = String(r)
|
|
169
|
+
.toUpperCase()
|
|
170
|
+
.replace(/(\d+)\s*$/, (m) => m.padStart(pad, '0'));
|
|
171
|
+
if (to !== r) targets.set(to, (targets.get(to) || 0) + 1);
|
|
172
|
+
plan.push({ from: r, to, action: to === r ? 'noop' : 'normalize' });
|
|
173
|
+
}
|
|
174
|
+
for (const p of plan) if (p.action === 'normalize' && targets.get(p.to) > 1) p.action = 'collision';
|
|
175
|
+
return { plan, normalizeCount: plan.filter((p) => p.action === 'normalize').length, collisions: plan.filter((p) => p.action === 'collision').length };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── project_hygiene ────────────────────────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* Report project hygiene issues over a supplied structure (the live server reports it; Node judges).
|
|
181
|
+
* @param {{clips?:Array, timelines?:Array, bins?:Array, versions?:Array}} project
|
|
182
|
+
*/
|
|
183
|
+
export function projectHygiene(project = {}) {
|
|
184
|
+
const clips = project.clips || [];
|
|
185
|
+
const offlineClips = clips.filter((c) => c.online === false).map((c) => c.id);
|
|
186
|
+
// Dupes by hash (fallback path) — never by name alone.
|
|
187
|
+
const byHash = new Map();
|
|
188
|
+
for (const c of clips) {
|
|
189
|
+
const key = c.hash || c.path;
|
|
190
|
+
if (key == null) continue;
|
|
191
|
+
if (!byHash.has(key)) byHash.set(key, []);
|
|
192
|
+
byHash.get(key).push(c.id);
|
|
193
|
+
}
|
|
194
|
+
const dupes = [...byHash.entries()].filter(([, ids]) => ids.length > 1).map(([key, ids]) => ({ key, ids }));
|
|
195
|
+
const mixedFpsTimelines = (project.timelines || [])
|
|
196
|
+
.filter((t) => {
|
|
197
|
+
const fpsSet = new Set([...(t.clipFps || [])]);
|
|
198
|
+
if (t.fps != null) for (const f of t.clipFps || []) if (f !== t.fps) fpsSet.add(f);
|
|
199
|
+
return fpsSet.size > 1;
|
|
200
|
+
})
|
|
201
|
+
.map((t) => t.name);
|
|
202
|
+
const emptyBins = (project.bins || []).filter((b) => (b.clipCount ?? 0) === 0).map((b) => b.name);
|
|
203
|
+
const unlabeledVersions = (project.versions || []).filter((v) => !v.label).map((v) => v.name);
|
|
204
|
+
const findings = { offlineClips, dupes, mixedFpsTimelines, emptyBins, unlabeledVersions };
|
|
205
|
+
const issueCount = offlineClips.length + dupes.length + mixedFpsTimelines.length + emptyBins.length + unlabeledVersions.length;
|
|
206
|
+
return { clean: issueCount === 0, issueCount, findings, gate: 'review' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── turnover_package ───────────────────────────────────────────────────
|
|
210
|
+
/**
|
|
211
|
+
* Assemble a dated turnover package MANIFEST (dry-run — checksums existing files, plans the
|
|
212
|
+
* folder). Handles + reference are recorded, not enforced here.
|
|
213
|
+
* @param {Array<{path:string, category:'color'|'sound'|'vfx'|'reference', role?:string}>} inputs
|
|
214
|
+
* @param {{date?:string, name?:string, handles?:number}} opts date = 'YYYYMMDD' (caller-supplied; deterministic)
|
|
215
|
+
*/
|
|
216
|
+
export function turnoverPackage(inputs, opts = {}) {
|
|
217
|
+
const date = opts.date || 'UNDATED';
|
|
218
|
+
const name = opts.name || 'turnover';
|
|
219
|
+
const folder = `${date}_${name}_TransferFiles`;
|
|
220
|
+
const categories = {};
|
|
221
|
+
let totalBytes = 0;
|
|
222
|
+
const missing = [];
|
|
223
|
+
for (const it of inputs) {
|
|
224
|
+
if (!fs.existsSync(it.path)) {
|
|
225
|
+
missing.push(it.path);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const { sha256, size } = checksumFile(it.path);
|
|
229
|
+
const cat = it.category || 'other';
|
|
230
|
+
(categories[cat] ||= []).push({ name: path.basename(it.path), path: it.path, role: it.role || null, sha256, size });
|
|
231
|
+
totalBytes += size;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
folder,
|
|
235
|
+
handles: opts.handles ?? null,
|
|
236
|
+
categories,
|
|
237
|
+
fileCount: Object.values(categories).reduce((n, a) => n + a.length, 0),
|
|
238
|
+
totalBytes,
|
|
239
|
+
missing,
|
|
240
|
+
dryRun: true,
|
|
241
|
+
gate: 'review',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-meta-db — set/clear a graded node's LABEL (F6) + COLOR (F15) directly in a live Project.db,
|
|
3
|
+
* via the surgical grade-Body patcher (no UI, no lossy generator). Node-convention/provenance spec.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors offline-ref-db.mjs: project-CLOSED gate → auto-backup → schema guard → UPDATE → read-back verify.
|
|
6
|
+
* Resolve oversaves an open project, so the project MUST be closed in Resolve before patching.
|
|
7
|
+
*
|
|
8
|
+
* Join (RE'd 2026-06-22): Sm2TiItem.pLmVerTable → "ListMgt::LmVersion"(HasCorrection='1').Body, scoped to a
|
|
9
|
+
* timeline via Sm2TiTrack.Sequence → Sm2Sequence.Sm2Timeline_id → Sm2Timeline.Name. Body is a BLOB.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { loadSqlite, resolveDbPath, openGuarded, backup as backupDb, requireClosed } from './db-patch.mjs';
|
|
13
|
+
import { patchGradeBody, readNodeMeta } from './grade-body-patch.mjs';
|
|
14
|
+
|
|
15
|
+
const FIND_GRADE_SQL = `
|
|
16
|
+
SELECT v.rowid AS vid, lower(hex(v.Body)) AS body, i.Name AS clip
|
|
17
|
+
FROM Sm2TiItem i
|
|
18
|
+
JOIN Sm2TiTrack t ON i.Sm2TiTrack_id = t.Sm2TiTrack_id
|
|
19
|
+
JOIN Sm2Sequence s ON t.Sequence = s.Sm2Sequence_id
|
|
20
|
+
JOIN Sm2Timeline tl ON s.Sm2Timeline_id = tl.Sm2Timeline_id
|
|
21
|
+
JOIN "ListMgt::LmVersion" v ON v."ListMgt::LmVersionTable_id" = i.pLmVerTable AND v.HasCorrection = '1'
|
|
22
|
+
WHERE tl.Name = ? AND t.Type = 0
|
|
23
|
+
ORDER BY i.Start ASC`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set/clear a graded node's label/color in a project's timeline.
|
|
27
|
+
* @param {object} opts
|
|
28
|
+
* @param {string} [opts.projectDb] absolute Project.db path (or use projectName)
|
|
29
|
+
* @param {string} [opts.projectName] disk-DB project name (resolved to its Project.db)
|
|
30
|
+
* @param {string} opts.timeline timeline name
|
|
31
|
+
* @param {number} [opts.nodeIndex=0] node to edit
|
|
32
|
+
* @param {string|null} [opts.label] undefined=leave · null=clear · string=set
|
|
33
|
+
* @param {string|null} [opts.color] undefined=leave · null=clear · string=set (e.g. "Blue")
|
|
34
|
+
* @param {boolean} opts.iConfirmProjectClosed REQUIRED true — Resolve oversaves an open project
|
|
35
|
+
* @param {boolean} [opts.backup=true]
|
|
36
|
+
*/
|
|
37
|
+
export async function setNodeLabelColorInProject(opts) {
|
|
38
|
+
const { timeline, nodeIndex = 0, label, color, backup = true } = opts;
|
|
39
|
+
requireClosed(opts);
|
|
40
|
+
if (label === undefined && color === undefined) throw new Error('nothing to do: provide label and/or color');
|
|
41
|
+
const dbPath = resolveDbPath(opts);
|
|
42
|
+
const bak = backup ? backupDb(dbPath) : null;
|
|
43
|
+
const db = openGuarded(dbPath, { writable: true, table: 'ListMgt::LmVersion', column: 'Body' });
|
|
44
|
+
try {
|
|
45
|
+
const rows = db.prepare(FIND_GRADE_SQL).all(timeline);
|
|
46
|
+
if (!rows.length) throw new Error(`no graded clip (HasCorrection=1) on timeline "${timeline}" — apply a grade first`);
|
|
47
|
+
const target = rows[0];
|
|
48
|
+
const before = readNodeMeta(target.body, nodeIndex);
|
|
49
|
+
const newHex = await patchGradeBody(target.body, nodeIndex, { label, color });
|
|
50
|
+
db.prepare('UPDATE "ListMgt::LmVersion" SET Body = ? WHERE rowid = ?').run(Buffer.from(newHex, 'hex'), target.vid);
|
|
51
|
+
// read-back verify
|
|
52
|
+
const after = db.prepare('SELECT lower(hex(Body)) AS body FROM "ListMgt::LmVersion" WHERE rowid = ?').get(target.vid);
|
|
53
|
+
const meta = readNodeMeta(after.body, nodeIndex);
|
|
54
|
+
const want = (v, got) => (v === undefined ? true : v === null ? got == null : got === v);
|
|
55
|
+
const verified = want(label, meta.label) && want(color, meta.color);
|
|
56
|
+
return { projectDb: dbPath, backup: bak, clip: target.clip, version: target.vid, nodeIndex, before, after: meta, verified };
|
|
57
|
+
} finally {
|
|
58
|
+
db.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { loadSqlite };
|