davinci-resolve-mcp 2.167.0 → 2.169.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 +59 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +9 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/README.md +2 -1
- package/resolve-advanced/server/author-interchange.mjs +30 -1
- package/resolve-advanced/server/editorial.mjs +30 -5
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/server/tools/editorial.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/__tests__/place-generator.test.js +34 -0
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +2 -1
- package/resolve-advanced/vendor/drp-format/index.js +2 -0
- package/resolve-advanced/vendor/drp-format/place-generator.js +60 -6
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
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.169.0 — E112: round-trip QC is colour-aware
|
|
6
|
+
|
|
7
|
+
`verify_roundtrip` merged every generator leg out of the compare as "black",
|
|
8
|
+
so a fade-to-white that came back black — or a colour matte that lost its
|
|
9
|
+
colour — passed QC on geometry alone.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`verify_roundtrip` compares generator colours.** An input leg carrying a
|
|
14
|
+
fill colour must come back on the same track over its span with the same
|
|
15
|
+
colour (±1/255), else `generator-colour` fails; `generatorColours` reports
|
|
16
|
+
the compare. Black-only turnovers compare nothing and stay silent.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Resolve's own re-export of an authored colour reads back.** The FCP7
|
|
21
|
+
writer emits a Solid Color's colour as the FxPlug parameter `input_1`
|
|
22
|
+
(not Premiere's `fillcolor`); the walker now reads any RGB-valued
|
|
23
|
+
generator parameter. Live loop closed on 19.1.3.7: the E110 fade-to-white
|
|
24
|
+
turnover conformed, imported, rendered, re-exported, and verified
|
|
25
|
+
`pass: true` with both colours (white, and 128/64/191) compared — the
|
|
26
|
+
writer echoing the authored `EffectFiltersBA` is a second witness to the
|
|
27
|
+
blob layout. A black-for-white export fails as `generator-colour`.
|
|
28
|
+
|
|
29
|
+
### Measured (filed in api-limitations)
|
|
30
|
+
|
|
31
|
+
- The XML importer ignores a `Dip to Color Dissolve`'s colour parameter:
|
|
32
|
+
white and red imported as byte-identical default blobs and rendered inert.
|
|
33
|
+
The dip colour stays GUI-only on 19.1.3.7 (E111).
|
|
34
|
+
|
|
35
|
+
## What's New in v2.168.0 — E110: Solid Color has a colour — fade-to-white and colour mattes author
|
|
36
|
+
|
|
37
|
+
The one thing the black-leg machinery could not do was be anything but
|
|
38
|
+
black: the Solid Color generator's colour lives in an `EffectFiltersBA` blob
|
|
39
|
+
nobody had ground truth for, and Resolve 19 exposes the colour only in its
|
|
40
|
+
UI. Resolve's own FCP7 XML importer turned out to be the capture route.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **`drt.assemble` generator elements take `color`** (`{r,g,b[,a]}` as 0..1
|
|
45
|
+
floats or 0..255 ints): `placeGenerator` authors the 55-byte
|
|
46
|
+
`EffectFiltersBA` byte-for-byte as Resolve's writer emits it (header, fixed
|
|
47
|
+
prefix, flag, big-endian uint16 ARGB, pad, a second black record).
|
|
48
|
+
`solidColorEffectBlob` / `decodeSolidColorEffectBlob` are exported.
|
|
49
|
+
- **XMEML generatoritem `fillcolor` carries through** `parse_interchange`
|
|
50
|
+
(Premiere Color Matte and Resolve's own export alike) as `color` on the BL
|
|
51
|
+
leg, and the bridge authors the coloured generator — so a turnover's
|
|
52
|
+
fade-to-white or colour matte conforms instead of turning black.
|
|
53
|
+
|
|
54
|
+
### Measured (filed in api-limitations)
|
|
55
|
+
|
|
56
|
+
- Resolve's FCP7 importer honours `fillcolor`: red and blue generators
|
|
57
|
+
rendered Y81 U90 V240 and Y41 U240 V110 (BT.601 limited-range exact) and
|
|
58
|
+
`EXPORT_FCP_7_XML` writes the colour back. Render-verified end to end from
|
|
59
|
+
an offline-authored `.drt`: a clip → white fade climbs 124 → 154 → 182 →
|
|
60
|
+
209 → 232 → 234 across its 24-frame window, the white plateau reads
|
|
61
|
+
234/128/128, and a custom (128, 64, 191) matte lands at Y100 U174 V147
|
|
62
|
+
against a BT.601 expectation of 99.8 / 174.3 / 147.0.
|
|
63
|
+
|
|
5
64
|
## What's New in v2.167.0 — E109: flat AAF sound slots keep their lanes; AAF audio cross-fades render
|
|
6
65
|
|
|
7
66
|
An Avid turnover carries dialog, music and effects as SEPARATE flat sound
|
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.169.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:**
|
|
15
|
+
**Totals:** 39 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
|
|
@@ -309,6 +309,14 @@ equivalent, blocking full automation.
|
|
|
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
|
|
|
312
|
+
### ImportTimelineFromFile FCP7 XML generatoritem fillcolor (honoured; EXPORT_DRT blob layout)
|
|
313
|
+
|
|
314
|
+
- **Object:** `MediaPool`
|
|
315
|
+
- **Signature:** `(filePath.xml) -> Timeline`
|
|
316
|
+
- **Behavior:** Resolve's FCP7 XML importer HONOURS a generatoritem's `fillcolor` parameter (measured on Studio 19.1.3.7, E110): a Premiere-shaped Color Matte (effectid Color, category Matte) and a Solid Color generatoritem, both with <red>/<green>/<blue>/<alpha> 0..255 values, imported as Solid Color items and rendered Y81 U90 V240 (red) and Y41 U240 V110 (blue) — exact BT.601 limited-range values for a 640x360 timeline. EXPORT_FCP_7_XML writes the fillcolor back (same 0..255 channels). EXPORT_DRT carries the colour as a 55-byte <EffectFiltersBA> on the Sm2TiGenerator: 8-byte header (version 2, length 47), a fixed 20-byte prefix, a flag byte, then big-endian uint16 A R G B (0xffff = full) plus a pad word, then a second, black colour record; only the ARGB words differed between the red and blue captures. The default generator has an EMPTY EffectFiltersBA. The same importer does NOT carry a TRANSITION's parameters: a `Dip to Color Dissolve` transitionitem with a white and with a red `color` parameter imported as `Dip To Color Dissolve` elements whose 37-byte EffectFiltersBA were byte-identical (defaults), and the transition rendered inert (luma flat through its window, the E66 law) — the dip colour stays GUI-only on 19.1.3.7 (measured, E111). The writer emits a Solid Color's colour as the FxPlug parameter `input_1` (effectid FxPlugWrapper:C18E8B62_…), not `fillcolor` — an authored EffectFiltersBA colour came back exactly (white, 128/64/191; E112).
|
|
317
|
+
- **Workaround / current handling:** Author fade-to-white / colour mattes by placing a Solid Color generator with that blob (drp-format placeGenerator `color`, drt.assemble elements[].color) — or carry an XMEML generatoritem fillcolor through editorial.parse_interchange; the bridge authors the coloured leg.
|
|
318
|
+
- **Tags:** xml, import, generator, colour, export, drt
|
|
319
|
+
|
|
312
320
|
### ProjectManager.CreateProject (discards an unsaved current project)
|
|
313
321
|
|
|
314
322
|
- **Object:** `ProjectManager`
|
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.169.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
|
@@ -99,7 +99,8 @@ Each dispatches on an `action`. Highlights:
|
|
|
99
99
|
(fps/codec/colorspace/TC + card gaps), `sync` (TC picture↔sound + drift/MOS), `relink_manifest`,
|
|
100
100
|
`rename_plan` (refuses camera originals) / `reel_normalize`, `turnover_package`, `project_hygiene`.
|
|
101
101
|
- **`editorial`** — editorial integrity: `parse_interchange` (EDL/OTIO/XMEML natively — XMEML audio
|
|
102
|
-
cross-fades
|
|
102
|
+
cross-fades, numbered audio lanes and generatoritem `fillcolor` (fade-to-white / colour
|
|
103
|
+
mattes) included — AAF via pyaaf2,
|
|
103
104
|
**.prproj via gunzip+XML** — pass the file PATH for the binary ones), `list_sequences` (one picker entry
|
|
104
105
|
point across xml/edl/otio/drt/drp/aaf/prproj), `convert_to_interchange` (author OTIO/EDL/DRT that Resolve
|
|
105
106
|
imports, from events or a parsed source — **the .prproj→Resolve conform bridge**, no Premiere needed;
|
|
@@ -324,6 +324,9 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
324
324
|
// only authorable shape.
|
|
325
325
|
if (durationFrames < 0) continue;
|
|
326
326
|
const el = { type: 'generator', generatorName: 'Solid Color', track: vTrackEarly, startFrame: recIn, durationFrames, srcIn: 0 };
|
|
327
|
+
// A coloured leg (XMEML fillcolor — fade-to-white, a colour matte) keeps
|
|
328
|
+
// its colour: the generator's EffectFiltersBA is authored from it (E110).
|
|
329
|
+
if (e.color && typeof e.color === 'object') el.color = { r: e.color.r, g: e.color.g, b: e.color.b, ...(e.color.a != null ? { a: e.color.a } : {}) };
|
|
327
330
|
blackLegs.push(el);
|
|
328
331
|
if (e.transition) {
|
|
329
332
|
let d = Math.max(2, toTl(e.transition.duration || 0, e.fps) || 2);
|
|
@@ -902,7 +905,7 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
902
905
|
if (!v.length) return [];
|
|
903
906
|
const off = anchorOfSide(evts);
|
|
904
907
|
return v
|
|
905
|
-
.map((e) => ({ track: canonTrack(e.track), source: mapSource(canonSource(e.source)), recIn: e.recIn - off, recOut: e.recOut - off, srcIn: e.srcIn ?? 0, speed: e.speed ?? 100, reverse: Boolean(e.reverse) }))
|
|
908
|
+
.map((e) => ({ track: canonTrack(e.track), source: mapSource(canonSource(e.source)), recIn: e.recIn - off, recOut: e.recOut - off, srcIn: e.srcIn ?? 0, speed: e.speed ?? 100, reverse: Boolean(e.reverse), color: e.color && typeof e.color === 'object' ? { r: Number(e.color.r) || 0, g: Number(e.color.g) || 0, b: Number(e.color.b) || 0 } : null }))
|
|
906
909
|
.sort((a, b) => a.track.localeCompare(b.track) || a.recIn - b.recIn);
|
|
907
910
|
};
|
|
908
911
|
// FADES (E94): BL legs on the input side and the Solid Color generators a
|
|
@@ -972,6 +975,30 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
972
975
|
|
|
973
976
|
const a0 = norm(inputEvents);
|
|
974
977
|
const b0 = norm(exportedEvents);
|
|
978
|
+
// GENERATOR COLOUR (E112): a black leg is only "black" by default — an
|
|
979
|
+
// XMEML fillcolor rides on it (fade-to-white, colour mattes; E110), and
|
|
980
|
+
// Resolve's FCP7 writer emits the colour back. Black legs still merge out
|
|
981
|
+
// of the pairwise geometry compare (their extents follow the picture), but
|
|
982
|
+
// every input leg that CARRIES a colour must find an exported leg on the
|
|
983
|
+
// same track overlapping its span with the same colour — a white fade that
|
|
984
|
+
// came back black is a conform error geometry alone cannot see.
|
|
985
|
+
const generatorColours = { compared: 0, mismatches: [] };
|
|
986
|
+
const colourKey = (c) => (c ? [c.r, c.g, c.b].map((v) => Math.round(v * 255)).join(',') : '0,0,0');
|
|
987
|
+
const colourTol = 1 / 255 + 1e-9;
|
|
988
|
+
const sameColour = (x, y) => ['r', 'g', 'b'].every((k) => Math.abs((x ? x[k] : 0) - (y ? y[k] : 0)) <= colourTol);
|
|
989
|
+
{
|
|
990
|
+
const inLegs = a0.filter((e) => isBlackSeg(e) && e.color);
|
|
991
|
+
const exLegs = b0.filter((e) => isBlackSeg(e));
|
|
992
|
+
for (const leg of inLegs) {
|
|
993
|
+
const partner = exLegs.find((x) => x.track === leg.track && x.recIn < leg.recOut && x.recOut > leg.recIn);
|
|
994
|
+
generatorColours.compared += 1;
|
|
995
|
+
if (!partner) {
|
|
996
|
+
generatorColours.mismatches.push({ kind: 'generator-colour', track: leg.track, record: [leg.recIn, leg.recOut], input: colourKey(leg.color), exported: null });
|
|
997
|
+
} else if (!sameColour(leg.color, partner.color)) {
|
|
998
|
+
generatorColours.mismatches.push({ kind: 'generator-colour', track: leg.track, record: [leg.recIn, leg.recOut], input: colourKey(leg.color), exported: colourKey(partner.color) });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
975
1002
|
const a = a0.filter((e) => !isBlackSeg(e));
|
|
976
1003
|
const b = b0.filter((e) => !isBlackSeg(e));
|
|
977
1004
|
const blackSegments = { input: a0.length - a.length, exported: b0.length - b.length };
|
|
@@ -1039,8 +1066,10 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
1039
1066
|
}
|
|
1040
1067
|
mismatches.push(...markers.mismatches);
|
|
1041
1068
|
}
|
|
1069
|
+
mismatches.push(...generatorColours.mismatches);
|
|
1042
1070
|
return {
|
|
1043
1071
|
pass: mismatches.length === 0, pairs: n, srcOffsets, mismatches, markers,
|
|
1072
|
+
...(generatorColours.compared ? { generatorColours } : {}),
|
|
1044
1073
|
...(markersNotInExport ? { markersNotInExport } : {}),
|
|
1045
1074
|
...(blackSegments.input || blackSegments.exported ? { blackSegments } : {}),
|
|
1046
1075
|
...(fadeReshapedBoundaries.length ? { fadeReshapedBoundaries } : {}),
|
|
@@ -289,6 +289,26 @@ export function parseOTIO(otio, opts = {}) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// ── XMEML (FCP7 XML) — light clipitem parse ────────────────────────────
|
|
292
|
+
/**
|
|
293
|
+
* A generatoritem's `fillcolor` parameter → {r,g,b,a} in 0..1, or null when
|
|
294
|
+
* absent/black (the default Solid Color). FCP7 XML writes 0..255 channels
|
|
295
|
+
* (Premiere Color Matte and Resolve's own export alike).
|
|
296
|
+
*/
|
|
297
|
+
function xmemlFillColor(g) {
|
|
298
|
+
const params = g && g.effect && g.effect.parameter ? (Array.isArray(g.effect.parameter) ? g.effect.parameter : [g.effect.parameter]) : [];
|
|
299
|
+
for (const pm of params) {
|
|
300
|
+
// Premiere writes the matte colour as `fillcolor`; Resolve's OWN writer
|
|
301
|
+
// emits a Solid Color's colour as the FxPlug input parameter `input_1`
|
|
302
|
+
// (measured E112) — any RGB-valued generator parameter is the colour.
|
|
303
|
+
if (!pm.value || typeof pm.value !== 'object' || pm.value.red == null || pm.value.green == null || pm.value.blue == null) continue;
|
|
304
|
+
const ch = (k) => { const v = Number(pm.value[k]); return Number.isFinite(v) ? Math.max(0, Math.min(255, v)) / 255 : 0; };
|
|
305
|
+
const c = { r: ch('red'), g: ch('green'), b: ch('blue'), a: pm.value.alpha != null ? ch('alpha') : 1 };
|
|
306
|
+
if (c.r === 0 && c.g === 0 && c.b === 0) return null; // black = the default leg
|
|
307
|
+
return c;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
292
312
|
export function parseXMEMLEvents(xml, opts = {}) {
|
|
293
313
|
const { XMLParser } = require('fast-xml-parser');
|
|
294
314
|
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' });
|
|
@@ -366,7 +386,7 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
366
386
|
const itemMarkers = mks
|
|
367
387
|
.filter((mk) => Number.isFinite(Number(mk.in)))
|
|
368
388
|
.map((mk) => ({ frame: Number(mk.in) - inF, name: mk.name != null ? String(mk.name) : undefined, note: mk.comment != null ? String(mk.comment) : undefined }));
|
|
369
|
-
events.push(evt({ index: idx++, track, source: name, srcIn: Number.isFinite(inF) ? inF + inAdj : inF, srcOut: Number.isFinite(outF) ? outF + inAdj : outF, recIn: start, recOut: end, speed, reverse, itemMarkers: itemMarkers.length ? itemMarkers : undefined, fps: seqRate }));
|
|
389
|
+
events.push(evt({ index: idx++, track, source: name, srcIn: Number.isFinite(inF) ? inF + inAdj : inF, srcOut: Number.isFinite(outF) ? outF + inAdj : outF, recIn: start, recOut: end, speed, reverse, itemMarkers: itemMarkers.length ? itemMarkers : undefined, color: it.__genColor || undefined, fps: seqRate }));
|
|
370
390
|
}
|
|
371
391
|
}
|
|
372
392
|
};
|
|
@@ -435,12 +455,17 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
435
455
|
const frame = al === 'start-black' || al === 'start' ? s0 : al === 'end-black' || al === 'end' ? e0 : Math.round((s0 + e0) / 2);
|
|
436
456
|
return { frame, start: s0, end: e0 };
|
|
437
457
|
}).filter(Boolean);
|
|
438
|
-
// Solid Color generatoritems are
|
|
439
|
-
// fade's black side)
|
|
458
|
+
// Solid Color / Color Matte generatoritems are GENERATOR legs: black by
|
|
459
|
+
// default (what Resolve writes for a fade's black side), or the
|
|
460
|
+
// `fillcolor` the item declares — Resolve's importer honours it and
|
|
461
|
+
// its writer emits it back (E110, render-measured). They walk as BL
|
|
462
|
+
// events carrying `color` so fades AND fade-to-colour round-trip.
|
|
440
463
|
const gens = t.generatoritem ? (Array.isArray(t.generatoritem) ? t.generatoritem : [t.generatoritem]) : [];
|
|
441
464
|
for (const g of gens) {
|
|
442
|
-
|
|
443
|
-
|
|
465
|
+
const gname = String(g.name || '');
|
|
466
|
+
const eid = String((g.effect && (g.effect.effectid || g.effect.name)) || '');
|
|
467
|
+
if (!(/solid color|black|colou?r matte/i.test(gname) || /^(solid color|color)$/i.test(eid))) continue;
|
|
468
|
+
walk([{ ...g, name: 'BL', filter: undefined, marker: undefined, __genColor: xmemlFillColor(g) }], label, false);
|
|
444
469
|
}
|
|
445
470
|
clipCursor = 0;
|
|
446
471
|
if (t.clipitem) walk(t.clipitem, label);
|
|
@@ -51,7 +51,7 @@ const assembleSchema = z.object({
|
|
|
51
51
|
spec: z
|
|
52
52
|
.object({})
|
|
53
53
|
.passthrough()
|
|
54
|
-
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),freeze? (true = hold source frame srcIn for the whole cut; video-only; render-proven frozen on 19 via freezedetect),ramp? ([{durationFrames,speed},...] >=2 LINEAR segments from the cut head, srcIn honored; video-only; render-proven cadence on 19 — eased/curved ramps are NOT authorable, an interp!=0 keyframe crashes Resolve on import (measured)),audioOnly?+track? (explicit AUDIO placement on audio track 1-16; presence suppresses the A1 mirror; A1-A8 and the raised A9-A16 ceiling render-verified on 19),markers? (ITEM markers/clip locators: [{frame ITEM-relative, color?, name?, note?, duration?, customData?}] — readback-verified on 19; NOTE Resolve's own EXPORT_DRT drops item markers, this authoring path is the only .drt carrier)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' | 'audio' cross-fade), type? ('dissolve' default | 'wipe' | 'dip' | 'additive' | 'smooth-cut' | 'non-additive' — all render-verified on 19 w/ midpoint fingerprints; XMEML/EDL transition codes route automatically, and NOTE Resolve's own XMEML importer writes transitions that render INERT, so this route beats it)}], markers?: [{frame (timeline-absolute), color? (16 names), name?, note?, duration?, customData?}] (readback-verified on 19), compounds?: [{name, startFrame (parent, absolute), durationFrames, track?, cuts:[{mediaFilePath, startFrame (INNER, 0-based), durationFrames, srcIn?}], compounds?: [same, nested — frames inner-relative]}] (multiple PARALLEL compounds compose AND compounds NEST recursively — depth-2 through depth-4 playback render-verified on 19 (the old depth-2 black was a missing SequenceSetup key, fixed); inner cuts need captured templates w/ native clips), subtitles?: [{startFrame (timeline-absolute), durationFrames, text}] + subtitlesSrt? (raw SRT, cues anchor at the origin; readback-verified on 19; angle-bracket runs read as SRT markup), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}] }. startFrame is timeline-absolute (origin 86400)."),
|
|
54
|
+
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),freeze? (true = hold source frame srcIn for the whole cut; video-only; render-proven frozen on 19 via freezedetect),ramp? ([{durationFrames,speed},...] >=2 LINEAR segments from the cut head, srcIn honored; video-only; render-proven cadence on 19 — eased/curved ramps are NOT authorable, an interp!=0 keyframe crashes Resolve on import (measured)),audioOnly?+track? (explicit AUDIO placement on audio track 1-16; presence suppresses the A1 mirror; A1-A8 and the raised A9-A16 ceiling render-verified on 19),markers? (ITEM markers/clip locators: [{frame ITEM-relative, color?, name?, note?, duration?, customData?}] — readback-verified on 19; NOTE Resolve's own EXPORT_DRT drops item markers, this authoring path is the only .drt carrier)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' | 'audio' cross-fade), type? ('dissolve' default | 'wipe' | 'dip' | 'additive' | 'smooth-cut' | 'non-additive' — all render-verified on 19 w/ midpoint fingerprints; XMEML/EDL transition codes route automatically, and NOTE Resolve's own XMEML importer writes transitions that render INERT, so this route beats it)}], markers?: [{frame (timeline-absolute), color? (16 names), name?, note?, duration?, customData?}] (readback-verified on 19), compounds?: [{name, startFrame (parent, absolute), durationFrames, track?, cuts:[{mediaFilePath, startFrame (INNER, 0-based), durationFrames, srcIn?}], compounds?: [same, nested — frames inner-relative]}] (multiple PARALLEL compounds compose AND compounds NEST recursively — depth-2 through depth-4 playback render-verified on 19 (the old depth-2 black was a missing SequenceSetup key, fixed); inner cuts need captured templates w/ native clips), subtitles?: [{startFrame (timeline-absolute), durationFrames, text}] + subtitlesSrt? (raw SRT, cues anchor at the origin; readback-verified on 19; angle-bracket runs read as SRT markup), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), color? ({r,g,b[,a]} 0..1 floats or 0..255 ints — a Solid Color FILL; authored as the EffectFiltersBA Resolve's own writer emits, render-verified E110: white 235, BT.601-exact colours), ...}] }. startFrame is timeline-absolute (origin 86400)."),
|
|
55
55
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
56
56
|
targetAppVersion: z
|
|
57
57
|
.union([z.string(), z.number()])
|
|
@@ -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 (paired in record order) + Solid Color generators, XMEML audio-track transitionitems as audio cross-fades on numbered lanes A/A2/…, and CMX FROM/TO CLIP NAME comments over the generic AX reel — + AAF via pyaaf2 (flat sound/picture slots number A/A2/… and V/V2/… in slot order so separate beds keep their lanes; NestedScope layers keep layer numbering) + 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).',
|
|
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 (paired in record order) + Solid Color / Color Matte generators carrying their `fillcolor` (BL legs with `color` → fade-to-white and colour mattes author, E110), XMEML audio-track transitionitems as audio cross-fades on numbered lanes A/A2/…, and CMX FROM/TO CLIP NAME comments over the generic AX reel — + AAF via pyaaf2 (flat sound/picture slots number A/A2/… and V/V2/… in slot order so separate beds keep their lanes; NestedScope layers keep layer numbering) + 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; COLOUR-AWARE: an input generator leg carrying a fillcolor (fade-to-white, colour matte) must come back on the same track over its span with the same colour — Resolve\'s FCP7 writer emits it — else generator-colour fails (generatorColours reports the compare); 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);
|
|
@@ -38,3 +38,37 @@ test('placeGenerator honors generatorName + requires startFrame', async () => {
|
|
|
38
38
|
assert.ok(/<PrettyType>Gray Scale<\/PrettyType>/.test(v1));
|
|
39
39
|
await assert.rejects(() => placeGenerator(buffer, { trackIndex: 1 }), /startFrame/);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
// E110: Solid Color fill colour. Ground truth is Resolve 19.1.3.7's OWN writer:
|
|
43
|
+
// FCP7 generatoritem fillcolor red / blue imported, rendered Y81 U90 V240 and
|
|
44
|
+
// Y41 U240 V110 (BT.601 limited-range exact), and EXPORT_DRT wrote these two
|
|
45
|
+
// 55-byte EffectFiltersBA blobs — only the ARGB words differ.
|
|
46
|
+
const { solidColorEffectBlob, decodeSolidColorEffectBlob } = require('../place-generator');
|
|
47
|
+
const RED_BLOB = '000000020000002f800a2c080a18004a004a2408061a0f0a0d320b01ffffffff000000000000220f0a0d320b00ffff0000000000000000';
|
|
48
|
+
const BLUE_BLOB = '000000020000002f800a2c080a18004a004a2408061a0f0a0d320b01ffff00000000ffff0000220f0a0d320b00ffff0000000000000000';
|
|
49
|
+
|
|
50
|
+
test('solidColorEffectBlob reproduces Resolve-written red and blue byte-for-byte (E110)', () => {
|
|
51
|
+
assert.equal(solidColorEffectBlob({ r: 1, g: 0, b: 0 }), RED_BLOB);
|
|
52
|
+
assert.equal(solidColorEffectBlob({ r: 0, g: 0, b: 255 }), BLUE_BLOB); // 0..255 ints accepted
|
|
53
|
+
assert.deepEqual(decodeSolidColorEffectBlob(RED_BLOB), { a: 1, r: 1, g: 0, b: 0 });
|
|
54
|
+
assert.deepEqual(decodeSolidColorEffectBlob(BLUE_BLOB), { a: 1, r: 0, g: 0, b: 1 });
|
|
55
|
+
const mid = decodeSolidColorEffectBlob(solidColorEffectBlob({ r: 0.5, g: 0.25, b: 0.75 }));
|
|
56
|
+
assert.ok(Math.abs(mid.r - 0.5) < 1e-4 && Math.abs(mid.g - 0.25) < 1e-4 && Math.abs(mid.b - 0.75) < 1e-4);
|
|
57
|
+
// Null controls: not-that-shape decodes to null; out-of-range refuses.
|
|
58
|
+
assert.equal(decodeSolidColorEffectBlob(''), null);
|
|
59
|
+
assert.equal(decodeSolidColorEffectBlob('00' + RED_BLOB), null);
|
|
60
|
+
assert.throws(() => solidColorEffectBlob({ r: 300, g: 0, b: 0 }), /exceeds range|0\.\.255/);
|
|
61
|
+
assert.throws(() => solidColorEffectBlob({ r: -1, g: 0, b: 0 }), />= 0/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('placeGenerator writes the colour blob only when a colour is given (E110)', async () => {
|
|
65
|
+
const { buffer, startFrame } = await createEmptyProject({ timelineName: 'GenColour' });
|
|
66
|
+
const withColour = await placeGenerator(buffer, { startFrame, durationFrames: 48, trackIndex: 2, color: { r: 1, g: 1, b: 1 } });
|
|
67
|
+
const v2 = await genOnTrack(withColour.buffer, 2);
|
|
68
|
+
assert.ok(v2.includes(`<EffectFiltersBA>${solidColorEffectBlob({ r: 1, g: 1, b: 1 })}</EffectFiltersBA>`), 'white blob authored');
|
|
69
|
+
assert.deepEqual(withColour.color, { r: 1, g: 1, b: 1, a: 1 });
|
|
70
|
+
const plain = await placeGenerator(buffer, { startFrame, durationFrames: 48, trackIndex: 2 });
|
|
71
|
+
const v2plain = await genOnTrack(plain.buffer, 2);
|
|
72
|
+
assert.ok(/<EffectFiltersBA\s*\/>|<EffectFiltersBA>\s*<\/EffectFiltersBA>/.test(v2plain), 'default stays the empty blob');
|
|
73
|
+
assert.equal(plain.color, null);
|
|
74
|
+
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* elements?: [
|
|
11
11
|
* { type: 'title', track, startFrame, durationFrames?, text?, font?, style?, size?,
|
|
12
12
|
* color?, vJustify?, hJustify? },
|
|
13
|
-
* { type: 'generator', track, startFrame, durationFrames?, generatorName? },
|
|
13
|
+
* { type: 'generator', track, startFrame, durationFrames?, generatorName?, color? ({r,g,b} 0..1 — Solid Color fill, E110) },
|
|
14
14
|
* ],
|
|
15
15
|
* transitions?: [ { track, atFrame, durationFrames? } ], // need two abutting clips at atFrame
|
|
16
16
|
* }
|
|
@@ -266,6 +266,7 @@ async function assembleTimeline(spec = {}) {
|
|
|
266
266
|
({ buffer } = await placeGenerator(buffer, {
|
|
267
267
|
generatorName: el.generatorName, trackIndex: el.track,
|
|
268
268
|
startFrame: el.startFrame, durationFrames: el.durationFrames, templateVersion,
|
|
269
|
+
color: el.color,
|
|
269
270
|
}));
|
|
270
271
|
} else {
|
|
271
272
|
throw new Error(`assembleTimeline: elements[${i}] unknown type "${el.type}" (title|generator)`);
|
|
@@ -117,6 +117,8 @@ module.exports = {
|
|
|
117
117
|
|
|
118
118
|
// Place a built-in generator (Solid Color, etc.) on a chosen video track.
|
|
119
119
|
placeGenerator: require('./place-generator').placeGenerator,
|
|
120
|
+
solidColorEffectBlob: require('./place-generator').solidColorEffectBlob,
|
|
121
|
+
decodeSolidColorEffectBlob: require('./place-generator').decodeSolidColorEffectBlob,
|
|
120
122
|
|
|
121
123
|
// Insert a cross-dissolve between two abutting clips (the one op the Resolve API can't do).
|
|
122
124
|
placeTransition: require('./place-transition').placeTransition,
|
|
@@ -4,8 +4,18 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Built-in generators are `<Sm2TiGenerator>` with a `<PrettyType>` naming the generator
|
|
6
6
|
* (Solid Color, Gray Scale, …) — small, no Fusion-comp blob (unlike Text+, which is an
|
|
7
|
-
* Sm2TiVideoClip). The default has an empty `<EffectFiltersBA>` (default params/color)
|
|
8
|
-
*
|
|
7
|
+
* Sm2TiVideoClip). The default has an empty `<EffectFiltersBA>` (default params/color).
|
|
8
|
+
*
|
|
9
|
+
* COLOR (E110, ground truth from Resolve's OWN writer, Studio 19.1.3.7): a Solid Color
|
|
10
|
+
* with a non-default color carries a 55-byte `<EffectFiltersBA>`. Resolve's FCP7 XML
|
|
11
|
+
* importer honours a generatoritem `fillcolor` (red rendered Y81 U90 V240, blue
|
|
12
|
+
* Y41 U240 V110 — exact BT.601 limited-range), EXPORT_DRT then wrote:
|
|
13
|
+
* 00000002 0000002f version 2, payload length 47
|
|
14
|
+
* 80 0a 2c 08 0a 18 00 4a 00 4a 24 08 06 1a 0f 0a 0d 32 0b fixed prefix
|
|
15
|
+
* 01 AAAA RRRR GGGG BBBB 0000 flag + big-endian uint16 ARGB (+ pad)
|
|
16
|
+
* 22 0f 0a 0d 32 0b 00 ffff 0000 0000 0000 0000 second colour, black
|
|
17
|
+
* Only the ARGB words differed between the red and blue captures. `opts.color`
|
|
18
|
+
* ({r,g,b} in 0..1, or 0..255 ints) populates that blob; omitted = the empty default.
|
|
9
19
|
*
|
|
10
20
|
* Clone-based: carry the bundled Solid Color template and swap PrettyType/Name/Start/
|
|
11
21
|
* Duration + a fresh DbId. The same Sm2TiGenerator shape serves the other simple built-in
|
|
@@ -50,11 +60,12 @@ function snippetPathFor(templateVersion) {
|
|
|
50
60
|
* @param {number} opts.startFrame - timeline start frame (required; must be >= timeline origin).
|
|
51
61
|
* @param {number} [opts.durationFrames=120]
|
|
52
62
|
* @param {string} [opts.timelineUuid]
|
|
63
|
+
* @param {{r:number,g:number,b:number,a?:number}} [opts.color] - Solid Color fill (0..1 floats, or 0..255 ints).
|
|
53
64
|
* @returns {Promise<{buffer:Buffer, entry:string, timelineUuid:string, trackIndex:number,
|
|
54
|
-
* generatorName:string, videoTrackCount:number, createdTracks:number}>}
|
|
65
|
+
* generatorName:string, videoTrackCount:number, createdTracks:number, color:object|null}>}
|
|
55
66
|
*/
|
|
56
67
|
async function placeGenerator(drpInput, opts = {}) {
|
|
57
|
-
const { generatorName = 'Solid Color', trackIndex = 2, startFrame, durationFrames = 120, timelineUuid, templateVersion } = opts;
|
|
68
|
+
const { generatorName = 'Solid Color', trackIndex = 2, startFrame, durationFrames = 120, timelineUuid, templateVersion, color } = opts;
|
|
58
69
|
if (!Number.isInteger(startFrame)) throw new TypeError('placeGenerator: startFrame (int) is required');
|
|
59
70
|
if (!Number.isInteger(trackIndex) || trackIndex < 1) throw new TypeError('placeGenerator: trackIndex must be a positive integer');
|
|
60
71
|
if (/[<>]/.test(generatorName)) throw new Error('placeGenerator: generatorName must not contain < or >');
|
|
@@ -68,6 +79,14 @@ async function placeGenerator(drpInput, opts = {}) {
|
|
|
68
79
|
gen = gen.replace(/<Name>[\s\S]*?<\/Name>/, `<Name>${escapeXml(generatorName)}</Name>`);
|
|
69
80
|
gen = gen.replace(/<Start>\d+<\/Start>/, `<Start>${startFrame}</Start>`);
|
|
70
81
|
gen = gen.replace(/<Duration>\d+<\/Duration>/, `<Duration>${durationFrames}</Duration>`);
|
|
82
|
+
const colorNorm = color != null ? normalizeColor(color) : null;
|
|
83
|
+
if (colorNorm) {
|
|
84
|
+
const blob = solidColorEffectBlob(colorNorm);
|
|
85
|
+
if (!/<EffectFiltersBA\s*\/>|<EffectFiltersBA>\s*<\/EffectFiltersBA>/.test(gen)) {
|
|
86
|
+
throw new Error('placeGenerator: template carries no empty <EffectFiltersBA/> to populate with the colour');
|
|
87
|
+
}
|
|
88
|
+
gen = gen.replace(/<EffectFiltersBA\s*\/>|<EffectFiltersBA>\s*<\/EffectFiltersBA>/, `<EffectFiltersBA>${blob}</EffectFiltersBA>`);
|
|
89
|
+
}
|
|
71
90
|
|
|
72
91
|
const { match: vtv, tracks } = getTrackVec(seqXml, 'video');
|
|
73
92
|
if (tracks.length === 0) throw new Error('placeGenerator: no existing video track to clone from');
|
|
@@ -97,8 +116,43 @@ async function placeGenerator(drpInput, opts = {}) {
|
|
|
97
116
|
const buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
98
117
|
return {
|
|
99
118
|
buffer, entry, timelineUuid: seqId, trackIndex, generatorName,
|
|
100
|
-
videoTrackCount: tracks.length, createdTracks,
|
|
119
|
+
videoTrackCount: tracks.length, createdTracks, color: colorNorm,
|
|
101
120
|
};
|
|
102
121
|
}
|
|
103
122
|
|
|
104
|
-
|
|
123
|
+
// ── Solid Color effect blob (E110) ─────────────────────────────────────
|
|
124
|
+
const BLOB_PREFIX = '000000020000002f800a2c080a18004a004a2408061a0f0a0d320b01';
|
|
125
|
+
const BLOB_SUFFIX = '0000220f0a0d320b00ffff0000000000000000';
|
|
126
|
+
|
|
127
|
+
/** {r,g,b[,a]} in 0..1 floats or 0..255 ints → 0..1 floats. Refuses NaN/out-of-range. */
|
|
128
|
+
function normalizeColor(c) {
|
|
129
|
+
if (!c || typeof c !== 'object') throw new TypeError('placeGenerator: color must be {r,g,b[,a]}');
|
|
130
|
+
const vals = ['r', 'g', 'b'].map((k) => Number(c[k]));
|
|
131
|
+
if (vals.some((v) => !Number.isFinite(v) || v < 0)) throw new RangeError('placeGenerator: color r/g/b must be finite numbers >= 0');
|
|
132
|
+
const a = c.a == null ? null : Number(c.a);
|
|
133
|
+
if (a != null && (!Number.isFinite(a) || a < 0)) throw new RangeError('placeGenerator: color a must be a finite number >= 0');
|
|
134
|
+
const all = a == null ? vals : [...vals, a];
|
|
135
|
+
const eightBit = all.some((v) => v > 1);
|
|
136
|
+
const norm = (v) => (eightBit ? v / 255 : v);
|
|
137
|
+
const out = { r: norm(vals[0]), g: norm(vals[1]), b: norm(vals[2]), a: a == null ? 1 : norm(a) };
|
|
138
|
+
for (const k of ['r', 'g', 'b', 'a']) if (out[k] > 1) throw new RangeError(`placeGenerator: color ${k} exceeds range (0..1 floats or 0..255 ints)`);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Encode a normalized {r,g,b,a} (0..1) as the 55-byte Solid Color EffectFiltersBA hex. */
|
|
143
|
+
function solidColorEffectBlob(color) {
|
|
144
|
+
const c = normalizeColor(color);
|
|
145
|
+
const w = (v) => Math.round(v * 65535).toString(16).padStart(4, '0');
|
|
146
|
+
return `${BLOB_PREFIX}${w(c.a)}${w(c.r)}${w(c.g)}${w(c.b)}${BLOB_SUFFIX}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Decode a Solid Color EffectFiltersBA hex → {r,g,b,a} in 0..1, or null when it is not that shape. */
|
|
150
|
+
function decodeSolidColorEffectBlob(hex) {
|
|
151
|
+
const h = String(hex || '').toLowerCase();
|
|
152
|
+
if (h.length !== 110 || !h.startsWith(BLOB_PREFIX) || !h.endsWith(BLOB_SUFFIX)) return null;
|
|
153
|
+
const words = h.slice(BLOB_PREFIX.length, BLOB_PREFIX.length + 16);
|
|
154
|
+
const v = (i) => parseInt(words.slice(i * 4, i * 4 + 4), 16) / 65535;
|
|
155
|
+
return { a: v(0), r: v(1), g: v(2), b: v(3) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { placeGenerator, TEMPLATE_PATH, solidColorEffectBlob, decodeSolidColorEffectBlob, normalizeColor };
|
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.169.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
package/src/utils/api_truth.py
CHANGED
|
@@ -2794,6 +2794,47 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2794
2794
|
"submit": "missing",
|
|
2795
2795
|
"mitigation": ["editorial.verify_roundtrip audioNotInExport", "timeline.export_timeline_checked"],
|
|
2796
2796
|
},
|
|
2797
|
+
{
|
|
2798
|
+
"symbol": "ImportTimelineFromFile FCP7 XML generatoritem fillcolor (honoured; EXPORT_DRT blob layout)",
|
|
2799
|
+
"object": "MediaPool",
|
|
2800
|
+
"signature": "(filePath.xml) -> Timeline",
|
|
2801
|
+
"reality": "Resolve's FCP7 XML importer HONOURS a generatoritem's "
|
|
2802
|
+
"`fillcolor` parameter (measured on Studio 19.1.3.7, E110): "
|
|
2803
|
+
"a Premiere-shaped Color Matte (effectid Color, category "
|
|
2804
|
+
"Matte) and a Solid Color generatoritem, both with "
|
|
2805
|
+
"<red>/<green>/<blue>/<alpha> 0..255 values, imported as "
|
|
2806
|
+
"Solid Color items and rendered Y81 U90 V240 (red) and "
|
|
2807
|
+
"Y41 U240 V110 (blue) — exact BT.601 limited-range values "
|
|
2808
|
+
"for a 640x360 timeline. EXPORT_FCP_7_XML writes the "
|
|
2809
|
+
"fillcolor back (same 0..255 channels). EXPORT_DRT carries "
|
|
2810
|
+
"the colour as a 55-byte <EffectFiltersBA> on the "
|
|
2811
|
+
"Sm2TiGenerator: 8-byte header (version 2, length 47), a "
|
|
2812
|
+
"fixed 20-byte prefix, a flag byte, then big-endian uint16 "
|
|
2813
|
+
"A R G B (0xffff = full) plus a pad word, then a second, "
|
|
2814
|
+
"black colour record; only the ARGB words differed between "
|
|
2815
|
+
"the red and blue captures. The default generator has an "
|
|
2816
|
+
"EMPTY EffectFiltersBA. The same importer does NOT carry a "
|
|
2817
|
+
"TRANSITION's parameters: a `Dip to Color Dissolve` "
|
|
2818
|
+
"transitionitem with a white and with a red `color` "
|
|
2819
|
+
"parameter imported as `Dip To Color Dissolve` elements "
|
|
2820
|
+
"whose 37-byte EffectFiltersBA were byte-identical "
|
|
2821
|
+
"(defaults), and the transition rendered inert (luma flat "
|
|
2822
|
+
"through its window, the E66 law) — the dip colour stays "
|
|
2823
|
+
"GUI-only on 19.1.3.7 (measured, E111). The writer emits "
|
|
2824
|
+
"a Solid Color's colour as the FxPlug parameter `input_1` "
|
|
2825
|
+
"(effectid FxPlugWrapper:C18E8B62_…), not `fillcolor` — an "
|
|
2826
|
+
"authored EffectFiltersBA colour came back exactly (white, "
|
|
2827
|
+
"128/64/191; E112).",
|
|
2828
|
+
"recommended": "Author fade-to-white / colour mattes by placing a "
|
|
2829
|
+
"Solid Color generator with that blob "
|
|
2830
|
+
"(drp-format placeGenerator `color`, drt.assemble "
|
|
2831
|
+
"elements[].color) — or carry an XMEML generatoritem "
|
|
2832
|
+
"fillcolor through editorial.parse_interchange; the "
|
|
2833
|
+
"bridge authors the coloured leg.",
|
|
2834
|
+
"tags": ["xml", "import", "generator", "colour", "export", "drt"],
|
|
2835
|
+
"submit": "missing",
|
|
2836
|
+
"mitigation": ["drp-format placeGenerator color", "editorial.parse_interchange fillcolor", "drt.assemble_from_interchange"],
|
|
2837
|
+
},
|
|
2797
2838
|
{
|
|
2798
2839
|
"symbol": "ProjectManager.CreateProject (discards an unsaved current project)",
|
|
2799
2840
|
"object": "ProjectManager",
|