davinci-resolve-mcp 2.145.2 → 2.147.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 +37 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/native-drt-authoring.md +1 -0
- package/docs/reference/api-limitations.md +9 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +32 -1
- package/resolve-advanced/server/tools/drt.mjs +62 -1
- package/resolve-advanced/vendor/drp-format/templates/empty-project-r19.drp +0 -0
- package/resolve-advanced/vendor/drp-format/templates/empty-project.drp +0 -0
- package/resolve-advanced/vendor/drp-format/templates/media-clip-h264.drp +0 -0
- package/resolve-advanced/vendor/drp-format/templates/media-clip-r19.drp +0 -0
- package/resolve-advanced/vendor/drp-format/timeline-markers-blob.js +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +21 -0
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.147.0 — the round-trip QC loop learns markers
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`editorial.verify_roundtrip` is marker-aware**: timeline markers compare
|
|
10
|
+
through the loop (min-anchored frames within tolerance, names when both
|
|
11
|
+
sides carry them). A re-export with NO markers while the turnover has them
|
|
12
|
+
raises the `markersNotInExport` honesty flag without failing the pass — a
|
|
13
|
+
missing exporter capability is not a conform drift.
|
|
14
|
+
|
|
15
|
+
### Measured (new Resolve bug, filed in api-limitations)
|
|
16
|
+
|
|
17
|
+
- **`EXPORT_OTIO` drops timeline markers wholesale**: two markers readable
|
|
18
|
+
through the marker API, zero in the exported .otio — while Resolve's own
|
|
19
|
+
OTIO *importer* reads Marker objects fine. Marker-fidelity checks must go
|
|
20
|
+
through the marker API, never an OTIO re-export.
|
|
21
|
+
|
|
22
|
+
## What's New in v2.146.0 — bins, and the folder registry law
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`assemble_project` `timelines[].folder`** — place reels in named Master
|
|
27
|
+
bins (entries sharing a name share the bin; media stays in Master).
|
|
28
|
+
Live-proven: a Reels bin holding both timeline clips, both timelines
|
|
29
|
+
materialized, and a binned reel rendering its exact content.
|
|
30
|
+
|
|
31
|
+
### Measured (the folder registry law)
|
|
32
|
+
|
|
33
|
+
- **The parent folder's FieldsBlob is the subfolder registry.** Media and
|
|
34
|
+
timeline children are discovered by scan; subfolders are NOT — an
|
|
35
|
+
unregistered bin directory imports as nothing and silently takes its
|
|
36
|
+
clips' timelines with it. The registry's inner format is byte-verified
|
|
37
|
+
against the template harvest (a keyed child-id dict in a protobuf wrapper,
|
|
38
|
+
zstd-framed). Natively created Resolve projects carry an EMPTY folder blob
|
|
39
|
+
when binless — the assembly templates now match that convention
|
|
40
|
+
(render-verified as a no-op).
|
|
41
|
+
|
|
5
42
|
## What's New in v2.145.2 — launcher metadata before dependencies
|
|
6
43
|
|
|
7
44
|
### Fixed
|
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.147.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -62,6 +62,7 @@ window. `render.verify_output` covers the container-level checks.
|
|
|
62
62
|
| Nested compounds | `compounds[].compounds` (depth-2 through depth-4 playback render-verified) | v2.134–2.141 |
|
|
63
63
|
| Fusion titles | `elements: [{type:'title', text}]` — **21-gen hosts only** | v2.108 |
|
|
64
64
|
| Multi-timeline projects | `drt.assemble_project` (reel-per-timeline .drp; import as a project) | v2.145 |
|
|
65
|
+
| Pool bins | `assemble_project` `timelines[].folder` (named Master subfolders, registry-backed) | v2.146 |
|
|
65
66
|
|
|
66
67
|
`assemble_from_interchange` drives the same engine from an EDL / OTIO /
|
|
67
68
|
FCP7-XML / AAF / **.prproj** (Premiere, read offline — no Premiere needed)
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 35 missing capabilities,
|
|
15
|
+
**Totals:** 35 missing capabilities, 48 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
|
|
@@ -679,3 +679,11 @@ values, or automation-hostile modal prompts.
|
|
|
679
679
|
- **Behavior:** Item-level markers (clip locators) are not serialized into the exported .drt at all. They live in the project database as Sm2TiItemLockableBlob rows (same wire codec as timeline markers, BlobOwner = the item's DbId — located by byte search in a live Project.db), and readback via TimelineItem.GetMarkers is fine, but the export omits the blobs even after SaveProject — measured on Studio 19.1.3.7. Asymmetrically, ImportTimelineFromFile ACCEPTS an authored Sm2TiItemLockableBlob and the markers read back perfectly.
|
|
680
680
|
- **Workaround / current handling:** Do not rely on .drt archives to carry clip markers. To deliver item markers in a .drt, author them offline (drt.assemble cuts[].markers writes the accepted blob); to preserve markers from a live timeline, read them via the marker API and re-author.
|
|
681
681
|
- **Tags:** timeline, export, drt, markers, silent-failure
|
|
682
|
+
|
|
683
|
+
### Timeline.Export EXPORT_OTIO (drops timeline markers)
|
|
684
|
+
|
|
685
|
+
- **Object:** `Timeline`
|
|
686
|
+
- **Signature:** `(filePath, EXPORT_OTIO) -> bool`
|
|
687
|
+
- **Behavior:** Timeline markers present and readable through the marker API do not appear in the exported .otio at all — the OTIO Marker schema exists and Resolve's importer reads it, but the exporter writes none (measured on Studio 19.1.3.7: two markers read back at frames 12/72; the export carried zero). Any marker-fidelity QC built on an OTIO re-export silently sees an unmarked timeline.
|
|
688
|
+
- **Workaround / current handling:** Do not use EXPORT_OTIO to carry or verify markers. editorial.verify_roundtrip reports this case as `markersNotInExport` (honesty flag, not a failure); read markers through the marker API for fidelity checks, and author them offline via drt.assemble spec.markers when a .drt must carry them.
|
|
689
|
+
- **Tags:** timeline, export, otio, markers, silent-failure
|
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.147.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
|
@@ -746,5 +746,36 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
|
|
|
746
746
|
mismatches.push({ kind: 'source-frames', at: i, source: x.source, expectedOffset: srcOffsets[x.source], gotOffset: off });
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
|
-
|
|
749
|
+
// MARKERS (E88): compare track-'MARKER' pseudo-events, min-anchored to
|
|
750
|
+
// each side's own VIDEO record origin like everything else. When the
|
|
751
|
+
// re-export carries NO markers at all while the input has them, that is
|
|
752
|
+
// reported as markersNotInExport rather than failed — several export
|
|
753
|
+
// formats drop markers wholesale, and a missing capability is not a
|
|
754
|
+
// conform drift. When both sides carry markers, they compare strictly.
|
|
755
|
+
const mks = (evts, anchor) => evts
|
|
756
|
+
.filter((e) => String(e.track) === 'MARKER' && e.recIn != null)
|
|
757
|
+
.map((e) => ({ frame: e.recIn - anchor, name: e.name || '' }))
|
|
758
|
+
.sort((x, y) => x.frame - y.frame);
|
|
759
|
+
const anchorOf = (evts) => {
|
|
760
|
+
const v = vids(evts);
|
|
761
|
+
return v.length ? Math.min(...v.map((e) => e.recIn)) : 0;
|
|
762
|
+
};
|
|
763
|
+
const am = mks(inputEvents, anchorOf(inputEvents));
|
|
764
|
+
const bm = mks(exportedEvents, anchorOf(exportedEvents));
|
|
765
|
+
const markers = { input: am.length, exported: bm.length, mismatches: [] };
|
|
766
|
+
let markersNotInExport = false;
|
|
767
|
+
if (am.length && !bm.length) {
|
|
768
|
+
markersNotInExport = true;
|
|
769
|
+
} else {
|
|
770
|
+
if (am.length !== bm.length) markers.mismatches.push({ kind: 'marker-count', input: am.length, exported: bm.length });
|
|
771
|
+
for (let i = 0; i < Math.min(am.length, bm.length); i += 1) {
|
|
772
|
+
if (Math.abs(am[i].frame - bm[i].frame) > recTol) {
|
|
773
|
+
markers.mismatches.push({ kind: 'marker-frame', at: i, input: am[i].frame, exported: bm[i].frame });
|
|
774
|
+
} else if (am[i].name && bm[i].name && am[i].name !== bm[i].name) {
|
|
775
|
+
markers.mismatches.push({ kind: 'marker-name', at: i, input: am[i].name, exported: bm[i].name });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
mismatches.push(...markers.mismatches);
|
|
779
|
+
}
|
|
780
|
+
return { pass: mismatches.length === 0, pairs: n, srcOffsets, mismatches, markers, ...(markersNotInExport ? { markersNotInExport } : {}) };
|
|
750
781
|
}
|
|
@@ -303,16 +303,23 @@ export const drtTool = {
|
|
|
303
303
|
// single-timeline .drt import path only takes one timeline per file.
|
|
304
304
|
const p = z.object({
|
|
305
305
|
timelines: z.array(z.object({}).passthrough()).min(2)
|
|
306
|
-
.describe(
|
|
306
|
+
.describe("Two or more assembleTimeline specs (same shape as `assemble` spec); timelineName required and unique per entry. Optional per-entry `folder` places that timeline's pool clip in a named Master subfolder (bins for reel-per-timeline packages; entries sharing a name share the bin)."),
|
|
307
307
|
outputPath: z.string().describe('Where the multi-timeline .drp is written'),
|
|
308
308
|
targetAppVersion: z.union([z.string(), z.number()]).optional(),
|
|
309
309
|
}).parse(args);
|
|
310
310
|
const names = p.timelines.map((t, i) => t.timelineName || `Timeline ${i + 1}`);
|
|
311
311
|
if (new Set(names).size !== names.length) throw new Error(`assemble_project: timelineName must be unique per timeline (got: ${names.join(', ')})`);
|
|
312
|
+
const folders = p.timelines.map((t) => {
|
|
313
|
+
if (t.folder === undefined) return null;
|
|
314
|
+
const f = String(t.folder).trim();
|
|
315
|
+
if (!f || /[\/\\]/.test(f)) throw new Error(`assemble_project: folder must be a plain bin name (no path separators): ${JSON.stringify(t.folder)}`);
|
|
316
|
+
return f;
|
|
317
|
+
});
|
|
312
318
|
const { assembleTimeline } = drp();
|
|
313
319
|
const buffers = [];
|
|
314
320
|
for (const [i, spec] of p.timelines.entries()) {
|
|
315
321
|
const s = { ...spec, timelineName: names[i] };
|
|
322
|
+
delete s.folder;
|
|
316
323
|
if (s.templateVersion === undefined && p.targetAppVersion !== undefined) {
|
|
317
324
|
s.templateVersion = parseFloat(p.targetAppVersion) >= 21 ? 21 : 19;
|
|
318
325
|
}
|
|
@@ -434,6 +441,60 @@ export const drtTool = {
|
|
|
434
441
|
for (const id of remap.values()) baseIds.add(id);
|
|
435
442
|
for (const id of clusterText.match(UUID_RE) || []) if (!remap.has(id)) baseIds.add(id);
|
|
436
443
|
}
|
|
444
|
+
// SUBFOLDERS (E87): a bin is just a directory + its own MpFolder.xml —
|
|
445
|
+
// the directory TREE is the registry (measured: no folder vec exists;
|
|
446
|
+
// children carry <MpFolder> back-refs). Move each foldered timeline's
|
|
447
|
+
// pool clip from Master's MediaVec into its bin's, and repoint the
|
|
448
|
+
// back-ref. Media elements stay in Master (shared by design).
|
|
449
|
+
if (folders.some(Boolean)) {
|
|
450
|
+
const masterFolderId = mpXml.match(/<Sm2MpFolder DbId="([^"]+)"/)[1];
|
|
451
|
+
const poolId = (mpXml.match(/<MediaPool>([^<]+)<\/MediaPool>/) || [])[1] || '';
|
|
452
|
+
const bins = new Map();
|
|
453
|
+
for (const [i, folder] of folders.entries()) {
|
|
454
|
+
if (!folder) continue;
|
|
455
|
+
if (!bins.has(folder)) {
|
|
456
|
+
const binId = randomUUID();
|
|
457
|
+
const bin = { id: binId, entry: `MediaPool/Master/${folder}/MpFolder.xml`, clips: [] };
|
|
458
|
+
bins.set(folder, bin);
|
|
459
|
+
}
|
|
460
|
+
const bin = bins.get(folder);
|
|
461
|
+
const tlRe = new RegExp(`<Element>\\s*<Sm2MpTimelineClip DbId="[^"]+">(?:(?!<\\/Element>\\s*<Element>)[\\s\\S])*?<Name>${names[i].replace(/[.*+?^$()|[\]{}]/g, '\\$&')}<\\/Name>[\\s\\S]*?<\\/Sm2MpTimelineClip>\\s*<\\/Element>`);
|
|
462
|
+
const hit = mpXml.match(tlRe);
|
|
463
|
+
if (!hit) throw new Error(`assemble_project: could not locate pool clip for timeline ${names[i]} to move into folder ${folder}`);
|
|
464
|
+
mpXml = mpXml.replace(hit[0], '');
|
|
465
|
+
bin.clips.push(hit[0].replace(/<MpFolder>[^<]*<\/MpFolder>/, `<MpFolder>${bin.id}</MpFolder>`));
|
|
466
|
+
}
|
|
467
|
+
// Register the bins in Master's FieldsBlob — the parent folder blob
|
|
468
|
+
// is the SUBFOLDER registry (measured: with it blanked, a bin's
|
|
469
|
+
// directory + MpFolder.xml import as NOTHING — its clips and their
|
|
470
|
+
// timelines all vanish; media/timeline children are discovered by
|
|
471
|
+
// scan, subfolders are not). Inner format byte-verified against the
|
|
472
|
+
// template harvest: protobuf{field2: keyedDict{"0": binId, ...},
|
|
473
|
+
// field4: time-varint} in the [u32 2][u32 len][0x81][zstd] wrapper.
|
|
474
|
+
const { zstdRawFrame } = requireCjs('../../vendor/drp-format/timeline-markers-blob.js');
|
|
475
|
+
const binIds = [...bins.values()].map((b) => b.id);
|
|
476
|
+
const childDict = encodeKeyedDict({ hdr: 1, entries: binIds.map((id, i) => ({ key: String(i), type: 0x0a, subType: 0, value: id })) });
|
|
477
|
+
const inner = Buffer.concat([
|
|
478
|
+
Buffer.from([0x12, childDict.length]), childDict,
|
|
479
|
+
Buffer.from([0x20]), Buffer.from('b6cba6a90d', 'hex'),
|
|
480
|
+
]);
|
|
481
|
+
const frame = zstdRawFrame(inner);
|
|
482
|
+
const folderBlob = Buffer.concat([
|
|
483
|
+
Buffer.from([0, 0, 0, 2]),
|
|
484
|
+
(() => { const b = Buffer.alloc(4); b.writeUInt32BE(frame.length + 1, 0); return b; })(),
|
|
485
|
+
Buffer.from([0x81]), frame,
|
|
486
|
+
]).toString('hex');
|
|
487
|
+
mpXml = mpXml.replace(/(<Sm2MpFolder DbId="[^"]+">\s*)<FieldsBlob\/>/, `$1<FieldsBlob>${folderBlob}</FieldsBlob>`);
|
|
488
|
+
for (const [folder, bin] of bins) {
|
|
489
|
+
base.file(bin.entry,
|
|
490
|
+
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
491
|
+
`<Sm2MpFolder DbId="${bin.id}">\n <FieldsBlob/>\n <Name>${folder}</Name>\n` +
|
|
492
|
+
` <MpFolder>${masterFolderId}</MpFolder>\n <UniqueMediaPoolItemId>${randomUUID()}</UniqueMediaPoolItemId>\n` +
|
|
493
|
+
` <MediaVec>\n${bin.clips.join('\n')}\n </MediaVec>\n` +
|
|
494
|
+
` <MediaPool>${poolId}</MediaPool>\n <Folded>false</Folded>\n <ColorTag>FOLDER_COLOR_NONE</ColorTag>\n` +
|
|
495
|
+
` <LockSysId/>\n <DbSavedTime>0</DbSavedTime>\n</Sm2MpFolder>\n`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
437
498
|
base.file(mpP, mpXml);
|
|
438
499
|
base.file('project.xml', pjXml);
|
|
439
500
|
let outBuf = await base.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -156,4 +156,4 @@ function decodeTimelineMarkersBlob(buf) {
|
|
|
156
156
|
return markers;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
module.exports = { encodeTimelineMarkersBlob, decodeTimelineMarkersBlob, MARKER_COLOR_BITS };
|
|
159
|
+
module.exports = { encodeTimelineMarkersBlob, decodeTimelineMarkersBlob, MARKER_COLOR_BITS, zstdRawFrame };
|
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.147.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
|
@@ -2716,6 +2716,27 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2716
2716
|
"submit": "bug",
|
|
2717
2717
|
"mitigation": ["drt.assemble cuts[].markers"],
|
|
2718
2718
|
},
|
|
2719
|
+
{
|
|
2720
|
+
"symbol": "Timeline.Export EXPORT_OTIO (drops timeline markers)",
|
|
2721
|
+
"object": "Timeline",
|
|
2722
|
+
"signature": "(filePath, EXPORT_OTIO) -> bool",
|
|
2723
|
+
"reality": "Timeline markers present and readable through the marker "
|
|
2724
|
+
"API do not appear in the exported .otio at all — the OTIO "
|
|
2725
|
+
"Marker schema exists and Resolve's importer reads it, but "
|
|
2726
|
+
"the exporter writes none (measured on Studio 19.1.3.7: two "
|
|
2727
|
+
"markers read back at frames 12/72; the export carried "
|
|
2728
|
+
"zero). Any marker-fidelity QC built on an OTIO re-export "
|
|
2729
|
+
"silently sees an unmarked timeline.",
|
|
2730
|
+
"recommended": "Do not use EXPORT_OTIO to carry or verify markers. "
|
|
2731
|
+
"editorial.verify_roundtrip reports this case as "
|
|
2732
|
+
"`markersNotInExport` (honesty flag, not a failure); "
|
|
2733
|
+
"read markers through the marker API for fidelity "
|
|
2734
|
+
"checks, and author them offline via drt.assemble "
|
|
2735
|
+
"spec.markers when a .drt must carry them.",
|
|
2736
|
+
"tags": ["timeline", "export", "otio", "markers", "silent-failure"],
|
|
2737
|
+
"submit": "bug",
|
|
2738
|
+
"mitigation": ["editorial.verify_roundtrip markersNotInExport"],
|
|
2739
|
+
},
|
|
2719
2740
|
]
|
|
2720
2741
|
|
|
2721
2742
|
|