Until this week, everything my Raspberry Pi did was silent. It wrote notes, filled in dashboards, sent the odd email and scrolled messages across a little LED panel on my desk, but if I wasn’t looking at a screen I didn’t know about any of it. Now it can talk.
On 29 October Claude Code and I built two small things: a command-line tool that plays speech or audio on the Google speakers around the house, and a Voice switch on my LED display’s control page that reads out whatever the display is showing. The speech itself is generated on the Pi, not by a cloud service. This post is how it fits together and the few things that tripped us up.
Why bother?
I already had the speakers, and I already had a lot of automations producing small bits of useful information: the next train, which bin goes out, what’s flying overhead. Reading them means looking at something. Hearing them doesn’t.
I started it the way I start most things now: I dropped a task note into my vault’s Inbox asking for a way to play audio on the Google speakers. Before anything was built, I made three choices:
- Both speech and audio files, not just one or the other.
- The office speaker as the default, since that’s where I am when I’m working on this stuff.
- Local text-to-speech rather than the Google Translate voice that Home Assistant already had set up. I’d rather the words my scripts generate weren’t sent off to be turned into audio somewhere else.
The pieces
Most of the work turned out to be already done. Home Assistant was running and already knew about every speaker in the house, so the job was mainly joining things up:
- Home Assistant’s Google Cast integration exposes each speaker, and each speaker group, as a media player that can be told to play something.
- Piper, an open-source text-to-speech engine, runs in its own Docker container on the Pi. It uses the British English
en_GB-albavoice. - The Wyoming integration connects Home Assistant to Piper, which gives Home Assistant a new text-to-speech engine to use instead of the cloud one.
- Home Assistant’s REST API is the single way in. Anything on the Pi that wants to speak makes one API call that says “say this, on that speaker”, and Home Assistant gets Piper to render the audio and tells the speaker where to fetch it.
The access token for that API lives in a locked-down file on the Pi, like every other secret in this setup, and is read at run time.
SpeakerCast: one command
The first piece is SpeakerCast, a bash script of about a hundred lines wrapped around that API. It does three things:
speakercast say "…"speaks some text, on the office speaker unless I name another speaker or group.speakercast playplays an audio file or a URL. A local file is copied into a folder Home Assistant serves on my home network, so the speaker can fetch it, and anything in that folder older than a week is tidied away on each run.speakercast listshows every speaker and group Home Assistant knows about.
There’s an option to set the volume first, and a dry-run option that prints the calls it would make without making a sound. That last one matters more than it sounds: like every service on the Pi, SpeakerCast has a regression test, and a test that runs regularly and says “test” out loud in my office would not last long. The test checks everything short of actually playing anything.
It was built after midnight, so the only thing we couldn’t check that night was the bit you can hear. I ran the audible test the next morning, a “hello” and a local MP3 on the office speaker, and both played.
The one gotcha
Playing a local file needs Home Assistant to serve it over its /local/ web path, which comes from a www folder in its configuration. The catch is that Home Assistant only notices that folder if it exists when Home Assistant starts. Creating it isn’t enough; it needed one restart. Because a restart takes down the things that depend on Home Assistant, Claude waited until after my daily note had been created for the next day before doing it.
Giving the LED display a voice
The more fun half came that evening. On my desk there’s a Pi Zero with a Pimoroni Unicorn HAT HD, a 16×16 LED matrix that scrolls messages, with a web control page that’s part of my RailData server. It has a set of quick-send buttons that each fetch something and scroll it across the panel.
That page now has a Voice Off/On switch. With it on, anything I send to the panel is also spoken on the office speaker:
- any message I type in
- the next trains
- the next bin collection
- the aircraft overhead (the feed behind it was the subject of last week’s post)
- the AuroraWatch status
- command words like “time” and “date”
The switch starts Off, and the browser remembers it. Automated messages and the background animations stay silent, so the speaker only talks when I’ve asked it to.
It uses exactly the same route as SpeakerCast: one Home Assistant call with the Piper engine. If the speaker call fails, the error shows on the page, but the message still goes to the panel. The display was there first, so speech is never allowed to break it.
Writing for the ear, not the eye
Text written for a 16-pixel-high panel is full of shorthand, and Piper reads exactly what it’s given. So the server rewrites each message before it’s spoken:
- Command words become sentences. On the panel, “datetime” is drawn by the Pi Zero itself, so the server has to work out what to say, something like “It’s 20:17 on Thursday 29 October.”
- Shorthand is expanded. The dots that separate items become pauses, arrows become “to”, “ft” and “mi” become feet and miles, “exp” becomes “expected”, and compass points like NE become “north east”.
- Several messages become one sentence. Trains and bins send more than one message at a time. Speaking each one as a separate call doesn’t work, because the second call cuts the first off mid-sentence, so they’re joined into a single utterance.
That last one is easy to miss. On a screen, two messages one after the other are fine; out loud, one stamps on the other.
A pomodoro timer that tells you when to stop
The display also has a pomodoro timer mode, and later the same evening it got a voice too. With Voice on, I hear “Pomodoro started: 25 minutes” at the start, the time left when I pause or resume, “Pomodoro stopped” if I give up, and at the end “Pomodoro complete. Your 25 minutes are up, time for a break.”
This one needed a bit more thought, because the timer runs on the Pi Zero, not on the server. If the server tried to work out when 25 minutes were up, it would get it wrong as soon as I paused. So the Pi Zero now publishes an event over MQTT each time the timer changes state, and the server, which was already listening to the display for its live preview, picks up those events and speaks them. The finish is timed by the display itself, so it’s right after a pause, and it’s announced even if I’ve closed the control page.
We tested it with a one-minute timer: the start and the finish both played on the office speaker.
What I’ve learned
- Check what you already have. Home Assistant already knew every speaker in the house. The whole thing was a container, one integration and a hundred lines of bash.
- Keep the speech local. Piper runs in a small container next to everything else, and the text my scripts generate isn’t sent to a cloud voice service to be turned into audio.
- Make it opt-in and quiet by default. The switch starts Off, the tests never make a sound, and nothing automated speaks unless I’ve turned it on. A house that talks without being asked would get switched off quickly.
- Design for the ear. Shorthand, symbols and back-to-back messages all work on a screen and all fail out loud.
It also gives Claude a new way to reach me: I’ve told it that when an interactive session needs me, it can ask to announce that on the office speaker rather than waiting for me to notice the terminal.
A note on how this post was written: it was drafted by Claude Code from the SpeakerCast and RailData repos, their code history and the notes in my Obsidian vault, and written into WordPress as a draft through the WordPress API. The header image was generated with Google’s Gemini image model. I then reviewed and edited it before publishing.

Leave a Reply