davinci-resolve-mcp 2.42.0 → 2.44.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 CHANGED
@@ -2,6 +2,59 @@
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.44.0
6
+
7
+ Cross-clip entities + bin briefing v2 — Phase D of the analysis +
8
+ edit-engine program. Recurring people/places/props found by clustering the
9
+ visual embeddings, confirmed with one vision call per cluster.
10
+
11
+ - **Added** schema v11 (`entities` + `entity_appearances`) and
12
+ `src/utils/entities.py`: union-find clustering over the v10 CLIP frame
13
+ vectors (cosine threshold, no new deps), representative-frame selection,
14
+ ghost pruning across re-runs (labeled entities persist), and a
15
+ detection-state stash so `entity_index` always resolves against the exact
16
+ ordering the payload was issued with.
17
+ - **Added** `media_analysis` actions: `detect_entities` (clusters + deferred
18
+ one-frame-per-cluster confirmation payload, caps pre-checked),
19
+ `commit_entities` (kind/label/description with conservative-label rules;
20
+ `merge_with` collapses duplicate clusters), `list_entities`,
21
+ `prepare_bin_briefing` (entities + per-clip summaries, text-only), and
22
+ `commit_bin_summary` (host-synthesized briefing written above the v2.0
23
+ aggregate in `memory/bin_summary.md`).
24
+ - **Added** a "Recurring across this bin" card on the panel's Review page
25
+ (`/api/entities`), shown once labeled entities exist.
26
+ - **Validation**: full offline suite (1105 tests; 10 new). Live on the real
27
+ sample root: 3 clusters detected from 16 CLIP vectors; host-chat
28
+ confirmation labeled the shattered-windshield POV and the white rental
29
+ sedan and merged the sedan's two clusters; bin briefing synthesized and
30
+ committed; panel card verified and screenshots regenerated.
31
+
32
+ ## What's New in v2.43.0
33
+
34
+ Embeddings + similarity search — Phase C of the analysis + edit-engine
35
+ program. "Find clips/shots like this," locally, with no vendor token cost.
36
+
37
+ - **Added** `src/utils/embeddings.py` and schema v10 (`embeddings` table:
38
+ one float32 vector per entity/kind/model, with content hashes so re-runs
39
+ only re-embed what changed). Brute-force cosine — numpy when present.
40
+ - **Added** `media_analysis` actions `build_embeddings` (idempotent; clip
41
+ summaries, shot descriptions + deep field groups, transcript segments,
42
+ sampled frames with per-shot mean vectors) and `find_similar` (query by
43
+ free text, clip, or shot; `kind="text"|"visual"`; free-text visual queries
44
+ go through the CLIP text encoder).
45
+ - **Added** backend detection, never installation (the whisper pattern):
46
+ text = ollama `nomic-embed-text` (serving probe) or sentence-transformers;
47
+ visual = open_clip ViT-B-32. Capabilities and the Diagnostics → Tools page
48
+ list availability with install guidance.
49
+ - **Added** a `Semantic` toggle on the panel's Review search (shown only
50
+ when a text backend is detected) backed by `/api/search/semantic`, which
51
+ returns rows in the existing search-card shape.
52
+ - **Validation**: full offline suite (1095 tests; 14 new, backends mocked).
53
+ Live on the 2026-05-17 sample root: 54 text vectors in 1.2s via ollama
54
+ with correct top hits for three editorial queries; 27 CLIP vectors with
55
+ "cracked broken windshield glass" ranking the shattered-windshield frame
56
+ first; panel endpoint verified and screenshots regenerated.
57
+
5
58
  ## What's New in v2.42.0
6
59
 
