davinci-resolve-mcp 2.149.0 → 2.150.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,31 @@
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.150.0 — fades conform: BL legs author, and Resolve's own EDL importer drops them
6
+
7
+ ### Added
8
+
9
+ - **EDL fades author end-to-end** in `drt.assemble_from_interchange`: BL
10
+ (black) legs become Solid Color generator elements and the fades real
11
+ clip-to-generator dissolves. A CMX fade-in's zero-length BL slug grows
12
+ through the boundary-shift machinery (single-sided transitions refuse to
13
+ import — measured); the picture trims its head with source staying
14
+ record-aligned. Render-proven: luma ramps 18→123 across a 24-frame
15
+ fade-in and 123→16 across the fade-out, with the black tail holding.
16
+ Audio BL fades (to silence) drop with a stated reason — there is no
17
+ silence source to cross-fade against.
18
+ - Generator elements now insert into the track in **chronological item
19
+ order** (junction detection reads listed adjacency), and an edge-aligned
20
+ span whose leg is shorter than its boundary shift drops with a reason
21
+ instead of authoring a broken geometry.
22
+
23
+ ### Measured (new Resolve bug, filed in api-limitations)
24
+
25
+ - **Resolve's own EDL importer silently drops BL dissolves**: the fade-in
26
+ vanishes wholesale (frame 0 renders full-bright) and a fade-out leaves a
27
+ hard cut into the Solid Color generator it creates for the BL slug. A
28
+ single-sided transition element refuses to import entirely.
29
+
5
30
  ## What's New in v2.149.0 — subtitle render truth: ExportSubtitle lies on 19.x
6
31
 
7
32
  ### Measured (new Resolve bug, filed in api-limitations)
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.149.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.150.0-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.149.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.150.0-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.149.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.150.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 35 missing capabilities, 49 bugs / unreliable behaviors.
15
+ **Totals:** 35 missing capabilities, 50 bugs / unreliable behaviors.
16
16
 
17
17
  The authoritative source is the runtime-queryable `api_truth` ledger
18
18
  (`resolve_control api_truth "<query>"`); this document is generated from
@@ -688,6 +688,14 @@ values, or automation-hostile modal prompts.
688
688
  - **Workaround / current handling:** Do not use EXPORT_OTIO to carry or verify markers. editorial.verify_roundtrip reports this case as `markersNotInExport` (honesty flag, not a failure); read markers through the marker API for fidelity checks, and author them offline via drt.assemble spec.markers when a .drt must carry them.
689
689
  - **Tags:** timeline, export, otio, markers, silent-failure
690
690
 
691
+ ### MediaPool.ImportTimelineFromFile EDL (drops BL fades)
692
+
693
+ - **Object:** `MediaPool`
694
+ - **Signature:** `(filePath.edl, importOptions) -> Timeline`
695
+ - **Behavior:** EDL dissolves involving the BL (black) reel are dropped silently on import (measured on Studio 19.1.3.7, E91): a CMX fade-in (zero-length BL cut + D event) vanishes wholesale — frame 0 renders at full brightness — and a fade-out to a BL leg imports the BL as a Solid Color generator but drops the dissolve, leaving a hard cut to black. Import succeeds; the importer authors dissolves normally between two real media clips. A related law: a hand-authored SINGLE-SIDED transition element (span at a lone clip head or tail, only one neighboring item) refuses to import entirely — Resolve creates no timeline.
696
+ - **Workaround / current handling:** Conform EDLs with fades through drt.assemble_from_interchange: BL legs author as Solid Color generator elements and the fades as real clip-to-generator dissolves (render-verified: luma ramps 18->123 across a 24f fade-in and 123->16 across the fade-out, black tail holding 16). Audio BL fades (to silence) drop with a stated reason — there is no silence source to cross-fade against.
697
+ - **Tags:** edl, import, transitions, fades, silent-failure
698
+
691
699
  ### Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)
692
700
 
