davinci-resolve-mcp 2.95.2 → 2.95.3

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 CHANGED
@@ -2,6 +2,51 @@
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.95.3
6
+
7
+ Closes the retime entry's explicit `UNTESTED` warning: **reverse and
8
+ variable-speed ramps both work**, and fixes two decoder bugs found proving it.
9
+
10
+ ### Measured — reverse lands through both import routes
11
+
12
+ On Studio 21.0.4.5. OTIO with a negative `time_scalar` (-1) placed a clip reading
13
+ `GetSourceStartFrame` 95 → `GetSourceEndFrame` 46; EDL with a negative `M2` rate
14
+ (-24.0 at 24fps) read 48 → 0. **The API does expose direction** — a reversed clip
15
+ reports source start GREATER than source end, i.e. a negative span.
16
+
17
+ ### Measured — variable-speed ramps survive Resolve intact
18
+
19
+ They cannot be expressed in OTIO (`LinearTimeWarp` is a single `time_scalar`,
20
+ constant by construction), but they can be authored offline. A `Sm2TimeMap` built
21
+ with `media-timemap.buildTimemap` carrying two segments — 0–2s record at 0.5×,
22
+ 2–4s at 2.0× — was patched into a clip's `MediaTimemapBA`, imported, and
23
+ **re-exported by Resolve with the segments unchanged**. Confirmed independently
24
+ from the API side: the clip read source `0..120` over a 96-frame record, and
25
+ 2s@0.5× (24 source frames) + 2s@2.0× (96) is exactly 120.
26
+
27
+ ### Fixed — the timemap decoder reported a reverse as speed 0
28
+
29
+ Two real bugs in `media-timemap.js`, both exposed by the first reversed map it
30
+ ever saw:
31
+
32
+ - **Fixed-offset keyframe reads.** Each point omits whichever of
33
+ `recordSec`/`sourceSec` is zero (protobuf default-omission), so
34
+ `readDoubleLE(1)`/`readDoubleLE(10)` threw *"offset out of range"* on every
35
+ reversed map. Forward maps only decoded because both values happened to be
36
+ non-zero.
37
+ - **A hardcoded (0,0) origin.** A reversed clip starts at the far end of the
38
+ source, and Resolve encodes that starting offset as a **top-level protobuf
39
+ field 2** double. Assuming (0,0) made a reverse decode as **speed 0** — a
40
+ plausible wrong number, which is worse than a crash. It now decodes as −1.
41
+
42
+ ### Trap worth naming
43
+
44
+ `buildTimemap` returns a **Buffer**. Writing it into the XML without
45
+ `.toString('hex')` embeds mojibake, and the failure is silent: the clip imports
46
+ cleanly and reads `0..0` — indistinguishable from the degenerate-map signature
47
+ the ledger describes for xmeml imports, so it looks like Resolve rejected the
48
+ retime when it is a caller bug. It cost real time here before being caught.
49
+
5
50
  ## What's New in v2.95.2
6
51
 
7
52
  Adds the live round-trip harness the offline `.drp` tier never had — the absence
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.95.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.95.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.95.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.95.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.95.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.95.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -117,7 +117,7 @@ equivalent, blocking full automation.
117
117
  ### Clip speed / retime ratio and speed ramps
118
118
 
119
119
  - **Object:** `TimelineItem`
