davinci-resolve-mcp 2.161.0 → 2.163.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 +51 -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/server/aaf_probe.py +11 -0
- package/resolve-advanced/server/author-interchange.mjs +9 -1
- package/resolve-advanced/server/editorial.mjs +86 -9
- package/resolve-advanced/server/tools/editorial.mjs +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +21 -1
- package/src/utils/api_truth.py +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
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.163.0 — E105: QC through every export format
|
|
6
|
+
|
|
7
|
+
One faded, dissolved, retimed conform exported from Resolve three ways — OTIO,
|
|
8
|
+
FCP7 XML, and CMX EDL — and pushed through `editorial.verify_roundtrip`. All
|
|
9
|
+
three now close `pass: true`; getting there measured four laws of Resolve's own
|
|
10
|
+
writers and fixed the parsers to match.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Every FCP7-XML retime read as a FREEZE**: Resolve's timeremap effect
|
|
15
|
+
writes `speed` 50 followed by `variablespeed` 0, and a loose `/speed/`
|
|
16
|
+
parameter match let the second overwrite the first. Exact parameter
|
|
17
|
+
matching now, with the `reverse` flag read alongside.
|
|
18
|
+
- **FCP7 `-1` clip edges** (transition-adjacent) are resolved to the
|
|
19
|
+
transition's junction — the span center for `center` alignment — with the
|
|
20
|
+
clip's `in` advanced by the overlap offset so source stays record-aligned;
|
|
21
|
+
`out - in` is the record duration even under a retime. Solid Color
|
|
22
|
+
`generatoritem`s walk as black legs, and fade transitions attach to the
|
|
23
|
+
picture rather than the black.
|
|
24
|
+
- **CMX EDL exports name every file source reel `AX`** with the real names in
|
|
25
|
+
`* FROM/TO CLIP NAME` comments; `parseEDL` now applies them (TO = incoming,
|
|
26
|
+
FROM = outgoing of a dissolve pair), keeping specific reels intact.
|
|
27
|
+
- **`timeline.export_timeline_checked` refuses unresolved export constants
|
|
28
|
+
loudly.** A made-up `EXPORT_CMX_3600` reached `Timeline.Export` as a string
|
|
29
|
+
and came back as a bare `success: false`; the real constant is `EXPORT_EDL`,
|
|
30
|
+
and the refusal now lists the vocabulary.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `verify_roundtrip` reports `audioNotInExport` when the export carries no
|
|
35
|
+
audio at all (Resolve's EDL writer is video-only — measured) instead of
|
|
36
|
+
failing, mirroring `markersNotInExport`.
|
|
37
|
+
|
|
38
|
+
### Measured (filed in api-limitations)
|
|
39
|
+
|
|
40
|
+
- `EXPORT_EDL` is video-only, writes reel `AX` + clip-name comments, places
|
|
41
|
+
dissolve junctions at the CMX start-at-cut position, and writes the BL
|
|
42
|
+
fades its own importer drops.
|
|
43
|
+
|
|
44
|
+
## What's New in v2.162.0 — freeze parity: Avid 0% motion effects are freezes
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **An Avid freeze frame silently read as a 100% clip.** A motion effect at
|
|
49
|
+
0% (`PARAM_SPEED_RATIO_U 0.0`, or a flat speed map at 0) fell into the AAF
|
|
50
|
+
walker's "no play rate recoverable" branch, and the freeze vanished. An
|
|
51
|
+
explicit zero now reports `speed: 0, freeze: true`, and the bridge authors
|
|
52
|
+
the real freeze — completing freeze parity across all five ingest formats
|
|
53
|
+
(EDL `M2 000.0`, OTIO `FreezeFrame`, XMEML `timeremap` 0, PrProj
|
|
54
|
+
`InPoint == OutPoint`, and now AAF).
|
|
55
|
+
|
|
5
56
|
## What's New in v2.161.0 — OTIO freeze frames close the loop
|
|
6
57
|
|
|
7
58
|
### Fixed
|
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.163.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:** 36 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
|
|
@@ -301,6 +301,14 @@ equivalent, blocking full automation.
|
|
|
301
301
|
- **Workaround / current handling:** Read item.GetNodeGraph().GetNumNodes() before SetCDL and diagnose a False against the node count and clip type. Prove the applied look with a rendered frame (gallery_stills grab_and_export or Project.ExportCurrentFrameAsStill), not the return value.
|
|
302
302
|
- **Tags:** color, cdl, readback, silent-failure
|
|
303
303
|
|
|
304
|
+
### Timeline.Export EXPORT_EDL (video-only, reel AX, clip-name comments)
|
|
305
|
+
|
|
306
|
+
- **Object:** `Timeline`
|
|
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.
|
|
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
|
+
- **Tags:** timeline, export, edl, audio, silent-failure
|
|
311
|
+
|
|
304
312
|
## Bugs / Unreliable Behavior (please fix)
|
|
305
313
|
|
|
306
314
|
Methods that exist but misbehave — silent failures, unreliable return
|
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.163.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
|
@@ -726,6 +726,11 @@ def _retime_fields(op_group):
|
|
|
726
726
|
play = None
|
|
727
727
|
speed_map = None
|
|
728
728
|
offset_map = None
|
|
729
|
+
# An EXPLICIT zero play rate is a freeze (Avid motion effect at 0%,
|
|
730
|
+
# E104): PARAM_SPEED_RATIO_U == 0.0 or a flat PARAM_SPEED_MAP_U at 0.
|
|
731
|
+
# It must not read as "nothing recoverable" — a freeze that silently
|
|
732
|
+
# became 100% is the loudest timing lie of all.
|
|
733
|
+
zero_seen = False
|
|
729
734
|
length = _length(op_group)
|
|
730
735
|
for param in _op_parameters(op_group):
|
|
731
736
|
cls = type(param).__name__
|
|
@@ -754,6 +759,8 @@ def _retime_fields(op_group):
|
|
|
754
759
|
continue
|
|
755
760
|
if value:
|
|
756
761
|
play = value # *_U family stores the play rate directly
|
|
762
|
+
else:
|
|
763
|
+
zero_seen = True
|
|
757
764
|
curve = {}
|
|
758
765
|
for key, param in (("playRate", speed_map), ("sourceOffset", offset_map)):
|
|
759
766
|
if param is None:
|
|
@@ -782,7 +789,11 @@ def _retime_fields(op_group):
|
|
|
782
789
|
return {"speedVarying": True, **extra}
|
|
783
790
|
if play is None and values and values[0]:
|
|
784
791
|
play = values[0] # a flat map's single value IS the constant play rate
|
|
792
|
+
elif play is None and values and values[0] == 0:
|
|
793
|
+
zero_seen = True
|
|
785
794
|
if not play:
|
|
795
|
+
if zero_seen:
|
|
796
|
+
return {"speedRatio": 0.0, "speed": 0, "reverse": False, "freeze": True, **extra}
|
|
786
797
|
return extra
|
|
787
798
|
return {
|
|
788
799
|
"speedRatio": round(abs(play), 6),
|
|
@@ -990,7 +990,14 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
990
990
|
const aa = dedupeAud(norm(inputEvents, audsOf).filter((e) => !isBlackSeg(e)));
|
|
991
991
|
const ba = dedupeAud(norm(exportedEvents, audsOf).filter((e) => !isBlackSeg(e)));
|
|
992
992
|
let audio;
|
|
993
|
-
|
|
993
|
+
let audioNotInExport = false;
|
|
994
|
+
if (aa.length && !ba.length) {
|
|
995
|
+
// The export carries NO audio at all while the input declares some: a
|
|
996
|
+
// capability gap (Resolve's EXPORT_EDL is video-only, measured E105),
|
|
997
|
+
// reported as audioNotInExport rather than failed — like markers.
|
|
998
|
+
audio = { input: aa.length, exported: 0, compared: false, note: 'export carries no audio events (format capability, e.g. EDL is video-only) — not compared' };
|
|
999
|
+
audioNotInExport = true;
|
|
1000
|
+
} else if (aa.length) {
|
|
994
1001
|
audio = { input: aa.length, exported: ba.length, compared: true };
|
|
995
1002
|
comparePairs(aa, ba, junctionsFor(/^A\d*$/), 'audio');
|
|
996
1003
|
} else if (ba.length) {
|
|
@@ -1033,5 +1040,6 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
1033
1040
|
...(blackSegments.input || blackSegments.exported ? { blackSegments } : {}),
|
|
1034
1041
|
...(fadeReshapedBoundaries.length ? { fadeReshapedBoundaries } : {}),
|
|
1035
1042
|
...(audio ? { audio } : {}),
|
|
1043
|
+
...(audioNotInExport ? { audioNotInExport } : {}),
|
|
1036
1044
|
};
|
|
1037
1045
|
}
|
|
@@ -90,6 +90,29 @@ export function parseEDL(text, opts = {}) {
|
|
|
90
90
|
// at an absolute record timecode. Emitted as track 'MARKER' pseudo-events
|
|
91
91
|
// (recIn only) so the assemble bridge can author them; consumers that
|
|
92
92
|
// filter video/audio by track shape ignore them.
|
|
93
|
+
// `* FROM CLIP NAME:` / `* TO CLIP NAME:` comments (E105): Resolve's own
|
|
94
|
+
// EDL writer names every file source by the generic reel AX and carries
|
|
95
|
+
// the real clip name here. TO names the incoming (the last event, a D
|
|
96
|
+
// line's), FROM the outgoing (the event before a transition pair, else
|
|
97
|
+
// the last event). A generic reel (AX/BL-less) takes the clip name as
|
|
98
|
+
// its source so sourceMap and QC key on something real; a specific reel
|
|
99
|
+
// keeps it, with clipName carried alongside.
|
|
100
|
+
const cn = /^\*\s*(FROM|TO)\s+CLIP\s+NAME:\s*(.+?)\s*$/i.exec(line);
|
|
101
|
+
if (cn) {
|
|
102
|
+
const which = cn[1].toUpperCase();
|
|
103
|
+
const clipName = cn[2];
|
|
104
|
+
const last = [...events].reverse().find((e) => e.track !== 'MARKER');
|
|
105
|
+
let target = last;
|
|
106
|
+
if (which === 'FROM' && last && last.transition) {
|
|
107
|
+
const prev = [...events].reverse().find((e) => e !== last && e.track === last.track && e.recOut === last.recIn);
|
|
108
|
+
if (prev) target = prev;
|
|
109
|
+
}
|
|
110
|
+
if (target && !/^(BL|BLACK)$/i.test(String(target.source))) {
|
|
111
|
+
target.clipName = clipName;
|
|
112
|
+
if (/^AX$/i.test(String(target.source))) target.source = clipName;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
93
116
|
const loc = /^\*\s*LOC:\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s+(\S+)\s*(.*)$/i.exec(line);
|
|
94
117
|
if (loc) {
|
|
95
118
|
events.push(evt({
|
|
@@ -273,36 +296,69 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
273
296
|
const events = [];
|
|
274
297
|
let idx = 1;
|
|
275
298
|
const seqRate = opts.fps || 24;
|
|
299
|
+
let currentJunctions = [];
|
|
276
300
|
const walk = (node, track) => {
|
|
277
301
|
if (!node) return;
|
|
278
302
|
const items = Array.isArray(node) ? node : [node];
|
|
279
303
|
for (const it of items) {
|
|
280
304
|
const name = it.name || (it.file && it.file.name) || 'UNKNOWN';
|
|
281
|
-
const
|
|
282
|
-
const
|
|
305
|
+
const start0 = Number(it.start);
|
|
306
|
+
const end0 = Number(it.end);
|
|
283
307
|
const inF = Number(it.in);
|
|
284
308
|
const outF = Number(it.out);
|
|
285
309
|
let speed = 100,
|
|
286
310
|
reverse = false;
|
|
287
|
-
// speed via a timeremap/motion filter (
|
|
311
|
+
// speed via a timeremap/motion filter. EXACT parameter match (E105):
|
|
312
|
+
// Resolve's own FCP7 writer emits `speed` 50 followed by
|
|
313
|
+
// `variablespeed` 0 in the same effect, and a loose /speed/ match let
|
|
314
|
+
// the second overwrite the first — every retime read as a FREEZE.
|
|
288
315
|
const filters = it.filter ? (Array.isArray(it.filter) ? it.filter : [it.filter]) : [];
|
|
289
316
|
for (const fl of filters) {
|
|
290
317
|
const params = fl.effect && fl.effect.parameter ? (Array.isArray(fl.effect.parameter) ? fl.effect.parameter : [fl.effect.parameter]) : [];
|
|
291
318
|
for (const pm of params) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
319
|
+
const pid = String(pm.parameterid || pm.name || '').trim().toLowerCase();
|
|
320
|
+
if (pid === 'speed' && pm.value != null && typeof pm.value !== 'object') {
|
|
321
|
+
const v = Number(pm.value);
|
|
322
|
+
if (Number.isFinite(v)) { speed = Math.abs(v); reverse = v < 0; }
|
|
323
|
+
} else if (pid === 'reverse' && /^true$/i.test(String(pm.value))) {
|
|
324
|
+
reverse = true;
|
|
295
325
|
}
|
|
296
326
|
}
|
|
297
327
|
}
|
|
298
|
-
|
|
328
|
+
// FCP7 `-1` EDGES (E105, measured against Resolve's own FCP7 writer):
|
|
329
|
+
// a clipitem edge under a transitionitem is written as -1 and means
|
|
330
|
+
// "this edge is the adjacent transition's JUNCTION" — the span center
|
|
331
|
+
// for alignment center, its start/end for start-black/end-black.
|
|
332
|
+
// `out - in` is the RECORD duration even under a retime (Resolve
|
|
333
|
+
// writes out = in + record length), which anchors the missing edge.
|
|
334
|
+
let start = start0;
|
|
335
|
+
let end = end0;
|
|
336
|
+
let inAdj = 0;
|
|
337
|
+
const dur = Number.isFinite(inF) && Number.isFinite(outF) ? outF - inF : null;
|
|
338
|
+
const junctions = currentJunctions;
|
|
339
|
+
if (start === -1 && end !== -1 && dur != null) start = end - dur;
|
|
340
|
+
else if (end === -1 && start !== -1 && dur != null) end = start + dur;
|
|
341
|
+
else if (start === -1 && end === -1 && dur != null && junctions.length) {
|
|
342
|
+
const js = junctions.map((j) => j.frame).sort((a, b) => a - b);
|
|
343
|
+
const pair = js.find((a) => js.includes(a + dur));
|
|
344
|
+
if (pair != null) { start = pair; end = pair + dur; }
|
|
345
|
+
}
|
|
346
|
+
// A -1 START edge's `in` is the source at the OVERLAP start (the
|
|
347
|
+
// clip's material begins under the transition), while the resolved
|
|
348
|
+
// start is the junction — so `in` advances by the same offset to stay
|
|
349
|
+
// record-aligned (measured: srcIn read 12 short without this).
|
|
350
|
+
if (start0 === -1 && Number.isFinite(start)) {
|
|
351
|
+
const j = junctions.find((jj) => jj.frame === start);
|
|
352
|
+
if (j) inAdj = start - j.start;
|
|
353
|
+
}
|
|
354
|
+
if (Number.isFinite(start) && Number.isFinite(end) && start >= 0 && end >= 0) {
|
|
299
355
|
// FCP7 clipitem <marker> children are CLIP markers (frames relative
|
|
300
356
|
// to the clip's own <in>) — routed to item markers like OTIO's (E81).
|
|
301
357
|
const mks = it.marker ? (Array.isArray(it.marker) ? it.marker : [it.marker]) : [];
|
|
302
358
|
const itemMarkers = mks
|
|
303
359
|
.filter((mk) => Number.isFinite(Number(mk.in)))
|
|
304
360
|
.map((mk) => ({ frame: Number(mk.in) - inF, name: mk.name != null ? String(mk.name) : undefined, note: mk.comment != null ? String(mk.comment) : undefined }));
|
|
305
|
-
events.push(evt({ index: idx++, track, source: name, srcIn: inF, srcOut: outF, recIn: start, recOut: end, speed, reverse, itemMarkers: itemMarkers.length ? itemMarkers : undefined, fps: seqRate }));
|
|
361
|
+
events.push(evt({ index: idx++, track, source: name, srcIn: Number.isFinite(inF) ? inF + inAdj : inF, srcOut: Number.isFinite(outF) ? outF + inAdj : outF, recIn: start, recOut: end, speed, reverse, itemMarkers: itemMarkers.length ? itemMarkers : undefined, fps: seqRate }));
|
|
306
362
|
}
|
|
307
363
|
}
|
|
308
364
|
};
|
|
@@ -313,7 +369,24 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
313
369
|
vtracks.forEach((t, vi) => {
|
|
314
370
|
const label = vi === 0 ? 'V' : `V${vi + 1}`;
|
|
315
371
|
const before = events.length;
|
|
372
|
+
// Junctions of this track's transitionitems, for -1 edge resolution.
|
|
373
|
+
const tlist = t.transitionitem ? (Array.isArray(t.transitionitem) ? t.transitionitem : [t.transitionitem]) : [];
|
|
374
|
+
currentJunctions = tlist.map((tr) => {
|
|
375
|
+
const s0 = Number(tr.start), e0 = Number(tr.end);
|
|
376
|
+
if (!Number.isFinite(s0) || !Number.isFinite(e0)) return null;
|
|
377
|
+
const al = String(tr.alignment || 'center').toLowerCase();
|
|
378
|
+
const frame = al === 'start-black' || al === 'start' ? s0 : al === 'end-black' || al === 'end' ? e0 : Math.round((s0 + e0) / 2);
|
|
379
|
+
return { frame, start: s0, end: e0 };
|
|
380
|
+
}).filter(Boolean);
|
|
381
|
+
// Solid Color generatoritems are BLACK legs (what Resolve writes for a
|
|
382
|
+
// fade's black side); they walk as BL events so fades round-trip.
|
|
383
|
+
const gens = t.generatoritem ? (Array.isArray(t.generatoritem) ? t.generatoritem : [t.generatoritem]) : [];
|
|
384
|
+
for (const g of gens) {
|
|
385
|
+
if (!/solid color|black/i.test(String(g.name || ''))) continue;
|
|
386
|
+
walk([{ ...g, name: 'BL', filter: undefined, marker: undefined }], label);
|
|
387
|
+
}
|
|
316
388
|
if (t.clipitem) walk(t.clipitem, label);
|
|
389
|
+
currentJunctions = [];
|
|
317
390
|
// <transitionitem> siblings: attach each to the INCOMING clip event —
|
|
318
391
|
// the one whose record-in falls inside the transition's span. Resolve's
|
|
319
392
|
// OWN XMEML importer writes these as elements that render INERT on
|
|
@@ -326,7 +399,11 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
326
399
|
const s = Number(tr.start), e2 = Number(tr.end);
|
|
327
400
|
if (!Number.isFinite(s) || !Number.isFinite(e2)) continue;
|
|
328
401
|
const effectId = (tr.effect && (tr.effect.effectid || tr.effect.name)) || 'Cross Dissolve';
|
|
329
|
-
const
|
|
402
|
+
const isBLev = (ev) => /^(BL|BLACK)$/i.test(String(ev.source || '').trim());
|
|
403
|
+
const inSpan = (ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2;
|
|
404
|
+
// Prefer the PICTURE as the incoming: a Solid Color generatoritem in
|
|
405
|
+
// the same span is the fade's black side, not the clip fading in.
|
|
406
|
+
const incoming = events.slice(before).find((ev) => inSpan(ev) && !isBLev(ev)) || events.slice(before).find(inSpan);
|
|
330
407
|
// recStart carries the transitionitem's EXPLICIT record span start
|
|
331
408
|
// (sequence-relative) so the bridge reproduces the editor's actual
|
|
332
409
|
// alignment instead of assuming centered.
|
|
@@ -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 + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events incl. span-explicit transitions, BL fade legs, and
|
|
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/replaced/new/gone + timing flags), 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.163.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.163.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -6927,6 +6927,26 @@ def _export_timeline_checked(tl, p: Dict[str, Any]):
|
|
|
6927
6927
|
# (which can be None and silently degrade the EXPORT_* args to strings — the
|
|
6928
6928
|
# same failure class as issue #70).
|
|
6929
6929
|
spec = _timeline_export_spec(p, get_resolve())
|
|
6930
|
+
# An export type that did not resolve to a Resolve constant reaches
|
|
6931
|
+
# Timeline.Export as a STRING, which returns a bare False — reported as
|
|
6932
|
+
# success:false with no reason (measured E105 with a made-up
|
|
6933
|
+
# EXPORT_CMX_3600: the real constant is EXPORT_EDL). Refuse loudly with
|
|
6934
|
+
# the vocabulary instead of letting a typo read as an export failure.
|
|
6935
|
+
# Only when this build exposes the export vocabulary at all (a bare stub
|
|
6936
|
+
# or a degraded connection resolves nothing, and the old passthrough
|
|
6937
|
+
# stays the honest behavior there).
|
|
6938
|
+
_probe_const, _ = _timeline_export_value("EXPORT_OTIO", get_resolve())
|
|
6939
|
+
vocab_known = not isinstance(_probe_const, str)
|
|
6940
|
+
for field in ("export_type", "export_subtype"):
|
|
6941
|
+
val = spec.get(field)
|
|
6942
|
+
if vocab_known and isinstance(val, str) and val.startswith("EXPORT_"):
|
|
6943
|
+
return _err(
|
|
6944
|
+
f"{field} {val!r} is not a Resolve export constant on this build. "
|
|
6945
|
+
f"Known aliases: {', '.join(sorted(_TIMELINE_EXPORT_ALIASES))}; "
|
|
6946
|
+
"constants: EXPORT_AAF, EXPORT_DRT, EXPORT_EDL, EXPORT_FCP_7_XML, "
|
|
6947
|
+
"EXPORT_FCPXML_1_8/1_9/1_10, EXPORT_OTIO (subtypes EXPORT_NONE, "
|
|
6948
|
+
"EXPORT_AAF_NEW, EXPORT_AAF_EXISTING, EXPORT_CDL, EXPORT_SDL, EXPORT_MISSING_CLIPS)."
|
|
6949
|
+
)
|
|
6930
6950
|
if p.get("dry_run"):
|
|
6931
6951
|
return _ok(path=path, would_export=True, spec={k: v for k, v in spec.items() if k != "export_type"})
|
|
6932
6952
|
|
package/src/utils/api_truth.py
CHANGED
|
@@ -2764,6 +2764,36 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2764
2764
|
"submit": "bug",
|
|
2765
2765
|
"mitigation": ["drt.assemble_from_interchange black-leg authoring"],
|
|
2766
2766
|
},
|
|
2767
|
+
{
|
|
2768
|
+
"symbol": "Timeline.Export EXPORT_EDL (video-only, reel AX, clip-name comments)",
|
|
2769
|
+
"object": "Timeline",
|
|
2770
|
+
"signature": "(filePath, EXPORT_EDL, EXPORT_NONE) -> bool",
|
|
2771
|
+
"reality": "Resolve's CMX EDL writer (measured on Studio 19.1.3.7, "
|
|
2772
|
+
"E105) emits VIDEO events only — audio legs never appear; "
|
|
2773
|
+
"names every file source by the generic reel AX and carries "
|
|
2774
|
+
"the real names in `* FROM CLIP NAME:` / `* TO CLIP NAME:` "
|
|
2775
|
+
"comments; writes black legs as reel BL; places dissolve "
|
|
2776
|
+
"junctions at the CMX start-at-cut position (the overlap "
|
|
2777
|
+
"start, not the centered junction the timeline holds); and "
|
|
2778
|
+
"WRITES BL fades that its own EDL importer then drops. The "
|
|
2779
|
+
"FCP7 XML writer, by contrast, carries audio, writes "
|
|
2780
|
+
"transition-adjacent clip edges as -1 (the junction), and "
|
|
2781
|
+
"emits `speed` followed by `variablespeed` 0 in the same "
|
|
2782
|
+
"timeremap effect.",
|
|
2783
|
+
"recommended": "For round-trip QC prefer EXPORT_OTIO (carries audio, "
|
|
2784
|
+
"retimes as LinearTimeWarp/FreezeFrame, exact spans). "
|
|
2785
|
+
"When an EDL is the required deliverable, expect no "
|
|
2786
|
+
"audio (editorial.verify_roundtrip reports "
|
|
2787
|
+
"audioNotInExport) and resolve AX reels through the "
|
|
2788
|
+
"clip-name comments (parseEDL does). Use EXPORT_EDL — "
|
|
2789
|
+
"there is no EXPORT_CMX_3600 constant, and an unknown "
|
|
2790
|
+
"name reaches Export as a string that returns a bare "
|
|
2791
|
+
"False (timeline.export_timeline_checked now refuses "
|
|
2792
|
+
"it loudly).",
|
|
2793
|
+
"tags": ["timeline", "export", "edl", "audio", "silent-failure"],
|
|
2794
|
+
"submit": "missing",
|
|
2795
|
+
"mitigation": ["editorial.verify_roundtrip audioNotInExport", "timeline.export_timeline_checked"],
|
|
2796
|
+
},
|
|
2767
2797
|
{
|
|
2768
2798
|
"symbol": "Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)",
|
|
2769
2799
|
"object": "Project",
|