RESOLVE API / EDIT LAB / 03

Splitting a clip with Python: Resolve API limits

Calculating a cut is not executing it.

What split planning can do with Resolve API 21.1: identify the item, calculate two intervals and inspect the contract. No hidden keystrokes or invented SplitClip.

THE RESULT

A read-only cut plan with two nonempty intervals; the original item is unchanged.

PYTHON / RESOLVESTUDY MODEL
01item = exact_timeline_item(item_id)

Select an occurrence ID and read its actual boundaries and duration.

CHECK

Not a name or the first clip.

Switching steps changes the explanation. Nothing connects to Resolve or runs on your computer.

01 / BEFORE THE CODE

Set the conditions before you run.

01

Environment and reference

Use Python 3.9+ with a running Resolve Studio instance. Load the Blackmagic module from your own installation. S05 additionally checks the published 21.1 DaVinciResolveScript.pyi mirror; inspect the file shipped with your exact build rather than relying only on an older README.txt.

02

An exact target, not the first clip

Obtain project and timeline IDs from the earlier guides and the item ID from the S03 timeline map. Names help visual inspection but never select the mutation target. Track indices are 1-based and may change after structural edits.

03

Keep the experiment bounded

The script reads state only and has no edit-apply mode. Pause concurrent editing during comparison. Geometry plans cover whole frames only; they do not map source frames, effects, nested timelines or audio samples.

Current Studio edition guidance and the installed scripting documentation take precedence over older console or Free-edition tutorials.[1][4][3]

02 / API CONTRACT

Which object. Which method. Which result.

TimelineItemGetStart(True), GetEnd(True)
Input
The selected occurrence.
Returns
Timeline positions, including subframes when present.
Check
The whole-frame plan does not round subframe audio.
TimelineItemGetDuration(True)
Input
The same occurrence.
Returns
Duration in timeline frames.
Check
This model requires end - start == duration.
TimelineItemGetLeftOffset(True), GetRightOffset(True)
Input
The same object.
Returns
Reported available edge extensions.
Check
Informational read, not a trim instruction.
Авторский helper / Tutorial helperboundary_math(start, end, mode, ...)
Input
Explicit integer coordinates.
Returns
Geometry plan without application access.
Check
No SplitClip / SetStart / SetEnd calls.

Resolve method contracts are documented by Blackmagic; Python and author helpers are labelled separately. The defensive checks are our example design. A non-empty object is not proof of full API compatibility.[3]

03 / UNDERSTAND

What the API contract actually establishes

The published 21.1 DaVinciResolveScript.pyi copy does not declare universal Timeline.SplitClip / SplitClips or TimelineItem.Split / Trim methods, nor the SetStart / SetEnd / SetLeftOffset / SetRightOffset setter pairs. This is a finding about a specific document, not a statement that no future release could add such capabilities.

The old article moved the playhead and sent Ctrl+B through pyautogui, PowerShell or xdotool. That is UI automation with dependencies on focus, keyboard presets, track targeting and linked items. It is not a native Scripting API call and has been removed entirely here.[14]

04 / UNDERSTAND

Timeline frames are not source frames

Our plan uses absolute API coordinates on the current timeline. GetStartFrame separates those from offsets, while the starting timecode belongs to a label system. With a base of 86400, a cut at 86520 is an offset of 120 frames, not source-file frame 86520.

Different source rates, retiming, reverse, freeze frames, nested timelines and compound clips need an explicit time mapping before converting to source In/Out. This example does not infer one. Matching durations alone do not establish linear frame correspondence or preserved audio synchronisation.[14]

05 / UNDERSTAND

Conditions for a valid cut

Splitting [86400, 86640) at 86520 produces [86400, 86520) and [86520, 86640), each 120 frames long. The cut boundary ends the left interval and starts the right one. The total remains 240 frames without duplicating or losing a boundary frame.

A cut equal to start or end is refused because one part would be empty. Fractional coordinates or a mismatch between end-start and GetDuration are not silently rounded. Qualify the item’s units and endpoint convention in your installed Resolve first.

06 / UNDERSTAND

Explore the geometry without touching a project

Change the study clip’s start/end and then the cut or retained interval. Values are recalculated locally. Invalid input removes the previous results, so an obsolete valid answer cannot remain beside a new invalid value.

