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.
Files changed (92) hide show
  1. package/AGENTS.md +85 -0
  2. package/CHANGELOG.md +802 -0
  3. package/CLAUDE.md +15 -0
  4. package/LICENSE +21 -0
  5. package/README.md +159 -0
  6. package/SECURITY.md +53 -0
  7. package/bin/davinci-resolve-mcp.mjs +376 -0
  8. package/docs/README.md +56 -0
  9. package/docs/SKILL.md +1145 -0
  10. package/docs/authoring/fuse-dctl-authoring.md +242 -0
  11. package/docs/authoring/script-plugin-authoring.md +195 -0
  12. package/docs/contributing.md +82 -0
  13. package/docs/guides/color-decision-guide.md +387 -0
  14. package/docs/guides/editorial-decision-guide.md +136 -0
  15. package/docs/guides/media-analysis-guide.md +615 -0
  16. package/docs/guides/multicam-setup-guide.md +138 -0
  17. package/docs/install.md +198 -0
  18. package/docs/integrations/workflow-integrations.md +120 -0
  19. package/docs/kernels/README.md +28 -0
  20. package/docs/kernels/audio-fairlight-kernel.md +86 -0
  21. package/docs/kernels/color-grade-kernel.md +103 -0
  22. package/docs/kernels/extension-authoring-kernel.md +101 -0
  23. package/docs/kernels/fusion-composition-kernel.md +91 -0
  24. package/docs/kernels/media-pool-ingest-kernel.md +147 -0
  25. package/docs/kernels/project-lifecycle-kernel.md +120 -0
  26. package/docs/kernels/render-deliver-kernel.md +92 -0
  27. package/docs/kernels/review-annotation-kernel.md +110 -0
  28. package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
  29. package/docs/kernels/timeline-edit-kernel.md +189 -0
  30. package/docs/notes/codec-plugin-notes.md +136 -0
  31. package/docs/notes/dctl-notes.md +234 -0
  32. package/docs/notes/fusion-template-notes.md +136 -0
  33. package/docs/notes/lut-notes.md +136 -0
  34. package/docs/notes/openfx-notes.md +120 -0
  35. package/docs/process/release-process.md +152 -0
  36. package/docs/reference/api-coverage.md +488 -0
  37. package/docs/reference/resolve_scripting_api.txt +1012 -0
  38. package/examples/README.md +53 -0
  39. package/examples/markers/README.md +81 -0
  40. package/examples/media/README.md +94 -0
  41. package/examples/timeline/README.md +98 -0
  42. package/install.py +1196 -0
  43. package/package.json +52 -0
  44. package/scripts/audit_api_parity.py +275 -0
  45. package/scripts/live_media_analysis_polish_probe.py +65 -0
  46. package/src/__init__.py +3 -0
  47. package/src/analysis_dashboard.py +4936 -0
  48. package/src/control_panel.py +13 -0
  49. package/src/granular/__init__.py +17 -0
  50. package/src/granular/common.py +727 -0
  51. package/src/granular/folder.py +287 -0
  52. package/src/granular/gallery.py +306 -0
  53. package/src/granular/graph.py +309 -0
  54. package/src/granular/media_pool.py +679 -0
  55. package/src/granular/media_pool_item.py +852 -0
  56. package/src/granular/media_storage.py +179 -0
  57. package/src/granular/project.py +1594 -0
  58. package/src/granular/resolve_control.py +521 -0
  59. package/src/granular/timeline.py +1074 -0
  60. package/src/granular/timeline_item.py +2251 -0
  61. package/src/resolve_mcp_server.py +43 -0
  62. package/src/server.py +15691 -0
  63. package/src/utils/__init__.py +3 -0
  64. package/src/utils/app_control.py +319 -0
  65. package/src/utils/audio_fairlight_live_probe.py +263 -0
  66. package/src/utils/cdl.py +20 -0
  67. package/src/utils/cloud_operations.py +192 -0
  68. package/src/utils/color_grade_live_probe.py +444 -0
  69. package/src/utils/dctl_templates.py +368 -0
  70. package/src/utils/extension_authoring_live_probe.py +292 -0
  71. package/src/utils/fuse_templates.py +1968 -0
  72. package/src/utils/fusion_composition_live_probe.py +284 -0
  73. package/src/utils/layout_presets.py +333 -0
  74. package/src/utils/mcp_stdio.py +32 -0
  75. package/src/utils/media_analysis.py +3618 -0
  76. package/src/utils/media_analysis_jobs.py +796 -0
  77. package/src/utils/media_pool_ingest_live_probe.py +592 -0
  78. package/src/utils/multicam.py +393 -0
  79. package/src/utils/object_inspection.py +287 -0
  80. package/src/utils/platform.py +157 -0
  81. package/src/utils/project_lifecycle_live_probe.py +376 -0
  82. package/src/utils/project_properties.py +601 -0
  83. package/src/utils/render_deliver_live_probe.py +384 -0
  84. package/src/utils/resolve_connection.py +77 -0
  85. package/src/utils/review_annotation_live_probe.py +352 -0
  86. package/src/utils/script_templates.py +1193 -0
  87. package/src/utils/sync_detection.py +887 -0
  88. package/src/utils/timeline_conform_live_probe.py +280 -0
  89. package/src/utils/timeline_kernel_live_probe.py +1091 -0
  90. package/src/utils/timeline_kernel_probe.py +185 -0
  91. package/src/utils/timeline_title_text.py +87 -0
  92. 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("&", "&amp;")
73
+ .replace("<", "&lt;")
74
+ .replace(">", "&gt;")
75
+ .replace('"', "&quot;")
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
+ )