davinci-resolve-mcp 2.213.0 → 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/AGENTS.md CHANGED
@@ -78,12 +78,21 @@ semantics.
78
78
  limitation, add a `submit`-tagged entry to `src/utils/api_truth.py` and
79
79
  regenerate with `scripts/gen_api_limitations.py` (a drift guard enforces it)
80
80
  - Bundled Resolve API text: `docs/reference/resolve_scripting_api.txt` — refresh
81
- it by copying the shipped `Developer/Scripting/README.txt` over it wholesale,
82
- never by hand-adding the lines you already know about. The file's only value
83
- is that a diff against it is trustworthy; a hand-patch carries a newer
84
- `Last Updated:` header while still hiding everything you did not know to look
85
- for. That is exactly how it sat eight weeks stale at 26 May 2026 while ten
86
- documented 21.0.4 methods went unwired (PR #139)
81
+ it by copying the shipped scripting README over it wholesale, never by
82
+ hand-adding the lines you already know about. The file's only value is that a
83
+ diff against it is trustworthy; a hand-patch carries a newer `Last Updated:`
84
+ header while still hiding everything you did not know to look for. That is
85
+ exactly how it sat eight weeks stale at 26 May 2026 while ten documented
86
+ 21.0.4 methods went unwired (PR #139).
87
+ **The shipped README moved in 21.1.** `Developer/Scripting/README.txt` is
88
+ gone; the pack is now `README.md`, a typed `DaVinciResolveScript.pyi` (the
89
+ full signature surface, which the old README never carried) and a
90
+ `CHANGELOG.md` of per-version additions. Copying `README.md` over the bundled
91
+ file is still the right refresh, but it renumbers every line — and
92
+ `scripts/audit_api_parity.py`, `tests/test_resolve_bridge.py` and a few dozen
93
+ `resolve_scripting_api.txt line N` comments in `src/` read it. Re-run the
94
+ parity audit and re-anchor those comments in the same change, or leave the
95
+ file at its 21.0 revision and cite the `.pyi` for new work
87
96
 
88
97
  ## Key Paths
89
98
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,149 @@
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
+
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
56
+
57
+ Reported in #196 by @JosephConroy93, with a repro precise enough to reproduce
58
+ on the first run.
59
+
60
+ ### Fixed — Fusion keyframes (#196)
61
+
62
+ - **`fusion_comp add_keyframe` wrote the keyframe under `comp.Lock()`, so it
63
+ read back correctly and never rendered.** This is the comp-lock class this
64
+ repo documented in v2.98.5 for `set_input` — a value write under a lock
65
+ lands in the graph, `GetKeyFrames` lists it, Fusion's own page playback
66
+ interpolates it, and the delivered render ignores it — and `add_keyframe`
67
+ was the site that never got the fix. Measured on Studio 19.1.3.7 with a
68
+ Transform `Size` keyframed 2.0 → 1.0 over 47 frames on a media-backed clip:
69
+ through the shipped handler the render was bit-identical to the no-comp
70
+ baseline (PSNR inf); the identical writes with the lock removed rendered the
71
+ zoom (PSNR 13.3 dB against baseline, and frame 46 back within 44 dB of the
72
+ baseline as `Size` returned to 1.0). Four unlocked variants all rendered —
73
+ modifier locked with the write outside, `StartUndo`/`EndUndo` around both,
74
+ nothing wrapped, and a spline assigned directly — so the write pattern is
75
+ not the variable, the lock is. The handler now wraps the spline attach and
76
+ the first key in `StartUndo`/`EndUndo`, the escape `bulk_set_inputs`
77
+ already uses, which also gives the user one undo step per keyframe.
78
+ - **`delete_keyframe` moved off the lock for consistency, not because it was
79
+ broken.** It was mutation-checked: re-locking the delete and rendering still
80
+ removed the key from the output (frame 46 stayed at the 2.0 zoom, 5.7 dB
81
+ from baseline). The lock does not suppress a spline delete on 19.1.3.7.
82
+
83
+ ### Notes on the report
84
+
85
+ - The reporter's own patch — write outside the lock, modifier still inside —
86
+ made their render fail outright on Windows 19.1.3.7 and 21.0.4.5. That
87
+ variant rendered correctly here on macOS 19.1.3.7, so the failure was not
88
+ reproduced and its cause is unknown; the shipped fix uses the undo-wrapped
89
+ shape instead, which rendered on every attempt.
90
+ - Priming (an unrelated unlocked write first) did not rescue it for the
91
+ reporter. That matches the v2.98.8 mechanism only partly and was not
92
+ re-tested here.
93
+
94
+ ### Validation — #196
95
+
96
+ - `tests/test_fusion_value_write_lock.py` now recognises the keyframe write
97
+ shape (`tool[input][time] = value`) under a lock, and fails against the
98
+ pre-fix server naming `add_keyframe`'s line — the guard was confirmed to
99
+ fail before it was confirmed to pass.
100
+ - Live render witness on Studio 19.1.3.7 through the real `fusion_comp`
101
+ handlers: `add_keyframe` animates, `delete_keyframe` removes, `get_keyframes`
102
+ reads the written values. Full offline Python suite, drift guards and the
103
+ advanced Node suite green.
104
+
105
+ #197 and #198 contributed by @billcarroll, on the day Resolve 21.1 shipped. Also on landing: `timeline_markers get_thumbnail` read the thumbnail once too and now goes through the same settle helper; live-validated on the same scratch timeline (two of two reads), with the guard covering both sites.
106
+
107
+ ### Fixed — contact sheet and get_thumbnail (#198)
108
+
109
+ - **`timeline thumbnail_contact_sheet` returned "No thumbnail available" for
110
+ every frame.** It moved the playhead and read the thumbnail once,
111
+ immediately; the viewer has not caught up when the scripting call that
112
+ follows a playhead move lands, so each sample came back empty. The sheet now
113
+ reads through `_playhead_thumbnail_settled`, the polling helper the
114
+ single-frame path already used — it was the one caller not switched over.
115
+ Live-validated on Studio 19.1.3.7: three samples across a scratch timeline,
116
+ all three returned thumbnails. A static guard now fails the suite if any
117
+ `GetCurrentClipThumbnailImage` read appears outside that helper. (#198)
118
+
119
+ ### Documentation — Resolve 21.1 in the API ledger (#197)
120
+
121
+ - **Three `api_truth` entries corrected for Resolve 21.1** (#197), on the
122
+ strength of the contributor's attribute probe of Studio 21.1.0.14. Native
123
+ multicam clip creation is withdrawn as a gap (`MediaPool.CreateMulticamClip`,
124
+ `TimelineItem.FlattenMulticam`, `PerformMulticamSmartSwitch`,
125
+ `Timeline.AutoAlignClips` resolve on 21.1); transition **creation** is fixed
126
+ by `TimelineItem.AddTransition` while readback and cloning stay missing; and
127
+ the truncated `GetClipProperty('Transcription')` now has a real route around
128
+ it in `MediaPoolItem.GetTranscription()`, which returns per-word timing and
129
+ speakers for the source clip. `docs/reference/api-limitations.md` regenerated
130
+ (42 → 41 missing capabilities).
131
+ - **Provenance is stated in each entry.** No 21.1 build exists on the
132
+ maintainer's machine, so the entries record these as *reported by the
133
+ contributor, not reproduced here*, following the ledger's existing
134
+ measured / reported distinction. None of the methods was invoked; the claim
135
+ is existence and signature only, and each entry says what would falsify it.
136
+ - **The shipped scripting README moved in 21.1.** `Developer/Scripting/README.txt`
137
+ is gone in favour of `README.md`, a typed `DaVinciResolveScript.pyi` and a
138
+ `CHANGELOG.md`. `tests/live_resolve21_validation.py` reads either name, and
139
+ `AGENTS.md` explains why the bundled API text is not refreshed in the same
140
+ change (every `resolve_scripting_api.txt line N` anchor in `src/` would move).
141
+
142
+ ### Validation — #197 and #198
143
+
144
+ - Full offline Python suite, drift guards (api-limitations, agent rules,
145
+ release surfaces), the advanced Node suite. Contact sheet live-validated as
146
+ above; the api_truth change is documentation and needs no Resolve run.
147
+
5
148
  ## What's New in v2.213.0 — a ColorTrace that matches on media, then applies
6
149
 
7
150
  ### Added
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.0-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.0-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.0 版 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:** 42 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
@@ -79,18 +79,11 @@ equivalent, blocking full automation.
79
79
  - **Reference:** [issue #73](https://github.com/samuelgursky/davinci-resolve-mcp/issues/73)
80
80
  - **Tags:** missing-method, audio, channel
81
81
 
82
- ### Native multicam clip creation
83
-
84
- - **Object:** `MediaPool`
85
- - **Behavior:** There is no method to create a native multicam clip from a set of angles. Angles can be stacked onto tracks programmatically, but the multicam-clip conversion is a UI-only step.
86
- - **Workaround / current handling:** Prepare a stacked timeline (media_pool setup_multicam_timeline) and finish the multicam-clip conversion in the Resolve UI.
87
- - **Tags:** missing-method, media-pool, multicam
88
-
89
82
  ### Transition create / copy / clone
90
83
 
91
84
  - **Object:** `Timeline / TimelineItem`
92
- - **Behavior:** There is no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING: creation, cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — BUT a Solid Color generator AND a subtitle item read the same way (GetProperty() None, no MediaPoolItem; measured E115), so that pair only separates clips from non-clips. What separates a transition from a generator is GEOMETRY: a transition straddles a cut (one neighbour ends inside its span, another starts inside it) while a generator owns its span. timeline.get_items reports `kind` on that basis.
93
- - **Workaround / current handling:** Automated QC of existing transitions IS possible and is the main practical need — enumerate GetItemListInTrack, treat any item whose GetProperty() is empty and whose GetMediaPoolItem() is None as a transition, and read its name, start and duration. Removal is scriptable via Timeline.DeleteClips. To CREATE one, either apply it in the Resolve UI, or author it offline and import: the advanced server's drp place_transition writes a cross dissolve at an abutting cut ({track, atFrame, durationFrames}) and it round-trips into Resolve 21.0.4.5 reading back at the expected centered range.
85
+ - **Behavior:** CREATION IS FIXED IN 21.1, READBACK IS NOT. Reported by @billcarroll (PR #197) from an attribute probe on Studio 21.1.0.14 (2026-09-08; not reproduced here, no 21.1 install): TimelineItem.AddTransition resolves to a <BlackmagicFusion.PyFunctionCall object>, not None. Its stub signature is AddTransition(transitionOptions) -> TimelineItem | None, where transitionOptions carries type (e.g. 'Cross Dissolve'), category ('simple'|'fusion'|'ofx'|'audio'), position ('start'|'end'), alignment ('left'|'center'|'right') and an optional duration in frames. It was NOT invoked (a call mutates the user's timeline), so this entry claims existence and signature only. WHAT REMAINS MISSING ON 21.1: reading a transition back. There is still no accessor for an existing transition's type, alignment or duration beyond its name string and frame range, and no clone verb — alignment and duration are write-only arguments to AddTransition. The pre-21.1 statement, kept as the historical record: there was no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING (pre-21.1: creation too; on 21.1+ read the paragraph above): cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — BUT a Solid Color generator AND a subtitle item read the same way (GetProperty() None, no MediaPoolItem; measured E115), so that pair only separates clips from non-clips. What separates a transition from a generator is GEOMETRY: a transition straddles a cut (one neighbour ends inside its span, another starts inside it) while a generator owns its span. timeline.get_items reports `kind` on that basis.
86
+ - **Workaround / current handling:** Automated QC of existing transitions IS possible and is the main practical need — enumerate GetItemListInTrack, treat any item whose GetProperty() is empty and whose GetMediaPoolItem() is None as a transition, and read its name, start and duration. Removal is scriptable via Timeline.DeleteClips. To CREATE one, either apply it in the Resolve UI, or author it offline and import: the advanced server's drp place_transition writes a cross dissolve at an abutting cut ({track, atFrame, durationFrames}) and it round-trips into Resolve 21.0.4.5 reading back at the expected centered range. On 21.1+ prefer TimelineItem.AddTransition, which takes the type, category, edge, alignment and duration directly.
94
87
  - **Tags:** missing-method, timeline, transition
95
88
 
96
89
  ### Cloud project enumeration / export / user management
@@ -514,8 +507,8 @@ values, or automation-hostile modal prompts.
514
507
  ### MediaPoolItem.GetClipProperty('Transcription')
515
508
 
516
509
  - **Object:** `MediaPoolItem`
517
- - **Behavior:** Returns a PREVIEW of the transcription that ends in an ellipsis when the full transcript is longer than the property exposes.
518
- - **Workaround / current handling:** Treat a trailing ellipsis as truncation (see media_pool_item get_transcription's `truncated` flag).
510
+ - **Behavior:** Returns a PREVIEW of the transcription that ends in an ellipsis when the full transcript is longer than the property exposes. Reported still true on Studio 21.1.0.14 by @billcarroll (PR #197; not reproduced here, no 21.1 install) — the property is not the fix. 21.1 adds a SEPARATE method that is not truncated: MediaPoolItem.GetTranscription(useNestedClipTranscription=False) -> {language, segments[{start, end, text, speaker, words[{start, end, text}]}]}, with timecode strings rather than frame numbers. The contributor measured it on a live 21.1.0.14 against an already-transcribed interview clip: 1550 segments, per-word start/end timecodes, a populated `speaker` field, and '(...)' as Resolve's own silence marker. Note the transcript is of the SOURCE clip, so timeline positions must be mapped through GetStart()/GetSourceStartFrame() on the timeline item.
511
+ - **Workaround / current handling:** On 21.1+, call MediaPoolItem.GetTranscription() instead of reading the property. On 21.0.x and earlier, treat a trailing ellipsis as truncation (see media_pool_item get_transcription's `truncated` flag).
519
512
  - **Tags:** transcription, truncation
520
513
 
521
514
  ### ProjectManager.CreateProject (blocked by the current project)
@@ -548,6 +541,13 @@ values, or automation-hostile modal prompts.
548
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.
549
542
  - **Tags:** free-edition, studio-only, silent-failure, modal, ai, subtitle, transcription
550
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
+
551
551
  ### SetRenderSettings ExportSubtitle / SubtitleFormat had no observable effect
552
552
 
553
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.0"
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.0",
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.0"
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.0"
14
+ VERSION = "2.213.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -7067,8 +7067,15 @@ def _timeline_thumbnail_contact_sheet(proj, tl, p: Dict[str, Any]) -> Dict[str,
7067
7067
  )
7068
7068
  sampled.append(sample)
7069
7069
  continue
7070
- thumbnail = tl.GetCurrentClipThumbnailImage()
7071
- if not thumbnail:
7070
+ # Poll instead of reading once: the viewer has not caught
7071
+ # up when the scripting call right after a playhead move
7072
+ # lands, so a single read returns None on every sample and
7073
+ # the whole sheet comes back "No thumbnail available at
7074
+ # frame" while the same calls with a settle succeed.
7075
+ thumbnail, thumb_err = _playhead_thumbnail_settled(tl)
7076
+ if thumb_err:
7077
+ sample["error"] = thumb_err.get("error")
7078
+ elif not thumbnail:
7072
7079
  sample["error"] = (
7073
7080
  "No thumbnail available at frame"
7074
7081
  if on_color
@@ -14927,6 +14934,40 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
14927
14934
  original_tc = tl.GetCurrentTimecode()
14928
14935
  except Exception:
14929
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
14930
14971
 
14931
14972
  job = None
14932
14973
  try:
@@ -15020,16 +15061,28 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
15020
15061
  original_fc.get("format"), original_fc.get("codec"),
15021
15062
  restore_exc or "SetCurrentRenderFormatAndCodec returned False",
15022
15063
  )
15023
- # Best-effort, not a restore: without GetRenderSettings there is nothing
15024
- # to restore FROM, so put the mark range back to the whole timeline
15025
- # 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.)
15026
15071
  try:
15027
- proj.SetRenderSettings({
15028
- "SelectAllFrames": True,
15029
- "MarkIn": tl.GetStartFrame(),
15030
- "MarkOut": tl.GetEndFrame(),
15031
- "CustomName": "",
15032
- })
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)
15033
15086
  except Exception:
15034
15087
  pass
15035
15088
  try:
@@ -21305,11 +21358,15 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
21305
21358
  get_unique_id(clip_id) -> {id}
21306
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)
21307
21360
  clear_transcription(clip_id) -> {success}
21308
- get_transcription(clip_id) -> {text, truncated, status, has_transcription}
21309
- Read a clip's transcription. `truncated` flags when Resolve's preview
21310
- property cut the text off (the full transcript is longer). Clip-level and
21311
- separate from the timeline subtitle transcript (timeline.get_transcript)
21312
- 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.
21313
21370
  extract_frames(clip_id, timestamps, output_dir?) -> {frame_paths, output_dir, count, errors}
21314
21371
  Extract still JPEGs from the clip's source at the given timestamps (seconds)
21315
21372
  via ffmpeg. Source-safe: reads source, writes only to a scratch dir.
@@ -21562,13 +21619,45 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
21562
21619
  status = clip.GetClipProperty("Transcription Status")
21563
21620
  except Exception:
21564
21621
  status = None
21565
- return {
21622
+ out = {
21566
21623
  "clip_id": p.get("clip_id"),
21567
21624
  "text": text,
21568
21625
  "truncated": _is_truncated(text),
21569
21626
  "status": status or None,
21570
21627
  "has_transcription": bool(text.strip()),
21628
+ "segments": None,
21629
+ "language": None,
21630
+ "source": "clip_property",
21571
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
21572
21661
  elif action == "extract_frames":
21573
21662
  return _extract_clip_frames(clip, p)
21574
21663
  elif action == "perform_audio_classification":
@@ -25804,8 +25893,13 @@ def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> An
25804
25893
  # GetCurrentClipThumbnailImage returns None on every page but Color, and
25805
25894
  # says nothing about why — hold the Color page for the read rather than
25806
25895
  # reporting a page problem as a missing thumbnail.
25896
+ # Read through the settle helper: the first read after the page switch
25897
+ # comes back None before the viewer catches up (see PR #198, where the
25898
+ # contact sheet reported "No thumbnail available" on every frame).
25807
25899
  with _color_page_for_thumbnails(get_resolve()) as on_color:
25808
- thumbnail = tl.GetCurrentClipThumbnailImage()
25900
+ thumbnail, thumb_err = _playhead_thumbnail_settled(tl)
25901
+ if thumb_err:
25902
+ return thumb_err
25809
25903
  if thumbnail is None:
25810
25904
  return {
25811
25905
  "success": False,
@@ -30077,7 +30171,15 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
30077
30171
  tool = comp.FindTool(p["tool_name"])
30078
30172
  if not tool:
30079
30173
  return _err(f"Tool '{p['tool_name']}' not found")
30080
- comp.Lock()
30174
+ # No comp.Lock() here — see _FUSION_VALUE_WRITE_NOTE. The keyframe
30175
+ # assignment below is a value write, and under a lock it reads back
30176
+ # (GetKeyFrames lists both keys) while the RENDER ignores it: measured
30177
+ # on Studio 19.1.3.7 (issue #196), a Transform Size keyframed 2.0 -> 1.0
30178
+ # rendered bit-identical to the no-comp baseline (PSNR inf); the same
30179
+ # writes outside the lock rendered the zoom (PSNR 13.3 dB vs baseline).
30180
+ # StartUndo/EndUndo is the escape bulk_set_inputs already uses, and it
30181
+ # also makes "attach spline + first key" one undo step for the user.
30182
+ comp.StartUndo(f"Keyframe {p['input_name']}")
30081
30183
  try:
30082
30184
  inp = tool[p["input_name"]]
30083
30185
  if not inp:
@@ -30119,7 +30221,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
30119
30221
  tool[p["input_name"]][p["time"]] = p["value"]
30120
30222
  return _ok()
30121
30223
  finally:
30122
- comp.Unlock()
30224
+ comp.EndUndo(True)
30123
30225
 
30124
30226
  elif action == "get_keyframes":
30125
30227
  tool = comp.FindTool(p["tool_name"])
@@ -30145,11 +30247,13 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
30145
30247
  tool = comp.FindTool(p["tool_name"])
30146
30248
  if not tool:
30147
30249
  return _err(f"Tool '{p['tool_name']}' not found")
30148
- comp.Lock()
30250
+ # Same shape as add_keyframe: a spline edit is a value write, so it runs
30251
+ # under StartUndo/EndUndo rather than comp.Lock() (issue #196).
30252
+ comp.StartUndo(f"Delete keyframe {p.get('input_name')}")
30149
30253
  try:
30150
30254
  return _fusion_delete_keyframe(tool, p)
30151
30255
  finally:
30152
- comp.Unlock()
30256
+ comp.EndUndo(True)
30153
30257
 
30154
30258
  # --- Composition Control ---
30155
30259
  elif action == "get_comp_info":
@@ -689,9 +689,22 @@ API_TRUTH: List[Dict[str, Any]] = [
689
689
  "object": "MediaPoolItem",
690
690
  "reality": "Returns a PREVIEW of the transcription that ends in an "
691
691
  "ellipsis when the full transcript is longer than the property "
692
- "exposes.",
693
- "recommended": "Treat a trailing ellipsis as truncation (see "
694
- "media_pool_item get_transcription's `truncated` flag).",
692
+ "exposes. Reported still true on Studio 21.1.0.14 by @billcarroll (PR #197; not "
693
+ "reproduced here, no 21.1 install) the property is not "
694
+ "the fix. 21.1 adds a SEPARATE method that is not truncated: "
695
+ "MediaPoolItem.GetTranscription(useNestedClipTranscription=False) "
696
+ "-> {language, segments[{start, end, text, speaker, words[{start, "
697
+ "end, text}]}]}, with timecode strings rather than frame numbers. "
698
+ "The contributor measured it on a live 21.1.0.14 against an already-transcribed "
699
+ "interview clip: 1550 segments, per-word start/end timecodes, a "
700
+ "populated `speaker` field, and '(...)' as Resolve's own silence "
701
+ "marker. Note the transcript is of the SOURCE clip, so timeline "
702
+ "positions must be mapped through GetStart()/GetSourceStartFrame() "
703
+ "on the timeline item.",
704
+ "recommended": "On 21.1+, call MediaPoolItem.GetTranscription() instead of "
705
+ "reading the property. On 21.0.x and earlier, treat a "
706
+ "trailing ellipsis as truncation (see media_pool_item "
707
+ "get_transcription's `truncated` flag).",
695
708
  "tags": ["transcription", "truncation"],
696
709
  "submit": "bug",
697
710
  },
@@ -855,18 +868,51 @@ API_TRUTH: List[Dict[str, Any]] = [
855
868
  {
856
869
  "symbol": "Native multicam clip creation",
857
870
  "object": "MediaPool",
858
- "reality": "There is no method to create a native multicam clip from a set "
859
- "of angles. Angles can be stacked onto tracks programmatically, "
860
- "but the multicam-clip conversion is a UI-only step.",
861
- "recommended": "Prepare a stacked timeline (media_pool setup_multicam_timeline) "
862
- "and finish the multicam-clip conversion in the Resolve UI.",
863
- "tags": ["missing-method", "media-pool", "multicam"],
864
- "submit": "missing",
871
+ "reality": "WITHDRAWN on the strength of a contributor's probe of Studio 21.1.0.14 "
872
+ "(@billcarroll, PR #197, 2026-09-08; not reproduced here, no 21.1 "
873
+ "install): Resolve 21.1 adds "
874
+ "MediaPool.CreateMulticamClip(clips, multicamOptions) -> "
875
+ "list[MediaPoolItem], plus TimelineItem.FlattenMulticam, "
876
+ "TimelineItem.PerformMulticamSmartSwitch and "
877
+ "Timeline.AutoAlignClips. Measured by attribute probe on a live "
878
+ "21.1.0.14: each of those four resolves to a "
879
+ "<BlackmagicFusion.PyFunctionCall object>, not None — the same "
880
+ "discriminator that distinguishes a real method from an absent "
881
+ "one elsewhere in this registry. THE ENTRY IS NOT PROOF THE "
882
+ "METHODS WORK: none of them was invoked, because doing so mutates "
883
+ "the user's project. HISTORICAL, still true of 21.0.x and "
884
+ "earlier: there was no method to create a native multicam clip "
885
+ "from a set of angles; angles could be stacked onto tracks "
886
+ "programmatically but the multicam-clip conversion was a UI-only "
887
+ "step. Falsified further (either direction) by a session that "
888
+ "actually calls CreateMulticamClip on a scratch project.",
889
+ "recommended": "On 21.1+, call MediaPool.CreateMulticamClip. On 21.0.x and "
890
+ "earlier, prepare a stacked timeline (media_pool "
891
+ "setup_multicam_timeline) and finish the multicam-clip "
892
+ "conversion in the Resolve UI.",
893
+ "tags": ["media-pool", "multicam", "fixed-in-21.1"],
865
894
  },
866
895
  {
867
896
  "symbol": "Transition create / copy / clone",
868
897
  "object": "Timeline / TimelineItem",
869
- "reality": "There is no method to ADD or CLONE an edit transition — no "
898
+ "reality": "CREATION IS FIXED IN 21.1, READBACK IS NOT. Reported by @billcarroll "
899
+ "(PR #197) from an attribute probe on Studio 21.1.0.14 (2026-09-08; "
900
+ "not reproduced here, no 21.1 install): "
901
+ "TimelineItem.AddTransition resolves to a "
902
+ "<BlackmagicFusion.PyFunctionCall object>, not None. Its stub "
903
+ "signature is AddTransition(transitionOptions) -> TimelineItem | "
904
+ "None, where transitionOptions carries type (e.g. 'Cross "
905
+ "Dissolve'), category ('simple'|'fusion'|'ofx'|'audio'), position "
906
+ "('start'|'end'), alignment ('left'|'center'|'right') and an "
907
+ "optional duration in frames. It was NOT invoked (a call mutates "
908
+ "the user's timeline), so this entry claims existence and "
909
+ "signature only. WHAT REMAINS MISSING ON 21.1: reading a "
910
+ "transition back. There is still no accessor for an existing "
911
+ "transition's type, alignment or duration beyond its name string "
912
+ "and frame range, and no clone verb — alignment and duration are "
913
+ "write-only arguments to AddTransition. The pre-21.1 statement, "
914
+ "kept as the historical record: there was no method to ADD or "
915
+ "CLONE an edit transition — no "
870
916
  "AddTransition/CreateTransition/AddVideoTransition on Timeline "
871
917
  "or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on "
872
918
  "Studio 21.0.4.5 (2026-08-12): this entry previously said "
@@ -886,7 +932,8 @@ API_TRUTH: List[Dict[str, Any]] = [
886
932
  "a transition item and a clip item is GetProperty(): a "
887
933
  "transition returns an EMPTY dict where a video clip returns 26 "
888
934
  "transform keys; it also has no MediaPoolItem and no Fusion "
889
- "comp. WHAT IS GENUINELY MISSING: creation, cloning, and any "
935
+ "comp. WHAT IS GENUINELY MISSING (pre-21.1: creation too; on "
936
+ "21.1+ read the paragraph above): cloning, and any "
890
937
  "type/alignment/parameter detail — the transition's kind is "
891
938
  "knowable ONLY from its name string, and there is no way to "
892
939
  "read its alignment (centered/start/end) or edit its duration. "
@@ -913,7 +960,9 @@ API_TRUTH: List[Dict[str, Any]] = [
913
960
  "server's drp place_transition writes a cross dissolve at an "
914
961
  "abutting cut ({track, atFrame, durationFrames}) and it "
915
962
  "round-trips into Resolve 21.0.4.5 reading back at the "
916
- "expected centered range.",
963
+ "expected centered range. On 21.1+ prefer "
964
+ "TimelineItem.AddTransition, which takes the type, category, "
965
+ "edge, alignment and duration directly.",
917
966
  "tags": ["missing-method", "timeline", "transition"],
918
967
  "submit": "missing",
919
968
  },
@@ -1446,6 +1495,31 @@ API_TRUTH: List[Dict[str, Any]] = [
1446
1495
  "subtitle", "transcription"],
1447
1496
  "submit": "bug",
1448
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
+ },
1449
1523
  {
1450
1524
  "symbol": "SetRenderSettings ExportSubtitle / SubtitleFormat had no observable effect",
1451
1525
  "object": "Project (render settings)",
@@ -104,7 +104,9 @@ def at_least(live: Any, required: Any) -> Optional[bool]:
104
104
  #
105
105
  # measured — probed live by this project
106
106
  # reported — a user's live probe, credited, not independently reproduced
107
- # vendor — stated by Blackmagic's shipped Developer/Scripting/README.txt
107
+ # vendor — stated by Blackmagic's shipped Developer/Scripting docs
108
+ # (README.txt through 21.0; README.md, CHANGELOG.md and
109
+ # DaVinciResolveScript.pyi from 21.1 on)
108
110
  # documented — a floor this server already enforces at the call site, taken
109
111
  # from Blackmagic's release documentation. See CODE_FLOORS.
110
112
 
@@ -163,6 +165,18 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
163
165
  "note": "Reported against 21.0.4.5. Not reachable through this server yet.",
164
166
  "issue": 131,
165
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
+ },
166
180
  {
167
181
  "symbol": "Project.SetRenderSettings UseFullExtents",
168
182
  "introduced_in": "21.0.4",