The displayed intervals do not establish preservation of effects, linked audio, transitions or retiming. The calculator knows nothing about your project. It checks arithmetic and terminology before a separate operation on a copy rather than generating an executable edit call.

EDIT / SCOPE LABPlan ≠ executed edit

Synthetic whole-frame [start, end) coordinates: the end is excluded. No timecode, source-frame conversion or connection to Resolve. This diagram is not a write preview for your project.

Original duration
240
Retained frames
240
Planned intervals
[86400, 86520) + [86520, 86640)

A + B retains the duration. This is not a source In/Out calculation and does not call a native split method.

Local calculation only. Nothing is saved, sent or executed in Resolve.

07 / UNDERSTAND

What can follow the plan

A manual operation on a test copy is a transparent way to execute the prepared task. UI automation belongs in a separate adapter with explicit focus, shortcut, selection and readback requirements. This package does not include such an adapter.

Another path is to assemble a new test timeline from ranges using documented import/append APIs. That is reconstruction, not an in-place edit of an existing TimelineItem. It needs separate qualification of source mapping, endpoints, links, effects, transitions, Color/Fusion and audio. Deleting the original must not be the first step.[14]

08 / UNDERSTAND

Inspect the installed contract without executing code

The additional audit_edit_contract.py reads a local DaVinciResolveScript.pyi as text and parses its AST. It neither imports that file nor connects to Resolve. Its report records the document SHA-256, checked method presence, line numbers and argument names.

document_only_not_live_verified means a declaration was found in that particular file. A new signature still needs semantic review and a live test on a copy. A missing class or invalid file is not evidence that methods are absent: it is an explicitly failed audit.[15][14]

PYTHON / COMPLETE EXAMPLE

Read the code. Then run it deliberately.

Three files + an optional local document checker

Download plan_split_clip.py, resolve_api_edit.py and resolve_api_connection.py into one folder. The bundle contains seven examples, two helpers, the standalone audit_edit_contract.py and RU/EN instructions. The local document checker needs only Python and a .pyi file; it never connects to Resolve.

Download all examples + instructions (.zip) ↓
plan_split_clip.py
"""Read-only split plan. NO native edit, keyboard shortcut or reconstruction is executed."""
import argparse
from resolve_api_connection import connect
from resolve_api_edit import cli_target, inspect_boundary, print_result


def main():
    p = argparse.ArgumentParser(description=__doc__)
    cli_target(p)
    p.add_argument("--item-id", required=True)
    p.add_argument("--at", required=True, help="Absolute whole-frame API boundary")
    a = p.parse_args()
    return print_result(lambda: inspect_boundary(
        connect(), a.project_id, a.timeline_id, a.item_id, "split", at=a.at))


if __name__ == "__main__":
    raise SystemExit(main())

Edit helper — exact targets, plan checks and one mutation

Author tutorial code, not the SDK. Only unlock, one non-ripple deletion and one appended track can mutate Resolve. Split and trim stay read-only. Matching structural reads do not certify effects, routing or the entire project; no retries or automatic rollback are performed.

resolve_api_edit.py
"""Bounded Resolve timeline tutorials, S05. Author code, NOT the Blackmagic SDK.

Only unlock, delete-one (non-ripple) and append-one-track can write to Resolve.
Plan fingerprints bind observed fields, not every project property. Reads are
not atomic, there is no transaction or native timeout, and no automatic retry,
rollback, save, UI automation or file deletion is performed here.
"""
from __future__ import annotations
import copy
import hashlib
import json
import math
import re
from fractions import Fraction

TRACK_TYPES = ("video", "audio", "subtitle")
AUDIO_FORMATS = ("mono", "stereo", "lrc", "lcr", "lrcs", "lcrs", "quad",
                 "5.0", "5.0film", "5.1", "5.1film", "7.0", "7.0film", "7.1", "7.1film") + tuple(
                     "adaptive%d" % n for n in range(1, 37))
MAX_TRACKS = 128  # Tutorial bounds, NOT Resolve product limits.
MAX_ITEMS = 5000


class PreflightError(RuntimeError):
    """No write is issued by this helper when preflight fails."""


def invoke(obj, method, *args):
    fn = getattr(obj, method, None)
    if not callable(fn):
        raise PreflightError("Required method unavailable: " + method)
    try:
        return fn(*args)
    except Exception as exc:
        raise PreflightError("Read failed: " + method + ": " + type(exc).__name__) from exc


