davinci-resolve-mcp 4.15.4 → 4.15.5
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 +48 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +181 -9
- package/src/utils/api_truth.py +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
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.15.5 — DRX applies say which graph changed
|
|
6
|
+
|
|
7
|
+
`graph.apply_grade_from_drx` and `timeline_item_color.safe_apply_drx` no
|
|
8
|
+
longer report a bare `success` from `ApplyGradeFromDRX`. They read the graphs
|
|
9
|
+
back, and they refuse a timeline apply that would load no timeline grade.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **A timeline apply of a .drx with no timeline grade is refused** (#318).
|
|
14
|
+
A .drx keeps the clip grade in `<pClipFullVer>` and the timeline grade in
|
|
15
|
+
`<pTrackVer>`. `drx generate`'s single-node path writes no `<pTrackVer>`,
|
|
16
|
+
and native stills and the multi-node path write one with
|
|
17
|
+
`HasCorrection false`. On Studio 21.1.1.10 such an apply was reported to
|
|
18
|
+
return True, add nothing and empty the timeline graph. With
|
|
19
|
+
`source="timeline"`, both actions now return `DRX_NO_TIMELINE_GRADE` before
|
|
20
|
+
the confirm token is issued and before Resolve is called.
|
|
21
|
+
- **DRX applies read back every graph the grade could land in** (#319).
|
|
22
|
+
The node count, labels and LUTs of the clip graph, the clip's color-group
|
|
23
|
+
pre- and post-clip graphs and the timeline graph are read before and
|
|
24
|
+
after. The result carries `changed_graphs`, a per-graph before/after count
|
|
25
|
+
and a `verification` block, so the result no longer says
|
|
26
|
+
`readback_checked: false`. When only a graph you did not target changed,
|
|
27
|
+
the call fails with `DRX_APPLIED_TO_OTHER_GRAPH`. When no graph changed it
|
|
28
|
+
still reports success, with a `warning`: a grade with the same node
|
|
29
|
+
structure changes only parameters, which no readback reaches.
|
|
30
|
+
|
|
31
|
+
### Documentation
|
|
32
|
+
|
|
33
|
+
- Two `api_truth` entries for the reported behaviour, mapped to both apply
|
|
34
|
+
actions, with what Studio 19.1.3.7 did when we measured it.
|
|
35
|
+
- New live harness `tests/live_drx_apply_readback_validation.py`.
|
|
36
|
+
|
|
37
|
+
### Validation
|
|
38
|
+
|
|
39
|
+
- Full offline suite: 4,242 tests, OK. 12 new unit tests cover the file
|
|
40
|
+
check, both refusals, the group pre-clip landing case, a verified change,
|
|
41
|
+
the no-change warning, a color-group target and `safe_apply_drx`.
|
|
42
|
+
- Live on Studio 19.1.3.7, disposable project, one synthetic clip, a grade
|
|
43
|
+
from `drx generate`: 0 failures. The refusal fired and left the timeline
|
|
44
|
+
graph alone; the clip apply was detected through the node label
|
|
45
|
+
(`changed_graphs: ["item"]`); the same-structure re-apply warned; a
|
|
46
|
+
grouped clip's readback covered both group graphs.
|
|
47
|
+
- Neither reported behaviour reproduces on 19.1.3.7. Every timeline-graph
|
|
48
|
+
apply returned False and changed nothing, with or without a timeline
|
|
49
|
+
grade, so a successful timeline apply was not observed. A grouped clip took
|
|
50
|
+
the grade in its own graph. Both were reported on Studio 21.1.1.10, which
|
|
51
|
+
was not available for this release.
|
|
52
|
+
|
|
5
53
|
## What's New in v4.15.4 — add_keyframe no longer leaves a stray key
|
|
6
54
|
|
|
7
55
|
### Fixed
|
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
|
-
> 本翻译对应 v4.15.
|
|
15
|
+
> 本翻译对应 v4.15.5 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.15.
|
|
40
|
+
VERSION = "4.15.5"
|
|
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
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.15.
|
|
94
|
+
VERSION = "4.15.5"
|
|
95
95
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
96
96
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
97
97
|
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 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "4.15.
|
|
14
|
+
VERSION = "4.15.5"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -29221,6 +29221,160 @@ def _color_graph_from_params(proj, item, p: Dict[str, Any]):
|
|
|
29221
29221
|
return None, source, _err(f"Unknown color graph source: {source}")
|
|
29222
29222
|
|
|
29223
29223
|
|
|
29224
|
+
def _drx_track_grade_state(path) -> str:
|
|
29225
|
+
"""What a .drx holds for a TIMELINE apply: "present", "empty", "missing" or "unreadable".
|
|
29226
|
+
|
|
29227
|
+
A .drx keeps the clip grade in <pClipFullVer> and the timeline grade in
|
|
29228
|
+
<pTrackVer>. A timeline ApplyGradeFromDRX loads <pTrackVer>, and when that
|
|
29229
|
+
block has HasCorrection false it replaces the timeline graph with an empty
|
|
29230
|
+
one and still returns True (issue #318).
|
|
29231
|
+
"""
|
|
29232
|
+
try:
|
|
29233
|
+
with open(path, encoding="utf-8", errors="replace") as fh:
|
|
29234
|
+
text = fh.read()
|
|
29235
|
+
except OSError:
|
|
29236
|
+
return "unreadable"
|
|
29237
|
+
block = re.search(r"<pTrackVer>(.*?)</pTrackVer>", text, re.DOTALL)
|
|
29238
|
+
if not block:
|
|
29239
|
+
return "missing"
|
|
29240
|
+
has = re.search(r"<HasCorrection>\s*(true|false)\s*</HasCorrection>", block.group(1), re.IGNORECASE)
|
|
29241
|
+
return "present" if has and has.group(1).lower() == "true" else "empty"
|
|
29242
|
+
|
|
29243
|
+
|
|
29244
|
+
def _refuse_empty_track_grade(path, source):
|
|
29245
|
+
"""Error envelope when a timeline apply would load no timeline grade, else None."""
|
|
29246
|
+
if source != "timeline":
|
|
29247
|
+
return None
|
|
29248
|
+
state = _drx_track_grade_state(path)
|
|
29249
|
+
if state not in ("missing", "empty"):
|
|
29250
|
+
return None
|
|
29251
|
+
return _err(
|
|
29252
|
+
"This .drx carries no timeline grade (<pTrackVer> is "
|
|
29253
|
+
f"{'absent' if state == 'missing' else 'empty, HasCorrection false'}). Applying it to the "
|
|
29254
|
+
"timeline graph would change nothing and replace the existing timeline graph with an empty one.",
|
|
29255
|
+
code="DRX_NO_TIMELINE_GRADE", category="invalid_input",
|
|
29256
|
+
remediation="Apply it to a clip (source='item') instead, or use a .drx exported from a "
|
|
29257
|
+
"timeline grade. To clear the timeline graph on purpose, use reset_all_grades.",
|
|
29258
|
+
state={"path": path, "track_grade": state},
|
|
29259
|
+
)
|
|
29260
|
+
|
|
29261
|
+
|
|
29262
|
+
def _graph_fingerprint(g):
|
|
29263
|
+
"""Node count plus each node's label and LUT; None when the graph can't be read.
|
|
29264
|
+
|
|
29265
|
+
GetToolsInNode is left out on purpose: it crashes Resolve 19.1.3 on some graphs.
|
|
29266
|
+
"""
|
|
29267
|
+
if not g:
|
|
29268
|
+
return None
|
|
29269
|
+
try:
|
|
29270
|
+
count = int(g.GetNumNodes() or 0)
|
|
29271
|
+
except Exception:
|
|
29272
|
+
return None
|
|
29273
|
+
nodes = []
|
|
29274
|
+
for node_index in range(1, count + 1):
|
|
29275
|
+
row = {}
|
|
29276
|
+
for key, method_name in (("label", "GetNodeLabel"), ("lut", "GetLUT")):
|
|
29277
|
+
if _has_method(g, method_name):
|
|
29278
|
+
try:
|
|
29279
|
+
row[key] = _ser(getattr(g, method_name)(node_index))
|
|
29280
|
+
except Exception:
|
|
29281
|
+
row[key] = None
|
|
29282
|
+
nodes.append(row)
|
|
29283
|
+
return {"num_nodes": count, "nodes": nodes}
|
|
29284
|
+
|
|
29285
|
+
|
|
29286
|
+
def _drx_candidate_graphs(source, *, item=None, tl=None, group=None, layer_index=None):
|
|
29287
|
+
"""{name: getter} for every graph a .drx apply could land in, the target included.
|
|
29288
|
+
|
|
29289
|
+
The clip graph, its color group's pre/post-clip graphs and the timeline graph
|
|
29290
|
+
are all candidates: on a clip in a group the grade can land in the group's
|
|
29291
|
+
pre-clip graph instead of the clip's (issue #319).
|
|
29292
|
+
"""
|
|
29293
|
+
out = {}
|
|
29294
|
+
if item is not None:
|
|
29295
|
+
out["item"] = lambda: item.GetNodeGraph(layer_index) if layer_index is not None else item.GetNodeGraph()
|
|
29296
|
+
if group is None:
|
|
29297
|
+
try:
|
|
29298
|
+
group = item.GetColorGroup()
|
|
29299
|
+
except Exception:
|
|
29300
|
+
group = None
|
|
29301
|
+
if group is not None:
|
|
29302
|
+
out["color_group_pre"] = group.GetPreClipNodeGraph
|
|
29303
|
+
out["color_group_post"] = group.GetPostClipNodeGraph
|
|
29304
|
+
if tl is None:
|
|
29305
|
+
try:
|
|
29306
|
+
_, tl, _tl_err = _get_tl()
|
|
29307
|
+
except Exception:
|
|
29308
|
+
tl = None
|
|
29309
|
+
if tl is not None:
|
|
29310
|
+
out["timeline"] = tl.GetNodeGraph
|
|
29311
|
+
if source not in out:
|
|
29312
|
+
out[source] = None
|
|
29313
|
+
return out
|
|
29314
|
+
|
|
29315
|
+
|
|
29316
|
+
def _apply_drx_with_readback(graph, source, path, grade_mode, candidates):
|
|
29317
|
+
"""ApplyGradeFromDRX, with every candidate graph read before and after.
|
|
29318
|
+
|
|
29319
|
+
The native True does not say where the grade went or whether anything
|
|
29320
|
+
changed, so the result reports which graphs changed. A grade that lands only
|
|
29321
|
+
in another graph is a failure. Nothing changing is reported but not failed:
|
|
29322
|
+
a same-structure apply changes only parameters, which the API cannot read.
|
|
29323
|
+
"""
|
|
29324
|
+
def read(getter):
|
|
29325
|
+
try:
|
|
29326
|
+
return _graph_fingerprint(getter()) if getter else None
|
|
29327
|
+
except Exception:
|
|
29328
|
+
return None
|
|
29329
|
+
|
|
29330
|
+
if candidates.get(source) is None:
|
|
29331
|
+
candidates = {**candidates, source: lambda: graph}
|
|
29332
|
+
before = {name: read(getter) for name, getter in candidates.items()}
|
|
29333
|
+
native = bool(graph.ApplyGradeFromDRX(path, grade_mode))
|
|
29334
|
+
after = {name: read(getter) for name, getter in candidates.items()}
|
|
29335
|
+
|
|
29336
|
+
graphs = {}
|
|
29337
|
+
changed, unreadable = [], []
|
|
29338
|
+
for name in candidates:
|
|
29339
|
+
b, a = before[name], after[name]
|
|
29340
|
+
if b is None or a is None:
|
|
29341
|
+
unreadable.append(name)
|
|
29342
|
+
graphs[name] = {"readable": False}
|
|
29343
|
+
continue
|
|
29344
|
+
graphs[name] = {"num_nodes_before": b["num_nodes"], "num_nodes_after": a["num_nodes"],
|
|
29345
|
+
"changed": b != a}
|
|
29346
|
+
if b != a:
|
|
29347
|
+
changed.append(name)
|
|
29348
|
+
|
|
29349
|
+
landed_elsewhere = bool(changed) and source not in changed
|
|
29350
|
+
verification = {
|
|
29351
|
+
"verified": source in changed,
|
|
29352
|
+
"contradiction": landed_elsewhere,
|
|
29353
|
+
"method": "node count, labels and LUTs of each graph before and after",
|
|
29354
|
+
}
|
|
29355
|
+
result = {"success": native, "path": path, "source": source, "changed_graphs": changed,
|
|
29356
|
+
"graphs": graphs, "verification": verification}
|
|
29357
|
+
if unreadable:
|
|
29358
|
+
result["unreadable_graphs"] = unreadable
|
|
29359
|
+
if landed_elsewhere:
|
|
29360
|
+
err = _err(
|
|
29361
|
+
f"The grade was applied to the {', '.join(changed)} graph, not the {source} graph that "
|
|
29362
|
+
"was targeted. Resolve returned " + ("True" if native else "False") + ".",
|
|
29363
|
+
code="DRX_APPLIED_TO_OTHER_GRAPH", category="resolve_api_failed", retryable=False,
|
|
29364
|
+
remediation="Check the Node Editor's graph dropdown (Clip / Group Pre-Clip / Group "
|
|
29365
|
+
"Post-Clip / Timeline) on the Color page, then restore the changed graph "
|
|
29366
|
+
"from a still or grade version before retrying.",
|
|
29367
|
+
state={"targeted": source, "changed_graphs": changed},
|
|
29368
|
+
)
|
|
29369
|
+
return {**result, **err, "success": False}
|
|
29370
|
+
if native and not changed:
|
|
29371
|
+
result["warning"] = (
|
|
29372
|
+
"No graph changed its node count, labels or LUTs. Either the grade has the same node "
|
|
29373
|
+
"structure as before (only parameters changed, which the API cannot read back) or "
|
|
29374
|
+
"nothing was applied; confirm with a rendered frame or the scopes.")
|
|
29375
|
+
return result
|
|
29376
|
+
|
|
29377
|
+
|
|
29224
29378
|
def _grade_version_snapshot(item, p: Dict[str, Any]):
|
|
29225
29379
|
out = {"current": None, "local": [], "remote": [], "errors": []}
|
|
29226
29380
|
try:
|
|
@@ -29487,6 +29641,9 @@ def _safe_apply_drx(proj, item, p: Dict[str, Any]):
|
|
|
29487
29641
|
if not _has_method(graph, "ApplyGradeFromDRX"):
|
|
29488
29642
|
return _err(f"{source} graph does not expose ApplyGradeFromDRX",
|
|
29489
29643
|
code="APPLY_DRX_UNSUPPORTED", category="unsupported")
|
|
29644
|
+
refused = _refuse_empty_track_grade(path, source)
|
|
29645
|
+
if refused:
|
|
29646
|
+
return refused
|
|
29490
29647
|
if _coerce_bool(p.get("dry_run")):
|
|
29491
29648
|
return _ok(path=path, source=source, grade_mode=p.get("grade_mode", p.get("mode", 0)), would_apply=True)
|
|
29492
29649
|
# B2 — Whole-grade replacement is catastrophic; require confirmation.
|
|
@@ -29503,8 +29660,13 @@ def _safe_apply_drx(proj, item, p: Dict[str, Any]):
|
|
|
29503
29660
|
blocked = _consume_confirm_token(action="safe_apply_drx", params=p)
|
|
29504
29661
|
if blocked:
|
|
29505
29662
|
return blocked
|
|
29506
|
-
|
|
29507
|
-
|
|
29663
|
+
group = None
|
|
29664
|
+
if source in ("color_group_pre", "color_group_post"):
|
|
29665
|
+
group = next((g for g in proj.GetColorGroupsList() or [] if g.GetName() == p.get("group_name")), None)
|
|
29666
|
+
candidates = _drx_candidate_graphs(
|
|
29667
|
+
source, item=item, group=group,
|
|
29668
|
+
layer_index=p.get("layer_index") if source == "item" else None)
|
|
29669
|
+
return _apply_drx_with_readback(graph, source, path, p.get("grade_mode", p.get("mode", 0)), candidates)
|
|
29508
29670
|
|
|
29509
29671
|
|
|
29510
29672
|
# ── color_trace apply driver ─────────────────────────────────────────────────
|
|
@@ -29920,7 +30082,7 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
29920
30082
|
"safe_apply_drx": {
|
|
29921
30083
|
"summary": "Apply a .drx grade. REPLACES the target graph; gated by confirm_token.",
|
|
29922
30084
|
"params": "path: str, source?, grade_mode? (0=no keyframes, 1=source TC aligned, 2=start aligned), require_temp_path?, confirm_token?",
|
|
29923
|
-
"returns": "{success, path, source}",
|
|
30085
|
+
"returns": "{success, path, source, changed_graphs, graphs, verification, warning?} (same readback and refusals as graph.apply_grade_from_drx)",
|
|
29924
30086
|
"example": (
|
|
29925
30087
|
'timeline_item_color(action="safe_apply_drx", params={\n'
|
|
29926
30088
|
' "track_type": "video", "track_index": 1, "item_index": 0,\n'
|
|
@@ -30110,7 +30272,7 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
30110
30272
|
"apply_grade_from_drx": {
|
|
30111
30273
|
"summary": "REPLACES the entire node graph from a .drx. Confirm_token required.",
|
|
30112
30274
|
"params": "path, grade_mode? (0/1/2), source?",
|
|
30113
|
-
"returns": "{success}",
|
|
30275
|
+
"returns": "{success, path, source, changed_graphs, graphs, verification, warning?} — changed_graphs names every graph whose node count, labels or LUTs changed; DRX_APPLIED_TO_OTHER_GRAPH when only a graph you did not target changed; DRX_NO_TIMELINE_GRADE when source=timeline and the .drx has no timeline grade",
|
|
30114
30276
|
"example": (
|
|
30115
30277
|
'graph(action="apply_grade_from_drx", params={\n'
|
|
30116
30278
|
' "path": "/tmp/look.drx", "grade_mode": 0, "source": "item"\n'
|
|
@@ -30726,7 +30888,7 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
30726
30888
|
propose_grade -> {accepted: bool, validation, plan_id?, preview_path?, error?}
|
|
30727
30889
|
safe_set_cdl -> {success, validation, normalized, node_preflight, diagnosis?}
|
|
30728
30890
|
safe_copy_grade -> {success, targets, missing}
|
|
30729
|
-
safe_apply_drx -> {success, path, source} # first call may return confirm_token
|
|
30891
|
+
safe_apply_drx -> {success, path, source, changed_graphs, graphs, verification, warning?} # first call may return confirm_token
|
|
30730
30892
|
apply_trace_plan -> {success, timeline, applied, failed, skipped, summary} # dry_run → {resolution}; first live call returns confirm_token
|
|
30731
30893
|
grade_capabilities -> {item_methods, graph_sources, lut_export_types, guards}
|
|
30732
30894
|
grade_boundary_report -> {capabilities, item, color_groups, gallery}
|
|
@@ -30766,7 +30928,7 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
30766
30928
|
safe_copy_grade(target_ids, dry_run?, ...) -> {success, targets, missing}
|
|
30767
30929
|
Copies grade to N items; dry_run reports targets without mutating.
|
|
30768
30930
|
# example: action_help(name='<action_name>')
|
|
30769
|
-
safe_apply_drx(path, source?, grade_mode?, require_temp_path?) -> {success}
|
|
30931
|
+
safe_apply_drx(path, source?, grade_mode?, require_temp_path?) -> {success, path, source, changed_graphs, graphs, verification, warning?}
|
|
30770
30932
|
REPLACES the target graph. Captures a version snapshot first; require_temp_path defaults True.
|
|
30771
30933
|
# example: action_help(name='<action_name>')
|
|
30772
30934
|
apply_trace_plan(plan_path|plan, dry_run?, min_confidence?=0.8, start_tolerance?=0, grade_mode?=0, version_name?, require_temp_path?=True, allow_timeline_mismatch?, max_rows?=40, verbose?, report_path?, confirm_token?) -> {success, applied, failed, skipped, attention, summary, report_path}
|
|
@@ -31332,7 +31494,10 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
31332
31494
|
UNSAFE. Assigns a LUT to an existing node. No snapshot/dry_run.
|
|
31333
31495
|
set_node_cache(node_index, cache_value, source?, ...) -> {success}
|
|
31334
31496
|
set_node_enabled(node_index, enabled, source?, ...) -> {success}
|
|
31335
|
-
apply_grade_from_drx(path, grade_mode?, source?, ...) -> {success}
|
|
31497
|
+
apply_grade_from_drx(path, grade_mode?, source?, ...) -> {success, path, source, changed_graphs, graphs, verification, warning?}
|
|
31498
|
+
Reads the clip, color-group pre/post and timeline graphs before and after and
|
|
31499
|
+
reports changed_graphs; fails if the grade landed only in a graph not targeted.
|
|
31500
|
+
source="timeline" refuses a .drx whose <pTrackVer> has no correction.
|
|
31336
31501
|
CATASTROPHIC. REPLACES the entire node graph. Prefer timeline_item_color.safe_apply_drx
|
|
31337
31502
|
(confirm-token gated + validated; it does NOT snapshot — grab a still/.drx backup
|
|
31338
31503
|
first, and ALWAYS pass track_type/track_index/item_index explicitly: the default
|
|
@@ -31359,6 +31524,7 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
31359
31524
|
|
|
31360
31525
|
# Get the graph object based on source
|
|
31361
31526
|
g = None
|
|
31527
|
+
item = group = None
|
|
31362
31528
|
if source == "timeline":
|
|
31363
31529
|
_, tl, err = _get_tl()
|
|
31364
31530
|
if err:
|
|
@@ -31376,6 +31542,7 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
31376
31542
|
groups = proj.GetColorGroupsList() or []
|
|
31377
31543
|
for cg in groups:
|
|
31378
31544
|
if cg.GetName() == p.get("group_name"):
|
|
31545
|
+
group = cg
|
|
31379
31546
|
g = cg.GetPreClipNodeGraph() if source == "color_group_pre" else cg.GetPostClipNodeGraph()
|
|
31380
31547
|
break
|
|
31381
31548
|
if g is None:
|
|
@@ -31429,6 +31596,9 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
31429
31596
|
elif action == "set_node_enabled":
|
|
31430
31597
|
return {"success": bool(g.SetNodeEnabled(p["node_index"], p["enabled"]))}
|
|
31431
31598
|
elif action == "apply_grade_from_drx":
|
|
31599
|
+
refused = _refuse_empty_track_grade(p["path"], source)
|
|
31600
|
+
if refused:
|
|
31601
|
+
return refused
|
|
31432
31602
|
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
31433
31603
|
return _issue_confirm_token(
|
|
31434
31604
|
action="graph.apply_grade_from_drx",
|
|
@@ -31440,7 +31610,9 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
31440
31610
|
blocked = _consume_confirm_token(action="graph.apply_grade_from_drx", params=p)
|
|
31441
31611
|
if blocked:
|
|
31442
31612
|
return blocked
|
|
31443
|
-
|
|
31613
|
+
candidates = _drx_candidate_graphs(source, item=item, group=group,
|
|
31614
|
+
layer_index=p.get("layer_index"))
|
|
31615
|
+
return _apply_drx_with_readback(g, source, p["path"], p.get("grade_mode", p.get("mode", 0)), candidates)
|
|
31444
31616
|
elif action == "apply_arri_cdl_lut":
|
|
31445
31617
|
return {"success": bool(g.ApplyArriCdlLut())}
|
|
31446
31618
|
elif action == "reset_all_grades":
|
package/src/utils/api_truth.py
CHANGED
|
@@ -2016,6 +2016,62 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2016
2016
|
"reports {page: {before, switched, restored}}.",
|
|
2017
2017
|
"tags": ["silent-failure", "color", "grade", "page", "drx", "version"],
|
|
2018
2018
|
},
|
|
2019
|
+
{
|
|
2020
|
+
"symbol": "Graph.ApplyGradeFromDRX (timeline graph, empty pTrackVer)",
|
|
2021
|
+
"object": "Graph",
|
|
2022
|
+
"signature": "Timeline.GetNodeGraph().ApplyGradeFromDRX(path, gradeMode) -> bool",
|
|
2023
|
+
"reality": "A .drx holds the clip grade in <pClipFullVer> (VerType 0) and the "
|
|
2024
|
+
"timeline grade in <pTrackVer> (VerType 1); a timeline-graph apply "
|
|
2025
|
+
"loads <pTrackVer>. When that block has HasCorrection false, which is "
|
|
2026
|
+
"what drx generate (direct) writes and what a clip still carries, "
|
|
2027
|
+
"the apply returns True, adds no grade and replaces the existing "
|
|
2028
|
+
"timeline graph with an empty one, deleting any node on it. Copying "
|
|
2029
|
+
"the clip <Body> into <pTrackVer> with HasCorrection true applied as "
|
|
2030
|
+
"expected (0 -> 1 node, no other graph changed).",
|
|
2031
|
+
"recommended": "Check the file's <pTrackVer> before a timeline apply. "
|
|
2032
|
+
"graph.apply_grade_from_drx and timeline_item_color.safe_apply_drx "
|
|
2033
|
+
"with source='timeline' refuse a .drx whose <pTrackVer> is absent or "
|
|
2034
|
+
"has HasCorrection false (DRX_NO_TIMELINE_GRADE). Clear the "
|
|
2035
|
+
"timeline graph on purpose with reset_all_grades.",
|
|
2036
|
+
"tags": ["silent-failure", "color", "grade", "drx", "node-graph", "timeline"],
|
|
2037
|
+
"issue": 318,
|
|
2038
|
+
"mitigation": ["_refuse_empty_track_grade"],
|
|
2039
|
+
"verified_on": "DaVinci Resolve Studio 21.1.1.10 (issue #318; not re-measured here)",
|
|
2040
|
+
"measured": "2026-10-27 on Studio 19.1.3.7 (tests/live_drx_apply_readback_validation.py): "
|
|
2041
|
+
"every Timeline.GetNodeGraph().ApplyGradeFromDRX returned False and left the "
|
|
2042
|
+
"timeline graph at 0 nodes, with the timeline grade present or empty, so "
|
|
2043
|
+
"the empty-graph replacement does not reproduce on that build. drx "
|
|
2044
|
+
"generate's single-node path writes no <pTrackVer> at all; the multi-node "
|
|
2045
|
+
"path writes one with HasCorrection false.",
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
"symbol": "Graph.ApplyGradeFromDRX (grade lands in another graph)",
|
|
2049
|
+
"object": "Graph",
|
|
2050
|
+
"signature": "TimelineItem.GetNodeGraph().ApplyGradeFromDRX(path, gradeMode) -> bool",
|
|
2051
|
+
"reality": "The True return does not say which graph took the grade. On a clip "
|
|
2052
|
+
"in a color group, applying to the clip's graph replaced node 1 of "
|
|
2053
|
+
"the group's pre-clip graph instead (removing the LUT the group "
|
|
2054
|
+
"shared) and left the clip graph at one empty node. The manual says "
|
|
2055
|
+
"the Node Editor's graph dropdown (Clip / Group Pre-Clip / Group "
|
|
2056
|
+
"Post-Clip / Timeline) decides where a still's grade lands; whether "
|
|
2057
|
+
"the API follows the same rule has not been confirmed.",
|
|
2058
|
+
"recommended": "Read every graph the grade could land in before and after the "
|
|
2059
|
+
"apply. graph.apply_grade_from_drx and "
|
|
2060
|
+
"timeline_item_color.safe_apply_drx compare node count, labels and "
|
|
2061
|
+
"LUTs of the clip, group pre/post and timeline graphs, return "
|
|
2062
|
+
"changed_graphs, and fail with DRX_APPLIED_TO_OTHER_GRAPH when only "
|
|
2063
|
+
"an untargeted graph changed. No change at all is reported, not "
|
|
2064
|
+
"failed: a same-structure grade changes only parameters, which no "
|
|
2065
|
+
"readback reaches, so confirm with a rendered frame.",
|
|
2066
|
+
"tags": ["silent-failure", "color", "grade", "drx", "node-graph", "readback"],
|
|
2067
|
+
"issue": 319,
|
|
2068
|
+
"mitigation": ["_apply_drx_with_readback"],
|
|
2069
|
+
"verified_on": "DaVinci Resolve Studio 21.1.1.10 (issue #319; not re-measured here)",
|
|
2070
|
+
"measured": "2026-10-27 on Studio 19.1.3.7 (tests/live_drx_apply_readback_validation.py): "
|
|
2071
|
+
"a clip assigned to a color group took the grade in its own graph; the "
|
|
2072
|
+
"group pre/post graphs did not change. The readback detected the clip "
|
|
2073
|
+
"change through the node label.",
|
|
2074
|
+
},
|
|
2019
2075
|
{
|
|
2020
2076
|
"symbol": "MediaPool.AppendToTimeline clipInfo endFrame (exclusive bound)",
|
|
2021
2077
|
"object": "MediaPool",
|
|
@@ -4031,6 +4087,14 @@ ACTION_SYMBOLS: Dict[Tuple[str, str], List[str]] = {
|
|
|
4031
4087
|
("timeline", "apply_look_to_items"): ["TimelineItem.CopyGrades"],
|
|
4032
4088
|
("timeline_item_color", "export_lut"): ["TimelineItem.ExportLUT"],
|
|
4033
4089
|
("timeline_item_color", "safe_export_lut"): ["TimelineItem.ExportLUT"],
|
|
4090
|
+
("graph", "apply_grade_from_drx"): [
|
|
4091
|
+
"Graph.ApplyGradeFromDRX (timeline graph, empty pTrackVer)",
|
|
4092
|
+
"Graph.ApplyGradeFromDRX (grade lands in another graph)",
|
|
4093
|
+
],
|
|
4094
|
+
("timeline_item_color", "safe_apply_drx"): [
|
|
4095
|
+
"Graph.ApplyGradeFromDRX (timeline graph, empty pTrackVer)",
|
|
4096
|
+
"Graph.ApplyGradeFromDRX (grade lands in another graph)",
|
|
4097
|
+
],
|
|
4034
4098
|
("timeline", "duplicate"): ["Timeline.DuplicateTimeline"],
|
|
4035
4099
|
("project_manager", "archive"): ["ProjectManager.ArchiveProject"],
|
|
4036
4100
|
("fusion_comp", "add_modifier"): ["Tool.AddModifier", "Tool.AddModifier (NestControl inputs)"],
|