davinci-resolve-mcp 2.78.1 → 2.79.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/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.78.1"
14
+ VERSION = "2.79.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -5999,7 +5999,10 @@ _PRPROJ_REFUSAL = (
5999
5999
  "advanced MCP — editorial.list_sequences / editorial.parse_interchange (format 'prproj'); "
6000
6000
  "(2) convert it to an importable interchange — editorial.convert_to_interchange "
6001
6001
  "(target 'otio'|'edl'|'drt') — then import that here with import_timeline_checked. "
6002
- "Editorial timing/cuts/transitions/speed carry over; per-clip effects/Lumetri color do not. "
6002
+ "Editorial timing/cuts/transitions carry over; per-clip effects/Lumetri color do not. "
6003
+ "Speed/reverse carry on 'otio' and 'edl' ONLY — the DRT clip schema has no per-clip "
6004
+ "speed field, so 'drt' flattens every retime to 100% forward and reports them in "
6005
+ "`flattened`. Prefer 'otio' or 'edl' for a cut that carries retimes. "
6003
6006
  "Alternatively export FCP7 XML / AAF / FCPXML from Premiere and conform that."
6004
6007
  )
6005
6008
 
@@ -6008,6 +6011,12 @@ _PRPROJ_REFUSAL = (
6008
6011
  # pool after import, not by rewriting the file.
6009
6012
  _BINARY_INTERCHANGE_EXTS = {".aaf"}
6010
6013
 
6014
+ # Interchange that is not XML at all. `.otio` is JSON, so the sanitize/relink pass —
6015
+ # which parses the file as XML and rewrites <pathurl> elements — cannot run on it, and
6016
+ # the missing-media / generator-clip advice it exists to give is not the diagnosis for
6017
+ # an .otio that fails to import.
6018
+ _JSON_INTERCHANGE_EXTS = {".otio"}
6019
+
6011
6020
 
6012
6021
  def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
6013
6022
  path = p.get("path")
@@ -6018,12 +6027,50 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
6018
6027
  ext = os.path.splitext(path)[1].lower()
6019
6028
  if ext == ".prproj":
6020
6029
  return _err(_PRPROJ_REFUSAL, category="invalid_input")
6030
+ # ImportTimelineFromFile silently no-ops on the never-saved default project: it returns
6031
+ # nothing, creates no timeline, and reports no cause. The generic "Resolve created no
6032
+ # timeline" error that came back instead sent people to source-clip resolution and
6033
+ # sanitize_media — the wrong road entirely, because the file is fine.
6034
+ #
6035
+ # This is the last unrecorded member of a family the repo already documents: SaveProject
6036
+ # returns False here (and blocks forever headless), CreateProject fails against a dirty
6037
+ # untitled project, and DeleteProject's workaround warns against leaving the session on
6038
+ # this fallback. Refusing early is what makes the cause visible.
6039
+ #
6040
+ # The refusal is HARD — no override flag. An override would reintroduce exactly the
6041
+ # silent no-op this exists to kill, since the call cannot succeed either way.
6042
+ #
6043
+ # Resolve exposes no "is this project unsaved" predicate (no IsModified / IsSaved), so
6044
+ # the test is the literal fallback name. Known hole, stated rather than hidden: a
6045
+ # localized Resolve may not name it "Untitled Project", and this guard will miss it —
6046
+ # such a session gets the old generic error, not a wrong answer.
6047
+ from src.utils.project_cleanup import UNSAVED_DEFAULT_PROJECT
6048
+ try:
6049
+ _current_name = proj.GetName()
6050
+ except Exception:
6051
+ _current_name = None
6052
+ if _current_name == UNSAVED_DEFAULT_PROJECT:
6053
+ return _err(
6054
+ f"Cannot import a timeline into the never-saved default project "
6055
+ f"('{UNSAVED_DEFAULT_PROJECT}') — Resolve accepts the call and creates no "
6056
+ f"timeline, with no error naming the cause.",
6057
+ category="invalid_input",
6058
+ remediation=(
6059
+ "The project state is the problem, not the file. Save this project under a "
6060
+ "name, or load an existing named project (project_manager.load), then retry "
6061
+ "the import unchanged."
6062
+ ),
6063
+ )
6021
6064
  # AAF (and any binary interchange) is read natively by Resolve. The XML
6022
6065
  # sanitize/relink path parses the file as text, so it must be SKIPPED for AAF
6023
6066
  # even when sanitize_media / relink_search_roots is passed; fuzzy XML relink is
6024
6067
  # N/A (media links through the media pool). We still detect the created
6025
6068
  # (possibly offline) timeline via the before/after id diff.
6026
6069
  is_binary = ext in _BINARY_INTERCHANGE_EXTS
6070
+ # .otio is JSON. The sanitize/relink pass parses the file as XML, so it cannot run here
6071
+ # any more than it can on an AAF — and its advice (missing media, generator clips) is
6072
+ # not the diagnosis for an .otio that fails to import.
6073
+ is_json = ext in _JSON_INTERCHANGE_EXTS
6027
6074
  # sanitize_media (alias: relink_media) rewrites the XML to drop clipitems that
6028
6075
  # reference missing media or are generators (slug/solid w/ no pathurl) — both
6029
6076
  # abort Resolve's scripting-API import and leave the timeline fully offline.
@@ -6043,7 +6090,13 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
6043
6090
  "relinks via the media pool. Imported the file as-is; pass relink_search_roots to "
6044
6091
  "auto-relink after import."
6045
6092
  )
