davinci-resolve-mcp 4.8.29 → 4.9.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 +75 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +13 -0
- 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/server.py +118 -2
- package/src/utils/conform_lint.py +13 -1
- package/src/utils/destructive_hook.py +1 -0
- package/src/utils/execution_lifecycle.py +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,81 @@
|
|
|
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.0 — bounded Media Pool import
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`media_pool(action="import_bounded_media")`** creates one bounded Media
|
|
10
|
+
Pool item from a source file through
|
|
11
|
+
`MediaStorage.AddItemListToMediaPool([{media, startFrame, endFrame}])`.
|
|
12
|
+
Params: `source_path` (absolute, must exist), `start_frame`, `end_frame`,
|
|
13
|
+
`destination_folder` (must already exist) and `name`. The import lands in
|
|
14
|
+
the destination folder, which is made current only for the one call; the
|
|
15
|
+
previous current folder is restored afterwards, including when the import
|
|
16
|
+
fails. The result carries the item summary and the range properties
|
|
17
|
+
Resolve reports for it (`bounded_import_properties`). `start_frame` and
|
|
18
|
+
`end_frame` are passed through unchanged; the wrapper does not adjust for
|
|
19
|
+
end-frame inclusivity. If the rename fails after Resolve has created the
|
|
20
|
+
item, the error says the item exists under Resolve's default name and
|
|
21
|
+
returns its id in `state`, so it can be renamed or deleted rather than
|
|
22
|
+
imported twice. `media_storage.import_to_pool(item_infos)` remains the
|
|
23
|
+
raw passthrough. Contributed by @dmourati (#275).
|
|
24
|
+
- Contributor-validated on Studio 21.1.1 Build 10: this itemInfo form created
|
|
25
|
+
a native subclip, and `endFrame` was inclusive (0–300 gave 301 frames).
|
|
26
|
+
That is one tested configuration, not a guarantee for other builds or
|
|
27
|
+
media types; not measured on this project's 19.1.3.7 machine.
|
|
28
|
+
|
|
29
|
+
### Safety
|
|
30
|
+
|
|
31
|
+
- The action is registered as a write (`destructive_hook`) and rated LOW
|
|
32
|
+
(`execution_lifecycle`): it only adds an item, and neither the source nor
|
|
33
|
+
existing pool contents change. Safe mode, the dry-run refusal and the
|
|
34
|
+
operation log now see it. `docs/reference/readwrite-symmetry.md`
|
|
35
|
+
regenerated.
|
|
36
|
+
|
|
37
|
+
### Tests
|
|
38
|
+
|
|
39
|
+
- `tests/test_media_pool_ingest_probe.py`: input validation never reaches
|
|
40
|
+
Resolve, the folder is restored on success and failure, `MediaStorage`
|
|
41
|
+
comes from the Resolve object rather than the project manager, and a
|
|
42
|
+
failed rename reports the created item.
|
|
43
|
+
- `tests/test_execution_lifecycle.py`: the action classifies as a
|
|
44
|
+
recognised, destructive, LOW-risk write.
|
|
45
|
+
|
|
46
|
+
### Validation
|
|
47
|
+
|
|
48
|
+
- Offline suite green. The live result above is the contributor's, on
|
|
49
|
+
Studio 21.1.1 Build 10.
|
|
50
|
+
|
|
51
|
+
## What's New in v4.8.30 — conform_lint reports every reuse of one source
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **`editorial(action="conform_lint")` reported only the first reuse when
|
|
56
|
+
several pulls came from inside one long pull.** `check_duplicate_usage`
|
|
57
|
+
sorted each source's pulls by in-point and compared each one only with the
|
|
58
|
+
pull just before it. A long take followed by short callbacks lifted from
|
|
59
|
+
inside it therefore compared the second callback against the first, which
|
|
60
|
+
it does not overlap, and the reuse of the long take went unreported from
|
|
61
|
+
then on. An interview used at 0–5000 with callbacks at 100–200 and 300–400
|
|
62
|
+
reported one reuse instead of two. Each pull is now compared with the
|
|
63
|
+
earlier pull that reaches furthest, so every pull that overlaps any earlier
|
|
64
|
+
pull is flagged. Where out-points already increase that is the previous
|
|
65
|
+
pull, so ordinary material is reported exactly as before; adjacent pulls
|
|
66
|
+
are still two shots, not a duplicate. Contributed by @Dev-next-gen (#276).
|
|
67
|
+
|
|
68
|
+
### Tests
|
|
69
|
+
|
|
70
|
+
- `tests/test_conform_lint.py` gains `DuplicateUsageTests`, the check's first
|
|
71
|
+
tests: adjacent pulls stay unreported, and every callback inside one long
|
|
72
|
+
pull is reported. The second fails on v4.8.29 with the
|
|
73
|
+
`INTERVIEW`/`CALLBACK 2` pair missing.
|
|
74
|
+
|
|
75
|
+
### Validation
|
|
76
|
+
|
|
77
|
+
- `lint_timeline` is a pure function over a timeline snapshot; no Resolve
|
|
78
|
+
scripting call changed, so no live run was required.
|
|
79
|
+
|
|
5
80
|
## What's New in v4.8.29 — optional granular arguments accept an explicit null
|
|
6
81
|
|
|
7
82
|
### 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.0 版 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
|
@@ -2194,6 +2194,19 @@ media_pool(action="safe_import_sequence", params={
|
|
|
2194
2194
|
"end_index": 1048,
|
|
2195
2195
|
"target_folder": "Master/Plates"
|
|
2196
2196
|
})
|
|
2197
|
+
media_pool(action="import_bounded_media", params={
|
|
2198
|
+
"source_path": "/absolute/path/source.mov",
|
|
2199
|
+
"start_frame": 100,
|
|
2200
|
+
"end_frame": 240,
|
|
2201
|
+
"destination_folder": "Master/Selects",
|
|
2202
|
+
"name": "Selected range"
|
|
2203
|
+
})
|
|
2204
|
+
# start_frame/end_frame are raw Resolve startFrame/endFrame values. The MCP does
|
|
2205
|
+
# not add or subtract a frame; inspect bounded_import_properties after creation.
|
|
2206
|
+
# Contributor-validated on DaVinci Resolve Studio 21.1.1 Build 10 (#275): this
|
|
2207
|
+
# itemInfo form created a native subclip, and endFrame was inclusive (0–300
|
|
2208
|
+
# reported 301 frames). Treat that as an observed configuration-specific result,
|
|
2209
|
+
# not a guarantee for every Resolve version or media type.
|
|
2197
2210
|
media_pool(action="media_pool_boundary_report", params={"selected": True, "depth": 2})
|
|
2198
2211
|
# Positioned append (MediaPool.AppendToTimeline([{clipInfo}, ...])) — e.g. rebuild a subtitle row after delete_clips
|
|
2199
2212
|
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.0"
|
|
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.0"
|
|
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()}")
|
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.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -13530,6 +13530,106 @@ def _safe_import_media(mp, p: Dict[str, Any]):
|
|
|
13530
13530
|
_restore_current_folder(mp, previous)
|
|
13531
13531
|
|
|
13532
13532
|
|
|
13533
|
+
def _import_bounded_media(mp, ms, p: Dict[str, Any]):
|
|
13534
|
+
"""Create one bounded Media Pool item through MediaStorage.
|
|
13535
|
+
|
|
13536
|
+
Resolve's AddItemListToMediaPool itemInfo form is the public scripting API
|
|
13537
|
+
for this operation. ``media_storage.import_to_pool(item_infos)`` passes its
|
|
13538
|
+
``{media, startFrame, endFrame}`` maps straight through; this wrapper adds
|
|
13539
|
+
destination-folder scoping, naming, and created-item readback. ``start_frame``
|
|
13540
|
+
and ``end_frame`` are passed unchanged as Resolve's ``startFrame`` and
|
|
13541
|
+
``endFrame`` values; this wrapper does not infer or compensate for end-frame
|
|
13542
|
+
inclusivity. It imports into the *current* folder, so this helper deliberately
|
|
13543
|
+
scopes and restores that UI state around the one API call.
|
|
13544
|
+
"""
|
|
13545
|
+
source_path = p.get("source_path")
|
|
13546
|
+
if not isinstance(source_path, str) or not source_path:
|
|
13547
|
+
return _err("source_path must be a non-empty string", category="invalid_input")
|
|
13548
|
+
if not os.path.isabs(source_path):
|
|
13549
|
+
return _err("source_path must be an absolute path", category="invalid_input")
|
|
13550
|
+
path_err = _path_error(source_path, must_be_file=True)
|
|
13551
|
+
if path_err:
|
|
13552
|
+
return _err(path_err, category="invalid_input")
|
|
13553
|
+
|
|
13554
|
+
frames = {}
|
|
13555
|
+
for key in ("start_frame", "end_frame"):
|
|
13556
|
+
value = p.get(key)
|
|
13557
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
13558
|
+
return _err(f"{key} must be an integer", category="invalid_input")
|
|
13559
|
+
frames[key] = value
|
|
13560
|
+
if frames["start_frame"] < 0:
|
|
13561
|
+
return _err("start_frame must be greater than or equal to 0", category="invalid_input")
|
|
13562
|
+
if frames["end_frame"] < frames["start_frame"]:
|
|
13563
|
+
return _err("end_frame must be greater than or equal to start_frame", category="invalid_input")
|
|
13564
|
+
|
|
13565
|
+
destination_folder = p.get("destination_folder")
|
|
13566
|
+
if not isinstance(destination_folder, str) or not destination_folder:
|
|
13567
|
+
return _err("destination_folder must be a non-empty Media Pool folder path", category="invalid_input")
|
|
13568
|
+
if not _navigate_folder(mp, destination_folder):
|
|
13569
|
+
return _err(f"Target folder not found: {destination_folder}",
|
|
13570
|
+
code="FOLDER_NOT_FOUND", category="invalid_input",
|
|
13571
|
+
remediation=_FOLDER_ID_REMEDIATION)
|
|
13572
|
+
|
|
13573
|
+
name = p.get("name")
|
|
13574
|
+
if not isinstance(name, str) or not name.strip():
|
|
13575
|
+
return _err("name must be a non-empty string", category="invalid_input")
|
|
13576
|
+
|
|
13577
|
+
previous, folder_err = _set_current_folder_temporarily(mp, destination_folder)
|
|
13578
|
+
if folder_err:
|
|
13579
|
+
return folder_err
|
|
13580
|
+
try:
|
|
13581
|
+
item_info = {
|
|
13582
|
+
"media": source_path,
|
|
13583
|
+
"startFrame": frames["start_frame"],
|
|
13584
|
+
"endFrame": frames["end_frame"],
|
|
13585
|
+
}
|
|
13586
|
+
try:
|
|
13587
|
+
items = ms.AddItemListToMediaPool([item_info])
|
|
13588
|
+
except Exception as exc:
|
|
13589
|
+
return _err(f"AddItemListToMediaPool failed: {exc}")
|
|
13590
|
+
if not isinstance(items, list) or len(items) != 1:
|
|
13591
|
+
return _err(
|
|
13592
|
+
"Bounded import must return exactly one MediaPoolItem",
|
|
13593
|
+
code="BOUNDED_IMPORT_ITEM_COUNT_MISMATCH",
|
|
13594
|
+
category="resolve_api_failed",
|
|
13595
|
+
state={"returned_item_count": len(items) if isinstance(items, list) else None},
|
|
13596
|
+
)
|
|
13597
|
+
item = items[0]
|
|
13598
|
+
try:
|
|
13599
|
+
renamed = bool(item.SetName(name))
|
|
13600
|
+
except Exception as exc:
|
|
13601
|
+
return _err(
|
|
13602
|
+
f"Failed to rename bounded MediaPoolItem: {exc}; the item exists with Resolve's default name",
|
|
13603
|
+
state={"item": _media_pool_item_summary(item)},
|
|
13604
|
+
)
|
|
13605
|
+
if not renamed:
|
|
13606
|
+
return _err(
|
|
13607
|
+
"Failed to rename bounded MediaPoolItem; the item exists with Resolve's default name",
|
|
13608
|
+
state={"item": _media_pool_item_summary(item)},
|
|
13609
|
+
)
|
|
13610
|
+
|
|
13611
|
+
properties, _ = _safe_clip_call(item, "GetClipProperty", "")
|
|
13612
|
+
range_property_keys = (
|
|
13613
|
+
"File Path", "FPS", "Frames", "Duration", "Start TC", "End TC",
|
|
13614
|
+
"Sub Clip", "SubClip", "Is Sub Clip", "IsSubClip", "Is Subclip",
|
|
13615
|
+
)
|
|
13616
|
+
bounded_import_properties = {
|
|
13617
|
+
key: properties[key]
|
|
13618
|
+
for key in range_property_keys
|
|
13619
|
+
if isinstance(properties, dict) and properties.get(key) not in (None, "")
|
|
13620
|
+
}
|
|
13621
|
+
return _ok(
|
|
13622
|
+
source_path=source_path,
|
|
13623
|
+
start_frame=frames["start_frame"],
|
|
13624
|
+
end_frame=frames["end_frame"],
|
|
13625
|
+
destination_folder=destination_folder,
|
|
13626
|
+
item=_media_pool_item_summary(item),
|
|
13627
|
+
bounded_import_properties=bounded_import_properties,
|
|
13628
|
+
)
|
|
13629
|
+
finally:
|
|
13630
|
+
_restore_current_folder(mp, previous)
|
|
13631
|
+
|
|
13632
|
+
|
|
13533
13633
|
def _safe_import_sequence(mp, p: Dict[str, Any]):
|
|
13534
13634
|
pattern = p.get("FilePath") or p.get("file_path") or p.get("pattern")
|
|
13535
13635
|
if not pattern:
|
|
@@ -21438,6 +21538,14 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21438
21538
|
— image sequences: params.clip_infos is a list of
|
|
21439
21539
|
{FilePath, StartIndex, EndIndex} dicts (PascalCase keys per Resolve docs).
|
|
21440
21540
|
Example: [{"FilePath": "frame_%03d.dpx", "StartIndex": 1, "EndIndex": 100}]
|
|
21541
|
+
import_bounded_media(source_path, start_frame, end_frame, destination_folder, name)
|
|
21542
|
+
-> {success, source_path, start_frame, end_frame, destination_folder, item,
|
|
21543
|
+
bounded_import_properties}
|
|
21544
|
+
— creates one bounded Media Pool item through
|
|
21545
|
+
MediaStorage.AddItemListToMediaPool([{media, startFrame, endFrame}]).
|
|
21546
|
+
start_frame/end_frame pass through unchanged as raw Resolve API values;
|
|
21547
|
+
observed range semantics are reported from the created item properties.
|
|
21548
|
+
destination_folder must already exist; the previous current folder is restored.
|
|
21441
21549
|
delete_clips(clip_ids) -> {success}
|
|
21442
21550
|
DESTRUCTIVE. Removes clips from the Media Pool (does not touch source files).
|
|
21443
21551
|
An id matching no clip fails the whole call (CLIP_NOT_FOUND) rather than
|
|
@@ -21752,6 +21860,14 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21752
21860
|
return _err("Provide paths (simple) or clip_infos (image sequences)")
|
|
21753
21861
|
result = mp.ImportMedia(paths)
|
|
21754
21862
|
return {"imported": len(result) if result else 0}
|
|
21863
|
+
elif action == "import_bounded_media":
|
|
21864
|
+
# _get_mp() returns the ProjectManager as its first value, not the
|
|
21865
|
+
# Resolve application object. MediaStorage belongs to Resolve.
|
|
21866
|
+
resolve = get_resolve()
|
|
21867
|
+
ms = resolve.GetMediaStorage() if resolve else None
|
|
21868
|
+
if not ms:
|
|
21869
|
+
return _err("Failed to get MediaStorage")
|
|
21870
|
+
return _import_bounded_media(mp, ms, p)
|
|
21755
21871
|
elif action == "delete_clips":
|
|
21756
21872
|
clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="deleted")
|
|
21757
21873
|
if clips_err:
|
|
@@ -21887,7 +22003,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21887
22003
|
return _copy_clip_annotations(root, p)
|
|
21888
22004
|
elif action == "media_pool_boundary_report":
|
|
21889
22005
|
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])
|
|
22006
|
+
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
22007
|
|
|
21892
22008
|
|
|
21893
22009
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -337,7 +337,17 @@ def check_duplicate_usage(items: Sequence[Mapping[str, Any]]) -> List[Dict[str,
|
|
|
337
337
|
length = int(entry["timeline_end_frame"]) - int(entry["timeline_start_frame"])
|
|
338
338
|
spans.append((start, start + max(0, length), _name(entry)))
|
|
339
339
|
spans.sort()
|
|
340
|
-
|
|
340
|
+
# Each pull is compared against the furthest-reaching EARLIER pull, not
|
|
341
|
+
# against its immediate predecessor. Sorted by source in-point, one long
|
|
342
|
+
# pull is followed by every short callback lifted from inside it, so
|
|
343
|
+
# `spans[i - 1]` is the previous CALLBACK — which the next callback need
|
|
344
|
+
# not overlap — and the reuse of the long pull went unreported from the
|
|
345
|
+
# second callback onward. Where the out-points already increase the
|
|
346
|
+
# furthest pull *is* the predecessor, so ordinary material is unchanged.
|
|
347
|
+
furthest = spans[0]
|
|
348
|
+
for current in spans[1:]:
|
|
349
|
+
b_start, b_end, b_name = current
|
|
350
|
+
a_start, a_end, a_name = furthest
|
|
341
351
|
if b_start < a_end:
|
|
342
352
|
dupes.append({
|
|
343
353
|
"source": str(ref),
|
|
@@ -345,6 +355,8 @@ def check_duplicate_usage(items: Sequence[Mapping[str, Any]]) -> List[Dict[str,
|
|
|
345
355
|
"second": b_name,
|
|
346
356
|
"overlap_frames": min(a_end, b_end) - b_start,
|
|
347
357
|
})
|
|
358
|
+
if b_end > a_end:
|
|
359
|
+
furthest = current
|
|
348
360
|
if not dupes:
|
|
349
361
|
return []
|
|
350
362
|
return [_finding(
|
|
@@ -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"),
|