davinci-resolve-mcp 2.138.0 → 2.140.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 +54 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/native-drt-authoring.md +22 -0
- package/docs/reference/api-limitations.md +9 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/author-interchange.mjs +14 -6
- package/resolve-advanced/server/editorial.mjs +20 -1
- package/resolve-advanced/server/tools/drt.mjs +1 -1
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +24 -3
- package/resolve-advanced/vendor/drp-format/media-timemap.js +53 -1
- package/resolve-advanced/vendor/drp-format/place-transition.js +22 -2
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/api_truth.py +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
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.140.0 — the transition style registry
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Five more transition styles authored offline** — `transitions[].type:
|
|
10
|
+
'dip' | 'additive' | 'fade-to-color' | 'smooth-cut' | 'non-additive'`
|
|
11
|
+
(joining `dissolve` and `wipe`). The registry cracked in one probe:
|
|
12
|
+
`PrettyType` is the style selector — swapped onto the render-verified
|
|
13
|
+
dissolve skeleton, every style renders, each with its own midpoint
|
|
14
|
+
fingerprint on 19.1.3.7 (dip bottoms at pure black 16, additive saturates
|
|
15
|
+
at 233.8, fade-to-color plateaus dark at 77, smooth-cut blends at 179.9,
|
|
16
|
+
non-additive holds the brighter side past the cut). Unknown styles refuse
|
|
17
|
+
(unvetted strings are the measured crasher class).
|
|
18
|
+
- **XMEML `<transitionitem>` parsing** — dissolve-family effectids and wipe
|
|
19
|
+
names route to their styles automatically through
|
|
20
|
+
`assemble_from_interchange`; unrecognized effects fall back to a plain
|
|
21
|
+
dissolve rather than dropping (a blend at the right junction beats a hard
|
|
22
|
+
cut).
|
|
23
|
+
|
|
24
|
+
### Measured and closed (new Resolve bug, filed in api-limitations)
|
|
25
|
+
|
|
26
|
+
- **Resolve's own FCP7-XMEML importer writes video transitions that render
|
|
27
|
+
INERT** on 19.1.3: the transitionitem lands as a real element that reads
|
|
28
|
+
back through every API, but the outgoing clip plays through the window and
|
|
29
|
+
hard-cuts at its end — measured with both a plain Cross Dissolve and a Dip
|
|
30
|
+
to Color (E66, importSourceClips both ways). EDL-imported transitions
|
|
31
|
+
render fine; the defect is the XMEML path's element construction. Routing
|
|
32
|
+
the same XMEML through `assemble_from_interchange` authors transitions
|
|
33
|
+
that render (E69: the same dip turnover produced the exact 16.0 black
|
|
34
|
+
valley through our route). `docs/reference/api-limitations.md` carries the
|
|
35
|
+
new `bug` entry.
|
|
36
|
+
|
|
37
|
+
## What's New in v2.139.0 — speed ramps, and the easing crash law
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **Variable-speed ramps authored offline** (`cuts[].ramp:
|
|
42
|
+
[{durationFrames, speed}, …]` — two or more linear segments from the cut
|
|
43
|
+
head, `srcIn` honored). No harvest was needed: the engine honors
|
|
44
|
+
intermediate keyframes in the same seconds-domain keyed `Sm2TimeMap` the
|
|
45
|
+
constant retimes use. E63/E64 proof on 19.1.3.7: a 50%→100% knee read back
|
|
46
|
+
the exact source window AND rendered the predicted frame cadence (11/23
|
|
47
|
+
doubled frames in the half-speed window, none at full speed; the 2×
|
|
48
|
+
segment moved at 4.3× the 0.5× segment's per-frame motion), with `srcIn`
|
|
49
|
+
landing on the right source frame by luma.
|
|
50
|
+
|
|
51
|
+
### Measured and closed
|
|
52
|
+
|
|
53
|
+
- **Eased ramps are a crasher, not a boundary.** A keyframe with
|
|
54
|
+
`interp = 2` crashed Resolve outright on import (E65 — app death, headless
|
|
55
|
+
recovery per doctrine). The builders hardcode `interp = 0`, the test suite
|
|
56
|
+
asserts it, and the guide records the law: linear segments are the
|
|
57
|
+
authorable envelope on 19.1.3.
|
|
58
|
+
|
|
5
59
|
## What's New in v2.138.0 — wipes join the conform
|
|
6
60
|
|
|
7
61
|
### Added
|
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.140.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -43,10 +43,12 @@ window. `render.verify_output` covers the container-level checks.
|
|
|
43
43
|
| Multi-track video (V2+ stacking) | `cuts[].track` (video-only above V1) | v2.112 |
|
|
44
44
|
| Cross-dissolves | `transitions: [{track, atFrame, durationFrames}]` | v2.111 |
|
|
45
45
|
| Wipes (EDL `W`-codes) | `transitions[].type: 'wipe'` (single soft-edge style — host-importer parity) | v2.138 |
|
|
46
|
+
| Transition styles | `transitions[].type: 'dip' \| 'additive' \| 'fade-to-color' \| 'smooth-cut' \| 'non-additive'` (XMEML effectids route automatically) | v2.140 |
|
|
46
47
|
| Audio cross-fades | `transitions[].trackType: 'audio'` | v2.116 |
|
|
47
48
|
| Constant retimes, forward | `cuts[].speed` (e.g. `0.5`) | v2.113 |
|
|
48
49
|
| Constant retimes, reverse | `cuts[].reverse` | v2.114 |
|
|
49
50
|
| Freeze frames | `cuts[].freeze` (holds source frame `srcIn`) | v2.134 |
|
|
51
|
+
| Speed ramps | `cuts[].ramp: [{durationFrames, speed}, …]` (linear segments only) | v2.139 |
|
|
50
52
|
| Audio placements, A1–A8 | `cuts[].audioOnly + track` | v2.115 |
|
|
51
53
|
| Built-in generators | `elements: [{type:'generator', generatorName}]` | v2.110 |
|
|
52
54
|
| Custom start timecode | `spec.startFrame` / `preserveStartTimecode` | v2.117 |
|
|
@@ -96,6 +98,14 @@ the clip's `<In>`/`<Duration>` window into it in record-domain frames.
|
|
|
96
98
|
Reverse is the same map with the Y endpoints swapped, and `In` then measures
|
|
97
99
|
from the source end.
|
|
98
100
|
|
|
101
|
+
**Ramps are just more keyframes — but easing is a crasher.** The engine
|
|
102
|
+
honors intermediate keyframes in the same seconds-domain map (E63/E64:
|
|
103
|
+
a synthesized 50%→100% knee read back AND rendered with the exact predicted
|
|
104
|
+
frame cadence, srcIn baked into the first keyframe's Y, clip `In` at the cut
|
|
105
|
+
head). `cuts[].ramp` authors piecewise-constant segments. The keyframes'
|
|
106
|
+
`interp` field must stay **0**: an `interp=2` keyframe crashed Resolve
|
|
107
|
+
19.1.3 outright on import (app death, E65) — eased ramps are not authorable.
|
|
108
|
+
|
|
99
109
|
**Freezes are a third shape, not a flat retime.** A flat line in the retime's
|
|
100
110
|
frame domain reads back frozen but *renders moving* — the one divergence that
|
|
101
111
|
runs in the "working" direction. The real freeze (harvested from a live EDL
|
|
@@ -109,6 +119,18 @@ and zero-speed warps route to it through `assemble_from_interchange`.
|
|
|
109
119
|
(01:00:00:00 @ 24fps). Clips placed before the origin are silently dropped
|
|
110
120
|
by Resolve on import — `startFrame` is timeline-absolute.
|
|
111
121
|
|
|
122
|
+
**Transition styles live in PrettyType — and the host XMEML importer is
|
|
123
|
+
inert.** Swapping `PrettyType` on the working dissolve skeleton renders the
|
|
124
|
+
named style (midpoint fingerprints: dip bottoms at pure black 16, additive
|
|
125
|
+
saturates at 233.8, fade-to-color plateaus dark at 77, smooth-cut blends at
|
|
126
|
+
179.9, non-additive holds the brighter side). Within `Cross Dissolve`, the
|
|
127
|
+
FieldsBlob style-id distinguishes dissolve from wipe. Resolve's OWN FCP7-XMEML
|
|
128
|
+
importer writes video `<transitionitem>`s that read back but render INERT on
|
|
129
|
+
19.1.3 (measured: both a plain Cross Dissolve and a Dip to Color played the
|
|
130
|
+
outgoing clip through the window) — so routing an XMEML turnover through
|
|
131
|
+
`assemble_from_interchange` produces transitions that render where the host
|
|
132
|
+
importer's do not.
|
|
133
|
+
|
|
112
134
|
**Dissolve geometry.** A transition is authored only when the predecessor
|
|
113
135
|
ends exactly at the cut and both sides have handle media for the centered
|
|
114
136
|
span (incoming `srcIn ≥ dur/2`; outgoing `srcIn + dur + dur/2 ≤ frameCount`).
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 35 missing capabilities,
|
|
15
|
+
**Totals:** 35 missing capabilities, 46 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
|
|
@@ -663,3 +663,11 @@ values, or automation-hostile modal prompts.
|
|
|
663
663
|
- **Behavior:** Deleting or closing a project while its render job is still running orphans the render and wedges the whole render pipeline: the output file stops growing and Resolve idles at 0% CPU, IsRenderingInProgress on the NEXT current project reports True indefinitely, StopRendering does not clear it, NEW render jobs sit at 0% forever (then StartRendering starts returning False), and Resolve.Quit() is refused because the app believes a render is running — even project creation can start returning None behind the quit-confirm dialog (reproduced live on Studio 19.1.3.7, 2026-08-29).
|
|
664
664
|
- **Workaround / current handling:** Never close or delete a project while IsRenderingInProgress is True — StopRendering first, wait for False, then close. Once wedged, only a manual quit (confirming the dialog) or force-quit clears it; treat a True that persists at 0% CPU with a static output file as stuck rather than rendering. Poll GetRenderJobStatus for completion instead of IsRenderingInProgress, which this failure poisons.
|
|
665
665
|
- **Tags:** render, silent-failure, unreliable-return
|
|
666
|
+
|
|
667
|
+
### MediaPool.ImportTimelineFromFile (FCP7 XMEML video transitions render inert)
|
|
668
|
+
|
|
669
|
+
- **Object:** `MediaPool`
|
|
670
|
+
- **Signature:** `(filePath, {importOptions}) -> Timeline`
|
|
671
|
+
- **Behavior:** Video <transitionitem>s imported from an FCP7 XMEML land as real Sm2TiTransition elements that READ BACK through the item APIs but render INERT: the outgoing clip plays through the transition window and hard-cuts at its end. Measured on Studio 19.1.3.7 with both a plain Cross Dissolve and a Dip to Color Dissolve (midpoint frames byte-matched the outgoing clip; no blend, no dip). The same transition elements authored offline with a correct FieldsBlob render perfectly, and EDL-imported dissolves/wipes also render — the defect is specific to the XMEML import path's element construction.
|
|
672
|
+
- **Workaround / current handling:** Do not trust an XMEML-imported timeline's transitions without a render probe at a junction midpoint. To conform an XMEML turnover with working transitions, route it through drt.assemble_from_interchange (format 'xml'), which authors render-verified dissolves, wipes, and the dissolve-family styles from the same <transitionitem> data.
|
|
673
|
+
- **Tags:** timeline, import, xmeml, transition, readback, silent-failure
|
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.140.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
|
@@ -425,12 +425,20 @@ export function eventsToAssembleSpec(events, opts = {}) {
|
|
|
425
425
|
});
|
|
426
426
|
continue;
|
|
427
427
|
}
|
|
428
|
-
//
|
|
429
|
-
// W-
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
|
|
428
|
+
// Style mapping (all render-verified E61/E67/E68, midpoint-fingerprinted):
|
|
429
|
+
// EDL W-codes and XMEML wipe effectids → the single wipe style Resolve's
|
|
430
|
+
// own EDL importer maps every W-code to; XMEML dissolve-family effectids
|
|
431
|
+
// → their PrettyType styles; anything unrecognized → plain dissolve
|
|
432
|
+
// (never dropped — a blend at the right junction beats a hard cut).
|
|
433
|
+
const raw = String(c.type || '');
|
|
434
|
+
let kind = 'dissolve';
|
|
435
|
+
if (/^W\d*/i.test(raw) || /wipe/i.test(raw)) kind = 'wipe';
|
|
436
|
+
else if (/dip to color/i.test(raw)) kind = 'dip';
|
|
437
|
+
else if (/non-additive/i.test(raw)) kind = 'non-additive';
|
|
438
|
+
else if (/additive/i.test(raw)) kind = 'additive';
|
|
439
|
+
else if (/fade to color/i.test(raw)) kind = 'fade-to-color';
|
|
440
|
+
else if (/smooth cut/i.test(raw)) kind = 'smooth-cut';
|
|
441
|
+
transitions.push({ track: c.track, atFrame: c.atFrame, durationFrames: c.durationFrames, ...(kind === 'dissolve' ? {} : { type: kind }) });
|
|
434
442
|
}
|
|
435
443
|
// Audio cross-fades, same geometry rules (render-verified on 19.1.3.7 via
|
|
436
444
|
// the harvested cross-fade template: the highpass RMS ramps, not steps).
|
|
@@ -240,7 +240,26 @@ export function parseXMEMLEvents(xml, opts = {}) {
|
|
|
240
240
|
const media = seq && seq.media;
|
|
241
241
|
if (media) {
|
|
242
242
|
const vtracks = media.video && media.video.track ? (Array.isArray(media.video.track) ? media.video.track : [media.video.track]) : [];
|
|
243
|
-
vtracks.forEach((t, vi) => {
|
|
243
|
+
vtracks.forEach((t, vi) => {
|
|
244
|
+
const label = vi === 0 ? 'V' : `V${vi + 1}`;
|
|
245
|
+
const before = events.length;
|
|
246
|
+
if (t.clipitem) walk(t.clipitem, label);
|
|
247
|
+
// <transitionitem> siblings: attach each to the INCOMING clip event —
|
|
248
|
+
// the one whose record-in falls inside the transition's span. Resolve's
|
|
249
|
+
// OWN XMEML importer writes these as elements that render INERT on
|
|
250
|
+
// 19.1.3 (measured E66: dip AND plain cross dissolve both played the
|
|
251
|
+
// outgoing clip through the window, hard cut at the end), so routing
|
|
252
|
+
// the turnover through assemble_from_interchange authors transitions
|
|
253
|
+
// that actually render where the host importer's do not.
|
|
254
|
+
const titems = t.transitionitem ? (Array.isArray(t.transitionitem) ? t.transitionitem : [t.transitionitem]) : [];
|
|
255
|
+
for (const tr of titems) {
|
|
256
|
+
const s = Number(tr.start), e2 = Number(tr.end);
|
|
257
|
+
if (!Number.isFinite(s) || !Number.isFinite(e2)) continue;
|
|
258
|
+
const effectId = (tr.effect && (tr.effect.effectid || tr.effect.name)) || 'Cross Dissolve';
|
|
259
|
+
const incoming = events.slice(before).find((ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2);
|
|
260
|
+
if (incoming) incoming.transition = { type: String(effectId), duration: e2 - s };
|
|
261
|
+
}
|
|
262
|
+
});
|
|
244
263
|
const atracks = media.audio && media.audio.track ? (Array.isArray(media.audio.track) ? media.audio.track : [media.audio.track]) : [];
|
|
245
264
|
for (const t of atracks) if (t.clipitem) walk(t.clipitem, 'A');
|
|
246
265
|
}
|
|
@@ -51,7 +51,7 @@ const assembleSchema = z.object({
|
|
|
51
51
|
spec: z
|
|
52
52
|
.object({})
|
|
53
53
|
.passthrough()
|
|
54
|
-
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),freeze? (true = hold source frame srcIn for the whole cut; video-only; render-proven frozen on 19 via freezedetect),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' | 'audio' cross-fade), type? ('dissolve' default | 'wipe'
|
|
54
|
+
.describe("assembleTimeline spec: { timelineName?, startFrame? (timeline start frame @24, default 86400=01:00:00:00 — sets the start TIMECODE, render-verified on 19), media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?,track? (1-based video track; >1 = video-only, render-verified stacking),speed?/reverse? (constant retime, e.g. 0.5, forward or backwards; video-only; readback+render-verified on 19),freeze? (true = hold source frame srcIn for the whole cut; video-only; render-proven frozen on 19 via freezedetect),ramp? ([{durationFrames,speed},...] >=2 LINEAR segments from the cut head, srcIn honored; video-only; render-proven cadence on 19 — eased/curved ramps are NOT authorable, an interp!=0 keyframe crashes Resolve on import (measured)),audioOnly?+track? (explicit AUDIO placement on audio track 1-8; presence suppresses the A1 mirror; render-verified on 19)}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), transitions?: [{track, atFrame, durationFrames?, trackType? ('video' | 'audio' cross-fade), type? ('dissolve' default | 'wipe' | 'dip' | 'additive' | 'fade-to-color' | 'smooth-cut' | 'non-additive' — all render-verified on 19 w/ midpoint fingerprints; XMEML/EDL transition codes route automatically, and NOTE Resolve's own XMEML importer writes transitions that render INERT, so this route beats it)}], markers?: [{frame (timeline-absolute), color? (16 names), name?, note?, duration?, customData?}] (readback-verified on 19), compounds?: [{name, startFrame (parent, absolute), durationFrames, track?, cuts:[{mediaFilePath, startFrame (INNER, 0-based), durationFrames, srcIn?}], compounds?: [same, nested — frames inner-relative]}] (multiple PARALLEL compounds compose AND compounds NEST recursively — depth-2 AND depth-3 playback render-verified on 19 (the old depth-2 black was a missing SequenceSetup key, fixed); inner cuts need captured templates w/ native clips), subtitles?: [{startFrame (timeline-absolute), durationFrames, text}] + subtitlesSrt? (raw SRT, cues anchor at the origin; readback-verified on 19; angle-bracket runs read as SRT markup), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, generatorName? ('Solid Color'|'SMPTE Color Bar'|'Grey Scale' render-verified on 19), ...}] }. startFrame is timeline-absolute (origin 86400)."),
|
|
55
55
|
outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
|
|
56
56
|
targetAppVersion: z
|
|
57
57
|
.union([z.string(), z.number()])
|
|
@@ -25,7 +25,7 @@ const { createEmptyProject, addMediaClip, DEFAULT_START_FRAME } = require('./aut
|
|
|
25
25
|
const { loadMediaTemplate, transplantMediaElement, insertMediaElement } = require('./media-template-cache');
|
|
26
26
|
const JSZip = require('jszip');
|
|
27
27
|
const { cutSourceIntoClips } = require('./cut-media');
|
|
28
|
-
const { buildConstantSpeedTimemapKeyed, buildFreezeTimemapKeyed } = require('./media-timemap');
|
|
28
|
+
const { buildConstantSpeedTimemapKeyed, buildFreezeTimemapKeyed, buildRampTimemapKeyed } = require('./media-timemap');
|
|
29
29
|
const { encodeTimelineMarkersBlob } = require('./timeline-markers-blob');
|
|
30
30
|
const { placeSubtitles, parseSrt } = require('./place-subtitles');
|
|
31
31
|
const { placeCompound } = require('./place-compound');
|
|
@@ -78,7 +78,9 @@ async function assembleTimeline(spec = {}) {
|
|
|
78
78
|
// srcIn/duration are TIMELINE frames (24fps template); the media's
|
|
79
79
|
// extent converts: frameCount / mediaFps × 24.
|
|
80
80
|
const maxTimelineFrames = Math.floor((mediaSpec.frameCount / mediaSpec.fps) * 24);
|
|
81
|
-
|
|
81
|
+
// Ramps consume sum(duration × speed) source, not durationFrames —
|
|
82
|
+
// the ramp builder enforces its own source-extent bound.
|
|
83
|
+
if (!Array.isArray(cut.ramp) && (cut.srcIn ?? 0) + cut.durationFrames > maxTimelineFrames) {
|
|
82
84
|
throw new RangeError(
|
|
83
85
|
`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)`,
|
|
84
86
|
);
|
|
@@ -137,7 +139,26 @@ async function assembleTimeline(spec = {}) {
|
|
|
137
139
|
// captured before clip elements were harvested.
|
|
138
140
|
if (caches[i] && caches[i].videoClipElement) out.donorClipVideo = caches[i].videoClipElement;
|
|
139
141
|
if (caches[i] && caches[i].audioClipElement) out.donorClipAudio = caches[i].audioClipElement;
|
|
140
|
-
if (
|
|
142
|
+
if (Array.isArray(cut.ramp)) {
|
|
143
|
+
// VARIABLE-SPEED RAMP (E63, render-verified): piecewise-constant
|
|
144
|
+
// segments as one multi-keyframe Sm2TimeMap. Record domain starts
|
|
145
|
+
// at the cut head (In stays 0); srcIn bakes into the map's first
|
|
146
|
+
// keyframe. Video-only, like every retime.
|
|
147
|
+
if (cut.freeze || cut.reverse || (cut.speed !== undefined && cut.speed !== 1)) {
|
|
148
|
+
throw new RangeError('assembleTimeline: cut.ramp cannot combine with speed/reverse/freeze');
|
|
149
|
+
}
|
|
150
|
+
const fpsR = Math.round(src.spec.fps || 24);
|
|
151
|
+
const total = cut.ramp.reduce((s, seg) => s + (seg && seg.durationFrames || 0), 0);
|
|
152
|
+
if (total !== cut.durationFrames) {
|
|
153
|
+
throw new RangeError(`assembleTimeline: ramp segments sum to ${total} frames but the cut is ${cut.durationFrames}`);
|
|
154
|
+
}
|
|
155
|
+
out.timemap = buildRampTimemapKeyed({
|
|
156
|
+
segments: cut.ramp, srcIn: cut.srcIn ?? 0,
|
|
157
|
+
sourceFrames: src.spec.frameCount, fps: fpsR, uniqueId: randomUUID(),
|
|
158
|
+
}).toString('hex');
|
|
159
|
+
out.srcIn = 0; // record-domain In is the cut head; the map carries srcIn
|
|
160
|
+
delete out.ramp;
|
|
161
|
+
} else if (cut.freeze || cut.speed === 0) {
|
|
141
162
|
// FREEZE frame (r19 keyed map harvested from a live EDL M2 000.0
|
|
142
163
|
// import, E55; render-proven frozen). Holds source frame srcIn for
|
|
143
164
|
// the whole cut. The frozen clip's <In> stays EMPTY; the position
|
|
@@ -304,10 +304,62 @@ function buildFreezeTimemapKeyed({ freezeFrame, sourceFrames, fps = 24, uniqueId
|
|
|
304
304
|
] });
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/**
|
|
308
|
+
* VARIABLE-SPEED RAMP — piecewise-constant speed segments as a multi-keyframe
|
|
309
|
+
* r19 keyed Sm2TimeMap. E63 (2026-08-31): the engine honors intermediate
|
|
310
|
+
* keyframes with the SAME seconds-domain conventions as the constant maps —
|
|
311
|
+
* a synthesized 3-keyframe 50%→100% ramp read back source 0..36 over 48
|
|
312
|
+
* record frames AND rendered with exactly the predicted cadence (11/23
|
|
313
|
+
* doubled frames in the 50% window, 0/24 at 100%). Record domain starts at
|
|
314
|
+
* the cut head (clip In stays 0); srcIn bakes into the first keyframe's Y.
|
|
315
|
+
*
|
|
316
|
+
* @param {Array<{durationFrames:number, speed:number}>} segments - record-domain
|
|
317
|
+
* pieces, in order from the cut head; speeds are source/record multipliers.
|
|
318
|
+
*/
|
|
319
|
+
function buildRampTimemapKeyed({ segments, srcIn = 0, sourceFrames, fps = 24, uniqueId }) {
|
|
320
|
+
if (!Array.isArray(segments) || segments.length < 2) throw new TypeError('buildRampTimemapKeyed: segments must be an array of >= 2 {durationFrames, speed} pieces (use speed/freeze for a single one)');
|
|
321
|
+
if (!Number.isInteger(sourceFrames) || sourceFrames < 1) throw new TypeError('buildRampTimemapKeyed: sourceFrames must be a positive integer');
|
|
322
|
+
if (!Number.isInteger(srcIn) || srcIn < 0) throw new TypeError('buildRampTimemapKeyed: srcIn must be a non-negative integer');
|
|
323
|
+
const kf = (X, Y) => encodeKeyedDict({ hdr: 1, entries: [
|
|
324
|
+
{ key: 'interp', type: 0x02, subType: 0, value: 0 },
|
|
325
|
+
{ key: 'YOut', type: T_DOUBLE, subType: 0, value: 0 },
|
|
326
|
+
{ key: 'YIn', type: T_DOUBLE, subType: 0, value: 0 },
|
|
327
|
+
{ key: 'Y', type: T_DOUBLE, subType: 0, value: Y },
|
|
328
|
+
{ key: 'XOut', type: T_DOUBLE, subType: 0, value: 0 },
|
|
329
|
+
{ key: 'XIn', type: T_DOUBLE, subType: 0, value: 0 },
|
|
330
|
+
{ key: 'X', type: T_DOUBLE, subType: 0, value: X },
|
|
331
|
+
] }).toString('hex');
|
|
332
|
+
let x = 0;
|
|
333
|
+
let y = srcIn / fps;
|
|
334
|
+
const points = [[x, y]];
|
|
335
|
+
for (const [i, seg] of segments.entries()) {
|
|
336
|
+
if (!Number.isInteger(seg.durationFrames) || seg.durationFrames < 1) throw new TypeError(`buildRampTimemapKeyed: segments[${i}].durationFrames must be a positive integer`);
|
|
337
|
+
if (!(seg.speed > 0)) throw new RangeError(`buildRampTimemapKeyed: segments[${i}].speed must be > 0 (freeze/reverse segments are not authorable in a ramp)`);
|
|
338
|
+
x += seg.durationFrames / fps;
|
|
339
|
+
y += (seg.durationFrames * seg.speed) / fps;
|
|
340
|
+
points.push([x, y]);
|
|
341
|
+
}
|
|
342
|
+
const sourceEnd = y * fps;
|
|
343
|
+
if (sourceEnd > sourceFrames) {
|
|
344
|
+
throw new RangeError(`buildRampTimemapKeyed: ramp consumes source frame ${Math.ceil(sourceEnd)} but the media has ${sourceFrames}`);
|
|
345
|
+
}
|
|
346
|
+
const entries = points.map(([X, Y], i) => ({ key: String(i), type: T_BYTES, subType: 0, value: kf(X, Y) })).reverse();
|
|
347
|
+
const keyframes = encodeKeyedDict({ hdr: 1, entries }).toString('hex');
|
|
348
|
+
const [XMax, YMax] = points[points.length - 1];
|
|
349
|
+
return encodeKeyedDict({ hdr: 1, entries: [
|
|
350
|
+
{ key: 'YMax', type: T_DOUBLE, subType: 0, value: YMax },
|
|
351
|
+
{ key: 'XMax', type: T_DOUBLE, subType: 0, value: XMax },
|
|
352
|
+
{ key: 'UniqueId', type: T_STRING, subType: 0, value: uniqueId },
|
|
353
|
+
{ key: 'LastValidYOffset', type: T_DOUBLE, subType: 0, value: (sourceFrames - 1) / fps },
|
|
354
|
+
{ key: 'KeyframesBA', type: T_BYTES, subType: 0, value: keyframes },
|
|
355
|
+
{ key: 'DbType', type: T_STRING, subType: 0, value: 'Sm2TimeMap' },
|
|
356
|
+
] });
|
|
357
|
+
}
|
|
358
|
+
|
|
307
359
|
module.exports = {
|
|
308
360
|
decodeTimemap, encodeTimemap, encodeRetimedTimemap,
|
|
309
361
|
identityTimemap, buildConstantSpeedTimemap, buildConstantSpeedTimemapKeyed,
|
|
310
|
-
buildFreezeTimemapKeyed,
|
|
362
|
+
buildFreezeTimemapKeyed, buildRampTimemapKeyed,
|
|
311
363
|
buildTimemap, decodeProtobuf,
|
|
312
364
|
TYPE_LINEAR,
|
|
313
365
|
};
|
|
@@ -66,10 +66,28 @@ const clipDuration = (c) => { const m = c.match(/<Duration>(\d+)<\/Duration>/);
|
|
|
66
66
|
// wipe style, so one style is full parity with the host importer.
|
|
67
67
|
const WIPE_FIELDS_BLOB = '00000002000000158012120000002c789c636660642016000000da0005';
|
|
68
68
|
|
|
69
|
+
// PrettyType is the STYLE SELECTOR (measured E67/E68): swapping it on the
|
|
70
|
+
// working dissolve skeleton renders the named style, midpoint-fingerprinted
|
|
71
|
+
// on 19.1.3.7 — dip bottoms at pure black (16), additive saturates bright
|
|
72
|
+
// (233.8), fade-to-color holds a dark plateau (77), smooth-cut blends
|
|
73
|
+
// (179.9), non-additive holds the brighter side (234 past mid). Meanwhile
|
|
74
|
+
// Resolve's OWN XMEML importer writes these same elements in a form that
|
|
75
|
+
// renders INERT — so authoring through this path beats the host importer.
|
|
76
|
+
// Unknown PrettyTypes are refused, not probed: unvetted strings are exactly
|
|
77
|
+
// the class that crashed the app elsewhere (interp!=0, dangling ids).
|
|
78
|
+
const STYLE_PRETTY_TYPES = {
|
|
79
|
+
dip: 'Dip To Color Dissolve',
|
|
80
|
+
additive: 'Additive Dissolve',
|
|
81
|
+
'fade-to-color': 'Fade To Color',
|
|
82
|
+
'smooth-cut': 'Smooth Cut',
|
|
83
|
+
'non-additive': 'Non-Additive Dissolve',
|
|
84
|
+
};
|
|
85
|
+
const TRANSITION_TYPES = ['dissolve', 'wipe', ...Object.keys(STYLE_PRETTY_TYPES)];
|
|
86
|
+
|
|
69
87
|
async function placeTransition(drpInput, opts = {}) {
|
|
70
88
|
const { track, atFrame, durationFrames = 24, trackType = 'video', type = 'dissolve', timelineUuid } = opts;
|
|
71
|
-
if (type
|
|
72
|
-
if (type
|
|
89
|
+
if (!TRANSITION_TYPES.includes(type)) throw new Error(`placeTransition: type must be one of ${TRANSITION_TYPES.join(', ')}`);
|
|
90
|
+
if (type !== 'dissolve' && trackType === 'audio') throw new Error('placeTransition: styled transitions are video-only (audio junctions cross-fade)');
|
|
73
91
|
if (!Number.isInteger(track) || track < 1) throw new TypeError('placeTransition: track must be a positive integer');
|
|
74
92
|
if (!Number.isInteger(atFrame)) throw new TypeError('placeTransition: atFrame must be an integer');
|
|
75
93
|
if (!Number.isInteger(durationFrames) || durationFrames < 2) throw new TypeError('placeTransition: durationFrames must be an integer >= 2');
|
|
@@ -93,6 +111,8 @@ async function placeTransition(drpInput, opts = {}) {
|
|
|
93
111
|
trans = freshDbIds(trans);
|
|
94
112
|
if (type === 'wipe') {
|
|
95
113
|
trans = trans.replace(/<FieldsBlob>[0-9a-fA-F]*<\/FieldsBlob>/, `<FieldsBlob>${WIPE_FIELDS_BLOB}</FieldsBlob>`);
|
|
114
|
+
} else if (STYLE_PRETTY_TYPES[type]) {
|
|
115
|
+
trans = trans.replace(/<PrettyType>Cross Dissolve<\/PrettyType>/, `<PrettyType>${STYLE_PRETTY_TYPES[type]}</PrettyType>`);
|
|
96
116
|
}
|
|
97
117
|
const start = atFrame - Math.floor(durationFrames / 2); // centered (AlignmentType 2)
|
|
98
118
|
trans = trans.replace(/<Start>\d+<\/Start>/, `<Start>${start}</Start>`);
|
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.140.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
package/src/utils/api_truth.py
CHANGED
|
@@ -2668,6 +2668,32 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
2668
2668
|
"submit": "missing",
|
|
2669
2669
|
"mitigation": ["_cdl_node_preflight", "_cdl_failure_diagnosis"],
|
|
2670
2670
|
},
|
|
2671
|
+
{
|
|
2672
|
+
"symbol": "MediaPool.ImportTimelineFromFile (FCP7 XMEML video transitions render inert)",
|
|
2673
|
+
"object": "MediaPool",
|
|
2674
|
+
"signature": "(filePath, {importOptions}) -> Timeline",
|
|
2675
|
+
"reality": "Video <transitionitem>s imported from an FCP7 XMEML land as "
|
|
2676
|
+
"real Sm2TiTransition elements that READ BACK through the "
|
|
2677
|
+
"item APIs but render INERT: the outgoing clip plays through "
|
|
2678
|
+
"the transition window and hard-cuts at its end. Measured on "
|
|
2679
|
+
"Studio 19.1.3.7 with both a plain Cross Dissolve and a Dip "
|
|
2680
|
+
"to Color Dissolve (midpoint frames byte-matched the "
|
|
2681
|
+
"outgoing clip; no blend, no dip). The same transition "
|
|
2682
|
+
"elements authored offline with a correct FieldsBlob render "
|
|
2683
|
+
"perfectly, and EDL-imported dissolves/wipes also render — "
|
|
2684
|
+
"the defect is specific to the XMEML import path's element "
|
|
2685
|
+
"construction.",
|
|
2686
|
+
"recommended": "Do not trust an XMEML-imported timeline's transitions "
|
|
2687
|
+
"without a render probe at a junction midpoint. To "
|
|
2688
|
+
"conform an XMEML turnover with working transitions, "
|
|
2689
|
+
"route it through drt.assemble_from_interchange "
|
|
2690
|
+
"(format 'xml'), which authors render-verified "
|
|
2691
|
+
"dissolves, wipes, and the dissolve-family styles from "
|
|
2692
|
+
"the same <transitionitem> data.",
|
|
2693
|
+
"tags": ["timeline", "import", "xmeml", "transition", "readback", "silent-failure"],
|
|
2694
|
+
"submit": "bug",
|
|
2695
|
+
"mitigation": ["drt.assemble_from_interchange"],
|
|
2696
|
+
},
|
|
2671
2697
|
]
|
|
2672
2698
|
|
|
2673
2699
|
|