davinci-resolve-mcp 2.101.0 → 2.103.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 +124 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +14 -0
- package/docs/kernels/audio-fairlight-kernel.md +33 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +39 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +116 -12
- package/src/utils/mix_plan.py +612 -0
- package/src/utils/offline_fallback.py +307 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,130 @@
|
|
|
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.103.0
|
|
6
|
+
|
|
7
|
+
**An unreachable Resolve no longer ends the work.** The interchange authoring that can
|
|
8
|
+
write an importable timeline has been in this repository the whole time, one process
|
|
9
|
+
away, while a connection failure stopped everything. This routes to it.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`timeline author_offline`** — write an importable timeline from a file-path clip plan
|
|
14
|
+
with no Resolve connection. Served **above** the connection check, because it exists
|
|
15
|
+
for the case where there is none. Targets in preference order:
|
|
16
|
+
- **`drt`** (default) — Resolve-native, carries track structure. Stamped at project
|
|
17
|
+
version 17 (Resolve 21.0); older builds need the advanced server's
|
|
18
|
+
`drt(action='downgrade')`. Verified map: 18.0.4 -> 11, 19.1.x -> 14, 21.0 -> 17.
|
|
19
|
+
- **`otio`** — round-trips through this repo's own parser and carries gaps, per-clip
|
|
20
|
+
speed, and transitions. The target to pick when the plan has retimes.
|
|
21
|
+
- **`edl`** — CMX3600: video cuts and M2 speed, nothing else.
|
|
22
|
+
- **`timeline offline_fallback_capabilities`** — whether authoring is available here, and
|
|
23
|
+
why not if it is not.
|
|
24
|
+
- **`offline_alternative` on every not-connected error** — naming what could be produced.
|
|
25
|
+
|
|
26
|
+
### It is an offer, never a substitute
|
|
27
|
+
|
|
28
|
+
A caller who asked to build a timeline *in Resolve* has not succeeded because a file was
|
|
29
|
+
written somewhere. The connection error stays an error, the block says outright that
|
|
30
|
+
authoring does not complete what failed, and nothing is authored unless it is asked for.
|
|
31
|
+
A test asserts both halves, because an offer that reads as success is worse than no offer.
|
|
32
|
+
|
|
33
|
+
### Two silent failures, now named
|
|
34
|
+
|
|
35
|
+
- `media_tc_origin_assumed` — OTIO source frames are **timecode-absolute**. An event with
|
|
36
|
+
no media timecode origin imports as an *empty* timeline: the file opens, nothing
|
|
37
|
+
appears, and no error is raised. Every event that had to assume an origin is named,
|
|
38
|
+
with the fix (`media_start_tc_frame` per clip).
|
|
39
|
+
- `retimes_flattened` — a `.drt` carries no per-clip speed field, so retimes flatten to
|
|
40
|
+
100% forward. Every event that lost one is named, with OTIO as the target that keeps it.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
`_check()` emitted its own flat `NOT_CONNECTED` error asserting Resolve might not be
|
|
45
|
+
running and pointing every reader at a Studio-only preference — the same three wrong
|
|
46
|
+
claims `_not_connected_error` was written to stop making in v2.63, still being made here
|
|
47
|
+
because two producers of one error had drifted apart. It now delegates, so the message
|
|
48
|
+
distinguishes "not running" from "running but refusing scripting" from "the bridge is
|
|
49
|
+
enabled and silent", and free-edition users stop being sent to check a Studio install.
|
|
50
|
+
|
|
51
|
+
### Design notes
|
|
52
|
+
|
|
53
|
+
- **Frame numbers are at the timeline rate and `end_frame` is EXCLUSIVE**, matching
|
|
54
|
+
`AppendToTimeline`'s half-open range. The two shapes disagreeing would be a one-frame
|
|
55
|
+
error on every clip — exactly the kind that survives review.
|
|
56
|
+
- **Authoring runs in Node** against `resolve-advanced/server/author-interchange.mjs`
|
|
57
|
+
rather than a second Python writer. Two writers to keep in agreement means the one that
|
|
58
|
+
drifts is always the copy nobody runs. Without Node it refuses and says why.
|
|
59
|
+
|
|
60
|
+
### Validation
|
|
61
|
+
|
|
62
|
+
- Offline suite: 2959 passed, 1 skipped, 719 subtests, 0 failures.
|
|
63
|
+
- All three targets authored and read back: OTIO parsed as a Timeline document with
|
|
64
|
+
timecode-absolute source ranges, DRT and EDL written and inspected.
|
|
65
|
+
- Three deliberate mutations (swallowing the media-origin warning, treating `end_frame`
|
|
66
|
+
as inclusive, and marking the connection error as a success) were each caught.
|
|
67
|
+
- No Resolve behavior changed by the authoring path itself; live import validation of an
|
|
68
|
+
authored file is **not** included in this release.
|
|
69
|
+
|
|
70
|
+
## What's New in v2.102.0
|
|
71
|
+
|
|
72
|
+
**A rough mix that reports what it achieved, not what it intended.** The pieces were
|
|
73
|
+
already here — `media_analysis` measures EBU R128 loudness and detects silence,
|
|
74
|
+
`delivery_targets` holds the standards, `loudness_qc` grades a finished file. What was
|
|
75
|
+
missing is the step between measuring and grading: deciding the gains.
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- **`media_analysis mix_plan`** — dialogue-normalisation gain, a music-bed level relative
|
|
80
|
+
to it, and ducking windows derived from silence detection **on the dialogue stem**, so
|
|
81
|
+
the bed follows the words rather than a hand-placed envelope. `dry_run` defaults to
|
|
82
|
+
true and renders nothing.
|
|
83
|
+
- **The achieved loudness is measured, not derived.** The premix is rendered, then
|
|
84
|
+
re-measured; `achieved` carries integrated LUFS, true peak, loudness range, and the
|
|
85
|
+
delta from target. A plan that hits its target on paper and clips on true peak is a
|
|
86
|
+
failed plan, and only the measurement tells you which one you have.
|
|
87
|
+
- **Dialogue-anchored, then programme-trimmed.** Anchoring dialogue at target is right
|
|
88
|
+
for a dialogue-gated standard and wrong for a full-programme one the moment a bed is
|
|
89
|
+
added. For non-dialogue-gated standards one measured trim is applied to everything
|
|
90
|
+
equally — preserving the dialogue-to-bed relationship — and reported as
|
|
91
|
+
`program_normalize.trim_db`. It never runs on a dialogue-gated standard, where
|
|
92
|
+
dialogue is the figure being graded.
|
|
93
|
+
- **Nothing else is corrected.** `loudness_off_target`, `true_peak_over`, and `clipped`
|
|
94
|
+
come back as flags with remedies, never as a quietly normalised file.
|
|
95
|
+
- Standards come from `delivery_targets` (`web`, `podcast`, `ebu_r128`, `atsc_a85`,
|
|
96
|
+
`ott_dialogue_gated`) — the table the delivery tools already grade against, not a
|
|
97
|
+
second copy.
|
|
98
|
+
- **`media_analysis measure_loudness`** — integrated LUFS, loudness range, and true peak
|
|
99
|
+
per file.
|
|
100
|
+
- **`media_analysis mix_plan_capabilities`** — dependency state, known standards, and the
|
|
101
|
+
defaults, including the music-bed offset, which is the number most likely to be argued
|
|
102
|
+
with and so is named rather than buried.
|
|
103
|
+
|
|
104
|
+
### Fixed while building it
|
|
105
|
+
|
|
106
|
+
The new loudness parser reads the `Summary:` block **and** drops ebur128's per-frame
|
|
107
|
+
progress lines, which carry their own `I:` and `LRA:` fields. A plain last-match-wins
|
|
108
|
+
parse is correct only because ffmpeg happens to print the summary last, and scoping to
|
|
109
|
+
the summary alone still swallows a progress line printed after it. Both steps are needed;
|
|
110
|
+
a test with a trailing progress line pins it.
|
|
111
|
+
|
|
112
|
+
### Scope
|
|
113
|
+
|
|
114
|
+
A rough mix: gain staging, a bed, and ducking. No EQ, compression, de-essing, or
|
|
115
|
+
limiting, and the module says so in its capabilities rather than leaving it implied.
|
|
116
|
+
|
|
117
|
+
### Validation
|
|
118
|
+
|
|
119
|
+
- Offline suite: 2924 passed, 1 skipped, 711 subtests, 0 failures.
|
|
120
|
+
- End-to-end through real ffmpeg on generated tones: target hit from measurement, the
|
|
121
|
+
programme trim landing a hot bed on R128, a dialogue-gated standard refusing the trim,
|
|
122
|
+
and clipping reported rather than normalised away.
|
|
123
|
+
- Three deliberate mutations (silent peak normalisation, trimming a dialogue-gated
|
|
124
|
+
standard, and dropping the parser scoping) were each caught. The parser mutation was
|
|
125
|
+
caught only after the test was strengthened — the first version of it passed against
|
|
126
|
+
both the fix and its absence.
|
|
127
|
+
- No Resolve behavior changed; live test not required.
|
|
128
|
+
|
|
5
129
|
## What's New in v2.101.0
|
|
6
130
|
|
|
7
131
|
**A grade can now reject itself.** `assess_grade` has measured grade damage since
|
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.103.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,20 @@ Key actions:
|
|
|
552
552
|
`load_user_preferences_preset`
|
|
553
553
|
- `quit` — terminates Resolve (destructive; confirm with user first)
|
|
554
554
|
|
|
555
|
+
**Offline timeline authoring on `timeline`** — served above the connection check:
|
|
556
|
+
`author_offline` writes an importable `.drt` / `.otio` / `.edl` from a clip plan when
|
|
557
|
+
Resolve is unreachable, and `offline_fallback_capabilities` reports whether it can. Every
|
|
558
|
+
not-connected error carries an `offline_alternative` block naming it. Authoring a file
|
|
559
|
+
does not complete a failed live operation — the timeline is not in a project until it is
|
|
560
|
+
imported. See `docs/kernels/timeline-conform-interchange-kernel.md`.
|
|
561
|
+
|
|
562
|
+
**Offline audio and image QC on `media_analysis`** — no Resolve connection required:
|
|
563
|
+
`measure_loudness`, `mix_plan` / `mix_plan_capabilities` (dialogue-anchored rough mix
|
|
564
|
+
with dialogue-following ducking, rendered and re-measured), and `assess_grade` /
|
|
565
|
+
`grade_loop` / `grade_loop_capabilities` (numeric grade-damage QC and the retry ladder
|
|
566
|
+
that backs a look off until it stops damaging the picture). See
|
|
567
|
+
`docs/kernels/audio-fairlight-kernel.md` and `docs/kernels/color-grade-kernel.md`.
|
|
568
|
+
|
|
555
569
|
**`layout_presets`** — Save, load, export, import, delete UI layout presets.
|
|
556
570
|
`list` (Resolve 21.0.4+) enumerates the saved preset names the other actions
|
|
557
571
|
take.
|
|
@@ -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,45 @@ Supported aliases include `aaf`, `drt`, `edl`, `edl_cdl`, `edl_sdl`,
|
|
|
67
67
|
- Synthetic-only unlink, missing-media detection, relink candidate planning,
|
|
68
68
|
and safe relink all worked through generated media.
|
|
69
69
|
|
|
70
|
+
## When Resolve is unreachable
|
|
71
|
+
|
|
72
|
+
An unreachable Resolve used to end the work. It no longer has to: `timeline` serves two
|
|
73
|
+
actions **above** the connection check, and every not-connected error carries an
|
|
74
|
+
`offline_alternative` block naming them.
|
|
75
|
+
|
|
76
|
+
- `author_offline(clips[], output_path, target?, name?, fps?, start_timecode?,
|
|
77
|
+
resolution?)` — write an importable timeline from a file-path clip plan.
|
|
78
|
+
Targets, in preference order:
|
|
79
|
+
- **`drt`** (default) — Resolve-native, carries track structure. Stamped at project
|
|
80
|
+
version 17 (Resolve 21.0); older builds need the advanced server's
|
|
81
|
+
`drt(action='downgrade')`. Verified map: 18.0.4 → 11, 19.1.x → 14, 21.0 → 17.
|
|
82
|
+
- **`otio`** — round-trips through this repo's own parser and carries gaps, per-clip
|
|
83
|
+
speed, and transitions. The target to pick when the plan has retimes.
|
|
84
|
+
- **`edl`** — CMX3600: video cuts and M2 speed, nothing else.
|
|
85
|
+
- `offline_fallback_capabilities()` — whether authoring is available here, and why not
|
|
86
|
+
if it is not.
|
|
87
|
+
|
|
88
|
+
Frame numbers are at the timeline rate and `end_frame` is **EXCLUSIVE**, matching
|
|
89
|
+
`AppendToTimeline`'s half-open range.
|
|
90
|
+
|
|
91
|
+
**It is an offer, not a substitute.** A caller who asked to build a timeline *in Resolve*
|
|
92
|
+
has not succeeded because a file was written. The connection error stays an error, the
|
|
93
|
+
block says outright that authoring does not complete what failed, and nothing is authored
|
|
94
|
+
unless someone asks for it.
|
|
95
|
+
|
|
96
|
+
Two warnings the result can carry, both for failures that are otherwise silent:
|
|
97
|
+
|
|
98
|
+
- `media_tc_origin_assumed` — OTIO source frames are **timecode-absolute**. An event with
|
|
99
|
+
no media timecode origin imports as an *empty* timeline: the file opens, nothing
|
|
100
|
+
appears, no error is raised. Pass `media_start_tc_frame` (or an absolute
|
|
101
|
+
`src_tc_frame`) per clip. Every event that had to assume is named.
|
|
102
|
+
- `retimes_flattened` — a `.drt` has no per-clip speed field, so retimes flatten to 100%
|
|
103
|
+
forward. Every event that lost one is named; author OTIO to keep them.
|
|
104
|
+
|
|
105
|
+
Authoring runs in Node against `resolve-advanced/server/author-interchange.mjs` rather
|
|
106
|
+
than a second Python writer — two writers to keep in agreement means the one that drifts
|
|
107
|
+
is always the copy nobody runs. Without Node it refuses and says so.
|
|
108
|
+
|
|
70
109
|
## Boundaries
|
|
71
110
|
|
|
72
111
|
- Interchange formats are not semantically equivalent. DRT is the strongest
|
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.103.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.103.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.103.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -970,7 +970,7 @@ def _not_connected_error():
|
|
|
970
970
|
running = resolve_is_running()
|
|
971
971
|
bridge_on = _bridge_requested()
|
|
972
972
|
if bridge_on:
|
|
973
|
-
return _err(
|
|
973
|
+
return _with_offline_alternative(_err(
|
|
974
974
|
"The in-app bridge is enabled but not answering.",
|
|
975
975
|
code="BRIDGE_UNAVAILABLE", category="not_connected",
|
|
976
976
|
# `not_connected` defaults to retryable because auto-launch may
|
|
@@ -986,9 +986,9 @@ def _not_connected_error():
|
|
|
986
986
|
"run `launchctl setenv PYTHON3HOME \"$(python3 -c 'import sys; "
|
|
987
987
|
"print(sys.prefix)')\"` (launchctl, not export) and restart Resolve.",
|
|
988
988
|
state={"resolve_running": running, "bridge_enabled": True},
|
|
989
|
-
)
|
|
989
|
+
))
|
|
990
990
|
if running:
|
|
991
|
-
return _err(
|
|
991
|
+
return _with_offline_alternative(_err(
|
|
992
992
|
"DaVinci Resolve is running but is not answering the scripting API.",
|
|
993
993
|
code="SCRIPTING_UNAVAILABLE", category="not_connected",
|
|
994
994
|
# Not retryable: a preference has to change, or the bridge has to be
|
|
@@ -1006,13 +1006,33 @@ def _not_connected_error():
|
|
|
1006
1006
|
# responses, and the in-app bridge is not an option for the former.
|
|
1007
1007
|
state={"resolve_running": True, "bridge_enabled": False,
|
|
1008
1008
|
"headless": _resolve_runtime.is_headless()},
|
|
1009
|
-
)
|
|
1010
|
-
return _err(
|
|
1009
|
+
))
|
|
1010
|
+
return _with_offline_alternative(_err(
|
|
1011
1011
|
"DaVinci Resolve is not running and could not be started.",
|
|
1012
1012
|
code="RESOLVE_NOT_RUNNING", category="not_connected",
|
|
1013
1013
|
remediation="Start DaVinci Resolve and open a project, then retry.",
|
|
1014
1014
|
state={"resolve_running": bool(running), "bridge_enabled": False},
|
|
1015
|
-
)
|
|
1015
|
+
))
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def _with_offline_alternative(error: Dict[str, Any]) -> Dict[str, Any]:
|
|
1019
|
+
"""Attach the offline-authoring offer to a not-connected error.
|
|
1020
|
+
|
|
1021
|
+
An offer, not a substitute: the error stays an error, and the block says outright
|
|
1022
|
+
that authoring a file does not complete the operation that just failed. Rerouting
|
|
1023
|
+
silently would turn "your project now has this timeline" into a claim that is false
|
|
1024
|
+
in the only sense that matters.
|
|
1025
|
+
"""
|
|
1026
|
+
try:
|
|
1027
|
+
from src.utils import offline_fallback as _offline_mod
|
|
1028
|
+
|
|
1029
|
+
alternative = _offline_mod.offline_alternative()
|
|
1030
|
+
if alternative.get("available"):
|
|
1031
|
+
error.setdefault("error", {})["offline_alternative"] = alternative
|
|
1032
|
+
except Exception:
|
|
1033
|
+
# A connection error must survive anything wrong with the fallback path.
|
|
1034
|
+
pass
|
|
1035
|
+
return error
|
|
1016
1036
|
|
|
1017
1037
|
|
|
1018
1038
|
def _destructive_versioning_provider() -> Optional[Tuple[Any, Any, str, Optional[str]]]:
|
|
@@ -2489,11 +2509,12 @@ def _check():
|
|
|
2489
2509
|
)
|
|
2490
2510
|
resolve = get_resolve()
|
|
2491
2511
|
if resolve is None:
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2512
|
+
# Delegate rather than assert. This branch used to claim Resolve might not be
|
|
2513
|
+
# running and point every reader at a Studio-only preference — the same three
|
|
2514
|
+
# wrong claims `_not_connected_error` was written to stop making, still being
|
|
2515
|
+
# made here because two producers of the same error drifted apart. It also
|
|
2516
|
+
# carries the offline-authoring offer, which this branch never had.
|
|
2517
|
+
return None, None, _not_connected_error()
|
|
2497
2518
|
pm = resolve.GetProjectManager()
|
|
2498
2519
|
if pm is None:
|
|
2499
2520
|
return None, None, _err(
|
|
@@ -20128,6 +20149,52 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
20128
20149
|
from src.utils import grade_loop as _grade_loop_mod
|
|
20129
20150
|
|
|
20130
20151
|
return _ok(**_grade_loop_mod.capabilities())
|
|
20152
|
+
if action in {"mix_plan", "measure_loudness", "mix_plan_capabilities"}:
|
|
20153
|
+
# Gain staging between measuring loudness and grading it: dialogue-norm gain,
|
|
20154
|
+
# a bed level relative to it, and ducking windows the dialogue itself implies.
|
|
20155
|
+
# The render is measured afterwards, so what comes back is the loudness
|
|
20156
|
+
# achieved rather than the arithmetic meant to produce it.
|
|
20157
|
+
from src.utils import mix_plan as _mix_plan_mod
|
|
20158
|
+
|
|
20159
|
+
if action == "mix_plan_capabilities":
|
|
20160
|
+
return _ok(**_mix_plan_mod.capabilities())
|
|
20161
|
+
try:
|
|
20162
|
+
if action == "measure_loudness":
|
|
20163
|
+
paths = p.get("paths") or ([p["path"]] if p.get("path") else [])
|
|
20164
|
+
if not paths:
|
|
20165
|
+
return _err("measure_loudness requires path or paths")
|
|
20166
|
+
return _ok(measurements=[_mix_plan_mod.measure(str(item)) for item in paths])
|
|
20167
|
+
|
|
20168
|
+
dialogue = p.get("dialogue") or ([p["path"]] if p.get("path") else [])
|
|
20169
|
+
if isinstance(dialogue, str):
|
|
20170
|
+
dialogue = [dialogue]
|
|
20171
|
+
kwargs = dict(
|
|
20172
|
+
music=p.get("music") or [],
|
|
20173
|
+
sfx=p.get("sfx") or [],
|
|
20174
|
+
standard=str(p.get("standard") or _mix_plan_mod.DEFAULT_STANDARD),
|
|
20175
|
+
target_lufs=(
|
|
20176
|
+
float(p["target_lufs"]) if p.get("target_lufs") is not None else None
|
|
20177
|
+
),
|
|
20178
|
+
bed_offset_lu=float(p.get("bed_offset_lu", _mix_plan_mod.DEFAULT_BED_OFFSET_LU)),
|
|
20179
|
+
duck_db=float(p.get("duck_db", _mix_plan_mod.DEFAULT_DUCK_DB)),
|
|
20180
|
+
attack_s=float(p.get("attack_s", _mix_plan_mod.DEFAULT_ATTACK_S)),
|
|
20181
|
+
release_s=float(p.get("release_s", _mix_plan_mod.DEFAULT_RELEASE_S)),
|
|
20182
|
+
hold_s=float(p.get("hold_s", _mix_plan_mod.DEFAULT_HOLD_S)),
|
|
20183
|
+
)
|
|
20184
|
+
if p.get("dry_run", True):
|
|
20185
|
+
return _ok(**_mix_plan_mod.plan(dialogue, **kwargs))
|
|
20186
|
+
return _ok(**_mix_plan_mod.render(
|
|
20187
|
+
dialogue,
|
|
20188
|
+
output_path=(p.get("output_path") or p.get("outputPath")) or None,
|
|
20189
|
+
program_normalize=p.get("program_normalize"),
|
|
20190
|
+
**kwargs,
|
|
20191
|
+
))
|
|
20192
|
+
except _mix_plan_mod.MixPlanError as exc:
|
|
20193
|
+
return _err(str(exc), code="MIX_PLAN_REFUSED", category="invalid_input",
|
|
20194
|
+
remediation=(
|
|
20195
|
+
"Supply dialogue=[...] stems that carry audio. The mix is "
|
|
20196
|
+
"anchored to dialogue, so it cannot be planned without one."
|
|
20197
|
+
))
|
|
20131
20198
|
if action == "image_qc_capabilities":
|
|
20132
20199
|
from src.utils import image_qc as _image_qc_mod
|
|
20133
20200
|
|
|
@@ -21082,6 +21149,9 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
21082
21149
|
"assess_grade",
|
|
21083
21150
|
"grade_loop",
|
|
21084
21151
|
"grade_loop_capabilities",
|
|
21152
|
+
"mix_plan",
|
|
21153
|
+
"mix_plan_capabilities",
|
|
21154
|
+
"measure_loudness",
|
|
21085
21155
|
"image_qc_capabilities",
|
|
21086
21156
|
"install_guidance",
|
|
21087
21157
|
"resolve_output_root",
|
|
@@ -22826,6 +22896,8 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
22826
22896
|
|
|
22827
22897
|
|
|
22828
22898
|
_TIMELINE_ACTIONS = [
|
|
22899
|
+
# Offline authoring — served without a Resolve connection, above the _check() gate.
|
|
22900
|
+
"author_offline", "offline_fallback_capabilities",
|
|
22829
22901
|
"list", "get_current", "set_current", "get_name", "set_name", "get_start_frame",
|
|
22830
22902
|
"get_end_frame", "get_start_timecode", "set_start_timecode", "get_track_count",
|
|
22831
22903
|
"add_track", "delete_track", "get_track_sub_type", "set_track_enable",
|
|
@@ -23082,6 +23154,38 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
23082
23154
|
# action_help is pull-on-demand metadata; no Resolve connection needed.
|
|
23083
23155
|
if action == "action_help":
|
|
23084
23156
|
return _action_help("timeline", p)
|
|
23157
|
+
if action in {"author_offline", "offline_fallback_capabilities"}:
|
|
23158
|
+
# Deliberately above the connection check: these exist FOR the case where there
|
|
23159
|
+
# is no connection. Authoring writes a file the user imports; it does not make
|
|
23160
|
+
# a failed live operation succeed, and the response says so.
|
|
23161
|
+
from src.utils import offline_fallback as _offline_mod
|
|
23162
|
+
|
|
23163
|
+
if action == "offline_fallback_capabilities":
|
|
23164
|
+
return _ok(**_offline_mod.capabilities())
|
|
23165
|
+
err, _clean = _validate_params(p, {
|
|
23166
|
+
"output_path": {"type": str, "required": True, "non_empty": True},
|
|
23167
|
+
})
|
|
23168
|
+
if err:
|
|
23169
|
+
return _err(err)
|
|
23170
|
+
try:
|
|
23171
|
+
return _ok(**_offline_mod.author(
|
|
23172
|
+
p.get("clips") or [],
|
|
23173
|
+
str(p["output_path"]),
|
|
23174
|
+
target=str(p.get("target") or _offline_mod.DEFAULT_TARGET),
|
|
23175
|
+
name=str(p.get("name") or "Offline Conform"),
|
|
23176
|
+
fps=float(p.get("fps") or _offline_mod.DEFAULT_FPS),
|
|
23177
|
+
start_timecode=str(p.get("start_timecode") or "01:00:00:00"),
|
|
23178
|
+
resolution=str(p.get("resolution") or "1920x1080"),
|
|
23179
|
+
))
|
|
23180
|
+
except _offline_mod.OfflineFallbackError as exc:
|
|
23181
|
+
return _err(str(exc), code="OFFLINE_AUTHORING_REFUSED",
|
|
23182
|
+
category="invalid_input", retryable=False,
|
|
23183
|
+
remediation=(
|
|
23184
|
+
"Supply clips=[{path, start_frame, end_frame}] with frame "
|
|
23185
|
+
"numbers at the timeline rate; end_frame is EXCLUSIVE. Add "
|
|
23186
|
+
"media_start_tc_frame per clip so source frames are "
|
|
23187
|
+
"timecode-absolute, or the import can produce an empty timeline."
|
|
23188
|
+
))
|
|
23085
23189
|
pm, proj, err = _check()
|
|
23086
23190
|
if err:
|
|
23087
23191
|
return err
|