693
701
  - **Object:** `Project`
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.149.0"
40
+ VERSION = "2.150.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.149.0",
3
+ "version": "2.150.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -258,7 +258,13 @@ export function eventsToAssembleSpec(events, opts = {}) {
258
258
  const audioSkipped = events.length - vids.length - audsRaw.length - markerEvents.length;
259
259
  if (!vids.length) throw new Error('eventsToAssembleSpec: no video events with record ranges');
260
260
 
261
- const unmapped = [...new Set([...vids, ...auds].map((e) => e.source).filter((srcName) => !sourceMap[srcName]))];
261
+ // BL/BLACK reels are the EDL's built-in black source — they never need a
262
+ // sourceMap entry. Video BL legs author as Solid Color generator elements
263
+ // (Resolve's own EDL importer creates exactly that for the slug — but DROPS
264
+ // the fade dissolves around it, measured E91); audio BL legs are silence,
265
+ // which an empty track already is.
266
+ const isBL = (srcName) => /^(BL|BLACK)$/i.test(String(srcName || '').trim());
267
+ const unmapped = [...new Set([...vids, ...auds].map((e) => e.source).filter((srcName) => !isBL(srcName) && !sourceMap[srcName]))];
262
268
  if (unmapped.length) {
263
269
  throw new Error(
264
270
  `eventsToAssembleSpec: unmapped source reel(s): ${unmapped.join(', ')} — ` +
@@ -285,12 +291,40 @@ export function eventsToAssembleSpec(events, opts = {}) {
285
291
  const minRec = preserveStartTimecode ? 0 : minRecRaw;
286
292
  const ORIGIN = preserveStartTimecode ? 0 : DEFAULT_ORIGIN;
287
293
  const startFrame = preserveStartTimecode ? minRecRaw : DEFAULT_ORIGIN;
294
+ const blackLegs = [];
288
295
  for (const e of vids) {
289
296
  const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
290
297
  const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
291
298
  const durationFrames = recOut - recIn;
299
+ const vTrackEarly = trackNum(e.track);
300
+ if (isBL(e.source)) {
301
+ // BL leg → Solid Color generator element (renders black; the authored
302
+ // clip↔generator dissolve render-verified E91: luma ramps 124→16
303
+ // through the junction). ZERO-length BL is kept as a placement so a
304
+ // fade-in's boundary-shift can GROW it — the CMX fade-in form is a
305
+ // zero-length BL cut followed by a D event, and a single-sided
306
+ // transition refuses to import (measured E91), so the shift is the
307
+ // only authorable shape.
308
+ if (durationFrames < 0) continue;
309
+ const el = { type: 'generator', generatorName: 'Solid Color', track: vTrackEarly, startFrame: recIn, durationFrames, srcIn: 0 };
310
+ blackLegs.push(el);
311
+ if (e.transition) {
312
+ let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
313
+ d += d % 2;
314
+ let pre = Math.floor(d / 2);
315
+ if (e.transition.alignment === 'start') pre = 0;
316
+ transitionCandidates.push({
317
+ atFrame: recIn, durationFrames: d, track: vTrackEarly, pre,
318
+ explicitSpan: e.transition.alignment != null,
319
+ index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
320
+ source: e.source, srcIn: Infinity, incomingCutRef: el, cutPoint: e.transition.cutPoint,
321
+ });
322
+ }
323
+ placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: Infinity, durationFrames, track: vTrackEarly, cutRef: el });
324
+ continue;
325
+ }
292
326
  if (durationFrames <= 0) continue;
293
- const vTrack = trackNum(e.track);
327
+ const vTrack = vTrackEarly;
294
328
  const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps), ...(vTrack > 1 ? { track: vTrack } : {}) };
295
329
  // CLIP markers from the turnover (OTIO clip markers, XMEML clipitem
296
330
  // <marker>s) become ITEM markers on the cut (frames clip-relative,