6046
- sanitize = sanitize_requested and not is_binary
6093
+ elif is_json and (sanitize_requested or _binary_has_roots):
6094
+ binary_relink_note = (
6095
+ f"sanitize_media / XML path-rewrite relink are N/A for {ext} (JSON, not XML) — "
6096
+ "imported as-is. Media links by the target_url recorded in the file; relink via "
6097
+ "the media pool afterward (see `relink`)."
6098
+ )
6099
+ sanitize = sanitize_requested and not is_binary and not is_json
6047
6100
  if not sanitize and p.get("require_temp_path", True) and not _render_temp_path_ok(path):
6048
6101
  return _err(
6049
6102
  "path must be under the system temp directory unless require_temp_path=False",
@@ -6140,6 +6193,26 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
6140
6193
  "Otherwise verify it exports/opens in Resolve directly, or convert to "
6141
6194
  "FCP7 XML / FCPXML upstream and import that."
6142
6195
  )
6196
+ elif is_json:
6197
+ # Measured on 19.1.3: Resolve DOES import OTIO through the scripting API —
6198
+ # its own EXPORT_OTIO output re-imports cleanly. What it refuses is a
6199
+ # document that is valid OTIO but not Resolve-shaped, and the shape it cares
6200
+ # about most is the source frame ORIGIN: a clip's source_range must sit
6201
+ # inside the media's real timecode range, so 0-based source offsets against
6202
+ # media that starts at 01:00:00:00 produce no timeline at all. Missing media
6203
+ # and sanitize_media are not the diagnosis here and pointing at them wastes
6204
+ # the caller's time on a file whose media is online.
6205
+ remediation = (
6206
+ f"Resolve created no timeline from this {ext}. This is usually the "
6207
+ "document's shape, not its media: Resolve expects Clip.2 with a "
6208
+ "media_references map, an available_range on each reference, and "
6209
+ "source_range frames expressed against the media's own timecode origin "
6210
+ "(0-based source offsets fail when the media does not start at "
6211
+ "00:00:00:00). Author it with editorial.convert_to_interchange "
6212
+ "(target 'otio'), supplying each event's media start timecode, and check "
6213
+ "the returned mediaOriginAssumed list. To compare against a known-good "
6214
+ "file, export any timeline with timeline.export EXPORT_OTIO."
6215
+ )
6143
6216
  elif sanitize:
6144
6217
  remediation = None
6145
6218
  else:
@@ -6180,6 +6253,9 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
6180
6253
  if is_binary:
6181
6254
  msg += (" Relink via the media pool (right-click → Relink Clips) or point Resolve "
6182
6255
  "at the media roots — AAF media links there, not by rewriting the file.")
6256
+ elif is_json:
6257
+ msg += (" Relink via the media pool — .otio is JSON, so sanitize_media cannot "
6258
+ "rewrite its paths; check each clip's target_url instead.")
6183
6259
  elif not sanitize:
6184
6260
  msg += (" Retry with sanitize_media=True to drop missing-media/generator "
6185
6261
  "clips so the remaining media links automatically.")