def ident(value, label):
    if not isinstance(value, str) or not value.strip() or len(value) > 1024:
        raise PreflightError("Missing or invalid " + label)
    return value


def number(value, label):
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise PreflightError(label + " must be a finite API number (not None/bool/text)")
    if isinstance(value, float) and not math.isfinite(value):
        raise PreflightError(label + " is not finite")
    return Fraction(str(value))


def count(value, label, maximum=MAX_TRACKS):
    n = number(value, label)
    if n.denominator != 1 or not 0 <= n <= maximum:
        raise PreflightError(label + " must be an integer within this tutorial's bound")
    return int(n)


def boolean(value, label):
    if type(value) is not bool:
        raise PreflightError(label + " must be an explicit boolean")
    return value


def text(value, label):
    if not isinstance(value, str):
        raise PreflightError(label + " must be a string")
    return value


def fingerprint(value):
    raw = json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False)
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


def get_target(resolve, project_id=None, timeline_id=None):
    manager = invoke(resolve, "GetProjectManager")
    if not manager:
        raise PreflightError("Project manager unavailable")
    project = invoke(manager, "GetCurrentProject")
    if not project:
        raise PreflightError("No open project")
    timeline = invoke(project, "GetCurrentTimeline")
    if not timeline:
        raise PreflightError("No current timeline")
    pid = ident(invoke(project, "GetUniqueId"), "project ID")
    tid = ident(invoke(timeline, "GetUniqueId"), "timeline ID")
    if project_id is not None and pid != project_id:
        raise PreflightError("Active project does not match --project-id")
    if timeline_id is not None and tid != timeline_id:
        raise PreflightError("Active timeline does not match --timeline-id")
    return project, timeline, pid, tid


def capture(resolve, project_id=None, timeline_id=None):
    """All top-level tracks/items, no recursive flattening and no source-file I/O."""
    project, timeline, pid, tid = get_target(resolve, project_id, timeline_id)
    state = {"project_id": pid, "timeline_id": tid,
             "project_name": text(invoke(project, "GetName"), "project name"),
             "timeline_name": text(invoke(timeline, "GetName"), "timeline name"),
             "base_frame": str(number(invoke(timeline, "GetStartFrame"), "base frame")),
             "start_timecode": text(invoke(timeline, "GetStartTimecode"), "start timecode"),
             "tracks": []}
    objects = {}
    for kind in TRACK_TYPES:
        n = count(invoke(timeline, "GetTrackCount", kind), "track count")
        for index in range(1, n + 1):
            track = {"type": kind, "index": index,
                     "name": text(invoke(timeline, "GetTrackName", kind, index), "track name"),
                     "locked": boolean(invoke(timeline, "GetIsTrackLocked", kind, index), "track lock"),
                     "enabled": boolean(invoke(timeline, "GetIsTrackEnabled", kind, index), "track enable"),
                     "items": []}
            if kind == "audio":
                track["format"] = ident(invoke(timeline, "GetTrackSubType", kind, index), "audio format")
            items = invoke(timeline, "GetItemListInTrack", kind, index)
            if not isinstance(items, list):
                raise PreflightError("Item list unavailable; None is not an empty track")
            if len(items) + len(objects) > MAX_ITEMS:
                raise PreflightError("Snapshot exceeds tutorial item bound")
            for item in items:
                uid = ident(invoke(item, "GetUniqueId"), "timeline item ID")
                if uid in objects:
                    raise PreflightError("Ambiguous duplicate timeline item ID")
                start = number(invoke(item, "GetStart", True), "item start")
                end = number(invoke(item, "GetEnd", True), "item end")
                duration = number(invoke(item, "GetDuration", True), "item duration")
                if end < start or duration < 0:
                    raise PreflightError("Inconsistent item geometry")
                record = {"id": uid, "name": text(invoke(item, "GetName"), "item name"),
                          "start": str(start), "end_raw": str(end), "duration": str(duration),
                          "enabled": boolean(invoke(item, "GetClipEnabled"), "clip enabled")}
                track["items"].append(record)
                objects[uid] = item
            track["items"].sort(key=lambda x: x["id"])
            state["tracks"].append(track)
    get_target(resolve, pid, tid)  # Do not certify data after an observed target switch.
    return state, timeline, objects


