davinci-resolve-mcp 2.100.0 → 2.102.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/CHANGELOG.md +120 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +7 -0
- package/docs/guides/color-decision-guide.md +30 -0
- package/docs/kernels/audio-fairlight-kernel.md +33 -0
- package/docs/kernels/color-grade-kernel.md +22 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +104 -1
- package/src/utils/cube_lut.py +270 -0
- package/src/utils/grade_loop.py +356 -0
- package/src/utils/mix_plan.py +612 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,126 @@
|
|
|
2
2
|
|
|
3
3
|
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.
|
|
4
4
|
|
|
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
|
+
|
|
64
|
+
## What's New in v2.101.0
|
|
65
|
+
|
|
66
|
+
**A grade can now reject itself.** `assess_grade` has measured grade damage since
|
|
67
|
+
v2.68.0 — banding in a sky, highlight levels collapsing, shadow grain amplified into
|
|
68
|
+
noise — and every flag it raises carries a remedy. Nothing consumed that report. The
|
|
69
|
+
measurement existed; the loop did not, so the remedy "reduce the strength" was advice an
|
|
70
|
+
agent had no way to act on.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **`media_analysis grade_loop`** — the retry ladder. Applies a look LUT, measures the
|
|
75
|
+
real decoded frame, and on any flag retries with the same look attenuated toward
|
|
76
|
+
identity (strength x 0.8 per rung, floored at 0.5, three tries by default). The first
|
|
77
|
+
strength that clears every sampled frame wins.
|
|
78
|
+
- **A flagged result is never reported acceptable.** An exhausted ladder returns
|
|
79
|
+
`needs_human` with the best attempt and its remaining flags — never a quiet success
|
|
80
|
+
at a strength that still bands.
|
|
81
|
+
- **Every sampled frame must pass.** `times=[...]` samples several timestamps and the
|
|
82
|
+
report names the one that failed; a grade clean on the frame you happened to check
|
|
83
|
+
is not a grade that passed.
|
|
84
|
+
- **The best attempt is the gentlest.** When nothing converges, attempts rank by flag
|
|
85
|
+
count with ties broken by the smallest colour shift — equal damage means taking the
|
|
86
|
+
one a human has less to undo.
|
|
87
|
+
- **It does not touch the project.** The result is an apply manifest with
|
|
88
|
+
`safe_to_apply`, and a flagged result carries the reason it is blocked.
|
|
89
|
+
- `dry_run` defaults to true and reports the ffmpeg decode budget before anyone
|
|
90
|
+
commits to it. `cost_tier` defaults to `numeric`, because escalating every rung to
|
|
91
|
+
vision would spend host turns on attempts that exist to be rejected.
|
|
92
|
+
- **`media_analysis grade_loop_capabilities`** — dependency state, ladder constants, and
|
|
93
|
+
an explicit statement of which modes exist.
|
|
94
|
+
- **`src/utils/cube_lut.py`** — read, write, and attenuate 3D `.cube` LUTs. Attenuation
|
|
95
|
+
is a blend toward identity, the same operation a LUT mix control performs. Exact at
|
|
96
|
+
both endpoints: strength 1.0 returns the table unchanged and 0.0 returns true
|
|
97
|
+
identity. 1D LUTs are refused by name, and attenuation on a non-unit
|
|
98
|
+
`DOMAIN_MIN`/`DOMAIN_MAX` is refused because identity is only identity on 0..1.
|
|
99
|
+
|
|
100
|
+
### Not built, and said so
|
|
101
|
+
|
|
102
|
+
The in-loop **live** mode — apply in Resolve, render a frame, assess, repeat — is not
|
|
103
|
+
implemented. It needs a single-frame render per rung, and shipping it unvalidated would
|
|
104
|
+
put a "verified live" claim behind something no runnable command has produced.
|
|
105
|
+
`grade_loop_capabilities()` says this in the response rather than only in the docs. The
|
|
106
|
+
offline LUT ladder is complete and validated.
|
|
107
|
+
|
|
108
|
+
### Documentation
|
|
109
|
+
|
|
110
|
+
- `docs/guides/color-decision-guide.md` — a new "Rejecting Your Own Grade" section on
|
|
111
|
+
when measurement beats eyeballing a compressed preview.
|
|
112
|
+
- `docs/kernels/color-grade-kernel.md` — the numeric grade-QC actions and their
|
|
113
|
+
display-referred-only contract.
|
|
114
|
+
|
|
115
|
+
### Validation
|
|
116
|
+
|
|
117
|
+
- Offline suite: 2889 passed, 1 skipped, 711 subtests, 0 failures.
|
|
118
|
+
- End-to-end through real ffmpeg on generated media: a look that converges only after
|
|
119
|
+
backing off, and one that never converges and says so.
|
|
120
|
+
- Two deliberate mutations — `acceptable` hard-coded true, and a rung passing on its
|
|
121
|
+
first clean frame — were each caught by the new tests.
|
|
122
|
+
- No Resolve behavior changed; live test not required. A test asserts no Resolve
|
|
123
|
+
connection is attempted.
|
|
124
|
+
|
|
5
125
|
## What's New in v2.100.0
|
|
6
126
|
|
|
7
127
|
**The craft guidance is now readable by any MCP client.** This repository carries a
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.102.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/docs/SKILL.md
CHANGED
|
@@ -552,6 +552,13 @@ Key actions:
|
|
|
552
552
|
`load_user_preferences_preset`
|
|
553
553
|
- `quit` — terminates Resolve (destructive; confirm with user first)
|
|
554
554
|
|
|
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
|
+
|
|
555
562
|
**`layout_presets`** — Save, load, export, import, delete UI layout presets.
|
|
556
563
|
`list` (Resolve 21.0.4+) enumerates the saved preset names the other actions
|
|
557
564
|
take.
|
|
@@ -211,6 +211,36 @@ If untreated/current/after comparison is not available through the API in the
|
|
|
211
211
|
moment, say which part is unavailable and whether the user wants a blind/global
|
|
212
212
|
pass. Do not imply that a grade was reviewed if no rendered frames were checked.
|
|
213
213
|
|
|
214
|
+
## Rejecting Your Own Grade
|
|
215
|
+
|
|
216
|
+
Looking at frames tells you whether a grade reads right. It does not reliably tell you
|
|
217
|
+
whether it *damaged* the picture — banding in a sky, highlight levels collapsing, shadow
|
|
218
|
+
grain amplified into visible noise. Those are measurable, and measuring them is cheaper
|
|
219
|
+
and steadier than eyeballing a compressed preview.
|
|
220
|
+
|
|
221
|
+
`media_analysis assess_grade` measures one graded frame against its source and returns
|
|
222
|
+
flags with remedies. `media_analysis grade_loop` closes the loop: it applies a look LUT,
|
|
223
|
+
measures the real decoded result, and on any flag retries with the same look attenuated
|
|
224
|
+
toward identity (strength × 0.8 per rung, floored at 0.5, three tries by default). The
|
|
225
|
+
first strength that clears every sampled frame wins.
|
|
226
|
+
|
|
227
|
+
Three properties matter more than the convenience:
|
|
228
|
+
|
|
229
|
+
- **A flagged result is never reported acceptable.** When the ladder is exhausted the
|
|
230
|
+
answer is `needs_human`, carrying the best attempt and what it still fails on. There
|
|
231
|
+
is no strength at which the loop shrugs and ships.
|
|
232
|
+
- **Every sampled frame must pass.** Pass several timestamps — a grade that is clean on
|
|
233
|
+
the frame you happened to check and bands two hundred frames later has not passed
|
|
234
|
+
anything. The report names the frame that failed.
|
|
235
|
+
- **It does not touch the project.** The loop returns an apply manifest and stops.
|
|
236
|
+
Applying a grade is still a deliberate, version-protected step, and a result carrying
|
|
237
|
+
unresolved flags should reach a human before it reaches a timeline.
|
|
238
|
+
|
|
239
|
+
Reach for it when applying an unfamiliar look LUT to unfamiliar footage, which is
|
|
240
|
+
exactly where "it looked fine on the first shot" goes wrong. The flags are advisory
|
|
241
|
+
thresholds, not standards — the raw measurements come back too, so a colorist can
|
|
242
|
+
disagree with the flag rather than only with the verdict.
|
|
243
|
+
|
|
214
244
|
## Safe Color Workflow
|
|
215
245
|
|
|
216
246
|
Before changing color:
|
|
@@ -53,6 +53,39 @@ All kernel actions are exposed through `timeline`.
|
|
|
53
53
|
- Subtitle generation from the generated timeline returned true.
|
|
54
54
|
- Fairlight preset listing and the full boundary report worked.
|
|
55
55
|
|
|
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
|
+
|
|
56
89
|
## Boundaries
|
|
57
90
|
|
|
58
91
|
- Timeline item audio properties may be readable as `None` and can reject writes
|
|
@@ -67,6 +67,28 @@ All actions are exposed through `timeline_item_color`.
|
|
|
67
67
|
and delete worked.
|
|
68
68
|
- Gallery capability and album list/create calls worked.
|
|
69
69
|
|
|
70
|
+
## Numeric grade QC (`media_analysis`)
|
|
71
|
+
|
|
72
|
+
Not part of the live kernel — no Resolve connection is involved — but it belongs to the
|
|
73
|
+
same decision. Both actions measure a decoded frame of the real result, never a
|
|
74
|
+
simulated transform, because LUT interpolation and encode rounding are where banding is
|
|
75
|
+
actually introduced.
|
|
76
|
+
|
|
77
|
+
- `assess_grade(source_path, time_seconds, graded_path|lut_path, working_space)` —
|
|
78
|
+
flags (`flat`, `washed_out`, `milky`, `noisy`, `clipped`, `posterized`, `banding`),
|
|
79
|
+
each with a remedy, plus the raw tonal/noise/damage measurements.
|
|
80
|
+
- `grade_loop(source_path, lut_path, times[], strength?, max_tries?, strength_floor?,
|
|
81
|
+
dry_run?)` — the retry ladder over `assess_grade`. Attenuates the look toward identity
|
|
82
|
+
until every sampled frame clears, or returns `needs_human` with the best attempt.
|
|
83
|
+
`dry_run` defaults to **true** and reports the ffmpeg decode budget first.
|
|
84
|
+
- `grade_loop_capabilities()` — dependency state, ladder constants, and which modes
|
|
85
|
+
exist. The in-loop **live** mode (apply in Resolve, render, assess) is **not built**;
|
|
86
|
+
the loop returns an apply manifest instead of driving the project.
|
|
87
|
+
|
|
88
|
+
Both are display-referred only. Log and scene-referred encodings run through the same
|
|
89
|
+
arithmetic happily and produce meaningless numbers, so `working_space` must be declared
|
|
90
|
+
and non-display-referred values are refused rather than guessed at.
|
|
91
|
+
|
|
70
92
|
## Boundaries
|
|
71
93
|
|
|
72
94
|
- Node graph internals are intentionally limited by Resolve's public API. The
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.102.0"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.102.0"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.102.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -20076,6 +20076,104 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
20076
20076
|
"metrics are undefined on them and will not be guessed at."
|
|
20077
20077
|
),
|
|
20078
20078
|
)
|
|
20079
|
+
if action == "grade_loop":
|
|
20080
|
+
# The retry ladder that consumes assess_grade's own verdict: apply the look,
|
|
20081
|
+
# measure the real decoded frame, and on any flag retry with the look
|
|
20082
|
+
# attenuated toward identity. Exhausting the ladder returns needs_human with
|
|
20083
|
+
# the best attempt — never a quiet success at a strength that still bands.
|
|
20084
|
+
from src.utils import grade_loop as _grade_loop_mod
|
|
20085
|
+
|
|
20086
|
+
source = str(p.get("source_path") or p.get("sourcePath") or "")
|
|
20087
|
+
lut = str(p.get("lut_path") or p.get("lutPath") or "")
|
|
20088
|
+
err, _clean = _validate_params(
|
|
20089
|
+
{"source_path": source, "lut_path": lut},
|
|
20090
|
+
{
|
|
20091
|
+
"source_path": {"type": str, "required": True, "non_empty": True},
|
|
20092
|
+
"lut_path": {"type": str, "required": True, "non_empty": True},
|
|
20093
|
+
},
|
|
20094
|
+
)
|
|
20095
|
+
if err:
|
|
20096
|
+
return _err(err)
|
|
20097
|
+
kwargs = dict(
|
|
20098
|
+
times=p.get("times"),
|
|
20099
|
+
time_seconds=p.get("time_seconds", p.get("timeSeconds")),
|
|
20100
|
+
strength=float(p.get("strength", 1.0) or 1.0),
|
|
20101
|
+
max_tries=int(p.get("max_tries", p.get("maxTries", _grade_loop_mod.DEFAULT_MAX_TRIES))),
|
|
20102
|
+
strength_floor=float(
|
|
20103
|
+
p.get("strength_floor", p.get("strengthFloor", _grade_loop_mod.DEFAULT_STRENGTH_FLOOR))
|
|
20104
|
+
),
|
|
20105
|
+
working_space=str(p.get("working_space") or p.get("workingSpace") or "rec709"),
|
|
20106
|
+
cost_tier=str(p.get("cost_tier") or p.get("costTier") or _grade_loop_mod.DEFAULT_COST_TIER),
|
|
20107
|
+
)
|
|
20108
|
+
try:
|
|
20109
|
+
# Dry run by default: the ladder can spend a dozen ffmpeg decodes per clip,
|
|
20110
|
+
# and the plan names that budget before anyone commits to it.
|
|
20111
|
+
if p.get("dry_run", True):
|
|
20112
|
+
return _ok(**_grade_loop_mod.plan(source, lut, **kwargs))
|
|
20113
|
+
return _ok(**_grade_loop_mod.run(
|
|
20114
|
+
source, lut,
|
|
20115
|
+
output_dir=(p.get("output_dir") or p.get("outputDir")) or None,
|
|
20116
|
+
**kwargs,
|
|
20117
|
+
))
|
|
20118
|
+
except (_grade_loop_mod.GradeLoopError, _grade_loop_mod.cube_lut.CubeLutError) as exc:
|
|
20119
|
+
return _err(str(exc), code="GRADE_LOOP_REFUSED", category="invalid_input",
|
|
20120
|
+
remediation=(
|
|
20121
|
+
"Supply an existing source_path and a 3D .cube lut_path, plus "
|
|
20122
|
+
"times=[seconds,...] to sample. A grade clean on one frame is "
|
|
20123
|
+
"not a grade that passed."
|
|
20124
|
+
))
|
|
20125
|
+
except _grade_loop_mod.image_qc.ImageQcError as exc:
|
|
20126
|
+
return _err(str(exc), code="IMAGE_QC_REFUSED", category="invalid_input")
|
|
20127
|
+
if action == "grade_loop_capabilities":
|
|
20128
|
+
from src.utils import grade_loop as _grade_loop_mod
|
|
20129
|
+
|
|
20130
|
+
return _ok(**_grade_loop_mod.capabilities())
|
|
20131
|
+
if action in {"mix_plan", "measure_loudness", "mix_plan_capabilities"}:
|
|
20132
|
+
# Gain staging between measuring loudness and grading it: dialogue-norm gain,
|
|
20133
|
+
# a bed level relative to it, and ducking windows the dialogue itself implies.
|
|
20134
|
+
# The render is measured afterwards, so what comes back is the loudness
|
|
20135
|
+
# achieved rather than the arithmetic meant to produce it.
|
|
20136
|
+
from src.utils import mix_plan as _mix_plan_mod
|
|
20137
|
+
|
|
20138
|
+
if action == "mix_plan_capabilities":
|
|
20139
|
+
return _ok(**_mix_plan_mod.capabilities())
|
|
20140
|
+
try:
|
|
20141
|
+
if action == "measure_loudness":
|
|
20142
|
+
paths = p.get("paths") or ([p["path"]] if p.get("path") else [])
|
|
20143
|
+
if not paths:
|
|
20144
|
+
return _err("measure_loudness requires path or paths")
|
|
20145
|
+
return _ok(measurements=[_mix_plan_mod.measure(str(item)) for item in paths])
|
|
20146
|
+
|
|
20147
|
+
dialogue = p.get("dialogue") or ([p["path"]] if p.get("path") else [])
|
|
20148
|
+
if isinstance(dialogue, str):
|
|
20149
|
+
dialogue = [dialogue]
|
|
20150
|
+
kwargs = dict(
|
|
20151
|
+
music=p.get("music") or [],
|
|
20152
|
+
sfx=p.get("sfx") or [],
|
|
20153
|
+
standard=str(p.get("standard") or _mix_plan_mod.DEFAULT_STANDARD),
|
|
20154
|
+
target_lufs=(
|
|
20155
|
+
float(p["target_lufs"]) if p.get("target_lufs") is not None else None
|
|
20156
|
+
),
|
|
20157
|
+
bed_offset_lu=float(p.get("bed_offset_lu", _mix_plan_mod.DEFAULT_BED_OFFSET_LU)),
|
|
20158
|
+
duck_db=float(p.get("duck_db", _mix_plan_mod.DEFAULT_DUCK_DB)),
|
|
20159
|
+
attack_s=float(p.get("attack_s", _mix_plan_mod.DEFAULT_ATTACK_S)),
|
|
20160
|
+
release_s=float(p.get("release_s", _mix_plan_mod.DEFAULT_RELEASE_S)),
|
|
20161
|
+
hold_s=float(p.get("hold_s", _mix_plan_mod.DEFAULT_HOLD_S)),
|
|
20162
|
+
)
|
|
20163
|
+
if p.get("dry_run", True):
|
|
20164
|
+
return _ok(**_mix_plan_mod.plan(dialogue, **kwargs))
|
|
20165
|
+
return _ok(**_mix_plan_mod.render(
|
|
20166
|
+
dialogue,
|
|
20167
|
+
output_path=(p.get("output_path") or p.get("outputPath")) or None,
|
|
20168
|
+
program_normalize=p.get("program_normalize"),
|
|
20169
|
+
**kwargs,
|
|
20170
|
+
))
|
|
20171
|
+
except _mix_plan_mod.MixPlanError as exc:
|
|
20172
|
+
return _err(str(exc), code="MIX_PLAN_REFUSED", category="invalid_input",
|
|
20173
|
+
remediation=(
|
|
20174
|
+
"Supply dialogue=[...] stems that carry audio. The mix is "
|
|
20175
|
+
"anchored to dialogue, so it cannot be planned without one."
|
|
20176
|
+
))
|
|
20079
20177
|
if action == "image_qc_capabilities":
|
|
20080
20178
|
from src.utils import image_qc as _image_qc_mod
|
|
20081
20179
|
|
|
@@ -21028,6 +21126,11 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
21028
21126
|
"capabilities",
|
|
21029
21127
|
"recheck_capabilities",
|
|
21030
21128
|
"assess_grade",
|
|
21129
|
+
"grade_loop",
|
|
21130
|
+
"grade_loop_capabilities",
|
|
21131
|
+
"mix_plan",
|
|
21132
|
+
"mix_plan_capabilities",
|
|
21133
|
+
"measure_loudness",
|
|
21031
21134
|
"image_qc_capabilities",
|
|
21032
21135
|
"install_guidance",
|
|
21033
21136
|
"resolve_output_root",
|