@@ -181,7 +181,14 @@ API_TRUTH: List[Dict[str, Any]] = [
181
181
  "five consecutive imports with no media duplicated. Use DRT "
182
182
  "for one-shot hand-offs only. For OTIO, pass "
183
183
  "importSourceClips=True with a sourceClipsPath. See "
184
- "docs/guides/headless-edit-loop.md.",
184
+ "docs/guides/headless-edit-loop.md. For a consolidated Avid "
185
+ "AAF specifically, do NOT expect this call (or Reconform "
186
+ "from Bins, or the UI's 'Link to source camera files') to "
187
+ "conform it to camera originals — all three were measured "
188
+ "against a real turnover and all three fail, the UI option "
189
+ "linking 878 of 882 items with only 144 correct while "
190
+ "rendering as a fully conformed timeline. See "
191
+ "docs/guides/conforming-an-avid-aaf.md.",
185
192
  "tags": ["timeline", "import", "interchange", "silent-failure", "conform"],
186
193
  "submit": "bug",
187
194
  },
@@ -274,11 +281,119 @@ API_TRUTH: List[Dict[str, Any]] = [
274
281
  "— naming nothing actionable — when the Media Pool's CURRENT "
275
282
  "folder is not the folder holding the clips, even though every "
276
283
  "clip_id passed is valid and resolvable. Reported against "
277
- "Resolve Studio in PR #99.",
284
+ "Resolve Studio in PR #99. "
285
+ "SEPARATELY, its clipInfo HAS NO TRACK FIELD: there is no way "
286
+ "to say which video or audio track a clip should land on, so "
287
+ "this call cannot build a multi-track timeline. The asymmetry "
288
+ "is the surprise — MediaPool.AppendToTimeline's clipInfo DOES "
289
+ "take `trackIndex`, so the two clipInfo shapes are not the same "
290
+ "shape, and code that works against one silently loses track "
291
+ "assignment against the other.",
278
292
  "recommended": "Call MediaPool.SetCurrentFolder() to the clips' bin "
279
293
  "before creating the timeline (media_pool "
280
- "set_current_folder). Valid ids are not sufficient.",
281
- "tags": ["media-pool", "timeline", "unhelpful-error"],
294
+ "set_current_folder). Valid ids are not sufficient. "
295
+ "For multi-track placement do not use this call at all: "
296
+ "create an EMPTY timeline (media_pool create_timeline), add "
297
+ "the tracks you need (timeline add_track), then place each "
298
+ "clip with MediaPool.AppendToTimeline passing clipInfo "
299
+ "`trackIndex` (media_pool append_to_timeline with "
300
+ "track_index).",
301
+ "tags": ["media-pool", "timeline", "unhelpful-error", "missing-method", "conform"],
302
+ "submit": "bug",
303
+ },
304
+ {
305
+ "symbol": "MediaPool.AppendToTimeline (overlapping records — earlier item wins)",
306
+ "object": "MediaPool",
307
+ "signature": "([{mediaPoolItem, startFrame, endFrame, recordFrame, "
308
+ "trackIndex, mediaType}]) -> [TimelineItem]",
309
+ "reality": "AppendToTimeline does NOT overwrite an overlapping record. "
310
+ "When two clipInfos resolve to record ranges that overlap on "
311
+ "the same track, the EARLIER item wins and the later append is "
312
+ "dropped — measured while placing a real turnover. There is no "
313
+ "overwrite edit mode to reach for either: AppendToTimeline is "
314
+ "the only programmatic placement the API offers, so a consumer "
315
+ "that assumes overwrite semantics gets a timeline that is "
316
+ "silently SHORT by the number of colliding events, with no "
317
+ "error to say which ones lost.",
318
+ "recommended": "Resolve collisions BEFORE appending — the API will not do "
319
+ "it for you. Cap each event's placed duration at the next "
320
+ "event's recordFrame on the same track, or place the "
321
+ "colliding events on separate tracks. Verify by comparing "
322
+ "the timeline's item count against the number of clipInfos "
323
+ "you sent, per track, and finish with detect_gaps_overlaps; "
324
+ "a count that matches is the only evidence nothing was "
325
+ "dropped.",
326
+ "tags": ["timeline", "edit", "silent-failure", "conform", "media-pool"],
327
+ "submit": "bug",
328
+ },
329
+ {
330
+ "symbol": "MediaPool.AppendToTimeline (errored-chunk placements are not durable across a save)",
331
+ "object": "MediaPool",
332
+ "signature": "([clipInfos]) -> [TimelineItem]",
333
+ "reality": "Placements made by an append call whose response ERRORED are "
334
+ "not durable: they appear in the timeline, every in-session "
335
+ "read agrees they are there, and THE SAVE DISCARDS THEM. "
336
+ "Measured on a real turnover — a timeline verified at 573 items "
337
+ "immediately after construction held 500 after the save, a loss "
338
+ "of exactly one errored append chunk's worth. The API reported "
339
+ "the failure, the items appeared anyway, and nothing between "
340
+ "construction and the save disagreed with the wrong number. "
341
+ "This is the dangerous shape: the witness is derived from the "
342
+ "same unsaved state as the thing it is checking, so it cannot "
343
+ "contradict it. Only a POST-SAVE read can.",
344
+ "recommended": "Treat the save as the verification boundary, not the end "
345
+ "of the job. Save, RE-READ the timeline's item count from "
346
+ "the saved project, and re-append whatever is missing "
347
+ "(bounded — two rounds is enough in practice); only then "
348
+ "report success. A timeline that is still short after that, "
349
+ "or that will not save, is a FAILURE — report it as one "
350
+ "rather than returning the in-session count. Never claim a "
351
+ "construction succeeded on a pre-save read alone, and treat "
352
+ "any errored append chunk as suspect even when its items "
353
+ "are visibly present.",
354
+ "tags": ["timeline", "edit", "silent-failure", "data-loss", "conform",
355
+ "unreliable-return", "media-pool"],
356
+ "submit": "bug",
357
+ },
358
+ {
359
+ "symbol": "MediaPool.ImportTimelineFromFile (.otio document shape)",
360
+ "object": "MediaPool",
361
+ "signature": "(filePath, {importOptions}) -> Timeline",
362
+ "reality": "Resolve DOES import OTIO through the scripting API — but only "
363
+ "a Resolve-shaped document, and it rejects anything else by "
364
+ "creating NO timeline and returning None, with no error naming "
365
+ "a cause. Established on 19.1.3.7 by exporting a timeline with "
366
+ "Timeline.Export(..., EXPORT_OTIO) and feeding Resolve's own "
367
+ "file straight back: it re-imports cleanly (3 items, 3 linked), "
368
+ "while a valid hand-authored OTIO of the same cut, same project, "
369
+ "same session, same three online media files, produced nothing. "
370
+ "So a refusal is a SHAPE problem, not a media problem. "
371
+ "The requirement that actually decides it is the SOURCE FRAME "
372
+ "ORIGIN: a clip's source_range must be expressed against the "
373
+ "media's own timecode range. Media carrying an embedded start "
374
+ "TC of 01:00:00:00 gets an available_range starting at frame "
375
+ "86400, and the clip's source_range must start there too — "
376
+ "0-based source offsets, the natural reading of 'source in "
377
+ "point', put the clip outside the media's real range and the "
378
+ "import dies. Isolated by bisection: 0-based fails and absolute "
379
+ "succeeds whether or not the stack/track source_range is null. "
380
+ "Resolve also writes (and expects) Clip.2 with a "
381
+ "`media_references` MAP plus `active_media_reference_key`, not "
382
+ "Clip.1 with a singular `media_reference`; an `available_range` "
383
+ "on each reference; a BARE path in `target_url`, not a file:// "
384
+ "URL; `enabled` and `metadata` throughout; `global_start_time`; "
385
+ "and track names in its own form ('Video 1').",
386
+ "recommended": "Author OTIO for Resolve by mirroring what Resolve itself "
387
+ "exports, and give every event its media timecode origin. "
388
+ "editorial.convert_to_interchange (target 'otio') does this "
389
+ "and reports any event whose origin had to be assumed in "
390
+ "`mediaOriginAssumed` — a non-empty list means the file will "
391
+ "only import if that media really starts at 00:00:00:00. To "
392
+ "debug a refusal, export any timeline with EXPORT_OTIO and "
393
+ "diff your document against it; do NOT chase missing media "
394
+ "or reach for sanitize_media, which cannot even parse a "
395
+ ".otio (it is JSON, not XML).",
396
+ "tags": ["timeline", "import", "interchange", "otio", "silent-failure", "conform"],
282
397
  "submit": "bug",
283
398
  },
