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 +68 -0
- package/README.md +1 -1
- package/docs/SKILL.md +39 -2
- package/docs/guides/control-panel.md +7 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +25 -5
- package/src/granular/common.py +1 -1
- package/src/server.py +133 -1
- package/src/utils/analysis_store.py +927 -0
- package/src/utils/deep_vision.py +643 -0
- package/src/utils/media_analysis.py +77 -1
- 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
|
|
@@ -4250,7 +4265,7 @@ def build_host_chat_paths_payload(
|
|
|
4250
4265
|
if refusal is not None:
|
|
4251
4266
|
return refusal
|
|
4252
4267
|
|
|
4253
|
-
|
|
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 =
|
|
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(
|