Skip to content

smart crop: frame each shot for the delivery frame - #36

Merged
OrellBuehler merged 7 commits into
mainfrom
feat/smart-crop
Aug 27, 2026
Merged

smart crop: frame each shot for the delivery frame#36
OrellBuehler merged 7 commits into
mainfrom
feat/smart-crop

Conversation

@OrellBuehler

Copy link
Copy Markdown
Owner

Why

Kerf can already be told what frame a project is cut for (Timeline.format), and the last release made it check whether the cut suits where it's going. What it could not do was decide which part of each shot survives the reshape — and that is most of the work when 16:9 footage becomes a 9:16 Reel.

Both fits pick that axis without looking. Cover takes the middle; Contain keeps everything and shrinks it into a letterboxed strip. Neither is right when the subject stands in the left third, which is where a subject usually stands.

What

smart_crop samples where each shot's content actually sits and writes the crop that keeps it — per clip, so a cut of six shots gets six framings rather than one compromise.

  • engine::salience_map — one ffmpeg pass decodes ~48 tiny gray frames of a source window and scores each cell by edge energy plus frame-to-frame motion, so a locked-off talking head scores on detail and a follow shot on both. Deliberately not face detection: no model to ship, no licence to carry, and the answer only has to beat a centre crop — which is what the alternative actually is.
  • SalienceMap::crop_for (pure, unit-tested) slides a window of the delivery aspect across that map and returns the crop that keeps the content, with a CENTER_BIAS so a flat map resolves to the plain centre crop rather than to whichever edge won by rounding.
  • Project::smart_crop applies it as one undoable Smart crop revision, split three ways for the lock-free pattern so the decodes never hold the project lock. Clips already the delivery shape are skipped; so are 360-reframed clips, whose virtual camera is the framing decision. A pass that changes nothing writes no revision.

The result is an ordinary Transform crop, which the graph already applies before the fit scale — so the preview, the scrubbed still and the export all follow from the one change, and the inspector's sliders still have the last word. No new filter code in the export graph; every pre-existing graph test is byte-identical.

Surfaces

  • Tauri smart_crop(clip_id?) and MCP smart_crop — both plan under the lock, decode with it released, apply under it again. The server instructions now pair it with set_delivery_format, since an agent that reshapes to 9:16 without it keeps whatever was in the middle of every shot and has no way to know.
  • Inspector grows a Framing section above the crop sliders it writes: Smart crop, Reset crop, and a line explaining why the button is off when the shot already matches the frame or is 360.
  • Agent panel gains a Frame for the delivery preset chip for the whole cut.
  • src/lib/smart-crop.ts mirrors only the shape arithmetic for the browser harness (the way platforms.ts does) — with no decoder it lands on the centre window, which keeps bun run dev drivable.

Verification

  • 249 kerf-core tests green (--no-default-features), including 9 new ones over the pure crop math, the arg builder and the frame scoring, plus a graph test that the crop lands before the fit.
  • New #[ignore]d real-ffmpeg test: synthesizes a 16:9 shot with its only content in the left third, samples it, and asserts the 9:16 window keeps it — a centre crop would miss it entirely. Passes locally, along with the rest of the --ignored suite (downloads_a_real_model still fails in WSL2, as before and unrelated).
  • cargo clippy --workspace --no-default-features --all-targets clean; bun run check clean; 42 bun tests green.
  • Driven end to end through the browser harness with Playwright: refuses with no delivery frame set, then frames every shot and records the revision; the inspector's Framing section enables and disables as the frame changes.

Note: this branch also carries the three unpushed delivery-last-mile commits (cover frames + platform readiness) that were sitting on local main.

🤖 Generated with Claude Code

Two halves of the last mile a social-video cut has to travel after the
render, both in kerf-core so the GUI and an agent get them together.

`export_still` writes the composited timeline at a given time to a real
file at full delivery resolution, through the same graph the export
renders — so a cover is literally a frame of the video it fronts, at the
shape the project is cut for, rather than a screenshot to crop back into
agreement. The still arg builder grew a sink (`StillOutput`) instead of a
second copy: the preview keeps its MJPEG pipe, a cover gets a JPEG or PNG
file, and every existing still test is untouched. An `--ignored` test
runs the binary and probes the result, because an arg builder that never
produced a file would look identical from here.

