davinci-resolve-mcp 2.181.0 → 2.183.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 +40 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/tools/drt.mjs +32 -3
- package/resolve-advanced/vendor/drt-format/drt-parser.js +1 -1
- package/resolve-advanced/vendor/drt-format/index.js +1 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +95 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
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.183.0 — E129/E130: `import_from_drp` names from the pool, imports timelines, keeps compounds
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`timeline.import_from_drp` named containers after their first clip.**
|
|
10
|
+
Resolve's own compound-timeline export listed as "cut_src.mp4" /
|
|
11
|
+
"white_src.mp4", "by name" could not find the real timeline, and the
|
|
12
|
+
default (all containers) imported the inner compound containers as
|
|
13
|
+
separate, hollow timelines. The lister now names each container from the
|
|
14
|
+
pool (`Sm2MpTimelineClip` / `Sm2MpCompoundClip` embed the `Sm2Sequence`
|
|
15
|
+
the container's `<Sequence>` names) and reports `kind`; the default
|
|
16
|
+
imports every TIMELINE; explicit names and indexes are unchanged (E129).
|
|
17
|
+
- **The Python extractor dropped a timeline's compound containers.** A
|
|
18
|
+
compound is a pool `Sm2MpCompoundClip` whose embedded sequence lives in
|
|
19
|
+
its own container (the E45 law `drt.extract_from_drp` already honours), so
|
|
20
|
+
a timeline with compounds imported with hollow compounds. The extractor now
|
|
21
|
+
keeps them recursively (MediaRefs → compound pool elements → embedded
|
|
22
|
+
sequence ids → containers) and `metadata.json` lists `keptSeqContainers`.
|
|
23
|
+
Verified on the fixture: E57_NESTED keeps E57_OUT and E57_IN; E57_OUT keeps
|
|
24
|
+
E57_IN; the bundled template keeps its one container (E130). The
|
|
25
|
+
bundled template's sequence now lists as `MediaTemplate` on this route
|
|
26
|
+
too, where it used to read as its clip `sample.mp4`.
|
|
27
|
+
|
|
28
|
+
## What's New in v2.182.0 — E128: `extract_from_drp` defaults to the pool's timeline, not the first-sorted compound
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **The default extraction picked an inner compound.** `extract_from_drp`
|
|
33
|
+
used index 0 by default, and SeqContainers list name-sorted by DbId; on
|
|
34
|
+
Resolve's own export of a compound timeline (the E127 fixture) index 0 was
|
|
35
|
+
the inner compound E57_IN, so the default emitted a hollow inner container
|
|
36
|
+
instead of the timeline. The default is now the first container the pool
|
|
37
|
+
kinds as a timeline (index 0 only when the export carries no pool kinds);
|
|
38
|
+
`timelineName` picks a container by its pool name; an explicit
|
|
39
|
+
`timelineIndex` still means exactly that container. The result reports
|
|
40
|
+
`pickedBy`, the container's name and kind, and every container kept (a
|
|
41
|
+
timeline keeps its compounds recursively). Verified on the fixture: the
|
|
42
|
+
default yields E57_NESTED with E57_OUT and E57_IN kept; `timelineName`
|
|
43
|
+
E57_OUT yields E57_OUT + E57_IN; index 0 yields E57_IN alone.
|
|
44
|
+
|
|
5
45
|
## What's New in v2.181.0 — E127: DRT timelines get their real names, and compounds their kind
|
|
6
46
|
|
|
7
47
|
### 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.183.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.183.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
|
@@ -97,7 +97,8 @@ const injectIntoDrpSchema = z.object({
|
|
|
97
97
|
const extractFromDrpSchema = z.object({
|
|
98
98
|
drpPath: z.string().describe('Source .drp'),
|
|
99
99
|
outputPath: z.string().describe('Path for the emitted .drt'),
|
|
100
|
-
timelineIndex: z.number().int().nonnegative().optional().describe('Which SeqContainer to extract (0-based,
|
|
100
|
+
timelineIndex: z.number().int().nonnegative().optional().describe('Which SeqContainer to extract (0-based). Omitted: the first container the pool kinds as a TIMELINE — SeqContainers list name-sorted by DbId, so index 0 is often a compound\'s inner container (measured, E128)'),
|
|
101
|
+
timelineName: z.string().optional().describe('Extract the container the pool names this (timeline or compound) — see list_sequences'),
|
|
101
102
|
});
|
|
102
103
|
|
|
103
104
|
// Verified Resolve app-version → on-disk <ProjectVersion> map (the import GATE).
|
|
@@ -153,7 +154,7 @@ function requirePathArg(args, key, action) {
|
|
|
153
154
|
export const drtTool = {
|
|
154
155
|
name: 'drt',
|
|
155
156
|
description:
|
|
156
|
-
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble_from_interchange (EDL/OTIO/XML/AAF + sourceMap → IMPORTABLE RENDERING native .drt in one call; retimes AUTHOR — constant speed fwd/rev AND zero-speed freezes (EDL M2 000.0; render-proven frozen); cross-dissolves are AUTHORED when the cut abuts with handles both sides (render-verified on 19), else dropped with reason; fades AUTHOR across ALL FOUR formats (EDL BL legs, OTIO gap-adjacent Transitions, XMEML edge transitionitems, AAF filler-adjacent Transitions) and AAF overlap-consuming dissolves reconcile+author (CutPoint honored; before E93 they threw at the overlap gate) — BL legs become Solid Color generators and the fade a real clip-to-generator dissolve, luma-ramp render-verified, while Resolve\'s OWN EDL importer drops BL dissolves silently; ledger in `conform`), assemble (spec → IMPORTABLE native-schema .drt via template-spliced real structures; pass targetAppVersion e.g. \'19.1\' for pre-21 hosts), parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index,kind,nestedIn}] to drive a "which sequence?" picker — names and kind (timeline|compound) come from the pool folder, since a SeqContainer carries no timeline name and its first <Name> is a CLIP\'s (measured, E127); a compound container is nestedIn every timeline that places it, and a media-less clip named after a compound is tagged `compound` in parse), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
|
|
157
|
+
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble_from_interchange (EDL/OTIO/XML/AAF + sourceMap → IMPORTABLE RENDERING native .drt in one call; retimes AUTHOR — constant speed fwd/rev AND zero-speed freezes (EDL M2 000.0; render-proven frozen); cross-dissolves are AUTHORED when the cut abuts with handles both sides (render-verified on 19), else dropped with reason; fades AUTHOR across ALL FOUR formats (EDL BL legs, OTIO gap-adjacent Transitions, XMEML edge transitionitems, AAF filler-adjacent Transitions) and AAF overlap-consuming dissolves reconcile+author (CutPoint honored; before E93 they threw at the overlap gate) — BL legs become Solid Color generators and the fade a real clip-to-generator dissolve, luma-ramp render-verified, while Resolve\'s OWN EDL importer drops BL dissolves silently; ledger in `conform`), assemble (spec → IMPORTABLE native-schema .drt via template-spliced real structures; pass targetAppVersion e.g. \'19.1\' for pre-21 hosts), parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index,kind,nestedIn}] to drive a "which sequence?" picker — names and kind (timeline|compound) come from the pool folder, since a SeqContainer carries no timeline name and its first <Name> is a CLIP\'s (measured, E127); a compound container is nestedIn every timeline that places it, and a media-less clip named after a compound is tagged `compound` in parse), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — by timelineName, an explicit timelineIndex, or by default the first container the pool kinds as a TIMELINE (index 0 is name-sorted by DbId and was an inner compound on Resolve\'s own export, E128); a timeline keeps its compound containers recursively; feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
|
|
157
158
|
async handler({ action, args }) {
|
|
158
159
|
if (action === 'parse') {
|
|
159
160
|
const p = parseSchema.parse(requirePathArg(args, 'drtPath', 'parse'));
|
|
@@ -579,7 +580,32 @@ export const drtTool = {
|
|
|
579
580
|
const p = extractFromDrpSchema.parse(args);
|
|
580
581
|
const drpZip = await JSZip.loadAsync(await fs.readFile(p.drpPath));
|
|
581
582
|
const seqEntries = drt().listSeqContainerEntries(drpZip);
|
|
582
|
-
|
|
583
|
+
// Which container (E128): SeqContainers list name-sorted by DbId, so a
|
|
584
|
+
// bare index 0 picked whichever container's uuid sorted first — on
|
|
585
|
+
// Resolve's export of a compound timeline that was an INNER compound.
|
|
586
|
+
// Resolve by pool name/kind unless an index is given explicitly.
|
|
587
|
+
const poolNames = await drt().loadPoolSequenceNames(drpZip);
|
|
588
|
+
const containerInfo = [];
|
|
589
|
+
for (const entryName of seqEntries) {
|
|
590
|
+
const sx = await drpZip.file(entryName).async('string');
|
|
591
|
+
const sid = (sx.match(/<Sequence>([0-9a-f-]{36})<\/Sequence>/) || [])[1];
|
|
592
|
+
const pool = sid ? poolNames.get(sid) : null;
|
|
593
|
+
containerInfo.push({ entry: entryName, name: pool ? pool.name : null, kind: pool ? pool.kind : null });
|
|
594
|
+
}
|
|
595
|
+
let idx;
|
|
596
|
+
let pickedBy;
|
|
597
|
+
if (p.timelineName != null) {
|
|
598
|
+
idx = containerInfo.findIndex((c) => c.name === p.timelineName);
|
|
599
|
+
if (idx < 0) return { error: `no SeqContainer named ${JSON.stringify(p.timelineName)} — containers: ${containerInfo.map((c) => `${c.name ?? '?'}${c.kind ? ` (${c.kind})` : ''}`).join(', ')}` };
|
|
600
|
+
pickedBy = 'timelineName';
|
|
601
|
+
} else if (p.timelineIndex != null) {
|
|
602
|
+
idx = p.timelineIndex;
|
|
603
|
+
pickedBy = 'timelineIndex';
|
|
604
|
+
} else {
|
|
605
|
+
idx = containerInfo.findIndex((c) => c.kind === 'timeline');
|
|
606
|
+
pickedBy = idx >= 0 ? 'first timeline in the pool' : 'first container (pool carries no kinds)';
|
|
607
|
+
if (idx < 0) idx = 0;
|
|
608
|
+
}
|
|
583
609
|
if (idx >= seqEntries.length) {
|
|
584
610
|
return { error: `timelineIndex ${idx} out of range (${seqEntries.length} SeqContainers)` };
|
|
585
611
|
}
|
|
@@ -674,6 +700,9 @@ export const drtTool = {
|
|
|
674
700
|
outputPath: p.outputPath,
|
|
675
701
|
bytes: outBuf.length,
|
|
676
702
|
sourceSeqContainer: keepEntry,
|
|
703
|
+
pickedBy,
|
|
704
|
+
container: { name: containerInfo[idx].name, kind: containerInfo[idx].kind },
|
|
705
|
+
keptContainers: [...keepContainers].map((e) => ({ entry: e, name: (containerInfo.find((c) => c.entry === e) || {}).name ?? null })),
|
|
677
706
|
droppedTimelines,
|
|
678
707
|
note: 'The imported timeline is named after the FILE. Source must be a SAVED project export — ExportProject snapshots the saved DB state, so unsaved edits are absent.',
|
|
679
708
|
};
|
|
@@ -23,6 +23,7 @@ module.exports = {
|
|
|
23
23
|
// Canonical SeqContainer-entry matcher (both tool-authored SeqContainer<N>.xml and
|
|
24
24
|
// real Resolve SeqContainer/<uuid>.xml). Shared so callers don't re-inline the regex.
|
|
25
25
|
listSeqContainerEntries: drtParser.listSeqContainerEntries,
|
|
26
|
+
loadPoolSequenceNames: drtParser.loadPoolSequenceNames,
|
|
26
27
|
|
|
27
28
|
// Resolve version registry + retargeting (re-stamp to a target Resolve version).
|
|
28
29
|
// The registry + re-stamp core are universal across DRT/DRP/DRX (same version stamp);
|
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.183.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.183.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
|