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.
@@ -1436,6 +1436,28 @@ TOOL_INSTALL: Dict[str, Dict[str, Any]] = {
1436
1436
  "verify": "whisper --help",
1437
1437
  "notes": "Pure-Python reference implementation. Choose this OR whisper_cpp OR mlx_whisper.",
1438
1438
  },
1439
+ "ollama_embeddings": {
1440
+ "label": "ollama + nomic-embed-text",
1441
+ "bundle": "embeddings",
1442
+ "required_for": ["semantic search (text embeddings)", "find_similar"],
1443
+ "commands": {
1444
+ "macos": "brew install ollama && ollama pull nomic-embed-text",
1445
+ "linux": "curl -fsSL https://ollama.com/install.sh | sh && ollama pull nomic-embed-text",
1446
+ "windows": "winget install Ollama.Ollama, then: ollama pull nomic-embed-text",
1447
+ },
1448
+ "verify": "ollama list",
1449
+ "notes": "Local embedding model (~270 MB). sentence-transformers is an alternative text backend.",
1450
+ },
1451
+ "open_clip": {
1452
+ "label": "open_clip (CLIP visual embeddings)",
1453
+ "bundle": "embeddings",
1454
+ "required_for": ["visual similarity (find_similar kind=visual)", "cross-clip entity clustering"],
1455
+ "commands": {
1456
+ "all": "pip install open_clip_torch",
1457
+ },
1458
+ "verify": "python -c \"import open_clip\"",
1459
+ "notes": "Needs torch. Model weights (~350 MB) download on first use.",
1460
+ },
1439
1461
  "whisper_cpp": {
1440
1462
  "label": "whisper.cpp",
1441
1463
  "bundle": "transcription",
@@ -1585,6 +1607,17 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1585
1607
 
1586
1608
  sync_events = detect_sync_event_capabilities()
1587
1609
 
1610
+ # Phase C — embedding backends (detected like the whisper backends; the
1611
+ # ollama probe is a short local HTTP call and fails fast when not serving).
1612
+ try:
1613
+ from src.utils import embeddings as _embeddings
1614
+
1615
+ embedding_caps = _embeddings.detect_embedding_capabilities()
1616
+ except Exception: # noqa: BLE001 — detection must never break capabilities
1617
+ embedding_caps = {"text": {"available": False, "backends": []},
1618
+ "visual": {"available": False, "backends": []},
1619
+ "install_guidance": {}}
1620
+
1588
1621
  platform_id, machine = _runtime_platform_id()
1589
1622
 
1590
1623
  def _tool_entry(name: str, available: bool, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@@ -1607,7 +1640,18 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1607
1640
  "whisper_cpp": _tool_entry("whisper_cpp", bool(whisper_cpp), {"path": whisper_cpp}),
1608
1641
  "mlx_whisper": _tool_entry("mlx_whisper", bool(mlx_whisper), {"python_module": "mlx_whisper"}),
1609
1642
  "opencv": _tool_entry("opencv", bool(cv2), {"python_module": "cv2"}),
1643
+ "ollama_embeddings": _tool_entry(
1644
+ "ollama_embeddings",
1645
+ bool(embedding_caps.get("text", {}).get("available")),
1646
+ {"backends": embedding_caps.get("text", {}).get("backends", [])},
1647
+ ),
1648
+ "open_clip": _tool_entry(
1649
+ "open_clip",
1650
+ bool(embedding_caps.get("visual", {}).get("available")),
1651
+ {"python_module": "open_clip"},
1652
+ ),
1610
1653
  },
1654
+ "embeddings": embedding_caps,
1611
1655
  "transcription": {
1612
1656
  "available": bool(whisper_cli or whisper_cpp or mlx_whisper),
1613
1657
  "backends": [
@@ -4265,7 +4309,7 @@ def build_host_chat_paths_payload(
4265
4309
  if refusal is not None:
4266
4310
  return refusal
4267
4311
 
4268
- return {
4312
+ payload: Dict[str, Any] = {
4269
4313
  "success": True,
4270
4314
  "status": "pending_host_analysis",
4271
4315
  "provider": HOST_CHAT_PATHS_PROVIDER,
@@ -4327,6 +4371,21 @@ def build_host_chat_paths_payload(
4327
4371
  "the clip's analysis directory; commit_vision finishes the run."
4328
4372
  ),
4329
4373
  }
4374
+ # Phase B — deep depth: each shot_descriptions entry must additionally
4375
+ # carry the per-shot field groups. The extra keys flow through
4376
+ # commit_vision → canonical blob → subjective_fields rows unchanged.
4377
+ if str(options.get("depth") or "").lower() == "deep":
4378
+ from src.utils import deep_vision as _deep_vision
4379
+
4380
+ payload["deep_shot_schema"] = _deep_vision.deep_shot_schema()
4381
+ payload["deep_schema_reference"] = _deep_vision.DEEP_SHOT_SCHEMA_REFERENCE
4382
+ payload["instructions"] += (
4383
+ " DEEP PASS: in addition to `description`, every shot_descriptions "
4384
+ "entry MUST include the field groups in `deep_shot_schema` (visual, "
4385
+ "content, production, editorial, cuttability, confidence), using the "
4386
+ "enum values verbatim and 'unknown'/null when frame evidence is thin."
4387
+ )
4388
+ return payload
4330
4389
 
4331
4390
 
4332
4391
  def _vision_analysis(record: Dict[str, Any], motion: Dict[str, Any], options: Dict[str, Any], artifacts: Dict[str, Any], capabilities: Dict[str, Any]) -> Dict[str, Any]:
@@ -5096,6 +5155,9 @@ async def execute_plan_async(
5096
5155
  # Same for project_root — caps recording needs it to address the per-
5097
5156
  # project usage DB; falling back to the plan's output_root is fine.
5098
5157
  "project_root": params.get("project_root") or output_root,
5158
+ # Phase B — depth threads into the vision payload builder so deep runs
5159
+ # carry the per-shot field-group schema.
5160
+ "depth": plan.get("depth", DEFAULT_DEPTH),
5099
5161
  }
5100
5162
  keep_frame_artifacts_for_vision = vision_uses_chat_context(options, caps)
5101
5163
  depth = plan.get("depth", DEFAULT_DEPTH)
@@ -5114,6 +5176,37 @@ async def execute_plan_async(
5114
5176
  }
5115
5177
  _write_json(os.path.join(output_root, "capabilities.json"), caps)
5116
5178
 
5179
+ # Phase B — deep depth is opt-in with an explicit cost estimate first.
5180
+ # The per-shot field-group pass multiplies vision spend, so the first call
5181
+ # returns the estimate; re-call with confirm_deep=true to run. Caps still
5182
+ # apply downstream — confirmation does not bypass budgets.
5183
+ if (
5184
+ depth == "deep"
5185
+ and vision_uses_chat_context(options, caps)
5186
+ and not _coerce_bool(params.get("confirm_deep") or params.get("confirmDeep"), default=False)
5187
+ ):
5188
+ executing = [
5189
+ clip for clip in plan.get("clips", [])
5190
+ if not (clip.get("skip_execution") and (clip.get("existing_report") or {}).get("path"))
5191
+ ]
5192
+ estimated_frames = sum(int(c.get("analysis_keyframe_budget") or 0) for c in executing)
5193
+ return {
5194
+ "success": True,
5195
+ "status": "confirmation_required",
5196
+ "reason": "deep_depth_cost_estimate",
5197
+ "estimate": {
5198
+ "clip_count": len(executing),
5199
+ "estimated_frames": estimated_frames,
5200
+ "estimated_vision_tokens": estimated_frames * AVG_VISION_TOKENS_PER_FRAME,
5201
+ "tokens_per_frame_assumption": AVG_VISION_TOKENS_PER_FRAME,
5202
+ },
5203
+ "note": (
5204
+ "Deep analysis fills per-shot Visual/Content/Editorial field groups "
5205
+ "and costs vision tokens accordingly. Re-call the same analyze action "
5206
+ "with confirm_deep=true to proceed, or drop depth to 'standard'."
5207
+ ),
5208
+ }
5209
+
5117
5210
  for clip_plan in plan.get("clips", []):
5118
5211
  record = clip_plan["record"]
5119
5212
  artifacts = clip_plan["artifacts"]
@@ -29,7 +29,7 @@ from typing import Callable, Dict, Iterator, Optional, Tuple
29
29
 
30
30
  logger = logging.getLogger("resolve-mcp.timeline-brain-db")
31
31
 
32
- SCHEMA_VERSION = 9
32
+ SCHEMA_VERSION = 10
33
33
  DB_FILENAME = "timeline_brain.sqlite"
34
34
  SOUL_DIRNAME = "_soul"
35
35
 
@@ -687,6 +687,38 @@ def _migrate_v9_analysis_core(conn: sqlite3.Connection) -> None:
687
687
  )
688
688
 
689
689
 
690
+ @register_migration(10)
691
+ def _migrate_v10_embeddings(conn: sqlite3.Connection) -> None:
692
+ """C3 — embeddings for similarity search (Phase C of the analysis program).
693
+
694
+ One row per (entity, kind, model). Vectors are float32 BLOBs; similarity
695
+ is brute-force cosine in the app layer (thousands of rows, not millions).
696
+ `content_hash` fingerprints the embedded content so re-runs only re-embed
697
+ entities whose text/frames changed.
698
+ """
699
+ conn.executescript(
700
+ """
701
+ CREATE TABLE IF NOT EXISTS embeddings (
702
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
703
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('clip', 'shot', 'frame', 'segment')),
704
+ entity_uuid TEXT NOT NULL,
705
+ embedding_kind TEXT NOT NULL, -- 'text' | 'visual'
706
+ model_name TEXT NOT NULL,
707
+ dimension INTEGER NOT NULL,
708
+ vector BLOB NOT NULL,
709
+ content_hash TEXT,
710
+ computed_at TEXT NOT NULL,
711
+ UNIQUE(entity_type, entity_uuid, embedding_kind, model_name)
712
+ );
713
+
714
+ CREATE INDEX IF NOT EXISTS ix_embeddings_kind
715
+ ON embeddings(embedding_kind, model_name);
716
+ CREATE INDEX IF NOT EXISTS ix_embeddings_entity
717
+ ON embeddings(entity_type, entity_uuid);
718
+ """
719
+ )
720
+
721
+
690
722
  def latest_version(conn: sqlite3.Connection, timeline_name: str) -> Optional[int]:
691
723
  """Return the highest archived version number for `timeline_name`, or None."""
692
724
  row = conn.execute(