davinci-resolve-mcp 2.104.5 → 2.104.6
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 +29 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/editorial.mjs +13 -2
- package/resolve-advanced/server/media-inventory.mjs +13 -2
- package/resolve-advanced/vendor/drp-format/__tests__/framerate-encoding.test.js +20 -3
- package/resolve-advanced/vendor/drp-format/seq-container-builder.js +19 -7
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
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.104.6
|
|
6
|
+
|
|
7
|
+
**A correction to the v2.104.2 StartFrame fix — measured against Resolve
|
|
8
|
+
itself.** SMPTE non-drop timecode counts NOMINAL frames: the fields multiply
|
|
9
|
+
by the integer base (30 for 29.97, 24 for 23.976), not the exact rate.
|
|
10
|
+
Measured live on Studio 19.1.3.7: a 29.97 timeline at 01:00:00:00 reads
|
|
11
|
+
GetStartFrame 108000 = 3600 x 30, and a 23.976 one reads 86400 = 3600 x 24.
|
|
12
|
+
Issue #168's reporter expected round(3600 x 30000/1001) = 107892 — they said
|
|
13
|
+
plainly they had patched defensively without verifying Resolve — and the
|
|
14
|
+
v2.104.2 fix shipped that expectation. Both the original fractional product
|
|
15
|
+
and the rounded 107892 were wrong; the Python converters (which always used
|
|
16
|
+
nominal) and the Node converters now agree.
|
|
17
|
+
|
|
18
|
+
Three Node converters move to nominal-base counting, with drop-frame
|
|
19
|
+
handling (semicolon timecodes) matching the Python formula:
|
|
20
|
+
|
|
21
|
+
- `drt.author`'s SeqContainer StartFrame (01:00:00:00 at 29.97 now writes
|
|
22
|
+
108000; at 23.976, 86400)
|
|
23
|
+
- `editorial.tcToFrames` — the exact-rate product undercounted NTSC
|
|
24
|
+
timecode by 0.1% (108 frames per hour), which touched every EDL/AAF
|
|
25
|
+
source/record conversion at 29.97
|
|
26
|
+
- `media-inventory.tcToFrames` — whose own framesToTc was already nominal,
|
|
27
|
+
so the tc->frames->tc round trip was asymmetric at NTSC rates until now
|
|
28
|
+
|
|
29
|
+
The conform fixtures are integer-rate, which is how the exact-rate
|
|
30
|
+
convention survived: nothing in the suite exercised an NTSC timecode
|
|
31
|
+
conversion end to end. Regression tests now pin the measured nominal values
|
|
32
|
+
and a drop-frame case.
|
|
33
|
+
|
|
5
34
|
## What's New in v2.104.5
|
|
6
35
|
|
|
7
36
|
The recent bug classes, generalized into guards — and the sweeps found the
|
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.104.
|
|
15
|
+
> 本翻译对应 v2.104.6 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.104.
|
|
40
|
+
VERSION = "2.104.6"
|
|
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
|
@@ -23,10 +23,21 @@ const require = createRequire(import.meta.url);
|
|
|
23
23
|
// ── timecode ───────────────────────────────────────────────────────────
|
|
24
24
|
const TC_RE = /^(\d{2}):(\d{2}):(\d{2})[:;](\d{2,3})$/;
|
|
25
25
|
export function tcToFrames(tc, fps) {
|
|
26
|
-
const
|
|
26
|
+
const raw = String(tc).trim();
|
|
27
|
+
const m = TC_RE.exec(raw);
|
|
27
28
|
if (!m || !fps) return null;
|
|
28
29
|
const [, h, mm, s, f] = m.map(Number);
|
|
29
|
-
|
|
30
|
+
// SMPTE timecode counts NOMINAL frames (base 30 for 29.97, 24 for 23.976) —
|
|
31
|
+
// measured against Resolve's own GetStartFrame (Studio 19.1.3.7). The
|
|
32
|
+
// previous exact-rate product undercounted NTSC by 0.1% (108 frames/hour).
|
|
33
|
+
const nominal = Math.round(fps);
|
|
34
|
+
let frames = (h * 3600 + mm * 60 + s) * nominal + f;
|
|
35
|
+
if (raw.includes(';')) {
|
|
36
|
+
const dropped = Math.round(nominal * 0.0666666667);
|
|
37
|
+
const totalMinutes = h * 60 + mm;
|
|
38
|
+
frames -= dropped * (totalMinutes - Math.floor(totalMinutes / 10));
|
|
39
|
+
}
|
|
40
|
+
return frames;
|
|
30
41
|
}
|
|
31
42
|
const isTc = (t) => TC_RE.test(t);
|
|
32
43
|
|
|
@@ -17,10 +17,21 @@ import { probeMedia } from './ffprobe-media.mjs';
|
|
|
17
17
|
/** "HH:MM:SS:FF" (or ';' DF) → integer frames at fps. Returns null if unparseable. */
|
|
18
18
|
export function tcToFrames(tc, fps) {
|
|
19
19
|
if (typeof tc !== 'string' || !fps) return null;
|
|
20
|
-
const
|
|
20
|
+
const raw = tc.trim();
|
|
21
|
+
const m = /^(\d{1,2}):(\d{2}):(\d{2})[:;](\d{2,3})$/.exec(raw);
|
|
21
22
|
if (!m) return null;
|
|
22
23
|
const [, h, mm, s, f] = m.map(Number);
|
|
23
|
-
|
|
24
|
+
// Nominal-base counting, matching framesToTc below and Resolve's own
|
|
25
|
+
// GetStartFrame (measured, Studio 19.1.3.7) — the exact-rate product made
|
|
26
|
+
// the tc->frames->tc round trip asymmetric at NTSC rates.
|
|
27
|
+
const nominal = Math.round(fps);
|
|
28
|
+
let frames = (h * 3600 + mm * 60 + s) * nominal + f;
|
|
29
|
+
if (raw.includes(';')) {
|
|
30
|
+
const dropped = Math.round(nominal * 0.0666666667);
|
|
31
|
+
const totalMinutes = h * 60 + mm;
|
|
32
|
+
frames -= dropped * (totalMinutes - Math.floor(totalMinutes / 10));
|
|
33
|
+
}
|
|
34
|
+
return frames;
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
export function framesToTc(frames, fps) {
|
|
@@ -34,14 +34,31 @@ test('the three formerly-wrong table rates never reappear', () => {
|
|
|
34
34
|
assert.notStrictEqual(encodeFrameRate(59.94), '286b55e253f9ed3f');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
test('StartFrame
|
|
37
|
+
test('StartFrame counts NOMINAL frames and startFrame wins', async () => {
|
|
38
|
+
// Measured against Resolve itself (Studio 19.1.3.7): 01:00:00:00 at 29.97
|
|
39
|
+
// reads GetStartFrame 108000 = 3600 x 30, and 23.976 reads 86400 = 3600 x
|
|
40
|
+
// 24 — SMPTE NDF counts by the integer base, not the exact rate. Both the
|
|
41
|
+
// pre-#168 fractional product and its first rounded fix (107892) were wrong.
|
|
38
42
|
const tl = { name: 'T', videoTracks: [], audioTracks: [] };
|
|
39
|
-
const
|
|
43
|
+
const ntsc30 = await buildSeqContainerFile(tl, {
|
|
40
44
|
frameRate: 30000 / 1001, startTimecode: '01:00:00:00',
|
|
41
45
|
});
|
|
42
|
-
assert.match(
|
|
46
|
+
assert.match(ntsc30, /<StartFrame>108000<\/StartFrame>/);
|
|
47
|
+
const ntsc24 = await buildSeqContainerFile(tl, {
|
|
48
|
+
frameRate: 24000 / 1001, startTimecode: '01:00:00:00',
|
|
49
|
+
});
|
|
50
|
+
assert.match(ntsc24, /<StartFrame>86400<\/StartFrame>/);
|
|
43
51
|
const explicit = await buildSeqContainerFile(tl, {
|
|
44
52
|
frameRate: 30000 / 1001, startTimecode: '01:00:00:00', startFrame: 99999,
|
|
45
53
|
});
|
|
46
54
|
assert.match(explicit, /<StartFrame>99999<\/StartFrame>/);
|
|
47
55
|
});
|
|
56
|
+
|
|
57
|
+
test('drop-frame start timecode subtracts the dropped numbers', async () => {
|
|
58
|
+
// 00:01:00;02 DF at 29.97: minute 1 drops 2 numbers -> 1800 + 2 - 2 = 1800.
|
|
59
|
+
const tl = { name: 'T', videoTracks: [], audioTracks: [] };
|
|
60
|
+
const df = await buildSeqContainerFile(tl, {
|
|
61
|
+
frameRate: 30000 / 1001, startTimecode: '00:01:00;02',
|
|
62
|
+
});
|
|
63
|
+
assert.match(df, /<StartFrame>1800<\/StartFrame>/);
|
|
64
|
+
});
|
|
@@ -363,15 +363,27 @@ function buildMarkersFieldsBlob(markers, frameRate) {
|
|
|
363
363
|
function timecodeToFrames(timecode, fps = 24) {
|
|
364
364
|
if (!timecode || typeof timecode !== 'string') return 0;
|
|
365
365
|
|
|
366
|
-
const
|
|
367
|
-
|
|
366
|
+
const dropFrame = timecode.includes(';');
|
|
367
|
+
const parts = timecode.replace(/;/g, ':').split(':').map(Number);
|
|
368
|
+
if (parts.length !== 4 || parts.some(Number.isNaN)) return 0;
|
|
368
369
|
|
|
369
370
|
const [hh, mm, ss, ff] = parts;
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
371
|
+
// SMPTE timecode counts NOMINAL frames: the fields multiply by the integer
|
|
372
|
+
// base (30 for 29.97, 24 for 23.976), not the exact rate. Measured against
|
|
373
|
+
// Resolve itself (Studio 19.1.3.7): a 29.97 timeline at 01:00:00:00 reads
|
|
374
|
+
// GetStartFrame 108000 = 3600 x 30, and a 23.976 one reads 86400 = 3600 x
|
|
375
|
+
// 24. Both the pre-#168 exact-rate product (107892.107...) and its rounded
|
|
376
|
+
// fix (107892) were wrong; nominal is what Resolve stores.
|
|
377
|
+
const nominal = Math.round(fps);
|
|
378
|
+
let frames = (hh * 3600 + mm * 60 + ss) * nominal + ff;
|
|
379
|
+
if (dropFrame) {
|
|
380
|
+
// Drop-frame skips 2 (or 4 at 59.94) TC numbers per minute except every
|
|
381
|
+
// tenth minute — same formula as the Python _timecode_to_frame_id.
|
|
382
|
+
const dropped = Math.round(nominal * 0.0666666667);
|
|
383
|
+
const totalMinutes = hh * 60 + mm;
|
|
384
|
+
frames -= dropped * (totalMinutes - Math.floor(totalMinutes / 10));
|
|
385
|
+
}
|
|
386
|
+
return frames;
|
|
375
387
|
}
|
|
376
388
|
|
|
377
389
|
// =============================================================================
|
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.104.
|
|
90
|
+
VERSION = "2.104.6"
|
|
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()}")
|