davinci-resolve-mcp 2.40.0 → 2.41.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
@@ -5199,6 +5214,15 @@ async def execute_plan_async(
5199
5214
  if vision_pending:
5200
5215
  analysis["vision_status"] = "pending_host_analysis"
5201
5216
  analysis["vision_token"] = vision.get("vision_token")
5217
+ # C1 — DB rows first (canonical), then the derived JSON export. The DB
5218
+ # lives under output_root (same root as clips/), not the caps root.
5219
+ db_ingest = _ingest_report_into_db(
5220
+ output_root,
5221
+ analysis,
5222
+ os.path.dirname(artifacts["analysis_json"]),
5223
+ )
5224
+ if not db_ingest.get("success"):
5225
+ clip_result["db_ingest_error"] = db_ingest.get("error")
5202
5226
  _write_json(artifacts["analysis_json"], analysis)
5203
5227
  cleanup_frames_requested = _coerce_bool(params.get("cleanup_frames"), default=False)
5204
5228
  if cleanup_frames_requested and not vision_pending and artifacts.get("frames_dir"):
@@ -5773,6 +5797,8 @@ def commit_visual_analysis(
5773
5797
  report.pop("vision_status", None)
5774
5798
  report.pop("vision_token", None)
5775
5799
  report["vision_committed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
5800
+ # C1 — DB rows first (canonical), then the derived JSON export.
5801
+ db_ingest = _ingest_report_into_db(root, report, clip_dir_path)
5776
5802
  _write_json(analysis_json_path, report)
5777
5803
 
5778
5804
  index_status_info: Dict[str, Any] = {}
@@ -5813,6 +5839,7 @@ def commit_visual_analysis(
5813
5839
  "index": index_status_info,
5814
5840
  "analysis_registry": registry_status,
5815
5841
  "corrections": corrections_metrics,
5842
+ "db_ingest": db_ingest,
5816
5843
  })
5817
5844
 
5818
5845
 
@@ -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(