davinci-resolve-mcp 2.103.5 → 2.104.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.
- package/CHANGELOG.md +82 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +41 -1
- package/docs/reference/readwrite-symmetry.md +2 -6
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +302 -4
- package/src/utils/api_truth.py +101 -5
- package/src/utils/resolve_versions.py +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
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.104.1
|
|
6
|
+
|
|
7
|
+
**The job metadata lies too — verify_output now cross-checks the timeline.**
|
|
8
|
+
Finishing v2.104.0's pending live validation exposed a hole in the new
|
|
9
|
+
`render.verify_output`: in the issue #164 case (content before the timeline
|
|
10
|
+
start) Resolve rewrites the render job's own MarkIn/MarkOut down to the
|
|
11
|
+
collapsed extent. Measured live on Studio 19.1.3.7: 96 frames of content
|
|
12
|
+
placed before the start turned an explicit 96-frame mark range into a 1-frame
|
|
13
|
+
job — Complete at 100%, a 1-frame black stub, and a clean duration ratio,
|
|
14
|
+
because the expected duration was computed from the job's own lying range.
|
|
15
|
+
|
|
16
|
+
The only truthful readback in that state is the timeline items themselves,
|
|
17
|
+
which report their real positions. `verify_output` now finds the job's
|
|
18
|
+
timeline and cross-checks it: video items starting before the timeline's
|
|
19
|
+
start frame warn (the #164 signature by direct evidence), and a mark range
|
|
20
|
+
under half the items' extent warns (the collapse signature). Callers can also
|
|
21
|
+
pass `expected_frames` or `expected_duration_seconds` outright. Live-verified
|
|
22
|
+
both ways on 19.1.3.7: the healthy render verifies clean, the stub now fails
|
|
23
|
+
with both warnings.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `render.verify_output` no longer trusts the job's MarkIn/MarkOut as the
|
|
28
|
+
expected duration — the #164 stub previously verified clean at ratio 1.0.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- The api_truth recordFrame entry now documents the mark-range rewrite and
|
|
33
|
+
the item-readback discriminator; the Blackmagic-facing report regenerated.
|
|
34
|
+
|
|
35
|
+
## What's New in v2.104.0
|
|
36
|
+
|
|
37
|
+
The read/write symmetry audit's worklist, worked. PR #162's AST rewrite left
|
|
38
|
+
eight `set_` actions with no readback; live probing on Studio 19.1.3.7 sorted
|
|
39
|
+
them into four the API supports and four it simply cannot — and turned up a
|
|
40
|
+
render-pipeline failure mode along the way.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- `media_pool.get_clip_marks` — read mark in/out for a set of media-pool clips,
|
|
45
|
+
the read twin of `set_clip_marks` (live-verified round trip: set 12/60, read
|
|
46
|
+
12/60).
|
|
47
|
+
- `timeline.get_clips_linked` — per-item link readback via
|
|
48
|
+
`TimelineItem.GetLinkedItems` (live-verified: a video item returns its audio
|
|
49
|
+
twin).
|
|
50
|
+
- `timeline.get_title_text` — the read twin of `set_title_text`. Resolves the
|
|
51
|
+
same heuristic title-property keys as the setter, and falls back to reading
|
|
52
|
+
`StyledText` off the TextPlus tool in the item's Fusion comp — on Studio
|
|
53
|
+
19.1.3 the property route exposes no title keys at all (the setter fails
|
|
54
|
+
there too), while the comp route reads and writes fine.
|
|
55
|
+
- `media_pool_item_markers.get_name` — the markers group carried `set_name`
|
|
56
|
+
with no read twin.
|
|
57
|
+
- `render.verify_output(job_id)` — checks the actual output file against the
|
|
58
|
+
job's own mark range: existence, size, ffprobe duration, and a
|
|
59
|
+
duration-ratio warning when a Complete job produced a near-empty stub (the
|
|
60
|
+
issue #164 signature: content the render engine never visited). Verify
|
|
61
|
+
before deleting the job — deleted jobs carry no TargetDir to check.
|
|
62
|
+
|
|
63
|
+
### Documented (api_truth, Blackmagic-facing report regenerated)
|
|
64
|
+
|
|
65
|
+
Four readbacks the API cannot express, each now a submit-tagged entry:
|
|
66
|
+
`SetCDL` (no GetCDL anywhere — read grades via DRX decode instead),
|
|
67
|
+
`SetNodeEnabled` (no GetNodeEnabled), `SetKeyframeInterpolation` (nothing
|
|
68
|
+
returns interpolation; the whole keyframe family is absent on 19.1.3), and
|
|
69
|
+
`SetHighPriority` (no getter, irreversible per session). The symmetry report's
|
|
70
|
+
high-signal gap list is now exactly these four.
|
|
71
|
+
|
|
72
|
+
And one render-pipeline bug found the hard way: **deleting or closing a
|
|
73
|
+
project while its render job is running wedges Resolve** — the orphaned
|
|
74
|
+
render's `IsRenderingInProgress` sticks True on every subsequent project,
|
|
75
|
+
`StopRendering` does not clear it, new render jobs sit at 0% forever, then
|
|
76
|
+
`StartRendering` starts returning False and `Resolve.Quit()` is refused
|
|
77
|
+
behind a quit-confirm dialog. Reproduced live on Studio 19.1.3.7. Poll
|
|
78
|
+
`GetRenderJobStatus` for completion, never `IsRenderingInProgress`, and never
|
|
79
|
+
close a project mid-render.
|
|
80
|
+
|
|
81
|
+
### Version ledger
|
|
82
|
+
|
|
83
|
+
`MediaPoolItem.GetMarkInOut` and `TimelineItem.GetLinkedItems` enter the
|
|
84
|
+
evidence gates as measured-present on 19.1.3.7 (introduction versions
|
|
85
|
+
unbisected; the floors err toward refusing on older builds).
|
|
86
|
+
|
|
5
87
|
## What's New in v2.103.5
|
|
6
88
|
|
|
7
89
|
Two fixes that fell out of auditing the code around this week's releases — the
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.104.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:**
|
|
15
|
+
**Totals:** 33 missing capabilities, 39 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
|
|
@@ -239,6 +239,38 @@ equivalent, blocking full automation.
|
|
|
239
239
|
- **Workaround / current handling:** Check GetRenderCodecs(format) first; when it is empty, treat the format as unreachable through this API rather than guessing a codec value. Render audio-only via ExportVideo=False on a format that does expose codecs, or drive it from a saved render preset.
|
|
240
240
|
- **Tags:** render, deliver, audio, unsupported
|
|
241
241
|
|
|
242
|
+
### TimelineItem.SetCDL (write-only — no GetCDL anywhere)
|
|
243
|
+
|
|
244
|
+
- **Object:** `TimelineItem`
|
|
245
|
+
- **Signature:** `({NodeIndex, Slope, Offset, Power, Saturation}) -> Bool`
|
|
246
|
+
- **Behavior:** SetCDL writes a node's CDL but no object exposes a read: no GetCDL on TimelineItem or Graph in the API reference, and dir() on a live Graph confirms (Studio 19.1.3.7). A grade applied via SetCDL cannot be read back, diffed, or verified through the API.
|
|
247
|
+
- **Workaround / current handling:** Track intended CDL values in the caller, or read the actual grade by exporting a DRX still and decoding it (this repo's drx tool decodes 100% of DRX params — slope/offset/power/sat included).
|
|
248
|
+
- **Tags:** color, missing-method, readback
|
|
249
|
+
|
|
250
|
+
### Graph.SetNodeEnabled (write-only — no GetNodeEnabled)
|
|
251
|
+
|
|
252
|
+
- **Object:** `Graph`
|
|
253
|
+
- **Signature:** `(nodeIndex, bool) -> Bool`
|
|
254
|
+
- **Behavior:** A node's bypass state can be set but never read: no GetNodeEnabled in the API reference, and dir() on a live Graph confirms (Studio 19.1.3.7). After a SetNodeEnabled the caller cannot verify it took, and the pre-existing state of a node someone toggled in the UI is unknowable.
|
|
255
|
+
- **Workaround / current handling:** Treat node-enable state as write-only: record what you set, and verify visually (rendered-frame compare) when the state matters.
|
|
256
|
+
- **Tags:** color, missing-method, readback
|
|
257
|
+
|
|
258
|
+
### TimelineItem.SetKeyframeInterpolation (write-only)
|
|
259
|
+
|
|
260
|
+
- **Object:** `TimelineItem`
|
|
261
|
+
- **Signature:** `(property, frame, type) -> Bool`
|
|
262
|
+
- **Behavior:** Interpolation can be written per keyframe but nothing returns it: GetKeyframeAtIndex/GetPropertyAtKeyframeIndex expose frame and value only (API reference). On Studio 19.1.3.7 the whole keyframe method family is absent from dir() — these methods are 20.x+.
|
|
263
|
+
- **Workaround / current handling:** Record interpolation choices in the caller; readback is not available at any version.
|
|
264
|
+
- **Tags:** timeline, missing-method, readback, keyframes
|
|
265
|
+
|
|
266
|
+
### Resolve.SetHighPriority (write-only, irreversible per session)
|
|
267
|
+
|
|
268
|
+
- **Object:** `Resolve`
|
|
269
|
+
- **Signature:** `() -> Bool`
|
|
270
|
+
- **Behavior:** Raises the Resolve process priority; there is no getter and no way to lower it again through the API (confirmed absent from dir() on Studio 19.1.3.7).
|
|
271
|
+
- **Workaround / current handling:** Call it only when the user asked for a long render on a dedicated machine; state cannot be read back or undone without restarting Resolve.
|
|
272
|
+
- **Tags:** app-control, missing-method, readback
|
|
273
|
+
|
|
242
274
|
### TimelineItem.CreateMagicMask (needs operator clicks)
|
|
243
275
|
|
|
244
276
|
- **Object:** `TimelineItem`
|
|
@@ -564,3 +596,11 @@ values, or automation-hostile modal prompts.
|
|
|
564
596
|
- **Behavior:** The returned TimelineItem objects can have an unreadable/empty GetUniqueId, notably when the clipInfo recordFrame lands in a span still occupied by another item (any duplicate-then-delete 'move' whose offset is smaller than the item duration hits this). The item may not actually exist on the timeline. A session that trusted the non-empty return and deleted the sources lost 26 clips (Portugal timeline, 2026-08-19).
|
|
565
597
|
- **Workaround / current handling:** Never treat AppendToTimeline's return as proof of placement. Re-enumerate the track and match on record frame + duration before any dependent delete; never shift items by duplicate-then-delete into occupied spans — use timeline.ripple_insert, which rebuilds the tail into free space and verifies by readback.
|
|
566
598
|
- **Tags:** editorial, silent-failure, unreliable-return, timeline
|
|
599
|
+
|
|
600
|
+
### Project.IsRenderingInProgress (stuck True after deleting the rendering project)
|
|
601
|
+
|
|
602
|
+
- **Object:** `Project`
|
|
603
|
+
- **Signature:** `() -> Bool`
|
|
604
|
+
- **Behavior:** Deleting or closing a project while its render job is still running orphans the render and wedges the whole render pipeline: the output file stops growing and Resolve idles at 0% CPU, IsRenderingInProgress on the NEXT current project reports True indefinitely, StopRendering does not clear it, NEW render jobs sit at 0% forever (then StartRendering starts returning False), and Resolve.Quit() is refused because the app believes a render is running — even project creation can start returning None behind the quit-confirm dialog (reproduced live on Studio 19.1.3.7, 2026-08-29).
|
|
605
|
+
- **Workaround / current handling:** Never close or delete a project while IsRenderingInProgress is True — StopRendering first, wait for False, then close. Once wedged, only a manual quit (confirming the dialog) or force-quit clears it; treat a True that persists at 0% CPU with a static output file as stuck rather than rendering. Poll GetRenderJobStatus for completion instead of IsRenderingInProgress, which this failure poisons.
|
|
606
|
+
- **Tags:** render, silent-failure, unreliable-return
|
|
@@ -3,19 +3,15 @@
|
|
|
3
3
|
# Read/Write Symmetry Audit
|
|
4
4
|
|
|
5
5
|
- write-style action occurrences scanned: **116**
|
|
6
|
-
- write-style action occurrences with a matching read: **
|
|
7
|
-
- distinct high-signal `set_` actions without a direct/known readback: **
|
|
6
|
+
- write-style action occurrences with a matching read: **71**
|
|
7
|
+
- distinct high-signal `set_` actions without a direct/known readback: **4**
|
|
8
8
|
|
|
9
9
|
## High-signal gaps — `set_` with no direct/known readback
|
|
10
10
|
|
|
11
11
|
- `set_cdl`
|
|
12
|
-
- `set_clip_marks`
|
|
13
|
-
- `set_clips_linked`
|
|
14
12
|
- `set_high_priority`
|
|
15
13
|
- `set_keyframe_interpolation`
|
|
16
|
-
- `set_name`
|
|
17
14
|
- `set_node_enabled`
|
|
18
|
-
- `set_title_text`
|
|
19
15
|
|
|
20
16
|
## Low-signal (create/add/insert/apply/import — usually expected): 40 distinct names
|
|
21
17
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.104.1"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.104.1"
|
|
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.
|
|
14
|
+
VERSION = "2.104.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -5612,6 +5612,76 @@ def _timeline_title_property_scan(tl, p: Dict[str, Any]):
|
|
|
5612
5612
|
}
|
|
5613
5613
|
|
|
5614
5614
|
|
|
5615
|
+
def _timeline_get_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
5616
|
+
"""Read a title item's text back — the read twin of set_title_text.
|
|
5617
|
+
|
|
5618
|
+
Same key resolution as the setter: an explicit property_key wins, otherwise
|
|
5619
|
+
the heuristic title-key candidates from the item's property map. Returns the
|
|
5620
|
+
first key that holds a non-empty string, plus every candidate's value so a
|
|
5621
|
+
caller can see which key actually carries the text on this generator.
|
|
5622
|
+
"""
|
|
5623
|
+
item, err = _timeline_resolve_item_optional(tl, p)
|
|
5624
|
+
if err:
|
|
5625
|
+
return err
|
|
5626
|
+
property_key = p.get("property_key") or p.get("key")
|
|
5627
|
+
keys: List[str] = []
|
|
5628
|
+
if property_key:
|
|
5629
|
+
keys.append(str(property_key))
|
|
5630
|
+
else:
|
|
5631
|
+
flat, exc_text = _timeline_item_get_property_map(item, _ser)
|
|
5632
|
+
if exc_text and not flat:
|
|
5633
|
+
return _err(f"GetProperty failed: {exc_text}")
|
|
5634
|
+
for row in _candidate_title_property_keys(flat):
|
|
5635
|
+
if row["key"] not in keys:
|
|
5636
|
+
keys.append(row["key"])
|
|
5637
|
+
if not keys:
|
|
5638
|
+
keys = ["Styled Text", "StyledText", "Text", "Rich Text"]
|
|
5639
|
+
values: List[Dict[str, Any]] = []
|
|
5640
|
+
text = None
|
|
5641
|
+
text_key = None
|
|
5642
|
+
for key in keys:
|
|
5643
|
+
rec: Dict[str, Any] = {"property_key": key}
|
|
5644
|
+
try:
|
|
5645
|
+
value = item.GetProperty(key)
|
|
5646
|
+
except Exception as exc:
|
|
5647
|
+
rec["error"] = str(exc)
|
|
5648
|
+
values.append(rec)
|
|
5649
|
+
continue
|
|
5650
|
+
rec["value"] = _ser(value)
|
|
5651
|
+
values.append(rec)
|
|
5652
|
+
if text is None and isinstance(value, str) and value.strip():
|
|
5653
|
+
text = value
|
|
5654
|
+
text_key = key
|
|
5655
|
+
source = "property" if text is not None else None
|
|
5656
|
+
if text is None:
|
|
5657
|
+
# SetProperty/GetProperty title keys are not exposed on every build
|
|
5658
|
+
# (absent for Text+ on Studio 19.1.3, where set_title_text also
|
|
5659
|
+
# fails). The Fusion comp carries the same text as the TextPlus
|
|
5660
|
+
# tool's StyledText input, which reads back fine there — so fall
|
|
5661
|
+
# through to the comp when the property route finds nothing.
|
|
5662
|
+
try:
|
|
5663
|
+
if int(item.GetFusionCompCount() or 0) > 0:
|
|
5664
|
+
comp = item.GetFusionCompByIndex(1)
|
|
5665
|
+
tools = comp.GetToolList(False, "TextPlus") if comp else None
|
|
5666
|
+
for key in (tools or {}):
|
|
5667
|
+
value = tools[key].GetInput("StyledText")
|
|
5668
|
+
if isinstance(value, str) and value.strip():
|
|
5669
|
+
text = value
|
|
5670
|
+
text_key = "StyledText"
|
|
5671
|
+
source = "fusion_comp"
|
|
5672
|
+
break
|
|
5673
|
+
except Exception as exc:
|
|
5674
|
+
values.append({"fusion_comp_error": str(exc)})
|
|
5675
|
+
return {
|
|
5676
|
+
"success": text is not None,
|
|
5677
|
+
"timeline_item_id": _safe_timeline_item_id(item),
|
|
5678
|
+
"text": text,
|
|
5679
|
+
"property_key": text_key,
|
|
5680
|
+
"source": source,
|
|
5681
|
+
"values": values,
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
|
|
5615
5685
|
def _timeline_set_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
5616
5686
|
item, err = _timeline_resolve_item_optional(tl, p)
|
|
5617
5687
|
if err:
|
|
@@ -8634,6 +8704,7 @@ _MEDIA_POOL_KERNEL_ACTIONS = [
|
|
|
8634
8704
|
"link_proxy_checked",
|
|
8635
8705
|
"link_full_resolution_checked",
|
|
8636
8706
|
"set_clip_marks",
|
|
8707
|
+
"get_clip_marks",
|
|
8637
8708
|
"clear_clip_marks",
|
|
8638
8709
|
"copy_clip_annotations",
|
|
8639
8710
|
"media_pool_boundary_report",
|
|
@@ -12843,6 +12914,41 @@ def _clip_media_signature(clip):
|
|
|
12843
12914
|
return signature
|
|
12844
12915
|
|
|
12845
12916
|
|
|
12917
|
+
def _ffprobe_media_summary(path: str) -> Optional[Dict[str, Any]]:
|
|
12918
|
+
"""Small duration/stream summary of a rendered file; None when ffprobe is unusable."""
|
|
12919
|
+
ffprobe = shutil.which("ffprobe")
|
|
12920
|
+
if not ffprobe:
|
|
12921
|
+
return None
|
|
12922
|
+
try:
|
|
12923
|
+
proc = subprocess.run(
|
|
12924
|
+
[ffprobe, "-v", "error", "-show_entries",
|
|
12925
|
+
"format=duration,size:stream=codec_type,codec_name",
|
|
12926
|
+
"-of", "json", path],
|
|
12927
|
+
capture_output=True, encoding="utf-8", errors="replace",
|
|
12928
|
+
timeout=20, stdin=subprocess.DEVNULL,
|
|
12929
|
+
)
|
|
12930
|
+
except Exception:
|
|
12931
|
+
return None
|
|
12932
|
+
if proc.returncode != 0:
|
|
12933
|
+
return {"error": (proc.stderr or "ffprobe failed").strip()[:300]}
|
|
12934
|
+
try:
|
|
12935
|
+
payload = json.loads(proc.stdout or "{}")
|
|
12936
|
+
except ValueError:
|
|
12937
|
+
return None
|
|
12938
|
+
fmt = payload.get("format") or {}
|
|
12939
|
+
summary: Dict[str, Any] = {
|
|
12940
|
+
"streams": [
|
|
12941
|
+
{"codec_type": st.get("codec_type"), "codec_name": st.get("codec_name")}
|
|
12942
|
+
for st in payload.get("streams") or []
|
|
12943
|
+
],
|
|
12944
|
+
}
|
|
12945
|
+
try:
|
|
12946
|
+
summary["duration_seconds"] = float(fmt.get("duration"))
|
|
12947
|
+
except (TypeError, ValueError):
|
|
12948
|
+
summary["duration_seconds"] = None
|
|
12949
|
+
return summary
|
|
12950
|
+
|
|
12951
|
+
|
|
12846
12952
|
def _probe_media_file(path: str):
|
|
12847
12953
|
ffprobe = shutil.which("ffprobe")
|
|
12848
12954
|
if not ffprobe:
|
|
@@ -13371,6 +13477,23 @@ def _set_clip_marks(root, mp, p: Dict[str, Any]):
|
|
|
13371
13477
|
return {"success": all(row.get("success") for row in results), "count": len(results), "missing": missing, "results": results}
|
|
13372
13478
|
|
|
13373
13479
|
|
|
13480
|
+
def _get_clip_marks(root, mp, p: Dict[str, Any]):
|
|
13481
|
+
"""Read mark in/out for a set of media-pool clips — the read twin of set_clip_marks."""
|
|
13482
|
+
resolved, err = _clips_from_params(root, mp, p)
|
|
13483
|
+
if err:
|
|
13484
|
+
return err
|
|
13485
|
+
clips, missing = resolved
|
|
13486
|
+
results = []
|
|
13487
|
+
for clip in clips:
|
|
13488
|
+
clip_id = _safe_media_pool_item_id(clip)
|
|
13489
|
+
gate = _requires_method(clip, "GetMarkInOut", "19.1")
|
|
13490
|
+
if gate:
|
|
13491
|
+
return gate
|
|
13492
|
+
marks = clip.GetMarkInOut() or {}
|
|
13493
|
+
results.append({"clip_id": clip_id, "name": clip.GetName(), "marks": _ser(marks)})
|
|
13494
|
+
return {"success": True, "count": len(results), "missing": missing, "results": results}
|
|
13495
|
+
|
|
13496
|
+
|
|
13374
13497
|
def _clear_clip_marks(root, mp, p: Dict[str, Any]):
|
|
13375
13498
|
resolved, err = _clips_from_params(root, mp, p)
|
|
13376
13499
|
if err:
|
|
@@ -18457,6 +18580,16 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
18457
18580
|
delete_all_jobs() -> {success}
|
|
18458
18581
|
list_jobs() -> {jobs}
|
|
18459
18582
|
get_job_status(job_id) -> {status}
|
|
18583
|
+
verify_output(job_id, expected_frames?, expected_duration_seconds?) -> {status, output_path, output_exists, output_size_bytes, output_probe, mark_range_frames, timeline_item_extent_frames, expected_duration_seconds, duration_ratio, warnings, verified}
|
|
18584
|
+
JobStatus reports Complete even when the render produced a near-empty
|
|
18585
|
+
stub (content before the timeline start, issue #164), and Resolve
|
|
18586
|
+
rewrites the job's own MarkIn/MarkOut down to the collapsed extent, so
|
|
18587
|
+
the job metadata lies too. This checks the actual output file, and
|
|
18588
|
+
cross-checks the job's timeline items (which read back their real
|
|
18589
|
+
positions): items before the timeline start and a mark range far
|
|
18590
|
+
smaller than the item extent both warn. Pass expected_frames or
|
|
18591
|
+
expected_duration_seconds when you know what the render should hold.
|
|
18592
|
+
Verify BEFORE deleting the job; deleted jobs carry no TargetDir.
|
|
18460
18593
|
start(job_ids?, interactive?) -> {success}
|
|
18461
18594
|
stop() -> {success}
|
|
18462
18595
|
is_rendering() -> {rendering}
|
|
@@ -18521,6 +18654,143 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
18521
18654
|
return {"jobs": _ser(proj.GetRenderJobList())}
|
|
18522
18655
|
elif action == "get_job_status":
|
|
18523
18656
|
return _ser(proj.GetRenderJobStatus(p["job_id"]))
|
|
18657
|
+
elif action == "verify_output":
|
|
18658
|
+
# JobStatus lies by omission: a job whose content renders as nothing
|
|
18659
|
+
# (e.g. clips placed before the timeline start, issue #164) still
|
|
18660
|
+
# reports Complete at 100% with a stub output file. Verify the file.
|
|
18661
|
+
job_id = p.get("job_id")
|
|
18662
|
+
if not job_id:
|
|
18663
|
+
return _err("verify_output requires job_id")
|
|
18664
|
+
status = _ser(proj.GetRenderJobStatus(job_id)) or {}
|
|
18665
|
+
jobs = _ser(proj.GetRenderJobList()) or []
|
|
18666
|
+
job = next((j for j in jobs if j.get("JobId") == job_id), None)
|
|
18667
|
+
if job is None:
|
|
18668
|
+
return _err(
|
|
18669
|
+
f"Render job {job_id!r} is not in the render queue "
|
|
18670
|
+
"(deleted jobs cannot be verified — verify before deleting)",
|
|
18671
|
+
code="JOB_NOT_FOUND", category="invalid_input",
|
|
18672
|
+
)
|
|
18673
|
+
result: Dict[str, Any] = {"job_id": job_id, "status": status}
|
|
18674
|
+
target_dir = job.get("TargetDir")
|
|
18675
|
+
filename = job.get("OutputFilename")
|
|
18676
|
+
warnings: List[str] = []
|
|
18677
|
+
output_path = None
|
|
18678
|
+
if target_dir and filename:
|
|
18679
|
+
output_path = os.path.join(target_dir, filename)
|
|
18680
|
+
else:
|
|
18681
|
+
warnings.append("Job carries no TargetDir/OutputFilename to verify against.")
|
|
18682
|
+
result["output_path"] = output_path
|
|
18683
|
+
# The job's own MarkIn/MarkOut cannot be the only truth source: when
|
|
18684
|
+
# timeline content sits before the timeline's start frame (issue #164)
|
|
18685
|
+
# Resolve rewrites the job's mark range down to the collapsed extent,
|
|
18686
|
+
# so the job metadata lies right along with JobStatus (measured live on
|
|
18687
|
+
# Studio 19.1.3.7: 96 frames of content before start -> job range of 1
|
|
18688
|
+
# frame, Complete at 100%, a 1-frame black stub). Cross-check the
|
|
18689
|
+
# job's timeline items, which DO read back their real positions, and
|
|
18690
|
+
# let the caller override expected duration outright.
|
|
18691
|
+
try:
|
|
18692
|
+
fps = float(str(job.get("FrameRate")))
|
|
18693
|
+
except (TypeError, ValueError):
|
|
18694
|
+
fps = None
|
|
18695
|
+
try:
|
|
18696
|
+
mark_frames = int(job.get("MarkOut")) - int(job.get("MarkIn")) + 1
|
|
18697
|
+
except (TypeError, ValueError):
|
|
18698
|
+
mark_frames = None
|
|
18699
|
+
result["mark_range_frames"] = mark_frames
|
|
18700
|
+
tl_for_job = None
|
|
18701
|
+
try:
|
|
18702
|
+
for i in range(1, int(proj.GetTimelineCount() or 0) + 1):
|
|
18703
|
+
cand = proj.GetTimelineByIndex(i)
|
|
18704
|
+
if cand and cand.GetName() == job.get("TimelineName"):
|
|
18705
|
+
tl_for_job = cand
|
|
18706
|
+
break
|
|
18707
|
+
except Exception:
|
|
18708
|
+
tl_for_job = None
|
|
18709
|
+
if tl_for_job is not None:
|
|
18710
|
+
try:
|
|
18711
|
+
tl_start = int(tl_for_job.GetStartFrame())
|
|
18712
|
+
extent_lo, extent_hi, before_start = None, None, 0
|
|
18713
|
+
for ti in range(1, int(tl_for_job.GetTrackCount("video") or 0) + 1):
|
|
18714
|
+
for it in tl_for_job.GetItemListInTrack("video", ti) or []:
|
|
18715
|
+
it_start, it_end = int(it.GetStart()), int(it.GetEnd())
|
|
18716
|
+
extent_lo = it_start if extent_lo is None else min(extent_lo, it_start)
|
|
18717
|
+
extent_hi = it_end if extent_hi is None else max(extent_hi, it_end)
|
|
18718
|
+
if it_start < tl_start:
|
|
18719
|
+
before_start += 1
|
|
18720
|
+
if before_start:
|
|
18721
|
+
warnings.append(
|
|
18722
|
+
f"{before_start} video item(s) on timeline "
|
|
18723
|
+
f"{job.get('TimelineName')!r} start before the timeline's "
|
|
18724
|
+
f"start frame {tl_start} — the render engine never visits "
|
|
18725
|
+
"that content (recordFrame counts from absolute frame "
|
|
18726
|
+
"zero, issue #164). The items read back as placed; the "
|
|
18727
|
+
"render is a stub."
|
|
18728
|
+
)
|
|
18729
|
+
if (extent_lo is not None and mark_frames is not None
|
|
18730
|
+
and mark_frames < (extent_hi - extent_lo) * 0.5):
|
|
18731
|
+
warnings.append(
|
|
18732
|
+
f"The job's mark range ({mark_frames} frame(s)) is under "
|
|
18733
|
+
f"half the timeline's item extent "
|
|
18734
|
+
f"({extent_hi - extent_lo} frames) — Resolve rewrote the "
|
|
18735
|
+
"range down to a collapsed extent, which is how a stub "
|
|
18736
|
+
"render reports a clean duration ratio."
|
|
18737
|
+
)
|
|
18738
|
+
result["timeline_item_extent_frames"] = (
|
|
18739
|
+
None if extent_lo is None else extent_hi - extent_lo
|
|
18740
|
+
)
|
|
18741
|
+
except Exception:
|
|
18742
|
+
pass
|
|
18743
|
+
else:
|
|
18744
|
+
warnings.append(
|
|
18745
|
+
f"Timeline {job.get('TimelineName')!r} was not found in the "
|
|
18746
|
+
"project, so item-extent cross-checks were skipped."
|
|
18747
|
+
)
|
|
18748
|
+
if output_path:
|
|
18749
|
+
exists = os.path.exists(output_path)
|
|
18750
|
+
result["output_exists"] = exists
|
|
18751
|
+
if exists:
|
|
18752
|
+
result["output_size_bytes"] = os.path.getsize(output_path)
|
|
18753
|
+
probe = _ffprobe_media_summary(output_path)
|
|
18754
|
+
if probe:
|
|
18755
|
+
result["output_probe"] = probe
|
|
18756
|
+
duration = probe.get("duration_seconds")
|
|
18757
|
+
# Expected duration: caller-supplied truth wins; the job's
|
|
18758
|
+
# mark range is the fallback (see collapse caveat above).
|
|
18759
|
+
expected = None
|
|
18760
|
+
if p.get("expected_duration_seconds") is not None:
|
|
18761
|
+
try:
|
|
18762
|
+
expected = float(p["expected_duration_seconds"])
|
|
18763
|
+
except (TypeError, ValueError):
|
|
18764
|
+
expected = None
|
|
18765
|
+
elif p.get("expected_frames") is not None and fps:
|
|
18766
|
+
try:
|
|
18767
|
+
expected = int(p["expected_frames"]) / fps
|
|
18768
|
+
except (TypeError, ValueError):
|
|
18769
|
+
expected = None
|
|
18770
|
+
elif mark_frames is not None and fps:
|
|
18771
|
+
expected = mark_frames / fps
|
|
18772
|
+
result["expected_duration_seconds"] = expected
|
|
18773
|
+
if duration is not None and expected and expected > 0:
|
|
18774
|
+
result["duration_ratio"] = round(duration / expected, 4)
|
|
18775
|
+
if duration < expected * 0.5:
|
|
18776
|
+
warnings.append(
|
|
18777
|
+
f"Output duration {duration:.2f}s is under half the job's "
|
|
18778
|
+
f"mark range ({expected:.2f}s). A Complete status with a "
|
|
18779
|
+
"near-empty file is the signature of content the render "
|
|
18780
|
+
"engine never visited — e.g. clips placed before the "
|
|
18781
|
+
"timeline start (recordFrame counted from absolute zero, "
|
|
18782
|
+
"issue #164)."
|
|
18783
|
+
)
|
|
18784
|
+
else:
|
|
18785
|
+
if status.get("JobStatus") == "Complete":
|
|
18786
|
+
warnings.append(
|
|
18787
|
+
"JobStatus is Complete but the output file does not exist."
|
|
18788
|
+
)
|
|
18789
|
+
result["warnings"] = warnings
|
|
18790
|
+
result["verified"] = bool(
|
|
18791
|
+
output_path and result.get("output_exists") and not warnings
|
|
18792
|
+
)
|
|
18793
|
+
return result
|
|
18524
18794
|
elif action == "start":
|
|
18525
18795
|
job_ids = p.get("job_ids")
|
|
18526
18796
|
interactive = p.get("interactive", False)
|
|
@@ -18645,7 +18915,7 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
18645
18915
|
return _safe_quick_export(proj, p)
|
|
18646
18916
|
elif action == "export_render_boundary_report":
|
|
18647
18917
|
return _export_render_boundary_report(proj, p)
|
|
18648
|
-
return _unknown(action, ["add_job","delete_job","delete_all_jobs","list_jobs","get_job_status","start","stop","is_rendering","get_formats","get_codecs","get_format_and_codec","set_format_and_codec","get_mode","set_mode","get_resolutions","get_settings","set_settings","list_presets","load_preset","save_preset","delete_preset","quick_export_presets","quick_export",*_RENDER_KERNEL_ACTIONS])
|
|
18918
|
+
return _unknown(action, ["add_job","delete_job","delete_all_jobs","list_jobs","get_job_status","verify_output","start","stop","is_rendering","get_formats","get_codecs","get_format_and_codec","set_format_and_codec","get_mode","set_mode","get_resolutions","get_settings","set_settings","list_presets","load_preset","save_preset","delete_preset","quick_export_presets","quick_export",*_RENDER_KERNEL_ACTIONS])
|
|
18649
18919
|
|
|
18650
18920
|
|
|
18651
18921
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -19185,6 +19455,8 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
19185
19455
|
return _link_full_resolution_checked(root, p)
|
|
19186
19456
|
elif action == "set_clip_marks":
|
|
19187
19457
|
return _set_clip_marks(root, mp, p)
|
|
19458
|
+
elif action == "get_clip_marks":
|
|
19459
|
+
return _get_clip_marks(root, mp, p)
|
|
19188
19460
|
elif action == "clear_clip_marks":
|
|
19189
19461
|
return _clear_clip_marks(root, mp, p)
|
|
19190
19462
|
elif action == "copy_clip_annotations":
|
|
@@ -19854,6 +20126,8 @@ def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None
|
|
|
19854
20126
|
return {"flags": clip.GetFlagList()}
|
|
19855
20127
|
elif action == "clear_flags":
|
|
19856
20128
|
return {"success": bool(clip.ClearFlags(p["color"]))}
|
|
20129
|
+
elif action == "get_name":
|
|
20130
|
+
return {"name": clip.GetName()}
|
|
19857
20131
|
elif action == "set_name":
|
|
19858
20132
|
missing = _requires_method(clip, "SetName", "20.2")
|
|
19859
20133
|
if missing:
|
|
@@ -19880,7 +20154,7 @@ def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None
|
|
|
19880
20154
|
if not replacement_path:
|
|
19881
20155
|
return _err("Provide path or file_path")
|
|
19882
20156
|
return {"success": bool(clip.ReplaceClipPreserveSubClip(replacement_path))}
|
|
19883
|
-
return _unknown(action, ["add","get_all","get_by_custom_data","update_custom_data","get_custom_data","delete_by_color","delete_at_frame","delete_by_custom_data","add_flag","get_flags","clear_flags","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip"])
|
|
20157
|
+
return _unknown(action, ["add","get_all","get_by_custom_data","update_custom_data","get_custom_data","delete_by_color","delete_at_frame","delete_by_custom_data","add_flag","get_flags","clear_flags","get_name","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip"])
|
|
19884
20158
|
|
|
19885
20159
|
|
|
19886
20160
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -22924,7 +23198,8 @@ _TIMELINE_ACTIONS = [
|
|
|
22924
23198
|
"overwrite_range", "lift_range", "story_spine_report", "create_variant_from_ranges",
|
|
22925
23199
|
"bulk_set_item_properties", "apply_look_to_items", "thumbnail_contact_sheet",
|
|
22926
23200
|
"marker_thumbnail_review", "edit_kernel_capabilities", "probe_edit_kernel_item",
|
|
22927
|
-
"title_property_scan", "set_title_text", "
|
|
23201
|
+
"title_property_scan", "set_title_text", "get_title_text", "get_clips_linked",
|
|
23202
|
+
"bulk_set_title_text", "create_compound_clip",
|
|
22928
23203
|
"create_fusion_clip", "import_into_timeline", "export", "get_setting", "set_setting",
|
|
22929
23204
|
"insert_generator", "insert_fusion_generator", "insert_fusion_composition",
|
|
22930
23205
|
"insert_ofx_generator", "insert_title", "insert_fusion_title", "get_unique_id",
|
|
@@ -23418,6 +23693,29 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
23418
23693
|
return _timeline_title_property_scan(tl, p)
|
|
23419
23694
|
elif action == "set_title_text":
|
|
23420
23695
|
return _timeline_set_title_text(tl, p)
|
|
23696
|
+
elif action == "get_title_text":
|
|
23697
|
+
return _timeline_get_title_text(tl, p)
|
|
23698
|
+
elif action == "get_clips_linked":
|
|
23699
|
+
ids = p.get("clip_ids") or ([p["clip_id"]] if p.get("clip_id") else None)
|
|
23700
|
+
if not ids:
|
|
23701
|
+
return _err("get_clips_linked requires clip_id or clip_ids")
|
|
23702
|
+
groups = []
|
|
23703
|
+
for cid in ids:
|
|
23704
|
+
it = _find_timeline_item_by_id(tl, cid)
|
|
23705
|
+
if not it:
|
|
23706
|
+
groups.append({"clip_id": cid, "error": "not found"})
|
|
23707
|
+
continue
|
|
23708
|
+
missing = _requires_method(it, "GetLinkedItems", "19.1")
|
|
23709
|
+
if missing:
|
|
23710
|
+
return missing
|
|
23711
|
+
linked = it.GetLinkedItems() or []
|
|
23712
|
+
groups.append({
|
|
23713
|
+
"clip_id": cid,
|
|
23714
|
+
"name": it.GetName(),
|
|
23715
|
+
"linked": bool(linked),
|
|
23716
|
+
"linked_items": [{"name": li.GetName(), "id": li.GetUniqueId()} for li in linked],
|
|
23717
|
+
})
|
|
23718
|
+
return {"groups": groups, "count": len(groups)}
|
|
23421
23719
|
elif action == "bulk_set_title_text":
|
|
23422
23720
|
return _timeline_bulk_set_title_text(tl, p)
|
|
23423
23721
|
elif action == "create_compound_clip":
|
package/src/utils/api_truth.py
CHANGED
|
@@ -1875,10 +1875,20 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1875
1875
|
"(frame 0 == first frame), while recordFrame and "
|
|
1876
1876
|
"TimelineItem.GetStart/GetEnd are absolute. Distinct from "
|
|
1877
1877
|
"the null-id entry below, which is a recordFrame landing in "
|
|
1878
|
-
"an OCCUPIED span rather than before the start.
|
|
1878
|
+
"an OCCUPIED span rather than before the start. Worse, the "
|
|
1879
|
+
"render JOB's own metadata lies too: Resolve rewrites the "
|
|
1880
|
+
"job's MarkIn/MarkOut down to the collapsed extent (measured "
|
|
1881
|
+
"live on 19.1.3.7: 96 frames of content before start -> an "
|
|
1882
|
+
"explicit 96-frame mark range became a 1-frame job, Complete "
|
|
1883
|
+
"at 100%, a 1-frame black stub), so output-duration vs "
|
|
1884
|
+
"job-mark-range checks read clean. The only truthful "
|
|
1885
|
+
"readback is the timeline items themselves, which report "
|
|
1886
|
+
"their real GetStart/GetEnd — before the start frame. "
|
|
1887
|
+
"Verified on "
|
|
1879
1888
|
"Studio 20.3.2.9 (v2.17.1 probe: relative record_frame 12 "
|
|
1880
|
-
"landed at 86400 + 12 = 86412, absolute preserved 86484)
|
|
1881
|
-
"independently on Studio 21.0.4.5 (issue #164)
|
|
1889
|
+
"landed at 86400 + 12 = 86412, absolute preserved 86484), "
|
|
1890
|
+
"independently on Studio 21.0.4.5 (issue #164), and the "
|
|
1891
|
+
"mark-range rewrite on Studio 19.1.3.7 (2026-08-29).",
|
|
1882
1892
|
"recommended": "Offset every recordFrame by timeline.GetStartFrame(). "
|
|
1883
1893
|
"This server already does it: "
|
|
1884
1894
|
"media_pool.append_to_timeline defaults to "
|
|
@@ -1888,8 +1898,10 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1888
1898
|
"value below the timeline start is refused outright. "
|
|
1889
1899
|
"When driving the API directly, "
|
|
1890
1900
|
"never treat JobStatus Complete as proof a render "
|
|
1891
|
-
"worked —
|
|
1892
|
-
"
|
|
1901
|
+
"worked — and do not trust the job's MarkIn/MarkOut as "
|
|
1902
|
+
"the expected duration either. render.verify_output "
|
|
1903
|
+
"(v2.104.1) cross-checks the job against its timeline's "
|
|
1904
|
+
"item extents and flags items before the start frame.",
|
|
1893
1905
|
"tags": ["timeline", "edit", "render", "silent-failure", "media-pool"],
|
|
1894
1906
|
"issue": 164,
|
|
1895
1907
|
},
|
|
@@ -2267,6 +2279,90 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2267
2279
|
"mitigation": ["_append_and_recover_timeline_item duplicate_verified gate",
|
|
2268
2280
|
"_timeline_ripple_insert_impl"],
|
|
2269
2281
|
},
|
|
2282
|
+
{
|
|
2283
|
+
"symbol": "TimelineItem.SetCDL (write-only — no GetCDL anywhere)",
|
|
2284
|
+
"object": "TimelineItem",
|
|
2285
|
+
"signature": "({NodeIndex, Slope, Offset, Power, Saturation}) -> Bool",
|
|
2286
|
+
"reality": "SetCDL writes a node's CDL but no object exposes a read: "
|
|
2287
|
+
"no GetCDL on TimelineItem or Graph in the API reference, "
|
|
2288
|
+
"and dir() on a live Graph confirms (Studio 19.1.3.7). A "
|
|
2289
|
+
"grade applied via SetCDL cannot be read back, diffed, or "
|
|
2290
|
+
"verified through the API.",
|
|
2291
|
+
"recommended": "Track intended CDL values in the caller, or read the "
|
|
2292
|
+
"actual grade by exporting a DRX still and decoding it "
|
|
2293
|
+
"(this repo's drx tool decodes 100% of DRX params — "
|
|
2294
|
+
"slope/offset/power/sat included).",
|
|
2295
|
+
"tags": ["color", "missing-method", "readback"],
|
|
2296
|
+
"submit": "missing",
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
"symbol": "Graph.SetNodeEnabled (write-only — no GetNodeEnabled)",
|
|
2300
|
+
"object": "Graph",
|
|
2301
|
+
"signature": "(nodeIndex, bool) -> Bool",
|
|
2302
|
+
"reality": "A node's bypass state can be set but never read: no "
|
|
2303
|
+
"GetNodeEnabled in the API reference, and dir() on a live "
|
|
2304
|
+
"Graph confirms (Studio 19.1.3.7). After a SetNodeEnabled "
|
|
2305
|
+
"the caller cannot verify it took, and the pre-existing "
|
|
2306
|
+
"state of a node someone toggled in the UI is unknowable.",
|
|
2307
|
+
"recommended": "Treat node-enable state as write-only: record what "
|
|
2308
|
+
"you set, and verify visually (rendered-frame compare) "
|
|
2309
|
+
"when the state matters.",
|
|
2310
|
+
"tags": ["color", "missing-method", "readback"],
|
|
2311
|
+
"submit": "missing",
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
"symbol": "TimelineItem.SetKeyframeInterpolation (write-only)",
|
|
2315
|
+
"object": "TimelineItem",
|
|
2316
|
+
"signature": "(property, frame, type) -> Bool",
|
|
2317
|
+
"reality": "Interpolation can be written per keyframe but nothing "
|
|
2318
|
+
"returns it: GetKeyframeAtIndex/GetPropertyAtKeyframeIndex "
|
|
2319
|
+
"expose frame and value only (API reference). On Studio "
|
|
2320
|
+
"19.1.3.7 the whole keyframe method family is absent from "
|
|
2321
|
+
"dir() — these methods are 20.x+.",
|
|
2322
|
+
"recommended": "Record interpolation choices in the caller; readback "
|
|
2323
|
+
"is not available at any version.",
|
|
2324
|
+
"tags": ["timeline", "missing-method", "readback", "keyframes"],
|
|
2325
|
+
"submit": "missing",
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
"symbol": "Resolve.SetHighPriority (write-only, irreversible per session)",
|
|
2329
|
+
"object": "Resolve",
|
|
2330
|
+
"signature": "() -> Bool",
|
|
2331
|
+
"reality": "Raises the Resolve process priority; there is no getter "
|
|
2332
|
+
"and no way to lower it again through the API (confirmed "
|
|
2333
|
+
"absent from dir() on Studio 19.1.3.7).",
|
|
2334
|
+
"recommended": "Call it only when the user asked for a long render on "
|
|
2335
|
+
"a dedicated machine; state cannot be read back or "
|
|
2336
|
+
"undone without restarting Resolve.",
|
|
2337
|
+
"tags": ["app-control", "missing-method", "readback"],
|
|
2338
|
+
"submit": "missing",
|
|
2339
|
+
},
|
|
2340
|
+
{
|
|
2341
|
+
"symbol": "Project.IsRenderingInProgress (stuck True after deleting the rendering project)",
|
|
2342
|
+
"object": "Project",
|
|
2343
|
+
"signature": "() -> Bool",
|
|
2344
|
+
"reality": "Deleting or closing a project while its render job is "
|
|
2345
|
+
"still running orphans the render and wedges the whole "
|
|
2346
|
+
"render pipeline: the output file stops growing and "
|
|
2347
|
+
"Resolve idles at 0% CPU, IsRenderingInProgress on the "
|
|
2348
|
+
"NEXT current project reports True indefinitely, "
|
|
2349
|
+
"StopRendering does not clear it, NEW render jobs sit at "
|
|
2350
|
+
"0% forever (then StartRendering starts returning False), "
|
|
2351
|
+
"and Resolve.Quit() is refused because the app believes a "
|
|
2352
|
+
"render is running — even project creation can start "
|
|
2353
|
+
"returning None behind the quit-confirm dialog "
|
|
2354
|
+
"(reproduced live on Studio 19.1.3.7, 2026-08-29).",
|
|
2355
|
+
"recommended": "Never close or delete a project while "
|
|
2356
|
+
"IsRenderingInProgress is True — StopRendering first, "
|
|
2357
|
+
"wait for False, then close. Once wedged, only a "
|
|
2358
|
+
"manual quit (confirming the dialog) or force-quit "
|
|
2359
|
+
"clears it; treat a True that persists at 0% CPU with "
|
|
2360
|
+
"a static output file as stuck rather than rendering. "
|
|
2361
|
+
"Poll GetRenderJobStatus for completion instead of "
|
|
2362
|
+
"IsRenderingInProgress, which this failure poisons.",
|
|
2363
|
+
"tags": ["render", "silent-failure", "unreliable-return"],
|
|
2364
|
+
"submit": "bug",
|
|
2365
|
+
},
|
|
2270
2366
|
{
|
|
2271
2367
|
"symbol": "TimelineItem.CreateMagicMask (needs operator clicks)",
|
|
2272
2368
|
"object": "TimelineItem",
|
|
@@ -118,6 +118,23 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
|
|
|
118
118
|
"workaround is unavailable.",
|
|
119
119
|
"issue": 128,
|
|
120
120
|
},
|
|
121
|
+
{
|
|
122
|
+
"symbol": "MediaPoolItem.GetMarkInOut",
|
|
123
|
+
"introduced_in": "19.1",
|
|
124
|
+
"source": "measured",
|
|
125
|
+
"note": "Present on Studio 19.1.3.7 (confirmed live 2026-08-29, "
|
|
126
|
+
"set/get round trip). Introduction version not bisected — "
|
|
127
|
+
"19.1 is the highest floor this repo can attest, so older "
|
|
128
|
+
"builds may have it too and the gate errs toward refusing.",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"symbol": "TimelineItem.GetLinkedItems",
|
|
132
|
+
"introduced_in": "19.1",
|
|
133
|
+
"source": "measured",
|
|
134
|
+
"note": "Present on Studio 19.1.3.7 (confirmed live 2026-08-29, "
|
|
135
|
+
"returned the audio twin of a video item). Introduction "
|
|
136
|
+
"version not bisected — same caveat as GetMarkInOut.",
|
|
137
|
+
},
|
|
121
138
|
{
|
|
122
139
|
# Named on Project, not Timeline: the shipped README lists it in the
|
|
123
140
|
# Project section and src/server.py calls it on the project handle. The
|