@@ -363,7 +397,21 @@ export function eventsToAssembleSpec(events, opts = {}) {
363
397
  const audioPlacements = [];
364
398
  const audioRetimesSkipped = [];
365
399
  const audioTransCandidates = [];
400
+ let audioBlackLegsSkipped = 0;
366
401
  for (const e of auds) {
402
+ if (isBL(e.source)) {
403
+ // Audio BL = silence, which an empty track already is. A fade to/from
404
+ // it has no authorable form (no silence source to cross-fade against);
405
+ // the level steps at the cut instead of ramping — stated, not silent.
406
+ audioBlackLegsSkipped += 1;
407
+ if (e.transition) {
408
+ droppedTransitions.push({
409
+ index: e.index, type: e.transition.type, duration: e.transition.duration, trackType: 'audio',
410
+ reason: 'audio fade to/from BL (silence) — no silence source to cross-fade against; the level steps at the cut',
411
+ });
412
+ }
413
+ continue;
414
+ }
367
415
  const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
368
416
  const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
369
417
  const durationFrames = recOut - recIn;
@@ -408,7 +456,10 @@ export function eventsToAssembleSpec(events, opts = {}) {
408
456
 
409
457
  // Overlap is judged PER VIDEO TRACK — V2 stacking over V1 is legitimate
410
458
  // conform geometry (render-verified: an upper-track clip covers the lower).
411
- placements.sort((a, b) => a.start - b.start);
459
+ // end-tiebreak keeps a zero-length BL placement AHEAD of the picture leg
460
+ // that starts at the same frame (fade-in form) — start-only sorting would
461
+ // read them as an overlap.
462
+ placements.sort((a, b) => a.start - b.start || a.end - b.end);
412
463
  const byTrack = new Map();
413
464
  for (const pl of placements) {
414
465
  if (!byTrack.has(pl.track)) byTrack.set(pl.track, []);
@@ -457,10 +508,14 @@ export function eventsToAssembleSpec(events, opts = {}) {
457
508
  // FULL-duration outgoing tail.
458
509
  const pre = c.pre ?? c.durationFrames / 2;
459
510
  const post = c.durationFrames - pre;
511
+ // A BL side has infinite handles: a Solid Color generator extends freely
512
+ // in both directions (srcIn is already Infinity on BL candidates).
460
513
  const bHandle = c.srcIn >= pre;
461
514
  const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
462
515
  const aFrames = aSpec && Number(aSpec.frameCount);
463
- const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false;
516
+ const aHandle = isBL(prev.source)
517
+ ? true
518
+ : (Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false);
464
519
  if (!bHandle || !aHandle) {
465
520
  droppedTransitions.push({
466
521
  index: c.index, type: c.type, duration: c.rawDuration,
@@ -496,6 +551,16 @@ export function eventsToAssembleSpec(events, opts = {}) {
496
551
  // span, the edge law), else center.
497
552
  const cp = Number.isInteger(c.cutPoint) ? Math.min(c.durationFrames - 1, Math.max(1, c.cutPoint)) : c.durationFrames / 2;
498
553
  const shift = (pre === 0 ? 1 : -1) * cp;
554
+ // The shift must leave BOTH legs with positive duration — a leg
555
+ // shorter than the boundary shift cannot host its half of the span
556
+ // (and a zero-length survivor would re-create the inert edge form).
557
+ if (c.incomingCutRef.durationFrames - shift <= 0 || prev.cutRef.durationFrames + shift <= 0) {
558
+ droppedTransitions.push({
559
+ index: c.index, type: c.type, duration: c.rawDuration,
560
+ reason: `a leg is shorter than the ${Math.abs(shift)}f boundary shift the edge-aligned span requires`,
561
+ });
562
+ continue;
563
+ }
499
564
  prev.cutRef.durationFrames += shift;
500
565
  c.incomingCutRef.startFrame += shift;
501
566
  c.incomingCutRef.srcIn += shift;
@@ -563,8 +628,15 @@ export function eventsToAssembleSpec(events, opts = {}) {
563
628
  }))
564
629
  .filter((m) => m.frame >= (preserveStartTimecode ? startFrame : DEFAULT_ORIGIN));
565
630
 
631
+ // BL legs with surviving extent (a fade-in's zero-length slug GROWS through
632
+ // the boundary shift; one that never met a dissolve stays zero and drops
633
+ // out). srcIn was only scaffolding for the shift math — strip it.
634
+ const elements = blackLegs
635
+ .filter((el) => el.durationFrames > 0)
636
+ .map(({ srcIn: _srcIn, ...rest }) => rest);
637
+
566
638
  return {
567
- spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(markers.length ? { markers } : {}), ...(transitions.length ? { transitions } : {}) },
639
+ spec: { timelineName, media, ...(preserveStartTimecode ? { startFrame } : {}), ...(markers.length ? { markers } : {}), ...(transitions.length ? { transitions } : {}), ...(elements.length ? { elements } : {}) },
568
640
  report: {
569
641
  videoEvents: vids.length,
570
642
  sources: media.length,
@@ -573,7 +645,10 @@ export function eventsToAssembleSpec(events, opts = {}) {
573
645
  audioChannelLegsMerged,
574
646
  authoredAudioEvents: audioPlacements.length,
575
647
  audioRetimesSkipped,
576
- upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1).length,
648
+ ...(elements.length || audioBlackLegsSkipped ? {
649
+ blackLegs: { authoredGenerators: elements.length, audioSilenceLegsSkipped: audioBlackLegsSkipped },
650
+ } : {}),
651
+ upperTrackCutsVideoOnly: placements.filter((pl) => pl.track > 1 && !isBL(pl.source)).length,
577
652
  flattenedRetimes,
578
653
  authoredRetimes,
579
654
  authoredTransitions: transitions,
@@ -153,7 +153,7 @@ function requirePathArg(args, key, action) {
153
153
  export const drtTool = {
154
154
  name: 'drt',
155
155
  description:
156
- 'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble_from_interchange (EDL/OTIO/XML/AAF + sourceMap → IMPORTABLE RENDERING native .drt in one call; retimes AUTHOR — constant speed fwd/rev AND zero-speed freezes (EDL M2 000.0; render-proven frozen); cross-dissolves are AUTHORED when the cut abuts with handles both sides (render-verified on 19), else dropped with reason; ledger in `conform`), assemble (spec → IMPORTABLE native-schema .drt via template-spliced real structures; pass targetAppVersion e.g. \'19.1\' for pre-21 hosts), parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index}] to drive a "which sequence?" picker), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
156
+ 'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble_from_interchange (EDL/OTIO/XML/AAF + sourceMap → IMPORTABLE RENDERING native .drt in one call; retimes AUTHOR — constant speed fwd/rev AND zero-speed freezes (EDL M2 000.0; render-proven frozen); cross-dissolves are AUTHORED when the cut abuts with handles both sides (render-verified on 19), else dropped with reason; EDL BL fades AUTHOR too — BL legs become Solid Color generators and the fade a real clip-to-generator dissolve, luma-ramp render-verified, while Resolve\'s OWN EDL importer drops BL dissolves silently; ledger in `conform`), assemble (spec → IMPORTABLE native-schema .drt via template-spliced real structures; pass targetAppVersion e.g. \'19.1\' for pre-21 hosts), parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index}] to drive a "which sequence?" picker), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
157
157
  async handler({ action, args }) {
158
158
  if (action === 'parse') {
159
159
  const p = parseSchema.parse(requirePathArg(args, 'drtPath', 'parse'));
@@ -27,6 +27,8 @@ const {
27
27
  insertClipIntoTrack,
28
28
  replaceTrackVec,
29
29
  getTrackVec,
30
+ getItemsInner,
31
+ setItemsInner,
30
32
  freshDbIds,
31
33
  } = require('./seq-surgery');
32
34
 
@@ -72,7 +74,23 @@ async function placeGenerator(drpInput, opts = {}) {
72
74
  let createdTracks = 0;
73
75
  const cloneSource = tracks[0];
74
76
  while (tracks.length < trackIndex) { tracks.push(emptyTrackClone(cloneSource)); createdTracks += 1; }
75
- tracks[trackIndex - 1] = insertClipIntoTrack(tracks[trackIndex - 1], gen);
77
+ // Insert in CHRONOLOGICAL item order, not appended: placeTransition detects
78
+ // junctions by listed adjacency, so a generator that precedes a clip in
79
+ // time (a fade-in's black slug, E91) must precede it in <Items> too.
80
+ // Compound-clip elements are not scanned (their nested inner <Element>s
81
+ // defeat the lazy regex); a generator meant to sit before one appends.
82
+ const track = tracks[trackIndex - 1];
83
+ const innerItems = getItemsInner(track);
84
+ const clipRe = /<Element>\s*<Sm2Ti(?:VideoClip|AudioClip|Generator)\b[\s\S]*?<\/Sm2Ti(?:VideoClip|AudioClip|Generator)>\s*<\/Element>/g;
85
+ let insertAt = -1;
86
+ let m;
87
+ while ((m = clipRe.exec(innerItems)) !== null) {
88
+ const sm = /<Start>(\d+)<\/Start>/.exec(m[0]);
89
+ if (sm && parseInt(sm[1], 10) > startFrame) { insertAt = m.index; break; }
90
+ }
91
+ tracks[trackIndex - 1] = insertAt >= 0
92
+ ? setItemsInner(track, innerItems.slice(0, insertAt) + gen + innerItems.slice(insertAt))
93
+ : insertClipIntoTrack(track, gen);
76
94
 
77
95
  const xml = replaceTrackVec(seqXml, 'video', vtv, tracks);
78
96
  zip.file(entry, xml);
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.149.0"
90
+ VERSION = "2.150.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.149.0"
14
+ VERSION = "2.150.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -2737,6 +2737,33 @@ API_TRUTH: List[Dict[str, Any]] = [
2737
2737
  "submit": "bug",
2738
2738
  "mitigation": ["editorial.verify_roundtrip markersNotInExport"],
2739
2739
  },
2740
+ {
2741
+ "symbol": "MediaPool.ImportTimelineFromFile EDL (drops BL fades)",
2742
+ "object": "MediaPool",
2743
+ "signature": "(filePath.edl, importOptions) -> Timeline",
2744
+ "reality": "EDL dissolves involving the BL (black) reel are dropped "
2745
+ "silently on import (measured on Studio 19.1.3.7, E91): a "
2746
+ "CMX fade-in (zero-length BL cut + D event) vanishes "
2747
+ "wholesale — frame 0 renders at full brightness — and a "
2748
+ "fade-out to a BL leg imports the BL as a Solid Color "
2749
+ "generator but drops the dissolve, leaving a hard cut to "
2750
+ "black. Import succeeds; the importer authors dissolves "
2751
+ "normally between two real media clips. A related law: a "
2752
+ "hand-authored SINGLE-SIDED transition element (span at a "
2753
+ "lone clip head or tail, only one neighboring item) "
2754
+ "refuses to import entirely — Resolve creates no timeline.",
2755
+ "recommended": "Conform EDLs with fades through "
2756
+ "drt.assemble_from_interchange: BL legs author as "
2757
+ "Solid Color generator elements and the fades as real "
2758
+ "clip-to-generator dissolves (render-verified: luma "
2759
+ "ramps 18->123 across a 24f fade-in and 123->16 "
2760
+ "across the fade-out, black tail holding 16). Audio "
2761
+ "BL fades (to silence) drop with a stated reason — "
2762
+ "there is no silence source to cross-fade against.",
2763
+ "tags": ["edl", "import", "transitions", "fades", "silent-failure"],
2764
+ "submit": "bug",
2765
+ "mitigation": ["drt.assemble_from_interchange black-leg authoring"],
2766
+ },
2740
2767
  {
2741
2768
  "symbol": "Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)",
2742
2769
  "object": "Project",