davinci-resolve-mcp 2.113.0 → 2.114.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.
@@ -529,7 +529,7 @@ values, or automation-hostile modal prompts.
529
529
  ### MediaTimemapBA keyframes are generation-split; 19.x silently ignores the R21 protobuf form
530
530
 
531
531
  - **Object:** `Sm2TimeMap (per-clip retime blob)`
532
- - **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).
532
+ - **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).
533
533
  - **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.
534
534
  - **Tags:** retime, import, silent-failure, drt
535
535
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.113.0"
40
+ VERSION = "2.114.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.113.0",
3
+ "version": "2.114.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -202,9 +202,8 @@ export function eventsToOTIO(events, opts = {}) {
202
202
  * timeline runs 24fps with origin 86400, so rec/src frames convert as
203
203
  * round(frames × 24 / nominalFps). Placement anchors the EARLIEST video
204
204
  * event at the origin. Honesty ledger in the returned report: flattened
205
- * reverse retimes (forward constant speeds are AUTHORED as real Sm2TimeMaps
206
- * — r19 keyed form, render/readback-verified; reverse flattens with the
207
- * reason), authored vs
205
+ * zero-speed freezes (constant retimes — forward AND reverse — are AUTHORED
206
+ * as real Sm2TimeMaps, r19 keyed form, readback/render-verified), authored vs
208
207
  * dropped transitions (cross-dissolves are AUTHORED when the predecessor
209
208
  * abuts the cut and both sides have handle media — render-verified on
210
209
  * 19.1.3.7; otherwise dropped with the reason, as a cut at the boundary),
@@ -258,15 +257,17 @@ export function eventsToAssembleSpec(events, opts = {}) {
258
257
  const vTrack = trackNum(e.track);
259
258
  const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), ...(vTrack > 1 ? { track: vTrack } : {}) };
260
259
  if ((e.speed ?? 100) !== 100 || e.reverse) {
261
- if (e.reverse || !(e.speed > 0)) {
262
- // Reverse needs a descending timemap (not yet measured) — flatten, with the reason.
263
- flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse, reason: 'reverse not supported — played forward at 100%' });
260
+ const spd = Math.abs(e.speed ?? 100);
261
+ if (!(spd > 0)) {
262
+ flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse, reason: 'zero speed (freeze) not supported — played forward at 100%' });
264
263
  } else {
265
- // Forward constant speed: authored as a real Sm2TimeMap on the cut
266
- // (r19 keyed form; render/readback-verified on 19.1.3.7). Audio for
267
- // retimed cuts is video-only downstream.
268
- cut.speed = e.speed / 100;
269
- authoredRetimes.push({ index: e.index, source: e.source, speed: e.speed });
264
+ // Constant speed, forward or reverse: authored as a real Sm2TimeMap
265
+ // on the cut (r19 keyed form; readback/render-verified on 19.1.3.7 —
266
+ // reverse reads back source 71→23 for a srcIn-24 dur-48 cut). Audio
267
+ // for retimed cuts is video-only downstream.
268
+ if (spd !== 100) cut.speed = spd / 100;
269
+ if (e.reverse) cut.reverse = true;
270
+ authoredRetimes.push({ index: e.index, source: e.source, speed: spd, ...(e.reverse ? { reverse: true } : {}) });
270
271
  }
271
272
  }
