Building the Voice-to-Blog Pipeline, and Fighting Python to Do It
TL;DR
- Built a pipeline that chunks voice memos under Whisper’s 24MB limit, transcribes each piece, and reassembles them into a markdown post.
- Most of the day went to Python environment problems rather than to the pipeline itself.
- The IDE couldn’t resolve imports because its interpreter didn’t match Poetry’s venv.
- Python 3.13 removed
audioop, which PyDub still needs; 3.11 via pyenv fixed it. - The chain that works: pyenv installs,
poetry env usebinds,poetry locksyncs, IDE points at the venv.
What I’m building
A pipeline that turns voice memos into blog posts. Audio in, markdown out, published to my site. Whisper for transcription, chat completion to shape the transcript into something readable.
Transcription has one hard constraint: Whisper caps uploads at 24MB. So the pipeline segments the audio into chunks under that limit, transcribes each, and recombines the text into a coherent whole. A ZSH script was the original entry point. It’s redundant now that Python could invoke it directly, but it was useful while iterating. The script walks directories with os.walk looking for M4A files, which is what macOS voice memos produce.
It’s fancy and I’m loving it. What I didn’t expect was that the pipeline would be the easy part.
The environment ate the day
Four separate traps, all of them environmental rather than logical.
The IDE couldn’t resolve my libraries. poetry env info showed the project’s virtual environment, and the IDE was pointing at a completely different Python. Copy-pasting the venv path into the IDE’s “new interpreter path” option fixed it. The tell that it worked: the project name now appears in the interpreter path.
Spaces in paths. The script targets the current day’s recording directory, and that path has spaces in it. Escaped them manually and access worked.
PyDub’s dependency chain. Audio handling needed nuanced installation. Discovering pipx — isolated environments for Python packages, roughly Node’s npx — pushed me off pip and onto Poetry for dependency management, which smoothed out most of the version incompatibilities in one move.
Python 3.13 removed audioop. This was the real one, and it’s the reason the previous trap existed at all. Syntax warnings led me to a module that 3.13 dropped and PyDub still depends on. Installing 3.11 through pyenv resolved it.
The chain that actually works
Having lost a day to this, here’s the sequence, because skipping any link is where I kept going wrong.
|
|
pyenv shell scopes the version to the current session. Then bind Poetry to that exact interpreter — pyenv which python returns the path for whatever is active:
|
|
This is the link I’d been missing. Without it Poetry picks its own Python and the version I carefully installed does nothing.
|
|
Regenerates the lock file against the new version. Skip it and dependency errors surface later looking unrelated.
Finally, point the IDE at the venv path Poetry prints, something like ~/Library/Caches/pypoetry/virtualenvs/[project-name]-py3.11. That’s the step that fixes unresolved imports.
Cleaning up the wreckage
With the pipeline running I went back to tidy the Python versions I’d accumulated, and made a mess doing it.
pyenv uninstall refused on a version it claimed wasn’t installed — pyenv only manages what pyenv set up, and some of these came from Homebrew. I reached for brew uninstall python@3.13.5 and stopped short, because unlinking something essential mid-cleanup would cost me the rest of the day. Better to leave clutter than to brute-force it without knowing what depends on what.
I did manage one genuine misstep: rm -rf ~/.venvs/whisper while still inside that virtual environment. A terminal refresh sorted it out, but that’s a lesson about deactivating first.
Tools that point at a Python version need re-pointing after a switch — poetry env use and pipx reinstall --all were the ones that mattered. Watchman only matters if specific plugins are involved.
First real test
Then I recorded a note purely to test the pipeline end to end, and it worked. Front matter in place, headings organised, filler words stripped.
That last part is the piece I care about most. The “ums” clutter without adding anything, so they go — but an intentional “um” for emphasis should survive, because it’s doing conversational work. The goal is to keep the raw, exploratory texture of thinking out loud while cutting what’s genuinely noise.
That’s the balance I need to keep watching as this runs unattended: the pipeline is useful precisely as long as it doesn’t sand my voice down into AI-modulated smoothness. I’ll be tweaking those prompts for a while.
Deployment is next — this runs on GitLab, so the API keys need to live in environment variables rather than anywhere near the repo.
ryer.io