davinci-resolve-mcp 2.74.0 → 2.75.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
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.75.0
6
+
7
+ The offline AAF reader now recovers retime ratios instead of only flagging them.
8
+
9
+ ### Added
10
+
11
+ - **Motion Control speed recovery.** OperationGroup parameters are read: a
12
+ constant `SpeedRatio` emits `speedRatio` (play rate) and corrects `speed`, so
13
+ consumers reading only `speed` are no longer told 100 for a 175% clip. Variable
14
+ timewarps (multi-point speed maps) report `speedVarying: true` rather than a
15
+ fabricated number — the reader's honest-refuse contract extends to speeds.
16
+ Note the stored AAF rational is RECORD/SOURCE (Edit Protocol output-over-input),
17
+ the inverse of play rate; the reader emits play rate, verified against the
18
+ length identity (sourceLen = recordLen / |ratio|) and Avid's own speed maps.
19
+
20
+ ### Fixed
21
+
22
+ - **Motion Control events inflated `recOut`.** Record advancement used the inner
23
+ source clip's length instead of the OperationGroup's declared record length, so
24
+ fast-motion clips claimed more record time than they occupy (and slow motion
25
+ claimed less). On a real 83-minute turnover this produced 40 spurious record
26
+ overlaps; with the declared length driving advancement, one remains — a
27
+ two-input blend genuinely sharing its record span.
28
+
5
29
  ## What's New in v2.74.0
6
30
 
