davinci-resolve-mcp 2.67.1 → 2.68.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 +147 -0
- package/README.md +27 -2
- package/docs/SKILL.md +20 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/bridge_differential.py +431 -0
- package/scripts/install_resolve_bridge.py +260 -0
- package/scripts/resolve_bridge_launcher.py +202 -0
- package/scripts/resolve_bridge_probe.py +162 -0
- package/scripts/resolve_capability_probe.py +270 -0
- package/src/granular/common.py +6 -1
- package/src/server.py +378 -41
- package/src/utils/bridge_differential.py +375 -0
- package/src/utils/captions.py +323 -0
- package/src/utils/colorimetry.py +196 -0
- package/src/utils/delivery_targets.py +225 -0
- package/src/utils/edit_engine.py +181 -7
- package/src/utils/image_qc.py +499 -0
- package/src/utils/page_lock.py +5 -1
- package/src/utils/resolve_bridge.py +557 -0
- package/src/utils/resolve_bridge_client.py +423 -0
- package/src/utils/resolve_bridge_ops.py +714 -0
- package/src/utils/resolve_connection.py +29 -1
- package/src/utils/silence_ripple.py +256 -8
- package/src/utils/strata_analyzers.py +7 -0
- package/src/utils/strata_faces.py +6 -0
- package/src/utils/transcript_edit.py +372 -0
package/src/server.py
CHANGED
|
@@ -11,11 +11,13 @@ 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.68.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
18
18
|
import sys
|
|
19
|
+
import functools
|
|
20
|
+
import inspect
|
|
19
21
|
import json
|
|
20
22
|
import logging
|
|
21
23
|
import math
|
|
@@ -122,6 +124,7 @@ from src.utils.fusion_group_settings import (
|
|
|
122
124
|
from src.utils import analysis_runs as _analysis_runs
|
|
123
125
|
from src.utils import brain_edits as _brain_edits
|
|
124
126
|
from src.utils import edit_engine as _edit_engine_mod
|
|
127
|
+
from src.utils import transcript_edit as _transcript_edit_defaults
|
|
125
128
|
from src.utils import media_pool_changes as _media_pool_changes
|
|
126
129
|
from src.utils import timeline_versioning as _timeline_versioning
|
|
127
130
|
from src.utils import project_spec as _project_spec
|
|
@@ -803,11 +806,32 @@ def _is_resolve_handle_live(candidate) -> bool:
|
|
|
803
806
|
return False
|
|
804
807
|
|
|
805
808
|
|
|
809
|
+
def _bridge_requested() -> bool:
|
|
810
|
+
"""Has the operator asked for the in-app bridge?
|
|
811
|
+
|
|
812
|
+
Read at call time rather than at import, so a server started before the
|
|
813
|
+
variable was set still honours it.
|
|
814
|
+
"""
|
|
815
|
+
try:
|
|
816
|
+
from src.utils import resolve_bridge_client
|
|
817
|
+
|
|
818
|
+
return resolve_bridge_client.bridge_enabled()
|
|
819
|
+
except Exception: # pragma: no cover - the client is optional
|
|
820
|
+
return False
|
|
821
|
+
|
|
822
|
+
|
|
806
823
|
def _try_connect():
|
|
807
824
|
"""Attempt to connect to Resolve once. Returns resolve object or None."""
|
|
808
825
|
global resolve
|
|
809
826
|
with _resolve_lock:
|
|
810
|
-
|
|
827
|
+
# `dvr_script` is Blackmagic's module, and it ships with the *installer*,
|
|
828
|
+
# not the App Store build — a free-edition-only machine has no
|
|
829
|
+
# Developer/Scripting/Modules tree at all, so the import fails. Bailing
|
|
830
|
+
# here made the bridge unreachable on precisely the configuration it
|
|
831
|
+
# exists for: `connect_resolve` accepts None in bridge mode, and this
|
|
832
|
+
# returned before ever calling it. Verified by blocking the import with a
|
|
833
|
+
# healthy bridge listening — get_resolve() answered None.
|
|
834
|
+
if dvr_script is None and not _bridge_requested():
|
|
811
835
|
return None
|
|
812
836
|
try:
|
|
813
837
|
candidate = connect_resolve(dvr_script)
|
|
@@ -822,13 +846,34 @@ def _try_connect():
|
|
|
822
846
|
resolve = None
|
|
823
847
|
return None
|
|
824
848
|
|
|
849
|
+
#: macOS puts the two editions in different places, and only the first was ever
|
|
850
|
+
#: checked — so an auto-launch on a machine with both would start Studio even
|
|
851
|
+
#: when the free edition was the one in use. Ordered installer-first to keep the
|
|
852
|
+
#: previous behaviour where Studio is present.
|
|
853
|
+
_MACOS_RESOLVE_APPS = (
|
|
854
|
+
"/Applications/DaVinci Resolve/DaVinci Resolve.app", # installer / Studio
|
|
855
|
+
"/Applications/DaVinci Resolve.app", # App Store, free
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
825
859
|
def _launch_resolve():
|
|
826
860
|
"""Launch DaVinci Resolve and wait for it to become available."""
|
|
861
|
+
if _bridge_requested():
|
|
862
|
+
# Launching cannot produce a bridge. The listener only exists once
|
|
863
|
+
# someone runs Workspace > Scripts > resolve_bridge inside an already
|
|
864
|
+
# running Resolve, so starting the application here would open a window
|
|
865
|
+
# nobody asked for and still fail to connect.
|
|
866
|
+
logger.error(
|
|
867
|
+
"The in-app bridge is enabled but not answering. Start it from "
|
|
868
|
+
"Workspace > Scripts > resolve_bridge inside the running Resolve; "
|
|
869
|
+
"launching the application cannot start it."
|
|
870
|
+
)
|
|
871
|
+
return False
|
|
827
872
|
sys_name = platform.system().lower()
|
|
828
873
|
if sys_name == "darwin":
|
|
829
|
-
app_path =
|
|
830
|
-
if
|
|
831
|
-
logger.error(
|
|
874
|
+
app_path = next((p for p in _MACOS_RESOLVE_APPS if os.path.exists(p)), None)
|
|
875
|
+
if app_path is None:
|
|
876
|
+
logger.error("DaVinci Resolve not found in %s", list(_MACOS_RESOLVE_APPS))
|
|
832
877
|
return False
|
|
833
878
|
subprocess.Popen(["open", app_path], stdin=subprocess.DEVNULL)
|
|
834
879
|
elif sys_name == "windows":
|
|
@@ -1182,6 +1227,120 @@ _CATEGORY_RETRYABLE_DEFAULT: Dict[str, bool] = {
|
|
|
1182
1227
|
_RETRYABLE_UNSET = object()
|
|
1183
1228
|
|
|
1184
1229
|
|
|
1230
|
+
class _MissingParam(KeyError):
|
|
1231
|
+
"""A tool action subscripted a parameter the caller did not supply.
|
|
1232
|
+
|
|
1233
|
+
Subclasses `KeyError` so any existing `except KeyError` in the codebase keeps
|
|
1234
|
+
behaving as it did; what it adds is the ability to tell a *missing argument*
|
|
1235
|
+
apart from a KeyError on some other dict — a Resolve settings map, a parsed
|
|
1236
|
+
JSON body — which would otherwise be reported to the caller as "you forgot an
|
|
1237
|
+
argument" when the real fault is ours.
|
|
1238
|
+
"""
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
class _Params(dict):
|
|
1242
|
+
"""The params dict, wired so a missing key is a diagnosable refusal.
|
|
1243
|
+
|
|
1244
|
+
Actions have historically reached into `p["track_type"]` directly. When the
|
|
1245
|
+
key is absent that raises a bare `KeyError`, which escapes the action, escapes
|
|
1246
|
+
the tool, and reaches the client as
|
|
1247
|
+
`ToolError: Error executing tool timeline: 'track_type'` — no code, no
|
|
1248
|
+
category, no remediation, and nothing an agent can route on. Measured across
|
|
1249
|
+
the whole surface: **99 of 512 declared actions** failed that way.
|
|
1250
|
+
|
|
1251
|
+
Two ways to fix that were available. Rewriting all 99 sites to use
|
|
1252
|
+
`contracts.validate` gives the best individual errors but is 99 chances to get
|
|
1253
|
+
a spec wrong, and leaves the 100th site to be written next month. Catching
|
|
1254
|
+
`KeyError` at the tool boundary closes the class in one place but cannot tell
|
|
1255
|
+
which dict raised, so an unrelated internal KeyError would be mislabelled as
|
|
1256
|
+
the caller's mistake.
|
|
1257
|
+
|
|
1258
|
+
This is the third option: keep the boundary catch, but make the *parameters*
|
|
1259
|
+
raise something only they can raise. False positives become impossible by
|
|
1260
|
+
construction.
|
|
1261
|
+
|
|
1262
|
+
`contracts.validate` remains the better tool wherever a type, a range or an
|
|
1263
|
+
enum also needs checking, and the actions that already use it keep doing so.
|
|
1264
|
+
This is deliberately **not** a migration of the 99 — rewriting them would add
|
|
1265
|
+
99 opportunities to write a spec wrong without improving the defect being
|
|
1266
|
+
fixed here, and it would still leave the next new action unguarded. What is
|
|
1267
|
+
lost by not migrating is bounded: a *wrong* value still reaches Resolve, which
|
|
1268
|
+
answers None or False, so the result is a truthful failure rather than a
|
|
1269
|
+
misleading success.
|
|
1270
|
+
"""
|
|
1271
|
+
|
|
1272
|
+
def __missing__(self, key):
|
|
1273
|
+
raise _MissingParam(key)
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _params(params: Optional[Dict[str, Any]]) -> "_Params":
|
|
1277
|
+
"""Every tool body starts here, so every action gets the diagnosable dict.
|
|
1278
|
+
|
|
1279
|
+
Note `_Params(params or {})` rather than a truthiness dance: an empty
|
|
1280
|
+
`_Params` is falsy like any empty dict, so `p = params or {}` would have
|
|
1281
|
+
quietly handed back a *plain* dict in exactly the no-arguments case this
|
|
1282
|
+
exists to serve.
|
|
1283
|
+
"""
|
|
1284
|
+
return _Params(params or {})
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def _missing_param_error(exc: _MissingParam, action: str) -> Dict[str, Any]:
|
|
1288
|
+
"""Turn a missing parameter into the envelope the rest of the surface uses."""
|
|
1289
|
+
name = exc.args[0] if exc.args else "parameter"
|
|
1290
|
+
slug = re.sub(r"[^A-Z0-9]+", "_", str(name).upper()).strip("_") or "PARAMETER"
|
|
1291
|
+
return _err(
|
|
1292
|
+
f"'{name}' is required",
|
|
1293
|
+
code=f"MISSING_{slug}",
|
|
1294
|
+
category="invalid_input",
|
|
1295
|
+
remediation=f"pass {name} in params, e.g. params={{\"{name}\": ...}}",
|
|
1296
|
+
state={"action": action, "missing": str(name)},
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
def _guarded_action_name(args, kwargs) -> str:
|
|
1301
|
+
"""The `action` this call was for, wherever it was passed."""
|
|
1302
|
+
if "action" in kwargs:
|
|
1303
|
+
return str(kwargs["action"])
|
|
1304
|
+
return str(args[0]) if args else "?"
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _guard_missing_params(fn):
|
|
1308
|
+
"""Tool decorator: report a missing parameter instead of leaking a KeyError.
|
|
1309
|
+
|
|
1310
|
+
Sits *under* `@mcp.tool()` so FastMCP registers the guarded callable.
|
|
1311
|
+
`functools.wraps` keeps the name, docstring and signature FastMCP builds the
|
|
1312
|
+
tool schema from.
|
|
1313
|
+
|
|
1314
|
+
Two things this has to preserve, both learned by breaking them:
|
|
1315
|
+
|
|
1316
|
+
- **Async tools must stay coroutine functions.** `media_analysis` is `async`,
|
|
1317
|
+
and a synchronous wrapper around it returned an un-awaited coroutine that
|
|
1318
|
+
FastMCP could not use — the tool stopped answering entirely, taking its 71
|
|
1319
|
+
actions with it. `inspect.iscoroutinefunction` decides which wrapper to
|
|
1320
|
+
build, and a test now pins that async tools are still async afterwards.
|
|
1321
|
+
- **The signature is not (action, params).** `media_analysis` also takes
|
|
1322
|
+
`ctx: Optional[Context]`, so the wrapper forwards `*args, **kwargs` rather
|
|
1323
|
+
than naming parameters it does not know about.
|
|
1324
|
+
"""
|
|
1325
|
+
if inspect.iscoroutinefunction(fn):
|
|
1326
|
+
@functools.wraps(fn)
|
|
1327
|
+
async def wrapper(*args, **kwargs):
|
|
1328
|
+
try:
|
|
1329
|
+
return await fn(*args, **kwargs)
|
|
1330
|
+
except _MissingParam as exc:
|
|
1331
|
+
return _missing_param_error(exc, _guarded_action_name(args, kwargs))
|
|
1332
|
+
else:
|
|
1333
|
+
@functools.wraps(fn)
|
|
1334
|
+
def wrapper(*args, **kwargs):
|
|
1335
|
+
try:
|
|
1336
|
+
return fn(*args, **kwargs)
|
|
1337
|
+
except _MissingParam as exc:
|
|
1338
|
+
return _missing_param_error(exc, _guarded_action_name(args, kwargs))
|
|
1339
|
+
|
|
1340
|
+
wrapper.__wrapped_by_missing_param_guard__ = True
|
|
1341
|
+
return wrapper
|
|
1342
|
+
|
|
1343
|
+
|
|
1185
1344
|
def _err(message, *, code=None, category=None, retryable=_RETRYABLE_UNSET,
|
|
1186
1345
|
remediation=None, reason=None, state=None):
|
|
1187
1346
|
"""Return a structured error envelope.
|
|
@@ -12644,6 +12803,7 @@ def _setup_clear_defaults(keys: Any, dry_run: bool) -> Dict[str, Any]:
|
|
|
12644
12803
|
|
|
12645
12804
|
|
|
12646
12805
|
@mcp.tool()
|
|
12806
|
+
@_guard_missing_params
|
|
12647
12807
|
def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
12648
12808
|
"""Configure MCP conversational defaults and setup preferences.
|
|
12649
12809
|
|
|
@@ -12657,7 +12817,7 @@ def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
12657
12817
|
media_analysis.*: analysis, metadata, marker, reporting, and workflow defaults
|
|
12658
12818
|
updates.*: MCP update policy, interval, and snooze defaults
|
|
12659
12819
|
"""
|
|
12660
|
-
p = params
|
|
12820
|
+
p = _params(params)
|
|
12661
12821
|
if action in {"schema", "capabilities", "options"}:
|
|
12662
12822
|
return {
|
|
12663
12823
|
"actions": ["schema", "get_defaults", "set_defaults", "clear_defaults"],
|
|
@@ -12797,6 +12957,7 @@ def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
12797
12957
|
|
|
12798
12958
|
|
|
12799
12959
|
@mcp.tool()
|
|
12960
|
+
@_guard_missing_params
|
|
12800
12961
|
def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
12801
12962
|
"""App-level DaVinci Resolve operations.
|
|
12802
12963
|
|
|
@@ -12839,7 +13000,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
12839
13000
|
restore_state(state_token) -> {success, restored: {...}}
|
|
12840
13001
|
— Returns Resolve to a previously-saved state.
|
|
12841
13002
|
"""
|
|
12842
|
-
p = params
|
|
13003
|
+
p = _params(params)
|
|
12843
13004
|
|
|
12844
13005
|
# api_truth is a static knowledge lookup — no Resolve connection needed.
|
|
12845
13006
|
if action == "api_truth":
|
|
@@ -13816,6 +13977,7 @@ def _close_control_panel() -> Dict[str, Any]:
|
|
|
13816
13977
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
13817
13978
|
|
|
13818
13979
|
@mcp.tool()
|
|
13980
|
+
@_guard_missing_params
|
|
13819
13981
|
def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
13820
13982
|
"""Manage DaVinci Resolve UI layout presets.
|
|
13821
13983
|
|
|
@@ -13827,7 +13989,7 @@ def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
|
|
|
13827
13989
|
import_preset(path, name?) -> {success}
|
|
13828
13990
|
delete(name) -> {success}
|
|
13829
13991
|
"""
|
|
13830
|
-
p = params
|
|
13992
|
+
p = _params(params)
|
|
13831
13993
|
r = get_resolve()
|
|
13832
13994
|
if r is None:
|
|
13833
13995
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
@@ -13862,6 +14024,7 @@ def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
|
|
|
13862
14024
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
13863
14025
|
|
|
13864
14026
|
@mcp.tool()
|
|
14027
|
+
@_guard_missing_params
|
|
13865
14028
|
def render_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
13866
14029
|
"""Import/export render and burn-in presets.
|
|
13867
14030
|
|
|
@@ -13871,7 +14034,7 @@ def render_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
|
|
|
13871
14034
|
import_burnin(path) -> {success}
|
|
13872
14035
|
export_burnin(name, path) -> {success}
|
|
13873
14036
|
"""
|
|
13874
|
-
p = params
|
|
14037
|
+
p = _params(params)
|
|
13875
14038
|
r = get_resolve()
|
|
13876
14039
|
if r is None:
|
|
13877
14040
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
@@ -14772,6 +14935,7 @@ def _project_lint_live(r, pm) -> Dict[str, Any]:
|
|
|
14772
14935
|
|
|
14773
14936
|
|
|
14774
14937
|
@mcp.tool()
|
|
14938
|
+
@_guard_missing_params
|
|
14775
14939
|
def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
14776
14940
|
"""Manage DaVinci Resolve projects.
|
|
14777
14941
|
|
|
@@ -14813,7 +14977,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
14813
14977
|
settings are applied in dependency order; markers only added if absent. Hooks run only
|
|
14814
14978
|
when run_hooks=true (executes shell from the spec — opt-in).
|
|
14815
14979
|
"""
|
|
14816
|
-
p = params
|
|
14980
|
+
p = _params(params)
|
|
14817
14981
|
r = get_resolve()
|
|
14818
14982
|
if r is None:
|
|
14819
14983
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
@@ -14920,6 +15084,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
14920
15084
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
14921
15085
|
|
|
14922
15086
|
@mcp.tool()
|
|
15087
|
+
@_guard_missing_params
|
|
14923
15088
|
def project_manager_folders(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
14924
15089
|
"""Navigate and manage project folders in the Project Manager.
|
|
14925
15090
|
|
|
@@ -14932,7 +15097,7 @@ def project_manager_folders(action: str, params: Optional[Dict[str, Any]] = None
|
|
|
14932
15097
|
goto_root() -> {success}
|
|
14933
15098
|
goto_parent() -> {success}
|
|
14934
15099
|
"""
|
|
14935
|
-
p = params
|
|
15100
|
+
p = _params(params)
|
|
14936
15101
|
r = get_resolve()
|
|
14937
15102
|
if r is None:
|
|
14938
15103
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
@@ -14964,6 +15129,7 @@ def project_manager_folders(action: str, params: Optional[Dict[str, Any]] = None
|
|
|
14964
15129
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
14965
15130
|
|
|
14966
15131
|
@mcp.tool()
|
|
15132
|
+
@_guard_missing_params
|
|
14967
15133
|
def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
14968
15134
|
"""Cloud project operations (requires DaVinci Resolve cloud infrastructure).
|
|
14969
15135
|
|
|
@@ -14973,7 +15139,7 @@ def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None)
|
|
|
14973
15139
|
import_project(path, settings) -> {success}
|
|
14974
15140
|
restore(folder_path, settings) -> {success}
|
|
14975
15141
|
"""
|
|
14976
|
-
p = params
|
|
15142
|
+
p = _params(params)
|
|
14977
15143
|
r = get_resolve()
|
|
14978
15144
|
if r is None:
|
|
14979
15145
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
@@ -15002,6 +15168,7 @@ def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None)
|
|
|
15002
15168
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
15003
15169
|
|
|
15004
15170
|
@mcp.tool()
|
|
15171
|
+
@_guard_missing_params
|
|
15005
15172
|
def project_manager_database(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
15006
15173
|
"""Manage DaVinci Resolve project databases.
|
|
15007
15174
|
|
|
@@ -15010,7 +15177,7 @@ def project_manager_database(action: str, params: Optional[Dict[str, Any]] = Non
|
|
|
15010
15177
|
list() -> {databases}
|
|
15011
15178
|
set_current(db_info) -> {success} — db_info: {DbType, DbName}
|
|
15012
15179
|
"""
|
|
15013
|
-
p = params
|
|
15180
|
+
p = _params(params)
|
|
15014
15181
|
r = get_resolve()
|
|
15015
15182
|
if r is None:
|
|
15016
15183
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
@@ -15033,6 +15200,7 @@ def project_manager_database(action: str, params: Optional[Dict[str, Any]] = Non
|
|
|
15033
15200
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
15034
15201
|
|
|
15035
15202
|
@mcp.tool()
|
|
15203
|
+
@_guard_missing_params
|
|
15036
15204
|
def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
15037
15205
|
"""Project metadata, settings, and color groups.
|
|
15038
15206
|
|
|
@@ -15057,7 +15225,7 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
|
|
|
15057
15225
|
apply_fairlight_preset(preset_name) -> {success}
|
|
15058
15226
|
generate_speech(speech_generation_settings, timecode?) -> {success, new, new_id} — Resolve 21+, AI Speech Generator; creates new audio media (confirm-gated)
|
|
15059
15227
|
"""
|
|
15060
|
-
p = params
|
|
15228
|
+
p = _params(params)
|
|
15061
15229
|
_, proj, err = _check()
|
|
15062
15230
|
if err:
|
|
15063
15231
|
return err
|
|
@@ -15240,6 +15408,7 @@ _RENDER_KERNEL_ACTIONS = [
|
|
|
15240
15408
|
"export_render_boundary_report",
|
|
15241
15409
|
"list_delivery_targets",
|
|
15242
15410
|
"resolve_delivery_target",
|
|
15411
|
+
"list_loudness_standards",
|
|
15243
15412
|
"prepare_delivery_job",
|
|
15244
15413
|
]
|
|
15245
15414
|
|
|
@@ -15618,6 +15787,9 @@ def _resolve_delivery_target_live(proj, p: Dict[str, Any]):
|
|
|
15618
15787
|
|
|
15619
15788
|
fps = _delivery_timeline_fps(proj)
|
|
15620
15789
|
qc_spec = _delivery_targets.to_qc_spec(target, timeline_fps=fps)
|
|
15790
|
+
# Loudness is a separate projection feeding advanced `loudness_qc`, not a
|
|
15791
|
+
# deliverable_qc field — the mix sets programme loudness, not the render.
|
|
15792
|
+
loudness = _delivery_targets.to_loudness_target(target)
|
|
15621
15793
|
return (
|
|
15622
15794
|
{
|
|
15623
15795
|
"target": target.id,
|
|
@@ -15633,12 +15805,40 @@ def _resolve_delivery_target_live(proj, p: Dict[str, Any]):
|
|
|
15633
15805
|
if qc_spec
|
|
15634
15806
|
else "This target renders an image sequence; deliverable_qc probes a single file, so it has no QC spec."
|
|
15635
15807
|
),
|
|
15808
|
+
"loudness_target": loudness,
|
|
15809
|
+
"loudness_note": (
|
|
15810
|
+
None
|
|
15811
|
+
if loudness
|
|
15812
|
+
else "This target pins no programme loudness. Name one with "
|
|
15813
|
+
"overrides={'loudness_standard': ...}; see render(action='list_loudness_standards')."
|
|
15814
|
+
),
|
|
15636
15815
|
"notes": list(target.notes),
|
|
15637
15816
|
},
|
|
15638
15817
|
None,
|
|
15639
15818
|
)
|
|
15640
15819
|
|
|
15641
15820
|
|
|
15821
|
+
def _list_loudness_standards(proj, p: Dict[str, Any]):
|
|
15822
|
+
"""Named programme-loudness contracts, so an agent cites one rather than inventing numbers."""
|
|
15823
|
+
return (
|
|
15824
|
+
{
|
|
15825
|
+
"standards": _delivery_targets.list_loudness_standards(),
|
|
15826
|
+
"lra_advisory_lu": _delivery_targets.LRA_ADVISORY_LU,
|
|
15827
|
+
"usage": (
|
|
15828
|
+
"Attach with render(action='resolve_delivery_target', params={'target': ..., "
|
|
15829
|
+
"'overrides': {'loudness_standard': '<id>'}}), then hand the returned "
|
|
15830
|
+
"loudness_target.target to the advanced server's loudness_qc."
|
|
15831
|
+
),
|
|
15832
|
+
"note": (
|
|
15833
|
+
"Dialogue-gated standards emit no gradeable integrated value: loudness_qc "
|
|
15834
|
+
"measures full-program loudness, so that figure travels in meta for a "
|
|
15835
|
+
"dialogue-gated meter and only true peak is asserted."
|
|
15836
|
+
),
|
|
15837
|
+
},
|
|
15838
|
+
None,
|
|
15839
|
+
)
|
|
15840
|
+
|
|
15841
|
+
|
|
15642
15842
|
def _list_delivery_targets(proj, p: Dict[str, Any]):
|
|
15643
15843
|
tier = p.get("tier")
|
|
15644
15844
|
if tier is not None and tier not in _delivery_targets.TIERS:
|
|
@@ -15778,6 +15978,7 @@ def _export_render_boundary_report(proj, p: Dict[str, Any]):
|
|
|
15778
15978
|
|
|
15779
15979
|
|
|
15780
15980
|
@mcp.tool()
|
|
15981
|
+
@_guard_missing_params
|
|
15781
15982
|
def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
15782
15983
|
"""Render pipeline: jobs, presets, formats, codecs, and rendering.
|
|
15783
15984
|
|
|
@@ -15826,7 +16027,7 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
15826
16027
|
resolved against the live matrix, so an intent unavailable on this
|
|
15827
16028
|
machine/license fails with the actual available lists.
|
|
15828
16029
|
"""
|
|
15829
|
-
p = params
|
|
16030
|
+
p = _params(params)
|
|
15830
16031
|
_, proj, err = _check()
|
|
15831
16032
|
if err:
|
|
15832
16033
|
return err
|
|
@@ -15864,6 +16065,9 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
15864
16065
|
elif action == "resolve_delivery_target":
|
|
15865
16066
|
_resolved, _target_err = _resolve_delivery_target_live(proj, p)
|
|
15866
16067
|
return _target_err if _target_err else _ok(**_resolved)
|
|
16068
|
+
elif action == "list_loudness_standards":
|
|
16069
|
+
_standards, _standards_err = _list_loudness_standards(proj, p)
|
|
16070
|
+
return _standards_err if _standards_err else _ok(**_standards)
|
|
15867
16071
|
elif action == "prepare_delivery_job":
|
|
15868
16072
|
return _prepare_delivery_job(proj, p)
|
|
15869
16073
|
elif action == "set_format_and_codec":
|
|
@@ -15966,6 +16170,7 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
15966
16170
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
15967
16171
|
|
|
15968
16172
|
@mcp.tool()
|
|
16173
|
+
@_guard_missing_params
|
|
15969
16174
|
def media_storage(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
15970
16175
|
"""Browse storage volumes and import media into the Media Pool.
|
|
15971
16176
|
|
|
@@ -15982,7 +16187,7 @@ def media_storage(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
15982
16187
|
add_clip_mattes(clip_id, paths, stereo_eye?) -> {success}
|
|
15983
16188
|
add_timeline_mattes(paths) -> {items}
|
|
15984
16189
|
"""
|
|
15985
|
-
p = params
|
|
16190
|
+
p = _params(params)
|
|
15986
16191
|
r = get_resolve()
|
|
15987
16192
|
if r is None:
|
|
15988
16193
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
@@ -16033,6 +16238,7 @@ def media_storage(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
16033
16238
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
16034
16239
|
|
|
16035
16240
|
@mcp.tool()
|
|
16241
|
+
@_guard_missing_params
|
|
16036
16242
|
@_destructive_op("media_pool")
|
|
16037
16243
|
def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
16038
16244
|
"""Manage the Media Pool: folders, clips, timelines, import/export.
|
|
@@ -16120,7 +16326,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
16120
16326
|
copy_clip_annotations(source_clip_id, target_clip_ids, include_markers?, include_flags?, include_clip_color?, dry_run?) -> {success, results}
|
|
16121
16327
|
media_pool_boundary_report(depth?, clip_ids?, selected?) -> {capabilities, media_pool, items?}
|
|
16122
16328
|
"""
|
|
16123
|
-
p = params
|
|
16329
|
+
p = _params(params)
|
|
16124
16330
|
_, proj, mp, err = _get_mp()
|
|
16125
16331
|
if err:
|
|
16126
16332
|
return err
|
|
@@ -16497,6 +16703,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
16497
16703
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
16498
16704
|
|
|
16499
16705
|
@mcp.tool()
|
|
16706
|
+
@_guard_missing_params
|
|
16500
16707
|
def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
16501
16708
|
"""Operations on Media Pool folders.
|
|
16502
16709
|
|
|
@@ -16515,7 +16722,7 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16515
16722
|
analyze_for_slate(path?, marker_color?) -> {success} — Resolve 21+, AI Slate ID Extra
|
|
16516
16723
|
remove_motion_blur(path?, deblur_option?) -> {success, created} — Resolve 21+; renders NEW media (confirm-gated)
|
|
16517
16724
|
"""
|
|
16518
|
-
p = params
|
|
16725
|
+
p = _params(params)
|
|
16519
16726
|
_, _, mp, err = _get_mp()
|
|
16520
16727
|
if err:
|
|
16521
16728
|
return err
|
|
@@ -16638,6 +16845,7 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
16638
16845
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
16639
16846
|
|
|
16640
16847
|
@mcp.tool()
|
|
16848
|
+
@_guard_missing_params
|
|
16641
16849
|
def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
16642
16850
|
"""Operations on a media pool clip. Identify clip by clip_id.
|
|
16643
16851
|
|
|
@@ -16689,7 +16897,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
16689
16897
|
verified empirically on Resolve Studio 20.3.2.9. If BMD changes the
|
|
16690
16898
|
behavior the clip will still be selected — editor double-clicks to load.
|
|
16691
16899
|
"""
|
|
16692
|
-
p = params
|
|
16900
|
+
p = _params(params)
|
|
16693
16901
|
_, _, mp, err = _get_mp()
|
|
16694
16902
|
if err:
|
|
16695
16903
|
return err
|
|
@@ -17002,6 +17210,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
17002
17210
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
17003
17211
|
|
|
17004
17212
|
@mcp.tool()
|
|
17213
|
+
@_guard_missing_params
|
|
17005
17214
|
def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
17006
17215
|
"""Markers and flags on media pool clips. Identify clip by clip_id.
|
|
17007
17216
|
|
|
@@ -17022,7 +17231,7 @@ def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None
|
|
|
17022
17231
|
monitor_growing_file(clip_id) -> {success}
|
|
17023
17232
|
replace_clip_preserve_sub_clip(clip_id, path) -> {success}
|
|
17024
17233
|
"""
|
|
17025
|
-
p = params
|
|
17234
|
+
p = _params(params)
|
|
17026
17235
|
_, _, mp, err = _get_mp()
|
|
17027
17236
|
if err:
|
|
17028
17237
|
return err
|
|
@@ -17134,6 +17343,7 @@ def _opt_number(value: Any) -> Optional[float]:
|
|
|
17134
17343
|
|
|
17135
17344
|
|
|
17136
17345
|
@mcp.tool()
|
|
17346
|
+
@_guard_missing_params
|
|
17137
17347
|
async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, ctx: Optional[Context] = None) -> Dict[str, Any]:
|
|
17138
17348
|
"""Project-scoped media analysis and guarded metadata publishing.
|
|
17139
17349
|
|
|
@@ -17230,7 +17440,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
17230
17440
|
vision-dependent metadata to Resolve. Works with any MCP client whose chat
|
|
17231
17441
|
model is vision-capable; no sampling/createMessage support required.
|
|
17232
17442
|
"""
|
|
17233
|
-
p = _media_analysis_apply_setup_defaults(action, dict(params or {}))
|
|
17443
|
+
p = _params(_media_analysis_apply_setup_defaults(action, dict(params or {})))
|
|
17234
17444
|
|
|
17235
17445
|
# First-run frame-sampling prompt: if the user has never chosen a sampling
|
|
17236
17446
|
# mode (and didn't pass one this call), ask before spending any vision
|
|
@@ -17288,6 +17498,38 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
17288
17498
|
|
|
17289
17499
|
if action == "capabilities":
|
|
17290
17500
|
return _media_analysis_capabilities_for_request(ctx)
|
|
17501
|
+
if action == "assess_grade":
|
|
17502
|
+
# Numeric grade-damage QC. Distinct from advanced `verify_grade`, which
|
|
17503
|
+
# asks whether Resolve applied what was asked; this asks whether the
|
|
17504
|
+
# resulting image is damaged. Free and deterministic — the vision block
|
|
17505
|
+
# in the result says whether a paid second opinion is warranted.
|
|
17506
|
+
from src.utils import image_qc as _image_qc_mod
|
|
17507
|
+
|
|
17508
|
+
try:
|
|
17509
|
+
return _ok(**_image_qc_mod.assess_grade(
|
|
17510
|
+
str(p.get("source_path") or p.get("sourcePath") or ""),
|
|
17511
|
+
time_seconds=float(p.get("time_seconds", p.get("timeSeconds", 0.0)) or 0.0),
|
|
17512
|
+
graded_path=(p.get("graded_path") or p.get("gradedPath")) or None,
|
|
17513
|
+
lut_path=(p.get("lut_path") or p.get("lutPath")) or None,
|
|
17514
|
+
working_space=str(p.get("working_space") or p.get("workingSpace") or "rec709"),
|
|
17515
|
+
cost_tier=str(p.get("cost_tier") or p.get("costTier") or _image_qc_mod.DEFAULT_COST_TIER),
|
|
17516
|
+
))
|
|
17517
|
+
except _image_qc_mod.ImageQcError as exc:
|
|
17518
|
+
return _err(
|
|
17519
|
+
str(exc),
|
|
17520
|
+
code="IMAGE_QC_REFUSED",
|
|
17521
|
+
category="invalid_input",
|
|
17522
|
+
remediation=(
|
|
17523
|
+
"Supply exactly one of graded_path or lut_path, and declare a display-referred "
|
|
17524
|
+
"working_space. Log/scene-referred frames must be converted first — these "
|
|
17525
|
+
"metrics are undefined on them and will not be guessed at."
|
|
17526
|
+
),
|
|
17527
|
+
)
|
|
17528
|
+
if action == "image_qc_capabilities":
|
|
17529
|
+
from src.utils import image_qc as _image_qc_mod
|
|
17530
|
+
|
|
17531
|
+
return _ok(**_image_qc_mod.capabilities(), cost_tiers=list(_image_qc_mod.COST_TIERS),
|
|
17532
|
+
default_cost_tier=_image_qc_mod.DEFAULT_COST_TIER)
|
|
17291
17533
|
if action == "recheck_capabilities":
|
|
17292
17534
|
# Re-runs detection (shutil.which / importlib.util.find_spec) so a tool
|
|
17293
17535
|
# an agent just installed flips from missing → available without the
|
|
@@ -18197,6 +18439,8 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
18197
18439
|
return _unknown(action, [
|
|
18198
18440
|
"capabilities",
|
|
18199
18441
|
"recheck_capabilities",
|
|
18442
|
+
"assess_grade",
|
|
18443
|
+
"image_qc_capabilities",
|
|
18200
18444
|
"install_guidance",
|
|
18201
18445
|
"resolve_output_root",
|
|
18202
18446
|
"plan",
|
|
@@ -18272,6 +18516,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
18272
18516
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
18273
18517
|
|
|
18274
18518
|
@mcp.tool()
|
|
18519
|
+
@_guard_missing_params
|
|
18275
18520
|
def timeline_versioning(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
18276
18521
|
"""Timeline version-on-mutate, archive, rollback, and brain-edit history (C6).
|
|
18277
18522
|
|
|
@@ -18319,7 +18564,7 @@ def timeline_versioning(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
18319
18564
|
if ctx is None:
|
|
18320
18565
|
return _err("No current project / can't resolve project root")
|
|
18321
18566
|
resolve_h, project_h, project_root, project_name = ctx
|
|
18322
|
-
p = params
|
|
18567
|
+
p = _params(params)
|
|
18323
18568
|
|
|
18324
18569
|
if action == "begin_run":
|
|
18325
18570
|
return _analysis_runs.begin_run(
|
|
@@ -18644,6 +18889,7 @@ def _edit_engine_linked_audio_tracks(
|
|
|
18644
18889
|
|
|
18645
18890
|
|
|
18646
18891
|
@mcp.tool()
|
|
18892
|
+
@_guard_missing_params
|
|
18647
18893
|
@_destructive_op("edit_engine")
|
|
18648
18894
|
def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
18649
18895
|
"""Evidence-driven edit loops (Phase E): selects assembly, tighten, swap.
|
|
@@ -18685,7 +18931,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
18685
18931
|
timeline (version-archived first).
|
|
18686
18932
|
- list_plans(limit?) / get_plan(plan_id)
|
|
18687
18933
|
"""
|
|
18688
|
-
p = params
|
|
18934
|
+
p = _params(params)
|
|
18689
18935
|
|
|
18690
18936
|
def _project_context(*, need_resolve: bool) -> Tuple[Optional[Any], Optional[Any], Optional[str], Optional[Dict[str, Any]]]:
|
|
18691
18937
|
# An explicit analysis_root always wins — the evidence DB the caller
|
|
@@ -18752,6 +18998,71 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
18752
18998
|
include_audio=str(p.get("include_audio", p.get("includeAudio", True))).strip().lower() not in {"false", "0", "no", "none", "off"},
|
|
18753
18999
|
)
|
|
18754
19000
|
|
|
19001
|
+
if action == "generate_captions":
|
|
19002
|
+
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19003
|
+
if err:
|
|
19004
|
+
return err
|
|
19005
|
+
from src.utils import captions as _captions_mod
|
|
19006
|
+
from src.utils import strata as _strata_mod
|
|
19007
|
+
|
|
19008
|
+
conn, clip, clip_err = _strata_mod.resolve_clip(
|
|
19009
|
+
project_root, p.get("clip_ref") or p.get("clipRef") or p.get("clip_id"), require_media=False
|
|
19010
|
+
)
|
|
19011
|
+
if clip_err:
|
|
19012
|
+
return clip_err
|
|
19013
|
+
_words = _strata_mod.read_words(conn, clip["clip_uuid"])
|
|
19014
|
+
_opts = {}
|
|
19015
|
+
for _key, _param in (
|
|
19016
|
+
("max_chars_per_line", "maxCharsPerLine"), ("max_lines", "maxLines"),
|
|
19017
|
+
("max_block_seconds", "maxBlockSeconds"), ("min_block_seconds", "minBlockSeconds"),
|
|
19018
|
+
("min_gap_seconds", "minGapSeconds"), ("pause_break_seconds", "pauseBreakSeconds"),
|
|
19019
|
+
):
|
|
19020
|
+
_value = p.get(_key, p.get(_param))
|
|
19021
|
+
if _value is not None:
|
|
19022
|
+
_opts[_key] = int(_value) if _key in {"max_chars_per_line", "max_lines"} else float(_value)
|
|
19023
|
+
try:
|
|
19024
|
+
_result = _captions_mod.generate(
|
|
19025
|
+
_words,
|
|
19026
|
+
fmt=str(p.get("format") or p.get("fmt") or "srt").lower(),
|
|
19027
|
+
with_chapters=str(p.get("with_chapters", p.get("withChapters", False))).strip().lower() in {"true", "1", "yes", "on"},
|
|
19028
|
+
**_opts,
|
|
19029
|
+
)
|
|
19030
|
+
except _captions_mod.CaptionError as exc:
|
|
19031
|
+
return _err(str(exc), code="CAPTION_PARAMS_INVALID", category="invalid_input")
|
|
19032
|
+
_result["clip"] = {"clip_uuid": clip["clip_uuid"], "clip_name": clip.get("clip_name")}
|
|
19033
|
+
return _result
|
|
19034
|
+
|
|
19035
|
+
if action == "plan_transcript_tighten":
|
|
19036
|
+
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19037
|
+
if err:
|
|
19038
|
+
return err
|
|
19039
|
+
return _edit_engine_mod.plan_transcript_tighten(
|
|
19040
|
+
project_root,
|
|
19041
|
+
clip_ref=p.get("clip_ref") or p.get("clipRef") or p.get("clip_id"),
|
|
19042
|
+
remove_fillers=str(p.get("remove_fillers", p.get("removeFillers", True))).strip().lower() not in {"false", "0", "no", "off"},
|
|
19043
|
+
remove_false_starts=str(p.get("remove_false_starts", p.get("removeFalseStarts", True))).strip().lower() not in {"false", "0", "no", "off"},
|
|
19044
|
+
collapse_pauses=str(p.get("collapse_pauses", p.get("collapsePauses", True))).strip().lower() not in {"false", "0", "no", "off"},
|
|
19045
|
+
max_pause=float(p.get("max_pause", p.get("maxPause", _transcript_edit_defaults.DEFAULT_MAX_PAUSE_S))),
|
|
19046
|
+
handle=float(p.get("handle", _transcript_edit_defaults.DEFAULT_HANDLE_S)),
|
|
19047
|
+
min_cut=float(p.get("min_cut", p.get("minCut", _transcript_edit_defaults.DEFAULT_MIN_CUT_S))),
|
|
19048
|
+
)
|
|
19049
|
+
|
|
19050
|
+
if action == "search_spoken_content":
|
|
19051
|
+
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19052
|
+
if err:
|
|
19053
|
+
return err
|
|
19054
|
+
query = p.get("query") or p.get("q")
|
|
19055
|
+
if not isinstance(query, str) or not query.strip():
|
|
19056
|
+
return _err("query is required", code="MISSING_QUERY", category="invalid_input")
|
|
19057
|
+
return _edit_engine_mod.search_spoken_content(
|
|
19058
|
+
project_root,
|
|
19059
|
+
query=query,
|
|
19060
|
+
mode=str(p.get("mode") or "phrase"),
|
|
19061
|
+
context_seconds=float(p.get("context_seconds", p.get("contextSeconds", 1.5))),
|
|
19062
|
+
handle_seconds=float(p.get("handle_seconds", p.get("handleSeconds", 0.5))),
|
|
19063
|
+
max_hits=int(p.get("max_hits", p.get("maxHits", 200))),
|
|
19064
|
+
)
|
|
19065
|
+
|
|
18755
19066
|
if action == "plan_silence_ripple":
|
|
18756
19067
|
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
18757
19068
|
if err:
|
|
@@ -18765,7 +19076,14 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
18765
19076
|
items=items,
|
|
18766
19077
|
timeline_name=tl.GetName(),
|
|
18767
19078
|
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
18768
|
-
|
|
19079
|
+
# Omitted => None => the gate is calibrated per item from that
|
|
19080
|
+
# slice's own dynamics. Passing the old fixed default here would
|
|
19081
|
+
# make auto-calibration unreachable through the MCP surface.
|
|
19082
|
+
threshold_db=(
|
|
19083
|
+
float(_th)
|
|
19084
|
+
if (_th := (p.get("threshold_db") if p.get("threshold_db") is not None else p.get("thresholdDb"))) is not None
|
|
19085
|
+
else None
|
|
19086
|
+
),
|
|
18769
19087
|
min_strip_frames=float(p.get("min_strip_frames") if p.get("min_strip_frames") is not None else p.get("minStripFrames") if p.get("minStripFrames") is not None else _edit_engine_mod.DEFAULT_SILENCE_MIN_STRIP_FRAMES),
|
|
18770
19088
|
pre_head_frames=float(p.get("pre_head_frames") if p.get("pre_head_frames") is not None else p.get("preHeadFrames") if p.get("preHeadFrames") is not None else _edit_engine_mod.DEFAULT_SILENCE_PRE_HEAD_FRAMES),
|
|
18771
19089
|
post_tail_frames=float(p.get("post_tail_frames") if p.get("post_tail_frames") is not None else p.get("postTailFrames") if p.get("postTailFrames") is not None else _edit_engine_mod.DEFAULT_SILENCE_POST_TAIL_FRAMES),
|
|
@@ -19344,6 +19662,9 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
19344
19662
|
"plan_tighten",
|
|
19345
19663
|
"execute_tighten",
|
|
19346
19664
|
"plan_silence_ripple",
|
|
19665
|
+
"plan_transcript_tighten",
|
|
19666
|
+
"generate_captions",
|
|
19667
|
+
"search_spoken_content",
|
|
19347
19668
|
"execute_silence_ripple",
|
|
19348
19669
|
"plan_swap",
|
|
19349
19670
|
"execute_swap",
|
|
@@ -19375,6 +19696,7 @@ _TIMELINE_ACTIONS = [
|
|
|
19375
19696
|
|
|
19376
19697
|
|
|
19377
19698
|
@mcp.tool()
|
|
19699
|
+
@_guard_missing_params
|
|
19378
19700
|
@_destructive_op("timeline")
|
|
19379
19701
|
def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
19380
19702
|
"""Timeline operations: tracks, clips, import/export, generators, titles.
|
|
@@ -19560,7 +19882,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
19560
19882
|
For long-form per-action guidance and a worked example, call:
|
|
19561
19883
|
timeline(action="action_help", params={"name": "<action>"})
|
|
19562
19884
|
"""
|
|
19563
|
-
p = params
|
|
19885
|
+
p = _params(params)
|
|
19564
19886
|
# action_help is pull-on-demand metadata; no Resolve connection needed.
|
|
19565
19887
|
if action == "action_help":
|
|
19566
19888
|
return _action_help("timeline", p)
|
|
@@ -19961,7 +20283,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
19961
20283
|
result["ignored_state_keys"] = ignored_state
|
|
19962
20284
|
return result
|
|
19963
20285
|
elif action == "extract_source_frame_ranges":
|
|
19964
|
-
p = params
|
|
20286
|
+
p = _params(params)
|
|
19965
20287
|
handles = int(p.get("handles", 24))
|
|
19966
20288
|
gap_max = int(p.get("gap_max", 30))
|
|
19967
20289
|
audio_ext = tuple(
|
|
@@ -20148,6 +20470,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
20148
20470
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
20149
20471
|
|
|
20150
20472
|
@mcp.tool()
|
|
20473
|
+
@_guard_missing_params
|
|
20151
20474
|
@_destructive_op("timeline_markers")
|
|
20152
20475
|
def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
20153
20476
|
"""Markers and playhead operations on the current timeline.
|
|
@@ -20183,7 +20506,7 @@ def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> An
|
|
|
20183
20506
|
export_review_report(scope?, include_capabilities?) -> {title, annotations, capabilities?}
|
|
20184
20507
|
annotation_boundary_report(scope?) -> {capabilities, annotations}
|
|
20185
20508
|
"""
|
|
20186
|
-
p = params
|
|
20509
|
+
p = _params(params)
|
|
20187
20510
|
_, tl, err = _get_tl()
|
|
20188
20511
|
if err:
|
|
20189
20512
|
return err
|
|
@@ -20268,6 +20591,7 @@ def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> An
|
|
|
20268
20591
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
20269
20592
|
|
|
20270
20593
|
@mcp.tool()
|
|
20594
|
+
@_guard_missing_params
|
|
20271
20595
|
@_destructive_op("timeline_ai")
|
|
20272
20596
|
def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
20273
20597
|
"""AI and analysis operations on the current timeline.
|
|
@@ -20281,7 +20605,7 @@ def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
20281
20605
|
grab_still() -> {success}
|
|
20282
20606
|
grab_all_stills(source?) -> {count}
|
|
20283
20607
|
"""
|
|
20284
|
-
p = params
|
|
20608
|
+
p = _params(params)
|
|
20285
20609
|
_, tl, err = _get_tl()
|
|
20286
20610
|
if err:
|
|
20287
20611
|
return err
|
|
@@ -20322,6 +20646,7 @@ def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
20322
20646
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
20323
20647
|
|
|
20324
20648
|
@mcp.tool()
|
|
20649
|
+
@_guard_missing_params
|
|
20325
20650
|
@_destructive_op("timeline_item")
|
|
20326
20651
|
def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
20327
20652
|
"""Properties, transforms, speed, keyframes, and metadata for a timeline item.
|
|
@@ -20373,7 +20698,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
20373
20698
|
|
|
20374
20699
|
Default: track_type="video", track_index=1, item_index=0
|
|
20375
20700
|
"""
|
|
20376
|
-
p = params
|
|
20701
|
+
p = _params(params)
|
|
20377
20702
|
tl, item, err = _get_item(p)
|
|
20378
20703
|
if err:
|
|
20379
20704
|
return err
|
|
@@ -20561,6 +20886,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
20561
20886
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
20562
20887
|
|
|
20563
20888
|
@mcp.tool()
|
|
20889
|
+
@_guard_missing_params
|
|
20564
20890
|
@_destructive_op("timeline_item_markers")
|
|
20565
20891
|
def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
20566
20892
|
"""Markers, flags, and clip color on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
|
|
@@ -20583,7 +20909,7 @@ def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None)
|
|
|
20583
20909
|
|
|
20584
20910
|
Default: track_type="video", track_index=1, item_index=0
|
|
20585
20911
|
"""
|
|
20586
|
-
p = params
|
|
20912
|
+
p = _params(params)
|
|
20587
20913
|
_, item, err = _get_item(p)
|
|
20588
20914
|
if err:
|
|
20589
20915
|
return err
|
|
@@ -20638,6 +20964,7 @@ def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None)
|
|
|
20638
20964
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
20639
20965
|
|
|
20640
20966
|
@mcp.tool()
|
|
20967
|
+
@_guard_missing_params
|
|
20641
20968
|
@_destructive_op("timeline_item_fusion")
|
|
20642
20969
|
def timeline_item_fusion(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
20643
20970
|
"""Fusion composition operations on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
|
|
@@ -20658,7 +20985,7 @@ def timeline_item_fusion(action: str, params: Optional[Dict[str, Any]] = None) -
|
|
|
20658
20985
|
|
|
20659
20986
|
Default: track_type="video", track_index=1, item_index=0
|
|
20660
20987
|
"""
|
|
20661
|
-
p = params
|
|
20988
|
+
p = _params(params)
|
|
20662
20989
|
_, item, err = _get_item(p)
|
|
20663
20990
|
if err:
|
|
20664
20991
|
return err
|
|
@@ -21939,6 +22266,7 @@ def _grade_evidence_base(proj, item, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
21939
22266
|
}
|
|
21940
22267
|
|
|
21941
22268
|
@mcp.tool()
|
|
22269
|
+
@_guard_missing_params
|
|
21942
22270
|
@_destructive_op("timeline_item_color")
|
|
21943
22271
|
def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
21944
22272
|
"""Color grading, versions, LUTs, cache, and AI tools on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
|
|
@@ -22029,7 +22357,7 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
22029
22357
|
For long-form per-action guidance and a worked example, call:
|
|
22030
22358
|
timeline_item_color(action="action_help", params={"name": "<action>"})
|
|
22031
22359
|
"""
|
|
22032
|
-
p = params
|
|
22360
|
+
p = _params(params)
|
|
22033
22361
|
if action == "action_help":
|
|
22034
22362
|
return _action_help("timeline_item_color", p)
|
|
22035
22363
|
_, item, err = _get_item(p)
|
|
@@ -22144,6 +22472,7 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
22144
22472
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22145
22473
|
|
|
22146
22474
|
@mcp.tool()
|
|
22475
|
+
@_guard_missing_params
|
|
22147
22476
|
@_destructive_op("timeline_item_takes")
|
|
22148
22477
|
def timeline_item_takes(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
22149
22478
|
"""Take management on timeline items. Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
|
|
@@ -22159,7 +22488,7 @@ def timeline_item_takes(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
22159
22488
|
|
|
22160
22489
|
Default: track_type="video", track_index=1, item_index=0
|
|
22161
22490
|
"""
|
|
22162
|
-
p = params
|
|
22491
|
+
p = _params(params)
|
|
22163
22492
|
_, item, err = _get_item(p)
|
|
22164
22493
|
if err:
|
|
22165
22494
|
return err
|
|
@@ -22192,6 +22521,7 @@ def timeline_item_takes(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
22192
22521
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22193
22522
|
|
|
22194
22523
|
@mcp.tool()
|
|
22524
|
+
@_guard_missing_params
|
|
22195
22525
|
def gallery(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
22196
22526
|
"""Gallery album management.
|
|
22197
22527
|
|
|
@@ -22207,7 +22537,7 @@ def gallery(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, A
|
|
|
22207
22537
|
|
|
22208
22538
|
album_index is 0-based into the still albums list.
|
|
22209
22539
|
"""
|
|
22210
|
-
p = params
|
|
22540
|
+
p = _params(params)
|
|
22211
22541
|
_, proj, err = _check()
|
|
22212
22542
|
if err:
|
|
22213
22543
|
return err
|
|
@@ -22257,6 +22587,7 @@ def gallery(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, A
|
|
|
22257
22587
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22258
22588
|
|
|
22259
22589
|
@mcp.tool()
|
|
22590
|
+
@_guard_missing_params
|
|
22260
22591
|
def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
22261
22592
|
"""Manage stills in gallery albums (best results on Color page).
|
|
22262
22593
|
|
|
@@ -22277,7 +22608,7 @@ def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
|
|
|
22277
22608
|
File data is inlined in the response (DRX as text, images as base64).
|
|
22278
22609
|
cleanup (default true) deletes exported files from disk after inlining.
|
|
22279
22610
|
"""
|
|
22280
|
-
p = params
|
|
22611
|
+
p = _params(params)
|
|
22281
22612
|
_, proj, err = _check()
|
|
22282
22613
|
if err:
|
|
22283
22614
|
return err
|
|
@@ -22410,6 +22741,7 @@ def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
|
|
|
22410
22741
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22411
22742
|
|
|
22412
22743
|
@mcp.tool()
|
|
22744
|
+
@_guard_missing_params
|
|
22413
22745
|
@_destructive_op("graph")
|
|
22414
22746
|
def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
22415
22747
|
"""Node graph operations (color grading nodes). Source can be timeline, timeline item, or color group.
|
|
@@ -22449,7 +22781,7 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
22449
22781
|
For long-form per-action guidance and a worked example, call:
|
|
22450
22782
|
graph(action="action_help", params={"name": "<action>"})
|
|
22451
22783
|
"""
|
|
22452
|
-
p = params
|
|
22784
|
+
p = _params(params)
|
|
22453
22785
|
if action == "action_help":
|
|
22454
22786
|
return _action_help("graph", p)
|
|
22455
22787
|
source = p.get("source", "timeline")
|
|
@@ -22552,6 +22884,7 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
22552
22884
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
22553
22885
|
|
|
22554
22886
|
@mcp.tool()
|
|
22887
|
+
@_guard_missing_params
|
|
22555
22888
|
def color_group(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
22556
22889
|
"""Manage color groups and their node graphs.
|
|
22557
22890
|
|
|
@@ -22563,7 +22896,7 @@ def color_group(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
22563
22896
|
get_pre_clip_graph(group_name) -> {available, num_nodes}
|
|
22564
22897
|
get_post_clip_graph(group_name) -> {available, num_nodes}
|
|
22565
22898
|
"""
|
|
22566
|
-
p = params
|
|
22899
|
+
p = _params(params)
|
|
22567
22900
|
_, proj, err = _check()
|
|
22568
22901
|
if err:
|
|
22569
22902
|
return err
|
|
@@ -23474,6 +23807,7 @@ def _fusion_get_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
23474
23807
|
|
|
23475
23808
|
|
|
23476
23809
|
@mcp.tool()
|
|
23810
|
+
@_guard_missing_params
|
|
23477
23811
|
def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
23478
23812
|
"""Fusion composition node graph operations.
|
|
23479
23813
|
|
|
@@ -23544,7 +23878,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
23544
23878
|
ColorCorrector, RectangleMask, EllipseMask, Tracker, MediaIn, MediaOut,
|
|
23545
23879
|
Loader, Saver, Glow, FilmGrain, CornerPositioner, DeltaKeyer, UltraKeyer
|
|
23546
23880
|
"""
|
|
23547
|
-
p = params
|
|
23881
|
+
p = _params(params)
|
|
23548
23882
|
|
|
23549
23883
|
if action == "bulk_set_inputs":
|
|
23550
23884
|
return _fusion_comp_bulk_set_inputs(p)
|
|
@@ -24044,6 +24378,7 @@ def _validate_glsl_minimal(source: str) -> Dict[str, Any]:
|
|
|
24044
24378
|
|
|
24045
24379
|
|
|
24046
24380
|
@mcp.tool()
|
|
24381
|
+
@_guard_missing_params
|
|
24047
24382
|
def fuse_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
24048
24383
|
"""Author and install Fusion Fuse plugins (.fuse files).
|
|
24049
24384
|
|
|
@@ -24070,7 +24405,7 @@ def fuse_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
24070
24405
|
docs/authoring/fuse-dctl-authoring.md for the per-kind option spec.
|
|
24071
24406
|
list_templates() -> {kinds}
|
|
24072
24407
|
"""
|
|
24073
|
-
p = params
|
|
24408
|
+
p = _params(params)
|
|
24074
24409
|
|
|
24075
24410
|
if action == "path":
|
|
24076
24411
|
return {"fuses_dir": _fuses_dir()}
|
|
@@ -24287,6 +24622,7 @@ _DCTL_VALID_CATEGORIES = ("lut", "aces_idt", "aces_odt")
|
|
|
24287
24622
|
|
|
24288
24623
|
|
|
24289
24624
|
@mcp.tool()
|
|
24625
|
+
@_guard_missing_params
|
|
24290
24626
|
def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
24291
24627
|
"""Author and install DCTL files (Color page custom shaders + ACES transforms).
|
|
24292
24628
|
|
|
@@ -24323,7 +24659,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
|
|
|
24323
24659
|
pass that as the `category` argument to install().
|
|
24324
24660
|
list_templates() -> {kinds, kind_categories}
|
|
24325
24661
|
"""
|
|
24326
|
-
p = params
|
|
24662
|
+
p = _params(params)
|
|
24327
24663
|
|
|
24328
24664
|
def _category(default: str = "lut") -> Tuple[Optional[Dict[str, Any]], str]:
|
|
24329
24665
|
cat = p.get("category", default)
|
|
@@ -25216,6 +25552,7 @@ def _extension_boundary_report(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
25216
25552
|
|
|
25217
25553
|
|
|
25218
25554
|
@mcp.tool()
|
|
25555
|
+
@_guard_missing_params
|
|
25219
25556
|
def script_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
25220
25557
|
"""Author and install Resolve-page Lua/Python scripts (Workspace → Scripts menu).
|
|
25221
25558
|
|
|
@@ -25271,7 +25608,7 @@ def script_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
25271
25608
|
refresh_or_restart_required(extension_type, category?) -> {refresh_luts, restart_required}
|
|
25272
25609
|
extension_boundary_report(include_template_matrix?) -> {capabilities, template_matrix, dry_run_probes}
|
|
25273
25610
|
"""
|
|
25274
|
-
p = params
|
|
25611
|
+
p = _params(params)
|
|
25275
25612
|
|
|
25276
25613
|
if action == "extension_capabilities":
|
|
25277
25614
|
return _extension_capabilities()
|