01 / BEFORE THE CODE
До кода — условия запуска.
Среда и документация
Пример рассчитан на внешний Python 3.9+ и запущенный Resolve Studio. Модуль Blackmagic берётся из установленного приложения. Для S05 сигнатуры дополнительно сверены с опубликованной копией DaVinciResolveScript.pyi 21.1; проверьте файл своей сборки, а не только старый README.txt.
Точная цель, не первый попавшийся клип
Получите ID проекта и таймлайна из предыдущих руководств; ID вхождения клипа — из карты S03. Имя нужно для визуальной сверки, но не для выбора цели. Индексы дорожек начинаются с 1 и могут изменяться после структурных правок.
Граница учебного опыта
Скрипт только читает данные и не имеет режима применения правки. Остановите параллельное редактирование на время сверки. Геометрические планы проверяют только целые кадры; исходники, эффекты, nested timeline и аудиосэмплы не пересчитываются.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
GetStart(True), GetEnd(True)- Вход
- Выбранное вхождение.
- Возвращает
- Координаты на таймлайне, включая дробные при наличии.
- Проверить
- План целых кадров не округляет дробное аудио.
GetDuration(True)- Вход
- То же вхождение.
- Возвращает
- Длительность в кадрах таймлайна.
- Проверить
- Требуется end - start == duration для нашей модели.
GetLeftOffset(True), GetRightOffset(True)- Вход
- Тот же объект.
- Возвращает
- Прочитанные доступные запасы края.
- Проверить
- Информационное чтение, не команда обрезки.
boundary_math(start, end, mode, ...)- Вход
- Явные целые координаты.
- Возвращает
- План геометрии без доступа к приложению.
- Проверить
- Никакого SplitClip / SetStart / SetEnd.
Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]
03 / UNDERSTAND
Что действительно подтверждено в API
В опубликованной копии DaVinciResolveScript.pyi 21.1 не объявлены универсальные Timeline.SplitClip / SplitClips и TimelineItem.Split / Trim, а также setter-пары SetStart / SetEnd / SetLeftOffset / SetRightOffset. Это результат проверки конкретного документа, а не обещание, что таких возможностей никогда не будет ни в одном выпуске.
Старый материал двигал playhead и отправлял Ctrl+B через pyautogui, PowerShell или xdotool. Это UI-автоматизация со своими зависимостями: фокус окна, раскладка горячих клавиш, выбранные дорожки и связанные клипы. Она не является нативным вызовом Scripting API и здесь полностью убрана.[14]
04 / UNDERSTAND
Кадр таймлайна и кадр исходника — не одно число
Параметры нашего плана относятся к абсолютным координатам API текущего таймлайна. GetStartFrame помогает отличить их от смещения от начала, а стартовый таймкод — от метки в системе часов. Если начало равно 86400, точка 86520 означает смещение 120 кадров, а не позицию 86520 в исходном файле.
При другой частоте исходника, ретайме, reverse, freeze frame, nested timeline или compound clip перенос координаты в source In/Out требует отдельного отображения времени. Этот пример его не угадывает. Даже совпадение длительностей не доказывает линейное соответствие кадров или сохранение синхронизации звука.[14]
05 / UNDERSTAND
Условия корректного разреза
Для [86400, 86640) разрез 86520 даёт [86400, 86520) и [86520, 86640), по 120 кадров. Точка разреза не принадлежит левой половине, но начинает правую. Сумма длительностей равна 240: кадр на границе не дублируется и не теряется.
Точка, равная start или end, отклоняется: одна половина была бы пустой. Вхождение с дробными координатами или несовпадением end-start и GetDuration тоже не приводится к целым молча. Сначала нужно квалифицировать его временные единицы и правило границы в установленном Resolve.
06 / UNDERSTAND
Разберите геометрию без риска для проекта
Поменяйте начало и конец учебного клипа, затем задайте точку разреза или оставляемый диапазон. Числа пересчитываются локально. При ошибке ввода прежние результаты исчезают — устаревший правильный ответ не остаётся рядом с новым ошибочным значением.
Видимые интервалы не подтверждают сохранность эффектов, linked audio, переходов или ретайма. Калькулятор не знает вашего проекта. Его назначение — проверить арифметику и терминологию перед самостоятельной работой на копии, а не выдать готовый монтажный вызов.
Учебные целокадровые координаты [начало, конец): конец не входит в диапазон. Здесь нет таймкода, пересчёта в исходник или подключения к Resolve. Это не предпросмотр правки вашего проекта.
- Исходная длительность
- 240
- Останется кадров
- 240
- Диапазоны плана
- [86400, 86520) + [86520, 86640)
A + B сохраняют длительность. Это не расчёт In/Out исходника и не вызов нативного метода разреза.
Только локальный расчёт. Ничего не сохраняется, не отправляется и не выполняется в Resolve.
07 / UNDERSTAND
Чем можно продолжить после плана
Ручной монтаж на тестовой копии — прозрачный способ выполнить подготовленную задачу. UI-автоматизация допустима только как отдельный адаптер с явным предупреждением о фокусе, горячих клавишах, выделении и проверке результата. В этом пакете такого адаптера нет.
Другой путь — собрать новый тестовый таймлайн из диапазонов через документированные API импорта и добавления. Это реконструкция, не in-place правка существующего TimelineItem. Для неё отдельно проверяются source mapping, границы, связи, эффекты, переходы, Color/Fusion и звук. Удаление оригинала не должно быть первым шагом.[14]
08 / UNDERSTAND
Проверьте документацию своей установки без запуска кода
Дополнительный audit_edit_contract.py читает локальный DaVinciResolveScript.pyi как текст и разбирает AST. Он не импортирует этот файл и не подключается к Resolve. В отчёте указаны SHA-256 документа, наличие проверяемых методов, строки и имена аргументов.
Результат document_only_not_live_verified означает только наличие описания в этом конкретном файле. Новая сигнатура потребует проверки её семантики и живого сценария на копии. Отсутствующий класс или некорректный файл — это не «методы отсутствуют», а незавершённый аудит с явной ошибкой.[15][14]
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Положите plan_split_clip.py, resolve_api_edit.py и resolve_api_connection.py в одну папку. В комплекте семь примеров, два helper, самостоятельный audit_edit_contract.py и инструкции RU/EN. Проверке документа нужны только Python и локальный .pyi; она не подключается к Resolve.
Скачать все примеры с инструкцией (.zip) ↓"""Read-only split plan. NO native edit, keyboard shortcut or reconstruction is executed."""
import argparse
from resolve_api_connection import connect
from resolve_api_edit import cli_target, inspect_boundary, print_result
def main():
p = argparse.ArgumentParser(description=__doc__)
cli_target(p)
p.add_argument("--item-id", required=True)
p.add_argument("--at", required=True, help="Absolute whole-frame API boundary")
a = p.parse_args()
return print_result(lambda: inspect_boundary(
connect(), a.project_id, a.timeline_id, a.item_id, "split", at=a.at))
if __name__ == "__main__":
raise SystemExit(main())
Edit-helper — точные цели, проверка плана и один вызов
Авторский учебный код, не SDK. Запись возможна только для разблокировки, одного удаления без ripple и добавления одной дорожки. Split и trim остаются чтением. Совпавшие структурные чтения не сертифицируют эффекты, маршрутизацию и весь проект; повторов и автоматического отката нет.
"""Bounded Resolve timeline tutorials, S05. Author code, NOT the Blackmagic SDK.
Only unlock, delete-one (non-ripple) and append-one-track can write to Resolve.
Plan fingerprints bind observed fields, not every project property. Reads are
not atomic, there is no transaction or native timeout, and no automatic retry,
rollback, save, UI automation or file deletion is performed here.
"""
from __future__ import annotations
import copy
import hashlib
import json
import math
import re
from fractions import Fraction
TRACK_TYPES = ("video", "audio", "subtitle")
AUDIO_FORMATS = ("mono", "stereo", "lrc", "lcr", "lrcs", "lcrs", "quad",
"5.0", "5.0film", "5.1", "5.1film", "7.0", "7.0film", "7.1", "7.1film") + tuple(
"adaptive%d" % n for n in range(1, 37))
MAX_TRACKS = 128 # Tutorial bounds, NOT Resolve product limits.
MAX_ITEMS = 5000
class PreflightError(RuntimeError):
"""No write is issued by this helper when preflight fails."""
def invoke(obj, method, *args):
fn = getattr(obj, method, None)
if not callable(fn):
raise PreflightError("Required method unavailable: " + method)
try:
return fn(*args)
except Exception as exc:
raise PreflightError("Read failed: " + method + ": " + type(exc).__name__) from exc
def ident(value, label):
if not isinstance(value, str) or not value.strip() or len(value) > 1024:
raise PreflightError("Missing or invalid " + label)
return value
def number(value, label):
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise PreflightError(label + " must be a finite API number (not None/bool/text)")
if isinstance(value, float) and not math.isfinite(value):
raise PreflightError(label + " is not finite")
return Fraction(str(value))
def count(value, label, maximum=MAX_TRACKS):
n = number(value, label)
if n.denominator != 1 or not 0 <= n <= maximum:
raise PreflightError(label + " must be an integer within this tutorial's bound")
return int(n)
def boolean(value, label):
if type(value) is not bool:
raise PreflightError(label + " must be an explicit boolean")
return value
def text(value, label):
if not isinstance(value, str):
raise PreflightError(label + " must be a string")
return value
def fingerprint(value):
raw = json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def get_target(resolve, project_id=None, timeline_id=None):
manager = invoke(resolve, "GetProjectManager")
if not manager:
raise PreflightError("Project manager unavailable")
project = invoke(manager, "GetCurrentProject")
if not project:
raise PreflightError("No open project")
timeline = invoke(project, "GetCurrentTimeline")
if not timeline:
raise PreflightError("No current timeline")
pid = ident(invoke(project, "GetUniqueId"), "project ID")
tid = ident(invoke(timeline, "GetUniqueId"), "timeline ID")
if project_id is not None and pid != project_id:
raise PreflightError("Active project does not match --project-id")
if timeline_id is not None and tid != timeline_id:
raise PreflightError("Active timeline does not match --timeline-id")
return project, timeline, pid, tid
def capture(resolve, project_id=None, timeline_id=None):
"""All top-level tracks/items, no recursive flattening and no source-file I/O."""
project, timeline, pid, tid = get_target(resolve, project_id, timeline_id)
state = {"project_id": pid, "timeline_id": tid,
"project_name": text(invoke(project, "GetName"), "project name"),
"timeline_name": text(invoke(timeline, "GetName"), "timeline name"),
"base_frame": str(number(invoke(timeline, "GetStartFrame"), "base frame")),
"start_timecode": text(invoke(timeline, "GetStartTimecode"), "start timecode"),
"tracks": []}
objects = {}
for kind in TRACK_TYPES:
n = count(invoke(timeline, "GetTrackCount", kind), "track count")
for index in range(1, n + 1):
track = {"type": kind, "index": index,
"name": text(invoke(timeline, "GetTrackName", kind, index), "track name"),
"locked": boolean(invoke(timeline, "GetIsTrackLocked", kind, index), "track lock"),
"enabled": boolean(invoke(timeline, "GetIsTrackEnabled", kind, index), "track enable"),
"items": []}
if kind == "audio":
track["format"] = ident(invoke(timeline, "GetTrackSubType", kind, index), "audio format")
items = invoke(timeline, "GetItemListInTrack", kind, index)
if not isinstance(items, list):
raise PreflightError("Item list unavailable; None is not an empty track")
if len(items) + len(objects) > MAX_ITEMS:
raise PreflightError("Snapshot exceeds tutorial item bound")
for item in items:
uid = ident(invoke(item, "GetUniqueId"), "timeline item ID")
if uid in objects:
raise PreflightError("Ambiguous duplicate timeline item ID")
start = number(invoke(item, "GetStart", True), "item start")
end = number(invoke(item, "GetEnd", True), "item end")
duration = number(invoke(item, "GetDuration", True), "item duration")
if end < start or duration < 0:
raise PreflightError("Inconsistent item geometry")
record = {"id": uid, "name": text(invoke(item, "GetName"), "item name"),
"start": str(start), "end_raw": str(end), "duration": str(duration),
"enabled": boolean(invoke(item, "GetClipEnabled"), "clip enabled")}
track["items"].append(record)
objects[uid] = item
track["items"].sort(key=lambda x: x["id"])
state["tracks"].append(track)
get_target(resolve, pid, tid) # Do not certify data after an observed target switch.
return state, timeline, objects
def stable_snapshot(resolve, project_id=None, timeline_id=None):
first, _, _ = capture(resolve, project_id, timeline_id)
second, timeline, objects = capture(resolve, first["project_id"], first["timeline_id"])
if first != second:
raise PreflightError("Observed structure changed during reads; stop and inspect")
return second, timeline, objects
def locate(state, item_id):
matches = [(track, item) for track in state["tracks"] for item in track["items"] if item["id"] == item_id]
if len(matches) != 1:
raise PreflightError("Exactly one item with --item-id is required; no name/range fallback")
return matches[0]
def linked_ids(item):
linked = invoke(item, "GetLinkedItems")
if not isinstance(linked, list):
raise PreflightError("Linked-item list unknown; no permission to delete")
own = ident(invoke(item, "GetUniqueId"), "item ID")
return sorted(set(ident(invoke(x, "GetUniqueId"), "linked item ID") for x in linked) - {own})
def checked_index(value):
if type(value) is not int or value < 1 or value > MAX_TRACKS:
raise PreflightError("Track index must be 1-based and within the tutorial bound")
return value
def build_plan(state, objects, operation, **options):
"""Pure policy except explicit read-only link/type checks for deletion."""
expected = copy.deepcopy(state)
plan = {"schema": "videomontazher.resolve.edit-plan.v1", "operation": operation,
"before": state, "options": options, "write_required": True,
"scope": "Top-level structural fields only; not an atomic project snapshot"}
if operation == "unlock":
kind, index = options.get("track_type"), checked_index(options.get("index"))
if kind not in TRACK_TYPES:
raise PreflightError("Unknown track type")
matches = [t for t in expected["tracks"] if t["type"] == kind and t["index"] == index]
if not matches:
raise PreflightError("Target track does not exist")
plan["write_required"] = matches[0]["locked"]
matches[0]["locked"] = False
plan["call"] = "Timeline.SetTrackLock(type, index, False)"
elif operation == "delete":
item_id = ident(options.get("item_id"), "target item ID")
track, record = locate(state, item_id)
if track["locked"]:
raise PreflightError("Target track is locked; unlocking is a separate reviewed task")
kind = invoke(objects[item_id], "GetType")
if kind not in ("video", "audio"):
raise PreflightError("This example deletes only an identified video/audio item")
links = linked_ids(objects[item_id])
if links:
raise PreflightError("Linked items exist; this single-item example will not unlink or expand scope")
plan["target_type"] = kind
plan["linked_ids"] = links
for t in expected["tracks"]:
t["items"] = [i for i in t["items"] if i["id"] != item_id]
plan["call"] = "Timeline.DeleteClips([exact_item], False)"
elif operation in ("ensure_audio", "ensure_video"):
kind = "audio" if operation == "ensure_audio" else "video"
index = checked_index(options.get("index"))
tracks = [t for t in state["tracks"] if t["type"] == kind]
fmt = options.get("audio_format")
if kind == "audio" and fmt not in AUDIO_FORMATS:
raise PreflightError("Explicit supported audio format required")
if index <= len(tracks):
if kind == "audio" and tracks[index - 1]["format"] != fmt:
raise PreflightError("Existing track format differs; not converting or replacing it")
plan["write_required"] = False
elif index != len(tracks) + 1:
raise PreflightError("Only the next missing track can be added in one plan; no fill loop")
plan["track_type"] = kind
plan["call"] = "Timeline.AddTrack('audio', explicit_format)" if kind == "audio" else "Timeline.AddTrack('video')"
expected = None # New track name/initial flags are read, not predicted.
else:
raise PreflightError("Unknown operation")
plan["expected_structure"] = expected
plan["token"] = fingerprint(plan)
return plan
def verify_added(before, after, kind, index, fmt):
old = before["tracks"]
new = [t for t in after["tracks"] if t["type"] == kind and t["index"] == index]
if len(new) != 1 or new[0]["items"] or (kind == "audio" and new[0]["format"] != fmt):
return False
stripped = copy.deepcopy(after)
stripped["tracks"] = [t for t in after["tracks"] if not (t["type"] == kind and t["index"] == index)]
return len(after["tracks"]) == len(old) + 1 and stripped == before
def run_action(resolve, project_id, timeline_id, operation, apply=False, expect_plan=None, test_copy=False, **options):
ident(project_id, "explicit --project-id")
ident(timeline_id, "explicit --timeline-id")
before, timeline, objects = stable_snapshot(resolve, project_id, timeline_id)
plan = build_plan(before, objects, operation, **options)
if not apply:
return {"status": "plan_only", "plan": plan}
if not test_copy or not isinstance(expect_plan, str) or expect_plan != plan["token"]:
raise PreflightError("Apply requires --test-copy and the exact reviewed --expect-plan token")
# Re-read immediately before dispatch, including links/type for deletion.
fresh, timeline, objects = stable_snapshot(resolve, project_id, timeline_id)
fresh_plan = build_plan(fresh, objects, operation, **options)
if fresh_plan["token"] != expect_plan:
raise PreflightError("Plan became stale before dispatch; no write issued")
if not fresh_plan["write_required"]:
return {"status": "already_satisfied", "write_issued": False, "plan": fresh_plan}
if operation == "unlock":
method, args = "SetTrackLock", (options["track_type"], options["index"], False)
elif operation == "delete":
method, args = "DeleteClips", ([objects[options["item_id"]]], False)
elif operation == "ensure_audio":
method, args = "AddTrack", ("audio", options["audio_format"])
else:
method, args = "AddTrack", ("video",)
fn = getattr(timeline, method, None)
if not callable(fn):
raise PreflightError("Documented write method unavailable; no alternative is attempted")
# From this point even False / an exception may accompany a side effect.
returned, call_error = None, None
try:
returned = fn(*args) # ONE native write. Never retry automatically.
except Exception as exc:
call_error = type(exc).__name__
result = {"status": "outcome_unconfirmed", "write_issued": True,
"method": method, "returned_true": returned is True,
"call_error": call_error, "plan_token": expect_plan,
"structural_match": False,
"next": "Read actual state and inspect Resolve. Do not blindly retry or roll back."}
try:
after, _, _ = stable_snapshot(resolve, project_id, timeline_id)
result["after"] = after
if operation.startswith("ensure_"):
matched = verify_added(before, after, fresh_plan["track_type"], options["index"], options.get("audio_format"))
else:
matched = after == fresh_plan["expected_structure"]
result["structural_match"] = matched
if matched and returned is True and call_error is None:
result["status"] = "verified_observed_scope"
result["next"] = "Confirm the edit in Resolve. Unobserved properties/effects are not certified."
except Exception as exc:
result["readback_error"] = type(exc).__name__ + ": " + str(exc)
return result
def input_frame(value, label):
if isinstance(value, bool) or not isinstance(value, (str, int)):
raise PreflightError(label + " must be integer frame text")
value = str(value)
if not value or len(value) > 18 or value.strip() != value or re.fullmatch(r"-?[0-9]+", value) is None:
raise PreflightError(label + " must be integer frame text")
n = int(value)
if abs(n) > 10**12:
raise PreflightError(label + " exceeds tutorial bound")
return n
def boundary_math(start, end, mode, at=None, new_start=None, new_end=None):
"""Half-open whole-frame mathematical plan, no source-range conversion."""
start, end = input_frame(start, "start"), input_frame(end, "end")
if end <= start:
raise PreflightError("A positive [start, end) interval is required")
if mode == "split":
at = input_frame(at, "cut")
if not start < at < end:
raise PreflightError("Cut must be strictly inside the item, not on an edge")
return {"mode": mode, "before": [start, end], "segments": [[start, at], [at, end]],
"durations": [at - start, end - at], "duration_preserved": True}
if mode == "trim":
new_start, new_end = input_frame(new_start, "new start"), input_frame(new_end, "new end")
if not start <= new_start < new_end <= end:
raise PreflightError("Trim must keep a nonempty subinterval; extending handles is outside this plan")
return {"mode": mode, "before": [start, end], "keep": [new_start, new_end],
"remove_head": new_start - start, "remove_tail": end - new_end,
"duration": new_end - new_start, "unchanged": [new_start, new_end] == [start, end]}
raise PreflightError("Only split/trim mathematical plans are supported")
def inspect_boundary(resolve, project_id, timeline_id, item_id, mode, **values):
state, _, objects = stable_snapshot(resolve, project_id, timeline_id)
track, item = locate(state, ident(item_id, "item ID"))
start, end, duration = (Fraction(item[k]) for k in ("start", "end_raw", "duration"))
if any(x.denominator != 1 for x in (start, end, duration)) or end - start != duration:
raise PreflightError("This whole-frame model requires end-start == duration. No rounding or boundary guessing.")
geometry = boundary_math(int(start), int(end), mode, **values)
report = {"schema": "videomontazher.resolve.boundary-plan.v1", "status": "read_only_plan_no_executor",
"project_id": state["project_id"], "timeline_id": state["timeline_id"],
"track": {"type": track["type"], "index": track["index"], "locked": track["locked"]},
"item": item, "geometry": geometry, "snapshot_token": fingerprint(state),
"source_mapping": "not_calculated", "write_issued": False,
"next": "Manual operation on a copy, or a separately qualified reconstruction adapter. No implicit UI keys."}
# Optional context is reported explicitly, never silently interpreted as zero.
for name in ("GetLeftOffset", "GetRightOffset"):
try:
report[name] = {"status": "read", "frames_raw": str(number(invoke(objects[item_id], name, True), name))}
except Exception as exc:
report[name] = {"status": "unavailable", "error": type(exc).__name__}
fresh, _, _ = stable_snapshot(resolve, state["project_id"], state["timeline_id"])
if fresh != state:
raise PreflightError("Structure changed while preparing the boundary plan")
return report
def read_audio_tracks(resolve, project_id=None, timeline_id=None):
"""Targeted inventory: does not require clip access, buses or media decoding."""
def read():
project, timeline, pid, tid = get_target(resolve, project_id, timeline_id)
n = count(invoke(timeline, "GetTrackCount", "audio"), "audio track count")
rows = []
for index in range(1, n + 1):
rows.append({"index": index,
"name": text(invoke(timeline, "GetTrackName", "audio", index), "track name"),
"format": ident(invoke(timeline, "GetTrackSubType", "audio", index), "audio format"),
"enabled": boolean(invoke(timeline, "GetIsTrackEnabled", "audio", index), "track enable"),
"locked": boolean(invoke(timeline, "GetIsTrackLocked", "audio", index), "track lock")})
get_target(resolve, pid, tid)
return {"project_id": pid, "timeline_id": tid, "audio_track_count": n, "tracks": rows,
"scope": "Track inventory only; not clip channels, buses, routing or audible output"}
first, second = read(), read()
if first != second:
raise PreflightError("Audio track inventory changed during reads")
return second
def cli_target(parser, required=True):
parser.add_argument("--project-id", required=required)
parser.add_argument("--timeline-id", required=required)
def cli_write(parser):
parser.add_argument("--apply", action="store_true", help="Issue at most one write after review")
parser.add_argument("--expect-plan", help="Exact token from a separately reviewed plan")
parser.add_argument("--test-copy", action="store_true", help="Acknowledge this is a disposable test copy")
def print_result(fn):
try:
result = fn()
except Exception as exc:
print(json.dumps({"status": "stopped", "error": type(exc).__name__ + ": " + str(exc)}, indent=2))
return 2
print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
return 3 if result.get("status") == "outcome_unconfirmed" else 0
Дополнительно: проверить локальный .pyi без исполнения
Самостоятельный проверяющий разбирает только ограниченный локальный файл интерфейса. Объявления относятся к этому документу, а не к живой проверке возможностей или разрешению менять проект. Версия Python должна поддерживать синтаксис файла.
"""Read a LOCAL .pyi as text; never import or execute it. No connection to Resolve.
Presence is documentation evidence, not a live capability test. Missing names
refer only to this exact file, not every past or future Resolve release.
"""
import argparse
import ast
import hashlib
import json
from pathlib import Path
CHECKS = {
"Timeline": ["GetTrackCount", "AddTrack", "GetTrackSubType", "SetTrackLock",
"GetIsTrackLocked", "DeleteClips", "SplitClip", "SplitClips", "TrimClip"],
"TimelineItem": ["GetType", "GetLinkedItems", "GetStart", "GetEnd", "GetDuration",
"GetLeftOffset", "GetRightOffset", "SetStart", "SetEnd",
"SetLeftOffset", "SetRightOffset", "Split", "Trim"],
}
def audit(path):
path = Path(path)
if path.suffix.lower() != ".pyi":
raise ValueError("Pass the installed DaVinciResolveScript.pyi, not a script or binary")
if not path.is_file():
raise ValueError("A regular local .pyi file is required")
with path.open("rb") as stream:
raw = stream.read(2 * 1024 * 1024 + 1)
if len(raw) > 2 * 1024 * 1024:
raise ValueError("File exceeds this tutorial's 2 MiB bound")
tree = ast.parse(raw.decode("utf-8-sig"), filename=path.name)
result = {}
for name, wanted in CHECKS.items():
classes = [n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == name]
if len(classes) != 1:
raise ValueError("Expected one top-level class: " + name)
methods = {n.name: n for n in classes[0].body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))}
result[name] = {key: {"documented_in_this_file": key in methods,
"line": methods[key].lineno if key in methods else None,
"parameters": [a.arg for a in methods[key].args.args] if key in methods else []}
for key in wanted}
return {"schema": "videomontazher.resolve.local-contract-audit.v1",
"file_name": path.name, "sha256": hashlib.sha256(raw).hexdigest(),
"bytes": len(raw), "status": "document_only_not_live_verified", "classes": result}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--doc", type=Path, required=True)
args = parser.parse_args()
try:
print(json.dumps(audit(args.doc), ensure_ascii=False, indent=2))
return 0
except Exception as exc:
print(json.dumps({"status": "not_audited", "error": type(exc).__name__ + ": " + str(exc)}))
return 2
if __name__ == "__main__":
raise SystemExit(main())
Общий helper подключения — посмотреть и скачать
Этот файл находит и загружает установленный мост. При импорте Python может записывать кэш байткода, а нативный вызов — зависнуть; это не песочница и не механизм тайм-аута. Используйте доверенную локальную установку.
"""Load the installed Blackmagic module; do not install a similarly named package.
This helper uses standard desktop installation paths or RESOLVE_SCRIPT_API.
It does not enable scripting, start Resolve, open projects or retry API calls.
Validate Python/native-library compatibility against your installed README.txt.
"""
from __future__ import annotations
import importlib.util
import os
from pathlib import Path
import struct
import sys
class ResolveConnectionError(RuntimeError):
"""An explicit environment, module, or application-connection failure."""
def scripting_root() -> Path:
configured = os.environ.get("RESOLVE_SCRIPT_API")
if configured is not None:
if not configured.strip() or configured != configured.strip():
raise ResolveConnectionError("RESOLVE_SCRIPT_API is empty or has outer whitespace")
if configured.startswith(('"', "'")) or configured.endswith(('"', "'")):
raise ResolveConnectionError("Do not store literal outer quotes in RESOLVE_SCRIPT_API")
root = Path(os.path.expandvars(configured)).expanduser()
elif sys.platform == "win32":
root = Path(os.environ.get("PROGRAMDATA", r"C:\ProgramData")) / "Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting"
elif sys.platform == "darwin":
root = Path("/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting")
elif sys.platform.startswith("linux"):
root = Path("/opt/resolve/Developer/Scripting")
else:
raise ResolveConnectionError("Set RESOLVE_SCRIPT_API for this platform/installation")
if not root.is_absolute():
raise ResolveConnectionError("RESOLVE_SCRIPT_API must be an absolute Scripting directory")
return root
def module_file() -> Path:
# RESOLVE_SCRIPT_API points to Scripting, not its Modules subdirectory.
return scripting_root() / "Modules" / "DaVinciResolveScript.py"
def load_resolve_module():
if struct.calcsize("P") * 8 != 64:
raise ResolveConnectionError("Use a compatible 64-bit Python interpreter")
source = module_file()
if not source.is_file():
raise ResolveConnectionError("Official DaVinciResolveScript.py not found at: " + str(source))
configured_lib = os.environ.get("RESOLVE_SCRIPT_LIB")
if configured_lib is not None:
lib = Path(os.path.expandvars(configured_lib)).expanduser()
if not lib.is_absolute() or not lib.is_file():
raise ResolveConnectionError("RESOLVE_SCRIPT_LIB must name the installed native library file")
name = "DaVinciResolveScript"
spec = importlib.util.spec_from_file_location(name, source)
if spec is None or spec.loader is None:
raise ResolveConnectionError("Cannot create a loader for the official module")
previous = sys.modules.get(name)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
try:
spec.loader.exec_module(module)
# Blackmagic's wrapper can replace its sys.modules entry with a native module.
loaded = sys.modules[name]
if not callable(getattr(loaded, "scriptapp", None)):
raise ResolveConnectionError("Loaded module does not expose scriptapp")
return loaded
except Exception as exc:
if previous is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = previous
raise ResolveConnectionError("Official module was found, but loading it failed") from exc
def connect():
module = load_resolve_module()
try:
resolve = module.scriptapp("Resolve")
except Exception as exc:
raise ResolveConnectionError("scriptapp failed; inspect the original exception") from exc
if not resolve:
raise ResolveConnectionError("No Resolve object: check running Studio, Local access and Python environment")
return resolve
def project_manager(resolve):
manager = resolve.GetProjectManager()
if not manager:
raise ResolveConnectionError("Resolve connected, but Project Manager is unavailable")
return manager
Команды запуска
Получить план без правки
python plan_split_clip.py --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID" --item-id "ITEM_ID" --at 86520Числа — учебный пример, замените их прочитанными координатами своей тестовой цели.
Проверить локальные объявления
python audit_edit_contract.py --doc "PATH_TO/DaVinciResolveScript.pyi"Это дополнительный скрипт из комплекта, не модуль Blackmagic. Никакого импорта .pyi или вызовов Resolve.
Что делает код
Установить контекст
Чтение начинается с проекта и таймлайна; отсутствие объекта отличается от корректного пустого состояния.
Проверить реальные значения
Числа и списки проверяются по типу и диапазону. Ошибка метода не превращается в ноль, пустой список или совпадение с первым клипом.
Сформировать отчёт
Отчёт оставляет идентичность и единицы явными. Математический план показывает желаемую геометрию, но не формирует разрешение на её применение.
Сверить самостоятельно
Проверьте результат в интерфейсе. Скрипт не запускает клавиатурные команды, не меняет границы клипа и не пересобирает монтаж вместо вас.
Офлайн-тесты проверяют авторскую логику на двойниках API. Нативные вызовы, внешние Python-сборки и поведение конкретного Resolve нужно отдельно проверить на своей системе.
Чтение, проверка схемы и построение плана не равны выполнению монтажа. Не добавляйте неподтверждённый setter, удаление с повторным импортом или горячую клавишу как незаметный fallback.
Отчёт может содержать имена проектов, дорожек и клипов. Обезличьте его вручную перед публикацией; скрипт не отправляет данные в интернет и не обезличивает их автоматически.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
Параметр --apply не принимается
Возможная причинаВ этой статье нет исполнителя изменения границ.
Следующая проверкаЭто ожидаемый отказ argparse. Используйте план для отдельной ручной проверки, не добавляйте неподтверждённый setter.
Граница вне клипа
Возможная причинаКоордината перепутана со смещением или диапазон выходит за исходный.
Следующая проверкаПроверьте начало API и [start, end), затем задайте целые координаты в той же системе.
end-start не равен duration
Возможная причинаОбъект не соответствует целокадровой модели этого учебного примера.
Следующая проверкаСохраните сырые значения и проверьте тип, субкадры и документ установленной версии. Не добавляйте +1 вслепую.
Документ не прошёл аудит
Возможная причинаПередан другой файл, отсутствует класс или файл не разбирается как .pyi.
Следующая проверкаНайдите установленную спецификацию. Ошибка разбора не подтверждает отсутствие метода в Resolve.
PRACTICE / YOUR OWN EVIDENCE
Сверьте план с ручным действием
- Выберите тестовый клип с целыми границами; запишите его ID, start, end и duration.
- Запустите read-only пример и проверьте, что write_issued равно false.
- В отдельной копии вручную выполните согласованную геометрию и сравните длительности.
- Проверьте звук, эффекты и соседние элементы отдельно: план их сохранность не подтверждает.
План верно описывает интервалы, исходный скрипт ничего не меняет, а ручная приёмка отделена от арифметики.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Получится разрезать клип этим скриптом?
Нет. Он читает точный объект и строит проверяемый план. В проверенной схеме не подтверждён универсальный нативный исполнитель для этой задачи, поэтому приложение остаётся без изменений.[14]
Почему не оставить горячую клавишу?
Клавиша зависит от фокуса, раскладки команд, выделения и целевых дорожек. Это отдельная UI-автоматизация, а не Scripting API. Её нельзя скрывать внутри функции с обещанием точного изменения одного ID.
Зачем тогда нужен план?
Он фиксирует цель, единицы, интервалы и критерий приёмки. Это полезно для ручной работы и разработки отдельного проверяемого адаптера. Но арифметическую корректность нельзя называть выполненным монтажом.
Можно передать диапазоны в AppendToTimeline?
Не напрямую. План использует координаты таймлайна, а startFrame/endFrame в описании добавляемого клипа относятся к исходнику. Нужны отдельный source mapping и проверка границы; создание новых вхождений не сохраняет автоматически все свойства старого.[14]
В моей версии появился нужный метод. Что делать?
Зафиксируйте .pyi и версию, проверьте сигнатуру, единицы, побочные эффекты и возврат. Затем испытайте новый исполнитель на тестовой копии. Вывод этого материала ограничен проверенными файлами и не запрещает новые возможности.[14]
SOURCES / VERSION BOUNDARIES
Источники, а не подразумеваемая сертификация.
Сверка источников, офлайн-тесты Python и проверка интерфейса — разные виды подтверждения. Ни один из них не означает, что каждый пример проверен со всеми сборками Studio, ОС и версиями Python. Указанная дата — дата редакционной проверки, не сертификат живого прогона.
- Blackmagic Design — DaVinci Resolve Studio 21.1 release notes
8 сентября 2026. В объявлении выпуска Python scripting отнесён к Studio. Это источник требований редакции, а не протокол испытания наших примеров.
- Blackmagic Design — Support Center
Каталог обновлений проверен 5 октября 2026: опубликована ветка 21.1.1. Последний выпуск не равен минимальной версии каждого метода.
- Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)
Публичная копия первичного README Blackmagic, поддерживаемая X-Raym. По ней сверены базовые сигнатуры; это не официальный хостинг и не README 21.1.1. Оригинал своей установки: Help → Documentation → Developer → Scripting → README.txt; расположение пункта зависит от версии.
- Blackmagic Design — Scripting and Automation
Официальное описание Python/Lua scripting и интеграций Studio. Нельзя переносить обещания Studio на Free без проверки конкретного выпуска.
- Blackmagic DaVinciResolveScript.pyi 21.1 — published mirror
Для S05 проверена опубликованная копия первичного интерфейсного файла Blackmagic 21.1. Хостинг сторонний; сигнатуры не заменяют живую проверку. Сверьте Developer/Scripting/DaVinciResolveScript.pyi своей установки. Вывод об отсутствии split/trim ограничен конкретным документом.
- Python — ast: parsing source without executing it
Локальный проверяющий читает ограниченный .pyi как текст и строит AST, без импорта и исполнения. Синтаксис должен поддерживаться выбранным интерпретатором; ошибка разбора не доказывает отсутствие метода.