davinci-resolve-mcp 2.141.0 → 2.142.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.142.1 — the AAF leg joins the span fix
6
+
7
+ ### Fixed
8
+
9
+ - **AAF dissolves were misaligned by half their duration**: the AAF walker
10
+ rewinds the incoming clip to the overlap START (the Edit Protocol
11
+ subtraction), so the parsed junction is the overlap's first frame — and the
12
+ old centered placement put half the blend before the overlap even began.
13
+ AAF transitions now carry `alignment: 'start'` through the same
14
+ render-proven span machinery as EDL (v2.142.0): span `[overlapStart,
15
+ overlapStart+dur)`, boundary shifted to the middle — which lands the
16
+ notional cut exactly at the AAF CutPoint default (centered in the overlap).
17
+
18
+ ## What's New in v2.142.0 — transition span fidelity, and the edge law
19
+
20
+ ### Changed (conform fidelity)
21
+
22
+ - **Transition spans now follow the turnover's actual geometry** instead of
23
+ always centering: EDL dissolves/wipes span `[cut, cut+dur)` (the CMX
24
+ convention), OTIO transitions use their explicit in/out offsets, XMEML
25
+ transitionitems use their own record span, and spec-level
26
+ `transitions[].startFrame` overrides. Handle requirements follow the real
27
+ span — a start-at-cut EDL dissolve needs NO incoming handle and a
28
+ full-duration outgoing tail. Render-verified: the authored EDL dissolve
29
+ blends exactly `[cut, cut+24)` (pure outgoing through cut−1, linear blend,
30
+ midpoint 182), matching Resolve's own EDL importer frame-for-frame.
31
+
32
+ ### Measured (the edge law)
33
+
34
+ - A transition's rendered span follows `<Start>`/`<Duration>`
35
+ (`AlignmentType` is cosmetic), **but the clip boundary must sit strictly
36
+ inside the span** — an edge-aligned span (Start == the boundary) renders
37
+ inert. That is why Resolve's own EDL importer moves the cut +dur/2 and
38
+ centers; the bridge now reproduces exactly that cut-reshaping. Off-center
39
+ spans render fine (an uneven `[cut−6, cut+18)` span blended linearly
40
+ across its full width).
41
+
5
42
  ## What's New in v2.141.0 — OTIO transitions, depth-4, and honest gaps
6
43
 
7
44
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.141.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.142.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.141.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.142.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.141.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.142.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -135,10 +135,20 @@ outgoing clip through the window) — so routing an XMEML turnover through
135
135
  `assemble_from_interchange` produces transitions that render where the host
136
136
  importer's do not.
137
137
 
138
- **Dissolve geometry.** A transition is authored only when the predecessor
139
- ends exactly at the cut and both sides have handle media for the centered
140
- span (incoming `srcIn dur/2`; outgoing `srcIn + dur + dur/2 frameCount`).
141
- Everything else stays in `droppedTransitions` with the reason.
138
+ **Dissolve geometry spans follow the turnover, and the edge law.** A
139
+ transition's rendered span follows `<Start>`/`<Duration>`; `AlignmentType`
140
+ is cosmetic. The clip boundary must sit STRICTLY INSIDE the span an
141
+ edge-aligned span (Start == the boundary) renders inert (measured), which is
142
+ why Resolve's own EDL importer moves the cut +dur/2 and centers. The bridge
143
+ reproduces per-format geometry exactly: EDL dissolves span [cut, cut+dur)
144
+ with the boundary shifted +dur/2 (CMX convention, host-importer parity —
145
+ render-verified: blend f48→f71 on a cut at 48); OTIO uses its explicit
146
+ in/out offsets; XMEML uses the transitionitem's own record span; spec-level
147
+ `transitions[].startFrame` overrides. Handles follow the actual span: the
148
+ incoming needs `srcIn ≥` the pre-cut portion, the outgoing needs tail media
149
+ for the post-cut portion. Off-center spans render (blend measured across an
150
+ uneven [cut-6, cut+18) span); everything unauthorable stays in
151
+ `droppedTransitions` with the reason.
142
152
 
143
153
  ## Verification checklist for a delivered .drt
