tutori / docs /field-notes.md
ProCreations's picture
v2: teacher's-studio design, MiniCPM5 planner+coach, expanding board, field notes
4972370 verified
|
Raw
History Blame Contribute Delete
6.57 kB
# Field Notes: Building Tutori, a Whiteboard Tutor That Draws While It Talks
*What we built for the HF Build Small Hackathon, what broke, and what 17B
parameters taught us about engineering around small models.*
[**Tutori**](https://huggingface.co/spaces/build-small-hackathon/tutori) is a
voice tutor. You ask it anything out loud; it researches the question if it
needs to, then teaches you in spoken steps while sketching the idea on a
whiteboard — stroke by stroke, in sync with its own voice, like a teacher who
never gets tired of your questions.
I built it for my parents. They're behind on today's technology, and the
firehose of AI news is impenetrable from the outside. With Tutori they just
ask — "what is Gemma?" — and get a patient, drawn-out explanation at their
pace. They actually use it. That's the only benchmark that ever mattered.
Everything runs on the Space itself via ZeroGPU: Gemma 4 12B teaches,
MiniCPM5 1B plans research and coaches, Higgs Audio v2 speaks, Whisper turbo
listens. No cloud APIs, no keys. Here is what we learned building it.
## Lesson 1: The model is 20% of a drawing tutor. Geometry is the other 80%.
Our first whiteboards were a disaster — overlapping labels, triangles
assembled from three unrelated lines, values scattered like confetti. We
tried prompting harder. It did not work, and it never works: a language
model freehanding 2D coordinates is a model being asked to do geometry
without eyes.
What worked was splitting the job:
- **A composite op vocabulary.** The model stopped placing twelve primitives
and started saying `{"op": "polygon", "side_labels": ["a", "b", "c"]}`
one op, with the geometry math done deterministically in the renderer.
- **A layout engine with zero tolerance.** Every op the model emits passes
through a placement pass: weighted obstacle maps, candidate scanning,
shrink-on-crowding, a capacity accountant that drops what won't fit, and a
hard guarantee that nothing overlapping ever reaches the screen.
- **A fuzz harness.** We generate hundreds of synthetic "worst-case" lessons
and require zero violations before any deploy. The harness caught ten real
bugs that user screenshots later would have.
The model proposes; the engine disposes.
## Lesson 2: ZeroGPU is a hardware constraint disguised as a free GPU.
ZeroGPU gives every Space an H200 slice with one catch: your GPU code runs in
disposable forked workers with a per-turn time budget. Almost every hard
problem we hit traces back to this:
- **NeMo crashes the workers** if imported in the main process, and costs
30–60s per fresh worker if loaded lazily — so our planned Nemotron ASR
became Whisper (preloads with everything, transcribes in a second).
- **Mamba-hybrid models can't really run.** Nemotron-H's Mamba-2 layers need
fused Triton kernels that won't JIT-compile inside a 59-second window, and
the pure-PyTorch fallback allocates multi-gigabyte tensors at prefill and
OOMs the worker. We learned this the expensive way — see Lesson 4.
- **Fast generators get coalesced.** Gradio collapses rapid streaming yields
into the latest state, which silently dropped lesson steps until we made
every payload cumulative and let the browser deduplicate.
None of this is in a tutorial. All of it is reproducible on a $0 Space, which
is exactly why the constraint is worth embracing.
## Lesson 3: Small models need formats, not freedom.
Three times we needed structured output from a model ≤1B parameters, and
three times the same arc played out. Ask MiniCPM5 1B for nested JSON and it
writes brilliant content with one missing bracket. Give it a few-shot example
and it copies the example verbatim. What finally worked was a *line
protocol*:
```
PROFILE: {"level": "beginner", "last_topic": "rainbows", ...}
NEXT1: What happens to the colors inside a raindrop?
NEXT2: Why do we see bands instead of a smear?
NEXT3: Could a rainbow form at night?
```
One regex per line, code-side guards against parroting, and the 1B model
became a reliable study coach — it updates the learner's profile after every
lesson and writes the three sticky-note follow-up questions you see in the
app. Small models are superb employees and terrible freelancers: define the
job precisely and they shine.
## Lesson 4: Fine-tune honestly, and let the boring pipeline win.
For the Well-Tuned badge we built a
[dataset](https://huggingface.co/datasets/ProCreations/tutori-whiteboard-lessons)
of 7,109 gold whiteboard lesson steps — programmatically generated across 8
diagram families and 78 topics, every single lesson validated to render with
zero overlaps *before* it could enter the training set. Then we LoRA-tuned
two dedicated "board artists":
[Nemotron 3 Nano 4B](https://huggingface.co/ProCreations/tutori-board-nemotron)
and [Gemma 4 12B](https://huggingface.co/ProCreations/tutori-board-gemma).
Both learned the job almost perfectly on paper (99% held-out token accuracy).
The Gemma artist drew the best gradient-descent diagram we've ever gotten
from any model — correct axes, slope arrows, the actual update rule. And we
still shipped neither. The Nemotron artist can't execute on ZeroGPU at all
(Lesson 2), and the Gemma artist lost a live A/B for a humbling reason: the
artist never sees the researched facts, only the narration — so a lesson
about that week's space news got confidently improvised geometry. Meanwhile
the boring pipeline — the big teacher model plus the deterministic layout
engine — kept quietly producing good boards.
Publishing the models, the dataset, and the scorecard felt better than
flipping the flag. The README says exactly why it's off.
## Lesson 5: A custom face is mostly CSS — and one JS gotcha.
The "stunning UI" pass that chases the Off-Brand badge is not a rewrite. It's
a design system (chalkboard scene, paper cards, sticky notes, two bundled
fonts), about 400 lines of CSS over Gradio's components, a hand-drawn canvas
renderer we already had, and one hard-won discovery: Gradio's CSS
preprocessor silently drops `:has()` selectors and everything after them —
tag elements with a class from JavaScript instead.
## The scorecard
| | |
|---|---|
| Models on the Space | 4 (Gemma 4 12B · Higgs v2 3B · MiniCPM5 1B · Whisper 0.8B) — Σ ~16.9B |
| Models fine-tuned & published | 2 + 1 dataset |
| Cloud APIs | 0 |
| Layout-engine fuzz violations at ship | 0 of 600 |
| GPU budget per lesson | 59 seconds, every model included |
Built small. It was the constraint that made it good.
*— SSH/ProCreations, June 2026*