davinci-resolve-mcp 4.9.3 → 4.10.0

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,55 @@
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 v4.10.0 — subtitle caption and preset editing in saved projects
6
+
7
+ ### Added
8
+
9
+ - **Subtitle editing through `project_db`.** Resolve's scripting API can
10
+ generate and list subtitles but cannot change caption text, word timing or
11
+ a track's animation controls (`TimelineItem.SetProperty` does not reach
12
+ them). Six new actions edit them in a saved local project's `Project.db`:
13
+ - `list_captions`, `check_captions` and `write_captions`: caption text,
14
+ frame bounds and explicit per-word timing; add, replace and delete in one
15
+ transaction, preserving unknown fields and the original AI words.
16
+ - `list_subtitle_presets`, `copy_subtitle_preset` and
17
+ `set_subtitle_preset`: clone a compatible saved animation preset, and edit
18
+ Word Highlight's font, position, text / highlight / outline colours and
19
+ outline thickness.
20
+ - Writes require Resolve **fully quit** (checked through the process list,
21
+ plus `iConfirmProjectClosed:true`), take a unique SQLite snapshot backup
22
+ including committed WAL pages, refuse unsupported schemas, and run the edit
23
+ and its readback in one transaction. `verified:true` means database
24
+ readback, not a render. SQLite uses `better-sqlite3`, or `node:sqlite` on
25
+ Node 22.16+ / 23.8+. See `docs/guides/subtitle-track-editing.md`.
26
+ - A live `TimelineItem.SetProperty` on a subtitle item is refused before the
27
+ automatic timeline archive, with a pointer to these actions.
28
+
29
+ Contributed by @sidevconcept (#273).
30
+
31
+ ### Changed during review
32
+
33
+ - **No writes while Resolve runs.** An opt-in to write a project that is not
34
+ currently loaded was removed. Measured on Studio 19.1.3.7: a project loaded
35
+ earlier in the session is served from memory when loaded again, so a disk
36
+ write to it is not shown, and saving after editing the same rows
37
+ overwrites it. A fresh launch does read it.
38
+ - **Text colour.** The writer set the Word Highlight macro's `Clone` colour
39
+ controls, which do not render, leaving the template's default pale yellow;
40
+ it now writes the Text+ shading inputs (`Red1`/`Green1`/`Blue1`/`Alpha1`).
41
+ - **Handle release.** A guarded database is closed when a schema query throws,
42
+ a likely cause of the slow tests reported on Windows.
43
+
44
+ ### Validation
45
+
46
+ - Contributor-validated on Studio 21.1.0.17 / Windows: a synthetic caption
47
+ edit, full quit, database write, relaunch and burn-in render passed on all
48
+ 630 decoded frames, with white base text, yellow highlighting and exact
49
+ word transitions, checked against a natively configured control. Not
50
+ rendered on this project's 19.1.3.7 host.
51
+ - Offline: Node advanced suite 1,036 tests / 0 failed in a clean worktree;
52
+ Python suite green.
53
+
5
54
  ## What's New in v4.9.3 — .drp bin registry for two or more bins; broader temp-root guard tests
6
55
 
7
56
  ### Fixed
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-4.9.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.10.0-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-37%20(389%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-4.9.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.10.0-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-37%20(389%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
- > 本翻译对应 v4.9.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.10.0 版 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
@@ -418,6 +418,14 @@ Operating rules an agent must know:
418
418
  `iConfirmProjectClosed:true`; every write auto-backs-up and read-back
419
419
  verifies. Resolve caches open projects in memory: after patching, fully QUIT
420
420
  and relaunch Resolve or the patch will not be visible.
421
+ - **Subtitle DB edits**: `project_db` `list_captions` / `write_captions` /
422
+ `check_captions` cover text, bounds, explicit per-word timing, add and delete.
423
+ `list_subtitle_presets` / `copy_subtitle_preset` / `set_subtitle_preset` handle
424
+ animated Fusion holders separately from basic `list_subtitle_styles`.
425
+ New subtitle writes enforce full Resolve quit BEFORE patching, snapshot the
426
+ SQLite DB and verify transactionally. See
427
+ [subtitle-track editing](guides/subtitle-track-editing.md) for selectors,
428
+ Word Highlight controls, dry-run examples and live-validation limits.
421
429
  - **Guards are load-bearing.** Advanced tools refuse rather than fabricate
422
430
  (silent-lie guards): a thrown "refused" error usually means wrong input space,
423
431
  log-encoded frames, or missing media — read the message before retrying.
@@ -0,0 +1,227 @@
1
+ # Subtitle tracks: captions and animated presets
2
+
3
+ The advanced server's `project_db` tool edits saved local SQLite projects.
4
+ The live server can generate subtitles with `timeline_ai.create_subtitles`,
5
+ enumerate their items, read names/bounds and delete items. It cannot edit caption
6
+ text, word timings or animation controls through `TimelineItem.SetProperty`.
7
+ Such subtitle property writes are now refused before auto-archiving.
8
+
9
+ There are two independent kinds of subtitle styling:
10
+
11
+ - `list_subtitle_styles` / `set_subtitle_style` read/edit the basic track
12
+ `EffectFiltersBA` font and position. `styled:false` does **not** mean the
13
+ track has no animation preset.
14
+ - `list_subtitle_presets` / `copy_subtitle_preset` operate on the linked Fusion
15
+ holder and composition. `set_subtitle_preset` edits named Word Highlight
16
+ controls inside its nested compressed tool section.
17
+
18
+ ## Required workflow
19
+
20
+ 1. Generate captions in Resolve and save the project. Inspect it with
21
+ `list_captions`, `list_subtitle_presets` and `check_captions`.
22
+ 2. Preview edits with `dryRun:true`. This reads the saved database; unsaved
23
+ Resolve changes are not visible. Preview IDs for proposed new captions are
24
+ provisional.
25
+ 3. Save and **fully quit Resolve before writing**. New subtitle write actions
26
+ require `iConfirmProjectClosed:true` and independently inspect running
27
+ processes. Closing only the project is insufficient. Failure to inspect
28
+ processes also refuses the write. Loading a *different* project is not
29
+ enough either: measured on Studio 19.1.3.7, a project loaded earlier in the
30
+ session is served from memory when it is loaded again, so a disk write to
31
+ it is not shown, and saving after editing the same rows overwrites it.
32
+ 4. Apply the edits, then relaunch Resolve and inspect/render the result.
33
+
34
+ Each new subtitle write creates a unique `Project.db.subtitle-<time>-<uuid>.bak`
35
+ SQLite snapshot, including committed WAL pages. Edits and readback verification
36
+ run in one transaction: a failure rolls everything back. Preserve the returned
37
+ backup. To recover, quit Resolve and restore that snapshot using your normal
38
+ project-library recovery procedure; do not replace a live database.
39
+
40
+ `verified:true` means **database readback**, not that Resolve rendered the
41
+ change. Writes never touch source media. Shared/unsupported item dependencies,
42
+ ambiguous timelines, missing tracks and mismatched schemas are refused.
43
+
44
+ ## Selectors and discovery
45
+
46
+ Use `projectName` or an explicit `projectDb` path. Discovery searches the
47
+ standard Windows `%APPDATA%/Blackmagic Design/DaVinci Resolve/Support`, macOS
48
+ Application Support (including the App Store sandbox), and Linux
49
+ `~/.local/share/DaVinciResolve` libraries. Both `Resolve Project Library` and
50
+ `Resolve Disk Database` layouts are considered. Relocated libraries need an
51
+ explicit path; these subtitle actions do not operate on Postgres libraries.
52
+
53
+ `timeline` is the saved timeline name; `track` is the 1-based subtitle-track
54
+ index (default 1). Vector associations determine track order. Duplicate timeline
55
+ names require disambiguation in Resolve before editing.
56
+
57
+ SQLite uses `better-sqlite3` when its native binding loads, otherwise
58
+ [`node:sqlite`](https://nodejs.org/docs/latest-v24.x/api/sqlite.html) on a suitable
59
+ Node runtime (22.16+ or 23.8+, which provide `node:sqlite` `backup()`; tested on
60
+ 24.14.1 and 22.22.3). Zstd reads use native
61
+ `node:zlib` when available and otherwise `fzstd`. Caption writes emit the raw
62
+ `0x80` envelope, avoiding a native compression dependency.
63
+
64
+ ## Captions
65
+
66
+ ```json
67
+ {"action":"list_captions","args":{"projectName":"My Project","timeline":"Reel"}}
68
+ ```
69
+
70
+ Results contain `fps`, `timeUnit:"timeline_frames"` and `captions` with
71
+ `id`, `text`, `start`, `end`, `words:[{text,start,end}]`, `originalWords`,
72
+ `anchor` and any `decodeIssues`. Bounds are absolute timeline frames, with
73
+ exclusive ends. Word frames may be fractional because storage uses 60 ticks
74
+ per second. Caption starts/ends must be nonnegative integer frames.
75
+
76
+ Current words come from protobuf fields 14/15/16; original AI words from
77
+ 18/19/20. Field 21 is a frame anchor. The decoder accounts for a caption moved
78
+ after that anchor was written. `Name` is only a fallback: UI text edits can
79
+ leave it stale, and can produce inconsistent word intervals. No assumption is
80
+ made that saving in Resolve will repair those intervals.
81
+
82
+ ```json
83
+ {
84
+ "action":"write_captions",
85
+ "args":{
86
+ "projectName":"My Project", "timeline":"Reel", "dryRun":true,
87
+ "replace":[{
88
+ "id":"<caption-id>", "text":"Hello world", "start":108000, "end":108060,
89
+ "words":[{"text":"Hello","start":108000,"end":108030},
90
+ {"text":"world","start":108030,"end":108060}]
91
+ }],
92
+ "add":[{
93
+ "text":"Welcome", "start":108090, "end":108120,
94
+ "words":[{"text":"Welcome","start":108090,"end":108120}]
95
+ }],
96
+ "delete":["<another-caption-id>"]
97
+ }
98
+ }
99
+ ```
100
+
101
+ For a write, replace `dryRun:true` with `iConfirmProjectClosed:true` after
102
+ quitting Resolve. `replace` supplies complete caption content/timing, not a
103
+ partial patch. Text must equal trimmed words joined with spaces. New captions
104
+ clone a caption already on that track (optionally `templateCaptionId`) to
105
+ preserve the unknown schema fields; adding to a completely empty track is
106
+ currently refused. Replacements preserve original AI words and unknown fields;
107
+ additions clear inherited AI history and markers. `Items` associations are
108
+ re-indexed by time without modifying `FusionCompHolderItems`.
109
+
110
+ Invalid word intervals, gaps, overlaps, conflicting IDs, or captions that
111
+ overlap are rejected. Boundaries are quantised to 60 Hz before verification;
112
+ a word that becomes zero length after rounding is also rejected. Existing
113
+ untouched captions are reported by QC rather than silently retimed.
114
+
115
+ `check_captions` takes the same selectors and optional `maxCharacters` (24 by
116
+ default). It reports gaps, invalid/missing timings, overlaps, out-of-bounds
117
+ words, decode count mismatches and long lines. The character limit is a
118
+ heuristic for vertical captions, **not** a pixel-width calculation: font,
119
+ size and position determine actual clipping. It never rewrites timing.
120
+
121
+ ## Animated preset copying and controls
122
+
123
+ ```json
124
+ {
125
+ "action":"copy_subtitle_preset",
126
+ "args":{
127
+ "sourceProject":"Reference", "sourceTimeline":"Reel", "sourceTrack":1,
128
+ "projectName":"My Project", "timeline":"Reel", "track":1,
129
+ "replace":false, "dryRun":true,
130
+ "inputs":{
131
+ "font":"Open Sans", "fontStyle":"Semibold", "size":0.08,
132
+ "position":[0.5,0.3],
133
+ "textRed":1, "textGreen":1, "textBlue":1, "textAlpha":1,
134
+ "highlightRed":1, "highlightGreen":0.8, "highlightBlue":0,
135
+ "outlineEnabled":1, "outlineRed":0, "outlineGreen":0, "outlineBlue":0,
136
+ "thickness":0.03
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ `sourceProjectDb` can replace `sourceProject`. For compatibility with the
143
+ workflow brief, `source_project`, `source_timeline`, `target_project` and
144
+ `target_timeline` alias those project/timeline selectors; `project` aliases
145
+ `projectName`. Do not pass an alias and its canonical selector together.
146
+
147
+ Copying clones the holder, its track association and its composition with new
148
+ UUIDs. An occupied destination requires `replace:true`; the source is unchanged.
149
+ Omit `inputs` to copy the preset bytes unchanged. To adjust an existing preset,
150
+ use `set_subtitle_preset` with target selectors and `inputs`.
151
+
152
+ Colour channels are 0–1, `outlineEnabled` is 0 or 1, `size` is Fusion's
153
+ normalised text size (not points), `position` is Fusion Center `[x,y]`, and
154
+ `thickness` is the template's HOutlineThickness value. Parameter editing is
155
+ restricted to the inspected Word Highlight template and literal Template
156
+ TextPlus inputs. Connected/animated inputs and unsupported layouts are refused.
157
+ Missing input values in the listing mean Resolve/template defaults, not zero.
158
+ Other supported composition envelopes can be cloned unchanged, but cannot have
159
+ their parameter meanings guessed. Preset holder Duration is preserved from the
160
+ reference (observed 150); it is not extended to caption-track length.
161
+
162
+ ## Evidence and outstanding live acceptance
163
+
164
+ On 2026-10-02 a read-only probe on Studio 21.1.0.17/Windows confirmed the API
165
+ boundary on an 11-caption timeline. Saved local project inspection confirmed
166
+ the caption encoding and Word Highlight's nested zlib inputs. Synthetic tests
167
+ exercise both codecs, cloning, transactional rollback, frame-rate arithmetic,
168
+ guard refusal, backup readback and preservation of unrelated tracks.
169
+
170
+ On 2026-10-06, a synthetic-only caption edit → full quit → database write →
171
+ relaunch → BurnIn render acceptance passed on Studio 21.1.0.17 / Windows.
172
+ Seven generated captions were edited through the actual `project_db` handler:
173
+ six replacements, one deletion and one addition, with explicit word boundaries.
174
+ A fresh native Word Highlight preset was created in the disposable project;
175
+ no personal project or media was used as a preset reference.
176
+
177
+ The 21-second 1080x1920 / 30 fps render contained 630 frames. All 420 caption
178
+ frames contained text, all seven word transitions matched the written timings,
179
+ and no caption pixels touched the frame edges or appeared in the gaps. The
180
+ rendered text was visually inspected for all seven captions in both highlight
181
+ states. Resolve's live text/bounds and saved word timing readback also matched.
182
+ The original frame check did not assert base text colour. The maintainer's
183
+ [acceptance review](https://github.com/samuelgursky/davinci-resolve-mcp/pull/273#issuecomment-6048433383)
184
+ found that the non-highlighted word remained pale yellow. The codec now targets
185
+ TextPlus shading inputs (`Red1`, `Green1`, `Blue1`, `Alpha1`) rather than the
186
+ macro's `Clone` controls. On 2026-10-08, the original track's Inspector confirmed
187
+ `#ffeb85` text. A fresh native Word Highlight, configured through Resolve's UI
188
+ with `#ffffff` text and `#ffff00` highlight, rendered white. After a full quit,
189
+ the corrected MCP write and relaunch also rendered white with Segoe UI Black,
190
+ centred position and black outline at thickness 0.1.
191
+
192
+ Both new 1080x1920 renders passed checks on all 630 decoded frames: all 420
193
+ caption frames had white base text and yellow highlighting, with exact word
194
+ transitions and no text in the gaps or at the frame edges. Bright fill pixels
195
+ were measured independently per word, and the original render failed the new
196
+ white-text assertion on all 420 caption frames. All seven captions were also
197
+ visually reviewed in both highlight states; the pixel check is not OCR. Selected
198
+ frame PNGs retain the full render resolution. Generated frames and reports are
199
+ kept outside the repository. The earlier green-pixel live harness was removed
200
+ because its pass flag did not establish word timing or text colour.
201
+
202
+ The earlier failed synthetic render prompted a codec regression fix: inserting
203
+ absent controls after a final input without a trailing comma produced invalid
204
+ Lua. The writer now prepends comma-terminated inputs, and a regression fixture
205
+ covers the missing separator. The new acceptance run exercises this path.
206
+
207
+ That live run also exposed Resolve's association constraints: DbIndex must be
208
+ nonnegative and its composite primary key uses ON CONFLICT REPLACE. The writer
209
+ now rebuilds only the selected track's Items vector within the transaction,
210
+ avoiding both rejected temporary indices and lost neighbours during reordering.
211
+ The regression fixture includes those constraints.
212
+
213
+ To repeat acceptance, use a disposable project and synthetic speech/media:
214
+ generate subtitles, apply Word Highlight with explicit colours, correct two
215
+ captions, add one and delete one, reopen Resolve, then render with
216
+ `ExportSubtitle:true, SubtitleFormat:"BurnIn"`. Inspect rendered frames across
217
+ every word boundary and at the frame edges. Readback alone cannot establish
218
+ visible highlighting, absence of blank frames, or safe margins. Assert the
219
+ non-highlighted text colour separately from the highlighted word, check every
220
+ decoded frame against the written word intervals, and check the caption gaps.
221
+ Review the rendered words visually too: pixel colour checks are not OCR.
222
+
223
+ Also still unverified: holder Duration semantics, per-caption preset rows,
224
+ whether/when UI edits are retimed on save, and SRT import preserving word
225
+ timings on 21.1. A previously reported ImportMedia(SRT)+AppendToTimeline route
226
+ on 21.0.4.5 is not evidence that SRT carries word timings on another build.
227
+ Repeat the probe and render test after Resolve updates.
@@ -1,5 +1,10 @@
1
1
  # Audio / Fairlight Kernel
2
2
 
3
+ For caption text, per-word timing, additions/deletions and animated subtitle
4
+ presets, use advanced `project_db` after saving and fully quitting Resolve.
5
+ See [subtitle-track editing](../guides/subtitle-track-editing.md). The basic
6
+ `set_subtitle_style` action does not edit animated Fusion presets.
7
+
3
8
  The Audio / Fairlight kernel expands `timeline` into a safer audio-state,
4
9
  mapping, voice isolation, auto-sync, transcription, subtitle, and Fairlight
5
10
  boundary layer.
@@ -167,22 +167,22 @@ equivalent, blocking full automation.
167
167
  ### Per-subtitle text content and timing editing
168
168
 
169
169
  - **Object:** `TimelineItem (subtitle track)`
170
- - **Behavior:** TimelineItem on a subtitle track exposes only 21 standard transform/composite properties (Pan, Tilt, ZoomX, Opacity, Crop, etc.). There are no methods to get or set subtitle text (GetText/SetText), start time, end time, or duration for individual subtitle items. Subtitles created via CreateSubtitlesFromAudio or imported via the Resolve UI cannot have their content or timing read or modified programmatically. Verified via dir() and GetProperty() on Resolve 21.0.0.48.
171
- - **Workaround / current handling:** No workaround exists — subtitle text and timing are completely inaccessible from the scripting API. Must be edited in the Resolve UI.
170
+ - **Behavior:** GetName, GetStart and GetEnd read caption names and bounds, and Timeline.DeleteClips can delete captions. There are no public methods to set caption text or per-word timings. Read-only live probe on Studio 21.1.0.17 (Windows, 2026-10-02): GetType=generator, GetProperty returned an empty dict, GetFusionCompCount=0, and dir() exposed no caption/text methods; the supplied reproduction also reports false GetProperty/SetProperty results. A subtitle SetProperty request must be refused before timeline auto-archiving. In the supplied 21.1 reproduction, UI text edits leave Name stale and move original AI words/times to protobuf f18/19/20; current f14/15/16 may contain invalid intervals. Whether a later save re-times them is not established here.
171
+ - **Workaround / current handling:** Use advanced project_db list_captions / write_captions / check_captions on a local SQLite project with Resolve fully quit. Current text comes from f14, not stale Name. f15/16 and f19/20 use 60 ticks/sec; f21 is the original caption frame anchor. The tool returns timeline-frame times rebased to the item's current Start. Writes need explicit word times, preserve unknown fields, reject gaps/zero durations and re-index Items atomically, with backup and readback. This is an unsupported DB workaround; render/reopen validation of the implementation remains required before release.
172
172
  - **Tags:** missing-method, subtitle, text, timing
173
173
 
174
174
  ### Subtitle track styling and presets
175
175
 
176
176
  - **Object:** `TimelineItem / Timeline / Project`
177
- - **Behavior:** There is no API method to set or query subtitle font family, font size, text color, background color, outline, shadow, position, alignment, or to apply/query subtitle style presets. TimelineItem.GetProperty() on subtitle items returns only transform/composite keys. Timeline.GetSetting() and Project.GetSetting() return None for all probed subtitle-style keys (e.g. 'subtitleFontName', 'subtitleFontSize', 'subtitleTextColor', 'subtitleBackgroundColor', 'subtitlePosition', 'subtitleAlignment', 'subtitlePreset', 'subtitleStyle'). Verified via dir(), GetProperty(), and GetSetting() on Resolve 21.0.0.48.
178
- - **Workaround / current handling:** No API workaround exists, but the style IS reachable below the API: it lives in Sm2TiTrack.FieldsBlob for Type=2 tracks, as an EffectFiltersBA payload whose effect 136 carries a Qt QFont descriptor (param 18) and a normalised position vector (param 17). Exposed as project_db list_subtitle_styles / set_subtitle_style (font family/size/weight/italic + position). Confirmed live on BOTH editions 2026-08-06 — Studio 19.1.3 and free 21.0.3, identical behaviour: Resolve opens a patched track and re-serialises it back to its own zstd form with the patched values intact, so it genuinely parses the write. Caveats: whole-TRACK style not per-caption, project must be CLOSED, Resolve must be fully quit and relaunched afterwards, and the track must already carry a style blob (a freshly added subtitle track has none until it is styled once in the UI). Burn-in overlays via Fusion titles remain a visual alternative but do not produce subtitle tracks.
177
+ - **Behavior:** There is no API method to set or query subtitle font family, font size, text color, background color, outline, shadow, position, alignment, or to apply/query subtitle style presets. Older probes returned only transform/composite keys; a read-only Studio 21.1.0.17 probe returned an empty dict. Timeline.GetSetting() and Project.GetSetting() return None for all probed subtitle-style keys (e.g. 'subtitleFontName', 'subtitleFontSize', 'subtitleTextColor', 'subtitleBackgroundColor', 'subtitlePosition', 'subtitleAlignment', 'subtitlePreset', 'subtitleStyle'). Verified via dir(), GetProperty(), and GetSetting() on Resolve 21.0.0.48.
178
+ - **Workaround / current handling:** No API workaround exists, but the style IS reachable below the API: it lives in Sm2TiTrack.FieldsBlob for Type=2 tracks, as an EffectFiltersBA payload whose effect 136 carries a Qt QFont descriptor (param 18) and a normalised position vector (param 17). Exposed as project_db list_subtitle_styles / set_subtitle_style (font family/size/weight/italic + position). Confirmed live on BOTH editions 2026-08-06 — Studio 19.1.3 and free 21.0.3, identical behaviour: Resolve opens a patched track and re-serialises it back to its own zstd form with the patched values intact, so it genuinely parses the write. Caveats: whole-TRACK style not per-caption, project must be CLOSED, Resolve must be fully quit and relaunched afterwards, and the track must already carry a style blob (a freshly added subtitle track has none until it is styled once in the UI). Burn-in overlays via Fusion titles remain a visual alternative but do not produce subtitle tracks. Animated Word Highlight is DIFFERENT: list_subtitle_styles may report styled:false while an animation is present. Sm2TiItem_Sm2TiTrack links a Sm2TiVideoClip holder via FusionCompHolderItems to a Sm2TiCompositionTable. The supplied Studio 21.1.0.17 reproduction verified cloning these three rows; local read-only inspection corroborated the format. Use list_subtitle_presets / copy_subtitle_preset, or set_subtitle_preset for named Word Highlight inputs inside the nested zlib tool section. These new actions check that Resolve is fully quit BEFORE writing. Holder Duration is preserved; its rendering semantics and per-caption preset layouts remain unverified.
179
179
  - **Tags:** missing-method, subtitle, style, preset
180
180
 
181
181
  ### Speech recognition engine selection and SRT import
182
182
 
183
183
  - **Object:** `Timeline`
184
- - **Behavior:** Timeline.CreateSubtitlesFromAudio(autoCaptionSettings) always uses the built-in Resolve speech recognition engine. There is no API parameter to select an alternative provider (e.g. whisper-cli, Google Speech, AWS Transcribe). The language selection via resolve.AUTO_CAPTION_LANGUAGE_* is the only customization; the engine itself cannot be changed. Furthermore, there is no API method to import an SRT file into a subtitle track programmatically — File -> Import -> Subtitle is UI-only.
185
- - **Workaround / current handling:** No workaround exists for provider selection or SRT import. External transcripts must be converted to SRT and imported through the Resolve UI.
184
+ - **Behavior:** Timeline.CreateSubtitlesFromAudio(autoCaptionSettings) always uses the built-in Resolve speech recognition engine. There is no API parameter to select an alternative provider (e.g. whisper-cli, Google Speech, AWS Transcribe). The language selection via resolve.AUTO_CAPTION_LANGUAGE_* is the only customization; the engine itself cannot be changed. There is no dedicated subtitle import method. However, MediaPool.ImportMedia(srt) + AppendToTimeline was reported working on Studio 21.0.4.5 (issue #169). Whether that route carries word timing for animated presets on 21.1 has not been verified; SRT contains only cue timing.
185
+ - **Workaround / current handling:** Use Resolve's engine for CreateSubtitlesFromAudio. For external SRT, test ImportMedia + AppendToTimeline on the exact installed build or import through the UI. Use project_db write_captions for explicit word timings with Resolve fully quit; do not assume SRT supplies them.
186
186
  - **Tags:** missing-method, subtitle, transcription, speech-recognition, asr
187
187
 
188
188
  ### Media Pool folder rename
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.9.3"
40
+ VERSION = "4.10.0"
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": "4.9.3",
3
+ "version": "4.10.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -107,6 +107,12 @@ Each dispatches on an `action`. Highlights:
107
107
  Includes `list_subtitle_styles` / `set_subtitle_style` — caption font family/size/weight/italic
108
108
  and normalised position, which the scripting API cannot touch at all. Whole-track (not
109
109
  per-caption); project must be CLOSED and Resolve fully quit + relaunched afterwards.
110
+ Animated presets use separate `list_subtitle_presets` / `copy_subtitle_preset` /
111
+ `set_subtitle_preset` actions; caption text and word timing use `list_captions` /
112
+ `write_captions` / `check_captions`. These SQLite writes enforce full Resolve
113
+ quit before editing, take unique backups and verify in a transaction.
114
+ See [subtitle-track editing](../docs/guides/subtitle-track-editing.md) for
115
+ parameters, Word Highlight controls and the pending live-render acceptance.
110
116
  - **`pipeline`** — the DB-as-truth pipeline foundation (see below).
111
117
  - **`deliverable`** — deliverable QC / compliance: `deliverable_qc` (ffprobe a render vs its spec →
112
118
  pass/fail per field), `loudness_qc` (ebur128 LUFS/true-peak/LRA), `reframe_blanking_check`,
@@ -6,12 +6,14 @@
6
6
  * verify. The schema map is Resolve 21 / ProjectVersion 17 (the design notes design notes);
7
7
  * the column guard refuses rather than corrupting if the schema differs.
8
8
  *
9
- * Needs the optional native dep `better-sqlite3` (lazy).
9
+ * Uses optional better-sqlite3 or node:sqlite (lazy, compatible adapter).
10
10
  */
11
11
 
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import os from 'node:os';
15
+ import childProcess from 'node:child_process';
16
+ import { randomUUID } from 'node:crypto';
15
17
  import { createRequire } from 'node:module';
16
18
 
17
19
  const require = createRequire(import.meta.url);
@@ -41,9 +43,8 @@ export const PROJECT_LIBRARY_ROOT = path.join(os.homedir(), 'Library/Application
41
43
  * Project.db found", sending exactly the users the in-app bridge exists to serve
42
44
  * hunting for a path they have no reason to know.
43
45
  *
44
- * macOS-only: the sandbox container is an Apple construct. Where the free
45
- * edition keeps its library on Windows and Linux is NOT verified here, so
46
- * nothing is guessed for those platforms — pass `projectDb` explicitly there.
46
+ * macOS-only: the sandbox container is an Apple construct. Windows/Linux
47
+ * use the platform roots returned by projectLibraryRoots below.
47
48
  */
48
49
  export const LITE_DB_ROOT = path.join(
49
50
  os.homedir(),
@@ -51,7 +52,16 @@ export const LITE_DB_ROOT = path.join(
51
52
  );
52
53
 
53
54
  /** Every root searched when resolving a project by name, Studio first. */
54
- export const DB_ROOTS = [DISK_DB_ROOT, PROJECT_LIBRARY_ROOT, LITE_DB_ROOT];
55
+ export function projectLibraryRoots(platform = process.platform, home = os.homedir(), env = process.env) {
56
+ const join = platform === 'win32' ? path.win32.join : path.posix.join;
57
+ const libraries = (base) => ['Resolve Project Library', 'Resolve Disk Database']
58
+ .map((name) => join(base, name, 'Resolve Projects'));
59
+ if (platform === 'win32') return libraries(join(env.APPDATA || join(home, 'AppData', 'Roaming'),
60
+ 'Blackmagic Design', 'DaVinci Resolve', 'Support'));
61
+ if (platform === 'linux') return libraries(join(home, '.local', 'share', 'DaVinciResolve'));
62
+ return [DISK_DB_ROOT, PROJECT_LIBRARY_ROOT, LITE_DB_ROOT];
63
+ }
64
+ export const DB_ROOTS = projectLibraryRoots();
55
65
 
56
66
  /**
57
67
  * Filter roots to the ones that answer a readdir within `deadlineMs`.
@@ -92,7 +102,75 @@ export async function responsiveRoots(roots = DB_ROOTS, deadlineMs = 3000) {
92
102
  }
93
103
 
94
104
  export function loadSqlite() {
95
- return requireBetterSqlite3('Project.db patching');
105
+ // A successful require alone doesn't detect a native ABI mismatch: the
106
+ // bindings are loaded by the constructor. Probe before selecting a backend.
107
+ try {
108
+ const Database = requireBetterSqlite3('Project.db patching');
109
+ const probe = new Database(':memory:');
110
+ probe.close();
111
+ return Database;
112
+ } catch (nativeError) {
113
+ // DatabaseSync shipped in 22.13, but snapshotBackup needs sqlite.backup(),
114
+ // added in 22.16 (23.8 on the 23 line). Without it a write would fail only
115
+ // after opening the database, with "backup is not a function".
116
+ let builtin;
117
+ try { builtin = require('node:sqlite'); } catch { builtin = null; }
118
+ if (typeof builtin?.backup !== 'function') {
119
+ throw new Error(`${nativeError.message} Alternatively use Node 22.16+ (or 23.8+), whose node:sqlite provides backup().`);
120
+ }
121
+ return BuiltinSqlite;
122
+ }
123
+ }
124
+
125
+ // Small better-sqlite3-compatible surface used by Project.db consumers. Keep
126
+ // rows as ordinary objects and blobs as Buffers on either backend.
127
+ export class BuiltinSqlite {
128
+ constructor(filename, { readonly = false } = {}) {
129
+ const { DatabaseSync } = require('node:sqlite');
130
+ this.db = new DatabaseSync(filename, { readOnly: readonly, enableForeignKeyConstraints: false });
131
+ }
132
+ exec(sql) { this.db.exec(sql); return this; }
133
+ close() { this.db.close(); }
134
+ prepare(sql) {
135
+ const stmt = this.db.prepare(sql);
136
+ const row = (r) => r && Object.fromEntries(Object.entries(r)
137
+ .map(([k, v]) => [k, v instanceof Uint8Array ? Buffer.from(v) : v]));
138
+ return { get: (...args) => row(stmt.get(...args)),
139
+ all: (...args) => stmt.all(...args).map(row), run: (...args) => stmt.run(...args) };
140
+ }
141
+ transaction(fn) {
142
+ return (...args) => {
143
+ this.exec('BEGIN IMMEDIATE');
144
+ try { const result = fn(...args); this.exec('COMMIT'); return result; }
145
+ catch (error) { this.exec('ROLLBACK'); throw error; }
146
+ };
147
+ }
148
+ backup(filename) { return require('node:sqlite').backup(this.db, filename); }
149
+ }
150
+
151
+ /** Verify the process is gone; a caller assertion alone is not sufficient. */
152
+ export function requireResolveQuit(opts) {
153
+ requireClosed(opts);
154
+ if (resolveRunning()) throw new Error('Fully QUIT Resolve before subtitle database writes (Resolve is running).');
155
+ }
156
+
157
+ function resolveRunning() {
158
+ const win = process.platform === 'win32';
159
+ const result = childProcess.spawnSync(win ? 'tasklist.exe' : 'ps',
160
+ win ? ['/FO', 'CSV', '/NH'] : ['-A', '-o', 'comm='],
161
+ { encoding: 'utf8', timeout: 5000, windowsHide: true });
162
+ if (result.error || result.status !== 0) throw new Error('Cannot verify Resolve is quit; refusing database write.');
163
+ return result.stdout.split(/\r?\n/).some((line) => win
164
+ ? /^"Resolve\.exe",/i.test(line.trim())
165
+ : /(^|\/)resolve(?:\.exe)?$/i.test(line.trim()));
166
+ }
167
+
168
+ /** SQLite snapshot includes committed WAL pages; never overwrite an older backup. */
169
+ export async function snapshotBackup(dbPath) {
170
+ const filename = `${dbPath}.subtitle-${Date.now()}-${randomUUID()}.bak`;
171
+ const db = openGuarded(dbPath);
172
+ try { await db.backup(filename); } finally { db.close(); }
173
+ return filename;
96
174
  }
97
175
 
98
176
  /**
@@ -137,7 +215,7 @@ export function resolveDbPath({ projectDb, projectName, roots, skippedRoots = []
137
215
  'are invisible until macOS unwedges it. '
138
216
  : '') +
139
217
  'If Resolve keeps its projects elsewhere — a relocated library, a network/Postgres ' +
140
- 'database, or the free edition on Windows/Linux — pass projectDb with the full path.',
218
+ 'database — pass projectDb with the full local SQLite path (Postgres is not a Project.db file).',
141
219
  );
142
220
  }
143
221
  if (hits.length > 1) {
@@ -153,19 +231,22 @@ export function openGuarded(dbPath, { writable = false, table, column } = {}) {
153
231
  const Database = loadSqlite();
154
232
  if (!fs.existsSync(dbPath)) throw new Error(`Project.db not found: ${dbPath}`);
155
233
  const db = new Database(dbPath, { readonly: !writable });
156
- if (table && column) {
157
- // Quote the table identifier — Resolve tables like "ListMgt::LmVersion" contain "::" which is an
158
- // illegal token unquoted (the PRAGMA would fail with "unrecognized token: :").
159
- const cols = db
160
- .prepare(`PRAGMA table_info("${String(table).replace(/"/g, '""')}")`)
161
- .all()
162
- .map((c) => c.name);
163
- if (!cols.includes(column)) {
164
- db.close();
165
- throw new Error(`${table}.${column} not found — unsupported Project.db schema/version; refusing to patch.`);
234
+ try {
235
+ if (table && column) {
236
+ // Quote identifiers: Resolve table names can contain "::".
237
+ const cols = db
238
+ .prepare(`PRAGMA table_info("${String(table).replace(/"/g, '""')}")`)
239
+ .all()
240
+ .map((c) => c.name);
241
+ if (!cols.includes(column)) {
242
+ throw new Error(`${table}.${column} not found — unsupported Project.db schema/version; refusing to patch.`);
243
+ }
166
244
  }
245
+ return db;
246
+ } catch (error) {
247
+ db.close();
248
+ throw error;
167
249
  }
168
- return db;
169
250
  }
170
251
 
171
252
  export function backup(dbPath) {