144
154
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.141.0"
40
+ VERSION = "2.142.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.141.0",
3
+ "version": "2.142.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -1169,7 +1169,12 @@ def _walk_components(sequence, *, track, fps, rec, state, depth, transition=None
1169
1169
  try:
1170
1170
  if cls == "Transition":
1171
1171
  duration = _length(comp)
1172
- pending_transition = {"type": "dissolve", "duration": duration}
1172
+ # alignment 'start': after the rewind, the incoming clip's recIn IS the
1173
+ # overlap start, so the transition span is [recIn, recIn+duration) — and
1174
+ # the bridge's boundary-shift lands the notional cut at recIn+duration/2,
1175
+ # exactly the AAF CutPoint default (centered in the overlap). The old
1176
+ # centered-on-recIn placement put HALF the blend before the overlap began.
1177
+ pending_transition = {"type": "dissolve", "duration": duration, "alignment": "start"}
1173
1178
  # Rewind: the next component overlaps the previous one by `duration`.
1174
1179
  # Clamped at this sequence's own start — a leading transition has no
1175
1180
  # preceding material to overlap, and a negative record position would
@@ -307,14 +307,26 @@ export function eventsToAssembleSpec(events, opts = {}) {
307
307
  // be authored (abutting predecessor + handles both sides) is decided
308
308
  // after all placements are known.
309
309
  let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
310
- d += d % 2; // placeTransition centers on the cut; keep it even
310
+ d += d % 2; // even duration keeps every alignment on whole frames
311
+ // Span geometry per format (E73): `pre` = frames of the span BEFORE
312
+ // the cut. EDL: 0 (CMX start-at-cut — matches what Resolve's own EDL
313
+ // importer authors, E61 harvest); OTIO: the explicit in_offset; XMEML:
314
+ // derived from the transitionitem's own record start; no info: centered.
315
+ let pre = Math.floor(d / 2);
316
+ if (e.transition.alignment === 'start') pre = 0;
317
+ else if (e.transition.inOffset != null) pre = Math.min(d, Math.max(0, toTl(e.transition.inOffset, e.fps)));
318
+ else if (e.transition.recStart != null) {
319
+ const spanStartAbs = ORIGIN + (toTl(e.transition.recStart, e.fps) - minRec);
320
+ pre = Math.min(d, Math.max(0, recIn - spanStartAbs));
321
+ }
311
322
  transitionCandidates.push({
312
- atFrame: recIn, durationFrames: d, track: vTrack,
323
+ atFrame: recIn, durationFrames: d, track: vTrack, pre,
324
+ explicitSpan: e.transition.alignment != null || e.transition.inOffset != null || e.transition.recStart != null,
313
325
  index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
314
- source: e.source, srcIn: cut.srcIn,
326
+ source: e.source, srcIn: cut.srcIn, incomingCutRef: cut,
315
327
  });
316
328
  }
317
- placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: cut.srcIn, durationFrames, track: vTrack });
329
+ placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: cut.srcIn, durationFrames, track: vTrack, cutRef: cut });
318
330
  if (!perSource.has(e.source)) perSource.set(e.source, []);
319
331
  perSource.get(e.source).push(cut);
320
332
  }
@@ -341,13 +353,17 @@ export function eventsToAssembleSpec(events, opts = {}) {
341
353
  if (e.transition) {
342
354
  let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
343
355
  d += d % 2;
356
+ let pre = Math.floor(d / 2);
357
+ if (e.transition.alignment === 'start') pre = 0;
358
+ else if (e.transition.inOffset != null) pre = Math.min(d, Math.max(0, toTl(e.transition.inOffset, e.fps)));
344
359
  audioTransCandidates.push({
345
- atFrame: recIn, durationFrames: d, track,
360
+ atFrame: recIn, durationFrames: d, track, pre,
361
+ explicitSpan: e.transition.alignment != null || e.transition.inOffset != null,
346
362
  index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
347
- source: e.source, srcIn: cut.srcIn,
363
+ source: e.source, srcIn: cut.srcIn, incomingCutRef: cut,
348
364
  });
349
365
  }
350
- audioPlacements.push({ start: recIn, end: recOut, index: e.index, track, source: e.source, srcIn: cut.srcIn, durationFrames });
366
+ audioPlacements.push({ start: recIn, end: recOut, index: e.index, track, source: e.source, srcIn: cut.srcIn, durationFrames, cutRef: cut });
351
367
  if (!perSource.has(e.source)) perSource.set(e.source, []);
352
368
  perSource.get(e.source).push(cut);
353
369
  }
