davinci-resolve-mcp 2.28.1 → 2.29.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 +43 -0
- package/README.md +5 -5
- package/docs/SKILL.md +9 -3
- package/docs/contributing.md +1 -1
- package/docs/install.md +2 -2
- package/docs/reference/api-coverage.md +2 -2
- package/docs/reference/resolve_scripting_api.txt +15 -3
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +40 -0
- package/src/granular/common.py +1 -1
- package/src/granular/folder.py +124 -0
- package/src/granular/media_pool_item.py +109 -0
- package/src/granular/project.py +56 -0
- package/src/granular/resolve_control.py +17 -0
- package/src/resolve_mcp_server.py +1 -1
- package/src/server.py +186 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
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.29.0
|
|
6
|
+
|
|
7
|
+
Adds the **DaVinci Resolve 21.0** scripting-API additions. Every new method is
|
|
8
|
+
runtime-detected (`_requires_method`/capability flags), so the tools stay inert
|
|
9
|
+
on older Resolve builds and activate automatically on Resolve 21+.
|
|
10
|
+
|
|
11
|
+
**New AI analysis actions** on the `folder` and `media_pool_item` compound tools
|
|
12
|
+
(and mirrored as granular `--full` tools):
|
|
13
|
+
|
|
14
|
+
- `perform_audio_classification` / `clear_audio_classification` — classify clip
|
|
15
|
+
audio into categories and subcategories.
|
|
16
|
+
- `analyze_for_intellisearch(identify_faces?, is_better_mode?)` — IntelliSearch
|
|
17
|
+
analysis with optional face identification. Requires the *AI IntelliSearch* Extra.
|
|
18
|
+
- `analyze_for_slate(marker_color?)` — slate/clapboard detection that drops a
|
|
19
|
+
marker of the chosen color (validated against the 16 Resolve marker colors).
|
|
20
|
+
Requires the *AI Slate ID* Extra.
|
|
21
|
+
- `remove_motion_blur(deblur_option?)` — renders motion-deblurred copies. This
|
|
22
|
+
**creates new media files** (source media is never modified) and is therefore
|
|
23
|
+
**confirm-token gated**: the first call returns a preview + token, the second
|
|
24
|
+
call (with the token) runs.
|
|
25
|
+
|
|
26
|
+
**Speaker-detection transcription.** `transcribe_audio` now accepts an optional
|
|
27
|
+
`use_speaker_detection` boolean (Resolve 21+); omit it to use the project's
|
|
28
|
+
Speech Recognition setting.
|
|
29
|
+
|
|
30
|
+
**Speech generation.** `project_settings(action="generate_speech", ...)` wraps
|
|
31
|
+
`Project.GenerateSpeech` (AI text-to-speech). It creates a new audio item and
|
|
32
|
+
optionally places it on the timeline, so it is also confirm-token gated.
|
|
33
|
+
Requires the *AI Speech Generator* Extra. Granular `--full` tool: `generate_speech`.
|
|
34
|
+
|
|
35
|
+
**Session control.** `resolve_control(action="disable_background_tasks_for_current_session")`
|
|
36
|
+
wraps `Resolve.DisableBackgroundTasksForCurrentResolveSession()` to quiet
|
|
37
|
+
background work during heavy scripted runs.
|
|
38
|
+
|
|
39
|
+
**Capability surface.** The `media_analysis` transcription-capability report and
|
|
40
|
+
the control panel's boot payload (`resolve.ai_features`) now list which 21.0 AI
|
|
41
|
+
methods are available and which Extras each gated method needs.
|
|
42
|
+
|
|
43
|
+
Notes: these are Resolve-local GPU/AI operations and do not consume the
|
|
44
|
+
Claude-side analysis token budget, so they are not metered by the analysis-caps
|
|
45
|
+
layer; the derivative-creating ones are protected by the confirm-token gate
|
|
46
|
+
instead. The granular `--full` server grew from 329 to 341 tools.
|
|
47
|
+
|
|
5
48
|
## What's New in v2.28.1
|
|
6
49
|
|
|
7
50
|
Bug-fix release.
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
|
-
[-blue.svg)](#server-modes)
|
|
7
7
|
[](docs/reference/api-coverage.md#test-results)
|
|
8
8
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
9
9
|
[](https://www.python.org/downloads/)
|
|
@@ -50,7 +50,7 @@ The command starts a localhost server and opens the control panel in your browse
|
|
|
50
50
|
| Mode | Entry point | Tools | Best for |
|
|
51
51
|
|------|-------------|-------|----------|
|
|
52
52
|
| Compound | `src/server.py` | 32 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
|
|
53
|
-
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` |
|
|
53
|
+
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 341 | Power users who want one MCP tool per Resolve API method. |
|
|
54
54
|
|
|
55
55
|
The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
|
|
56
56
|
|
|
@@ -99,7 +99,7 @@ The default server is a local stdio process launched by your MCP client; it does
|
|
|
99
99
|
|
|
100
100
|
| Metric | Value |
|
|
101
101
|
|--------|-------|
|
|
102
|
-
| MCP Tools | **32** compound / **
|
|
102
|
+
| MCP Tools | **32** compound / **341** granular |
|
|
103
103
|
| Kernel Actions | **136** guarded workflow actions across 9 compound tools |
|
|
104
104
|
| API Methods Covered | **336/336** (100%) |
|
|
105
105
|
| Methods Live Tested | **331/336** (98.5%) |
|
|
@@ -136,7 +136,7 @@ Extension authoring references live in [docs/authoring](docs/authoring/). Resolv
|
|
|
136
136
|
- Python 3.10+ (3.10-3.12 is the lowest-risk range). Python 3.13/3.14 also work on recent Resolve builds (verified on Studio 20.3.2); older builds may fail to connect on 3.13+, in which case use 3.10-3.12.
|
|
137
137
|
- Resolve external scripting set to **Local**.
|
|
138
138
|
|
|
139
|
-
Resolve 19.1.3 remains the compatibility baseline. Resolve 20.x scripting calls are additive, version-guarded, and live-tested on 20.3.2. Resolve 21
|
|
139
|
+
Resolve 19.1.3 remains the compatibility baseline. Resolve 20.x scripting calls are additive, version-guarded, and live-tested on 20.3.2. Resolve 21.0 scripting additions (audio classification, speaker-detection transcription, IntelliSearch, slate analysis, motion-deblur, speech generation, session background-task control) are exposed behind runtime capability detection, so they stay inert on older builds and activate automatically on Resolve 21+.
|
|
140
140
|
|
|
141
141
|
## Development
|
|
142
142
|
|
package/docs/SKILL.md
CHANGED
|
@@ -101,7 +101,7 @@ before mutating Resolve state.
|
|
|
101
101
|
| Mode | Entry point | Tool count | Use when |
|
|
102
102
|
|---|---|---|---|
|
|
103
103
|
| Compound (default) | `src/server.py` | 32 tools | Most workflows — keeps context lean |
|
|
104
|
-
| Granular (full) | `src/server.py --full` |
|
|
104
|
+
| Granular (full) | `src/server.py --full` | 341 tools | Power users needing one tool per API method |
|
|
105
105
|
|
|
106
106
|
This skill document covers the **compound server** (the default). Each compound
|
|
107
107
|
tool accepts an `action` string and an optional `params` object.
|
|
@@ -442,7 +442,10 @@ Note: `folder path` arguments use slash notation like `"Master/SubFolder"`.
|
|
|
442
442
|
**`folder`** — Operations on a specific Media Pool folder.
|
|
443
443
|
|
|
444
444
|
Key actions: `get_clips(path?)`, `get_subfolders(path?)`, `export(path?, export_path)`,
|
|
445
|
-
`transcribe_audio(path?)`, `clear_transcription(path?)
|
|
445
|
+
`transcribe_audio(path?, use_speaker_detection?)`, `clear_transcription(path?)`,
|
|
446
|
+
`perform_audio_classification(path?)`, `analyze_for_intellisearch(path?, identify_faces?, is_better_mode?)`,
|
|
447
|
+
`analyze_for_slate(path?, marker_color?)`, `remove_motion_blur(path?, deblur_option?)` (Resolve 21+;
|
|
448
|
+
the last three need AI Extras, and `remove_motion_blur` is confirm-token gated)
|
|
446
449
|
|
|
447
450
|
**`media_pool_item`** — Read/write clip metadata and properties. All actions
|
|
448
451
|
require a `clip_id` (the UUID returned by `GetUniqueId()`).
|
|
@@ -452,7 +455,10 @@ Key actions: `get_name`, `get_metadata(key?)`, `set_metadata(key, value)`,
|
|
|
452
455
|
`set_clip_color(color)`, `link_proxy(proxy_path)`, `replace_clip(path)`,
|
|
453
456
|
`set_name(name)`, `link_full_resolution_media(path)`,
|
|
454
457
|
`replace_clip_preserve_sub_clip(path)`, `monitor_growing_file`,
|
|
455
|
-
`transcribe_audio`, `
|
|
458
|
+
`transcribe_audio(use_speaker_detection?)`, `perform_audio_classification`,
|
|
459
|
+
`analyze_for_intellisearch(identify_faces?, is_better_mode?)`, `analyze_for_slate(marker_color?)`,
|
|
460
|
+
`remove_motion_blur(deblur_option?)` (Resolve 21+; AI Extras / confirm-token gated as noted above),
|
|
461
|
+
`get_audio_mapping`, `get_mark_in_out`, `set_mark_in_out`
|
|
456
462
|
|
|
457
463
|
**`media_pool_item_markers`** — Markers and flags on clips in the Media Pool.
|
|
458
464
|
All actions require a `clip_id`.
|
package/docs/contributing.md
CHANGED
|
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
|
|
|
64
64
|
├── install.py # Universal installer (macOS/Windows/Linux)
|
|
65
65
|
├── src/
|
|
66
66
|
│ ├── server.py # Compound MCP server — 32 tools (default)
|
|
67
|
-
│ ├── resolve_mcp_server.py # Thin full-server entrypoint —
|
|
67
|
+
│ ├── resolve_mcp_server.py # Thin full-server entrypoint — 341 tools
|
|
68
68
|
│ ├── granular/ # Modular full-server implementation
|
|
69
69
|
│ └── utils/ # Platform detection, Resolve connection helpers
|
|
70
70
|
├── tests/ # 5-phase live API test suite + Resolve 20 delta (331/331 pass)
|
package/docs/install.md
CHANGED
|
@@ -87,7 +87,7 @@ The MCP server comes in two modes:
|
|
|
87
87
|
| Mode | File | Tools | Best For |
|
|
88
88
|
|------|------|-------|----------|
|
|
89
89
|
| **Compound** (default) | `src/server.py` | 32 | Most users — fast, clean, low context usage |
|
|
90
|
-
| **Full** | `src/resolve_mcp_server.py` |
|
|
90
|
+
| **Full** | `src/resolve_mcp_server.py` | 341 | Power users who want one tool per API method |
|
|
91
91
|
|
|
92
92
|
The compound server's `timeline_item` tool includes dedicated actions for common workflows:
|
|
93
93
|
|
|
@@ -102,7 +102,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
|
|
|
102
102
|
|
|
103
103
|
The installer uses the compound server by default. To use the full server:
|
|
104
104
|
```bash
|
|
105
|
-
python src/server.py --full # Launch full
|
|
105
|
+
python src/server.py --full # Launch full 341-tool server
|
|
106
106
|
# Or point your MCP config directly at src/resolve_mcp_server.py
|
|
107
107
|
```
|
|
108
108
|
|
|
@@ -6,7 +6,7 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
|
|
|
6
6
|
|
|
7
7
|
| Metric | Value |
|
|
8
8
|
|--------|-------|
|
|
9
|
-
| MCP Tools | **33** compound (default) / **
|
|
9
|
+
| MCP Tools | **33** compound (default) / **341** granular |
|
|
10
10
|
| Kernel Actions | **136** guarded MCP workflow actions across 9 compound tools |
|
|
11
11
|
| API Methods Covered | **336/336** (100%) |
|
|
12
12
|
| Methods Live Tested | **331/336** (98.5%) |
|
|
@@ -17,7 +17,7 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
|
|
|
17
17
|
|
|
18
18
|
## API Coverage
|
|
19
19
|
|
|
20
|
-
Every non-deprecated method in the DaVinci Resolve Scripting API is covered. The default compound server exposes **33 tools** that group related operations by action parameter, keeping LLM context windows lean. The full granular server provides **
|
|
20
|
+
Every non-deprecated method in the DaVinci Resolve Scripting API is covered. The default compound server exposes **33 tools** that group related operations by action parameter, keeping LLM context windows lean. The full granular server provides **341 individual tools** for power users. Both modes cover all 13 API object classes. MCP-level kernel actions are tracked separately in [Kernel Action Coverage](../kernels/README.md).
|
|
21
21
|
|
|
22
22
|
The 33rd compound tool is `timeline_versioning` (C6) — an MCP-level workflow
|
|
23
23
|
tool, not a wrapper around a Resolve API method. It surfaces the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Last Updated:
|
|
1
|
+
Last Updated: 5 May 2026
|
|
2
2
|
-------------------------
|
|
3
3
|
In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import
|
|
4
4
|
modules for scripting access (DaVinciResolve.py) and some representative examples.
|
|
@@ -105,6 +105,7 @@ Resolve
|
|
|
105
105
|
GetKeyframeMode() --> keyframeMode # Returns the currently set keyframe mode (int). Refer to section 'Keyframe Mode information' below for details.
|
|
106
106
|
SetKeyframeMode(keyframeMode) --> Bool # Returns True when 'keyframeMode'(enum) is successfully set. Refer to section 'Keyframe Mode information' below for details.
|
|
107
107
|
GetFairlightPresets() --> [presetNames...]. # Returns a list of Fairlight presets by name
|
|
108
|
+
DisableBackgroundTasksForCurrentResolveSession()--> None # Disables all background tasks for current Resolve session.
|
|
108
109
|
|
|
109
110
|
ProjectManager
|
|
110
111
|
ArchiveProject(projectName,
|
|
@@ -199,6 +200,7 @@ Project
|
|
|
199
200
|
AddColorGroup(groupName) --> ColorGroup # Creates a new ColorGroup. groupName must be a unique string.
|
|
200
201
|
DeleteColorGroup(colorGroup) --> Bool # Deletes the given color group and sets clips to ungrouped.
|
|
201
202
|
ApplyFairlightPresetToCurrentTimeline(name) --> Bool # Apply Fairlight Preset of given name to the current timeline, returns True if successful, False otherwise.
|
|
203
|
+
GenerateSpeech({speechGenerationSettings}, timecode) --> MediaPoolItem # Generates an audio MediaPoolItem based on the given speechGenerationSettings and adds it to the timeline at the stated timecode if "AddToTimeline" is True. Returns the newly generated MediaPoolItem.
|
|
202
204
|
|
|
203
205
|
MediaStorage
|
|
204
206
|
GetMountedVolumeList() --> [paths...] # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage.
|
|
@@ -263,8 +265,13 @@ Folder
|
|
|
263
265
|
GetIsFolderStale() --> bool # Returns true if folder is stale in collaboration mode, false otherwise
|
|
264
266
|
GetUniqueId() --> string # Returns a unique ID for the media pool folder
|
|
265
267
|
Export(filePath) --> bool # Returns true if export of DRB folder to filePath is successful, false otherwise
|
|
266
|
-
TranscribeAudio()
|
|
268
|
+
TranscribeAudio(useSpeakerDetection=None) --> Bool # Transcribes audio of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise
|
|
267
269
|
ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise.
|
|
270
|
+
PerformAudioClassification() --> Bool # Analyzes and classifies the audio of the MediaPoolItems within the folder and nested folders into categories and subcategories.
|
|
271
|
+
ClearAudioClassification() --> Bool # Clears audio classification of the MediaPoolItems within the folder and nested folders.
|
|
272
|
+
RemoveMotionBlur({deblurOption}) --> [[MediaPoolItem, MediaPoolItem]...] # Applies motion deblur on MediaPoolItems in the folder. Returns a list of pairs mapping original to newly created MediaPoolItems.
|
|
273
|
+
AnalyzeForIntellisearch(identifyFaces, isBetterMode) --> Bool # Performs Intellisearch analysis on all MediaPoolItems in the folder. identifyFaces specifies whether to identify faces; isBetterMode specifies whether to use Better mode. Returns True if required packages are installed and analysis is successful.
|
|
274
|
+
AnalyzeForSlate(markerColor) --> Bool # Performs Slate analysis on all MediaPoolItems in the folder using the current settings and specified markerColor. Returns True if required packages are installed and analysis is successful.
|
|
268
275
|
|
|
269
276
|
MediaPoolItem
|
|
270
277
|
GetName() --> string # Returns the clip name.
|
|
@@ -304,8 +311,13 @@ MediaPoolItem
|
|
|
304
311
|
ReplaceClip(filePath) --> Bool # Replaces the underlying asset and metadata of MediaPoolItem with the specified absolute clip path.
|
|
305
312
|
ReplaceClipPreserveSubClip(filePath) --> Bool # Replaces the underlying asset and metadata of a video or audio clip with the specified absolute clip path, preserving original sub clip extents.
|
|
306
313
|
GetUniqueId() --> string # Returns a unique ID for the media pool item
|
|
307
|
-
TranscribeAudio()
|
|
314
|
+
TranscribeAudio(useSpeakerDetection=None) --> Bool # Transcribes audio of the MediaPoolItem. Returns True if successful; False otherwise
|
|
308
315
|
ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItem. Returns True if successful; False otherwise.
|
|
316
|
+
PerformAudioClassification() --> Bool # Analyzes and classifies the audio of a MediaPoolItem into categories and subcategories.
|
|
317
|
+
ClearAudioClassification() --> Bool # Clears audio classification of the MediaPoolItem.
|
|
318
|
+
RemoveMotionBlur({deblurOption}) --> MediaPoolItem # Applies motion deblur on the MediaPoolItem. Returns the newly created MediaPoolItem.
|
|
319
|
+
AnalyzeForIntellisearch(identifyFaces, isBetterMode) --> Bool # Performs Intellisearch analysis on the MediaPoolItem. identifyFaces specifies whether to identify faces; isBetterMode specifies whether to use Better mode. Returns True if required packages are installed and analysis is successful.
|
|
320
|
+
AnalyzeForSlate(markerColor) --> Bool # Performs Slate analysis on the MediaPoolItem using the current settings and specified markerColor. Returns True if required packages are installed and analysis is successful.
|
|
309
321
|
GetAudioMapping() --> json formatted string # Returns a string with MediaPoolItem's audio mapping information. Check 'Audio Mapping' section below for more information.
|
|
310
322
|
GetMarkInOut() --> {mark} # Returns dict of in/out marks set (keys omitted if not set), example:
|
|
311
323
|
# {'video': {'in': 0, 'out': 134}, 'audio': {'in': 0, 'out': 134}}
|
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.29.0"
|
|
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
|
@@ -10654,6 +10654,45 @@ def _current_resolve_project_id() -> Tuple[Optional[str], Optional[str]]:
|
|
|
10654
10654
|
|
|
10655
10655
|
|
|
10656
10656
|
@_serialize_resolve
|
|
10657
|
+
def _resolve_ai_features(resolve: Any) -> Dict[str, Any]:
|
|
10658
|
+
"""Report which Resolve 21.0 AI scripting methods are available on the
|
|
10659
|
+
connected build, plus the Extra each AI-gated method requires. Presence is
|
|
10660
|
+
detected via getattr (no Resolve round-trips beyond fetching the handles),
|
|
10661
|
+
so this stays cheap enough for the boot handshake.
|
|
10662
|
+
"""
|
|
10663
|
+
def has(obj: Any, name: str) -> bool:
|
|
10664
|
+
return bool(obj) and callable(getattr(obj, name, None))
|
|
10665
|
+
|
|
10666
|
+
project = folder = None
|
|
10667
|
+
try:
|
|
10668
|
+
pm = resolve.GetProjectManager()
|
|
10669
|
+
project = pm.GetCurrentProject() if pm else None
|
|
10670
|
+
mp = project.GetMediaPool() if project else None
|
|
10671
|
+
folder = mp.GetRootFolder() if mp else None
|
|
10672
|
+
except Exception:
|
|
10673
|
+
pass
|
|
10674
|
+
|
|
10675
|
+
features = {
|
|
10676
|
+
"disable_background_tasks": has(resolve, "DisableBackgroundTasksForCurrentResolveSession"),
|
|
10677
|
+
"generate_speech": has(project, "GenerateSpeech"),
|
|
10678
|
+
"perform_audio_classification": has(folder, "PerformAudioClassification"),
|
|
10679
|
+
"clear_audio_classification": has(folder, "ClearAudioClassification"),
|
|
10680
|
+
"analyze_for_intellisearch": has(folder, "AnalyzeForIntellisearch"),
|
|
10681
|
+
"analyze_for_slate": has(folder, "AnalyzeForSlate"),
|
|
10682
|
+
"remove_motion_blur": has(folder, "RemoveMotionBlur"),
|
|
10683
|
+
}
|
|
10684
|
+
return {
|
|
10685
|
+
"features": features,
|
|
10686
|
+
"available_count": sum(1 for v in features.values() if v),
|
|
10687
|
+
# Methods that additionally need an Extras download to actually run.
|
|
10688
|
+
"requires_extra": {
|
|
10689
|
+
"analyze_for_intellisearch": "AI IntelliSearch",
|
|
10690
|
+
"analyze_for_slate": "AI Slate ID",
|
|
10691
|
+
"generate_speech": "AI Speech Generator",
|
|
10692
|
+
},
|
|
10693
|
+
}
|
|
10694
|
+
|
|
10695
|
+
|
|
10657
10696
|
def _resolve_identity() -> Dict[str, Any]:
|
|
10658
10697
|
resolve, error = _connect_resolve_read_only()
|
|
10659
10698
|
if not resolve:
|
|
@@ -10668,6 +10707,7 @@ def _resolve_identity() -> Dict[str, Any]:
|
|
|
10668
10707
|
"version_string": str(version_string) if version_string else None,
|
|
10669
10708
|
"version": list(version_tuple) if isinstance(version_tuple, (list, tuple)) else None,
|
|
10670
10709
|
"page": str(page) if page else None,
|
|
10710
|
+
"ai_features": _resolve_ai_features(resolve),
|
|
10671
10711
|
}
|
|
10672
10712
|
|
|
10673
10713
|
|
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.29.0"
|
|
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/granular/folder.py
CHANGED
|
@@ -294,3 +294,127 @@ def folder_clear_transcription(folder_path: str = "") -> Dict[str, Any]:
|
|
|
294
294
|
folder = mp.GetCurrentFolder()
|
|
295
295
|
result = folder.ClearTranscription()
|
|
296
296
|
return {"success": bool(result)}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _resolve_folder(mp, folder_path):
|
|
300
|
+
"""Return (folder, error). Empty path -> current folder."""
|
|
301
|
+
if folder_path:
|
|
302
|
+
folder = _navigate_to_folder(mp, folder_path)
|
|
303
|
+
if not folder:
|
|
304
|
+
return None, {"error": f"Folder '{folder_path}' not found"}
|
|
305
|
+
return folder, None
|
|
306
|
+
return mp.GetCurrentFolder(), None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
_MARKER_COLORS = [
|
|
310
|
+
"Blue", "Cyan", "Green", "Yellow", "Red", "Pink", "Purple", "Fuchsia",
|
|
311
|
+
"Rose", "Lavender", "Sky", "Mint", "Lemon", "Sand", "Cocoa", "Cream",
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@mcp.tool()
|
|
316
|
+
def folder_perform_audio_classification(folder_path: str = "") -> Dict[str, Any]:
|
|
317
|
+
"""Classify audio of all clips in a Media Pool folder into categories (Resolve 21+).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
folder_path: Path from root. Empty for current folder.
|
|
321
|
+
"""
|
|
322
|
+
_, mp, err = _get_mp()
|
|
323
|
+
if err:
|
|
324
|
+
return err
|
|
325
|
+
folder, err = _resolve_folder(mp, folder_path)
|
|
326
|
+
if err:
|
|
327
|
+
return err
|
|
328
|
+
if not hasattr(folder, "PerformAudioClassification"):
|
|
329
|
+
return {"error": "PerformAudioClassification requires DaVinci Resolve 21+"}
|
|
330
|
+
return {"success": bool(folder.PerformAudioClassification())}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@mcp.tool()
|
|
334
|
+
def folder_clear_audio_classification(folder_path: str = "") -> Dict[str, Any]:
|
|
335
|
+
"""Clear audio classification for all clips in a Media Pool folder (Resolve 21+).
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
folder_path: Path from root. Empty for current folder.
|
|
339
|
+
"""
|
|
340
|
+
_, mp, err = _get_mp()
|
|
341
|
+
if err:
|
|
342
|
+
return err
|
|
343
|
+
folder, err = _resolve_folder(mp, folder_path)
|
|
344
|
+
if err:
|
|
345
|
+
return err
|
|
346
|
+
if not hasattr(folder, "ClearAudioClassification"):
|
|
347
|
+
return {"error": "ClearAudioClassification requires DaVinci Resolve 21+"}
|
|
348
|
+
return {"success": bool(folder.ClearAudioClassification())}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@mcp.tool()
|
|
352
|
+
def folder_analyze_for_intellisearch(folder_path: str = "", identify_faces: bool = False, is_better_mode: bool = False) -> Dict[str, Any]:
|
|
353
|
+
"""Run IntelliSearch analysis on all clips in a folder (Resolve 21+, requires AI IntelliSearch Extra).
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
folder_path: Path from root. Empty for current folder.
|
|
357
|
+
identify_faces: Whether to identify faces.
|
|
358
|
+
is_better_mode: Use Better mode (vs Faster).
|
|
359
|
+
"""
|
|
360
|
+
_, mp, err = _get_mp()
|
|
361
|
+
if err:
|
|
362
|
+
return err
|
|
363
|
+
folder, err = _resolve_folder(mp, folder_path)
|
|
364
|
+
if err:
|
|
365
|
+
return err
|
|
366
|
+
if not hasattr(folder, "AnalyzeForIntellisearch"):
|
|
367
|
+
return {"error": "AnalyzeForIntellisearch requires DaVinci Resolve 21+"}
|
|
368
|
+
return {"success": bool(folder.AnalyzeForIntellisearch(bool(identify_faces), bool(is_better_mode)))}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@mcp.tool()
|
|
372
|
+
def folder_analyze_for_slate(folder_path: str = "", marker_color: str = "Blue") -> Dict[str, Any]:
|
|
373
|
+
"""Run Slate analysis on all clips in a folder (Resolve 21+, requires AI Slate ID Extra).
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
folder_path: Path from root. Empty for current folder.
|
|
377
|
+
marker_color: Marker color for detected slates (Blue, Cyan, Green, Yellow, Red, Pink,
|
|
378
|
+
Purple, Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa, Cream).
|
|
379
|
+
"""
|
|
380
|
+
_, mp, err = _get_mp()
|
|
381
|
+
if err:
|
|
382
|
+
return err
|
|
383
|
+
folder, err = _resolve_folder(mp, folder_path)
|
|
384
|
+
if err:
|
|
385
|
+
return err
|
|
386
|
+
if not hasattr(folder, "AnalyzeForSlate"):
|
|
387
|
+
return {"error": "AnalyzeForSlate requires DaVinci Resolve 21+"}
|
|
388
|
+
if marker_color not in _MARKER_COLORS:
|
|
389
|
+
return {"error": f"Invalid marker_color '{marker_color}'. Valid: {', '.join(_MARKER_COLORS)}"}
|
|
390
|
+
return {"success": bool(folder.AnalyzeForSlate(marker_color))}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@mcp.tool()
|
|
394
|
+
def folder_remove_motion_blur(folder_path: str = "", deblur_option: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
395
|
+
"""Render motion-deblurred copies of all clips in a folder (Resolve 21+).
|
|
396
|
+
|
|
397
|
+
Creates NEW media files; source media is not modified.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
folder_path: Path from root. Empty for current folder.
|
|
401
|
+
deblur_option: Settings dict (FileName, Format, Codec, EncodingProfile,
|
|
402
|
+
UseExtremeMode, UseMarkInMarkOut, RenderAtSourceRes, UseMoreGpuMemory).
|
|
403
|
+
"""
|
|
404
|
+
_, mp, err = _get_mp()
|
|
405
|
+
if err:
|
|
406
|
+
return err
|
|
407
|
+
folder, err = _resolve_folder(mp, folder_path)
|
|
408
|
+
if err:
|
|
409
|
+
return err
|
|
410
|
+
if not hasattr(folder, "RemoveMotionBlur"):
|
|
411
|
+
return {"error": "RemoveMotionBlur requires DaVinci Resolve 21+"}
|
|
412
|
+
result = folder.RemoveMotionBlur(deblur_option or {})
|
|
413
|
+
created = []
|
|
414
|
+
for pair in (result or []):
|
|
415
|
+
try:
|
|
416
|
+
orig, new = pair
|
|
417
|
+
created.append({"original": orig.GetName(), "new": new.GetName(), "new_id": new.GetUniqueId()})
|
|
418
|
+
except Exception:
|
|
419
|
+
continue
|
|
420
|
+
return {"success": bool(result), "created": created}
|
|
@@ -909,3 +909,112 @@ def clear_clip_mark_in_out(clip_id: str) -> Dict[str, Any]:
|
|
|
909
909
|
return {"error": f"Clip {clip_id} not found"}
|
|
910
910
|
result = clip.ClearMarkInOut()
|
|
911
911
|
return {"success": bool(result)}
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
_MARKER_COLORS = [
|
|
915
|
+
"Blue", "Cyan", "Green", "Yellow", "Red", "Pink", "Purple", "Fuchsia",
|
|
916
|
+
"Rose", "Lavender", "Sky", "Mint", "Lemon", "Sand", "Cocoa", "Cream",
|
|
917
|
+
]
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
@mcp.tool()
|
|
921
|
+
def perform_clip_audio_classification(clip_id: str) -> Dict[str, Any]:
|
|
922
|
+
"""Classify a clip's audio into categories and subcategories (Resolve 21+).
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
clip_id: Unique ID of the clip.
|
|
926
|
+
"""
|
|
927
|
+
_, mp, err = _get_mp()
|
|
928
|
+
if err:
|
|
929
|
+
return err
|
|
930
|
+
clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
|
|
931
|
+
if not clip:
|
|
932
|
+
return {"error": f"Clip {clip_id} not found"}
|
|
933
|
+
if not hasattr(clip, "PerformAudioClassification"):
|
|
934
|
+
return {"error": "PerformAudioClassification requires DaVinci Resolve 21+"}
|
|
935
|
+
return {"success": bool(clip.PerformAudioClassification())}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
@mcp.tool()
|
|
939
|
+
def clear_clip_audio_classification(clip_id: str) -> Dict[str, Any]:
|
|
940
|
+
"""Clear a clip's audio classification (Resolve 21+).
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
clip_id: Unique ID of the clip.
|
|
944
|
+
"""
|
|
945
|
+
_, mp, err = _get_mp()
|
|
946
|
+
if err:
|
|
947
|
+
return err
|
|
948
|
+
clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
|
|
949
|
+
if not clip:
|
|
950
|
+
return {"error": f"Clip {clip_id} not found"}
|
|
951
|
+
if not hasattr(clip, "ClearAudioClassification"):
|
|
952
|
+
return {"error": "ClearAudioClassification requires DaVinci Resolve 21+"}
|
|
953
|
+
return {"success": bool(clip.ClearAudioClassification())}
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
@mcp.tool()
|
|
957
|
+
def analyze_clip_for_intellisearch(clip_id: str, identify_faces: bool = False, is_better_mode: bool = False) -> Dict[str, Any]:
|
|
958
|
+
"""Run IntelliSearch analysis on a clip (Resolve 21+, requires AI IntelliSearch Extra).
|
|
959
|
+
|
|
960
|
+
Args:
|
|
961
|
+
clip_id: Unique ID of the clip.
|
|
962
|
+
identify_faces: Whether to identify faces.
|
|
963
|
+
is_better_mode: Use Better mode (vs Faster).
|
|
964
|
+
"""
|
|
965
|
+
_, mp, err = _get_mp()
|
|
966
|
+
if err:
|
|
967
|
+
return err
|
|
968
|
+
clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
|
|
969
|
+
if not clip:
|
|
970
|
+
return {"error": f"Clip {clip_id} not found"}
|
|
971
|
+
if not hasattr(clip, "AnalyzeForIntellisearch"):
|
|
972
|
+
return {"error": "AnalyzeForIntellisearch requires DaVinci Resolve 21+"}
|
|
973
|
+
return {"success": bool(clip.AnalyzeForIntellisearch(bool(identify_faces), bool(is_better_mode)))}
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@mcp.tool()
|
|
977
|
+
def analyze_clip_for_slate(clip_id: str, marker_color: str = "Blue") -> Dict[str, Any]:
|
|
978
|
+
"""Run Slate analysis on a clip (Resolve 21+, requires AI Slate ID Extra).
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
clip_id: Unique ID of the clip.
|
|
982
|
+
marker_color: Marker color for detected slates (Blue, Cyan, Green, Yellow, Red, Pink,
|
|
983
|
+
Purple, Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa, Cream).
|
|
984
|
+
"""
|
|
985
|
+
_, mp, err = _get_mp()
|
|
986
|
+
if err:
|
|
987
|
+
return err
|
|
988
|
+
clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
|
|
989
|
+
if not clip:
|
|
990
|
+
return {"error": f"Clip {clip_id} not found"}
|
|
991
|
+
if not hasattr(clip, "AnalyzeForSlate"):
|
|
992
|
+
return {"error": "AnalyzeForSlate requires DaVinci Resolve 21+"}
|
|
993
|
+
if marker_color not in _MARKER_COLORS:
|
|
994
|
+
return {"error": f"Invalid marker_color '{marker_color}'. Valid: {', '.join(_MARKER_COLORS)}"}
|
|
995
|
+
return {"success": bool(clip.AnalyzeForSlate(marker_color))}
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
@mcp.tool()
|
|
999
|
+
def remove_clip_motion_blur(clip_id: str, deblur_option: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
1000
|
+
"""Render a motion-deblurred copy of a clip (Resolve 21+).
|
|
1001
|
+
|
|
1002
|
+
Creates a NEW media file; the source clip is not modified.
|
|
1003
|
+
|
|
1004
|
+
Args:
|
|
1005
|
+
clip_id: Unique ID of the clip.
|
|
1006
|
+
deblur_option: Settings dict (FileName, Format, Codec, EncodingProfile,
|
|
1007
|
+
UseExtremeMode, UseMarkInMarkOut, RenderAtSourceRes, UseMoreGpuMemory).
|
|
1008
|
+
"""
|
|
1009
|
+
_, mp, err = _get_mp()
|
|
1010
|
+
if err:
|
|
1011
|
+
return err
|
|
1012
|
+
clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
|
|
1013
|
+
if not clip:
|
|
1014
|
+
return {"error": f"Clip {clip_id} not found"}
|
|
1015
|
+
if not hasattr(clip, "RemoveMotionBlur"):
|
|
1016
|
+
return {"error": "RemoveMotionBlur requires DaVinci Resolve 21+"}
|
|
1017
|
+
new_clip = clip.RemoveMotionBlur(deblur_option or {})
|
|
1018
|
+
if not new_clip:
|
|
1019
|
+
return {"success": False}
|
|
1020
|
+
return {"success": True, "new": new_clip.GetName(), "new_id": new_clip.GetUniqueId()}
|
package/src/granular/project.py
CHANGED
|
@@ -1592,3 +1592,59 @@ def load_cloud_project(project_name: str, project_media_path: str, sync_mode: st
|
|
|
1592
1592
|
if project:
|
|
1593
1593
|
return {"success": True, "project_name": project.GetName()}
|
|
1594
1594
|
return {"success": False, "error": "Failed to load cloud project. Check cloud settings and connectivity."}
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
@mcp.tool()
|
|
1598
|
+
def generate_speech(text_input: str, voice_model: str = "", timecode: str = "",
|
|
1599
|
+
add_to_timeline: bool = False, audio_track: Optional[int] = None,
|
|
1600
|
+
custom_voice_file: str = "", speed: Optional[int] = None,
|
|
1601
|
+
variation: Optional[int] = None, pitch: Optional[int] = None,
|
|
1602
|
+
generation_id: Optional[int] = None, filename: str = "") -> Dict[str, Any]:
|
|
1603
|
+
"""Generate AI text-to-speech audio and add it to the media pool (Resolve 21+).
|
|
1604
|
+
|
|
1605
|
+
Requires the AI Speech Generator Extra. Creates a NEW audio MediaPoolItem; if
|
|
1606
|
+
add_to_timeline is True it is placed on the timeline at the given timecode.
|
|
1607
|
+
|
|
1608
|
+
Args:
|
|
1609
|
+
text_input: Text to synthesize (required).
|
|
1610
|
+
voice_model: Voice model name (e.g. "Female 1", "Male 1", "Custom Voice").
|
|
1611
|
+
timecode: Timeline timecode to place the clip at when add_to_timeline is True.
|
|
1612
|
+
add_to_timeline: Whether to add the generated clip to the timeline.
|
|
1613
|
+
audio_track: Audio track index for timeline placement.
|
|
1614
|
+
custom_voice_file: Full path to a custom voice file (for "Custom Voice").
|
|
1615
|
+
speed: Speech speed.
|
|
1616
|
+
variation: Voice variation.
|
|
1617
|
+
pitch: Voice pitch.
|
|
1618
|
+
generation_id: Generation ID.
|
|
1619
|
+
filename: Output filename.
|
|
1620
|
+
"""
|
|
1621
|
+
pm, current_project = get_current_project()
|
|
1622
|
+
if not current_project:
|
|
1623
|
+
return {"error": "No project currently open"}
|
|
1624
|
+
if not hasattr(current_project, "GenerateSpeech"):
|
|
1625
|
+
return {"error": "GenerateSpeech requires DaVinci Resolve 21+ and the AI Speech Generator Extra"}
|
|
1626
|
+
if not text_input:
|
|
1627
|
+
return {"error": "text_input is required"}
|
|
1628
|
+
settings: Dict[str, Any] = {"TextInput": text_input}
|
|
1629
|
+
if voice_model:
|
|
1630
|
+
settings["VoiceModel"] = voice_model
|
|
1631
|
+
if custom_voice_file:
|
|
1632
|
+
settings["CustomVoiceFile"] = custom_voice_file
|
|
1633
|
+
if speed is not None:
|
|
1634
|
+
settings["Speed"] = speed
|
|
1635
|
+
if variation is not None:
|
|
1636
|
+
settings["Variation"] = variation
|
|
1637
|
+
if pitch is not None:
|
|
1638
|
+
settings["Pitch"] = pitch
|
|
1639
|
+
if generation_id is not None:
|
|
1640
|
+
settings["GenerationID"] = generation_id
|
|
1641
|
+
if filename:
|
|
1642
|
+
settings["Filename"] = filename
|
|
1643
|
+
if add_to_timeline:
|
|
1644
|
+
settings["AddToTimeline"] = True
|
|
1645
|
+
if audio_track is not None:
|
|
1646
|
+
settings["AudioTrack"] = audio_track
|
|
1647
|
+
new_item = current_project.GenerateSpeech(settings, timecode or "")
|
|
1648
|
+
if not new_item:
|
|
1649
|
+
return {"success": False, "error": "GenerateSpeech returned no media item"}
|
|
1650
|
+
return {"success": True, "new": new_item.GetName(), "new_id": new_item.GetUniqueId()}
|
|
@@ -293,6 +293,23 @@ def delete_layout_preset_tool(preset_name: str) -> Dict[str, Any]:
|
|
|
293
293
|
return {"success": bool(result), "preset_name": preset_name}
|
|
294
294
|
|
|
295
295
|
|
|
296
|
+
@mcp.tool()
|
|
297
|
+
def disable_background_tasks_for_current_session() -> Dict[str, Any]:
|
|
298
|
+
"""Disable all background tasks for the current Resolve session (Resolve 21+).
|
|
299
|
+
|
|
300
|
+
Useful before heavy scripted operations so Resolve does not run background
|
|
301
|
+
work that competes for resources. Resets when Resolve restarts.
|
|
302
|
+
"""
|
|
303
|
+
resolve = get_resolve()
|
|
304
|
+
if resolve is None:
|
|
305
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
306
|
+
missing = _requires_method(resolve, "DisableBackgroundTasksForCurrentResolveSession", "21.0")
|
|
307
|
+
if missing:
|
|
308
|
+
return missing
|
|
309
|
+
resolve.DisableBackgroundTasksForCurrentResolveSession()
|
|
310
|
+
return {"success": True}
|
|
311
|
+
|
|
312
|
+
|
|
296
313
|
@mcp.resource("resolve://app/state")
|
|
297
314
|
def get_app_state_endpoint() -> Dict[str, Any]:
|
|
298
315
|
"""Get DaVinci Resolve application state information."""
|
|
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
|
|
|
34
34
|
if __name__ == "__main__":
|
|
35
35
|
try:
|
|
36
36
|
start_background_update_check(VERSION, project_dir, logger)
|
|
37
|
-
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (
|
|
37
|
+
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (341 granular tools)")
|
|
38
38
|
run_fastmcp_stdio(mcp)
|
|
39
39
|
except KeyboardInterrupt:
|
|
40
40
|
logger.info("Server shutdown requested")
|
package/src/server.py
CHANGED
|
@@ -8,10 +8,10 @@ Each tool groups related operations via an 'action' parameter.
|
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
python src/server.py # Start the MCP server
|
|
11
|
-
python src/server.py --full # Start the
|
|
11
|
+
python src/server.py --full # Start the 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.29.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -658,6 +658,12 @@ _TOKEN_GATED_DESTRUCTIVE_ACTIONS = frozenset({
|
|
|
658
658
|
("timeline", "delete_track"),
|
|
659
659
|
("graph", "apply_grade_from_drx"),
|
|
660
660
|
("graph", "reset_all_grades"),
|
|
661
|
+
# 21.0 AI ops that render/generate NEW media files (additive, but expensive
|
|
662
|
+
# and irreversible without manual cleanup) — gated so they never run by
|
|
663
|
+
# surprise. They never modify source media.
|
|
664
|
+
("folder", "remove_motion_blur"),
|
|
665
|
+
("media_pool_item", "remove_motion_blur"),
|
|
666
|
+
("project_settings", "generate_speech"),
|
|
661
667
|
})
|
|
662
668
|
|
|
663
669
|
|
|
@@ -5404,6 +5410,11 @@ def _transcription_capabilities(mp, p: Dict[str, Any]):
|
|
|
5404
5410
|
"summary": _media_pool_item_summary(clip),
|
|
5405
5411
|
"transcribe_audio": _has_method(clip, "TranscribeAudio"),
|
|
5406
5412
|
"clear_transcription": _has_method(clip, "ClearTranscription"),
|
|
5413
|
+
"perform_audio_classification": _has_method(clip, "PerformAudioClassification"),
|
|
5414
|
+
"clear_audio_classification": _has_method(clip, "ClearAudioClassification"),
|
|
5415
|
+
"analyze_for_intellisearch": _has_method(clip, "AnalyzeForIntellisearch"),
|
|
5416
|
+
"analyze_for_slate": _has_method(clip, "AnalyzeForSlate"),
|
|
5417
|
+
"remove_motion_blur": _has_method(clip, "RemoveMotionBlur"),
|
|
5407
5418
|
}
|
|
5408
5419
|
for clip in clips
|
|
5409
5420
|
],
|
|
@@ -5411,10 +5422,16 @@ def _transcription_capabilities(mp, p: Dict[str, Any]):
|
|
|
5411
5422
|
"name": current_folder.GetName() if current_folder else None,
|
|
5412
5423
|
"transcribe_audio": _has_method(current_folder, "TranscribeAudio") if current_folder else False,
|
|
5413
5424
|
"clear_transcription": _has_method(current_folder, "ClearTranscription") if current_folder else False,
|
|
5425
|
+
"perform_audio_classification": _has_method(current_folder, "PerformAudioClassification") if current_folder else False,
|
|
5426
|
+
"clear_audio_classification": _has_method(current_folder, "ClearAudioClassification") if current_folder else False,
|
|
5427
|
+
"analyze_for_intellisearch": _has_method(current_folder, "AnalyzeForIntellisearch") if current_folder else False,
|
|
5428
|
+
"analyze_for_slate": _has_method(current_folder, "AnalyzeForSlate") if current_folder else False,
|
|
5429
|
+
"remove_motion_blur": _has_method(current_folder, "RemoveMotionBlur") if current_folder else False,
|
|
5414
5430
|
},
|
|
5415
5431
|
"notes": [
|
|
5416
|
-
"This action reports capability only; use media_pool_item/folder
|
|
5417
|
-
"Transcription may require Resolve Studio AI components and can run asynchronously.",
|
|
5432
|
+
"This action reports capability only; use media_pool_item/folder actions to mutate disposable or approved clips.",
|
|
5433
|
+
"Transcription/audio-classification may require Resolve Studio AI components and can run asynchronously.",
|
|
5434
|
+
"analyze_for_intellisearch requires the 'AI IntelliSearch' Extra; analyze_for_slate requires 'AI Slate ID'; remove_motion_blur creates new media and is confirm-gated (Resolve 21+).",
|
|
5418
5435
|
],
|
|
5419
5436
|
}
|
|
5420
5437
|
|
|
@@ -10336,6 +10353,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
10336
10353
|
quit() -> {success}
|
|
10337
10354
|
get_fairlight_presets() -> {presets}
|
|
10338
10355
|
set_high_priority() -> {success}
|
|
10356
|
+
disable_background_tasks_for_current_session() -> {success} — Resolve 21+
|
|
10339
10357
|
open_control_panel(port?, host?, open_browser?) -> {success, url, pid, port, status}
|
|
10340
10358
|
— Launches the analysis control panel (src/analysis_dashboard.py) as a background process.
|
|
10341
10359
|
Idempotent: returns the existing URL if already running.
|
|
@@ -10429,7 +10447,13 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
10429
10447
|
return {"presets": _ser(r.GetFairlightPresets())}
|
|
10430
10448
|
elif action == "set_high_priority":
|
|
10431
10449
|
return {"success": bool(r.SetHighPriority())}
|
|
10432
|
-
|
|
10450
|
+
elif action == "disable_background_tasks_for_current_session":
|
|
10451
|
+
missing = _requires_method(r, "DisableBackgroundTasksForCurrentResolveSession", "21.0")
|
|
10452
|
+
if missing:
|
|
10453
|
+
return missing
|
|
10454
|
+
r.DisableBackgroundTasksForCurrentResolveSession()
|
|
10455
|
+
return _ok()
|
|
10456
|
+
return _unknown(action, ["launch","get_version","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"])
|
|
10433
10457
|
|
|
10434
10458
|
|
|
10435
10459
|
# ─── V2 C4: Per-field corrections with provenance + changelog ────────────────
|
|
@@ -12327,6 +12351,7 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
|
|
|
12327
12351
|
add_color_group(name) -> {success, name}
|
|
12328
12352
|
delete_color_group(name) -> {success}
|
|
12329
12353
|
apply_fairlight_preset(preset_name) -> {success}
|
|
12354
|
+
generate_speech(speech_generation_settings, timecode?) -> {success, new, new_id} — Resolve 21+, AI Speech Generator; creates new audio media (confirm-gated)
|
|
12330
12355
|
"""
|
|
12331
12356
|
p = params or {}
|
|
12332
12357
|
_, proj, err = _check()
|
|
@@ -12376,7 +12401,37 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
|
|
|
12376
12401
|
if missing:
|
|
12377
12402
|
return missing
|
|
12378
12403
|
return {"success": bool(proj.ApplyFairlightPresetToCurrentTimeline(p["preset_name"]))}
|
|
12379
|
-
|
|
12404
|
+
elif action == "generate_speech":
|
|
12405
|
+
missing = _requires_method(proj, "GenerateSpeech", "21.0")
|
|
12406
|
+
if missing:
|
|
12407
|
+
return missing
|
|
12408
|
+
settings = _first_param(p, "speech_generation_settings", "speechGenerationSettings", "settings", default=None) or {}
|
|
12409
|
+
if not isinstance(settings, dict) or not settings.get("TextInput"):
|
|
12410
|
+
return _err("generate_speech requires speech_generation_settings with a 'TextInput' string. "
|
|
12411
|
+
"Optional keys: VoiceModel, CustomVoiceFile, Speed, Variation, Pitch, GenerationID, Filename, AddToTimeline, AudioTrack.")
|
|
12412
|
+
timecode = _first_param(p, "timecode", default="") or ""
|
|
12413
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
12414
|
+
return _issue_confirm_token(
|
|
12415
|
+
action="project_settings.generate_speech",
|
|
12416
|
+
params=p,
|
|
12417
|
+
preview={
|
|
12418
|
+
"operation": "project_settings.generate_speech",
|
|
12419
|
+
"warning": "Generates a NEW AI text-to-speech audio item"
|
|
12420
|
+
+ (" and adds it to the timeline." if settings.get("AddToTimeline") else "."),
|
|
12421
|
+
"text_input": settings.get("TextInput"),
|
|
12422
|
+
"voice_model": settings.get("VoiceModel"),
|
|
12423
|
+
"add_to_timeline": bool(settings.get("AddToTimeline")),
|
|
12424
|
+
"timecode": timecode,
|
|
12425
|
+
},
|
|
12426
|
+
)
|
|
12427
|
+
blocked = _consume_confirm_token(action="project_settings.generate_speech", params=p)
|
|
12428
|
+
if blocked:
|
|
12429
|
+
return blocked
|
|
12430
|
+
new_item = proj.GenerateSpeech(settings, timecode)
|
|
12431
|
+
if not new_item:
|
|
12432
|
+
return {"success": False}
|
|
12433
|
+
return {"success": True, "new": new_item.GetName(), "new_id": new_item.GetUniqueId()}
|
|
12434
|
+
return _unknown(action, ["get_name","set_name","get_setting","set_setting","get_unique_id","get_presets","set_preset","refresh_luts","get_gallery","export_frame_as_still","load_burnin_preset","insert_audio","get_color_groups","add_color_group","delete_color_group","apply_fairlight_preset","generate_speech"])
|
|
12380
12435
|
|
|
12381
12436
|
|
|
12382
12437
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -13316,8 +13371,13 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
13316
13371
|
is_stale(path?) -> {stale}
|
|
13317
13372
|
get_unique_id(path?) -> {id}
|
|
13318
13373
|
export(path?, export_path) -> {success}
|
|
13319
|
-
transcribe_audio(path?) -> {success}
|
|
13374
|
+
transcribe_audio(path?, use_speaker_detection?) -> {success} — use_speaker_detection is Resolve 21+
|
|
13320
13375
|
clear_transcription(path?) -> {success}
|
|
13376
|
+
perform_audio_classification(path?) -> {success} — Resolve 21+
|
|
13377
|
+
clear_audio_classification(path?) -> {success} — Resolve 21+
|
|
13378
|
+
analyze_for_intellisearch(path?, identify_faces?, is_better_mode?) -> {success} — Resolve 21+, AI IntelliSearch Extra
|
|
13379
|
+
analyze_for_slate(path?, marker_color?) -> {success} — Resolve 21+, AI Slate ID Extra
|
|
13380
|
+
remove_motion_blur(path?, deblur_option?) -> {success, created} — Resolve 21+; renders NEW media (confirm-gated)
|
|
13321
13381
|
"""
|
|
13322
13382
|
p = params or {}
|
|
13323
13383
|
_, _, mp, err = _get_mp()
|
|
@@ -13344,10 +13404,66 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
13344
13404
|
elif action == "export":
|
|
13345
13405
|
return {"success": bool(f.Export(p["export_path"]))}
|
|
13346
13406
|
elif action == "transcribe_audio":
|
|
13347
|
-
|
|
13407
|
+
usd = _first_param(p, "use_speaker_detection", "useSpeakerDetection")
|
|
13408
|
+
if usd is None:
|
|
13409
|
+
return {"success": bool(f.TranscribeAudio())}
|
|
13410
|
+
return {"success": bool(f.TranscribeAudio(bool(usd)))}
|
|
13348
13411
|
elif action == "clear_transcription":
|
|
13349
13412
|
return {"success": bool(f.ClearTranscription())}
|
|
13350
|
-
|
|
13413
|
+
elif action == "perform_audio_classification":
|
|
13414
|
+
missing = _requires_method(f, "PerformAudioClassification", "21.0")
|
|
13415
|
+
if missing:
|
|
13416
|
+
return missing
|
|
13417
|
+
return {"success": bool(f.PerformAudioClassification())}
|
|
13418
|
+
elif action == "clear_audio_classification":
|
|
13419
|
+
missing = _requires_method(f, "ClearAudioClassification", "21.0")
|
|
13420
|
+
if missing:
|
|
13421
|
+
return missing
|
|
13422
|
+
return {"success": bool(f.ClearAudioClassification())}
|
|
13423
|
+
elif action == "analyze_for_intellisearch":
|
|
13424
|
+
missing = _requires_method(f, "AnalyzeForIntellisearch", "21.0")
|
|
13425
|
+
if missing:
|
|
13426
|
+
return missing
|
|
13427
|
+
identify_faces = bool(_first_param(p, "identify_faces", "identifyFaces", default=False))
|
|
13428
|
+
is_better_mode = bool(_first_param(p, "is_better_mode", "isBetterMode", default=False))
|
|
13429
|
+
return {"success": bool(f.AnalyzeForIntellisearch(identify_faces, is_better_mode))}
|
|
13430
|
+
elif action == "analyze_for_slate":
|
|
13431
|
+
missing = _requires_method(f, "AnalyzeForSlate", "21.0")
|
|
13432
|
+
if missing:
|
|
13433
|
+
return missing
|
|
13434
|
+
marker_color = _first_param(p, "marker_color", "markerColor", default="Blue")
|
|
13435
|
+
if marker_color not in _MARKER_COLORS:
|
|
13436
|
+
return _err(f"Invalid marker_color {marker_color!r}. Valid colors: {', '.join(_MARKER_COLORS)}")
|
|
13437
|
+
return {"success": bool(f.AnalyzeForSlate(marker_color))}
|
|
13438
|
+
elif action == "remove_motion_blur":
|
|
13439
|
+
missing = _requires_method(f, "RemoveMotionBlur", "21.0")
|
|
13440
|
+
if missing:
|
|
13441
|
+
return missing
|
|
13442
|
+
deblur = _first_param(p, "deblur_option", "deblurOption", default=None) or {}
|
|
13443
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
13444
|
+
return _issue_confirm_token(
|
|
13445
|
+
action="folder.remove_motion_blur",
|
|
13446
|
+
params=p,
|
|
13447
|
+
preview={
|
|
13448
|
+
"operation": "folder.remove_motion_blur",
|
|
13449
|
+
"warning": "Renders NEW deblurred media files for clips in the folder; source media is not modified.",
|
|
13450
|
+
"folder": f.GetName(),
|
|
13451
|
+
"deblur_option": deblur,
|
|
13452
|
+
},
|
|
13453
|
+
)
|
|
13454
|
+
blocked = _consume_confirm_token(action="folder.remove_motion_blur", params=p)
|
|
13455
|
+
if blocked:
|
|
13456
|
+
return blocked
|
|
13457
|
+
result = f.RemoveMotionBlur(deblur)
|
|
13458
|
+
created = []
|
|
13459
|
+
for pair in (result or []):
|
|
13460
|
+
try:
|
|
13461
|
+
orig, new = pair
|
|
13462
|
+
created.append({"original": orig.GetName(), "new": new.GetName(), "new_id": new.GetUniqueId()})
|
|
13463
|
+
except Exception:
|
|
13464
|
+
continue
|
|
13465
|
+
return {"success": bool(result), "created": created}
|
|
13466
|
+
return _unknown(action, ["get_clips","get_name","get_subfolders","is_stale","get_unique_id","export","transcribe_audio","clear_transcription","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur"])
|
|
13351
13467
|
|
|
13352
13468
|
|
|
13353
13469
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -13378,8 +13494,13 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13378
13494
|
monitor_growing_file(clip_id) -> {success}
|
|
13379
13495
|
replace_clip_preserve_sub_clip(clip_id, path) -> {success}
|
|
13380
13496
|
get_unique_id(clip_id) -> {id}
|
|
13381
|
-
transcribe_audio(clip_id) -> {success}
|
|
13497
|
+
transcribe_audio(clip_id, use_speaker_detection?) -> {success} — use_speaker_detection is Resolve 21+
|
|
13382
13498
|
clear_transcription(clip_id) -> {success}
|
|
13499
|
+
perform_audio_classification(clip_id) -> {success} — Resolve 21+
|
|
13500
|
+
clear_audio_classification(clip_id) -> {success} — Resolve 21+
|
|
13501
|
+
analyze_for_intellisearch(clip_id, identify_faces?, is_better_mode?) -> {success} — Resolve 21+, AI IntelliSearch Extra
|
|
13502
|
+
analyze_for_slate(clip_id, marker_color?) -> {success} — Resolve 21+, AI Slate ID Extra
|
|
13503
|
+
remove_motion_blur(clip_id, deblur_option?) -> {success, new, new_id} — Resolve 21+; renders NEW media (confirm-gated)
|
|
13383
13504
|
get_audio_mapping(clip_id) -> {mapping}
|
|
13384
13505
|
get_mark_in_out(clip_id) -> {mark}
|
|
13385
13506
|
set_mark_in_out(clip_id, mark_in, mark_out, type?) -> {success}
|
|
@@ -13570,9 +13691,60 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13570
13691
|
elif action == "get_unique_id":
|
|
13571
13692
|
return {"id": clip.GetUniqueId()}
|
|
13572
13693
|
elif action == "transcribe_audio":
|
|
13573
|
-
|
|
13694
|
+
usd = _first_param(p, "use_speaker_detection", "useSpeakerDetection")
|
|
13695
|
+
if usd is None:
|
|
13696
|
+
return {"success": bool(clip.TranscribeAudio())}
|
|
13697
|
+
return {"success": bool(clip.TranscribeAudio(bool(usd)))}
|
|
13574
13698
|
elif action == "clear_transcription":
|
|
13575
13699
|
return {"success": bool(clip.ClearTranscription())}
|
|
13700
|
+
elif action == "perform_audio_classification":
|
|
13701
|
+
missing = _requires_method(clip, "PerformAudioClassification", "21.0")
|
|
13702
|
+
if missing:
|
|
13703
|
+
return missing
|
|
13704
|
+
return {"success": bool(clip.PerformAudioClassification())}
|
|
13705
|
+
elif action == "clear_audio_classification":
|
|
13706
|
+
missing = _requires_method(clip, "ClearAudioClassification", "21.0")
|
|
13707
|
+
if missing:
|
|
13708
|
+
return missing
|
|
13709
|
+
return {"success": bool(clip.ClearAudioClassification())}
|
|
13710
|
+
elif action == "analyze_for_intellisearch":
|
|
13711
|
+
missing = _requires_method(clip, "AnalyzeForIntellisearch", "21.0")
|
|
13712
|
+
if missing:
|
|
13713
|
+
return missing
|
|
13714
|
+
identify_faces = bool(_first_param(p, "identify_faces", "identifyFaces", default=False))
|
|
13715
|
+
is_better_mode = bool(_first_param(p, "is_better_mode", "isBetterMode", default=False))
|
|
13716
|
+
return {"success": bool(clip.AnalyzeForIntellisearch(identify_faces, is_better_mode))}
|
|
13717
|
+
elif action == "analyze_for_slate":
|
|
13718
|
+
missing = _requires_method(clip, "AnalyzeForSlate", "21.0")
|
|
13719
|
+
if missing:
|
|
13720
|
+
return missing
|
|
13721
|
+
marker_color = _first_param(p, "marker_color", "markerColor", default="Blue")
|
|
13722
|
+
if marker_color not in _MARKER_COLORS:
|
|
13723
|
+
return _err(f"Invalid marker_color {marker_color!r}. Valid colors: {', '.join(_MARKER_COLORS)}")
|
|
13724
|
+
return {"success": bool(clip.AnalyzeForSlate(marker_color))}
|
|
13725
|
+
elif action == "remove_motion_blur":
|
|
13726
|
+
missing = _requires_method(clip, "RemoveMotionBlur", "21.0")
|
|
13727
|
+
if missing:
|
|
13728
|
+
return missing
|
|
13729
|
+
deblur = _first_param(p, "deblur_option", "deblurOption", default=None) or {}
|
|
13730
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
13731
|
+
return _issue_confirm_token(
|
|
13732
|
+
action="media_pool_item.remove_motion_blur",
|
|
13733
|
+
params=p,
|
|
13734
|
+
preview={
|
|
13735
|
+
"operation": "media_pool_item.remove_motion_blur",
|
|
13736
|
+
"warning": "Renders a NEW deblurred media file; the source clip is not modified.",
|
|
13737
|
+
"clip": clip.GetName(),
|
|
13738
|
+
"deblur_option": deblur,
|
|
13739
|
+
},
|
|
13740
|
+
)
|
|
13741
|
+
blocked = _consume_confirm_token(action="media_pool_item.remove_motion_blur", params=p)
|
|
13742
|
+
if blocked:
|
|
13743
|
+
return blocked
|
|
13744
|
+
new_clip = clip.RemoveMotionBlur(deblur)
|
|
13745
|
+
if not new_clip:
|
|
13746
|
+
return {"success": False}
|
|
13747
|
+
return {"success": True, "new": new_clip.GetName(), "new_id": new_clip.GetUniqueId()}
|
|
13576
13748
|
elif action == "get_audio_mapping":
|
|
13577
13749
|
return {"mapping": clip.GetAudioMapping()}
|
|
13578
13750
|
elif action == "get_mark_in_out":
|
|
@@ -13581,7 +13753,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13581
13753
|
return {"success": bool(clip.SetMarkInOut(p["mark_in"], p["mark_out"], p.get("type", "all")))}
|
|
13582
13754
|
elif action == "clear_mark_in_out":
|
|
13583
13755
|
return {"success": bool(clip.ClearMarkInOut(p.get("type", "all")))}
|
|
13584
|
-
return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer"])
|
|
13756
|
+
return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer"])
|
|
13585
13757
|
|
|
13586
13758
|
|
|
13587
13759
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -20106,9 +20278,9 @@ def _resource_install_guidance() -> Dict[str, Any]:
|
|
|
20106
20278
|
if __name__ == "__main__":
|
|
20107
20279
|
start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
|
|
20108
20280
|
|
|
20109
|
-
# Support --full flag to run the
|
|
20281
|
+
# Support --full flag to run the 341-tool granular server instead
|
|
20110
20282
|
if "--full" in sys.argv:
|
|
20111
|
-
logger.info("Starting full
|
|
20283
|
+
logger.info("Starting full 341-tool granular server...")
|
|
20112
20284
|
sys.argv = [arg for arg in sys.argv if arg != "--full"]
|
|
20113
20285
|
from src.granular import mcp as granular_mcp
|
|
20114
20286
|
|