davinci-resolve-mcp 2.41.0 → 2.43.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 +54 -0
- package/README.md +1 -1
- package/docs/SKILL.md +39 -2
- package/docs/guides/control-panel.md +12 -3
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +81 -2
- package/src/granular/common.py +1 -1
- package/src/server.py +77 -1
- package/src/utils/analysis_store.py +4 -1
- package/src/utils/deep_vision.py +643 -0
- package/src/utils/embeddings.py +668 -0
- package/src/utils/media_analysis.py +94 -1
- package/src/utils/timeline_brain_db.py +33 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
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.43.0
|
|
6
|
+
|
|
7
|
+
Embeddings + similarity search — Phase C of the analysis + edit-engine
|
|
8
|
+
program. "Find clips/shots like this," locally, with no vendor token cost.
|
|
9
|
+
|
|
10
|
+
- **Added** `src/utils/embeddings.py` and schema v10 (`embeddings` table:
|
|
11
|
+
one float32 vector per entity/kind/model, with content hashes so re-runs
|
|
12
|
+
only re-embed what changed). Brute-force cosine — numpy when present.
|
|
13
|
+
- **Added** `media_analysis` actions `build_embeddings` (idempotent; clip
|
|
14
|
+
summaries, shot descriptions + deep field groups, transcript segments,
|
|
15
|
+
sampled frames with per-shot mean vectors) and `find_similar` (query by
|
|
16
|
+
free text, clip, or shot; `kind="text"|"visual"`; free-text visual queries
|
|
17
|
+
go through the CLIP text encoder).
|
|
18
|
+
- **Added** backend detection, never installation (the whisper pattern):
|
|
19
|
+
text = ollama `nomic-embed-text` (serving probe) or sentence-transformers;
|
|
20
|
+
visual = open_clip ViT-B-32. Capabilities and the Diagnostics → Tools page
|
|
21
|
+
list availability with install guidance.
|
|
22
|
+
- **Added** a `Semantic` toggle on the panel's Review search (shown only
|
|
23
|
+
when a text backend is detected) backed by `/api/search/semantic`, which
|
|
24
|
+
returns rows in the existing search-card shape.
|
|
25
|
+
- **Validation**: full offline suite (1095 tests; 14 new, backends mocked).
|
|
26
|
+
Live on the 2026-05-17 sample root: 54 text vectors in 1.2s via ollama
|
|
27
|
+
with correct top hits for three editorial queries; 27 CLIP vectors with
|
|
28
|
+
"cracked broken windshield glass" ranking the shattered-windshield frame
|
|
29
|
+
first; panel endpoint verified and screenshots regenerated.
|
|
30
|
+
|
|
31
|
+
## What's New in v2.42.0
|
|
32
|
+
|
|
33
|
+
Deep shot-level vision tier — Phase B of the analysis + edit-engine program.
|
|
34
|
+
Opt-in, estimate-first per-shot field filling for the Visual / Content /
|
|
35
|
+
Production / Editorial / Cuttability groups the shot pages already render.
|
|
36
|
+
|
|
37
|
+
- **Added** `src/utils/deep_vision.py` and three `media_analysis` actions:
|
|
38
|
+
`deepen` (estimate → confirm_token → deferred host-vision payload per
|
|
39
|
+
clip/shot), `commit_shot_vision` (writes `vision_deep_v1` provenance rows,
|
|
40
|
+
updates the canonical blob, re-exports analysis.json in lockstep), and
|
|
41
|
+
`vision_pending_sweep` (lists clips stuck in `pending_host_analysis`;
|
|
42
|
+
`reoffer=true` returns the stored payload, `expire=true` stamps them).
|
|
43
|
+
- **Added** deep depth to the analyze flow: `depth="deep"` extends the
|
|
44
|
+
deferred payload with the per-shot schema and requires `confirm_deep=true`
|
|
45
|
+
after a token-cost estimate. Caps pre-call refusal applies on both paths.
|
|
46
|
+
- **Added** panel affordances: `Deepen analysis` (clip view) and `Deepen this
|
|
47
|
+
shot` (shot view) copy ready-made chat prompts, per the chat-first UX.
|
|
48
|
+
Shots with no sampled frames on disk get 1–2 frames re-extracted via
|
|
49
|
+
ffmpeg, downscaled per caps (source media stays read-only).
|
|
50
|
+
- **Fixed** a provenance bug in the analysis store: a source re-deriving an
|
|
51
|
+
unchanged value no longer re-attributes the row (a deep pass would
|
|
52
|
+
otherwise claim every untouched field as `vision_deep_v1`).
|
|
53
|
+
- **Validation**: full offline suite (1081 tests; 15 new), and a real
|
|
54
|
+
end-to-end deep pass on the 2026-05-17 sample clip — estimate → confirm →
|
|
55
|
+
frames read by the host chat → commit → rows/blob/export parity → fields
|
|
56
|
+
visible in the panel shot view. Control-panel guide screenshots
|
|
57
|
+
regenerated from the live panel.
|
|
58
|
+
|
|
5
59
|
## What's New in v2.41.0
|
|
6
60
|
|
|
7
61
|
DB-canonical clip analysis (C1) — Phase A of the analysis + edit-engine
|
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/docs/SKILL.md
CHANGED
|
@@ -494,8 +494,45 @@ Key actions: `capabilities`, `install_guidance`, `resolve_output_root`, `plan`,
|
|
|
494
494
|
`cancel_batch_job`, `resume_batch_job`, `review_timeline_markers`,
|
|
495
495
|
`cleanup_artifacts`, `db_status`, `db_ingest`, `get_panel_state`,
|
|
496
496
|
`set_panel_state`, `session_start_context`, `update_clip_field`,
|
|
497
|
-
`update_shot_field`, `get_field_history`, `revert_field`,
|
|
498
|
-
`list_corrections
|
|
497
|
+
`update_shot_field`, `get_field_history`, `revert_field`,
|
|
498
|
+
`list_corrections`, `deepen`, `commit_shot_vision`, `vision_pending_sweep`,
|
|
499
|
+
`build_embeddings`, and `find_similar`.
|
|
500
|
+
|
|
501
|
+
**Embeddings + similarity (v2.43.0+).** Local-compute semantic search; no
|
|
502
|
+
vendor tokens, so nothing here touches the caps ledger. Backends are
|
|
503
|
+
detected, never installed (capabilities lists them with install guidance):
|
|
504
|
+
text = ollama serving `nomic-embed-text` or sentence-transformers; visual =
|
|
505
|
+
open_clip (ViT-B-32, needs torch).
|
|
506
|
+
- `build_embeddings(kinds=["text","visual"]?, clip_id?)` — idempotent;
|
|
507
|
+
embeds clip summaries, shot descriptions (+ deep field groups), transcript
|
|
508
|
+
segments, and sampled frames (per-shot visual vector = mean of its
|
|
509
|
+
frames'). Only re-embeds entities whose content changed.
|
|
510
|
+
- `find_similar(text=… | clip_id=… | clip_id+shot_index, kind="text"|"visual",
|
|
511
|
+
entity_types?, limit?)` — brute-force cosine over the project's vectors.
|
|
512
|
+
Free-text visual queries use the CLIP text encoder ("cracked windshield"
|
|
513
|
+
finds the frame). Results carry scores plus clip/shot/segment context.
|
|
514
|
+
The panel search box gains a `Semantic` toggle when a text backend is
|
|
515
|
+
detected. Vectors live in the per-project DB (schema v10).
|
|
516
|
+
|
|
517
|
+
**Deep shot-level vision tier (v2.42.0+).** Opt-in, estimate-first. Two
|
|
518
|
+
entry points share one per-shot schema (Visual / Content / Production /
|
|
519
|
+
Editorial / Cuttability / description / confidence):
|
|
520
|
+
- `depth="deep"` on any analyze action extends the deferred host-vision
|
|
521
|
+
payload with `deep_shot_schema`; each `shot_descriptions` entry must carry
|
|
522
|
+
the field groups. The first deep run returns `confirmation_required` with
|
|
523
|
+
a token-cost estimate — re-call with `confirm_deep=true`. Caps still apply.
|
|
524
|
+
- `deepen(clip_id|clip_dir, shot_index?|shot_indices?)` runs the pass
|
|
525
|
+
post-hoc on an already-analyzed clip. First call returns the estimate +
|
|
526
|
+
`confirm_token`; re-call with the token to get the deferred payload, read
|
|
527
|
+
its `frame_paths`, and commit via
|
|
528
|
+
`commit_shot_vision(clip_id, shots=[{shot_index, ...groups...}],
|
|
529
|
+
vision_token)`. Deep fields land as `vision_deep_v1` provenance rows;
|
|
530
|
+
human corrections always survive. Shots with no sampled frames on disk get
|
|
531
|
+
1–2 frames re-extracted via ffmpeg (read-only on source media).
|
|
532
|
+
`vision_pending_sweep(expire?, max_age_days?, reoffer?)` lists clips stuck
|
|
533
|
+
in `pending_host_analysis`; `reoffer=true` returns each clip's stored
|
|
534
|
+
deferred payload to finish the run, `expire=true` stamps them
|
|
535
|
+
`expired_host_analysis` so pendings never linger silently.
|
|
499
536
|
|
|
500
537
|
**DB-canonical analysis store (v2.41.0+).** The per-project SQLite DB
|
|
501
538
|
(`_soul/timeline_brain.sqlite`, schema v9+) is the source of truth for clip
|
|
@@ -78,7 +78,11 @@ evidence base (analyzed / superseded / vision-pending / warnings) with
|
|
|
78
78
|
humanized source-trust and analysis-layer chips. Cards show a representative
|
|
79
79
|
thumbnail, duration, shot count, status, and summary one-liner; grid and list
|
|
80
80
|
layouts are available, and full-text search covers clips, summaries, tags, and
|
|
81
|
-
transcripts once the search index is built.
|
|
81
|
+
transcripts once the search index is built. When a local text-embedding
|
|
82
|
+
backend is detected (ollama with `nomic-embed-text`, or
|
|
83
|
+
sentence-transformers), a `Semantic` toggle appears next to the search box —
|
|
84
|
+
it searches by meaning instead of exact words, ranking clips, shots, and
|
|
85
|
+
transcript lines by similarity to your query.
|
|
82
86
|
|
|
83
87
|
### Clip detail
|
|
84
88
|
|
|
@@ -86,7 +90,10 @@ transcripts once the search index is built.
|
|
|
86
90
|
|
|
87
91
|
The clip view shows the full summary, tags, star rating, editorial notes, and
|
|
88
92
|
a contact sheet of every detected shot. From here you can open the clip in
|
|
89
|
-
Resolve, jump to the transcript, or click into any shot.
|
|
93
|
+
Resolve, jump to the transcript, or click into any shot. `Deepen analysis`
|
|
94
|
+
copies a ready-made chat prompt that asks your MCP session to run the opt-in
|
|
95
|
+
deep shot pass (cost estimate first, then per-shot Visual / Content /
|
|
96
|
+
Editorial / Cuttability fields).
|
|
90
97
|
|
|
91
98
|
### Transcript
|
|
92
99
|
|
|
@@ -107,7 +114,9 @@ start", "After cut", "Flash frame", "Motion peak"). Subjective fields are
|
|
|
107
114
|
editable inline; edits are kept with the clip's analysis and merged on top of
|
|
108
115
|
future re-analysis so human notes survive fresh vision runs. `Open in Resolve`
|
|
109
116
|
jumps straight to the clip in the source viewer with the shot's mark in/out
|
|
110
|
-
set.
|
|
117
|
+
set. The field groups (Visual, Content, Production, Editorial, Cuttability)
|
|
118
|
+
are filled by the opt-in deep vision pass — `Deepen this shot` copies a chat
|
|
119
|
+
prompt that runs it for just this shot, estimate first.
|
|
111
120
|
|
|
112
121
|
### Media → History
|
|
113
122
|
|
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.43.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
|
@@ -4065,6 +4065,9 @@ HTML = r"""<!doctype html>
|
|
|
4065
4065
|
</div>
|
|
4066
4066
|
<div class="review-bin-filters">
|
|
4067
4067
|
<input id="reviewSearchInput" type="search" placeholder="Search clips, summaries, tags, transcripts…" autocomplete="off">
|
|
4068
|
+
<label id="reviewSemanticToggle" class="review-semantic-toggle" style="display:none" title="Search by meaning using local text embeddings instead of exact words.">
|
|
4069
|
+
<input id="reviewSemanticCheckbox" type="checkbox"> Semantic
|
|
4070
|
+
</label>
|
|
4068
4071
|
<select id="reviewBinFilter" aria-label="Filter by bin">
|
|
4069
4072
|
<option value="">All bins</option>
|
|
4070
4073
|
</select>
|
|
@@ -6201,6 +6204,13 @@ HTML = r"""<!doctype html>
|
|
|
6201
6204
|
syncPreferencesPanel();
|
|
6202
6205
|
applyPreferencesToControls(prefs);
|
|
6203
6206
|
renderVersionBadge();
|
|
6207
|
+
// Semantic search rides on a local text-embedding backend; only show
|
|
6208
|
+
// the toggle when one is detected (ollama nomic-embed-text or
|
|
6209
|
+
// sentence-transformers).
|
|
6210
|
+
if (state.boot?.capabilities?.embeddings?.text?.available) {
|
|
6211
|
+
const toggle = $('reviewSemanticToggle');
|
|
6212
|
+
if (toggle) toggle.style.display = '';
|
|
6213
|
+
}
|
|
6204
6214
|
// Paint the previous inventory immediately (stale) so a slow first
|
|
6205
6215
|
// /api/resolve/media — common with network source media — doesn't leave the
|
|
6206
6216
|
// panel blank. The live fetch below replaces it and clears the stale flag.
|
|
@@ -8258,7 +8268,11 @@ HTML = r"""<!doctype html>
|
|
|
8258
8268
|
renderReviewBin();
|
|
8259
8269
|
return;
|
|
8260
8270
|
}
|
|
8261
|
-
const
|
|
8271
|
+
const semantic = !!$('reviewSemanticCheckbox')?.checked;
|
|
8272
|
+
const endpoint = semantic
|
|
8273
|
+
? `/api/search/semantic?q=${encodeURIComponent(query)}&limit=40`
|
|
8274
|
+
: `/api/index/query?q=${encodeURIComponent(query)}&limit=40`;
|
|
8275
|
+
const payload = await api(endpoint)
|
|
8262
8276
|
.catch(err => ({ success: false, error: String(err) }));
|
|
8263
8277
|
state.review.searchResults = payload;
|
|
8264
8278
|
renderReviewBin();
|
|
@@ -8390,7 +8404,7 @@ HTML = r"""<!doctype html>
|
|
|
8390
8404
|
<span class="meta">${formatDuration(card.duration_seconds)} · ${shots.length} shots</span>
|
|
8391
8405
|
${cls.primary_use ? `<span class="review-chip">${escapeHtml(cls.primary_use)}</span>` : ''}
|
|
8392
8406
|
${cls.select_potential ? `<span class="review-chip ${selectChipClass(cls.select_potential)}">${escapeHtml(cls.select_potential)}</span>` : ''}
|
|
8393
|
-
<div class="actions"><button class="secondary" id="reviewClipTranscriptBtn">Transcript</button><button class="secondary" id="reviewClipOpenInResolveBtn">Open in Resolve</button></div>
|
|
8407
|
+
<div class="actions"><button class="secondary" id="reviewClipTranscriptBtn">Transcript</button><button class="secondary" id="reviewClipOpenInResolveBtn">Open in Resolve</button><button class="secondary" data-copy-chat-prompt="${escapeHtml(`Deepen the analysis of clip “${card.clip_name || state.review.currentClipId}”: call media_analysis(action="deepen", params={"clip_id": "${card.clip_id || state.review.currentClipId}"}), show me the cost estimate, and after I confirm, read the frames and commit the per-shot fields via commit_shot_vision.`)}">Deepen analysis</button></div>
|
|
8394
8408
|
`;
|
|
8395
8409
|
$('reviewClipSummary').textContent = data.clip_summary || data.clip_summary_oneliner || '';
|
|
8396
8410
|
$('reviewClipTags').innerHTML = tags.map(t => `<span class="review-chip">${escapeHtml(t)}</span>`).join('');
|
|
@@ -9440,6 +9454,7 @@ HTML = r"""<!doctype html>
|
|
|
9440
9454
|
<span class="meta">${escapeHtml(shot.description || '')}</span>
|
|
9441
9455
|
<div class="actions">
|
|
9442
9456
|
<button class="secondary" id="reviewShotOpenInResolveBtn">Open in Resolve</button>
|
|
9457
|
+
<button class="secondary" data-copy-chat-prompt="${escapeHtml(`Deepen shot ${shotIndex} of clip “${(state.review.currentClipData && state.review.currentClipData.card && state.review.currentClipData.card.clip_name) || clipId}”: call media_analysis(action="deepen", params={"clip_id": "${clipId}", "shot_index": ${shotIndex}}), show me the cost estimate, and after I confirm, read the frames and commit the per-shot fields via commit_shot_vision.`)}">Deepen this shot</button>
|
|
9443
9458
|
<button id="reviewShotEditToggleBtn" ${editing ? '' : 'class="secondary"'}>${editToggleLabel}</button>
|
|
9444
9459
|
</div>
|
|
9445
9460
|
`;
|
|
@@ -11082,6 +11097,11 @@ HTML = r"""<!doctype html>
|
|
|
11082
11097
|
runReviewSearch(q).catch(alertError);
|
|
11083
11098
|
}, 250);
|
|
11084
11099
|
});
|
|
11100
|
+
// Semantic toggle re-runs the current query through the embeddings index.
|
|
11101
|
+
$('reviewSemanticCheckbox')?.addEventListener('change', () => {
|
|
11102
|
+
const q = $('reviewSearchInput').value.trim();
|
|
11103
|
+
if (q) runReviewSearch(q).catch(alertError);
|
|
11104
|
+
});
|
|
11085
11105
|
// Bin dropdown.
|
|
11086
11106
|
$('reviewBinFilter').addEventListener('change', event => {
|
|
11087
11107
|
state.review.binFilter = event.target.value || '';
|
|
@@ -12469,6 +12489,57 @@ def _v2_load_analysis_db_first(project_root: str, clip_dir: str) -> Optional[Dic
|
|
|
12469
12489
|
return _v2_load_analysis(clip_dir)
|
|
12470
12490
|
|
|
12471
12491
|
|
|
12492
|
+
def _v2_semantic_search(project_root: str, q: str, *, limit: int = 20) -> Dict[str, Any]:
|
|
12493
|
+
"""Semantic search over the embeddings index, shaped like /api/index/query
|
|
12494
|
+
rows so the existing search-card renderer works unchanged."""
|
|
12495
|
+
text = (q or "").strip()
|
|
12496
|
+
if not text:
|
|
12497
|
+
return {"success": True, "results": []}
|
|
12498
|
+
try:
|
|
12499
|
+
from src.utils import embeddings, timeline_brain_db as tbd
|
|
12500
|
+
|
|
12501
|
+
found = embeddings.find_similar(project_root, text=text, kind="text", limit=limit)
|
|
12502
|
+
if not found.get("success"):
|
|
12503
|
+
return found
|
|
12504
|
+
conn = tbd.connect(project_root)
|
|
12505
|
+
resolve_ids: Dict[str, Optional[str]] = {}
|
|
12506
|
+
|
|
12507
|
+
def resolve_clip_id(clip_uuid: Optional[str]) -> Optional[str]:
|
|
12508
|
+
if not clip_uuid:
|
|
12509
|
+
return None
|
|
12510
|
+
if clip_uuid not in resolve_ids:
|
|
12511
|
+
row = conn.execute(
|
|
12512
|
+
"SELECT resolve_clip_id, clip_dir FROM clips WHERE clip_uuid = ?",
|
|
12513
|
+
(clip_uuid,),
|
|
12514
|
+
).fetchone()
|
|
12515
|
+
resolve_ids[clip_uuid] = (row["resolve_clip_id"] or row["clip_dir"]) if row else None
|
|
12516
|
+
return resolve_ids[clip_uuid]
|
|
12517
|
+
|
|
12518
|
+
rows: List[Dict[str, Any]] = []
|
|
12519
|
+
for hit in found.get("results") or []:
|
|
12520
|
+
entity_type = hit.get("entity_type")
|
|
12521
|
+
clip_uuid = hit.get("clip_uuid") or (hit.get("entity_uuid") if entity_type == "clip" else None)
|
|
12522
|
+
row: Dict[str, Any] = {
|
|
12523
|
+
"result_type": "transcript" if entity_type == "segment" else "semantic",
|
|
12524
|
+
"score": hit.get("score"),
|
|
12525
|
+
"clip_id": resolve_clip_id(clip_uuid),
|
|
12526
|
+
"clip_name": hit.get("clip_name"),
|
|
12527
|
+
}
|
|
12528
|
+
if entity_type == "shot":
|
|
12529
|
+
row["start_seconds"] = hit.get("time_seconds_start")
|
|
12530
|
+
row["summary"] = hit.get("description")
|
|
12531
|
+
elif entity_type == "segment":
|
|
12532
|
+
row["start_seconds"] = hit.get("start_seconds")
|
|
12533
|
+
row["summary"] = hit.get("text")
|
|
12534
|
+
else:
|
|
12535
|
+
row["summary"] = hit.get("summary")
|
|
12536
|
+
rows.append(row)
|
|
12537
|
+
rows = _v2_enrich_search_results(project_root, rows)
|
|
12538
|
+
return {"success": True, "query": text, "model": found.get("model"), "results": rows}
|
|
12539
|
+
except Exception as exc: # noqa: BLE001 — search must fail soft in the panel
|
|
12540
|
+
return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
12541
|
+
|
|
12542
|
+
|
|
12472
12543
|
def _v2_clip_duration(report: Dict[str, Any]) -> Optional[float]:
|
|
12473
12544
|
marker_plan = report.get("clip_analysis_markers") if isinstance(report.get("clip_analysis_markers"), dict) else {}
|
|
12474
12545
|
duration = marker_plan.get("duration_seconds")
|
|
@@ -14315,6 +14386,14 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
14315
14386
|
payload["results"] = _v2_enrich_search_results(self.state.project_root, payload["results"])
|
|
14316
14387
|
self._json(payload)
|
|
14317
14388
|
return
|
|
14389
|
+
if path == "/api/search/semantic":
|
|
14390
|
+
q = (query.get("q") or [""])[0]
|
|
14391
|
+
try:
|
|
14392
|
+
limit = int((query.get("limit") or ["20"])[0])
|
|
14393
|
+
except (TypeError, ValueError):
|
|
14394
|
+
limit = 20
|
|
14395
|
+
self._json(_v2_semantic_search(self.state.project_root, q, limit=limit))
|
|
14396
|
+
return
|
|
14318
14397
|
# ─── C6 timeline-history surface ───────────────────────────────
|
|
14319
14398
|
if path == "/api/timeline_versions":
|
|
14320
14399
|
self._json(list_timelines_with_versions(self.state.project_root))
|
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.43.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
|
@@ -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.43.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -15020,6 +15020,13 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
15020
15020
|
# C1 — DB-canonical analysis store.
|
|
15021
15021
|
"db_status",
|
|
15022
15022
|
"db_ingest",
|
|
15023
|
+
# Phase B — deep shot-level vision tier.
|
|
15024
|
+
"deepen",
|
|
15025
|
+
"commit_shot_vision",
|
|
15026
|
+
"vision_pending_sweep",
|
|
15027
|
+
# Phase C — embeddings + similarity.
|
|
15028
|
+
"build_embeddings",
|
|
15029
|
+
"find_similar",
|
|
15023
15030
|
}:
|
|
15024
15031
|
root = resolve_media_analysis_output_root(
|
|
15025
15032
|
project_name=project_name,
|
|
@@ -15079,6 +15086,70 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
15079
15086
|
if action == "db_ingest":
|
|
15080
15087
|
from src.utils import analysis_store
|
|
15081
15088
|
return analysis_store.ingest_project(project_root)
|
|
15089
|
+
# Phase B — deep shot-level vision tier.
|
|
15090
|
+
if action == "deepen":
|
|
15091
|
+
from src.utils import deep_vision
|
|
15092
|
+
clip_ref = p.get("clip_id") or p.get("clipId") or p.get("clip_dir") or p.get("clipDir") or p.get("file_path") or p.get("filePath")
|
|
15093
|
+
if not clip_ref:
|
|
15094
|
+
return _err("deepen requires clip_id, clip_dir, or file_path")
|
|
15095
|
+
raw_indices = p.get("shot_indices") or p.get("shotIndices")
|
|
15096
|
+
if raw_indices is None and p.get("shot_index") is not None:
|
|
15097
|
+
raw_indices = [p.get("shot_index")]
|
|
15098
|
+
return deep_vision.deepen_clip(
|
|
15099
|
+
project_root,
|
|
15100
|
+
clip_ref=clip_ref,
|
|
15101
|
+
shot_indices=[int(i) for i in raw_indices] if raw_indices else None,
|
|
15102
|
+
confirm_token=p.get("confirm_token") or p.get("confirmToken"),
|
|
15103
|
+
job_id=p.get("job_id") or p.get("jobId"),
|
|
15104
|
+
)
|
|
15105
|
+
if action == "commit_shot_vision":
|
|
15106
|
+
from src.utils import deep_vision
|
|
15107
|
+
clip_ref = p.get("clip_id") or p.get("clipId") or p.get("clip_dir") or p.get("clipDir") or p.get("file_path") or p.get("filePath")
|
|
15108
|
+
if not clip_ref:
|
|
15109
|
+
return _err("commit_shot_vision requires clip_id, clip_dir, or file_path")
|
|
15110
|
+
return deep_vision.commit_shot_vision(
|
|
15111
|
+
project_root,
|
|
15112
|
+
shots=p.get("shots"),
|
|
15113
|
+
vision_token=p.get("vision_token") or p.get("visionToken"),
|
|
15114
|
+
clip_ref=clip_ref,
|
|
15115
|
+
author=p.get("author") or "host_chat",
|
|
15116
|
+
)
|
|
15117
|
+
if action == "vision_pending_sweep":
|
|
15118
|
+
from src.utils import deep_vision
|
|
15119
|
+
return deep_vision.vision_pending_sweep(
|
|
15120
|
+
project_root,
|
|
15121
|
+
expire=_media_analysis_bool(p.get("expire"), False),
|
|
15122
|
+
max_age_days=p.get("max_age_days") or p.get("maxAgeDays"),
|
|
15123
|
+
reoffer=_media_analysis_bool(p.get("reoffer"), False),
|
|
15124
|
+
)
|
|
15125
|
+
# Phase C — embeddings + similarity (local compute; no token caps).
|
|
15126
|
+
if action == "build_embeddings":
|
|
15127
|
+
from src.utils import embeddings
|
|
15128
|
+
kinds = p.get("kinds") or p.get("kind") or ["text"]
|
|
15129
|
+
if isinstance(kinds, str):
|
|
15130
|
+
kinds = [kinds]
|
|
15131
|
+
return embeddings.build_embeddings(
|
|
15132
|
+
project_root,
|
|
15133
|
+
kinds=[str(k).strip().lower() for k in kinds],
|
|
15134
|
+
clip_ref=p.get("clip_id") or p.get("clipId") or p.get("clip_dir") or p.get("clipDir"),
|
|
15135
|
+
include_segments=_media_analysis_bool(p.get("include_segments", p.get("includeSegments")), True),
|
|
15136
|
+
max_frames_per_clip=int(p.get("max_frames_per_clip") or p.get("maxFramesPerClip") or 16),
|
|
15137
|
+
)
|
|
15138
|
+
if action == "find_similar":
|
|
15139
|
+
from src.utils import embeddings
|
|
15140
|
+
entity_types = p.get("entity_types") or p.get("entityTypes")
|
|
15141
|
+
if isinstance(entity_types, str):
|
|
15142
|
+
entity_types = [entity_types]
|
|
15143
|
+
return embeddings.find_similar(
|
|
15144
|
+
project_root,
|
|
15145
|
+
text=p.get("text") or p.get("query"),
|
|
15146
|
+
clip_ref=p.get("clip_id") or p.get("clipId") or p.get("clip_dir") or p.get("clipDir"),
|
|
15147
|
+
shot_index=p.get("shot_index") if p.get("shot_index") is not None else p.get("shotIndex"),
|
|
15148
|
+
shot_uuid=p.get("shot_uuid") or p.get("shotUuid"),
|
|
15149
|
+
kind=p.get("kind") or "text",
|
|
15150
|
+
entity_types=entity_types,
|
|
15151
|
+
limit=int(p.get("limit") or 10),
|
|
15152
|
+
)
|
|
15082
15153
|
if action in {"build_index", "rebuild_index"}:
|
|
15083
15154
|
return build_analysis_index(project_root, index_path=p.get("index_path") or p.get("indexPath"))
|
|
15084
15155
|
if action == "index_status":
|
|
@@ -15490,6 +15561,11 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
15490
15561
|
"list_corrections",
|
|
15491
15562
|
"db_status",
|
|
15492
15563
|
"db_ingest",
|
|
15564
|
+
"deepen",
|
|
15565
|
+
"commit_shot_vision",
|
|
15566
|
+
"vision_pending_sweep",
|
|
15567
|
+
"build_embeddings",
|
|
15568
|
+
"find_similar",
|
|
15493
15569
|
"get_caps",
|
|
15494
15570
|
"set_caps_preset",
|
|
15495
15571
|
"get_usage",
|
|
@@ -253,7 +253,10 @@ def _write_subjective(
|
|
|
253
253
|
value_json = _dumps(value)
|
|
254
254
|
existing = current.get(field_path)
|
|
255
255
|
if existing is not None:
|
|
256
|
-
|
|
256
|
+
# Identical value → keep the existing row and its provenance. A
|
|
257
|
+
# different source re-deriving the same value must not re-attribute
|
|
258
|
+
# it (a deep pass would otherwise claim every unchanged field).
|
|
259
|
+
if str(existing["value_json"]) == value_json:
|
|
257
260
|
continue
|
|
258
261
|
if (
|
|
259
262
|
str(existing["source"]) == HUMAN_SOURCE
|