120
- - **Behavior:** SetProperty exposes only retime *quality* (RetimeProcess, MotionEstimation) and transform/crop/composite/opacity keys — not the speed value itself. There is no way to set a clip to a given % speed, reverse it, or author a speed ramp. Verified against the documented SetProperty key list AND by live mutating attempt on 21.0.0: SetProperty('Speed'|'PlaybackSpeed'|'RetimeSpeed'|'ClipSpeed', 50) all return False, while SetProperty('RetimeProcess', 1) returns True. THE READ SIDE IS AS DEAD AS THE WRITE SIDE, which is easy to miss: re-measured on Studio 19.1.3.7 against a placed item, GetProperty('Speed'), GetProperty('PlaybackSpeed'), GetProperty('RetimeSpeed') and GetProperty('ClipSpeed') ALL return None, and the keyless GetProperty() dict (26 keys on that item) carries no speed value at all — its only retime key is RetimeProcess, which is quality, not ratio. SetProperty('Speed', 1.75) returned False on 19.1.3.7 too, so the write refusal is not specific to 21.0.0. Note the 21.0.0 stamp above covers the SetProperty measurements only. THE SCRIPTING-API xmeml IMPORT BUILDS NO RETIME — and the way it fails is worse than a no-op. First, what Premiere actually writes, because having this backwards is what produced the wrong contract this entry published in 2.79.0–2.79.1 (see CORRECTION below). In an FCP7 XML a retimed clipitem's <in>/<out> live in the POST-RETIME (warped) domain and always span the RECORD duration; pproTicksIn/pproTicksOut carry the TRUE SOURCE position; and <duration> is the file length expressed in the warped domain. A real 200% clip at 24 fps: <in>1957</in> <out>1971</out> — span 14, EQUAL to its record span; pproTicksIn 41425776000000 and pproTicksOut 41722128000000, which at 254016000000/24 = 10584000000 ticks per frame are source frames 3914 and 3942, exactly 1957x2 and 1971x2, a 28-frame source span over a 14-frame record span; <duration>24292</duration> for a 48584-frame file; and a graphdict mapping warped to true source with the ratio as its slope (when 17910 -> value 35820). So for a retimed clip <in> and pproTicksIn are SUPPOSED to disagree, by exactly the ratio. The same relationship seen from the other side is already encoded in this repo: resolve-advanced/server/prproj.mjs derives Premiere speed from tick geometry as |srcSpan / recSpan| * 100, reversing when in > out. Against that convention, measured on 19.1.3/19.1.3.7: (a) the importer IGNORES the scalar Time Remap speed filter and the clips arrive at 100%; (b) `graphdict` is ignored too — re-tested in Premiere's exact convention with one 100% control clip and one 200% clip per timeline, a document carrying warped <in>/<out>, true-source pproTicks, <duration> = fileLen/ratio and a constant-slope graphdict imports cleanly, the control lands correct, and NO retime is built: recalibrated 2026-08-05, every xmeml-imported clip carries a DEGENERATE time map in Project.db (Sm2TimeMap with an empty source axis — five Time Remap shapes re-measured, 15/15 clips degenerate), so no speed exists in the project data, and the API source witness reads 0/0 on those clips (see WITNESS CALIBRATION below); emitting the identical document WITHOUT the graphdict gives the identical result; (c) `reverse` does not survive either; (d) THE HAZARD, and it is the part that bites: Resolve reads <in> LITERALLY as the true source frame, honouring neither the ticks nor the graphdict. Import a genuine Premiere XML that contains retimes and every retimed clip is placed at in / ratio — the 200% clip above lands on source frame 1957 instead of 3914. There is no error, the cut lengths are still correct, every clip is linked and online, and the timeline renders — so it reads as a good conform while sitting at the wrong moment of the right file. This is the same failure class as the Avid AAF camera-file link (docs/guides/conforming-an-avid-aaf.md): wrong in a way only a frame comparison against a reference can see. SCOPE: all of the above is the SCRIPTING-API import (ImportTimelineFromFile). Resolve's UI importer (File > Import > Timeline) has NOT been tested, and that is how editors usually conform a Premiere XML — do not read this as covering it. CORRECTION: this entry as published in 2.79.0–2.79.1 also claimed that any <in>/<pproTicksIn> inconsistency is silently REJECTED in both orientations. That claim was FALSE and has been removed — it came from an emitter writing ticks = in x ticks-per-frame at every speed, so what it observed was its own malformed files being refused. The graphdict evidence published with it (dead in FOUR shapes, 0 of 2 landed, a 200% clip emitted in 200 / out 296 'clamped' to out 248) described that same malformed input being normalized and is replaced by the re-test above. The conclusion is unchanged; only its evidence is. Placement is NOT the problem: the same route imported 573 clips with 572 of 573 matching by track and record position with source frames exact, and the importer BUILT a 59-frame dissolve. The retime gap is specific, not general. TRAP: Resolve's own FCP7 export cannot witness a speed. It writes a DEGENERATE Time Remap on every clip — `speed` value 0 (not 100) and a graphdict whose keyframe `value`s are all 0 while its `when`s carry the clip's source in/out — so anyone verifying a retime by round-tripping through EXPORT_FCP_7_XML is reading furniture, and the identity Time Remap blocks present on every clip are what make the route look like it should work. WITNESS CALIBRATION (2026-08-05, Studio 19.1.3.7) — the positive control this entry previously lacked now exists, and it RETRACTS the witness the 2.80.0 revision of this entry recommended. The rig removed every confound: the SAME clip placed twice, adjacent, in ONE timeline, the second copy hand-set to 200% in the UI (the only way to make one — see above). GetSourceStartFrame/GetSourceEndFrame separated the copies — 1822..1870 (span 48) at 100% vs 1822..1918 (span 96) at 200% — while GetLeftOffset/GetRightOffset did NOT: 1822..1870 at 100% vs 911..959 at 200%, which is exactly position / 2. GetLeftOffset reports the WARPED (record-side) domain — position / speed, the `In` column of Project.db's Sm2TiItem — so it is exact for PLACEMENT and blind for SPEED BY CONSTRUCTION: its span equals the record span at every speed. The speed itself lives in the item's Sm2TimeMap blob (keyframe slope = ratio; the hand-set 200% reads slope exactly 2.0), which is what GetSourceStart/EndFrame and EXPORT_EDL read. THE SPEED WITNESS is therefore the GetSourceStart/EndFrame span vs the record duration. CAVEAT: on xmeml-IMPORTED timelines those return 0/0 — the importer leaves the time map's source axis empty — and a 0/0 read is UNKNOWN, never 'no retime'. Cross-checks that work everywhere: the Sm2TimeMap slope read from a saved Project.db, and the EXPORT_EDL M2 rate (rate = fps x speed/100, so 048.0 = 200% at 24 fps; `M2 ... 000.0` on every clip is the degenerate-map furniture of an xmeml import — ignore it). TWO IMPORT ROUTES DO BUILD CONSTANT RETIMES (measured 2026-08-05, media linked, judged via the calibrated witnesses above): (1) OTIO LinearTimeWarp through ImportTimelineFromFile — 200% (src 200..296 over a 48-frame record) and 50% (src 300..324 over a 48-frame record) both landed with correct source in-points; the saved Project.db shows slope 2.0 and 0.5. Emission rules: the document must be Resolve-shaped with TIMECODE-ABSOLUTE source frames (see the ImportTimelineFromFile .otio entry), the effect is `LinearTimeWarp.1` with `time_scalar`, and `source_range.duration` is the RECORD span — OTIO semantics, the time_scalar handles source consumption; sending the source span as the duration builds a spec-correctly longer clip, not a retime. (2) EDL M2 — 200% landed (src 100..196 over a 48-frame record), linked. Author the shape Resolve's own EXPORT_EDL writes: the event line's source span EQUALS the record span even under M2; the `M2 <reel> <rate> <srcInTC>` line carries the play rate in fps (048.0 = 200% at 24); `* FROM CLIP NAME:` comments drive pool linking. UNTESTED as import routes: reverse (negative time_scalar / reverse M2) and varying-speed mapsdo not extrapolate the constant-speed result to them.
120
+ - **Behavior:** SetProperty exposes only retime *quality* (RetimeProcess, MotionEstimation) and transform/crop/composite/opacity keys — not the speed value itself. There is no way to set a clip to a given % speed, reverse it, or author a speed ramp. Verified against the documented SetProperty key list AND by live mutating attempt on 21.0.0: SetProperty('Speed'|'PlaybackSpeed'|'RetimeSpeed'|'ClipSpeed', 50) all return False, while SetProperty('RetimeProcess', 1) returns True. THE READ SIDE IS AS DEAD AS THE WRITE SIDE, which is easy to miss: re-measured on Studio 19.1.3.7 against a placed item, GetProperty('Speed'), GetProperty('PlaybackSpeed'), GetProperty('RetimeSpeed') and GetProperty('ClipSpeed') ALL return None, and the keyless GetProperty() dict (26 keys on that item) carries no speed value at all — its only retime key is RetimeProcess, which is quality, not ratio. SetProperty('Speed', 1.75) returned False on 19.1.3.7 too, so the write refusal is not specific to 21.0.0. Note the 21.0.0 stamp above covers the SetProperty measurements only. THE SCRIPTING-API xmeml IMPORT BUILDS NO RETIME — and the way it fails is worse than a no-op. First, what Premiere actually writes, because having this backwards is what produced the wrong contract this entry published in 2.79.0–2.79.1 (see CORRECTION below). In an FCP7 XML a retimed clipitem's <in>/<out> live in the POST-RETIME (warped) domain and always span the RECORD duration; pproTicksIn/pproTicksOut carry the TRUE SOURCE position; and <duration> is the file length expressed in the warped domain. A real 200% clip at 24 fps: <in>1957</in> <out>1971</out> — span 14, EQUAL to its record span; pproTicksIn 41425776000000 and pproTicksOut 41722128000000, which at 254016000000/24 = 10584000000 ticks per frame are source frames 3914 and 3942, exactly 1957x2 and 1971x2, a 28-frame source span over a 14-frame record span; <duration>24292</duration> for a 48584-frame file; and a graphdict mapping warped to true source with the ratio as its slope (when 17910 -> value 35820). So for a retimed clip <in> and pproTicksIn are SUPPOSED to disagree, by exactly the ratio. The same relationship seen from the other side is already encoded in this repo: resolve-advanced/server/prproj.mjs derives Premiere speed from tick geometry as |srcSpan / recSpan| * 100, reversing when in > out. Against that convention, measured on 19.1.3/19.1.3.7: (a) the importer IGNORES the scalar Time Remap speed filter and the clips arrive at 100%; (b) `graphdict` is ignored too — re-tested in Premiere's exact convention with one 100% control clip and one 200% clip per timeline, a document carrying warped <in>/<out>, true-source pproTicks, <duration> = fileLen/ratio and a constant-slope graphdict imports cleanly, the control lands correct, and NO retime is built: recalibrated 2026-08-05, every xmeml-imported clip carries a DEGENERATE time map in Project.db (Sm2TimeMap with an empty source axis — five Time Remap shapes re-measured, 15/15 clips degenerate), so no speed exists in the project data, and the API source witness reads 0/0 on those clips (see WITNESS CALIBRATION below); emitting the identical document WITHOUT the graphdict gives the identical result; (c) `reverse` does not survive either; (d) THE HAZARD, and it is the part that bites: Resolve reads <in> LITERALLY as the true source frame, honouring neither the ticks nor the graphdict. Import a genuine Premiere XML that contains retimes and every retimed clip is placed at in / ratio — the 200% clip above lands on source frame 1957 instead of 3914. There is no error, the cut lengths are still correct, every clip is linked and online, and the timeline renders — so it reads as a good conform while sitting at the wrong moment of the right file. This is the same failure class as the Avid AAF camera-file link (docs/guides/conforming-an-avid-aaf.md): wrong in a way only a frame comparison against a reference can see. SCOPE: all of the above is the SCRIPTING-API import (ImportTimelineFromFile). Resolve's UI importer (File > Import > Timeline) has NOT been tested, and that is how editors usually conform a Premiere XML — do not read this as covering it. CORRECTION: this entry as published in 2.79.0–2.79.1 also claimed that any <in>/<pproTicksIn> inconsistency is silently REJECTED in both orientations. That claim was FALSE and has been removed — it came from an emitter writing ticks = in x ticks-per-frame at every speed, so what it observed was its own malformed files being refused. The graphdict evidence published with it (dead in FOUR shapes, 0 of 2 landed, a 200% clip emitted in 200 / out 296 'clamped' to out 248) described that same malformed input being normalized and is replaced by the re-test above. The conclusion is unchanged; only its evidence is. Placement is NOT the problem: the same route imported 573 clips with 572 of 573 matching by track and record position with source frames exact, and the importer BUILT a 59-frame dissolve. The retime gap is specific, not general. TRAP: Resolve's own FCP7 export cannot witness a speed. It writes a DEGENERATE Time Remap on every clip — `speed` value 0 (not 100) and a graphdict whose keyframe `value`s are all 0 while its `when`s carry the clip's source in/out — so anyone verifying a retime by round-tripping through EXPORT_FCP_7_XML is reading furniture, and the identity Time Remap blocks present on every clip are what make the route look like it should work. WITNESS CALIBRATION (2026-08-05, Studio 19.1.3.7) — the positive control this entry previously lacked now exists, and it RETRACTS the witness the 2.80.0 revision of this entry recommended. The rig removed every confound: the SAME clip placed twice, adjacent, in ONE timeline, the second copy hand-set to 200% in the UI (the only way to make one — see above). GetSourceStartFrame/GetSourceEndFrame separated the copies — 1822..1870 (span 48) at 100% vs 1822..1918 (span 96) at 200% — while GetLeftOffset/GetRightOffset did NOT: 1822..1870 at 100% vs 911..959 at 200%, which is exactly position / 2. GetLeftOffset reports the WARPED (record-side) domain — position / speed, the `In` column of Project.db's Sm2TiItem — so it is exact for PLACEMENT and blind for SPEED BY CONSTRUCTION: its span equals the record span at every speed. The speed itself lives in the item's Sm2TimeMap blob (keyframe slope = ratio; the hand-set 200% reads slope exactly 2.0), which is what GetSourceStart/EndFrame and EXPORT_EDL read. THE SPEED WITNESS is therefore the GetSourceStart/EndFrame span vs the record duration. CAVEAT: on xmeml-IMPORTED timelines those return 0/0 — the importer leaves the time map's source axis empty — and a 0/0 read is UNKNOWN, never 'no retime'. Cross-checks that work everywhere: the Sm2TimeMap slope read from a saved Project.db, and the EXPORT_EDL M2 rate (rate = fps x speed/100, so 048.0 = 200% at 24 fps; `M2 ... 000.0` on every clip is the degenerate-map furniture of an xmeml import — ignore it). TWO IMPORT ROUTES DO BUILD CONSTANT RETIMES (measured 2026-08-05, media linked, judged via the calibrated witnesses above): (1) OTIO LinearTimeWarp through ImportTimelineFromFile — 200% (src 200..296 over a 48-frame record) and 50% (src 300..324 over a 48-frame record) both landed with correct source in-points; the saved Project.db shows slope 2.0 and 0.5. Emission rules: the document must be Resolve-shaped with TIMECODE-ABSOLUTE source frames (see the ImportTimelineFromFile .otio entry), the effect is `LinearTimeWarp.1` with `time_scalar`, and `source_range.duration` is the RECORD span — OTIO semantics, the time_scalar handles source consumption; sending the source span as the duration builds a spec-correctly longer clip, not a retime. (2) EDL M2 — 200% landed (src 100..196 over a 48-frame record), linked. Author the shape Resolve's own EXPORT_EDL writes: the event line's source span EQUALS the record span even under M2; the `M2 <reel> <rate> <srcInTC>` line carries the play rate in fps (048.0 = 200% at 24); `* FROM CLIP NAME:` comments drive pool linking. REVERSE AND VARYING-SPEED RAMPS ARE NOW MEASURED TOO (2026-08-12, Studio 21.0.4.5) — this entry previously warned they were UNTESTED; that warning is closed and both work. REVERSE lands through BOTH import routes: OTIO with a negative time_scalar (-1) placed a clip reading GetSourceStartFrame 95 -> GetSourceEndFrame 46, and EDL with a negative M2 rate (-24.0 at 24fps) read 48 -> 0. So THE API DOES EXPOSE DIRECTION, which is worth knowing: a reversed clip reports GetSourceStartFrame GREATER THAN GetSourceEndFrame (a negative span). VARYING-SPEED RAMPS cannot be expressed in OTIO (LinearTimeWarp is a single time_scalar — constant by construction) but CAN be authored offline and survive Resolve intact: a Sm2TimeMap built with media-timemap.buildTimemap carrying two segments (0-2s record at 0.5x, 2-4s at 2.0x) was patched into a clip's MediaTimemapBA, imported, and re-exported by Resolve with the segments UNCHANGED. Independent confirmation from the API side: the placed clip read GetSourceStart/EndFrame 0..120 over a 96-frame record, and 2s at 0.5x (24 source frames) + 2s at 2.0x (96) is exactly 120. TWO TRAPS worth naming. (1) buildTimemap returns a BUFFER; writing it into the XML without .toString('hex') embeds mojibake, and the failure is SILENT the clip imports fine and reads 0..0, which is indistinguishable from the degenerate-map signature this entry describes for xmeml imports, so it reads as 'Resolve rejected the retime' when it is really a caller bug. (2) a REVERSED map does not start at (0,0): Resolve encodes the starting source offset as a TOP-LEVEL protobuf field 2 double, and each keyframe point omits whichever of record/source is zero (protobuf default-omission). Anything reading fixed offsets or assuming a (0,0) origin decodes a reverse as SPEED 0 rather than -1. drp-format's own decoder had both bugs and is fixed.
121
121
  - **Workaround / current handling:** Set clip speed/retime in the Resolve UI, or BUILD it by import: OTIO LinearTimeWarp and EDL M2 both construct constant retimes through ImportTimelineFromFile (measured — emission rules in reality above); xmeml does not, in any Time Remap shape. To READ a retime back, judge speed by the GetSourceStart/EndFrame span vs the record duration — a 0/0 read (xmeml-imported timelines) is UNKNOWN, never 'no retime' — and cross-check with the Sm2TimeMap slope in a saved Project.db or the EXPORT_EDL M2 rate. Do NOT read speed with GetProperty (None), witness it via EXPORT_FCP_7_XML (degenerate), or judge it from GetLeftOffset/GetRightOffset — the 2.80.0 revision of this entry recommended that pair as the witness and it is blind by construction: it reads the warped domain (position / speed) and its span equals the record span at every speed. Keep it for PLACEMENT checks only. Reverse and varying-speed maps remain untested as import routes. And if you are importing a real Premiere XML that contains retimes, treat every retimed clip's source position as WRONG — placed at <in>, i.e. in / ratio — until it is checked against a reference; the lengths and the links will look right.
