davinci-resolve-mcp 2.71.0 → 2.72.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 +93 -0
- package/README.md +6 -6
- package/docs/SKILL.md +14 -1
- package/docs/contributing.md +2 -1
- package/docs/reference/api-coverage.md +28 -8
- package/docs/reference/api-limitations.md +59 -4
- package/docs/reference/resolve_scripting_api.txt +116 -10
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +175 -41
- package/src/utils/api_truth.py +210 -10
package/install.py
CHANGED
|
@@ -36,7 +36,7 @@ from src.utils.update_check import (
|
|
|
36
36
|
|
|
37
37
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
VERSION = "2.
|
|
39
|
+
VERSION = "2.72.0"
|
|
40
40
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
41
41
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
42
42
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
|
|
|
85
85
|
handlers=[logging.StreamHandler()],
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
VERSION = "2.
|
|
88
|
+
VERSION = "2.72.0"
|
|
89
89
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
90
90
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
91
91
|
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 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.72.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -1784,6 +1784,36 @@ def _requires_method(obj, method_name, min_version):
|
|
|
1784
1784
|
return None
|
|
1785
1785
|
return _err(f"{method_name} requires DaVinci Resolve {min_version}+")
|
|
1786
1786
|
|
|
1787
|
+
def _ai_result(returned):
|
|
1788
|
+
"""Normalize a Resolve 21 AI-method return into (ok, message).
|
|
1789
|
+
|
|
1790
|
+
The AI methods do not agree on how they report a missing Extras pack, and
|
|
1791
|
+
one of the two shapes is a trap. Verified live on Studio 21.0.2.4 with only
|
|
1792
|
+
AI Motion Deblur installed:
|
|
1793
|
+
|
|
1794
|
+
- `AnalyzeForSlate` -> False
|
|
1795
|
+
- `AnalyzeForIntellisearch` -> "Required package 'AI Intellisearch -
|
|
1796
|
+
Faster' is not installed."
|
|
1797
|
+
- `GenerateSpeech` -> "Required Package, 'AI Speech Generator' is not
|
|
1798
|
+
Installed."
|
|
1799
|
+
|
|
1800
|
+
A non-empty string is truthy, so `bool(returned)` reports success for a call
|
|
1801
|
+
that definitively did not run, and treating the return as a MediaPoolItem
|
|
1802
|
+
raises AttributeError. Route every AI return through here instead: a string
|
|
1803
|
+
is always a failure, and its text is the reason worth surfacing.
|
|
1804
|
+
"""
|
|
1805
|
+
if isinstance(returned, str):
|
|
1806
|
+
return False, returned.strip() or None
|
|
1807
|
+
return bool(returned), None
|
|
1808
|
+
|
|
1809
|
+
def _ai_result_payload(returned):
|
|
1810
|
+
"""`{"success": ...}` plus the Resolve-supplied reason when there is one."""
|
|
1811
|
+
ok, message = _ai_result(returned)
|
|
1812
|
+
payload = {"success": ok}
|
|
1813
|
+
if message:
|
|
1814
|
+
payload["error"] = message
|
|
1815
|
+
return payload
|
|
1816
|
+
|
|
1787
1817
|
def _is_truncated(text):
|
|
1788
1818
|
"""True if a transcription preview was cut off.
|
|
1789
1819
|
|
|
@@ -3795,6 +3825,81 @@ def _timeline_item_ids(items):
|
|
|
3795
3825
|
return ids
|
|
3796
3826
|
|
|
3797
3827
|
|
|
3828
|
+
def _timeline_items_presence(tl, items):
|
|
3829
|
+
"""Are these timeline items still on the timeline? present/absent/unknown.
|
|
3830
|
+
|
|
3831
|
+
'absent' is a positive finding: every item was identifiable and a
|
|
3832
|
+
completed track walk did not see any of them. A walk that raised, that
|
|
3833
|
+
could not enumerate a single track, or items whose unique ID cannot be
|
|
3834
|
+
read all yield 'unknown' — the readback saw nothing, which is not the
|
|
3835
|
+
same as nothing being there. Callers must never treat 'unknown' as
|
|
3836
|
+
verified-gone.
|
|
3837
|
+
"""
|
|
3838
|
+
target_ids = []
|
|
3839
|
+
unreadable_item = False
|
|
3840
|
+
for item in items:
|
|
3841
|
+
item_id = _safe_timeline_item_id(item)
|
|
3842
|
+
if item_id:
|
|
3843
|
+
target_ids.append(item_id)
|
|
3844
|
+
else:
|
|
3845
|
+
unreadable_item = True
|
|
3846
|
+
target_ids = set(target_ids)
|
|
3847
|
+
|
|
3848
|
+
tracks_walked = 0
|
|
3849
|
+
walk_failed = False
|
|
3850
|
+
for track_type in ("video", "audio", "subtitle"):
|
|
3851
|
+
try:
|
|
3852
|
+
track_count = int(tl.GetTrackCount(track_type) or 0)
|
|
3853
|
+
except Exception:
|
|
3854
|
+
walk_failed = True
|
|
3855
|
+
continue
|
|
3856
|
+
for index in range(1, track_count + 1):
|
|
3857
|
+
try:
|
|
3858
|
+
track_items = tl.GetItemListInTrack(track_type, index) or []
|
|
3859
|
+
except Exception:
|
|
3860
|
+
walk_failed = True
|
|
3861
|
+
continue
|
|
3862
|
+
tracks_walked += 1
|
|
3863
|
+
for track_item in track_items:
|
|
3864
|
+
# A sighting is definitive even if another track failed.
|
|
3865
|
+
if _safe_timeline_item_id(track_item) in target_ids:
|
|
3866
|
+
return "present"
|
|
3867
|
+
|
|
3868
|
+
if walk_failed or tracks_walked == 0 or unreadable_item or not target_ids:
|
|
3869
|
+
return "unknown"
|
|
3870
|
+
return "absent"
|
|
3871
|
+
|
|
3872
|
+
|
|
3873
|
+
def _timeline_delete_clips_verified(tl, items, ripple):
|
|
3874
|
+
"""Timeline.DeleteClips with readback-and-retry.
|
|
3875
|
+
|
|
3876
|
+
api_truth 'Timeline.DeleteClips (flaky first attempt)': the call can
|
|
3877
|
+
return False while every item is still present, and an identical retry
|
|
3878
|
+
then succeeds. On a False, read the tracks back:
|
|
3879
|
+
|
|
3880
|
+
absent -> the delete landed despite the False; report success.
|
|
3881
|
+
present -> retry the identical call once, then read back again.
|
|
3882
|
+
unknown -> report failure and do NOT retry. An unverifiable delete must
|
|
3883
|
+
not be claimed as success, and a retry whose outcome we
|
|
3884
|
+
equally cannot read is a second destructive call bought with
|
|
3885
|
+
no information.
|
|
3886
|
+
|
|
3887
|
+
ripple=True caveat: a retry is not idempotent in principle. If the first
|
|
3888
|
+
call deleted some items and left others, the readback reports 'present'
|
|
3889
|
+
for the survivors and the retry passes the original list back in — stale
|
|
3890
|
+
handles to already-deleted items included. That could not be made to
|
|
3891
|
+
misbehave against a fake; it is recorded, not resolved.
|
|
3892
|
+
"""
|
|
3893
|
+
if bool(tl.DeleteClips(items, ripple)):
|
|
3894
|
+
return True
|
|
3895
|
+
presence = _timeline_items_presence(tl, items)
|
|
3896
|
+
if presence != "present":
|
|
3897
|
+
return presence == "absent"
|
|
3898
|
+
if bool(tl.DeleteClips(items, ripple)):
|
|
3899
|
+
return True
|
|
3900
|
+
return _timeline_items_presence(tl, items) == "absent"
|
|
3901
|
+
|
|
3902
|
+
|
|
3798
3903
|
def _timeline_items_by_ids(tl, ids, track_types=("video", "audio", "subtitle")):
|
|
3799
3904
|
ids_set = {str(item_id) for item_id in ids if item_id is not None}
|
|
3800
3905
|
found = []
|
|
@@ -4168,7 +4273,7 @@ def _timeline_duplicate_clips_impl(proj, tl, p: Dict[str, Any], *, delete_source
|
|
|
4168
4273
|
seen_delete_ids.add(item_id)
|
|
4169
4274
|
if delete_items:
|
|
4170
4275
|
try:
|
|
4171
|
-
out["deleted_sources"] =
|
|
4276
|
+
out["deleted_sources"] = _timeline_delete_clips_verified(tl, delete_items, bool(p.get("ripple", False)))
|
|
4172
4277
|
out["deleted_source_ids"] = _timeline_item_ids(delete_items)
|
|
4173
4278
|
except Exception as exc:
|
|
4174
4279
|
out["deleted_sources"] = False
|
|
@@ -4283,7 +4388,7 @@ def _timeline_copy_range_impl(proj, tl, p: Dict[str, Any], *, overwrite: bool =
|
|
|
4283
4388
|
if existing_start < dest_end and existing_end > dest_start:
|
|
4284
4389
|
delete_targets.append(existing)
|
|
4285
4390
|
if delete_targets:
|
|
4286
|
-
deleted =
|
|
4391
|
+
deleted = _timeline_delete_clips_verified(tl, delete_targets, False)
|
|
4287
4392
|
|
|
4288
4393
|
results = []
|
|
4289
4394
|
for track_type, source_track, item, overlap_start, overlap_end in items:
|
|
@@ -4376,7 +4481,7 @@ def _timeline_lift_range_impl(tl, p: Dict[str, Any]):
|
|
|
4376
4481
|
return {"success": True, "deleted": 0, "range": {"start": start, "end": end}}
|
|
4377
4482
|
deleted_ids = _timeline_item_ids(delete_items)
|
|
4378
4483
|
return {
|
|
4379
|
-
"success":
|
|
4484
|
+
"success": _timeline_delete_clips_verified(tl, delete_items, bool(p.get("ripple", False))),
|
|
4380
4485
|
"deleted": len(delete_items),
|
|
4381
4486
|
"deleted_ids": deleted_ids,
|
|
4382
4487
|
"range": {"start": start, "end": end},
|
|
@@ -15384,6 +15489,7 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
|
|
|
15384
15489
|
delete_color_group(name) -> {success}
|
|
15385
15490
|
apply_fairlight_preset(preset_name) -> {success}
|
|
15386
15491
|
generate_speech(speech_generation_settings, timecode?) -> {success, new, new_id} — Resolve 21+, AI Speech Generator; creates new audio media (confirm-gated)
|
|
15492
|
+
reset_intellisearch_analysis() -> {success} — Resolve 21+; clears the project's IntelliSearch analysis data
|
|
15387
15493
|
"""
|
|
15388
15494
|
p = _params(params)
|
|
15389
15495
|
_, proj, err = _check()
|
|
@@ -15482,16 +15588,29 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
|
|
|
15482
15588
|
return blocked
|
|
15483
15589
|
with _ai_ledger_timed("generate_speech") as _rec:
|
|
15484
15590
|
new_item = proj.GenerateSpeech(settings, timecode)
|
|
15485
|
-
|
|
15486
|
-
|
|
15591
|
+
# GenerateSpeech returns an error STRING when the AI Speech Generator
|
|
15592
|
+
# Extra is absent (verified on Studio 21.0.2.4), not a MediaPoolItem.
|
|
15593
|
+
# A bare truthiness test lets that string through to .GetName() and
|
|
15594
|
+
# raises AttributeError, so normalize before touching the result.
|
|
15595
|
+
ok, message = _ai_result(new_item)
|
|
15596
|
+
_rec.success = ok
|
|
15597
|
+
if ok:
|
|
15487
15598
|
path, nbytes = _clip_file_size(new_item)
|
|
15488
15599
|
_rec.output_path = path
|
|
15489
15600
|
_rec.output_bytes = nbytes
|
|
15490
|
-
if not
|
|
15491
|
-
return {"success": False}
|
|
15601
|
+
if not ok:
|
|
15602
|
+
return {"success": False, "error": message} if message else {"success": False}
|
|
15492
15603
|
return {"success": True, "new": new_item.GetName(), "new_id": new_item.GetUniqueId(),
|
|
15493
15604
|
"output_path": _rec.output_path, "output_bytes": _rec.output_bytes}
|
|
15494
|
-
|
|
15605
|
+
elif action == "reset_intellisearch_analysis":
|
|
15606
|
+
missing = _requires_method(proj, "ResetIntellisearchAnalysis", "21.0")
|
|
15607
|
+
if missing:
|
|
15608
|
+
return missing
|
|
15609
|
+
with _ai_ledger_timed("reset_intellisearch_analysis") as _rec:
|
|
15610
|
+
result = _ai_result_payload(proj.ResetIntellisearchAnalysis())
|
|
15611
|
+
_rec.success = result["success"]
|
|
15612
|
+
return result
|
|
15613
|
+
return _unknown(action, ["get_name","set_name","get_setting","set_setting","get_unique_id","get_presets","set_preset","refresh_luts","get_gallery","export_frame_as_still","project_summary","load_burnin_preset","insert_audio","get_color_groups","add_color_group","delete_color_group","apply_fairlight_preset","generate_speech","reset_intellisearch_analysis"])
|
|
15495
15614
|
|
|
15496
15615
|
|
|
15497
15616
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -16931,17 +17050,17 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16931
17050
|
if missing:
|
|
16932
17051
|
return missing
|
|
16933
17052
|
with _ai_ledger_timed("perform_audio_classification") as _rec:
|
|
16934
|
-
|
|
16935
|
-
_rec.success =
|
|
16936
|
-
return
|
|
17053
|
+
result = _ai_result_payload(f.PerformAudioClassification())
|
|
17054
|
+
_rec.success = result["success"]
|
|
17055
|
+
return result
|
|
16937
17056
|
elif action == "clear_audio_classification":
|
|
16938
17057
|
missing = _requires_method(f, "ClearAudioClassification", "21.0")
|
|
16939
17058
|
if missing:
|
|
16940
17059
|
return missing
|
|
16941
17060
|
with _ai_ledger_timed("clear_audio_classification") as _rec:
|
|
16942
|
-
|
|
16943
|
-
_rec.success =
|
|
16944
|
-
return
|
|
17061
|
+
result = _ai_result_payload(f.ClearAudioClassification())
|
|
17062
|
+
_rec.success = result["success"]
|
|
17063
|
+
return result
|
|
16945
17064
|
elif action == "analyze_for_intellisearch":
|
|
16946
17065
|
missing = _requires_method(f, "AnalyzeForIntellisearch", "21.0")
|
|
16947
17066
|
if missing:
|
|
@@ -16949,9 +17068,9 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16949
17068
|
identify_faces = bool(_first_param(p, "identify_faces", "identifyFaces", default=False))
|
|
16950
17069
|
is_better_mode = bool(_first_param(p, "is_better_mode", "isBetterMode", default=False))
|
|
16951
17070
|
with _ai_ledger_timed("analyze_for_intellisearch") as _rec:
|
|
16952
|
-
|
|
16953
|
-
_rec.success =
|
|
16954
|
-
return
|
|
17071
|
+
result = _ai_result_payload(f.AnalyzeForIntellisearch(identify_faces, is_better_mode))
|
|
17072
|
+
_rec.success = result["success"]
|
|
17073
|
+
return result
|
|
16955
17074
|
elif action == "analyze_for_slate":
|
|
16956
17075
|
missing = _requires_method(f, "AnalyzeForSlate", "21.0")
|
|
16957
17076
|
if missing:
|
|
@@ -16960,9 +17079,9 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16960
17079
|
if marker_color not in _MARKER_COLORS:
|
|
16961
17080
|
return _err(f"Invalid marker_color {marker_color!r}. Valid colors: {', '.join(_MARKER_COLORS)}")
|
|
16962
17081
|
with _ai_ledger_timed("analyze_for_slate") as _rec:
|
|
16963
|
-
|
|
16964
|
-
_rec.success =
|
|
16965
|
-
return
|
|
17082
|
+
result = _ai_result_payload(f.AnalyzeForSlate(marker_color))
|
|
17083
|
+
_rec.success = result["success"]
|
|
17084
|
+
return result
|
|
16966
17085
|
elif action == "remove_motion_blur":
|
|
16967
17086
|
missing = _requires_method(f, "RemoveMotionBlur", "21.0")
|
|
16968
17087
|
if missing:
|
|
@@ -16987,11 +17106,18 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16987
17106
|
if blocked:
|
|
16988
17107
|
return blocked
|
|
16989
17108
|
with _ai_ledger_timed("remove_motion_blur") as _rec:
|
|
17109
|
+
# RemoveMotionBlur needs the AI Motion Deblur Extra, so it belongs to
|
|
17110
|
+
# the same family as the methods above: absent the pack, the return
|
|
17111
|
+
# can be an error STRING rather than the documented list. Iterating a
|
|
17112
|
+
# string yields characters, the pair-unpack raises, `except Exception`
|
|
17113
|
+
# swallows it, and the action reported success:true with created:[]
|
|
17114
|
+
# — a silent lie in the confirm-gated path that renders new media.
|
|
16990
17115
|
result = f.RemoveMotionBlur(deblur)
|
|
16991
|
-
|
|
17116
|
+
ok, message = _ai_result(result)
|
|
17117
|
+
_rec.success = ok
|
|
16992
17118
|
created = []
|
|
16993
17119
|
total_bytes = 0
|
|
16994
|
-
for pair in (result or []):
|
|
17120
|
+
for pair in (result or []) if ok else []:
|
|
16995
17121
|
try:
|
|
16996
17122
|
orig, new = pair
|
|
16997
17123
|
path, nbytes = _clip_file_size(new)
|
|
@@ -17005,7 +17131,10 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
17005
17131
|
if created:
|
|
17006
17132
|
_rec.output_path = created[0].get("output_path")
|
|
17007
17133
|
_rec.output_bytes = total_bytes or None
|
|
17008
|
-
|
|
17134
|
+
payload = {"success": ok, "created": created}
|
|
17135
|
+
if message:
|
|
17136
|
+
payload["error"] = message
|
|
17137
|
+
return payload
|
|
17009
17138
|
return _unknown(action, ["get_clips","get_name","get_subfolders","is_stale","get_unique_id","export","transcribe_audio","clear_transcription","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur"])
|
|
17010
17139
|
|
|
17011
17140
|
|
|
@@ -17329,17 +17458,17 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17329
17458
|
if missing:
|
|
17330
17459
|
return missing
|
|
17331
17460
|
with _ai_ledger_timed("perform_audio_classification", clip_id=p.get("clip_id")) as _rec:
|
|
17332
|
-
|
|
17333
|
-
_rec.success =
|
|
17334
|
-
return
|
|
17461
|
+
result = _ai_result_payload(clip.PerformAudioClassification())
|
|
17462
|
+
_rec.success = result["success"]
|
|
17463
|
+
return result
|
|
17335
17464
|
elif action == "clear_audio_classification":
|
|
17336
17465
|
missing = _requires_method(clip, "ClearAudioClassification", "21.0")
|
|
17337
17466
|
if missing:
|
|
17338
17467
|
return missing
|
|
17339
17468
|
with _ai_ledger_timed("clear_audio_classification", clip_id=p.get("clip_id")) as _rec:
|
|
17340
|
-
|
|
17341
|
-
_rec.success =
|
|
17342
|
-
return
|
|
17469
|
+
result = _ai_result_payload(clip.ClearAudioClassification())
|
|
17470
|
+
_rec.success = result["success"]
|
|
17471
|
+
return result
|
|
17343
17472
|
elif action == "analyze_for_intellisearch":
|
|
17344
17473
|
missing = _requires_method(clip, "AnalyzeForIntellisearch", "21.0")
|
|
17345
17474
|
if missing:
|
|
@@ -17347,9 +17476,9 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17347
17476
|
identify_faces = bool(_first_param(p, "identify_faces", "identifyFaces", default=False))
|
|
17348
17477
|
is_better_mode = bool(_first_param(p, "is_better_mode", "isBetterMode", default=False))
|
|
17349
17478
|
with _ai_ledger_timed("analyze_for_intellisearch", clip_id=p.get("clip_id")) as _rec:
|
|
17350
|
-
|
|
17351
|
-
_rec.success =
|
|
17352
|
-
return
|
|
17479
|
+
result = _ai_result_payload(clip.AnalyzeForIntellisearch(identify_faces, is_better_mode))
|
|
17480
|
+
_rec.success = result["success"]
|
|
17481
|
+
return result
|
|
17353
17482
|
elif action == "analyze_for_slate":
|
|
17354
17483
|
missing = _requires_method(clip, "AnalyzeForSlate", "21.0")
|
|
17355
17484
|
if missing:
|
|
@@ -17358,9 +17487,9 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17358
17487
|
if marker_color not in _MARKER_COLORS:
|
|
17359
17488
|
return _err(f"Invalid marker_color {marker_color!r}. Valid colors: {', '.join(_MARKER_COLORS)}")
|
|
17360
17489
|
with _ai_ledger_timed("analyze_for_slate", clip_id=p.get("clip_id")) as _rec:
|
|
17361
|
-
|
|
17362
|
-
_rec.success =
|
|
17363
|
-
return
|
|
17490
|
+
result = _ai_result_payload(clip.AnalyzeForSlate(marker_color))
|
|
17491
|
+
_rec.success = result["success"]
|
|
17492
|
+
return result
|
|
17364
17493
|
elif action == "remove_motion_blur":
|
|
17365
17494
|
missing = _requires_method(clip, "RemoveMotionBlur", "21.0")
|
|
17366
17495
|
if missing:
|
|
@@ -17385,14 +17514,19 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17385
17514
|
if blocked:
|
|
17386
17515
|
return blocked
|
|
17387
17516
|
with _ai_ledger_timed("remove_motion_blur", clip_id=p.get("clip_id")) as _rec:
|
|
17517
|
+
# Same shape as generate_speech: a MediaPoolItem return, and an error
|
|
17518
|
+
# STRING when the AI Motion Deblur Extra is absent. `_clip_file_size`
|
|
17519
|
+
# swallows its own AttributeError, so the string survived to
|
|
17520
|
+
# `.GetName()` and raised there instead.
|
|
17388
17521
|
new_clip = clip.RemoveMotionBlur(deblur)
|
|
17389
|
-
|
|
17390
|
-
|
|
17522
|
+
ok, message = _ai_result(new_clip)
|
|
17523
|
+
_rec.success = ok
|
|
17524
|
+
if ok:
|
|
17391
17525
|
path, nbytes = _clip_file_size(new_clip)
|
|
17392
17526
|
_rec.output_path = path
|
|
17393
17527
|
_rec.output_bytes = nbytes
|
|
17394
|
-
if not
|
|
17395
|
-
return {"success": False}
|
|
17528
|
+
if not ok:
|
|
17529
|
+
return {"success": False, "error": message} if message else {"success": False}
|
|
17396
17530
|
return {"success": True, "new": new_clip.GetName(), "new_id": new_clip.GetUniqueId(),
|
|
17397
17531
|
"output_path": _rec.output_path, "output_bytes": _rec.output_bytes}
|
|
17398
17532
|
elif action == "get_audio_mapping":
|
|
@@ -20773,7 +20907,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
20773
20907
|
blocked = _consume_confirm_token(action="timeline.delete_clips_ripple", params=p)
|
|
20774
20908
|
if blocked:
|
|
20775
20909
|
return blocked
|
|
20776
|
-
return {"success":
|
|
20910
|
+
return {"success": _timeline_delete_clips_verified(tl, found, ripple)}
|
|
20777
20911
|
elif action == "set_clips_linked":
|
|
20778
20912
|
ids_set = set(p["clip_ids"])
|
|
20779
20913
|
found = []
|
package/src/utils/api_truth.py
CHANGED
|
@@ -26,7 +26,7 @@ When you add or change a ``submit``-tagged entry, regenerate the report
|
|
|
26
26
|
"""
|
|
27
27
|
from typing import Any, Dict, List, Optional
|
|
28
28
|
|
|
29
|
-
VERIFIED_ON = "DaVinci Resolve Studio 21.0.
|
|
29
|
+
VERIFIED_ON = "DaVinci Resolve Studio 21.0.2"
|
|
30
30
|
|
|
31
31
|
# Each entry: symbol, object, reality, recommended, tags. `signature` optional.
|
|
32
32
|
API_TRUTH: List[Dict[str, Any]] = [
|
|
@@ -717,18 +717,143 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
717
717
|
{
|
|
718
718
|
"symbol": "hasattr() / getattr() on Resolve API objects (attribute fabrication)",
|
|
719
719
|
"object": "(all Resolve scripting objects)",
|
|
720
|
-
"reality": "
|
|
721
|
-
"
|
|
722
|
-
"
|
|
723
|
-
"impossible
|
|
724
|
-
"Razor, AddNode, GenerateProxy,
|
|
725
|
-
"
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
720
|
+
"reality": "UNRESOLVED — the two measurements do not test the same thing. "
|
|
721
|
+
"On 21.0.0 the bridge was recorded as returning a callable for "
|
|
722
|
+
"ANY attribute name, making capability detection by hasattr "
|
|
723
|
+
"impossible; the evidence was REAL API method names borrowed from "
|
|
724
|
+
"other object types (SetStart, Razor, AddNode, GenerateProxy, "
|
|
725
|
+
"AddSmartBin reported present on objects that do not have them). "
|
|
726
|
+
"A 21.0.2.4 control probe of the invented name "
|
|
727
|
+
"'TotallyMadeUpMethod_xyz123' returned getattr-callable False on "
|
|
728
|
+
"all eight object types, matching dir() in every case. That does "
|
|
729
|
+
"NOT refute the 21.0.0 record: if the bridge resolves any name "
|
|
730
|
+
"known to the RemoteObject method table rather than literally any "
|
|
731
|
+
"string, an invented name is correctly rejected on both builds and "
|
|
732
|
+
"the probe never exercised the failing case. Re-running the probe "
|
|
733
|
+
"with those five real names is what would settle it; until then, "
|
|
734
|
+
"assume fabrication is possible.",
|
|
735
|
+
"recommended": "Use dir(obj) membership for capability probes. It is correct "
|
|
736
|
+
"on every build measured, and it is the only form not affected "
|
|
737
|
+
"by whichever way this resolves. server._has_method uses "
|
|
738
|
+
"hasattr/getattr and so may over-report on builds where "
|
|
739
|
+
"fabrication is live — that is the case _requires_method gates "
|
|
740
|
+
"guard, so it matters most exactly where it is least tested. "
|
|
741
|
+
"Calling a fabricated method typically returns None/False with "
|
|
742
|
+
"no error.",
|
|
729
743
|
"tags": ["bridge", "introspection", "silent-failure"],
|
|
730
744
|
"submit": "bug",
|
|
731
745
|
},
|
|
746
|
+
{
|
|
747
|
+
"symbol": "Resolve 21 AI methods (AnalyzeForIntellisearch, GenerateSpeech, "
|
|
748
|
+
"AnalyzeForSlate) — inconsistent failure return type",
|
|
749
|
+
"object": "MediaPoolItem / Folder / Project",
|
|
750
|
+
"signature": "-> Bool (documented)",
|
|
751
|
+
"reality": "When the required Extras pack is not installed, these methods do "
|
|
752
|
+
"not agree on how they say so, and the documented Bool is not what "
|
|
753
|
+
"you get. Verified live on Studio 21.0.2.4 with only AI Motion "
|
|
754
|
+
"Deblur installed: AnalyzeForSlate returned False, but "
|
|
755
|
+
"AnalyzeForIntellisearch returned the STRING \"Required package 'AI "
|
|
756
|
+
"Intellisearch - Faster' is not installed.\" and GenerateSpeech "
|
|
757
|
+
"returned the STRING \"Required Package, 'AI Speech Generator' is "
|
|
758
|
+
"not Installed.\". A non-empty string is truthy in Python, so "
|
|
759
|
+
"bool(result) reports SUCCESS for a call that definitively did not "
|
|
760
|
+
"run, and treating GenerateSpeech's return as a MediaPoolItem "
|
|
761
|
+
"raises AttributeError: 'str' object has no attribute 'GetName'.",
|
|
762
|
+
"recommended": "Never bool() an AI-method return directly. Route it through "
|
|
763
|
+
"server._ai_result / _ai_result_payload, which treat any string "
|
|
764
|
+
"as a failure and surface its text as the error — the message is "
|
|
765
|
+
"the only machine-readable signal that an Extras pack is "
|
|
766
|
+
"missing, since there is no scripting API to enumerate "
|
|
767
|
+
"installed Extras.",
|
|
768
|
+
"tags": ["ai", "extras", "unreliable-return", "silent-failure", "resolve-21"],
|
|
769
|
+
"submit": "bug",
|
|
770
|
+
"mitigation": ["_ai_result", "_ai_result_payload"],
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
"symbol": "Installed AI Extras packs are not discoverable from scripting",
|
|
774
|
+
"object": "Resolve",
|
|
775
|
+
"reality": "AnalyzeForIntellisearch, AnalyzeForSlate, GenerateSpeech and "
|
|
776
|
+
"RemoveMotionBlur each require a separately-downloaded Extras pack, "
|
|
777
|
+
"but nothing in the scripting API reports which packs are installed. "
|
|
778
|
+
"A caller cannot distinguish 'the Extra is missing' from 'the "
|
|
779
|
+
"analysis ran and found nothing' ahead of time; on 21.0.2.4 two of "
|
|
780
|
+
"the four leak the reason only as free text in the return value, and "
|
|
781
|
+
"AnalyzeForSlate's bare False carries no reason at all.",
|
|
782
|
+
"recommended": "Until an API exists, treat a string return as the reason and "
|
|
783
|
+
"read the pack names out of the Extras directory "
|
|
784
|
+
"(Blackmagic Design/DaVinci Resolve/Extras/*/log.dpl1) for "
|
|
785
|
+
"diagnostics only — that path is undocumented and may change.",
|
|
786
|
+
"tags": ["ai", "extras", "introspection", "resolve-21"],
|
|
787
|
+
"submit": "missing",
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
"symbol": "Folder.AnalyzeForSlate / MediaPoolItem.AnalyzeForSlate markerColor",
|
|
791
|
+
"object": "MediaPoolItem / Folder",
|
|
792
|
+
"signature": "(markerColor) -> Bool",
|
|
793
|
+
"reality": "The shipped 21.0.2 scripting README says markerColor must be one of "
|
|
794
|
+
"the resolve.MARKER_* constants (resolve.MARKER_BLUE etc.). Those "
|
|
795
|
+
"constants do not exist: on Studio 21.0.2.4, "
|
|
796
|
+
"[c for c in dir(resolve) if c.startswith('MARKER_')] is empty. "
|
|
797
|
+
"There is therefore no documented-correct way to call this method. "
|
|
798
|
+
"The plain colour string the server passes is the only option "
|
|
799
|
+
"available, and it returns False here — though with AI Slate ID "
|
|
800
|
+
"absent, a string-rejection bug cannot be distinguished from the "
|
|
801
|
+
"missing pack on this machine.",
|
|
802
|
+
"recommended": "Keep passing the plain colour name (server._MARKER_COLORS) — "
|
|
803
|
+
"the documented constants are unavailable. Re-test on a machine "
|
|
804
|
+
"with the AI Slate ID Extra installed before concluding the "
|
|
805
|
+
"string form is rejected.",
|
|
806
|
+
# Deliberately NOT tagged `enum`: that tag denotes the issue-#70 class,
|
|
807
|
+
# where plain strings are rejected and a resolver must translate them
|
|
808
|
+
# into live enum constants. Here the documented constants do not exist
|
|
809
|
+
# on the handle at all, so there is nothing to resolve — the defect is
|
|
810
|
+
# in the documentation, not in a missing resolver.
|
|
811
|
+
"tags": ["ai", "extras", "missing-constant", "documentation", "resolve-21"],
|
|
812
|
+
"submit": "bug",
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
"symbol": "Project.ResetIntellisearchAnalysis",
|
|
816
|
+
"object": "Project",
|
|
817
|
+
"signature": "() -> Bool",
|
|
818
|
+
"reality": "Documented in the scripting README shipped with Resolve 21.0.2 "
|
|
819
|
+
"(dated 26 May 2026) but absent from the 5 May 2026 copy the repo "
|
|
820
|
+
"bundled, so it was missing from the coverage tables. Present in "
|
|
821
|
+
"dir(project) and returns True on Studio 21.0.2.4.",
|
|
822
|
+
"recommended": "Exposed as project_settings('reset_intellisearch_analysis').",
|
|
823
|
+
"tags": ["resolve-21", "documentation"],
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
"symbol": "Resolve.DisableBackgroundTasksForCurrentResolveSession",
|
|
827
|
+
"object": "Resolve",
|
|
828
|
+
"signature": "() -> None",
|
|
829
|
+
"reality": "Returns None, so a caller cannot tell whether it took effect, and "
|
|
830
|
+
"there is no Enable... counterpart anywhere in the shipped 21.0.2 "
|
|
831
|
+
"scripting README — the only documented way back is restarting "
|
|
832
|
+
"Resolve. The scope is the whole session, so a script disables "
|
|
833
|
+
"background tasks for every project open in that instance, not just "
|
|
834
|
+
"its own. Present in dir(resolve) on Studio 21.0.2.4; deliberately "
|
|
835
|
+
"not executed during validation for exactly that reason.",
|
|
836
|
+
"recommended": "Treat as irreversible within a session. server returns _ok() "
|
|
837
|
+
"unconditionally because there is nothing to check.",
|
|
838
|
+
"tags": ["resolve-21", "unreliable-return", "irreversible", "session-wide"],
|
|
839
|
+
"submit": "missing",
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"symbol": "MediaPoolItem.PerformAudioClassification / ClearAudioClassification",
|
|
843
|
+
"object": "MediaPoolItem / Folder",
|
|
844
|
+
"signature": "() -> Bool",
|
|
845
|
+
"reality": "Both work without any Extras pack and the effect is observable, "
|
|
846
|
+
"which is unusual for this family. Verified on Studio 21.0.2.4 "
|
|
847
|
+
"against a synthetic speech clip: PerformAudioClassification "
|
|
848
|
+
"returned True and set the clip property 'Category' from '' to "
|
|
849
|
+
"'Dialogue' (also surfacing Category/Subcategory in GetMetadata); "
|
|
850
|
+
"ClearAudioClassification returned True and reset 'Category' to "
|
|
851
|
+
"'Uncategorized' — note the cleared state is 'Uncategorized', NOT "
|
|
852
|
+
"the original empty string.",
|
|
853
|
+
"recommended": "Read back GetClipProperty('Category'); treat both '' and "
|
|
854
|
+
"'Uncategorized' as unclassified.",
|
|
855
|
+
"tags": ["ai", "audio", "resolve-21", "readback"],
|
|
856
|
+
},
|
|
732
857
|
{
|
|
733
858
|
"symbol": "subprocess inheriting stdin under the MCP stdio server",
|
|
734
859
|
"object": "(server runtime)",
|
|
@@ -807,6 +932,81 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
807
932
|
"when mirroring keep-ranges into clipInfos.",
|
|
808
933
|
"tags": ["timeline", "edit", "off-by-one", "readback"],
|
|
809
934
|
},
|
|
935
|
+
{
|
|
936
|
+
"symbol": "Timeline.DeleteClips (flaky first attempt)",
|
|
937
|
+
"object": "Timeline",
|
|
938
|
+
"signature": "([TimelineItem], ripple) -> bool",
|
|
939
|
+
"reality": "Can return False on the first call even when every item in "
|
|
940
|
+
"the list is a valid, present TimelineItem; an identical "
|
|
941
|
+
"immediate retry succeeds. Observed once, on Studio 21.0 "
|
|
942
|
+
"during a cut-video edit session (items confirmed still "
|
|
943
|
+
"present after the False, deleted cleanly on retry). Cause "
|
|
944
|
+
"unknown — do NOT read this as the ProjectManager."
|
|
945
|
+
"DeleteProject shape: that one has an identified mechanism "
|
|
946
|
+
"(the project being, or recently having been, current) that "
|
|
947
|
+
"retrying does not clear, whereas a single retry cleared "
|
|
948
|
+
"this in the one instance seen. One observation is not a "
|
|
949
|
+
"mechanism; if a retry is ever seen to fail repeatedly here, "
|
|
950
|
+
"this entry needs revisiting.",
|
|
951
|
+
"recommended": "Treat a False return as advisory: re-list the track and "
|
|
952
|
+
"check whether the items are actually gone; if still "
|
|
953
|
+
"present, retry the identical call once before failing. "
|
|
954
|
+
"A readback that raised, enumerated nothing, or covered "
|
|
955
|
+
"items whose unique ID cannot be read is UNKNOWN, not "
|
|
956
|
+
"gone — never report an unverifiable delete as success, "
|
|
957
|
+
"and do not spend a second destructive call on an "
|
|
958
|
+
"outcome you equally cannot read.",
|
|
959
|
+
"tags": ["unreliable-return", "flaky", "timeline", "edit"],
|
|
960
|
+
"submit": "bug",
|
|
961
|
+
"mitigation": ["_timeline_delete_clips_verified", "_timeline_items_presence"],
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
"symbol": "Timeline.DeleteClips (linked audio not deleted)",
|
|
965
|
+
"object": "Timeline",
|
|
966
|
+
"signature": "([TimelineItem], ripple) -> bool",
|
|
967
|
+
"reality": "Deleting video items does NOT delete their linked audio "
|
|
968
|
+
"items — the UI's linked-selection behavior does not apply "
|
|
969
|
+
"to the API, which deletes exactly the items passed. The "
|
|
970
|
+
"orphaned audio stays on its track and collides with any "
|
|
971
|
+
"later append into the same record range.",
|
|
972
|
+
"recommended": "When deleting a video item that has linked audio, list "
|
|
973
|
+
"the audio track(s) (timeline get_items, track_type "
|
|
974
|
+
"'audio'), find the overlapping linked items, and pass "
|
|
975
|
+
"their IDs in the same delete. Verify with "
|
|
976
|
+
"detect_gaps_overlaps across both track types.",
|
|
977
|
+
"tags": ["timeline", "edit", "audio", "silent-failure"],
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
"symbol": "MediaPool.AppendToTimeline with mixed-fps sources (duration floor)",
|
|
981
|
+
"object": "MediaPool",
|
|
982
|
+
"signature": "([{mediaPoolItem, startFrame, endFrame, recordFrame, ...}]) -> [TimelineItem]",
|
|
983
|
+
"reality": "start/endFrame are in SOURCE frames. When the source fps "
|
|
984
|
+
"differs from the timeline fps (e.g. 24.0 or 29.97 source in "
|
|
985
|
+
"a 23.976 timeline), Resolve converts the source range to "
|
|
986
|
+
"timeline frames by flooring — so a range planned to fill an "
|
|
987
|
+
"exact record slot lands one frame short, leaving a 1-frame "
|
|
988
|
+
"gap before the next clip.",
|
|
989
|
+
"recommended": "Plan durations in timeline frames "
|
|
990
|
+
"(floor(src_frames * timeline_fps / source_fps)); if the "
|
|
991
|
+
"floored duration misses the slot, extend endFrame by a "
|
|
992
|
+
"source frame and re-check. Always finish with "
|
|
993
|
+
"detect_gaps_overlaps.",
|
|
994
|
+
"tags": ["timeline", "edit", "off-by-one", "mixed-fps"],
|
|
995
|
+
"submit": "bug",
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
"symbol": "MediaPool.ImportMedia (current-folder destination only)",
|
|
999
|
+
"object": "MediaPool",
|
|
1000
|
+
"signature": "([paths] | [clipInfos]) -> [MediaPoolItem]",
|
|
1001
|
+
"reality": "Imports always land in the CURRENT media pool folder; the "
|
|
1002
|
+
"call has no destination-folder parameter, and passing an "
|
|
1003
|
+
"unrecognized one to the MCP tool is silently ignored.",
|
|
1004
|
+
"recommended": "SetCurrentFolder to the target bin first (media_pool "
|
|
1005
|
+
"set_current_folder), import, then restore the previous "
|
|
1006
|
+
"current folder if it matters.",
|
|
1007
|
+
"tags": ["media-pool", "import"],
|
|
1008
|
+
"submit": "missing",
|
|
1009
|
+
},
|
|
810
1010
|
{
|
|
811
1011
|
"symbol": "Graph.SetLUT (master-LUT-dir-only resolution)",
|
|
812
1012
|
"object": "Graph",
|