Skip to content

Typography on video

Captions carry what is said; the other text on a video is written for it — the title that opens a reel, the name and role of the person speaking, a price, a line pulled from the interview, the call to action at the end. Text on Video (video/text_overlay) draws that text into the picture with the fonts you choose, where you choose, for as long as you choose, locally and at no credit cost. It shares its styling, the Text Style (typography, a madoo.video-typography/v1 object), with the captions of Video Subtitles: one brand style can dress both.

System fonts are available to every workspace, in their regular, bold and italic variants and often more:

Category Families
Sans-serif Inter, Lato, Montserrat, Nunito, Open Sans, Oswald, Poppins, Raleway, Roboto, Work Sans
Serif Libre Baskerville, Lora, Merriweather, Noto Serif, Playfair Display, PT Serif
Display Abril Fatface, Bebas Neue
Handwriting Caveat, Dancing Script, Pacifico, Permanent Marker
Monospace Fira Code, Roboto Mono, Source Code Pro

A workspace’s own fonts — a brand typeface, a licensed display face — are uploaded once, in the editor or through the API, as one family with a TTF or OTF file per variant; the regular variant is required:

Terminal window
curl -s -X POST "$BASE_URL/api/v1/fonts" \
-H "Authorization: Bearer $TOKEN" \
-F "displayName=Acme Sans" -F "category=sans-serif" -F "licenseAcknowledged=true" \
-F "files=@AcmeSans-Regular.ttf" -F "files=@AcmeSans-Bold.ttf"

licenseAcknowledged records that the licence allows the workspace to use the font. Web formats, variable fonts and font collections are refused; a file may be up to 20 MB. A new file for the same family is a new revision: workflows that pinned the earlier one keep it. The assets API has the details.

A workflow pins the exact font it uses. The font of a Text Style is a madoo.font-reference/v1: the family, the variant, the weight and style, the version and the fingerprint of its files. A render draws exactly those letters, next year as today, even after the family gets a new revision or is archived for new work. There is no need to write a reference by hand: the editor fills it in, and list_fonts returns, for every variant, a ready reference to paste into the style as it is — in the contract’s own field names (familyGuid, versionGuid). Without a query, list_fonts lists the families and their variants, to browse; ask the family you choose with query and its variants come with their references (the references of every font at once would not fit in one answer). Through REST, GET /api/v1/fonts returns the same reference with snake_case field names, like the rest of the API: write them as familyGuid and versionGuid in a style.

Every character is checked before anything is drawn. If the chosen font has no glyph for a character of the text — Cyrillic, Greek or Arabic in a Latin-only font, an emoji, a rare symbol — the step stops and names the character (FONT_GLYPH_COVERAGE_INSUFFICIENT) instead of drawing empty boxes. Scripts that join or reorder their letters, such as Arabic, Hebrew or Devanagari, are rendered with complex shaping; they need a font that covers them, uploaded like any other.

In its simplest form Text on Video draws one Text (text), for the whole video or between Show Start and Show End (show_start, show_end, in seconds). The text can come from another step on the Text input — a product name from a spreadsheet, a headline written by a language model — and then it wins over the parameter.

For more than one text, Text Layers (text_track) lists them: each layer has an id, its text, a start and an end in seconds, and optionally its own typography. All the layers are drawn in one render pass — the video is encoded once, however many texts it carries — and each can differ in font, size, colour, place and motion:

{
"schema": "madoo.video-text-track/v1",
"events": [
{ "id": "title", "text": "We are going back", "start": 0, "end": 2.8, "typography": { "…": "…" } },
{ "id": "name", "text": "Reid Wiseman", "start": 0.6, "end": 5.6, "typography": { "…": "…" } }
]
}

The exact shape of a track is madoo.video-text-track/v1, and of a style madoo.video-typography/v1: read either with get_json_schema, or in the contracts appendix.