def stable_snapshot(resolve, project_id=None, timeline_id=None):
    first, _, _ = capture(resolve, project_id, timeline_id)
    second, timeline, objects = capture(resolve, first["project_id"], first["timeline_id"])
    if first != second:
        raise PreflightError("Observed structure changed during reads; stop and inspect")
    return second, timeline, objects


def locate(state, item_id):
    matches = [(track, item) for track in state["tracks"] for item in track["items"] if item["id"] == item_id]
    if len(matches) != 1:
        raise PreflightError("Exactly one item with --item-id is required; no name/range fallback")
    return matches[0]


def linked_ids(item):
    linked = invoke(item, "GetLinkedItems")
    if not isinstance(linked, list):
        raise PreflightError("Linked-item list unknown; no permission to delete")
    own = ident(invoke(item, "GetUniqueId"), "item ID")
    return sorted(set(ident(invoke(x, "GetUniqueId"), "linked item ID") for x in linked) - {own})


def checked_index(value):
    if type(value) is not int or value < 1 or value > MAX_TRACKS:
        raise PreflightError("Track index must be 1-based and within the tutorial bound")
    return value


def build_plan(state, objects, operation, **options):
    """Pure policy except explicit read-only link/type checks for deletion."""
    expected = copy.deepcopy(state)
    plan = {"schema": "videomontazher.resolve.edit-plan.v1", "operation": operation,
            "before": state, "options": options, "write_required": True,
            "scope": "Top-level structural fields only; not an atomic project snapshot"}
    if operation == "unlock":
        kind, index = options.get("track_type"), checked_index(options.get("index"))
        if kind not in TRACK_TYPES:
            raise PreflightError("Unknown track type")
        matches = [t for t in expected["tracks"] if t["type"] == kind and t["index"] == index]
        if not matches:
            raise PreflightError("Target track does not exist")
        plan["write_required"] = matches[0]["locked"]
        matches[0]["locked"] = False
        plan["call"] = "Timeline.SetTrackLock(type, index, False)"
    elif operation == "delete":
        item_id = ident(options.get("item_id"), "target item ID")
        track, record = locate(state, item_id)
        if track["locked"]:
            raise PreflightError("Target track is locked; unlocking is a separate reviewed task")
        kind = invoke(objects[item_id], "GetType")
        if kind not in ("video", "audio"):
            raise PreflightError("This example deletes only an identified video/audio item")
        links = linked_ids(objects[item_id])
        if links:
            raise PreflightError("Linked items exist; this single-item example will not unlink or expand scope")
        plan["target_type"] = kind
        plan["linked_ids"] = links
        for t in expected["tracks"]:
            t["items"] = [i for i in t["items"] if i["id"] != item_id]
        plan["call"] = "Timeline.DeleteClips([exact_item], False)"
    elif operation in ("ensure_audio", "ensure_video"):
        kind = "audio" if operation == "ensure_audio" else "video"
        index = checked_index(options.get("index"))
        tracks = [t for t in state["tracks"] if t["type"] == kind]
        fmt = options.get("audio_format")
        if kind == "audio" and fmt not in AUDIO_FORMATS:
            raise PreflightError("Explicit supported audio format required")
        if index <= len(tracks):
            if kind == "audio" and tracks[index - 1]["format"] != fmt:
                raise PreflightError("Existing track format differs; not converting or replacing it")
            plan["write_required"] = False
        elif index != len(tracks) + 1:
            raise PreflightError("Only the next missing track can be added in one plan; no fill loop")
        plan["track_type"] = kind
        plan["call"] = "Timeline.AddTrack('audio', explicit_format)" if kind == "audio" else "Timeline.AddTrack('video')"
        expected = None  # New track name/initial flags are read, not predicted.
    else:
        raise PreflightError("Unknown operation")
    plan["expected_structure"] = expected
    plan["token"] = fingerprint(plan)
    return plan


def verify_added(before, after, kind, index, fmt):
    old = before["tracks"]
    new = [t for t in after["tracks"] if t["type"] == kind and t["index"] == index]
    if len(new) != 1 or new[0]["items"] or (kind == "audio" and new[0]["format"] != fmt):
        return False
    stripped = copy.deepcopy(after)
    stripped["tracks"] = [t for t in after["tracks"] if not (t["type"] == kind and t["index"] == index)]
    return len(after["tracks"]) == len(old) + 1 and stripped == before


