davinci-resolve-mcp 2.165.0 → 2.167.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 +55 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +9 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/README.md +7 -3
- package/resolve-advanced/server/aaf_probe.py +14 -2
- package/resolve-advanced/server/author-interchange.mjs +12 -7
- package/resolve-advanced/server/editorial.mjs +61 -44
- package/resolve-advanced/server/tools/editorial.mjs +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +21 -4
- package/src/utils/api_truth.py +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,61 @@
|
|
|
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.167.0 — E109: flat AAF sound slots keep their lanes; AAF audio cross-fades render
|
|
6
|
+
|
|
7
|
+
An Avid turnover carries dialog, music and effects as SEPARATE flat sound
|
|
8
|
+
MobSlots. The AAF walker labelled every flat slot `A` (only `NestedScope`
|
|
9
|
+
layers were numbered), so a dialog lane and a music bed collided on A1 and
|
|
10
|
+
the bridge refused the whole turnover ("audio events overlap on audio track
|
|
11
|
+
1 — one track cannot hold both"). The sound `Transition` between the dialog
|
|
12
|
+
clips parsed fine; the lane collapse was the block.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Flat AAF slots number per media kind in slot order** (`A`, `A2`, `A3` …
|
|
17
|
+
/ `V`, `V2` …). The first slot of a kind keeps the bare letter; `NestedScope`
|
|
18
|
+
layers keep their own layer numbering. Render-measured on 19.1.3.7: an AAF
|
|
19
|
+
with a dialog lane (−21 dBFS tone → 24f `MonoAudioDissolve` → −41 dBFS
|
|
20
|
+
tone) over a quiet music bed on its own slot conforms, imports, and renders
|
|
21
|
+
the cross-fade −26.0 → −29.3 → −32.5 → −37.5 → −41.6 dBFS across exactly its
|
|
22
|
+
window, with the second lane audibly present (−41.6 vs −47.1 for one lane).
|
|
23
|
+
- **Channel legs still place once.** Resolve's own AAF export writes one slot
|
|
24
|
+
per audio channel with identical legs; the bridge's merge (and
|
|
25
|
+
`verify_roundtrip`'s dedupe) now key on source/range rather than the lane,
|
|
26
|
+
so channel legs of one clip merge while a different bed on its own lane
|
|
27
|
+
never does.
|
|
28
|
+
|
|
29
|
+
## What's New in v2.166.0 — E108: XMEML audio cross-fades conform and render
|
|
30
|
+
|
|
31
|
+
The XMEML walker only looked at `<transitionitem>`s on VIDEO tracks, so an
|
|
32
|
+
audio cross-fade vanished at parse and never reached the bridge that
|
|
33
|
+
authors them (OTIO carried its audio Transition all along). Every audio
|
|
34
|
+
track also walked as `A`, collapsing multi-track audio onto one lane where
|
|
35
|
+
OTIO and AAF number `A`, `A2`, `A3` …
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **XMEML audio-track transitionitems attach** to the incoming audio event
|
|
40
|
+
exactly as video's do (one shared attach pass), and the bridge authors the
|
|
41
|
+
cross-fade. Render-measured on 19.1.3.7 against a control timeline: a 24f
|
|
42
|
+
`Cross Fade (+3dB)` from a −21 dBFS tone to a −41 dBFS tone ramps
|
|
43
|
+
−27.1 → −30.6 → −34.2 → −40.2 → −47.1 dBFS across the window in 0.25 s
|
|
44
|
+
RMS steps where the control steps hard at the cut.
|
|
45
|
+
- **XMEML audio tracks number like OTIO/AAF** (`A`, `A2`, …), so the
|
|
46
|
+
bridge places each lane on its own audio track instead of stacking
|
|
47
|
+
everything on A1.
|
|
48
|
+
- **`media_pool.capture_media_template` saves the current project before
|
|
49
|
+
switching to its scratch project.** `CreateProject` replaces the current
|
|
50
|
+
project, and an unsaved one is simply gone afterwards — the restore cannot
|
|
51
|
+
`LoadProject` a name that existed only in memory (measured: a freshly
|
|
52
|
+
created project with two imported timelines vanished, and Resolve fell
|
|
53
|
+
back to a transient "Untitled Project"). A failed save refuses the capture.
|
|
54
|
+
|
|
55
|
+
### Measured (filed in api-limitations)
|
|
56
|
+
|
|
57
|
+
- `ProjectManager.CreateProject` while an UNSAVED project is current
|
|
58
|
+
discards that project without error.
|
|
59
|
+
|
|
5
60
|
## What's New in v2.165.0 — E107: frame QC reads Resolve's own XML and samples clear of transitions
|
|
6
61
|
|
|
7
62
|
The lineage store's `ingest_xml` was measured against a verbatim
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.167.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:**
|
|
15
|
+
**Totals:** 38 missing capabilities, 50 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
|
|
@@ -309,6 +309,14 @@ equivalent, blocking full automation.
|
|
|
309
309
|
- **Workaround / current handling:** For round-trip QC prefer EXPORT_OTIO (carries audio, retimes as LinearTimeWarp/FreezeFrame, exact spans). When an EDL is the required deliverable, expect no audio (editorial.verify_roundtrip reports audioNotInExport) and resolve AX reels through the clip-name comments (parseEDL does). Use EXPORT_EDL — there is no EXPORT_CMX_3600 constant, and an unknown name reaches Export as a string that returns a bare False (timeline.export_timeline_checked now refuses it loudly).
|
|
310
310
|
- **Tags:** timeline, export, edl, audio, silent-failure
|
|
311
311
|
|
|
312
|
+
### ProjectManager.CreateProject (discards an unsaved current project)
|
|
313
|
+
|
|
314
|
+
- **Object:** `ProjectManager`
|
|
315
|
+
- **Signature:** `(projectName) -> Project`
|
|
316
|
+
- **Behavior:** CreateProject replaces the CURRENT project with the new one. If the current project was never saved it is simply gone — no dialog headless, no error, and a later LoadProject of its name fails because the name existed only in memory (measured on Studio 19.1.3.7, E108: a project created via CreateProject with two imported timelines vanished when a media-template capture created its scratch project; the restore landed on a transient "Untitled Project" that is not in the project list either).
|
|
317
|
+
- **Workaround / current handling:** SaveProject() before any CreateProject/LoadProject switch when the current project may be unsaved; the MCP's capture_media_template now does and refuses on a failed save.
|
|
318
|
+
- **Tags:** project, lifecycle, silent-failure, headless
|
|
319
|
+
|
|
312
320
|
### Timeline.Export EXPORT_FCP_7_XML (no pproTicksIn, -1 edges under transitions)
|
|
313
321
|
|
|
314
322
|
- **Object:** `Timeline`
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.167.0"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
|
@@ -98,7 +98,8 @@ Each dispatches on an `action`. Highlights:
|
|
|
98
98
|
- **`media`** — media front-end / AE: `ingest_verify` (hash seal/verify/dupes-by-hash), `media_inventory`
|
|
99
99
|
(fps/codec/colorspace/TC + card gaps), `sync` (TC picture↔sound + drift/MOS), `relink_manifest`,
|
|
100
100
|
`rename_plan` (refuses camera originals) / `reel_normalize`, `turnover_package`, `project_hygiene`.
|
|
101
|
-
- **`editorial`** — editorial integrity: `parse_interchange` (EDL/OTIO/XMEML natively
|
|
101
|
+
- **`editorial`** — editorial integrity: `parse_interchange` (EDL/OTIO/XMEML natively — XMEML audio
|
|
102
|
+
cross-fades and numbered audio lanes included — AAF via pyaaf2,
|
|
102
103
|
**.prproj via gunzip+XML** — pass the file PATH for the binary ones), `list_sequences` (one picker entry
|
|
103
104
|
point across xml/edl/otio/drt/drp/aaf/prproj), `convert_to_interchange` (author OTIO/EDL/DRT that Resolve
|
|
104
105
|
imports, from events or a parsed source — **the .prproj→Resolve conform bridge**, no Premiere needed;
|
|
@@ -200,8 +201,11 @@ plausible-looking short event list. Three limits are deliberate:
|
|
|
200
201
|
- **Effect-only layers produce no events.** A layer wrapping `ScopeReference` (subtitle burns, blends,
|
|
201
202
|
mattes) applies to what shows through from below and references no media of its own. OTIO materializes
|
|
202
203
|
such layers as tracks of gaps, so its track count can exceed the number of layers with media.
|
|
203
|
-
- **
|
|
204
|
-
|
|
204
|
+
- **Flat slots number per media kind in slot order** (`A`, `A2`, `A3` … / `V`, `V2` …): an Avid
|
|
205
|
+
turnover carries dialog, music and effects as SEPARATE flat sound MobSlots, and labelling them all
|
|
206
|
+
`A` stacked every bed onto one lane where the bridge refuses the overlap (measured, E109). The first
|
|
207
|
+
slot of a kind keeps the bare letter; `NestedScope` layers keep their own layer numbering. This is
|
|
208
|
+
what `editorial.mjs`'s `/^A\d*$/` audio-follows-video heuristic reads (`A`, `A2`, … all count as audio).
|
|
205
209
|
|
|
206
210
|
## Provenance & license
|
|
207
211
|
Vendored libraries are clean offline format-interop and deterministic compute code: no secrets, no
|
|
@@ -1248,7 +1248,8 @@ def _walk_slot(segment, *, prefix, fps, state):
|
|
|
1248
1248
|
|
|
1249
1249
|
A NestedScope slot is a multi-layer track: each layer gets its own numbered label
|
|
1250
1250
|
(V1..Vn / A1..An) and restarts at record 0, because layers are parallel, not
|
|
1251
|
-
sequential. A plain (single-layer) slot keeps the
|
|
1251
|
+
sequential. A plain (single-layer) slot keeps the label the caller numbered it
|
|
1252
|
+
with (first of its kind bare "V"/"A", then "A2", "A3" … per slot order — E109).
|
|
1252
1253
|
"""
|
|
1253
1254
|
if type(segment).__name__ == "NestedScope":
|
|
1254
1255
|
for n, layer in enumerate(_nested_layers(segment), start=1):
|
|
@@ -1456,6 +1457,13 @@ def probe(path):
|
|
|
1456
1457
|
# `idx` is monotonic across the WHOLE mob, every slot and every nested layer.
|
|
1457
1458
|
state = {"idx": 1, "events": [], "unhandled": {}, "effectsWithoutEvents": {}}
|
|
1458
1459
|
edit_fps = None
|
|
1460
|
+
# FLAT slots number per media kind in slot order (A, A2, A3 … / V, V2 …):
|
|
1461
|
+
# an Avid turnover carries dialog, music and effects as SEPARATE flat
|
|
1462
|
+
# sound MobSlots, and labelling them all "A" stacked every bed onto one
|
|
1463
|
+
# lane where the bridge refuses the overlap (measured, E109). The first
|
|
1464
|
+
# slot of a kind keeps the bare letter (compat with every reader that
|
|
1465
|
+
# matches /^A$/); NestedScope layers keep their own layer numbering.
|
|
1466
|
+
kind_ordinal = {"A": 0, "V": 0}
|
|
1459
1467
|
for slot in getattr(mob, "slots", []) or []:
|
|
1460
1468
|
seg = getattr(slot, "segment", None)
|
|
1461
1469
|
if seg is None:
|
|
@@ -1467,7 +1475,11 @@ def probe(path):
|
|
|
1467
1475
|
fps = _fps_from_edit_rate(getattr(slot, "edit_rate", None))
|
|
1468
1476
|
if edit_fps is None:
|
|
1469
1477
|
edit_fps = fps # picks the timecode slot to trust — see above
|
|
1470
|
-
|
|
1478
|
+
letter = _media_kind_to_track(slot)
|
|
1479
|
+
kind_ordinal[letter] += 1
|
|
1480
|
+
is_nested = getattr(seg, "class_name", "") == "NestedScope" or type(seg).__name__ == "NestedScope"
|
|
1481
|
+
label = letter if (kind_ordinal[letter] == 1 or is_nested) else f"{letter}{kind_ordinal[letter]}"
|
|
1482
|
+
_walk_slot(seg, prefix=label, fps=fps, state=state)
|
|
1471
1483
|
events = state["events"]
|
|
1472
1484
|
sequences.append(
|
|
1473
1485
|
{
|
|
@@ -254,15 +254,19 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
254
254
|
const isMarker = (t) => t === 'MARKER';
|
|
255
255
|
const vids = events.filter((e) => !isAudio(e.track) && !isMarker(e.track) && e.recIn != null && e.recOut != null);
|
|
256
256
|
// AAF exports one event per audio CHANNEL — a stereo/dual-mono clip arrives
|
|
257
|
-
// as identical
|
|
258
|
-
// audio event duplicated). Merge exact duplicates (same
|
|
259
|
-
// so they place once instead of refusing as a
|
|
257
|
+
// as identical legs, one per channel slot (measured on a Resolve 19 rich
|
|
258
|
+
// export: every audio event duplicated). Merge exact duplicates (same
|
|
259
|
+
// source/range/srcIn) so they place once instead of refusing as a
|
|
260
|
+
// same-track overlap. The key deliberately ignores the LANE: since E109
|
|
261
|
+
// flat slots number A, A2, A3 …, so channel legs of one clip arrive on
|
|
262
|
+
// different lanes — still one clip, placed once — while a different bed on
|
|
263
|
+
// its own lane (dialog A, music A2) keeps its lane and never merges.
|
|
260
264
|
const audsRaw = events.filter((e) => isAudio(e.track) && e.recIn != null && e.recOut != null);
|
|
261
265
|
const seenAud = new Set();
|
|
262
266
|
const auds = [];
|
|
263
267
|
let audioChannelLegsMerged = 0;
|
|
264
268
|
for (const e of audsRaw) {
|
|
265
|
-
const k = `${e.
|
|
269
|
+
const k = `${e.source}|${e.recIn}|${e.recOut}|${e.srcIn}|${e.speed ?? 100}|${e.reverse ? 1 : 0}`;
|
|
266
270
|
if (seenAud.has(k)) { audioChannelLegsMerged += 1; continue; }
|
|
267
271
|
seenAud.add(k);
|
|
268
272
|
auds.push(e);
|
|
@@ -976,12 +980,13 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
976
980
|
// AUDIO (E97): compared only when the INPUT declares audio events — a
|
|
977
981
|
// video-only turnover legitimately re-exports with audio (the A1
|
|
978
982
|
// convenience mirror), which is reported informationally, never failed.
|
|
979
|
-
// AAF channel legs dedupe (same
|
|
980
|
-
//
|
|
983
|
+
// AAF channel legs dedupe (the same source/range arrives once per channel
|
|
984
|
+
// slot — on numbered lanes since E109, so the key ignores the lane the way
|
|
985
|
+
// the bridge's placement merge does); BL/silence legs merge out like black.
|
|
981
986
|
const dedupeAud = (list) => {
|
|
982
987
|
const seen = new Set();
|
|
983
988
|
return list.filter((e) => {
|
|
984
|
-
const k = `${e.
|
|
989
|
+
const k = `${e.source}|${e.recIn}|${e.recOut}|${e.srcIn}`;
|
|
985
990
|
if (seen.has(k)) return false;
|
|
986
991
|
seen.add(k);
|
|
987
992
|
return true;
|
|
@@ -370,6 +370,55 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
};
|
|
373
|
+
// Attach a track's <transitionitem> siblings to its walked events. Shared
|
|
374
|
+
// by VIDEO and AUDIO tracks (E108): audio cross-fades used to be dropped at
|
|
375
|
+
// parse because only the video walk looked at transitionitems, so an XMEML
|
|
376
|
+
// audio cross-fade never reached the bridge that authors them.
|
|
377
|
+
const attachTransitions = (t, label, before) => {
|
|
378
|
+
// <transitionitem> siblings: attach each to the INCOMING clip event —
|
|
379
|
+
// the one whose record-in falls inside the transition's span. Resolve's
|
|
380
|
+
// OWN XMEML importer writes these as elements that render INERT on
|
|
381
|
+
// 19.1.3 (measured E66: dip AND plain cross dissolve both played the
|
|
382
|
+
// outgoing clip through the window, hard cut at the end), so routing
|
|
383
|
+
// the turnover through assemble_from_interchange authors transitions
|
|
384
|
+
// that actually render where the host importer's do not.
|
|
385
|
+
const titems = t.transitionitem ? (Array.isArray(t.transitionitem) ? t.transitionitem : [t.transitionitem]) : [];
|
|
386
|
+
for (const tr of titems) {
|
|
387
|
+
const s = Number(tr.start), e2 = Number(tr.end);
|
|
388
|
+
if (!Number.isFinite(s) || !Number.isFinite(e2)) continue;
|
|
389
|
+
const effectId = (tr.effect && (tr.effect.effectid || tr.effect.name)) || 'Cross Dissolve';
|
|
390
|
+
const isBLev = (ev) => /^(BL|BLACK)$/i.test(String(ev.source || '').trim());
|
|
391
|
+
const inSpan = (ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2;
|
|
392
|
+
// Prefer the PICTURE as the incoming: a Solid Color generatoritem in
|
|
393
|
+
// the same span is the fade's black side, not the clip fading in.
|
|
394
|
+
const incoming = events.slice(before).find((ev) => inSpan(ev) && !isBLev(ev)) || events.slice(before).find(inSpan);
|
|
395
|
+
// recStart carries the transitionitem's EXPLICIT record span start
|
|
396
|
+
// (sequence-relative) so the bridge reproduces the editor's actual
|
|
397
|
+
// alignment instead of assuming centered.
|
|
398
|
+
if (incoming) {
|
|
399
|
+
incoming.transition = { type: String(effectId), duration: e2 - s, recStart: s };
|
|
400
|
+
// No clip ENDS inside the span → nothing precedes: a fade-IN from
|
|
401
|
+
// black. Synthesize the zero-length BL leg (E91/E92) so the bridge
|
|
402
|
+
// authors a real black-to-picture dissolve instead of dropping it.
|
|
403
|
+
const outgoing = events.slice(before).find((ev) => ev.track === label && ev !== incoming && ev.recOut > s - 1 && ev.recOut <= e2);
|
|
404
|
+
if (!outgoing) {
|
|
405
|
+
events.push(evt({ index: idx++, track: label, source: 'BL', recIn: incoming.recIn, recOut: incoming.recIn, fps: seqRate }));
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
// No clip STARTS inside the span: a fade-OUT into black past the
|
|
409
|
+
// last clip. Attach the transition to a synthetic BL leg at the
|
|
410
|
+
// outgoing clip's end (the bridge grows it forward).
|
|
411
|
+
const outgoing = events.slice(before).find((ev) => ev.track === label && ev.recOut > s - 1 && ev.recOut <= e2);
|
|
412
|
+
if (outgoing) {
|
|
413
|
+
events.push(evt({
|
|
414
|
+
index: idx++, track: label, source: 'BL',
|
|
415
|
+
recIn: outgoing.recOut, recOut: outgoing.recOut,
|
|
416
|
+
transition: { type: String(effectId), duration: e2 - s, recStart: s }, fps: seqRate,
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
};
|
|
373
422
|
const seq = doc.xmeml && doc.xmeml.sequence;
|
|
374
423
|
const media = seq && seq.media;
|
|
375
424
|
if (media) {
|
|
@@ -396,52 +445,20 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
396
445
|
clipCursor = 0;
|
|
397
446
|
if (t.clipitem) walk(t.clipitem, label);
|
|
398
447
|
currentJunctions = [];
|
|
399
|
-
|
|
400
|
-
// the one whose record-in falls inside the transition's span. Resolve's
|
|
401
|
-
// OWN XMEML importer writes these as elements that render INERT on
|
|
402
|
-
// 19.1.3 (measured E66: dip AND plain cross dissolve both played the
|
|
403
|
-
// outgoing clip through the window, hard cut at the end), so routing
|
|
404
|
-
// the turnover through assemble_from_interchange authors transitions
|
|
405
|
-
// that actually render where the host importer's do not.
|
|
406
|
-
const titems = t.transitionitem ? (Array.isArray(t.transitionitem) ? t.transitionitem : [t.transitionitem]) : [];
|
|
407
|
-
for (const tr of titems) {
|
|
408
|
-
const s = Number(tr.start), e2 = Number(tr.end);
|
|
409
|
-
if (!Number.isFinite(s) || !Number.isFinite(e2)) continue;
|
|
410
|
-
const effectId = (tr.effect && (tr.effect.effectid || tr.effect.name)) || 'Cross Dissolve';
|
|
411
|
-
const isBLev = (ev) => /^(BL|BLACK)$/i.test(String(ev.source || '').trim());
|
|
412
|
-
const inSpan = (ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2;
|
|
413
|
-
// Prefer the PICTURE as the incoming: a Solid Color generatoritem in
|
|
414
|
-
// the same span is the fade's black side, not the clip fading in.
|
|
415
|
-
const incoming = events.slice(before).find((ev) => inSpan(ev) && !isBLev(ev)) || events.slice(before).find(inSpan);
|
|
416
|
-
// recStart carries the transitionitem's EXPLICIT record span start
|
|
417
|
-
// (sequence-relative) so the bridge reproduces the editor's actual
|
|
418
|
-
// alignment instead of assuming centered.
|
|
419
|
-
if (incoming) {
|
|
420
|
-
incoming.transition = { type: String(effectId), duration: e2 - s, recStart: s };
|
|
421
|
-
// No clip ENDS inside the span → nothing precedes: a fade-IN from
|
|
422
|
-
// black. Synthesize the zero-length BL leg (E91/E92) so the bridge
|
|
423
|
-
// authors a real black-to-picture dissolve instead of dropping it.
|
|
424
|
-
const outgoing = events.slice(before).find((ev) => ev.track === label && ev !== incoming && ev.recOut > s - 1 && ev.recOut <= e2);
|
|
425
|
-
if (!outgoing) {
|
|
426
|
-
events.push(evt({ index: idx++, track: label, source: 'BL', recIn: incoming.recIn, recOut: incoming.recIn, fps: seqRate }));
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
// No clip STARTS inside the span: a fade-OUT into black past the
|
|
430
|
-
// last clip. Attach the transition to a synthetic BL leg at the
|
|
431
|
-
// outgoing clip's end (the bridge grows it forward).
|
|
432
|
-
const outgoing = events.slice(before).find((ev) => ev.track === label && ev.recOut > s - 1 && ev.recOut <= e2);
|
|
433
|
-
if (outgoing) {
|
|
434
|
-
events.push(evt({
|
|
435
|
-
index: idx++, track: label, source: 'BL',
|
|
436
|
-
recIn: outgoing.recOut, recOut: outgoing.recOut,
|
|
437
|
-
transition: { type: String(effectId), duration: e2 - s, recStart: s }, fps: seqRate,
|
|
438
|
-
}));
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
448
|
+
attachTransitions(t, label, before);
|
|
442
449
|
});
|
|
450
|
+
// Audio tracks number like OTIO/AAF (A, A2, A3 …) so multi-track audio
|
|
451
|
+
// keeps its lanes instead of collapsing onto A1 (E108), and their
|
|
452
|
+
// transitionitems attach the same way video's do.
|
|
443
453
|
const atracks = media.audio && media.audio.track ? (Array.isArray(media.audio.track) ? media.audio.track : [media.audio.track]) : [];
|
|
444
|
-
|
|
454
|
+
atracks.forEach((t, ai) => {
|
|
455
|
+
const label = ai === 0 ? 'A' : `A${ai + 1}`;
|
|
456
|
+
const before = events.length;
|
|
457
|
+
clipCursor = 0;
|
|
458
|
+
if (t.clipitem) walk(t.clipitem, label);
|
|
459
|
+
currentJunctions = [];
|
|
460
|
+
attachTransitions(t, label, before);
|
|
461
|
+
});
|
|
445
462
|
}
|
|
446
463
|
return events;
|
|
447
464
|
}
|
|
@@ -86,7 +86,7 @@ const markerSchema = z.object({
|
|
|
86
86
|
export const editorialTool = {
|
|
87
87
|
name: 'editorial',
|
|
88
88
|
description:
|
|
89
|
-
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively — incl. Resolve-written FCP7 -1 junction edges + Solid Color generators, and CMX FROM/TO CLIP NAME comments over the generic AX reel — + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events incl. span-explicit transitions, BL fade legs, and freezes as zero-speed events (OTIO FreezeFrame, XMEML timeremap 0, PrProj in==out, AAF 0% motion effects); for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source; the EDL target writes CMX transition pairs incl. BL fades — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/trimmed/replaced/new/gone PLUS the junction diff: transition_added/transition_dropped/transition_changed with fade in/out or dissolve, outgoing/incoming, span and duration/type/pre-roll deltas — zero-length CMX carrier lines and the BL legs that carry fades fold into the junction diff instead of reading as gone/new sources; events pair by closest record position, consumed once, so a source cut twice at two speeds compares instance to instance; + timing flags incl. transition_dropped and dropped_split_audio on any A-track), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base; BL-aware — black legs need no source, fades no black-side handles, and a fade-out tail requirement lands on the picture source), marker_roundtrip (markers with provenance tags), verify_roundtrip (input events vs re-export events -> pass/mismatches + fitted per-source TC offsets + marker compare w/ markersNotInExport honesty flag; FADE-AWARE: BL/Solid-Color legs merge out as blackSegments and fade-window boundary reshapes are excused into fadeReshapedBoundaries instead of failing; RETIME-AWARE: speed/reverse compare pairwise — EXPORT_OTIO carries an authored Sm2TimeMap back as LinearTimeWarp (measured), so a flattened/lost retime fails as drift geometry alone cannot catch; AUDIO-AWARE: declared audio events compare (a video-only export such as EXPORT_EDL flags audioNotInExport instead of failing) (channel legs deduped, mismatches tagged trackType audio) while the mirrored-A1 export of a video-only turnover stays informational; the conform QC loop-closer). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
|
|
89
|
+
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively — incl. Resolve-written FCP7 -1 junction edges (paired in record order) + Solid Color generators, XMEML audio-track transitionitems as audio cross-fades on numbered lanes A/A2/…, and CMX FROM/TO CLIP NAME comments over the generic AX reel — + AAF via pyaaf2 (flat sound/picture slots number A/A2/… and V/V2/… in slot order so separate beds keep their lanes; NestedScope layers keep layer numbering) + PRPROJ via gunzip+XML → normalized events incl. span-explicit transitions, BL fade legs, and freezes as zero-speed events (OTIO FreezeFrame, XMEML timeremap 0, PrProj in==out, AAF 0% motion effects); for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source; the EDL target writes CMX transition pairs incl. BL fades — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/trimmed/replaced/new/gone PLUS the junction diff: transition_added/transition_dropped/transition_changed with fade in/out or dissolve, outgoing/incoming, span and duration/type/pre-roll deltas — zero-length CMX carrier lines and the BL legs that carry fades fold into the junction diff instead of reading as gone/new sources; events pair by closest record position, consumed once, so a source cut twice at two speeds compares instance to instance; + timing flags incl. transition_dropped and dropped_split_audio on any A-track), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base; BL-aware — black legs need no source, fades no black-side handles, and a fade-out tail requirement lands on the picture source), marker_roundtrip (markers with provenance tags), verify_roundtrip (input events vs re-export events -> pass/mismatches + fitted per-source TC offsets + marker compare w/ markersNotInExport honesty flag; FADE-AWARE: BL/Solid-Color legs merge out as blackSegments and fade-window boundary reshapes are excused into fadeReshapedBoundaries instead of failing; RETIME-AWARE: speed/reverse compare pairwise — EXPORT_OTIO carries an authored Sm2TimeMap back as LinearTimeWarp (measured), so a flattened/lost retime fails as drift geometry alone cannot catch; AUDIO-AWARE: declared audio events compare (a video-only export such as EXPORT_EDL flags audioNotInExport instead of failing) (channel legs deduped, mismatches tagged trackType audio) while the mirrored-A1 export of a video-only turnover stays informational; the conform QC loop-closer). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
|
|
90
90
|
async handler({ action, args }) {
|
|
91
91
|
if (action === 'parse_interchange') {
|
|
92
92
|
const p = parseSchema.parse(args);
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.167.0"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
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 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.167.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -13885,8 +13885,10 @@ def _capture_media_template(r, pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
13885
13885
|
<Element> + the timeline clips' <MediaRef> id. drt.assemble then
|
|
13886
13886
|
TRANSPLANTS the cached element, which renders identically to native.
|
|
13887
13887
|
|
|
13888
|
-
|
|
13889
|
-
switches back
|
|
13888
|
+
Saves the current project, switches to a scratch project during capture
|
|
13889
|
+
and switches back (an unsaved current project would otherwise be LOST —
|
|
13890
|
+
CreateProject replaces it and the restore cannot load an unsaved name);
|
|
13891
|
+
requires the media file to exist locally.
|
|
13890
13892
|
"""
|
|
13891
13893
|
import hashlib
|
|
13892
13894
|
import zipfile as _zipfile
|
|
@@ -13913,6 +13915,21 @@ def _capture_media_template(r, pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
13913
13915
|
except Exception:
|
|
13914
13916
|
previous = None
|
|
13915
13917
|
|
|
13918
|
+
# CreateProject(scratch) replaces the CURRENT project, and a current
|
|
13919
|
+
# project that was never saved is simply gone afterwards — the restore
|
|
13920
|
+
# below cannot LoadProject a name that exists only in memory (measured
|
|
13921
|
+
# 2026-09-01, E108: a freshly created project with two imported timelines
|
|
13922
|
+
# vanished; Resolve fell back to a transient "Untitled Project"). Save it
|
|
13923
|
+
# first so the switch-back lands on the caller's own project.
|
|
13924
|
+
if previous:
|
|
13925
|
+
try:
|
|
13926
|
+
if not pm.SaveProject():
|
|
13927
|
+
return _err(
|
|
13928
|
+
f"could not save the current project {previous!r} before the capture switches "
|
|
13929
|
+
"away from it — an unsaved project would be lost; save it (project_manager.save) and retry"
|
|
13930
|
+
)
|
|
13931
|
+
except Exception as exc: # pragma: no cover - API surprise
|
|
13932
|
+
return _err(f"SaveProject before capture raised: {exc}")
|
|
13916
13933
|
scratch = f"_mcp_media_tpl_{int(time.time())}"
|
|
13917
13934
|
proj = pm.CreateProject(scratch)
|
|
13918
13935
|
if not proj:
|
|
@@ -19829,7 +19846,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
19829
19846
|
capture_media_template(media_path) -> {cache_path, media_ref}
|
|
19830
19847
|
Captures the file's NATIVE Resolve media descriptors into
|
|
19831
19848
|
~/.config/davinci-resolve-mcp/media-templates/ via a disposable
|
|
19832
|
-
project (switches
|
|
19849
|
+
project (saves the current project, switches to a scratch project during capture and switches back — an unsaved current project would otherwise be lost).
|
|
19833
19850
|
Offline drt.assemble then transplants them — the only measured way
|
|
19834
19851
|
authored media actually RENDERS; synthesized descriptors import and
|
|
19835
19852
|
read back fine but render black or 'Full resolution media not found'.
|
package/src/utils/api_truth.py
CHANGED
|
@@ -2794,6 +2794,28 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2794
2794
|
"submit": "missing",
|
|
2795
2795
|
"mitigation": ["editorial.verify_roundtrip audioNotInExport", "timeline.export_timeline_checked"],
|
|
2796
2796
|
},
|
|
2797
|
+
{
|
|
2798
|
+
"symbol": "ProjectManager.CreateProject (discards an unsaved current project)",
|
|
2799
|
+
"object": "ProjectManager",
|
|
2800
|
+
"signature": "(projectName) -> Project",
|
|
2801
|
+
"reality": "CreateProject replaces the CURRENT project with the new "
|
|
2802
|
+
"one. If the current project was never saved it is simply "
|
|
2803
|
+
"gone — no dialog headless, no error, and a later "
|
|
2804
|
+
"LoadProject of its name fails because the name existed "
|
|
2805
|
+
"only in memory (measured on Studio 19.1.3.7, E108: a "
|
|
2806
|
+
"project created via CreateProject with two imported "
|
|
2807
|
+
"timelines vanished when a media-template capture created "
|
|
2808
|
+
"its scratch project; the restore landed on a transient "
|
|
2809
|
+
"\"Untitled Project\" that is not in the project list "
|
|
2810
|
+
"either).",
|
|
2811
|
+
"recommended": "SaveProject() before any CreateProject/LoadProject "
|
|
2812
|
+
"switch when the current project may be unsaved; the "
|
|
2813
|
+
"MCP's capture_media_template now does and refuses on a "
|
|
2814
|
+
"failed save.",
|
|
2815
|
+
"tags": ["project", "lifecycle", "silent-failure", "headless"],
|
|
2816
|
+
"submit": "missing",
|
|
2817
|
+
"mitigation": ["media_pool.capture_media_template saves first", "project_manager.save"],
|
|
2818
|
+
},
|
|
2797
2819
|
{
|
|
2798
2820
|
"symbol": "Timeline.Export EXPORT_FCP_7_XML (no pproTicksIn, -1 edges under transitions)",
|
|
2799
2821
|
"object": "Timeline",
|