Skip to content

Commit ecb998b

Browse files
committed
feat(audio): a rough mix that measures what it achieved
The pieces were already here — media_analysis measures EBU R128 loudness and detects silence, delivery_targets holds the standards, loudness_qc grades a finished file. Missing was the step between measuring and grading: deciding the gains. Adds `media_analysis mix_plan`, `measure_loudness`, and `mix_plan_capabilities`. mix_plan derives the dialogue-normalisation gain, a music-bed level relative to it, and ducking windows from silence detection on the dialogue stem, so the bed follows the words rather than a hand-placed envelope. dry_run defaults to true and renders nothing. What makes it worth having: - The achieved loudness is MEASURED. The premix is rendered, then re-measured; `achieved` carries integrated LUFS, true peak, range and the delta from target. A plan that hits its target on paper and clips on true peak is a failed plan, and only measuring distinguishes them. - Dialogue-anchored, then programme-trimmed. Anchoring dialogue at target is right for a dialogue-gated standard and wrong for a full-programme one the moment a bed is added. For non-dialogue-gated standards one measured trim is applied to everything equally, preserving the dialogue-to-bed relationship, and reported as program_normalize.trim_db. It never runs on a dialogue-gated standard, where dialogue is the figure being graded. - Nothing else is corrected. loudness_off_target, true_peak_over and clipped come back as flags with remedies, never as a quietly normalised file. Standards come from delivery_targets, not a second copy of the same table. The loudness parser reads the Summary block AND drops ebur128's per-frame progress lines, which carry their own I: and LRA:. Last-match-wins is correct only because ffmpeg happens to print the summary last, and scoping to the summary alone still swallows a progress line printed after it. Both steps are needed; a test with a trailing progress line pins it. Scope is a rough mix — gain staging, a bed, ducking. No EQ, compression, de-essing or limiting, and capabilities() says so rather than leaving it implied. Release 2.102.0. Suite 2889 -> 2924. Three mutations (silent peak normalisation, trimming a dialogue-gated standard, dropping the parser scoping) were each caught; the parser one only after the test was strengthened, since the first version passed with and without the fix.
1 parent d7e31f0 commit ecb998b

13 files changed

Lines changed: 1144 additions & 9 deletions

File tree

.claude/skills/resolve-audio/SKILL.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ Thin router; depth stays in the kernel.
1212
- **Offline planning + bus routing**`resolve-advanced/README.md`
1313
`audio_plan`, `fairlight`, `audio`.
1414

15+
## Rough mix before anything else
16+
17+
If the ask is "make this sound balanced" rather than "route these buses", start with
18+
`media_analysis mix_plan` (Python server, no Resolve needed). Give it the dialogue stems
19+
and it derives the dialogue-normalisation gain, the music-bed level under it, and ducking
20+
windows from the dialogue's own silence — then renders a premix and **measures it**, so
21+
what you report is the loudness achieved rather than the gain arithmetic.
22+
23+
`dry_run` defaults to true: show the gains and the window count first. On a
24+
full-programme standard (`ebu_r128`, `web`) a measured programme trim lands the whole mix
25+
on target; on `ott_dialogue_gated` it deliberately does not trim, because dialogue is the
26+
figure being graded. Flags (`loudness_off_target`, `true_peak_over`, `clipped`) come back
27+
with remedies and are never auto-corrected — report them, do not paper over them.
28+
1529
## Two servers — plan/measure offline, apply live
1630

1731
| Job | Server | Tools |

CHANGELOG.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,65 @@
22

33
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
44

