davinci-resolve-mcp 2.58.0 → 2.59.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 +43 -0
- package/README.md +4 -11
- package/install.py +12 -4
- package/package.json +1 -1
- package/resolve-advanced/README.md +6 -1
- package/resolve-advanced/server/aaf.mjs +104 -0
- package/resolve-advanced/server/author-interchange.mjs +152 -0
- package/resolve-advanced/server/editorial.mjs +17 -5
- package/resolve-advanced/server/prproj.mjs +262 -0
- package/resolve-advanced/server/sequences.mjs +86 -0
- package/resolve-advanced/server/tools/drt.mjs +9 -1
- package/resolve-advanced/server/tools/editorial.mjs +80 -7
- package/src/granular/common.py +1 -1
- package/src/server.py +286 -8
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.59.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -4340,6 +4340,7 @@ _TIMELINE_CONFORM_KERNEL_ACTIONS = [
|
|
|
4340
4340
|
"source_range_report",
|
|
4341
4341
|
"export_timeline_checked",
|
|
4342
4342
|
"import_timeline_checked",
|
|
4343
|
+
"import_from_drp",
|
|
4343
4344
|
"compare_timelines",
|
|
4344
4345
|
"probe_interchange_roundtrip",
|
|
4345
4346
|
"detect_missing_media",
|
|
@@ -5345,18 +5346,108 @@ def _timeline_media_coverage(tl) -> Dict[str, Any]:
|
|
|
5345
5346
|
return {"total": total, "linked": linked, "offline": total - linked}
|
|
5346
5347
|
|
|
5347
5348
|
|
|
5349
|
+
def _collect_media_pool_items(tl):
|
|
5350
|
+
"""Unique Media Pool Items referenced by a timeline's items (for RelinkClips)."""
|
|
5351
|
+
items = []
|
|
5352
|
+
seen = set()
|
|
5353
|
+
for track_type in ("video", "audio"):
|
|
5354
|
+
try:
|
|
5355
|
+
count = int(tl.GetTrackCount(track_type) or 0)
|
|
5356
|
+
except Exception:
|
|
5357
|
+
count = 0
|
|
5358
|
+
for i in range(1, count + 1):
|
|
5359
|
+
for item in (tl.GetItemListInTrack(track_type, i) or []):
|
|
5360
|
+
try:
|
|
5361
|
+
mpi = item.GetMediaPoolItem()
|
|
5362
|
+
except Exception:
|
|
5363
|
+
mpi = None
|
|
5364
|
+
if mpi is not None and id(mpi) not in seen:
|
|
5365
|
+
seen.add(id(mpi))
|
|
5366
|
+
items.append(mpi)
|
|
5367
|
+
return items
|
|
5368
|
+
|
|
5369
|
+
|
|
5370
|
+
def _binary_post_import_relink(tl, mp, search_roots):
|
|
5371
|
+
"""Fuzzy-relink parity for binary interchange (AAF): after import, ask Resolve's media pool
|
|
5372
|
+
to relink the timeline's Media Pool Items against each search root. This is the media-pool
|
|
5373
|
+
analogue of the XML path-rewrite relink (which can't run on a binary file)."""
|
|
5374
|
+
roots = [r for r in (search_roots or []) if isinstance(r, str) and os.path.isdir(r)]
|
|
5375
|
+
if not roots:
|
|
5376
|
+
return {"attempted": False, "reason": "no existing search roots", "roots": search_roots or []}
|
|
5377
|
+
items = _collect_media_pool_items(tl)
|
|
5378
|
+
if not items:
|
|
5379
|
+
return {"attempted": False, "reason": "no Media Pool Items to relink", "roots": roots}
|
|
5380
|
+
before = _timeline_media_coverage(tl)
|
|
5381
|
+
calls = []
|
|
5382
|
+
for root in roots:
|
|
5383
|
+
try:
|
|
5384
|
+
ok = bool(mp.RelinkClips(items, root))
|
|
5385
|
+
calls.append({"root": root, "ok": ok})
|
|
5386
|
+
except Exception as e:
|
|
5387
|
+
calls.append({"root": root, "ok": False, "error": str(e)})
|
|
5388
|
+
after = _timeline_media_coverage(tl)
|
|
5389
|
+
return {
|
|
5390
|
+
"attempted": True,
|
|
5391
|
+
"roots": roots,
|
|
5392
|
+
"item_count": len(items),
|
|
5393
|
+
"calls": calls,
|
|
5394
|
+
"before": before,
|
|
5395
|
+
"after": after,
|
|
5396
|
+
"linked_delta": after["linked"] - before["linked"],
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
|
|
5400
|
+
_PRPROJ_REFUSAL = (
|
|
5401
|
+
"Resolve has no native .prproj importer (Premiere's project format is gzip-compressed "
|
|
5402
|
+
"XML, not an interchange). Two offline routes, no Premiere needed: (1) read it with the "
|
|
5403
|
+
"advanced MCP — editorial.list_sequences / editorial.parse_interchange (format 'prproj'); "
|
|
5404
|
+
"(2) convert it to an importable interchange — editorial.convert_to_interchange "
|
|
5405
|
+
"(target 'otio'|'edl'|'drt') — then import that here with import_timeline_checked. "
|
|
5406
|
+
"Editorial timing/cuts/transitions/speed carry over; per-clip effects/Lumetri color do not. "
|
|
5407
|
+
"Alternatively export FCP7 XML / AAF / FCPXML from Premiere and conform that."
|
|
5408
|
+
)
|
|
5409
|
+
|
|
5410
|
+
# Binary interchange formats Resolve reads NATIVELY — the XML sanitize/relink pass
|
|
5411
|
+
# (which parses the file as text) does not apply. Relinking happens via the media
|
|
5412
|
+
# pool after import, not by rewriting the file.
|
|
5413
|
+
_BINARY_INTERCHANGE_EXTS = {".aaf"}
|
|
5414
|
+
|
|
5415
|
+
|
|
5348
5416
|
def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
5349
5417
|
path = p.get("path")
|
|
5350
5418
|
if not path:
|
|
5351
5419
|
return _err("path is required")
|
|
5352
5420
|
if not os.path.exists(path):
|
|
5353
5421
|
return _err(f"path does not exist: {path}")
|
|
5422
|
+
ext = os.path.splitext(path)[1].lower()
|
|
5423
|
+
if ext == ".prproj":
|
|
5424
|
+
return _err(_PRPROJ_REFUSAL, category="invalid_input")
|
|
5425
|
+
# AAF (and any binary interchange) is read natively by Resolve. The XML
|
|
5426
|
+
# sanitize/relink path parses the file as text, so it must be SKIPPED for AAF
|
|
5427
|
+
# even when sanitize_media / relink_search_roots is passed; fuzzy XML relink is
|
|
5428
|
+
# N/A (media links through the media pool). We still detect the created
|
|
5429
|
+
# (possibly offline) timeline via the before/after id diff.
|
|
5430
|
+
is_binary = ext in _BINARY_INTERCHANGE_EXTS
|
|
5354
5431
|
# sanitize_media (alias: relink_media) rewrites the XML to drop clipitems that
|
|
5355
5432
|
# reference missing media or are generators (slug/solid w/ no pathurl) — both
|
|
5356
5433
|
# abort Resolve's scripting-API import and leave the timeline fully offline.
|
|
5357
5434
|
# The original is only read; the sanitized copy lands in a temp dir, so the
|
|
5358
5435
|
# temp-path guard never applies to it.
|
|
5359
|
-
|
|
5436
|
+
sanitize_requested = bool(p.get("sanitize_media") or p.get("relink_media"))
|
|
5437
|
+
binary_relink_note = None
|
|
5438
|
+
_binary_has_roots = bool(p.get("relink_search_roots") or p.get("search_roots"))
|
|
5439
|
+
if is_binary and _binary_has_roots:
|
|
5440
|
+
binary_relink_note = (
|
|
5441
|
+
f"XML path-rewrite relink is N/A for {ext} (binary) — imported as-is, then relinked "
|
|
5442
|
+
"via the media pool against the search roots (see `relink`)."
|
|
5443
|
+
)
|
|
5444
|
+
elif is_binary and sanitize_requested:
|
|
5445
|
+
binary_relink_note = (
|
|
5446
|
+
f"sanitize_media is N/A for {ext} (binary) — Resolve reads it natively and media "
|
|
5447
|
+
"relinks via the media pool. Imported the file as-is; pass relink_search_roots to "
|
|
5448
|
+
"auto-relink after import."
|
|
5449
|
+
)
|
|
5450
|
+
sanitize = sanitize_requested and not is_binary
|
|
5360
5451
|
if not sanitize and p.get("require_temp_path", True) and not _render_temp_path_ok(path):
|
|
5361
5452
|
return _err(
|
|
5362
5453
|
"path must be under the system temp directory unless require_temp_path=False",
|
|
@@ -5375,7 +5466,7 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
5375
5466
|
search_roots = p.get("relink_search_roots") or p.get("search_roots")
|
|
5376
5467
|
if isinstance(search_roots, str):
|
|
5377
5468
|
search_roots = [search_roots]
|
|
5378
|
-
if search_roots and not sanitize:
|
|
5469
|
+
if search_roots and not sanitize and not is_binary:
|
|
5379
5470
|
sanitize = True # relinking is meaningless without the sanitize/rewrite pass
|
|
5380
5471
|
|
|
5381
5472
|
sanitize_report = None
|
|
@@ -5412,6 +5503,8 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
5412
5503
|
out = _ok(path=path, import_path=import_path, options=options, would_import=True)
|
|
5413
5504
|
if sanitize_report is not None:
|
|
5414
5505
|
out["sanitize"] = sanitize_report
|
|
5506
|
+
if binary_relink_note:
|
|
5507
|
+
out["note"] = binary_relink_note
|
|
5415
5508
|
return out
|
|
5416
5509
|
|
|
5417
5510
|
before_ids = set()
|
|
@@ -5432,15 +5525,32 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
5432
5525
|
if t and str(t.GetUniqueId()) not in before_ids:
|
|
5433
5526
|
imported = t
|
|
5434
5527
|
if not imported:
|
|
5528
|
+
if is_binary:
|
|
5529
|
+
remediation = (
|
|
5530
|
+
f"Resolve created no timeline from this {ext}. Verify it exports/opens in "
|
|
5531
|
+
"Resolve directly, or convert to FCP7 XML / FCPXML upstream and import that."
|
|
5532
|
+
)
|
|
5533
|
+
elif sanitize:
|
|
5534
|
+
remediation = None
|
|
5535
|
+
else:
|
|
5536
|
+
remediation = (
|
|
5537
|
+
"This XML may reference missing media or contain generator clips, which "
|
|
5538
|
+
"abort the scripting-API import. Retry with sanitize_media=True."
|
|
5539
|
+
)
|
|
5435
5540
|
return _err(
|
|
5436
5541
|
"Failed to import timeline (Resolve created no timeline)",
|
|
5437
|
-
remediation=
|
|
5438
|
-
"This XML may reference missing media or contain generator clips, which "
|
|
5439
|
-
"abort the scripting-API import. Retry with sanitize_media=True.",
|
|
5542
|
+
remediation=remediation,
|
|
5440
5543
|
)
|
|
5441
5544
|
|
|
5442
5545
|
imported_id = str(imported.GetUniqueId())
|
|
5443
5546
|
media = _timeline_media_coverage(imported)
|
|
5547
|
+
# AAF fuzzy-relink parity: when search roots are given for a binary import, relink through
|
|
5548
|
+
# the media pool after import (the XML path-rewrite relink can't run on a binary file).
|
|
5549
|
+
relink_result = None
|
|
5550
|
+
if is_binary and search_roots:
|
|
5551
|
+
relink_result = _binary_post_import_relink(imported, mp, search_roots)
|
|
5552
|
+
if relink_result.get("after"):
|
|
5553
|
+
media = relink_result["after"]
|
|
5444
5554
|
out = _ok(
|
|
5445
5555
|
name=imported.GetName(),
|
|
5446
5556
|
id=imported_id,
|
|
@@ -5451,15 +5561,166 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
5451
5561
|
)
|
|
5452
5562
|
if sanitize_report is not None:
|
|
5453
5563
|
out["sanitize"] = sanitize_report
|
|
5564
|
+
if relink_result is not None:
|
|
5565
|
+
out["relink"] = relink_result
|
|
5566
|
+
if binary_relink_note:
|
|
5567
|
+
out["note"] = binary_relink_note
|
|
5454
5568
|
if media["total"] and media["offline"]:
|
|
5455
5569
|
msg = f"{media['offline']} of {media['total']} timeline items are offline (no linked media)."
|
|
5456
|
-
if
|
|
5570
|
+
if is_binary:
|
|
5571
|
+
msg += (" Relink via the media pool (right-click → Relink Clips) or point Resolve "
|
|
5572
|
+
"at the media roots — AAF media links there, not by rewriting the file.")
|
|
5573
|
+
elif not sanitize:
|
|
5457
5574
|
msg += (" Retry with sanitize_media=True to drop missing-media/generator "
|
|
5458
5575
|
"clips so the remaining media links automatically.")
|
|
5459
5576
|
out["warning"] = msg
|
|
5460
5577
|
return out
|
|
5461
5578
|
|
|
5462
5579
|
|
|
5580
|
+
# A .drp/.drt is a zip of SeqContainer*.xml entries. Two on-disk naming conventions,
|
|
5581
|
+
# mirroring the Node drt-format parser: tool-authored `<folder>/SeqContainer<N>.xml`
|
|
5582
|
+
# and real-Resolve `SeqContainer/<uuid>.xml`. Never match MpFolder/project/Gallery.
|
|
5583
|
+
_SEQ_CONTAINER_RE = re.compile(r"(^|/)SeqContainer(\d*\.xml|/[^/]+\.xml)$")
|
|
5584
|
+
|
|
5585
|
+
|
|
5586
|
+
def _drp_seq_containers(zf) -> List[Dict[str, Any]]:
|
|
5587
|
+
"""List a .drp/.drt zip's SeqContainers as [{entry, name, index}] (0-based, sorted)."""
|
|
5588
|
+
entries = sorted(n for n in zf.namelist() if not n.endswith("/") and _SEQ_CONTAINER_RE.search(n))
|
|
5589
|
+
out = []
|
|
5590
|
+
for index, entry in enumerate(entries):
|
|
5591
|
+
try:
|
|
5592
|
+
xml = zf.read(entry).decode("utf-8", "replace")
|
|
5593
|
+
except Exception:
|
|
5594
|
+
xml = ""
|
|
5595
|
+
m = re.search(r"<Name>([\s\S]*?)</Name>", xml)
|
|
5596
|
+
out.append({"entry": entry, "name": (m.group(1).strip() if m else None), "index": index})
|
|
5597
|
+
return out
|
|
5598
|
+
|
|
5599
|
+
|
|
5600
|
+
def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str) -> None:
|
|
5601
|
+
"""Write a minimal .drt (zip) holding one SeqContainer as Primary1/SeqContainer1.xml."""
|
|
5602
|
+
import zipfile
|
|
5603
|
+
|
|
5604
|
+
with zipfile.ZipFile(drp_path, "r") as zf:
|
|
5605
|
+
xml = zf.read(seq_entry)
|
|
5606
|
+
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as out:
|
|
5607
|
+
out.writestr("Primary1/SeqContainer1.xml", xml)
|
|
5608
|
+
out.writestr(
|
|
5609
|
+
"metadata.json",
|
|
5610
|
+
json.dumps(
|
|
5611
|
+
{
|
|
5612
|
+
"source": "import_from_drp",
|
|
5613
|
+
"sourceDrp": drp_path,
|
|
5614
|
+
"sourceSeqContainer": seq_entry,
|
|
5615
|
+
"exportedFrom": "davinci-resolve-mcp timeline.import_from_drp",
|
|
5616
|
+
},
|
|
5617
|
+
indent=2,
|
|
5618
|
+
),
|
|
5619
|
+
)
|
|
5620
|
+
|
|
5621
|
+
|
|
5622
|
+
def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
5623
|
+
"""
|
|
5624
|
+
Extract the chosen timelines from a .drp (offline zip surgery) and import each into the
|
|
5625
|
+
running Resolve. Requires Resolve open. Mirrors: drt.extract_from_drp → import_timeline_checked,
|
|
5626
|
+
but as one call returning per-timeline results.
|
|
5627
|
+
"""
|
|
5628
|
+
import zipfile
|
|
5629
|
+
|
|
5630
|
+
drp_path = p.get("drpPath") or p.get("drp_path") or p.get("path")
|
|
5631
|
+
if not drp_path:
|
|
5632
|
+
return _err("drpPath is required")
|
|
5633
|
+
if not os.path.exists(drp_path):
|
|
5634
|
+
return _err(f"drpPath does not exist: {drp_path}")
|
|
5635
|
+
ext = os.path.splitext(drp_path)[1].lower()
|
|
5636
|
+
if ext == ".prproj":
|
|
5637
|
+
return _err(_PRPROJ_REFUSAL, category="invalid_input")
|
|
5638
|
+
|
|
5639
|
+
try:
|
|
5640
|
+
with zipfile.ZipFile(drp_path, "r") as zf:
|
|
5641
|
+
containers = _drp_seq_containers(zf)
|
|
5642
|
+
except zipfile.BadZipFile:
|
|
5643
|
+
return _err(f"Not a valid .drp/.drt (not a zip archive): {drp_path}", category="invalid_input")
|
|
5644
|
+
if not containers:
|
|
5645
|
+
return _err(f"No SeqContainer entries found in {drp_path} — is this a .drp/.drt?", category="invalid_input")
|
|
5646
|
+
|
|
5647
|
+
# Selection: by name(s), by 0-based index(es), or ALL when neither is given.
|
|
5648
|
+
want_names = p.get("timelineNames") or p.get("timeline_names")
|
|
5649
|
+
if isinstance(want_names, str):
|
|
5650
|
+
want_names = [want_names]
|
|
5651
|
+
want_indexes = p.get("timelineIndexes") or p.get("timeline_indexes")
|
|
5652
|
+
if isinstance(want_indexes, int):
|
|
5653
|
+
want_indexes = [want_indexes]
|
|
5654
|
+
|
|
5655
|
+
selected: List[Dict[str, Any]] = []
|
|
5656
|
+
available = [{"name": c["name"], "index": c["index"]} for c in containers]
|
|
5657
|
+
if want_names:
|
|
5658
|
+
by_name = {}
|
|
5659
|
+
for c in containers:
|
|
5660
|
+
if c["name"]:
|
|
5661
|
+
by_name.setdefault(c["name"], c)
|
|
5662
|
+
for nm in want_names:
|
|
5663
|
+
c = by_name.get(nm)
|
|
5664
|
+
if not c:
|
|
5665
|
+
return _err(
|
|
5666
|
+
f"Timeline named {nm!r} not found in .drp",
|
|
5667
|
+
remediation=f"Available: {[a['name'] for a in available]}",
|
|
5668
|
+
category="invalid_input",
|
|
5669
|
+
)
|
|
5670
|
+
selected.append(c)
|
|
5671
|
+
elif want_indexes:
|
|
5672
|
+
for i in want_indexes:
|
|
5673
|
+
match = next((c for c in containers if c["index"] == int(i)), None)
|
|
5674
|
+
if not match:
|
|
5675
|
+
return _err(f"timelineIndex {i} out of range (0..{len(containers) - 1})", category="invalid_input")
|
|
5676
|
+
selected.append(match)
|
|
5677
|
+
else:
|
|
5678
|
+
selected = list(containers)
|
|
5679
|
+
|
|
5680
|
+
# Per-timeline options passthrough (importSourceClips etc.) minus selection keys.
|
|
5681
|
+
base = {
|
|
5682
|
+
k: v
|
|
5683
|
+
for k, v in p.items()
|
|
5684
|
+
if k not in ("drpPath", "drp_path", "path", "timelineNames", "timeline_names", "timelineIndexes", "timeline_indexes", "dry_run")
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5687
|
+
tmp_dir = tempfile.mkdtemp(prefix="drp_import_")
|
|
5688
|
+
results = []
|
|
5689
|
+
imported_count = 0
|
|
5690
|
+
for c in selected:
|
|
5691
|
+
safe = re.sub(r"[^A-Za-z0-9._-]+", "_", c["name"] or f"seq{c['index'] + 1}")
|
|
5692
|
+
drt_path = os.path.join(tmp_dir, f"{c['index'] + 1}_{safe}.drt")
|
|
5693
|
+
entry = {"requested": c["name"], "index": c["index"]}
|
|
5694
|
+
try:
|
|
5695
|
+
_extract_seqcontainer_from_drp(drp_path, c["entry"], drt_path)
|
|
5696
|
+
except Exception as e:
|
|
5697
|
+
entry.update(success=False, error=f"extract failed: {e}")
|
|
5698
|
+
results.append(entry)
|
|
5699
|
+
continue
|
|
5700
|
+
if p.get("dry_run"):
|
|
5701
|
+
entry.update(success=True, would_import=True, drt_path=drt_path)
|
|
5702
|
+
results.append(entry)
|
|
5703
|
+
continue
|
|
5704
|
+
sub = dict(base)
|
|
5705
|
+
sub["path"] = drt_path
|
|
5706
|
+
# .drt lives under a temp dir; the temp-path guard passes regardless.
|
|
5707
|
+
res = _import_timeline_checked(proj, mp, sub)
|
|
5708
|
+
ok = bool(res.get("success"))
|
|
5709
|
+
entry.update(res)
|
|
5710
|
+
if ok:
|
|
5711
|
+
imported_count += 1
|
|
5712
|
+
results.append(entry)
|
|
5713
|
+
|
|
5714
|
+
return _ok(
|
|
5715
|
+
drpPath=drp_path,
|
|
5716
|
+
selected=len(selected),
|
|
5717
|
+
imported=imported_count,
|
|
5718
|
+
available=available,
|
|
5719
|
+
results=results,
|
|
5720
|
+
dry_run=bool(p.get("dry_run")),
|
|
5721
|
+
)
|
|
5722
|
+
|
|
5723
|
+
|
|
5463
5724
|
def _timeline_by_selector(proj, p: Dict[str, Any], *, prefix: str):
|
|
5464
5725
|
timeline_id = p.get(f"{prefix}_timeline_id") or p.get(f"{prefix}_id")
|
|
5465
5726
|
timeline_index = p.get(f"{prefix}_timeline_index") or p.get(f"{prefix}_index")
|
|
@@ -18366,7 +18627,14 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
18366
18627
|
source_range_report(handles?, merge?) -> {ranges, occurrences}
|
|
18367
18628
|
export_timeline_checked(path, format?|type?, subtype?, require_temp_path?, dry_run?) -> {success, path, size}
|
|
18368
18629
|
import_timeline_checked(path, options?, timeline_name?, import_source_clips?, sanitize_media?, require_temp_path?, dry_run?) -> {success, name, id, media, sanitize?, warning?}
|
|
18369
|
-
Imports an FCP7 xmeml / FCPXML timeline.
|
|
18630
|
+
Imports an FCP7 xmeml / FCPXML / AAF timeline. AAF (.aaf) is read natively by
|
|
18631
|
+
Resolve — the XML sanitize pass is SKIPPED for it (media relinks via the media
|
|
18632
|
+
pool, not by rewriting the file); a `note` says so. For AAF, passing
|
|
18633
|
+
relink_search_roots triggers a POST-import media-pool relink (RelinkClips against
|
|
18634
|
+
each root) — fuzzy-relink parity by a media-pool mechanism; the `relink` block
|
|
18635
|
+
reports before/after coverage. Premiere .prproj has no Resolve importer — refused
|
|
18636
|
+
with a message pointing to the advanced MCP's offline read + convert_to_interchange
|
|
18637
|
+
bridge (no Premiere needed). `media` reports {total, linked, offline}
|
|
18370
18638
|
coverage; `warning` flags offline items. sanitize_media=True (recommended for
|
|
18371
18639
|
Premiere/FCP exports) rewrites the XML first — dropping clipitems that reference
|
|
18372
18640
|
MISSING media or are generators (slug/solid with no media file), both of which
|
|
@@ -18384,6 +18652,11 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
18384
18652
|
metric; a conflicting relink is VETOED — reverted and reported under
|
|
18385
18653
|
`sanitize.flagged` for human review, never silently applied. verify_threshold
|
|
18386
18654
|
(default 0.90) sets the structural-match bar.
|
|
18655
|
+
import_from_drp(drpPath, timelineNames?|timelineIndexes?, import_source_clips?, dry_run?) -> {success, selected, imported, available, results}
|
|
18656
|
+
Extract chosen timelines from a .drp (offline zip surgery → temp .drt each) and
|
|
18657
|
+
import each into the running Resolve. Omit the selector to import ALL. Enumerate
|
|
18658
|
+
first with the advanced MCP drt.list_sequences / editorial.list_sequences to get
|
|
18659
|
+
[{id, name, eventCount}] for a "which sequence?" picker. Requires Resolve open.
|
|
18387
18660
|
compare_timelines(right_timeline_id?|right_timeline_index?|left_snapshot?, right_snapshot?) -> {match, differences}
|
|
18388
18661
|
probe_interchange_roundtrip(format?, output_dir?, cleanup_imported?) -> {success, export, import, comparison}
|
|
18389
18662
|
detect_missing_media(sanitized?|sanitize_paths?, omit_raw_paths?) -> {missing, missing_count, diagnosis}
|
|
@@ -18438,6 +18711,11 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
18438
18711
|
if mp_err:
|
|
18439
18712
|
return mp_err
|
|
18440
18713
|
return _import_timeline_checked(proj, mp, p)
|
|
18714
|
+
elif action == "import_from_drp":
|
|
18715
|
+
_, _, mp, mp_err = _get_mp()
|
|
18716
|
+
if mp_err:
|
|
18717
|
+
return mp_err
|
|
18718
|
+
return _import_from_drp(proj, mp, p)
|
|
18441
18719
|
elif action == "safe_auto_sync_audio":
|
|
18442
18720
|
_, _, mp, mp_err = _get_mp()
|
|
18443
18721
|
if mp_err:
|