davinci-resolve-mcp 2.123.0 → 2.125.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/guides/native-drt-authoring.md +9 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +59 -0
- package/resolve-advanced/server/tools/drt.mjs +12 -3
- package/resolve-advanced/server/tools/editorial.mjs +16 -2
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
|
@@ -54,8 +54,9 @@ window. `render.verify_output` covers the container-level checks.
|
|
|
54
54
|
| Fusion titles | `elements: [{type:'title', text}]` — **21-gen hosts only** | v2.108 |
|
|
55
55
|
|
|
56
56
|
`assemble_from_interchange` drives the same engine from an EDL / OTIO /
|
|
57
|
-
FCP7-XML / AAF
|
|
58
|
-
|
|
57
|
+
FCP7-XML / AAF / **.prproj** (Premiere, read offline — no Premiere needed)
|
|
58
|
+
plus a `sourceMap` — all five formats are route-proven end-to-end
|
|
59
|
+
(parse → assemble → import → measured frames and RMS) — and
|
|
59
60
|
returns an honesty ledger
|
|
60
61
|
(`authoredTransitions`, `droppedTransitions` with reasons, `authoredRetimes`,
|
|
61
62
|
`flattenedRetimes`, `authoredAudioEvents`, `upperTrackCutsVideoOnly`).
|
|
@@ -110,6 +111,12 @@ Everything else stays in `droppedTransitions` with the reason.
|
|
|
110
111
|
3. For audio: RMS per window (silence = -inf is a failed placement).
|
|
111
112
|
4. Never trust `created_new: false` — a same-named timeline already in the
|
|
112
113
|
project is returned as "success" (internal-name-wins law).
|
|
114
|
+
5. Close the loop with `editorial.verify_roundtrip`: parse the original
|
|
115
|
+
interchange and Resolve's own re-export of the imported timeline, and the
|
|
116
|
+
verifier normalizes track labels, source naming, and per-source
|
|
117
|
+
TC-absolute source frames (fitting the offsets, e.g. 86400 for a
|
|
118
|
+
01:00:00:00 source) — `pass: true` means the authored timeline's export
|
|
119
|
+
matches the turnover's intent event-for-event.
|
|
113
120
|
|
|
114
121
|
## References
|
|
115
122
|
|
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.125.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
|
@@ -575,3 +575,62 @@ export async function authorInterchange(events, target, opts = {}) {
|
|
|
575
575
|
}
|
|
576
576
|
throw new Error(`authorInterchange: unknown target '${target}' (otio|edl|drt)`);
|
|
577
577
|
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* verifyRoundtrip — assert that a re-EXPORT of an authored timeline matches
|
|
581
|
+
* the interchange it was built from, normalizing the three cross-format
|
|
582
|
+
* conventions measured on a live AAF→assemble→import→OTIO-export loop
|
|
583
|
+
* (19.1.3.7):
|
|
584
|
+
* 1. track labels: first tracks read 'V'/'A' in one format, 'V1'/'A1' in
|
|
585
|
+
* the other — canonicalized to the numbered form;
|
|
586
|
+
* 2. source names: AAF mob name vs file basename ('rt_source_1' vs
|
|
587
|
+
* 'rt_source_1.mov') — compared after stripping the extension,
|
|
588
|
+
* case-insensitively;
|
|
589
|
+
* 3. source frames: Resolve's OTIO export is TIMECODE-ABSOLUTE while
|
|
590
|
+
* event lists are usually source-relative — a CONSTANT per-source
|
|
591
|
+
* offset is fitted from the first pair and every other pair must agree
|
|
592
|
+
* (the offset itself is reported, e.g. 86400 for a 01:00:00:00 source).
|
|
593
|
+
* Record positions are min-anchored per side. Video events only (audio
|
|
594
|
+
* channel legs merge by design).
|
|
595
|
+
*
|
|
596
|
+
* @returns {{pass:boolean, pairs:number, srcOffsets:Object, mismatches:Array}}
|
|
597
|
+
*/
|
|
598
|
+
export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
599
|
+
const recTol = opts.recTol ?? 1;
|
|
600
|
+
const srcTol = opts.srcTol ?? 1;
|
|
601
|
+
const canonTrack = (t) => {
|
|
602
|
+
const m = /^([VA])(\d+)?$/.exec(String(t || ''));
|
|
603
|
+
return m ? `${m[1]}${m[2] || '1'}` : String(t);
|
|
604
|
+
};
|
|
605
|
+
const canonSource = (x) => String(x || '').replace(/\.[^.]+$/, '').toLowerCase();
|
|
606
|
+
const vids = (evts) => evts.filter((e) => /^V\d*$/.test(String(e.track)) && e.recIn != null && e.recOut != null);
|
|
607
|
+
const norm = (evts) => {
|
|
608
|
+
const v = vids(evts);
|
|
609
|
+
if (!v.length) return [];
|
|
610
|
+
const off = Math.min(...v.map((e) => e.recIn));
|
|
611
|
+
return v
|
|
612
|
+
.map((e) => ({ track: canonTrack(e.track), source: canonSource(e.source), recIn: e.recIn - off, recOut: e.recOut - off, srcIn: e.srcIn ?? 0 }))
|
|
613
|
+
.sort((a, b) => a.track.localeCompare(b.track) || a.recIn - b.recIn);
|
|
614
|
+
};
|
|
615
|
+
const a = norm(inputEvents);
|
|
616
|
+
const b = norm(exportedEvents);
|
|
617
|
+
const mismatches = [];
|
|
618
|
+
if (a.length !== b.length) mismatches.push({ kind: 'count', input: a.length, exported: b.length });
|
|
619
|
+
const srcOffsets = {};
|
|
620
|
+
const n = Math.min(a.length, b.length);
|
|
621
|
+
for (let i = 0; i < n; i += 1) {
|
|
622
|
+
const x = a[i], y = b[i];
|
|
623
|
+
if (x.track !== y.track) { mismatches.push({ kind: 'track', at: i, input: x.track, exported: y.track }); continue; }
|
|
624
|
+
if (x.source !== y.source) { mismatches.push({ kind: 'source', at: i, input: x.source, exported: y.source }); continue; }
|
|
625
|
+
if (Math.abs(x.recIn - y.recIn) > recTol || Math.abs(x.recOut - y.recOut) > recTol) {
|
|
626
|
+
mismatches.push({ kind: 'record', at: i, input: [x.recIn, x.recOut], exported: [y.recIn, y.recOut] });
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const off = y.srcIn - x.srcIn;
|
|
630
|
+
if (srcOffsets[x.source] === undefined) srcOffsets[x.source] = off;
|
|
631
|
+
else if (Math.abs(off - srcOffsets[x.source]) > srcTol) {
|
|
632
|
+
mismatches.push({ kind: 'source-frames', at: i, source: x.source, expectedOffset: srcOffsets[x.source], gotOffset: off });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return { pass: mismatches.length === 0, pairs: n, srcOffsets, mismatches };
|
|
636
|
+
}
|
|
@@ -30,7 +30,7 @@ const authorSchema = z.object({
|
|
|
30
30
|
});
|
|
31
31
|
const validateSchema = z.object({ drtPath: z.string().describe('Absolute path to a .drt file') });
|
|
32
32
|
const assembleFromInterchangeSchema = z.object({
|
|
33
|
-
format: z.enum(['edl', 'otio', 'xml', 'aaf']).describe('Interchange format of the input'),
|
|
33
|
+
format: z.enum(['edl', 'otio', 'xml', 'aaf', 'prproj']).describe('Interchange format of the input'),
|
|
34
34
|
path: z.string().optional().describe('Path to the interchange file (aaf REQUIRES a path)'),
|
|
35
35
|
content: z.string().optional().describe('Inline interchange text (edl/otio/xml)'),
|
|
36
36
|
fps: z.number().optional().describe('Event frame rate for parsing (default 24; use e.g. 29.97 for NTSC EDLs)'),
|
|
@@ -211,14 +211,23 @@ export const drtTool = {
|
|
|
211
211
|
const { parseInterchange } = await import('../editorial.mjs');
|
|
212
212
|
const { eventsToAssembleSpec } = await import('../author-interchange.mjs');
|
|
213
213
|
let content = p.content;
|
|
214
|
+
let events;
|
|
214
215
|
if (p.format === 'aaf') {
|
|
215
216
|
if (!p.path) return { error: 'aaf input requires path' };
|
|
216
217
|
content = p.path;
|
|
218
|
+
} else if (p.format === 'prproj') {
|
|
219
|
+
// Premiere: offline gunzip+graph read (no Premiere, no Resolve import
|
|
220
|
+
// path). parsePrproj flattens EVERY sequence's events; a multi-sequence
|
|
221
|
+
// .prproj with overlapping record ranges refuses naturally in the
|
|
222
|
+
// overlap check — pre-split via editorial.list_sequences if needed.
|
|
223
|
+
if (!p.path) return { error: 'prproj input requires path' };
|
|
224
|
+
const { parsePrproj } = await import('../prproj.mjs');
|
|
225
|
+
events = parsePrproj(p.path);
|
|
217
226
|
} else if (!content) {
|
|
218
227
|
if (!p.path) return { error: 'provide content or path' };
|
|
219
228
|
content = await fs.readFile(p.path, 'utf8');
|
|
220
229
|
}
|
|
221
|
-
|
|
230
|
+
if (!events) events = parseInterchange(p.format, content, { fps: p.fps ?? 24 });
|
|
222
231
|
if (!events || !events.length) return { error: 'no events parsed from the interchange input' };
|
|
223
232
|
const { spec, report } = eventsToAssembleSpec(events, {
|
|
224
233
|
sourceMap: p.sourceMap, timelineName: p.timelineName,
|
|
@@ -248,7 +257,7 @@ export const drtTool = {
|
|
|
248
257
|
stamped,
|
|
249
258
|
conform: report,
|
|
250
259
|
note:
|
|
251
|
-
'Import with timeline.import_timeline_checked (timeline is named after the FILE). ' +
|
|
260
|
+
'Import with timeline.import_timeline_checked (timeline is named after the FILE). Dissolves/cross-fades, forward+reverse retimes, multi-track video, audio events and markers are AUTHORED when geometry allows; everything else drops WITH a reason — see `conform` for the ledger.' +
|
|
252
261
|
'Retimes are flattened and transitions become cuts — see `conform` for the ledger.',
|
|
253
262
|
};
|
|
254
263
|
}
|
|
@@ -17,7 +17,7 @@ import { parseInterchange, diffChangelist, timingGuards, conformManifest, marker
|
|
|
17
17
|
import { parseAAF, parseAafDocument } from '../aaf.mjs';
|
|
18
18
|
import { parsePrproj, parsePrprojDoc } from '../prproj.mjs';
|
|
19
19
|
import { listSequences, detectFormat } from '../sequences.mjs';
|
|
20
|
-
import { authorInterchange } from '../author-interchange.mjs';
|
|
20
|
+
import { authorInterchange, verifyRoundtrip } from '../author-interchange.mjs';
|
|
21
21
|
|
|
22
22
|
const eventArray = z.array(z.object({}).passthrough());
|
|
23
23
|
|
|
@@ -86,7 +86,7 @@ const markerSchema = z.object({
|
|
|
86
86
|
export const editorialTool = {
|
|
87
87
|
name: 'editorial',
|
|
88
88
|
description:
|
|
89
|
-
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events; for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base), marker_roundtrip (markers with provenance tags). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
|
|
89
|
+
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events; for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base), marker_roundtrip (markers with provenance tags), verify_roundtrip (input events vs re-export events -> pass/mismatches + fitted per-source TC offsets; the conform QC loop-closer). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
|
|
90
90
|
async handler({ action, args }) {
|
|
91
91
|
if (action === 'parse_interchange') {
|
|
92
92
|
const p = parseSchema.parse(args);
|
|
@@ -145,6 +145,20 @@ export const editorialTool = {
|
|
|
145
145
|
const p = conformManifestSchema.parse(args);
|
|
146
146
|
return conformManifest(p.events, p.resolution, { minHandle: p.minHandle, expectTcBase: p.expectTcBase });
|
|
147
147
|
}
|
|
148
|
+
if (action === 'verify_roundtrip') {
|
|
149
|
+
// Round-trip QC: input interchange events vs a re-EXPORT of the
|
|
150
|
+
// authored timeline, normalized for the three measured cross-format
|
|
151
|
+
// conventions (track label, source naming, per-source TC-absolute
|
|
152
|
+
// source frames). Live-proven: AAF -> assemble -> import -> Resolve
|
|
153
|
+
// OTIO export verified pass with srcOffsets 86400.
|
|
154
|
+
const p = z.object({
|
|
155
|
+
input: z.array(z.any()).describe('Normalized events of the ORIGINAL interchange (parse_interchange output)'),
|
|
156
|
+
exported: z.array(z.any()).describe('Normalized events of the re-export (parse_interchange on the exported OTIO/EDL/XML)'),
|
|
157
|
+
recTol: z.number().optional(),
|
|
158
|
+
srcTol: z.number().optional(),
|
|
159
|
+
}).parse(args);
|
|
160
|
+
return verifyRoundtrip(p.input, p.exported, { recTol: p.recTol, srcTol: p.srcTol });
|
|
161
|
+
}
|
|
148
162
|
if (action === 'marker_roundtrip') {
|
|
149
163
|
const p = markerSchema.parse(args);
|
|
150
164
|
return markerRoundtrip(p.markers, { provenanceTag: p.provenanceTag });
|
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.125.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()}")
|