davinci-resolve-mcp 4.11.0 → 4.11.1
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 +30 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +3 -3
- package/src/utils/captions.py +10 -4
- package/src/utils/cube_lut.py +21 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
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 v4.11.1 — `.cube` input ranges, `"false"` import flags and the caption character cap
|
|
6
|
+
|
|
7
|
+
Three fixes from @Dev-next-gen (#286, #287, #288), each with a reproduction and a
|
|
8
|
+
regression test.
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
|
|
12
|
+
- **A 3D `.cube` carrying `LUT_3D_INPUT_RANGE` could not be read** (#288).
|
|
13
|
+
`read_cube` knew `DOMAIN_MIN`/`DOMAIN_MAX` but not Resolve's own header form,
|
|
14
|
+
so the line fell through to the data-row branch and the whole file was refused
|
|
15
|
+
as a "non-numeric entry". `read_lut_summary` and `attenuate_lut_file` both
|
|
16
|
+
failed on it. The keyword is now read as one domain for all three channels;
|
|
17
|
+
attenuation still refuses a non-unit range, exactly as it does for
|
|
18
|
+
`DOMAIN_MIN`/`DOMAIN_MAX`.
|
|
19
|
+
- **On landing:** a short or non-numeric `LUT_3D_INPUT_RANGE` escaped as a bare
|
|
20
|
+
`ValueError`, and a short `DOMAIN_MIN`/`DOMAIN_MAX` produced a silently short
|
|
21
|
+
domain list. All three headers now go through one reader that raises
|
|
22
|
+
`CubeLutError` naming the file, line and keyword.
|
|
23
|
+
- **`timeline(action="import_timeline_checked")` read `"false"` as true** (#287).
|
|
24
|
+
`sanitize_media` / `relink_media` and `import_source_clips` were read with bare
|
|
25
|
+
`bool()`, so `sanitize_media="false"` still handed Resolve the sanitized copy
|
|
26
|
+
instead of the file passed, and `import_source_clips="false"` reached
|
|
27
|
+
`ImportTimelineFromFile` as `importSourceClips: True`. Both now go through
|
|
28
|
+
`_coerce_bool`, as the rest of `server.py` has since v4.8.20.
|
|
29
|
+
- **Caption orphan rebalancing could break `max_chars_per_line`** (#286).
|
|
30
|
+
Moving the last word of the penultimate line onto a one-word final line never
|
|
31
|
+
re-checked the cap, so "we had a wonderful conversation" at 20 characters came
|
|
32
|
+
out with a 22-character second line in the SRT/WebVTT. The move now happens
|
|
33
|
+
only when the merged line fits; a lone last word is the lesser fault.
|
|
34
|
+
|
|
5
35
|
## What's New in v4.11.0 — `rename_plan` collides names a case-insensitive volume treats as one
|
|
6
36
|
|
|
7
37
|
### Fixes
|
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
|
-
> 本翻译对应 v4.11.
|
|
15
|
+
> 本翻译对应 v4.11.1 版 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 = "4.11.
|
|
40
|
+
VERSION = "4.11.1"
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.11.
|
|
94
|
+
VERSION = "4.11.1"
|
|
95
95
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
96
96
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
97
97
|
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 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "4.11.
|
|
14
|
+
VERSION = "4.11.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -7699,7 +7699,7 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
7699
7699
|
# abort Resolve's scripting-API import and leave the timeline fully offline.
|
|
7700
7700
|
# The original is only read; the sanitized copy lands in a temp dir, so the
|
|
7701
7701
|
# temp-path guard never applies to it.
|
|
7702
|
-
sanitize_requested =
|
|
7702
|
+
sanitize_requested = _coerce_bool(p.get("sanitize_media")) or _coerce_bool(p.get("relink_media"))
|
|
7703
7703
|
binary_relink_note = None
|
|
7704
7704
|
_binary_has_roots = bool(p.get("relink_search_roots") or p.get("search_roots"))
|
|
7705
7705
|
if is_binary and _binary_has_roots:
|
|
@@ -7730,7 +7730,7 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
7730
7730
|
if p.get("timeline_name") and "timelineName" not in options:
|
|
7731
7731
|
options["timelineName"] = p["timeline_name"]
|
|
7732
7732
|
if "import_source_clips" in p and "importSourceClips" not in options:
|
|
7733
|
-
options["importSourceClips"] =
|
|
7733
|
+
options["importSourceClips"] = _coerce_bool(p["import_source_clips"])
|
|
7734
7734
|
|
|
7735
7735
|
# Optional fuzzy relink: when search roots are given, missing-at-original-path
|
|
7736
7736
|
# clips are matched against on-disk media (exact/ext-agnostic/normalized/reel)
|
package/src/utils/captions.py
CHANGED
|
@@ -83,13 +83,19 @@ def _wrap(text: str, max_chars: int, max_lines: int) -> Optional[List[str]]:
|
|
|
83
83
|
return lines if len(lines) <= max_lines else None
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def _no_orphan(lines: List[str]) -> List[str]:
|
|
87
|
-
"""Rebalance so no line is a lone word when a neighbour could give one up.
|
|
86
|
+
def _no_orphan(lines: List[str], max_chars: int) -> List[str]:
|
|
87
|
+
"""Rebalance so no line is a lone word when a neighbour could give one up.
|
|
88
|
+
|
|
89
|
+
It cannot when the merged line would break the character cap `_wrap` has
|
|
90
|
+
just enforced; an orphan is the lesser of the two faults.
|
|
91
|
+
"""
|
|
88
92
|
if len(lines) < 2:
|
|
89
93
|
return lines
|
|
90
94
|
if len(lines[-1].split()) == 1 and len(lines[-2].split()) > 2:
|
|
91
95
|
head = lines[-2].split()
|
|
92
|
-
|
|
96
|
+
merged = f"{head[-1]} {lines[-1]}"
|
|
97
|
+
if len(merged) <= max_chars:
|
|
98
|
+
lines = lines[:-2] + [" ".join(head[:-1]), merged]
|
|
93
99
|
return lines
|
|
94
100
|
|
|
95
101
|
|
|
@@ -147,7 +153,7 @@ def build_blocks(
|
|
|
147
153
|
_word_time(current[-1], "end_seconds", "end")
|
|
148
154
|
or _word_time(current[-1], "start_seconds", "start")
|
|
149
155
|
),
|
|
150
|
-
"lines": _no_orphan(lines),
|
|
156
|
+
"lines": _no_orphan(lines, max_chars_per_line),
|
|
151
157
|
"word_count": len(current),
|
|
152
158
|
})
|
|
153
159
|
current.clear()
|
package/src/utils/cube_lut.py
CHANGED
|
@@ -28,7 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
|
|
29
29
|
import os
|
|
30
30
|
import re
|
|
31
|
-
from typing import Any, Dict, Optional, Tuple
|
|
31
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
32
32
|
|
|
33
33
|
try:
|
|
34
34
|
import numpy as _np
|
|
@@ -56,6 +56,19 @@ def _require_numpy() -> None:
|
|
|
56
56
|
raise CubeLutError("numpy is required for LUT arithmetic (pip install numpy)")
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def _header_floats(path: str, line_number: int, line: str, count: int) -> List[float]:
|
|
60
|
+
"""The `count` numbers after a header keyword, or a CubeLutError naming the line."""
|
|
61
|
+
values = line.split()[1:count + 1]
|
|
62
|
+
try:
|
|
63
|
+
parsed = [float(value) for value in values]
|
|
64
|
+
except ValueError:
|
|
65
|
+
parsed = []
|
|
66
|
+
if len(parsed) != count:
|
|
67
|
+
keyword = line.split()[0]
|
|
68
|
+
raise CubeLutError(f"{path}:{line_number}: malformed {keyword} (expected {count} numbers)")
|
|
69
|
+
return parsed
|
|
70
|
+
|
|
71
|
+
|
|
59
72
|
def read_cube(path: str) -> Dict[str, Any]:
|
|
60
73
|
"""Parse a 3D `.cube` file into `{size, table, domain_min, domain_max, title}`.
|
|
61
74
|
|
|
@@ -100,10 +113,15 @@ def read_cube(path: str) -> Dict[str, Any]:
|
|
|
100
113
|
title = match.group(1) if match else ""
|
|
101
114
|
continue
|
|
102
115
|
if upper.startswith("DOMAIN_MIN"):
|
|
103
|
-
domain_min =
|
|
116
|
+
domain_min = _header_floats(path, line_number, line, 3)
|
|
104
117
|
continue
|
|
105
118
|
if upper.startswith("DOMAIN_MAX"):
|
|
106
|
-
domain_max =
|
|
119
|
+
domain_max = _header_floats(path, line_number, line, 3)
|
|
120
|
+
continue
|
|
121
|
+
if upper.startswith("LUT_3D_INPUT_RANGE"):
|
|
122
|
+
# Resolve's form of DOMAIN_MIN/MAX: one min and one max for all channels.
|
|
123
|
+
low, high = _header_floats(path, line_number, line, 2)
|
|
124
|
+
domain_min, domain_max = [low] * 3, [high] * 3
|
|
107
125
|
continue
|
|
108
126
|
parts = line.split()
|
|
109
127
|
if len(parts) != 3:
|