Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
PROOFCUT_TTSNoPython executable of a venv with qwen-tts and CUDA torch. Unlocks the 'vo-synth' tool.
PROOFCUT_VLMNoPython executable of a venv with torch, transformers, bitsandbytes and Pillow (CUDA GPU). Unlocks the 'describe' tool.
PROOFCUT_FACENoPython executable of a venv with insightface, onnxruntime and opencv-python. Unlocks the 'reframe-detect' tool.
PROOFCUT_MELTNoPath to the MLT melt executable. Overrides automatic detection (including Kdenlive's flatpak).
PROOFCUT_WHISPERNoPath to the whisper executable/installation. Overrides PATH lookup for whisper.
PROOFCUT_TTS_MODELNoPath to a local Qwen3-TTS snapshot.
PROOFCUT_TTS_VOICENoDirectory holding a reference clip of the voice for TTS.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
pingA

Check that the proofcut MCP server is alive, and report its version.

project is the project every tool addresses when path is omitted, and bound_by says how it was chosen: -C at startup, or cwd because the server was started inside a project. Both are null when it is unbound.

doctorA

Probe every external binary proofcut depends on, and name each one's trap.

Takes no project — it answers the question asked before there is one. Report-only: nothing is installed and nothing is written. ok reads the required section alone; the four optional entries each gate one feature (cards, describe, reframe_detect, vo_synth) and everything else works without them.

Every failing entry carries the fix, not just the ✗ — where melt actually lives, why PyPI's auto-editor is the wrong program, what to set on a box with no display.

server says which project this server is bound to and how (ping's own answer), since that decides which path a call may name.

initA

Create a proofcut project directory at path.

Writes proofcut.json and the empty assets/, cache/, media/ and renders/ directories, and nothing else — no media, no timeline. Refuses a directory that already holds a project rather than resetting it, so it is safe to call when unsure. Next is import_media, then a transcript, then seed_timeline.

migrate_projectA

Bring an older project manifest forward to the current schema version.

Every other tool refuses a project written by an older proofcut rather than guessing at a layout it does not recognise; this is what clears that. It is forward-only, and it copies the manifest into cache/history/ before writing. plan=True reports the version and the steps without writing, which is how to ask what a project is before deciding to change it. A project whose manifest is still lucid.json (written before the rename) is renamed to proofcut.json first, reported as the first step.

import_mediaA

Register a media file with the project, probing it with ffprobe.

Links the media by default rather than copying it. Returns the clip record, including the clip_id every other tool takes, and audio_streams — how many the container holds, since every other audio field on the record describes only the first.

A container with more than one audio stream is refused rather than registered as if the first were the recording: whisper picks a stream of its own and MLT picks again at render, so the others would be missing from the film with every check clean. mix=True sums them into one track (two mics of one performance); audio_stream=k keeps one, numbered from 0 in ffmpeg's own audio ordering. Either writes a derived copy under cache/mixed/ that every later op reads without knowing it.

A chapter list or the data/text track it rides on — a movie rip's own inherited from its parent film — is stripped unconditionally, with no flag to opt out: there is no legitimate choice to offer, unlike the audio-stream one. strip/stripped on the record say so when it happened; duration is corrected from the real video/audio streams either way it was detected.

sheet=True by default: a contact_sheet of the clip's first ten seconds rides along on the returned record — cached frames from thumbnail(), so the clip's own opening (credits, black, a slate) is seen before it is cued to a shot rather than discovered after. Pass sheet=False to skip it. --no-sheet on the CLI.

list_mediaA

List media files under source_dir that import_media could register.

What hands an unattended agent source paths on a real job, since an agent confined to proofcut's tools (the agent panel's --tools ToolSearch) has no directory listing of its own (HISTORY.md § The seventh queue item, decided and built). A filename filter, not a probe — import_media is still what decides a file is actually usable. Each entry's already_imported is checked against this project's own registered clips, so a repeated call does not keep re-suggesting footage already on the asset list. source_dir names wherever the footage lives and is not confined to the project.

clip_roleA

Read or set a clip's import role — voiceover vs footage.

Called with no role and no reset it just reports what is stored; role must be "voiceover" or "footage" (ops.CLIP_ROLES); reset clears it back to undeclared.

This changes nothing about how transcribe/attach_transcript or describe treat the clip. Both already gate on their own evidence — a transcript file, has_video — and neither reads this field, so an undeclared clip is exactly as eligible for both as it always was. It is the assets pane's grouping, purely, and setting one is not a schema bump for that reason: an additive optional field on an existing clip record.

clip_rmA

Un-register a clip import_media added, when nothing depends on it yet.

Refused, naming every reason, if the clip is on the timeline, cued, held, the music bed's own clip, marked unspoken, transcribed or described — clear those first (cue_rm/hold_rm/unspoken_rm/music reset=True, or proofcut undo) or use undo back to before the import instead. The media on disk is never touched either way.

attach_transcriptA

Ingest an existing word-timed whisper JSON as this clip's transcript.

Checks the transcript against itself for near_duplicates — adjacent runs of words that sound like the same line said twice. That is a retake verify can never catch once both takes are cut into the edit, since nothing then disagrees with the timeline. A hit is not a verdict: a deliberate callback line looks the same as a swallowed retake here.

Also reports suspect_durations, overlaps and repeats. An overlaps seam is a retake splice whisper read straight across, interleaving both takes and inventing words nobody said — check it before drawing anything derived from this transcript. repeats is a back-to-back duplicated phrase, the shape a retake makes when it survives as distinct words rather than as a seam — a different subset of retakes than overlaps finds, not a smaller one. Use transcript_checks to see all four again later.

transcribeA

Transcribe a clip's own media with whisper, and attach the result.

attach_transcript's ASR-driven sibling: use that when the recording already has a transcript, this when it needs one made. Takes minutes on a long recording — there is no timeout, so let it run. Reports near_duplicates, suspect_durations, overlaps and repeats the same way attach_transcript does.

It replaces whatever transcript the clip already had, and it is the one mutation undo cannot reach: a transcript is its own file, so this writes neither the manifest nor the timeline and nothing is snapshotted. There is no cache either — a second call spends the same minutes again. get_transcript first if a transcript might already be there.

hearA

What does clip_id's source audio actually say between start and end?

Use this when the transcript and the audio might disagree — a word with a suspect duration, a hole with no words in it, a stretch that reads clean but sounds wrong. It runs the same short-overlapping-window pass verify(windowed=True) runs, over the clip's own media across the span (source seconds), and comes back with heard_words/heard_text beside the attached transcript's own words over that span (transcript_words). No need to seed, export and verify to hear your source material.

Reports, never attaches — nothing is written and no word index moves. Where the two disagree, cut_by_time addresses what the transcript has no word for. heard_words can be empty: silence is a real answer. One whisper run over the span; end past the clip is refused.

get_transcriptA

Read a clip's transcript.

With search, returns each match as a word range ready to hand to cut_by_transcript — prefer this to reading the whole transcript. With first/last, returns that window of words. Indices are inclusive.

At most limit words come back per call. total_words is the whole transcript, last_word where this reply stopped, and next_first — only present when words were left out — is the first to ask for next.

resolve_phraseA

Resolve a phrase to a word range against clip_id's transcript.

What phrase= on cue_add/cue_rm/unspoken_add/unspoken_rm/vo_extend/music/ locate calls internally, exposed on its own so a resolution — including its full ambiguity list — can be inspected without attempting a write. Companion to get_transcript with search=, which lists every match with no cursor/occurrence/fuzzy; this picks exactly one, or explains why it can't.

after skips matches at or before that word index (forward cursor, -1 means from the start). More than one exact match with no occurrence given fails with every candidate's word range and text — pass occurrence (1-based) to pick one, or narrow the phrase. Zero exact matches falls back to a fuzzy match (fuzzy=False to refuse instead) — ratio is set only on a fuzzy hit, never disguised as exact. Read-only.

transcript_checksA

Re-check an already-attached transcript against itself.

Returns the same four findings attach_transcript does — near_duplicates, suspect_durations, overlaps, repeats — for a transcript attached earlier, whose findings were reported once and are otherwise gone. Omit clip_id for every clip that has a transcript.

Read overlaps before anything derived from this transcript is drawn on screen. A seam there is whisper reading across a retake splice and interleaving both takes, which invents words nobody said — and they read as ordinary English, so a human proofread finds some and is blind to the rest. repeats catches the other shape a retake takes: one that survived transcription as distinct, cleanly-timed duplicated words rather than as an interleaved seam. Reads only; it never writes.

attribute_speakersA

Label each word with the mic that was loudest while it was spoken.

For a co-hosted recording captured on one mic per speaker. It is one pass over the transcript that is already attached — never a second ASR run, and transcribing each mic separately is measured and dead: half of each mic's own transcript is the other person, at every isolation tried. Transcribe once, from the mix or either mic, then call this.

streams are ffmpeg audio ordinals into the registered container (0, 1), and labels names them in the same order — one label per stream, defaulting to speaker1, speaker2. The speaker lands on the word: it is a label and never an address, so every cue, description, mark, music anchor and caption still resolves through (clip_id, word_index) and nothing else moves.

It reports; it does not decide below the floor. apply is off by default. The rule is ~99% correct per word on clear speech and at chance on words spoken over each other, and margin_db is what half-knows the difference — anything under it comes back in ambiguous_spans to go and listen to, with the three words either side. Read unmeasurable separately from ambiguous: it means the mics ran out before the transcript did, which is a different recording problem. Applying keeps any label already on a word this refuses to call.

describeA

Describe footage in fixed windows, so b-roll can be found by what is in it.

A description is (clip_id, src_start, src_end, text) in source seconds, which is why cutting the edit can never invalidate one. Omit clip_id to describe every video clip that has not been described yet; name one to do just that clip. Audio-only clips are refused — their words are what transcribe indexes.

This is a job, not a request. Cost is about three seconds per window regardless of how much footage the window spans, so a project's footage is minutes of GPU time. Run it with plan=True first: that resolves the whole work list and the estimate, and reports whether this machine can run the model at all, without loading anything.

Already-described clips are skipped unless force. Do not widen window to save time without a reason — a single pass over a whole clip describes six frames as six people, fluently and with nothing saying it is wrong.

Read errors and truncated in the result. A truncated description stops mid-fact and reads exactly like a complete one, and a window is never evidence of a continuous shot: the model narrates across a cut inside one as though it were a single take.

The descriptions are written into the project, and force replaces the ones a clip already has; without it an already-described clip is skipped, so a repeat costs nothing and changes nothing.

describe_lsA

Read the footage descriptions, to find b-roll by what is in it.

This is the search. There is no ranking and no similarity score to ask for — you read the descriptions and pick, which is why the prompt behind them asks for concrete nouns. Each entry is (clip_id, src_start, src_end, text) in source seconds, so what you pick stays valid however the edit is cut.

contains filters: whitespace-separated terms, case-insensitive, and every term must appear — "kitchen knife" matches "a knife on the kitchen counter". Reach for it before reading everything on a large project; words says how much text came back.

Two things not to over-read. A window is evidence of what is visible in a span, never of a continuous shot — the model narrates across a cut inside one as though it were a single take. And an entry with truncated true stopped mid-fact and reads exactly like a complete description.

A clip listed under clips with windows: 0 has not been described yet; describe is what indexes it.

card_templatesA

The card templates proofcut ships, and the slots each one takes.

Read this before card_new: each slot says what it is for, whether it is required, and what it defaults to. The palette and font stacks are slots too, so a card can be restyled without authoring an SVG by hand.

Call it with no name to choose one, then with name to read only that template's slots — the whole table is long.

fontsA

Will the caption font actually draw on this machine?

Reports two answers side by side and does not merge them: fontconfig says whether the family is present, render burns the family and an impossible family and compares the pixels. Identical pixels mean the name is substituting whatever fontconfig claims — the only way to settle which face drew is to measure a render.

path is optional: with a project, this checks the font that project's caption style would burn; without one, proofcut's default. install copies the vendored face where this OS's font system looks (fontconfig, CoreText or DirectWrite) and is off by default, because it writes into the home directory.

card_newA

Make a card from a template: fill its slots, write the SVG, render it.

name is the <name> in card:<name> — the key a cue points at. Both the SVG source and the PNG are written under the project's assets/cards/, so the card can be re-edited later and re-rendered with card_render rather than redrawn.

A slot value is text. A newline inside one is a line break wherever the template accepts multiple lines; nothing wraps automatically, because a guessed wrap overflows the frame without saying so. Ratings are numbers out of five, to the nearest half.

Leave width/height unset unless you mean something other than this film. They default to the project's own canvas, which is what stops a card from pillarboxing inside the frame it was made for; naming a size that is not the project's is how a card loses a quarter of its width to black bar. Given at all, both must be.

Refused if a card of this name exists, unless overwrite — a cue may already point at it. Read font_warnings in the result: a template naming a face this machine lacks still renders, in a substitute, with nothing else to say so.

card_renderA

Rasterise assets/cards/<name>.svg into the PNG card:<name> shows.

Author the SVG under the project's assets/cards/, then render it here; both files are kept, so a card can be re-edited rather than redrawn. The PNG is what a card: cue resolves to, so a card is not usable until this has run.

width/height are given together or not at all and set the render size — the document is drawn at that scale rather than rasterised and resampled — and they fit rather than distort, so a size at a different aspect from the document's comes back smaller on one axis. Omitted, the document renders at its own declared size.

Every call reports the fonts the document names and what fontconfig will actually draw. Read font_warnings: a card naming a face this machine lacks renders pixel-identically to one naming a face it has, so nothing downstream can catch the substitution.

card_reauthorA

Draw recorded cards again at the shape this project renders at now.

Reach for this after canvas — a card is the only thing in a project whose shape a canvas change cannot fix on its own, because the aspect is baked into the SVG it was drawn from. Re-rendering the old SVG at the new size would pillarbox the card inside the frame; this fills the template again at the new canvas, from what card_new recorded.

With no name it sweeps every recorded card the canvas has left behind, plus any whose files have gone missing. Named, it redraws that one whatever its canvas.

Read unrecorded in the result. Those are cards with files on disk and no record of what made them — nothing can re-author one, and the way to fix it is card_new with overwrite, which records it on the way past. plan reports what would change and writes nothing.

card_safe_zonesA

Measure a rendered card's ink in and around a platform's reserved band.

Report only — nothing here blocks a render, and there is no default floor: SCENE_THRESHOLD's own history is that a threshold gets pinned by looking at real output, not picked cold, and this check has had exactly one look so far. platform is one of proofcut's own zones (tiktok-organic, tiktok-ads, reels, shorts, worst-case) or one an applied pack's active variant declares — pack_show lists both.

Reads card from its already-rendered PNG, never from the manifest's recorded slots alone, so the ink it measures is the ink actually on disk. Refuses a card with no PNG yet (card_new/card_render it first) or a platform neither source declares.

pack_applyA

Load a channel preset pack, resolve and snapshot every variant, activate one.

pack_path is an external file — never confined to the project, the same way import_media's source is not — because a pack typically lives in a separate branding repo. Every declared variant is resolved and hashed, not only the one variant activates, so pack_activate can switch between them later with no file re-read; nothing after this call ever depends on pack_path staying reachable.

For every font role, fonts.probe asks whether the declared family actually draws on this machine — a family that does not refuses the whole call unless allow_fallback (then its declared CSS fallback is used and recorded, never silent); one that draws but is vendored nowhere proofcut knows about is recorded font_provenance: "unvendored" rather than refused, since the render here is genuinely correct today. install_fonts vendors the pack's own fonts/ directory if it ships one — off by default, since it writes into $HOME.

Writes nothing to caption styling or to any card already on disk; a card picks up the new style only when card_new/card_reauthor next draws it, and captions only via pack_apply_captions. plan resolves and probes without writing.

pack_activateA

Switch the active pack variant to one already snapshotted by pack_apply.

No file re-read — refuses an unknown variant by name, naming the ones that are actually available, rather than trying to load it here.

pack_apply_captionsA

Apply the active pack variant's caption preset, through caption_style.

Concrete resolved fields, never a live pointer: this reads the preset's already-resolved dict off the snapshot and hands it to the ordinary caption_style call, so a later pack swap can never silently overwrite a project's caption look out from under it. Separate from pack_apply on purpose — applying a pack never restyles captions on its own, only this does.

pack_showA

What a pack declares — from its file, a project's snapshot, or both.

pack_path alone reads and resolves the file fresh, needing no project (card_templates's own shape). path alone reports what a project actually has applied, from its stored snapshot — never the file again. Both together compares "what the file says now" against "what the project is still running."

pack_statusA

Active pack variant, and which cards/captions have drifted from it.

A card is stale when its own recorded pack_hash no longer matches the active variant's current hash — not wrong, since card_new only pre-merges a pack's style and a per-call slot still wins, but worth a card_reauthor to catch up. caption_preset_stale is the same question for whatever pack_apply_captions last wrote.

cue_addA

Add a picture cue: from word_index of clip_id onward, show asset.

Source-addressed like a word range — asset is an opaque key or path, not checked against disk here; build_shots resolves it, the same way assemble_scream.py's CUES table did by hand. Refused if a cue already sits at that exact word; cue_rm it first to replace it. Echoes the resolved word plus three either side, the same convention every word-indexed tool follows.

Addressed by word_index or phrase (exactly one) — a phrase binds to its first word ("from this word onward"). after/occurrence disambiguate a phrase matching more than once; a resolved phrase is stored alongside the word index, additive metadata cue_reresolve can re-derive after a re-record.

src_start pins where inside asset the shot reads from: seconds in that asset's own source time, which is exactly the number describe_ls reports for a window. This is how a moment you found with describe gets placed — without it the shot reads from wherever the per-asset cursor had got to, which is right for re-using a clip and wrong for showing the thing you searched for.

It is an in-point and never a range: the out-point stays derived from the next cue through the edit, so a later cut still renumbers the shot correctly. The cost is a refusal instead of a rewind — if the shot's length runs past the end of the asset from that in-point, build_shots and the picture lane report it rather than quietly showing the asset's opening seconds instead. Shorten the shot with another cue, or pin earlier. A card takes no src_start; a held frame has no playhead.

cue_rmA

Remove one picture cue, addressed the way cue_add placed it.

Give word_index, or phrase to resolve against clip_id's transcript (its first word, cue_add's own binding). Refuses, listing every cue, when none sits at that word — cue_ls shows the table first. Shots re-project from the cues that remain; no other cue moves. Replacing a cue's asset is cue_rm then cue_add, since cue_add refuses an occupied word. undo puts it back.

cue_lsA

List the picture cue table, each entry echoed with its resolved word.

Read-only. Omit clip_id to see every clip's cues. Ordered by (clip_id, word_index), not by resolved timeline position — that needs the edit's surviving ranges, which is build_shots's job.

cue_reresolveA

Re-resolve every phrase-addressed cue, unspoken mark and music-bed boundary against the current transcript, and report what moved.

A re-record replaces a clip's transcript wholesale, and every stored word_index on that clip potentially now addresses the wrong word — already true today of a plain word-index entry, and this does not close that gap for one. What it closes it for is an entry that also carries the phrase it was placed with: re-resolving says where that same wording landed now, without hand re-indexing a whole cue table.

apply=False (default): report only, nothing is written — the same posture as reframe_detect/unspoken_detect. apply=True rewrites word_index in place for every entry whose phrase still resolves to exactly one match; anything ambiguous or unresolved is reported and left untouched, never guessed. An entry with no stored phrase is reported as "action": "unchanged (no phrase to re-resolve)", not silently skipped.

assetsA

Every asset a cue can point at — clip or card — for an assets pane.

The cue vocabulary is clip_id or card:name, so this lists both: each clip with its probe metadata, transcript/description presence, role and media.playability verdict; each card with what it was made from, whether its files exist, and whether it has a re-author record. Every entry carries cues, how many cues reference it — "is this used" is the question an assets pane exists to answer. Read-only.

unspoken_addA

Mark a word the transcript holds and the recording never said.

Whisper transcribes straight across a retake splice and emits words from both takes interleaved, so words appear in the index that nobody said. They are in the transcript and nowhere else — not the audio, not the render — so captions draw them and verify expects them.

This writes a mark beside the transcript and never touches the transcript itself: word indices must not renumber, or every cue pointing at one would move. Captions, caption_view and verify all stop expecting the word; no audio, timing or shot changes, because the seconds around it are the take that was kept. Echoes the word it resolved to, plus three either side.

Addressed by word_index or phrase — but unlike cue_add, a phrase resolving to more than one word is refused rather than bound to an edge: unspoken addresses exactly one word, and picking a side of a wider match would silently mark the wrong one half the time. Narrow the phrase, or pass occurrence= if it is disambiguation rather than width.

Prefer unspoken_detect to find them: it is evidence rather than reading, and reading for sense provably misses the grammatical ones.

unspoken_rmA

Unmark a word unspoken_add marked, putting it back into captions and verify.

Address it by word_index, or by a phrase resolving to exactly one word. Refuses a word that is not marked. The transcript file is never edited either way — a mark is a manifest entry — so no cue or caption renumbers. unspoken_ls lists the marks; undo restores one.

unspoken_lsA

Every word marked never-spoken, with what the transcript says now.

Read-only. stale is a mark whose recorded text and current text disagree — the transcript was re-attached under it. A stale mark is never applied, so re-transcribing surfaces as a list to re-check rather than as words disappearing from a caption file.

unspoken_detectA

Propose the words a render's own transcription says were never spoken.

Candidates come from two mechanisms and one witness decides both. A seam is where whisper read across a splice and invented a word; a fragment is where a cut left a sliver of a real one, which draws as a whole word on screen and is inaudible. The witness is the render: the candidate's word is counted in the timeline over a short window and in the render's own transcription over the same seconds, and it is proposed only where the timeline has more of them than the render heard. Counted rather than looked up because the inventions are function words — asking whether the render says "the" near here answers yes off the real one beside it.

apply=False by default, like reframe_detect: this changes what a caption says, and a wrong mark deletes a real word from every check proofcut has. Read the echoes first.

transcript_path takes an existing transcription of the render, which is what verify leaves in cache/verify/. Pass it explicitly — it is never found automatically, because a re-render under the same filename would otherwise be judged against the previous render's audio.

build_shotsA

Project the cue table into contiguous shots over the current edit.

Maps each cue's word through the edit's surviving ranges to a timeline frame, resolves its asset to a checked path (card:name under assets/cards/, else a registered video clip_id), and runs each shot to the next cue — the last to the edit's own frame total. Refuses if a cue's word was cut from the edit; fix it with cue_rm/cue_add first.

fps picks the frame grid; it defaults to the project's timebase, which for an audio-only project is milliseconds rather than frames. Pass the rate export will use to see the frames the export actually cuts at.

seed_timelineA

Lay a clip down as the timeline, silence-cut by auto-editor by default.

edit_expr passes auto-editor's edit language straight through, e.g. "(or audio:0.03 motion:0.06)".

Writes project.otio and replaces any timeline already there — every cut made since the last seed included. It seeds a project rather than re-cutting one, and a re-seed with the same arguments lands the same timeline. The old one is snapshotted first, so undo puts it back.

cut_by_transcriptA

Cut or keep inclusive word ranges, e.g. cut=[[30, 45], [120, 131]].

Pass exactly one of cut or keep. pad widens each range on both sides in seconds, to land the cut in the silence between words. The timeline is snapshotted first, so this is undoable.

Every range echoes back the words it resolved to, plus the few words either side of it — an index one past the intended phrase reads fine on its own and is only visibly wrong next to its neighbours. pad_reach names any neighbour the padding eats, since padding is in seconds and the echoed text is not.

through_pause=True (cut only) extends each range's trailing edge through the pause after its last word, whenever that gap is wide enough to have drawn a [N.Ns] marker in the transcript pane — so cutting a phrase also removes the dead air after it instead of leaving it playing. A no-op when the trailing gap is too short to have drawn a marker.

plan=True returns that whole payload — including what the timeline would become — without writing anything. Prefer it over cutting and undoing.

Refused if a range's first or last word claims a suspect duration (see attach_transcript/transcribe's suspect_durations) — that word's end/start is what the cut boundary resolves to, and it is usually hiding a retake rather than ending where it claims. Check the word, then retry with confirm_suspect=True if the boundary is actually fine. Under plan=True these are reported as suspect_boundaries instead of refused.

