davinci-resolve-mcp 2.142.1 → 2.144.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 +39 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/native-drt-authoring.md +1 -0
- 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 +9 -0
- package/resolve-advanced/server/author-interchange.mjs +36 -12
- package/resolve-advanced/server/editorial.mjs +15 -6
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +45 -0
- package/resolve-advanced/vendor/drp-format/place-transition.js +6 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
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.144.0 — turnover clip markers ride the items
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **OTIO clip markers and FCP7-XMEML clipitem `<marker>`s route to ITEM
|
|
10
|
+
markers** on their cuts through `assemble_from_interchange` (frames
|
|
11
|
+
clip-relative, colors mapped, notes preserved; markers on trimmed-away
|
|
12
|
+
material drop rather than refuse). Track-level markers still author as
|
|
13
|
+
timeline markers. Built on v2.143's `Sm2TiItemLockableBlob` authoring —
|
|
14
|
+
the only .drt carrier for clip markers, since Resolve's own exporter
|
|
15
|
+
drops them.
|
|
16
|
+
|
|
17
|
+
## What's New in v2.143.0 — item markers, and two refinements
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Item-level markers (clip locators) authored offline** — `cuts[].markers`
|
|
22
|
+
(frames item-relative), on video AND explicit-audio items. Found by a raw
|
|
23
|
+
byte-hunt in a live Project.db: item markers ride an
|
|
24
|
+
`Sm2TiItemLockableBlob` in `project.xml`'s LocableBlobSet — the same wire
|
|
25
|
+
codec as timeline markers with the clip's DbId as owner. The kicker, filed
|
|
26
|
+
as a new Resolve bug in api-limitations: **Resolve's own EXPORT_DRT drops
|
|
27
|
+
item-marker blobs entirely** (even after SaveProject), while
|
|
28
|
+
`ImportTimelineFromFile` accepts an authored one — this authoring path is
|
|
29
|
+
the only way a .drt carries clip markers. Live-verified readback on video
|
|
30
|
+
and A3 audio items through the tool layer.
|
|
31
|
+
- **AAF `CutPoint` honored** — when an AAF transition names where the
|
|
32
|
+
notional cut sits within the overlap, the boundary shift lands there
|
|
33
|
+
(clamped strictly inside the span per the edge law) instead of centering.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **`fade-to-color` demoted from the transition style set** — on the
|
|
38
|
+
dissolve skeleton it is duration/direction-erratic (a 24f junction
|
|
39
|
+
rendered a ~77 plateau; a 48f one rendered a single black frame then a
|
|
40
|
+
hard cut), and its real parameter blob is unharvestable while the XMEML
|
|
41
|
+
importer stays inert. Turnover Fade-To-Colors now map to `dip`, which is
|
|
42
|
+
duration-stable (clean symmetric valley measured at both 24f and 48f).
|
|
43
|
+
|
|
5
44
|
## What's New in v2.142.1 — the AAF leg joins the span fix
|
|
6
45
|
|
|
7
46
|
### 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.144.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -53,6 +53,7 @@ window. `render.verify_output` covers the container-level checks.
|
|
|
53
53
|
| Built-in generators | `elements: [{type:'generator', generatorName}]` | v2.110 |
|
|
54
54
|
| Custom start timecode | `spec.startFrame` / `preserveStartTimecode` | v2.117 |
|
|
55
55
|
| Timeline markers | `spec.markers` (16 colors, notes, durations, customData) | v2.118 |
|
|
56
|
+
| Item markers (clip locators) | `cuts[].markers` (frames item-relative; video + audio items) | v2.143 |
|
|
56
57
|
| Turnover markers | EDL `* LOC:` locators + OTIO markers → authored | v2.119 |
|
|
57
58
|
| TC-bearing sources (AAF route) | native clip capture (`MediaStartTime`), channel-leg merge | v2.120–2.122 |
|
|
58
59
|
| Subtitles | `spec.subtitles` / `spec.subtitlesSrt` (raw SRT) | v2.128 |
|
|
@@ -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, 47 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
|
|
@@ -671,3 +671,11 @@ values, or automation-hostile modal prompts.
|
|
|
671
671
|
- **Behavior:** Video <transitionitem>s imported from an FCP7 XMEML land as real Sm2TiTransition elements that READ BACK through the item APIs but render INERT: the outgoing clip plays through the transition window and hard-cuts at its end. Measured on Studio 19.1.3.7 with both a plain Cross Dissolve and a Dip to Color Dissolve (midpoint frames byte-matched the outgoing clip; no blend, no dip). The same transition elements authored offline with a correct FieldsBlob render perfectly, and EDL-imported dissolves/wipes also render — the defect is specific to the XMEML import path's element construction.
|
|
672
672
|
- **Workaround / current handling:** Do not trust an XMEML-imported timeline's transitions without a render probe at a junction midpoint. To conform an XMEML turnover with working transitions, route it through drt.assemble_from_interchange (format 'xml'), which authors render-verified dissolves, wipes, and the dissolve-family styles from the same <transitionitem> data.
|
|
673
673
|
- **Tags:** timeline, import, xmeml, transition, readback, silent-failure
|
|
674
|
+
|
|
675
|
+
### Timeline.Export EXPORT_DRT (drops timeline-item markers)
|
|
676
|
+
|
|
677
|
+
- **Object:** `Timeline`
|
|
678
|
+
- **Signature:** `(filePath, EXPORT_DRT) -> bool`
|
|
679
|
+
- **Behavior:** Item-level markers (clip locators) are not serialized into the exported .drt at all. They live in the project database as Sm2TiItemLockableBlob rows (same wire codec as timeline markers, BlobOwner = the item's DbId — located by byte search in a live Project.db), and readback via TimelineItem.GetMarkers is fine, but the export omits the blobs even after SaveProject — measured on Studio 19.1.3.7. Asymmetrically, ImportTimelineFromFile ACCEPTS an authored Sm2TiItemLockableBlob and the markers read back perfectly.
|
|
680
|
+
- **Workaround / current handling:** Do not rely on .drt archives to carry clip markers. To deliver item markers in a .drt, author them offline (drt.assemble cuts[].markers writes the accepted blob); to preserve markers from a live timeline, read them via the marker API and re-author.
|
|
681
|
+
- **Tags:** timeline, export, drt, markers, silent-failure
|
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.144.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
|
@@ -1175,6 +1175,15 @@ def _walk_components(sequence, *, track, fps, rec, state, depth, transition=None
|
|
|
1175
1175
|
# exactly the AAF CutPoint default (centered in the overlap). The old
|
|
1176
1176
|
# centered-on-recIn placement put HALF the blend before the overlap began.
|
|
1177
1177
|
pending_transition = {"type": "dissolve", "duration": duration, "alignment": "start"}
|
|
1178
|
+
# CutPoint (optional): where the notional cut sits WITHIN the
|
|
1179
|
+
# overlap. The bridge shifts the reshaped clip boundary there
|
|
1180
|
+
# instead of the centered default, preserving Avid's intent.
|
|
1181
|
+
try:
|
|
1182
|
+
cut_point = comp["CutPoint"].value
|
|
1183
|
+
if cut_point is not None:
|
|
1184
|
+
pending_transition["cutPoint"] = int(cut_point)
|
|
1185
|
+
except (KeyError, AttributeError, TypeError, ValueError):
|
|
1186
|
+
pass
|
|
1178
1187
|
# Rewind: the next component overlaps the previous one by `duration`.
|
|
1179
1188
|
# Clamped at this sequence's own start — a leading transition has no
|
|
1180
1189
|
# preceding material to overlap, and a negative record position would
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { drt } from './libs.mjs';
|
|
12
12
|
|
|
13
|
+
const COLOR_MAP = {
|
|
14
|
+
blue: 'Blue', cyan: 'Cyan', green: 'Green', yellow: 'Yellow', red: 'Red',
|
|
15
|
+
pink: 'Pink', purple: 'Purple', magenta: 'Fuchsia', fuchsia: 'Fuchsia',
|
|
16
|
+
rose: 'Rose', lavender: 'Lavender', sky: 'Sky', mint: 'Mint',
|
|
17
|
+
lemon: 'Lemon', sand: 'Sand', cocoa: 'Cocoa', cream: 'Cream',
|
|
18
|
+
orange: 'Sand', white: 'Cream', black: 'Cocoa',
|
|
19
|
+
};
|
|
20
|
+
|
|
13
21
|
const pad = (n, w = 2) => String(Math.max(0, Math.floor(n))).padStart(w, '0');
|
|
14
22
|
|
|
15
23
|
/** frames → CMX timecode at fps (non-drop). */
|
|
@@ -284,6 +292,21 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
284
292
|
if (durationFrames <= 0) continue;
|
|
285
293
|
const vTrack = trackNum(e.track);
|
|
286
294
|
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), ...(vTrack > 1 ? { track: vTrack } : {}) };
|
|
295
|
+
// CLIP markers from the turnover (OTIO clip markers, XMEML clipitem
|
|
296
|
+
// <marker>s) become ITEM markers on the cut (frames clip-relative,
|
|
297
|
+
// nominal→timeline converted; anything landing outside the cut is
|
|
298
|
+
// dropped rather than refused — a marker on trimmed-away material).
|
|
299
|
+
if (Array.isArray(e.itemMarkers) && e.itemMarkers.length) {
|
|
300
|
+
const ims = e.itemMarkers
|
|
301
|
+
.map((m) => ({
|
|
302
|
+
frame: Math.round(toTl(m.frame ?? 0, e.fps)),
|
|
303
|
+
color: COLOR_MAP[String(m.color || '').toLowerCase()] || 'Blue',
|
|
304
|
+
...(m.name ? { name: m.name } : {}),
|
|
305
|
+
...(m.note ? { note: m.note } : {}),
|
|
306
|
+
}))
|
|
307
|
+
.filter((m) => m.frame >= 0 && m.frame < durationFrames);
|
|
308
|
+
if (ims.length) cut.markers = ims;
|
|
309
|
+
}
|
|
287
310
|
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
288
311
|
const spd = Math.abs(e.speed ?? 100);
|
|
289
312
|
if (!(spd > 0)) {
|
|
@@ -323,7 +346,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
323
346
|
atFrame: recIn, durationFrames: d, track: vTrack, pre,
|
|
324
347
|
explicitSpan: e.transition.alignment != null || e.transition.inOffset != null || e.transition.recStart != null,
|
|
325
348
|
index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
|
|
326
|
-
source: e.source, srcIn: cut.srcIn, incomingCutRef: cut,
|
|
349
|
+
source: e.source, srcIn: cut.srcIn, incomingCutRef: cut, cutPoint: e.transition.cutPoint,
|
|
327
350
|
});
|
|
328
351
|
}
|
|
329
352
|
placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: cut.srcIn, durationFrames, track: vTrack, cutRef: cut });
|
|
@@ -360,7 +383,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
360
383
|
atFrame: recIn, durationFrames: d, track, pre,
|
|
361
384
|
explicitSpan: e.transition.alignment != null || e.transition.inOffset != null,
|
|
362
385
|
index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
|
|
363
|
-
source: e.source, srcIn: cut.srcIn, incomingCutRef: cut,
|
|
386
|
+
source: e.source, srcIn: cut.srcIn, incomingCutRef: cut, cutPoint: e.transition.cutPoint,
|
|
364
387
|
});
|
|
365
388
|
}
|
|
366
389
|
audioPlacements.push({ start: recIn, end: recOut, index: e.index, track, source: e.source, srcIn: cut.srcIn, durationFrames, cutRef: cut });
|
|
@@ -457,7 +480,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
457
480
|
else if (/dip to color/i.test(raw)) kind = 'dip';
|
|
458
481
|
else if (/non-additive/i.test(raw)) kind = 'non-additive';
|
|
459
482
|
else if (/additive/i.test(raw)) kind = 'additive';
|
|
460
|
-
else if (/fade to color/i.test(raw)) kind = 'fade-to-color
|
|
483
|
+
else if (/fade to color/i.test(raw)) kind = 'dip'; // fade-to-color is erratic on the skeleton (measured); dip is the verified fade-to-black-and-back
|
|
461
484
|
else if (/smooth cut/i.test(raw)) kind = 'smooth-cut';
|
|
462
485
|
// EDGE LAW (E73, measured): the clip boundary must sit STRICTLY INSIDE
|
|
463
486
|
// the transition span — an edge-aligned span (Start == boundary) renders
|
|
@@ -468,7 +491,11 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
468
491
|
let atFrame = c.atFrame;
|
|
469
492
|
const spanStartAbs = c.atFrame - pre;
|
|
470
493
|
if (c.explicitSpan && (pre === 0 || pre === c.durationFrames)) {
|
|
471
|
-
|
|
494
|
+
// AAF CutPoint: the notional cut's offset within the overlap — shift
|
|
495
|
+
// the reshaped boundary there when given (clamped strictly inside the
|
|
496
|
+
// span, the edge law), else center.
|
|
497
|
+
const cp = Number.isInteger(c.cutPoint) ? Math.min(c.durationFrames - 1, Math.max(1, c.cutPoint)) : c.durationFrames / 2;
|
|
498
|
+
const shift = (pre === 0 ? 1 : -1) * cp;
|
|
472
499
|
prev.cutRef.durationFrames += shift;
|
|
473
500
|
c.incomingCutRef.startFrame += shift;
|
|
474
501
|
c.incomingCutRef.srcIn += shift;
|
|
@@ -507,7 +534,11 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
507
534
|
let atFrame = c.atFrame;
|
|
508
535
|
const spanStartAbs = c.atFrame - pre;
|
|
509
536
|
if (c.explicitSpan && (pre === 0 || pre === c.durationFrames)) {
|
|
510
|
-
|
|
537
|
+
// AAF CutPoint: the notional cut's offset within the overlap — shift
|
|
538
|
+
// the reshaped boundary there when given (clamped strictly inside the
|
|
539
|
+
// span, the edge law), else center.
|
|
540
|
+
const cp = Number.isInteger(c.cutPoint) ? Math.min(c.durationFrames - 1, Math.max(1, c.cutPoint)) : c.durationFrames / 2;
|
|
541
|
+
const shift = (pre === 0 ? 1 : -1) * cp;
|
|
511
542
|
prev.cutRef.durationFrames += shift;
|
|
512
543
|
c.incomingCutRef.startFrame += shift;
|
|
513
544
|
c.incomingCutRef.srcIn += shift;
|
|
@@ -524,13 +555,6 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
524
555
|
// Turnover markers (EDL * LOC: locators, OTIO Marker objects) → authored
|
|
525
556
|
// timeline markers. Interchange colors map to the measured Resolve names;
|
|
526
557
|
// unknown colors fall back to Blue. Frames are timeline-absolute like cuts.
|
|
527
|
-
const COLOR_MAP = {
|
|
528
|
-
blue: 'Blue', cyan: 'Cyan', green: 'Green', yellow: 'Yellow', red: 'Red',
|
|
529
|
-
pink: 'Pink', purple: 'Purple', magenta: 'Fuchsia', fuchsia: 'Fuchsia',
|
|
530
|
-
rose: 'Rose', lavender: 'Lavender', sky: 'Sky', mint: 'Mint',
|
|
531
|
-
lemon: 'Lemon', sand: 'Sand', cocoa: 'Cocoa', cream: 'Cream',
|
|
532
|
-
orange: 'Sand', white: 'Cream', black: 'Cocoa',
|
|
533
|
-
};
|
|
534
558
|
const markers = markerEvents
|
|
535
559
|
.map((e) => ({
|
|
536
560
|
frame: ORIGIN + (toTl(e.recIn, e.fps) - minRec),
|
|
@@ -59,6 +59,7 @@ function evt(o) {
|
|
|
59
59
|
speed: o.speed ?? 100,
|
|
60
60
|
reverse: o.reverse ?? false,
|
|
61
61
|
transition: o.transition || null,
|
|
62
|
+
...(o.itemMarkers !== undefined ? { itemMarkers: o.itemMarkers } : {}),
|
|
62
63
|
fps: o.fps ?? null,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
@@ -190,13 +191,14 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
const src = (child.media_reference && (child.media_reference.target_url || child.media_reference.name)) || child.name || 'UNKNOWN';
|
|
194
|
+
// CLIP markers belong to the ITEM, not the sequence — carried as
|
|
195
|
+
// itemMarkers (frames CLIP-relative) so the bridge authors them as
|
|
196
|
+
// Sm2TiItemLockableBlobs (E80). Track-level markers above remain
|
|
197
|
+
// timeline markers.
|
|
198
|
+
const itemMarkers = [];
|
|
193
199
|
for (const mk of child.markers || []) {
|
|
194
200
|
const mrStart = (mk.marked_range && mk.marked_range.start_time && mk.marked_range.start_time.value) || 0;
|
|
195
|
-
|
|
196
|
-
index: idx++, track: 'MARKER', source: '',
|
|
197
|
-
recIn: rec + (mrStart - startVal), recOut: null,
|
|
198
|
-
name: mk.name || undefined, color: mk.color || undefined, fps: rate,
|
|
199
|
-
}));
|
|
201
|
+
itemMarkers.push({ frame: mrStart - startVal, name: mk.name || undefined, color: mk.color || undefined });
|
|
200
202
|
}
|
|
201
203
|
events.push(
|
|
202
204
|
evt({
|
|
@@ -210,6 +212,7 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
210
212
|
speed: Math.abs(speed),
|
|
211
213
|
reverse,
|
|
212
214
|
transition: pendingTransition,
|
|
215
|
+
itemMarkers: itemMarkers.length ? itemMarkers : undefined,
|
|
213
216
|
fps: rate,
|
|
214
217
|
}),
|
|
215
218
|
);
|
|
@@ -252,7 +255,13 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
257
|
if (Number.isFinite(start) && Number.isFinite(end)) {
|
|
255
|
-
|
|
258
|
+
// FCP7 clipitem <marker> children are CLIP markers (frames relative
|
|
259
|
+
// to the clip's own <in>) — routed to item markers like OTIO's (E81).
|
|
260
|
+
const mks = it.marker ? (Array.isArray(it.marker) ? it.marker : [it.marker]) : [];
|
|
261
|
+
const itemMarkers = mks
|
|
262
|
+
.filter((mk) => Number.isFinite(Number(mk.in)))
|
|
263
|
+
.map((mk) => ({ frame: Number(mk.in) - inF, name: mk.name != null ? String(mk.name) : undefined, note: mk.comment != null ? String(mk.comment) : undefined }));
|
|
264
|
+
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 }));
|
|
256
265
|
}
|
|
257
266
|
}
|
|
258
267
|
};
|
|
@@ -51,7 +51,7 @@ const assembleSchema = z.object({
|
|
|
51
51
|
spec: z
|
|
52
52
|
.object({})
|
|
53
53
|
.passthrough()
|
|
54
|
-
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),freeze? (true = hold source frame srcIn for the whole cut; video-only; render-proven frozen on 19 via freezedetect),ramp? ([{durationFrames,speed},...] >=2 LINEAR segments from the cut head, srcIn honored; video-only; render-proven cadence on 19 — eased/curved ramps are NOT authorable, an interp!=0 keyframe crashes Resolve on import (measured)),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' | 'audio' cross-fade), type? ('dissolve' default | 'wipe' | 'dip' | 'additive' | '
|
|
54
|
+
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),freeze? (true = hold source frame srcIn for the whole cut; video-only; render-proven frozen on 19 via freezedetect),ramp? ([{durationFrames,speed},...] >=2 LINEAR segments from the cut head, srcIn honored; video-only; render-proven cadence on 19 — eased/curved ramps are NOT authorable, an interp!=0 keyframe crashes Resolve on import (measured)),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19),markers? (ITEM markers/clip locators: [{frame ITEM-relative, color?, name?, note?, duration?, customData?}] — readback-verified on 19; NOTE Resolve's own EXPORT_DRT drops item markers, this authoring path is the only .drt carrier)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' | 'audio' cross-fade), type? ('dissolve' default | 'wipe' | 'dip' | 'additive' | 'smooth-cut' | 'non-additive' — all render-verified on 19 w/ midpoint fingerprints; XMEML/EDL transition codes route automatically, and NOTE Resolve's own XMEML importer writes transitions that render INERT, so this route beats it)}], markers?: [{frame (timeline-absolute), color? (16 names), name?, note?, duration?, customData?}] (readback-verified on 19), compounds?: [{name, startFrame (parent, absolute), durationFrames, track?, cuts:[{mediaFilePath, startFrame (INNER, 0-based), durationFrames, srcIn?}], compounds?: [same, nested — frames inner-relative]}] (multiple PARALLEL compounds compose AND compounds NEST recursively — depth-2 through depth-4 playback render-verified on 19 (the old depth-2 black was a missing SequenceSetup key, fixed); inner cuts need captured templates w/ native clips), subtitles?: [{startFrame (timeline-absolute), durationFrames, text}] + subtitlesSrt? (raw SRT, cues anchor at the origin; readback-verified on 19; angle-bracket runs read as SRT markup), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}] }. startFrame is timeline-absolute (origin 86400)."),
|
|
55
55
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
56
56
|
targetAppVersion: z
|
|
57
57
|
.union([z.string(), z.number()])
|
|
@@ -209,6 +209,51 @@ async function assembleTimeline(spec = {}) {
|
|
|
209
209
|
const { buffer: baseBuffer, timelineName: tlName, startFrame } = base;
|
|
210
210
|
let buffer = baseBuffer;
|
|
211
211
|
|
|
212
|
+
// ITEM-level markers (clip locators): cuts[].markers, frames ITEM-relative.
|
|
213
|
+
// They live in project.xml's LocableBlobSet as Sm2TiItemLockableBlob —
|
|
214
|
+
// same wire codec as timeline markers, BlobOwner = the CLIP's DbId
|
|
215
|
+
// (measured E78 via a live DB byte-hunt: the .drt EXPORTER drops these
|
|
216
|
+
// blobs entirely, but the IMPORTER accepts an authored one — frames,
|
|
217
|
+
// colors, notes, durations and customData all read back through the
|
|
218
|
+
// marker API). Placed right after cuts, while the parent container is
|
|
219
|
+
// still the only one (compounds haven't run).
|
|
220
|
+
{
|
|
221
|
+
const itemMarkerCuts = [];
|
|
222
|
+
(Array.isArray(media) ? media : media ? [media] : []).forEach((src) => {
|
|
223
|
+
(src.cuts || []).forEach((cut) => {
|
|
224
|
+
if (Array.isArray(cut.markers) && cut.markers.length) itemMarkerCuts.push(cut);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
if (itemMarkerCuts.length) {
|
|
228
|
+
const { getTrackVec } = require('./seq-surgery');
|
|
229
|
+
const zipIM = await JSZip.loadAsync(buffer);
|
|
230
|
+
const seqName = Object.keys(zipIM.files).find((n) => !zipIM.files[n].dir && /SeqContainer\/.+\.xml$/.test(n));
|
|
231
|
+
const seqXml = await zipIM.file(seqName).async('string');
|
|
232
|
+
let pj = await zipIM.file('project.xml').async('string');
|
|
233
|
+
if (!/<LocableBlobSet>[\s\S]*?<\/LocableBlobSet>/.test(pj)) throw new Error('assembleTimeline: project.xml has no LocableBlobSet for item markers');
|
|
234
|
+
for (const cut of itemMarkerCuts) {
|
|
235
|
+
for (const m of cut.markers) {
|
|
236
|
+
if (!Number.isInteger(m.frame) || m.frame < 0 || m.frame >= cut.durationFrames) {
|
|
237
|
+
throw new RangeError(`assembleTimeline: item marker frame ${m.frame} outside the cut (frames are ITEM-relative, [0, ${cut.durationFrames}))`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const kind = cut.audioOnly ? 'audio' : 'video';
|
|
241
|
+
const { tracks } = getTrackVec(seqXml, kind);
|
|
242
|
+
const trackXml = tracks[(cut.track ?? 1) - 1];
|
|
243
|
+
if (!trackXml) throw new Error(`assembleTimeline: item markers — track ${cut.track ?? 1} not found`);
|
|
244
|
+
const tag = cut.audioOnly ? 'Sm2TiAudioClip' : 'Sm2TiVideoClip';
|
|
245
|
+
const re = new RegExp(`<${tag} DbId="([^"]+)">(?:(?!<\\/${tag}>)[\\s\\S])*?<Start>${cut.startFrame}<\\/Start>`);
|
|
246
|
+
const hit = trackXml.match(re);
|
|
247
|
+
if (!hit) throw new Error(`assembleTimeline: item markers — no ${kind} clip starting at ${cut.startFrame} on track ${cut.track ?? 1}`);
|
|
248
|
+
const blob = encodeTimelineMarkersBlob(cut.markers);
|
|
249
|
+
const el = `<Element>\n <Sm2TiItemLockableBlob DbId="${randomUUID()}">\n <FieldsBlob>${blob.toString('hex')}</FieldsBlob>\n <BlobOwner>${hit[1]}</BlobOwner>\n <DbSavedTime>0</DbSavedTime>\n </Sm2TiItemLockableBlob>\n </Element>\n `;
|
|
250
|
+
pj = pj.replace('</LocableBlobSet>', `${el}</LocableBlobSet>`);
|
|
251
|
+
}
|
|
252
|
+
zipIM.file('project.xml', pj);
|
|
253
|
+
buffer = await zipIM.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
212
257
|
for (const [i, el] of elements.entries()) {
|
|
213
258
|
if (!el || typeof el !== 'object') throw new TypeError(`assembleTimeline: elements[${i}] must be an object`);
|
|
214
259
|
if (el.type === 'title') {
|
|
@@ -75,10 +75,15 @@ const WIPE_FIELDS_BLOB = '00000002000000158012120000002c789c636660642016000000da
|
|
|
75
75
|
// renders INERT — so authoring through this path beats the host importer.
|
|
76
76
|
// Unknown PrettyTypes are refused, not probed: unvetted strings are exactly
|
|
77
77
|
// the class that crashed the app elsewhere (interp!=0, dangling ids).
|
|
78
|
+
// 'Fade To Color' is deliberately ABSENT: on the dissolve skeleton its
|
|
79
|
+
// behavior is duration/direction-erratic (24f rendered a ~77 plateau, 48f a
|
|
80
|
+
// single black frame then a hard cut — measured E68/E75; its real params
|
|
81
|
+
// live in an EffectFiltersBA no importer path can harvest, the XMEML
|
|
82
|
+
// importer being inert). Dip is the verified equivalent (fade to black and
|
|
83
|
+
// back, clean symmetric valley at 24f AND 48f).
|
|
78
84
|
const STYLE_PRETTY_TYPES = {
|
|
79
85
|
dip: 'Dip To Color Dissolve',
|
|
80
86
|
additive: 'Additive Dissolve',
|
|
81
|
-
'fade-to-color': 'Fade To Color',
|
|
82
87
|
'smooth-cut': 'Smooth Cut',
|
|
83
88
|
'non-additive': 'Non-Additive Dissolve',
|
|
84
89
|
};
|
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.144.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
|
@@ -2694,6 +2694,28 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2694
2694
|
"submit": "bug",
|
|
2695
2695
|
"mitigation": ["drt.assemble_from_interchange"],
|
|
2696
2696
|
},
|
|
2697
|
+
{
|
|
2698
|
+
"symbol": "Timeline.Export EXPORT_DRT (drops timeline-item markers)",
|
|
2699
|
+
"object": "Timeline",
|
|
2700
|
+
"signature": "(filePath, EXPORT_DRT) -> bool",
|
|
2701
|
+
"reality": "Item-level markers (clip locators) are not serialized into "
|
|
2702
|
+
"the exported .drt at all. They live in the project database "
|
|
2703
|
+
"as Sm2TiItemLockableBlob rows (same wire codec as timeline "
|
|
2704
|
+
"markers, BlobOwner = the item's DbId — located by byte "
|
|
2705
|
+
"search in a live Project.db), and readback via "
|
|
2706
|
+
"TimelineItem.GetMarkers is fine, but the export omits the "
|
|
2707
|
+
"blobs even after SaveProject — measured on Studio 19.1.3.7. "
|
|
2708
|
+
"Asymmetrically, ImportTimelineFromFile ACCEPTS an authored "
|
|
2709
|
+
"Sm2TiItemLockableBlob and the markers read back perfectly.",
|
|
2710
|
+
"recommended": "Do not rely on .drt archives to carry clip markers. To "
|
|
2711
|
+
"deliver item markers in a .drt, author them offline "
|
|
2712
|
+
"(drt.assemble cuts[].markers writes the accepted blob); "
|
|
2713
|
+
"to preserve markers from a live timeline, read them via "
|
|
2714
|
+
"the marker API and re-author.",
|
|
2715
|
+
"tags": ["timeline", "export", "drt", "markers", "silent-failure"],
|
|
2716
|
+
"submit": "bug",
|
|
2717
|
+
"mitigation": ["drt.assemble cuts[].markers"],
|
|
2718
|
+
},
|
|
2697
2719
|
]
|
|
2698
2720
|
|
|
2699
2721
|
|