Skip to the content
PCBKitNew board
2 layers70 × 50 mmBilly, the printed circuit board in 3D

Offline Big Mouth Billy Bass voice jokester powered by an ESP32-S3 and a local GPU backend.

Size
70 × 50 mm
Copper layers
2
Thickness
1.6 mm
Parts
82
Nets
85
Pads
321
Tracks, vias
587, 104
Pours
7
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
9 Sep 2026
Updated
13 Sep 2026

Overview

Billy is an open-source printed circuit board by ryandrake08, from the ryandrake08/billy repository on GitHub, published under the Apache-2.0 licence. It is a 2-layer board of 70 × 50 mm with 82 parts on 85 nets, routed with 587 tracks and 104 vias.

It is built around the ESP32-S3-WROOM-1-N8, DRV8833PWPR and AP63203WU, with the USBLC6-2SC6, AO3400A and AO3401A among its other parts. Its parts include 27 capacitors, 17 resistors, 10 connectors, 7 ICs and 5 transistors. It belongs with the ESP32 boards of this library.

Main components

9

README

README.md

Smart Mouth Billy Bass LLM jokester

Billy is a Big Mouth Billy Bass novelty toy converted into an offline, LAN-only voice jokester. An ESP32-S3 inside the fish captures speech, drives the motors, and plays back audio; a separate GPU box does speech-to-text, LLM inference, and text-to-speech. There is no internet dependency anywhere in the pipeline.

The fish's' persona, conversation history, text cleanup, and configuraion all live on the backend, in an business-logic "shim" application in front of the LLM. The fish just captures audio, makes three HTTP calls per turn, plays the reply, and animates the mouth/head/tail in sync with it.

Architecture

text
        FISH (ESP32-S3)                                BACKEND (LAN)
  ┌──────────────────────────┐                  ┌───────────────────────────┐
  │ mic → capture utterance  │─── utterance ───▶│ whisper.cpp        :8081  │
  │                          │◀───── text ──────│                           │
  |                          |                  |                           |    ┌─────────────────┐
  │ text → shim              │────── text ─────▶│ shim (persona,     :8000  │──▶ | llama.cpp :8080 |
  │                          │◀─── response ────│  history, text cleaning)  │    └─────────────────┘
  │                          │                  │                           │
  │ sentence → TTS           │──── sentence ───▶│ Kokoro             :8880  │
  │                          │◀──── audio ──────│                           │
  │ amp + motors             │                  └───────────────────────────┘
  └──────────────────────────┘

Three calls per conversational turn:

CallTargetRequest → Response
STTwhisper.cpp :8081, directPOST /inference (multipart wav, response_format=verbose_json, language=auto) → {"text", "detected_language", ...}
brainshim :8000POST /v1/respond {session, text, language} → SSE stream of {"sentence", "voice"}
TTSKokoro :8880, directPOST /v1/audio/speech {model, input, voice} → wav bytes

STT and TTS are dumb audio↔text transforms called directly; only the brain hop goes through the shim, which is a text-only service that never touches audio. The shim streams sentences as the LLM generates them, so the caller can synthesize and play sentence 1 while sentence 2 is still being generated — this pipelining is what keeps time-to-first-audio low.

There are three implementations of this protocol, and they should be indentical: the reference clients src/client/billy_cli.py (Python) and src/cli/ (C), and the ESP32 firmware (src/firmware/main/net.c). The CLIs exist to document the protocol the firmware has to match — the C one doubles as a check on how much of the firmware's own protocol code (WAV header, hand-rolled JSON parsing, SSE reassembly) is genuinely ESP-IDF-independent and portable to a host build; it turned out to be all of it.

Repository layout

PathWhat
board/Custom PCB for the fish's electronics (KiCad).
src/firmware/ESP32-S3 firmware (C, ESP-IDF).
src/shim/Backend application-logic layer (Python/FastAPI).
src/client/CLI reference client (Python) — a mic+speaker stand-in for the fish.
src/cli/CLI reference client (C) — built against libcurl/PortAudio/soxr.
src/tests/End-to-end integration test for the multi-language STT→shim→LLM pipeline.

