davinci-resolve-mcp 2.115.1 → 2.116.1

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.
@@ -42,6 +42,7 @@ window. `render.verify_output` covers the container-level checks.
42
42
  | Media cuts, multi-source | `media: [{mediaFilePath, spec, cuts}]` | v2.106–2.107 |
43
43
  | Multi-track video (V2+ stacking) | `cuts[].track` (video-only above V1) | v2.112 |
44
44
  | Cross-dissolves | `transitions: [{track, atFrame, durationFrames}]` | v2.111 |
45
+ | Audio cross-fades | `transitions[].trackType: 'audio'` | v2.116 |
45
46
  | Constant retimes, forward | `cuts[].speed` (e.g. `0.5`) | v2.113 |
46
47
  | Constant retimes, reverse | `cuts[].reverse` | v2.114 |
47
48
  | Audio placements, A1–A8 | `cuts[].audioOnly + track` | v2.115 |
@@ -536,7 +536,7 @@ values, or automation-hostile modal prompts.
536
536
  ### MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form
537
537
 
538
538
  - **Object:** `Sm2TimeMap (per-clip retime blob)`
539
- - **Behavior:** Resolve 21 encodes a retimed clip's KeyframesBA as protobuf points; Resolve 19.1.3 encodes it as a keyed-dict of keyed-dict keyframes ({interp, YOut, YIn, Y, XOut, XIn, X}). On import, 19 SILENTLY IGNORES the protobuf form — the clip reads back and plays at 100% with no warning (measured: identical timelines, one per form; protobuf → source 0..96 over 96 frames, keyed → source 0..48 over 96 frames and a live 50% render). The map spans the WHOLE source stretched by 1/speed; the clip's <In>/<Duration> window into it in RECORD frames (srcIn converts by /speed). REVERSE is the same envelope with the Y endpoints swapped - kf0=(0,YMax), kf1=(XMax,0) - and In then measures from the source END: (frames - srcIn - dur*speed)/speed (measured: a reversed srcIn-24 dur-48 cut reads back source 71->23).
539
+ - **Behavior:** Resolve 21 encodes a retimed clip's KeyframesBA as protobuf points; Resolve 19.1.3 encodes it as a keyed-dict of keyed-dict keyframes ({interp, YOut, YIn, Y, XOut, XIn, X}). On import, 19 SILENTLY IGNORES the protobuf form — the clip reads back and plays at 100% with no warning (measured: identical timelines, one per form; protobuf → source 0..96 over 96 frames, keyed → source 0..48 over 96 frames and a live 50% render). The map spans the WHOLE source stretched by 1/speed; the clip's <In>/<Duration> window into it in RECORD frames (srcIn converts by /speed). REVERSE is the same envelope with the Y endpoints swapped - kf0=(0,YMax), kf1=(XMax,0) - and In then measures from the source END: (frames - srcIn - dur*speed)/speed (measured: a reversed srcIn-24 dur-48 cut reads back source 71->23). A FLAT map (both keyframes at the same Y - a freeze) is the one shape where readback and render DIVERGE: the item reads back frozen (source 96..96) but renders MOVING (48/48 unique frames measured). Do not author freezes as flat timemaps.
540
540
  - **Workaround / current handling:** Author retimes for pre-21 hosts with the keyed form (drt.assemble cuts[].speed does this; encoder byte-exact against a live 19.1.3.7 harvest). Treat any cross-generation timemap as unverified until a readback shows the retimed source range.
541
541
  - **Tags:** retime, import, silent-failure, drt
542
542
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.115.1"
40
+ VERSION = "2.116.1"
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.115.1",
3
+ "version": "2.116.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -299,6 +299,7 @@ export function eventsToAssembleSpec(events, opts = {}) {
299
299
  const audioTrackNum = (t) => { const m = /^A(\d+)?$/.exec(String(t)); return m && m[1] ? parseInt(m[1], 10) : 1; };
300
300
  const audioPlacements = [];
