davinci-resolve-mcp 2.104.6 → 2.104.8
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 +76 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +9 -1
- package/install.py +1 -1
- package/package.json +2 -2
- package/resolve-advanced/package.json +7 -8
- package/resolve-advanced/server/tools/drt.mjs +14 -1
- package/resolve-advanced/vendor/drt-format/drt-builder.js +8 -3
- package/resolve-advanced/vendor/drx-codec/drx-generator.js +3 -3
- package/src/granular/common.py +1 -1
- package/src/server.py +156 -14
- package/src/utils/api_truth.py +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,82 @@
|
|
|
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.8
|
|
6
|
+
|
|
7
|
+
More laps: the aggregation class swept to completion, and the dependency
|
|
8
|
+
stack brought to zero known vulnerabilities.
|
|
9
|
+
|
|
10
|
+
**Four more envelope lies fixed.** An AST sweep for per-op result lists under
|
|
11
|
+
success-shaped envelopes (the class import_from_drp exposed) found four bulk
|
|
12
|
+
tools whose top level ignored their rows: `bulk_set_title_text`,
|
|
13
|
+
`fusion_comp.bulk_set_expressions`, and `bulk_set_inputs` returned bare
|
|
14
|
+
{results, op_count} — all-failed and all-succeeded calls indistinguishable
|
|
15
|
+
without reading every row — and `fusion_comp.add_mask` hard-coded
|
|
16
|
+
success:true over failable input writes, so a mask whose every parameter
|
|
17
|
+
failed to apply read as configured while sitting default-shaped on the clip.
|
|
18
|
+
All four now report success/succeeded/failed with partial warnings, through
|
|
19
|
+
one shared summarizer. Also swept and clean: flow-prescribing remediation
|
|
20
|
+
texts (every named action exists), absolute-belief comments in tests and
|
|
21
|
+
source, and the Python tree for further aggregation suspects (the remaining
|
|
22
|
+
hits are read-only listers).
|
|
23
|
+
|
|
24
|
+
**Dependency stack: 15 advisories to zero.** `npm audit fix` cleared the
|
|
25
|
+
non-breaking set (hono, fast-xml-parser, fast-uri, ip-address, js-yaml and
|
|
26
|
+
friends); adm-zip moved to 0.6.0; the `uuid` dependency is GONE (three call
|
|
27
|
+
sites now use Node's built-in crypto.randomUUID); and sharp moved to 0.35.4,
|
|
28
|
+
which clears four libvips CVEs in the image-decode paths that media QC feeds
|
|
29
|
+
untrusted files into.
|
|
30
|
+
|
|
31
|
+
**Node floor: >=20.9 (warn-don't-block).** sharp 0.35 requires Node 20.9+,
|
|
32
|
+
and Node 18 has been end-of-life since April 2025 — on an 18.20 interpreter
|
|
33
|
+
npm silently produced a BROKEN install (engines-skipped platform binding,
|
|
34
|
+
"up to date", no sharp module, 57 tests gone from the count). Both
|
|
35
|
+
package.json engines now say >=20.9, which warns older interpreters without
|
|
36
|
+
blocking, matching the Python floor policy. CI publishes on Node 24; the
|
|
37
|
+
full Node suite runs green on 20.19 (823 tests, sharp included).
|
|
38
|
+
|
|
39
|
+
## What's New in v2.104.7
|
|
40
|
+
|
|
41
|
+
The DRT import thread, chased to ground by live bisection on Studio 19.1.3.7
|
|
42
|
+
— and the tool had been giving instructions that could not work.
|
|
43
|
+
|
|
44
|
+
**What a .drt import actually requires, measured.** A real Resolve export
|
|
45
|
+
re-imports; the same archive minus ONLY project.xml is refused; removing
|
|
46
|
+
MpFolder.xml or renaming the SeqContainer path changes nothing. Tool-authored
|
|
47
|
+
DRTs fail on two counts: they omit project.xml AND use a flat template
|
|
48
|
+
container schema (<StartFrame>/<StartTC> elements) that Resolve never wrote —
|
|
49
|
+
its native containers are blob-based Sm2TiTrack/Sm2TiVideoClip structures.
|
|
50
|
+
Worse: a refused .drt import can raise a modal error dialog that BLOCKS the
|
|
51
|
+
scripting call indefinitely (observed live — the call neither returns nor
|
|
52
|
+
times out until a human dismisses the dialog), and .drt import names the
|
|
53
|
+
timeline after the FILE, not the container's internal name — a third naming
|
|
54
|
+
authority beside FCP7 (internal name) and OTIO (timelineName option). All of
|
|
55
|
+
it is now a submit-tagged api_truth entry.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- `import_timeline_checked` refuses tool-authored .drt/.drp BEFORE calling
|
|
60
|
+
Resolve — the shape is detectable from the zip alone, and refusing early is
|
|
61
|
+
what prevents the scripting-blocking dialog. The error names the actual
|
|
62
|
+
cause and points at routes that work (OTIO authoring; Resolve's own .drt
|
|
63
|
+
exports) instead of the old media/sanitize misdiagnosis.
|
|
64
|
+
- `import_from_drp` no longer reports success:true when every selected
|
|
65
|
+
timeline failed to import (the discarded-outcome aggregation class); a
|
|
66
|
+
partial import is labeled partial with a warning.
|
|
67
|
+
- Both extractors (`import_from_drp`'s and `drt.extract_from_drp`) now carry
|
|
68
|
+
the source archive's project.xml into the extracted .drt — measured as
|
|
69
|
+
necessary. NOT yet sufficient: a .drp-sourced native container repacked
|
|
70
|
+
with its project.xml was still refused on 19.1.3.7, so extraction-based
|
|
71
|
+
import remains unreliable on this build and is documented as such.
|
|
72
|
+
|
|
73
|
+
### Documented
|
|
74
|
+
|
|
75
|
+
- The .prproj refusal and offline-authoring guidance no longer tell users to
|
|
76
|
+
author a 'drt' and import it — that instruction could never work; they
|
|
77
|
+
point at 'otio'/'edl'. The drt tool and drt-builder docstrings state the
|
|
78
|
+
authored template's actual role (offline/DB workflows, injection, parsing)
|
|
79
|
+
and that real-Resolve exports are the only known-importable .drt files.
|
|
80
|
+
|
|
5
81
|
## What's New in v2.104.6
|
|
6
82
|
|
|
7
83
|
**A correction to the v2.104.2 StartFrame fix — measured against Resolve
|
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.8 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 33 missing capabilities,
|
|
15
|
+
**Totals:** 33 missing capabilities, 41 bugs / unreliable behaviors.
|
|
16
16
|
|
|
17
17
|
The authoritative source is the runtime-queryable `api_truth` ledger
|
|
18
18
|
(`resolve_control api_truth "<query>"`); this document is generated from
|
|
@@ -526,6 +526,14 @@ values, or automation-hostile modal prompts.
|
|
|
526
526
|
- **Reference:** [issue #171](https://github.com/samuelgursky/davinci-resolve-mcp/issues/171)
|
|
527
527
|
- **Tags:** timeline, import, silent-failure, unreliable-return
|
|
528
528
|
|
|
529
|
+
### MediaPool.ImportTimelineFromFile (.drt requirements and filename naming)
|
|
530
|
+
|
|
531
|
+
- **Object:** `MediaPool`
|
|
532
|
+
- **Signature:** `(drtPath, {importSourceClips, ...}) -> Timeline`
|
|
533
|
+
- **Behavior:** Measured by bisection on Studio 19.1.3.7 against a real .drt export: (1) the archive MUST contain project.xml — the same archive minus only that entry is refused, while removing MediaPool/MpFolder.xml or renaming the SeqContainer path changes nothing; (2) the container XML must be Resolve's native blob-based schema — a syntactic SeqContainer with flat template elements is refused even beside a genuine project.xml; (3) the imported timeline is named after the FILE (real_2997.drt -> 'real_2997'), not the container's internal name — a third naming authority beside FCP7 (internal name wins) and OTIO (timelineName option wins); and (4) a refused import can raise a modal error dialog that BLOCKS the scripting call until a human dismisses it — the call neither returns nor times out.
|
|
534
|
+
- **Workaround / current handling:** Import only .drt files Resolve itself exported — the sufficient set is unmapped beyond that: even a .drp-sourced native container repacked WITH its project.xml was refused (measured 19.1.3.7), so extraction-based .drt import cannot be relied on either. Name the timeline by naming the FILE. Never batch speculative .drt imports unattended — one refusal can hold the whole session hostage behind its dialog. timeline.import_timeline_checked detects the authored template shape up front and refuses with this diagnosis instead of importing; for a reliable programmatic import use OTIO or FCP7 XML.
|
|
535
|
+
- **Tags:** timeline, import, silent-failure, headless
|
|
536
|
+
|
|
529
537
|
### Timeline.DeleteClips (requires the Edit page; flaky first attempt)
|
|
530
538
|
|
|
531
539
|
- **Object:** `Timeline`
|
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.8"
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "davinci-resolve-mcp",
|
|
3
|
-
"version": "2.104.
|
|
3
|
+
"version": "2.104.8",
|
|
4
4
|
"description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Samuel Gursky <samgursky@gmail.com>",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"post-production"
|
|
67
67
|
],
|
|
68
68
|
"engines": {
|
|
69
|
-
"node": ">=
|
|
69
|
+
"node": ">=20.9"
|
|
70
70
|
},
|
|
71
71
|
"publishConfig": {
|
|
72
72
|
"access": "public"
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"version": "0.0.0-dev",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"description": "Beyond-the-API DaVinci Resolve MCP
|
|
7
|
-
"comment_commonjs": "Intentionally NO \"type\": \"module\"
|
|
8
|
-
"comment_exports": "Public library surface so a downstream consumer can import the codec/grading/pipeline API, not just spawn the MCP bin. `.` = the lib barrel; `./mcp` = the stdio server entry. Still private:true
|
|
6
|
+
"description": "Beyond-the-API DaVinci Resolve MCP \u2014 offline .drp/.drt/.drx authoring/editing. Sibling Node server to the Python davinci-resolve-mcp. Dev-local package.json; deps promote to the repo root at packaging.",
|
|
7
|
+
"comment_commonjs": "Intentionally NO \"type\": \"module\" \u2014 the vendored libs are CommonJS (require()). The repo root is type:module, so this nested manifest scopes resolve-advanced/**/*.js back to CommonJS.",
|
|
8
|
+
"comment_exports": "Public library surface so a downstream consumer can import the codec/grading/pipeline API, not just spawn the MCP bin. `.` = the lib barrel; `./mcp` = the stdio server entry. Still private:true \u2014 publish name/registry + un-privating are a parked decision.",
|
|
9
9
|
"main": "./server/lib.mjs",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./server/lib.mjs",
|
|
@@ -20,11 +20,10 @@
|
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
23
|
-
"adm-zip": "^0.
|
|
23
|
+
"adm-zip": "^0.6.0",
|
|
24
24
|
"fast-xml-parser": "^5.7.3",
|
|
25
25
|
"fzstd": "^0.1.1",
|
|
26
26
|
"jszip": "^3.10.1",
|
|
27
|
-
"uuid": "^9.0.0",
|
|
28
27
|
"zod": "^3.25.76",
|
|
29
28
|
"zstd-codec": "^0.1.5"
|
|
30
29
|
},
|
|
@@ -32,10 +31,10 @@
|
|
|
32
31
|
"better-sqlite3": "^11.10.0",
|
|
33
32
|
"js-yaml": "^4.3.0",
|
|
34
33
|
"pg": "^8.22.0",
|
|
35
|
-
"sharp": "^0.
|
|
34
|
+
"sharp": "^0.35.4"
|
|
36
35
|
},
|
|
37
36
|
"peerDependenciesMeta": {
|
|
38
|
-
"_comment": "audio (split/trim/convert) + conform frame ops need system ffmpeg/ffprobe on PATH (NOT bundled
|
|
37
|
+
"_comment": "audio (split/trim/convert) + conform frame ops need system ffmpeg/ffprobe on PATH (NOT bundled \u2014 FFmpeg binaries are GPL; this package stays MIT). Install ffmpeg yourself: brew install ffmpeg."
|
|
39
38
|
},
|
|
40
39
|
"scripts": {
|
|
41
40
|
"test": "node --test vendor/drp-format/__tests__/*.test.js vendor/drt-format/__tests__/*.test.js vendor/drx-codec/__tests__/*.test.js test/*.test.mjs",
|
|
@@ -43,7 +42,7 @@
|
|
|
43
42
|
"test:server": "node --test test/*.test.mjs"
|
|
44
43
|
},
|
|
45
44
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
45
|
+
"node": ">=20.9"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"prettier": "^3.9.4"
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* drt tool — DaVinci Resolve Timeline (.drt) format. All actions local/offline.
|
|
3
3
|
*
|
|
4
4
|
* parse — .drt/.drp path → { timelines, metadata, seqContainers }
|
|
5
|
-
* author — spec → .drt bytes written to outputPath
|
|
5
|
+
* author — spec → .drt bytes written to outputPath. TEMPLATE SCHEMA: the
|
|
6
|
+
* output serves offline/DB workflows (inject_into_drp, parsing, diffing);
|
|
7
|
+
* Resolve's ImportTimelineFromFile REFUSES it (measured 19.1.3.7 — the
|
|
8
|
+
* native format is blob-based with a project.xml this shape lacks). For a
|
|
9
|
+
* file Resolve imports, author OTIO, or extract a REAL container with
|
|
10
|
+
* extract_from_drp.
|
|
6
11
|
* validate — .drt path → { valid, errors }
|
|
7
12
|
* inject_into_drp — graft a .drt's SeqContainers into an existing .drp
|
|
8
13
|
* extract_from_drp — pull a SeqContainer out of a .drp as a .drt
|
|
@@ -154,6 +159,14 @@ export const drtTool = {
|
|
|
154
159
|
const xml = await drpZip.file(seqEntries[idx]).async('string');
|
|
155
160
|
const out = new JSZip();
|
|
156
161
|
out.file('Primary1/SeqContainer1.xml', xml);
|
|
162
|
+
// ImportTimelineFromFile refuses a .drt without project.xml (measured
|
|
163
|
+
// by bisection on Studio 19.1.3.7), so carry the source's over.
|
|
164
|
+
const projectEntry = Object.keys(drpZip.files).find(
|
|
165
|
+
(n) => n === 'project.xml' || n.endsWith('/project.xml'),
|
|
166
|
+
);
|
|
167
|
+
if (projectEntry) {
|
|
168
|
+
out.file('project.xml', await drpZip.file(projectEntry).async('string'));
|
|
169
|
+
}
|
|
157
170
|
out.file(
|
|
158
171
|
'metadata.json',
|
|
159
172
|
JSON.stringify(
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DRT builder — package a timeline-only Resolve archive.
|
|
3
3
|
*
|
|
4
|
-
* DRT and DRP share the SeqContainer/MpFolder schema
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* DRT and DRP share the SeqContainer/MpFolder schema. NOTE (measured on
|
|
5
|
+
* Studio 19.1.3.7): real Resolve .drt exports DO include a project.xml, and
|
|
6
|
+
* ImportTimelineFromFile refuses archives without one — and refuses this
|
|
7
|
+
* module's template-schema containers regardless (Resolve's native container
|
|
8
|
+
* is blob-based Sm2TiTrack/Sm2TiVideoClip, not the flat elements written
|
|
9
|
+
* here). buildDRT output therefore serves offline/DB workflows (injection,
|
|
10
|
+
* parsing, diffing), not direct live import. This module reuses drp-format's
|
|
11
|
+
* packageFullDRP with includeProjectXml: false.
|
|
7
12
|
*
|
|
8
13
|
* Spec shape — same as buildDRP minus the project shell:
|
|
9
14
|
* {
|
|
@@ -34,7 +34,7 @@ try {
|
|
|
34
34
|
} catch {
|
|
35
35
|
ZstdCodec = null;
|
|
36
36
|
}
|
|
37
|
-
const {
|
|
37
|
+
const { randomUUID: uuidv4 } = require('node:crypto');
|
|
38
38
|
|
|
39
39
|
// Import shared DRX parameter library
|
|
40
40
|
const drxParams = require('../drx-parameters');
|
|
@@ -1859,7 +1859,7 @@ async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
|
|
|
1859
1859
|
const bodyHex = body.toString('hex');
|
|
1860
1860
|
|
|
1861
1861
|
// Generate UUIDs
|
|
1862
|
-
const {
|
|
1862
|
+
const { randomUUID: uuidv4 } = require('node:crypto');
|
|
1863
1863
|
const stillId = uuidv4();
|
|
1864
1864
|
const clipVersionId = uuidv4();
|
|
1865
1865
|
|
|
@@ -1873,7 +1873,7 @@ async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
|
|
|
1873
1873
|
${pTrackVerXml}`;
|
|
1874
1874
|
} else {
|
|
1875
1875
|
// Generate default pTrackVer body
|
|
1876
|
-
const trackVersionId = require('
|
|
1876
|
+
const trackVersionId = require('node:crypto').randomUUID();
|
|
1877
1877
|
const trackBodyHex = await generatePTrackVerBody(width, height);
|
|
1878
1878
|
pTrackVerSection = `
|
|
1879
1879
|
<pTrackVer>
|
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.8"
|
|
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.104.
|
|
14
|
+
VERSION = "2.104.8"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -624,8 +624,10 @@ reverse-subclip repair, lineage, and grade tracing with no Resolve open.
|
|
|
624
624
|
|
|
625
625
|
- Live: timeline probe_timeline_structure / detect_gaps_overlaps /
|
|
626
626
|
source_range_report / conform_boundary_report; export_timeline_checked /
|
|
627
|
-
import_timeline_checked (drt
|
|
628
|
-
|
|
627
|
+
import_timeline_checked (a REAL-Resolve .drt — exported by Resolve or pulled
|
|
628
|
+
out with drt.extract_from_drp — is the only lossless project-native
|
|
629
|
+
round-trip; tool-AUTHORED .drt files use a template schema Resolve's import
|
|
630
|
+
refuses; EDL/FCPXML drop relationships); detect_missing_media -> build_relink_plan
|
|
629
631
|
(read-only, bounded) -> media_pool.safe_relink with approved paths only.
|
|
630
632
|
- XML import via the scripting API goes OFFLINE (missing-media/generators abort);
|
|
631
633
|
use import_timeline_checked with media sanitize (FCP7/FCPXML), then exact-path
|
|
@@ -5785,6 +5787,35 @@ def _timeline_set_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
5785
5787
|
}
|
|
5786
5788
|
|
|
5787
5789
|
|
|
5790
|
+
def _summarize_bulk_results(results: List[Dict[str, Any]], op_count: int) -> Dict[str, Any]:
|
|
5791
|
+
"""Envelope for per-op result lists whose top level must reflect the rows.
|
|
5792
|
+
|
|
5793
|
+
A bare {results, op_count} envelope made all-failed and all-succeeded
|
|
5794
|
+
calls indistinguishable to a caller reading only the top level (the same
|
|
5795
|
+
aggregation class as import_from_drp reporting success over failed rows).
|
|
5796
|
+
"""
|
|
5797
|
+
succeeded = sum(1 for row in results if row.get("success"))
|
|
5798
|
+
failed = op_count - succeeded
|
|
5799
|
+
out: Dict[str, Any] = {
|
|
5800
|
+
"results": results,
|
|
5801
|
+
"op_count": op_count,
|
|
5802
|
+
"succeeded": succeeded,
|
|
5803
|
+
"failed": failed,
|
|
5804
|
+
"success": failed == 0,
|
|
5805
|
+
}
|
|
5806
|
+
if failed and succeeded:
|
|
5807
|
+
out["partial"] = True
|
|
5808
|
+
out["warning"] = (
|
|
5809
|
+
f"{succeeded} of {op_count} ops succeeded; see results[].error "
|
|
5810
|
+
"for the failures."
|
|
5811
|
+
)
|
|
5812
|
+
elif failed:
|
|
5813
|
+
out["error"] = (
|
|
5814
|
+
f"All {op_count} op(s) failed — see results[].error for each cause."
|
|
5815
|
+
)
|
|
5816
|
+
return out
|
|
5817
|
+
|
|
5818
|
+
|
|
5788
5819
|
def _timeline_bulk_set_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
5789
5820
|
ops = p.get("ops")
|
|
5790
5821
|
if not isinstance(ops, list) or not ops:
|
|
@@ -5799,7 +5830,7 @@ def _timeline_bulk_set_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
5799
5830
|
out = _timeline_set_title_text(tl, merged)
|
|
5800
5831
|
out["index"] = index
|
|
5801
5832
|
results.append(out)
|
|
5802
|
-
return
|
|
5833
|
+
return _summarize_bulk_results(results, len(ops))
|
|
5803
5834
|
|
|
5804
5835
|
|
|
5805
5836
|
_TIMELINE_CONFORM_KERNEL_ACTIONS = [
|
|
@@ -7016,11 +7047,14 @@ _PRPROJ_REFUSAL = (
|
|
|
7016
7047
|
"XML, not an interchange). Two offline routes, no Premiere needed: (1) read it with the "
|
|
7017
7048
|
"advanced MCP — editorial.list_sequences / editorial.parse_interchange (format 'prproj'); "
|
|
7018
7049
|
"(2) convert it to an importable interchange — editorial.convert_to_interchange "
|
|
7019
|
-
"(target 'otio'|'edl'
|
|
7050
|
+
"(target 'otio'|'edl') — then import that here with import_timeline_checked. "
|
|
7020
7051
|
"Editorial timing/cuts/transitions carry over; per-clip effects/Lumetri color do not. "
|
|
7021
|
-
"Speed/reverse carry on 'otio' and 'edl' ONLY
|
|
7022
|
-
"
|
|
7023
|
-
"
|
|
7052
|
+
"Speed/reverse carry on 'otio' and 'edl' ONLY. Do NOT pick target 'drt' for a live "
|
|
7053
|
+
"import: tool-AUTHORED .drt files use a template schema that "
|
|
7054
|
+
"ImportTimelineFromFile refuses (measured on 19.1.3.7 — Resolve's native "
|
|
7055
|
+
"container format is blob-based and includes a project.xml the authored "
|
|
7056
|
+
"shape lacks); authored DRTs are for offline/DB workflows, and 'drt' also "
|
|
7057
|
+
"flattens every retime to 100% forward (reported in `flattened`). "
|
|
7024
7058
|
"Alternatively export FCP7 XML / AAF / FCPXML from Premiere and conform that."
|
|
7025
7059
|
)
|
|
7026
7060
|
|
|
@@ -7071,6 +7105,33 @@ def _rewrite_fcp7_sequence_name(xml_path: str, new_name: str):
|
|
|
7071
7105
|
return out_path, True, None
|
|
7072
7106
|
|
|
7073
7107
|
|
|
7108
|
+
def _drt_looks_tool_authored(path: str) -> Optional[str]:
|
|
7109
|
+
"""Name the tell when a .drt/.drp is the authored TEMPLATE shape, else None.
|
|
7110
|
+
|
|
7111
|
+
Real Resolve archives carry a project.xml and blob-based SeqContainers
|
|
7112
|
+
(Sm2TiTrack/FieldsBlob children, clip <Start> elements). The tool-authored
|
|
7113
|
+
template instead writes flat container-level <StartFrame>/<StartTC>
|
|
7114
|
+
elements and omits project.xml — and ImportTimelineFromFile refuses both
|
|
7115
|
+
of those (measured: a real export re-imports; the same archive minus
|
|
7116
|
+
project.xml, or with an authored container, does not).
|
|
7117
|
+
"""
|
|
7118
|
+
import zipfile
|
|
7119
|
+
try:
|
|
7120
|
+
with zipfile.ZipFile(path) as zf:
|
|
7121
|
+
names = zf.namelist()
|
|
7122
|
+
has_project_xml = any(n == "project.xml" or n.endswith("/project.xml") for n in names)
|
|
7123
|
+
for n in names:
|
|
7124
|
+
if _SEQ_CONTAINER_RE.search(n or "") and n.endswith(".xml"):
|
|
7125
|
+
head = zf.read(n)[:4096].decode("utf-8", "replace")
|
|
7126
|
+
if "<StartFrame>" in head or "<StartTC>" in head:
|
|
7127
|
+
return "flat container-level <StartFrame>/<StartTC> elements"
|
|
7128
|
+
if not has_project_xml:
|
|
7129
|
+
return "no project.xml in the archive"
|
|
7130
|
+
except Exception:
|
|
7131
|
+
return None
|
|
7132
|
+
return None
|
|
7133
|
+
|
|
7134
|
+
|
|
7074
7135
|
def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
7075
7136
|
path = p.get("path")
|
|
7076
7137
|
if not path:
|
|
@@ -7080,6 +7141,31 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
7080
7141
|
ext = os.path.splitext(path)[1].lower()
|
|
7081
7142
|
if ext == ".prproj":
|
|
7082
7143
|
return _err(_PRPROJ_REFUSAL, category="invalid_input")
|
|
7144
|
+
if ext in {".drt", ".drp"}:
|
|
7145
|
+
# Refuse BEFORE calling Resolve: a refused .drt import can raise a
|
|
7146
|
+
# modal error dialog that blocks the scripting call indefinitely
|
|
7147
|
+
# (measured live — the call neither returns nor times out until a
|
|
7148
|
+
# human dismisses the dialog), so a post-failure diagnosis may never
|
|
7149
|
+
# run. The authored template shape is detectable from the zip alone.
|
|
7150
|
+
authored_shape = _drt_looks_tool_authored(path)
|
|
7151
|
+
if authored_shape:
|
|
7152
|
+
return _err(
|
|
7153
|
+
f"This {ext} carries the tool-AUTHORED template schema "
|
|
7154
|
+
f"({authored_shape}), which Resolve's ImportTimelineFromFile "
|
|
7155
|
+
"refuses — the native format is blob-based and includes a "
|
|
7156
|
+
"project.xml (measured on Studio 19.1.3.7). Worse, the refusal "
|
|
7157
|
+
"raises a modal dialog that blocks all scripting until a human "
|
|
7158
|
+
"dismisses it, so this import is not attempted at all.",
|
|
7159
|
+
category="invalid_input",
|
|
7160
|
+
remediation=(
|
|
7161
|
+
"Authored DRTs serve offline/DB workflows, not live import. "
|
|
7162
|
+
"For a live import, author OTIO instead "
|
|
7163
|
+
"(editorial.convert_to_interchange target 'otio' imports "
|
|
7164
|
+
"cleanly), or import a REAL Resolve .drt — one Resolve "
|
|
7165
|
+
"exported, or one pulled from a .drp with "
|
|
7166
|
+
"drt.extract_from_drp."
|
|
7167
|
+
),
|
|
7168
|
+
)
|
|
7083
7169
|
# ImportTimelineFromFile silently no-ops on the never-saved default project: it returns
|
|
7084
7170
|
# nothing, creates no timeline, and reports no cause. The generic "Resolve created no
|
|
7085
7171
|
# timeline" error that came back instead sent people to source-clip resolution and
|
|
@@ -7379,13 +7465,26 @@ def _drp_seq_containers(zf) -> List[Dict[str, Any]]:
|
|
|
7379
7465
|
|
|
7380
7466
|
|
|
7381
7467
|
def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str) -> None:
|
|
7382
|
-
"""Write a
|
|
7468
|
+
"""Write a .drt (zip) holding one SeqContainer plus the source's project.xml.
|
|
7469
|
+
|
|
7470
|
+
ImportTimelineFromFile refuses a .drt without project.xml (measured by
|
|
7471
|
+
bisection on Studio 19.1.3.7: a real export minus only project.xml is
|
|
7472
|
+
refused; project.xml + container with no MpFolder imports). The source
|
|
7473
|
+
.drp/.drt carries one — copy it, or the extract cannot import.
|
|
7474
|
+
"""
|
|
7383
7475
|
import zipfile
|
|
7384
7476
|
|
|
7385
7477
|
with zipfile.ZipFile(drp_path, "r") as zf:
|
|
7386
7478
|
xml = zf.read(seq_entry)
|
|
7479
|
+
project_xml = None
|
|
7480
|
+
for candidate in zf.namelist():
|
|
7481
|
+
if candidate == "project.xml" or candidate.endswith("/project.xml"):
|
|
7482
|
+
project_xml = zf.read(candidate)
|
|
7483
|
+
break
|
|
7387
7484
|
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as out:
|
|
7388
7485
|
out.writestr("Primary1/SeqContainer1.xml", xml)
|
|
7486
|
+
if project_xml is not None:
|
|
7487
|
+
out.writestr("project.xml", project_xml)
|
|
7389
7488
|
out.writestr(
|
|
7390
7489
|
"metadata.json",
|
|
7391
7490
|
json.dumps(
|
|
@@ -7494,7 +7593,10 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
|
7494
7593
|
imported_count += 1
|
|
7495
7594
|
results.append(entry)
|
|
7496
7595
|
|
|
7497
|
-
|
|
7596
|
+
# Top-level success must reflect the rows: _ok() over a result list whose
|
|
7597
|
+
# every import failed read as a successful call (the #161 aggregation
|
|
7598
|
+
# class — the wrapper reported what it meant to do, not what happened).
|
|
7599
|
+
out = _ok(
|
|
7498
7600
|
drpPath=drp_path,
|
|
7499
7601
|
selected=len(selected),
|
|
7500
7602
|
imported=imported_count,
|
|
@@ -7502,6 +7604,19 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
|
7502
7604
|
results=results,
|
|
7503
7605
|
dry_run=bool(p.get("dry_run")),
|
|
7504
7606
|
)
|
|
7607
|
+
if not p.get("dry_run") and selected and imported_count == 0:
|
|
7608
|
+
out["success"] = False
|
|
7609
|
+
out["error"] = (
|
|
7610
|
+
f"None of the {len(selected)} selected timeline(s) imported — "
|
|
7611
|
+
"see results[].error for each cause."
|
|
7612
|
+
)
|
|
7613
|
+
elif not p.get("dry_run") and imported_count < len(selected):
|
|
7614
|
+
out["partial"] = True
|
|
7615
|
+
out["warning"] = (
|
|
7616
|
+
f"{imported_count} of {len(selected)} selected timeline(s) "
|
|
7617
|
+
"imported; see results[].error for the failures."
|
|
7618
|
+
)
|
|
7619
|
+
return out
|
|
7505
7620
|
|
|
7506
7621
|
|
|
7507
7622
|
def _timeline_by_selector(proj, p: Dict[str, Any], *, prefix: str):
|
|
@@ -23761,7 +23876,12 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
23761
23876
|
metric; a conflicting relink is VETOED — reverted and reported under
|
|
23762
23877
|
`sanitize.flagged` for human review, never silently applied. verify_threshold
|
|
23763
23878
|
(default 0.90) sets the structural-match bar.
|
|
23764
|
-
import_from_drp(drpPath, timelineNames?|timelineIndexes?, import_source_clips?, dry_run?) -> {success, selected, imported, available, results}
|
|
23879
|
+
import_from_drp(drpPath, timelineNames?|timelineIndexes?, import_source_clips?, dry_run?) -> {success, selected, imported, available, results, partial?}
|
|
23880
|
+
CAVEAT (measured 19.1.3.7): Resolve refuses extracted containers even
|
|
23881
|
+
repacked with the source's project.xml — the sufficient import set
|
|
23882
|
+
beyond Resolve's own .drt exports is unmapped, so expect failures on
|
|
23883
|
+
this build and read results[].error; success reflects the rows. For a
|
|
23884
|
+
reliable programmatic import use OTIO or FCP7 XML.
|
|
23765
23885
|
Extract chosen timelines from a .drp (offline zip surgery → temp .drt each) and
|
|
23766
23886
|
import each into the running Resolve. Omit the selector to import ALL. Enumerate
|
|
23767
23887
|
first with the advanced MCP drt.list_sequences / editorial.list_sequences to get
|
|
@@ -27494,7 +27614,7 @@ def _fusion_comp_bulk_set_expressions(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
27494
27614
|
results.append({"index": index, "error": error_message})
|
|
27495
27615
|
elif keep_undo:
|
|
27496
27616
|
results.append({"index": index, "success": True, "expression": op["expression"]})
|
|
27497
|
-
return
|
|
27617
|
+
return _summarize_bulk_results(results, len(ops))
|
|
27498
27618
|
|
|
27499
27619
|
|
|
27500
27620
|
def _fusion_probe_group_published_inputs(comp, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -27619,7 +27739,7 @@ def _fusion_comp_bulk_set_inputs(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
27619
27739
|
elif keep_undo:
|
|
27620
27740
|
results.append({"index": index, "success": True})
|
|
27621
27741
|
|
|
27622
|
-
return
|
|
27742
|
+
return _summarize_bulk_results(results, len(ops))
|
|
27623
27743
|
|
|
27624
27744
|
|
|
27625
27745
|
_FUSION_KERNEL_ACTIONS = [
|
|
@@ -28024,12 +28144,28 @@ def _fusion_add_mask(comp, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
28024
28144
|
rec["error"] = str(exc)
|
|
28025
28145
|
results.append(rec)
|
|
28026
28146
|
|
|
28147
|
+
failed_inputs = [row for row in results if not row.get("success")]
|
|
28027
28148
|
out: Dict[str, Any] = {
|
|
28028
|
-
|
|
28149
|
+
# The tool was added either way, but success must mean "the mask that
|
|
28150
|
+
# was asked for": an unconditional True over failed input rows left a
|
|
28151
|
+
# default-shaped mask reading as configured.
|
|
28152
|
+
"success": not failed_inputs or len(failed_inputs) < len(results),
|
|
28029
28153
|
"tool_name": tool_name,
|
|
28030
28154
|
"tool_type": attrs.get("TOOLS_RegID", tool_type),
|
|
28031
28155
|
"inputs_set": results,
|
|
28032
28156
|
}
|
|
28157
|
+
if failed_inputs and len(failed_inputs) == len(results):
|
|
28158
|
+
out["success"] = False
|
|
28159
|
+
out["error"] = (
|
|
28160
|
+
f"The {tool_name} tool was added but all {len(results)} input "
|
|
28161
|
+
"write(s) failed — the mask is default-shaped; see inputs_set[].error."
|
|
28162
|
+
)
|
|
28163
|
+
elif failed_inputs:
|
|
28164
|
+
out["partial"] = True
|
|
28165
|
+
out["warning"] = (
|
|
28166
|
+
f"{len(failed_inputs)} of {len(results)} mask input(s) failed to "
|
|
28167
|
+
"apply; see inputs_set[].error."
|
|
28168
|
+
)
|
|
28033
28169
|
|
|
28034
28170
|
# Optional wiring: connect this mask into a tool's mask input.
|
|
28035
28171
|
connect_to = p.get("connect_to")
|
|
@@ -28051,6 +28187,12 @@ def _fusion_add_mask(comp, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
28051
28187
|
}
|
|
28052
28188
|
except Exception as exc:
|
|
28053
28189
|
out["connection"] = {"success": False, "error": str(exc)}
|
|
28190
|
+
if isinstance(out.get("connection"), dict) and not out["connection"].get("success"):
|
|
28191
|
+
out["success"] = False
|
|
28192
|
+
out.setdefault(
|
|
28193
|
+
"error",
|
|
28194
|
+
"The mask was added but could not be connected — see `connection`.",
|
|
28195
|
+
)
|
|
28054
28196
|
return out
|
|
28055
28197
|
|
|
28056
28198
|
|
package/src/utils/api_truth.py
CHANGED
|
@@ -1879,6 +1879,39 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1879
1879
|
"submit": "bug",
|
|
1880
1880
|
"issue": 171,
|
|
1881
1881
|
},
|
|
1882
|
+
{
|
|
1883
|
+
"symbol": "MediaPool.ImportTimelineFromFile (.drt requirements and filename naming)",
|
|
1884
|
+
"object": "MediaPool",
|
|
1885
|
+
"signature": "(drtPath, {importSourceClips, ...}) -> Timeline",
|
|
1886
|
+
"reality": "Measured by bisection on Studio 19.1.3.7 against a real "
|
|
1887
|
+
".drt export: (1) the archive MUST contain project.xml — "
|
|
1888
|
+
"the same archive minus only that entry is refused, while "
|
|
1889
|
+
"removing MediaPool/MpFolder.xml or renaming the "
|
|
1890
|
+
"SeqContainer path changes nothing; (2) the container XML "
|
|
1891
|
+
"must be Resolve's native blob-based schema — a syntactic "
|
|
1892
|
+
"SeqContainer with flat template elements is refused even "
|
|
1893
|
+
"beside a genuine project.xml; (3) the imported timeline is "
|
|
1894
|
+
"named after the FILE (real_2997.drt -> 'real_2997'), not "
|
|
1895
|
+
"the container's internal name — a third naming authority "
|
|
1896
|
+
"beside FCP7 (internal name wins) and OTIO (timelineName "
|
|
1897
|
+
"option wins); and (4) a refused import can raise a modal "
|
|
1898
|
+
"error dialog that BLOCKS the scripting call until a human "
|
|
1899
|
+
"dismisses it — the call neither returns nor times out.",
|
|
1900
|
+
"recommended": "Import only .drt files Resolve itself exported — "
|
|
1901
|
+
"the sufficient set is unmapped beyond that: even a "
|
|
1902
|
+
".drp-sourced native container repacked WITH its "
|
|
1903
|
+
"project.xml was refused (measured 19.1.3.7), so "
|
|
1904
|
+
"extraction-based .drt import cannot be relied on "
|
|
1905
|
+
"either. Name the timeline by naming the FILE. Never "
|
|
1906
|
+
"batch speculative .drt imports unattended — one "
|
|
1907
|
+
"refusal can hold the whole session hostage behind "
|
|
1908
|
+
"its dialog. timeline.import_timeline_checked detects "
|
|
1909
|
+
"the authored template shape up front and refuses "
|
|
1910
|
+
"with this diagnosis instead of importing; for a "
|
|
1911
|
+
"reliable programmatic import use OTIO or FCP7 XML.",
|
|
1912
|
+
"tags": ["timeline", "import", "silent-failure", "headless"],
|
|
1913
|
+
"submit": "bug",
|
|
1914
|
+
},
|
|
1882
1915
|
{
|
|
1883
1916
|
"symbol": "MediaPool.AppendToTimeline clipInfo recordFrame (timeline-absolute origin)",
|
|
1884
1917
|
"object": "MediaPool",
|