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 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
- [![Version](https://img.shields.io/badge/version-2.51.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.52.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-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 evidence for each
726
- timeline item (items without transcripts are reported in `skipped`, never
727
- silently trimmed). `execute_tighten(plan_id)` assembles a tightened
728
- VARIANT timeline from the plan's keep ranges — true partial trims; the
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.51.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.51.0",
3
+ "version": "2.52.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.51.0"
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.51.0"
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 evidence for the
16736
- current (or named) timeline.
16737
- - execute_tighten(plan_id, confirm_token?) duplicates the timeline and
16738
- applies the lifts (ripple) to the DUPLICATE, never the original.
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
  }
@@ -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?}. Lifts are returned in
357
- timeline frames, latest-first ready.
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