301
301
  const audioRetimesSkipped = [];
302
+ const audioTransCandidates = [];
302
303
  for (const e of auds) {
303
304
  const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
304
305
  const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
@@ -309,7 +310,16 @@ export function eventsToAssembleSpec(events, opts = {}) {
309
310
  }
310
311
  const track = audioTrackNum(e.track);
311
312
  const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), audioOnly: true, track };
312
- audioPlacements.push({ start: recIn, end: recOut, index: e.index, track });
313
+ if (e.transition) {
314
+ let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
315
+ d += d % 2;
316
+ audioTransCandidates.push({
317
+ atFrame: recIn, durationFrames: d, track,
318
+ index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
319
+ source: e.source, srcIn: cut.srcIn,
320
+ });
321
+ }
322
+ audioPlacements.push({ start: recIn, end: recOut, index: e.index, track, source: e.source, srcIn: cut.srcIn, durationFrames });
313
323
  if (!perSource.has(e.source)) perSource.set(e.source, []);
314
324
  perSource.get(e.source).push(cut);
315
325
  }
@@ -384,6 +394,29 @@ export function eventsToAssembleSpec(events, opts = {}) {
384
394
  }
385
395
  transitions.push({ track: c.track, atFrame: c.atFrame, durationFrames: c.durationFrames });
386
396
  }
397
+ // Audio cross-fades, same geometry rules (render-verified on 19.1.3.7 via
398
+ // the harvested cross-fade template: the highpass RMS ramps, not steps).
399
+ for (const c of audioTransCandidates) {
400
+ const prev = audioPlacements.find((pl) => pl.track === c.track && pl.end === c.atFrame);
401
+ if (!prev) {
402
+ droppedTransitions.push({ index: c.index, type: c.type, duration: c.rawDuration, trackType: 'audio', reason: 'no abutting predecessor at the cut' });
403
+ continue;
404
+ }
405
+ const half = c.durationFrames / 2;
406
+ const bHandle = c.srcIn >= half;
407
+ const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
408
+ const aFrames = aSpec && Number(aSpec.frameCount);
409
+ const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + half <= aFrames : false;
410
+ if (!bHandle || !aHandle) {
411
+ droppedTransitions.push({
412
+ index: c.index, type: c.type, duration: c.rawDuration, trackType: 'audio',
413
+ reason: `insufficient handles for a centered ${c.durationFrames}f cross-fade` +
414
+ `${bHandle ? '' : ' (incoming srcIn < half)'}${aHandle ? '' : ' (outgoing tail media < half)'}`,
415
+ });
416
+ continue;
417
+ }
418
+ transitions.push({ track: c.track, atFrame: c.atFrame, durationFrames: c.durationFrames, trackType: 'audio' });
419
+ }
387
420
 
