davinci-resolve-mcp 2.163.0 → 2.164.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 +36 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/kernels/timeline-conform-interchange-kernel.md +2 -1
- package/docs/kernels/timeline-edit-kernel.md +4 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/README.md +6 -2
- package/resolve-advanced/server/editorial.mjs +187 -40
- package/resolve-advanced/server/tools/editorial.mjs +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
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.164.0 — E106: the changelist sees junctions
|
|
6
|
+
|
|
7
|
+
`editorial.turnover_changelist` diffed clips and was blind to everything
|
|
8
|
+
that happens *between* them. Measured on a faded, dissolved, retimed EDL
|
|
9
|
+
pair: a 24→12-frame dissolve change reported nothing, both dropped fades
|
|
10
|
+
read as "BL gone", and the zero-length CMX carrier line of a dissolve's
|
|
11
|
+
outgoing side read as "B002 gone". The timing guards paired first-row-wins,
|
|
12
|
+
so an identical cut with one A2 leg dropped flagged a FALSE flattened
|
|
13
|
+
retime and never flagged the audio drop (`track === 'A'` missed `A2`).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The changelist diffs junctions.** `transition_added` /
|
|
18
|
+
`transition_dropped` / `transition_changed` entries name the outgoing and
|
|
19
|
+
incoming sources, classify fade in/out vs dissolve, and carry the span and
|
|
20
|
+
duration/type/pre-roll deltas — a dissolve reshaped from centered to
|
|
21
|
+
start-at-cut is a change even when both clips stayed put. Spans derive
|
|
22
|
+
exactly as the bridge places them (CMX start-at-cut, OTIO `in_offset`,
|
|
23
|
+
XMEML/PrProj `recStart`, AAF overlap start).
|
|
24
|
+
- **Carrier lines and fade legs never read as sources.** Zero-length events
|
|
25
|
+
(CMX outgoing marker lines, the synthesized BL fade slugs) and the black
|
|
26
|
+
legs a transition references fold into the junction diff; the changelist
|
|
27
|
+
reports how many in `carriersFolded`. A cut diffed against itself is
|
|
28
|
+
silent on every axis.
|
|
29
|
+
- **`timingGuards` pairs instance-to-instance.** Same track+source, closest
|
|
30
|
+
record position, consumed once — the pairing the changelist already used —
|
|
31
|
+
so a source cut twice at two speeds no longer cross-compares. Flags carry
|
|
32
|
+
`recIn`. Dropped-audio detection reads any `A`-track (`A`, `A2`, …), and a
|
|
33
|
+
lost fade or dissolve is now a timing lie (`transition_dropped`).
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `editorial.mjs` exports `transitionSpan(event)` and
|
|
38
|
+
`listTransitions(events)` — the junction model shared by the changelist
|
|
39
|
+
and the guards.
|
|
40
|
+
|
|
5
41
|
## What's New in v2.163.0 — E105: QC through every export format
|
|
6
42
|
|
|
7
43
|
One faded, dissolved, retimed conform exported from Resolve three ways — OTIO,
|
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.164.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -142,7 +142,8 @@ files and the project DB.
|
|
|
142
142
|
inside `.drp`/`.drt` as `<OfflineClip>` entries; patch them here.
|
|
143
143
|
- **`editorial`** — `parse_interchange` (EDL/OTIO/XMEML natively; **AAF via pyaaf2**,
|
|
144
144
|
multi-layer Avid turnovers included — honest refuse only when pyaaf2 is absent),
|
|
145
|
-
`turnover_changelist` (moved/retimed/replaced/new/gone
|
|
145
|
+
`turnover_changelist` (moved/retimed/trimmed/replaced/new/gone plus transitions
|
|
146
|
+
added/dropped/changed with fade in/out, with timing guards),
|
|
146
147
|
`conform_manifest`, `marker_roundtrip`.
|
|
147
148
|
- **`drt` / `project_db`** — timeline file authoring and DB patching.
|
|
148
149
|
|
|
@@ -188,8 +188,10 @@ timelines and reasons over editorial interchange with **no Resolve running**:
|
|
|
188
188
|
|
|
189
189
|
- **`editorial`** — `parse_interchange` (EDL/OTIO/XMEML natively; **AAF via pyaaf2**,
|
|
190
190
|
multi-layer Avid turnovers included — honest refuse only when pyaaf2 is absent),
|
|
191
|
-
`turnover_changelist` (moved/retimed/replaced/new/gone between two cuts
|
|
192
|
-
|
|
191
|
+
`turnover_changelist` (moved/retimed/trimmed/replaced/new/gone between two cuts PLUS
|
|
192
|
+
the junction diff — transitions added/dropped/changed with fade in/out and span deltas,
|
|
193
|
+
CMX carriers and BL fade legs folded in — with timing silent-lie guards),
|
|
194
|
+
`conform_manifest`, `marker_roundtrip`.
|
|
193
195
|
- **`drt`** — `.drt` timeline file authoring + structural diff.
|
|
194
196
|
|
|
195
197
|
Use these to answer "what changed between v3 and v4" or to hand a conform an
|
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.164.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
|
@@ -105,7 +105,11 @@ Each dispatches on an `action`. Highlights:
|
|
|
105
105
|
the `otio` and `edl` targets only — this flat `drt` target has no per-clip speed field and reports
|
|
106
106
|
every retime it flattens in `flattened`; for a `.drt` that AUTHORS retimes (plus dissolves,
|
|
107
107
|
multi-track, audio) use `drt.assemble_from_interchange` instead),
|
|
108
|
-
`turnover_changelist` (moved/retimed/replaced/new/gone
|
|
108
|
+
`turnover_changelist` (moved/retimed/trimmed/replaced/new/gone PLUS the junction diff —
|
|
109
|
+
`transition_added`/`transition_dropped`/`transition_changed` with fade in/out, outgoing/incoming, span and
|
|
110
|
+
duration/type/pre-roll deltas; CMX carrier lines and BL fade legs fold into the junctions instead of
|
|
111
|
+
reading as gone/new sources; events pair instance-to-instance by closest record position — + timing
|
|
112
|
+
silent-lie guards incl. lost transitions and dropped audio on any A-track), `conform_manifest`,
|
|
109
113
|
`marker_roundtrip`.
|
|
110
114
|
- **`provenance`** — provenance / audit: `gallery_lineage`, `grade_provenance` ("why is this graded this
|
|
111
115
|
way"), `cdl_export` (+ `cdl_diff`, round-trip asserted), `revision_tracking`, `episode_report`.
|
|
@@ -196,7 +200,7 @@ plausible-looking short event list. Three limits are deliberate:
|
|
|
196
200
|
mattes) applies to what shows through from below and references no media of its own. OTIO materializes
|
|
197
201
|
such layers as tracks of gaps, so its track count can exceed the number of layers with media.
|
|
198
202
|
- **Only `NestedScope` layers are numbered.** Non-nested slots keep the flat `V`/`A` label, which is what
|
|
199
|
-
`editorial.mjs`'s
|
|
203
|
+
`editorial.mjs`'s `/^A\d*$/` audio-follows-video heuristic reads (`A`, `A2`, … all count as audio).
|
|
200
204
|
|
|
201
205
|
## Provenance & license
|
|
202
206
|
Vendored libraries are clean offline format-interop and deterministic compute code: no secrets, no
|
|
@@ -476,38 +476,139 @@ export function parseInterchange(format, content, opts = {}) {
|
|
|
476
476
|
|
|
477
477
|
// ── turnover_changelist ────────────────────────────────────────────────
|
|
478
478
|
const sig = (e) => `${e.track}:${e.source}`;
|
|
479
|
+
const isBlackSource = (s) => /^(bl|black|solid color)$/i.test(String(s || '').trim());
|
|
480
|
+
const isAudioTrack = (t) => /^A\d*$/i.test(String(t || ''));
|
|
481
|
+
// A zero-length event is a CARRIER, never a cut: the outgoing marker line of
|
|
482
|
+
// a CMX dissolve pair, or the zero-length BL slug every parser synthesizes at
|
|
483
|
+
// a fade (E91/E92/E93). It has no picture of its own to be moved/gone/new.
|
|
484
|
+
const isCarrier = (e) => e.recIn != null && e.recOut != null && e.recIn === e.recOut;
|
|
479
485
|
|
|
480
486
|
/**
|
|
481
|
-
*
|
|
487
|
+
* The record SPAN a transition event occupies, derived exactly the way the
|
|
488
|
+
* bridge places it (author-interchange): alignment 'start' → the span begins
|
|
489
|
+
* at the incoming's recIn (CMX start-at-cut / AAF overlap start); OTIO
|
|
490
|
+
* inOffset → that many frames before the cut; XMEML/PrProj recStart → the
|
|
491
|
+
* explicit span start; nothing declared → centered. `junction` is the
|
|
492
|
+
* incoming event's recIn — the same frame verify_roundtrip's fade windows
|
|
493
|
+
* key on.
|
|
494
|
+
* @param {Object} e normalized event carrying `transition`
|
|
495
|
+
*/
|
|
496
|
+
export function transitionSpan(e) {
|
|
497
|
+
const t = e.transition;
|
|
498
|
+
if (!t || e.recIn == null) return null;
|
|
499
|
+
const d = Math.max(0, Number(t.duration) || 0);
|
|
500
|
+
let pre;
|
|
501
|
+
if (t.alignment === 'start') pre = 0;
|
|
502
|
+
else if (t.inOffset != null) pre = Math.max(0, Math.min(d, Number(t.inOffset) || 0));
|
|
503
|
+
else if (t.recStart != null) pre = Math.max(0, Math.min(d, e.recIn - Number(t.recStart)));
|
|
504
|
+
else pre = Math.floor(d / 2);
|
|
505
|
+
const start = e.recIn - pre;
|
|
506
|
+
return { start, end: start + d, junction: e.recIn, pre, duration: d };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Every junction in an event list as a transition record: span, type, the
|
|
511
|
+
* OUTGOING event (the same-track neighbour whose extent touches the span
|
|
512
|
+
* start — a CMX carrier line, an abutting clip, or the overlapping AAF
|
|
513
|
+
* predecessor) and the INCOMING (the carrier of the `transition` field), with
|
|
514
|
+
* black on either side classified as a fade. Pure over events.
|
|
515
|
+
* @param {Array} events
|
|
516
|
+
* @returns {Array<{track,type,duration,start,end,junction,pre,outgoing,incoming,fade,incomingEvent,outgoingEvent}>}
|
|
517
|
+
*/
|
|
518
|
+
export function listTransitions(events) {
|
|
519
|
+
const out = [];
|
|
520
|
+
for (const e of events) {
|
|
521
|
+
if (!e.transition || !(Number(e.transition.duration) > 0)) continue;
|
|
522
|
+
const span = transitionSpan(e);
|
|
523
|
+
if (!span) continue;
|
|
524
|
+
const cands = events.filter((o) => o !== e && o.track === e.track && o.recIn != null && o.recOut != null
|
|
525
|
+
&& o.recIn <= span.start && o.recOut >= span.start);
|
|
526
|
+
// The latest-ending toucher is the outgoing: a carrier line sits exactly
|
|
527
|
+
// at the span start, an abutting clip ends there, an AAF predecessor
|
|
528
|
+
// overlaps past it. Prefer a picture over black only when both touch.
|
|
529
|
+
cands.sort((a, b) => (b.recOut - a.recOut) || ((events.indexOf(b)) - events.indexOf(a)));
|
|
530
|
+
let outgoingEvent = cands[0] || null;
|
|
531
|
+
if (!outgoingEvent) {
|
|
532
|
+
// Nothing touches the span start (a pre-rolled fade-in at track start
|
|
533
|
+
// whose span begins before frame 0, or a gap-adjacent junction): the
|
|
534
|
+
// outgoing is whatever last ENDED at or before the junction — the
|
|
535
|
+
// zero-length BL slug the parsers synthesize sits exactly there.
|
|
536
|
+
const before = events.filter((o) => o !== e && o.track === e.track && o.recOut != null && o.recOut <= span.junction);
|
|
537
|
+
before.sort((a, b) => (b.recOut - a.recOut) || (events.indexOf(b) - events.indexOf(a)));
|
|
538
|
+
outgoingEvent = before[0] || null;
|
|
539
|
+
}
|
|
540
|
+
const outgoing = outgoingEvent ? outgoingEvent.source : null;
|
|
541
|
+
const incoming = e.source;
|
|
542
|
+
const fade = isBlackSource(outgoing) ? 'in' : isBlackSource(incoming) ? 'out' : null;
|
|
543
|
+
out.push({
|
|
544
|
+
track: e.track, type: e.transition.type || 'dissolve', duration: span.duration,
|
|
545
|
+
start: span.start, end: span.end, junction: span.junction, pre: span.pre,
|
|
546
|
+
outgoing, incoming, fade, incomingEvent: e, outgoingEvent,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Pair old and new PICTURE/AUDIO events once each: same track+source, closest
|
|
554
|
+
* record position wins, consumed on match (a source used twice at two speeds
|
|
555
|
+
* pairs each instance with its own — never first-row-wins). Carriers and the
|
|
556
|
+
* black legs a transition references never enter the pairing: they are the
|
|
557
|
+
* junction's business (listTransitions), not sources.
|
|
558
|
+
*/
|
|
559
|
+
function pairEvents(oldEvents, newEvents, recTol = 1) {
|
|
560
|
+
const cutsOf = (events) => {
|
|
561
|
+
const trs = listTransitions(events);
|
|
562
|
+
const fadeBlack = new Set();
|
|
563
|
+
for (const t of trs) {
|
|
564
|
+
if (t.outgoingEvent && isBlackSource(t.outgoingEvent.source)) fadeBlack.add(t.outgoingEvent);
|
|
565
|
+
if (isBlackSource(t.incomingEvent.source)) fadeBlack.add(t.incomingEvent);
|
|
566
|
+
}
|
|
567
|
+
const cuts = [], carriers = [];
|
|
568
|
+
for (const e of events) {
|
|
569
|
+
if (e.track === 'MARKER') continue;
|
|
570
|
+
if (isCarrier(e) || fadeBlack.has(e)) carriers.push(e);
|
|
571
|
+
else cuts.push(e);
|
|
572
|
+
}
|
|
573
|
+
return { cuts, carriers, transitions: trs };
|
|
574
|
+
};
|
|
575
|
+
const o = cutsOf(oldEvents), n = cutsOf(newEvents);
|
|
576
|
+
const pool = o.cuts.map((e) => ({ e, used: false }));
|
|
577
|
+
const pairs = [], unmatchedNew = [];
|
|
578
|
+
for (const ne of n.cuts) {
|
|
579
|
+
const cands = pool.filter((p) => !p.used && sig(p.e) === sig(ne));
|
|
580
|
+
if (!cands.length) { unmatchedNew.push(ne); continue; }
|
|
581
|
+
cands.sort((a, b) => Math.abs((a.e.recIn ?? 0) - (ne.recIn ?? 0)) - Math.abs((b.e.recIn ?? 0) - (ne.recIn ?? 0)));
|
|
582
|
+
cands[0].used = true;
|
|
583
|
+
pairs.push({ oe: cands[0].e, ne });
|
|
584
|
+
}
|
|
585
|
+
const unmatchedOld = pool.filter((p) => !p.used).map((p) => p.e);
|
|
586
|
+
return { pairs, unmatchedOld, unmatchedNew, old: o, new: n, recTol };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Diff two normalized event lists → per-event {kind: moved|retimed|trimmed|replaced|new|gone}
|
|
591
|
+
* PLUS per-junction {kind: transition_added|transition_dropped|transition_changed}
|
|
592
|
+
* (fade 'in'/'out' or null for a dissolve). Zero-length carrier lines and the
|
|
593
|
+
* black legs that carry fades fold into the junction diff instead of reading
|
|
594
|
+
* as gone/new sources; a dissolve whose duration, type or span shape changed
|
|
595
|
+
* is a change even when both clips stayed put.
|
|
482
596
|
* @param {Array} oldEvents
|
|
483
597
|
* @param {Array} newEvents
|
|
484
598
|
* @param {{recTolerance?:number}} [opts]
|
|
485
599
|
*/
|
|
486
600
|
export function diffChangelist(oldEvents, newEvents, opts = {}) {
|
|
487
601
|
const recTol = opts.recTolerance ?? 1;
|
|
488
|
-
const
|
|
602
|
+
const P = pairEvents(oldEvents, newEvents, recTol);
|
|
489
603
|
const changes = [];
|
|
490
604
|
|
|
491
|
-
for (const ne of
|
|
492
|
-
// Prefer a same-source match; among those, the closest record position.
|
|
493
|
-
const candidates = oldPool.filter((o) => !o.used && sig(o.e) === sig(ne));
|
|
494
|
-
let match = null;
|
|
495
|
-
if (candidates.length) {
|
|
496
|
-
candidates.sort((a, b) => Math.abs((a.e.recIn ?? 0) - (ne.recIn ?? 0)) - Math.abs((b.e.recIn ?? 0) - (ne.recIn ?? 0)));
|
|
497
|
-
match = candidates[0];
|
|
498
|
-
}
|
|
499
|
-
if (!match) {
|
|
500
|
-
changes.push({ kind: 'new', source: ne.source, track: ne.track, newRecIn: ne.recIn });
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
match.used = true;
|
|
504
|
-
const oe = match.e;
|
|
605
|
+
for (const { oe, ne } of P.pairs) {
|
|
505
606
|
const deltas = {};
|
|
506
607
|
let kind = 'unchanged';
|
|
507
|
-
if (Math.abs((oe.speed ?? 100) - (ne.speed ?? 100)) > 0.01 || oe.reverse !== ne.reverse) {
|
|
608
|
+
if (Math.abs((oe.speed ?? 100) - (ne.speed ?? 100)) > 0.01 || Boolean(oe.reverse) !== Boolean(ne.reverse)) {
|
|
508
609
|
kind = 'retimed';
|
|
509
610
|
deltas.speed = { old: oe.speed, new: ne.speed };
|
|
510
|
-
if (oe.reverse !== ne.reverse) deltas.reverse = { old: oe.reverse, new: ne.reverse };
|
|
611
|
+
if (Boolean(oe.reverse) !== Boolean(ne.reverse)) deltas.reverse = { old: oe.reverse, new: ne.reverse };
|
|
511
612
|
} else if (Math.abs((oe.recIn ?? 0) - (ne.recIn ?? 0)) > recTol) {
|
|
512
613
|
kind = 'moved';
|
|
513
614
|
deltas.recIn = { old: oe.recIn, new: ne.recIn };
|
|
@@ -517,51 +618,97 @@ export function diffChangelist(oldEvents, newEvents, opts = {}) {
|
|
|
517
618
|
}
|
|
518
619
|
if (kind !== 'unchanged') changes.push({ kind, source: ne.source, track: ne.track, oldRecIn: oe.recIn, newRecIn: ne.recIn, deltas });
|
|
519
620
|
}
|
|
621
|
+
for (const ne of P.unmatchedNew) changes.push({ kind: 'new', source: ne.source, track: ne.track, newRecIn: ne.recIn });
|
|
520
622
|
// Unconsumed old events → gone (unless a 'new' at the same rec position → replaced).
|
|
521
|
-
for (const
|
|
522
|
-
const replacement = changes.find((c) => c.kind === 'new' && c.track ===
|
|
623
|
+
for (const oe of P.unmatchedOld) {
|
|
624
|
+
const replacement = changes.find((c) => c.kind === 'new' && c.track === oe.track && Math.abs((c.newRecIn ?? 0) - (oe.recIn ?? 0)) <= recTol);
|
|
523
625
|
if (replacement) {
|
|
524
626
|
replacement.kind = 'replaced';
|
|
525
|
-
replacement.oldSource =
|
|
526
|
-
replacement.oldRecIn =
|
|
627
|
+
replacement.oldSource = oe.source;
|
|
628
|
+
replacement.oldRecIn = oe.recIn;
|
|
527
629
|
} else {
|
|
528
|
-
changes.push({ kind: 'gone', source:
|
|
630
|
+
changes.push({ kind: 'gone', source: oe.source, track: oe.track, oldRecIn: oe.recIn });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Junction diff: a transition is identified by what it joins (track,
|
|
635
|
+
// outgoing, incoming, fade side) and pairs with the closest junction; only
|
|
636
|
+
// its SHAPE (duration / type / pre-roll) makes a change — a junction that
|
|
637
|
+
// travelled with its clips is the clips' move, already reported.
|
|
638
|
+
const tKey = (t) => `${t.track}|${t.outgoing ?? ''}|${t.incoming}|${t.fade ?? 'x'}`;
|
|
639
|
+
const tPool = P.old.transitions.map((t) => ({ t, used: false }));
|
|
640
|
+
const tDesc = (t) => ({ track: t.track, outgoing: t.outgoing, incoming: t.incoming, fade: t.fade, type: t.type, duration: t.duration });
|
|
641
|
+
for (const nt of P.new.transitions) {
|
|
642
|
+
const cands = tPool.filter((p) => !p.used && tKey(p.t) === tKey(nt));
|
|
643
|
+
if (!cands.length) {
|
|
644
|
+
changes.push({ kind: 'transition_added', ...tDesc(nt), newRecIn: nt.junction, newSpan: [nt.start, nt.end] });
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
cands.sort((a, b) => Math.abs(a.t.junction - nt.junction) - Math.abs(b.t.junction - nt.junction));
|
|
648
|
+
cands[0].used = true;
|
|
649
|
+
const ot = cands[0].t;
|
|
650
|
+
const deltas = {};
|
|
651
|
+
if (ot.duration !== nt.duration) deltas.duration = { old: ot.duration, new: nt.duration };
|
|
652
|
+
if (String(ot.type) !== String(nt.type)) deltas.type = { old: ot.type, new: nt.type };
|
|
653
|
+
if (ot.pre !== nt.pre) deltas.pre = { old: ot.pre, new: nt.pre };
|
|
654
|
+
if (Object.keys(deltas).length) {
|
|
655
|
+
changes.push({
|
|
656
|
+
kind: 'transition_changed', ...tDesc(nt), oldRecIn: ot.junction, newRecIn: nt.junction,
|
|
657
|
+
oldSpan: [ot.start, ot.end], newSpan: [nt.start, nt.end], deltas,
|
|
658
|
+
});
|
|
529
659
|
}
|
|
530
660
|
}
|
|
661
|
+
for (const p of tPool.filter((x) => !x.used)) {
|
|
662
|
+
changes.push({ kind: 'transition_dropped', ...tDesc(p.t), oldRecIn: p.t.junction, oldSpan: [p.t.start, p.t.end] });
|
|
663
|
+
}
|
|
664
|
+
|
|
531
665
|
const counts = {};
|
|
532
666
|
for (const c of changes) counts[c.kind] = (counts[c.kind] || 0) + 1;
|
|
533
667
|
changes.sort((a, b) => (a.newRecIn ?? a.oldRecIn ?? 0) - (b.newRecIn ?? b.oldRecIn ?? 0));
|
|
534
|
-
return {
|
|
668
|
+
return {
|
|
669
|
+
changes, counts, changedCount: changes.length,
|
|
670
|
+
transitions: { old: P.old.transitions.length, new: P.new.transitions.length },
|
|
671
|
+
carriersFolded: { old: P.old.carriers.length, new: P.new.carriers.length },
|
|
672
|
+
gate: 'review',
|
|
673
|
+
};
|
|
535
674
|
}
|
|
536
675
|
|
|
537
676
|
// ── TIMING silent-lie guards ───────────────────────────────────────────
|
|
538
677
|
/**
|
|
539
678
|
* Detect timing lies between an old (locked-cut) and new (conformed) event list.
|
|
679
|
+
* Pairs events the same way the changelist does (closest record position,
|
|
680
|
+
* consumed once) so a source cut twice at two speeds is compared instance to
|
|
681
|
+
* instance — never the second old against the first new.
|
|
540
682
|
* @returns {{flags:Array<{kind, source, detail}>}}
|
|
541
683
|
*/
|
|
542
684
|
export function timingGuards(oldEvents, newEvents) {
|
|
543
685
|
const flags = [];
|
|
544
|
-
const
|
|
545
|
-
for (const
|
|
546
|
-
|
|
547
|
-
|
|
686
|
+
const P = pairEvents(oldEvents, newEvents);
|
|
687
|
+
for (const oe of P.unmatchedOld) {
|
|
688
|
+
// A dropped audio event where its video sibling survives → dropped J/L-cut audio.
|
|
689
|
+
if (isAudioTrack(oe.track) && P.new.cuts.some((x) => !isAudioTrack(x.track) && x.track !== 'MARKER' && x.source === oe.source))
|
|
690
|
+
flags.push({ kind: 'dropped_split_audio', source: oe.source, track: oe.track, detail: `audio event (${oe.track}) gone but video sibling present (J/L-cut lost)` });
|
|
548
691
|
}
|
|
549
|
-
for (const oe of
|
|
550
|
-
const matches = newBySig.get(sig(oe)) || [];
|
|
551
|
-
const ne = matches[0];
|
|
552
|
-
if (!ne) {
|
|
553
|
-
// A dropped audio event where its video sibling survives → dropped J/L-cut audio.
|
|
554
|
-
if (oe.track === 'A' && newEvents.some((x) => x.track !== 'A' && x.source === oe.source))
|
|
555
|
-
flags.push({ kind: 'dropped_split_audio', source: oe.source, detail: 'audio event gone but video sibling present (J/L-cut lost)' });
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
692
|
+
for (const { oe, ne } of P.pairs) {
|
|
558
693
|
// Flattened retime: a speed ramp/change flattened to 100%.
|
|
559
694
|
if ((oe.speed ?? 100) !== 100 && (ne.speed ?? 100) === 100)
|
|
560
|
-
flags.push({ kind: 'flattened_retime', source: oe.source, detail: `speed ${oe.speed}% → 100% (retime lost)` });
|
|
695
|
+
flags.push({ kind: 'flattened_retime', source: oe.source, recIn: oe.recIn, detail: `speed ${oe.speed}% → 100% (retime lost)` });
|
|
561
696
|
// Reverse dropped.
|
|
562
|
-
if (oe.reverse && !ne.reverse) flags.push({ kind: 'reverse_dropped', source: oe.source, detail: 'reversed clip conformed forward' });
|
|
697
|
+
if (oe.reverse && !ne.reverse) flags.push({ kind: 'reverse_dropped', source: oe.source, recIn: oe.recIn, detail: 'reversed clip conformed forward' });
|
|
563
698
|
// Framerate/pulldown slip.
|
|
564
|
-
if (oe.fps && ne.fps && oe.fps !== ne.fps) flags.push({ kind: 'framerate_slip', source: oe.source, detail: `fps ${oe.fps} → ${ne.fps}` });
|
|
699
|
+
if (oe.fps && ne.fps && oe.fps !== ne.fps) flags.push({ kind: 'framerate_slip', source: oe.source, recIn: oe.recIn, detail: `fps ${oe.fps} → ${ne.fps}` });
|
|
700
|
+
}
|
|
701
|
+
// A fade or dissolve the conform lost is a timing lie too: the picture
|
|
702
|
+
// edges stay put while the blend at the junction silently hard-cuts.
|
|
703
|
+
const tKey = (t) => `${t.track}|${t.outgoing ?? ''}|${t.incoming}|${t.fade ?? 'x'}`;
|
|
704
|
+
const newKeys = P.new.transitions.map(tKey);
|
|
705
|
+
for (const ot of P.old.transitions) {
|
|
706
|
+
const i = newKeys.indexOf(tKey(ot));
|
|
707
|
+
if (i >= 0) { newKeys.splice(i, 1); continue; }
|
|
708
|
+
flags.push({
|
|
709
|
+
kind: 'transition_dropped', source: ot.incoming, track: ot.track, recIn: ot.junction,
|
|
710
|
+
detail: ot.fade ? `fade-${ot.fade} (${ot.duration}f) at ${ot.junction} lost` : `${ot.outgoing}→${ot.incoming} dissolve (${ot.duration}f) at ${ot.junction} lost`,
|
|
711
|
+
});
|
|
565
712
|
}
|
|
566
713
|
return { flags, flagged: flags.length > 0 };
|
|
567
714
|
}
|
|
@@ -86,7 +86,7 @@ const markerSchema = z.object({
|
|
|
86
86
|
export const editorialTool = {
|
|
87
87
|
name: 'editorial',
|
|
88
88
|
description:
|
|
89
|
-
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively — incl. Resolve-written FCP7 -1 junction edges + Solid Color generators, and CMX FROM/TO CLIP NAME comments over the generic AX reel — + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events incl. span-explicit transitions, BL fade legs, and freezes as zero-speed events (OTIO FreezeFrame, XMEML timeremap 0, PrProj in==out, AAF 0% motion effects); for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source; the EDL target writes CMX transition pairs incl. BL fades — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base; BL-aware — black legs need no source, fades no black-side handles, and a fade-out tail requirement lands on the picture source), marker_roundtrip (markers with provenance tags), verify_roundtrip (input events vs re-export events -> pass/mismatches + fitted per-source TC offsets + marker compare w/ markersNotInExport honesty flag; FADE-AWARE: BL/Solid-Color legs merge out as blackSegments and fade-window boundary reshapes are excused into fadeReshapedBoundaries instead of failing; RETIME-AWARE: speed/reverse compare pairwise — EXPORT_OTIO carries an authored Sm2TimeMap back as LinearTimeWarp (measured), so a flattened/lost retime fails as drift geometry alone cannot catch; AUDIO-AWARE: declared audio events compare (a video-only export such as EXPORT_EDL flags audioNotInExport instead of failing) (channel legs deduped, mismatches tagged trackType audio) while the mirrored-A1 export of a video-only turnover stays informational; the conform QC loop-closer). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
|
|
89
|
+
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively — incl. Resolve-written FCP7 -1 junction edges + Solid Color generators, and CMX FROM/TO CLIP NAME comments over the generic AX reel — + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events incl. span-explicit transitions, BL fade legs, and freezes as zero-speed events (OTIO FreezeFrame, XMEML timeremap 0, PrProj in==out, AAF 0% motion effects); for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source; the EDL target writes CMX transition pairs incl. BL fades — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/trimmed/replaced/new/gone PLUS the junction diff: transition_added/transition_dropped/transition_changed with fade in/out or dissolve, outgoing/incoming, span and duration/type/pre-roll deltas — zero-length CMX carrier lines and the BL legs that carry fades fold into the junction diff instead of reading as gone/new sources; events pair by closest record position, consumed once, so a source cut twice at two speeds compares instance to instance; + timing flags incl. transition_dropped and dropped_split_audio on any A-track), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base; BL-aware — black legs need no source, fades no black-side handles, and a fade-out tail requirement lands on the picture source), marker_roundtrip (markers with provenance tags), verify_roundtrip (input events vs re-export events -> pass/mismatches + fitted per-source TC offsets + marker compare w/ markersNotInExport honesty flag; FADE-AWARE: BL/Solid-Color legs merge out as blackSegments and fade-window boundary reshapes are excused into fadeReshapedBoundaries instead of failing; RETIME-AWARE: speed/reverse compare pairwise — EXPORT_OTIO carries an authored Sm2TimeMap back as LinearTimeWarp (measured), so a flattened/lost retime fails as drift geometry alone cannot catch; AUDIO-AWARE: declared audio events compare (a video-only export such as EXPORT_EDL flags audioNotInExport instead of failing) (channel legs deduped, mismatches tagged trackType audio) while the mirrored-A1 export of a video-only turnover stays informational; the conform QC loop-closer). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
|
|
90
90
|
async handler({ action, args }) {
|
|
91
91
|
if (action === 'parse_interchange') {
|
|
92
92
|
const p = parseSchema.parse(args);
|
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.164.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()}")
|