cut_by_timeA

Cut spans of RENDER/TIMELINE time — what a human reports watching an export.

Each span is [start, end) in the seconds the current export plays at (what timeline_status/verify describe), not source time and not word indices. proofcut converts each span to the source interval(s) it plays — the inverse of the mapping captions and playback use — and cuts those through the same Edit.remove path cut_by_transcript uses. The render timestamp is never stored: the conversion happens once, here, at call time.

All spans resolve against the CURRENT timeline before any is applied, so a list of notes from one watch stays valid together even though a real cut would shift every later timestamp. Overlapping spans are refused rather than silently double-applied.

Every piece echoes the source interval it produced (more than one when the span crosses an earlier cut or a clip boundary) and the words it overlaps there, plus three neighbours either side — the human check that the timestamp actually hit the intended flub. pad widens only the OUTER edges of each requested span. plan=True resolves and reports without writing, identically to cut_by_transcript.

Refused the same way cut_by_transcript is if a span overlaps a word with a suspect duration; confirm_suspect=True or plan=True behave the same.

A second call is not the same call. These are render timestamps, and this cut moves everything after it, so the same numbers name different material next time — take them off a fresh watch rather than reusing a list across two calls. That is also why one call takes every span at once.

restoreA

Un-cut whichever part of these inclusive word ranges is not currently in the timeline.

