davinci-resolve-mcp 2.218.1 → 2.219.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 +89 -0
- package/README.md +4 -4
- package/README.zh-CN.md +5 -5
- package/docs/SKILL.md +3 -1
- package/docs/contributing.md +1 -1
- package/docs/install.md +2 -2
- package/docs/reference/api-coverage.md +3 -1
- package/docs/reference/api-limitations.md +1 -1
- package/docs/reference/readwrite-symmetry.md +3 -3
- package/docs/reference/resolve211-multicam.md +59 -0
- package/docs/reference/resolve211-native-transitions.md +11 -2
- package/install.py +2 -2
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/resolve_211.py +32 -1
- package/src/resolve_mcp_server.py +1 -1
- package/src/server.py +23 -6
- package/src/utils/api_truth.py +23 -3
- package/src/utils/destructive_hook.py +2 -0
- package/src/utils/execution_lifecycle.py +2 -0
- package/src/utils/resolve211_multicam.py +68 -0
- package/src/utils/resolve_versions.py +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,95 @@
|
|
|
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.219.0 — native Resolve 21.1 multicam creation and flattening
|
|
6
|
+
|
|
7
|
+
Contributed by @legionsound (#211), live-validated on Studio 21.1.0.14.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`media_pool create_multicam_clip`**, with the granular twin
|
|
12
|
+
`create_multicam_clip`, calling 21.1's native `CreateMulticamClip`. It takes
|
|
13
|
+
a list of media-pool unique IDs and an options dictionary covering all
|
|
14
|
+
eleven documented `MulticamOptions` fields. Enum fields accept either a
|
|
15
|
+
documented Resolve constant name or an integral native value; omitted fields
|
|
16
|
+
stay omitted rather than being filled with invented defaults. It returns the
|
|
17
|
+
ids and names of the clips **actually created**, and an empty native result
|
|
18
|
+
stays `success: false`.
|
|
19
|
+
- **`timeline_item flatten_multicam`**, with the granular twin
|
|
20
|
+
`flatten_timeline_item_multicam`, calling native `FlattenMulticam` with
|
|
21
|
+
either documented grade constant. Flattening replaces the item, so the tool
|
|
22
|
+
documentation tells callers to re-query the track afterwards.
|
|
23
|
+
- Every clip ID is resolved **before** anything is written, and an unknown or
|
|
24
|
+
duplicated ID refuses with nothing created. That is all-or-nothing input
|
|
25
|
+
resolution, not a transaction — the module says so in its own docstring
|
|
26
|
+
rather than implying a guarantee the native call does not offer.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Both actions are registered in **both** write tables — the
|
|
31
|
+
`destructive_hook` registry and the MEDIUM-risk set in
|
|
32
|
+
`execution_lifecycle` — so safe mode, the dry-run refusal, the audit log and
|
|
33
|
+
the operation log all treat them as the mutations they are. Verified by
|
|
34
|
+
probing the classifier directly rather than reading the diff. Tool count 368
|
|
35
|
+
→ 370 across the docs and the generated agent-rule files.
|
|
36
|
+
- The existing stacked-timeline multicam workflow is unchanged and still
|
|
37
|
+
available; the native route is an addition, not a replacement.
|
|
38
|
+
|
|
39
|
+
### Validation
|
|
40
|
+
|
|
41
|
+
- Full suite green: 3,453 passed, 1 skipped. Both write tables probed directly:
|
|
42
|
+
`media_pool.create_multicam_clip` and `timeline_item.flatten_multicam` each
|
|
43
|
+
classify MEDIUM / destructive / recognised.
|
|
44
|
+
- The return shape is right by documentation as well as by measurement — the
|
|
45
|
+
shipped 21.1 stub declares `CreateMulticamClip(clips, multicamOptions) ->
|
|
46
|
+
list[MediaPoolItem]`.
|
|
47
|
+
- Live evidence is @legionsound's, measured on Studio 21.1.0.14 with synthetic
|
|
48
|
+
media in a disposable project, and this was the family where a **rendered**
|
|
49
|
+
comparison rather than a readback was the bar it had to clear: both
|
|
50
|
+
interfaces created native multicam items, rendered, flattened with
|
|
51
|
+
`COPY_GRADE`, and rendered again — all four complete decoded RGB movies
|
|
52
|
+
identical at 144 frames, media type becoming Video, clip span unchanged.
|
|
53
|
+
Not reproduced here; this machine is Studio 19.1.3.7, below the 21.1 floor,
|
|
54
|
+
where both methods refuse with their version error.
|
|
55
|
+
- **What that evidence does not cover**, stated so it is not read as more: it
|
|
56
|
+
proves the natively-selected angle survives flattening in an ungraded
|
|
57
|
+
fixture. Angle ordering, alternate-angle selection, grade transfer, audio
|
|
58
|
+
routing and synchronisation are unverified. Smart Switch and
|
|
59
|
+
`AutoAlignClips` are deliberately not part of this change.
|
|
60
|
+
|
|
61
|
+
## What's New in v2.218.2 — the AddTransition null-duration boundary, measured
|
|
62
|
+
|
|
63
|
+
Measured by @legionsound on Studio 21.1.0.14 (#209), recorded here; no behavior
|
|
64
|
+
changed.
|
|
65
|
+
|
|
66
|
+
### Documentation
|
|
67
|
+
|
|
68
|
+
- **An explicit `"duration": null` is not a special case.** v2.218.0 shipped
|
|
69
|
+
`add_transition` forwarding an explicit null verbatim rather than dropping
|
|
70
|
+
the key, on the principle that inventing a default would misreport what the
|
|
71
|
+
server asked Resolve for — but only the 24-frame case had been measured, so
|
|
72
|
+
whether Resolve read a null as "automatic" or refused it was an open
|
|
73
|
+
question raised on #209. It is now answered: on fresh timelines with the same
|
|
74
|
+
handled red/blue fixture, **omitting the key and passing `duration: null`
|
|
75
|
+
behaved identically**, each creating a transition of 8 frames spanning 67–75
|
|
76
|
+
around a cut at 71.
|
|
77
|
+
- So the shipped behavior is correct as written, and the ledger now says why a
|
|
78
|
+
future wrapper must **not** strip an explicit null to route around a
|
|
79
|
+
refusal — there is no refusal to route around. `resolve_control api_truth
|
|
80
|
+
"AddTransition"` carries this, alongside the standing 21.1 gap it does not
|
|
81
|
+
close: there is still no accessor for an existing transition's type,
|
|
82
|
+
alignment or duration beyond its name and frame range, and no clone verb.
|
|
83
|
+
- The 8 frames is recorded as **what that build chose for that fixture, not a
|
|
84
|
+
documented default**, and these two cases were creation and readback only —
|
|
85
|
+
they were not rendered, unlike the 24-frame fixture behind v2.218.0.
|
|
86
|
+
|
|
87
|
+
### Validation
|
|
88
|
+
|
|
89
|
+
- Ledger and generated `docs/reference/api-limitations.md` regenerated; full
|
|
90
|
+
suite green. No code path changed, so no live Resolve run was required here —
|
|
91
|
+
and this machine is Studio 19.1.3.7, below the 21.1 floor, where the method
|
|
92
|
+
refuses by design. The measurement is @legionsound's on 21.1.0.14.
|
|
93
|
+
|
|
5
94
|
## What's New in v2.218.1 — Windows 11 process detection survives the removal of WMIC
|
|
6
95
|
|
|
7
96
|
Reported by @Nikibakht (#210), verified on Windows 11 Pro build 26200.
|
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
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
|
-
[-blue.svg)](#server-modes)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#server-modes)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
@@ -133,7 +133,7 @@ The command starts a loopback-only server and opens the control panel in your br
|
|
|
133
133
|
| Mode | Entry point | Tools | Best for |
|
|
134
134
|
|------|-------------|-------|----------|
|
|
135
135
|
| Compound | `src/server.py` | 36 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
|
|
136
|
-
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` |
|
|
136
|
+
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 370 | Power users who want one MCP tool per Resolve API method. |
|
|
137
137
|
|
|
138
138
|
The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
|
|
139
139
|
|
|
@@ -365,7 +365,7 @@ The default server is a local stdio process launched by your MCP client; it does
|
|
|
365
365
|
|
|
366
366
|
| Metric | Value |
|
|
367
367
|
|--------|-------|
|
|
368
|
-
| MCP Tools | **36** compound / **
|
|
368
|
+
| MCP Tools | **36** compound / **370** granular (live server) |
|
|
369
369
|
| Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
|
|
370
370
|
| Kernel Actions | **136** guarded workflow actions across 9 compound tools |
|
|
371
371
|
| API Methods Covered | **361/361** (100%) |
|
package/README.zh-CN.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
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
|
-
[-blue.svg)](#服务器模式)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#服务器模式)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.219.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -88,7 +88,7 @@ venv/bin/python -m src.control_panel
|
|
|
88
88
|
| 模式 | 入口 | 工具数 | 适合谁 |
|
|
89
89
|
|------|------|--------|--------|
|
|
90
90
|
| Compound(复合) | `src/server.py` | 36 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
|
|
91
|
-
| Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` |
|
|
91
|
+
| Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 370 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
|
|
92
92
|
|
|
93
93
|
除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
|
|
94
94
|
|
|
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
|
|
|
226
226
|
|
|
227
227
|
| 指标 | 数值 |
|
|
228
228
|
|------|------|
|
|
229
|
-
| MCP 工具 | **36** 复合 / **
|
|
229
|
+
| MCP 工具 | **36** 复合 / **370** 细粒度(实时服务器) |
|
|
230
230
|
| Advanced(离线)工具 | **18**——.drp/.drt/.drx + 数据库创作,无需 Resolve 运行 |
|
|
231
231
|
| 内核 action | 9 个复合工具下 **136** 个带护栏的工作流 action |
|
|
232
232
|
| API 方法覆盖 | **361/361**(100%) |
|
package/docs/SKILL.md
CHANGED
|
@@ -345,7 +345,7 @@ to the user as verified.
|
|
|
345
345
|
| Mode | Entry point | Tool count | Use when |
|
|
346
346
|
|---|---|---|---|
|
|
347
347
|
| Compound (default) | `src/server.py` | 36 tools | Most workflows — keeps context lean |
|
|
348
|
-
| Granular (full) | `src/server.py --full` |
|
|
348
|
+
| Granular (full) | `src/server.py --full` | 370 tools | Power users needing one tool per API method |
|
|
349
349
|
|
|
350
350
|
Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
|
|
351
351
|
for edition, presets, audio formats/codecs, normalization modes, speed, fades
|
|
@@ -2483,3 +2483,5 @@ The full API reference is in `docs/reference/resolve_scripting_api.txt`.
|
|
|
2483
2483
|
Native Resolve 21.1 speed and fade setters: see [speed/fades](reference/resolve211-speed-fades.md) for options, version guards and contributor validation limits.
|
|
2484
2484
|
|
|
2485
2485
|
Native 21.1 transition creation: see [transition controls](reference/resolve211-native-transitions.md) for options, item-index changes and contributor-rendered evidence.
|
|
2486
|
+
|
|
2487
|
+
Native multicam creation and flattening: [21.1 controls](reference/resolve211-multicam.md), with contributor render evidence and remaining family coverage.
|
package/docs/contributing.md
CHANGED
|
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
|
|
|
64
64
|
├── install.py # Universal installer (macOS/Windows/Linux)
|
|
65
65
|
├── src/
|
|
66
66
|
│ ├── server.py # Compound MCP server — 36 tools (default)
|
|
67
|
-
│ ├── resolve_mcp_server.py # Thin full-server entrypoint —
|
|
67
|
+
│ ├── resolve_mcp_server.py # Thin full-server entrypoint — 370 tools
|
|
68
68
|
│ ├── granular/ # Modular full-server implementation
|
|
69
69
|
│ └── utils/ # Platform detection, Resolve connection helpers
|
|
70
70
|
├── tests/ # offline suite (test_*.py) + live harnesses (live_*.py):
|
package/docs/install.md
CHANGED
|
@@ -144,7 +144,7 @@ The MCP server comes in two modes:
|
|
|
144
144
|
| Mode | File | Tools | Best For |
|
|
145
145
|
|------|------|-------|----------|
|
|
146
146
|
| **Compound** (default) | `src/server.py` | 36 | Most users — fast, clean, low context usage |
|
|
147
|
-
| **Full** | `src/resolve_mcp_server.py` |
|
|
147
|
+
| **Full** | `src/resolve_mcp_server.py` | 370 | Power users who want one tool per API method |
|
|
148
148
|
|
|
149
149
|
The compound server's `timeline_item` tool includes dedicated actions for common workflows:
|
|
150
150
|
|
|
@@ -159,7 +159,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
|
|
|
159
159
|
|
|
160
160
|
The installer uses the compound server by default. To use the full server:
|
|
161
161
|
```bash
|
|
162
|
-
python src/server.py --full # Launch full
|
|
162
|
+
python src/server.py --full # Launch full 370-tool server
|
|
163
163
|
# Or point your MCP config directly at src/resolve_mcp_server.py
|
|
164
164
|
```
|
|
165
165
|
|
|
@@ -25,7 +25,7 @@ Every non-deprecated method in the bundled legacy README is represented. This
|
|
|
25
25
|
does not claim complete coverage of the newer Resolve 21.1 typed API. The
|
|
26
26
|
default compound server exposes **36 tools** that group related operations by
|
|
27
27
|
action parameter, keeping LLM context windows lean. The full granular server
|
|
28
|
-
provides **
|
|
28
|
+
provides **370 individual tools** for power users. The legacy coverage spans
|
|
29
29
|
13 API object classes. MCP-level kernel actions are tracked separately in
|
|
30
30
|
[Kernel Action Coverage](../kernels/README.md).
|
|
31
31
|
|
|
@@ -633,3 +633,5 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
|
|
|
633
633
|
Native speed and fade setters in both interfaces are documented in [Resolve 21.1 speed/fades](resolve211-speed-fades.md), including sampled video validation and unverified cases.
|
|
634
634
|
|
|
635
635
|
Native 21.1 transition creation: see [transition controls](resolve211-native-transitions.md) for options, item-index changes and contributor-rendered evidence.
|
|
636
|
+
|
|
637
|
+
Native multicam creation and flattening: [21.1 controls](resolve211-multicam.md), with contributor render evidence and remaining family coverage.
|
|
@@ -82,7 +82,7 @@ equivalent, blocking full automation.
|
|
|
82
82
|
### Transition create / copy / clone
|
|
83
83
|
|
|
84
84
|
- **Object:** `Timeline / TimelineItem`
|
|
85
|
-
- **Behavior:** CREATION IS FIXED IN 21.1, READBACK IS NOT. Reported by @billcarroll (PR #197) from an attribute probe on Studio 21.1.0.14 (2026-09-08; not reproduced here, no 21.1 install): TimelineItem.AddTransition resolves to a <BlackmagicFusion.PyFunctionCall object>, not None. Its stub signature is AddTransition(transitionOptions) -> TimelineItem | None, where transitionOptions carries type (e.g. 'Cross Dissolve'), category ('simple'|'fusion'|'ofx'|'audio'), position ('start'|'end'), alignment ('left'|'center'|'right') and an optional duration in frames. That original probe did not invoke the method. UPDATE, contributor-validated by @legionsound on Studio 21.1.0.14, macOS, 2026-09-09 (not reproduced by the maintainer on 19.1.3.7): a synthetic red/blue pair with handles accepted a 24-frame centered Cross Dissolve. GetStart/End returned 59/83 around cut 71, GetDuration returned 24, and source clip spans were unchanged. Both community interfaces rendered identical 142-frame movies with a progressive red-to-blue blend. With zero handles the native call returned None. This validates that fixture, not other effects, audio transitions or alignments. See resolve211-native-transitions.md. WHAT REMAINS MISSING ON 21.1: reading a transition back. There is still no accessor for an existing transition's type, alignment or duration beyond its name string and frame range, and no clone verb — alignment and duration are write-only arguments to AddTransition. The pre-21.1 statement, kept as the historical record: there was no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING (pre-21.1: creation too; on 21.1+ read the paragraph above): cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — BUT a Solid Color generator AND a subtitle item read the same way (GetProperty() None, no MediaPoolItem; measured E115), so that pair only separates clips from non-clips. What separates a transition from a generator is GEOMETRY: a transition straddles a cut (one neighbour ends inside its span, another starts inside it) while a generator owns its span. timeline.get_items reports `kind` on that basis.
|
|
85
|
+
- **Behavior:** CREATION IS FIXED IN 21.1, READBACK IS NOT. Reported by @billcarroll (PR #197) from an attribute probe on Studio 21.1.0.14 (2026-09-08; not reproduced here, no 21.1 install): TimelineItem.AddTransition resolves to a <BlackmagicFusion.PyFunctionCall object>, not None. Its stub signature is AddTransition(transitionOptions) -> TimelineItem | None, where transitionOptions carries type (e.g. 'Cross Dissolve'), category ('simple'|'fusion'|'ofx'|'audio'), position ('start'|'end'), alignment ('left'|'center'|'right') and an optional duration in frames. That original probe did not invoke the method. UPDATE, contributor-validated by @legionsound on Studio 21.1.0.14, macOS, 2026-09-09 (not reproduced by the maintainer on 19.1.3.7): a synthetic red/blue pair with handles accepted a 24-frame centered Cross Dissolve. GetStart/End returned 59/83 around cut 71, GetDuration returned 24, and source clip spans were unchanged. Both community interfaces rendered identical 142-frame movies with a progressive red-to-blue blend. With zero handles the native call returned None. This validates that fixture, not other effects, audio transitions or alignments. See resolve211-native-transitions.md. DURATION IS OPTIONAL AND AN EXPLICIT NULL IS NOT A SPECIAL CASE, measured by @legionsound on Studio 21.1.0.14 (2026-09-09) on fresh timelines with the same handled red/blue fixture: omitting the duration key and passing duration=None behaved IDENTICALLY, both creating a transition at GetDuration()==8 spanning 67-75 around a cut at 71. So a wrapper must NOT strip an explicit null to work around a refusal — there is no refusal to work around. The 8 frames is what that build chose for that fixture, NOT a documented default; this was creation and readback only, not rendered. WHAT REMAINS MISSING ON 21.1: reading a transition back. There is still no accessor for an existing transition's type, alignment or duration beyond its name string and frame range, and no clone verb — alignment and duration are write-only arguments to AddTransition. The pre-21.1 statement, kept as the historical record: there was no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING (pre-21.1: creation too; on 21.1+ read the paragraph above): cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — BUT a Solid Color generator AND a subtitle item read the same way (GetProperty() None, no MediaPoolItem; measured E115), so that pair only separates clips from non-clips. What separates a transition from a generator is GEOMETRY: a transition straddles a cut (one neighbour ends inside its span, another starts inside it) while a generator owns its span. timeline.get_items reports `kind` on that basis.
|
|
86
86
|
- **Workaround / current handling:** Automated QC of existing transitions IS possible and is the main practical need — enumerate GetItemListInTrack, treat any item whose GetProperty() is empty and whose GetMediaPoolItem() is None as a transition, and read its name, start and duration. Removal is scriptable via Timeline.DeleteClips. To CREATE one, either apply it in the Resolve UI, or author it offline and import: the advanced server's drp place_transition writes a cross dissolve at an abutting cut ({track, atFrame, durationFrames}) and it round-trips into Resolve 21.0.4.5 reading back at the expected centered range. On 21.1+ prefer TimelineItem.AddTransition, which takes the type, category, edge, alignment and duration directly.
|
|
87
87
|
- **Tags:** missing-method, timeline, transition
|
|
88
88
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Read/Write Symmetry Audit
|
|
4
4
|
|
|
5
|
-
- write-style action occurrences scanned: **
|
|
5
|
+
- write-style action occurrences scanned: **121**
|
|
6
6
|
- write-style action occurrences with a matching read: **73**
|
|
7
7
|
- distinct high-signal `set_` actions without a direct/known readback: **4**
|
|
8
8
|
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
- `set_keyframe_interpolation`
|
|
14
14
|
- `set_node_enabled`
|
|
15
15
|
|
|
16
|
-
## Low-signal (create/add/insert/apply/import — usually expected):
|
|
16
|
+
## Low-signal (create/add/insert/apply/import — usually expected): 43 distinct names
|
|
17
17
|
|
|
18
|
-
`add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
|
|
18
|
+
`add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_multicam_clip`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Native Resolve 21.1 multicam creation and flattening
|
|
2
|
+
|
|
3
|
+
Compound `media_pool create_multicam_clip` accepts `clip_ids` (media-pool unique
|
|
4
|
+
IDs) and optional `options`. Granular `create_multicam_clip` takes the same
|
|
5
|
+
arguments. Every ID must resolve before the native call; missing/duplicate IDs
|
|
6
|
+
are errors, never silently dropped angles. Success returns the IDs and names of
|
|
7
|
+
all returned multicam clips. An empty native result is success:false. This does
|
|
8
|
+
not replace the existing stacked-timeline preparation workflow on older Resolve.
|
|
9
|
+
|
|
10
|
+
All eleven MulticamOptions fields are supported: name, startTimecode, frameRate,
|
|
11
|
+
angleSyncMode, channelConfig, multicamAudioMode, angleNameMode, splitAtGaps,
|
|
12
|
+
useFullClipExtents, createBinForSourceClips and detectSameCameraClipsMode.
|
|
13
|
+
Documented Resolve constant names (for example MULTICAM_ANGLE_SYNC_TIMECODE) are
|
|
14
|
+
resolved against the live Resolve object. Integral native numeric constant values
|
|
15
|
+
are also accepted; the wrapper does not invent their numbering. Unknown option
|
|
16
|
+
keys, invalid named constants, malformed booleans and non-positive/non-finite
|
|
17
|
+
frame rates are refused. Omitted options stay omitted. Resolve controls native
|
|
18
|
+
defaults, including createBinForSourceClips=true, which can reorganize source
|
|
19
|
+
clips in the media pool. No source files are modified by this wrapper.
|
|
20
|
+
|
|
21
|
+
Compound `timeline_item flatten_multicam` and granular
|
|
22
|
+
`flatten_timeline_item_multicam` use the normal 1-based track/0-based item
|
|
23
|
+
locators and optional `grade_option`, default FLATTEN_MULTICAM_COPY_GRADE.
|
|
24
|
+
FLATTEN_MULTICAM_RETAIN_GRADE_FROM_ANGLE is also accepted. This replaces the
|
|
25
|
+
multicam item with its active angle, so re-query the timeline afterward. Native
|
|
26
|
+
false stays success:false. Both methods require 21.1 and callable APIs; both
|
|
27
|
+
compound actions are registered as destructive MEDIUM-risk writes in the two
|
|
28
|
+
risk tables. Granular annotations also declare destructive writes. Explicit
|
|
29
|
+
compound dry-run requests are refused before mutation.
|
|
30
|
+
|
|
31
|
+
## Contributor validation
|
|
32
|
+
|
|
33
|
+
Contributor-validated on macOS Studio 21.1.0.14 using generated red/blue media in
|
|
34
|
+
a disposable 640x360/24 fps project. Both interfaces created a native Multicam
|
|
35
|
+
media-pool item, appended it to a timeline, rendered it, flattened with COPY_GRADE,
|
|
36
|
+
and rendered again. All four complete decoded RGB movies were identical, each
|
|
37
|
+
144 frames. Flattened media type changed from Multicam to Video; start0 and
|
|
38
|
+
144-frame duration were preserved. This proves preservation of the native-selected angle video in
|
|
39
|
+
an ungraded fixture. It does not prove second-angle switching, grade-copy versus
|
|
40
|
+
retained-grade differences, audio routing, audio synchronization, gap splitting
|
|
41
|
+
or camera-detection behavior. These options are documented pass-through, not
|
|
42
|
+
claimed live verified.
|
|
43
|
+
|
|
44
|
+
`tests/live_resolve211_multicam.py OUTPUT_DIR` requires the named disposable
|
|
45
|
+
project Codex Multicam Validation 20260909 with synthetic red.mov and blue.mov
|
|
46
|
+
six-second clips. It creates timelines and render jobs and saves that project.
|
|
47
|
+
Use the synthetic FFmpeg fixture commands in resolve211-native-transitions.md;
|
|
48
|
+
never run the fixture against production media. Compare complete decoded frames
|
|
49
|
+
from compound-before/compound-flattened/granular-before/granular-flattened.mov.
|
|
50
|
+
|
|
51
|
+
Smart Switch and AutoAlignClips remain separate work. Native timecode alignment
|
|
52
|
+
has positive position-readback evidence, but rendered/waveform acceptance remains
|
|
53
|
+
open. Silent color-card Smart Switch returned false and is not a meaningful
|
|
54
|
+
positive speaking-camera test. This contribution does not claim the entire
|
|
55
|
+
multicam family completed.
|
|
56
|
+
|
|
57
|
+
Do not infer angle order from clip_ids order. The wrapper forwards the requested
|
|
58
|
+
source order, but this measurement establishes the selected angle and its
|
|
59
|
+
preservation, not a contract for Resolve's default angle selection.
|
|
@@ -45,8 +45,17 @@ The included scratch test exercises both community interfaces and creates render
|
|
|
45
45
|
jobs for independent movie inspection. Unit tests cover actual returned spans,
|
|
46
46
|
missing native methods, None/False failures, malformed options, optional duration,
|
|
47
47
|
write classification, and dry-run refusal. Audio transitions, Fusion/OFX effects,
|
|
48
|
-
other alignments
|
|
49
|
-
|
|
48
|
+
other alignments and repeated insertion are not live-validated by this
|
|
49
|
+
contribution. Support for their documented options is pass-through.
|
|
50
|
+
|
|
51
|
+
Automatic duration was measured afterwards, by @legionsound on Studio 21.1.0.14
|
|
52
|
+
(2026-09-09), on fresh timelines with the same handled red/blue fixture: omitting
|
|
53
|
+
the `duration` key and passing `duration: null` behaved identically, each creating
|
|
54
|
+
a transition of 8 frames spanning 67-75 around a cut at 71. An explicit null is
|
|
55
|
+
therefore not a special case, and a wrapper must not strip it to route around a
|
|
56
|
+
refusal — there is no refusal. The 8 frames is what that build chose for that
|
|
57
|
+
fixture, not a documented default, and these two cases were creation and readback
|
|
58
|
+
only; they were not rendered.
|
|
50
59
|
|
|
51
60
|
`python tests/live_resolve211_transitions.py OUTPUT_DIR` requires a disposable
|
|
52
61
|
project named `Codex Native Transition Validation 20260909`, set to 640x360/24 fps,
|
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.219.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
|
|
@@ -1543,7 +1543,7 @@ def verify_resolve_connection(python_path, api_path, lib_path):
|
|
|
1543
1543
|
|
|
1544
1544
|
def print_banner():
|
|
1545
1545
|
title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
|
|
1546
|
-
subtitle = "36 compound ·
|
|
1546
|
+
subtitle = "36 compound · 370 full · 3 platforms"
|
|
1547
1547
|
print()
|
|
1548
1548
|
print(bold(" ╔══════════════════════════════════════════════════════╗"))
|
|
1549
1549
|
print(bold(f" ║{title:^54}║"))
|
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.219.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()}")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Native Resolve 21.1 discovery and editing controls."""
|
|
2
|
+
from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
|
|
2
3
|
from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
|
|
3
4
|
from src.granular.common import (
|
|
4
5
|
mcp, READ_ONLY_TOOL, WRITE_TOOL, DESTRUCTIVE_TOOL, get_resolve, get_current_project,
|
|
5
|
-
_get_timeline, _get_timeline_item, _requires_method, has_method,
|
|
6
|
+
_get_timeline, _get_timeline_item, _find_clip_by_id, _requires_method, has_method,
|
|
6
7
|
)
|
|
7
8
|
|
|
8
9
|
|
|
@@ -198,3 +199,33 @@ def add_timeline_item_transition(options: dict, track_type: str = "video", track
|
|
|
198
199
|
if missing:
|
|
199
200
|
return missing
|
|
200
201
|
return transition_result(item.AddTransition(dict(options)))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@mcp.tool(annotations=DESTRUCTIVE_TOOL)
|
|
205
|
+
def create_multicam_clip(clip_ids: list[str], options: dict | None = None) -> dict:
|
|
206
|
+
"""Create native 21.1 multicam clips. Options follow MulticamOptions; named Resolve constants or numeric values accepted. Resolves every ID before writing."""
|
|
207
|
+
_, p = get_current_project()
|
|
208
|
+
if p is None:
|
|
209
|
+
return {"error": "No project currently open"}
|
|
210
|
+
mp = p.GetMediaPool()
|
|
211
|
+
missing = _requires_method(mp, "CreateMulticamClip", "21.1")
|
|
212
|
+
if missing:
|
|
213
|
+
return missing
|
|
214
|
+
return create_multicam(get_resolve(), mp, clip_ids, {} if options is None else options, _find_clip_by_id)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@mcp.tool(annotations=DESTRUCTIVE_TOOL)
|
|
218
|
+
def flatten_timeline_item_multicam(grade_option: str = "FLATTEN_MULTICAM_COPY_GRADE", track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
|
|
219
|
+
"""Flatten a native multicam item using COPY_GRADE or RETAIN_GRADE_FROM_ANGLE. Re-query items after replacement."""
|
|
220
|
+
if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
|
|
221
|
+
return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
|
|
222
|
+
item, error = _get_timeline_item(track_type, track_index, item_index)
|
|
223
|
+
if error:
|
|
224
|
+
return error
|
|
225
|
+
missing = _requires_method(item, "FlattenMulticam", "21.1")
|
|
226
|
+
if missing:
|
|
227
|
+
return missing
|
|
228
|
+
grade, error = resolve_constant(get_resolve(), grade_option, GRADES)
|
|
229
|
+
if error:
|
|
230
|
+
return {"error": error}
|
|
231
|
+
return {"success": bool(item.FlattenMulticam(grade))}
|
|
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
|
|
|
34
34
|
if __name__ == "__main__":
|
|
35
35
|
try:
|
|
36
36
|
start_background_update_check(VERSION, project_dir, logger)
|
|
37
|
-
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (
|
|
37
|
+
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (370 granular tools)")
|
|
38
38
|
run_fastmcp_stdio(mcp)
|
|
39
39
|
except KeyboardInterrupt:
|
|
40
40
|
logger.info("Server shutdown requested")
|
package/src/server.py
CHANGED
|
@@ -8,10 +8,10 @@ Each tool groups related operations via an 'action' parameter.
|
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
python src/server.py # Start the MCP server
|
|
11
|
-
python src/server.py --full # Start the
|
|
11
|
+
python src/server.py --full # Start the 370-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.219.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -42,6 +42,7 @@ for p in [current_dir, project_dir]:
|
|
|
42
42
|
if p not in sys.path:
|
|
43
43
|
sys.path.insert(0, p)
|
|
44
44
|
|
|
45
|
+
from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
|
|
45
46
|
from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
|
|
46
47
|
|
|
47
48
|
# Platform-specific Resolve paths
|
|
@@ -20826,6 +20827,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
20826
20827
|
exist. Raw mutators below do not validate paths, support dry_run, or normalize errors.
|
|
20827
20828
|
|
|
20828
20829
|
Actions:
|
|
20830
|
+
create_multicam_clip(clip_ids, options?) -> {success, clips} — native 21.1 multicam creation.
|
|
20829
20831
|
get_root_folder() -> {name, id}
|
|
20830
20832
|
get_current_folder() -> {name, id}
|
|
20831
20833
|
set_current_folder(path) -> {success} — path like "Master/SubFolder"
|
|
@@ -21036,6 +21038,11 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21036
21038
|
created_new=True,
|
|
21037
21039
|
versioned_name=bool(existing and create_name != p.get("name")),
|
|
21038
21040
|
) if tl else _err("Failed to create timeline")
|
|
21041
|
+
elif action == "create_multicam_clip":
|
|
21042
|
+
missing = _requires_method(mp, "CreateMulticamClip", "21.1")
|
|
21043
|
+
if missing:
|
|
21044
|
+
return missing
|
|
21045
|
+
return create_multicam(get_resolve(), mp, p.get("clip_ids"), {} if p.get("options") is None else p["options"], _find_clip)
|
|
21039
21046
|
elif action == "setup_multicam_timeline":
|
|
21040
21047
|
return _setup_multicam_timeline(proj, mp, p)
|
|
21041
21048
|
elif action == "import_timeline":
|
|
@@ -21298,7 +21305,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21298
21305
|
return _copy_clip_annotations(root, p)
|
|
21299
21306
|
elif action == "media_pool_boundary_report":
|
|
21300
21307
|
return _media_pool_boundary_report(mp, p)
|
|
21301
|
-
return _unknown(action, ["get_root_folder","get_current_folder","set_current_folder","add_subfolder","delete_folders","move_folders","refresh","create_timeline","create_timeline_from_clips","import_timeline","delete_timelines","append_to_timeline","import_media","delete_clips","move_clips","relink","unlink","export_metadata","get_unique_id","create_stereo_clip","auto_sync_audio","get_selected","set_selected","get_clip_mattes","get_timeline_mattes","delete_clip_mattes","import_folder",*_MEDIA_POOL_KERNEL_ACTIONS])
|
|
21308
|
+
return _unknown(action, ["create_multicam_clip","get_root_folder","get_current_folder","set_current_folder","add_subfolder","delete_folders","move_folders","refresh","create_timeline","create_timeline_from_clips","import_timeline","delete_timelines","append_to_timeline","import_media","delete_clips","move_clips","relink","unlink","export_metadata","get_unique_id","create_stereo_clip","auto_sync_audio","get_selected","set_selected","get_clip_mattes","get_timeline_mattes","delete_clip_mattes","import_folder",*_MEDIA_POOL_KERNEL_ACTIONS])
|
|
21302
21309
|
|
|
21303
21310
|
|
|
21304
21311
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -26274,6 +26281,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
26274
26281
|
Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
|
|
26275
26282
|
|
|
26276
26283
|
Actions:
|
|
26284
|
+
flatten_multicam(grade_option?, ...) -> {success} — native 21.1, replaces multicam with its current angle.
|
|
26277
26285
|
add_transition(options, ...) -> {success, transition?} — native 21.1 transition; reports actual span.
|
|
26278
26286
|
set_speed(options, ...) -> {success} — native 21.1 speed options; RippleTimeline defaults false.
|
|
26279
26287
|
set_fades(options, ...) -> {success} — native 21.1 FadeIn/FadeOut integer frames.
|
|
@@ -26331,6 +26339,15 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
26331
26339
|
if err:
|
|
26332
26340
|
return err
|
|
26333
26341
|
|
|
26342
|
+
if action == "flatten_multicam":
|
|
26343
|
+
missing = _requires_method(item, "FlattenMulticam", "21.1")
|
|
26344
|
+
if missing:
|
|
26345
|
+
return missing
|
|
26346
|
+
grade, error = resolve_constant(get_resolve(), p.get("grade_option", "FLATTEN_MULTICAM_COPY_GRADE"), GRADES)
|
|
26347
|
+
if error:
|
|
26348
|
+
return _err(error)
|
|
26349
|
+
return {"success": bool(item.FlattenMulticam(grade))}
|
|
26350
|
+
|
|
26334
26351
|
if action == "add_transition":
|
|
26335
26352
|
options = p.get("options")
|
|
26336
26353
|
error = validate_transition_options(options)
|
|
@@ -26552,7 +26569,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
26552
26569
|
return _err(f"Invalid interpolation. Must be one of: {', '.join(valid)}")
|
|
26553
26570
|
return {"success": bool(item.SetKeyframeInterpolation(p["property"], p["frame"], p["interpolation"]))}
|
|
26554
26571
|
|
|
26555
|
-
return _unknown(action, ["add_transition","set_speed","set_fades","get_speed","get_fades","get_output_blanking","get_use_timeline_for_output_blanking","get_name","get_property","set_property","get_duration","get_start","get_end","get_source_start_frame","get_source_end_frame","get_source_start_time","get_source_end_time","get_left_offset","get_right_offset","set_clip_enabled","get_clip_enabled","update_sidecar","get_unique_id","get_media_pool_item","get_stereo_convergence","get_stereo_left_window","get_stereo_right_window","get_linked_items","get_track_type_and_index","get_source_audio_mapping","load_burnin_preset","set_name","get_voice_isolation_state","set_voice_isolation_state","get_retime","set_retime","get_transform","set_transform","get_crop","set_crop","get_composite","set_composite","get_audio","set_audio","get_keyframes","add_keyframe","modify_keyframe","delete_keyframe","set_keyframe_interpolation"])
|
|
26572
|
+
return _unknown(action, ["flatten_multicam","add_transition","set_speed","set_fades","get_speed","get_fades","get_output_blanking","get_use_timeline_for_output_blanking","get_name","get_property","set_property","get_duration","get_start","get_end","get_source_start_frame","get_source_end_frame","get_source_start_time","get_source_end_time","get_left_offset","get_right_offset","set_clip_enabled","get_clip_enabled","update_sidecar","get_unique_id","get_media_pool_item","get_stereo_convergence","get_stereo_left_window","get_stereo_right_window","get_linked_items","get_track_type_and_index","get_source_audio_mapping","load_burnin_preset","set_name","get_voice_isolation_state","set_voice_isolation_state","get_retime","set_retime","get_transform","set_transform","get_crop","set_crop","get_composite","set_composite","get_audio","set_audio","get_keyframes","add_keyframe","modify_keyframe","delete_keyframe","set_keyframe_interpolation"])
|
|
26556
26573
|
|
|
26557
26574
|
|
|
26558
26575
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -32674,9 +32691,9 @@ if __name__ == "__main__":
|
|
|
32674
32691
|
start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
|
|
32675
32692
|
_install_threaded_tool_dispatch(mcp)
|
|
32676
32693
|
|
|
32677
|
-
# Support --full flag to run the
|
|
32694
|
+
# Support --full flag to run the 370-tool granular server instead
|
|
32678
32695
|
if "--full" in sys.argv:
|
|
32679
|
-
logger.info("Starting full
|
|
32696
|
+
logger.info("Starting full 370-tool granular server...")
|
|
32680
32697
|
sys.argv = [arg for arg in sys.argv if arg != "--full"]
|
|
32681
32698
|
from src.granular import mcp as granular_mcp
|
|
32682
32699
|
|
package/src/utils/api_truth.py
CHANGED
|
@@ -878,9 +878,19 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
878
878
|
"21.1.0.14: each of those four resolves to a "
|
|
879
879
|
"<BlackmagicFusion.PyFunctionCall object>, not None — the same "
|
|
880
880
|
"discriminator that distinguishes a real method from an absent "
|
|
881
|
-
"one elsewhere in this registry.
|
|
882
|
-
"
|
|
883
|
-
"
|
|
881
|
+
"one elsewhere in this registry. That original probe did not "
|
|
882
|
+
"invoke the methods. UPDATE: contributor-validated by "
|
|
883
|
+
"@legionsound on Studio 21.1.0.14, macOS, 2026-09-09; not "
|
|
884
|
+
"reproduced by the maintainer on 19.1.3.7. Both community "
|
|
885
|
+
"interfaces created a Multicam from synthetic red/blue clips "
|
|
886
|
+
"and flattened it with COPY_GRADE. Four complete decoded "
|
|
887
|
+
"144-frame renders were identical before/after flattening "
|
|
888
|
+
"and across interfaces; media type changed Multicam to Video "
|
|
889
|
+
"without changing the selected clip span. This verifies the "
|
|
890
|
+
"native-selected angle in an ungraded fixture, not angle "
|
|
891
|
+
"ordering, grade transfer, audio sync or Smart Switch. See "
|
|
892
|
+
"resolve211-multicam.md for exact limits. HISTORICAL, still "
|
|
893
|
+
"true of 21.0.x and "
|
|
884
894
|
"earlier: there was no method to create a native multicam clip "
|
|
885
895
|
"from a set of angles; angles could be stacked onto tracks "
|
|
886
896
|
"programmatically but the multicam-clip conversion was a UI-only "
|
|
@@ -915,6 +925,16 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
915
925
|
"red-to-blue blend. With zero handles the native call returned "
|
|
916
926
|
"None. This validates that fixture, not other effects, audio "
|
|
917
927
|
"transitions or alignments. See resolve211-native-transitions.md. "
|
|
928
|
+
"DURATION IS OPTIONAL AND AN EXPLICIT NULL IS NOT A SPECIAL CASE, "
|
|
929
|
+
"measured by @legionsound on Studio 21.1.0.14 (2026-09-09) on "
|
|
930
|
+
"fresh timelines with the same handled red/blue fixture: omitting "
|
|
931
|
+
"the duration key and passing duration=None behaved IDENTICALLY, "
|
|
932
|
+
"both creating a transition at GetDuration()==8 spanning 67-75 "
|
|
933
|
+
"around a cut at 71. So a wrapper must NOT strip an explicit null "
|
|
934
|
+
"to work around a refusal — there is no refusal to work around. "
|
|
935
|
+
"The 8 frames is what that build chose for that fixture, NOT a "
|
|
936
|
+
"documented default; this was creation and readback only, not "
|
|
937
|
+
"rendered. "
|
|
918
938
|
"WHAT REMAINS MISSING ON 21.1: reading a "
|
|
919
939
|
"transition back. There is still no accessor for an existing "
|
|
920
940
|
"transition's type, alignment or duration beyond its name string "
|
|
@@ -74,6 +74,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
74
74
|
"create_timeline_from_clips",
|
|
75
75
|
"append_to_timeline",
|
|
76
76
|
"setup_multicam_timeline",
|
|
77
|
+
"create_multicam_clip",
|
|
77
78
|
"create_stereo_clip",
|
|
78
79
|
"auto_sync_audio",
|
|
79
80
|
"set_clip_marks",
|
|
@@ -151,6 +152,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
151
152
|
# classifier did not recognise them, so safe mode, the dry-run refusal,
|
|
152
153
|
# the audit log and the operation log all skipped a call that rewrites a
|
|
153
154
|
# clip's speed — and, with RippleTimeline true, moves every clip after it.
|
|
155
|
+
"flatten_multicam",
|
|
154
156
|
"add_transition",
|
|
155
157
|
"set_speed",
|
|
156
158
|
"set_fades",
|
|
@@ -274,6 +274,8 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
274
274
|
# Native 21.1 setters (#208): `set_speed` changes duration and, with
|
|
275
275
|
# RippleTimeline true, moves every clip after it; `set_fades` rewrites
|
|
276
276
|
# how the clip's edges render. Existing content altered, not deleted.
|
|
277
|
+
("media_pool", "create_multicam_clip"),
|
|
278
|
+
("timeline_item", "flatten_multicam"),
|
|
277
279
|
("timeline_item", "add_transition"),
|
|
278
280
|
("timeline_item", "set_speed"),
|
|
279
281
|
("timeline_item", "set_fades"),
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Native multicam options and all-or-nothing input resolution (not transactionality)."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
ENUMS = {
|
|
5
|
+
'angleSyncMode': tuple('MULTICAM_ANGLE_SYNC_'+v for v in ('IN','OUT','TIMECODE','AUDIO','MARKER')),
|
|
6
|
+
'angleNameMode': tuple('MULTICAM_ANGLE_NAME_'+v for v in ('SEQUENTIAL','ANGLE','CAMERA','CLIP','FILE')),
|
|
7
|
+
'multicamAudioMode': tuple('MULTICAM_AUDIO_'+v for v in ('ADAPTIVE','SOURCE','REFERENCE','ALL')),
|
|
8
|
+
'detectSameCameraClipsMode': ('MULTICAM_DETECT_NONE',) + tuple('MULTICAM_DETECT_BY_'+v for v in ('CAMERA_NUMBER','ANGLE','REEL_NUMBER','REEL_NAME','ROLL_CARD')),
|
|
9
|
+
'channelConfig': ('AUDIO_SYNC_CHANNEL_AUTOMATIC','AUDIO_SYNC_CHANNEL_MIX'),
|
|
10
|
+
}
|
|
11
|
+
BOOLS = {'splitAtGaps','useFullClipExtents','createBinForSourceClips'}
|
|
12
|
+
STRINGS = {'name','startTimecode'}
|
|
13
|
+
GRADES = ('FLATTEN_MULTICAM_COPY_GRADE','FLATTEN_MULTICAM_RETAIN_GRADE_FROM_ANGLE')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_constant(r, value, names):
|
|
17
|
+
if isinstance(value,str):
|
|
18
|
+
if value not in names:
|
|
19
|
+
return None, 'Unknown constant: '+value
|
|
20
|
+
native=getattr(r,value,None)
|
|
21
|
+
if type(native) not in (int,float) or not math.isfinite(native):
|
|
22
|
+
return None, 'Constant unavailable: '+value
|
|
23
|
+
return native,None
|
|
24
|
+
if type(value) in (int,float):
|
|
25
|
+
try:
|
|
26
|
+
if math.isfinite(value) and int(value)==value:
|
|
27
|
+
return value,None
|
|
28
|
+
except OverflowError:
|
|
29
|
+
pass
|
|
30
|
+
return None,'Use a documented constant name or integral native constant value'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_multicam(r, mp, clip_ids, options, find_clip):
|
|
34
|
+
if not isinstance(clip_ids,list) or not clip_ids or any(not isinstance(i,str) or not i for i in clip_ids):
|
|
35
|
+
return {'error':'clip_ids must be a non-empty list of media-pool unique IDs'}
|
|
36
|
+
if len(set(clip_ids))!=len(clip_ids):
|
|
37
|
+
return {'error':'clip_ids must not contain duplicates'}
|
|
38
|
+
if not isinstance(options,dict):
|
|
39
|
+
return {'error':'options must be a dictionary'}
|
|
40
|
+
if set(options)-(set(ENUMS)|BOOLS|STRINGS|{'frameRate'}):
|
|
41
|
+
return {'error':'Unknown multicam option'}
|
|
42
|
+
normalized=dict(options)
|
|
43
|
+
for key,value in options.items():
|
|
44
|
+
if key in BOOLS:
|
|
45
|
+
if type(value) is not bool:
|
|
46
|
+
return {'error':key+' must be a boolean'}
|
|
47
|
+
elif key in STRINGS:
|
|
48
|
+
if not isinstance(value,str) or not value.strip():
|
|
49
|
+
return {'error':key+' must be a non-empty string'}
|
|
50
|
+
elif key=='frameRate':
|
|
51
|
+
try:
|
|
52
|
+
valid=type(value) in (int,float) and math.isfinite(value) and value>0
|
|
53
|
+
except OverflowError:
|
|
54
|
+
valid=False
|
|
55
|
+
if not valid:
|
|
56
|
+
return {'error':'frameRate must be a positive finite number'}
|
|
57
|
+
elif key in ENUMS:
|
|
58
|
+
normalized[key],error=resolve_constant(r,value,ENUMS[key])
|
|
59
|
+
if error:
|
|
60
|
+
return {'error':key+': '+error}
|
|
61
|
+
root=mp.GetRootFolder()
|
|
62
|
+
clips=[find_clip(root,i) for i in clip_ids]
|
|
63
|
+
if any(c is None for c in clips):
|
|
64
|
+
return {'error':'One or more clip_ids were not found; no multicam was created'}
|
|
65
|
+
created=mp.CreateMulticamClip(clips,normalized)
|
|
66
|
+
if not created:
|
|
67
|
+
return {'success':False,'clips':[]}
|
|
68
|
+
return {'success':True,'clips':[{'id':c.GetUniqueId(),'name':c.GetName()} for c in created]}
|
|
@@ -225,6 +225,8 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
|
|
|
225
225
|
# disagree, which is what keeps the two from drifting apart again.
|
|
226
226
|
|
|
227
227
|
CODE_FLOORS: Dict[str, str] = {
|
|
228
|
+
"MediaPool.CreateMulticamClip": "21.1",
|
|
229
|
+
"TimelineItem.FlattenMulticam": "21.1",
|
|
228
230
|
"TimelineItem.AddTransition": "21.1",
|
|
229
231
|
"TimelineItem.SetSpeed": "21.1",
|
|
230
232
|
"TimelineItem.SetFades": "21.1",
|