How It Works

Last updated: 2026-11-03. Companion docs: built-so-far.md](built-so-far.md) (what each file does), [testing.md](testing.md) (evidence), [spec-v2.md](spec-v2.md) (the plan format), [board-templates.md](board-templates.md) (the pictures), [privacy.md.

1. End-to-end flow

`text Parent signs in (Google) → agrees to the terms → adds a child (hero name + password) │ Child (browser / installed PWA) ▼ Camera / photo / PDF ──► POST /api/questions several questions on the page? the child picks one └──► POST /api/extract the chosen question → ProblemJSON (Muse Spark 1.3 vision) Typed question ────────► (no upload; sent straight to the lesson socket) │ ▼ WS /ws/lesson {"type":"prepare", ...} ├─ beat 1 at once: the teacher reads the question and marks the numbers (agent/prepare.py) └─ meanwhile PLAN (agent/plan.py) Muse Spark 1.3 drafts in parallel waves ─┐ fine-tuned Muse Glimmer races (off live) ─┤──► verify_v2 (code) ──fail──► next wave, with the errors │ │ pass Muse Spark 1.2 answers on its own ────────┴──► answers agree? ── no ──► careful re-solve / decline │ yes ▼ LESSON (agent/lesson.py): board steps + Piper voice + checkpoints, all from the verified spec the child answers by voice (Muse Voice Transcribe, streamed), typing or on the board code checks each answer: try again → hint 1 → hint 2 → shown ▼ END: badges, method levels, "Try one like this" (a verified twin), saved to My lessons `

Nothing a model writes reaches the child until code has checked it.

2. Step by step

2.1 Sign-in and consent (app/accounts.py, app/access.py)

2.2 Read the question (agent/extract.py)

  1. Find the questions. A photo or PDF goes to POST /api/questions. Muse Spark 1.3 lists the questions on the page; if there are several, the child taps one.
  2. Read one. POST /api/extract reads that question into a ProblemJSON ({question, level, problem_type, entities, numbers, asked, ...}). PDFs with a text layer are read without a model; scanned PDFs are rendered to images first.
  3. Clean. School LaTeX becomes plain text ($\frac{2}{5}$ → 2/5, $4 000 → $4000).
  4. Refuse early. Questions that need a figure, table or graph, or that have several parts, get a friendly "I can't help with this one" before any planning.

Photos are processed in memory and never written to disk.

2.3 Teach while planning (agent/prepare.py)

The board doesn't wait for the plan. As soon as the question is known, the teacher reads it aloud and marks the key numbers and who is who. This "beat 1" comes from the question text alone, so it is safe to show before anything is verified.

2.4 Plan (agent/plan.py)

2.5 Verify (agent/spec_v2.py, agent/verify_v2.py) — no model involved

2.6 Two models must agree (agent/check.py)

Muse Spark 1.2 answers the plain question on its own, in parallel, as one number. The plan is accepted only if both answers match (within 0.005). On a disagreement, one careful re-solve replaces the quick answer; the planner is only told "an independent solver got a different answer", never the number. If no draft agrees, the tutor declines: it won't show an answer it couldn't check.

2.7 The live lesson (app/lesson_ws.py, agent/lesson.py)

WebSocket messages:

Client → serverMeaning
start (spec, optional lesson_id, board)Begin a lesson on a verified spec. Anonymous only for built-in examples.
prepare (question or problem, board)Plan a new problem and teach beat 1 meanwhile. Uses one plan of quota.
nextThe last event finished playing; continue.
textThe child typed something (max 500 characters).
show_meShow this step.
board (op: units / mark / move / grow, value)A move on a board task.
mic (on)Start or end a voice turn. Binary frames carry 24 kHz mono PCM while it is on.

Server → client: lesson, step, ask, verdict, tutor, heard, thinking, recap, end, error, prepare, plan, plan_failed, mic. Speaking events carry their audio; every event carries a server timestamp (?debug=1 shows a timing overlay).

2.8 Personalisation (agent/learner.py, agent/badges.py)

2.9 After the lesson

3. Models

JobModel
Read photos and PDFs, plan, answer the child's questionsclaude-opus-5-5
Independent second answerclaude-opus-5-5 (set CLAUDE_MODEL_CHECK to change)
Escalation on hard problemsclaude-opus-5-5 at higher effort
Plan racer, fine-tuned with LoRAMuse Glimmer 30B, on Fireworks AI
The child's voice, streamedmuse-voice-transcribe-1.0
Teacher's voicePiper (offline; Meta has no TTS API)

Standard tier only, never Contributor models. All Meta calls go through agent/client.py. Names, emails, hero names and progress are never sent to any model.

4. Errors the child or parent can see

SituationResult
Not signed in, on a new problem"Sign in first." (examples still work)
Parent hasn't agreed to the terms"A parent needs to agree to the terms first."
Daily limit reachedA friendly "come back tomorrow" message
Figure, table, graph or multi-part questionDeclined before planning
No plan passes the checks, or the two answers don't agree"I could not check my working on this problem, so I won't show an answer I'm not sure of."
A model is unavailableA try-again-later message; nothing unverified is shown

5. Configuration

See the README's settings table. In short: ANTHROPIC_API_KEY (required for AI), GOOGLE_CLIENT_ID and SESSION_SECRET (parent sign-in), PUBLIC_DEPLOY=1 (public mode), ADMIN_EMAILS (who opens /admin), PLANNER_MODEL=glimmer with FIREWORKS_API_KEY and GLIMMER_MODEL_PATH (the Glimmer race), PLANS_PER_FAMILY_DAY and PLANS_PER_DAY (quotas). Locally, without PUBLIC_DEPLOY, everything is open for development and the tests.

Back to Qimora