def run_action(resolve, project_id, timeline_id, operation, apply=False, expect_plan=None, test_copy=False, **options):
    ident(project_id, "explicit --project-id")
    ident(timeline_id, "explicit --timeline-id")
    before, timeline, objects = stable_snapshot(resolve, project_id, timeline_id)
    plan = build_plan(before, objects, operation, **options)
    if not apply:
        return {"status": "plan_only", "plan": plan}
    if not test_copy or not isinstance(expect_plan, str) or expect_plan != plan["token"]:
        raise PreflightError("Apply requires --test-copy and the exact reviewed --expect-plan token")
    # Re-read immediately before dispatch, including links/type for deletion.
    fresh, timeline, objects = stable_snapshot(resolve, project_id, timeline_id)
    fresh_plan = build_plan(fresh, objects, operation, **options)
    if fresh_plan["token"] != expect_plan:
        raise PreflightError("Plan became stale before dispatch; no write issued")
    if not fresh_plan["write_required"]:
        return {"status": "already_satisfied", "write_issued": False, "plan": fresh_plan}
    if operation == "unlock":
        method, args = "SetTrackLock", (options["track_type"], options["index"], False)
    elif operation == "delete":
        method, args = "DeleteClips", ([objects[options["item_id"]]], False)
    elif operation == "ensure_audio":
        method, args = "AddTrack", ("audio", options["audio_format"])
    else:
        method, args = "AddTrack", ("video",)
    fn = getattr(timeline, method, None)
    if not callable(fn):
        raise PreflightError("Documented write method unavailable; no alternative is attempted")
    # From this point even False / an exception may accompany a side effect.
    returned, call_error = None, None
    try:
        returned = fn(*args)  # ONE native write. Never retry automatically.
    except Exception as exc:
        call_error = type(exc).__name__
    result = {"status": "outcome_unconfirmed", "write_issued": True,
              "method": method, "returned_true": returned is True,
              "call_error": call_error, "plan_token": expect_plan,
              "structural_match": False,
              "next": "Read actual state and inspect Resolve. Do not blindly retry or roll back."}
    try:
        after, _, _ = stable_snapshot(resolve, project_id, timeline_id)
        result["after"] = after
        if operation.startswith("ensure_"):
            matched = verify_added(before, after, fresh_plan["track_type"], options["index"], options.get("audio_format"))
        else:
            matched = after == fresh_plan["expected_structure"]
        result["structural_match"] = matched
        if matched and returned is True and call_error is None:
            result["status"] = "verified_observed_scope"
            result["next"] = "Confirm the edit in Resolve. Unobserved properties/effects are not certified."
    except Exception as exc:
        result["readback_error"] = type(exc).__name__ + ": " + str(exc)
    return result


def input_frame(value, label):
    if isinstance(value, bool) or not isinstance(value, (str, int)):
        raise PreflightError(label + " must be integer frame text")
    value = str(value)
    if not value or len(value) > 18 or value.strip() != value or re.fullmatch(r"-?[0-9]+", value) is None:
        raise PreflightError(label + " must be integer frame text")
    n = int(value)
    if abs(n) > 10**12:
        raise PreflightError(label + " exceeds tutorial bound")
    return n


def boundary_math(start, end, mode, at=None, new_start=None, new_end=None):
    """Half-open whole-frame mathematical plan, no source-range conversion."""
    start, end = input_frame(start, "start"), input_frame(end, "end")
    if end <= start:
        raise PreflightError("A positive [start, end) interval is required")
    if mode == "split":
        at = input_frame(at, "cut")
        if not start < at < end:
            raise PreflightError("Cut must be strictly inside the item, not on an edge")
        return {"mode": mode, "before": [start, end], "segments": [[start, at], [at, end]],
                "durations": [at - start, end - at], "duration_preserved": True}
    if mode == "trim":
        new_start, new_end = input_frame(new_start, "new start"), input_frame(new_end, "new end")
        if not start <= new_start < new_end <= end:
            raise PreflightError("Trim must keep a nonempty subinterval; extending handles is outside this plan")
        return {"mode": mode, "before": [start, end], "keep": [new_start, new_end],
                "remove_head": new_start - start, "remove_tail": end - new_end,
                "duration": new_end - new_start, "unchanged": [new_start, new_end] == [start, end]}
    raise PreflightError("Only split/trim mathematical plans are supported")