Same range shape as cut_by_transcript's cut=/keep=. Each range resolves to source time exactly like a cut does; only the part Edit.gaps says is actually absent comes back — material still present in the request is left alone, a request spanning two separate cuts restores both as separate pieces, a request only touching part of one cut restores only that part. Restoring only ever brings back material the source recording already has (bounded by the clip's own registered duration), so the timeline stays a subset of the source throughout — this is not vo_extend (PLAN.md parks that separately), which would add material the source never had.

pad matches cut_by_transcript's own pad: pass the same value used on the original cut to bring back its padding sliver, not just the words.

Unlike a cut, there is no suspect-duration refusal — a boundary that looks like it swallowed a retake is exactly the kind of thing restore exists to bring back, not a mistake to guard against.

Refused if clip_id has no surviving segment anywhere in the edit (nothing left of it to splice the range next to — undo or re-seed instead), or if its segments are not contiguous in the edit (an interleaved multi-source timeline, which restore does not support yet).

plan=True resolves and reports without writing, identically to cut_by_transcript.

locateA

Where does a SOURCE word or SOURCE time play in the current render?

cut_by_time's read-only mirror, and the tool to reach for before quoting any timestamp to a human: word indices and transcript times address the original recording, so they are NOT render times and every accumulated cut moves them further apart.

