davinci-resolve-mcp 2.213.2 → 2.214.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 +72 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/kernels/color-grade-kernel.md +1 -1
- package/docs/kernels/timeline-conform-interchange-kernel.md +2 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/README.md +6 -2
- package/resolve-advanced/server/drp-timeline-clips.mjs +155 -0
- package/resolve-advanced/server/tools/color_trace.mjs +23 -7
- package/src/granular/common.py +1 -1
- package/src/server.py +67 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,78 @@
|
|
|
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.214.0 — color_trace reads exported .drp files, and the trace is live-validated
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`color_trace plan` takes `sourceDrp` / `targetDrp`.** The v2.213.0 matcher
|
|
10
|
+
read both timelines from `Project.db`, which a Postgres, network or cloud
|
|
11
|
+
library does not have. `ProjectManager.ExportProject` works on any project by
|
|
12
|
+
name without loading it, and the `.drp` it writes carries every field the
|
|
13
|
+
matcher keys on — name, record start, duration, in-point, media path, reel,
|
|
14
|
+
media ref, and the active grade version's body inline — so either side of a
|
|
15
|
+
plan can now be an exported `.drp`. The reader
|
|
16
|
+
(`resolve-advanced/server/drp-timeline-clips.mjs`) resolves a timeline name
|
|
17
|
+
through `MediaPool/**/MpFolder.xml` to its sequence id and reads the
|
|
18
|
+
`SeqContainer/<uuid>.xml` whose tracks reference it; it returns the same row
|
|
19
|
+
shape as the DB reader, active-version-first.
|
|
20
|
+
- **`apply_trace_plan` writes its full report to a file.** A real conform is
|
|
21
|
+
800+ plan rows, and the first live run's per-clip tables (290k characters)
|
|
22
|
+
blew past what a tool response can carry. The full tables now go to
|
|
23
|
+
`report_path` (default `dry-run-report.json` / `apply-report.json` next to
|
|
24
|
+
the plan); the response keeps the summary, an `attention` list (ties,
|
|
25
|
+
partial overlaps, every skip that is not bulk `unmatched` /
|
|
26
|
+
`no-source-grade`) and the first `max_rows` rows (default 40, `verbose:
|
|
27
|
+
true` for everything). `failed` is never truncated.
|
|
28
|
+
|
|
29
|
+
### Live-validated (Resolve Studio 19.1.3.7, Postgres library)
|
|
30
|
+
|
|
31
|
+
- Source: a 659-clip picture-lock turnover with 263 graded clips. Target: an
|
|
32
|
+
878-clip conform of the next version, media on a different volume, reels
|
|
33
|
+
mostly empty, clip names carrying an extension the source names lack.
|
|
34
|
+
- Plan: 613 matched (583 by file name + source-range overlap, 3 by reel, 27 by
|
|
35
|
+
name only), 265 unmatched (reference masters, offline screeners, clips not
|
|
36
|
+
in the source), 266 with a grade to carry. The 12 name-only matches sat
|
|
37
|
+
below the default 0.8 gate and were skipped.
|
|
38
|
+
- Apply: 254 of 254 resolved clips graded, 0 failures, timeline archived as
|
|
39
|
+
`_archived_v01` first, a local version `traced V07 Sizing` added per clip.
|
|
40
|
+
Six sampled clips read back with exactly the node count their `.drx`
|
|
41
|
+
decodes to (9, 9, 8, 9, 5, 1) and the source's tools (Reduce Noise, a film
|
|
42
|
+
LUT, Halation, Glow, HDR wheels, hue curves).
|
|
43
|
+
- Reported for review, not hidden: 70 applied clips span more source range
|
|
44
|
+
than the graded section they matched (lowest overlap 14 percent), 2 applied
|
|
45
|
+
on a tie between identical candidates.
|
|
46
|
+
|
|
47
|
+
### Notes
|
|
48
|
+
|
|
49
|
+
- Reading the Postgres library directly was not attempted: Resolve keeps the
|
|
50
|
+
connection password in plain text in `dblist.conf`, and the `.drp` route
|
|
51
|
+
needs no credential at all. `resolve-advanced/README.md` still says
|
|
52
|
+
`project_read` handles "SQLite or Postgres"; only SQLite is implemented.
|
|
53
|
+
|
|
54
|
+
## What's New in v2.213.3 — the capture docstring says what is restored and what is only reset
|
|
55
|
+
|
|
56
|
+
### Documentation
|
|
57
|
+
|
|
58
|
+
- **`_playhead_frame_render`'s docstring still said the mark range was not
|
|
59
|
+
readable.** Since v2.213.2 it is: `Timeline.GetMarkInOut` is read before the
|
|
60
|
+
capture and put back afterwards, offset into `SetRenderSettings`' absolute
|
|
61
|
+
frame space. The docstring now lists the three things that actually happen
|
|
62
|
+
on the way out — format and codec genuinely restored, the mark range
|
|
63
|
+
restored when one was set (whole timeline as the fallback), and TargetDir
|
|
64
|
+
and CustomName reset because nothing can read them back — and notes that
|
|
65
|
+
`GetRenderSettings` is still absent as of 21.1. It is the first thing a
|
|
66
|
+
caller reads to decide whether `quality="frame"` is safe against their
|
|
67
|
+
render setup. Contributed in #201 by @billcarroll.
|
|
68
|
+
- **#196 confirmed on Windows.** The reporter pulled v2.213.1 and confirms the
|
|
69
|
+
keyframe fix renders on Windows, which closes the one platform question the
|
|
70
|
+
v2.213.1 notes left open.
|
|
71
|
+
|
|
72
|
+
### Validation
|
|
73
|
+
|
|
74
|
+
- Docstring only; no behavior changed and no Resolve run required. Full
|
|
75
|
+
offline suite, drift guards and the advanced Node suite green.
|
|
76
|
+
|
|
5
77
|
## What's New in v2.213.2 — the full transcript on 21.1, and a frame capture that puts the user's mark range back in the right frame space
|
|
6
78
|
|
|
7
79
|
Both contributed by @billcarroll (#199, #200).
|
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.214.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -33,7 +33,7 @@ All actions are exposed through `timeline_item_color`.
|
|
|
33
33
|
| `safe_set_cdl` | Validate and normalize CDL payloads before calling `SetCDL`; supports dry run. |
|
|
34
34
|
| `safe_copy_grade` | Resolve target timeline item IDs before calling `CopyGrades`; supports dry run. |
|
|
35
35
|
| `safe_apply_drx` | Validate DRX file existence and temp-path guard before calling `ApplyGradeFromDRX`. |
|
|
36
|
-
| `apply_trace_plan` | Apply the advanced server's `color_trace` plan to the current timeline: resolve each entry to a live clip by (name, record start, duration), dry-run resolution table, one confirm_token for the batch, timeline archived first, then `ApplyGradeFromDRX` per clip; `version_name` adds a local version per clip so the previous grade survives. Unresolved entries are reported, never guessed. |
|
|
36
|
+
| `apply_trace_plan` | Apply the advanced server's `color_trace` plan to the current timeline: resolve each entry to a live clip by (name, record start, duration), dry-run resolution table, one confirm_token for the batch, timeline archived first, then `ApplyGradeFromDRX` per clip; `version_name` adds a local version per clip so the previous grade survives. Unresolved entries are reported, never guessed; the full per-clip tables go to `report_path`, the response carries the summary, an `attention` list and the first `max_rows` rows. |
|
|
37
37
|
| `safe_export_lut` | Resolve LUT export type aliases and require temp output paths by default. |
|
|
38
38
|
| `grade_version_snapshot` | Read current, local, and remote grade version names. |
|
|
39
39
|
| `grade_version_restore` | Safely load a named local/remote version after verifying it exists. |
|
|
@@ -139,7 +139,8 @@ files and the project DB.
|
|
|
139
139
|
compared clear of its transition windows, and Resolve's own FCP7 export
|
|
140
140
|
ingests with its `-1` edges resolved and its missing ticks tolerated).
|
|
141
141
|
- **`color_trace`** — cross-project clip matching on media identity (pool id /
|
|
142
|
-
file path / reel / file name + source-range overlap; names last)
|
|
142
|
+
file path / reel / file name + source-range overlap; names last), from a
|
|
143
|
+
`Project.db` or an exported `.drp` (the Postgres/network-library route) → a trace
|
|
143
144
|
plan with a lossless `.drx` per graded match and a `plan.json`. The live half
|
|
144
145
|
is `timeline_item_color.apply_trace_plan` (dry-run resolution table → one
|
|
145
146
|
confirm_token → ApplyGradeFromDRX per resolved clip, timeline archived first).
|
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.214.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
|
@@ -86,9 +86,13 @@ Each dispatches on an `action`. Highlights:
|
|
|
86
86
|
timeline (any two projects, from `Project.db`, read-only, no Resolve) on **media identity** —
|
|
87
87
|
pool item id / file path / reel / file name plus source-range overlap, so a stringout cut into
|
|
88
88
|
graded sections, a renamed clip, or relinked media still traces; clip names are the last resort.
|
|
89
|
-
|
|
89
|
+
Either side is a `Project.db` (`…ProjectDb` / `…ProjectName`) **or an exported `.drp`**
|
|
90
|
+
(`sourceDrp` / `targetDrp`) — the `.drp` route is how a Postgres / network / cloud library
|
|
91
|
+
is read, since `ProjectManager.ExportProject` works on any project by name without loading
|
|
92
|
+
it. Emits one lossless `.drx` per graded match plus a `plan.json`; the live server's
|
|
90
93
|
`timeline_item_color.apply_trace_plan` applies it (dry-run resolution table, one
|
|
91
|
-
confirm_token for the batch, timeline archived first).
|
|
94
|
+
confirm_token for the batch, timeline archived first, full per-clip report to a file).
|
|
95
|
+
Live-validated 2026-09-08: 878-clip conform, 254 grades carried, 0 failures.
|
|
92
96
|
- **`project_read` / `project_db`** — read/patch the Resolve project DB (SQLite or Postgres).
|
|
93
97
|
Includes `list_subtitle_styles` / `set_subtitle_style` — caption font family/size/weight/italic
|
|
94
98
|
and normalised position, which the scripting API cannot touch at all. Whole-track (not
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeline clips from an exported .drp — the DB-agnostic twin of
|
|
3
|
+
* project_read.readTimelineClips. A Postgres / cloud / network library has no
|
|
4
|
+
* Project.db to open, but ProjectManager.ExportProject works on ANY project by
|
|
5
|
+
* name without loading it, and the .drp it writes carries every field the
|
|
6
|
+
* color_trace matcher keys on:
|
|
7
|
+
*
|
|
8
|
+
* MediaPool/**\/MpFolder.xml <Sm2Timeline><Name>…</Name><Sequence><Sm2Sequence DbId=SEQ>
|
|
9
|
+
* SeqContainer/<uuid>.xml <Sm2TiTrack><Type>0</Type><Sequence>SEQ</Sequence><Items>
|
|
10
|
+
* <Sm2TiVideoClip DbId=…> Name/Start/Duration/In/MediaRef/
|
|
11
|
+
* MediaFilePath/MediaReelNumber/pLmVerTable(pActive → LmVersion Body)
|
|
12
|
+
*
|
|
13
|
+
* Measured on Resolve 19.1.3.7 exports (DbPrjVer 14): one container per
|
|
14
|
+
* timeline, tracks reference their sequence by id, the grade body sits inline
|
|
15
|
+
* in the clip's version table. Row shape matches readTimelineClips so the
|
|
16
|
+
* matcher cannot tell the two sources apart.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
|
|
24
|
+
const scalar = (xml, tag) => {
|
|
25
|
+
const m = xml.match(new RegExp(`<${tag}>([^<]*)</${tag}>`));
|
|
26
|
+
return m ? m[1].trim() : null;
|
|
27
|
+
};
|
|
28
|
+
const blank = (xml, tag) => new RegExp(`<${tag}/>`).test(xml);
|
|
29
|
+
|
|
30
|
+
/** Walk every `<Sm2Timeline>` in the media-pool folder XMLs → [{name, timelineId, sequenceId}]. */
|
|
31
|
+
export function listDrpTimelines(mpFolderXmls) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const xml of mpFolderXmls) {
|
|
34
|
+
for (const m of xml.matchAll(/<Sm2Timeline DbId="([^"]+)">([\s\S]*?)<\/Sm2Timeline>/g)) {
|
|
35
|
+
const body = m[2];
|
|
36
|
+
const name = scalar(body, 'Name');
|
|
37
|
+
const seq = body.match(/<Sm2Sequence DbId="([^"]+)"/);
|
|
38
|
+
out.push({ name, timelineId: m[1], sequenceId: seq ? seq[1] : null });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Decode `<In>` the way project_read does: plain decimal, or the FIRST field of a
|
|
45
|
+
* pipe-joined composite ('47|0000eaffffffef3f'). Anything else → null, never 0. */
|
|
46
|
+
function decodeIn(v) {
|
|
47
|
+
if (v === null || v === '') return null;
|
|
48
|
+
const head = String(v).split('|', 1)[0];
|
|
49
|
+
return /^-?\d+(\.\d+)?$/.test(head) ? Number(head) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Active grade of one clip element: {gradeVersion, gradeBody} or nulls.
|
|
53
|
+
* pActive names the active version; fall back to the first corrected one. */
|
|
54
|
+
function activeGrade(clipXml) {
|
|
55
|
+
const vt = clipXml.match(/<pLmVerTable>([\s\S]*?)<\/pLmVerTable>/);
|
|
56
|
+
if (!vt) return { gradeVersion: null, gradeBody: null };
|
|
57
|
+
const active = scalar(vt[1], 'pActive');
|
|
58
|
+
const versions = [...vt[1].matchAll(/<ListMgt::LmVersion DbId="([^"]+)">([\s\S]*?)<\/ListMgt::LmVersion>/g)].map((m) => {
|
|
59
|
+
const body = m[2];
|
|
60
|
+
const bodyHex = (body.match(/<Body>([0-9a-fA-F\s]*)<\/Body>/) || [null, ''])[1];
|
|
61
|
+
return {
|
|
62
|
+
id: m[1],
|
|
63
|
+
name: scalar(body, 'Name'),
|
|
64
|
+
corrected: /<HasCorrection>true<\/HasCorrection>/.test(body),
|
|
65
|
+
bodyHex: bodyHex ? bodyHex.replace(/[^0-9a-fA-F]/g, '').toLowerCase() : '',
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
const pick = versions.find((v) => v.id === active && v.corrected && v.bodyHex) || versions.find((v) => v.corrected && v.bodyHex);
|
|
69
|
+
return pick ? { gradeVersion: pick.name, gradeBody: pick.bodyHex } : { gradeVersion: null, gradeBody: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Clips of the tracks that belong to `sequenceId` inside one SeqContainer XML.
|
|
73
|
+
* trackType 0 = video (Sm2TiVideoClip), 1 = audio (Sm2TiAudioClip). */
|
|
74
|
+
export function parseSeqContainerClips(containerXml, sequenceId, { includeGrade = false } = {}) {
|
|
75
|
+
const rows = [];
|
|
76
|
+
for (const t of containerXml.matchAll(/<Sm2TiTrack DbId="([^"]+)">([\s\S]*?)<\/Sm2TiTrack>/g)) {
|
|
77
|
+
const trackXml = t[2];
|
|
78
|
+
if (sequenceId && scalar(trackXml, 'Sequence') !== sequenceId) continue;
|
|
79
|
+
const trackType = Number(scalar(trackXml, 'Type') ?? 0);
|
|
80
|
+
const tag = trackType === 0 ? 'Sm2TiVideoClip' : 'Sm2TiAudioClip';
|
|
81
|
+
for (const c of trackXml.matchAll(new RegExp(`<${tag} DbId="([^"]+)">([\\s\\S]*?)</${tag}>`, 'g'))) {
|
|
82
|
+
const x = c[2];
|
|
83
|
+
const g = includeGrade ? activeGrade(x) : { gradeVersion: null, gradeBody: null };
|
|
84
|
+
rows.push({
|
|
85
|
+
itemId: c[1],
|
|
86
|
+
name: scalar(x, 'Name'),
|
|
87
|
+
trackType,
|
|
88
|
+
start: Number(scalar(x, 'Start')),
|
|
89
|
+
duration: Number(scalar(x, 'Duration')),
|
|
90
|
+
reel: blank(x, 'MediaReelNumber') ? '' : scalar(x, 'MediaReelNumber') || '',
|
|
91
|
+
mediaStart: scalar(x, 'MediaStartTime'),
|
|
92
|
+
sourceIn: decodeIn(scalar(x, 'In')),
|
|
93
|
+
mediaPath: blank(x, 'MediaFilePath') ? '' : scalar(x, 'MediaFilePath') || '',
|
|
94
|
+
poolId: scalar(x, 'MediaRef'), // per-project media ref; equal only inside one project
|
|
95
|
+
trackId: t[1],
|
|
96
|
+
gradeVersion: g.gradeVersion,
|
|
97
|
+
hasGrade: /<HasCorrection>true<\/HasCorrection>/.test(x),
|
|
98
|
+
...(includeGrade ? { gradeBody: g.gradeBody } : {}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
rows.sort((a, b) => a.trackType - b.trackType || a.start - b.start);
|
|
103
|
+
return rows;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Unzip a .drp into a temp dir and return {dir, mpFolderXmls, containers:[{file, xml}]}. */
|
|
107
|
+
export function openDrp(drpPath) {
|
|
108
|
+
if (!fs.existsSync(drpPath)) throw new Error(`.drp not found: ${drpPath}`);
|
|
109
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'drp-clips-'));
|
|
110
|
+
const r = spawnSync('unzip', ['-q', '-o', drpPath, '-d', dir], { encoding: 'utf8' });
|
|
111
|
+
if (r.status !== 0) throw new Error(`unzip failed for ${drpPath}: ${(r.stderr || '').slice(-200)}`);
|
|
112
|
+
const walk = (d, out = []) => {
|
|
113
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
114
|
+
const p = path.join(d, e.name);
|
|
115
|
+
if (e.isDirectory()) walk(p, out);
|
|
116
|
+
else out.push(p);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
120
|
+
const files = walk(dir);
|
|
121
|
+
const read = (p) => fs.readFileSync(p, 'utf8');
|
|
122
|
+
return {
|
|
123
|
+
dir,
|
|
124
|
+
mpFolderXmls: files.filter((p) => /MpFolder\.xml$/.test(p)).map(read),
|
|
125
|
+
containers: files.filter((p) => /(^|\/)SeqContainer\/[^/]+\.xml$/.test(p)).map((p) => ({ file: p, xml: read(p) })),
|
|
126
|
+
close: () => {
|
|
127
|
+
try {
|
|
128
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
} catch {
|
|
130
|
+
/* ignore */
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** readTimelineClips twin for a .drp: clips of the named timeline, video by default. */
|
|
137
|
+
export function readTimelineClipsFromDrp(drpPath, timeline, trackType = 'video', includeGrade = false) {
|
|
138
|
+
const drp = openDrp(drpPath);
|
|
139
|
+
try {
|
|
140
|
+
const timelines = listDrpTimelines(drp.mpFolderXmls);
|
|
141
|
+
const tl = timelines.find((t) => t.name === timeline);
|
|
142
|
+
if (!tl) {
|
|
143
|
+
throw new Error(`timeline "${timeline}" not found in ${drpPath}. Timelines: ${timelines.map((t) => JSON.stringify(t.name)).join(', ') || '(none)'}`);
|
|
144
|
+
}
|
|
145
|
+
if (!tl.sequenceId) throw new Error(`timeline "${timeline}" has no <Sm2Sequence> in its media-pool entry`);
|
|
146
|
+
const holder = drp.containers.find((c) => c.xml.includes(`<Sequence>${tl.sequenceId}</Sequence>`));
|
|
147
|
+
if (!holder) throw new Error(`no SeqContainer references sequence ${tl.sequenceId} for timeline "${timeline}"`);
|
|
148
|
+
let rows = parseSeqContainerClips(holder.xml, tl.sequenceId, { includeGrade });
|
|
149
|
+
if (trackType === 'video') rows = rows.filter((r) => r.trackType === 0);
|
|
150
|
+
else if (trackType === 'audio') rows = rows.filter((r) => r.trackType !== 0);
|
|
151
|
+
return rows;
|
|
152
|
+
} finally {
|
|
153
|
+
drp.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -32,12 +32,28 @@ import crypto from 'node:crypto';
|
|
|
32
32
|
import { z } from 'zod';
|
|
33
33
|
import { resolveDbPath } from '../db-patch.mjs';
|
|
34
34
|
import { readTimelineClips } from './project_read.mjs';
|
|
35
|
+
import { readTimelineClipsFromDrp } from '../drp-timeline-clips.mjs';
|
|
35
36
|
|
|
36
37
|
const side = (name) => ({
|
|
37
38
|
[`${name}ProjectDb`]: z.string().optional(),
|
|
38
39
|
[`${name}ProjectName`]: z.string().optional(),
|
|
40
|
+
[`${name}Drp`]: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe(
|
|
44
|
+
`An exported .drp of the ${name} project (ProjectManager.ExportProject / project_manager safe_project_export). The DB-agnostic route: use it when the project lives in a Postgres / network / cloud library that has no Project.db to open.`,
|
|
45
|
+
),
|
|
39
46
|
[`${name}Timeline`]: z.string(),
|
|
40
47
|
});
|
|
48
|
+
|
|
49
|
+
/** One side of the plan: a .drp when given, else a Project.db (path or name). */
|
|
50
|
+
function sideClips(p, name, includeGrade) {
|
|
51
|
+
const drp = p[`${name}Drp`];
|
|
52
|
+
const timeline = p[`${name}Timeline`];
|
|
53
|
+
if (drp) return { origin: { drp, timeline }, clips: readTimelineClipsFromDrp(drp, timeline, 'video', includeGrade) };
|
|
54
|
+
const db = resolveDbPath({ projectDb: p[`${name}ProjectDb`], projectName: p[`${name}ProjectName`] });
|
|
55
|
+
return { origin: { projectDb: db, timeline }, clips: readTimelineClips(db, timeline, 'video', includeGrade) };
|
|
56
|
+
}
|
|
41
57
|
const planSchema = z.object({
|
|
42
58
|
...side('source'),
|
|
43
59
|
...side('target'),
|
|
@@ -274,14 +290,14 @@ const clipRef = (c) => ({
|
|
|
274
290
|
export const colorTraceTool = {
|
|
275
291
|
name: 'color_trace',
|
|
276
292
|
description:
|
|
277
|
-
'Better ColorTrace — match clips between a SOURCE and TARGET timeline (cross-project, from Project.db
|
|
293
|
+
'Better ColorTrace — match clips between a SOURCE and TARGET timeline (cross-project, from Project.db OR an exported .drp — the .drp route covers Postgres/network/cloud libraries — read-only, no Resolve) on media identity (pool id / file path / reel / file name + source-range overlap), names last → a trace plan with a lossless .drx per graded match and a plan.json for timeline_item_color.apply_trace_plan on the live server. Action: plan.',
|
|
278
294
|
async handler({ action, args }) {
|
|
279
295
|
if (action === 'plan') {
|
|
280
296
|
const p = planSchema.parse(args);
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
const srcClips =
|
|
284
|
-
const tgtClips =
|
|
297
|
+
const src = sideClips(p, 'source', true); // includeGrade
|
|
298
|
+
const tgt = sideClips(p, 'target', false);
|
|
299
|
+
const srcClips = src.clips;
|
|
300
|
+
const tgtClips = tgt.clips;
|
|
285
301
|
if (!srcClips.length) throw new Error(`source timeline "${p.sourceTimeline}" has no video clips`);
|
|
286
302
|
if (!tgtClips.length) throw new Error(`target timeline "${p.targetTimeline}" has no video clips`);
|
|
287
303
|
if (p.emitDir) fs.mkdirSync(p.emitDir, { recursive: true });
|
|
@@ -330,8 +346,8 @@ export const colorTraceTool = {
|
|
|
330
346
|
});
|
|
331
347
|
const matched = matches.filter((m) => m.source).length;
|
|
332
348
|
const result = {
|
|
333
|
-
source: {
|
|
334
|
-
target: {
|
|
349
|
+
source: { ...src.origin, clips: srcClips.length, graded: srcClips.filter((c) => c.gradeBody).length },
|
|
350
|
+
target: { ...tgt.origin, clips: tgtClips.length },
|
|
335
351
|
summary: { matched, unmatched: tgtClips.length - matched, gradesReady, ambiguous, belowThreshold, byMethod },
|
|
336
352
|
emitDir: p.emitDir || null,
|
|
337
353
|
planPath: null,
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.214.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
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.214.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -14866,12 +14866,18 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
|
|
|
14866
14866
|
A single-frame render honours the grade, Fusion and titles, is frame-exact,
|
|
14867
14867
|
runs in well under a second, and needs no GUI panel or foreground window.
|
|
14868
14868
|
|
|
14869
|
-
The cost is that render settings are project-level state
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14869
|
+
The cost is that render settings are project-level state, and there is still
|
|
14870
|
+
no GetRenderSettings to read them back from (absent as of 21.1). Three
|
|
14871
|
+
different things happen on the way out:
|
|
14872
|
+
- Format and codec are readable via GetCurrentRenderFormatAndCodec and are
|
|
14873
|
+
genuinely restored.
|
|
14874
|
+
- The mark range is readable via Timeline.GetMarkInOut, so a range the
|
|
14875
|
+
caller had set is put back (offset into SetRenderSettings' absolute
|
|
14876
|
+
frame space); with no range set it falls back to the whole timeline.
|
|
14877
|
+
- TargetDir and CustomName are readable from nowhere, so they are reset to
|
|
14878
|
+
sane values rather than restored.
|
|
14879
|
+
Callers who need a strictly side-effect-free read should use
|
|
14880
|
+
quality="thumbnail" and accept per-clip granularity.
|
|
14875
14881
|
"""
|
|
14876
14882
|
fmt = str(p.get("format", "jpg")).lower().lstrip(".")
|
|
14877
14883
|
if fmt == "jpeg":
|
|
@@ -27087,6 +27093,33 @@ def _resolve_trace_plan(tl, plan: Dict[str, Any], p: Dict[str, Any]) -> List[Dic
|
|
|
27087
27093
|
return rows
|
|
27088
27094
|
|
|
27089
27095
|
|
|
27096
|
+
_TRACE_MAX_ROWS_DEFAULT = 40
|
|
27097
|
+
|
|
27098
|
+
|
|
27099
|
+
def _trace_report(p: Dict[str, Any], plan: Dict[str, Any], kind: str, payload: Dict[str, Any]) -> Optional[str]:
|
|
27100
|
+
"""Write the full per-clip tables next to the plan (or to report_path) and
|
|
27101
|
+
return the path. A real cut is 800+ rows — far past what a tool response
|
|
27102
|
+
should carry — so the file is the durable artifact and the response stays
|
|
27103
|
+
compact (see max_rows)."""
|
|
27104
|
+
report_path = p.get("report_path")
|
|
27105
|
+
if not report_path:
|
|
27106
|
+
plan_path = p.get("plan_path")
|
|
27107
|
+
base = os.path.dirname(os.path.abspath(plan_path)) if plan_path else tempfile.gettempdir()
|
|
27108
|
+
report_path = os.path.join(base, f"{kind}-report.json")
|
|
27109
|
+
try:
|
|
27110
|
+
with open(report_path, "w", encoding="utf-8") as fh:
|
|
27111
|
+
json.dump({"kind": f"color_trace.{kind}", "written_at": _time.time(),
|
|
27112
|
+
"plan_source": plan.get("source"), **payload}, fh, indent=1, default=str)
|
|
27113
|
+
return report_path
|
|
27114
|
+
except Exception as exc:
|
|
27115
|
+
logger.warning("apply_trace_plan: could not write %s report: %s", kind, exc)
|
|
27116
|
+
return None
|
|
27117
|
+
|
|
27118
|
+
|
|
27119
|
+
def _trace_compact(rows: List[Dict[str, Any]], max_rows: int) -> Dict[str, Any]:
|
|
27120
|
+
return {"count": len(rows), "truncated": len(rows) > max_rows, "rows": rows[:max_rows]}
|
|
27121
|
+
|
|
27122
|
+
|
|
27090
27123
|
def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
27091
27124
|
plan, err = _load_trace_plan(p)
|
|
27092
27125
|
if err:
|
|
@@ -27120,12 +27153,25 @@ def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
27120
27153
|
public = [{k: v for k, v in r.items() if not k.startswith("_")} for r in rows]
|
|
27121
27154
|
grade_mode = p.get("grade_mode", 0)
|
|
27122
27155
|
version_name = p.get("version_name")
|
|
27156
|
+
max_rows = p.get("max_rows", _TRACE_MAX_ROWS_DEFAULT)
|
|
27157
|
+
max_rows = len(public) if p.get("verbose") else max(0, int(max_rows))
|
|
27158
|
+
# Entries that deserve a human look: skipped for a reason other than the
|
|
27159
|
+
# two bulk ones, or applied on a tie / a partial source-range overlap.
|
|
27160
|
+
bulk = {"unmatched", "no-source-grade"}
|
|
27161
|
+
attention = [r for r in public if (r["status"] != "apply" and r["reason"] not in bulk)
|
|
27162
|
+
or (r["status"] == "apply" and r["ambiguous"])]
|
|
27123
27163
|
|
|
27124
27164
|
if p.get("dry_run"):
|
|
27165
|
+
report_path = _trace_report(p, plan, "dry-run", {"summary": summary, "resolution": public})
|
|
27125
27166
|
return _ok(dry_run=True, timeline=live_name, plan_source=plan.get("source"),
|
|
27126
|
-
summary=summary,
|
|
27167
|
+
summary=summary, report_path=report_path,
|
|
27168
|
+
attention=_trace_compact(attention, max_rows),
|
|
27169
|
+
resolution=_trace_compact(public, max_rows),
|
|
27170
|
+
grade_mode=grade_mode, version_name=version_name)
|
|
27127
27171
|
if not to_apply:
|
|
27128
|
-
|
|
27172
|
+
report_path = _trace_report(p, plan, "apply", {"summary": summary, "resolution": public, "applied": [], "failed": []})
|
|
27173
|
+
return _ok(timeline=live_name, applied=[], failed=[], summary=summary, report_path=report_path,
|
|
27174
|
+
attention=_trace_compact(attention, max_rows), resolution=_trace_compact(public, max_rows),
|
|
27129
27175
|
note="nothing to apply — every plan entry was skipped (see skipped_by_reason)")
|
|
27130
27176
|
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
27131
27177
|
preview = {
|
|
@@ -27171,13 +27217,17 @@ def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
27171
27217
|
rec["status"] = "applied" if ok else "failed"
|
|
27172
27218
|
(applied if ok else failed).append(rec)
|
|
27173
27219
|
summary.update({"applied": len(applied), "failed": len(failed)})
|
|
27220
|
+
skipped = [r for r in public if r["status"] != "apply"]
|
|
27221
|
+
report_path = _trace_report(p, plan, "apply", {"summary": summary, "applied": applied, "failed": failed, "skipped": skipped})
|
|
27174
27222
|
return {
|
|
27175
27223
|
"success": not failed,
|
|
27176
27224
|
"timeline": live_name,
|
|
27177
27225
|
"plan_source": plan.get("source"),
|
|
27178
|
-
"
|
|
27179
|
-
"
|
|
27180
|
-
"
|
|
27226
|
+
"report_path": report_path,
|
|
27227
|
+
"applied": _trace_compact(applied, max_rows),
|
|
27228
|
+
"failed": failed, # never truncated: every failure is actionable
|
|
27229
|
+
"skipped": _trace_compact(skipped, max_rows),
|
|
27230
|
+
"attention": _trace_compact(attention, max_rows),
|
|
27181
27231
|
"summary": summary,
|
|
27182
27232
|
"grade_mode": grade_mode,
|
|
27183
27233
|
"version_name": version_name,
|
|
@@ -27305,8 +27355,8 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
27305
27355
|
},
|
|
27306
27356
|
"apply_trace_plan": {
|
|
27307
27357
|
"summary": "Apply a color_trace plan (advanced server) to the CURRENT timeline: one ApplyGradeFromDRX per resolved clip. REPLACES those graphs; gated by confirm_token; timeline archived first.",
|
|
27308
|
-
"params": "plan_path: str (planPath from color_trace plan with emitDir) | plan: dict, dry_run?, min_confidence? (default 0.8), start_tolerance? (frames, default 0), grade_mode? (0=no keyframes, 1=source TC aligned, 2=start aligned), version_name? (AddVersion per clip before applying), require_temp_path? (default True), allow_timeline_mismatch?, confirm_token?",
|
|
27309
|
-
"returns": "{success, timeline,
|
|
27358
|
+
"params": "plan_path: str (planPath from color_trace plan with emitDir) | plan: dict, dry_run?, min_confidence? (default 0.8), start_tolerance? (frames, default 0), grade_mode? (0=no keyframes, 1=source TC aligned, 2=start aligned), version_name? (AddVersion per clip before applying), require_temp_path? (default True), allow_timeline_mismatch?, max_rows? (default 40; verbose=True returns every row), report_path? (default <plan dir>/dry-run-report.json | apply-report.json), confirm_token?",
|
|
27359
|
+
"returns": "{success, timeline, report_path, summary: {plan_matches, would_apply, applied, failed, skipped_by_reason, ambiguous_in_plan}, attention: {count, truncated, rows}, applied: {count, truncated, rows}, failed: [...] (never truncated), skipped: {count, truncated, rows}}; dry_run → {resolution: {count, truncated, rows: [{target, source, method, confidence, status, reason, live}]}}",
|
|
27310
27360
|
"example": (
|
|
27311
27361
|
'# 1. advanced server (no Resolve needed):\n'
|
|
27312
27362
|
'# color_trace(action="plan", {sourceProjectName: "SHOW_v07", sourceTimeline: "REEL_01 v07",\n'
|
|
@@ -28141,13 +28191,15 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
28141
28191
|
safe_apply_drx(path, source?, grade_mode?, require_temp_path?) -> {success}
|
|
28142
28192
|
REPLACES the target graph. Captures a version snapshot first; require_temp_path defaults True.
|
|
28143
28193
|
# example: action_help(name='<action_name>')
|
|
28144
|
-
apply_trace_plan(plan_path|plan, dry_run?, min_confidence?=0.8, start_tolerance?=0, grade_mode?=0, version_name?, require_temp_path?=True, allow_timeline_mismatch?, confirm_token?) -> {success, applied, failed, skipped, summary}
|
|
28194
|
+
apply_trace_plan(plan_path|plan, dry_run?, min_confidence?=0.8, start_tolerance?=0, grade_mode?=0, version_name?, require_temp_path?=True, allow_timeline_mismatch?, max_rows?=40, verbose?, report_path?, confirm_token?) -> {success, applied, failed, skipped, attention, summary, report_path}
|
|
28145
28195
|
The live half of the advanced server's color_trace (a ColorTrace that matches on media
|
|
28146
28196
|
identity, cross-project, from the project DB). Resolves each plan entry to a clip on the
|
|
28147
28197
|
CURRENT timeline by (name, record start, duration), then ApplyGradeFromDRX per clip —
|
|
28148
28198
|
one confirm_token for the batch, timeline archived first. dry_run returns the resolution
|
|
28149
28199
|
table without a token. version_name adds a new local version per clip before applying so
|
|
28150
28200
|
the previous grade stays intact. Never guesses: unresolved entries are reported, not applied.
|
|
28201
|
+
The full per-clip tables go to report_path (default: next to the plan); the response carries
|
|
28202
|
+
the summary, `attention` (ties, partial overlaps, non-bulk skips) and the first max_rows rows.
|
|
28151
28203
|
# example: action_help(name='<action_name>')
|
|
28152
28204
|
safe_export_lut(type?, path, require_temp_path?) -> {success, path, size}
|
|
28153
28205
|
Sandboxed LUT export.
|