7
60
  Deep shot-level vision tier — Phase B of the analysis + edit-engine program.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.42.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.44.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-33%20(341%20full)-blue.svg)](#server-modes)
package/docs/SKILL.md CHANGED
@@ -495,8 +495,44 @@ Key actions: `capabilities`, `install_guidance`, `resolve_output_root`, `plan`,
495
495
  `cleanup_artifacts`, `db_status`, `db_ingest`, `get_panel_state`,
496
496
  `set_panel_state`, `session_start_context`, `update_clip_field`,
497
497
  `update_shot_field`, `get_field_history`, `revert_field`,
498
- `list_corrections`, `deepen`, `commit_shot_vision`, and
499
- `vision_pending_sweep`.
498
+ `list_corrections`, `deepen`, `commit_shot_vision`, `vision_pending_sweep`,
499
+ `build_embeddings`, `find_similar`, `detect_entities`, `commit_entities`,
500
+ `list_entities`, `prepare_bin_briefing`, and `commit_bin_summary`.
501
+
502
+ **Cross-clip entities + bin briefing v2 (v2.44.0+).** Recurring
503
+ people/places/props across a project's media, found cheaply and confirmed
504
+ with ONE vision call per cluster:
505
+ - `detect_entities(threshold?, min_cluster_size?)` clusters the v10 CLIP
506
+ frame vectors (build visual embeddings first), writes provisional entity
507
+ rows + appearances, and returns a deferred payload with one
508
+ representative frame per cluster (caps pre-checked, estimate inlined).
509
+ The host chat reads those frames and calls
510
+ `commit_entities(entities=[{entity_index, kind, label, description,
511
+ confidence, merge_with?}], vision_token)` — conservative labels only
512
+ (describe what's visible; never guess names). `merge_with` collapses
513
+ clusters that show the same entity.
514
+ - `list_entities` returns labeled entities with per-clip/shot appearances;
515
+ the panel's Review page shows a "Recurring across this bin" card.
516
+ - `prepare_bin_briefing` returns entities + per-clip summaries (text-only,
517
+ no vision cost); the host writes a colleague-style markdown briefing and
518
+ calls `commit_bin_summary(briefing, briefing_token)`, which lands in
519
+ `memory/bin_summary.md` above the v2.0 aggregate.
520
+
521
+ **Embeddings + similarity (v2.43.0+).** Local-compute semantic search; no
522
+ vendor tokens, so nothing here touches the caps ledger. Backends are
523
+ detected, never installed (capabilities lists them with install guidance):
524
+ text = ollama serving `nomic-embed-text` or sentence-transformers; visual =
525
+ open_clip (ViT-B-32, needs torch).
526
+ - `build_embeddings(kinds=["text","visual"]?, clip_id?)` — idempotent;
527
+ embeds clip summaries, shot descriptions (+ deep field groups), transcript
528
+ segments, and sampled frames (per-shot visual vector = mean of its
529
+ frames'). Only re-embeds entities whose content changed.
530
+ - `find_similar(text=… | clip_id=… | clip_id+shot_index, kind="text"|"visual",
531
+ entity_types?, limit?)` — brute-force cosine over the project's vectors.
532
+ Free-text visual queries use the CLIP text encoder ("cracked windshield"
533
+ finds the frame). Results carry scores plus clip/shot/segment context.
534
+ The panel search box gains a `Semantic` toggle when a text backend is
535
+ detected. Vectors live in the per-project DB (schema v10).
500
536
 
501
537
  **Deep shot-level vision tier (v2.42.0+).** Opt-in, estimate-first. Two
502
538
  entry points share one per-shot schema (Visual / Content / Production /
@@ -78,7 +78,14 @@ 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. Once cross-clip entity
86
+ detection has run (`detect_entities` + a one-frame-per-cluster confirmation
87
+ in chat), a `Recurring across this bin` card lists the labeled people,
88
+ places, and objects with their shot counts.
82
89
 
83
90
  ### Clip detail
84
91
 
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.42.0"
38
+ VERSION = "2.44.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.42.0",
3
+ "version": "2.44.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -2114,6 +2114,30 @@ HTML = r"""<!doctype html>
2114
2114
  margin-bottom: var(--space-3);
2115
2115
  flex-wrap: wrap;
2116
2116
  }
2117
+ .review-semantic-toggle {
2118
+ display: inline-flex;
2119
+ gap: 6px;
2120
+ align-items: center;
2121
+ color: var(--text-secondary);
2122
+ font-size: var(--ops-text-label);
2123
+ white-space: nowrap;
2124
+ }
2125
+ .review-entities {
2126
+ margin-bottom: var(--space-3);
2127
+ padding: var(--space-2) var(--space-3);
2128
+ border: 1px solid var(--border-default);
2129
+ border-radius: var(--radius-md);
2130
+ }
2131
+ .review-entities-title {
2132
+ color: var(--text-secondary);
2133
+ font-size: var(--ops-text-label);
2134
+ margin-bottom: 6px;
2135
+ }
2136
+ .review-entities-chips {
2137
+ display: flex;
2138
+ gap: var(--space-2);
2139
+ flex-wrap: wrap;
2140
+ }
2117
2141
  .review-bin-filters input[type="search"] {
2118
2142
  flex: 1 1 280px;
2119
2143
  min-width: 200px;
@@ -4065,6 +4089,9 @@ HTML = r"""<!doctype html>
4065
4089
  </div>
4066
4090
  <div class="review-bin-filters">
4067
4091
  <input id="reviewSearchInput" type="search" placeholder="Search clips, summaries, tags, transcripts…" autocomplete="off">
4092
+ <label id="reviewSemanticToggle" class="review-semantic-toggle" style="display:none" title="Search by meaning using local text embeddings instead of exact words.">
4093
+ <input id="reviewSemanticCheckbox" type="checkbox"> Semantic
4094
+ </label>
4068
4095
  <select id="reviewBinFilter" aria-label="Filter by bin">
4069
4096
  <option value="">All bins</option>
4070
4097
  </select>
@@ -4074,6 +4101,7 @@ HTML = r"""<!doctype html>
4074
4101
  </div>
4075
4102
  </div>
4076
4103
  <div id="reviewBinSummary" class="review-bin-summary">Loading analyzed clips…</div>
4104
+ <div id="reviewEntitiesCard" class="review-entities" style="display:none"></div>
4077
4105
  <div id="reviewBinGrid" class="review-grid"></div>
4078
4106
  <div id="reviewSearchResults" class="review-search-results" style="display:none"></div>
4079
4107
  </div>
@@ -6201,6 +6229,13 @@ HTML = r"""<!doctype html>
6201
6229
  syncPreferencesPanel();
6202
6230
  applyPreferencesToControls(prefs);
6203
6231
  renderVersionBadge();
6232
+ // Semantic search rides on a local text-embedding backend; only show
6233
+ // the toggle when one is detected (ollama nomic-embed-text or
6234
+ // sentence-transformers).
6235
+ if (state.boot?.capabilities?.embeddings?.text?.available) {
6236
+ const toggle = $('reviewSemanticToggle');
6237
+ if (toggle) toggle.style.display = '';
6238
+ }
6204
6239
  // Paint the previous inventory immediately (stale) so a slow first
6205
6240
  // /api/resolve/media — common with network source media — doesn't leave the
6206
6241
  // panel blank. The live fetch below replaces it and clears the stale flag.
@@ -8083,6 +8118,22 @@ HTML = r"""<!doctype html>
8083
8118
  renderReviewBin();
8084
8119
  // Coverage runs in parallel — failures here must not block the clip grid.
8085
8120
  refreshReadinessCard().catch(() => {});
8121
+ refreshEntitiesCard().catch(() => {});
8122
+ }
8123
+
8124
+ // Recurring people/places/props detected across the bin (Phase D).
8125
+ // Hidden until at least one labeled entity exists.
8126
+ async function refreshEntitiesCard() {
8127
+ const card = $('reviewEntitiesCard');
8128
+ if (!card) return;
8129
+ const data = await api('/api/entities').catch(() => null);
8130
+ const labeled = (data?.entities || []).filter(e => e.label);
8131
+ if (!labeled.length) { card.style.display = 'none'; return; }
8132
+ card.style.display = '';
8133
+ card.innerHTML = `<div class="review-entities-title">Recurring across this bin</div>
8134
+ <div class="review-entities-chips">${labeled.map(e =>
8135
+ `<span class="review-chip" title="${escapeHtml(e.description || '')}">${escapeHtml(e.label)} · ${e.kind || 'unknown'} · ${e.shot_count || e.cluster_size || 0} shots</span>`
8136
+ ).join('')}</div>`;
8086
8137
  }
8087
8138
 
8088
8139
  async function refreshReadinessCard() {
@@ -8258,7 +8309,11 @@ HTML = r"""<!doctype html>
8258
8309
  renderReviewBin();
8259
8310
  return;
8260
8311
  }
8261
- const payload = await api(`/api/index/query?q=${encodeURIComponent(query)}&limit=40`)
8312
+ const semantic = !!$('reviewSemanticCheckbox')?.checked;
8313
+ const endpoint = semantic
8314
+ ? `/api/search/semantic?q=${encodeURIComponent(query)}&limit=40`
8315
+ : `/api/index/query?q=${encodeURIComponent(query)}&limit=40`;
8316
+ const payload = await api(endpoint)
8262
8317
  .catch(err => ({ success: false, error: String(err) }));
8263
8318
  state.review.searchResults = payload;
8264
8319
  renderReviewBin();
@@ -11083,6 +11138,11 @@ HTML = r"""<!doctype html>
11083
11138
  runReviewSearch(q).catch(alertError);
11084
11139
  }, 250);
11085
11140
  });
