davinci-resolve-mcp 2.149.0 → 2.151.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 +40 -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/author-interchange.mjs +99 -6
- package/resolve-advanced/server/editorial.mjs +57 -2
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/place-generator.js +19 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
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.151.0 — fade parity: OTIO and XMEML fades author too
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **OTIO and XMEML fades route through the black machinery** shipped for EDL
|
|
10
|
+
BL legs in v2.150.0. OTIO: a Transition adjacent to a Gap (or the track
|
|
11
|
+
edge) is a fade — gap-then-Transition-then-Clip fades in, Transition-then-
|
|
12
|
+
Gap (or Transition as the last child) fades out. XMEML: an edge
|
|
13
|
+
transitionitem whose span has no outgoing clip fades in, no incoming clip
|
|
14
|
+
fades out. All synthesize BL pseudo-events; the bridge materializes a
|
|
15
|
+
zero-length black leg to cover its side of the span whenever the boundary
|
|
16
|
+
shift alone won't grow it (empty track renders black, so the growth is
|
|
17
|
+
render-neutral). Live OTIO render: fade-in luma ramps 18→123, the centered
|
|
18
|
+
fade-out 124→18 — identical geometry from all three parsers.
|
|
19
|
+
|
|
20
|
+
## What's New in v2.150.0 — fades conform: BL legs author, and Resolve's own EDL importer drops them
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **EDL fades author end-to-end** in `drt.assemble_from_interchange`: BL
|
|
25
|
+
(black) legs become Solid Color generator elements and the fades real
|
|
26
|
+
clip-to-generator dissolves. A CMX fade-in's zero-length BL slug grows
|
|
27
|
+
through the boundary-shift machinery (single-sided transitions refuse to
|
|
28
|
+
import — measured); the picture trims its head with source staying
|
|
29
|
+
record-aligned. Render-proven: luma ramps 18→123 across a 24-frame
|
|
30
|
+
fade-in and 123→16 across the fade-out, with the black tail holding.
|
|
31
|
+
Audio BL fades (to silence) drop with a stated reason — there is no
|
|
32
|
+
silence source to cross-fade against.
|
|
33
|
+
- Generator elements now insert into the track in **chronological item
|
|
34
|
+
order** (junction detection reads listed adjacency), and an edge-aligned
|
|
35
|
+
span whose leg is shorter than its boundary shift drops with a reason
|
|
36
|
+
instead of authoring a broken geometry.
|
|
37
|
+
|
|
38
|
+
### Measured (new Resolve bug, filed in api-limitations)
|
|
39
|
+
|
|
40
|
+
- **Resolve's own EDL importer silently drops BL dissolves**: the fade-in
|
|
41
|
+
vanishes wholesale (frame 0 renders full-bright) and a fade-out leaves a
|
|
42
|
+
hard cut into the Solid Color generator it creates for the BL slug. A
|
|
43
|
+
single-sided transition element refuses to import entirely.
|
|
44
|
+
|
|
5
45
|
## What's New in v2.149.0 — subtitle render truth: ExportSubtitle lies on 19.x
|
|
6
46
|
|
|
7
47
|
### Measured (new Resolve bug, filed in api-limitations)
|
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.151.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:** 35 missing capabilities,
|
|
15
|
+
**Totals:** 35 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
|
|
@@ -688,6 +688,14 @@ values, or automation-hostile modal prompts.
|
|
|
688
688
|
- **Workaround / current handling:** Do not use EXPORT_OTIO to carry or verify markers. editorial.verify_roundtrip reports this case as `markersNotInExport` (honesty flag, not a failure); read markers through the marker API for fidelity checks, and author them offline via drt.assemble spec.markers when a .drt must carry them.
|
|
689
689
|
- **Tags:** timeline, export, otio, markers, silent-failure
|
|
690
690
|
|
|
691
|
+
### MediaPool.ImportTimelineFromFile EDL (drops BL fades)
|
|
692
|
+
|
|
693
|
+
- **Object:** `MediaPool`
|
|
694
|
+
- **Signature:** `(filePath.edl, importOptions) -> Timeline`
|
|
695
|
+
- **Behavior:** EDL dissolves involving the BL (black) reel are dropped silently on import (measured on Studio 19.1.3.7, E91): a CMX fade-in (zero-length BL cut + D event) vanishes wholesale — frame 0 renders at full brightness — and a fade-out to a BL leg imports the BL as a Solid Color generator but drops the dissolve, leaving a hard cut to black. Import succeeds; the importer authors dissolves normally between two real media clips. A related law: a hand-authored SINGLE-SIDED transition element (span at a lone clip head or tail, only one neighboring item) refuses to import entirely — Resolve creates no timeline.
|
|
696
|
+
- **Workaround / current handling:** Conform EDLs with fades through drt.assemble_from_interchange: BL legs author as Solid Color generator elements and the fades as real clip-to-generator dissolves (render-verified: luma ramps 18->123 across a 24f fade-in and 123->16 across the fade-out, black tail holding 16). Audio BL fades (to silence) drop with a stated reason — there is no silence source to cross-fade against.
|
|
697
|
+
- **Tags:** edl, import, transitions, fades, silent-failure
|
|
698
|
+
|
|
691
699
|
### Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)
|
|
692
700
|
|
|
693
701
|
- **Object:** `Project`
|
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.151.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
|
@@ -258,7 +258,13 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
258
258
|
const audioSkipped = events.length - vids.length - audsRaw.length - markerEvents.length;
|
|
259
259
|
if (!vids.length) throw new Error('eventsToAssembleSpec: no video events with record ranges');
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
// BL/BLACK reels are the EDL's built-in black source — they never need a
|
|
262
|
+
// sourceMap entry. Video BL legs author as Solid Color generator elements
|
|
263
|
+
// (Resolve's own EDL importer creates exactly that for the slug — but DROPS
|
|
264
|
+
// the fade dissolves around it, measured E91); audio BL legs are silence,
|
|
265
|
+
// which an empty track already is.
|
|
266
|
+
const isBL = (srcName) => /^(BL|BLACK)$/i.test(String(srcName || '').trim());
|
|
267
|
+
const unmapped = [...new Set([...vids, ...auds].map((e) => e.source).filter((srcName) => !isBL(srcName) && !sourceMap[srcName]))];
|
|
262
268
|
if (unmapped.length) {
|
|
263
269
|
throw new Error(
|
|
264
270
|
`eventsToAssembleSpec: unmapped source reel(s): ${unmapped.join(', ')} — ` +
|
|
@@ -285,12 +291,45 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
285
291
|
const minRec = preserveStartTimecode ? 0 : minRecRaw;
|
|
286
292
|
const ORIGIN = preserveStartTimecode ? 0 : DEFAULT_ORIGIN;
|
|
287
293
|
const startFrame = preserveStartTimecode ? minRecRaw : DEFAULT_ORIGIN;
|
|
294
|
+
const blackLegs = [];
|
|
288
295
|
for (const e of vids) {
|
|
289
296
|
const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
|
|
290
297
|
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
291
298
|
const durationFrames = recOut - recIn;
|
|
299
|
+
const vTrackEarly = trackNum(e.track);
|
|
300
|
+
if (isBL(e.source)) {
|
|
301
|
+
// BL leg → Solid Color generator element (renders black; the authored
|
|
302
|
+
// clip↔generator dissolve render-verified E91: luma ramps 124→16
|
|
303
|
+
// through the junction). ZERO-length BL is kept as a placement so a
|
|
304
|
+
// fade-in's boundary-shift can GROW it — the CMX fade-in form is a
|
|
305
|
+
// zero-length BL cut followed by a D event, and a single-sided
|
|
306
|
+
// transition refuses to import (measured E91), so the shift is the
|
|
307
|
+
// only authorable shape.
|
|
308
|
+
if (durationFrames < 0) continue;
|
|
309
|
+
const el = { type: 'generator', generatorName: 'Solid Color', track: vTrackEarly, startFrame: recIn, durationFrames, srcIn: 0 };
|
|
310
|
+
blackLegs.push(el);
|
|
311
|
+
if (e.transition) {
|
|
312
|
+
let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
|
|
313
|
+
d += d % 2;
|
|
314
|
+
let pre = Math.floor(d / 2);
|
|
315
|
+
if (e.transition.alignment === 'start') pre = 0;
|
|
316
|
+
else if (e.transition.inOffset != null) pre = Math.min(d, Math.max(0, toTl(e.transition.inOffset, e.fps)));
|
|
317
|
+
else if (e.transition.recStart != null) {
|
|
318
|
+
const spanStartAbs = ORIGIN + (toTl(e.transition.recStart, e.fps) - minRec);
|
|
319
|
+
pre = Math.min(d, Math.max(0, recIn - spanStartAbs));
|
|
320
|
+
}
|
|
321
|
+
transitionCandidates.push({
|
|
322
|
+
atFrame: recIn, durationFrames: d, track: vTrackEarly, pre,
|
|
323
|
+
explicitSpan: e.transition.alignment != null || e.transition.inOffset != null || e.transition.recStart != null,
|
|
324
|
+
index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
|
|
325
|
+
source: e.source, srcIn: Infinity, incomingCutRef: el, cutPoint: e.transition.cutPoint,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: Infinity, durationFrames, track: vTrackEarly, cutRef: el });
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
292
331
|
if (durationFrames <= 0) continue;
|
|
293
|
-
const vTrack =
|
|
332
|
+
const vTrack = vTrackEarly;
|
|
294
333
|
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), ...(vTrack > 1 ? { track: vTrack } : {}) };
|
|
295
334
|
// CLIP markers from the turnover (OTIO clip markers, XMEML clipitem
|
|
296
335
|
// <marker>s) become ITEM markers on the cut (frames clip-relative,
|
|
@@ -363,7 +402,21 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
363
402
|
const audioPlacements = [];
|
|
364
403
|
const audioRetimesSkipped = [];
|
|
365
404
|
const audioTransCandidates = [];
|
|
405
|
+
let audioBlackLegsSkipped = 0;
|
|
366
406
|
for (const e of auds) {
|
|
407
|
+
if (isBL(e.source)) {
|
|
408
|
+
// Audio BL = silence, which an empty track already is. A fade to/from
|
|
409
|
+
// it has no authorable form (no silence source to cross-fade against);
|
|
410
|
+
// the level steps at the cut instead of ramping — stated, not silent.
|
|
411
|
+
audioBlackLegsSkipped += 1;
|
|
412
|
+
if (e.transition) {
|
|
413
|
+
droppedTransitions.push({
|
|
414
|
+
index: e.index, type: e.transition.type, duration: e.transition.duration, trackType: 'audio',
|
|
415
|
+
reason: 'audio fade to/from BL (silence) — no silence source to cross-fade against; the level steps at the cut',
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
367
420
|
const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
|
|
368
421
|
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
369
422
|
const durationFrames = recOut - recIn;
|
|
@@ -408,7 +461,10 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
408
461
|
|
|
409
462
|
// Overlap is judged PER VIDEO TRACK — V2 stacking over V1 is legitimate
|
|
410
463
|
// conform geometry (render-verified: an upper-track clip covers the lower).
|
|
411
|
-
|
|
464
|
+
// end-tiebreak keeps a zero-length BL placement AHEAD of the picture leg
|
|
465
|
+
// that starts at the same frame (fade-in form) — start-only sorting would
|
|
466
|
+
// read them as an overlap.
|
|
467
|
+
placements.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
412
468
|
const byTrack = new Map();
|
|
413
469
|
for (const pl of placements) {
|
|
414
470
|
if (!byTrack.has(pl.track)) byTrack.set(pl.track, []);
|
|
@@ -457,10 +513,27 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
457
513
|
// FULL-duration outgoing tail.
|
|
458
514
|
const pre = c.pre ?? c.durationFrames / 2;
|
|
459
515
|
const post = c.durationFrames - pre;
|
|
516
|
+
// OTIO/XMEML fades (E92): a zero-length BL leg MATERIALIZES to cover its
|
|
517
|
+
// side of the span whenever the boundary shift alone won't grow it —
|
|
518
|
+
// empty track renders black, so growth beyond the picture is
|
|
519
|
+
// render-neutral, and placeTransition needs a physical item on each side
|
|
520
|
+
// of the junction. (The CMX pre===0 form grows through the shift below.)
|
|
521
|
+
if (isBL(prev.source) && prev.cutRef.durationFrames === 0 && pre > 0) {
|
|
522
|
+
prev.cutRef.startFrame -= pre;
|
|
523
|
+
prev.cutRef.durationFrames += pre;
|
|
524
|
+
prev.start -= pre;
|
|
525
|
+
}
|
|
526
|
+
if (isBL(c.source) && c.incomingCutRef.durationFrames === 0 && post > 0) {
|
|
527
|
+
c.incomingCutRef.durationFrames += post;
|
|
528
|
+
}
|
|
529
|
+
// A BL side has infinite handles: a Solid Color generator extends freely
|
|
530
|
+
// in both directions (srcIn is already Infinity on BL candidates).
|
|
460
531
|
const bHandle = c.srcIn >= pre;
|
|
461
532
|
const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
|
|
462
533
|
const aFrames = aSpec && Number(aSpec.frameCount);
|
|
463
|
-
const aHandle =
|
|
534
|
+
const aHandle = isBL(prev.source)
|
|
535
|
+
? true
|
|
536
|
+
: (Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false);
|
|
464
537
|
if (!bHandle || !aHandle) {
|
|
465
538
|
droppedTransitions.push({
|
|
466
539
|
index: c.index, type: c.type, duration: c.rawDuration,
|
|
@@ -496,6 +569,16 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
496
569
|
// span, the edge law), else center.
|
|
497
570
|
const cp = Number.isInteger(c.cutPoint) ? Math.min(c.durationFrames - 1, Math.max(1, c.cutPoint)) : c.durationFrames / 2;
|
|
498
571
|
const shift = (pre === 0 ? 1 : -1) * cp;
|
|
572
|
+
// The shift must leave BOTH legs with positive duration — a leg
|
|
573
|
+
// shorter than the boundary shift cannot host its half of the span
|
|
574
|
+
// (and a zero-length survivor would re-create the inert edge form).
|
|
575
|
+
if (c.incomingCutRef.durationFrames - shift <= 0 || prev.cutRef.durationFrames + shift <= 0) {
|
|
576
|
+
droppedTransitions.push({
|
|
577
|
+
index: c.index, type: c.type, duration: c.rawDuration,
|
|
578
|
+
reason: `a leg is shorter than the ${Math.abs(shift)}f boundary shift the edge-aligned span requires`,
|
|
579
|
+
});
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
499
582
|
prev.cutRef.durationFrames += shift;
|
|
500
583
|
c.incomingCutRef.startFrame += shift;
|
|
501
584
|
c.incomingCutRef.srcIn += shift;
|
|
@@ -563,8 +646,15 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
563
646
|
}))
|
|
564
647
|
.filter((m) => m.frame >= (preserveStartTimecode ? startFrame : DEFAULT_ORIGIN));
|
|
565
648
|
|
|
649
|
+
// BL legs with surviving extent (a fade-in's zero-length slug GROWS through
|
|
650
|
+
// the boundary shift; one that never met a dissolve stays zero and drops
|
|
651
|
+
// out). srcIn was only scaffolding for the shift math — strip it.
|
|
652
|
+
const elements = blackLegs
|
|
653
|
+
.filter((el) => el.durationFrames > 0)
|
|
654
|
+
.map(({ srcIn: _srcIn, ...rest }) => rest);
|
|
655
|
+
|
|
566
656
|
return {
|
|
567
|
-
spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(markers.length ? { markers } : {}), ...(transitions.length ? { transitions } : {}) },
|
|
657
|
+
spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(markers.length ? { markers } : {}), ...(transitions.length ? { transitions } : {}), ...(elements.length ? { elements } : {}) },
|
|
568
658
|
report: {
|
|
569
659
|
videoEvents: vids.length,
|
|
570
660
|
sources: media.length,
|
|
@@ -573,7 +663,10 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
573
663
|
audioChannelLegsMerged,
|
|
574
664
|
authoredAudioEvents: audioPlacements.length,
|
|
575
665
|
audioRetimesSkipped,
|
|
576
|
-
|
|
666
|
+
...(elements.length || audioBlackLegsSkipped ? {
|
|
667
|
+
blackLegs: { authoredGenerators: elements.length, audioSilenceLegsSkipped: audioBlackLegsSkipped },
|
|
668
|
+
} : {}),
|
|
669
|
+
upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1 && !isBL(pl.source)).length,
|
|
577
670
|
flattenedRetimes,
|
|
578
671
|
authoredRetimes,
|
|
579
672
|
authoredTransitions: transitions,
|
|
@@ -162,13 +162,31 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
162
162
|
// field so the bridge can author it (type strings like SMPTE_Dissolve
|
|
163
163
|
// map through the same style table as XMEML effectids).
|
|
164
164
|
let pendingTransition = null;
|
|
165
|
+
// Track start counts as black: a Transition with a Gap (or nothing) on
|
|
166
|
+
// one side is a FADE. It routes through the same BL machinery as EDL BL
|
|
167
|
+
// dissolves (E91/E92): a synthetic zero-length BL event materializes the
|
|
168
|
+
// black side, and the bridge authors a real clip↔generator dissolve —
|
|
169
|
+
// empty track renders black anyway, so the growth is render-neutral.
|
|
170
|
+
let atBlack = true;
|
|
171
|
+
const lastRate = () => opts.fps || 24;
|
|
165
172
|
for (const child of track.children || []) {
|
|
166
173
|
const schema = child.OTIO_SCHEMA || '';
|
|
167
174
|
const dur = (child.source_range && child.source_range.duration && child.source_range.duration.value) || 0;
|
|
168
175
|
const rate = (child.source_range && child.source_range.duration && child.source_range.duration.rate) || opts.fps || 24;
|
|
169
176
|
if (schema.startsWith('Gap')) {
|
|
177
|
+
if (pendingTransition) {
|
|
178
|
+
// Transition then Gap = fade-out into black: attach it to a
|
|
179
|
+
// synthetic BL leg at the junction (the bridge grows it forward).
|
|
180
|
+
events.push(evt({
|
|
181
|
+
index: idx++, track: kind, source: 'BL',
|
|
182
|
+
recIn: rec, recOut: rec, transition: pendingTransition, fps: lastRate(),
|
|
183
|
+
}));
|
|
184
|
+
pendingTransition = null;
|
|
185
|
+
} else {
|
|
186
|
+
pendingTransition = null; // a mid-timeline gap breaks a junction
|
|
187
|
+
}
|
|
170
188
|
rec += dur;
|
|
171
|
-
|
|
189
|
+
atBlack = true;
|
|
172
190
|
continue;
|
|
173
191
|
}
|
|
174
192
|
if (schema.startsWith('Transition')) {
|
|
@@ -180,6 +198,15 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
180
198
|
continue;
|
|
181
199
|
}
|
|
182
200
|
if (schema.startsWith('Clip')) {
|
|
201
|
+
if (pendingTransition && atBlack) {
|
|
202
|
+
// Gap (or track start) then Transition then Clip = fade-IN from
|
|
203
|
+
// black — the CMX zero-length-BL form, grown by the bridge.
|
|
204
|
+
events.push(evt({
|
|
205
|
+
index: idx++, track: kind, source: 'BL',
|
|
206
|
+
recIn: rec, recOut: rec, fps: rate,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
atBlack = false;
|
|
183
210
|
const startVal = (child.source_range && child.source_range.start_time && child.source_range.start_time.value) || 0;
|
|
184
211
|
// Retime via a LinearTimeWarp effect (time_scalar).
|
|
185
212
|
let speed = 100,
|
|
@@ -220,6 +247,13 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
220
247
|
rec += dur;
|
|
221
248
|
}
|
|
222
249
|
}
|
|
250
|
+
if (pendingTransition) {
|
|
251
|
+
// Transition as the LAST child = fade-out to the end of the track.
|
|
252
|
+
events.push(evt({
|
|
253
|
+
index: idx++, track: kind, source: 'BL',
|
|
254
|
+
recIn: rec, recOut: rec, transition: pendingTransition, fps: lastRate(),
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
223
257
|
}
|
|
224
258
|
return events;
|
|
225
259
|
}
|
|
@@ -289,7 +323,28 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
289
323
|
// recStart carries the transitionitem's EXPLICIT record span start
|
|
290
324
|
// (sequence-relative) so the bridge reproduces the editor's actual
|
|
291
325
|
// alignment instead of assuming centered.
|
|
292
|
-
if (incoming)
|
|
326
|
+
if (incoming) {
|
|
327
|
+
incoming.transition = { type: String(effectId), duration: e2 - s, recStart: s };
|
|
328
|
+
// No clip ENDS inside the span → nothing precedes: a fade-IN from
|
|
329
|
+
// black. Synthesize the zero-length BL leg (E91/E92) so the bridge
|
|
330
|
+
// authors a real black-to-picture dissolve instead of dropping it.
|
|
331
|
+
const outgoing = events.slice(before).find((ev) => ev.track === label && ev !== incoming && ev.recOut > s - 1 && ev.recOut <= e2);
|
|
332
|
+
if (!outgoing) {
|
|
333
|
+
events.push(evt({ index: idx++, track: label, source: 'BL', recIn: incoming.recIn, recOut: incoming.recIn, fps: seqRate }));
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// No clip STARTS inside the span: a fade-OUT into black past the
|
|
337
|
+
// last clip. Attach the transition to a synthetic BL leg at the
|
|
338
|
+
// outgoing clip's end (the bridge grows it forward).
|
|
339
|
+
const outgoing = events.slice(before).find((ev) => ev.track === label && ev.recOut > s - 1 && ev.recOut <= e2);
|
|
340
|
+
if (outgoing) {
|
|
341
|
+
events.push(evt({
|
|
342
|
+
index: idx++, track: label, source: 'BL',
|
|
343
|
+
recIn: outgoing.recOut, recOut: outgoing.recOut,
|
|
344
|
+
transition: { type: String(effectId), duration: e2 - s, recStart: s }, fps: seqRate,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
293
348
|
}
|
|
294
349
|
});
|
|
295
350
|
const atracks = media.audio && media.audio.track ? (Array.isArray(media.audio.track) ? media.audio.track : [media.audio.track]) : [];
|
|
@@ -153,7 +153,7 @@ function requirePathArg(args, key, action) {
|
|
|
153
153
|
export const drtTool = {
|
|
154
154
|
name: 'drt',
|
|
155
155
|
description:
|
|
156
|
-
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble_from_interchange (EDL/OTIO/XML/AAF + sourceMap → IMPORTABLE RENDERING native .drt in one call; retimes AUTHOR — constant speed fwd/rev AND zero-speed freezes (EDL M2 000.0; render-proven frozen); cross-dissolves are AUTHORED when the cut abuts with handles both sides (render-verified on 19), else dropped with reason; ledger in `conform`), assemble (spec → IMPORTABLE native-schema .drt via template-spliced real structures; pass targetAppVersion e.g. \'19.1\' for pre-21 hosts), parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index}] to drive a "which sequence?" picker), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
|
|
156
|
+
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble_from_interchange (EDL/OTIO/XML/AAF + sourceMap → IMPORTABLE RENDERING native .drt in one call; retimes AUTHOR — constant speed fwd/rev AND zero-speed freezes (EDL M2 000.0; render-proven frozen); cross-dissolves are AUTHORED when the cut abuts with handles both sides (render-verified on 19), else dropped with reason; fades AUTHOR across formats (EDL BL legs, OTIO gap-adjacent Transitions, XMEML edge transitionitems) — BL legs become Solid Color generators and the fade a real clip-to-generator dissolve, luma-ramp render-verified, while Resolve\'s OWN EDL importer drops BL dissolves silently; ledger in `conform`), assemble (spec → IMPORTABLE native-schema .drt via template-spliced real structures; pass targetAppVersion e.g. \'19.1\' for pre-21 hosts), parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index}] to drive a "which sequence?" picker), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
|
|
157
157
|
async handler({ action, args }) {
|
|
158
158
|
if (action === 'parse') {
|
|
159
159
|
const p = parseSchema.parse(requirePathArg(args, 'drtPath', 'parse'));
|
|
@@ -27,6 +27,8 @@ const {
|
|
|
27
27
|
insertClipIntoTrack,
|
|
28
28
|
replaceTrackVec,
|
|
29
29
|
getTrackVec,
|
|
30
|
+
getItemsInner,
|
|
31
|
+
setItemsInner,
|
|
30
32
|
freshDbIds,
|
|
31
33
|
} = require('./seq-surgery');
|
|
32
34
|
|
|
@@ -72,7 +74,23 @@ async function placeGenerator(drpInput, opts = {}) {
|
|
|
72
74
|
let createdTracks = 0;
|
|
73
75
|
const cloneSource = tracks[0];
|
|
74
76
|
while (tracks.length < trackIndex) { tracks.push(emptyTrackClone(cloneSource)); createdTracks += 1; }
|
|
75
|
-
|
|
77
|
+
// Insert in CHRONOLOGICAL item order, not appended: placeTransition detects
|
|
78
|
+
// junctions by listed adjacency, so a generator that precedes a clip in
|
|
79
|
+
// time (a fade-in's black slug, E91) must precede it in <Items> too.
|
|
80
|
+
// Compound-clip elements are not scanned (their nested inner <Element>s
|
|
81
|
+
// defeat the lazy regex); a generator meant to sit before one appends.
|
|
82
|
+
const track = tracks[trackIndex - 1];
|
|
83
|
+
const innerItems = getItemsInner(track);
|
|
84
|
+
const clipRe = /<Element>\s*<Sm2Ti(?:VideoClip|AudioClip|Generator)\b[\s\S]*?<\/Sm2Ti(?:VideoClip|AudioClip|Generator)>\s*<\/Element>/g;
|
|
85
|
+
let insertAt = -1;
|
|
86
|
+
let m;
|
|
87
|
+
while ((m = clipRe.exec(innerItems)) !== null) {
|
|
88
|
+
const sm = /<Start>(\d+)<\/Start>/.exec(m[0]);
|
|
89
|
+
if (sm && parseInt(sm[1], 10) > startFrame) { insertAt = m.index; break; }
|
|
90
|
+
}
|
|
91
|
+
tracks[trackIndex - 1] = insertAt >= 0
|
|
92
|
+
? setItemsInner(track, innerItems.slice(0, insertAt) + gen + innerItems.slice(insertAt))
|
|
93
|
+
: insertClipIntoTrack(track, gen);
|
|
76
94
|
|
|
77
95
|
const xml = replaceTrackVec(seqXml, 'video', vtv, tracks);
|
|
78
96
|
zip.file(entry, xml);
|
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.151.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
package/src/utils/api_truth.py
CHANGED
|
@@ -2737,6 +2737,33 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2737
2737
|
"submit": "bug",
|
|
2738
2738
|
"mitigation": ["editorial.verify_roundtrip markersNotInExport"],
|
|
2739
2739
|
},
|
|
2740
|
+
{
|
|
2741
|
+
"symbol": "MediaPool.ImportTimelineFromFile EDL (drops BL fades)",
|
|
2742
|
+
"object": "MediaPool",
|
|
2743
|
+
"signature": "(filePath.edl, importOptions) -> Timeline",
|
|
2744
|
+
"reality": "EDL dissolves involving the BL (black) reel are dropped "
|
|
2745
|
+
"silently on import (measured on Studio 19.1.3.7, E91): a "
|
|
2746
|
+
"CMX fade-in (zero-length BL cut + D event) vanishes "
|
|
2747
|
+
"wholesale — frame 0 renders at full brightness — and a "
|
|
2748
|
+
"fade-out to a BL leg imports the BL as a Solid Color "
|
|
2749
|
+
"generator but drops the dissolve, leaving a hard cut to "
|
|
2750
|
+
"black. Import succeeds; the importer authors dissolves "
|
|
2751
|
+
"normally between two real media clips. A related law: a "
|
|
2752
|
+
"hand-authored SINGLE-SIDED transition element (span at a "
|
|
2753
|
+
"lone clip head or tail, only one neighboring item) "
|
|
2754
|
+
"refuses to import entirely — Resolve creates no timeline.",
|
|
2755
|
+
"recommended": "Conform EDLs with fades through "
|
|
2756
|
+
"drt.assemble_from_interchange: BL legs author as "
|
|
2757
|
+
"Solid Color generator elements and the fades as real "
|
|
2758
|
+
"clip-to-generator dissolves (render-verified: luma "
|
|
2759
|
+
"ramps 18->123 across a 24f fade-in and 123->16 "
|
|
2760
|
+
"across the fade-out, black tail holding 16). Audio "
|
|
2761
|
+
"BL fades (to silence) drop with a stated reason — "
|
|
2762
|
+
"there is no silence source to cross-fade against.",
|
|
2763
|
+
"tags": ["edl", "import", "transitions", "fades", "silent-failure"],
|
|
2764
|
+
"submit": "bug",
|
|
2765
|
+
"mitigation": ["drt.assemble_from_interchange black-leg authoring"],
|
|
2766
|
+
},
|
|
2740
2767
|
{
|
|
2741
2768
|
"symbol": "Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)",
|
|
2742
2769
|
"object": "Project",
|