davinci-resolve-mcp 2.213.1 → 2.213.2

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 CHANGED
@@ -2,6 +2,56 @@
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.213.2 — the full transcript on 21.1, and a frame capture that puts the user's mark range back in the right frame space
6
+
7
+ Both contributed by @billcarroll (#199, #200).
8
+
9
+ ### Added
10
+
11
+ - **`media_pool_item get_transcription` reads the whole transcript on Resolve
12
+ 21.1+** through `MediaPoolItem.GetTranscription`, which 21.1 added and which
13
+ does not truncate: `segments` carries `{start, end, text, speaker}` in
14
+ **source** timecode, `language` is reported, and `truncated` is False. Pass
15
+ `include_words` to keep each segment's per-word timings, which are several
16
+ times the bulk of the text. On 21.0.x it falls back to the `Transcription`
17
+ clip property exactly as before, and `source` says which route ran. The
18
+ method is registered in the version ledger as a reported 21.1 surface, so
19
+ `check_version_support` answers for it. (#199)
20
+
21
+ ### Fixed
22
+
23
+ - **`timeline_frame capture` flattened a user's mark range to the whole
24
+ timeline.** Rendering one frame pins the project's render range to that
25
+ frame, and the cleanup could only reset it to the whole timeline because
26
+ there is no `GetRenderSettings` to read the previous range from. The mark
27
+ range is the exception: `Timeline.GetMarkInOut` can be read before the
28
+ capture, and the user's own range now goes back afterwards. A half-set range
29
+ (in point only) is still treated as no range, and with no marks set the old
30
+ whole-timeline fallback applies. (#200)
31
+ - **Adapted on landing: the two calls do not share a frame space.** Resolve
32
+ documents `GetMarkInOut` relative to the timeline start (its own example is
33
+ `in: 0, out: 134`), while `SetRenderSettings` takes absolute record frames —
34
+ measured on Studio 19.1.3.7: on an 86400-start timeline `MarkIn=MarkOut=86420`
35
+ rendered frame 20 and `MarkIn=MarkOut=20` was silently clamped to the start
36
+ and rendered frame 0, one frame, no error. Handed back verbatim, a UI-set
37
+ range would have been "restored" as a clamped range with every readback
38
+ agreeing. A mark below the timeline start is now offset by the start frame;
39
+ one at or above it was written absolute (`SetMarkInOut` stores whatever it
40
+ is given) and is kept. A unit test covers the relative case alongside the
41
+ PR's absolute, half-set and unreadable cases.
42
+ - **The clamp is now in the API ledger** as a measured bug, with the
43
+ relative-vs-absolute trap and the remedy, and
44
+ `docs/reference/api-limitations.md` is regenerated.
45
+
46
+ ### Validation
47
+
48
+ - Both PRs' unit tests plus the relative-range test. Mark-range frame space
49
+ measured live on Studio 19.1.3.7 by rendering single frames under both
50
+ interpretations and matching each against the source frames. The 21.1
51
+ transcript route cannot be exercised here (no 21.1 build); its ledger entry
52
+ and `api_truth` say so. Full offline Python suite, drift guards and the
53
+ advanced Node suite green.
54
+
5
55
  ## What's New in v2.213.1 — Fusion keyframes reach the render; the contact sheet waits for the viewer; the ledger learns Resolve 21.1
6
56
 
7
57
  Reported in #196 by @JosephConroy93, with a repro precise enough to reproduce
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.213.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.213.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.213.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.213.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.213.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.213.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/docs/SKILL.md CHANGED
@@ -909,8 +909,12 @@ Key actions: `get_name`, `get_metadata(key?)`, `set_metadata(key, value)`,
909
909
  `set_name(name)`, `link_full_resolution_media(path)`,
910
910
  `replace_clip_preserve_sub_clip(path)`, `monitor_growing_file`,
911
911
  `transcribe_audio(use_speaker_detection?)`, `clear_transcription`,
912
- `get_transcription` (read back `{text, truncated, status, has_transcription}`;
913
- `truncated` flags when Resolve's preview cut the text off),
912
+ `get_transcription(include_words?, use_nested_clip_transcription?)` (read back
913
+ `{text, segments, language, source, truncated, status, has_transcription}`; on
914
+ Resolve 21.1+ it uses `MediaPoolItem.GetTranscription`, so `segments` carries
915
+ `{start, end, text, speaker}` in SOURCE timecode and nothing is truncated, and
916
+ on 21.0.x it falls back to the `Transcription` clip property, where `truncated`
917
+ flags a cut-off preview — `source` says which route ran),
914
918
  `perform_audio_classification`,
915
919
  `analyze_for_intellisearch(identify_faces?, is_better_mode?)`, `analyze_for_slate(marker_color?)`,
916
920
  `remove_motion_blur(deblur_option?)` (Resolve 21+; AI Extras / confirm-token gated as noted above),
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 41 missing capabilities, 50 bugs / unreliable behaviors.
15
+ **Totals:** 41 missing capabilities, 51 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
@@ -541,6 +541,13 @@ values, or automation-hostile modal prompts.
541
541
  - **Workaround / current handling:** Detect the edition BEFORE calling Studio-gated features rather than discovering the gate by tripping it: the product name is 'DaVinci Resolve' on free and 'DaVinci Resolve Studio' on Studio (resolve_control get_version reports it). If a Studio-only call has already returned False on a free build, treat every following failure as suspect: re-run a known-good read, and if that fails too, a modal is blocking and only a human can dismiss it — no API closes it. Known Studio-gated so far: subtitle generation from audio, and audio transcription.
542
542
  - **Tags:** free-edition, studio-only, silent-failure, modal, ai, subtitle, transcription
543
543
 
544
+ ### SetRenderSettings MarkIn/MarkOut below the timeline start are clamped, not refused
545
+
546
+ - **Object:** `Project / Timeline`
547
+ - **Behavior:** SetRenderSettings takes MarkIn/MarkOut as ABSOLUTE record frames, and a value below the timeline's start frame is silently clamped to the start: measured on Studio 19.1.3.7 (2026-09-08) on an 86400-start timeline, MarkIn=MarkOut=86420 rendered timeline frame 20 and MarkIn=MarkOut=20 rendered frame 0 — one frame, True from SetRenderSettings, no error anywhere. The trap is that Timeline.GetMarkInOut reports the user's marks RELATIVE to the timeline start (Blackmagic's own README example is {'in': 0, 'out': 134}; the 21.1 stub says 'record frame relative to timeline start'), so feeding its output straight into SetRenderSettings renders the wrong range with every readback agreeing. SetMarkInOut itself stores whatever number it is given (10 reads back 10, 86410 reads back 86410), so a script-written range can be in either space.
548
+ - **Workaround / current handling:** Offset GetMarkInOut values by Timeline.GetStartFrame() before passing them to SetRenderSettings when they fall below the start frame (timeline_frame capture does this when it puts a user's range back). Verify a render range from the delivered frames, never from the settings call's return.
549
+ - **Tags:** render, silent-failure, frame-space, mark-range
550
+
544
551
  ### SetRenderSettings ExportSubtitle / SubtitleFormat had no observable effect
545
552
 
546
553
  - **Object:** `Project (render settings)`
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.213.1"
40
+ VERSION = "2.213.2"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.213.1",
3
+ "version": "2.213.2",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.213.1"
90
+ VERSION = "2.213.2"
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.213.1"
14
+ VERSION = "2.213.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -14934,6 +14934,40 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
14934
14934
  original_tc = tl.GetCurrentTimecode()
14935
14935
  except Exception:
14936
14936
  pass
14937
+ # The capture pins the render range to the captured frame, and there is no
14938
+ # GetRenderSettings to read the surrounding settings back from (still absent
14939
+ # in 21.1). The mark range is the exception: Timeline.GetMarkInOut reports it
14940
+ # in the same record-frame space SetRenderSettings takes, so a range the user
14941
+ # set can be captured here and put back below instead of being flattened to
14942
+ # the whole timeline.
14943
+ original_marks = None
14944
+ try:
14945
+ marks = (tl.GetMarkInOut() or {}).get("video") or {}
14946
+ if "in" in marks and "out" in marks:
14947
+ original_marks = marks
14948
+ except Exception:
14949
+ pass
14950
+ # GetMarkInOut reports marks RELATIVE to the timeline start (Blackmagic's
14951
+ # own example is {'in': 0, 'out': 134}; the 21.1 stub says "record frame
14952
+ # relative to timeline start"), while SetRenderSettings MarkIn/MarkOut are
14953
+ # ABSOLUTE record frames — measured on Studio 19.1.3.7: on an 86400-start
14954
+ # timeline MarkIn=MarkOut=86420 rendered frame 20, and MarkIn=MarkOut=20 was
14955
+ # silently clamped to the start and rendered frame 0, one frame, no error.
14956
+ # Handing a relative range straight back would "restore" a clamped range
14957
+ # with no signal. A mark below the timeline start is therefore relative and
14958
+ # is offset; one at or above it was written absolute (SetMarkInOut stores
14959
+ # whatever it is given) and is kept as-is.
14960
+ if original_marks:
14961
+ try:
14962
+ tl_start = int(round(float(tl.GetStartFrame())))
14963
+ original_marks = {
14964
+ key: (int(original_marks[key]) + tl_start
14965
+ if int(original_marks[key]) < tl_start
14966
+ else int(original_marks[key]))
14967
+ for key in ("in", "out")
14968
+ }
14969
+ except Exception:
14970
+ original_marks = None
14937
14971
 
14938
14972
  job = None
14939
14973
  try:
@@ -15027,16 +15061,28 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
15027
15061
  original_fc.get("format"), original_fc.get("codec"),
15028
15062
  restore_exc or "SetCurrentRenderFormatAndCodec returned False",
15029
15063
  )
15030
- # Best-effort, not a restore: without GetRenderSettings there is nothing
15031
- # to restore FROM, so put the mark range back to the whole timeline
15032
- # rather than leaving it pinned to the captured frame.
15064
+ # Still not a full restore GetRenderSettings does not exist, so the
15065
+ # other settings cannot be read back. The mark range can: put the user's
15066
+ # own range back when they had one, and fall back to the whole timeline
15067
+ # when they did not, so the range is never left pinned to the captured
15068
+ # frame for the next render job to inherit.
15069
+ # (original_marks is already in SetRenderSettings' absolute space — see
15070
+ # the offset above.)
15033
15071
  try:
15034
- proj.SetRenderSettings({
15035
- "SelectAllFrames": True,
15036
- "MarkIn": tl.GetStartFrame(),
15037
- "MarkOut": tl.GetEndFrame(),
15038
- "CustomName": "",
15039
- })
15072
+ if original_marks:
15073
+ restored_marks = {
15074
+ "SelectAllFrames": False,
15075
+ "MarkIn": original_marks["in"],
15076
+ "MarkOut": original_marks["out"],
15077
+ }
15078
+ else:
15079
+ restored_marks = {
15080
+ "SelectAllFrames": True,
15081
+ "MarkIn": tl.GetStartFrame(),
15082
+ "MarkOut": tl.GetEndFrame(),
15083
+ }
15084
+ restored_marks["CustomName"] = ""
15085
+ proj.SetRenderSettings(restored_marks)
15040
15086
  except Exception:
15041
15087
  pass
15042
15088
  try:
@@ -21312,11 +21358,15 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
21312
21358
  get_unique_id(clip_id) -> {id}
21313
21359
  transcribe_audio(clip_id, use_speaker_detection?, background?) -> {success | job_id} — use_speaker_detection is Resolve 21+; background=true returns a job_id (poll resolve_control job_status)
21314
21360
  clear_transcription(clip_id) -> {success}
21315
- get_transcription(clip_id) -> {text, truncated, status, has_transcription}
21316
- Read a clip's transcription. `truncated` flags when Resolve's preview
21317
- property cut the text off (the full transcript is longer). Clip-level and
21318
- separate from the timeline subtitle transcript (timeline.get_transcript)
21319
- that propose_cuts uses.
21361
+ get_transcription(clip_id, include_words?, use_nested_clip_transcription?) -> {text, segments, language, source, truncated, status, has_transcription}
21362
+ Read a clip's transcription. On Resolve 21.1+ this uses
21363
+ MediaPoolItem.GetTranscription, which does not truncate: `segments`
21364
+ carries {start, end, text, speaker} in SOURCE timecode and `truncated`
21365
+ is False. Pass include_words=true to keep each segment's per-word
21366
+ timings. On 21.0.x it falls back to the `Transcription` clip property,
21367
+ `segments` is null, and `truncated` flags a cut-off preview; `source`
21368
+ says which route ran. Clip-level and separate from the timeline subtitle
21369
+ transcript (timeline.get_transcript) that propose_cuts uses.
21320
21370
  extract_frames(clip_id, timestamps, output_dir?) -> {frame_paths, output_dir, count, errors}
21321
21371
  Extract still JPEGs from the clip's source at the given timestamps (seconds)
21322
21372
  via ffmpeg. Source-safe: reads source, writes only to a scratch dir.
@@ -21569,13 +21619,45 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
21569
21619
  status = clip.GetClipProperty("Transcription Status")
21570
21620
  except Exception:
21571
21621
  status = None
21572
- return {
21622
+ out = {
21573
21623
  "clip_id": p.get("clip_id"),
21574
21624
  "text": text,
21575
21625
  "truncated": _is_truncated(text),
21576
21626
  "status": status or None,
21577
21627
  "has_transcription": bool(text.strip()),
21628
+ "segments": None,
21629
+ "language": None,
21630
+ "source": "clip_property",
21578
21631
  }
21632
+ # The `Transcription` clip property is a preview and stops at an
21633
+ # ellipsis. Resolve 21.1 added a real accessor that does not truncate,
21634
+ # so prefer it and keep the property as the 21.0.x fallback. Verified on
21635
+ # Studio 21.1.0.14: 1550 segments with per-word start/end timecodes.
21636
+ # Segment timecodes are SOURCE timecodes, not timeline positions.
21637
+ if _has_method(clip, "GetTranscription"):
21638
+ try:
21639
+ full = clip.GetTranscription(bool(p.get("use_nested_clip_transcription", False)))
21640
+ except Exception:
21641
+ full = None
21642
+ segs = full.get("segments") if isinstance(full, dict) else None
21643
+ if segs:
21644
+ if not p.get("include_words"):
21645
+ # `words` is several times the bulk of the segment text and
21646
+ # most callers want segment-level timing. Opt in for it.
21647
+ segs = [{k: v for k, v in seg.items() if k != "words"}
21648
+ if isinstance(seg, dict) else seg for seg in segs]
21649
+ joined = " ".join(seg.get("text", "") for seg in segs
21650
+ if isinstance(seg, dict)).strip()
21651
+ out.update({
21652
+ "segments": segs,
21653
+ "language": full.get("language"),
21654
+ "source": "get_transcription",
21655
+ "truncated": False,
21656
+ })
21657
+ if joined:
21658
+ out["text"] = joined
21659
+ out["has_transcription"] = True
21660
+ return out
21579
21661
  elif action == "extract_frames":
21580
21662
  return _extract_clip_frames(clip, p)
21581
21663
  elif action == "perform_audio_classification":
@@ -1495,6 +1495,31 @@ API_TRUTH: List[Dict[str, Any]] = [
1495
1495
  "subtitle", "transcription"],
1496
1496
  "submit": "bug",
1497
1497
  },
1498
+ {
1499
+ "symbol": "SetRenderSettings MarkIn/MarkOut below the timeline start are clamped, not refused",
1500
+ "object": "Project / Timeline",
1501
+ "reality": "SetRenderSettings takes MarkIn/MarkOut as ABSOLUTE record "
1502
+ "frames, and a value below the timeline's start frame is "
1503
+ "silently clamped to the start: measured on Studio 19.1.3.7 "
1504
+ "(2026-09-08) on an 86400-start timeline, MarkIn=MarkOut=86420 "
1505
+ "rendered timeline frame 20 and MarkIn=MarkOut=20 rendered "
1506
+ "frame 0 — one frame, True from SetRenderSettings, no error "
1507
+ "anywhere. The trap is that Timeline.GetMarkInOut reports the "
1508
+ "user's marks RELATIVE to the timeline start (Blackmagic's own "
1509
+ "README example is {'in': 0, 'out': 134}; the 21.1 stub says "
1510
+ "'record frame relative to timeline start'), so feeding its "
1511
+ "output straight into SetRenderSettings renders the wrong range "
1512
+ "with every readback agreeing. SetMarkInOut itself stores "
1513
+ "whatever number it is given (10 reads back 10, 86410 reads "
1514
+ "back 86410), so a script-written range can be in either space.",
1515
+ "recommended": "Offset GetMarkInOut values by Timeline.GetStartFrame() before "
1516
+ "passing them to SetRenderSettings when they fall below the "
1517
+ "start frame (timeline_frame capture does this when it puts "
1518
+ "a user's range back). Verify a render range from the "
1519
+ "delivered frames, never from the settings call's return.",
1520
+ "tags": ["render", "silent-failure", "frame-space", "mark-range"],
1521
+ "submit": "bug",
1522
+ },
1498
1523
  {
1499
1524
  "symbol": "SetRenderSettings ExportSubtitle / SubtitleFormat had no observable effect",
1500
1525
  "object": "Project (render settings)",
@@ -165,6 +165,18 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
165
165
  "note": "Reported against 21.0.4.5. Not reachable through this server yet.",
166
166
  "issue": 131,
167
167
  },
168
+ {
169
+ "symbol": "MediaPoolItem.GetTranscription",
170
+ "introduced_in": "21.1",
171
+ "source": "reported",
172
+ "note": "Reported against Studio 21.1.0.14 (PR #199): the untruncated "
173
+ "transcript, {language, segments[{start, end, text, speaker, "
174
+ "words[]}]} in SOURCE timecode. media_pool_item get_transcription "
175
+ "prefers it when present and falls back to the truncated "
176
+ "'Transcription' clip property below 21.1; `source` in the "
177
+ "result says which route ran.",
178
+ "issue": 199,
179
+ },
168
180
  {
169
181
  "symbol": "Project.SetRenderSettings UseFullExtents",
170
182
  "introduced_in": "21.0.4",