davinci-resolve-mcp 2.40.0 → 2.42.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,74 @@
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.42.0
6
+
7
+ Deep shot-level vision tier — Phase B of the analysis + edit-engine program.
8
+ Opt-in, estimate-first per-shot field filling for the Visual / Content /
9
+ Production / Editorial / Cuttability groups the shot pages already render.
10
+
11
+ - **Added** `src/utils/deep_vision.py` and three `media_analysis` actions:
12
+ `deepen` (estimate → confirm_token → deferred host-vision payload per
13
+ clip/shot), `commit_shot_vision` (writes `vision_deep_v1` provenance rows,
14
+ updates the canonical blob, re-exports analysis.json in lockstep), and
15
+ `vision_pending_sweep` (lists clips stuck in `pending_host_analysis`;
16
+ `reoffer=true` returns the stored payload, `expire=true` stamps them).
17
+ - **Added** deep depth to the analyze flow: `depth="deep"` extends the
18
+ deferred payload with the per-shot schema and requires `confirm_deep=true`
19
+ after a token-cost estimate. Caps pre-call refusal applies on both paths.
20
+ - **Added** panel affordances: `Deepen analysis` (clip view) and `Deepen this
21
+ shot` (shot view) copy ready-made chat prompts, per the chat-first UX.
22
+ Shots with no sampled frames on disk get 1–2 frames re-extracted via
23
+ ffmpeg, downscaled per caps (source media stays read-only).
24
+ - **Fixed** a provenance bug in the analysis store: a source re-deriving an
25
+ unchanged value no longer re-attributes the row (a deep pass would
26
+ otherwise claim every untouched field as `vision_deep_v1`).
27
+ - **Validation**: full offline suite (1081 tests; 15 new), and a real
28
+ end-to-end deep pass on the 2026-05-17 sample clip — estimate → confirm →
29
+ frames read by the host chat → commit → rows/blob/export parity → fields
30
+ visible in the panel shot view. Control-panel guide screenshots
31
+ regenerated from the live panel.
32
+
33
+ ## What's New in v2.41.0
34
+
35
+ DB-canonical clip analysis (C1) — Phase A of the analysis + edit-engine
36
+ program. The per-project SQLite DB is now the source of truth for clip
37
+ analysis; `analysis.json` becomes a derived export written in lockstep.
38
+
39
+ - **Added** schema v9 to the per-project timeline-brain DB: `clips`,
40
+ `clip_aliases`, `analysis_reports` (canonical full payload), `shots`,
41
+ `subjective_fields` + `field_changelog` (per-field provenance),
42
+ `transcript_segments`, `frames`, and `qc_observations`.
43
+ - **Added** `src/utils/analysis_store.py`: transactional ingest, export with
44
+ human-correction overlay (human rows always win and survive re-analysis),
45
+ alias-based clip lookup, shot ids stable under one-second boundary jitter,
46
+ and a round-trip guard verified against a real sample analysis root.
47
+ - **Added** `media_analysis` actions `db_status` (schema version + row
48
+ counts) and `db_ingest` (one-shot migration of existing JSON reports and
49
+ `corrections.json` sidecars into the DB).
50
+ - **Changed** the analysis write path: `execute_plan` and `commit_vision`
51
+ write DB rows first, then export the JSON. Panel clip/shot endpoints read
52
+ DB-first with JSON fallback for pre-v9 reports and job-linked report dirs.
53
+ - **Changed** `update_clip_field` / `update_shot_field` / `revert_field` to
54
+ mirror corrections into the DB as row-level provenance (the
55
+ `corrections.json` sidecar remains for compatibility).
56
+ - **Fixed** eight V2 actions that were unreachable from MCP dispatch since
57
+ v2.24.0 (`get_panel_state`, `set_panel_state`, `session_start_context`,
58
+ `update_shot_field`, `update_clip_field`, `get_field_history`,
59
+ `revert_field`, `list_corrections`): they were checked inside the
60
+ project-root dispatch block but missing from its membership set. The
61
+ control panel proxied to the helpers directly, which masked it.
62
+ - **Fixed** the action-list drift guard to inspect async tool functions
63
+ (media_analysis had drifted unchecked) and to fail on actions that are
64
+ unreachable inside membership blocks — the exact class above. Four
65
+ reachable-but-unadvertised actions (`coverage_report`,
66
+ `get_resolve_ai_usage`, `get_ai_governance`, `set_ai_governance`) are now
67
+ listed in the unknown-action error.
68
+ - **Validation**: full offline suite (1066 tests; 12 new), round-trip guard
69
+ on the real 2026-05-17 sample root, and a live headless pipeline run on
70
+ synthetic media verifying rows-then-export parity, row-level corrections,
71
+ and DB-first panel reads.
72
+
5
73
  ## What's New in v2.40.0
