I built a podcast with a local voice model in about an hour

A small desktop voice synthesizer sends a ribbon of audio across the room to a sturdy little server rack, where headphones wait on a hook.
Artificial IntelligenceLeave a Comment on I built a podcast with a local voice model in about an hour

I built a podcast with a local voice model in about an hour

I wanted a morning briefing I could listen to on my walk, without sending the script to a cloud voice service. About an hour of keyboard time later, I have one. A text-to-speech model on my Mac makes the audio, a small VPS hosts a private podcast feed, and my phone plays it in Overcast.

Two things stuck with me.

Running a local voice model on a Mac is easy

I used Kokoro-82M through MLX-Audio on an Apple Silicon Mac. It lives in its own isolated uv environment, and the server only listens on localhost. I auditioned four British male voices and settled on bm_lewis. Installing it and getting it to speak was far easier than I expected. A script goes in, an MP3 comes out, and nothing leaves my machine.

I can listen right away, tweak the script if a sentence sounds awkward, and run it again. Good voice synthesis used to mean a cloud API. Now it’s something you can try on your laptop this afternoon.

An AI assistant driving a VPS keeps earning its place

A Mac can make the episode, but a podcast has to live somewhere when the Mac is asleep. That’s the VPS. My AI assistant writes and runs the infrastructure as code: an Ansible playbook that deploys a small FastAPI service in Docker behind the gateway I already had. The Mac uploads the finished MP3, the VPS serves a private RSS feed, and Overcast does the rest. The VPS never runs the model.

Why the hour is worth it

Building with AI takes some investment. But once you start putting the pieces together, they compound. The VPS and my infrastructure as code were already in place from earlier projects, so the podcast took an hour. A local voice model, a VPS, infrastructure as code, and an assistant that knows how to use all of it: each new idea gets faster because the last one is already in place.

An hour invested now means I have a pseudo executive assistant briefing me on my walk every morning. That’s a great return.

Try it yourself

Start with the Mac. Hearing one MP3 tells you whether the voice works for your material. Paste this into your favorite AI coding agent:

I have an Apple Silicon Mac and want to run text-to-speech locally, with no cloud API. Inspect my machine first. Then set up Kokoro-82M (mlx-community/Kokoro-82M-bf16) through MLX-Audio (mlx-audio[server,tts], version pinned) in an isolated uv environment so it doesn’t touch my other Python projects. Include misaki[en] and the en_core_web_sm spaCy model, and install espeak-ng and ffmpeg with Homebrew. Run the MLX-Audio server on localhost only, port 8765. Generate an MP3 through its OpenAI-compatible /v1/audio/speech endpoint using a short script as input, the British male voice bm_lewis, lang_code b, and response_format mp3. Check the result with ffprobe and fully decode it with ffmpeg before saving it. Write to a temporary file and rename it only on success. Work in small steps and tell me what each command changes.

If you want it on your phone, give your assistant this next. You’ll need a VPS with Docker and a domain with HTTPS already working:

I have a VPS running Docker behind an nginx gateway that already has a domain and HTTPS. Build a private single-user podcast feed as a small FastAPI service in Docker, deployed with an Ansible playbook. The container listens on localhost, nginx proxies a path prefix to it, and episodes live on a mounted data volume. GET /health returns ok. POST /api/episodes takes a multipart upload (date, duration_seconds, sha256, and the MP3 as audio) with a Bearer publisher token from my Ansible inventory. Recompute the SHA-256 and reject a mismatch, confirm the file is a real MP3 whose duration matches duration_seconds, and reject future dates and dates older than today plus the six previous days. The first upload for a date is immutable: an identical retry succeeds, different content returns 409. Serve RSS at /private/<listener_token>/feed.xml with itunes:block set to yes, one episode per date, and GUIDs like morning-brief:<date>. Serve each MP3 from a tokenized path that supports range requests. Add a CLI inside the container to create, rotate and revoke listener tokens, and store only a hash of each token. Turn off access logging in nginx and uvicorn so tokens don’t end up in logs. Add a daily cron job that deletes expired episodes. Show me the playbook diff and stop before you deploy anything.

Software Architect and Senior Full Stack Developer excited about crafting innovative user experiences with GenAI and Blockchain.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top