Welcome. This is the gentle, conceptual introduction to InkSlop's scripting language. It teaches you what the language is, how tags fit together, what order they should go in, and the habits that make a script turn out well.
When you want the exhaustive list of every tag and parameter, head to the DSL Reference. This page is the guide you read first; that page is the dictionary you keep open beside it.
InkSlop videos are described with a small markup language, a DSL (domain-specific language). Instead of dragging clips around a timeline, you write a short script made of tags, and the renderer turns it into a finished video.
The single most important idea is this:
Speech drives timing. You do not set timestamps. You write narration, and everything else (the background, the music, the effects) stretches to fit how long that narration takes to speak.
That is why most scripts are built around one block of text-to-speech. You write what should be said, then wrap it in the layers that should play while it is said.
Every tag is written between square brackets. There are two shapes.
A block tag has an opening tag, some content, and a matching closing tag. The content can be plain text or more tags nested inside.
[tag attr="value"]
content or nested tags
[/tag]A self-closing tag stands alone: it has no content and no separate closing tag. The / goes just before the final bracket.
[tag attr="value"/]You use the self-closing form for things that happen at a single point, like a sound effect or a quick camera shake.
Attributes configure a tag. They always follow name="value", and the value is always quoted, even numbers and booleans:
voice="af_heart"volume="0.3"subtitles="true"duration="2s" or duration="500ms"from="100%"A line starting with # is a comment and is ignored by the renderer.
# This line is a note to yourself, not part of the video
[tts]This line is spoken.[/tts]That is the entire syntax. Everything else is just knowing which tags exist and how to nest them.
Almost every script follows the same outer-to-inner shape. Think of it as layers stacked from the back of the screen to the front:
[video] clip, an [image], or an AI-generated [ai-image]. This is the outermost tag.[music] track that plays underneath everything. It wraps the narration so it covers the whole video.[tts] block holding all the spoken words.[zoom], [shake], [image], [sound] and similar, placed inside the narration at the exact words they should line up with.Written out, the recommended nesting order looks like this:
[video ...] # 1. background, outermost
[music ...] # 2. music wraps all content
[tts ...] # 3. one narration block
...text, with effects inline... # 4. effects live inside the tts
[/tts]
[/music]
[/video]Why this order? Because timing flows from the inside out. The [tts] decides how long the narration is, the [music] and [video] around it inherit that duration (with size="content"), and so the whole video ends up exactly as long as the words take to speak. Put the background on the outside and the speech on the inside, and the lengths take care of themselves.
A few rules make the layering predictable.
One narration block. Use exactly one [tts] block for the whole script. This keeps all the audio in a single, natural-sounding take. The only good reasons to use more than one are genuinely different speakers, or an intentional silent gap. Do not split narration just because the scene, background, or effect changed; those changes happen inside the single [tts].
Music covers everything. Place [music] so it wraps all the narration. A video with silent patches feels broken. If the mood truly must change, chain two tracks back-to-back with no gap rather than leaving a hole.
Backgrounds fill the frame. The background tag ([video], [image], or [ai-image]) sits on the outside and, with size="content" and fit="loop", stretches or loops to match the narration length.
Effects and media go inline, by word. Anything placed inside [tts] is positioned by where it sits in the text, not by a timestamp. A [shake/] dropped between two words fires exactly when those words are spoken; a [zoom]...[/zoom] wrapping a phrase runs for exactly as long as that phrase takes.
[tts engine="kokoro_tts" voice="af_heart"]
Everything was calm, and then [shake intensity="8" duration="0.3s"/] the ground split open.
[/tts]The shake happens on the word "the" because that is where you wrote it.
The narration voice is chosen on the [tts] tag with two attributes: engine (which TTS system) and voice (which voice within it). If you leave them off, the tag falls back to the app's configured defaults.
[tts engine="kokoro_tts" voice="af_heart"]
This line is spoken by the Heart voice on the standard engine.
[/tts]For most videos, use kokoro_tts. It runs on CPU, is fast and cheap, and is the sensible default. Voice IDs follow a simple convention: the prefix marks accent and gender: af_ American female, am_ American male (British voices use bf_ / bm_). Good starting points:
af_heart: warm, engaging narrator (American female)af_bella: clear and articulate (American female)am_michael: professional narrator (American male)am_adam: deep, comedic delivery (American male)The full set of standard voices lives in the voice selector on the create page (and in VOICE_MAPPINGS in the codebase). Pick one and set it once on your single [tts] block.
Two higher-fidelity engines are available when you need more than standard English narration. They cost more credits, so reach for them only when the standard voices are not enough.
qwen3_tts: higher-fidelity, multilingual voices (English plus languages like Japanese, Korean, and Chinese) and support for voice cloning. Cloning is done ahead of time by uploading an audio sample (a paid feature); once cloned you pass the returned voice ID as voice="...".mimo_tts: expressive English and Chinese voices (e.g. Mia, Chloe, Milo, Dean) with natural-language style control via an instruct attribute. The instruction is prepended to your text to steer delivery:[tts engine="mimo_tts" voice="Mia" instruct="Speak with calm authority, slow pace, deep resonance"]
The results were not what anyone expected.
[/tts]Prefer standard kokoro_tts voices unless you specifically need a non-English language, voice cloning, or fine style control.
Every file="..." attribute (on [video], [music], [image], and the fallback on [ai-image]/[ai-video]) points at one of three kinds of asset. How you write the path tells the renderer which kind it is.
1. Library assets (bundled). The built-in clips and tracks. Write the path exactly as it appears in the library (the video and music selectors on the create page expose the exact s3Key). A library path has no special prefix. Anything that is not a user/ path or an s3:// URI is treated as a library reference and resolved from the public asset bucket for you.
[video file="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"] ... [/video]
[music file="music/Wallpaper.opus" volume="0.3" fit="loop"] ... [/music]Music library files are .opus, never .mp3. Library paths never contain spaces.
2. User uploads. Files you upload yourself are referenced with a user/ prefix.
[image file="user/my-photo.png" size="content" fit="cover"] ... [/image]3. Direct S3 URIs. A fully-qualified s3:// URI is used as-is, with no rewriting.
[video file="s3://my-bucket/clips/intro.mp4" fit="loop"] ... [/video]The one rule that applies to all three: use exact paths. Copy them from the selector or your uploads. Never invent or guess a file path, and never use a display name in place of the path.
Here is the smallest useful script. A looping background video, and one line of narration with subtitles.
[video file="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
[tts engine="kokoro_tts" voice="af_heart" subtitles="true"]
Here is the very first video I made with InkSlop.
It plays over a looping background and speaks these two sentences.
[/tts]
[/video]That is a complete, valid video. The clip loops for exactly as long as the narration lasts, subtitles appear automatically, and you are done.
Now add music, a couple of camera effects, and an image overlay, all inside the same single [tts] block. Notice that nothing new happens to the structure; we are just filling in the layers described above.
[video file="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
[music file="music/Wallpaper.opus" volume="0.3" fit="loop"]
[tts engine="kokoro_tts" voice="af_heart" subtitles="true"]
Let me show you three things I learned this week.
[zoom from="100%" to="115%" easing="ease-in-out"]The first one completely changed how I work.[/zoom]
Take a look at this. [image file="user/chart.png" duration="3s"/] The numbers speak for themselves.
And then, out of nowhere, [shake intensity="6" duration="0.4s"/] everything clicked.
That is the whole story. Thanks for watching.
[/tts]
[/music]
[/video]Read it top to bottom: a background clip, quiet music underneath, and one continuous narration. The zoom runs across a single sentence, the image pops up at the word it follows, and the shake hits on cue, all synced to the speech because they live inside the [tts].
Keep all narration in one [tts] block and let sentence breaks do the pacing. Short, punchy sentences feel fast; longer ones feel calm. Because timing follows the words, you control pace simply by how you write, not by fiddling with durations. If you need a beat of silence after the words end, add a [pause duration="2s"/] so the background and music can breathe.
Pick a background that suits the energy of the piece and then mostly leave it alone. A calm explainer wants steady footage or a still image; an energetic listicle wants something with more movement. Avoid switching backgrounds so often that the viewer cannot settle.
Less is more. Camera effects ([zoom], [pan], [tilt], [shake]) are seasoning, not the meal.
[zoom] brings a static frame to life.When in doubt, reach for one subtle zoom rather than three competing effects.
If you use [ai-image] (or, when real motion is essential, [ai-video]) for backgrounds, two habits matter.
First, always add a Ken Burns [zoom] to an [ai-image] so the still frame has life, and alternate the direction between scenes: if one zooms in, let the next zoom out.
Second, and most important: images are generated independently, so without guidance they will clash in style. Append the same short consistency suffix to every prompt in the script (one phrase that locks in art style, palette, and lighting) and reuse it word-for-word. For example, a cinematic set:
[ai-image prompt="misty mountain valley at sunrise, cinematic photography, golden hour light, warm tones, consistent color grade" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="cover"]
[zoom from="100%" to="122%" easing="ease-in-out"]
[tts]The valley woke slowly in the morning light.[/tts]
[/zoom]
[/ai-image]
[ai-image prompt="dense pine forest path, cinematic photography, golden hour light, warm tones, consistent color grade" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="cover"]
[zoom from="122%" to="100%" easing="ease-in-out"]
[tts]A single trail led deeper into the trees.[/tts]
[/zoom]
[/ai-image]Both prompts share the suffix cinematic photography, golden hour light, warm tones, consistent color grade, so the scenes look like they belong to the same video. Always include a fallback pointing to a real library file in case generation is unavailable, and make sure the background layers cover the whole timeline with no gaps.
[tts] for scene changes. Keep one block; change visuals inside it.[music], or chain tracks with no gap.volume="0.3", not volume=0.3.[tag] needs a matching [/tag].You now know the shape of a script and the order tags go in. When you need the precise parameters for any tag (every size mode, easing curve, subtitle position, and timing anchor), open the DSL Reference. To generate videos programmatically, see the API Reference. Or just head to the create page and start writing.
Ready to write the real thing?