5+
## What's New in v2.102.0
6+
7+
**A rough mix that reports what it achieved, not what it intended.** The pieces were
8+
already here — `media_analysis` measures EBU R128 loudness and detects silence,
9+
`delivery_targets` holds the standards, `loudness_qc` grades a finished file. What was
10+
missing is the step between measuring and grading: deciding the gains.
11+
12+
### Added
13+
14+
- **`media_analysis mix_plan`** — dialogue-normalisation gain, a music-bed level relative
15+
to it, and ducking windows derived from silence detection **on the dialogue stem**, so
16+
the bed follows the words rather than a hand-placed envelope. `dry_run` defaults to
17+
true and renders nothing.
18+
- **The achieved loudness is measured, not derived.** The premix is rendered, then
19+
re-measured; `achieved` carries integrated LUFS, true peak, loudness range, and the
20+
delta from target. A plan that hits its target on paper and clips on true peak is a
21+
failed plan, and only the measurement tells you which one you have.
22+
- **Dialogue-anchored, then programme-trimmed.** Anchoring dialogue at target is right
23+
for a dialogue-gated standard and wrong for a full-programme one the moment a bed is
24+
added. For non-dialogue-gated standards one measured trim is applied to everything
25+
equally — preserving the dialogue-to-bed relationship — and reported as
26+
`program_normalize.trim_db`. It never runs on a dialogue-gated standard, where
27+
dialogue is the figure being graded.
28+
- **Nothing else is corrected.** `loudness_off_target`, `true_peak_over`, and `clipped`
29+
come back as flags with remedies, never as a quietly normalised file.
30+
- Standards come from `delivery_targets` (`web`, `podcast`, `ebu_r128`, `atsc_a85`,
31+
`ott_dialogue_gated`) — the table the delivery tools already grade against, not a
32+
second copy.
33+
- **`media_analysis measure_loudness`** — integrated LUFS, loudness range, and true peak
34+
per file.
35+
- **`media_analysis mix_plan_capabilities`** — dependency state, known standards, and the
36+
defaults, including the music-bed offset, which is the number most likely to be argued
37+
with and so is named rather than buried.
38+
39+
### Fixed while building it
40+
41+
The new loudness parser reads the `Summary:` block **and** drops ebur128's per-frame
42+
progress lines, which carry their own `I:` and `LRA:` fields. A plain last-match-wins
43+
parse is correct only because ffmpeg happens to print the summary last, and scoping to
44+
the summary alone still swallows a progress line printed after it. Both steps are needed;
45+
a test with a trailing progress line pins it.
46+
47+
### Scope
48+
49+
A rough mix: gain staging, a bed, and ducking. No EQ, compression, de-essing, or
50+
limiting, and the module says so in its capabilities rather than leaving it implied.
51+
52+
### Validation
53+
54+
- Offline suite: 2924 passed, 1 skipped, 711 subtests, 0 failures.
55+
- End-to-end through real ffmpeg on generated tones: target hit from measurement, the
56+
programme trim landing a hot bed on R128, a dialogue-gated standard refusing the trim,
57+
and clipping reported rather than normalised away.
58+
- Three deliberate mutations (silent peak normalisation, trimming a dialogue-gated
59+
standard, and dropping the parser scoping) were each caught. The parser mutation was
60+
caught only after the test was strengthened — the first version of it passed against
61+
both the fix and its absence.
62+
- No Resolve behavior changed; live test not required.
63+
564
## What's New in v2.101.0
665

766
**A grade can now reject itself.** `assess_grade` has measured grade damage since

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
English | [简体中文](README.zh-CN.md)
44

