This document describes all available tags, their parameters, and timing behavior.
[tag param="value"]
content
[/tag][tag param="value"/]# This is a comment (shell-style)param="value" (quotes required)param="1.5" or param="100"param="true" or param="false"param="2s" or param="500ms"param="50%"These tags define scope and contain other elements.
The root container for video content. Defines the visual layer.
[video file="path/to/video.mp4" size="content" fit="none"]
...children...
[/video]| Parameter | Type | Default | Description |
|---|---|---|---|
file |
string | required | Path to video file |
id |
string | auto | Element ID for time anchoring |
from |
time | 0 |
Start position in the video file |
size |
enum | "content" |
Duration mode (see below) |
fit |
enum | "none" |
How to fit video to duration |
| Value | Behavior |
|---|---|
content |
Video duration = duration of children (TTS, etc.) |
natural |
Video plays its full natural length |
| Value | Behavior |
|---|---|
none |
No adjustment |
trim |
Cut video to fit duration |
loop |
Loop video to fill duration |
Basic looping video:
[video file="background.mp4" size="content" fit="loop"]
[tts]This narration determines video length[/tts]
[/video]Start video from 30 seconds in:
[video file="long_video.mp4" from="30s" fit="loop"]
[tts]This starts from the 30 second mark and loops from there[/tts]
[/video]Text-to-speech block. Generates audio and optional subtitles.
[tts engine="kokoro_tts" voice="af_heart"]
Text to speak with [blue]styled words[/blue].
[/tts]| Parameter | Type | Default | Description |
|---|---|---|---|
engine |
string | config default | TTS engine to use |
voice |
string | config default | Voice ID |
id |
string | auto | Element ID for time anchoring |
subtitles |
boolean | "true" |
Show subtitles |
sub-position |
enum | "center" |
Vertical position: top, center, bottom. Keep center for social video: TikTok, Shorts and Reels cover the bottom with captions and buttons, and the top with search and tabs |
sub-align |
enum | "center" |
Horizontal alignment: left, center, right |
highlight |
boolean | "true" |
Highlight current word in subtitles |
lang |
string | engine default | Narration language: en-US, en-GB, es, fr, it, pt-BR, hi, ja, zh, de, nl, pl, tr, ru, ko, ar, id, vi, sv. Without a voice, a native voice for that language is chosen. |
sub-style |
enum | "default" |
Caption style: default (line with the spoken word highlighted), karaoke (each word fills with colour as it is spoken), pop (one big word at a time that pops in, TikTok style) |
sub-highlight-color |
color | red | Colour of the spoken word (#RRGGBB); for pop, the word colour |
sub-emoji |
boolean | "false" |
Show a matching emoji above captions that mention a keyword (money π°, fire π₯, love β€οΈ, cat π±, β¦) |
sub-mode |
enum | word chunks | "line" puts one source line per subtitle (verse) |
sub-font-size |
number | 48 |
Subtitle font size in pixels |
sub-weight |
enum | bold | "regular" or "bold" |
sub-margin-l |
number | 20 |
Left edge of the text column, in 1920-wide pixels |
sub-margin-r |
number | 20 |
Right inset of the text column |
sub-margin-v |
number | 30 |
Vertical inset (ignored for center positions) |
sub-page-lines |
number | 0 |
>1 makes lines accumulate, then clear together |
sub-line-height |
number | 1.55x size | Row spacing in paged mode |
sub-fade |
number | 0 |
Fade-in for each line, in milliseconds |
sub-page-hold |
time | 0 |
Beat on a full page before it clears |
sub-final-hold |
time | 0 |
How long the last page stays on screen after the narration ends |
font |
string | default font | Subtitle font file |
instruct |
string | none | Delivery direction for mimo_tts, e.g. "Tense and hushed, like a ghost story". Not read aloud. qwen3_tts accepts it too, but in our tests it had no reliable effect |
audio-file |
string | none | With engine="upload_audio": your own recording to use instead of TTS |
subtitle-file |
string | none | With engine="upload_audio": an SRT file with the timing for your recording. Without it, the text inside the tag is spread across the recording |
7 (top-left) 8 (top-center) 9 (top-right)
4 (middle-left) 5 (middle-center) 6 (middle-right)
1 (bottom-left) 2 (bottom-center) 3 (bottom-right)By default subtitles are grouped into roughly three-word chunks, set bold, centred,
and near the frame edges, the short-form caption look. The sub-* attributes above
retune that without affecting any script that leaves them unset.
By default each line replaces the last. sub-page-lines instead lets them
accumulate: every line fades in beneath the previous one until the page is full,
then the whole block clears at once and the next page starts empty.
[tts ... sub-mode="line" sub-page-lines="6" sub-fade="450" sub-page-hold="0.8s"]The block is positioned for its full height from the first line, so lines are added
beneath one another without the earlier ones shifting up. A final short page centres
itself. Six lines at sub-font-size="42" fills the space between the plate's ornament
rules; raise sub-page-lines and lower sub-line-height together if you want more.
sub-mode="line" is the important one for verse: instead of chunking by word count
it puts one line of your script on screen at a time, so a poem's lineation
survives to the video. Combine it with the margins to confine the text to a column:
[tts engine="mimo_tts_voicedesign" voice="..." sub-mode="line"
sub-position="center" sub-align="right" font="noto-serif"
sub-font-size="42" sub-weight="regular"
sub-margin-l="860" sub-margin-r="88" highlight="false"]
If you can keep your head when all about you
Are losing theirs and blaming it on you,
[/tts]That reserves the left 860px of the frame for a portrait and sets the verse in the column beside it. See The Plate Format.
| Engine | voice is⦠|
Use for |
|---|---|---|
kokoro_tts |
a preset voice ID (af_heart, am_michael, β¦) |
the default for most videos |
qwen3_tts |
a preset voice ID | non-English and multilingual voices |
mimo_tts |
a preset voice ID | expressive English/Chinese with instruct style control |
mimo_tts_voicedesign |
a free-form description of the speaker | literary, cinematic, and dramatic narration |
This engine has no preset voices. The voice attribute is a description of the speaker in
English, and the model builds that voice. Following Xiaomi's guidance:
The house narrator voice, used by every script in poem_video_examples/:
An aristocratic elderly male in his 80s, speaking with a resonant, dark British accent that
carries a subtle, textured gravel. The voice is deep but perfectly clear and legible,
maintaining a slow, dramatic pace as if an elegant, calculating villain is speaking directly
to you.[tts] tag in the script, character for character.
A reworded description on one block produces a different voice and the cut is audible.instruct. The description already carries the delivery.font="roboto-black" and sub-position="center" for the cinematic look.[tts engine="kokoro_tts" voice="am_michael" sub-position="center" sub-align="center"]
Hello world! This text will be spoken and displayed.
[/tts]Background music track.
[music file="song.mp3" fit="loop"]
...children...
[/music]| Parameter | Type | Default | Description |
|---|---|---|---|
file |
string | required | Path to audio file |
id |
string | auto | Element ID for time anchoring |
from |
time | 0 |
Start position in the audio file |
volume |
number | 1.0 |
Volume level (0.0 = silent, 1.0 = normal, 2.0 = 2x) |
start |
anchor | none | When to start on timeline (see Time Anchors) |
end |
anchor | none | When to end on timeline |
fit |
enum | "loop" |
How to fit audio: none, trim, loop |
duck |
level | none | Lower the music by this much while narration plays, e.g. "-12db". It fades down just before speech and back up after. |
fade-in |
time | 0 |
Fade up from silence over this long at the start, e.g. "2s" |
fade-out |
time | 0 |
Fade down to silence over this long, ending where the music ends (the end of what it wraps, or the file's end when self-closing) |
Music that fades in and out around the narration:
[music file="song.mp3" volume="0.4" fade-in="2s" fade-out="3s"]
[tts]Once upon a time...[/tts]
[/music]Music that ducks under the narration:
[music file="song.mp3" volume="0.6" duck="-12db"/]Basic looping music:
[music file="background.mp3" fit="loop"]
[tts]Music plays throughout this narration[/tts]
[tts]And continues through this one too[/tts]
[/music]Skip intro and start from chorus (at 45 seconds):
[music file="song.mp3" from="45s" fit="loop"]
[tts]Music starts at the chorus and loops from there[/tts]
[/music]Quiet background music at 30% volume:
[music file="ambient.mp3" volume="0.3" fit="loop"]
[tts]The music is subtle behind the narration[/tts]
[/music]Reddit-style cards. Each card is narrated like a [tts] block, and its text is revealed line by line on screen as it is read. Put them inside a background [video] or [image], one [social-post] followed by any number of [social-comment] tags. This is what the Reddit Stories template produces.
[video file="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
[social-post title="AITA for labelling my coworker's lunch?" subtitle="r/AmItheAsshole" author="u/tupperware_tired" pfp="preset:red" engine="kokoro_tts" voice="af_heart"]
Every Monday I meal prep five lunches and put them in the office fridge.
Three weeks in a row, my Wednesday lunch vanished.
[/social-post]
[social-comment author="u/fridge_justice" pfp="preset:blue" engine="kokoro_tts" voice="am_michael"]
NTA. You fixed the containers.
[/social-comment]
[/video]| Parameter | Tags | Default | Description |
|---|---|---|---|
title |
post | none | Post title, shown in large text at the top of the card |
subtitle |
post | none | Subreddit or community name, e.g. r/AskReddit |
author |
both | none | Username shown on the card |
pfp |
both | none | Profile picture: a file, or a built-in avatar preset:red, preset:blue, preset:green, preset:yellow, preset:orange, preset:purple, preset:cyan, preset:light_blue |
narrate-title |
post | "true" |
Read the title aloud before the body |
engine / voice / instruct |
both | config default | Voice for this card, as for [tts]. Each card can have its own voice |
subtitles |
both | "false" |
Also show subtitles (the card text is usually enough) |
highlight |
both | "false" |
Highlight the current word |
A quiz question card with a small label, the question, and up to four option boxes (AβD), drawn over the background like the social cards. The narration inside it normally has subtitles="false", since the card shows the text. A round countdown timer appears in the middle of the frame, then the answer is revealed: the correct option turns green and the others fade. Use it for trivia, multiple-choice and "would you rather" videos.
[quiz-card question="..." options="A|B|C" answer="2" countdown="5"]
[tts subtitles="false"]The question, read aloud.[/tts]
[pause duration="5s"/]
[tts subtitles="false"]The answer is B.[/tts]
[/quiz-card]| Parameter | Type | Default | Description |
|---|---|---|---|
question |
string | "" |
Question shown in bold on the card |
options |
string | "" |
Up to 4 options separated by | |
answer |
number | 0 |
1-based index of the correct option; 0 = none (e.g. would you rather) |
countdown |
time | 5 |
Length of the countdown timer, in seconds |
label |
string | "" |
Small heading above the question, e.g. Question 2 of 5 or Would you rather |
reveal |
string | "" |
Text shown in a green box at the reveal (the answer for open questions, or a follow-up line) |
theme |
string | "dark" |
dark (matches the Reddit card) or light |
card-color |
hex | theme | Card and timer background |
text-color |
hex | theme | Question and option text |
accent-color |
hex | theme | Label, letter badges and timer ring |
option-color |
hex | theme | Option box background |
correct-color |
hex | green | Correct option and reveal box |
id |
string | auto | Element ID |
Attribute text can't contain double quotes, and options can't contain |.
Custom colours are applied on top of theme; borders, faded options and the timer track are blended from them. Example: theme="dark" card-color="#1e1b4b" accent-color="#f59e0b".
[tts] inside the card ends (the question has been read) and runs for countdown seconds.[pause] as long as the countdown right after the question so the timer has time to run. Ticks go inside it: [sound file="sfx/tick.mp3"/][pause duration="1s"/] once per second.[sound file="sfx/ding.mp3"/] right after the countdown marks the reveal.[pause duration="1s"/] so the revealed card is seen before the next one.Multiple choice, with ticks and a chime:
[video file="minecraft_1.mp4" size="content" fit="loop"]
[quiz-card label="Question 1 of 3" question="What is the capital of Australia?" options="Sydney|Canberra|Melbourne" answer="2" countdown="3"]
[tts subtitles="false"]What is the capital of Australia? Sydney, Canberra, or Melbourne?[/tts]
[sound file="sfx/tick.mp3"/][pause duration="1s"/]
[sound file="sfx/tick.mp3"/][pause duration="1s"/]
[sound file="sfx/tick.mp3"/][pause duration="1s"/]
[sound file="sfx/ding.mp3"/]
[tts subtitles="false"]The answer is Canberra.[/tts]
[pause duration="1s"/]
[/quiz-card]
[/video]Open question (answer shown in the reveal box):
[quiz-card label="Question 2 of 3" question="How many hearts does an octopus have?" reveal="Three" countdown="5"]
[tts subtitles="false"]How many hearts does an octopus have?[/tts]
[pause duration="5s"/]
[tts subtitles="false"]The answer is three.[/tts]
[pause duration="1s"/]
[/quiz-card]Would you rather (no correct answer):
[quiz-card label="Would you rather" options="Be able to fly|Be invisible" reveal="Plot twist: only one foot off the ground" countdown="5"]
[tts subtitles="false"]Would you rather be able to fly, or be invisible?[/tts]
[pause duration="5s"/]
[tts subtitles="false"]Plot twist: you can only fly one foot off the ground.[/tts]
[pause duration="1s"/]
[/quiz-card]These tags insert media at specific points.
Display an image overlay with positioning, sizing, and opacity controls.
Self-closing (inline in TTS):
[image file="photo.png" duration="2.0"/]Wrapping (with children):
[image file="photo.png"]
[tts]Text spoken while image displays[/tts]
[/image]| Parameter | Type | Default | Description |
|---|---|---|---|
file |
string | required | Path to image file |
id |
string | auto | Element ID |
duration |
time | 1.0 |
Display duration (self-closing only) |
position |
enum | "center" |
Preset position (see below) |
x |
dimension | none | Custom X position (overrides position) |
y |
dimension | none | Custom Y position (overrides position) |
width |
dimension | none | Target width |
height |
dimension | none | Target height |
scale |
number | 1.0 |
Scale factor (e.g., 0.5 = half size) |
fit |
enum | "contain" |
How image fits target size |
opacity |
number | 1.0 |
Transparency (0.0 = invisible, 1.0 = opaque) |
Dimensions can be specified as:
"200px" or "200""50%" (relative to frame width/height)| Value | Location |
|---|---|
center |
Dead center (default) |
top |
Top center |
bottom |
Bottom center |
left |
Left center |
right |
Right center |
top-left |
Top left corner |
top-right |
Top right corner |
bottom-left |
Bottom left corner |
bottom-right |
Bottom right corner |
| Value | Behavior |
|---|---|
contain |
Scale to fit within target, preserve aspect ratio (default) |
cover |
Scale to cover target, may crop |
fill |
Stretch to fill target exactly |
none |
Use original size (only scale applies) |
duration secondsBasic centered image:
[image file="logo.png" duration="3s"/]Corner watermark at 50% opacity:
[image file="watermark.png" position="bottom-right" scale="0.3" opacity="0.5"/]Centered with 10% padding on all sides:
[image file="photo.png" width="80%" height="80%" fit="contain"/]Custom position:
[image file="icon.png" x="100px" y="50%"/]Cover a region (crops to fill):
[image file="background.jpg" width="50%" height="50%" fit="cover" position="top-left"/]Inline in TTS:
[tts]
Look at this [image file="cat.png" duration="3s" position="center" scale="0.8"/] cute cat!
[/tts]Generate a still image at render time from a text prompt, using an AI image engine. Resolves to an [image] at render time, so it can be a base background layer or an overlay just like [image]. Add a [zoom] inside it for cinematic motion.
Wrapping (base layer for a scene):
[ai-image prompt="misty mountain valley at sunrise" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="cover"]
[zoom from="100%" to="115%" easing="ease-in-out"]
[tts]Narration for this scene[/tts]
[/zoom]
[/ai-image]Self-closing (swap background mid-narration, inside TTS):
[ai-image prompt="close-up street level, neon lights" fallback="video/minecraft/BXUA2FncVPI.mp4" duration="4s"/]| Attribute | Type | Default | Description |
|---|---|---|---|
prompt |
string | required | Text description of the image to generate |
fallback |
string | recommended | Library file path used if generation fails (e.g. out of credits or moderation block). Must be an exact library path, with no spaces, no display names |
fallback-prompt |
string | none | Alternate prompt tried if the main prompt fails |
fallback-prompt-2 |
string | none | Second alternate prompt tried if both above fail |
engine |
string | "flux-pro" |
Force a specific engine. Leave unset to auto-try the chain: flux-klein β flux-pro β sdxl-turbo |
id |
string | auto | Element ID for time anchoring |
duration |
time | 5.0 |
Display duration (self-closing only) |
size |
enum | "content" |
Duration mode: content or natural |
position |
enum | "center" |
Preset position (same presets as [image]) |
fit |
enum | "cover" |
How the image fits the frame: contain, cover, fill, none |
width |
dimension | none | Target width (px or %) |
height |
dimension | none | Target height (px or %) |
fallback. If AI generation fails and no fallback is set, rendering fails. The fallback must be an exact path from the asset library.[zoom] for visual motion, and alternate zoom direction between consecutive images (in, then out) for a natural rhythm.Scene-by-scene backgrounds, alternating zoom direction:
[ai-image prompt="misty mountain valley at sunrise, cinematic, warm tones" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="cover"]
[zoom from="100%" to="122%" easing="ease-in-out"]
[tts]First scene zooms in.[/tts]
[/zoom]
[/ai-image]
[ai-image prompt="dense pine forest path, cinematic, warm tones" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="cover"]
[zoom from="122%" to="100%" easing="ease-in-out"]
[tts]Next scene zooms out in the opposite direction.[/tts]
[/zoom]
[/ai-image]Swap the background mid-narration without splitting the audio:
[ai-image prompt="establishing wide shot, city skyline" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="cover"]
[zoom from="100%" to="110%" easing="ease-in-out"]
[tts]
Opening narration over the first image.
[ai-image prompt="close-up street level, neon lights" fallback="video/minecraft/BXUA2FncVPI.mp4"/]
Background swaps here while narration continues uninterrupted.
[/tts]
[/zoom]
[/ai-image]Force a specific engine:
[ai-image prompt="watercolor koi pond" fallback="video/minecraft/BXUA2FncVPI.mp4" engine="sdxl-turbo" fit="cover"/]Generate a real video clip at render time from a text prompt, using an AI video engine. Resolves to a [video] at render time. Use only when genuine motion is essential (flowing water, fire, crowds, weather, action); for static or atmospheric scenes prefer [ai-image] with a [zoom] instead, since it is faster and far cheaper.
[ai-video prompt="time lapse of clouds moving over mountains" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
[tts]Narration for this scene[/tts]
[/ai-video]| Attribute | Type | Default | Description |
|---|---|---|---|
prompt |
string | required | Text description of the clip to generate |
fallback |
string | recommended | Library file path used if generation fails (e.g. out of credits or moderation block). Must be an exact library path, with no spaces, no display names |
fallback-prompt |
string | none | Alternate prompt tried if the main prompt fails |
fallback-prompt-2 |
string | none | Second alternate prompt tried if both above fail |
engine |
string | "veo" |
AI video engine to use |
id |
string | auto | Element ID for time anchoring |
size |
enum | "content" |
Duration mode: content or natural |
fit |
enum | "loop" |
How the clip fits the container duration: none, trim, loop |
fit="loop" to fill longer narration.fallback. If AI generation fails and no fallback is set, rendering fails. The fallback must be an exact path from the asset library.[ai-image], this tag has no position, width, height, or duration attributes.Motion scene that needs real video:
[ai-video prompt="crashing ocean waves against rocks, slow motion" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
[tts]The storm rolled in from the sea.[/tts]
[/ai-video]Mostly images with one video highlight:
[ai-image prompt="quiet harbor at dawn, cinematic" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="cover"]
[zoom from="100%" to="112%" easing="ease-in-out"]
[tts]The town was still asleep.[/tts]
[/zoom]
[/ai-image]
[ai-video prompt="flock of birds taking flight over the water" fallback="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
[tts]Then, all at once, the sky came alive.[/tts]
[/ai-video]Play a sound effect.
[sound file="explosion.wav"/]| Parameter | Type | Default | Description |
|---|---|---|---|
file |
string | required | Path to audio file |
id |
string | auto | Element ID |
fade-in |
time | 0 |
Fade up from silence over this long, e.g. "300ms" |
fade-out |
time | 0 |
Fade down to silence over this long, ending where the sound ends (for a [sound] that wraps content, the end of what it wraps) |
If fade-in and fade-out together are longer than the sound, they are shortened in proportion.
[tts]
The door slammed [sound file="slam.wav"/] shut.
[/tts]Add silence/time without speech. Useful for letting video/music continue after TTS ends.
[pause duration="3s"/]| Parameter | Type | Default | Description |
|---|---|---|---|
duration |
time | 1.0 |
Duration of the pause |
id |
string | auto | Element ID (for anchoring) |
Add 3 seconds after narration:
[video file="bg.mp4" fit="loop"]
[music file="song.mp3" fit="loop"]
[tts]Here's the content[/tts]
[pause duration="3s"/]
[/music]
[/video]Use as anchor point:
[tts id="narration"]Main content[/tts]
[pause duration="2s" id="outro"/]
[music file="outro.mp3" start="outro:start"/]Style tags modify subtitle appearance. They only work inside [tts] blocks.
Generic styling tag with full control.
[style color="#FF0000" bold="true" italic="false" underline="false" size="48"]
styled text
[/style]| Parameter | Type | Default | Description |
|---|---|---|---|
color |
hex color | none | Text color (#RRGGBB or #RGB) |
bold |
boolean | "false" |
Bold text |
italic |
boolean | "false" |
Italic text |
underline |
boolean | "false" |
Underlined text |
size |
number | none | Font size in pixels |
[tts]
This is [style color="#FF6600" bold="true"]orange and bold[/style] text.
[/tts]Convenience tags for common colors.
[red]This text is red[/red][blue]This text is blue[/blue][green]This text is green[/green]Styles can be nested. Inner styles override outer styles:
[tts]
[blue]Blue text with [red]red word[/red] inside[/blue]
[/tts]Effect tags add animated camera movements to video/image content. They can be used in two ways:
Ken Burns style zoom effect. Creates smooth zoom in or zoom out animation.
[zoom from="100%" to="120%" easing="ease-out" focus-x="50%" focus-y="50%"]
...children...
[/zoom]| Parameter | Type | Default | Description |
|---|---|---|---|
from |
percentage | "100%" |
Starting zoom level |
to |
percentage | "120%" |
Ending zoom level |
easing |
enum | "linear" |
Animation curve (see below) |
focus-x |
percentage | "50%" |
Horizontal focus point |
focus-y |
percentage | "50%" |
Vertical focus point |
id |
string | auto | Element ID |
| Value | Behavior |
|---|---|
linear |
Constant speed (default) |
ease-in |
Start slow, accelerate |
ease-out |
Start fast, decelerate |
ease-in-out |
Slow at both ends |
Subtle zoom in on image:
[image file="photo.png"]
[zoom from="100%" to="110%"]
[tts]Look at this beautiful scenery[/tts]
[/zoom]
[/image]Zoom out with focus on top-left:
[zoom from="150%" to="100%" focus-x="25%" focus-y="25%" easing="ease-out"]
[tts]Revealing the full picture[/tts]
[/zoom]Inline zoom for emphasis:
[tts]
This is normal, but [zoom from="100%" to="120%"]this part is important[/zoom] and back to normal.
[/tts]Horizontal camera movement. Scales up content 1.5x to provide room for movement.
[pan from="left" to="right" easing="linear"]
...children...
[/pan]| Parameter | Type | Default | Description |
|---|---|---|---|
from |
position | "0%" |
Starting horizontal position |
to |
position | "100%" |
Ending horizontal position |
easing |
enum | "linear" |
Animation curve |
id |
string | auto | Element ID |
| Keyword | Percentage |
|---|---|
left |
0% |
center |
50% |
right |
100% |
Or use any percentage value like "25%", "75%", etc.
Pan from left to right:
[pan from="left" to="right"]
[tts]Panning across the landscape[/tts]
[/pan]Pan from center to right with easing:
[pan from="center" to="right" easing="ease-in-out"]
[tts]Following the action[/tts]
[/pan]Vertical camera movement. Scales up content 1.5x to provide room for movement.
[tilt from="top" to="bottom" easing="linear"]
...children...
[/tilt]| Parameter | Type | Default | Description |
|---|---|---|---|
from |
position | "0%" |
Starting vertical position |
to |
position | "100%" |
Ending vertical position |
easing |
enum | "linear" |
Animation curve |
id |
string | auto | Element ID |
| Keyword | Percentage |
|---|---|
top |
0% |
center |
50% |
bottom |
100% |
Tilt from top to bottom:
[tilt from="top" to="bottom"]
[tts]Scanning down the building[/tts]
[/tilt]Tilt up with ease-out:
[tilt from="bottom" to="top" easing="ease-out"]
[tts]Looking up at the sky[/tts]
[/tilt]Camera shake effect for impact, tension, or emphasis. Uses oscillating motion with optional decay.
Wrapping form:
[shake intensity="5" frequency="30" decay="true"]
...children...
[/shake]Self-closing form:
[shake intensity="10" frequency="20" duration="0.5s"/]| Parameter | Type | Default | Description |
|---|---|---|---|
intensity |
number | 5.0 |
Shake magnitude in pixels |
frequency |
number | 30.0 |
Oscillation frequency (Hz) |
decay |
boolean | "true" |
Shake diminishes over time |
duration |
time | 0.5s |
Duration (self-closing only) |
id |
string | auto | Element ID |
Self-closing shake during impact (inline in TTS):
[tts]
And then [shake intensity="8" duration="0.3s"/] the explosion hit!
[/tts]The shake starts exactly when "the" is spoken and lasts 0.3 seconds.
Wrapping shake timed to words (inline in TTS):
[tts]
Stay calm during [shake intensity="3"]the earthquake[/shake] warning.
[/tts]The shake spans from when "the" is spoken until "earthquake" finishes.
Wrapping shake around TTS:
[shake intensity="6" frequency="25" decay="true"]
[tts]The earthquake rumbled beneath us[/tts]
[/shake]Intense persistent shake:
[shake intensity="12" frequency="40" decay="false"]
[tts]The machine vibrated violently[/tts]
[/shake]A colour look on the footage and images: black and white, warm, vintage and so on. Captions, on-screen text and cards are drawn on top and keep their colours. Looks are free.
Wrapping form (the look covers what's inside):
[filter look="bw"]
...children...
[/filter]Self-closing form (from here to the end, or for duration):
[filter look="warm"/]
[filter look="vintage" duration="3s"/]| Parameter | Type | Default | Description |
|---|---|---|---|
look |
string | required | bw, noir, sepia, vintage, warm, cool, vivid, faded, dark |
duration |
time | to the end | How long a self-closing look lasts |
id |
string | auto | Element ID |
A flashback in black and white:
[video file="story.mp4"]
[tts]We were happy back then.[/tts]
[filter look="bw"][tts]It was the summer of 1999.[/tts][/filter]
[tts]Then everything changed.[/tts]
[/video]A few words, inside narration:
[tts]It all went [filter look="noir"]dark and cold[/filter] after that.[/tts]A second video with the main picture: stacked (split screen, like gameplay under a story) or as a small inset in a corner (picture-in-picture). The second video is muted and loops. Captions sit in the middle by default, which is the seam of a 50/50 split.
Wrapping form (for what's inside):
[split file="parkour.mp4"]
...children...
[/split]Self-closing form (from here to the end, or for duration):
[split file="parkour.mp4"/]
[split file="facecam.mp4" mode="pip" position="top-right" size="0.3"/]| Parameter | Type | Default | Description |
|---|---|---|---|
file |
string | required | The second video |
mode |
string | split |
split (stacked) or pip (corner inset) |
position |
string | bottom / bottom-right |
split: top or bottom; pip: top-left, top-right, bottom-left, bottom-right |
ratio |
number | 0.5 |
split: the main picture's share of the height (0.25β0.75) |
size |
number | 0.35 |
pip: the inset's share of the width (0.15β0.6) |
duration |
time | to the end | How long a self-closing split lasts |
id |
string | auto | Element ID |
[video file="story-background.mp4" size="content" fit="loop"]
[split file="minecraft-parkour.mp4" ratio="0.5"/]
[tts]Let me tell you about the worst day of my life.[/tts]
[/video]How the next [video] replaces the one before it. Put it between the two clips; it takes no time of its own and does not change the video's length (the new clip starts duration early and animates in over the end of the previous one).
[video file="video/a.mp4" size="content" fit="loop"][tts]First part.[/tts][/video]
[transition type="crossfade" duration="0.6s"/]
[video file="video/b.mp4" size="content" fit="loop"][tts]Second part.[/tts][/video]| Parameter | Type | Default | Description |
|---|---|---|---|
type |
enum | "crossfade" |
crossfade, fade (dip to black), slide-left, slide-right, slide-up, slide-down, wipe-left, wipe-right, wipe-up, wipe-down |
duration |
time | 0.5s |
Between 0.05s and 3s |
slide-* moves the new clip in (slide-left enters from the right edge, moving left); wipe-* keeps both clips still and sweeps an edge across in that direction.
On-screen text, such as a title, a list number or a label. Wrap narration to show it while those words are spoken, or use it self-closing with a duration. Inside [tts], a self-closing [text/] appears on the word where it is written.
[text value="Fact #1" position="top" background="#00000099"]
[tts]Octopuses have three hearts.[/tts]
[/text]
[tts]And the best part [text value="π 3 hearts" duration="2s"/] is how they use them.[/tts]| Parameter | Type | Default | Description |
|---|---|---|---|
value |
string | required | The text. Long lines wrap; \n forces a line break. |
position |
enum | "top" |
top, bottom, center, left, right, top-left, top-right, bottom-left, bottom-right. bottom stays above the captions. |
x, y |
px or % | none | Exact position instead of position |
size |
number | 72 |
Font size in pixels at 1080 px wide (scaled for other sizes) |
color |
color | "#FFFFFF" |
#RRGGBB, #RRGGBBAA or a basic name (white, yellow, red, β¦) |
background |
color | none | Box behind the text, e.g. "#00000099" |
font |
string | arial |
arial, noto-sans, noto-serif, roboto-black, verdana-italic, felt-condolences |
duration |
time | 3s |
Self-closing only; wrapping text lasts as long as its content |
fade |
time | 0.2s |
Fade in and out |
Emoji are not drawn in [text]; use them in captions (see sub-style on tts).
A thin bar that fills from left to right over the whole video. It helps viewers see how much is left, which keeps them watching short videos. Put it anywhere, once.
[progress-bar color="#FFD60A" height="12" position="bottom"/]| Parameter | Type | Default | Description |
|---|---|---|---|
color |
color | "#FFFFFF" |
Bar colour |
height |
number | 10 |
Pixels at 1920 px tall (scaled for other sizes) |
position |
enum | "bottom" |
top or bottom |
opacity |
number | 0.9 |
0β1 |
Marks the start of a chapter. Chapters are added to the description when the video is published to YouTube (as 00:00 Title lines), and are returned in the video.completed webhook. Inside [tts] it marks the word where it is written.
[chapter title="Intro"/]
[tts]Welcome back.[/tts]
[chapter title="The science"/]
[tts]Here is how it worksβ¦[/tts]YouTube shows chapters only when there are at least three, the first is at 0:00 and each is at least 10 seconds long. InkSlop adds a 0:00 "Intro" if needed and merges chapters that are too short; if fewer than three are left, no chapters are added.
Effects can be combined by nesting them:
[zoom from="100%" to="115%" easing="ease-in"]
[pan from="left" to="center"]
[tts]A dramatic reveal with zoom and pan[/tts]
[/pan]
[/zoom]Effects apply from inside out - the innermost effect is applied first, then outer effects transform the result.
The timing system is content-driven, not timestamp-driven.
Speech drives timing. Media reacts to content, not seconds.
Elements inside a container are processed sequentially:
[video file="bg.mp4"]
[tts]First narration[/tts] # 0.0s - 2.5s
[tts]Second narration[/tts] # 2.5s - 5.0s
[tts]Third narration[/tts] # 5.0s - 8.0s
[/video]Media inside TTS is positioned by word location, not timestamps:
[tts]
Hello [image file="wave.png" duration="1"/] world!
[/tts]The image appears when "world" is spoken, calculated from TTS word timings.
Effects can also be placed inside TTS blocks to get word-level timing:
Self-closing effects (like shake with duration) start at their text position:
[tts]
And then [shake intensity="5" duration="0.3s"/] the explosion hit!
[/tts]The shake effect starts exactly when "the" would be spoken (between "then" and "the").
Wrapping effects span the duration of the wrapped text:
[tts]
Hello world, [zoom from="100%" to="120%"]this is important[/zoom] okay bye
[/tts]The zoom effect starts when "this" is spoken and ends when "important" finishes.
This allows precise synchronization of camera effects with specific words:
[tts]
Stay calm during [shake intensity="3"]the earthquake[/shake] warning.
Now [pan from="left" to="right"]look across the horizon[/pan] slowly.
[/tts]Reference other elements' timing using anchors:
element_id:start # When element starts
element_id:end # When element ends
element_id:start+2s # 2 seconds after element starts
element_id:end-0.5s # 0.5 seconds before element ends[tts id="narration"]The main content[/tts]
[music file="outro.mp3" start="narration:end-1s"]
# Music starts 1 second before narration ends
[/music]Container duration = sum of children durations
[video file="bg.mp4" size="content"]
[tts]3 seconds of speech[/tts] # Video is 3 seconds
[/video]Container uses its natural file duration
[video file="intro.mp4" size="natural"]
# Video plays full 10 seconds regardless of children
[/video]When container duration differs from file duration:
| Mode | Behavior |
|---|---|
none |
No adjustment (may have black frames or cut off) |
trim |
Cut file to fit duration |
loop |
Repeat file to fill duration |
[video file="nature.mp4" size="content" fit="loop"]
[tts engine="kokoro_tts" voice="af_heart"]
Welcome to our nature documentary.
Today we explore the forest.
[/tts]
[/video][video file="presentation.mp4"]
[tts sub-position="bottom" sub-align="center"]
The answer is [red]42[/red].
This is [style color="#FFD700" bold="true"]golden[/style] knowledge.
[/tts]
[/video][video file="action.mp4"]
[tts voice="narrator"]
The hero approaches the door.
[sound file="footsteps.wav"/]
[/tts]
[tts voice="hero"]
[sound file="door_knock.wav"/]
Is anyone there?
[/tts]
[/video][video file="intro.mp4"]
[music file="ambient.mp3" fit="loop"]
[tts]
In the beginning, there was silence.
[/tts]
[tts]
Then came the music.
[/tts]
[/music]
[/video][video file="lecture.mp4"]
[tts]
Let me show you a diagram.
[image file="diagram.png" duration="5"/]
As you can see, the process flows left to right.
[/tts]
[/video][video file="scene.mp4"]
[tts id="main"]
The main story content goes here.
It could be quite long.
[/tts]
[music file="credits.mp3" start="main:end-2s" fit="trim"]
[tts]
Thanks for watching.
[/tts]
[/music]
[/video]The highest-quality structure this tool produces. Use it when the source text deserves to be
treated as writing rather than as a caption: poems, monologues, scripture, myth, folklore,
history, horror. Do not use it for explainers, listicles, or gameplay commentary; those want
one [tts] over one background.
Working examples of every rule below live in poem_video_examples/.
One [music] wraps the whole script. Inside it, one scene per stanza or beat. Each scene is an
[ai-image] for that beat, a camera move wrapping its [tts], and a short held beat after the
narration so the move lands before the cut.
# resolution: 16:9
# Title: Author (year, public domain)
# STYLE SUFFIX (repeated verbatim on every prompt):
# gothic engraving rendered as oil painting, single-candle chiaroscuro,
# ink-black and deep umber with cold blue window light, ominous, high contrast, cinematic
[music file="music/Ghost_Story.opus" volume="0.20" fit="loop"]
# Scene 1: the scholar over his books. Slow push in.
# (one line, wrapped here only so it fits the page; see rule 9)
[ai-image prompt="A weary scholar slumped over ancient books in a cramped midnight study, one guttering candle, wide shot, <STYLE SUFFIX>" fallback-prompt="An old study at midnight, a single candle over stacked leather books, <STYLE SUFFIX>" fallback-prompt-2="A single candle burning on a stack of old books in a dark room" fallback="video/satisfying/e1emEay0ink.mp4" size="content" fit="cover"]
[zoom from="100%" to="114%" easing="ease-in-out"]
[tts engine="mimo_tts_voicedesign" voice="<full voice description>" sub-position="bottom" font="roboto-black"]
Once upon a midnight dreary, while I pondered, weak and weary,
Over many a quaint and curious volume of forgotten loreβ
[/tts]
[pause duration="0.6s"/]
[/zoom]
[/ai-image]
# ... one more scene per stanza, alternating the camera move ...
[pause duration="3s"/]
[/music]1. One image per beat, never per line. A stanza, a sentence group, or a single hammer-blow line that earns its own frame: two to five lines of narration per scene. Six or more scenes reads as a film; two or three reads as a slideshow.
2. Write the style suffix once, in a comment at the top, then paste it onto every prompt unchanged. It must lock art style, palette, light, and mood. Naming a specific medium (oil painting, engraving, tenebrist portrait) holds far better across scenes than naming a genre.
3. Never repeat a camera move back to back. Cycle push-in β pull-out β pan β push-in β tilt
β pull-out. Zooms carry the ordinary beats; a [pan] is for something the eye travels across (a
tide, a corridor, a horizon); a [tilt] is for a reveal above or below the frame. Keep the
amplitude modest: 100%β114%, or 122%β100%. In 9:16 keep it gentler still, because a
tall crop magnifies motion.
4. Aim the push. On a close-up, set focus-x / focus-y on the thing being pushed onto:
an eye sits around focus-y="42%", not at the frame centre. A push that drifts off its subject
is worse than no push at all.
5. Shake only on violence. Inline, on the exact words that break, strike, or collapse:
[shake intensity="6" frequency="24" decay="true"]things fall apart[/shake]. Use decay="true"
for a single impact; leave it off for sustained menace. Two or three in a whole script; past
that it reads as a broken camera.
6. One colour accent per scene at most, and only on the text's own key word.
[style color="#a11f1f" bold="true"]blood-dimmed tide[/style]. Take the hex from the same
palette as the style suffix so the subtitle belongs to the picture. Accenting every other phrase
destroys the effect entirely.
7. Hold the beat. Put [pause duration="0.6s"/] inside the camera move, after [/tts].
Because wrapping effects take their duration from their children, the pause extends the move:
the image stays, the camera finishes travelling, and the line lands before the cut. Give the
closing line 1.2sβ1.4s, then a final [pause duration="3s"/] under the music.
8. Give every [ai-image] a two-step prompt fallback before the library fallback.
fallback-prompt is the same image in plainer words (keep the style suffix); fallback-prompt-2
is the simplest literal version with no style suffix at all, which is what survives a moderation
block. The library fallback video is a last resort that will look wrong in a cinematic piece,
so the job of the prompt chain is never to reach it.
9. Keep every opening tag on a single line, however long it gets. An [ai-image] carrying a
prompt plus two fallback prompts makes for a very long line, and that is expected. Never wrap a
tag's attributes onto continuation lines. The renderer's parser tolerates it, but the in-app DSL
editor validates tags line by line and will report a wrapped tag as unclosed, flagging every
matching [/ai-image] as an unexpected closing tag.
One track, wrapping everything, at volume="0.20"β"0.24" so it sits under the voice rather
than beside it. Public-domain classical (Beethoven, Chopin, Satie) needs no attribution at all
and suits this register; Kevin MacLeod tracks require CC BY attribution in the video description.
The long-form counterpart to the Cinematic scene formula, for reading a poem or passage in full. Everything that never moves (the author portrait, the ornament rules, the name block) is baked into one static PNG, and the verse is rendered over it a line at a time as subtitles.
Nothing is generated at render time except the voice. No [ai-image], no
[ai-video]. A plate video costs TTS plus render, which is what makes it viable for
long readings where a scene-per-stanza film would be expensive.
# resolution: 16:9
[music file="music/Chopin_Nocturne_Op9_No1.opus" volume="0.18" fit="loop"]
[image file="user/plates/kipling-plate.png" fit="cover"]
[tts engine="mimo_tts_voicedesign" voice="<full voice description>"
sub-mode="line" sub-position="center" sub-align="right"
font="noto-serif" sub-font-size="42" sub-weight="regular"
sub-margin-l="860" sub-margin-r="88" highlight="false"]
<the whole poem, one line per line>
[/tts]
[pause duration="3s"/]
[/image]
[/music]1. Build the plate with scripts/make_author_plate.py. It renders a 1920x1080
PNG: portrait faded into the black at the left, two ornament rules bracketing the
text area, and the author name and dates set bottom-right. Upload it and reference
the returned path.
2. The margins and the plate must agree. sub-margin-l is the left edge of the
text column in 1920-wide pixels, and it has to clear the portrait. The default pairing
is a portrait across the left ~45% and sub-margin-l="860".
3. Keep lines inside the column. At sub-font-size="42" in Noto Serif, about 58
characters fit between sub-margin-l and the right inset. A longer line wraps and
breaks the one-line-at-a-time effect.
4. sub-weight="regular" and highlight="false". The default subtitle face is
bold with a moving word highlight: right for captions, wrong for verse. Both are
needed for the calm engraved look.
5. Set the verse, not a paragraph. sub-mode="line" breaks on the newlines in
your script, so the script's lineation is what appears on screen. If your source has
been flattened to one line, re-line it first with _reline_verse() in
content_pipeline/dsl_generator.py does this from capitalisation and punctuation.
6. Let the verse accumulate. sub-page-lines="6" with sub-fade="450" fills the
bracketed space a line at a time and then clears, which reads as turning a page. One
line at a time on an otherwise empty frame reads as a caption instead.
7. One track, no camera move. The composition is still; a zoom would drift the portrait off its intended position. Public-domain classical suits the register and carries no attribution obligation.
The format is wired into the content pipeline, which reads the text from a CSV rather than having it retyped:
python scripts/content_pipeline/batch.py --preset poems --type plate --dry-rungenerate_plate_dsl(row, config) builds the script; the plate PNG comes from
plate_bg_map[author] in the preset.
The renderer organizes elements into layers:
| Layer | Elements | Behavior |
|---|---|---|
video |
VideoTag | Base visual layer |
image |
ImageTag | Overlaid on video |
effect |
ZoomTag, PanTag, TiltTag, ShakeTag | Visual effects applied to video/image |
primary |
TTSTag | Main audio + subtitles |
music |
MusicTag | Background audio |
sfx |
SoundTag | Sound effects |
Audio layers are mixed together. Video/image layers are composited. Effects are applied as FFmpeg filters.
The following features from the design spec are not yet implemented:
[meta] - Global project settings[scene] - Scene grouping[bg] - Background (use [video] instead)[emotion] - Emotional TTS modifiers for part of a narration (for a whole [tts] block, use its instruct parameter with mimo_tts)[filter] - Visual filters[fade] - Fade to or from black at the start or end of the video (between two clips, use [transition type="fade"])Built-in assets you can reference directly by file path. The full library is available in the video and music selectors on the create page.
Tip: right-click any asset in the selector
Right-clicking a video clip or music track gives you quick options to copy its file path (paste straight into your script), copy a direct link to the file, and copy the attribution text if the asset requires credit.
| Category | File path | Duration |
|---|---|---|
| Minecraft | video/minecraft/BXUA2FncVPI.mp4 | 10:30 |
| Subway Surfers | video/subway_surfers/i0M4ARe9v0Y.mp4 | 5:11 |
| Satisfying | video/satisfying/JvI-02Q69ms.mp4 | 3:00 |
| CS Surfing | video/cs_surfing/kuPPZCtLX4w.mp4 | 4:48 |
Browse all clips in the video selector on the create page.
| Name | File path | Mood |
|---|---|---|
Sneaky Snitch Kevin MacLeod | music/Sneaky_Snitch.opus | |
Scheming Weasel (faster version) Kevin MacLeod | music/Scheming_Weasel_(faster_version).opus | |
Monkeys Spinning Monkeys Kevin MacLeod | music/Monkeys_Spinning_Monkeys.opus | |
Fluffing a Duck Kevin MacLeod | music/Fluffing_a_Duck.opus |
Browse all tracks in the music selector on the create page.
How to use assets
Video: [video file="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
Music: [music file="music/Sneaky_Snitch.opus" volume="0.3" fit="loop"]
More from InkSlop