Two clocks, and this reports the Edit's. timeline_start/ timeline_end are 0 = the Edit's own first frame, unchanged whether or not a head (a cold open) is configured. head_seconds rides along (0.0 with none) so a caller that needs the actual render time — this tool's own stated purpose — can add it: render time = Edit time + head_seconds.

Address it one way per call — first/last are inclusive word indices (last defaults to first), source_start/source_end are seconds into the recording (omit source_end to locate an instant), or phrase — a phrase naturally is a range, so it resolves straight to first/last with no edge to pick (after/occurrence disambiguate a phrase matching more than once).

Read present first. False means the material is not in the render, and beyond_source distinguishes "you cut it" from "the recording never went that far". A partially-cut range is normal: placements lists each surviving piece in playback order with the source coordinates saying which part of the phrase it is, covered how much survives, and contiguous whether the survivors still play back-to-back. Word mode (and phrase mode, which resolves into it) echoes the resolved words plus three either side; time mode echoes the words the interval overlaps, or its nearest neighbours if it landed in silence. Read-only: nothing is written.

timeline_statusA

Report the current timeline: duration, segment count, undo depth.

head/tail echo the cold open / finishing pass set with the head/ tail tools, or null for either with none. expected_frames/ expected_duration are what export would lay down — timeline_duration alone stays the Edit's own length even with a head or a tail configured, since the Edit never grows to describe either bookend.

This is the tool to call first, to see what state a project is in — a fresh or un-seeded project answers seeded: false with the clip list rather than refusing (TRIAL.md § timeline_status is the first call an agent makes and it refuses on a fresh project).

timeline_viewA

The whole edit at once: segments, cut seams, and every word's fate.

timeline_status counts things; this says what they are. Each segment carries both coordinate systems (source in, timeline out), each seam is named by the surviving words either side of it rather than by the second it currently sits at, and each word reports whether it survived, how much of it did, and where it now plays.

Survival is an overlap test, so a word a cut split reports present with partial set — that is normal on whisper timings, not a defect. Words with a suspect duration carry the same flag attach_transcript reported.

This is locate asked once for the whole clip instead of once per range, and it is what the proofcut web view draws. Read-only.

shots is the picture lane the cue table projects — null when there are no cues, and null with a shots_error message when the plan refuses (a cue that was cut, or a shot longer than the asset it points at). The refusal is reported here rather than raised, because this is the view a person uses to find the cue to fix. shots_rate is the frame grid it was quantised on, which is export's rate and not timebase.

segments/shots/seams stay Edit-relative even with a head configured — see head's own docstring for the two-clock rule. head_seconds is the offset a render-time reader needs (0.0 with none); head is the stored config plus its resolved frame count.

words is a window of limit from first (words_total, words_next); the lanes are always whole. get_transcript with search= finds a word faster than paging here.

propertiesA

Project/clip/cue detail for a properties inspector, composed only.

