Skip to content

Session cut inspection, editing and export

CRE-608 supplies studio.cut_operations and python bin/studio_cut.py for inspecting, saving, and editing an assembly without an app, Node, Pillow, or an index. CRE-609 adds durable export requests through studio.cut_exports. Only explicit export or export-resume commands render; reads and edits never queue generation or move source media.

Commands

Run from the repository root. These examples assume an existing writable production named example-film with a workspace shot registry and supplied media. They are API examples, not a cue to modify a real production. --workspace <directory> goes before the subcommand; otherwise the shared workspace resolver applies.

python bin/studio_cut.py show example-film
python bin/studio_cut.py save example-film assembly-input.json --operation-id cut-save-001
python bin/studio_cut.py edit example-film marker --args '{"time":1.5,"label":"Check entrance"}' --operation-id cut-marker-001
python bin/studio_cut.py edit example-film trim --args '{"clipId":"a","edge":"end","delta":-0.5}' --operation-id cut-trim-001

--name selects a cut (default assembly); names are 1–48 letters/digits/dashes/underscores starting with a letter or digit, with filesystem aliases refused. The resolver locates the document at <name>.cut.json in the production's cut slot (cut/ on the plain layout, 07-cut/ on the numbered one; docs/WORKSPACE.md). python -m studio.cut_cli is equivalent to the shim.

show returns doc, saved, duration, jumpPoints, missing, and an expected revision map. A saved document is normalized in memory; missing media appears in missing. No document, cache, journal or lock is created by inspection. When no cut exists, the existing engine builds a proposal from the registry's live pointers. This proposal uses ffprobe to measure available media and sidecar notes for markers. Missing media/capabilities are reported in missing; an empty or incomplete proposal is not evidence that the production is complete. Without a workspace registry, the command refuses rather than inventing one or migrating legacy repository state.

save accepts a JSON document file. edit applies a verb to current disk state, or the live proposal when no document has been saved. Both require --operation-id. Use a new ID for a new edit, and the identical ID and arguments for a retry. A committed retry returns its original result even if later edits have changed the current cut. Pass --expected '<JSON revision map>' from show when a save/edit must refuse a changed observation. A full save without expected revisions is an explicit replacement and may overwrite intervening edits; operation-based edits preserve unrelated current content.

Edit JSON arguments
trim clipId, edge (start/end), signed delta seconds
split clipId, timeline at seconds
weld No arguments; close gaps in order
set_take clipId, exact production-relative take
add Exact take or a shot with a live pointer; optional desired at and explicit duration seconds
remove clipId; leave the resulting gap
move clipId, desired start seconds
marker time seconds and label

The existing engine owns edit semantics, frame timing, trim memory and one-lane placement. Adding media measures its duration through ffprobe unless an explicit duration is supplied. An explicit duration is the caller's assertion and is not a verified media measurement. Unsupported effects, titles, transitions, and additional tracks refuse. Take selection in a cut does not approve a take or change the shot's live pointer.

API, validation and recovery

The Python API is inspect(slug, name=None, workspace_root=None), save(slug, doc, *, name=None, operation_id, expected=None, workspace_root=None, timeout=10), and edit(slug, op, args=None, *, name=None, operation_id, expected=None, workspace_root=None, timeout=10). Writes return ok, operation_id, production, changed, events, and cut. CLI success is JSON on stdout; errors are JSON on stderr with exits 2 (invalid input/domain refusal), 3 (revision/recovery/lock conflict), or 4 (filesystem failure). Help exits 0.

Writes validate archive policy inside the shared operation, confine the cut and media references after realpath resolution, reject corrupt existing JSON, and require referenced active media. Unsafe paths refuse before normalization can discard them. Invalid edit numbers refuse rather than becoming zero. Ordinary engine normalization still clamps supported document values and reports missing references on read. Media bytes are not hashed; immutability remains the landing contract. Unknown fields retain the existing engine's normalization behavior, not a new lossless schema promise.

Cut bytes and one durable history event join the production transaction journal. Read revisions and required media are checked before commit and during recovery. If a write is interrupted, retain its ID and retry the same request; recognized partial writes roll forward. Changed archive policy, missing required media, unexpected edits, or a conflicting ID refuse and leave the prepared journal for reconciliation. Do not delete a journal to bypass a conflict. Recovery is not instantaneous multi-file visibility or a network-share guarantee.

Participating CLI and Studio save/edit/export writers share the production transaction. The low-level cut.save and legacy cut.export APIs do not; direct callers must migrate before advertising general concurrent mutation safety. Historical app reads retain their compatibility behavior.

Explicit export and recovery

These commands assemble supplied media with the existing ffmpeg engine. They do not invoke a provider or supply generation authorization. Use them for an export the director requested; --encoder software is the CLI default. auto uses the engine's hardware selection with its existing software fallback. ffmpeg and ffprobe must be available for encoding; the core remains stdlib-only.

python bin/studio_cut.py export example-film --export-id assembly-001 --encoder software
python bin/studio_cut.py exports example-film
python bin/studio_cut.py export-status example-film assembly-001
python bin/studio_cut.py export-resume example-film assembly-001
python bin/studio_cut.py export-cancel example-film assembly-001

export --prepare-only records a saved-cut snapshot and input hashes without rendering. --expected accepts the observation from show. Repeating the identical export ID, name, encoder and expectations returns the same request; changed arguments conflict. A new export needs a new ID. IDs are 1–48 lowercase letters/digits/dashes/underscores; unsafe filesystem aliases refuse. Output is <name>--<export-id>.mp4 in the exports slot (cut/exports/ plain, 07-cut/exports/ numbered), with its normal stem-based .meta.json sidecar. This replaces the app's former reusable <name>.mp4 destination: repeated exports retain separate provenance instead of replacing a previous result.

State is durable under .studio/exports/: queued → rendering → rendered → completed, with failed and explicit cancelled outcomes. exports and export-status are read-only and never recover or render. A rendering record means the last recorded phase, not proof a worker is alive. Inspect it after an interrupted process; export-resume explicitly continues. There is no automatic startup worker. A per-request OS lock refuses a second worker, and a per-cut active request prevents overlapping new encodes. Cancellation requires the worker lock to be free, preserves all evidence, and releases an unfinished request; it does not stop a running process or remove files.

The request captures the saved cut and SHA-256 hashes of its media. Encoding checks those hashes before and after; changed or missing inputs refuse rather than silently rendering a different source. Each attempt uses a unique temporary output, so resuming after a parent process dies cannot reuse a file still being written by an orphan encoder. Interrupted encoding may need a new encode; recorded rendered or completed results do not encode again. Failed attempts and orphan temporary files remain available for diagnosis; there is no implicit bulk cleanup.

Finalization installs the completed bytes without replacing an existing destination, then commits the sidecar, export record, completion status and history together. It reads the latest cut under the production lock and appends the export record, preserving intervening edits. The sidecar records the captured cut revision, output hash, actual encoder and source takes. A crash after file installation resumes metadata finalization. Pending transaction recovery checks the output hash; unrelated changes refuse. A complete file can briefly exist before its sidecar, but the durable request accounts for that intermediate state and the export is not reported complete until both are recorded.

Installation requires hard-link support on the production's local volume (verified on this Windows workspace). Unsupported filesystems refuse and retain the rendered request; no overwrite fallback or network-share guarantee is provided. Source immutability is still required during encoding: before/after hashes detect different bytes, not a change-away-and-back history. Archive-policy changes or conflicting external edits require reconciliation. Preserve .studio/exports/, operation journals and history with a production handoff.