davinci-resolve-mcp 2.104.9 → 2.105.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/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +3 -3
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/db-patch.mjs +48 -5
- package/resolve-advanced/server/tools/drt.mjs +114 -13
- package/resolve-advanced/server/tools/project_db.mjs +19 -9
- package/src/granular/common.py +1 -1
- package/src/server.py +96 -37
- package/src/utils/api_truth.py +38 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,84 @@
|
|
|
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.105.0
|
|
6
|
+
|
|
7
|
+
**Native-schema DRT authoring — the parked "project, not a lap" — shipped.**
|
|
8
|
+
Tool-authored .drt files that Resolve's ImportTimelineFromFile actually
|
|
9
|
+
accepts, live-verified end to end on Studio 19.1.3.7.
|
|
10
|
+
|
|
11
|
+
The door was already half-open: the repo's template-splice engine
|
|
12
|
+
(assembleTimeline + the real Resolve-21 empty-project capture) authors
|
|
13
|
+
native-schema .drp archives, and the final bisection showed a .drt IS a .drp
|
|
14
|
+
that ImportTimelineFromFile accepts. What stood between them was the version
|
|
15
|
+
gate (the template stamps DbPrjVer 17; a 19.1.3 host wants 14) and a set of
|
|
16
|
+
extraction traps nobody had mapped.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`drt.assemble`** — spec → importable native-schema .drt (titles,
|
|
21
|
+
generators, transitions), with `targetAppVersion` stamping for pre-21
|
|
22
|
+
hosts. Live-verified: assembled archives import with every element intact.
|
|
23
|
+
- **`drt.extract_from_drp` rebuilt on the measured recipe**: keep
|
|
24
|
+
project.xml + MediaPool + the SeqContainer at its ORIGINAL uuid path, drop
|
|
25
|
+
Gallery, and remove other timelines' Sm2MpTimelineClip blocks (matched via
|
|
26
|
+
the kept container's track Sequence DbIds) so they don't arrive as ghost
|
|
27
|
+
empty timelines. The Python extractor behind `timeline.import_from_drp`
|
|
28
|
+
implements the same recipe. Live-verified: single-timeline extracts from a
|
|
29
|
+
two-timeline project import cleanly, one timeline, clips intact.
|
|
30
|
+
|
|
31
|
+
### The .drt import contract, fully mapped (api_truth rewritten)
|
|
32
|
+
|
|
33
|
+
A whole saved-project export renamed .drt imports, clips intact.
|
|
34
|
+
Requirements: project.xml; MpFolder.xml (it holds the Sm2Sequence/Sm2Timeline
|
|
35
|
+
objects); the SeqContainer's ORIGINAL uuid path — renaming it "succeeds"
|
|
36
|
+
with an EMPTY timeline, no error, the nastiest variant; version stamps at or
|
|
37
|
+
below the host; native blob schema; and a SAVED source project —
|
|
38
|
+
ExportProject snapshots the saved DB state, so an unsaved timeline exports
|
|
39
|
+
empty tracks (the trap that produced v2.104.7's "necessary but not
|
|
40
|
+
sufficient" verdict, now corrected). Every Sm2MpTimelineClip block imports
|
|
41
|
+
as a timeline; extras arrive as ghosts unless removed.
|
|
42
|
+
|
|
43
|
+
### Fixed in passing
|
|
44
|
+
|
|
45
|
+
- **project_db lookups can no longer hang on an unresponsive library root.**
|
|
46
|
+
Mid-session, macOS rendered the Lite sandbox container path unresponsive at
|
|
47
|
+
the filesystem level (`ls` itself hung) — which froze the Node test suite
|
|
48
|
+
and would have frozen every projectName lookup. Roots are now probed with a
|
|
49
|
+
deadline (`responsiveRoots`); unresponsive ones are skipped and NAMED in
|
|
50
|
+
the not-found error. The root-walking tests are hermetic now — suites must
|
|
51
|
+
not depend on machine paths that an OS can wedge.
|
|
52
|
+
- The flat-authored-shape refusal in import_timeline_checked now points at
|
|
53
|
+
`drt.assemble` as the importable authoring route.
|
|
54
|
+
|
|
55
|
+
## What's New in v2.104.10
|
|
56
|
+
|
|
57
|
+
Stones turned on the live-validation backlog, on Studio 19.1.3.7.
|
|
58
|
+
|
|
59
|
+
**AAF live import, validated at last** (marked "NOT live-validated" since
|
|
60
|
+
2026-07-06): an EXPORT_AAF/EXPORT_AAF_NEW round trip imports cleanly with
|
|
61
|
+
importSourceClips=false, lands fully offline (the documented turnover shape),
|
|
62
|
+
and preserves the start timecode. The naming matrix across import formats is
|
|
63
|
+
now complete and in api_truth — FCP7 XML ignores timelineName (internal name
|
|
64
|
+
wins, the #171 trap); AAF honours timelineName when given, else its internal
|
|
65
|
+
name; OTIO honours timelineName; .drt names the timeline after the file. Only
|
|
66
|
+
FCP7 exhibits the returned-existing trap. The AAF post-import relink leg was
|
|
67
|
+
exercised too: under importSourceClips=false it correctly reports "no Media
|
|
68
|
+
Pool Items to relink" — the API relinks pool items, and none exist on that
|
|
69
|
+
path — so its precondition is now stated instead of assumed.
|
|
70
|
+
|
|
71
|
+
**safe_quick_export verifies its output.** RenderWithQuickExport's status
|
|
72
|
+
dict was the last render surface trusted without a file check: a success
|
|
73
|
+
status that wrote nothing read as an export. The files that actually landed
|
|
74
|
+
in TargetDir are now listed with size and ffprobe duration, and a success
|
|
75
|
+
status with no new file flips to an error.
|
|
76
|
+
|
|
77
|
+
**Housekeeping:** the delete-locked scratch project from the v2.104.7 session
|
|
78
|
+
is gone — the pre-restart DeleteProject had returned False while actually
|
|
79
|
+
succeeding, the documented DeleteProject lie caught in the wild. A clean
|
|
80
|
+
Resolve quit/relaunch verified the wedge entry's other half: Quit() works
|
|
81
|
+
when no orphaned render holds the pipeline.
|
|
82
|
+
|
|
5
83
|
## What's New in v2.104.9
|
|
6
84
|
|
|
7
85
|
**The NTSC coverage gap, closed.** The conform fixtures were integer-rate
|
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.
|
|
15
|
+
> 本翻译对应 v2.105.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -521,7 +521,7 @@ values, or automation-hostile modal prompts.
|
|
|
521
521
|
|
|
522
522
|
- **Object:** `MediaPool`
|
|
523
523
|
- **Signature:** `(filePath, {timelineName, importSourceClips, ...}) -> Timeline`
|
|
524
|
-
- **Behavior:** For FCP7 XML, the sequence name INSIDE the file wins over the timelineName import option. When the internal name matches an existing timeline, the call returns that EXISTING timeline — no error, no new timeline — so an export→edit→re-import loop keying uniqueness on the option 'succeeds' while operating on one timeline forever (issue #171, Studio 21.0.4.5; wrapper behavior verified on 19.1.3.7). Distinct from the documented repeated-timelineName None return: here the option is fresh and the file's name is stale.
|
|
524
|
+
- **Behavior:** For FCP7 XML, the sequence name INSIDE the file wins over the timelineName import option. When the internal name matches an existing timeline, the call returns that EXISTING timeline — no error, no new timeline — so an export→edit→re-import loop keying uniqueness on the option 'succeeds' while operating on one timeline forever (issue #171, Studio 21.0.4.5; wrapper behavior verified on 19.1.3.7). Distinct from the documented repeated-timelineName None return: here the option is fresh and the file's name is stale. THE NAMING AUTHORITY DIFFERS PER FORMAT (all measured on 19.1.3.7): FCP7 XML ignores timelineName entirely (internal <name> wins); AAF honours timelineName when given and falls back to its internal name; OTIO honours timelineName; and .drt names the timeline after the FILE (see the .drt entry below). Only FCP7 exhibits the returned-existing trap.
|
|
525
525
|
- **Workaround / current handling:** Rewrite the <sequence><name> inside the file to the intended name before importing — timeline.import_timeline_checked does this automatically for FCP7 XML and errors when a non-rewritable format still returns an existing timeline. Never treat a truthy return as proof of creation; check the returned timeline's id against the pre-import set.
|
|
526
526
|
- **Reference:** [issue #171](https://github.com/samuelgursky/davinci-resolve-mcp/issues/171)
|
|
527
527
|
- **Tags:** timeline, import, silent-failure, unreliable-return
|
|
@@ -530,8 +530,8 @@ values, or automation-hostile modal prompts.
|
|
|
530
530
|
|
|
531
531
|
- **Object:** `MediaPool`
|
|
532
532
|
- **Signature:** `(drtPath, {importSourceClips, ...}) -> Timeline`
|
|
533
|
-
- **Behavior:**
|
|
534
|
-
- **Workaround / current handling:**
|
|
533
|
+
- **Behavior:** Fully mapped by bisection on Studio 19.1.3.7: a .drt IS a .drp that ImportTimelineFromFile accepts — a whole saved-project export renamed .drt imports, clips intact. Requirements: (1) project.xml present; (2) MediaPool/MpFolder.xml present — it holds the Sm2Sequence/Sm2Timeline objects; (3) the SeqContainer keeps its ORIGINAL uuid path — renaming it 'succeeds' with an EMPTY timeline (items=0, no error), the nastiest variant; (4) version stamps at or below the host's ProjectVersion; (5) native blob schema — flat template containers are refused; (6) the source must be a SAVED export (ExportProject snapshots the saved DB state, so an unsaved timeline exports EMPTY tracks). Every Sm2MpTimelineClip block in MpFolder imports as a timeline: extra blocks arrive as ghost empty timelines unless removed (match blocks via the kept container's track <Sequence> DbIds). The imported timeline is named after the FILE, and a refused import can raise a modal dialog that BLOCKS the scripting call until a human dismisses it.
|
|
534
|
+
- **Workaround / current handling:** Follow the recipe: drt.extract_from_drp implements it (original container path, MpFolder carried, ghost blocks removed, Gallery dropped), and drt.assemble authors importable native-schema archives from scratch (template-spliced; pass targetAppVersion on pre-21 hosts). Save the project before ExportProject. Name the timeline by naming the FILE. Never batch speculative .drt imports unattended — one refusal can hold the session hostage behind its dialog; timeline.import_timeline_checked refuses the flat authored shape up front for exactly that reason.
|
|
535
535
|
- **Tags:** timeline, import, silent-failure, headless
|
|
536
536
|
|
|
537
537
|
### Timeline.DeleteClips (requires the Edit page; flaky first attempt)
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.105.0"
|
|
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
|
@@ -52,6 +52,44 @@ export const LITE_DB_ROOT = path.join(
|
|
|
52
52
|
/** Every root searched when resolving a project by name, Studio first. */
|
|
53
53
|
export const DB_ROOTS = [DISK_DB_ROOT, PROJECT_LIBRARY_ROOT, LITE_DB_ROOT];
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Filter roots to the ones that answer a readdir within `deadlineMs`.
|
|
57
|
+
*
|
|
58
|
+
* macOS can render a path UNRESPONSIVE at the filesystem level — measured
|
|
59
|
+
* live 2026-08-30: `ls` on the Lite sandbox container hung indefinitely
|
|
60
|
+
* (File-Provider/container materialization), which made every projectName
|
|
61
|
+
* lookup and the test suite hang with it. A hung readdir cannot be
|
|
62
|
+
* cancelled; the race abandons it (one leaked threadpool op) and reports the
|
|
63
|
+
* root as skipped so the caller can say WHERE it could not look.
|
|
64
|
+
* Returns { roots, skipped: [{root, reason}] }.
|
|
65
|
+
*/
|
|
66
|
+
export async function responsiveRoots(roots = DB_ROOTS, deadlineMs = 3000) {
|
|
67
|
+
const fsp = require('node:fs/promises');
|
|
68
|
+
const results = await Promise.all(
|
|
69
|
+
roots.map(async (root) => {
|
|
70
|
+
try {
|
|
71
|
+
const answer = fsp
|
|
72
|
+
.readdir(root)
|
|
73
|
+
.then(() => 'ok', (err) => (err && err.code === 'ENOENT' ? 'absent' : 'error'));
|
|
74
|
+
const timer = new Promise((resolve) => {
|
|
75
|
+
const t = setTimeout(() => resolve('timeout'), deadlineMs);
|
|
76
|
+
if (t.unref) t.unref();
|
|
77
|
+
});
|
|
78
|
+
const outcome = await Promise.race([answer, timer]);
|
|
79
|
+
return { root, outcome };
|
|
80
|
+
} catch {
|
|
81
|
+
return { root, outcome: 'error' };
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
roots: results.filter((r) => r.outcome === 'ok' || r.outcome === 'absent').map((r) => r.root),
|
|
87
|
+
skipped: results
|
|
88
|
+
.filter((r) => r.outcome === 'timeout')
|
|
89
|
+
.map((r) => ({ root: r.root, reason: `unresponsive after ${deadlineMs}ms (hung filesystem path — skipped)` })),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
55
93
|
export function loadSqlite() {
|
|
56
94
|
try {
|
|
57
95
|
return require('better-sqlite3');
|
|
@@ -85,17 +123,22 @@ export function findProjectDb(projectName, root = DISK_DB_ROOT) {
|
|
|
85
123
|
return hits;
|
|
86
124
|
}
|
|
87
125
|
|
|
88
|
-
export function resolveDbPath({ projectDb, projectName }) {
|
|
126
|
+
export function resolveDbPath({ projectDb, projectName, roots, skippedRoots = [] }) {
|
|
89
127
|
if (projectDb) return projectDb;
|
|
90
128
|
if (!projectName) throw new Error('provide projectDb (path) or projectName');
|
|
129
|
+
const searchRoots = roots || DB_ROOTS;
|
|
91
130
|
// Deduplicate: a project present under both roots must not read as ambiguous
|
|
92
131
|
// just because the same file was found twice.
|
|
93
|
-
const hits = [...new Set(
|
|
132
|
+
const hits = [...new Set(searchRoots.flatMap((root) => findProjectDb(projectName, root)))];
|
|
94
133
|
if (!hits.length) {
|
|
95
134
|
throw new Error(
|
|
96
|
-
`no Project.db found for project "${projectName}". Searched
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
`no Project.db found for project "${projectName}". Searched: ` +
|
|
136
|
+
`${searchRoots.join(', ')}. ` +
|
|
137
|
+
(skippedRoots.length
|
|
138
|
+
? `SKIPPED unresponsive root(s): ${skippedRoots.map((r) => r.root).join(', ')} — ` +
|
|
139
|
+
'a hung filesystem path (File Provider / sandbox container); its projects ' +
|
|
140
|
+
'are invisible until macOS unwedges it. '
|
|
141
|
+
: '') +
|
|
99
142
|
'If Resolve keeps its projects elsewhere — a relocated library, a network/Postgres ' +
|
|
100
143
|
'database, or the free edition on Windows/Linux — pass projectDb with the full path.',
|
|
101
144
|
);
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import fs from 'node:fs/promises';
|
|
20
20
|
import { z } from 'zod';
|
|
21
21
|
import JSZip from 'jszip';
|
|
22
|
-
import { drt } from '../libs.mjs';
|
|
22
|
+
import { drt, drp } from '../libs.mjs';
|
|
23
23
|
import { summarizeDrtTimelines } from '../sequences.mjs';
|
|
24
24
|
|
|
25
25
|
const parseSchema = z.object({ drtPath: z.string().describe('Absolute path to a .drt (or .drp) file') });
|
|
@@ -29,6 +29,48 @@ const authorSchema = z.object({
|
|
|
29
29
|
outputPath: z.string().describe('Absolute path where the .drt will be written'),
|
|
30
30
|
});
|
|
31
31
|
const validateSchema = z.object({ drtPath: z.string().describe('Absolute path to a .drt file') });
|
|
32
|
+
const assembleSchema = z.object({
|
|
33
|
+
spec: z
|
|
34
|
+
.object({})
|
|
35
|
+
.passthrough()
|
|
36
|
+
.describe("assembleTimeline spec: { timelineName?, elements: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
|
|
37
|
+
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
38
|
+
targetAppVersion: z
|
|
39
|
+
.union([z.string(), z.number()])
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Resolve version that must import it, e.g. '19.1' — stamps ProjectVersion down from the template's Resolve-21 capture. Omit for 21+ hosts."),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Rewrite version stamps across every XML entry of a JSZip; returns patch counts. */
|
|
45
|
+
async function applyVersionStamps(zip, targetPV, appVer) {
|
|
46
|
+
const out = new JSZip();
|
|
47
|
+
let elementPatches = 0;
|
|
48
|
+
let stampPatches = 0;
|
|
49
|
+
const jobs = [];
|
|
50
|
+
zip.forEach((path, e) => {
|
|
51
|
+
if (e.dir) return;
|
|
52
|
+
jobs.push(
|
|
53
|
+
(async () => {
|
|
54
|
+
if (!path.endsWith('.xml')) {
|
|
55
|
+
out.file(path, await e.async('nodebuffer'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let xml = await e.async('string');
|
|
59
|
+
xml = xml.replace(/<ProjectVersion>\d+<\/ProjectVersion>/g, () => {
|
|
60
|
+
elementPatches += 1;
|
|
61
|
+
return `<ProjectVersion>${targetPV}</ProjectVersion>`;
|
|
62
|
+
});
|
|
63
|
+
xml = xml.replace(/DbAppVer="[^"]*" DbPrjVer="[^"]*"/g, () => {
|
|
64
|
+
stampPatches += 1;
|
|
65
|
+
return `DbAppVer="${appVer}" DbPrjVer="${targetPV}"`;
|
|
66
|
+
});
|
|
67
|
+
out.file(path, xml);
|
|
68
|
+
})(),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
await Promise.all(jobs);
|
|
72
|
+
return { out, elementPatches, stampPatches };
|
|
73
|
+
}
|
|
32
74
|
const injectIntoDrpSchema = z.object({
|
|
33
75
|
drtPath: z.string().describe('Source .drt'),
|
|
34
76
|
drpPath: z.string().describe('Target.drp to inject into'),
|
|
@@ -93,7 +135,7 @@ function requirePathArg(args, key, action) {
|
|
|
93
135
|
export const drtTool = {
|
|
94
136
|
name: 'drt',
|
|
95
137
|
description:
|
|
96
|
-
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index}] to drive a "which sequence?" picker), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
|
|
138
|
+
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble (spec → IMPORTABLE native-schema .drt via template-spliced real structures; pass targetAppVersion e.g. \'19.1\' for pre-21 hosts), parse, list_sequences (enumerate the timelines inside a .drp/.drt → [{id,name,eventCount,index}] to drive a "which sequence?" picker), author, validate, inject_into_drp, extract_from_drp (pull one SeqContainer out as a .drt — feed the .drt to the Python davinci-resolve MCP timeline.import_timeline_checked, or use timeline.import_from_drp to do both), downgrade (stamp <ProjectVersion> down so an OLDER Resolve will import a .drt/.drp from a newer one — pass targetAppVersion like "19.1.3" or targetProjectVersion).',
|
|
97
139
|
async handler({ action, args }) {
|
|
98
140
|
if (action === 'parse') {
|
|
99
141
|
const p = parseSchema.parse(requirePathArg(args, 'drtPath', 'parse'));
|
|
@@ -148,6 +190,37 @@ export const drtTool = {
|
|
|
148
190
|
await fs.writeFile(p.outputPath, outBuf);
|
|
149
191
|
return { outputPath: p.outputPath, bytes: outBuf.length, seqContainersInjected: injected, projectFolder };
|
|
150
192
|
}
|
|
193
|
+
if (action === 'assemble') {
|
|
194
|
+
const p = assembleSchema.parse(args);
|
|
195
|
+
// Native-schema authoring: template-spliced real Resolve structures
|
|
196
|
+
// (drp-format assembleTimeline), which ImportTimelineFromFile accepts —
|
|
197
|
+
// unlike drt.author's flat template. Measured live on Studio 19.1.3.7:
|
|
198
|
+
// assembled + stamped to the host's ProjectVersion imports with every
|
|
199
|
+
// element intact.
|
|
200
|
+
const { assembleTimeline } = drp();
|
|
201
|
+
const { buffer, timelineName, startFrame } = await assembleTimeline(p.spec);
|
|
202
|
+
let outBuf = buffer;
|
|
203
|
+
let stamped = null;
|
|
204
|
+
if (p.targetAppVersion !== undefined) {
|
|
205
|
+
const targetPV = resolveTargetProjectVersion({ targetAppVersion: p.targetAppVersion });
|
|
206
|
+
const appVer = `${p.targetAppVersion}${'.0'.repeat(Math.max(0, 4 - String(p.targetAppVersion).split('.').length))}`;
|
|
207
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
208
|
+
const { out, elementPatches, stampPatches } = await applyVersionStamps(zip, targetPV, appVer);
|
|
209
|
+
outBuf = await out.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
210
|
+
stamped = { targetProjectVersion: targetPV, elementPatches, stampPatches };
|
|
211
|
+
}
|
|
212
|
+
await fs.writeFile(p.outputPath, outBuf);
|
|
213
|
+
return {
|
|
214
|
+
outputPath: p.outputPath,
|
|
215
|
+
bytes: outBuf.length,
|
|
216
|
+
timelineName,
|
|
217
|
+
startFrame,
|
|
218
|
+
stamped,
|
|
219
|
+
note:
|
|
220
|
+
'Import with timeline.import_timeline_checked — the imported timeline is named after the FILE. ' +
|
|
221
|
+
'On a host older than Resolve 21, pass targetAppVersion or the version gate refuses the archive.',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
151
224
|
if (action === 'extract_from_drp') {
|
|
152
225
|
const p = extractFromDrpSchema.parse(args);
|
|
153
226
|
const drpZip = await JSZip.loadAsync(await fs.readFile(p.drpPath));
|
|
@@ -156,16 +229,37 @@ export const drtTool = {
|
|
|
156
229
|
if (idx >= seqEntries.length) {
|
|
157
230
|
return { error: `timelineIndex ${idx} out of range (${seqEntries.length} SeqContainers)` };
|
|
158
231
|
}
|
|
159
|
-
|
|
232
|
+
// The importable-.drt recipe, measured by bisection on Studio 19.1.3.7:
|
|
233
|
+
// a .drt IS a .drp that ImportTimelineFromFile accepts. project.xml and
|
|
234
|
+
// MediaPool/ are REQUIRED (the Sm2Sequence/Sm2Timeline objects live in
|
|
235
|
+
// MpFolder.xml); the SeqContainer must keep its ORIGINAL uuid path —
|
|
236
|
+
// renaming it imports an EMPTY timeline with no error; Gallery.xml is
|
|
237
|
+
// droppable. Other timelines' MpFolder blocks must go too, or each
|
|
238
|
+
// arrives as a ghost empty timeline (matched via the container tracks'
|
|
239
|
+
// <Sequence> DbId, which appears inside exactly one Sm2MpTimelineClip).
|
|
240
|
+
const keepEntry = seqEntries[idx];
|
|
241
|
+
const keepXml = await drpZip.file(keepEntry).async('string');
|
|
242
|
+
const keepSeqIds = [...keepXml.matchAll(/<Sequence>([0-9a-f-]{36})<\/Sequence>/g)].map((m) => m[1]);
|
|
160
243
|
const out = new JSZip();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
244
|
+
let droppedTimelines = 0;
|
|
245
|
+
for (const name of Object.keys(drpZip.files)) {
|
|
246
|
+
const entry = drpZip.files[name];
|
|
247
|
+
if (entry.dir) continue;
|
|
248
|
+
if (name === 'Gallery.xml') continue;
|
|
249
|
+
const isSeq = seqEntries.includes(name);
|
|
250
|
+
if (isSeq && name !== keepEntry) continue;
|
|
251
|
+
let content = await entry.async(name.endsWith('.xml') ? 'string' : 'nodebuffer');
|
|
252
|
+
if (name.endsWith('MpFolder.xml') && seqEntries.length > 1) {
|
|
253
|
+
content = content.replace(
|
|
254
|
+
/<Element>\s*<Sm2MpTimelineClip DbId="[^"]+">(?:(?!<\/Sm2MpTimelineClip>)[\s\S])*?<\/Sm2MpTimelineClip>\s*<\/Element>/g,
|
|
255
|
+
(block) => {
|
|
256
|
+
if (keepSeqIds.some((id) => block.includes(id))) return block;
|
|
257
|
+
droppedTimelines += 1;
|
|
258
|
+
return '';
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
out.file(name, content);
|
|
169
263
|
}
|
|
170
264
|
out.file(
|
|
171
265
|
'metadata.json',
|
|
@@ -173,7 +267,8 @@ export const drtTool = {
|
|
|
173
267
|
{
|
|
174
268
|
source: 'extract_from_drp',
|
|
175
269
|
sourceDrp: p.drpPath,
|
|
176
|
-
sourceSeqContainer:
|
|
270
|
+
sourceSeqContainer: keepEntry,
|
|
271
|
+
droppedTimelines,
|
|
177
272
|
exportedFrom: 'davinci-resolve-advanced-mcp drt.extract_from_drp',
|
|
178
273
|
},
|
|
179
274
|
null,
|
|
@@ -182,7 +277,13 @@ export const drtTool = {
|
|
|
182
277
|
);
|
|
183
278
|
const outBuf = await out.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
184
279
|
await fs.writeFile(p.outputPath, outBuf);
|
|
185
|
-
return {
|
|
280
|
+
return {
|
|
281
|
+
outputPath: p.outputPath,
|
|
282
|
+
bytes: outBuf.length,
|
|
283
|
+
sourceSeqContainer: keepEntry,
|
|
284
|
+
droppedTimelines,
|
|
285
|
+
note: 'The imported timeline is named after the FILE. Source must be a SAVED project export — ExportProject snapshots the saved DB state, so unsaved edits are absent.',
|
|
286
|
+
};
|
|
186
287
|
}
|
|
187
288
|
if (action === 'downgrade') {
|
|
188
289
|
const p = downgradeSchema.parse(args);
|
|
@@ -19,7 +19,17 @@
|
|
|
19
19
|
|
|
20
20
|
import { z } from 'zod';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
|
-
import { resolveDbPath, openGuarded, backup, requireClosed } from '../db-patch.mjs';
|
|
22
|
+
import { resolveDbPath as _resolveDbPathRaw, responsiveRoots, openGuarded, backup, requireClosed } from '../db-patch.mjs';
|
|
23
|
+
|
|
24
|
+
// projectName discovery walks library roots that macOS can render UNRESPONSIVE
|
|
25
|
+
// (measured: a hung Lite sandbox container froze every lookup). Probe the
|
|
26
|
+
// roots with a deadline first and search only the ones that answer; the
|
|
27
|
+
// not-found error then names any skipped root.
|
|
28
|
+
async function resolveDbPath(p) {
|
|
29
|
+
if (p.projectDb || !p.projectName) return _resolveDbPathRaw(p);
|
|
30
|
+
const { roots, skipped } = await responsiveRoots();
|
|
31
|
+
return _resolveDbPathRaw({ ...p, roots, skippedRoots: skipped });
|
|
32
|
+
}
|
|
23
33
|
|
|
24
34
|
const require = createRequire(import.meta.url);
|
|
25
35
|
|
|
@@ -96,7 +106,7 @@ export const projectDbTool = {
|
|
|
96
106
|
async handler({ action, args }) {
|
|
97
107
|
if (action === 'list_folders') {
|
|
98
108
|
const p = listFoldersSchema.parse(args);
|
|
99
|
-
const db = openGuarded(resolveDbPath(p), { table: 'Sm2MpFolder', column: 'Name' });
|
|
109
|
+
const db = openGuarded(await resolveDbPath(p), { table: 'Sm2MpFolder', column: 'Name' });
|
|
100
110
|
try {
|
|
101
111
|
return { folders: db.prepare('SELECT Name, ColorTag FROM Sm2MpFolder ORDER BY Name').all() };
|
|
102
112
|
} finally {
|
|
@@ -106,7 +116,7 @@ export const projectDbTool = {
|
|
|
106
116
|
if (action === 'rename_folder') {
|
|
107
117
|
const p = renameFolderSchema.parse(args);
|
|
108
118
|
requireClosed(p);
|
|
109
|
-
const dbPath = resolveDbPath(p);
|
|
119
|
+
const dbPath = await resolveDbPath(p);
|
|
110
120
|
const bak = backup(dbPath);
|
|
111
121
|
const db = openGuarded(dbPath, { writable: true, table: 'Sm2MpFolder', column: 'Name' });
|
|
112
122
|
try {
|
|
@@ -121,7 +131,7 @@ export const projectDbTool = {
|
|
|
121
131
|
if (action === 'set_folder_color') {
|
|
122
132
|
const p = folderColorSchema.parse(args);
|
|
123
133
|
requireClosed(p);
|
|
124
|
-
const dbPath = resolveDbPath(p);
|
|
134
|
+
const dbPath = await resolveDbPath(p);
|
|
125
135
|
const bak = backup(dbPath);
|
|
126
136
|
const db = openGuarded(dbPath, { writable: true, table: 'Sm2MpFolder', column: 'ColorTag' });
|
|
127
137
|
try {
|
|
@@ -135,7 +145,7 @@ export const projectDbTool = {
|
|
|
135
145
|
}
|
|
136
146
|
if (action === 'list_clips') {
|
|
137
147
|
const p = listClipsSchema.parse(args);
|
|
138
|
-
const db = openGuarded(resolveDbPath(p), { table: 'Sm2MpMedia', column: 'MarkIn' });
|
|
148
|
+
const db = openGuarded(await resolveDbPath(p), { table: 'Sm2MpMedia', column: 'MarkIn' });
|
|
139
149
|
try {
|
|
140
150
|
return { clips: db.prepare('SELECT Name, MarkIn, MarkOut FROM Sm2MpMedia ORDER BY Name LIMIT 500').all() };
|
|
141
151
|
} finally {
|
|
@@ -146,7 +156,7 @@ export const projectDbTool = {
|
|
|
146
156
|
const p = clipMarksSchema.parse(args);
|
|
147
157
|
requireClosed(p);
|
|
148
158
|
if (p.markIn == null && p.markOut == null) throw new Error('provide markIn and/or markOut');
|
|
149
|
-
const dbPath = resolveDbPath(p);
|
|
159
|
+
const dbPath = await resolveDbPath(p);
|
|
150
160
|
const bak = backup(dbPath);
|
|
151
161
|
const db = openGuarded(dbPath, { writable: true, table: 'Sm2MpMedia', column: 'MarkIn' });
|
|
152
162
|
try {
|
|
@@ -163,7 +173,7 @@ export const projectDbTool = {
|
|
|
163
173
|
const p = relayoutSchema.parse(args);
|
|
164
174
|
const layout = require('../../vendor/drx-codec/node-layout.js');
|
|
165
175
|
const layoutOpts = { originX: p.originX, originY: p.originY, spacingX: p.spacingX };
|
|
166
|
-
const dbPath = resolveDbPath(p);
|
|
176
|
+
const dbPath = await resolveDbPath(p);
|
|
167
177
|
const write = !p.dryRun;
|
|
168
178
|
if (write) requireClosed(p);
|
|
169
179
|
const bak = write ? backup(dbPath) : null;
|
|
@@ -228,7 +238,7 @@ export const projectDbTool = {
|
|
|
228
238
|
if (action === 'list_subtitle_styles') {
|
|
229
239
|
const p = listSubtitleStylesSchema.parse(args);
|
|
230
240
|
const style = require('../../vendor/drp-format/subtitle-style.js');
|
|
231
|
-
const db = openGuarded(resolveDbPath(p), { table: 'Sm2TiTrack', column: 'FieldsBlob' });
|
|
241
|
+
const db = openGuarded(await resolveDbPath(p), { table: 'Sm2TiTrack', column: 'FieldsBlob' });
|
|
232
242
|
try {
|
|
233
243
|
const counts = new Map();
|
|
234
244
|
const tracks = [];
|
|
@@ -281,7 +291,7 @@ export const projectDbTool = {
|
|
|
281
291
|
|
|
282
292
|
const write = !p.dryRun;
|
|
283
293
|
if (write) requireClosed(p);
|
|
284
|
-
const dbPath = resolveDbPath(p);
|
|
294
|
+
const dbPath = await resolveDbPath(p);
|
|
285
295
|
const bak = write ? backup(dbPath) : null;
|
|
286
296
|
const db = openGuarded(dbPath, { writable: write, table: 'Sm2TiTrack', column: 'FieldsBlob' });
|
|
287
297
|
try {
|
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.
|
|
90
|
+
VERSION = "2.105.0"
|
|
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.
|
|
14
|
+
VERSION = "2.105.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -7158,12 +7158,12 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
|
|
|
7158
7158
|
"dismisses it, so this import is not attempted at all.",
|
|
7159
7159
|
category="invalid_input",
|
|
7160
7160
|
remediation=(
|
|
7161
|
-
"
|
|
7162
|
-
"For
|
|
7163
|
-
"(
|
|
7164
|
-
"
|
|
7165
|
-
"
|
|
7166
|
-
"
|
|
7161
|
+
"Flat-authored DRTs serve offline/DB workflows, not live "
|
|
7162
|
+
"import. For an importable .drt, use drt.assemble "
|
|
7163
|
+
"(native-schema authoring from a spec; pass "
|
|
7164
|
+
"targetAppVersion e.g. '19.1' on pre-21 hosts), "
|
|
7165
|
+
"drt.extract_from_drp on a SAVED .drp export, or author "
|
|
7166
|
+
"OTIO (editorial.convert_to_interchange target 'otio')."
|
|
7167
7167
|
),
|
|
7168
7168
|
)
|
|
7169
7169
|
# ImportTimelineFromFile silently no-ops on the never-saved default project: it returns
|
|
@@ -7465,38 +7465,62 @@ def _drp_seq_containers(zf) -> List[Dict[str, Any]]:
|
|
|
7465
7465
|
|
|
7466
7466
|
|
|
7467
7467
|
def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str) -> None:
|
|
7468
|
-
"""Write
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7468
|
+
"""Write an IMPORTABLE .drt holding one timeline from a .drp/.drt archive.
|
|
7469
|
+
|
|
7470
|
+
The recipe, measured by bisection on Studio 19.1.3.7: a .drt IS a .drp
|
|
7471
|
+
that ImportTimelineFromFile accepts. project.xml and MediaPool/ are
|
|
7472
|
+
REQUIRED (the Sm2Sequence/Sm2Timeline objects live in MpFolder.xml); the
|
|
7473
|
+
SeqContainer must keep its ORIGINAL uuid path — renaming it imports an
|
|
7474
|
+
EMPTY timeline with no error; Gallery.xml is droppable; and other
|
|
7475
|
+
timelines' Sm2MpTimelineClip blocks must be removed from MpFolder.xml or
|
|
7476
|
+
each arrives as a ghost empty timeline (matched via the kept container's
|
|
7477
|
+
track <Sequence> DbIds, which appear inside exactly one block). Source
|
|
7478
|
+
must be a SAVED export — ExportProject snapshots the saved DB state.
|
|
7474
7479
|
"""
|
|
7475
7480
|
import zipfile
|
|
7476
7481
|
|
|
7477
7482
|
with zipfile.ZipFile(drp_path, "r") as zf:
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7483
|
+
names = [n for n in zf.namelist() if not n.endswith("/")]
|
|
7484
|
+
seq_entries = [n for n in names if _SEQ_CONTAINER_RE.search(n)]
|
|
7485
|
+
keep_xml = zf.read(seq_entry).decode("utf-8", "replace")
|
|
7486
|
+
keep_seq_ids = re.findall(r"<Sequence>([0-9a-f-]{36})</Sequence>", keep_xml)
|
|
7487
|
+
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as out:
|
|
7488
|
+
for name in names:
|
|
7489
|
+
if name == "Gallery.xml":
|
|
7490
|
+
continue
|
|
7491
|
+
if name in seq_entries and name != seq_entry:
|
|
7492
|
+
continue
|
|
7493
|
+
data = zf.read(name)
|
|
7494
|
+
if name.endswith("MpFolder.xml") and len(seq_entries) > 1:
|
|
7495
|
+
text = data.decode("utf-8", "replace")
|
|
7496
|
+
|
|
7497
|
+
def _keep_block(match):
|
|
7498
|
+
block = match.group(0)
|
|
7499
|
+
if any(sid in block for sid in keep_seq_ids):
|
|
7500
|
+
return block
|
|
7501
|
+
return ""
|
|
7502
|
+
|
|
7503
|
+
text = re.sub(
|
|
7504
|
+
r"<Element>\s*<Sm2MpTimelineClip DbId=\"[^\"]+\">"
|
|
7505
|
+
r"(?:(?!</Sm2MpTimelineClip>)[\s\S])*?"
|
|
7506
|
+
r"</Sm2MpTimelineClip>\s*</Element>",
|
|
7507
|
+
_keep_block,
|
|
7508
|
+
text,
|
|
7509
|
+
)
|
|
7510
|
+
data = text.encode("utf-8")
|
|
7511
|
+
out.writestr(name, data)
|
|
7512
|
+
out.writestr(
|
|
7513
|
+
"metadata.json",
|
|
7514
|
+
json.dumps(
|
|
7515
|
+
{
|
|
7516
|
+
"source": "import_from_drp",
|
|
7517
|
+
"sourceDrp": drp_path,
|
|
7518
|
+
"sourceSeqContainer": seq_entry,
|
|
7519
|
+
"exportedFrom": "davinci-resolve-mcp timeline.import_from_drp",
|
|
7520
|
+
},
|
|
7521
|
+
indent=2,
|
|
7522
|
+
),
|
|
7523
|
+
)
|
|
7500
7524
|
|
|
7501
7525
|
|
|
7502
7526
|
def _import_from_drp(proj, mp, p: Dict[str, Any]):
|
|
@@ -18975,8 +18999,40 @@ def _safe_quick_export(proj, p: Dict[str, Any]):
|
|
|
18975
18999
|
return {"success": False, "validation": validation}
|
|
18976
19000
|
if p.get("dry_run") or not p.get("allow_render", False):
|
|
18977
19001
|
return _ok(would_render=False, preset=preset, params=params, validation=validation)
|
|
19002
|
+
before = set()
|
|
19003
|
+
if target_dir and os.path.isdir(target_dir):
|
|
19004
|
+
before = set(os.listdir(target_dir))
|
|
18978
19005
|
status = _ser(proj.RenderWithQuickExport(preset, params))
|
|
18979
|
-
|
|
19006
|
+
out: Dict[str, Any] = {
|
|
19007
|
+
"success": not (isinstance(status, dict) and status.get("error")),
|
|
19008
|
+
"status": status,
|
|
19009
|
+
"params": params,
|
|
19010
|
+
}
|
|
19011
|
+
# RenderWithQuickExport's status dict is trusted nowhere else in this
|
|
19012
|
+
# repo's render surface anymore (issue #164 taught that lesson) — check
|
|
19013
|
+
# what actually landed on disk. Best-effort: TargetDir is known, so a
|
|
19014
|
+
# successful export must have produced at least one new file there.
|
|
19015
|
+
if out["success"] and target_dir and os.path.isdir(target_dir):
|
|
19016
|
+
new_files = sorted(set(os.listdir(target_dir)) - before)
|
|
19017
|
+
outputs = []
|
|
19018
|
+
for name in new_files:
|
|
19019
|
+
path = os.path.join(target_dir, name)
|
|
19020
|
+
if not os.path.isfile(path):
|
|
19021
|
+
continue
|
|
19022
|
+
rec: Dict[str, Any] = {"path": path, "size_bytes": os.path.getsize(path)}
|
|
19023
|
+
probe = _ffprobe_media_summary(path)
|
|
19024
|
+
if probe:
|
|
19025
|
+
rec["duration_seconds"] = probe.get("duration_seconds")
|
|
19026
|
+
outputs.append(rec)
|
|
19027
|
+
out["outputs"] = outputs
|
|
19028
|
+
if not outputs:
|
|
19029
|
+
out["success"] = False
|
|
19030
|
+
out["error"] = (
|
|
19031
|
+
"RenderWithQuickExport reported success but wrote no file to "
|
|
19032
|
+
f"{target_dir} — treat the status dict as unreliable and check "
|
|
19033
|
+
"the Deliver page's render queue."
|
|
19034
|
+
)
|
|
19035
|
+
return out
|
|
18980
19036
|
|
|
18981
19037
|
|
|
18982
19038
|
def _export_render_boundary_report(proj, p: Dict[str, Any]):
|
|
@@ -19049,7 +19105,10 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
|
|
|
19049
19105
|
way to read back — see the SetRenderSettings api_truth entry.
|
|
19050
19106
|
render_job_lifecycle_probe(target_dir, settings?, format?, codec?, custom_name?) -> {success, job_id, status_before_delete}
|
|
19051
19107
|
quick_export_capabilities() -> {presets, safe_params, guards}
|
|
19052
|
-
safe_quick_export(preset, target_dir?|params?, custom_name?, dry_run?, allow_render?) -> {success, status}
|
|
19108
|
+
safe_quick_export(preset, target_dir?|params?, custom_name?, dry_run?, allow_render?) -> {success, status, outputs}
|
|
19109
|
+
After a live export, the files that actually landed in TargetDir are
|
|
19110
|
+
listed with size and ffprobe duration; a success status that wrote no
|
|
19111
|
+
file flips success to false (the issue #164 trust lesson).
|
|
19053
19112
|
export_render_boundary_report(include_matrix?, max_pairs?, include_quick_export?) -> {capabilities, settings, matrix?}
|
|
19054
19113
|
list_delivery_targets(tier?, check_availability?) -> {targets, tiers, schema_version}
|
|
19055
19114
|
resolve_delivery_target(target, overrides?) -> {format_id, codec_id, settings, qc_spec}
|
package/src/utils/api_truth.py
CHANGED
|
@@ -1866,7 +1866,13 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1866
1866
|
"(issue #171, Studio 21.0.4.5; wrapper behavior verified on "
|
|
1867
1867
|
"19.1.3.7). Distinct from the documented repeated-"
|
|
1868
1868
|
"timelineName None return: here the option is fresh and the "
|
|
1869
|
-
"file's name is stale."
|
|
1869
|
+
"file's name is stale. THE NAMING AUTHORITY DIFFERS PER "
|
|
1870
|
+
"FORMAT (all measured on 19.1.3.7): FCP7 XML ignores "
|
|
1871
|
+
"timelineName entirely (internal <name> wins); AAF honours "
|
|
1872
|
+
"timelineName when given and falls back to its internal "
|
|
1873
|
+
"name; OTIO honours timelineName; and .drt names the "
|
|
1874
|
+
"timeline after the FILE (see the .drt entry below). Only "
|
|
1875
|
+
"FCP7 exhibits the returned-existing trap.",
|
|
1870
1876
|
"recommended": "Rewrite the <sequence><name> inside the file to the "
|
|
1871
1877
|
"intended name before importing — "
|
|
1872
1878
|
"timeline.import_timeline_checked does this "
|
|
@@ -1883,32 +1889,37 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1883
1889
|
"symbol": "MediaPool.ImportTimelineFromFile (.drt requirements and filename naming)",
|
|
1884
1890
|
"object": "MediaPool",
|
|
1885
1891
|
"signature": "(drtPath, {importSourceClips, ...}) -> Timeline",
|
|
1886
|
-
"reality": "
|
|
1887
|
-
".
|
|
1888
|
-
"
|
|
1889
|
-
"
|
|
1890
|
-
"
|
|
1891
|
-
"
|
|
1892
|
-
"
|
|
1893
|
-
"
|
|
1894
|
-
"
|
|
1895
|
-
"
|
|
1896
|
-
"
|
|
1897
|
-
"
|
|
1898
|
-
"
|
|
1899
|
-
"
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
"
|
|
1908
|
-
"
|
|
1909
|
-
"
|
|
1910
|
-
"
|
|
1911
|
-
"
|
|
1892
|
+
"reality": "Fully mapped by bisection on Studio 19.1.3.7: a .drt IS "
|
|
1893
|
+
"a .drp that ImportTimelineFromFile accepts — a whole "
|
|
1894
|
+
"saved-project export renamed .drt imports, clips intact. "
|
|
1895
|
+
"Requirements: (1) project.xml present; (2) MediaPool/"
|
|
1896
|
+
"MpFolder.xml present — it holds the Sm2Sequence/"
|
|
1897
|
+
"Sm2Timeline objects; (3) the SeqContainer keeps its "
|
|
1898
|
+
"ORIGINAL uuid path — renaming it 'succeeds' with an "
|
|
1899
|
+
"EMPTY timeline (items=0, no error), the nastiest variant; "
|
|
1900
|
+
"(4) version stamps at or below the host's ProjectVersion; "
|
|
1901
|
+
"(5) native blob schema — flat template containers are "
|
|
1902
|
+
"refused; (6) the source must be a SAVED export "
|
|
1903
|
+
"(ExportProject snapshots the saved DB state, so an "
|
|
1904
|
+
"unsaved timeline exports EMPTY tracks). Every "
|
|
1905
|
+
"Sm2MpTimelineClip block in MpFolder imports as a "
|
|
1906
|
+
"timeline: extra blocks arrive as ghost empty timelines "
|
|
1907
|
+
"unless removed (match blocks via the kept container's "
|
|
1908
|
+
"track <Sequence> DbIds). The imported timeline is named "
|
|
1909
|
+
"after the FILE, and a refused import can raise a modal "
|
|
1910
|
+
"dialog that BLOCKS the scripting call until a human "
|
|
1911
|
+
"dismisses it.",
|
|
1912
|
+
"recommended": "Follow the recipe: drt.extract_from_drp implements "
|
|
1913
|
+
"it (original container path, MpFolder carried, ghost "
|
|
1914
|
+
"blocks removed, Gallery dropped), and drt.assemble "
|
|
1915
|
+
"authors importable native-schema archives from "
|
|
1916
|
+
"scratch (template-spliced; pass targetAppVersion on "
|
|
1917
|
+
"pre-21 hosts). Save the project before ExportProject. "
|
|
1918
|
+
"Name the timeline by naming the FILE. Never batch "
|
|
1919
|
+
"speculative .drt imports unattended — one refusal "
|
|
1920
|
+
"can hold the session hostage behind its dialog; "
|
|
1921
|
+
"timeline.import_timeline_checked refuses the flat "
|
|
1922
|
+
"authored shape up front for exactly that reason.",
|
|
1912
1923
|
"tags": ["timeline", "import", "silent-failure", "headless"],
|
|
1913
1924
|
"submit": "bug",
|
|
1914
1925
|
},
|