davinci-resolve-mcp 2.26.0 → 2.27.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/AGENTS.md +3 -1
- package/CHANGELOG.md +51 -0
- package/README.md +3 -3
- package/bin/davinci-resolve-mcp.mjs +64 -7
- package/docs/SKILL.md +17 -3
- package/docs/guides/media-analysis-guide.md +27 -0
- package/docs/install.md +10 -1
- package/install.py +73 -8
- package/package.json +1 -1
- package/src/analysis_dashboard.py +82 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +287 -4
- package/src/utils/analysis_caps.py +28 -19
- package/src/utils/media_analysis.py +229 -30
package/AGENTS.md
CHANGED
|
@@ -93,7 +93,9 @@ venv/bin/python tests/test_import.py
|
|
|
93
93
|
venv/bin/python scripts/audit_api_parity.py
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
Python 3.10-3.12 is
|
|
96
|
+
Python 3.10+ is required (the MCP SDK floor). 3.10-3.12 is the lowest-risk range
|
|
97
|
+
for Resolve scripting; 3.13/3.14 are accepted and verified on Resolve Studio
|
|
98
|
+
20.3.2, but older Resolve builds may fail to connect on 3.13+.
|
|
97
99
|
|
|
98
100
|
## Development Notes
|
|
99
101
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
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.27.0
|
|
6
|
+
|
|
7
|
+
**Frame-sampling modes (issue #46)** — how many frames a clip gets for visual
|
|
8
|
+
analysis is now governed by a `sampling_mode`, decoupled from `depth` (which
|
|
9
|
+
still controls which layers run). A fixed frame count over-sampled short clips
|
|
10
|
+
and under-covered long ones; the demand-driven engine already scaled by
|
|
11
|
+
duration/content, but the caps layer was flat-truncating its output back to 8
|
|
12
|
+
frames — that flat cap was the real cause of long-clip under-coverage.
|
|
13
|
+
|
|
14
|
+
Four clearly-tiered modes, organized so token cost is predictable per tier:
|
|
15
|
+
|
|
16
|
+
- **Economy** (`fixed`) — flat N evenly-spaced, content-blind frames. Cheapest and
|
|
17
|
+
most predictable; good for proxies/triage.
|
|
18
|
+
- **Balanced** (`per_minute`) — `clamp(minutes × frames_per_minute, floor, ceiling)`
|
|
19
|
+
(defaults 4/min, 3–80). Cost is linear in footage length; content-blind.
|
|
20
|
+
- **Thorough** (`adaptive_capped`, recommended/default) — content-aware: samples
|
|
21
|
+
shot boundaries, representatives, and flash candidates, bounded to `[floor,
|
|
22
|
+
ceiling]`. Best coverage with a bounded cost.
|
|
23
|
+
- **Thorough (uncapped)** (`adaptive`) — content-aware with no per-clip ceiling
|
|
24
|
+
(up to the 512-frame hard cap). Use only when clips are short or few.
|
|
25
|
+
|
|
26
|
+
The first time you analyze without a saved default, the tool returns a
|
|
27
|
+
`confirmation_required` response with a `sampling_mode_prompt`; choosing a mode
|
|
28
|
+
saves it as your standing default (mirrors `timed_markers_default`). Pass
|
|
29
|
+
`sampling_mode` per call any time for a one-off that doesn't change the default.
|
|
30
|
+
Tunables (`frames_per_minute`, `frame_floor`, `frame_ceiling`) and the mode are
|
|
31
|
+
all exposed in the control panel (Preferences → Frame sampling mode) with a live
|
|
32
|
+
per-clip token-cost estimate; batch jobs honor the saved default.
|
|
33
|
+
|
|
34
|
+
Analysis-caps presets were retuned so `frames_per_clip` is now a *safety ceiling*
|
|
35
|
+
(minimal/standard/generous = 12/80/200), not the primary frame dial, and the
|
|
36
|
+
per-clip/job/day vision-token caps were raised so the default Thorough mode isn't
|
|
37
|
+
refused by the per-clip token cap. Cache reuse re-samples only when switching up
|
|
38
|
+
the thoroughness rank; a richer prior report still satisfies a cheaper mode. Adds
|
|
39
|
+
`tests/test_sampling_modes.py` (30 tests). Validated end-to-end on a synthetic
|
|
40
|
+
multi-shot clip with real ffmpeg frame extraction.
|
|
41
|
+
|
|
42
|
+
## What's New in v2.26.1
|
|
43
|
+
|
|
44
|
+
**Python 3.13 / 3.14 support (issue #45)** — `npx davinci-resolve-mcp setup`
|
|
45
|
+
previously hard-refused any interpreter outside 3.10–3.12, so it failed outright
|
|
46
|
+
on Python 3.14. The 3.12 ceiling was based on a stale assumption that Resolve's
|
|
47
|
+
scripting bridge has ABI incompatibilities on 3.13+. Verified empirically against
|
|
48
|
+
DaVinci Resolve Studio 20.3.2.9: Python 3.14.4 connects and exercises the
|
|
49
|
+
dict/list marshalling paths cleanly. The launcher and installer now enforce only
|
|
50
|
+
the 3.10 floor (the `mcp[cli]` SDK requirement) with no upper cap. Python 3.13/3.14
|
|
51
|
+
are accepted with a soft heads-up; `setup`/`doctor` surface a precise,
|
|
52
|
+
connection-aware hint only when Resolve is running but the bridge returns no
|
|
53
|
+
connection on 3.13+. Sub-3.10 interpreters get an actionable error instead of a
|
|
54
|
+
dead end. `server.py` warns (never exits) on 3.13+. Adds 6 version-gate unit tests.
|
|
55
|
+
|
|
5
56
|
## What's New in v2.26.0
|
|
6
57
|
|
|
7
58
|
**Fusion group-settings helpers** — Three new `fusion_comp` actions for
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
|
7
7
|
[](docs/reference/api-coverage.md#test-results)
|
|
8
8
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
9
|
-
[](https://www.python.org/downloads/)
|
|
10
10
|
[](https://opensource.org/licenses/MIT)
|
|
11
11
|
|
|
12
12
|
A Model Context Protocol (MCP) server that lets AI assistants control DaVinci Resolve Studio through the official Scripting API. It provides full API coverage plus guarded workflow helpers for editing, media pool organization, render setup, review markers, grading, Fusion, Fairlight, project lifecycle tasks, extension authoring, and source-safe media analysis.
|
|
@@ -133,7 +133,7 @@ Extension authoring references live in [docs/authoring](docs/authoring/). Resolv
|
|
|
133
133
|
## Requirements
|
|
134
134
|
|
|
135
135
|
- DaVinci Resolve Studio 18.5+ on macOS, Windows, or Linux. The free edition does not support external scripting.
|
|
136
|
-
- Python 3.10-3.12
|
|
136
|
+
- Python 3.10+ (3.10-3.12 is the lowest-risk range). Python 3.13/3.14 also work on recent Resolve builds (verified on Studio 20.3.2); older builds may fail to connect on 3.13+, in which case use 3.10-3.12.
|
|
137
137
|
- Resolve external scripting set to **Local**.
|
|
138
138
|
|
|
139
139
|
Resolve 19.1.3 remains the compatibility baseline. Resolve 20.x scripting calls are additive, version-guarded, and live-tested on 20.3.2. Resolve 21 beta APIs are intentionally deferred until stable.
|
|
@@ -11,6 +11,16 @@ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|
|
11
11
|
const VERSION = readPackageVersion();
|
|
12
12
|
const MANAGED_MARKER = ".davinci-resolve-mcp-managed.json";
|
|
13
13
|
|
|
14
|
+
// The only hard Python floor is the MCP SDK: mcp[cli] requires 3.10+.
|
|
15
|
+
// We do NOT cap the upper bound. Resolve's scripting bridge (fusionscript)
|
|
16
|
+
// loads cleanly into newer interpreters on recent builds — Python 3.14 is
|
|
17
|
+
// verified working against Resolve Studio 20.3.2. Older Resolve builds may
|
|
18
|
+
// fail to connect on 3.13+, but the version number is a poor proxy for that;
|
|
19
|
+
// the connection check in `setup`/`doctor` is the real signal, so we proceed
|
|
20
|
+
// with a soft heads-up rather than refusing to run.
|
|
21
|
+
const PY_MIN_MINOR = 10;
|
|
22
|
+
const PY_ABI_RISK_MINOR = 13;
|
|
23
|
+
|
|
14
24
|
const SYNC_ITEMS = [
|
|
15
25
|
"bin",
|
|
16
26
|
"src",
|
|
@@ -54,7 +64,8 @@ Examples:
|
|
|
54
64
|
|
|
55
65
|
Environment:
|
|
56
66
|
DAVINCI_RESOLVE_MCP_INSTALL_ROOT Override the managed install directory.
|
|
57
|
-
DAVINCI_RESOLVE_MCP_PYTHON Python executable to use.
|
|
67
|
+
DAVINCI_RESOLVE_MCP_PYTHON Python executable to use (3.10+). Set this to
|
|
68
|
+
pin a specific interpreter, e.g. python3.12.
|
|
58
69
|
PYTHON Fallback Python executable to use.
|
|
59
70
|
`;
|
|
60
71
|
}
|
|
@@ -193,17 +204,24 @@ function pythonCandidates() {
|
|
|
193
204
|
if (explicit) {
|
|
194
205
|
candidates.push(explicit);
|
|
195
206
|
}
|
|
207
|
+
// Prefer the lowest-ABI-risk interpreters first, then newer ones, then the
|
|
208
|
+
// generic launchers. All 3.10+ are accepted; ordering just picks the safest
|
|
209
|
+
// when several are installed.
|
|
196
210
|
if (process.platform === "win32" && commandExists("py")) {
|
|
197
211
|
candidates.push(
|
|
198
212
|
{ command: "py", args: ["-3.12"] },
|
|
199
213
|
{ command: "py", args: ["-3.11"] },
|
|
200
|
-
{ command: "py", args: ["-3.10"] }
|
|
214
|
+
{ command: "py", args: ["-3.10"] },
|
|
215
|
+
{ command: "py", args: ["-3.13"] },
|
|
216
|
+
{ command: "py", args: ["-3.14"] }
|
|
201
217
|
);
|
|
202
218
|
}
|
|
203
219
|
candidates.push(
|
|
204
220
|
{ command: "python3.12", args: [] },
|
|
205
221
|
{ command: "python3.11", args: [] },
|
|
206
222
|
{ command: "python3.10", args: [] },
|
|
223
|
+
{ command: "python3.13", args: [] },
|
|
224
|
+
{ command: "python3.14", args: [] },
|
|
207
225
|
{ command: "python3", args: [] },
|
|
208
226
|
{ command: "python", args: [] }
|
|
209
227
|
);
|
|
@@ -224,8 +242,9 @@ function checkPython(candidate) {
|
|
|
224
242
|
}
|
|
225
243
|
try {
|
|
226
244
|
const info = JSON.parse(result.stdout.trim());
|
|
227
|
-
const supported = info.major === 3 && info.minor >=
|
|
228
|
-
|
|
245
|
+
const supported = info.major === 3 && info.minor >= PY_MIN_MINOR;
|
|
246
|
+
const abiRisk = info.major === 3 && info.minor >= PY_ABI_RISK_MINOR;
|
|
247
|
+
return { ...candidate, ...info, supported, abiRisk };
|
|
229
248
|
} catch {
|
|
230
249
|
return null;
|
|
231
250
|
}
|
|
@@ -244,8 +263,40 @@ function findSupportedPython() {
|
|
|
244
263
|
}
|
|
245
264
|
}
|
|
246
265
|
|
|
247
|
-
|
|
248
|
-
|
|
266
|
+
throw new Error(unsupportedPythonMessage(checked));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Print the 3.13+ heads-up for run modes that never invoke install.py
|
|
270
|
+
// (server/control-panel/batch). setup/doctor stay quiet here because
|
|
271
|
+
// install.py emits a richer, connection-aware note of its own.
|
|
272
|
+
function maybeWarnAbiRisk(info) {
|
|
273
|
+
if (info && info.abiRisk) {
|
|
274
|
+
console.warn(abiRiskNote(info));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function abiRiskNote(info) {
|
|
279
|
+
return (
|
|
280
|
+
`Note: using Python ${info.major}.${info.minor}.${info.micro}. ` +
|
|
281
|
+
`This is verified working on recent Resolve builds (Studio 20.3.2). ` +
|
|
282
|
+
`If Resolve fails to connect (scriptapp("Resolve") returns None), install ` +
|
|
283
|
+
`Python 3.10-3.12 and pin it with DAVINCI_RESOLVE_MCP_PYTHON=/path/to/python3.12.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function unsupportedPythonMessage(checked) {
|
|
288
|
+
const found = checked.length ? ` Found: ${checked.join(", ")}.` : "";
|
|
289
|
+
const lines = [
|
|
290
|
+
`Python 3.${PY_MIN_MINOR} or newer is required (the MCP SDK needs Python 3.${PY_MIN_MINOR}+).${found}`,
|
|
291
|
+
"",
|
|
292
|
+
"How to fix:",
|
|
293
|
+
" - Install Python 3.12 (the lowest-risk version for Resolve), e.g.:",
|
|
294
|
+
" macOS: brew install python@3.12 (or: pyenv install 3.12)",
|
|
295
|
+
" Linux: pyenv install 3.12 (or your distro's python3.12 package)",
|
|
296
|
+
" Windows: install Python 3.12 from python.org",
|
|
297
|
+
` - Point the launcher at it: DAVINCI_RESOLVE_MCP_PYTHON=/path/to/python3.12 npx ${APP_NAME} setup`,
|
|
298
|
+
];
|
|
299
|
+
return lines.join("\n");
|
|
249
300
|
}
|
|
250
301
|
|
|
251
302
|
function venvPython(root) {
|
|
@@ -258,7 +309,10 @@ function venvPython(root) {
|
|
|
258
309
|
}
|
|
259
310
|
const info = checkPython({ command: executable, args: [] });
|
|
260
311
|
if (!info || !info.supported) {
|
|
261
|
-
throw new Error(
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Managed venv Python must be 3.${PY_MIN_MINOR} or newer. ` +
|
|
314
|
+
`Re-run setup to recreate it: ${executable}`
|
|
315
|
+
);
|
|
262
316
|
}
|
|
263
317
|
return info;
|
|
264
318
|
}
|
|
@@ -326,6 +380,7 @@ function commandDoctor(args) {
|
|
|
326
380
|
function commandServer(args) {
|
|
327
381
|
const root = syncManagedInstall(installRoot());
|
|
328
382
|
const python = venvPython(root) || findSupportedPython();
|
|
383
|
+
maybeWarnAbiRisk(python);
|
|
329
384
|
const serverScript = path.join(root, "src", "server.py");
|
|
330
385
|
const [command, ...commandArgs] = pythonCommandLine(python, [serverScript, ...args]);
|
|
331
386
|
run(command, commandArgs, { cwd: root });
|
|
@@ -334,6 +389,7 @@ function commandServer(args) {
|
|
|
334
389
|
function commandControlPanel(args) {
|
|
335
390
|
const root = syncManagedInstall(installRoot());
|
|
336
391
|
const python = venvPython(root) || findSupportedPython();
|
|
392
|
+
maybeWarnAbiRisk(python);
|
|
337
393
|
const [command, ...commandArgs] = pythonCommandLine(python, ["-m", "src.control_panel", ...args]);
|
|
338
394
|
run(command, commandArgs, { cwd: root });
|
|
339
395
|
}
|
|
@@ -341,6 +397,7 @@ function commandControlPanel(args) {
|
|
|
341
397
|
function commandBatch(args) {
|
|
342
398
|
const root = syncManagedInstall(installRoot());
|
|
343
399
|
const python = venvPython(root) || findSupportedPython();
|
|
400
|
+
maybeWarnAbiRisk(python);
|
|
344
401
|
const [command, ...commandArgs] = pythonCommandLine(python, ["-m", "src.batch_cli", ...args]);
|
|
345
402
|
run(command, commandArgs, { cwd: root });
|
|
346
403
|
}
|
package/docs/SKILL.md
CHANGED
|
@@ -468,6 +468,16 @@ for existing reports. `quick` uses ffprobe metadata; `standard` adds ffmpeg
|
|
|
468
468
|
read-through checks,
|
|
469
469
|
cut-boundary analysis from full-stream scene detection, flash-frame candidates,
|
|
470
470
|
motion/variance scoring, analysis keyframes, and sidecar reports.
|
|
471
|
+
`depth` controls which layers run; a separate `sampling_mode` controls how many
|
|
472
|
+
frames each clip gets for visual analysis (and thus token cost): `fixed`
|
|
473
|
+
(Economy, flat content-blind frames), `per_minute` (Balanced, frames scale with
|
|
474
|
+
duration), `adaptive_capped` (Thorough, content-aware bounded to
|
|
475
|
+
`[frame_floor, frame_ceiling]` — recommended/default), or `adaptive` (Thorough
|
|
476
|
+
uncapped). When no default is saved, the first analyze returns
|
|
477
|
+
`confirmation_required` with a `sampling_mode_prompt`; choosing a mode saves it
|
|
478
|
+
as the default. Pass `sampling_mode` per call for a one-off. The mode owns frame
|
|
479
|
+
count — `analysis_caps.frames_per_clip` is a safety ceiling above it, not the
|
|
480
|
+
primary dial.
|
|
471
481
|
By default, planning checks the active project's analysis root and bounded
|
|
472
482
|
related project-version roots for existing reports, then marks matching clips
|
|
473
483
|
`skip_execution=true` when those reports already contain the requested
|
|
@@ -1247,9 +1257,13 @@ timeline item returns `False` in Resolve. Use `get_node_graph` without a
|
|
|
1247
1257
|
if the Gallery panel is open in the Resolve UI on the Color page. Instruct the
|
|
1248
1258
|
user to open it via Workspace menu if export fails.
|
|
1249
1259
|
|
|
1250
|
-
**Python version** —
|
|
1251
|
-
|
|
1252
|
-
|
|
1260
|
+
**Python version** — the only hard requirement is Python **3.10+** (the MCP SDK
|
|
1261
|
+
floor). There is no upper cap: 3.13/3.14 are accepted, and Python 3.14 is verified
|
|
1262
|
+
working against Resolve Studio 20.3.2. On *older* Resolve builds the scripting
|
|
1263
|
+
bridge may still fail to load on 3.13+ (`scriptapp("Resolve")` returns `None`);
|
|
1264
|
+
`setup`/`doctor` warn on 3.13+ and their connection check surfaces a real failure.
|
|
1265
|
+
If that happens, recreate the venv with Python 3.10–3.12 (the lowest-risk range).
|
|
1266
|
+
The running server only warns on 3.13+ rather than exiting.
|
|
1253
1267
|
|
|
1254
1268
|
**Resolve version guards** — Resolve 20-specific actions return a clear
|
|
1255
1269
|
`requires DaVinci Resolve 20.x+` error when called against older builds. Resolve
|
|
@@ -93,6 +93,33 @@ FFprobe is required. If missing:
|
|
|
93
93
|
via host_chat_paths (finalized per clip with commit_vision, ~2-5 minutes per
|
|
94
94
|
file plus host-chat read time)
|
|
95
95
|
|
|
96
|
+
`depth` controls *which layers run*. How many frames each clip gets for visual
|
|
97
|
+
analysis is a separate axis — the **sampling mode** — because a fixed frame count
|
|
98
|
+
over-samples short clips and under-covers long ones.
|
|
99
|
+
|
|
100
|
+
### 3b. Frame-sampling mode
|
|
101
|
+
|
|
102
|
+
Pass `sampling_mode` on any analyze action, or set a standing default in the
|
|
103
|
+
control panel (Preferences → Frame sampling mode). The mode owns frame count and
|
|
104
|
+
thus token cost; `analysis_caps.frames_per_clip` is now a safety ceiling above it,
|
|
105
|
+
not the primary dial.
|
|
106
|
+
|
|
107
|
+
- **Economy** (`fixed`) — flat N frames (depth-derived, default 8) regardless of
|
|
108
|
+
clip length. Cheapest and most predictable; good for proxies/triage.
|
|
109
|
+
- **Balanced** (`per_minute`) — `frames = clamp(minutes × frames_per_minute, floor,
|
|
110
|
+
ceiling)` (defaults 4/min, 3–80). Cost is linear in footage length; content-blind.
|
|
111
|
+
- **Thorough** (`adaptive_capped`, **recommended**) — content-aware: samples shot
|
|
112
|
+
boundaries, representatives, and flash candidates, bounded to `[floor, ceiling]`
|
|
113
|
+
(3–80). Best coverage with a bounded cost.
|
|
114
|
+
- **Thorough (uncapped)** (`adaptive`) — content-aware with no per-clip ceiling
|
|
115
|
+
(up to the absolute 512-frame hard cap). Use only when clips are short or few.
|
|
116
|
+
|
|
117
|
+
Tunables (`frames_per_minute`, `frame_floor`, `frame_ceiling`) apply to Balanced
|
|
118
|
+
and Thorough. The first time you analyze without a saved default, the tool returns
|
|
119
|
+
a `confirmation_required` response with a `sampling_mode_prompt`; re-run with
|
|
120
|
+
`sampling_mode=<choice>` (which saves it as your default) or pick it in the panel.
|
|
121
|
+
Pass `sampling_mode` explicitly any time for a one-off that doesn't change the default.
|
|
122
|
+
|
|
96
123
|
---
|
|
97
124
|
|
|
98
125
|
## Analysis Commands
|
package/docs/install.md
CHANGED
|
@@ -5,9 +5,18 @@ This guide covers Resolve requirements, the universal installer, supported MCP c
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- **DaVinci Resolve Studio** 18.5+ (macOS, Windows, or Linux) — the free edition does not support external scripting
|
|
8
|
-
- **Python 3.10
|
|
8
|
+
- **Python 3.10+** (the MCP SDK requires 3.10). **3.10–3.12 is the lowest-risk
|
|
9
|
+
choice**; 3.13/3.14 also work on recent Resolve builds — see below
|
|
9
10
|
- DaVinci Resolve running with **Preferences > General > "External scripting using"** set to **Local**
|
|
10
11
|
|
|
12
|
+
> **Python 3.13 / 3.14:** these are **allowed** — setup will use them and warn.
|
|
13
|
+
> Python 3.14 is verified working against DaVinci Resolve Studio 20.3.2. On
|
|
14
|
+
> *older* Resolve builds the scripting bridge may fail to load on 3.13+
|
|
15
|
+
> (`scriptapp("Resolve")` returns `None`); setup's connection check will tell you
|
|
16
|
+
> if that happens. If it does, install a 3.10–3.12 interpreter
|
|
17
|
+
> (`brew install python@3.12`, `pyenv install 3.12`, or python.org on Windows) and
|
|
18
|
+
> point the launcher at it with `DAVINCI_RESOLVE_MCP_PYTHON=/path/to/python3.12`.
|
|
19
|
+
|
|
11
20
|
Validated live coverage is based on **DaVinci Resolve 19.1.3 Studio** for the original API surface, plus **DaVinci Resolve 20.3.2 Studio** for the Resolve 20.0-20.2.2 scripting additions. Resolve 21 beta APIs are intentionally deferred until a stable release.
|
|
12
21
|
|
|
13
22
|
## Quick Start
|
package/install.py
CHANGED
|
@@ -35,9 +35,14 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.
|
|
38
|
+
VERSION = "2.27.0"
|
|
39
|
+
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
|
+
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
|
+
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
|
42
|
+
# may fail to connect on 3.13+, but the connection check is the real signal,
|
|
43
|
+
# so we proceed with a heads-up rather than refusing to run.
|
|
39
44
|
SUPPORTED_PYTHON_MIN = (3, 10)
|
|
40
|
-
|
|
45
|
+
PYTHON_ABI_RISK_MIN = (3, 13)
|
|
41
46
|
|
|
42
47
|
# ─── Colors (disabled on Windows cmd without ANSI support) ────────────────────
|
|
43
48
|
|
|
@@ -77,7 +82,12 @@ def platform_name():
|
|
|
77
82
|
|
|
78
83
|
def is_supported_python_version(version):
|
|
79
84
|
major, minor = version[:2]
|
|
80
|
-
return major == 3 and
|
|
85
|
+
return major == 3 and minor >= SUPPORTED_PYTHON_MIN[1]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_abi_risk_python_version(version):
|
|
89
|
+
major, minor = version[:2]
|
|
90
|
+
return major == 3 and minor >= PYTHON_ABI_RISK_MIN[1]
|
|
81
91
|
|
|
82
92
|
|
|
83
93
|
def format_python_version(version):
|
|
@@ -85,9 +95,38 @@ def format_python_version(version):
|
|
|
85
95
|
|
|
86
96
|
|
|
87
97
|
def python_requirement_text():
|
|
98
|
+
return f"Python {SUPPORTED_PYTHON_MIN[0]}.{SUPPORTED_PYTHON_MIN[1]} or newer"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_ABI_NOTE_PRINTED = False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def print_abi_risk_note_once(version, label="Python"):
|
|
105
|
+
"""Emit the 3.13+ heads-up at most once per installer run."""
|
|
106
|
+
global _ABI_NOTE_PRINTED
|
|
107
|
+
if _ABI_NOTE_PRINTED:
|
|
108
|
+
return
|
|
109
|
+
_ABI_NOTE_PRINTED = True
|
|
110
|
+
print(f" {yellow(label + ':')} {python_abi_risk_note(version)}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def python_abi_risk_note(version):
|
|
114
|
+
return (
|
|
115
|
+
f"Using Python {format_python_version(version)}. Verified working on recent "
|
|
116
|
+
f"Resolve builds (Studio 20.3.2). If Resolve fails to connect "
|
|
117
|
+
f"(scriptapp(\"Resolve\") returns None), install Python 3.10-3.12 and re-run "
|
|
118
|
+
f"with it, e.g.: python3.12 install.py"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def python_fix_hint():
|
|
88
123
|
return (
|
|
89
|
-
|
|
90
|
-
|
|
124
|
+
" How to fix:\n"
|
|
125
|
+
" - Install Python 3.12 (the lowest-risk version for Resolve), e.g.:\n"
|
|
126
|
+
" macOS: brew install python@3.12 (or: pyenv install 3.12)\n"
|
|
127
|
+
" Linux: pyenv install 3.12 (or your distro's python3.12 package)\n"
|
|
128
|
+
" Windows: install Python 3.12 from python.org\n"
|
|
129
|
+
" - Re-run with that interpreter, e.g.: python3.12 install.py"
|
|
91
130
|
)
|
|
92
131
|
|
|
93
132
|
|
|
@@ -120,10 +159,13 @@ def require_supported_python(python_path, label="Python"):
|
|
|
120
159
|
if not is_supported_python_version(version):
|
|
121
160
|
print(
|
|
122
161
|
f" {red(label + ':')} {python_requirement_text()} is required "
|
|
123
|
-
f"
|
|
162
|
+
f"(the MCP SDK needs 3.10+); found {format_python_version(version)} "
|
|
124
163
|
f"at {python_path}"
|
|
125
164
|
)
|
|
165
|
+
print(python_fix_hint())
|
|
126
166
|
sys.exit(1)
|
|
167
|
+
if is_abi_risk_python_version(version):
|
|
168
|
+
print_abi_risk_note_once(version, label)
|
|
127
169
|
return version
|
|
128
170
|
|
|
129
171
|
|
|
@@ -132,10 +174,13 @@ def require_current_python(label="Python"):
|
|
|
132
174
|
if not is_supported_python_version(version):
|
|
133
175
|
print(
|
|
134
176
|
f" {red(label + ':')} {python_requirement_text()} is required "
|
|
135
|
-
f"
|
|
177
|
+
f"(the MCP SDK needs 3.10+); current interpreter is "
|
|
136
178
|
f"{format_python_version(version)} at {sys.executable}"
|
|
137
179
|
)
|
|
180
|
+
print(python_fix_hint())
|
|
138
181
|
sys.exit(1)
|
|
182
|
+
if is_abi_risk_python_version(version):
|
|
183
|
+
print_abi_risk_note_once(version, label)
|
|
139
184
|
return version
|
|
140
185
|
|
|
141
186
|
# ─── Resolve Path Detection ──────────────────────────────────────────────────
|
|
@@ -1411,14 +1456,34 @@ def main():
|
|
|
1411
1456
|
|
|
1412
1457
|
if api_path:
|
|
1413
1458
|
success, message = verify_resolve_connection(python_path, api_path, lib_path)
|
|
1459
|
+
try:
|
|
1460
|
+
py_abi_risk = is_abi_risk_python_version(_version_for_python(python_path))
|
|
1461
|
+
except Exception:
|
|
1462
|
+
py_abi_risk = False
|
|
1414
1463
|
if success:
|
|
1415
1464
|
if "not running" in message.lower():
|
|
1416
1465
|
print(f" API: {green('Module loads OK')}")
|
|
1417
|
-
|
|
1466
|
+
# If Resolve IS running but the bridge still reported no connection
|
|
1467
|
+
# on a 3.13+ interpreter, that is the ABI-mismatch signature.
|
|
1468
|
+
if resolve_running and py_abi_risk:
|
|
1469
|
+
print(
|
|
1470
|
+
f" Resolve: {yellow('Running, but the scripting bridge returned no connection')}"
|
|
1471
|
+
)
|
|
1472
|
+
print(
|
|
1473
|
+
f" This can happen on Python 3.13+ with older Resolve builds. "
|
|
1474
|
+
f"If MCP tools fail, recreate the venv with Python 3.10-3.12."
|
|
1475
|
+
)
|
|
1476
|
+
else:
|
|
1477
|
+
print(f" Resolve: {yellow('Not running')} — start Resolve to use MCP tools")
|
|
1418
1478
|
else:
|
|
1419
1479
|
print(f" Connected: {green(message)}")
|
|
1420
1480
|
else:
|
|
1421
1481
|
print(f" Verify: {yellow(message)}")
|
|
1482
|
+
if py_abi_risk:
|
|
1483
|
+
print(
|
|
1484
|
+
f" On Python 3.13+ this may be an ABI mismatch with Resolve's "
|
|
1485
|
+
f"scripting library — try Python 3.10-3.12 if it persists."
|
|
1486
|
+
)
|
|
1422
1487
|
else:
|
|
1423
1488
|
print(f" {yellow('Skipped')} — Resolve API path not detected")
|
|
1424
1489
|
|
package/package.json
CHANGED
|
@@ -4272,6 +4272,21 @@ HTML = r"""<!doctype html>
|
|
|
4272
4272
|
</select>
|
|
4273
4273
|
</label>
|
|
4274
4274
|
<label>Default sample frames <input id="prefFrames" type="number" min="0" max="48" value="8"></label>
|
|
4275
|
+
<label>Frame sampling mode
|
|
4276
|
+
<select id="prefSamplingMode" onchange="updateSamplingModeHint()">
|
|
4277
|
+
<option value="ask">ask · choose on first analysis</option>
|
|
4278
|
+
<option value="fixed">Economy · flat frames, cheapest & most predictable</option>
|
|
4279
|
+
<option value="per_minute">Balanced · frames scale with duration (linear cost)</option>
|
|
4280
|
+
<option value="adaptive_capped">Thorough · content-aware, bounded cost (recommended)</option>
|
|
4281
|
+
<option value="adaptive">Thorough (uncapped) · content-aware, up to 512 frames</option>
|
|
4282
|
+
</select>
|
|
4283
|
+
</label>
|
|
4284
|
+
<small class="pref-hint" id="samplingModeHint" style="display:block;margin:-4px 0 8px;opacity:0.75;"></small>
|
|
4285
|
+
<div class="pref-inline-row" style="display:flex;gap:10px;flex-wrap:wrap;">
|
|
4286
|
+
<label>Frames / minute <input id="prefSamplingRate" type="number" min="0.1" step="0.5" value="4" oninput="updateSamplingModeHint()"></label>
|
|
4287
|
+
<label>Frame floor <input id="prefSamplingFloor" type="number" min="1" value="3" oninput="updateSamplingModeHint()"></label>
|
|
4288
|
+
<label>Frame ceiling <input id="prefSamplingCeiling" type="number" min="1" value="80" oninput="updateSamplingModeHint()"></label>
|
|
4289
|
+
</div>
|
|
4275
4290
|
<label>Persistence
|
|
4276
4291
|
<select id="prefAnalysisPersistence">
|
|
4277
4292
|
<option value="session_only">session only</option>
|
|
@@ -4755,6 +4770,10 @@ HTML = r"""<!doctype html>
|
|
|
4755
4770
|
prefVisionDefault: 'Controls whether visual frame analysis is used by default when an operation supports it.',
|
|
4756
4771
|
prefTranscriptionDefault: 'Sets the default answer for transcript generation on audio-bearing clips.',
|
|
4757
4772
|
prefSlateDetectionDefault: 'Controls whether slate detection should run or ask before adding slate-informed context.',
|
|
4773
|
+
prefSamplingMode: 'Chooses how many frames each clip gets for visual analysis: Economy (flat), Balanced (scales with duration), or Thorough (content-aware, bounded). Drives both coverage and token cost.',
|
|
4774
|
+
prefSamplingRate: 'Frames sampled per minute in Balanced mode (also seeds Thorough on short clips).',
|
|
4775
|
+
prefSamplingFloor: 'Minimum frames per clip for duration/content-scaled modes.',
|
|
4776
|
+
prefSamplingCeiling: 'Maximum frames per clip for Balanced and Thorough modes (the Thorough per-clip cap).',
|
|
4758
4777
|
prefAnalysisPersistence: 'Chooses whether analysis artifacts stay session-only or keep reusable reports and frames.',
|
|
4759
4778
|
prefAnalysisSummaryStyle: 'Tunes the language of generated summaries for editorial, QC, producer, or full-detail review.',
|
|
4760
4779
|
prefReportFormat: 'Chooses compact readable reports, full reports, or machine-readable output for downstream agents.',
|
|
@@ -9088,6 +9107,12 @@ HTML = r"""<!doctype html>
|
|
|
9088
9107
|
setControlValue('prefSourceTrust', media.source_trust || 'auto');
|
|
9089
9108
|
setControlValue('prefDepth', media.default_depth || 'standard');
|
|
9090
9109
|
setControlValue('prefFrames', media.default_sample_frames ?? 8);
|
|
9110
|
+
// sampling_mode_default is null when unset → show "ask".
|
|
9111
|
+
setControlValue('prefSamplingMode', media.sampling_mode_default || 'ask');
|
|
9112
|
+
setControlValue('prefSamplingRate', media.sampling_frames_per_minute ?? 4);
|
|
9113
|
+
setControlValue('prefSamplingFloor', media.sampling_frame_floor ?? 3);
|
|
9114
|
+
setControlValue('prefSamplingCeiling', media.sampling_frame_ceiling ?? 80);
|
|
9115
|
+
updateSamplingModeHint();
|
|
9091
9116
|
setControlValue('prefAnalysisPersistence', media.analysis_persistence);
|
|
9092
9117
|
const legacySummaryMap = { assistant_editor: 'creative', producer: 'creative', qc: 'technical' };
|
|
9093
9118
|
const summaryStyle = legacySummaryMap[media.analysis_summary_style] || media.analysis_summary_style || 'concise';
|
|
@@ -9182,6 +9207,38 @@ HTML = r"""<!doctype html>
|
|
|
9182
9207
|
};
|
|
9183
9208
|
}
|
|
9184
9209
|
|
|
9210
|
+
// Rough per-frame vision cost for the estimate (≈768px frame at typical
|
|
9211
|
+
// tokenization). The engine's pre-call refusal estimates more conservatively.
|
|
9212
|
+
const SAMPLING_TOKENS_PER_FRAME = 450;
|
|
9213
|
+
function _fmtTokens(frames) {
|
|
9214
|
+
const k = (frames * SAMPLING_TOKENS_PER_FRAME) / 1000;
|
|
9215
|
+
return k >= 1 ? `~${k.toFixed(k < 10 ? 1 : 0)}k tokens` : `~${Math.round(k * 1000)} tokens`;
|
|
9216
|
+
}
|
|
9217
|
+
function updateSamplingModeHint() {
|
|
9218
|
+
const hintEl = document.getElementById('samplingModeHint');
|
|
9219
|
+
if (!hintEl) return;
|
|
9220
|
+
const mode = ($('prefSamplingMode') || {}).value || 'ask';
|
|
9221
|
+
const rate = Number(($('prefSamplingRate') || {}).value) || 4;
|
|
9222
|
+
const floor = Number(($('prefSamplingFloor') || {}).value) || 3;
|
|
9223
|
+
const ceil = Number(($('prefSamplingCeiling') || {}).value) || 80;
|
|
9224
|
+
const fixed = Number(($('prefFrames') || {}).value) || 8;
|
|
9225
|
+
let msg = '';
|
|
9226
|
+
if (mode === 'ask') {
|
|
9227
|
+
msg = 'You will be asked to pick a mode the first time you analyze. Recommended: Thorough.';
|
|
9228
|
+
} else if (mode === 'fixed') {
|
|
9229
|
+
msg = `Economy — flat ${fixed} frames per clip regardless of length (${_fmtTokens(fixed)}/clip). Most predictable.`;
|
|
9230
|
+
} else if (mode === 'per_minute') {
|
|
9231
|
+
const oneMin = Math.max(floor, Math.min(ceil, Math.round(rate)));
|
|
9232
|
+
const tenMin = Math.max(floor, Math.min(ceil, Math.round(rate * 10)));
|
|
9233
|
+
msg = `Balanced — ${rate}/min, bounded ${floor}–${ceil}. ~${oneMin}f (${_fmtTokens(oneMin)}) for 1 min · ~${tenMin}f (${_fmtTokens(tenMin)}) for 10 min. Linear cost.`;
|
|
9234
|
+
} else if (mode === 'adaptive_capped') {
|
|
9235
|
+
msg = `Thorough — content-aware (shot boundaries + flashes), bounded ${floor}–${ceil} frames/clip (${_fmtTokens(floor)}–${_fmtTokens(ceil)}). Best coverage, bounded cost.`;
|
|
9236
|
+
} else if (mode === 'adaptive') {
|
|
9237
|
+
msg = `Thorough (uncapped) — content-aware, no per-clip ceiling (up to 512 frames, ${_fmtTokens(512)}). Use only for short/few clips.`;
|
|
9238
|
+
}
|
|
9239
|
+
hintEl.textContent = msg;
|
|
9240
|
+
}
|
|
9241
|
+
|
|
9185
9242
|
function setupPreferencePayload() {
|
|
9186
9243
|
let markerColors = {};
|
|
9187
9244
|
try {
|
|
@@ -9197,6 +9254,10 @@ HTML = r"""<!doctype html>
|
|
|
9197
9254
|
source_trust: $('prefSourceTrust').value,
|
|
9198
9255
|
default_depth: $('prefDepth').value,
|
|
9199
9256
|
default_sample_frames: Number($('prefFrames').value || 8),
|
|
9257
|
+
sampling_mode_default: $('prefSamplingMode').value,
|
|
9258
|
+
sampling_frames_per_minute: Number($('prefSamplingRate').value || 4),
|
|
9259
|
+
sampling_frame_floor: Number($('prefSamplingFloor').value || 3),
|
|
9260
|
+
sampling_frame_ceiling: Number($('prefSamplingCeiling').value || 80),
|
|
9200
9261
|
analysis_persistence: $('prefAnalysisPersistence').value,
|
|
9201
9262
|
analysis_summary_style: $('prefAnalysisSummaryStyle').value,
|
|
9202
9263
|
report_format: $('prefReportFormat').value,
|
|
@@ -13299,6 +13360,27 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
13299
13360
|
"cleanup_frames": True,
|
|
13300
13361
|
"reuse_project_roots": self.state.related_project_roots(),
|
|
13301
13362
|
}
|
|
13363
|
+
# Honor the saved frame-sampling mode (or an explicit per-job override)
|
|
13364
|
+
# so batch runs match the user's chosen coverage/cost. Falls back to the
|
|
13365
|
+
# recommended mode when the user hasn't set a default yet (batch jobs
|
|
13366
|
+
# shouldn't block on the first-run prompt).
|
|
13367
|
+
try:
|
|
13368
|
+
from src.server import (
|
|
13369
|
+
_media_analysis_effective_preferences as _ma_eff_prefs,
|
|
13370
|
+
)
|
|
13371
|
+
from src.utils import media_analysis as _ma_mod
|
|
13372
|
+
_ma_prefs = _ma_eff_prefs()
|
|
13373
|
+
params["sampling_mode"] = (
|
|
13374
|
+
body.get("sampling_mode")
|
|
13375
|
+
or _ma_prefs.get("sampling_mode_default")
|
|
13376
|
+
or _ma_mod.RECOMMENDED_SAMPLING_MODE
|
|
13377
|
+
)
|
|
13378
|
+
params["frames_per_minute"] = body.get("frames_per_minute") or _ma_prefs.get("sampling_frames_per_minute")
|
|
13379
|
+
params["frame_floor"] = body.get("frame_floor") or _ma_prefs.get("sampling_frame_floor")
|
|
13380
|
+
params["frame_ceiling"] = body.get("frame_ceiling") or _ma_prefs.get("sampling_frame_ceiling")
|
|
13381
|
+
except Exception:
|
|
13382
|
+
# Best-effort; the engine still applies its own defaults.
|
|
13383
|
+
pass
|
|
13302
13384
|
with self.state.lock:
|
|
13303
13385
|
created = create_batch_job_from_paths(
|
|
13304
13386
|
project_name=self.state.project_name,
|
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.27.0"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
logger.info(f"Detected platform: {get_platform()}")
|