davinci-resolve-mcp 2.170.0 → 2.172.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 +48 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +3 -3
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +12 -1
- package/resolve-advanced/server/editorial.mjs +18 -8
- package/resolve-advanced/server/tools/editorial.mjs +4 -2
- package/src/granular/common.py +1 -1
- package/src/server.py +42 -24
- package/src/utils/api_truth.py +20 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
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.172.0 — E117: colour QC knows which writers are colour-blind
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`verify_roundtrip` no longer fails a colour compare against a re-export
|
|
10
|
+
that cannot carry colour.** Measured on 19.1.3.7: Resolve's OTIO writer
|
|
11
|
+
emits a Solid Color as a `Clip` with a null `media_reference` and empty
|
|
12
|
+
`Resolve_OTIO` metadata — no colour anywhere (its FCP7 XML writer echoes
|
|
13
|
+
the colour as `input_1`). Pass `exportedFormat` (otio|edl|xml|drt): a
|
|
14
|
+
colour-blind export reports `generatorColourNotInExport` (like
|
|
15
|
+
`markersNotInExport`) instead of `generator-colour` failures; an XML
|
|
16
|
+
export keeps the strict compare. Without the format the compare stays
|
|
17
|
+
strict. Resolve's OTIO export is a permanent fixture.
|
|
18
|
+
|
|
19
|
+
### Measured (filed in api-limitations)
|
|
20
|
+
|
|
21
|
+
- `EXPORT_OTIO` writes generators as media-less clips named after the
|
|
22
|
+
generator with no parameters; `Cross Dissolve` transitions carry a
|
|
23
|
+
`transitionCustomCurvesKeyframes` 0→1 curve in `Resolve_OTIO` metadata.
|
|
24
|
+
|
|
25
|
+
## What's New in v2.171.0 — E114: audio-lane `-1` edges take their own lane's junctions
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **XMEML audio cross-fades lost 12 source frames on the incoming clip.**
|
|
30
|
+
Resolve's FCP7 writer emits an audio cross-fade as a transitionitem on the
|
|
31
|
+
audio track with `-1` clip edges exactly like video (measured on 19.1.3.7
|
|
32
|
+
from the E109 AAF conform; its OTIO writer emits the same cross-fade as a
|
|
33
|
+
`Custom_Transition` with 12/12 offsets). The E108 audio walk attached the
|
|
34
|
+
transition but never computed that lane's junction list, so the incoming
|
|
35
|
+
clip's `<in>` (the source at the OVERLAP start) lost its junction offset and
|
|
36
|
+
`verify_roundtrip` failed the AAF → conform → import → XML loop with a
|
|
37
|
+
12-frame audio `source-frames` drift. Each lane now resolves against its
|
|
38
|
+
own transitionitems; the loop verifies `pass: true` through both writers.
|
|
39
|
+
- **v2.170.0's `kind` classifier was wrong for generators and subtitles.**
|
|
40
|
+
Measured on 19.1.3.7 (E115): a Solid Color generator and a subtitle item
|
|
41
|
+
return no MediaPoolItem and `None` from GetProperty() — exactly like a
|
|
42
|
+
transition — so the "no media, empty properties" rule labelled both
|
|
43
|
+
`transition`. The discriminator is now GEOMETRY: a transition straddles a
|
|
44
|
+
cut (one neighbour ends inside its span, another starts inside it), a
|
|
45
|
+
generator owns its span, subtitle tracks report `subtitle`, and known
|
|
46
|
+
transition names short-circuit. Verified against Resolve's own enumeration
|
|
47
|
+
of the E107 fades timeline (generator, dissolve, clip, dissolve, clip,
|
|
48
|
+
dissolve, generator).
|
|
49
|
+
- **v2.170.0 also shipped with two red Python tests** — an item-shape
|
|
50
|
+
assertion in the `get_items` selector test that did not expect the new
|
|
51
|
+
`kind` field. The expectation is updated.
|
|
52
|
+
|
|
5
53
|
## What's New in v2.170.0 — E113: `get_items` knows a transition from a clip
|
|
6
54
|
|
|
7
55
|
### Added
|
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.172.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -89,7 +89,7 @@ equivalent, blocking full automation.
|
|
|
89
89
|
### Transition create / copy / clone
|
|
90
90
|
|
|
91
91
|
- **Object:** `Timeline / TimelineItem`
|
|
92
|
-
- **Behavior:** There is no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING: creation, cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty
|
|
92
|
+
- **Behavior:** There is no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING: creation, cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — BUT a Solid Color generator AND a subtitle item read the same way (GetProperty() None, no MediaPoolItem; measured E115), so that pair only separates clips from non-clips. What separates a transition from a generator is GEOMETRY: a transition straddles a cut (one neighbour ends inside its span, another starts inside it) while a generator owns its span. timeline.get_items reports `kind` on that basis.
|
|
93
93
|
- **Workaround / current handling:** Automated QC of existing transitions IS possible and is the main practical need — enumerate GetItemListInTrack, treat any item whose GetProperty() is empty and whose GetMediaPoolItem() is None as a transition, and read its name, start and duration. Removal is scriptable via Timeline.DeleteClips. To CREATE one, either apply it in the Resolve UI, or author it offline and import: the advanced server's drp place_transition writes a cross dissolve at an abutting cut ({track, atFrame, durationFrames}) and it round-trips into Resolve 21.0.4.5 reading back at the expected centered range.
|
|
94
94
|
- **Tags:** missing-method, timeline, transition
|
|
95
95
|
|
|
@@ -305,7 +305,7 @@ equivalent, blocking full automation.
|
|
|
305
305
|
|
|
306
306
|
- **Object:** `Timeline`
|
|
307
307
|
- **Signature:** `(filePath, EXPORT_EDL, EXPORT_NONE) -> bool`
|
|
308
|
-
- **Behavior:** Resolve's CMX EDL writer (measured on Studio 19.1.3.7, E105) emits VIDEO events only — audio legs never appear; names every file source by the generic reel AX and carries the real names in `* FROM CLIP NAME:` / `* TO CLIP NAME:` comments; writes black legs as reel BL; places dissolve junctions at the CMX start-at-cut position (the overlap start, not the centered junction the timeline holds); and WRITES BL fades that its own EDL importer then drops. The FCP7 XML writer, by contrast, carries audio, writes transition-adjacent clip edges as -1 (the junction), and emits `speed` followed by `variablespeed` 0 in the same timeremap effect.
|
|
308
|
+
- **Behavior:** Resolve's CMX EDL writer (measured on Studio 19.1.3.7, E105) emits VIDEO events only — audio legs never appear; names every file source by the generic reel AX and carries the real names in `* FROM CLIP NAME:` / `* TO CLIP NAME:` comments; writes black legs as reel BL; places dissolve junctions at the CMX start-at-cut position (the overlap start, not the centered junction the timeline holds); and WRITES BL fades that its own EDL importer then drops. The FCP7 XML writer, by contrast, carries audio, writes transition-adjacent clip edges as -1 (the junction), and emits `speed` followed by `variablespeed` 0 in the same timeremap effect. AUDIO cross-fades are written the same way — a transitionitem on the audio track between -1-edged clipitems (measured E114); the OTIO writer emits them as a `Custom_Transition` with symmetric offsets on the Audio track.
|
|
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
|
|
|
@@ -313,7 +313,7 @@ equivalent, blocking full automation.
|
|
|
313
313
|
|
|
314
314
|
- **Object:** `MediaPool`
|
|
315
315
|
- **Signature:** `(filePath.xml) -> Timeline`
|
|
316
|
-
- **Behavior:** Resolve's FCP7 XML importer HONOURS a generatoritem's `fillcolor` parameter (measured on Studio 19.1.3.7, E110): a Premiere-shaped Color Matte (effectid Color, category Matte) and a Solid Color generatoritem, both with <red>/<green>/<blue>/<alpha> 0..255 values, imported as Solid Color items and rendered Y81 U90 V240 (red) and Y41 U240 V110 (blue) — exact BT.601 limited-range values for a 640x360 timeline. EXPORT_FCP_7_XML writes the fillcolor back (same 0..255 channels). EXPORT_DRT carries the colour as a 55-byte <EffectFiltersBA> on the Sm2TiGenerator: 8-byte header (version 2, length 47), a fixed 20-byte prefix, a flag byte, then big-endian uint16 A R G B (0xffff = full) plus a pad word, then a second, black colour record; only the ARGB words differed between the red and blue captures. The default generator has an EMPTY EffectFiltersBA. The same importer does NOT carry a TRANSITION's parameters: a `Dip to Color Dissolve` transitionitem with a white and with a red `color` parameter imported as `Dip To Color Dissolve` elements whose 37-byte EffectFiltersBA were byte-identical (defaults), and the transition rendered inert (luma flat through its window, the E66 law) — the dip colour stays GUI-only on 19.1.3.7 (measured, E111). The writer emits a Solid Color's colour as the FxPlug parameter `input_1` (effectid FxPlugWrapper:C18E8B62_…), not `fillcolor` — an authored EffectFiltersBA colour came back exactly (white, 128/64/191; E112).
|
|
316
|
+
- **Behavior:** Resolve's FCP7 XML importer HONOURS a generatoritem's `fillcolor` parameter (measured on Studio 19.1.3.7, E110): a Premiere-shaped Color Matte (effectid Color, category Matte) and a Solid Color generatoritem, both with <red>/<green>/<blue>/<alpha> 0..255 values, imported as Solid Color items and rendered Y81 U90 V240 (red) and Y41 U240 V110 (blue) — exact BT.601 limited-range values for a 640x360 timeline. EXPORT_FCP_7_XML writes the fillcolor back (same 0..255 channels). EXPORT_DRT carries the colour as a 55-byte <EffectFiltersBA> on the Sm2TiGenerator: 8-byte header (version 2, length 47), a fixed 20-byte prefix, a flag byte, then big-endian uint16 A R G B (0xffff = full) plus a pad word, then a second, black colour record; only the ARGB words differed between the red and blue captures. The default generator has an EMPTY EffectFiltersBA. The same importer does NOT carry a TRANSITION's parameters: a `Dip to Color Dissolve` transitionitem with a white and with a red `color` parameter imported as `Dip To Color Dissolve` elements whose 37-byte EffectFiltersBA were byte-identical (defaults), and the transition rendered inert (luma flat through its window, the E66 law) — the dip colour stays GUI-only on 19.1.3.7 (measured, E111). The writer emits a Solid Color's colour as the FxPlug parameter `input_1` (effectid FxPlugWrapper:C18E8B62_…), not `fillcolor` — an authored EffectFiltersBA colour came back exactly (white, 128/64/191; E112). The OTIO writer CANNOT: EXPORT_OTIO emits a Solid Color as a Clip.2 named 'Solid Color' with a NULL media_reference, metadata {"Resolve_OTIO": {}} and a parameterless Transform effect — no colour anywhere (measured E117) — so only an XML re-export witnesses a generator colour (editorial.verify_roundtrip exportedFormat → generatorColourNotInExport).
|
|
317
317
|
- **Workaround / current handling:** Author fade-to-white / colour mattes by placing a Solid Color generator with that blob (drp-format placeGenerator `color`, drt.assemble elements[].color) — or carry an XMEML generatoritem fillcolor through editorial.parse_interchange; the bridge authors the coloured leg.
|
|
318
318
|
- **Tags:** xml, import, generator, colour, export, drt
|
|
319
319
|
|
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.172.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
|
@@ -986,10 +986,20 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
986
986
|
const colourKey = (c) => (c ? [c.r, c.g, c.b].map((v) => Math.round(v * 255)).join(',') : '0,0,0');
|
|
987
987
|
const colourTol = 1 / 255 + 1e-9;
|
|
988
988
|
const sameColour = (x, y) => ['r', 'g', 'b'].every((k) => Math.abs((x ? x[k] : 0) - (y ? y[k] : 0)) <= colourTol);
|
|
989
|
+
// Which writers can WITNESS a generator colour (measured, E112/E117):
|
|
990
|
+
// Resolve's FCP7 XML export echoes it (`input_1`); its OTIO export writes
|
|
991
|
+
// a Solid Color as a Clip with a NULL media_reference and empty metadata —
|
|
992
|
+
// no colour anywhere — and an EDL/DRT re-export carries none either. A
|
|
993
|
+
// colour compare against such an export can only ever "fail", so it is
|
|
994
|
+
// reported as generatorColourNotInExport instead (like markersNotInExport).
|
|
995
|
+
const exportedFormat = String(opts.exportedFormat || '').toLowerCase();
|
|
996
|
+
const colourCapable = exportedFormat === '' || /^(xml|xmeml|fcp7|fcpxml)$/.test(exportedFormat);
|
|
997
|
+
let generatorColourNotInExport = false;
|
|
989
998
|
{
|
|
990
999
|
const inLegs = a0.filter((e) => isBlackSeg(e) && e.color);
|
|
1000
|
+
if (inLegs.length && !colourCapable) generatorColourNotInExport = true;
|
|
991
1001
|
const exLegs = b0.filter((e) => isBlackSeg(e));
|
|
992
|
-
for (const leg of inLegs) {
|
|
1002
|
+
for (const leg of generatorColourNotInExport ? [] : inLegs) {
|
|
993
1003
|
const partner = exLegs.find((x) => x.track === leg.track && x.recIn < leg.recOut && x.recOut > leg.recIn);
|
|
994
1004
|
generatorColours.compared += 1;
|
|
995
1005
|
if (!partner) {
|
|
@@ -1070,6 +1080,7 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
1070
1080
|
return {
|
|
1071
1081
|
pass: mismatches.length === 0, pairs: n, srcOffsets, mismatches, markers,
|
|
1072
1082
|
...(generatorColours.compared ? { generatorColours } : {}),
|
|
1083
|
+
...(generatorColourNotInExport ? { generatorColourNotInExport } : {}),
|
|
1073
1084
|
...(markersNotInExport ? { markersNotInExport } : {}),
|
|
1074
1085
|
...(blackSegments.input || blackSegments.exported ? { blackSegments } : {}),
|
|
1075
1086
|
...(fadeReshapedBoundaries.length ? { fadeReshapedBoundaries } : {}),
|
|
@@ -390,6 +390,18 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
};
|
|
393
|
+
// A track's transitionitems as -1-edge junctions (alignment-dependent frame
|
|
394
|
+
// plus the span), shared by video and audio lanes.
|
|
395
|
+
const trackJunctionsOf = (t) => {
|
|
396
|
+
const tlist = t.transitionitem ? (Array.isArray(t.transitionitem) ? t.transitionitem : [t.transitionitem]) : [];
|
|
397
|
+
return tlist.map((tr) => {
|
|
398
|
+
const s0 = Number(tr.start), e0 = Number(tr.end);
|
|
399
|
+
if (!Number.isFinite(s0) || !Number.isFinite(e0)) return null;
|
|
400
|
+
const al = String(tr.alignment || 'center').toLowerCase();
|
|
401
|
+
const frame = al === 'start-black' || al === 'start' ? s0 : al === 'end-black' || al === 'end' ? e0 : Math.round((s0 + e0) / 2);
|
|
402
|
+
return { frame, start: s0, end: e0 };
|
|
403
|
+
}).filter(Boolean);
|
|
404
|
+
};
|
|
393
405
|
// Attach a track's <transitionitem> siblings to its walked events. Shared
|
|
394
406
|
// by VIDEO and AUDIO tracks (E108): audio cross-fades used to be dropped at
|
|
395
407
|
// parse because only the video walk looked at transitionitems, so an XMEML
|
|
@@ -447,14 +459,7 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
447
459
|
const label = vi === 0 ? 'V' : `V${vi + 1}`;
|
|
448
460
|
const before = events.length;
|
|
449
461
|
// Junctions of this track's transitionitems, for -1 edge resolution.
|
|
450
|
-
|
|
451
|
-
currentJunctions = tlist.map((tr) => {
|
|
452
|
-
const s0 = Number(tr.start), e0 = Number(tr.end);
|
|
453
|
-
if (!Number.isFinite(s0) || !Number.isFinite(e0)) return null;
|
|
454
|
-
const al = String(tr.alignment || 'center').toLowerCase();
|
|
455
|
-
const frame = al === 'start-black' || al === 'start' ? s0 : al === 'end-black' || al === 'end' ? e0 : Math.round((s0 + e0) / 2);
|
|
456
|
-
return { frame, start: s0, end: e0 };
|
|
457
|
-
}).filter(Boolean);
|
|
462
|
+
currentJunctions = trackJunctionsOf(t);
|
|
458
463
|
// Solid Color / Color Matte generatoritems are GENERATOR legs: black by
|
|
459
464
|
// default (what Resolve writes for a fade's black side), or the
|
|
460
465
|
// `fillcolor` the item declares — Resolve's importer honours it and
|
|
@@ -480,6 +485,11 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
480
485
|
const label = ai === 0 ? 'A' : `A${ai + 1}`;
|
|
481
486
|
const before = events.length;
|
|
482
487
|
clipCursor = 0;
|
|
488
|
+
// Audio lanes carry -1 edges under their cross-fades exactly like video
|
|
489
|
+
// (Resolve's writer, measured E114: the incoming clip's <in> is the
|
|
490
|
+
// source at the overlap start and needs the junction offset) — the
|
|
491
|
+
// junction list must be THIS lane's, not the last video track's.
|
|
492
|
+
currentJunctions = trackJunctionsOf(t);
|
|
483
493
|
if (t.clipitem) walk(t.clipitem, label);
|
|
484
494
|
currentJunctions = [];
|
|
485
495
|
attachTransitions(t, label, before);
|
|
@@ -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 (paired in record order) + Solid Color / Color Matte generators carrying their `fillcolor` (BL legs with `color` → fade-to-white and colour mattes author, E110), 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; COLOUR-AWARE: an input generator leg carrying a fillcolor (fade-to-white, colour matte) must come back on the same track over its span with the same colour — Resolve\'s FCP7 writer emits it — else generator-colour fails (generatorColours reports the compare); 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 / Color Matte generators carrying their `fillcolor` (BL legs with `color` → fade-to-white and colour mattes author, E110), 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; COLOUR-AWARE: an input generator leg carrying a fillcolor (fade-to-white, colour matte) must come back on the same track over its span with the same colour — Resolve\'s FCP7 writer emits it — else generator-colour fails (generatorColours reports the compare) — pass exportedFormat: an OTIO/EDL re-export cannot carry colour (measured) and reports generatorColourNotInExport instead of failing; 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);
|
|
@@ -158,6 +158,8 @@ export const editorialTool = {
|
|
|
158
158
|
srcTol: z.number().optional(),
|
|
159
159
|
sourceMap: z.record(z.object({ mediaFilePath: z.string() }).passthrough()).optional()
|
|
160
160
|
.describe('The SAME reel→{mediaFilePath} map the assemble used — lets an EDL reel (CUTSRC) match the re-export\'s file basename (cut_src)'),
|
|
161
|
+
exportedFormat: z.string().optional()
|
|
162
|
+
.describe('Format of the re-export (otio|edl|xml|drt). Generator COLOUR can only be witnessed by an XML re-export (Resolve\'s OTIO/EDL writers carry none — measured); pass it so a colour compare against a colour-blind export reports generatorColourNotInExport instead of failing'),
|
|
161
163
|
}).parse(args);
|
|
162
164
|
// EDL reels vs exported basenames: derive the alias table from the
|
|
163
165
|
// sourceMap that drove the assemble (the one authority linking them).
|
|
@@ -166,7 +168,7 @@ export const editorialTool = {
|
|
|
166
168
|
const base = String(src.mediaFilePath).split('/').pop();
|
|
167
169
|
if (base) sourceAliases[reel] = base;
|
|
168
170
|
}
|
|
169
|
-
return verifyRoundtrip(p.input, p.exported, { recTol: p.recTol, srcTol: p.srcTol, sourceAliases });
|
|
171
|
+
return verifyRoundtrip(p.input, p.exported, { recTol: p.recTol, srcTol: p.srcTol, sourceAliases, exportedFormat: p.exportedFormat });
|
|
170
172
|
}
|
|
171
173
|
if (action === 'marker_roundtrip') {
|
|
172
174
|
const p = markerSchema.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.172.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.172.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -2604,33 +2604,51 @@ def _find_timeline_item_by_id(tl, timeline_item_id) -> Optional[Any]:
|
|
|
2604
2604
|
return None
|
|
2605
2605
|
|
|
2606
2606
|
|
|
2607
|
-
|
|
2608
|
-
"""
|
|
2607
|
+
_TRANSITION_NAMES = {
|
|
2608
|
+
"cross dissolve", "dip to color dissolve", "additive dissolve", "non-additive dissolve",
|
|
2609
|
+
"smooth cut", "blur dissolve", "cross fade", "cross fade 0db", "cross fade +3db", "cross fade -3db",
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
|
|
2613
|
+
def _describe_track_items(items, track_type: str):
|
|
2614
|
+
"""GetItemListInTrack entries → [{name, id, start, end, duration, kind}].
|
|
2609
2615
|
|
|
2610
|
-
Transitions ARE items in that list (api_truth)
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2616
|
+
Transitions ARE items in that list (api_truth). What does NOT tell them
|
|
2617
|
+
apart, measured 2026-09-01 on Studio 19.1.3.7 (E113/E115): the name (an
|
|
2618
|
+
AUDIO cross-fade enumerates with an EMPTY name), GetMediaPoolItem() (a
|
|
2619
|
+
Solid Color generator and a subtitle have none either) and GetProperty()
|
|
2620
|
+
(generator and subtitle both return None, like a transition). What does:
|
|
2621
|
+
GEOMETRY — a transition straddles a cut, so one neighbour ENDS inside its
|
|
2622
|
+
span and another STARTS inside it; a generator or clip owns its span.
|
|
2623
|
+
kind: clip (has a MediaPoolItem) | transition (known name, or no media and
|
|
2624
|
+
straddles a cut) | generator (no media, owns its span) | subtitle (track).
|
|
2615
2625
|
"""
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
has_media = it.GetMediaPoolItem() is not None
|
|
2619
|
-
except Exception:
|
|
2620
|
-
has_media = True # an API surprise must not demote a clip to a transition
|
|
2621
|
-
if not has_media:
|
|
2626
|
+
raw = []
|
|
2627
|
+
for it in items or []:
|
|
2622
2628
|
try:
|
|
2623
|
-
|
|
2629
|
+
has_media = it.GetMediaPoolItem() is not None
|
|
2624
2630
|
except Exception:
|
|
2625
|
-
|
|
2626
|
-
|
|
2631
|
+
has_media = True # an API surprise must not demote a clip
|
|
2632
|
+
raw.append({
|
|
2633
|
+
"name": it.GetName(), "id": it.GetUniqueId(), "start": it.GetStart(),
|
|
2634
|
+
"end": it.GetEnd(), "duration": it.GetDuration(), "_media": has_media,
|
|
2635
|
+
})
|
|
2636
|
+
out = []
|
|
2637
|
+
for r in raw:
|
|
2638
|
+
name = str(r["name"] or "").strip().lower()
|
|
2639
|
+
if track_type == "subtitle":
|
|
2640
|
+
kind = "subtitle"
|
|
2641
|
+
elif r["_media"]:
|
|
2642
|
+
kind = "clip"
|
|
2643
|
+
elif name in _TRANSITION_NAMES:
|
|
2627
2644
|
kind = "transition"
|
|
2628
2645
|
else:
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2646
|
+
s0, e0 = r["start"], r["end"]
|
|
2647
|
+
ends_inside = any(o is not r and o["end"] is not None and s0 < o["end"] <= e0 for o in raw)
|
|
2648
|
+
starts_inside = any(o is not r and o["start"] is not None and s0 <= o["start"] < e0 for o in raw)
|
|
2649
|
+
kind = "transition" if (ends_inside and starts_inside) else "generator"
|
|
2650
|
+
out.append({k: v for k, v in r.items() if k != "_media"} | {"kind": kind})
|
|
2651
|
+
return out
|
|
2634
2652
|
|
|
2635
2653
|
|
|
2636
2654
|
def _get_timeline_item_for_fusion(p: Dict[str, Any]):
|
|
@@ -24444,7 +24462,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
24444
24462
|
if err:
|
|
24445
24463
|
return _err(err)
|
|
24446
24464
|
items = tl.GetItemListInTrack(track_type, track_index)
|
|
24447
|
-
return {"items":
|
|
24465
|
+
return {"items": _describe_track_items(items, track_type)}
|
|
24448
24466
|
elif action == "delete_clips":
|
|
24449
24467
|
# Find timeline items by unique IDs
|
|
24450
24468
|
ids_set = set(p["clip_ids"])
|
|
@@ -26232,7 +26250,7 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
26232
26250
|
},
|
|
26233
26251
|
"timeline": {
|
|
26234
26252
|
"get_items": {
|
|
26235
|
-
"summary": "List items on one track as a summary (name/id/start/end/duration/kind). Transitions ARE items — a video Cross Dissolve by name, an AUDIO cross-fade with an EMPTY name — so `kind` (clip | transition | generator) is
|
|
26253
|
+
"summary": "List items on one track as a summary (name/id/start/end/duration/kind). Transitions ARE items — a video Cross Dissolve by name, an AUDIO cross-fade with an EMPTY name — and generators/subtitles look identical to them by media-pool item and properties (all None), so `kind` (clip | transition | generator | subtitle) is decided by geometry: a transition straddles a cut.",
|
|
26236
26254
|
"params": "track_type (video|audio|subtitle), index|track_index (1-based)",
|
|
26237
26255
|
"returns": "{items: [{name, id, start, end, duration, kind}]}",
|
|
26238
26256
|
"example": 'timeline(action="get_items", params={"track_type": "video", "index": 1})',
|
package/src/utils/api_truth.py
CHANGED
|
@@ -895,9 +895,14 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
895
895
|
"with an EMPTY GetName() (24 frames, centered on the cut, "
|
|
896
896
|
"between the two clips) — so on audio lanes even the kind is "
|
|
897
897
|
"not readable from the name. The discriminator that holds for "
|
|
898
|
-
"BOTH: GetMediaPoolItem() is None AND GetProperty() is empty
|
|
899
|
-
"a Solid Color generator
|
|
900
|
-
"
|
|
898
|
+
"BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — "
|
|
899
|
+
"BUT a Solid Color generator AND a subtitle item read the same "
|
|
900
|
+
"way (GetProperty() None, no MediaPoolItem; measured E115), so "
|
|
901
|
+
"that pair only separates clips from non-clips. What separates "
|
|
902
|
+
"a transition from a generator is GEOMETRY: a transition "
|
|
903
|
+
"straddles a cut (one neighbour ends inside its span, another "
|
|
904
|
+
"starts inside it) while a generator owns its span. "
|
|
905
|
+
"timeline.get_items reports `kind` on that basis.",
|
|
901
906
|
"recommended": "Automated QC of existing transitions IS possible and is "
|
|
902
907
|
"the main practical need — enumerate GetItemListInTrack, "
|
|
903
908
|
"treat any item whose GetProperty() is empty and whose "
|
|
@@ -2787,7 +2792,11 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2787
2792
|
"FCP7 XML writer, by contrast, carries audio, writes "
|
|
2788
2793
|
"transition-adjacent clip edges as -1 (the junction), and "
|
|
2789
2794
|
"emits `speed` followed by `variablespeed` 0 in the same "
|
|
2790
|
-
"timeremap effect."
|
|
2795
|
+
"timeremap effect. AUDIO cross-fades are written the same way "
|
|
2796
|
+
"— a transitionitem on the audio track between -1-edged "
|
|
2797
|
+
"clipitems (measured E114); the OTIO writer emits them as a "
|
|
2798
|
+
"`Custom_Transition` with symmetric offsets on the Audio "
|
|
2799
|
+
"track.",
|
|
2791
2800
|
"recommended": "For round-trip QC prefer EXPORT_OTIO (carries audio, "
|
|
2792
2801
|
"retimes as LinearTimeWarp/FreezeFrame, exact spans). "
|
|
2793
2802
|
"When an EDL is the required deliverable, expect no "
|
|
@@ -2832,7 +2841,13 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2832
2841
|
"a Solid Color's colour as the FxPlug parameter `input_1` "
|
|
2833
2842
|
"(effectid FxPlugWrapper:C18E8B62_…), not `fillcolor` — an "
|
|
2834
2843
|
"authored EffectFiltersBA colour came back exactly (white, "
|
|
2835
|
-
"128/64/191; E112)."
|
|
2844
|
+
"128/64/191; E112). The OTIO writer CANNOT: EXPORT_OTIO "
|
|
2845
|
+
"emits a Solid Color as a Clip.2 named 'Solid Color' with a "
|
|
2846
|
+
"NULL media_reference, metadata {\"Resolve_OTIO\": {}} and a "
|
|
2847
|
+
"parameterless Transform effect — no colour anywhere "
|
|
2848
|
+
"(measured E117) — so only an XML re-export witnesses a "
|
|
2849
|
+
"generator colour (editorial.verify_roundtrip exportedFormat "
|
|
2850
|
+
"→ generatorColourNotInExport).",
|
|
2836
2851
|
"recommended": "Author fade-to-white / colour mattes by placing a "
|
|
2837
2852
|
"Solid Color generator with that blob "
|
|
2838
2853
|
"(drp-format placeGenerator `color`, drt.assemble "
|