388
421
  return {
389
422
  spec: { timelineName, media, ...(transitions.length ? { transitions } : {}) },
@@ -46,7 +46,7 @@ const assembleSchema = z.object({
46
46
  spec: z
47
47
  .object({})
48
48
  .passthrough()
49
- .describe("assembleTimeline spec: { timelineName?, 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), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
49
+ .describe("assembleTimeline spec: { timelineName?, 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)}], 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)."),
50
50
  outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
51
51
  targetAppVersion: z
52
52
  .union([z.string(), z.number()])
@@ -44,5 +44,7 @@ test('placeTransition errors when no abutting boundary at atFrame', async () =>
44
44
  test('placeTransition validates args', async () => {
45
45
  const buf = await synth2();
46
46
  await assert.rejects(() => placeTransition(buf, { track: 1, atFrame: 100, durationFrames: 1 }), /durationFrames/);
47
- await assert.rejects(() => placeTransition(buf, { track: 1, atFrame: 100, trackType: 'audio' }), /only video/);
47
+ // trackType 'audio' is now supported (harvested cross-fade template,
48
+ // v2.116.0); an unknown trackType still refuses.
49
+ await assert.rejects(() => placeTransition(buf, { track: 1, atFrame: 100, trackType: 'subtitle' }), /video or audio/);
48
50
  });
@@ -181,6 +181,7 @@ async function assembleTimeline(spec = {}) {
181
181
  if (!tr || typeof tr !== 'object') throw new TypeError(`assembleTimeline: transitions[${i}] must be an object`);
182
182
  ({ buffer } = await placeTransition(buffer, {
183
183
  track: tr.track, atFrame: tr.atFrame, durationFrames: tr.durationFrames,
184
+ trackType: tr.trackType || 'video',
184
185
  }));
185
186
  }
186
187
 
@@ -33,6 +33,11 @@ function splitItems(itemsInner) {
33
33
  }
34
34
 
35
35
  const TEMPLATE_PATH = path.join(__dirname, 'templates', 'transition-cross-dissolve.xml');
36
+ // Audio cross-fade, harvested from a live 19.1.3.7 XMEML import of an FCP7
37
+ // KGAudioTransCrossFade (render-verified: the highpass RMS ramps through the
38
+ // junction instead of stepping). PrettyType reads "Final Cut Pro 7" — that is
39
+ // what Resolve itself stores for it, and it renders.
40
+ const AUDIO_TEMPLATE_PATH = path.join(__dirname, 'templates', 'transition-cross-fade-r19.xml');
36
41
 
37
42
  const clipStart = (c) => { const m = c.match(/<Start>(\d+)<\/Start>/); return m ? parseInt(m[1], 10) : null; };
38
43
  const clipDuration = (c) => { const m = c.match(/<Duration>(\d+)<\/Duration>/); return m ? parseInt(m[1], 10) : null; };
@@ -45,7 +50,7 @@ const clipDuration = (c) => { const m = c.match(/<Duration>(\d+)<\/Duration>/);
45
50
  * @param {number} opts.track - 1-based video track.
46
51
  * @param {number} opts.atFrame - the cut frame (where one clip ends and the next begins).
47
52
  * @param {number} [opts.durationFrames=24] - transition length (even number recommended; centered).
48
- * @param {'video'} [opts.trackType='video'] - audio cross-fade uses a different template (not bundled).
53
+ * @param {'video'|'audio'} [opts.trackType='video'] - 'audio' places the harvested cross-fade (render-verified on 19.1.3).
49
54
  * @param {string} [opts.timelineUuid]
50
55
  * @returns {Promise<{buffer:Buffer, entry:string, timelineUuid:string, track:number,
51
56
  * atFrame:number, start:number, durationFrames:number, transitionDbId:string|null}>}
@@ -55,7 +60,7 @@ async function placeTransition(drpInput, opts = {}) {
55
60
  if (!Number.isInteger(track) || track < 1) throw new TypeError('placeTransition: track must be a positive integer');
56
61
  if (!Number.isInteger(atFrame)) throw new TypeError('placeTransition: atFrame must be an integer');
57
62
  if (!Number.isInteger(durationFrames) || durationFrames < 2) throw new TypeError('placeTransition: durationFrames must be an integer >= 2');
58
- if (trackType !== 'video') throw new Error('placeTransition: only video cross-dissolve is supported (no bundled audio template)');
63
+ if (trackType !== 'video' && trackType !== 'audio') throw new Error('placeTransition: trackType must be video or audio');
59
64
 
60
65
  const zip = await loadDrpZip(drpInput);
61
66
  const { entry, xml: seqXml, seqId } = await selectTargetSeq(zip, timelineUuid);
@@ -71,7 +76,7 @@ async function placeTransition(drpInput, opts = {}) {
71
76
  }
72
77
  if (leftIdx < 0) throw new Error(`placeTransition: no abutting clip boundary at frame ${atFrame} on track ${track}`);
73
78
 
74
- let trans = fs.readFileSync(TEMPLATE_PATH, 'utf8').trim();
79
+ let trans = fs.readFileSync(trackType === 'audio' ? AUDIO_TEMPLATE_PATH : TEMPLATE_PATH, 'utf8').trim();
75
80
  trans = freshDbIds(trans);
76
81
  const start = atFrame - Math.floor(durationFrames / 2); // centered (AlignmentType 2)
77
82
  trans = trans.replace(/<Start>\d+<\/Start>/, `<Start>${start}</Start>`);
@@ -0,0 +1,22 @@
1
+ <Element>
2
+ <Sm2TiTransition DbId="43e55250-3117-4f6e-abf0-ce028e94bf71">
3
+ <FieldsBlob>000000020000001b8012180000002c789c636660640001e62b75aa0c04000033c3017f</FieldsBlob>
4
+ <PrettyType>Final Cut Pro 7</PrettyType>
5
+ <Name/>
6
+ <Start>36</Start>
7
+ <Duration>24</Duration>
8
+ <LinkedItemSync/>
9
+ <WasDisbanded>false</WasDisbanded>
10
+ <MarkersBA/>
11
+ <UiMemento>0</UiMemento>
12
+ <Flags>0</Flags>
13
+ <PriorityIndex>0</PriorityIndex>
14
+ <EffectFiltersBA>00000002000001418128b52ffd60ea02b50900d24e3631608bda00016b1886611886618aaa90c45a8b2c4bb74e225968cc77bbadcdb43f34bdb7510008cd286032991041dcafba3a6db563c78e1f207ad278bd56901d3be66054645e53f64ec70e0cfdd6395ec67bbd5ad6a24a3d7f9d724c23acdc237c3969bcd14eafb71f24ba8e1ffab3d3eb4423ac3212140da3e3030d6c4098b087fe98b5b46efaa5cf4b7da7f3948a06887acc1863ab5b85577fe4cb10e35c6c4c1b8b08035e8d6996843765b48b674648a9f9ac295016da65d268eb7bb08f0c615802d148a3258514040c7448221f13001601828924325a012300e9f0abc6e90af86f499c4a043085b2b044a134bcb479940460f4f9df4213e27805a1a8862fe8bcb86058bb0b23aeab01486d4073cdcda64fc1d02980a688b20aada10ad6ead65663955acbce363a856d7330570bd2da6206</EffectFiltersBA>
15
+ <ImportExportMetadataBA/>
16
+ <RenderTextEnabled>true</RenderTextEnabled>
17
+ <RenderTextGanged>true</RenderTextGanged>
18
+ <RenderTextPrefixed>true</RenderTextPrefixed>
19
+ <AlignmentType>2</AlignmentType>
20
+ <Position>2</Position>
21
+ </Sm2TiTransition>
22
+ </Element>
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.115.1"
90
+ VERSION = "2.116.1"
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.115.1"
14
+ VERSION = "2.116.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -1957,7 +1957,12 @@ API_TRUTH: List[Dict[str, Any]] = [
1957
1957
  "envelope with the Y endpoints swapped - kf0=(0,YMax), "
1958
1958
  "kf1=(XMax,0) - and In then measures from the source "
1959
1959
  "END: (frames - srcIn - dur*speed)/speed (measured: a "
1960
- "reversed srcIn-24 dur-48 cut reads back source 71->23).",
1960
+ "reversed srcIn-24 dur-48 cut reads back source 71->23). "
1961
+ "A FLAT map (both keyframes at the same Y - a freeze) is "
1962
+ "the one shape where readback and render DIVERGE: the "
1963
+ "item reads back frozen (source 96..96) but renders "
1964
+ "MOVING (48/48 unique frames measured). Do not author "
1965
+ "freezes as flat timemaps.",
1961
1966
  "recommended": "Author retimes for pre-21 hosts with the keyed "
1962
1967
  "form (drt.assemble cuts[].speed does this; encoder "
1963
1968
  "byte-exact against a live 19.1.3.7 harvest). Treat "