stingo

Getting started

Install it, render the example, and change one line to see what happens.

stingo turns a text file into an MP4. This page gets one rendering on your machine, then changes a line so you can see the loop it is built around.

#What you need

Bun 1.3 or newer and ffmpeg 6 or newer on your PATH.

ffmpeg is not optional — it decodes footage, mixes audio and encodes the video.

bun --version
ffmpeg -version | head -1

Bun is a hard requirement rather than a preference: the pipeline drives ffmpeg through Bun.spawn and reads files through Bun.file.

#Install it

bun add @hersidev/stingo

That gives you the library and two commands, stingo and stingo-mcp.

The package is scoped because npm refused the bare name — stingo is two letters from string, so it read as typosquatting. The commands are unaffected.

#Your first film

# film.yaml
title: It works
canvas: { preset: vertical, fps: 30 }
taste: bootdev

scenes:
  - block: title
    kicker: first render
    text: It works
    sub: That is the whole file.

  - block: stat
    value: 33ms
    label: per frame
bunx stingo render film.yaml

Fonts ship with the package, so there is nothing else to install. Every key above scenes: is optional — they are spelled out here so you can see where the shape, the frame rate and the look are set.

#Or run the example from a clone

The repository carries a five-minute example film and everything used to make the launch video, which is more interesting than a two-scene script:

git clone https://github.com/aynaash/stingo
cd stingo && bun install

bun stingo render examples/goroutines/video.yaml

That writes out/video.mp4 — about five minutes of vertical video, cut to a 128.9 BPM track. The first render is the slow one: fonts load, the syntax highlighter warms up, and every frame is drawn from scratch.

Add --draft while you are iterating. It drops the encoder to ultrafast and the quality to crf 30, which is unwatchable for publishing and perfectly fine for checking a layout.

#See the cut before you render it

Rendering thousands of frames to find out a scene is too short is a waste. plan resolves the whole timeline and prints it, rendering nothing:

bun stingo plan examples/goroutines/video.yaml
   0 title      0:00→0:04  4.17s (8.9 beats)   title-0
   1 statement  0:04→0:07  3.72s (8.0 beats)   statement-1
   2 stat       0:07→0:11  3.72s (8.0 beats)   stat-2

Every scene is a whole number of beats because the taste profile says cutOn: bar, so scene ends snap to the detected downbeat grid.

#Look at one frame

bun stingo still examples/goroutines/video.yaml --at 45 -o frame.png

A still goes through the same pixel path as a render — same fonts, same texture pass — so what you see is what the video will contain. This is the fastest way to check a layout.

#Change something

Open examples/goroutines/video.yaml and find the first scene:

- block: title
  kicker: concurrency
  text: Your program is waiting
  sub: Most of the time, it is doing nothing at all.
  bg: { kind: beams, opacity: 0.6, speed: 0.8 }

Change text, then render a still of it:

bun stingo still examples/goroutines/video.yaml --at 2.5 -o frame.png

No timeline to nudge, no keyframe to find. That loop — edit a line, look at a frame — is the entire premise.

#Change how it looks

The script above contains no colours. Those live in a separate file, so you can swap the whole look without touching a word of content:

bun stingo still examples/goroutines/video.yaml --at 23 --taste dusk -o dusk.png
bun stingo still examples/goroutines/video.yaml --at 23 --taste bootdev -o boot.png

Or derive a fresh profile from a single brand colour:

bun stingo taste "#ff7a18" --name Ember --mood bouncy --save ember.json
bun stingo still examples/goroutines/video.yaml --at 23 --taste ember.json -o ember.png

stingo taste does not just tint things. It places your hue on a measured lightness ramp, bleeds a trace of it into the neutrals, and lifts any colour that fails the contrast floors — it will tell you when it does.

#Scrub it live

bun stingo preview examples/goroutines/video.yaml

Serves a scrubbable preview on localhost:4321 that re-plans when you save the file. Because any frame is one function call, seeking is not a render — it is a lookup.

#Write your own

The smallest document that renders:

scenes:
  - block: title
    text: It works
    sub: That is the whole file.
bun stingo render my.yaml

That is the whole file. title defaults, the canvas defaults to vertical at 30fps, and taste defaults to the house profile — so every line above the scenes: key in the earlier example was optional.

Scene length is inferred from content and clamped by the taste's pacing, so you can leave dur off until a scene actually feels wrong.

#Check it before you render it

bun stingo doctor my.yaml

One pass over everything a render needs: ffmpeg, the fonts your taste asks for, the taste itself, the music track, every camera take, every image, the timing, and whether your words fit their frames and their scenes. It reports all of it at once and names a fix for each failure, so you are not discovering one problem per render.

bun stingo sheet my.yaml -o sheet.png

And once there is more than a handful of scenes, a contact sheet — one frame from the middle of each, in a grid. Nobody can hold a six-minute video in their head, and scrubbing only ever shows you one moment at a time.

#Where to go next

#The examples

Four in the repo, each answering a different question.

examples/quickstart the smallest thing that renders — a film written in TypeScript
examples/goroutines a full explainer: 50-odd scenes, code, charts, a diagram, cut to a track
examples/demo the fifty-second demo on the front of this site, narrated
examples/building-stingo a talking-head film, with a shot list and a recording guide

quickstart and goroutines render with nothing but the repo. demo and building-stingo need takes you record — each has a README saying exactly what to shoot, and tools/ingest.ts turns a folder of phone clips into the files their scripts name.