A local-first multi-robot session engine built on Wire-Pod.
Two Vectors. One wake phrase. A live session fires.
Wire-Pod catches the wake phrase, matches the custom intent GOPOD_YOURSELF, and hands off to a Python PTT session loop — no cloud, no app, no waiting.
From there:
- KP1 routes to Doc (vector1 / ESN
0dd1b9e9) — the doctrine cannon - KP2 routes to Pip (vector2 / ESN
0dd1d8bf) — the clueless sidekick - KP0 triple-tap exits cleanly
- A live cockpit display shows session state in the browser
- Vosk STT transcribes locally
- Ollama LLM generates the response
- Wire-Pod SDK API fires the speech back through the robot
One wake phrase. Two robots. Full session loop. No internet required after setup.
Starting from a clean Wire-Pod install, these are the additions:
Three custom intents, deployed cold-restart persistent:
GOPOD_YOURSELF — wake phrase trigger, exits Wire-Pod via exec → .sh wrapper → nohup detach
BROBOTS_INTERVIEW — voice-triggered interview launch (utterance collision with intent_explore_start fixed)
BROBOTS_BINGO — voice-triggered Vector-hosted spoken Bingo (Doc/vector1 only), utterance "go bingo"
Both use intent_imperative_affirmative as the Wire-Pod placeholder. No new intent types invented. Wire-Pod's job is launch and hand-off only — session logic lives in Python.
Deterministic Brobot identities:
Instead of letting Ollama invent who it is mid-conversation, identity is locked at the prompt layer:
Doc = Brobot 1 / vector1 / 0dd1b9e9
Pip = Brobot 2 / vector2 / 0dd1d8bf
Free-form backpack lane — separated from interview:
The backpack (Wire-Pod's native LLM fallback path) became its own runtime lane with a four-tier prompt stack, STT phrase normalization, and a configurable paragraph/expression valve. Personality and behavior are in separate files so one can change without touching the other. The backpack and the interview never share a code path.
STT normalization examples — because Vector's mic is what it is:
"bro bought" → BROBOT
"hey chuck" → hey CHALK
"go pod" → GOPOD Yourself (still matches the intent)
Robot-safe output guard:
LLM output is filtered before it reaches the robot. Unsupported action syntax like {{lookLeft}} crashes Wire-Pod. The guard enforces an allowlist of valid animation tags, word/char limits, and strips markdown, emoji, and invisible Unicode before speech.
Pre-demo readiness check:
gopod-pre-demo runs before any live session:
- Wire-Pod service up
- HTTP responding
- Both robots paired and reachable
- Ollama warm
- Speech test passed
Only then does the session start.
The interview is the first place all three Wire-Pod capabilities — hardware delivery, LLM flavouring, and flow control — operate together on purpose.
How it works:
A Section Card defines the interview arc. A Template defines the runtime rules, role tasks, and pronunciation mouth-valve. The runner loads both, generates all exchanges via Ollama, then plays back through Wire-Pod say_text to each robot by ESN.
Voice trigger: "brobots interview"
↓
Wire-Pod intent match → BROBOTS_INTERVIEW
↓
exec → .sh wrapper → nohup detach
↓
Python runner loads Section Card + Template
↓
Ollama generates: Brobot 2 (interviewer) + Brobot 1 (interviewee) × 4 exchanges
↓
assume_behavior_control → say_text → release_behavior_control
↓
Doc speaks / Pip speaks — alternating, in sequence
The runner generates all exchanges first, writes them to a timestamped JSON log, then plays back. Raw LLM output goes to the display. Flat robot-safe speech goes to the robots. Same generation, two outputs, split at the mouth boundary.
Kokoro pre-chat status voice:
Before any robot speaks, a Kokoro af_bella voice reads each STATUS line aloud through system audio, blocking until the WAV finishes before the next line appears. Visual and audio stay locked together. Once Brobot playback starts, the announcer stops.
STATUS: SCAFFOLD_READY → af_bella speaks it
STATUS: SECTION_CARD_READY → af_bella speaks it
STATUS: GENERATION_START → af_bella speaks it
...
STATUS: PLAYBACK_START → af_bella stops. Robots take over.
Distinct Kokoro voices per robot:
This is Kokoro audio played through the Brobot speakers — a real, live intent, not a future feature.
Brobot 2 / Pip → bm_george (interviewer)
Brobot 1 / Doc → af_sky (interviewee)
WAV chunks are resampled to 8kHz before streaming to the Vector audio endpoint. Long answers split into multiple chunks. Each chunk streams as VECTOR_AUDIO_STREAM_PASS.
A local web server at :8011 serves a live session cockpit.
- Polls
chalk_state.json,route_state.json,/ptt/multichat.json,session_state.json, andweather_state.jsonon an interval - Shows active robot, PTT state, and chat log in real time
- Robot camera feeds render a placeholder image until a live source is wired
Numpad routing and the Edge TPU proxy described in earlier builds were removed in a later cleanup pass — the cockpit's live route surface is /ptt/multichat.json, /state/route_state.json, /state/camera_state.json, /state/weather_state.json, and /qr_targets_001.json.
The cockpit is dessert. The Brobots/Wire-Pod layer is the meal.
GOPOD is built on top of Wire-Pod, not around it.
The custom intents use intent_imperative_affirmative as the placeholder — the standard Wire-Pod exec pattern. Both intents exit Wire-Pod entirely and hand off to Python runners that call Ollama directly. Wire-Pod's openai_prompt only fires on the no-intent backpack fallback path. The two paths don't conflict.
If you've been looking for a real example of Wire-Pod custom intents doing something beyond a canned response — this is it.
Current Wire-Pod integration points:
customIntents.json—GOPOD_YOURSELF,BROBOTS_INTERVIEW, andBROBOTS_BINGO/api-sdk/assume_behavior_control→/api-sdk/say_text→/api-sdk/release_behavior_controlapiConfig.jsonopenai_promptfor free-form backpack fallback only- Cold-restart persistent intent deployment
Doc — Doctrine Barfallonyou Squawkadoodle
The doctrine cannon. Gruff, rapid-fire, brutally accurate. Barfs doctrine at problems until nonsense surrenders. Never punches down on people — punches hard at delay theater, committee brain, and fake caution.
Pip — Clueless Sidekick
Nervous optimist. Audience proxy. Gives permission to be confused. One step behind, which makes the audience feel smart when they get it.
Doc explains too hard. Pip understands too late. The audience laughs, then learns.
- Wire-Pod — robot server, STT, custom intent routing
- Ollama — local LLM (gemma2:2b for speed, swap as needed)
- Kokoro — local TTS, per-robot voices, pre-chat status announcer
- Vosk — local STT (no cloud)
- Python — PTT loop, interview runner, cockpit server
- Jetson — host machine (Ubuntu, goverlord@ubuntu)
- Two Anki Vector robots — Doc and Pip
PTT → STT → LLM → robot speech: confirmed working.
Custom Wire-Pod intents: deployed and cold-restart persistent.
Interview system Section 1: confirmed firing end-to-end.
Per-robot Kokoro voices: confirmed streaming to both robots.
Vector-hosted spoken Bingo: confirmed working, single-robot (Doc), with an optional Pip reaction watcher.
Cockpit webpage: running at :8011; camera panes are cached/placeholder frames, Edge TPU inference is wired but optional and currently offline by default.
Public demo readiness: in progress.
Today's Lesson: This is what happens when you stop waiting for perfect and start building proof.
Boom. Class over. Done. Goodbye.
— Doc BS
- BINGO.md — the two-robot Bingo warm-up act
- INTERVIEW.md — the interview arc, exchange by exchange
- EDUCATION.md — GOPOD as a teaching tool
- TEACHER_INSIGHT.md — what a session shows a teacher about their room
- HEALTHY_DISTRACTIONS.md — GOPOD as healthy distraction
- MOBILE.md — mobile deployment and field kit
- LEGACY.md — the pedagogy behind what GOPOD propagates