| 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. |