`platform.rs` answers whether the cut is ready to go somewhere. It keeps
two limits apart that are usually conflated: what a platform *rejects*,
and what it accepts and then stops distributing — a four-minute Reel
uploads fine and is shown only to existing followers, which is the worse
of the two because nothing tells you. Errors, warnings and tips, each
phrased with the real numbers ("0:20 over", "cutting 1:00 would keep it
in the feed"). Aspect is compared as a ratio, so 720x1280 reads as the
right shape and merely soft. Limits verified 2026-08-25 and advisory —
Kerf says what it thinks and exports what you ask for.
The GUI gets export_cover / platform_targets / platform_check, plus
reveal_path so a finished render can be found — it opens the containing
folder rather than the file, since "show me where it went" is not a
request to launch a player. The heavy one follows the usual shape:
resolve under the lock, decode with it released.

The agent gets platform_check and export_cover, and the server
instructions now tell it to check before reporting a cut finished — an
agent that assembles a four-minute Reel has done the work and lost the
audience, and nothing in the file would have told it.
The export dialog now leads with where this render can go: "Ready for
Instagram Reels · YouTube Shorts · TikTok", then any length error or
reach warning on its own line, then one collapsed line for shape. That
last grouping is the point — a landscape cut earns a near-identical
"will be letterboxed" from all four vertical feeds, and four lines say
nothing four times, so issues carry an `IssueKind` and the panel folds
them into one line naming the platforms. It judges the frame this render
will actually produce, not the project's: a 9:16 project exported at
1920x1080 is a landscape file and is told so.

The cover frame is saved from the preview's context menu at the
playhead, and both a finished export and a saved cover offer "Show in
folder" — a path in a toast is not much use on its own.

kerf-core decides all of it. `platforms.ts` mirrors the check for the
browser harness only, so the panel can be driven under `bun run dev`;
verified there against both a 16:9 and a 9:16 render.
Reshaping a cut throws away most of one axis, and both fits pick that axis
without looking: Cover takes the middle, Contain keeps everything and shrinks
it into a letterboxed strip. Neither is right when the subject stands in the
left third, which is where a subject usually stands.

engine::salience_map decodes ~48 tiny gray frames of a source window in one
ffmpeg pass and scores each cell by edge energy plus frame-to-frame motion, so
a locked-off talking head scores on detail and a follow shot on both. Not face
detection: no model to ship, and the answer only has to beat a centre crop.

SalienceMap::crop_for then slides a window of the delivery aspect across that
map and returns the crop that keeps the content, with a centre bias so a flat
map resolves to the plain centre crop rather than to whichever edge won by
rounding. Project::smart_crop applies it per clip as one revision, split for
the lock-free pattern so the decodes don't hold the project lock.

The result is an ordinary Transform crop, which the graph already applies
before the fit scale — so the preview, the still and the export all follow, and
the inspector's sliders still have the last word.
Both follow the shape every heavy op here uses: plan under the project lock,
decode with it released, apply under it again — one short ffmpeg pass per clip
must not freeze the window or stall the other MCP tools.

The server instructions pair smart_crop with set_delivery_format, because an
agent that reshapes a cut to 9:16 without it keeps whatever happened to be in
the middle of every shot and has no way to know.
The inspector grows a Framing section above the crop sliders it writes: Smart
crop for the selected shot, Reset crop, and — when the shot already matches the
frame, or is 360 and framed by its virtual camera — a line saying why the
button is off rather than a control that would refuse.

The agent panel gains a "Frame for the delivery" chip for the whole cut, next
to the other presets that run a local op.

smart-crop.ts mirrors only the *shape* arithmetic for the browser harness, the
way platforms.ts does: with no decoder to sample with it lands on the centre
window, and which part of the shot survives is the half that only exists with
media behind it.
The delivery last mile went in unformatted too, so this covers platform.rs
and platform_check alongside the smart crop additions.
@OrellBuehler
OrellBuehler merged commit 53a047b into main Aug 27, 2026
23 of 24 checks passed
@OrellBuehler
OrellBuehler deleted the feat/smart-crop branch August 27, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant