davinci-resolve-mcp 2.92.0 → 2.93.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 +62 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/reference/api-limitations.md +3 -3
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +93 -37
- package/src/utils/api_truth.py +57 -20
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,68 @@
|
|
|
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.93.0
|
|
6
|
+
|
|
7
|
+
`source_end` is a source frame again, and the guidance v2.91.0 shipped about WAV
|
|
8
|
+
frame rates was wrong. Both found by measuring rather than reasoning, live on
|
|
9
|
+
Studio 19.1.3.7 with synthetic media in a disposable project.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`source_end` was `source_start + duration`, and that duration is a TIMELINE
|
|
14
|
+
duration.** So the sum was unit-mixed the moment the media and timeline rates
|
|
15
|
+
differed. Measured against the `endFrame` actually sent, it overshot by
|
|
16
|
+
**+24, +26, +108 and +149 frames** on a WAV counting at 24 in a 29.97 timeline
|
|
17
|
+
— and `extract_source_frame_ranges` builds pull ranges out of it, reporting
|
|
18
|
+
widths of 543 and 749 for clips that consume 435 and 600 source frames.
|
|
19
|
+
The direction was safe (a longer pull); the number was wrong, and anything
|
|
20
|
+
sizing an archive, a consolidation or a pull list inherited it.
|
|
21
|
+
|
|
22
|
+
It is now `round(GetSourceEndTime × source_fps)`. Seconds carry no frame-rate
|
|
23
|
+
assumption, so the product is a source frame by construction, and the field
|
|
24
|
+
stays **EXCLUSIVE** exactly as every caller already read it. Over 8 items in
|
|
25
|
+
both regimes it equals the `endFrame` sent every time, and it reproduces the
|
|
26
|
+
old value wherever the old value was already right — so no matched-rate
|
|
27
|
+
consumer moves, which the full suite confirms without a single existing
|
|
28
|
+
expectation changing. It falls back to the old sum only when the second-reader
|
|
29
|
+
or the rate is unreadable.
|
|
30
|
+
|
|
31
|
+
- **`GetSourceEndFrame` was the obvious candidate and it is not usable raw.**
|
|
32
|
+
Measured over 12 items, it is **exclusive when the source and timeline rates
|
|
33
|
+
match and inclusive when they differ** — off by one in exactly the case a
|
|
34
|
+
caller reaches for it. Not a media-type split either: the same WAV imported at
|
|
35
|
+
29.97 into a 29.97 timeline reads exclusive like video, and only the mismatch
|
|
36
|
+
flips it. Building on it would have meant branching on a rate comparison the
|
|
37
|
+
code would first have to reconstruct.
|
|
38
|
+
|
|
39
|
+
### Corrected
|
|
40
|
+
|
|
41
|
+
- **A WAV is not 24 fps.** v2.91.0's ledger entry said a WAV "carries no frame
|
|
42
|
+
rate, so Resolve falls back to 24" and told callers to treat one as 24 fps.
|
|
43
|
+
That is wrong. A WAV takes the **project's `timelineFrameRate` at import** and
|
|
44
|
+
freezes it: one 400.000 s file imported at 24 reads `FPS 24.0` /
|
|
45
|
+
`Duration 00:06:40:00`, the same file imported at 29.97 reads `29.97` /
|
|
46
|
+
`00:06:39:18`, and changing the project rate after import leaves the clip on
|
|
47
|
+
its original rate. So the trap is "the project moved after import", not "audio
|
|
48
|
+
is always 24" — and a WAV imported at 29.97 has no mismatch at all. Anyone who
|
|
49
|
+
followed the old advice on such a file would have converted a correct number
|
|
50
|
+
into a wrong one. Corrected in the ledger, the `resolve-rough-cut` traps table,
|
|
51
|
+
the `probe_timeline_structure` and `create_variant_from_ranges` action help,
|
|
52
|
+
and the helper docstrings: every site now says read the rate, never assume it.
|
|
53
|
+
|
|
54
|
+
### Validation
|
|
55
|
+
|
|
56
|
+
- Suite: 2628 passed, 1 skipped (7 new cases covering both regimes, the rounding
|
|
57
|
+
boundaries, and each fallback). Static checks and drift guards clean.
|
|
58
|
+
- Live on **Studio 19.1.3.7**, ffmpeg-generated synthetic media, disposable
|
|
59
|
+
project deleted after each run. The first measurement pass was discarded and
|
|
60
|
+
redone: it zipped `AppendToTimeline`'s return against the request list, two
|
|
61
|
+
entries came back unreadable, and the resulting misalignment looked exactly
|
|
62
|
+
like reader noise. Every number above comes from appending one range at a time.
|
|
63
|
+
- **Not tested here:** Resolve 21.x, and retimed clips — there is no clip-speed
|
|
64
|
+
API to build one from, so whether the new route also fixes the retime case
|
|
65
|
+
(where the old arithmetic is wrong for the same reason) is untested.
|
|
66
|
+
|
|
5
67
|
## What's New in v2.92.0
|
|
6
68
|
|
|
7
69
|
Two corrections to advice this project was giving confidently and wrongly, both
|
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.93.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -382,12 +382,12 @@ values, or automation-hostile modal prompts.
|
|
|
382
382
|
- **Workaround / current handling:** Verify source-side placement with GetLeftOffset, which is exact. Treat GetSourceStartFrame as approximate, and never diff it against a sent startFrame to decide whether a clip landed right. Scope: placement at 100% speed. On a retimed clip the two read DIFFERENT domains — GetLeftOffset is warped (position / speed), GetSourceStartFrame is true source — see the retime entry's witness calibration before comparing them.
|
|
383
383
|
- **Tags:** off-by-one, unreliable-return, timeline, conform, verify
|
|
384
384
|
|
|
385
|
-
### TimelineItem.GetSourceStartFrame on an AUDIO item (media-rate frames;
|
|
385
|
+
### TimelineItem.GetSourceStartFrame on an AUDIO item (media-rate frames; a WAV freezes the PROJECT rate at import)
|
|
386
386
|
|
|
387
387
|
- **Object:** `TimelineItem`
|
|
388
388
|
- **Signature:** `() -> int # source frame, counted in the MEDIA's frame rate`
|
|
389
|
-
- **Behavior:** The value is counted in the source MEDIA's own frame rate, not the timeline's
|
|
390
|
-
- **Workaround / current handling:** Convert an audio item's source frames with the MEDIA's rate, never the timeline's: seconds = source_start / media_fps
|
|
389
|
+
- **Behavior:** The value is counted in the source MEDIA's own frame rate, not the timeline's. CORRECTION (2026-08-10, Studio 19.1.3.7): an earlier version of this entry said a WAV 'carries no frame rate, so Resolve falls back to 24 fps'. That is WRONG, and 24 is not a constant to rely on. A WAV takes the PROJECT's timelineFrameRate AT IMPORT and freezes it. Measured with one 400.000 s 48 kHz WAV imported into three project states: project at 24 -> clip FPS 24.0, Duration 00:06:40:00 (9600 frames = 400 s); project at 29.97 -> clip FPS 29.97, Duration 00:06:39:18 (11988 frames = 400 s); and changing the project rate to 29.97 AFTER import left the clip reading 24.0 (SetSetting returned True and the project did move). So the mismatch is not 'audio is always 24' but 'the clip kept the rate the project had when it was imported, and the project moved afterwards' — which also means a WAV imported into a 29.97 project behaves exactly like video, with no trap at all. ALWAYS read the clip's FPS property; never assume 24. The original 21.0.3.7 report below is consistent with this: that project was at 24 when the WAV was imported. Reading the frames at the timeline rate lands minutes away from the real position in the file. Verified live on Studio 21.0.3.7 (2026-08-09, 29.97 fps timeline): a ZOOM0028.WAV item reported source_start 56871, which is 56871 / 24 = 2369.6 s into the file, NOT the 1897.6 s a 29.97 fps reading gives — a 471.9 s (7 min 52 s) error. Nothing looks wrong, because timeline probe_timeline_structure derives source_end as source_start + timeline_duration: the start/end pair stays internally consistent whatever rate you assume. VIDEO items are NOT affected — two 29.97 fps items (KR020007.MOV, IMG_0001.mov) on the same timeline reported source frames in their own, matching rate, confirmed against ffprobe durations and span arithmetic. This is the read-side twin of the AppendToTimeline mixed-fps entry below: that one is about writing source frames whose rate differs from the timeline's, this one about reading them back and not knowing which rate they are in. The rate was pinned by regression, not assumed: across 12 items of the same WAV, GetSourceStartFrame advances at 24.000 fps against the item's own GetSourceStartTime (24.0000/24.0007/23.9995 over spans up to 22 minutes). The same measurement exposed a second unit trap: on an AUDIO item GetLeftOffset advances at 29.970 — the TIMELINE rate — so the two readers describe the same edit point in DIFFERENT frame spaces (60687 vs 75784 for one item). On video they share the source space. Caveat on the absolute zero: Resolve's model of this file is 133003 frames (Duration 01:32:21:19 at 24 fps = 5541.79 s) while its true PCM length is 266264768 samples / 48 kHz = 5547.18 s, a 0.097% difference we have not explained — so frames/24 is exact in Resolve's source-time space, which is the space every other Resolve call uses, but may sit ~2 s off the byte position in a 40-minute-deep offset. Re-confirmed on Studio 19.1.3.7 (2026-08-10) with synthetic media, so this is not a 21.x regression: a 300 s 48 kHz WAV reports FPS 24, and appending source frames 4800-5235 of it to a 29.97 fps timeline yields a timeline duration of 543 (= 435 x 29.97/24), which is the conversion happening in the open. The same run measured the cost of the derived end: source_end came back 5343 (4800 + 543) where the true source end is 5235, so source_end / 24 reports 222.625 s against a real 218.133 s from GetSourceEndTime — 4.49 s out, on a clip only 18.1 s long. GetSourceStartTime read exactly 200.0 s (= 4800/24) on the same item. The matching VIDEO item (29.97 source in a 29.97 timeline) was unaffected in both: 24.524 s read against 24.525 s derived. Both second-readers exist on 19.1.3.7, so the GetSourceEndFrame fallback below is for builds older still. GetSourceEndFrame ITSELF changes convention between the two regimes and cannot be used raw: measured over 12 items on 19.1.3.7, it is EXCLUSIVE (equals the endFrame sent) when the source rate equals the timeline rate, and INCLUSIVE (one less) when they differ — off by one in exactly the case a caller reaches for it. It is not a media-type split: the same WAV imported at 29.97 into a 29.97 timeline read exclusive, like video, and only the rate MISMATCH flipped it. What IS stable across both regimes is GetSourceEndTime x media_fps, which was exact on 12 of 12 valid items (30.633 s x 24 = 735.19 -> 735; 24.524 s x 29.97 = 734.98 -> 735) — seconds carry no frame-rate assumption, so the product is in source space by construction.
|
|
390
|
+
- **Workaround / current handling:** Convert an audio item's source frames with the MEDIA's rate, never the timeline's: seconds = source_start / media_fps. READ media_fps from the media-pool item's 'FPS' clip property (or ffprobe) every time — do not infer it from the timeline, and do NOT hard-code 24 for a WAV: that number is whatever the project rate was when the clip was imported, so it is 24 only for a project that was at 24, and a WAV imported at 29.97 has no mismatch at all. Feed the frames back to timeline create_variant_from_ranges in the same media-rate space you read them in; it converts on placement and reports the conversion in items[].duration_delta. The separate GetSourceStartFrame entry above (off-by-one vs GetLeftOffset) applies on top of this — the rate question is which unit the number is in, not whether it is exact. Mitigated in-process: _timeline_item_summary now emits source_fps and source_start_seconds/source_end_seconds beside the frames, so the number always arrives with its unit; on the GetLeftOffset fallback for an audio item it reports the rate as unknown rather than converting a timeline-frame value at the media rate. source_end is no longer source_start + TIMELINE duration: as of v2.93.0 it is round(GetSourceEndTime x media_fps), which is a SOURCE frame by construction and stays EXCLUSIVE as every caller already assumed. Measured live on 19.1.3.7 over 8 items in both regimes, it equals the endFrame actually sent every time, and it reproduces the old value exactly wherever the old value was already right — so matched-rate media does not move. The old arithmetic overshot by +24/+26/+108/+149 frames on the mismatched WAV, and timeline extract_source_frame_ranges built pull ranges out of it: widths of 543 and 749 for clips that consume 435 and 600 source frames. It falls back to the old sum only when GetSourceEndTime or the rate is unreadable.
|
|
391
391
|
- **Tags:** timeline, audio, wav, frame-rate, mixed-fps, silent-failure, readback
|
|
392
392
|
|
|
393
393
|
### Studio-gated calls on the free edition raise a modal that blocks LATER calls
|
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.93.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
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.93.0"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.93.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -3058,12 +3058,16 @@ def _media_item_source_fps(media_pool_item, clip_properties=None):
|
|
|
3058
3058
|
"""The frame rate a media-pool item's SOURCE frames are counted in.
|
|
3059
3059
|
|
|
3060
3060
|
Source frames (GetSourceStartFrame / GetLeftOffset) are expressed in the
|
|
3061
|
-
MEDIA's own rate, never the timeline's
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3061
|
+
MEDIA's own rate, never the timeline's. A WAV has no intrinsic rate, so it
|
|
3062
|
+
takes the PROJECT's timelineFrameRate at IMPORT and freezes it — measured on
|
|
3063
|
+
Studio 19.1.3.7: imported at 24 it reads 24.0, imported at 29.97 it reads
|
|
3064
|
+
29.97, and moving the project afterwards does not change it. So the rate is
|
|
3065
|
+
read here every time and never assumed; 24 in particular is not a WAV
|
|
3066
|
+
constant, only the value a project that was at 24 handed its imports. Reading
|
|
3067
|
+
a mismatched offset at the timeline rate lands minutes from the real position
|
|
3068
|
+
in the file and nothing errors (see the api_truth entry
|
|
3069
|
+
"GetSourceStartFrame on an AUDIO item"). Returns None when the rate cannot be
|
|
3070
|
+
read, so callers surface "unknown" rather than a guess.
|
|
3067
3071
|
|
|
3068
3072
|
Pass ``clip_properties`` when the caller already holds the item's property
|
|
3069
3073
|
dict — the probe path does, so this costs it no extra bridge call.
|
|
@@ -3114,13 +3118,16 @@ def _timeline_item_source_time(item, method):
|
|
|
3114
3118
|
|
|
3115
3119
|
|
|
3116
3120
|
def _timeline_item_source_end_frame(item):
|
|
3117
|
-
"""
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3121
|
+
"""`GetSourceEndFrame` raw — WARNING: its end convention is not fixed.
|
|
3122
|
+
|
|
3123
|
+
Measured on Studio 19.1.3.7 (2026-08-10, 12 items, both regimes): this reader
|
|
3124
|
+
is **exclusive** when the source rate equals the timeline rate, and
|
|
3125
|
+
**inclusive** when they differ — off by one in exactly the case a caller
|
|
3126
|
+
reaches for it. It is not a media-type split; a WAV imported at 29.97 into a
|
|
3127
|
+
29.97 timeline reads exclusive like video, and only the rate MISMATCH flips
|
|
3128
|
+
it. Prefer `_timeline_item_source_end_exclusive`, which sidesteps the
|
|
3129
|
+
convention entirely. Kept raw for the seconds fallback, where a one-frame
|
|
3130
|
+
difference is below the reported precision.
|
|
3124
3131
|
"""
|
|
3125
3132
|
if not _has_method(item, "GetSourceEndFrame"):
|
|
3126
3133
|
return None
|
|
@@ -3130,6 +3137,45 @@ def _timeline_item_source_end_frame(item):
|
|
|
3130
3137
|
return None
|
|
3131
3138
|
|
|
3132
3139
|
|
|
3140
|
+
def _timeline_item_source_end_exclusive(item, source_start, duration, source_fps):
|
|
3141
|
+
"""The EXCLUSIVE source-space end frame — the contract every caller assumes.
|
|
3142
|
+
|
|
3143
|
+
`source_end` has always been exclusive (`src_end_excl` at the append site,
|
|
3144
|
+
`source_end - 1` wherever an inclusive end is wanted). What was wrong was the
|
|
3145
|
+
arithmetic: `source_start + duration` adds a TIMELINE duration to a SOURCE
|
|
3146
|
+
frame, so it is unit-mixed the moment the two rates differ. Measured on
|
|
3147
|
+
Studio 19.1.3.7 against the endFrame actually sent, it overshot by +24, +26,
|
|
3148
|
+
+108 and +149 frames on a 24 fps WAV in a 29.97 timeline, while being exact
|
|
3149
|
+
on every matched-rate item.
|
|
3150
|
+
|
|
3151
|
+
Resolve's own second-reader settles it without knowing the timeline rate:
|
|
3152
|
+
seconds carry no frame-rate assumption, so `GetSourceEndTime x source_fps` is
|
|
3153
|
+
in source space by construction. Measured exact on **12 of 12** valid items
|
|
3154
|
+
across both regimes and both media types — 30.633 s x 24 = 735.19 -> 735,
|
|
3155
|
+
24.524 s x 29.97 = 734.98 -> 735 — and it reproduces the derived value
|
|
3156
|
+
wherever the derived value was already right, so nothing moves for
|
|
3157
|
+
matched-rate media.
|
|
3158
|
+
|
|
3159
|
+
Deliberately NOT `GetSourceEndFrame`: that reader changes convention between
|
|
3160
|
+
the two regimes (see above), so building on it would mean branching on a
|
|
3161
|
+
rate comparison this function would first have to reconstruct.
|
|
3162
|
+
|
|
3163
|
+
Falls back to the historical `source_start + duration` when the second-reader
|
|
3164
|
+
or the rate is unreadable — same value as before, so an older build loses the
|
|
3165
|
+
correction rather than the field.
|
|
3166
|
+
"""
|
|
3167
|
+
end_seconds = _timeline_item_source_time(item, "GetSourceEndTime")
|
|
3168
|
+
if end_seconds is not None and source_fps:
|
|
3169
|
+
end_frame = int(round(end_seconds * source_fps))
|
|
3170
|
+
# A source end at or before the start means the readers disagree about
|
|
3171
|
+
# this item; the derived value is the safer answer than a negative span.
|
|
3172
|
+
if source_start is None or end_frame > source_start:
|
|
3173
|
+
return end_frame
|
|
3174
|
+
if source_start is not None and duration is not None:
|
|
3175
|
+
return source_start + duration
|
|
3176
|
+
return None
|
|
3177
|
+
|
|
3178
|
+
|
|
3133
3179
|
def _timeline_item_source_seconds(item, source_start, source_end, source_fps):
|
|
3134
3180
|
"""(start_seconds, end_seconds) into the source file, or None each.
|
|
3135
3181
|
|
|
@@ -3161,15 +3207,21 @@ def _timeline_item_summary(item, track_info=None, *, media_pool_item=None,
|
|
|
3161
3207
|
pass
|
|
3162
3208
|
duration = _timeline_item_duration(item, start, end)
|
|
3163
3209
|
source_start, source_start_origin = _timeline_item_source_start_with_origin(item)
|
|
3164
|
-
if source_start is not None and duration is not None:
|
|
3165
|
-
source_end = source_start + duration
|
|
3166
3210
|
if track_info is None:
|
|
3167
3211
|
track_info, _ = _timeline_item_track_info(item)
|
|
3168
3212
|
if media_pool_item is None:
|
|
3169
3213
|
media_pool_item = _timeline_item_media_pool_item(item)
|
|
3170
|
-
# source_* are in the MEDIA's frame rate; report it and the
|
|
3171
|
-
# so a caller never has to guess which rate the frame numbers are in.
|
|
3172
|
-
|
|
3214
|
+
# source_* are in the MEDIA's frame rate; report it and the seconds beside
|
|
3215
|
+
# them so a caller never has to guess which rate the frame numbers are in.
|
|
3216
|
+
media_fps = _media_item_source_fps(media_pool_item, clip_properties)
|
|
3217
|
+
# EXCLUSIVE, as it has always been — but computed in source space now, not
|
|
3218
|
+
# by adding a timeline duration to a source frame. media_fps rather than
|
|
3219
|
+
# source_fps below: the end comes from GetSourceEndTime, which is independent
|
|
3220
|
+
# of whichever reader produced source_start, so the audio caveat that blanks
|
|
3221
|
+
# source_fps must not blank the end as well.
|
|
3222
|
+
source_end = _timeline_item_source_end_exclusive(
|
|
3223
|
+
item, source_start, duration, media_fps)
|
|
3224
|
+
source_fps = media_fps
|
|
3173
3225
|
if source_start_origin == "GetLeftOffset" and (track_info or (None,))[0] == "audio":
|
|
3174
3226
|
# GetLeftOffset counts an audio item in TIMELINE frames, so pairing it
|
|
3175
3227
|
# with the media rate would produce a confidently wrong number. Report
|
|
@@ -3186,9 +3238,9 @@ def _timeline_item_summary(item, track_info=None, *, media_pool_item=None,
|
|
|
3186
3238
|
"end": end,
|
|
3187
3239
|
"duration": duration,
|
|
3188
3240
|
"source_start": source_start,
|
|
3189
|
-
#
|
|
3190
|
-
#
|
|
3191
|
-
#
|
|
3241
|
+
# EXCLUSIVE source frame. Read from source space via GetSourceEndTime;
|
|
3242
|
+
# falls back to source_start + TIMELINE duration only when that reader or
|
|
3243
|
+
# the media rate is unavailable, which is the old unit-mixed value.
|
|
3192
3244
|
"source_end": source_end,
|
|
3193
3245
|
"source_fps": source_fps,
|
|
3194
3246
|
"source_start_seconds": source_start_seconds,
|
|
@@ -21535,8 +21587,10 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
21535
21587
|
create_variant_from_ranges takes SOURCE start_frame/end_frame; extract_source_frame_ranges
|
|
21536
21588
|
and source_range_report return SOURCE ranges. A SOURCE frame is counted in the MEDIA's own
|
|
21537
21589
|
frame rate, not the timeline's: an AUDIO item's source_start/source_end read back in the
|
|
21538
|
-
file's rate
|
|
21539
|
-
|
|
21590
|
+
file's rate. A WAV has no intrinsic rate and freezes the PROJECT's rate at import, so it
|
|
21591
|
+
differs from the timeline whenever the project moved afterwards — read source_fps, never
|
|
21592
|
+
assume 24, and converting at the timeline rate is silently wrong by minutes
|
|
21593
|
+
(resolve_control api_truth "GetSourceStartFrame").
|
|
21540
21594
|
|
|
21541
21595
|
Actions:
|
|
21542
21596
|
list() -> {timelines}
|
|
@@ -21656,14 +21710,16 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
21656
21710
|
Default handles=24, gap_max=30. Use handles=0 for gap-only auto handles.
|
|
21657
21711
|
conform_capabilities() -> {supported, partially_supported, unsupported, export_aliases}
|
|
21658
21712
|
probe_timeline_structure(track_types?, include_markers?, include_clip_properties?) -> {tracks, markers}
|
|
21659
|
-
Each item reports source_start
|
|
21660
|
-
|
|
21661
|
-
|
|
21662
|
-
|
|
21663
|
-
|
|
21664
|
-
|
|
21665
|
-
|
|
21666
|
-
|
|
21713
|
+
Each item reports source_start/source_end (SOURCE frames, end EXCLUSIVE) in the
|
|
21714
|
+
MEDIA's frame rate, the source_fps they are counted in, and
|
|
21715
|
+
source_start_seconds/source_end_seconds. Use the reported source_fps — a WAV
|
|
21716
|
+
freezes the PROJECT's rate at import, so it differs from the timeline whenever
|
|
21717
|
+
the project moved afterwards, and dividing by the timeline rate is then wrong by
|
|
21718
|
+
minutes. source_fps is null when the rate could not be read; treat the frames as
|
|
21719
|
+
unitless then, do not assume the timeline's. source_end comes from
|
|
21720
|
+
GetSourceEndTime x source_fps, so it is a source frame even when the rates
|
|
21721
|
+
differ; it falls back to source_start + TIMELINE duration (unit-mixed) only when
|
|
21722
|
+
that reader or the rate is unavailable.
|
|
21667
21723
|
detect_gaps_overlaps(track_types?, min_gap?) -> {gaps, overlaps}
|
|
21668
21724
|
source_range_report(handles?, merge?) -> {ranges, occurrences}
|
|
21669
21725
|
export_timeline_checked(path, format?|type?, subtype?, require_temp_path?, dry_run?, background?) -> {success, path, size | job_id}
|
|
@@ -23493,10 +23549,10 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
23493
23549
|
"track_index is the 1-based destination track WITHIN track_type (default 1); the "
|
|
23494
23550
|
"variant is created with enough video/audio tracks to cover the highest index used, "
|
|
23495
23551
|
"so multicam angles can be rebuilt onto V2/V3 instead of collapsing onto V1. "
|
|
23496
|
-
"SOURCE frames are counted in the MEDIA's frame rate, not the timeline's —
|
|
23497
|
-
"
|
|
23498
|
-
"item\"); pass
|
|
23499
|
-
"reports the conversion. "
|
|
23552
|
+
"SOURCE frames are counted in the MEDIA's frame rate, not the timeline's — read the "
|
|
23553
|
+
"clip's source_fps rather than assuming one, since a WAV freezes the PROJECT's rate "
|
|
23554
|
+
"at import (api_truth \"GetSourceStartFrame on an AUDIO item\"); pass the frames in "
|
|
23555
|
+
"that space, placement converts and items[].duration_delta reports the conversion. "
|
|
23500
23556
|
"pack=true butts clips together at the end of each track (gap-free, ignores record_frame)"
|
|
23501
23557
|
),
|
|
23502
23558
|
"returns": "{success, id, items} — items[].placed = placed frames; items[].range = the requested range",
|
|
@@ -23508,7 +23564,7 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
|
23508
23564
|
' {"clip_id": "<cam3-id>", "start_frame": 1500, "end_frame": 1600,\n'
|
|
23509
23565
|
' "track_index": 2},\n'
|
|
23510
23566
|
' {"clip_id": "<wav-id>", "track_type": "audio", "track_index": 1,\n'
|
|
23511
|
-
' "start_frame": 56871, "end_frame": 57591} #
|
|
23567
|
+
' "start_frame": 56871, "end_frame": 57591} # frames in the WAV\'s own source_fps\n'
|
|
23512
23568
|
' ],\n'
|
|
23513
23569
|
' "dry_run": True\n'
|
|
23514
23570
|
'})'
|
package/src/utils/api_truth.py
CHANGED
|
@@ -661,12 +661,28 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
661
661
|
"submit": "bug",
|
|
662
662
|
},
|
|
663
663
|
{
|
|
664
|
-
"symbol": "TimelineItem.GetSourceStartFrame on an AUDIO item (media-rate frames;
|
|
664
|
+
"symbol": "TimelineItem.GetSourceStartFrame on an AUDIO item (media-rate frames; a WAV freezes the PROJECT rate at import)",
|
|
665
665
|
"object": "TimelineItem",
|
|
666
666
|
"signature": "() -> int # source frame, counted in the MEDIA's frame rate",
|
|
667
667
|
"reality": "The value is counted in the source MEDIA's own frame rate, not "
|
|
668
|
-
"the timeline's
|
|
669
|
-
"
|
|
668
|
+
"the timeline's. CORRECTION (2026-08-10, Studio 19.1.3.7): an "
|
|
669
|
+
"earlier version of this entry said a WAV 'carries no frame rate, "
|
|
670
|
+
"so Resolve falls back to 24 fps'. That is WRONG, and 24 is not a "
|
|
671
|
+
"constant to rely on. A WAV takes the PROJECT's timelineFrameRate "
|
|
672
|
+
"AT IMPORT and freezes it. Measured with one 400.000 s 48 kHz WAV "
|
|
673
|
+
"imported into three project states: project at 24 -> clip FPS "
|
|
674
|
+
"24.0, Duration 00:06:40:00 (9600 frames = 400 s); project at "
|
|
675
|
+
"29.97 -> clip FPS 29.97, Duration 00:06:39:18 (11988 frames = "
|
|
676
|
+
"400 s); and changing the project rate to 29.97 AFTER import left "
|
|
677
|
+
"the clip reading 24.0 (SetSetting returned True and the project "
|
|
678
|
+
"did move). So the mismatch is not 'audio is always 24' but "
|
|
679
|
+
"'the clip kept the rate the project had when it was imported, "
|
|
680
|
+
"and the project moved afterwards' — which also means a WAV "
|
|
681
|
+
"imported into a 29.97 project behaves exactly like video, with "
|
|
682
|
+
"no trap at all. ALWAYS read the clip's FPS property; never "
|
|
683
|
+
"assume 24. The original 21.0.3.7 report below is consistent with "
|
|
684
|
+
"this: that project was at 24 when the WAV was imported. "
|
|
685
|
+
"Reading the frames at the timeline rate lands "
|
|
670
686
|
"minutes away from the real position in the file. Verified live "
|
|
671
687
|
"on Studio 21.0.3.7 (2026-08-09, 29.97 fps timeline): a "
|
|
672
688
|
"ZOOM0028.WAV item reported source_start 56871, which is "
|
|
@@ -712,13 +728,28 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
712
728
|
"29.97 timeline) was unaffected in both: 24.524 s read against "
|
|
713
729
|
"24.525 s derived. Both second-readers exist on 19.1.3.7, so "
|
|
714
730
|
"the GetSourceEndFrame fallback below is for builds older "
|
|
715
|
-
"still."
|
|
731
|
+
"still. GetSourceEndFrame ITSELF changes convention between "
|
|
732
|
+
"the two regimes and cannot be used raw: measured over 12 "
|
|
733
|
+
"items on 19.1.3.7, it is EXCLUSIVE (equals the endFrame "
|
|
734
|
+
"sent) when the source rate equals the timeline rate, and "
|
|
735
|
+
"INCLUSIVE (one less) when they differ — off by one in "
|
|
736
|
+
"exactly the case a caller reaches for it. It is not a "
|
|
737
|
+
"media-type split: the same WAV imported at 29.97 into a "
|
|
738
|
+
"29.97 timeline read exclusive, like video, and only the "
|
|
739
|
+
"rate MISMATCH flipped it. What IS stable across both "
|
|
740
|
+
"regimes is GetSourceEndTime x media_fps, which was exact "
|
|
741
|
+
"on 12 of 12 valid items (30.633 s x 24 = 735.19 -> 735; "
|
|
742
|
+
"24.524 s x 29.97 = 734.98 -> 735) — seconds carry no "
|
|
743
|
+
"frame-rate assumption, so the product is in source space "
|
|
744
|
+
"by construction.",
|
|
716
745
|
"recommended": "Convert an audio item's source frames with the MEDIA's rate, "
|
|
717
|
-
"never the timeline's: seconds = source_start / media_fps
|
|
718
|
-
"
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
746
|
+
"never the timeline's: seconds = source_start / media_fps. "
|
|
747
|
+
"READ media_fps from the media-pool item's 'FPS' clip property "
|
|
748
|
+
"(or ffprobe) every time — do not infer it from the timeline, "
|
|
749
|
+
"and do NOT hard-code 24 for a WAV: that number is whatever "
|
|
750
|
+
"the project rate was when the clip was imported, so it is 24 "
|
|
751
|
+
"only for a project that was at 24, and a WAV imported at "
|
|
752
|
+
"29.97 has no mismatch at all. Feed the frames back to timeline "
|
|
722
753
|
"create_variant_from_ranges in the same media-rate space you "
|
|
723
754
|
"read them in; it converts on placement and reports the "
|
|
724
755
|
"conversion in items[].duration_delta. The separate "
|
|
@@ -730,21 +761,27 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
730
761
|
"beside the frames, so the number always arrives with its "
|
|
731
762
|
"unit; on the GetLeftOffset fallback for an audio item it "
|
|
732
763
|
"reports the rate as unknown rather than converting a "
|
|
733
|
-
"timeline-frame value at the media rate.
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
"
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
"
|
|
740
|
-
"
|
|
741
|
-
"
|
|
742
|
-
"
|
|
764
|
+
"timeline-frame value at the media rate. source_end is "
|
|
765
|
+
"no longer source_start + TIMELINE duration: as of "
|
|
766
|
+
"v2.93.0 it is round(GetSourceEndTime x media_fps), "
|
|
767
|
+
"which is a SOURCE frame by construction and stays "
|
|
768
|
+
"EXCLUSIVE as every caller already assumed. Measured "
|
|
769
|
+
"live on 19.1.3.7 over 8 items in both regimes, it "
|
|
770
|
+
"equals the endFrame actually sent every time, and it "
|
|
771
|
+
"reproduces the old value exactly wherever the old value "
|
|
772
|
+
"was already right — so matched-rate media does not "
|
|
773
|
+
"move. The old arithmetic overshot by +24/+26/+108/+149 "
|
|
774
|
+
"frames on the mismatched WAV, and timeline "
|
|
775
|
+
"extract_source_frame_ranges built pull ranges out of "
|
|
776
|
+
"it: widths of 543 and 749 for clips that consume 435 "
|
|
777
|
+
"and 600 source frames. It falls back to the old sum "
|
|
778
|
+
"only when GetSourceEndTime or the rate is unreadable.",
|
|
743
779
|
"tags": ["timeline", "audio", "wav", "frame-rate", "mixed-fps",
|
|
744
780
|
"silent-failure", "readback"],
|
|
745
781
|
"submit": "bug",
|
|
746
782
|
"mitigation": ["_media_item_source_fps", "_source_frames_to_seconds",
|
|
747
|
-
"_timeline_item_source_seconds"
|
|
783
|
+
"_timeline_item_source_seconds",
|
|
784
|
+
"_timeline_item_source_end_exclusive"],
|
|
748
785
|
},
|
|
749
786
|
{
|
|
750
787
|
"symbol": "Razor / blade / split a timeline item",
|