board/ — Custom PCB

KiCad project at board/billy/. Replaces the toy's stock control board with a single board carrying the ESP32-S3, motor drivers, amp, buck regulator, and power-input protection, designed to fit inside the original toy's chassis (~70×50mm).

PartRole
ESP32-S3-WROOM-1-N4R2Controller — WiFi, dual I²S, deep sleep
TDK ICS-43434 (I²S MEMS mic)Microphone — mounted as a separate satellite board, off the main PCB, for acoustic placement
MAX98357A (I²S class-D amp)Speaker drive
2× TI DRV8833 (dual H-bridge)3 motor channels — mouth, head, tail (all spring-return, unidirectional)
DIODES AP63203 buck6V → 3.3V logic/audio rail
AOS AO3401A / AO3400A FETs (battery reverse-polarity protection, USB/motor-rail isolation pass FET, status LED + photocell/Vmotor-sense divider gating)Power-input protection and deep-sleep peripheral gating
Analog Devices LTC4412Ideal-diode controller — motor rail vs. USB arbitration into the buck
MCC B5817W SchottkyPassive ideal diode — USB VBUS into the buck
2× 5A slow-blow fusesBattery and DC-jack input protection

The KiCad schematic and PCB are authoritative for hardware connectivity and layout. Firmware board.h mirrors the schematic's GPIO assignments and must stay synchronized with it.

Workflow:

bash
open board/billy/billy.kicad_pro
kicad-cli sch erc --format json --severity-all -o erc.json board/billy/billy.kicad_sch
kicad-cli pcb drc --format json --severity-all --schematic-parity -o drc.json board/billy/billy.kicad_pcb

src/firmware/ — ESP32-S3 firmware

Captures speech, calls the backend for STT → LLM → TTS, plays the reply, and drives the mouth/head/tail motors in sync with it. Targets esp32s3 (tested with ESP-IDF v6.0.2).

Layering

LayerFilesResponsibility
appmain.capp_main() wires everything together; runloop_task runs the IDLE→ACTIVATE→LISTEN→THINK→SPEAK→IDLE state machine.
transportnet.c/.hThe fish↔backend contract described above — STT direct, brain hop through the shim, TTS direct.
deviceaudio.c/.h, motors.c/.h, activation.c/.h, peripherals.c/.hFish-specific setup and algorithms built on the HAL: I²S mic/amp + mouth lip-sync envelope; 2× DRV8833 motor choreography with nFAULT telemetry and deferred Vmotor capture; button/mode-switch/wake-word activation + deep sleep; WS2812 status LED + photocell/Vmotor-sense reads, all gated by one shared enable line.
HALhal.c/.hGeneric, pin-parameterized primitives (GPIO, ADC1, LED strip, PWM, I²S, deep sleep) with no fish-specific naming and no board.h dependency of its own.

