davinci-resolve-mcp 2.182.0 → 2.184.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 +32 -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 +95 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
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.184.0 — E131: a nested Stack's audio tracks flatten onto the parent audio lanes
|
|
6
|
+
|
|
7
|
+
### Verified
|
|
8
|
+
|
|
9
|
+
- A compound's inner Audio tracks flatten onto the parent's audio lanes
|
|
10
|
+
(`A`, `A2` …) tagged `fromCompound`, and the bridge places them on those
|
|
11
|
+
lanes — three audio placements across two lanes, none dropped. Pinned as a
|
|
12
|
+
test; the flatten from E120 already did this.
|
|
13
|
+
|
|
14
|
+
## What's New in v2.183.0 — E129/E130: `import_from_drp` names from the pool, imports timelines, keeps compounds
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **`timeline.import_from_drp` named containers after their first clip.**
|
|
19
|
+
Resolve's own compound-timeline export listed as "cut_src.mp4" /
|
|
20
|
+
"white_src.mp4", "by name" could not find the real timeline, and the
|
|
21
|
+
default (all containers) imported the inner compound containers as
|
|
22
|
+
separate, hollow timelines. The lister now names each container from the
|
|
23
|
+
pool (`Sm2MpTimelineClip` / `Sm2MpCompoundClip` embed the `Sm2Sequence`
|
|
24
|
+
the container's `<Sequence>` names) and reports `kind`; the default
|
|
25
|
+
imports every TIMELINE; explicit names and indexes are unchanged (E129).
|
|
26
|
+
- **The Python extractor dropped a timeline's compound containers.** A
|
|
27
|
+
compound is a pool `Sm2MpCompoundClip` whose embedded sequence lives in
|
|
28
|
+
its own container (the E45 law `drt.extract_from_drp` already honours), so
|
|
29
|
+
a timeline with compounds imported with hollow compounds. The extractor now
|
|
30
|
+
keeps them recursively (MediaRefs → compound pool elements → embedded
|
|
31
|
+
sequence ids → containers) and `metadata.json` lists `keptSeqContainers`.
|
|
32
|
+
Verified on the fixture: E57_NESTED keeps E57_OUT and E57_IN; E57_OUT keeps
|
|
33
|
+
E57_IN; the bundled template keeps its one container (E130). The
|
|
34
|
+
bundled template's sequence now lists as `MediaTemplate` on this route
|
|
35
|
+
too, where it used to read as its clip `sample.mp4`.
|
|
36
|
+
|
|
5
37
|
## What's New in v2.182.0 — E128: `extract_from_drp` defaults to the pool's timeline, not the first-sorted compound
|
|
6
38
|
|
|
7
39
|
### 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
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.184.0 版 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 = "2.
|
|
40
|
+
VERSION = "2.184.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
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.184.0"
|
|
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.184.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -7574,17 +7574,58 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
7574
7574
|
_SEQ_CONTAINER_RE = re.compile(r"(^|/)SeqContainer(\d*\.xml|/[^/]+\.xml)$")
|
|
7575
7575
|
|
|
7576
7576
|
|
|
7577
|
+
def _drp_pool_sequence_names(zf) -> Dict[str, Dict[str, str]]:
|
|
7578
|
+
"""Map an embedded Sm2Sequence DbId → {name, kind} from every MpFolder*.xml.
|
|
7579
|
+
|
|
7580
|
+
A SeqContainer XML carries NO timeline name — its first <Name> is the first
|
|
7581
|
+
CLIP's (measured on Resolve 19.1.3.7's EXPORT_DRT of a compound timeline,
|
|
7582
|
+
E127/E129) — while the pool's Sm2MpTimelineClip (a timeline) and
|
|
7583
|
+
Sm2MpCompoundClip (a compound) embed the Sm2Sequence whose DbId the
|
|
7584
|
+
container's track-level <Sequence> names. That is where names and kinds live.
|
|
7585
|
+
"""
|
|
7586
|
+
out: Dict[str, Dict[str, str]] = {}
|
|
7587
|
+
for entry in zf.namelist():
|
|
7588
|
+
if entry.endswith("/") or not re.search(r"(^|/)MpFolder[^/]*\.xml$", entry):
|
|
7589
|
+
continue
|
|
7590
|
+
try:
|
|
7591
|
+
xml = zf.read(entry).decode("utf-8", "replace")
|
|
7592
|
+
except Exception:
|
|
7593
|
+
continue
|
|
7594
|
+
for m in re.finditer(r'<(Sm2MpTimelineClip|Sm2MpCompoundClip) DbId="([^"]+)">([\s\S]*?)</\1>', xml):
|
|
7595
|
+
body = m.group(3)
|
|
7596
|
+
nm = re.search(r"<Name>([\s\S]*?)</Name>", body)
|
|
7597
|
+
name = nm.group(1).strip() if nm else None
|
|
7598
|
+
kind = "compound" if m.group(1) == "Sm2MpCompoundClip" else "timeline"
|
|
7599
|
+
for sm in re.finditer(r'<Sm2Sequence DbId="([^"]+)">', body):
|
|
7600
|
+
out.setdefault(sm.group(1), {"name": name, "kind": kind})
|
|
7601
|
+
return out
|
|
7602
|
+
|
|
7603
|
+
|
|
7577
7604
|
def _drp_seq_containers(zf) -> List[Dict[str, Any]]:
|
|
7578
|
-
"""List a .drp/.drt zip's SeqContainers as [{entry, name, index}] (0-based, sorted).
|
|
7605
|
+
"""List a .drp/.drt zip's SeqContainers as [{entry, name, kind, index}] (0-based, sorted).
|
|
7606
|
+
|
|
7607
|
+
`name` is the POOL's name for the sequence when the archive carries one
|
|
7608
|
+
(the container's own first <Name> is a clip's — E129); `kind` is
|
|
7609
|
+
'timeline' | 'compound' | None.
|
|
7610
|
+
"""
|
|
7579
7611
|
entries = sorted(n for n in zf.namelist() if not n.endswith("/") and _SEQ_CONTAINER_RE.search(n))
|
|
7612
|
+
pool = _drp_pool_sequence_names(zf)
|
|
7580
7613
|
out = []
|
|
7581
7614
|
for index, entry in enumerate(entries):
|
|
7582
7615
|
try:
|
|
7583
7616
|
xml = zf.read(entry).decode("utf-8", "replace")
|
|
7584
7617
|
except Exception:
|
|
7585
7618
|
xml = ""
|
|
7619
|
+
sid = re.search(r"<Sequence>([0-9a-f-]{36})</Sequence>", xml)
|
|
7620
|
+
info = pool.get(sid.group(1)) if sid else None
|
|
7586
7621
|
m = re.search(r"<Name>([\s\S]*?)</Name>", xml)
|
|
7587
|
-
|
|
7622
|
+
fallback = m.group(1).strip() if m else None
|
|
7623
|
+
out.append({
|
|
7624
|
+
"entry": entry,
|
|
7625
|
+
"name": (info["name"] if info and info.get("name") else fallback),
|
|
7626
|
+
"kind": (info["kind"] if info else None),
|
|
7627
|
+
"index": index,
|
|
7628
|
+
})
|
|
7588
7629
|
return out
|
|
7589
7630
|
|
|
7590
7631
|
|
|
@@ -7608,11 +7649,49 @@ def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str)
|
|
|
7608
7649
|
seq_entries = [n for n in names if _SEQ_CONTAINER_RE.search(n)]
|
|
7609
7650
|
keep_xml = zf.read(seq_entry).decode("utf-8", "replace")
|
|
7610
7651
|
keep_seq_ids = re.findall(r"<Sequence>([0-9a-f-]{36})</Sequence>", keep_xml)
|
|
7652
|
+
# COMPOUND CLIPS (E130, the E45 law ported from drt.extract_from_drp): a
|
|
7653
|
+
# compound is a pool Sm2MpCompoundClip with an EMBEDDED Sm2Sequence
|
|
7654
|
+
# whose tracks live in their OWN SeqContainer — dropping that container
|
|
7655
|
+
# ships a compound that imports and reads back but is hollow. Walk the
|
|
7656
|
+
# kept container's MediaRefs → compound pool elements → embedded
|
|
7657
|
+
# sequence ids → keep those containers too, recursively.
|
|
7658
|
+
mp_texts = {}
|
|
7659
|
+
for name in names:
|
|
7660
|
+
if name.endswith("MpFolder.xml"):
|
|
7661
|
+
mp_texts[name] = zf.read(name).decode("utf-8", "replace")
|
|
7662
|
+
seq_xml_by_entry = {seq_entry: keep_xml}
|
|
7663
|
+
keep_containers = {seq_entry}
|
|
7664
|
+
seen_refs = set()
|
|
7665
|
+
queue = [keep_xml]
|
|
7666
|
+
while queue:
|
|
7667
|
+
xml = queue.pop()
|
|
7668
|
+
for ref in re.findall(r"<MediaRef>([0-9a-f-]{36})</MediaRef>", xml):
|
|
7669
|
+
if ref in seen_refs:
|
|
7670
|
+
continue
|
|
7671
|
+
for mp_xml in mp_texts.values():
|
|
7672
|
+
cm = re.search(r'<Sm2MpCompoundClip DbId="%s">[\s\S]*?</Sm2MpCompoundClip>' % re.escape(ref), mp_xml)
|
|
7673
|
+
if not cm:
|
|
7674
|
+
continue
|
|
7675
|
+
seen_refs.add(ref)
|
|
7676
|
+
im = re.search(r'<Sm2Sequence DbId="([0-9a-f-]{36})">', cm.group(0))
|
|
7677
|
+
if not im:
|
|
7678
|
+
continue
|
|
7679
|
+
inner = im.group(1)
|
|
7680
|
+
for entry_name in seq_entries:
|
|
7681
|
+
if entry_name in keep_containers:
|
|
7682
|
+
continue
|
|
7683
|
+
sx = seq_xml_by_entry.get(entry_name)
|
|
7684
|
+
if sx is None:
|
|
7685
|
+
sx = zf.read(entry_name).decode("utf-8", "replace")
|
|
7686
|
+
seq_xml_by_entry[entry_name] = sx
|
|
7687
|
+
if ("<Sequence>%s</Sequence>" % inner) in sx:
|
|
7688
|
+
keep_containers.add(entry_name)
|
|
7689
|
+
queue.append(sx)
|
|
7611
7690
|
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as out:
|
|
7612
7691
|
for name in names:
|
|
7613
7692
|
if name == "Gallery.xml":
|
|
7614
7693
|
continue
|
|
7615
|
-
if name in seq_entries and name
|
|
7694
|
+
if name in seq_entries and name not in keep_containers:
|
|
7616
7695
|
continue
|
|
7617
7696
|
data = zf.read(name)
|
|
7618
7697
|
if name.endswith("MpFolder.xml") and len(seq_entries) > 1:
|
|
@@ -7640,6 +7719,7 @@ def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str)
|
|
|
7640
7719
|
"source": "import_from_drp",
|
|
7641
7720
|
"sourceDrp": drp_path,
|
|
7642
7721
|
"sourceSeqContainer": seq_entry,
|
|
7722
|
+
"keptSeqContainers": sorted(keep_containers),
|
|
7643
7723
|
"exportedFrom": "davinci-resolve-mcp timeline.import_from_drp",
|
|
7644
7724
|
},
|
|
7645
7725
|
indent=2,
|
|
@@ -7681,7 +7761,7 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
|
7681
7761
|
want_indexes = [want_indexes]
|
|
7682
7762
|
|
|
7683
7763
|
selected: List[Dict[str, Any]] = []
|
|
7684
|
-
available = [{"name": c["name"], "index": c["index"]} for c in containers]
|
|
7764
|
+
available = [{"name": c["name"], "kind": c.get("kind"), "index": c["index"]} for c in containers]
|
|
7685
7765
|
if want_names:
|
|
7686
7766
|
by_name = {}
|
|
7687
7767
|
for c in containers:
|
|
@@ -7703,7 +7783,11 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
|
7703
7783
|
return _err(f"timelineIndex {i} out of range (0..{len(containers) - 1})", category="invalid_input")
|
|
7704
7784
|
selected.append(match)
|
|
7705
7785
|
else:
|
|
7706
|
-
|
|
7786
|
+
# ALL means every TIMELINE (E129): a compound's inner container is not a
|
|
7787
|
+
# timeline of its own — extraction keeps it with the timeline that
|
|
7788
|
+
# places it — so importing it separately lands a hollow duplicate.
|
|
7789
|
+
timelines_only = [c for c in containers if c.get("kind") == "timeline"]
|
|
7790
|
+
selected = timelines_only if timelines_only else list(containers)
|
|
7707
7791
|
|
|
7708
7792
|
# Per-timeline options passthrough (importSourceClips etc.) minus selection
|
|
7709
7793
|
# keys; background is a top-level option, so each sub-step import runs
|
|
@@ -24255,6 +24339,11 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
24255
24339
|
`sanitize.flagged` for human review, never silently applied. verify_threshold
|
|
24256
24340
|
(default 0.90) sets the structural-match bar.
|
|
24257
24341
|
import_from_drp(drpPath, timelineNames?|timelineIndexes?, import_source_clips?, dry_run?) -> {success, selected, imported, available, results, partial?}
|
|
24342
|
+
timelineNames are the POOL's names (a SeqContainer's own first <Name>
|
|
24343
|
+
is a clip's — measured E129); `available` rows carry kind
|
|
24344
|
+
(timeline|compound). Omitted selection = every TIMELINE: a compound's
|
|
24345
|
+
inner container is not a timeline of its own, it travels with the
|
|
24346
|
+
timeline that places it (kept recursively in the extracted .drt, E130).
|
|
24258
24347
|
CAVEAT (measured 19.1.3.7): Resolve refuses extracted containers even
|
|
24259
24348
|
repacked with the source's project.xml — the sufficient import set
|
|
24260
24349
|
beyond Resolve's own .drt exports is unmapped, so expect failures on
|