01 / BEFORE THE CODE
До кода — условия запуска.
Среда и интерфейс
Внешний Python 3.9+ и запущенный Resolve Studio. S06 использует опубликованный новый .pyi, в том числе GetSettings/SetSettings. Сверьте документ установленной версии: старый README не подменяется тем же контрактом. Универсальная совместимость с Free, всеми ОС и кодировщиками не заявляется.
Точная цель
Используйте ID текущего проекта/таймлайна, не похожие имена. Каталогу пресетов и форматов достаточно проекта; V1 и изменяющим примерам нужен таймлайн. Скрипты не открывают и не переключают проект автоматически.
Отдельная тестовая копия
Сначала план, затем --apply с точными ID, --test-copy и просмотренным --expect-plan. Рендер и параллельные изменения должны быть остановлены. Отпечаток не является блокировкой или полным снимком Deliver.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
SetRenderSettings(settings)- Вход
- Словарь поддерживаемых ключей; пример использует узкий список.
- Возвращает
- Bool ответа.
- Проверить
- Не трактовать True как проверку всех параметров и файла.
FormatWidth / FormatHeight- Вход
- Два int в пикселях.
- Возвращает
- Параметры запроса вывода.
- Проверить
- Ввод не должен менять разрешение проекта автоматически.
TargetDir / CustomName- Вход
- Абсолютный существующий каталог / имя без пути.
- Возвращает
- Уточнение места и имени будущего вывода.
- Проверить
- Наличие каталога не доказывает права и успешную запись.
UseUniqueFilenames / UniqueFilenameStyle- Вход
- Bool и int; в примере true и 1.
- Возвращает
- Политика уникального имени с суффиксом.
- Проверить
- Не замена ручной проверке имён и политике файлов.
VideoQuality- Вход
- 0 — Auto; положительное целое — настройка битрейта по контракту.
- Возвращает
- Вход для поддерживающего параметр кодека.
- Проверить
- Не угадывать единицы или универсальное «лучшее» значение.
Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[16]
03 / UNDERSTAND
Правильные ключи вместо похожих названий
В проверенном RenderSettings используются FormatWidth и FormatHeight, UseUniqueFilenames и VideoQuality. Старые ResolutionWidth, ResolutionHeight, UniqueFilenames, Quality и VideoBitrate не подставляются как очевидные синонимы. API-контракт определяется точным написанием, а не близостью слов.
Format и VideoCodec выбираются через SetCurrentRenderFormatAndCodec, Single clip / Individual clips — через отдельный режим. AudioBitrate в рассмотренном списке полей не обнаружен; нельзя добавлять его в словарь только потому, что такой контрол встречается в интерфейсе. Полный список своей версии сверяйте с локальным документом.[16]
04 / UNDERSTAND
Девять ключей — осознанно уже полного SDK
Учебный JSON допускает TargetDir, CustomName, ExportVideo, ExportAudio, UseUniqueFilenames, UniqueFilenameStyle, FormatWidth, FormatHeight и VideoQuality. Первые шесть обязательны; размеры задаются вместе или не задаются вообще. VideoQuality необязателен. Непереданные параметры остаются вне этого запроса — не объявляются проверенными значениями по умолчанию.
Профиль рассчитан на Single clip с видео: ExportVideo и UseUniqueFilenames равны true, UniqueFilenameStyle равен 1. ExportAudio задаётся явно как true или false, но флаг сам не проверяет микс, каналы, наличие звука или кодек. Более широкий audio-only, In/Out, subtitle и alpha workflow требует собственного контракта; неизвестные ключи здесь отклоняются.[16]
05 / UNDERSTAND
Каталог и JSON проверяются до подключения
Основной скрипт читает UTF-8 JSON не более 16 КиБ, отклоняет повторяющиеся ключи, NaN, неверные типы и лишние поля. Не используется eval, импорт конфигурации как Python или исполнение текста. Для чисел нет молчаливого превращения строки "1920" в integer.
TargetDir должен быть уже существующим абсолютным каталогом локальной ОС. Не создаются папки, не разворачивается ~ и не выполняется пробная запись. CustomName — короткое переносимое имя без разделителей пути, системных device names и опасных символов. Эти проверки уменьшают неоднозначность, но не подтверждают свободное место, права, mount-состояние во время рендера или отсутствие конфликтов имён.[16][10][8]
06 / UNDERSTAND
Битрейт, размер и оставшиеся настройки
В документах VideoQuality различает Auto, положительное числовое значение и строковые уровни для подходящих кодеков. Наш JSON принимает только целые числа, начиная с 0. В рассмотренном описании нет достаточного основания молча умножать все значения на 1000: единицы и соответствие контролу Deliver нужно установить на своей сборке и кодеке.
Если размеры переданы, helper требует их присутствия в GetRenderResolutions текущей пары. Это консервативная политика примера, не заявление, что нестандартный размер никогда не поддерживается. Если размеры пропущены, пример не читает и не подтверждает унаследованное output resolution. Ни один из этих вариантов не меняет рабочее разрешение проекта.[16][3]
07 / UNDERSTAND
Соберите и проверьте свой черновик
Введите объект или переключите учебный пример. Парсер различает повтор ключа, неизвестное поле, неверный тип и несогласованную пару размеров. При ошибке старый численный результат скрывается, а копирование и загрузка недопустимого черновика блокируются.
Файл .json содержит текущий валидированный объект, а не исходную заготовку. Браузер не знает вашей файловой системы и не обращается к Resolve: наличие папки, кодек, лицензия, проект и реальная семантика не проверены. Путь /path/to/existing/output нужно заменить до запуска Python.[16]
Без отправки, сохранения и автоматического обезличивания. Не вводите личные пути. При уходе изменения пропадут.
Запрос вывода: 1920 × 1080. Аудио: запрошено.
Это не предпросмотр картинки и не подключение к Resolve.
Не проверены: путь вашей ОС, наличие папки, права, кодек, проект, содержимое пресета и итоговый файл. Учебный путь нужно заменить самостоятельно.
Девять разрешённых ключей
TargetDirCustomNameExportVideoExportAudioUseUniqueFilenamesUniqueFilenameStyleFormatWidthFormatHeightVideoQuality
Список намеренно уже полного SDK. Контейнер/кодек и режим вывода задаются отдельными методами; диапазоны и очередь разобраны в следующих темах.
08 / UNDERSTAND
После True — ручная сверка, не автоматический рендер
План связывает запрос с ID проекта/таймлайна, наблюдаемой парой, режимом и ограниченными настройками. Он не включает полный скрытый набор Deliver. Перед apply остановите параллельные изменения; совпавшие чтения не создают транзакцию и не гарантируют отсутствие гонки.
После единственного SetRenderSettings True вместе с корректным чтением цели означает accepted_pending_manual_verification и код выхода 3. Это не команда повторить запрос. Сверьте Deliver, затем отдельно создавайте и проверяйте задание. False, исключение или смена цели оставляют outcome_unconfirmed; автоматических retry, rollback и «резервных» параметров нет.[16]
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Положите set_render_settings.py, resolve_api_output.py и resolve_api_connection.py в одну папку. Комплект содержит шесть примеров, оба helper, audit_output_contract.py, шаблон JSON и инструкции RU/EN. В шаблоне указан путь-заглушка; замените и проверьте его перед использованием. Официальный SDK в комплект не включён.
Скачать все примеры с инструкцией (.zip) ↓"""Validate a small explicit JSON settings object and send it once. Never render."""
import argparse
from resolve_api_connection import connect
from resolve_api_output import cli_target, cli_write, load_settings, print_result, run_action
def main():
p = argparse.ArgumentParser(description=__doc__)
cli_target(p)
cli_write(p)
p.add_argument("--settings", required=True, help="Local UTF-8 JSON file, at most 16 KiB")
a = p.parse_args()
def task():
payload = load_settings(a.settings) # Reject invalid settings before native connection.
return run_action(connect(), a.project_id, a.timeline_id, "settings", settings=payload,
apply=a.apply, expect_plan=a.expect_plan, test_copy=a.test_copy)
return print_result(task)
if __name__ == "__main__":
raise SystemExit(main())
Output-helper — точные цели и один вызов настройки
Авторский учебный код, не SDK. План сравнивает наблюдаемое состояние, но не блокирует Resolve. Каждый apply допускает один изменяющий вызов: без заданий рендера, повторов, создания папок и автоматического восстановления. Имя пресета не подтверждает его содержимое. Принятые настройки остаются на ручной проверке.
"""S06 author tutorials: output settings, NOT the Blackmagic SDK.
A plan is the default. Each apply sends at most one mutating Resolve call.
No queue creation, rendering, directory creation, retry, rollback or project
save. Observations are not atomic. There is no generic GetRenderSettings in
our reviewed interface: accepted settings/presets still need manual review.
Targets and bounds are tutorial policy, not Resolve product limitations.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
import math
import re
from pathlib import Path
MAX_ITEMS = 5000
MAX_DIMENSION = 32768 # Tutorial bound, not a claim about encoder support.
MAX_JSON_BYTES = 16384
SETTING_KEYS = frozenset({"TargetDir", "CustomName", "ExportVideo", "ExportAudio",
"UseUniqueFilenames", "UniqueFilenameStyle", "FormatWidth",
"FormatHeight", "VideoQuality"})
OBSERVED_SETTINGS = ("timelineResolutionWidth", "timelineResolutionHeight",
"timelinePixelAspectRatio", "timelineFrameRate",
"timelineOutputResMatchTimelineRes", "timelineOutputResolutionWidth",
"timelineOutputResolutionHeight", "useCustomSettings")
class PreflightError(RuntimeError):
"""Failure before this helper sends any mutation."""
def read(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 string(value, label):
if not isinstance(value, str) or not value.strip() or len(value) > 4096:
raise PreflightError("Missing or invalid " + label)
try:
value.encode("utf-8")
except UnicodeError as exc:
raise PreflightError("Invalid Unicode in " + label) from exc
return value
def integer(value, label, low=0, high=MAX_ITEMS):
if type(value) is not int or not low <= value <= high:
raise PreflightError(label + " must be an integer in the tutorial's explicit bounds")
return value
def boolean(value, label):
if type(value) is not bool:
raise PreflightError(label + " must be a boolean, not None/0/1/text")
return value
def finite_json(value, label="API value"):
"""No default=str coercion of unknown native objects."""
if value is None or type(value) in (str, int, bool):
return value
if type(value) is float and math.isfinite(value):
return value
if isinstance(value, list):
return [finite_json(v, label) for v in value]
if isinstance(value, dict) and all(isinstance(k, str) for k in value):
return {k: finite_json(v, label) for k, v in value.items()}
raise PreflightError("Unsupported JSON type in " + label)
def fingerprint(value):
data = json.dumps(value, sort_keys=True, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def target(resolve, project_id=None, timeline_id=None, need_timeline=True):
manager = read(resolve, "GetProjectManager")
if not manager:
raise PreflightError("Project Manager unavailable")
project = read(manager, "GetCurrentProject")
if not project:
raise PreflightError("No current project")
pid = string(read(project, "GetUniqueId"), "project ID")
if project_id is not None and pid != project_id:
raise PreflightError("Active project does not match --project-id")
if not need_timeline:
return project, None, pid, None
timeline = read(project, "GetCurrentTimeline")
if not timeline:
raise PreflightError("No current timeline")
tid = string(read(timeline, "GetUniqueId"), "timeline 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 preset_names(project):
raw = read(project, "GetRenderPresetList")
if not isinstance(raw, list) or len(raw) > MAX_ITEMS:
raise PreflightError("Expected the documented list of preset names; None is not []")
return [string(x, "preset name") for x in raw]
def list_presets(resolve, project_id=None):
project, _, pid, _ = target(resolve, project_id, need_timeline=False)
names = preset_names(project)
if preset_names(project) != names:
raise PreflightError("Preset catalogue changed while reading")
target(resolve, pid, need_timeline=False)
return {"status": "read_only", "project_id": pid, "presets": names,
"duplicates": sorted({n for n in names if names.count(n) > 1}),
"empty": len(names) == 0, "loaded_preset": False,
"scope": "Names only; no preset content, project preset or Quick Export query"}
def parse_resolution(raw):
if not isinstance(raw, str) or len(raw) > 100:
raise PreflightError("Resolution must be an explicit width x height string")
match = re.fullmatch(r"\s*([0-9]+)\s*[xX×]\s*([0-9]+)\s*", raw)
if not match:
raise PreflightError("Unknown Resolution format; no search through unrelated properties")
return tuple(integer(int(v), "dimension", 1, MAX_DIMENSION) for v in match.groups())
def v1_report(timeline):
n = integer(read(timeline, "GetTrackCount", "video"), "video track count", 0, 128)
if n == 0:
return {"track_exists": False, "track_enabled": None, "items": [],
"active_sizes": [], "complete": False, "reason": "no_v1"}
enabled = boolean(read(timeline, "GetIsTrackEnabled", "video", 1), "V1 enabled")
items = read(timeline, "GetItemListInTrack", "video", 1)
if not isinstance(items, list) or len(items) > MAX_ITEMS:
raise PreflightError("V1 list unreadable or beyond tutorial bound")
rows, ids, active_sizes = [], set(), set()
active_count, complete = 0, True
for item in items:
uid = string(read(item, "GetUniqueId"), "timeline item ID")
if uid in ids:
raise PreflightError("Duplicate timeline item ID; target is ambiguous")
ids.add(uid)
active = enabled and boolean(read(item, "GetClipEnabled"), "clip enabled")
row = {"id": uid, "active_on_v1": active, "status": "unread"}
active_count += int(active)
try:
source = read(item, "GetMediaPoolItem")
if not source:
row["status"] = "no_media_pool_item"
else:
row["source_id"] = string(read(source, "GetUniqueId"), "source ID")
raw = read(source, "GetClipProperty", "Resolution")
row["resolution_raw"] = finite_json(raw)
try:
w, h = parse_resolution(raw)
row.update(status="dimensions_read", width=w, height=h)
if active:
active_sizes.add((w, h))
except PreflightError:
row["status"] = "unparsed_resolution"
except PreflightError as exc:
row.update(status="read_error", error=str(exc))
if active and row["status"] != "dimensions_read":
complete = False
rows.append(row)
return {"track_exists": True, "track_enabled": enabled, "items": sorted(rows, key=lambda r: r["id"]),
"active_count": active_count, "active_sizes": [list(v) for v in sorted(active_sizes)],
"complete": bool(complete and active_count),
"scope": "Reported source pixel dimensions only; no rotation, PAR, scaling, visibility or decoding analysis"}
def inspect_v1(resolve, project_id=None, timeline_id=None):
_, t, pid, tid = target(resolve, project_id, timeline_id)
first = v1_report(t)
second = v1_report(t)
if first != second:
raise PreflightError("V1 observations changed")
target(resolve, pid, tid)
return {"status": "read_only" if second["complete"] else "partial_observation",
"project_id": pid, "timeline_id": tid, "v1": second}
def mapping(value, label):
if not isinstance(value, dict) or len(value) > MAX_ITEMS:
raise PreflightError("Expected dictionary: " + label)
return {string(k, label + " description"): string(v, label + " ID") for k, v in value.items()}
def format_catalog(project, container="mov"):
formats = mapping(read(project, "GetRenderFormats"), "render formats")
if container not in formats.values():
raise PreflightError("Requested extension is not advertised by GetRenderFormats")
codecs = mapping(read(project, "GetRenderCodecs", container), "render codecs")
return {"formats": formats, "container": container, "codecs": codecs}
def current_pair(project):
pair = mapping(read(project, "GetCurrentRenderFormatAndCodec"), "current pair")
if not {"format", "codec"} <= pair.keys():
raise PreflightError("Current format/codec keys are missing")
return {"format": pair["format"], "codec": pair["codec"]}
def advertised_sizes(project, pair):
raw = read(project, "GetRenderResolutions", pair["format"], pair["codec"])
if not isinstance(raw, list) or len(raw) > MAX_ITEMS:
raise PreflightError("Resolution catalogue is not a list")
sizes = set()
for row in raw:
if not isinstance(row, dict) or not {"Width", "Height"} <= row.keys():
raise PreflightError("Invalid resolution catalogue row")
sizes.add((integer(row["Width"], "Width", 1, MAX_DIMENSION),
integer(row["Height"], "Height", 1, MAX_DIMENSION)))
return [list(v) for v in sorted(sizes)]
def observe_settings(obj):
raw = read(obj, "GetSettings")
if not isinstance(raw, dict):
raise PreflightError("GetSettings must return a dictionary; use the interface matching your build")
return {k: finite_json(raw[k], k) for k in OBSERVED_SETTINGS if k in raw}
def capture(resolve, project_id, timeline_id, with_v1=False):
project, timeline, pid, tid = target(resolve, project_id, timeline_id)
jobs = read(project, "GetRenderJobList")
if not isinstance(jobs, list) or len(jobs) > MAX_ITEMS:
raise PreflightError("Queue identifiers cannot be observed")
ids = []
for job in jobs:
if not isinstance(job, dict):
raise PreflightError("Unexpected render job row")
ids.append(string(job.get("JobId"), "render job ID"))
if len(ids) != len(set(ids)):
raise PreflightError("Duplicate queue IDs")
state = {"project_id": pid, "timeline_id": tid,
"pair": current_pair(project),
"mode": integer(read(project, "GetCurrentRenderMode"), "render mode", 0, 1),
"rendering": boolean(read(project, "IsRenderingInProgress"), "rendering flag"),
"queue_ids": sorted(ids), "project_settings": observe_settings(project),
"timeline_settings": observe_settings(timeline)}
if with_v1:
state["v1"] = v1_report(timeline)
state["timeline_count"] = integer(read(project, "GetTimelineCount"), "timeline count", 1, MAX_ITEMS)
target(resolve, pid, tid)
return state, project
def stable_capture(resolve, pid, tid, with_v1=False):
first, _ = capture(resolve, pid, tid, with_v1)
second, project = capture(resolve, pid, tid, with_v1)
if first != second:
raise PreflightError("Observed state changed during preflight")
return second, project
def no_duplicate_keys(pairs):
result = {}
for key, value in pairs:
if key in result:
raise PreflightError("Duplicate JSON key: " + key)
result[key] = value
return result
def validate_settings(value, check_path=True):
if not isinstance(value, dict) or not value:
raise PreflightError("Expected a nonempty flat settings object")
unknown = set(value) - SETTING_KEYS
if unknown:
raise PreflightError("Outside this tutorial's allowlist: " + ", ".join(sorted(unknown)))
required = {"TargetDir", "CustomName", "ExportVideo", "ExportAudio", "UseUniqueFilenames", "UniqueFilenameStyle"}
if not required <= value.keys():
raise PreflightError("Missing required keys: " + ", ".join(sorted(required - value.keys())))
directory = string(value["TargetDir"], "TargetDir")
if any(ord(c) < 32 for c in directory):
raise PreflightError("Control character in TargetDir")
name = string(value["CustomName"], "CustomName")
if len(name) > 120 or name != name.strip() or name.endswith(".") or re.search(r'[<>:"/\\|?*\x00-\x1f]', name):
raise PreflightError("CustomName must be a portable basename, not a path")
if name in (".", "..") or re.fullmatch(r"(?i)(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?", name):
raise PreflightError("Reserved CustomName")
if value["ExportVideo"] is not True or value["UseUniqueFilenames"] is not True:
raise PreflightError("This video tutorial requires ExportVideo and UseUniqueFilenames = true")
boolean(value["ExportAudio"], "ExportAudio")
integer(value["UniqueFilenameStyle"], "UniqueFilenameStyle", 1, 1)
if ("FormatWidth" in value) != ("FormatHeight" in value):
raise PreflightError("Provide both FormatWidth and FormatHeight")
for key in ("FormatWidth", "FormatHeight"):
if key in value:
integer(value[key], key, 1, MAX_DIMENSION)
if "VideoQuality" in value:
integer(value["VideoQuality"], "VideoQuality", 0, 1000000000)
if check_path:
p = Path(directory)
if not p.is_absolute() or not p.is_dir():
raise PreflightError("TargetDir must already be an absolute local directory; no mkdir or ~ expansion")
return copy.deepcopy(value)
def load_settings(path):
with Path(path).open("rb") as f:
raw = f.read(MAX_JSON_BYTES + 1)
if len(raw) > MAX_JSON_BYTES:
raise PreflightError("Settings file exceeds 16 KiB")
try:
value = json.loads(raw.decode("utf-8"), object_pairs_hook=no_duplicate_keys,
parse_constant=lambda x: (_ for _ in ()).throw(PreflightError("Non-finite JSON number")))
except (ValueError, UnicodeError) as exc:
raise PreflightError("Invalid UTF-8 JSON settings") from exc
return validate_settings(value)
def prepare(resolve, pid, tid, action, **options):
with_v1 = action == "resolution"
state, project = stable_capture(resolve, pid, tid, with_v1)
if state["rendering"]:
raise PreflightError("Rendering is active; do not change Deliver settings")
plan = {"schema": "resolve.output.plan.v1", "action": action, "observed_before": state,
"verification_scope": "Selected observations only; no complete render-settings getter or atomic snapshot"}
if action == "pair":
codec = string(options.get("codec"), "exact codec ID")
catalogue = format_catalog(project, "mov")
labels = [k for k, v in catalogue["codecs"].items() if v == codec]
if not labels or not all(re.search(r"(?i)(h\.?265|hevc)", k) for k in labels):
raise PreflightError("Chosen codec must be advertised as H.265/HEVC for mov; no alias probing")
if state["mode"] != 1:
raise PreflightError("Select Single clip manually first; this example does not change render mode")
plan.update(method="SetCurrentRenderFormatAndCodec", args=["mov", codec], catalogue=catalogue,
write_required=state["pair"] != {"format": "mov", "codec": codec})
elif action == "preset":
name = string(options.get("preset"), "exact preset name")
names = preset_names(project)
if names.count(name) != 1:
raise PreflightError("Exact preset name must occur once; no substring/fallback selection")
plan.update(method="LoadRenderPreset", args=[name], preset_names=names, write_required=True,
preset_content="not observable; same name does not bind preset contents")
elif action == "settings":
payload = validate_settings(options.get("settings"))
if state["mode"] != 1:
raise PreflightError("Select Single clip manually first")
if "FormatWidth" in payload:
sizes = advertised_sizes(project, state["pair"])
if [payload["FormatWidth"], payload["FormatHeight"]] not in sizes:
raise PreflightError("Dimensions are not in the advertised list; custom sizes need a separate qualification")
plan["advertised_sizes"] = sizes
plan.update(method="SetRenderSettings", args=[payload], write_required=True)
elif action == "resolution":
width = integer(options.get("width"), "width", 1, MAX_DIMENSION)
height = integer(options.get("height"), "height", 1, MAX_DIMENSION)
scope = options.get("scope")
if scope not in ("render", "project"):
raise PreflightError("Choose --scope render or project explicitly")
v1 = state["v1"]
if not v1["complete"] or [width, height] not in v1["active_sizes"]:
raise PreflightError("Need complete active V1 dimensions and an explicitly selected reported pair")
if len(v1["active_sizes"]) > 1 and options.get("accept_mixed") is not True:
raise PreflightError("Mixed dimensions: review the report and explicitly --accept-mixed")
plan["scope"] = scope
if scope == "render":
if state["mode"] != 1:
raise PreflightError("Choose Single clip before setting output dimensions")
sizes = advertised_sizes(project, state["pair"])
if [width, height] not in sizes:
raise PreflightError("Selected dimensions are not advertised for the current pair")
plan.update(method="SetRenderSettings", args=[{"FormatWidth": width, "FormatHeight": height}],
advertised_sizes=sizes, write_required=True)
else:
if state["timeline_count"] != 1 or state["timeline_settings"].get("useCustomSettings") != "0":
raise PreflightError("Project-scope tutorial needs one timeline using project settings; no forced inheritance")
for current in (state["project_settings"], state["timeline_settings"]):
for key in ("timelineResolutionWidth", "timelineResolutionHeight"):
raw = current.get(key)
if not isinstance(raw, str) or not re.fullmatch(r"[0-9]+", raw):
raise PreflightError("Project/timeline dimension readback unavailable")
integer(int(raw), key, 1, MAX_DIMENSION)
payload = {"timelineResolutionWidth": str(width), "timelineResolutionHeight": str(height)}
plan.update(method="SetSettings", args=[payload], write_required=any(
state[key].get(k) != v for key in ("project_settings", "timeline_settings") for k, v in payload.items()))
else:
raise PreflightError("Unknown action")
if not callable(getattr(project, plan["method"], None)):
raise PreflightError("Required write method is not exposed; do not guess alternate setters")
plan["plan_sha256"] = fingerprint(plan)
return plan, project
def run_action(resolve, project_id, timeline_id, action, *, apply=False, expect_plan=None,
test_copy=False, preset_reviewed=False, **options):
plan, _ = prepare(resolve, project_id, timeline_id, action, **options)
if not apply:
return {"status": "plan_only", "plan": plan}
if not test_copy or not project_id or not timeline_id or not expect_plan:
raise PreflightError("Apply requires exact IDs, --test-copy and --expect-plan")
if action == "preset" and not preset_reviewed:
raise PreflightError("Review the broad preset contents and pass --preset-reviewed")
if expect_plan != plan["plan_sha256"]:
raise PreflightError("Reviewed plan does not match current observed fields")
fresh, project = prepare(resolve, project_id, timeline_id, action, **options)
if fresh != plan:
raise PreflightError("Plan changed immediately before apply")
if not plan["write_required"]:
return {"status": "already_satisfied_observed_scope", "plan": plan, "mutation_sent": False}
# Exactly one invocation. Any exception below can follow partial side effects.
report = {"status": "outcome_unconfirmed", "plan": plan, "mutation_sent": True,
"retry_allowed": False, "full_render_settings_verified": False}
try:
response = getattr(project, plan["method"])(*plan["args"])
report["response_type"] = type(response).__name__
report["returned_true"] = response is True
except Exception as exc:
report["error"] = "Mutation raised " + type(exc).__name__
response = None
try:
after, _ = stable_capture(resolve, project_id, timeline_id, action == "resolution")
report["observed_after"] = after
except Exception as exc:
report["readback_error"] = type(exc).__name__ + ": " + str(exc)
return report
before = plan["observed_before"]
expected = copy.deepcopy(before)
if action == "pair":
expected["pair"] = {"format": "mov", "codec": options["codec"]}
elif action == "resolution" and plan["scope"] == "project":
for key in ("project_settings", "timeline_settings"):
expected[key].update(plan["args"][0])
if action == "preset":
# Pair/mode may change on purpose; IDs, queue membership and project/timeline
# geometry must still be the fields observed, not a different target.
observed_ok = all(after[k] == before[k] for k in before if k not in ("pair", "mode"))
else:
observed_ok = after == expected
report["observed_scope_matches"] = observed_ok
if response is True and observed_ok:
report["status"] = ("verified_observed_scope" if action == "pair" or
(action == "resolution" and plan["scope"] == "project")
else "accepted_pending_manual_verification")
return report
def inspect_mov(resolve, project_id=None):
project, _, pid, _ = target(resolve, project_id, need_timeline=False)
first = format_catalog(project)
if format_catalog(project) != first:
raise PreflightError("Format catalogue changed")
target(resolve, pid, need_timeline=False)
return {"status": "read_only", "project_id": pid, "catalogue": first,
"scope": "Advertised IDs, not an encode test or proof of bit depth, profile or hardware encoder"}
def cli_target(parser, timeline=True):
parser.add_argument("--project-id", help="Exact current project ID; required for apply")
if timeline:
parser.add_argument("--timeline-id", help="Exact current timeline ID; required for apply")
def cli_write(parser):
parser.add_argument("--apply", action="store_true", help="One mutation after plan checks")
parser.add_argument("--test-copy", action="store_true", help="Confirm a separate test copy")
parser.add_argument("--expect-plan", help="SHA-256 printed by the reviewed plan")
def print_result(fn):
try:
value = fn()
except Exception as exc:
print(json.dumps({"status": "preflight_failed", "error": type(exc).__name__ + ": " + str(exc)}, ensure_ascii=False))
return 2
print(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False))
# 3 is a deliberate pending/unknown/partial result, NOT permission to retry.
return 3 if value["status"] in ("outcome_unconfirmed", "accepted_pending_manual_verification", "partial_observation") else 0
Дополнительно: проверить контракт вывода в локальном .pyi
Самостоятельный проверяющий разбирает ограниченный файл интерфейса без импорта. Вывод относится к этому документу, а не к живому Resolve и не к разрешению изменять проект.
"""Inspect an installed .pyi for S06 declarations WITHOUT importing or executing it.
An interface declaration is not an encoder capability or successful live call.
Missing names are reported as NOT DECLARED HERE, not as absent in all versions.
"""
import argparse
import ast
import hashlib
import json
from pathlib import Path
QUERIES = {
"Project": ("GetRenderPresetList", "LoadRenderPreset", "GetRenderFormats", "GetRenderCodecs",
"GetCurrentRenderFormatAndCodec", "SetCurrentRenderFormatAndCodec", "GetCurrentRenderMode",
"SetCurrentRenderMode", "GetRenderResolutions", "SetRenderSettings", "GetRenderSettings",
"GetSettings", "SetSettings"),
"RenderSettings": ("FormatWidth", "FormatHeight", "ResolutionWidth", "ResolutionHeight",
"TargetDir", "CustomName", "UseUniqueFilenames", "UniqueFilenameStyle",
"VideoQuality", "Format", "VideoCodec", "IndividualClips", "AudioBitrate"),
}
def audit(path):
with Path(path).open("rb") as f:
raw = f.read(2 * 1024 * 1024 + 1)
if len(raw) > 2 * 1024 * 1024:
raise ValueError("Document exceeds the 2 MiB tutorial bound")
tree = ast.parse(raw.decode("utf-8-sig"))
classes = {n.name: n for n in tree.body if isinstance(n, ast.ClassDef)}
if not all(name in classes for name in QUERIES):
raise ValueError("Expected Project and RenderSettings classes in this interface document")
result = {}
for owner, names in QUERIES.items():
found = {}
for node in classes[owner].body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
found[node.name] = {"line": node.lineno, "kind": "method", "arguments": ast.unparse(node.args)}
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
found[node.target.id] = {"line": node.lineno, "kind": "field", "type": ast.unparse(node.annotation)}
result[owner] = {name: found.get(name, {"status": "not_declared_here"}) for name in names}
return {"document_sha256": hashlib.sha256(raw).hexdigest(), "declarations": result,
"live_verified": False, "scope": "This file only; not native execution or encoder support"}
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("interface_file", type=Path)
a = p.parse_args()
try:
print(json.dumps(audit(a.interface_file), ensure_ascii=False, indent=2))
except (OSError, UnicodeError, ValueError, SyntaxError, RecursionError) as exc:
print(json.dumps({"status": "audit_failed", "error": type(exc).__name__ + ": " + str(exc)}))
return 2
return 0
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
Команды запуска
План из JSON
python set_render_settings.py --settings settings.example.json --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID"Сначала замените учебный путь. Папку нужно создать самостоятельно до запуска.
Применение одного объекта
python set_render_settings.py --settings settings.example.json --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID" --apply --test-copy --expect-plan "PLAN_SHA256"После изменения файла нужен новый просмотренный план. True оставляет ручную проверку и код 3.
Проверка локального интерфейса
python audit_output_contract.py "/path/to/DaVinciResolveScript.pyi"Только разбор текста. Файл не импортируется и не исполняется.
Что делает код
Подключиться осознанно
Общий helper загружает официальный мост из установленного приложения. Он не запускает Resolve, не ставит SDK и не выбирает другой проект.
Прочитать точные поля
Проверяются документированные формы ответа, неизвестность сохраняется. Учебные пределы заданы явно: 5000 элементов/строк каталога, 128 видеодорожек и 32768 на сторону размера.
Отделить наблюдение от изменения
План связывает только наблюдаемый набор. Один apply отправляет не более одной правки: без retry, запасного setter, сохранения проекта и работы с очередью.
Правильно прочитать результат
Код 0 — завершённое наблюдение, план или проверенная узкая область. Код 2 — отказ предварительной проверки. Код 3 — частичность, неизвестность или ожидание ручной проверки; его нельзя повторять вслепую.
Авторский учебный код, не SDK Blackmagic. Сверка документации и офлайн-тесты не сертифицируют нативные вызовы.
В S06 нет AddRenderJob, StartRendering, очистки очереди, автоматического создания папок и сохранения проекта.
Приоритет у интерфейса установленного приложения. SetSettings/GetSettings относятся к проверенному новому контракту; fallback на старые методы не угадывается.
Отчёты могут содержать введённые вами локальные пути и прочитанные ID. Обезличьте их перед публикацией; автоматическое удаление личных данных не обещается.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
Отказ предварительной проверки
Возможная причинаЦель, форма каталога или обязательное значение не совпало с проверенным контрактом.
Следующая проверкаПроверьте отчёт и документацию своей установки. Не добавляйте придуманные псевдонимы и не заменяйте None удобным значением.
Частичное наблюдение
Возможная причинаАктивный источник, свойство или каталог не удалось прочитать полностью.
Следующая проверкаСохраните недоступные элементы в отчёте и выясните причину. Нельзя незаметно отбросить их ради применения размера.
accepted_pending_manual_verification
Возможная причинаSetter принял запрос, но полного чтения текущих Render Settings в этом контракте нет.
Следующая проверкаПроверьте Deliver вручную. Не заявляйте проверенный файл и не используйте код 3 для автоматического повтора.
outcome_unconfirmed
Возможная причинаИзменяющий вызов вернул отказ, исключение или не подтвердился на прежней цели.
Следующая проверкаУстановите фактическое состояние приложения до следующей правки. Автоматического отката и второй попытки нет.
PRACTICE / YOUR OWN EVIDENCE
Проверьте задачу на отдельной копии
- Скачайте комплект и замените TargetDir в settings.example.json на существующую тестовую папку.
- Получите план с точными ID и сверьте каждый передаваемый ключ.
- После одного apply вручную проверьте Deliver и убедитесь, что очередь не пополнилась.
Передан ровно просмотренный объект; ошибка JSON не вызвала подключения; принятый запрос не назван завершённым экспортом.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Где взять полный список параметров?
В Developer/Scripting документации вашей установки. В комплект включён audit_output_contract.py: он читает .pyi без исполнения и показывает объявления, но не проверяет живой Resolve.[16]
Почему JSON не принимает все поля из статьи?
Инструмент ограничен девятью явно проверяемыми полями. Остальные требуют другой семантики и проверки зависимостей; это не заявление об отсутствии этих возможностей у Resolve.[16]
Код 3 означает, что надо повторить apply?
Нет. Это ожидающая ручной проверки, частичная или неизвестная операция. Сначала выясните фактическое состояние; не ставьте автоматический retry по ненулевому коду.[16]
Можно гарантировать, что существующий файл не перезапишется?
Нет такой общей гарантии по одной настройке. Пример не запускает рендер; уникальное имя запрашивается, но папка, финальное имя, существующие файлы и политика записи должны проверяться в этапе выполнения.[16]
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 — pathlib
Обход каталога, абсолютные пути и проверки файлов. Расширение не подтверждает поддержку кодека или успешное декодирование.
- Python — json encoder and decoder
Типы JSON, ensure_ascii, allow_nan и явная сериализация. Наш экспорт не использует default=str для сокрытия неизвестных API-объектов.
- Blackmagic DaVinciResolveScript.pyi — output contracts, reviewed 6 October 2026
Опубликованная на стороннем хостинге копия первичного интерфейса Blackmagic. Для S06 проверены RenderSettings, методы пресетов, форматов, кодеков, размеров и GetSettings/SetSettings. Это не доказательство минимальной версии или живой совместимости. Сверьте .pyi своей установки; проверенный документ не объявляет полный GetRenderSettings.
- Blackmagic Design — official release catalogue, checked 6 October 2026
В официальном каталоге на 6 октября 2026 опубликована Resolve/Studio 21.1.1. Наличие выпуска не подтверждает работу учебных примеров на данной сборке.