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.
Files changed (328) hide show
  1. package/AGENTS.md +20 -0
  2. package/CHANGELOG.md +197 -2
  3. package/LICENSE +1 -1
  4. package/README.md +73 -1
  5. package/bin/davinci-resolve-advanced-mcp.mjs +20 -0
  6. package/docs/README.md +19 -0
  7. package/docs/SKILL.md +62 -1
  8. package/docs/contributing.md +1 -1
  9. package/docs/guides/control-panel.md +9 -0
  10. package/docs/kernels/README.md +21 -0
  11. package/docs/kernels/audio-fairlight-kernel.md +22 -0
  12. package/docs/kernels/color-grade-kernel.md +51 -0
  13. package/docs/kernels/fusion-composition-kernel.md +17 -0
  14. package/docs/kernels/media-pool-ingest-kernel.md +20 -0
  15. package/docs/kernels/render-deliver-kernel.md +32 -0
  16. package/docs/kernels/timeline-conform-interchange-kernel.md +39 -0
  17. package/docs/kernels/timeline-edit-kernel.md +17 -0
  18. package/docs/process/release-process.md +29 -7
  19. package/docs/reference/api-coverage.md +1 -1
  20. package/docs/reference/api-limitations.md +38 -1
  21. package/install.py +27 -4
  22. package/package.json +23 -2
  23. package/resolve-advanced/README.md +194 -0
  24. package/resolve-advanced/package.json +50 -0
  25. package/resolve-advanced/server/black-balance.mjs +92 -0
  26. package/resolve-advanced/server/capabilities.mjs +86 -0
  27. package/resolve-advanced/server/cdl-io.mjs +76 -0
  28. package/resolve-advanced/server/contrast-normalize.mjs +180 -0
  29. package/resolve-advanced/server/db-patch.mjs +91 -0
  30. package/resolve-advanced/server/deliverable-entities.mjs +86 -0
  31. package/resolve-advanced/server/deliverable-qc.mjs +270 -0
  32. package/resolve-advanced/server/editorial.mjs +370 -0
  33. package/resolve-advanced/server/exposure-level.mjs +129 -0
  34. package/resolve-advanced/server/extract-frames.mjs +196 -0
  35. package/resolve-advanced/server/ffprobe-media.mjs +88 -0
  36. package/resolve-advanced/server/gamut-legal.mjs +112 -0
  37. package/resolve-advanced/server/grade-body-patch.mjs +246 -0
  38. package/resolve-advanced/server/grade-transfer.mjs +66 -0
  39. package/resolve-advanced/server/group-grade-read.mjs +137 -0
  40. package/resolve-advanced/server/index.mjs +105 -0
  41. package/resolve-advanced/server/lib.mjs +84 -0
  42. package/resolve-advanced/server/libs.mjs +65 -0
  43. package/resolve-advanced/server/lineage-db.mjs +479 -0
  44. package/resolve-advanced/server/lut-apply.mjs +61 -0
  45. package/resolve-advanced/server/match-to-reference.mjs +173 -0
  46. package/resolve-advanced/server/media-inventory.mjs +169 -0
  47. package/resolve-advanced/server/media-ops.mjs +243 -0
  48. package/resolve-advanced/server/node-meta-db.mjs +62 -0
  49. package/resolve-advanced/server/node-provenance.mjs +102 -0
  50. package/resolve-advanced/server/offline-ref-db.mjs +420 -0
  51. package/resolve-advanced/server/offline-ref.mjs +179 -0
  52. package/resolve-advanced/server/project-db.mjs +233 -0
  53. package/resolve-advanced/server/provenance-audit.mjs +210 -0
  54. package/resolve-advanced/server/qc-frame.mjs +224 -0
  55. package/resolve-advanced/server/qc-sampler.mjs +67 -0
  56. package/resolve-advanced/server/readback.mjs +77 -0
  57. package/resolve-advanced/server/render-manifest.mjs +93 -0
  58. package/resolve-advanced/server/reverse-clip-db.mjs +270 -0
  59. package/resolve-advanced/server/runner-apply-contract.mjs +64 -0
  60. package/resolve-advanced/server/runner.mjs +244 -0
  61. package/resolve-advanced/server/saturation-match.mjs +108 -0
  62. package/resolve-advanced/server/scope-read.mjs +257 -0
  63. package/resolve-advanced/server/season-look.mjs +96 -0
  64. package/resolve-advanced/server/shot-intent.mjs +82 -0
  65. package/resolve-advanced/server/shot-match.mjs +147 -0
  66. package/resolve-advanced/server/skin-match.mjs +205 -0
  67. package/resolve-advanced/server/spec-compile.mjs +217 -0
  68. package/resolve-advanced/server/tone-curve-transfer.mjs +184 -0
  69. package/resolve-advanced/server/tool-catalog.mjs +265 -0
  70. package/resolve-advanced/server/tools/audio.mjs +54 -0
  71. package/resolve-advanced/server/tools/audio_plan.mjs +58 -0
  72. package/resolve-advanced/server/tools/capabilities.mjs +16 -0
  73. package/resolve-advanced/server/tools/color_trace.mjs +165 -0
  74. package/resolve-advanced/server/tools/conform.mjs +291 -0
  75. package/resolve-advanced/server/tools/deliverable.mjs +105 -0
  76. package/resolve-advanced/server/tools/drp.mjs +232 -0
  77. package/resolve-advanced/server/tools/drt.mjs +200 -0
  78. package/resolve-advanced/server/tools/drx.mjs +943 -0
  79. package/resolve-advanced/server/tools/editorial.mjs +69 -0
  80. package/resolve-advanced/server/tools/fairlight.mjs +108 -0
  81. package/resolve-advanced/server/tools/fusion.mjs +64 -0
  82. package/resolve-advanced/server/tools/media.mjs +102 -0
  83. package/resolve-advanced/server/tools/offline_ref.mjs +123 -0
  84. package/resolve-advanced/server/tools/pipeline.mjs +137 -0
  85. package/resolve-advanced/server/tools/project_db.mjs +200 -0
  86. package/resolve-advanced/server/tools/project_read.mjs +212 -0
  87. package/resolve-advanced/server/tools/provenance.mjs +73 -0
  88. package/resolve-advanced/server/verify-grade.mjs +113 -0
  89. package/resolve-advanced/server/white-balance-match.mjs +122 -0
  90. package/resolve-advanced/vendor/audio/format-converter.js +330 -0
  91. package/resolve-advanced/vendor/audio/split.js +173 -0
  92. package/resolve-advanced/vendor/audio/trim.js +188 -0
  93. package/resolve-advanced/vendor/audio-fairlight/index.js +391 -0
  94. package/resolve-advanced/vendor/conform-qc/adapters/frame-sampler.js +56 -0
  95. package/resolve-advanced/vendor/conform-qc/adapters/local-ffmpeg-sampler.js +65 -0
  96. package/resolve-advanced/vendor/conform-qc/adapters/rendernode-ffmpeg-sampler.js +50 -0
  97. package/resolve-advanced/vendor/conform-qc/adapters/resolve/driver.py +176 -0
  98. package/resolve-advanced/vendor/conform-qc/adapters/resolve-driver.js +61 -0
  99. package/resolve-advanced/vendor/conform-qc/adapters/resolve-headless-driver.js +86 -0
  100. package/resolve-advanced/vendor/conform-qc/adapters/vision-validator.js +75 -0
  101. package/resolve-advanced/vendor/conform-qc/cli.js +63 -0
  102. package/resolve-advanced/vendor/conform-qc/compare/advisory.js +106 -0
  103. package/resolve-advanced/vendor/conform-qc/compare/decode.js +36 -0
  104. package/resolve-advanced/vendor/conform-qc/compare/index.js +61 -0
  105. package/resolve-advanced/vendor/conform-qc/compare/locate.js +70 -0
  106. package/resolve-advanced/vendor/conform-qc/compare/metrics.js +409 -0
  107. package/resolve-advanced/vendor/conform-qc/docs/runbook.md +72 -0
  108. package/resolve-advanced/vendor/conform-qc/index.js +49 -0
  109. package/resolve-advanced/vendor/conform-qc/knowledge/index.js +110 -0
  110. package/resolve-advanced/vendor/conform-qc/ops/conform-core.js +137 -0
  111. package/resolve-advanced/vendor/conform-qc/ops/conform-diff.js +109 -0
  112. package/resolve-advanced/vendor/conform-qc/ops/insert.js +108 -0
  113. package/resolve-advanced/vendor/conform-qc/ops/local-conform.js +61 -0
  114. package/resolve-advanced/vendor/conform-qc/ops/patch.js +80 -0
  115. package/resolve-advanced/vendor/conform-qc/ops/trigger.js +32 -0
  116. package/resolve-advanced/vendor/conform-qc/ops/verify.js +105 -0
  117. package/resolve-advanced/vendor/conform-qc/ops/workflow.js +30 -0
  118. package/resolve-advanced/vendor/conform-qc/oracle/emulate.js +155 -0
  119. package/resolve-advanced/vendor/conform-qc/oracle/index.js +38 -0
  120. package/resolve-advanced/vendor/conform-qc/oracle/resolve.js +196 -0
  121. package/resolve-advanced/vendor/conform-qc/package.json +16 -0
  122. package/resolve-advanced/vendor/conform-qc/packaging/emit-aaf.js +30 -0
  123. package/resolve-advanced/vendor/conform-qc/packaging/emit-fcp7.js +64 -0
  124. package/resolve-advanced/vendor/conform-qc/packaging/index.js +169 -0
  125. package/resolve-advanced/vendor/conform-qc/packaging/media-ops.js +56 -0
  126. package/resolve-advanced/vendor/conform-qc/packaging/otio.js +50 -0
  127. package/resolve-advanced/vendor/conform-qc/packaging/relink.js +50 -0
  128. package/resolve-advanced/vendor/conform-qc/packaging/surgical-relink.js +195 -0
  129. package/resolve-advanced/vendor/conform-qc/parse/index.js +54 -0
  130. package/resolve-advanced/vendor/conform-qc/parse/merge-dto.js +27 -0
  131. package/resolve-advanced/vendor/conform-qc/parse/xmeml-geometry.js +256 -0
  132. package/resolve-advanced/vendor/conform-qc/reference/burnin-ocr.js +100 -0
  133. package/resolve-advanced/vendor/conform-qc/reference/sidecar.js +37 -0
  134. package/resolve-advanced/vendor/conform-qc/repair/analysis.js +62 -0
  135. package/resolve-advanced/vendor/conform-qc/repair/index.js +78 -0
  136. package/resolve-advanced/vendor/conform-qc/repair/media-analysis.js +139 -0
  137. package/resolve-advanced/vendor/conform-qc/repair/media-index.js +135 -0
  138. package/resolve-advanced/vendor/conform-qc/repair/strategies.js +250 -0
  139. package/resolve-advanced/vendor/conform-qc/report/index.js +133 -0
  140. package/resolve-advanced/vendor/conform-qc/roles/index.js +96 -0
  141. package/resolve-advanced/vendor/conform-qc/synthetic/generate.js +233 -0
  142. package/resolve-advanced/vendor/conform-qc/test/advisory.test.js +85 -0
  143. package/resolve-advanced/vendor/conform-qc/test/cli.test.js +39 -0
  144. package/resolve-advanced/vendor/conform-qc/test/compare.test.js +152 -0
  145. package/resolve-advanced/vendor/conform-qc/test/conform-diff.test.js +70 -0
  146. package/resolve-advanced/vendor/conform-qc/test/emulate.test.js +51 -0
  147. package/resolve-advanced/vendor/conform-qc/test/frame-sampler.test.js +30 -0
  148. package/resolve-advanced/vendor/conform-qc/test/geometry.test.js +112 -0
  149. package/resolve-advanced/vendor/conform-qc/test/knowledge.test.js +56 -0
  150. package/resolve-advanced/vendor/conform-qc/test/lifecycle.test.js +110 -0
  151. package/resolve-advanced/vendor/conform-qc/test/local-ffmpeg-sampler.test.js +66 -0
  152. package/resolve-advanced/vendor/conform-qc/test/locate.test.js +63 -0
  153. package/resolve-advanced/vendor/conform-qc/test/media-analysis.test.js +63 -0
  154. package/resolve-advanced/vendor/conform-qc/test/media-index-normalized.test.js +65 -0
  155. package/resolve-advanced/vendor/conform-qc/test/media-index-origin.test.js +53 -0
  156. package/resolve-advanced/vendor/conform-qc/test/metrics-fast.test.js +91 -0
  157. package/resolve-advanced/vendor/conform-qc/test/oracle-reverse.test.js +86 -0
  158. package/resolve-advanced/vendor/conform-qc/test/oracle.test.js +136 -0
  159. package/resolve-advanced/vendor/conform-qc/test/p0-acceptance.test.js +60 -0
  160. package/resolve-advanced/vendor/conform-qc/test/p1-acceptance.test.js +75 -0
  161. package/resolve-advanced/vendor/conform-qc/test/p45-patch-acceptance.test.js +102 -0
  162. package/resolve-advanced/vendor/conform-qc/test/packaging.test.js +133 -0
  163. package/resolve-advanced/vendor/conform-qc/test/parse-stubs.test.js +28 -0
  164. package/resolve-advanced/vendor/conform-qc/test/repair-strategies.test.js +128 -0
  165. package/resolve-advanced/vendor/conform-qc/test/repair.test.js +84 -0
  166. package/resolve-advanced/vendor/conform-qc/test/report.test.js +52 -0
  167. package/resolve-advanced/vendor/conform-qc/test/resolve-readback.test.js +71 -0
  168. package/resolve-advanced/vendor/conform-qc/test/roles.test.js +107 -0
  169. package/resolve-advanced/vendor/conform-qc/test/runbook.test.js +30 -0
  170. package/resolve-advanced/vendor/conform-qc/test/scale-residual.test.js +40 -0
  171. package/resolve-advanced/vendor/conform-qc/test/sidecar.test.js +51 -0
  172. package/resolve-advanced/vendor/conform-qc/test/smoke.test.js +87 -0
  173. package/resolve-advanced/vendor/conform-qc/test/surgical-relink.test.js +73 -0
  174. package/resolve-advanced/vendor/conform-qc/test/synthetic.test.js +98 -0
  175. package/resolve-advanced/vendor/conform-qc/test/verify.test.js +93 -0
  176. package/resolve-advanced/vendor/conform-qc/test/vision-validator.test.js +43 -0
  177. package/resolve-advanced/vendor/conform-qc/test/workflow-driver.test.js +46 -0
  178. package/resolve-advanced/vendor/conform-qc/toolset/index.js +53 -0
  179. package/resolve-advanced/vendor/drp-format/README.md +116 -0
  180. package/resolve-advanced/vendor/drp-format/__tests__/_resolve-verify.js +39 -0
  181. package/resolve-advanced/vendor/drp-format/__tests__/add-media-clip.test.js +30 -0
  182. package/resolve-advanced/vendor/drp-format/__tests__/assemble-timeline.test.js +68 -0
  183. package/resolve-advanced/vendor/drp-format/__tests__/author-project.test.js +57 -0
  184. package/resolve-advanced/vendor/drp-format/__tests__/composition-text.test.js +98 -0
  185. package/resolve-advanced/vendor/drp-format/__tests__/diff.test.js +272 -0
  186. package/resolve-advanced/vendor/drp-format/__tests__/effect-filters-compressed.test.js +143 -0
  187. package/resolve-advanced/vendor/drp-format/__tests__/fixtures/p0-3-session30-empty-effectfilters.drp +0 -0
  188. package/resolve-advanced/vendor/drp-format/__tests__/fixtures/retimed-timemap-50pct.hex +1 -0
  189. package/resolve-advanced/vendor/drp-format/__tests__/fixtures/retimed-timemap-dynamic.hex +1 -0
  190. package/resolve-advanced/vendor/drp-format/__tests__/inject-grades.test.js +238 -0
  191. package/resolve-advanced/vendor/drp-format/__tests__/keyed-dict.test.js +85 -0
  192. package/resolve-advanced/vendor/drp-format/__tests__/lut-refs.test.js +119 -0
  193. package/resolve-advanced/vendor/drp-format/__tests__/media-blobs.test.js +25 -0
  194. package/resolve-advanced/vendor/drp-format/__tests__/media-timemap.test.js +119 -0
  195. package/resolve-advanced/vendor/drp-format/__tests__/place-fusion-title.test.js +52 -0
  196. package/resolve-advanced/vendor/drp-format/__tests__/place-generator.test.js +40 -0
  197. package/resolve-advanced/vendor/drp-format/__tests__/place-transition.test.js +48 -0
  198. package/resolve-advanced/vendor/drp-format/__tests__/protobuf-wire.test.js +64 -0
  199. package/resolve-advanced/vendor/drp-format/__tests__/relink-media.test.js +106 -0
  200. package/resolve-advanced/vendor/drp-format/__tests__/splice-clips.test.js +256 -0
  201. package/resolve-advanced/vendor/drp-format/assemble-timeline.js +65 -0
  202. package/resolve-advanced/vendor/drp-format/author-project.js +118 -0
  203. package/resolve-advanced/vendor/drp-format/composition-text.js +213 -0
  204. package/resolve-advanced/vendor/drp-format/curves-encoder.js +763 -0
  205. package/resolve-advanced/vendor/drp-format/diff.js +310 -0
  206. package/resolve-advanced/vendor/drp-format/drp-packager.js +201 -0
  207. package/resolve-advanced/vendor/drp-format/drp-validator.js +809 -0
  208. package/resolve-advanced/vendor/drp-format/effect-encoder.js +640 -0
  209. package/resolve-advanced/vendor/drp-format/extract-lut-refs.js +119 -0
  210. package/resolve-advanced/vendor/drp-format/grade-encoder.example.js +133 -0
  211. package/resolve-advanced/vendor/drp-format/grade-encoder.js +401 -0
  212. package/resolve-advanced/vendor/drp-format/grade-encoder.test.js +171 -0
  213. package/resolve-advanced/vendor/drp-format/grade-encoder.verify.js +249 -0
  214. package/resolve-advanced/vendor/drp-format/grade-node-extractor.js +495 -0
  215. package/resolve-advanced/vendor/drp-format/grade-parameter-decoder.js +774 -0
  216. package/resolve-advanced/vendor/drp-format/index.js +174 -0
  217. package/resolve-advanced/vendor/drp-format/inject-grades.js +219 -0
  218. package/resolve-advanced/vendor/drp-format/keyed-dict.js +227 -0
  219. package/resolve-advanced/vendor/drp-format/marker-encoder.js +532 -0
  220. package/resolve-advanced/vendor/drp-format/media-blobs.js +53 -0
  221. package/resolve-advanced/vendor/drp-format/media-timemap.js +164 -0
  222. package/resolve-advanced/vendor/drp-format/mp-folder-builder.js +293 -0
  223. package/resolve-advanced/vendor/drp-format/node-tree-encoder.js +1246 -0
  224. package/resolve-advanced/vendor/drp-format/package.json +18 -0
  225. package/resolve-advanced/vendor/drp-format/place-fusion-title.js +131 -0
  226. package/resolve-advanced/vendor/drp-format/place-generator.js +77 -0
  227. package/resolve-advanced/vendor/drp-format/place-transition.js +91 -0
  228. package/resolve-advanced/vendor/drp-format/power-window-encoder.js +776 -0
  229. package/resolve-advanced/vendor/drp-format/protobuf-wire.js +111 -0
  230. package/resolve-advanced/vendor/drp-format/qualifier-encoder.js +706 -0
  231. package/resolve-advanced/vendor/drp-format/relink-media.js +218 -0
  232. package/resolve-advanced/vendor/drp-format/rich-title-encoder.js +447 -0
  233. package/resolve-advanced/vendor/drp-format/seq-container-builder.js +572 -0
  234. package/resolve-advanced/vendor/drp-format/seq-surgery.js +155 -0
  235. package/resolve-advanced/vendor/drp-format/splice-clips.js +445 -0
  236. package/resolve-advanced/vendor/drp-format/templates/empty-project.drp +0 -0
  237. package/resolve-advanced/vendor/drp-format/templates/fusion-title.xml +56 -0
  238. package/resolve-advanced/vendor/drp-format/templates/generator-solid-color.xml +21 -0
  239. package/resolve-advanced/vendor/drp-format/templates/media-clip-h264.drp +0 -0
  240. package/resolve-advanced/vendor/drp-format/templates/transition-cross-dissolve.xml +22 -0
  241. package/resolve-advanced/vendor/drp-format/tool-registry.descriptor.js +80 -0
  242. package/resolve-advanced/vendor/drp-format/utils/safe-archive.js +54 -0
  243. package/resolve-advanced/vendor/drp-format/xml-builder.js +759 -0
  244. package/resolve-advanced/vendor/drt-format/__tests__/_resolve-verify.js +21 -0
  245. package/resolve-advanced/vendor/drt-format/__tests__/build.test.js +113 -0
  246. package/resolve-advanced/vendor/drt-format/__tests__/multi-format.test.js +93 -0
  247. package/resolve-advanced/vendor/drt-format/__tests__/parse-real-resolve21.test.js +51 -0
  248. package/resolve-advanced/vendor/drt-format/__tests__/parse.test.js +134 -0
  249. package/resolve-advanced/vendor/drt-format/__tests__/resolve-versions.test.js +120 -0
  250. package/resolve-advanced/vendor/drt-format/__tests__/schema-fingerprint.test.js +62 -0
  251. package/resolve-advanced/vendor/drt-format/__tests__/smoke.test.js +27 -0
  252. package/resolve-advanced/vendor/drt-format/__tests__/validate.test.js +109 -0
  253. package/resolve-advanced/vendor/drt-format/capabilities.js +69 -0
  254. package/resolve-advanced/vendor/drt-format/capability-domains.js +56 -0
  255. package/resolve-advanced/vendor/drt-format/drt-builder.js +42 -0
  256. package/resolve-advanced/vendor/drt-format/drt-parser.js +164 -0
  257. package/resolve-advanced/vendor/drt-format/drt-validator.js +98 -0
  258. package/resolve-advanced/vendor/drt-format/index.js +45 -0
  259. package/resolve-advanced/vendor/drt-format/package.json +17 -0
  260. package/resolve-advanced/vendor/drt-format/resolve-versions.js +153 -0
  261. package/resolve-advanced/vendor/drt-format/schema-fingerprint.js +74 -0
  262. package/resolve-advanced/vendor/drt-format/templates/drt-project-shell.xml +124 -0
  263. package/resolve-advanced/vendor/drx-codec/__tests__/curves-roundtrip.test.js +148 -0
  264. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-face-refinement.drx +58 -0
  265. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-filmgrain.drx +58 -0
  266. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-glow.drx +58 -0
  267. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-lens-flare.drx +58 -0
  268. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-3-beauty-25.drx +58 -0
  269. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-3-beauty-75.drx +58 -0
  270. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p5-1-node-no-lut.drx +58 -0
  271. package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p5-1-node-with-lut.drx +58 -0
  272. package/resolve-advanced/vendor/drx-codec/__tests__/hsl-curves-roundtrip.test.js +117 -0
  273. package/resolve-advanced/vendor/drx-codec/__tests__/lut-refs-roundtrip.test.js +112 -0
  274. package/resolve-advanced/vendor/drx-codec/__tests__/matte-roundtrip.test.js +167 -0
  275. package/resolve-advanced/vendor/drx-codec/__tests__/ofx-roundtrip.test.js +215 -0
  276. package/resolve-advanced/vendor/drx-codec/__tests__/ofx-slugs-discovery.test.js +149 -0
  277. package/resolve-advanced/vendor/drx-codec/__tests__/power-window-roundtrip.test.js +230 -0
  278. package/resolve-advanced/vendor/drx-codec/__tests__/qualifier-roundtrip.test.js +226 -0
  279. package/resolve-advanced/vendor/drx-codec/cdl-exporter.js +470 -0
  280. package/resolve-advanced/vendor/drx-codec/color-nlp-parser.js +833 -0
  281. package/resolve-advanced/vendor/drx-codec/data/color-nlp-action-mappings.json +2232 -0
  282. package/resolve-advanced/vendor/drx-codec/drx-generator.js +4123 -0
  283. package/resolve-advanced/vendor/drx-codec/drx-merger.js +320 -0
  284. package/resolve-advanced/vendor/drx-codec/drx-parser.js +1808 -0
  285. package/resolve-advanced/vendor/drx-codec/extract-custom-curves.js +215 -0
  286. package/resolve-advanced/vendor/drx-codec/extract-hsl-curves.js +180 -0
  287. package/resolve-advanced/vendor/drx-codec/extract-lut-refs.js +158 -0
  288. package/resolve-advanced/vendor/drx-codec/extract-matte-finesse.js +83 -0
  289. package/resolve-advanced/vendor/drx-codec/extract-ofx-params.js +226 -0
  290. package/resolve-advanced/vendor/drx-codec/extract-power-window.js +184 -0
  291. package/resolve-advanced/vendor/drx-codec/extract-qualifier.js +99 -0
  292. package/resolve-advanced/vendor/drx-codec/index.js +109 -0
  293. package/resolve-advanced/vendor/drx-codec/node-layout.js +249 -0
  294. package/resolve-advanced/vendor/drx-codec/resolved-drx-generator.js +393 -0
  295. package/resolve-advanced/vendor/drx-codec/tool-resolver.js +974 -0
  296. package/resolve-advanced/vendor/drx-codec/training-examples.js +548 -0
  297. package/resolve-advanced/vendor/drx-codec/vocabulary-intents.js +539 -0
  298. package/resolve-advanced/vendor/drx-parameters/CALIBRATION-STATUS.md +319 -0
  299. package/resolve-advanced/vendor/drx-parameters/DRX-VALUE-SCALING.md +251 -0
  300. package/resolve-advanced/vendor/drx-parameters/calibrated-ranges.json +17737 -0
  301. package/resolve-advanced/vendor/drx-parameters/corrector-types.js +120 -0
  302. package/resolve-advanced/vendor/drx-parameters/index.js +193 -0
  303. package/resolve-advanced/vendor/drx-parameters/parameter-codec.js +451 -0
  304. package/resolve-advanced/vendor/drx-parameters/parameter-ids.js +1292 -0
  305. package/resolve-advanced/vendor/drx-parameters/parameter-ranges.js +570 -0
  306. package/resolve-advanced/vendor/drx-parameters/parameter-validator.js +392 -0
  307. package/resolve-advanced/vendor/drx-parameters/resolvefx-registry.json +14973 -0
  308. package/resolve-advanced/vendor/drx-parameters/tool-registry.descriptor.js +38 -0
  309. package/resolve-advanced/vendor/drx-parameters/tools/drx-analyzer.js +371 -0
  310. package/resolve-advanced/vendor/fairlight/index.js +833 -0
  311. package/resolve-advanced/vendor/fusion-codec/composition-generator.js +401 -0
  312. package/resolve-advanced/vendor/fusion-codec/templates/blur-region.js +103 -0
  313. package/resolve-advanced/vendor/fusion-codec/templates/color-correct.js +87 -0
  314. package/resolve-advanced/vendor/fusion-codec/templates/film-grain.js +106 -0
  315. package/resolve-advanced/vendor/fusion-codec/templates/lower-third.js +241 -0
  316. package/resolve-advanced/vendor/fusion-codec/templates/picture-in-picture.js +188 -0
  317. package/resolve-advanced/vendor/fusion-codec/templates/text-overlay.js +161 -0
  318. package/resolve-advanced/vendor/fusion-codec/templates/title-card.js +184 -0
  319. package/resolve-advanced/vendor/fusion-codec/templates/vignette.js +107 -0
  320. package/resolve-advanced/vendor/fusion-codec/templates/watermark.js +101 -0
  321. package/scripts/agent-rules/README.md +68 -0
  322. package/src/analysis_dashboard.py +233 -1
  323. package/src/granular/common.py +1 -1
  324. package/src/server.py +479 -29
  325. package/src/utils/api_truth.py +117 -0
  326. package/src/utils/cut_ir.py +1 -1
  327. package/src/utils/failure_tracker.py +0 -2
  328. 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 };