7
31
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.74.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.75.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-blue.svg)](#server-modes)
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.74.0"
39
+ VERSION = "2.75.0"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (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.74.0",
3
+ "version": "2.75.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -19,6 +19,16 @@ trusting, so the Node server shells out to this helper, which uses the pure-Pyth
19
19
  Normalized event shape mirrors resolve-advanced/server/editorial.mjs `evt()`:
20
20
  { index, track, source, srcIn, srcOut, recIn, recOut, speed, reverse, transition, fps }
21
21
 
22
+ Retime (motion-effect) events additionally carry `"effect"` and, when the ratio is
23
+ recoverable from the OperationGroup's parameters (see _retime_fields):
24
+ * constant ratio → `"speedRatio"`: play-rate float (1.75 = 175%), `"speed"`:
25
+ round(playRate*100), `"reverse"`: true for backwards play.
26
+ * variable speed → `"speedVarying": true` and speed stays 100 — a timewarp has
27
+ no single honest number, so none is fabricated.
28
+ * unrecoverable → the flag alone, speed stays 100 (unchanged old contract).
29
+ For retimes, srcIn/srcOut are the SOURCE-side range while recIn/recOut span the
30
+ OperationGroup's DECLARED (record) length — they differ by the ratio.
31
+
22
32
  Honest-refuse discipline (no fake parses):
23
33
  * exit 3 → pyaaf2 not installed (stderr: AAF_PROBE_NO_PYAAF2)
24
34
  * exit 4 → file unreadable / not an AAF (stderr: AAF_PROBE_UNREADABLE: <detail>)
@@ -40,6 +50,8 @@ Segment model (Avid Media Composer picture turnovers):
40
50
  * Sequence — ordered `.components`, laid end to end.
41
51
  * OperationGroup — effect wrapper. Its `.segments` are the effect INPUTS, and the
42
52
  primary input is usually a nested Sequence (not a bare SourceClip).
53
+ Its `.parameters` carry the retime ratio for motion effects, and
54
+ its own declared length is the RECORD duration of the effect.
43
55
  * Selector — an enabled/disabled layer variant; the live one is `Selected`.
44
56
  * ScopeReference — "show the NestedScope layer beneath me": real record time, no
45
57
  clip of this layer's own. Treated as a gap, like Filler.
@@ -224,6 +236,123 @@ def _operation_name(comp):
224
236
  return ""
225
237
 
226
238
 
239
+ # ── Retime (Motion Control) parameter recovery ─────────────────────────────────
240
+ # Avid stores a retime's ratio on the OperationGroup's PARAMETERS. Verified against
241
+ # real Media Composer turnovers by cross-checking the inner SourceClip length vs the
242
+ # group's declared record length AND the PARAM_SPEED_MAP_U control-point values:
243
+ # * "SpeedRatio" (AAF Edit Protocol ParameterDef, a ConstantValue rational) is the
244
+ # RECORD/SOURCE length ratio — i.e. the INVERSE of the play rate. A 175% fast
245
+ # motion is stored as 4/7 (100 record frames consume 175 source frames); reverse
246
+ # play is a NEGATIVE rational (-1/1 = 100% backwards). Mixed-rate pulldown
247
+ # wrappers appear as 1000/1001.
248
+ # * "PARAM_SPEED_MAP_U" (Avid, a VaryingValue) has control points whose VALUES are
249
+ # play-rate scalars directly (1.75 = 175%, negative = reverse). More than one
250
+ # distinct value means the speed VARIES across the clip.
251
+ # * "PARAM_SPEED_RATIO_U" (Avid, a ConstantValue) belongs to the same *_U family
252
+ # as the speed map, so its value is a play-rate scalar, not a SpeedRatio.
253
+
254
+ # The AAF Edit Protocol parameter-definition id for SpeedRatio, so a file whose
255
+ # dictionary lost the human name still resolves.
256
+ _SPEED_RATIO_AUID = "72559a80-24d7-11d3-8a50-0050040ef7d2"
257
+
258
+
259
+ def _op_parameters(op_group):
260
+ """An OperationGroup's Parameter objects; [] when absent or unreadable."""
261
+ try:
262
+ prop = getattr(op_group, "parameters", None)
263
+ if prop is None:
264
+ return []
265
+ value = getattr(prop, "value", None)
266
+ return list(value if value is not None else prop)
267
+ except Exception:
268
+ return []
269
+
270
+
271
+ def _param_name(param):
272
+ try:
273
+ return str(param.name or "")
274
+ except Exception:
275
+ return ""
276
+
277
+
278
+ def _param_is(param, name, auid=None):
279
+ if _param_name(param) == name:
280
+ return True
281
+ if auid:
282
+ try:
283
+ return str(param.auid).lower() == auid
284
+ except Exception:
285
+ return False
286
+ return False
287
+
288
+
289
+ def _pointlist_values(varying):
290
+ """Control-point VALUES of a VaryingValue's point list, or None if unreadable."""
291
+ points = getattr(varying, "pointlist", None)
292
+ if points is None:
293
+ return None
294
+ inner = getattr(points, "value", None)
295
+ if inner is not None:
296
+ points = inner
297
+ try:
298
+ return [float(p.value) for p in points]
299
+ except Exception:
300
+ return None
301
+
302
+
303
+ def _retime_fields(op_group):
304
+ """Extra event fields recovered from a retime OperationGroup's parameters.
305
+
306
+ Returns one of:
307
+ {"speedRatio": <play-rate float>, "speed": <int %>, "reverse": <bool>}
308
+ {"speedVarying": True} — a variable-speed timewarp; no single honest number
309
+ {} — nothing recoverable (flag-only, speed stays 100)
310
+ """
311
+ play = None
312
+ speed_map = None
313
+ for param in _op_parameters(op_group):
314
+ cls = type(param).__name__
315
+ if cls == "VaryingValue":
316
+ if _param_name(param) == "PARAM_SPEED_MAP_U":
317
+ speed_map = param
318
+ continue
319
+ if cls != "ConstantValue":
320
+ continue
321
+ if _param_is(param, "SpeedRatio", _SPEED_RATIO_AUID):
322
+ try:
323
+ value = param.value
324
+ num = int(value.numerator)
325
+ den = int(value.denominator)
326
+ except Exception:
327
+ continue
328
+ if num:
329
+ play = den / num # stored record/source → play rate is the inverse
330
+ elif _param_name(param) == "PARAM_SPEED_RATIO_U" and play is None:
331
+ try:
332
+ value = float(param.value)
333
+ except Exception:
334
+ continue
335
+ if value:
336
+ play = value # *_U family stores the play rate directly
337
+ if speed_map is not None:
338
+ values = _pointlist_values(speed_map)
339
+ if values is None:
340
+ # A speed map we cannot read: we can neither call the speed constant
341
+ # nor prove it varies — recover nothing rather than guess.
342
+ return {}
343
+ if len(set(values)) > 1:
344
+ return {"speedVarying": True}
345
+ if play is None and values and values[0]:
346
+ play = values[0] # a flat map's single value IS the constant play rate
347
+ if not play:
348
+ return {}
349
+ return {
350
+ "speedRatio": round(abs(play), 6),
351
+ "speed": int(round(abs(play) * 100)),
352
+ "reverse": play < 0,
353
+ }
354
+
355
+
227
356
  def _walk_segment(segment, *, track, fps, rec, state, depth=0, transition=None):
228
357
  """
229
358
  Emit normalized events for ONE segment placed at record position `rec`.
@@ -289,10 +418,24 @@ def _walk_segment(segment, *, track, fps, rec, state, depth=0, transition=None):
289
418
  _walk_segment(inp, track=track, fps=fps, rec=rec, state=state, depth=depth + 1, transition=transition)
290
419
  op_name = _operation_name(segment)
291
420
  if op_name and ("speed" in op_name.lower() or "motion" in op_name.lower()):
292
- # We can detect that a retime is present but not reliably its ratio
293
- # offline; flag it honestly rather than fake a speed number.
421
+ # A retime. Its ratio is recoverable from the group's PARAMETERS (see
422
+ # _retime_fields): a constant ratio updates speed/speedRatio so
423
+ # consumers reading only `speed` are no longer told 100; a variable
424
+ # timewarp is reported as speedVarying: true; an unreadable one keeps
425
+ # the old flag-only contract. A number is never fabricated.
426
+ extra = _retime_fields(segment)
294
427
  for ev in state["events"][before:]:
295
428
  ev["effect"] = op_name
429
+ ev.update(extra)
430
+ # The group's DECLARED length is the RECORD duration; the inner
431
+ # SourceClip's length is the SOURCE-side range — under a retime
432
+ # they differ by the ratio, so an event whose recOut was advanced
433
+ # by the source length inflates (fast motion) or undershoots
434
+ # (slow motion) its real record span. The container declared-
435
+ # length rule (this file's convention for rec advancement)
436
+ # applies to the events too.
437
+ if declared > 0:
438
+ ev["recOut"] = max(ev["recIn"], rec + declared)
296
439
  return declared
297
440
 
298
441
  # Unknown component — advance by its declared length, don't fake an event, and
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.74.0"
88
+ VERSION = "2.75.0"
89
89
  logger = logging.getLogger("davinci-resolve-mcp")
90
90
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
91
  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 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.74.0"
14
+ VERSION = "2.75.0"
15
15
 
16
16
  import base64
17
17
  import os