davinci-resolve-mcp 2.35.0 → 2.35.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 +9 -0
- package/README.md +1 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +56 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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.35.1
|
|
6
|
+
|
|
7
|
+
- **Added** `media_pool_item(action="extract_frames", clip_id, timestamps, output_dir?)`
|
|
8
|
+
extracts still JPEGs from a clip's source media at the given timestamps (seconds)
|
|
9
|
+
via ffmpeg. Source-safe: it reads the source and writes only to a scratch output
|
|
10
|
+
directory — it never modifies, transcodes, or proxies the source. (Closes a
|
|
11
|
+
read/write-symmetry gap: the analysis sampler existed internally but had no
|
|
12
|
+
standalone frame-extraction tool.)
|
|
13
|
+
|
|
5
14
|
## What's New in v2.35.0
|
|
6
15
|
|
|
7
16
|
The Cut-IR executor — transcript-driven editing now closes the loop onto the
|
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)
|
package/install.py
CHANGED
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.35.
|
|
38
|
+
VERSION = "2.35.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.35.
|
|
83
|
+
VERSION = "2.35.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.35.
|
|
14
|
+
VERSION = "2.35.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -5510,6 +5510,55 @@ def _audio_mapping_report(mp, tl, p: Dict[str, Any]):
|
|
|
5510
5510
|
return {"timeline_items": timeline_items, "media_pool_items": clip_rows}
|
|
5511
5511
|
|
|
5512
5512
|
|
|
5513
|
+
def _extract_clip_frames(clip, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
5514
|
+
"""Extract still frames from a clip's source media at given timestamps.
|
|
5515
|
+
|
|
5516
|
+
Source-safe: reads the source file with ffmpeg and writes JPEGs to a scratch
|
|
5517
|
+
output directory only — it never modifies, transcodes, or proxies the source.
|
|
5518
|
+
`timestamps` is a list of seconds. Returns the written frame paths.
|
|
5519
|
+
"""
|
|
5520
|
+
try:
|
|
5521
|
+
src = clip.GetClipProperty("File Path")
|
|
5522
|
+
except Exception:
|
|
5523
|
+
src = None
|
|
5524
|
+
if not src or not os.path.isfile(src):
|
|
5525
|
+
return _err("Clip has no readable source file (File Path)")
|
|
5526
|
+
timestamps = p.get("timestamps")
|
|
5527
|
+
if not isinstance(timestamps, list) or not timestamps:
|
|
5528
|
+
return _err("extract_frames requires 'timestamps' (a non-empty list of seconds)")
|
|
5529
|
+
if not shutil.which("ffmpeg"):
|
|
5530
|
+
return _err("ffmpeg not found on PATH")
|
|
5531
|
+
out_dir = p.get("output_dir") or tempfile.mkdtemp(prefix="mcp_frames_")
|
|
5532
|
+
try:
|
|
5533
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
5534
|
+
except OSError as exc:
|
|
5535
|
+
return _err(f"Could not create output_dir: {exc}")
|
|
5536
|
+
paths, errors = [], []
|
|
5537
|
+
for i, ts in enumerate(timestamps):
|
|
5538
|
+
out_path = os.path.join(out_dir, f"frame_{i:04d}.jpg")
|
|
5539
|
+
try:
|
|
5540
|
+
safe_run(
|
|
5541
|
+
["ffmpeg", "-y", "-ss", str(float(ts)), "-i", src,
|
|
5542
|
+
"-frames:v", "1", "-q:v", "2", out_path],
|
|
5543
|
+
capture_output=True, timeout=60,
|
|
5544
|
+
)
|
|
5545
|
+
except (OSError, subprocess.SubprocessError, ValueError) as exc:
|
|
5546
|
+
errors.append({"timestamp": ts, "error": str(exc)})
|
|
5547
|
+
continue
|
|
5548
|
+
if os.path.isfile(out_path):
|
|
5549
|
+
paths.append(out_path)
|
|
5550
|
+
else:
|
|
5551
|
+
errors.append({"timestamp": ts, "error": "no frame written (timestamp out of range?)"})
|
|
5552
|
+
return {
|
|
5553
|
+
"success": bool(paths),
|
|
5554
|
+
"source": src,
|
|
5555
|
+
"output_dir": out_dir,
|
|
5556
|
+
"frame_paths": paths,
|
|
5557
|
+
"count": len(paths),
|
|
5558
|
+
"errors": errors,
|
|
5559
|
+
}
|
|
5560
|
+
|
|
5561
|
+
|
|
5513
5562
|
def _clip_name(clip):
|
|
5514
5563
|
try:
|
|
5515
5564
|
return clip.GetName()
|
|
@@ -13797,6 +13846,9 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
13797
13846
|
get_transcription(clip_id) -> {text, truncated, status, has_transcription}
|
|
13798
13847
|
Read a clip's transcription. `truncated` flags when Resolve's preview
|
|
13799
13848
|
property cut the text off (the full transcript is longer).
|
|
13849
|
+
extract_frames(clip_id, timestamps, output_dir?) -> {frame_paths, output_dir, count, errors}
|
|
13850
|
+
Extract still JPEGs from the clip's source at the given timestamps (seconds)
|
|
13851
|
+
via ffmpeg. Source-safe: reads source, writes only to a scratch dir.
|
|
13800
13852
|
perform_audio_classification(clip_id) -> {success} — Resolve 21+
|
|
13801
13853
|
clear_audio_classification(clip_id) -> {success} — Resolve 21+
|
|
13802
13854
|
analyze_for_intellisearch(clip_id, identify_faces?, is_better_mode?) -> {success} — Resolve 21+, AI IntelliSearch Extra
|
|
@@ -14012,6 +14064,8 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
14012
14064
|
"status": status or None,
|
|
14013
14065
|
"has_transcription": bool(text.strip()),
|
|
14014
14066
|
}
|
|
14067
|
+
elif action == "extract_frames":
|
|
14068
|
+
return _extract_clip_frames(clip, p)
|
|
14015
14069
|
elif action == "perform_audio_classification":
|
|
14016
14070
|
missing = _requires_method(clip, "PerformAudioClassification", "21.0")
|
|
14017
14071
|
if missing:
|
|
@@ -14096,7 +14150,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
14096
14150
|
return {"success": bool(clip.SetMarkInOut(clean["mark_in"], clean["mark_out"], p.get("type", "all")))}
|
|
14097
14151
|
elif action == "clear_mark_in_out":
|
|
14098
14152
|
return {"success": bool(clip.ClearMarkInOut(p.get("type", "all")))}
|
|
14099
|
-
return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_transcription","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"])
|
|
14153
|
+
return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_transcription","extract_frames","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer"])
|
|
14100
14154
|
|
|
14101
14155
|
|
|
14102
14156
|
# ═══════════════════════════════════════════════════════════════════════════════
|