122
122
  - **Tags:** missing-method, timeline, retime, speed, interchange, silent-failure, unreliable-return
123
123
 
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.95.2"
39
+ VERSION = "2.95.3"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.95.2",
3
+ "version": "2.95.3",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -117,3 +117,59 @@ test('identityTimemap builds a [02][end,0,end,0,end] map', () => {
117
117
  assert.strictEqual(b.seconds.length, 5);
118
118
  assert.strictEqual(Math.round(b.seconds[0] * (30000 / 1001)), 4575);
119
119
  });
120
+
121
+ // --- Reverse maps: the origin is NOT always (0,0) ---------------------------
122
+ // Captured from a clip reversed via EDL M2 / OTIO negative time_scalar and
123
+ // exported by DaVinci Resolve Studio 21.0.4.5. Two things here used to break:
124
+ // 1. Each keyframe point omits whichever of recordSec/sourceSec is 0 —
125
+ // protobuf default-omission — so the old fixed-offset reader
126
+ // (readDoubleLE(1) / readDoubleLE(10)) threw "offset out of range".
127
+ // 2. The starting source offset is a TOP-LEVEL field 2 double. Assuming a
128
+ // (0,0) origin made a reverse decode as speed 0 — a plausible wrong
129
+ // number rather than an error, which is the worse failure.
130
+ const REVERSED_KEYFRAMES_BA = '800a09115655555555d517400a09095655555555d51740';
131
+
132
+ function reversedMapHex() {
133
+ const { encodeKeyedDict } = require('../keyed-dict');
134
+ const T_DOUBLE = 6; const T_STRING = 10; const T_BYTES = 12;
135
+ return encodeKeyedDict({
136
+ hdr: 1,
137
+ entries: [
138
+ { key: 'YMin', type: T_DOUBLE, subType: 0, value: -1 },
139
+ { key: 'YMax', type: T_DOUBLE, subType: 0, value: -1 },
140
+ { key: 'XMax', type: T_DOUBLE, subType: 0, value: 5.958333333333334 },
141
+ { key: 'UniqueId', type: T_STRING, subType: 0, value: '4dbe3e42-ab1e-4b93-8118-67fdc376c962' },
142
+ { key: 'LastValidYOffset', type: T_DOUBLE, subType: 0, value: 5.958333333333333 },
143
+ { key: 'KeyframesBA', type: T_BYTES, subType: 0, value: REVERSED_KEYFRAMES_BA },
144
+ { key: 'DbType', type: T_STRING, subType: 0, value: 'Sm2TimeMap' },
145
+ ],
146
+ }).toString('hex');
147
+ }
148
+
149
+ test('decodeTimemap: a reversed map decodes without throwing', () => {
150
+ assert.doesNotThrow(() => decodeTimemap(reversedMapHex()));
151
+ });
152
+
153
+ test('decodeTimemap: a reversed map reports NEGATIVE speed, not 0', () => {
154
+ const d = decodeTimemap(reversedMapHex());
155
+ assert.equal(d.segments.length, 1);
156
+ assert.equal(d.segments[0].speed, -1);
157
+ assert.ok(d.segments[0].speed < 0, 'reverse must not decode as speed 0');
158
+ });
159
+
160
+ test('_decodeKeyframePoint: a point may omit either value (protobuf default 0)', () => {
161
+ // sourceSec-only and recordSec-only points both appear in one real reversed map.
162
+ const d = decodeTimemap(reversedMapHex());
163
+ assert.equal(d.recordDurationSec, 5.958333333333334);
164
+ assert.equal(d.sourceDurationSec, 5.958333333333333);
165
+ });
166
+
167
+ test('forward maps are unaffected by the origin fix', () => {
168
+ const fwd = buildTimemap({
169
+ keyframes: [{ recordSec: 2, sourceSec: 1 }, { recordSec: 4, sourceSec: 5 }],
170
+ sourceDurationSec: 6, recordDurationSec: 4, uniqueId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
171
+ });
172
+ const d = decodeTimemap(fwd.toString('hex'));
173
+ assert.equal(d.variable, true);
174
+ assert.deepEqual(d.segments.map((s) => s.speed), [0.5, 2]);
175
+ });
@@ -41,17 +41,69 @@ function _isKeyedForm(b) {
41
41
  * (fixed64 LE doubles). The map starts at the implicit (0,0); constant speed has one keyframe,
42
42
  * a variable-speed ramp has one keyframe per added speed point.
43
43
  */
44
+ /**
45
+ * One keyframe point: an inner message of field 1 (recordSec) and field 2
46
+ * (sourceSec), both wire type 1 (64-bit double) — tags 0x09 and 0x11.
47
+ *
48
+ * Both are OPTIONAL. Protobuf omits a field whose value is the default (0), so
49
+ * a point can legitimately carry only one of the two, and fixed offsets do not
50
+ * work. This is not theoretical: a REVERSED clip exported by Resolve 21.0.4.5
51
+ * encodes its points as `0a 09 11 <double>` (sourceSec only) and
52
+ * `0a 09 09 <double>` (recordSec only), and the previous fixed-offset reader
53
+ * — `readDoubleLE(1)` / `readDoubleLE(10)` — threw
54
+ * "offset out of range … Received 10" on every reversed map. Forward maps only
55
+ * decoded because both values happened to be non-zero.
56
+ */
57
+ function _decodeKeyframePoint(buf) {
58
+ let recordSec = 0;
59
+ let sourceSec = 0;
60
+ let i = 0;
61
+ while (i < buf.length) {
62
+ const tag = buf[i];
63
+ if (tag === 0x09 && i + 9 <= buf.length) { recordSec = buf.readDoubleLE(i + 1); i += 9; }
64
+ else if (tag === 0x11 && i + 9 <= buf.length) { sourceSec = buf.readDoubleLE(i + 1); i += 9; }
65
+ else break; // unknown tag or truncated — stop rather than misread
66
+ }
67
+ return { recordSec, sourceSec };
68
+ }
69
+
70
+ /**
71
+ * Keyframe points plus the map's ORIGIN.
72
+ *
73
+ * The origin is not always (0,0). A reversed clip starts at the far end of the
74
+ * source and walks backwards, and Resolve encodes that starting source offset as
75
+ * a TOP-LEVEL field 2 double alongside the keyframe messages. Measured on a
76
+ * reversed clip exported by Studio 21.0.4.5:
77
+ *
78
+ * 80 0a 09 field 160 = 9
79
+ * 11 5655555555d51740 field 2 = 5.9583 <- origin sourceSec
80
+ * 0a 09 09 5655555555d51740 field 1 = { recordSec: 5.9583 }
81
+ *
82
+ * Reading that as an implicit (0,0) origin yields a segment from (0,0) to
83
+ * (5.9583, 0) — slope 0 — so a reverse decodes as "speed 0", a plausible-looking
84
+ * wrong answer rather than an error. With the origin it is (0, 5.9583) to
85
+ * (5.9583, 0): slope -1, a reverse.
86
+ */
44
87
  function _decodeKeyframes(hex) {
45
- if (hex == null) return [];
88
+ if (hex == null) return { origin: { recordSec: 0, sourceSec: 0 }, keyframes: [] };
46
89
  const fields = decodeProtobuf(hex);
47
- return fields
48
- .filter((f) => f.field === 1 && f.wire === 2)
49
- .map((f) => ({ recordSec: f.value.readDoubleLE(1), sourceSec: f.value.readDoubleLE(10) }));
90
+ const originField = fields.find((f) => f.field === 2 && f.wire === 1);
91
+ const originSource = originField
92
+ ? (Buffer.isBuffer(originField.value)
93
+ ? originField.value.readDoubleLE(0)
94
+ : Number(originField.value))
95
+ : 0;
96
+ return {
97
+ origin: { recordSec: 0, sourceSec: Number.isFinite(originSource) ? originSource : 0 },
98
+ keyframes: fields
99
+ .filter((f) => f.field === 1 && f.wire === 2)
100
+ .map((f) => _decodeKeyframePoint(f.value)),
101
+ };
50
102
  }
51
103
 
52
- /** Per-segment speeds from the keyframe points (slope Δsource/Δrecord); starts at (0,0). */
53
- function _segments(keyframes) {
54
- const pts = [{ recordSec: 0, sourceSec: 0 }, ...keyframes];
104
+ /** Per-segment speeds from the keyframe points (slope Δsource/Δrecord). */
105
+ function _segments(keyframes, origin = { recordSec: 0, sourceSec: 0 }) {
106
+ const pts = [origin, ...keyframes];
55
107
  const segs = [];
56
108
  for (let i = 1; i < pts.length; i++) {
57
109
  const dr = pts[i].recordSec - pts[i - 1].recordSec;
@@ -68,8 +120,8 @@ function decodeTimemap(input) {
68
120
  const get = (k) => { const e = entries.find((x) => x.key === k); return e ? e.value : undefined; };
69
121
  const recordDurationSec = get('XMax');
70
122
  const sourceDurationSec = get('LastValidYOffset');
71
- const keyframes = _decodeKeyframes(get('KeyframesBA'));
72
- const segments = _segments(keyframes);
123
+ const { origin, keyframes } = _decodeKeyframes(get('KeyframesBA'));
124
+ const segments = _segments(keyframes, origin);
73
125
  // The EXACT speed lives in the keyframe ratios (source/record per segment); XMax and
74
126
  // LastValidYOffset are frame-quantized. `speed` is the first segment's (whole clip if 1 kf);
75
127
  // `segments` carries the full variable-speed ramp.
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.95.2"
90
+ VERSION = "2.95.3"
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.95.2"
14
+ VERSION = "2.95.3"
15
15
 
16
16
  import base64
17
17
  import os
@@ -1057,9 +1057,39 @@ API_TRUTH: List[Dict[str, Any]] = [
1057
1057
  "source span EQUALS the record span even under M2; the "
1058
1058
  "`M2 <reel> <rate> <srcInTC>` line carries the play rate in "
1059
1059
  "fps (048.0 = 200% at 24); `* FROM CLIP NAME:` comments drive "
1060
- "pool linking. UNTESTED as import routes: reverse (negative "
1061
- "time_scalar / reverse M2) and varying-speed maps do not "
1062
- "extrapolate the constant-speed result to them.",
1060
+ "pool linking. REVERSE AND VARYING-SPEED RAMPS ARE NOW "
1061
+ "MEASURED TOO (2026-08-12, Studio 21.0.4.5) — this entry "
1062
+ "previously warned they were UNTESTED; that warning is "
1063
+ "closed and both work. REVERSE lands through BOTH import "
1064
+ "routes: OTIO with a negative time_scalar (-1) placed a clip "
1065
+ "reading GetSourceStartFrame 95 -> GetSourceEndFrame 46, and "
1066
+ "EDL with a negative M2 rate (-24.0 at 24fps) read 48 -> 0. "
1067
+ "So THE API DOES EXPOSE DIRECTION, which is worth knowing: a "
1068
+ "reversed clip reports GetSourceStartFrame GREATER THAN "
1069
+ "GetSourceEndFrame (a negative span). VARYING-SPEED RAMPS "
1070
+ "cannot be expressed in OTIO (LinearTimeWarp is a single "
1071
+ "time_scalar — constant by construction) but CAN be authored "
1072
+ "offline and survive Resolve intact: a Sm2TimeMap built with "
1073
+ "media-timemap.buildTimemap carrying two segments (0-2s "
1074
+ "record at 0.5x, 2-4s at 2.0x) was patched into a clip's "
1075
+ "MediaTimemapBA, imported, and re-exported by Resolve with "
1076
+ "the segments UNCHANGED. Independent confirmation from the "
1077
+ "API side: the placed clip read GetSourceStart/EndFrame 0..120 "
1078
+ "over a 96-frame record, and 2s at 0.5x (24 source frames) + "
1079
+ "2s at 2.0x (96) is exactly 120. TWO TRAPS worth naming. "
1080
+ "(1) buildTimemap returns a BUFFER; writing it into the XML "
1081
+ "without .toString('hex') embeds mojibake, and the failure is "
1082
+ "SILENT — the clip imports fine and reads 0..0, which is "
1083
+ "indistinguishable from the degenerate-map signature this "
1084
+ "entry describes for xmeml imports, so it reads as 'Resolve "
1085
+ "rejected the retime' when it is really a caller bug. "
1086
+ "(2) a REVERSED map does not start at (0,0): Resolve encodes "
1087
+ "the starting source offset as a TOP-LEVEL protobuf field 2 "
1088
+ "double, and each keyframe point omits whichever of "
1089
+ "record/source is zero (protobuf default-omission). Anything "
1090
+ "reading fixed offsets or assuming a (0,0) origin decodes a "
1091
+ "reverse as SPEED 0 rather than -1. drp-format's own decoder "
1092
+ "had both bugs and is fixed.",
1063
1093
  "recommended": "Set clip speed/retime in the Resolve UI, or BUILD it by "
1064
1094
  "import: OTIO LinearTimeWarp and EDL M2 both construct "
1065
1095
  "constant retimes through ImportTimelineFromFile (measured "