@kenkaiiii/gg-editor 0.6.2 → 0.6.4
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/README.md +5 -1
- package/dist/core/doctor-runner.js +45 -11
- package/dist/core/doctor-runner.js.map +1 -1
- package/dist/core/hosts/lazy.d.ts +5 -5
- package/dist/core/hosts/lazy.d.ts.map +1 -1
- package/dist/core/hosts/lazy.js +16 -0
- package/dist/core/hosts/lazy.js.map +1 -1
- package/dist/core/hosts/premiere/adapter.d.ts +4 -1
- package/dist/core/hosts/premiere/adapter.d.ts.map +1 -1
- package/dist/core/hosts/premiere/adapter.js +6 -5
- package/dist/core/hosts/premiere/adapter.js.map +1 -1
- package/dist/core/hosts/premiere/bridge-source.d.ts.map +1 -1
- package/dist/core/hosts/premiere/bridge-source.js +45 -0
- package/dist/core/hosts/premiere/bridge-source.js.map +1 -1
- package/dist/core/hosts/resolve/adapter.d.ts +6 -2
- package/dist/core/hosts/resolve/adapter.d.ts.map +1 -1
- package/dist/core/hosts/resolve/adapter.js +13 -5
- package/dist/core/hosts/resolve/adapter.js.map +1 -1
- package/dist/core/hosts/resolve/bridge-source.d.ts.map +1 -1
- package/dist/core/hosts/resolve/bridge-source.js +376 -2
- package/dist/core/hosts/resolve/bridge-source.js.map +1 -1
- package/dist/core/hosts/resolve/bridge.d.ts +22 -4
- package/dist/core/hosts/resolve/bridge.d.ts.map +1 -1
- package/dist/core/hosts/resolve/bridge.integration.test.js +338 -0
- package/dist/core/hosts/resolve/bridge.integration.test.js.map +1 -1
- package/dist/core/hosts/resolve/bridge.js +56 -6
- package/dist/core/hosts/resolve/bridge.js.map +1 -1
- package/dist/core/hosts/resolve/bridge.test.js +61 -1
- package/dist/core/hosts/resolve/bridge.test.js.map +1 -1
- package/dist/core/hosts/types.d.ts +81 -0
- package/dist/core/hosts/types.d.ts.map +1 -1
- package/dist/core/hosts/types.js.map +1 -1
- package/dist/core/safe-paths.d.ts +47 -0
- package/dist/core/safe-paths.d.ts.map +1 -0
- package/dist/core/safe-paths.js +102 -0
- package/dist/core/safe-paths.js.map +1 -0
- package/dist/core/safe-paths.test.d.ts +2 -0
- package/dist/core/safe-paths.test.d.ts.map +1 -0
- package/dist/core/safe-paths.test.js +76 -0
- package/dist/core/safe-paths.test.js.map +1 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +125 -0
- package/dist/skills.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +33 -1
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/compose-layered.d.ts.map +1 -1
- package/dist/tools/compose-layered.js +2 -1
- package/dist/tools/compose-layered.js.map +1 -1
- package/dist/tools/compose-thumbnail.d.ts.map +1 -1
- package/dist/tools/compose-thumbnail.js +10 -2
- package/dist/tools/compose-thumbnail.js.map +1 -1
- package/dist/tools/concat-videos.d.ts.map +1 -1
- package/dist/tools/concat-videos.js +2 -1
- package/dist/tools/concat-videos.js.map +1 -1
- package/dist/tools/cut-filler-words.d.ts.map +1 -1
- package/dist/tools/cut-filler-words.js +2 -1
- package/dist/tools/cut-filler-words.js.map +1 -1
- package/dist/tools/extract-frame.d.ts.map +1 -1
- package/dist/tools/extract-frame.js +9 -2
- package/dist/tools/extract-frame.js.map +1 -1
- package/dist/tools/fusion-comp.d.ts +49 -0
- package/dist/tools/fusion-comp.d.ts.map +1 -0
- package/dist/tools/fusion-comp.js +129 -0
- package/dist/tools/fusion-comp.js.map +1 -0
- package/dist/tools/generate-gif.d.ts.map +1 -1
- package/dist/tools/generate-gif.js +12 -2
- package/dist/tools/generate-gif.js.map +1 -1
- package/dist/tools/host-eval.d.ts +35 -0
- package/dist/tools/host-eval.d.ts.map +1 -0
- package/dist/tools/host-eval.js +70 -0
- package/dist/tools/host-eval.js.map +1 -0
- package/dist/tools/host-tools.test.js +42 -0
- package/dist/tools/host-tools.test.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/index.test.js +4 -0
- package/dist/tools/index.test.js.map +1 -1
- package/dist/tools/overlay-watermark.d.ts.map +1 -1
- package/dist/tools/overlay-watermark.js +2 -1
- package/dist/tools/overlay-watermark.js.map +1 -1
- package/dist/tools/reformat-timeline.d.ts.map +1 -1
- package/dist/tools/reformat-timeline.js +3 -2
- package/dist/tools/reformat-timeline.js.map +1 -1
- package/dist/tools/reorder-timeline.d.ts.map +1 -1
- package/dist/tools/reorder-timeline.js +3 -2
- package/dist/tools/reorder-timeline.js.map +1 -1
- package/dist/tools/transcribe.d.ts.map +1 -1
- package/dist/tools/transcribe.js +2 -1
- package/dist/tools/transcribe.js.map +1 -1
- package/dist/tools/write-ass.d.ts.map +1 -1
- package/dist/tools/write-ass.js +3 -2
- package/dist/tools/write-ass.js.map +1 -1
- package/dist/tools/write-edl.d.ts.map +1 -1
- package/dist/tools/write-edl.js +3 -2
- package/dist/tools/write-edl.js.map +1 -1
- package/dist/tools/write-fcpxml.d.ts.map +1 -1
- package/dist/tools/write-fcpxml.js +3 -2
- package/dist/tools/write-fcpxml.js.map +1 -1
- package/dist/tools/write-keyword-captions.d.ts.map +1 -1
- package/dist/tools/write-keyword-captions.js +2 -1
- package/dist/tools/write-keyword-captions.js.map +1 -1
- package/dist/tools/write-lower-third.d.ts.map +1 -1
- package/dist/tools/write-lower-third.js +3 -2
- package/dist/tools/write-lower-third.js.map +1 -1
- package/dist/tools/write-srt.d.ts.map +1 -1
- package/dist/tools/write-srt.js +3 -2
- package/dist/tools/write-srt.js.map +1 -1
- package/dist/tools/write-title-card.d.ts.map +1 -1
- package/dist/tools/write-title-card.js +3 -2
- package/dist/tools/write-title-card.js.map +1 -1
- package/package.json +3 -3
|
@@ -355,6 +355,23 @@ def _fmt3(triple):
|
|
|
355
355
|
return "%g %g %g" % (float(triple[0]), float(triple[1]), float(triple[2]))
|
|
356
356
|
|
|
357
357
|
|
|
358
|
+
def _ensure_color_page():
|
|
359
|
+
"""Best-effort switch to the Color page when the current page isn't already
|
|
360
|
+
color. Returns True if we issued an OpenPage call, False otherwise. Used by
|
|
361
|
+
SetCDL / CopyGrades retry logic — those API calls silently no-op on the
|
|
362
|
+
wrong page even though the values land in the data model.
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
r = get_resolve()
|
|
366
|
+
cur = r.GetCurrentPage() if hasattr(r, "GetCurrentPage") else None
|
|
367
|
+
if cur == "color":
|
|
368
|
+
return False
|
|
369
|
+
r.OpenPage("color")
|
|
370
|
+
return True
|
|
371
|
+
except Exception:
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
|
|
358
375
|
def m_set_primary_correction(params):
|
|
359
376
|
clip_id = str(params["clipId"])
|
|
360
377
|
node_index = int(params.get("nodeIndex", 1) or 1)
|
|
@@ -371,10 +388,16 @@ def m_set_primary_correction(params):
|
|
|
371
388
|
if params.get("saturation") is not None:
|
|
372
389
|
cdl["Saturation"] = "%g" % float(params["saturation"])
|
|
373
390
|
ok = item.SetCDL(cdl)
|
|
391
|
+
if not ok:
|
|
392
|
+
# Common cause: agent set CDL while user was on another page. Auto-
|
|
393
|
+
# switch to Color and retry once. If it fails again, the user/clip is
|
|
394
|
+
# genuinely the problem — surface the original message.
|
|
395
|
+
if _ensure_color_page():
|
|
396
|
+
ok = item.SetCDL(cdl)
|
|
374
397
|
if not ok:
|
|
375
398
|
raise RuntimeError(
|
|
376
399
|
f"SetCDL returned False for node {node_index}. "
|
|
377
|
-
f"fix:
|
|
400
|
+
f"fix: ensure the clip exists and the project's color page is reachable."
|
|
378
401
|
)
|
|
379
402
|
return {"clipId": clip_id, "nodeIndex": node_index}
|
|
380
403
|
|
|
@@ -389,9 +412,12 @@ def m_copy_grade(params):
|
|
|
389
412
|
if not hasattr(src, "CopyGrades"):
|
|
390
413
|
raise RuntimeError("this Resolve version's TimelineItem has no CopyGrades")
|
|
391
414
|
ok = src.CopyGrades(targets)
|
|
415
|
+
if not ok:
|
|
416
|
+
if _ensure_color_page():
|
|
417
|
+
ok = src.CopyGrades(targets)
|
|
392
418
|
if not ok:
|
|
393
419
|
raise RuntimeError(
|
|
394
|
-
"CopyGrades returned False. fix:
|
|
420
|
+
"CopyGrades returned False. fix: ensure source + targets are valid timeline items."
|
|
395
421
|
)
|
|
396
422
|
return {"sourceClipId": source_id, "copied": len(targets)}
|
|
397
423
|
|
|
@@ -592,6 +618,352 @@ def m_import_subtitles(params):
|
|
|
592
618
|
return {"imported": True, "attached": True}
|
|
593
619
|
|
|
594
620
|
|
|
621
|
+
# ── Fusion ──────────────────────────────────────────────────
|
|
622
|
+
#
|
|
623
|
+
# Fusion compositions live either at the timeline-clip level (every
|
|
624
|
+
# TimelineItem can carry one or more 'Fusion comps') OR at the project level
|
|
625
|
+
# via the standalone Fusion page. The agent operates on whichever the user
|
|
626
|
+
# is currently focused on — we resolve the comp once per call:
|
|
627
|
+
#
|
|
628
|
+
# - clipId given: item.GetFusionCompByIndex(1) on that timeline item.
|
|
629
|
+
# - no clipId: fusion.GetCurrentComp() on the active Fusion page.
|
|
630
|
+
#
|
|
631
|
+
# We deliberately keep the surface narrow (8 actions, the Fusion-page tools
|
|
632
|
+
# every short-form workflow needs) instead of mirroring the entire Fusion
|
|
633
|
+
# Python API. Power users have m_execute_code for the rest.
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _resolve_fusion_comp(clip_id):
|
|
637
|
+
if clip_id:
|
|
638
|
+
item = _find_timeline_item(clip_id)
|
|
639
|
+
if not hasattr(item, "GetFusionCompCount") or not hasattr(item, "GetFusionCompByIndex"):
|
|
640
|
+
raise RuntimeError("this Resolve version's TimelineItem has no Fusion comp accessors")
|
|
641
|
+
count = int(item.GetFusionCompCount() or 0)
|
|
642
|
+
if count < 1:
|
|
643
|
+
# Try to add one. Modern Resolve exposes AddFusionComp; older
|
|
644
|
+
# builds need the user to do it manually on the Fusion page.
|
|
645
|
+
if hasattr(item, "AddFusionComp"):
|
|
646
|
+
comp = item.AddFusionComp()
|
|
647
|
+
if comp is None:
|
|
648
|
+
raise RuntimeError(f"AddFusionComp returned None for clip {clip_id}")
|
|
649
|
+
return comp
|
|
650
|
+
raise RuntimeError(
|
|
651
|
+
f"clip {clip_id} has no Fusion comp; create one in Resolve (right-click → New Fusion Composition) first."
|
|
652
|
+
)
|
|
653
|
+
comp = item.GetFusionCompByIndex(1)
|
|
654
|
+
if comp is None:
|
|
655
|
+
raise RuntimeError(f"GetFusionCompByIndex(1) returned None for clip {clip_id}")
|
|
656
|
+
return comp
|
|
657
|
+
# No clip — use the active Fusion page comp.
|
|
658
|
+
r = get_resolve()
|
|
659
|
+
try:
|
|
660
|
+
fusion = r.Fusion()
|
|
661
|
+
except Exception as e:
|
|
662
|
+
raise RuntimeError(f"Resolve.Fusion() unavailable: {e}")
|
|
663
|
+
if fusion is None:
|
|
664
|
+
raise RuntimeError("Fusion is not available in this Resolve build")
|
|
665
|
+
comp = fusion.GetCurrentComp() if hasattr(fusion, "GetCurrentComp") else None
|
|
666
|
+
if comp is None:
|
|
667
|
+
raise RuntimeError(
|
|
668
|
+
"No active Fusion comp. fix: switch to the Fusion page (open_page('fusion')) on a clip with a comp, or pass clipId."
|
|
669
|
+
)
|
|
670
|
+
return comp
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _fusion_node_summary(node):
|
|
674
|
+
# Best-effort, never throws — the caller is reading a list of nodes.
|
|
675
|
+
try:
|
|
676
|
+
name = node.GetAttrs("TOOLS_Name") if hasattr(node, "GetAttrs") else None
|
|
677
|
+
except Exception:
|
|
678
|
+
name = None
|
|
679
|
+
try:
|
|
680
|
+
toolid = node.GetAttrs("TOOLS_RegID") if hasattr(node, "GetAttrs") else None
|
|
681
|
+
except Exception:
|
|
682
|
+
toolid = None
|
|
683
|
+
if not name and hasattr(node, "Name"):
|
|
684
|
+
try:
|
|
685
|
+
name = node.Name
|
|
686
|
+
except Exception:
|
|
687
|
+
pass
|
|
688
|
+
if not toolid and hasattr(node, "ID"):
|
|
689
|
+
try:
|
|
690
|
+
toolid = node.ID
|
|
691
|
+
except Exception:
|
|
692
|
+
pass
|
|
693
|
+
return {"name": name, "toolId": toolid}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def m_fusion_comp(params):
|
|
697
|
+
action = str(params.get("action") or "")
|
|
698
|
+
if not action:
|
|
699
|
+
raise RuntimeError("fusion_comp requires an 'action' field")
|
|
700
|
+
clip_id = params.get("clipId")
|
|
701
|
+
comp = _resolve_fusion_comp(str(clip_id) if clip_id else None)
|
|
702
|
+
|
|
703
|
+
if action == "list_nodes":
|
|
704
|
+
nodes = []
|
|
705
|
+
try:
|
|
706
|
+
tool_list = comp.GetToolList(False) or {}
|
|
707
|
+
except Exception as e:
|
|
708
|
+
raise RuntimeError(f"GetToolList failed: {e}")
|
|
709
|
+
# GetToolList returns a 1-indexed dict on some builds; iterate values.
|
|
710
|
+
try:
|
|
711
|
+
iterator = tool_list.values() if hasattr(tool_list, "values") else list(tool_list)
|
|
712
|
+
except Exception:
|
|
713
|
+
iterator = []
|
|
714
|
+
for node in iterator:
|
|
715
|
+
nodes.append(_fusion_node_summary(node))
|
|
716
|
+
return {"nodes": nodes, "count": len(nodes)}
|
|
717
|
+
|
|
718
|
+
if action == "add_node":
|
|
719
|
+
tool_id = str(params.get("toolId") or "")
|
|
720
|
+
if not tool_id:
|
|
721
|
+
raise RuntimeError("add_node requires 'toolId' (e.g. 'TextPlus', 'Background', 'Merge')")
|
|
722
|
+
if not hasattr(comp, "AddTool"):
|
|
723
|
+
raise RuntimeError("this Resolve build's Comp has no AddTool")
|
|
724
|
+
node = comp.AddTool(tool_id)
|
|
725
|
+
if node is None:
|
|
726
|
+
raise RuntimeError(f"AddTool('{tool_id}') returned None; verify the tool ID exists")
|
|
727
|
+
wanted_name = params.get("name")
|
|
728
|
+
if wanted_name and hasattr(node, "SetAttrs"):
|
|
729
|
+
try:
|
|
730
|
+
node.SetAttrs({"TOOLS_Name": str(wanted_name)})
|
|
731
|
+
except Exception:
|
|
732
|
+
pass
|
|
733
|
+
return _fusion_node_summary(node)
|
|
734
|
+
|
|
735
|
+
if action == "delete_node":
|
|
736
|
+
name = str(params.get("name") or "")
|
|
737
|
+
if not name:
|
|
738
|
+
raise RuntimeError("delete_node requires 'name'")
|
|
739
|
+
node = comp.FindTool(name) if hasattr(comp, "FindTool") else None
|
|
740
|
+
if node is None:
|
|
741
|
+
raise RuntimeError(f"node not found: {name}")
|
|
742
|
+
if not hasattr(node, "Delete"):
|
|
743
|
+
raise RuntimeError("this Resolve build's Tool has no Delete")
|
|
744
|
+
node.Delete()
|
|
745
|
+
return {"deleted": name}
|
|
746
|
+
|
|
747
|
+
if action == "connect":
|
|
748
|
+
from_name = str(params.get("fromNode") or "")
|
|
749
|
+
to_name = str(params.get("toNode") or "")
|
|
750
|
+
from_output = str(params.get("fromOutput") or "Output")
|
|
751
|
+
to_input = str(params.get("toInput") or "Input")
|
|
752
|
+
if not from_name or not to_name:
|
|
753
|
+
raise RuntimeError("connect requires 'fromNode' and 'toNode'")
|
|
754
|
+
a = comp.FindTool(from_name)
|
|
755
|
+
b = comp.FindTool(to_name)
|
|
756
|
+
if a is None or b is None:
|
|
757
|
+
raise RuntimeError(f"node not found (from={from_name}, to={to_name})")
|
|
758
|
+
try:
|
|
759
|
+
out = a.FindMainOutput(1) if from_output == "Output" else getattr(a, from_output, None)
|
|
760
|
+
except Exception:
|
|
761
|
+
out = None
|
|
762
|
+
if out is None:
|
|
763
|
+
raise RuntimeError(f"output '{from_output}' not found on {from_name}")
|
|
764
|
+
try:
|
|
765
|
+
inp = getattr(b, to_input, None)
|
|
766
|
+
if inp is None and hasattr(b, "FindMainInput"):
|
|
767
|
+
inp = b.FindMainInput(1)
|
|
768
|
+
except Exception:
|
|
769
|
+
inp = None
|
|
770
|
+
if inp is None:
|
|
771
|
+
raise RuntimeError(f"input '{to_input}' not found on {to_name}")
|
|
772
|
+
try:
|
|
773
|
+
inp.ConnectTo(out)
|
|
774
|
+
except Exception as e:
|
|
775
|
+
raise RuntimeError(f"ConnectTo failed: {e}")
|
|
776
|
+
return {"from": from_name, "to": to_name}
|
|
777
|
+
|
|
778
|
+
if action == "set_input":
|
|
779
|
+
name = str(params.get("node") or "")
|
|
780
|
+
input_name = str(params.get("input") or "")
|
|
781
|
+
if not name or not input_name:
|
|
782
|
+
raise RuntimeError("set_input requires 'node' and 'input'")
|
|
783
|
+
if "value" not in params:
|
|
784
|
+
raise RuntimeError("set_input requires 'value'")
|
|
785
|
+
node = comp.FindTool(name)
|
|
786
|
+
if node is None:
|
|
787
|
+
raise RuntimeError(f"node not found: {name}")
|
|
788
|
+
try:
|
|
789
|
+
attr = getattr(node, input_name)
|
|
790
|
+
except Exception:
|
|
791
|
+
raise RuntimeError(f"input '{input_name}' not found on {name}")
|
|
792
|
+
try:
|
|
793
|
+
# Fusion inputs accept assignment directly: node.Input = value.
|
|
794
|
+
setattr(node, input_name, params["value"])
|
|
795
|
+
except Exception as e:
|
|
796
|
+
raise RuntimeError(f"set_input failed: {e}")
|
|
797
|
+
return {"node": name, "input": input_name}
|
|
798
|
+
|
|
799
|
+
if action == "get_input":
|
|
800
|
+
name = str(params.get("node") or "")
|
|
801
|
+
input_name = str(params.get("input") or "")
|
|
802
|
+
if not name or not input_name:
|
|
803
|
+
raise RuntimeError("get_input requires 'node' and 'input'")
|
|
804
|
+
node = comp.FindTool(name)
|
|
805
|
+
if node is None:
|
|
806
|
+
raise RuntimeError(f"node not found: {name}")
|
|
807
|
+
try:
|
|
808
|
+
attr = getattr(node, input_name)
|
|
809
|
+
except Exception:
|
|
810
|
+
raise RuntimeError(f"input '{input_name}' not found on {name}")
|
|
811
|
+
value = None
|
|
812
|
+
try:
|
|
813
|
+
# Fusion inputs are callable for current-value access on some builds.
|
|
814
|
+
value = attr() if callable(attr) else attr
|
|
815
|
+
except Exception:
|
|
816
|
+
value = attr
|
|
817
|
+
try:
|
|
818
|
+
json.dumps(value)
|
|
819
|
+
except Exception:
|
|
820
|
+
value = repr(value)
|
|
821
|
+
return {"node": name, "input": input_name, "value": value}
|
|
822
|
+
|
|
823
|
+
if action == "set_keyframe":
|
|
824
|
+
name = str(params.get("node") or "")
|
|
825
|
+
input_name = str(params.get("input") or "")
|
|
826
|
+
frame = params.get("frame")
|
|
827
|
+
if frame is None or not name or not input_name or "value" not in params:
|
|
828
|
+
raise RuntimeError("set_keyframe requires 'node', 'input', 'frame', 'value'")
|
|
829
|
+
node = comp.FindTool(name)
|
|
830
|
+
if node is None:
|
|
831
|
+
raise RuntimeError(f"node not found: {name}")
|
|
832
|
+
try:
|
|
833
|
+
attr = getattr(node, input_name)
|
|
834
|
+
except Exception:
|
|
835
|
+
raise RuntimeError(f"input '{input_name}' not found on {name}")
|
|
836
|
+
try:
|
|
837
|
+
# Comp.SetKeyFrames is the canonical path; older builds use
|
|
838
|
+
# input.SetKeyFrames or .SetExpression. Try the modern one first.
|
|
839
|
+
ok = comp.SetKeyFrames({attr: {int(frame): params["value"]}})
|
|
840
|
+
except Exception:
|
|
841
|
+
ok = False
|
|
842
|
+
if not ok and hasattr(attr, "SetKeyFrames"):
|
|
843
|
+
try:
|
|
844
|
+
attr.SetKeyFrames({int(frame): params["value"]})
|
|
845
|
+
ok = True
|
|
846
|
+
except Exception:
|
|
847
|
+
ok = False
|
|
848
|
+
if not ok:
|
|
849
|
+
raise RuntimeError("could not set keyframe via Comp.SetKeyFrames or Input.SetKeyFrames")
|
|
850
|
+
return {"node": name, "input": input_name, "frame": int(frame)}
|
|
851
|
+
|
|
852
|
+
if action == "set_render_range":
|
|
853
|
+
start = params.get("start")
|
|
854
|
+
end = params.get("end")
|
|
855
|
+
if start is None or end is None:
|
|
856
|
+
raise RuntimeError("set_render_range requires 'start' and 'end'")
|
|
857
|
+
try:
|
|
858
|
+
comp.SetAttrs({"COMPN_RenderStart": int(start), "COMPN_RenderEnd": int(end)})
|
|
859
|
+
except Exception as e:
|
|
860
|
+
raise RuntimeError(f"SetAttrs failed: {e}")
|
|
861
|
+
return {"start": int(start), "end": int(end)}
|
|
862
|
+
|
|
863
|
+
raise RuntimeError(
|
|
864
|
+
f"unknown fusion_comp action: {action}; valid: list_nodes, add_node, delete_node, connect, set_input, get_input, set_keyframe, set_render_range"
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def m_execute_code(params):
|
|
869
|
+
"""Escape hatch: run arbitrary user code with the Resolve API pre-bound.
|
|
870
|
+
|
|
871
|
+
Pre-bound globals available to the snippet:
|
|
872
|
+
resolve, project, projectManager, mediaPool, mediaStorage, timeline,
|
|
873
|
+
fusion (lazy — None if Fusion comp is unavailable), dvr (the
|
|
874
|
+
DaVinciResolveScript module itself).
|
|
875
|
+
|
|
876
|
+
Result delivery (in priority order):
|
|
877
|
+
1. Call set_result(value) — explicit, supports any JSON-serialisable value.
|
|
878
|
+
2. Assign to a top-level 'result' variable.
|
|
879
|
+
3. Otherwise: stdout text only.
|
|
880
|
+
|
|
881
|
+
stdout is captured and returned regardless. The snippet runs in the same
|
|
882
|
+
process as every other bridge call, so a crash here CAN take the bridge
|
|
883
|
+
down. We trap exceptions but a segfault in fusionscript.so cannot be caught.
|
|
884
|
+
"""
|
|
885
|
+
code = params.get("code")
|
|
886
|
+
if not isinstance(code, str) or not code.strip():
|
|
887
|
+
raise RuntimeError("execute_code requires a non-empty 'code' string.")
|
|
888
|
+
|
|
889
|
+
import io
|
|
890
|
+
import contextlib
|
|
891
|
+
|
|
892
|
+
resolve = get_resolve()
|
|
893
|
+
project_manager = resolve.GetProjectManager()
|
|
894
|
+
project = project_manager.GetCurrentProject() if project_manager else None
|
|
895
|
+
media_pool = project.GetMediaPool() if project else None
|
|
896
|
+
media_storage = resolve.GetMediaStorage()
|
|
897
|
+
try:
|
|
898
|
+
timeline = project.GetCurrentTimeline() if project else None
|
|
899
|
+
except Exception:
|
|
900
|
+
timeline = None
|
|
901
|
+
try:
|
|
902
|
+
fusion = resolve.Fusion()
|
|
903
|
+
except Exception:
|
|
904
|
+
fusion = None
|
|
905
|
+
|
|
906
|
+
# JSON-serialisability check that won't crash on Resolve API objects (which
|
|
907
|
+
# are PyRemoteObject and not JSON-able). Anything non-serialisable is
|
|
908
|
+
# coerced to its repr so the agent at least sees what came back.
|
|
909
|
+
def _safe(value):
|
|
910
|
+
try:
|
|
911
|
+
json.dumps(value)
|
|
912
|
+
return value
|
|
913
|
+
except Exception:
|
|
914
|
+
return repr(value)
|
|
915
|
+
|
|
916
|
+
holder = {"result": None, "set": False}
|
|
917
|
+
|
|
918
|
+
def set_result(value):
|
|
919
|
+
holder["result"] = _safe(value)
|
|
920
|
+
holder["set"] = True
|
|
921
|
+
|
|
922
|
+
g = {
|
|
923
|
+
"__builtins__": __builtins__,
|
|
924
|
+
"resolve": resolve,
|
|
925
|
+
"project": project,
|
|
926
|
+
"projectManager": project_manager,
|
|
927
|
+
"mediaPool": media_pool,
|
|
928
|
+
"mediaStorage": media_storage,
|
|
929
|
+
"timeline": timeline,
|
|
930
|
+
"fusion": fusion,
|
|
931
|
+
"dvr": dvr,
|
|
932
|
+
"set_result": set_result,
|
|
933
|
+
"json": json,
|
|
934
|
+
}
|
|
935
|
+
l = {}
|
|
936
|
+
|
|
937
|
+
buf = io.StringIO()
|
|
938
|
+
with contextlib.redirect_stdout(buf):
|
|
939
|
+
try:
|
|
940
|
+
exec(code, g, l)
|
|
941
|
+
except Exception as ex:
|
|
942
|
+
# Re-raise with the captured stdout attached so the agent gets both
|
|
943
|
+
# the print() output that ran before the crash AND the error.
|
|
944
|
+
stdout = buf.getvalue()
|
|
945
|
+
tail = ("\nstdout: " + stdout[-1500:]) if stdout else ""
|
|
946
|
+
raise RuntimeError(f"{ex.__class__.__name__}: {ex}{tail}") from ex
|
|
947
|
+
|
|
948
|
+
stdout = buf.getvalue()
|
|
949
|
+
|
|
950
|
+
# Result resolution: explicit set_result wins; else top-level 'result'.
|
|
951
|
+
if holder["set"]:
|
|
952
|
+
result_value = holder["result"]
|
|
953
|
+
elif "result" in l:
|
|
954
|
+
result_value = _safe(l["result"])
|
|
955
|
+
else:
|
|
956
|
+
result_value = None
|
|
957
|
+
|
|
958
|
+
out = {"result": result_value}
|
|
959
|
+
if stdout:
|
|
960
|
+
# Cap stdout to keep tool output token-economical.
|
|
961
|
+
if len(stdout) > 4000:
|
|
962
|
+
stdout = stdout[:2000] + "\n…[truncated]…\n" + stdout[-1500:]
|
|
963
|
+
out["stdout"] = stdout
|
|
964
|
+
return out
|
|
965
|
+
|
|
966
|
+
|
|
595
967
|
METHODS = {
|
|
596
968
|
"ping": m_ping,
|
|
597
969
|
"get_timeline": m_get_timeline,
|
|
@@ -616,6 +988,8 @@ METHODS = {
|
|
|
616
988
|
"add_track": m_add_track,
|
|
617
989
|
"set_clip_volume": m_set_clip_volume,
|
|
618
990
|
"import_subtitles": m_import_subtitles,
|
|
991
|
+
"fusion_comp": m_fusion_comp,
|
|
992
|
+
"execute_code": m_execute_code,
|
|
619
993
|
}
|
|
620
994
|
|
|
621
995
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-source.js","sourceRoot":"","sources":["../../../../src/core/hosts/resolve/bridge-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"bridge-source.js","sourceRoot":"","sources":["../../../../src/core/hosts/resolve/bridge-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAy/BlC,CAAC"}
|
|
@@ -8,9 +8,22 @@ export declare class ResolveBridge {
|
|
|
8
8
|
private readyPromise?;
|
|
9
9
|
private nextId;
|
|
10
10
|
private bridgePath?;
|
|
11
|
+
/**
|
|
12
|
+
* The child process is dead when:
|
|
13
|
+
* - we never spawned it,
|
|
14
|
+
* - it exited naturally (exitCode !== null),
|
|
15
|
+
* - or it was signalled (signalCode !== null).
|
|
16
|
+
*
|
|
17
|
+
* Node's ChildProcess exposes both fields directly so we don't keep a
|
|
18
|
+
* parallel `dead` flag in sync — the standard idiom across real codebases
|
|
19
|
+
* (mastra, openclaw, AFFiNE, midscene, paseo, takt, rivet) reads them
|
|
20
|
+
* straight off the child.
|
|
21
|
+
*/
|
|
22
|
+
private isChildDead;
|
|
11
23
|
/**
|
|
12
24
|
* Start the bridge. Idempotent — subsequent calls return the same readiness
|
|
13
|
-
* promise.
|
|
25
|
+
* promise. After the bridge dies (Resolve quit, Python crash), the next
|
|
26
|
+
* call respawns from scratch.
|
|
14
27
|
*/
|
|
15
28
|
ensureStarted(): Promise<void>;
|
|
16
29
|
private handshakeDone;
|
|
@@ -25,9 +38,15 @@ export declare class ResolveBridge {
|
|
|
25
38
|
kill(): void;
|
|
26
39
|
get scriptPath(): string | undefined;
|
|
27
40
|
}
|
|
28
|
-
interface PythonCmd {
|
|
41
|
+
export interface PythonCmd {
|
|
29
42
|
cmd: string;
|
|
30
43
|
args: string[];
|
|
44
|
+
/**
|
|
45
|
+
* `sys.prefix` of the interpreter, when probing succeeded. Used on Windows
|
|
46
|
+
* to set PYTHONHOME — multi-Python systems need it set or Resolve 20.3 can
|
|
47
|
+
* crash on bridge startup (samuelgursky/davinci-resolve-mcp #26).
|
|
48
|
+
*/
|
|
49
|
+
prefix?: string;
|
|
31
50
|
}
|
|
32
51
|
/**
|
|
33
52
|
* Probe for a working Python 3 interpreter. Order matters:
|
|
@@ -46,6 +65,5 @@ export declare function findPython(): PythonCmd | undefined;
|
|
|
46
65
|
* Honours pre-set env vars (so power users with custom installs aren't
|
|
47
66
|
* overridden).
|
|
48
67
|
*/
|
|
49
|
-
export declare function resolveEnv(): NodeJS.ProcessEnv;
|
|
50
|
-
export {};
|
|
68
|
+
export declare function resolveEnv(py?: PythonCmd): NodeJS.ProcessEnv;
|
|
51
69
|
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../../../src/core/hosts/resolve/bridge.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,WAAY,SAAQ,KAAK;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAC,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAAC,CAAgB;IACrC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,UAAU,CAAC,CAAS;IAE5B
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../../../src/core/hosts/resolve/bridge.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,WAAY,SAAQ,KAAK;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAC,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAkC;IACjD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAAC,CAAgB;IACrC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,UAAU,CAAC,CAAS;IAE5B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW;IAKnB;;;;OAIG;IACH,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAgG9B,OAAO,CAAC,aAAa,CAAS;IAE9B,OAAO,CAAC,UAAU;IAoClB;;;OAGG;IACG,IAAI,CAAC,CAAC,GAAG,OAAO,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GAClC,OAAO,CAAC,CAAC,CAAC;IAiDb,IAAI,IAAI,IAAI;IAeZ,IAAI,UAAU,IAAI,MAAM,GAAG,SAAS,CAEnC;CACF;AAID,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,IAAI,SAAS,GAAG,SAAS,CA4BlD;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC,UAAU,CAkC5D"}
|