01 / BEFORE THE CODE
До кода — условия запуска.
Сначала выберите контекст
Без --timeline расчёт выполняется в Python 3.8+ без Resolve. С --timeline нужен запущенный Resolve Studio и helper подключения из комплекта. Чистая математика не должна требовать открытого проекта.
Точная частота и единицы
Поддерживаемые частоты этого примера: 24, 25, 30, 48, 50, 60 и 24000/1001, 30000/1001, 48000/1001, 60000/1001. Это граница нашего helper, а не список возможностей Resolve. Дробь задаётся явно; строка 29.97 не заменяется молча на 30000/1001.
Граница безопасного опыта
Метки ограничены одними сутками, целыми кадрами и явно выбранным DF/NDF. Переход через полночь, отрицательные длительности, поля interlaced и субкадровый звук не угадываются. Для чтения приложения остановите воспроизведение; для перемещения playhead используйте тестовую копию и не меняйте цель во время вызова.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
Fraction(seconds_text)- Вход
- Десятичная строка или p/q.
- Возвращает
- Точное рациональное число.
- Проверить
- Не float 12.5 из неизвестного вычисления.
seconds_to_frames(seconds, clock, rounding)- Вход
- Длительность, точная частота, явная политика.
- Возвращает
- frames_exact, offset_frames, error_seconds_exact.
- Проверить
- Отрицательная длительность отклоняется.
seconds_at_timeline(resolve, ...)- Вход
- Те же данные и открытая цель.
- Возвращает
- Offset, state, API-координата и метка.
- Проверить
- Никаких seek, append или render.
GetStartFrame()- Вход
- Без аргументов, только в --timeline.
- Возвращает
- Начальная координата API.
- Проверить
- Прибавить один раз после округления offset.
Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[3]
03 / UNDERSTAND
Секунды длительности — не поля часов таймкода
Физическая длительность t переводится в кадры как t × точная частота. Для 100 секунд при 24000/1001 получается 2400000/1001 кадра, а не 2400. В то же время NDF-метка 00:01:40:00 обозначает 2400 кадров при номинале 24. Она соответствует 100.1 секунды в этой временной базе.
Режим DF/NDF меняет обозначение кадра, но не количество кадров, приходящееся на заданную физическую длительность при одной частоте. Поэтому чистому seconds_to_frames не нужен выбор нумерации. --mode появляется только с --timeline, когда к результату добавляется стартовая метка и форматируется целевой таймкод.[9][12]
04 / UNDERSTAND
Четыре политики вместо скрытого round
exact требует целого результата и иначе выдаёт ошибку. floor выбирает нижнюю границу кадра; ceil — верхнюю. nearest выбирает ближайшую, а ровно половину для неотрицательной длительности округляет вверх. Это явно заданная half-up-политика, не стандартное поведение Python round на ничьей.
Например, 0.02 секунды при 25 fps дают 0.5 кадра: floor = 0, ceil = 1, nearest = 1, exact — ошибка. Python round использует ties-to-even, поэтому round(0.5) дал бы 0. Ни одна политика не является правильной для всех задач: точка playhead, начало отрезка и конец диапазона могут требовать разных правил.[13]
05 / UNDERSTAND
Результат включает ошибку округления
Для 12.5 секунды при 24000/1001 точный ответ равен 300000/1001 кадра. При nearest выбираются 300 кадров, чья длительность — 12.5125 секунды. Ошибка составляет +0.0125 секунды, то есть 1/80. В отчёте она хранится как точная строковая дробь, а не как утверждение «попали точно».
Определение ошибки: offset_frames / rate − исходные секунды. Отрицательный знак означает выбранную более раннюю границу, положительный — более позднюю. Короткая десятичная частота 23.976 не подменяет 24000/1001. Ввод секунд берётся строкой до вычислений: создание Fraction из уже округлённого float не восстанавливает исходное намерение.[9]
06 / UNDERSTAND
Посмотрите, как меняются число и ошибка
В калькуляторе выберите «Секунды → Кадры», 25 fps и значение 0.02. Переключайте exact, floor, ceil и nearest. При exact численный результат должен исчезнуть; остальные режимы показывают соответствующие кадры и знак ошибки. Изменение policy не означает изменение FPS проекта.
Затем выберите 24000/1001, 12.5 секунды и nearest. Увидите 300 кадров и ошибку 1/80 секунды. Метка рассчитывается относительно указанного start_timecode; API coordinate — относительно отдельного base. Это объяснение на введённых данных, а не подтверждение совместимости вашей установленной сборки.
Точные кадры: 240. Ошибка, секунды: 0.
Кадр API и метка начала — независимые опорные значения, одно не вычисляется из другого. Калькулятор ограничен одними сутками таймкода, целокадровыми метками и перечисленными частотами. Настройки и границы вашего проекта он не проверяет. Ввод не отправляется и не сохраняется.
07 / UNDERSTAND
Привязка к API после квантования; интервалы отдельно
В --timeline сначала читаются реальные часы и ID цели, затем проверяется выбранная частота и вычисляется offset. Координата равна start_frame + offset_frames. Не используйте round(base + seconds × fps) с неявной политикой: математическая опора и решение о границе должны быть видны независимо.
Функция вычисляет точку, не In/Out range. Для интервала нельзя бездумно округлить оба конца одинаково или перенести inclusive/exclusive-конвенцию другого API. Чтение возвращает inside_conservative_bounds, но не переносит указатель и не вставляет клипы. Отрицательные секунды отклоняются, а не превращаются через max(0, seconds) в другую задачу.[3]
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Скачайте seconds_to_timeline_frames.py и resolve_api_timecode.py. Для чтения приложения или перехода playhead нужен также resolve_api_connection.py. В комплекте шесть примеров, два helper и инструкции RU/EN. Чистые конвертеры не импортируют модуль Blackmagic; расчёт секунд подключается только с --timeline.
Скачать все примеры с инструкцией (.zip) ↓"""Exact seconds -> offset; optional read-only timeline anchoring. No float/default FPS."""
import argparse
import json
from resolve_api_timecode import Clock, seconds_to_frames, seconds_at_timeline
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("seconds", help="Exact text: 12.5 or 1001/24000, not a timecode label")
p.add_argument("--rate", required=True)
p.add_argument("--rounding", choices=("exact", "floor", "ceil", "nearest"), required=True)
p.add_argument("--timeline", action="store_true", help="Read the open timeline to anchor the offset")
p.add_argument("--mode", choices=("ndf", "df"), help="Required only with --timeline")
a = p.parse_args()
if a.timeline and not a.mode:
p.error("--timeline also requires --mode")
if not a.timeline and a.mode:
p.error("DF/NDF labels are irrelevant to pure seconds conversion; omit --mode")
if a.timeline:
from resolve_api_connection import connect
result = seconds_at_timeline(connect(), a.seconds, a.rate, a.mode, a.rounding)
else:
# NDF here only constructs the rate carrier; seconds math never uses label mode.
result = seconds_to_frames(a.seconds, Clock(a.rate, "ndf"), a.rounding)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Timecode-helper — точная математика, опоры и контролируемый переход
Авторская реализация, не SDK Blackmagic. DF/NDF-метки отделены от секунд длительности. Clock принимает явно выбранную точную частоту, а не угадывает её из сырой строки FPS. Только move_playhead с apply может вызвать setter, и только один раз.
"""S04 author helpers: exact timecode math and explicitly scoped Resolve reads.
Python 3.8+. Standard library only. Not a Blackmagic SDK, transaction or sandbox.
String timecode policy: HH:MM:SS:FF (NDF) / HH:MM:SS;FF (DF), within one 24h day.
Supported timebases: 24/25/30/48/50/60 and 24000/30000/48000/60000 over 1001.
DF is deliberately limited to 30000/1001 and 60000/1001. No silent rate aliases.
Only move_playhead(..., apply=True) calls a setter, once, after reviewed guards.
"""
from dataclasses import dataclass
from fractions import Fraction
import hashlib
import json
import re
from typing import Any, Dict
class ClockError(ValueError):
"""Input or observed state does not meet this example's contract."""
class OutcomeUnknown(RuntimeError):
"""A setter was dispatched but its result was not fully verified. Do not retry."""
def integer(value: Any, name: str) -> int:
# bool is a subclass of int; never accept it as a frame index.
if type(value) is not int:
raise ClockError(name + " must be an integer, not a float, bool or string")
return value
def exact_number(value: Any, name: str) -> Fraction:
if isinstance(value, Fraction):
return value
if type(value) is int:
return Fraction(value)
if not isinstance(value, str) or len(value) > 80:
raise ClockError(name + " must be exact decimal/rational text (not float)")
# Avoid scientific exponents/unbounded integers and locale-dependent commas.
if not re.fullmatch(r"[+-]?[0-9]{1,18}(?:\.[0-9]{1,18}|/[0-9]{1,18})?", value):
raise ClockError(name + " requires ASCII decimal or numerator/denominator text")
try:
return Fraction(value)
except (ValueError, ZeroDivisionError) as exc:
raise ClockError(name + " is invalid") from exc
SUPPORTED = {Fraction(x): x for x in (24, 25, 30, 48, 50, 60)}
SUPPORTED.update({Fraction(x * 1000, 1001): x for x in (24, 30, 48, 60)})
@dataclass(frozen=True)
class Clock:
rate: Fraction
mode: str
def __post_init__(self):
rate = exact_number(self.rate, "rate")
object.__setattr__(self, "rate", rate)
if rate not in SUPPORTED:
raise ClockError("Unsupported/ambiguous rate. Use e.g. 24 or 30000/1001, not 29.97")
if self.mode not in ("ndf", "df"):
raise ClockError("mode must explicitly be ndf or df")
if self.mode == "df" and rate not in (Fraction(30000, 1001), Fraction(60000, 1001)):
raise ClockError("DF example supports only 30000/1001 and 60000/1001")
@property
def nominal(self) -> int:
return SUPPORTED[self.rate]
@property
def drop(self) -> int:
return self.nominal // 15 if self.mode == "df" else 0
@property
def frames_per_day(self) -> int:
return self.nominal * 86400 - self.drop * (1440 - 144)
def frames_to_timecode(frames: int, clock: Clock) -> str:
"""Actual whole frames since TC zero -> label. No wrapping or rounding."""
frames = integer(frames, "frames")
if not 0 <= frames < clock.frames_per_day:
raise ClockError("Frame count must be within one timecode day; no negative/wrap")
labelled = frames
if clock.drop:
# Independent implementation of the ten-minute/minute numbering rule.
per_ten = clock.nominal * 600 - clock.drop * 9
per_short_minute = clock.nominal * 60 - clock.drop
tens, rest = divmod(frames, per_ten)
skipped_minutes = max(0, (rest - clock.drop) // per_short_minute)
labelled += clock.drop * (tens * 9 + skipped_minutes)
seconds, ff = divmod(labelled, clock.nominal)
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
separator = ";" if clock.mode == "df" else ":"
return "{:02d}:{:02d}:{:02d}{}{:02d}".format(hours, minutes, seconds, separator, ff)
def timecode_to_frames(timecode: str, clock: Clock) -> int:
"""Label -> actual whole frames since TC zero. Invalid labels never become 0."""
if not isinstance(timecode, str):
raise ClockError("timecode must be text")
match = re.fullmatch(r"([0-9]{2}):([0-9]{2}):([0-9]{2})([:;])([0-9]{2})", timecode)
if not match:
raise ClockError("Expected HH:MM:SS:FF (NDF) or HH:MM:SS;FF (DF)")
h, m, s, sep, f = match.groups()
h, m, s, f = map(int, (h, m, s, f))
if h >= 24 or m >= 60 or s >= 60 or f >= clock.nominal:
raise ClockError("Timecode field outside this clock's range")
if sep != (";" if clock.mode == "df" else ":"):
raise ClockError("Timecode separator disagrees with explicit DF/NDF mode")
if clock.drop and m % 10 != 0 and s == 0 and f < clock.drop:
raise ClockError("This label is skipped by drop-frame numbering")
minutes = h * 60 + m
count = (h * 3600 + m * 60 + s) * clock.nominal + f
return count - clock.drop * (minutes - minutes // 10)
def offset_to_timecode(offset: int, start_timecode: str, clock: Clock) -> str:
offset = integer(offset, "offset")
if offset < 0:
raise ClockError("Negative offsets are outside this tutorial's scope")
return frames_to_timecode(timecode_to_frames(start_timecode, clock) + offset, clock)
def timecode_to_offset(timecode: str, start_timecode: str, clock: Clock) -> int:
offset = timecode_to_frames(timecode, clock) - timecode_to_frames(start_timecode, clock)
if offset < 0:
raise ClockError("Timecode precedes start; no implicit midnight wrap")
return offset
def seconds_to_frames(seconds: Any, clock: Clock, rounding: str) -> Dict[str, Any]:
"""Duration -> offset, with exact arithmetic and an explicit rounding policy."""
value = exact_number(seconds, "seconds")
if value < 0:
raise ClockError("Negative durations are rejected, not clamped to zero")
exact = value * clock.rate
floor = exact.numerator // exact.denominator
if rounding == "exact":
if exact.denominator != 1:
raise ClockError("Duration is not a whole frame; choose a rounding policy")
result = floor
elif rounding == "floor":
result = floor
elif rounding == "ceil":
result = floor + (exact.denominator != 1)
elif rounding == "nearest":
# Documented half-up for non-negative durations, not Python's ties-to-even.
result = floor + ((exact - floor) >= Fraction(1, 2))
else:
raise ClockError("rounding must be exact, floor, ceil or nearest")
return {"rate": str(clock.rate), "seconds_exact": str(value), "frames_exact": str(exact),
"offset_frames": result, "rounding": rounding,
"error_seconds_exact": str(Fraction(result, 1) / clock.rate - value)}
def call(obj: Any, method: str, *args: Any) -> Any:
try:
fn = getattr(obj, method, None)
if not callable(fn):
raise ClockError(method + " is not callable")
return fn(*args)
except ClockError:
raise
except Exception as exc:
raise ClockError(method + " failed; original exception retained") from exc
def text(value: Any, field: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ClockError(field + " must be non-empty text")
return value
def context(resolve):
manager = call(resolve, "GetProjectManager")
project = call(manager, "GetCurrentProject")
if project is None:
raise ClockError("No open project")
timeline = call(project, "GetCurrentTimeline")
if timeline is None:
raise ClockError("No active timeline")
identity = {"project_id": text(call(project, "GetUniqueId"), "project ID"),
"timeline_id": text(call(timeline, "GetUniqueId"), "timeline ID")}
return project, timeline, identity
def observation(resolve, include_playhead=False):
_, timeline, identity = context(resolve)
result = dict(identity)
result.update({"start_frame": integer(call(timeline, "GetStartFrame"), "start_frame"),
"end_frame": integer(call(timeline, "GetEndFrame"), "end_frame"),
"start_timecode_raw": text(call(timeline, "GetStartTimecode"), "start timecode"),
"fps_raw": text(call(timeline, "GetSetting", "timelineFrameRate"), "timeline FPS")})
if include_playhead:
page = call(resolve, "GetCurrentPage")
if page not in ("cut", "edit", "color", "fairlight", "deliver"):
raise ClockError("Open Cut/Edit/Color/Fairlight/Deliver manually; no page switch is made")
result["page"] = page
result["current_timecode_raw"] = text(call(timeline, "GetCurrentTimecode"), "current timecode")
_, _, after = context(resolve)
if identity != after:
raise ClockError("Active target changed while reading")
return result
def read_clock(resolve, include_playhead=False):
"""Two matching observations, NOT an atomic snapshot or a process timeout."""
a = observation(resolve, include_playhead)
b = observation(resolve, include_playhead)
if a != b:
raise ClockError("Clock observations differ; stop playback/editing and inspect")
return dict(a, observation_policy="two_matching_reads_not_atomic")
# These labels are accepted only AFTER the caller explicitly selects the exact rate.
# The report retains the raw label and marks that rate origin as user-declared.
DISPLAY_LABELS = {Fraction(24000, 1001): "23.976", Fraction(30000, 1001): "29.97",
Fraction(48000, 1001): "47.952", Fraction(60000, 1001): "59.94"}
def validate_clock(state, clock: Clock):
raw = state["fps_raw"].strip()
has_df = raw.upper().endswith(" DF")
numeric = raw[:-3].strip() if has_df else raw
actual = exact_number(numeric, "observed FPS label")
alias = DISPLAY_LABELS.get(clock.rate)
if actual != clock.rate and (alias is None or actual != Fraction(alias)):
raise ClockError("Explicit rate conflicts with the observed timeline FPS label")
if has_df and clock.mode != "df":
raise ClockError("Observed FPS label says DF but NDF was requested")
# Absence of DF in the FPS string does not itself prove NDF; inspect start label.
timecode_to_frames(state["start_timecode_raw"], clock)
if "current_timecode_raw" in state:
timecode_to_frames(state["current_timecode_raw"], clock)
def seconds_at_timeline(resolve, seconds, rate, mode, rounding):
clock = Clock(rate, mode)
state = read_clock(resolve)
validate_clock(state, clock)
result = seconds_to_frames(seconds, clock, rounding)
result.update({"state": state, "rate_origin": "explicit_user_selection_checked_against_label",
"absolute_timeline_frame": state["start_frame"] + result["offset_frames"],
"timecode": offset_to_timecode(result["offset_frames"], state["start_timecode_raw"], clock),
"inside_conservative_bounds": state["start_frame"] + result["offset_frames"] < state["end_frame"],
"operation": "calculation_only_no_seek_or_insert"})
return result
def make_plan(state, offset, clock):
validate_clock(state, clock)
offset = integer(offset, "offset")
absolute = state["start_frame"] + offset
if offset < 0 or not state["start_frame"] <= absolute < state["end_frame"]:
raise ClockError("Target outside conservative [start, end) guard; end endpoint is excluded")
target_tc = offset_to_timecode(offset, state["start_timecode_raw"], clock)
plan = {"state": state, "offset_frames": offset, "absolute_timeline_frame": absolute,
"target_timecode": target_tc, "rate": str(clock.rate), "mode": clock.mode,
"rate_origin": "explicit_user_selection_checked_against_label"}
payload = json.dumps(plan, sort_keys=True, ensure_ascii=True, separators=(",", ":"))
plan["plan_digest"] = hashlib.sha256(payload.encode("utf-8")).hexdigest()
return plan
def move_playhead(resolve, offset, rate, mode, apply=False, expected_project_id=None,
expected_timeline_id=None, expected_plan_digest=None):
"""Plan only unless explicit guards match. Setter dispatched at most once."""
if type(apply) is not bool:
raise ClockError("apply must be bool")
clock = Clock(rate, mode)
before = read_clock(resolve, include_playhead=True)
plan = make_plan(before, offset, clock)
if not apply:
return dict(plan, status="plan_only", setter_called=False)
if (not expected_project_id or not expected_timeline_id or not expected_plan_digest or
expected_project_id != before["project_id"] or expected_timeline_id != before["timeline_id"] or
expected_plan_digest != plan["plan_digest"]):
raise ClockError("--apply requires matching reviewed project, timeline and plan digest")
if read_clock(resolve, include_playhead=True) != before:
raise ClockError("Target or playhead changed since the plan; no setter called")
_, timeline, identity = context(resolve)
if any(identity[k] != before[k] for k in identity):
raise ClockError("Target changed before dispatch")
if before["current_timecode_raw"] == plan["target_timecode"]:
return dict(plan, status="already_at_target", setter_called=False)
try:
# The only setter in this file. No fallback, polling, save, playback or retry.
accepted = call(timeline, "SetCurrentTimecode", plan["target_timecode"])
after = read_clock(resolve, include_playhead=True)
static_before = {k: v for k, v in before.items() if k != "current_timecode_raw"}
static_after = {k: v for k, v in after.items() if k != "current_timecode_raw"}
if accepted is not True or static_before != static_after:
raise ClockError("Setter not confirmed or target/clock changed")
if after["current_timecode_raw"] != plan["target_timecode"]:
raise ClockError("Read-back does not match requested timecode exactly")
except Exception as exc:
raise OutcomeUnknown("Playhead setter dispatched; inspect Resolve before any retry") from exc
return dict(plan, status="read_back_matched_check_ui", setter_called=True, after=after,
ui_verified=False)
Общий 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
Команды запуска
Чистая математика без Resolve
python seconds_to_timeline_frames.py 12.5 --rate 24000/1001 --rounding nearestОжидается offset 300 и error_seconds_exact = 1/80.
Точная привязка 12.5 секунды к 24 NDF
python seconds_to_timeline_frames.py 12.5 --rate 24 --rounding exact --timeline --mode ndfТребует согласованной открытой цели. Offset = 300; base берётся у API, не из статьи.
Что делает код
Сохранить точный ввод
Секунды и частота принимаются текстом; Fraction не получает скрыто округлённый float.
Выбрать сетку
exact/floor/ceil/nearest обрабатываются отдельно, half-up явно реализован без round.
Вычислить ошибку
Ошибка возвращается точной дробью относительно исходной длительности.
Прочитать по запросу
--timeline включает подключение и base, но не добавляет изменяющих вызовов.
Все преобразования в helper — авторские функции, не методы Blackmagic. Успешный расчёт не подтверждает FPS, настройки проекта или возможность вызова в установленном Resolve.
При ошибке старый результат не подставляется. Native-вызовы не имеют добавленного тайм-аута; два одинаковых чтения не создают атомарный снимок и не блокируют интерфейс.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
Duration is not a whole frame
Возможная причинаВыбран exact, но результат дробный.
Следующая проверкаОсознанно выберите floor/ceil/nearest или скорректируйте вход.
Negative durations rejected
Возможная причинаПередана отрицательная длительность.
Следующая проверкаПроверьте origin и задачу; не меняйте знак и не обнуляйте молча.
Unsupported/ambiguous rate
Возможная причинаВместо точной дроби передана 23.976/29.97.
Следующая проверкаУточните реальную временную базу и задайте её явно.
Выход за границы таймлайна
Возможная причинаВремя больше содержимого, хотя арифметический ответ корректен.
Следующая проверкаУчитывайте inside_conservative_bounds перед отдельным действием; расчёт сам ничего не запускает.
PRACTICE / YOUR OWN EVIDENCE
Проверьте половину кадра и дробную частоту
- Посчитайте 0.02 секунды при 25 fps четырьмя политиками.
- Посчитайте 12.5 секунды при 24000/1001 и проверьте 1/80 секунды ошибки.
- В --timeline сравните base + offset с API coordinate; отрицательный ввод должен быть отклонён.
Точный результат не теряется, политика видна, знак ошибки верен. Начало прибавлено один раз, API не изменён.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Почему нет режима DF без --timeline?
DF относится к меткам. При переводе физической длительности в число кадров нужна частота, а не нумерация.[12]
Можно использовать round для ближайшего кадра?
Можно в собственной политике, но его ties-to-even нужно явно документировать. Здесь выбрана half-up для неотрицательных значений.[13]
Что выбрать для точного аудиомонтажа?
Целокадровая сетка недостаточна для sample-accurate задачи. Нужны частота дискретизации, единицы конкретного аудио API и отдельные преобразования.
Скрипт меняет FPS или начало?
Нет. Он только вычисляет, а в --timeline дополнительно читает состояние. SetSetting и SetStartTimecode не вызываются.
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 — fractions.Fraction
Точное представление прочитанного числового текста. Десятичная метка 23.976 не доказывает, что исходная частота равна 24000/1001; сырая строка сохраняется.
- FFmpeg — timecode.c (primary implementation)
Первичная реализация различает номинал, рациональную частоту, DF-пропуски и флаг перехода суток. Наш независимый учебный helper имеет более узкий явный scope: две DF-частоты, строгие метки и запрет автоматического wrap.
- Python — round and tie handling
Python round использует округление ничьей к чётному. В S04 nearest намеренно определён как half-up для неотрицательных длительностей; exact, floor и ceil выделены отдельно.