davinci-resolve-mcp 2.212.1 → 2.212.3
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 +63 -0
- package/README.md +2 -2
- package/README.zh-CN.md +3 -3
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/operation_result.py +45 -63
- package/src/utils/update_check.py +26 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,69 @@
|
|
|
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.212.3 — a cached update prompt is re-judged against the version actually running
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A persisted "update available" outlived the upgrade it recommended.** The
|
|
10
|
+
update checker caches its last verdict (in memory and in
|
|
11
|
+
`logs/update-check.json`) and serves it without network for the throttle
|
|
12
|
+
interval. That verdict was stored as a *status*, not as a comparison, so
|
|
13
|
+
after upgrading from the version it was computed for — 2.135.0 → 2.210.0,
|
|
14
|
+
say — a server now running 2.212.1 kept prompting to update to 2.210.0.
|
|
15
|
+
Every cached path (in-memory, persisted, and the throttled branch of
|
|
16
|
+
`check_for_updates`) now re-compares the cached `latest_version` against
|
|
17
|
+
the running version and reclassifies to `update_available` /
|
|
18
|
+
`up_to_date` / `current_ahead`; `error` and `disabled` results are left
|
|
19
|
+
as they were. Contributed in #194 by @diesdaas.
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
- **The READMEs said there was no beat detection; there has been for some
|
|
24
|
+
time.** Both the English and Simplified Chinese "not supported" tables
|
|
25
|
+
claimed "no beat or downbeat detection yet" — twenty lines below an
|
|
26
|
+
optional-extras table listing `pip install librosa` for exactly that. The
|
|
27
|
+
row now describes what actually exists: optional `librosa` beat detection
|
|
28
|
+
and beat / bar / phrase cut-point plans, downbeats inferred from the first
|
|
29
|
+
beat with `beat_offset` for pickups, and cut *points* rather than a finished
|
|
30
|
+
music edit. Speech-silence tools remain the wrong instrument for music.
|
|
31
|
+
|
|
32
|
+
### Validation
|
|
33
|
+
|
|
34
|
+
- Four regression tests from the PR cover the persisted, in-memory and
|
|
35
|
+
throttled cache paths plus the preserved error/disabled states. On landing,
|
|
36
|
+
the update-check tests gained a `setUp` that resets the module-wide cache
|
|
37
|
+
around every test, so a seeded verdict cannot leak into another module's
|
|
38
|
+
cached-status read later in the run. Full offline suite, drift guards and
|
|
39
|
+
the advanced Node suite are green. No Resolve behavior changed; live test
|
|
40
|
+
not required.
|
|
41
|
+
|
|
42
|
+
## What's New in v2.212.2 — failed verification evidence survives the operation envelope
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **A payload that carried its own `verification` block won outright, hiding
|
|
47
|
+
contradicting evidence beside it.** `extract_verification` returned any
|
|
48
|
+
pre-shaped `verification` dict untouched, so a top-level `contradiction:
|
|
49
|
+
true`, a `readback.missing` list, or a failed check inside the block itself
|
|
50
|
+
could sit next to `status: "passed"`. Evidence now merges: the explicit
|
|
51
|
+
status, readback misses, post-state readback and property-restore failures
|
|
52
|
+
are all collected, and precedence runs contradiction > failed > partial >
|
|
53
|
+
passed > unverified. Contributed in #195 by @denoise.
|
|
54
|
+
- **Bulk command counts no longer count as verification.** `succeeded` /
|
|
55
|
+
`failed` tallies record what the server sent, not what Resolve honoured; a
|
|
56
|
+
`succeeded: 3, failed: 0` result used to read `verification.status:
|
|
57
|
+
"passed"` with no readback at all. The counts still drive the envelope's
|
|
58
|
+
own `status` (`partial` when both are non-zero), but the verification block
|
|
59
|
+
stays `unverified` until real evidence — a readback — arrives.
|
|
60
|
+
|
|
61
|
+
### Validation
|
|
62
|
+
|
|
63
|
+
- The PR's regression tests plus two added on landing: bulk counts cannot
|
|
64
|
+
mask a failed readback, and readback evidence is what establishes a pass on
|
|
65
|
+
a bulk result. Full offline suite, the drift guards and the advanced Node
|
|
66
|
+
suite are green. No Resolve behavior changed; live test not required.
|
|
67
|
+
|
|
5
68
|
## What's New in v2.212.1 — the networked transport's generated bearer token no longer lands in server.log
|
|
6
69
|
|
|
7
70
|
### Fixed
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
|
@@ -339,7 +339,7 @@ cheaper to read it here than to discover it mid-project.
|
|
|
339
339
|
| Not supported | Why, and what you get instead |
|
|
340
340
|
|---|---|
|
|
341
341
|
| **Choosing the best take** | Performance is most of what makes a take right, and none of it is measurable from a waveform or a transcript. `rank_takes` ranks *fluency* — fillers, restarts, script coverage — and says so in every response. The take that plays is regularly the least fluent one, because the hesitation is often the acting. Use it to find the clean safety take, not to choose the read. |
|
|
342
|
-
| **
|
|
342
|
+
| **Automatic music editing** | Optional `librosa` support provides beat detection and beat/bar/phrase cut-point plans, not a finished assembly. Downbeats are inferred from the first beat; use `beat_offset` for pickups. Speech-silence tools are unsuitable for finding musical edit points. |
|
|
343
343
|
| **Judging a cut** | Nothing here has an opinion about whether an edit is good. Every destructive action is plan → review → confirm for that reason. |
|
|
344
344
|
| **Replacing an editor** | The output is a first-pass assembly, in the assistant-editor sense: ingest, sync, organize, string out, flag problems. It is a starting point you cut, not a finished cut. Defaults are deliberately **generous** — a first assembly is supposed to run long, because trimming is fast and visible while recovering discarded material is slow and invisible. |
|
|
345
345
|
| **Modifying your source media** | By design and without exception — see below. |
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.212.
|
|
15
|
+
> 本翻译对应 v2.212.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -207,7 +207,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
|
|
|
207
207
|
| 不支持 | 原因,以及你能得到什么 |
|
|
208
208
|
|---|---|
|
|
209
209
|
| **挑选最佳条次** | 表演是决定一条好坏的大头,而这些从波形或转写里都量不出来。`rank_takes` 排的是*流畅度*——填充词、重来、稿子覆盖率——并在每次响应里说明这一点。最终用的那条常常是最不流畅的一条,因为迟疑往往正是表演。用它找干净的保底条,别用它挑读法。 |
|
|
210
|
-
|
|
|
210
|
+
| **自动音乐剪辑** | 可选的 `librosa` 支持节拍检测和按节拍、小节、乐句规划剪切点,但不会生成完整剪辑。强拍从第一个节拍推断;遇到弱起时用 `beat_offset` 调整。语音静音检测工具不适合寻找音乐剪切点。 |
|
|
211
211
|
| **评判剪辑好坏** | 这里没有任何东西对"这一刀剪得好不好"持有观点。正因如此,一切破坏性操作都走 计划 → 审阅 → 确认。 |
|
|
212
212
|
| **取代剪辑师** | 输出是助理剪辑意义上的初剪:摄入、同步、整理、串片、标记问题。它是给你继续剪的起点,不是成片。默认参数刻意**宽松**——初剪本来就该偏长,因为往下修快且看得见,而找回已丢弃的素材慢且看不见。 |
|
|
213
213
|
| **修改你的源媒体** | 设计如此,无例外——见下文。 |
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.212.
|
|
40
|
+
VERSION = "2.212.3"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.212.
|
|
90
|
+
VERSION = "2.212.3"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -166,75 +166,57 @@ def extract_verification(raw: Any) -> Dict[str, Any]:
|
|
|
166
166
|
if not isinstance(raw, dict):
|
|
167
167
|
return unverified
|
|
168
168
|
|
|
169
|
-
# An impl that already speaks this shape wins outright.
|
|
170
169
|
existing = raw.get("verification")
|
|
171
|
-
if isinstance(existing, dict)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
contradiction =
|
|
170
|
+
existing = existing if isinstance(existing, dict) else {}
|
|
171
|
+
checks = list(existing.get("checks") or [])
|
|
172
|
+
statuses = []
|
|
173
|
+
existing_status = existing.get("status")
|
|
174
|
+
if existing_status in {"passed", "failed", "partial", "contradiction"}:
|
|
175
|
+
statuses.append(existing_status)
|
|
176
|
+
elif existing.get("verified") is True:
|
|
177
|
+
statuses.append("passed")
|
|
178
|
+
elif existing.get("verified") is False:
|
|
179
|
+
statuses.append("failed")
|
|
180
|
+
contradiction = existing.get("contradiction") is True or raw.get("contradiction") is True
|
|
182
181
|
|
|
183
182
|
readback = raw.get("readback")
|
|
184
|
-
if isinstance(readback, dict):
|
|
185
|
-
missing = readback
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# verify_by_readback's own shape: a mutation that reported success while the
|
|
195
|
-
# post-state disagrees is a contradiction, this repo's single most valuable
|
|
196
|
-
# reliability signal — it must not be flattened into a plain failure.
|
|
197
|
-
if "verified" in raw:
|
|
198
|
-
verified = bool(raw["verified"])
|
|
199
|
-
contradiction = bool(raw.get("contradiction"))
|
|
200
|
-
checks.append({
|
|
201
|
-
"check": "readback_post_state",
|
|
202
|
-
"passed": verified,
|
|
203
|
-
"contradiction": contradiction,
|
|
204
|
-
"observed": raw.get("observed"),
|
|
205
|
-
})
|
|
206
|
-
status = "contradiction" if contradiction else ("passed" if verified else "failed")
|
|
183
|
+
if isinstance(readback, dict) and isinstance(readback.get("missing"), list):
|
|
184
|
+
missing = readback["missing"]
|
|
185
|
+
checks.append({"check": "readback_verification", "passed": not missing,
|
|
186
|
+
"missing_items": len(missing)})
|
|
187
|
+
statuses.append("failed" if missing else "passed")
|
|
188
|
+
|
|
189
|
+
if type(raw.get("verified")) is bool:
|
|
190
|
+
checks.append({"check": "readback_post_state", "passed": raw["verified"],
|
|
191
|
+
"contradiction": contradiction, "observed": raw.get("observed")})
|
|
192
|
+
statuses.append("passed" if raw["verified"] else "failed")
|
|
207
193
|
|
|
208
194
|
if "property_restore_failures" in raw:
|
|
209
195
|
failures = _as_int(raw.get("property_restore_failures"))
|
|
210
|
-
checks.append({
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if not checks:
|
|
236
|
-
return unverified
|
|
237
|
-
return {"status": status, "checks": checks, "contradiction": contradiction}
|
|
196
|
+
checks.append({"check": "property_restore", "passed": failures == 0,
|
|
197
|
+
"restored_items": _as_int(raw.get("properties_restored_items")),
|
|
198
|
+
"failures": failures})
|
|
199
|
+
if failures:
|
|
200
|
+
statuses.append("partial")
|
|
201
|
+
|
|
202
|
+
for check in checks:
|
|
203
|
+
if isinstance(check, dict):
|
|
204
|
+
contradiction = contradiction or check.get("contradiction") is True
|
|
205
|
+
if check.get("passed") is False:
|
|
206
|
+
statuses.append("partial" if check.get("check") == "property_restore" else "failed")
|
|
207
|
+
# Command counts are not evidence that Resolve honored those commands.
|
|
208
|
+
if contradiction or "contradiction" in statuses:
|
|
209
|
+
status = "contradiction"
|
|
210
|
+
contradiction = True
|
|
211
|
+
elif "failed" in statuses:
|
|
212
|
+
status = "failed"
|
|
213
|
+
elif "partial" in statuses:
|
|
214
|
+
status = "partial"
|
|
215
|
+
elif "passed" in statuses:
|
|
216
|
+
status = "passed"
|
|
217
|
+
else:
|
|
218
|
+
status = "unverified"
|
|
219
|
+
return {**existing, "status": status, "checks": checks, "contradiction": contradiction}
|
|
238
220
|
|
|
239
221
|
|
|
240
222
|
# ─── Changes ─────────────────────────────────────────────────────────────────
|
|
@@ -135,14 +135,13 @@ def get_cached_update_status(
|
|
|
135
135
|
with _cached_lock:
|
|
136
136
|
cached = dict(_cached_status)
|
|
137
137
|
if cached.get("status") != "unknown":
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
cached = _reclassify_cached_status(cached, current_version)
|
|
139
|
+
_set_cached_status(cached)
|
|
140
140
|
return cached
|
|
141
141
|
|
|
142
142
|
state = _read_state(update_state_path(project_dir, env))
|
|
143
143
|
if state:
|
|
144
|
-
|
|
145
|
-
state["current_version"] = current_version
|
|
144
|
+
state = _reclassify_cached_status(state, current_version)
|
|
146
145
|
if "update_mode" not in state:
|
|
147
146
|
state["update_mode"] = get_update_mode(project_dir, env)
|
|
148
147
|
return state
|
|
@@ -193,11 +192,12 @@ def check_for_updates(
|
|
|
193
192
|
and previous
|
|
194
193
|
and checked_at - float(previous.get("checked_at", 0)) < interval_seconds
|
|
195
194
|
):
|
|
196
|
-
cached =
|
|
195
|
+
cached = _reclassify_cached_status(previous, current_version)
|
|
197
196
|
cached["cached"] = True
|
|
198
197
|
cached["update_mode"] = update_mode
|
|
199
198
|
cached["next_check_at"] = float(previous.get("checked_at", 0)) + interval_seconds
|
|
200
199
|
cached["next_check_at_iso"] = _format_timestamp(cached["next_check_at"])
|
|
200
|
+
_write_state(state_path, cached)
|
|
201
201
|
_set_cached_status(cached)
|
|
202
202
|
return cached
|
|
203
203
|
|
|
@@ -621,6 +621,27 @@ def _version_text(value: Any) -> str:
|
|
|
621
621
|
return match.group(0) if match else str(value or "").strip()
|
|
622
622
|
|
|
623
623
|
|
|
624
|
+
def _reclassify_cached_status(
|
|
625
|
+
result: Mapping[str, Any], current_version: Optional[str]
|
|
626
|
+
) -> Dict[str, Any]:
|
|
627
|
+
"""Refresh cached status against the version of the running MCP."""
|
|
628
|
+
refreshed = dict(result)
|
|
629
|
+
if refreshed.get("status") not in _SUCCESS_STATUSES:
|
|
630
|
+
return refreshed
|
|
631
|
+
if current_version:
|
|
632
|
+
refreshed["current_version"] = current_version
|
|
633
|
+
comparison = compare_versions(current_version, refreshed.get("latest_version"))
|
|
634
|
+
if comparison is None:
|
|
635
|
+
return refreshed
|
|
636
|
+
if comparison < 0:
|
|
637
|
+
refreshed["status"] = "update_available"
|
|
638
|
+
elif comparison > 0:
|
|
639
|
+
refreshed["status"] = "current_ahead"
|
|
640
|
+
else:
|
|
641
|
+
refreshed["status"] = "up_to_date"
|
|
642
|
+
return refreshed
|
|
643
|
+
|
|
644
|
+
|
|
624
645
|
def _read_state(path: Path) -> Dict[str, Any]:
|
|
625
646
|
try:
|
|
626
647
|
with path.open("r", encoding="utf-8") as handle:
|