davinci-resolve-mcp 2.68.2 → 2.69.1
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 +184 -0
- package/README.md +46 -3
- package/docs/SKILL.md +181 -1
- package/docs/install.md +27 -1
- package/install.py +26 -1
- package/package.json +1 -1
- package/resolve-advanced/LICENSE +21 -0
- package/resolve-advanced/package.json +2 -1
- package/resolve-advanced/vendor/conform-qc/package.json +1 -0
- package/resolve-advanced/vendor/drp-format/package.json +1 -0
- package/resolve-advanced/vendor/drt-format/package.json +1 -0
- package/scripts/doctor.py +129 -3
- package/src/granular/common.py +1 -1
- package/src/server.py +522 -3
- package/src/utils/beat_detection.py +242 -0
- package/src/utils/bridge_differential.py +68 -2
- package/src/utils/broll_placement.py +201 -0
- package/src/utils/conform_lint.py +437 -0
- package/src/utils/edit_engine.py +633 -1
- package/src/utils/edit_handles.py +206 -0
- package/src/utils/edit_report.py +360 -0
- package/src/utils/first_impression.py +212 -0
- package/src/utils/prebalance.py +471 -0
- package/src/utils/project_journal.py +360 -0
- package/src/utils/reference_match.py +203 -0
- package/src/utils/rhythm_audit.py +252 -0
- package/src/utils/rule_of_six.py +217 -0
- package/src/utils/setup_sheet.py +121 -0
- package/src/utils/shot_assembly.py +259 -0
- package/src/utils/silence_ripple.py +89 -2
- package/src/utils/sound_density.py +253 -0
- package/src/utils/split_edits.py +180 -0
- package/src/utils/take_ranking.py +206 -0
- package/src/utils/transcript_edit.py +101 -5
- package/src/utils/turnover.py +187 -0
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.69.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -124,6 +124,10 @@ from src.utils.fusion_group_settings import (
|
|
|
124
124
|
from src.utils import analysis_runs as _analysis_runs
|
|
125
125
|
from src.utils import brain_edits as _brain_edits
|
|
126
126
|
from src.utils import edit_engine as _edit_engine_mod
|
|
127
|
+
from src.utils import edit_report as _edit_report_mod
|
|
128
|
+
from src.utils import conform_lint as _conform_lint_mod
|
|
129
|
+
from src.utils import first_impression as _first_impression_mod
|
|
130
|
+
from src.utils import project_journal as _journal_mod
|
|
127
131
|
from src.utils import transcript_edit as _transcript_edit_defaults
|
|
128
132
|
from src.utils import media_pool_changes as _media_pool_changes
|
|
129
133
|
from src.utils import timeline_versioning as _timeline_versioning
|
|
@@ -163,8 +167,13 @@ mcp = FastMCP(
|
|
|
163
167
|
"DaVinciResolveMCP",
|
|
164
168
|
instructions=(
|
|
165
169
|
"DaVinci Resolve MCP Server — controls Resolve via its Scripting API. "
|
|
166
|
-
"
|
|
167
|
-
"
|
|
170
|
+
"If no Resolve is running, tools launch one (up to 60s on the first call); "
|
|
171
|
+
"if one is already running they never launch a second. "
|
|
172
|
+
"On a connection error, read the error's own remediation field — it names the "
|
|
173
|
+
"fix that applies. External scripting is Studio-only, but the free edition is "
|
|
174
|
+
"reachable via the in-app bridge (Workspace > Scripts > resolve_bridge, with "
|
|
175
|
+
"DAVINCI_RESOLVE_BRIDGE=1), so a connection error does NOT mean the free "
|
|
176
|
+
"edition is unsupported."
|
|
168
177
|
),
|
|
169
178
|
)
|
|
170
179
|
|
|
@@ -18828,10 +18837,137 @@ def _edit_engine_collect_items(tl, *, track_index: Optional[int] = None) -> List
|
|
|
18828
18837
|
row["audio_track_indices"] = audio_indices
|
|
18829
18838
|
except Exception:
|
|
18830
18839
|
row["audio_track_indices"] = []
|
|
18840
|
+
_edit_engine_add_conform_fields(item, row)
|
|
18831
18841
|
rows.append(row)
|
|
18832
18842
|
return rows
|
|
18833
18843
|
|
|
18834
18844
|
|
|
18845
|
+
def _edit_engine_add_conform_fields(item, row: Dict[str, Any]) -> None:
|
|
18846
|
+
"""Extra per-item metadata the conform lint needs.
|
|
18847
|
+
|
|
18848
|
+
Best-effort and individually guarded: a field this API does not expose on a
|
|
18849
|
+
given build must leave the rest of the row intact. Absent fields are
|
|
18850
|
+
reported by the lint as *not checked* rather than silently passing, so a
|
|
18851
|
+
missing value degrades coverage honestly instead of manufacturing a clean
|
|
18852
|
+
bill of health.
|
|
18853
|
+
"""
|
|
18854
|
+
mpi = None
|
|
18855
|
+
try:
|
|
18856
|
+
mpi = item.GetMediaPoolItem()
|
|
18857
|
+
except Exception:
|
|
18858
|
+
mpi = None
|
|
18859
|
+
if mpi is not None:
|
|
18860
|
+
for key, prop in (
|
|
18861
|
+
("source_start_timecode", "Start TC"),
|
|
18862
|
+
("reel_name", "Reel Name"),
|
|
18863
|
+
("clip_fps", "FPS"),
|
|
18864
|
+
):
|
|
18865
|
+
try:
|
|
18866
|
+
value = mpi.GetClipProperty(prop)
|
|
18867
|
+
except Exception:
|
|
18868
|
+
continue
|
|
18869
|
+
if value in (None, ""):
|
|
18870
|
+
continue
|
|
18871
|
+
if key == "clip_fps":
|
|
18872
|
+
try:
|
|
18873
|
+
row[key] = float(value)
|
|
18874
|
+
except (TypeError, ValueError):
|
|
18875
|
+
continue
|
|
18876
|
+
else:
|
|
18877
|
+
row[key] = value
|
|
18878
|
+
try:
|
|
18879
|
+
fusion_names = [c.GetName() for c in (item.GetFusionCompNameList() or [])] \
|
|
18880
|
+
if _has_method(item, "GetFusionCompNameList") else []
|
|
18881
|
+
except Exception:
|
|
18882
|
+
fusion_names = []
|
|
18883
|
+
effects: List[str] = [n for n in fusion_names if n]
|
|
18884
|
+
for getter in ("GetVersionNameList",):
|
|
18885
|
+
if not _has_method(item, getter):
|
|
18886
|
+
continue
|
|
18887
|
+
try:
|
|
18888
|
+
extra = getattr(item, getter)(0) or []
|
|
18889
|
+
effects.extend(str(e) for e in extra if e)
|
|
18890
|
+
except Exception:
|
|
18891
|
+
pass
|
|
18892
|
+
if effects:
|
|
18893
|
+
row["effects"] = effects
|
|
18894
|
+
|
|
18895
|
+
|
|
18896
|
+
def _attach_audit_report(result: Dict[str, Any], params: Dict[str, Any]) -> Dict[str, Any]:
|
|
18897
|
+
"""Give every audit a human-readable form on request (invariant I5).
|
|
18898
|
+
|
|
18899
|
+
Off by default because a rendered report costs tokens on every call and the
|
|
18900
|
+
structured payload is what an agent acts on. But it must always be one
|
|
18901
|
+
parameter away: an audit a human cannot read is an audit they will re-do by
|
|
18902
|
+
hand, which is the cost the tool exists to remove.
|
|
18903
|
+
"""
|
|
18904
|
+
if not isinstance(result, dict) or not result.get("success"):
|
|
18905
|
+
return result
|
|
18906
|
+
result["report_available"] = "pass include_report=true for a Markdown report"
|
|
18907
|
+
flag = params.get("include_report", params.get("includeReport", False))
|
|
18908
|
+
if str(flag).strip().lower() in {"true", "1", "yes", "on"}:
|
|
18909
|
+
try:
|
|
18910
|
+
result["report_markdown"] = _edit_report_mod.render_any(result)
|
|
18911
|
+
except Exception as exc: # a renderer must never break the audit
|
|
18912
|
+
result["report_error"] = f"report rendering failed: {exc}"
|
|
18913
|
+
return result
|
|
18914
|
+
|
|
18915
|
+
|
|
18916
|
+
def _edit_engine_collect_audio_items(tl) -> List[Dict[str, Any]]:
|
|
18917
|
+
"""Audio-track item boundaries, for split-edit analysis.
|
|
18918
|
+
|
|
18919
|
+
Only the boundaries are needed — a split edit is defined by where the audio
|
|
18920
|
+
cut sits relative to the picture cut, not by what is in the clip.
|
|
18921
|
+
"""
|
|
18922
|
+
rows: List[Dict[str, Any]] = []
|
|
18923
|
+
try:
|
|
18924
|
+
track_count = int(tl.GetTrackCount("audio") or 0)
|
|
18925
|
+
except Exception:
|
|
18926
|
+
return rows
|
|
18927
|
+
for index in range(1, track_count + 1):
|
|
18928
|
+
try:
|
|
18929
|
+
items = tl.GetItemListInTrack("audio", index) or []
|
|
18930
|
+
except Exception:
|
|
18931
|
+
continue
|
|
18932
|
+
for item in items:
|
|
18933
|
+
try:
|
|
18934
|
+
rows.append({
|
|
18935
|
+
"track_index": index,
|
|
18936
|
+
"item_name": item.GetName(),
|
|
18937
|
+
"timeline_start_frame": _frame_int(item.GetStart()),
|
|
18938
|
+
"timeline_end_frame": _frame_int(item.GetEnd()),
|
|
18939
|
+
})
|
|
18940
|
+
except Exception:
|
|
18941
|
+
continue
|
|
18942
|
+
return rows
|
|
18943
|
+
|
|
18944
|
+
|
|
18945
|
+
def _edit_engine_marker_beats(tl, fps: float) -> List[float]:
|
|
18946
|
+
"""Timeline markers as story beats, in seconds.
|
|
18947
|
+
|
|
18948
|
+
Markers are where an editor has already said "something happens here", which
|
|
18949
|
+
makes them the best available proxy for a story beat. Absent markers are
|
|
18950
|
+
reported by the rhythm criterion as beats-not-supplied rather than as an
|
|
18951
|
+
absence of beats.
|
|
18952
|
+
"""
|
|
18953
|
+
beats: List[float] = []
|
|
18954
|
+
try:
|
|
18955
|
+
markers = tl.GetMarkers() or {}
|
|
18956
|
+
except Exception:
|
|
18957
|
+
return beats
|
|
18958
|
+
start = 0
|
|
18959
|
+
try:
|
|
18960
|
+
start = int(tl.GetStartFrame() or 0)
|
|
18961
|
+
except Exception:
|
|
18962
|
+
pass
|
|
18963
|
+
for frame in markers:
|
|
18964
|
+
try:
|
|
18965
|
+
beats.append((float(frame) + start) / (fps or 24.0))
|
|
18966
|
+
except (TypeError, ValueError):
|
|
18967
|
+
continue
|
|
18968
|
+
return sorted(beats)
|
|
18969
|
+
|
|
18970
|
+
|
|
18835
18971
|
def _edit_engine_timeline_fps(tl) -> float:
|
|
18836
18972
|
try:
|
|
18837
18973
|
fps = float(tl.GetSetting("timelineFrameRate") or 0)
|
|
@@ -19069,6 +19205,30 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
19069
19205
|
return _err("plan file failed its fingerprint check — re-plan")
|
|
19070
19206
|
return {"success": True, "plan": plan}
|
|
19071
19207
|
|
|
19208
|
+
if action == "plan_report":
|
|
19209
|
+
# A 340-entry keep_ranges array is not reviewable, and the rational
|
|
19210
|
+
# response to a machine you cannot audit is to re-check it by hand —
|
|
19211
|
+
# which is the cost the tool was supposed to remove. This renders any
|
|
19212
|
+
# plan as Markdown: what changes, why, what was deliberately left alone,
|
|
19213
|
+
# what could NOT be checked, and what needs a human.
|
|
19214
|
+
_r, _proj, project_root, err = _project_context(need_resolve=False)
|
|
19215
|
+
if err:
|
|
19216
|
+
return err
|
|
19217
|
+
plan = _edit_engine_mod.load_plan(project_root, str(p.get("plan_id") or p.get("planId") or ""))
|
|
19218
|
+
if not plan:
|
|
19219
|
+
return _err("plan not found")
|
|
19220
|
+
if plan.get("_corrupt"):
|
|
19221
|
+
return _err("plan file failed its fingerprint check — re-plan")
|
|
19222
|
+
_rows = p.get("max_detail_rows") if p.get("max_detail_rows") is not None else p.get("maxDetailRows")
|
|
19223
|
+
return {
|
|
19224
|
+
"success": True,
|
|
19225
|
+
"plan_id": plan.get("plan_id"),
|
|
19226
|
+
"summary": _edit_report_mod.summarize_for_chat(plan),
|
|
19227
|
+
"report_markdown": _edit_report_mod.render_plan_report(
|
|
19228
|
+
plan, max_detail_rows=int(_rows) if _rows is not None else 25
|
|
19229
|
+
),
|
|
19230
|
+
}
|
|
19231
|
+
|
|
19072
19232
|
if action == "plan_selects":
|
|
19073
19233
|
_r, _proj, project_root, err = _project_context(need_resolve=False)
|
|
19074
19234
|
if err:
|
|
@@ -19150,6 +19310,22 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
19150
19310
|
min_cut=float(p.get("min_cut", p.get("minCut", _transcript_edit_defaults.DEFAULT_MIN_CUT_S))),
|
|
19151
19311
|
)
|
|
19152
19312
|
|
|
19313
|
+
if action == "rank_takes":
|
|
19314
|
+
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19315
|
+
if err:
|
|
19316
|
+
return err
|
|
19317
|
+
refs = p.get("clip_refs") or p.get("clipRefs") or p.get("clip_ids") or []
|
|
19318
|
+
if isinstance(refs, str):
|
|
19319
|
+
refs = [refs]
|
|
19320
|
+
if not isinstance(refs, (list, tuple)) or not refs:
|
|
19321
|
+
return _err("clip_refs is required (a list of clips to compare)",
|
|
19322
|
+
code="MISSING_CLIP_REFS", category="invalid_input")
|
|
19323
|
+
return _edit_engine_mod.rank_takes(
|
|
19324
|
+
project_root,
|
|
19325
|
+
clip_refs=list(refs),
|
|
19326
|
+
script=p.get("script") if isinstance(p.get("script"), str) else None,
|
|
19327
|
+
)
|
|
19328
|
+
|
|
19153
19329
|
if action == "search_spoken_content":
|
|
19154
19330
|
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19155
19331
|
if err:
|
|
@@ -19193,6 +19369,332 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
19193
19369
|
include_audio=str(p.get("include_audio", p.get("includeAudio", True))).strip().lower() not in {"false", "0", "no", "none", "off"},
|
|
19194
19370
|
)
|
|
19195
19371
|
|
|
19372
|
+
if action == "rule_of_six_audit":
|
|
19373
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19374
|
+
if err:
|
|
19375
|
+
return err
|
|
19376
|
+
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())
|
|
19377
|
+
if not tl:
|
|
19378
|
+
return _err("Timeline not found")
|
|
19379
|
+
_fps = _edit_engine_timeline_fps(tl)
|
|
19380
|
+
return _attach_audit_report(_edit_engine_mod.rule_of_six_audit(
|
|
19381
|
+
items=_edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex")),
|
|
19382
|
+
timeline_name=tl.GetName(),
|
|
19383
|
+
timeline_fps=_fps,
|
|
19384
|
+
story_beats=_edit_engine_marker_beats(tl, _fps),
|
|
19385
|
+
), p)
|
|
19386
|
+
|
|
19387
|
+
if action == "split_edit_audit":
|
|
19388
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19389
|
+
if err:
|
|
19390
|
+
return err
|
|
19391
|
+
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())
|
|
19392
|
+
if not tl:
|
|
19393
|
+
return _err("Timeline not found")
|
|
19394
|
+
return _edit_engine_mod.split_edit_audit(
|
|
19395
|
+
video_items=_edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex")),
|
|
19396
|
+
audio_items=_edit_engine_collect_audio_items(tl),
|
|
19397
|
+
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
19398
|
+
)
|
|
19399
|
+
|
|
19400
|
+
if action == "sound_density_audit":
|
|
19401
|
+
_r, _proj, project_root, err = _project_context(need_resolve=False)
|
|
19402
|
+
if err:
|
|
19403
|
+
return err
|
|
19404
|
+
media = p.get("track_media") or p.get("trackMedia") or {}
|
|
19405
|
+
if not isinstance(media, dict):
|
|
19406
|
+
return _err("track_media must be a mapping of {name: path}",
|
|
19407
|
+
code="INVALID_TRACK_MEDIA", category="invalid_input")
|
|
19408
|
+
_limit = p.get("stream_limit") if p.get("stream_limit") is not None else p.get("streamLimit")
|
|
19409
|
+
_dur = p.get("duration_seconds") if p.get("duration_seconds") is not None else p.get("durationSeconds")
|
|
19410
|
+
return _attach_audit_report(_edit_engine_mod.sound_density_audit(
|
|
19411
|
+
track_media={str(k): str(v) for k, v in media.items()},
|
|
19412
|
+
stream_limit=float(_limit) if _limit is not None else None,
|
|
19413
|
+
duration_seconds=float(_dur) if _dur is not None else None,
|
|
19414
|
+
), p)
|
|
19415
|
+
|
|
19416
|
+
if action == "setup_sheet":
|
|
19417
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19418
|
+
if err:
|
|
19419
|
+
return err
|
|
19420
|
+
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())
|
|
19421
|
+
if not tl:
|
|
19422
|
+
return _err("Timeline not found")
|
|
19423
|
+
return _attach_audit_report(_edit_engine_mod.setup_sheet(
|
|
19424
|
+
items=_edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex")),
|
|
19425
|
+
timeline_name=tl.GetName(),
|
|
19426
|
+
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
19427
|
+
), p)
|
|
19428
|
+
|
|
19429
|
+
if action == "first_impression":
|
|
19430
|
+
# Measures nothing. Captures the one perception that cannot be recovered
|
|
19431
|
+
# once the editor has seen the film too many times to feel it fresh.
|
|
19432
|
+
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19433
|
+
if err:
|
|
19434
|
+
return err
|
|
19435
|
+
sub = str(p.get("op") or p.get("sub_action") or "").strip().lower()
|
|
19436
|
+
log_id = p.get("log_id") or p.get("logId")
|
|
19437
|
+
if sub == "list":
|
|
19438
|
+
return {"success": True, "logs": _first_impression_mod.list_logs(project_root)}
|
|
19439
|
+
if sub in ("start", "record", "lock", "get") and not log_id:
|
|
19440
|
+
return _err("log_id is required", code="MISSING_LOG_ID", category="invalid_input")
|
|
19441
|
+
if sub == "start":
|
|
19442
|
+
return _first_impression_mod.start_pass(
|
|
19443
|
+
project_root,
|
|
19444
|
+
timeline_name=str(p.get("timeline_name") or p.get("timelineName") or ""),
|
|
19445
|
+
log_id=str(log_id),
|
|
19446
|
+
viewer=p.get("viewer"),
|
|
19447
|
+
)
|
|
19448
|
+
if sub == "record":
|
|
19449
|
+
_t = p.get("time_seconds") if p.get("time_seconds") is not None else p.get("timeSeconds")
|
|
19450
|
+
try:
|
|
19451
|
+
return _first_impression_mod.record(
|
|
19452
|
+
project_root, log_id=str(log_id),
|
|
19453
|
+
time_seconds=float(_t or 0.0), text=str(p.get("text") or ""),
|
|
19454
|
+
)
|
|
19455
|
+
except _first_impression_mod.LogLocked as exc:
|
|
19456
|
+
return _err(str(exc), code="LOG_LOCKED", category="invalid_input", retryable=False)
|
|
19457
|
+
if sub == "lock":
|
|
19458
|
+
return _first_impression_mod.lock(project_root, log_id=str(log_id))
|
|
19459
|
+
if sub == "get":
|
|
19460
|
+
log = _first_impression_mod.load(project_root, log_id=str(log_id))
|
|
19461
|
+
return {"success": bool(log), "log": log} if log else _err("log not found")
|
|
19462
|
+
if sub == "diff":
|
|
19463
|
+
first = _first_impression_mod.load(project_root, log_id=str(p.get("first_log_id") or p.get("firstLogId") or ""))
|
|
19464
|
+
later = _first_impression_mod.load(project_root, log_id=str(p.get("later_log_id") or p.get("laterLogId") or ""))
|
|
19465
|
+
if not first or not later:
|
|
19466
|
+
return _err("both first_log_id and later_log_id must name existing logs")
|
|
19467
|
+
return {"success": True, **_first_impression_mod.diff(first, later)}
|
|
19468
|
+
return _err("op must be one of: start, record, lock, get, list, diff",
|
|
19469
|
+
code="UNKNOWN_OP", category="invalid_input")
|
|
19470
|
+
|
|
19471
|
+
if action == "journal":
|
|
19472
|
+
# The paperwork every craft role keeps and every tool skips: ingest log,
|
|
19473
|
+
# accumulating known issues, session prep, handoff, status.
|
|
19474
|
+
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19475
|
+
if err:
|
|
19476
|
+
return err
|
|
19477
|
+
op = str(p.get("op") or p.get("sub_action") or "").strip().lower()
|
|
19478
|
+
if op == "append":
|
|
19479
|
+
return _journal_mod.append(
|
|
19480
|
+
project_root,
|
|
19481
|
+
kind=str(p.get("kind") or "note"),
|
|
19482
|
+
summary=str(p.get("summary") or ""),
|
|
19483
|
+
detail=p.get("detail"),
|
|
19484
|
+
ref=p.get("ref"),
|
|
19485
|
+
data=p.get("data") if isinstance(p.get("data"), dict) else None,
|
|
19486
|
+
timestamp=time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
19487
|
+
)
|
|
19488
|
+
if op == "read":
|
|
19489
|
+
return {"success": True,
|
|
19490
|
+
"records": _journal_mod.read(project_root, kind=p.get("kind"))}
|
|
19491
|
+
if op == "known_issues":
|
|
19492
|
+
state = _journal_mod.open_issues(project_root)
|
|
19493
|
+
return {"success": True, **state,
|
|
19494
|
+
"report_markdown": _journal_mod.render_known_issues(project_root)}
|
|
19495
|
+
if op == "ingest_log":
|
|
19496
|
+
return {"success": True,
|
|
19497
|
+
"report_markdown": _journal_mod.render_ingest_log(project_root)}
|
|
19498
|
+
if op == "session_prep":
|
|
19499
|
+
_f = lambda k: (float(p[k]) if p.get(k) is not None else None)
|
|
19500
|
+
return {"success": True, "report_markdown": _journal_mod.render_session_prep(
|
|
19501
|
+
project_root,
|
|
19502
|
+
session_kind=str(p.get("session_kind") or "colour"),
|
|
19503
|
+
prep_hours=_f("prep_hours"),
|
|
19504
|
+
estimated_hours_saved=_f("estimated_hours_saved"),
|
|
19505
|
+
hourly_rate=_f("hourly_rate"),
|
|
19506
|
+
ready=p.get("ready") if isinstance(p.get("ready"), list) else None,
|
|
19507
|
+
outstanding=p.get("outstanding") if isinstance(p.get("outstanding"), list) else None,
|
|
19508
|
+
)}
|
|
19509
|
+
if op == "handoff":
|
|
19510
|
+
return {"success": True, "report_markdown": _journal_mod.render_handoff(
|
|
19511
|
+
project=str(p.get("project") or (proj.GetName() if proj else "project")),
|
|
19512
|
+
to=str(p.get("to") or "receiving facility"),
|
|
19513
|
+
timeline_name=p.get("timeline_name") or p.get("timelineName"),
|
|
19514
|
+
technical=p.get("technical") if isinstance(p.get("technical"), dict) else None,
|
|
19515
|
+
included=p.get("included") if isinstance(p.get("included"), list) else None,
|
|
19516
|
+
known_issues=[i.get("summary") for i in _journal_mod.open_issues(project_root)["open"]],
|
|
19517
|
+
notes=p.get("notes"),
|
|
19518
|
+
)}
|
|
19519
|
+
if op == "picture_lock":
|
|
19520
|
+
_r2, proj2, _pr, err2 = _project_context(need_resolve=True)
|
|
19521
|
+
if err2:
|
|
19522
|
+
return err2
|
|
19523
|
+
tl = (_find_timeline_by_name(proj2, p.get("timeline_name") or p.get("timelineName"))[0] if (p.get("timeline_name") or p.get("timelineName")) else proj2.GetCurrentTimeline())
|
|
19524
|
+
if not tl:
|
|
19525
|
+
return _err("Timeline not found")
|
|
19526
|
+
return _journal_mod.set_picture_lock(
|
|
19527
|
+
project_root, timeline_name=tl.GetName(),
|
|
19528
|
+
items=_edit_engine_collect_items(tl),
|
|
19529
|
+
timestamp=time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
19530
|
+
)
|
|
19531
|
+
if op == "check_lock":
|
|
19532
|
+
_r2, proj2, _pr, err2 = _project_context(need_resolve=True)
|
|
19533
|
+
if err2:
|
|
19534
|
+
return err2
|
|
19535
|
+
tl = (_find_timeline_by_name(proj2, p.get("timeline_name") or p.get("timelineName"))[0] if (p.get("timeline_name") or p.get("timelineName")) else proj2.GetCurrentTimeline())
|
|
19536
|
+
if not tl:
|
|
19537
|
+
return _err("Timeline not found")
|
|
19538
|
+
return {"success": True, **_journal_mod.check_picture_lock(
|
|
19539
|
+
project_root, timeline_name=tl.GetName(),
|
|
19540
|
+
items=_edit_engine_collect_items(tl),
|
|
19541
|
+
)}
|
|
19542
|
+
if op == "status":
|
|
19543
|
+
_pc = p.get("percent_complete") if p.get("percent_complete") is not None else p.get("percentComplete")
|
|
19544
|
+
return {"success": True, "report_markdown": _journal_mod.render_status(
|
|
19545
|
+
project=str(p.get("project") or (proj.GetName() if proj else "project")),
|
|
19546
|
+
phase=str(p.get("phase") or "unspecified"),
|
|
19547
|
+
percent_complete=float(_pc) if _pc is not None else None,
|
|
19548
|
+
blockers=p.get("blockers") if isinstance(p.get("blockers"), list) else None,
|
|
19549
|
+
next_milestone=p.get("next_milestone") or p.get("nextMilestone"),
|
|
19550
|
+
open_issue_count=len(_journal_mod.open_issues(project_root)["open"]),
|
|
19551
|
+
)}
|
|
19552
|
+
return _err("op must be one of: append, read, known_issues, ingest_log, "
|
|
19553
|
+
"session_prep, handoff, status, picture_lock, check_lock",
|
|
19554
|
+
code="UNKNOWN_OP", category="invalid_input")
|
|
19555
|
+
|
|
19556
|
+
if action == "plan_reference_match":
|
|
19557
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19558
|
+
if err:
|
|
19559
|
+
return err
|
|
19560
|
+
ref = p.get("reference_media") or p.get("referenceMedia")
|
|
19561
|
+
if not ref:
|
|
19562
|
+
return _err("reference_media is required (path to the graded reference still or clip)",
|
|
19563
|
+
code="MISSING_REFERENCE", category="invalid_input")
|
|
19564
|
+
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())
|
|
19565
|
+
if not tl:
|
|
19566
|
+
return _err("Timeline not found")
|
|
19567
|
+
_at = p.get("reference_at_seconds") if p.get("reference_at_seconds") is not None else p.get("referenceAtSeconds")
|
|
19568
|
+
_max = p.get("max_items") if p.get("max_items") is not None else p.get("maxItems")
|
|
19569
|
+
return _edit_engine_mod.plan_reference_match(
|
|
19570
|
+
project_root,
|
|
19571
|
+
reference_media=str(ref),
|
|
19572
|
+
reference_at_seconds=float(_at) if _at is not None else 0.0,
|
|
19573
|
+
items=_edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex")),
|
|
19574
|
+
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
19575
|
+
max_items=int(_max) if _max is not None else 200,
|
|
19576
|
+
)
|
|
19577
|
+
|
|
19578
|
+
if action == "plan_string_out":
|
|
19579
|
+
shots = p.get("shots")
|
|
19580
|
+
if not isinstance(shots, list) or not shots:
|
|
19581
|
+
return _err("shots is required: [{name, start_seconds, end_seconds, motion_energy?}]",
|
|
19582
|
+
code="MISSING_SHOTS", category="invalid_input")
|
|
19583
|
+
return _edit_engine_mod.plan_string_out(
|
|
19584
|
+
shots=shots, order=str(p.get("order") or "chronological"))
|
|
19585
|
+
|
|
19586
|
+
if action == "propose_structure":
|
|
19587
|
+
topics = p.get("topics")
|
|
19588
|
+
if not isinstance(topics, list) or not topics:
|
|
19589
|
+
return _err("topics is required: [{label, total_seconds, clip_count}]",
|
|
19590
|
+
code="MISSING_TOPICS", category="invalid_input")
|
|
19591
|
+
return _edit_engine_mod.propose_structure(topics=topics)
|
|
19592
|
+
|
|
19593
|
+
if action == "plan_broll":
|
|
19594
|
+
beats = p.get("beats")
|
|
19595
|
+
candidates = p.get("candidates")
|
|
19596
|
+
if not isinstance(beats, list) or not beats:
|
|
19597
|
+
return _err("beats is required: [{start_seconds, end_seconds, text?, protected?}]",
|
|
19598
|
+
code="MISSING_BEATS", category="invalid_input")
|
|
19599
|
+
if not isinstance(candidates, list) or not candidates:
|
|
19600
|
+
return _err("candidates is required: [{name, duration_seconds, relevance?, beat_index?}]",
|
|
19601
|
+
code="MISSING_CANDIDATES", category="invalid_input")
|
|
19602
|
+
return _edit_engine_mod.plan_broll(
|
|
19603
|
+
beats=beats, candidates=candidates,
|
|
19604
|
+
allow_reuse=str(p.get("allow_reuse", p.get("allowReuse", False))).strip().lower() in {"true", "1", "yes", "on"},
|
|
19605
|
+
)
|
|
19606
|
+
|
|
19607
|
+
if action == "plan_turnover":
|
|
19608
|
+
dests = p.get("destinations") or p.get("destination")
|
|
19609
|
+
if isinstance(dests, str):
|
|
19610
|
+
dests = [dests]
|
|
19611
|
+
if not isinstance(dests, list) or not dests:
|
|
19612
|
+
return _err("destinations is required: any of sound, vfx, color",
|
|
19613
|
+
code="MISSING_DESTINATIONS", category="invalid_input")
|
|
19614
|
+
_hf = p.get("handle_frames") if p.get("handle_frames") is not None else p.get("handleFrames")
|
|
19615
|
+
return _edit_engine_mod.plan_turnover(
|
|
19616
|
+
destinations=dests,
|
|
19617
|
+
contents=p.get("contents") if isinstance(p.get("contents"), dict) else {},
|
|
19618
|
+
version=str(p.get("version") or "v01"),
|
|
19619
|
+
handle_frames=int(_hf) if _hf is not None else None,
|
|
19620
|
+
)
|
|
19621
|
+
|
|
19622
|
+
if action == "conform_lint":
|
|
19623
|
+
# The online editor's checklist, run before turnover instead of
|
|
19624
|
+
# discovered after picture lock in someone else's suite.
|
|
19625
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19626
|
+
if err:
|
|
19627
|
+
return err
|
|
19628
|
+
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())
|
|
19629
|
+
if not tl:
|
|
19630
|
+
return _err("Timeline not found")
|
|
19631
|
+
items = _edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex"))
|
|
19632
|
+
return _attach_audit_report(_conform_lint_mod.lint_timeline({
|
|
19633
|
+
"timeline_name": tl.GetName(),
|
|
19634
|
+
"timeline_fps": _edit_engine_timeline_fps(tl),
|
|
19635
|
+
"items": items,
|
|
19636
|
+
}), p)
|
|
19637
|
+
|
|
19638
|
+
if action == "plan_beat_cuts":
|
|
19639
|
+
_r, proj, project_root, err = _project_context(need_resolve=False)
|
|
19640
|
+
if err:
|
|
19641
|
+
return err
|
|
19642
|
+
_fps = p.get("timeline_fps") if p.get("timeline_fps") is not None else p.get("timelineFps")
|
|
19643
|
+
return _edit_engine_mod.plan_beat_cuts(
|
|
19644
|
+
project_root,
|
|
19645
|
+
clip_ref=p.get("clip_ref") or p.get("clipRef"),
|
|
19646
|
+
media_path=p.get("media_path") or p.get("mediaPath"),
|
|
19647
|
+
timeline_fps=float(_fps) if _fps is not None else 24.0,
|
|
19648
|
+
mode=str(p.get("mode") or "phrase"),
|
|
19649
|
+
beats_per_bar=int(p.get("beats_per_bar") or p.get("beatsPerBar") or 4),
|
|
19650
|
+
bars_per_phrase=int(p.get("bars_per_phrase") or p.get("barsPerPhrase") or 8),
|
|
19651
|
+
beat_offset=int(p.get("beat_offset") or p.get("beatOffset") or 0),
|
|
19652
|
+
min_shot_seconds=float(p.get("min_shot_seconds") or p.get("minShotSeconds") or 0.0),
|
|
19653
|
+
)
|
|
19654
|
+
|
|
19655
|
+
if action == "plan_prebalance":
|
|
19656
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19657
|
+
if err:
|
|
19658
|
+
return err
|
|
19659
|
+
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())
|
|
19660
|
+
if not tl:
|
|
19661
|
+
return _err("Timeline not found")
|
|
19662
|
+
items = _edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex"))
|
|
19663
|
+
_max = p.get("max_items") if p.get("max_items") is not None else p.get("maxItems")
|
|
19664
|
+
return _edit_engine_mod.plan_prebalance(
|
|
19665
|
+
project_root,
|
|
19666
|
+
items=items,
|
|
19667
|
+
timeline_name=tl.GetName(),
|
|
19668
|
+
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
19669
|
+
max_items=int(_max) if _max is not None else 200,
|
|
19670
|
+
)
|
|
19671
|
+
|
|
19672
|
+
if action == "plan_dead_space_markers":
|
|
19673
|
+
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19674
|
+
if err:
|
|
19675
|
+
return err
|
|
19676
|
+
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())
|
|
19677
|
+
if not tl:
|
|
19678
|
+
return _err("Timeline not found")
|
|
19679
|
+
items = _edit_engine_collect_items(tl, track_index=p.get("track_index") or p.get("trackIndex"))
|
|
19680
|
+
return _edit_engine_mod.plan_dead_space_markers(
|
|
19681
|
+
project_root,
|
|
19682
|
+
items=items,
|
|
19683
|
+
timeline_name=tl.GetName(),
|
|
19684
|
+
timeline_fps=_edit_engine_timeline_fps(tl),
|
|
19685
|
+
# Same calibrate-by-default contract as plan_silence_ripple: what you
|
|
19686
|
+
# review here must be what that would remove.
|
|
19687
|
+
threshold_db=(
|
|
19688
|
+
float(_th)
|
|
19689
|
+
if (_th := (p.get("threshold_db") if p.get("threshold_db") is not None else p.get("thresholdDb"))) is not None
|
|
19690
|
+
else None
|
|
19691
|
+
),
|
|
19692
|
+
tightness=p.get("tightness"),
|
|
19693
|
+
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),
|
|
19694
|
+
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),
|
|
19695
|
+
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),
|
|
19696
|
+
)
|
|
19697
|
+
|
|
19196
19698
|
if action == "plan_swap":
|
|
19197
19699
|
_r, proj, project_root, err = _project_context(need_resolve=True)
|
|
19198
19700
|
if err:
|
|
@@ -19765,7 +20267,23 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
19765
20267
|
"plan_tighten",
|
|
19766
20268
|
"execute_tighten",
|
|
19767
20269
|
"plan_silence_ripple",
|
|
20270
|
+
"plan_dead_space_markers",
|
|
20271
|
+
"conform_lint",
|
|
20272
|
+
"rule_of_six_audit",
|
|
20273
|
+
"split_edit_audit",
|
|
20274
|
+
"sound_density_audit",
|
|
20275
|
+
"setup_sheet",
|
|
20276
|
+
"first_impression",
|
|
20277
|
+
"journal",
|
|
20278
|
+
"plan_prebalance",
|
|
20279
|
+
"plan_reference_match",
|
|
20280
|
+
"plan_beat_cuts",
|
|
20281
|
+
"plan_string_out",
|
|
20282
|
+
"propose_structure",
|
|
20283
|
+
"plan_broll",
|
|
20284
|
+
"plan_turnover",
|
|
19768
20285
|
"plan_transcript_tighten",
|
|
20286
|
+
"rank_takes",
|
|
19769
20287
|
"generate_captions",
|
|
19770
20288
|
"search_spoken_content",
|
|
19771
20289
|
"execute_silence_ripple",
|
|
@@ -19773,6 +20291,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
19773
20291
|
"execute_swap",
|
|
19774
20292
|
"list_plans",
|
|
19775
20293
|
"get_plan",
|
|
20294
|
+
"plan_report",
|
|
19776
20295
|
])
|
|
19777
20296
|
|
|
19778
20297
|
|