davinci-resolve-mcp 2.78.0 → 2.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/README.md +2 -1
- package/docs/guides/conforming-an-avid-aaf.md +91 -0
- package/docs/reference/api-limitations.md +40 -12
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/README.md +3 -1
- package/resolve-advanced/server/author-interchange.mjs +154 -16
- package/resolve-advanced/server/tools/editorial.mjs +5 -1
- package/resolve-advanced/vendor/conform-qc/packaging/emit-fcp7.js +155 -3
- package/resolve-advanced/vendor/conform-qc/packaging/index.js +5 -1
- package/resolve-advanced/vendor/conform-qc/test/packaging.test.js +152 -13
- package/src/granular/common.py +1 -1
- package/src/server.py +145 -37
- package/src/utils/api_truth.py +219 -29
- package/src/utils/page_lock.py +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,128 @@
|
|
|
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.79.0
|
|
6
|
+
|
|
7
|
+
Six silent failures in the conform path, found by conforming a real Avid picture turnover
|
|
8
|
+
and cross-checking every step against Resolve's own behaviour on 19.1.3. The common shape:
|
|
9
|
+
a call that reports success, or reports nothing at all, and leaves you with a timeline you
|
|
10
|
+
believe is right.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`convert_to_interchange` promised speed survives the DRT target. It does not.** The DRT
|
|
15
|
+
spec builder read four fields per event — start, duration, in, mediaFilePath — and never
|
|
16
|
+
read `speed` or `reverse`. One level down is why: the DRT clip schema has no per-clip speed
|
|
17
|
+
field at all, so there is nothing to write a retime into. A 200% clip and a reversed clip
|
|
18
|
+
both landed at 100% forward and the caller was told the conversion succeeded — inside the
|
|
19
|
+
module whose own description opens by naming "flattened retime → flag, skip-not-fake" as
|
|
20
|
+
its guarantee. Since carrying speed through was not available, the `drt` target now returns
|
|
21
|
+
**`flattened`**: one entry per retimed or reversed event, with its index, recIn, speed,
|
|
22
|
+
reverse, source and reason. It is always an array (empty when there are no retimes), so a
|
|
23
|
+
caller can tell "none to lose" from "this build is too old to report". `otio` (LinearTimeWarp)
|
|
24
|
+
and `edl` (M2) still carry retimes and are now named as the targets to use for a cut that
|
|
25
|
+
has them.
|
|
26
|
+
- **The vendored FCP7 emitter produced XML that Resolve 19.1.3 imports as nothing.** Three
|
|
27
|
+
defects, all failing the same silent way — a clean-looking file and a Resolve that does
|
|
28
|
+
nothing. (1) The file def had no `<timecode>` element; Resolve rejects the ENTIRE import
|
|
29
|
+
over one absent block, not the one clip. (2) `<pathurl>` got XML escaping where it needed
|
|
30
|
+
URL escaping, now percent-encoded per segment so `/` separators survive. (3) The sequence
|
|
31
|
+
had no `<rate>`, which bisection showed was the single element standing between "no
|
|
32
|
+
timeline" and a working import. Because a *wrong* timecode block is as fatal as a missing
|
|
33
|
+
one, the emitter never guesses: clips whose media timecode is unknown emit no block and are
|
|
34
|
+
reported through `fcp7TimecodeCoverage()`, which `buildPackage` attaches as `fcp7Timecode`
|
|
35
|
+
with an `importable` flag.
|
|
36
|
+
- **Importing a timeline into the never-saved `Untitled Project` silently no-opped.** Resolve
|
|
37
|
+
accepted the call, created nothing, and named no cause; the generic "Resolve created no
|
|
38
|
+
timeline" error that came back pointed at missing media and `sanitize_media` — the wrong
|
|
39
|
+
road, because the file is fine. `import_timeline_checked` now refuses before the call with
|
|
40
|
+
a remediation naming the project state. **Behaviour change:** a call that previously
|
|
41
|
+
returned a generic error now refuses earlier with a different message. The refusal is hard
|
|
42
|
+
and has no override flag, because the call cannot succeed either way.
|
|
43
|
+
- **OTIO authored by `convert_to_interchange` would not import into Resolve.** Filed as a
|
|
44
|
+
scripting-API limitation; it was not one. Exporting a timeline with `EXPORT_OTIO` and
|
|
45
|
+
feeding Resolve's own file back proved the API imports OTIO fine — what it refuses is a
|
|
46
|
+
document that is valid OTIO but not Resolve-shaped. The decisive requirement is that source
|
|
47
|
+
frames be **timecode-absolute**: media starting at 01:00:00:00 has an available range at
|
|
48
|
+
frame 86400, and 0-based source offsets put the clip outside it. The emitter now mirrors
|
|
49
|
+
Resolve's own shape (Clip.2 with a `media_references` map, `available_range`, bare
|
|
50
|
+
`target_url`, `global_start_time`) and takes each event's origin via `mediaStartTcFrame` or
|
|
51
|
+
an absolute `srcTcFrame`. Events whose origin had to be assumed come back in
|
|
52
|
+
**`mediaOriginAssumed`** rather than producing a file that imports as nothing.
|
|
53
|
+
- **`.otio` failures were misdiagnosed as missing media.** A `.otio` is JSON, so the
|
|
54
|
+
sanitize/relink pass cannot parse it and its advice never applied. `sanitize_media` is now
|
|
55
|
+
N/A for `.otio` as it already was for `.aaf`, and the no-timeline remediation names the
|
|
56
|
+
document's shape and the frame origin instead.
|
|
57
|
+
|
|
58
|
+
### Documentation
|
|
59
|
+
|
|
60
|
+
- `api_truth`: the retime entry now records that **the read side is as dead as the write
|
|
61
|
+
side** — `GetProperty('Speed'|'PlaybackSpeed'|'RetimeSpeed'|'ClipSpeed')` all return None on
|
|
62
|
+
19.1.3.7, and the keyless property dict carries no speed value at all. It also records that
|
|
63
|
+
the interchange route is closed (scalar speed filter ignored, `graphdict` dead in four
|
|
64
|
+
shapes, `reverse` dropped, in↔pproTicks inconsistency rejected in both orientations) and the
|
|
65
|
+
trap that makes it expensive to find: Resolve's own FCP7 export writes a degenerate Time
|
|
66
|
+
Remap, so `EXPORT_FCP_7_XML` cannot witness a speed. Each claim now says which Resolve
|
|
67
|
+
version it was measured on.
|
|
68
|
+
- `api_truth`: **`AppendToTimeline` does not overwrite an overlapping record** — the earlier
|
|
69
|
+
item wins and the later append is dropped, leaving a silently short timeline.
|
|
70
|
+
- `api_truth`: **placements from an errored append chunk are not durable across a save** — a
|
|
71
|
+
timeline verified at 573 items held 500 afterward. Every in-session read agrees with the
|
|
72
|
+
wrong number; only a post-save read catches it.
|
|
73
|
+
- `api_truth`: `CreateTimelineFromClips`' clipInfo has **no track field** while
|
|
74
|
+
`AppendToTimeline`'s does, with the empty-timeline + `add_track` + per-clip append workaround.
|
|
75
|
+
- `api_truth`: what Resolve's OTIO importer actually requires, and how to debug a refusal by
|
|
76
|
+
diffing against an `EXPORT_OTIO` file.
|
|
77
|
+
- **New guide — `docs/guides/conforming-an-avid-aaf.md`.** Three Resolve-native mechanisms
|
|
78
|
+
measured against one real turnover; all three fail. The most convincing-looking one fails
|
|
79
|
+
worst: "Link to source camera files" links 878 of 882 items with **only 144 correct — 734
|
|
80
|
+
wrong takes, 84%** — and then renders as a fully conformed timeline, with no offline media
|
|
81
|
+
and no warning. The guide explains why the matching lands on adjacent takes and says plainly
|
|
82
|
+
that the only witness which catches it is a frame comparison against a reference.
|
|
83
|
+
|
|
84
|
+
### Validation
|
|
85
|
+
|
|
86
|
+
- 2417 Python unit tests, 744 Node advanced tests, plus 8 fixture-free packaging tests, all
|
|
87
|
+
green. `packaging.test.js` also no longer fails to load on a fresh clone: it read a
|
|
88
|
+
git-ignored fixture at module scope, which took the fixture-free tests down with it.
|
|
89
|
+
- Live-validated on DaVinci Resolve Studio 19.1.3.7 with synthetic media: the fixed FCP7 XML
|
|
90
|
+
imports 2 items / 2 linked where the same file with its timecode blocks stripped imports
|
|
91
|
+
nothing; the authored `.otio` imports 3 items / 3 linked and re-reads frame-exact.
|
|
92
|
+
|
|
93
|
+
## What's New in v2.78.1
|
|
94
|
+
|
|
95
|
+
`Timeline.DeleteClips` is page-gated. Contributed by
|
|
96
|
+
[@billcarroll](https://github.com/billcarroll) in
|
|
97
|
+
[#117](https://github.com/samuelgursky/davinci-resolve-mcp/pull/117).
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
|
|
101
|
+
- **Every delete-capable action failed whenever the UI was left on another page.**
|
|
102
|
+
`Timeline.DeleteClips` deterministically returns False and deletes nothing off
|
|
103
|
+
the Edit page (verified: Fairlight), retries included — the readback-and-retry
|
|
104
|
+
helper from v2.71.1 handles a *flaky* False correctly, but retrying cannot clear
|
|
105
|
+
a page gate. Live verification on Studio 21.0: three identical retries against
|
|
106
|
+
132 valid, unlocked, present items all returned False with all 132 still on the
|
|
107
|
+
track; one `OpenPage("edit")` and the same call returned True and left 0. Track
|
|
108
|
+
lock and enable state were clear throughout — a page gate, not a lock.
|
|
109
|
+
`timeline` `delete_clips` / `move_clips` / `overwrite_range` / `lift_range` /
|
|
110
|
+
`apply_cuts` and `edit_engine execute_swap` now hold the Edit page for the call
|
|
111
|
+
and restore the caller's page after.
|
|
112
|
+
|
|
113
|
+
The guard is serialized through `page_lock`, alongside the existing Color-page
|
|
114
|
+
guard for thumbnail capture: Resolve has one globally-active page, so an
|
|
115
|
+
unserialized switch-work-restore races every other page-switching operation, and
|
|
116
|
+
under the threaded dispatch from v2.62.0 a concurrent thumbnail capture flipping
|
|
117
|
+
to Color mid-delete would land the delete on the wrong page — reintroducing this
|
|
118
|
+
same bug. `apply_cuts` holds the guard once around its loop rather than per cut,
|
|
119
|
+
which would otherwise cost 2N page flips for N cuts.
|
|
120
|
+
|
|
121
|
+
The `api_truth` entry is restructured into the two distinct failures now known to
|
|
122
|
+
share this call: wrong page (deterministic, mechanism identified, verified) and
|
|
123
|
+
flaky first attempt (one observation, cause not established — and the entry now
|
|
124
|
+
records that the page state at the time was not captured, so it cannot be ruled in
|
|
125
|
+
or out as the first failure in disguise).
|
|
126
|
+
|
|
5
127
|
## What's New in v2.78.0
|
|
6
128
|
|
|
7
129
|
The AAF probe reports where in the *source* an event actually lives, not where it
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
|
@@ -273,6 +273,7 @@ For method-by-method status, see [API Coverage and Test Results](docs/reference/
|
|
|
273
273
|
| [Media Analysis Guide](docs/guides/media-analysis-guide.md) | Source-safe FFprobe, FFmpeg, Whisper, sidecar, and analysis-root workflows |
|
|
274
274
|
| [Multicam Setup Helper Guide](docs/guides/multicam-setup-guide.md) | Stacked timeline prep, helper/API boundary, and Resolve UI conversion steps |
|
|
275
275
|
| [Editorial Decision Guide](docs/guides/editorial-decision-guide.md) | Project-owned editorial craft guidance for analysis and timeline decisions |
|
|
276
|
+
| [Conforming an Avid AAF](docs/guides/conforming-an-avid-aaf.md) | Why all three Resolve-native routes fail on a consolidated turnover, and which one is dangerous |
|
|
276
277
|
| [Color Decision Guide](docs/guides/color-decision-guide.md) | Project-owned color correction guidance and Resolve color API boundaries |
|
|
277
278
|
| [Contributing and Project Layout](docs/contributing.md) | Contribution workflow, platform support, security notes, repository structure |
|
|
278
279
|
| [Security Policy](SECURITY.md) | Local stdio trust boundary, tool metadata, confirmation guidance, reporting |
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Conforming a consolidated Avid AAF: what Resolve can and cannot do for you
|
|
2
|
+
|
|
3
|
+
You have an Avid picture turnover — an AAF plus consolidated media — and a folder
|
|
4
|
+
of camera originals. You want Resolve to relink the cut to those originals.
|
|
5
|
+
|
|
6
|
+
Three Resolve-native mechanisms look like they will do this. All three were
|
|
7
|
+
measured against one real 83-minute turnover (878 events, 882 timeline items,
|
|
8
|
+
multi-layer, consolidated media) on Resolve 19.1.3. **All three fail, and the one
|
|
9
|
+
that looks most convincing fails worst.**
|
|
10
|
+
|
|
11
|
+
This guide is about Resolve's own behaviour. It is reproducible by anyone with an
|
|
12
|
+
Avid AAF and a folder of camera originals.
|
|
13
|
+
|
|
14
|
+
## The short answer
|
|
15
|
+
|
|
16
|
+
| mechanism | what it does | verdict |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| API `ImportTimelineFromFile` | `importSourceClips: true` creates **no timeline** at all. `importSourceClips: false` creates 882 items with **zero** media-pool items | unusable as a conform |
|
|
19
|
+
| **Reconform from Bins** (timecode-only) | **678 / 882 linked** — and the links are the **wrong takes** | silently wrong |
|
|
20
|
+
| **UI Import AAF + "Link to source camera files"** | **878 / 882 linked, only 144 correct — 734 wrong takes (84%)** | 🚨 **dangerous** |
|
|
21
|
+
|
|
22
|
+
The camera-file link is the option whose name promises exactly what you want. It
|
|
23
|
+
is the one to avoid.
|
|
24
|
+
|
|
25
|
+
## Why the camera-file link is the dangerous option
|
|
26
|
+
|
|
27
|
+
It links almost everything — 878 of 882 items — and **84% of those links are the
|
|
28
|
+
wrong take.** Four consecutive cuts pointed at the same wrong file. A head slate
|
|
29
|
+
linked to unrelated archival footage.
|
|
30
|
+
|
|
31
|
+
And then it **renders as a fully conformed timeline.** There is no offline media,
|
|
32
|
+
no red frames, no error, no warning. Every signal a conform is normally checked
|
|
33
|
+
against says the job is done. You have to compare against a picture reference,
|
|
34
|
+
frame by frame, to discover that most of the cut is showing the wrong moment.
|
|
35
|
+
|
|
36
|
+
A conform that is 16% correct and looks 100% correct is worse than one that
|
|
37
|
+
obviously fails, because the failure survives review.
|
|
38
|
+
|
|
39
|
+
## Why the matching goes wrong
|
|
40
|
+
|
|
41
|
+
Two reasons, and neither is a bug you can configure away.
|
|
42
|
+
|
|
43
|
+
**Name matching is impossible.** A consolidated Avid turnover names its clips
|
|
44
|
+
with instance names like `<something>.new.01` — names that belong to Avid's own
|
|
45
|
+
media database and match no file on disk. There is nothing to match on.
|
|
46
|
+
|
|
47
|
+
**Timecode alone is ambiguous.** Fall back to timecode and you get adjacent-take
|
|
48
|
+
drift: the matcher lands on a *neighbouring take from the same roll* — one
|
|
49
|
+
camera-roll index away from the right one. Those takes were often shot minutes
|
|
50
|
+
apart, overlap in timecode, and are visually similar enough to pass a glance.
|
|
51
|
+
That is the shape of the 678 Reconform-from-Bins links and the 734 wrong
|
|
52
|
+
camera-file links alike.
|
|
53
|
+
|
|
54
|
+
Adjacent takes are the worst possible failure mode: close enough to look
|
|
55
|
+
plausible, wrong enough to be unusable.
|
|
56
|
+
|
|
57
|
+
## What to do instead
|
|
58
|
+
|
|
59
|
+
**Conform against the consolidated media the AAF actually references, not against
|
|
60
|
+
camera originals.** The AAF's source references describe the consolidated
|
|
61
|
+
fragments; those match. Relinking to camera originals is a *second*, separate
|
|
62
|
+
problem — one that needs the source position and source timecode of each event,
|
|
63
|
+
not just its take name.
|
|
64
|
+
|
|
65
|
+
If you must reach the camera originals, you need per-event evidence beyond the
|
|
66
|
+
AAF's own clip names:
|
|
67
|
+
|
|
68
|
+
- the **physical source position and timecode** of each cut (`aaf_probe.py` emits
|
|
69
|
+
`srcPos`, `srcTcFrame`, `srcTc`, `srcTcFps`, `srcTcDrop` per event — a
|
|
70
|
+
fragment-relative source offset is not a position in the take, which is its own
|
|
71
|
+
well-documented trap);
|
|
72
|
+
- the sequence **start timecode** (`startTimecode` / `startFrame`), because a cut
|
|
73
|
+
built at Resolve's 01:00:00:00 default when the AAF starts at 00:59:50:00 is ten
|
|
74
|
+
seconds out everywhere and only visible against a linked reference;
|
|
75
|
+
- an independent **picture reference** to verify against.
|
|
76
|
+
|
|
77
|
+
**Verify against a reference, always.** Whatever route you take, the only witness
|
|
78
|
+
that catches adjacent-take drift is a frame comparison against a reference render
|
|
79
|
+
or picture reference. Item counts, "linked" counts, and the absence of offline
|
|
80
|
+
media all agree with a conform that is 84% wrong. A witness derived from the same
|
|
81
|
+
state as the thing it is checking cannot contradict it.
|
|
82
|
+
|
|
83
|
+
## Related
|
|
84
|
+
|
|
85
|
+
- `docs/reference/api-limitations.md` — the scripting-API entries behind this,
|
|
86
|
+
including `MediaPool.ImportTimelineFromFile` and the `AppendToTimeline`
|
|
87
|
+
placement and durability limits you will hit if you build the timeline
|
|
88
|
+
yourself.
|
|
89
|
+
- `docs/guides/headless-edit-loop.md` — which interchange formats relink at all
|
|
90
|
+
when media has moved (DRT, OTIO and EDL do not).
|
|
91
|
+
</content>
|
|
@@ -12,7 +12,7 @@ that none exists).
|
|
|
12
12
|
|
|
13
13
|
**Verified on:** DaVinci Resolve Studio 21.0.2
|
|
14
14
|
|
|
15
|
-
**Totals:** 25 missing capabilities,
|
|
15
|
+
**Totals:** 25 missing capabilities, 28 bugs / unreliable behaviors.
|
|
16
16
|
|
|
17
17
|
The authoritative source is the runtime-queryable `api_truth` ledger
|
|
18
18
|
(`resolve_control api_truth "<query>"`); this document is generated from
|
|
@@ -117,9 +117,9 @@ 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.
|
|
121
|
-
- **Workaround / current handling:** Set clip speed/retime in the Resolve UI; no scripted equivalent exists.
|
|
122
|
-
- **Tags:** missing-method, timeline, retime, speed
|
|
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 INTERCHANGE ROUTE IS ALSO CLOSED — carrying a retime in through FCP7 XML does not work either, measured on 19.1.3 with three real files, clips at 100/200/50% on V1, each variant imported as its own timeline: (a) the importer IGNORES the scalar Time Remap speed filter and the clips arrive at 100%; (b) `graphdict` is dead in FOUR separate shapes — the full Premiere form (variablespeed=0 + speed + reverse=FALSE + frameblending + 4-keyframe graphdict + FCPCurve), graphdict alone with no <speed> param, a 100% REVERSE (reverse=TRUE plus a descending graphdict, the exact shape a real Premiere export carries), and variablespeed=1 + graphdict — 0 of 2 retimes landed in every one; (c) `reverse` did not survive either; (d) any <in>/<pproTicksIn> inconsistency is silently REJECTED, measured in BOTH orientations. The emitted maps were verified constant-slope against real Premiere exports before importing, and every imported clip came back consuming exactly its record span of source (a 200% clip emitted in 200 / out 296 was clamped to out 248 — 48 source frames over a 48-frame record span). 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.
|
|
121
|
+
- **Workaround / current handling:** Set clip speed/retime in the Resolve UI; no scripted equivalent exists, and no interchange route carries one in. Do NOT read speed back with GetProperty (None) or witness it via EXPORT_FCP_7_XML (degenerate). Read the clip's GEOMETRY instead — GetLeftOffset / GetRightOffset, which on the 200% clip read 200 / 248 and agreed with the export's in/out. Caveat worth stating: no positive control has been run — no clip KNOWN to be retimed has been read back through those two witnesses, because there is no scripting path to create one. So the geometry witness is the best available, not a proven one. Either way the interchange route is rejected: no retime was built, and none could have been verified.
|
|
122
|
+
- **Tags:** missing-method, timeline, retime, speed, interchange, silent-failure, unreliable-return
|
|
123
123
|
|
|
124
124
|
### Color node graph editing and primary grade values
|
|
125
125
|
|
|
@@ -281,7 +281,7 @@ values, or automation-hostile modal prompts.
|
|
|
281
281
|
- **Object:** `MediaPool`
|
|
282
282
|
- **Signature:** `(filePath, {importOptions}) -> Timeline`
|
|
283
283
|
- **Behavior:** Returns None — no error, no exception — when the requested `timelineName` already exists. Measured: importing one file three times with the same name succeeded once and returned None twice. An iterative loop that reuses a fixed name therefore works exactly once and then silently does nothing. DRT ignores importOptions entirely (timelineName, importSourceClips and sourceClipsFolders are all invalid for it): the timeline is named after the FILE, repeats auto-uniquify ('iter', 'iter 2', 'iter 3'), and because importSourceClips cannot be disabled each DRT import adds another copy of the source media to the pool. OTIO is the one format that will NOT relink from the pool — with importSourceClips=False its timeline rebuilds with correct structure and every item OFFLINE, in both GUI and headless.
|
|
284
|
-
- **Workaround / current handling:** For a repeatable loop use FCP7 XML or AAF with a UNIQUE timelineName per iteration plus importSourceClips=False and sourceClipsFolders=[root] — verified frame-exact over five consecutive imports with no media duplicated. Use DRT for one-shot hand-offs only. For OTIO, pass importSourceClips=True with a sourceClipsPath. See docs/guides/headless-edit-loop.md.
|
|
284
|
+
- **Workaround / current handling:** For a repeatable loop use FCP7 XML or AAF with a UNIQUE timelineName per iteration plus importSourceClips=False and sourceClipsFolders=[root] — verified frame-exact over five consecutive imports with no media duplicated. Use DRT for one-shot hand-offs only. For OTIO, pass importSourceClips=True with a sourceClipsPath. See docs/guides/headless-edit-loop.md. For a consolidated Avid AAF specifically, do NOT expect this call (or Reconform from Bins, or the UI's 'Link to source camera files') to conform it to camera originals — all three were measured against a real turnover and all three fail, the UI option linking 878 of 882 items with only 144 correct while rendering as a fully conformed timeline. See docs/guides/conforming-an-avid-aaf.md.
|
|
285
285
|
- **Tags:** timeline, import, interchange, silent-failure, conform
|
|
286
286
|
|
|
287
287
|
### Timeline.Export(EXPORT_FCPXML_1_10)
|
|
@@ -295,9 +295,33 @@ values, or automation-hostile modal prompts.
|
|
|
295
295
|
### MediaPool.CreateTimelineFromClips
|
|
296
296
|
|
|
297
297
|
- **Object:** `MediaPool`
|
|
298
|
-
- **Behavior:** Fails with a bare 'Failed to create timeline from clip_infos' — naming nothing actionable — when the Media Pool's CURRENT folder is not the folder holding the clips, even though every clip_id passed is valid and resolvable. Reported against Resolve Studio in PR #99.
|
|
299
|
-
- **Workaround / current handling:** Call MediaPool.SetCurrentFolder() to the clips' bin before creating the timeline (media_pool set_current_folder). Valid ids are not sufficient.
|
|
300
|
-
- **Tags:** media-pool, timeline, unhelpful-error
|
|
298
|
+
- **Behavior:** Fails with a bare 'Failed to create timeline from clip_infos' — naming nothing actionable — when the Media Pool's CURRENT folder is not the folder holding the clips, even though every clip_id passed is valid and resolvable. Reported against Resolve Studio in PR #99. SEPARATELY, its clipInfo HAS NO TRACK FIELD: there is no way to say which video or audio track a clip should land on, so this call cannot build a multi-track timeline. The asymmetry is the surprise — MediaPool.AppendToTimeline's clipInfo DOES take `trackIndex`, so the two clipInfo shapes are not the same shape, and code that works against one silently loses track assignment against the other.
|
|
299
|
+
- **Workaround / current handling:** Call MediaPool.SetCurrentFolder() to the clips' bin before creating the timeline (media_pool set_current_folder). Valid ids are not sufficient. For multi-track placement do not use this call at all: create an EMPTY timeline (media_pool create_timeline), add the tracks you need (timeline add_track), then place each clip with MediaPool.AppendToTimeline passing clipInfo `trackIndex` (media_pool append_to_timeline with track_index).
|
|
300
|
+
- **Tags:** media-pool, timeline, unhelpful-error, missing-method, conform
|
|
301
|
+
|
|
302
|
+
### MediaPool.AppendToTimeline (overlapping records — earlier item wins)
|
|
303
|
+
|
|
304
|
+
- **Object:** `MediaPool`
|
|
305
|
+
- **Signature:** `([{mediaPoolItem, startFrame, endFrame, recordFrame, trackIndex, mediaType}]) -> [TimelineItem]`
|
|
306
|
+
- **Behavior:** AppendToTimeline does NOT overwrite an overlapping record. When two clipInfos resolve to record ranges that overlap on the same track, the EARLIER item wins and the later append is dropped — measured while placing a real turnover. There is no overwrite edit mode to reach for either: AppendToTimeline is the only programmatic placement the API offers, so a consumer that assumes overwrite semantics gets a timeline that is silently SHORT by the number of colliding events, with no error to say which ones lost.
|
|
307
|
+
- **Workaround / current handling:** Resolve collisions BEFORE appending — the API will not do it for you. Cap each event's placed duration at the next event's recordFrame on the same track, or place the colliding events on separate tracks. Verify by comparing the timeline's item count against the number of clipInfos you sent, per track, and finish with detect_gaps_overlaps; a count that matches is the only evidence nothing was dropped.
|
|
308
|
+
- **Tags:** timeline, edit, silent-failure, conform, media-pool
|
|
309
|
+
|
|
310
|
+
### MediaPool.AppendToTimeline (errored-chunk placements are not durable across a save)
|
|
311
|
+
|
|
312
|
+
- **Object:** `MediaPool`
|
|
313
|
+
- **Signature:** `([clipInfos]) -> [TimelineItem]`
|
|
314
|
+
- **Behavior:** Placements made by an append call whose response ERRORED are not durable: they appear in the timeline, every in-session read agrees they are there, and THE SAVE DISCARDS THEM. Measured on a real turnover — a timeline verified at 573 items immediately after construction held 500 after the save, a loss of exactly one errored append chunk's worth. The API reported the failure, the items appeared anyway, and nothing between construction and the save disagreed with the wrong number. This is the dangerous shape: the witness is derived from the same unsaved state as the thing it is checking, so it cannot contradict it. Only a POST-SAVE read can.
|
|
315
|
+
- **Workaround / current handling:** Treat the save as the verification boundary, not the end of the job. Save, RE-READ the timeline's item count from the saved project, and re-append whatever is missing (bounded — two rounds is enough in practice); only then report success. A timeline that is still short after that, or that will not save, is a FAILURE — report it as one rather than returning the in-session count. Never claim a construction succeeded on a pre-save read alone, and treat any errored append chunk as suspect even when its items are visibly present.
|
|
316
|
+
- **Tags:** timeline, edit, silent-failure, data-loss, conform, unreliable-return, media-pool
|
|
317
|
+
|
|
318
|
+
### MediaPool.ImportTimelineFromFile (.otio document shape)
|
|
319
|
+
|
|
320
|
+
- **Object:** `MediaPool`
|
|
321
|
+
- **Signature:** `(filePath, {importOptions}) -> Timeline`
|
|
322
|
+
- **Behavior:** Resolve DOES import OTIO through the scripting API — but only a Resolve-shaped document, and it rejects anything else by creating NO timeline and returning None, with no error naming a cause. Established on 19.1.3.7 by exporting a timeline with Timeline.Export(..., EXPORT_OTIO) and feeding Resolve's own file straight back: it re-imports cleanly (3 items, 3 linked), while a valid hand-authored OTIO of the same cut, same project, same session, same three online media files, produced nothing. So a refusal is a SHAPE problem, not a media problem. The requirement that actually decides it is the SOURCE FRAME ORIGIN: a clip's source_range must be expressed against the media's own timecode range. Media carrying an embedded start TC of 01:00:00:00 gets an available_range starting at frame 86400, and the clip's source_range must start there too — 0-based source offsets, the natural reading of 'source in point', put the clip outside the media's real range and the import dies. Isolated by bisection: 0-based fails and absolute succeeds whether or not the stack/track source_range is null. Resolve also writes (and expects) Clip.2 with a `media_references` MAP plus `active_media_reference_key`, not Clip.1 with a singular `media_reference`; an `available_range` on each reference; a BARE path in `target_url`, not a file:// URL; `enabled` and `metadata` throughout; `global_start_time`; and track names in its own form ('Video 1').
|
|
323
|
+
- **Workaround / current handling:** Author OTIO for Resolve by mirroring what Resolve itself exports, and give every event its media timecode origin. editorial.convert_to_interchange (target 'otio') does this and reports any event whose origin had to be assumed in `mediaOriginAssumed` — a non-empty list means the file will only import if that media really starts at 00:00:00:00. To debug a refusal, export any timeline with EXPORT_OTIO and diff your document against it; do NOT chase missing media or reach for sanitize_media, which cannot even parse a .otio (it is JSON, not XML).
|
|
324
|
+
- **Tags:** timeline, import, interchange, otio, silent-failure, conform
|
|
301
325
|
|
|
302
326
|
### TimelineItem.AddFusionComp / LoadFusionCompByName
|
|
303
327
|
|
|
@@ -374,13 +398,17 @@ values, or automation-hostile modal prompts.
|
|
|
374
398
|
- **Reference:** [issue #77](https://github.com/samuelgursky/davinci-resolve-mcp/issues/77)
|
|
375
399
|
- **Tags:** unreliable-return, silent-failure, metadata, reel-name
|
|
376
400
|
|
|
377
|
-
### Timeline.DeleteClips (flaky first attempt)
|
|
401
|
+
### Timeline.DeleteClips (requires the Edit page; flaky first attempt)
|
|
378
402
|
|
|
379
403
|
- **Object:** `Timeline`
|
|
380
404
|
- **Signature:** `([TimelineItem], ripple) -> bool`
|
|
381
|
-
- **Behavior:**
|
|
382
|
-
|
|
383
|
-
-
|
|
405
|
+
- **Behavior:** Two distinct failures share this call.
|
|
406
|
+
|
|
407
|
+
**1. Wrong page (deterministic, has a mechanism).** With the UI on the Fairlight page, DeleteClips returns False and deletes nothing, no matter how many times it is retried. Verified 2026-08-04 on Studio 21.0: three identical retries against 132 valid, unlocked, present TimelineItems all returned False with all 132 still on the track; a single `Resolve.OpenPage("edit")` followed by the same call returned True and left 0 items. Track lock and enable state were confirmed clear before and after, so this is a page gate, not a lock. This is the case the previous entry's falsification condition anticipated — a retry seen to fail repeatedly — and revisiting it found the mechanism.
|
|
408
|
+
|
|
409
|
+
**2. Flaky first attempt (one observation, no mechanism).** Independently of the page, the call has been seen once to return False with every item still present, where an identical immediate retry succeeded (Studio 21.0, cut-video edit session). Whether the UI was on the Edit page at the time was not recorded, so it cannot be ruled in or out as failure 1 in disguise. Do NOT read this as the ProjectManager.DeleteProject shape: that one has an identified mechanism (the project being, or recently having been, current) that retrying does not clear. One observation is still not a mechanism; if an immediate retry is ever seen to fail repeatedly while the UI is confirmed on the Edit page, this sub-entry needs revisiting.
|
|
410
|
+
- **Workaround / current handling:** Open the Edit page first (`Resolve.OpenPage("edit")`) — a caller that deletes clips from a script must not assume the user left the UI on Edit, and a Fairlight or Color session is a completely ordinary place for them to be. Then treat a False return as advisory: re-list the track and check whether the items are actually gone; if still present, retry the identical call once before failing. A readback that raised, enumerated nothing, or covered items whose unique ID cannot be read is UNKNOWN, not gone — never report an unverifiable delete as success, and do not spend a second destructive call on an outcome you equally cannot read.
|
|
411
|
+
- **Tags:** unreliable-return, flaky, silent-failure, page-dependent, timeline, edit
|
|
384
412
|
|
|
385
413
|
### MediaPool.AppendToTimeline with mixed-fps sources (duration floor)
|
|
386
414
|
|
package/install.py
CHANGED
|
@@ -36,7 +36,7 @@ from src.utils.update_check import (
|
|
|
36
36
|
|
|
37
37
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
VERSION = "2.
|
|
39
|
+
VERSION = "2.79.0"
|
|
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
|
@@ -98,7 +98,9 @@ Each dispatches on an `action`. Highlights:
|
|
|
98
98
|
**.prproj via gunzip+XML** — pass the file PATH for the binary ones), `list_sequences` (one picker entry
|
|
99
99
|
point across xml/edl/otio/drt/drp/aaf/prproj), `convert_to_interchange` (author OTIO/EDL/DRT that Resolve
|
|
100
100
|
imports, from events or a parsed source — **the .prproj→Resolve conform bridge**, no Premiere needed;
|
|
101
|
-
editorial timing/cuts/transitions
|
|
101
|
+
editorial timing/cuts/transitions carry, per-clip effects/color do not; speed/reverse carry on
|
|
102
|
+
the `otio` and `edl` targets only — `drt` has no per-clip speed field and reports every retime it
|
|
103
|
+
flattens in `flattened`),
|
|
102
104
|
`turnover_changelist` (moved/retimed/replaced/new/gone + timing silent-lie guards), `conform_manifest`,
|
|
103
105
|
`marker_roundtrip`.
|
|
104
106
|
- **`provenance`** — provenance / audit: `gallery_lineage`, `grade_provenance` ("why is this graded this
|
|
@@ -28,12 +28,71 @@ const byTrack = (events) => {
|
|
|
28
28
|
return groups;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Events whose media timecode ORIGIN had to be assumed as 0 when authoring OTIO. Resolve
|
|
33
|
+
* measures a clip's source_range against the media's real timecode range, so an event with
|
|
34
|
+
* no origin only imports if its media genuinely starts at 00:00:00:00 — otherwise the file
|
|
35
|
+
* looks fine and Resolve creates no timeline. Supply `mediaStartTcFrame` (or an absolute
|
|
36
|
+
* `srcTcFrame`) per event to clear it. Always an array, empty when every event carried one.
|
|
37
|
+
*/
|
|
38
|
+
export function otioMediaOriginAssumed(events) {
|
|
39
|
+
const out = [];
|
|
40
|
+
(events || []).forEach((e, index) => {
|
|
41
|
+
if (e.srcTcFrame != null || e.mediaStartTcFrame != null || e.startTcFrame != null) return;
|
|
42
|
+
out.push({
|
|
43
|
+
index,
|
|
44
|
+
recIn: e.recIn ?? 0,
|
|
45
|
+
srcIn: e.srcIn ?? 0,
|
|
46
|
+
source: e.source || '',
|
|
47
|
+
reason: 'no media timecode origin — assumed 00:00:00:00; Resolve rejects the import if the media starts elsewhere',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rt = (value, rate) => ({ OTIO_SCHEMA: 'RationalTime.1', rate: Number(rate), value: Number(value) });
|
|
54
|
+
const tr = (start, duration, rate) => ({ OTIO_SCHEMA: 'TimeRange.1', duration: rt(duration, rate), start_time: rt(start, rate) });
|
|
55
|
+
const basename = (p) => String(p || '').split('/').pop() || 'UNKNOWN';
|
|
56
|
+
|
|
31
57
|
/**
|
|
32
58
|
* Build an OTIO timeline doc (plain object) from normalized events. Inserts gaps so record
|
|
33
59
|
* positions are exact; emits LinearTimeWarp for speed/reverse and Transition items where present.
|
|
60
|
+
*
|
|
61
|
+
* The emitted shape mirrors what Resolve's own EXPORT_OTIO writes, because Resolve's importer
|
|
62
|
+
* is far pickier than the OTIO spec. Measured on 19.1.3: a Resolve-authored .otio re-imports
|
|
63
|
+
* through MediaPool.ImportTimelineFromFile (3 items, 3 linked) while the shape this function
|
|
64
|
+
* used to emit produced NO TIMELINE — same project, same session, same three online media
|
|
65
|
+
* files. So the scripting API imports OTIO fine; what it rejects is a document that is valid
|
|
66
|
+
* OTIO but not Resolve-shaped. The parts that matter:
|
|
67
|
+
*
|
|
68
|
+
* - `Clip.2` with a `media_references` MAP + `active_media_reference_key`, not `Clip.1`
|
|
69
|
+
* with a singular `media_reference`. This is the one that most looks like a free choice
|
|
70
|
+
* and is not.
|
|
71
|
+
* - `available_range` on the external reference, and a clip `name` that is the media
|
|
72
|
+
* BASENAME rather than its full path.
|
|
73
|
+
* - SOURCE FRAMES ARE TIMECODE-ABSOLUTE. This is the one that decides the import. The
|
|
74
|
+
* media used to measure this carries an embedded start TC of 01:00:00:00, so Resolve
|
|
75
|
+
* writes its `available_range` starting at frame 86400 and the clip's `source_range`
|
|
76
|
+
* at 86400 too. Emitting the same cut with 0-based source offsets — the natural
|
|
77
|
+
* reading of "source in point" — produced NO TIMELINE, because frame 0 is outside the
|
|
78
|
+
* media's real range. Bisected: 0-based fails and absolute succeeds whether or not the
|
|
79
|
+
* stack/track `source_range` is null, so it is the frame origin that matters and not
|
|
80
|
+
* the surrounding shape. Same lesson as srcIn being fragment-relative on consolidated
|
|
81
|
+
* media: a source position means nothing without the origin it is measured from.
|
|
82
|
+
* - `enabled: true` on stack, tracks and clips; `metadata` objects present (Resolve stamps
|
|
83
|
+
* its own `Resolve_OTIO` block and reads the tracks' back).
|
|
84
|
+
* - `global_start_time` on the timeline, and track names in Resolve's own form ("Video 1").
|
|
85
|
+
*
|
|
86
|
+
* Give the media's TC origin per event as `mediaStartTcFrame` (or an already-absolute
|
|
87
|
+
* `srcTcFrame`). Without it this falls back to a 0 origin, which only imports when the media
|
|
88
|
+
* really does start at 00:00:00:00 — so the events that had to be assumed are reported back
|
|
89
|
+
* in `mediaOriginAssumed` rather than silently producing a file that imports as nothing.
|
|
90
|
+
*
|
|
91
|
+
* Handy inverse: this repo's parseOTIO still reads what we emit, so the bridge round-trips.
|
|
34
92
|
*/
|
|
35
93
|
export function eventsToOTIO(events, opts = {}) {
|
|
36
94
|
const fps = opts.fps || events.find((e) => e.fps)?.fps || 24;
|
|
95
|
+
const startFrame = opts.startFrame ?? opts.globalStartFrame ?? 0;
|
|
37
96
|
const groups = byTrack(events);
|
|
38
97
|
const tracks = [];
|
|
39
98
|
for (const [kind, list] of [
|
|
@@ -48,40 +107,88 @@ export function eventsToOTIO(events, opts = {}) {
|
|
|
48
107
|
if (recIn > rec) {
|
|
49
108
|
children.push({
|
|
50
109
|
OTIO_SCHEMA: 'Gap.1',
|
|
51
|
-
|
|
110
|
+
metadata: {},
|
|
111
|
+
source_range: { OTIO_SCHEMA: 'TimeRange.1', duration: rt(recIn - rec, fps), start_time: rt(0, fps) },
|
|
112
|
+
effects: [],
|
|
113
|
+
markers: [],
|
|
114
|
+
enabled: true,
|
|
52
115
|
});
|
|
53
116
|
rec = recIn;
|
|
54
117
|
}
|
|
55
118
|
const recDur = (e.recOut ?? recIn) - recIn;
|
|
119
|
+
const clipFps = e.fps || fps;
|
|
120
|
+
const srcIn = e.srcIn ?? 0;
|
|
121
|
+
// The media's timecode origin. srcTcFrame (already absolute) wins; otherwise the
|
|
122
|
+
// media start TC plus the in-point. A 0 origin is an ASSUMPTION, and it is recorded.
|
|
123
|
+
const mediaStart = e.mediaStartTcFrame ?? e.startTcFrame ?? null;
|
|
124
|
+
const srcStart = e.srcTcFrame ?? (mediaStart ?? 0) + srcIn;
|
|
125
|
+
const availStart = e.srcTcFrame != null ? e.srcTcFrame - srcIn : (mediaStart ?? 0);
|
|
56
126
|
const clip = {
|
|
57
|
-
OTIO_SCHEMA: 'Clip.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
start_time: { OTIO_SCHEMA: 'RationalTime.1', value: e.srcIn ?? 0, rate: e.fps || fps },
|
|
62
|
-
duration: { OTIO_SCHEMA: 'RationalTime.1', value: recDur, rate: e.fps || fps },
|
|
63
|
-
},
|
|
64
|
-
media_reference: { OTIO_SCHEMA: 'ExternalReference.1', target_url: e.source || '' },
|
|
127
|
+
OTIO_SCHEMA: 'Clip.2',
|
|
128
|
+
metadata: {},
|
|
129
|
+
name: basename(e.source),
|
|
130
|
+
source_range: tr(srcStart, recDur, clipFps),
|
|
65
131
|
effects: [],
|
|
66
132
|
markers: [],
|
|
133
|
+
enabled: true,
|
|
134
|
+
media_references: {
|
|
135
|
+
DEFAULT_MEDIA: {
|
|
136
|
+
OTIO_SCHEMA: 'ExternalReference.1',
|
|
137
|
+
metadata: {},
|
|
138
|
+
name: basename(e.source),
|
|
139
|
+
// Resolve writes a BARE path here, not a file:// URL.
|
|
140
|
+
target_url: e.source || '',
|
|
141
|
+
available_range: tr(availStart, e.mediaDuration ?? e.srcAvailDuration ?? Math.max(recDur, srcIn + recDur), clipFps),
|
|
142
|
+
available_image_bounds: null,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
active_media_reference_key: 'DEFAULT_MEDIA',
|
|
67
146
|
};
|
|
68
147
|
if ((e.speed ?? 100) !== 100 || e.reverse) {
|
|
69
|
-
clip.effects.push({ OTIO_SCHEMA: 'LinearTimeWarp.1', name: 'Speed', time_scalar: (e.reverse ? -1 : 1) * ((e.speed ?? 100) / 100) });
|
|
148
|
+
clip.effects.push({ OTIO_SCHEMA: 'LinearTimeWarp.1', name: 'Speed', metadata: {}, effect_name: 'LinearTimeWarp', time_scalar: (e.reverse ? -1 : 1) * ((e.speed ?? 100) / 100) });
|
|
70
149
|
}
|
|
71
150
|
if (e.transition) {
|
|
72
151
|
children.push({
|
|
73
152
|
OTIO_SCHEMA: 'Transition.1',
|
|
153
|
+
metadata: {},
|
|
154
|
+
name: 'Cross Dissolve',
|
|
74
155
|
transition_type: 'SMPTE_Dissolve',
|
|
75
|
-
in_offset:
|
|
76
|
-
out_offset:
|
|
156
|
+
in_offset: rt(Math.ceil((e.transition.duration || 0) / 2), fps),
|
|
157
|
+
out_offset: rt(Math.floor((e.transition.duration || 0) / 2), fps),
|
|
77
158
|
});
|
|
78
159
|
}
|
|
79
160
|
children.push(clip);
|
|
80
161
|
rec = recIn + recDur;
|
|
81
162
|
}
|
|
82
|
-
tracks.push({
|
|
163
|
+
tracks.push({
|
|
164
|
+
OTIO_SCHEMA: 'Track.1',
|
|
165
|
+
metadata: {},
|
|
166
|
+
name: `${kind} 1`,
|
|
167
|
+
source_range: tr(0, rec, fps),
|
|
168
|
+
effects: [],
|
|
169
|
+
markers: [],
|
|
170
|
+
enabled: true,
|
|
171
|
+
children,
|
|
172
|
+
kind,
|
|
173
|
+
});
|
|
83
174
|
}
|
|
84
|
-
|
|
175
|
+
const stackDur = tracks.reduce((m, t) => Math.max(m, t.source_range.duration.value), 0);
|
|
176
|
+
return {
|
|
177
|
+
OTIO_SCHEMA: 'Timeline.1',
|
|
178
|
+
metadata: {},
|
|
179
|
+
name: opts.name || 'Conformed',
|
|
180
|
+
global_start_time: rt(startFrame, fps),
|
|
181
|
+
tracks: {
|
|
182
|
+
OTIO_SCHEMA: 'Stack.1',
|
|
183
|
+
metadata: {},
|
|
184
|
+
name: 'tracks',
|
|
185
|
+
source_range: tr(0, stackDur, fps),
|
|
186
|
+
effects: [],
|
|
187
|
+
markers: [],
|
|
188
|
+
enabled: true,
|
|
189
|
+
children: tracks,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
85
192
|
}
|
|
86
193
|
|
|
87
194
|
/** Build a CMX3600 EDL string (cuts + M2 speed). Video events only, per EDL convention. */
|
|
@@ -108,6 +215,35 @@ export function eventsToEDL(events, opts = {}) {
|
|
|
108
215
|
return lines.join('\n') + '\n';
|
|
109
216
|
}
|
|
110
217
|
|
|
218
|
+
const isRetimed = (e) => (e.speed ?? 100) !== 100 || Boolean(e.reverse);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Which events lose their retime on the DRT target. The DRT clip schema
|
|
222
|
+
* (vendor/drp-format/seq-container-builder.js) carries start / duration / in /
|
|
223
|
+
* mediaFilePath / mediaStartTime / mediaFrameRate and nothing else — there is no per-clip
|
|
224
|
+
* speed field to write a retime into, so a retimed event lands at 100% forward. OTIO
|
|
225
|
+
* (LinearTimeWarp) and EDL (M2) do carry it; only DRT flattens.
|
|
226
|
+
*
|
|
227
|
+
* Reporting it is this cluster's skip-not-fake contract: a flattened retime that nobody is
|
|
228
|
+
* told about is a timeline the caller believes is conformed and is not. `index` is the
|
|
229
|
+
* event's position in the ORIGINAL events array, so a caller can map straight back.
|
|
230
|
+
*/
|
|
231
|
+
export function drtFlattenedRetimes(events) {
|
|
232
|
+
const out = [];
|
|
233
|
+
(events || []).forEach((e, index) => {
|
|
234
|
+
if (!isRetimed(e)) return;
|
|
235
|
+
out.push({
|
|
236
|
+
index,
|
|
237
|
+
recIn: e.recIn ?? 0,
|
|
238
|
+
speed: e.speed ?? 100,
|
|
239
|
+
reverse: Boolean(e.reverse),
|
|
240
|
+
source: e.source || '',
|
|
241
|
+
reason: 'DRT clip schema has no per-clip speed field — retime flattened to 100% forward',
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
111
247
|
/** Build a buildDRT spec (Resolve-native .drt) from normalized events. */
|
|
112
248
|
export function eventsToDrtSpec(events, opts = {}) {
|
|
113
249
|
const fps = opts.fps || events.find((e) => e.fps)?.fps || 24;
|
|
@@ -133,12 +269,14 @@ export function eventsToDrtSpec(events, opts = {}) {
|
|
|
133
269
|
/**
|
|
134
270
|
* Author `events` into `target` interchange. Returns { target, content?, spec?, bytes? }.
|
|
135
271
|
* For 'drt', when outputPath is given the .drt bytes are written; otherwise the spec is returned.
|
|
272
|
+
* The 'drt' target additionally returns `flattened` — ALWAYS an array, empty when the cut
|
|
273
|
+
* carries no retimes, so a caller can tell "none to lose" from "this build is too old to say".
|
|
136
274
|
*/
|
|
137
275
|
export async function authorInterchange(events, target, opts = {}) {
|
|
138
276
|
const t = String(target || 'otio').toLowerCase();
|
|
139
277
|
if (t === 'otio') {
|
|
140
278
|
const doc = eventsToOTIO(events, opts);
|
|
141
|
-
return { target: 'otio', content: JSON.stringify(doc, null, 2), doc };
|
|
279
|
+
return { target: 'otio', content: JSON.stringify(doc, null, 2), doc, mediaOriginAssumed: otioMediaOriginAssumed(events) };
|
|
142
280
|
}
|
|
143
281
|
if (t === 'edl') {
|
|
144
282
|
return { target: 'edl', content: eventsToEDL(events, opts) };
|
|
@@ -146,7 +284,7 @@ export async function authorInterchange(events, target, opts = {}) {
|
|
|
146
284
|
if (t === 'drt') {
|
|
147
285
|
const spec = eventsToDrtSpec(events, opts);
|
|
148
286
|
const buf = await drt().buildDRT(spec);
|
|
149
|
-
return { target: 'drt', spec, buffer: buf, bytes: buf.length };
|
|
287
|
+
return { target: 'drt', spec, buffer: buf, bytes: buf.length, flattened: drtFlattenedRetimes(events) };
|
|
150
288
|
}
|
|
151
289
|
throw new Error(`authorInterchange: unknown target '${target}' (otio|edl|drt)`);
|
|
152
290
|
}
|
|
@@ -82,7 +82,7 @@ const markerSchema = z.object({
|
|
|
82
82
|
export const editorialTool = {
|
|
83
83
|
name: 'editorial',
|
|
84
84
|
description:
|
|
85
|
-
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events; for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions
|
|
85
|
+
'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events; for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — the DRT clip schema has no per-clip speed field, so target `drt` flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one; `flattened` is always present on `drt`, empty when there were none), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base), marker_roundtrip (markers with provenance tags). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
|
|
86
86
|
async handler({ action, args }) {
|
|
87
87
|
if (action === 'parse_interchange') {
|
|
88
88
|
const p = parseSchema.parse(args);
|
|
@@ -125,6 +125,10 @@ export const editorialTool = {
|
|
|
125
125
|
eventCount: events.length,
|
|
126
126
|
...(written || { content: authored.content }),
|
|
127
127
|
...(authored.spec ? { spec: authored.spec } : {}),
|
|
128
|
+
// skip-not-fake: the DRT clip schema has no per-clip speed field, so retimes cannot
|
|
129
|
+
// ride into a .drt. Name every event that lost one rather than returning a timeline
|
|
130
|
+
// the caller believes is conformed. Always present on 'drt' (empty = none to lose).
|
|
131
|
+
...(authored.flattened ? { flattened: authored.flattened, flattenedCount: authored.flattened.length } : {}),
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
if (action === 'turnover_changelist') {
|