davinci-resolve-mcp 2.53.0 → 2.54.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 +49 -0
- package/README.md +6 -1
- package/docs/SKILL.md +9 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +208 -13
- package/src/utils/api_truth.py +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
|
|
4
4
|
|
|
5
|
+
## What's New in v2.54.1
|
|
6
|
+
|
|
7
|
+
One more instance of the enum-keyed silent-failure class (issue #70), plus a
|
|
8
|
+
guard so the next one can't ship unnoticed.
|
|
9
|
+
|
|
10
|
+
- **Fixed** the raw `timeline.export` action passed `type`/`subtype` straight to
|
|
11
|
+
`Timeline.Export`, which needs resolved `resolve.EXPORT_*` enum *values* — a
|
|
12
|
+
JSON/MCP caller can't pass a live enum, so the action silently wrote nothing
|
|
13
|
+
for every caller. It now resolves friendly format names (and `EXPORT_*`
|
|
14
|
+
constant names) via the same `_timeline_export_spec` resolver that
|
|
15
|
+
`export_timeline_checked` uses, and reports the resolved `export_type`/
|
|
16
|
+
`export_subtype`.
|
|
17
|
+
- **Fixed** `export_timeline_checked` resolved enum constants against the module
|
|
18
|
+
global `resolve` (which can be `None` and silently degrade the `EXPORT_*` args
|
|
19
|
+
to strings); it now uses `get_resolve()`, matching the issue-#70 lesson.
|
|
20
|
+
- **Added** an `api_truth` ↔ mitigation guard test: every `enum`-tagged catalog
|
|
21
|
+
entry must declare a `mitigation` (the resolver/wrapper functions), each of
|
|
22
|
+
which must exist in `src.server`. The next raw enum passthrough — a documented
|
|
23
|
+
symbol with no real resolver, or a renamed/removed resolver — now fails CI.
|
|
24
|
+
Added a `Timeline.Export` catalog entry and wired `mitigation` onto the
|
|
25
|
+
`AutoSyncAudio`, `CreateSubtitlesFromAudio`, and CloudProject entries.
|
|
26
|
+
|
|
27
|
+
## What's New in v2.54.0
|
|
28
|
+
|
|
29
|
+
Hardens the rest of the enum-keyed settings APIs against the same silent-failure
|
|
30
|
+
class fixed for `AutoSyncAudio` in v2.53.0 (issue #70). Several Resolve methods
|
|
31
|
+
key their settings dict by `resolve.<CONST>` enum attributes and silently reject
|
|
32
|
+
the whole call when handed plain string keys — returning `False`/`None` with
|
|
33
|
+
nothing applied and no error.
|
|
34
|
+
|
|
35
|
+
- **Fixed** `CreateSubtitlesFromAudio` (both `timeline_ai.create_subtitles` and
|
|
36
|
+
`timeline.subtitle_generation_probe`) now resolves human-readable
|
|
37
|
+
`autoCaptionSettings` — `language`, `preset`, `line_break`, `chars_per_line`,
|
|
38
|
+
`gap` — into live `SUBTITLE_*`/`AUTO_CAPTION_*` enum keys/values. Unknown keys
|
|
39
|
+
and unresolvable values are dropped and reported in `ignored_settings` instead
|
|
40
|
+
of poisoning the call, and generation is read-back verified against the
|
|
41
|
+
timeline's subtitle track count (the boolean return is unreliable).
|
|
42
|
+
- **Fixed** the `ProjectManager` CloudProject family (`create`/`load`/
|
|
43
|
+
`import_project`/`restore`) resolves `{cloudSettings}` into live
|
|
44
|
+
`CLOUD_SETTING_*`/`CLOUD_SYNC_*` enums (`project_name`, `media_path`,
|
|
45
|
+
`is_collab`, `sync_mode`, `is_camera_access`), dropping unknown keys into
|
|
46
|
+
`ignored_settings`.
|
|
47
|
+
- **Changed** the string→enum resolution is now a shared `_resolve_enum_settings`
|
|
48
|
+
primitive driven by per-field specs, so the next enum-keyed API gets the same
|
|
49
|
+
treatment without re-implementing it.
|
|
50
|
+
- **Added** `api_truth` entries for `Timeline.CreateSubtitlesFromAudio` and the
|
|
51
|
+
CloudProject family, documenting the silent-rejection + unreliable-return
|
|
52
|
+
behavior alongside the existing `AutoSyncAudio` entry.
|
|
53
|
+
|
|
5
54
|
## What's New in v2.53.0
|
|
6
55
|
|
|
7
56
|
Two improvements to audio sync reliability and inventory control.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
|
@@ -163,3 +163,8 @@ Samuel Gursky (samgursky@gmail.com)
|
|
|
163
163
|
- Blackmagic Design for DaVinci Resolve and its scripting API
|
|
164
164
|
- The Model Context Protocol team for enabling AI assistant integration
|
|
165
165
|
- Anthropic for Claude Code, used extensively in development and testing
|
|
166
|
+
|
|
167
|
+
### Community contributions
|
|
168
|
+
|
|
169
|
+
- [@rgxdev](https://github.com/rgxdev) — configurable Media Pool inventory walk
|
|
170
|
+
(exclude bins + inventory limit), [#69](https://github.com/samuelgursky/davinci-resolve-mcp/pull/69) (v2.53.0)
|
package/docs/SKILL.md
CHANGED
|
@@ -1005,7 +1005,12 @@ helpers:
|
|
|
1005
1005
|
Unrecognized keys are dropped and echoed back in `ignored_settings` rather than
|
|
1006
1006
|
silently failing the call.
|
|
1007
1007
|
- `transcription_capabilities(clip_ids?|selected?)`
|
|
1008
|
-
- `subtitle_generation_probe(settings?, allow_generate?)`
|
|
1008
|
+
- `subtitle_generation_probe(settings?, allow_generate?)` — `settings` accepts
|
|
1009
|
+
human-readable keys resolved to live `SUBTITLE_*`/`AUTO_CAPTION_*` enums:
|
|
1010
|
+
`language` (e.g. `english`, `korean`), `preset` (`default`|`teletext`|`netflix`),
|
|
1011
|
+
`line_break` (`single`|`double`), `chars_per_line` (1–60), `gap` (0–10).
|
|
1012
|
+
Unrecognized keys/values are dropped and echoed in `ignored_settings`; generation
|
|
1013
|
+
is read-back verified against the subtitle track count.
|
|
1009
1014
|
- `fairlight_boundary_report`
|
|
1010
1015
|
|
|
1011
1016
|
**`timeline_markers`** — Markers and playhead on the current timeline.
|
|
@@ -1036,7 +1041,9 @@ file to disk.
|
|
|
1036
1041
|
|
|
1037
1042
|
**`timeline_ai`** — AI/ML analysis on the current timeline.
|
|
1038
1043
|
|
|
1039
|
-
Key actions: `create_subtitles(settings?)
|
|
1044
|
+
Key actions: `create_subtitles(settings?)` (human-readable settings resolved to
|
|
1045
|
+
`SUBTITLE_*`/`AUTO_CAPTION_*` enums + read-back verified — see
|
|
1046
|
+
`subtitle_generation_probe`), `detect_scene_cuts`,
|
|
1040
1047
|
`grab_still`, `grab_all_stills(source?)`, `analyze_dolby_vision`
|
|
1041
1048
|
|
|
1042
1049
|
---
|
package/install.py
CHANGED
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.
|
|
38
|
+
VERSION = "2.54.1"
|
|
39
39
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
40
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
41
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.54.1"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.54.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -5042,7 +5042,10 @@ def _export_timeline_checked(tl, p: Dict[str, Any]):
|
|
|
5042
5042
|
folder = os.path.dirname(os.path.abspath(path))
|
|
5043
5043
|
if folder:
|
|
5044
5044
|
os.makedirs(folder, exist_ok=True)
|
|
5045
|
-
|
|
5045
|
+
# Resolve enum constants via get_resolve(), not the module global `resolve`
|
|
5046
|
+
# (which can be None and silently degrade the EXPORT_* args to strings — the
|
|
5047
|
+
# same failure class as issue #70).
|
|
5048
|
+
spec = _timeline_export_spec(p, get_resolve())
|
|
5046
5049
|
if p.get("dry_run"):
|
|
5047
5050
|
return _ok(path=path, would_export=True, spec={k: v for k, v in spec.items() if k != "export_type"})
|
|
5048
5051
|
with long_resolve_op("timeline.export_timeline_checked"):
|
|
@@ -6040,6 +6043,182 @@ def _normalize_auto_sync_settings(settings: Dict[str, Any], resolve_obj=None):
|
|
|
6040
6043
|
return normalized, ignored
|
|
6041
6044
|
|
|
6042
6045
|
|
|
6046
|
+
def _resolve_enum_settings(resolve_obj, settings, field_specs):
|
|
6047
|
+
"""Resolve a human-readable settings dict into the live enum-keyed dict that
|
|
6048
|
+
enum-keyed Resolve APIs require.
|
|
6049
|
+
|
|
6050
|
+
Several Resolve methods (``MediaPool.AutoSyncAudio``,
|
|
6051
|
+
``Timeline.CreateSubtitlesFromAudio``, the ``ProjectManager`` CloudProject
|
|
6052
|
+
family) key their settings dict by ``resolve.<CONST>`` enum attributes — and
|
|
6053
|
+
for many fields expect ``resolve.<CONST>`` enum *values* too. They silently
|
|
6054
|
+
reject the WHOLE call when handed a plain string key (returning False/None
|
|
6055
|
+
with nothing applied), so unrecognized keys must be dropped, not forwarded.
|
|
6056
|
+
See utils/api_truth.py for the catalogued symbols.
|
|
6057
|
+
|
|
6058
|
+
``field_specs`` is an iterable of dicts, one per supported field:
|
|
6059
|
+
``aliases`` human keys accepted for this field (case-insensitive)
|
|
6060
|
+
``key_const`` name of the ``resolve`` enum attribute used as the dict key
|
|
6061
|
+
``key_fallback`` key used when the live handle can't resolve ``key_const``
|
|
6062
|
+
(defaults to ``key_const``); keeps offline behavior readable
|
|
6063
|
+
``values`` optional ``{lowercased_str: enum_const_name}`` value map
|
|
6064
|
+
``coerce`` optional ``"int"`` | ``"bool"`` cast for plain values
|
|
6065
|
+
``bounds`` optional ``(min, max)`` clamp applied after int coercion
|
|
6066
|
+
|
|
6067
|
+
Returns ``(normalized, ignored)``. ``ignored`` lists input keys that no spec
|
|
6068
|
+
claimed AND recognized keys whose value could not be resolved (sorted) — so a
|
|
6069
|
+
field that silently fails to apply is reported rather than hidden.
|
|
6070
|
+
"""
|
|
6071
|
+
if not settings:
|
|
6072
|
+
return {}, []
|
|
6073
|
+
normalized = {}
|
|
6074
|
+
consumed = set()
|
|
6075
|
+
lower = {str(k).strip().lower(): k for k in settings}
|
|
6076
|
+
|
|
6077
|
+
for spec in field_specs:
|
|
6078
|
+
src_key = next((lower[a.lower()] for a in spec["aliases"] if a.lower() in lower), None)
|
|
6079
|
+
if src_key is None:
|
|
6080
|
+
continue
|
|
6081
|
+
raw = settings[src_key]
|
|
6082
|
+
key = _resolve_audio_constant(resolve_obj, spec["key_const"], spec.get("key_fallback", spec["key_const"]))
|
|
6083
|
+
values_map = spec.get("values")
|
|
6084
|
+
if values_map is not None and isinstance(raw, str):
|
|
6085
|
+
const_name = values_map.get(raw.strip().lower())
|
|
6086
|
+
if const_name is None:
|
|
6087
|
+
continue # unknown enum value — leave src_key unconsumed -> ignored
|
|
6088
|
+
value = _resolve_audio_constant(resolve_obj, const_name, raw)
|
|
6089
|
+
elif spec.get("coerce") == "int":
|
|
6090
|
+
try:
|
|
6091
|
+
value = int(raw) if not isinstance(raw, bool) else int(raw)
|
|
6092
|
+
except (TypeError, ValueError):
|
|
6093
|
+
continue
|
|
6094
|
+
lo, hi = spec.get("bounds", (None, None))
|
|
6095
|
+
if lo is not None:
|
|
6096
|
+
value = max(lo, value)
|
|
6097
|
+
if hi is not None:
|
|
6098
|
+
value = min(hi, value)
|
|
6099
|
+
elif spec.get("coerce") == "bool":
|
|
6100
|
+
value = bool(raw)
|
|
6101
|
+
else:
|
|
6102
|
+
value = raw
|
|
6103
|
+
normalized[key] = value
|
|
6104
|
+
consumed.add(src_key)
|
|
6105
|
+
|
|
6106
|
+
ignored = sorted(str(key) for key in settings if key not in consumed)
|
|
6107
|
+
return normalized, ignored
|
|
6108
|
+
|
|
6109
|
+
|
|
6110
|
+
# Timeline.CreateSubtitlesFromAudio({autoCaptionSettings}) — enum-keyed exactly
|
|
6111
|
+
# like AutoSyncAudio (docs/reference/resolve_scripting_api.txt lines 733-771).
|
|
6112
|
+
_AUTO_CAPTION_LANGUAGES = {
|
|
6113
|
+
"auto": "AUTO_CAPTION_AUTO",
|
|
6114
|
+
"danish": "AUTO_CAPTION_DANISH",
|
|
6115
|
+
"dutch": "AUTO_CAPTION_DUTCH",
|
|
6116
|
+
"english": "AUTO_CAPTION_ENGLISH",
|
|
6117
|
+
"french": "AUTO_CAPTION_FRENCH",
|
|
6118
|
+
"german": "AUTO_CAPTION_GERMAN",
|
|
6119
|
+
"italian": "AUTO_CAPTION_ITALIAN",
|
|
6120
|
+
"japanese": "AUTO_CAPTION_JAPANESE",
|
|
6121
|
+
"korean": "AUTO_CAPTION_KOREAN",
|
|
6122
|
+
"mandarin_simplified": "AUTO_CAPTION_MANDARIN_SIMPLIFIED",
|
|
6123
|
+
"mandarin_traditional": "AUTO_CAPTION_MANDARIN_TRADITIONAL",
|
|
6124
|
+
"norwegian": "AUTO_CAPTION_NORWEGIAN",
|
|
6125
|
+
"portuguese": "AUTO_CAPTION_PORTUGUESE",
|
|
6126
|
+
"russian": "AUTO_CAPTION_RUSSIAN",
|
|
6127
|
+
"spanish": "AUTO_CAPTION_SPANISH",
|
|
6128
|
+
"swedish": "AUTO_CAPTION_SWEDISH",
|
|
6129
|
+
}
|
|
6130
|
+
_AUTO_CAPTION_PRESETS = {
|
|
6131
|
+
"default": "AUTO_CAPTION_SUBTITLE_DEFAULT",
|
|
6132
|
+
"subtitle_default": "AUTO_CAPTION_SUBTITLE_DEFAULT",
|
|
6133
|
+
"teletext": "AUTO_CAPTION_TELETEXT",
|
|
6134
|
+
"netflix": "AUTO_CAPTION_NETFLIX",
|
|
6135
|
+
}
|
|
6136
|
+
_AUTO_CAPTION_LINE_BREAKS = {
|
|
6137
|
+
"single": "AUTO_CAPTION_LINE_SINGLE",
|
|
6138
|
+
"double": "AUTO_CAPTION_LINE_DOUBLE",
|
|
6139
|
+
}
|
|
6140
|
+
_AUTO_CAPTION_FIELD_SPECS = (
|
|
6141
|
+
{"aliases": ["language", "lang", "SUBTITLE_LANGUAGE"], "key_const": "SUBTITLE_LANGUAGE", "key_fallback": "language", "values": _AUTO_CAPTION_LANGUAGES},
|
|
6142
|
+
{"aliases": ["preset", "caption_preset", "SUBTITLE_CAPTION_PRESET"], "key_const": "SUBTITLE_CAPTION_PRESET", "key_fallback": "captionPreset", "values": _AUTO_CAPTION_PRESETS},
|
|
6143
|
+
{"aliases": ["line_break", "linebreak", "SUBTITLE_LINE_BREAK"], "key_const": "SUBTITLE_LINE_BREAK", "key_fallback": "lineBreak", "values": _AUTO_CAPTION_LINE_BREAKS},
|
|
6144
|
+
{"aliases": ["chars_per_line", "charsperline", "SUBTITLE_CHARS_PER_LINE"], "key_const": "SUBTITLE_CHARS_PER_LINE", "key_fallback": "charsPerLine", "coerce": "int", "bounds": (1, 60)},
|
|
6145
|
+
{"aliases": ["gap", "SUBTITLE_GAP"], "key_const": "SUBTITLE_GAP", "key_fallback": "gap", "coerce": "int", "bounds": (0, 10)},
|
|
6146
|
+
)
|
|
6147
|
+
|
|
6148
|
+
|
|
6149
|
+
def _normalize_auto_caption_settings(settings, resolve_obj=None):
|
|
6150
|
+
"""Translate human-readable subtitle settings (e.g. ``{"language": "korean"}``)
|
|
6151
|
+
into live ``SUBTITLE_*``/``AUTO_CAPTION_*`` enum keys/values for
|
|
6152
|
+
``CreateSubtitlesFromAudio``. Returns ``(normalized, ignored)``."""
|
|
6153
|
+
return _resolve_enum_settings(resolve_obj, dict(settings or {}), _AUTO_CAPTION_FIELD_SPECS)
|
|
6154
|
+
|
|
6155
|
+
|
|
6156
|
+
# ProjectManager CloudProject family — {cloudSettings} is keyed by
|
|
6157
|
+
# resolve.CLOUD_SETTING_* constants with resolve.CLOUD_SYNC_* sync-mode values
|
|
6158
|
+
# (docs/reference/resolve_scripting_api.txt lines 588-603). Same silent-rejection
|
|
6159
|
+
# failure mode as AutoSyncAudio when handed plain string keys.
|
|
6160
|
+
_CLOUD_SYNC_MODES = {
|
|
6161
|
+
"none": "CLOUD_SYNC_NONE",
|
|
6162
|
+
"proxy_only": "CLOUD_SYNC_PROXY_ONLY",
|
|
6163
|
+
"proxy": "CLOUD_SYNC_PROXY_ONLY",
|
|
6164
|
+
"proxy_and_orig": "CLOUD_SYNC_PROXY_AND_ORIG",
|
|
6165
|
+
"proxy_and_original": "CLOUD_SYNC_PROXY_AND_ORIG",
|
|
6166
|
+
}
|
|
6167
|
+
_CLOUD_SETTINGS_FIELD_SPECS = (
|
|
6168
|
+
{"aliases": ["project_name", "name", "CLOUD_SETTING_PROJECT_NAME"], "key_const": "CLOUD_SETTING_PROJECT_NAME", "key_fallback": "projectName"},
|
|
6169
|
+
{"aliases": ["project_media_path", "media_path", "CLOUD_SETTING_PROJECT_MEDIA_PATH"], "key_const": "CLOUD_SETTING_PROJECT_MEDIA_PATH", "key_fallback": "projectMediaPath"},
|
|
6170
|
+
{"aliases": ["is_collab", "collab", "CLOUD_SETTING_IS_COLLAB"], "key_const": "CLOUD_SETTING_IS_COLLAB", "key_fallback": "isCollab", "coerce": "bool"},
|
|
6171
|
+
{"aliases": ["sync_mode", "syncmode", "CLOUD_SETTING_SYNC_MODE"], "key_const": "CLOUD_SETTING_SYNC_MODE", "key_fallback": "syncMode", "values": _CLOUD_SYNC_MODES},
|
|
6172
|
+
{"aliases": ["is_camera_access", "camera_access", "CLOUD_SETTING_IS_CAMERA_ACCESS"], "key_const": "CLOUD_SETTING_IS_CAMERA_ACCESS", "key_fallback": "isCameraAccess", "coerce": "bool"},
|
|
6173
|
+
)
|
|
6174
|
+
|
|
6175
|
+
|
|
6176
|
+
def _normalize_cloud_settings(settings, resolve_obj=None):
|
|
6177
|
+
"""Translate human-readable cloud-project settings (e.g.
|
|
6178
|
+
``{"project_name": "X", "sync_mode": "proxy_only"}``) into live
|
|
6179
|
+
``CLOUD_SETTING_*``/``CLOUD_SYNC_*`` enum keys/values. Returns
|
|
6180
|
+
``(normalized, ignored)``."""
|
|
6181
|
+
return _resolve_enum_settings(resolve_obj, dict(settings or {}), _CLOUD_SETTINGS_FIELD_SPECS)
|
|
6182
|
+
|
|
6183
|
+
|
|
6184
|
+
def _safe_create_subtitles(tl, p: Dict[str, Any]):
|
|
6185
|
+
"""CreateSubtitlesFromAudio with enum-resolved settings + readback.
|
|
6186
|
+
|
|
6187
|
+
The raw boolean is unreliable (like AutoSyncAudio), so we verify by reading
|
|
6188
|
+
the timeline's subtitle track count before/after and report the real delta.
|
|
6189
|
+
"""
|
|
6190
|
+
settings, ignored = _normalize_auto_caption_settings(p.get("settings"), get_resolve())
|
|
6191
|
+
if p.get("dry_run", False):
|
|
6192
|
+
return _ok(would_create_subtitles=True, settings=settings, ignored_settings=ignored)
|
|
6193
|
+
|
|
6194
|
+
def _subtitle_track_count():
|
|
6195
|
+
try:
|
|
6196
|
+
return int(tl.GetTrackCount("subtitle") or 0)
|
|
6197
|
+
except Exception:
|
|
6198
|
+
return 0
|
|
6199
|
+
|
|
6200
|
+
res = verify_by_readback(
|
|
6201
|
+
mutate=lambda: tl.CreateSubtitlesFromAudio(settings),
|
|
6202
|
+
observe=_subtitle_track_count,
|
|
6203
|
+
snapshot=_subtitle_track_count,
|
|
6204
|
+
compare=lambda before, after: {
|
|
6205
|
+
"verified": bool((after or 0) > (before or 0)),
|
|
6206
|
+
"subtitle_tracks_before": before,
|
|
6207
|
+
"subtitle_tracks_after": after,
|
|
6208
|
+
},
|
|
6209
|
+
label="create_subtitles_from_audio",
|
|
6210
|
+
intent={"settings_keys": sorted(settings.keys()) if settings else []},
|
|
6211
|
+
)
|
|
6212
|
+
return {
|
|
6213
|
+
"success": res["success_raw"],
|
|
6214
|
+
"verified": res["verified"],
|
|
6215
|
+
"subtitle_tracks_before": res.get("subtitle_tracks_before"),
|
|
6216
|
+
"subtitle_tracks_after": res.get("subtitle_tracks_after"),
|
|
6217
|
+
"settings": settings,
|
|
6218
|
+
"ignored_settings": ignored,
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
|
|
6043
6222
|
def _transcription_capabilities(mp, p: Dict[str, Any]):
|
|
6044
6223
|
root = mp.GetRootFolder()
|
|
6045
6224
|
clips = []
|
|
@@ -6085,13 +6264,14 @@ def _transcription_capabilities(mp, p: Dict[str, Any]):
|
|
|
6085
6264
|
|
|
6086
6265
|
|
|
6087
6266
|
def _subtitle_generation_probe(tl, p: Dict[str, Any]):
|
|
6088
|
-
settings =
|
|
6267
|
+
settings, ignored = _normalize_auto_caption_settings(p.get("settings"), get_resolve())
|
|
6089
6268
|
if not p.get("allow_generate", False):
|
|
6090
|
-
return _ok(would_generate=True, settings=settings,
|
|
6269
|
+
return _ok(would_generate=True, settings=settings, ignored_settings=ignored,
|
|
6270
|
+
note="Pass allow_generate=True to call CreateSubtitlesFromAudio.")
|
|
6091
6271
|
if not _has_method(tl, "CreateSubtitlesFromAudio"):
|
|
6092
6272
|
return _err("CreateSubtitlesFromAudio unavailable")
|
|
6093
6273
|
with long_resolve_op("timeline.create_subtitles_from_audio"):
|
|
6094
|
-
return
|
|
6274
|
+
return _safe_create_subtitles(tl, p)
|
|
6095
6275
|
|
|
6096
6276
|
|
|
6097
6277
|
def _fairlight_boundary_report(proj, mp, tl, p: Dict[str, Any]):
|
|
@@ -13711,16 +13891,21 @@ def project_manager_cloud(action: str, params: Optional[Dict[str, Any]] = None)
|
|
|
13711
13891
|
return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
|
|
13712
13892
|
pm = r.GetProjectManager()
|
|
13713
13893
|
|
|
13894
|
+
# cloudSettings is enum-keyed (CLOUD_SETTING_*/CLOUD_SYNC_*); resolve string
|
|
13895
|
+
# keys against the live handle so human-readable settings aren't silently
|
|
13896
|
+
# rejected. Unrecognized keys are dropped and reported in ignored_settings.
|
|
13897
|
+
settings, ignored = _normalize_cloud_settings(p.get("settings"), r)
|
|
13898
|
+
|
|
13714
13899
|
if action == "create":
|
|
13715
|
-
proj = pm.CreateCloudProject(
|
|
13716
|
-
return _ok(name=proj.GetName()) if proj else _err("Failed to create cloud project")
|
|
13900
|
+
proj = pm.CreateCloudProject(settings)
|
|
13901
|
+
return _ok(name=proj.GetName(), ignored_settings=ignored) if proj else _err("Failed to create cloud project")
|
|
13717
13902
|
elif action == "load":
|
|
13718
|
-
proj = pm.LoadCloudProject(
|
|
13719
|
-
return _ok(name=proj.GetName()) if proj else _err("Failed to load cloud project")
|
|
13903
|
+
proj = pm.LoadCloudProject(settings)
|
|
13904
|
+
return _ok(name=proj.GetName(), ignored_settings=ignored) if proj else _err("Failed to load cloud project")
|
|
13720
13905
|
elif action == "import_project":
|
|
13721
|
-
return {"success": bool(pm.ImportCloudProject(p["path"],
|
|
13906
|
+
return {"success": bool(pm.ImportCloudProject(p["path"], settings)), "ignored_settings": ignored}
|
|
13722
13907
|
elif action == "restore":
|
|
13723
|
-
return {"success": bool(pm.RestoreCloudProject(p["folder_path"],
|
|
13908
|
+
return {"success": bool(pm.RestoreCloudProject(p["folder_path"], settings)), "ignored_settings": ignored}
|
|
13724
13909
|
return _unknown(action, ["create","load","import_project","restore"])
|
|
13725
13910
|
|
|
13726
13911
|
|
|
@@ -17669,8 +17854,18 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
17669
17854
|
elif action == "import_into_timeline":
|
|
17670
17855
|
return {"success": bool(tl.ImportIntoTimeline(p["path"], p.get("options", {})))}
|
|
17671
17856
|
elif action == "export":
|
|
17857
|
+
# Timeline.Export needs resolved resolve.EXPORT_* enum *values*, which a
|
|
17858
|
+
# JSON/MCP caller cannot pass — handing it a string silently fails. Resolve
|
|
17859
|
+
# friendly type/subtype names (or EXPORT_* constant names) the same way
|
|
17860
|
+
# export_timeline_checked does. This raw action keeps no path sandbox.
|
|
17861
|
+
spec = _timeline_export_spec(p, get_resolve())
|
|
17672
17862
|
with long_resolve_op("timeline.export"):
|
|
17673
|
-
|
|
17863
|
+
success = bool(tl.Export(p["path"], spec["export_type"], spec["export_subtype"]))
|
|
17864
|
+
return {
|
|
17865
|
+
"success": success,
|
|
17866
|
+
"export_type": spec["export_type_name"],
|
|
17867
|
+
"export_subtype": spec["export_subtype_name"],
|
|
17868
|
+
}
|
|
17674
17869
|
elif action == "get_setting":
|
|
17675
17870
|
return {"settings": _ser(tl.GetSetting(p.get("name", "")))}
|
|
17676
17871
|
elif action == "set_setting":
|
|
@@ -18118,7 +18313,7 @@ def timeline_ai(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
18118
18313
|
|
|
18119
18314
|
if action == "create_subtitles":
|
|
18120
18315
|
with long_resolve_op("timeline_ai.create_subtitles"):
|
|
18121
|
-
return
|
|
18316
|
+
return _safe_create_subtitles(tl, p)
|
|
18122
18317
|
elif action == "detect_scene_cuts":
|
|
18123
18318
|
with long_resolve_op("timeline_ai.detect_scene_cuts"):
|
|
18124
18319
|
return {"success": bool(tl.DetectSceneCuts())}
|
package/src/utils/api_truth.py
CHANGED
|
@@ -28,6 +28,54 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
28
28
|
"resolve handle, and verify by reading each clip's "
|
|
29
29
|
"'Synced Audio' property (see verify_by_readback).",
|
|
30
30
|
"tags": ["unreliable-return", "silent-failure", "audio", "enum"],
|
|
31
|
+
"mitigation": ["_normalize_auto_sync_settings", "_safe_auto_sync_audio"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"symbol": "Timeline.CreateSubtitlesFromAudio",
|
|
35
|
+
"object": "Timeline",
|
|
36
|
+
"signature": "(autoCaptionSettings) -> bool",
|
|
37
|
+
"reality": "Same failure mode as AutoSyncAudio: the autoCaptionSettings "
|
|
38
|
+
"dict is keyed by resolve.SUBTITLE_* enum constants with "
|
|
39
|
+
"resolve.AUTO_CAPTION_* enum values, so plain string keys like "
|
|
40
|
+
"{'language': 'korean'} are silently rejected (returns False, "
|
|
41
|
+
"no subtitle track created). The boolean is also unreliable.",
|
|
42
|
+
"recommended": "Resolve the SUBTITLE_*/AUTO_CAPTION_* constants via the "
|
|
43
|
+
"live resolve handle (server._normalize_auto_caption_settings) "
|
|
44
|
+
"and verify by reading the timeline's subtitle track count "
|
|
45
|
+
"before/after (server._safe_create_subtitles).",
|
|
46
|
+
"tags": ["unreliable-return", "silent-failure", "subtitle", "enum"],
|
|
47
|
+
"mitigation": ["_normalize_auto_caption_settings", "_safe_create_subtitles"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"symbol": "ProjectManager CloudProject family (Create/Load/Import/RestoreCloudProject)",
|
|
51
|
+
"object": "ProjectManager",
|
|
52
|
+
"signature": "(..., cloudSettings) -> Project | bool",
|
|
53
|
+
"reality": "All four take an enum-keyed {cloudSettings} dict "
|
|
54
|
+
"(resolve.CLOUD_SETTING_* keys, resolve.CLOUD_SYNC_* sync-mode "
|
|
55
|
+
"values). Plain string keys are silently rejected, so a settings "
|
|
56
|
+
"dict built from human-readable keys yields no project / False.",
|
|
57
|
+
"recommended": "Resolve the CLOUD_SETTING_*/CLOUD_SYNC_* constants via the "
|
|
58
|
+
"live resolve handle (server._normalize_cloud_settings) "
|
|
59
|
+
"before calling, and treat the bool return from "
|
|
60
|
+
"Import/RestoreCloudProject as advisory.",
|
|
61
|
+
"tags": ["silent-failure", "project", "cloud", "enum"],
|
|
62
|
+
"mitigation": ["_normalize_cloud_settings"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"symbol": "Timeline.Export",
|
|
66
|
+
"object": "Timeline",
|
|
67
|
+
"signature": "(fileName, exportType, exportSubtype) -> bool",
|
|
68
|
+
"reality": "exportType/exportSubtype must be resolve.EXPORT_* enum *values* "
|
|
69
|
+
"resolved from the live handle. A JSON/MCP caller cannot pass a "
|
|
70
|
+
"live enum, and a plain string ('fcpxml', or even the constant "
|
|
71
|
+
"name 'EXPORT_FCPXML_1_10') is silently rejected with no file "
|
|
72
|
+
"written.",
|
|
73
|
+
"recommended": "Map a friendly format/subtype to the EXPORT_* constant and "
|
|
74
|
+
"resolve it against the live handle "
|
|
75
|
+
"(server._timeline_export_spec) before calling; verify the "
|
|
76
|
+
"output file exists afterward.",
|
|
77
|
+
"tags": ["silent-failure", "timeline", "export", "enum"],
|
|
78
|
+
"mitigation": ["_timeline_export_spec", "_timeline_export_value"],
|
|
31
79
|
},
|
|
32
80
|
{
|
|
33
81
|
"symbol": "Composition.Paste",
|