This post is part of the Voice Assistant on Raspberry Pi series.
Two Raspberry Pi 4s, a few hours of setup, and at the end you have a voice assistant running entirely on your local network. Before writing a single line of .NET code, here’s the setup for both Pis.
Two Pis, two roles
Each Pi has a distinct job in this setup:
| Pi Client (2 GB) | Pi Brain (4 GB) | |
|---|---|---|
| Role | Audio, GPIO, orchestration | Local LLM (Ollama) |
| Talks to | Pi Brain over HTTP | - |
| Stack | .NET 10 Worker Service, Whisper, Piper TTS | Ollama + Llama 3.2 3B |
| Hostname | pi-client | pi-cerveau |
Step 1: Flash Raspberry Pi OS Lite 64-bit
On both Pis, use the same base image: Raspberry Pi OS Lite (64-bit). No desktop environment, everything runs over SSH.
Download and install Raspberry Pi Imager.
In the Imager:
- OS: Raspberry Pi OS (other) → Raspberry Pi OS Lite (64-bit)
- Storage: your microSD card
- Click Edit Settings (⚙️) before flashing:
Hostname : pi-client (or pi-cerveau for the second one)
Username : gabriel
Password : [your choice]
Wi-Fi SSID : [your network]
Wi-Fi Pass : [your password]
Locale : America/Toronto
SSH : ✅ Enable SSH (Use password authentication)
Repeat for the second Pi, changing only the hostname to pi-cerveau.
Step 2: First boot and SSH
Once both Pis are plugged in and booted, find their IPs:
# Ping by hostname (mDNS)
ping pi-client.local
ping pi-cerveau.local
If mDNS doesn’t work, check your router’s DHCP lease table for the assigned IPs.
Connect via SSH:
ssh gabriel@pi-client.local
ssh gabriel@pi-cerveau.local
Step 3: System update
On both Pis:
sudo apt update && sudo apt full-upgrade -y
sudo apt autoremove -y
sudo reboot
Reconnect after the reboot.
Step 4: Install .NET 10
On both Pis:
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 10.0
Add .NET to your PATH permanently:
echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.bashrc
echo 'export PATH=$PATH:$HOME/.dotnet:$HOME/.dotnet/tools' >> ~/.bashrc
source ~/.bashrc
Verify the install:
dotnet --version
# Expected: 10.x.x
Pi Client only: if you have a monitor attached, enable Screen Blanking via
sudo raspi-config→ Display Options → Screen Blanking → Enable. Avoid editing/boot/firmware/cmdline.txtfor this;consoleblank=300can be silently ignored on Pi OS Bookworm whenplymouthis active.
Step 5: ALSA audio (Pi Client only)
The Pi Client handles the microphone and speaker. Install the ALSA libraries:
sudo apt install -y \
alsa-utils \
libsndfile1 \
portaudio19-dev \
ffmpeg
Test the microphone
Plug in your USB microphone:
# List input devices
arecord -l
# Record 5 seconds
arecord -D hw:1,0 -f cd -t wav -d 5 test.wav
# Play it back
aplay test.wav
If
arecord -ldoesn’t see your mic, tryhw:2,0orhw:0,0depending on what’s listed.
Test the speaker
# List output devices
aplay -l
# Audio test
speaker-test -t wav -c 2
Step 6: Whisper system dependencies (Pi Client only)
We use Whisper.net with its native runtime. Install the required system packages:
sudo apt install -y cmake build-essential
The
Whisper.netNuGet package and its runtimes get added directly in the .NET project. We’ll get to that in article #2.
Step 7: Install Piper TTS (Pi Client only)
Piper is a lightweight TTS engine that runs natively on ARM64.
mkdir -p ~/piper && cd ~/piper
# Download the ARM64 binary
wget https://github.com/rhasspy/piper/releases/latest/download/piper_linux_aarch64.tar.gz
tar -xzf piper_linux_aarch64.tar.gz
# The tar extracts into a piper/ subdirectory — PATH needs to point to ~/piper/piper
echo 'export PATH=$PATH:$HOME/piper/piper' >> ~/.bashrc
source ~/.bashrc
piper --version
Download a French voice
mkdir -p ~/piper-voices && cd ~/piper-voices
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/low/fr_FR-siwis-low.onnx
wget https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/siwis/low/fr_FR-siwis-low.onnx.json
Test Piper
echo "Bonjour, je suis votre assistant vocal." | \
piper --model ~/piper-voices/fr_FR-siwis-low.onnx --output_raw | \
aplay -r 22050 -f S16_LE -t raw -
That’s the wow moment of the whole setup.
Step 8: Install Ollama (Pi Brain only)
curl -fsSL https://ollama.com/install.sh | sh
Ollama installs as a systemd service automatically. Verify:
ollama --version
systemctl status ollama
Pull the model
# Llama 3.2 3B: solid quality/speed balance for 4 GB of RAM
ollama pull llama3.2:3b
About 2 GB to download. In article #4 we switch to
llama3.2:1bbecause 3B consistently hits the timeout on a loaded Pi 4 GB. Pull 3B for now to test, but expect to swap it.
Quick French test
ollama run llama3.2:3b "Réponds en français : quelle est la capitale du Québec?"
Expose Ollama on the local network
By default, Ollama only listens on localhost (127.0.0.1:11434). You need to expose it so the Pi Client can reach it. sudo systemctl edit ollama may not create the file correctly on Pi OS, so do it manually:
sudo mkdir -p /etc/systemd/system/ollama.service.d/
sudo nano /etc/systemd/system/ollama.service.d/override.conf
Paste exactly this:
[Service]
Environment="OLLAMA_HOST=0.0.0.0"
Save (Ctrl+X, Y, Enter), then:
sudo systemctl daemon-reload
sudo systemctl restart ollama
Check that it’s now listening on all interfaces:
systemctl status ollama | grep Listening
# Expected: Listening on [::]:11434
Then from the Pi Client:
curl http://pi-cerveau.local:11434/api/tags
You should get a JSON response listing llama3.2:3b.
Step 9: Static IPs (optional)
To keep both Pis consistently reachable, set up fixed IPs in your router using DHCP reservations by MAC address:
# Find the MAC address
ip link show eth0 | grep ether
Both Pis are reachable on the network, .NET 10 is installed, and Piper speaks. Ready for code.
Series articles
- Setting Up Both Raspberry Pis (this article)
- .NET 10 Worker Service and Audio Pipeline
- Ollama Integration and Home Context
- Memory, Silence Detection, and systemd
- Real-Time Weather and Swapping to the Claude API
- Function Calling: Teaching Tools to the Assistant
- Retrospective, Lessons Learned, and v2 Roadmap
In article #2, we’ll build the .NET 10 Worker Service and wire up the GPIO button to the full audio pipeline, no LLM yet, just to confirm the entire audio chain works end to end.
This post was written with AI assistance and edited by me.