6
74
 
7
75
  Control panel UX overhaul + docs refresh, from a full live audit of every
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.40.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.42.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
@@ -491,8 +491,45 @@ Key actions: `capabilities`, `install_guidance`, `resolve_output_root`, `plan`,
491
491
  `publish_clip_metadata`, `commit_vision`, `summarize`, `get_report`,
492
492
  `build_index`, `index_status`, `query_index`, `start_batch_job`,
493
493
  `run_batch_job_slice`, `batch_job_status`, `list_batch_jobs`,
494
- `cancel_batch_job`, `resume_batch_job`, `review_timeline_markers`, and
495
- `cleanup_artifacts`.
494
+ `cancel_batch_job`, `resume_batch_job`, `review_timeline_markers`,
495
+ `cleanup_artifacts`, `db_status`, `db_ingest`, `get_panel_state`,
496
+ `set_panel_state`, `session_start_context`, `update_clip_field`,
497
+ `update_shot_field`, `get_field_history`, `revert_field`,
498
+ `list_corrections`, `deepen`, `commit_shot_vision`, and
499
+ `vision_pending_sweep`.
500
+
501
+ **Deep shot-level vision tier (v2.42.0+).** Opt-in, estimate-first. Two
502
+ entry points share one per-shot schema (Visual / Content / Production /
503
+ Editorial / Cuttability / description / confidence):
504
+ - `depth="deep"` on any analyze action extends the deferred host-vision
505
+ payload with `deep_shot_schema`; each `shot_descriptions` entry must carry
506
+ the field groups. The first deep run returns `confirmation_required` with
507
+ a token-cost estimate — re-call with `confirm_deep=true`. Caps still apply.
508
+ - `deepen(clip_id|clip_dir, shot_index?|shot_indices?)` runs the pass
509
+ post-hoc on an already-analyzed clip. First call returns the estimate +
510
+ `confirm_token`; re-call with the token to get the deferred payload, read
511
+ its `frame_paths`, and commit via
512
+ `commit_shot_vision(clip_id, shots=[{shot_index, ...groups...}],
513
+ vision_token)`. Deep fields land as `vision_deep_v1` provenance rows;
514
+ human corrections always survive. Shots with no sampled frames on disk get
515
+ 1–2 frames re-extracted via ffmpeg (read-only on source media).
516
+ `vision_pending_sweep(expire?, max_age_days?, reoffer?)` lists clips stuck
517
+ in `pending_host_analysis`; `reoffer=true` returns each clip's stored
518
+ deferred payload to finish the run, `expire=true` stamps them
519
+ `expired_host_analysis` so pendings never linger silently.
520
+
521
+ **DB-canonical analysis store (v2.41.0+).** The per-project SQLite DB
522
+ (`_soul/timeline_brain.sqlite`, schema v9+) is the source of truth for clip
523
+ analysis; `analysis.json` is a derived export written in lockstep. Analysis
524
+ runs write rows first (clips, shots, per-field subjective provenance,
525
+ transcript segments, sampled frames, QC observations) and then export the
526
+ JSON. Human corrections recorded via `update_clip_field` / `update_shot_field`
527
+ live as row-level provenance and always survive re-analysis. Readers
528
+ (panel API, exports) load DB-first and fall back to `analysis.json` for
529
+ reports that predate v9. `db_status` reports schema version + row counts;
530
+ `db_ingest` migrates an existing project's JSON reports (and
531
+ `corrections.json` sidecars) into the DB — run it once on older analysis
532
+ roots.
496
533
  The tool never installs
