davinci-resolve-mcp 2.49.0 → 2.50.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 +22 -0
- package/README.md +1 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/media_analysis.py +93 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
|
|
4
4
|
|
|
5
|
+
## What's New in v2.50.0
|
|
6
|
+
|
|
7
|
+
The last JSON-fed readers now source from the DB-canonical analysis store
|
|
8
|
+
(consistency/perf hygiene — the JSON export is lockstep with the DB, so this
|
|
9
|
+
is shape-preserving by construction).
|
|
10
|
+
|
|
11
|
+
- **Changed** `summarize_reports` aggregates from the DB (blob + human
|
|
12
|
+
overlay) when every report dir on disk is covered by an ingested clip row;
|
|
13
|
+
pre-v9 roots and MIXED roots (some clips not ingested) fall back WHOLESALE
|
|
14
|
+
to the JSON walk — a partial DB view would silently under-report. The
|
|
15
|
+
summary gains a `"source": "db"|"json"` key for observability; the F1
|
|
16
|
+
provenance map (source_reports / missing_reports) is unchanged.
|
|
17
|
+
- **Changed** `build_analysis_index` sources local reports from the DB
|
|
18
|
+
instead of re-parsing every `analysis.json`; job-linked EXTERNAL report
|
|
19
|
+
paths (their rows live under another project's DB) and pre-v9 dirs keep
|
|
20
|
+
the JSON read. The FTS schema and the query surface are identical; the
|
|
21
|
+
result gains `report_sources` counts.
|
|
22
|
+
- **Added** DB-vs-JSON parity tests (semantic equality with normalized
|
|
23
|
+
ordering) plus wholesale-fallback regressions for mixed and pre-v9 roots;
|
|
24
|
+
live-validated on the sample analysis root (summary, index counts, and
|
|
25
|
+
FTS query results identical on both paths).
|
|
26
|
+
|
|
5
27
|
## What's New in v2.49.0
|
|
6
28
|
|
|
7
29
|
Cross-shot relationships (spec §4 — pattern recognition only): the shot
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
package/install.py
CHANGED
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.
|
|
38
|
+
VERSION = "2.50.0"
|
|
39
39
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
40
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
41
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.50.0"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -5994,20 +5994,68 @@ def load_report(project_root: str, report_path: Optional[str] = None, clip_dir:
|
|
|
5994
5994
|
return {"success": True, "path": path, "report": payload}
|
|
5995
5995
|
|
|
5996
5996
|
|
|
5997
|
-
def
|
|
5998
|
-
|
|
5997
|
+
def _collect_reports_for_summary(root: str) -> Tuple[List[Dict[str, Any]], List[str], str]:
|
|
5998
|
+
"""(reports, report_paths, source) for summarize_reports.
|
|
5999
|
+
|
|
6000
|
+
DB-first: when every report dir on disk is covered by an ingested clip
|
|
6001
|
+
row, reports come from the DB-canonical store (blob + human overlay —
|
|
6002
|
+
identical content to the lockstep JSON export). Pre-v9 roots and MIXED
|
|
6003
|
+
roots (some clips not ingested) fall back WHOLESALE to the JSON walk —
|
|
6004
|
+
a partial DB view would silently under-report.
|
|
6005
|
+
"""
|
|
5999
6006
|
clips_root = os.path.join(root, "clips")
|
|
6000
|
-
|
|
6001
|
-
report_paths: List[str] = []
|
|
6007
|
+
disk_paths: List[str] = []
|
|
6002
6008
|
if os.path.isdir(clips_root):
|
|
6003
6009
|
for dirpath, _, filenames in os.walk(clips_root):
|
|
6004
6010
|
if "analysis.json" in filenames:
|
|
6005
|
-
|
|
6011
|
+
disk_paths.append(os.path.join(dirpath, "analysis.json"))
|
|
6012
|
+
disk_paths.sort()
|
|
6013
|
+
|
|
6014
|
+
try:
|
|
6015
|
+
from src.utils import analysis_store, timeline_brain_db
|
|
6016
|
+
|
|
6017
|
+
conn = timeline_brain_db.connect(root)
|
|
6018
|
+
db_dirs = {
|
|
6019
|
+
str(r["clip_dir"]): str(r["clip_uuid"])
|
|
6020
|
+
for r in conn.execute(
|
|
6021
|
+
"SELECT clip_dir, clip_uuid FROM clips WHERE clip_dir IS NOT NULL"
|
|
6022
|
+
).fetchall()
|
|
6023
|
+
}
|
|
6024
|
+
except Exception: # noqa: BLE001 — no DB (pre-v9) → JSON
|
|
6025
|
+
db_dirs = {}
|
|
6026
|
+
if disk_paths and db_dirs:
|
|
6027
|
+
dir_names = [os.path.basename(os.path.dirname(p)) for p in disk_paths]
|
|
6028
|
+
if all(name in db_dirs for name in dir_names):
|
|
6029
|
+
from src.utils import analysis_store
|
|
6030
|
+
|
|
6031
|
+
reports: List[Dict[str, Any]] = []
|
|
6032
|
+
complete = True
|
|
6033
|
+
for path, name in zip(disk_paths, dir_names):
|
|
6006
6034
|
try:
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6035
|
+
report = analysis_store.export_report(root, db_dirs[name])
|
|
6036
|
+
except Exception: # noqa: BLE001
|
|
6037
|
+
report = None
|
|
6038
|
+
if not isinstance(report, dict):
|
|
6039
|
+
complete = False
|
|
6040
|
+
break
|
|
6041
|
+
reports.append(report)
|
|
6042
|
+
if complete:
|
|
6043
|
+
return reports, disk_paths, "db"
|
|
6044
|
+
|
|
6045
|
+
reports = []
|
|
6046
|
+
report_paths: List[str] = []
|
|
6047
|
+
for path in disk_paths:
|
|
6048
|
+
try:
|
|
6049
|
+
reports.append(_read_json(path))
|
|
6050
|
+
report_paths.append(path)
|
|
6051
|
+
except (OSError, json.JSONDecodeError):
|
|
6052
|
+
continue
|
|
6053
|
+
return reports, report_paths, "json"
|
|
6054
|
+
|
|
6055
|
+
|
|
6056
|
+
def summarize_reports(project_root: str) -> Dict[str, Any]:
|
|
6057
|
+
root = normalize_path(project_root)
|
|
6058
|
+
reports, report_paths, reports_source = _collect_reports_for_summary(root)
|
|
6011
6059
|
warnings = []
|
|
6012
6060
|
motion_counts: Dict[str, int] = {}
|
|
6013
6061
|
tags: Dict[str, int] = {}
|
|
@@ -6047,6 +6095,7 @@ def summarize_reports(project_root: str) -> Dict[str, Any]:
|
|
|
6047
6095
|
summary = {
|
|
6048
6096
|
"success": True,
|
|
6049
6097
|
"project_root": root,
|
|
6098
|
+
"source": reports_source, # "db" (canonical store) | "json" (walk fallback)
|
|
6050
6099
|
"clip_reports": len(reports),
|
|
6051
6100
|
"motion_distribution": motion_counts,
|
|
6052
6101
|
"technical_warning_count": len(warnings),
|
|
@@ -7202,9 +7251,42 @@ def build_analysis_index(project_root: str, *, index_path: Optional[Any] = None)
|
|
|
7202
7251
|
conn.execute("INSERT INTO index_metadata (key, value) VALUES (?, ?)", ("fts_enabled", "1" if fts_enabled else "0"))
|
|
7203
7252
|
conn.execute("INSERT INTO index_metadata (key, value) VALUES (?, ?)", ("image_blob_policy", "excluded"))
|
|
7204
7253
|
|
|
7254
|
+
# DB-first sourcing: local reports whose clip dir is ingested come from
|
|
7255
|
+
# the DB-canonical store (blob + human overlay — identical content to
|
|
7256
|
+
# the lockstep JSON export) instead of re-parsing every analysis.json.
|
|
7257
|
+
# Pre-v9 dirs and job-linked EXTERNAL report paths (their rows live
|
|
7258
|
+
# under another project's DB) keep the JSON read. The index schema and
|
|
7259
|
+
# the query surface are unchanged either way.
|
|
7260
|
+
clips_root_prefix = os.path.realpath(os.path.join(root, "clips")) + os.sep
|
|
7261
|
+
try:
|
|
7262
|
+
from src.utils import timeline_brain_db as _brain_db
|
|
7263
|
+
|
|
7264
|
+
_db_dirs = {
|
|
7265
|
+
str(r["clip_dir"]): str(r["clip_uuid"])
|
|
7266
|
+
for r in _brain_db.connect(root).execute(
|
|
7267
|
+
"SELECT clip_dir, clip_uuid FROM clips WHERE clip_dir IS NOT NULL"
|
|
7268
|
+
).fetchall()
|
|
7269
|
+
}
|
|
7270
|
+
except Exception: # noqa: BLE001 — no DB (pre-v9) → JSON for everything
|
|
7271
|
+
_db_dirs = {}
|
|
7272
|
+
report_sources = {"db": 0, "json": 0}
|
|
7205
7273
|
for report_path in sorted(_iter_analysis_report_files(root)):
|
|
7206
7274
|
try:
|
|
7207
|
-
report =
|
|
7275
|
+
report = None
|
|
7276
|
+
if _db_dirs and os.path.realpath(report_path).startswith(clips_root_prefix):
|
|
7277
|
+
clip_uuid = _db_dirs.get(os.path.basename(os.path.dirname(report_path)))
|
|
7278
|
+
if clip_uuid:
|
|
7279
|
+
try:
|
|
7280
|
+
from src.utils import analysis_store as _analysis_store
|
|
7281
|
+
|
|
7282
|
+
report = _analysis_store.export_report(root, clip_uuid)
|
|
7283
|
+
except Exception: # noqa: BLE001 — fall back per-report
|
|
7284
|
+
report = None
|
|
7285
|
+
if isinstance(report, dict):
|
|
7286
|
+
report_sources["db"] += 1
|
|
7287
|
+
else:
|
|
7288
|
+
report = _read_json(report_path)
|
|
7289
|
+
report_sources["json"] += 1
|
|
7208
7290
|
row_counts = _insert_analysis_report_into_index(conn, report_path, report, fts_enabled=fts_enabled)
|
|
7209
7291
|
counts["clips"] += 1
|
|
7210
7292
|
for key, value in row_counts.items():
|
|
@@ -7244,6 +7326,7 @@ def build_analysis_index(project_root: str, *, index_path: Optional[Any] = None)
|
|
|
7244
7326
|
"image_blob_policy": "excluded",
|
|
7245
7327
|
"fts_enabled": bool(counts["clips"]) and _sqlite_table_exists(db_path, "clips_fts"),
|
|
7246
7328
|
"counts": counts,
|
|
7329
|
+
"report_sources": report_sources, # how many reports came from the DB vs JSON
|
|
7247
7330
|
"failed_report_count": len(failed_reports),
|
|
7248
7331
|
"failed_reports": failed_reports[:50],
|
|
7249
7332
|
"size_bytes": os.path.getsize(db_path) if os.path.isfile(db_path) else 0,
|