davinci-resolve-mcp 2.44.0 → 2.46.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 +35 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/timeline.py +2 -1
- package/src/server.py +1264 -18
- 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/project_lint.py +10 -1
- package/src/utils/project_spec.py +31 -0
|
@@ -83,6 +83,7 @@ class Spec:
|
|
|
83
83
|
project: str
|
|
84
84
|
color_preset: Optional[str] = None
|
|
85
85
|
settings: Dict[str, str] = field(default_factory=dict)
|
|
86
|
+
bins: List[str] = field(default_factory=list)
|
|
86
87
|
timelines: List[TimelineSpec] = field(default_factory=list)
|
|
87
88
|
hooks: List[Hook] = field(default_factory=list)
|
|
88
89
|
|
|
@@ -145,6 +146,20 @@ def spec_from_dict(data: Dict[str, Any]) -> Spec:
|
|
|
145
146
|
if not isinstance(settings, dict):
|
|
146
147
|
raise SpecError("`settings` must be a mapping.")
|
|
147
148
|
|
|
149
|
+
bins: List[str] = []
|
|
150
|
+
for raw_bin in data.get("bins") or []:
|
|
151
|
+
if isinstance(raw_bin, str) and raw_bin.strip():
|
|
152
|
+
bin_path = raw_bin.strip().strip("/")
|
|
153
|
+
elif isinstance(raw_bin, dict) and raw_bin.get("path"):
|
|
154
|
+
bin_path = str(raw_bin["path"]).strip().strip("/")
|
|
155
|
+
else:
|
|
156
|
+
raise SpecError(f"Each bin needs a non-empty path: {raw_bin!r}")
|
|
157
|
+
# Live bin paths are always reported Master-prefixed; normalize here so
|
|
158
|
+
# an unprefixed spec bin doesn't read as a perpetually-pending change.
|
|
159
|
+
if bin_path != "Master" and not bin_path.startswith("Master/"):
|
|
160
|
+
bin_path = f"Master/{bin_path}"
|
|
161
|
+
bins.append(bin_path)
|
|
162
|
+
|
|
148
163
|
timelines: List[TimelineSpec] = []
|
|
149
164
|
for raw_tl in (data.get("timelines") or []):
|
|
150
165
|
if not isinstance(raw_tl, dict) or not raw_tl.get("name"):
|
|
@@ -160,6 +175,7 @@ def spec_from_dict(data: Dict[str, Any]) -> Spec:
|
|
|
160
175
|
project=str(project),
|
|
161
176
|
color_preset=preset,
|
|
162
177
|
settings={str(k): str(v) for k, v in settings.items()},
|
|
178
|
+
bins=bins,
|
|
163
179
|
timelines=timelines,
|
|
164
180
|
hooks=_parse_hooks(data.get("hooks")),
|
|
165
181
|
)
|
|
@@ -252,6 +268,15 @@ def plan_spec(spec: Spec, live: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
252
268
|
f"{live_settings.get(key)!r} -> {want!r}", {"key": key, "value": want},
|
|
253
269
|
))
|
|
254
270
|
|
|
271
|
+
# Media Pool bins. Paths are slash-delimited from Master, e.g.
|
|
272
|
+
# "Master/Media/Scene_01"; creation is idempotent in the executor.
|
|
273
|
+
live_bins = set(live.get("bins") or [])
|
|
274
|
+
for bin_path in spec.bins:
|
|
275
|
+
if bin_path in live_bins:
|
|
276
|
+
actions.append(Action("noop", f"bin:{bin_path}", "exists"))
|
|
277
|
+
else:
|
|
278
|
+
actions.append(Action("ensure", f"bin:{bin_path}", "ensure media-pool bin", {"path": bin_path}))
|
|
279
|
+
|
|
255
280
|
# Timelines + their settings + markers. NOTE: `fps` is a *creation-time*
|
|
256
281
|
# property handled by ensure_timeline — Resolve refuses SetSetting on
|
|
257
282
|
# timelineFrameRate after a timeline exists — so it is never emitted as a
|
|
@@ -300,6 +325,7 @@ def _spec_desired_state(spec: Spec) -> Dict[str, Any]:
|
|
|
300
325
|
return {
|
|
301
326
|
"project": spec.project,
|
|
302
327
|
"settings": {k: _norm_setting_value(v) for k, v in effective_settings(spec).items()},
|
|
328
|
+
"bins": list(spec.bins),
|
|
303
329
|
"timelines": [
|
|
304
330
|
{
|
|
305
331
|
"name": tl.name,
|
|
@@ -324,6 +350,7 @@ def _spec_normalized_state(live: Dict[str, Any], spec: Spec) -> Dict[str, Any]:
|
|
|
324
350
|
return {
|
|
325
351
|
"project": live.get("project"),
|
|
326
352
|
"settings": {k: _norm_setting_value(live_settings.get(k)) for k in desired if k in live_settings},
|
|
353
|
+
"bins": [path for path in (live.get("bins") or []) if path in set(spec.bins)],
|
|
327
354
|
"timelines": [
|
|
328
355
|
{
|
|
329
356
|
"name": name,
|
|
@@ -356,6 +383,7 @@ def apply_spec(
|
|
|
356
383
|
ensure_timeline(name, fps) -> bool
|
|
357
384
|
set_timeline_setting(timeline_name, key, value) -> bool
|
|
358
385
|
add_marker(timeline_name, marker: dict) -> bool
|
|
386
|
+
ensure_bin(path) -> bool
|
|
359
387
|
|
|
360
388
|
Hooks execute only when `run_hooks=True` AND a `run_hook` callable is given —
|
|
361
389
|
arbitrary shell from a spec stays opt-in. Failures accumulate when
|
|
@@ -397,6 +425,9 @@ def apply_spec(
|
|
|
397
425
|
_record(bool(executor.set_project_setting(key, desired[key])),
|
|
398
426
|
f"project:{spec.project}/setting:{key}", str(desired[key]))
|
|
399
427
|
|
|
428
|
+
for bin_path in spec.bins:
|
|
429
|
+
_record(bool(executor.ensure_bin(bin_path)), f"bin:{bin_path}")
|
|
430
|
+
|
|
400
431
|
# Timelines. `fps` is creation-time only (ensure_timeline); never set as a
|
|
401
432
|
# post-creation timelineFrameRate setting — Resolve refuses that.
|
|
402
433
|
live_tls = {tl.get("name"): tl for tl in (live.get("timelines") or [])}
|