The layers can also arrive on the Timed Text Track input from an upstream step — a language model that writes the titles of a video from its transcript, a spreadsheet of prices with their timings — and then they replace both Text and Text Layers. A text on the Text input and a track together are refused (TEXT_TRACK_INPUT_XOR). The Text Style of the node styles a single text, and every layer that does not bring a typography of its own.

When the layout is fixed but some words come from other steps — the name of a place, a formatted price, a title an AI model wrote — write them as placeholders in the texts of Text Layers: {{place}}, da {{price}} EUR. Each distinct name becomes a text input of the node, named like the placeholder, and its value replaces the marker when the step runs. The inputs take text: a number — a price from Calculate, a count — goes through Format as Text (text/format) first, which writes it the way the language expects (485 → 485 € in Italian). The timings and styles stay in the track, and the video is still encoded once, where before every text from another step needed a Text on Video node of its own and an encode of the whole video. A name is a letter followed by letters, digits or _, at most 32 per node, and it cannot be the name of an input the node already has (video, text, text_track). Placeholders are filled only in the Text Layers parameter: a track that arrives on the Timed Text Track input is drawn as it is.

The same fields as for captions (see Typography: how captions look), with the ranges that matter when designing text:

Part Fields What to know
Size mode fixed (pixels) or relative (% of the video height), value, min, max The size of the letters, as in a design tool; relative reads the same at 720p and 4K. min/max bound it (8–500 px).
Text color, opacity, transform (none, uppercase, lowercase), letterSpacing (−10 to 50), lineSpacing (0.75 to 3) Condensed display faces in capitals often want a little letter spacing and tight line spacing.
Layout anchor (nine positions), alignment (left, center, right), offsetXPercent/offsetYPercent (−100 to 100), maxWidthPercent (10–100), maxLines (1–8), overflow (fit, clip, fail) The anchor puts the text in a corner, on an edge or in the centre of the safe area; the offsets move it by a share of the frame.
Safe area preset none, title-safe, social-portrait, custom; four margins (0–45%) social-portrait keeps text clear of the name, description and buttons a vertical feed draws over the video.
Legibility outline (width 0–20), shadow (offset, blur 0–20, opacity), background (a box: colour, opacity, padding 0–200) An outline for text over busy pictures, a shadow for a softer look, a box when the text must read on anything.
Motion entrance, exit (none, fade), durationMs (0–2000) A short fade — 200 to 350 ms — makes titles appear and leave without a jump.

The presets of the editor are starting points that set several fields at once: minimal (a thin outline and a soft shadow, no box, title-safe), social (a box inside the social-portrait safe area), high-contrast (a stronger outline and a darker box) and lower-third (left-aligned at the bottom left, 65% wide, on a box).

When the text does not fit its lines, fit makes it smaller down to min, clip drops the lines beyond maxLines, and fail stops the step with TEXT_OVERFLOW — the right choice when a text that does not fit must not be published smaller than the brand allows.

A Text on Video without a Text Style is drawn by an older renderer with a handful of parameters (font_size, font_color, position, shadow, margin) and the worker’s fonts; it remains for existing workflows. New workflows always pass a Text Style — the editor adds one to every new node.

  • A title card. Anchor top-center or center, a display face (Bebas Neue, Abril Fatface, Oswald) at 7–10% of the height, capitals with a little letter spacing, an outline and a shadow, a fade of 250 ms.
  • A lower third. Two layers anchored bottom-left and left-aligned — the name in a bold weight on a dark box, the role smaller on a coloured box — the name moved up by the height of the role (offsetYPercent about −5%), both inside the social safe area on a vertical video or title-safe on a landscape one.
  • A price or a figure. A heavy weight, fixed size so it never shrinks, overflow: fail, in a corner with a box in the brand colour.
  • A quote. A serif or an italic, centred a little below the middle, three lines at most, a soft shadow.
  • A call to action. Dark text on a bright box at the bottom of the safe area, in the last seconds, with a fade.
  • Text beside the picture, not over it. On a landscape video presented in a vertical frame, put the text in the bands — the letterbox lab works out the offsets.

