davinci-resolve-mcp 2.103.3 → 2.103.5
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 +83 -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 +17 -2
- package/src/server.py +19 -2
- package/src/utils/api_truth.py +40 -0
- package/src/utils/media_analysis.py +37 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,89 @@
|
|
|
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.103.5
|
|
6
|
+
|
|
7
|
+
Two fixes that fell out of auditing the code around this week's releases — the
|
|
8
|
+
same failure classes as #161 and #164, found one tier up from where each was
|
|
9
|
+
originally fixed.
|
|
10
|
+
|
|
11
|
+
**Cache reuse was permanently poisoned on most real installs.** The v2.103.3
|
|
12
|
+
transcription fix taught batch jobs that a declined transcription is not a clip
|
|
13
|
+
failure — but the cache layer had the same default-ON blindness.
|
|
14
|
+
`_report_missing_layers` counted any non-success transcript as a missing layer,
|
|
15
|
+
and the capability gate only screens for a missing backend, not for the stock
|
|
16
|
+
configuration: Whisper installed, `allow_model_download` unset. On such a
|
|
17
|
+
machine every analysis writes a declined "skipped" transcript, every cached
|
|
18
|
+
report carries `missing_layers: ['transcription']`, and `find_reusable_report`
|
|
19
|
+
returns `reusable: False` — forever. Every analyze call silently re-ran full
|
|
20
|
+
analysis (frame extraction included) and produced the same skipped transcript
|
|
21
|
+
again. An unfixable loop, reproduced end-to-end before the fix and green after.
|
|
22
|
+
|
|
23
|
+
A transcript-less report is now a missing layer only when a re-run could supply
|
|
24
|
+
the transcript: the cached payload shows a real attempt that failed (a timeout
|
|
25
|
+
retry may succeed), or the current options would now actually run a backend
|
|
26
|
+
(mock or HTTP backends, or `allow_model_download=true`). Because the check is
|
|
27
|
+
recomputed per request, flipping `allow_model_download` on later correctly
|
|
28
|
+
refuses reuse and finally produces the transcript.
|
|
29
|
+
|
|
30
|
+
**Absolute recordFrames below the timeline start are now refused.** Issue #164
|
|
31
|
+
documented that `recordFrame` counts from Resolve's global frame zero and that
|
|
32
|
+
content placed before the timeline start reads back correctly while rendering
|
|
33
|
+
as ~0 frames. The wrapper's `record_frame_mode='relative'` default shields
|
|
34
|
+
callers — but `record_frame_mode='absolute'` passed any value straight through,
|
|
35
|
+
so an absolute-mode caller with relative-style values reproduced the silent
|
|
36
|
+
stub through this server's own tools. `_normalize_record_frame` (both the
|
|
37
|
+
compound and granular copies) now rejects an absolute value below the
|
|
38
|
+
timeline's start frame with an error naming the convention; internal
|
|
39
|
+
absolute-mode flows (`ripple_insert` cursors) derive their frames from
|
|
40
|
+
timeline reads and cannot trip it. The Resolve UI cannot place content there,
|
|
41
|
+
so no legitimate call is lost.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- A declined transcription (no `allow_model_download` opt-in, unavailable or
|
|
46
|
+
not-implemented backend) no longer marks cached analysis reports
|
|
47
|
+
incomplete, so report reuse works on default installs again. Opting into
|
|
48
|
+
model downloads later invalidates reuse and produces the transcript.
|
|
49
|
+
- `record_frame_mode='absolute'` values below the timeline start frame are
|
|
50
|
+
refused with a remediation instead of silently placing content the render
|
|
51
|
+
engine never visits (#164).
|
|
52
|
+
|
|
53
|
+
## What's New in v2.103.4
|
|
54
|
+
|
|
55
|
+
**A frame-numbering trap, documented where agents will look it up.** Issue #164
|
|
56
|
+
by @jonathandahl-cmyk arrived as a detailed report that `AppendToTimeline`'s
|
|
57
|
+
`trackIndex`/`recordFrame` corrupt a timeline — every readback correct, render
|
|
58
|
+
produces a ~6KB stub. Their own same-day correction found the real cause, and it
|
|
59
|
+
is simpler and nastier: **`recordFrame` is timeline-absolute.** It counts from
|
|
60
|
+
Resolve's global frame zero, so `recordFrame=0` on a default `01:00:00:00`
|
|
61
|
+
timeline places the clip at frame 0 — an hour before the timeline's own start at
|
|
62
|
+
86400. The items genuinely exist and are internally consistent, so
|
|
63
|
+
`AppendToTimeline` returns them, every `Get*` reads back the expected values,
|
|
64
|
+
and the render engine — which only walks the timeline's own start→end range —
|
|
65
|
+
reports `JobStatus: Complete` at 100% while writing a near-empty stub.
|
|
66
|
+
|
|
67
|
+
The assumption was easy to make because Resolve uses both conventions side by
|
|
68
|
+
side: marker `frameId`s *are* timeline-relative (frame 0 == first frame), while
|
|
69
|
+
`recordFrame` and `TimelineItem.GetStart()`/`GetEnd()` are absolute.
|
|
70
|
+
|
|
71
|
+
This server's own callers were never exposed: `media_pool.append_to_timeline`
|
|
72
|
+
has defaulted to `record_frame_mode="relative"` — adding `GetStartFrame()` for
|
|
73
|
+
you — since v2.17.1, which live-validated the exact arithmetic (relative 12 →
|
|
74
|
+
86412; absolute preserved 86484). What was missing was the catalog entry: the
|
|
75
|
+
API-truth table had five `AppendToTimeline` entries (the half-open `endFrame`
|
|
76
|
+
bound, occupied-span null-ids, mixed-fps duration floors…) but never the
|
|
77
|
+
absolute origin, which fails more silently than any of them. It now records the
|
|
78
|
+
convention, the render-lies-too behavior, why the wrapper default exists, and
|
|
79
|
+
that `JobStatus: Complete` is not proof a render produced frames.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- `src/utils/api_truth.py` gains
|
|
84
|
+
`MediaPool.AppendToTimeline clipInfo recordFrame (timeline-absolute origin)`
|
|
85
|
+
(#164). Internal entry — an undocumented convention, not a Resolve defect —
|
|
86
|
+
so the Blackmagic-facing limitations report is unchanged.
|
|
87
|
+
|
|
5
88
|
## What's New in v2.103.3
|
|
6
89
|
|
|
7
90
|
**A batch transcription fix that would have failed every clip.** Issue #160 by
|
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.103.
|
|
15
|
+
> 本翻译对应 v2.103.5 版 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.103.
|
|
40
|
+
VERSION = "2.103.5"
|
|
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.103.
|
|
90
|
+
VERSION = "2.103.5"
|
|
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()}")
|
|
@@ -572,7 +572,22 @@ def _normalize_record_frame(ci, index, timeline_start_frame=None):
|
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
start = _frame_int(timeline_start_frame)
|
|
575
|
-
if
|
|
575
|
+
if mode == "absolute":
|
|
576
|
+
# recordFrame counts from Resolve's global frame zero; a value below
|
|
577
|
+
# the timeline start renders as ~0 frames while every readback agrees
|
|
578
|
+
# (issue #164). No legitimate placement exists there — refuse.
|
|
579
|
+
if start not in (None, 0) and rf < start:
|
|
580
|
+
return None, {
|
|
581
|
+
"error": (
|
|
582
|
+
f"clip_infos[{index}] recordFrame {rf} is before the timeline "
|
|
583
|
+
f"start frame {start}. recordFrame is timeline-absolute, so "
|
|
584
|
+
"content placed there reads back correctly but renders as ~0 "
|
|
585
|
+
"frames. Use record_frame_mode='relative' (default) or pass an "
|
|
586
|
+
f"absolute frame >= {start}."
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
return rf, None
|
|
590
|
+
if start in (None, 0):
|
|
576
591
|
return rf, None
|
|
577
592
|
if mode == "auto":
|
|
578
593
|
return (start + rf) if rf < start else rf, None
|
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.103.
|
|
14
|
+
VERSION = "2.103.5"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -2894,7 +2894,24 @@ def _normalize_record_frame(
|
|
|
2894
2894
|
)
|
|
2895
2895
|
|
|
2896
2896
|
start = _frame_int(timeline_start_frame)
|
|
2897
|
-
if
|
|
2897
|
+
if mode == "absolute":
|
|
2898
|
+
# recordFrame counts from Resolve's global frame zero. An absolute
|
|
2899
|
+
# value below the timeline's own start frame places content the render
|
|
2900
|
+
# engine never visits: every readback agrees, the render reports
|
|
2901
|
+
# Complete, and the output is a near-empty stub (issue #164; see the
|
|
2902
|
+
# api_truth recordFrame timeline-absolute origin entry). The Resolve UI
|
|
2903
|
+
# cannot place content there, so there is no legitimate case — refuse.
|
|
2904
|
+
if start not in (None, 0) and rf < start:
|
|
2905
|
+
return None, _err(
|
|
2906
|
+
f"clip_infos[{index}] recordFrame {rf} is before the timeline start "
|
|
2907
|
+
f"frame {start}. recordFrame is timeline-absolute (counted from frame "
|
|
2908
|
+
"zero, not from the timeline's start), so content placed there reads "
|
|
2909
|
+
"back correctly but renders as ~0 frames. Use the default "
|
|
2910
|
+
"record_frame_mode='relative' with an offset from the timeline start, "
|
|
2911
|
+
f"or pass an absolute frame >= {start}."
|
|
2912
|
+
)
|
|
2913
|
+
return rf, None
|
|
2914
|
+
if start in (None, 0):
|
|
2898
2915
|
return rf, None
|
|
2899
2916
|
if mode == "auto":
|
|
2900
2917
|
return (start + rf) if rf < start else rf, None
|
package/src/utils/api_truth.py
CHANGED
|
@@ -1853,6 +1853,46 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1853
1853
|
"when mirroring keep-ranges into clipInfos.",
|
|
1854
1854
|
"tags": ["timeline", "edit", "off-by-one", "readback"],
|
|
1855
1855
|
},
|
|
1856
|
+
{
|
|
1857
|
+
"symbol": "MediaPool.AppendToTimeline clipInfo recordFrame (timeline-absolute origin)",
|
|
1858
|
+
"object": "MediaPool",
|
|
1859
|
+
"signature": "([{mediaPoolItem, startFrame, endFrame, recordFrame, "
|
|
1860
|
+
"trackIndex, mediaType}]) -> [TimelineItem]",
|
|
1861
|
+
"reality": "clipInfo recordFrame is TIMELINE-ABSOLUTE: it counts from "
|
|
1862
|
+
"Resolve's global frame zero, not from the timeline's own "
|
|
1863
|
+
"GetStartFrame(). A timeline with the default 01:00:00:00 "
|
|
1864
|
+
"start begins at frame 86400, so recordFrame=0 places the "
|
|
1865
|
+
"item about an hour BEFORE the timeline starts. Nothing "
|
|
1866
|
+
"reports the mistake, because the items are real and "
|
|
1867
|
+
"self-consistent: AppendToTimeline returns them, "
|
|
1868
|
+
"GetName/GetSourceStartFrame/GetSourceEndFrame all read back "
|
|
1869
|
+
"the expected values, and the bin count is right. Only a "
|
|
1870
|
+
"render exposes it — the engine walks the timeline's own "
|
|
1871
|
+
"start->end range, so the job reports JobStatus Complete at "
|
|
1872
|
+
"100% in under 2s and writes a ~6KB stub for a 405s "
|
|
1873
|
+
"timeline. The trap is that the two frame conventions sit "
|
|
1874
|
+
"side by side: marker frameIds ARE timeline-relative "
|
|
1875
|
+
"(frame 0 == first frame), while recordFrame and "
|
|
1876
|
+
"TimelineItem.GetStart/GetEnd are absolute. Distinct from "
|
|
1877
|
+
"the null-id entry below, which is a recordFrame landing in "
|
|
1878
|
+
"an OCCUPIED span rather than before the start. Verified on "
|
|
1879
|
+
"Studio 20.3.2.9 (v2.17.1 probe: relative record_frame 12 "
|
|
1880
|
+
"landed at 86400 + 12 = 86412, absolute preserved 86484) and "
|
|
1881
|
+
"independently on Studio 21.0.4.5 (issue #164).",
|
|
1882
|
+
"recommended": "Offset every recordFrame by timeline.GetStartFrame(). "
|
|
1883
|
+
"This server already does it: "
|
|
1884
|
+
"media_pool.append_to_timeline defaults to "
|
|
1885
|
+
"record_frame_mode='relative' and adds the start frame, "
|
|
1886
|
+
"so pass record_frame_mode='absolute' only for raw "
|
|
1887
|
+
"Resolve frame numbers — and since v2.103.5 an absolute "
|
|
1888
|
+
"value below the timeline start is refused outright. "
|
|
1889
|
+
"When driving the API directly, "
|
|
1890
|
+
"never treat JobStatus Complete as proof a render "
|
|
1891
|
+
"worked — check the output file's duration, not just "
|
|
1892
|
+
"that the job finished.",
|
|
1893
|
+
"tags": ["timeline", "edit", "render", "silent-failure", "media-pool"],
|
|
1894
|
+
"issue": 164,
|
|
1895
|
+
},
|
|
1856
1896
|
{
|
|
1857
1897
|
"symbol": "Timeline.DeleteClips (requires the Edit page; flaky first attempt)",
|
|
1858
1898
|
"object": "Timeline",
|
|
@@ -227,6 +227,30 @@ def transcription_attempt_failed(transcript: Any, *, enabled: bool) -> bool:
|
|
|
227
227
|
return status not in TRANSCRIPTION_UNATTEMPTED_STATUSES
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
def transcription_options_would_attempt(transcription: Dict[str, Any]) -> bool:
|
|
231
|
+
"""True when these options would actually run a transcription backend.
|
|
232
|
+
|
|
233
|
+
Mirrors _transcribe's early-outs. The local backends (whisper_cli,
|
|
234
|
+
mlx_whisper — also what a backend of None resolves to) refuse to run
|
|
235
|
+
without allow_model_download=true, whisper_cpp is not_implemented, and
|
|
236
|
+
the resolve backend is refused by design; mock and HTTP-provider
|
|
237
|
+
backends always attempt. Used by _report_missing_layers to decide
|
|
238
|
+
whether re-running an analysis could produce a transcript a cached
|
|
239
|
+
report lacks — if it could not, the cached report is as complete as a
|
|
240
|
+
fresh run would be. Known blind spot: backend None on a machine whose
|
|
241
|
+
only configured backend is an HTTP provider reports False here; name
|
|
242
|
+
the http_* backend explicitly to get cache invalidation.
|
|
243
|
+
"""
|
|
244
|
+
backend = transcription.get("backend")
|
|
245
|
+
if backend in {"mock", "local_mock"}:
|
|
246
|
+
return True
|
|
247
|
+
if isinstance(backend, str) and backend.startswith(HTTP_TRANSCRIPTION_BACKEND_PREFIX):
|
|
248
|
+
return True
|
|
249
|
+
if backend in {"whisper_cpp", "resolve"}:
|
|
250
|
+
return False
|
|
251
|
+
return _coerce_bool(transcription.get("allow_model_download"), default=False)
|
|
252
|
+
|
|
253
|
+
|
|
230
254
|
def _annotate_clip_transcript_failure(clip_result: Dict[str, Any], transcript: Any) -> None:
|
|
231
255
|
"""Mark a clip failed when requested transcription did not complete.
|
|
232
256
|
|
|
@@ -3665,7 +3689,19 @@ def _report_missing_layers(report: Dict[str, Any], depth: str, options: Dict[str
|
|
|
3665
3689
|
if _coerce_bool(transcription.get("enabled"), default=DEFAULT_TRANSCRIPTION_ENABLED):
|
|
3666
3690
|
transcript = report.get("transcription") or {}
|
|
3667
3691
|
if not transcript.get("success") or transcript.get("status") == "skipped":
|
|
3668
|
-
missing
|
|
3692
|
+
# A transcript-less report is a missing layer only when a re-run
|
|
3693
|
+
# could supply the transcript: either the cached payload shows a
|
|
3694
|
+
# real attempt that failed (a retry may fix a timeout), or the
|
|
3695
|
+
# current options would now actually run a backend. Transcription
|
|
3696
|
+
# is enabled by default while allow_model_download is not, so on a
|
|
3697
|
+
# stock install every report carries a declined "skipped" payload —
|
|
3698
|
+
# counting that as missing made every cached report reusable=False
|
|
3699
|
+
# forever, silently defeating cache reuse with full re-analysis
|
|
3700
|
+
# that could never produce the transcript either.
|
|
3701
|
+
if transcription_attempt_failed(
|
|
3702
|
+
transcript, enabled=True
|
|
3703
|
+
) or transcription_options_would_attempt(transcription):
|
|
3704
|
+
missing.append("transcription")
|
|
3669
3705
|
vision = options.get("vision") or {}
|
|
3670
3706
|
if _coerce_bool(vision.get("enabled"), default=False):
|
|
3671
3707
|
visual = report.get("visual") or {}
|