davinci-resolve-mcp 2.58.0 → 2.60.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 +92 -0
- package/README.md +4 -11
- package/docs/SKILL.md +4 -2
- package/install.py +12 -4
- package/package.json +1 -1
- package/resolve-advanced/README.md +6 -1
- package/resolve-advanced/server/aaf.mjs +104 -0
- package/resolve-advanced/server/author-interchange.mjs +152 -0
- package/resolve-advanced/server/editorial.mjs +17 -5
- package/resolve-advanced/server/prproj.mjs +262 -0
- package/resolve-advanced/server/sequences.mjs +86 -0
- package/resolve-advanced/server/tools/drt.mjs +9 -1
- package/resolve-advanced/server/tools/editorial.mjs +80 -7
- package/src/granular/common.py +1 -1
- package/src/server.py +375 -13
- package/src/utils/api_truth.py +24 -0
- package/src/utils/destructive_hook.py +4 -1
- package/src/utils/edit_engine.py +34 -3
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premiere .prproj offline reader — enumeration + normalized events, NO Premiere, NO new deps.
|
|
3
|
+
*
|
|
4
|
+
* A .prproj is gzip-compressed XML (Premiere CC 2013+; CS6 was plain XML). The XML is a FLAT
|
|
5
|
+
* object-reference graph: every object carries an `ObjectID` and is linked from elsewhere by
|
|
6
|
+
* `ObjectRef`. We gunzip (node `zlib`), parse (`fast-xml-parser`, already a dep), index every
|
|
7
|
+
* object by ObjectID, then walk Sequence → Video/AudioTracks → Track → TrackItems → *ClipTrackItem.
|
|
8
|
+
*
|
|
9
|
+
* Time is in TICKS (254,016,000,000 per second — factors by every standard frame/sample rate).
|
|
10
|
+
* We derive from tick geometry alone: cuts, source in/out, timeline position, SPEED/retime
|
|
11
|
+
* (srcDur/recDur), REVERSE (in>out), transitions (VideoTransitionTrackItem span), and markers.
|
|
12
|
+
*
|
|
13
|
+
* HONEST limits (skip-not-fake): the schema is proprietary and version-drifting (Project Version
|
|
14
|
+
* 25→42+); editorial timing/structure decodes with high fidelity, but per-clip EFFECTS / Lumetri
|
|
15
|
+
* COLOR are NOT translated (that's the Premiere→Resolve semantic gap present in every turnover
|
|
16
|
+
* format, not a reader limit). Resolve has no native .prproj importer — this is offline READ; to
|
|
17
|
+
* conform, convert the events to OTIO/EDL/DRT (see authorInterchange) and import that.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import zlib from 'node:zlib';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
|
|
25
|
+
export const TICKS_PER_SECOND = 254016000000;
|
|
26
|
+
|
|
27
|
+
/** Read a .prproj into its XML string, transparently handling gzip (CC) and plain XML (CS6). */
|
|
28
|
+
export function readPrprojXml(pathOrBuffer) {
|
|
29
|
+
const buf = Buffer.isBuffer(pathOrBuffer) ? pathOrBuffer : fs.readFileSync(pathOrBuffer);
|
|
30
|
+
if (buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b) return zlib.gunzipSync(buf).toString('utf8');
|
|
31
|
+
const s = buf.toString('utf8');
|
|
32
|
+
if (s.includes('<PremiereData')) return s; // CS6 / uncompressed
|
|
33
|
+
throw new Error('Not a .prproj: no gzip magic and no <PremiereData> root. A .prproj is gzip-compressed XML (CC) or plain XML (CS6).');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const asArray = (v) => (v == null ? [] : Array.isArray(v) ? v : [v]);
|
|
37
|
+
|
|
38
|
+
/** Text of a child element, tolerating fast-xml-parser's {#text} wrapping when the node has attrs. */
|
|
39
|
+
function childText(node, tag) {
|
|
40
|
+
if (!node) return null;
|
|
41
|
+
const v = node[tag];
|
|
42
|
+
if (v == null) return null;
|
|
43
|
+
if (typeof v === 'object') return v['#text'] != null ? v['#text'] : null;
|
|
44
|
+
return v;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nodeName = (node) => childText(node?.Node?.Properties, 'Name');
|
|
48
|
+
|
|
49
|
+
/** ticks → whole frames at fps. */
|
|
50
|
+
function ticksToFrames(ticks, fps) {
|
|
51
|
+
const t = Number(ticks);
|
|
52
|
+
if (!Number.isFinite(t) || !fps) return null;
|
|
53
|
+
return Math.round(t / (TICKS_PER_SECOND / fps));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Sequence FrameRate is a rate (e.g. 25) OR ticks-per-frame (huge); normalize to fps. */
|
|
57
|
+
function deriveFps(seqNode) {
|
|
58
|
+
const raw = childText(seqNode?.Node?.Properties, 'FrameRate');
|
|
59
|
+
const v = Number(raw);
|
|
60
|
+
if (!Number.isFinite(v) || v <= 0) return 24;
|
|
61
|
+
if (v > 100000) return +(TICKS_PER_SECOND / v).toFixed(3); // stored as ticks-per-frame
|
|
62
|
+
return v;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Parse the XML into an ObjectID → {tag, node} index. Objects are flat children of PremiereData. */
|
|
66
|
+
function indexObjects(xml) {
|
|
67
|
+
const { XMLParser } = require('fast-xml-parser');
|
|
68
|
+
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' });
|
|
69
|
+
const doc = parser.parse(xml);
|
|
70
|
+
const pd = doc.PremiereData;
|
|
71
|
+
if (!pd) throw new Error('Not a .prproj: missing <PremiereData> root.');
|
|
72
|
+
const byId = new Map();
|
|
73
|
+
for (const [tag, val] of Object.entries(pd)) {
|
|
74
|
+
if (tag.startsWith('@_')) continue;
|
|
75
|
+
for (const node of asArray(val)) {
|
|
76
|
+
if (node && typeof node === 'object' && node['@_ObjectID'] != null) byId.set(String(node['@_ObjectID']), { tag, node });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { byId, projectVersion: firstProjectVersion(byId) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function firstProjectVersion(byId) {
|
|
83
|
+
for (const { tag, node } of byId.values()) {
|
|
84
|
+
if (tag === 'Project' && node['@_Version'] != null) return Number(node['@_Version']);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ref = (node, tag) => {
|
|
90
|
+
const r = asArray(node?.[tag])[0];
|
|
91
|
+
return r ? String(r['@_ObjectRef'] ?? '') : null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Best-effort source label for a clip item: media basename, else project-item name, else UNKNOWN. */
|
|
95
|
+
function resolveSourceName(clipNode, byId) {
|
|
96
|
+
const cpiId = ref(clipNode, 'ClipProjectItem') || ref(clipNode, 'SubClip') || ref(clipNode, 'ProjectItem');
|
|
97
|
+
const cpi = cpiId ? byId.get(cpiId) : null;
|
|
98
|
+
if (cpi) {
|
|
99
|
+
const path = findFirstText(cpi.node, ['ActualMediaFilePath', 'FilePath', 'RelativePath'], 6);
|
|
100
|
+
if (path) return String(path).split(/[\\/]/).pop();
|
|
101
|
+
const nm = nodeName(cpi.node);
|
|
102
|
+
if (nm) return nm;
|
|
103
|
+
}
|
|
104
|
+
return nodeName(clipNode) || 'UNKNOWN';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Shallow recursive search for the first non-empty text under any of `tags` (depth-bounded). */
|
|
108
|
+
function findFirstText(node, tags, depth) {
|
|
109
|
+
if (!node || typeof node !== 'object' || depth < 0) return null;
|
|
110
|
+
for (const tag of tags) {
|
|
111
|
+
const t = childText(node, tag);
|
|
112
|
+
if (t) return t;
|
|
113
|
+
}
|
|
114
|
+
for (const k of Object.keys(node)) {
|
|
115
|
+
if (k.startsWith('@_') || k === '#text') continue;
|
|
116
|
+
for (const child of asArray(node[k])) {
|
|
117
|
+
const found = findFirstText(child, tags, depth - 1);
|
|
118
|
+
if (found) return found;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function clipEvent(clipNode, byId, track, fps, index) {
|
|
125
|
+
const start = childText(clipNode, 'Start');
|
|
126
|
+
const end = childText(clipNode, 'End');
|
|
127
|
+
const inPt = childText(clipNode, 'InPoint');
|
|
128
|
+
const outPt = childText(clipNode, 'OutPoint');
|
|
129
|
+
const recIn = ticksToFrames(start, fps);
|
|
130
|
+
const recOut = ticksToFrames(end, fps);
|
|
131
|
+
const srcIn = ticksToFrames(inPt, fps);
|
|
132
|
+
const srcOut = ticksToFrames(outPt, fps);
|
|
133
|
+
// Speed from tick geometry: |source span| / |timeline span|. Reverse when in > out.
|
|
134
|
+
let speed = 100;
|
|
135
|
+
let reverse = false;
|
|
136
|
+
const srcSpan = Number(outPt) - Number(inPt);
|
|
137
|
+
const recSpan = Number(end) - Number(start);
|
|
138
|
+
if (Number.isFinite(srcSpan) && Number.isFinite(recSpan) && recSpan !== 0) {
|
|
139
|
+
speed = +(Math.abs(srcSpan / recSpan) * 100).toFixed(2);
|
|
140
|
+
reverse = srcSpan < 0;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
index,
|
|
144
|
+
track,
|
|
145
|
+
source: resolveSourceName(clipNode, byId),
|
|
146
|
+
srcIn: reverse ? srcOut : srcIn,
|
|
147
|
+
srcOut: reverse ? srcIn : srcOut,
|
|
148
|
+
recIn,
|
|
149
|
+
recOut,
|
|
150
|
+
speed,
|
|
151
|
+
reverse,
|
|
152
|
+
transition: null,
|
|
153
|
+
fps,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function walkSequence(seqEntry, byId) {
|
|
158
|
+
const fps = deriveFps(seqEntry.node);
|
|
159
|
+
const events = [];
|
|
160
|
+
let idx = 1;
|
|
161
|
+
for (const [container, trackKind] of [
|
|
162
|
+
['VideoTracks', 'V'],
|
|
163
|
+
['AudioTracks', 'A'],
|
|
164
|
+
]) {
|
|
165
|
+
const trackRefs = asArray(seqEntry.node[container]?.Track);
|
|
166
|
+
for (const tref of trackRefs) {
|
|
167
|
+
const tEntry = byId.get(String(tref['@_ObjectRef'] ?? ''));
|
|
168
|
+
if (!tEntry) continue;
|
|
169
|
+
const itemRefs = asArray(tEntry.node.TrackItems?.TrackItem);
|
|
170
|
+
const transitions = [];
|
|
171
|
+
const clipEvents = [];
|
|
172
|
+
for (const iref of itemRefs) {
|
|
173
|
+
const cEntry = byId.get(String(iref['@_ObjectRef'] ?? ''));
|
|
174
|
+
if (!cEntry) continue;
|
|
175
|
+
if (/ClipTrackItem$/.test(cEntry.tag)) {
|
|
176
|
+
clipEvents.push(clipEvent(cEntry.node, byId, trackKind, fps, idx++));
|
|
177
|
+
} else if (/Transition/.test(cEntry.tag)) {
|
|
178
|
+
const dur = ticksToFrames(Number(childText(cEntry.node, 'End')) - Number(childText(cEntry.node, 'Start')), fps);
|
|
179
|
+
transitions.push({ recIn: ticksToFrames(childText(cEntry.node, 'Start'), fps), duration: dur || 0 });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Attach each transition to the clip that begins at its record position (best-effort).
|
|
183
|
+
for (const tr of transitions) {
|
|
184
|
+
const hit = clipEvents.find((e) => e.recIn === tr.recIn);
|
|
185
|
+
if (hit) hit.transition = { type: 'dissolve', duration: tr.duration };
|
|
186
|
+
}
|
|
187
|
+
events.push(...clipEvents);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return events;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Enumerate marker objects (project- or sequence-level) with tick→frame positions. */
|
|
194
|
+
function collectMarkers(byId, fps) {
|
|
195
|
+
const markers = [];
|
|
196
|
+
for (const { tag, node } of byId.values()) {
|
|
197
|
+
if (tag !== 'Marker') continue;
|
|
198
|
+
markers.push({
|
|
199
|
+
frame: ticksToFrames(childText(node, 'Position'), fps),
|
|
200
|
+
duration: ticksToFrames(childText(node, 'Duration'), fps) || 0,
|
|
201
|
+
name: childText(node, 'Name') || '',
|
|
202
|
+
note: childText(node, 'Comment') || '',
|
|
203
|
+
type: Number(childText(node, 'MarkerType')) || 0,
|
|
204
|
+
colorIndex: Number(childText(node, 'ColorIndex')) || 0,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return markers;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sequenceEntries(byId) {
|
|
211
|
+
const seqs = [];
|
|
212
|
+
for (const entry of byId.values()) if (entry.tag === 'Sequence') seqs.push(entry);
|
|
213
|
+
return seqs;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parse a .prproj into per-sequence structure.
|
|
218
|
+
* @returns {{projectVersion:number|null, sequences:Array<{id,name,fps,eventCount,events,markers}>, mediaPaths:string[]}}
|
|
219
|
+
*/
|
|
220
|
+
export function parsePrprojDoc(pathOrBuffer) {
|
|
221
|
+
const xml = readPrprojXml(pathOrBuffer);
|
|
222
|
+
const { byId, projectVersion } = indexObjects(xml);
|
|
223
|
+
const sequences = sequenceEntries(byId).map((entry) => {
|
|
224
|
+
const fps = deriveFps(entry.node);
|
|
225
|
+
const events = walkSequence(entry, byId);
|
|
226
|
+
return {
|
|
227
|
+
id: String(entry.node['@_ObjectID']),
|
|
228
|
+
name: nodeName(entry.node) || `Sequence ${entry.node['@_ObjectID']}`,
|
|
229
|
+
fps,
|
|
230
|
+
eventCount: events.length,
|
|
231
|
+
events,
|
|
232
|
+
markers: collectMarkers(byId, fps),
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
const mediaPaths = [...new Set(collectMediaPaths(byId))].sort();
|
|
236
|
+
return { projectVersion, sequences, mediaPaths };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function collectMediaPaths(byId) {
|
|
240
|
+
const paths = [];
|
|
241
|
+
for (const { node } of byId.values()) {
|
|
242
|
+
for (const tag of ['ActualMediaFilePath', 'FilePath']) {
|
|
243
|
+
const t = childText(node, tag);
|
|
244
|
+
if (t) paths.push(String(t));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return paths;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** FLAT normalized-event list across every sequence (mirrors parseEDL/parseOTIO output). */
|
|
251
|
+
export function parsePrproj(pathOrBuffer) {
|
|
252
|
+
const { sequences } = parsePrprojDoc(pathOrBuffer);
|
|
253
|
+
const events = [];
|
|
254
|
+
for (const s of sequences) for (const e of s.events) events.push(e);
|
|
255
|
+
return events;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Enumerate sequences for the picker. */
|
|
259
|
+
export function listPrprojSequences(pathOrBuffer) {
|
|
260
|
+
const { sequences } = parsePrprojDoc(pathOrBuffer);
|
|
261
|
+
return sequences.map((s, index) => ({ id: s.id, name: s.name, eventCount: s.eventCount, index }));
|
|
262
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* list_sequences — ONE offline enumeration entry point for the app's "which sequence?" picker.
|
|
3
|
+
*
|
|
4
|
+
* Works across xml/fcpxml/edl/otio/drt/drp/aaf and returns a uniform
|
|
5
|
+
* [{ id, name, eventCount, index }]
|
|
6
|
+
* so the Conform surface can drive a single selector regardless of the turnover format.
|
|
7
|
+
* .prproj is an honest refuse. AAF goes through pyaaf2 (aaf.mjs); drt/drp reuse the DRT parser.
|
|
8
|
+
*
|
|
9
|
+
* Detection is by file extension unless an explicit `format` is passed.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { parseInterchange } from './editorial.mjs';
|
|
15
|
+
import { listAafSequences } from './aaf.mjs';
|
|
16
|
+
import { listPrprojSequences } from './prproj.mjs';
|
|
17
|
+
import { drt } from './libs.mjs';
|
|
18
|
+
|
|
19
|
+
const EXT_FORMAT = {
|
|
20
|
+
'.edl': 'edl',
|
|
21
|
+
'.otio': 'otio',
|
|
22
|
+
'.xml': 'xml',
|
|
23
|
+
'.fcpxml': 'xml',
|
|
24
|
+
'.xmeml': 'xml',
|
|
25
|
+
'.drt': 'drt',
|
|
26
|
+
'.drp': 'drp',
|
|
27
|
+
'.aaf': 'aaf',
|
|
28
|
+
'.prproj': 'prproj',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Normalize a format string / file extension to a canonical parser key. */
|
|
32
|
+
export function detectFormat(filePath, explicit) {
|
|
33
|
+
if (explicit) {
|
|
34
|
+
const f = String(explicit).toLowerCase();
|
|
35
|
+
if (['xmeml', 'fcp7', 'fcpxml'].includes(f)) return 'xml';
|
|
36
|
+
return f;
|
|
37
|
+
}
|
|
38
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
39
|
+
const fmt = EXT_FORMAT[ext];
|
|
40
|
+
if (!fmt) {
|
|
41
|
+
throw new Error(`list_sequences: unknown extension '${ext || '(none)'}' — pass an explicit format (edl|otio|xml|drt|drp|aaf).`);
|
|
42
|
+
}
|
|
43
|
+
return fmt;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Map a parseDRT() result → uniform [{id,name,eventCount,index}]. Shared with the drt tool. */
|
|
47
|
+
export function summarizeDrtTimelines(parsed) {
|
|
48
|
+
return (parsed.timelines || []).map((tl, index) => {
|
|
49
|
+
const vids = (tl.videoTracks || []).reduce((n, t) => n + (t.clips ? t.clips.length : 0), 0);
|
|
50
|
+
const auds = (tl.audioTracks || []).reduce((n, t) => n + (t.clips ? t.clips.length : 0), 0);
|
|
51
|
+
return {
|
|
52
|
+
id: parsed.seqContainers && parsed.seqContainers[index] ? parsed.seqContainers[index] : tl.name || `seq${index + 1}`,
|
|
53
|
+
name: tl.name || `Sequence ${index + 1}`,
|
|
54
|
+
eventCount: vids + auds,
|
|
55
|
+
index,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Enumerate sequences in a turnover file.
|
|
62
|
+
* @param {string} filePath absolute path to the interchange/project file
|
|
63
|
+
* @param {{format?:string, fps?:number}} [opts]
|
|
64
|
+
* @returns {Promise<Array<{id:string,name:string,eventCount:number,index:number}>>}
|
|
65
|
+
*/
|
|
66
|
+
export async function listSequences(filePath, opts = {}) {
|
|
67
|
+
const fmt = detectFormat(filePath, opts.format);
|
|
68
|
+
|
|
69
|
+
if (fmt === 'prproj') return listPrprojSequences(filePath);
|
|
70
|
+
|
|
71
|
+
if (fmt === 'aaf') {
|
|
72
|
+
const seqs = await listAafSequences(filePath);
|
|
73
|
+
return seqs.map((s, index) => ({ ...s, index }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (fmt === 'drt' || fmt === 'drp') {
|
|
77
|
+
const parsed = await drt().parseDRT(filePath);
|
|
78
|
+
return summarizeDrtTimelines(parsed);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Text interchange (edl/otio/xml/xmeml/fcp7) — a single sequence per file.
|
|
82
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
83
|
+
const events = parseInterchange(fmt, content, { fps: opts.fps });
|
|
84
|
+
const name = path.basename(filePath);
|
|
85
|
+
return [{ id: name, name, eventCount: events.length, index: 0 }];
|
|
86
|
+
}
|
|
@@ -15,8 +15,10 @@ import fs from 'node:fs/promises';
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import JSZip from 'jszip';
|
|
17
17
|
import { drt } from '../libs.mjs';
|
|
18
|
+
import { summarizeDrtTimelines } from '../sequences.mjs';
|
|
18
19
|
|
|
19
20
|
const parseSchema = z.object({ drtPath: z.string().describe('Absolute path to a .drt (or .drp) file') });
|
|
21
|
+
const listSequencesSchema = z.object({ drpPath: z.string().describe('Absolute path to a .drp (or .drt) file') });
|
|
20
22
|
const authorSchema = z.object({
|
|
21
23
|
spec: z.object({}).passthrough().describe('{ timelines, mediaPool?, metadata? } — buildDRP shape minus the project shell'),
|
|
22
24
|
outputPath: z.string().describe('Absolute path where the .drt will be written'),
|
|
@@ -71,12 +73,18 @@ const downgradeSchema = z.object({
|
|
|
71
73
|
export const drtTool = {
|
|
72
74
|
name: 'drt',
|
|
73
75
|
description:
|
|
74
|
-
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: parse, author, validate, inject_into_drp, extract_from_drp, 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).',
|
|
76
|
+
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: 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).',
|
|
75
77
|
async handler({ action, args }) {
|
|
76
78
|
if (action === 'parse') {
|
|
77
79
|
const p = parseSchema.parse(args);
|
|
78
80
|
return drt().parseDRT(p.drtPath);
|
|
79
81
|
}
|
|
82
|
+
if (action === 'list_sequences') {
|
|
83
|
+
const p = listSequencesSchema.parse(args);
|
|
84
|
+
const parsed = await drt().parseDRT(p.drpPath);
|
|
85
|
+
const sequences = summarizeDrtTimelines(parsed);
|
|
86
|
+
return { path: p.drpPath, count: sequences.length, sequences };
|
|
87
|
+
}
|
|
80
88
|
if (action === 'author') {
|
|
81
89
|
const p = authorSchema.parse(args);
|
|
82
90
|
const buf = await drt().buildDRT(p.spec);
|
|
@@ -3,22 +3,60 @@
|
|
|
3
3
|
* changelist + conform manifest with TIMING silent-lie guards. Report-only (gate: review). No Resolve.
|
|
4
4
|
*
|
|
5
5
|
* Actions:
|
|
6
|
-
* parse_interchange
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* parse_interchange — EDL / OTIO / XMEML / AAF (pyaaf2) / PRPROJ (gunzip+XML) → normalized events
|
|
7
|
+
* list_sequences — ONE picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}]
|
|
8
|
+
* convert_to_interchange— events (or a parsed source) → OTIO/EDL/DRT Resolve CAN import (the .prproj bridge)
|
|
9
|
+
* turnover_changelist — diff old vs new events → moved/retimed/replaced/new/gone (+timing flags)
|
|
10
|
+
* conform_manifest — per-event assert: source resolved, handles, retime, reverse, TC-base
|
|
11
|
+
* marker_roundtrip — marker/note round-trip with provenance tags
|
|
10
12
|
*/
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
11
14
|
import { z } from 'zod';
|
|
12
15
|
import { parseInterchange, diffChangelist, timingGuards, conformManifest, markerRoundtrip } from '../editorial.mjs';
|
|
16
|
+
import { parseAAF } from '../aaf.mjs';
|
|
17
|
+
import { parsePrproj, parsePrprojDoc } from '../prproj.mjs';
|
|
18
|
+
import { listSequences, detectFormat } from '../sequences.mjs';
|
|
19
|
+
import { authorInterchange } from '../author-interchange.mjs';
|
|
13
20
|
|
|
14
21
|
const eventArray = z.array(z.object({}).passthrough());
|
|
15
22
|
|
|
23
|
+
/** Parse any supported turnover file (by path) into normalized events. Async (AAF spawns). */
|
|
24
|
+
async function parseAnySource(sourcePath, sourceFormat) {
|
|
25
|
+
const fmt = detectFormat(sourcePath, sourceFormat);
|
|
26
|
+
if (fmt === 'aaf') return parseAAF(sourcePath);
|
|
27
|
+
if (fmt === 'prproj') return parsePrproj(sourcePath);
|
|
28
|
+
if (fmt === 'drt' || fmt === 'drp')
|
|
29
|
+
throw new Error('convert_to_interchange: .drt/.drp already import into Resolve directly — use timeline.import_from_drp, not the bridge.');
|
|
30
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
31
|
+
return parseInterchange(fmt, content, {});
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
const parseSchema = z.object({
|
|
17
|
-
format: z.enum(['edl', 'otio', 'xml', 'xmeml', 'fcp7', 'aaf']),
|
|
18
|
-
content: z
|
|
35
|
+
format: z.enum(['edl', 'otio', 'xml', 'xmeml', 'fcp7', 'aaf', 'prproj']),
|
|
36
|
+
content: z
|
|
37
|
+
.union([z.string(), z.object({}).passthrough()])
|
|
38
|
+
.describe('EDL text / OTIO JSON (string or object) / XMEML string. For AAF or PRPROJ (binary): the file PATH.'),
|
|
39
|
+
fps: z.number().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const listSequencesSchema = z.object({
|
|
43
|
+
path: z.string().describe('Absolute path to an xml/fcpxml/edl/otio/drt/drp/aaf/prproj file'),
|
|
44
|
+
format: z.string().optional().describe('Override format detection (edl|otio|xml|drt|drp|aaf|prproj)'),
|
|
19
45
|
fps: z.number().optional(),
|
|
20
46
|
});
|
|
21
47
|
|
|
48
|
+
const convertSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
events: eventArray.optional().describe('Normalized events to author (or provide sourcePath+sourceFormat)'),
|
|
51
|
+
sourcePath: z.string().optional().describe('Parse this file first (edl/otio/xml/aaf/prproj) then author'),
|
|
52
|
+
sourceFormat: z.string().optional().describe('Format of sourcePath (default: detect by extension)'),
|
|
53
|
+
target: z.enum(['otio', 'edl', 'drt']).default('otio').describe('Interchange to author (Resolve-importable)'),
|
|
54
|
+
outputPath: z.string().optional().describe('Write the authored file here; otherwise return the content'),
|
|
55
|
+
name: z.string().optional(),
|
|
56
|
+
fps: z.number().optional(),
|
|
57
|
+
})
|
|
58
|
+
.describe('Author an interchange Resolve can import from events or a parsed source — the .prproj→Resolve bridge');
|
|
59
|
+
|
|
22
60
|
const changelistSchema = z.object({
|
|
23
61
|
old: eventArray.describe('Old (locked-cut) normalized events, OR provide oldFormat+oldContent'),
|
|
24
62
|
new: eventArray.describe('New (turnover) normalized events'),
|
|
@@ -43,13 +81,48 @@ const markerSchema = z.object({
|
|
|
43
81
|
export const editorialTool = {
|
|
44
82
|
name: 'editorial',
|
|
45
83
|
description:
|
|
46
|
-
'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 → events; AAF
|
|
84
|
+
'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), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}]), 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/speed survive, per-clip effects/color do not), 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).',
|
|
47
85
|
async handler({ action, args }) {
|
|
48
86
|
if (action === 'parse_interchange') {
|
|
49
87
|
const p = parseSchema.parse(args);
|
|
88
|
+
// Binary formats parse out-of-band from a PATH: AAF via pyaaf2, .prproj via gunzip+XML.
|
|
89
|
+
if (p.format === 'aaf') {
|
|
90
|
+
const events = await parseAAF(p.content);
|
|
91
|
+
return { format: 'aaf', count: events.length, events };
|
|
92
|
+
}
|
|
93
|
+
if (p.format === 'prproj') {
|
|
94
|
+
const doc = parsePrprojDoc(p.content);
|
|
95
|
+
const events = parsePrproj(p.content);
|
|
96
|
+
return { format: 'prproj', count: events.length, events, projectVersion: doc.projectVersion, mediaPaths: doc.mediaPaths };
|
|
97
|
+
}
|
|
50
98
|
const events = parseInterchange(p.format, p.content, { fps: p.fps });
|
|
51
99
|
return { format: p.format, count: events.length, events };
|
|
52
100
|
}
|
|
101
|
+
if (action === 'list_sequences') {
|
|
102
|
+
const p = listSequencesSchema.parse(args);
|
|
103
|
+
const sequences = await listSequences(p.path, { format: p.format, fps: p.fps });
|
|
104
|
+
return { path: p.path, count: sequences.length, sequences };
|
|
105
|
+
}
|
|
106
|
+
if (action === 'convert_to_interchange') {
|
|
107
|
+
const p = convertSchema.parse(args);
|
|
108
|
+
let events = p.events;
|
|
109
|
+
if (!events || !events.length) {
|
|
110
|
+
if (!p.sourcePath) throw new Error('convert_to_interchange: provide events, or sourcePath (+sourceFormat)');
|
|
111
|
+
events = await parseAnySource(p.sourcePath, p.sourceFormat);
|
|
112
|
+
}
|
|
113
|
+
const authored = await authorInterchange(events, p.target, { name: p.name, fps: p.fps });
|
|
114
|
+
let written = null;
|
|
115
|
+
if (p.outputPath) {
|
|
116
|
+
await fs.writeFile(p.outputPath, authored.buffer || authored.content);
|
|
117
|
+
written = { outputPath: p.outputPath, bytes: (authored.buffer || Buffer.from(authored.content)).length };
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
target: authored.target,
|
|
121
|
+
eventCount: events.length,
|
|
122
|
+
...(written || { content: authored.content }),
|
|
123
|
+
...(authored.spec ? { spec: authored.spec } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
53
126
|
if (action === 'turnover_changelist') {
|
|
54
127
|
const p = changelistSchema.parse(args);
|
|
55
128
|
const result = diffChangelist(p.old, p.new, { recTolerance: p.recTolerance });
|
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.60.0"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
logger.info(f"Detected platform: {get_platform()}")
|