def inspect_boundary(resolve, project_id, timeline_id, item_id, mode, **values):
    state, _, objects = stable_snapshot(resolve, project_id, timeline_id)
    track, item = locate(state, ident(item_id, "item ID"))
    start, end, duration = (Fraction(item[k]) for k in ("start", "end_raw", "duration"))
    if any(x.denominator != 1 for x in (start, end, duration)) or end - start != duration:
        raise PreflightError("This whole-frame model requires end-start == duration. No rounding or boundary guessing.")
    geometry = boundary_math(int(start), int(end), mode, **values)
    report = {"schema": "videomontazher.resolve.boundary-plan.v1", "status": "read_only_plan_no_executor",
              "project_id": state["project_id"], "timeline_id": state["timeline_id"],
              "track": {"type": track["type"], "index": track["index"], "locked": track["locked"]},
              "item": item, "geometry": geometry, "snapshot_token": fingerprint(state),
              "source_mapping": "not_calculated", "write_issued": False,
              "next": "Manual operation on a copy, or a separately qualified reconstruction adapter. No implicit UI keys."}
    # Optional context is reported explicitly, never silently interpreted as zero.
    for name in ("GetLeftOffset", "GetRightOffset"):
        try:
            report[name] = {"status": "read", "frames_raw": str(number(invoke(objects[item_id], name, True), name))}
        except Exception as exc:
            report[name] = {"status": "unavailable", "error": type(exc).__name__}
    fresh, _, _ = stable_snapshot(resolve, state["project_id"], state["timeline_id"])
    if fresh != state:
        raise PreflightError("Structure changed while preparing the boundary plan")
    return report


def read_audio_tracks(resolve, project_id=None, timeline_id=None):
    """Targeted inventory: does not require clip access, buses or media decoding."""
    def read():
        project, timeline, pid, tid = get_target(resolve, project_id, timeline_id)
        n = count(invoke(timeline, "GetTrackCount", "audio"), "audio track count")
        rows = []
        for index in range(1, n + 1):
            rows.append({"index": index,
                         "name": text(invoke(timeline, "GetTrackName", "audio", index), "track name"),
                         "format": ident(invoke(timeline, "GetTrackSubType", "audio", index), "audio format"),
                         "enabled": boolean(invoke(timeline, "GetIsTrackEnabled", "audio", index), "track enable"),
                         "locked": boolean(invoke(timeline, "GetIsTrackLocked", "audio", index), "track lock")})
        get_target(resolve, pid, tid)
        return {"project_id": pid, "timeline_id": tid, "audio_track_count": n, "tracks": rows,
                "scope": "Track inventory only; not clip channels, buses, routing or audible output"}
    first, second = read(), read()
    if first != second:
        raise PreflightError("Audio track inventory changed during reads")
    return second


def cli_target(parser, required=True):
    parser.add_argument("--project-id", required=required)
    parser.add_argument("--timeline-id", required=required)


def cli_write(parser):
    parser.add_argument("--apply", action="store_true", help="Issue at most one write after review")
    parser.add_argument("--expect-plan", help="Exact token from a separately reviewed plan")
    parser.add_argument("--test-copy", action="store_true", help="Acknowledge this is a disposable test copy")


def print_result(fn):
    try:
        result = fn()
    except Exception as exc:
        print(json.dumps({"status": "stopped", "error": type(exc).__name__ + ": " + str(exc)}, indent=2))
        return 2
    print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))
    return 3 if result.get("status") == "outcome_unconfirmed" else 0

Optional: inspect your local .pyi without execution

This standalone checker only parses a bounded local interface file. Reported declarations belong to that document; they are not a live capability probe or a permission to run a mutation. Use a Python version that supports the file’s syntax.

audit_edit_contract.py
"""Read a LOCAL .pyi as text; never import or execute it. No connection to Resolve.
Presence is documentation evidence, not a live capability test. Missing names
refer only to this exact file, not every past or future Resolve release.
"""
import argparse
import ast
import hashlib
import json
from pathlib import Path

