davinci-resolve-mcp 2.112.0 → 2.113.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/docs/reference/api-limitations.md +8 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +17 -4
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +18 -1
- package/resolve-advanced/vendor/drp-format/cut-media.js +13 -2
- package/resolve-advanced/vendor/drp-format/media-timemap.js +48 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +22 -0
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 33 missing capabilities,
|
|
15
|
+
**Totals:** 33 missing capabilities, 43 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
|
|
@@ -526,6 +526,13 @@ values, or automation-hostile modal prompts.
|
|
|
526
526
|
- **Reference:** [issue #171](https://github.com/samuelgursky/davinci-resolve-mcp/issues/171)
|
|
527
527
|
- **Tags:** timeline, import, silent-failure, unreliable-return
|
|
528
528
|
|
|
529
|
+
### MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form
|
|
530
|
+
|
|
531
|
+
- **Object:** `Sm2TimeMap (per-clip retime blob)`
|
|
532
|
+
- **Behavior:** Resolve 21 encodes a retimed clip's KeyframesBA as protobuf points; Resolve 19.1.3 encodes it as a keyed-dict of keyed-dict keyframes ({interp, YOut, YIn, Y, XOut, XIn, X}). On import, 19 SILENTLY IGNORES the protobuf form — the clip reads back and plays at 100% with no warning (measured: identical timelines, one per form; protobuf → source 0..96 over 96 frames, keyed → source 0..48 over 96 frames and a live 50% render). The map spans the WHOLE source stretched by 1/speed; the clip's <In>/<Duration> window into it in RECORD frames (srcIn converts by /speed).
|
|
533
|
+
- **Workaround / current handling:** Author retimes for pre-21 hosts with the keyed form (drt.assemble cuts[].speed does this; encoder byte-exact against a live 19.1.3.7 harvest). Treat any cross-generation timemap as unverified until a readback shows the retimed source range.
|
|
534
|
+
- **Tags:** retime, import, silent-failure, drt
|
|
535
|
+
|
|
529
536
|
### Imported Fusion comps render via byte-keyed disk cache on 19.x (offline comp edits render black)
|
|
530
537
|
|
|
531
538
|
- **Object:** `Fusion / render engine`
|
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.113.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
|
@@ -202,7 +202,9 @@ export function eventsToOTIO(events, opts = {}) {
|
|
|
202
202
|
* timeline runs 24fps with origin 86400, so rec/src frames convert as
|
|
203
203
|
* round(frames × 24 / nominalFps). Placement anchors the EARLIEST video
|
|
204
204
|
* event at the origin. Honesty ledger in the returned report: flattened
|
|
205
|
-
* retimes (
|
|
205
|
+
* reverse retimes (forward constant speeds are AUTHORED as real Sm2TimeMaps
|
|
206
|
+
* — r19 keyed form, render/readback-verified; reverse flattens with the
|
|
207
|
+
* reason), authored vs
|
|
206
208
|
* dropped transitions (cross-dissolves are AUTHORED when the predecessor
|
|
207
209
|
* abuts the cut and both sides have handle media — render-verified on
|
|
208
210
|
* 19.1.3.7; otherwise dropped with the reason, as a cut at the boundary),
|
|
@@ -241,6 +243,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
241
243
|
// 'V'/'V1' → 1, 'V2' → 2, … (parsers number video tracks; EDL is single-V).
|
|
242
244
|
const trackNum = (t) => { const m = /^V(\d+)?$/.exec(String(t || 'V')); return m ? (m[1] ? parseInt(m[1], 10) : 1) : 1; };
|
|
243
245
|
const flattenedRetimes = [];
|
|
246
|
+
const authoredRetimes = [];
|
|
244
247
|
const droppedTransitions = [];
|
|
245
248
|
const transitionCandidates = [];
|
|
246
249
|
const perSource = new Map();
|
|
@@ -252,11 +255,20 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
252
255
|
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
253
256
|
const durationFrames = recOut - recIn;
|
|
254
257
|
if (durationFrames <= 0) continue;
|
|
255
|
-
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
256
|
-
flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse });
|
|
257
|
-
}
|
|
258
258
|
const vTrack = trackNum(e.track);
|
|
259
259
|
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), ...(vTrack > 1 ? { track: vTrack } : {}) };
|
|
260
|
+
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
261
|
+
if (e.reverse || !(e.speed > 0)) {
|
|
262
|
+
// Reverse needs a descending timemap (not yet measured) — flatten, with the reason.
|
|
263
|
+
flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse, reason: 'reverse not supported — played forward at 100%' });
|
|
264
|
+
} else {
|
|
265
|
+
// Forward constant speed: authored as a real Sm2TimeMap on the cut
|
|
266
|
+
// (r19 keyed form; render/readback-verified on 19.1.3.7). Audio for
|
|
267
|
+
// retimed cuts is video-only downstream.
|
|
268
|
+
cut.speed = e.speed / 100;
|
|
269
|
+
authoredRetimes.push({ index: e.index, source: e.source, speed: e.speed });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
260
272
|
if (e.transition) {
|
|
261
273
|
// A dissolve INTO this event, at its record-in boundary. Whether it can
|
|
262
274
|
// be authored (abutting predecessor + handles both sides) is decided
|
|
@@ -338,6 +350,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
338
350
|
audioEventsSkipped: audioSkipped,
|
|
339
351
|
upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1).length,
|
|
340
352
|
flattenedRetimes,
|
|
353
|
+
authoredRetimes,
|
|
341
354
|
authoredTransitions: transitions,
|
|
342
355
|
droppedTransitions,
|
|
343
356
|
origin: ORIGIN,
|
|
@@ -46,7 +46,7 @@ const assembleSchema = z.object({
|
|
|
46
46
|
spec: z
|
|
47
47
|
.object({})
|
|
48
48
|
.passthrough()
|
|
49
|
-
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
|
|
49
|
+
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed? (forward constant retime, e.g. 0.5; video-only; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
|
|
50
50
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
51
51
|
targetAppVersion: z
|
|
52
52
|
.union([z.string(), z.number()])
|
|
@@ -25,6 +25,8 @@ const { createEmptyProject, addMediaClip, DEFAULT_START_FRAME } = require('./aut
|
|
|
25
25
|
const { loadMediaTemplate, transplantMediaElement, insertMediaElement } = require('./media-template-cache');
|
|
26
26
|
const JSZip = require('jszip');
|
|
27
27
|
const { cutSourceIntoClips } = require('./cut-media');
|
|
28
|
+
const { buildConstantSpeedTimemapKeyed } = require('./media-timemap');
|
|
29
|
+
const { randomUUID } = require('node:crypto');
|
|
28
30
|
const { placeFusionTitle } = require('./place-fusion-title');
|
|
29
31
|
const { placeGenerator } = require('./place-generator');
|
|
30
32
|
const { placeTransition } = require('./place-transition');
|
|
@@ -111,7 +113,22 @@ async function assembleTimeline(spec = {}) {
|
|
|
111
113
|
const allCuts = [];
|
|
112
114
|
sources.forEach((src, i) => {
|
|
113
115
|
(src.cuts || []).forEach((cut) => {
|
|
114
|
-
|
|
116
|
+
const out = mediaRefs[i] ? { ...cut, mediaRef: mediaRefs[i] } : { ...cut };
|
|
117
|
+
if (cut.speed !== undefined && cut.speed !== 1) {
|
|
118
|
+
// Constant-speed retime (forward only). The Sm2TimeMap spans the
|
|
119
|
+
// whole source stretched by 1/speed; the clip windows into it with
|
|
120
|
+
// RECORD-domain In/Duration (measured live on 19.1.3.7), so the
|
|
121
|
+
// source-domain srcIn converts by /speed here.
|
|
122
|
+
if (!(cut.speed > 0)) throw new RangeError('assembleTimeline: cut.speed must be > 0 (reverse not supported)');
|
|
123
|
+
const fps = Math.round(src.spec.fps || 24);
|
|
124
|
+
out.timemap = buildConstantSpeedTimemapKeyed({
|
|
125
|
+
speed: cut.speed, sourceFrames: src.spec.frameCount, fps,
|
|
126
|
+
uniqueId: randomUUID(),
|
|
127
|
+
}).toString('hex');
|
|
128
|
+
out.srcIn = Math.round((cut.srcIn ?? 0) / cut.speed);
|
|
129
|
+
delete out.speed;
|
|
130
|
+
}
|
|
131
|
+
allCuts.push(out);
|
|
115
132
|
});
|
|
116
133
|
});
|
|
117
134
|
allCuts.sort((a, b) => a.startFrame - b.startFrame);
|
|
@@ -59,6 +59,9 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
59
59
|
if (cut.track !== undefined && (!Number.isInteger(cut.track) || cut.track < 1)) {
|
|
60
60
|
throw new TypeError(`cutSourceIntoClips: cuts[${i}].track must be a positive integer`);
|
|
61
61
|
}
|
|
62
|
+
if (cut.timemap !== undefined && !/^[0-9a-fA-F]+$/.test(cut.timemap)) {
|
|
63
|
+
throw new TypeError(`cutSourceIntoClips: cuts[${i}].timemap must be a hex MediaTimemapBA blob`);
|
|
64
|
+
}
|
|
62
65
|
});
|
|
63
66
|
|
|
64
67
|
const zip = await loadDrpZip(drpInput);
|
|
@@ -76,6 +79,12 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
76
79
|
// element instead of the donor's.
|
|
77
80
|
c = c.replace(/<MediaRef>[0-9a-f-]{36}<\/MediaRef>/, `<MediaRef>${cut.mediaRef}</MediaRef>`);
|
|
78
81
|
}
|
|
82
|
+
if (cut.timemap) {
|
|
83
|
+
// Constant-speed retime: swap the identity MediaTimemapBA for the
|
|
84
|
+
// caller-built Sm2TimeMap (r19 keyed form render/readback-verified;
|
|
85
|
+
// the clip's <In>/<Duration> are RECORD-domain, measured live).
|
|
86
|
+
c = c.replace(/<MediaTimemapBA>[0-9a-fA-F]*<\/MediaTimemapBA>/, `<MediaTimemapBA>${cut.timemap}</MediaTimemapBA>`);
|
|
87
|
+
}
|
|
79
88
|
return c;
|
|
80
89
|
};
|
|
81
90
|
|
|
@@ -87,8 +96,10 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
87
96
|
if (!clips.length) continue; // audio-less media: nothing to cut on A1
|
|
88
97
|
const donor = clips[0];
|
|
89
98
|
if (trackType === 'audio') {
|
|
90
|
-
// A1 mirrors track-1 video cuts only; higher video tracks stay video-only
|
|
91
|
-
|
|
99
|
+
// A1 mirrors track-1 video cuts only; higher video tracks stay video-only,
|
|
100
|
+
// and so do RETIMED cuts (the audio clone would need its own timemap and
|
|
101
|
+
// pitch handling — video-only is stated, not silent).
|
|
102
|
+
const a1 = cuts.filter((cut) => (cut.track ?? 1) === 1 && !cut.timemap).map((cut) => cloneCut(donor, cut));
|
|
92
103
|
tracks[0] = setItemsInner(tracks[0], a1.join(''));
|
|
93
104
|
xml = replaceTrackVec(xml, trackType, match, tracks);
|
|
94
105
|
continue;
|
|
@@ -209,8 +209,55 @@ function buildConstantSpeedTimemap({ speed, sourceDurationSec, uniqueId, recordD
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* r19-generation constant-speed Sm2TimeMap. Resolve 19.x encodes KeyframesBA
|
|
214
|
+
* as a keyed-dict of keyed-dict keyframes ({interp,YOut,YIn,Y,XOut,XIn,X}),
|
|
215
|
+
* NOT the R21 protobuf points — and 19 silently IGNORES the protobuf form on
|
|
216
|
+
* import (measured: item read back at 100%). Shape harvested from a live
|
|
217
|
+
* 19.1.3.7 XMEML retime and rebuilt byte-exact. The map spans the ENTIRE
|
|
218
|
+
* source stretched by 1/speed (the clip's Start/Duration/In window into it):
|
|
219
|
+
* YMax = (sourceFrames-1)/fps — full source extent, seconds
|
|
220
|
+
* XMax = (sourceFrames/speed - 1)/fps — full retimed extent, seconds
|
|
221
|
+
* kf0 = (0,0); kf1 = (XMax, XMax*speed); linear (zero handles, interp 0)
|
|
222
|
+
*
|
|
223
|
+
* @param {object} p
|
|
224
|
+
* @param {number} p.speed - source/record ratio (0.5 = 50%). Forward only.
|
|
225
|
+
* @param {number} p.sourceFrames - full source frame count at p.fps.
|
|
226
|
+
* @param {number} [p.fps=24]
|
|
227
|
+
* @param {string} p.uniqueId - fresh uuid (bare, no braces).
|
|
228
|
+
* @returns {Buffer}
|
|
229
|
+
*/
|
|
230
|
+
function buildConstantSpeedTimemapKeyed({ speed, sourceFrames, fps = 24, uniqueId }) {
|
|
231
|
+
if (!(speed > 0)) throw new RangeError('buildConstantSpeedTimemapKeyed: speed must be > 0 (reverse not supported here)');
|
|
232
|
+
if (!Number.isInteger(sourceFrames) || sourceFrames < 1) throw new TypeError('buildConstantSpeedTimemapKeyed: sourceFrames must be a positive integer');
|
|
233
|
+
const YMax = (sourceFrames - 1) / fps;
|
|
234
|
+
const XMax = (sourceFrames / speed - 1) / fps;
|
|
235
|
+
const kf = (X, Y) => encodeKeyedDict({ hdr: 1, entries: [
|
|
236
|
+
{ key: 'interp', type: 0x02, subType: 0, value: 0 },
|
|
237
|
+
{ key: 'YOut', type: T_DOUBLE, subType: 0, value: 0 },
|
|
238
|
+
{ key: 'YIn', type: T_DOUBLE, subType: 0, value: 0 },
|
|
239
|
+
{ key: 'Y', type: T_DOUBLE, subType: 0, value: Y },
|
|
240
|
+
{ key: 'XOut', type: T_DOUBLE, subType: 0, value: 0 },
|
|
241
|
+
{ key: 'XIn', type: T_DOUBLE, subType: 0, value: 0 },
|
|
242
|
+
{ key: 'X', type: T_DOUBLE, subType: 0, value: X },
|
|
243
|
+
] }).toString('hex');
|
|
244
|
+
const keyframes = encodeKeyedDict({ hdr: 1, entries: [
|
|
245
|
+
{ key: '1', type: T_BYTES, subType: 0, value: kf(XMax, XMax * speed) },
|
|
246
|
+
{ key: '0', type: T_BYTES, subType: 0, value: kf(0, 0) },
|
|
247
|
+
] }).toString('hex');
|
|
248
|
+
return encodeKeyedDict({ hdr: 1, entries: [
|
|
249
|
+
{ key: 'YMax', type: T_DOUBLE, subType: 0, value: YMax },
|
|
250
|
+
{ key: 'XMax', type: T_DOUBLE, subType: 0, value: XMax },
|
|
251
|
+
{ key: 'UniqueId', type: T_STRING, subType: 0, value: uniqueId },
|
|
252
|
+
{ key: 'LastValidYOffset', type: T_DOUBLE, subType: 0, value: YMax },
|
|
253
|
+
{ key: 'KeyframesBA', type: T_BYTES, subType: 0, value: keyframes },
|
|
254
|
+
{ key: 'DbType', type: T_STRING, subType: 0, value: 'Sm2TimeMap' },
|
|
255
|
+
] });
|
|
256
|
+
}
|
|
257
|
+
|
|
212
258
|
module.exports = {
|
|
213
259
|
decodeTimemap, encodeTimemap, encodeRetimedTimemap,
|
|
214
|
-
identityTimemap, buildConstantSpeedTimemap,
|
|
260
|
+
identityTimemap, buildConstantSpeedTimemap, buildConstantSpeedTimemapKeyed,
|
|
261
|
+
buildTimemap, decodeProtobuf,
|
|
215
262
|
TYPE_LINEAR,
|
|
216
263
|
};
|
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.113.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
|
@@ -1914,6 +1914,28 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1914
1914
|
"structural readback cannot see this class.",
|
|
1915
1915
|
"tags": ["media-pool", "import", "render", "silent-failure", "readback"],
|
|
1916
1916
|
},
|
|
1917
|
+
{
|
|
1918
|
+
"symbol": "MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form",
|
|
1919
|
+
"object": "Sm2TimeMap (per-clip retime blob)",
|
|
1920
|
+
"reality": "Resolve 21 encodes a retimed clip's KeyframesBA as "
|
|
1921
|
+
"protobuf points; Resolve 19.1.3 encodes it as a "
|
|
1922
|
+
"keyed-dict of keyed-dict keyframes ({interp, YOut, YIn, "
|
|
1923
|
+
"Y, XOut, XIn, X}). On import, 19 SILENTLY IGNORES the "
|
|
1924
|
+
"protobuf form — the clip reads back and plays at 100% "
|
|
1925
|
+
"with no warning (measured: identical timelines, one per "
|
|
1926
|
+
"form; protobuf → source 0..96 over 96 frames, keyed → "
|
|
1927
|
+
"source 0..48 over 96 frames and a live 50% render). "
|
|
1928
|
+
"The map spans the WHOLE source stretched by 1/speed; "
|
|
1929
|
+
"the clip's <In>/<Duration> window into it in RECORD "
|
|
1930
|
+
"frames (srcIn converts by /speed).",
|
|
1931
|
+
"recommended": "Author retimes for pre-21 hosts with the keyed "
|
|
1932
|
+
"form (drt.assemble cuts[].speed does this; encoder "
|
|
1933
|
+
"byte-exact against a live 19.1.3.7 harvest). Treat "
|
|
1934
|
+
"any cross-generation timemap as unverified until a "
|
|
1935
|
+
"readback shows the retimed source range.",
|
|
1936
|
+
"tags": ["retime", "import", "silent-failure", "drt"],
|
|
1937
|
+
"submit": "bug",
|
|
1938
|
+
},
|
|
1917
1939
|
{
|
|
1918
1940
|
"symbol": "Imported Fusion comps render via byte-keyed disk cache on 19.x (offline comp edits render black)",
|
|
1919
1941
|
"object": "Fusion / render engine",
|