@@ -412,16 +428,21 @@ export function eventsToAssembleSpec(events, opts = {}) {
412
428
  droppedTransitions.push({ index: c.index, type: c.type, duration: c.rawDuration, reason: 'no abutting predecessor at the cut' });
413
429
  continue;
414
430
  }
415
- const half = c.durationFrames / 2;
416
- const bHandle = c.srcIn >= half;
431
+ // Handle math follows the actual span: `pre` frames before the cut need
432
+ // incoming pre-roll (srcIn >= pre); `post` frames after need outgoing
433
+ // tail media. A start-at-cut EDL dissolve needs NO incoming handle and a
434
+ // FULL-duration outgoing tail.
435
+ const pre = c.pre ?? c.durationFrames / 2;
436
+ const post = c.durationFrames - pre;
437
+ const bHandle = c.srcIn >= pre;
417
438
  const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
418
439
  const aFrames = aSpec && Number(aSpec.frameCount);
419
- const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + half <= aFrames : false;
440
+ const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false;
420
441
  if (!bHandle || !aHandle) {
421
442
  droppedTransitions.push({
422
443
  index: c.index, type: c.type, duration: c.rawDuration,
423
- reason: `insufficient handles for a centered ${c.durationFrames}f dissolve` +
424
- `${bHandle ? '' : ' (incoming srcIn < half)'}${aHandle ? '' : ' (outgoing tail media < half)'}`,
444
+ reason: `insufficient handles for a ${c.durationFrames}f dissolve spanning [cut-${pre}, cut+${post})` +
445
+ `${bHandle ? '' : ` (incoming srcIn < ${pre})`}${aHandle ? '' : ` (outgoing tail media < ${post})`}`,
425
446
  });
426
447
  continue;
427
448
  }
@@ -438,7 +459,28 @@ export function eventsToAssembleSpec(events, opts = {}) {
438
459
  else if (/additive/i.test(raw)) kind = 'additive';
439
460
  else if (/fade to color/i.test(raw)) kind = 'fade-to-color';
440
461
  else if (/smooth cut/i.test(raw)) kind = 'smooth-cut';
441
- transitions.push({ track: c.track, atFrame: c.atFrame, durationFrames: c.durationFrames, ...(kind === 'dissolve' ? {} : { type: kind }) });
462
+ // EDGE LAW (E73, measured): the clip boundary must sit STRICTLY INSIDE
463
+ // the transition span — an edge-aligned span (Start == boundary) renders
464
+ // INERT. Resolve's own EDL importer solves the CMX start-at-cut case by
465
+ // MOVING the cut half the duration and centering; reproduce exactly that:
466
+ // extend the outgoing clip, trim the incoming head (source stays
467
+ // aligned), and keep the RENDERED span [cut-pre, cut-pre+d) unchanged.
468
+ let atFrame = c.atFrame;
469
+ const spanStartAbs = c.atFrame - pre;
470
+ if (c.explicitSpan && (pre === 0 || pre === c.durationFrames)) {
471
+ const shift = (pre === 0 ? 1 : -1) * (c.durationFrames / 2);
472
+ prev.cutRef.durationFrames += shift;
473
+ c.incomingCutRef.startFrame += shift;
474
+ c.incomingCutRef.srcIn += shift;
475
+ c.incomingCutRef.durationFrames -= shift;
476
+ prev.end += shift;
477
+ atFrame += shift;
478
+ }
479
+ transitions.push({
480
+ track: c.track, atFrame, durationFrames: c.durationFrames,
481
+ ...(kind === 'dissolve' ? {} : { type: kind }),
482
+ ...(c.explicitSpan ? { startFrame: spanStartAbs } : {}),
483
+ });
442
484
  }
443
485
  // Audio cross-fades, same geometry rules (render-verified on 19.1.3.7 via
444
486
  // the harvested cross-fade template: the highpass RMS ramps, not steps).
