davinci-resolve-mcp 2.124.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.
|
@@ -111,6 +111,12 @@ Everything else stays in `droppedTransitions` with the reason.
|
|
|
111
111
|
3. For audio: RMS per window (silence = -inf is a failed placement).
|
|
112
112
|
4. Never trust `created_new: false` — a same-named timeline already in the
|
|
113
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.
|
|
114
120
|
|
|
115
121
|
## References
|
|
116
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
|
+
}
|
|
@@ -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()}")
|