The timing of the lab’s text layers over the 12.6 s reel: title, Bebas Neue, 0 to 2.8 s; name, Inter 700, and role, Inter, 0.6 to 5.6 s; quote, Montserrat 700 italic, 5.9 to 9.6 s; cta, Inter 700, 9.9 to 12.6 s

The vertical reel of the editing lab gets five text layers in one Text on Video node, with the system fonts every workspace has:

  • title, 0–2.8 s — “We are going back” in Bebas Neue at 8.5% of the height, capitals, an outline and a shadow, anchored at the top of the social safe area;
  • name and role, 0.6–5.6 s — a lower third: “Reid Wiseman” in Inter Bold capitals on a dark box, and under it “Commander · Artemis II” in Inter on a yellow box, both at the bottom left;
  • quote, 5.9–9.6 s — “Everybody is excited about this.” in Montserrat Bold Italic, centred below the middle, with a soft shadow and a slower fade;
  • cta, 9.9–12.6 s — “Follow the mission · nasa.gov/artemis” in Inter Bold capitals, dark on a yellow box, at the bottom.

Four moments of the titled reel: the title at 1.5 s with the lower third; the lower third alone at 3.5 s; the quote at 7.5 s; the call to action at 11.5 s

The reel is 720×1280 and 12.6 s, re-encoded at 540×960 for this page. One render pass, 0 credits.

Terminal window
node madoo-install-example.mjs lab-typography --api <your Madoo API URL> --key <key.json> --run --save titled

To dress your own video, replace the texts and timings of the layers; to use your brand’s typeface, upload it and put its reference from list_fonts in the font of each style.

Media: “Artemis Media Resource — Interview Soundbites”, NASA (public domain; NASA does not endorse this documentation). Music in the reel: “Local Forecast – Elevator” Kevin MacLeod (incompetech.com), licensed under Creative Commons: By Attribution 4.0 License, http://creativecommons.org/licenses/by/4.0/.

Code Meaning Fix
EMPTY_TEXT there is no text: the parameter is empty and nothing arrived on the Text input write the text, or connect the step that produces it
TEXT_TRACK_REQUIRES_TYPOGRAPHY (also at validation) the step has a text track but no Typography: a track is drawn only with a typography style set Typography (the editor fills in its default; through the API, copy the default from get_node_type), or use the single Text instead of a track
TEXT_TRACK_INPUT_XOR a text on the Text input and a track of layers together keep one of the two
TEXT_PLACEHOLDER_MISSING a layer uses {{name}} and nothing arrived on the input name; the message gives the layer, e.g. $.events[2].text connect a text to that input, or remove the placeholder
TEXT_PLACEHOLDER_NOT_TEXT a file other than text or JSON arrived on a placeholder input connect a text
TYPOGRAPHY_INVALID a Text Style or a layer is incomplete or out of range; the message names the field start from a preset in the editor, or fix the field
FONT_REFERENCE_INVALID the font is incomplete (a missing version, weight or fingerprint) take the reference from list_fonts
FONT_NOT_FOUND, FONT_VERSION_NOT_FOUND, FONT_VARIANT_NOT_FOUND the font, its version or its variant does not exist in the workspace or the system take the reference from list_fonts, or upload the font
FONT_MAPPING_REQUIRED the font the workflow pins is no longer available choose a replacement font for it in the editor
FONT_GLYPH_COVERAGE_INSUFFICIENT the font has no letter for a character of the text choose a font that covers the script, or upload one
TEXT_OVERFLOW a text does not fit its lines at the minimum size and overflow is fail allow more lines or a wider line, lower min, or shorten the text
MEDIA_TOO_LARGE, MEDIA_WORK_CAPACITY the video is over 2 GiB, or the worker is busy with other media cut long videos into segments; a busy worker is retried automatically