davinci-resolve-mcp 2.148.0 → 2.150.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 +44 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +17 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +81 -6
- 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 +25 -4
- package/src/utils/api_truth.py +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
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.150.0 — fades conform: BL legs author, and Resolve's own EDL importer drops them
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **EDL fades author end-to-end** in `drt.assemble_from_interchange`: BL
|
|
10
|
+
(black) legs become Solid Color generator elements and the fades real
|
|
11
|
+
clip-to-generator dissolves. A CMX fade-in's zero-length BL slug grows
|
|
12
|
+
through the boundary-shift machinery (single-sided transitions refuse to
|
|
13
|
+
import — measured); the picture trims its head with source staying
|
|
14
|
+
record-aligned. Render-proven: luma ramps 18→123 across a 24-frame
|
|
15
|
+
fade-in and 123→16 across the fade-out, with the black tail holding.
|
|
16
|
+
Audio BL fades (to silence) drop with a stated reason — there is no
|
|
17
|
+
silence source to cross-fade against.
|
|
18
|
+
- Generator elements now insert into the track in **chronological item
|
|
19
|
+
order** (junction detection reads listed adjacency), and an edge-aligned
|
|
20
|
+
span whose leg is shorter than its boundary shift drops with a reason
|
|
21
|
+
instead of authoring a broken geometry.
|
|
22
|
+
|
|
23
|
+
### Measured (new Resolve bug, filed in api-limitations)
|
|
24
|
+
|
|
25
|
+
- **Resolve's own EDL importer silently drops BL dissolves**: the fade-in
|
|
26
|
+
vanishes wholesale (frame 0 renders full-bright) and a fade-out leaves a
|
|
27
|
+
hard cut into the Solid Color generator it creates for the BL slug. A
|
|
28
|
+
single-sided transition element refuses to import entirely.
|
|
29
|
+
|
|
30
|
+
## What's New in v2.149.0 — subtitle render truth: ExportSubtitle lies on 19.x
|
|
31
|
+
|
|
32
|
+
### Measured (new Resolve bug, filed in api-limitations)
|
|
33
|
+
|
|
34
|
+
- **`SetRenderSettings` subtitle keys are accepted-and-inert on 19.x**: with
|
|
35
|
+
authored subtitle cues readback-verified and the track enabled,
|
|
36
|
+
`ExportSubtitle: true` + every `SubtitleFormat` mode (`BurnIn`,
|
|
37
|
+
`SeparateFile`, `EmbeddedCaptions`) returns True — and the renders carry no
|
|
38
|
+
burned-in pixels, no sidecar file, and no caption stream. The keys are
|
|
39
|
+
documented in the Resolve 21 API reference. Quirk: `ExportSubtitle` alone
|
|
40
|
+
returns False; the pair returns True.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **`render.set_settings` warns on pre-21 hosts** when the subtitle-delivery
|
|
45
|
+
keys are set, naming the measured inertness and the verification steps —
|
|
46
|
+
the accepted-then-ignored warning pattern that already covers
|
|
47
|
+
`AddFrameHandles` under `UseFullExtents`.
|
|
48
|
+
|
|
5
49
|
## What's New in v2.148.0 — the audio ceiling doubles: A1–A16
|
|
6
50
|
|
|
7
51
|
### Changed
|
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.150.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
|
|
@@ -687,3 +687,19 @@ values, or automation-hostile modal prompts.
|
|
|
687
687
|
- **Behavior:** Timeline markers present and readable through the marker API do not appear in the exported .otio at all — the OTIO Marker schema exists and Resolve's importer reads it, but the exporter writes none (measured on Studio 19.1.3.7: two markers read back at frames 12/72; the export carried zero). Any marker-fidelity QC built on an OTIO re-export silently sees an unmarked timeline.
|
|
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
|
+
|
|
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
|
+
|
|
699
|
+
### Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)
|
|
700
|
+
|
|
701
|
+
- **Object:** `Project`
|
|
702
|
+
- **Signature:** `({'ExportSubtitle': bool, 'SubtitleFormat': str}) -> bool`
|
|
703
|
+
- **Behavior:** On Studio 19.1.3.7 the subtitle-delivery keys documented in the Resolve 21 API reference are accepted and fully inert: SetRenderSettings returns True for all three SubtitleFormat modes ('BurnIn', 'SeparateFile', 'EmbeddedCaptions'), and the renders carry no burned-in pixels (frame-extract verified), no sidecar subtitle file, and no embedded caption track (stream-probe verified) — with the subtitle cues readback-verified on the timeline and the subtitle track enabled. Quirk: ExportSubtitle alone returns False; the pair returns True. All three outputs are stream-identical to a no-subtitle render.
|
|
704
|
+
- **Workaround / current handling:** Do not trust a True return for subtitle delivery on a pre-21 host — verify the output (extract a frame for burn-in, list the target directory for a sidecar, ffprobe streams for embedded captions). On 19.x subtitle export requires the UI render page. render.set_settings warns when these keys are set on a pre-21 host.
|
|
705
|
+
- **Tags:** render, subtitle, burn-in, silent-failure, version-gated
|
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.150.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,40 @@ 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
|
+
transitionCandidates.push({
|
|
317
|
+
atFrame: recIn, durationFrames: d, track: vTrackEarly, pre,
|
|
318
|
+
explicitSpan: e.transition.alignment != null,
|
|
319
|
+
index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
|
|
320
|
+
source: e.source, srcIn: Infinity, incomingCutRef: el, cutPoint: e.transition.cutPoint,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: Infinity, durationFrames, track: vTrackEarly, cutRef: el });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
292
326
|
if (durationFrames <= 0) continue;
|
|
293
|
-
const vTrack =
|
|
327
|
+
const vTrack = vTrackEarly;
|
|
294
328
|
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), ...(vTrack > 1 ? { track: vTrack } : {}) };
|
|
295
329
|
// CLIP markers from the turnover (OTIO clip markers, XMEML clipitem
|
|
296
330
|
// <marker>s) become ITEM markers on the cut (frames clip-relative,
|
|
@@ -363,7 +397,21 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
363
397
|
const audioPlacements = [];
|
|
364
398
|
const audioRetimesSkipped = [];
|
|
365
399
|
const audioTransCandidates = [];
|
|
400
|
+
let audioBlackLegsSkipped = 0;
|
|
366
401
|
for (const e of auds) {
|
|
402
|
+
if (isBL(e.source)) {
|
|
403
|
+
// Audio BL = silence, which an empty track already is. A fade to/from
|
|
404
|
+
// it has no authorable form (no silence source to cross-fade against);
|
|
405
|
+
// the level steps at the cut instead of ramping — stated, not silent.
|
|
406
|
+
audioBlackLegsSkipped += 1;
|
|
407
|
+
if (e.transition) {
|
|
408
|
+
droppedTransitions.push({
|
|
409
|
+
index: e.index, type: e.transition.type, duration: e.transition.duration, trackType: 'audio',
|
|
410
|
+
reason: 'audio fade to/from BL (silence) — no silence source to cross-fade against; the level steps at the cut',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
367
415
|
const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
|
|
368
416
|
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
369
417
|
const durationFrames = recOut - recIn;
|
|
@@ -408,7 +456,10 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
408
456
|
|
|
409
457
|
// Overlap is judged PER VIDEO TRACK — V2 stacking over V1 is legitimate
|
|
410
458
|
// conform geometry (render-verified: an upper-track clip covers the lower).
|
|
411
|
-
|
|
459
|
+
// end-tiebreak keeps a zero-length BL placement AHEAD of the picture leg
|
|
460
|
+
// that starts at the same frame (fade-in form) — start-only sorting would
|
|
461
|
+
// read them as an overlap.
|
|
462
|
+
placements.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
412
463
|
const byTrack = new Map();
|
|
413
464
|
for (const pl of placements) {
|
|
414
465
|
if (!byTrack.has(pl.track)) byTrack.set(pl.track, []);
|
|
@@ -457,10 +508,14 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
457
508
|
// FULL-duration outgoing tail.
|
|
458
509
|
const pre = c.pre ?? c.durationFrames / 2;
|
|
459
510
|
const post = c.durationFrames - pre;
|
|
511
|
+
// A BL side has infinite handles: a Solid Color generator extends freely
|
|
512
|
+
// in both directions (srcIn is already Infinity on BL candidates).
|
|
460
513
|
const bHandle = c.srcIn >= pre;
|
|
461
514
|
const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
|
|
462
515
|
const aFrames = aSpec && Number(aSpec.frameCount);
|
|
463
|
-
const aHandle =
|
|
516
|
+
const aHandle = isBL(prev.source)
|
|
517
|
+
? true
|
|
518
|
+
: (Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false);
|
|
464
519
|
if (!bHandle || !aHandle) {
|
|
465
520
|
droppedTransitions.push({
|
|
466
521
|
index: c.index, type: c.type, duration: c.rawDuration,
|
|
@@ -496,6 +551,16 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
496
551
|
// span, the edge law), else center.
|
|
497
552
|
const cp = Number.isInteger(c.cutPoint) ? Math.min(c.durationFrames - 1, Math.max(1, c.cutPoint)) : c.durationFrames / 2;
|
|
498
553
|
const shift = (pre === 0 ? 1 : -1) * cp;
|
|
554
|
+
// The shift must leave BOTH legs with positive duration — a leg
|
|
555
|
+
// shorter than the boundary shift cannot host its half of the span
|
|
556
|
+
// (and a zero-length survivor would re-create the inert edge form).
|
|
557
|
+
if (c.incomingCutRef.durationFrames - shift <= 0 || prev.cutRef.durationFrames + shift <= 0) {
|
|
558
|
+
droppedTransitions.push({
|
|
559
|
+
index: c.index, type: c.type, duration: c.rawDuration,
|
|
560
|
+
reason: `a leg is shorter than the ${Math.abs(shift)}f boundary shift the edge-aligned span requires`,
|
|
561
|
+
});
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
499
564
|
prev.cutRef.durationFrames += shift;
|
|
500
565
|
c.incomingCutRef.startFrame += shift;
|
|
501
566
|
c.incomingCutRef.srcIn += shift;
|
|
@@ -563,8 +628,15 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
563
628
|
}))
|
|
564
629
|
.filter((m) => m.frame >= (preserveStartTimecode ? startFrame : DEFAULT_ORIGIN));
|
|
565
630
|
|
|
631
|
+
// BL legs with surviving extent (a fade-in's zero-length slug GROWS through
|
|
632
|
+
// the boundary shift; one that never met a dissolve stays zero and drops
|
|
633
|
+
// out). srcIn was only scaffolding for the shift math — strip it.
|
|
634
|
+
const elements = blackLegs
|
|
635
|
+
.filter((el) => el.durationFrames > 0)
|
|
636
|
+
.map(({ srcIn: _srcIn, ...rest }) => rest);
|
|
637
|
+
|
|
566
638
|
return {
|
|
567
|
-
spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(markers.length ? { markers } : {}), ...(transitions.length ? { transitions } : {}) },
|
|
639
|
+
spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(markers.length ? { markers } : {}), ...(transitions.length ? { transitions } : {}), ...(elements.length ? { elements } : {}) },
|
|
568
640
|
report: {
|
|
569
641
|
videoEvents: vids.length,
|
|
570
642
|
sources: media.length,
|
|
@@ -573,7 +645,10 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
573
645
|
audioChannelLegsMerged,
|
|
574
646
|
authoredAudioEvents: audioPlacements.length,
|
|
575
647
|
audioRetimesSkipped,
|
|
576
|
-
|
|
648
|
+
...(elements.length || audioBlackLegsSkipped ? {
|
|
649
|
+
blackLegs: { authoredGenerators: elements.length, audioSilenceLegsSkipped: audioBlackLegsSkipped },
|
|
650
|
+
} : {}),
|
|
651
|
+
upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1 && !isBL(pl.source)).length,
|
|
577
652
|
flattenedRetimes,
|
|
578
653
|
authoredRetimes,
|
|
579
654
|
authoredTransitions: transitions,
|
|
@@ -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; EDL BL fades AUTHOR too — 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.150.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.150.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -18730,7 +18730,7 @@ def _render_preset_pin(proj, preset_name: str):
|
|
|
18730
18730
|
return {"preset": preset_name, "loaded": True}, None
|
|
18731
18731
|
|
|
18732
18732
|
|
|
18733
|
-
def _render_settings_warnings(settings: Dict[str, Any]):
|
|
18733
|
+
def _render_settings_warnings(settings: Dict[str, Any], version_major: Optional[int] = None):
|
|
18734
18734
|
"""Inter-key combinations Resolve accepts and then silently ignores.
|
|
18735
18735
|
|
|
18736
18736
|
SetRenderSettings returns True for these, so nothing downstream would ever
|
|
@@ -18745,6 +18745,17 @@ def _render_settings_warnings(settings: Dict[str, Any]):
|
|
|
18745
18745
|
"AddFrameHandles is ignored while UseFullExtents is true: Resolve renders the "
|
|
18746
18746
|
"clip's full extents and the handle count silently does nothing. Drop one of the two."
|
|
18747
18747
|
)
|
|
18748
|
+
if ("ExportSubtitle" in settings or "SubtitleFormat" in settings) and (
|
|
18749
|
+
version_major is not None and version_major < 21
|
|
18750
|
+
):
|
|
18751
|
+
warnings.append(
|
|
18752
|
+
"ExportSubtitle/SubtitleFormat are INERT on this Resolve generation: measured on "
|
|
18753
|
+
"Studio 19.1.3.7, SetRenderSettings returns True for all three SubtitleFormat modes "
|
|
18754
|
+
"and the render carries no burn-in, no sidecar file, and no embedded caption track "
|
|
18755
|
+
"(cues readback-verified, subtitle track enabled). The keys are documented in the "
|
|
18756
|
+
"Resolve 21 API reference. Verify the output (frame extract / sidecar check) before "
|
|
18757
|
+
"claiming caption delivery; on 19.x burn-in requires the UI render page."
|
|
18758
|
+
)
|
|
18748
18759
|
return warnings
|
|
18749
18760
|
|
|
18750
18761
|
|
|
@@ -19283,6 +19294,12 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
19283
19294
|
"Same as project", or "None"). AddFrameHandles is ignored while
|
|
19284
19295
|
UseFullExtents is true — the call still succeeds, so that pairing
|
|
19285
19296
|
comes back in warnings rather than silently doing nothing.
|
|
19297
|
+
ExportSubtitle (bool) + SubtitleFormat ("BurnIn" | "SeparateFile" |
|
|
19298
|
+
"EmbeddedCaptions") are documented for Resolve 21 but measured INERT
|
|
19299
|
+
on 19.x — SetRenderSettings returns True and the render carries no
|
|
19300
|
+
burn-in, sidecar, or caption track — so setting them on a pre-21
|
|
19301
|
+
host comes back in warnings; verify the output before claiming
|
|
19302
|
+
caption delivery.
|
|
19286
19303
|
list_presets() -> {presets}
|
|
19287
19304
|
load_preset(name) -> {success}
|
|
19288
19305
|
save_preset(name) -> {success}
|
|
@@ -19577,8 +19594,12 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
19577
19594
|
if ignored_settings:
|
|
19578
19595
|
result["ignored_settings"] = ignored_settings
|
|
19579
19596
|
# Accepted-then-ignored key combinations (21.0.4 AddFrameHandles under
|
|
19580
|
-
# UseFullExtents) read as a clean success without this.
|
|
19581
|
-
|
|
19597
|
+
# UseFullExtents; 19.x ExportSubtitle) read as a clean success without this.
|
|
19598
|
+
try:
|
|
19599
|
+
_ver_major = int((get_resolve().GetVersion() or [0])[0])
|
|
19600
|
+
except Exception:
|
|
19601
|
+
_ver_major = None
|
|
19602
|
+
warnings = _render_settings_warnings(settings, version_major=_ver_major)
|
|
19582
19603
|
if warnings:
|
|
19583
19604
|
result["warnings"] = warnings
|
|
19584
19605
|
return result
|
package/src/utils/api_truth.py
CHANGED
|
@@ -2737,6 +2737,60 @@ 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
|
+
},
|
|
2767
|
+
{
|
|
2768
|
+
"symbol": "Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)",
|
|
2769
|
+
"object": "Project",
|
|
2770
|
+
"signature": "({'ExportSubtitle': bool, 'SubtitleFormat': str}) -> bool",
|
|
2771
|
+
"reality": "On Studio 19.1.3.7 the subtitle-delivery keys documented "
|
|
2772
|
+
"in the Resolve 21 API reference are accepted and fully "
|
|
2773
|
+
"inert: SetRenderSettings returns True for all three "
|
|
2774
|
+
"SubtitleFormat modes ('BurnIn', 'SeparateFile', "
|
|
2775
|
+
"'EmbeddedCaptions'), and the renders carry no burned-in "
|
|
2776
|
+
"pixels (frame-extract verified), no sidecar subtitle "
|
|
2777
|
+
"file, and no embedded caption track (stream-probe "
|
|
2778
|
+
"verified) — with the subtitle cues readback-verified on "
|
|
2779
|
+
"the timeline and the subtitle track enabled. Quirk: "
|
|
2780
|
+
"ExportSubtitle alone returns False; the pair returns "
|
|
2781
|
+
"True. All three outputs are stream-identical to a "
|
|
2782
|
+
"no-subtitle render.",
|
|
2783
|
+
"recommended": "Do not trust a True return for subtitle delivery on "
|
|
2784
|
+
"a pre-21 host — verify the output (extract a frame "
|
|
2785
|
+
"for burn-in, list the target directory for a "
|
|
2786
|
+
"sidecar, ffprobe streams for embedded captions). On "
|
|
2787
|
+
"19.x subtitle export requires the UI render page. "
|
|
2788
|
+
"render.set_settings warns when these keys are set on "
|
|
2789
|
+
"a pre-21 host.",
|
|
2790
|
+
"tags": ["render", "subtitle", "burn-in", "silent-failure", "version-gated"],
|
|
2791
|
+
"submit": "bug",
|
|
2792
|
+
"mitigation": ["render.set_settings warnings"],
|
|
2793
|
+
},
|
|
2740
2794
|
]
|
|
2741
2795
|
|
|
2742
2796
|
|