davinci-resolve-mcp 2.136.0 → 2.136.1
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 +487 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/agent-rules/sync_portable_assets.py +7 -7
- package/scripts/audit_api_parity.py +4 -4
- package/scripts/bisect_headless_hang.py +1 -1
- package/scripts/doctor.py +2 -2
- package/scripts/gen_api_limitations.py +2 -2
- package/scripts/mode_matrix.py +3 -3
- package/scripts/mode_matrix_worker.py +1 -1
- package/scripts/pixel_equality.py +2 -2
- package/scripts/render_stress.py +2 -2
- package/scripts/resolve_bridge_probe.py +2 -2
- package/scripts/resolve_capability_probe.py +1 -1
- package/scripts/roundtrip_matrix.py +2 -2
- package/scripts/roundtrip_rich.py +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,493 @@
|
|
|
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.136.1 — the changelog catches up
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **README badges and this changelog had silently frozen at v2.108.0** while
|
|
10
|
+
28 releases shipped with notes only on GitHub Releases. All entries
|
|
11
|
+
v2.109.0-v2.136.0 are now ported here verbatim, both README badges (and the
|
|
12
|
+
zh-CN correspondence line) track the released version again, and a new
|
|
13
|
+
drift guard (`tests/test_release_surface_drift.py`) fails the suite if any
|
|
14
|
+
of these surfaces lag a version bump — the release-process mandate is now
|
|
15
|
+
enforceable instead of aspirational.
|
|
16
|
+
- **Windows `UnicodeDecodeError` in tests/scripts** — adapted from
|
|
17
|
+
[PR #174](https://github.com/samuelgursky/davinci-resolve-mcp/pull/174) by
|
|
18
|
+
@Chosen-3: 151 `open()`/`read_text()`/`write_text()` call sites across 47
|
|
19
|
+
files gained `encoding="utf-8"` (locale-encoding fallback crashes on
|
|
20
|
+
cp1252 the moment a read target holds non-ASCII bytes; binary-mode and
|
|
21
|
+
`PIL.Image.open` sites correctly exempt). A companion AST guard
|
|
22
|
+
(`tests/test_utf8_encoding_discipline.py`) keeps new unencoded text-mode
|
|
23
|
+
calls out of `tests/` and `scripts/` — its first catch was this repo's own
|
|
24
|
+
day-old sync script.
|
|
25
|
+
|
|
26
|
+
## What's New in v2.136.0 — portable agent assets
|
|
27
|
+
|
|
28
|
+
Adapted from [PR #173](https://github.com/samuelgursky/davinci-resolve-mcp/pull/173) by @jin386 — the portable `.agents/` layout lands, with the review findings folded in rather than waiting on a revision round.
|
|
29
|
+
|
|
30
|
+
### What's new
|
|
31
|
+
|
|
32
|
+
- **`.agents/` is the host-neutral canonical layer**: `.agents/skills` (the skill corpus, now what the `knowledge` MCP tool serves and what Codex reads directly), `.agents/roles` (reviewer bodies), `.agents/hooks` (canonical guard logic with a shared `hook_runtime`).
|
|
33
|
+
- **Codex support as contributed**: `.codex/hooks.json` wiring, hook shims, and native agent TOMLs, plus the cross-host portability test suite.
|
|
34
|
+
- **Claude Code loses nothing**: `.claude/skills` adapters are content-complete, byte-identical copies — never pointer stubs — so the rich "Apply when…" trigger descriptions, named craft-skill references, and `user-invocable` flags survive verbatim (Claude routes on the frontmatter at selection time). `.claude/agents` keep their frontmatter, including the deliberate `model: opus` pins. `CLAUDE.md` stays intentionally short.
|
|
35
|
+
- **Safety kept narrow**: `source_media_guard`'s scratch-exemption prefixes remain `claude-`/`codex-` only — the proposed `agent-` prefix would have exempted any real `agent-*` directory from the source-media deny.
|
|
36
|
+
- **Drift cannot land**: `scripts/agent-rules/sync_portable_assets.py` (+`--check`) restores the invariant from either edit point, and `tests/test_portable_asset_parity.py` fails the suite on divergence, missing adapters (the "new skill silently never loads" failure mode), lost frontmatter pins, or widened scratch prefixes.
|
|
37
|
+
|
|
38
|
+
Suites: Node 853, Python 3123 + 845 subtests (portability + parity families added).
|
|
39
|
+
|
|
40
|
+
## What's New in v2.135.0 — audio.trim never trimmed
|
|
41
|
+
|
|
42
|
+
The E59 protocol-layer sweep — the v2.133.0 smoke-test harness pointed at the *other* 16 advanced tools — found one real silent lie and confirmed the rest of the surface healthy.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
**`audio.trim` never trimmed.** Two stacked failures, both invisible to success-shaped output: the non-strict schema silently stripped mistyped window keys (so `{start, duration}` copied the whole file and reported success), and the tool's own advertised `durationFrames` was never in the vendored module's vocabulary (`{startTime, endTime, duration}` in seconds) — even a correct call returned the full file as a "trim". Schemas across the audio tool are now `.strict()` (unknown keys refuse; extra ffmpeg knobs belong in `opts`), `durationFrames` is required (a windowless trim is a no-op copy wearing a trim's name — use `convert`), and a new optional `fps` (default 24) converts it to seconds. Live-verified through the MCP layer: `durationFrames: 24` → exactly 1.000 s of output.
|
|
47
|
+
|
|
48
|
+
### Swept clean
|
|
49
|
+
|
|
50
|
+
All 18 dispatchers refuse unknown actions with structured errors; offline happy paths verified for drp, drx, fusion, audio_plan, pipeline, editorial, conform, media, deliverable, and capabilities.
|
|
51
|
+
|
|
52
|
+
Suites: Node 853, Python 3111 + 799 subtests.
|
|
53
|
+
|
|
54
|
+
## What's New in v2.134.1 — the nesting envelope extends
|
|
55
|
+
|
|
56
|
+
A follow-up measurement to v2.134.0: depth-3 nesting also renders (E58 — a compound inside a compound inside a compound, triple-nested white measured at 234 through the full spec → import → render route on 19.1.3.7). The SequenceSetup fix generalizes; nesting depth is no longer the boundary. Tool doc, guide, and code comments updated from "deeper unverified" to the measured envelope. Suites: Node 852, Python 3111+799.
|
|
57
|
+
|
|
58
|
+
## What's New in v2.134.0 — freeze frames and nested compounds
|
|
59
|
+
|
|
60
|
+
Two measured boundaries — both previously closed as "not authorable" — reopened with new harvest angles and closed for real, each render-proven on Studio 19.1.3.7.
|
|
61
|
+
|
|
62
|
+
### Freeze frames: authored offline (`cuts[].freeze`)
|
|
63
|
+
|
|
64
|
+
The old finding said no harvest path existed. One did: Resolve's EDL importer honors `M2 <reel> 000.0` motion memos, giving the first real frozen clip whose bytes could be read (E55: reads back source N..N **and** renders frozen — freezedetect-proven, the direction the earlier synthetic always failed in). The real `Sm2TimeMap` is flat in **seconds**, not frames: `YMin = YMax = Y = frozenFrame/fps`, `XMax = 60000` (a sentinel domain), and the clip's `<In>` stays empty. `buildFreezeTimemapKeyed` reproduces the harvest byte-exactly; `cuts[].freeze: true` (or `speed: 0`) authors it, and `assemble_from_interchange` now **authors** zero-speed events (EDL M2 freezes, zero-speed warps) instead of flattening them with a reason. Proof: an offline-authored freeze at a *different* source frame holds luma 125.09 for exactly 2.000 s, then the following cut resumes motion.
|
|
65
|
+
|
|
66
|
+
### Nested compounds: depth-2 black solved (`compounds[].compounds`)
|
|
67
|
+
|
|
68
|
+
`Timeline.CreateCompoundClip` works on 19.1.3, so a real doubly nested compound was made live and its archive diffed against the synthetic one that rendered black. Exactly one delta mattered: a Resolve-made compound's embedded pool `Sm2Sequence` FieldsBlob carries a **`SequenceSetup`** key (a 347-byte constant project-format blob) the donor template lacked. With it added, doubly nested synthetic content renders — bisect confirmed `SequenceSetup` alone flips it (E56), and the full tool-layer route proves it end to end (E57: white 234 through two nesting levels with flanking cuts intact). `spec.compounds` now nests recursively; depth-2 playback is render-verified, deeper composes structurally but is unverified.
|
|
69
|
+
|
|
70
|
+
### Verification
|
|
71
|
+
|
|
72
|
+
- Node (vendor + server): 852 passed (5 new tests incl. a byte-exact freeze-harvest fixture and a SequenceSetup template guard)
|
|
73
|
+
- Python: 3111 passed + 799 subtests
|
|
74
|
+
- Live: E55 harvests, E56 freeze + depth-2 bisect renders, E57 nested-spec render — all measured by frame luma / freezedetect
|
|
75
|
+
|
|
76
|
+
## What's New in v2.133.0 — the tool layer meets its own surface
|
|
77
|
+
|
|
78
|
+
The first end-to-end pass of the entire v2.106–v2.132 native-DRT authoring surface **through the MCP protocol layer** (every earlier proof drove the modules directly). A kitchen-sink spec — media cuts, cross-dissolve, 0.5x retime, V2 stacking, explicit audio placement, a compound clip, markers, and SRT subtitles in ONE assemble — was authored offline, imported, read back, and render-verified on Studio 19.1.3.7 (frame luma 122.9 / 181.6 mid-dissolve / 234 / 125.8 retime / 125.5 compound-inner; tone at the mono-strip -24.1 dB, then silence). The smoke test caught three real defects; all are fixed and regression-tested.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **Subtitles (and marker ownership) vanished when compounds were in the spec.** Both placement steps ran after compound insertion but still targeted the first name-sorted `SeqContainer` — and a compound's inner container matches that pattern. Measured live: the imported timeline had no subtitle track; the cues sat inside the compound. The parent container id is now pinned once, before any compound exists, and threaded through subtitle placement and the marker blob's owner.
|
|
83
|
+
- **`editorial.verify_roundtrip` could not close an EDL loop.** The zero-duration outgoing dissolve leg was paired as a real event (count mismatch), and EDL reel names (`CUTSRC`) had no way to match the re-export's file basenames (`cut_src`). Zero-length events are dropped, and a new `sourceMap` parameter — the same map that drove the assemble — derives the reel→basename aliases. EDL → assemble → import → OTIO-export → verify now passes with fitted per-source offsets.
|
|
84
|
+
- **`assemble_from_interchange` result note contradicted itself**, appending the stale pre-v2.111 "transitions become cuts" text after the authored-ledger sentence.
|
|
85
|
+
- **Headless recovery (#172):** a `-nogui` boot that never becomes scriptable still holds the one-per-machine singleton, wedging the GUI too. `resolve_headless.py start` now kills the instance it spawned when its readiness check fails; `stop --force` escalates TERM→KILL for an unanswering instance (unclean — expect project locks and a slow next boot); the headless-edit-loop guide names the precondition and a 30-second preflight.
|
|
86
|
+
|
|
87
|
+
### Verification
|
|
88
|
+
|
|
89
|
+
- Python: 3111 passed + 799 subtests (6 new recovery-path tests)
|
|
90
|
+
- Node (vendor + server): 847 passed (2 new regression tests)
|
|
91
|
+
- Live: kitchen-sink render probe + EDL round-trip pass on 19.1.3.7
|
|
92
|
+
|
|
93
|
+
## What's New in v2.132.1 — the nesting boundary
|
|
94
|
+
|
|
95
|
+
Knowledge release. **Depth-2 compound nesting renders black**: a compound placed inside another compound's inner container composes structurally — imports fully linked, reads back — but the doubly nested content renders black (the readback-blind class again). Depth-1, multiple parallel compounds per archive, remains the render-verified envelope; the tool doc now states the boundary.
|
|
96
|
+
|
|
97
|
+
Also corrected during cleanup: the crash-window "phantom projects" never existed — a project created moments before a Resolve crash dies with the instance (no DB row, no folder), and `DeleteProject` returning `False` afterwards means *nothing to delete*. Lesson recorded: re-list after a crash before diagnosing project state.
|
|
98
|
+
|
|
99
|
+
Suites: Node 845 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
100
|
+
|
|
101
|
+
## What's New in v2.132.0 — multiple compounds compose
|
|
102
|
+
|
|
103
|
+
### The one-per-archive restriction falls
|
|
104
|
+
|
|
105
|
+
Three separate dangling references each hard-crash Resolve's importer — mapped one crash at a time, then confirmed with an all-encodings reference sweep:
|
|
106
|
+
|
|
107
|
+
1. the pool element's `<MpFolder>` (v2.131)
|
|
108
|
+
2. the embedded sequence blob's keyed **`SeqRef`** — it names the inner *container's* uuid, patched through the keyed-dict codec
|
|
109
|
+
3. the embedded sequence's **`<Parent>`** — pointing back at the compound's own pool id
|
|
110
|
+
|
|
111
|
+
With all three rewired, every cluster identity freshens safely and **multiple compounds compose in one archive**. Also fixed en route: container listing is name-sorted, so an inner container could alphabetically precede the parent and swallow the next compound's item — the parent is now pinned explicitly.
|
|
112
|
+
|
|
113
|
+
**Render proof:** parent cut 124.5 → CMP_A's inner white 234 → CMP_B's inner cut 125.3 — two offline-authored nested timelines playing back to back on 19.1.3.
|
|
114
|
+
|
|
115
|
+
Suites: Node 845 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
116
|
+
|
|
117
|
+
## What's New in v2.131.0 — compound clips authored offline
|
|
118
|
+
|
|
119
|
+
### Nested timelines, fully offline
|
|
120
|
+
|
|
121
|
+
`drt.assemble` gains `spec.compounds`: author a compound clip — a nested timeline with its own inner edit — entirely offline, and it **renders** after import.
|
|
122
|
+
|
|
123
|
+
**Render proof (fresh project, 19.1.3):** parent cut (124.5) → the compound's inner cut at source offset 96 (125.3) → the compound's inner white (234). An offline-authored nested edit, playing.
|
|
124
|
+
|
|
125
|
+
**Two crash laws paid for the summit** (Resolve died twice mapping them):
|
|
126
|
+
- The compound cluster's identities ride **verbatim** — the embedded `Sm2Sequence` FieldsBlob encodes them, and freshening the XML ids around the unchanged blob crashes the importer outright. Hence: one compound per archive for now.
|
|
127
|
+
- A dangling `<MpFolder>` reference in the pool element also crashes the importer — it's rewired to the target pool's folder.
|
|
128
|
+
|
|
129
|
+
Inner content uses the ordinary cuts machinery on the inner container (origin frame 0), cloning the sources' captured native clips — `cut-media` now supports donor-less tracks when every cut carries one.
|
|
130
|
+
|
|
131
|
+
Suites: Node 845 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
132
|
+
|
|
133
|
+
## What's New in v2.130.0 — compound clips survive extraction
|
|
134
|
+
|
|
135
|
+
### The hollow-compound bug
|
|
136
|
+
|
|
137
|
+
A compound clip in a `.drp` is a pool `Sm2MpCompoundClip` embedding a full `Sm2Sequence` — whose actual tracks live in their **own SeqContainer**. The extraction recipe kept only the target timeline's container, so any timeline containing a compound extracted into a `.drt` whose compound imported, read back… and was **hollow**.
|
|
138
|
+
|
|
139
|
+
`extract_from_drp` now walks the kept container's `MediaRef`s → compound pool elements → embedded sequence ids → keeps the inner containers too, recursively (compounds nest).
|
|
140
|
+
|
|
141
|
+
**Live proof:** the fixed extraction imports 3/3 linked with the compound intact, and the archive **renders the compound's inner content** (cut 125.3 → white 234, audio −21.1 dB) — compound clips fully survive the `.drt` route on 19.1.3.
|
|
142
|
+
|
|
143
|
+
Also banked: the full `.drp` anatomy of compounds (embedded sequence identity, Fairlight blob, the hidden `000_Archive` pool location, the generic item blob) — the map for offline compound *authoring* later.
|
|
144
|
+
|
|
145
|
+
Suites: Node 844 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
146
|
+
|
|
147
|
+
## What's New in v2.129.0 — sidecar SRT in the conform route
|
|
148
|
+
|
|
149
|
+
Turnover packages usually ship a sidecar `.srt` next to the edit. `assemble_from_interchange` now takes `subtitlesSrtPath` and authors the cues onto the subtitle track in the same call — EDL/OTIO/AAF/XML/prproj in, picture + audio + subtitles out.
|
|
150
|
+
|
|
151
|
+
Suites: Node 843 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
152
|
+
|
|
153
|
+
## What's New in v2.128.0 — subtitles authored; track matrix complete
|
|
154
|
+
|
|
155
|
+
### The last track type falls
|
|
156
|
+
|
|
157
|
+
Subtitles turn out to be the **simplest item in the whole schema**: a plain `Sm2TiGenerator` with `PrettyType Subtitle` and the cue text in `<Name>` — no blobs at all, on a Type-2 track. No Fusion comp means the byte-keyed cache law doesn't apply, and the payload is API-visible after import.
|
|
158
|
+
|
|
159
|
+
`drt.assemble` gains `spec.subtitles` (frame-addressed cues) and `spec.subtitlesSrt` (**raw SRT in, cues out** — composes with `spec.startFrame`). Overlapping cues refuse; the track vec is synthesized from the harvested shape.
|
|
160
|
+
|
|
161
|
+
**Live proof:** SRT cues plus a spec-level cue import and read back at exact frames with their text. One measured caveat, documented: Resolve reads angle-bracket runs in cue text as SRT formatting markup and strips unknown tags from display (standard subtitle semantics — the authored XML carries them escaped and intact).
|
|
162
|
+
|
|
163
|
+
With this, the native authoring matrix covers **every track type**: video (cuts, stacking, dissolves, retimes), audio (placements, crossfades), and subtitles — plus markers, start TC, generators, and five interchange formats in.
|
|
164
|
+
|
|
165
|
+
Suites: Node 842 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
166
|
+
|
|
167
|
+
## What's New in v2.127.1 — audio ignores timemaps (measured)
|
|
168
|
+
|
|
169
|
+
Knowledge release closing the audio-retime question: a 50% keyed `Sm2TimeMap` on an imported **audio** clip *reads back* retimed (source 0..48 over 96 record frames) but **renders at 100%** — pitch and spectrum identical to the 1× reference. The audio engine ignores clip timemaps entirely while readback honors them: the readback/render divergence class, audio edition. Audio retimes remain honestly skipped, with the ledger reason now carrying the measurement.
|
|
170
|
+
|
|
171
|
+
Suites: Node 841 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
172
|
+
|
|
173
|
+
## What's New in v2.127.0 — sequence picker and reel aliasing
|
|
174
|
+
|
|
175
|
+
Two conform-ergonomics upgrades surfaced by real turnover shapes:
|
|
176
|
+
|
|
177
|
+
- **Multi-sequence containers**: `assemble_from_interchange` gains `sequenceName` / `sequenceIndex` for AAF and `.prproj`. When exactly one sequence carries events it auto-picks; when several do, it refuses and lists them (`index:name`) instead of flattening into an overlap refusal.
|
|
178
|
+
- **Reel aliasing**: sources now group **by file**, not by reel — multiple reels mapped to one `mediaFilePath` (Avid mob vs tape names, re-linked dailies) merge into a single source with combined cuts, instead of demanding a captured template per reel name.
|
|
179
|
+
|
|
180
|
+
Suites: Node 841 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
181
|
+
|
|
182
|
+
## What's New in v2.126.0 — AAF route fixed at the tool layer; harness parity
|
|
183
|
+
|
|
184
|
+
### The last gap in the AAF story
|
|
185
|
+
|
|
186
|
+
Two closures:
|
|
187
|
+
|
|
188
|
+
**A since-birth bug, fixed.** `assemble_from_interchange` with `format: 'aaf'` fell through to the sync parser — which throws for AAF — so the tool-layer AAF route had *never* worked (every earlier proof called the parser library directly). The handler now awaits the async `parseAAF`, and `aaf.mjs` falls back to the repo venv's Python (where `pyaaf2` lives), so the route works with zero environment setup. A stubbed regression test pins it.
|
|
189
|
+
|
|
190
|
+
**Harness parity.** The shipped `capture_media_template` ran live for both fixture sources — capturing `mediaStartTime` 3600 and the native clip elements through the real code path — and the tool-handler route produced renders **identical** to the hand-verified E36 run (126.376 / 95.965 / 95.964 / 126.373, audio −21.08 dB). Nothing hand-rolled remains in the chain.
|
|
191
|
+
|
|
192
|
+
Suites: Node 840 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
193
|
+
|
|
194
|
+
## What's New in v2.125.0 — the round-trip QC loop closes
|
|
195
|
+
|
|
196
|
+
### Prove the conform, don't trust it
|
|
197
|
+
|
|
198
|
+
New `editorial.verify_roundtrip`: parse the original turnover, parse Resolve's own re-export of the timeline you authored from it, and get a verdict — normalized for the three conventions that otherwise drown the diff in noise:
|
|
199
|
+
|
|
200
|
+
- track labels (`V` ≡ `V1`)
|
|
201
|
+
- source naming (AAF mob name vs file basename, extension-stripped)
|
|
202
|
+
- source frames (Resolve's OTIO export is **timecode-absolute** — a constant per-source offset is fitted, reported, and enforced)
|
|
203
|
+
|
|
204
|
+
**Live proof:** rich AAF → `assemble_from_interchange` → import → Resolve's own OTIO export → `pass: true`, 4 pairs, `srcOffsets` = 86400 for both sources — exactly their 01:00:00:00 TC bases. The record geometry survives the entire loop to the frame.
|
|
205
|
+
|
|
206
|
+
Real drift still trips it: a 5-frame source slip or a 2-frame record slip returns `pass: false` with the mismatch kind and location.
|
|
207
|
+
|
|
208
|
+
Suites: Node 839 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
209
|
+
|
|
210
|
+
## What's New in v2.124.0 — the Premiere leg; five formats proven
|
|
211
|
+
|
|
212
|
+
### .prproj in, frames out — no Premiere required
|
|
213
|
+
|
|
214
|
+
`assemble_from_interchange` gains `format: 'prproj'`: the Premiere project is read **offline** (gunzip + object-graph walk), converted through the same authoring bridge, and lands as a linked, rendering `.drt`.
|
|
215
|
+
|
|
216
|
+
**Live proof through the actual tool handler:** a schema-faithful synthetic `.prproj` (two sources on V1 + an audio event) → `.drt` → import (3/3 linked, fresh project) → render: 122.99 / 234, audio −21.1 dB.
|
|
217
|
+
|
|
218
|
+
That makes **all five interchange formats route-proven end-to-end**: EDL, OTIO, AAF, FCP7 XML, and `.prproj` — parse → assemble → import → measured frames and RMS.
|
|
219
|
+
|
|
220
|
+
Also: the result note that still claimed "retimes are flattened and transitions become cuts" (stale since v2.111/v2.113) now states the authored-ledger truth.
|
|
221
|
+
|
|
222
|
+
Suites: Node 837 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
223
|
+
|
|
224
|
+
## What's New in v2.123.0 — four formats proven; cross-link guard
|
|
225
|
+
|
|
226
|
+
### Route coverage complete
|
|
227
|
+
|
|
228
|
+
**All four interchange formats — EDL, OTIO, AAF, and FCP7 XML — are now route-proven end-to-end**: parse → `assemble_from_interchange` → `.drt` → import → measured frames and RMS. The XMEML leg (E37): two sources cut on V1 (122.99 / 234) with an explicit A1 audio event continuing at −21.1 dB under the second cut.
|
|
229
|
+
|
|
230
|
+
**And the guard the merge law demands:** `import_timeline_checked` now cross-checks `.drt`/`.drp` imports — the archive's `<MediaFilePath>` set vs the files the imported items *actually* link to. A missing expected file returns `cross_link_warning` with the full `{expected, actual, missing}` comparison. This catches the coarse-identity cross-link that `linked == total` is provably blind to (the wrongly-linked items read back fully linked, wrong clip name and all).
|
|
231
|
+
|
|
232
|
+
Suites: Node 836 pass / 0 fail; Python 3105 passed + 799 subtests.
|
|
233
|
+
|
|
234
|
+
## What's New in v2.122.0 — the AAF route, coast to coast
|
|
235
|
+
|
|
236
|
+
### AAF in, frames out
|
|
237
|
+
|
|
238
|
+
The full route is proven: a rich Resolve-exported AAF → `assemble_from_interchange` → `.drt` → import → render, **every window frame-accurate** (Studio 19.1.3.7, headless):
|
|
239
|
+
|
|
240
|
+
| Window | Expected | Measured |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| V1 rt_source_1 | ~126 | 126.4 |
|
|
243
|
+
| V2 rt_source_2 stacked over V1 | ~96 | **95.97** |
|
|
244
|
+
| V1's rt_source_2 cut | ~96 | **95.96** |
|
|
245
|
+
| V1 rt_source_1 tail | ~126 | 126.4 |
|
|
246
|
+
| Audio (both source windows) | tone | −21 dB |
|
|
247
|
+
|
|
248
|
+
Channel-leg merge, V2 stacking, and TC-bearing sources (embedded 01:00:00:00 via `MediaStartTime`) verified in one render. The v2.120 native-donor clone path is now **render-verified**.
|
|
249
|
+
|
|
250
|
+
**New law (`api_truth`):** `ImportTimelineFromFile` merges pool media by a *coarse* identity across imports — two different files (different names and sizes, mtimes 1 s apart) carried identity blobs byte-identical except uuids, and in a non-empty project the second file's clips silently played the first file's picture. Fresh projects materialize both correctly; verify per-item paths (or render probes) after importing into non-empty projects.
|
|
251
|
+
|
|
252
|
+
Also recorded: the modal-wedge failure mode and its recovery (force-kill + headless relaunch; headless is *not* modal-immune — a would-be dialog hangs the call; a hard-wedged render has no API exit).
|
|
253
|
+
|
|
254
|
+
Suites: Node 836 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
255
|
+
|
|
256
|
+
## What's New in v2.121.0 — one marker codec everywhere
|
|
257
|
+
|
|
258
|
+
Offline consolidation release (live validation is paused on a stuck Resolve dialog — see v2.120.0). All marker paths now share the single measured codec:
|
|
259
|
+
|
|
260
|
+
- `seq-container-builder` encodes lockable-blob markers with `timeline-markers-blob` (byte-exact vs a live Resolve export) instead of the deprecated simplified encoder
|
|
261
|
+
- `editorial.marker_roundtrip` adds a **binary** round-trip through the real codec, with provenance riding in `customData` — the result gains `blobRoundTrip`
|
|
262
|
+
- `parseOTIO` picks up **track-level** markers (record-time `marked_range`) alongside clip-level ones
|
|
263
|
+
|
|
264
|
+
Suites: Node 836 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
265
|
+
|
|
266
|
+
## What's New in v2.120.0 — AAF channel-leg merge; native-donor path staged
|
|
267
|
+
|
|
268
|
+
### The AAF leg, part one
|
|
269
|
+
|
|
270
|
+
Driving a real Resolve-exported AAF through `assemble_from_interchange` surfaced two truths and staged one architecture change:
|
|
271
|
+
|
|
272
|
+
- **AAF duplicates audio per channel.** Every A-track event in a rich Resolve 19 export arrives twice (one per channel leg). The bridge now merges identical legs instead of refusing them as a same-track overlap (`report.audioChannelLegsMerged`, tested); skipped-audio accounting corrected.
|
|
273
|
+
- **Embedded source timecode matters.** A `.mov` with embedded 01:00:00:00 fails the render with *"Full resolution media not found at 01:00:00:00"* — the native clip stores `<MediaStartTime>` in seconds where the template donor has 0. `capture_media_template` now harvests `mediaStartTime` plus the source's native timeline-clip elements.
|
|
274
|
+
- **Native-donor clone path (staged, live-unverified).** `cut-media` can clone the source's own captured clip (per track type, wrapper kept). Only caches carrying the new fields reach it — every existing capture keeps the proven donor path. Live verification is pending: a stuck Resolve modal (import-failure dialog) wedged the session mid-expedition — after it, even previously-proven files refused to import, so every later measurement was of the wedge, not the code. The resume plan is recorded.
|
|
275
|
+
|
|
276
|
+
Suites: Node 835 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
277
|
+
|
|
278
|
+
## What's New in v2.119.0 — turnover markers ride the conform
|
|
279
|
+
|
|
280
|
+
### Locators survive the trip
|
|
281
|
+
|
|
282
|
+
Editorial marks up a cut; the conform should keep those marks. Now it does: **EDL `* LOC:` locators** (the Avid convention) and **OTIO `Marker` objects** parse into the normalized event stream and come out the other end as real timeline markers in the assembled `.drt` — names, colors, exact frames.
|
|
283
|
+
|
|
284
|
+
**Full-route proof:** an EDL with two `LOC` lines imports as a timeline whose markers read back at exactly frames 24 and 60 with their names and mapped colors (Red / Green) through the marker API.
|
|
285
|
+
|
|
286
|
+
### Changes
|
|
287
|
+
- `parseEDL`: `* LOC:` lines → `track: 'MARKER'` pseudo-events (never miscounted as skipped audio)
|
|
288
|
+
- `parseOTIO`: clip markers → record-position MARKER events
|
|
289
|
+
- `eventsToAssembleSpec`: authors `spec.markers`; interchange colors map onto the measured 16-color Resolve palette (MAGENTA→Fuchsia, ORANGE→Sand, WHITE→Cream, BLACK→Cocoa; unknown→Blue); `report.authoredMarkers`
|
|
290
|
+
- 2 new bridge tests
|
|
291
|
+
|
|
292
|
+
Suites after last edit: Node 834 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
293
|
+
|
|
294
|
+
## What's New in v2.118.0 — timeline markers authored offline
|
|
295
|
+
|
|
296
|
+
### Markers ride the .drt now
|
|
297
|
+
|
|
298
|
+
`drt.assemble` gains `spec.markers` — timeline markers with all 16 colors, names, notes, durations, and `customData`, authored fully offline and verified by API readback after import.
|
|
299
|
+
|
|
300
|
+
**The decode:** markers live in `project.xml` as a `Sm2SequenceLockableBlob` (owner = the timeline's `Sm2Sequence` DbId) wrapping a zstd-framed protobuf. Resolve itself emits **raw-block zstd** for small payloads and accepts it on import — so the codec needs no zstd library. The new `timeline-markers-blob.js` encoder is **byte-exact** against Resolve 19.1.3.7's own export (fixture checked in).
|
|
301
|
+
|
|
302
|
+
**The correction:** the legacy `marker-encoder.js` color map was wrong (Yellow is 16, not 8; Purple is 128, not 131072) and its output never matched a real export — now deprecated with a pointer. The full 16-color bit map was harvested live, one marker per color.
|
|
303
|
+
|
|
304
|
+
**Proof:** offline-authored markers (Red with note + duration 12; Mint with `customData`) read back perfectly through the marker API after import.
|
|
305
|
+
|
|
306
|
+
Suites after last edit: Node 832 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
307
|
+
|
|
308
|
+
## What's New in v2.117.0 — start-timecode fidelity
|
|
309
|
+
|
|
310
|
+
### The conform emulator keeps the real start TC
|
|
311
|
+
|
|
312
|
+
AAF/EDL turnovers rarely start at 01:00:00:00 — and until now the assembled timeline silently did. `assemble_from_interchange` gains `preserveStartTimecode: true`: the timeline starts at the turnover's **real first record frame** (the long-standing AAF rule "build at THAT start" — now automated).
|
|
313
|
+
|
|
314
|
+
**The discovery:** a timeline's start timecode lives in exactly one non-cosmetic place in a `.drp`/`.drt` — the pool `Sm2MpTimelineClip`'s `MediaExtents` blob, 16 bytes of LE doubles `[startSeconds, durationSeconds]`. Patch it offline, keep clips at absolute frames ≥ the new origin, and the import lands at the new start TC and renders.
|
|
315
|
+
|
|
316
|
+
**Proof:** offline patch to 02:00:00:00 → readback `02:00:00:00`, live frame; full route: a 00:59:52:00 EDL → timeline at 00:59:52:00 (86208–86304) with both sources rendering correctly.
|
|
317
|
+
|
|
318
|
+
### Changes
|
|
319
|
+
- `drt.assemble`: `spec.startFrame` (frames @24; before-origin cuts still refuse, against the new origin)
|
|
320
|
+
- `assemble_from_interchange`: `preserveStartTimecode`
|
|
321
|
+
- `api_truth` MediaExtents entry; 2 new tests
|
|
322
|
+
|
|
323
|
+
Suites after last edit: Node 830 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
324
|
+
|
|
325
|
+
## What's New in v2.116.2 — flat-target wording routed to assemble
|
|
326
|
+
|
|
327
|
+
Doc-clarity release from a post-release drift review (which found everything else clean — generated files, tool counts, api-limitations, version stamps). `convert_to_interchange`'s flat DRT target still flattens retimes by design, but the claim "the DRT clip schema has no per-clip speed field" read misleadingly now that `drt.assemble` authors retimes via `Sm2TimeMap` (v2.113+). The tool description and `resolve-advanced/README.md` now name the flat target explicitly and route to `drt.assemble_from_interchange` for authored retimes, dissolves, multi-track video, and audio.
|
|
328
|
+
|
|
329
|
+
Suites: Node 828 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
330
|
+
|
|
331
|
+
## What's New in v2.116.1 — the flat-timemap divergence
|
|
332
|
+
|
|
333
|
+
Knowledge release. Freeze-frame probe: a **flat** keyed `Sm2TimeMap` (both keyframes at the same source Y) is the one timemap shape where readback and render *disagree in the trusting direction* — the imported item reads back frozen (source 96..96) but **renders moving** (48/48 unique frames). Freezes therefore stay in `flattenedRetimes` with the reason rather than being authored as flat maps. Recorded in `api_truth` (the readback-blind class now has a member that lies in both directions).
|
|
334
|
+
|
|
335
|
+
Suites: Node 828 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
336
|
+
|
|
337
|
+
## What's New in v2.116.0 — audio cross-fades authored
|
|
338
|
+
|
|
339
|
+
### The conform emulator learns audio cross-fades
|
|
340
|
+
|
|
341
|
+
An audio dissolve in interchange now becomes a **real, rendering cross-fade** in the assembled `.drt`.
|
|
342
|
+
|
|
343
|
+
**The harvest:** Resolve has no API for transitions, so we let it author one — an FCP7 `KGAudioTransCrossFade` imported via XMEML lands as an audio `Sm2TiTransition` (PrettyType "Final Cut Pro 7", which is what Resolve itself stores — and renders). That element is now a bundled template.
|
|
344
|
+
|
|
345
|
+
**Render proof:** the offline-authored crossfade's highpass-RMS **ramps** through the junction (−27.6 → −25.6 → −23.0 → −21.9 dB), identical in shape to a Resolve-authored control; a butt cut steps.
|
|
346
|
+
|
|
347
|
+
### Changes
|
|
348
|
+
- `placeTransition` `trackType: 'audio'`; `drt.assemble` `transitions[].trackType`
|
|
349
|
+
- `eventsToAssembleSpec`: audio dissolves authored under the same abut/handle geometry; drops carry `trackType: 'audio'` and the reason
|
|
350
|
+
- XMEML gotcha recorded: an `<audio><channelcount>` block inside a *file definition* aborts the whole import silently
|
|
351
|
+
- 2 new bridge tests; template wrapper guard extended
|
|
352
|
+
|
|
353
|
+
Suites after last edit: Node 828 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
354
|
+
|
|
355
|
+
## What's New in v2.115.1 — native DRT authoring guide
|
|
356
|
+
|
|
357
|
+
Documentation release: [docs/guides/native-drt-authoring.md](https://github.com/samuelgursky/davinci-resolve-mcp/blob/main/docs/guides/native-drt-authoring.md) consolidates the offline-authoring subsystem (v2.105–v2.115) — every capability with its spec surface, the four measured laws (Fusion comp byte-keyed cache, Fairlight strip, Sm2TimeMap generation split, timeline origin), the readback-is-blind verification doctrine, and a delivery checklist. AGENTS.md links it from the Conform/Interchange workflow row; per-IDE agent rules regenerated.
|
|
358
|
+
|
|
359
|
+
Suites: Node 826 pass / 0 fail; Python 3101 passed + 799 subtests.
|
|
360
|
+
|
|
361
|
+
## What's New in v2.115.0 — audio authored: the Fairlight strip law
|
|
362
|
+
|
|
363
|
+
### The conform emulator learns audio
|
|
364
|
+
|
|
365
|
+
A-track events in interchange now come out the other end as **real, playing audio clips** — the last big honesty-ledger item (`audioEventsSkipped`) falls.
|
|
366
|
+
|
|
367
|
+
**The law (measured by elimination):** audio tracks cannot be grown offline. The per-timeline Fairlight model (`FLStudioModelBA`, inside the media pool's `Sm2Sequence.FieldsBlob`) holds one mixer strip per audio track — a cloned track imports fine, reads back fine, and renders **silent**. We made the clip byte-identical to a live-authored one, the track byte-identical (`SubType` is the channel-format code — 1=mono — not an ordinal), shared the pool entry with a playing A1 clip: still silent. Only a template *captured* with the tracks plays. Audio aliveness is readback-blind — verify by rendered RMS.
|
|
368
|
+
|
|
369
|
+
**The fix is the capture-once pattern again:** the r19 media template was re-captured live with **8 mono audio tracks** (valid strips ride along). `audioOnly` cuts land on A1–A8 and render at native level; placements beyond the ceiling refuse with instructions.
|
|
370
|
+
|
|
371
|
+
**Full-route proof:** OTIO with V + two audio tracks → `.drt` → import → render: A1 tone −21.09 dB, A2 tone −24.08 dB (exactly the native control), video alive throughout.
|
|
372
|
+
|
|
373
|
+
### Changes
|
|
374
|
+
- `drt.assemble`: `cuts[].audioOnly` + `track` (1–8); explicit audio suppresses the A1 convenience mirror; audio clones carry their own source identity (donor-identity clones were part of the silence)
|
|
375
|
+
- `eventsToAssembleSpec`: A-track events authored with per-track overlap checks; audio retimes skipped with reason; OTIO/EDL audio tracks numbered (`A`, `A2`, …)
|
|
376
|
+
- `api_truth`: Fairlight-strip entry (silent-failure class); 3 new tests
|
|
377
|
+
|
|
378
|
+
Suites after last edit: Node 826 pass / 0 fail; Python 3101 passed + 796 subtests.
|
|
379
|
+
|
|
380
|
+
## What's New in v2.114.0 — reverse retimes authored
|
|
381
|
+
|
|
382
|
+
### The last flattened retime falls
|
|
383
|
+
|
|
384
|
+
Reversed clips in interchange (OTIO negative `time_scalar`, XMEML/EDL reverse) are now **authored** into the assembled .drt — `flattenedRetimes` only holds zero-speed freezes.
|
|
385
|
+
|
|
386
|
+
**The shape:** reverse is the same r19 keyed `Sm2TimeMap` with the Y endpoints swapped — kf0=(0, YMax), kf1=(XMax, 0), a descending line. The encoder is **byte-exact** against Resolve 19.1.3.7's own −100% retime export.
|
|
387
|
+
|
|
388
|
+
**The In rule (measured):** for a reversed clip, `<In>` measures from the source **end**: `(sourceFrames − srcIn − dur×speed)/speed`. Offline proof: a reversed srcIn-24 dur-48 cut reads back source 71→23 — exactly the prediction — and renders 48 live frames.
|
|
389
|
+
|
|
390
|
+
### Changes
|
|
391
|
+
- `drt.assemble`: `cuts[].reverse` (composable with `cuts[].speed`)
|
|
392
|
+
- `eventsToAssembleSpec`: reverse authored; ledger reasons updated
|
|
393
|
+
- `api_truth` timemap entry extended with the reverse shape + In-from-end rule
|
|
394
|
+
|
|
395
|
+
Suites after last edit: Node 823 pass / 0 fail; Python 3101 passed + 796 subtests.
|
|
396
|
+
|
|
397
|
+
## What's New in v2.113.0 — retimes authored: the r19 Sm2TimeMap
|
|
398
|
+
|
|
399
|
+
### The conform emulator learns speed
|
|
400
|
+
|
|
401
|
+
A 50% `LinearTimeWarp` in OTIO now comes out the other end as a **real retime** in the imported timeline — not a flattened 100% clip.
|
|
402
|
+
|
|
403
|
+
**The discovery:** `Sm2TimeMap` keyframes are generation-split. Resolve 21 stores protobuf points; Resolve 19 stores a keyed-dict of keyed-dict keyframes — and **19 silently ignores the protobuf form on import** (the clip reads back at 100%, no warning). The new `buildConstantSpeedTimemapKeyed` encoder emits the r19 form and is **byte-exact** against a timemap authored by Resolve 19.1.3.7 itself.
|
|
404
|
+
|
|
405
|
+
**Full-route proof:** OTIO `time_scalar: 0.5` → `assemble_from_interchange` → import → the item reads source 96..120 over 48 record frames (50% at source offset 96, the exact interchange intent) and renders live.
|
|
406
|
+
|
|
407
|
+
### Semantics measured
|
|
408
|
+
- The timemap spans the **whole source** stretched by 1/speed; the clip's `<In>`/`<Duration>` window into it in **record-domain** frames (`srcIn` converts by `/speed`)
|
|
409
|
+
- Retimed cuts are video-only on A1 (audio would need its own timemap + pitch handling — stated in the ledger, not silent)
|
|
410
|
+
- Reverse still flattens, with the reason; `report.authoredRetimes` joins the ledger
|
|
411
|
+
|
|
412
|
+
### Changes
|
|
413
|
+
- `drt.assemble`: `cuts[].speed` (forward constant, e.g. 0.5)
|
|
414
|
+
- `eventsToAssembleSpec`: forward speeds authored, reverse flattened with reason
|
|
415
|
+
- `api_truth`: generation-split entry (silent-failure class); harvest fixture + byte-exactness unit test
|
|
416
|
+
|
|
417
|
+
Suites after last edit: Node 822 pass / 0 fail; Python 3101 passed + 796 subtests.
|
|
418
|
+
|
|
419
|
+
## What's New in v2.112.0 — multi-track video authoring
|
|
420
|
+
|
|
421
|
+
### The conform emulator goes multi-track
|
|
422
|
+
|
|
423
|
+
Two-video-track interchange (OTIO/XMEML) now assembles into a .drt with real V2+ stacking — and it renders.
|
|
424
|
+
|
|
425
|
+
**Render proof (Studio 19.1.3.7):** two-track OTIO → `assemble_from_interchange` → import (3/3 linked) → render: V1 testsrc at 122.8/125.5 with the V2 white insert covering the middle at exactly **234**.
|
|
426
|
+
|
|
427
|
+
### Changes
|
|
428
|
+
- `cutSourceIntoClips`: cuts gain `track` (1-based); missing video tracks grown as empty clones; track>1 cuts are **video-only** (their audio would overlap A1 — stated, not silent)
|
|
429
|
+
- OTIO/XMEML parsers number video tracks `V, V2, V3, …`; EDL stays single-V
|
|
430
|
+
- `eventsToAssembleSpec`: overlap judged **per video track** (V2 over V1 is legitimate geometry); dissolves match predecessors on their own track; ledger gains `upperTrackCutsVideoOnly`
|
|
431
|
+
- 2 new Node tests: V2 cut mapping, per-track overlap refusal naming the track
|
|
432
|
+
|
|
433
|
+
Suites after last edit: Node 820 pass / 0 fail; Python 3101 passed + 796 subtests.
|
|
434
|
+
|
|
435
|
+
## What's New in v2.111.0 — dissolves authored coast-to-coast
|
|
436
|
+
|
|
437
|
+
### The conform emulator learns dissolves
|
|
438
|
+
|
|
439
|
+
An EDL `D`-event now comes out the other end as a **real, rendering Cross Dissolve** — not a cut.
|
|
440
|
+
|
|
441
|
+
**Render proof (Studio 19.1.3.7):** an offline-authored `Sm2TiTransition` over transplanted cross-source media blends exactly through the cut — outgoing testsrc 123.9 → 130.8 → **181.6 at mid-dissolve** (predicted (124+234)/2 = 179) → 223.2 → incoming white 234. Transitions carry no Fusion comp, so the byte-keyed comp-cache law (v2.109.0/v2.110.0) does not apply: the harvested transition renders live on 19.
|
|
442
|
+
|
|
443
|
+
### Changes
|
|
444
|
+
- `eventsToAssembleSpec` **authors** a cross-dissolve when the predecessor ends exactly at the cut and both sides have handle media for the centered span; every non-authorable dissolve stays in `droppedTransitions` **with the reason** (no abutting predecessor / insufficient handles, side named). The report gains `authoredTransitions`.
|
|
445
|
+
- Full route re-proven live: EDL `D 024` → `drt.assemble_from_interchange` → `.drt` → `timeline.import_timeline_checked` → render → 181.6 mid-blend.
|
|
446
|
+
- `drt` tool doc updated: transitions no longer "become cuts".
|
|
447
|
+
- 4 new Node tests cover the authored / no-incoming-handle / no-outgoing-tail / record-gap branches.
|
|
448
|
+
|
|
449
|
+
Suites after last edit: Node 818 pass / 0 fail; Python 3101 passed + 796 subtests.
|
|
450
|
+
|
|
451
|
+
## What's New in v2.110.0 — offline generators render on 19; cache law scoped to titles
|
|
452
|
+
|
|
453
|
+
### Element expedition, part two — generators are exempt
|
|
454
|
+
|
|
455
|
+
v2.109.0 mapped the law: imported Fusion comps on Resolve 19.x render only via the machine's byte-keyed Fusion disk cache. This release proves the carve-out: **built-in generators are plain `Sm2TiGenerator` clips with no Fusion comp, and they render live from a fully offline-authored .drt** — measured on Studio 19.1.3.7 over transplanted white media (YAVG 234):
|
|
456
|
+
|
|
457
|
+
| Element | YAVG | Verdict |
|
|
458
|
+
|---|---|---|
|
|
459
|
+
| Solid Color on V2 | 16.0 | alive — covers the white |
|
|
460
|
+
| half-coverage control | 16 / 234 in one render | discrimination clean |
|
|
461
|
+
| `PrettyType` → SMPTE Color Bar | 104.9 | bars render |
|
|
462
|
+
| `PrettyType` → Grey Scale | 125.1 | ramp renders |
|
|
463
|
+
|
|
464
|
+
So offline element authoring on pre-21 is real for generators (slates, leaders, bars, solids) — only Fusion **titles** remain cache-bound, with the live `timeline.set_title_text` post-import flow as the working alternative.
|
|
465
|
+
|
|
466
|
+
### Changes
|
|
467
|
+
- `drt.assemble`: `elementsWarning` now fires **only for title elements** on pre-21 targets and documents verified generator kinds; spec doc lists `generatorName` options
|
|
468
|
+
- `api_truth`: generator exemption added to the byte-keyed cache-law entry; `api-limitations.md` regenerated
|
|
469
|
+
- **Version stamps unified**: v2.109.0's bump missed `install.py` and `src/granular/common.py` — the CI smoke test correctly **blocked** that npm publish (2.109.0 never reached npm). All four stamps now move together, enforced by `test_npm_package_metadata`
|
|
470
|
+
- New Node test: generator kind selection lands in the sequence XML; warning gate is title-only
|
|
471
|
+
|
|
472
|
+
Suites after last edit: Node 814 pass / 0 fail; Python 3101 passed + 796 subtests.
|
|
473
|
+
|
|
474
|
+
## What's New in v2.109.0 — element render law: byte-keyed Fusion cache on 19.x
|
|
475
|
+
|
|
476
|
+
### Element transplant expedition — verdict
|
|
477
|
+
|
|
478
|
+
**The law (measured on Studio 19.1.3.7):** a Fusion comp arriving via timeline import renders on Resolve 19.x **only** when the machine's Fusion disk cache holds frames keyed to the comp blob's *exact compressed bytes*. An identity recompression — byte-identical Lua, different zlib bytes, framing verified consistent — imported and read back perfectly but rendered black, while the untouched harvest rendered its cached frames. The live-render fallback for imported comps produces no frames on 19; Resolve 21-generation hosts render imported comps live (where the title/generator primitives were originally proven).
|
|
479
|
+
|
|
480
|
+
**Consequence:** offline text patching of Fusion comps for a 19.x host is impossible *by design* — no valid re-encoding can hit the byte-keyed cache.
|
|
481
|
+
|
|
482
|
+
**The working pre-21 flow:** `drt.assemble` media offline (native-descriptor transplant renders everywhere), then set title text **post-import** with `timeline.set_title_text` (its Fusion-comp write path is live-verified on 19.1.3).
|
|
483
|
+
|
|
484
|
+
### Changes
|
|
485
|
+
- `composition-text`: wrong plaintext dual-mode branch reverted (both generations share identical nested framing); law documented at `rewriteInner`
|
|
486
|
+
- r19 title/generator snippets `<Element>`-wrapped (raw clips concatenated into Items made render jobs fail with no status); guard test added
|
|
487
|
+
- `snippetPathFor(templateVersion)` selects r19 snippets for pre-21 targets; `drt.assemble` `elementsWarning` now states the law and the working flow
|
|
488
|
+
- `api_truth`: new entry *Imported Fusion comps render via byte-keyed disk cache on 19.x*; `api-limitations.md` regenerated
|
|
489
|
+
|
|
490
|
+
Suites: Node 813 pass / 0 fail; Python 3101 passed + 796 subtests.
|
|
491
|
+
|
|
5
492
|
## What's New in v2.108.0
|
|
6
493
|
|
|
7
494
|
**The conform emulator, coast to coast.** An interchange file goes in; an
|
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.136.1 版 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.136.
|
|
40
|
+
VERSION = "2.136.1"
|
|
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
|
@@ -72,24 +72,24 @@ def run(check_only: bool) -> int:
|
|
|
72
72
|
problems.append(f"skill '{name}': canonical exists but .claude adapter is missing — it would never load in Claude Code")
|
|
73
73
|
if not check_only and agent_p.exists():
|
|
74
74
|
claude_p.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
-
claude_p.write_text(agent_p.read_text())
|
|
75
|
+
claude_p.write_text(agent_p.read_text(encoding="utf-8"), encoding="utf-8")
|
|
76
76
|
continue
|
|
77
|
-
src = claude_p.read_text()
|
|
78
|
-
if not agent_p.exists() or agent_p.read_text() != src:
|
|
77
|
+
src = claude_p.read_text(encoding="utf-8")
|
|
78
|
+
if not agent_p.exists() or agent_p.read_text(encoding="utf-8") != src:
|
|
79
79
|
problems.append(f"skill '{name}': .agents copy {'missing' if not agent_p.exists() else 'differs'}")
|
|
80
80
|
if not check_only:
|
|
81
81
|
agent_p.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
-
agent_p.write_text(src)
|
|
82
|
+
agent_p.write_text(src, encoding="utf-8")
|
|
83
83
|
for name, claude_p, role_p in role_pairs():
|
|
84
84
|
if not claude_p.exists():
|
|
85
85
|
problems.append(f"role '{name}': .agents/roles exists but .claude/agents is missing — restore the frontmatter file by hand (it carries tools/model pins this script cannot invent)")
|
|
86
86
|
continue
|
|
87
|
-
body = strip_frontmatter(claude_p.read_text())
|
|
88
|
-
if not role_p.exists() or role_p.read_text() != body:
|
|
87
|
+
body = strip_frontmatter(claude_p.read_text(encoding="utf-8"))
|
|
88
|
+
if not role_p.exists() or role_p.read_text(encoding="utf-8") != body:
|
|
89
89
|
problems.append(f"role '{name}': .agents/roles {'missing' if not role_p.exists() else 'differs from the .claude/agents body'}")
|
|
90
90
|
if not check_only:
|
|
91
91
|
role_p.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
-
role_p.write_text(body)
|
|
92
|
+
role_p.write_text(body, encoding="utf-8")
|
|
93
93
|
if problems:
|
|
94
94
|
for p in problems:
|
|
95
95
|
print(("DRIFT: " if check_only else "SYNCED: ") + p)
|
|
@@ -49,7 +49,7 @@ def parse_documented_methods(docs_path: Path) -> Dict[str, Set[str]]:
|
|
|
49
49
|
"""
|
|
50
50
|
classes: Dict[str, Set[str]] = {}
|
|
51
51
|
current_class: str | None = None
|
|
52
|
-
text = docs_path.read_text()
|
|
52
|
+
text = docs_path.read_text(encoding="utf-8")
|
|
53
53
|
# Truncate at the first deprecation marker
|
|
54
54
|
for marker in ("\nDeprecated Resolve API Functions",
|
|
55
55
|
"\nUnsupported Resolve API Functions"):
|
|
@@ -86,7 +86,7 @@ def collect_source_text() -> str:
|
|
|
86
86
|
if "__pycache__" in py_path.parts:
|
|
87
87
|
continue
|
|
88
88
|
try:
|
|
89
|
-
parts.append(py_path.read_text())
|
|
89
|
+
parts.append(py_path.read_text(encoding="utf-8"))
|
|
90
90
|
except (OSError, UnicodeDecodeError):
|
|
91
91
|
continue
|
|
92
92
|
return "\n".join(parts)
|
|
@@ -100,7 +100,7 @@ def find_broken_api_imports() -> List[Tuple[Path, int, str]]:
|
|
|
100
100
|
if "__pycache__" in py_path.parts:
|
|
101
101
|
continue
|
|
102
102
|
try:
|
|
103
|
-
for i, line in enumerate(py_path.read_text().splitlines(), 1):
|
|
103
|
+
for i, line in enumerate(py_path.read_text(encoding="utf-8").splitlines(), 1):
|
|
104
104
|
if pattern.search(line):
|
|
105
105
|
hits.append((py_path.relative_to(REPO_ROOT), i, line.strip()))
|
|
106
106
|
except (OSError, UnicodeDecodeError):
|
|
@@ -199,7 +199,7 @@ def find_undocumented_method_wrappers(
|
|
|
199
199
|
if "__pycache__" in py_path.parts:
|
|
200
200
|
continue
|
|
201
201
|
try:
|
|
202
|
-
for i, line in enumerate(py_path.read_text().splitlines(), 1):
|
|
202
|
+
for i, line in enumerate(py_path.read_text(encoding="utf-8").splitlines(), 1):
|
|
203
203
|
for m in call_pattern.finditer(line):
|
|
204
204
|
name = m.group(1)
|
|
205
205
|
if name in documented or name in seen:
|
package/scripts/doctor.py
CHANGED
|
@@ -240,7 +240,7 @@ def _normalize_separators(text: str) -> str:
|
|
|
240
240
|
def file_contains(path: Path, needles: list[str]) -> tuple[bool, str]:
|
|
241
241
|
if not path.exists():
|
|
242
242
|
return False, "missing"
|
|
243
|
-
text = _normalize_separators(path.read_text(errors="replace"))
|
|
243
|
+
text = _normalize_separators(path.read_text(errors="replace", encoding="utf-8"))
|
|
244
244
|
missing = [needle for needle in needles if _normalize_separators(needle) not in text]
|
|
245
245
|
if missing:
|
|
246
246
|
return False, "missing: " + ", ".join(missing)
|
|
@@ -250,7 +250,7 @@ def file_contains(path: Path, needles: list[str]) -> tuple[bool, str]:
|
|
|
250
250
|
def version_from_server() -> str:
|
|
251
251
|
if not SERVER.exists():
|
|
252
252
|
return "unknown"
|
|
253
|
-
match = re.search(r'^VERSION\s*=\s*"([^"]+)"', SERVER.read_text(errors="replace"), re.M)
|
|
253
|
+
match = re.search(r'^VERSION\s*=\s*"([^"]+)"', SERVER.read_text(errors="replace", encoding="utf-8"), re.M)
|
|
254
254
|
return match.group(1) if match else "unknown"
|
|
255
255
|
|
|
256
256
|
|
|
@@ -139,7 +139,7 @@ def main(argv: list[str]) -> int:
|
|
|
139
139
|
args = _parse_args(argv)
|
|
140
140
|
content = render()
|
|
141
141
|
if args.check:
|
|
142
|
-
current = DOC_PATH.read_text() if DOC_PATH.exists() else ""
|
|
142
|
+
current = DOC_PATH.read_text(encoding="utf-8") if DOC_PATH.exists() else ""
|
|
143
143
|
if current != content:
|
|
144
144
|
print(
|
|
145
145
|
f"STALE: {DOC_PATH.relative_to(REPO_ROOT)} is out of date.\n"
|
|
@@ -149,7 +149,7 @@ def main(argv: list[str]) -> int:
|
|
|
149
149
|
return 1
|
|
150
150
|
print(f"OK: {DOC_PATH.relative_to(REPO_ROOT)} is up to date.")
|
|
151
151
|
return 0
|
|
152
|
-
DOC_PATH.write_text(content)
|
|
152
|
+
DOC_PATH.write_text(content, encoding="utf-8")
|
|
153
153
|
print(f"Wrote {DOC_PATH.relative_to(REPO_ROOT)}")
|
|
154
154
|
return 0
|
|
155
155
|
|
package/scripts/mode_matrix.py
CHANGED
|
@@ -162,7 +162,7 @@ def completed_probes(path: Path) -> Dict[str, Dict[str, Any]]:
|
|
|
162
162
|
results: Dict[str, Dict[str, Any]] = {}
|
|
163
163
|
if not path.exists():
|
|
164
164
|
return results
|
|
165
|
-
for line in path.read_text(errors="replace").splitlines():
|
|
165
|
+
for line in path.read_text(errors="replace", encoding="utf-8").splitlines():
|
|
166
166
|
line = line.strip()
|
|
167
167
|
if not line:
|
|
168
168
|
continue
|
|
@@ -183,7 +183,7 @@ def last_phase(path: Path) -> Optional[Dict[str, Any]]:
|
|
|
183
183
|
if not path.exists():
|
|
184
184
|
return None
|
|
185
185
|
marker = None
|
|
186
|
-
for line in path.read_text(errors="replace").splitlines():
|
|
186
|
+
for line in path.read_text(errors="replace", encoding="utf-8").splitlines():
|
|
187
187
|
line = line.strip()
|
|
188
188
|
if not line:
|
|
189
189
|
continue
|
|
@@ -197,7 +197,7 @@ def last_phase(path: Path) -> Optional[Dict[str, Any]]:
|
|
|
197
197
|
|
|
198
198
|
|
|
199
199
|
def append(path: Path, record: Dict[str, Any]) -> None:
|
|
200
|
-
with path.open("a") as handle:
|
|
200
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
201
201
|
handle.write(json.dumps(record, default=str) + "\n")
|
|
202
202
|
|
|
203
203
|
|
|
@@ -177,7 +177,7 @@ def main() -> int:
|
|
|
177
177
|
wanted = set(args.probes)
|
|
178
178
|
probes = [p for p in CATALOGUE if p.name in wanted]
|
|
179
179
|
|
|
180
|
-
with args.out.open("a") as handle:
|
|
180
|
+
with args.out.open("a", encoding="utf-8") as handle:
|
|
181
181
|
try:
|
|
182
182
|
build_fixture(ctx)
|
|
183
183
|
except Exception as exc:
|
|
@@ -103,7 +103,7 @@ def build_assets() -> None:
|
|
|
103
103
|
for g in (0.0, 1.0):
|
|
104
104
|
for r in (0.0, 1.0):
|
|
105
105
|
lines.append(f"{min(b + 0.15, 1.0):.6f} {g:.6f} {min(r + 0.15, 1.0):.6f}")
|
|
106
|
-
LUT.write_text("\n".join(lines) + "\n")
|
|
106
|
+
LUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
def frame_hashes(path: Path) -> List[str]:
|
|
@@ -338,7 +338,7 @@ def run(mode: str, out: Path) -> int:
|
|
|
338
338
|
|
|
339
339
|
|
|
340
340
|
def compare(gui_path: Path, headless_path: Path, out: Optional[Path]) -> int:
|
|
341
|
-
gui, headless = json.loads(gui_path.read_text()), json.loads(headless_path.read_text())
|
|
341
|
+
gui, headless = json.loads(gui_path.read_text(encoding="utf-8")), json.loads(headless_path.read_text(encoding="utf-8"))
|
|
342
342
|
if gui["metadata"]["headless"] is not False or headless["metadata"]["headless"] is not True:
|
|
343
343
|
print("REFUSE: mode metadata does not match the arguments.", file=sys.stderr)
|
|
344
344
|
return 1
|
package/scripts/render_stress.py
CHANGED
|
@@ -161,7 +161,7 @@ class CrashWatch:
|
|
|
161
161
|
self.offset = size # rotation/truncation resets the mark
|
|
162
162
|
return None
|
|
163
163
|
try:
|
|
164
|
-
with CRASH_ARCHIVE.open("r", errors="replace") as handle:
|
|
164
|
+
with CRASH_ARCHIVE.open("r", errors="replace", encoding="utf-8") as handle:
|
|
165
165
|
handle.seek(self.offset)
|
|
166
166
|
text = handle.read()
|
|
167
167
|
except OSError:
|
|
@@ -172,7 +172,7 @@ class CrashWatch:
|
|
|
172
172
|
|
|
173
173
|
def log_tail(lines: int = 80) -> str:
|
|
174
174
|
try:
|
|
175
|
-
content = RESOLVE_LOG.read_text(errors="replace").splitlines()
|
|
175
|
+
content = RESOLVE_LOG.read_text(errors="replace", encoding="utf-8").splitlines()
|
|
176
176
|
except OSError:
|
|
177
177
|
return ""
|
|
178
178
|
return "\n".join(content[-lines:])
|
|
@@ -56,7 +56,7 @@ def process_name(pid):
|
|
|
56
56
|
except Exception:
|
|
57
57
|
return ""
|
|
58
58
|
try: # Linux
|
|
59
|
-
with open("/proc/%d/comm" % pid) as handle:
|
|
59
|
+
with open("/proc/%d/comm" % pid, encoding="utf-8") as handle:
|
|
60
60
|
return handle.read().strip()
|
|
61
61
|
except (OSError, IOError):
|
|
62
62
|
pass
|
|
@@ -142,7 +142,7 @@ def main():
|
|
|
142
142
|
directory = os.path.dirname(REPORT_PATH)
|
|
143
143
|
if not os.path.isdir(directory):
|
|
144
144
|
os.makedirs(directory)
|
|
145
|
-
with open(REPORT_PATH, "w") as handle:
|
|
145
|
+
with open(REPORT_PATH, "w", encoding="utf-8") as handle:
|
|
146
146
|
json.dump(probe, handle, indent=2, sort_keys=True)
|
|
147
147
|
written = REPORT_PATH
|
|
148
148
|
except (OSError, IOError) as exc:
|
|
@@ -231,7 +231,7 @@ def main():
|
|
|
231
231
|
try:
|
|
232
232
|
if not os.path.isdir(REPORT_DIR):
|
|
233
233
|
os.makedirs(REPORT_DIR)
|
|
234
|
-
with open(path, "w") as handle:
|
|
234
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
235
235
|
json.dump(report, handle, indent=2, sort_keys=True)
|
|
236
236
|
written = path
|
|
237
237
|
except (OSError, IOError) as exc:
|
|
@@ -391,8 +391,8 @@ def _verdict(entry: Dict[str, Any], strategy: str) -> str:
|
|
|
391
391
|
|
|
392
392
|
|
|
393
393
|
def compare(gui_path: Path, headless_path: Path, out: Optional[Path]) -> int:
|
|
394
|
-
gui = json.loads(gui_path.read_text())
|
|
395
|
-
headless = json.loads(headless_path.read_text())
|
|
394
|
+
gui = json.loads(gui_path.read_text(encoding="utf-8"))
|
|
395
|
+
headless = json.loads(headless_path.read_text(encoding="utf-8"))
|
|
396
396
|
assert gui["metadata"]["headless"] is False, f"{gui_path} is not a GUI run"
|
|
397
397
|
assert headless["metadata"]["headless"] is True, f"{headless_path} is not a headless run"
|
|
398
398
|
|
|
@@ -385,7 +385,7 @@ def run(mode: str, out: Path) -> int:
|
|
|
385
385
|
|
|
386
386
|
|
|
387
387
|
def compare(gui_path: Path, headless_path: Path, out: Optional[Path]) -> int:
|
|
388
|
-
gui, headless = json.loads(gui_path.read_text()), json.loads(headless_path.read_text())
|
|
388
|
+
gui, headless = json.loads(gui_path.read_text(encoding="utf-8")), json.loads(headless_path.read_text(encoding="utf-8"))
|
|
389
389
|
assert gui["metadata"]["headless"] is False and headless["metadata"]["headless"] is True
|
|
390
390
|
lines = ["# Complex-cut round trip: what each format keeps", "",
|
|
391
391
|
"Two video tracks, audio, a per-item transform, clip colours, flags, item and "
|
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.136.
|
|
90
|
+
VERSION = "2.136.1"
|
|
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()}")
|