5-
[![Version](https://img.shields.io/badge/version-2.101.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5+
[![Version](https://img.shields.io/badge/version-2.102.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
66
[![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
77
[![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
88
[![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)

README.zh-CN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[English](README.md) | 简体中文
44

5-
[![Version](https://img.shields.io/badge/version-2.101.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5+
[![Version](https://img.shields.io/badge/version-2.102.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
66
[![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
77
[![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
88
[![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
1212
[![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
1313
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
1414

15-
> 本翻译对应 v2.101.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15+
> 本翻译对应 v2.102.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
1616
1717
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
1818

docs/SKILL.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,13 @@ Key actions:
552552
`load_user_preferences_preset`
553553
- `quit` — terminates Resolve (destructive; confirm with user first)
554554

555+
**Offline audio and image QC on `media_analysis`** — no Resolve connection required:
556+
`measure_loudness`, `mix_plan` / `mix_plan_capabilities` (dialogue-anchored rough mix
557+
with dialogue-following ducking, rendered and re-measured), and `assess_grade` /
558+
`grade_loop` / `grade_loop_capabilities` (numeric grade-damage QC and the retry ladder
559+
that backs a look off until it stops damaging the picture). See
560+
`docs/kernels/audio-fairlight-kernel.md` and `docs/kernels/color-grade-kernel.md`.
561+
555562
**`layout_presets`** — Save, load, export, import, delete UI layout presets.
556563
`list` (Resolve 21.0.4+) enumerates the saved preset names the other actions
557564
take.

docs/kernels/audio-fairlight-kernel.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,39 @@ All kernel actions are exposed through `timeline`.
5353
- Subtitle generation from the generated timeline returned true.
5454
- Fairlight preset listing and the full boundary report worked.
5555

56+
## Rough mix (`media_analysis`)
57+
58+
Offline and file-based — no Resolve connection — sitting between measuring loudness and
59+
grading it against a spec. The step nothing else covered was deciding the gains.
60+
61+
- `measure_loudness(path|paths)` — integrated LUFS, loudness range, and true peak per
62+
file via ffmpeg `ebur128`.
63+
- `mix_plan(dialogue[], music?, sfx?, standard?, target_lufs?, bed_offset_lu?, duck_db?,
64+
attack_s?, release_s?, hold_s?, dry_run?, output_path?, program_normalize?)` —
65+
dialogue-normalisation gain, a bed level relative to it, and ducking windows derived
66+
from silence detection **on the dialogue stem**, so the bed follows the words rather
67+
than a hand-placed envelope. `dry_run` defaults to true and renders nothing.
68+
- `mix_plan_capabilities()` — dependency state, the standards it knows, and the defaults.
69+
70+
Three things worth knowing before using it:
71+
72+
- **The achieved loudness is measured, not derived.** The premix is rendered, then
73+
re-measured, and `achieved` carries what came back. A plan that hits its target on
74+
paper and clips on true peak is a failed plan; only the measurement distinguishes them.
75+
- **Dialogue-anchored, then programme-trimmed.** Anchoring dialogue at target is right
76+
for a dialogue-gated standard and wrong for a full-programme one the moment a bed is
77+
added. For non-dialogue-gated standards a single measured trim is applied to everything
78+
equally — preserving the dialogue-to-bed relationship — and reported as
79+
`program_normalize.trim_db`. It never runs on a dialogue-gated standard.
80+
- **Nothing else is corrected.** Over true peak or clipped comes back as a flag with a
81+
remedy, never as a quietly normalised file. Pulling the mix down to fix a peak would
82+
move the loudness off the target it just hit.
83+
84+
Standards come from `src/utils/delivery_targets.py` (`web`, `podcast`, `ebu_r128`,
85+
`atsc_a85`, `ott_dialogue_gated`) — the same table the delivery tools grade against, not
86+
a second copy. It is a rough mix: gain staging, a bed, and ducking. No EQ, compression,
87+
de-essing, or limiting.
88+
5689
## Boundaries
5790

5891
- Timeline item audio properties may be readable as `None` and can reject writes

install.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
# ─── Version ──────────────────────────────────────────────────────────────────
3939

40-
VERSION = "2.101.0"
40+
VERSION = "2.102.0"
4141
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
4242
# Resolve's scripting bridge loads into newer interpreters on recent builds
4343
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "davinci-resolve-mcp",
3-
"version": "2.101.0",
3+
"version": "2.102.0",
44
"description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
55
"license": "MIT",
66
"author": "Samuel Gursky <samgursky@gmail.com>",

src/granular/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
handlers=[logging.StreamHandler()],
8888
)
8989

90-
VERSION = "2.101.0"
90+
VERSION = "2.102.0"
9191
logger = logging.getLogger("davinci-resolve-mcp")
9292
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
9393
logger.info(f"Detected platform: {get_platform()}")

0 commit comments

Comments
 (0)