497
534
  dependencies and validates that outputs stay under
498
535
  `davinci-resolve-mcp-analysis` project roots rather than beside source media.
@@ -86,7 +86,10 @@ transcripts once the search index is built.
86
86
 
87
87
  The clip view shows the full summary, tags, star rating, editorial notes, and
88
88
  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.
89
+ Resolve, jump to the transcript, or click into any shot. `Deepen analysis`
90
+ copies a ready-made chat prompt that asks your MCP session to run the opt-in
91
+ deep shot pass (cost estimate first, then per-shot Visual / Content /
92
+ Editorial / Cuttability fields).
90
93
 
91
94
  ### Transcript
92
95
 
@@ -107,7 +110,9 @@ start", "After cut", "Flash frame", "Motion peak"). Subjective fields are
107
110
  editable inline; edits are kept with the clip's analysis and merged on top of
108
111
  future re-analysis so human notes survive fresh vision runs. `Open in Resolve`
109
112
  jumps straight to the clip in the source viewer with the shot's mark in/out
110
- set.
113
+ set. The field groups (Visual, Content, Production, Editorial, Cuttability)
114
+ are filled by the opt-in deep vision pass — `Deepen this shot` copies a chat
115
+ prompt that runs it for just this shot, estimate first.
111
116
 
112
117
  ### Media → History
113
118
 
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.40.0"
38
+ VERSION = "2.42.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.40.0",
3
+ "version": "2.42.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -8390,7 +8390,7 @@ HTML = r"""<!doctype html>
8390
8390
  <span class="meta">${formatDuration(card.duration_seconds)} · ${shots.length} shots</span>
8391
8391
  ${cls.primary_use ? `<span class="review-chip">${escapeHtml(cls.primary_use)}</span>` : ''}
