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.
- package/CHANGELOG.md +40 -0
- package/README.md +1 -1
- package/docs/SKILL.md +18 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +23 -4
- package/src/granular/common.py +1 -1
- package/src/server.py +90 -1
- package/src/utils/analysis_store.py +924 -0
- package/src/utils/media_analysis.py +27 -0
- package/src/utils/timeline_brain_db.py +179 -1
|
@@ -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 =
|
|
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(
|