davinci-resolve-mcp 4.12.0 → 4.13.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 +54 -0
- package/README.md +4 -4
- package/README.zh-CN.md +5 -5
- package/docs/SKILL.md +7 -1
- package/docs/install.md +2 -2
- package/docs/reference/api-coverage.md +1 -1
- package/docs/reference/api-limitations.md +25 -6
- package/install.py +2 -2
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/project.py +18 -1
- package/src/granular/resolve_control.py +23 -0
- package/src/resolve_mcp_server.py +1 -1
- package/src/server.py +92 -5
- package/src/utils/api_truth.py +116 -9
- package/src/utils/app_control.py +92 -0
- package/src/utils/bool_params.py +23 -0
- package/src/utils/destructive_hook.py +4 -1
- package/src/utils/execution_lifecycle.py +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
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.13.0 — Resolve 21.1 render-setting parity, `SetHighPriority(bool)`, and a project-destroying preset order
|
|
6
|
+
|
|
7
|
+
Lands the three items queued for @legionsound in #207. The 21.1 behaviour
|
|
8
|
+
below is **their measurement on Studio 21.1.0.14 / macOS**, recorded as
|
|
9
|
+
*reported*; this machine runs 19.1.3.7 and did not reproduce it.
|
|
10
|
+
|
|
11
|
+
### New
|
|
12
|
+
|
|
13
|
+
- **`resolve_control set_high_priority` takes `high`** (alias
|
|
14
|
+
`high_priority`, default `true`), and there is a new granular
|
|
15
|
+
**`set_high_priority(high)`** tool. On 21.1+ it calls
|
|
16
|
+
`SetHighPriority(bool)`, which 21.1 types as script execution priority and
|
|
17
|
+
which is reversible there: `high=false` restores normal. The no-argument call
|
|
18
|
+
this server used to make returns `False` on 21.1 and does nothing. Builds
|
|
19
|
+
before 21.1 keep the no-argument call, and `high=false` is refused on them,
|
|
20
|
+
because they cannot lower priority without a restart. `high` is coerced
|
|
21
|
+
(`"false"` is false); anything that doesn't name a boolean is refused before
|
|
22
|
+
any call. No build has a getter, so results carry `readback: null` and claim
|
|
23
|
+
no readback. Registered as a write in both the destructive registry and the
|
|
24
|
+
lifecycle risk sets (MEDIUM).
|
|
25
|
+
- **Compound render settings accept `AudioFormat` and `UseUniqueFilenames`.**
|
|
26
|
+
The compound whitelist held 31 of the 33 typed `RenderSettings` fields and
|
|
27
|
+
reported these two as ignored; granular already forwarded them. A test now
|
|
28
|
+
checks the whitelist against every field of the bundled 21.1 stub's
|
|
29
|
+
`RenderSettings`. `UseUniqueFilenames` is coerced on every path (compound
|
|
30
|
+
`set_settings`, `safe_set_render_settings`, `prepare_render_job`, granular
|
|
31
|
+
`set_render_settings`), and an unrecognised value is refused with
|
|
32
|
+
`INVALID_RENDER_SETTING`. `AudioFormat` with `ExportVideo: true` warns,
|
|
33
|
+
since the stub documents it for audio-only renders.
|
|
34
|
+
|
|
35
|
+
### API truth ledger
|
|
36
|
+
|
|
37
|
+
- **`Project.StartRendering` is state-dependent** (reported, 21.1.0.14):
|
|
38
|
+
freshly configured jobs sometimes return `False` and stay Ready, while an
|
|
39
|
+
identical later job completes. Direct official calls reproduced both
|
|
40
|
+
outcomes. Mapped to `render start`, so the fact rides on its results. Verify
|
|
41
|
+
job status and output; nothing retries automatically.
|
|
42
|
+
- **Project-settings presets mutated before a project's first `SaveProject`
|
|
43
|
+
destroy the project** (reported, 21.1.0.14, reproduced twice): saving a
|
|
44
|
+
preset, changing resolution, then updating, applying or deleting the preset,
|
|
45
|
+
then saving and closing. Every call returned `True` and SQLite
|
|
46
|
+
`integrity_check` said `ok`, but `LoadProject` then failed with "DaVinci
|
|
47
|
+
Resolve failed to open project". Save the project once before any preset
|
|
48
|
+
write, and prove it survived only by reloading it. No preset-write wrappers
|
|
49
|
+
ship yet; this entry lands first on purpose.
|
|
50
|
+
- The `Resolve.SetHighPriority` entry is now version-scoped: irreversible with
|
|
51
|
+
no argument before 21.1 (measured on 19.1.3.7), and a reversible boolean on
|
|
52
|
+
21.1 (reported).
|
|
53
|
+
|
|
54
|
+
### Counts
|
|
55
|
+
|
|
56
|
+
- Granular tools 389 → 390; compound stays at 37. Docs and generated agent
|
|
57
|
+
rules updated.
|
|
58
|
+
|
|
5
59
|
## What's New in v4.12.0 — Live subtitle presets and title text across Fusion title tools
|
|
6
60
|
|
|
7
61
|
Contributed by @sidevconcept (#281). The live behaviour below rests on their
|
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` | 37 | 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` | 390 | 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
|
|
|
@@ -410,7 +410,7 @@ The default server is a local stdio process launched by your MCP client; it does
|
|
|
410
410
|
|
|
411
411
|
| Metric | Value |
|
|
412
412
|
|--------|-------|
|
|
413
|
-
| MCP Tools | **37** compound / **
|
|
413
|
+
| MCP Tools | **37** compound / **390** granular (live server) |
|
|
414
414
|
| Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
|
|
415
415
|
| Kernel Actions | **136** guarded workflow actions across 9 compound tools |
|
|
416
416
|
| 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
|
-
> 本翻译对应 v4.
|
|
15
|
+
> 本翻译对应 v4.13.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` | 37 | 大多数助手的默认模式。相关的 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` | 390 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
|
|
92
92
|
|
|
93
93
|
除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
|
|
94
94
|
|
|
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
|
|
|
226
226
|
|
|
227
227
|
| 指标 | 数值 |
|
|
228
228
|
|------|------|
|
|
229
|
-
| MCP 工具 | **37** 复合 / **
|
|
229
|
+
| MCP 工具 | **37** 复合 / **390** 细粒度(实时服务器) |
|
|
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
|
@@ -353,7 +353,7 @@ to the user as verified.
|
|
|
353
353
|
| Mode | Entry point | Tool count | Use when |
|
|
354
354
|
|---|---|---|---|
|
|
355
355
|
| Compound (default) | `src/server.py` | 37 tools | Most workflows — keeps context lean |
|
|
356
|
-
| Granular (full) | `src/server.py --full` |
|
|
356
|
+
| Granular (full) | `src/server.py --full` | 390 tools | Power users needing one tool per API method |
|
|
357
357
|
|
|
358
358
|
Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
|
|
359
359
|
for edition, presets, audio formats/codecs, normalization modes, speed, fades
|
|
@@ -796,6 +796,12 @@ Key actions:
|
|
|
796
796
|
is fine. No connection needed, and it never launches Resolve
|
|
797
797
|
- `get_page` / `open_page(page)` — read or switch the active page
|
|
798
798
|
- `get_keyframe_mode` / `set_keyframe_mode(mode)`
|
|
799
|
+
- `set_high_priority(high?)` — script execution priority (granular:
|
|
800
|
+
`set_high_priority`). `high` defaults to true; on Resolve 21.1+ it calls
|
|
801
|
+
`SetHighPriority(bool)` and `high=false` restores normal. Older builds take no
|
|
802
|
+
argument and cannot lower it again this session, so `high=false` is refused
|
|
803
|
+
there. No build has a getter — report the native return, never a confirmed
|
|
804
|
+
state. Only call when the user asked for it
|
|
799
805
|
- `get_fairlight_presets` — Resolve 20.2.2+; returns available Fairlight
|
|
800
806
|
preset names
|
|
801
807
|
- `list/save/load/delete/import/export_user_preferences_preset` — Resolve
|
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` | 37 | Most users — fast, clean, low context usage |
|
|
147
|
-
| **Full** | `src/resolve_mcp_server.py` |
|
|
147
|
+
| **Full** | `src/resolve_mcp_server.py` | 390 | 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 390-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 **37 tools** that group related operations by
|
|
27
27
|
action parameter, keeping LLM context windows lean. The full granular server
|
|
28
|
-
provides **
|
|
28
|
+
provides **390 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
|
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 42 missing capabilities,
|
|
15
|
+
**Totals:** 42 missing capabilities, 58 bugs / unreliable behaviors.
|
|
16
16
|
|
|
17
17
|
The authoritative source is the runtime-queryable `api_truth` ledger
|
|
18
18
|
(`resolve_control api_truth "<query>"`); this document is generated from
|
|
@@ -288,13 +288,14 @@ equivalent, blocking full automation.
|
|
|
288
288
|
- **Workaround / current handling:** Record interpolation choices in the caller; readback is not available at any version.
|
|
289
289
|
- **Tags:** timeline, missing-method, readback, keyframes
|
|
290
290
|
|
|
291
|
-
### Resolve.SetHighPriority (write-only
|
|
291
|
+
### Resolve.SetHighPriority (write-only; irreversible before 21.1)
|
|
292
292
|
|
|
293
293
|
- **Object:** `Resolve`
|
|
294
|
-
- **Signature:** `() -> Bool`
|
|
295
|
-
- **Behavior:**
|
|
296
|
-
- **Workaround / current handling:**
|
|
297
|
-
- **
|
|
294
|
+
- **Signature:** `() -> Bool (19.x); (highPriority: bool) -> bool (21.1 typed stub)`
|
|
295
|
+
- **Behavior:** VERSION-SCOPED. On Studio 19.1.3.7 (measured here) the method takes no argument and raises priority; there is no getter and no way to lower it again through the API (no getter in dir()), so it is irreversible for the session. Resolve 21.1 retyped it SetHighPriority(highPriority: bool) -> bool, documented as 'script execution priority to high or normal'. Reported by @legionsound (issue #207) from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 install). One live session, in order: (False) -> True, (True) -> True, () -> False, (1) -> False, ('high') -> False, (False) -> True. So on 21.1 the no-argument call is a NO-OP that reports its own failure, only a real bool is accepted (1 and 'high' are rejected like the missing argument), and the setting is REVERSIBLE: SetHighPriority(False) restores normal. There is still no getter on any build — dir() on the live 21.1 Resolve object shows SetHighPriority as the only priority-related name — so the current state cannot be read back on either generation.
|
|
296
|
+
- **Workaround / current handling:** On 21.1+ pass a real bool; never forward 1 or a string (coerce first — resolve_control set_high_priority and granular set_high_priority do, and refuse values that name no boolean). On older builds call it only when the user asked for a long scripted job on a dedicated machine: it cannot be lowered without restarting Resolve, which is why both tools refuse high=false there. No build offers readback, so report the native return, never a confirmed state.
|
|
297
|
+
- **Reference:** [issue #207](https://github.com/samuelgursky/davinci-resolve-mcp/issues/207)
|
|
298
|
+
- **Tags:** app-control, missing-method, readback, version-gated, reported
|
|
298
299
|
|
|
299
300
|
### TimelineItem.CreateMagicMask (needs operator clicks)
|
|
300
301
|
|
|
@@ -796,6 +797,24 @@ values, or automation-hostile modal prompts.
|
|
|
796
797
|
- **Workaround / current handling:** Any wrapper around ValidateDCTL must pass the native diagnostic through verbatim and must not reflow or rewrite the user's source to dodge it; ship the multi-line identity fixture as the known-good control. A 'no return value' error on a one-line function is this quirk, not a missing return — re-run the validation with the function laid out across lines before believing it. This server's own `dctl validate` is a static, offline check (entry point, brace balance, float suffixes) and does not call ValidateDCTL at all. The separate `dctl validate_native` and granular `validate_dctl_native` now expose the native validator without changing source or diagnostics. Both interfaces were contributor-validated against the three fixtures on Studio 21.1.0.14; see resolve211-dctl-validation.md.
|
|
797
798
|
- **Tags:** dctl, validation, unreliable-return, version-gated, reported
|
|
798
799
|
|
|
800
|
+
### Project.StartRendering
|
|
801
|
+
|
|
802
|
+
- **Object:** `Project`
|
|
803
|
+
- **Signature:** `(jobIds: list[str], isInteractiveMode=False) -> bool (21.1 typed stub)`
|
|
804
|
+
- **Behavior:** STATE-DEPENDENT RETURN. Reported by @legionsound (issue #207) from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 install). In one session, some freshly configured render jobs made StartRendering return False and remain Ready, while an identical job configured later in the same session started and completed. Official direct calls reproduced BOTH outcomes, so a False from one interface and a True from another in that sequence is not an interface difference. The trigger was not isolated; no root cause is established. The same session confirmed the two 21.1 RenderSettings keys the compound whitelist used to drop: UseUniqueFilenames=True gave queued filenames Resolve's '_V1-0001' suffix, and AudioFormat='wav' with LPCM produced complete 14-second stereo 48 kHz 24-bit WAVs whose decoded PCM matched byte for byte across both community interfaces.
|
|
805
|
+
- **Workaround / current handling:** Treat the boolean as a hint in both directions. After start, read render(action='get_job_status') and render(action='verify_output') for the actual output file before reporting a render as running or done; a False with the job still Ready means nothing rendered. Do not auto-retry in a wrapper: the measured later success was a NEW identical job, not a retry of the same one, so a loop would hide the state it depends on. Surface the False and let the caller decide whether to re-queue.
|
|
806
|
+
- **Reference:** [issue #207](https://github.com/samuelgursky/davinci-resolve-mcp/issues/207)
|
|
807
|
+
- **Tags:** render, unreliable-return, state-dependent, version-gated, reported
|
|
808
|
+
|
|
809
|
+
### Project settings presets mutated before the project's first SaveProject
|
|
810
|
+
|
|
811
|
+
- **Object:** `Project`
|
|
812
|
+
- **Signature:** `SaveCurrentProjectSettingsAsNewPreset / UpdateProjectSettingsPreset / SetProjectSettingsPreset / DeleteProjectSettingsPreset (presetName) -> bool (21.1+)`
|
|
813
|
+
- **Behavior:** DESTROYS THE PROJECT ITSELF, NOT A GRADE, AND REPORTS SUCCESS THROUGHOUT. Reported by @legionsound (issue #207) from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 install). On a NEW project that had never been saved: save a project-settings preset (SaveCurrentProjectSettingsAsNewPreset), change the timeline resolution, then update (UpdateProjectSettingsPreset), apply (SetProjectSettingsPreset) and delete (DeleteProjectSettingsPreset) the preset, then SaveProject and close. Every native call returned True. SQLite PRAGMA integrity_check on the project database still reported ok, but its Sm2MultiSysData table had ZERO rows, and LoadProject then failed with 'DaVinci Resolve failed to open project.' Reproduced twice, through community wrappers and through direct native calls. The control — the same sequence with one explicit SaveProject BEFORE any preset mutation — kept its Sm2MultiSysData row and reopened. Which single call in the sequence is responsible was not isolated, and preset Export/Import were not part of it, so do not assume any preset write is safe on an unsaved project. ProjectManager.GetProjectLastModifiedTime cannot serve as a 'has this project been saved' guard: it returned None even for saved projects on the same build.
|
|
814
|
+
- **Workaround / current handling:** Before ANY project-settings-preset write on 21.1, call ProjectManager.SaveProject() on the current project and require it to return True; never mutate presets on a project created in this session that has not been saved. Do not read the True returns, a clean integrity_check, or the absence of an error as evidence the project survived — the only proof is closing it and reloading it. A wrapper must not save silently on the caller's behalf (the first save is the caller's decision), so it should require an explicit assertion that the project has been saved, and refuse without one. If the project already holds work, back it up (export a .drp) before the first preset write.
|
|
815
|
+
- **Reference:** [issue #207](https://github.com/samuelgursky/davinci-resolve-mcp/issues/207)
|
|
816
|
+
- **Tags:** destructive, unrecoverable, project, order-dependent, silent-failure, unreliable-return, version-gated, reported
|
|
817
|
+
|
|
799
818
|
### TimelineItem.ExportLUT
|
|
800
819
|
|
|
801
820
|
- **Object:** `TimelineItem`
|
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.13.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 = "37 compound ·
|
|
1546
|
+
subtitle = "37 compound · 390 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
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.
|
|
94
|
+
VERSION = "4.13.0"
|
|
95
95
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
96
96
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
97
97
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/granular/project.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Project, render, cache, cloud, and project-property tools."""
|
|
2
2
|
|
|
3
3
|
from src.granular.common import * # noqa: F401,F403
|
|
4
|
+
from src.utils.bool_params import recognised_bool
|
|
4
5
|
from src.utils.page_lock import restoring_page
|
|
5
6
|
|
|
6
7
|
resolve = ResolveProxy()
|
|
@@ -1109,6 +1110,11 @@ def get_render_job_list() -> Dict[str, Any]:
|
|
|
1109
1110
|
def start_rendering_jobs(job_ids: Optional[List[str]] = None, is_interactive_mode: bool = False) -> Dict[str, Any]:
|
|
1110
1111
|
"""Start rendering jobs. If no job IDs specified, renders all queued jobs.
|
|
1111
1112
|
|
|
1113
|
+
StartRendering is state-dependent on Resolve 21.1 (api_truth
|
|
1114
|
+
'Project.StartRendering'): a freshly configured job can return False and
|
|
1115
|
+
stay Ready while an identical later job completes. Check job status and the
|
|
1116
|
+
output file afterwards; this tool does not retry.
|
|
1117
|
+
|
|
1112
1118
|
Args:
|
|
1113
1119
|
job_ids: Optional list of job IDs to render. If None, renders all.
|
|
1114
1120
|
is_interactive_mode: If True, enables interactive rendering mode.
|
|
@@ -1224,8 +1230,19 @@ def set_render_settings(settings: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1224
1230
|
ExportAlpha (bool), EncodingProfile (str), MultiPassEncode (bool),
|
|
1225
1231
|
AlphaMode (int), NetworkOptimization (bool), ClipStartFrame (int),
|
|
1226
1232
|
TimelineStartTimecode (str), ReplaceExistingFilesInPlace (bool),
|
|
1227
|
-
ExportSubtitle (bool), SubtitleFormat ("BurnIn", "EmbeddedCaptions", "SeparateFile")
|
|
1233
|
+
ExportSubtitle (bool), SubtitleFormat ("BurnIn", "EmbeddedCaptions", "SeparateFile"),
|
|
1234
|
+
and the Resolve 21.1 typed keys UseUniqueFilenames (bool; "true"/"false"
|
|
1235
|
+
strings are coerced, anything else is refused before the call) and
|
|
1236
|
+
AudioFormat (audio file extension, e.g. "wav"; documented for
|
|
1237
|
+
ExportVideo false). A True return means the settings were taken, not
|
|
1238
|
+
that a render will succeed: verify job status and the output file
|
|
1239
|
+
after starting (see api_truth 'Project.StartRendering').
|
|
1228
1240
|
"""
|
|
1241
|
+
if isinstance(settings, dict) and "UseUniqueFilenames" in settings:
|
|
1242
|
+
coerced = recognised_bool(settings["UseUniqueFilenames"])
|
|
1243
|
+
if coerced is None:
|
|
1244
|
+
return {"error": "UseUniqueFilenames must be a boolean (true/false)"}
|
|
1245
|
+
settings = dict(settings, UseUniqueFilenames=coerced)
|
|
1229
1246
|
resolve = get_resolve()
|
|
1230
1247
|
if resolve is None:
|
|
1231
1248
|
return {"error": "Not connected to DaVinci Resolve"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from src.granular.common import * # noqa: F401,F403
|
|
4
4
|
import os
|
|
5
5
|
from src.utils import typed_api_search
|
|
6
|
+
from src.utils.app_control import set_high_priority as set_resolve_high_priority
|
|
6
7
|
|
|
7
8
|
resolve = ResolveProxy()
|
|
8
9
|
|
|
@@ -316,6 +317,28 @@ def disable_background_tasks_for_current_session() -> Dict[str, Any]:
|
|
|
316
317
|
return {"success": True}
|
|
317
318
|
|
|
318
319
|
|
|
320
|
+
@mcp.tool()
|
|
321
|
+
@granular_destructive_op()
|
|
322
|
+
def set_high_priority(high: bool = True) -> Dict[str, Any]:
|
|
323
|
+
"""Set Resolve's script execution priority to high or normal.
|
|
324
|
+
|
|
325
|
+
Granular counterpart of resolve_control set_high_priority. On Resolve
|
|
326
|
+
21.1+ this calls SetHighPriority(bool) and is reversible (high=False
|
|
327
|
+
restores normal; reported on Studio 21.1.0.14, where the old no-argument
|
|
328
|
+
call is a no-op that returns False). Older builds take no argument and can
|
|
329
|
+
only raise priority for the session, so high=False is refused there. No
|
|
330
|
+
build has a getter, so nothing is read back.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
high: True for high priority (default), False for normal. "true"/"false"
|
|
334
|
+
strings are coerced; anything else is refused before the call.
|
|
335
|
+
"""
|
|
336
|
+
resolve = get_resolve()
|
|
337
|
+
if resolve is None:
|
|
338
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
339
|
+
return set_resolve_high_priority(resolve, high)
|
|
340
|
+
|
|
341
|
+
|
|
319
342
|
@mcp.resource("resolve://app/state")
|
|
320
343
|
def get_app_state_endpoint() -> Dict[str, Any]:
|
|
321
344
|
"""Get DaVinci Resolve application state information."""
|
|
@@ -35,7 +35,7 @@ if __name__ == "__main__":
|
|
|
35
35
|
try:
|
|
36
36
|
connect_at_startup()
|
|
37
37
|
start_background_update_check(VERSION, project_dir, logger)
|
|
38
|
-
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (
|
|
38
|
+
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (390 granular tools)")
|
|
39
39
|
run_fastmcp_stdio(mcp)
|
|
40
40
|
except KeyboardInterrupt:
|
|
41
41
|
logger.info("Server shutdown requested")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "4.
|
|
14
|
+
VERSION = "4.13.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -79,7 +79,11 @@ from src.utils.proc import safe_run
|
|
|
79
79
|
from src.utils.readback import verify_by_readback, verification_stats as _verification_stats
|
|
80
80
|
from src.utils import operation_result as _operation_result
|
|
81
81
|
from src.utils import operation_log as _operation_log
|
|
82
|
-
from src.utils.bool_params import
|
|
82
|
+
from src.utils.bool_params import (
|
|
83
|
+
coerce_bool as _coerce_bool,
|
|
84
|
+
explicit_bool_param as _explicit_bool_param,
|
|
85
|
+
recognised_bool as _recognised_bool,
|
|
86
|
+
)
|
|
83
87
|
from src.utils.confirm_tokens import (
|
|
84
88
|
ConfirmTokenStore as _ConfirmTokenStore,
|
|
85
89
|
gate_required_from as _gate_required_from,
|
|
@@ -17092,7 +17096,13 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17092
17096
|
— one 'Class.Method' or one TypedDict name. No connection needed.
|
|
17093
17097
|
api_surface() -> {methods, option_types, option_fields, objects}
|
|
17094
17098
|
— what the shipped stub contains. No connection needed.
|
|
17095
|
-
set_high_priority() -> {success}
|
|
17099
|
+
set_high_priority(high?) -> {success, high_priority, signature, reversible, readback: null}
|
|
17100
|
+
— script execution priority. high: true (default, the old meaning)
|
|
17101
|
+
or false for normal. Resolve 21.1+ calls SetHighPriority(bool) and
|
|
17102
|
+
is reversible (reported on 21.1.0.14: the old no-argument call is a
|
|
17103
|
+
no-op there). Older builds take no argument and can only RAISE it
|
|
17104
|
+
for the session, so high=false is refused there. No getter exists
|
|
17105
|
+
on any build: nothing is read back.
|
|
17096
17106
|
disable_background_tasks_for_current_session() -> {success} — Resolve 21+
|
|
17097
17107
|
list_user_preferences_presets() -> {presets} — Resolve 21.0.4+
|
|
17098
17108
|
save_user_preferences_preset(name) -> {success} — Resolve 21.0.4+
|
|
@@ -17537,7 +17547,23 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17537
17547
|
return missing
|
|
17538
17548
|
return {"presets": _ser(r.GetFairlightPresets())}
|
|
17539
17549
|
elif action == "set_high_priority":
|
|
17540
|
-
|
|
17550
|
+
from src.utils.app_control import set_high_priority as _set_high_priority
|
|
17551
|
+
# `high` defaults to True so a bare call keeps its old meaning; the
|
|
17552
|
+
# helper picks SetHighPriority(bool) on 21.1+ and the no-arg form below.
|
|
17553
|
+
# The raw value goes to the helper, which coerces it with
|
|
17554
|
+
# recognised_bool and REFUSES a value naming no boolean, rather than
|
|
17555
|
+
# _coerce_bool's fall-back-to-default (that would turn "hgih" into True).
|
|
17556
|
+
_high_key = next((k for k in ("high", "high_priority") if k in p), None)
|
|
17557
|
+
_hp = _set_high_priority(r, p[_high_key] if _high_key else True)
|
|
17558
|
+
if _hp.get("refused"):
|
|
17559
|
+
return _err(
|
|
17560
|
+
_hp["error"],
|
|
17561
|
+
code="HIGH_PRIORITY_REFUSED",
|
|
17562
|
+
category="invalid_input",
|
|
17563
|
+
reason="Nothing was sent to Resolve.SetHighPriority.",
|
|
17564
|
+
state={k: v for k, v in _hp.items() if k not in ("error", "refused", "success")},
|
|
17565
|
+
)
|
|
17566
|
+
return _hp
|
|
17541
17567
|
elif action == "disable_background_tasks_for_current_session":
|
|
17542
17568
|
missing = _requires_method(r, "DisableBackgroundTasksForCurrentResolveSession", "21.0")
|
|
17543
17569
|
if missing:
|
|
@@ -20300,6 +20326,7 @@ _RENDER_SETTING_KEYS = [
|
|
|
20300
20326
|
"MarkOut",
|
|
20301
20327
|
"TargetDir",
|
|
20302
20328
|
"CustomName",
|
|
20329
|
+
"UseUniqueFilenames",
|
|
20303
20330
|
"UniqueFilenameStyle",
|
|
20304
20331
|
"ExportVideo",
|
|
20305
20332
|
"ExportAudio",
|
|
@@ -20308,6 +20335,7 @@ _RENDER_SETTING_KEYS = [
|
|
|
20308
20335
|
"FrameRate",
|
|
20309
20336
|
"PixelAspectRatio",
|
|
20310
20337
|
"VideoQuality",
|
|
20338
|
+
"AudioFormat",
|
|
20311
20339
|
"AudioCodec",
|
|
20312
20340
|
"AudioBitDepth",
|
|
20313
20341
|
"AudioSampleRate",
|
|
@@ -20560,6 +20588,12 @@ def _render_settings_warnings(settings: Dict[str, Any], version_major: Optional[
|
|
|
20560
20588
|
"AddFrameHandles is ignored while UseFullExtents is true: Resolve renders the "
|
|
20561
20589
|
"clip's full extents and the handle count silently does nothing. Drop one of the two."
|
|
20562
20590
|
)
|
|
20591
|
+
if "AudioFormat" in settings and settings.get("ExportVideo") is True:
|
|
20592
|
+
warnings.append(
|
|
20593
|
+
"AudioFormat is documented for audio-only renders (ExportVideo false) in the "
|
|
20594
|
+
"Resolve 21.1 typed stub; with ExportVideo true the container comes from the "
|
|
20595
|
+
"render format instead. Verify the output's container before relying on it."
|
|
20596
|
+
)
|
|
20563
20597
|
if ("ExportSubtitle" in settings or "SubtitleFormat" in settings) and (
|
|
20564
20598
|
version_major is not None and version_major < 21
|
|
20565
20599
|
):
|
|
@@ -20574,11 +20608,40 @@ def _render_settings_warnings(settings: Dict[str, Any], version_major: Optional[
|
|
|
20574
20608
|
return warnings
|
|
20575
20609
|
|
|
20576
20610
|
|
|
20611
|
+
#: RenderSettings keys typed `bool` in the 21.1 stub that are COERCED rather than
|
|
20612
|
+
#: required to be a JSON boolean: an MCP client that sends "false" means false,
|
|
20613
|
+
#: and forwarding the string would hand Resolve a truthy value. Only keys added
|
|
20614
|
+
#: alongside this helper are listed; the older bool keys keep their strict check.
|
|
20615
|
+
_RENDER_COERCED_BOOL_KEYS = ("UseUniqueFilenames",)
|
|
20616
|
+
|
|
20617
|
+
|
|
20618
|
+
def _coerce_render_bool_settings(settings: Any):
|
|
20619
|
+
"""Return ``(settings_copy, errors)`` with the coerced bool keys normalised.
|
|
20620
|
+
|
|
20621
|
+
A value that does not unambiguously name a boolean is an error, never a
|
|
20622
|
+
default: ``UseUniqueFilenames: "maybe"`` must not quietly become False.
|
|
20623
|
+
"""
|
|
20624
|
+
if not isinstance(settings, dict):
|
|
20625
|
+
return settings, []
|
|
20626
|
+
out = dict(settings)
|
|
20627
|
+
errors = []
|
|
20628
|
+
for key in _RENDER_COERCED_BOOL_KEYS:
|
|
20629
|
+
if key not in out:
|
|
20630
|
+
continue
|
|
20631
|
+
coerced = _recognised_bool(out[key])
|
|
20632
|
+
if coerced is None:
|
|
20633
|
+
errors.append(f"{key} must be a boolean (true/false)")
|
|
20634
|
+
else:
|
|
20635
|
+
out[key] = coerced
|
|
20636
|
+
return out, errors
|
|
20637
|
+
|
|
20638
|
+
|
|
20577
20639
|
def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_target: bool = False):
|
|
20578
20640
|
if not isinstance(settings, dict) or not settings:
|
|
20579
20641
|
return None, _err("settings must be a non-empty object")
|
|
20642
|
+
settings, coerce_errors = _coerce_render_bool_settings(settings)
|
|
20580
20643
|
unknown = sorted(key for key in settings if key not in _RENDER_SETTING_KEYS)
|
|
20581
|
-
errors =
|
|
20644
|
+
errors = list(coerce_errors)
|
|
20582
20645
|
target_dir = settings.get("TargetDir")
|
|
20583
20646
|
if target_dir is not None:
|
|
20584
20647
|
if not isinstance(target_dir, str) or not target_dir:
|
|
@@ -20597,6 +20660,8 @@ def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_
|
|
|
20597
20660
|
errors.append(f"{key} must be a boolean")
|
|
20598
20661
|
if "AddFrameHandles" in settings and isinstance(settings["AddFrameHandles"], int) and settings["AddFrameHandles"] < 0:
|
|
20599
20662
|
errors.append("AddFrameHandles must be >= 0")
|
|
20663
|
+
if "AudioFormat" in settings and (not isinstance(settings["AudioFormat"], str) or not settings["AudioFormat"].strip()):
|
|
20664
|
+
errors.append("AudioFormat must be a non-empty string (an audio file extension, e.g. 'wav')")
|
|
20600
20665
|
if "DataBurnIn" in settings and not isinstance(settings["DataBurnIn"], str):
|
|
20601
20666
|
errors.append("DataBurnIn must be a string (a burn-in preset name, 'Same as project', or 'None')")
|
|
20602
20667
|
if "MarkIn" in settings and "MarkOut" in settings and settings["MarkOut"] < settings["MarkIn"]:
|
|
@@ -20641,6 +20706,7 @@ def _safe_set_render_settings(proj, p: Dict[str, Any]):
|
|
|
20641
20706
|
return {"success": False, "validation": validation}
|
|
20642
20707
|
if _coerce_bool(p.get("dry_run")):
|
|
20643
20708
|
return _ok(validation=validation)
|
|
20709
|
+
settings = validation["settings"] # coerced bool keys, see _coerce_render_bool_settings
|
|
20644
20710
|
before = _render_settings_snapshot(proj)
|
|
20645
20711
|
success = bool(proj.SetRenderSettings(settings))
|
|
20646
20712
|
after_settings = _ser(proj.GetRenderSettings()) if _has_method(proj, "GetRenderSettings") else {}
|
|
@@ -20675,6 +20741,7 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
|
|
|
20675
20741
|
return {"success": False, "validation": validation}
|
|
20676
20742
|
if _coerce_bool(p.get("dry_run")):
|
|
20677
20743
|
return _ok(validation=validation, format=p.get("format"), codec=p.get("codec"))
|
|
20744
|
+
settings = validation["settings"] # coerced bool keys, see _coerce_render_bool_settings
|
|
20678
20745
|
before = _render_settings_snapshot(proj)
|
|
20679
20746
|
# Pin the base render state before layering explicit settings on top. Without
|
|
20680
20747
|
# this the job inherits the Deliver page's loaded preset for every key the
|
|
@@ -21096,6 +21163,10 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
21096
21163
|
collapse warning unless the stated expectation matches the mark range.
|
|
21097
21164
|
Verify BEFORE deleting the job; deleted jobs carry no TargetDir.
|
|
21098
21165
|
start(job_ids?, interactive?) -> {success}
|
|
21166
|
+
StartRendering is state-dependent on 21.1 (api_truth
|
|
21167
|
+
'Project.StartRendering'): a freshly configured job can return False
|
|
21168
|
+
and stay Ready while an identical later job completes. Read the job
|
|
21169
|
+
status and the output; this action does not retry.
|
|
21099
21170
|
stop() -> {success}
|
|
21100
21171
|
is_rendering() -> {rendering}
|
|
21101
21172
|
get_formats() -> {formats}
|
|
@@ -21118,6 +21189,14 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
21118
21189
|
burn-in, sidecar, or caption track — so setting them on a pre-21
|
|
21119
21190
|
host comes back in warnings; verify the output before claiming
|
|
21120
21191
|
caption delivery.
|
|
21192
|
+
Resolve 21.1 typed keys: UseUniqueFilenames (bool; "true"/"false"
|
|
21193
|
+
strings are coerced, anything else is refused before the call) and
|
|
21194
|
+
AudioFormat (audio file extension, e.g. "wav" — the stub documents it
|
|
21195
|
+
for ExportVideo false, so pairing it with ExportVideo true warns).
|
|
21196
|
+
Reported on Studio 21.1.0.14: UseUniqueFilenames=true gave queued
|
|
21197
|
+
files a "_V1-0001" suffix, and AudioFormat "wav" + LPCM rendered
|
|
21198
|
+
48 kHz/24-bit WAVs. A True here only means the settings were taken;
|
|
21199
|
+
verify the job (get_job_status / verify_output) after start.
|
|
21121
21200
|
list_presets() -> {presets}
|
|
21122
21201
|
load_preset(name) -> {success}
|
|
21123
21202
|
save_preset(name) -> {success}
|
|
@@ -21427,6 +21506,14 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
21427
21506
|
# Whitelist to documented keys; SetRenderSettings silently ignores unknown
|
|
21428
21507
|
# keys, so a typo'd setting would be dropped with no signal (P1 #6).
|
|
21429
21508
|
settings, ignored_settings = _filter_to_keys(p["settings"], _RENDER_SETTING_KEYS)
|
|
21509
|
+
settings, bool_errors = _coerce_render_bool_settings(settings)
|
|
21510
|
+
if bool_errors:
|
|
21511
|
+
return _err(
|
|
21512
|
+
"; ".join(bool_errors),
|
|
21513
|
+
code="INVALID_RENDER_SETTING",
|
|
21514
|
+
category="invalid_input",
|
|
21515
|
+
reason="Nothing was sent to SetRenderSettings.",
|
|
21516
|
+
)
|
|
21430
21517
|
result = {"success": bool(proj.SetRenderSettings(settings))}
|
|
21431
21518
|
if ignored_settings:
|
|
21432
21519
|
result["ignored_settings"] = ignored_settings
|
package/src/utils/api_truth.py
CHANGED
|
@@ -2955,17 +2955,40 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2955
2955
|
"submit": "missing",
|
|
2956
2956
|
},
|
|
2957
2957
|
{
|
|
2958
|
-
"symbol": "Resolve.SetHighPriority (write-only
|
|
2958
|
+
"symbol": "Resolve.SetHighPriority (write-only; irreversible before 21.1)",
|
|
2959
2959
|
"object": "Resolve",
|
|
2960
|
-
"signature": "() -> Bool",
|
|
2961
|
-
"reality": "
|
|
2962
|
-
"
|
|
2963
|
-
"
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2960
|
+
"signature": "() -> Bool (19.x); (highPriority: bool) -> bool (21.1 typed stub)",
|
|
2961
|
+
"reality": "VERSION-SCOPED. On Studio 19.1.3.7 (measured here) the "
|
|
2962
|
+
"method takes no argument and raises priority; there is no "
|
|
2963
|
+
"getter and no way to lower it again through the API "
|
|
2964
|
+
"(no getter in dir()), so it is irreversible for the session. "
|
|
2965
|
+
"Resolve 21.1 retyped it SetHighPriority(highPriority: bool) "
|
|
2966
|
+
"-> bool, documented as 'script execution priority to high or "
|
|
2967
|
+
"normal'. Reported by @legionsound (issue #207) from Studio "
|
|
2968
|
+
"21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 "
|
|
2969
|
+
"install). One live session, in order: (False) -> True, "
|
|
2970
|
+
"(True) -> True, () -> False, (1) -> False, ('high') -> False, "
|
|
2971
|
+
"(False) -> True. So on 21.1 the no-argument call is a NO-OP "
|
|
2972
|
+
"that reports its own failure, only a real bool is accepted "
|
|
2973
|
+
"(1 and 'high' are rejected like the missing argument), and "
|
|
2974
|
+
"the setting is REVERSIBLE: SetHighPriority(False) restores "
|
|
2975
|
+
"normal. There is still no getter on any build — dir() on the "
|
|
2976
|
+
"live 21.1 Resolve object shows SetHighPriority as the only "
|
|
2977
|
+
"priority-related name — so the current state cannot be read "
|
|
2978
|
+
"back on either generation.",
|
|
2979
|
+
"recommended": "On 21.1+ pass a real bool; never forward 1 or a string "
|
|
2980
|
+
"(coerce first — resolve_control set_high_priority and "
|
|
2981
|
+
"granular set_high_priority do, and refuse values that "
|
|
2982
|
+
"name no boolean). On older builds call it only when the "
|
|
2983
|
+
"user asked for a long scripted job on a dedicated "
|
|
2984
|
+
"machine: it cannot be lowered without restarting "
|
|
2985
|
+
"Resolve, which is why both tools refuse high=false "
|
|
2986
|
+
"there. No build offers readback, so report the native "
|
|
2987
|
+
"return, never a confirmed state.",
|
|
2988
|
+
"tags": ["app-control", "missing-method", "readback", "version-gated", "reported"],
|
|
2968
2989
|
"submit": "missing",
|
|
2990
|
+
"issue": 207,
|
|
2991
|
+
"mitigation": ["resolve_control.set_high_priority", "set_high_priority"],
|
|
2969
2992
|
},
|
|
2970
2993
|
{
|
|
2971
2994
|
"symbol": "Project.IsRenderingInProgress (stuck True after deleting the rendering project)",
|
|
@@ -3471,6 +3494,86 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
3471
3494
|
"tags": ["dctl", "validation", "unreliable-return", "version-gated", "reported"],
|
|
3472
3495
|
"submit": "bug",
|
|
3473
3496
|
},
|
|
3497
|
+
{
|
|
3498
|
+
"symbol": "Project.StartRendering",
|
|
3499
|
+
"object": "Project",
|
|
3500
|
+
"signature": "(jobIds: list[str], isInteractiveMode=False) -> bool (21.1 typed stub)",
|
|
3501
|
+
"reality": "STATE-DEPENDENT RETURN. Reported by @legionsound (issue #207) "
|
|
3502
|
+
"from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced "
|
|
3503
|
+
"here (no 21.1 install). In one session, some freshly "
|
|
3504
|
+
"configured render jobs made StartRendering return False and "
|
|
3505
|
+
"remain Ready, while an identical job configured later in the "
|
|
3506
|
+
"same session started and completed. Official direct calls "
|
|
3507
|
+
"reproduced BOTH outcomes, so a False from one interface and a "
|
|
3508
|
+
"True from another in that sequence is not an interface "
|
|
3509
|
+
"difference. The trigger was not isolated; no root cause is "
|
|
3510
|
+
"established. The same session confirmed the two 21.1 "
|
|
3511
|
+
"RenderSettings keys the compound whitelist used to drop: "
|
|
3512
|
+
"UseUniqueFilenames=True gave queued filenames Resolve's "
|
|
3513
|
+
"'_V1-0001' suffix, and AudioFormat='wav' with LPCM produced "
|
|
3514
|
+
"complete 14-second stereo 48 kHz 24-bit WAVs whose decoded PCM "
|
|
3515
|
+
"matched byte for byte across both community interfaces.",
|
|
3516
|
+
"recommended": "Treat the boolean as a hint in both directions. After "
|
|
3517
|
+
"start, read render(action='get_job_status') and "
|
|
3518
|
+
"render(action='verify_output') for the actual output file "
|
|
3519
|
+
"before reporting a render as running or done; a False "
|
|
3520
|
+
"with the job still Ready means nothing rendered. Do not "
|
|
3521
|
+
"auto-retry in a wrapper: the measured later success was a "
|
|
3522
|
+
"NEW identical job, not a retry of the same one, so a loop "
|
|
3523
|
+
"would hide the state it depends on. Surface the False and "
|
|
3524
|
+
"let the caller decide whether to re-queue.",
|
|
3525
|
+
"tags": ["render", "unreliable-return", "state-dependent", "version-gated", "reported"],
|
|
3526
|
+
"submit": "bug",
|
|
3527
|
+
"issue": 207,
|
|
3528
|
+
"verified_on": "DaVinci Resolve Studio 21.1.0.14",
|
|
3529
|
+
},
|
|
3530
|
+
{
|
|
3531
|
+
"symbol": "Project settings presets mutated before the project's first SaveProject",
|
|
3532
|
+
"object": "Project",
|
|
3533
|
+
"signature": "SaveCurrentProjectSettingsAsNewPreset / UpdateProjectSettingsPreset / "
|
|
3534
|
+
"SetProjectSettingsPreset / DeleteProjectSettingsPreset (presetName) "
|
|
3535
|
+
"-> bool (21.1+)",
|
|
3536
|
+
"reality": "DESTROYS THE PROJECT ITSELF, NOT A GRADE, AND REPORTS SUCCESS "
|
|
3537
|
+
"THROUGHOUT. Reported by @legionsound (issue #207) from Studio "
|
|
3538
|
+
"21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 "
|
|
3539
|
+
"install). On a NEW project that had never been saved: save a "
|
|
3540
|
+
"project-settings preset (SaveCurrentProjectSettingsAsNewPreset), "
|
|
3541
|
+
"change the timeline resolution, then update "
|
|
3542
|
+
"(UpdateProjectSettingsPreset), apply (SetProjectSettingsPreset) "
|
|
3543
|
+
"and delete (DeleteProjectSettingsPreset) the preset, then "
|
|
3544
|
+
"SaveProject and close. Every native call returned True. "
|
|
3545
|
+
"SQLite PRAGMA integrity_check on the project database still "
|
|
3546
|
+
"reported ok, but its Sm2MultiSysData table had ZERO rows, and "
|
|
3547
|
+
"LoadProject then failed with 'DaVinci Resolve failed to open "
|
|
3548
|
+
"project.' Reproduced twice, through community wrappers and "
|
|
3549
|
+
"through direct native calls. The control — the same sequence "
|
|
3550
|
+
"with one explicit SaveProject BEFORE any preset mutation — kept "
|
|
3551
|
+
"its Sm2MultiSysData row and reopened. Which single call in the "
|
|
3552
|
+
"sequence is responsible was not isolated, and preset "
|
|
3553
|
+
"Export/Import were not part of it, so do not assume any preset "
|
|
3554
|
+
"write is safe on an unsaved project. "
|
|
3555
|
+
"ProjectManager.GetProjectLastModifiedTime cannot serve as a "
|
|
3556
|
+
"'has this project been saved' guard: it returned None even for "
|
|
3557
|
+
"saved projects on the same build.",
|
|
3558
|
+
"recommended": "Before ANY project-settings-preset write on 21.1, call "
|
|
3559
|
+
"ProjectManager.SaveProject() on the current project and "
|
|
3560
|
+
"require it to return True; never mutate presets on a "
|
|
3561
|
+
"project created in this session that has not been saved. "
|
|
3562
|
+
"Do not read the True returns, a clean integrity_check, or "
|
|
3563
|
+
"the absence of an error as evidence the project survived "
|
|
3564
|
+
"— the only proof is closing it and reloading it. A "
|
|
3565
|
+
"wrapper must not save silently on the caller's behalf "
|
|
3566
|
+
"(the first save is the caller's decision), so it should "
|
|
3567
|
+
"require an explicit assertion that the project has been "
|
|
3568
|
+
"saved, and refuse without one. If the project already "
|
|
3569
|
+
"holds work, back it up (export a .drp) before the first "
|
|
3570
|
+
"preset write.",
|
|
3571
|
+
"tags": ["destructive", "unrecoverable", "project", "order-dependent",
|
|
3572
|
+
"silent-failure", "unreliable-return", "version-gated", "reported"],
|
|
3573
|
+
"submit": "bug",
|
|
3574
|
+
"issue": 207,
|
|
3575
|
+
"verified_on": "DaVinci Resolve Studio 21.1.0.14",
|
|
3576
|
+
},
|
|
3474
3577
|
# ── Measured on Studio 21.1.0.14, 2026-09-09 (trap-aware execution work) ──
|
|
3475
3578
|
{
|
|
3476
3579
|
"symbol": "TimelineItem.CopyGrades",
|
|
@@ -3714,6 +3817,10 @@ ACTION_SYMBOLS: Dict[Tuple[str, str], List[str]] = {
|
|
|
3714
3817
|
("fusion_comp", "add_modifier"): ["Tool.AddModifier", "Tool.AddModifier (NestControl inputs)"],
|
|
3715
3818
|
("fusion_comp", "add_keyframe"): ["Tool.AddModifier (NestControl inputs)"],
|
|
3716
3819
|
("project_manager", "safe_project_archive"): ["ProjectManager.ArchiveProject"],
|
|
3820
|
+
("render", "start"): ["Project.StartRendering"],
|
|
3821
|
+
("resolve_control", "set_high_priority"): [
|
|
3822
|
+
"Resolve.SetHighPriority (write-only; irreversible before 21.1)",
|
|
3823
|
+
],
|
|
3717
3824
|
}
|
|
3718
3825
|
|
|
3719
3826
|
|
package/src/utils/app_control.py
CHANGED
|
@@ -335,3 +335,95 @@ def open_project_settings(resolve_obj) -> Dict[str, Any]:
|
|
|
335
335
|
def open_preferences(resolve_obj) -> Dict[str, Any]:
|
|
336
336
|
"""Open the Preferences dialog. See `_open_dialog` for the result shape."""
|
|
337
337
|
return _open_dialog(resolve_obj, "OpenPreferences", "Preferences")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ── Script execution priority ───────────────────────────────────────────────
|
|
341
|
+
#
|
|
342
|
+
# Resolve 21.1 retyped `Resolve.SetHighPriority(highPriority: bool) -> bool`
|
|
343
|
+
# ("Sets the script execution priority to high or normal"). Reported by
|
|
344
|
+
# @legionsound on Studio 21.1.0.14 (issue #207): (False) -> True, (True) ->
|
|
345
|
+
# True, and the no-argument call this server used to make -> False, a no-op;
|
|
346
|
+
# (1) and ("high") also -> False. So on 21.1 the argument must be a real bool
|
|
347
|
+
# and the setting is reversible. On 19.1.3.7 the method takes no argument and
|
|
348
|
+
# there is no way to lower it again within the session. No build has a getter.
|
|
349
|
+
|
|
350
|
+
#: First build whose stub types the boolean argument.
|
|
351
|
+
HIGH_PRIORITY_TYPED_FLOOR = "21.1"
|
|
352
|
+
|
|
353
|
+
_NO_READBACK_NOTE = (
|
|
354
|
+
"Resolve exposes no getter for script execution priority on any build, so "
|
|
355
|
+
"the state cannot be read back; success reflects the native return only."
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def set_high_priority(resolve_obj, high: Any = True) -> Dict[str, Any]:
|
|
360
|
+
"""Set script execution priority, choosing the call form by build.
|
|
361
|
+
|
|
362
|
+
`high` is coerced with `recognised_bool` (so "false" means False) and a
|
|
363
|
+
value that names no boolean is refused before any call: Resolve 21.1
|
|
364
|
+
returns a silent False for 1 or "high", so forwarding a guess would turn
|
|
365
|
+
the caller's mistake into a no-op. The native call always receives a real
|
|
366
|
+
bool. Returns a plain dict; a refusal carries `error` and `refused: True`
|
|
367
|
+
and means nothing was called.
|
|
368
|
+
"""
|
|
369
|
+
from src.utils.bool_params import recognised_bool
|
|
370
|
+
from src.utils.resolve_versions import at_least
|
|
371
|
+
|
|
372
|
+
requested = recognised_bool(high)
|
|
373
|
+
if requested is None:
|
|
374
|
+
return {
|
|
375
|
+
"success": False,
|
|
376
|
+
"refused": True,
|
|
377
|
+
"error": "high must be a boolean (true for high priority, false for normal)",
|
|
378
|
+
}
|
|
379
|
+
try:
|
|
380
|
+
version = resolve_obj.GetVersion()
|
|
381
|
+
except Exception: # noqa: BLE001 — an unreadable version routes as unknown
|
|
382
|
+
version = None
|
|
383
|
+
typed = at_least(version, HIGH_PRIORITY_TYPED_FLOOR)
|
|
384
|
+
base: Dict[str, Any] = {
|
|
385
|
+
"high_priority": requested,
|
|
386
|
+
"resolve_version": version,
|
|
387
|
+
"readback": None,
|
|
388
|
+
"note": _NO_READBACK_NOTE,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
def _typed_call() -> Dict[str, Any]:
|
|
392
|
+
ok = bool(resolve_obj.SetHighPriority(requested))
|
|
393
|
+
out = dict(base, success=ok, signature="SetHighPriority(bool)", reversible=True)
|
|
394
|
+
if not ok:
|
|
395
|
+
out["warning"] = (
|
|
396
|
+
"SetHighPriority returned False. On 21.1 that is the answer to a "
|
|
397
|
+
"rejected call; the priority is most likely unchanged, but it "
|
|
398
|
+
"cannot be read back to confirm."
|
|
399
|
+
)
|
|
400
|
+
return out
|
|
401
|
+
|
|
402
|
+
def _legacy_call() -> Dict[str, Any]:
|
|
403
|
+
if not requested:
|
|
404
|
+
return dict(
|
|
405
|
+
base,
|
|
406
|
+
success=False,
|
|
407
|
+
refused=True,
|
|
408
|
+
signature="SetHighPriority()",
|
|
409
|
+
reversible=False,
|
|
410
|
+
error=(
|
|
411
|
+
"This Resolve build takes SetHighPriority() with no argument and "
|
|
412
|
+
"can only RAISE priority; it cannot be lowered back to normal "
|
|
413
|
+
"without restarting Resolve (Resolve 21.1+ accepts a boolean). "
|
|
414
|
+
"Nothing was called."
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
ok = bool(resolve_obj.SetHighPriority())
|
|
418
|
+
return dict(base, success=ok, signature="SetHighPriority()", reversible=False)
|
|
419
|
+
|
|
420
|
+
if typed is True:
|
|
421
|
+
return _typed_call()
|
|
422
|
+
if typed is False:
|
|
423
|
+
return _legacy_call()
|
|
424
|
+
# Version unreadable: try the typed form and fall back only on the
|
|
425
|
+
# signature mismatch an older build raises, never on a False return.
|
|
426
|
+
try:
|
|
427
|
+
return _typed_call()
|
|
428
|
+
except TypeError:
|
|
429
|
+
return _legacy_call()
|
package/src/utils/bool_params.py
CHANGED
|
@@ -31,3 +31,26 @@ def explicit_bool_param(params: Optional[Dict[str, Any]], *keys: str) -> Optiona
|
|
|
31
31
|
if key in params:
|
|
32
32
|
return coerce_bool(params[key])
|
|
33
33
|
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def recognised_bool(value: Any) -> Optional[bool]:
|
|
37
|
+
"""Coerce only values that unambiguously name a boolean, else None.
|
|
38
|
+
|
|
39
|
+
``coerce_bool`` falls back to a default for anything it does not recognise,
|
|
40
|
+
which is right for an optional flag and wrong for a value that is forwarded
|
|
41
|
+
to Resolve as the setting itself: ``"maybe"`` must be refused, not silently
|
|
42
|
+
read as ``False``. Accepts real bools, the true/false strings
|
|
43
|
+
``coerce_bool`` knows, and the integers 0 and 1. Everything else is None,
|
|
44
|
+
so the caller can refuse it with a reason.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(value, bool):
|
|
47
|
+
return value
|
|
48
|
+
if isinstance(value, int) and value in (0, 1):
|
|
49
|
+
return bool(value)
|
|
50
|
+
if isinstance(value, str):
|
|
51
|
+
lowered = value.strip().lower()
|
|
52
|
+
if lowered in _TRUE_STRINGS:
|
|
53
|
+
return True
|
|
54
|
+
if lowered in _FALSE_STRINGS:
|
|
55
|
+
return False
|
|
56
|
+
return None
|
|
@@ -109,7 +109,10 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
109
109
|
"project_settings": frozenset({"delete_color_group"}),
|
|
110
110
|
"render": frozenset({"delete_all_jobs", "delete_job", "delete_preset"}),
|
|
111
111
|
"render_presets": frozenset({"delete_burnin"}),
|
|
112
|
-
|
|
112
|
+
# set_high_priority: an app-wide setting with no getter on any build, and
|
|
113
|
+
# irreversible for the session before 21.1 (issue #207). A write every gate
|
|
114
|
+
# must see, even though it touches no project content.
|
|
115
|
+
"resolve_control": frozenset({"delete_user_preferences_preset", "set_high_priority"}),
|
|
113
116
|
"media_pool": frozenset({
|
|
114
117
|
"delete_clips",
|
|
115
118
|
"delete_folders",
|
|
@@ -282,6 +282,12 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
282
282
|
# wrappers refuse those unless acknowledge_trap. See archive_guard.py.
|
|
283
283
|
("project_manager", "archive"),
|
|
284
284
|
("project_manager", "safe_project_archive"),
|
|
285
|
+
# Script execution priority. App-wide, no getter on any build, and
|
|
286
|
+
# before 21.1 it cannot be lowered again within the session; on 21.1
|
|
287
|
+
# SetHighPriority(False) restores normal (reported, issue #207).
|
|
288
|
+
# MEDIUM: a real write to state nothing can read back, but no content
|
|
289
|
+
# is touched, so safe mode should not block it.
|
|
290
|
+
("resolve_control", "set_high_priority"),
|
|
285
291
|
# 21.0 AI deblur renders NEW media and never touches the source (it is
|
|
286
292
|
# confirm-token gated for that reason). The `remove_` prefix rule rated
|
|
287
293
|
# it HIGH on its name alone, which would make safe mode block a create.
|