272
273
  if (e.transition) {
@@ -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? (forward constant retime, e.g. 0.5; video-only; 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)}]} | [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)."),
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()])
@@ -114,19 +114,28 @@ async function assembleTimeline(spec = {}) {
114
114
  sources.forEach((src, i) => {
115
115
  (src.cuts || []).forEach((cut) => {
116
116
  const out = mediaRefs[i] ? { ...cut, mediaRef: mediaRefs[i] } : { ...cut };
117
- if (cut.speed !== undefined && cut.speed !== 1) {
117
+ if (cut.reverse || (cut.speed !== undefined && cut.speed !== 1)) {
118
118
  // Constant-speed retime (forward only). The Sm2TimeMap spans the
119
119
  // whole source stretched by 1/speed; the clip windows into it with
120
120
  // RECORD-domain In/Duration (measured live on 19.1.3.7), so the
121
121
  // source-domain srcIn converts by /speed here.
122
- if (!(cut.speed > 0)) throw new RangeError('assembleTimeline: cut.speed must be > 0 (reverse not supported)');
122
+ const speed = cut.speed ?? 1;
123
+ if (!(speed > 0)) throw new RangeError('assembleTimeline: cut.speed must be > 0 (use cut.reverse for backwards)');
123
124
  const fps = Math.round(src.spec.fps || 24);
124
125
  out.timemap = buildConstantSpeedTimemapKeyed({
125
- speed: cut.speed, sourceFrames: src.spec.frameCount, fps,
126
- uniqueId: randomUUID(),
126
+ speed, sourceFrames: src.spec.frameCount, fps,
127
+ uniqueId: randomUUID(), reverse: !!cut.reverse,
127
128
  }).toString('hex');
128
- out.srcIn = Math.round((cut.srcIn ?? 0) / cut.speed);
129
+ // Record-domain In. Forward: srcIn/speed. Reverse: the clip plays
130
+ // source [srcIn, srcIn + dur*speed) BACKWARDS — the map descends
131
+ // from the source tail, so In measures from the END:
132
+ // (sourceFrames - srcIn - dur*speed)/speed (measured live: In=0
133
+ // on a reversed full-speed clip reads back source 191→143).
134
+ out.srcIn = cut.reverse
135
+ ? Math.round((src.spec.frameCount - (cut.srcIn ?? 0) - cut.durationFrames * speed) / speed)
136
+ : Math.round((cut.srcIn ?? 0) / speed);
129
137
  delete out.speed;
138
+ delete out.reverse;
130
139
  }
131
140
  allCuts.push(out);
132
141
  });
@@ -227,8 +227,8 @@ function buildConstantSpeedTimemap({ speed, sourceDurationSec, uniqueId, recordD
227
227
  * @param {string} p.uniqueId - fresh uuid (bare, no braces).
228
228
  * @returns {Buffer}
229
229
  */
230
- function buildConstantSpeedTimemapKeyed({ speed, sourceFrames, fps = 24, uniqueId }) {
231
- if (!(speed > 0)) throw new RangeError('buildConstantSpeedTimemapKeyed: speed must be > 0 (reverse not supported here)');
230
+ function buildConstantSpeedTimemapKeyed({ speed, sourceFrames, fps = 24, uniqueId, reverse = false }) {
231
+ if (!(speed > 0)) throw new RangeError('buildConstantSpeedTimemapKeyed: speed must be > 0 (pass reverse:true for backwards)');
232
232
  if (!Number.isInteger(sourceFrames) || sourceFrames < 1) throw new TypeError('buildConstantSpeedTimemapKeyed: sourceFrames must be a positive integer');
233
233
  const YMax = (sourceFrames - 1) / fps;
234
234
  const XMax = (sourceFrames / speed - 1) / fps;
@@ -241,9 +241,13 @@ function buildConstantSpeedTimemapKeyed({ speed, sourceFrames, fps = 24, uniqueI
241
241
  { key: 'XIn', type: T_DOUBLE, subType: 0, value: 0 },
242
242
  { key: 'X', type: T_DOUBLE, subType: 0, value: X },
243
243
  ] }).toString('hex');
244
+ // Reverse (harvested from a live 19.1.3.7 -100% XMEML retime): the SAME
245
+ // envelope with the Y endpoints swapped — kf0=(0, YMax), kf1=(XMax, 0),
246
+ // a descending line. Forward keeps kf1 Y = XMax*speed (harvest: +half-frame
247
+ // convention falls out of the arithmetic, byte-exact either way).
244
248
  const keyframes = encodeKeyedDict({ hdr: 1, entries: [
245
- { key: '1', type: T_BYTES, subType: 0, value: kf(XMax, XMax * speed) },
246
- { key: '0', type: T_BYTES, subType: 0, value: kf(0, 0) },
249
+ { key: '1', type: T_BYTES, subType: 0, value: reverse ? kf(XMax, 0) : kf(XMax, XMax * speed) },
250
+ { key: '0', type: T_BYTES, subType: 0, value: reverse ? kf(0, YMax) : kf(0, 0) },
247
251
  ] }).toString('hex');
248
252
  return encodeKeyedDict({ hdr: 1, entries: [
249
253
  { key: 'YMax', type: T_DOUBLE, subType: 0, value: YMax },
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.113.0"
90
+ VERSION = "2.114.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.113.0"
14
+ VERSION = "2.114.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -1927,7 +1927,11 @@ API_TRUTH: List[Dict[str, Any]] = [
1927
1927
  "source 0..48 over 96 frames and a live 50% render). "
1928
1928
  "The map spans the WHOLE source stretched by 1/speed; "
1929
1929
  "the clip's <In>/<Duration> window into it in RECORD "
1930
- "frames (srcIn converts by /speed).",
1930
+ "frames (srcIn converts by /speed). REVERSE is the same "
1931
+ "envelope with the Y endpoints swapped - kf0=(0,YMax), "
1932
+ "kf1=(XMax,0) - and In then measures from the source "
1933
+ "END: (frames - srcIn - dur*speed)/speed (measured: a "
1934
+ "reversed srcIn-24 dur-48 cut reads back source 71->23).",
1931
1935
  "recommended": "Author retimes for pre-21 hosts with the keyed "
1932
1936
  "form (drt.assemble cuts[].speed does this; encoder "
1933
1937
  "byte-exact against a live 19.1.3.7 harvest). Treat "