11141
+ // Semantic toggle re-runs the current query through the embeddings index.
11142
+ $('reviewSemanticCheckbox')?.addEventListener('change', () => {
11143
+ const q = $('reviewSearchInput').value.trim();
11144
+ if (q) runReviewSearch(q).catch(alertError);
11145
+ });
11086
11146
  // Bin dropdown.
11087
11147
  $('reviewBinFilter').addEventListener('change', event => {
11088
11148
  state.review.binFilter = event.target.value || '';
@@ -12470,6 +12530,57 @@ def _v2_load_analysis_db_first(project_root: str, clip_dir: str) -> Optional[Dic
12470
12530
  return _v2_load_analysis(clip_dir)
12471
12531
 
12472
12532
 
12533
+ def _v2_semantic_search(project_root: str, q: str, *, limit: int = 20) -> Dict[str, Any]:
12534
+ """Semantic search over the embeddings index, shaped like /api/index/query
12535
+ rows so the existing search-card renderer works unchanged."""
12536
+ text = (q or "").strip()
12537
+ if not text:
12538
+ return {"success": True, "results": []}
12539
+ try:
12540
+ from src.utils import embeddings, timeline_brain_db as tbd
12541
+
12542
+ found = embeddings.find_similar(project_root, text=text, kind="text", limit=limit)
12543
+ if not found.get("success"):
12544
+ return found
12545
+ conn = tbd.connect(project_root)
12546
+ resolve_ids: Dict[str, Optional[str]] = {}
12547
+
12548
+ def resolve_clip_id(clip_uuid: Optional[str]) -> Optional[str]:
12549
+ if not clip_uuid:
12550
+ return None
12551
+ if clip_uuid not in resolve_ids:
12552
+ row = conn.execute(
12553
+ "SELECT resolve_clip_id, clip_dir FROM clips WHERE clip_uuid = ?",
12554
+ (clip_uuid,),
12555
+ ).fetchone()
12556
+ resolve_ids[clip_uuid] = (row["resolve_clip_id"] or row["clip_dir"]) if row else None
12557
+ return resolve_ids[clip_uuid]
12558
+
12559
+ rows: List[Dict[str, Any]] = []
12560
+ for hit in found.get("results") or []:
12561
+ entity_type = hit.get("entity_type")
12562
+ clip_uuid = hit.get("clip_uuid") or (hit.get("entity_uuid") if entity_type == "clip" else None)
12563
+ row: Dict[str, Any] = {
12564
+ "result_type": "transcript" if entity_type == "segment" else "semantic",
12565
+ "score": hit.get("score"),
12566
+ "clip_id": resolve_clip_id(clip_uuid),
12567
+ "clip_name": hit.get("clip_name"),
12568
+ }
12569
+ if entity_type == "shot":
12570
+ row["start_seconds"] = hit.get("time_seconds_start")
12571
+ row["summary"] = hit.get("description")
12572
+ elif entity_type == "segment":
12573
+ row["start_seconds"] = hit.get("start_seconds")
12574
+ row["summary"] = hit.get("text")
12575
+ else:
12576
+ row["summary"] = hit.get("summary")
12577
+ rows.append(row)
12578
+ rows = _v2_enrich_search_results(project_root, rows)
12579
+ return {"success": True, "query": text, "model": found.get("model"), "results": rows}
12580
+ except Exception as exc: # noqa: BLE001 — search must fail soft in the panel
12581
+ return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
12582
+
12583
+
12473
12584
  def _v2_clip_duration(report: Dict[str, Any]) -> Optional[float]:
12474
12585
  marker_plan = report.get("clip_analysis_markers") if isinstance(report.get("clip_analysis_markers"), dict) else {}
12475
12586
  duration = marker_plan.get("duration_seconds")
@@ -14316,6 +14427,22 @@ class Handler(BaseHTTPRequestHandler):
14316
14427
  payload["results"] = _v2_enrich_search_results(self.state.project_root, payload["results"])
14317
14428
  self._json(payload)
14318
14429
  return
14430
+ if path == "/api/entities":
14431
+ try:
14432
+ from src.utils import entities as _entities
14433
+
14434
+ self._json(_entities.list_entities(self.state.project_root))
14435
+ except Exception as exc: # noqa: BLE001 — panel reads fail soft
14436
+ self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
14437
+ return
14438
+ if path == "/api/search/semantic":
14439
+ q = (query.get("q") or [""])[0]
14440
+ try:
14441
+ limit = int((query.get("limit") or ["20"])[0])
14442
+ except (TypeError, ValueError):
14443
+ limit = 20
14444
+ self._json(_v2_semantic_search(self.state.project_root, q, limit=limit))
14445
+ return
14319
14446
  # ─── C6 timeline-history surface ───────────────────────────────
14320
14447
  if path == "/api/timeline_versions":
14321
14448
  self._json(list_timelines_with_versions(self.state.project_root))
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.42.0"
83
+ VERSION = "2.44.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.42.0"
14
+ VERSION = "2.44.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -15024,6 +15024,15 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
15024
15024
  "deepen",
15025
15025
  "commit_shot_vision",
15026
15026
  "vision_pending_sweep",
15027
+ # Phase C — embeddings + similarity.
15028
+ "build_embeddings",
15029
+ "find_similar",
15030
+ # Phase D — cross-clip entities + bin briefing v2.
15031
+ "detect_entities",
15032
+ "commit_entities",
15033
+ "list_entities",
15034
+ "prepare_bin_briefing",
15035
+ "commit_bin_summary",
15027
15036
  }:
15028
15037
  root = resolve_media_analysis_output_root(
15029
15038
  project_name=project_name,
@@ -15119,6 +15128,66 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
15119
15128
  max_age_days=p.get("max_age_days") or p.get("maxAgeDays"),
15120
15129
  reoffer=_media_analysis_bool(p.get("reoffer"), False),
15121
15130
  )
15131
+ # Phase C — embeddings + similarity (local compute; no token caps).
15132
+ if action == "build_embeddings":
15133
+ from src.utils import embeddings
15134
+ kinds = p.get("kinds") or p.get("kind") or ["text"]
15135
+ if isinstance(kinds, str):
15136
+ kinds = [kinds]
15137
+ return embeddings.build_embeddings(
15138
+ project_root,
15139
+ kinds=[str(k).strip().lower() for k in kinds],
15140
+ clip_ref=p.get("clip_id") or p.get("clipId") or p.get("clip_dir") or p.get("clipDir"),
15141
+ include_segments=_media_analysis_bool(p.get("include_segments", p.get("includeSegments")), True),
15142
+ max_frames_per_clip=int(p.get("max_frames_per_clip") or p.get("maxFramesPerClip") or 16),
15143
+ )
15144
+ if action == "find_similar":
15145
+ from src.utils import embeddings
15146
+ entity_types = p.get("entity_types") or p.get("entityTypes")
15147
+ if isinstance(entity_types, str):
15148
+ entity_types = [entity_types]
15149
+ return embeddings.find_similar(
15150
+ project_root,
15151
+ text=p.get("text") or p.get("query"),
15152
+ clip_ref=p.get("clip_id") or p.get("clipId") or p.get("clip_dir") or p.get("clipDir"),
15153
+ shot_index=p.get("shot_index") if p.get("shot_index") is not None else p.get("shotIndex"),
15154
+ shot_uuid=p.get("shot_uuid") or p.get("shotUuid"),
15155
+ kind=p.get("kind") or "text",
15156
+ entity_types=entity_types,
15157
+ limit=int(p.get("limit") or 10),
15158
+ )
15159
+ # Phase D — cross-clip entities + bin briefing v2.
15160
+ if action == "detect_entities":
15161
+ from src.utils import entities
15162
+ return entities.detect_entities(
15163
+ project_root,
15164
+ threshold=float(p.get("threshold") or entities.DEFAULT_CLUSTER_THRESHOLD),
15165
+ min_cluster_size=int(p.get("min_cluster_size") or p.get("minClusterSize") or entities.DEFAULT_MIN_CLUSTER_SIZE),
15166
+ max_clusters=int(p.get("max_clusters") or p.get("maxClusters") or 24),
15167
+ job_id=p.get("job_id") or p.get("jobId"),
15168
+ )
15169
+ if action == "commit_entities":
15170
+ from src.utils import entities
15171
+ return entities.commit_entities(
15172
+ project_root,
15173
+ entities_payload=p.get("entities"),
15174
+ vision_token=p.get("vision_token") or p.get("visionToken"),
15175
+ author=p.get("author") or "host_chat",
15176
+ )
15177
+ if action == "list_entities":
15178
+ from src.utils import entities
15179
+ return entities.list_entities(project_root, kind=p.get("kind"))
15180
+ if action == "prepare_bin_briefing":
15181
+ from src.utils import entities
15182
+ return entities.prepare_bin_briefing(project_root)
15183
+ if action == "commit_bin_summary":
15184
+ from src.utils import entities
15185
+ return entities.commit_bin_summary(
15186
+ project_root,
15187
+ briefing=p.get("briefing") or p.get("summary"),
15188
+ briefing_token=p.get("briefing_token") or p.get("briefingToken"),
15189
+ author=p.get("author") or "host_chat",
15190
+ )
15122
15191
  if action in {"build_index", "rebuild_index"}:
15123
15192
  return build_analysis_index(project_root, index_path=p.get("index_path") or p.get("indexPath"))
15124
15193
  if action == "index_status":
@@ -15533,6 +15602,13 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
15533
15602
  "deepen",
15534
15603
  "commit_shot_vision",
15535
15604
  "vision_pending_sweep",
15605
+ "build_embeddings",
15606
+ "find_similar",
15607
+ "detect_entities",
15608
+ "commit_entities",
15609
+ "list_entities",
15610
+ "prepare_bin_briefing",
15611
+ "commit_bin_summary",
15536
15612
  "get_caps",
15537
15613
  "set_caps_preset",
15538
15614
  "get_usage",