davinci-resolve-mcp 2.118.0 → 2.119.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.
@@ -49,6 +49,7 @@ window. `render.verify_output` covers the container-level checks.
49
49
  | Built-in generators | `elements: [{type:'generator', generatorName}]` | v2.110 |
50
50
  | Custom start timecode | `spec.startFrame` / `preserveStartTimecode` | v2.117 |
51
51
  | Timeline markers | `spec.markers` (16 colors, notes, durations, customData) | v2.118 |
52
+ | Turnover markers | EDL `* LOC:` locators + OTIO markers → authored | v2.119 |
52
53
  | Fusion titles | `elements: [{type:'title', text}]` — **21-gen hosts only** | v2.108 |
53
54
 
54
55
  `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.118.0"
40
+ VERSION = "2.119.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.118.0",
3
+ "version": "2.119.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -229,9 +229,11 @@ export function eventsToAssembleSpec(events, opts = {}) {
229
229
  }
230
230
  const DEFAULT_ORIGIN = 86400;
231
231
  const isAudio = (t) => /^A\d*$/.test(String(t || ''));
232
- const vids = events.filter((e) => !isAudio(e.track) && e.recIn != null && e.recOut != null);
232
+ const isMarker = (t) => t === 'MARKER';
233
+ const vids = events.filter((e) => !isAudio(e.track) && !isMarker(e.track) && e.recIn != null && e.recOut != null);
233
234
  const auds = events.filter((e) => isAudio(e.track) && e.recIn != null && e.recOut != null);
234
- const audioSkipped = events.length - vids.length - auds.length;
235
+ const markerEvents = events.filter((e) => isMarker(e.track) && e.recIn != null);
236
+ const audioSkipped = events.length - vids.length - auds.length - markerEvents.length;
235
237
  if (!vids.length) throw new Error('eventsToAssembleSpec: no video events with record ranges');
236
238
 
237
239
  const unmapped = [...new Set([...vids, ...auds].map((e) => e.source).filter((srcName) => !sourceMap[srcName]))];
@@ -426,12 +428,31 @@ export function eventsToAssembleSpec(events, opts = {}) {
426
428
  transitions.push({ track: c.track, atFrame: c.atFrame, durationFrames: c.durationFrames, trackType: 'audio' });
427
429
  }
428
430
 
431
+ // Turnover markers (EDL * LOC: locators, OTIO Marker objects) → authored
432
+ // timeline markers. Interchange colors map to the measured Resolve names;
433
+ // unknown colors fall back to Blue. Frames are timeline-absolute like cuts.
434
+ const COLOR_MAP = {
435
+ blue: 'Blue', cyan: 'Cyan', green: 'Green', yellow: 'Yellow', red: 'Red',
436
+ pink: 'Pink', purple: 'Purple', magenta: 'Fuchsia', fuchsia: 'Fuchsia',
437
+ rose: 'Rose', lavender: 'Lavender', sky: 'Sky', mint: 'Mint',
438
+ lemon: 'Lemon', sand: 'Sand', cocoa: 'Cocoa', cream: 'Cream',
439
+ orange: 'Sand', white: 'Cream', black: 'Cocoa',
440
+ };
441
+ const markers = markerEvents
442
+ .map((e) => ({
443
+ frame: ORIGIN + (toTl(e.recIn, e.fps) - minRec),
444
+ color: COLOR_MAP[String(e.color || '').toLowerCase()] || 'Blue',
445
+ ...(e.name ? { name: e.name } : {}),
446
+ }))
447
+ .filter((m) => m.frame >= (preserveStartTimecode ? startFrame : DEFAULT_ORIGIN));
448
+
429
449
  return {
430
- spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(transitions.length ? { transitions } : {}) },
450
+ spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(markers.length ? { markers } : {}), ...(transitions.length ? { transitions } : {}) },
431
451
  report: {
432
452
  videoEvents: vids.length,
433
453
  sources: media.length,
434
454
  audioEventsSkipped: audioSkipped,
455
+ authoredMarkers: markers.length,
435
456
  authoredAudioEvents: audioPlacements.length,
436
457
  audioRetimesSkipped,
437
458
  upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1).length,
@@ -49,6 +49,8 @@ function evt(o) {
49
49
  return {
50
50
  index: o.index ?? null,
51
51
  track: o.track || 'V',
52
+ ...(o.name !== undefined ? { name: o.name } : {}),
53
+ ...(o.color !== undefined ? { color: o.color } : {}),
52
54
  source: o.source || 'UNKNOWN',
53
55
  srcIn: o.srcIn ?? null,
54
56
  srcOut: o.srcOut ?? null,
@@ -83,6 +85,19 @@ export function parseEDL(text, opts = {}) {
83
85
  }
84
86
  continue;
85
87
  }
88
+ // Avid-style locators: `* LOC: 01:00:01:12 BLUE marker text` — a marker
89
+ // at an absolute record timecode. Emitted as track 'MARKER' pseudo-events
90
+ // (recIn only) so the assemble bridge can author them; consumers that
91
+ // filter video/audio by track shape ignore them.
92
+ const loc = /^\*\s*LOC:\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s+(\S+)\s*(.*)$/i.exec(line);
93
+ if (loc) {
94
+ events.push(evt({
95
+ index: events.length + 1, track: 'MARKER', source: '',
96
+ recIn: tcToFrames(loc[1], fps), recOut: null,
97
+ name: loc[3].trim() || undefined, color: loc[2], fps,
98
+ }));
99
+ continue;
100
+ }
86
101
  const tokens = line.split(/\s+/);
87
102
  if (!/^\d+$/.test(tokens[0])) continue; // not an event line
88
103
  const tcs = tokens.filter(isTc);
@@ -147,6 +162,14 @@ export function parseOTIO(otio, opts = {}) {
147
162
  }
148
163
  }
149
164
  const src = (child.media_reference && (child.media_reference.target_url || child.media_reference.name)) || child.name || 'UNKNOWN';
165
+ for (const mk of child.markers || []) {
166
+ const mrStart = (mk.marked_range && mk.marked_range.start_time && mk.marked_range.start_time.value) || 0;
167
+ events.push(evt({
168
+ index: idx++, track: 'MARKER', source: '',
169
+ recIn: rec + (mrStart - startVal), recOut: null,
170
+ name: mk.name || undefined, color: mk.color || undefined, fps: rate,
171
+ }));
172
+ }
150
173
  events.push(
151
174
  evt({
152
175
  index: idx++,
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.118.0"
90
+ VERSION = "2.119.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.118.0"
14
+ VERSION = "2.119.0"
15
15
 
16
16
  import base64
17
17
  import os