> ## Documentation Index
> Fetch the complete documentation index at: https://docs.framelane.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Cutting

> Take source ranges out of video and audio in one atomic project op, by time or by transcript words, and let the rest of the timeline close up.

The `cut` op removes stretches of a clip's **source** — the dead air, the false start,
the "um" — from a project. Send it like any other op, through
`POST /v1/projects/{id}/ops` or the MCP `edit_project` tool:

```json theme={null}
{
  "ops": [
    {
      "op": "cut",
      "ids": ["talk"],
      "remove": [
        { "start": 12.4, "end": 13.1, "category": "filler", "reason": "um" },
        { "start": 41.0, "end": 47.5, "category": "repeat", "reason": "second take is cleaner" }
      ]
    }
  ],
  "if_version": 3
}
```

The renderer plays one source range per element, so a cut splits each target into
ordinary elements, one per kept range (its **pieces**), and moves everything else on the
timeline in the same version. Nothing is half-applied: if any part of the op cannot be
made, the whole batch is refused and the head stays where it was.

All times in a cut are **source seconds** — seconds into the file — never timeline
seconds. A range that was already removed lies outside every piece, so sending the same
cut twice changes nothing.

## Say what goes

Use exactly one of these three:

| Field | Meaning |
| - | - |
| `remove` | Source ranges to take out: `{start, end, category?, reason?}`. Overlapping, unsorted and repeated ranges merge. `category` and `reason` come back in the cut report. |
| `keep` | Source ranges to keep: `{start, end}`. Everything else in each target goes. Handy for a short taken from a long recording. |
| `words` | Transcript word indices instead of times. See [Cutting by words](#cutting-by-words). |

`ids` lists the video or audio elements to cut: an element, or pieces an earlier cut
made. Each target is cut only inside its own source window (`in_point` to `out_point`);
a range outside every target is listed once, in the first target's `skipped_ranges`, and
does nothing. To cut a
talking head whose sound is a separate audio element, list both: they are cut in step.
Leave one out and the report warns, because it plays on out of step after the first join.

## Cutting by words

Cut with the transcript's own word timings rather than times you work out yourself:

```json theme={null}
{
  "op": "cut",
  "ids": ["talk", "talk_audio"],
  "words": {
    "transcript_task_id": "task_01J8QR2K5VKDGN2T4FBM3CZYX7",
    "remove": [[14, 14], [52, 61]],
    "silences": {}
  }
}
```

* `transcript_task_id` is a completed [`transcribe`](https://docs.framelane.io/tasks/transcribe) task in your
  workspace, made from the same `source_url` the targets play. Transcribe with
  `disfluencies: true` if you want to cut filler words: without it they are left out of
  the transcript and have no index.
* `remove` (or `keep`) takes word-index ranges `[i, j]`, both ends included, listed in
  order with each word once. Indices are integers; `"14"` is refused.
* A cut takes about 90% of the pause on each side of the removed words. It leaves 10% of
  each pause next to the words that stay, and never less than 20 ms, so the cut never
  lands inside a word you kept.
* The server converts the transcript's millisecond timings itself, and first clamps any
  words the recogniser overlapped, so every pause is zero or more.
* If the transcript's mean word confidence is under 0.8, the report says so: preview the
  joins before you render.

### Silences

`silences` shortens long pauses between the words that stay, including the pause left
at a join where words were removed. Send `{}` for the defaults:

| Field | Default | Meaning |
| - | - | - |
| `within` | `0.5` | Shorten a pause inside a sentence that lasts at least this many seconds… |
| `keep_within` | `0.1` | …leaving this much of it on each side. |
| `between` | `1.1` | Shorten a pause between sentences that lasts at least this long… |
| `keep_between` | `0.25` | …leaving this much on each side. |
| `trim_ends` | `true` | Cut the silence before the first word (to 20 ms before it) and after the last (keeping 0.25 s). |

A sentence ends at `.`, `!`, `?`, `。`, `！` or `？`, and the last word always ends one, so
an unpunctuated final sentence is treated as finished speech. A pause counts only for
the part that still plays, so running the same silence pass twice changes nothing.

## Pieces

The first piece keeps the element's id. Each later piece is named after the element and
the second its source starts at, in milliseconds: cutting `talk` at 12.4–13.1 s makes
`talk` and `talk~13100`. The names come from the source, so the same cut always makes the
same names, and sending it again makes none. A piece keeps its name for good, though: if a
later cut takes the head of `talk~13100`, that piece starts later than 13.1 s under the
same name, so read where a piece starts from its `in_point`. Splitting one cut over
several ops can therefore name the pieces differently from a single op; what plays where
is the same.

Every piece is a full copy of the element with its own `in_point`, `out_point` and
`time`. Its motion goes where it plays: an entrance on the first piece, an exit on the
last (still anchored to the end), a mid-clip accent on the piece it lands in, and loops
and slow drifts such as Ken Burns on every piece, in phase.

## Ripple

`ripple` decides what happens to the rest of the timeline:

* **`all`** (the default) closes every gap, as if the removed moments never played:
  * Later elements move up, with their animations and keyframes.
  * An overlay that plays across a cut gets shorter: video and audio through their
    `out_point`, everything else through `duration`. A music bed under the speech simply
    ends sooner.
  * Captions (`word_animation`) are re-timed. A word with less than a quarter of it left
    is dropped, from the caption's text too when the text's words line up with the
    timings.
  * The request `duration`, when it is set, shrinks by what was removed.
* **`none`** leaves each piece where it played, gaps and all, and moves nothing else.

Either way, a transition into a cut element stays on its first piece and one out of it
moves to the last piece, and groups list the new pieces beside the element.

An element that would lie entirely inside a removed range — a title over the take you
cut — makes the op fail with its id, so nothing disappears silently. Move it, or send
`"contained": "remove"` to delete it with the cut. A caption with none of its words
left is removed either way.

## Dry run

Add `"dry_run": true` to the ops body to see exactly what the batch would do — the
diff, the changed ids, the lint and the cut report — without saving a version. It works
for any batch, not only cuts, and checks `if_version` like a real apply. Read the
report, then send the same body without `dry_run`.

The result's `cut_report` has one entry per target:

| Field | Meaning |
| - | - |
| `id` | The target. |
| `pieces` | Its pieces in source order; empty if it was removed whole. |
| `removed` | The source ranges taken out, after snapping and merging, with their `category` and `reason`. |
| `removed_seconds` | How much shorter the target is on the timeline. |
| `skipped_ranges` | Ranges that changed nothing: under one frame here, or outside every target (listed on the first target only). |
| `warnings` | Anything to check. The first target also carries what the cut did to other elements, and transcript notes. |

## Joins and fades

Each new join gets a short linear audio fade on both sides, 15 ms by default, so a cut
through a waveform does not click. Set `join_fade` (0–0.5 s; `0` for hard joins). A fade
the element already had stays on its outer ends: the fade-in on the first piece, the
fade-out on the last. These fades shape the sound only; the picture cuts hard.

## Limits

* Cut boundaries snap inward to the output's frame grid (`frame_rate`, 30 by default),
  where the clip plays them at its `speed`, so every join lands on a frame and a removal
  never grows past what you asked for. Ranges that touch or overlap merge first; a
  removal shorter than one frame is then skipped.
* A piece shorter than two frames is removed with its neighbours and reported: the
  renderer drops video and audio items under 10 ms.
* One cut op makes at most 300 pieces. Each piece re-opens and seeks its source when it
  renders, so many short pieces render slowly; the `MANY_SHORT_PIECES` warning says when
  a source has 20 or more pieces with a median under half a second, and `SLIVER_PIECE`
  flags any video or audio element under two frames.
* Speed-ramped elements, `loop: true` elements and track-matte sources cannot be cut.
  A speed-ramped overlay cannot play across a cut either.
* A target without an `out_point` takes its length from the stored upload. If the file
  was never uploaded, set `out_point` first.
* Targets that overlap on the timeline must remove the same moments: a video and its
  own audio track, cut with the same ranges, are fine.
* Removing everything a cut targets is refused. Delete an element with `remove_element`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.