Snoot is a small companion robot inside a plush polar bear. It is in design. There is no physical prototype yet. So everything we can show about how it works comes from software: firmware run in simulation, models of its servos, checks on its motion clips.
That makes explaining it hard. A diagram can claim anything. We wanted short films in the style of 3Blue1Brown, where each spoken claim rests on a number some test printed. Between 2026-10-20 and 2026-10-22, AI coding agents built the pipeline and five films, with a person reading every script line for its claims. This post walks through the pipeline using the pilot film, "From words to a gesture".
The pipeline at a glance
- Script. One line per beat. Each line is a sentence from our technical page, or a shorter one with the same claim and number.
- Evidence. Each number in the script comes from a test run, named beside the line.
- Scene code. Manim Community 0.22 draws each beat, rendered at 2560 × 1440 and 30 fps.
- Voice. One take from ElevenLabs v3 in a stand-in voice. Word times come from Whisper (the faster-whisper build).
- The narration gate. Every heard word must match its script word, or the run stops.
- Captions. Drawn from the same word times as the animation.
- The mix gates. Music under the voice, then loudness, true peak and per-word clearance checks.
The output is a 2560 × 1440 film, a 1280-wide phone cut, two posters and a WebVTT caption file.
Script first, then evidence
The pilot has six beats. Every number in it came from our end-to-end test, run on 2026-10-20. In that test, a scripted agent sends a joke through our relay to the robot's firmware, which runs in simulation. The firmware logs when each gesture's stroke lands on its own clock. A servo model then estimates how far the physical servos would trail.
The film draws that run's own data: the joke's 21 word times, its 2 strokes and their 2 lags. It doesn't draw an idealised version.
| Claim in the narration | Basis |
|---|---|
| "each hit lands within 3 milliseconds of its word" | Two runs on 2026-10-20: worst 0.5 ms (real voice), worst 2.1 ms (a timing voice, one word every 0.5 s). A re-run on 2026-10-21: 2.1 ms and 1.5 ms. |
| "about 32 to 44 milliseconds behind" | The servo model's lag in the same runs: 32, 36 and 44 ms. Marked EST on screen, and spoken as "a model, not a bench". |
| "Every move is checked" | Motion clips are checked for joint limits, speed, load and the arms' fabric clearance from the head. The wave drawn passes at 10.1 mm. |
Two places where the film is weaker than it looks:
- The nod never played. The joke cues a nod on "union". In real speech, "union" comes 2.3 s after "My", while the shrug is still playing. The firmware plays one motion at a time, so the nod was dropped. In the timing-voice run, "union" came 4 s after "My", and the nod fit. The film shows only the 2 gestures that played: the shrug and the wave.
- "Checked", not "passes". When we wrote the script, 3 of 33 clips failed the fabric-gap check and were being re-fitted. The narration says every move "is checked", which is true. It doesn't say every move passes, which was not. The 10.1 mm on screen comes from the wave, a clip that passes.
The curves' shapes are drawn, not measured. Only the times and the gap are data.
Scene code on the narration's clock
Each scene subclasses one shared base. The base holds the narration's clock. A beat doesn't guess its timing; it waits for the voice:
at(t)waits untiltseconds on the narration's clock.word(i, text)returns the start time of a given word in linei.- Each beat starts a fixed lead before its line, for example
at(starts[2] - 0.2).
So when a new voice take moves a line by half a second, the animation follows on the next render. Nobody re-times by hand.
The pilot has 6 beats in 41.8 s. The five films run 192.5 s in total (45.2, 15.8, 34.8, 54.8 and 41.8 s, measured with ffprobe on 2026-11-13). We have no recorded render times, so we don't quote one.
The narration gate
The voice take is one recording of all six lines. Whisper transcribes it with word times. The narration step then maps each heard word onto its script word, by position.
A heard word passes if it matches its script word (case and punctuation aside), if it is the number in words ("three" for "3"), or if it is on a short list of known misses. Anything else stops the run. No silent fix, no fuzzy match.
if same(a, b):
times.append([h["start"], h["end"]]); i += 1; k += 1
elif i + 1 < len(script) and (a + norm(script[i + 1]) == b or JOINED.get((a, norm(script[i + 1]))) == b): # two as one
cut = h["start"] + (h["end"] - h["start"]) * len(a) / (len(a) + len(norm(script[i + 1])))
times += [[h["start"], cut], [cut, h["end"]]]; i += 2; k += 1
elif k + 1 < len(heard) and b + norm(heard[k + 1]["word"]) == a: # one script word heard as two
times.append([h["start"], heard[k + 1]["end"]]); i += 1; k += 2
else:
sys.exit(f"STOP: word {i}: script {script[i]!r} vs heard {h['word']!r}")
The gate stopped us several times. Each stop became a named exception, never a looser rule:
- "a count" heard as "account". Two script words, one heard word. The gate gives both words that word's time, split by letter count.
- "goes soft" heard as "go soft". The two s sounds merge. Listed as the same sound.
- "cues" heard as "queues". Same sound, other spelling.
- A stretched first word. Whisper sometimes pulls a line's first word back into the pause before it. In one pilot take, the "The" that opens line 5 started 1.32 s before the next word. A caption keyed to that time would appear in silence. The gate now caps any word's start at 0.6 s before the next word.
For the pilot, all 86 of 86 words matched.
Captions that can't drift
The burned-in captions and the .vtt file come from the same word times, through the same function.
- The function splits each line into chunks of at most 2 caption lines, 24 characters each, preferring a sentence's end.
- Each chunk appears 0.1 s before its first word.
- In the
.vtt, a chunk ends where the next chunk of its line starts, or 0.15 s before the next line.
Manim draws the chunks as the scene renders. The same scene file, run with a file name, writes the .vtt. There is no second timing source to fall out of step. The pilot's file has 13 cues.
We burn captions in because our page plays films inline with no native controls. The .vtt ships beside each film and feeds the transcript.
The mix gates
One soft instrumental bed runs under all five films. The bed stays level, with no ducking, so it never pumps. Three checks run, and each one stops the run on a miss:
| Gate | Rule | Pilot result |
|---|---|---|
| Word clearance | Each word's loudest 50 ms at least 12 dB over the music | Worst word "lands" at 13.7 s: 16.9 dB. Median word: 24.2 dB. |
| Loudness | −16 LUFS integrated, one gain for the whole mix | −16.0 LUFS |
| True peak | At or under −1 dBTP, 4× oversampled | −4.6 dBTP |
The bed sits 20 LU under the voice. The word check widens each word by ±60 ms, since Whisper's times can be off by a phoneme.
One gain sets the whole mix to −16 LUFS, so the bed keeps its gap under the voice. If that gain would push the true peak over −1 dBTP, the run stops. A limiter would fix the peak, but it would also change the gap we just measured. The fix is a new take or a lower target, decided by a person.
What we'd tell another team
- Gate the voice against the script. A transcript that "mostly matches" lets a wrong number into a caption.
- One clock. Animation, captions and the caption file read the same word times.
- Say what the test showed, and no more. "Checked" and "passes" are different claims. So are a servo model and a bench.
- Show the miss. The nod that didn't play is a real limit of one-motion-at-a-time firmware. We logged it as a follow-up instead of animating it in.
Snoot's voice in these films is a stand-in, and every number is from simulation. When a prototype exists, the same pipeline can draw from bench runs.
This post is about Snoot: A plush bear that performs. It sees you, talks, and makes a show out of your day.