davinci-resolve-mcp 2.131.0 → 2.132.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 +1 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +8 -0
- package/resolve-advanced/vendor/drp-format/place-compound.js +39 -19
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
|
@@ -53,7 +53,7 @@ window. `render.verify_output` covers the container-level checks.
|
|
|
53
53
|
| TC-bearing sources (AAF route) | native clip capture (`MediaStartTime`), channel-leg merge | v2.120–2.122 |
|
|
54
54
|
| Subtitles | `spec.subtitles` / `spec.subtitlesSrt` (raw SRT) | v2.128 |
|
|
55
55
|
| Compound clips | survive `extract_from_drp` → `.drt` (inner containers kept, recursive) | v2.130 |
|
|
56
|
-
| Compound authoring | `spec.compounds` (
|
|
56
|
+
| Compound authoring | `spec.compounds` (multiple compose; nested edits render) | v2.131–2.132 |
|
|
57
57
|
| Fusion titles | `elements: [{type:'title', text}]` — **21-gen hosts only** | v2.108 |
|
|
58
58
|
|
|
59
59
|
`assemble_from_interchange` drives the same engine from an EDL / OTIO /
|
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.132.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
|
@@ -51,7 +51,7 @@ const assembleSchema = z.object({
|
|
|
51
51
|
spec: z
|
|
52
52
|
.object({})
|
|
53
53
|
.passthrough()
|
|
54
|
-
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' dissolve | 'audio' cross-fade, both render-verified on 19)}], markers?: [{frame (timeline-absolute), color? (16 names), name?, note?, duration?, customData?}] (readback-verified on 19), compounds?: [{name, startFrame (parent, absolute), durationFrames, track?, cuts:[{mediaFilePath, startFrame (INNER, 0-based), durationFrames, srcIn?}]}] (
|
|
54
|
+
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' dissolve | 'audio' cross-fade, both render-verified on 19)}], markers?: [{frame (timeline-absolute), color? (16 names), name?, note?, duration?, customData?}] (readback-verified on 19), compounds?: [{name, startFrame (parent, absolute), durationFrames, track?, cuts:[{mediaFilePath, startFrame (INNER, 0-based), durationFrames, srcIn?}]}] (multiple compose; inner cuts need captured templates w/ native clips; render-verified nested playback incl. two compounds on 19), subtitles?: [{startFrame (timeline-absolute), durationFrames, text}] + subtitlesSrt? (raw SRT, cues anchor at the origin; readback-verified on 19; angle-bracket runs read as SRT markup), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}] }. startFrame is timeline-absolute (origin 86400)."),
|
|
55
55
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
56
56
|
targetAppVersion: z
|
|
57
57
|
.union([z.string(), z.number()])
|
|
@@ -221,6 +221,13 @@ async function assembleTimeline(spec = {}) {
|
|
|
221
221
|
insertedExtra.add(fp);
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
+
// Pin the PARENT container id BEFORE inserting inner containers: entry
|
|
225
|
+
// listing is name-sorted, so an inner container can alphabetically
|
|
226
|
+
// precede the parent and swallow the next compound's item (measured —
|
|
227
|
+
// CMP_B landed inside CMP_A's inner timeline).
|
|
228
|
+
const zip0 = await JSZip.loadAsync(buffer);
|
|
229
|
+
const parentEntry0 = Object.keys(zip0.files).find((n) => !zip0.files[n].dir && /SeqContainer\/.+\.xml$/.test(n));
|
|
230
|
+
const parentContainerId = ((await zip0.file(parentEntry0).async('string')).match(/<Sm2SequenceContainer DbId="([^"]+)"/) || [])[1];
|
|
224
231
|
for (const [ci, comp] of spec.compounds.entries()) {
|
|
225
232
|
if (!comp || typeof comp !== 'object') throw new TypeError(`assembleTimeline: compounds[${ci}] must be an object`);
|
|
226
233
|
const res = await placeCompound(buffer, {
|
|
@@ -228,6 +235,7 @@ async function assembleTimeline(spec = {}) {
|
|
|
228
235
|
startFrame: comp.startFrame,
|
|
229
236
|
durationFrames: comp.durationFrames,
|
|
230
237
|
track: comp.track,
|
|
238
|
+
timelineUuid: parentContainerId,
|
|
231
239
|
});
|
|
232
240
|
buffer = res.buffer;
|
|
233
241
|
const innerCuts = (comp.cuts || []).map((cut, i) => {
|
|
@@ -29,6 +29,7 @@ const path = require('node:path');
|
|
|
29
29
|
const crypto = require('node:crypto');
|
|
30
30
|
const { escapeXml } = require('./xml-builder');
|
|
31
31
|
const { loadDrpZip, selectTargetSeq, getTrackVec, replaceTrackVec, getItemsInner, setItemsInner, freshDbIds } = require('./seq-surgery');
|
|
32
|
+
const { decodeKeyedDict, encodeKeyedDict } = require('./keyed-dict');
|
|
32
33
|
|
|
33
34
|
const T = (f) => path.join(__dirname, 'templates', f);
|
|
34
35
|
|
|
@@ -56,28 +57,47 @@ async function placeCompound(drpInput, opts = {}) {
|
|
|
56
57
|
const zip = await loadDrpZip(drpInput);
|
|
57
58
|
const { entry, xml: parentXml } = await selectTargetSeq(zip, timelineUuid);
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const inner0 = fs.readFileSync(T('compound-inner-r19.xml'), 'utf8');
|
|
69
|
-
const innerContainerId = inner0.match(/<Sm2SequenceContainer DbId="([^"]+)"/)[1];
|
|
70
|
-
if (zip.file(`SeqContainer/${innerContainerId}.xml`)) {
|
|
71
|
-
throw new Error('placeCompound: only one compound per archive is supported (the harvested donor identities are kept verbatim)');
|
|
72
|
-
}
|
|
60
|
+
// Cluster identity: the embedded Sm2Sequence FieldsBlob carries a keyed
|
|
61
|
+
// `SeqRef` entry naming the INNER CONTAINER's uuid — freshening the XML
|
|
62
|
+
// ids WITHOUT patching that entry orphans the blob and CRASHES Resolve on
|
|
63
|
+
// import (the app died, twice, before this was decoded). With SeqRef
|
|
64
|
+
// patched through the keyed-dict codec, every cluster id can be fresh, so
|
|
65
|
+
// multiple compounds per archive compose.
|
|
66
|
+
const compoundId = crypto.randomUUID();
|
|
67
|
+
const innerSequenceId = crypto.randomUUID();
|
|
68
|
+
const innerContainerId = crypto.randomUUID();
|
|
73
69
|
|
|
74
|
-
// 1) Inner container:
|
|
75
|
-
//
|
|
76
|
-
|
|
70
|
+
// 1) Inner container: fresh container/track ids, tracks point at the new
|
|
71
|
+
// embedded sequence id, Items emptied (content added by the caller via
|
|
72
|
+
// the cuts machinery, 0-based frames).
|
|
73
|
+
let inner = fs.readFileSync(T('compound-inner-r19.xml'), 'utf8');
|
|
74
|
+
inner = freshDbIds(inner);
|
|
75
|
+
inner = inner.replace(/<Sm2SequenceContainer DbId="[^"]+"/, `<Sm2SequenceContainer DbId="${innerContainerId}"`);
|
|
76
|
+
inner = inner.replace(/<Sequence>[0-9a-f-]{36}<\/Sequence>/g, `<Sequence>${innerSequenceId}</Sequence>`);
|
|
77
|
+
inner = inner.replace(/<Items>[\s\S]*?<\/Items>/g, '<Items/>');
|
|
77
78
|
zip.file(`SeqContainer/${innerContainerId}.xml`, inner);
|
|
78
79
|
|
|
79
|
-
// 2) Pool compound element:
|
|
80
|
-
let pool =
|
|
80
|
+
// 2) Pool compound element: fresh identities + blob SeqRef patch.
|
|
81
|
+
let pool = fs.readFileSync(T('compound-pool-r19.xml'), 'utf8');
|
|
82
|
+
pool = pool.replace(/<Sm2MpCompoundClip DbId="[^"]+"/, `<Sm2MpCompoundClip DbId="${compoundId}"`);
|
|
83
|
+
pool = pool.replace(/<Sm2Sequence DbId="[^"]+"/, `<Sm2Sequence DbId="${innerSequenceId}"`);
|
|
84
|
+
pool = pool.replace(/<UniqueSequenceId>[^<]*<\/UniqueSequenceId>/, `<UniqueSequenceId>${crypto.randomUUID()}</UniqueSequenceId>`);
|
|
85
|
+
pool = pool.replace(/<UniqueMediaPoolItemId>[^<]*<\/UniqueMediaPoolItemId>/, `<UniqueMediaPoolItemId>${crypto.randomUUID()}</UniqueMediaPoolItemId>`);
|
|
86
|
+
pool = pool.replace(/<SmPTZRPreset DbId="[^"]+"/, `<SmPTZRPreset DbId="${crypto.randomUUID()}"`);
|
|
87
|
+
// The embedded sequence's <Parent> points BACK at the compound's own id —
|
|
88
|
+
// the reference sweep found it as the one un-rewired link (a dangling
|
|
89
|
+
// Parent is presumably as fatal as the dangling MpFolder/SeqRef were).
|
|
90
|
+
pool = pool.replace(/<Parent>[0-9a-f-]{36}<\/Parent>/, `<Parent>${compoundId}</Parent>`);
|
|
91
|
+
{
|
|
92
|
+
const fbM = pool.match(/(<Sm2Sequence DbId="[^"]+">\s*<FieldsBlob>)([0-9a-fA-F]+)(<\/FieldsBlob>)/);
|
|
93
|
+
if (!fbM) throw new Error('placeCompound: embedded Sm2Sequence FieldsBlob not found');
|
|
94
|
+
const dict = decodeKeyedDict(Buffer.from(fbM[2], 'hex'));
|
|
95
|
+
const seqRef = dict.entries.find((e) => e.key === 'SeqRef');
|
|
96
|
+
if (!seqRef) throw new Error('placeCompound: embedded blob has no SeqRef entry — schema drift, refusing to author a crasher');
|
|
97
|
+
seqRef.value = innerContainerId;
|
|
98
|
+
const patched = encodeKeyedDict({ hdr: dict.hdr, entries: dict.entries }).toString('hex');
|
|
99
|
+
pool = pool.replace(fbM[0], `${fbM[1]}${patched}${fbM[3]}`);
|
|
100
|
+
}
|
|
81
101
|
pool = pool.replace(/<Name>[^<]*<\/Name>/, `<Name>${escapeXml(name)}</Name>`);
|
|
82
102
|
// MediaExtents = [startSec, durationSec] LE doubles; inner starts at 0.
|
|
83
103
|
const me = Buffer.alloc(16);
|
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.132.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()}")
|