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,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Trim — 2-pop detection and program trim
|
|
3
|
+
*
|
|
4
|
+
* Detects 1kHz sync pops (2-pop) in audio and trims to program length.
|
|
5
|
+
* Standard: 2-pop is 1 frame of 1kHz tone at exactly -2:00 before FFOA.
|
|
6
|
+
*
|
|
7
|
+
* Dependencies: FFmpeg 5+ (v7.1.1 installed)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execFile, spawn } = require('child_process');
|
|
11
|
+
const { promisify } = require('util');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
let ffmpegPath = 'ffmpeg';
|
|
18
|
+
let ffprobePath = 'ffprobe';
|
|
19
|
+
try {
|
|
20
|
+
ffmpegPath = require('ffmpeg-static');
|
|
21
|
+
ffprobePath = require('ffprobe-static').path;
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detect 2-pops (1kHz sync tones) in audio.
|
|
26
|
+
*
|
|
27
|
+
* Uses bandpass filter at 1kHz + envelope follower to find short bursts
|
|
28
|
+
* of 1kHz tone that match 2-pop characteristics.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} inputPath - Path to audio file
|
|
31
|
+
* @param {Object} [options]
|
|
32
|
+
* @param {number} [options.frequency=1000] - Expected tone frequency in Hz
|
|
33
|
+
* @param {number} [options.minDuration=0.02] - Min pop duration in seconds
|
|
34
|
+
* @param {number} [options.maxDuration=0.1] - Max pop duration in seconds
|
|
35
|
+
* @param {number} [options.threshold=-30] - Detection threshold in dBFS
|
|
36
|
+
* @returns {Promise<{pops: Array<{time: number, duration: number, level: number}>, count: number}>}
|
|
37
|
+
*/
|
|
38
|
+
async function detectPops(inputPath, options = {}) {
|
|
39
|
+
const freq = options.frequency || 1000;
|
|
40
|
+
const minDur = options.minDuration || 0.02;
|
|
41
|
+
const maxDur = options.maxDuration || 0.1;
|
|
42
|
+
const threshold = options.threshold || -30;
|
|
43
|
+
|
|
44
|
+
// Bandpass around 1kHz, then detect tone bursts
|
|
45
|
+
const { stderr } = await execFileAsync(ffmpegPath, [
|
|
46
|
+
'-i', inputPath,
|
|
47
|
+
'-af', [
|
|
48
|
+
`bandpass=f=${freq}:width_type=h:w=200`,
|
|
49
|
+
`silencedetect=noise=${threshold}dB:d=${minDur}`,
|
|
50
|
+
].join(','),
|
|
51
|
+
'-f', 'null', '-',
|
|
52
|
+
], { maxBuffer: 50 * 1024 * 1024, timeout: 120000 });
|
|
53
|
+
|
|
54
|
+
// Parse silence boundaries — the NON-silent regions are our pops
|
|
55
|
+
const starts = [...stderr.matchAll(/silence_end:\s*([\d.]+)/g)].map(m => parseFloat(m[1]));
|
|
56
|
+
const ends = [...stderr.matchAll(/silence_start:\s*([\d.]+)/g)].map(m => parseFloat(m[1]));
|
|
57
|
+
|
|
58
|
+
// Get file duration
|
|
59
|
+
const { stdout: probeOut } = await execFileAsync(ffprobePath, [
|
|
60
|
+
'-v', 'quiet', '-show_entries', 'format=duration',
|
|
61
|
+
'-of', 'csv=p=0', inputPath,
|
|
62
|
+
]);
|
|
63
|
+
const totalDuration = parseFloat(probeOut.trim());
|
|
64
|
+
|
|
65
|
+
// Build pop list from non-silent regions
|
|
66
|
+
const pops = [];
|
|
67
|
+
// First non-silent region starts at 0 (if no initial silence) or at first silence_end
|
|
68
|
+
for (let i = 0; i < starts.length; i++) {
|
|
69
|
+
const popStart = starts[i];
|
|
70
|
+
const popEnd = i < ends.length ? ends[i + 1] || totalDuration : totalDuration;
|
|
71
|
+
const popDuration = popEnd - popStart;
|
|
72
|
+
|
|
73
|
+
// Filter to likely 2-pops: short bursts within expected duration range
|
|
74
|
+
if (popDuration >= minDur && popDuration <= maxDur) {
|
|
75
|
+
pops.push({
|
|
76
|
+
time: popStart,
|
|
77
|
+
duration: popDuration,
|
|
78
|
+
level: 0, // Could measure RMS but not critical
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { pops, count: pops.length };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Trim audio to program length using 2-pop as reference.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} inputPath - Path to input audio
|
|
90
|
+
* @param {string} outputPath - Path for trimmed output
|
|
91
|
+
* @param {Object} options
|
|
92
|
+
* @param {number} [options.startTime] - Start time in seconds (auto-detect from 2-pop if not provided)
|
|
93
|
+
* @param {number} [options.endTime] - End time in seconds
|
|
94
|
+
* @param {number} [options.duration] - Duration in seconds (alternative to endTime)
|
|
95
|
+
* @param {number} [options.fadeIn=0] - Fade-in duration in seconds
|
|
96
|
+
* @param {number} [options.fadeOut=0] - Fade-out duration in seconds
|
|
97
|
+
* @param {number} [options.preRoll=2] - Seconds of pre-roll before 2-pop (default 2s per SMPTE)
|
|
98
|
+
* @param {Function} [options.onProgress] - Progress callback
|
|
99
|
+
* @returns {Promise<{outputPath: string, startTime: number, endTime: number, duration: number, popsDetected: number}>}
|
|
100
|
+
*/
|
|
101
|
+
async function trim(inputPath, outputPath, options = {}) {
|
|
102
|
+
const { fadeIn = 0, fadeOut = 0, preRoll = 2, onProgress = () => {} } = options;
|
|
103
|
+
|
|
104
|
+
onProgress(10);
|
|
105
|
+
|
|
106
|
+
let startTime = options.startTime;
|
|
107
|
+
let endTime = options.endTime;
|
|
108
|
+
let popsDetected = 0;
|
|
109
|
+
|
|
110
|
+
// Auto-detect from 2-pop if no explicit start/end
|
|
111
|
+
if (startTime === undefined) {
|
|
112
|
+
const { pops } = await detectPops(inputPath);
|
|
113
|
+
popsDetected = pops.length;
|
|
114
|
+
|
|
115
|
+
if (pops.length >= 1) {
|
|
116
|
+
// First pop is head 2-pop — program starts preRoll seconds after it
|
|
117
|
+
startTime = pops[0].time + preRoll;
|
|
118
|
+
} else {
|
|
119
|
+
startTime = 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (pops.length >= 2 && endTime === undefined && !options.duration) {
|
|
123
|
+
// Second pop is tail 2-pop — program ends preRoll seconds before it
|
|
124
|
+
endTime = pops[pops.length - 1].time - preRoll;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
onProgress(40);
|
|
129
|
+
|
|
130
|
+
// Build FFmpeg args
|
|
131
|
+
const args = ['-y', '-i', inputPath];
|
|
132
|
+
|
|
133
|
+
if (startTime > 0) {
|
|
134
|
+
args.push('-ss', String(startTime));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.duration) {
|
|
138
|
+
args.push('-t', String(options.duration));
|
|
139
|
+
} else if (endTime !== undefined) {
|
|
140
|
+
args.push('-to', String(endTime));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build filter chain for fades
|
|
144
|
+
const filters = [];
|
|
145
|
+
if (fadeIn > 0) {
|
|
146
|
+
filters.push(`afade=t=in:st=0:d=${fadeIn}`);
|
|
147
|
+
}
|
|
148
|
+
if (fadeOut > 0) {
|
|
149
|
+
const dur = options.duration || (endTime !== undefined ? endTime - startTime : 0);
|
|
150
|
+
if (dur > 0) {
|
|
151
|
+
filters.push(`afade=t=out:st=${dur - fadeOut}:d=${fadeOut}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (filters.length > 0) {
|
|
156
|
+
args.push('-af', filters.join(','));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
args.push('-c:a', 'pcm_s24le', outputPath);
|
|
160
|
+
|
|
161
|
+
onProgress(50);
|
|
162
|
+
|
|
163
|
+
await execFileAsync(ffmpegPath, args, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
|
|
164
|
+
|
|
165
|
+
onProgress(90);
|
|
166
|
+
|
|
167
|
+
// Get output duration
|
|
168
|
+
const { stdout: durOut } = await execFileAsync(ffprobePath, [
|
|
169
|
+
'-v', 'quiet', '-show_entries', 'format=duration',
|
|
170
|
+
'-of', 'csv=p=0', outputPath,
|
|
171
|
+
]);
|
|
172
|
+
const outputDuration = parseFloat(durOut.trim());
|
|
173
|
+
|
|
174
|
+
onProgress(100);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
outputPath,
|
|
178
|
+
startTime,
|
|
179
|
+
endTime: endTime || startTime + outputDuration,
|
|
180
|
+
duration: outputDuration,
|
|
181
|
+
popsDetected,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
detectPops,
|
|
187
|
+
trim,
|
|
188
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fairlight Audio Intelligence
|
|
3
|
+
*
|
|
4
|
+
* Audio track planning, coverage analysis, and loudness validation
|
|
5
|
+
* for DaVinci Resolve Fairlight workflows.
|
|
6
|
+
*
|
|
7
|
+
* Provides content-type-specific audio templates (documentary,
|
|
8
|
+
* narrative, podcast, social) and tools to analyze clip coverage,
|
|
9
|
+
* detect gaps/overlaps/silence, and validate loudness targets.
|
|
10
|
+
*
|
|
11
|
+
* @module post-tools/audio-fairlight
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Audio track channel types recognized by Resolve.
|
|
16
|
+
* @type {Object<string, string>}
|
|
17
|
+
*/
|
|
18
|
+
const CHANNEL_TYPES = {
|
|
19
|
+
MONO: 'mono',
|
|
20
|
+
STEREO: 'stereo',
|
|
21
|
+
SURROUND_51: '5.1',
|
|
22
|
+
SURROUND_71: '7.1',
|
|
23
|
+
ADAPTIVE: 'adaptive1',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Audio templates for different content types.
|
|
28
|
+
*
|
|
29
|
+
* Each template defines tracks with name, channel type, and routing.
|
|
30
|
+
* Bus assignments reflect standard Fairlight mixing conventions.
|
|
31
|
+
*
|
|
32
|
+
* @type {Object}
|
|
33
|
+
*/
|
|
34
|
+
const AUDIO_TEMPLATES = {
|
|
35
|
+
DOCUMENTARY: {
|
|
36
|
+
name: 'Documentary',
|
|
37
|
+
description: 'Interview-driven documentary with natural sound and music',
|
|
38
|
+
tracks: [
|
|
39
|
+
{ name: 'DX L', label: 'Dialogue Left', channelType: 'mono', bus: 'DX', role: 'dialogue' },
|
|
40
|
+
{ name: 'DX R', label: 'Dialogue Right', channelType: 'mono', bus: 'DX', role: 'dialogue' },
|
|
41
|
+
{ name: 'NAT', label: 'Natural Sound', channelType: 'stereo', bus: 'NAT', role: 'nat_sound' },
|
|
42
|
+
{ name: 'MX', label: 'Music', channelType: 'stereo', bus: 'MX', role: 'music' },
|
|
43
|
+
{ name: 'VO', label: 'Voice Over', channelType: 'mono', bus: 'DX', role: 'voiceover' },
|
|
44
|
+
{ name: 'SFX', label: 'Sound Effects', channelType: 'stereo', bus: 'FX', role: 'sfx' },
|
|
45
|
+
],
|
|
46
|
+
buses: ['DX', 'NAT', 'MX', 'FX', 'MAIN'],
|
|
47
|
+
outputFormat: 'stereo',
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
NARRATIVE: {
|
|
51
|
+
name: 'Narrative Feature',
|
|
52
|
+
description: 'Scripted narrative with full surround sound design',
|
|
53
|
+
tracks: [
|
|
54
|
+
{ name: 'DX', label: 'Production Dialogue', channelType: '5.1', bus: 'DX', role: 'dialogue' },
|
|
55
|
+
{ name: 'ADR', label: 'ADR / Loop Group', channelType: '5.1', bus: 'DX', role: 'dialogue' },
|
|
56
|
+
{ name: 'FX', label: 'Sound Effects', channelType: '5.1', bus: 'FX', role: 'sfx' },
|
|
57
|
+
{ name: 'BG', label: 'Backgrounds / Ambience', channelType: '5.1', bus: 'FX', role: 'ambience' },
|
|
58
|
+
{ name: 'FLY', label: 'Foley', channelType: '5.1', bus: 'FX', role: 'foley' },
|
|
59
|
+
{ name: 'MX', label: 'Music Score', channelType: '5.1', bus: 'MX', role: 'music' },
|
|
60
|
+
{ name: 'MX2', label: 'Music Source / Diegetic', channelType: 'stereo', bus: 'MX', role: 'music' },
|
|
61
|
+
{ name: 'FG', label: 'Full Mix Guide', channelType: 'stereo', bus: 'REF', role: 'guide' },
|
|
62
|
+
],
|
|
63
|
+
buses: ['DX', 'FX', 'MX', 'REF', 'MAIN'],
|
|
64
|
+
outputFormat: '5.1',
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
PODCAST: {
|
|
68
|
+
name: 'Podcast',
|
|
69
|
+
description: 'Multi-host podcast with music beds',
|
|
70
|
+
tracks: [
|
|
71
|
+
{ name: 'HOST', label: 'Host Microphone', channelType: 'mono', bus: 'DX', role: 'dialogue' },
|
|
72
|
+
{ name: 'GUEST', label: 'Guest Microphone', channelType: 'mono', bus: 'DX', role: 'dialogue' },
|
|
73
|
+
{ name: 'GUEST2', label: 'Guest 2 Microphone', channelType: 'mono', bus: 'DX', role: 'dialogue' },
|
|
74
|
+
{ name: 'MX', label: 'Music / Beds', channelType: 'stereo', bus: 'MX', role: 'music' },
|
|
75
|
+
{ name: 'SFX', label: 'Stingers / SFX', channelType: 'stereo', bus: 'FX', role: 'sfx' },
|
|
76
|
+
{ name: 'PHONE', label: 'Phone / Remote', channelType: 'mono', bus: 'DX', role: 'dialogue' },
|
|
77
|
+
],
|
|
78
|
+
buses: ['DX', 'MX', 'FX', 'MAIN'],
|
|
79
|
+
outputFormat: 'stereo',
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
SOCIAL: {
|
|
83
|
+
name: 'Social Media',
|
|
84
|
+
description: 'Quick-turnaround social content',
|
|
85
|
+
tracks: [
|
|
86
|
+
{ name: 'MIX', label: 'Main Mix', channelType: 'stereo', bus: 'MAIN', role: 'mix' },
|
|
87
|
+
{ name: 'VO', label: 'Voice Over', channelType: 'mono', bus: 'DX', role: 'voiceover' },
|
|
88
|
+
{ name: 'MX', label: 'Music', channelType: 'stereo', bus: 'MX', role: 'music' },
|
|
89
|
+
],
|
|
90
|
+
buses: ['DX', 'MX', 'MAIN'],
|
|
91
|
+
outputFormat: 'stereo',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Loudness targets for common delivery specs.
|
|
97
|
+
* @type {Object}
|
|
98
|
+
*/
|
|
99
|
+
const LOUDNESS_TARGETS = {
|
|
100
|
+
broadcast_us: { standard: 'ATSC A/85', targetLUFS: -24, tolerance: 2, truePeakdBTP: -2 },
|
|
101
|
+
broadcast_eu: { standard: 'EBU R128', targetLUFS: -23, tolerance: 1, truePeakdBTP: -1 },
|
|
102
|
+
streaming: { standard: 'Streaming', targetLUFS: -16, tolerance: 2, truePeakdBTP: -1 },
|
|
103
|
+
cinema: { standard: 'SMPTE RP 200', targetLUFS: -20, tolerance: 2, truePeakdBTP: -3 },
|
|
104
|
+
podcast: { standard: 'Podcast', targetLUFS: -16, tolerance: 2, truePeakdBTP: -1 },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Select an audio template by content type.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} contentType — 'documentary'|'narrative'|'podcast'|'social'
|
|
111
|
+
* @returns {object} template definition
|
|
112
|
+
*/
|
|
113
|
+
function selectTemplate(contentType) {
|
|
114
|
+
const key = contentType.toUpperCase().replace(/[\s-]/g, '_');
|
|
115
|
+
const template = AUDIO_TEMPLATES[key];
|
|
116
|
+
if (!template) {
|
|
117
|
+
const available = Object.keys(AUDIO_TEMPLATES).map(k => k.toLowerCase()).join(', ');
|
|
118
|
+
throw new Error(`Unknown content type "${contentType}". Available: ${available}`);
|
|
119
|
+
}
|
|
120
|
+
return JSON.parse(JSON.stringify(template));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate a Fairlight track creation plan from a template.
|
|
125
|
+
*
|
|
126
|
+
* Returns instructions suitable for Resolve timeline track creation:
|
|
127
|
+
* AddTrack('audio', subtype) calls and track naming.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} template — from selectTemplate or AUDIO_TEMPLATES
|
|
130
|
+
* @param {object} [opts]
|
|
131
|
+
* @param {number} [opts.startIndex=1] — first audio track index
|
|
132
|
+
* @returns {object} { tracks: [...], buses: [...], resolveCommands: [...] }
|
|
133
|
+
*/
|
|
134
|
+
function generateTrackPlan(template, opts = {}) {
|
|
135
|
+
const { startIndex = 1 } = opts;
|
|
136
|
+
|
|
137
|
+
const tracks = template.tracks.map((t, i) => ({
|
|
138
|
+
index: startIndex + i,
|
|
139
|
+
name: t.name,
|
|
140
|
+
label: t.label,
|
|
141
|
+
channelType: t.channelType,
|
|
142
|
+
bus: t.bus,
|
|
143
|
+
role: t.role,
|
|
144
|
+
resolveSubtype: t.channelType === '5.1' ? '5.1'
|
|
145
|
+
: t.channelType === '7.1' ? '7.1'
|
|
146
|
+
: t.channelType === 'stereo' ? 'stereo'
|
|
147
|
+
: 'mono',
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
const resolveCommands = tracks.map(t => ({
|
|
151
|
+
action: 'AddTrack',
|
|
152
|
+
args: ['audio', t.resolveSubtype],
|
|
153
|
+
then: { action: 'SetTrackName', args: ['audio', t.index, t.name] },
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
templateName: template.name,
|
|
158
|
+
outputFormat: template.outputFormat,
|
|
159
|
+
trackCount: tracks.length,
|
|
160
|
+
tracks,
|
|
161
|
+
buses: template.buses,
|
|
162
|
+
resolveCommands,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Analyze audio clip coverage on a timeline.
|
|
168
|
+
*
|
|
169
|
+
* Detects gaps, overlaps, and silence per track.
|
|
170
|
+
*
|
|
171
|
+
* @param {object[]} clips — [{ trackIndex, trackName, startTime, endTime, hasAudio? }]
|
|
172
|
+
* @param {object} [opts]
|
|
173
|
+
* @param {number} [opts.minGap=0.5] — minimum gap (seconds) to flag
|
|
174
|
+
* @param {number} [opts.timelineStart=0]
|
|
175
|
+
* @param {number} [opts.timelineEnd] — if omitted, derived from last clip
|
|
176
|
+
* @returns {object} { tracks: { [trackName]: { clips, gaps, overlaps, coveragePercent } }, summary }
|
|
177
|
+
*/
|
|
178
|
+
function analyzeAudioCoverage(clips, opts = {}) {
|
|
179
|
+
const { minGap = 0.5, timelineStart = 0 } = opts;
|
|
180
|
+
|
|
181
|
+
// Group by track
|
|
182
|
+
const trackGroups = {};
|
|
183
|
+
for (const clip of clips) {
|
|
184
|
+
const key = clip.trackName || `Track ${clip.trackIndex}`;
|
|
185
|
+
if (!trackGroups[key]) trackGroups[key] = [];
|
|
186
|
+
trackGroups[key].push(clip);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Derive timeline end
|
|
190
|
+
let timelineEnd = opts.timelineEnd;
|
|
191
|
+
if (timelineEnd === undefined) {
|
|
192
|
+
timelineEnd = 0;
|
|
193
|
+
for (const clip of clips) {
|
|
194
|
+
if (clip.endTime > timelineEnd) timelineEnd = clip.endTime;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const totalDuration = timelineEnd - timelineStart;
|
|
199
|
+
const tracks = {};
|
|
200
|
+
let totalGaps = 0;
|
|
201
|
+
let totalOverlaps = 0;
|
|
202
|
+
|
|
203
|
+
for (const [trackName, trackClips] of Object.entries(trackGroups)) {
|
|
204
|
+
// Sort by start time
|
|
205
|
+
const sorted = [...trackClips].sort((a, b) => a.startTime - b.startTime);
|
|
206
|
+
const gaps = [];
|
|
207
|
+
const overlaps = [];
|
|
208
|
+
|
|
209
|
+
// Check gap before first clip
|
|
210
|
+
if (sorted.length > 0 && sorted[0].startTime - timelineStart > minGap) {
|
|
211
|
+
gaps.push({
|
|
212
|
+
startTime: timelineStart,
|
|
213
|
+
endTime: sorted[0].startTime,
|
|
214
|
+
duration: sorted[0].startTime - timelineStart,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check between clips
|
|
219
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
220
|
+
const prev = sorted[i - 1];
|
|
221
|
+
const curr = sorted[i];
|
|
222
|
+
|
|
223
|
+
if (curr.startTime < prev.endTime) {
|
|
224
|
+
// Overlap
|
|
225
|
+
overlaps.push({
|
|
226
|
+
clipA: i - 1,
|
|
227
|
+
clipB: i,
|
|
228
|
+
startTime: curr.startTime,
|
|
229
|
+
endTime: Math.min(prev.endTime, curr.endTime),
|
|
230
|
+
duration: Math.min(prev.endTime, curr.endTime) - curr.startTime,
|
|
231
|
+
});
|
|
232
|
+
} else if (curr.startTime - prev.endTime > minGap) {
|
|
233
|
+
// Gap
|
|
234
|
+
gaps.push({
|
|
235
|
+
startTime: prev.endTime,
|
|
236
|
+
endTime: curr.startTime,
|
|
237
|
+
duration: curr.startTime - prev.endTime,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check gap after last clip
|
|
243
|
+
if (sorted.length > 0) {
|
|
244
|
+
const last = sorted[sorted.length - 1];
|
|
245
|
+
if (timelineEnd - last.endTime > minGap) {
|
|
246
|
+
gaps.push({
|
|
247
|
+
startTime: last.endTime,
|
|
248
|
+
endTime: timelineEnd,
|
|
249
|
+
duration: timelineEnd - last.endTime,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Calculate coverage
|
|
255
|
+
let coveredTime = 0;
|
|
256
|
+
const merged = _mergeIntervals(sorted.map(c => [c.startTime, c.endTime]));
|
|
257
|
+
for (const [s, e] of merged) {
|
|
258
|
+
coveredTime += Math.min(e, timelineEnd) - Math.max(s, timelineStart);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const coveragePercent = totalDuration > 0
|
|
262
|
+
? Math.round((coveredTime / totalDuration) * 10000) / 100
|
|
263
|
+
: 0;
|
|
264
|
+
|
|
265
|
+
totalGaps += gaps.length;
|
|
266
|
+
totalOverlaps += overlaps.length;
|
|
267
|
+
|
|
268
|
+
tracks[trackName] = {
|
|
269
|
+
clipCount: sorted.length,
|
|
270
|
+
gaps,
|
|
271
|
+
overlaps,
|
|
272
|
+
coveragePercent,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
tracks,
|
|
278
|
+
summary: {
|
|
279
|
+
trackCount: Object.keys(tracks).length,
|
|
280
|
+
totalGaps,
|
|
281
|
+
totalOverlaps,
|
|
282
|
+
timelineDuration: totalDuration,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Merge overlapping intervals.
|
|
289
|
+
* @param {number[][]} intervals — [[start, end], ...]
|
|
290
|
+
* @returns {number[][]}
|
|
291
|
+
*/
|
|
292
|
+
function _mergeIntervals(intervals) {
|
|
293
|
+
if (intervals.length === 0) return [];
|
|
294
|
+
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
|
|
295
|
+
const merged = [sorted[0]];
|
|
296
|
+
|
|
297
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
298
|
+
const last = merged[merged.length - 1];
|
|
299
|
+
if (sorted[i][0] <= last[1]) {
|
|
300
|
+
last[1] = Math.max(last[1], sorted[i][1]);
|
|
301
|
+
} else {
|
|
302
|
+
merged.push(sorted[i]);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return merged;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Validate loudness measurements against a target.
|
|
311
|
+
*
|
|
312
|
+
* @param {object} measurements — { integratedLUFS, truePeakdBTP, shortTermMax?, momentaryMax?, LRA? }
|
|
313
|
+
* @param {number|string} targetLUFS — target in LUFS or a preset key (e.g. 'broadcast_us')
|
|
314
|
+
* @returns {object} { pass, results: [{ check, value, target, pass, message }] }
|
|
315
|
+
*/
|
|
316
|
+
function checkLoudness(measurements, targetLUFS) {
|
|
317
|
+
let target;
|
|
318
|
+
|
|
319
|
+
if (typeof targetLUFS === 'string') {
|
|
320
|
+
const preset = LOUDNESS_TARGETS[targetLUFS.toLowerCase().replace(/[\s-]/g, '_')];
|
|
321
|
+
if (!preset) throw new Error(`Unknown loudness target "${targetLUFS}"`);
|
|
322
|
+
target = preset;
|
|
323
|
+
} else {
|
|
324
|
+
target = { targetLUFS, tolerance: 2, truePeakdBTP: -1 };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const results = [];
|
|
328
|
+
let allPass = true;
|
|
329
|
+
|
|
330
|
+
// Integrated loudness check
|
|
331
|
+
const intDiff = Math.abs(measurements.integratedLUFS - target.targetLUFS);
|
|
332
|
+
const intPass = intDiff <= target.tolerance;
|
|
333
|
+
if (!intPass) allPass = false;
|
|
334
|
+
|
|
335
|
+
results.push({
|
|
336
|
+
check: 'integrated_loudness',
|
|
337
|
+
value: measurements.integratedLUFS,
|
|
338
|
+
target: target.targetLUFS,
|
|
339
|
+
tolerance: target.tolerance,
|
|
340
|
+
pass: intPass,
|
|
341
|
+
message: intPass
|
|
342
|
+
? `Integrated loudness ${measurements.integratedLUFS} LUFS within tolerance`
|
|
343
|
+
: `Integrated loudness ${measurements.integratedLUFS} LUFS outside tolerance (target: ${target.targetLUFS} +/- ${target.tolerance})`,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// True peak check
|
|
347
|
+
if (measurements.truePeakdBTP !== undefined && target.truePeakdBTP !== undefined) {
|
|
348
|
+
const tpPass = measurements.truePeakdBTP <= target.truePeakdBTP;
|
|
349
|
+
if (!tpPass) allPass = false;
|
|
350
|
+
|
|
351
|
+
results.push({
|
|
352
|
+
check: 'true_peak',
|
|
353
|
+
value: measurements.truePeakdBTP,
|
|
354
|
+
target: target.truePeakdBTP,
|
|
355
|
+
pass: tpPass,
|
|
356
|
+
message: tpPass
|
|
357
|
+
? `True peak ${measurements.truePeakdBTP} dBTP within limit`
|
|
358
|
+
: `True peak ${measurements.truePeakdBTP} dBTP exceeds limit of ${target.truePeakdBTP} dBTP`,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// LRA check (informational, typical range 5-20 LU)
|
|
363
|
+
if (measurements.LRA !== undefined) {
|
|
364
|
+
const lraWarn = measurements.LRA > 20;
|
|
365
|
+
results.push({
|
|
366
|
+
check: 'loudness_range',
|
|
367
|
+
value: measurements.LRA,
|
|
368
|
+
pass: !lraWarn,
|
|
369
|
+
message: lraWarn
|
|
370
|
+
? `Loudness range ${measurements.LRA} LU is unusually wide (>20 LU)`
|
|
371
|
+
: `Loudness range ${measurements.LRA} LU is within normal bounds`,
|
|
372
|
+
});
|
|
373
|
+
if (lraWarn) allPass = false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
pass: allPass,
|
|
378
|
+
standard: target.standard || 'Custom',
|
|
379
|
+
results,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
AUDIO_TEMPLATES,
|
|
385
|
+
CHANNEL_TYPES,
|
|
386
|
+
LOUDNESS_TARGETS,
|
|
387
|
+
selectTemplate,
|
|
388
|
+
generateTrackPlan,
|
|
389
|
+
analyzeAudioCoverage,
|
|
390
|
+
checkLoudness,
|
|
391
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* adapters/frame-sampler.js — the FrameSampler adapter interface (spec §3.2).
|
|
5
|
+
*
|
|
6
|
+
* Extract a frame from a media reference at a given SOURCE frame and resolution.
|
|
7
|
+
* The core never touches ffmpeg/Resolve directly — a surface injects a concrete
|
|
8
|
+
* sampler (render-node ffmpeg in the cloud, local ffmpeg in the post-assistant).
|
|
9
|
+
* Output matches the comparator's decoded-frame shape ({data,width,height},
|
|
10
|
+
* grayscale, 0..1) so a sampled frame flows straight into compare/.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class FrameSampler {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string|object} mediaRef resolved media (path / asset+version / stream)
|
|
16
|
+
* @param {number} frame SOURCE frame to extract (the Oracle's derivedSampleFrame)
|
|
17
|
+
* @param {{width:number,height:number}} size
|
|
18
|
+
* @returns {Promise<{data:Float64Array,width:number,height:number}>}
|
|
19
|
+
*/
|
|
20
|
+
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
|
21
|
+
async sample(mediaRef, frame, size) {
|
|
22
|
+
throw new Error('FrameSampler.sample must be implemented by an adapter');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Duck-typed check that an object satisfies the FrameSampler interface. */
|
|
27
|
+
function isFrameSampler(obj) {
|
|
28
|
+
return !!obj && typeof obj.sample === 'function';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A deterministic in-memory sampler for tests — no media, no ffmpeg. The frame
|
|
33
|
+
* content is a stable hash of (mediaRef, frame), so the same request always
|
|
34
|
+
* yields the same pixels and different requests differ.
|
|
35
|
+
*/
|
|
36
|
+
class FakeFrameSampler extends FrameSampler {
|
|
37
|
+
// eslint-disable-next-line class-methods-use-this
|
|
38
|
+
async sample(mediaRef, frame, { width, height }) {
|
|
39
|
+
const n = width * height;
|
|
40
|
+
const data = new Float64Array(n);
|
|
41
|
+
let h = 2166136261 >>> 0;
|
|
42
|
+
const key = `${typeof mediaRef === 'object' ? JSON.stringify(mediaRef) : mediaRef}:${frame}`;
|
|
43
|
+
for (let i = 0; i < key.length; i++) {
|
|
44
|
+
h ^= key.charCodeAt(i);
|
|
45
|
+
h = Math.imul(h, 16777619);
|
|
46
|
+
}
|
|
47
|
+
for (let i = 0; i < n; i++) {
|
|
48
|
+
h ^= i;
|
|
49
|
+
h = Math.imul(h, 16777619);
|
|
50
|
+
data[i] = ((h >>> 8) & 0xff) / 255;
|
|
51
|
+
}
|
|
52
|
+
return { data, width, height };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { FrameSampler, FakeFrameSampler, isFrameSampler };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* adapters/local-ffmpeg-sampler.js — the local FrameSampler impl (spec §3.2).
|
|
5
|
+
*
|
|
6
|
+
* Extracts a single SOURCE frame from a media file via local ffmpeg and decodes
|
|
7
|
+
* it to the normalized-grayscale shape the comparator consumes. This is the
|
|
8
|
+
* post-assistant / local-surface sampler; the render node injects its own
|
|
9
|
+
* ffmpeg-over-R2 sampler with the same interface.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { spawn } = require('child_process');
|
|
13
|
+
const { FrameSampler } = require('./frame-sampler');
|
|
14
|
+
const { decodeGrayNormalized } = require('../compare/decode');
|
|
15
|
+
|
|
16
|
+
/** Run ffmpeg, capture stdout (the extracted PNG) as a Buffer. */
|
|
17
|
+
function runFfmpeg(args, ffmpegPath) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const proc = spawn(ffmpegPath, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
20
|
+
const out = [];
|
|
21
|
+
const err = [];
|
|
22
|
+
proc.stdout.on('data', (d) => out.push(d));
|
|
23
|
+
proc.stderr.on('data', (d) => err.push(d));
|
|
24
|
+
proc.on('error', reject);
|
|
25
|
+
proc.on('close', (code) => {
|
|
26
|
+
if (code === 0) resolve(Buffer.concat(out));
|
|
27
|
+
else reject(new Error(`ffmpeg exited ${code}: ${Buffer.concat(err).toString().slice(-500)}`));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class LocalFfmpegFrameSampler extends FrameSampler {
|
|
33
|
+
constructor({ ffmpegPath = 'ffmpeg' } = {}) {
|
|
34
|
+
super();
|
|
35
|
+
this.ffmpegPath = ffmpegPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} mediaRef local file path
|
|
40
|
+
* @param {number} frame 0-based source frame index
|
|
41
|
+
* @param {{width:number,height:number}} size
|
|
42
|
+
*/
|
|
43
|
+
async sample(mediaRef, frame, size) {
|
|
44
|
+
if (typeof mediaRef !== 'string') {
|
|
45
|
+
throw new Error('LocalFfmpegFrameSampler: mediaRef must be a local file path');
|
|
46
|
+
}
|
|
47
|
+
// Select exactly the requested frame, emit one PNG to stdout.
|
|
48
|
+
const args = [
|
|
49
|
+
'-v', 'error',
|
|
50
|
+
'-i', mediaRef,
|
|
51
|
+
'-vf', `select=eq(n\\,${Math.round(frame)})`,
|
|
52
|
+
'-vframes', '1',
|
|
53
|
+
'-f', 'image2pipe',
|
|
54
|
+
'-c:v', 'png',
|
|
55
|
+
'-',
|
|
56
|
+
];
|
|
57
|
+
const png = await runFfmpeg(args, this.ffmpegPath);
|
|
58
|
+
if (!png || png.length === 0) {
|
|
59
|
+
throw new Error(`LocalFfmpegFrameSampler: no frame ${frame} extracted from ${mediaRef}`);
|
|
60
|
+
}
|
|
61
|
+
return decodeGrayNormalized(png, size);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { LocalFfmpegFrameSampler };
|