davinci-resolve-mcp 2.43.0 → 2.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +3 -3
- package/docs/SKILL.md +56 -1
- package/docs/guides/control-panel.md +4 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +49 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +549 -1
- package/src/utils/analysis_store.py +20 -2
- package/src/utils/destructive_hook.py +5 -0
- package/src/utils/edit_engine.py +646 -0
- package/src/utils/entities.py +579 -0
- package/src/utils/timeline_brain_db.py +48 -1
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.45.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -112,6 +112,7 @@ from src.utils.fusion_group_settings import (
|
|
|
112
112
|
)
|
|
113
113
|
from src.utils import analysis_runs as _analysis_runs
|
|
114
114
|
from src.utils import brain_edits as _brain_edits
|
|
115
|
+
from src.utils import edit_engine as _edit_engine_mod
|
|
115
116
|
from src.utils import media_pool_changes as _media_pool_changes
|
|
116
117
|
from src.utils import timeline_versioning as _timeline_versioning
|
|
117
118
|
from src.utils import project_spec as _project_spec
|
|
@@ -782,6 +783,10 @@ _destructive_hook.register_preference_provider(_destructive_preference_provider)
|
|
|
782
783
|
_TOKEN_GATED_DESTRUCTIVE_ACTIONS = frozenset({
|
|
783
784
|
("timeline", "delete_track"),
|
|
784
785
|
("timeline", "apply_cuts"),
|
|
786
|
+
# Phase E edit-engine loops: plan → confirm → execute.
|
|
787
|
+
("edit_engine", "execute_selects"),
|
|
788
|
+
("edit_engine", "execute_tighten"),
|
|
789
|
+
("edit_engine", "execute_swap"),
|
|
785
790
|
("graph", "apply_grade_from_drx"),
|
|
786
791
|
("graph", "reset_all_grades"),
|
|
787
792
|
# 21.0 AI ops that render/generate NEW media files (additive, but expensive
|
|
@@ -15027,6 +15032,12 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
15027
15032
|
# Phase C — embeddings + similarity.
|
|
15028
15033
|
"build_embeddings",
|
|
15029
15034
|
"find_similar",
|
|
15035
|
+
# Phase D — cross-clip entities + bin briefing v2.
|
|
15036
|
+
"detect_entities",
|
|
15037
|
+
"commit_entities",
|
|
15038
|
+
"list_entities",
|
|
15039
|
+
"prepare_bin_briefing",
|
|
15040
|
+
"commit_bin_summary",
|
|
15030
15041
|
}:
|
|
15031
15042
|
root = resolve_media_analysis_output_root(
|
|
15032
15043
|
project_name=project_name,
|
|
@@ -15150,6 +15161,38 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
15150
15161
|
entity_types=entity_types,
|
|
15151
15162
|
limit=int(p.get("limit") or 10),
|
|
15152
15163
|
)
|
|
15164
|
+
# Phase D — cross-clip entities + bin briefing v2.
|
|
15165
|
+
if action == "detect_entities":
|
|
15166
|
+
from src.utils import entities
|
|
15167
|
+
return entities.detect_entities(
|
|
15168
|
+
project_root,
|
|
15169
|
+
threshold=float(p.get("threshold") or entities.DEFAULT_CLUSTER_THRESHOLD),
|
|
15170
|
+
min_cluster_size=int(p.get("min_cluster_size") or p.get("minClusterSize") or entities.DEFAULT_MIN_CLUSTER_SIZE),
|
|
15171
|
+
max_clusters=int(p.get("max_clusters") or p.get("maxClusters") or 24),
|
|
15172
|
+
job_id=p.get("job_id") or p.get("jobId"),
|
|
15173
|
+
)
|
|
15174
|
+
if action == "commit_entities":
|
|
15175
|
+
from src.utils import entities
|
|
15176
|
+
return entities.commit_entities(
|
|
15177
|
+
project_root,
|
|
15178
|
+
entities_payload=p.get("entities"),
|
|
15179
|
+
vision_token=p.get("vision_token") or p.get("visionToken"),
|
|
15180
|
+
author=p.get("author") or "host_chat",
|
|
15181
|
+
)
|
|
15182
|
+
if action == "list_entities":
|
|
15183
|
+
from src.utils import entities
|
|
15184
|
+
return entities.list_entities(project_root, kind=p.get("kind"))
|
|
15185
|
+
if action == "prepare_bin_briefing":
|
|
15186
|
+
from src.utils import entities
|
|
15187
|
+
return entities.prepare_bin_briefing(project_root)
|
|
15188
|
+
if action == "commit_bin_summary":
|
|
15189
|
+
from src.utils import entities
|
|
15190
|
+
return entities.commit_bin_summary(
|
|
15191
|
+
project_root,
|
|
15192
|
+
briefing=p.get("briefing") or p.get("summary"),
|
|
15193
|
+
briefing_token=p.get("briefing_token") or p.get("briefingToken"),
|
|
15194
|
+
author=p.get("author") or "host_chat",
|
|
15195
|
+
)
|
|
15153
15196
|
if action in {"build_index", "rebuild_index"}:
|
|
15154
15197
|
return build_analysis_index(project_root, index_path=p.get("index_path") or p.get("indexPath"))
|
|
15155
15198
|
if action == "index_status":
|
|
@@ -15566,6 +15609,11 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
15566
15609
|
"vision_pending_sweep",
|
|
15567
15610
|
"build_embeddings",
|
|
15568
15611
|
"find_similar",
|
|
15612
|
+
"detect_entities",
|
|
15613
|
+
"commit_entities",
|
|
15614
|
+
"list_entities",
|
|
15615
|
+
"prepare_bin_briefing",
|
|
15616
|
+
"commit_bin_summary",
|
|
15569
15617
|
"get_caps",
|
|
15570
15618
|
"set_caps_preset",
|
|
15571
15619
|
"get_usage",
|
|
@@ -15715,6 +15763,506 @@ def timeline_versioning(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
15715
15763
|
return _err(f"Unknown action: {action}")
|
|
15716
15764
|
|
|
15717
15765
|
|
|
15766
|
+
def _edit_engine_collect_items(tl, *, track_index: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
15767
|
+
"""Serialize video timeline items with the source mapping the planner needs."""
|
|
15768
|
+
rows: List[Dict[str, Any]] = []
|
|
15769
|
+
track_count = int(tl.GetTrackCount("video") or 0)
|
|
15770
|
+
indices = [int(track_index)] if track_index else range(1, track_count + 1)
|
|
15771
|
+
for index in indices:
|
|
15772
|
+
try:
|
|
15773
|
+
items = tl.GetItemListInTrack("video", index) or []
|
|
15774
|
+
except Exception:
|
|
15775
|
+
continue
|
|
15776
|
+
for item in items:
|
|
15777
|
+
try:
|
|
15778
|
+
row: Dict[str, Any] = {
|
|
15779
|
+
"track_index": index,
|
|
15780
|
+
"item_name": item.GetName(),
|
|
15781
|
+
"timeline_start_frame": _frame_int(item.GetStart()),
|
|
15782
|
+
"timeline_end_frame": _frame_int(item.GetEnd()),
|
|
15783
|
+
}
|
|
15784
|
+
except Exception:
|
|
15785
|
+
continue
|
|
15786
|
+
source_start = None
|
|
15787
|
+
if _has_method(item, "GetSourceStartFrame"):
|
|
15788
|
+
try:
|
|
15789
|
+
source_start = _frame_int(item.GetSourceStartFrame())
|
|
15790
|
+
except Exception:
|
|
15791
|
+
source_start = None
|
|
15792
|
+
if source_start is None and _has_method(item, "GetLeftOffset"):
|
|
15793
|
+
try:
|
|
15794
|
+
source_start = _frame_int(item.GetLeftOffset())
|
|
15795
|
+
except Exception:
|
|
15796
|
+
source_start = None
|
|
15797
|
+
row["source_start_frame"] = source_start or 0
|
|
15798
|
+
try:
|
|
15799
|
+
mpi = item.GetMediaPoolItem()
|
|
15800
|
+
if mpi is not None:
|
|
15801
|
+
row["media_ref"] = mpi.GetUniqueId()
|
|
15802
|
+
try:
|
|
15803
|
+
row["media_path"] = mpi.GetClipProperty("File Path")
|
|
15804
|
+
except Exception:
|
|
15805
|
+
pass
|
|
15806
|
+
except Exception:
|
|
15807
|
+
pass
|
|
15808
|
+
rows.append(row)
|
|
15809
|
+
return rows
|
|
15810
|
+
|
|
15811
|
+
|
|
15812
|
+
def _edit_engine_timeline_fps(tl) -> float:
|
|
15813
|
+
try:
|
|
15814
|
+
fps = float(tl.GetSetting("timelineFrameRate") or 0)
|
|
15815
|
+
return fps if fps > 0 else 24.0
|
|
15816
|
+
except Exception:
|
|
15817
|
+
return 24.0
|
|
15818
|
+
|
|
15819
|
+
|
|
15820
|
+
def _edit_engine_capture(tl) -> Dict[str, Any]:
|
|
15821
|
+
"""Duration/clip-count readback for execute results."""
|
|
15822
|
+
out: Dict[str, Any] = {}
|
|
15823
|
+
try:
|
|
15824
|
+
out["duration_seconds"] = _brain_edits.capture_metric(_brain_edits.METRIC_DURATION_SECONDS, tl)
|
|
15825
|
+
out["clip_count"] = _brain_edits.capture_metric(_brain_edits.METRIC_CLIP_COUNT, tl)
|
|
15826
|
+
except Exception:
|
|
15827
|
+
pass
|
|
15828
|
+
return out
|
|
15829
|
+
|
|
15830
|
+
|
|
15831
|
+
@mcp.tool()
|
|
15832
|
+
@_destructive_op("edit_engine")
|
|
15833
|
+
def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
15834
|
+
"""Evidence-driven edit loops (Phase E): selects assembly, tighten, swap.
|
|
15835
|
+
|
|
15836
|
+
Every loop is plan → confirm → execute. plan_* actions are dry-run by
|
|
15837
|
+
construction: they query the DB-canonical analysis store and return a
|
|
15838
|
+
per-decision rationale plus a stored plan_id. execute_* actions require a
|
|
15839
|
+
confirm_token, run under the version-on-mutate hook (the timeline is
|
|
15840
|
+
archived first), and return before/after metrics as readback.
|
|
15841
|
+
|
|
15842
|
+
Actions:
|
|
15843
|
+
- plan_selects(min_select_potential?, max_duration_seconds?, timeline_name?,
|
|
15844
|
+
max_shots?, handle_seconds?, analysis_root?) — rank shots by deep-tier
|
|
15845
|
+
select potential (clip-level fallback), story-spine order. Additive.
|
|
15846
|
+
- execute_selects(plan_id, confirm_token?) — creates a NEW selects timeline
|
|
15847
|
+
from the plan's clip in/out ranges. Nothing existing is touched.
|
|
15848
|
+
- plan_tighten(timeline_name?, target_ratio?, min_pause_seconds?,
|
|
15849
|
+
handle_seconds?) — dead-air lifts from transcript-gap evidence for the
|
|
15850
|
+
current (or named) timeline.
|
|
15851
|
+
- execute_tighten(plan_id, confirm_token?) — duplicates the timeline and
|
|
15852
|
+
applies the lifts (ripple) to the DUPLICATE, never the original.
|
|
15853
|
+
- plan_swap(track_index?, timeline_start_frame | item_name, kind?, limit?)
|
|
15854
|
+
— alternates for one timeline item via the similarity index.
|
|
15855
|
+
- execute_swap(plan_id, alternate_index, confirm_token?) — replaces the
|
|
15856
|
+
item in place (lift + positioned append, same slot) on the current
|
|
15857
|
+
timeline (version-archived first).
|
|
15858
|
+
- list_plans(limit?) / get_plan(plan_id)
|
|
15859
|
+
"""
|
|
15860
|
+
p = params or {}
|
|
15861
|
+
|
|
15862
|
+
def _project_context(*, need_resolve: bool) -> Tuple[Optional[Any], Optional[Any], Optional[str], Optional[Dict[str, Any]]]:
|
|
15863
|
+
# An explicit analysis_root always wins — the evidence DB the caller
|
|
15864
|
+
# analyzed into may differ from the provider's derived root (e.g. a
|
|
15865
|
+
# headless analysis keyed by project name only).
|
|
15866
|
+
analysis_root = p.get("analysis_root") or p.get("analysisRoot")
|
|
15867
|
+
explicit_root = str(analysis_root) if analysis_root and os.path.isdir(str(analysis_root)) else None
|
|
15868
|
+
vctx = _destructive_versioning_provider()
|
|
15869
|
+
if vctx is not None:
|
|
15870
|
+
r, proj, project_root, _name = vctx
|
|
15871
|
+
return r, proj, explicit_root or project_root, None
|
|
15872
|
+
if not need_resolve and explicit_root:
|
|
15873
|
+
return None, None, explicit_root, None
|
|
15874
|
+
return None, None, None, _err(
|
|
15875
|
+
"No project context — open a Resolve project (or pass analysis_root for plan_selects)."
|
|
15876
|
+
)
|
|
15877
|
+
|
|
15878
|
+
if action == "list_plans":
|
|
15879
|
+
_r, _proj, project_root, err = _project_context(need_resolve=False)
|
|
15880
|
+
if err:
|
|
15881
|
+
return err
|
|
15882
|
+
return _edit_engine_mod.list_plans(project_root, limit=int(p.get("limit") or 20))
|
|
15883
|
+
|
|
15884
|
+
if action == "get_plan":
|
|
15885
|
+
_r, _proj, project_root, err = _project_context(need_resolve=False)
|
|
15886
|
+
if err:
|
|
15887
|
+
return err
|
|
15888
|
+
plan = _edit_engine_mod.load_plan(project_root, str(p.get("plan_id") or p.get("planId") or ""))
|
|
15889
|
+
if not plan:
|
|
15890
|
+
return _err("plan not found")
|
|
15891
|
+
if plan.get("_corrupt"):
|
|
15892
|
+
return _err("plan file failed its fingerprint check — re-plan")
|
|
15893
|
+
return {"success": True, "plan": plan}
|
|
15894
|
+
|
|
15895
|
+
if action == "plan_selects":
|
|
15896
|
+
_r, _proj, project_root, err = _project_context(need_resolve=False)
|
|
15897
|
+
if err:
|
|
15898
|
+
return err
|
|
15899
|
+
return _edit_engine_mod.plan_selects(
|
|
15900
|
+
project_root,
|
|
15901
|
+
timeline_name=p.get("timeline_name") or p.get("timelineName"),
|
|
15902
|
+
max_duration_seconds=p.get("max_duration_seconds") or p.get("maxDurationSeconds"),
|
|
15903
|
+
min_select_potential=str(p.get("min_select_potential") or p.get("minSelectPotential") or "medium"),
|
|
15904
|
+
handle_seconds=float(p.get("handle_seconds") or p.get("handleSeconds") or _edit_engine_mod.DEFAULT_HANDLE_SECONDS),
|
|
15905
|
+
max_shots=int(p.get("max_shots") or p.get("maxShots") or 60),
|
|
15906
|
+
)
|
|
15907
|
+
|
|
15908
|
+
if action == "plan_tighten":
|
|
15909
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
15910
|
+
if err:
|
|
15911
|
+
return err
|
|
15912
|
+
tl = (_find_timeline_by_name(proj, p.get("timeline_name") or p.get("timelineName"))[0] if (p.get("timeline_name") or p.get("timelineName")) else proj.GetCurrentTimeline())
|
|
15913
|
+
if not tl:
|
|
15914
|
+
return _err("Timeline not found")
|
|
15915
|
+
items = _edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex"))
|
|
15916
|
+
return _edit_engine_mod.plan_tighten(
|
|
15917
|
+
project_root,
|
|
15918
|
+
items=items,
|
|
15919
|
+
timeline_name=tl.GetName(),
|
|
15920
|
+
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
15921
|
+
target_ratio=p.get("target_ratio") or p.get("targetRatio"),
|
|
15922
|
+
min_pause_seconds=float(p.get("min_pause_seconds") or p.get("minPauseSeconds") or _edit_engine_mod.DEFAULT_MIN_PAUSE_SECONDS),
|
|
15923
|
+
handle_seconds=float(p.get("handle_seconds") or p.get("handleSeconds") or _edit_engine_mod.DEFAULT_HANDLE_SECONDS),
|
|
15924
|
+
)
|
|
15925
|
+
|
|
15926
|
+
if action == "plan_swap":
|
|
15927
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
15928
|
+
if err:
|
|
15929
|
+
return err
|
|
15930
|
+
tl = (_find_timeline_by_name(proj, p.get("timeline_name") or p.get("timelineName"))[0] if (p.get("timeline_name") or p.get("timelineName")) else proj.GetCurrentTimeline())
|
|
15931
|
+
if not tl:
|
|
15932
|
+
return _err("Timeline not found")
|
|
15933
|
+
items = _edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex"))
|
|
15934
|
+
target = None
|
|
15935
|
+
wanted_start = p.get("timeline_start_frame") if p.get("timeline_start_frame") is not None else p.get("timelineStartFrame")
|
|
15936
|
+
wanted_name = p.get("item_name") or p.get("itemName")
|
|
15937
|
+
for row in items:
|
|
15938
|
+
if wanted_start is not None and int(row["timeline_start_frame"]) == int(wanted_start):
|
|
15939
|
+
target = row
|
|
15940
|
+
break
|
|
15941
|
+
if wanted_name and row.get("item_name") == wanted_name:
|
|
15942
|
+
target = row
|
|
15943
|
+
break
|
|
15944
|
+
if target is None:
|
|
15945
|
+
return _err(
|
|
15946
|
+
"Target item not found — pass timeline_start_frame or item_name "
|
|
15947
|
+
f"(items: {[{'name': r.get('item_name'), 'start': r['timeline_start_frame']} for r in items[:20]]})"
|
|
15948
|
+
)
|
|
15949
|
+
return _edit_engine_mod.plan_swap(
|
|
15950
|
+
project_root,
|
|
15951
|
+
item=target,
|
|
15952
|
+
timeline_name=tl.GetName(),
|
|
15953
|
+
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
15954
|
+
kind=str(p.get("kind") or "visual"),
|
|
15955
|
+
limit=int(p.get("limit") or 5),
|
|
15956
|
+
)
|
|
15957
|
+
|
|
15958
|
+
if action == "execute_selects":
|
|
15959
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
15960
|
+
if err:
|
|
15961
|
+
return err
|
|
15962
|
+
plan = _edit_engine_mod.load_plan(project_root, str(p.get("plan_id") or p.get("planId") or ""))
|
|
15963
|
+
if not plan or plan.get("_corrupt"):
|
|
15964
|
+
return _err("plan not found (or failed its fingerprint check) — re-plan")
|
|
15965
|
+
if plan.get("kind") != "selects":
|
|
15966
|
+
return _err(f"plan {plan.get('plan_id')} is a {plan.get('kind')} plan")
|
|
15967
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
15968
|
+
return _issue_confirm_token(
|
|
15969
|
+
action="edit_engine.execute_selects", params=p,
|
|
15970
|
+
preview={
|
|
15971
|
+
"operation": "edit_engine.execute_selects",
|
|
15972
|
+
"warning": "Creates a NEW selects timeline; no existing timeline is modified.",
|
|
15973
|
+
"timeline_name": plan.get("timeline_name"),
|
|
15974
|
+
"decision_count": len(plan.get("decisions") or []),
|
|
15975
|
+
"estimated_duration_seconds": plan.get("estimated_duration_seconds"),
|
|
15976
|
+
},
|
|
15977
|
+
)
|
|
15978
|
+
blocked = _consume_confirm_token(action="edit_engine.execute_selects", params=p)
|
|
15979
|
+
if blocked:
|
|
15980
|
+
return blocked
|
|
15981
|
+
mp = proj.GetMediaPool()
|
|
15982
|
+
if not mp:
|
|
15983
|
+
return _err("No media pool")
|
|
15984
|
+
root_folder = mp.GetRootFolder()
|
|
15985
|
+
name = str(plan.get("timeline_name") or "Selects")
|
|
15986
|
+
if _find_timeline_by_name(proj, name)[0]:
|
|
15987
|
+
name = f"{name} {time.strftime('%H%M%S')}"
|
|
15988
|
+
tl = mp.CreateEmptyTimeline(name)
|
|
15989
|
+
if not tl:
|
|
15990
|
+
return _err("Failed to create selects timeline")
|
|
15991
|
+
try:
|
|
15992
|
+
proj.SetCurrentTimeline(tl)
|
|
15993
|
+
except Exception:
|
|
15994
|
+
pass
|
|
15995
|
+
timeline_start = _timeline_start_frame(tl)
|
|
15996
|
+
built = []
|
|
15997
|
+
build_errors = []
|
|
15998
|
+
record_cursor = 0
|
|
15999
|
+
for i, ci in enumerate(plan.get("clip_infos") or []):
|
|
16000
|
+
append_ci = dict(ci)
|
|
16001
|
+
# Sequential assembly: each select lands after the previous one.
|
|
16002
|
+
append_ci.setdefault("record_frame", record_cursor)
|
|
16003
|
+
append_ci.setdefault("track_index", 1)
|
|
16004
|
+
row, row_err = _build_append_clip_info_dict(root_folder, append_ci, i, timeline_start)
|
|
16005
|
+
if row_err:
|
|
16006
|
+
build_errors.append({"index": i, "error": row_err.get("error")})
|
|
16007
|
+
continue
|
|
16008
|
+
built.append(row)
|
|
16009
|
+
record_cursor += int(append_ci.get("end_frame", 0)) - int(append_ci.get("start_frame", 0)) + 1
|
|
16010
|
+
appended = mp.AppendToTimeline(built) if built else None
|
|
16011
|
+
readback = _edit_engine_capture(tl)
|
|
16012
|
+
run_id = _analysis_runs.current_run_id()
|
|
16013
|
+
try:
|
|
16014
|
+
_brain_edits.log_brain_edit(
|
|
16015
|
+
project_root=project_root,
|
|
16016
|
+
analysis_run_id=run_id or "edit-engine",
|
|
16017
|
+
edit_type="edit_engine.selects_result",
|
|
16018
|
+
tool_name="edit_engine",
|
|
16019
|
+
action_name="execute_selects",
|
|
16020
|
+
timeline_after=tl.GetName(),
|
|
16021
|
+
target_metric=_brain_edits.METRIC_CLIP_COUNT,
|
|
16022
|
+
metric_direction="target_value",
|
|
16023
|
+
after_value=readback.get("clip_count"),
|
|
16024
|
+
rationale=plan.get("summary"),
|
|
16025
|
+
params={"plan_id": plan.get("plan_id")},
|
|
16026
|
+
result_summary={"appended": len(built), "build_errors": build_errors},
|
|
16027
|
+
)
|
|
16028
|
+
except Exception:
|
|
16029
|
+
pass
|
|
16030
|
+
_edit_engine_mod.mark_plan_executed(project_root, plan["plan_id"], {
|
|
16031
|
+
"timeline_name": tl.GetName(),
|
|
16032
|
+
"appended": len(built),
|
|
16033
|
+
"build_errors": build_errors,
|
|
16034
|
+
**readback,
|
|
16035
|
+
})
|
|
16036
|
+
return {
|
|
16037
|
+
"success": bool(appended),
|
|
16038
|
+
"timeline_name": tl.GetName(),
|
|
16039
|
+
"appended": len(built),
|
|
16040
|
+
"build_errors": build_errors,
|
|
16041
|
+
"readback": readback,
|
|
16042
|
+
"plan_id": plan.get("plan_id"),
|
|
16043
|
+
}
|
|
16044
|
+
|
|
16045
|
+
if action == "execute_tighten":
|
|
16046
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
16047
|
+
if err:
|
|
16048
|
+
return err
|
|
16049
|
+
plan = _edit_engine_mod.load_plan(project_root, str(p.get("plan_id") or p.get("planId") or ""))
|
|
16050
|
+
if not plan or plan.get("_corrupt"):
|
|
16051
|
+
return _err("plan not found (or failed its fingerprint check) — re-plan")
|
|
16052
|
+
if plan.get("kind") != "tighten":
|
|
16053
|
+
return _err(f"plan {plan.get('plan_id')} is a {plan.get('kind')} plan")
|
|
16054
|
+
lifts = plan.get("lifts") or []
|
|
16055
|
+
keep_ranges = plan.get("keep_ranges") or []
|
|
16056
|
+
if not keep_ranges:
|
|
16057
|
+
return _err("plan has no keep_ranges — re-plan with this version")
|
|
16058
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
16059
|
+
return _issue_confirm_token(
|
|
16060
|
+
action="edit_engine.execute_tighten", params=p,
|
|
16061
|
+
preview={
|
|
16062
|
+
"operation": "edit_engine.execute_tighten",
|
|
16063
|
+
"warning": (
|
|
16064
|
+
"Assembles a tightened VARIANT timeline from the plan's keep "
|
|
16065
|
+
"ranges; the original timeline is not modified."
|
|
16066
|
+
),
|
|
16067
|
+
"timeline_name": plan.get("timeline_name"),
|
|
16068
|
+
"lift_count": len(lifts),
|
|
16069
|
+
"keep_range_count": len(keep_ranges),
|
|
16070
|
+
"estimated_removed_seconds": sum(l.get("duration_seconds") or 0 for l in lifts),
|
|
16071
|
+
},
|
|
16072
|
+
)
|
|
16073
|
+
blocked = _consume_confirm_token(action="edit_engine.execute_tighten", params=p)
|
|
16074
|
+
if blocked:
|
|
16075
|
+
return blocked
|
|
16076
|
+
source_tl, _src_index = _find_timeline_by_name(proj, plan.get("timeline_name"))
|
|
16077
|
+
if not source_tl:
|
|
16078
|
+
return _err(f"Timeline '{plan.get('timeline_name')}' not found")
|
|
16079
|
+
before = _edit_engine_capture(source_tl)
|
|
16080
|
+
variant_name = f"{plan.get('timeline_name')} — tightened {time.strftime('%H%M%S')}"
|
|
16081
|
+
variant = _timeline_create_variant_from_ranges(proj, source_tl, {
|
|
16082
|
+
"ranges": keep_ranges,
|
|
16083
|
+
"name": variant_name,
|
|
16084
|
+
})
|
|
16085
|
+
if not variant.get("success"):
|
|
16086
|
+
return {"success": False, "error": f"variant assembly failed: {variant.get('error')}", "variant": variant}
|
|
16087
|
+
new_tl, _new_index = _find_timeline_by_name(proj, variant.get("name") or variant_name)
|
|
16088
|
+
after = _edit_engine_capture(new_tl) if new_tl else {}
|
|
16089
|
+
run_id = _analysis_runs.current_run_id()
|
|
16090
|
+
try:
|
|
16091
|
+
_brain_edits.log_brain_edit(
|
|
16092
|
+
project_root=project_root,
|
|
16093
|
+
analysis_run_id=run_id or "edit-engine",
|
|
16094
|
+
edit_type="edit_engine.tighten_result",
|
|
16095
|
+
tool_name="edit_engine",
|
|
16096
|
+
action_name="execute_tighten",
|
|
16097
|
+
timeline_before=plan.get("timeline_name"),
|
|
16098
|
+
timeline_after=variant.get("name") or variant_name,
|
|
16099
|
+
target_metric=_brain_edits.METRIC_DURATION_SECONDS,
|
|
16100
|
+
metric_direction="decrease",
|
|
16101
|
+
before_value=before.get("duration_seconds"),
|
|
16102
|
+
after_value=after.get("duration_seconds"),
|
|
16103
|
+
rationale=plan.get("summary"),
|
|
16104
|
+
params={"plan_id": plan.get("plan_id")},
|
|
16105
|
+
result_summary={"keep_ranges": len(keep_ranges), "lifts": len(lifts)},
|
|
16106
|
+
)
|
|
16107
|
+
except Exception:
|
|
16108
|
+
pass
|
|
16109
|
+
_edit_engine_mod.mark_plan_executed(project_root, plan["plan_id"], {
|
|
16110
|
+
"variant_timeline": variant.get("name") or variant_name,
|
|
16111
|
+
"keep_ranges": len(keep_ranges),
|
|
16112
|
+
"lifts": len(lifts),
|
|
16113
|
+
"before": before,
|
|
16114
|
+
"after": after,
|
|
16115
|
+
})
|
|
16116
|
+
return {
|
|
16117
|
+
"success": True,
|
|
16118
|
+
"original_timeline": plan.get("timeline_name"),
|
|
16119
|
+
"variant_timeline": variant.get("name") or variant_name,
|
|
16120
|
+
"lifts_applied": len(lifts),
|
|
16121
|
+
"keep_ranges": len(keep_ranges),
|
|
16122
|
+
"lift_rationales": [
|
|
16123
|
+
{"lift": [l["timeline_start_frame"], l["timeline_end_frame"]], "rationale": l.get("rationale")}
|
|
16124
|
+
for l in lifts
|
|
16125
|
+
],
|
|
16126
|
+
"readback": {
|
|
16127
|
+
"before": before,
|
|
16128
|
+
"after": after,
|
|
16129
|
+
"removed_seconds": (
|
|
16130
|
+
round(before["duration_seconds"] - after["duration_seconds"], 2)
|
|
16131
|
+
if before.get("duration_seconds") is not None and after.get("duration_seconds") is not None
|
|
16132
|
+
else None
|
|
16133
|
+
),
|
|
16134
|
+
},
|
|
16135
|
+
"plan_id": plan.get("plan_id"),
|
|
16136
|
+
}
|
|
16137
|
+
|
|
16138
|
+
if action == "execute_swap":
|
|
16139
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
16140
|
+
if err:
|
|
16141
|
+
return err
|
|
16142
|
+
plan = _edit_engine_mod.load_plan(project_root, str(p.get("plan_id") or p.get("planId") or ""))
|
|
16143
|
+
if not plan or plan.get("_corrupt"):
|
|
16144
|
+
return _err("plan not found (or failed its fingerprint check) — re-plan")
|
|
16145
|
+
if plan.get("kind") != "swap":
|
|
16146
|
+
return _err(f"plan {plan.get('plan_id')} is a {plan.get('kind')} plan")
|
|
16147
|
+
alternates = plan.get("alternates") or []
|
|
16148
|
+
try:
|
|
16149
|
+
alt_index = int(p.get("alternate_index") if p.get("alternate_index") is not None else p.get("alternateIndex"))
|
|
16150
|
+
except (TypeError, ValueError):
|
|
16151
|
+
return _err(f"execute_swap requires alternate_index (0-{len(alternates) - 1})")
|
|
16152
|
+
if not (0 <= alt_index < len(alternates)):
|
|
16153
|
+
return _err(f"alternate_index out of range (0-{len(alternates) - 1})")
|
|
16154
|
+
alternate = alternates[alt_index]
|
|
16155
|
+
item_block = plan.get("item") or {}
|
|
16156
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
16157
|
+
return _issue_confirm_token(
|
|
16158
|
+
action="edit_engine.execute_swap", params=p,
|
|
16159
|
+
preview={
|
|
16160
|
+
"operation": "edit_engine.execute_swap",
|
|
16161
|
+
"warning": (
|
|
16162
|
+
"Replaces one timeline item in place (lift + positioned append). "
|
|
16163
|
+
"A timeline version is archived first."
|
|
16164
|
+
),
|
|
16165
|
+
"timeline_name": plan.get("timeline_name"),
|
|
16166
|
+
"slot": [item_block.get("timeline_start_frame"), item_block.get("timeline_end_frame")],
|
|
16167
|
+
"replacement": {
|
|
16168
|
+
"clip_name": alternate.get("clip_name"),
|
|
16169
|
+
"shot_index": alternate.get("shot_index"),
|
|
16170
|
+
"score": alternate.get("score"),
|
|
16171
|
+
},
|
|
16172
|
+
},
|
|
16173
|
+
)
|
|
16174
|
+
blocked = _consume_confirm_token(action="edit_engine.execute_swap", params=p)
|
|
16175
|
+
if blocked:
|
|
16176
|
+
return blocked
|
|
16177
|
+
tl, _tl_index = _find_timeline_by_name(proj, plan.get("timeline_name"))
|
|
16178
|
+
if not tl:
|
|
16179
|
+
return _err(f"Timeline '{plan.get('timeline_name')}' not found")
|
|
16180
|
+
try:
|
|
16181
|
+
proj.SetCurrentTimeline(tl)
|
|
16182
|
+
except Exception:
|
|
16183
|
+
pass
|
|
16184
|
+
before = _edit_engine_capture(tl)
|
|
16185
|
+
lift = _timeline_lift_range_impl(tl, {
|
|
16186
|
+
"start_frame": item_block.get("timeline_start_frame"),
|
|
16187
|
+
"end_frame": item_block.get("timeline_end_frame"),
|
|
16188
|
+
"allow_partial_item_delete": True,
|
|
16189
|
+
"ripple": False,
|
|
16190
|
+
})
|
|
16191
|
+
if not lift.get("success"):
|
|
16192
|
+
return {"success": False, "error": f"lift failed: {lift.get('error')}", "lift": lift}
|
|
16193
|
+
mp = proj.GetMediaPool()
|
|
16194
|
+
root_folder = mp.GetRootFolder()
|
|
16195
|
+
timeline_start = _timeline_start_frame(tl)
|
|
16196
|
+
frame_range = alternate.get("source_frame_range") or [0, 0]
|
|
16197
|
+
ci = {
|
|
16198
|
+
"clip_id": alternate.get("resolve_clip_id"),
|
|
16199
|
+
"start_frame": int(frame_range[0]),
|
|
16200
|
+
"end_frame": int(frame_range[1]),
|
|
16201
|
+
# Item starts come from item.GetStart() — absolute timeline frames.
|
|
16202
|
+
"record_frame": int(item_block.get("timeline_start_frame")),
|
|
16203
|
+
"record_frame_mode": "absolute",
|
|
16204
|
+
"track_index": int(item_block.get("track_index") or 1),
|
|
16205
|
+
}
|
|
16206
|
+
row, row_err = _build_append_clip_info_dict(root_folder, ci, 0, timeline_start)
|
|
16207
|
+
if row_err:
|
|
16208
|
+
return {"success": False, "error": f"swap append failed: {row_err.get('error')}", "lifted": lift}
|
|
16209
|
+
appended = mp.AppendToTimeline([row])
|
|
16210
|
+
after = _edit_engine_capture(tl)
|
|
16211
|
+
run_id = _analysis_runs.current_run_id()
|
|
16212
|
+
try:
|
|
16213
|
+
_brain_edits.log_brain_edit(
|
|
16214
|
+
project_root=project_root,
|
|
16215
|
+
analysis_run_id=run_id or "edit-engine",
|
|
16216
|
+
edit_type="edit_engine.swap_result",
|
|
16217
|
+
tool_name="edit_engine",
|
|
16218
|
+
action_name="execute_swap",
|
|
16219
|
+
timeline_after=tl.GetName(),
|
|
16220
|
+
target_metric=_brain_edits.METRIC_CLIP_COUNT,
|
|
16221
|
+
metric_direction="target_value",
|
|
16222
|
+
before_value=before.get("clip_count"),
|
|
16223
|
+
after_value=after.get("clip_count"),
|
|
16224
|
+
rationale=(
|
|
16225
|
+
f"Swapped slot {item_block.get('timeline_start_frame')}-"
|
|
16226
|
+
f"{item_block.get('timeline_end_frame')} with "
|
|
16227
|
+
f"{alternate.get('clip_name')} shot {alternate.get('shot_index')}: "
|
|
16228
|
+
f"{alternate.get('rationale')}"
|
|
16229
|
+
),
|
|
16230
|
+
params={"plan_id": plan.get("plan_id"), "alternate_index": alt_index},
|
|
16231
|
+
result_summary={"appended": bool(appended)},
|
|
16232
|
+
)
|
|
16233
|
+
except Exception:
|
|
16234
|
+
pass
|
|
16235
|
+
_edit_engine_mod.mark_plan_executed(project_root, plan["plan_id"], {
|
|
16236
|
+
"alternate_index": alt_index,
|
|
16237
|
+
"replacement": alternate.get("clip_name"),
|
|
16238
|
+
"before": before,
|
|
16239
|
+
"after": after,
|
|
16240
|
+
})
|
|
16241
|
+
return {
|
|
16242
|
+
"success": bool(appended),
|
|
16243
|
+
"timeline_name": tl.GetName(),
|
|
16244
|
+
"replaced_slot": [item_block.get("timeline_start_frame"), item_block.get("timeline_end_frame")],
|
|
16245
|
+
"replacement": {
|
|
16246
|
+
"clip_name": alternate.get("clip_name"),
|
|
16247
|
+
"shot_index": alternate.get("shot_index"),
|
|
16248
|
+
"score": alternate.get("score"),
|
|
16249
|
+
},
|
|
16250
|
+
"readback": {"before": before, "after": after},
|
|
16251
|
+
"plan_id": plan.get("plan_id"),
|
|
16252
|
+
}
|
|
16253
|
+
|
|
16254
|
+
return _unknown(action, [
|
|
16255
|
+
"plan_selects",
|
|
16256
|
+
"execute_selects",
|
|
16257
|
+
"plan_tighten",
|
|
16258
|
+
"execute_tighten",
|
|
16259
|
+
"plan_swap",
|
|
16260
|
+
"execute_swap",
|
|
16261
|
+
"list_plans",
|
|
16262
|
+
"get_plan",
|
|
16263
|
+
])
|
|
16264
|
+
|
|
16265
|
+
|
|
15718
16266
|
@mcp.tool()
|
|
15719
16267
|
@_destructive_op("timeline")
|
|
15720
16268
|
def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
@@ -473,7 +473,25 @@ def ingest_report(
|
|
|
473
473
|
),
|
|
474
474
|
)
|
|
475
475
|
|
|
476
|
-
# Sampled frames.
|
|
476
|
+
# Sampled frames. Shot mapping uses frame_indices_used when present,
|
|
477
|
+
# with a time-containment fallback (some commit paths don't record
|
|
478
|
+
# which frame indices fed which shot).
|
|
479
|
+
shot_intervals: List[Tuple[float, float, str]] = []
|
|
480
|
+
for entry in shot_entries:
|
|
481
|
+
if not isinstance(entry, dict):
|
|
482
|
+
continue
|
|
483
|
+
s, e = entry.get("time_seconds_start"), entry.get("time_seconds_end")
|
|
484
|
+
if isinstance(s, (int, float)) and isinstance(e, (int, float)):
|
|
485
|
+
shot_intervals.append((float(s), float(e), shot_uuid_for(clip_uuid, s, e)))
|
|
486
|
+
|
|
487
|
+
def _shot_for_time(t: Any) -> Optional[str]:
|
|
488
|
+
if not isinstance(t, (int, float)):
|
|
489
|
+
return None
|
|
490
|
+
for s, e, uuid_ in shot_intervals:
|
|
491
|
+
if s <= float(t) < e:
|
|
492
|
+
return uuid_
|
|
493
|
+
return None
|
|
494
|
+
|
|
477
495
|
conn.execute("DELETE FROM frames WHERE clip_uuid = ?", (clip_uuid,))
|
|
478
496
|
keyframes = motion.get("analysis_keyframes") if isinstance(motion.get("analysis_keyframes"), list) else []
|
|
479
497
|
for kf in keyframes:
|
|
@@ -492,7 +510,7 @@ def ingest_report(
|
|
|
492
510
|
""",
|
|
493
511
|
(
|
|
494
512
|
clip_uuid,
|
|
495
|
-
frame_to_shot.get(frame_index),
|
|
513
|
+
frame_to_shot.get(frame_index) or _shot_for_time(kf.get("time_seconds")),
|
|
496
514
|
frame_index,
|
|
497
515
|
kf.get("time_seconds") if isinstance(kf.get("time_seconds"), (int, float)) else None,
|
|
498
516
|
kf.get("frame_path") or kf.get("path"),
|
|
@@ -56,6 +56,11 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
56
56
|
"delete_clip_mattes",
|
|
57
57
|
"delete_timelines",
|
|
58
58
|
}),
|
|
59
|
+
"edit_engine": frozenset({
|
|
60
|
+
"execute_selects",
|
|
61
|
+
"execute_tighten",
|
|
62
|
+
"execute_swap",
|
|
63
|
+
}),
|
|
59
64
|
"timeline": frozenset({
|
|
60
65
|
"create_timeline",
|
|
61
66
|
"create_timeline_from_clips",
|