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.
@@ -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` (one per archive; nested edit renders) | v2.131 |
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.131.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.131.0",
3
+ "version": "2.132.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -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?}]}] (ONE per archive; inner cuts need captured templates w/ native clips; render-verified nested playback 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)."),
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
- // The harvested compound cluster keeps its ORIGINAL GUIDs verbatim: the
60
- // embedded Sm2Sequence FieldsBlob almost certainly encodes cluster
61
- // identities, and freshening the XML ids around an unchanged blob CRASHED
62
- // Resolve on import (measured, E47/E47c — the app died, twice). The
63
- // harvest GUIDs cannot collide with an assembled archive's fresh ids, so
64
- // verbatim is safe — but it limits authoring to ONE compound per archive.
65
- const pool0 = fs.readFileSync(T('compound-pool-r19.xml'), 'utf8');
66
- const compoundId = pool0.match(/<Sm2MpCompoundClip DbId="([^"]+)"/)[1];
67
- const innerSequenceId = pool0.match(/<Sm2Sequence DbId="([^"]+)"/)[1];
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: template verbatim, Items emptied (content added by
75
- // the caller via the cuts machinery, 0-based frames).
76
- const inner = inner0.replace(/<Items>[\s\S]*?<\/Items>/g, '<Items/>');
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: verbatim identities; only name + extents change.
80
- let pool = pool0;
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);
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.131.0"
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()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.131.0"
14
+ VERSION = "2.132.0"
15
15
 
16
16
  import base64
17
17
  import os