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.
@@ -2230,6 +2230,21 @@ def _read_json(path: str) -> Dict[str, Any]:
2230
2230
  return json.load(f)
2231
2231
 
2232
2232
 
2233
+ def _ingest_report_into_db(project_root: str, report: Dict[str, Any], clip_dir: Optional[str]) -> Dict[str, Any]:
2234
+ """C1 — write a report into the DB-canonical store (rows in a transaction).
2235
+
2236
+ Best-effort by design: a DB failure must never break the analysis run,
2237
+ because the JSON export still lands on disk and every reader falls back
2238
+ to it. The failure is surfaced in the result for the caller to report.
2239
+ """
2240
+ try:
2241
+ from src.utils import analysis_store
2242
+
2243
+ return analysis_store.ingest_report(project_root, report, clip_dir=clip_dir)
2244
+ except Exception as exc: # noqa: BLE001 — DB trouble must not kill analysis
2245
+ return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
2246
+
2247
+
2233
2248
  def _fraction_to_float(value: Any) -> Optional[float]:
2234
2249
  if value in (None, "", "0/0"):
2235
2250
  return None
@@ -4250,7 +4265,7 @@ def build_host_chat_paths_payload(
4250
4265
  if refusal is not None:
4251
4266
  return refusal
4252
4267
 
4253
- return {
4268
+ payload: Dict[str, Any] = {
4254
4269
  "success": True,
4255
4270
  "status": "pending_host_analysis",
4256
4271
  "provider": HOST_CHAT_PATHS_PROVIDER,
@@ -4312,6 +4327,21 @@ def build_host_chat_paths_payload(
4312
4327
  "the clip's analysis directory; commit_vision finishes the run."
4313
4328
  ),
4314
4329
  }
4330
+ # Phase B — deep depth: each shot_descriptions entry must additionally
4331
+ # carry the per-shot field groups. The extra keys flow through
4332
+ # commit_vision → canonical blob → subjective_fields rows unchanged.
4333
+ if str(options.get("depth") or "").lower() == "deep":
4334
+ from src.utils import deep_vision as _deep_vision
4335
+
4336
+ payload["deep_shot_schema"] = _deep_vision.deep_shot_schema()
4337
+ payload["deep_schema_reference"] = _deep_vision.DEEP_SHOT_SCHEMA_REFERENCE
4338
+ payload["instructions"] += (
4339
+ " DEEP PASS: in addition to `description`, every shot_descriptions "
4340
+ "entry MUST include the field groups in `deep_shot_schema` (visual, "
4341
+ "content, production, editorial, cuttability, confidence), using the "
4342
+ "enum values verbatim and 'unknown'/null when frame evidence is thin."
4343
+ )
4344
+ return payload
4315
4345
 
4316
4346
 
4317
4347
  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]:
@@ -5081,6 +5111,9 @@ async def execute_plan_async(
5081
5111
  # Same for project_root — caps recording needs it to address the per-
5082
5112
  # project usage DB; falling back to the plan's output_root is fine.
5083
5113
  "project_root": params.get("project_root") or output_root,
5114
+ # Phase B — depth threads into the vision payload builder so deep runs
5115
+ # carry the per-shot field-group schema.
5116
+ "depth": plan.get("depth", DEFAULT_DEPTH),
5084
5117
  }
5085
5118
  keep_frame_artifacts_for_vision = vision_uses_chat_context(options, caps)
5086
5119
  depth = plan.get("depth", DEFAULT_DEPTH)
@@ -5099,6 +5132,37 @@ async def execute_plan_async(
5099
5132
  }
5100
5133
  _write_json(os.path.join(output_root, "capabilities.json"), caps)
5101
5134
 
5135
+ # Phase B — deep depth is opt-in with an explicit cost estimate first.
5136
+ # The per-shot field-group pass multiplies vision spend, so the first call
5137
+ # returns the estimate; re-call with confirm_deep=true to run. Caps still
5138
+ # apply downstream — confirmation does not bypass budgets.
5139
+ if (
5140
+ depth == "deep"
5141
+ and vision_uses_chat_context(options, caps)
5142
+ and not _coerce_bool(params.get("confirm_deep") or params.get("confirmDeep"), default=False)
5143
+ ):
5144
+ executing = [
5145
+ clip for clip in plan.get("clips", [])
5146
+ if not (clip.get("skip_execution") and (clip.get("existing_report") or {}).get("path"))
5147
+ ]
5148
+ estimated_frames = sum(int(c.get("analysis_keyframe_budget") or 0) for c in executing)
5149
+ return {
5150
+ "success": True,
5151
+ "status": "confirmation_required",
5152
+ "reason": "deep_depth_cost_estimate",
5153
+ "estimate": {
5154
+ "clip_count": len(executing),
5155
+ "estimated_frames": estimated_frames,
5156
+ "estimated_vision_tokens": estimated_frames * AVG_VISION_TOKENS_PER_FRAME,
5157
+ "tokens_per_frame_assumption": AVG_VISION_TOKENS_PER_FRAME,
5158
+ },
5159
+ "note": (
5160
+ "Deep analysis fills per-shot Visual/Content/Editorial field groups "
5161
+ "and costs vision tokens accordingly. Re-call the same analyze action "
5162
+ "with confirm_deep=true to proceed, or drop depth to 'standard'."
5163
+ ),
5164
+ }
5165
+
5102
5166
  for clip_plan in plan.get("clips", []):
5103
5167
  record = clip_plan["record"]
5104
5168
  artifacts = clip_plan["artifacts"]
@@ -5199,6 +5263,15 @@ async def execute_plan_async(
5199
5263
  if vision_pending:
5200
5264
  analysis["vision_status"] = "pending_host_analysis"
5201
5265
  analysis["vision_token"] = vision.get("vision_token")
5266
+ # C1 — DB rows first (canonical), then the derived JSON export. The DB
5267
+ # lives under output_root (same root as clips/), not the caps root.
5268
+ db_ingest = _ingest_report_into_db(
5269
+ output_root,
5270
+ analysis,
5271
+ os.path.dirname(artifacts["analysis_json"]),
5272
+ )
5273
+ if not db_ingest.get("success"):
5274
+ clip_result["db_ingest_error"] = db_ingest.get("error")
5202
5275
  _write_json(artifacts["analysis_json"], analysis)
5203
5276
  cleanup_frames_requested = _coerce_bool(params.get("cleanup_frames"), default=False)
5204
5277
  if cleanup_frames_requested and not vision_pending and artifacts.get("frames_dir"):
@@ -5773,6 +5846,8 @@ def commit_visual_analysis(
5773
5846
  report.pop("vision_status", None)
5774
5847
  report.pop("vision_token", None)
5775
5848
  report["vision_committed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
5849
+ # C1 — DB rows first (canonical), then the derived JSON export.
5850
+ db_ingest = _ingest_report_into_db(root, report, clip_dir_path)
5776
5851
  _write_json(analysis_json_path, report)
5777
5852
 
5778
5853
  index_status_info: Dict[str, Any] = {}
@@ -5813,6 +5888,7 @@ def commit_visual_analysis(
5813
5888
  "index": index_status_info,
5814
5889
  "analysis_registry": registry_status,
5815
5890
  "corrections": corrections_metrics,
5891
+ "db_ingest": db_ingest,
5816
5892
  })
5817
5893
 
5818
5894
 
@@ -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 = 8
32
+ SCHEMA_VERSION = 9
33
33
  DB_FILENAME = "timeline_brain.sqlite"
34
34
  SOUL_DIRNAME = "_soul"
35
35
 
@@ -509,6 +509,184 @@ def _migrate_v5_analysis_token_usage(conn: sqlite3.Connection) -> None:
509
509
  )
510
510
 
511
511
 
512
+ @register_migration(9)
513
+ def _migrate_v9_analysis_core(conn: sqlite3.Connection) -> None:
514
+ """C1 — DB-canonical clip analysis (Phase A of the analysis/edit-engine program).
515
+
516
+ The DB becomes the source of truth for clip analysis; analysis.json becomes
517
+ a derived export written in lockstep. Shape is hybrid: a canonical
518
+ full-report blob per clip (`analysis_reports`) plus normalized tables for
519
+ what downstream phases query (shots, subjective fields with per-field
520
+ provenance, transcript segments, sampled frames, QC observations).
521
+ Computed layers (technical/motion/cuts/audio) stay inside the blob — they
522
+ re-derive cleanly from source media and nothing queries them row-wise.
523
+ """
524
+ conn.executescript(
525
+ """
526
+ CREATE TABLE IF NOT EXISTS clips (
527
+ clip_uuid TEXT PRIMARY KEY, -- rename-stable canonical hash (12-hex)
528
+ clip_dir TEXT, -- report folder name under clips/
529
+ resolve_clip_id TEXT,
530
+ media_id TEXT,
531
+ clip_name TEXT,
532
+ file_path TEXT,
533
+ bin_path TEXT,
534
+ duration_seconds REAL,
535
+ fps REAL,
536
+ resolution TEXT,
537
+ media_type TEXT,
538
+ summary TEXT,
539
+ overall_motion_level TEXT,
540
+ cut_count INTEGER,
541
+ shot_count INTEGER,
542
+ analysis_version TEXT,
543
+ depth TEXT,
544
+ signature_hash TEXT,
545
+ analyzed_at TEXT,
546
+ vision_status TEXT,
547
+ vision_committed_at TEXT,
548
+ created_at TEXT NOT NULL,
549
+ updated_at TEXT NOT NULL
550
+ );
551
+
552
+ CREATE INDEX IF NOT EXISTS ix_clips_clip_name ON clips(clip_name);
553
+ CREATE INDEX IF NOT EXISTS ix_clips_file_path ON clips(file_path);
554
+
555
+ -- Every stable id a clip is known by (legacy hashes, clip_id, media_id,
556
+ -- folder hash, normalized file path) → clip_uuid. Lookup table only.
557
+ CREATE TABLE IF NOT EXISTS clip_aliases (
558
+ alias TEXT NOT NULL,
559
+ clip_uuid TEXT NOT NULL,
560
+ kind TEXT,
561
+ PRIMARY KEY (alias, clip_uuid)
562
+ );
563
+
564
+ CREATE INDEX IF NOT EXISTS ix_clip_aliases_clip ON clip_aliases(clip_uuid);
565
+
566
+ -- Canonical full analysis payload. analysis.json is exported FROM this.
567
+ CREATE TABLE IF NOT EXISTS analysis_reports (
568
+ clip_uuid TEXT PRIMARY KEY,
569
+ report_json TEXT NOT NULL,
570
+ signature_hash TEXT,
571
+ analyzed_at TEXT,
572
+ written_at TEXT NOT NULL,
573
+ FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
574
+ );
575
+
576
+ CREATE TABLE IF NOT EXISTS shots (
577
+ shot_uuid TEXT PRIMARY KEY, -- short_hash(clip_uuid + rounded time region)
578
+ clip_uuid TEXT NOT NULL,
579
+ shot_index INTEGER NOT NULL,
580
+ time_seconds_start REAL,
581
+ time_seconds_end REAL,
582
+ description TEXT,
583
+ qc_flags_json TEXT,
584
+ frame_indices_json TEXT,
585
+ extra_json TEXT, -- forward-compat: any other shot keys
586
+ created_at TEXT NOT NULL,
587
+ updated_at TEXT NOT NULL,
588
+ UNIQUE(clip_uuid, shot_index),
589
+ FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
590
+ );
591
+
592
+ CREATE INDEX IF NOT EXISTS ix_shots_clip ON shots(clip_uuid, shot_index);
593
+
594
+ -- Per-field provenance for subjective (vision/human) values.
595
+ -- Current value = the row with superseded_at IS NULL.
596
+ CREATE TABLE IF NOT EXISTS subjective_fields (
597
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
598
+ entity_type TEXT NOT NULL CHECK(entity_type IN ('clip', 'shot')),
599
+ entity_uuid TEXT NOT NULL,
600
+ field_path TEXT NOT NULL,
601
+ value_json TEXT NOT NULL,
602
+ confidence TEXT,
603
+ source TEXT NOT NULL, -- 'vision_v0.2' | 'human' | ...
604
+ source_model TEXT,
605
+ author TEXT NOT NULL,
606
+ timestamp TEXT NOT NULL,
607
+ superseded_at TEXT
608
+ );
609
+
610
+ CREATE INDEX IF NOT EXISTS ix_sf_entity
611
+ ON subjective_fields(entity_type, entity_uuid, field_path);
612
+ CREATE INDEX IF NOT EXISTS ix_sf_current
613
+ ON subjective_fields(entity_type, entity_uuid, field_path)
614
+ WHERE superseded_at IS NULL;
615
+
616
+ CREATE TABLE IF NOT EXISTS field_changelog (
617
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
618
+ entity_type TEXT NOT NULL,
619
+ entity_uuid TEXT NOT NULL,
620
+ field_path TEXT NOT NULL,
621
+ previous_value_json TEXT,
622
+ new_value_json TEXT NOT NULL,
623
+ previous_source TEXT,
624
+ new_source TEXT NOT NULL,
625
+ previous_author TEXT,
626
+ new_author TEXT NOT NULL,
627
+ change_reason TEXT,
628
+ timestamp TEXT NOT NULL
629
+ );
630
+
631
+ CREATE INDEX IF NOT EXISTS ix_changelog_entity
632
+ ON field_changelog(entity_type, entity_uuid, timestamp);
633
+
634
+ CREATE TABLE IF NOT EXISTS transcript_segments (
635
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
636
+ clip_uuid TEXT NOT NULL,
637
+ segment_index INTEGER NOT NULL,
638
+ start_seconds REAL,
639
+ end_seconds REAL,
640
+ text TEXT,
641
+ speaker_id TEXT,
642
+ UNIQUE(clip_uuid, segment_index),
643
+ FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
644
+ );
645
+
646
+ CREATE INDEX IF NOT EXISTS ix_transcript_segments_clip
647
+ ON transcript_segments(clip_uuid, start_seconds);
648
+
649
+ CREATE TABLE IF NOT EXISTS frames (
650
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
651
+ clip_uuid TEXT NOT NULL,
652
+ shot_uuid TEXT,
653
+ frame_index INTEGER NOT NULL,
654
+ time_seconds REAL,
655
+ frame_path TEXT,
656
+ selection_reason TEXT,
657
+ motion_peak INTEGER NOT NULL DEFAULT 0,
658
+ UNIQUE(clip_uuid, frame_index),
659
+ FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
660
+ );
661
+
662
+ CREATE INDEX IF NOT EXISTS ix_frames_clip ON frames(clip_uuid, frame_index);
663
+ CREATE INDEX IF NOT EXISTS ix_frames_shot ON frames(shot_uuid);
664
+
665
+ CREATE TABLE IF NOT EXISTS qc_observations (
666
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
667
+ clip_uuid TEXT NOT NULL,
668
+ shot_uuid TEXT,
669
+ observation_type TEXT NOT NULL,
670
+ severity TEXT NOT NULL DEFAULT 'info',
671
+ message TEXT NOT NULL,
672
+ related_shot_indices_json TEXT,
673
+ confidence TEXT,
674
+ source TEXT NOT NULL,
675
+ resolved INTEGER NOT NULL DEFAULT 0,
676
+ resolved_by TEXT,
677
+ resolved_at TEXT,
678
+ resolution_note TEXT,
679
+ created_at TEXT NOT NULL,
680
+ FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
681
+ );
682
+
683
+ CREATE INDEX IF NOT EXISTS ix_qc_clip ON qc_observations(clip_uuid);
684
+ CREATE INDEX IF NOT EXISTS ix_qc_unresolved
685
+ ON qc_observations(clip_uuid, resolved) WHERE resolved = 0;
686
+ """
687
+ )
688
+
689
+
512
690
  def latest_version(conn: sqlite3.Connection, timeline_name: str) -> Optional[int]:
513
691
  """Return the highest archived version number for `timeline_name`, or None."""
514
692
  row = conn.execute(