Editing the video
How to edit a video through the Slicer API — word ids in the video text, one operation catalog, elements by time, revisions, captions from speech, AI at cost.
On this page
- How edits work
- Get the video as text
- Send operations
- Export when you're done
- Operations
- Speech and fragments
- Fragment and video settings
- Frame, mask, speed and sound over time
- Captions
- Elements
- Music and voice
- Your photos and videos over the video
- Picture in picture
- Values
- Elements by time
- Skipped operations
- Revisions
- Captions from speech
- AI assistant
- Caption translation
- Auto color
- Your own director
- Brief
- Frames
- Plan
- Plan issues
- Style
- Caption look
- The edit document (advanced)
- Example: remove fillers and add a hook
- What's next
A finished video or the draft of a project without the AI director can be edited just like in the app's editor: cut words and pauses, split, trim, reorder and duplicate fragments, add a freeze frame, fix captions, add a hook, plaques, titles, emoji, zooms and markers, change speed (and the speed curve), sound, transitions, effects and colour, framing, mask, chroma key and keyframes, set music and voice, put your own photos and videos over the video — full frame or in a window, change the cover and the frame format. Edits are free and show up in the editor right away; the video file is updated by export. Only Slicer's AI features are paid: captions from speech, the AI assistant and caption translation, all at cost. Your agent can also edit the whole video itself, for free: it writes the plan, and Slicer applies the rules of its AI director to it — Your own director.
How edits work
Get the video as text
GET /v1/projects/{id}/transcriptreturns the video as text: fragments in order, words withidinside them, marks for cut and highlighted words, and elements — placed on words or by video time.Response (shortened) { "rev": 7, "durationSec": 31.4, "text": "…", "blocks": [ { "id": "b1", "position": 0, "sourceIndex": 0, "kind": "words", "words": [ { "id": 12, "text": "Today", "cut": false, "accent": false }, { "id": 13, "text": "I'll", "cut": false, "accent": false }, { "id": 14, "text": "um", "cut": true, "accent": false }, { "id": 15, "text": "show", "cut": false, "accent": true } ] }, { "id": "b2", "position": 1, "sourceIndex": 2, "kind": "range", "words": [] } ], "elements": { "hook": "A reel in 3 minutes", "plaques": [], "cards": [], "emoji": [{ "id": "e1", "emoji": "🚀", "wordId": 15, "start": 1.2, "end": 2.4 }], "zooms": [{ "id": "z1", "fromWordId": 12, "toWordId": 15, "start": 0, "end": 2.4 }], "titles": [ { "id": "t1", "text": "Step 1", "font": "punch", "size": 96, "color": "#FFFFFF", "background": "pill", "align": "center", "x": 540, "y": 420, "fromWordId": null, "toWordId": null, "start": 8, "end": 11, "anchor": { "blockId": "b2", "at": 3.5, "duration": 3 } } ], "markers": [{ "id": "m1", "time": 12.5, "label": "Music here", "color": "yellow" }] } }kind: "words"is a speech fragment with words;kind: "range"is a fragment by source time: b-roll, a photo or a clip without recognized speech. It may have no words.startandendof an element are where it sits in the video, in seconds. Nostart— the element isn't visible right now (its words are cut).- An element placed by time rather than on words comes with
fromWordId: nulland ananchor— the fragment, the second of its source and the duration (Elements by time). markersare the author's notes on the timeline; they don't appear in the video.
The
textfield is the same video as a single string in a form convenient for a language model: you can pass it to your own AI agent so it can suggest edits. Fragments without speech are shown in it with their time in the video.Send operations
POST /v1/projects/{id}/editswith therevfrom step 1 and a list of operations (up to 80 at a time):Request body { "rev": 7, "operations": [ { "op": "cut_fillers" }, { "op": "set_hook", "text": "A reel in 3 minutes" }, { "op": "set_accent", "wordId": 18 }, { "op": "set_emoji", "wordId": 18, "emoji": "🔥" }, { "op": "add_plaque", "fromWordId": 20, "toWordId": 26, "kind": "list", "text": "Three steps", "caption": "Upload\nPick a style\nExport" }, { "op": "add_title", "start": 8, "duration": 3, "text": "Step 1", "titleBackground": "pill", "titleIn": "pop" }, { "op": "tune_block", "id": "b3", "transition": { "kind": "dissolve", "duration": 0.4 }, "animIn": { "kind": "zoomIn", "duration": 0.5 } } ] }Response { "ok": true, "data": { "rev": 8, "applied": 7, "skipped": [], "durationSec": 29.8 } }Export when you're done
Edits are saved immediately. To get an updated file, run an export — it's free.
Operations
Slicer has a single operation catalog: the same operations are used by manual edits in the editor, the AI assistant, the
director in the Chat, the API and the edit_project MCP tool. So the result matches what you'd get in the editor.
Words are addressed by their id from the video text, fragments and elements by their own id. A word range is
fromWordId…toWordId, inclusive; fragment positions start at 0.
Speech and fragments
| Operation | Fields | What it does |
|---|---|---|
cut_words | fromWordId, toWordId | cut words out of the video |
restore_words | fromWordId, toWordId | bring back cut or unused source words |
cut_fillers | — | cut filler words |
cut_silences | minPause, blockIds | remove pauses by silence, free and without speech recognition: splits fragments by time (kind: "range") at pauses longer than minPause — 0.3, 0.5 (default) or 1 second; without blockIds — in all such fragments |
add_source | sourceIndex, position, srcStart, srcEnd | put a project source into the video as a new fragment: without srcStart/srcEnd — its speech by words or the whole source; without position — at the end |
remove_block | id | delete a fragment together with the elements placed on it by time |
move_block | id, position | move a fragment |
slip_block | id, delta | show another piece of the same source in a b-roll fragment: delta in seconds, + — later footage; the fragment's length and place stay the same |
split_block | id, at | split a fragment in two at the video moment at (s) inside it; both parts keep its settings |
trim_block | id, edge, delta | trim or extend a fragment: edge start or end, delta — how far to move the edge, in seconds of the video (+ — later, − — earlier), −60…60. start +1 cuts the first second, end +1 adds a second of the source |
duplicate_block | id | a copy of the fragment right after it |
freeze_frame | id, at, duration | a freeze frame: a new silent fragment with a held frame. at — a video moment inside the fragment (default — its last frame), duration — 0.3–10 s (default 2) |
set_hold | id, duration | the length of a freeze frame, 0.3–10 s |
replace_source | id, sourceIndex, srcStart | show another source in a fragment from second srcStart (default 0): in b-roll the picture is replaced in place, the length stays; in a fragment with speech the speech stays and the new picture goes over it as an insert |
Fragment and video settings
| Operation | Fields | What it does |
|---|---|---|
tune_block | id, speed, volume, fadeIn, fadeOut, effect, transition, filter, adjust, lut, animIn, animOut, background | speed, volume, sound fade-in and fade-out, effect, transition into this fragment, color — filter, Adjust and LUT, entrance and exit animations, the background behind a fitted or masked picture — blur (a blurred frame) or a colour #RRGGBB; null resets a field |
set_process | id, process | free fragment processing — a list of reverse, stabilize, denoise (noise reduction), enhance (audio enhancement); replaces these four, an empty list — no processing. The server prepares the result in the background, usually within a minute |
apply_keyframe_preset | id, preset | a smooth camera move across the whole fragment by keyframes: zoomIn, zoomOut, panLeft, panRight, panUp, panDown; replaces the fragment's keyframes |
apply_look | id, blockIds, parts | copy the look of fragment id to others: without blockIds — to all other fragments; parts — what to copy: speed, audio, color, effect, transition, animations, mask, background, framing (default — everything except framing) |
set_all_transitions | transition | one transition on every cut (null — remove all) |
set_all_effects | effect | one effect on every fragment (null — remove all) |
set_all_color | filter, adjust, lut | one color on every fragment: filter, Adjust and LUT; whatever the request doesn't include is removed (filter: null alone removes all color) |
set_format | format | frame format: 9:16, 4:5, 1:1, 16:9 |
set_cover | start, coverTitle, coverStyle, coverIntro | the cover: the frame (a moment of the video, s), a title on it up to 60 characters (null — no title), the title look hook or plaque, and whether to show the cover as the first frame |
Frame, mask, speed and sound over time
Since API 1.5.0.
| Operation | Fields | What it does |
|---|---|---|
set_framing | id, mode, cx, cy, zoom | fragment framing: auto — automatic, crop — fill the frame (window centre cx, cy — shares of the source, 0–1), fit — the whole picture with a background, track — the frame follows the face; zoom — 1–3. track works only if faces in this part were already found — by the AI montage or by “Follow the face” in the editor; otherwise the operation is skipped |
set_keyframe | id, t, x, y, scale, rotate, opacity, ease | a keyframe t seconds from the fragment start: shift x, y (shares of the frame, −1…1), scale 0.2–4, clockwise rotation −180…180°, opacity 0–1 (below 1 — fades to black), easing to the next keyframe smooth (default), linear, in (speeds up), out (slows down). A keyframe at that moment is changed; fields not in the request take the frame's look at t. Up to 24 per fragment; a move needs two keyframes |
remove_keyframe | id, t | remove the keyframe at t |
set_mask | id, shape, size, feather, cx, cy, rotate, invert, maskKeys | the mask: shape circle, rounded, heart, stripe (none — remove), size 0.2–1, feather 0–1, centre shift cx, cy — −0.5…0.5 shares of the frame from the centre, rotation in degrees, invert — video outside the shape, background inside; maskKeys — animation [{ "t", "cx", "cy", "size", "rotate" }], up to 24 points (null — no animation). Fields not in the request stay as they were |
set_chromakey | id, keyColor, similarity, blend, spill, keyBackground, keyImage | chroma key, free: the screen colour #rrggbb (null — off), similarity 0.05–0.5 (0.15 by default), edge softness 0–0.3 (0.1), spill — remove the coloured fringe (true by default); behind — the colour keyBackground or a project photo keyImage (an image upload id, null — colour) |
set_speed_curve | id, preset or speedPoints | a speed curve of a footage fragment without speech: a ready one — montage, hero, bullet, flash, rampUp, rampDown — or your own [{ "t", "speed" }]: 2–8 points, t — share of the fragment 0–1, speed 0.1–8, linear in between. The fragment's sound is muted on a curve; preset: null — normal speed |
set_smooth | id, on | smooth slow motion of a footage fragment slower than 1× (a source piece up to 10 s): the server interpolates frames in the background, free |
set_volume_keys | id, volumeKeys | fragment volume by points [{ "t", "v" }]: t — seconds from the fragment start, v — 0–1, linear in between, up to 24 points; replaces the constant volume, null — constant again |
[
{ "op": "set_speed_curve", "id": "b4", "preset": "bullet" },
{ "op": "set_smooth", "id": "b4", "on": true },
{ "op": "set_keyframe", "id": "b5", "t": 0, "rotate": 0 },
{ "op": "set_keyframe", "id": "b5", "t": 2.5, "rotate": 12, "scale": 1.2 }
]Captions
| Operation | Fields | What it does |
|---|---|---|
set_text | wordId, text | fix a caption word (up to 200 characters) |
set_accent, clear_accent | wordId | highlight a word with the accent color or remove the highlight |
set_caption_position | placement | caption position: top, center, bottom — like a manual move in the editor: every phrase goes to this height, and smart Auto doesn't move them (style) |
set_phrase_text | fromWordId, toWordId, text | rewrite the caption text of a whole phrase — typos, names, numbers; the speech doesn't change |
reset_phrase_text | fromWordId, toWordId | the caption text of these words — back to the recognized speech |
Elements
Elements are placed on words (fromWordId, toWordId or wordId) or — where there is no speech — by video time
(start, duration).
| Operation | Fields | What it does |
|---|---|---|
set_hook, remove_hook | text (up to 120 characters) | a catchy title for the first seconds |
add_plaque, remove_plaque | words or time, text, caption, kind / id | a plaque: a title up to 60 characters and lines separated by \n; kind — title, number, list, quote, name |
add_title, update_title, remove_title | words or time, text and look / id | a title — a free text layer, up to 3 lines separated by \n (up to 200 characters); its look is described below |
set_emoji, remove_emoji | wordId or start + duration, emoji / id or wordId | an emoji over the video; only from Slicer's set (GET /v1/styles → emoji) |
add_zoom, remove_zoom | words or time, scale, motion / id | a camera zoom: scale 1.05–1.6 (default 1.3), motion push — a smooth push-in (default), pull — a pull-out, punch — a sharp jump |
add_card, remove_card | words or time, text, caption / id | a media card: text — an image description in English, caption — a label |
add_marker, update_marker, remove_marker | wordId or start, label, color / id | a marker — a note for the author on the timeline, up to 40 characters, not shown in the video; color — cyan, yellow, red, green, violet |
add_sound, update_sound, remove_sound | wordId or start, kind / id, kind / id | a sound at a moment of the video: whoosh, pop, ding, click, swipe, rise, boom, chime; it moves with its fragment and plays even with sound effects turned off; up to 60 sounds per video. To move a sound — move_element with element: "sound" |
update_plaque | id, text, caption, kind | change a plaque's title, lines or kind in place |
update_card | id, caption, uploadId | a media card: the label ("" — none) and your own photo from the project media instead of the picture from a description, free (null — back to the description) |
update_zoom | id, scale, motion | change a zoom's scale and motion in place |
move_element | element, id, start, duration | move an element to another moment of the video: element — plaque, card, zoom, emoji, insert, title, sound or marker; over speech it sticks to the words there |
set_element_position | element, id, x, y | move an emoji, card or plaque (element — emoji, card, plaque) around the frame: its centre in the 1080×1920 frame; null — back to the default place |
Music and voice
Since API 1.7.0. A track or a voice recording is a project source (sourceIndex) or a file from the project media
(uploadId — your own files added to the project in the editor). Voice recording ids are in the
edit document, the voices list.
| Operation | Fields | What it does |
|---|---|---|
set_music | sourceIndex or uploadId, volume, sourceVolume, ducking, offset, fadeIn, fadeOut | background music: an audio source or audio from the project media; without sourceIndex and uploadId — change the current music. Music volume 0–1 (usually 0.2–0.35), sourceVolume — fragment sound under the music 0–1, ducking — music gets quieter under speech, offset — the track second to start from, fade-in and fade-out — 0–5 s |
remove_music | — | remove the background music |
fit_music | on | true — fit the track to the video length: it ends together with the video on a bar line; false — play it as is |
fit_to_beats | every | snap the cuts of footage fragments to the music beats: every beat (1, default), every 2nd or 4th |
add_voice | sourceIndex or uploadId, start, volume | a voice recording or an audio source on the voice track from video second start; volume 0–2 (default 1). Fragment sound and music get quieter under the voice |
move_voice | id, start | move a recording to another moment of the video |
trim_voice | id, edge, at | trim a recording: edge start or end and its new time in the video at (s) |
set_voice_volume | id, volume | recording volume, 0–2 |
remove_voice | id | remove a recording from the track |
[
{ "op": "set_music", "sourceIndex": 3, "volume": 0.25, "ducking": true, "fadeOut": 1.5 },
{ "op": "fit_music", "on": true },
{ "op": "fit_to_beats", "every": 2 }
]Your photos and videos over the video
Since API 1.7.0. An insert covers the picture with a photo or video while the speech goes on — for example, to show what
the author is talking about. It is placed on words or by time, like elements; insert ids are in the
edit document, the inserts list.
| Operation | Fields | What it does |
|---|---|---|
add_insert | sourceIndex or uploadId, fromWordId + toWordId or start + duration, fit, srcStart, mode | a photo or video from the sources or project media over the video: fit — cover (fills the frame) or contain (the whole picture), srcStart — for a video, from this second, mode — full (full frame, default) or pip (in a window; the window look — set_pip) |
update_insert | id, fit, srcStart | the insert's fit and the start of its video |
remove_insert | id | remove an insert |
{ "op": "add_insert", "sourceIndex": 2, "fromWordId": 40, "toWordId": 52, "mode": "pip" }Picture in picture
Since API 1.6.0. An insert — your own photo or video over the video (its id is in the edit document, the
inserts list) — can cover the whole frame or sit in a window.
| Operation | Fields | What it does |
|---|---|---|
set_pip | id, mode, x, y, scale, rotate, opacity, shape, radius, borderColor, borderWidth, volume | mode: "pip" (default) — the insert as a window over the video, "full" — back to full frame. Window centre x, y — in the 1080×1920 frame (may be partly past the edge), width scale — share of the frame width 0.1–1, clockwise rotation −180…180°, opacity 0–1, shape rect or circle, corner rounding radius — share of the shorter side 0–0.5, border — colour #rrggbb (null — no border) and width 0–0.1, volume — sound of the video in the window 0–1 (default 0). Fields not in the request stay as they were; for a window with keyframes the position is changed in the first keyframe. Up to 10 windows per video: an 11th is skipped |
{ "op": "set_pip", "id": "i4k2m9q", "x": 800, "y": 1500, "scale": 0.4, "shape": "circle", "borderColor": "#ffffff", "borderWidth": 0.03 }Window keyframes are set in the editor; in the edit document they are inserts[].pip.keyframes.
Values
speed— 0.25 to 4;volume— 0 to 1;fadeIn,fadeOut— 0 to 3 seconds;effect—shake,pulse,flash,cinema,vignette,grain,blur,glitch,vhs;transition—{ "kind", "duration" }, duration — 0.2 to 1.2 seconds; kind —fade,flash,zoom,dissolve,blur, slidesslide,slideRight,slideUp,slideDown, wipeswipe,wipeRight,wipeUp,wipeDown, circlecircleOpenandcircleClose,zoomIn,pixelize,radial,scatter;filter—{ "id", "strength" }, filter —bw,warm,cool,vintage,sepia,vivid,pastel,noir,cinema, strength — 0 to 1;adjust— Adjust:exposure,brightness,contrast,saturation,temperature,tint, each from −1 to 1 (0.15 is a noticeable step); replaces all of the fragment's sliders at once;lut—{ "preset", "strength" }or{ "uploadId", "strength" }: a built-in LUT —teal-orange,film,matte,bleach,golden,moody,nordic,cross,mono-film,vibrant;uploadId— your own.cubefrom My LUTs (its id is in the edit document on a fragment that uses it); strength — 0 to 1;animIn,animOut—{ "kind", "duration" }, kind —fade,zoomIn,zoomOut,slideLeft,slideRight,slideUp,slideDown, duration — 0.2 to 2 seconds.
A title's look uses fields with the title prefix; in update_title, null resets a field:
| Field | Value |
|---|---|
titleFont | a font from GET /v1/styles → fonts |
titleSize | font size, 24–200 px on a 1080×1920 frame |
titleColor, titleBackgroundColor | text and background colour, #RRGGBB |
titleBackground | background: none, box (per line), pill (rounded) |
titleOutline, titleShadow | letter outline and shadow, true / false |
titleAlign | alignment: left, center, right |
titleX, titleY | position on a 1080×1920 frame: the horizontal anchor (edge or centre, by alignment) and the vertical centre |
titleIn, titleOut | entrance fade, slide, pop, typewriter (letter by letter) and exit fade, slide, pop |
The full operation schema:
EditOperation
| Field | Type | Description |
|---|
Elements by time
A video without speech — b-roll, photos, a draft without recognized speech — has no words, so plaques, titles, emoji,
zooms, cards and markers are placed by time: start is a second of the video, duration is the length in seconds of
the video (0.3–60).
{ "op": "add_title", "start": 4, "duration": 2.5, "text": "Morning at the coffee shop", "titleIn": "typewriter" }{ "op": "add_zoom", "start": 9, "duration": 2, "scale": 1.2, "motion": "pull" }An element holds on to the fragment it sits over, like a connected clip: move the fragment — the element moves with it; delete the fragment — the element goes too. Over speech, prefer placing elements on words: that placement survives text edits. If an operation has both words and time, the words win.
Skipped operations
Operations are applied in order. If an operation changes nothing — the word isn't found, the fragment doesn't exist, the
emoji isn't in the set — it's skipped and the rest are applied. Skipped operations are listed in skipped:
{ "rev": 9, "applied": 2, "skipped": [{ "index": 1, "op": "remove_plaque", "reason": "element_not_found" }], "durationSec": 29.8 }reason | Why |
|---|---|
word_not_found_or_nothing_to_change | no such word, or the edit is already applied |
words_or_start_required | the element has neither words nor start, there are no such words, or there is no fragment at that moment of the video |
block_not_found | no fragment with this id |
source_not_found | no source with this index |
element_not_found | no element or fragment with this id |
emoji_not_allowed_or_word_not_found | the emoji isn't in Slicer's set, or no such word |
bad_arguments | the operation's fields failed validation |
unknown_operation | there is no such operation |
block_not_footage | a speed curve and smooth slow motion work only on a time-based footage fragment, without speech and not a freeze frame |
keyframe_not_found | there is no keyframe at that moment of the fragment |
block_not_found_or_no_face_data | no fragment with this id, or for track no faces have been found in this part yet |
block_or_source_not_found | no fragment with this id, or no suitable source |
audio_not_found | no such audio in the sources or project media |
no_music_or_beats | the video has no music, or the track's beats haven't been found yet |
recording_not_found | no such voice recording or audio |
media_or_place_not_found | no such photo or video, or no place for the insert: no such words, or no fragment at that moment of the video |
word_not_found | no such word |
nothing_to_change | the operation changes nothing |
Revisions
A video has a revision number rev; every saved edit increases it. An edit with an outdated revision is rejected with
409 stale_document — the video was changed in the editor or by another request. Re-read the video text and repeat the
edit with the new rev. This way edits from the API and the app never overwrite each other.
Captions from speech
In a project without the AI director the speech isn't recognised, so there are no captions, and
the fragments in the video text are by time. POST /v1/projects/{id}/transcribe recognises the speech in the sources —
like Captions from speech in the editor (Captions from speech). It also
recognises sources added to a project after the montage.
curl https://api.slicer.media/v1/projects/318/transcribe \
-H "Authorization: Bearer $SLICER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"language":"auto"}'| Field | What it sets |
|---|---|
sources | source indexes; by default — every source with sound whose speech isn't recognised yet |
language | the speech language: a two-letter code or auto — the language from the project settings, or, if none is set, Slicer detects it |
confirmCost | the price you agree to; without it the response is a Quote and nothing starts |
Recognition is priced at cost per minute of sound of the chosen sources: without confirmCost you get a Quote with
estimate (the expected price) and cost (the maximum), atCost: true — as with the AI assistant. Repeat
the request with "confirmCost" equal to cost: the tokens are reserved, and the actual cost is charged, never more than
cost. Requires the montage:run scope.
Recognition runs in the background. The response to the start is a job { id, status, sources, createdAt }; follow it
with GET /v1/projects/{id}/transcribe, the project's job list:
status | Meaning |
|---|---|
processing | the speech is being recognised |
ready | done: the words are in the video text, the captions follow the speech |
failed | it didn't work, error has a readable message; the reserved tokens came back |
When the job is ready, re-read GET /v1/projects/{id}/transcript: the fragments now have words with ids, you can place
elements on them and edit captions (set_text, set_accent, set_phrase_text). The edit document itself doesn't
change. POST /v1/estimate with kind: "transcribe" and projectId names the price in advance.
| Response | Meaning |
|---|---|
400 nothing_to_transcribe | the chosen sources have no sound or their speech is already recognised |
409 busy | speech in the project is already being recognised — wait until it's done |
409 not_ready | the project isn't ready for edits yet |
409 sources_expired | the sources were deleted after the retention period |
503 ai_unavailable | speech recognition is temporarily unavailable |
AI assistant
A command in plain words — the assistant picks the operations from the same catalog itself:
curl https://api.slicer.media/v1/projects/318/assistant \
-H "Authorization: Bearer $SLICER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"command":"Make the video more dynamic and add emoji on the key words","rev":9}'The assistant is priced at cost: a command costs exactly what its AI calls cost, with no markup. Without
confirmCost you get the price:
{ "ok": true, "data": { "status": "quote", "cost": 2, "free": false, "estimate": 0.4, "atCost": true, "available": 240, "enough": true, "spendLimitLeft": 176 } }atCost: true— the operation is priced at cost;estimate— the expected price in tokens, fractional;cost— the maximum price: repeat the request with"confirmCost"equal to it. The actual cost is charged, never more thancost.
With confirmCost the response is { rev, summary, applied, charged, costTokens, durationSec }: summary describes what
was done, costTokens is the actual cost of the command (fractional), charged is how many whole tokens were taken from
the balance now. The fractional remainder accumulates with the project and is charged once it adds up to a whole token.
Tokens are charged only if the assistant changed something. Requires the montage:run scope.
Caption translation
POST /v1/projects/{id}/translate with language — en, es, pt, de, fr, it, pl, tr, id, uk, ru —
and rev. The translation is laid out word by word, so accents and emoji stay in place. Translation is priced at cost
too: the price (Quote with estimate and cost) and confirmation work as for the assistant. The response is
{ rev, phrases, charged, costTokens }; you're charged only if the translation succeeded.
Auto color
POST /v1/projects/{id}/auto-color balances color without AI and for free — from 10 frames of the fragment, like the
Auto and Match all to this buttons in the editor. The body has rev, blockId and mode:
auto— correct fragmentblockId: exposure, contrast, saturation and white balance;match— make the other fragments look likeblockId: they get its filter and LUT, and each gets its own Adjust values.
{ "rev": 7, "blockId": "b2", "mode": "match" }The result is saved right away. The response has the new revision, Adjust values by fragment id (adjust), how many
fragments changed (applied) and the video's duration.
Your own director
Your AI agent can edit the video itself — on its side, for free in Slicer: it looks at the frames and writes the plan, and
Slicer applies the rules of its AI director to the plan on the server and answers with fixes and issues. The rules
themselves are not passed to the agent. How it works step by step — Your own director in the MCP
section: the get_director_brief, get_frames and submit_director_plan tools are built on these routes.
| Route | What it does | Scope |
|---|---|---|
POST /v1/projects/{id}/director/brief | the material — speech by word ids, pauses, retakes, wishes (material), the sources (sources), general tips (guide), the plan schema (planSchema), the schema of observations from the frames (scenesSchema) and rev | projects:write |
GET /v1/projects/{id}/sources/{index}/frames | frames of a source: JPEG in base64, 512 px on the longer side, up to 16 per request | projects:read |
POST /v1/projects/{id}/director/plan | a plan (plan) and observations from the frames (scenes) → the video, a new revision; the answer has fixes (fixes) and issues (issues) | projects:write |
All three are free. If the video has speech, recognise it first — Captions from speech (at cost): without words the director has nothing to cut.
Brief
The body is optional: director settings — mode (auto, talking, story, product, aesthetic), pace (calm,
balanced, dynamic), effects (off, light, bold), targetDurationSec (10–180, null — the director decides,
up to 90 seconds), hook, plaques, emoji, cameraMotion. Fields you pass are saved to the project: Slicer applies
the rules to the plan by them.
curl https://api.slicer.media/v1/projects/318/director/brief \
-H "Authorization: Bearer $SLICER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"pace":"dynamic","targetDurationSec":30}'| Response field | What it holds |
|---|---|
projectId, rev | the project and the video revision — rev is needed for the plan |
settings | the project's director settings |
sources | the sources: number (index), kind (kind), duration, file name and how many recognised words it has |
speechRecognized | whether the speech is recognised |
material | the material as text: the sources, speech by word ids w<ID>, pauses, [RETAKE …] takes, the author's wishes |
guide | a few general tips — the brief has no rules of the Slicer AI director |
planSchema | the JSON schema of the plan |
scenesSchema | the JSON schema of observations from the frames |
next | what to do next |
Frames
times — seconds of the source, comma-separated (up to 16), or evenly: from, to, count (8 frames over the whole
source by default). Seconds outside the source are clamped to its edge. A photo returns one image; audio has no frames
(400 no_picture), a missing source — 404 source_not_found.
curl "https://api.slicer.media/v1/projects/318/sources/0/frames?times=2.5,7,12.4" \
-H "Authorization: Bearer $SLICER_TOKEN"{ "ok": true, "data": { "sourceIndex": 0, "durationSec": 48.2, "frames": [{ "time": 2.5, "mimeType": "image/jpeg", "data": "/9j/4AAQ…" }] } }Plan
The body has rev (from the brief or from the previous answer), plan in planSchema and, optionally, scenes — up to
400 observations in scenesSchema: sourceIndex, start and end in seconds of the source, kind (talking_head,
demo, people, screen, broll, unusable), interest from 1 to 5 and description. What the kinds mean and how
Slicer places footage by them — Observations from the frames.
{
"rev": 3,
"plan": {
"concept": "Three steps from milk to pattern",
"segments": [
{ "kind": "speech", "role": "hook", "fromWordId": 41, "toWordId": 47, "why": "A strong opening" },
{ "kind": "footage", "role": "body", "sourceIndex": 0, "fromSec": 14.5, "toSec": 19, "why": "Showing the pattern" }
],
"hook": "Latte art in a minute"
},
"scenes": [
{ "sourceIndex": 0, "start": 0, "end": 14.5, "kind": "talking_head", "interest": 3, "description": "The barista talks at the counter" },
{ "sourceIndex": 0, "start": 14.5, "end": 21, "kind": "demo", "interest": 5, "description": "Draws a heart on a latte" }
]
}On the server the rules of the Slicer director are applied to the plan: speech by words, footage by the observations,
restrained editing, length. The plan becomes the video right away — a new revision, even if it has issues. Media cards
from a description are not accepted — their picture is paid; the update_card operation sets your own photo. Fragments
are framed in the centre; fix the framing with the set_framing operation.
| Response | Meaning |
|---|---|
200 | { rev, durationSec, planSegments, fragments, fixes, issues, video } — the video is saved: fixes — what Slicer corrected itself, issues — plan issues, video — the video as text to check |
400 bad_plan | the plan doesn't match the schema; message lists the first errors |
400 empty_plan | the plan has no fragments |
409 stale_document | the video changed after the brief or the previous plan — re-read it and send the plan with the new rev |
409 busy / 409 not_ready / 409 sources_expired | the project is rendering, isn't ready for edits yet, or its sources were deleted |
Plan issues
An issue is { code, segment, message }: a code, the plan fragment number from zero (only if the issue is about a
fragment) and what to fix — in English, for the model to read. Fix the plan and send the whole plan again with the new
rev; when issues is empty, the plan is fine.
| Code | What's wrong |
|---|---|
cuts_thought | the fragment stops in the middle of a sentence |
starts_mid_thought | the fragment starts in the middle of a sentence |
single_word | the fragment is a single word — make it a complete thought or remove it |
two_takes | the video has two takes of the same thought — keep the best one |
weak_ending | the video ends mid-phrase rather than on a complete sentence |
too_long | the video is noticeably longer than the target length or, without one, longer than 90 seconds |
too_short | the video is noticeably shorter than the target length |
unused_source | a source without speech — a video or a photo — isn't used in the plan: show it where it belongs in the story |
footage_is_talking_head | the footage shows a silent talking head instead of an action, a product or a result (by the observations) |
unusable_footage | the footage comes from a part marked unusable in the observations |
nothing_shown | the observations have demo moments or b-roll, but the plan has neither footage nor cutaways |
hook_long | the hook is longer than 8 seconds |
hook_title_long | the hook title is longer than 6 words |
Style
PATCH /v1/projects/{id}/style changes the template, font, accent color, caption look, sound effects and intro zoom —
for free. The change is partial: only the fields you pass change, everything else — including the other fields of
subtitles and params — stays as it was.
{ "templateId": "clean", "fontId": "editorial", "colorId": "mint", "subtitles": { "design": "karaoke", "size": "large" } }The new style shows in the editor right away and in the file after export. To have the AI rebuild the video with different montage settings, use a rebuild.
Caption look
The subtitles fields are the same as in the editor's Caption style sheet
(Captions). Word highlight, entrance, words on screen, letter case and colour by
speaker — since API 1.8.0.
| Field | Values |
|---|---|
enabled | show captions: true or false |
design | the design: dynamic, karaoke, classic, minimal, pop — Pop, box — Box, word — One word |
size | the size: small, medium, large |
position | the position: auto — each phrase where the frame has the most free space: off the face, hook, plaques, titles and picture-in-picture windows; top, center, bottom — every phrase in one place |
speakerColors | each speaker's words in their own color: true or false |
highlight | the spoken-word highlight: color — the accent color, pop — the word grows while the others make room, box — an accent box, underline — an underline, none — no highlight |
animation | the phrase entrance: fade, pop — a springy pop, slide — from below, words — words as they are spoken, none — at once |
words | words on screen: one, few — 2–3, line — a line of up to six words, phrase — a phrase of up to two lines |
letterCase | letter case: upper — capitals, normal — as spoken |
highlight, animation, words and letterCase also take auto, the default: the way the design intends (for
letterCase — the font). The templates are bold, clean and pop.
{ "subtitles": { "highlight": "pop" } }How a subtitles change behaves:
- only the fields you pass change, the others stay as they were. Before API 1.8.0 a partial
subtitleschange reset the other fields to their defaults; - a new
designwithouthighlight,animationandwordsresets them toauto— like picking a design in the editor: the design looks the way it is meant to right away. Size, position, letter case and colour by speaker stay. To change the design and set the highlight at once, pass both fields:
{ "subtitles": { "design": "karaoke", "highlight": "pop" } }Names and descriptions of the values in both languages are in the GET /v1/styles catalog: designs — subtitleDesigns,
positions — subtitlePositions, word highlight, entrance, words on screen and letter case — subtitleOptions:
[
{
"id": "highlight",
"title": { "ru": "Выделение слова", "en": "Word highlight" },
"help": { "ru": "Как выделяется слово, которое сейчас звучит.", "en": "How the word being spoken is highlighted." },
"values": [
{ "id": "auto", "title": { "ru": "Как в оформлении", "en": "As in the style" }, "description": { "ru": "Так, как задумано выбранным оформлением.", "en": "As the chosen style intends." } },
{ "id": "pop", "title": { "ru": "Увеличение", "en": "Pop" }, "description": { "ru": "Слово, которое звучит, увеличивается и подсвечивается, соседние слова плавно расступаются.", "en": "The spoken word grows and lights up, the others smoothly make room." } }
]
}
]The same fields are accepted by settings.subtitles when you create a project (Video settings) and by
the set_style MCP tool.
The edit document (advanced)
GET /v1/projects/{id}/document returns the whole edit document — the complete description of the video that the
preview and the render are built from — with its revision. A PUT to the same path with { rev, document } replaces it
entirely. The document goes through the same checks as edits from the editor; unknown fields are dropped.
Example: remove fillers and add a hook
// node polish.mjs 318
const API = 'https://api.slicer.media/v1';
const headers = { Authorization: `Bearer ${process.env.SLICER_TOKEN}`, 'Content-Type': 'application/json' };
const id = process.argv[2];
async function call(method, path, body) {
const response = await fetch(`${API}${path}`, { method, headers, body: body && JSON.stringify(body) });
const json = await response.json();
if (!json.ok) throw Object.assign(new Error(json.error.message), { code: json.error.code });
return json.data;
}
for (let attempt = 0; attempt < 3; attempt += 1) {
const { rev, blocks } = await call('GET', `/projects/${id}/transcript`);
const words = blocks.flatMap((block) => block.words).filter((word) => !word.cut);
try {
const result = await call('POST', `/projects/${id}/edits`, {
rev,
operations: [
{ op: 'cut_fillers' },
{ op: 'set_hook', text: 'A reel in 3 minutes' },
{ op: 'set_accent', wordId: words[0].id },
],
});
console.log(`Applied: ${result.applied}, length: ${result.durationSec} s`);
break;
} catch (error) {
if (error.code !== 'stale_document') throw error; // the video changed — re-read and retry
}
}# python polish.py 318
import os
import sys
import requests
API = "https://api.slicer.media/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SLICER_TOKEN']}"
project_id = sys.argv[1]
class ApiError(Exception):
def __init__(self, error):
super().__init__(error["message"])
self.code = error["code"]
def call(method, path, json=None):
body = session.request(method, API + path, json=json, timeout=60).json()
if not body["ok"]:
raise ApiError(body["error"])
return body["data"]
for attempt in range(3):
transcript = call("GET", f"/projects/{project_id}/transcript")
words = [w for block in transcript["blocks"] for w in block["words"] if not w["cut"]]
try:
result = call("POST", f"/projects/{project_id}/edits", {
"rev": transcript["rev"],
"operations": [
{"op": "cut_fillers"},
{"op": "set_hook", "text": "A reel in 3 minutes"},
{"op": "set_accent", "wordId": words[0]["id"]},
],
})
print(f"Applied: {result['applied']}, length: {result['durationSec']} s")
break
except ApiError as error:
if error.code != "stale_document": # the video changed — re-read and retry
raise