davinci-resolve-mcp 4.15.5 → 4.17.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 +117 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +31 -2
- package/docs/kernels/timeline-conform-interchange-kernel.md +6 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/tools/drx.mjs +23 -1
- package/resolve-advanced/vendor/drx-codec/drx-generator.js +113 -9
- package/src/granular/common.py +3 -2
- package/src/server.py +442 -56
- package/src/utils/mcp_error_flag.py +81 -0
- package/src/utils/media_paths.py +76 -0
- package/src/utils/operation_result.py +5 -0
- package/src/utils/resolve_bridge.py +117 -29
- package/src/utils/resolve_bridge_client.py +102 -34
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,123 @@
|
|
|
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 v4.17.0 — Power windows for Resolve 21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`drx generate` takes `resolveVersion`** (#328, thanks @hpaul). A major
|
|
10
|
+
number (`21`) or a version string (`"21.0.4"`). From 21 on, circle and
|
|
11
|
+
linear power windows are written the way Resolve 21 exports them: corrector
|
|
12
|
+
type 0x10004 (65540) with the full parameter set, `REF_WIDTH` 288 and an
|
|
13
|
+
identity transform matrix, and 0x10004 added to the node's corrector-type
|
|
14
|
+
list. Resolve 21 loads the older corrector-type-4 form into the Window
|
|
15
|
+
palette but renders an empty mask, which also hides the node's grade.
|
|
16
|
+
The default is unchanged, so output for Resolve 19/20 is byte-for-byte
|
|
17
|
+
what it was. Gradient and polygon/curve windows are not rewritten. The
|
|
18
|
+
format follows justhung69/resolve-mcp (MIT), pixel-verified there on free
|
|
19
|
+
21.0.4.5.
|
|
20
|
+
- The node's corrector-type list is now varint encoded, because 65540 does
|
|
21
|
+
not fit in one byte. Every type id below 128 encodes to the same byte as
|
|
22
|
+
before.
|
|
23
|
+
|
|
24
|
+
### Validation
|
|
25
|
+
|
|
26
|
+
- Advanced Node suite on Node 20 with a fresh `npm ci`, PR merged on
|
|
27
|
+
`main`: 1,214 tests, 1,153 passed, 0 failed, 61 skipped (8 new for the
|
|
28
|
+
Resolve 21 window layout). PR CI green on Python 3.10/3.11 and Node 20/22.
|
|
29
|
+
- Not live-validated on Resolve 21: the maintainer's box runs Studio
|
|
30
|
+
19.1.3.7. The window layout is checked against Resolve 21 fixtures only.
|
|
31
|
+
|
|
32
|
+
## What's New in v4.16.0 — Calls name their timeline, and failures say so
|
|
33
|
+
|
|
34
|
+
Eight reports from a Studio 21.1 / Windows deployment (#320–#327) and the
|
|
35
|
+
open half of #318. The theme: a call that did something other than what the
|
|
36
|
+
caller asked, or failed, must not look like a success.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Pin a call to a timeline** (#320). `timeline`, `timeline_item*`,
|
|
41
|
+
`timeline_markers`, `timeline_frame`, `timeline_ai`, `graph` and `render`
|
|
42
|
+
accept `timeline_id` or `timeline_name`. The server makes that timeline
|
|
43
|
+
current for the call and restores the caller's timeline afterwards,
|
|
44
|
+
verified by readback. Unknown ids (`TIMELINE_NOT_FOUND`), ambiguous names
|
|
45
|
+
(`TIMELINE_NAME_AMBIGUOUS`) and an id/name pair that disagree are refused
|
|
46
|
+
before the action runs. Before, a `timeline_id` nothing read was dropped,
|
|
47
|
+
and the write landed on whichever timeline was current.
|
|
48
|
+
- **Results name the timeline acted on** (#320): `_operation.timeline`
|
|
49
|
+
`{name, id}`, plus `pinned`, `restored_current` or `restore_failed` on a
|
|
50
|
+
pinned call.
|
|
51
|
+
- **Unread params are reported** (#320). A successful call carrying keys the
|
|
52
|
+
action never read returns `ignored_params` and an envelope warning. They
|
|
53
|
+
are reported after the call, not refused before it: the server has no
|
|
54
|
+
per-action schema, and a static scan of the reachable code could not bound
|
|
55
|
+
the key set for 25 of the 37 tools.
|
|
56
|
+
- **`timeline_item_id` on the timeline item tools** (#323). On 21.1
|
|
57
|
+
`item_index` counts transitions, so it shifts whenever one is added or
|
|
58
|
+
removed. Every tool that resolves an item through the shared lookup
|
|
59
|
+
accepts the item's `GetUniqueId()` and refuses an unknown id with
|
|
60
|
+
`ITEM_NOT_FOUND`.
|
|
61
|
+
- **`drx generate` can target the timeline** (#318).
|
|
62
|
+
`target: "timeline"` writes the grade into `<pTrackVer>`, the block a
|
|
63
|
+
timeline `ApplyGradeFromDRX` loads, in the shape the reporter applied on
|
|
64
|
+
Studio 21.1.1.10. The default stays `"clip"`.
|
|
65
|
+
- **`duplicate` takes `make_current`** and returns the copy's `id` and the
|
|
66
|
+
`current` timeline (#322).
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- **Failed calls set MCP `isError: true`** (#321). Every error-envelope
|
|
71
|
+
result used to arrive with `isError: false`, so spec-following hooks and
|
|
72
|
+
retry policies counted refusals as successes. The flag is set at the
|
|
73
|
+
protocol boundary; the JSON body is unchanged. `{"success": false}` without
|
|
74
|
+
an `error` key is not flagged.
|
|
75
|
+
- **`timeline.duplicate` leaves the original current** (#322) unless
|
|
76
|
+
`make_current: true`. Resolve makes the copy current, so "duplicate as a
|
|
77
|
+
backup, then edit" edited the backup.
|
|
78
|
+
- **FCPXML round trip on the server's own export** (#324).
|
|
79
|
+
`import_timeline_checked` accepts the bundle directory 21.1 writes for
|
|
80
|
+
FCPXML and imports the `Info.fcpxml` inside.
|
|
81
|
+
- **Online image sequences are not reported missing** (#325). A path like
|
|
82
|
+
`name.[0000-8999].jpeg` is checked by its first and last frames (printf and
|
|
83
|
+
`####` patterns by one matching frame). Before, `probe_timeline_structure`
|
|
84
|
+
said `file_exists: false`, and `detect_missing_media` and
|
|
85
|
+
`build_relink_plan` listed the clips for relink.
|
|
86
|
+
- **The bridge honours `allowed_media_roots` / `allowed_output_roots`**
|
|
87
|
+
(#326). `load_config` dropped both, so the policies were always the home
|
|
88
|
+
folder. The generic `call` path the MCP server uses was never gated by
|
|
89
|
+
them. The bridge's named path operations now follow the configured roots,
|
|
90
|
+
including the installer's default output root (`~/Movies`).
|
|
91
|
+
|
|
92
|
+
### Performance
|
|
93
|
+
|
|
94
|
+
- **The bridge keeps one connection open** (#327). Each request is still
|
|
95
|
+
signed with its own nonce; only the TCP connection is reused, retired after
|
|
96
|
+
20 s idle on the client and 30 s on the bridge. macOS loopback: 0.40 ms a
|
|
97
|
+
call per connection, 0.17 ms kept alive, and no TIME_WAIT socket per call.
|
|
98
|
+
Windows was not measured here. Re-run `scripts/install_resolve_bridge.py`
|
|
99
|
+
to update the in-Resolve copy; an older bridge keeps working, one
|
|
100
|
+
connection per call.
|
|
101
|
+
|
|
102
|
+
### Validation
|
|
103
|
+
|
|
104
|
+
- Full offline suite: 4,289 tests, OK. 47 new tests across the error flag,
|
|
105
|
+
pinning, unread params, duplicate, item ids, FCPXML bundles, image
|
|
106
|
+
sequences, bridge roots and bridge keep-alive (over a real loopback
|
|
107
|
+
bridge).
|
|
108
|
+
- Advanced Node suite on Node 20 with a fresh `npm ci`: 1,206 tests, 0
|
|
109
|
+
failures (4 new for `target: "timeline"`).
|
|
110
|
+
- Live on Studio 19.1.3.7, over MCP stdio against a disposable project with
|
|
111
|
+
synthetic media: pinned reads and writes land on the named timeline and
|
|
112
|
+
the caller's timeline comes back; unknown and conflicting selectors
|
|
113
|
+
refuse; `ignored_params`; `timeline_item_id` and its `ITEM_NOT_FOUND`;
|
|
114
|
+
`duplicate` with and without `make_current` and pinned; `isError` on
|
|
115
|
+
failures and not on successes. 21 checks, 2 cycles. The first cycle found
|
|
116
|
+
a pinned `duplicate` reporting the pinned timeline as `current` after the
|
|
117
|
+
caller's had been restored; fixed before release.
|
|
118
|
+
- The 21.1-specific behaviour (transitions in `GetItemListInTrack`, FCPXML
|
|
119
|
+
bundles, timeline DRX applies) cannot be reproduced on 19.1.3.7 and is
|
|
120
|
+
not live-validated.
|
|
121
|
+
|
|
5
122
|
## What's New in v4.15.5 — DRX applies say which graph changed
|
|
6
123
|
|
|
7
124
|
`graph.apply_grade_from_drx` and `timeline_item_color.safe_apply_drx` no
|
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
|
-
> 本翻译对应 v4.
|
|
15
|
+
> 本翻译对应 v4.17.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
|
@@ -215,6 +215,30 @@ tool — did it happen, was it verified, what changed:
|
|
|
215
215
|
not read a missing `changes` as a no-op.
|
|
216
216
|
- **`warnings`** — present only when there are any.
|
|
217
217
|
- **`execution_id`** — correlates one call across logs and transcripts.
|
|
218
|
+
- **`timeline`** — on timeline-scoped tools, `{name, id}` of the timeline the
|
|
219
|
+
call acted on. A pinned call (below) adds `pinned: true` and
|
|
220
|
+
`restored_current`, or `restore_failed: true` if the caller's timeline could
|
|
221
|
+
not be made current again.
|
|
222
|
+
|
|
223
|
+
A failed call (an `{"error": {...}}` body) also comes back with MCP
|
|
224
|
+
`isError: true`, so hooks and retry policies that follow the spec see the
|
|
225
|
+
failure without parsing the body.
|
|
226
|
+
|
|
227
|
+
**Unread params are reported.** When an action succeeds without reading some
|
|
228
|
+
key you passed, the result carries `ignored_params: [...]` and the envelope
|
|
229
|
+
lists them under `warnings`. Treat it as "that key did nothing": a misspelled
|
|
230
|
+
key, an option this action does not take, or an alternative another key
|
|
231
|
+
overrode (an `index` passed alongside `id`).
|
|
232
|
+
|
|
233
|
+
**Pinning a call to a timeline.** `timeline`, `timeline_item*`,
|
|
234
|
+
`timeline_markers`, `timeline_frame`, `timeline_ai`, `graph` and `render`
|
|
235
|
+
act on the current timeline unless you pass `timeline_id` (preferred) or
|
|
236
|
+
`timeline_name`. The server then makes that timeline current for the call
|
|
237
|
+
and restores yours afterwards, so a user clicking another timeline mid-session
|
|
238
|
+
cannot redirect your write. An unknown id, an ambiguous name or an id/name
|
|
239
|
+
pair that disagree is refused before anything runs. Exceptions, where these
|
|
240
|
+
keys keep their own meaning: `timeline.set_current` (selects by
|
|
241
|
+
`timeline_id`) and the imports (`timeline_name` names the new timeline).
|
|
218
242
|
|
|
219
243
|
The envelope is namespaced under `_operation` rather than merged into the top
|
|
220
244
|
level because `status`, `operation`, `warnings`, `result` and `changes` are all
|
|
@@ -1949,8 +1973,11 @@ Key actions: `create_subtitles(settings?)` (human-readable settings resolved to
|
|
|
1949
1973
|
|
|
1950
1974
|
Timeline items are identified by `track_type`, `track_index`, and `item_index`
|
|
1951
1975
|
(all default to `"video"`, `1`, `0` respectively — the first clip on the first
|
|
1952
|
-
video track)
|
|
1953
|
-
on
|
|
1976
|
+
video track), or by `timeline_item_id`, the item's `GetUniqueId()`. Prefer the
|
|
1977
|
+
id: on Resolve 21.1 `item_index` also counts transitions, so it shifts whenever
|
|
1978
|
+
a transition is added or removed. Read ids with `timeline.get_items` or
|
|
1979
|
+
`timeline.probe_timeline_structure`. An unknown id is refused with
|
|
1980
|
+
`ITEM_NOT_FOUND`; it never falls back to item 0.
|
|
1954
1981
|
|
|
1955
1982
|
**`timeline_item`** — Properties, transform, speed, audio, keyframes.
|
|
1956
1983
|
|
|
@@ -2467,6 +2494,8 @@ reports, deletes the project, and removes generated media. See
|
|
|
2467
2494
|
| `"No project open"` | No project is currently loaded | Call `project_manager(action="load", params={"name": "..."})` |
|
|
2468
2495
|
| `"No current timeline"` | Project has no timeline set as current | Call `timeline(action="set_current", params={"index": 1})` |
|
|
2469
2496
|
| `"No item at index N"` | `item_index` out of range for the track | Call `timeline(action="get_items", ...)` first to find valid indices |
|
|
2497
|
+
| `ITEM_NOT_FOUND` | No item with that `timeline_item_id` on the target timeline | Re-read ids; pass `timeline_id` if the item is on another timeline |
|
|
2498
|
+
| `TIMELINE_NOT_FOUND` / `TIMELINE_NAME_AMBIGUOUS` | The pin names no timeline, or several share the name | `timeline(action="list")` and pin by `timeline_id` |
|
|
2470
2499
|
| `"Clip not found"` | Stale or wrong `clip_id` | Re-fetch IDs via `media_pool(action="get_selected")` or `folder(action="get_clips")` |
|
|
2471
2500
|
| `"Gallery not available"` | Not on Color page | `resolve_control(action="open_page", params={"page": "color"})` |
|
|
2472
2501
|
| `"GrabStill failed"` | Not on Color page or no clip under playhead | Switch to Color page, move playhead over a clip |
|
|
@@ -62,6 +62,12 @@ Supported aliases include `aaf`, `drt`, `edl`, `edl_cdl`, `edl_sdl`,
|
|
|
62
62
|
- Source range reporting grouped generated media paths with requested handles.
|
|
63
63
|
- Guarded exports succeeded for FCPXML, DRT, EDL, AAF, and OTIO.
|
|
64
64
|
- FCPXML directory-style exports are normalized with a `primary_file` path.
|
|
65
|
+
`import_timeline_checked` also accepts the bundle directory itself (the
|
|
66
|
+
export's `path`) and imports the `Info.fcpxml` inside it.
|
|
67
|
+
- Clip fades set with `SetFades` do not survive an FCPXML round trip (reported
|
|
68
|
+
on Studio 21.1: imported clips read FadeIn/FadeOut 0 while Cross Dissolves
|
|
69
|
+
survive). This is likely Resolve's FCPXML writer; it has not been measured
|
|
70
|
+
on other formats.
|
|
65
71
|
- DRT export/import/compare round-trip succeeded and cleaned up the imported
|
|
66
72
|
timeline.
|
|
67
73
|
- Synthetic-only unlink, missing-media detection, relink candidate planning,
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.
|
|
40
|
+
VERSION = "4.17.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
|
@@ -250,6 +250,23 @@ const generateSchema = z.object({
|
|
|
250
250
|
'units (wheels raw, saturation ~1.0 neutral) for lossless/programmatic callers.'
|
|
251
251
|
),
|
|
252
252
|
metadata: z.object({}).passthrough().optional().describe('Optional { label, width, height } metadata'),
|
|
253
|
+
target: z
|
|
254
|
+
.enum(['clip', 'timeline'])
|
|
255
|
+
.optional()
|
|
256
|
+
.describe(
|
|
257
|
+
"'clip' (default) writes the grade as a clip grade. 'timeline' writes it as the timeline grade " +
|
|
258
|
+
"(<pTrackVer>), for graph.apply_grade_from_drx with source='timeline'; a clip-target file applied to " +
|
|
259
|
+
'the timeline is refused (DRX_NO_TIMELINE_GRADE).'
|
|
260
|
+
),
|
|
261
|
+
resolveVersion: z
|
|
262
|
+
.union([z.number().int().min(1), z.string()])
|
|
263
|
+
.optional()
|
|
264
|
+
.describe(
|
|
265
|
+
"Resolve version the file is for, as a major number (21) or a version string ('21.0.4'). From 21 on, " +
|
|
266
|
+
'circle and linear power windows are written in the Resolve 21 form (corrector type 0x10004, full parameter ' +
|
|
267
|
+
'set), which renders on Resolve 21; the older form loads there but renders an empty mask. Default: the ' +
|
|
268
|
+
'Resolve 19 form. Not live-verified here; the format comes from justhung69/resolve-mcp.'
|
|
269
|
+
),
|
|
253
270
|
outputPath: z.string().optional().describe('If set, write the .drx here'),
|
|
254
271
|
});
|
|
255
272
|
|
|
@@ -684,7 +701,12 @@ export const drxTool = {
|
|
|
684
701
|
if (action === 'generate') {
|
|
685
702
|
const p = generateSchema.parse(args);
|
|
686
703
|
const warnings = [];
|
|
687
|
-
const
|
|
704
|
+
const metadata = {
|
|
705
|
+
...(p.metadata || {}),
|
|
706
|
+
...(p.target ? { target: p.target } : {}),
|
|
707
|
+
...(p.resolveVersion !== undefined ? { resolveVersion: p.resolveVersion } : {}),
|
|
708
|
+
};
|
|
709
|
+
const out = await drxGenerator().generateDRX(normalizeGradeParams(p.gradeParams, warnings), metadata);
|
|
688
710
|
const content = typeof out === 'string' ? out : out?.content || out?.drxContent;
|
|
689
711
|
const warn = warnings.length ? { warnings } : {};
|
|
690
712
|
if (p.outputPath && content) {
|
|
@@ -222,6 +222,7 @@ const PARAM_IDS = {
|
|
|
222
222
|
* Corrector Type IDs from shared library
|
|
223
223
|
*/
|
|
224
224
|
const CORRECTOR_TYPES = drxParams.CORRECTOR_TYPES;
|
|
225
|
+
const POWER_WINDOW_IDS = drxParams.POWER_WINDOWS;
|
|
225
226
|
|
|
226
227
|
/**
|
|
227
228
|
* Presence marker IDs from shared library
|
|
@@ -547,7 +548,7 @@ function buildHSLCurveParams(colorParams) {
|
|
|
547
548
|
* @param {Object} options - Additional options (label, enabled, correctorTypes)
|
|
548
549
|
*/
|
|
549
550
|
function createNode(nodeId, xPos, yPos, colorParams = null, options = {}) {
|
|
550
|
-
const { label = '', enabled = true, nodeIndex = nodeId } = options;
|
|
551
|
+
const { label = '', enabled = true, nodeIndex = nodeId, resolveVersion } = options;
|
|
551
552
|
|
|
552
553
|
// Build node header with fields in order: F1, F2, F4, F5, F6, F7?, F8
|
|
553
554
|
// Native Resolve analysis shows: id(F1), index(F2), pos(F4,F5), label?(F6), F8=44
|
|
@@ -749,9 +750,11 @@ function createNode(nodeId, xPos, yPos, colorParams = null, options = {}) {
|
|
|
749
750
|
}
|
|
750
751
|
}
|
|
751
752
|
if (colorParams) {
|
|
752
|
-
const win = buildWindowParams(colorParams.window);
|
|
753
|
+
const win = buildWindowParams(colorParams.window, { resolveVersion });
|
|
753
754
|
if (win.transform.length > 0) {
|
|
754
|
-
correctorBlocks.push(buildCorrectorBlock(
|
|
755
|
+
correctorBlocks.push(buildCorrectorBlock(win.transformType, win.transform));
|
|
756
|
+
// Resolve 21 also lists its window type in the node's corrector-type list.
|
|
757
|
+
if (win.transformType !== CORRECTOR_TYPES.POWER_WINDOW && presenceTypes.length > 0) presenceTypes.push(win.transformType);
|
|
755
758
|
}
|
|
756
759
|
if (win.softMask.length > 0) {
|
|
757
760
|
// Linear-window softness mask (0x0870xxxx) lives under corrector type 3 in live data.
|
|
@@ -881,7 +884,7 @@ function createNode(nodeId, xPos, yPos, colorParams = null, options = {}) {
|
|
|
881
884
|
|
|
882
885
|
// Add compact presence list as F2 (raw byte array of type IDs)
|
|
883
886
|
if (presenceTypes.length > 0) {
|
|
884
|
-
const presenceBytes = Buffer.
|
|
887
|
+
const presenceBytes = Buffer.concat(presenceTypes.map(encodeVarint)); // varints: the R21 window type is > 127
|
|
885
888
|
f9Parts.push(protoBytes(2, presenceBytes));
|
|
886
889
|
debugLog('[DRX] Added F2 presence list:', presenceTypes);
|
|
887
890
|
}
|
|
@@ -1636,6 +1639,7 @@ function generateProtobuf(gradeParams, options = {}) {
|
|
|
1636
1639
|
width = 1920,
|
|
1637
1640
|
height = 1080,
|
|
1638
1641
|
label = '',
|
|
1642
|
+
resolveVersion,
|
|
1639
1643
|
} = options;
|
|
1640
1644
|
|
|
1641
1645
|
const timestamp = generateTimestamp();
|
|
@@ -1667,6 +1671,7 @@ function generateProtobuf(gradeParams, options = {}) {
|
|
|
1667
1671
|
const node = createNode(nodeId, xPos, yPos, nodeConfig.params, {
|
|
1668
1672
|
label: nodeConfig.label || `Node ${nodeId}`,
|
|
1669
1673
|
enabled: nodeConfig.enabled !== false,
|
|
1674
|
+
resolveVersion,
|
|
1670
1675
|
});
|
|
1671
1676
|
containerParts.push(node);
|
|
1672
1677
|
});
|
|
@@ -1686,6 +1691,7 @@ function generateProtobuf(gradeParams, options = {}) {
|
|
|
1686
1691
|
}
|
|
1687
1692
|
const node = createNode(1, 500, 300, gradeParams, {
|
|
1688
1693
|
label: label,
|
|
1694
|
+
resolveVersion,
|
|
1689
1695
|
});
|
|
1690
1696
|
containerParts.push(node);
|
|
1691
1697
|
// firstNodeId and lastNodeId both stay 1
|
|
@@ -1752,6 +1758,7 @@ async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
|
|
|
1752
1758
|
baseNodeId: passedBaseNodeId,
|
|
1753
1759
|
firstNodeId: passedFirstNodeId,
|
|
1754
1760
|
lastNodeId: passedLastNodeId,
|
|
1761
|
+
resolveVersion,
|
|
1755
1762
|
} = metadata;
|
|
1756
1763
|
|
|
1757
1764
|
const timestamp = generateTimestamp();
|
|
@@ -1806,6 +1813,7 @@ async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
|
|
|
1806
1813
|
enabled: nodeConfig.enabled !== false,
|
|
1807
1814
|
nodeIndex: nodeIndex, // Pass separate index for F2 field
|
|
1808
1815
|
clipVersionId,
|
|
1816
|
+
resolveVersion,
|
|
1809
1817
|
}
|
|
1810
1818
|
);
|
|
1811
1819
|
containerParts.push(node);
|
|
@@ -1974,10 +1982,15 @@ async function generateDRX(gradeParams, metadata = {}) {
|
|
|
1974
1982
|
sourceTimeline = 'Timeline 1',
|
|
1975
1983
|
sourceTC = '00:00:00:00',
|
|
1976
1984
|
recordTC = '01:00:00:00',
|
|
1985
|
+
target = 'clip',
|
|
1986
|
+
resolveVersion,
|
|
1977
1987
|
} = metadata;
|
|
1988
|
+
if (target !== 'clip' && target !== 'timeline') {
|
|
1989
|
+
throw new Error(`target must be 'clip' or 'timeline', got ${JSON.stringify(target)}`);
|
|
1990
|
+
}
|
|
1978
1991
|
|
|
1979
1992
|
// Generate the body (pass label for single-node mode)
|
|
1980
|
-
const body = await generateDRXBody(gradeParams, { width, height, label });
|
|
1993
|
+
const body = await generateDRXBody(gradeParams, { width, height, label, resolveVersion });
|
|
1981
1994
|
const bodyHex = body.toString('hex');
|
|
1982
1995
|
|
|
1983
1996
|
// Generate UUIDs
|
|
@@ -1988,6 +2001,31 @@ async function generateDRX(gradeParams, metadata = {}) {
|
|
|
1988
2001
|
// Current timestamp
|
|
1989
2002
|
const now = new Date().toISOString().replace('Z', '');
|
|
1990
2003
|
|
|
2004
|
+
// A timeline ApplyGradeFromDRX loads <pTrackVer> (VerType 1), not the clip
|
|
2005
|
+
// grade in <pClipFullVer>. This path used to write no <pTrackVer> at all, so a
|
|
2006
|
+
// timeline apply loaded nothing and replaced the timeline graph with an empty
|
|
2007
|
+
// one (#318). target:'timeline' carries the grade in <pTrackVer> and marks the
|
|
2008
|
+
// clip part uncorrected: the shape the #318 reporter applied successfully on
|
|
2009
|
+
// Studio 21.1.1.10 (timeline graph 0 -> 1 node, no other graph changed).
|
|
2010
|
+
const forTimeline = target === 'timeline';
|
|
2011
|
+
const trackSection = forTimeline
|
|
2012
|
+
? `
|
|
2013
|
+
<pTrackVer>
|
|
2014
|
+
<ListMgt::LmVersion DbId="${uuidv4()}">
|
|
2015
|
+
<FieldsBlob/>
|
|
2016
|
+
<Name/>
|
|
2017
|
+
<HasCorrection>true</HasCorrection>
|
|
2018
|
+
<VerType>1</VerType>
|
|
2019
|
+
<ImplVersion>1</ImplVersion>
|
|
2020
|
+
<IncludedInRecording>true</IncludedInRecording>
|
|
2021
|
+
<FlatPassEnabled>false</FlatPassEnabled>
|
|
2022
|
+
<RGBAOutputEnabled>false</RGBAOutputEnabled>
|
|
2023
|
+
<Body>${bodyHex}</Body>
|
|
2024
|
+
<UseVersionClipProcParams>true</UseVersionClipProcParams>
|
|
2025
|
+
</ListMgt::LmVersion>
|
|
2026
|
+
</pTrackVer>`
|
|
2027
|
+
: '';
|
|
2028
|
+
|
|
1991
2029
|
// Build DRX XML
|
|
1992
2030
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1993
2031
|
<!--DbAppVer="19.1.3.0007" DbPrjVer="14"-->
|
|
@@ -2010,7 +2048,7 @@ async function generateDRX(gradeParams, metadata = {}) {
|
|
|
2010
2048
|
<ListMgt::LmVersion DbId="${versionId}">
|
|
2011
2049
|
<FieldsBlob/>
|
|
2012
2050
|
<Name/>
|
|
2013
|
-
<HasCorrection
|
|
2051
|
+
<HasCorrection>${forTimeline ? 'false' : 'true'}</HasCorrection>
|
|
2014
2052
|
<VerType>0</VerType>
|
|
2015
2053
|
<ImplVersion>1</ImplVersion>
|
|
2016
2054
|
<IncludedInRecording>true</IncludedInRecording>
|
|
@@ -2019,7 +2057,7 @@ async function generateDRX(gradeParams, metadata = {}) {
|
|
|
2019
2057
|
<Body>${bodyHex}</Body>
|
|
2020
2058
|
<UseVersionClipProcParams>true</UseVersionClipProcParams>
|
|
2021
2059
|
</ListMgt::LmVersion>
|
|
2022
|
-
</pClipFullVer
|
|
2060
|
+
</pClipFullVer>${trackSection}
|
|
2023
2061
|
<PrimaryCCMode>0</PrimaryCCMode>
|
|
2024
2062
|
</Gallery::GyStill>
|
|
2025
2063
|
`;
|
|
@@ -2774,8 +2812,8 @@ function buildLumaQualifierParams(lumaParams) {
|
|
|
2774
2812
|
* @param {number} [windowParams.soft4=0] - Softness 4 (linear only)
|
|
2775
2813
|
* @returns {Array} - Parameter entries for window corrector
|
|
2776
2814
|
*/
|
|
2777
|
-
function buildWindowParams(windowParams) {
|
|
2778
|
-
const groups = { transform: [], softMask: [], gradient: [], shape: [] };
|
|
2815
|
+
function buildWindowParams(windowParams, { resolveVersion } = {}) {
|
|
2816
|
+
const groups = { transform: [], transformType: CORRECTOR_TYPES.POWER_WINDOW, softMask: [], gradient: [], shape: [] };
|
|
2779
2817
|
const paramEntries = groups.transform;
|
|
2780
2818
|
const { POWER_WINDOWS } = drxParams;
|
|
2781
2819
|
|
|
@@ -2911,12 +2949,78 @@ function buildWindowParams(windowParams) {
|
|
|
2911
2949
|
paramEntries.push(createParameterEntry(POWER_WINDOWS.SOFT_REF, w.soft1 * 16));
|
|
2912
2950
|
}
|
|
2913
2951
|
|
|
2952
|
+
if (isResolve21(resolveVersion) && !isVertexShape && paramEntries.length > 0) {
|
|
2953
|
+
groups.transform = toResolve21WindowTransform(paramEntries);
|
|
2954
|
+
groups.transformType = CORRECTOR_TYPE_WINDOW_R21;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2914
2957
|
if (paramEntries.length > 0 || groups.softMask.length > 0) {
|
|
2915
2958
|
debugLog('[DRX] Window params built:', paramEntries.length, '+', groups.softMask.length, isLinear ? '(linear: ct4 + ct3 mask)' : '(circle: ct4)');
|
|
2916
2959
|
}
|
|
2917
2960
|
return groups;
|
|
2918
2961
|
}
|
|
2919
2962
|
|
|
2963
|
+
// Resolve 21 stores a circle/linear window transform as corrector type 0x10004 with the full
|
|
2964
|
+
// parameter set, not as corrector type 4 holding only the non-neutral parameters. The type 4
|
|
2965
|
+
// form loads into the Window palette on Resolve 21 but renders an empty mask, which also hides
|
|
2966
|
+
// the grade of the node. Order and neutral values follow what Resolve 21.0.4 exports for a
|
|
2967
|
+
// fresh circle window, as measured and pixel-verified by justhung69 in
|
|
2968
|
+
// https://github.com/justhung69/resolve-mcp (server/drx_r21.py, MIT, (c) 2026 Bùi Xuân Hưng).
|
|
2969
|
+
const CORRECTOR_TYPE_WINDOW_R21 = 0x10004;
|
|
2970
|
+
const R21_WINDOW_ORDER = [
|
|
2971
|
+
[POWER_WINDOW_IDS.ROTATE, 0],
|
|
2972
|
+
[POWER_WINDOW_IDS.ASPECT, 0],
|
|
2973
|
+
[POWER_WINDOW_IDS.SOFT_REF, 0],
|
|
2974
|
+
[POWER_WINDOW_IDS.SIZE, 1],
|
|
2975
|
+
[POWER_WINDOW_IDS.PAN, 0],
|
|
2976
|
+
[POWER_WINDOW_IDS.TILT, 0],
|
|
2977
|
+
];
|
|
2978
|
+
|
|
2979
|
+
/** True when `resolveVersion` (a major number such as 21, or a string such as "21.0.4") is Resolve 21 or later. */
|
|
2980
|
+
function isResolve21(resolveVersion) {
|
|
2981
|
+
return Number.parseInt(resolveVersion, 10) >= 21;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
/** Read the parameter id out of an F3 parameter entry built by createParameterEntry. */
|
|
2985
|
+
function parameterEntryId(entry) {
|
|
2986
|
+
let i = 1; // skip the F3 tag
|
|
2987
|
+
while (entry[i++] & 0x80); // skip the entry length
|
|
2988
|
+
i++; // skip the F1 tag
|
|
2989
|
+
let id = 0;
|
|
2990
|
+
let shift = 0;
|
|
2991
|
+
let byte;
|
|
2992
|
+
do {
|
|
2993
|
+
byte = entry[i++];
|
|
2994
|
+
id += (byte & 0x7f) * 2 ** shift;
|
|
2995
|
+
shift += 7;
|
|
2996
|
+
} while (byte & 0x80);
|
|
2997
|
+
return id;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
function toResolve21WindowTransform(entries) {
|
|
3001
|
+
const byId = new Map(entries.map((entry) => [parameterEntryId(entry), entry]));
|
|
3002
|
+
const take = (id) => {
|
|
3003
|
+
const entry = byId.get(id);
|
|
3004
|
+
byId.delete(id);
|
|
3005
|
+
return entry;
|
|
3006
|
+
};
|
|
3007
|
+
const out = R21_WINDOW_ORDER.map(([id, neutral]) => take(id) || createParameterEntry(id, neutral));
|
|
3008
|
+
take(POWER_WINDOW_IDS.WINDOW_TYPE);
|
|
3009
|
+
const identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
3010
|
+
const matrix = protoBytes(3, Buffer.concat([
|
|
3011
|
+
protoVarint(1, POWER_WINDOW_IDS.TRACKING_BLOB),
|
|
3012
|
+
protoBytes(2, protoBytes(10, Buffer.concat(identity.map((v, i) => protoFloat32(i + 1, v))))),
|
|
3013
|
+
]));
|
|
3014
|
+
out.push(
|
|
3015
|
+
createVarintParameterEntry(POWER_WINDOW_IDS.WINDOW_TYPE, 2),
|
|
3016
|
+
createParameterEntry(POWER_WINDOW_IDS.REF_WIDTH, 288),
|
|
3017
|
+
createParameterEntry(POWER_WINDOW_IDS.UNK_0E, 0),
|
|
3018
|
+
createParameterEntry(POWER_WINDOW_IDS.UNK_0F, 0),
|
|
3019
|
+
take(POWER_WINDOW_IDS.TRACKING_BLOB) || matrix,
|
|
3020
|
+
);
|
|
3021
|
+
return out.concat([...byId.values()]); // e.g. opacity, kept after the measured set
|
|
3022
|
+
}
|
|
3023
|
+
|
|
2920
3024
|
/**
|
|
2921
3025
|
* Check if any secondary correction tools are used (qualifier, window, HDR wheels)
|
|
2922
3026
|
* Used to determine if we need to add those corrector blocks
|
package/src/granular/common.py
CHANGED
|
@@ -19,6 +19,7 @@ for path in (SRC_DIR, PROJECT_DIR):
|
|
|
19
19
|
|
|
20
20
|
from mcp.server.fastmcp import FastMCP
|
|
21
21
|
from mcp.types import ToolAnnotations
|
|
22
|
+
from src.utils.mcp_error_flag import ErrorFlaggingFastMCP
|
|
22
23
|
|
|
23
24
|
from src.utils.app_control import (
|
|
24
25
|
get_app_state,
|
|
@@ -91,14 +92,14 @@ if not logging.getLogger().handlers:
|
|
|
91
92
|
handlers=[logging.StreamHandler()],
|
|
92
93
|
)
|
|
93
94
|
|
|
94
|
-
VERSION = "4.
|
|
95
|
+
VERSION = "4.17.0"
|
|
95
96
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
96
97
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
97
98
|
logger.info(f"Detected platform: {get_platform()}")
|
|
98
99
|
logger.info(f"Using Resolve API path: {RESOLVE_API_PATH}")
|
|
99
100
|
logger.info(f"Using Resolve library path: {RESOLVE_LIB_PATH}")
|
|
100
101
|
|
|
101
|
-
mcp =
|
|
102
|
+
mcp = ErrorFlaggingFastMCP("DaVinciResolveMCP")
|
|
102
103
|
if hasattr(mcp, "_mcp_server"):
|
|
103
104
|
mcp._mcp_server.version = VERSION
|
|
104
105
|
|