davinci-resolve-mcp 2.106.0 → 2.108.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 +56 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +97 -0
- package/resolve-advanced/server/tools/drt.mjs +69 -2
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +70 -45
- package/resolve-advanced/vendor/drp-format/cut-media.js +8 -0
- package/resolve-advanced/vendor/drp-format/media-template-cache.js +13 -1
- package/resolve-advanced/vendor/drp-format/place-fusion-title.js +18 -1
- package/resolve-advanced/vendor/drp-format/place-generator.js +17 -2
- package/resolve-advanced/vendor/drp-format/templates/fusion-title-r19.xml +54 -0
- package/resolve-advanced/vendor/drp-format/templates/generator-solid-color-r19.xml +19 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
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.108.0
|
|
6
|
+
|
|
7
|
+
**The conform emulator, coast to coast.** An interchange file goes in; an
|
|
8
|
+
importable, RENDERING native .drt comes out — one call.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`drt.assemble_from_interchange`**: EDL/OTIO/XML/AAF + a sourceMap
|
|
13
|
+
(reel → {mediaFilePath, spec}) → parse → frame-convert → multi-source
|
|
14
|
+
assemble with native-descriptor transplant → stamped .drt. The
|
|
15
|
+
events→spec bridge (`eventsToAssembleSpec`) anchors the earliest video
|
|
16
|
+
event at the timeline origin, converts nominal-base event frames to the
|
|
17
|
+
24fps template timeline (round(frames × 24 / nominalFps) — butt cuts stay
|
|
18
|
+
gapless through the conversion, verified at 29.97), groups cuts per
|
|
19
|
+
source, refuses overlapping record ranges and unmapped reels loudly, and
|
|
20
|
+
returns an honesty ledger: flattened retimes (the clip schema has no
|
|
21
|
+
per-clip speed), transitions treated as cuts, audio events skipped (cuts
|
|
22
|
+
carry their own linked A1).
|
|
23
|
+
|
|
24
|
+
Live-proofed on Studio 19.1.3.7 with the full route: a three-event EDL
|
|
25
|
+
cutting between two sources (with an M2 retime line) assembled, imported
|
|
26
|
+
6/6 linked with exact source in-points, and rendered each event's OWN
|
|
27
|
+
pixels — YAVG 125.6 / 234 / 125.5 — with a full-range render verifying at
|
|
28
|
+
duration ratio 1.0 and the retime present in the ledger.
|
|
29
|
+
|
|
30
|
+
### Scoped honestly
|
|
31
|
+
|
|
32
|
+
Title/generator elements on a pre-21 host are NOT render-verified: the
|
|
33
|
+
harvested snippets are Resolve-21 structures that import and read back
|
|
34
|
+
correctly but render black on 19.1.3 (measured), and the r19 snippet
|
|
35
|
+
harvest is incomplete (the generator's Sm2TiCompositionTable dependency,
|
|
36
|
+
the title's per-generation comp-blob layout) — with the partial harvest,
|
|
37
|
+
render jobs fail outright, which is worse. Snippet selection stays on the
|
|
38
|
+
R21 structures, `drt.assemble` warns when elements target a pre-21 host,
|
|
39
|
+
and the harvested r19 snippets ship in the templates directory for the
|
|
40
|
+
element-transplant expedition. Media cuts render everywhere the transplant
|
|
41
|
+
path covers.
|
|
42
|
+
|
|
43
|
+
## What's New in v2.107.0
|
|
44
|
+
|
|
45
|
+
**Multi-source media authoring.** `drt.assemble`'s media support grows from
|
|
46
|
+
one source to many: `media` accepts an array of `{mediaFilePath, spec, cuts}`
|
|
47
|
+
sources, each cut landing on the shared V1/A1 with its own source's
|
|
48
|
+
transplanted native descriptors. New plumbing: `insertMediaElement` appends
|
|
49
|
+
additional native pool elements into MpFolder's MediaVec (folder-parent id
|
|
50
|
+
adopted), and `cutSourceIntoClips` accepts a per-cut `mediaRef` so each clone
|
|
51
|
+
points at ITS source. Multi-source strictly requires a captured native
|
|
52
|
+
template for every source (the render-verified transplant path); the refusal
|
|
53
|
+
names `media_pool.capture_media_template` per missing file — a repoint
|
|
54
|
+
fallback that renders black across N sources would be a trap, not a feature.
|
|
55
|
+
|
|
56
|
+
Live-verified end to end on Studio 19.1.3.7 with luma fingerprints: a
|
|
57
|
+
timeline interleaving cuts from two sources (testsrc + solid white) imported
|
|
58
|
+
6/6 linked and rendered each cut's OWN pixels — YAVG 125.6 / 234 / 125.5
|
|
59
|
+
across the three cuts, matching each source's signature exactly.
|
|
60
|
+
|
|
5
61
|
## What's New in v2.106.0
|
|
6
62
|
|
|
7
63
|
**Media clips in native DRT authoring — cut real footage into an importable,
|
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.108.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.108.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
|
@@ -192,6 +192,103 @@ export function eventsToOTIO(events, opts = {}) {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/** Build a CMX3600 EDL string (cuts + M2 speed). Video events only, per EDL convention. */
|
|
195
|
+
/**
|
|
196
|
+
* eventsToAssembleSpec — normalized interchange events → drt.assemble media spec.
|
|
197
|
+
*
|
|
198
|
+
* The coast-to-coast bridge: parseInterchange's events (EDL/AAF/OTIO/XML)
|
|
199
|
+
* become an importable, RENDERING native .drt via assembleTimeline's
|
|
200
|
+
* transplant path. Frame math: event frames are NOMINAL-base at the event's
|
|
201
|
+
* fps (v2.104.6 convention, measured against Resolve); the assemble template
|
|
202
|
+
* timeline runs 24fps with origin 86400, so rec/src frames convert as
|
|
203
|
+
* round(frames × 24 / nominalFps). Placement anchors the EARLIEST video
|
|
204
|
+
* event at the origin. Honesty ledger in the returned report: flattened
|
|
205
|
+
* retimes (the template clip schema has no per-clip speed), dropped
|
|
206
|
+
* transitions (treated as cuts at their boundary), and skipped audio events
|
|
207
|
+
* (cuts carry linked A1 audio from their own source already).
|
|
208
|
+
*
|
|
209
|
+
* @param {Array} events - normalized events (parseInterchange shape)
|
|
210
|
+
* @param {object} opts
|
|
211
|
+
* @param {Object<string,{mediaFilePath:string, spec:object}>} opts.sourceMap -
|
|
212
|
+
* event.source (reel) → media file + spec. Every VIDEO event's source must
|
|
213
|
+
* be mapped; unmapped reels refuse with the reel names listed.
|
|
214
|
+
* @param {string} [opts.timelineName]
|
|
215
|
+
* @returns {{spec: object, report: object}}
|
|
216
|
+
*/
|
|
217
|
+
export function eventsToAssembleSpec(events, opts = {}) {
|
|
218
|
+
const { sourceMap, timelineName } = opts;
|
|
219
|
+
if (!Array.isArray(events) || !events.length) {
|
|
220
|
+
throw new TypeError('eventsToAssembleSpec: events must be a non-empty array');
|
|
221
|
+
}
|
|
222
|
+
if (!sourceMap || typeof sourceMap !== 'object') {
|
|
223
|
+
throw new TypeError('eventsToAssembleSpec: sourceMap {reel: {mediaFilePath, spec}} is required');
|
|
224
|
+
}
|
|
225
|
+
const ORIGIN = 86400;
|
|
226
|
+
const vids = events.filter((e) => e.track !== 'A' && e.recIn != null && e.recOut != null);
|
|
227
|
+
const audioSkipped = events.length - vids.length;
|
|
228
|
+
if (!vids.length) throw new Error('eventsToAssembleSpec: no video events with record ranges');
|
|
229
|
+
|
|
230
|
+
const unmapped = [...new Set(vids.map((e) => e.source).filter((srcName) => !sourceMap[srcName]))];
|
|
231
|
+
if (unmapped.length) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`eventsToAssembleSpec: unmapped source reel(s): ${unmapped.join(', ')} — ` +
|
|
234
|
+
'every video event needs a sourceMap entry {mediaFilePath, spec}',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const toTl = (frames, fps) => Math.round((frames * 24) / Math.round(fps || 24));
|
|
239
|
+
const flattenedRetimes = [];
|
|
240
|
+
const droppedTransitions = [];
|
|
241
|
+
const perSource = new Map();
|
|
242
|
+
const placements = [];
|
|
243
|
+
|
|
244
|
+
const minRec = Math.min(...vids.map((e) => toTl(e.recIn, e.fps)));
|
|
245
|
+
for (const e of vids) {
|
|
246
|
+
const recIn = ORIGIN + (toTl(e.recIn, e.fps) - minRec);
|
|
247
|
+
const recOut = ORIGIN + (toTl(e.recOut, e.fps) - minRec);
|
|
248
|
+
const durationFrames = recOut - recIn;
|
|
249
|
+
if (durationFrames <= 0) continue;
|
|
250
|
+
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
251
|
+
flattenedRetimes.push({ index: e.index, source: e.source, speed: e.speed, reverse: !!e.reverse });
|
|
252
|
+
}
|
|
253
|
+
if (e.transition) {
|
|
254
|
+
droppedTransitions.push({ index: e.index, type: e.transition.type, duration: e.transition.duration });
|
|
255
|
+
}
|
|
256
|
+
const cut = { startFrame: recIn, durationFrames, srcIn: toTl(e.srcIn ?? 0, e.fps) };
|
|
257
|
+
placements.push({ start: recIn, end: recOut, index: e.index });
|
|
258
|
+
if (!perSource.has(e.source)) perSource.set(e.source, []);
|
|
259
|
+
perSource.get(e.source).push(cut);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
placements.sort((a, b) => a.start - b.start);
|
|
263
|
+
for (let i = 1; i < placements.length; i += 1) {
|
|
264
|
+
if (placements[i].start < placements[i - 1].end) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`eventsToAssembleSpec: events ${placements[i - 1].index} and ${placements[i].index} ` +
|
|
267
|
+
'overlap on the record track after frame conversion — a single V1 cannot hold both. ' +
|
|
268
|
+
'Resolve the overlap upstream (transitions count as cuts at their boundary here).',
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const media = [...perSource.entries()].map(([reel, cuts]) => ({
|
|
274
|
+
mediaFilePath: sourceMap[reel].mediaFilePath,
|
|
275
|
+
spec: sourceMap[reel].spec,
|
|
276
|
+
cuts,
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
spec: { timelineName, media },
|
|
281
|
+
report: {
|
|
282
|
+
videoEvents: vids.length,
|
|
283
|
+
sources: media.length,
|
|
284
|
+
audioEventsSkipped: audioSkipped,
|
|
285
|
+
flattenedRetimes,
|
|
286
|
+
droppedTransitions,
|
|
287
|
+
origin: ORIGIN,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
195
292
|
export function eventsToEDL(events, opts = {}) {
|
|
196
293
|
const fps = opts.fps || events.find((e) => e.fps)?.fps || 24;
|
|
197
294
|
const vids = events.filter((e) => e.track !== 'A').sort((a, b) => (a.recIn ?? 0) - (b.recIn ?? 0));
|
|
@@ -29,11 +29,24 @@ 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 assembleFromInterchangeSchema = z.object({
|
|
33
|
+
format: z.enum(['edl', 'otio', 'xml', 'aaf']).describe('Interchange format of the input'),
|
|
34
|
+
path: z.string().optional().describe('Path to the interchange file (aaf REQUIRES a path)'),
|
|
35
|
+
content: z.string().optional().describe('Inline interchange text (edl/otio/xml)'),
|
|
36
|
+
fps: z.number().optional().describe('Event frame rate for parsing (default 24; use e.g. 29.97 for NTSC EDLs)'),
|
|
37
|
+
sourceMap: z
|
|
38
|
+
.record(z.object({ mediaFilePath: z.string(), spec: z.object({}).passthrough() }))
|
|
39
|
+
.describe('reel/source name → {mediaFilePath, spec:{width,height,frameCount,fps}}; every video event must map'),
|
|
40
|
+
timelineName: z.string().optional(),
|
|
41
|
+
outputPath: z.string().describe('Where the importable .drt is written'),
|
|
42
|
+
targetAppVersion: z.union([z.string(), z.number()]).optional()
|
|
43
|
+
.describe("Host Resolve version, e.g. '19.1' for pre-21"),
|
|
44
|
+
});
|
|
32
45
|
const assembleSchema = z.object({
|
|
33
46
|
spec: z
|
|
34
47
|
.object({})
|
|
35
48
|
.passthrough()
|
|
36
|
-
.describe("assembleTimeline spec: { timelineName?,
|
|
49
|
+
.describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
|
|
37
50
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
38
51
|
targetAppVersion: z
|
|
39
52
|
.union([z.string(), z.number()])
|
|
@@ -135,7 +148,7 @@ function requirePathArg(args, key, action) {
|
|
|
135
148
|
export const drtTool = {
|
|
136
149
|
name: 'drt',
|
|
137
150
|
description:
|
|
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).',
|
|
151
|
+
'DaVinci Resolve Timeline (.drt) operations — offline, no Resolve required. Actions: assemble_from_interchange (EDL/OTIO/XML/AAF + sourceMap → IMPORTABLE RENDERING native .drt in one call; retimes flatten, transitions become cuts, ledger in `conform`), 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).',
|
|
139
152
|
async handler({ action, args }) {
|
|
140
153
|
if (action === 'parse') {
|
|
141
154
|
const p = parseSchema.parse(requirePathArg(args, 'drtPath', 'parse'));
|
|
@@ -190,6 +203,52 @@ export const drtTool = {
|
|
|
190
203
|
await fs.writeFile(p.outputPath, outBuf);
|
|
191
204
|
return { outputPath: p.outputPath, bytes: outBuf.length, seqContainersInjected: injected, projectFolder };
|
|
192
205
|
}
|
|
206
|
+
if (action === 'assemble_from_interchange') {
|
|
207
|
+
// Coast-to-coast conform: interchange in, importable RENDERING .drt out.
|
|
208
|
+
const p = assembleFromInterchangeSchema.parse(args);
|
|
209
|
+
const { parseInterchange } = await import('../editorial.mjs');
|
|
210
|
+
const { eventsToAssembleSpec } = await import('../author-interchange.mjs');
|
|
211
|
+
let content = p.content;
|
|
212
|
+
if (p.format === 'aaf') {
|
|
213
|
+
if (!p.path) return { error: 'aaf input requires path' };
|
|
214
|
+
content = p.path;
|
|
215
|
+
} else if (!content) {
|
|
216
|
+
if (!p.path) return { error: 'provide content or path' };
|
|
217
|
+
content = await fs.readFile(p.path, 'utf8');
|
|
218
|
+
}
|
|
219
|
+
const events = parseInterchange(p.format, content, { fps: p.fps ?? 24 });
|
|
220
|
+
if (!events || !events.length) return { error: 'no events parsed from the interchange input' };
|
|
221
|
+
const { spec, report } = eventsToAssembleSpec(events, {
|
|
222
|
+
sourceMap: p.sourceMap, timelineName: p.timelineName,
|
|
223
|
+
});
|
|
224
|
+
if (p.targetAppVersion !== undefined) {
|
|
225
|
+
spec.templateVersion = parseFloat(p.targetAppVersion) >= 21 ? 21 : 19;
|
|
226
|
+
}
|
|
227
|
+
const { assembleTimeline } = drp();
|
|
228
|
+
const { buffer, timelineName, mediaDescriptor } = await assembleTimeline(spec);
|
|
229
|
+
let outBuf = buffer;
|
|
230
|
+
let stamped = null;
|
|
231
|
+
if (p.targetAppVersion !== undefined) {
|
|
232
|
+
const targetPV = resolveTargetProjectVersion({ targetAppVersion: p.targetAppVersion });
|
|
233
|
+
const appVer = `${p.targetAppVersion}${'.0'.repeat(Math.max(0, 4 - String(p.targetAppVersion).split('.').length))}`;
|
|
234
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
235
|
+
const { out } = await applyVersionStamps(zip, targetPV, appVer);
|
|
236
|
+
outBuf = await out.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
237
|
+
stamped = { targetProjectVersion: targetPV };
|
|
238
|
+
}
|
|
239
|
+
await fs.writeFile(p.outputPath, outBuf);
|
|
240
|
+
return {
|
|
241
|
+
outputPath: p.outputPath,
|
|
242
|
+
bytes: outBuf.length,
|
|
243
|
+
timelineName,
|
|
244
|
+
mediaDescriptor,
|
|
245
|
+
stamped,
|
|
246
|
+
conform: report,
|
|
247
|
+
note:
|
|
248
|
+
'Import with timeline.import_timeline_checked (timeline is named after the FILE). ' +
|
|
249
|
+
'Retimes are flattened and transitions become cuts — see `conform` for the ledger.',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
193
252
|
if (action === 'assemble') {
|
|
194
253
|
const p = assembleSchema.parse(args);
|
|
195
254
|
// Native-schema authoring: template-spliced real Resolve structures
|
|
@@ -225,6 +284,14 @@ export const drtTool = {
|
|
|
225
284
|
stamped,
|
|
226
285
|
templateVersion: spec.templateVersion ?? 21,
|
|
227
286
|
mediaDescriptor: mediaDescriptor ?? 'none',
|
|
287
|
+
...((spec.elements || []).length && (spec.templateVersion ?? 21) < 21
|
|
288
|
+
? {
|
|
289
|
+
elementsWarning:
|
|
290
|
+
'title/generator elements on a pre-21 host are NOT render-verified: the harvested ' +
|
|
291
|
+
'snippets are Resolve-21 structures, which import and read back correctly but render ' +
|
|
292
|
+
'black on 19.1.3 (measured). Media cuts render; verify element output before delivery.',
|
|
293
|
+
}
|
|
294
|
+
: {}),
|
|
228
295
|
...(mediaDescriptor === 'repoint-fallback'
|
|
229
296
|
? {
|
|
230
297
|
warning:
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
const { createEmptyProject, addMediaClip, DEFAULT_START_FRAME } = require('./author-project');
|
|
25
|
-
const { loadMediaTemplate, transplantMediaElement } = require('./media-template-cache');
|
|
25
|
+
const { loadMediaTemplate, transplantMediaElement, insertMediaElement } = require('./media-template-cache');
|
|
26
26
|
const JSZip = require('jszip');
|
|
27
27
|
const { cutSourceIntoClips } = require('./cut-media');
|
|
28
28
|
const { placeFusionTitle } = require('./place-fusion-title');
|
|
@@ -35,24 +35,31 @@ async function assembleTimeline(spec = {}) {
|
|
|
35
35
|
if (!Array.isArray(transitions)) throw new TypeError('assembleTimeline: transitions must be an array');
|
|
36
36
|
|
|
37
37
|
let base;
|
|
38
|
+
let mediaDescriptorState = 'none';
|
|
38
39
|
if (media) {
|
|
39
|
-
// Media authoring:
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// Media authoring: cut one or MORE sources into placements. Every source
|
|
41
|
+
// carries {mediaFilePath, spec, cuts}. Multi-source requires a native
|
|
42
|
+
// media template CAPTURED for every source (capture-once transplant is
|
|
43
|
+
// the only measured way authored media renders); single-source may fall
|
|
44
|
+
// back to descriptor repoint, which imports but usually will not render.
|
|
45
|
+
const sources = Array.isArray(media) ? media : [media];
|
|
46
|
+
if (!sources.length) throw new TypeError('assembleTimeline: media must not be empty');
|
|
47
|
+
const caches = sources.map((src) => loadMediaTemplate(src.mediaFilePath));
|
|
48
|
+
if (sources.length > 1) {
|
|
49
|
+
const missing = sources.filter((src, i) => !caches[i]).map((src) => src.mediaFilePath);
|
|
50
|
+
if (missing.length) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'assembleTimeline: multi-source authoring requires a captured native media template for EVERY source — ' +
|
|
53
|
+
`missing for: ${missing.join(', ')}. Run media_pool.capture_media_template(path) with Resolve open for each.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
44
56
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (Array.isArray(cuts) && cuts.length) {
|
|
49
|
-
// Placement guards, loudly: clips before the timeline origin are
|
|
50
|
-
// DROPPED by Resolve on import with no error, and a source range past
|
|
51
|
-
// the media's end reads back as a truncated clip.
|
|
52
|
-
cuts.forEach((cut, i) => {
|
|
57
|
+
const validateCuts = (src, label) => {
|
|
58
|
+
const mediaSpec = src.spec;
|
|
59
|
+
(src.cuts || []).forEach((cut, i) => {
|
|
53
60
|
if (cut.startFrame < DEFAULT_START_FRAME) {
|
|
54
61
|
throw new RangeError(
|
|
55
|
-
`assembleTimeline:
|
|
62
|
+
`assembleTimeline: ${label}.cuts[${i}].startFrame ${cut.startFrame} is before the timeline origin ${DEFAULT_START_FRAME} — Resolve silently drops it on import`,
|
|
56
63
|
);
|
|
57
64
|
}
|
|
58
65
|
if (mediaSpec && Number.isFinite(mediaSpec.frameCount) && Number.isFinite(mediaSpec.fps)) {
|
|
@@ -61,12 +68,55 @@ async function assembleTimeline(spec = {}) {
|
|
|
61
68
|
const maxTimelineFrames = Math.floor((mediaSpec.frameCount / mediaSpec.fps) * 24);
|
|
62
69
|
if ((cut.srcIn ?? 0) + cut.durationFrames > maxTimelineFrames) {
|
|
63
70
|
throw new RangeError(
|
|
64
|
-
`assembleTimeline:
|
|
71
|
+
`assembleTimeline: ${label}.cuts[${i}] reads past the media's end — (srcIn ?? 0) + durationFrames exceeds ${maxTimelineFrames} timeline frames (media ${mediaSpec.frameCount} frames @ ${mediaSpec.fps} fps on the 24fps template timeline)`,
|
|
65
72
|
);
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
});
|
|
69
|
-
|
|
76
|
+
};
|
|
77
|
+
sources.forEach((src, i) => validateCuts(src, sources.length > 1 ? `media[${i}]` : 'media'));
|
|
78
|
+
|
|
79
|
+
base = await addMediaClip({
|
|
80
|
+
mediaFile: sources[0].mediaFilePath, spec: sources[0].spec, timelineName, templateVersion,
|
|
81
|
+
});
|
|
82
|
+
base.startFrame = DEFAULT_START_FRAME;
|
|
83
|
+
|
|
84
|
+
// Transplant/insert native pool elements BEFORE cutting, so each cut can
|
|
85
|
+
// reference its source's MediaRef.
|
|
86
|
+
let zip = await JSZip.loadAsync(base.buffer);
|
|
87
|
+
const mpPath = 'MediaPool/Master/MpFolder.xml';
|
|
88
|
+
let mpXml = await zip.file(mpPath).async('string');
|
|
89
|
+
const seqNames = Object.keys(zip.files).filter((n) => /SeqContainer\/.+\.xml$/.test(n) || /\/SeqContainer\d*\.xml$/.test(n));
|
|
90
|
+
const seqXmls = [];
|
|
91
|
+
for (const n of seqNames) seqXmls.push(await zip.file(n).async('string'));
|
|
92
|
+
const mediaRefs = [];
|
|
93
|
+
if (caches[0]) {
|
|
94
|
+
const res = transplantMediaElement(mpXml, seqXmls, caches[0]);
|
|
95
|
+
mpXml = res.mpXml;
|
|
96
|
+
for (let i = 0; i < seqNames.length; i += 1) seqXmls[i] = res.seqXmls[i];
|
|
97
|
+
mediaRefs[0] = caches[0].mediaRef;
|
|
98
|
+
mediaDescriptorState = 'native-transplant';
|
|
99
|
+
} else {
|
|
100
|
+
mediaRefs[0] = null; // donor already points at the repointed entry
|
|
101
|
+
mediaDescriptorState = 'repoint-fallback';
|
|
102
|
+
}
|
|
103
|
+
for (let i = 1; i < sources.length; i += 1) {
|
|
104
|
+
mpXml = insertMediaElement(mpXml, caches[i].poolElement);
|
|
105
|
+
mediaRefs[i] = caches[i].mediaRef;
|
|
106
|
+
}
|
|
107
|
+
zip.file(mpPath, mpXml);
|
|
108
|
+
seqNames.forEach((n, i) => zip.file(n, seqXmls[i]));
|
|
109
|
+
base.buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
110
|
+
|
|
111
|
+
const allCuts = [];
|
|
112
|
+
sources.forEach((src, i) => {
|
|
113
|
+
(src.cuts || []).forEach((cut) => {
|
|
114
|
+
allCuts.push(mediaRefs[i] ? { ...cut, mediaRef: mediaRefs[i] } : { ...cut });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
allCuts.sort((a, b) => a.startFrame - b.startFrame);
|
|
118
|
+
if (allCuts.length) {
|
|
119
|
+
const cutRes = await cutSourceIntoClips(base.buffer, { cuts: allCuts });
|
|
70
120
|
base.buffer = cutRes.buffer;
|
|
71
121
|
}
|
|
72
122
|
} else {
|
|
@@ -81,12 +131,12 @@ async function assembleTimeline(spec = {}) {
|
|
|
81
131
|
({ buffer } = await placeFusionTitle(buffer, {
|
|
82
132
|
trackIndex: el.track, startFrame: el.startFrame, durationFrames: el.durationFrames,
|
|
83
133
|
text: el.text, font: el.font, style: el.style, size: el.size, color: el.color,
|
|
84
|
-
vJustify: el.vJustify, hJustify: el.hJustify, name: el.name,
|
|
134
|
+
vJustify: el.vJustify, hJustify: el.hJustify, name: el.name, templateVersion,
|
|
85
135
|
}));
|
|
86
136
|
} else if (el.type === 'generator') {
|
|
87
137
|
({ buffer } = await placeGenerator(buffer, {
|
|
88
138
|
generatorName: el.generatorName, trackIndex: el.track,
|
|
89
|
-
startFrame: el.startFrame, durationFrames: el.durationFrames,
|
|
139
|
+
startFrame: el.startFrame, durationFrames: el.durationFrames, templateVersion,
|
|
90
140
|
}));
|
|
91
141
|
} else {
|
|
92
142
|
throw new Error(`assembleTimeline: elements[${i}] unknown type "${el.type}" (title|generator)`);
|
|
@@ -100,32 +150,7 @@ async function assembleTimeline(spec = {}) {
|
|
|
100
150
|
}));
|
|
101
151
|
}
|
|
102
152
|
|
|
103
|
-
|
|
104
|
-
// for this file, swap the pool media element and rewire MediaRefs — the
|
|
105
|
-
// only measured way an authored timeline's media actually RENDERS (the
|
|
106
|
-
// repoint fallback imports and reads back fine but the render engine
|
|
107
|
-
// refuses or paints black; see media-template-cache).
|
|
108
|
-
let mediaDescriptor = 'none';
|
|
109
|
-
if (media && media.mediaFilePath) {
|
|
110
|
-
const cached = loadMediaTemplate(media.mediaFilePath);
|
|
111
|
-
if (cached) {
|
|
112
|
-
const zip = await JSZip.loadAsync(buffer);
|
|
113
|
-
const mpPath = 'MediaPool/Master/MpFolder.xml';
|
|
114
|
-
const seqNames = Object.keys(zip.files).filter((n) => /SeqContainer\/.+\.xml$/.test(n) || /\/SeqContainer\d*\.xml$/.test(n));
|
|
115
|
-
const mpXml = await zip.file(mpPath).async('string');
|
|
116
|
-
const seqXmls = [];
|
|
117
|
-
for (const n of seqNames) seqXmls.push(await zip.file(n).async('string'));
|
|
118
|
-
const res = transplantMediaElement(mpXml, seqXmls, cached);
|
|
119
|
-
zip.file(mpPath, res.mpXml);
|
|
120
|
-
seqNames.forEach((n, i) => zip.file(n, res.seqXmls[i]));
|
|
121
|
-
buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
122
|
-
mediaDescriptor = 'native-transplant';
|
|
123
|
-
} else {
|
|
124
|
-
mediaDescriptor = 'repoint-fallback';
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return { buffer, timelineName: tlName, startFrame, mediaDescriptor };
|
|
153
|
+
return { buffer, timelineName: tlName, startFrame, mediaDescriptor: mediaDescriptorState };
|
|
129
154
|
}
|
|
130
155
|
|
|
131
156
|
module.exports = { assembleTimeline };
|
|
@@ -49,6 +49,9 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
49
49
|
if (cut.srcIn !== undefined && (!Number.isInteger(cut.srcIn) || cut.srcIn < 0)) {
|
|
50
50
|
throw new TypeError(`cutSourceIntoClips: cuts[${i}].srcIn must be a non-negative integer`);
|
|
51
51
|
}
|
|
52
|
+
if (cut.mediaRef !== undefined && !/^[0-9a-f-]{36}$/.test(cut.mediaRef)) {
|
|
53
|
+
throw new TypeError(`cutSourceIntoClips: cuts[${i}].mediaRef must be a uuid`);
|
|
54
|
+
}
|
|
52
55
|
});
|
|
53
56
|
|
|
54
57
|
const zip = await loadDrpZip(drpInput);
|
|
@@ -68,6 +71,11 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
|
|
|
68
71
|
c = setClipStart(c, cut.startFrame);
|
|
69
72
|
c = setClipDuration(c, cut.durationFrames);
|
|
70
73
|
c = setClipIn(c, cut.srcIn ?? 0);
|
|
74
|
+
if (cut.mediaRef) {
|
|
75
|
+
// Multi-source: point this cut at ITS source's transplanted pool
|
|
76
|
+
// element instead of the donor's.
|
|
77
|
+
c = c.replace(/<MediaRef>[0-9a-f-]{36}<\/MediaRef>/, `<MediaRef>${cut.mediaRef}</MediaRef>`);
|
|
78
|
+
}
|
|
71
79
|
return c;
|
|
72
80
|
});
|
|
73
81
|
if (trackType === 'video') {
|
|
@@ -68,4 +68,16 @@ function transplantMediaElement(mpXml, seqXmls, { poolElement, mediaRef }) {
|
|
|
68
68
|
return { mpXml: outMp, seqXmls: outSeqs };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
/** Insert an ADDITIONAL native media <Element> into MpFolder's MediaVec. */
|
|
72
|
+
function insertMediaElement(mpXml, poolElement) {
|
|
73
|
+
const close = mpXml.indexOf('</MediaVec>');
|
|
74
|
+
if (close < 0) throw new Error('insertMediaElement: no MediaVec in MpFolder');
|
|
75
|
+
const folderId = (mpXml.match(/<Sm2MpVideoClip[^>]*>[\s\S]*?<MpFolder>([0-9a-f-]{36})<\/MpFolder>/) || [])[1];
|
|
76
|
+
let element = poolElement;
|
|
77
|
+
if (folderId) {
|
|
78
|
+
element = element.replace(/<MpFolder>[0-9a-f-]{36}<\/MpFolder>/, `<MpFolder>${folderId}</MpFolder>`);
|
|
79
|
+
}
|
|
80
|
+
return mpXml.slice(0, close) + element + mpXml.slice(close);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { CACHE_DIR, cachePathFor, loadMediaTemplate, transplantMediaElement, insertMediaElement };
|
|
@@ -37,6 +37,22 @@ const {
|
|
|
37
37
|
} = require('./seq-surgery');
|
|
38
38
|
|
|
39
39
|
const TEMPLATE_PATH = path.join(__dirname, 'templates', 'fusion-title.xml');
|
|
40
|
+
// Snippets are generation-bound like the project templates: an R21-harvested
|
|
41
|
+
// clip imports on 19 and renders BLACK (measured 2026-08-30). r19 variant
|
|
42
|
+
// harvested live from Studio 19.1.3.7.
|
|
43
|
+
const TEMPLATE_PATH_R19 = path.join(__dirname, 'templates', 'fusion-title-r19.xml');
|
|
44
|
+
function snippetPathFor(templateVersion) {
|
|
45
|
+
// The r19 snippets are harvested but NOT yet render-viable: the generator's
|
|
46
|
+
// separate Sm2TiCompositionTable dependency is not carried, and the title's
|
|
47
|
+
// comp-blob patching assumes the R21 layout — with them, render jobs FAIL
|
|
48
|
+
// outright (measured), which is worse than the R21 snippet's silent black.
|
|
49
|
+
// Selection stays on the R21 snippet for every generation until the element
|
|
50
|
+
// transplant (comp table + per-generation blob patch) lands; drt.assemble
|
|
51
|
+
// warns when elements target a pre-21 host.
|
|
52
|
+
void templateVersion;
|
|
53
|
+
void TEMPLATE_PATH_R19;
|
|
54
|
+
return TEMPLATE_PATH;
|
|
55
|
+
}
|
|
40
56
|
|
|
41
57
|
/**
|
|
42
58
|
* Place a Fusion Title clip onto a chosen video track of a timeline in a .drp.
|
|
@@ -72,6 +88,7 @@ async function placeFusionTitle(drpInput, opts = {}) {
|
|
|
72
88
|
color,
|
|
73
89
|
timelineUuid,
|
|
74
90
|
titleClipXml,
|
|
91
|
+
templateVersion,
|
|
75
92
|
} = opts;
|
|
76
93
|
|
|
77
94
|
if (!Number.isInteger(startFrame)) throw new TypeError('placeFusionTitle: startFrame (int) is required');
|
|
@@ -82,7 +99,7 @@ async function placeFusionTitle(drpInput, opts = {}) {
|
|
|
82
99
|
let xml = seqXml;
|
|
83
100
|
|
|
84
101
|
// Prepare the title clip (clone + rewrite identity/timing).
|
|
85
|
-
let clip = titleClipXml || fs.readFileSync(
|
|
102
|
+
let clip = titleClipXml || fs.readFileSync(snippetPathFor(templateVersion), 'utf8');
|
|
86
103
|
clip = clip.trim();
|
|
87
104
|
if (!/PrettyType>\s*Fusion Title/.test(clip)) {
|
|
88
105
|
throw new Error('placeFusionTitle: template is not a Fusion Title clip');
|
|
@@ -28,6 +28,21 @@ const {
|
|
|
28
28
|
} = require('./seq-surgery');
|
|
29
29
|
|
|
30
30
|
const TEMPLATE_PATH = path.join(__dirname, 'templates', 'generator-solid-color.xml');
|
|
31
|
+
const TEMPLATE_PATH_R19 = path.join(__dirname, 'templates', 'generator-solid-color-r19.xml');
|
|
32
|
+
// Generation-bound like every other harvested structure (R21 snippet renders
|
|
33
|
+
// black on 19 — measured); r19 variant harvested live from 19.1.3.7.
|
|
34
|
+
function snippetPathFor(templateVersion) {
|
|
35
|
+
// The r19 snippets are harvested but NOT yet render-viable: the generator's
|
|
36
|
+
// separate Sm2TiCompositionTable dependency is not carried, and the title's
|
|
37
|
+
// comp-blob patching assumes the R21 layout — with them, render jobs FAIL
|
|
38
|
+
// outright (measured), which is worse than the R21 snippet's silent black.
|
|
39
|
+
// Selection stays on the R21 snippet for every generation until the element
|
|
40
|
+
// transplant (comp table + per-generation blob patch) lands; drt.assemble
|
|
41
|
+
// warns when elements target a pre-21 host.
|
|
42
|
+
void templateVersion;
|
|
43
|
+
void TEMPLATE_PATH_R19;
|
|
44
|
+
return TEMPLATE_PATH;
|
|
45
|
+
}
|
|
31
46
|
|
|
32
47
|
/**
|
|
33
48
|
* Place a built-in generator on a chosen video track.
|
|
@@ -43,7 +58,7 @@ const TEMPLATE_PATH = path.join(__dirname, 'templates', 'generator-solid-color.x
|
|
|
43
58
|
* generatorName:string, videoTrackCount:number, createdTracks:number}>}
|
|
44
59
|
*/
|
|
45
60
|
async function placeGenerator(drpInput, opts = {}) {
|
|
46
|
-
const { generatorName = 'Solid Color', trackIndex = 2, startFrame, durationFrames = 120, timelineUuid } = opts;
|
|
61
|
+
const { generatorName = 'Solid Color', trackIndex = 2, startFrame, durationFrames = 120, timelineUuid, templateVersion } = opts;
|
|
47
62
|
if (!Number.isInteger(startFrame)) throw new TypeError('placeGenerator: startFrame (int) is required');
|
|
48
63
|
if (!Number.isInteger(trackIndex) || trackIndex < 1) throw new TypeError('placeGenerator: trackIndex must be a positive integer');
|
|
49
64
|
if (/[<>]/.test(generatorName)) throw new Error('placeGenerator: generatorName must not contain < or >');
|
|
@@ -51,7 +66,7 @@ async function placeGenerator(drpInput, opts = {}) {
|
|
|
51
66
|
const zip = await loadDrpZip(drpInput);
|
|
52
67
|
const { entry, xml: seqXml, seqId } = await selectTargetSeq(zip, timelineUuid);
|
|
53
68
|
|
|
54
|
-
let gen = fs.readFileSync(
|
|
69
|
+
let gen = fs.readFileSync(snippetPathFor(templateVersion), 'utf8').trim();
|
|
55
70
|
gen = freshDbIds(gen);
|
|
56
71
|
gen = gen.replace(/<PrettyType>[\s\S]*?<\/PrettyType>/, `<PrettyType>${escapeXml(generatorName)}</PrettyType>`);
|
|
57
72
|
gen = gen.replace(/<Name>[\s\S]*?<\/Name>/, `<Name>${escapeXml(generatorName)}</Name>`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<Sm2TiVideoClip DbId="8c729549-aded-4c80-b143-c1fce23c5712">
|
|
2
|
+
<FieldsBlob>0000000200000027800a120a0e2205546578742b4a05546578742b200112100000000000000005ffffffffffffffff</FieldsBlob>
|
|
3
|
+
<PrettyType>Fusion Title</PrettyType>
|
|
4
|
+
<Name>Fusion Title</Name>
|
|
5
|
+
<Start>86400</Start>
|
|
6
|
+
<Duration>120</Duration>
|
|
7
|
+
<LinkedItemSync/>
|
|
8
|
+
<WasDisbanded>false</WasDisbanded>
|
|
9
|
+
<MarkersBA/>
|
|
10
|
+
<UiMemento>0</UiMemento>
|
|
11
|
+
<Flags>0</Flags>
|
|
12
|
+
<PriorityIndex>0</PriorityIndex>
|
|
13
|
+
<EffectFiltersBA/>
|
|
14
|
+
<ImportExportMetadataBA/>
|
|
15
|
+
<RenderTextEnabled>true</RenderTextEnabled>
|
|
16
|
+
<RenderTextGanged>true</RenderTextGanged>
|
|
17
|
+
<RenderTextPrefixed>true</RenderTextPrefixed>
|
|
18
|
+
<CompositionTable>
|
|
19
|
+
<Sm2TiCompositionTable DbId="fda722ba-c81f-4fa2-ae79-9f33b26a6bb1">
|
|
20
|
+
<FieldsBlob>000000010000000100000016004c006100730074004d006f006400540069006d0065000000040000000000002254b6</FieldsBlob>
|
|
21
|
+
<DbSavedTime>2309245</DbSavedTime>
|
|
22
|
+
<CompositionBA>000006b1789c75547f6c1445145e31d4585142a28d4222eb69c51ff763f7eedabb2bb4a6f67ab686daa3bd1e840acddcec6cbbedeece6677b677a536863420d5c81f448931261543c4a0a04453153585a860b531284ab46862556c4a358809218d4a9cd9dd6b8fa89bdbcbbc37efbd79effbbe598ee3aee138ee5afa967390c39ccde91ca1d612fa3a5bcb3881eba24ec0691c72c2e8b3926ba0b11a67d07f8b53688642573ac773e2428a4453087d99cd7165a906ac19d8528882757e906fb04d13e924a36888afe5053fdf867409996d40ef668e41e612c5043fe4e71f52710ea8ffb5e315694e527fc4cf37291be8829836f2f369150ce400eceb302440500b96bc63da093211f66c19a8168dcd22d3625dd5f2be24c82a3a5468371656fb110db72505f36222280623414110623e5a03f423a9d526864d2cb76a1352a50cc6aa6726150be45454ea5a8f61df558e7a56b855962d448a0058b6c6d21666708f685015c372067726b608d6928000c793696c49afafcf34763910f832a840eef7b1b8940934b490953691ec1a8c0467d1ac532000244abf0b6adac4850167d50e81d383e867a9f4975550decd662bb15834896460ab647127927496ad06a3d875b7f7e07c4736c3a8d3491641824dab883babc11ab64d944406e96103283a893bfdbb34b935b06dc2227b033af4e07194612b92376591b02276ed3a304a4f6aa12a3015a0b24691f9af46ffb711863956b1e9b5629224b22095aaa2772f0a88f9db8166a8c85a74a66c55a5c043c5d396d78a57340d488f7b740b7019e9f479333400d883ac1adf16d648a8c3a2ea0c5940b391da6d9b56df40a805f72bc80a39714c1d211416a02c08301046220c44e3914820511dad0e88f1980c130921918b89a1585c4e54e5c46820160d4b816804c881845c950b54272242541644291783a1388c851355d14400488806c1b810a0299100146588c211581513c3a194cd462ac2e3482d854d0d1097098a345d84a31e3fd4561893623056fac4998228da0c26f7fa3a26458c0e972ff15cadd4887366876e51c891e41ce87e1f1a7566791f86948af359b7cca04304bb78cd94350888c3e5822e525877db6ed470afe2593e57830652d5861e04fbdc00ddb9d30b0a6beed6b1893662537ac4d672c82ca17e71ab9e560086554abf2b29cd30916595941be20ed06f6e61eae93dc77b3f1496edbc7ccfdd336d5f0687cfbd719aef2ceb2c9f3dfae6d9570e1d3b327ce7e9c3fb2ed47dbcf5dea5abcbeb3e19affb76bc7279ee7aedc05ddb0bd3bf37dd706e9bb8ab2dbf7bdb99b28b2f4f6e3ef8e98a0bef4f4e3d7c5f2c3b29cf275f58f3e375aba64efe31bfeed2b1b53fe8ab6bef18bae9a3fcf21dcf066e3e31b6a7977ff0833978cbedafcf6eac8fd4ece6275e9d9b782b37b29fbff81820cfbc5673eaa5bd475b0f9f3cf1cecacd8faefafa2f587872c36cf4c6bf376daffcaee9d2f13ae3b315fbbee832767db56ee7955b9f5a3356c88f8c2f9d4e9f3f04464641e56d736f0f6f7a4e786ffe9b2bfed1ba1d4bb68e9e7af167f079fcf1113cb3e57cd9ee2315fba77fab1fffa5efa75fcb0eeac9e0e8c804ff67c5da5cf8ddb364eff7cf57cc3cf1003f76e6f23fb8ff3f78</CompositionBA>
|
|
23
|
+
<ActiveCompositionIdx>0</ActiveCompositionIdx>
|
|
24
|
+
</Sm2TiCompositionTable>
|
|
25
|
+
</CompositionTable>
|
|
26
|
+
<In/>
|
|
27
|
+
<MixedFrameRateAlignment>0</MixedFrameRateAlignment>
|
|
28
|
+
<MediaStartTime>0</MediaStartTime>
|
|
29
|
+
<MediaFilePath/>
|
|
30
|
+
<MediaReelNumber/>
|
|
31
|
+
<MediaFrameRate>00000000000038400000000000000000</MediaFrameRate>
|
|
32
|
+
<MediaTimemapBA>024013d55555555555</MediaTimemapBA>
|
|
33
|
+
<LastChangedTime>0</LastChangedTime>
|
|
34
|
+
<LastRenderedTime>0</LastRenderedTime>
|
|
35
|
+
<IsMarkedForCaching>false</IsMarkedForCaching>
|
|
36
|
+
<IsForceConformed>true</IsForceConformed>
|
|
37
|
+
<MatchConflictState>0</MatchConflictState>
|
|
38
|
+
<UseOppositeSrcForLeftEye>false</UseOppositeSrcForLeftEye>
|
|
39
|
+
<UseOppositeSrcForRightEye>false</UseOppositeSrcForRightEye>
|
|
40
|
+
<RenderCacheBA/>
|
|
41
|
+
<CurrentSelectorIdx>962869300</CurrentSelectorIdx>
|
|
42
|
+
<IsPreConformed>false</IsPreConformed>
|
|
43
|
+
<PreConformMediaExtents>00000100000030c20000010000003042</PreConformMediaExtents>
|
|
44
|
+
<MediaMetadata/>
|
|
45
|
+
<Thumbnail>
|
|
46
|
+
<BtThumnail DbId="ac0024f2-2407-47f8-9789-eafdbd585ae2">
|
|
47
|
+
<FieldsBlob/>
|
|
48
|
+
<ImgWidth>-1</ImgWidth>
|
|
49
|
+
<ImgHeight>-1</ImgHeight>
|
|
50
|
+
<Buffer/>
|
|
51
|
+
</BtThumnail>
|
|
52
|
+
</Thumbnail>
|
|
53
|
+
<ThumbnailDirtyFlag>true</ThumbnailDirtyFlag>
|
|
54
|
+
</Sm2TiVideoClip>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<Sm2TiGenerator DbId="9d9cd9a6-f64f-49b9-a661-18f4f9c51781">
|
|
2
|
+
<FieldsBlob/>
|
|
3
|
+
<PrettyType>Solid Color</PrettyType>
|
|
4
|
+
<Name>Solid Color</Name>
|
|
5
|
+
<Start>86520</Start>
|
|
6
|
+
<Duration>120</Duration>
|
|
7
|
+
<LinkedItemSync/>
|
|
8
|
+
<WasDisbanded>false</WasDisbanded>
|
|
9
|
+
<MarkersBA/>
|
|
10
|
+
<UiMemento>0</UiMemento>
|
|
11
|
+
<Flags>0</Flags>
|
|
12
|
+
<PriorityIndex>0</PriorityIndex>
|
|
13
|
+
<EffectFiltersBA/>
|
|
14
|
+
<ImportExportMetadataBA/>
|
|
15
|
+
<RenderTextEnabled>true</RenderTextEnabled>
|
|
16
|
+
<RenderTextGanged>true</RenderTextGanged>
|
|
17
|
+
<RenderTextPrefixed>true</RenderTextPrefixed>
|
|
18
|
+
<In/>
|
|
19
|
+
</Sm2TiGenerator>
|
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.108.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()}")
|