davinci-resolve-mcp 2.60.0 → 2.61.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 +85 -0
- package/README.md +2 -2
- package/docs/SKILL.md +38 -2
- package/docs/install.md +8 -0
- package/docs/reference/api-limitations.md +9 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +179 -1
- package/src/utils/analysis_store.py +36 -1
- package/src/utils/api_truth.py +32 -0
- package/src/utils/strata.py +565 -0
- package/src/utils/strata_analyzers.py +684 -0
- package/src/utils/strata_faces.py +347 -0
- package/src/utils/strata_queries.py +643 -0
- package/src/utils/strata_story.py +311 -0
- package/src/utils/timeline_brain_db.py +121 -1
- package/src/utils/timeline_versioning.py +19 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,91 @@
|
|
|
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.61.0
|
|
6
|
+
|
|
7
|
+
Perception strata — a timecoded track model over every analyzed clip, plus the
|
|
8
|
+
query layer that turns it into editorial answers. Ten new `media_analysis`
|
|
9
|
+
actions, all local compute (ffmpeg + numpy; the face tier detects opencv +
|
|
10
|
+
mediapipe and degrades honestly when absent). Everything measures, compares,
|
|
11
|
+
flags, or aims — nothing decides; the cut is the editor's.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Schema v13: perception strata** — two generic track shapes instead of a
|
|
16
|
+
table per sensor: `events` (point/span occurrences: pause, breath,
|
|
17
|
+
hesitation, blink, beat, downbeat, …) and `curves` (float32 time series:
|
|
18
|
+
pitch, vocal_energy, speech_rate, motion_energy, face curves), plus two
|
|
19
|
+
promotions out of the report blob: `transcript_words` (per-word timestamps
|
|
20
|
+
as queryable rows) and `story_beats` (units of meaning over the transcript).
|
|
21
|
+
Machine re-runs replace their own rows; `source='human'` rows are append-only
|
|
22
|
+
and always win.
|
|
23
|
+
- **Local analyzers (`strata_run`)** — prosody (pitch/energy/speech-rate curves
|
|
24
|
+
+ pause/breath/hesitation events), beat grid (beat/downbeat + tempo), motion
|
|
25
|
+
energy (per-frame luma difference), and face strata (blink/gaze/expression
|
|
26
|
+
via opencv + mediapipe when installed). `strata_status` reports the per-clip
|
|
27
|
+
track inventory and what this machine can run; `backfill_words` promotes
|
|
28
|
+
word timestamps already sitting in stored report blobs — no re-analysis.
|
|
29
|
+
- **`take_diff(clip_a, clip_b, text?)`** — align two deliveries of the same
|
|
30
|
+
line on transcript words and diff pace, pauses, pitch, and energy. Deltas
|
|
31
|
+
only; it never picks a winner.
|
|
32
|
+
- **`cut_candidates(clip_id, time_seconds)`** — the joint solver: scores every
|
|
33
|
+
frame around an intended cut on the cut-point grammar (cut on the blink,
|
|
34
|
+
don't cut mid-word, don't bisect a breath, pauses are doors, land on the
|
|
35
|
+
beat, cut inside movement) with human-readable reasons per candidate and
|
|
36
|
+
honest reporting of missing tracks.
|
|
37
|
+
- **`strata_query`** — the strata as one queryable surface: windowed
|
|
38
|
+
cross-track bundles per clip, or project-wide word find with a joined
|
|
39
|
+
±context bundle per hit. **`timeline_strata`** projects clip strata through
|
|
40
|
+
a versioned timeline's recorded placements.
|
|
41
|
+
- **Story beats (host-LLM pass)** — `plan_story_beats` assembles a timecoded
|
|
42
|
+
digest + JSON schema (the server never calls an LLM), the host chat produces
|
|
43
|
+
the beats, `commit_story_beats` validates and persists them append-only;
|
|
44
|
+
`list_story_beats` reads them back.
|
|
45
|
+
- **Schema v14: timeline timebase snapshot** — `timeline_versions` now records
|
|
46
|
+
the timeline's fps and start frame at archive time, so `timeline_strata`
|
|
47
|
+
returns timeline-relative frames/seconds instead of assuming 24fps against
|
|
48
|
+
absolute (start-timecode-inclusive) snapshot frames.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
Pre-release review (multi-agent, 8 finder angles + adversarial verification)
|
|
53
|
+
caught and fixed before anything shipped:
|
|
54
|
+
|
|
55
|
+
- **Re-ingest cascade wipe** — the ingest `INSERT OR REPLACE` on the clips row
|
|
56
|
+
cascade-deleted every clip-child table under `PRAGMA foreign_keys=ON`. The
|
|
57
|
+
legacy children are rebuilt during ingest, so this was invisible until the
|
|
58
|
+
strata tables (which ingest does NOT rebuild — human annotations included)
|
|
59
|
+
landed on the same cascade. The clips row is now a true upsert and is never
|
|
60
|
+
deleted.
|
|
61
|
+
- **Words-less re-analysis wiped word rows** — `ingest_transcript_words`
|
|
62
|
+
deleted before validating; a re-analysis without transcription (or a mixed
|
|
63
|
+
report whose words lived only at the top level) silently destroyed
|
|
64
|
+
previously backfilled rows. Now parses first, preserves existing rows when
|
|
65
|
+
the incoming report has no words, and falls back per-segment to the
|
|
66
|
+
top-level words list.
|
|
67
|
+
- **Machine story-beat commits could replace human rows** — the content-hash
|
|
68
|
+
`beat_uuid` let `INSERT OR REPLACE` overwrite a human beat sharing the same
|
|
69
|
+
span+label and erased supersede history on identical recommits. Beat rows
|
|
70
|
+
now get random UUIDs and plain INSERTs; duplicate spans within one commit
|
|
71
|
+
are skipped and reported.
|
|
72
|
+
- **Dispatch hardening** — stringified numbers from LLM clients no longer
|
|
73
|
+
crash `strata_query` (TypeError in curve slicing) or silently redirect
|
|
74
|
+
`timeline_strata` to the latest version; `cut_candidates` rejects
|
|
75
|
+
non-positive fps instead of ZeroDivisionError; word matches escape SQL LIKE
|
|
76
|
+
metacharacters; clip bundles fetch every recorded track (gesture_boundary,
|
|
77
|
+
loudness, custom human tracks) instead of a hardcoded subset; analyzer
|
|
78
|
+
ffmpeg calls go through the stdio-safe `proc.safe_run`; a broken
|
|
79
|
+
mediapipe/protobuf install degrades to "face unavailable" instead of
|
|
80
|
+
crashing `strata_status`.
|
|
81
|
+
|
|
82
|
+
### Validation
|
|
83
|
+
|
|
84
|
+
- Full offline suite: 1,418 tests green (17 new regression tests covering the
|
|
85
|
+
fixes above). Static checks, drift guards, npm pack, and API-parity audit
|
|
86
|
+
all pass. No live Resolve behavior changed beyond two defensive getter reads
|
|
87
|
+
(`GetSetting("timelineFrameRate")`, `GetStartFrame()`) at archive time, both
|
|
88
|
+
try/except-guarded and unit-tested against mock timelines.
|
|
89
|
+
|
|
5
90
|
## What's New in v2.60.0
|
|
6
91
|
|
|
7
92
|
A community bug-fix bundle — three external contributions plus the three issues
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
A Model Context Protocol (MCP) server that lets AI assistants control DaVinci Resolve Studio through the official Scripting API. It provides full API coverage plus guarded workflow helpers for editing, media pool organization, render setup, review markers, grading, Fusion, Fairlight, project lifecycle tasks, extension authoring, and source-safe media analysis.
|
|
14
14
|
|
|
15
|
-
[](docs/guides/control-panel.md)
|
|
15
|
+
[](docs/guides/control-panel.md)
|
|
16
16
|
|
|
17
17
|
A local browser control panel ships with the server for inspecting Resolve state, running source-safe analysis, drilling into analyzed clips and shots, and editing analysis output inline. See the [Control Panel Guide](docs/guides/control-panel.md) for the full tour.
|
|
18
18
|
|
package/docs/SKILL.md
CHANGED
|
@@ -551,8 +551,10 @@ Key actions: `capabilities`, `install_guidance`, `resolve_output_root`, `plan`,
|
|
|
551
551
|
`list_corrections`, `deepen`, `commit_shot_vision`, `vision_pending_sweep`,
|
|
552
552
|
`build_embeddings`, `find_similar`, `detect_entities`, `commit_entities`,
|
|
553
553
|
`list_entities`, `prepare_bin_briefing`, `commit_bin_summary`,
|
|
554
|
-
`detect_shot_relationships`, `commit_shot_relationships`,
|
|
555
|
-
`list_shot_relationships
|
|
554
|
+
`detect_shot_relationships`, `commit_shot_relationships`,
|
|
555
|
+
`list_shot_relationships`, `strata_status`, `backfill_words`, `strata_run`,
|
|
556
|
+
`take_diff`, `cut_candidates`, `strata_query`, `timeline_strata`,
|
|
557
|
+
`plan_story_beats`, `commit_story_beats`, and `list_story_beats`.
|
|
556
558
|
|
|
557
559
|
**Cross-clip entities + bin briefing v2 (v2.44.0+).** Recurring
|
|
558
560
|
people/places/props across a project's media, found cheaply and confirmed
|
|
@@ -596,6 +598,40 @@ no editorial suggestions): `same_setup_as` / `alt_take_of` (symmetric) and
|
|
|
596
598
|
confirmed `alt_take_of` alternates over raw cosine (the rationale states
|
|
597
599
|
which basis ranked each alternate).
|
|
598
600
|
|
|
601
|
+
**Perception strata (v2.61.0+, schema v13/v14).** A timecoded track model over
|
|
602
|
+
each analyzed clip — events (pause/breath/hesitation/blink/beat/downbeat/…),
|
|
603
|
+
sampled curves (pitch/vocal_energy/speech_rate/motion_energy/face curves),
|
|
604
|
+
per-word transcript rows, and story beats. Local compute only (ffmpeg + numpy;
|
|
605
|
+
face tier needs opencv + mediapipe); machine re-runs replace their own rows,
|
|
606
|
+
human rows are append-only and always win. These measure and rank — they never
|
|
607
|
+
decide; the editor picks.
|
|
608
|
+
- `strata_status(clip_id?)` — project or per-clip track inventory plus what
|
|
609
|
+
this machine can run (`analyzer_capabilities`).
|
|
610
|
+
- `strata_run(clip_id, analyzers?)` — run prosody / beat_grid / motion_energy
|
|
611
|
+
/ face on one clip (default: whatever is available locally).
|
|
612
|
+
- `backfill_words()` — promote word timestamps already inside stored report
|
|
613
|
+
blobs into queryable `transcript_words` rows; idempotent, no re-analysis.
|
|
614
|
+
- `take_diff(clip_a, clip_b, text?)` — align two takes on transcript words
|
|
615
|
+
and diff their delivery (pace, pauses, pitch, energy). Deltas only, no
|
|
616
|
+
winner.
|
|
617
|
+
- `cut_candidates(clip_id, time_seconds, window_seconds?, fps?, limit?)` —
|
|
618
|
+
rank cut frames around an intended joint with human-readable reasons
|
|
619
|
+
(blink / word-gap / pause / breath / beat / motion); missing tracks are
|
|
620
|
+
reported, never treated as "no signal".
|
|
621
|
+
- `strata_query(clip_id?, start_seconds?, end_seconds?, match_word?, …)` —
|
|
622
|
+
one queryable surface: a windowed cross-track bundle for a clip, or a
|
|
623
|
+
project-wide word find with a joined ±context bundle per hit.
|
|
624
|
+
- `timeline_strata(timeline_name, timeline_version?, …)` — project clip
|
|
625
|
+
strata through a versioned timeline's recorded placements. Snapshot frames
|
|
626
|
+
are absolute record frames (start-timecode-inclusive); snapshots from
|
|
627
|
+
schema v14+ carry the timeline's fps/start frame so placements also get
|
|
628
|
+
timeline-relative frames and seconds.
|
|
629
|
+
- `plan_story_beats(clip_id)` / `commit_story_beats(clip_id, beats)` /
|
|
630
|
+
`list_story_beats(clip_id)` — host-LLM pass over the transcript digest
|
|
631
|
+
(the server never calls an LLM): beats are units of meaning with types
|
|
632
|
+
(topic/claim/revelation/emotional/anecdote/question/callback), links, and
|
|
633
|
+
supersede semantics.
|
|
634
|
+
|
|
599
635
|
**Embeddings + similarity (v2.43.0+).** Local-compute semantic search; no
|
|
600
636
|
vendor tokens, so nothing here touches the caps ledger. Backends are
|
|
601
637
|
detected, never installed (capabilities lists them with install guidance):
|
package/docs/install.md
CHANGED
|
@@ -64,6 +64,14 @@ The installer can automatically configure any of these clients:
|
|
|
64
64
|
|
|
65
65
|
You can configure multiple clients at once, or use `--clients manual` to get copy-paste config snippets.
|
|
66
66
|
|
|
67
|
+
For [Autohand Code](https://github.com/autohandai/code-cli/), register the managed launcher after setup:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
autohand mcp add davinci-resolve npx davinci-resolve-mcp server
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Autohand adds npx's `-y` flag automatically. Add `--scope project` after `mcp add` to keep the registration in the current workspace.
|
|
74
|
+
|
|
67
75
|
### Installer Options
|
|
68
76
|
|
|
69
77
|
```bash
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.0
|
|
14
14
|
|
|
15
|
-
**Totals:**
|
|
15
|
+
**Totals:** 19 missing capabilities, 11 bugs / unreliable behaviors.
|
|
16
16
|
|
|
17
17
|
The authoritative source is the runtime-queryable `api_truth` ledger
|
|
18
18
|
(`resolve_control api_truth "<query>"`); this document is generated from
|
|
@@ -134,6 +134,14 @@ equivalent, blocking full automation.
|
|
|
134
134
|
- **Workaround / current handling:** Position clips with AppendToTimeline clipInfo recordFrame, or perform insert/overwrite/replace edits in the Resolve UI.
|
|
135
135
|
- **Tags:** missing-method, timeline, edit
|
|
136
136
|
|
|
137
|
+
### Render in Place / bake a timeline clip to new media
|
|
138
|
+
|
|
139
|
+
- **Object:** `Timeline / TimelineItem / MediaPool`
|
|
140
|
+
- **Behavior:** There is no scripting method for the Edit-page clip context-menu action 'Render in Place', which bakes a clip (including its Fusion composition and effects) into a NEW rendered media file and drops that file back on the timeline at the same position, replacing the source clip. No Render*/Bake*/Freeze* method exists on Timeline, TimelineItem or MediaPool in the Resolve scripting API reference (BMD docs) or a dir() audit. NOTE the frequently-confused-but-distinct sibling: the render *cache* (a temporary, non-destructive cache of a clip's Color/Fusion output that reduces playback load WITHOUT creating a new media file) IS scriptable — TimelineItem.SetColorOutputCache / SetFusionOutputCache ('Render Cache Color/Fusion Output' menu actions) and Graph.SetNodeCacheMode. Render in Place is the permanent, media-producing bake; the render cache is the transient one.
|
|
141
|
+
- **Workaround / current handling:** If the goal is only to reduce playback/render load, use the render cache — exposed as timeline_item get_color_cache/set_color_cache/get_fusion_cache/set_fusion_cache and the Color-page graph node cache_mode (no new media, fully reversible). If you genuinely need a baked media file, render the clip's in/out range from the Deliver page (proj.AddRenderJob with MarkIn/MarkOut) and relink/append the result yourself, or run Render in Place from the Resolve UI. There is no one-call API equivalent. See issue #86.
|
|
142
|
+
- **Reference:** [issue #86](https://github.com/samuelgursky/davinci-resolve-mcp/issues/86)
|
|
143
|
+
- **Tags:** missing-method, timeline, render, cache, render-in-place, bake
|
|
144
|
+
|
|
137
145
|
### Smart Bins / Power Bins creation
|
|
138
146
|
|
|
139
147
|
- **Object:** `MediaPool`
|
package/install.py
CHANGED
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.
|
|
38
|
+
VERSION = "2.61.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
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.61.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.
|
|
14
|
+
VERSION = "2.61.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -16680,6 +16680,40 @@ def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None
|
|
|
16680
16680
|
# TOOL 15: media_analysis
|
|
16681
16681
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
16682
16682
|
|
|
16683
|
+
_STRATA_CLIP_REF_KEYS = (
|
|
16684
|
+
"clip_id", "clipId", "clip_uuid", "clipUuid",
|
|
16685
|
+
"clip_dir", "clipDir", "file_path", "filePath",
|
|
16686
|
+
)
|
|
16687
|
+
|
|
16688
|
+
|
|
16689
|
+
def _strata_clip_ref(p: Dict[str, Any]) -> Any:
|
|
16690
|
+
"""The clip ref from any accepted alias — one place, so aliases can't drift
|
|
16691
|
+
between action handlers."""
|
|
16692
|
+
for key in _STRATA_CLIP_REF_KEYS:
|
|
16693
|
+
value = p.get(key)
|
|
16694
|
+
if value:
|
|
16695
|
+
return value
|
|
16696
|
+
return None
|
|
16697
|
+
|
|
16698
|
+
|
|
16699
|
+
def _opt_number(value: Any) -> Optional[float]:
|
|
16700
|
+
"""Coerce int/float/numeric-string to float; anything else → None.
|
|
16701
|
+
|
|
16702
|
+
LLM clients routinely stringify numbers; params must not crash (or be
|
|
16703
|
+
silently dropped) because "12.5" arrived instead of 12.5.
|
|
16704
|
+
"""
|
|
16705
|
+
if isinstance(value, bool):
|
|
16706
|
+
return None
|
|
16707
|
+
if isinstance(value, (int, float)):
|
|
16708
|
+
return float(value)
|
|
16709
|
+
if isinstance(value, str):
|
|
16710
|
+
try:
|
|
16711
|
+
return float(value.strip())
|
|
16712
|
+
except ValueError:
|
|
16713
|
+
return None
|
|
16714
|
+
return None
|
|
16715
|
+
|
|
16716
|
+
|
|
16683
16717
|
@mcp.tool()
|
|
16684
16718
|
async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, ctx: Optional[Context] = None) -> Dict[str, Any]:
|
|
16685
16719
|
"""Project-scoped media analysis and guarded metadata publishing.
|
|
@@ -16745,6 +16779,16 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
16745
16779
|
cancel_batch_job(job_id) -> stop future slices
|
|
16746
16780
|
resume_batch_job(job_id) -> requeue a canceled or interrupted job
|
|
16747
16781
|
cleanup_artifacts(frames_only=true) -> remove generated frame artifacts only
|
|
16782
|
+
strata_status(clip_id?) -> perception-strata inventory (events/curves/words/story beats) for the project or one clip
|
|
16783
|
+
backfill_words(analysis_root?) -> promote word timestamps from stored report blobs into transcript_words rows
|
|
16784
|
+
strata_run(clip_id, analyzers?) -> run local strata analyzers (prosody | beat_grid | motion_energy | face) on one clip; default = whatever this machine can run
|
|
16785
|
+
take_diff(clip_a, clip_b, text?) -> align two takes on transcript words and diff their delivery (pace, pauses, pitch, energy) — measures only, never picks
|
|
16786
|
+
cut_candidates(clip_id, time_seconds, window_seconds?, fps?, limit?) -> rank cut frames around an intended joint with reasons (blink/pause/word-gap/breath/beat/motion) — aims, never decides
|
|
16787
|
+
strata_query(clip_id?, start_seconds?, end_seconds?, match_word?, context_seconds?, include_curve_values?, limit?) -> cross-track window bundle for a clip, or project-wide word find with joined context per hit
|
|
16788
|
+
timeline_strata(timeline_name, timeline_version?, record_start_frame?, record_end_frame?, fps?) -> project clip strata through a timeline's recorded placements (whole-clip bundles; frames are absolute record frames incl. start-timecode offset; fps defaults to the snapshot's stored value; source-offset mapping needs the live item)
|
|
16789
|
+
plan_story_beats(clip_id) -> timecoded transcript digest + prosody evidence + JSON schema for the host model to segment into story beats (no LLM call server-side)
|
|
16790
|
+
commit_story_beats(clip_id, beats, source_model?) -> validate + persist host-produced story beats; machine commits supersede machine rows only, human rows always win
|
|
16791
|
+
list_story_beats(clip_id) -> current (non-superseded) story beats for a clip
|
|
16748
16792
|
|
|
16749
16793
|
Analysis outputs stay under a davinci-resolve-mcp-analysis project root
|
|
16750
16794
|
and are validated so they are never written beside source media. Executed
|
|
@@ -17043,6 +17087,17 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
17043
17087
|
"detect_shot_relationships",
|
|
17044
17088
|
"commit_shot_relationships",
|
|
17045
17089
|
"list_shot_relationships",
|
|
17090
|
+
# Perception strata (schema v13) — timecoded track model.
|
|
17091
|
+
"strata_status",
|
|
17092
|
+
"backfill_words",
|
|
17093
|
+
"strata_run",
|
|
17094
|
+
"take_diff",
|
|
17095
|
+
"cut_candidates",
|
|
17096
|
+
"strata_query",
|
|
17097
|
+
"timeline_strata",
|
|
17098
|
+
"plan_story_beats",
|
|
17099
|
+
"commit_story_beats",
|
|
17100
|
+
"list_story_beats",
|
|
17046
17101
|
}:
|
|
17047
17102
|
root = resolve_media_analysis_output_root(
|
|
17048
17103
|
project_name=project_name,
|
|
@@ -17102,6 +17157,119 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
17102
17157
|
if action == "db_ingest":
|
|
17103
17158
|
from src.utils import analysis_store
|
|
17104
17159
|
return analysis_store.ingest_project(project_root)
|
|
17160
|
+
# Perception strata (schema v13).
|
|
17161
|
+
if action == "strata_status":
|
|
17162
|
+
from src.utils import strata, strata_analyzers
|
|
17163
|
+
clip_ref = _strata_clip_ref(p)
|
|
17164
|
+
status = strata.strata_status(project_root, clip_ref)
|
|
17165
|
+
if status.get("success") and clip_ref is None:
|
|
17166
|
+
status["analyzer_capabilities"] = strata_analyzers.capabilities()
|
|
17167
|
+
return status
|
|
17168
|
+
if action == "backfill_words":
|
|
17169
|
+
from src.utils import strata
|
|
17170
|
+
return strata.backfill_transcript_words(project_root)
|
|
17171
|
+
if action == "strata_run":
|
|
17172
|
+
from src.utils import strata_analyzers
|
|
17173
|
+
clip_ref = _strata_clip_ref(p)
|
|
17174
|
+
if not clip_ref:
|
|
17175
|
+
return _err("strata_run requires clip_id (or clip_uuid / clip_dir / file_path)")
|
|
17176
|
+
analyzers = p.get("analyzers") or p.get("analyzer")
|
|
17177
|
+
if isinstance(analyzers, str):
|
|
17178
|
+
analyzers = [analyzers]
|
|
17179
|
+
return strata_analyzers.run_analyzers(project_root, clip_ref, analyzers)
|
|
17180
|
+
if action == "take_diff":
|
|
17181
|
+
from src.utils import strata_queries
|
|
17182
|
+
clip_a = p.get("clip_a") or p.get("clipA") or p.get("take_a") or p.get("takeA")
|
|
17183
|
+
clip_b = p.get("clip_b") or p.get("clipB") or p.get("take_b") or p.get("takeB")
|
|
17184
|
+
if not clip_a or not clip_b:
|
|
17185
|
+
return _err("take_diff requires clip_a and clip_b")
|
|
17186
|
+
return strata_queries.take_diff(project_root, clip_a, clip_b, text=p.get("text") or p.get("line"))
|
|
17187
|
+
if action == "cut_candidates":
|
|
17188
|
+
from src.utils import strata_queries
|
|
17189
|
+
clip_ref = _strata_clip_ref(p)
|
|
17190
|
+
t = _opt_number(p.get("time_seconds", p.get("timeSeconds")))
|
|
17191
|
+
if not clip_ref or t is None:
|
|
17192
|
+
return _err("cut_candidates requires clip_id and a numeric time_seconds")
|
|
17193
|
+
fps_raw = p.get("fps")
|
|
17194
|
+
fps = _opt_number(fps_raw)
|
|
17195
|
+
if fps_raw is not None and fps is None:
|
|
17196
|
+
return _err(f"fps must be a number, got {fps_raw!r}")
|
|
17197
|
+
return strata_queries.cut_candidates(
|
|
17198
|
+
project_root,
|
|
17199
|
+
clip_ref,
|
|
17200
|
+
t,
|
|
17201
|
+
window_seconds=_opt_number(p.get("window_seconds", p.get("windowSeconds"))) or 0.35,
|
|
17202
|
+
fps=fps,
|
|
17203
|
+
limit=int(_opt_number(p.get("limit")) or 7),
|
|
17204
|
+
)
|
|
17205
|
+
if action == "strata_query":
|
|
17206
|
+
from src.utils import strata_queries
|
|
17207
|
+
start_raw = p.get("start_seconds", p.get("startSeconds"))
|
|
17208
|
+
end_raw = p.get("end_seconds", p.get("endSeconds"))
|
|
17209
|
+
start = _opt_number(start_raw)
|
|
17210
|
+
end = _opt_number(end_raw)
|
|
17211
|
+
if (start_raw is not None and start is None) or (end_raw is not None and end is None):
|
|
17212
|
+
return _err("start_seconds/end_seconds must be numbers")
|
|
17213
|
+
return strata_queries.strata_query(
|
|
17214
|
+
project_root,
|
|
17215
|
+
clip_ref=_strata_clip_ref(p),
|
|
17216
|
+
start_seconds=start,
|
|
17217
|
+
end_seconds=end,
|
|
17218
|
+
match_word=p.get("match_word") or p.get("matchWord") or p.get("word"),
|
|
17219
|
+
context_seconds=_opt_number(p.get("context_seconds", p.get("contextSeconds"))) or 2.0,
|
|
17220
|
+
include_curve_values=bool(p.get("include_curve_values", p.get("includeCurveValues", False))),
|
|
17221
|
+
limit=int(_opt_number(p.get("limit")) or 20),
|
|
17222
|
+
)
|
|
17223
|
+
if action == "timeline_strata":
|
|
17224
|
+
from src.utils import strata_queries
|
|
17225
|
+
timeline_name = p.get("timeline_name") or p.get("timelineName") or p.get("timeline")
|
|
17226
|
+
if not timeline_name:
|
|
17227
|
+
return _err("timeline_strata requires timeline_name")
|
|
17228
|
+
version_raw = p.get("timeline_version", p.get("timelineVersion"))
|
|
17229
|
+
version = _opt_number(version_raw)
|
|
17230
|
+
if version_raw is not None and version is None:
|
|
17231
|
+
return _err(f"timeline_version must be an integer, got {version_raw!r}")
|
|
17232
|
+
fps_raw = p.get("fps")
|
|
17233
|
+
fps = _opt_number(fps_raw)
|
|
17234
|
+
if fps_raw is not None and fps is None:
|
|
17235
|
+
return _err(f"fps must be a number, got {fps_raw!r}")
|
|
17236
|
+
start_frame = _opt_number(p.get("record_start_frame", p.get("recordStartFrame")))
|
|
17237
|
+
end_frame = _opt_number(p.get("record_end_frame", p.get("recordEndFrame")))
|
|
17238
|
+
return strata_queries.timeline_strata(
|
|
17239
|
+
project_root,
|
|
17240
|
+
str(timeline_name),
|
|
17241
|
+
timeline_version=int(version) if version is not None else None,
|
|
17242
|
+
record_start_frame=int(start_frame) if start_frame is not None else None,
|
|
17243
|
+
record_end_frame=int(end_frame) if end_frame is not None else None,
|
|
17244
|
+
fps=fps,
|
|
17245
|
+
include_curve_values=bool(p.get("include_curve_values", p.get("includeCurveValues", False))),
|
|
17246
|
+
)
|
|
17247
|
+
if action == "plan_story_beats":
|
|
17248
|
+
from src.utils import strata_story
|
|
17249
|
+
clip_ref = _strata_clip_ref(p)
|
|
17250
|
+
if not clip_ref:
|
|
17251
|
+
return _err("plan_story_beats requires clip_id")
|
|
17252
|
+
return strata_story.plan_story_beats(project_root, clip_ref)
|
|
17253
|
+
if action == "commit_story_beats":
|
|
17254
|
+
from src.utils import strata_story
|
|
17255
|
+
clip_ref = _strata_clip_ref(p)
|
|
17256
|
+
if not clip_ref:
|
|
17257
|
+
return _err("commit_story_beats requires clip_id")
|
|
17258
|
+
beats = p.get("beats")
|
|
17259
|
+
if beats is None:
|
|
17260
|
+
return _err("commit_story_beats requires beats")
|
|
17261
|
+
return strata_story.commit_story_beats(
|
|
17262
|
+
project_root,
|
|
17263
|
+
clip_ref,
|
|
17264
|
+
beats,
|
|
17265
|
+
source_model=p.get("source_model") or p.get("sourceModel"),
|
|
17266
|
+
)
|
|
17267
|
+
if action == "list_story_beats":
|
|
17268
|
+
from src.utils import strata_story
|
|
17269
|
+
clip_ref = _strata_clip_ref(p)
|
|
17270
|
+
if not clip_ref:
|
|
17271
|
+
return _err("list_story_beats requires clip_id")
|
|
17272
|
+
return strata_story.list_story_beats(project_root, clip_ref)
|
|
17105
17273
|
# Phase B — deep shot-level vision tier.
|
|
17106
17274
|
if action == "deepen":
|
|
17107
17275
|
from src.utils import deep_vision
|
|
@@ -17660,6 +17828,16 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
17660
17828
|
"detect_shot_relationships",
|
|
17661
17829
|
"commit_shot_relationships",
|
|
17662
17830
|
"list_shot_relationships",
|
|
17831
|
+
"strata_status",
|
|
17832
|
+
"backfill_words",
|
|
17833
|
+
"strata_run",
|
|
17834
|
+
"take_diff",
|
|
17835
|
+
"cut_candidates",
|
|
17836
|
+
"strata_query",
|
|
17837
|
+
"timeline_strata",
|
|
17838
|
+
"plan_story_beats",
|
|
17839
|
+
"commit_story_beats",
|
|
17840
|
+
"list_story_beats",
|
|
17663
17841
|
"get_caps",
|
|
17664
17842
|
"set_caps_preset",
|
|
17665
17843
|
"get_usage",
|
|
@@ -335,9 +335,15 @@ def ingest_report(
|
|
|
335
335
|
"SELECT created_at FROM clips WHERE clip_uuid = ?", (clip_uuid,)
|
|
336
336
|
).fetchone()
|
|
337
337
|
created_at = str(existing["created_at"]) if existing else now
|
|
338
|
+
# True upsert — never INSERT OR REPLACE: REPLACE deletes the existing
|
|
339
|
+
# row first, and with PRAGMA foreign_keys=ON that cascade-deletes every
|
|
340
|
+
# child table (events/curves/transcript_words/story_beats included),
|
|
341
|
+
# wiping strata rows — human annotations among them — on every
|
|
342
|
+
# re-ingest. The v13 strata tables are NOT rebuilt by ingest, so the
|
|
343
|
+
# clips row must be updated in place.
|
|
338
344
|
conn.execute(
|
|
339
345
|
"""
|
|
340
|
-
INSERT
|
|
346
|
+
INSERT INTO clips
|
|
341
347
|
(clip_uuid, clip_dir, resolve_clip_id, media_id, clip_name,
|
|
342
348
|
file_path, bin_path, duration_seconds, fps, resolution,
|
|
343
349
|
media_type, summary, overall_motion_level, cut_count,
|
|
@@ -345,6 +351,28 @@ def ingest_report(
|
|
|
345
351
|
analyzed_at, vision_status, vision_committed_at,
|
|
346
352
|
created_at, updated_at)
|
|
347
353
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
354
|
+
ON CONFLICT(clip_uuid) DO UPDATE SET
|
|
355
|
+
clip_dir=excluded.clip_dir,
|
|
356
|
+
resolve_clip_id=excluded.resolve_clip_id,
|
|
357
|
+
media_id=excluded.media_id,
|
|
358
|
+
clip_name=excluded.clip_name,
|
|
359
|
+
file_path=excluded.file_path,
|
|
360
|
+
bin_path=excluded.bin_path,
|
|
361
|
+
duration_seconds=excluded.duration_seconds,
|
|
362
|
+
fps=excluded.fps,
|
|
363
|
+
resolution=excluded.resolution,
|
|
364
|
+
media_type=excluded.media_type,
|
|
365
|
+
summary=excluded.summary,
|
|
366
|
+
overall_motion_level=excluded.overall_motion_level,
|
|
367
|
+
cut_count=excluded.cut_count,
|
|
368
|
+
shot_count=excluded.shot_count,
|
|
369
|
+
analysis_version=excluded.analysis_version,
|
|
370
|
+
depth=excluded.depth,
|
|
371
|
+
signature_hash=excluded.signature_hash,
|
|
372
|
+
analyzed_at=excluded.analyzed_at,
|
|
373
|
+
vision_status=excluded.vision_status,
|
|
374
|
+
vision_committed_at=excluded.vision_committed_at,
|
|
375
|
+
updated_at=excluded.updated_at
|
|
348
376
|
""",
|
|
349
377
|
(
|
|
350
378
|
clip_uuid,
|
|
@@ -473,6 +501,12 @@ def ingest_report(
|
|
|
473
501
|
),
|
|
474
502
|
)
|
|
475
503
|
|
|
504
|
+
# Word-level timestamps (v13): promote segments[*].words to rows so
|
|
505
|
+
# word-boundary queries never have to parse the report blob.
|
|
506
|
+
from src.utils import strata as _strata
|
|
507
|
+
|
|
508
|
+
words_written = _strata.ingest_transcript_words(conn, clip_uuid, transcription)
|
|
509
|
+
|
|
476
510
|
# Sampled frames. Shot mapping uses frame_indices_used when present,
|
|
477
511
|
# with a time-containment fallback (some commit paths don't record
|
|
478
512
|
# which frame indices fed which shot).
|
|
@@ -576,6 +610,7 @@ def ingest_report(
|
|
|
576
610
|
"shot_count": len(shot_entries),
|
|
577
611
|
"subjective_fields_written": subj_written,
|
|
578
612
|
"subjective_fields_preserved_human": subj_skipped_human,
|
|
613
|
+
"transcript_words_written": words_written,
|
|
579
614
|
}
|
|
580
615
|
|
|
581
616
|
|
package/src/utils/api_truth.py
CHANGED
|
@@ -355,6 +355,38 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
355
355
|
"tags": ["missing-method", "timeline", "edit"],
|
|
356
356
|
"submit": "missing",
|
|
357
357
|
},
|
|
358
|
+
{
|
|
359
|
+
"symbol": "Render in Place / bake a timeline clip to new media",
|
|
360
|
+
"object": "Timeline / TimelineItem / MediaPool",
|
|
361
|
+
"reality": "There is no scripting method for the Edit-page clip "
|
|
362
|
+
"context-menu action 'Render in Place', which bakes a clip "
|
|
363
|
+
"(including its Fusion composition and effects) into a NEW "
|
|
364
|
+
"rendered media file and drops that file back on the timeline "
|
|
365
|
+
"at the same position, replacing the source clip. No "
|
|
366
|
+
"Render*/Bake*/Freeze* method exists on Timeline, TimelineItem "
|
|
367
|
+
"or MediaPool in the Resolve scripting API reference (BMD docs) "
|
|
368
|
+
"or a dir() audit. NOTE the frequently-confused-but-distinct "
|
|
369
|
+
"sibling: the render *cache* (a temporary, non-destructive "
|
|
370
|
+
"cache of a clip's Color/Fusion output that reduces playback "
|
|
371
|
+
"load WITHOUT creating a new media file) IS scriptable — "
|
|
372
|
+
"TimelineItem.SetColorOutputCache / SetFusionOutputCache "
|
|
373
|
+
"('Render Cache Color/Fusion Output' menu actions) and "
|
|
374
|
+
"Graph.SetNodeCacheMode. Render in Place is the permanent, "
|
|
375
|
+
"media-producing bake; the render cache is the transient one.",
|
|
376
|
+
"recommended": "If the goal is only to reduce playback/render load, use "
|
|
377
|
+
"the render cache — exposed as timeline_item "
|
|
378
|
+
"get_color_cache/set_color_cache/get_fusion_cache/"
|
|
379
|
+
"set_fusion_cache and the Color-page graph node cache_mode "
|
|
380
|
+
"(no new media, fully reversible). If you genuinely need a "
|
|
381
|
+
"baked media file, render the clip's in/out range from the "
|
|
382
|
+
"Deliver page (proj.AddRenderJob with MarkIn/MarkOut) and "
|
|
383
|
+
"relink/append the result yourself, or run Render in Place "
|
|
384
|
+
"from the Resolve UI. There is no one-call API equivalent. "
|
|
385
|
+
"See issue #86.",
|
|
386
|
+
"tags": ["missing-method", "timeline", "render", "cache", "render-in-place", "bake"],
|
|
387
|
+
"submit": "missing",
|
|
388
|
+
"issue": 86,
|
|
389
|
+
},
|
|
358
390
|
{
|
|
359
391
|
"symbol": "Smart Bins / Power Bins creation",
|
|
360
392
|
"object": "MediaPool",
|