284
399
  {
@@ -554,10 +669,62 @@ API_TRUTH: List[Dict[str, Any]] = [
554
669
  "against the documented SetProperty key list AND by live "
555
670
  "mutating attempt on 21.0.0: SetProperty('Speed'|'PlaybackSpeed'"
556
671
  "|'RetimeSpeed'|'ClipSpeed', 50) all return False, while "
557
- "SetProperty('RetimeProcess', 1) returns True.",
672
+ "SetProperty('RetimeProcess', 1) returns True. "
673
+ "THE READ SIDE IS AS DEAD AS THE WRITE SIDE, which is easy to "
674
+ "miss: re-measured on Studio 19.1.3.7 against a placed item, "
675
+ "GetProperty('Speed'), GetProperty('PlaybackSpeed'), "
676
+ "GetProperty('RetimeSpeed') and GetProperty('ClipSpeed') ALL "
677
+ "return None, and the keyless GetProperty() dict (26 keys on "
678
+ "that item) carries no speed value at all — its only retime key "
679
+ "is RetimeProcess, which is quality, not ratio. SetProperty("
680
+ "'Speed', 1.75) returned False on 19.1.3.7 too, so the write "
681
+ "refusal is not specific to 21.0.0. Note the 21.0.0 stamp above "
682
+ "covers the SetProperty measurements only. "
683
+ "THE INTERCHANGE ROUTE IS ALSO CLOSED — carrying a retime in "
684
+ "through FCP7 XML does not work either, measured on 19.1.3 with "
685
+ "three real files, clips at 100/200/50% on V1, each variant "
686
+ "imported as its own timeline: (a) the importer IGNORES the "
687
+ "scalar Time Remap speed filter and the clips arrive at 100%; "
688
+ "(b) `graphdict` is dead in FOUR separate shapes — the full "
689
+ "Premiere form (variablespeed=0 + speed + reverse=FALSE + "
690
+ "frameblending + 4-keyframe graphdict + FCPCurve), graphdict "
691
+ "alone with no <speed> param, a 100% REVERSE (reverse=TRUE plus "
692
+ "a descending graphdict, the exact shape a real Premiere export "
693
+ "carries), and variablespeed=1 + graphdict — 0 of 2 retimes "
694
+ "landed in every one; (c) `reverse` did not survive either; "
695
+ "(d) any <in>/<pproTicksIn> inconsistency is silently REJECTED, "
696
+ "measured in BOTH orientations. The emitted maps were verified "
697
+ "constant-slope against real Premiere exports before importing, "
698
+ "and every imported clip came back consuming exactly its record "
699
+ "span of source (a 200% clip emitted in 200 / out 296 was "
700
+ "clamped to out 248 — 48 source frames over a 48-frame record "
701
+ "span). Placement is NOT the problem: the same route imported "
702
+ "573 clips with 572 of 573 matching by track and record position "
703
+ "with source frames exact, and the importer BUILT a 59-frame "
704
+ "dissolve. The retime gap is specific, not general. "
705
+ "TRAP: Resolve's own FCP7 export cannot witness a speed. It "
706
+ "writes a DEGENERATE Time Remap on every clip — `speed` value 0 "
707
+ "(not 100) and a graphdict whose keyframe `value`s are all 0 "
708
+ "while its `when`s carry the clip's source in/out — so anyone "
709
+ "verifying a retime by round-tripping through EXPORT_FCP_7_XML "
710
+ "is reading furniture, and the identity Time Remap blocks "
711
+ "present on every clip are what make the route look like it "
712
+ "should work.",
558
713
  "recommended": "Set clip speed/retime in the Resolve UI; no scripted "
559
- "equivalent exists.",
560
- "tags": ["missing-method", "timeline", "retime", "speed"],
714
+ "equivalent exists, and no interchange route carries one "
715
+ "in. Do NOT read speed back with GetProperty (None) or "
716
+ "witness it via EXPORT_FCP_7_XML (degenerate). Read the "
717
+ "clip's GEOMETRY instead — GetLeftOffset / GetRightOffset, "
718
+ "which on the 200% clip read 200 / 248 and agreed with the "
719
+ "export's in/out. Caveat worth stating: no positive control "
720
+ "has been run — no clip KNOWN to be retimed has been read "
721
+ "back through those two witnesses, because there is no "
722
+ "scripting path to create one. So the geometry witness is "
723
+ "the best available, not a proven one. Either way the "
724
+ "interchange route is rejected: no retime was built, and "
725
+ "none could have been verified.",
726
+ "tags": ["missing-method", "timeline", "retime", "speed", "interchange",
727
+ "silent-failure", "unreliable-return"],
561
728
  "submit": "missing",
562
729
  },
563
730
  {