davinci-resolve-mcp 2.69.3 → 2.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +1 -1
- package/docs/SKILL.md +78 -1
- package/docs/guides/headless-edit-loop.md +238 -0
- package/docs/reference/api-limitations.md +65 -4
- package/docs/reference/headless-capability-matrix.md +56 -0
- package/docs/reference/headless-cli.md +751 -0
- package/docs/reference/pixel-equality.md +11 -0
- package/docs/reference/roundtrip-fidelity.md +17 -0
- package/docs/reference/roundtrip-rich-fidelity.md +18 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/bisect_headless_hang.py +125 -0
- package/scripts/contact_sheet.py +209 -0
- package/scripts/headless_differential.py +578 -0
- package/scripts/mode_matrix.py +345 -0
- package/scripts/mode_matrix_worker.py +232 -0
- package/scripts/pixel_equality.py +402 -0
- package/scripts/relink_moved_media.py +267 -0
- package/scripts/render_stress.py +542 -0
- package/scripts/resolve_headless.py +225 -0
- package/scripts/roundtrip_matrix.py +434 -0
- package/scripts/roundtrip_rich.py +431 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +92 -67
- package/src/utils/api_truth.py +302 -4
- package/src/utils/headless_differential.py +363 -0
- package/src/utils/mode_matrix.py +338 -0
- package/src/utils/probe_catalogue.py +796 -0
- package/src/utils/project_cleanup.py +61 -9
- package/src/utils/resolve_runtime.py +233 -0
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.70.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -105,6 +105,7 @@ from src.utils.media_analysis_jobs import (
|
|
|
105
105
|
)
|
|
106
106
|
from src.utils.platform import get_resolve_paths, get_resolve_plugin_paths
|
|
107
107
|
from src.utils.resolve_connection import connect_resolve
|
|
108
|
+
from src.utils import resolve_runtime as _resolve_runtime
|
|
108
109
|
from src.utils.lut_paths import master_lut_dir, ensure_lut_in_master
|
|
109
110
|
from src.utils import fuse_templates, dctl_templates, script_templates
|
|
110
111
|
from src.utils.timeline_title_text import (
|
|
@@ -855,18 +856,16 @@ def _try_connect():
|
|
|
855
856
|
resolve = None
|
|
856
857
|
return None
|
|
857
858
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
#: when the free edition was the one in use. Ordered installer-first to keep the
|
|
861
|
-
#: previous behaviour where Studio is present.
|
|
862
|
-
_MACOS_RESOLVE_APPS = (
|
|
863
|
-
"/Applications/DaVinci Resolve/DaVinci Resolve.app", # installer / Studio
|
|
864
|
-
"/Applications/DaVinci Resolve.app", # App Store, free
|
|
865
|
-
)
|
|
866
|
-
|
|
859
|
+
def _launch_resolve(headless: Optional[bool] = None):
|
|
860
|
+
"""Launch DaVinci Resolve and wait for it to become available.
|
|
867
861
|
|
|
868
|
-
|
|
869
|
-
|
|
862
|
+
`headless=None` defers to the DAVINCI_RESOLVE_HEADLESS environment variable.
|
|
863
|
+
A headless launch is capability-identical to a GUI one (see
|
|
864
|
+
docs/reference/headless-cli.md) and immune to the modal dialogs that
|
|
865
|
+
otherwise wedge an agent-driven session, so it is the better default for
|
|
866
|
+
unattended work — but it stays opt-in, because starting an invisible
|
|
867
|
+
application on a workstation is not something to do behind the user's back.
|
|
868
|
+
"""
|
|
870
869
|
if _bridge_requested():
|
|
871
870
|
# Launching cannot produce a bridge. The listener only exists once
|
|
872
871
|
# someone runs Workspace > Scripts > resolve_bridge inside an already
|
|
@@ -878,28 +877,17 @@ def _launch_resolve():
|
|
|
878
877
|
"launching the application cannot start it."
|
|
879
878
|
)
|
|
880
879
|
return False
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
return False
|
|
887
|
-
subprocess.Popen(["open", app_path], stdin=subprocess.DEVNULL)
|
|
888
|
-
elif sys_name == "windows":
|
|
889
|
-
app_path = r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Resolve.exe"
|
|
890
|
-
if not os.path.exists(app_path):
|
|
891
|
-
logger.error(f"DaVinci Resolve not found at {app_path}")
|
|
892
|
-
return False
|
|
893
|
-
subprocess.Popen([app_path], stdin=subprocess.DEVNULL)
|
|
894
|
-
elif sys_name == "linux":
|
|
895
|
-
app_path = "/opt/resolve/bin/resolve"
|
|
896
|
-
if not os.path.exists(app_path):
|
|
897
|
-
logger.error(f"DaVinci Resolve not found at {app_path}")
|
|
898
|
-
return False
|
|
899
|
-
subprocess.Popen([app_path], stdin=subprocess.DEVNULL)
|
|
900
|
-
else:
|
|
880
|
+
if headless is None:
|
|
881
|
+
headless = _resolve_runtime.prefers_headless()
|
|
882
|
+
command = _resolve_runtime.launch_command(bool(headless))
|
|
883
|
+
if command is None:
|
|
884
|
+
logger.error("DaVinci Resolve not found on this platform's known install paths")
|
|
901
885
|
return False
|
|
902
|
-
|
|
886
|
+
subprocess.Popen(command, stdin=subprocess.DEVNULL)
|
|
887
|
+
logger.info(
|
|
888
|
+
"Launched DaVinci Resolve (%s), waiting for it to respond...",
|
|
889
|
+
"headless" if headless else "with UI",
|
|
890
|
+
)
|
|
903
891
|
for i in range(30):
|
|
904
892
|
time.sleep(2)
|
|
905
893
|
if _try_connect():
|
|
@@ -908,16 +896,6 @@ def _launch_resolve():
|
|
|
908
896
|
logger.warning("Resolve did not respond within 60s after launch")
|
|
909
897
|
return False
|
|
910
898
|
|
|
911
|
-
#: Process names that mean a DaVinci Resolve application is already running.
|
|
912
|
-
#: Deliberately not the full path: the App Store build lives at
|
|
913
|
-
#: `/Applications/DaVinci Resolve.app` and the installer build inside
|
|
914
|
-
#: `/Applications/DaVinci Resolve/`, and either one counts.
|
|
915
|
-
_RESOLVE_PROCESS_PATTERNS = (
|
|
916
|
-
"DaVinci Resolve.app/Contents/MacOS/Resolve", # macOS, both editions
|
|
917
|
-
"Resolve.exe", # Windows
|
|
918
|
-
"/opt/resolve/bin/resolve", # Linux
|
|
919
|
-
)
|
|
920
|
-
|
|
921
899
|
|
|
922
900
|
def resolve_is_running() -> Optional[bool]:
|
|
923
901
|
"""Is a Resolve application already up? None when it cannot be determined.
|
|
@@ -926,17 +904,7 @@ def resolve_is_running() -> Optional[bool]:
|
|
|
926
904
|
unanswerable question never silently becomes "nothing is running" — which is
|
|
927
905
|
the answer that leads to launching something.
|
|
928
906
|
"""
|
|
929
|
-
|
|
930
|
-
if platform.system().lower() == "windows":
|
|
931
|
-
out = subprocess.run(["tasklist"], capture_output=True, text=True, timeout=10, check=False)
|
|
932
|
-
else:
|
|
933
|
-
out = subprocess.run(["ps", "-Ao", "command="], capture_output=True, text=True, timeout=10, check=False)
|
|
934
|
-
if out.returncode != 0 and not out.stdout:
|
|
935
|
-
return None
|
|
936
|
-
listing = out.stdout or ""
|
|
937
|
-
except Exception: # pragma: no cover - defensive; an unknown answer is None
|
|
938
|
-
return None
|
|
939
|
-
return any(pattern in listing for pattern in _RESOLVE_PROCESS_PATTERNS)
|
|
907
|
+
return _resolve_runtime.runtime_mode()["running"]
|
|
940
908
|
|
|
941
909
|
|
|
942
910
|
def get_resolve():
|
|
@@ -1017,7 +985,11 @@ def _not_connected_error():
|
|
|
1017
985
|
remediation="On Studio: Preferences > General > 'External scripting using' = Local. "
|
|
1018
986
|
"On the free edition: install the in-app bridge, run "
|
|
1019
987
|
"Workspace > Scripts > resolve_bridge, and set DAVINCI_RESOLVE_BRIDGE=1.",
|
|
1020
|
-
|
|
988
|
+
# Which Resolve is refusing matters to the reader: a headless render
|
|
989
|
+
# worker and the editor the user is looking at warrant different
|
|
990
|
+
# responses, and the in-app bridge is not an option for the former.
|
|
991
|
+
state={"resolve_running": True, "bridge_enabled": False,
|
|
992
|
+
"headless": _resolve_runtime.is_headless()},
|
|
1021
993
|
)
|
|
1022
994
|
return _err(
|
|
1023
995
|
"DaVinci Resolve is not running and could not be started.",
|
|
@@ -13074,7 +13046,18 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13074
13046
|
"""App-level DaVinci Resolve operations.
|
|
13075
13047
|
|
|
13076
13048
|
Actions:
|
|
13077
|
-
launch() -> {success, message
|
|
13049
|
+
launch(headless?) -> {success, message, running, headless, guidance}
|
|
13050
|
+
— Launch DaVinci Resolve if not running. Call this FIRST if any tool returns
|
|
13051
|
+
a 'Not connected' error. headless=true starts it with no UI (-nogui):
|
|
13052
|
+
capability-identical to the GUI and immune to the modal dialogs that can
|
|
13053
|
+
block a script indefinitely. Errors rather than starting a second instance
|
|
13054
|
+
if one is already running in the other mode.
|
|
13055
|
+
runtime_mode() -> {running, headless, instances, guidance}
|
|
13056
|
+
— Is Resolve up, and does it have a UI? Answers with no connection needed.
|
|
13057
|
+
headless is null when it cannot be determined — do NOT read that as false.
|
|
13058
|
+
There is no way to tell from the scripting API itself; this reads the
|
|
13059
|
+
process argv. Consult before any LoadProject/CloseProject: with a UI,
|
|
13060
|
+
an unsaved outgoing project raises a dialog no script can dismiss.
|
|
13078
13061
|
get_version() -> {product, version, version_string, mcp: {version, update, update_decision}}
|
|
13079
13062
|
— mcp.update_decision piggybacks the cached update check: if its action is
|
|
13080
13063
|
"notify" or "prompt", mention the available MCP update to the user ONCE
|
|
@@ -13172,11 +13155,58 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13172
13155
|
clear_update_prompt_preferences(project_dir, env=_setup_update_env())
|
|
13173
13156
|
return _ok(**_mcp_update_status_payload())
|
|
13174
13157
|
|
|
13158
|
+
# runtime_mode reads the operating system, not Resolve, so it answers even
|
|
13159
|
+
# when nothing is running — which is exactly when an agent needs it.
|
|
13160
|
+
if action == "runtime_mode":
|
|
13161
|
+
payload = _resolve_runtime.describe()
|
|
13162
|
+
# If something IS running, add the one reading that distinguishes a
|
|
13163
|
+
# healthy instance from a wedged one. A Resolve can come up attached to
|
|
13164
|
+
# NO database: it answers GetProductName, GetVersionString and
|
|
13165
|
+
# GetCurrentPage normally, so every liveness check passes, while
|
|
13166
|
+
# CreateProject and LoadProject return False forever and SaveProject
|
|
13167
|
+
# returns None. Observed after an unclean shutdown, and it does not
|
|
13168
|
+
# recover on its own — only a restart clears it.
|
|
13169
|
+
if payload.get("running"):
|
|
13170
|
+
try:
|
|
13171
|
+
r = _try_connect() and resolve
|
|
13172
|
+
database = r.GetProjectManager().GetCurrentDatabase() if r else None
|
|
13173
|
+
except Exception:
|
|
13174
|
+
database = None
|
|
13175
|
+
payload["database"] = database
|
|
13176
|
+
payload["database_attached"] = bool(database)
|
|
13177
|
+
if payload.get("determinable") and not database:
|
|
13178
|
+
payload["guidance"] = (
|
|
13179
|
+
"WEDGED: Resolve is running and answers version queries, but no "
|
|
13180
|
+
"database is attached — CreateProject/LoadProject will return "
|
|
13181
|
+
"False indefinitely and SaveProject returns None. This does not "
|
|
13182
|
+
"recover; quit and restart Resolve before doing any work."
|
|
13183
|
+
) + " || " + payload["guidance"]
|
|
13184
|
+
return _ok(**payload)
|
|
13185
|
+
|
|
13175
13186
|
# launch works even when Resolve is not connected
|
|
13176
13187
|
if action == "launch":
|
|
13188
|
+
headless = p.get("headless")
|
|
13189
|
+
if headless is not None:
|
|
13190
|
+
# An explicit request has to survive the "already running" shortcut
|
|
13191
|
+
# in get_resolve: connecting to the GUI instance someone asked to
|
|
13192
|
+
# replace with a headless one is not what was asked for.
|
|
13193
|
+
mode = _resolve_runtime.runtime_mode()
|
|
13194
|
+
if mode["running"] and bool(headless) != bool(mode["headless"]):
|
|
13195
|
+
return _err(
|
|
13196
|
+
"DaVinci Resolve is already running in the other mode.",
|
|
13197
|
+
code="RESOLVE_MODE_CONFLICT", category="not_connected", retryable=False,
|
|
13198
|
+
reason="Only one Resolve can run at a time; a second instance fights the "
|
|
13199
|
+
"singleton and has been observed to crash-loop rather than fail cleanly.",
|
|
13200
|
+
remediation="Quit the running instance first (resolve_control quit), then "
|
|
13201
|
+
"launch again with the mode you want.",
|
|
13202
|
+
state=mode,
|
|
13203
|
+
)
|
|
13204
|
+
if not mode["running"]:
|
|
13205
|
+
_launch_resolve(headless=bool(headless))
|
|
13177
13206
|
r = get_resolve() # auto-launches if not running
|
|
13178
13207
|
if r is not None:
|
|
13179
|
-
return _ok(message="DaVinci Resolve is running and connected."
|
|
13208
|
+
return _ok(message="DaVinci Resolve is running and connected.",
|
|
13209
|
+
**_resolve_runtime.describe())
|
|
13180
13210
|
return _not_connected_error()
|
|
13181
13211
|
|
|
13182
13212
|
r = get_resolve() # auto-launches if not running
|
|
@@ -13228,7 +13258,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13228
13258
|
return missing
|
|
13229
13259
|
r.DisableBackgroundTasksForCurrentResolveSession()
|
|
13230
13260
|
return _ok()
|
|
13231
|
-
return _unknown(action, ["launch","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"])
|
|
13261
|
+
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"])
|
|
13232
13262
|
|
|
13233
13263
|
|
|
13234
13264
|
# ─── V2 C4: Per-field corrections with provenance + changelog ────────────────
|
|
@@ -20440,7 +20470,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
20440
20470
|
set_mark_in_out(mark_in, mark_out, type?) -> {success}
|
|
20441
20471
|
clear_mark_in_out(type?) -> {success}
|
|
20442
20472
|
convert_to_stereo() -> {success}
|
|
20443
|
-
get_items_in_track(track_type, track_index|index) -> {items} —
|
|
20473
|
+
get_items_in_track(track_type, track_index|index) -> {items} — alias of get_items
|
|
20444
20474
|
get_voice_isolation_state(track_index) -> {isEnabled, amount}
|
|
20445
20475
|
set_voice_isolation_state(track_index, state) -> {success}
|
|
20446
20476
|
extract_source_frame_ranges(handles?, gap_max?, skip_extensions?) -> {timeline_name, frame_ranges, occurrences, ...}
|
|
@@ -20645,7 +20675,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
20645
20675
|
return {"name": tl.GetTrackName(p["track_type"], p["index"])}
|
|
20646
20676
|
elif action == "set_track_name":
|
|
20647
20677
|
return {"success": bool(tl.SetTrackName(p["track_type"], p["index"], p["name"]))}
|
|
20648
|
-
elif action
|
|
20678
|
+
elif action in {"get_items", "get_items_in_track"}:
|
|
20649
20679
|
track_type, track_index, err = _track_selector(p)
|
|
20650
20680
|
if err:
|
|
20651
20681
|
return _err(err)
|
|
@@ -20884,11 +20914,6 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
20884
20914
|
return {"success": bool(tl.ClearMarkInOut(p.get("type", "all")))}
|
|
20885
20915
|
elif action == "convert_to_stereo":
|
|
20886
20916
|
return {"success": bool(tl.ConvertTimelineToStereo())}
|
|
20887
|
-
elif action == "get_items_in_track":
|
|
20888
|
-
track_type, track_index, err = _track_selector(p)
|
|
20889
|
-
if err:
|
|
20890
|
-
return _err(err)
|
|
20891
|
-
return {"items": _ser(tl.GetItemListInTrack(track_type, track_index))}
|
|
20892
20917
|
elif action == "get_voice_isolation_state":
|
|
20893
20918
|
missing = _requires_method(tl, "GetVoiceIsolationState", "20.1")
|
|
20894
20919
|
if missing:
|
|
@@ -22235,9 +22260,9 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
22235
22260
|
"example": 'timeline(action="get_items", params={"track_type": "video", "index": 1})',
|
|
22236
22261
|
},
|
|
22237
22262
|
"get_items_in_track": {
|
|
22238
|
-
"summary": "
|
|
22263
|
+
"summary": "Alias of get_items: list items on one track (name/id/start/end/duration).",
|
|
22239
22264
|
"params": "track_type (video|audio|subtitle), track_index|index (1-based)",
|
|
22240
|
-
"returns": "{items}",
|
|
22265
|
+
"returns": "{items: [{name, id, start, end, duration}]}",
|
|
22241
22266
|
"example": 'timeline(action="get_items_in_track", params={"track_type": "audio", "track_index": 1})',
|
|
22242
22267
|
},
|
|
22243
22268
|
"duplicate_clips": {
|
package/src/utils/api_truth.py
CHANGED
|
@@ -101,10 +101,233 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
101
101
|
"reality": "Returns False (no deletion) when the target project is, or "
|
|
102
102
|
"recently was, the current project, and is flaky on the first "
|
|
103
103
|
"attempt — so a single bool() call leaves the project undeleted "
|
|
104
|
-
"with no useful error."
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
"with no useful error. CloseProject reliably releases it: a "
|
|
105
|
+
"delete that had failed six times in a row, a second apart, "
|
|
106
|
+
"succeeded on the first attempt after one. Switching away with "
|
|
107
|
+
"LoadProject is NOT reliable — it left the delete failing "
|
|
108
|
+
"permanently after a heavily-used project, yet succeeded for a "
|
|
109
|
+
"project that had only been created and loaded. Whatever "
|
|
110
|
+
"distinguishes the two (modification? an open timeline?) is "
|
|
111
|
+
"not established, so LoadProject-away cannot be depended on.",
|
|
112
|
+
"recommended": "CloseProject the target FIRST (that is what releases it), "
|
|
113
|
+
"then LoadProject some named project so the session is not "
|
|
114
|
+
"left on the unsaveable 'Untitled Project' fallback, then "
|
|
115
|
+
"delete. Use src/utils/project_cleanup.py:delete_project_safely, "
|
|
116
|
+
"which does exactly that.",
|
|
117
|
+
"tags": ["unreliable-return", "project", "flaky", "session-lock"],
|
|
118
|
+
"submit": "bug",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"symbol": "Project.SetSetting('timelinePlaybackFrameRate')",
|
|
122
|
+
"object": "Project",
|
|
123
|
+
"reality": "Returns False for every value form tried (string, int, "
|
|
124
|
+
"float), both before and after a timeline exists, so the "
|
|
125
|
+
"playback frame rate cannot be set from the API at all. "
|
|
126
|
+
"Reported by a community contributor against Resolve Studio "
|
|
127
|
+
"while assembling a vertical timeline (PR #99).",
|
|
128
|
+
"recommended": "Ask the user to set it in Project Settings > Master "
|
|
129
|
+
"Settings > Playback frame rate as a SETUP step, before "
|
|
130
|
+
"any timeline exists. Read it back to confirm; do not "
|
|
131
|
+
"report it as set on the strength of the call alone.",
|
|
132
|
+
"tags": ["project-settings", "silent-failure", "timeline"],
|
|
133
|
+
"submit": "missing",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"symbol": "ProjectManager.GetCurrentDatabase",
|
|
137
|
+
"object": "ProjectManager",
|
|
138
|
+
"signature": "() -> {DbType, DbName}",
|
|
139
|
+
"reality": "Returns None when Resolve has come up without attaching to a "
|
|
140
|
+
"project database — a state it reaches after an unclean "
|
|
141
|
+
"shutdown, and does not recover from on its own. In that state "
|
|
142
|
+
"the application looks entirely healthy: it accepts scripting "
|
|
143
|
+
"connections, and GetProductName, GetVersionString, "
|
|
144
|
+
"GetCurrentPage and GetCurrentProject all answer normally. But "
|
|
145
|
+
"CreateProject and LoadProject return False indefinitely, "
|
|
146
|
+
"SaveProject returns None, and some calls block forever in "
|
|
147
|
+
"the scripting transport rather than returning at all. "
|
|
148
|
+
"Observed headless on Studio 19.1.3.7 after force-killing a "
|
|
149
|
+
"wedged instance; the replacement took 2m05s to become "
|
|
150
|
+
"scriptable and came up with no database.",
|
|
151
|
+
"recommended": "Treat a non-None GetCurrentDatabase() as the liveness "
|
|
152
|
+
"check, not a successful connection — every cheaper check "
|
|
153
|
+
"passes in the wedged state. `resolve_control "
|
|
154
|
+
"runtime_mode` reports `database_attached` for exactly "
|
|
155
|
+
"this. There is no repair: quit and restart Resolve.",
|
|
156
|
+
"tags": ["project", "database", "headless", "silent-failure", "startup"],
|
|
157
|
+
"mitigation": ["runtime_mode"],
|
|
158
|
+
"submit": "bug",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"symbol": "MediaPool.ImportTimelineFromFile",
|
|
162
|
+
"object": "MediaPool",
|
|
163
|
+
"signature": "(filePath, {importOptions}) -> Timeline",
|
|
164
|
+
"reality": "Returns None — no error, no exception — when the requested "
|
|
165
|
+
"`timelineName` already exists. Measured: importing one file "
|
|
166
|
+
"three times with the same name succeeded once and returned "
|
|
167
|
+
"None twice. An iterative loop that reuses a fixed name "
|
|
168
|
+
"therefore works exactly once and then silently does nothing. "
|
|
169
|
+
"DRT ignores importOptions entirely (timelineName, "
|
|
170
|
+
"importSourceClips and sourceClipsFolders are all invalid for "
|
|
171
|
+
"it): the timeline is named after the FILE, repeats "
|
|
172
|
+
"auto-uniquify ('iter', 'iter 2', 'iter 3'), and because "
|
|
173
|
+
"importSourceClips cannot be disabled each DRT import adds "
|
|
174
|
+
"another copy of the source media to the pool. OTIO is the one "
|
|
175
|
+
"format that will NOT relink from the pool — with "
|
|
176
|
+
"importSourceClips=False its timeline rebuilds with correct "
|
|
177
|
+
"structure and every item OFFLINE, in both GUI and headless.",
|
|
178
|
+
"recommended": "For a repeatable loop use FCP7 XML or AAF with a UNIQUE "
|
|
179
|
+
"timelineName per iteration plus importSourceClips=False "
|
|
180
|
+
"and sourceClipsFolders=[root] — verified frame-exact over "
|
|
181
|
+
"five consecutive imports with no media duplicated. Use DRT "
|
|
182
|
+
"for one-shot hand-offs only. For OTIO, pass "
|
|
183
|
+
"importSourceClips=True with a sourceClipsPath. See "
|
|
184
|
+
"docs/guides/headless-edit-loop.md.",
|
|
185
|
+
"tags": ["timeline", "import", "interchange", "silent-failure", "conform"],
|
|
186
|
+
"submit": "bug",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"symbol": "Timeline.Export / ImportTimelineFromFile (format fidelity)",
|
|
190
|
+
"object": "Timeline",
|
|
191
|
+
"reality": "The formats are NOT interchangeable, and which one is right "
|
|
192
|
+
"depends on the job — measured identically in GUI and "
|
|
193
|
+
"headless. On a rich cut (2 video tracks, audio, per-item "
|
|
194
|
+
"transform, colours, flags, item and timeline markers) only "
|
|
195
|
+
"DRT kept everything. FCP7 XML kept the cut, tracks and most "
|
|
196
|
+
"of the transform, losing only FlipX. AAF kept the cut and "
|
|
197
|
+
"tracks but RESET THE WHOLE TRANSFORM to defaults. FCPXML 1.10 "
|
|
198
|
+
"silently dropped an audio track (2v2a in, 2v1a out). OTIO "
|
|
199
|
+
"lost the source association. EDL collapsed to 1v1a. "
|
|
200
|
+
"But with media MOVED and sourceClipsPath given, the ranking "
|
|
201
|
+
"inverts: FCP7 XML, AAF and FCPXML 1.10 relinked 3/3 and stayed "
|
|
202
|
+
"frame-exact, while DRT relinked 0/3 (sourceClipsPath is not "
|
|
203
|
+
"valid for DRT, so it cannot be told where the media went), EDL "
|
|
204
|
+
"relinked 0/3, and OTIO's import returned None outright.",
|
|
205
|
+
"recommended": "Pick per job: DRT for full fidelity with media in place; "
|
|
206
|
+
"FCP7 XML or AAF for an iterative loop or any conform where "
|
|
207
|
+
"media may move; never DRT/OTIO/EDL for a relink. Note the "
|
|
208
|
+
"cut stays frame-exact even when relinking fails, so a "
|
|
209
|
+
"failed relink leaves a correct offline timeline rather "
|
|
210
|
+
"than a wrong one. See docs/guides/headless-edit-loop.md.",
|
|
211
|
+
"tags": ["timeline", "export", "import", "interchange", "conform"],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"symbol": "BMD_RESOLVE_*_DIR environment variables",
|
|
215
|
+
"object": "Resolve",
|
|
216
|
+
"reality": "BMD_RESOLVE_CONFIG_DIR / SUPPORT_DIR / LUT_DIR genuinely "
|
|
217
|
+
"isolate an instance — a launch with them set builds a private "
|
|
218
|
+
"config.dat plus its own Resolve Project Library, Fairlight, "
|
|
219
|
+
"DolbyVision and easyDCP trees, and the resulting instance sees "
|
|
220
|
+
"ZERO of the machine's real projects. They do NOT defeat the "
|
|
221
|
+
"single-instance guard: started while another Resolve was "
|
|
222
|
+
"running, the second process created its private tree and then "
|
|
223
|
+
"exited silently, logging nothing. So isolated instances are "
|
|
224
|
+
"possible and PARALLEL instances are not. A fresh config also "
|
|
225
|
+
"omits `System.Scripting.Mode = 1`, so an isolated instance is "
|
|
226
|
+
"unreachable by scripting until that line is appended to its "
|
|
227
|
+
"config.dat (plain ASCII) — until then it runs, opens no "
|
|
228
|
+
"listener, and looks exactly like a hang. "
|
|
229
|
+
"BMD_RESOLVE_LOGS_DIR only partly takes: LogArchive/ and "
|
|
230
|
+
"gpudetect.bin appear there, the main log does not.",
|
|
231
|
+
"recommended": "Use for a CI/render worker that must not see or lock the "
|
|
232
|
+
"operator's projects. Seed the scripting preference after "
|
|
233
|
+
"the first boot. Do not expect to run two at once. Recipe "
|
|
234
|
+
"in docs/reference/headless-cli.md.",
|
|
235
|
+
"tags": ["headless", "isolation", "singleton", "ci", "startup"],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"symbol": "Resolve -fastmode",
|
|
239
|
+
"object": "Resolve",
|
|
240
|
+
"reality": "Starts a process that stays alive at 0% CPU and never opens "
|
|
241
|
+
"the scripting listener — port 15000 is not bound at all, and "
|
|
242
|
+
"it was still unreachable after two minutes. Exercised in an "
|
|
243
|
+
"isolated sandbox on Studio 19.1.3.7.",
|
|
244
|
+
"recommended": "Not usable for automation; do not pass it. (`-activate` "
|
|
245
|
+
"and `-deactivate` remain deliberately unexercised: their "
|
|
246
|
+
"adjacent strings show they mutate this machine's Studio "
|
|
247
|
+
"licence activation.)",
|
|
248
|
+
"tags": ["headless", "cli-flags"],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"symbol": "Timeline.Export(EXPORT_FCPXML_1_10)",
|
|
252
|
+
"object": "Timeline",
|
|
253
|
+
"signature": "(fileName, EXPORT_FCPXML_1_10, EXPORT_NONE) -> bool",
|
|
254
|
+
"reality": "Returns True and creates a BUNDLE DIRECTORY at the given "
|
|
255
|
+
"path containing `Info.fcpxml`, rather than a file — the "
|
|
256
|
+
"`.fcpxmld` shape Final Cut uses. Two consequences bite "
|
|
257
|
+
"immediately: a `stat().st_size` check reads the directory "
|
|
258
|
+
"inode (96 bytes here) and concludes the export is empty, and "
|
|
259
|
+
"ImportTimelineFromFile on that path fails because it is a "
|
|
260
|
+
"directory. Every other EXPORT_* type in this build writes a "
|
|
261
|
+
"plain file, so code that treats them uniformly gets this one "
|
|
262
|
+
"wrong. Pointed at the inner member, the format round-trips "
|
|
263
|
+
"frame-exactly in both modes.",
|
|
264
|
+
"recommended": "After exporting, check `Path(p).is_dir()` and import "
|
|
265
|
+
"`next(Path(p).glob('*.fcpxml'))` instead. Do not size-"
|
|
266
|
+
"check the export path itself.",
|
|
267
|
+
"tags": ["timeline", "export", "interchange", "fcpxml", "silent-failure"],
|
|
268
|
+
"submit": "bug",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"symbol": "MediaPool.CreateTimelineFromClips",
|
|
272
|
+
"object": "MediaPool",
|
|
273
|
+
"reality": "Fails with a bare 'Failed to create timeline from clip_infos' "
|
|
274
|
+
"— naming nothing actionable — when the Media Pool's CURRENT "
|
|
275
|
+
"folder is not the folder holding the clips, even though every "
|
|
276
|
+
"clip_id passed is valid and resolvable. Reported against "
|
|
277
|
+
"Resolve Studio in PR #99.",
|
|
278
|
+
"recommended": "Call MediaPool.SetCurrentFolder() to the clips' bin "
|
|
279
|
+
"before creating the timeline (media_pool "
|
|
280
|
+
"set_current_folder). Valid ids are not sufficient.",
|
|
281
|
+
"tags": ["media-pool", "timeline", "unhelpful-error"],
|
|
282
|
+
"submit": "bug",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"symbol": "TimelineItem.AddFusionComp / LoadFusionCompByName",
|
|
286
|
+
"object": "TimelineItem (media-backed clip)",
|
|
287
|
+
"reality": "A Fusion composition created on a media clip through the API "
|
|
288
|
+
"is not applied at render WHEN MEDIAOUT HAS NO PATH FROM "
|
|
289
|
+
"MEDIAIN. The original blanket form of this entry — 'never "
|
|
290
|
+
"applied at render' — was too broad and was corrected on "
|
|
291
|
+
"2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created "
|
|
292
|
+
"entirely through the API on an ordinary media clip, DOES "
|
|
293
|
+
"render. PSNR between the plain and Fusion renders of the same "
|
|
294
|
+
"timeline was 22.7 dB (identical would be infinite), the file "
|
|
295
|
+
"shrank 22.5 MB -> 14.8 MB as a blur should, and the output was "
|
|
296
|
+
"frame-for-frame identical in GUI and headless. A first attempt "
|
|
297
|
+
"that wired ONLY MediaOut -> Blur, leaving the Blur with no "
|
|
298
|
+
"source, made the render job come back 'Failed' with an "
|
|
299
|
+
"887-byte file — so an unrooted graph does not merely get "
|
|
300
|
+
"bypassed, it can take the render down. "
|
|
301
|
+
"What still stands is the original observation for the "
|
|
302
|
+
"configuration it actually tested, which is retained below and "
|
|
303
|
+
"has NOT been re-measured: AddFusionComp() returns the comp, "
|
|
304
|
+
"AddTool/Connect/SetInput all succeed, and the whole graph "
|
|
305
|
+
"reads back correctly (GetCompCount 1, MediaOut1.Input wired "
|
|
306
|
+
"to the new tool, StyledText returning the value just set) — "
|
|
307
|
+
"but the rendered output is byte-for-byte the untouched source "
|
|
308
|
+
"media. Verified live on Studio 19.1.3.7 with the strongest "
|
|
309
|
+
"form of the test: MediaOut1 fed ONLY by a Text+, with no path "
|
|
310
|
+
"from MediaIn at all, still rendered the unmodified clip. "
|
|
311
|
+
"LoadFusionCompByName on the sole comp does not activate it "
|
|
312
|
+
"either. Contrast InsertFusionTitleIntoTimeline, whose comp "
|
|
313
|
+
"DOES render — text set via SetInput('StyledText') appears in "
|
|
314
|
+
"the output — so this is specific to comps attached to "
|
|
315
|
+
"media-backed clips, not to Fusion through the API generally.",
|
|
316
|
+
"recommended": "Wire the graph so MediaOut descends from MediaIn — that "
|
|
317
|
+
"is the difference between a comp that renders and one "
|
|
318
|
+
"that is silently bypassed, and it is what made this look "
|
|
319
|
+
"like 'Fusion never renders from the API'. Never leave a "
|
|
320
|
+
"tool unrooted: a MediaOut fed by a tool with no source "
|
|
321
|
+
"failed the render job outright. "
|
|
322
|
+
"For text or effects over picture, insert a Fusion title/"
|
|
323
|
+
"generator as its own timeline clip and set its Text+ "
|
|
324
|
+
"(fusion_comp set_text_plus), rather than attaching a comp "
|
|
325
|
+
"to the media clip. Note the destination track cannot be "
|
|
326
|
+
"chosen from the API (see the Track Selector entry), so "
|
|
327
|
+
"overlaying onto an existing clip's track is not currently "
|
|
328
|
+
"reachable end-to-end. Building the comp in the Fusion page "
|
|
329
|
+
"UI works; only the API-created comp is ignored.",
|
|
330
|
+
"tags": ["fusion", "silent-failure", "render"],
|
|
108
331
|
"submit": "bug",
|
|
109
332
|
},
|
|
110
333
|
{
|
|
@@ -636,6 +859,81 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
636
859
|
"tags": ["render", "deliver", "audio", "unsupported"],
|
|
637
860
|
"submit": "missing",
|
|
638
861
|
},
|
|
862
|
+
{
|
|
863
|
+
"symbol": "ProjectManager.SaveProject",
|
|
864
|
+
"object": "ProjectManager",
|
|
865
|
+
"signature": "() -> bool",
|
|
866
|
+
"reality": "On the default, never-saved project named 'Untitled Project' "
|
|
867
|
+
"this call CANNOT succeed — the project has no location and "
|
|
868
|
+
"there is no SaveProjectAs to give it one — and the two modes "
|
|
869
|
+
"fail differently. In the GUI it returns False. HEADLESS IT "
|
|
870
|
+
"BLOCKS FOREVER: measured on a cold -nogui boot with the "
|
|
871
|
+
"database verified attached immediately beforehand, no return "
|
|
872
|
+
"after 45s, the client parked in Fusion::RemoteApp::WaitPkt. "
|
|
873
|
+
"Resolve wants a Save-As dialog and waits for an answer that "
|
|
874
|
+
"cannot arrive. It also degrades the instance: after the hung "
|
|
875
|
+
"call was interrupted, SaveProject began returning None "
|
|
876
|
+
"instantly on that instance. On a named project it is fine in "
|
|
877
|
+
"both modes.",
|
|
878
|
+
"recommended": "Guard it: only call SaveProject when "
|
|
879
|
+
"GetCurrentProject().GetName() != 'Untitled Project'. "
|
|
880
|
+
"There is nothing to save on the default project and the "
|
|
881
|
+
"call cannot succeed, so skipping it loses nothing. Do NOT "
|
|
882
|
+
"reach for headless to dodge the GUI's save dialog — "
|
|
883
|
+
"headless turns that dialog into an unbounded hang that no "
|
|
884
|
+
"human can clear.",
|
|
885
|
+
"tags": ["project", "modal", "headless", "hang", "silent-failure", "unreliable-return"],
|
|
886
|
+
"submit": "bug",
|
|
887
|
+
"mitigation": ["save_project_if_safe"],
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
"symbol": "Resolve.GetCurrentPage",
|
|
891
|
+
"object": "Resolve",
|
|
892
|
+
"signature": "() -> string",
|
|
893
|
+
"reality": "A headless (-nogui) instance answers with a real page name, "
|
|
894
|
+
"not None, and OpenPage() succeeds and reads back for all "
|
|
895
|
+
"seven pages with no UI present. Nothing on the scripting API "
|
|
896
|
+
"distinguishes a headless Resolve from one with a window — "
|
|
897
|
+
"product name, version, layout presets, Gallery handles and "
|
|
898
|
+
"Fusion comps all behave identically (Studio 19.1.3.7, 238 "
|
|
899
|
+
"paired observations, zero headless-only failures).",
|
|
900
|
+
"recommended": "Do not infer headlessness from any API reading. Use "
|
|
901
|
+
"resolve_control runtime_mode, which reads -nogui out of "
|
|
902
|
+
"the process argv — the only place it is visible.",
|
|
903
|
+
"tags": ["headless", "page", "detection"],
|
|
904
|
+
"mitigation": ["runtime_mode", "resolve_runtime.is_headless"],
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
"symbol": "GalleryStillAlbum.ExportStills",
|
|
908
|
+
"object": "GalleryStillAlbum",
|
|
909
|
+
"signature": "(galleryStills, folderPath, filePrefix, format) -> bool",
|
|
910
|
+
"reality": "PANEL-dependent, not mode-dependent — and the distinction "
|
|
911
|
+
"took three revisions of this entry to pin down, so the "
|
|
912
|
+
"evidence is recorded rather than summarised. Across four "
|
|
913
|
+
"controlled 92-probe sweeps (2 GUI, 2 headless, 2026-08-01) it "
|
|
914
|
+
"returned False and wrote nothing in ALL FOUR, while "
|
|
915
|
+
"Timeline.GrabStill() succeeded in all four — so it is not "
|
|
916
|
+
"being handed an empty still. In one earlier GUI session it "
|
|
917
|
+
"DID work, returning True and writing 2 files. The variable "
|
|
918
|
+
"that differed is not the mode: it is whether the Gallery "
|
|
919
|
+
"panel was visible on the Color page, which depends on the "
|
|
920
|
+
"restored workspace layout and which the harness does not "
|
|
921
|
+
"control. A headless session can never satisfy it, so in "
|
|
922
|
+
"practice the call never works headless; a GUI session "
|
|
923
|
+
"satisfies it only sometimes. "
|
|
924
|
+
"Project.ExportCurrentFrameAsStill worked in all four runs in "
|
|
925
|
+
"both modes.",
|
|
926
|
+
"recommended": "Do not use ExportStills unattended in either mode — a "
|
|
927
|
+
"GUI session is not sufficient, only a GUI session with "
|
|
928
|
+
"the Gallery panel open. Use "
|
|
929
|
+
"Project.ExportCurrentFrameAsStill for pixels (verified in "
|
|
930
|
+
"both modes, four for four) or drp.extract_node_graphs for "
|
|
931
|
+
"grades. If ExportStills must be used, have the user open "
|
|
932
|
+
"Workspace > Gallery first and verify the written files "
|
|
933
|
+
"rather than trusting the return.",
|
|
934
|
+
"tags": ["gallery", "stills", "headless", "unreliable-return"],
|
|
935
|
+
"submit": "bug",
|
|
936
|
+
},
|
|
639
937
|
]
|
|
640
938
|
|
|
641
939
|
|