CHECKS = {
    "Timeline": ["GetTrackCount", "AddTrack", "GetTrackSubType", "SetTrackLock",
                 "GetIsTrackLocked", "DeleteClips", "SplitClip", "SplitClips", "TrimClip"],
    "TimelineItem": ["GetType", "GetLinkedItems", "GetStart", "GetEnd", "GetDuration",
                     "GetLeftOffset", "GetRightOffset", "SetStart", "SetEnd",
                     "SetLeftOffset", "SetRightOffset", "Split", "Trim"],
}


def audit(path):
    path = Path(path)
    if path.suffix.lower() != ".pyi":
        raise ValueError("Pass the installed DaVinciResolveScript.pyi, not a script or binary")
    if not path.is_file():
        raise ValueError("A regular local .pyi file is required")
    with path.open("rb") as stream:
        raw = stream.read(2 * 1024 * 1024 + 1)
    if len(raw) > 2 * 1024 * 1024:
        raise ValueError("File exceeds this tutorial's 2 MiB bound")
    tree = ast.parse(raw.decode("utf-8-sig"), filename=path.name)
    result = {}
    for name, wanted in CHECKS.items():
        classes = [n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == name]
        if len(classes) != 1:
            raise ValueError("Expected one top-level class: " + name)
        methods = {n.name: n for n in classes[0].body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))}
        result[name] = {key: {"documented_in_this_file": key in methods,
                             "line": methods[key].lineno if key in methods else None,
                             "parameters": [a.arg for a in methods[key].args.args] if key in methods else []}
                        for key in wanted}
    return {"schema": "videomontazher.resolve.local-contract-audit.v1",
            "file_name": path.name, "sha256": hashlib.sha256(raw).hexdigest(),
            "bytes": len(raw), "status": "document_only_not_live_verified", "classes": result}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--doc", type=Path, required=True)
    args = parser.parse_args()
    try:
        print(json.dumps(audit(args.doc), ensure_ascii=False, indent=2))
        return 0
    except Exception as exc:
        print(json.dumps({"status": "not_audited", "error": type(exc).__name__ + ": " + str(exc)}))
        return 2


if __name__ == "__main__":
    raise SystemExit(main())

Shared connection helper — inspect and download

This file only locates and loads the installed bridge. Native imports can write Python bytecode caches and can block; this is not a sandbox or a timeout mechanism. Use a trusted local installation.

