davinci-resolve-mcp 2.23.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/AGENTS.md +85 -0
- package/CHANGELOG.md +802 -0
- package/CLAUDE.md +15 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/SECURITY.md +53 -0
- package/bin/davinci-resolve-mcp.mjs +376 -0
- package/docs/README.md +56 -0
- package/docs/SKILL.md +1145 -0
- package/docs/authoring/fuse-dctl-authoring.md +242 -0
- package/docs/authoring/script-plugin-authoring.md +195 -0
- package/docs/contributing.md +82 -0
- package/docs/guides/color-decision-guide.md +387 -0
- package/docs/guides/editorial-decision-guide.md +136 -0
- package/docs/guides/media-analysis-guide.md +615 -0
- package/docs/guides/multicam-setup-guide.md +138 -0
- package/docs/install.md +198 -0
- package/docs/integrations/workflow-integrations.md +120 -0
- package/docs/kernels/README.md +28 -0
- package/docs/kernels/audio-fairlight-kernel.md +86 -0
- package/docs/kernels/color-grade-kernel.md +103 -0
- package/docs/kernels/extension-authoring-kernel.md +101 -0
- package/docs/kernels/fusion-composition-kernel.md +91 -0
- package/docs/kernels/media-pool-ingest-kernel.md +147 -0
- package/docs/kernels/project-lifecycle-kernel.md +120 -0
- package/docs/kernels/render-deliver-kernel.md +92 -0
- package/docs/kernels/review-annotation-kernel.md +110 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
- package/docs/kernels/timeline-edit-kernel.md +189 -0
- package/docs/notes/codec-plugin-notes.md +136 -0
- package/docs/notes/dctl-notes.md +234 -0
- package/docs/notes/fusion-template-notes.md +136 -0
- package/docs/notes/lut-notes.md +136 -0
- package/docs/notes/openfx-notes.md +120 -0
- package/docs/process/release-process.md +152 -0
- package/docs/reference/api-coverage.md +488 -0
- package/docs/reference/resolve_scripting_api.txt +1012 -0
- package/examples/README.md +53 -0
- package/examples/markers/README.md +81 -0
- package/examples/media/README.md +94 -0
- package/examples/timeline/README.md +98 -0
- package/install.py +1196 -0
- package/package.json +52 -0
- package/scripts/audit_api_parity.py +275 -0
- package/scripts/live_media_analysis_polish_probe.py +65 -0
- package/src/__init__.py +3 -0
- package/src/analysis_dashboard.py +4936 -0
- package/src/control_panel.py +13 -0
- package/src/granular/__init__.py +17 -0
- package/src/granular/common.py +727 -0
- package/src/granular/folder.py +287 -0
- package/src/granular/gallery.py +306 -0
- package/src/granular/graph.py +309 -0
- package/src/granular/media_pool.py +679 -0
- package/src/granular/media_pool_item.py +852 -0
- package/src/granular/media_storage.py +179 -0
- package/src/granular/project.py +1594 -0
- package/src/granular/resolve_control.py +521 -0
- package/src/granular/timeline.py +1074 -0
- package/src/granular/timeline_item.py +2251 -0
- package/src/resolve_mcp_server.py +43 -0
- package/src/server.py +15691 -0
- package/src/utils/__init__.py +3 -0
- package/src/utils/app_control.py +319 -0
- package/src/utils/audio_fairlight_live_probe.py +263 -0
- package/src/utils/cdl.py +20 -0
- package/src/utils/cloud_operations.py +192 -0
- package/src/utils/color_grade_live_probe.py +444 -0
- package/src/utils/dctl_templates.py +368 -0
- package/src/utils/extension_authoring_live_probe.py +292 -0
- package/src/utils/fuse_templates.py +1968 -0
- package/src/utils/fusion_composition_live_probe.py +284 -0
- package/src/utils/layout_presets.py +333 -0
- package/src/utils/mcp_stdio.py +32 -0
- package/src/utils/media_analysis.py +3618 -0
- package/src/utils/media_analysis_jobs.py +796 -0
- package/src/utils/media_pool_ingest_live_probe.py +592 -0
- package/src/utils/multicam.py +393 -0
- package/src/utils/object_inspection.py +287 -0
- package/src/utils/platform.py +157 -0
- package/src/utils/project_lifecycle_live_probe.py +376 -0
- package/src/utils/project_properties.py +601 -0
- package/src/utils/render_deliver_live_probe.py +384 -0
- package/src/utils/resolve_connection.py +77 -0
- package/src/utils/review_annotation_live_probe.py +352 -0
- package/src/utils/script_templates.py +1193 -0
- package/src/utils/sync_detection.py +887 -0
- package/src/utils/timeline_conform_live_probe.py +280 -0
- package/src/utils/timeline_kernel_live_probe.py +1091 -0
- package/src/utils/timeline_kernel_probe.py +185 -0
- package/src/utils/timeline_title_text.py +87 -0
- package/src/utils/update_check.py +610 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Helpers for timeline edit kernel capability probe reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
PROBE_STATUSES = {
|
|
13
|
+
"supported",
|
|
14
|
+
"partially_supported",
|
|
15
|
+
"read_only",
|
|
16
|
+
"write_only_unverifiable",
|
|
17
|
+
"version_or_page_dependent",
|
|
18
|
+
"unsupported",
|
|
19
|
+
"not_applicable",
|
|
20
|
+
"error",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def utc_timestamp() -> str:
|
|
25
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_timeline_item_property_keys(api_text: str) -> List[str]:
|
|
29
|
+
"""Return documented TimelineItem GetProperty/SetProperty keys in doc order."""
|
|
30
|
+
start_marker = "The supported keys with their accepted values are:"
|
|
31
|
+
end_marker = "Values beyond the range will be clipped"
|
|
32
|
+
start = api_text.find(start_marker)
|
|
33
|
+
if start < 0:
|
|
34
|
+
return []
|
|
35
|
+
end = api_text.find(end_marker, start)
|
|
36
|
+
section = api_text[start:end if end >= 0 else None]
|
|
37
|
+
keys: List[str] = []
|
|
38
|
+
for match in re.finditer(r'^\s+"([^"]+)"\s*:', section, flags=re.MULTILINE):
|
|
39
|
+
key = match.group(1)
|
|
40
|
+
if key not in keys:
|
|
41
|
+
keys.append(key)
|
|
42
|
+
return keys
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_api_class_methods(api_text: str, class_name: str) -> List[str]:
|
|
46
|
+
"""Return method names documented under a top-level API class section."""
|
|
47
|
+
lines = api_text.splitlines()
|
|
48
|
+
start_index: Optional[int] = None
|
|
49
|
+
for index, line in enumerate(lines):
|
|
50
|
+
if line.strip() == class_name:
|
|
51
|
+
start_index = index + 1
|
|
52
|
+
break
|
|
53
|
+
if start_index is None:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
methods: List[str] = []
|
|
57
|
+
for line in lines[start_index:]:
|
|
58
|
+
stripped = line.strip()
|
|
59
|
+
if stripped and not line.startswith(" ") and re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", stripped):
|
|
60
|
+
break
|
|
61
|
+
match = re.match(r"\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", line)
|
|
62
|
+
if match:
|
|
63
|
+
method = match.group(1)
|
|
64
|
+
if method not in methods:
|
|
65
|
+
methods.append(method)
|
|
66
|
+
return methods
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ordered_unique(values: Iterable[str]) -> List[str]:
|
|
70
|
+
out: List[str] = []
|
|
71
|
+
for value in values:
|
|
72
|
+
if value not in out:
|
|
73
|
+
out.append(value)
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def values_match(actual: Any, expected: Any) -> bool:
|
|
78
|
+
if isinstance(expected, bool):
|
|
79
|
+
return bool(actual) is expected
|
|
80
|
+
try:
|
|
81
|
+
return abs(float(actual) - float(expected)) <= 0.001
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
return actual == expected
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ProbeRecorder:
|
|
87
|
+
"""Collects normalized capability probe records and renders reports."""
|
|
88
|
+
|
|
89
|
+
def __init__(self) -> None:
|
|
90
|
+
self.records: List[Dict[str, Any]] = []
|
|
91
|
+
|
|
92
|
+
def record(
|
|
93
|
+
self,
|
|
94
|
+
category: str,
|
|
95
|
+
name: str,
|
|
96
|
+
status: str,
|
|
97
|
+
*,
|
|
98
|
+
details: Optional[Dict[str, Any]] = None,
|
|
99
|
+
evidence: Optional[Any] = None,
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
if status not in PROBE_STATUSES:
|
|
102
|
+
raise ValueError(f"unknown probe status: {status}")
|
|
103
|
+
item = {
|
|
104
|
+
"category": category,
|
|
105
|
+
"name": name,
|
|
106
|
+
"status": status,
|
|
107
|
+
"details": details or {},
|
|
108
|
+
}
|
|
109
|
+
if evidence is not None:
|
|
110
|
+
item["evidence"] = evidence
|
|
111
|
+
self.records.append(item)
|
|
112
|
+
return item
|
|
113
|
+
|
|
114
|
+
def record_exception(self, category: str, name: str, exc: Exception, *, details: Optional[Dict[str, Any]] = None):
|
|
115
|
+
payload = dict(details or {})
|
|
116
|
+
payload["exception"] = repr(exc)
|
|
117
|
+
return self.record(category, name, "error", details=payload)
|
|
118
|
+
|
|
119
|
+
def counts(self) -> Dict[str, int]:
|
|
120
|
+
counts = Counter(record["status"] for record in self.records)
|
|
121
|
+
return {status: counts.get(status, 0) for status in sorted(PROBE_STATUSES)}
|
|
122
|
+
|
|
123
|
+
def to_report(self, metadata: Dict[str, Any], artifacts: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
124
|
+
return {
|
|
125
|
+
"metadata": metadata,
|
|
126
|
+
"artifacts": artifacts or {},
|
|
127
|
+
"counts": self.counts(),
|
|
128
|
+
"records": self.records,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def render_markdown_report(report: Dict[str, Any]) -> str:
|
|
133
|
+
metadata = report.get("metadata", {})
|
|
134
|
+
counts = report.get("counts", {})
|
|
135
|
+
records = report.get("records", [])
|
|
136
|
+
artifacts = report.get("artifacts", {})
|
|
137
|
+
|
|
138
|
+
title = metadata.get("title", "Timeline Edit Kernel Capability Probe")
|
|
139
|
+
|
|
140
|
+
lines = [
|
|
141
|
+
f"# {title}",
|
|
142
|
+
"",
|
|
143
|
+
"## Run",
|
|
144
|
+
"",
|
|
145
|
+
f"- Timestamp: `{metadata.get('timestamp_utc', '')}`",
|
|
146
|
+
f"- Resolve: `{metadata.get('product', '')} {metadata.get('version_string', '')}`",
|
|
147
|
+
f"- Python: `{metadata.get('python', '')}`",
|
|
148
|
+
f"- Platform: `{metadata.get('platform', '')}`",
|
|
149
|
+
f"- Project: `{metadata.get('project_name', '')}`",
|
|
150
|
+
"",
|
|
151
|
+
"## Counts",
|
|
152
|
+
"",
|
|
153
|
+
]
|
|
154
|
+
for status in sorted(PROBE_STATUSES):
|
|
155
|
+
lines.append(f"- `{status}`: {counts.get(status, 0)}")
|
|
156
|
+
|
|
157
|
+
if artifacts:
|
|
158
|
+
lines.extend(["", "## Artifacts", ""])
|
|
159
|
+
for key, value in artifacts.items():
|
|
160
|
+
lines.append(f"- `{key}`: `{value}`")
|
|
161
|
+
|
|
162
|
+
by_category: Dict[str, List[Dict[str, Any]]] = {}
|
|
163
|
+
for record in records:
|
|
164
|
+
by_category.setdefault(record["category"], []).append(record)
|
|
165
|
+
|
|
166
|
+
lines.extend(["", "## Records", ""])
|
|
167
|
+
for category in sorted(by_category):
|
|
168
|
+
lines.extend([f"### {category}", ""])
|
|
169
|
+
lines.append("| Name | Status | Notes |")
|
|
170
|
+
lines.append("|---|---:|---|")
|
|
171
|
+
for record in by_category[category]:
|
|
172
|
+
details = record.get("details", {})
|
|
173
|
+
note_parts = []
|
|
174
|
+
for key in ("reason", "read", "write", "readback", "restore", "page", "item_type"):
|
|
175
|
+
if key in details:
|
|
176
|
+
note_parts.append(f"{key}={json.dumps(details[key], default=str)}")
|
|
177
|
+
if not note_parts and details:
|
|
178
|
+
note_parts.append(json.dumps(details, default=str, sort_keys=True)[:220])
|
|
179
|
+
notes = "; ".join(note_parts).replace("|", "\\|")
|
|
180
|
+
lines.append(
|
|
181
|
+
f"| `{record['name']}` | `{record['status']}` | {notes} |"
|
|
182
|
+
)
|
|
183
|
+
lines.append("")
|
|
184
|
+
|
|
185
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Helpers for Edit-page Text+ / generator text via undocumented TimelineItem.GetProperty keys."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
TITLE_TEXT_KEY_HINTS = (
|
|
8
|
+
"styled text",
|
|
9
|
+
"styledtext",
|
|
10
|
+
"text+",
|
|
11
|
+
"rich text",
|
|
12
|
+
"caption",
|
|
13
|
+
"subtitle",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def flatten_timeline_item_properties(props: Any) -> Dict[str, Any]:
|
|
18
|
+
if props is None:
|
|
19
|
+
return {}
|
|
20
|
+
if isinstance(props, dict):
|
|
21
|
+
return {str(k): v for k, v in props.items()}
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def timeline_item_get_property_map(
|
|
26
|
+
item: Any, serialize_fn: Callable[[Any], Any]
|
|
27
|
+
) -> Tuple[Dict[str, Any], Optional[str]]:
|
|
28
|
+
last_err: Optional[str] = None
|
|
29
|
+
saw_empty_success = False
|
|
30
|
+
for getter in (lambda: item.GetProperty(), lambda: item.GetProperty("")):
|
|
31
|
+
try:
|
|
32
|
+
raw = getter()
|
|
33
|
+
except TypeError:
|
|
34
|
+
continue
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
last_err = str(exc)
|
|
37
|
+
continue
|
|
38
|
+
flat = flatten_timeline_item_properties(serialize_fn(raw))
|
|
39
|
+
if flat:
|
|
40
|
+
return flat, None
|
|
41
|
+
saw_empty_success = True
|
|
42
|
+
return {}, None if saw_empty_success else last_err or "GetProperty failed"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def candidate_title_property_keys(flat: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
46
|
+
"""Prefer dict keys that may hold Text+ / generator rich text (not in public API docs)."""
|
|
47
|
+
candidates: List[Dict[str, Any]] = []
|
|
48
|
+
for key, value in flat.items():
|
|
49
|
+
if not isinstance(value, str):
|
|
50
|
+
continue
|
|
51
|
+
stripped = value.strip()
|
|
52
|
+
if not stripped:
|
|
53
|
+
continue
|
|
54
|
+
lk = key.lower()
|
|
55
|
+
score = 0
|
|
56
|
+
for hint in TITLE_TEXT_KEY_HINTS:
|
|
57
|
+
if hint in lk:
|
|
58
|
+
score += 10
|
|
59
|
+
if stripped.startswith("<?xml") or ("<" in stripped and ">" in stripped):
|
|
60
|
+
score += 5
|
|
61
|
+
if lk == "text":
|
|
62
|
+
score += 8
|
|
63
|
+
if score > 0 or len(stripped) > 24:
|
|
64
|
+
preview = stripped[:200] + ("…" if len(stripped) > 200 else "")
|
|
65
|
+
candidates.append({"key": key, "score": score, "value_preview": preview})
|
|
66
|
+
candidates.sort(key=lambda row: (-row["score"], row["key"]))
|
|
67
|
+
return candidates
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def escape_xml_text_body(s: str) -> str:
|
|
71
|
+
return (
|
|
72
|
+
s.replace("&", "&")
|
|
73
|
+
.replace("<", "<")
|
|
74
|
+
.replace(">", ">")
|
|
75
|
+
.replace('"', """)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def plain_to_minimal_styled_xml(plain: str) -> str:
|
|
80
|
+
"""Best-effort Text+ styled payload when only plain copy is available; Resolve may normalize."""
|
|
81
|
+
body = escape_xml_text_body(plain)
|
|
82
|
+
return (
|
|
83
|
+
'<?xml version="1.0" encoding="UTF-8"?>'
|
|
84
|
+
"<StyledElement>"
|
|
85
|
+
f"<Paragraph><TextRun>{body}</TextRun></Paragraph>"
|
|
86
|
+
"</StyledElement>"
|
|
87
|
+
)
|