A Pokémon you actually talk to — it hears you, " "thinks, and talks back in a cute electric voice. Every model runs on your " "own machine. Pull the Wi-Fi and it keeps going.
" "" "Pocket Pikachu lives on your desktop and reacts to you in real time. " "It is built to feel like a companion you keep — not a window you close.
Tap the mic and just speak. It transcribes, replies, and answers out loud " "in its own voice — voice in, voice out.
No API keys, no servers, nothing leaves your machine. The brain, the voice, " "and the ears all run in-process. Works in airplane mode.
Happy, hyper, surprised, sad, sleepy — the pet animates to match the moment " "and the bond grows the more you check in.
The same 3D Pikachu, animated frame-by-frame from real sprite sheets — " "the desktop app swaps these in response to what's happening.
A two-minute tour of the desktop companion — hello, chat, and " "nap-on-command.
Three small models, each doing one job, all under 1B params. " "This Space runs the voice on a free org GPU.
| Role | Model | Where it runs |
|---|---|---|
| 🧠 Brain | openbmb/MiniCPM5-1B-GGUF (Q4_K_M) | "
"OpenBMB · llama.cpp on CPU |
| 🗣️ Voice | openbmb/VoxCPM-0.5B | "
"OpenBMB · neural TTS on GPU |
| 👂 Ears | " "nvidia/nemotron-speech-streaming-en "
"— faster-whisper fallback | "
"NVIDIA Nemotron (native) · faster-whisper here |
On the voice. We worked with OpenBMB's " "VoxCPM for Pika's speech — a neural, reference-conditioned TTS that gives " "the pet a consistent, characterful electric voice instead of a generic readout. " "On this org GPU Space VoxCPM is the voice. A lighter CPU-only fallback exists for " "the free-CPU sibling Space so the pet can still talk where no GPU is available, " "but VoxCPM is the voice we built the character around.
" "On the ears. Pika listens with NVIDIA Nemotron streaming ASR — " "low-latency, fully local transcription that runs in the native macOS app. " "Nemotron's NeMo toolchain can't build inside a Gradio Space container, so on this " "GPU-limited Space the ears fall back to faster-whisper for the same job. " "Nemotron is the real ears; faster-whisper is the fallback for where Nemotron can't " "run.
" "What it actually took to make a talking pet that runs entirely on a " "laptop, under a thousand-token wallet.
The bet was never that a 1B model could be a whole product. " "It was that a 1B model could be a character — if the deterministic code " "around it owned everything brittle, and the model only did the part it's good at: " "one warm, in-character line at a time.
" "Every model loads in-process and lazy-loads on first request, so the " "Space boots fast and weights download on first use. A turn is a straight pipeline: " "mic audio → ears transcribe (NVIDIA Nemotron in the native app, " "faster-whisper as the fallback here) → MiniCPM5-1B replies through " "llama.cpp → VoxCPM speaks the reply. No network calls, no API keys, no " "telemetry. The honesty test we held ourselves to: turn the Wi-Fi off and the whole " "loop still works. It does.
" "The brain is capped at 72 output tokens per turn at "
"temperature 0.35. That budget forced a real design decision: the "
"system prompt asks for exactly one compact, useful line — say the catchphrase "
"once, then one genuinely helpful sentence, no markdown, no stage directions, no "
"<think> tags. A tiny model rambles when you let it; a tiny model "
"with a tight contract and a tight budget stays in character.
Small models confabulate time, weather, and system details with total "
"confidence. So those never come from the model. A keyless tool layer "
"(gather_tool_facts) resolves time, weather, and lookups locally and "
"injects them into the turn as tool facts the model must quote exactly. The "
"prompt is explicit: do not invent weather, time, model, or system data. The model "
"writes the personality; the code owns the facts.
Raw 1B output needs cleanup to feel like a pet. We strip leaked "
"<think> blocks, trim runaway leading “Pika pika pika…” "
"so the catchphrase lands once, require a minimum of real (non-mascot) words before "
"we trust a reply, and compact everything to a single tidy sentence. When a reply "
"fails the substance check, the pet falls back to a warm in-character line rather "
"than shipping noise.
VoxCPM is conditioned on a short reference clip plus pitch-up / tempo-down "
"styling through a tiny ffmpeg pass (asetrate + atempo), "
"which is what makes Pika sound like Pika and not a narrator. On ZeroGPU the GPU "
"only attaches inside the @spaces.GPU turn function, so all per-turn "
"inference is funneled through one decorated call — the brain and ears stay on CPU, "
"the voice gets the GPU exactly when it needs it.
OOM-killed the free build (exit 137) before a single turn ran. "
"ZeroGPU already ships a CUDA torch — pinning it re-downloads ~5GB and kills the "
"build. The fix was to let the platform own torch and never re-pin it.Honest takeaways from shipping a 100%-local talking pet on a " "thousand-token budget.
This runs the real stack live on a free org GPU — speak or type, " "and Pikachu answers out loud. First turn warms up the models.
🎤 Tap record and talk — Pikachu talks back " "out loud. Or 💬 type below.
" ) reply_audio = gr.Audio(label="Pikachu says", autoplay=True, interactive=False, show_label=False) chat = gr.Chatbot(elem_id="pet-chat", height=260, show_label=False, avatar_images=(None, PIKA_HAPPY)) with gr.Row(): mic = gr.Audio(sources=["microphone"], type="filepath", label="🎤 Talk to Pikachu", show_label=True) with gr.Row(): textbox = gr.Textbox(placeholder="💬 …or type something to Pikachu", show_label=False, scale=4, container=False) send_btn = gr.Button("Send", scale=1, variant="primary", elem_classes="talk-btn") with gr.Row(): checkin_btn = gr.Button("☀️ Daily check-in", elem_classes="talk-btn") gr.HTML(STACK_HTML) gr.HTML(FIELD_NOTES_HTML) gr.HTML(LEARNINGS_HTML) gr.HTML(credits_html()) mic.stop_recording(handle_voice, inputs=[mic, chat], outputs=[chat, reply_audio, sprite, mic]) send_btn.click(handle_text, inputs=[textbox, chat], outputs=[chat, reply_audio, sprite, textbox]) textbox.submit(handle_text, inputs=[textbox, chat], outputs=[chat, reply_audio, sprite, textbox]) checkin_btn.click(handle_checkin, inputs=[chat], outputs=[chat, reply_audio, sprite]) return demo demo = build_app() demo.css = CUSTOM_CSS os.environ.setdefault("GRADIO_ALLOWED_PATHS", str(HERE)) demo.queue() if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")), allowed_paths=[str(HERE)])