davinci-resolve-mcp 2.144.0 → 2.145.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 +26 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/native-drt-authoring.md +1 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/tools/drt.mjs +161 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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.145.0 — whole projects, assembled offline
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`drt.assemble_project`: multi-timeline .drp archives** — two or more
|
|
10
|
+
full assemble specs merge into one project file (reel-per-timeline conform
|
|
11
|
+
packages). Import as a project (`safe_project_import`); pull singles with
|
|
12
|
+
`extract_from_drp`. Live-proven: a two-reel .drp imported with both
|
|
13
|
+
timelines materialized and the second reel rendering its exact content.
|
|
14
|
+
|
|
15
|
+
### The five merge laws (each found by a failed import)
|
|
16
|
+
|
|
17
|
+
1. Template-fixed cluster identities collide — the pool clip, its version
|
|
18
|
+
table, container, sequence and track ids must remap; media pool element
|
|
19
|
+
ids are the deliberate exception (fixed by capture, so identical sources
|
|
20
|
+
dedup and MediaRefs keep pointing at the survivor).
|
|
21
|
+
2. Keyed FieldsBlobs carry uuids as UTF-16 (`ActiveVer`, `SeqRef`) —
|
|
22
|
+
invisible to plaintext remaps; patched through the keyed codec.
|
|
23
|
+
3. `project.xml`'s `<TimelineHandleVec>` is THE timeline registry: a pool
|
|
24
|
+
clip absent from it imports as pool furniture and never materializes.
|
|
25
|
+
4. Folder children live INSIDE `<MediaVec>` — an element appended after its
|
|
26
|
+
close parses fine and is silently invisible.
|
|
27
|
+
5. Media pool tags are `Sm2MpVideoClip`/`Sm2MpAudioClip`, not `Sm2MpMedia`
|
|
28
|
+
— an over-narrow tag set let media ids into the remap and every
|
|
29
|
+
`MediaRef` in the merged reel dangled (whole reel offline).
|
|
30
|
+
|
|
5
31
|
## What's New in v2.144.0 — turnover clip markers ride the items
|
|
6
32
|
|
|
7
33
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.145.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -61,6 +61,7 @@ window. `render.verify_output` covers the container-level checks.
|
|
|
61
61
|
| Compound authoring | `spec.compounds` (multiple compose; nested edits render) | v2.131–2.132 |
|
|
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
|
+
| Multi-timeline projects | `drt.assemble_project` (reel-per-timeline .drp; import as a project) | v2.145 |
|
|
64
65
|
|
|
65
66
|
`assemble_from_interchange` drives the same engine from an EDL / OTIO /
|
|
66
67
|
FCP7-XML / AAF / **.prproj** (Premiere, read offline — no Premiere needed)
|
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.145.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
|
@@ -291,6 +291,167 @@ export const drtTool = {
|
|
|
291
291
|
'Import with timeline.import_timeline_checked (timeline is named after the FILE). Dissolves/cross-fades, forward+reverse retimes, multi-track video, audio events and markers are AUTHORED when geometry allows; everything else drops WITH a reason — see `conform` for the ledger.',
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
|
+
if (action === 'assemble_project') {
|
|
295
|
+
// MULTI-TIMELINE archive (E85): assemble each timeline spec with the
|
|
296
|
+
// full single-timeline engine, then merge the later archives into the
|
|
297
|
+
// first — SeqContainers copied wholesale, MpFolder elements merged with
|
|
298
|
+
// DbId dedup (captured media templates carry FIXED pool DbIds, so the
|
|
299
|
+
// same source assembled twice IS the same element and every clip's
|
|
300
|
+
// MediaRef already points at it), and LocableBlobSet marker blobs
|
|
301
|
+
// carried over. Import the result as a PROJECT
|
|
302
|
+
// (project_manager.import_project / safe_project_import) — the
|
|
303
|
+
// single-timeline .drt import path only takes one timeline per file.
|
|
304
|
+
const p = z.object({
|
|
305
|
+
timelines: z.array(z.object({}).passthrough()).min(2)
|
|
306
|
+
.describe('Two or more assembleTimeline specs (same shape as `assemble` spec); timelineName required and unique per entry'),
|
|
307
|
+
outputPath: z.string().describe('Where the multi-timeline .drp is written'),
|
|
308
|
+
targetAppVersion: z.union([z.string(), z.number()]).optional(),
|
|
309
|
+
}).parse(args);
|
|
310
|
+
const names = p.timelines.map((t, i) => t.timelineName || `Timeline ${i + 1}`);
|
|
311
|
+
if (new Set(names).size !== names.length) throw new Error(`assemble_project: timelineName must be unique per timeline (got: ${names.join(', ')})`);
|
|
312
|
+
const { assembleTimeline } = drp();
|
|
313
|
+
const buffers = [];
|
|
314
|
+
for (const [i, spec] of p.timelines.entries()) {
|
|
315
|
+
const s = { ...spec, timelineName: names[i] };
|
|
316
|
+
if (s.templateVersion === undefined && p.targetAppVersion !== undefined) {
|
|
317
|
+
s.templateVersion = parseFloat(p.targetAppVersion) >= 21 ? 21 : 19;
|
|
318
|
+
}
|
|
319
|
+
buffers.push((await assembleTimeline(s)).buffer);
|
|
320
|
+
}
|
|
321
|
+
// Every assembly reuses the TEMPLATE's fixed identities across the
|
|
322
|
+
// whole TIMELINE CLUSTER — pool clip element (which nests a version
|
|
323
|
+
// table with back-references: MpTimelineClip, pActive, UniqueSequenceId,
|
|
324
|
+
// UniqueMediaPoolItemId, numeric IDs), the parent container, its shared
|
|
325
|
+
// sequence and track ids — so archives 2..n silently collide with
|
|
326
|
+
// archive 1 (measured twice: the merge imported as ONE timeline, first
|
|
327
|
+
// via container overwrite, then via UniqueMediaPoolItemId dedup).
|
|
328
|
+
// Rather than freshening named fields one by one, remap EVERY uuid the
|
|
329
|
+
// later archive's cluster shares with the first archive's cluster —
|
|
330
|
+
// except media-element ids, which are shared BY DESIGN (captured
|
|
331
|
+
// templates carry fixed pool DbIds so identical sources dedup and
|
|
332
|
+
// MediaRefs keep pointing at the survivor). The keyed SeqRef inside the
|
|
333
|
+
// pool clip's embedded sequence blob is patched through the codec.
|
|
334
|
+
const { createRequire } = await import('node:module');
|
|
335
|
+
const requireCjs = createRequire(import.meta.url);
|
|
336
|
+
const { decodeKeyedDict, encodeKeyedDict } = requireCjs('../../vendor/drp-format/keyed-dict.js');
|
|
337
|
+
const { randomUUID } = await import('node:crypto');
|
|
338
|
+
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
|
|
339
|
+
const TL_EL_RE = /<Element>\s*<Sm2MpTimelineClip DbId="[^"]+">[\s\S]*?<\/Sm2MpTimelineClip>\s*<\/Element>/g;
|
|
340
|
+
// Media pool elements: Sm2MpVideoClip/Sm2MpAudioClip (captured
|
|
341
|
+
// template media — the tag is NOT Sm2MpMedia; measured the hard way
|
|
342
|
+
// when an over-narrow pattern left media ids in the remap set and
|
|
343
|
+
// every item MediaRef in the merged reel went dangling → offline)
|
|
344
|
+
// plus Sm2MpCompoundClip clusters.
|
|
345
|
+
const MEDIA_EL_RE = /<Element>\s*<Sm2Mp(?:VideoClip|AudioClip|Media|CompoundClip) DbId="[^"]+">[\s\S]*?<\/Sm2Mp(?:VideoClip|AudioClip|Media|CompoundClip)>\s*<\/Element>/g;
|
|
346
|
+
const base = await JSZip.loadAsync(buffers[0]);
|
|
347
|
+
const mpP = 'MediaPool/Master/MpFolder.xml';
|
|
348
|
+
let mpXml = await base.file(mpP).async('string');
|
|
349
|
+
let pjXml = await base.file('project.xml').async('string');
|
|
350
|
+
const baseIds = new Set((mpXml + pjXml).match(UUID_RE) || []);
|
|
351
|
+
for (const n of Object.keys(base.files)) {
|
|
352
|
+
if (!base.files[n].dir && /SeqContainer\/.+\.xml$/.test(n)) {
|
|
353
|
+
for (const id of (await base.file(n).async('string')).match(UUID_RE) || []) baseIds.add(id);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let nextPoolId = 101; // template pool clips carry <ID>100</ID>
|
|
357
|
+
for (const buf of buffers.slice(1)) {
|
|
358
|
+
const zip = await JSZip.loadAsync(buf);
|
|
359
|
+
let mp2 = await zip.file(mpP).async('string');
|
|
360
|
+
const mediaEls = mp2.match(MEDIA_EL_RE) || [];
|
|
361
|
+
const mediaIds = new Set(mediaEls.flatMap((el) => el.match(UUID_RE) || []));
|
|
362
|
+
const tlEl = (mp2.match(TL_EL_RE) || [])[0];
|
|
363
|
+
if (!tlEl) throw new Error('assemble_project: merged archive has no timeline pool clip');
|
|
364
|
+
// cluster text: the pool clip element + every SeqContainer entry
|
|
365
|
+
const containers = {};
|
|
366
|
+
for (const n of Object.keys(zip.files)) {
|
|
367
|
+
if (!zip.files[n].dir && /SeqContainer\/.+\.xml$/.test(n)) containers[n] = await zip.file(n).async('string');
|
|
368
|
+
}
|
|
369
|
+
const clusterText = tlEl + Object.values(containers).join('');
|
|
370
|
+
const remap = new Map();
|
|
371
|
+
for (const id of new Set(clusterText.match(UUID_RE) || [])) {
|
|
372
|
+
if (baseIds.has(id) && !mediaIds.has(id)) remap.set(id, randomUUID());
|
|
373
|
+
}
|
|
374
|
+
const apply = (text) => { let t = text; for (const [a, b] of remap) t = t.split(a).join(b); return t; };
|
|
375
|
+
// Keyed FieldsBlobs store uuid VALUES as UTF-16 — invisible to the
|
|
376
|
+
// plaintext remap. (Measured the hard way: the pool clip's top
|
|
377
|
+
// FieldsBlob is keyed {ActiveVer→uuid}; left unpatched it pointed
|
|
378
|
+
// REEL_02's active version at REEL_01's cluster and the timeline
|
|
379
|
+
// silently never materialized.) Decode every keyed blob, remap any
|
|
380
|
+
// string entry matching a remapped uuid, re-encode. Non-keyed blobs
|
|
381
|
+
// (zstd bodies etc.) decode-fail and pass through untouched.
|
|
382
|
+
const applyBlobs = (text) => text.replace(/<FieldsBlob>([0-9a-fA-F]+)<\/FieldsBlob>/g, (whole, hex) => {
|
|
383
|
+
try {
|
|
384
|
+
const dict = decodeKeyedDict(Buffer.from(hex, 'hex'));
|
|
385
|
+
let changed = false;
|
|
386
|
+
for (const e of dict.entries) {
|
|
387
|
+
if (typeof e.value === 'string' && remap.has(e.value)) { e.value = remap.get(e.value); changed = true; }
|
|
388
|
+
}
|
|
389
|
+
if (!changed) return whole;
|
|
390
|
+
return `<FieldsBlob>${encodeKeyedDict({ hdr: dict.hdr, entries: dict.entries }).toString('hex')}</FieldsBlob>`;
|
|
391
|
+
} catch { return whole; }
|
|
392
|
+
});
|
|
393
|
+
// pool clip: uuid remap + keyed SeqRef patch + fresh numeric IDs
|
|
394
|
+
let tlNew = applyBlobs(apply(tlEl)).replace(/<ID>\d+<\/ID>/g, `<ID>${nextPoolId}<\/ID>`.replace('<\/ID>', '</ID>'));
|
|
395
|
+
nextPoolId += 1;
|
|
396
|
+
const fbM = tlNew.match(/(<Sm2Sequence DbId="[^"]+">\s*<FieldsBlob>)([0-9a-fA-F]+)(<\/FieldsBlob>)/);
|
|
397
|
+
if (fbM) {
|
|
398
|
+
const dict = decodeKeyedDict(Buffer.from(fbM[2], 'hex'));
|
|
399
|
+
const seqRef = dict.entries.find((e) => e.key === 'SeqRef');
|
|
400
|
+
if (seqRef && remap.has(String(seqRef.value))) {
|
|
401
|
+
seqRef.value = remap.get(String(seqRef.value));
|
|
402
|
+
tlNew = tlNew.replace(fbM[0], `${fbM[1]}${encodeKeyedDict({ hdr: dict.hdr, entries: dict.entries }).toString('hex')}${fbM[3]}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const [n, xml] of Object.entries(containers)) {
|
|
406
|
+
const remapped = applyBlobs(apply(xml));
|
|
407
|
+
const cid = (remapped.match(/<Sm2SequenceContainer DbId="([^"]+)"/) || [])[1];
|
|
408
|
+
base.file(cid ? `SeqContainer/${cid}.xml` : n, remapped);
|
|
409
|
+
}
|
|
410
|
+
// Children live INSIDE <MediaVec> — an element appended after its
|
|
411
|
+
// close parses fine and is silently invisible (measured: the whole
|
|
412
|
+
// REEL_02 subtree vanished while every id chain checked out).
|
|
413
|
+
mpXml = mpXml.replace('</MediaVec>', `${tlNew}\n</MediaVec>`);
|
|
414
|
+
for (const el of mediaEls) {
|
|
415
|
+
const id = el.match(/DbId="([^"]+)"/)[1];
|
|
416
|
+
if (!mpXml.includes(`DbId="${id}"`)) mpXml = mpXml.replace('</MediaVec>', `${el}\n</MediaVec>`);
|
|
417
|
+
}
|
|
418
|
+
const pj2 = await zip.file('project.xml').async('string');
|
|
419
|
+
for (const blob of pj2.match(/<Element>\s*<Sm2(?:Sequence|TiItem)LockableBlob DbId="[^"]+">[\s\S]*?<\/Sm2(?:Sequence|TiItem)LockableBlob>\s*<\/Element>/g) || []) {
|
|
420
|
+
pjXml = pjXml.replace('</LocableBlobSet>', `${apply(blob)}</LocableBlobSet>`);
|
|
421
|
+
}
|
|
422
|
+
// THE timeline registry (found the hard way — two invisible merges):
|
|
423
|
+
// project.xml's <TimelineHandleVec> lists each pool clip's embedded
|
|
424
|
+
// Sm2Timeline DbId; a pool clip absent from the vec imports as pool
|
|
425
|
+
// furniture but never materializes as a timeline. Carry the later
|
|
426
|
+
// archive's handles over, remapped with its cluster.
|
|
427
|
+
const vec2 = pj2.match(/<TimelineHandleVec>([\s\S]*?)<\/TimelineHandleVec>/);
|
|
428
|
+
if (vec2) {
|
|
429
|
+
for (const h of vec2[1].match(/<Element>[^<]*<\/Element>/g) || []) {
|
|
430
|
+
pjXml = pjXml.replace('</TimelineHandleVec>', ` ${apply(h)}\n </TimelineHandleVec>`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// future merges must not collide with THIS timeline's fresh ids either
|
|
434
|
+
for (const id of remap.values()) baseIds.add(id);
|
|
435
|
+
for (const id of clusterText.match(UUID_RE) || []) if (!remap.has(id)) baseIds.add(id);
|
|
436
|
+
}
|
|
437
|
+
base.file(mpP, mpXml);
|
|
438
|
+
base.file('project.xml', pjXml);
|
|
439
|
+
let outBuf = await base.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
440
|
+
let stamped = null;
|
|
441
|
+
if (p.targetAppVersion !== undefined) {
|
|
442
|
+
const targetPV = resolveTargetProjectVersion({ targetAppVersion: p.targetAppVersion });
|
|
443
|
+
const appVer = `${p.targetAppVersion}${'.0'.repeat(Math.max(0, 4 - String(p.targetAppVersion).split('.').length))}`;
|
|
444
|
+
const zip = await JSZip.loadAsync(outBuf);
|
|
445
|
+
const { out, elementPatches, stampPatches } = await applyVersionStamps(zip, targetPV, appVer);
|
|
446
|
+
outBuf = await out.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
447
|
+
stamped = { targetProjectVersion: targetPV, elementPatches, stampPatches };
|
|
448
|
+
}
|
|
449
|
+
await fs.writeFile(p.outputPath, outBuf);
|
|
450
|
+
return {
|
|
451
|
+
outputPath: p.outputPath, bytes: outBuf.length, timelines: names, stamped,
|
|
452
|
+
note: 'Import as a PROJECT (project_manager.safe_project_import); timeline.import_timeline_checked takes one timeline per file — use drt.extract_from_drp to pull singles.',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
294
455
|
if (action === 'assemble') {
|
|
295
456
|
const p = assembleSchema.parse(args);
|
|
296
457
|
// Native-schema authoring: template-spliced real Resolve structures
|
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.145.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()}")
|