davinci-resolve-mcp 2.110.0 → 2.111.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/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.111.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,9 +202,11 @@ 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 (the template clip schema has no per-clip speed),
|
|
206
|
-
* transitions (
|
|
207
|
-
*
|
|
205
|
+
* retimes (the template clip schema has no per-clip speed), authored vs
|
|
206
|
+
* dropped transitions (cross-dissolves are AUTHORED when the predecessor
|
|
207
|
+
* abuts the cut and both sides have handle media — render-verified on
|
|
208
|
+
* 19.1.3.7; otherwise dropped with the reason, as a cut at the boundary),
|
|
209
|
+
* and skipped audio events (cuts carry linked A1 audio already).
|
|
208
210
|
*
|
|
209
211
|
* @param {Array} events - normalized events (parseInterchange shape)
|
|
210
212
|
* @param {object} opts
|
|
@@ -238,6 +240,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
238
240
|
const toTl = (frames, fps) => Math.round((frames * 24) / Math.round(fps || 24));
|
|
239
241
|
const flattenedRetimes = [];
|
|
240
242
|
const droppedTransitions = [];
|
|
243
|
+
const transitionCandidates = [];
|
|
241
244
|
const perSource = new Map();
|
|
242
245
|
const placements = [];
|
|
243
246
|
|
|
@@ -250,11 +253,20 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
250
253
|
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
251
254
|
flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse });
|
|
252
255
|
}
|
|
256
|
+
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps) };
|
|
253
257
|
if (e.transition) {
|
|
254
|
-
|
|
258
|
+
// A dissolve INTO this event, at its record-in boundary. Whether it can
|
|
259
|
+
// be authored (abutting predecessor + handles both sides) is decided
|
|
260
|
+
// after all placements are known.
|
|
261
|
+
let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
|
|
262
|
+
d += d % 2; // placeTransition centers on the cut; keep it even
|
|
263
|
+
transitionCandidates.push({
|
|
264
|
+
atFrame: recIn, durationFrames: d,
|
|
265
|
+
index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
|
|
266
|
+
source: e.source, srcIn: cut.srcIn,
|
|
267
|
+
});
|
|
255
268
|
}
|
|
256
|
-
|
|
257
|
-
placements.push({ start: recIn, end: recOut, index: e.index });
|
|
269
|
+
placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: cut.srcIn, durationFrames });
|
|
258
270
|
if (!perSource.has(e.source)) perSource.set(e.source, []);
|
|
259
271
|
perSource.get(e.source).push(cut);
|
|
260
272
|
}
|
|
@@ -276,13 +288,44 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
276
288
|
cuts,
|
|
277
289
|
}));
|
|
278
290
|
|
|
291
|
+
// Author cross-dissolves where the geometry allows it (render-verified on
|
|
292
|
+
// 19.1.3.7: an offline Sm2TiTransition over transplanted cross-source media
|
|
293
|
+
// blends 124→181.6→234 at the cut — transitions carry no Fusion comp, so
|
|
294
|
+
// the byte-keyed comp-cache law does not apply). A candidate is authorable
|
|
295
|
+
// when a predecessor ends EXACTLY at its cut and both sides have handle
|
|
296
|
+
// media for the centered span; anything else stays in droppedTransitions
|
|
297
|
+
// with the reason.
|
|
298
|
+
const transitions = [];
|
|
299
|
+
for (const c of transitionCandidates) {
|
|
300
|
+
const prev = placements.find((pl) => pl.end === c.atFrame);
|
|
301
|
+
if (!prev) {
|
|
302
|
+
droppedTransitions.push({ index: c.index, type: c.type, duration: c.rawDuration, reason: 'no abutting predecessor at the cut' });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const half = c.durationFrames / 2;
|
|
306
|
+
const bHandle = c.srcIn >= half;
|
|
307
|
+
const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
|
|
308
|
+
const aFrames = aSpec && Number(aSpec.frameCount);
|
|
309
|
+
const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + half <= aFrames : false;
|
|
310
|
+
if (!bHandle || !aHandle) {
|
|
311
|
+
droppedTransitions.push({
|
|
312
|
+
index: c.index, type: c.type, duration: c.rawDuration,
|
|
313
|
+
reason: `insufficient handles for a centered ${c.durationFrames}f dissolve` +
|
|
314
|
+
`${bHandle ? '' : ' (incoming srcIn < half)'}${aHandle ? '' : ' (outgoing tail media < half)'}`,
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
transitions.push({ track: 1, atFrame: c.atFrame, durationFrames: c.durationFrames });
|
|
319
|
+
}
|
|
320
|
+
|
|
279
321
|
return {
|
|
280
|
-
spec: { timelineName, media },
|
|
322
|
+
spec: { timelineName, media, ...(transitions.length ? { transitions } : {}) },
|
|
281
323
|
report: {
|
|
282
324
|
videoEvents: vids.length,
|
|
283
325
|
sources: media.length,
|
|
284
326
|
audioEventsSkipped: audioSkipped,
|
|
285
327
|
flattenedRetimes,
|
|
328
|
+
authoredTransitions: transitions,
|
|
286
329
|
droppedTransitions,
|
|
287
330
|
origin: ORIGIN,
|
|
288
331
|
},
|
|
@@ -148,7 +148,7 @@ function requirePathArg(args, key, action) {
|
|
|
148
148
|
export const drtTool = {
|
|
149
149
|
name: 'drt',
|
|
150
150
|
description:
|
|
151
|
-
'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 flatten
|
|
151
|
+
'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 flatten; 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).',
|
|
152
152
|
async handler({ action, args }) {
|
|
153
153
|
if (action === 'parse') {
|
|
154
154
|
const p = parseSchema.parse(requirePathArg(args, 'drtPath', 'parse'));
|
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.111.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()}")
|