davinci-resolve-mcp 2.51.0 → 2.52.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 +19 -0
- package/README.md +1 -1
- package/docs/SKILL.md +9 -5
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +44 -5
- package/src/utils/edit_engine.py +45 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.52.0
|
|
6
|
+
|
|
7
|
+
`edit_engine` tighten now carries audio. Previously `execute_tighten`
|
|
8
|
+
assembled a video-only variant — a speech-driven dead-air cut came out
|
|
9
|
+
silent, with nothing in the preview or readback to warn you (issue #67).
|
|
10
|
+
|
|
11
|
+
- **Fixed** `plan_tighten` mirrors every kept video range onto its linked
|
|
12
|
+
audio track(s) with identical source frames, so the assembled variant
|
|
13
|
+
stays frame-locked and audible. Audio targets the item's detected linked
|
|
14
|
+
audio tracks (same `GetLinkedItems` matching `execute_swap` uses), falling
|
|
15
|
+
back to audio track 1 where a single linked A/V clip's audio lives.
|
|
16
|
+
- **Added** `include_audio` parameter to `plan_tighten` (default `true`).
|
|
17
|
+
Pass `include_audio=false` for the prior video-only assembly.
|
|
18
|
+
- **Added** an `audio_accounting` block to the `execute_tighten` confirm
|
|
19
|
+
preview and readback (planned vs. actual audio/video item counts), so a
|
|
20
|
+
silent variant can no longer ship unnoticed. Old video-only plans
|
|
21
|
+
re-executed against this build still work and are now loudly flagged as
|
|
22
|
+
silent.
|
|
23
|
+
|
|
5
24
|
## What's New in v2.51.0
|
|
6
25
|
|
|
7
26
|
CLAP audio embeddings — the final phase of the post-program improvements.
|
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/docs/SKILL.md
CHANGED
|
@@ -722,11 +722,15 @@ clip-count readback plus `brain_edits` rationale rows.
|
|
|
722
722
|
`execute_selects(plan_id)` creates a NEW selects timeline from the plan's
|
|
723
723
|
per-shot source ranges — additive; nothing existing is touched.
|
|
724
724
|
- `plan_tighten(timeline_name?, target_ratio?, min_pause_seconds?,
|
|
725
|
-
handle_seconds?)` — dead-air lifts from transcript-gap
|
|
726
|
-
timeline item (items without transcripts are reported in
|
|
727
|
-
silently trimmed). `execute_tighten(plan_id)` assembles a
|
|
728
|
-
VARIANT timeline from the plan's keep ranges — true partial
|
|
729
|
-
original timeline is never mutated.
|
|
725
|
+
handle_seconds?, include_audio?)` — dead-air lifts from transcript-gap
|
|
726
|
+
evidence for each timeline item (items without transcripts are reported in
|
|
727
|
+
`skipped`, never silently trimmed). `execute_tighten(plan_id)` assembles a
|
|
728
|
+
tightened VARIANT timeline from the plan's keep ranges — true partial
|
|
729
|
+
trims; the original timeline is never mutated. v2.52.0+: kept ranges mirror
|
|
730
|
+
onto each item's linked audio track(s) so the variant is audible (a
|
|
731
|
+
speech-driven cut was previously silent — #67); pass
|
|
732
|
+
`include_audio=false` for a video-only assembly, and the
|
|
733
|
+
`execute_tighten` readback carries an `audio_accounting` block.
|
|
730
734
|
- `plan_swap(timeline_start_frame | item_name, kind="visual"|"text",
|
|
731
735
|
limit?)` — alternates for one timeline item via the similarity index,
|
|
732
736
|
filtered to shots long enough to fill the slot exactly.
|
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.52.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
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.52.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/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.52.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -16592,6 +16592,15 @@ def _edit_engine_collect_items(tl, *, track_index: Optional[int] = None) -> List
|
|
|
16592
16592
|
pass
|
|
16593
16593
|
except Exception:
|
|
16594
16594
|
pass
|
|
16595
|
+
# Linked audio track indices so the planner can mirror kept ranges
|
|
16596
|
+
# onto sound (see #67 — tighten variants were coming out silent).
|
|
16597
|
+
try:
|
|
16598
|
+
audio_indices, _audio_note = _edit_engine_linked_audio_tracks(
|
|
16599
|
+
tl, item, row["timeline_start_frame"], row["timeline_end_frame"]
|
|
16600
|
+
)
|
|
16601
|
+
row["audio_track_indices"] = audio_indices
|
|
16602
|
+
except Exception:
|
|
16603
|
+
row["audio_track_indices"] = []
|
|
16595
16604
|
rows.append(row)
|
|
16596
16605
|
return rows
|
|
16597
16606
|
|
|
@@ -16732,10 +16741,13 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
16732
16741
|
- execute_selects(plan_id, confirm_token?) — creates a NEW selects timeline
|
|
16733
16742
|
from the plan's clip in/out ranges. Nothing existing is touched.
|
|
16734
16743
|
- plan_tighten(timeline_name?, target_ratio?, min_pause_seconds?,
|
|
16735
|
-
handle_seconds?) — dead-air lifts from transcript-gap
|
|
16736
|
-
current (or named) timeline.
|
|
16737
|
-
|
|
16738
|
-
|
|
16744
|
+
handle_seconds?, include_audio?) — dead-air lifts from transcript-gap
|
|
16745
|
+
evidence for the current (or named) timeline. Kept ranges mirror onto the
|
|
16746
|
+
items' linked audio tracks by default so the variant is audible; pass
|
|
16747
|
+
include_audio=false for a video-only (silent) assembly.
|
|
16748
|
+
- execute_tighten(plan_id, confirm_token?) — assembles a tightened VARIANT
|
|
16749
|
+
timeline from the plan's keep ranges (video + mirrored audio), never
|
|
16750
|
+
mutating the original. Readback includes an audio_accounting block.
|
|
16739
16751
|
- plan_swap(track_index?, timeline_start_frame | item_name, kind?, limit?)
|
|
16740
16752
|
— alternates for one timeline item via the similarity index.
|
|
16741
16753
|
- execute_swap(plan_id, alternate_index, confirm_token?) — replaces the
|
|
@@ -16807,6 +16819,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
16807
16819
|
target_ratio=p.get("target_ratio") or p.get("targetRatio"),
|
|
16808
16820
|
min_pause_seconds=float(p.get("min_pause_seconds") or p.get("minPauseSeconds") or _edit_engine_mod.DEFAULT_MIN_PAUSE_SECONDS),
|
|
16809
16821
|
handle_seconds=float(p.get("handle_seconds") or p.get("handleSeconds") or _edit_engine_mod.DEFAULT_HANDLE_SECONDS),
|
|
16822
|
+
include_audio=str(p.get("include_audio", p.get("includeAudio", True))).strip().lower() not in {"false", "0", "no", "none", "off"},
|
|
16810
16823
|
)
|
|
16811
16824
|
|
|
16812
16825
|
if action == "plan_swap":
|
|
@@ -16951,6 +16964,8 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
16951
16964
|
keep_ranges = plan.get("keep_ranges") or []
|
|
16952
16965
|
if not keep_ranges:
|
|
16953
16966
|
return _err("plan has no keep_ranges — re-plan with this version")
|
|
16967
|
+
audio_keep_ranges = sum(1 for r in keep_ranges if str(r.get("track_type", "video")).lower() == "audio")
|
|
16968
|
+
video_keep_ranges = len(keep_ranges) - audio_keep_ranges
|
|
16954
16969
|
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
16955
16970
|
return _issue_confirm_token(
|
|
16956
16971
|
action="edit_engine.execute_tighten", params=p,
|
|
@@ -16963,6 +16978,13 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
16963
16978
|
"timeline_name": plan.get("timeline_name"),
|
|
16964
16979
|
"lift_count": len(lifts),
|
|
16965
16980
|
"keep_range_count": len(keep_ranges),
|
|
16981
|
+
"video_keep_range_count": video_keep_ranges,
|
|
16982
|
+
"audio_keep_range_count": audio_keep_ranges,
|
|
16983
|
+
"audio_note": (
|
|
16984
|
+
f"Variant carries audio ({audio_keep_ranges} audio ranges mirror the video cuts)."
|
|
16985
|
+
if audio_keep_ranges
|
|
16986
|
+
else "WARNING: variant is VIDEO-ONLY (silent) — plan has no audio ranges."
|
|
16987
|
+
),
|
|
16966
16988
|
"estimated_removed_seconds": sum(l.get("duration_seconds") or 0 for l in lifts),
|
|
16967
16989
|
},
|
|
16968
16990
|
)
|
|
@@ -17039,6 +17061,23 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
17039
17061
|
else None
|
|
17040
17062
|
),
|
|
17041
17063
|
"structural_diff": structural_diff,
|
|
17064
|
+
"audio_accounting": {
|
|
17065
|
+
"planned_audio_ranges": audio_keep_ranges,
|
|
17066
|
+
"planned_video_ranges": video_keep_ranges,
|
|
17067
|
+
"variant_audio_items": sum(
|
|
17068
|
+
1 for it in (variant.get("items") or [])
|
|
17069
|
+
if (it.get("range") or {}).get("media_type") == 2
|
|
17070
|
+
),
|
|
17071
|
+
"variant_video_items": sum(
|
|
17072
|
+
1 for it in (variant.get("items") or [])
|
|
17073
|
+
if (it.get("range") or {}).get("media_type") == 1
|
|
17074
|
+
),
|
|
17075
|
+
"note": (
|
|
17076
|
+
"Variant carries audio mirrored from the video cuts."
|
|
17077
|
+
if audio_keep_ranges
|
|
17078
|
+
else "Variant is VIDEO-ONLY (silent) — re-plan with include_audio=True for sound."
|
|
17079
|
+
),
|
|
17080
|
+
},
|
|
17042
17081
|
},
|
|
17043
17082
|
"plan_id": plan.get("plan_id"),
|
|
17044
17083
|
}
|
package/src/utils/edit_engine.py
CHANGED
|
@@ -348,13 +348,23 @@ def plan_tighten(
|
|
|
348
348
|
target_ratio: Optional[float] = None,
|
|
349
349
|
min_pause_seconds: float = DEFAULT_MIN_PAUSE_SECONDS,
|
|
350
350
|
handle_seconds: float = DEFAULT_HANDLE_SECONDS,
|
|
351
|
+
include_audio: bool = True,
|
|
351
352
|
) -> Dict[str, Any]:
|
|
352
353
|
"""Propose dead-air lifts for a timeline.
|
|
353
354
|
|
|
354
355
|
`items` rows come from the server (Resolve read): each needs
|
|
355
356
|
{timeline_start_frame, timeline_end_frame, source_start_frame,
|
|
356
|
-
media_ref (clip id / path / hash), item_name?}.
|
|
357
|
-
|
|
357
|
+
media_ref (clip id / path / hash), item_name?}. Optionally each row may
|
|
358
|
+
carry {audio_track_indices: [int, ...]} naming the audio tracks that hold
|
|
359
|
+
the item's linked audio. Lifts are returned in timeline frames, latest-first
|
|
360
|
+
ready.
|
|
361
|
+
|
|
362
|
+
When ``include_audio`` (the default), each kept video range is mirrored onto
|
|
363
|
+
matching audio range(s) so the assembled variant carries sound — a
|
|
364
|
+
speech-driven cut would otherwise come out silent (see #67). Audio is
|
|
365
|
+
mirrored to the item's detected ``audio_track_indices``; absent that, it
|
|
366
|
+
falls back to audio track 1, which is where a single linked A/V clip's audio
|
|
367
|
+
lives.
|
|
358
368
|
"""
|
|
359
369
|
if not items:
|
|
360
370
|
return {"success": False, "error": "No timeline items supplied"}
|
|
@@ -475,6 +485,11 @@ def plan_tighten(
|
|
|
475
485
|
cursor = max(cursor, lift_end_sec)
|
|
476
486
|
if spec["src_end_sec"] - cursor > 0.05:
|
|
477
487
|
segments.append((cursor, spec["src_end_sec"]))
|
|
488
|
+
audio_indices: List[int] = []
|
|
489
|
+
if include_audio:
|
|
490
|
+
audio_indices = [int(i) for i in (item.get("audio_track_indices") or []) if int(i) > 0]
|
|
491
|
+
if not audio_indices:
|
|
492
|
+
audio_indices = [1]
|
|
478
493
|
for seg_start, seg_end in segments:
|
|
479
494
|
start_frame = int(round(seg_start * clip_fps))
|
|
480
495
|
end_frame = max(start_frame + 1, int(round(seg_end * clip_fps)) - 1)
|
|
@@ -485,23 +500,40 @@ def plan_tighten(
|
|
|
485
500
|
"track_type": "video",
|
|
486
501
|
"track_index": int(item.get("track_index") or 1),
|
|
487
502
|
})
|
|
503
|
+
# Mirror each kept video range onto its linked audio track(s) with
|
|
504
|
+
# identical source frames so the variant stays frame-locked and
|
|
505
|
+
# audible. mediaType 2 pulls the same media-pool item's audio.
|
|
506
|
+
for audio_index in audio_indices:
|
|
507
|
+
keep_ranges.append({
|
|
508
|
+
"clip_id": spec["resolve_clip_id"],
|
|
509
|
+
"start_frame": start_frame,
|
|
510
|
+
"end_frame": end_frame,
|
|
511
|
+
"track_type": "audio",
|
|
512
|
+
"media_type": 2,
|
|
513
|
+
"track_index": audio_index,
|
|
514
|
+
})
|
|
488
515
|
|
|
489
516
|
removed_frames = sum(l["timeline_end_frame"] - l["timeline_start_frame"] for l in lifts)
|
|
517
|
+
audio_keep_range_count = sum(1 for r in keep_ranges if r.get("track_type") == "audio")
|
|
518
|
+
video_keep_range_count = len(keep_ranges) - audio_keep_range_count
|
|
490
519
|
plan = save_plan(project_root, {
|
|
491
520
|
"kind": "tighten",
|
|
492
521
|
"timeline_name": timeline_name,
|
|
493
522
|
"timeline_fps": fps,
|
|
494
523
|
"lifts": lifts,
|
|
495
524
|
"keep_ranges": keep_ranges,
|
|
525
|
+
"include_audio": bool(include_audio),
|
|
496
526
|
"skipped": skipped,
|
|
497
527
|
"summary": (
|
|
498
528
|
f"{len(lifts)} dead-air lifts, ~{round(removed_frames / fps, 1)}s removed "
|
|
499
|
-
f"from '{timeline_name}' (assembled as a tightened variant
|
|
529
|
+
f"from '{timeline_name}' (assembled as a tightened variant"
|
|
530
|
+
f"{', video + audio' if include_audio else ', video only'})"
|
|
500
531
|
),
|
|
501
532
|
"settings": {
|
|
502
533
|
"target_ratio": target_ratio,
|
|
503
534
|
"min_pause_seconds": min_pause_seconds,
|
|
504
535
|
"handle_seconds": handle_seconds,
|
|
536
|
+
"include_audio": bool(include_audio),
|
|
505
537
|
},
|
|
506
538
|
})
|
|
507
539
|
return {
|
|
@@ -514,11 +546,20 @@ def plan_tighten(
|
|
|
514
546
|
"estimated_removed_seconds": round(removed_frames / fps, 2),
|
|
515
547
|
"lifts": lifts,
|
|
516
548
|
"keep_range_count": len(keep_ranges),
|
|
549
|
+
"video_keep_range_count": video_keep_range_count,
|
|
550
|
+
"audio_keep_range_count": audio_keep_range_count,
|
|
551
|
+
"include_audio": bool(include_audio),
|
|
517
552
|
"skipped": skipped,
|
|
518
553
|
"note": (
|
|
519
554
|
"Dry-run plan. Execute with edit_engine(action='execute_tighten', "
|
|
520
555
|
"params={plan_id}) — a tightened VARIANT timeline is assembled from "
|
|
521
|
-
"the keep ranges; the original timeline is never mutated."
|
|
556
|
+
"the keep ranges; the original timeline is never mutated. "
|
|
557
|
+
+ (
|
|
558
|
+
f"Audio is mirrored onto matching tracks ({audio_keep_range_count} "
|
|
559
|
+
"audio ranges) so the variant is audible."
|
|
560
|
+
if include_audio
|
|
561
|
+
else "include_audio=False: the variant will be VIDEO-ONLY (silent)."
|
|
562
|
+
)
|
|
522
563
|
),
|
|
523
564
|
}
|
|
524
565
|
|