8392
8392
  ${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>
8393
+ <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
8394
  `;
8395
8395
  $('reviewClipSummary').textContent = data.clip_summary || data.clip_summary_oneliner || '';
8396
8396
  $('reviewClipTags').innerHTML = tags.map(t => `<span class="review-chip">${escapeHtml(t)}</span>`).join('');
@@ -9440,6 +9440,7 @@ HTML = r"""<!doctype html>
9440
9440
  <span class="meta">${escapeHtml(shot.description || '')}</span>
9441
9441
  <div class="actions">
9442
9442
  <button class="secondary" id="reviewShotOpenInResolveBtn">Open in Resolve</button>
9443
+ <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
9444
  <button id="reviewShotEditToggleBtn" ${editing ? '' : 'class="secondary"'}>${editToggleLabel}</button>
9444
9445
  </div>
9445
9446
  `;
@@ -12450,6 +12451,25 @@ def _v2_load_analysis(clip_dir: str) -> Optional[Dict[str, Any]]:
12450
12451
  return None
12451
12452
 
12452
12453
 
12454
+ def _v2_load_analysis_db_first(project_root: str, clip_dir: str) -> Optional[Dict[str, Any]]:
12455
+ """C1 — DB-canonical reader with JSON fallback.
12456
+
12457
+ Falls back to analysis.json for reports that predate schema v9 and for
12458
+ job-linked report dirs whose rows live under another project's DB.
12459
+ """
12460
+ try:
12461
+ from src.utils import analysis_store
12462
+
12463
+ report = analysis_store.load_db_report(
12464
+ project_root, clip_dir=os.path.basename(clip_dir.rstrip("/\\"))
12465
+ )
12466
+ if report is not None:
12467
+ return report
12468
+ except Exception:
12469
+ pass
12470
+ return _v2_load_analysis(clip_dir)
12471
+
12472
+
12453
12473
  def _v2_clip_duration(report: Dict[str, Any]) -> Optional[float]:
12454
12474
  marker_plan = report.get("clip_analysis_markers") if isinstance(report.get("clip_analysis_markers"), dict) else {}
12455
12475
  duration = marker_plan.get("duration_seconds")
@@ -12530,7 +12550,7 @@ def list_analyzed_clips(project_root: str) -> Dict[str, Any]:
12530
12550
  """List analyzed clips for the bin grid. One row per analysis.json found."""
12531
12551
  rows: List[Dict[str, Any]] = []
12532
12552
  for slug, clip_dir in _v2_iter_analysis_dirs(project_root):
12533
- report = _v2_load_analysis(clip_dir)
12553
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12534
12554
  if report is None:
12535
12555
  continue
12536
12556
  rows.append(_v2_clip_summary_card(slug, clip_dir, report))
@@ -12561,7 +12581,7 @@ def get_analyzed_clip(project_root: str, clip_id: str) -> Dict[str, Any]:
12561
12581
  clip_dir = _v2_find_clip_dir(project_root, clip_id)
12562
12582
  if not clip_dir:
12563
12583
  return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
12564
- report = _v2_load_analysis(clip_dir)
12584
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12565
12585
  if report is None:
12566
12586
  return {"success": False, "error": "analysis.json unreadable"}
12567
12587
  visual = report.get("visual") if isinstance(report.get("visual"), dict) else {}
@@ -12598,7 +12618,7 @@ def get_analyzed_clip_shots(project_root: str, clip_id: str) -> Dict[str, Any]:
12598
12618
  clip_dir = _v2_find_clip_dir(project_root, clip_id)
12599
12619
  if not clip_dir:
12600
12620
  return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
12601
- report = _v2_load_analysis(clip_dir)
12621
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12602
12622
  if report is None:
12603
12623
  return {"success": False, "error": "analysis.json unreadable"}
12604
12624
  visual = report.get("visual") if isinstance(report.get("visual"), dict) else {}
@@ -12615,7 +12635,7 @@ def get_analyzed_clip_shot(project_root: str, clip_id: str, shot_index: int) ->
12615
12635
  clip_dir = _v2_find_clip_dir(project_root, clip_id)
12616
12636
  if not clip_dir:
12617
12637
  return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
12618
- report = _v2_load_analysis(clip_dir)
12638
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12619
12639
  if report is None:
12620
12640
  return {"success": False, "error": "analysis.json unreadable"}
12621
12641
  visual = report.get("visual") if isinstance(report.get("visual"), dict) else {}
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.40.0"
83
+ VERSION = "2.42.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.40.0"
14
+ VERSION = "2.42.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -11215,6 +11215,25 @@ def _v2_update_field(project_root: str, p: Dict[str, Any], *, entity_type: str)
11215
11215
  write_result = _v2_write_corrections(path, data)
11216
11216
  if not write_result.get("success"):
11217
11217
  return write_result
11218
+ # C1 — mirror the correction into the DB-canonical store (row-level
11219
+ # provenance). Best-effort: the sidecar remains authoritative for projects
11220
+ # whose DB predates v9 or lacks ingested rows.
11221
+ db_result: Dict[str, Any]
11222
+ try:
11223
+ from src.utils import analysis_store
11224
+ db_result = analysis_store.record_human_correction(
11225
+ project_root,
11226
+ clip_ref=clip_id or os.path.basename(os.path.dirname(path)),
11227
+ entity_type=entity_type,
11228
+ entity_uuid=entity_uuid,
11229
+ field_path=field_path,
11230
+ value=new_value,
11231
+ author=author,
11232
+ reason=reason,
11233
+ confidence=str(confidence) if confidence else None,
11234
+ )
11235
+ except Exception as exc: # noqa: BLE001 — DB mirror must not break corrections
11236
+ db_result = {"success": False, "error": f"{type(exc).__name__}: {exc}"}
11218
11237
  return {
11219
11238
  "success": True,
11220
11239
  "entity_type": entity_type,
@@ -11225,6 +11244,7 @@ def _v2_update_field(project_root: str, p: Dict[str, Any], *, entity_type: str)
11225
11244
  "author": author,
11226
11245
  "timestamp": now,
11227
11246
  "corrections_path": path,
11247
+ "db": db_result,
11228
11248
  }
11229
11249
 
11230
11250
 
@@ -11307,6 +11327,35 @@ def _v2_revert_field(project_root: str, p: Dict[str, Any]) -> Dict[str, Any]:
11307
11327
  write_result = _v2_write_corrections(path, data)
11308
11328
  if not write_result.get("success"):
11309
11329
  return write_result
11330
+ # C1 — mirror the revert into the DB-canonical store. A revert back to a
11331
+ # human value re-records it; a revert to machine-derived clears the human
11332
+ # row so the export overlay falls back to the blob value.
11333
+ db_result: Dict[str, Any]
11334
+ try:
11335
+ from src.utils import analysis_store
11336
+ clip_ref = clip_id or os.path.basename(os.path.dirname(path))
11337
+ if action_taken == "removed (back to machine-derived)" or revert_source != "human":
11338
+ db_result = analysis_store.clear_human_field(
11339
+ project_root,
11340
+ clip_ref=clip_ref,
11341
+ entity_type=entity_type,
11342
+ entity_uuid=entity_uuid,
11343
+ field_path=field_path,
11344
+ author=author,
11345
+ )
11346
+ else:
11347
+ db_result = analysis_store.record_human_correction(
11348
+ project_root,
11349
+ clip_ref=clip_ref,
11350
+ entity_type=entity_type,
11351
+ entity_uuid=entity_uuid,
11352
+ field_path=field_path,
11353
+ value=revert_to,
11354
+ author=author,
11355
+ reason=f"revert by {author}",
11356
+ )
11357
+ except Exception as exc: # noqa: BLE001 — DB mirror must not break corrections
11358
+ db_result = {"success": False, "error": f"{type(exc).__name__}: {exc}"}
11310
11359
  return {
11311
11360
  "success": True,
11312
11361
  "action": action_taken,
@@ -11314,6 +11363,7 @@ def _v2_revert_field(project_root: str, p: Dict[str, Any]) -> Dict[str, Any]:
11314
11363
  "reverted_value": revert_to,
11315
11364
  "timestamp": now,
11316
11365
  "corrections_path": path,
11366
+ "db": db_result,
11317
11367
  }
11318
11368
 
11319
11369
 
@@ -14955,6 +15005,25 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
14955
15005
  "cancel_batch_job",
14956
15006
  "resume_batch_job",
14957
15007
  "cleanup_artifacts",
15008
+ # V2 session/panel state + C4 corrections. These checks lived inside
15009
+ # this block since v2.24.0 but were missing from the membership set,
15010
+ # making them unreachable from MCP dispatch (the panel proxied to the
15011
+ # helpers directly, which masked it). Fixed as part of C1 (Phase A).
15012
+ "get_panel_state",
15013
+ "set_panel_state",
15014
+ "session_start_context",
15015
+ "update_shot_field",
15016
+ "update_clip_field",
15017
+ "get_field_history",
15018
+ "revert_field",
15019
+ "list_corrections",
15020
+ # C1 — DB-canonical analysis store.
15021
+ "db_status",
15022
+ "db_ingest",
15023
+ # Phase B — deep shot-level vision tier.
15024
+ "deepen",
15025
+ "commit_shot_vision",
15026
+ "vision_pending_sweep",
14958
15027
  }:
