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.
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "4.8.19"
14
+ VERSION = "4.8.21"
15
15
 
16
16
  import base64
17
17
  import os
@@ -1276,7 +1276,7 @@ def _ai_governance_gate(op: str, p: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1276
1276
  """
1277
1277
  if _ai_governance_mode() != "enforce":
1278
1278
  return None
1279
- if p.get("override_governance") or p.get("overrideGovernance"):
1279
+ if _coerce_bool(p.get("override_governance")) or _coerce_bool(p.get("overrideGovernance")):
1280
1280
  return None
1281
1281
  check = _ai_governance_check(op)
1282
1282
  if not check.get("applies") or not check.get("exceeded"):
@@ -2474,7 +2474,7 @@ def _annotation_target(scope: str, p: Dict[str, Any], tl=None):
2474
2474
  return None, err
2475
2475
  return tl, None
2476
2476
  if scope == "timeline_item":
2477
- if p.get("current"):
2477
+ if _coerce_bool(p.get("current")):
2478
2478
  if tl is None:
2479
2479
  _, tl, err = _get_tl()
2480
2480
  if err:
@@ -2555,7 +2555,7 @@ def _probe_annotations(tl, p: Dict[str, Any]):
2555
2555
 
2556
2556
 
2557
2557
  def _normalize_marker_payload_action(tl, p: Dict[str, Any]):
2558
- marker, err = _marker_add_payload(p, tl=tl, default_to_current=bool(p.get("default_to_current", False)))
2558
+ marker, err = _marker_add_payload(p, tl=tl, default_to_current=_coerce_bool(p.get("default_to_current"), False))
2559
2559
  if err:
2560
2560
  return err
2561
2561
  return {"marker": marker}
@@ -2630,13 +2630,13 @@ def _clear_annotations_by_scope(tl, p: Dict[str, Any]):
2630
2630
  if not _has_method(target, "DeleteMarkerByCustomData"):
2631
2631
  return _err(f"{scope} does not expose DeleteMarkerByCustomData")
2632
2632
  return {"success": bool(target.DeleteMarkerByCustomData(_first_param(p, "custom_data", "customData", default="")))}
2633
- color = p.get("color", "All" if p.get("all", True) else "Blue")
2633
+ color = p.get("color", "All" if _coerce_bool(p.get("all"), True) else "Blue")
2634
2634
  if not _has_method(target, "DeleteMarkersByColor"):
2635
2635
  return _err(f"{scope} does not expose DeleteMarkersByColor")
2636
2636
  result = {"success": bool(target.DeleteMarkersByColor(color)), "color": color}
2637
- if p.get("clear_flags") and _has_method(target, "ClearFlags"):
2637
+ if _coerce_bool(p.get("clear_flags")) and _has_method(target, "ClearFlags"):
2638
2638
  result["flags_cleared"] = bool(target.ClearFlags(p.get("flag_color", "All")))
2639
- if p.get("clear_clip_color") and _has_method(target, "ClearClipColor"):
2639
+ if _coerce_bool(p.get("clear_clip_color")) and _has_method(target, "ClearClipColor"):
2640
2640
  result["clip_color_cleared"] = bool(target.ClearClipColor())
2641
2641
  return result
2642
2642
 
@@ -2647,7 +2647,7 @@ def _export_review_report(tl, p: Dict[str, Any]):
2647
2647
  "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
2648
2648
  "annotations": _probe_annotations(tl, p),
2649
2649
  }
2650
- if p.get("include_capabilities", True):
2650
+ if _coerce_bool(p.get("include_capabilities"), True):
2651
2651
  report["capabilities"] = _annotation_capabilities()
2652
2652
  return report
2653
2653
 
@@ -4859,7 +4859,7 @@ def _append_and_recover_timeline_item(
4859
4859
 
4860
4860
  def _timeline_duplicate_clips_impl(proj, tl, p: Dict[str, Any], *, delete_sources: bool = False, resolve=None):
4861
4861
  ids = p.get("clip_ids") or p.get("ids")
4862
- selected = bool(p.get("selected", False))
4862
+ selected = _coerce_bool(p.get("selected"), False)
4863
4863
  if ids is not None and not isinstance(ids, list):
4864
4864
  return _err("duplicate_clips requires clip_ids (list of timeline item unique IDs)")
4865
4865
  if not ids and not selected:
@@ -4872,7 +4872,7 @@ def _timeline_duplicate_clips_impl(proj, tl, p: Dict[str, Any], *, delete_source
4872
4872
  copy_properties, copy_err = _normalize_copy_properties(p.get("copy_properties", p.get("copyProperties")))
4873
4873
  if copy_err:
4874
4874
  return copy_err
4875
- if p.get("copy_keyframes", p.get("copyKeyframes", False)) and "keyframes" not in copy_properties:
4875
+ if _coerce_bool(p.get("copy_keyframes", p.get("copyKeyframes")), False) and "keyframes" not in copy_properties:
4876
4876
  copy_properties.append("keyframes")
4877
4877
  try:
4878
4878
  offset = int(p.get("record_frame_offset", p.get("recordFrameOffset", 0)))
@@ -4906,7 +4906,7 @@ def _timeline_duplicate_clips_impl(proj, tl, p: Dict[str, Any], *, delete_source
4906
4906
  seen_ids.add(sid)
4907
4907
 
4908
4908
  include_types = _normalize_include_linked(p.get("include_linked", p.get("includeLinked")))
4909
- relink = bool(p.get("relink", p.get("restore_linked", p.get("restoreLinked", bool(include_types)))))
4909
+ relink = _coerce_bool(p.get("relink", p.get("restore_linked", p.get("restoreLinked"))), bool(include_types))
4910
4910
  results: List[Dict[str, Any]] = []
4911
4911
  source_delete_items = []
4912
4912
 
@@ -5518,7 +5518,7 @@ def _timeline_ripple_insert_impl(proj, tl, p: Dict[str, Any], *, resolve=None) -
5518
5518
 
5519
5519
 
5520
5520
  def _range_frames_from_params(tl, p: Dict[str, Any]):
5521
- if p.get("use_mark_in_out", p.get("useMarkInOut", False)):
5521
+ if _coerce_bool(p.get("use_mark_in_out", p.get("useMarkInOut")), False):
5522
5522
  mark = tl.GetMarkInOut() or {}
5523
5523
  mark_type = p.get("mark_type", p.get("markType", "video"))
5524
5524
  if mark_type not in mark:
@@ -5600,7 +5600,7 @@ def _timeline_copy_range_impl(proj, tl, p: Dict[str, Any], *, overwrite: bool =
5600
5600
  copy_properties, copy_err = _normalize_copy_properties(p.get("copy_properties", p.get("copyProperties")))
5601
5601
  if copy_err:
5602
5602
  return copy_err
5603
- if p.get("copy_keyframes", p.get("copyKeyframes", False)) and "keyframes" not in copy_properties:
5603
+ if _coerce_bool(p.get("copy_keyframes", p.get("copyKeyframes")), False) and "keyframes" not in copy_properties:
5604
5604
  copy_properties.append("keyframes")
5605
5605
  mp = proj.GetMediaPool()
5606
5606
  if not mp:
@@ -5863,7 +5863,7 @@ def _timeline_item_probe(item):
5863
5863
 
5864
5864
  def _timeline_probe_edit_kernel_item(tl, p: Dict[str, Any]):
5865
5865
  ids = p.get("clip_ids") or p.get("ids")
5866
- selected = bool(p.get("selected", False))
5866
+ selected = _coerce_bool(p.get("selected"), False)
5867
5867
  items = []
5868
5868
  if ids:
5869
5869
  if not isinstance(ids, list):
@@ -6010,10 +6010,10 @@ def _timeline_set_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
6010
6010
  return _err("set_title_text requires params.text (string)")
6011
6011
 
6012
6012
  property_key = p.get("property_key") or p.get("key")
6013
- as_styled_xml = bool(p.get("as_styled_xml", p.get("styled", False)))
6014
- try_plain_first = bool(p.get("try_plain_first", True))
6015
- readback = bool(p.get("readback", False))
6016
- try_heuristic_keys = bool(p.get("try_heuristic_keys", not bool(property_key)))
6013
+ as_styled_xml = _coerce_bool(p.get("as_styled_xml", p.get("styled")), False)
6014
+ try_plain_first = _coerce_bool(p.get("try_plain_first"), True)
6015
+ readback = _coerce_bool(p.get("readback"), False)
6016
+ try_heuristic_keys = _coerce_bool(p.get("try_heuristic_keys"), not bool(property_key))
6017
6017
 
6018
6018
  if as_styled_xml:
6019
6019
  payload_modes = [(text, "as_given")]
@@ -6069,7 +6069,7 @@ def _timeline_set_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
6069
6069
  # route get_title_text already reads. Deliberately a bare, UNLOCKED
6070
6070
  # SetInput: the comp-lock render bug (api_truth / v2.98.5) eats writes
6071
6071
  # wrapped in Comp.Lock(), and unlocked writes are the safe ones.
6072
- if not bool(p.get("as_styled_xml", p.get("styled", False))):
6072
+ if not _coerce_bool(p.get("as_styled_xml", p.get("styled")), False):
6073
6073
  try:
6074
6074
  if int(item.GetFusionCompCount() or 0) > 0:
6075
6075
  comp = item.GetFusionCompByIndex(1)
@@ -6349,7 +6349,7 @@ def _detect_gaps_overlaps_from_snapshot(snapshot: Dict[str, Any], p: Optional[Di
6349
6349
  def _source_ranges_from_snapshot(snapshot: Dict[str, Any], p: Optional[Dict[str, Any]] = None):
6350
6350
  p = p or {}
6351
6351
  handles = int(p.get("handles", 0))
6352
- merge = bool(p.get("merge", True))
6352
+ merge = _coerce_bool(p.get("merge"), True)
6353
6353
  ranges: Dict[str, List[List[int]]] = {}
6354
6354
  occurrences = []
6355
6355
  for track_type, type_payload in (snapshot.get("tracks") or {}).items():
@@ -6508,7 +6508,7 @@ def _timeline_bulk_set_item_properties(tl, p: Dict[str, Any]) -> Dict[str, Any]:
6508
6508
  if not isinstance(ops, list) or not ops:
6509
6509
  return _err("bulk_set_item_properties requires params.ops: non-empty list of objects")
6510
6510
  dry_run = _coerce_bool(p.get("dry_run"), False)
6511
- readback = bool(p.get("readback", False))
6511
+ readback = _coerce_bool(p.get("readback"), False)
6512
6512
  results = []
6513
6513
  for index, op in enumerate(ops):
6514
6514
  if not isinstance(op, dict):
@@ -6668,7 +6668,7 @@ def _timeline_apply_look_to_items(tl, p: Dict[str, Any]) -> Dict[str, Any]:
6668
6668
  out["copy_grades"] = False
6669
6669
  out["copy_grades_error"] = str(exc)
6670
6670
  out["success"] = (
6671
- all(row.get("set_cdl", True) for row in results)
6671
+ all(_coerce_bool(row.get("set_cdl"), True) for row in results)
6672
6672
  and (source_item is None or bool(out.get("copy_grades")))
6673
6673
  )
6674
6674
  return out
@@ -6812,7 +6812,7 @@ def _timeline_create_variant_from_ranges(proj, source_tl, p: Dict[str, Any]) ->
6812
6812
  # pack=True butts clips together at the end of each track (record_frame is
6813
6813
  # ignored); Resolve packs by actual placed duration, so the result is gap-free
6814
6814
  # even when source and timeline frame rates differ.
6815
- pack = bool(p.get("pack", False))
6815
+ pack = _coerce_bool(p.get("pack"), False)
6816
6816
  built = []
6817
6817
  cursor_by_track: Dict[Tuple[int, int], int] = {}
6818
6818
  max_tracks = {"video": 1, "audio": 1}
@@ -7321,7 +7321,7 @@ def _export_timeline_checked(tl, p: Dict[str, Any]):
7321
7321
  path = p.get("path")
7322
7322
  if not path:
7323
7323
  return _err("path is required")
7324
- if p.get("require_temp_path", True) and not _render_temp_path_ok(path):
7324
+ if _coerce_bool(p.get("require_temp_path"), True) and not _render_temp_path_ok(path):
7325
7325
  return _err("path must be under the system temp directory unless require_temp_path=False")
7326
7326
  folder = os.path.dirname(os.path.abspath(path))
7327
7327
  if folder:
@@ -7702,7 +7702,7 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
7702
7702
  "the media pool afterward (see `relink`)."
7703
7703
  )
7704
7704
  sanitize = sanitize_requested and not is_binary and not is_json
7705
- if not sanitize and p.get("require_temp_path", True) and not _render_temp_path_ok(path):
7705
+ if not sanitize and _coerce_bool(p.get("require_temp_path"), True) and not _render_temp_path_ok(path):
7706
7706
  return _err(
7707
7707
  "path must be under the system temp directory unless require_temp_path=False",
7708
7708
  remediation="Pass require_temp_path=False to import from this location, or "
@@ -7731,7 +7731,7 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
7731
7731
  san_kwargs["search_roots"] = list(search_roots)
7732
7732
  if p.get("relink_min_confidence") is not None:
7733
7733
  san_kwargs["min_confidence"] = float(p["relink_min_confidence"])
7734
- if p.get("verify_visually") or p.get("reference_movie"):
7734
+ if _coerce_bool(p.get("verify_visually")) or p.get("reference_movie"):
7735
7735
  san_kwargs["verify_visually"] = True
7736
7736
  if p.get("reference_movie"):
7737
7737
  san_kwargs["reference_movie"] = p["reference_movie"]
@@ -8376,14 +8376,14 @@ def _compare_timelines(proj, tl, p: Dict[str, Any]):
8376
8376
 
8377
8377
  def _probe_interchange_roundtrip(proj, mp, tl, p: Dict[str, Any]):
8378
8378
  output_dir = p.get("output_dir") or tempfile.mkdtemp(prefix="mcp_conform_roundtrip_")
8379
- if p.get("require_temp_path", True) and not _render_temp_path_ok(output_dir):
8379
+ if _coerce_bool(p.get("require_temp_path"), True) and not _render_temp_path_ok(output_dir):
8380
8380
  return _err("output_dir must be under the system temp directory unless require_temp_path=False")
8381
8381
  os.makedirs(output_dir, exist_ok=True)
8382
8382
  spec = _timeline_export_spec(p, resolve)
8383
8383
  base_name = p.get("name") or f"roundtrip_{str(spec['requested']).lower()}"
8384
8384
  path = p.get("path") or os.path.join(output_dir, base_name + spec["extension"])
8385
8385
  # background is a top-level option; a sub-step export must run synchronously.
8386
- export_result = _export_timeline_checked(tl, {**p, "path": path, "require_temp_path": p.get("require_temp_path", True), "background": False, "async_job": False})
8386
+ export_result = _export_timeline_checked(tl, {**p, "path": path, "require_temp_path": _coerce_bool(p.get("require_temp_path"), True), "background": False, "async_job": False})
8387
8387
  if export_result.get("error") or not export_result.get("success"):
8388
8388
  return {"success": False, "stage": "export", "export": export_result}
8389
8389
  import_path = export_result.get("primary_file") or export_result.get("path") or path
@@ -8391,14 +8391,14 @@ def _probe_interchange_roundtrip(proj, mp, tl, p: Dict[str, Any]):
8391
8391
  requested_key = str(spec["requested"]).lower()
8392
8392
  if "drt" not in requested_key:
8393
8393
  import_options.setdefault("timelineName", p.get("imported_timeline_name", f"{tl.GetName()} {spec['requested']} Roundtrip"))
8394
- import_options.setdefault("importSourceClips", bool(p.get("import_source_clips", False)))
8394
+ import_options.setdefault("importSourceClips", _coerce_bool(p.get("import_source_clips"), False))
8395
8395
  import_result = _import_timeline_checked(
8396
8396
  proj,
8397
8397
  mp,
8398
8398
  {
8399
8399
  "path": import_path,
8400
8400
  "options": import_options,
8401
- "require_temp_path": p.get("require_temp_path", True),
8401
+ "require_temp_path": _coerce_bool(p.get("require_temp_path"), True),
8402
8402
  },
8403
8403
  )
8404
8404
  if import_result.get("error") or not import_result.get("success"):
@@ -8410,7 +8410,7 @@ def _probe_interchange_roundtrip(proj, mp, tl, p: Dict[str, Any]):
8410
8410
  if imported_tl:
8411
8411
  comparison = _compare_timeline_snapshots(_timeline_conform_snapshot(tl, p), _timeline_conform_snapshot(imported_tl, p))
8412
8412
  cleanup_result = None
8413
- if p.get("cleanup_imported", True) and imported_tl:
8413
+ if _coerce_bool(p.get("cleanup_imported"), True) and imported_tl:
8414
8414
  cleanup_result = {"success": bool(mp.DeleteTimelines([imported_tl]))}
8415
8415
  return {
8416
8416
  "success": True,
@@ -8605,13 +8605,13 @@ def _detect_missing_media_from_snapshot(snapshot: Dict[str, Any]):
8605
8605
  def _detect_missing_media(tl, p: Dict[str, Any]):
8606
8606
  snapshot = _timeline_conform_snapshot(tl, {**p, "include_clip_properties": True})
8607
8607
  report = _detect_missing_media_from_snapshot(snapshot)
8608
- if p.get("sanitize_paths") or p.get("sanitized"):
8608
+ if _coerce_bool(p.get("sanitize_paths")) or _coerce_bool(p.get("sanitized")):
8609
8609
  report = dict(report)
8610
8610
  report["missing"] = [
8611
8611
  {
8612
8612
  **row,
8613
8613
  "file_path_sanitized": _sanitize_media_path(row.get("file_path")),
8614
- "file_path": None if p.get("omit_raw_paths", True) else row.get("file_path"),
8614
+ "file_path": None if _coerce_bool(p.get("omit_raw_paths"), True) else row.get("file_path"),
8615
8615
  }
8616
8616
  for row in report.get("missing", [])
8617
8617
  ]
@@ -8630,7 +8630,7 @@ def _bounded_basename_matches(
8630
8630
  max_files = max(1, int(p.get("max_files_scanned", p.get("maxFilesScanned", 50000)) or 50000))
8631
8631
  max_depth_raw = p.get("max_depth", p.get("maxDepth"))
8632
8632
  max_depth = int(max_depth_raw) if max_depth_raw is not None else None
8633
- all_matches = bool(p.get("all_matches", False))
8633
+ all_matches = _coerce_bool(p.get("all_matches"), False)
8634
8634
  matches: List[str] = []
8635
8635
  files_scanned = 0
8636
8636
  dirs_scanned = 0
@@ -8680,7 +8680,7 @@ def _build_relink_plan(tl, p: Dict[str, Any]):
8680
8680
  return _err(f"search_roots must be existing directories: {invalid}")
8681
8681
  missing_report = _detect_missing_media(tl, p)
8682
8682
  diagnosis = missing_report.get("diagnosis") or _missing_media_diagnosis(missing_report.get("missing", []))
8683
- if p.get("skip_search_when_volume_missing", True) and any(
8683
+ if _coerce_bool(p.get("skip_search_when_volume_missing"), True) and any(
8684
8684
  not row.get("mounted") for row in diagnosis.get("missing_volumes", [])
8685
8685
  ):
8686
8686
  return {
@@ -8713,7 +8713,7 @@ def _build_relink_plan(tl, p: Dict[str, Any]):
8713
8713
  **row,
8714
8714
  "wanted_basename": wanted,
8715
8715
  "timeline_occurrence_count": len(rows),
8716
- "candidate_paths": [] if p.get("sanitize_paths") or p.get("sanitized") else matches,
8716
+ "candidate_paths": [] if _coerce_bool(p.get("sanitize_paths")) or _coerce_bool(p.get("sanitized")) else matches,
8717
8717
  "candidate_paths_sanitized": sanitized_matches,
8718
8718
  "candidate_count": len(matches),
8719
8719
  "scan": scan,
@@ -8722,7 +8722,7 @@ def _build_relink_plan(tl, p: Dict[str, Any]):
8722
8722
  return {
8723
8723
  "success": True,
8724
8724
  "dry_run": True,
8725
- "search_roots": [] if p.get("sanitize_paths") or p.get("sanitized") else search_roots,
8725
+ "search_roots": [] if _coerce_bool(p.get("sanitize_paths")) or _coerce_bool(p.get("sanitized")) else search_roots,
8726
8726
  "search_roots_sanitized": [_sanitize_media_path(root, keep_filename=False) for root in search_roots],
8727
8727
  "candidate_count": sum(1 for row in candidates if row["candidate_count"]),
8728
8728
  "missing_count": missing_report.get("missing_count", 0),
@@ -8916,7 +8916,7 @@ def _safe_set_audio_properties(tl, p: Dict[str, Any]):
8916
8916
  row["readback"] = item.GetProperty(key)
8917
8917
  except Exception as exc:
8918
8918
  row["readback_error"] = str(exc)
8919
- if p.get("restore", True) and not isinstance(original.get(key), dict):
8919
+ if _coerce_bool(p.get("restore"), True) and not isinstance(original.get(key), dict):
8920
8920
  try:
8921
8921
  row["restore"] = bool(item.SetProperty(key, original[key]))
8922
8922
  except Exception as exc:
@@ -9361,7 +9361,7 @@ def _transcription_capabilities(mp, p: Dict[str, Any]):
9361
9361
  return _err("clip_ids must be a list")
9362
9362
  clips = [_find_clip(root, str(clip_id)) for clip_id in ids]
9363
9363
  clips = [clip for clip in clips if clip]
9364
- elif p.get("selected"):
9364
+ elif _coerce_bool(p.get("selected")):
9365
9365
  clips = mp.GetSelectedClips() or []
9366
9366
  current_folder = mp.GetCurrentFolder()
9367
9367
  return {
@@ -10300,7 +10300,7 @@ def _media_analysis_records_from_target(mp, p: Dict[str, Any], project=None) ->
10300
10300
 
10301
10301
  elif target_type == "clip":
10302
10302
  clip_id = target.get("clip_id") or p.get("clip_id")
10303
- selected = bool(target.get("selected") or p.get("selected"))
10303
+ selected = bool(_coerce_bool(target.get("selected")) or _coerce_bool(p.get("selected")))
10304
10304
  clips = []
10305
10305
  if selected:
10306
10306
  try:
@@ -10362,7 +10362,7 @@ def _media_analysis_records_from_target(mp, p: Dict[str, Any], project=None) ->
10362
10362
  target.update({"type": "clips", "clip_ids": clip_ids, "skipped": skipped})
10363
10363
  elif target_type == "bin":
10364
10364
  path = target.get("path") or p.get("bin_path") or p.get("path") or "Master"
10365
- recursive = bool(target.get("recursive", p.get("recursive", True)))
10365
+ recursive = _coerce_bool(target.get("recursive", p.get("recursive")), True)
10366
10366
  folder = _navigate_folder(mp, path)
10367
10367
  if not folder:
10368
10368
  return None, target, warnings, _err(f"Folder not found: {path}")
@@ -10370,14 +10370,14 @@ def _media_analysis_records_from_target(mp, p: Dict[str, Any], project=None) ->
10370
10370
  warnings.extend(folder_warnings)
10371
10371
  target.update({"type": "bin", "path": path, "recursive": recursive})
10372
10372
  elif target_type == "project":
10373
- recursive = bool(target.get("recursive", True))
10373
+ recursive = _coerce_bool(target.get("recursive"), True)
10374
10374
  records, folder_warnings = _media_analysis_folder_records(mp.GetRootFolder(), "Master", recursive=recursive)
10375
10375
  warnings.extend(folder_warnings)
10376
10376
  target.update({"type": "project", "recursive": recursive})
10377
10377
  else:
10378
10378
  return None, target, warnings, _err("target.type must be one of file, clip, clips, bin, project, sequence, timeline")
10379
10379
 
10380
- records, duplicate_count = _media_analysis_dedupe_records(records, bool(p.get("include_duplicates", target.get("include_duplicates", False))))
10380
+ records, duplicate_count = _media_analysis_dedupe_records(records, _coerce_bool(p.get("include_duplicates", target.get("include_duplicates")), False))
10381
10381
  if duplicate_count:
10382
10382
  warnings.append(f"Deduped {duplicate_count} repeated source media reference(s)")
10383
10383
  if not records:
@@ -12489,7 +12489,7 @@ def _media_analysis_metadata_writeback_enabled(p: Dict[str, Any]) -> bool:
12489
12489
  "writeToResolve",
12490
12490
  )
12491
12491
  if raw is None:
12492
- return bool(_media_analysis_effective_preferences().get("metadata_writeback_default", True))
12492
+ return _coerce_bool(_media_analysis_effective_preferences().get("metadata_writeback_default"), True)
12493
12493
  return _media_analysis_bool(raw, True)
12494
12494
 
12495
12495
 
@@ -12752,7 +12752,7 @@ def _media_analysis_apply_setup_defaults(action: str, p: Dict[str, Any]) -> Dict
12752
12752
  "write_to_resolve",
12753
12753
  "writeToResolve",
12754
12754
  ):
12755
- out["publish_metadata"] = bool(prefs.get("metadata_writeback_default", True))
12755
+ out["publish_metadata"] = _coerce_bool(prefs.get("metadata_writeback_default"), True)
12756
12756
  applied["metadata_writeback_default"] = out["publish_metadata"]
12757
12757
 
12758
12758
  persistence = prefs.get("analysis_persistence")
@@ -13325,7 +13325,7 @@ def _media_pool_probe(mp, p: Dict[str, Any]):
13325
13325
  def _media_pool_probe_ingest_items(mp, p: Dict[str, Any]):
13326
13326
  root = mp.GetRootFolder()
13327
13327
  ids = p.get("clip_ids") or p.get("ids")
13328
- selected = bool(p.get("selected", False))
13328
+ selected = _coerce_bool(p.get("selected"), False)
13329
13329
  clips = []
13330
13330
  warnings = []
13331
13331
  if ids:
@@ -13376,7 +13376,7 @@ def _string_list_param(p: Dict[str, Any], key: str):
13376
13376
 
13377
13377
  def _clips_from_params(root, mp, p: Dict[str, Any], *, key: str = "clip_ids"):
13378
13378
  ids = p.get(key) or p.get("ids")
13379
- selected = bool(p.get("selected", False))
13379
+ selected = _coerce_bool(p.get("selected"), False)
13380
13380
  clips = []
13381
13381
  missing = []
13382
13382
  if ids:
@@ -13587,7 +13587,7 @@ def _copy_metadata(root, p: Dict[str, Any]):
13587
13587
  keys = set(p["keys"])
13588
13588
  metadata = {key: value for key, value in metadata.items() if key in keys}
13589
13589
  third_party = {}
13590
- if p.get("include_third_party", True):
13590
+ if _coerce_bool(p.get("include_third_party"), True):
13591
13591
  third_party = source.GetThirdPartyMetadata("") or {}
13592
13592
  results = []
13593
13593
  for target_id in target_ids:
@@ -14310,7 +14310,7 @@ def _link_proxy_checked(root, p: Dict[str, Any]):
14310
14310
  return missing
14311
14311
  check_compatibility = bool(
14312
14312
  p.get("check_compatibility")
14313
- or p.get("require_compatible")
14313
+ or _coerce_bool(p.get("require_compatible"))
14314
14314
  or p.get("expected_codec")
14315
14315
  or p.get("expected_profile")
14316
14316
  or p.get("codec")
@@ -14329,7 +14329,7 @@ def _link_proxy_checked(root, p: Dict[str, Any]):
14329
14329
  "proxy_path": proxy_path,
14330
14330
  "dry_run": _coerce_bool(p.get("dry_run")),
14331
14331
  "check_compatibility": check_compatibility,
14332
- "require_compatible": bool(p.get("require_compatible")),
14332
+ "require_compatible": _coerce_bool(p.get("require_compatible")),
14333
14333
  "expected_codec": p.get("expected_codec") or p.get("codec"),
14334
14334
  "expected_profile": p.get("expected_profile") or p.get("profile"),
14335
14335
  }
@@ -14363,7 +14363,7 @@ def _link_proxy_checked(root, p: Dict[str, Any]):
14363
14363
  ),
14364
14364
  )
14365
14365
  return out
14366
- if p.get("require_compatible") and compatibility is not None and not compatibility.get("compatible"):
14366
+ if _coerce_bool(p.get("require_compatible")) and compatibility is not None and not compatibility.get("compatible"):
14367
14367
  out = _err(
14368
14368
  "Proxy media is not compatible with the source clip; refusing LinkProxyMedia",
14369
14369
  code="PROXY_INCOMPATIBLE",
@@ -14697,7 +14697,7 @@ def _media_pool_boundary_report(mp, p: Dict[str, Any]):
14697
14697
  "capabilities": _media_pool_ingest_capabilities(),
14698
14698
  "media_pool": _media_pool_probe(mp, {"depth": p.get("depth", 1)}),
14699
14699
  }
14700
- if p.get("clip_ids") or p.get("selected"):
14700
+ if p.get("clip_ids") or _coerce_bool(p.get("selected")):
14701
14701
  report["items"] = _media_pool_probe_ingest_items(mp, p)
14702
14702
  return report
14703
14703
 
@@ -18563,7 +18563,7 @@ def _probe_project_settings(project, p: Dict[str, Any]) -> Dict[str, Any]:
18563
18563
  except Exception as exc:
18564
18564
  row["error"] = str(exc)
18565
18565
  out["candidate_settings"][key] = row
18566
- if p.get("try_write"):
18566
+ if _coerce_bool(p.get("try_write")):
18567
18567
  settings = {
18568
18568
  key: row["value"]
18569
18569
  for key, row in out["candidate_settings"].items()
@@ -18591,7 +18591,7 @@ def _safe_set_project_settings(project, p: Dict[str, Any]) -> Dict[str, Any]:
18591
18591
  except Exception as exc:
18592
18592
  original[key] = {"error": str(exc)}
18593
18593
  if _coerce_bool(p.get("dry_run")):
18594
- return _ok(would_set=settings, original=original, restore=p.get("restore", True))
18594
+ return _ok(would_set=settings, original=original, restore=_coerce_bool(p.get("restore"), True))
18595
18595
  results: Dict[str, Any] = {}
18596
18596
  for key, value in settings.items():
18597
18597
  row: Dict[str, Any] = {"requested": value, "original": original.get(key)}
@@ -18604,7 +18604,7 @@ def _safe_set_project_settings(project, p: Dict[str, Any]) -> Dict[str, Any]:
18604
18604
  row["readback"] = _ser(project.GetSetting(key))
18605
18605
  except Exception as exc:
18606
18606
  row["readback_error"] = str(exc)
18607
- if p.get("restore", True) and not isinstance(original.get(key), dict):
18607
+ if _coerce_bool(p.get("restore"), True) and not isinstance(original.get(key), dict):
18608
18608
  try:
18609
18609
  row["restore"] = bool(project.SetSetting(key, original[key]))
18610
18610
  row["restored_value"] = _ser(project.GetSetting(key))
@@ -18617,7 +18617,7 @@ def _safe_set_project_settings(project, p: Dict[str, Any]) -> Dict[str, Any]:
18617
18617
 
18618
18618
  def _safe_project_create(pm, resolve_obj, p: Dict[str, Any]) -> Dict[str, Any]:
18619
18619
  name = p.get("name")
18620
- invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
18620
+ invalid = _require_disposable_project_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
18621
18621
  if invalid:
18622
18622
  return invalid
18623
18623
  media_location_path = p.get("media_location_path") or p.get("mediaLocationPath")
@@ -18625,7 +18625,7 @@ def _safe_project_create(pm, resolve_obj, p: Dict[str, Any]) -> Dict[str, Any]:
18625
18625
  path_err = _project_path_guard(
18626
18626
  media_location_path,
18627
18627
  field="media_location_path",
18628
- require_temp_path=p.get("require_temp_media_location", True),
18628
+ require_temp_path=_coerce_bool(p.get("require_temp_media_location"), True),
18629
18629
  )
18630
18630
  if path_err:
18631
18631
  return path_err
@@ -18640,15 +18640,15 @@ def _safe_project_create(pm, resolve_obj, p: Dict[str, Any]) -> Dict[str, Any]:
18640
18640
 
18641
18641
  def _safe_project_export(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18642
18642
  name = p.get("name")
18643
- invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
18643
+ invalid = _require_disposable_project_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
18644
18644
  if invalid:
18645
18645
  return invalid
18646
18646
  path = p.get("path")
18647
- path_err = _project_path_guard(path, require_temp_path=p.get("require_temp_path", True))
18647
+ path_err = _project_path_guard(path, require_temp_path=_coerce_bool(p.get("require_temp_path"), True))
18648
18648
  if path_err:
18649
18649
  return path_err
18650
18650
  os.makedirs(_project_path_parent(path), exist_ok=True)
18651
- with_stills = bool(p.get("with_stills_and_luts", False))
18651
+ with_stills = _coerce_bool(p.get("with_stills_and_luts"), False)
18652
18652
  if _coerce_bool(p.get("dry_run")):
18653
18653
  return _ok(would_export=True, name=name, path=path, with_stills_and_luts=with_stills)
18654
18654
  return {"success": bool(pm.ExportProject(name, path, with_stills))}
@@ -18656,13 +18656,13 @@ def _safe_project_export(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18656
18656
 
18657
18657
  def _safe_project_import(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18658
18658
  path = p.get("path")
18659
- path_err = _project_path_guard(path, require_temp_path=p.get("require_temp_path", True))
18659
+ path_err = _project_path_guard(path, require_temp_path=_coerce_bool(p.get("require_temp_path"), True))
18660
18660
  if path_err:
18661
18661
  return path_err
18662
18662
  if not os.path.exists(path):
18663
18663
  return _err(f"path does not exist: {path}")
18664
18664
  name = p.get("name")
18665
- invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
18665
+ invalid = _require_disposable_project_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
18666
18666
  if invalid:
18667
18667
  return invalid
18668
18668
  if _coerce_bool(p.get("dry_run")):
@@ -18672,11 +18672,11 @@ def _safe_project_import(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18672
18672
 
18673
18673
  def _safe_project_archive(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18674
18674
  name = p.get("name")
18675
- invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
18675
+ invalid = _require_disposable_project_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
18676
18676
  if invalid:
18677
18677
  return invalid
18678
18678
  path = p.get("path")
18679
- path_err = _project_path_guard(path, require_temp_path=p.get("require_temp_path", True))
18679
+ path_err = _project_path_guard(path, require_temp_path=_coerce_bool(p.get("require_temp_path"), True))
18680
18680
  if path_err:
18681
18681
  return path_err
18682
18682
  flags, flag_err = archive_guard.read_flags(p)
@@ -18707,13 +18707,13 @@ def _safe_project_archive(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18707
18707
 
18708
18708
  def _safe_project_restore(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18709
18709
  path = p.get("path")
18710
- path_err = _project_path_guard(path, require_temp_path=p.get("require_temp_path", True))
18710
+ path_err = _project_path_guard(path, require_temp_path=_coerce_bool(p.get("require_temp_path"), True))
18711
18711
  if path_err:
18712
18712
  return path_err
18713
18713
  if not os.path.exists(path):
18714
18714
  return _err(f"path does not exist: {path}")
18715
18715
  name = p.get("name")
18716
- invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
18716
+ invalid = _require_disposable_project_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
18717
18717
  if invalid:
18718
18718
  return invalid
18719
18719
  if _coerce_bool(p.get("dry_run")):
@@ -18723,7 +18723,7 @@ def _safe_project_restore(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18723
18723
 
18724
18724
  def _safe_project_delete(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18725
18725
  name = p.get("name")
18726
- invalid = _require_disposable_project_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
18726
+ invalid = _require_disposable_project_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
18727
18727
  if invalid:
18728
18728
  return invalid
18729
18729
  if _coerce_bool(p.get("dry_run")):
@@ -18736,7 +18736,7 @@ def _safe_project_delete(pm, p: Dict[str, Any]) -> Dict[str, Any]:
18736
18736
  if current_name == name:
18737
18737
  if not _coerce_bool(p.get("close_current")):
18738
18738
  return _err("Refusing to delete the currently open project; pass close_current=True")
18739
- saved = bool(pm.SaveProject()) if p.get("save_current", True) else None
18739
+ saved = bool(pm.SaveProject()) if _coerce_bool(p.get("save_current"), True) else None
18740
18740
  closed = bool(pm.CloseProject(current))
18741
18741
  if not closed:
18742
18742
  return _err(f"Failed to close current project '{name}' before delete")
@@ -19112,13 +19112,13 @@ def _spec_action(r, pm, action: str, p: Dict[str, Any]) -> Dict[str, Any]:
19112
19112
  return _ok(project=spec.project,
19113
19113
  **_project_spec.apply_spec(spec, executor, dry_run=True))
19114
19114
  # apply_spec
19115
- run_hooks = bool(p.get("run_hooks", False))
19115
+ run_hooks = _coerce_bool(p.get("run_hooks"), False)
19116
19116
  try:
19117
19117
  result = _project_spec.apply_spec(
19118
19118
  spec, executor,
19119
19119
  dry_run=_coerce_bool(p.get("dry_run"), False),
19120
19120
  run_hooks=run_hooks,
19121
- continue_on_error=bool(p.get("continue_on_error", False)),
19121
+ continue_on_error=_coerce_bool(p.get("continue_on_error"), False),
19122
19122
  run_hook=_make_spec_hook_runner() if run_hooks else None,
19123
19123
  )
19124
19124
  except _project_spec.SpecError as exc:
@@ -19473,7 +19473,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
19473
19473
  except Exception:
19474
19474
  rendering = False
19475
19475
  if rendering:
19476
- if not p.get("stop_render"):
19476
+ if not _coerce_bool(p.get("stop_render")):
19477
19477
  return _err(
19478
19478
  "A render is in progress on this project. Closing now would "
19479
19479
  "orphan the render and wedge Resolve's render pipeline — the "
@@ -19519,7 +19519,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
19519
19519
  })
19520
19520
  if err:
19521
19521
  return _err(err)
19522
- return {"success": bool(pm.ExportProject(p["name"], p["path"], p.get("with_stills_and_luts", True)))}
19522
+ return {"success": bool(pm.ExportProject(p["name"], p["path"], _coerce_bool(p.get("with_stills_and_luts"), True)))}
19523
19523
  elif action == "archive":
19524
19524
  err, _clean = _validate_params(p, {
19525
19525
  "name": {"type": str, "required": True, "non_empty": True},
@@ -19787,7 +19787,7 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
19787
19787
  elif action == "project_summary":
19788
19788
  return _project_summary(
19789
19789
  proj,
19790
- include_clips=bool(p.get("include_clips")),
19790
+ include_clips=_coerce_bool(p.get("include_clips")),
19791
19791
  clip_limit=int(p.get("clip_limit", 50)),
19792
19792
  )
19793
19793
  elif action == "load_burnin_preset":
@@ -20214,7 +20214,7 @@ def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_
20214
20214
  def _validate_render_settings_action(p: Dict[str, Any]):
20215
20215
  validation, err = _validate_render_settings_payload(
20216
20216
  p.get("settings"),
20217
- require_temp_target=bool(p.get("require_temp_target", False)),
20217
+ require_temp_target=_coerce_bool(p.get("require_temp_target"), False),
20218
20218
  )
20219
20219
  if err:
20220
20220
  return err
@@ -20238,7 +20238,7 @@ def _safe_set_render_settings(proj, p: Dict[str, Any]):
20238
20238
  settings = p.get("settings")
20239
20239
  validation, err = _validate_render_settings_payload(
20240
20240
  settings,
20241
- require_temp_target=bool(p.get("require_temp_target", False)),
20241
+ require_temp_target=_coerce_bool(p.get("require_temp_target"), False),
20242
20242
  )
20243
20243
  if err:
20244
20244
  return err
@@ -20256,7 +20256,7 @@ def _safe_set_render_settings(proj, p: Dict[str, Any]):
20256
20256
  "after": after_settings,
20257
20257
  "diff": _settings_diff(settings, after_settings),
20258
20258
  }
20259
- if p.get("restore") and isinstance(before.get("settings"), dict):
20259
+ if _coerce_bool(p.get("restore")) and isinstance(before.get("settings"), dict):
20260
20260
  result["restore_success"] = bool(proj.SetRenderSettings(before["settings"]))
20261
20261
  return result
20262
20262
 
@@ -20267,13 +20267,13 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
20267
20267
  return _err("target_dir or settings.TargetDir is required")
20268
20268
  if not os.path.isdir(target_dir):
20269
20269
  return _err(f"target_dir does not exist: {target_dir}")
20270
- if p.get("require_temp_target", True) and not _render_temp_path_ok(target_dir):
20270
+ if _coerce_bool(p.get("require_temp_target"), True) and not _render_temp_path_ok(target_dir):
20271
20271
  return _err("target_dir must be under the system temp directory unless require_temp_target=False")
20272
20272
  settings = dict(p.get("settings") or {})
20273
20273
  settings.setdefault("TargetDir", target_dir)
20274
20274
  if p.get("custom_name"):
20275
20275
  settings["CustomName"] = p["custom_name"]
20276
- validation, err = _validate_render_settings_payload(settings, require_temp_target=p.get("require_temp_target", True))
20276
+ validation, err = _validate_render_settings_payload(settings, require_temp_target=_coerce_bool(p.get("require_temp_target"), True))
20277
20277
  if err:
20278
20278
  return err
20279
20279
  if not validation["valid"]:
@@ -20516,7 +20516,7 @@ def _list_delivery_targets(proj, p: Dict[str, Any]):
20516
20516
  extras = _delivery_target_extras()
20517
20517
  listing = _delivery_targets.list_targets(extras, tier=tier)
20518
20518
 
20519
- if p.get("check_availability"):
20519
+ if _coerce_bool(p.get("check_availability")):
20520
20520
  # Resolve every target against the live matrix so the caller sees what
20521
20521
  # this machine/license can actually render, not just what ships.
20522
20522
  formats = _render_formats(proj)
@@ -20543,7 +20543,7 @@ def _list_delivery_targets(proj, p: Dict[str, Any]):
20543
20543
  targets=listing,
20544
20544
  schema_version=_delivery_targets.SCHEMA_VERSION,
20545
20545
  tiers=list(_delivery_targets.TIERS),
20546
- availability_checked=bool(p.get("check_availability")),
20546
+ availability_checked=_coerce_bool(p.get("check_availability")),
20547
20547
  )
20548
20548
 
20549
20549
 
@@ -20618,7 +20618,7 @@ def _safe_quick_export(proj, p: Dict[str, Any]):
20618
20618
  params["EnableUpload"] = False
20619
20619
  validation, err = _validate_render_settings_payload(
20620
20620
  {key: value for key, value in params.items() if key in _RENDER_SETTING_KEYS},
20621
- require_temp_target=bool(p.get("require_temp_target", True)),
20621
+ require_temp_target=_coerce_bool(p.get("require_temp_target"), True),
20622
20622
  )
20623
20623
  if err:
20624
20624
  return err
@@ -20667,9 +20667,9 @@ def _export_render_boundary_report(proj, p: Dict[str, Any]):
20667
20667
  "capabilities": _render_capabilities(proj),
20668
20668
  "settings": _render_settings_snapshot(proj),
20669
20669
  }
20670
- if p.get("include_matrix", True):
20670
+ if _coerce_bool(p.get("include_matrix"), True):
20671
20671
  report["matrix"] = _probe_render_matrix(proj, {"max_pairs": p.get("max_pairs")})
20672
- if p.get("include_quick_export", True):
20672
+ if _coerce_bool(p.get("include_quick_export"), True):
20673
20673
  report["quick_export"] = _quick_export_capabilities(proj)
20674
20674
  return report
20675
20675
 
@@ -20945,7 +20945,7 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
20945
20945
  return result
20946
20946
  elif action == "start":
20947
20947
  job_ids = p.get("job_ids")
20948
- interactive = p.get("interactive", False)
20948
+ interactive = _coerce_bool(p.get("interactive"), False)
20949
20949
  if job_ids:
20950
20950
  return {"success": bool(proj.StartRendering(job_ids, interactive))}
20951
20951
  return {"success": bool(proj.StartRendering(interactive))}
@@ -21946,7 +21946,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
21946
21946
  # when the clip loads in source viewer. Caller passes seconds; we convert
21947
21947
  # to frames using the clip's fps from properties.
21948
21948
  mark_set: Optional[Dict[str, Any]] = None
21949
- if p.get("clear_marks") or p.get("clearMarks"):
21949
+ if _coerce_bool(p.get("clear_marks")) or _coerce_bool(p.get("clearMarks")):
21950
21950
  try:
21951
21951
  clip.ClearMarkInOut(p.get("mark_type") or "all")
21952
21952
  mark_set = {"cleared": True}
@@ -22007,7 +22007,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
22007
22007
 
22008
22008
  # Bring Resolve to the foreground so the editor doesn't have to
22009
22009
  # alt-tab. Default on; pass focus_app=false to suppress.
22010
- focus_app = p.get("focus_app", p.get("focusApp", True))
22010
+ focus_app = _coerce_bool(p.get("focus_app", p.get("focusApp")), True)
22011
22011
  focus_result: Optional[Dict[str, Any]] = None
22012
22012
  if focus_app:
22013
22013
  focus_result = _activate_resolve_window()
@@ -22017,7 +22017,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
22017
22017
  # call, so we send the keyboard equivalent of "Go to In" once the
22018
22018
  # app is in the foreground. Default on when a mark was set.
22019
22019
  jump_result: Optional[Dict[str, Any]] = None
22020
- wants_jump = p.get("jump_to_mark_in", p.get("jumpToMarkIn", True))
22020
+ wants_jump = _coerce_bool(p.get("jump_to_mark_in", p.get("jumpToMarkIn")), True)
22021
22021
  applied_mark = bool(mark_set and mark_set.get("applied"))
22022
22022
  if wants_jump and applied_mark and focus_result and focus_result.get("activated"):
22023
22023
  jump_result = _send_resolve_keystroke_go_to_mark_in()
@@ -22167,12 +22167,12 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
22167
22167
  # Segment timecodes are SOURCE timecodes, not timeline positions.
22168
22168
  if _has_method(clip, "GetTranscription"):
22169
22169
  try:
22170
- full = clip.GetTranscription(bool(p.get("use_nested_clip_transcription", False)))
22170
+ full = clip.GetTranscription(_coerce_bool(p.get("use_nested_clip_transcription"), False))
22171
22171
  except Exception:
22172
22172
  full = None
22173
22173
  segs = full.get("segments") if isinstance(full, dict) else None
22174
22174
  if segs:
22175
- if not p.get("include_words"):
22175
+ if not _coerce_bool(p.get("include_words")):
22176
22176
  # `words` is several times the bulk of the segment text and
22177
22177
  # most callers want segment-level timing. Opt in for it.
22178
22178
  segs = [{k: v for k, v in seg.items() if k != "words"}
@@ -22799,7 +22799,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
22799
22799
  if vctx is None:
22800
22800
  return _err("No project context — open a Resolve project first")
22801
22801
  _r, _proj, project_root, _name = vctx
22802
- session_only = bool(p.get("session_only", False))
22802
+ session_only = _coerce_bool(p.get("session_only"), False)
22803
22803
  session_id = _AI_LEDGER_SESSION_ID if session_only else None
22804
22804
  limit = _safe_int(p.get("limit"), 50, minimum=1, maximum=1000)
22805
22805
  return {
@@ -22913,7 +22913,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
22913
22913
  project_id=project_id,
22914
22914
  analysis_root=p.get("analysis_root"),
22915
22915
  source_paths=p.get("source_paths") or p.get("sourcePaths") or [],
22916
- create=bool(p.get("create", False)),
22916
+ create=_coerce_bool(p.get("create"), False),
22917
22917
  )
22918
22918
 
22919
22919
  if action in {
@@ -22978,7 +22978,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
22978
22978
  project_id=project_id,
22979
22979
  analysis_root=p.get("analysis_root"),
22980
22980
  source_paths=[],
22981
- create=bool(p.get("create", False)),
22981
+ create=_coerce_bool(p.get("create"), False),
22982
22982
  )
22983
22983
  if not root.get("success"):
22984
22984
  return root
@@ -23091,7 +23091,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
23091
23091
  end_seconds=end,
23092
23092
  match_word=p.get("match_word") or p.get("matchWord") or p.get("word"),
23093
23093
  context_seconds=_opt_number(p.get("context_seconds", p.get("contextSeconds"))) or 2.0,
23094
- include_curve_values=bool(p.get("include_curve_values", p.get("includeCurveValues", False))),
23094
+ include_curve_values=_coerce_bool(p.get("include_curve_values", p.get("includeCurveValues")), False),
23095
23095
  limit=int(_opt_number(p.get("limit")) or 20),
23096
23096
  )
23097
23097
  if action == "timeline_strata":
@@ -23116,7 +23116,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
23116
23116
  record_start_frame=int(start_frame) if start_frame is not None else None,
23117
23117
  record_end_frame=int(end_frame) if end_frame is not None else None,
23118
23118
  fps=fps,
23119
- include_curve_values=bool(p.get("include_curve_values", p.get("includeCurveValues", False))),
23119
+ include_curve_values=_coerce_bool(p.get("include_curve_values", p.get("includeCurveValues")), False),
23120
23120
  )
23121
23121
  if action == "plan_story_beats":
23122
23122
  from src.utils import strata_story
@@ -23319,7 +23319,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
23319
23319
  if not job_id:
23320
23320
  return _err("resume_batch_job requires job_id")
23321
23321
  return resume_media_analysis_batch_job(project_root, str(job_id))
23322
- return cleanup_media_analysis_artifacts(project_root, frames_only=bool(p.get("frames_only", True)))
23322
+ return cleanup_media_analysis_artifacts(project_root, frames_only=_coerce_bool(p.get("frames_only"), True))
23323
23323
 
23324
23324
  if action == "review_timeline_markers":
23325
23325
  tl = proj.GetCurrentTimeline()
@@ -23514,11 +23514,11 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
23514
23514
  if action == "analyze_file":
23515
23515
  target.update({"type": "file", "path": p.get("path") or p.get("file_path") or p.get("filePath") or target.get("path")})
23516
23516
  elif action == "analyze_clip":
23517
- target.update({"type": "clip", "clip_id": p.get("clip_id") or target.get("clip_id"), "selected": p.get("selected", target.get("selected", False))})
23517
+ target.update({"type": "clip", "clip_id": p.get("clip_id") or target.get("clip_id"), "selected": _coerce_bool(p.get("selected", target.get("selected")), False)})
23518
23518
  elif action == "analyze_bin":
23519
- target.update({"type": "bin", "path": p.get("bin_path") or p.get("path") or target.get("path") or "Master", "recursive": p.get("recursive", target.get("recursive", True))})
23519
+ target.update({"type": "bin", "path": p.get("bin_path") or p.get("path") or target.get("path") or "Master", "recursive": _coerce_bool(p.get("recursive", target.get("recursive")), True)})
23520
23520
  elif action == "analyze_project":
23521
- target.update({"type": "project", "recursive": p.get("recursive", target.get("recursive", True))})
23521
+ target.update({"type": "project", "recursive": _coerce_bool(p.get("recursive", target.get("recursive")), True)})
23522
23522
  elif action in {"analyze_timeline", "analyze_sequence"}:
23523
23523
  target.update({
23524
23524
  "type": "sequence",
@@ -24047,7 +24047,7 @@ def _attach_audit_report(result: Dict[str, Any], params: Dict[str, Any]) -> Dict
24047
24047
  if not isinstance(result, dict) or not result.get("success"):
24048
24048
  return result
24049
24049
  result["report_available"] = "pass include_report=true for a Markdown report"
24050
- flag = params.get("include_report", params.get("includeReport", False))
24050
+ flag = _coerce_bool(params.get("include_report", params.get("includeReport")), False)
24051
24051
  if str(flag).strip().lower() in {"true", "1", "yes", "on"}:
24052
24052
  try:
24053
24053
  result["report_markdown"] = _edit_report_mod.render_any(result)
@@ -24401,7 +24401,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
24401
24401
  target_ratio=p.get("target_ratio") or p.get("targetRatio"),
24402
24402
  min_pause_seconds=float(p.get("min_pause_seconds") or p.get("minPauseSeconds") or _edit_engine_mod.DEFAULT_MIN_PAUSE_SECONDS),
24403
24403
  handle_seconds=float(p.get("handle_seconds") or p.get("handleSeconds") or _edit_engine_mod.DEFAULT_HANDLE_SECONDS),
24404
- include_audio=str(p.get("include_audio", p.get("includeAudio", True))).strip().lower() not in {"false", "0", "no", "none", "off"},
24404
+ include_audio=str(_coerce_bool(p.get("include_audio", p.get("includeAudio")), True)).strip().lower() not in {"false", "0", "no", "none", "off"},
24405
24405
  )
24406
24406
 
24407
24407
  if action == "generate_captions":
@@ -24430,7 +24430,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
24430
24430
  _result = _captions_mod.generate(
24431
24431
  _words,
24432
24432
  fmt=str(p.get("format") or p.get("fmt") or "srt").lower(),
24433
- with_chapters=str(p.get("with_chapters", p.get("withChapters", False))).strip().lower() in {"true", "1", "yes", "on"},
24433
+ with_chapters=str(_coerce_bool(p.get("with_chapters", p.get("withChapters")), False)).strip().lower() in {"true", "1", "yes", "on"},
24434
24434
  **_opts,
24435
24435
  )
24436
24436
  except _captions_mod.CaptionError as exc:
@@ -24445,9 +24445,9 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
24445
24445
  return _edit_engine_mod.plan_transcript_tighten(
24446
24446
  project_root,
24447
24447
  clip_ref=p.get("clip_ref") or p.get("clipRef") or p.get("clip_id"),
24448
- remove_fillers=str(p.get("remove_fillers", p.get("removeFillers", True))).strip().lower() not in {"false", "0", "no", "off"},
24449
- remove_false_starts=str(p.get("remove_false_starts", p.get("removeFalseStarts", True))).strip().lower() not in {"false", "0", "no", "off"},
24450
- collapse_pauses=str(p.get("collapse_pauses", p.get("collapsePauses", True))).strip().lower() not in {"false", "0", "no", "off"},
24448
+ remove_fillers=str(_coerce_bool(p.get("remove_fillers", p.get("removeFillers")), True)).strip().lower() not in {"false", "0", "no", "off"},
24449
+ remove_false_starts=str(_coerce_bool(p.get("remove_false_starts", p.get("removeFalseStarts")), True)).strip().lower() not in {"false", "0", "no", "off"},
24450
+ collapse_pauses=str(_coerce_bool(p.get("collapse_pauses", p.get("collapsePauses")), True)).strip().lower() not in {"false", "0", "no", "off"},
24451
24451
  max_pause=float(p.get("max_pause", p.get("maxPause", _transcript_edit_defaults.DEFAULT_MAX_PAUSE_S))),
24452
24452
  handle=float(p.get("handle", _transcript_edit_defaults.DEFAULT_HANDLE_S)),
24453
24453
  min_cut=float(p.get("min_cut", p.get("minCut", _transcript_edit_defaults.DEFAULT_MIN_CUT_S))),
@@ -24509,7 +24509,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
24509
24509
  min_strip_frames=float(p.get("min_strip_frames") if p.get("min_strip_frames") is not None else p.get("minStripFrames") if p.get("minStripFrames") is not None else _edit_engine_mod.DEFAULT_SILENCE_MIN_STRIP_FRAMES),
24510
24510
  pre_head_frames=float(p.get("pre_head_frames") if p.get("pre_head_frames") is not None else p.get("preHeadFrames") if p.get("preHeadFrames") is not None else _edit_engine_mod.DEFAULT_SILENCE_PRE_HEAD_FRAMES),
24511
24511
  post_tail_frames=float(p.get("post_tail_frames") if p.get("post_tail_frames") is not None else p.get("postTailFrames") if p.get("postTailFrames") is not None else _edit_engine_mod.DEFAULT_SILENCE_POST_TAIL_FRAMES),
24512
- include_audio=str(p.get("include_audio", p.get("includeAudio", True))).strip().lower() not in {"false", "0", "no", "none", "off"},
24512
+ include_audio=str(_coerce_bool(p.get("include_audio", p.get("includeAudio")), True)).strip().lower() not in {"false", "0", "no", "none", "off"},
24513
24513
  )
24514
24514
 
24515
24515
  if action == "rule_of_six_audit":
@@ -24744,7 +24744,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
24744
24744
  code="MISSING_CANDIDATES", category="invalid_input")
24745
24745
  return _edit_engine_mod.plan_broll(
24746
24746
  beats=beats, candidates=candidates,
24747
- allow_reuse=str(p.get("allow_reuse", p.get("allowReuse", False))).strip().lower() in {"true", "1", "yes", "on"},
24747
+ allow_reuse=str(_coerce_bool(p.get("allow_reuse", p.get("allowReuse")), False)).strip().lower() in {"true", "1", "yes", "on"},
24748
24748
  )
24749
24749
 
24750
24750
  if action == "plan_turnover":
@@ -26085,7 +26085,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
26085
26085
  mpi = tl.GetMediaPoolItem()
26086
26086
  return {"name": mpi.GetName(), "id": mpi.GetUniqueId()} if mpi else {"name": None, "id": None}
26087
26087
  elif action == "get_transcript":
26088
- return _timeline_transcript(tl, with_timecodes=bool(p.get("with_timecodes")))
26088
+ return _timeline_transcript(tl, with_timecodes=_coerce_bool(p.get("with_timecodes")))
26089
26089
  elif action == "propose_cuts":
26090
26090
  # Dry-run only: detect mechanical cuts (fillers, long pauses, repeats)
26091
26091
  # from the timeline's subtitle transcript. Proposes; never edits.
@@ -27379,7 +27379,7 @@ def _grade_item_snapshot(item, proj=None, p: Optional[Dict[str, Any]] = None):
27379
27379
  out["errors"].append({"method": method_name, "error": str(exc)})
27380
27380
  try:
27381
27381
  graph = item.GetNodeGraph(p["layer_index"]) if "layer_index" in p else item.GetNodeGraph()
27382
- out["node_graph"] = _graph_snapshot(graph, include_nodes=p.get("include_nodes", True), max_nodes=p.get("max_nodes", 3))
27382
+ out["node_graph"] = _graph_snapshot(graph, include_nodes=_coerce_bool(p.get("include_nodes"), True), max_nodes=p.get("max_nodes", 3))
27383
27383
  except Exception as exc:
27384
27384
  out["node_graph"] = {"available": False, "error": str(exc)}
27385
27385
  try:
@@ -27432,7 +27432,7 @@ def _probe_color_node_graph(proj, item, p: Dict[str, Any]):
27432
27432
  return err
27433
27433
  snapshot = _graph_snapshot(
27434
27434
  graph,
27435
- include_nodes=p.get("include_nodes", True),
27435
+ include_nodes=_coerce_bool(p.get("include_nodes"), True),
27436
27436
  max_nodes=p.get("max_nodes", 3),
27437
27437
  )
27438
27438
  snapshot["source"] = source
@@ -27573,7 +27573,7 @@ def _safe_export_lut(item, p: Dict[str, Any]):
27573
27573
  path = p.get("path")
27574
27574
  if not path:
27575
27575
  return _err("path is required")
27576
- if p.get("require_temp_path", True) and not _grade_temp_path_ok(path):
27576
+ if _coerce_bool(p.get("require_temp_path"), True) and not _grade_temp_path_ok(path):
27577
27577
  return _err("path must be under the system temp directory unless require_temp_path=False")
27578
27578
  folder = os.path.dirname(os.path.abspath(path))
27579
27579
  if folder:
@@ -27602,7 +27602,7 @@ def _safe_apply_drx(proj, item, p: Dict[str, Any]):
27602
27602
  return _err("path is required", code="MISSING_PATH", category="invalid_input")
27603
27603
  if not os.path.isfile(path):
27604
27604
  return _err(f"DRX file not found: {path}", code="DRX_NOT_FOUND", category="invalid_input")
27605
- if p.get("require_temp_path", True) and not _grade_temp_path_ok(path):
27605
+ if _coerce_bool(p.get("require_temp_path"), True) and not _grade_temp_path_ok(path):
27606
27606
  return _err("DRX path must be under the system temp directory unless require_temp_path=False",
27607
27607
  code="DRX_PATH_NOT_TEMP", category="invalid_input")
27608
27608
  graph, source, err = _color_graph_from_params(proj, item, p)
@@ -27701,7 +27701,7 @@ def _resolve_trace_plan(tl, plan: Dict[str, Any], p: Dict[str, Any]) -> List[Dic
27701
27701
  """Map each plan match onto a live clip. status: apply | skip (+reason)."""
27702
27702
  min_conf = float(p.get("min_confidence", 0.8))
27703
27703
  tol = int(p.get("start_tolerance", 0))
27704
- require_temp = p.get("require_temp_path", True)
27704
+ require_temp = _coerce_bool(p.get("require_temp_path"), True)
27705
27705
  live = _live_video_items(tl)
27706
27706
  rows: List[Dict[str, Any]] = []
27707
27707
  for m in plan["matches"]:
@@ -27796,7 +27796,7 @@ def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
27796
27796
  return err
27797
27797
  live_name = tl.GetName()
27798
27798
  plan_tl = (plan.get("target") or {}).get("timeline")
27799
- if plan_tl and plan_tl != live_name and not p.get("allow_timeline_mismatch"):
27799
+ if plan_tl and plan_tl != live_name and not _coerce_bool(p.get("allow_timeline_mismatch")):
27800
27800
  return _err(
27801
27801
  f"plan targets timeline {plan_tl!r} but the current timeline is {live_name!r}",
27802
27802
  code="TIMELINE_MISMATCH", category="invalid_input",
@@ -27821,7 +27821,7 @@ def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
27821
27821
  grade_mode = p.get("grade_mode", 0)
27822
27822
  version_name = p.get("version_name")
27823
27823
  max_rows = p.get("max_rows", _TRACE_MAX_ROWS_DEFAULT)
27824
- max_rows = len(public) if p.get("verbose") else max(0, int(max_rows))
27824
+ max_rows = len(public) if _coerce_bool(p.get("verbose")) else max(0, int(max_rows))
27825
27825
  # Entries that deserve a human look: skipped for a reason other than the
27826
27826
  # two bulk ones, or applied on a tie / a partial source-range overlap.
27827
27827
  bulk = {"unmatched", "no-source-grade"}
@@ -28000,7 +28000,7 @@ def _grade_boundary_report(proj, item, p: Dict[str, Any]):
28000
28000
  "color_groups": _color_group_capabilities(proj),
28001
28001
  "gallery": _gallery_capabilities(proj),
28002
28002
  }
28003
- if p.get("include_timeline_graph", True):
28003
+ if _coerce_bool(p.get("include_timeline_graph"), True):
28004
28004
  report["timeline_graph"] = _probe_color_node_graph(proj, item, {"source": "timeline", "include_nodes": False})
28005
28005
  return report
28006
28006
 
@@ -28487,7 +28487,7 @@ def _propose_grade(proj, p: Dict[str, Any]) -> Dict[str, Any]:
28487
28487
  "frame_paths": list(p.get("frame_paths") or []),
28488
28488
  "validation": {"valid": True, "notes": []},
28489
28489
  }
28490
- if not p.get("execute"):
28490
+ if not _coerce_bool(p.get("execute")):
28491
28491
  payload["executed"] = False
28492
28492
  return payload
28493
28493
 
@@ -28730,7 +28730,7 @@ def _grade_evidence_base(proj, item, p: Dict[str, Any]) -> Dict[str, Any]:
28730
28730
  # Optional coverage (best-effort; coverage_report needs a media pool item)
28731
28731
  coverage_summary = None
28732
28732
  coverage_warnings_count = 0
28733
- if p.get("include_coverage", True):
28733
+ if _coerce_bool(p.get("include_coverage"), True):
28734
28734
  mp_item = _timeline_item_media_pool_item(item)
28735
28735
  if mp_item is not None:
28736
28736
  try:
@@ -29298,8 +29298,8 @@ def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
29298
29298
  return _err("folder_path is required")
29299
29299
  prefix = p.get("prefix", "still")
29300
29300
  fmt = p.get("format", "dpx")
29301
- delete_after = p.get("delete_after", True)
29302
- cleanup = p.get("cleanup", True)
29301
+ delete_after = _coerce_bool(p.get("delete_after"), True)
29302
+ cleanup = _coerce_bool(p.get("cleanup"), True)
29303
29303
  # Redirect sandbox/temp paths that Resolve can't access
29304
29304
  folder_path = _resolve_safe_dir(folder_path)
29305
29305
  folder_pre_existed = os.path.isdir(folder_path)
@@ -29697,7 +29697,7 @@ def _fusion_group_settings_export(comp, p: Dict[str, Any]) -> Dict[str, Any]:
29697
29697
  "path": path,
29698
29698
  "group_name": group_name,
29699
29699
  "parse_warning": f"saved, but summary parse failed: {exc}",
29700
- **_fusion_group_advisory(p.get("include_advisory", False)),
29700
+ **_fusion_group_advisory(_coerce_bool(p.get("include_advisory"), False)),
29701
29701
  }
29702
29702
  return {
29703
29703
  "success": True,
@@ -29705,7 +29705,7 @@ def _fusion_group_settings_export(comp, p: Dict[str, Any]) -> Dict[str, Any]:
29705
29705
  "group_name": group_name,
29706
29706
  "published_inputs": parsed["published_inputs"],
29707
29707
  "input_count": parsed["input_count"],
29708
- **_fusion_group_advisory(p.get("include_advisory", False)),
29708
+ **_fusion_group_advisory(_coerce_bool(p.get("include_advisory"), False)),
29709
29709
  }
29710
29710
 
29711
29711
 
@@ -29740,7 +29740,7 @@ def _fusion_group_settings_splice_inputs(p: Dict[str, Any]) -> Dict[str, Any]:
29740
29740
  "success": True,
29741
29741
  "dest_path": summary["dest_path"],
29742
29742
  "summary": summary,
29743
- **_fusion_group_advisory(p.get("include_advisory", False)),
29743
+ **_fusion_group_advisory(_coerce_bool(p.get("include_advisory"), False)),
29744
29744
  }
29745
29745
 
29746
29746
 
@@ -29848,7 +29848,7 @@ def _fusion_group_settings_load(comp, p: Dict[str, Any]) -> Dict[str, Any]:
29848
29848
  "Group preserved. If Edit Controls don't refresh, select the group in "
29849
29849
  "Fusion and use UI Load Settings to remap InstanceInput order."
29850
29850
  ),
29851
- **_fusion_group_advisory(p.get("include_advisory", False)),
29851
+ **_fusion_group_advisory(_coerce_bool(p.get("include_advisory"), False)),
29852
29852
  }
29853
29853
 
29854
29854
 
@@ -29965,7 +29965,7 @@ def _fusion_probe_group_published_inputs(comp, p: Dict[str, Any]) -> Dict[str, A
29965
29965
  "live_inputs": live_inputs,
29966
29966
  "live_input_count": len(live_inputs),
29967
29967
  "file_summary": file_summary,
29968
- **_fusion_group_advisory(p.get("include_advisory", False)),
29968
+ **_fusion_group_advisory(_coerce_bool(p.get("include_advisory"), False)),
29969
29969
  }
29970
29970
 
29971
29971
 
@@ -30116,7 +30116,7 @@ def _fusion_comp_snapshot(comp, p: Dict[str, Any]):
30116
30116
  max_tools = int(max_tools)
30117
30117
  except (TypeError, ValueError):
30118
30118
  max_tools = 20
30119
- include_io = bool(p.get("include_io", False))
30119
+ include_io = _coerce_bool(p.get("include_io"), False)
30120
30120
  for idx in list(tool_list)[:max_tools]:
30121
30121
  tools.append(_fusion_tool_summary(tool_list[idx], include_io=include_io))
30122
30122
  return {
@@ -30166,7 +30166,7 @@ def _safe_add_fusion_tool(comp, p: Dict[str, Any]):
30166
30166
  return _err(f"Failed to add tool '{tool_type}'. Check the tool ID is valid.")
30167
30167
  if p.get("name"):
30168
30168
  tool.SetAttrs({"TOOLS_Name": p["name"]})
30169
- return _ok(tool=_fusion_tool_summary(tool, include_io=p.get("include_io", True)))
30169
+ return _ok(tool=_fusion_tool_summary(tool, include_io=_coerce_bool(p.get("include_io"), True)))
30170
30170
  finally:
30171
30171
  comp.Unlock()
30172
30172
 
@@ -30178,7 +30178,7 @@ def _probe_fusion_tool(comp, p: Dict[str, Any]):
30178
30178
  tool = comp.FindTool(name)
30179
30179
  if not tool:
30180
30180
  return {"found": False, "tool_name": name}
30181
- return {"found": True, "tool": _fusion_tool_summary(tool, include_io=p.get("include_io", True))}
30181
+ return {"found": True, "tool": _fusion_tool_summary(tool, include_io=_coerce_bool(p.get("include_io"), True))}
30182
30182
 
30183
30183
 
30184
30184
  def _safe_set_fusion_inputs(comp, p: Dict[str, Any]):
@@ -30202,7 +30202,7 @@ def _safe_set_fusion_inputs(comp, p: Dict[str, Any]):
30202
30202
  else:
30203
30203
  tool.SetInput(input_name, value)
30204
30204
  row = {"success": True}
30205
- if p.get("readback", True):
30205
+ if _coerce_bool(p.get("readback"), True):
30206
30206
  try:
30207
30207
  row["value"] = _ser(tool.GetInput(input_name, p["time"])) if "time" in p else _ser(tool.GetInput(input_name))
30208
30208
  except Exception as exc:
@@ -30238,7 +30238,7 @@ def _safe_connect_fusion_tools(comp, p: Dict[str, Any]):
30238
30238
  def _fusion_boundary_report(comp, p: Dict[str, Any]):
30239
30239
  return {
30240
30240
  "capabilities": _fusion_graph_capabilities(comp),
30241
- "composition": _fusion_comp_snapshot(comp, {**p, "include_io": p.get("include_io", True)}),
30241
+ "composition": _fusion_comp_snapshot(comp, {**p, "include_io": _coerce_bool(p.get("include_io"), True)}),
30242
30242
  }
30243
30243
 
30244
30244
 
@@ -30372,7 +30372,7 @@ def _fusion_add_mask(comp, p: Dict[str, Any]) -> Dict[str, Any]:
30372
30372
 
30373
30373
  x = p.get("x", -1)
30374
30374
  y = p.get("y", -1)
30375
- readback = bool(p.get("readback", True))
30375
+ readback = _coerce_bool(p.get("readback"), True)
30376
30376
 
30377
30377
  # The lock covers only the STRUCTURAL half (AddTool + rename). The input
30378
30378
  # writes below must run outside it — see _FUSION_VALUE_WRITE_NOTE: a value
@@ -30524,7 +30524,7 @@ def _fusion_set_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
30524
30524
  if err:
30525
30525
  return err
30526
30526
  input_id = p.get("input_name", "StyledText")
30527
- readback = bool(p.get("readback", True))
30527
+ readback = _coerce_bool(p.get("readback"), True)
30528
30528
  # No comp.Lock() around a value write — see _FUSION_VALUE_WRITE_NOTE.
30529
30529
  try:
30530
30530
  tool.SetInput(input_id, text)
@@ -31243,7 +31243,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
31243
31243
  return _ok()
31244
31244
 
31245
31245
  elif action == "end_undo":
31246
- comp.EndUndo(p.get("keep", True))
31246
+ comp.EndUndo(_coerce_bool(p.get("keep"), True))
31247
31247
  return _ok()
31248
31248
 
31249
31249
  # --- Node Layout (FlowView) ---
@@ -31495,7 +31495,7 @@ def fuse_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
31495
31495
  d = _fuses_dir()
31496
31496
  if not os.path.isdir(d):
31497
31497
  return {"fuses": []}
31498
- show_all = bool(p.get("all", False))
31498
+ show_all = _coerce_bool(p.get("all"), False)
31499
31499
  out = []
31500
31500
  for fn in sorted(os.listdir(d)):
31501
31501
  if not fn.endswith(".fuse"):
@@ -31874,7 +31874,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
31874
31874
  root = root if sd is None else os.path.join(root, sd)
31875
31875
  if not os.path.isdir(root):
31876
31876
  return {"files": []}
31877
- show_all = bool(p.get("all", False))
31877
+ show_all = _coerce_bool(p.get("all"), False)
31878
31878
  out = []
31879
31879
  for fn in sorted(os.listdir(root)):
31880
31880
  if not fn.lower().endswith(_DCTL_VALID_EXT):
@@ -32295,7 +32295,7 @@ def _safe_install_extension(p: Dict[str, Any]) -> Dict[str, Any]:
32295
32295
  if err:
32296
32296
  return err
32297
32297
  name = p.get("name") or _extension_template_name(extension_type, p.get("kind", "lifecycle"))
32298
- invalid = _extension_safe_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
32298
+ invalid = _extension_safe_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
32299
32299
  if invalid:
32300
32300
  return invalid
32301
32301
  source = p.get("source")
@@ -32322,7 +32322,7 @@ def _safe_install_extension(p: Dict[str, Any]) -> Dict[str, Any]:
32322
32322
  if not isinstance(source, str) or not source.strip():
32323
32323
  return _err("source must be a non-empty string")
32324
32324
  marker = {"fuse": _FUSE_MARKER, "dctl": _DCTL_MARKER, "script": _SCRIPT_MARKER}[extension_type]
32325
- if p.get("require_marker", True) and not _source_has_marker(source, marker):
32325
+ if _coerce_bool(p.get("require_marker"), True) and not _source_has_marker(source, marker):
32326
32326
  return _err(f"source must include {marker} unless require_marker=False")
32327
32327
  if _coerce_bool(p.get("dry_run")):
32328
32328
  return _ok(
@@ -32335,7 +32335,7 @@ def _safe_install_extension(p: Dict[str, Any]) -> Dict[str, Any]:
32335
32335
  }),
32336
32336
  )
32337
32337
  if extension_type == "fuse":
32338
- return fuse_plugin("install", {"name": name, "source": source, "overwrite": p.get("overwrite", False)})
32338
+ return fuse_plugin("install", {"name": name, "source": source, "overwrite": _coerce_bool(p.get("overwrite"), False)})
32339
32339
  if extension_type == "dctl":
32340
32340
  category = p.get("category") or dctl_templates.KIND_CATEGORY.get(kind, "lut")
32341
32341
  return dctl("install", {
@@ -32344,7 +32344,7 @@ def _safe_install_extension(p: Dict[str, Any]) -> Dict[str, Any]:
32344
32344
  "category": category,
32345
32345
  "subdir": p.get("subdir"),
32346
32346
  "ext": p.get("ext", ".dctl"),
32347
- "overwrite": p.get("overwrite", False),
32347
+ "overwrite": _coerce_bool(p.get("overwrite"), False),
32348
32348
  })
32349
32349
  language = _normalize_script_language(p.get("language", options.get("language", "lua")))
32350
32350
  category = p.get("script_category", p.get("category", "Utility"))
@@ -32362,7 +32362,7 @@ def _safe_remove_extension(p: Dict[str, Any]) -> Dict[str, Any]:
32362
32362
  if err:
32363
32363
  return err
32364
32364
  name = p.get("name")
32365
- invalid = _extension_safe_name(name, allow_non_mcp_name=p.get("allow_non_mcp_name", False))
32365
+ invalid = _extension_safe_name(name, allow_non_mcp_name=_coerce_bool(p.get("allow_non_mcp_name"), False))
32366
32366
  if invalid:
32367
32367
  return invalid
32368
32368
  if extension_type == "fuse":
@@ -32402,7 +32402,7 @@ def _safe_remove_extension(p: Dict[str, Any]) -> Dict[str, Any]:
32402
32402
  marker = _SCRIPT_MARKER
32403
32403
  if not os.path.isfile(path):
32404
32404
  return _err(f"No {extension_type} extension named '{name}' at {path}")
32405
- if p.get("require_marker", True) and not _file_has_marker(path, marker):
32405
+ if _coerce_bool(p.get("require_marker"), True) and not _file_has_marker(path, marker):
32406
32406
  return _err(f"Refusing to remove unmarked file at {path}; pass require_marker=False only if you intend this")
32407
32407
  if _coerce_bool(p.get("dry_run")):
32408
32408
  return _ok(would_remove=True, extension_type=extension_type, name=name, path=path)
@@ -32429,19 +32429,19 @@ def _probe_fuse_lifecycle(p: Dict[str, Any]) -> Dict[str, Any]:
32429
32429
  if isinstance(source, str):
32430
32430
  out["has_marker"] = _source_has_marker(source, _FUSE_MARKER)
32431
32431
  out["validation"] = fuse_plugin("validate", {"source": source})
32432
- if p.get("include_template_matrix"):
32432
+ if _coerce_bool(p.get("include_template_matrix")):
32433
32433
  out["template_matrix"] = _extension_template_matrix()["fuse"]
32434
- if p.get("install"):
32434
+ if _coerce_bool(p.get("install")):
32435
32435
  install = script_plugin("safe_install_extension", {
32436
32436
  "extension_type": "fuse",
32437
32437
  "name": name,
32438
32438
  "source": source,
32439
- "overwrite": p.get("overwrite", True),
32439
+ "overwrite": _coerce_bool(p.get("overwrite"), True),
32440
32440
  })
32441
32441
  out["install"] = install
32442
32442
  out["read"] = fuse_plugin("read", {"name": name}) if install.get("success") else None
32443
32443
  out["list"] = fuse_plugin("list")
32444
- if p.get("cleanup", True):
32444
+ if _coerce_bool(p.get("cleanup"), True):
32445
32445
  out["remove"] = script_plugin("safe_remove_extension", {"extension_type": "fuse", "name": name})
32446
32446
  return out
32447
32447
 
@@ -32466,29 +32466,29 @@ def _probe_dctl_lifecycle(p: Dict[str, Any]) -> Dict[str, Any]:
32466
32466
  if isinstance(source, str):
32467
32467
  out["has_marker"] = _source_has_marker(source, _DCTL_MARKER)
32468
32468
  out["validation"] = dctl("validate", {"source": source})
32469
- if p.get("include_template_matrix"):
32469
+ if _coerce_bool(p.get("include_template_matrix")):
32470
32470
  out["template_matrix"] = _extension_template_matrix()["dctl"]
32471
- if p.get("install"):
32471
+ if _coerce_bool(p.get("install")):
32472
32472
  install = script_plugin("safe_install_extension", {
32473
32473
  "extension_type": "dctl",
32474
32474
  "name": name,
32475
32475
  "source": source,
32476
32476
  "category": category,
32477
32477
  "subdir": subdir,
32478
- "overwrite": p.get("overwrite", True),
32478
+ "overwrite": _coerce_bool(p.get("overwrite"), True),
32479
32479
  })
32480
32480
  out["install"] = install
32481
32481
  out["read"] = dctl("read", {"name": name, "category": category, "subdir": subdir}) if install.get("success") else None
32482
32482
  out["list"] = dctl("list", {"category": category, "subdir": subdir})
32483
- if p.get("refresh_luts") and category == "lut":
32483
+ if _coerce_bool(p.get("refresh_luts")) and category == "lut":
32484
32484
  out["refresh_luts"] = project_settings("refresh_luts")
32485
- if p.get("cleanup", True):
32485
+ if _coerce_bool(p.get("cleanup"), True):
32486
32486
  out["remove"] = script_plugin("safe_remove_extension", {"extension_type": "dctl", "name": name, "category": category, "subdir": subdir})
32487
32487
  return out
32488
32488
 
32489
32489
 
32490
32490
  def _probe_script_lifecycle(p: Dict[str, Any]) -> Dict[str, Any]:
32491
- if p.get("execute"):
32491
+ if _coerce_bool(p.get("execute")):
32492
32492
  # Refused rather than ignored: silently skipping it would report a
32493
32493
  # lifecycle probe as complete for a step it never ran.
32494
32494
  return _err(
@@ -32518,21 +32518,21 @@ def _probe_script_lifecycle(p: Dict[str, Any]) -> Dict[str, Any]:
32518
32518
  if isinstance(source, str):
32519
32519
  out["has_marker"] = _source_has_marker(source, _SCRIPT_MARKER)
32520
32520
  out["validation"] = script_plugin("validate", {"source": source, "language": language})
32521
- if p.get("include_template_matrix"):
32521
+ if _coerce_bool(p.get("include_template_matrix")):
32522
32522
  out["template_matrix"] = _extension_template_matrix()["script"]
32523
- if p.get("install"):
32523
+ if _coerce_bool(p.get("install")):
32524
32524
  install = script_plugin("safe_install_extension", {
32525
32525
  "extension_type": "script",
32526
32526
  "name": name,
32527
32527
  "source": source,
32528
32528
  "category": category,
32529
32529
  "language": language,
32530
- "overwrite": p.get("overwrite", True),
32530
+ "overwrite": _coerce_bool(p.get("overwrite"), True),
32531
32531
  })
32532
32532
  out["install"] = install
32533
32533
  out["read"] = script_plugin("read", {"name": name, "category": category, "language": language}) if install.get("success") else None
32534
32534
  out["list"] = script_plugin("list", {"category": category, "language": language})
32535
- if p.get("cleanup", True):
32535
+ if _coerce_bool(p.get("cleanup"), True):
32536
32536
  out["remove"] = script_plugin("safe_remove_extension", {
32537
32537
  "extension_type": "script",
32538
32538
  "name": name,
@@ -32543,7 +32543,7 @@ def _probe_script_lifecycle(p: Dict[str, Any]) -> Dict[str, Any]:
32543
32543
 
32544
32544
 
32545
32545
  def _extension_boundary_report(p: Dict[str, Any]) -> Dict[str, Any]:
32546
- include_matrix = p.get("include_template_matrix", True)
32546
+ include_matrix = _coerce_bool(p.get("include_template_matrix"), True)
32547
32547
  return {
32548
32548
  "capabilities": _extension_capabilities(),
32549
32549
  "refresh_restart": {
@@ -32664,7 +32664,7 @@ def script_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
32664
32664
  if language_filter and language_filter not in _SCRIPT_VALID_LANG:
32665
32665
  return _err(f"Invalid language '{language_filter}'. "
32666
32666
  f"Valid: {list(_SCRIPT_VALID_LANG)}; aliases: ['python', 'python3']")
32667
- show_all = bool(p.get("all", False))
32667
+ show_all = _coerce_bool(p.get("all"), False)
32668
32668
 
32669
32669
  paths = get_resolve_plugin_paths()
32670
32670
  if category:
@@ -32887,7 +32887,7 @@ def knowledge(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
32887
32887
  return _ok(**_knowledge_mod.get(
32888
32888
  str(p["topic"]),
32889
32889
  section=str(section) if section else None,
32890
- inline=bool(p.get("inline", True)),
32890
+ inline=_coerce_bool(p.get("inline"), True),
32891
32891
  ))
32892
32892
  if action == "search":
32893
32893
  err, _clean = _validate_params(p, {