board.h is the firmware representation of the schematic pin map. components/wakeword is the on-device wake-word detector (TensorFlow Lite Micro, ported from ESPHome's micro_wake_word component). See its own models/ATTRIBUTION.md for training provenance.

Build

bash
source ~/.espressif/tools/activate_idf_v6.0.2.sh
cd src/firmware
export WIFI_SSID="your-2.4GHz-ssid" WIFI_PASSWORD="your-password" BACKEND_HOST="your-backend-hostname-or-ip"
idf.py set-target esp32s3      # once
idf.py build
idf.py -p <port> flash monitor

WiFi credentials and the backend host are build-environment variables injected as compile definitions (main/CMakeLists.txt). Changing one needs idf.py reconfigure. ./clean.sh removes all generated build state.

Debug

Step-debug with breakpoints over the S3's native USB-Serial-JTAG:

bash
openocd -f board/esp32s3-builtin.cfg    # terminal 1
idf.py -p <port> gdb                    # terminal 2

The DevKitC-1 has two USB ports — flashing uses the UART/CP2102 port; OpenOCD needs the separate native-USB port instead. The production board has a USB port and a three pin header (TTL level) to one of the ESP32's built-in UARTs. Only use a USB-to-UART cable that uses 3.3V with this header! In addition to the direct-to-UART header, the production board has a two pin header for firmware flashing. Jumping the header during a reset causes the chip to enter UART download (flashing) mode instead of booting normally from flash memory. The UART/FLASH headers are intended to be a backup in case flashing over USB fails, and the header can be left unpopulated if unused.

Runtime behavior

runloop_task starts immediately on boot and idles waiting for activation; it does not block on WiFi or the backend being reachable. Activation depends on the mode switch: button mode (press-to-talk, deep-sleeps between turns) or wake-word mode (mic stays live, listens for "hey billy"; a button press also works as a manual override). Flipping the switch mid-wait preempts immediately. Runtime-tunable constants (VAD thresholds, mouth/tail timing, and HTTP timeouts) are fetched from the shim's /v1/config every loop cycle rather than compiled in, with compiled-in fallbacks if the shim is unreachable. Each capture's VAD threshold adapts to the room's ambient noise level, measured during the pre-capture drain window, so a noisy room still reads as silence once the user stops talking. The DRV8833 nFAULT line is logged but does not gate motor commands because it asserts unreliably on the board; the drivers' independent silicon overcurrent, thermal, and undervoltage protection remains active.


src/shim/ — backend application-logic layer

The brain layer in front of llama.cpp. Holds the application logic that has to live off the fish for it to stay a thin client:

  • persona / system prompt — edit text.py, reload;
  • conversation history / session state — kept server-side, keyed by session id;
  • model quirks — the enable_thinking request flag and <think>-block stripping;
  • markdown/emoji scrubbing — the LLM still emits stray formatting that TTS would mangle;
  • sentence splitting + streaming — emits clean sentences as they're ready so the caller can pipeline TTS for low first-audio latency;
  • per-language voice routing (text.py's LANGUAGE_VOICES) — English (am_onyx) plus Spanish, Brazilian Portuguese, Mandarin, Japanese, Hindi, and Italian, picked from the caller's detected language and overridable per sentence for an explicit switch mid-reply;
  • runtime firmware config (fish_config.py) — tunable firmware constants served over GET /v1/config, so they can be retuned without a reflash.

Protocol

CallRequestResponse
POST /v1/respond{"session": "default", "text": "<user utterance>", "language": "english"}text/event-stream: one data: {"sentence": ..., "voice": ...} per spoken sentence, then data: [DONE]
POST /v1/reset{"session": "default"}{"reset": "<session>"}
GET /v1/config—Runtime firmware-tunable constants
GET /health—{status, llm_url, llm_model, llm_reachable, sessions}

Run

bash
cd src/shim
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/uvicorn billy_shim:app --host 0.0.0.0 --port 8000

Config via env: BILLY_LLM_URL (default http://localhost:8080), BILLY_LLM_MODEL (default Qwen3.5-9B). Tests use plain asserts rather than pytest:

bash
.venv/bin/python test_text.py
.venv/bin/python test_config.py
.venv/bin/python test_session.py

Deploy

Shim program can live anywhere. Recommend using your OS's init system to start and manage it.


src/client/ — CLI reference client (Python)

A command-line stand-in for the fish: drives the full backend pipeline (record → STT → shim → TTS → play) from any host with a mic and speaker.

Run

bash
cd src/client
python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt
./billy_cli.py --host <backend-host>     # ports 8000/8081/8880 derived from host

Press Enter to start talking, speak, press Enter to stop. Ctrl-C quits.

--voice <kokoro-voice> overrides the voice for the whole session (browse the list at http://<backend-host>:8880/web); otherwise each sentence uses the shim's own per-sentence choice. --session <id> sets the shim conversation id; --shim-url/--stt-url/--tts-url override individual endpoints. Uses the system default audio devices.

--input-wav <path> skips the mic entirely: runs one turn on that WAV file's bytes (16 kHz mono, same as the mic path itself records) and exits — for testing the same utterance repeatedly without re-recording it by hand each time.

Each turn prints various latency measurements.


src/cli/ — CLI reference client (C)

A second, independent reference client for the same contract — same record → STT → shim → TTS → play pipeline as the Python client, same flags, same latency breakdown, but built as a plain C binary via CMake instead of a Python venv.

External dependencies (none of this is vendored): libcurl (HTTP, mirrors httpx), portaudio (mic/speaker I/O, mirrors sounddevice), soxr (output resampling, same library the Python soxr package binds to). On macOS via MacPorts: port install curl portaudio soxr.

Build & run

bash
cd src/cli
cmake -S . -B build && cmake --build build
./build/billy_cli --host <backend-host>     # ports 8000/8081/8880 derived from host

Press Enter to start talking, speak, press Enter to stop. Ctrl-C quits, same as the Python client.

Same --voice/--session/--shim-url/--stt-url/--tts-url flags as the Python client, same system-default audio devices. TTS audio is resampled to the output device's native rate in software (soxr, SOXR_HQ quality).


src/tests/ — end-to-end integration test

Exercises the STT→shim→LLM chain for every supported language. For each language, a "golden" WAV asking Billy to repeat a fixed phrase is transcribed by real whisper.cpp, then sent to a shim, checking the reply comes back in the right voice and language.

bash
cd src/tests
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
./.venv/bin/python test_language_integration.py     # needs src/shim/.venv already set up

The golden WAVs in golden/ are gitignored: generate_golden.py makes them with Microsoft's free edge-tts. The test does this itself the first time golden/ is missing a file.


Backend engines

Three off-the-shelf inference engines run on the backend GPU host alongside the shim, all under systemd (Restart=always, enabled at boot):

ServicePortModelGPU
llama.cpp8080Qwen3.5-9B, Q8_0, non-thinking modeyes
whisper.cpp8081large-v3yes
Kokoro (Kokoro-FastAPI)8880am_onyx + 6 other language voices (see src/shim/)yes

All three plus the shim are expected to fit resident in ~14GB of VRAM.


License

Source code and documentation are licensed under Apache License 2.0. The trained wake-word model src/firmware/components/wakeword/models/hey_billy.tflite is explicitly excluded; see NOTICE and its ATTRIBUTION.md.

Parts

82
QtyValueFootprintReferences
9100nCapacitor_SMD:C_0603_1608MetricC9, C18, C19, C22, C25, C26, C27, C28, C29
810kResistor_SMD:R_0603_1608MetricR1, R2, R3, R4, R5, R6, R11, R18
4100kResistor_SMD:R_0603_1608MetricR9, R10, R13, R19
41uCapacitor_SMD:C_0603_1608MetricC1, C2, C3, C15
4MountingHoleMountingHole:MountingHole_3.2mm_M3_ISO7380_PadH1, H2, H3, H4
4Screw_Terminal_01x02TerminalBlock:TerminalBlock_Xinya_XY308-2.54-2P_1x02_P2.54mm_HorizontalJ1, J3, J4, J10
30.2Resistor_SMD:R_1206_3216MetricR7, R8, R12
310uCapacitor_SMD:C_0603_1608MetricC7, C8, C14
3AO3400APackage_TO_SOT_SMD:SOT-23Q4, Q6, Q7
3TestPointTestPoint:TestPoint_Pad_1.0x1.0mmTP2, TP9, TP10
210nCapacitor_SMD:C_0603_1608MetricC5, C12
22.2uCapacitor_SMD:C_0603_1608MetricC6, C13
2220pCapacitor_SMD:C_0603_1608MetricC10, C11
2220uCapacitor_SMD:CP_Elec_6.3x5.9C4, C20
222uCapacitor_SMD:C_1206_3216MetricC16, C24
230mInductor_SMD:L_0603_1608MetricL1, L2
2AO3401APackage_TO_SOT_SMD:SOT-23Q1, Q2
2DRV8833PWPRPackage_SO:HTSSOP-16-1EP_4.4x5mm_P0.65mm_EP3.4x5mmU2, U4
2FuseFuse:Fuse_Littelfuse-NANO2-451_453F1, F2
110uCapacitor_SMD:C_1206_3216MetricC17
13.3uInductor_SMD:L_APV_ANR4030L3
1470Resistor_SMD:R_0603_1608MetricR17
175kResistor_SMD:R_0603_1608MetricR15
1AP63203WUPackage_TO_SOT_SMD:TSOT-23-6U6
1B5817WDiode_SMD:D_SOD-323D2
1Conn_01x02Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalJ5
1Conn_01x03Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalJ6
1Conn_01x06Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_HorizontalJ2
1ESP32-S3-WROOM-1PCM_Espressif:ESP32-S3-WROOM-1U1
1LOGOLOGOG***
1LTC4412Package_TO_SOT_SMD:TSOT-23-6U5
1MAX98357APackage_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mmU3
1Screw_Terminal_01x03TerminalBlock:TerminalBlock_Xinya_XY308-2.54-3P_1x03_P2.54mm_HorizontalJ11
1Screw_Terminal_01x06TerminalBlock:TerminalBlock_Xinya_XY308-2.54-6P_1x06_P2.54mm_HorizontalJ7
1TS-1088-AR02016Button_Switch_SMD:SW_SPST_TS-1088-xR020SW1
1USBLC6-2SC6Package_TO_SOT_SMD:SOT-23-6U7
1USB_B_MicroConnector_USB:USB_Micro-B_Amphenol_10118194-0001LF_HorizontalJ12
1WS2812B-V6LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mmD1

Questions

How big is the Billy PCB?

It measures 70 × 50 mm and has 2 copper layers, 1.6 mm thick.

What parts does the Billy use?

82 parts, built around the ESP32-S3-WROOM-1-N8, DRV8833PWPR, AP63203WU and USBLC6-2SC6. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Billy KiCad files?

From the ryandrake08/billy repository on GitHub, at the commit this page was made from: the board is board/billy/billy.kicad_pcb and its schematic board/billy/billy.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Billy design in my own project?

Its repository publishes it under the Apache-2.0 licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).

Similar boards

  • 2 layers50 × 50 mmPCB DRONE, the printed circuit board in 3D

    PCB DRONE

    shreejita56/XiZi-PCB-DRONE

    So I made a custom PCB based on Esp32 . It holds an IMU and a Barometer for control . It has a normal 4 Motor Layout 2 motors will move clock wise and 2 will be anti-clock wise direction.

    68 parts
  • 2 layers100 × 73.9 mmSecurity Alarm Panel, the printed circuit board in 3D

    Security Alarm Panel

    Matyas604/Security-Alarm-Panel

    Home Assistant Smart Home Alarm Control Panel with fingerprint reader, NFC/RFID card reader, motion radar, and touchscreen TFT display. It can control your smart home, not just your home security system.

    132 parts1GPL-3.0
  • 2 layers137 × 84 mmESP32-Geiger Counter, the printed circuit board in 3D

    ESP32-Geiger Counter

    AronASzucs/Avalanche32

    Handheld ESP32-S3 Geiger counter with a firmware-regulated 400 V boost, SBM-20 tube, USB-C charging and OLED display.

    89 parts3
  • 4 layers54 × 120 mmKartonaut, the printed circuit board in 3D

    Kartonaut

    Jan-Philipp-Munsch/kartonaut

    TVC model rocket (after BPS.space Scout F): Teensy 4.1 flight firmware, mainboard spec, project log

    174 partsMIT
  • 2 layers82 × 60 mmEEE3088F PCB Project, the printed circuit board in 3D

    EEE3088F PCB Project

    LlamaLuLu/Micromouse-Lula-and-Rejoice

    Our Micromouse power subsystem for the Design course EEE3088F

    107 parts
  • 2 layers46.5 × 78.5 mmShaun proj, the printed circuit board in 3D

    Shaun proj

    SFBdragon/eee3088f-project

    Collaborative repo for the UCT EEE3088F Project of team 69.

    99 partsCERN-OHL-P-2.0
  • 4 layers50 × 50 mmWireslesslock, the printed circuit board in 3D

    Wireslesslock

    Shehroz30/wireslesslock

    69 partsMIT