davinci-resolve-mcp 4.8.30 → 4.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +20 -1
- package/docs/reference/readwrite-symmetry.md +3 -3
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/timeline_item.py +11 -1
- package/src/server.py +210 -11
- package/src/utils/destructive_hook.py +1 -0
- package/src/utils/execution_lifecycle.py +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,94 @@
|
|
|
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 v4.9.1 — a rejected audio level write now says why, and what to do instead
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **`safe_set_audio_properties` and `timeline_item set_audio` explain a
|
|
10
|
+
refused Volume / Pan / EQ write.** Resolve's scripting API has no write path
|
|
11
|
+
for audio clip or track level — `SetProperty('Volume'/'Level'/'Gain')`
|
|
12
|
+
returned `False` when measured live on 21.0.0, and `'Pan'` is the *video* transform
|
|
13
|
+
key, so it returns `True` while the audio pan does not move. A caller who saw
|
|
14
|
+
`{"write": false}` (or a `Pan` that "succeeded" and changed nothing) had no
|
|
15
|
+
way to tell "bad value" from "this cannot be written from the API at all" —
|
|
16
|
+
and the second is a different task. Both actions now attach a
|
|
17
|
+
`known_limitation` block when a level or EQ write fails, and on every `Pan`
|
|
18
|
+
write (its success is the misleading case): the
|
|
19
|
+
`api_truth` ledger entry plus the concrete ways around it (bake the gain into
|
|
20
|
+
a rendered copy of the source with ffmpeg; or save the mix once as a
|
|
21
|
+
Fairlight preset and apply it per-timeline with
|
|
22
|
+
`project_settings apply_fairlight_preset`, on Resolve 20.2.2+ only; the
|
|
23
|
+
preset methods are absent on 19.x). `AudioSyncOffset` writes are
|
|
24
|
+
unaffected — they work, and are not flagged. The legacy granular
|
|
25
|
+
`set_timeline_item_audio` returns the same guidance as its failure string.
|
|
26
|
+
Contributed by @youssefm3208-jpg (#279).
|
|
27
|
+
|
|
28
|
+
### Changed on landing
|
|
29
|
+
|
|
30
|
+
- A `Volume` / `Level` / `Gain` / EQ write is flagged only when Resolve
|
|
31
|
+
refused it, so a build that honours the write is not told it is impossible.
|
|
32
|
+
- The Fairlight-preset workaround names its Resolve 20.2.2 floor.
|
|
33
|
+
|
|
34
|
+
### Tests
|
|
35
|
+
|
|
36
|
+
- `tests/test_audio_fairlight_probe.py`: a refused Volume write carries the
|
|
37
|
+
ledger entry and workarounds; a Pan write is flagged even when it returns
|
|
38
|
+
True; `AudioSyncOffset` is not flagged; an honoured Volume write is not
|
|
39
|
+
flagged; the preset workaround states its version floor.
|
|
40
|
+
|
|
41
|
+
### Validation
|
|
42
|
+
|
|
43
|
+
- Response-shape change only; no Resolve scripting call changed, so no live
|
|
44
|
+
run was required. The Volume/Pan behaviour is the `api_truth` ledger's live
|
|
45
|
+
measurement on 21.0.0.
|
|
46
|
+
|
|
47
|
+
## What's New in v4.9.0 — bounded Media Pool import
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`media_pool(action="import_bounded_media")`** creates one bounded Media
|
|
52
|
+
Pool item from a source file through
|
|
53
|
+
`MediaStorage.AddItemListToMediaPool([{media, startFrame, endFrame}])`.
|
|
54
|
+
Params: `source_path` (absolute, must exist), `start_frame`, `end_frame`,
|
|
55
|
+
`destination_folder` (must already exist) and `name`. The import lands in
|
|
56
|
+
the destination folder, which is made current only for the one call; the
|
|
57
|
+
previous current folder is restored afterwards, including when the import
|
|
58
|
+
fails. The result carries the item summary and the range properties
|
|
59
|
+
Resolve reports for it (`bounded_import_properties`). `start_frame` and
|
|
60
|
+
`end_frame` are passed through unchanged; the wrapper does not adjust for
|
|
61
|
+
end-frame inclusivity. If the rename fails after Resolve has created the
|
|
62
|
+
item, the error says the item exists under Resolve's default name and
|
|
63
|
+
returns its id in `state`, so it can be renamed or deleted rather than
|
|
64
|
+
imported twice. `media_storage.import_to_pool(item_infos)` remains the
|
|
65
|
+
raw passthrough. Contributed by @dmourati (#275).
|
|
66
|
+
- Contributor-validated on Studio 21.1.1 Build 10: this itemInfo form created
|
|
67
|
+
a native subclip, and `endFrame` was inclusive (0–300 gave 301 frames).
|
|
68
|
+
That is one tested configuration, not a guarantee for other builds or
|
|
69
|
+
media types; not measured on this project's 19.1.3.7 machine.
|
|
70
|
+
|
|
71
|
+
### Safety
|
|
72
|
+
|
|
73
|
+
- The action is registered as a write (`destructive_hook`) and rated LOW
|
|
74
|
+
(`execution_lifecycle`): it only adds an item, and neither the source nor
|
|
75
|
+
existing pool contents change. Safe mode, the dry-run refusal and the
|
|
76
|
+
operation log now see it. `docs/reference/readwrite-symmetry.md`
|
|
77
|
+
regenerated.
|
|
78
|
+
|
|
79
|
+
### Tests
|
|
80
|
+
|
|
81
|
+
- `tests/test_media_pool_ingest_probe.py`: input validation never reaches
|
|
82
|
+
Resolve, the folder is restored on success and failure, `MediaStorage`
|
|
83
|
+
comes from the Resolve object rather than the project manager, and a
|
|
84
|
+
failed rename reports the created item.
|
|
85
|
+
- `tests/test_execution_lifecycle.py`: the action classifies as a
|
|
86
|
+
recognised, destructive, LOW-risk write.
|
|
87
|
+
|
|
88
|
+
### Validation
|
|
89
|
+
|
|
90
|
+
- Offline suite green. The live result above is the contributor's, on
|
|
91
|
+
Studio 21.1.1 Build 10.
|
|
92
|
+
|
|
5
93
|
## What's New in v4.8.30 — conform_lint reports every reuse of one source
|
|
6
94
|
|
|
7
95
|
### Fixed
|
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
|
-
> 本翻译对应 v4.
|
|
15
|
+
> 本翻译对应 v4.9.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/docs/SKILL.md
CHANGED
|
@@ -1787,6 +1787,10 @@ helpers:
|
|
|
1787
1787
|
- `probe_audio_track(track_index?)`
|
|
1788
1788
|
- `probe_audio_item(track_type?, track_index?, item_index?)`
|
|
1789
1789
|
- `safe_set_audio_properties(properties, restore?, dry_run?, track_type?, track_index?, item_index?)`
|
|
1790
|
+
— Volume / Pan / EQ writes are not honoured by Resolve's API; a request for
|
|
1791
|
+
any of them returns a `known_limitation` block with the workaround (bake gain
|
|
1792
|
+
into a rendered copy, or apply a Fairlight preset). `AudioSyncOffset` writes
|
|
1793
|
+
do work.
|
|
1790
1794
|
- `audio_mix_capability_report(...)`
|
|
1791
1795
|
- `voice_isolation_capabilities(track_index?, track_type?, item_index?)`
|
|
1792
1796
|
- `audio_mapping_report(clip_ids?)`
|
|
@@ -1911,7 +1915,9 @@ Key actions:
|
|
|
1911
1915
|
- `get_transform` / `set_transform(Pan?, Tilt?, ZoomX?, ZoomY?, RotationAngle?, ...)`
|
|
1912
1916
|
- `get_crop` / `set_crop(CropLeft?, CropRight?, CropTop?, CropBottom?, ...)`
|
|
1913
1917
|
- `get_composite` / `set_composite(Opacity?, CompositeMode?)`
|
|
1914
|
-
- `get_audio` / `set_audio(Volume?, Pan?, AudioSyncOffset?)`
|
|
1918
|
+
- `get_audio` / `set_audio(Volume?, Pan?, AudioSyncOffset?)` — Resolve ignores
|
|
1919
|
+
Volume / Pan / EQ writes on audio (returns a `known_limitation` block); only
|
|
1920
|
+
`AudioSyncOffset` / `AudioSyncOffsetIsManual` take effect
|
|
1915
1921
|
- `get_voice_isolation_state` / `set_voice_isolation_state(state)` — Resolve
|
|
1916
1922
|
20.1+; audio timeline items only
|
|
1917
1923
|
- `get_keyframes(property)`, `add_keyframe(property, frame, value)`,
|
|
@@ -2194,6 +2200,19 @@ media_pool(action="safe_import_sequence", params={
|
|
|
2194
2200
|
"end_index": 1048,
|
|
2195
2201
|
"target_folder": "Master/Plates"
|
|
2196
2202
|
})
|
|
2203
|
+
media_pool(action="import_bounded_media", params={
|
|
2204
|
+
"source_path": "/absolute/path/source.mov",
|
|
2205
|
+
"start_frame": 100,
|
|
2206
|
+
"end_frame": 240,
|
|
2207
|
+
"destination_folder": "Master/Selects",
|
|
2208
|
+
"name": "Selected range"
|
|
2209
|
+
})
|
|
2210
|
+
# start_frame/end_frame are raw Resolve startFrame/endFrame values. The MCP does
|
|
2211
|
+
# not add or subtract a frame; inspect bounded_import_properties after creation.
|
|
2212
|
+
# Contributor-validated on DaVinci Resolve Studio 21.1.1 Build 10 (#275): this
|
|
2213
|
+
# itemInfo form created a native subclip, and endFrame was inclusive (0–300
|
|
2214
|
+
# reported 301 frames). Treat that as an observed configuration-specific result,
|
|
2215
|
+
# not a guarantee for every Resolve version or media type.
|
|
2197
2216
|
media_pool(action="media_pool_boundary_report", params={"selected": True, "depth": 2})
|
|
2198
2217
|
# Positioned append (MediaPool.AppendToTimeline([{clipInfo}, ...])) — e.g. rebuild a subtitle row after delete_clips
|
|
2199
2218
|
media_pool(action="append_to_timeline", params={"clip_infos": [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Read/Write Symmetry Audit
|
|
4
4
|
|
|
5
|
-
- write-style action occurrences scanned: **
|
|
5
|
+
- write-style action occurrences scanned: **126**
|
|
6
6
|
- write-style action occurrences with a matching read: **76**
|
|
7
7
|
- distinct high-signal `set_` actions without a direct/known readback: **4**
|
|
8
8
|
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
- `set_keyframe_interpolation`
|
|
14
14
|
- `set_node_enabled`
|
|
15
15
|
|
|
16
|
-
## Low-signal (create/add/insert/apply/import — usually expected):
|
|
16
|
+
## Low-signal (create/add/insert/apply/import — usually expected): 45 distinct names
|
|
17
17
|
|
|
18
|
-
`add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_modifier`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_multicam_clip`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
|
|
18
|
+
`add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_modifier`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_multicam_clip`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_bounded_media`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.
|
|
40
|
+
VERSION = "4.9.1"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.
|
|
94
|
+
VERSION = "4.9.1"
|
|
95
95
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
96
96
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
97
97
|
logger.info(f"Detected platform: {get_platform()}")
|
|
@@ -716,7 +716,17 @@ def set_timeline_item_audio(timeline_item_id: str,
|
|
|
716
716
|
|
|
717
717
|
return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
|
|
718
718
|
else:
|
|
719
|
-
return
|
|
719
|
+
return (
|
|
720
|
+
f"Failed to set audio properties for timeline item "
|
|
721
|
+
f"'{timeline_item.GetName()}'. Resolve's scripting API has no "
|
|
722
|
+
"write path for audio level, pan, or EQ — SetProperty covers "
|
|
723
|
+
"the video transform only, so 'Volume'/'Gain' return False and "
|
|
724
|
+
"'Pan' moves the video transform, not the audio pan. Work "
|
|
725
|
+
"around it by baking the gain into a rendered copy of the "
|
|
726
|
+
"source (ffmpeg volume=NdB) and importing that, or (Resolve "
|
|
727
|
+
"20.2.2+) by saving a Fairlight preset in the UI and applying "
|
|
728
|
+
"it with project_settings apply_fairlight_preset."
|
|
729
|
+
)
|
|
720
730
|
except Exception as e:
|
|
721
731
|
return f"Error setting timeline item audio properties: {str(e)}"
|
|
722
732
|
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "4.
|
|
14
|
+
VERSION = "4.9.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -803,10 +803,13 @@ edits audio files with no Resolve open. Plan/measure offline, apply live.
|
|
|
803
803
|
- Offline `audio`: split (silence/TC/intervals) / trim / convert (needs ffmpeg on
|
|
804
804
|
PATH — GPL, not bundled). Align/loudness-measure not yet vendored.
|
|
805
805
|
|
|
806
|
-
Timeline audio SetProperty
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
806
|
+
Timeline audio SetProperty for level/pan/EQ does not work — 'Volume'/'Gain'
|
|
807
|
+
return false and 'Pan' writes the video transform, not the audio pan; the
|
|
808
|
+
public API exposes no Fairlight faders or automation curves. safe_set_audio_properties
|
|
809
|
+
and timeline_item set_audio return a `known_limitation` block pointing at the
|
|
810
|
+
fix: bake the gain into a rendered copy of the source, or apply a saved
|
|
811
|
+
Fairlight preset. Use fairlight for bus structure only. The offline audio ops
|
|
812
|
+
write NEW files to scratch, never over source (AGENTS.md).
|
|
810
813
|
|
|
811
814
|
Depth: docs/kernels/audio-fairlight-kernel.md."""
|
|
812
815
|
|
|
@@ -8807,7 +8810,7 @@ def _audio_capabilities():
|
|
|
8807
8810
|
"partially_supported": {
|
|
8808
8811
|
"voice_isolation": "Track/item voice isolation depends on Resolve version, license, page state, and audio content.",
|
|
8809
8812
|
"transcription_subtitles": "Transcription and subtitle generation can be asynchronous and may require installed AI components.",
|
|
8810
|
-
"audio_property_writes": "
|
|
8813
|
+
"audio_property_writes": "Level/pan/EQ writes are not honoured: SetProperty('Volume'/'Gain') returns False and 'Pan' writes the video transform. AudioSyncOffset writes do work. safe_set_audio_properties returns a known_limitation block with the bake / Fairlight-preset workaround.",
|
|
8811
8814
|
"auto_sync": "AutoSyncAudio depends on media content, channel layout, and selected sync settings.",
|
|
8812
8815
|
},
|
|
8813
8816
|
"unsupported": {
|
|
@@ -8899,6 +8902,61 @@ def _probe_audio_item(tl, p: Dict[str, Any]):
|
|
|
8899
8902
|
return _timeline_item_audio_snapshot(item)
|
|
8900
8903
|
|
|
8901
8904
|
|
|
8905
|
+
_AUDIO_LEVEL_KEYS = {"Volume", "Level", "Gain", "AudioVolume", "Pan", "EQEnable", "EQEnabled"}
|
|
8906
|
+
|
|
8907
|
+
|
|
8908
|
+
def _audio_write_limitation(written: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
8909
|
+
"""Guidance for an audio level/pan/EQ write that Resolve will not honour.
|
|
8910
|
+
|
|
8911
|
+
`written` maps each requested key to whether SetProperty returned True.
|
|
8912
|
+
`SetProperty` on a TimelineItem covers the *video* transform only.
|
|
8913
|
+
'Volume'/'Level'/'Gain' returned False when measured live on 21.0.0, and
|
|
8914
|
+
'Pan' is the video-transform key — it returns True while the audio pan
|
|
8915
|
+
stays put. A caller who sees `write: false` (or a Pan write that
|
|
8916
|
+
"succeeds" and changes nothing) has hit a missing feature, not a bad
|
|
8917
|
+
value, and the way around it is a different task: bake the gain into the
|
|
8918
|
+
media, or drive Fairlight. Level/EQ keys are flagged only when the write
|
|
8919
|
+
failed, so a build that honours them is not contradicted; Pan is always
|
|
8920
|
+
flagged, because its success is the misleading case.
|
|
8921
|
+
"""
|
|
8922
|
+
hit = sorted({
|
|
8923
|
+
k for k, ok in written.items()
|
|
8924
|
+
if k in _AUDIO_LEVEL_KEYS and (k == "Pan" or not ok)
|
|
8925
|
+
})
|
|
8926
|
+
if not hit:
|
|
8927
|
+
return None
|
|
8928
|
+
entry = next(iter(lookup_api_truth("Fairlight audio levels")), None)
|
|
8929
|
+
out = {
|
|
8930
|
+
"keys": hit,
|
|
8931
|
+
"reason": "no_api_write_path",
|
|
8932
|
+
"explanation": (
|
|
8933
|
+
"Resolve's scripting API cannot set audio clip or track level, pan, "
|
|
8934
|
+
"EQ, or automation. SetProperty writes the video transform only: "
|
|
8935
|
+
"'Volume'/'Level'/'Gain' return False, and 'Pan' is the video "
|
|
8936
|
+
"transform key so it returns True while the audio pan is unchanged."
|
|
8937
|
+
),
|
|
8938
|
+
"workarounds": [
|
|
8939
|
+
"Bake the level into a rendered copy of the source (ffmpeg "
|
|
8940
|
+
"volume=NdB, plus afade / atrim for fades and trims) and import "
|
|
8941
|
+
"that — the level is then part of the file.",
|
|
8942
|
+
"For a repeatable whole mix (Resolve 20.2.2+; the preset methods "
|
|
8943
|
+
"do not exist on 19.x), save it once as a Fairlight preset in "
|
|
8944
|
+
"the Resolve UI, then apply it per timeline with "
|
|
8945
|
+
"project_settings apply_fairlight_preset "
|
|
8946
|
+
"(names from resolve_control get_fairlight_presets).",
|
|
8947
|
+
"Set individual faders / pan / EQ on the Fairlight page by hand.",
|
|
8948
|
+
],
|
|
8949
|
+
"ledger_verified_on": _API_TRUTH_VERIFIED_ON,
|
|
8950
|
+
}
|
|
8951
|
+
if entry:
|
|
8952
|
+
out["ledger"] = {
|
|
8953
|
+
"symbol": entry.get("symbol"),
|
|
8954
|
+
"reality": entry.get("reality"),
|
|
8955
|
+
"recommended": entry.get("recommended"),
|
|
8956
|
+
}
|
|
8957
|
+
return out
|
|
8958
|
+
|
|
8959
|
+
|
|
8902
8960
|
def _safe_set_audio_properties(tl, p: Dict[str, Any]):
|
|
8903
8961
|
item, err = _audio_item_from_params(tl, p)
|
|
8904
8962
|
if err:
|
|
@@ -8938,7 +8996,18 @@ def _safe_set_audio_properties(tl, p: Dict[str, Any]):
|
|
|
8938
8996
|
row["restore"] = False
|
|
8939
8997
|
row["restore_error"] = str(exc)
|
|
8940
8998
|
results[key] = row
|
|
8941
|
-
|
|
8999
|
+
success = all(row.get("write") for row in results.values())
|
|
9000
|
+
out = {"success": success, "results": results}
|
|
9001
|
+
# 'Volume' never writes and 'Pan' moves the video transform, not the audio
|
|
9002
|
+
# pan — so a bare {"write": false} (or a Pan that "succeeds" inertly) leaves
|
|
9003
|
+
# the caller guessing. Attach the ledger entry and the bake / preset route
|
|
9004
|
+
# whenever one of those keys was in play.
|
|
9005
|
+
limitation = _audio_write_limitation(
|
|
9006
|
+
{k: row.get("write") for k, row in results.items()}
|
|
9007
|
+
)
|
|
9008
|
+
if limitation:
|
|
9009
|
+
out["known_limitation"] = limitation
|
|
9010
|
+
return out
|
|
8942
9011
|
|
|
8943
9012
|
|
|
8944
9013
|
def _voice_isolation_capabilities(tl, p: Dict[str, Any]):
|
|
@@ -13530,6 +13599,106 @@ def _safe_import_media(mp, p: Dict[str, Any]):
|
|
|
13530
13599
|
_restore_current_folder(mp, previous)
|
|
13531
13600
|
|
|
13532
13601
|
|
|
13602
|
+
def _import_bounded_media(mp, ms, p: Dict[str, Any]):
|
|
13603
|
+
"""Create one bounded Media Pool item through MediaStorage.
|
|
13604
|
+
|
|
13605
|
+
Resolve's AddItemListToMediaPool itemInfo form is the public scripting API
|
|
13606
|
+
for this operation. ``media_storage.import_to_pool(item_infos)`` passes its
|
|
13607
|
+
``{media, startFrame, endFrame}`` maps straight through; this wrapper adds
|
|
13608
|
+
destination-folder scoping, naming, and created-item readback. ``start_frame``
|
|
13609
|
+
and ``end_frame`` are passed unchanged as Resolve's ``startFrame`` and
|
|
13610
|
+
``endFrame`` values; this wrapper does not infer or compensate for end-frame
|
|
13611
|
+
inclusivity. It imports into the *current* folder, so this helper deliberately
|
|
13612
|
+
scopes and restores that UI state around the one API call.
|
|
13613
|
+
"""
|
|
13614
|
+
source_path = p.get("source_path")
|
|
13615
|
+
if not isinstance(source_path, str) or not source_path:
|
|
13616
|
+
return _err("source_path must be a non-empty string", category="invalid_input")
|
|
13617
|
+
if not os.path.isabs(source_path):
|
|
13618
|
+
return _err("source_path must be an absolute path", category="invalid_input")
|
|
13619
|
+
path_err = _path_error(source_path, must_be_file=True)
|
|
13620
|
+
if path_err:
|
|
13621
|
+
return _err(path_err, category="invalid_input")
|
|
13622
|
+
|
|
13623
|
+
frames = {}
|
|
13624
|
+
for key in ("start_frame", "end_frame"):
|
|
13625
|
+
value = p.get(key)
|
|
13626
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
13627
|
+
return _err(f"{key} must be an integer", category="invalid_input")
|
|
13628
|
+
frames[key] = value
|
|
13629
|
+
if frames["start_frame"] < 0:
|
|
13630
|
+
return _err("start_frame must be greater than or equal to 0", category="invalid_input")
|
|
13631
|
+
if frames["end_frame"] < frames["start_frame"]:
|
|
13632
|
+
return _err("end_frame must be greater than or equal to start_frame", category="invalid_input")
|
|
13633
|
+
|
|
13634
|
+
destination_folder = p.get("destination_folder")
|
|
13635
|
+
if not isinstance(destination_folder, str) or not destination_folder:
|
|
13636
|
+
return _err("destination_folder must be a non-empty Media Pool folder path", category="invalid_input")
|
|
13637
|
+
if not _navigate_folder(mp, destination_folder):
|
|
13638
|
+
return _err(f"Target folder not found: {destination_folder}",
|
|
13639
|
+
code="FOLDER_NOT_FOUND", category="invalid_input",
|
|
13640
|
+
remediation=_FOLDER_ID_REMEDIATION)
|
|
13641
|
+
|
|
13642
|
+
name = p.get("name")
|
|
13643
|
+
if not isinstance(name, str) or not name.strip():
|
|
13644
|
+
return _err("name must be a non-empty string", category="invalid_input")
|
|
13645
|
+
|
|
13646
|
+
previous, folder_err = _set_current_folder_temporarily(mp, destination_folder)
|
|
13647
|
+
if folder_err:
|
|
13648
|
+
return folder_err
|
|
13649
|
+
try:
|
|
13650
|
+
item_info = {
|
|
13651
|
+
"media": source_path,
|
|
13652
|
+
"startFrame": frames["start_frame"],
|
|
13653
|
+
"endFrame": frames["end_frame"],
|
|
13654
|
+
}
|
|
13655
|
+
try:
|
|
13656
|
+
items = ms.AddItemListToMediaPool([item_info])
|
|
13657
|
+
except Exception as exc:
|
|
13658
|
+
return _err(f"AddItemListToMediaPool failed: {exc}")
|
|
13659
|
+
if not isinstance(items, list) or len(items) != 1:
|
|
13660
|
+
return _err(
|
|
13661
|
+
"Bounded import must return exactly one MediaPoolItem",
|
|
13662
|
+
code="BOUNDED_IMPORT_ITEM_COUNT_MISMATCH",
|
|
13663
|
+
category="resolve_api_failed",
|
|
13664
|
+
state={"returned_item_count": len(items) if isinstance(items, list) else None},
|
|
13665
|
+
)
|
|
13666
|
+
item = items[0]
|
|
13667
|
+
try:
|
|
13668
|
+
renamed = bool(item.SetName(name))
|
|
13669
|
+
except Exception as exc:
|
|
13670
|
+
return _err(
|
|
13671
|
+
f"Failed to rename bounded MediaPoolItem: {exc}; the item exists with Resolve's default name",
|
|
13672
|
+
state={"item": _media_pool_item_summary(item)},
|
|
13673
|
+
)
|
|
13674
|
+
if not renamed:
|
|
13675
|
+
return _err(
|
|
13676
|
+
"Failed to rename bounded MediaPoolItem; the item exists with Resolve's default name",
|
|
13677
|
+
state={"item": _media_pool_item_summary(item)},
|
|
13678
|
+
)
|
|
13679
|
+
|
|
13680
|
+
properties, _ = _safe_clip_call(item, "GetClipProperty", "")
|
|
13681
|
+
range_property_keys = (
|
|
13682
|
+
"File Path", "FPS", "Frames", "Duration", "Start TC", "End TC",
|
|
13683
|
+
"Sub Clip", "SubClip", "Is Sub Clip", "IsSubClip", "Is Subclip",
|
|
13684
|
+
)
|
|
13685
|
+
bounded_import_properties = {
|
|
13686
|
+
key: properties[key]
|
|
13687
|
+
for key in range_property_keys
|
|
13688
|
+
if isinstance(properties, dict) and properties.get(key) not in (None, "")
|
|
13689
|
+
}
|
|
13690
|
+
return _ok(
|
|
13691
|
+
source_path=source_path,
|
|
13692
|
+
start_frame=frames["start_frame"],
|
|
13693
|
+
end_frame=frames["end_frame"],
|
|
13694
|
+
destination_folder=destination_folder,
|
|
13695
|
+
item=_media_pool_item_summary(item),
|
|
13696
|
+
bounded_import_properties=bounded_import_properties,
|
|
13697
|
+
)
|
|
13698
|
+
finally:
|
|
13699
|
+
_restore_current_folder(mp, previous)
|
|
13700
|
+
|
|
13701
|
+
|
|
13533
13702
|
def _safe_import_sequence(mp, p: Dict[str, Any]):
|
|
13534
13703
|
pattern = p.get("FilePath") or p.get("file_path") or p.get("pattern")
|
|
13535
13704
|
if not pattern:
|
|
@@ -21438,6 +21607,14 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21438
21607
|
— image sequences: params.clip_infos is a list of
|
|
21439
21608
|
{FilePath, StartIndex, EndIndex} dicts (PascalCase keys per Resolve docs).
|
|
21440
21609
|
Example: [{"FilePath": "frame_%03d.dpx", "StartIndex": 1, "EndIndex": 100}]
|
|
21610
|
+
import_bounded_media(source_path, start_frame, end_frame, destination_folder, name)
|
|
21611
|
+
-> {success, source_path, start_frame, end_frame, destination_folder, item,
|
|
21612
|
+
bounded_import_properties}
|
|
21613
|
+
— creates one bounded Media Pool item through
|
|
21614
|
+
MediaStorage.AddItemListToMediaPool([{media, startFrame, endFrame}]).
|
|
21615
|
+
start_frame/end_frame pass through unchanged as raw Resolve API values;
|
|
21616
|
+
observed range semantics are reported from the created item properties.
|
|
21617
|
+
destination_folder must already exist; the previous current folder is restored.
|
|
21441
21618
|
delete_clips(clip_ids) -> {success}
|
|
21442
21619
|
DESTRUCTIVE. Removes clips from the Media Pool (does not touch source files).
|
|
21443
21620
|
An id matching no clip fails the whole call (CLIP_NOT_FOUND) rather than
|
|
@@ -21752,6 +21929,14 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21752
21929
|
return _err("Provide paths (simple) or clip_infos (image sequences)")
|
|
21753
21930
|
result = mp.ImportMedia(paths)
|
|
21754
21931
|
return {"imported": len(result) if result else 0}
|
|
21932
|
+
elif action == "import_bounded_media":
|
|
21933
|
+
# _get_mp() returns the ProjectManager as its first value, not the
|
|
21934
|
+
# Resolve application object. MediaStorage belongs to Resolve.
|
|
21935
|
+
resolve = get_resolve()
|
|
21936
|
+
ms = resolve.GetMediaStorage() if resolve else None
|
|
21937
|
+
if not ms:
|
|
21938
|
+
return _err("Failed to get MediaStorage")
|
|
21939
|
+
return _import_bounded_media(mp, ms, p)
|
|
21755
21940
|
elif action == "delete_clips":
|
|
21756
21941
|
clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="deleted")
|
|
21757
21942
|
if clips_err:
|
|
@@ -21887,7 +22072,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21887
22072
|
return _copy_clip_annotations(root, p)
|
|
21888
22073
|
elif action == "media_pool_boundary_report":
|
|
21889
22074
|
return _media_pool_boundary_report(mp, p)
|
|
21890
|
-
return _unknown(action, ["create_multicam_clip","get_root_folder","get_current_folder","set_current_folder","add_subfolder","delete_folders","move_folders","refresh","create_timeline","create_timeline_from_clips","import_timeline","delete_timelines","append_to_timeline","import_media","delete_clips","move_clips","relink","unlink","export_metadata","get_unique_id","create_stereo_clip","auto_sync_audio","get_selected","set_selected","get_clip_mattes","get_timeline_mattes","delete_clip_mattes","import_folder",*_MEDIA_POOL_KERNEL_ACTIONS])
|
|
22075
|
+
return _unknown(action, ["create_multicam_clip","get_root_folder","get_current_folder","set_current_folder","add_subfolder","delete_folders","move_folders","refresh","create_timeline","create_timeline_from_clips","import_timeline","delete_timelines","append_to_timeline","import_media","import_bounded_media","delete_clips","move_clips","relink","unlink","export_metadata","get_unique_id","create_stereo_clip","auto_sync_audio","get_selected","set_selected","get_clip_mattes","get_timeline_mattes","delete_clip_mattes","import_folder",*_MEDIA_POOL_KERNEL_ACTIONS])
|
|
21891
22076
|
|
|
21892
22077
|
|
|
21893
22078
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -25896,7 +26081,10 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
25896
26081
|
audio_capabilities() -> {supported, partially_supported, unsupported}
|
|
25897
26082
|
probe_audio_item(track_type?, track_index?, item_index?) -> {summary, audio_properties, source_audio_mapping}
|
|
25898
26083
|
probe_audio_track(track_index?) -> {track_count, enabled, locked, sub_type, voice_isolation}
|
|
25899
|
-
safe_set_audio_properties(properties, restore?, dry_run?, track_type?, track_index?, item_index?) -> {success, results}
|
|
26084
|
+
safe_set_audio_properties(properties, restore?, dry_run?, track_type?, track_index?, item_index?) -> {success, results, known_limitation?}
|
|
26085
|
+
— Volume/Pan/EQ writes are not honoured by Resolve's API; a request for
|
|
26086
|
+
any of them returns `known_limitation` with the bake / Fairlight-preset
|
|
26087
|
+
workaround. AudioSyncOffset writes do work.
|
|
25900
26088
|
audio_mix_capability_report(...) -> {capabilities, mix_recommendations}
|
|
25901
26089
|
voice_isolation_capabilities(track_index?, track_type?, item_index?) -> {timeline_track, item}
|
|
25902
26090
|
audio_mapping_report(clip_ids?) -> {timeline_items, media_pool_items}
|
|
@@ -26975,7 +27163,10 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
26975
27163
|
get_composite(...) -> {Opacity, CompositeMode}
|
|
26976
27164
|
set_composite(Opacity?, CompositeMode?, ...) -> {success}
|
|
26977
27165
|
get_audio(...) -> {Volume, Pan, AudioSyncOffset, ...}
|
|
26978
|
-
set_audio(Volume?, Pan?, ...) -> {success}
|
|
27166
|
+
set_audio(Volume?, Pan?, ...) -> {success, known_limitation?}
|
|
27167
|
+
— Resolve ignores Volume/Pan/EQ writes on audio; those return a
|
|
27168
|
+
`known_limitation` block (bake gain into the media, or apply a
|
|
27169
|
+
Fairlight preset). AudioSyncOffset / AudioSyncOffsetIsManual do work.
|
|
26979
27170
|
get_keyframes(property, ...) -> {property, count, keyframes}
|
|
26980
27171
|
add_keyframe(property, frame, value, ...) -> {success}
|
|
26981
27172
|
modify_keyframe(property, frame, new_value?, new_frame?, ...) -> {success}
|
|
@@ -27210,7 +27401,15 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
27210
27401
|
for k, v in p.items():
|
|
27211
27402
|
if k in valid:
|
|
27212
27403
|
results[k] = bool(item.SetProperty(k, v))
|
|
27213
|
-
|
|
27404
|
+
if not results:
|
|
27405
|
+
return _err(f"Specify one or more of: {', '.join(sorted(valid))}")
|
|
27406
|
+
out = _ok(**results)
|
|
27407
|
+
# Volume/Pan writes go nowhere on audio (see _audio_write_limitation);
|
|
27408
|
+
# AudioSyncOffset does work, so only flag when a level/pan key was asked.
|
|
27409
|
+
limitation = _audio_write_limitation(results)
|
|
27410
|
+
if limitation:
|
|
27411
|
+
out["known_limitation"] = limitation
|
|
27412
|
+
return out
|
|
27214
27413
|
|
|
27215
27414
|
# ── Keyframes ──
|
|
27216
27415
|
elif action == "get_keyframes":
|
|
@@ -236,6 +236,8 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
236
236
|
("media_pool", "create_timeline"),
|
|
237
237
|
("media_pool", "create_timeline_from_clips"),
|
|
238
238
|
("media_pool", "create_stereo_clip"),
|
|
239
|
+
# Adds one bounded item; the source and existing pool contents remain intact.
|
|
240
|
+
("media_pool", "import_bounded_media"),
|
|
239
241
|
# Per-item display properties: set them back and the item is as it was.
|
|
240
242
|
("timeline_item", "set_clip_enabled"),
|
|
241
243
|
("timeline_item", "set_name"),
|