davinci-resolve-mcp 2.82.0 → 2.86.1
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 +270 -1
- package/README.md +16 -3
- package/README.zh-CN.md +261 -0
- package/docs/SKILL.md +6 -3
- package/docs/process/release-process.md +6 -0
- package/docs/reference/api-coverage.md +17 -10
- package/docs/reference/api-limitations.md +23 -5
- package/install.py +3 -3
- package/package.json +1 -1
- package/resolve-advanced/server/db-patch.mjs +43 -4
- package/resolve-advanced/vendor/drp-format/subtitle-style.js +11 -5
- package/scripts/doctor.py +4 -2
- package/scripts/resolve_bridge_launcher.py +2 -1
- package/src/analysis_dashboard.py +2 -2
- package/src/granular/common.py +1 -1
- package/src/granular/timeline_item.py +27 -1
- package/src/server.py +221 -21
- package/src/utils/api_truth.py +86 -7
- package/src/utils/clip_colors.py +94 -0
- package/src/utils/edit_engine.py +6 -0
- package/src/utils/resolve_bridge_client.py +6 -3
- package/src/utils/resolve_connection.py +47 -12
- package/src/utils/resolve_versions.py +338 -0
- package/src/utils/swallowed_retakes.py +139 -0
- package/src/utils/take_ranking.py +18 -1
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.86.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -46,6 +46,8 @@ for p in [current_dir, project_dir]:
|
|
|
46
46
|
from src.utils.cdl import normalize_cdl_payload
|
|
47
47
|
from src.utils.mcp_stdio import run_fastmcp_stdio
|
|
48
48
|
from src.utils.api_truth import lookup_api_truth, VERIFIED_ON as _API_TRUTH_VERIFIED_ON
|
|
49
|
+
from src.utils import clip_colors as _clip_colors
|
|
50
|
+
from src.utils import resolve_versions as _resolve_versions
|
|
49
51
|
from src.utils.contracts import validate as _validate_params
|
|
50
52
|
from src.utils.cut_ir import build_cut_list as _build_cut_list
|
|
51
53
|
from src.utils.page_lock import (
|
|
@@ -179,9 +181,10 @@ mcp = FastMCP(
|
|
|
179
181
|
"if one is already running they never launch a second. "
|
|
180
182
|
"On a connection error, read the error's own remediation field — it names the "
|
|
181
183
|
"fix that applies. External scripting is Studio-only, but the free edition is "
|
|
182
|
-
"reachable via the in-app bridge (Workspace > Scripts > resolve_bridge
|
|
183
|
-
"
|
|
184
|
-
"
|
|
184
|
+
"reachable via the in-app bridge (Workspace > Scripts > resolve_bridge — it is "
|
|
185
|
+
"used automatically when external scripting is unavailable; "
|
|
186
|
+
"DAVINCI_RESOLVE_BRIDGE=1 only forces it), so a connection error does NOT mean "
|
|
187
|
+
"the free edition is unsupported."
|
|
185
188
|
),
|
|
186
189
|
)
|
|
187
190
|
|
|
@@ -936,8 +939,8 @@ def get_resolve():
|
|
|
936
939
|
"DaVinci Resolve is already running but is not answering the scripting API, "
|
|
937
940
|
"so it will NOT be launched again. Either enable Preferences > General > "
|
|
938
941
|
"'External scripting using' = Local (Studio only), or, on the free edition, "
|
|
939
|
-
"use the in-app bridge: install it
|
|
940
|
-
"
|
|
942
|
+
"use the in-app bridge: install it and run Workspace > Scripts > resolve_bridge "
|
|
943
|
+
"— once running it is used automatically, no environment variable needed."
|
|
941
944
|
)
|
|
942
945
|
return None
|
|
943
946
|
if already_running is None:
|
|
@@ -990,8 +993,10 @@ def _not_connected_error():
|
|
|
990
993
|
reason="External scripting is a Studio feature; the free edition refuses it "
|
|
991
994
|
"regardless of the preference. Resolve was NOT launched again.",
|
|
992
995
|
remediation="On Studio: Preferences > General > 'External scripting using' = Local. "
|
|
993
|
-
"On the free edition: install the in-app bridge
|
|
994
|
-
"Workspace > Scripts > resolve_bridge
|
|
996
|
+
"On the free edition: install the in-app bridge and run "
|
|
997
|
+
"Workspace > Scripts > resolve_bridge — once it is running it is used "
|
|
998
|
+
"automatically, no environment variable needed "
|
|
999
|
+
"(DAVINCI_RESOLVE_BRIDGE=1 only forces it and disables this fallback).",
|
|
995
1000
|
# Which Resolve is refusing matters to the reader: a headless render
|
|
996
1001
|
# worker and the editor the user is looking at warrant different
|
|
997
1002
|
# responses, and the in-app bridge is not an option for the former.
|
|
@@ -3541,7 +3546,9 @@ def _coerce_item_list(value):
|
|
|
3541
3546
|
|
|
3542
3547
|
def _get_selected_timeline_items(tl):
|
|
3543
3548
|
warnings = []
|
|
3544
|
-
|
|
3549
|
+
# GetSelectedClips is the documented name since Resolve 21.0.4; the other two
|
|
3550
|
+
# are legacy speculative probes kept for older/renamed builds.
|
|
3551
|
+
for method_name in ("GetSelectedClips", "GetSelectedTimelineItems", "GetSelectedItems"):
|
|
3545
3552
|
method = getattr(tl, method_name, None)
|
|
3546
3553
|
if not callable(method):
|
|
3547
3554
|
continue
|
|
@@ -7731,6 +7738,7 @@ _MEDIA_POOL_ITEM_METHODS = [
|
|
|
7731
7738
|
"GetMarkInOut",
|
|
7732
7739
|
"SetMarkInOut",
|
|
7733
7740
|
"ClearMarkInOut",
|
|
7741
|
+
"GetTimeline",
|
|
7734
7742
|
]
|
|
7735
7743
|
|
|
7736
7744
|
_MEDIA_POOL_METHODS = [
|
|
@@ -9006,6 +9014,66 @@ def _safe_int(value: Any, default: int, *, minimum: Optional[int] = None, maximu
|
|
|
9006
9014
|
return parsed
|
|
9007
9015
|
|
|
9008
9016
|
|
|
9017
|
+
def _set_clip_color_checked(obj, color, *, kind: str):
|
|
9018
|
+
"""SetClipColor, with the bare bool turned into something a caller can act on.
|
|
9019
|
+
|
|
9020
|
+
Two measured failures hide behind that bool (issue #124, enumerated live on
|
|
9021
|
+
Studio 19.1.3.7):
|
|
9022
|
+
|
|
9023
|
+
- A name outside the 16-name Edit-page palette is refused with `False` and
|
|
9024
|
+
nothing else. The marker constants are the only colour vocabulary the
|
|
9025
|
+
scripting reference enumerates, so they are what an agent reaches for, and
|
|
9026
|
+
most of them are refused.
|
|
9027
|
+
- On generator and title items the call returns `True` and the colour does
|
|
9028
|
+
not persist — `GetClipColor` still reads empty afterwards. Media-backed
|
|
9029
|
+
items on the same timeline persist correctly, so the bool is honest for
|
|
9030
|
+
some items and a lie for others.
|
|
9031
|
+
|
|
9032
|
+
So the return value is never trusted on its own: the colour is read back and
|
|
9033
|
+
the response reports what actually stuck.
|
|
9034
|
+
"""
|
|
9035
|
+
returned = bool(obj.SetClipColor(color))
|
|
9036
|
+
readback = None
|
|
9037
|
+
if _has_method(obj, "GetClipColor"):
|
|
9038
|
+
try:
|
|
9039
|
+
readback = obj.GetClipColor()
|
|
9040
|
+
except Exception: # pragma: no cover - defensive; getter is documented
|
|
9041
|
+
readback = None
|
|
9042
|
+
persisted = (readback == color) if readback is not None else None
|
|
9043
|
+
|
|
9044
|
+
if not returned:
|
|
9045
|
+
refusal = _clip_colors.clip_color_refusal(color)
|
|
9046
|
+
return _err(
|
|
9047
|
+
f"SetClipColor refused '{color}' on this {kind}",
|
|
9048
|
+
code="CLIP_COLOR_REJECTED",
|
|
9049
|
+
category="invalid_input",
|
|
9050
|
+
reason=refusal["reason"],
|
|
9051
|
+
remediation=refusal["remediation"],
|
|
9052
|
+
state=refusal["state"],
|
|
9053
|
+
)
|
|
9054
|
+
if persisted is False:
|
|
9055
|
+
# The generator/title case. Reporting success here is the silent lie.
|
|
9056
|
+
return {
|
|
9057
|
+
"success": False,
|
|
9058
|
+
"color": color,
|
|
9059
|
+
"readback": readback,
|
|
9060
|
+
"warnings": [{
|
|
9061
|
+
"code": "CLIP_COLOR_NOT_PERSISTED",
|
|
9062
|
+
"message": (
|
|
9063
|
+
f"SetClipColor returned True on this {kind} but the colour did "
|
|
9064
|
+
f"not persist — GetClipColor reads {readback!r}. Measured on "
|
|
9065
|
+
"generator and title items, which take the call and drop it; "
|
|
9066
|
+
"media-backed items on the same timeline persist correctly."
|
|
9067
|
+
),
|
|
9068
|
+
"remediation": (
|
|
9069
|
+
"Colour a media-backed item, or mark the generator another way "
|
|
9070
|
+
"(a timeline marker at its start reads back reliably)."
|
|
9071
|
+
),
|
|
9072
|
+
}],
|
|
9073
|
+
}
|
|
9074
|
+
return {"success": True, "color": color, "readback": readback}
|
|
9075
|
+
|
|
9076
|
+
|
|
9009
9077
|
def _filter_to_keys(settings: Any, allowed) -> Tuple[Dict[str, Any], list]:
|
|
9010
9078
|
"""Whitelist a settings/options dict to the keys a Resolve API documents.
|
|
9011
9079
|
|
|
@@ -13486,8 +13554,18 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13486
13554
|
ignore_mcp_update() -> {success, version, update, decision}
|
|
13487
13555
|
snooze_mcp_update(hours?) -> {success, version, update, decision}
|
|
13488
13556
|
clear_mcp_update_preferences() -> {success, version, update, decision}
|
|
13489
|
-
api_truth(query?) -> {verified_on, count, facts}
|
|
13490
|
-
facts about quirky/unreliable Resolve API
|
|
13557
|
+
api_truth(query?, resolve_version?) -> {verified_on, count, facts, live_version?}
|
|
13558
|
+
— look up behaviorally-verified facts about quirky/unreliable Resolve API
|
|
13559
|
+
behavior (no connection needed; never connects). When a build is already
|
|
13560
|
+
connected or resolve_version is passed, each fact carries a
|
|
13561
|
+
version_context saying whether it was measured on an older, newer or
|
|
13562
|
+
identical build — the API changes per PATCH release, so a fact from
|
|
13563
|
+
another build is a prior, not a finding.
|
|
13564
|
+
check_version_support(symbol?, resolve_version?) -> {live_version, support|unavailable_on_this_build}
|
|
13565
|
+
— does THIS build have that method? Answers "unknown" unless a gate is
|
|
13566
|
+
recorded, because most of the API has never been version-bisected and a
|
|
13567
|
+
false "available" is how an agent insists a missing method exists.
|
|
13568
|
+
Omit symbol for every recorded gate this build does not clear.
|
|
13491
13569
|
verification_stats() -> {stats} — readback-verification tally
|
|
13492
13570
|
(verified/contradicted/unverified) since server start (no connection needed).
|
|
13493
13571
|
job_status(job_id) -> {id, label, status, result?, error?, started_at, ended_at}
|
|
@@ -13515,10 +13593,72 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13515
13593
|
"""
|
|
13516
13594
|
p = _params(params)
|
|
13517
13595
|
|
|
13518
|
-
# api_truth is a static knowledge lookup — no Resolve connection needed
|
|
13596
|
+
# api_truth is a static knowledge lookup — no Resolve connection needed, and
|
|
13597
|
+
# that property is worth keeping: it is the one call that still answers when
|
|
13598
|
+
# Resolve is down or unreachable. So the live build is used only if one is
|
|
13599
|
+
# ALREADY connected, or if the caller names it. Never connect for this.
|
|
13519
13600
|
if action == "api_truth":
|
|
13520
13601
|
facts = lookup_api_truth(p.get("query"))
|
|
13521
|
-
|
|
13602
|
+
live_version = p.get("resolve_version")
|
|
13603
|
+
if not live_version and resolve is not None:
|
|
13604
|
+
try:
|
|
13605
|
+
live_version = resolve.GetVersionString()
|
|
13606
|
+
except Exception:
|
|
13607
|
+
live_version = None
|
|
13608
|
+
result = {
|
|
13609
|
+
"verified_on": _API_TRUTH_VERIFIED_ON,
|
|
13610
|
+
"count": len(facts),
|
|
13611
|
+
"facts": _resolve_versions.annotate_facts(
|
|
13612
|
+
facts, live_version, ledger_verified_on=_API_TRUTH_VERIFIED_ON
|
|
13613
|
+
),
|
|
13614
|
+
}
|
|
13615
|
+
if live_version:
|
|
13616
|
+
result["live_version"] = str(live_version)
|
|
13617
|
+
unavailable = _resolve_versions.gates_unavailable_on(live_version)
|
|
13618
|
+
if unavailable:
|
|
13619
|
+
result["unavailable_on_this_build"] = unavailable
|
|
13620
|
+
else:
|
|
13621
|
+
result["version_context"] = (
|
|
13622
|
+
"No live build known, so facts are unannotated. The scripting API "
|
|
13623
|
+
"changes per PATCH release, so a fact measured on another build is a "
|
|
13624
|
+
"prior rather than a finding. Pass resolve_version, or call "
|
|
13625
|
+
"get_version first."
|
|
13626
|
+
)
|
|
13627
|
+
return result
|
|
13628
|
+
if action == "check_version_support":
|
|
13629
|
+
# "Does THIS build have it?" asked directly, instead of inferred from
|
|
13630
|
+
# prose. Answers `unknown` unless a gate is recorded — see
|
|
13631
|
+
# utils/resolve_versions.py for why that default is the load-bearing one.
|
|
13632
|
+
live_version = p.get("resolve_version")
|
|
13633
|
+
if not live_version and resolve is not None:
|
|
13634
|
+
try:
|
|
13635
|
+
live_version = resolve.GetVersionString()
|
|
13636
|
+
except Exception:
|
|
13637
|
+
live_version = None
|
|
13638
|
+
if not live_version:
|
|
13639
|
+
return _err(
|
|
13640
|
+
"No Resolve version available",
|
|
13641
|
+
code="VERSION_UNKNOWN",
|
|
13642
|
+
category="invalid_input",
|
|
13643
|
+
reason="Nothing is connected and no resolve_version was passed.",
|
|
13644
|
+
remediation="Call get_version first, or pass resolve_version='21.0.4.5'.",
|
|
13645
|
+
)
|
|
13646
|
+
symbol = p.get("symbol")
|
|
13647
|
+
if symbol:
|
|
13648
|
+
return {
|
|
13649
|
+
"live_version": str(live_version),
|
|
13650
|
+
"support": _resolve_versions.availability(symbol, live_version),
|
|
13651
|
+
}
|
|
13652
|
+
return {
|
|
13653
|
+
"live_version": str(live_version),
|
|
13654
|
+
"unavailable_on_this_build": _resolve_versions.gates_unavailable_on(live_version),
|
|
13655
|
+
"known_gates": len(_resolve_versions.VERSION_GATES),
|
|
13656
|
+
"note": (
|
|
13657
|
+
"Only surfaces with recorded evidence appear here. Most of the "
|
|
13658
|
+
"scripting API has never been version-bisected, so an absence from "
|
|
13659
|
+
"this list is not a promise that a method exists."
|
|
13660
|
+
),
|
|
13661
|
+
}
|
|
13522
13662
|
if action == "verification_stats":
|
|
13523
13663
|
# Process-level readback-verification tally — no connection needed.
|
|
13524
13664
|
stats = _verification_stats()
|
|
@@ -13676,7 +13816,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13676
13816
|
return missing
|
|
13677
13817
|
r.DisableBackgroundTasksForCurrentResolveSession()
|
|
13678
13818
|
return _ok()
|
|
13679
|
-
return _unknown(action, ["launch","runtime_mode","get_version","api_truth","verification_stats","job_status","list_jobs","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
|
|
13819
|
+
return _unknown(action, ["launch","runtime_mode","get_version","api_truth","check_version_support","verification_stats","job_status","list_jobs","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
|
|
13680
13820
|
|
|
13681
13821
|
|
|
13682
13822
|
# ─── V2 C4: Per-field corrections with provenance + changelog ────────────────
|
|
@@ -15967,6 +16107,9 @@ _RENDER_SETTING_KEYS = [
|
|
|
15967
16107
|
"ReplaceExistingFilesInPlace",
|
|
15968
16108
|
"ExportSubtitle",
|
|
15969
16109
|
"SubtitleFormat",
|
|
16110
|
+
"UseFullExtents",
|
|
16111
|
+
"AddFrameHandles",
|
|
16112
|
+
"DataBurnIn",
|
|
15970
16113
|
]
|
|
15971
16114
|
|
|
15972
16115
|
_RENDER_KERNEL_ACTIONS = [
|
|
@@ -16186,6 +16329,24 @@ def _render_preset_pin(proj, preset_name: str):
|
|
|
16186
16329
|
return {"preset": preset_name, "loaded": True}, None
|
|
16187
16330
|
|
|
16188
16331
|
|
|
16332
|
+
def _render_settings_warnings(settings: Dict[str, Any]):
|
|
16333
|
+
"""Inter-key combinations Resolve accepts and then silently ignores.
|
|
16334
|
+
|
|
16335
|
+
SetRenderSettings returns True for these, so nothing downstream would ever
|
|
16336
|
+
tell the caller the key did nothing — the same silent-no-op class as the
|
|
16337
|
+
unknown-key drop that `_filter_to_keys` covers on the set path.
|
|
16338
|
+
"""
|
|
16339
|
+
warnings = []
|
|
16340
|
+
if not isinstance(settings, dict):
|
|
16341
|
+
return warnings
|
|
16342
|
+
if settings.get("UseFullExtents") is True and isinstance(settings.get("AddFrameHandles"), int) and settings["AddFrameHandles"] > 0:
|
|
16343
|
+
warnings.append(
|
|
16344
|
+
"AddFrameHandles is ignored while UseFullExtents is true: Resolve renders the "
|
|
16345
|
+
"clip's full extents and the handle count silently does nothing. Drop one of the two."
|
|
16346
|
+
)
|
|
16347
|
+
return warnings
|
|
16348
|
+
|
|
16349
|
+
|
|
16189
16350
|
def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_target: bool = False):
|
|
16190
16351
|
if not isinstance(settings, dict) or not settings:
|
|
16191
16352
|
return None, _err("settings must be a non-empty object")
|
|
@@ -16201,15 +16362,20 @@ def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_
|
|
|
16201
16362
|
errors.append("TargetDir must be under the system temp directory for this safe operation")
|
|
16202
16363
|
elif require_temp_target:
|
|
16203
16364
|
errors.append("TargetDir is required for this safe operation")
|
|
16204
|
-
for key in ("FormatWidth", "FormatHeight", "MarkIn", "MarkOut", "AudioBitDepth", "AudioSampleRate"):
|
|
16365
|
+
for key in ("FormatWidth", "FormatHeight", "MarkIn", "MarkOut", "AudioBitDepth", "AudioSampleRate", "AddFrameHandles"):
|
|
16205
16366
|
if key in settings and not isinstance(settings[key], int):
|
|
16206
16367
|
errors.append(f"{key} must be an integer")
|
|
16207
|
-
for key in ("SelectAllFrames", "ExportVideo", "ExportAudio", "ExportAlpha", "MultiPassEncode", "NetworkOptimization", "ReplaceExistingFilesInPlace", "ExportSubtitle"):
|
|
16368
|
+
for key in ("SelectAllFrames", "ExportVideo", "ExportAudio", "ExportAlpha", "MultiPassEncode", "NetworkOptimization", "ReplaceExistingFilesInPlace", "ExportSubtitle", "UseFullExtents"):
|
|
16208
16369
|
if key in settings and not isinstance(settings[key], bool):
|
|
16209
16370
|
errors.append(f"{key} must be a boolean")
|
|
16371
|
+
if "AddFrameHandles" in settings and isinstance(settings["AddFrameHandles"], int) and settings["AddFrameHandles"] < 0:
|
|
16372
|
+
errors.append("AddFrameHandles must be >= 0")
|
|
16373
|
+
if "DataBurnIn" in settings and not isinstance(settings["DataBurnIn"], str):
|
|
16374
|
+
errors.append("DataBurnIn must be a string (a burn-in preset name, 'Same as project', or 'None')")
|
|
16210
16375
|
if "MarkIn" in settings and "MarkOut" in settings and settings["MarkOut"] < settings["MarkIn"]:
|
|
16211
16376
|
errors.append("MarkOut must be greater than or equal to MarkIn")
|
|
16212
|
-
result = {"valid": not errors, "unknown_keys": unknown, "errors": errors,
|
|
16377
|
+
result = {"valid": not errors, "unknown_keys": unknown, "errors": errors,
|
|
16378
|
+
"warnings": _render_settings_warnings(settings), "settings": dict(settings)}
|
|
16213
16379
|
return result, None
|
|
16214
16380
|
|
|
16215
16381
|
|
|
@@ -16660,7 +16826,12 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16660
16826
|
set_mode(mode) -> {success}
|
|
16661
16827
|
get_resolutions(format, codec) -> {resolutions}
|
|
16662
16828
|
get_settings() -> {settings} (alias for set_render_settings with get)
|
|
16663
|
-
set_settings(settings) -> {success}
|
|
16829
|
+
set_settings(settings) -> {success, ignored_settings?, warnings?}
|
|
16830
|
+
Resolve 21.0.4+ settings keys: UseFullExtents (bool),
|
|
16831
|
+
AddFrameHandles (int >= 0), DataBurnIn (burn-in preset name,
|
|
16832
|
+
"Same as project", or "None"). AddFrameHandles is ignored while
|
|
16833
|
+
UseFullExtents is true — the call still succeeds, so that pairing
|
|
16834
|
+
comes back in warnings rather than silently doing nothing.
|
|
16664
16835
|
list_presets() -> {presets}
|
|
16665
16836
|
load_preset(name) -> {success}
|
|
16666
16837
|
save_preset(name) -> {success}
|
|
@@ -16784,6 +16955,11 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16784
16955
|
result = {"success": bool(proj.SetRenderSettings(settings))}
|
|
16785
16956
|
if ignored_settings:
|
|
16786
16957
|
result["ignored_settings"] = ignored_settings
|
|
16958
|
+
# Accepted-then-ignored key combinations (21.0.4 AddFrameHandles under
|
|
16959
|
+
# UseFullExtents) read as a clean success without this.
|
|
16960
|
+
warnings = _render_settings_warnings(settings)
|
|
16961
|
+
if warnings:
|
|
16962
|
+
result["warnings"] = warnings
|
|
16787
16963
|
return result
|
|
16788
16964
|
elif action == "list_presets":
|
|
16789
16965
|
return {"presets": proj.GetRenderPresetList()}
|
|
@@ -17611,6 +17787,9 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17611
17787
|
get_mark_in_out(clip_id) -> {mark}
|
|
17612
17788
|
set_mark_in_out(clip_id, mark_in, mark_out, type?) -> {success}
|
|
17613
17789
|
clear_mark_in_out(clip_id, type?) -> {success}
|
|
17790
|
+
get_timeline(clip_id) -> {is_timeline, timeline: {name, unique_id?, start_frame?, end_frame?}}
|
|
17791
|
+
— Resolve 21.0.4+. Resolves a Media Pool timeline entry to its timeline
|
|
17792
|
+
object summary; is_timeline=false for ordinary clips.
|
|
17614
17793
|
open_in_viewer(clip_id, page?, mark_in_seconds?, mark_out_seconds?, clear_marks?) -> {success, clip_id, clip_name, folder_name, page, mark_set}
|
|
17615
17794
|
— Switches to Media page (default) and selects the clip in the bin.
|
|
17616
17795
|
Resolve auto-loads the selected clip into the source viewer on Media page.
|
|
@@ -17783,7 +17962,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17783
17962
|
elif action == "get_clip_color":
|
|
17784
17963
|
return {"color": clip.GetClipColor()}
|
|
17785
17964
|
elif action == "set_clip_color":
|
|
17786
|
-
return
|
|
17965
|
+
return _set_clip_color_checked(clip, p["color"], kind="media pool item")
|
|
17787
17966
|
elif action == "clear_clip_color":
|
|
17788
17967
|
return {"success": bool(clip.ClearClipColor())}
|
|
17789
17968
|
elif action == "link_proxy":
|
|
@@ -17939,7 +18118,28 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17939
18118
|
return {"success": bool(clip.SetMarkInOut(clean["mark_in"], clean["mark_out"], p.get("type", "all")))}
|
|
17940
18119
|
elif action == "clear_mark_in_out":
|
|
17941
18120
|
return {"success": bool(clip.ClearMarkInOut(p.get("type", "all")))}
|
|
17942
|
-
|
|
18121
|
+
elif action == "get_timeline":
|
|
18122
|
+
missing = _requires_method(clip, "GetTimeline", "21.0.4")
|
|
18123
|
+
if missing:
|
|
18124
|
+
return missing
|
|
18125
|
+
try:
|
|
18126
|
+
tl_obj = clip.GetTimeline()
|
|
18127
|
+
except Exception as exc:
|
|
18128
|
+
return _err(f"GetTimeline failed: {exc}")
|
|
18129
|
+
if not tl_obj:
|
|
18130
|
+
return {"is_timeline": False, "timeline": None,
|
|
18131
|
+
"note": "This media pool item is not a timeline entry."}
|
|
18132
|
+
summary = {}
|
|
18133
|
+
for getter, key in (("GetName", "name"), ("GetUniqueId", "unique_id"),
|
|
18134
|
+
("GetStartFrame", "start_frame"), ("GetEndFrame", "end_frame")):
|
|
18135
|
+
if _has_method(tl_obj, getter):
|
|
18136
|
+
try:
|
|
18137
|
+
summary[key] = getattr(tl_obj, getter)()
|
|
18138
|
+
except Exception:
|
|
18139
|
+
pass
|
|
18140
|
+
return {"is_timeline": True, "timeline": summary,
|
|
18141
|
+
"note": "Address this timeline by name via the timeline tool."}
|
|
18142
|
+
return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_transcription","extract_frames","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer","get_timeline"])
|
|
17943
18143
|
|
|
17944
18144
|
|
|
17945
18145
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -22251,7 +22451,7 @@ def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None)
|
|
|
22251
22451
|
elif action == "get_clip_color":
|
|
22252
22452
|
return {"color": item.GetClipColor()}
|
|
22253
22453
|
elif action == "set_clip_color":
|
|
22254
|
-
return
|
|
22454
|
+
return _set_clip_color_checked(item, p["color"], kind="timeline item")
|
|
22255
22455
|
elif action == "clear_clip_color":
|
|
22256
22456
|
return {"success": bool(item.ClearClipColor())}
|
|
22257
22457
|
return _unknown(action, ["add","get_all","get_by_custom_data","update_custom_data","get_custom_data","delete_by_color","delete_at_frame","delete_by_custom_data","add_flag","get_flags","clear_flags","get_clip_color","set_clip_color","clear_clip_color"])
|
package/src/utils/api_truth.py
CHANGED
|
@@ -863,7 +863,10 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
863
863
|
"mix CAN be applied wholesale via "
|
|
864
864
|
"Project.ApplyFairlightPresetToCurrentTimeline(name), with the "
|
|
865
865
|
"available names from Resolve.GetFairlightPresets() — so "
|
|
866
|
-
"'no Fairlight write path exists' would be too strong."
|
|
866
|
+
"'no Fairlight write path exists' would be too strong. Both "
|
|
867
|
+
"methods require Resolve 20.2.2+; on 19.1.3 they are absent "
|
|
868
|
+
"(confirmed live 2026-08-06), so on older builds the "
|
|
869
|
+
"per-parameter gap really is the whole story.",
|
|
867
870
|
"recommended": "To reapply a known mix, save it once as a Fairlight preset "
|
|
868
871
|
"in the UI and apply it per-timeline with "
|
|
869
872
|
"ApplyFairlightPresetToCurrentTimeline (exposed as "
|
|
@@ -1005,7 +1008,8 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1005
1008
|
"a normalised position vector (param 17). Exposed as "
|
|
1006
1009
|
"project_db list_subtitle_styles / set_subtitle_style "
|
|
1007
1010
|
"(font family/size/weight/italic + position). Confirmed "
|
|
1008
|
-
"live on
|
|
1011
|
+
"live on BOTH editions 2026-08-06 — Studio 19.1.3 and "
|
|
1012
|
+
"free 21.0.3, identical behaviour: Resolve opens a patched track "
|
|
1009
1013
|
"and re-serialises it back to its own zstd form with the "
|
|
1010
1014
|
"patched values intact, so it genuinely parses the write. "
|
|
1011
1015
|
"Caveats: whole-TRACK style not per-caption, project must "
|
|
@@ -1067,13 +1071,29 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1067
1071
|
"NOT refute the 21.0.0 record: if the bridge resolves any name "
|
|
1068
1072
|
"known to the RemoteObject method table rather than literally any "
|
|
1069
1073
|
"string, an invented name is correctly rejected on both builds and "
|
|
1070
|
-
"the probe never exercised the failing case.
|
|
1071
|
-
"
|
|
1072
|
-
"
|
|
1074
|
+
"the probe never exercised the failing case. "
|
|
1075
|
+
"PARTLY SETTLED 2026-08-06 on Studio 19.1.3.7, DIRECT connection "
|
|
1076
|
+
"(not the bridge): the five real borrowed names were re-run as the "
|
|
1077
|
+
"entry asked, across Resolve, ProjectManager, Project, MediaPool, "
|
|
1078
|
+
"Timeline, Folder and TimelineItem — 42 checks. Every one returned "
|
|
1079
|
+
"getattr-callable False and dir()-absent, agreeing in all 42 cases, "
|
|
1080
|
+
"exactly like the invented control. So NO fabrication on the direct "
|
|
1081
|
+
"path on this build, and the borrowed-name case the 21.0.2.4 probe "
|
|
1082
|
+
"missed is now covered there. What the same run DID establish is "
|
|
1083
|
+
"narrower and worse than expected: bare hasattr() returns True for "
|
|
1084
|
+
"EVERY name on every object — real, borrowed or invented — while "
|
|
1085
|
+
"getattr returns None. hasattr alone is therefore not a weak probe, "
|
|
1086
|
+
"it is a constant True and carries no information at all. "
|
|
1087
|
+
"STILL OPEN: the 21.0.0 record was against the BRIDGE, which this "
|
|
1088
|
+
"run did not exercise, so bridge-side fabrication remains untested "
|
|
1089
|
+
"and the recommendation below stands unchanged.",
|
|
1073
1090
|
"recommended": "Use dir(obj) membership for capability probes. It is correct "
|
|
1074
1091
|
"on every build measured, and it is the only form not affected "
|
|
1075
|
-
"by whichever way this resolves.
|
|
1076
|
-
"
|
|
1092
|
+
"by whichever way this resolves. NEVER use bare hasattr(): it "
|
|
1093
|
+
"is a constant True on Resolve objects and cannot distinguish "
|
|
1094
|
+
"anything. server._has_method uses callable(getattr(...)), "
|
|
1095
|
+
"which agreed with dir() in all 42 direct-path checks on "
|
|
1096
|
+
"19.1.3.7 but may still over-report on a bridge build where "
|
|
1077
1097
|
"fabrication is live — that is the case _requires_method gates "
|
|
1078
1098
|
"guard, so it matters most exactly where it is least tested. "
|
|
1079
1099
|
"Calling a fabricated method typically returns None/False with "
|
|
@@ -1421,6 +1441,65 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1421
1441
|
"issue": 59,
|
|
1422
1442
|
"mitigation": ["_render_format_id", "_render_codec_id"],
|
|
1423
1443
|
},
|
|
1444
|
+
{
|
|
1445
|
+
"symbol": "SetClipColor (undocumented value space; marker constants are a decoy)",
|
|
1446
|
+
"object": "TimelineItem / MediaPoolItem",
|
|
1447
|
+
"signature": "(colorName) -> bool",
|
|
1448
|
+
"reality": "SetClipColor accepts exactly 16 names — the Edit-page clip-colour "
|
|
1449
|
+
"palette — and refuses everything else with a bare False, no "
|
|
1450
|
+
"exception and no other signal. Enumerated live on Studio "
|
|
1451
|
+
"19.1.3.7 (2026-08-06) against both objects; the accepted set is "
|
|
1452
|
+
"IDENTICAL on TimelineItem and MediaPoolItem: Orange, Apricot, "
|
|
1453
|
+
"Yellow, Lime, Olive, Green, Teal, Navy, Blue, Purple, Violet, "
|
|
1454
|
+
"Pink, Tan, Beige, Brown, Chocolate. The empty string is refused "
|
|
1455
|
+
"too. The trap is that the scripting reference documents "
|
|
1456
|
+
"colorName as a bare string with no enumerated values, while "
|
|
1457
|
+
"exporting the MARKER palette as constants (resolve.MARKER_ROSE, "
|
|
1458
|
+
"resolve.MARKER_FUCHSIA, ...) — so the only colour vocabulary "
|
|
1459
|
+
"reachable from the API surface is the wrong one. Cyan, Red, "
|
|
1460
|
+
"Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa and Cream "
|
|
1461
|
+
"are all marker-only and all refused. Five names overlap both "
|
|
1462
|
+
"palettes (Blue, Green, Yellow, Pink, Purple), which is why the "
|
|
1463
|
+
"decoy survives: reasoning from the marker constants scores 5 of "
|
|
1464
|
+
"16 and looks like the right vocabulary with a few gaps. The "
|
|
1465
|
+
"reporter's field experience sharpens this — the trap is not that "
|
|
1466
|
+
"a decoy vocabulary exists, it is that the decoy HALF-WORKS. "
|
|
1467
|
+
"Intermittent successes read as an unreliable API rather than as "
|
|
1468
|
+
"a wrong vocabulary, so the wrong conclusion is the natural one; "
|
|
1469
|
+
"a clean 0-for-8 would have exposed the mechanism immediately.",
|
|
1470
|
+
"recommended": "Pass only the 16 clip-colour names; on False, treat it as an "
|
|
1471
|
+
"invalid name rather than an item/lock/page problem. The set "
|
|
1472
|
+
"is pinned in utils/clip_colors.py and named in the refusal "
|
|
1473
|
+
"remediation, but deliberately NOT enforced — it was measured "
|
|
1474
|
+
"on one build and a later Resolve could extend it.",
|
|
1475
|
+
"tags": ["timeline-item", "media-pool", "silent-failure", "undocumented-enum"],
|
|
1476
|
+
"submit": "missing",
|
|
1477
|
+
"issue": 124,
|
|
1478
|
+
"mitigation": ["_set_clip_color_checked", "ti_set_clip_color"],
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
"symbol": "TimelineItem.SetClipColor on generator/title items",
|
|
1482
|
+
"object": "TimelineItem",
|
|
1483
|
+
"signature": "(colorName) -> bool",
|
|
1484
|
+
"reality": "On a generator or title item SetClipColor returns True with a "
|
|
1485
|
+
"VALID colour name and the colour does not persist — GetClipColor "
|
|
1486
|
+
"still reads '' immediately afterwards, and ClearClipColor "
|
|
1487
|
+
"changes nothing because there is nothing to clear. Measured on "
|
|
1488
|
+
"Studio 19.1.3.7 (2026-08-06) on a Solid Color generator. A "
|
|
1489
|
+
"media-backed item on the SAME timeline in the same session "
|
|
1490
|
+
"persists correctly (SetClipColor('Teal') -> True, GetClipColor "
|
|
1491
|
+
"-> 'Teal'), so the bool is honest for some items and a lie for "
|
|
1492
|
+
"others, with nothing in the return value to tell them apart. A "
|
|
1493
|
+
"timeline item's colour is also independent of its backing "
|
|
1494
|
+
"MediaPoolItem's: colouring the item leaves the pool clip at ''.",
|
|
1495
|
+
"recommended": "Never trust the bool alone — read GetClipColor back and "
|
|
1496
|
+
"compare. To mark a generator or title, use a timeline marker "
|
|
1497
|
+
"at its start instead; markers read back reliably.",
|
|
1498
|
+
"tags": ["timeline-item", "silent-failure", "generator", "readback-lies"],
|
|
1499
|
+
"submit": "bug",
|
|
1500
|
+
"issue": 124,
|
|
1501
|
+
"mitigation": ["_set_clip_color_checked", "ti_set_clip_color"],
|
|
1502
|
+
},
|
|
1424
1503
|
{
|
|
1425
1504
|
"symbol": "Project.SetRenderSettings (inherits the loaded preset)",
|
|
1426
1505
|
"object": "Project",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""The value space of SetClipColor, and an honest refusal when a name misses.
|
|
2
|
+
|
|
3
|
+
`SetClipColor(colorName) --> Bool` is documented with `colorName` as a bare
|
|
4
|
+
string and no enumerated values anywhere in the scripting reference. The same
|
|
5
|
+
reference *does* export a discoverable colour vocabulary — the marker constants
|
|
6
|
+
(`resolve.MARKER_ROSE`, `resolve.MARKER_FUCHSIA`, …) — so the only colour names
|
|
7
|
+
reachable from the API surface come from the marker palette, and those are
|
|
8
|
+
largely the ones `SetClipColor` refuses. Issue #124 named this a decoy
|
|
9
|
+
vocabulary, which is exactly right.
|
|
10
|
+
|
|
11
|
+
Enumerated live on **DaVinci Resolve Studio 19.1.3.7 (macOS, 2026-08-06)**
|
|
12
|
+
against both objects that expose the method. Both accept the same 16 names —
|
|
13
|
+
the Edit-page clip-colour palette — and refuse everything else with a bare
|
|
14
|
+
`False`, including every marker-only name and the empty string.
|
|
15
|
+
|
|
16
|
+
The five names that overlap the two palettes (Blue, Green, Yellow, Pink,
|
|
17
|
+
Purple) are what makes this trap so durable: an agent reasoning from the marker
|
|
18
|
+
constants gets five hits out of sixteen and concludes the vocabulary is right.
|
|
19
|
+
|
|
20
|
+
This set is deliberately NOT enforced. It was measured on one build, and a
|
|
21
|
+
future Resolve could extend it; hard-rejecting an unlisted name would turn a
|
|
22
|
+
working call into a failure on a build we have not seen. Callers pass the name
|
|
23
|
+
through, and when Resolve refuses, the remediation names the measured-valid set.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
VERIFIED_ON = "DaVinci Resolve Studio 19.1.3.7 (macOS, 2026-08-06)"
|
|
27
|
+
|
|
28
|
+
# Accepted by BOTH TimelineItem.SetClipColor and MediaPoolItem.SetClipColor.
|
|
29
|
+
CLIP_COLORS = (
|
|
30
|
+
"Orange", "Apricot", "Yellow", "Lime", "Olive", "Green", "Teal", "Navy",
|
|
31
|
+
"Blue", "Purple", "Violet", "Pink", "Tan", "Beige", "Brown", "Chocolate",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Measured refused. Kept explicit because these are the names an agent actually
|
|
35
|
+
# reaches for: every one is either a marker constant or a common colour word.
|
|
36
|
+
REFUSED_NAMES = (
|
|
37
|
+
"Cyan", "Red", "Fuchsia", "Rose", "Lavender", "Sky", "Mint", "Lemon",
|
|
38
|
+
"Sand", "Cocoa", "Cream", "Apple", "Magenta", "White", "Black", "Gray",
|
|
39
|
+
"Grey", "",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# The marker-palette names that are NOT clip colours — the decoy set proper.
|
|
43
|
+
MARKER_ONLY_NAMES = (
|
|
44
|
+
"Cyan", "Red", "Fuchsia", "Rose", "Lavender", "Sky", "Mint", "Lemon",
|
|
45
|
+
"Sand", "Cocoa", "Cream",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_known_clip_color(color) -> bool:
|
|
50
|
+
"""True if `color` is in the measured-valid set. Case-sensitive, as Resolve is."""
|
|
51
|
+
return isinstance(color, str) and color in CLIP_COLORS
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def clip_color_refusal(color) -> dict:
|
|
55
|
+
"""Error payload for a SetClipColor that came back False.
|
|
56
|
+
|
|
57
|
+
Separates 'you used a marker name' from 'Resolve refused for another
|
|
58
|
+
reason', because the caller cannot tell them apart from the bare bool.
|
|
59
|
+
"""
|
|
60
|
+
marker_decoy = isinstance(color, str) and color in MARKER_ONLY_NAMES
|
|
61
|
+
if marker_decoy:
|
|
62
|
+
reason = (
|
|
63
|
+
f"'{color}' is a MARKER colour, not a clip colour. The scripting "
|
|
64
|
+
"reference enumerates the marker constants and nothing else, so it "
|
|
65
|
+
"is the vocabulary an agent finds first — and SetClipColor refuses "
|
|
66
|
+
"most of it with a bare False."
|
|
67
|
+
)
|
|
68
|
+
elif is_known_clip_color(color):
|
|
69
|
+
reason = (
|
|
70
|
+
f"'{color}' is a valid clip colour on {VERIFIED_ON}, so this refusal "
|
|
71
|
+
"is about the item or the session, not the name. Note that "
|
|
72
|
+
"SetClipColor returns True WITHOUT persisting on generator and title "
|
|
73
|
+
"items — check GetClipColor rather than the bool."
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
reason = (
|
|
77
|
+
f"'{color}' is not in the clip-colour palette. SetClipColor refuses "
|
|
78
|
+
"undocumented names with a bare False and no other signal."
|
|
79
|
+
)
|
|
80
|
+
return {
|
|
81
|
+
"reason": reason,
|
|
82
|
+
"remediation": (
|
|
83
|
+
"Use one of the 16 Edit-page clip colours: "
|
|
84
|
+
+ ", ".join(CLIP_COLORS)
|
|
85
|
+
+ f". Measured on {VERIFIED_ON}; the set is not enforced here because "
|
|
86
|
+
"a later build could extend it."
|
|
87
|
+
),
|
|
88
|
+
"state": {
|
|
89
|
+
"requested_color": color,
|
|
90
|
+
"valid_colors": list(CLIP_COLORS),
|
|
91
|
+
"is_marker_only_name": marker_decoy,
|
|
92
|
+
"verified_on": VERIFIED_ON,
|
|
93
|
+
},
|
|
94
|
+
}
|
package/src/utils/edit_engine.py
CHANGED
|
@@ -1742,6 +1742,12 @@ def plan_transcript_tighten(
|
|
|
1742
1742
|
plan.get("keep_ranges") or [],
|
|
1743
1743
|
source_duration_seconds=float(duration) if isinstance(duration, (int, float)) and duration else None,
|
|
1744
1744
|
)
|
|
1745
|
+
# The class this planner is structurally blind to. Its false-start heuristic
|
|
1746
|
+
# only sees restarts the transcript actually reports; an immediate re-read is
|
|
1747
|
+
# emitted ONCE with the pause and second take absorbed into one word, so the
|
|
1748
|
+
# plan cannot propose removing what it was never told happened (issue #125).
|
|
1749
|
+
from src.utils import swallowed_retakes as _swallowed
|
|
1750
|
+
plan["possible_swallowed_retakes"] = _swallowed.swallowed_retake_report(words)
|
|
1745
1751
|
return plan
|
|
1746
1752
|
|
|
1747
1753
|
|
|
@@ -55,8 +55,10 @@ logger = logging.getLogger("resolve-mcp.bridge-client")
|
|
|
55
55
|
DEFAULT_CONFIG_PATH = Path.home() / ".config/davinci-resolve-mcp/bridge.json"
|
|
56
56
|
#: Env override, so a caller can point at a non-default bridge without editing code.
|
|
57
57
|
ENV_CONFIG_PATH = "DAVINCI_RESOLVE_BRIDGE_CONFIG"
|
|
58
|
-
#:
|
|
59
|
-
#:
|
|
58
|
+
#: Forces the bridge: it becomes the only transport tried, so its faults surface
|
|
59
|
+
#: directly. Absent does NOT mean "never try the bridge" — `connect_resolve` falls
|
|
60
|
+
#: back to it when a direct transport yields nothing, which is what lets the free
|
|
61
|
+
#: edition work with no configuration beyond starting the in-Resolve script.
|
|
60
62
|
ENV_ENABLE = "DAVINCI_RESOLVE_BRIDGE"
|
|
61
63
|
|
|
62
64
|
#: Operations this client cannot work without. The in-Resolve script is a *copy*
|
|
@@ -390,7 +392,8 @@ def connect(*, timeout: float = DEFAULT_TIMEOUT_SECONDS, require_enabled: bool =
|
|
|
390
392
|
"""
|
|
391
393
|
if require_enabled and not bridge_enabled():
|
|
392
394
|
raise BridgeUnavailable(
|
|
393
|
-
f"
|
|
395
|
+
f"This caller required an explicitly-enabled bridge: set {ENV_ENABLE}=1, "
|
|
396
|
+
"or call connect(require_enabled=False) to use the bridge as a fallback."
|
|
394
397
|
)
|
|
395
398
|
path = config_path()
|
|
396
399
|
try:
|