@@ -448,20 +490,35 @@ export function eventsToAssembleSpec(events, opts = {}) {
448
490
  droppedTransitions.push({ index: c.index, type: c.type, duration: c.rawDuration, trackType: 'audio', reason: 'no abutting predecessor at the cut' });
449
491
  continue;
450
492
  }
451
- const half = c.durationFrames / 2;
452
- const bHandle = c.srcIn >= half;
493
+ const pre = c.pre ?? c.durationFrames / 2;
494
+ const post = c.durationFrames - pre;
495
+ const bHandle = c.srcIn >= pre;
453
496
  const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
454
497
  const aFrames = aSpec && Number(aSpec.frameCount);
455
- const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + half <= aFrames : false;
498
+ const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false;
456
499
  if (!bHandle || !aHandle) {
457
500
  droppedTransitions.push({
458
501
  index: c.index, type: c.type, duration: c.rawDuration, trackType: 'audio',
459
- reason: `insufficient handles for a centered ${c.durationFrames}f cross-fade` +
460
- `${bHandle ? '' : ' (incoming srcIn < half)'}${aHandle ? '' : ' (outgoing tail media < half)'}`,
502
+ reason: `insufficient handles for a ${c.durationFrames}f cross-fade spanning [cut-${pre}, cut+${post})` +
503
+ `${bHandle ? '' : ` (incoming srcIn < ${pre})`}${aHandle ? '' : ` (outgoing tail media < ${post})`}`,
461
504
  });
462
505
  continue;
463
506
  }
464
- transitions.push({ track: c.track, atFrame: c.atFrame, durationFrames: c.durationFrames, trackType: 'audio' });
507
+ let atFrame = c.atFrame;
508
+ const spanStartAbs = c.atFrame - pre;
509
+ if (c.explicitSpan && (pre === 0 || pre === c.durationFrames)) {
510
+ const shift = (pre === 0 ? 1 : -1) * (c.durationFrames / 2);
511
+ prev.cutRef.durationFrames += shift;
512
+ c.incomingCutRef.startFrame += shift;
513
+ c.incomingCutRef.srcIn += shift;
514
+ c.incomingCutRef.durationFrames -= shift;
515
+ prev.end += shift;
516
+ atFrame += shift;
517
+ }
518
+ transitions.push({
519
+ track: c.track, atFrame, durationFrames: c.durationFrames, trackType: 'audio',
520
+ ...(c.explicitSpan ? { startFrame: spanStartAbs } : {}),
521
+ });
465
522
  }
466
523
 
467
524
  // Turnover markers (EDL * LOC: locators, OTIO Marker objects) → authored
@@ -118,7 +118,10 @@ export function parseEDL(text, opts = {}) {
118
118
  srcOut: tcToFrames(srcOut, fps),
119
119
  recIn: tcToFrames(recIn, fps),
120
120
  recOut: tcToFrames(recOut, fps),
121
- transition: transition && transition !== 'C' ? { type: transition, duration: dur || 0 } : null,
121
+ // CMX convention: the dissolve/wipe span STARTS at the cut (occupies
122
+ // the first `dur` frames of the incoming event) — matching what
123
+ // Resolve's own EDL importer authors (E61 harvest: Start == the cut).
124
+ transition: transition && transition !== 'C' ? { type: transition, duration: dur || 0, alignment: 'start' } : null,
122
125
  fps,
123
126
  }),
124
127
  );