14959
15028
  root = resolve_media_analysis_output_root(
14960
15029
  project_name=project_name,
@@ -15007,6 +15076,49 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
15007
15076
  return _v2_revert_field(project_root, p)
15008
15077
  if action == "list_corrections":
15009
15078
  return _v2_list_corrections(project_root, p)
15079
+ # C1 — DB-canonical analysis store (Phase A).
15080
+ if action == "db_status":
15081
+ from src.utils import analysis_store
15082
+ return analysis_store.db_status(project_root)
15083
+ if action == "db_ingest":
15084
+ from src.utils import analysis_store
15085
+ return analysis_store.ingest_project(project_root)
15086
+ # Phase B — deep shot-level vision tier.
15087
+ if action == "deepen":
15088
+ from src.utils import deep_vision
15089
+ 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")
15090
+ if not clip_ref:
15091
+ return _err("deepen requires clip_id, clip_dir, or file_path")
15092
+ raw_indices = p.get("shot_indices") or p.get("shotIndices")
15093
+ if raw_indices is None and p.get("shot_index") is not None:
15094
+ raw_indices = [p.get("shot_index")]
15095
+ return deep_vision.deepen_clip(
15096
+ project_root,
15097
+ clip_ref=clip_ref,
15098
+ shot_indices=[int(i) for i in raw_indices] if raw_indices else None,
15099
+ confirm_token=p.get("confirm_token") or p.get("confirmToken"),
15100
+ job_id=p.get("job_id") or p.get("jobId"),
15101
+ )
15102
+ if action == "commit_shot_vision":
15103
+ from src.utils import deep_vision
15104
+ 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")
15105
+ if not clip_ref:
15106
+ return _err("commit_shot_vision requires clip_id, clip_dir, or file_path")
15107
+ return deep_vision.commit_shot_vision(
15108
+ project_root,
15109
+ shots=p.get("shots"),
15110
+ vision_token=p.get("vision_token") or p.get("visionToken"),
15111
+ clip_ref=clip_ref,
15112
+ author=p.get("author") or "host_chat",
15113
+ )
15114
+ if action == "vision_pending_sweep":
15115
+ from src.utils import deep_vision
15116
+ return deep_vision.vision_pending_sweep(
15117
+ project_root,
15118
+ expire=_media_analysis_bool(p.get("expire"), False),
15119
+ max_age_days=p.get("max_age_days") or p.get("maxAgeDays"),
15120
+ reoffer=_media_analysis_bool(p.get("reoffer"), False),
15121
+ )
15010
15122
  if action in {"build_index", "rebuild_index"}:
15011
15123
  return build_analysis_index(project_root, index_path=p.get("index_path") or p.get("indexPath"))
15012
15124
  if action == "index_status":
@@ -15408,6 +15520,26 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
15408
15520
  "cancel_batch_job",
15409
15521
  "resume_batch_job",
15410
15522
  "cleanup_artifacts",
15523
+ "get_panel_state",
15524
+ "set_panel_state",
15525
+ "session_start_context",
15526
+ "update_shot_field",
15527
+ "update_clip_field",
15528
+ "get_field_history",
15529
+ "revert_field",
15530
+ "list_corrections",
15531
+ "db_status",
15532
+ "db_ingest",
15533
+ "deepen",
15534
+ "commit_shot_vision",
15535
+ "vision_pending_sweep",
15536
+ "get_caps",
15537
+ "set_caps_preset",
15538
+ "get_usage",
15539
+ "coverage_report",
15540
+ "get_resolve_ai_usage",
15541
+ "get_ai_governance",
15542
+ "set_ai_governance",
15411
15543
  ])
15412
15544
 
15413
15545