davinci-resolve-mcp 2.69.3 → 2.70.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.0
14
14
 
15
- **Totals:** 20 missing capabilities, 13 bugs / unreliable behaviors.
15
+ **Totals:** 21 missing capabilities, 20 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
@@ -41,6 +41,13 @@ Python bridge fabricates a callable for any attribute name (see the
41
41
  Functionality that exists in the Resolve UI but has no scripting API
42
42
  equivalent, blocking full automation.
43
43
 
44
+ ### Project.SetSetting('timelinePlaybackFrameRate')
45
+
46
+ - **Object:** `Project`
47
+ - **Behavior:** Returns False for every value form tried (string, int, float), both before and after a timeline exists, so the playback frame rate cannot be set from the API at all. Reported by a community contributor against Resolve Studio while assembling a vertical timeline (PR #99).
48
+ - **Workaround / current handling:** Ask the user to set it in Project Settings > Master Settings > Playback frame rate as a SETUP step, before any timeline exists. Read it back to confirm; do not report it as set on the strength of the call alone.
49
+ - **Tags:** project-settings, silent-failure, timeline
50
+
44
51
  ### Timeline.GetTimelineByName
45
52
 
46
53
  - **Object:** `Project`
@@ -226,9 +233,47 @@ values, or automation-hostile modal prompts.
226
233
 
227
234
  - **Object:** `ProjectManager`
228
235
  - **Signature:** `(projectName) -> bool`
229
- - **Behavior:** Returns False (no deletion) when the target project is, or recently was, the current project, and is flaky on the first attempt — so a single bool() call leaves the project undeleted with no useful error.
230
- - **Workaround / current handling:** Load/close away from the target first, then retry; use src/utils/project_cleanup.py:delete_project_safely.
231
- - **Tags:** unreliable-return, project, flaky
236
+ - **Behavior:** Returns False (no deletion) when the target project is, or recently was, the current project, and is flaky on the first attempt — so a single bool() call leaves the project undeleted with no useful error. CloseProject reliably releases it: a delete that had failed six times in a row, a second apart, succeeded on the first attempt after one. Switching away with LoadProject is NOT reliable — it left the delete failing permanently after a heavily-used project, yet succeeded for a project that had only been created and loaded. Whatever distinguishes the two (modification? an open timeline?) is not established, so LoadProject-away cannot be depended on.
237
+ - **Workaround / current handling:** CloseProject the target FIRST (that is what releases it), then LoadProject some named project so the session is not left on the unsaveable 'Untitled Project' fallback, then delete. Use src/utils/project_cleanup.py:delete_project_safely, which does exactly that.
238
+ - **Tags:** unreliable-return, project, flaky, session-lock
239
+
240
+ ### ProjectManager.GetCurrentDatabase
241
+
242
+ - **Object:** `ProjectManager`
243
+ - **Signature:** `() -> {DbType, DbName}`
244
+ - **Behavior:** Returns None when Resolve has come up without attaching to a project database — a state it reaches after an unclean shutdown, and does not recover from on its own. In that state the application looks entirely healthy: it accepts scripting connections, and GetProductName, GetVersionString, GetCurrentPage and GetCurrentProject all answer normally. But CreateProject and LoadProject return False indefinitely, SaveProject returns None, and some calls block forever in the scripting transport rather than returning at all. Observed headless on Studio 19.1.3.7 after force-killing a wedged instance; the replacement took 2m05s to become scriptable and came up with no database.
245
+ - **Workaround / current handling:** Treat a non-None GetCurrentDatabase() as the liveness check, not a successful connection — every cheaper check passes in the wedged state. `resolve_control runtime_mode` reports `database_attached` for exactly this. There is no repair: quit and restart Resolve.
246
+ - **Tags:** project, database, headless, silent-failure, startup
247
+
248
+ ### MediaPool.ImportTimelineFromFile
249
+
250
+ - **Object:** `MediaPool`
251
+ - **Signature:** `(filePath, {importOptions}) -> Timeline`
252
+ - **Behavior:** Returns None — no error, no exception — when the requested `timelineName` already exists. Measured: importing one file three times with the same name succeeded once and returned None twice. An iterative loop that reuses a fixed name therefore works exactly once and then silently does nothing. DRT ignores importOptions entirely (timelineName, importSourceClips and sourceClipsFolders are all invalid for it): the timeline is named after the FILE, repeats auto-uniquify ('iter', 'iter 2', 'iter 3'), and because importSourceClips cannot be disabled each DRT import adds another copy of the source media to the pool. OTIO is the one format that will NOT relink from the pool — with importSourceClips=False its timeline rebuilds with correct structure and every item OFFLINE, in both GUI and headless.
253
+ - **Workaround / current handling:** For a repeatable loop use FCP7 XML or AAF with a UNIQUE timelineName per iteration plus importSourceClips=False and sourceClipsFolders=[root] — verified frame-exact over five consecutive imports with no media duplicated. Use DRT for one-shot hand-offs only. For OTIO, pass importSourceClips=True with a sourceClipsPath. See docs/guides/headless-edit-loop.md.
254
+ - **Tags:** timeline, import, interchange, silent-failure, conform
255
+
256
+ ### Timeline.Export(EXPORT_FCPXML_1_10)
257
+
258
+ - **Object:** `Timeline`
259
+ - **Signature:** `(fileName, EXPORT_FCPXML_1_10, EXPORT_NONE) -> bool`
260
+ - **Behavior:** Returns True and creates a BUNDLE DIRECTORY at the given path containing `Info.fcpxml`, rather than a file — the `.fcpxmld` shape Final Cut uses. Two consequences bite immediately: a `stat().st_size` check reads the directory inode (96 bytes here) and concludes the export is empty, and ImportTimelineFromFile on that path fails because it is a directory. Every other EXPORT_* type in this build writes a plain file, so code that treats them uniformly gets this one wrong. Pointed at the inner member, the format round-trips frame-exactly in both modes.
261
+ - **Workaround / current handling:** After exporting, check `Path(p).is_dir()` and import `next(Path(p).glob('*.fcpxml'))` instead. Do not size-check the export path itself.
262
+ - **Tags:** timeline, export, interchange, fcpxml, silent-failure
263
+
264
+ ### MediaPool.CreateTimelineFromClips
265
+
266
+ - **Object:** `MediaPool`
267
+ - **Behavior:** Fails with a bare 'Failed to create timeline from clip_infos' — naming nothing actionable — when the Media Pool's CURRENT folder is not the folder holding the clips, even though every clip_id passed is valid and resolvable. Reported against Resolve Studio in PR #99.
268
+ - **Workaround / current handling:** Call MediaPool.SetCurrentFolder() to the clips' bin before creating the timeline (media_pool set_current_folder). Valid ids are not sufficient.
269
+ - **Tags:** media-pool, timeline, unhelpful-error
270
+
271
+ ### TimelineItem.AddFusionComp / LoadFusionCompByName
272
+
273
+ - **Object:** `TimelineItem (media-backed clip)`
274
+ - **Behavior:** A Fusion composition created on a media clip through the API is not applied at render WHEN MEDIAOUT HAS NO PATH FROM MEDIAIN. The original blanket form of this entry — 'never applied at render' — was too broad and was corrected on 2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created entirely through the API on an ordinary media clip, DOES render. PSNR between the plain and Fusion renders of the same timeline was 22.7 dB (identical would be infinite), the file shrank 22.5 MB -> 14.8 MB as a blur should, and the output was frame-for-frame identical in GUI and headless. A first attempt that wired ONLY MediaOut -> Blur, leaving the Blur with no source, made the render job come back 'Failed' with an 887-byte file — so an unrooted graph does not merely get bypassed, it can take the render down. What still stands is the original observation for the configuration it actually tested, which is retained below and has NOT been re-measured: AddFusionComp() returns the comp, AddTool/Connect/SetInput all succeed, and the whole graph reads back correctly (GetCompCount 1, MediaOut1.Input wired to the new tool, StyledText returning the value just set) — but the rendered output is byte-for-byte the untouched source media. Verified live on Studio 19.1.3.7 with the strongest form of the test: MediaOut1 fed ONLY by a Text+, with no path from MediaIn at all, still rendered the unmodified clip. LoadFusionCompByName on the sole comp does not activate it either. Contrast InsertFusionTitleIntoTimeline, whose comp DOES render — text set via SetInput('StyledText') appears in the output — so this is specific to comps attached to media-backed clips, not to Fusion through the API generally.
275
+ - **Workaround / current handling:** Wire the graph so MediaOut descends from MediaIn — that is the difference between a comp that renders and one that is silently bypassed, and it is what made this look like 'Fusion never renders from the API'. Never leave a tool unrooted: a MediaOut fed by a tool with no source failed the render job outright. For text or effects over picture, insert a Fusion title/generator as its own timeline clip and set its Text+ (fusion_comp set_text_plus), rather than attaching a comp to the media clip. Note the destination track cannot be chosen from the API (see the Track Selector entry), so overlaying onto an existing clip's track is not currently reachable end-to-end. Building the comp in the Fusion page UI works; only the API-created comp is ignored.
276
+ - **Tags:** fusion, silent-failure, render
232
277
 
233
278
  ### Composition.Paste
234
279
 
@@ -291,3 +336,19 @@ values, or automation-hostile modal prompts.
291
336
  - **Workaround / current handling:** Normalize both arguments through the live maps before calling: src.utils.render_ids.render_format_id_from_formats and render_codec_id_from_codecs accept a description or an id and return the id.
292
337
  - **Reference:** [issue #59](https://github.com/samuelgursky/davinci-resolve-mcp/issues/59)
293
338
  - **Tags:** render, deliver, silent-failure, id-vs-label
339
+
340
+ ### ProjectManager.SaveProject
341
+
342
+ - **Object:** `ProjectManager`
343
+ - **Signature:** `() -> bool`
344
+ - **Behavior:** On the default, never-saved project named 'Untitled Project' this call CANNOT succeed — the project has no location and there is no SaveProjectAs to give it one — and the two modes fail differently. In the GUI it returns False. HEADLESS IT BLOCKS FOREVER: measured on a cold -nogui boot with the database verified attached immediately beforehand, no return after 45s, the client parked in Fusion::RemoteApp::WaitPkt. Resolve wants a Save-As dialog and waits for an answer that cannot arrive. It also degrades the instance: after the hung call was interrupted, SaveProject began returning None instantly on that instance. On a named project it is fine in both modes.
345
+ - **Workaround / current handling:** Guard it: only call SaveProject when GetCurrentProject().GetName() != 'Untitled Project'. There is nothing to save on the default project and the call cannot succeed, so skipping it loses nothing. Do NOT reach for headless to dodge the GUI's save dialog — headless turns that dialog into an unbounded hang that no human can clear.
346
+ - **Tags:** project, modal, headless, hang, silent-failure, unreliable-return
347
+
348
+ ### GalleryStillAlbum.ExportStills
349
+
350
+ - **Object:** `GalleryStillAlbum`
351
+ - **Signature:** `(galleryStills, folderPath, filePrefix, format) -> bool`
352
+ - **Behavior:** PANEL-dependent, not mode-dependent — and the distinction took three revisions of this entry to pin down, so the evidence is recorded rather than summarised. Across four controlled 92-probe sweeps (2 GUI, 2 headless, 2026-08-01) it returned False and wrote nothing in ALL FOUR, while Timeline.GrabStill() succeeded in all four — so it is not being handed an empty still. In one earlier GUI session it DID work, returning True and writing 2 files. The variable that differed is not the mode: it is whether the Gallery panel was visible on the Color page, which depends on the restored workspace layout and which the harness does not control. A headless session can never satisfy it, so in practice the call never works headless; a GUI session satisfies it only sometimes. Project.ExportCurrentFrameAsStill worked in all four runs in both modes.
353
+ - **Workaround / current handling:** Do not use ExportStills unattended in either mode — a GUI session is not sufficient, only a GUI session with the Gallery panel open. Use Project.ExportCurrentFrameAsStill for pixels (verified in both modes, four for four) or drp.extract_node_graphs for grades. If ExportStills must be used, have the user open Workspace > Gallery first and verify the written files rather than trusting the return.
354
+ - **Tags:** gallery, stills, headless, unreliable-return
@@ -0,0 +1,56 @@
1
+ # GUI vs headless probe matrix
2
+
3
+ 105 probes, 2 GUI run(s) x 2 headless run(s).
4
+
5
+ | verdict | count |
6
+ | --- | --- |
7
+ | parity | 86 |
8
+ | flaky | 0 |
9
+ | hang_headless | 0 |
10
+ | hang_gui | 0 |
11
+ | headless_degraded | 2 |
12
+ | gui_degraded | 0 |
13
+ | both_hang | 0 |
14
+ | both_failed | 14 |
15
+ | divergent | 3 |
16
+ | untested | 0 |
17
+
18
+ ## Coverage by category
19
+
20
+ | category | probes | parity | hang_headless | headless_degraded | gui_degraded | divergent | untested |
21
+ | --- | --- | --- | --- | --- | --- | --- | --- |
22
+ | app | 8 | 6 | 0 | 0 | 0 | 0 | 0 |
23
+ | audio | 3 | 3 | 0 | 0 | 0 | 0 | 0 |
24
+ | color | 9 | 9 | 0 | 0 | 0 | 0 | 0 |
25
+ | editorial | 5 | 0 | 0 | 2 | 0 | 1 | 0 |
26
+ | fusion | 2 | 2 | 0 | 0 | 0 | 0 | 0 |
27
+ | gallery | 4 | 3 | 0 | 0 | 0 | 0 | 0 |
28
+ | interchange | 5 | 5 | 0 | 0 | 0 | 0 | 0 |
29
+ | layout | 4 | 4 | 0 | 0 | 0 | 0 | 0 |
30
+ | lifecycle | 3 | 3 | 0 | 0 | 0 | 0 | 0 |
31
+ | longop | 4 | 1 | 0 | 0 | 0 | 0 | 0 |
32
+ | media_pool | 8 | 5 | 0 | 0 | 0 | 0 | 0 |
33
+ | pages | 7 | 7 | 0 | 0 | 0 | 0 | 0 |
34
+ | project_manager | 8 | 8 | 0 | 0 | 0 | 0 | 0 |
35
+ | project_settings | 5 | 5 | 0 | 0 | 0 | 0 | 0 |
36
+ | projectlevel | 4 | 0 | 0 | 0 | 0 | 2 | 0 |
37
+ | render | 6 | 6 | 0 | 0 | 0 | 0 | 0 |
38
+ | timeline | 12 | 11 | 0 | 0 | 0 | 0 | 0 |
39
+ | timeline_item | 8 | 8 | 0 | 0 | 0 | 0 | 0 |
40
+
41
+ ## Mode-dependent findings
42
+
43
+ > **Re-verify every row below in isolation before believing it.** Probes run in catalogue order against one shared fixture, so a probe near the end has ~90 destructive probes' worth of accumulated state behind it, and the two modes can drift apart for reasons that have nothing to do with the mode. Measured: five findings here — nested timelines, clip linking, take selectors, multi-job queues and render presets — all reproduced as mode differences in a full sweep and all came out **identical** when re-run with `--only`. Isolation is the test:
44
+
45
+ > ```
46
+ > python scripts/mode_matrix.py run --mode gui --out iso_gui.jsonl --only <probe>
47
+ > python scripts/mode_matrix.py run --mode headless --out iso_hl.jsonl --only <probe>
48
+ > ```
49
+
50
+ | probe | verdict | GUI | headless | note |
51
+ | --- | --- | --- | --- | --- |
52
+ | `editorial.nested_timeline` | headless_degraded | ok: 'True/1' | raised | succeeded with a UI, failed without one |
53
+ | `editorial.set_clips_linked` | headless_degraded | ok: True | none: None | succeeded with a UI, failed without one |
54
+ | `editorial.take_selector` | divergent | ok: 'False/0/None/None' | ok: 'None/None/None/None' | both returned, values differ |
55
+ | `projectlevel.multi_job_queue` | divergent | ok: '3/3' | ok: '0/0' | both returned, values differ |
56
+ | `projectlevel.render_preset_roundtrip` | divergent | ok: 'True/True' | ok: 'None/None' | both returned, values differ |