01 / BEFORE THE CODE
До кода — условия запуска.
Подключение уже работает
Используйте локальный Python 3.8+ и Resolve Studio. Проверьте подключение по S01, затем положите основной пример и оба helper в одну папку. Официальный модуль Blackmagic берётся из установленного Resolve, не из этого комплекта.
Одна проверяемая цель
Откройте небольшой тестовый проект и нужный таймлайн. Сохраните изменения и не переключайте активные объекты во время чтения. Для создания нового таймлайна заранее выберите папку Media Pool; открытый таймлайн не обязателен.
Граница примера
Обход ограничен верхним уровнем: до 128 дорожек каждого типа и 10 000 элементов всего. Вложенные монтажи не раскрываются. Ограничение проверяется после получения списка: оно не прерывает зависший нативный вызов и не ограничивает его выделение памяти.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
collect_map(resolve)- Вход
- Подключённый объект Resolve.
- Возвращает
- JSON-совместимая карта с complete и issues.
- Проверить
- Неполную карту не экспортировать как полную.
json.dumps(..., allow_nan=False)- Вход
- Только явная схема примитивных данных.
- Возвращает
- Текст JSON.
- Проверить
- Никакого default=str для прокси.
Path.open("xb")- Вход
- Новый абсолютный путь .json.
- Возвращает
- Двоичный файловый поток.
- Проверить
- Существующее имя блокирует запись.
Path.read_bytes()- Вход
- Только что созданный путь.
- Возвращает
- Байты файла.
- Проверить
- Сравнить с payload до заявления об успехе.
Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]
03 / UNDERSTAND
JSON-карта не заменяет DRT, DRP или исходники
В отчёте есть монтажная структура верхнего уровня, но нет полного состояния эффектов, Fusion-графов, цветокоррекции, автоматизации Fairlight и вложенных монтажей. Отсутствие этих данных — заявленная граница схемы, а не ошибка сериализации. Из этого JSON нельзя обещать восстановить исходный проект.
Схема маркируется resolve-timeline-map/1. При изменении значения полей или единиц нужна осознанная версия формата. Программа-потребитель должна проверить schema_version, scope, units и complete до интерпретации items. Не используйте имя файла как доказательство формата и полноты.[3][10]
04 / UNDERSTAND
Сериализуем данные, не API-объекты
collect_map заранее превращает ответы в строки, числа, списки, словари, bool и null. json.dumps получает allow_nan=False: NaN и бесконечность не выдаются за нормальные координаты. ensure_ascii=False и UTF-8 сохраняют читаемые русские имена, не меняя их идентичность.
Не добавляйте default=str ради обхода TypeError: он превратит неизвестный прокси в бесполезную строку и спрячет дефект схемы. Сначала решите, какие проверяемые поля нужны от нового объекта. Сырые временные значения и нормализованный range остаются отдельными полями.[10]
05 / UNDERSTAND
Предпросмотр не пишет файл; --output создаёт новый
Без --output пример печатает отчёт в терминал. С параметром требуется абсолютный путь с расширением .json в существующей доверенной папке. Относительные пути и автоматическое создание каталога исключены. Если файл уже есть, получите FileExistsError и выберите новое имя после проверки.
Сериализация и лимит 16 МБ проверяются до открытия файла; режим xb исключает обычную перезапись существующего имени. Но это не транзакция файловой системы: нехватка места или сбой во время записи может оставить новый неполный файл. Пример не удаляет его автоматически и не обещает fsync, сетевую атомарность или защиту от изменения родительской папки.[11]
06 / UNDERSTAND
Проверьте байты — и содержимое перед передачей
После записи читаются байты и сравниваются с подготовленным payload. Итог сообщает путь, размер и SHA-256. Это проверка записанного содержимого, не подпись автора и не доказательство неизменности после завершения скрипта. Повторный экспорт лучше сохранять под отдельным именем.
Хотя исходные пути не читаются, названия проектов, таймлайнов, клипов и ID могут раскрывать рабочую информацию. Перед передачей вручную обезличьте копию. Никакой отправки на сайт нет. Перенаправление терминала через > живёт по правилам оболочки и может перезаписать файл в обход защиты --output.[10][11]
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Положите export_timeline_json.py, resolve_api_timeline.py и resolve_api_connection.py в одну папку. Комплект содержит семь примеров, оба helper и инструкции RU/EN. Код helper полностью доступен ниже. Для расчёта интервалов допущения — часть входа, не подтверждённый анализ картинки.
Скачать все примеры с инструкцией (.zip) ↓"""Export a checked map to a NEW local JSON file. No Resolve writes."""
import argparse
from resolve_api_connection import connect
from resolve_api_timeline import collect_map, export_map, dumps
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--output", help="Absolute .json path; existing files are never overwritten")
a = p.parse_args(argv)
report = collect_map(connect())
if a.output is None:
print(dumps(report)) # Preview only. Shell redirection has its own overwrite rules.
else:
print(dumps(export_map(report, a.output)))
return 0 if report["complete"] else 2
if __name__ == "__main__":
raise SystemExit(main())
Timeline-helper — обход, границы и интервальная модель
Авторский код, не SDK. Структурные чтения сравнивают два наблюдения, но не блокируют монтаж. Только create_empty изменяет Resolve. export_map создаёт явно указанный локальный файл. v1_segments не подтверждает видимость пикселей.
"""S03 tutorial helpers. Python 3.8+. Read-only except create_empty().
A double read detects some changes, not every concurrent edit. No atomic snapshot,
UI lock, native-call timeout, pixel analysis, source retime mapping, or live certification.
Limits apply after each native list call; they cannot bound its memory/time cost.
"""
from __future__ import annotations
from datetime import datetime, timezone
from fractions import Fraction
import hashlib
import json
import math
from pathlib import Path
import re
TRACK_TYPES = ("video", "audio", "subtitle")
MAX_TRACKS = 128
MAX_ITEMS = 10000
MAX_TIMELINES = 1000
class InspectionError(RuntimeError):
"""An observation cannot be trusted or a requested target is ambiguous."""
class OutcomeUnknown(InspectionError):
"""A write was attempted. Inspect Resolve; do not retry or roll back blindly."""
def call(obj, method, *args):
fn = getattr(obj, method, None)
if not callable(fn):
raise InspectionError("Required method unavailable: " + method)
return fn(*args) # Propagate native failure; never try an invented fallback.
def text(value, label):
if not isinstance(value, str) or not value.strip():
raise InspectionError(label + " is not a non-empty string")
return value
def uid(obj):
return text(call(obj, "GetUniqueId"), "unique ID")
def number(value):
if type(value) not in (int, float) or not math.isfinite(value) or abs(value) > 2**53 - 1:
raise InspectionError("Expected finite frame value within the exact integer range")
return value
def count(value, cap):
if type(value) is not int or not 0 <= value <= cap:
raise InspectionError("Invalid count or tutorial scope limit exceeded")
return value
def items(value, cap=MAX_ITEMS):
if not isinstance(value, (list, tuple)) or len(value) > cap:
raise InspectionError("Expected a bounded list; None is not an empty list")
if any(x is None for x in value):
raise InspectionError("Null object in native list")
return list(value)
def boolean(value):
if type(value) is not bool:
raise InspectionError("Unknown enabled state; do not coerce to True/False")
return value
def dumps(value):
return json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False)
def digest(value):
data = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def parse_rate(raw):
"""Describe the numeric text exactly; never infer 24000/1001 from 23.976."""
result = {"raw": raw if (isinstance(raw, str) or type(raw) is int or (type(raw) is float and math.isfinite(raw))) else None,
"status": "unavailable", "label_fraction": None,
"drop_frame_suffix": "unknown", "physical_timebase": "not_inferred"}
if not isinstance(raw, str) or not raw.strip():
return result
result["status"] = "unrecognized"
if len(raw) > 64:
return result
m = re.fullmatch(r"\s*(\d+(?:\.\d+)?|\d+/\d+)\s*(DF)?\s*", raw)
if not m:
return result
try:
rate = Fraction(m.group(1))
except (ValueError, ZeroDivisionError):
return result
if not 0 < rate <= 1000:
return result
result.update(status="parsed_label", label_fraction={"numerator": rate.numerator, "denominator": rate.denominator},
drop_frame_suffix="present" if m.group(2) else "not_marked")
return result
def project_context(resolve):
manager = call(resolve, "GetProjectManager")
if manager is None:
raise InspectionError("Project Manager unavailable")
project = call(manager, "GetCurrentProject")
if project is None:
raise InspectionError("Open a test project first")
return manager, project
def timeline_context(resolve):
manager, project = project_context(resolve)
timeline = call(project, "GetCurrentTimeline")
if timeline is None:
raise InspectionError("Open the intended timeline; no automatic selection")
return manager, project, timeline
def assert_current(manager, project_id, timeline_id):
current = call(manager, "GetCurrentProject")
if current is None or uid(current) != project_id:
raise InspectionError("Active project changed during the read")
active = call(current, "GetCurrentTimeline")
if active is None or uid(active) != timeline_id:
raise InspectionError("Active timeline changed during the read")
def read_rates(resolve):
manager, project, timeline = timeline_context(resolve)
identity = (uid(project), uid(timeline))
def read():
# Read the two owners independently. Project is never a silent fallback.
return {"timeline": parse_rate(call(timeline, "GetSetting", "timelineFrameRate")),
"project": parse_rate(call(project, "GetSetting", "timelineFrameRate"))}
first, second = read(), read()
assert_current(manager, *identity)
if first != second:
raise InspectionError("Rate settings changed between observations")
return {"project_id": identity[0], "timeline_id": identity[1], **second,
"status": "observed_not_timecode_calibrated", "consistency": "two_equal_reads_not_atomic"}
def collect_once(resolve, project, timeline, track_types):
issues = []
def observe(label, operation):
try:
return operation()
except Exception as exc:
issues.append({"field": label, "error_type": type(exc).__name__})
return None
result = {"schema_version": "resolve-timeline-map/1", "project_id": uid(project),
"project_name": text(call(project, "GetName"), "project name"),
"timeline_id": uid(timeline), "timeline_name": text(call(timeline, "GetName"), "timeline name"),
"resolve_version": text(call(resolve, "GetVersionString"), "Resolve version"),
"scope": {"track_types": list(track_types), "depth": "top_level_only", "source_paths": "not_read"},
"units": {"positions": "timeline_frames", "ranges": "half_open_start_plus_duration", "subframes": True},
"timeline_start_frame": number(call(timeline, "GetStartFrame")),
"timeline_end_frame_raw": number(call(timeline, "GetEndFrame")),
"start_timecode_raw": text(call(timeline, "GetStartTimecode"), "start timecode"),
"rate": parse_rate(call(timeline, "GetSetting", "timelineFrameRate")), "tracks": [], "issues": issues}
seen = set()
total = 0
for kind in track_types:
n = count(call(timeline, "GetTrackCount", kind), MAX_TRACKS)
for index in range(1, n + 1):
key = kind + ":" + str(index)
track = {"type": kind, "index": index,
"name": observe(key+".name", lambda: text(call(timeline, "GetTrackName", kind, index), "track name")),
"enabled": observe(key+".enabled", lambda: boolean(call(timeline, "GetIsTrackEnabled", kind, index))),
"items": []}
# Failure to enumerate is fatal: an unknown track cannot become empty.
native_items = items(call(timeline, "GetItemListInTrack", kind, index))
total += len(native_items)
count(total, MAX_ITEMS)
for item in native_items:
item_id = uid(item)
if item_id in seen:
raise InspectionError("Duplicate TimelineItem ID in enumeration")
seen.add(item_id)
row = {"id": item_id, "track_type": kind, "track_index": index}
row["name"] = observe(item_id+".name", lambda: text(call(item, "GetName"), "item name"))
row["enabled"] = observe(item_id+".enabled", lambda: boolean(call(item, "GetClipEnabled")))
pair = observe(item_id+".track", lambda: items(call(item, "GetTrackTypeAndIndex"), 2))
row["reported_track"] = pair
if pair is not None and (len(pair) != 2 or type(pair[1]) is not int or pair != [kind, index]):
issues.append({"field": item_id+".track", "error_type": "TrackMismatch"})
for field, method in (("start_raw", "GetStart"), ("end_raw", "GetEnd"), ("duration_raw", "GetDuration")):
row[field] = observe(item_id+"."+field, lambda method=method: number(call(item, method, True)))
a, b, d = row["start_raw"], row["end_raw"], row["duration_raw"]
row["range"] = None
if a is not None and b is not None and d is not None:
# This is a checked model, not a claim about every API endpoint's end convention.
if d <= 0 or b <= a or not math.isclose(b-a, d, rel_tol=0, abs_tol=1e-7):
issues.append({"field": item_id+".range", "error_type": "UnqualifiedBoundaryConvention"})
else:
row["range"] = {"start": a, "end_exclusive": number(a+d),
"offset_from_timeline_start": number(a-result["timeline_start_frame"])}
def source():
media = call(item, "GetMediaPoolItem")
return {"status": "none"} if media is None else {"status": "linked", "media_pool_id": uid(media)}
row["source"] = observe(item_id+".source", source)
track["items"].append(row)
track["items"].sort(key=lambda row: (row["start_raw"] is None, row["start_raw"] or 0, row["id"]))
result["tracks"].append(track)
result["item_count"] = total
result["complete"] = not issues
result["rate_usable_as_label"] = result["rate"]["status"] == "parsed_label"
return result
def collect_map(resolve, track_types=TRACK_TYPES):
kinds = tuple(track_types)
if not kinds or len(set(kinds)) != len(kinds) or any(x not in TRACK_TYPES for x in kinds):
raise InspectionError("Select distinct documented track types")
manager, project, timeline = timeline_context(resolve)
identity = uid(project), uid(timeline)
first = collect_once(resolve, project, timeline, kinds)
assert_current(manager, *identity)
second = collect_once(resolve, project, timeline, kinds)
assert_current(manager, *identity)
if first != second:
raise InspectionError("Timeline changed or reads disagree; no stable report produced")
second["consistency"] = "two_equal_reads_not_atomic"
second["captured_at_utc"] = datetime.now(timezone.utc).isoformat()
second["limitations"] = ["No nested timeline expansion, source-time mapping or compositing analysis",
"Equal reads do not prove no intervening edits or provide a database revision"]
return second
def locate_item(report, item_id):
matches = [x for t in report["tracks"] for x in t["items"] if x["id"] == item_id]
if len(matches) != 1:
raise InspectionError("Item ID must identify exactly one entry in the observed scope")
row = matches[0]
pair = row["reported_track"]
if not isinstance(pair, (list, tuple)) or len(pair) != 2 or type(pair[1]) is not int or list(pair) != [row["track_type"], row["track_index"]]:
raise InspectionError("Reported track was not verified")
return row
def export_map(report, output):
"""Explicit local file write, never overwrite. Does not change Resolve."""
if report.get("complete") is not True or report.get("issues") or report.get("consistency") != "two_equal_reads_not_atomic":
raise InspectionError("Refusing an incomplete/unmatched map as a complete export")
path = Path(output).expanduser()
if not path.is_absolute() or not path.parent.is_dir() or path.suffix.lower() != ".json":
raise InspectionError("Use an absolute .json path in an existing trusted directory")
payload = (dumps(report) + "\n").encode("utf-8") # Validate before opening output.
if len(payload) > 16 * 1024 * 1024:
raise InspectionError("Export exceeds tutorial size limit")
# Exclusive create handles existing files and final-component symlinks without overwrite.
# A disk failure may leave a partial new file. No silent deletion/repair follows.
with path.open("xb") as stream:
stream.write(payload)
stream.flush()
if path.read_bytes() != payload:
raise InspectionError("Export readback differs; inspect the new file")
return {"status": "written_and_read_back", "path": str(path), "bytes": len(payload),
"sha256": hashlib.sha256(payload).hexdigest(), "restorable_project_backup": False}
def subtract_ranges(base, covers):
"""Half-open interval subtraction. Pure math, not visibility detection."""
start, end = map(number, base)
if end <= start:
raise InspectionError("Base interval must have positive length")
clipped = []
for a, b in covers:
number(a); number(b)
if b <= a:
raise InspectionError("Cover interval must have positive length")
lo, hi = max(start, a), min(end, b)
if lo < hi:
clipped.append((lo, hi))
merged = []
for a, b in sorted(clipped):
if merged and a <= merged[-1][1]:
merged[-1] = (merged[-1][0], max(merged[-1][1], b))
else:
merged.append((a, b))
cursor, remaining = start, []
for a, b in merged:
if cursor < a:
remaining.append([cursor, a])
cursor = max(cursor, b)
if cursor < end:
remaining.append([cursor, end])
return remaining
def v1_segments(report, opaque_v2_model=False):
if report.get("complete") is not True or report.get("issues"):
raise InspectionError("Cannot calculate from incomplete observations")
tracks = {t["index"]: t for t in report["tracks"] if t["type"] == "video"}
if 1 not in tracks:
raise InspectionError("No V1 in this scope")
if report.get("scope", {}).get("track_types") != ["video"]:
raise InspectionError("Use an explicit video-only observation")
for idx, track in tracks.items():
boolean(track["enabled"])
if idx > 2 and track["enabled"] and any(boolean(x["enabled"]) for x in track["items"]):
raise InspectionError("Active V3+ items are outside the two-layer tutorial model")
result = {"status": "model_not_confirmed", "pixel_visibility_verified": False,
"model": "opaque_full_frame_V2_hard_cuts_only", "segments": [],
"assumptions": ["No alpha, opacity animation, crop, transform, transition or compositing effects",
"No adjustment clips, nested compositions or other visible layers"]}
if not opaque_v2_model:
return result # No numeric result that could be mistaken for actual visibility.
base_track = tracks[1]
if not base_track["enabled"]:
result.update(status="conditional_model_result", reason="V1_disabled")
return result
upper = tracks.get(2)
covers = []
if upper and upper["enabled"]:
for row in upper["items"]:
if boolean(row["enabled"]):
if row["range"] is None:
raise InspectionError("Unqualified V2 range")
covers.append((row["range"]["start"], row["range"]["end_exclusive"]))
for row in base_track["items"]:
if not boolean(row["enabled"]):
continue
if row["range"] is None:
raise InspectionError("Unqualified V1 range")
spans = subtract_ranges((row["range"]["start"], row["range"]["end_exclusive"]), covers)
result["segments"].append({"item_id": row["id"], "timeline_ranges": spans})
result["status"] = "conditional_model_result"
return result
def creation_state(resolve, name):
manager, project = project_context(resolve)
pool = call(project, "GetMediaPool")
folder = call(pool, "GetCurrentFolder")
if folder is None:
raise InspectionError("Choose a Media Pool folder")
n = count(call(project, "GetTimelineCount"), MAX_TIMELINES)
timelines = []
for i in range(1, n+1):
t = call(project, "GetTimelineByIndex", i)
if t is None:
raise InspectionError("Timeline enumeration incomplete")
timelines.append({"id": uid(t), "name": text(call(t, "GetName"), "timeline name")})
if len({x["id"] for x in timelines}) != n:
raise InspectionError("Repeated timeline ID")
active = call(project, "GetCurrentTimeline")
state = {"name": name, "project_id": uid(project), "folder_id": uid(folder),
"active_timeline_id": uid(active) if active is not None else None,
"timelines": sorted(timelines, key=lambda x: x["id"])}
if uid(call(manager, "GetCurrentProject")) != state["project_id"]:
raise InspectionError("Project changed")
return state, pool
def create_empty(resolve, name, apply=False, project_id=None, folder_id=None, plan_digest=None):
text(name, "new timeline name")
if len(name) > 128 or name != name.strip() or any(ord(c) < 32 for c in name):
raise InspectionError("Use a trimmed name, at most 128 characters, without control characters")
before, pool = creation_state(resolve, name)
again, _ = creation_state(resolve, name)
if before != again:
raise InspectionError("Creation plan changed between reads")
if any(x["name"].casefold() == name.casefold() for x in before["timelines"]):
raise InspectionError("Name already exists; no create-or-open fallback")
token = digest(before)
if not apply:
return {"status": "plan_only", "plan_digest": token, **before}
if (project_id, folder_id, plan_digest) != (before["project_id"], before["folder_id"], token):
raise InspectionError("Explicit project/folder IDs and matching reviewed digest required")
latest, pool = creation_state(resolve, name)
if latest != before:
raise InspectionError("State changed just before create")
try:
created = call(pool, "CreateEmptyTimeline", name)
if created is None:
raise InspectionError("CreateEmptyTimeline returned no object")
new_id = uid(created)
after, _ = creation_state(resolve, name)
old = before["timelines"]
expected = sorted(old+[{"id": new_id, "name": name}], key=lambda x: x["id"])
if new_id in {x["id"] for x in old} or after["timelines"] != expected:
raise InspectionError("Timeline inventory does not match exactly one new timeline")
if (after["project_id"], after["folder_id"]) != (project_id, folder_id):
raise InspectionError("Target changed during create")
if after["active_timeline_id"] not in (before["active_timeline_id"], new_id):
raise InspectionError("Unexpected active timeline after create")
for kind in TRACK_TYPES:
for index in range(1, count(call(created, "GetTrackCount", kind), MAX_TRACKS)+1):
if items(call(created, "GetItemListInTrack", kind, index)):
raise InspectionError("New timeline is not empty")
return {"status": "created_empty_readback_confirmed", "timeline_id": new_id,
"active_timeline_id": after["active_timeline_id"], "saved_by_script": False}
except Exception as exc:
raise OutcomeUnknown("Create attempted; inspect the project before any further write") from exc
Общий 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 export_timeline_json.pyПечатает данные без создания файла.
Новый файл Windows
python export_timeline_json.py --output "D:/Reports/timeline-s03.json"Папка должна существовать; выберите собственный путь.
Новый файл Linux/macOS
python3 export_timeline_json.py --output "/absolute/path/reports/timeline-s03.json"Замените путь. Нет автоматического создания папок или перезаписи.
Что делает код
Собрать
Сначала получаем ограниченную карту и проверяем два чтения. Ошибка обхода не создаёт файл с пустым списком.
Записать
Только явно заданный путь .json в существующей папке. Режим xb не перезапишет существующий файл.
Перечитать
Проверяется содержимое созданного файла, а не только отсутствие исключения write. Это не проверка восстановления монтажа.
Границы результата
Пример читает Resolve, но --output явно создаёт файл. Это не обещание отсутствия любых изменений файловой системы.
Пример читает Resolve, но --output явно создаёт файл. Это не обещание отсутствия любых изменений файловой системы.
Два одинаковых чтения — проверка согласованности наблюдений, не атомарный снимок и не блокировка совместного редактирования.
В JSON могут быть названия проектов, клипов и их ID. Исходные пути не читаются, но автоматического обезличивания остальных полей нет.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
FileExistsError
Возможная причинаИмя файла уже занято, в том числе ссылкой.
Следующая проверкаОсмотрите файл и задайте другое имя; не включайте скрытую перезапись.
Неполная карта
Возможная причинаcomplete false, issues или нет подтверждения двух чтений.
Следующая проверкаИспользуйте диагностический вывод и устраните причину; файл полной карты не создаётся.
JSON не сериализуется
Возможная причинаВ схему попал прокси или нечисловая координата.
Следующая проверкаИсправьте схему данных, а не добавляйте default=str.
Сбой диска или чтения файла
Возможная причинаЗапись могла оставить частичный новый файл.
Следующая проверкаПроверьте файл вручную; успех и автоматическая очистка не заявляются.
PRACTICE / YOUR OWN EVIDENCE
Проверьте полный путь экспорт → чтение
- Сначала просмотрите карту без --output.
- Экспортируйте в новое имя и загрузите файл обычным json.loads.
- Повторите запрос в то же имя: исходные байты должны сохраниться.
Схема и число вхождений совпали; UTF-8 читается; существующий файл не заменён; неполный отчёт отвергнут до записи.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Можно восстановить проект из JSON?
Нет. Это структурный отчёт с явно ограниченными полями. Для резервного копирования нужен отдельный штатный процесс с тестом восстановления.[3]
Почему не перезаписывать report.json?
Учебный пример защищает существующий отчёт. Осознанную замену, историю версий и атомарную публикацию нужно проектировать отдельно.[11]
SHA-256 доказывает достоверность карты?
Нет, он идентифицирует записанные байты. Достоверность полей определяется чтением и проверками, а не самим хешем.[3]
Экспорт меняет Resolve?
Нет. Изменяется только явно заданный локальный файл. Компонент сайта также не получает этот файл автоматически.[3]
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 без проверки конкретного выпуска.
- Python — json encoder and decoder
Типы JSON, ensure_ascii, allow_nan и явная сериализация. Наш экспорт не использует default=str для сокрытия неизвестных API-объектов.
- Python — open: exclusive creation and file modes
Режим x создаёт новый файл с отказом при существующем имени. Это не обещает файловую транзакцию или долговечность после аварии.