davinci-resolve-mcp 2.141.0 → 2.142.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/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/native-drt-authoring.md +14 -4
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/aaf_probe.py +6 -1
- package/resolve-advanced/server/author-interchange.mjs +76 -19
- package/resolve-advanced/server/editorial.mjs +11 -3
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +1 -0
- package/resolve-advanced/vendor/drp-format/place-transition.js +14 -2
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
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.142.1 — the AAF leg joins the span fix
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **AAF dissolves were misaligned by half their duration**: the AAF walker
|
|
10
|
+
rewinds the incoming clip to the overlap START (the Edit Protocol
|
|
11
|
+
subtraction), so the parsed junction is the overlap's first frame — and the
|
|
12
|
+
old centered placement put half the blend before the overlap even began.
|
|
13
|
+
AAF transitions now carry `alignment: 'start'` through the same
|
|
14
|
+
render-proven span machinery as EDL (v2.142.0): span `[overlapStart,
|
|
15
|
+
overlapStart+dur)`, boundary shifted to the middle — which lands the
|
|
16
|
+
notional cut exactly at the AAF CutPoint default (centered in the overlap).
|
|
17
|
+
|
|
18
|
+
## What's New in v2.142.0 — transition span fidelity, and the edge law
|
|
19
|
+
|
|
20
|
+
### Changed (conform fidelity)
|
|
21
|
+
|
|
22
|
+
- **Transition spans now follow the turnover's actual geometry** instead of
|
|
23
|
+
always centering: EDL dissolves/wipes span `[cut, cut+dur)` (the CMX
|
|
24
|
+
convention), OTIO transitions use their explicit in/out offsets, XMEML
|
|
25
|
+
transitionitems use their own record span, and spec-level
|
|
26
|
+
`transitions[].startFrame` overrides. Handle requirements follow the real
|
|
27
|
+
span — a start-at-cut EDL dissolve needs NO incoming handle and a
|
|
28
|
+
full-duration outgoing tail. Render-verified: the authored EDL dissolve
|
|
29
|
+
blends exactly `[cut, cut+24)` (pure outgoing through cut−1, linear blend,
|
|
30
|
+
midpoint 182), matching Resolve's own EDL importer frame-for-frame.
|
|
31
|
+
|
|
32
|
+
### Measured (the edge law)
|
|
33
|
+
|
|
34
|
+
- A transition's rendered span follows `<Start>`/`<Duration>`
|
|
35
|
+
(`AlignmentType` is cosmetic), **but the clip boundary must sit strictly
|
|
36
|
+
inside the span** — an edge-aligned span (Start == the boundary) renders
|
|
37
|
+
inert. That is why Resolve's own EDL importer moves the cut +dur/2 and
|
|
38
|
+
centers; the bridge now reproduces exactly that cut-reshaping. Off-center
|
|
39
|
+
spans render fine (an uneven `[cut−6, cut+18)` span blended linearly
|
|
40
|
+
across its full width).
|
|
41
|
+
|
|
5
42
|
## What's New in v2.141.0 — OTIO transitions, depth-4, and honest gaps
|
|
6
43
|
|
|
7
44
|
### 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.142.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -135,10 +135,20 @@ outgoing clip through the window) — so routing an XMEML turnover through
|
|
|
135
135
|
`assemble_from_interchange` produces transitions that render where the host
|
|
136
136
|
importer's do not.
|
|
137
137
|
|
|
138
|
-
**Dissolve geometry
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
**Dissolve geometry — spans follow the turnover, and the edge law.** A
|
|
139
|
+
transition's rendered span follows `<Start>`/`<Duration>`; `AlignmentType`
|
|
140
|
+
is cosmetic. The clip boundary must sit STRICTLY INSIDE the span — an
|
|
141
|
+
edge-aligned span (Start == the boundary) renders inert (measured), which is
|
|
142
|
+
why Resolve's own EDL importer moves the cut +dur/2 and centers. The bridge
|
|
143
|
+
reproduces per-format geometry exactly: EDL dissolves span [cut, cut+dur)
|
|
144
|
+
with the boundary shifted +dur/2 (CMX convention, host-importer parity —
|
|
145
|
+
render-verified: blend f48→f71 on a cut at 48); OTIO uses its explicit
|
|
146
|
+
in/out offsets; XMEML uses the transitionitem's own record span; spec-level
|
|
147
|
+
`transitions[].startFrame` overrides. Handles follow the actual span: the
|
|
148
|
+
incoming needs `srcIn ≥` the pre-cut portion, the outgoing needs tail media
|
|
149
|
+
for the post-cut portion. Off-center spans render (blend measured across an
|
|
150
|
+
uneven [cut-6, cut+18) span); everything unauthorable stays in
|
|
151
|
+
`droppedTransitions` with the reason.
|
|
142
152
|
|
|
143
153
|
## Verification checklist for a delivered .drt
|
|
144
154
|
|
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.142.1"
|
|
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
|
@@ -1169,7 +1169,12 @@ def _walk_components(sequence, *, track, fps, rec, state, depth, transition=None
|
|
|
1169
1169
|
try:
|
|
1170
1170
|
if cls == "Transition":
|
|
1171
1171
|
duration = _length(comp)
|
|
1172
|
-
|
|
1172
|
+
# alignment 'start': after the rewind, the incoming clip's recIn IS the
|
|
1173
|
+
# overlap start, so the transition span is [recIn, recIn+duration) — and
|
|
1174
|
+
# the bridge's boundary-shift lands the notional cut at recIn+duration/2,
|
|
1175
|
+
# exactly the AAF CutPoint default (centered in the overlap). The old
|
|
1176
|
+
# centered-on-recIn placement put HALF the blend before the overlap began.
|
|
1177
|
+
pending_transition = {"type": "dissolve", "duration": duration, "alignment": "start"}
|
|
1173
1178
|
# Rewind: the next component overlaps the previous one by `duration`.
|
|
1174
1179
|
# Clamped at this sequence's own start — a leading transition has no
|
|
1175
1180
|
# preceding material to overlap, and a negative record position would
|
|
@@ -307,14 +307,26 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
307
307
|
// be authored (abutting predecessor + handles both sides) is decided
|
|
308
308
|
// after all placements are known.
|
|
309
309
|
let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
|
|
310
|
-
d += d % 2; //
|
|
310
|
+
d += d % 2; // even duration keeps every alignment on whole frames
|
|
311
|
+
// Span geometry per format (E73): `pre` = frames of the span BEFORE
|
|
312
|
+
// the cut. EDL: 0 (CMX start-at-cut — matches what Resolve's own EDL
|
|
313
|
+
// importer authors, E61 harvest); OTIO: the explicit in_offset; XMEML:
|
|
314
|
+
// derived from the transitionitem's own record start; no info: centered.
|
|
315
|
+
let pre = Math.floor(d / 2);
|
|
316
|
+
if (e.transition.alignment === 'start') pre = 0;
|
|
317
|
+
else if (e.transition.inOffset != null) pre = Math.min(d, Math.max(0, toTl(e.transition.inOffset, e.fps)));
|
|
318
|
+
else if (e.transition.recStart != null) {
|
|
319
|
+
const spanStartAbs = ORIGIN + (toTl(e.transition.recStart, e.fps) - minRec);
|
|
320
|
+
pre = Math.min(d, Math.max(0, recIn - spanStartAbs));
|
|
321
|
+
}
|
|
311
322
|
transitionCandidates.push({
|
|
312
|
-
atFrame: recIn, durationFrames: d, track: vTrack,
|
|
323
|
+
atFrame: recIn, durationFrames: d, track: vTrack, pre,
|
|
324
|
+
explicitSpan: e.transition.alignment != null || e.transition.inOffset != null || e.transition.recStart != null,
|
|
313
325
|
index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
|
|
314
|
-
source: e.source, srcIn: cut.srcIn,
|
|
326
|
+
source: e.source, srcIn: cut.srcIn, incomingCutRef: cut,
|
|
315
327
|
});
|
|
316
328
|
}
|
|
317
|
-
placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: cut.srcIn, durationFrames, track: vTrack });
|
|
329
|
+
placements.push({ start: recIn, end: recOut, index: e.index, source: e.source, srcIn: cut.srcIn, durationFrames, track: vTrack, cutRef: cut });
|
|
318
330
|
if (!perSource.has(e.source)) perSource.set(e.source, []);
|
|
319
331
|
perSource.get(e.source).push(cut);
|
|
320
332
|
}
|
|
@@ -341,13 +353,17 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
341
353
|
if (e.transition) {
|
|
342
354
|
let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
|
|
343
355
|
d += d % 2;
|
|
356
|
+
let pre = Math.floor(d / 2);
|
|
357
|
+
if (e.transition.alignment === 'start') pre = 0;
|
|
358
|
+
else if (e.transition.inOffset != null) pre = Math.min(d, Math.max(0, toTl(e.transition.inOffset, e.fps)));
|
|
344
359
|
audioTransCandidates.push({
|
|
345
|
-
atFrame: recIn, durationFrames: d, track,
|
|
360
|
+
atFrame: recIn, durationFrames: d, track, pre,
|
|
361
|
+
explicitSpan: e.transition.alignment != null || e.transition.inOffset != null,
|
|
346
362
|
index: e.index, type: e.transition.type, rawDuration: e.transition.duration,
|
|
347
|
-
source: e.source, srcIn: cut.srcIn,
|
|
363
|
+
source: e.source, srcIn: cut.srcIn, incomingCutRef: cut,
|
|
348
364
|
});
|
|
349
365
|
}
|
|
350
|
-
audioPlacements.push({ start: recIn, end: recOut, index: e.index, track, source: e.source, srcIn: cut.srcIn, durationFrames });
|
|
366
|
+
audioPlacements.push({ start: recIn, end: recOut, index: e.index, track, source: e.source, srcIn: cut.srcIn, durationFrames, cutRef: cut });
|
|
351
367
|
if (!perSource.has(e.source)) perSource.set(e.source, []);
|
|
352
368
|
perSource.get(e.source).push(cut);
|
|
353
369
|
}
|
|
@@ -412,16 +428,21 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
412
428
|
droppedTransitions.push({ index: c.index, type: c.type, duration: c.rawDuration, reason: 'no abutting predecessor at the cut' });
|
|
413
429
|
continue;
|
|
414
430
|
}
|
|
415
|
-
|
|
416
|
-
|
|
431
|
+
// Handle math follows the actual span: `pre` frames before the cut need
|
|
432
|
+
// incoming pre-roll (srcIn >= pre); `post` frames after need outgoing
|
|
433
|
+
// tail media. A start-at-cut EDL dissolve needs NO incoming handle and a
|
|
434
|
+
// FULL-duration outgoing tail.
|
|
435
|
+
const pre = c.pre ?? c.durationFrames / 2;
|
|
436
|
+
const post = c.durationFrames - pre;
|
|
437
|
+
const bHandle = c.srcIn >= pre;
|
|
417
438
|
const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
|
|
418
439
|
const aFrames = aSpec && Number(aSpec.frameCount);
|
|
419
|
-
const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames +
|
|
440
|
+
const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false;
|
|
420
441
|
if (!bHandle || !aHandle) {
|
|
421
442
|
droppedTransitions.push({
|
|
422
443
|
index: c.index, type: c.type, duration: c.rawDuration,
|
|
423
|
-
reason: `insufficient handles for a
|
|
424
|
-
`${bHandle ? '' :
|
|
444
|
+
reason: `insufficient handles for a ${c.durationFrames}f dissolve spanning [cut-${pre}, cut+${post})` +
|
|
445
|
+
`${bHandle ? '' : ` (incoming srcIn < ${pre})`}${aHandle ? '' : ` (outgoing tail media < ${post})`}`,
|
|
425
446
|
});
|
|
426
447
|
continue;
|
|
427
448
|
}
|
|
@@ -438,7 +459,28 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
438
459
|
else if (/additive/i.test(raw)) kind = 'additive';
|
|
439
460
|
else if (/fade to color/i.test(raw)) kind = 'fade-to-color';
|
|
440
461
|
else if (/smooth cut/i.test(raw)) kind = 'smooth-cut';
|
|
441
|
-
|
|
462
|
+
// EDGE LAW (E73, measured): the clip boundary must sit STRICTLY INSIDE
|
|
463
|
+
// the transition span — an edge-aligned span (Start == boundary) renders
|
|
464
|
+
// INERT. Resolve's own EDL importer solves the CMX start-at-cut case by
|
|
465
|
+
// MOVING the cut half the duration and centering; reproduce exactly that:
|
|
466
|
+
// extend the outgoing clip, trim the incoming head (source stays
|
|
467
|
+
// aligned), and keep the RENDERED span [cut-pre, cut-pre+d) unchanged.
|
|
468
|
+
let atFrame = c.atFrame;
|
|
469
|
+
const spanStartAbs = c.atFrame - pre;
|
|
470
|
+
if (c.explicitSpan && (pre === 0 || pre === c.durationFrames)) {
|
|
471
|
+
const shift = (pre === 0 ? 1 : -1) * (c.durationFrames / 2);
|
|
472
|
+
prev.cutRef.durationFrames += shift;
|
|
473
|
+
c.incomingCutRef.startFrame += shift;
|
|
474
|
+
c.incomingCutRef.srcIn += shift;
|
|
475
|
+
c.incomingCutRef.durationFrames -= shift;
|
|
476
|
+
prev.end += shift;
|
|
477
|
+
atFrame += shift;
|
|
478
|
+
}
|
|
479
|
+
transitions.push({
|
|
480
|
+
track: c.track, atFrame, durationFrames: c.durationFrames,
|
|
481
|
+
...(kind === 'dissolve' ? {} : { type: kind }),
|
|
482
|
+
...(c.explicitSpan ? { startFrame: spanStartAbs } : {}),
|
|
483
|
+
});
|
|
442
484
|
}
|
|
443
485
|
// Audio cross-fades, same geometry rules (render-verified on 19.1.3.7 via
|
|
444
486
|
// the harvested cross-fade template: the highpass RMS ramps, not steps).
|
|
@@ -448,20 +490,35 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
448
490
|
droppedTransitions.push({ index: c.index, type: c.type, duration: c.rawDuration, trackType: 'audio', reason: 'no abutting predecessor at the cut' });
|
|
449
491
|
continue;
|
|
450
492
|
}
|
|
451
|
-
const
|
|
452
|
-
const
|
|
493
|
+
const pre = c.pre ?? c.durationFrames / 2;
|
|
494
|
+
const post = c.durationFrames - pre;
|
|
495
|
+
const bHandle = c.srcIn >= pre;
|
|
453
496
|
const aSpec = sourceMap[prev.source] && sourceMap[prev.source].spec;
|
|
454
497
|
const aFrames = aSpec && Number(aSpec.frameCount);
|
|
455
|
-
const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames +
|
|
498
|
+
const aHandle = Number.isFinite(aFrames) ? prev.srcIn + prev.durationFrames + post <= aFrames : false;
|
|
456
499
|
if (!bHandle || !aHandle) {
|
|
457
500
|
droppedTransitions.push({
|
|
458
501
|
index: c.index, type: c.type, duration: c.rawDuration, trackType: 'audio',
|
|
459
|
-
reason: `insufficient handles for a
|
|
460
|
-
`${bHandle ? '' :
|
|
502
|
+
reason: `insufficient handles for a ${c.durationFrames}f cross-fade spanning [cut-${pre}, cut+${post})` +
|
|
503
|
+
`${bHandle ? '' : ` (incoming srcIn < ${pre})`}${aHandle ? '' : ` (outgoing tail media < ${post})`}`,
|
|
461
504
|
});
|
|
462
505
|
continue;
|
|
463
506
|
}
|
|
464
|
-
|
|
507
|
+
let atFrame = c.atFrame;
|
|
508
|
+
const spanStartAbs = c.atFrame - pre;
|
|
509
|
+
if (c.explicitSpan && (pre === 0 || pre === c.durationFrames)) {
|
|
510
|
+
const shift = (pre === 0 ? 1 : -1) * (c.durationFrames / 2);
|
|
511
|
+
prev.cutRef.durationFrames += shift;
|
|
512
|
+
c.incomingCutRef.startFrame += shift;
|
|
513
|
+
c.incomingCutRef.srcIn += shift;
|
|
514
|
+
c.incomingCutRef.durationFrames -= shift;
|
|
515
|
+
prev.end += shift;
|
|
516
|
+
atFrame += shift;
|
|
517
|
+
}
|
|
518
|
+
transitions.push({
|
|
519
|
+
track: c.track, atFrame, durationFrames: c.durationFrames, trackType: 'audio',
|
|
520
|
+
...(c.explicitSpan ? { startFrame: spanStartAbs } : {}),
|
|
521
|
+
});
|
|
465
522
|
}
|
|
466
523
|
|
|
467
524
|
// Turnover markers (EDL * LOC: locators, OTIO Marker objects) → authored
|
|
@@ -118,7 +118,10 @@ export function parseEDL(text, opts = {}) {
|
|
|
118
118
|
srcOut: tcToFrames(srcOut, fps),
|
|
119
119
|
recIn: tcToFrames(recIn, fps),
|
|
120
120
|
recOut: tcToFrames(recOut, fps),
|
|
121
|
-
|
|
121
|
+
// CMX convention: the dissolve/wipe span STARTS at the cut (occupies
|
|
122
|
+
// the first `dur` frames of the incoming event) — matching what
|
|
123
|
+
// Resolve's own EDL importer authors (E61 harvest: Start == the cut).
|
|
124
|
+
transition: transition && transition !== 'C' ? { type: transition, duration: dur || 0, alignment: 'start' } : null,
|
|
122
125
|
fps,
|
|
123
126
|
}),
|
|
124
127
|
);
|
|
@@ -170,7 +173,9 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
170
173
|
if (schema.startsWith('Transition')) {
|
|
171
174
|
const inOff = (child.in_offset && child.in_offset.value) || 0;
|
|
172
175
|
const outOff = (child.out_offset && child.out_offset.value) || 0;
|
|
173
|
-
|
|
176
|
+
// in_offset = frames BEFORE the cut, out_offset = frames after; the
|
|
177
|
+
// bridge places the span [cut - inOffset, cut + outOffset).
|
|
178
|
+
pendingTransition = { type: String(child.transition_type || 'SMPTE_Dissolve'), duration: inOff + outOff, inOffset: inOff };
|
|
174
179
|
continue;
|
|
175
180
|
}
|
|
176
181
|
if (schema.startsWith('Clip')) {
|
|
@@ -272,7 +277,10 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
272
277
|
if (!Number.isFinite(s) || !Number.isFinite(e2)) continue;
|
|
273
278
|
const effectId = (tr.effect && (tr.effect.effectid || tr.effect.name)) || 'Cross Dissolve';
|
|
274
279
|
const incoming = events.slice(before).find((ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2);
|
|
275
|
-
|
|
280
|
+
// recStart carries the transitionitem's EXPLICIT record span start
|
|
281
|
+
// (sequence-relative) so the bridge reproduces the editor's actual
|
|
282
|
+
// alignment instead of assuming centered.
|
|
283
|
+
if (incoming) incoming.transition = { type: String(effectId), duration: e2 - s, recStart: s };
|
|
276
284
|
}
|
|
277
285
|
});
|
|
278
286
|
const atracks = media.audio && media.audio.track ? (Array.isArray(media.audio.track) ? media.audio.track : [media.audio.track]) : [];
|
|
@@ -85,7 +85,7 @@ const STYLE_PRETTY_TYPES = {
|
|
|
85
85
|
const TRANSITION_TYPES = ['dissolve', 'wipe', ...Object.keys(STYLE_PRETTY_TYPES)];
|
|
86
86
|
|
|
87
87
|
async function placeTransition(drpInput, opts = {}) {
|
|
88
|
-
const { track, atFrame, durationFrames = 24, trackType = 'video', type = 'dissolve', timelineUuid } = opts;
|
|
88
|
+
const { track, atFrame, durationFrames = 24, trackType = 'video', type = 'dissolve', spanStart, timelineUuid } = opts;
|
|
89
89
|
if (!TRANSITION_TYPES.includes(type)) throw new Error(`placeTransition: type must be one of ${TRANSITION_TYPES.join(', ')}`);
|
|
90
90
|
if (type !== 'dissolve' && trackType === 'audio') throw new Error('placeTransition: styled transitions are video-only (audio junctions cross-fade)');
|
|
91
91
|
if (!Number.isInteger(track) || track < 1) throw new TypeError('placeTransition: track must be a positive integer');
|
|
@@ -114,7 +114,19 @@ async function placeTransition(drpInput, opts = {}) {
|
|
|
114
114
|
} else if (STYLE_PRETTY_TYPES[type]) {
|
|
115
115
|
trans = trans.replace(/<PrettyType>Cross Dissolve<\/PrettyType>/, `<PrettyType>${STYLE_PRETTY_TYPES[type]}</PrettyType>`);
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
// Span geometry follows <Start>/<Duration>, NOT AlignmentType (the E61
|
|
118
|
+
// harvest of Resolve's own EDL import carries AlignmentType 2 with the
|
|
119
|
+
// span STARTING at the cut — the CMX convention — while our default
|
|
120
|
+
// centers it). spanStart (absolute) overrides for format parity: EDL
|
|
121
|
+
// dissolves start at the cut, OTIO splits by in/out offsets, XMEML
|
|
122
|
+
// transitionitems carry their explicit record span.
|
|
123
|
+
if (spanStart !== undefined) {
|
|
124
|
+
if (!Number.isInteger(spanStart)) throw new TypeError('placeTransition: spanStart must be an integer frame');
|
|
125
|
+
if (spanStart > atFrame || spanStart + durationFrames < atFrame) {
|
|
126
|
+
throw new RangeError(`placeTransition: span [${spanStart}, ${spanStart + durationFrames}) does not cover the cut at ${atFrame}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const start = spanStart !== undefined ? spanStart : atFrame - Math.floor(durationFrames / 2);
|
|
118
130
|
trans = trans.replace(/<Start>\d+<\/Start>/, `<Start>${start}</Start>`);
|
|
119
131
|
trans = trans.replace(/<Duration>\d+<\/Duration>/, `<Duration>${durationFrames}</Duration>`);
|
|
120
132
|
const transitionDbId = (trans.match(/<Sm2TiTransition DbId="([^"]+)"/) || [])[1] || null;
|
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.142.1"
|
|
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()}")
|