davinci-resolve-mcp 2.104.0 → 2.104.1
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 +30 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +90 -12
- package/src/utils/api_truth.py +17 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
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.104.1
|
|
6
|
+
|
|
7
|
+
**The job metadata lies too — verify_output now cross-checks the timeline.**
|
|
8
|
+
Finishing v2.104.0's pending live validation exposed a hole in the new
|
|
9
|
+
`render.verify_output`: in the issue #164 case (content before the timeline
|
|
10
|
+
start) Resolve rewrites the render job's own MarkIn/MarkOut down to the
|
|
11
|
+
collapsed extent. Measured live on Studio 19.1.3.7: 96 frames of content
|
|
12
|
+
placed before the start turned an explicit 96-frame mark range into a 1-frame
|
|
13
|
+
job — Complete at 100%, a 1-frame black stub, and a clean duration ratio,
|
|
14
|
+
because the expected duration was computed from the job's own lying range.
|
|
15
|
+
|
|
16
|
+
The only truthful readback in that state is the timeline items themselves,
|
|
17
|
+
which report their real positions. `verify_output` now finds the job's
|
|
18
|
+
timeline and cross-checks it: video items starting before the timeline's
|
|
19
|
+
start frame warn (the #164 signature by direct evidence), and a mark range
|
|
20
|
+
under half the items' extent warns (the collapse signature). Callers can also
|
|
21
|
+
pass `expected_frames` or `expected_duration_seconds` outright. Live-verified
|
|
22
|
+
both ways on 19.1.3.7: the healthy render verifies clean, the stub now fails
|
|
23
|
+
with both warnings.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `render.verify_output` no longer trusts the job's MarkIn/MarkOut as the
|
|
28
|
+
expected duration — the #164 stub previously verified clean at ratio 1.0.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- The api_truth recordFrame entry now documents the mark-range rewrite and
|
|
33
|
+
the item-readback discriminator; the Blackmagic-facing report regenerated.
|
|
34
|
+
|
|
5
35
|
## What's New in v2.104.0
|
|
6
36
|
|
|
7
37
|
The read/write symmetry audit's worklist, worked. PR #162's AST rewrite left
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.104.
|
|
15
|
+
> 本翻译对应 v2.104.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.104.
|
|
40
|
+
VERSION = "2.104.1"
|
|
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.104.
|
|
90
|
+
VERSION = "2.104.1"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.104.
|
|
14
|
+
VERSION = "2.104.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -18580,11 +18580,16 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
18580
18580
|
delete_all_jobs() -> {success}
|
|
18581
18581
|
list_jobs() -> {jobs}
|
|
18582
18582
|
get_job_status(job_id) -> {status}
|
|
18583
|
-
verify_output(job_id) -> {status, output_path, output_exists, output_size_bytes, output_probe, expected_duration_seconds, duration_ratio, warnings, verified}
|
|
18583
|
+
verify_output(job_id, expected_frames?, expected_duration_seconds?) -> {status, output_path, output_exists, output_size_bytes, output_probe, mark_range_frames, timeline_item_extent_frames, expected_duration_seconds, duration_ratio, warnings, verified}
|
|
18584
18584
|
JobStatus reports Complete even when the render produced a near-empty
|
|
18585
|
-
stub (content before the timeline start, issue #164)
|
|
18586
|
-
|
|
18587
|
-
the job
|
|
18585
|
+
stub (content before the timeline start, issue #164), and Resolve
|
|
18586
|
+
rewrites the job's own MarkIn/MarkOut down to the collapsed extent, so
|
|
18587
|
+
the job metadata lies too. This checks the actual output file, and
|
|
18588
|
+
cross-checks the job's timeline items (which read back their real
|
|
18589
|
+
positions): items before the timeline start and a mark range far
|
|
18590
|
+
smaller than the item extent both warn. Pass expected_frames or
|
|
18591
|
+
expected_duration_seconds when you know what the render should hold.
|
|
18592
|
+
Verify BEFORE deleting the job; deleted jobs carry no TargetDir.
|
|
18588
18593
|
start(job_ids?, interactive?) -> {success}
|
|
18589
18594
|
stop() -> {success}
|
|
18590
18595
|
is_rendering() -> {rendering}
|
|
@@ -18675,6 +18680,71 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
18675
18680
|
else:
|
|
18676
18681
|
warnings.append("Job carries no TargetDir/OutputFilename to verify against.")
|
|
18677
18682
|
result["output_path"] = output_path
|
|
18683
|
+
# The job's own MarkIn/MarkOut cannot be the only truth source: when
|
|
18684
|
+
# timeline content sits before the timeline's start frame (issue #164)
|
|
18685
|
+
# Resolve rewrites the job's mark range down to the collapsed extent,
|
|
18686
|
+
# so the job metadata lies right along with JobStatus (measured live on
|
|
18687
|
+
# Studio 19.1.3.7: 96 frames of content before start -> job range of 1
|
|
18688
|
+
# frame, Complete at 100%, a 1-frame black stub). Cross-check the
|
|
18689
|
+
# job's timeline items, which DO read back their real positions, and
|
|
18690
|
+
# let the caller override expected duration outright.
|
|
18691
|
+
try:
|
|
18692
|
+
fps = float(str(job.get("FrameRate")))
|
|
18693
|
+
except (TypeError, ValueError):
|
|
18694
|
+
fps = None
|
|
18695
|
+
try:
|
|
18696
|
+
mark_frames = int(job.get("MarkOut")) - int(job.get("MarkIn")) + 1
|
|
18697
|
+
except (TypeError, ValueError):
|
|
18698
|
+
mark_frames = None
|
|
18699
|
+
result["mark_range_frames"] = mark_frames
|
|
18700
|
+
tl_for_job = None
|
|
18701
|
+
try:
|
|
18702
|
+
for i in range(1, int(proj.GetTimelineCount() or 0) + 1):
|
|
18703
|
+
cand = proj.GetTimelineByIndex(i)
|
|
18704
|
+
if cand and cand.GetName() == job.get("TimelineName"):
|
|
18705
|
+
tl_for_job = cand
|
|
18706
|
+
break
|
|
18707
|
+
except Exception:
|
|
18708
|
+
tl_for_job = None
|
|
18709
|
+
if tl_for_job is not None:
|
|
18710
|
+
try:
|
|
18711
|
+
tl_start = int(tl_for_job.GetStartFrame())
|
|
18712
|
+
extent_lo, extent_hi, before_start = None, None, 0
|
|
18713
|
+
for ti in range(1, int(tl_for_job.GetTrackCount("video") or 0) + 1):
|
|
18714
|
+
for it in tl_for_job.GetItemListInTrack("video", ti) or []:
|
|
18715
|
+
it_start, it_end = int(it.GetStart()), int(it.GetEnd())
|
|
18716
|
+
extent_lo = it_start if extent_lo is None else min(extent_lo, it_start)
|
|
18717
|
+
extent_hi = it_end if extent_hi is None else max(extent_hi, it_end)
|
|
18718
|
+
if it_start < tl_start:
|
|
18719
|
+
before_start += 1
|
|
18720
|
+
if before_start:
|
|
18721
|
+
warnings.append(
|
|
18722
|
+
f"{before_start} video item(s) on timeline "
|
|
18723
|
+
f"{job.get('TimelineName')!r} start before the timeline's "
|
|
18724
|
+
f"start frame {tl_start} — the render engine never visits "
|
|
18725
|
+
"that content (recordFrame counts from absolute frame "
|
|
18726
|
+
"zero, issue #164). The items read back as placed; the "
|
|
18727
|
+
"render is a stub."
|
|
18728
|
+
)
|
|
18729
|
+
if (extent_lo is not None and mark_frames is not None
|
|
18730
|
+
and mark_frames < (extent_hi - extent_lo) * 0.5):
|
|
18731
|
+
warnings.append(
|
|
18732
|
+
f"The job's mark range ({mark_frames} frame(s)) is under "
|
|
18733
|
+
f"half the timeline's item extent "
|
|
18734
|
+
f"({extent_hi - extent_lo} frames) — Resolve rewrote the "
|
|
18735
|
+
"range down to a collapsed extent, which is how a stub "
|
|
18736
|
+
"render reports a clean duration ratio."
|
|
18737
|
+
)
|
|
18738
|
+
result["timeline_item_extent_frames"] = (
|
|
18739
|
+
None if extent_lo is None else extent_hi - extent_lo
|
|
18740
|
+
)
|
|
18741
|
+
except Exception:
|
|
18742
|
+
pass
|
|
18743
|
+
else:
|
|
18744
|
+
warnings.append(
|
|
18745
|
+
f"Timeline {job.get('TimelineName')!r} was not found in the "
|
|
18746
|
+
"project, so item-extent cross-checks were skipped."
|
|
18747
|
+
)
|
|
18678
18748
|
if output_path:
|
|
18679
18749
|
exists = os.path.exists(output_path)
|
|
18680
18750
|
result["output_exists"] = exists
|
|
@@ -18684,13 +18754,21 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
18684
18754
|
if probe:
|
|
18685
18755
|
result["output_probe"] = probe
|
|
18686
18756
|
duration = probe.get("duration_seconds")
|
|
18687
|
-
# Expected duration
|
|
18688
|
-
|
|
18689
|
-
|
|
18690
|
-
|
|
18691
|
-
|
|
18692
|
-
|
|
18693
|
-
|
|
18757
|
+
# Expected duration: caller-supplied truth wins; the job's
|
|
18758
|
+
# mark range is the fallback (see collapse caveat above).
|
|
18759
|
+
expected = None
|
|
18760
|
+
if p.get("expected_duration_seconds") is not None:
|
|
18761
|
+
try:
|
|
18762
|
+
expected = float(p["expected_duration_seconds"])
|
|
18763
|
+
except (TypeError, ValueError):
|
|
18764
|
+
expected = None
|
|
18765
|
+
elif p.get("expected_frames") is not None and fps:
|
|
18766
|
+
try:
|
|
18767
|
+
expected = int(p["expected_frames"]) / fps
|
|
18768
|
+
except (TypeError, ValueError):
|
|
18769
|
+
expected = None
|
|
18770
|
+
elif mark_frames is not None and fps:
|
|
18771
|
+
expected = mark_frames / fps
|
|
18694
18772
|
result["expected_duration_seconds"] = expected
|
|
18695
18773
|
if duration is not None and expected and expected > 0:
|
|
18696
18774
|
result["duration_ratio"] = round(duration / expected, 4)
|
package/src/utils/api_truth.py
CHANGED
|
@@ -1875,10 +1875,20 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1875
1875
|
"(frame 0 == first frame), while recordFrame and "
|
|
1876
1876
|
"TimelineItem.GetStart/GetEnd are absolute. Distinct from "
|
|
1877
1877
|
"the null-id entry below, which is a recordFrame landing in "
|
|
1878
|
-
"an OCCUPIED span rather than before the start.
|
|
1878
|
+
"an OCCUPIED span rather than before the start. Worse, the "
|
|
1879
|
+
"render JOB's own metadata lies too: Resolve rewrites the "
|
|
1880
|
+
"job's MarkIn/MarkOut down to the collapsed extent (measured "
|
|
1881
|
+
"live on 19.1.3.7: 96 frames of content before start -> an "
|
|
1882
|
+
"explicit 96-frame mark range became a 1-frame job, Complete "
|
|
1883
|
+
"at 100%, a 1-frame black stub), so output-duration vs "
|
|
1884
|
+
"job-mark-range checks read clean. The only truthful "
|
|
1885
|
+
"readback is the timeline items themselves, which report "
|
|
1886
|
+
"their real GetStart/GetEnd — before the start frame. "
|
|
1887
|
+
"Verified on "
|
|
1879
1888
|
"Studio 20.3.2.9 (v2.17.1 probe: relative record_frame 12 "
|
|
1880
|
-
"landed at 86400 + 12 = 86412, absolute preserved 86484)
|
|
1881
|
-
"independently on Studio 21.0.4.5 (issue #164)
|
|
1889
|
+
"landed at 86400 + 12 = 86412, absolute preserved 86484), "
|
|
1890
|
+
"independently on Studio 21.0.4.5 (issue #164), and the "
|
|
1891
|
+
"mark-range rewrite on Studio 19.1.3.7 (2026-08-29).",
|
|
1882
1892
|
"recommended": "Offset every recordFrame by timeline.GetStartFrame(). "
|
|
1883
1893
|
"This server already does it: "
|
|
1884
1894
|
"media_pool.append_to_timeline defaults to "
|
|
@@ -1888,8 +1898,10 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1888
1898
|
"value below the timeline start is refused outright. "
|
|
1889
1899
|
"When driving the API directly, "
|
|
1890
1900
|
"never treat JobStatus Complete as proof a render "
|
|
1891
|
-
"worked —
|
|
1892
|
-
"
|
|
1901
|
+
"worked — and do not trust the job's MarkIn/MarkOut as "
|
|
1902
|
+
"the expected duration either. render.verify_output "
|
|
1903
|
+
"(v2.104.1) cross-checks the job against its timeline's "
|
|
1904
|
+
"item extents and flags items before the start frame.",
|
|
1893
1905
|
"tags": ["timeline", "edit", "render", "silent-failure", "media-pool"],
|
|
1894
1906
|
"issue": 164,
|
|
1895
1907
|
},
|