@@ -170,7 +173,9 @@ export function parseOTIO(otio, opts = {}) {
170
173
  if (schema.startsWith('Transition')) {
171
174
  const inOff = (child.in_offset && child.in_offset.value) || 0;
172
175
  const outOff = (child.out_offset && child.out_offset.value) || 0;
173
- pendingTransition = { type: String(child.transition_type || 'SMPTE_Dissolve'), duration: inOff + outOff };
176
+ // in_offset = frames BEFORE the cut, out_offset = frames after; the
177
+ // bridge places the span [cut - inOffset, cut + outOffset).
178
+ pendingTransition = { type: String(child.transition_type || 'SMPTE_Dissolve'), duration: inOff + outOff, inOffset: inOff };
174
179
  continue;
175
180
  }
176
181
  if (schema.startsWith('Clip')) {
@@ -272,7 +277,10 @@ export function parseXMEMLEvents(xml, opts = {}) {
272
277
  if (!Number.isFinite(s) || !Number.isFinite(e2)) continue;
273
278
  const effectId = (tr.effect && (tr.effect.effectid || tr.effect.name)) || 'Cross Dissolve';
274
279
  const incoming = events.slice(before).find((ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2);
275
- if (incoming) incoming.transition = { type: String(effectId), duration: e2 - s };
280
+ // recStart carries the transitionitem's EXPLICIT record span start
281
+ // (sequence-relative) so the bridge reproduces the editor's actual
282
+ // alignment instead of assuming centered.
283
+ if (incoming) incoming.transition = { type: String(effectId), duration: e2 - s, recStart: s };
276
284
  }
277
285
  });
278
286
  const atracks = media.audio && media.audio.track ? (Array.isArray(media.audio.track) ? media.audio.track : [media.audio.track]) : [];
@@ -233,6 +233,7 @@ async function assembleTimeline(spec = {}) {
233
233
  track: tr.track, atFrame: tr.atFrame, durationFrames: tr.durationFrames,
234
234
  trackType: tr.trackType || 'video',
235
235
  type: tr.type || 'dissolve',
236
+ spanStart: tr.startFrame,
236
237
  }));
237
238
  }
238
239
 
@@ -85,7 +85,7 @@ const STYLE_PRETTY_TYPES = {
85
85
  const TRANSITION_TYPES = ['dissolve', 'wipe', ...Object.keys(STYLE_PRETTY_TYPES)];
86
86
 
87
87
  async function placeTransition(drpInput, opts = {}) {
88
- const { track, atFrame, durationFrames = 24, trackType = 'video', type = 'dissolve', timelineUuid } = opts;
88
+ const { track, atFrame, durationFrames = 24, trackType = 'video', type = 'dissolve', spanStart, timelineUuid } = opts;
89
89
  if (!TRANSITION_TYPES.includes(type)) throw new Error(`placeTransition: type must be one of ${TRANSITION_TYPES.join(', ')}`);
90
90
  if (type !== 'dissolve' && trackType === 'audio') throw new Error('placeTransition: styled transitions are video-only (audio junctions cross-fade)');
91
91
  if (!Number.isInteger(track) || track < 1) throw new TypeError('placeTransition: track must be a positive integer');
@@ -114,7 +114,19 @@ async function placeTransition(drpInput, opts = {}) {
114
114
  } else if (STYLE_PRETTY_TYPES[type]) {
115
115
  trans = trans.replace(/<PrettyType>Cross Dissolve<\/PrettyType>/, `<PrettyType>${STYLE_PRETTY_TYPES[type]}</PrettyType>`);
116
116
  }
117
- const start = atFrame - Math.floor(durationFrames / 2); // centered (AlignmentType 2)
117
+ // Span geometry follows <Start>/<Duration>, NOT AlignmentType (the E61
118
+ // harvest of Resolve's own EDL import carries AlignmentType 2 with the
119
+ // span STARTING at the cut — the CMX convention — while our default
120
+ // centers it). spanStart (absolute) overrides for format parity: EDL
121
+ // dissolves start at the cut, OTIO splits by in/out offsets, XMEML
122
+ // transitionitems carry their explicit record span.
123
+ if (spanStart !== undefined) {
124
+ if (!Number.isInteger(spanStart)) throw new TypeError('placeTransition: spanStart must be an integer frame');
125
+ if (spanStart > atFrame || spanStart + durationFrames < atFrame) {
126
+ throw new RangeError(`placeTransition: span [${spanStart}, ${spanStart + durationFrames}) does not cover the cut at ${atFrame}`);
127
+ }
128
+ }
129
+ const start = spanStart !== undefined ? spanStart : atFrame - Math.floor(durationFrames / 2);
118
130
  trans = trans.replace(/<Start>\d+<\/Start>/, `<Start>${start}</Start>`);
119
131
  trans = trans.replace(/<Duration>\d+<\/Duration>/, `<Duration>${durationFrames}</Duration>`);
120
132
  const transitionDbId = (trans.match(/<Sm2TiTransition DbId="([^"]+)"/) || [])[1] || null;
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.141.0"
90
+ VERSION = "2.142.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.141.0"
14
+ VERSION = "2.142.1"
15
15
 
16
16
  import base64
17
17
  import os