davinci-resolve-mcp 2.104.0 → 2.104.2
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 +77 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/headless-edit-loop.md +15 -0
- package/install.py +33 -12
- package/package.json +1 -1
- package/resolve-advanced/server/db-patch.mjs +15 -3
- package/resolve-advanced/server/offline-ref-db.mjs +8 -2
- package/resolve-advanced/vendor/drp-format/__tests__/framerate-encoding.test.js +47 -0
- package/resolve-advanced/vendor/drp-format/drp-packager.js +1 -0
- package/resolve-advanced/vendor/drp-format/seq-container-builder.js +11 -3
- package/resolve-advanced/vendor/drp-format/xml-builder.js +23 -19
- package/scripts/doctor.py +85 -39
- package/src/granular/common.py +1 -1
- package/src/granular/media_pool.py +15 -7
- package/src/granular/timeline.py +10 -5
- package/src/granular/timeline_item.py +21 -7
- package/src/server.py +497 -93
- package/src/utils/api_truth.py +17 -5
- package/src/utils/app_control.py +11 -1
- package/src/utils/audio_fairlight_live_probe.py +8 -1
- package/src/utils/color_grade_live_probe.py +8 -1
- package/src/utils/fusion_composition_live_probe.py +8 -1
- package/src/utils/probe_catalogue.py +15 -3
- package/src/utils/project_cleanup.py +17 -3
- package/src/utils/render_deliver_live_probe.py +8 -1
- package/src/utils/resolve_bridge_client.py +10 -1
- package/src/utils/resolve_writes.py +79 -0
- package/src/utils/review_annotation_live_probe.py +8 -1
- package/src/utils/timeline_conform_live_probe.py +8 -1
- package/src/utils/timeline_kernel_live_probe.py +7 -1
- package/src/utils/timeline_versioning.py +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,83 @@
|
|
|
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.2
|
|
6
|
+
|
|
7
|
+
A contributor batch: two merged PRs, one PR converted into its fix, and four
|
|
8
|
+
sharp issues from @andytsai821201-spec — all live- or repro-verified.
|
|
9
|
+
|
|
10
|
+
**Merged.** PR #166 by @matoberuc-afk routes SetCurrentTimeline and 28 other
|
|
11
|
+
discarded Resolve mutator returns through a checked helper — a refused
|
|
12
|
+
timeline switch now errors instead of silently sending the next edit to
|
|
13
|
+
whatever timeline was current. PR #170 by @FerroQuant makes the doctor and
|
|
14
|
+
installer probes bridge-first and hard-exits probe children after native
|
|
15
|
+
Fusion imports, extending PR #108's fusionscript-teardown rule to the
|
|
16
|
+
remaining short-lived probes.
|
|
17
|
+
|
|
18
|
+
**Fixed (from PR #165 by @Douglas4000).** `timeline_frame capture` died on the
|
|
19
|
+
free-edition bridge with "unexpected keyword argument 'isInteractiveMode'":
|
|
20
|
+
the bridge proxies Resolve calls positionally, and the single-frame render
|
|
21
|
+
used a keyword. The call is positional now, and the bridge proxy raises a
|
|
22
|
+
TypeError that names the rule instead of the bare stack trace.
|
|
23
|
+
|
|
24
|
+
**Fixed (#167).** `drt.author`'s hand-typed frame-rate hex table was wrong in
|
|
25
|
+
three of eight entries: 23.976 stored 30000/1001 (a different, plausible
|
|
26
|
+
rate), 29.97 stored 29.9739, and 59.94 stored 0.9367 — while validate stayed
|
|
27
|
+
green. The table is gone; rounded NTSC decimals snap to their exact rationals
|
|
28
|
+
and everything encodes through writeDoubleLE.
|
|
29
|
+
|
|
30
|
+
**Fixed (#168).** `drt.author` wrote fractional `<StartFrame>` values at
|
|
31
|
+
fractional rates (01:00:00:00 at 30000/1001 → 107892.107…) and ignored the
|
|
32
|
+
spec's `startFrame` field entirely. Frame indexes now round, and an explicit
|
|
33
|
+
startFrame wins over the timecode.
|
|
34
|
+
|
|
35
|
+
**Fixed (#169).** `project_db` by projectName never searched
|
|
36
|
+
`Resolve Project Library/Resolve Projects` — the root a stock modern Studio
|
|
37
|
+
install actually uses (this repo's own 19.1.3 machine uses the old
|
|
38
|
+
`Resolve Disk Database` name, so both are real). Both Studio roots and the
|
|
39
|
+
sandboxed free-edition root are searched and deduped.
|
|
40
|
+
|
|
41
|
+
**Fixed (#171).** Resolve honours the sequence name INSIDE an FCP7 XML over
|
|
42
|
+
the `timelineName` import option, so an iterating export→edit→import loop
|
|
43
|
+
with a stale internal name "succeeded" while returning the same existing
|
|
44
|
+
timeline forever. `import_timeline_checked` now rewrites the XML's internal
|
|
45
|
+
sequence name to the requested timelineName before importing (surgical text
|
|
46
|
+
replacement on a temp copy — DOCTYPE and clip names survive byte-for-byte),
|
|
47
|
+
and a format it cannot rewrite that still returns an existing timeline errors
|
|
48
|
+
instead of reporting success. Live-verified on Studio 19.1.3.7 with the
|
|
49
|
+
reporter's exact step sequence. The headless-edit-loop guide documents the
|
|
50
|
+
internal-name rule for raw-API callers.
|
|
51
|
+
|
|
52
|
+
## What's New in v2.104.1
|
|
53
|
+
|
|
54
|
+
**The job metadata lies too — verify_output now cross-checks the timeline.**
|
|
55
|
+
Finishing v2.104.0's pending live validation exposed a hole in the new
|
|
56
|
+
`render.verify_output`: in the issue #164 case (content before the timeline
|
|
57
|
+
start) Resolve rewrites the render job's own MarkIn/MarkOut down to the
|
|
58
|
+
collapsed extent. Measured live on Studio 19.1.3.7: 96 frames of content
|
|
59
|
+
placed before the start turned an explicit 96-frame mark range into a 1-frame
|
|
60
|
+
job — Complete at 100%, a 1-frame black stub, and a clean duration ratio,
|
|
61
|
+
because the expected duration was computed from the job's own lying range.
|
|
62
|
+
|
|
63
|
+
The only truthful readback in that state is the timeline items themselves,
|
|
64
|
+
which report their real positions. `verify_output` now finds the job's
|
|
65
|
+
timeline and cross-checks it: video items starting before the timeline's
|
|
66
|
+
start frame warn (the #164 signature by direct evidence), and a mark range
|
|
67
|
+
under half the items' extent warns (the collapse signature). Callers can also
|
|
68
|
+
pass `expected_frames` or `expected_duration_seconds` outright. Live-verified
|
|
69
|
+
both ways on 19.1.3.7: the healthy render verifies clean, the stub now fails
|
|
70
|
+
with both warnings.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- `render.verify_output` no longer trusts the job's MarkIn/MarkOut as the
|
|
75
|
+
expected duration — the #164 stub previously verified clean at ratio 1.0.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- The api_truth recordFrame entry now documents the mark-range rewrite and
|
|
80
|
+
the item-readback discriminator; the Blackmagic-facing report regenerated.
|
|
81
|
+
|
|
5
82
|
## What's New in v2.104.0
|
|
6
83
|
|
|
7
84
|
The read/write symmetry audit's worklist, worked. PR #162's AST rewrite left
|
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.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -61,6 +61,21 @@ name is taken. An iterative loop that reuses one name works exactly once and
|
|
|
61
61
|
then quietly does nothing, which is the worst possible failure for an automated
|
|
62
62
|
edit cycle. Make the name unique per iteration.
|
|
63
63
|
|
|
64
|
+
### A unique `timelineName` is still not enough — the file's internal name wins
|
|
65
|
+
|
|
66
|
+
Resolve honours the sequence name **inside** the interchange file over the
|
|
67
|
+
`timelineName` option (issue #171, measured on Studio 21.0.4.5). Export → edit
|
|
68
|
+
→ re-import with `timelineName: CUT_v002` while the XML still says `CUT_v001`
|
|
69
|
+
and Resolve hands back the **existing** `CUT_v001` timeline: the raw API
|
|
70
|
+
reports the old timeline as if it were the import, and the loop operates on one
|
|
71
|
+
timeline forever. When driving the raw API, bump the `<sequence><name>` inside
|
|
72
|
+
the XML each iteration.
|
|
73
|
+
|
|
74
|
+
`timeline.import_timeline_checked` handles both halves for you: it rewrites the
|
|
75
|
+
FCP7 XML's internal sequence name to the requested `timelineName` before
|
|
76
|
+
importing, and if a format it cannot rewrite still returns an existing timeline
|
|
77
|
+
it errors instead of reporting success.
|
|
78
|
+
|
|
64
79
|
### DRT ignores `timelineName` and re-imports the media
|
|
65
80
|
|
|
66
81
|
DRT is the native format and the obvious first choice, but it behaves
|
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.2"
|
|
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
|
|
@@ -1329,37 +1329,57 @@ def access_violation_message(returncode, version=None):
|
|
|
1329
1329
|
|
|
1330
1330
|
|
|
1331
1331
|
def verify_resolve_connection(python_path, api_path, lib_path):
|
|
1332
|
-
"""
|
|
1332
|
+
"""Probe Resolve without exposing short-lived children to Fusion teardown.
|
|
1333
|
+
|
|
1334
|
+
The persistent bridge is tried before Blackmagic's native scripting module.
|
|
1335
|
+
If direct scripting is genuinely required, the disposable child flushes its
|
|
1336
|
+
result and hard-exits after any native import attempt so fusionscript's
|
|
1337
|
+
background RemoteApp thread cannot race CPython finalization.
|
|
1338
|
+
"""
|
|
1333
1339
|
if not api_path:
|
|
1334
1340
|
return False, "Resolve API path not found"
|
|
1335
1341
|
|
|
1336
1342
|
env = {**os.environ, **build_server_env(python_path, api_path, lib_path)}
|
|
1337
1343
|
modules_path = env["PYTHONPATH"]
|
|
1338
1344
|
repo_root = str(Path(__file__).resolve().parent)
|
|
1339
|
-
# Route through connect_resolve so Network mode (RESOLVE_SCRIPT_HOST, propagated
|
|
1340
|
-
# into env by build_server_env) uses the explicit IP-targeted overload. Fall
|
|
1341
|
-
# back to Local-mode discovery if the helper cannot be imported.
|
|
1342
1345
|
test_script = textwrap.dedent(f"""\
|
|
1346
|
+
import os
|
|
1343
1347
|
import sys
|
|
1344
1348
|
sys.path.insert(0, {modules_path!r})
|
|
1345
1349
|
sys.path.insert(0, {repo_root!r})
|
|
1350
|
+
native_import_attempted = False
|
|
1346
1351
|
try:
|
|
1347
|
-
import DaVinciResolveScript as dvr
|
|
1348
1352
|
try:
|
|
1349
1353
|
from src.utils.resolve_connection import connect_resolve
|
|
1350
1354
|
except Exception:
|
|
1351
|
-
connect_resolve =
|
|
1352
|
-
|
|
1355
|
+
connect_resolve = None
|
|
1356
|
+
|
|
1357
|
+
resolve = None
|
|
1358
|
+
if connect_resolve is not None:
|
|
1359
|
+
resolve = connect_resolve(None)
|
|
1360
|
+
|
|
1361
|
+
if resolve is None:
|
|
1362
|
+
native_import_attempted = True
|
|
1363
|
+
import DaVinciResolveScript as dvr
|
|
1364
|
+
if connect_resolve is not None:
|
|
1365
|
+
resolve = connect_resolve(dvr)
|
|
1366
|
+
else:
|
|
1367
|
+
resolve = dvr.scriptapp('Resolve')
|
|
1368
|
+
|
|
1353
1369
|
if resolve:
|
|
1354
1370
|
name = resolve.GetProductName()
|
|
1355
1371
|
ver = resolve.GetVersionString()
|
|
1356
|
-
print(f"CONNECTED: {{name}} {{ver}}")
|
|
1372
|
+
print(f"CONNECTED: {{name}} {{ver}}", flush=True)
|
|
1357
1373
|
else:
|
|
1358
|
-
print("IMPORTED_OK: Module loads but Resolve not running or not responding")
|
|
1374
|
+
print("IMPORTED_OK: Module loads but Resolve not running or not responding", flush=True)
|
|
1359
1375
|
except ImportError as e:
|
|
1360
|
-
print(f"IMPORT_ERROR: {{e}}")
|
|
1376
|
+
print(f"IMPORT_ERROR: {{e}}", flush=True)
|
|
1361
1377
|
except Exception as e:
|
|
1362
|
-
print(f"ERROR: {{e}}")
|
|
1378
|
+
print(f"ERROR: {{e}}", flush=True)
|
|
1379
|
+
if native_import_attempted:
|
|
1380
|
+
sys.stdout.flush()
|
|
1381
|
+
sys.stderr.flush()
|
|
1382
|
+
os._exit(0)
|
|
1363
1383
|
""")
|
|
1364
1384
|
|
|
1365
1385
|
process_timeout = 10.0
|
|
@@ -1400,6 +1420,7 @@ def verify_resolve_connection(python_path, api_path, lib_path):
|
|
|
1400
1420
|
except Exception as e:
|
|
1401
1421
|
return False, str(e)
|
|
1402
1422
|
|
|
1423
|
+
|
|
1403
1424
|
# ─── Interactive UI ───────────────────────────────────────────────────────────
|
|
1404
1425
|
|
|
1405
1426
|
def print_banner():
|
package/package.json
CHANGED
|
@@ -18,6 +18,17 @@ const require = createRequire(import.meta.url);
|
|
|
18
18
|
|
|
19
19
|
export const DISK_DB_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Disk Database/Resolve Projects');
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The other Studio layout. A stock modern Studio install on macOS keeps its
|
|
23
|
+
* local library under "Resolve Project Library", not "Resolve Disk Database"
|
|
24
|
+
* (issue #169, confirmed on Studio 21.0.4.5: every local project sits at
|
|
25
|
+
* Resolve Project Library/Resolve Projects/Users/<dbuser>/Projects/<name>/).
|
|
26
|
+
* The recursive walk already spans the Users/<dbuser>/Projects segment, so
|
|
27
|
+
* listing the root is all that is needed. Which root exists depends on how
|
|
28
|
+
* and when the library was created — search both, dedupe, never guess.
|
|
29
|
+
*/
|
|
30
|
+
export const PROJECT_LIBRARY_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Project Library/Resolve Projects');
|
|
31
|
+
|
|
21
32
|
/**
|
|
22
33
|
* The FREE edition ships from the App Store and runs SANDBOXED, so its project
|
|
23
34
|
* library is not under Application Support at all — it lives inside the app's
|
|
@@ -39,7 +50,7 @@ export const LITE_DB_ROOT = path.join(
|
|
|
39
50
|
);
|
|
40
51
|
|
|
41
52
|
/** Every root searched when resolving a project by name, Studio first. */
|
|
42
|
-
export const DB_ROOTS = [DISK_DB_ROOT, LITE_DB_ROOT];
|
|
53
|
+
export const DB_ROOTS = [DISK_DB_ROOT, PROJECT_LIBRARY_ROOT, LITE_DB_ROOT];
|
|
43
54
|
|
|
44
55
|
export function loadSqlite() {
|
|
45
56
|
try {
|
|
@@ -82,8 +93,9 @@ export function resolveDbPath({ projectDb, projectName }) {
|
|
|
82
93
|
const hits = [...new Set(DB_ROOTS.flatMap((root) => findProjectDb(projectName, root)))];
|
|
83
94
|
if (!hits.length) {
|
|
84
95
|
throw new Error(
|
|
85
|
-
`no Project.db found for project "${projectName}". Searched the Studio
|
|
86
|
-
`(${DISK_DB_ROOT}) and the sandboxed free-edition
|
|
96
|
+
`no Project.db found for project "${projectName}". Searched the Studio libraries ` +
|
|
97
|
+
`(${DISK_DB_ROOT} and ${PROJECT_LIBRARY_ROOT}) and the sandboxed free-edition ` +
|
|
98
|
+
`library (${LITE_DB_ROOT}). ` +
|
|
87
99
|
'If Resolve keeps its projects elsewhere — a relocated library, a network/Postgres ' +
|
|
88
100
|
'database, or the free edition on Windows/Linux — pass projectDb with the full path.',
|
|
89
101
|
);
|
|
@@ -33,6 +33,9 @@ import { createRequire } from 'node:module';
|
|
|
33
33
|
const require = createRequire(import.meta.url);
|
|
34
34
|
|
|
35
35
|
const DISK_DB_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Disk Database/Resolve Projects');
|
|
36
|
+
// Modern Studio installs keep the local library under "Resolve Project
|
|
37
|
+
// Library" instead (issue #169) — same shape, different root name.
|
|
38
|
+
const PROJECT_LIBRARY_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Project Library/Resolve Projects');
|
|
36
39
|
|
|
37
40
|
function loadSqlite() {
|
|
38
41
|
try {
|
|
@@ -86,8 +89,11 @@ function openDb(dbPath, writable) {
|
|
|
86
89
|
function resolveDbPath({ projectDb, projectName }) {
|
|
87
90
|
if (projectDb) return projectDb;
|
|
88
91
|
if (!projectName) throw new Error('provide projectDb (path) or projectName');
|
|
89
|
-
const hits =
|
|
90
|
-
|
|
92
|
+
const hits = [...new Set([
|
|
93
|
+
...findProjectDb(projectName, DISK_DB_ROOT),
|
|
94
|
+
...findProjectDb(projectName, PROJECT_LIBRARY_ROOT),
|
|
95
|
+
])];
|
|
96
|
+
if (!hits.length) throw new Error(`no Project.db found for project "${projectName}" under ${DISK_DB_ROOT} or ${PROJECT_LIBRARY_ROOT}`);
|
|
91
97
|
if (hits.length > 1) throw new Error(`multiple Project.db match "${projectName}": ${hits.join(', ')} — pass projectDb explicitly`);
|
|
92
98
|
return hits[0];
|
|
93
99
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Issue #167: the hand-typed FRAME_RATE_ENCODINGS table stored 30000/1001 for
|
|
2
|
+
// 23.976, 29.9739 for 29.97, and 0.9367 for 59.94, while validate stayed
|
|
3
|
+
// green. Issue #168: fractional StartFrame at NTSC rates, startFrame ignored.
|
|
4
|
+
const { test } = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const { encodeFrameRate, snapFrameRate } = require('../xml-builder');
|
|
7
|
+
const { buildSeqContainerFile } = require('../seq-container-builder');
|
|
8
|
+
|
|
9
|
+
function decode(hex) {
|
|
10
|
+
return Buffer.from(hex, 'hex').readDoubleLE(0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('rounded NTSC decimals snap to their exact rationals', () => {
|
|
14
|
+
assert.ok(Math.abs(decode(encodeFrameRate(23.976)) - 24000 / 1001) < 1e-9);
|
|
15
|
+
assert.ok(Math.abs(decode(encodeFrameRate(29.97)) - 30000 / 1001) < 1e-9);
|
|
16
|
+
assert.ok(Math.abs(decode(encodeFrameRate(59.94)) - 60000 / 1001) < 1e-9);
|
|
17
|
+
assert.ok(Math.abs(decode(encodeFrameRate(23.98)) - 24000 / 1001) < 1e-9);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('exact rationals and integers round-trip untouched', () => {
|
|
21
|
+
for (const fps of [24, 25, 30, 48, 50, 60, 30000 / 1001, 24000 / 1001, 60000 / 1001]) {
|
|
22
|
+
assert.ok(Math.abs(decode(encodeFrameRate(fps)) - snapFrameRate(fps)) < 1e-12);
|
|
23
|
+
if (Number.isInteger(fps)) {
|
|
24
|
+
assert.strictEqual(snapFrameRate(fps), fps, `integer ${fps} must not snap`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('the three formerly-wrong table rates never reappear', () => {
|
|
30
|
+
// The old table's values, byte for byte. None may ever be produced again
|
|
31
|
+
// for the inputs that used to hit them.
|
|
32
|
+
assert.notStrictEqual(encodeFrameRate(23.976), '286b55e253f83d40');
|
|
33
|
+
assert.notStrictEqual(encodeFrameRate(29.97), '286b55e253f93d40');
|
|
34
|
+
assert.notStrictEqual(encodeFrameRate(59.94), '286b55e253f9ed3f');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('StartFrame is an integer at fractional rates and startFrame wins', async () => {
|
|
38
|
+
const tl = { name: 'T', videoTracks: [], audioTracks: [] };
|
|
39
|
+
const fromTc = await buildSeqContainerFile(tl, {
|
|
40
|
+
frameRate: 30000 / 1001, startTimecode: '01:00:00:00',
|
|
41
|
+
});
|
|
42
|
+
assert.match(fromTc, /<StartFrame>107892<\/StartFrame>/);
|
|
43
|
+
const explicit = await buildSeqContainerFile(tl, {
|
|
44
|
+
frameRate: 30000 / 1001, startTimecode: '01:00:00:00', startFrame: 99999,
|
|
45
|
+
});
|
|
46
|
+
assert.match(explicit, /<StartFrame>99999<\/StartFrame>/);
|
|
47
|
+
});
|
|
@@ -93,6 +93,7 @@ async function packageFullDRP(options) {
|
|
|
93
93
|
const seqContainerXml = await buildSeqContainerFile(timeline, {
|
|
94
94
|
frameRate: timeline.frameRate || 24,
|
|
95
95
|
startTimecode: timeline.startTimecode || '01:00:00:00',
|
|
96
|
+
startFrame: timeline.startFrame,
|
|
96
97
|
markers: timeline.markers || [],
|
|
97
98
|
resolution: timeline.resolution || '1920x1080',
|
|
98
99
|
});
|
|
@@ -60,8 +60,11 @@ async function buildSeqContainerFile(timeline, options = {}) {
|
|
|
60
60
|
// Build lockable blob with markers
|
|
61
61
|
const lockableBlob = buildLockableBlobElement(markers, lockableBlobId, frameRate);
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
|
|
63
|
+
// An explicit startFrame wins; otherwise derive it from the timecode
|
|
64
|
+
// (issue #168 — the spec field used to be ignored entirely).
|
|
65
|
+
const startFrame = Number.isFinite(options.startFrame)
|
|
66
|
+
? Math.round(options.startFrame)
|
|
67
|
+
: timecodeToFrames(startTimecode, frameRate);
|
|
65
68
|
|
|
66
69
|
// Combine video tracks: standard video tracks first, then Rich title tracks
|
|
67
70
|
const allVideoTrackElements = [...videoTrackElements, ...richTitleTrackElements];
|
|
@@ -364,7 +367,11 @@ function timecodeToFrames(timecode, fps = 24) {
|
|
|
364
367
|
if (parts.length !== 4) return 0;
|
|
365
368
|
|
|
366
369
|
const [hh, mm, ss, ff] = parts;
|
|
367
|
-
|
|
370
|
+
// A frame index is an integer. At fractional rates the seconds product is
|
|
371
|
+
// fractional (3600 x 30000/1001 = 107892.107...), and writing it raw put a
|
|
372
|
+
// fractional <StartFrame> in the XML (issue #168). Non-drop convention:
|
|
373
|
+
// round the seconds part, then add the frame component.
|
|
374
|
+
return Math.round(hh * 3600 * fps + mm * 60 * fps + ss * fps) + ff;
|
|
368
375
|
}
|
|
369
376
|
|
|
370
377
|
// =============================================================================
|
|
@@ -549,6 +556,7 @@ async function buildSeqContainerFiles(timelines, options = {}) {
|
|
|
549
556
|
...options,
|
|
550
557
|
frameRate: timeline.frameRate || options.frameRate || 24,
|
|
551
558
|
startTimecode: timeline.startTimecode || options.startTimecode || '01:00:00:00',
|
|
559
|
+
startFrame: timeline.startFrame ?? options.startFrame,
|
|
552
560
|
markers: timeline.markers || options.markers || [],
|
|
553
561
|
});
|
|
554
562
|
results.push({ filename: `SeqContainer${idx + 1}.xml`, content });
|
|
@@ -30,19 +30,28 @@ const DEFAULT_PROJECT_SETTINGS = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* Snap rounded NTSC decimals to their exact rationals.
|
|
34
|
+
*
|
|
35
|
+
* A caller writing 23.976 / 29.97 / 59.94 means 24000/1001, 30000/1001,
|
|
36
|
+
* 60000/1001 — the value Resolve itself stores. Integer rates are left
|
|
37
|
+
* untouched (24 × 1.001 = 24.024 sits outside the window), and an exact
|
|
38
|
+
* rational snaps to itself. This replaces a hand-typed hex lookup table whose
|
|
39
|
+
* 23.976 entry actually held 30000/1001, whose 29.97 entry held 29.9739, and
|
|
40
|
+
* whose 59.94 entry held 0.9367 (issue #167) — magic constants nobody could
|
|
41
|
+
* read were wrong for years while every exact input bypassed them.
|
|
35
42
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
43
|
+
function snapFrameRate(fps) {
|
|
44
|
+
const scaled = (fps * 1001) / 1000;
|
|
45
|
+
const nearest = Math.round(scaled);
|
|
46
|
+
if (
|
|
47
|
+
nearest > 0 &&
|
|
48
|
+
Math.abs(scaled - nearest) < 0.02 &&
|
|
49
|
+
Math.abs(fps - nearest) > 1e-9
|
|
50
|
+
) {
|
|
51
|
+
return (nearest * 1000) / 1001;
|
|
52
|
+
}
|
|
53
|
+
return fps;
|
|
54
|
+
}
|
|
46
55
|
|
|
47
56
|
// ============================================================================
|
|
48
57
|
// HELPER FUNCTIONS
|
|
@@ -73,14 +82,9 @@ function generateUUID() {
|
|
|
73
82
|
* // Returns: "0000000000003840"
|
|
74
83
|
*/
|
|
75
84
|
function encodeFrameRate(fps) {
|
|
76
|
-
// Check if we have a pre-encoded value
|
|
77
|
-
if (FRAME_RATE_ENCODINGS[fps]) {
|
|
78
|
-
return FRAME_RATE_ENCODINGS[fps];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
85
|
// Encode as IEEE 754 double precision (8 bytes, little-endian)
|
|
82
86
|
const buffer = Buffer.allocUnsafe(8);
|
|
83
|
-
buffer.writeDoubleLE(fps, 0);
|
|
87
|
+
buffer.writeDoubleLE(snapFrameRate(fps), 0);
|
|
84
88
|
return buffer.toString('hex');
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -755,5 +759,5 @@ module.exports = {
|
|
|
755
759
|
|
|
756
760
|
// Constants
|
|
757
761
|
DEFAULT_PROJECT_SETTINGS,
|
|
758
|
-
|
|
762
|
+
snapFrameRate
|
|
759
763
|
};
|
package/scripts/doctor.py
CHANGED
|
@@ -275,35 +275,71 @@ def resolve_probe(
|
|
|
275
275
|
if not PYTHON.exists():
|
|
276
276
|
return {"import_ok": False, "error": f"{PYTHON} is missing"}
|
|
277
277
|
|
|
278
|
+
# Disposable probe: ask the persistent bridge before importing Blackmagic's
|
|
279
|
+
# native Fusion module. fusionscript can leave a RemoteApp thread alive and
|
|
280
|
+
# crash CPython during interpreter finalization. If native import is needed,
|
|
281
|
+
# flush the result and hard-exit so those unsafe finalizers never run.
|
|
278
282
|
code = f"""
|
|
279
283
|
import json
|
|
284
|
+
import os
|
|
280
285
|
import sys
|
|
281
286
|
sys.path.insert(0, {str(REPO)!r})
|
|
282
287
|
sys.path.insert(0, {str(RESOLVE_MODULES)!r})
|
|
288
|
+
native_import_attempted = False
|
|
283
289
|
try:
|
|
284
|
-
import DaVinciResolveScript as dvr
|
|
285
290
|
from src.utils.resolve_connection import connect_resolve
|
|
286
291
|
except Exception as exc:
|
|
287
292
|
payload = {{"import_ok": False, "error": repr(exc)}}
|
|
288
293
|
else:
|
|
289
294
|
try:
|
|
290
|
-
resolve = connect_resolve(
|
|
295
|
+
resolve = connect_resolve(None)
|
|
291
296
|
except Exception as exc:
|
|
292
297
|
payload = {{
|
|
293
|
-
"import_ok":
|
|
294
|
-
"
|
|
298
|
+
"import_ok": None,
|
|
299
|
+
"import_skipped": True,
|
|
295
300
|
"resolve_connected": False,
|
|
296
301
|
"connection_error": repr(exc),
|
|
297
302
|
}}
|
|
298
303
|
else:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
304
|
+
if resolve is not None:
|
|
305
|
+
payload = {{
|
|
306
|
+
"import_ok": None,
|
|
307
|
+
"import_skipped": True,
|
|
308
|
+
"module": None,
|
|
309
|
+
"resolve_connected": True,
|
|
310
|
+
"transport": "bridge",
|
|
311
|
+
"product": resolve.GetProductName(),
|
|
312
|
+
"version": resolve.GetVersionString(),
|
|
313
|
+
}}
|
|
314
|
+
else:
|
|
315
|
+
native_import_attempted = True
|
|
316
|
+
try:
|
|
317
|
+
import DaVinciResolveScript as dvr
|
|
318
|
+
except Exception as exc:
|
|
319
|
+
payload = {{"import_ok": False, "error": repr(exc)}}
|
|
320
|
+
else:
|
|
321
|
+
try:
|
|
322
|
+
resolve = connect_resolve(dvr)
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
payload = {{
|
|
325
|
+
"import_ok": True,
|
|
326
|
+
"module": getattr(dvr, "__file__", None),
|
|
327
|
+
"resolve_connected": False,
|
|
328
|
+
"connection_error": repr(exc),
|
|
329
|
+
}}
|
|
330
|
+
else:
|
|
331
|
+
payload = {{
|
|
332
|
+
"import_ok": True,
|
|
333
|
+
"module": getattr(dvr, "__file__", None),
|
|
334
|
+
"resolve_connected": bool(resolve),
|
|
335
|
+
"product": resolve.GetProductName() if resolve else None,
|
|
336
|
+
"version": resolve.GetVersionString() if resolve else None,
|
|
337
|
+
}}
|
|
338
|
+
print(json.dumps(payload), flush=True)
|
|
339
|
+
if native_import_attempted:
|
|
340
|
+
sys.stdout.flush()
|
|
341
|
+
sys.stderr.flush()
|
|
342
|
+
os._exit(0)
|
|
307
343
|
"""
|
|
308
344
|
env = {
|
|
309
345
|
**os.environ,
|
|
@@ -481,37 +517,47 @@ def collect(
|
|
|
481
517
|
check(results, "OK" if pyver["ok"] else "FAIL", "Python version", pyver["stdout"] or pyver["stderr"])
|
|
482
518
|
|
|
483
519
|
probe = resolve_probe(resolve_host, resolve_timeout)
|
|
484
|
-
if probe.get("
|
|
520
|
+
if probe.get("import_skipped"):
|
|
521
|
+
check(
|
|
522
|
+
results,
|
|
523
|
+
"OK" if probe.get("resolve_connected") else "INFO",
|
|
524
|
+
"DaVinciResolveScript import",
|
|
525
|
+
"skipped — persistent bridge answered; native Fusion module not loaded"
|
|
526
|
+
if probe.get("resolve_connected")
|
|
527
|
+
else "skipped — bridge mode failed before native Fusion import",
|
|
528
|
+
)
|
|
529
|
+
elif probe.get("import_ok"):
|
|
485
530
|
check(results, "OK", "DaVinciResolveScript import", str(probe.get("module")))
|
|
486
|
-
if probe.get("resolve_connected"):
|
|
487
|
-
detail = f"{probe.get('product')} {probe.get('version')}"
|
|
488
|
-
check(results, "OK", "Resolve scripting connection", detail)
|
|
489
|
-
elif probe.get("connection_error"):
|
|
490
|
-
check(
|
|
491
|
-
results,
|
|
492
|
-
"FAIL",
|
|
493
|
-
"Resolve scripting connection",
|
|
494
|
-
str(probe["connection_error"]),
|
|
495
|
-
)
|
|
496
|
-
else:
|
|
497
|
-
# This is also exactly what the free edition looks like: the module
|
|
498
|
-
# imports fine and scriptapp refuses, because Blackmagic gates
|
|
499
|
-
# *external* scripting to Studio. Sending someone to toggle a
|
|
500
|
-
# preference that cannot help them is a dead end, so name the bridge
|
|
501
|
-
# here too.
|
|
502
|
-
check(
|
|
503
|
-
results,
|
|
504
|
-
"WARN",
|
|
505
|
-
"Resolve scripting connection",
|
|
506
|
-
"Module import worked, but scriptapp returned no object. On Studio: "
|
|
507
|
-
"External scripting = Local, or Network with --resolve-host set to "
|
|
508
|
-
"the Resolve host IP, then restart Resolve. On the FREE edition "
|
|
509
|
-
"external scripting is gated off entirely — use the in-app bridge "
|
|
510
|
-
"(see 'Free-edition bridge' below).",
|
|
511
|
-
)
|
|
512
531
|
else:
|
|
513
532
|
check(results, "FAIL", "DaVinciResolveScript import", str(probe.get("error")))
|
|
514
533
|
|
|
534
|
+
if probe.get("resolve_connected"):
|
|
535
|
+
detail = f"{probe.get('product')} {probe.get('version')}"
|
|
536
|
+
check(results, "OK", "Resolve scripting connection", detail)
|
|
537
|
+
elif probe.get("connection_error"):
|
|
538
|
+
check(
|
|
539
|
+
results,
|
|
540
|
+
"FAIL",
|
|
541
|
+
"Resolve scripting connection",
|
|
542
|
+
str(probe["connection_error"]),
|
|
543
|
+
)
|
|
544
|
+
elif probe.get("import_ok"):
|
|
545
|
+
# This is also exactly what the free edition looks like: the module
|
|
546
|
+
# imports fine and scriptapp refuses, because Blackmagic gates
|
|
547
|
+
# *external* scripting to Studio. Sending someone to toggle a
|
|
548
|
+
# preference that cannot help them is a dead end, so name the bridge
|
|
549
|
+
# here too.
|
|
550
|
+
check(
|
|
551
|
+
results,
|
|
552
|
+
"WARN",
|
|
553
|
+
"Resolve scripting connection",
|
|
554
|
+
"Module import worked, but scriptapp returned no object. On Studio: "
|
|
555
|
+
"External scripting = Local, or Network with --resolve-host set to "
|
|
556
|
+
"the Resolve host IP, then restart Resolve. On the FREE edition "
|
|
557
|
+
"external scripting is gated off entirely — use the in-app bridge "
|
|
558
|
+
"(see 'Free-edition bridge' below).",
|
|
559
|
+
)
|
|
560
|
+
|
|
515
561
|
results.extend(bridge_checks(probe))
|
|
516
562
|
|
|
517
563
|
results.extend(extras_checks())
|
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.2"
|
|
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()}")
|