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,1808 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DRX Parser - Extract node structures from DaVinci Resolve DRX files
|
|
3
|
+
*
|
|
4
|
+
* Parses DRX files to extract:
|
|
5
|
+
* - Node definitions (id, position, label, enabled, parameters)
|
|
6
|
+
* - Connections between nodes
|
|
7
|
+
* - Metadata (resolution, timestamps)
|
|
8
|
+
*
|
|
9
|
+
* Uses shared DRX parameter library for consistent parameter handling
|
|
10
|
+
* across the the pipeline platform.
|
|
11
|
+
*
|
|
12
|
+
* @module drx/drx-parser
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
// zstd-codec (WASM) with fzstd (pure JS) fallback for Electron asar bundles
|
|
18
|
+
let ZstdCodec;
|
|
19
|
+
try {
|
|
20
|
+
ZstdCodec = require('zstd-codec').ZstdCodec;
|
|
21
|
+
} catch {
|
|
22
|
+
ZstdCodec = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Import shared DRX parameter library
|
|
26
|
+
const drxParams = require('../drx-parameters');
|
|
27
|
+
|
|
28
|
+
// Build PARAM_IDS from shared library for backwards compatibility
|
|
29
|
+
// This maps parameter IDs to their semantic meaning
|
|
30
|
+
const PARAM_IDS = {};
|
|
31
|
+
for (const [paramId, info] of Object.entries(drxParams.PARAM_ID_MAP)) {
|
|
32
|
+
PARAM_IDS[paramId] = {
|
|
33
|
+
name: `${info.control}.${info.channel}`,
|
|
34
|
+
control: info.control,
|
|
35
|
+
channel: info.channel,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Re-export corrector types from shared library for backwards compatibility
|
|
40
|
+
const CORRECTOR_TYPES = drxParams.CORRECTOR_NAMES;
|
|
41
|
+
|
|
42
|
+
// ZSTD decoder — prefer Node's built-in zstd (Node 22+, available on Vercel)
|
|
43
|
+
// over zstd-codec (WASM) over fzstd (pure JS). See drx-generator.js for the
|
|
44
|
+
// reason: zstd-codec produced truncated frames on Vercel's runtime, and we
|
|
45
|
+
// want parser + generator to agree on which backend is authoritative.
|
|
46
|
+
const _nodeZlib = require('zlib');
|
|
47
|
+
const _HAS_NATIVE_ZSTD = typeof _nodeZlib.zstdDecompressSync === 'function';
|
|
48
|
+
|
|
49
|
+
let zstdCodec = null;
|
|
50
|
+
|
|
51
|
+
async function getZstdCodec() {
|
|
52
|
+
if (zstdCodec) return zstdCodec;
|
|
53
|
+
|
|
54
|
+
if (_HAS_NATIVE_ZSTD) {
|
|
55
|
+
zstdCodec = {
|
|
56
|
+
decompress(data, _size) {
|
|
57
|
+
const buf = data instanceof Buffer
|
|
58
|
+
? data
|
|
59
|
+
: Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
60
|
+
return _nodeZlib.zstdDecompressSync(buf);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return zstdCodec;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ZstdCodec) {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
ZstdCodec.run((zstd) => {
|
|
69
|
+
zstdCodec = new zstd.Streaming();
|
|
70
|
+
resolve(zstdCodec);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fzstd = require('fzstd');
|
|
76
|
+
zstdCodec = {
|
|
77
|
+
decompress(data, _size) {
|
|
78
|
+
return fzstd.decompress(data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return zstdCodec;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// P4A: LRU cache for parsed DRX content (keyed by SHA-256 hash of input)
|
|
85
|
+
// Avoids re-decompression + re-parsing for identical DRX data
|
|
86
|
+
const DRX_PARSE_CACHE_MAX = 50;
|
|
87
|
+
const drxParseCache = new Map();
|
|
88
|
+
|
|
89
|
+
function drxCacheGet(key) {
|
|
90
|
+
const entry = drxParseCache.get(key);
|
|
91
|
+
if (!entry) return undefined;
|
|
92
|
+
// Move to end (most recently used)
|
|
93
|
+
drxParseCache.delete(key);
|
|
94
|
+
drxParseCache.set(key, entry);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function drxCacheSet(key, value) {
|
|
99
|
+
if (drxParseCache.size >= DRX_PARSE_CACHE_MAX) {
|
|
100
|
+
// Evict oldest (first) entry
|
|
101
|
+
const firstKey = drxParseCache.keys().next().value;
|
|
102
|
+
drxParseCache.delete(firstKey);
|
|
103
|
+
}
|
|
104
|
+
drxParseCache.set(key, value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Protobuf parser for DRX body content
|
|
109
|
+
*/
|
|
110
|
+
class ProtobufParser {
|
|
111
|
+
constructor(buffer) {
|
|
112
|
+
this.buffer = buffer;
|
|
113
|
+
this.pos = 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
readVarint() {
|
|
117
|
+
let result = 0;
|
|
118
|
+
let shift = 0;
|
|
119
|
+
while (this.pos < this.buffer.length) {
|
|
120
|
+
const byte = this.buffer[this.pos++];
|
|
121
|
+
result |= (byte & 0x7f) << shift;
|
|
122
|
+
if ((byte & 0x80) === 0) break;
|
|
123
|
+
shift += 7;
|
|
124
|
+
if (shift > 35) throw new Error('Varint too long');
|
|
125
|
+
}
|
|
126
|
+
return result >>> 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
readFixed32() {
|
|
130
|
+
const val = this.buffer.readUInt32LE(this.pos);
|
|
131
|
+
this.pos += 4;
|
|
132
|
+
return val;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
readFloat32() {
|
|
136
|
+
const val = this.buffer.readFloatLE(this.pos);
|
|
137
|
+
this.pos += 4;
|
|
138
|
+
return val;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
readFixed64() {
|
|
142
|
+
const low = this.buffer.readUInt32LE(this.pos);
|
|
143
|
+
const high = this.buffer.readUInt32LE(this.pos + 4);
|
|
144
|
+
this.pos += 8;
|
|
145
|
+
return { low, high };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
readBytes(length) {
|
|
149
|
+
const bytes = this.buffer.slice(this.pos, this.pos + length);
|
|
150
|
+
this.pos += length;
|
|
151
|
+
return bytes;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
parse(maxDepth = 10) {
|
|
155
|
+
const result = { _fields: [] };
|
|
156
|
+
|
|
157
|
+
while (this.pos < this.buffer.length && maxDepth > 0) {
|
|
158
|
+
const startPos = this.pos;
|
|
159
|
+
let tag;
|
|
160
|
+
try {
|
|
161
|
+
tag = this.readVarint();
|
|
162
|
+
} catch {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (tag === 0) break;
|
|
167
|
+
|
|
168
|
+
const fieldNum = tag >>> 3;
|
|
169
|
+
const wireType = tag & 0x7;
|
|
170
|
+
|
|
171
|
+
let value;
|
|
172
|
+
let rawValue;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
switch (wireType) {
|
|
176
|
+
case 0: // Varint
|
|
177
|
+
rawValue = this.readVarint();
|
|
178
|
+
value = rawValue;
|
|
179
|
+
break;
|
|
180
|
+
case 1: // 64-bit
|
|
181
|
+
rawValue = this.readFixed64();
|
|
182
|
+
value = rawValue;
|
|
183
|
+
break;
|
|
184
|
+
case 2: // Length-delimited
|
|
185
|
+
const length = this.readVarint();
|
|
186
|
+
rawValue = this.readBytes(length);
|
|
187
|
+
// Try to parse as nested protobuf
|
|
188
|
+
try {
|
|
189
|
+
const nestedParser = new ProtobufParser(rawValue);
|
|
190
|
+
value = nestedParser.parse(maxDepth - 1);
|
|
191
|
+
if (!value._fields || value._fields.length === 0) {
|
|
192
|
+
value = rawValue;
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
value = rawValue;
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case 5: // 32-bit (fixed32/float)
|
|
199
|
+
rawValue = this.readFixed32();
|
|
200
|
+
const floatBuf = Buffer.alloc(4);
|
|
201
|
+
floatBuf.writeUInt32LE(rawValue);
|
|
202
|
+
value = floatBuf.readFloatLE(0);
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const field = {
|
|
212
|
+
fieldNum,
|
|
213
|
+
wireType,
|
|
214
|
+
value,
|
|
215
|
+
rawValue,
|
|
216
|
+
pos: startPos,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
result._fields.push(field);
|
|
220
|
+
|
|
221
|
+
const key = `F${fieldNum}`;
|
|
222
|
+
if (result[key] !== undefined) {
|
|
223
|
+
if (!Array.isArray(result[key])) {
|
|
224
|
+
result[key] = [result[key]];
|
|
225
|
+
}
|
|
226
|
+
result[key].push(value);
|
|
227
|
+
} else {
|
|
228
|
+
result[key] = value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Extract string from a field that may have been incorrectly parsed as nested protobuf
|
|
238
|
+
* @param {Object} parentObj - The parent object containing the field
|
|
239
|
+
* @param {number} fieldNum - The field number to extract
|
|
240
|
+
* @returns {string} - The extracted string or empty string
|
|
241
|
+
*/
|
|
242
|
+
function extractStringField(parentObj, fieldNum) {
|
|
243
|
+
const fieldKey = `F${fieldNum}`;
|
|
244
|
+
const fieldValue = parentObj[fieldKey];
|
|
245
|
+
|
|
246
|
+
// If it's already a Buffer, convert to string
|
|
247
|
+
if (Buffer.isBuffer(fieldValue)) {
|
|
248
|
+
return fieldValue.toString('utf8');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If it's already a string, use it directly
|
|
252
|
+
if (typeof fieldValue === 'string') {
|
|
253
|
+
return fieldValue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If it's an object (incorrectly parsed as nested protobuf), extract rawValue from _fields
|
|
257
|
+
if (fieldValue && typeof fieldValue === 'object' && parentObj._fields) {
|
|
258
|
+
const field = parentObj._fields.find(f => f.fieldNum === fieldNum);
|
|
259
|
+
if (field && Buffer.isBuffer(field.rawValue)) {
|
|
260
|
+
return field.rawValue.toString('utf8');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return '';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ColorSlice per-vector grid (Tier-2, RE'd 2026-06-22 — design note §16a). The grid
|
|
268
|
+
// param 0x86000606 (COLORSLICE.VECTOR_DATA) carries 7 repeated sub-messages at F24.F1,
|
|
269
|
+
// one per color vector, each: F1=enabled(varint), F3=sat(float32, default 1.0),
|
|
270
|
+
// F4=hue(float32, stored NEGATED of the UI value). Previously surfaced as an opaque
|
|
271
|
+
// NaN scalar; this lifts it into named per-vector params. (The per-vector "Center"
|
|
272
|
+
// values live in the sibling GRID_DATA_2 0x86000607 as a packed binary blob — not
|
|
273
|
+
// clean protobuf — so they are NOT decoded here; see §16a follow-up.)
|
|
274
|
+
const COLORSLICE_VECTOR_DATA_ID = 2248148486; // 0x86000606
|
|
275
|
+
const COLORSLICE_GRID2_ID = 2248148487; // 0x86000607 — per-vector "Center" values
|
|
276
|
+
const COLORSLICE_VECTORS = ['red', 'skin', 'yellow', 'green', 'cyan', 'blue', 'magenta'];
|
|
277
|
+
|
|
278
|
+
// Tier-2: HDR Zone adjustments (0x86000305). F16 → F1[] repeated zone sub-messages, each a
|
|
279
|
+
// raw buffer: 0a<len><name> 15<f32 exposure> 1d<f32 cbalY> 25<f32 cbalX> 3d<f32 saturation>.
|
|
280
|
+
// Zones differentiate by embedded name string, not param ID (RE'd 2026-06-22 on DRX_CALIB bars).
|
|
281
|
+
const HDR_ZONE_ADJUSTMENTS_ID = 2248147717; // 0x86000305
|
|
282
|
+
|
|
283
|
+
// Corrector-type-aware naming for the GRADIENT_WINDOW ↔ SAT_VS_SAT id collision (both 0x08F000xx,
|
|
284
|
+
// distinguished only by corrector type: gradient = 65554, sat-vs-sat = 3). The flat PARAM_IDS map
|
|
285
|
+
// resolves these to satVsSat.*; when the corrector is a gradient window, override to gradientWindow.*.
|
|
286
|
+
const GRADIENT_CORRECTOR_TYPE = 65554;
|
|
287
|
+
const GRADIENT_NAME_BY_ID = {
|
|
288
|
+
149946369: 'gradientWindow.type', // 0x08F00001
|
|
289
|
+
149946371: 'gradientWindow.rotation', // 0x08F00003
|
|
290
|
+
149946373: 'gradientWindow.handle1Pos', // 0x08F00005
|
|
291
|
+
149946374: 'gradientWindow.handle2Pos', // 0x08F00006
|
|
292
|
+
149946377: 'gradientWindow.offsetX', // 0x08F00009
|
|
293
|
+
149946378: 'gradientWindow.offsetY', // 0x08F0000A
|
|
294
|
+
149946379: 'gradientWindow.softness', // 0x08F0000B
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ct-aware relabel for the polygon window (corrector type 6). The 0x08D000xx param ids are registered
|
|
298
|
+
// in PARAM_ID_MAP under LUM_MIX.PARAM_1–11 (control 'lumMix', correctorType 5) — but live R21 data
|
|
299
|
+
// (Session 4, §16a) shows they are the POLYGON-WINDOW geometry, appearing on a ct6 corrector. Rather
|
|
300
|
+
// than a sweeping LUM_MIX symbol rename (the LUM_MIX/ct5 pathway is woven through the generator +
|
|
301
|
+
// drp-format and the ct5↔ct6 id reuse was NOT measured to be safe), relabel ONLY when the id appears
|
|
302
|
+
// under ct6 — exactly the collision-fix pattern used for GRADIENT_NAME_BY_ID. The actual shape geometry
|
|
303
|
+
// is still surfaced structurally via node.params.polygonVertices/polygonMatrix regardless of this label.
|
|
304
|
+
const POLYGON_CORRECTOR_TYPE = 6;
|
|
305
|
+
const POLYGON_NAME_BY_ID = {
|
|
306
|
+
147849218: 'polygonWindow.param1', // 0x08D00002
|
|
307
|
+
147849220: 'polygonWindow.param2', // 0x08D00004
|
|
308
|
+
147849222: 'polygonWindow.param3', // 0x08D00006 (shape vertex ring — see node.params.polygonVertices)
|
|
309
|
+
147849223: 'polygonWindow.param4', // 0x08D00007 (soft-edge vertex ring)
|
|
310
|
+
147849224: 'polygonWindow.param5', // 0x08D00008 (soft-edge vertex ring)
|
|
311
|
+
147849225: 'polygonWindow.param6', // 0x08D00009
|
|
312
|
+
147849226: 'polygonWindow.param7', // 0x08D0000A
|
|
313
|
+
147849227: 'polygonWindow.param8', // 0x08D0000B
|
|
314
|
+
147849228: 'polygonWindow.param9', // 0x08D0000C
|
|
315
|
+
147849232: 'polygonWindow.param10', // 0x08D00010
|
|
316
|
+
147849233: 'polygonWindow.param11', // 0x08D00011
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// The per-vector "Center" values live in GRID_DATA_2 (0x86000607) as a PACKED float32
|
|
320
|
+
// array — structure F12{F1{<packed 7×float32>}} — which is NOT valid nested protobuf
|
|
321
|
+
// (the generic parser misreads the packed bytes), so read it from the raw F2 bytes.
|
|
322
|
+
// (RE'd 2026-06-22 — design note §16a, C.) Returns the 7 center floats (identity scale).
|
|
323
|
+
function decodeColorSliceCenters(f2Bytes) {
|
|
324
|
+
if (!Buffer.isBuffer(f2Bytes) && !(f2Bytes instanceof Uint8Array)) return null;
|
|
325
|
+
const buf = Buffer.from(f2Bytes);
|
|
326
|
+
// Walk length-delimited fields to descend F12 → F1, then read the packed float32s.
|
|
327
|
+
const findField = (start, end, target) => {
|
|
328
|
+
let p = start;
|
|
329
|
+
while (p < end) {
|
|
330
|
+
const tag = buf[p++]; const fnum = tag >>> 3; const wt = tag & 7;
|
|
331
|
+
if (wt === 2) {
|
|
332
|
+
let len = 0, s = 0, b;
|
|
333
|
+
do { b = buf[p++]; len |= (b & 0x7f) << s; s += 7; } while (b & 0x80);
|
|
334
|
+
const cs = p; p += len;
|
|
335
|
+
if (fnum === target) return [cs, cs + len];
|
|
336
|
+
} else if (wt === 0) { while (buf[p] & 0x80) p++; p++; }
|
|
337
|
+
else if (wt === 5) { p += 4; } else if (wt === 1) { p += 8; } else return null;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
};
|
|
341
|
+
const f12 = findField(0, buf.length, 12); if (!f12) return null;
|
|
342
|
+
const f1 = findField(f12[0], f12[1], 1); if (!f1) return null;
|
|
343
|
+
const [s, e] = f1; const n = Math.floor((e - s) / 4); const out = [];
|
|
344
|
+
for (let i = 0; i < n; i++) out.push(buf.readFloatLE(s + i * 4));
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// The grid sits ~11 levels deep but the generic parser stops at maxDepth=10, so the
|
|
349
|
+
// per-vector sub-messages arrive as raw Buffers — re-parse them on demand here.
|
|
350
|
+
function ensureParsed(x) {
|
|
351
|
+
if (Buffer.isBuffer(x) || x instanceof Uint8Array) {
|
|
352
|
+
try {
|
|
353
|
+
const p = new ProtobufParser(Buffer.from(x)).parse(40);
|
|
354
|
+
return p && p._fields && p._fields.length ? p : null;
|
|
355
|
+
} catch { return null; }
|
|
356
|
+
}
|
|
357
|
+
return x && typeof x === 'object' ? x : null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function decodeColorSliceGrid(gridMsg) {
|
|
361
|
+
const msg = ensureParsed(gridMsg);
|
|
362
|
+
if (!msg) return null;
|
|
363
|
+
const f24 = ensureParsed(msg.F24);
|
|
364
|
+
if (!f24 || f24.F1 === undefined) return null;
|
|
365
|
+
const vecs = Array.isArray(f24.F1) ? f24.F1 : [f24.F1];
|
|
366
|
+
return vecs.map((raw, i) => {
|
|
367
|
+
const v = ensureParsed(raw) || {};
|
|
368
|
+
return {
|
|
369
|
+
vector: COLORSLICE_VECTORS[i] || `vec${i}`,
|
|
370
|
+
enabled: v.F1 !== undefined ? v.F1 === 1 : true,
|
|
371
|
+
sat: typeof v.F3 === 'number' ? v.F3 : 1, // F3 = saturation (default 1.0)
|
|
372
|
+
hue: typeof v.F4 === 'number' ? -v.F4 : 0, // F4 = hue, stored negated → report UI value
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Byte-walk one HDR zone sub-message buffer → {name, exposure, saturation, colorBalanceX/Y}.
|
|
378
|
+
// Fields: F1(0x0a)=name str, F2(0x15)=exposure f32, F3(0x1d)=cbalY f32, F4(0x25)=cbalX f32,
|
|
379
|
+
// F7(0x3d)=saturation f32. Default exposure 0, saturation 1.
|
|
380
|
+
function parseHdrZone(raw) {
|
|
381
|
+
if (!Buffer.isBuffer(raw) && !(raw instanceof Uint8Array)) return null;
|
|
382
|
+
const buf = Buffer.from(raw);
|
|
383
|
+
const z = { name: null, exposure: 0, saturation: 1, colorBalanceX: 0, colorBalanceY: 0 };
|
|
384
|
+
let p = 0;
|
|
385
|
+
while (p < buf.length) {
|
|
386
|
+
const tag = buf[p++]; const fnum = tag >>> 3; const wt = tag & 7;
|
|
387
|
+
if (wt === 2) {
|
|
388
|
+
let len = 0, s = 0, b; do { b = buf[p++]; len |= (b & 0x7f) << s; s += 7; } while (b & 0x80);
|
|
389
|
+
if (fnum === 1) z.name = buf.toString('utf8', p, p + len);
|
|
390
|
+
p += len;
|
|
391
|
+
} else if (wt === 5) {
|
|
392
|
+
const f = buf.readFloatLE(p); p += 4;
|
|
393
|
+
if (fnum === 2) z.exposure = f;
|
|
394
|
+
else if (fnum === 3) z.colorBalanceY = f;
|
|
395
|
+
else if (fnum === 4) z.colorBalanceX = f;
|
|
396
|
+
else if (fnum === 7) z.saturation = f;
|
|
397
|
+
} else if (wt === 0) { while (buf[p] & 0x80) p++; p++; }
|
|
398
|
+
else if (wt === 1) { p += 8; }
|
|
399
|
+
else break;
|
|
400
|
+
}
|
|
401
|
+
return z.name ? z : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Tier-2: 3D-qualifier selection volume (0x0830002A) — byte-level RE'd 2026-07-02 off the
|
|
405
|
+
// qualifier-3d fixture (key stroke across the bars, 8 samples). Layout: value envelope
|
|
406
|
+
// { F5: <packed buffer> } where the buffer = 9 × uint64 BIG-endian header (fields 0-7
|
|
407
|
+
// semantics unconfirmed — observed [2,1,1,0x88,7,0,0x40,0xBA]; field 8 = SAMPLE COUNT)
|
|
408
|
+
// followed by count × 3 float32 LITTLE-endian samples (x, y, radius) — the keyer's
|
|
409
|
+
// sampled chroma-plane point cloud (stroke path; radius observed constant 0.015).
|
|
410
|
+
const QUALIFIER_3D_VOLUME_ID = 0x0830002a; // 137363498
|
|
411
|
+
function decodeQualifier3dVolume(env) {
|
|
412
|
+
const m = ensureParsed(env);
|
|
413
|
+
if (!m || !Buffer.isBuffer(m.F5)) return null;
|
|
414
|
+
const buf = m.F5;
|
|
415
|
+
if (buf.length < 72) return null;
|
|
416
|
+
const header = [];
|
|
417
|
+
for (let i = 0; i < 9; i++) header.push(Number(buf.readBigUInt64BE(i * 8)));
|
|
418
|
+
const count = header[8];
|
|
419
|
+
const samples = [];
|
|
420
|
+
for (let i = 0; i < count && 72 + (i + 1) * 12 <= buf.length; i++) {
|
|
421
|
+
const off = 72 + i * 12;
|
|
422
|
+
samples.push({ x: buf.readFloatLE(off), y: buf.readFloatLE(off + 4), radius: buf.readFloatLE(off + 8) });
|
|
423
|
+
}
|
|
424
|
+
return samples.length ? { header, count, samples } : null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Tier-2: decode the HDR ZONE_ADJUSTMENTS blob (0x86000305) into per-zone named params.
|
|
428
|
+
function decodeHdrZones(adjMsg) {
|
|
429
|
+
const msg = ensureParsed(adjMsg);
|
|
430
|
+
if (!msg) return null;
|
|
431
|
+
const f16 = ensureParsed(msg.F16);
|
|
432
|
+
if (!f16 || f16.F1 === undefined) return null;
|
|
433
|
+
const zones = Array.isArray(f16.F1) ? f16.F1 : [f16.F1];
|
|
434
|
+
return zones.map((raw) => parseHdrZone(raw)).filter(Boolean);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Node "Input Sizing" (RE'd 2026-06-22, harness §16a). Stored outside the F9 corrector list in a
|
|
438
|
+
// transform structure whose param entries have a fixed signature in the decompressed body:
|
|
439
|
+
// 1a 0d 08 <kx> 80 c0 <kg> 01 12 05 0d <f32>
|
|
440
|
+
// where <kg>=group (0x81=A pan/tilt/zoom/rotate/width/height, 0x85=B pitch/yaw) and <kx>=param index.
|
|
441
|
+
// Pan/Tilt are normalized (UI px / frame dim); Zoom/Width/Height = direct mult; Rotate = degrees.
|
|
442
|
+
const SIZING_MAP = {
|
|
443
|
+
'81:81': { channel: 'width', id: 0x10310001 },
|
|
444
|
+
'82:81': { channel: 'height', id: 0x10310002 },
|
|
445
|
+
'83:81': { channel: 'zoom', id: 0x10310003 },
|
|
446
|
+
'84:81': { channel: 'rotate', id: 0x10310004 },
|
|
447
|
+
'85:81': { channel: 'pan', id: 0x10310005, normalized: 'width' },
|
|
448
|
+
'86:81': { channel: 'tilt', id: 0x10310006, normalized: 'height' },
|
|
449
|
+
'81:85': { channel: 'pitch', id: 0x10B10001 },
|
|
450
|
+
'82:85': { channel: 'yaw', id: 0x10B10002 },
|
|
451
|
+
};
|
|
452
|
+
function decodeNodeSizing(buf) {
|
|
453
|
+
if (!Buffer.isBuffer(buf)) return null;
|
|
454
|
+
const out = [];
|
|
455
|
+
for (let i = 0; i + 15 <= buf.length; i++) {
|
|
456
|
+
// 1a 0d 08 <kx> 80 c0 <kg> 01 12 05 0d <f32>
|
|
457
|
+
if (buf[i] !== 0x1a || buf[i + 1] !== 0x0d || buf[i + 2] !== 0x08) continue;
|
|
458
|
+
if (buf[i + 4] !== 0x80 || buf[i + 5] !== 0xc0 || buf[i + 7] !== 0x01) continue;
|
|
459
|
+
if (buf[i + 8] !== 0x12 || buf[i + 9] !== 0x05 || buf[i + 10] !== 0x0d) continue;
|
|
460
|
+
const kx = buf[i + 3], kg = buf[i + 6];
|
|
461
|
+
const info = SIZING_MAP[`${kx.toString(16)}:${kg.toString(16)}`];
|
|
462
|
+
if (!info) continue;
|
|
463
|
+
out.push({ ...info, value: buf.readFloatLE(i + 11) });
|
|
464
|
+
}
|
|
465
|
+
return out.length ? out : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Curve spline point: a sub-message `0d<f32 x> 15<f32 y>` — F1(fixed32)=x, F2(fixed32)=y.
|
|
469
|
+
// COORDINATE SPACE DIFFERS BY CURVE TYPE (RE'd 2026-06-22):
|
|
470
|
+
// - Custom Y/R/G/B (0x86000506–509): ~0–1024 (10-bit), tangent handles beyond [0,1024].
|
|
471
|
+
// - HSL curves (0x86000400–405): NORMALIZED — x∈[-1,1] (hue axis, periodic → wrap handles beyond ±1),
|
|
472
|
+
// y∈[0,1] with 0.5=neutral. (Hue-vs-Hue: y = 0.5 − HueRotate°/360.)
|
|
473
|
+
// The decoder is space-agnostic — it just reads the raw f32 x/y; the consumer interprets per curve.
|
|
474
|
+
function parseSplinePoint(raw) {
|
|
475
|
+
if (!Buffer.isBuffer(raw) && !(raw instanceof Uint8Array)) return null;
|
|
476
|
+
const buf = Buffer.from(raw);
|
|
477
|
+
let p = 0; let x, y;
|
|
478
|
+
while (p < buf.length) {
|
|
479
|
+
const tag = buf[p++]; const fnum = tag >>> 3; const wt = tag & 7;
|
|
480
|
+
if (wt === 5) { const f = buf.readFloatLE(p); p += 4; if (fnum === 1) x = f; else if (fnum === 2) y = f; }
|
|
481
|
+
else if (wt === 2) { let len = 0, s = 0, b; do { b = buf[p++]; len |= (b & 0x7f) << s; s += 7; } while (b & 0x80); p += len; }
|
|
482
|
+
else if (wt === 0) { while (buf[p] & 0x80) p++; p++; }
|
|
483
|
+
else if (wt === 1) { p += 8; } else break;
|
|
484
|
+
}
|
|
485
|
+
return (x !== undefined || y !== undefined) ? { x: x ?? 0, y: y ?? 0 } : null;
|
|
486
|
+
}
|
|
487
|
+
// Spline data params (custom curves 0x86000506–509, HSL curves 0x86000400–405) store points in F8.F1[].
|
|
488
|
+
const SPLINE_POINT_IDS = new Set([
|
|
489
|
+
0x86000506, 0x86000507, 0x86000508, 0x86000509, // custom Y/R/G/B
|
|
490
|
+
0x86000400, 0x86000401, 0x86000402, 0x86000403, 0x86000404, 0x86000405, // HSL curves
|
|
491
|
+
]);
|
|
492
|
+
function decodeSplinePoints(splineMsg) {
|
|
493
|
+
const msg = ensureParsed(splineMsg);
|
|
494
|
+
if (!msg) return null;
|
|
495
|
+
const f8 = ensureParsed(msg.F8);
|
|
496
|
+
if (!f8 || f8.F1 === undefined) return null;
|
|
497
|
+
const pts = Array.isArray(f8.F1) ? f8.F1 : [f8.F1];
|
|
498
|
+
return pts.map(parseSplinePoint).filter(Boolean);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Structural containers found in the completeness sweep (RE'd 2026-06-22 off the
|
|
502
|
+
// gradient-window fixture, which itself is a live BARS capture):
|
|
503
|
+
// 0x0830006f (qualifier corrector, ct2): value envelope = { F2: <varint> } — an
|
|
504
|
+
// internal qualifier mode flag (registry MODE_FLAG; observed 4). Lift the varint.
|
|
505
|
+
// 0x88f0000d (gradient corrector, ct65554): value envelope = { F10: { F1..F9 } } — a
|
|
506
|
+
// row-major 3×3 matrix (identity by default). Lift the 9 cells into an array.
|
|
507
|
+
const QUALIFIER_MODE_FLAG_ID = 137363567; // 0x0830006f
|
|
508
|
+
const GRADIENT_MATRIX_ID = 2297430029; // 0x88f0000d
|
|
509
|
+
// The gradient window (0x88f0000d), polygon shape (0x88d00014) and LINEAR-window softness mask
|
|
510
|
+
// (0x8870001e, ct3) all carry the same structural F10 3×3 matrix container (identity default).
|
|
511
|
+
// RE'd 2026-06-22 (gradient + polygon + linear-softness fixtures).
|
|
512
|
+
const LINEAR_SOFT_MATRIX_ID = 2289041438; // 0x8870001e
|
|
513
|
+
const STRUCT_MATRIX_NAMES = {
|
|
514
|
+
[GRADIENT_MATRIX_ID]: { name: 'gradientWindow.matrix', key: 'gradientMatrix' },
|
|
515
|
+
2295332884: { name: 'polygonShape.matrix', key: 'polygonMatrix' }, // 0x88d00014
|
|
516
|
+
[LINEAR_SOFT_MATRIX_ID]: { name: 'window.softMatrix', key: 'softMatrix' }, // 0x8870001e (ct3)
|
|
517
|
+
};
|
|
518
|
+
const STRUCT_MATRIX_IDS = new Set(Object.keys(STRUCT_MATRIX_NAMES).map(Number));
|
|
519
|
+
function decodeStructMatrix(env) {
|
|
520
|
+
const m = ensureParsed(env);
|
|
521
|
+
if (!m) return null;
|
|
522
|
+
const f10 = ensureParsed(m.F10);
|
|
523
|
+
if (!f10) return null;
|
|
524
|
+
const out = [];
|
|
525
|
+
for (let i = 1; i <= 9; i++) {
|
|
526
|
+
const v = f10[`F${i}`];
|
|
527
|
+
out.push(typeof v === 'number' ? v : 0);
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Polygon shape vertices — RE'd 2026-06-22 on DRX_CALIB (drew a 4-vertex polygon window). The shape
|
|
533
|
+
// lives under CORRECTOR TYPE 6 (NOT the registry's claimed 0x08B0/ct5; that POLYGON_WINDOW block is
|
|
534
|
+
// UNVALIDATED). Vertices are F9.F1[] point sub-messages — the SAME `0d<f32 x>15<f32 y>` codec as curve
|
|
535
|
+
// splines but under F9 (splines use F8) — in FRAME-PIXELS from center. IDs 0x08D00006/07/08 each carry
|
|
536
|
+
// the vertex ring (shape + inner/outer soft edges). These IDs are currently mislabeled "lumMix.*" in
|
|
537
|
+
// the registry (a deeper rename is deferred — see ledger). The vertices are surfaced additively here.
|
|
538
|
+
const POLYGON_VERTEX_IDS = new Set([0x08d00006, 0x08d00007, 0x08d00008]);
|
|
539
|
+
const LINEAR_SOFT_BBOX_ID = 141557780; // 0x08700014 — linear-window softness-mask 4-corner ring (ct3)
|
|
540
|
+
function decodePolygonVertices(env) {
|
|
541
|
+
const m = ensureParsed(env);
|
|
542
|
+
if (!m) return null;
|
|
543
|
+
const f9 = ensureParsed(m.F9);
|
|
544
|
+
if (!f9 || f9.F1 === undefined) return null;
|
|
545
|
+
const pts = Array.isArray(f9.F1) ? f9.F1 : [f9.F1];
|
|
546
|
+
return pts.map(parseSplinePoint).filter(Boolean);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Color Warper (chroma warp) pin list — RE'd LIVE 2026-06-22 on DRX_CALIB (Resolve 21, harness §16a).
|
|
550
|
+
// The registry's old mesh-vertex model (0x86000121 triplets) is WRONG for R21; the real shape is a pin
|
|
551
|
+
// list at 0x86000138: value envelope = { F27: { F1: [ <pin>, … ] } }. Each pin sub-message:
|
|
552
|
+
// F1=id, F2/F3=source chroma XY, F4/F5=dest chroma XY, F6=chromaRange, F7=exposure (skip-if-0),
|
|
553
|
+
// F8=tonalLow, F9=tonalHigh, F10=tonalPivot. Floats are identity-scale vs the UI Pin controls.
|
|
554
|
+
const COLOR_WARPER_PINS_ID = 0x86000138; // 2248147256
|
|
555
|
+
function parseColorWarperPin(raw) {
|
|
556
|
+
const m = ensureParsed(raw);
|
|
557
|
+
if (!m) return null;
|
|
558
|
+
const num = (v) => (typeof v === 'number' ? v : undefined);
|
|
559
|
+
const pin = {
|
|
560
|
+
id: num(m.F1),
|
|
561
|
+
srcX: num(m.F2), srcY: num(m.F3),
|
|
562
|
+
dstX: num(m.F4), dstY: num(m.F5),
|
|
563
|
+
chromaRange: num(m.F6),
|
|
564
|
+
exposure: num(m.F7) ?? 0,
|
|
565
|
+
tonalLow: num(m.F8), tonalHigh: num(m.F9), tonalPivot: num(m.F10),
|
|
566
|
+
};
|
|
567
|
+
// A real pin always carries the chroma source/dest coords.
|
|
568
|
+
return (pin.srcX !== undefined && pin.dstX !== undefined) ? pin : null;
|
|
569
|
+
}
|
|
570
|
+
function decodeColorWarperPins(env) {
|
|
571
|
+
const m = ensureParsed(env);
|
|
572
|
+
if (!m) return null;
|
|
573
|
+
const f27 = ensureParsed(m.F27);
|
|
574
|
+
if (!f27 || f27.F1 === undefined) return null;
|
|
575
|
+
const pins = Array.isArray(f27.F1) ? f27.F1 : [f27.F1];
|
|
576
|
+
return pins.map(parseColorWarperPin).filter(Boolean);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Keyframe tracks — RE'd + validated LIVE 2026-06-22 (§16a OPEN ITEM 2, 3-keyframe sweep). When a corrector
|
|
580
|
+
// is keyframed its F6 is a REPEATED field: one block per keyframe. Each block = { F1: timeUnits, F2: {F3:[params]} }
|
|
581
|
+
// where timeUnits = frame × 2 (half-frame/field units; F1 is ABSENT on block 0 = frame 0). Validated:
|
|
582
|
+
// frames 0/60/119 → F1 0/120/238, matte param 0xc30001d → 0.40/1.10/1.70. Builds paramId → [{frame,value}].
|
|
583
|
+
function buildKeyframeTracks(f6Blocks) {
|
|
584
|
+
const tracks = {};
|
|
585
|
+
for (const raw of f6Blocks) {
|
|
586
|
+
const b = ensureParsed(raw);
|
|
587
|
+
if (!b) continue;
|
|
588
|
+
const frame = typeof b.F1 === 'number' ? b.F1 / 2 : 0;
|
|
589
|
+
const cont = ensureParsed(b.F2);
|
|
590
|
+
if (!cont || cont.F3 === undefined) continue;
|
|
591
|
+
const params = Array.isArray(cont.F3) ? cont.F3 : [cont.F3];
|
|
592
|
+
for (const p of params) {
|
|
593
|
+
const pp = ensureParsed(p);
|
|
594
|
+
if (!pp || pp.F1 === undefined) continue;
|
|
595
|
+
let v = pp.F2;
|
|
596
|
+
if (v && typeof v === 'object' && v.F1 !== undefined) v = v.F1;
|
|
597
|
+
if (typeof v !== 'number') continue;
|
|
598
|
+
(tracks[pp.F1] || (tracks[pp.F1] = [])).push({ frame, value: v });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const k of Object.keys(tracks)) tracks[k].sort((a, b) => a.frame - b.frame);
|
|
602
|
+
return tracks;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Extract node data from parsed protobuf
|
|
607
|
+
* @param {Buffer} [rawBody] decompressed body — enables the node-Sizing body-scan (single-node graphs).
|
|
608
|
+
*/
|
|
609
|
+
function extractNodes(parsed, rawBody) {
|
|
610
|
+
const nodes = [];
|
|
611
|
+
|
|
612
|
+
if (!parsed.F1 || !parsed.F1.F7) return nodes;
|
|
613
|
+
|
|
614
|
+
const nodeFields = Array.isArray(parsed.F1.F7) ? parsed.F1.F7 : [parsed.F1.F7];
|
|
615
|
+
|
|
616
|
+
for (const nodeData of nodeFields) {
|
|
617
|
+
if (!nodeData._fields) continue;
|
|
618
|
+
|
|
619
|
+
const node = {
|
|
620
|
+
id: nodeData.F1,
|
|
621
|
+
index: nodeData.F2,
|
|
622
|
+
xPos: nodeData.F4,
|
|
623
|
+
yPos: nodeData.F5,
|
|
624
|
+
label: extractStringField(nodeData, 6),
|
|
625
|
+
// Node color — RE'd LIVE 2026-06-22 (node-convention/provenance spec). Stored in node field F15 as a
|
|
626
|
+
// plain string "ClipColor<Name>" (e.g. ClipColorBlue); absent = no color. Surface the bare color name.
|
|
627
|
+
color: (() => { const s = extractStringField(nodeData, 15); return s ? s.replace(/^ClipColor/, '') : null; })(),
|
|
628
|
+
enabled: nodeData.F7 === 1,
|
|
629
|
+
keyframed: false, // set true when a corrector carries a repeated F6 (keyframe relocation, §16a OPEN ITEM 2)
|
|
630
|
+
correctors: [],
|
|
631
|
+
// Structured parameters for easy merging
|
|
632
|
+
params: {
|
|
633
|
+
lift: { r: 0, g: 0, b: 0, master: 0 },
|
|
634
|
+
gamma: { r: 0, g: 0, b: 0, master: 0 },
|
|
635
|
+
gain: { r: 1, g: 1, b: 1, master: 1 },
|
|
636
|
+
offset: { r: 0, g: 0, b: 0 },
|
|
637
|
+
saturation: 50, // Resolve Primaries: 0-100, unity=50
|
|
638
|
+
contrast: 1.0,
|
|
639
|
+
pivot: 0.435,
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// OFX/ResolveFX tool list (node field F10) — plugin params are SELF-DESCRIBING on
|
|
644
|
+
// the wire (name string + float64/string value; enums are label strings), so no
|
|
645
|
+
// per-plugin registry is needed to read them. Verified against real project grades
|
|
646
|
+
// 2026-07-03 (filmgrain/CST/acestransform; 5,977 instances decoded by name). Values
|
|
647
|
+
// are surfaced RAW — float scaling vs the UI is NOT panel-confirmed yet, so treat
|
|
648
|
+
// per valueFidelity until a capture-sweep lands.
|
|
649
|
+
if (nodeData.F10) {
|
|
650
|
+
try {
|
|
651
|
+
// The generic parser recursively converts nested messages to {_fields} objects;
|
|
652
|
+
// extract-ofx-params wants the raw entry BYTES — recover them from rawValue.
|
|
653
|
+
let toolList = nodeData.F10;
|
|
654
|
+
if (toolList && toolList._fields) {
|
|
655
|
+
toolList = toolList._fields
|
|
656
|
+
.filter((f) => f.fieldNum === 1 && f.wireType === 2)
|
|
657
|
+
.map((f) => (Buffer.isBuffer(f.value) ? f.value : f.rawValue))
|
|
658
|
+
.filter(Buffer.isBuffer);
|
|
659
|
+
}
|
|
660
|
+
const ofxTools = require('./extract-ofx-params').extractOFXTools(toolList);
|
|
661
|
+
if (ofxTools.length) node.ofxTools = ofxTools;
|
|
662
|
+
} catch { /* malformed tool list — leave node.ofxTools unset */ }
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Extract correctors from Field 9
|
|
666
|
+
if (nodeData.F9 && nodeData.F9._fields) {
|
|
667
|
+
const correctorFields = nodeData.F9.F1 ? (Array.isArray(nodeData.F9.F1) ? nodeData.F9.F1 : [nodeData.F9.F1]) : [];
|
|
668
|
+
|
|
669
|
+
for (const corrector of correctorFields) {
|
|
670
|
+
if (!corrector._fields) continue;
|
|
671
|
+
|
|
672
|
+
const correctorType = corrector.F1;
|
|
673
|
+
const correctorInfo = {
|
|
674
|
+
type: correctorType,
|
|
675
|
+
typeName: CORRECTOR_TYPES[correctorType] || `Type${correctorType}`,
|
|
676
|
+
enabled: corrector.F3 === 1,
|
|
677
|
+
parameters: [],
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Keyframed grades relocate each corrector's values: F6 becomes a REPEATED field (a static base
|
|
681
|
+
// block + a keyframe-track block, the latter carrying an extra F1 marker). RE'd live 2026-06-22
|
|
682
|
+
// (§16a OPEN ITEM 2). Without this the static path (corrector.F6.F2) was undefined → 0 params for
|
|
683
|
+
// a keyframed grade. Read params from the base block (the one carrying F2.F3) and flag the node.
|
|
684
|
+
let f6 = corrector.F6;
|
|
685
|
+
if (Array.isArray(f6)) {
|
|
686
|
+
node.keyframed = true;
|
|
687
|
+
// Full keyframe-track decode: paramId → [{frame, value}] across all keyframe blocks.
|
|
688
|
+
const tracks = buildKeyframeTracks(f6);
|
|
689
|
+
const named = {};
|
|
690
|
+
for (const [pid, pts] of Object.entries(tracks)) {
|
|
691
|
+
if (pts.length < 2) continue; // a single point isn't an animation
|
|
692
|
+
const info = PARAM_IDS[pid];
|
|
693
|
+
named[info ? info.name : `param_${pid}`] = pts;
|
|
694
|
+
}
|
|
695
|
+
if (Object.keys(named).length) {
|
|
696
|
+
(node.keyframes || (node.keyframes = [])).push({ correctorType, tracks: named });
|
|
697
|
+
}
|
|
698
|
+
// Static-snapshot base block (the first block / frame 0) for the normal param path.
|
|
699
|
+
f6 = f6.find((b) => b && b.F2 && b.F2.F3 !== undefined) || f6[0];
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Extract parameters from Field 6 -> Field 2 -> Field 3[]
|
|
703
|
+
if (f6 && f6.F2) {
|
|
704
|
+
const paramContainer = f6.F2;
|
|
705
|
+
const paramFields = paramContainer.F3 ? (Array.isArray(paramContainer.F3) ? paramContainer.F3 : [paramContainer.F3]) : [];
|
|
706
|
+
|
|
707
|
+
for (const param of paramFields) {
|
|
708
|
+
if (!param._fields) continue;
|
|
709
|
+
|
|
710
|
+
const paramId = param.F1;
|
|
711
|
+
let paramValue = param.F2;
|
|
712
|
+
|
|
713
|
+
// Tier-2: ColorSlice per-vector grid (0x86000606) — decode the F24
|
|
714
|
+
// sub-messages into named per-vector params instead of an opaque NaN.
|
|
715
|
+
if (paramId === COLORSLICE_VECTOR_DATA_ID) {
|
|
716
|
+
const grid = decodeColorSliceGrid(param.F2);
|
|
717
|
+
if (grid && grid.length) {
|
|
718
|
+
node.params.colorSlice = grid;
|
|
719
|
+
for (const vec of grid) {
|
|
720
|
+
correctorInfo.parameters.push({ id: paramId, value: vec.enabled ? 1 : 0, name: `colorSlice.${vec.vector}.enabled`, vector: vec.vector, field: 'enabled' });
|
|
721
|
+
correctorInfo.parameters.push({ id: paramId, value: vec.sat, name: `colorSlice.${vec.vector}.sat`, vector: vec.vector, field: 'sat' });
|
|
722
|
+
correctorInfo.parameters.push({ id: paramId, value: vec.hue, name: `colorSlice.${vec.vector}.hue`, vector: vec.vector, field: 'hue' });
|
|
723
|
+
}
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Tier-2: ColorSlice per-vector "Center" values — a packed float32 array
|
|
729
|
+
// in GRID_DATA_2 (0x86000607), read from the raw F2 bytes (see §16a, C).
|
|
730
|
+
if (paramId === COLORSLICE_GRID2_ID) {
|
|
731
|
+
const f2 = param._fields && param._fields.find((f) => f.fieldNum === 2);
|
|
732
|
+
const centers = f2 ? decodeColorSliceCenters(f2.rawValue) : null;
|
|
733
|
+
if (centers && centers.length) {
|
|
734
|
+
if (!Array.isArray(node.params.colorSlice)) {
|
|
735
|
+
node.params.colorSlice = centers.map((_, i) => ({ vector: COLORSLICE_VECTORS[i] || `vec${i}` }));
|
|
736
|
+
}
|
|
737
|
+
centers.forEach((c, i) => {
|
|
738
|
+
if (node.params.colorSlice[i]) node.params.colorSlice[i].center = c;
|
|
739
|
+
correctorInfo.parameters.push({ id: paramId, value: c, name: `colorSlice.${COLORSLICE_VECTORS[i] || 'vec' + i}.center`, vector: COLORSLICE_VECTORS[i], field: 'center' });
|
|
740
|
+
});
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Tier-2: HDR zone adjustments (0x86000305) — lift the nested per-zone
|
|
746
|
+
// exposure/saturation/color-balance out of the opaque blob into named params.
|
|
747
|
+
if (paramId === HDR_ZONE_ADJUSTMENTS_ID) {
|
|
748
|
+
const zones = decodeHdrZones(param.F2);
|
|
749
|
+
if (zones && zones.length) {
|
|
750
|
+
node.params.hdrZones = zones;
|
|
751
|
+
for (const z of zones) {
|
|
752
|
+
const key = z.name.toLowerCase();
|
|
753
|
+
correctorInfo.parameters.push({ id: paramId, value: z.exposure, name: `hdrZone.${key}.exposure`, zone: z.name, field: 'exposure' });
|
|
754
|
+
correctorInfo.parameters.push({ id: paramId, value: z.saturation, name: `hdrZone.${key}.saturation`, zone: z.name, field: 'saturation' });
|
|
755
|
+
if (z.colorBalanceX) correctorInfo.parameters.push({ id: paramId, value: z.colorBalanceX, name: `hdrZone.${key}.colorBalanceX`, zone: z.name, field: 'colorBalanceX' });
|
|
756
|
+
if (z.colorBalanceY) correctorInfo.parameters.push({ id: paramId, value: z.colorBalanceY, name: `hdrZone.${key}.colorBalanceY`, zone: z.name, field: 'colorBalanceY' });
|
|
757
|
+
}
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Tier-2: 3D-qualifier selection volume (0x0830002A) — lift the packed sample
|
|
763
|
+
// point cloud (see decodeQualifier3dVolume) ADDITIVELY onto node.params.
|
|
764
|
+
if (paramId === QUALIFIER_3D_VOLUME_ID && !node.params.qualifier3d) {
|
|
765
|
+
const vol = decodeQualifier3dVolume(param.F2);
|
|
766
|
+
if (vol) {
|
|
767
|
+
node.params.qualifier3d = vol;
|
|
768
|
+
correctorInfo.parameters.push({ id: paramId, value: vol.count, name: 'qualifier.volume3dSampleCount' });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Blob RE: curve spline points (custom + HSL curves) — lift F8.F1[] point
|
|
773
|
+
// sub-messages into {x,y} arrays (coords in ~0–1024 space) on node.params.curvePoints.
|
|
774
|
+
// ADDITIVE: don't touch the param's own value (the extractCustomCurves/HSL secondary
|
|
775
|
+
// decoders still read param.value) — just expose the decoded points alongside.
|
|
776
|
+
if (SPLINE_POINT_IDS.has(paramId)) {
|
|
777
|
+
const pts = decodeSplinePoints(param.F2);
|
|
778
|
+
if (pts && pts.length) {
|
|
779
|
+
const info = PARAM_IDS[paramId];
|
|
780
|
+
const key = info ? info.name : `spline_${paramId}`;
|
|
781
|
+
if (!node.params.curvePoints) node.params.curvePoints = {};
|
|
782
|
+
node.params.curvePoints[key] = pts;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Tier-2: Color Warper chroma-warp pin list (0x86000138) — lift F2.F27.F1[] pins into
|
|
787
|
+
// named colorWarper.pin<N>.<field> params + node.params.colorWarper (RE'd live, §16a).
|
|
788
|
+
if (paramId === COLOR_WARPER_PINS_ID) {
|
|
789
|
+
const pins = decodeColorWarperPins(param.F2);
|
|
790
|
+
if (pins && pins.length) {
|
|
791
|
+
node.params.colorWarper = pins;
|
|
792
|
+
pins.forEach((pin, pi) => {
|
|
793
|
+
for (const [field, val] of Object.entries(pin)) {
|
|
794
|
+
if (val !== undefined) {
|
|
795
|
+
correctorInfo.parameters.push({ id: paramId, value: val, name: `colorWarper.pin${pi}.${field}`, pin: pi, field });
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Structural container: 3×3 matrix (gradient window 0x88f0000d ct65554, polygon shape
|
|
804
|
+
// 0x88d00014 ct6) — lift F10's 9 cells into an array instead of an opaque unknown_ object.
|
|
805
|
+
if (STRUCT_MATRIX_IDS.has(paramId)) {
|
|
806
|
+
const mtx = decodeStructMatrix(param.F2);
|
|
807
|
+
if (mtx) {
|
|
808
|
+
const meta = STRUCT_MATRIX_NAMES[paramId];
|
|
809
|
+
node.params[meta.key] = mtx;
|
|
810
|
+
correctorInfo.parameters.push({ id: paramId, value: mtx, name: meta.name });
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Polygon shape vertices (additive): lift the F9.F1[] point ring into node.params.polygonVertices.
|
|
815
|
+
if (POLYGON_VERTEX_IDS.has(paramId) && !node.params.polygonVertices) {
|
|
816
|
+
const verts = decodePolygonVertices(param.F2);
|
|
817
|
+
if (verts && verts.length) node.params.polygonVertices = verts;
|
|
818
|
+
}
|
|
819
|
+
// Linear-window softness mask bbox (0x08700014, ct3): same F9.F1[] point ring (the 4 corners).
|
|
820
|
+
if (paramId === LINEAR_SOFT_BBOX_ID && !node.params.softVertices) {
|
|
821
|
+
const verts = decodePolygonVertices(param.F2);
|
|
822
|
+
if (verts && verts.length) node.params.softVertices = verts;
|
|
823
|
+
}
|
|
824
|
+
// Structural container: qualifier internal mode flag (0x0830006f, ct2) —
|
|
825
|
+
// lift the varint out of the { F2 } envelope.
|
|
826
|
+
if (paramId === QUALIFIER_MODE_FLAG_ID && correctorType === 2) {
|
|
827
|
+
const mf = param.F2 && typeof param.F2.F2 === 'number' ? param.F2.F2 : null;
|
|
828
|
+
correctorInfo.parameters.push({ id: paramId, value: mf, name: 'qualifier.modeFlag' });
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (paramValue && paramValue.F1 !== undefined) {
|
|
833
|
+
paramValue = paramValue.F1;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const paramInfo = PARAM_IDS[paramId];
|
|
837
|
+
// ct-aware override: gradient-window params collide with sat-vs-sat, and polygon-window
|
|
838
|
+
// geometry (ct6) is mis-registered under LUM_MIX (ct5) — relabel by corrector type.
|
|
839
|
+
const ctName =
|
|
840
|
+
(correctorType === GRADIENT_CORRECTOR_TYPE && GRADIENT_NAME_BY_ID[paramId]) ||
|
|
841
|
+
(correctorType === POLYGON_CORRECTOR_TYPE && POLYGON_NAME_BY_ID[paramId]) ||
|
|
842
|
+
null;
|
|
843
|
+
correctorInfo.parameters.push({
|
|
844
|
+
id: paramId,
|
|
845
|
+
value: paramValue,
|
|
846
|
+
name: ctName || (paramInfo ? paramInfo.name : `unknown_${paramId}`),
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// Map to structured params
|
|
850
|
+
if (paramInfo) {
|
|
851
|
+
if (paramInfo.control === 'lift' && paramInfo.channel) {
|
|
852
|
+
node.params.lift[paramInfo.channel] = paramValue;
|
|
853
|
+
} else if (paramInfo.control === 'gamma' && paramInfo.channel) {
|
|
854
|
+
node.params.gamma[paramInfo.channel] = paramValue;
|
|
855
|
+
} else if (paramInfo.control === 'gain' && paramInfo.channel) {
|
|
856
|
+
node.params.gain[paramInfo.channel] = paramValue;
|
|
857
|
+
} else if (paramInfo.control === 'offset' && paramInfo.channel) {
|
|
858
|
+
node.params.offset[paramInfo.channel] = paramValue;
|
|
859
|
+
} else if (paramInfo.control === 'saturation') {
|
|
860
|
+
node.params.saturation = paramValue;
|
|
861
|
+
} else if (paramInfo.control === 'contrast') {
|
|
862
|
+
node.params.contrast = paramValue;
|
|
863
|
+
} else if (paramInfo.control === 'pivot') {
|
|
864
|
+
node.params.pivot = paramValue;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
node.correctors.push(correctorInfo);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
nodes.push(node);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Node "Input Sizing" lives outside the corrector list; scan the raw body for its signature.
|
|
878
|
+
// The scan can't attribute entries to a specific node, so only attach for single-node graphs.
|
|
879
|
+
if (rawBody && nodes.length === 1) {
|
|
880
|
+
const sizing = decodeNodeSizing(rawBody);
|
|
881
|
+
if (sizing && sizing.length) {
|
|
882
|
+
const node = nodes[0];
|
|
883
|
+
node.params.sizing = {};
|
|
884
|
+
for (const s of sizing) {
|
|
885
|
+
node.params.sizing[s.channel] = s.value;
|
|
886
|
+
node.correctors[0]?.parameters.push({ id: s.id, value: s.value, name: `sizing.${s.channel}`, channel: s.channel, normalized: s.normalized || null });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return nodes;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Extract connections from parsed protobuf
|
|
896
|
+
*/
|
|
897
|
+
function extractConnections(parsed) {
|
|
898
|
+
const connections = [];
|
|
899
|
+
|
|
900
|
+
if (!parsed.F1 || !parsed.F1.F8) return connections;
|
|
901
|
+
|
|
902
|
+
const connFields = Array.isArray(parsed.F1.F8) ? parsed.F1.F8 : [parsed.F1.F8];
|
|
903
|
+
|
|
904
|
+
for (const conn of connFields) {
|
|
905
|
+
if (!conn._fields) continue;
|
|
906
|
+
|
|
907
|
+
connections.push({
|
|
908
|
+
from: conn.F1,
|
|
909
|
+
to: conn.F3,
|
|
910
|
+
sourcePort: conn.F5,
|
|
911
|
+
targetPort: conn.F6,
|
|
912
|
+
index: conn.F7,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return connections;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Extract metadata from parsed protobuf
|
|
921
|
+
*/
|
|
922
|
+
function extractMetadata(parsed) {
|
|
923
|
+
const metadata = {
|
|
924
|
+
version: null,
|
|
925
|
+
width: null,
|
|
926
|
+
height: null,
|
|
927
|
+
timestamp: null,
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
if (parsed.F1) {
|
|
931
|
+
metadata.version = parsed.F1.F1;
|
|
932
|
+
if (parsed.F1.F3) {
|
|
933
|
+
metadata.width = parsed.F1.F3.F1;
|
|
934
|
+
metadata.height = parsed.F1.F3.F2;
|
|
935
|
+
}
|
|
936
|
+
metadata.timestamp = parsed.F1.F12;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return metadata;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Parse a DRX file and extract its structure
|
|
944
|
+
*
|
|
945
|
+
* @param {string} filePath - Path to the DRX file
|
|
946
|
+
* @returns {Promise<Object>} - Parsed DRX structure with nodes, connections, metadata
|
|
947
|
+
*/
|
|
948
|
+
async function parseDRX(filePath) {
|
|
949
|
+
const xml = fs.readFileSync(filePath, 'utf-8');
|
|
950
|
+
return parseDRXContent(xml);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Parse DRX XML content (without reading from file)
|
|
955
|
+
*
|
|
956
|
+
* @param {string} xml - DRX XML content
|
|
957
|
+
* @returns {Promise<Object>} - Parsed DRX structure
|
|
958
|
+
*/
|
|
959
|
+
async function parseDRXContent(xml) {
|
|
960
|
+
// P4A: Check LRU cache by SHA-256 of input XML
|
|
961
|
+
const cacheKey = crypto.createHash('sha256').update(xml).digest('hex');
|
|
962
|
+
const cached = drxCacheGet(cacheKey);
|
|
963
|
+
if (cached) {
|
|
964
|
+
return cached;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Extract XML metadata
|
|
968
|
+
const labelMatch = xml.match(/<Label>([^<]*)<\/Label>/);
|
|
969
|
+
const widthMatch = xml.match(/<Width>(\d+)<\/Width>/);
|
|
970
|
+
const heightMatch = xml.match(/<Height>(\d+)<\/Height>/);
|
|
971
|
+
const srcHintMatch = xml.match(/<SrcHint>([^<]*)<\/SrcHint>/);
|
|
972
|
+
const recTCMatch = xml.match(/<RecTC>([^<]*)<\/RecTC>/);
|
|
973
|
+
const srcTCMatch = xml.match(/<SrcTC>([^<]*)<\/SrcTC>/);
|
|
974
|
+
|
|
975
|
+
// Extract pTrackVer section for preservation during merge
|
|
976
|
+
const pTrackVerMatch = xml.match(/<pTrackVer>([\s\S]*?)<\/pTrackVer>/);
|
|
977
|
+
const pTrackVerXml = pTrackVerMatch ? pTrackVerMatch[0] : null;
|
|
978
|
+
|
|
979
|
+
// Extract body
|
|
980
|
+
const bodyMatch = xml.match(/<Body>([^<]+)<\/Body>/);
|
|
981
|
+
if (!bodyMatch) {
|
|
982
|
+
throw new Error('No Body found in DRX content');
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const hexBody = bodyMatch[1];
|
|
986
|
+
const buf = Buffer.from(hexBody, 'hex');
|
|
987
|
+
|
|
988
|
+
// Verify magic byte. 0x81 = zstd-compressed body; 0x80 = STORED (uncompressed
|
|
989
|
+
// protobuf follows directly) — found in real project exports 2026-07-03 (~10% of
|
|
990
|
+
// grades in large projects; presumably Resolve skips compression for small bodies).
|
|
991
|
+
if (buf[0] !== 0x81 && buf[0] !== 0x80) {
|
|
992
|
+
throw new Error(`Invalid DRX magic byte: 0x${buf[0].toString(16)}`);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Decompress (or use the stored bytes directly for 0x80)
|
|
996
|
+
const decompressed = buf[0] === 0x80
|
|
997
|
+
? buf.slice(1)
|
|
998
|
+
: Buffer.from((await getZstdCodec()).decompress(buf.slice(1)));
|
|
999
|
+
|
|
1000
|
+
// Parse protobuf
|
|
1001
|
+
const parser = new ProtobufParser(decompressed);
|
|
1002
|
+
const parsed = parser.parse();
|
|
1003
|
+
|
|
1004
|
+
// Extract structure
|
|
1005
|
+
const nodes = extractNodes(parsed, decompressed);
|
|
1006
|
+
const connections = extractConnections(parsed);
|
|
1007
|
+
const metadata = extractMetadata(parsed);
|
|
1008
|
+
|
|
1009
|
+
const result = {
|
|
1010
|
+
// XML metadata
|
|
1011
|
+
label: labelMatch ? labelMatch[1] : '',
|
|
1012
|
+
width: widthMatch ? parseInt(widthMatch[1], 10) : metadata.width || 1920,
|
|
1013
|
+
height: heightMatch ? parseInt(heightMatch[1], 10) : metadata.height || 1080,
|
|
1014
|
+
sourceTimeline: srcHintMatch ? srcHintMatch[1] : 'Timeline 1',
|
|
1015
|
+
recordTC: recTCMatch ? recTCMatch[1] : '01:00:00:00',
|
|
1016
|
+
sourceTC: srcTCMatch ? srcTCMatch[1] : '00:00:00:00',
|
|
1017
|
+
|
|
1018
|
+
// Preserved XML sections for merge
|
|
1019
|
+
pTrackVerXml,
|
|
1020
|
+
|
|
1021
|
+
// Protobuf data
|
|
1022
|
+
nodes,
|
|
1023
|
+
connections,
|
|
1024
|
+
metadata,
|
|
1025
|
+
|
|
1026
|
+
// Raw parsed data (for debugging)
|
|
1027
|
+
_raw: parsed,
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// P4A: Store in LRU cache
|
|
1031
|
+
drxCacheSet(cacheKey, result);
|
|
1032
|
+
return result;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Parse DRX from a hex-encoded body string (for direct API use)
|
|
1037
|
+
*
|
|
1038
|
+
* @param {string} hexBody - Hex-encoded DRX body
|
|
1039
|
+
* @returns {Promise<Object>} - Parsed nodes and connections
|
|
1040
|
+
*/
|
|
1041
|
+
async function parseDRXBody(hexBody) {
|
|
1042
|
+
const buf = Buffer.from(hexBody, 'hex');
|
|
1043
|
+
|
|
1044
|
+
// 0x81 = zstd-compressed; 0x80 = STORED/uncompressed (see parseDRXContent).
|
|
1045
|
+
if (buf[0] !== 0x81 && buf[0] !== 0x80) {
|
|
1046
|
+
throw new Error(`Invalid DRX magic byte: 0x${buf[0].toString(16)}`);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const decompressed = buf[0] === 0x80
|
|
1050
|
+
? buf.slice(1)
|
|
1051
|
+
: Buffer.from((await getZstdCodec()).decompress(buf.slice(1)));
|
|
1052
|
+
|
|
1053
|
+
const parser = new ProtobufParser(decompressed);
|
|
1054
|
+
const parsed = parser.parse();
|
|
1055
|
+
|
|
1056
|
+
return {
|
|
1057
|
+
nodes: extractNodes(parsed, decompressed),
|
|
1058
|
+
connections: extractConnections(parsed),
|
|
1059
|
+
metadata: extractMetadata(parsed),
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Default parameter values for comparison
|
|
1065
|
+
* Uses shared library for consistency
|
|
1066
|
+
*/
|
|
1067
|
+
const DEFAULT_PARAMS = {
|
|
1068
|
+
lift: drxParams.PARAMETER_RANGES.lift
|
|
1069
|
+
? { r: drxParams.getDefault('lift', 'r'), g: drxParams.getDefault('lift', 'g'), b: drxParams.getDefault('lift', 'b'), master: drxParams.getDefault('lift', 'master') }
|
|
1070
|
+
: { r: 0, g: 0, b: 0, master: 0 },
|
|
1071
|
+
gamma: drxParams.PARAMETER_RANGES.gamma
|
|
1072
|
+
? { r: drxParams.getDefault('gamma', 'r'), g: drxParams.getDefault('gamma', 'g'), b: drxParams.getDefault('gamma', 'b'), master: drxParams.getDefault('gamma', 'master') }
|
|
1073
|
+
: { r: 0, g: 0, b: 0, master: 0 },
|
|
1074
|
+
gain: drxParams.PARAMETER_RANGES.gain
|
|
1075
|
+
? { r: drxParams.getDefault('gain', 'r'), g: drxParams.getDefault('gain', 'g'), b: drxParams.getDefault('gain', 'b'), master: drxParams.getDefault('gain', 'master') }
|
|
1076
|
+
: { r: 1, g: 1, b: 1, master: 1 },
|
|
1077
|
+
offset: drxParams.PARAMETER_RANGES.offset
|
|
1078
|
+
? { r: drxParams.getDefault('offset', 'r'), g: drxParams.getDefault('offset', 'g'), b: drxParams.getDefault('offset', 'b') }
|
|
1079
|
+
: { r: 0, g: 0, b: 0 },
|
|
1080
|
+
saturation: drxParams.getDefault('saturation', 'master') ?? 50, // Resolve Primaries: 0-100, unity=50
|
|
1081
|
+
contrast: drxParams.getDefault('contrast', 'master') ?? 1.0,
|
|
1082
|
+
pivot: drxParams.getDefault('pivotFine', 'master') ?? 0.435,
|
|
1083
|
+
temperature: drxParams.getDefault('temperature', 'master') ?? 0,
|
|
1084
|
+
tint: drxParams.getDefault('tint', 'master') ?? 0,
|
|
1085
|
+
midtoneDetail: drxParams.getDefault('midtoneDetail', 'master') ?? 0,
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Tolerance for floating point comparison
|
|
1090
|
+
*/
|
|
1091
|
+
const PARAM_TOLERANCE = 0.001;
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Check if a node has any non-default parameter values
|
|
1095
|
+
* @param {Object} node - Parsed node object with params
|
|
1096
|
+
* @returns {boolean} - True if node has actual adjustments
|
|
1097
|
+
*/
|
|
1098
|
+
function nodeHasAdjustments(node) {
|
|
1099
|
+
if (!node || !node.params) return false;
|
|
1100
|
+
|
|
1101
|
+
const p = node.params;
|
|
1102
|
+
const d = DEFAULT_PARAMS;
|
|
1103
|
+
|
|
1104
|
+
// Check lift
|
|
1105
|
+
if (Math.abs(p.lift.r - d.lift.r) > PARAM_TOLERANCE ||
|
|
1106
|
+
Math.abs(p.lift.g - d.lift.g) > PARAM_TOLERANCE ||
|
|
1107
|
+
Math.abs(p.lift.b - d.lift.b) > PARAM_TOLERANCE ||
|
|
1108
|
+
Math.abs(p.lift.master - d.lift.master) > PARAM_TOLERANCE) {
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Check gamma
|
|
1113
|
+
if (Math.abs(p.gamma.r - d.gamma.r) > PARAM_TOLERANCE ||
|
|
1114
|
+
Math.abs(p.gamma.g - d.gamma.g) > PARAM_TOLERANCE ||
|
|
1115
|
+
Math.abs(p.gamma.b - d.gamma.b) > PARAM_TOLERANCE ||
|
|
1116
|
+
Math.abs(p.gamma.master - d.gamma.master) > PARAM_TOLERANCE) {
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Check gain
|
|
1121
|
+
if (Math.abs(p.gain.r - d.gain.r) > PARAM_TOLERANCE ||
|
|
1122
|
+
Math.abs(p.gain.g - d.gain.g) > PARAM_TOLERANCE ||
|
|
1123
|
+
Math.abs(p.gain.b - d.gain.b) > PARAM_TOLERANCE ||
|
|
1124
|
+
Math.abs(p.gain.master - d.gain.master) > PARAM_TOLERANCE) {
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Check offset
|
|
1129
|
+
if (Math.abs(p.offset.r - d.offset.r) > PARAM_TOLERANCE ||
|
|
1130
|
+
Math.abs(p.offset.g - d.offset.g) > PARAM_TOLERANCE ||
|
|
1131
|
+
Math.abs(p.offset.b - d.offset.b) > PARAM_TOLERANCE) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Check saturation, contrast, pivot
|
|
1136
|
+
if (Math.abs(p.saturation - d.saturation) > PARAM_TOLERANCE ||
|
|
1137
|
+
Math.abs(p.contrast - d.contrast) > PARAM_TOLERANCE ||
|
|
1138
|
+
Math.abs(p.pivot - d.pivot) > PARAM_TOLERANCE) {
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Check for HDR zone adjustments (FIXED 2026-01-17)
|
|
1143
|
+
// HDR zones are detected by:
|
|
1144
|
+
// 1. Node label starting with "HDR"
|
|
1145
|
+
// 2. Correctors containing HDR zone param ID (0x86000305)
|
|
1146
|
+
if (node.label && node.label.startsWith('HDR')) {
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Check correctors for HDR zone/global data
|
|
1151
|
+
if (node.correctors && node.correctors.length > 0) {
|
|
1152
|
+
const HDR_PARAM_IDS = [
|
|
1153
|
+
2248147715, // 0x86000303 - Black Offset
|
|
1154
|
+
2248147717, // 0x86000305 - Zone Adjustments
|
|
1155
|
+
2248147718, // 0x86000306 - Zone Definitions (range/falloff)
|
|
1156
|
+
2248147721, // 0x86000309 - Zone Metadata
|
|
1157
|
+
];
|
|
1158
|
+
for (const corrector of node.correctors) {
|
|
1159
|
+
if (corrector.parameters) {
|
|
1160
|
+
for (const param of corrector.parameters) {
|
|
1161
|
+
if (HDR_PARAM_IDS.includes(param.id)) {
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Check for any significant corrector data (curves, power windows, qualifiers, etc.)
|
|
1170
|
+
// If a node has correctors with substantial parameter data, it likely has a grade
|
|
1171
|
+
if (node.correctors && node.correctors.length > 0) {
|
|
1172
|
+
for (const corrector of node.correctors) {
|
|
1173
|
+
// If corrector has many parameters, it's likely meaningful grade data
|
|
1174
|
+
if (corrector.parameters && corrector.parameters.length > 20) {
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
// Check for curve data (typically has specific structure)
|
|
1178
|
+
if (corrector.curves && Object.keys(corrector.curves).length > 0) {
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
// Check for power window/qualifier data
|
|
1182
|
+
if (corrector.windows && corrector.windows.length > 0) {
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
if (corrector.qualifiers && Object.keys(corrector.qualifiers).length > 0) {
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Check raw node data for any non-default content
|
|
1192
|
+
// This catches grades we can't specifically parse but have data
|
|
1193
|
+
if (node.rawData && node.rawData.length > 500) {
|
|
1194
|
+
// A default node typically has minimal raw data
|
|
1195
|
+
// Significant raw data usually indicates grade adjustments
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/**
|
|
1203
|
+
* Check if a parsed DRX has any actual color adjustments
|
|
1204
|
+
* @param {Object} parsedDRX - Parsed DRX object from parseDRX/parseDRXContent
|
|
1205
|
+
* @returns {boolean} - True if any node has non-default adjustments
|
|
1206
|
+
*/
|
|
1207
|
+
function hasActualGrade(parsedDRX) {
|
|
1208
|
+
if (!parsedDRX || !parsedDRX.nodes || parsedDRX.nodes.length === 0) {
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return parsedDRX.nodes.some(node => nodeHasAdjustments(node));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/**
|
|
1216
|
+
* Format a numeric value for display
|
|
1217
|
+
* @param {number} val - Value to format
|
|
1218
|
+
* @param {number} defaultVal - Default value (for sign display)
|
|
1219
|
+
* @returns {string} - Formatted string with sign
|
|
1220
|
+
*/
|
|
1221
|
+
function formatValue(val, defaultVal = 0) {
|
|
1222
|
+
if (val === undefined || val === null) return '0';
|
|
1223
|
+
const diff = val - defaultVal;
|
|
1224
|
+
if (Math.abs(diff) < 0.001) return '0';
|
|
1225
|
+
const sign = diff > 0 ? '+' : '';
|
|
1226
|
+
return `${sign}${val.toFixed(3)}`;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Extract comprehensive tool breakdown from grade params
|
|
1231
|
+
* Returns structured data for UI display with formatted strings
|
|
1232
|
+
*
|
|
1233
|
+
* @param {Object} params - Grade parameters (can be from DRX node or high-level params)
|
|
1234
|
+
* @returns {Object} - Tool breakdown with raw values, formatted strings, and active tools list
|
|
1235
|
+
*/
|
|
1236
|
+
function extractToolBreakdown(params) {
|
|
1237
|
+
if (!params) {
|
|
1238
|
+
return {
|
|
1239
|
+
lift: { r: 0, g: 0, b: 0, master: 0 },
|
|
1240
|
+
gamma: { r: 0, g: 0, b: 0, master: 0 },
|
|
1241
|
+
gain: { r: 1, g: 1, b: 1, master: 1 },
|
|
1242
|
+
offset: { r: 0, g: 0, b: 0 },
|
|
1243
|
+
saturation: 50, // Resolve Primaries: 0-100, unity=50
|
|
1244
|
+
contrast: 1.0,
|
|
1245
|
+
pivot: 0.435,
|
|
1246
|
+
temperature: 0,
|
|
1247
|
+
tint: 0,
|
|
1248
|
+
formatted: {},
|
|
1249
|
+
activeTools: [],
|
|
1250
|
+
summary: '',
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const d = DEFAULT_PARAMS;
|
|
1255
|
+
|
|
1256
|
+
// Extract values with defaults
|
|
1257
|
+
const breakdown = {
|
|
1258
|
+
lift: {
|
|
1259
|
+
r: params.lift?.r ?? 0,
|
|
1260
|
+
g: params.lift?.g ?? 0,
|
|
1261
|
+
b: params.lift?.b ?? 0,
|
|
1262
|
+
master: params.lift?.master ?? 0,
|
|
1263
|
+
},
|
|
1264
|
+
gamma: {
|
|
1265
|
+
r: params.gamma?.r ?? 0,
|
|
1266
|
+
g: params.gamma?.g ?? 0,
|
|
1267
|
+
b: params.gamma?.b ?? 0,
|
|
1268
|
+
master: params.gamma?.master ?? 0,
|
|
1269
|
+
},
|
|
1270
|
+
gain: {
|
|
1271
|
+
r: params.gain?.r ?? 1,
|
|
1272
|
+
g: params.gain?.g ?? 1,
|
|
1273
|
+
b: params.gain?.b ?? 1,
|
|
1274
|
+
master: params.gain?.master ?? 1,
|
|
1275
|
+
},
|
|
1276
|
+
offset: {
|
|
1277
|
+
r: params.offset?.r ?? 0,
|
|
1278
|
+
g: params.offset?.g ?? 0,
|
|
1279
|
+
b: params.offset?.b ?? 0,
|
|
1280
|
+
},
|
|
1281
|
+
saturation: params.saturation ?? 50, // Resolve Primaries: unity=50
|
|
1282
|
+
contrast: params.contrast ?? 1.0,
|
|
1283
|
+
pivot: params.pivot ?? 0.435,
|
|
1284
|
+
temperature: params.temperature ?? 0,
|
|
1285
|
+
tint: params.tint ?? 0,
|
|
1286
|
+
formatted: {},
|
|
1287
|
+
activeTools: [],
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// Helper to format wheel controls (R, G, B, Master)
|
|
1291
|
+
const formatWheel = (wheel, name, defaultVal = 0) => {
|
|
1292
|
+
const parts = [];
|
|
1293
|
+
if (Math.abs(wheel.r - defaultVal) > PARAM_TOLERANCE) parts.push(`R:${formatValue(wheel.r, defaultVal)}`);
|
|
1294
|
+
if (Math.abs(wheel.g - defaultVal) > PARAM_TOLERANCE) parts.push(`G:${formatValue(wheel.g, defaultVal)}`);
|
|
1295
|
+
if (Math.abs(wheel.b - defaultVal) > PARAM_TOLERANCE) parts.push(`B:${formatValue(wheel.b, defaultVal)}`);
|
|
1296
|
+
if (wheel.master !== undefined && Math.abs(wheel.master - defaultVal) > PARAM_TOLERANCE) {
|
|
1297
|
+
parts.push(`M:${formatValue(wheel.master, defaultVal)}`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (parts.length > 0) {
|
|
1301
|
+
breakdown.formatted[name] = `${name}: ${parts.join(' ')}`;
|
|
1302
|
+
breakdown.activeTools.push(name);
|
|
1303
|
+
return true;
|
|
1304
|
+
}
|
|
1305
|
+
return false;
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
// Format each wheel control
|
|
1309
|
+
formatWheel(breakdown.lift, 'Lift', 0);
|
|
1310
|
+
formatWheel(breakdown.gamma, 'Gamma', 0);
|
|
1311
|
+
formatWheel(breakdown.gain, 'Gain', 1);
|
|
1312
|
+
formatWheel(breakdown.offset, 'Offset', 0);
|
|
1313
|
+
|
|
1314
|
+
// Format scalar controls
|
|
1315
|
+
if (Math.abs(breakdown.saturation - d.saturation) > PARAM_TOLERANCE) {
|
|
1316
|
+
breakdown.formatted.Saturation = `Sat: ${formatValue(breakdown.saturation, 50)}`;
|
|
1317
|
+
breakdown.activeTools.push('Saturation');
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (Math.abs(breakdown.contrast - d.contrast) > PARAM_TOLERANCE) {
|
|
1321
|
+
breakdown.formatted.Contrast = `Con: ${formatValue(breakdown.contrast, 1)}`;
|
|
1322
|
+
breakdown.activeTools.push('Contrast');
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if (Math.abs(breakdown.pivot - d.pivot) > 0.01) {
|
|
1326
|
+
breakdown.formatted.Pivot = `Pivot: ${breakdown.pivot.toFixed(3)}`;
|
|
1327
|
+
breakdown.activeTools.push('Pivot');
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (Math.abs(breakdown.temperature) > PARAM_TOLERANCE) {
|
|
1331
|
+
const sign = breakdown.temperature > 0 ? '+' : '';
|
|
1332
|
+
breakdown.formatted.Temperature = `Temp: ${sign}${breakdown.temperature.toFixed(2)}`;
|
|
1333
|
+
breakdown.activeTools.push('Temperature');
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (Math.abs(breakdown.tint) > PARAM_TOLERANCE) {
|
|
1337
|
+
const sign = breakdown.tint > 0 ? '+' : '';
|
|
1338
|
+
breakdown.formatted.Tint = `Tint: ${sign}${breakdown.tint.toFixed(2)}`;
|
|
1339
|
+
breakdown.activeTools.push('Tint');
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Generate summary string
|
|
1343
|
+
breakdown.summary = Object.values(breakdown.formatted).join(', ');
|
|
1344
|
+
|
|
1345
|
+
return breakdown;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Extract tool breakdown from a parsed DRX file
|
|
1350
|
+
* Merges params from all nodes
|
|
1351
|
+
*
|
|
1352
|
+
* @param {Object} parsedDRX - Parsed DRX object from parseDRX/parseDRXContent
|
|
1353
|
+
* @returns {Object} - Tool breakdown with merged params from all nodes
|
|
1354
|
+
*/
|
|
1355
|
+
function extractToolBreakdownFromDRX(parsedDRX) {
|
|
1356
|
+
if (!parsedDRX || !parsedDRX.nodes || parsedDRX.nodes.length === 0) {
|
|
1357
|
+
return extractToolBreakdown(null);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Merge params from all nodes
|
|
1361
|
+
const mergedParams = {
|
|
1362
|
+
lift: { r: 0, g: 0, b: 0, master: 0 },
|
|
1363
|
+
gamma: { r: 0, g: 0, b: 0, master: 0 },
|
|
1364
|
+
gain: { r: 1, g: 1, b: 1, master: 1 },
|
|
1365
|
+
offset: { r: 0, g: 0, b: 0 },
|
|
1366
|
+
saturation: 50, // Resolve Primaries: 0-100, unity=50
|
|
1367
|
+
contrast: 1.0,
|
|
1368
|
+
pivot: 0.435,
|
|
1369
|
+
temperature: 0,
|
|
1370
|
+
tint: 0,
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
for (const node of parsedDRX.nodes) {
|
|
1374
|
+
if (!node.params) continue;
|
|
1375
|
+
|
|
1376
|
+
// Merge wheel controls (additive for lift/gamma/offset, multiplicative concept for gain)
|
|
1377
|
+
for (const control of ['lift', 'gamma', 'offset']) {
|
|
1378
|
+
if (node.params[control]) {
|
|
1379
|
+
mergedParams[control].r += node.params[control].r || 0;
|
|
1380
|
+
mergedParams[control].g += node.params[control].g || 0;
|
|
1381
|
+
mergedParams[control].b += node.params[control].b || 0;
|
|
1382
|
+
if (node.params[control].master !== undefined) {
|
|
1383
|
+
mergedParams[control].master += node.params[control].master || 0;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Gain is multiplicative
|
|
1389
|
+
if (node.params.gain) {
|
|
1390
|
+
mergedParams.gain.r *= node.params.gain.r ?? 1;
|
|
1391
|
+
mergedParams.gain.g *= node.params.gain.g ?? 1;
|
|
1392
|
+
mergedParams.gain.b *= node.params.gain.b ?? 1;
|
|
1393
|
+
if (node.params.gain.master !== undefined) {
|
|
1394
|
+
mergedParams.gain.master *= node.params.gain.master ?? 1;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Saturation and contrast are multiplicative
|
|
1399
|
+
if (node.params.saturation !== undefined) {
|
|
1400
|
+
mergedParams.saturation *= node.params.saturation;
|
|
1401
|
+
}
|
|
1402
|
+
if (node.params.contrast !== undefined) {
|
|
1403
|
+
mergedParams.contrast *= node.params.contrast;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Pivot uses last value
|
|
1407
|
+
if (node.params.pivot !== undefined) {
|
|
1408
|
+
mergedParams.pivot = node.params.pivot;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Temperature and tint are additive
|
|
1412
|
+
if (node.params.temperature !== undefined) {
|
|
1413
|
+
mergedParams.temperature += node.params.temperature;
|
|
1414
|
+
}
|
|
1415
|
+
if (node.params.tint !== undefined) {
|
|
1416
|
+
mergedParams.tint += node.params.tint;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
return extractToolBreakdown(mergedParams);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Tolerance for sync drift detection (slightly higher than display tolerance)
|
|
1425
|
+
* Using 0.01 to avoid false positives from floating point precision issues
|
|
1426
|
+
*/
|
|
1427
|
+
const SYNC_DRIFT_TOLERANCE = 0.01;
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* Compare two sets of grade parameters to detect sync drift
|
|
1431
|
+
* Returns whether the parameters match within tolerance
|
|
1432
|
+
*
|
|
1433
|
+
* @param {Object} paramsA - First parameter set (from stored version's cumulativeState)
|
|
1434
|
+
* @param {Object} paramsB - Second parameter set (from parsed DRX breakdown)
|
|
1435
|
+
* @param {Object} options - Comparison options
|
|
1436
|
+
* @param {number} options.tolerance - Override default tolerance (default: 0.01)
|
|
1437
|
+
* @returns {Object} - { matches: boolean, differences: Array, summary: string }
|
|
1438
|
+
*/
|
|
1439
|
+
function compareGradeParameters(paramsA, paramsB, options = {}) {
|
|
1440
|
+
const tolerance = options.tolerance ?? SYNC_DRIFT_TOLERANCE;
|
|
1441
|
+
const differences = [];
|
|
1442
|
+
|
|
1443
|
+
// Helper to compare single values
|
|
1444
|
+
const compareValue = (valA, valB, paramPath, defaultVal = 0) => {
|
|
1445
|
+
const a = valA ?? defaultVal;
|
|
1446
|
+
const b = valB ?? defaultVal;
|
|
1447
|
+
const diff = Math.abs(a - b);
|
|
1448
|
+
if (diff > tolerance) {
|
|
1449
|
+
differences.push({
|
|
1450
|
+
param: paramPath,
|
|
1451
|
+
stored: a,
|
|
1452
|
+
current: b,
|
|
1453
|
+
diff: diff,
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1458
|
+
// Handle case where one or both are null/undefined
|
|
1459
|
+
if (!paramsA && !paramsB) {
|
|
1460
|
+
return { matches: true, differences: [], summary: 'Both neutral' };
|
|
1461
|
+
}
|
|
1462
|
+
if (!paramsA) {
|
|
1463
|
+
paramsA = DEFAULT_PARAMS;
|
|
1464
|
+
}
|
|
1465
|
+
if (!paramsB) {
|
|
1466
|
+
paramsB = DEFAULT_PARAMS;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// Compare wheel controls (lift, gamma, gain, offset)
|
|
1470
|
+
const wheels = ['lift', 'gamma', 'gain', 'offset'];
|
|
1471
|
+
const channels = ['r', 'g', 'b', 'master'];
|
|
1472
|
+
|
|
1473
|
+
for (const wheel of wheels) {
|
|
1474
|
+
const wheelA = paramsA[wheel] || {};
|
|
1475
|
+
const wheelB = paramsB[wheel] || {};
|
|
1476
|
+
|
|
1477
|
+
for (const ch of channels) {
|
|
1478
|
+
// Skip master for offset (it doesn't have master in some representations)
|
|
1479
|
+
if (wheel === 'offset' && ch === 'master') continue;
|
|
1480
|
+
|
|
1481
|
+
// Default values differ: gain defaults to 1, others to 0
|
|
1482
|
+
const defaultVal = wheel === 'gain' ? 1 : 0;
|
|
1483
|
+
const valA = wheelA[ch];
|
|
1484
|
+
const valB = wheelB[ch];
|
|
1485
|
+
|
|
1486
|
+
compareValue(valA, valB, `${wheel}.${ch}`, defaultVal);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Compare scalar controls
|
|
1491
|
+
const scalars = [
|
|
1492
|
+
{ name: 'saturation', default: 50 }, // Resolve Primaries: 0-100, unity=50
|
|
1493
|
+
{ name: 'contrast', default: 1 },
|
|
1494
|
+
{ name: 'pivot', default: 0.435 },
|
|
1495
|
+
{ name: 'temperature', default: 0 },
|
|
1496
|
+
{ name: 'tint', default: 0 },
|
|
1497
|
+
];
|
|
1498
|
+
|
|
1499
|
+
for (const scalar of scalars) {
|
|
1500
|
+
const valA = paramsA[scalar.name];
|
|
1501
|
+
const valB = paramsB[scalar.name];
|
|
1502
|
+
compareValue(valA, valB, scalar.name, scalar.default);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Generate summary
|
|
1506
|
+
let summary = '';
|
|
1507
|
+
if (differences.length === 0) {
|
|
1508
|
+
summary = 'Parameters match';
|
|
1509
|
+
} else if (differences.length <= 3) {
|
|
1510
|
+
summary = differences.map(d => `${d.param}: ${d.stored.toFixed(3)} → ${d.current.toFixed(3)}`).join(', ');
|
|
1511
|
+
} else {
|
|
1512
|
+
summary = `${differences.length} parameters differ`;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return {
|
|
1516
|
+
matches: differences.length === 0,
|
|
1517
|
+
differences,
|
|
1518
|
+
summary,
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Normalize semantic params (temperature, tint, contrast) to actual wheel values
|
|
1524
|
+
* This converts high-level params to the actual lift/gamma/gain values they produce
|
|
1525
|
+
*
|
|
1526
|
+
* Uses the same formulas as drx-generator.js parseAdjustmentsToNodes:
|
|
1527
|
+
* - temperature: gain.r += t*0.15, gain.b -= t*0.15, lift.r += t*0.08, lift.b -= t*0.08
|
|
1528
|
+
* - tint: gain.g -= t*0.15, lift.g -= t*0.08
|
|
1529
|
+
* - contrast: implemented via lift/gain (not contrast corrector)
|
|
1530
|
+
*
|
|
1531
|
+
* @param {Object} params - Semantic params (may include temperature, tint, etc.)
|
|
1532
|
+
* @returns {Object} - Normalized params with wheel values
|
|
1533
|
+
*/
|
|
1534
|
+
function normalizeSemanticParams(params) {
|
|
1535
|
+
if (!params) return params;
|
|
1536
|
+
|
|
1537
|
+
const result = {
|
|
1538
|
+
lift: { r: 0, g: 0, b: 0, master: 0 },
|
|
1539
|
+
gamma: { r: 0, g: 0, b: 0, master: 0 },
|
|
1540
|
+
gain: { r: 1, g: 1, b: 1, master: 1 },
|
|
1541
|
+
offset: { r: 0, g: 0, b: 0 },
|
|
1542
|
+
saturation: 50, // Resolve Primaries: 0-100, unity=50
|
|
1543
|
+
contrast: 1,
|
|
1544
|
+
pivot: params.pivot ?? 0.435,
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// Apply temperature to wheel values
|
|
1548
|
+
if (params.temperature && Math.abs(params.temperature) > 0.001) {
|
|
1549
|
+
const t = params.temperature;
|
|
1550
|
+
result.gain.r = 1 + (t * 0.15);
|
|
1551
|
+
result.gain.b = 1 - (t * 0.15);
|
|
1552
|
+
result.lift.r = t * 0.08;
|
|
1553
|
+
result.lift.b = -(t * 0.08);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Apply tint to wheel values
|
|
1557
|
+
if (params.tint && Math.abs(params.tint) > 0.001) {
|
|
1558
|
+
const t = params.tint;
|
|
1559
|
+
result.gain.g -= t * 0.15;
|
|
1560
|
+
result.lift.g -= t * 0.08;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Apply shadowLift to wheel values (matches drx-generator.js)
|
|
1564
|
+
if (params.shadowLift && Math.abs(params.shadowLift) > 0.001) {
|
|
1565
|
+
const s = params.shadowLift;
|
|
1566
|
+
result.lift.r += s * 0.25;
|
|
1567
|
+
result.lift.g += s * 0.25;
|
|
1568
|
+
result.lift.b += s * 0.25;
|
|
1569
|
+
result.lift.master += s * 0.15;
|
|
1570
|
+
console.log('[drx-parser] ShadowLift applied:', s, '→ lift.r:', result.lift.r);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// Apply highlightCompression to wheel values (matches drx-generator.js)
|
|
1574
|
+
if (params.highlightCompression && Math.abs(params.highlightCompression) > 0.001) {
|
|
1575
|
+
const h = params.highlightCompression;
|
|
1576
|
+
result.gain.r -= h * 0.1;
|
|
1577
|
+
result.gain.g -= h * 0.1;
|
|
1578
|
+
result.gain.b -= h * 0.1;
|
|
1579
|
+
console.log('[drx-parser] HighlightCompression applied:', h, '→ gain.r:', result.gain.r);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Apply semantic contrast to wheel values (matches drx-generator.js parseAdjustmentsToNodes)
|
|
1583
|
+
// IMPORTANT: drx-generator.js parseAdjustmentsToNodes implements contrast via lift/gain,
|
|
1584
|
+
// NOT the contrast scalar! The contrast scalar stays at 1.0 (neutral).
|
|
1585
|
+
// See drx-generator.js line 1987: "We implement contrast using Lift/Gain instead of the
|
|
1586
|
+
// Contrast Corrector (Type 2) because Type 2 was causing HSL qualifier issues."
|
|
1587
|
+
if (params.contrast && Math.abs(params.contrast) > 0.01) {
|
|
1588
|
+
const c = params.contrast;
|
|
1589
|
+
const contrastAmount = c * 0.15;
|
|
1590
|
+
// Lower shadows for more contrast (additive to existing lift)
|
|
1591
|
+
result.lift.r += -contrastAmount;
|
|
1592
|
+
result.lift.g += -contrastAmount;
|
|
1593
|
+
result.lift.b += -contrastAmount;
|
|
1594
|
+
result.lift.master += -contrastAmount * 0.5;
|
|
1595
|
+
// Raise highlights for more contrast (additive to existing gain which defaults to 1.0)
|
|
1596
|
+
result.gain.r += contrastAmount;
|
|
1597
|
+
result.gain.g += contrastAmount;
|
|
1598
|
+
result.gain.b += contrastAmount;
|
|
1599
|
+
result.gain.master += contrastAmount * 0.5;
|
|
1600
|
+
console.log('[drx-parser] Contrast (via lift/gain) applied:', c, '→ lift.r:', result.lift.r, 'gain.r:', result.gain.r);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Apply saturation - ALWAYS convert semantic (-1 to 1) to Resolve Primaries (0 to 100)
|
|
1604
|
+
// Semantic model: -1 = desaturated, 0 = neutral, 1 = oversaturated
|
|
1605
|
+
// Resolve Primaries: 0 = desaturated, 50 = neutral (unity), 100 = oversaturated
|
|
1606
|
+
// Formula matches drx-generator.js: params.saturation = 50 + (adjustments.saturation * 50)
|
|
1607
|
+
// NOTE: cumulativeState always stores semantic values from applyTweak() which clamps to -1..1
|
|
1608
|
+
if (params.saturation !== undefined) {
|
|
1609
|
+
// Always convert from semantic -1..1 to Resolve 0..100
|
|
1610
|
+
// semantic 0 -> actual 50, semantic -1 -> actual 0, semantic 1 -> actual 100
|
|
1611
|
+
result.saturation = 50 + (params.saturation * 50);
|
|
1612
|
+
console.log('[drx-parser] Saturation converted: semantic', params.saturation, '→ actual', result.saturation);
|
|
1613
|
+
} else {
|
|
1614
|
+
console.log('[drx-parser] Saturation undefined, using default:', result.saturation);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// DO NOT convert contrast to the contrast scalar!
|
|
1618
|
+
// The drx-generator.js parseAdjustmentsToNodes function implements contrast via lift/gain
|
|
1619
|
+
// (see above), so the contrast scalar stays at 1.0 (neutral) in the generated DRX.
|
|
1620
|
+
// We already applied the contrast adjustment to lift/gain above, so leave scalar at default.
|
|
1621
|
+
// result.contrast stays at 1 (the default set at initialization)
|
|
1622
|
+
if (params.contrast !== undefined) {
|
|
1623
|
+
console.log('[drx-parser] Contrast handled via lift/gain, scalar stays at:', result.contrast);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Copy any explicit wheel values from params (they override computed values)
|
|
1627
|
+
for (const wheel of ['lift', 'gamma', 'gain', 'offset']) {
|
|
1628
|
+
if (params[wheel]) {
|
|
1629
|
+
for (const ch of ['r', 'g', 'b', 'master']) {
|
|
1630
|
+
if (params[wheel][ch] !== undefined) {
|
|
1631
|
+
// For temperature/tint derived values, add to existing
|
|
1632
|
+
// For explicit values, use directly
|
|
1633
|
+
if ((wheel === 'gain' || wheel === 'lift') && (params.temperature || params.tint)) {
|
|
1634
|
+
// Values already applied from temperature/tint
|
|
1635
|
+
} else {
|
|
1636
|
+
result[wheel][ch] = params[wheel][ch];
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
return result;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
/**
|
|
1647
|
+
* Compare a stored version's cumulativeState with current Resolve grade
|
|
1648
|
+
* Convenience wrapper that parses the DRX and compares parameters
|
|
1649
|
+
*
|
|
1650
|
+
* @param {Object} storedParams - The cumulativeState from stored version
|
|
1651
|
+
* @param {string} drxContent - DRX content exported from Resolve
|
|
1652
|
+
* @returns {Promise<Object>} - { matches, differences, summary, resolveParams }
|
|
1653
|
+
*/
|
|
1654
|
+
async function compareSyncState(storedParams, drxContent) {
|
|
1655
|
+
try {
|
|
1656
|
+
// Debug: Log incoming stored params
|
|
1657
|
+
console.log('[drx-parser] compareSyncState input storedParams:', JSON.stringify({
|
|
1658
|
+
saturation: storedParams?.saturation,
|
|
1659
|
+
contrast: storedParams?.contrast,
|
|
1660
|
+
temperature: storedParams?.temperature,
|
|
1661
|
+
shadowLift: storedParams?.shadowLift,
|
|
1662
|
+
highlightCompression: storedParams?.highlightCompression,
|
|
1663
|
+
tint: storedParams?.tint,
|
|
1664
|
+
}));
|
|
1665
|
+
|
|
1666
|
+
// Parse the DRX to get current parameters
|
|
1667
|
+
const parsedDrx = await parseDRXContent(drxContent);
|
|
1668
|
+
const breakdown = extractToolBreakdownFromDRX(parsedDrx);
|
|
1669
|
+
|
|
1670
|
+
// Extract just the parameter values for comparison
|
|
1671
|
+
const resolveParams = {
|
|
1672
|
+
lift: breakdown.lift,
|
|
1673
|
+
gamma: breakdown.gamma,
|
|
1674
|
+
gain: breakdown.gain,
|
|
1675
|
+
offset: breakdown.offset,
|
|
1676
|
+
saturation: breakdown.saturation,
|
|
1677
|
+
contrast: breakdown.contrast,
|
|
1678
|
+
pivot: breakdown.pivot,
|
|
1679
|
+
temperature: breakdown.temperature || 0,
|
|
1680
|
+
tint: breakdown.tint || 0,
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
// Debug: Log DRX params
|
|
1684
|
+
console.log('[drx-parser] compareSyncState DRX resolveParams:', JSON.stringify({
|
|
1685
|
+
saturation: resolveParams.saturation,
|
|
1686
|
+
contrast: resolveParams.contrast,
|
|
1687
|
+
}));
|
|
1688
|
+
|
|
1689
|
+
// Normalize stored params to convert semantic values (temperature, tint)
|
|
1690
|
+
// to actual wheel values for accurate comparison
|
|
1691
|
+
const normalizedStoredParams = normalizeSemanticParams(storedParams);
|
|
1692
|
+
|
|
1693
|
+
// Debug: Log normalized params
|
|
1694
|
+
console.log('[drx-parser] compareSyncState normalized storedParams:', JSON.stringify({
|
|
1695
|
+
saturation: normalizedStoredParams?.saturation,
|
|
1696
|
+
contrast: normalizedStoredParams?.contrast,
|
|
1697
|
+
}));
|
|
1698
|
+
|
|
1699
|
+
const result = compareGradeParameters(normalizedStoredParams, resolveParams);
|
|
1700
|
+
|
|
1701
|
+
return {
|
|
1702
|
+
...result,
|
|
1703
|
+
resolveParams,
|
|
1704
|
+
nodeCount: parsedDrx.nodes?.length || 0,
|
|
1705
|
+
};
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
return {
|
|
1708
|
+
matches: false,
|
|
1709
|
+
differences: [],
|
|
1710
|
+
summary: `Parse error: ${error.message}`,
|
|
1711
|
+
error: error.message,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Compare two DRX contents directly (DRX-to-DRX comparison)
|
|
1718
|
+
* This is more accurate than comparing cumulativeState to DRX
|
|
1719
|
+
* because it avoids normalization/conversion mismatches.
|
|
1720
|
+
*
|
|
1721
|
+
* @param {string} storedDrxContent - The stored version's DRX content
|
|
1722
|
+
* @param {string} currentDrxContent - The current DRX from Resolve
|
|
1723
|
+
* @param {Object} options - Comparison options
|
|
1724
|
+
* @param {number} options.tolerance - Override default tolerance (default: 0.02 for DRX-to-DRX)
|
|
1725
|
+
* @returns {Promise<Object>} - { matches, differences, summary, storedParams, currentParams }
|
|
1726
|
+
*/
|
|
1727
|
+
async function compareDRXContent(storedDrxContent, currentDrxContent, options = {}) {
|
|
1728
|
+
// Use slightly higher tolerance for DRX-to-DRX since minor floating point
|
|
1729
|
+
// differences can occur during export/import cycles
|
|
1730
|
+
const tolerance = options.tolerance ?? 0.02;
|
|
1731
|
+
|
|
1732
|
+
try {
|
|
1733
|
+
// Parse both DRX files
|
|
1734
|
+
const storedParsed = await parseDRXContent(storedDrxContent);
|
|
1735
|
+
const currentParsed = await parseDRXContent(currentDrxContent);
|
|
1736
|
+
|
|
1737
|
+
// Extract tool breakdowns (actual wheel values)
|
|
1738
|
+
const storedBreakdown = extractToolBreakdownFromDRX(storedParsed);
|
|
1739
|
+
const currentBreakdown = extractToolBreakdownFromDRX(currentParsed);
|
|
1740
|
+
|
|
1741
|
+
// Build param objects for comparison
|
|
1742
|
+
const storedParams = {
|
|
1743
|
+
lift: storedBreakdown.lift,
|
|
1744
|
+
gamma: storedBreakdown.gamma,
|
|
1745
|
+
gain: storedBreakdown.gain,
|
|
1746
|
+
offset: storedBreakdown.offset,
|
|
1747
|
+
saturation: storedBreakdown.saturation,
|
|
1748
|
+
contrast: storedBreakdown.contrast,
|
|
1749
|
+
pivot: storedBreakdown.pivot,
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
const currentParams = {
|
|
1753
|
+
lift: currentBreakdown.lift,
|
|
1754
|
+
gamma: currentBreakdown.gamma,
|
|
1755
|
+
gain: currentBreakdown.gain,
|
|
1756
|
+
offset: currentBreakdown.offset,
|
|
1757
|
+
saturation: currentBreakdown.saturation,
|
|
1758
|
+
contrast: currentBreakdown.contrast,
|
|
1759
|
+
pivot: currentBreakdown.pivot,
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
// Compare parameters with specified tolerance
|
|
1763
|
+
const result = compareGradeParameters(storedParams, currentParams, { tolerance });
|
|
1764
|
+
|
|
1765
|
+
return {
|
|
1766
|
+
...result,
|
|
1767
|
+
storedParams,
|
|
1768
|
+
currentParams,
|
|
1769
|
+
storedNodeCount: storedParsed.nodes?.length || 0,
|
|
1770
|
+
currentNodeCount: currentParsed.nodes?.length || 0,
|
|
1771
|
+
};
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
console.error('[drx-parser] compareDRXContent error:', error.message);
|
|
1774
|
+
return {
|
|
1775
|
+
matches: false,
|
|
1776
|
+
differences: [],
|
|
1777
|
+
summary: `Parse error: ${error.message}`,
|
|
1778
|
+
error: error.message,
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
module.exports = {
|
|
1784
|
+
parseDRX,
|
|
1785
|
+
parseDRXContent,
|
|
1786
|
+
parseDRXBody,
|
|
1787
|
+
extractNodes,
|
|
1788
|
+
extractConnections,
|
|
1789
|
+
extractMetadata,
|
|
1790
|
+
ProtobufParser,
|
|
1791
|
+
PARAM_IDS,
|
|
1792
|
+
CORRECTOR_TYPES,
|
|
1793
|
+
DEFAULT_PARAMS,
|
|
1794
|
+
nodeHasAdjustments,
|
|
1795
|
+
hasActualGrade,
|
|
1796
|
+
// Tool breakdown utilities
|
|
1797
|
+
extractToolBreakdown,
|
|
1798
|
+
extractToolBreakdownFromDRX,
|
|
1799
|
+
formatValue,
|
|
1800
|
+
// Sync drift detection utilities
|
|
1801
|
+
SYNC_DRIFT_TOLERANCE,
|
|
1802
|
+
normalizeSemanticParams,
|
|
1803
|
+
compareGradeParameters,
|
|
1804
|
+
compareSyncState,
|
|
1805
|
+
compareDRXContent,
|
|
1806
|
+
// Re-export shared library for convenience
|
|
1807
|
+
drxParams,
|
|
1808
|
+
};
|