No arguments: status, canvas and caption_style's own reports. clip_id: adds that clip's assets entry, its reframe window table, and its whole cue_ls. Both clip_id and word_index: adds cue (the matching entry from that cue_ls, or null if the word carries none) and, only when cue is null, context — the word plus three either side, the same echo every word-indexed tool gives (a cue's own entry already carries this, so it is not duplicated). word_index needs clip_id.

finish_reportA

Duration/canvas/caption/picture/marks/seams report for Finish mode, composed only — the truth strip's own numbers.

duration: edit seconds, tail seconds, and their sum. canvas: the stored or footage-fallback canvas, plus each export preset's own ok/refusal-message. captions: whether a style is configured, its resolved font, and whether the last render actually burned it in ("yes"/"no"/"unknown" — unknown when no render log exists). picture: cue count, pinned count, and the picture plan's own refusal message when it has one. marks: unspoken marks applied vs. still stale. seams: the transcript's own overlap count. unused_clips: registered clips on no lane, cued nowhere, held nowhere, not the music bed — a clip imported and forgotten (TRIAL.md § Registered-and-not-on-the-timeline has no report of its own), clearable with clip_rm or by cueing it. flags: the rolled-up warnings behind all of the above, each one naming the mode that fixes it.

framing adds reframe_coverage's stale-framing numbers and their two flags, and is off by default because it decodes placed footage for a scene-cut scan — 5.7s wall and 46s of CPU on the film, uncached, every call. Off, framing is None, which means "not measured" rather than "nothing stale".

holds adds hold_check's own per-hold seam/transcription report against the last render — off by default for the same reason framing is: it decodes and transcribes render spans. None when not asked for, and also None when asked for but nothing has rendered here yet.

continuity adds continuity_check's finding count by kind (rewind, replay, short_shot, stub) and how many are currently accepted — also off by default, its stubs=True half paying the identical scene-cut decode framing does. None when not asked for.

undoA

Roll the project back one mutation — the timeline, the manifest, or both.

Mutating tools snapshot first (migrate_project keeps its own backup instead), so this undoes cuts, cues, framing, the music bed, caption style and the rest alike; call it again to go back further. The reply says what came back: timeline_restored, manifest_restored, and timeline_removed when undoing a seed_timeline leaves no timeline at all. Undoing an import un-registers the clip but leaves its media on disk. There is no redo, so read undo_depth first when stepping back more than once.

changesA

What the last steps mutations did — what undo that many times would roll back.

Read-only. Compares the snapshot every mutation already leaves against the live project. timeline.removed and timeline.added are source spans per clip with the words they carry and where they played, so a cut reads as the words it took out rather than as every later segment moving; a pure reorder is reordered; spans under 50 ms (a frame's edge moving) are only counted, in removed_slivers/added_slivers. manifest.keys lists each changed manifest key: records added/removed, and changed field by field where a record has a name (a cue by its word, a framing window by its in-point, a clip by its id); a word-addressed record echoes its word in brackets with three either side. Lists past 40 entries are cut, with exact _counts beside them. unchanged: true means the snapshot and the project agree. Words come from the transcripts as they stand now.

exportA

Export the timeline as an NLE project, or render it.

The default writes an MLT project Kdenlive opens; export_format=null renders media. The writer is chosen from the project, never from an argument: a single-source timeline goes through auto-editor, and a multi-source one — a cue table, a second clip, a canvas, a bed, a tail — is written as MLT by proofcut and rendered by melt, because auto-editor renders a second source at 720x576 while exiting 0. The reply names the writer, and a melt render reports resolution and frame count measured off the finished file.

preset bundles quality for a render; tiktok-reels checks 9:16 and never sets the shape — use canvas first. loudness masters to a LUFS target and refuses, leaving the render as it was, if it misses by more than 1 LU.

Captions are not burned by this — add_captions is its own step. Then check the file against the timeline with check_frames and verify; a render that exists is not a render that is right.

add_captionsA

Write word-timed ASS captions for the current timeline to output.

Timings follow the timeline, not the original recording, so captions stay correct after cuts; words that were cut are omitted and counted as words_cut.

The look comes from the project — set it with caption_style, see it with caption_view. The arguments here override it for this one file and are not written back, so regenerating after a cut is styled the project's way again. Leave them unset unless you specifically want a one-off.

The sidecar .ass is the default exit — Kdenlive loads it and it stays restylable. Pass burn (a render of THIS timeline) to burn the captions in with ffmpeg instead; against any other video the timings will not line up.

caption_viewA

The captions this timeline would produce, and the style in force.

add_captions without writing a file: the same cues, in timeline seconds, already grouped by the project's own break rules — so this is how to check a restyle, or read back what a caption actually says at some moment, before committing a file to it.

Reports rather than refuses: a project with no transcript, or one whose every word has been cut, comes back with an empty cues and a cues_error saying which. Read-only.

cues is a window of limit from first; cues_total is how many the film has and cues_next, when present, where to continue. Use locate to find the cue at a moment rather than paging to it.

caption_styleA

Read or change the caption look this project keeps.

The style is project state and the captions are derived from it, so a restyle survives every later cut: regenerating re-reads this. Call it with no arguments to read the current look and learn the field names; any argument sets that field and leaves the others alone. reset drops every override first — reset plus preset starts clean from a preset.

preset is the base look ("clean", "karaoke" for per-word highlight, or "boxed"); everything else overrides one of its fields, and only the overrides are stored.

Colours take "#rrggbb", "#rrggbbaa", a name ("yellow", "white", "red", …) or an ASS "&H…" value. text is the word's colour and highlight what it turns as it is spoken, which only shows with karaoke on. position is named: "bottom", "top", "top-right", and so on. Both come back resolved, because ASS quotes colours backwards and alpha-inverted.

plan validates and resolves without writing. Use caption_view to see the result on the actual timeline.

canvasA

Read or change the shape this project renders at.

The canvas is project state and every frame size derives from it — the MLT profile and the captions' reference canvas both read it, so a project cannot quote caption sizes against one shape and render another. Call it with no size to read what is in force plus the footage-derived shape it would fall back to; reset drops the override and returns to that shape.

size is "WIDTHxHEIGHT", e.g. "1080x1920" for a vertical reel. Both edges must be even.

Setting one has a routing consequence, reported as routes_through: an overridden project renders through the MLT writer whatever its source count, because auto-editor cannot be handed a canvas it will honour. An override that changes the aspect crops to fill rather than pillarboxing, so cropped names every clip that loses footage to it — use reframe to see or change which part of each one is kept.

headA

Read or change the cold open this project plays before its first frame.

tail's mirror at the other end of the film — the same read/partial- update/reset/plan shape — but its asset rule runs the other way: asset must be a registered clip_id, never card:name. A cold open is real footage with real dialogue by definition; tail forbids that because verify would gain a permanent disagreement it can never resolve, and a head is taught to account for its own words instead (verify's head_words_trimmed) rather than being restricted to silence. Call it with no arguments to read what is in force.

Setting asset or seconds for the first time needs both together; either alone after that updates just that field, tail's partial-update shape. src_start defaults to 0.0 on a first set. fade_in/fade_out default to 0.0 and — unlike tail's fade — are drawn from day one, the whole reason this feature exists (a hard butt-join between room tone and digital silence is exactly the seam a missing fade produces). gain_db defaults to 0.0, a flat non-fading level shift distinct from the fades.

Needs an existing picture cue lane covering the whole film, tail's own requirement — add cues first (cue_add) if the project does not have one. reset drops the head entirely. plan resolves and validates without writing.

tailA

Read or change the finishing pass this project plays after its last frame.

An end card or a bumper, applied by export itself rather than glued on afterward with ffmpeg — the fix for a defect that has already shipped: a finishing pass applied downstream of export is dropped by every derivation at exit 0, silently, because nothing in the project ever knew it existed (HISTORY.md § The bumper the teaser never had, § The end card). Call it with no arguments to read what is in force.

asset must be card:name, never a clip_id — verify diffs a render's own transcription against the timeline's words, and silence adds none of its own, which is exactly what a card behind it guarantees and a media clip would not. seconds is the tail's whole length, card included, not a hold with fade added on top of it (the known trap: xfade finishes exactly at the length it is given). fade is recorded and echoed but not yet drawn — this build cuts to the card hard, at seconds.

Setting asset or seconds for the first time needs both together; either alone after that updates just that field, the same partial-update shape caption_style has. reset drops the tail entirely.

Needs an existing picture cue lane covering the whole film — add cues first (cue_add) if the project does not have one; export names why otherwise. plan resolves and validates without writing.

musicA

Read or change the A2 music bed this project mixes under its edit.

Call with no arguments to read what is in force. The bed stores word indices and an asset, never a length: it starts where word_index_start of clip_id (the VO transcript) lands on the timeline and runs to where word_index_end ends — or to the end of the edit — so a cut before either boundary moves both. Duration is derived at build time.

The first set needs asset, clip_id and a start (word_index_start or phrase_start) together; after that each field updates on its own. A field set by phrase stores the phrase beside the index it resolved to, so cue_reresolve can re-derive it; set by plain index, the stored phrase is cleared. Both boundaries are echoed with their resolved words and neighbours — check them.

Beyond one asset from its head: passages (more pieces, each from its own word), rotate (assets in turn), crossfade, src_in; under levels the bed below the voice, duck dips it while the voice speaks, keyed off the edit's own audio at export. export's music field says what the render carried. clear_* and reset undo each; plan validates without writing.

vo_extendA

Open a gap in clip_id's track for material the recording never had.

The one tool allowed to grow the edit rather than cut it: a real hold in the VO, e.g. to let a line the footage carries play under it. Not the end card (tail), and not restore, which only brings back cut source.

Addressed by word_index or phrase — the last word before the gap, which must be on the timeline — for seconds. The stretch is a real silent WAV, registered like any clip; a second call at the same seconds reuses it.

Read covered_by. build_shots runs each shot to the next cue, so whatever picture was playing freezes across the hold by default, with shots_error, verify and check_frames all staying clean. It names every shot the gap now overlaps ([] with no cue table at all).

Two consequences are permanent once a hold lands: restore refuses across the seam, and export always goes through the MLT writer. plan=True reports covered_by without writing; its hold_clip_id is a placeholder.

vo_synthA

Say text in a cloned voice — render several seeds, rank them, read the winner back.

Zero-shot Qwen3-TTS from a ≈19s reference clip (voice); there is no built-in voice. Seeds seed .. seed+candidates-1 render in one process, each with sim (speaker-embedding likeness to the reference — a real take ≈0.99, a 3-semitone shift ≈0.96) and spread (voiced pitch movement). chosen is the best sim less a flatness penalty, since likeness alone keeps the flattest read. A render that hit max_seconds is capped and never wins while an uncapped one exists.

The winner is read back through whisper and heard/wer reported — a clone that sounds right and says the wrong words is the failure nothing else sees. A report, never a gate.

Renders are cached under cache/synth/, so a repeat spends no GPU. The splice is not cached: with clip_id + word_index the winner is registered and spliced in after that word through vo_extend's mechanism (melt routing, restore refusing across the seam, a covered_by report), and calling again splices a second time — check the timeline or undo rather than re-calling. plan=True reports the ranking and splice preview from cached renders only, and says rendered: False rather than spending the GPU.

hold_addA

Splice a hold into clip_id after gap_word_index: a real gap opens in the VO (vo_extend's own mechanism, reused) and a picture cue pins asset's own in-point, snapped to whole words with margin and refused, never clamped, when it cannot fit.

Addressed by (clip_id, gap_word_index), unique — a second hold_add at the same address is refused. gap_word_index/cue_word_index/ word_index_first+word_index_last each also accept a phrase alternative: gap_phrase binds its last word (the gap opens right after it), cue_phrase binds its first, and asset_phrase resolves against asset's own transcript and binds its first and last words to word_index_first/word_index_last together.

Everything else is resolved live: elapsed (how long the VO plays between the cue and the gap), src_start (deterministically — phrase_start - elapsed - head_margin), and hold_length (the phrase's own span plus both margins). Refused, with the measured numbers, when there is no room or the asset runs out.

Mix-only fields (head_margin/tail_margin/under/fade_in/ fade_out) are re-settable on an already-spliced hold by calling again with the same address and no change to word_index_first/ word_index_last — those two are one-way once spliced (hold_rm then hold_add again, or proofcut undo, are the only ways to resize one).

plan=True resolves and reports without writing anything.

hold_rmA

Drop a hold's record and its owned cue — the spliced silence stays.

vo_extend's own irreversibility, inherited: there is no clean "un-splice", only proofcut undo. After this the gap reverts to being an ordinary manufactured silence, a coherent pre-existing state rather than a broken one.

hold_underA

Play a film clip's own audio under a span of the VO, under LU below it (default 13) — no gap, unlike hold_add. The span is VO words (word_index_start/word_index_end, or phrase_start/phrase_end), and the audio reads from wherever the shot showing asset has got to at the span's first word, so asset must be on screen there — cue it first. A second call at the same (clip_id, word_index_start) replaces the entry; the music bed goes out across it. plan resolves without writing. Both boundary words are echoed with neighbours — check them.

hold_under_rmA

Drop the film audio under the VO addressed by (clip_id, word_index_start).

The inverse of hold_under: that span plays the VO alone again, and the music bed — which a hold gates out — comes back across it. Refused when no entry sits at that address, so a second call says so rather than doing nothing quietly. The audio was a manifest entry, not a splice, so no word moves and undo restores it.

hold_lsA

Every stored hold plus its live-resolved plan.

A hold that cannot currently resolve is reported inline (hold_error), never raised. Each item also carries cue_drift — a check between the hold's own owned cue and what it would compute fresh right now, since nothing stops a plain cue_rm/cue_add on that exact word from an unrelated caller.

hold_checkA

Transcribe each hold's own span off render and check its seams.

For each stored hold: the required phrase, transcribed off the render at the hold's live-resolved span, plus the level right at each edge against the quiet floor just after it — "still loud" (a word cut off) or a "noise-floor cliff" (a hard drop with nowhere graceful to land). Report, never refuse — a post-hoc listening check on a render that already exists, verify's and film_check's own stance.

finish_checkA

Check a delivered file against this project's timeline — verify/check_frames/check_black/film_check for a file an external mix pass produced, not one of proofcut's own renders.

final carries a cold open and/or holds concatenated on outside proofcut, so every position this reports is in final's own absolute seconds. prepend_seconds defaults to this project's stored head length; holds defaults to its stored holds, resolved live and offset the same way — pass either explicitly (an empty holds list included) to check a file against a different set than what is currently stored.

Eight checks, none individually fatal to the others: stream/chapter/ duration agreement against the timeline's own arithmetic; loudness (report only); blackdetect, with a run explained only when it falls inside the prepend or a hold's own span; each hold's own span transcribed and its seam levels measured; a windowed transcription of final diffed against the timeline's expected words, with every heard word inside the prepend or a hold filtered out first; every dropped run re-cut and re-transcribed on its own to catch a windowed-pass false miss at a window stitch (boundary_misses, recovered — a run that still cannot be found stays in missing, a real fault); and a self-repeat scan over the same filtered transcript. faults/ok aggregate all of it, and every run is logged (finishlog) so proofcut review serve can show a WARN badge keyed to the file's own sha256.

reelA

Derive a new project at dest holding [start, end) of this timeline.

start/end are render seconds naming the span to keep — the opposite direction from every other tool; the head and tail are cut through cut_by_time. Reach for this before setting a vertical canvas on a film: the canvas is project state, so pass the reel's shape here and it lands on the copy only.

Media is linked, not copied. Descriptions and reframes carry over. Cues carry over only where the reel keeps their word — read cues_dropped head-first, since one pruned just outside the kept span opens the reel on no picture. Survivors are pinned to the film's in-points (cues_pinned, or pins_error). Cards are re-authored at the new canvas (cards_unrecorded names any that cannot be); over_platform_cap says if it still runs long for a vertical feed.

Nothing after the film is inherited: tail_dropped and music_dropped name what the film had. An edge on a suspect-duration word — likely a hidden retake — refuses unless confirm_suspect; read suspect_edges. plan=True creates nothing.

reframeA

Read or set which part of each clip survives into the frame.

What makes a swapped canvas fill the frame instead of pillarboxing it. The default is a centre crop, which is wrong whenever the subject is not centred. Call with no clip_id to read the crops in force for every clip; clips[].windows is each clip's whole series.

A rect is a floor rather than a frame: grown to the canvas's shape, never shrunk into it, stored as asked and refit whenever the canvas moves; the reply gives both asked and the crop it became. src_start makes it a per-shot window, addressed on the source's own clock, so every placement of the clip picks it up. pane makes that window a stacked split for a shot one crop cannot hold; interp slides into it rather than stepping; fill="blur" draws it whole over a blurred copy of itself instead of cropping, for a shot every crop loses something from.

reset drops overrides (one clip, one window, or all); plan resolves without writing. Nothing here analyses the picture — reframe_detect proposes crops and writes through this tool. Judge a window on reframe_sheet, never on a watch: a wrong one reads as framing in motion.

reframe_detectA

Propose a framing window per camera shot, from where the faces are.

Every placement is split at its camera cuts, each window sampled at a few moments and centred on the faces found. Against fifteen hand-framed, approved windows it beats the centre crop on every measure (0.755 mean overlap against 0.568).

It proposes; it does not frame. apply is off by default: the pass is still about a quarter of a window's width out on average, and a wrong window reads as framing in motion. Look at reframe_sheet before applying. Applying writes through reframe and never over an existing override.

A window with no face is refused, never guessed at — expect about one in seven — and nothing is written for it, so read falls_back_to: at a clip's head that is the centre crop, anywhere else the previous shot's framing. Nothing here chooses the subject either.

A window one crop cannot hold comes back as a stacked split (rect and pane). Read subjects (per frame), not faces, which sums detections across samples and calls one face three. Needs PROOFCUT_FACE.

reframe_coverageA

Which placed seconds are framed by a window chosen for an earlier shot.

The question reframe_detect cannot answer: that one is about a proposal, this is about the project on disk. A refused proposal writes nothing, so a stretch can sit under a rect chosen for a shot that ended long before — 13.6s of one clip across four camera setups on the film, with the manifest, status and reframe_sheet all clean.

Every placement is walked against its source's scene cuts. A cut with no window boundary within a frame of it opens a stale stretch. Read stale_seconds — an override held across a cut, which looks deliberate — not default_seconds (the centre crop, only the default doing what it always did). Each stretch carries timeline_start; the fix is reframe_sheet to look, then reframe_detect on the clip.

steps is the mirror, and the one a viewer notices: a window boundary with no cut, where the frame slides sideways mid-take and reads as an edit that is not there. Each carries shift and nearest_cut.

Needs no face detector, reads and never writes — but it decodes placed footage, so it is seconds, not free.

continuity_checkA

Rewinds, replays, short shots, and film-internal-cut stubs — reports, never decides.

Rewind: a shot lands behind where its own asset last left off, with under gap seconds of timeline since. Replay: an earlier shot's source range is re-shown, gap seconds or more later — reported, never refused, because a deliberate narrative rhyme and a mistake look identical from the cue table alone. short_shot: under min_shot seconds (stills excluded). stub: a shot ends or begins right where its own footage has a real internal cut — likely a fragment rather than the shot itself.

The stored cold open (head) is walked as a pseudo-shot before the first real one, so a body shot that rewinds into the head's own footage is caught the same way a body-to-body rewind is. Overrun is never a finding: mlt.plan_picture already refuses it structurally, so nothing reaches this walk having overrun its asset.

stubs=True costs a scene-cut decode per distinct asset placed — stubs=False skips it. scene_threshold defaults to the pinned 0.15 but is caller-settable: darker footage from a different film has needed 0.12.

Findings already acknowledged by continuity_accept are dropped unless the shot moved under the mark, in which case they are kept and marked accepted_stale: True rather than silently re-suppressed.

continuity_acceptA

Acknowledge one continuity finding once — a deliberate rhyme, never re-reported every run.

Addressed the way a cue is (clip_id, word_index), plus kind, since one shot can carry more than one finding. Stores a fingerprint of the finding's own numbers; a later run whose recomputed fingerprint disagrees means the shot moved under the mark, and the finding is reported again rather than trusted blindly. Refuses when no finding of kind currently sits at that cue — continuity_check first, then accept what it found.

continuity_rejectA

Unmark a continuity finding, putting it back into continuity_check.

The inverse of continuity_accept: the acknowledgement is dropped from the manifest, so every later run reports that finding again instead of passing over it. Addressed exactly as it was accepted (clip_id, word_index, kind), and refused when no accepted finding of that kind sits there — so a second call says so rather than quietly doing nothing. Nothing on the timeline moves either way; an acknowledgement is a manifest entry, and undo puts it back.

continuity_lsA

Every accepted continuity finding, with whether it is still live and whether it still matches what was accepted (stale).

A finding that has disappeared entirely — the shot was re-cued away, or the issue was fixed — reports still_found: False rather than stale, since there is nothing live left to disagree with the mark.

reframe_sheetA

Draw every placement's framing window on its own source frames.

A framing decision is unreviewable without this. The hand-framed teaser had 2 of its 15 windows wrong and neither was visible in motion — a badly-placed window reads as framing. Drawn on the whole source frame, what the window leaves out sits right beside it.

Every placement the render shows is walked window by window, the window in force drawn in red and labelled with its rect. A row is a window shown, not a placement: each placement is split at the boundaries it crosses, so a window covering a small slice of a long placement still gets a row. window on a row is the source address reframe --src-start takes; windows is how many the whole placement crosses. Stills come back under skipped — a card is re-authored, never cropped.

A tile is evidence about an instant, not an approval of the span. A static rect over a moving subject has a best moment and a sample can land on it; extremes draws where the subject is leftmost, median and rightmost instead, worst first, with worst_offset on the row to sort by.

A page of rows comes back as an image, six windows by default, at a width vision reads verbatim; row keeps its project-wide number on every page. per_page: null is the whole project as a PNG path, for a person.

shot_sheetA

Look at the picture track — one labelled tile per shot, as an image.

This is the tool to call to see what the film looks like — the picture track of the edit as it stands. Its neighbours answer different questions with the same kind of picture: footage_sheet browses one registered clip's own material, contact_sheet looks at a clip's first ten seconds, and reframe_sheet reviews framing windows a page at a time. All four hand the bytes back, because an agent confined to proofcut's tools (the agent panel's --tools ToolSearch) can open no path at all.

One tile per shot, at the exact source second that shot reads from, four across and about two dozen a page — the measured ceiling before vision downscales the sheet and takes the labels with it. Each tile is labelled asset t=<timeline second>s src=<source second>s, and page walks a longer film.

asset on a row is the footage; clip_id is the transcript the cue is addressed against, which on a voiceover project is the VO and not anything you can see. Read asset.

Drawn from the same projection export renders, so a plan that refuses comes back as shots_error with no sheet rather than a picture of a film that will not render.

What you see here is a hypothesis, not a check. Nothing downstream reads a verdict formed off this sheet — confirm one with an op that measures (check_frames, verify, black, reframe_coverage).

out is the one thing here that writes where you say: the montage lands at that path, replacing whatever file is there. Without it a page is written into the project's own sheet cache, which nothing reads back as authored state.

footage_sheetA

Look at a clip's own footage — one labelled tile per moment, as an image.

The tool to see what is in some footage, as opposed to shot_sheet, which shows an existing edit's picture track. It needs no edit, cues or transcript, so it is the first look at b-roll, recordings and gameplay — material describe can search by text but cannot show. The bytes come back in the reply.

mode picks the instants: auto (described windows if the clip has any, else the interval), interval, describe (each tile beside its window's sentence), or scenes (one per detected cut — opt-in, since a continuous take has none and a scan decodes the whole clip). page walks a long recording. A tile with nothing in it is marked [blank] on the picture, so a black square is never mistaken for a frame that failed to extract.

What you see is a hypothesis, not a check — and this sheet is read to choose footage. synopsis is where a person says what a clip is; a tile shows what the camera saw, which is a different fact.

thumbnailA

One filmstrip frame for clip_id, at the source time nearest at.

at snaps to a multiple of interval before anything is extracted, and the frame is cached under cache/thumbs/ keyed by the clip's media size and mtime — a repeated ask for a nearby instant is a cache hit. The result is a path, not the image bytes; proofcut web serves those over /api/thumb/<clip_id>?at=. It never enters the manifest, so nothing that renders can reach it (the same wall the preview proxy has).

contact_sheetA

Look at a clip's own head — the first look, as an image.

The sheet to call before cueing anything to a clip you have not seen. Two shots of the film were cued to a clip's own head and got 4.5s of "BASED ON THE NOVEL BY THOMAS HARRIS" over black, because nobody had looked at its first seconds. Ten seconds at 1.5s spacing by default, each tile labelled with the source second it is.

The frames come from thumbnail()'s cache — no new cache location, no new manifest key, no new web route — and the montage of them comes back here as bytes, since an agent confined to proofcut's tools (the agent panel's --tools ToolSearch) cannot open a path. import_media makes the frames for every clip it registers, so this is usually a cache hit; call it to see them, to look further than ten seconds, or to redraw after a re-import.

An audio-only clip returns frames: [] and no sheet, not a refusal — the same "nothing to look at is not a failure" as check_frames. A box without magick returns the frames and a sheet_error.

synopsisA

Read, set or clear what a clip is — the corpus b-roll gets chosen from.

No clip_id lists every clip's synopsis and which are missing one; clip_id alone reads one; text writes; clear removes.

A synopsis is a different fact from a describe window. A description says what is in front of the camera — rooms, clothing, lighting. A synopsis says what the footage is: the work, the scene, the people, and whatever else decides whether it belongs under a sentence. It is meant to carry what no camera can see, because that is where the signal turned out to be — measured on real footage, the vision index chose the same clip a human did 2 times in 25, and this catalogue read by something that knows the material chose it 13.

Write these yourself. Nothing generates them: a model looking at the pixels cannot, and guessing a title from a filename would produce confident wrong placements rather than an obviously empty catalogue.

broll_briefA

The whole b-roll question as data: what there is, and what it goes under.

Returns the footage catalogue with each clip's synopsis and duration, then every shot position on the timeline with the narration that plays over it, how long it is held, and what is currently there. card: true positions are shown for rhythm and are not choices.

This is the half proofcut can do. Choosing is the other half, and it belongs to you: read the brief, decide which clip goes under which sentence, and write the answers back with cue_add, where the picture plan checks each one. Ranking the catalogue by text similarity was measured and does not work — the sentence that earns a clip routinely shares no word with any description of it.

missing_synopsis is the thing to fix first. A clip with no synopsis is invisible to any reasoning about the catalogue, so it will simply never be chosen.

verifyA

Transcribe a finished render and diff it against what the timeline says.

Run this after rendering, before calling an edit done. It transcribes the render with whisper and compares that word sequence to the one the timeline should play, which is the only check that catches a retake still in the picture: whisper collapses an immediate repeat into a single utterance, so a doubled phrase can be invisible in the source transcript and still be in the render.

Read repeated first — an entry there is a phrase the render plays more times than the timeline expects, i.e. a surviving retake, with the heard word index to look at. dropped is the opposite: words the timeline expects that the render never says, usually a cut that reached too far.

A clean single-pass result is not proof. This check has a known blind spot: the render's transcript is itself one whisper pass, which collapses a repeat the same way the source transcript did — three retakes survived a correct run of it on a real video. Set windowed=True to transcribe in short overlapping windows instead, which is what found them. It costs one whisper run over 2x the audio and uses a deliberately smaller model, so run the default first and escalate to it before calling an edit finished.

loud_gaps comes back either way and trusts no transcript: it measures the render's own energy and reports holes in the heard word map that hold sound anyway. An entry is a place to listen, not a verdict — a music bed or an attenuated noise can produce one. Read speech_db/threshold_db beside it.

similarity around 0.97 is normal on a clean render — whisper spells its own output differently on a second pass ("whodunit" / "who done it", "4" / "four"). Treat it as triage; diff is the artifact. Transcription takes minutes on a long render, and the result is cached under cache/verify/ and reported as heard_transcript — pass it back as transcript_path to re-diff without re-transcribing.

check_framesA

Check an export's frame count against what the timeline says it should be.

The picture-side counterpart to verify, which covers only the audio. Run this on the exported NLE project before rendering — that is where it is worth the most, because the count settles whether the cut positions are right for the price of reading a document rather than encoding one.

target is an NLE project (.kdenlive/.mlt/.xml, put to melt -consumer xml) or a finished render (counted with ffprobe). Omit it to just report expected_frames, the total the timeline lays down.

Read agrees first, then delta — how many frames the target has that the timeline does not. A non-zero delta on an NLE project means the render will not be the length the edit is, and notes says so when the cause is one proofcut already knows about. agrees is null, not false, for an audio-only render: it has no frames, so nothing was checked.

fps must match the rate the export ran at or the two sides are counting on different grids; it defaults to the rate export would have picked.

film_checkA

Compare this project against the export it is supposed to be.

check_frames answers whether an export agrees with this project's own arithmetic; it cannot catch this project being the wrong film to begin with — a project can pass every check it has and still be seeded from a stale stage of an outside edit (HISTORY.md § The VO the project was holding: 73 segments/410.963s sat in a project whose shipped film was 63 segments/336.269s, with the render, verify, the cue table and the shot plan all agreeing with the wrong one). This checks the project's timeline_duration against a reference file's own ffprobe duration — cheap, no frame counting, no melt. Segment count has nothing on the reference side to compare against once a film is encoded, so segments is reported alone and the notes say why.

reference is remembered: passing it stores it on the project (additive, no schema bump), so a later call with no argument re-asks the same question against the same file. reset drops the stored reference; plan resolves without writing. With no reference given or stored, this reports the project's own numbers and says there is nothing to compare them against, rather than raising.

import_editA

Lay a cut made in Kdenlive down as this project's timeline.

The supported way to bring an outside edit in. seed_timeline lays a clip down and lets auto-editor find the cuts; this takes a .kdenlive (or .mlt) playlist somebody already trimmed by hand and reads its surviving ranges into the timeline. It replaces the whole timeline, and the previous one is snapshotted first, so it is undoable like any other mutation — which is the part the hand-rolled version of this never had (HISTORY.md § The VO the project was holding: 63 ranges were parsed out of a .kdenlive and written straight to Edit, bypassing cut and its history).

Every clip the document references has to be registered already — the resources are matched against registered clips by resolved path, and any that do not match are named rather than imported behind your back. Pass clip_id for a single-source document whose media sits at a path this project does not know.

Ranges that overrun a clip's registered duration are clamped and reported in overshot, never silently dropped: auto-editor's own exports overshoot the tail by one frame, so a clean overshot is worth reading rather than assuming. plan resolves and checks without writing.

Refused by name rather than half-read: a <blank> in the playlist (real runtime an Edit has nowhere to put), and two playlists carrying different cuts (a multi-track picture edit, which proofcut's one linked A/V track has no shape for).

check_blackA

Scan a render for black stretches, and say whether each is the known kdenlive-export tail frame (picture.KNOWN_TAIL_FRAME) or a genuine defect.

target is required — unlike check_frames, there is no cheap no-target mode; there is nothing to detect black in without a render. A run is only ever explained when it sits at the tail and the frame delta against the timeline matches the known defect exactly; a black run inside the declared picture is always reported as a real defect.

spot_framesA

Pull count evenly-spaced frames (plus any explicit times) from a render as PNGs with signalstats luma, ranked darkest-first.

When target's own probed duration still matches the current timeline within a frame (mapping_trusted), each frame also reports which clip/word it lands near via Edit.source_at — refused, not guessed, when the render looks stale.

Like shot_sheet/footage_sheet/contact_sheet, the reply also carries a montage of the sampled frames as an image — frames[].png is a path, and an agent confined to proofcut's tools (the agent panel's --tools ToolSearch) has no Read to open one (TRIAL.md § spot_frames hands back paths the agent cannot open).

speech_overlapA

Does a proposed placement of clip_id overlap the VO's speech?

The prerequisite check behind "can this clip speak here?" — answer it before designing any ducking. at/clip_in/clip_out describe where clip_id would sit on the timeline (defaults: unplaced at 0, its whole duration) — the clip need not be on the timeline yet, and usually isn't, since the current model is single-track. VO's own words map through the existing edit (Edit.timeline_span); clip_id's map by offsetting into the proposed window instead. Both sides are trimmed with energy.believable first — an inflated word duration can hide a real seam — then merged into speech runs with max_gap tolerance, since a 0.05s gap is not a usable seam.

Read overlaps first: any entry means placing clip_id there would step on VO speech, not empty air — this caught exactly that on Billy/Stu, where the clip's speech nearly fully covered a VO thesis line with no clean seam to duck into. clean_seams (>= min_seam wide) are the windows where clip_id could speak without touching the VO. Read-only — nothing is written, and there is no plan=.

clip_id need not have a transcript. Without one the clip side is its energy envelope — runs of sound, reported as sound rather than speech (a sting or a swell counts too) — and clip_evidence in the result says "energy" so the reading is not mistaken for a word-level one. Pass clip_evidence="transcript" to refuse instead, or "energy" to force the envelope on a clip that has a transcript. The VO always needs its transcript.

attenuate_noisesA

Pull down short loud non-speech events instead of cutting them out.

An event only qualifies automatically when it is both short (max_event_seconds) and sitting in a word-map gap narrow enough to prove the map is dense around it (max_gap_seconds) — a wide gap disqualifies even a very short event, which is the false-positive class this exists to prevent (speech sitting in a hole the transcript never wrote down). Qualifying events are pulled down db via one ffmpeg pass, never cut, and written as a new derived copy that media_path() picks up automatically everywhere downstream; the original is always what a re-run reads from, so repeated calls never compound gain.

Unlike cut_by_transcript/cut_by_time, nothing here ever raises on what the scan finds — this is an automatic multi-candidate scan, not a handful of explicit ranges, so withholding is done per event rather than refusing the whole call. suspect_neighbours (a bounding word itself has a suspect duration — withheld unless confirm_suspect=True or plan=True) and disqualified (too long, or too wide a gap — never written, no override) are always reported in full, not only under plan=True.

proxy_transcodeA

Make footage the preview cannot decode playable in the window.

The other half of what the viewer already reports: an unplayable clip names its reason (hev1, 10-bit, an unopenable container, an undecodable audio track) and shows black. This transcodes a downscaled h264/aac/mp4 stand-in into the project's cache so it plays. One ffmpeg pass; a long clip is minutes.

The result is a preview artefact and cannot reach a render: nothing records it in the manifest, so media_path() — what export, verify and check_frames all resolve through — has no way to see it. That containment is structural, not a convention to be careful about.

Skips the work when a current proxy already exists (keyed by the source's size and mtime), so calling it on every unplayable clip in a project is cheap after the first pass. Refuses a clip that already plays, and refuses a file with no decodable streams — that is a broken file, not a codec problem, and it is the one refusal a transcode cannot close. force rebuilds a current proxy but does not override either refusal.

review_addA

Register a rendered file, sheet or A/B member for proofcut review serve.

Never copies source — a render already lives in renders/, a sheet in reframe_sheet's own directory — this just points name at it, so a served round has something to stream and a verdict has something to attach to.

Registering a name that already exists replaces that entry, and the verdict recorded against it stays — so re-pointing a name at a different file leaves yesterday's answer attached to today's bytes. Register the new file under a new name unless replacing it is what you mean.

kind is one of render, sheet, ab, control. A control requires baseline, the name of an already-registered item, and the two files' sha256 must match — a mismatch refuses the call. This is the rule the round that went wrong exists to enforce (HISTORY.md § The bumper the teaser never had): a page once served three cuts, one mislabelled "control" when it was a different, later render. Nothing is labelled a control here unless it is byte-identical to what it claims.

review_verdictA

Record a verdict against a review item registered by review_add.

verdict is a free string, not an enum — past review rounds answered yes/no, "loop"/"hold", or a specific choice by name, and a fixed vocabulary would misfit whichever question the next round is actually asking.

Refuses a name review_add has not registered. Calling it again for the same item replaces that item's answer rather than appending one, so a round holds one verdict per item, with the time it was recorded.

review_listA

List every item registered for this project's review round, and its verdict.

Read-only. Returns items (each review_add registration: name, kind, project-relative path, sha256 and any control baseline) and verdicts (keyed by item name: the verdict, its note and when it was recorded). Items are registered with review_add, judged with review_verdict, and served to a phone by proofcut review serve.

Prompts

Interactive templates invoked by user choice

NameDescription
cutCut recordings into a finished video: retakes out, b-roll under the lines it belongs to, captions burned in, rendered and checked against the timeline.
filmMake a finished film ready to upload: the cut, b-roll, music under the narration, an end card, captions, a loudness master, and the checks.
reviewCheck a finished project before it is uploaded, changing nothing: whether the render is the timeline, every word is heard, and the picture, captions, music and level are what the project says.

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A4/5.0

Scored across 93 tools

Disambiguation4/5

Every tool's purpose is sharply delineated, and the descriptions actively name their sibling tools and state the difference (shot_sheet vs footage_sheet, reframe vs reframe_detect, transcribe vs attach_transcript). However, several sibling clusters — verify/finish_check/hold_check, the four reframe tools, the three sheet tools, and pack_apply/pack_activate — share names and mechanisms closely enough that an agent must read carefully to select correctly, and 93 tools compounds the selection pressure.

Naming Consistency4/5

snake_case throughout with strong family conventions: cue_*/unspoken_*/hold_*/continuity_*/pack_*/review_* prefixes and consistent _add/_rm/_check/_detect/_apply suffixes. Deviations exist: list operations are inconsistently named (list_media vs cue_ls vs review_list vs describe_ls), and several single-noun get/set tools (head, tail, music, canvas, doctor, assets) break the verb_noun pattern.

Tool Count2/5

At 93 tools this is far beyond the 25+ threshold and overwhelms the agent-facing surface, even though the domain is broad. The count forces every tool call through a huge selection space and makes the set impractical to navigate from a plain tool list. Consolidation — e.g., folding hold_*, pack_*, or review_* families together, or merging read-only reports — would be needed to bring it into a usable range.

Completeness5/5

The surface covers the full proofcut pipeline end-to-end: project lifecycle, import, transcription and transcript QA, timeline editing, cues, captions, cards, packs, framing, holds, music, export, render verification, continuity, and human review. CRUD is consistently closed per family (add/rm/ls for cues and unspoken marks, check/accept/reject/ls for continuity, apply/show/status for packs). No obvious dead ends: every mutation is undoable, and deliberately unsupported operations (like manual transcript correction) are explicitly designed around rather than omitted.

Maintenance

ActivityActive
ResponsivenessResponsive