davinci-resolve-mcp 2.170.0 → 2.171.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 +28 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/editorial.mjs +18 -8
- package/src/granular/common.py +1 -1
- package/src/server.py +42 -24
- package/src/utils/api_truth.py +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
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.171.0 — E114: audio-lane `-1` edges take their own lane's junctions
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **XMEML audio cross-fades lost 12 source frames on the incoming clip.**
|
|
10
|
+
Resolve's FCP7 writer emits an audio cross-fade as a transitionitem on the
|
|
11
|
+
audio track with `-1` clip edges exactly like video (measured on 19.1.3.7
|
|
12
|
+
from the E109 AAF conform; its OTIO writer emits the same cross-fade as a
|
|
13
|
+
`Custom_Transition` with 12/12 offsets). The E108 audio walk attached the
|
|
14
|
+
transition but never computed that lane's junction list, so the incoming
|
|
15
|
+
clip's `<in>` (the source at the OVERLAP start) lost its junction offset and
|
|
16
|
+
`verify_roundtrip` failed the AAF → conform → import → XML loop with a
|
|
17
|
+
12-frame audio `source-frames` drift. Each lane now resolves against its
|
|
18
|
+
own transitionitems; the loop verifies `pass: true` through both writers.
|
|
19
|
+
- **v2.170.0's `kind` classifier was wrong for generators and subtitles.**
|
|
20
|
+
Measured on 19.1.3.7 (E115): a Solid Color generator and a subtitle item
|
|
21
|
+
return no MediaPoolItem and `None` from GetProperty() — exactly like a
|
|
22
|
+
transition — so the "no media, empty properties" rule labelled both
|
|
23
|
+
`transition`. The discriminator is now GEOMETRY: a transition straddles a
|
|
24
|
+
cut (one neighbour ends inside its span, another starts inside it), a
|
|
25
|
+
generator owns its span, subtitle tracks report `subtitle`, and known
|
|
26
|
+
transition names short-circuit. Verified against Resolve's own enumeration
|
|
27
|
+
of the E107 fades timeline (generator, dissolve, clip, dissolve, clip,
|
|
28
|
+
dissolve, generator).
|
|
29
|
+
- **v2.170.0 also shipped with two red Python tests** — an item-shape
|
|
30
|
+
assertion in the `get_items` selector test that did not expect the new
|
|
31
|
+
`kind` field. The expectation is updated.
|
|
32
|
+
|
|
5
33
|
## What's New in v2.170.0 — E113: `get_items` knows a transition from a clip
|
|
6
34
|
|
|
7
35
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.171.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -89,7 +89,7 @@ equivalent, blocking full automation.
|
|
|
89
89
|
### Transition create / copy / clone
|
|
90
90
|
|
|
91
91
|
- **Object:** `Timeline / TimelineItem`
|
|
92
|
-
- **Behavior:** There is no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING: creation, cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty
|
|
92
|
+
- **Behavior:** There is no method to ADD or CLONE an edit transition — no AddTransition/CreateTransition/AddVideoTransition on Timeline or TimelineItem (dir(), 21.0.4.5). CORRECTION, measured on Studio 21.0.4.5 (2026-08-12): this entry previously said transitions applied in the UI are 'invisible to and unmodifiable by scripts'. BOTH HALVES WERE WRONG and are withdrawn. A transition IS a first-class timeline item: a 12-frame Cross Dissolve applied through the Edit-page right-click menu enumerates in GetItemListInTrack('video', 1) as GetName()=='Cross Dissolve', GetStart()==86426, GetDuration()==12 — centered on a cut at 86432 — with a stable GetUniqueId() and a working GetTrackTypeAndIndex(). A transition authored offline into a .drp and imported reads IDENTICALLY, so the route that created it does not matter. It is also REMOVABLE: Timeline.DeleteClips([transition], False) returns True and deletes it, leaving both adjacent clips at their original starts and durations. THE DISCRIMINATOR between a transition item and a clip item is GetProperty(): a transition returns an EMPTY dict where a video clip returns 26 transform keys; it also has no MediaPoolItem and no Fusion comp. WHAT IS GENUINELY MISSING: creation, cloning, and any type/alignment/parameter detail — the transition's kind is knowable ONLY from its name string, and there is no way to read its alignment (centered/start/end) or edit its duration. AUDIO NUANCE (measured 2026-09-01 on 19.1.3.7, E113): an audio cross-fade enumerates in GetItemListInTrack('audio', n) with an EMPTY GetName() (24 frames, centered on the cut, between the two clips) — so on audio lanes even the kind is not readable from the name. The discriminator that holds for BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — BUT a Solid Color generator AND a subtitle item read the same way (GetProperty() None, no MediaPoolItem; measured E115), so that pair only separates clips from non-clips. What separates a transition from a generator is GEOMETRY: a transition straddles a cut (one neighbour ends inside its span, another starts inside it) while a generator owns its span. timeline.get_items reports `kind` on that basis.
|
|
93
93
|
- **Workaround / current handling:** Automated QC of existing transitions IS possible and is the main practical need — enumerate GetItemListInTrack, treat any item whose GetProperty() is empty and whose GetMediaPoolItem() is None as a transition, and read its name, start and duration. Removal is scriptable via Timeline.DeleteClips. To CREATE one, either apply it in the Resolve UI, or author it offline and import: the advanced server's drp place_transition writes a cross dissolve at an abutting cut ({track, atFrame, durationFrames}) and it round-trips into Resolve 21.0.4.5 reading back at the expected centered range.
|
|
94
94
|
- **Tags:** missing-method, timeline, transition
|
|
95
95
|
|
|
@@ -305,7 +305,7 @@ equivalent, blocking full automation.
|
|
|
305
305
|
|
|
306
306
|
- **Object:** `Timeline`
|
|
307
307
|
- **Signature:** `(filePath, EXPORT_EDL, EXPORT_NONE) -> bool`
|
|
308
|
-
- **Behavior:** Resolve's CMX EDL writer (measured on Studio 19.1.3.7, E105) emits VIDEO events only — audio legs never appear; names every file source by the generic reel AX and carries the real names in `* FROM CLIP NAME:` / `* TO CLIP NAME:` comments; writes black legs as reel BL; places dissolve junctions at the CMX start-at-cut position (the overlap start, not the centered junction the timeline holds); and WRITES BL fades that its own EDL importer then drops. The FCP7 XML writer, by contrast, carries audio, writes transition-adjacent clip edges as -1 (the junction), and emits `speed` followed by `variablespeed` 0 in the same timeremap effect.
|
|
308
|
+
- **Behavior:** Resolve's CMX EDL writer (measured on Studio 19.1.3.7, E105) emits VIDEO events only — audio legs never appear; names every file source by the generic reel AX and carries the real names in `* FROM CLIP NAME:` / `* TO CLIP NAME:` comments; writes black legs as reel BL; places dissolve junctions at the CMX start-at-cut position (the overlap start, not the centered junction the timeline holds); and WRITES BL fades that its own EDL importer then drops. The FCP7 XML writer, by contrast, carries audio, writes transition-adjacent clip edges as -1 (the junction), and emits `speed` followed by `variablespeed` 0 in the same timeremap effect. AUDIO cross-fades are written the same way — a transitionitem on the audio track between -1-edged clipitems (measured E114); the OTIO writer emits them as a `Custom_Transition` with symmetric offsets on the Audio track.
|
|
309
309
|
- **Workaround / current handling:** For round-trip QC prefer EXPORT_OTIO (carries audio, retimes as LinearTimeWarp/FreezeFrame, exact spans). When an EDL is the required deliverable, expect no audio (editorial.verify_roundtrip reports audioNotInExport) and resolve AX reels through the clip-name comments (parseEDL does). Use EXPORT_EDL — there is no EXPORT_CMX_3600 constant, and an unknown name reaches Export as a string that returns a bare False (timeline.export_timeline_checked now refuses it loudly).
|
|
310
310
|
- **Tags:** timeline, export, edl, audio, silent-failure
|
|
311
311
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.171.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
|
@@ -390,6 +390,18 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
};
|
|
393
|
+
// A track's transitionitems as -1-edge junctions (alignment-dependent frame
|
|
394
|
+
// plus the span), shared by video and audio lanes.
|
|
395
|
+
const trackJunctionsOf = (t) => {
|
|
396
|
+
const tlist = t.transitionitem ? (Array.isArray(t.transitionitem) ? t.transitionitem : [t.transitionitem]) : [];
|
|
397
|
+
return tlist.map((tr) => {
|
|
398
|
+
const s0 = Number(tr.start), e0 = Number(tr.end);
|
|
399
|
+
if (!Number.isFinite(s0) || !Number.isFinite(e0)) return null;
|
|
400
|
+
const al = String(tr.alignment || 'center').toLowerCase();
|
|
401
|
+
const frame = al === 'start-black' || al === 'start' ? s0 : al === 'end-black' || al === 'end' ? e0 : Math.round((s0 + e0) / 2);
|
|
402
|
+
return { frame, start: s0, end: e0 };
|
|
403
|
+
}).filter(Boolean);
|
|
404
|
+
};
|
|
393
405
|
// Attach a track's <transitionitem> siblings to its walked events. Shared
|
|
394
406
|
// by VIDEO and AUDIO tracks (E108): audio cross-fades used to be dropped at
|
|
395
407
|
// parse because only the video walk looked at transitionitems, so an XMEML
|
|
@@ -447,14 +459,7 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
447
459
|
const label = vi === 0 ? 'V' : `V${vi + 1}`;
|
|
448
460
|
const before = events.length;
|
|
449
461
|
// Junctions of this track's transitionitems, for -1 edge resolution.
|
|
450
|
-
|
|
451
|
-
currentJunctions = tlist.map((tr) => {
|
|
452
|
-
const s0 = Number(tr.start), e0 = Number(tr.end);
|
|
453
|
-
if (!Number.isFinite(s0) || !Number.isFinite(e0)) return null;
|
|
454
|
-
const al = String(tr.alignment || 'center').toLowerCase();
|
|
455
|
-
const frame = al === 'start-black' || al === 'start' ? s0 : al === 'end-black' || al === 'end' ? e0 : Math.round((s0 + e0) / 2);
|
|
456
|
-
return { frame, start: s0, end: e0 };
|
|
457
|
-
}).filter(Boolean);
|
|
462
|
+
currentJunctions = trackJunctionsOf(t);
|
|
458
463
|
// Solid Color / Color Matte generatoritems are GENERATOR legs: black by
|
|
459
464
|
// default (what Resolve writes for a fade's black side), or the
|
|
460
465
|
// `fillcolor` the item declares — Resolve's importer honours it and
|
|
@@ -480,6 +485,11 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
480
485
|
const label = ai === 0 ? 'A' : `A${ai + 1}`;
|
|
481
486
|
const before = events.length;
|
|
482
487
|
clipCursor = 0;
|
|
488
|
+
// Audio lanes carry -1 edges under their cross-fades exactly like video
|
|
489
|
+
// (Resolve's writer, measured E114: the incoming clip's <in> is the
|
|
490
|
+
// source at the overlap start and needs the junction offset) — the
|
|
491
|
+
// junction list must be THIS lane's, not the last video track's.
|
|
492
|
+
currentJunctions = trackJunctionsOf(t);
|
|
483
493
|
if (t.clipitem) walk(t.clipitem, label);
|
|
484
494
|
currentJunctions = [];
|
|
485
495
|
attachTransitions(t, label, before);
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.171.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.
|
|
14
|
+
VERSION = "2.171.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -2604,33 +2604,51 @@ def _find_timeline_item_by_id(tl, timeline_item_id) -> Optional[Any]:
|
|
|
2604
2604
|
return None
|
|
2605
2605
|
|
|
2606
2606
|
|
|
2607
|
-
|
|
2608
|
-
"""
|
|
2607
|
+
_TRANSITION_NAMES = {
|
|
2608
|
+
"cross dissolve", "dip to color dissolve", "additive dissolve", "non-additive dissolve",
|
|
2609
|
+
"smooth cut", "blur dissolve", "cross fade", "cross fade 0db", "cross fade +3db", "cross fade -3db",
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
|
|
2613
|
+
def _describe_track_items(items, track_type: str):
|
|
2614
|
+
"""GetItemListInTrack entries → [{name, id, start, end, duration, kind}].
|
|
2609
2615
|
|
|
2610
|
-
Transitions ARE items in that list (api_truth)
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2616
|
+
Transitions ARE items in that list (api_truth). What does NOT tell them
|
|
2617
|
+
apart, measured 2026-09-01 on Studio 19.1.3.7 (E113/E115): the name (an
|
|
2618
|
+
AUDIO cross-fade enumerates with an EMPTY name), GetMediaPoolItem() (a
|
|
2619
|
+
Solid Color generator and a subtitle have none either) and GetProperty()
|
|
2620
|
+
(generator and subtitle both return None, like a transition). What does:
|
|
2621
|
+
GEOMETRY — a transition straddles a cut, so one neighbour ENDS inside its
|
|
2622
|
+
span and another STARTS inside it; a generator or clip owns its span.
|
|
2623
|
+
kind: clip (has a MediaPoolItem) | transition (known name, or no media and
|
|
2624
|
+
straddles a cut) | generator (no media, owns its span) | subtitle (track).
|
|
2615
2625
|
"""
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
has_media = it.GetMediaPoolItem() is not None
|
|
2619
|
-
except Exception:
|
|
2620
|
-
has_media = True # an API surprise must not demote a clip to a transition
|
|
2621
|
-
if not has_media:
|
|
2626
|
+
raw = []
|
|
2627
|
+
for it in items or []:
|
|
2622
2628
|
try:
|
|
2623
|
-
|
|
2629
|
+
has_media = it.GetMediaPoolItem() is not None
|
|
2624
2630
|
except Exception:
|
|
2625
|
-
|
|
2626
|
-
|
|
2631
|
+
has_media = True # an API surprise must not demote a clip
|
|
2632
|
+
raw.append({
|
|
2633
|
+
"name": it.GetName(), "id": it.GetUniqueId(), "start": it.GetStart(),
|
|
2634
|
+
"end": it.GetEnd(), "duration": it.GetDuration(), "_media": has_media,
|
|
2635
|
+
})
|
|
2636
|
+
out = []
|
|
2637
|
+
for r in raw:
|
|
2638
|
+
name = str(r["name"] or "").strip().lower()
|
|
2639
|
+
if track_type == "subtitle":
|
|
2640
|
+
kind = "subtitle"
|
|
2641
|
+
elif r["_media"]:
|
|
2642
|
+
kind = "clip"
|
|
2643
|
+
elif name in _TRANSITION_NAMES:
|
|
2627
2644
|
kind = "transition"
|
|
2628
2645
|
else:
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2646
|
+
s0, e0 = r["start"], r["end"]
|
|
2647
|
+
ends_inside = any(o is not r and o["end"] is not None and s0 < o["end"] <= e0 for o in raw)
|
|
2648
|
+
starts_inside = any(o is not r and o["start"] is not None and s0 <= o["start"] < e0 for o in raw)
|
|
2649
|
+
kind = "transition" if (ends_inside and starts_inside) else "generator"
|
|
2650
|
+
out.append({k: v for k, v in r.items() if k != "_media"} | {"kind": kind})
|
|
2651
|
+
return out
|
|
2634
2652
|
|
|
2635
2653
|
|
|
2636
2654
|
def _get_timeline_item_for_fusion(p: Dict[str, Any]):
|
|
@@ -24444,7 +24462,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
24444
24462
|
if err:
|
|
24445
24463
|
return _err(err)
|
|
24446
24464
|
items = tl.GetItemListInTrack(track_type, track_index)
|
|
24447
|
-
return {"items":
|
|
24465
|
+
return {"items": _describe_track_items(items, track_type)}
|
|
24448
24466
|
elif action == "delete_clips":
|
|
24449
24467
|
# Find timeline items by unique IDs
|
|
24450
24468
|
ids_set = set(p["clip_ids"])
|
|
@@ -26232,7 +26250,7 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
26232
26250
|
},
|
|
26233
26251
|
"timeline": {
|
|
26234
26252
|
"get_items": {
|
|
26235
|
-
"summary": "List items on one track as a summary (name/id/start/end/duration/kind). Transitions ARE items — a video Cross Dissolve by name, an AUDIO cross-fade with an EMPTY name — so `kind` (clip | transition | generator) is
|
|
26253
|
+
"summary": "List items on one track as a summary (name/id/start/end/duration/kind). Transitions ARE items — a video Cross Dissolve by name, an AUDIO cross-fade with an EMPTY name — and generators/subtitles look identical to them by media-pool item and properties (all None), so `kind` (clip | transition | generator | subtitle) is decided by geometry: a transition straddles a cut.",
|
|
26236
26254
|
"params": "track_type (video|audio|subtitle), index|track_index (1-based)",
|
|
26237
26255
|
"returns": "{items: [{name, id, start, end, duration, kind}]}",
|
|
26238
26256
|
"example": 'timeline(action="get_items", params={"track_type": "video", "index": 1})',
|
package/src/utils/api_truth.py
CHANGED
|
@@ -895,9 +895,14 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
895
895
|
"with an EMPTY GetName() (24 frames, centered on the cut, "
|
|
896
896
|
"between the two clips) — so on audio lanes even the kind is "
|
|
897
897
|
"not readable from the name. The discriminator that holds for "
|
|
898
|
-
"BOTH: GetMediaPoolItem() is None AND GetProperty() is empty
|
|
899
|
-
"a Solid Color generator
|
|
900
|
-
"
|
|
898
|
+
"BOTH: GetMediaPoolItem() is None AND GetProperty() is empty — "
|
|
899
|
+
"BUT a Solid Color generator AND a subtitle item read the same "
|
|
900
|
+
"way (GetProperty() None, no MediaPoolItem; measured E115), so "
|
|
901
|
+
"that pair only separates clips from non-clips. What separates "
|
|
902
|
+
"a transition from a generator is GEOMETRY: a transition "
|
|
903
|
+
"straddles a cut (one neighbour ends inside its span, another "
|
|
904
|
+
"starts inside it) while a generator owns its span. "
|
|
905
|
+
"timeline.get_items reports `kind` on that basis.",
|
|
901
906
|
"recommended": "Automated QC of existing transitions IS possible and is "
|
|
902
907
|
"the main practical need — enumerate GetItemListInTrack, "
|
|
903
908
|
"treat any item whose GetProperty() is empty and whose "
|
|
@@ -2787,7 +2792,11 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2787
2792
|
"FCP7 XML writer, by contrast, carries audio, writes "
|
|
2788
2793
|
"transition-adjacent clip edges as -1 (the junction), and "
|
|
2789
2794
|
"emits `speed` followed by `variablespeed` 0 in the same "
|
|
2790
|
-
"timeremap effect."
|
|
2795
|
+
"timeremap effect. AUDIO cross-fades are written the same way "
|
|
2796
|
+
"— a transitionitem on the audio track between -1-edged "
|
|
2797
|
+
"clipitems (measured E114); the OTIO writer emits them as a "
|
|
2798
|
+
"`Custom_Transition` with symmetric offsets on the Audio "
|
|
2799
|
+
"track.",
|
|
2791
2800
|
"recommended": "For round-trip QC prefer EXPORT_OTIO (carries audio, "
|
|
2792
2801
|
"retimes as LinearTimeWarp/FreezeFrame, exact spans). "
|
|
2793
2802
|
"When an EDL is the required deliverable, expect no "
|