davinci-resolve-mcp 4.8.19 → 4.8.21

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.
@@ -2005,6 +2005,30 @@ API_TRUTH: List[Dict[str, Any]] = [
2005
2005
  "when mirroring keep-ranges into clipInfos.",
2006
2006
  "tags": ["timeline", "edit", "off-by-one", "readback"],
2007
2007
  },
2008
+ {
2009
+ "symbol": "Timeline.GetEndFrame (exclusive bound)",
2010
+ "object": "Timeline",
2011
+ "signature": "() -> int",
2012
+ "reality": "GetEndFrame() is an EXCLUSIVE bound, one past the last "
2013
+ "frame: a timeline's frame count is "
2014
+ "GetEndFrame() - GetStartFrame(), not that plus one. "
2015
+ "Measured live on Studio 19.1.3.7 (2026-09-26): a "
2016
+ "timeline whose only full-length item is a 32742-frame "
2017
+ "clip (media-pool 'Frames' 32742, 'End' 32741) reads "
2018
+ "GetStartFrame() 0 / GetEndFrame() 32742, and that item's "
2019
+ "TimelineItem.GetEnd() is also 32742 - the same half-open "
2020
+ "convention as clipInfo endFrame above. Until v4.8.21 two "
2021
+ "readers here (granular get_current_timeline and "
2022
+ "get_project_info's per-timeline duration) added 1 and "
2023
+ "reported 601 frames for a 600-frame timeline while every "
2024
+ "other duration path in the server did not (PR #269).",
2025
+ "recommended": "duration_frames = GetEndFrame() - GetStartFrame(). "
2026
+ "The last frame index is GetEndFrame() - 1. Apply the "
2027
+ "same rule to TimelineItem.GetEnd(): an item occupies "
2028
+ "[GetStart(), GetEnd()).",
2029
+ "tags": ["timeline", "off-by-one", "readback", "duration"],
2030
+ "verified_on": "DaVinci Resolve Studio 19.1.3.7",
2031
+ },
2008
2032
  {
2009
2033
  "symbol": "MediaPool.ImportTimelineFromFile (internal sequence name overrides timelineName)",
2010
2034
  "object": "MediaPool",
@@ -12,6 +12,8 @@ Resolve re-scan its LUT directory. ACES DCTLs are scanned only at startup.
12
12
 
13
13
  from typing import Any, Dict, List, Optional
14
14
 
15
+ from src.utils.bool_params import coerce_bool
16
+
15
17
 
16
18
  def header(name: str, kind: str) -> str:
17
19
  """Marker comment used by the dctl tool's `list` action."""
@@ -264,7 +266,7 @@ def aces_idt(name: str, options: Optional[Dict[str, Any]] = None) -> str:
264
266
  Install requires a Resolve restart. See docs/notes/dctl-notes.md → "DCTL And ACES".
265
267
  """
266
268
  options = options or {}
267
- parametric = bool(options.get("parametric", False))
269
+ parametric = coerce_bool(options.get("parametric"), False)
268
270
  body = options.get("body") or (
269
271
  " return make_float3(p_R, p_G, p_B);"
270
272
  )
@@ -290,7 +292,7 @@ def aces_odt(name: str, options: Optional[Dict[str, Any]] = None) -> str:
290
292
  Resolve which side of the pipeline this transform belongs to.
291
293
  """
292
294
  options = options or {}
293
- parametric = bool(options.get("parametric", False))
295
+ parametric = coerce_bool(options.get("parametric"), False)
294
296
  body = options.get("body") or (
295
297
  " return make_float3(p_R, p_G, p_B);"
296
298
  )
@@ -10,6 +10,8 @@ Reference: Fusion Fuse SDK (June 2023), Blackmagic Design.
10
10
 
11
11
  from typing import Any, Dict, Optional
12
12
 
13
+ from src.utils.bool_params import coerce_bool
14
+
13
15
  # Maps the public `type` argument to Fusion's internal ClassType enum.
14
16
  CLASS_TYPES = {
15
17
  "tool": "CT_Tool",
@@ -149,7 +151,7 @@ def per_pixel(name: str, options: Optional[Dict[str, Any]] = None) -> str:
149
151
  if n_inputs not in (1, 2):
150
152
  raise ValueError("per_pixel: inputs must be 1 or 2")
151
153
  expression = options.get("expression", " return p1")
152
- expose_amount = bool(options.get("amount", True))
154
+ expose_amount = coerce_bool(options.get("amount"), True)
153
155
 
154
156
  pixel_args = "x, y, p1, p2" if n_inputs == 2 else "x, y, p1"
155
157
 
@@ -1021,7 +1023,7 @@ def channel_op(name: str, options: Optional[Dict[str, Any]] = None) -> str:
1021
1023
  if operation not in valid_ops:
1022
1024
  raise ValueError(f"Invalid channel_op operation '{operation}'. "
1023
1025
  f"Valid: {list(valid_ops)}")
1024
- rgba_only = bool(options.get("rgba_only", True))
1026
+ rgba_only = coerce_bool(options.get("rgba_only"), True)
1025
1027
 
1026
1028
  aux_channels = ""
1027
1029
  aux_doc = ""
@@ -18,6 +18,8 @@ import time
18
18
  from pathlib import Path
19
19
  from typing import Any, Dict, Iterable, List, Optional, Tuple
20
20
 
21
+ from src.utils.bool_params import coerce_bool
22
+
21
23
  from src.utils.media_analysis import (
22
24
  ANALYSIS_VERSION,
23
25
  build_analysis_index,
@@ -641,7 +643,7 @@ def _finish_job_if_complete(
641
643
  _event(conn, job_id, "info", "Batch job completed", counts)
642
644
  conn.commit()
643
645
  summarize_reports(root)
644
- if params.get("auto_build_index", True) and not index_already_refreshed:
646
+ if coerce_bool(params.get("auto_build_index"), True) and not index_already_refreshed:
645
647
  _auto_build_index(conn, root, job_id, "Analysis index rebuilt")
646
648
 
647
649
 
@@ -781,7 +783,7 @@ def run_batch_job_slice(
781
783
  conn.commit()
782
784
 
783
785
  index_refreshed = False
784
- if params.get("auto_build_index", True) and any(
786
+ if coerce_bool(params.get("auto_build_index"), True) and any(
785
787
  row.get("status") in {"succeeded", "skipped"} for row in processed
786
788
  ):
787
789
  _auto_build_index(conn, root, job_id, "Analysis index refreshed after batch slice")
@@ -10,6 +10,8 @@ from __future__ import annotations
10
10
  import re
11
11
  from typing import Any, Callable, Dict, List, Optional, Tuple
12
12
 
13
+ from src.utils.bool_params import coerce_bool
14
+
13
15
 
14
16
  FindClip = Callable[[Any, str], Any]
15
17
 
@@ -323,8 +325,8 @@ def build_multicam_setup_plan(root: Any, params: Dict[str, Any], find_clip: Find
323
325
  if not sync_mode:
324
326
  return None, _err("sync_mode must be stack_start, record_frame, or source_timecode")
325
327
 
326
- include_video = bool(params.get("include_video", params.get("includeVideo", True)))
327
- include_audio = bool(params.get("include_audio", params.get("includeAudio", False)))
328
+ include_video = coerce_bool(params.get("include_video", params.get("includeVideo")), True)
329
+ include_audio = coerce_bool(params.get("include_audio", params.get("includeAudio")), False)
328
330
  if not include_video and not include_audio:
329
331
  return None, _err("At least one of include_video or include_audio must be true")
330
332
  audio_mode = _normalize_audio_mode(params.get("audio_track_mode", params.get("audioTrackMode")), include_audio)
@@ -347,7 +349,7 @@ def build_multicam_setup_plan(root: Any, params: Dict[str, Any], find_clip: Find
347
349
  angles: List[Dict[str, Any]] = []
348
350
  max_video_track = 0
349
351
  max_audio_track = 0
350
- allow_negative = bool(params.get("allow_negative_record_frame", params.get("allowNegativeRecordFrame", False)))
352
+ allow_negative = coerce_bool(params.get("allow_negative_record_frame", params.get("allowNegativeRecordFrame")), False)
351
353
  record_frame_mode = params.get("record_frame_mode", params.get("recordFrameMode", "relative"))
352
354
 
353
355
  for index, raw in enumerate(raw_angles):
@@ -592,7 +592,7 @@ def get_project_info(project_obj) -> Dict[str, Any]:
592
592
  timeline_info = {
593
593
  "name": timeline.GetName(),
594
594
  "isCurrent": timeline.GetName() == current_timeline_name,
595
- "duration": timeline.GetEndFrame() - timeline.GetStartFrame() + 1
595
+ "duration": timeline.GetEndFrame() - timeline.GetStartFrame()
596
596
  }
597
597
  project_info["timelines"].append(timeline_info)
598
598