davinci-resolve-mcp 2.104.0 → 2.104.2
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 +77 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/guides/headless-edit-loop.md +15 -0
- package/install.py +33 -12
- package/package.json +1 -1
- package/resolve-advanced/server/db-patch.mjs +15 -3
- package/resolve-advanced/server/offline-ref-db.mjs +8 -2
- package/resolve-advanced/vendor/drp-format/__tests__/framerate-encoding.test.js +47 -0
- package/resolve-advanced/vendor/drp-format/drp-packager.js +1 -0
- package/resolve-advanced/vendor/drp-format/seq-container-builder.js +11 -3
- package/resolve-advanced/vendor/drp-format/xml-builder.js +23 -19
- package/scripts/doctor.py +85 -39
- package/src/granular/common.py +1 -1
- package/src/granular/media_pool.py +15 -7
- package/src/granular/timeline.py +10 -5
- package/src/granular/timeline_item.py +21 -7
- package/src/server.py +497 -93
- package/src/utils/api_truth.py +17 -5
- package/src/utils/app_control.py +11 -1
- package/src/utils/audio_fairlight_live_probe.py +8 -1
- package/src/utils/color_grade_live_probe.py +8 -1
- package/src/utils/fusion_composition_live_probe.py +8 -1
- package/src/utils/probe_catalogue.py +15 -3
- package/src/utils/project_cleanup.py +17 -3
- package/src/utils/render_deliver_live_probe.py +8 -1
- package/src/utils/resolve_bridge_client.py +10 -1
- package/src/utils/resolve_writes.py +79 -0
- package/src/utils/review_annotation_live_probe.py +8 -1
- package/src/utils/timeline_conform_live_probe.py +8 -1
- package/src/utils/timeline_kernel_live_probe.py +7 -1
- package/src/utils/timeline_versioning.py +15 -2
package/src/utils/api_truth.py
CHANGED
|
@@ -1875,10 +1875,20 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1875
1875
|
"(frame 0 == first frame), while recordFrame and "
|
|
1876
1876
|
"TimelineItem.GetStart/GetEnd are absolute. Distinct from "
|
|
1877
1877
|
"the null-id entry below, which is a recordFrame landing in "
|
|
1878
|
-
"an OCCUPIED span rather than before the start.
|
|
1878
|
+
"an OCCUPIED span rather than before the start. Worse, the "
|
|
1879
|
+
"render JOB's own metadata lies too: Resolve rewrites the "
|
|
1880
|
+
"job's MarkIn/MarkOut down to the collapsed extent (measured "
|
|
1881
|
+
"live on 19.1.3.7: 96 frames of content before start -> an "
|
|
1882
|
+
"explicit 96-frame mark range became a 1-frame job, Complete "
|
|
1883
|
+
"at 100%, a 1-frame black stub), so output-duration vs "
|
|
1884
|
+
"job-mark-range checks read clean. The only truthful "
|
|
1885
|
+
"readback is the timeline items themselves, which report "
|
|
1886
|
+
"their real GetStart/GetEnd — before the start frame. "
|
|
1887
|
+
"Verified on "
|
|
1879
1888
|
"Studio 20.3.2.9 (v2.17.1 probe: relative record_frame 12 "
|
|
1880
|
-
"landed at 86400 + 12 = 86412, absolute preserved 86484)
|
|
1881
|
-
"independently on Studio 21.0.4.5 (issue #164)
|
|
1889
|
+
"landed at 86400 + 12 = 86412, absolute preserved 86484), "
|
|
1890
|
+
"independently on Studio 21.0.4.5 (issue #164), and the "
|
|
1891
|
+
"mark-range rewrite on Studio 19.1.3.7 (2026-08-29).",
|
|
1882
1892
|
"recommended": "Offset every recordFrame by timeline.GetStartFrame(). "
|
|
1883
1893
|
"This server already does it: "
|
|
1884
1894
|
"media_pool.append_to_timeline defaults to "
|
|
@@ -1888,8 +1898,10 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
1888
1898
|
"value below the timeline start is refused outright. "
|
|
1889
1899
|
"When driving the API directly, "
|
|
1890
1900
|
"never treat JobStatus Complete as proof a render "
|
|
1891
|
-
"worked —
|
|
1892
|
-
"
|
|
1901
|
+
"worked — and do not trust the job's MarkIn/MarkOut as "
|
|
1902
|
+
"the expected duration either. render.verify_output "
|
|
1903
|
+
"(v2.104.1) cross-checks the job against its timeline's "
|
|
1904
|
+
"item extents and flags items before the start frame.",
|
|
1893
1905
|
"tags": ["timeline", "edit", "render", "silent-failure", "media-pool"],
|
|
1894
1906
|
"issue": 164,
|
|
1895
1907
|
},
|
package/src/utils/app_control.py
CHANGED
|
@@ -78,13 +78,23 @@ def quit_resolve_app(resolve_obj, force: bool = False, save_project: bool = True
|
|
|
78
78
|
if project and save_project:
|
|
79
79
|
logger.info("Saving project before quitting")
|
|
80
80
|
# Try to save the project
|
|
81
|
+
# Only the exception was handled. SaveProject also reports a
|
|
82
|
+
# refusal with a bare False -- which is what a project attached
|
|
83
|
+
# to no database returns -- and quitting on that throws the
|
|
84
|
+
# session away silently.
|
|
81
85
|
try:
|
|
82
|
-
project.SaveProject()
|
|
86
|
+
saved = project.SaveProject()
|
|
83
87
|
except Exception as e:
|
|
84
88
|
logger.error(f"Failed to save project: {str(e)}")
|
|
85
89
|
if not force:
|
|
86
90
|
logger.error("Aborting quit due to save failure")
|
|
87
91
|
return False
|
|
92
|
+
else:
|
|
93
|
+
if saved is False:
|
|
94
|
+
logger.error("SaveProject returned False; the project was NOT saved")
|
|
95
|
+
if not force:
|
|
96
|
+
logger.error("Aborting quit due to save failure")
|
|
97
|
+
return False
|
|
88
98
|
|
|
89
99
|
# Attempt to quit using the API
|
|
90
100
|
if hasattr(resolve_obj, 'Quit') and callable(getattr(resolve_obj, 'Quit')):
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, Optional
|
|
15
15
|
|
|
16
16
|
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
17
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -146,7 +147,13 @@ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, An
|
|
|
146
147
|
timeline = media_pool.CreateTimelineFromClips(timeline_name, [video_clip])
|
|
147
148
|
if not timeline:
|
|
148
149
|
raise AssertionError("Failed to create audio timeline")
|
|
149
|
-
|
|
150
|
+
# A discarded False here would run the whole probe against whatever
|
|
151
|
+
# timeline was already current, and every measurement below would be a
|
|
152
|
+
# reading of the wrong thing reported as a reading of this one.
|
|
153
|
+
switched, switch_detail = set_current_timeline(project, timeline)
|
|
154
|
+
if not switched:
|
|
155
|
+
raise AssertionError(
|
|
156
|
+
describe_switch_failure(switch_detail, "probing the audio timeline"))
|
|
150
157
|
print(f"Created timeline: {timeline_name}")
|
|
151
158
|
|
|
152
159
|
video_clip_id = _clip_id(video_clip)
|
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any, Dict, Optional
|
|
16
16
|
|
|
17
17
|
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
18
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -188,7 +189,13 @@ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, An
|
|
|
188
189
|
timeline = media_pool.CreateTimelineFromClips(timeline_name, [clip])
|
|
189
190
|
if not timeline:
|
|
190
191
|
raise AssertionError("Failed to create color grade timeline")
|
|
191
|
-
|
|
192
|
+
# A discarded False here would run the whole probe against whatever
|
|
193
|
+
# timeline was already current, and every measurement below would be a
|
|
194
|
+
# reading of the wrong thing reported as a reading of this one.
|
|
195
|
+
switched, switch_detail = set_current_timeline(project, timeline)
|
|
196
|
+
if not switched:
|
|
197
|
+
raise AssertionError(
|
|
198
|
+
describe_switch_failure(switch_detail, "probing the color grade timeline"))
|
|
192
199
|
media_pool.AppendToTimeline([clip])
|
|
193
200
|
timeline.SetCurrentTimecode("01:00:00:01")
|
|
194
201
|
server.resolve_control("open_page", {"page": "color"})
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, Optional
|
|
15
15
|
|
|
16
16
|
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
17
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -139,7 +140,13 @@ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, An
|
|
|
139
140
|
timeline = media_pool.CreateTimelineFromClips(timeline_name, [clip])
|
|
140
141
|
if not timeline:
|
|
141
142
|
raise AssertionError("Failed to create Fusion timeline")
|
|
142
|
-
|
|
143
|
+
# A discarded False here would run the whole probe against whatever
|
|
144
|
+
# timeline was already current, and every measurement below would be a
|
|
145
|
+
# reading of the wrong thing reported as a reading of this one.
|
|
146
|
+
switched, switch_detail = set_current_timeline(project, timeline)
|
|
147
|
+
if not switched:
|
|
148
|
+
raise AssertionError(
|
|
149
|
+
describe_switch_failure(switch_detail, "probing the Fusion timeline"))
|
|
143
150
|
timeline.SetCurrentTimecode("01:00:00:01")
|
|
144
151
|
print(f"Created timeline: {timeline_name}")
|
|
145
152
|
|
|
@@ -25,6 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
from typing import Any, List
|
|
26
26
|
|
|
27
27
|
from src.utils.mode_matrix import Probe
|
|
28
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
28
29
|
|
|
29
30
|
CATALOGUE: List[Probe] = []
|
|
30
31
|
|
|
@@ -731,11 +732,22 @@ def _probe_editorial_nested(ctx):
|
|
|
731
732
|
if (c.GetClipProperty("Type") or "") == "Timeline"]
|
|
732
733
|
if not nested:
|
|
733
734
|
return None
|
|
734
|
-
|
|
735
|
+
# AppendToTimeline writes to the CURRENT timeline, so a discarded False
|
|
736
|
+
# here would nest the clip into ctx.timeline and then count PROBE_OUTER's
|
|
737
|
+
# empty track -- a measurement of the wrong timeline, reported as this one's.
|
|
738
|
+
switched, detail = set_current_timeline(ctx.project, outer)
|
|
739
|
+
if not switched:
|
|
740
|
+
return describe_switch_failure(detail, "nesting the probe timeline")
|
|
735
741
|
appended = pool.AppendToTimeline([{"mediaPoolItem": nested[0]}])
|
|
736
742
|
count = len(outer.GetItemListInTrack("video", 1) or [])
|
|
737
|
-
ctx.project
|
|
738
|
-
|
|
743
|
+
restored, restore_detail = set_current_timeline(ctx.project, ctx.timeline)
|
|
744
|
+
result = f"{bool(appended)}/{count}"
|
|
745
|
+
if not restored:
|
|
746
|
+
# The measurement stands; the teardown did not. Say so in the result
|
|
747
|
+
# rather than leaving the next probe on PROBE_OUTER without warning.
|
|
748
|
+
result += " (probe timeline left current: " + \
|
|
749
|
+
describe_switch_failure(restore_detail, "restoring the probe timeline") + ")"
|
|
750
|
+
return result
|
|
739
751
|
|
|
740
752
|
|
|
741
753
|
@probe("editorial.set_clips_linked", "editorial", timeout=90, needs=("timeline",),
|
|
@@ -13,9 +13,12 @@ disposable-project flow gets it for free:
|
|
|
13
13
|
instead of silently leaking.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import logging
|
|
16
17
|
import time
|
|
17
18
|
from typing import Any, Dict, Optional
|
|
18
19
|
|
|
20
|
+
logger = logging.getLogger("davinci-resolve-mcp.project_cleanup")
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
#: The name Resolve gives the project it opens when it has nothing else to open.
|
|
21
24
|
#: It has never been saved and has no location, so it cannot be saved.
|
|
@@ -103,10 +106,21 @@ def delete_project_safely(
|
|
|
103
106
|
# cannot be saved, so the next close or switch raises a modal no
|
|
104
107
|
# script can dismiss. That is a wedged application, not a tidy-up.
|
|
105
108
|
if switch_to and switch_to != name:
|
|
109
|
+
# A discarded False leaves the session on the unsaveable
|
|
110
|
+
# "Untitled Project" this comment exists to avoid, and the next
|
|
111
|
+
# close or switch raises a modal no script can dismiss. Logged
|
|
112
|
+
# loudly: the caller is mid-delete and cannot be aborted here,
|
|
113
|
+
# but a silent wedge is what made this hard to diagnose.
|
|
106
114
|
try:
|
|
107
|
-
pm.LoadProject(switch_to)
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
if not pm.LoadProject(switch_to):
|
|
116
|
+
logger.error(
|
|
117
|
+
"LoadProject(%r) returned False after closing %r; the "
|
|
118
|
+
"session is left on an unsaveable Untitled Project and "
|
|
119
|
+
"the next close will raise a modal", switch_to, name,
|
|
120
|
+
)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.error("LoadProject(%r) raised after closing %r: %s",
|
|
123
|
+
switch_to, name, exc)
|
|
110
124
|
|
|
111
125
|
last_error = None
|
|
112
126
|
for attempt in range(1 + max(0, int(retries))):
|
|
@@ -20,6 +20,7 @@ from pathlib import Path
|
|
|
20
20
|
from typing import Any, Dict, Optional
|
|
21
21
|
|
|
22
22
|
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
23
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -183,7 +184,13 @@ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, An
|
|
|
183
184
|
timeline = media_pool.CreateTimelineFromClips(timeline_name, [clip])
|
|
184
185
|
if not timeline:
|
|
185
186
|
raise AssertionError("Failed to create synthetic render timeline")
|
|
186
|
-
|
|
187
|
+
# A discarded False here would run the whole probe against whatever
|
|
188
|
+
# timeline was already current, and every measurement below would be a
|
|
189
|
+
# reading of the wrong thing reported as a reading of this one.
|
|
190
|
+
switched, switch_detail = set_current_timeline(project, timeline)
|
|
191
|
+
if not switched:
|
|
192
|
+
raise AssertionError(
|
|
193
|
+
describe_switch_failure(switch_detail, "probing the synthetic render timeline"))
|
|
187
194
|
print(f"Created timeline: {timeline_name}")
|
|
188
195
|
|
|
189
196
|
chosen = _choose_render_format_codec(server)
|
|
@@ -236,7 +236,16 @@ class _BoundMethod:
|
|
|
236
236
|
self._handle = handle
|
|
237
237
|
self._name = name
|
|
238
238
|
|
|
239
|
-
def __call__(self, *args: Any) -> Any:
|
|
239
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
240
|
+
if kwargs:
|
|
241
|
+
raise TypeError(
|
|
242
|
+
f"{self._name}() was called with keyword argument(s) "
|
|
243
|
+
f"{sorted(kwargs)} through the in-app bridge, which proxies "
|
|
244
|
+
"Resolve calls positionally. Pass every argument positionally "
|
|
245
|
+
"(Resolve's own signatures accept it) — e.g. "
|
|
246
|
+
"StartRendering([job_ids], False) instead of "
|
|
247
|
+
"StartRendering([job_ids], isInteractiveMode=False)."
|
|
248
|
+
)
|
|
240
249
|
result = self._transport.request(
|
|
241
250
|
"call",
|
|
242
251
|
{"target": self._handle, "method": self._name,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Resolve mutators whose return value must not be discarded.
|
|
2
|
+
|
|
3
|
+
Every write in the DaVinci Resolve scripting API reports itself with a bare
|
|
4
|
+
boolean and nothing else. Dropping that boolean turns a failed write into a
|
|
5
|
+
successful-looking no-op, which is the only failure shape worse than a crash:
|
|
6
|
+
the tool reports what it meant to do, and the project does not have it.
|
|
7
|
+
|
|
8
|
+
`SetCurrentTimeline` is the sharpest case in this repo, which is why it lives
|
|
9
|
+
here. `AppendToTimeline`, `GetCurrentTimeline` and every other "current
|
|
10
|
+
timeline" call writes to whatever the PROJECT believes is current, never to the
|
|
11
|
+
handle in scope, so a discarded `False` sends an assembly to the timeline the
|
|
12
|
+
editor was looking at while the caller reports the new timeline's name and id.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def unique_id(obj: Any) -> Optional[str]:
|
|
19
|
+
"""`obj.GetUniqueId()`, or None. Never raises."""
|
|
20
|
+
if obj is None:
|
|
21
|
+
return None
|
|
22
|
+
try:
|
|
23
|
+
return obj.GetUniqueId()
|
|
24
|
+
except Exception:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_current_timeline(project: Any, timeline: Any) -> Tuple[bool, Dict[str, Any]]:
|
|
29
|
+
"""Switch the project's current timeline. Returns (ok, detail).
|
|
30
|
+
|
|
31
|
+
The bare bool is not trusted on its own. A Resolve attached to no database
|
|
32
|
+
answers version queries normally while every project mutation returns False
|
|
33
|
+
or None, and a `True` that did not take has been observed there, so the
|
|
34
|
+
switch is confirmed by reading the current timeline back.
|
|
35
|
+
|
|
36
|
+
When the readback is unavailable (a build or transport that does not answer
|
|
37
|
+
`GetCurrentTimeline`) the bool is the only evidence there is, and it is
|
|
38
|
+
used. That is a weaker check, not a reason to refuse the work.
|
|
39
|
+
|
|
40
|
+
`detail` always carries `returned`, `wanted_timeline_id`,
|
|
41
|
+
`current_timeline_id` and, on an exception, `exception`.
|
|
42
|
+
"""
|
|
43
|
+
detail: Dict[str, Any] = {
|
|
44
|
+
"returned": None,
|
|
45
|
+
"wanted_timeline_id": unique_id(timeline),
|
|
46
|
+
"current_timeline_id": None,
|
|
47
|
+
}
|
|
48
|
+
try:
|
|
49
|
+
detail["returned"] = project.SetCurrentTimeline(timeline)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
detail["exception"] = str(exc)
|
|
52
|
+
return False, detail
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
detail["current_timeline_id"] = unique_id(project.GetCurrentTimeline())
|
|
56
|
+
except Exception:
|
|
57
|
+
detail["current_timeline_id"] = None
|
|
58
|
+
|
|
59
|
+
wanted = detail["wanted_timeline_id"]
|
|
60
|
+
current = detail["current_timeline_id"]
|
|
61
|
+
if wanted is not None and current is not None:
|
|
62
|
+
return wanted == current, detail
|
|
63
|
+
return bool(detail["returned"]), detail
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def describe_switch_failure(detail: Dict[str, Any], what: str) -> str:
|
|
67
|
+
"""One line naming what did not happen, for a log or an error envelope."""
|
|
68
|
+
if "exception" in detail:
|
|
69
|
+
return (
|
|
70
|
+
f"Could not switch the current timeline before {what}: "
|
|
71
|
+
f"{detail['exception']}. Nothing was written."
|
|
72
|
+
)
|
|
73
|
+
return (
|
|
74
|
+
f"Could not switch the current timeline before {what}. "
|
|
75
|
+
f"SetCurrentTimeline returned {detail.get('returned')!r} and the current "
|
|
76
|
+
f"timeline is still {detail.get('current_timeline_id')!r}, not "
|
|
77
|
+
f"{detail.get('wanted_timeline_id')!r}. Continuing would write to the "
|
|
78
|
+
"wrong timeline while reporting success."
|
|
79
|
+
)
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, Optional
|
|
15
15
|
|
|
16
16
|
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
17
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -148,7 +149,13 @@ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, An
|
|
|
148
149
|
timeline = media_pool.CreateTimelineFromClips(timeline_name, [clip])
|
|
149
150
|
if not timeline:
|
|
150
151
|
raise AssertionError("Failed to create review annotation timeline")
|
|
151
|
-
|
|
152
|
+
# A discarded False here would run the whole probe against whatever
|
|
153
|
+
# timeline was already current, and every measurement below would be a
|
|
154
|
+
# reading of the wrong thing reported as a reading of this one.
|
|
155
|
+
switched, switch_detail = set_current_timeline(project, timeline)
|
|
156
|
+
if not switched:
|
|
157
|
+
raise AssertionError(
|
|
158
|
+
describe_switch_failure(switch_detail, "probing the review annotation timeline"))
|
|
152
159
|
timeline.SetCurrentTimecode("01:00:00:01")
|
|
153
160
|
print(f"Created timeline: {timeline_name}")
|
|
154
161
|
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any, Dict, Optional
|
|
15
15
|
|
|
16
16
|
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
17
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -157,7 +158,13 @@ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, An
|
|
|
157
158
|
)
|
|
158
159
|
if not timeline:
|
|
159
160
|
raise AssertionError("Failed to create conform timeline")
|
|
160
|
-
|
|
161
|
+
# A discarded False here would run the whole probe against whatever
|
|
162
|
+
# timeline was already current, and every measurement below would be a
|
|
163
|
+
# reading of the wrong thing reported as a reading of this one.
|
|
164
|
+
switched, switch_detail = set_current_timeline(project, timeline)
|
|
165
|
+
if not switched:
|
|
166
|
+
raise AssertionError(
|
|
167
|
+
describe_switch_failure(switch_detail, "probing the conform timeline"))
|
|
161
168
|
media_pool.AppendToTimeline(
|
|
162
169
|
[
|
|
163
170
|
{"mediaPoolItem": clip_b, "startFrame": 0, "endFrame": 48, "recordFrame": 86472, "trackIndex": 1, "mediaType": 1},
|
|
@@ -947,7 +947,13 @@ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, An
|
|
|
947
947
|
audio_item = server._find_timeline_item_by_id(timeline, audio_id)
|
|
948
948
|
if not audio_item:
|
|
949
949
|
raise AssertionError(f"Could not recover audio item: {audio_id}")
|
|
950
|
-
|
|
950
|
+
# Everything the probe measures below assumes the pair is linked. A
|
|
951
|
+
# discarded False would measure UNLINKED behaviour and record it as
|
|
952
|
+
# linked behaviour.
|
|
953
|
+
if not timeline.SetClipsLinked([source_item, audio_item], True):
|
|
954
|
+
raise AssertionError(
|
|
955
|
+
"SetClipsLinked refused the source/audio pair; every linked-item "
|
|
956
|
+
"measurement below would be a reading of unlinked items")
|
|
951
957
|
|
|
952
958
|
source_duration = _frame_int(source_item.GetDuration())
|
|
953
959
|
metadata["source"] = {
|
|
@@ -35,6 +35,7 @@ from src.utils.resolve_probe import has_method
|
|
|
35
35
|
from typing import Any, Dict, List, Optional, Tuple
|
|
36
36
|
|
|
37
37
|
from src.utils import actor_identity, timeline_brain_db
|
|
38
|
+
from .resolve_writes import set_current_timeline, describe_switch_failure
|
|
38
39
|
|
|
39
40
|
logger = logging.getLogger("resolve-mcp.timeline-versioning")
|
|
40
41
|
|
|
@@ -166,7 +167,16 @@ def archive_current_timeline(
|
|
|
166
167
|
# DuplicateTimeline drops the duplicate in the *current* folder (Archive),
|
|
167
168
|
# but it also switches Resolve's current timeline to the duplicate. Restore
|
|
168
169
|
# the working timeline as the active one so downstream mutation hits it.
|
|
169
|
-
|
|
170
|
+
# A discarded False here sends every subsequent mutation into the ARCHIVE
|
|
171
|
+
# copy while the caller believes it is editing the working timeline, so the
|
|
172
|
+
# archive fails loudly instead.
|
|
173
|
+
switched, switch_detail = set_current_timeline(project, tl)
|
|
174
|
+
if not switched:
|
|
175
|
+
return {"success": False,
|
|
176
|
+
"error": describe_switch_failure(
|
|
177
|
+
switch_detail, "returning to the working timeline after archiving"),
|
|
178
|
+
"archived_timeline_name": archived_name,
|
|
179
|
+
"switch": switch_detail}
|
|
170
180
|
|
|
171
181
|
# Timebase snapshot — timeline_clip_usage stores ABSOLUTE record frames
|
|
172
182
|
# (GetStart() includes the start-timecode offset), so readers need the
|
|
@@ -612,10 +622,13 @@ def rollback_to_version(
|
|
|
612
622
|
if restored is None:
|
|
613
623
|
return {"success": False, "error": f"DuplicateTimeline('{restored_name}') failed"}
|
|
614
624
|
|
|
615
|
-
project
|
|
625
|
+
switched, switch_detail = set_current_timeline(project, restored)
|
|
616
626
|
|
|
617
627
|
return {
|
|
618
628
|
"success": True,
|
|
629
|
+
"current_timeline_switched": switched,
|
|
630
|
+
"switch_error": (None if switched else describe_switch_failure(
|
|
631
|
+
switch_detail, "opening the restored timeline")),
|
|
619
632
|
"rolled_back_from_version": version,
|
|
620
633
|
"restored_timeline_name": restored_name,
|
|
621
634
|
"archive_of_previous": archive_result,
|