resolve_api_connection.py
"""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

Launch commands

Read a plan without editing

Read a plan without editing
python plan_split_clip.py --project-id "PROJECT_ID" --timeline-id "TIMELINE_ID" --item-id "ITEM_ID" --at 86520

These are study coordinates; replace them with values read from your test target.

Inspect local declarations

Inspect local declarations
python audit_edit_contract.py --doc "PATH_TO/DaVinciResolveScript.pyi"

This additional script is part of the bundle, not a Blackmagic module. It never imports .pyi or calls Resolve.

What the code does

  1. Establish the context

    Read the project and timeline first. A missing application object is different from a valid empty state.

  2. Validate actual values

    Check numeric values and lists for type and range. A method failure never becomes zero, an empty list or a match to the first item.

  3. Build the report

    Keep identity and units explicit. A mathematical plan describes desired geometry but does not grant permission or provide an executor.

  4. Check in the application

    Compare the report with Resolve. The script sends no keyboard events, changes no item edge and does not reconstruct the edit on your behalf.

Boundaries to keep

Offline tests exercise our logic with API doubles. Native calls, external Python builds and the behaviour of your exact Resolve version require a separate local check.

Reading, schema inspection and planning are not editing. Do not add an unverified setter, delete-and-reimport sequence or keyboard shortcut as an implicit fallback.

Reports may contain project, track and clip names. Redact them manually before sharing; the script neither uploads them nor automatically anonymises them.

DEBUG / OBSERVATION FIRST

An error is a starting point, not a diagnosis.

--apply is rejected

Possible reasonThis article supplies no edge-editing executor.

Next checkThis is an intentional argparse refusal. Use the plan for a separate manual check; do not add an unverified setter.

Boundary outside the item

Possible reasonA coordinate was confused with an offset, or the range extends outside the item.

Next checkCheck the API origin and [start, end), then enter integer coordinates in the same system.

end-start differs from duration

Possible reasonThe item does not match this tutorial’s whole-frame model.

Next checkRetain raw values and inspect the item type, subframes and installed documentation. Do not blindly add one.

Document audit failed

Possible reasonThe wrong file was supplied, a class is absent or the file cannot be parsed as .pyi.

Next checkLocate the installed specification. A parse error does not establish absence of a Resolve method.

PRACTICE / YOUR OWN EVIDENCE

Compare the plan with a manual operation

  1. Choose a whole-frame test item and record its ID, start, end and duration.
  2. Run the read-only example and confirm write_issued is false.
  3. Perform the planned geometry manually in a separate copy and compare durations.
  4. Check audio, effects and neighbours separately; the plan does not certify their preservation.
ACCEPT WHEN

The intervals are correct, the script changes nothing, and manual acceptance is kept separate from arithmetic.

This is a checklist for your test, not a report of a test already performed on the website.

FAQ

Questions that matter in practice.

Does this script actually split the clip?

No. It reads an exact item and prepares a verifiable plan. The checked schema does not establish a universal native executor for this task, so the application remains unchanged.[14]

Why not keep a keyboard shortcut?

A keystroke depends on focus, command presets, selection and track targeting. That is separate UI automation, not Scripting API. It cannot be hidden inside a function promising an exact mutation by item ID.

What is the plan useful for?

It records the target, units, intervals and acceptance criteria. That supports manual work or development of a separately qualified adapter. Arithmetic correctness must not be labelled as an executed edit.

Can I pass these ranges to AppendToTimeline?

Not directly. This plan uses timeline coordinates, while startFrame/endFrame in append clip information refer to the source. Qualify source mapping and endpoints separately; new occurrences do not automatically preserve every property of the old one.[14]

My version has a new relevant method. What next?

Record the .pyi and build, then verify the signature, units, side effects and return semantics. Test a new executor on a copy. This guide’s findings are scoped to the reviewed files and do not rule out future functionality.[14]

SOURCES / VERSION BOUNDARIES

Sources, not implied certification.

The source review, offline Python tests and UI checks are different kinds of evidence. None establishes that every example works with every Studio build, operating system or Python version. The date below is the editorial review date, not a live-test certificate.

  1. Blackmagic Design — DaVinci Resolve Studio 21.1 release notes

    8 September 2026. The release announcement assigns Python scripting to Studio. Edition requirements, not a test report for our examples.

  2. Blackmagic Design — Support Center

    Release catalogue checked on 5 October 2026: 21.1.1 is listed. The latest release is not the minimum version of every method.

  3. Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)

    Public mirror of Blackmagic’s original README, maintained by X-Raym. Used for basic signatures; not official hosting and not a 21.1.1 README. Locate your installed Developer/Scripting/README.txt through Resolve’s Help/Documentation menu; exact menu nesting can vary.

  4. Blackmagic Design — Scripting and Automation

    Official description of Studio Python/Lua scripting and integrations. Do not extend Studio capabilities to Free without checking the exact release.

  5. Blackmagic DaVinciResolveScript.pyi 21.1 — published mirror

    S05 checks a published mirror of Blackmagic’s original 21.1 interface file. Third-party hosting; signatures do not replace live verification. Compare your installed Developer/Scripting/DaVinciResolveScript.pyi. Findings about absent split/trim declarations are scoped to that document.

  6. Python — ast: parsing source without executing it

    The local checker reads a bounded .pyi as text and builds an AST without importing or executing it. The interpreter must support its syntax; a parse error does not establish absence of a method.

KEEP BUILDING

Continue with a precise next task.

API / Timeline

Trimming a TimelineItem: a precise plan and API limits

Plan a shorter Resolve item using whole frames, an exclusive right boundary and explicit handle limits. Distinguish source In/Out without unsupported SetStart or SetEnd.

API / Timeline

Read the audio track count with Resolve API

Use GetTrackCount for audio, including empty and disabled tracks, indices, formats and repeated observation. Never confuse a failed read with zero, or tracks with channels.

API / Timeline

Build a timeline clip map

Build a versioned map of project, timeline, tracks and occurrences. Keep empty tracks, raw boundaries and enabled states; compare repeated reads and expose missing fields.

API / Timeline

Move the Resolve playhead with Python

SetCurrentTimecode without fallback methods: plan the seek, anchor the offset, verify project and timeline, require --apply and read back the result. NDF/DF within the selected timebase.

Back to the API library →MCP and AI assistants →