Every post on this blog has a spoken reading, generated text-to-speech (TTS), sitting under the title for anyone who'd rather listen. It's been driven by MiniMax since I added it. Two things have changed recently, and I think they're worth writing down. A caveat first: this is all new and still moving, so treat the specifics as a snapshot that may change.
The short version: there's now a second voice provider, ElevenLabs, and pronunciation dictionaries that work across both. Plus a quiet format change underneath.
Pronunciation dictionaries
The honest problem with generated speech is that it mispronounces exactly the words a technical blog uses most. A voice trained mostly on American English says "raowter" for router, gives cache an extra syllable, and has its own ideas about aluminium. Re-recording isn't an option when the audio is generated, so the fix is to tell the voice how to say specific words.
colophon now takes a pronunciation dictionary, and it applies to whichever provider you're on. The cheapest version is the bundled British one:
generation:
speech:
pronunciation_dict: en_GB # the dictionary shipped with colophon
Or point it at your own file, relative to the site root:
generation:
speech:
pronunciation_dict: pronunciation.yaml
The format is small on purpose. Each entry is a word plus one of two ways to fix it:
pronunciations:
- word: router
ipa: ˈruːtə # IPA: precise; rendered to each provider's phoneme mechanism
- word: nginx
say: engine x # respelling: substituted as plain text, works on any provider
The two knobs are worth understanding:
ipais the International Phonetic Alphabet (IPA) transcription, precise and unambiguous. colophon renders it to each provider's own phoneme mechanism (more on the ElevenLabs side of that below).sayis a plain respelling, substituted as ordinary text before synthesis. It's less exact but completely portable, any voice can read "engine x", no phoneme support required.
One piece of hard-won advice that's baked into the bundled dictionary: keep it small. Overriding a word the voice already says correctly almost always makes it worse. Add a word only after you've heard it get one wrong, and audition the fix before you keep it.
A second voice: ElevenLabs
MiniMax is cheap and fine, but ElevenLabs is the one people reach for when they want a specific, higher-fidelity voice, including a clone of their own. colophon now has an elevenlabs speech driver alongside the MiniMax one. Switching is a config change:
generation:
speech:
provider: elevenlabs
model: eleven_multilingual_v2 # the default; override if you like
voice: "<your ElevenLabs voice id>" # a premade voice, or your own cloned one
api_key: "{env:ELEVENLABS_API_KEY}"
As with every secret in colophon, the key only ever arrives through the environment, never written into the config. It's read from ELEVENLABS_API_KEY (or COLOPHON_ELEVENLABS_API_KEY), so it lives in your keyring or CI secrets exactly like the deploy tokens.
The API key permissions
ElevenLabs lets you scope an API key to specific capabilities, and colophon needs:
- Text to Speech: to generate the readings. This is the only permission you need if you're not using a pronunciation dictionary.
- Pronunciation Dictionaries (read and write): only if you set
pronunciation_dict. ElevenLabs doesn't take IPA inline; it applies pronunciation through an uploaded, versioned dictionary tied to your account. So colophon syncs youripaentries up to ElevenLabs (it lists, creates, and adds or removes rules on a dictionary it manages for you), then references that dictionary when it synthesises. That sync is why the key needs write access to dictionaries, not just speech.
So: a plain ElevenLabs reading needs Text to Speech alone; add Pronunciation Dictionaries (read/write) the moment you want your ipa overrides to take effect. The say respellings, being plain text substitution, need no extra permission on either provider.
The quiet change: WAV now, Opus later
Underneath both providers, the audio format changed. colophon no longer generates MP3; both drivers now return raw PCM (16 kHz mono) which colophon wraps as a WAV file. Two nice consequences fall out of that. The waveform you see under each reading is computed from that same PCM, so there's no second render just to draw it (the old MiniMax path did a separate call for the waveform, which doubled the token spend, that's gone). And WAV is trivially decodable, so the peaks come for free.
The obvious objection is size: WAV is uncompressed. That's the next piece of work, an Opus (SILK) encoder to shrink the readings without a heavyweight dependency. The plumbing already half-knows about it (.opus is a recognised type), it just isn't wired to an encoder yet. For now the trade is deliberate: simple, decoder-free, accurate waveforms, and pay the bytes until the encoder lands.
Putting it together
So the current shape of a reading: pick a provider (MiniMax or ElevenLabs), optionally give it your voice, and hand it a small pronunciation dictionary so it stops saying "raowter". The dictionary is provider-agnostic; the voice and the key are provider-specific; the output is a WAV and a waveform.
All of this is recent and still settling, so the field names and defaults may move. If you're wiring it up from this post, sanity-check against your own colophon.yaml and the current build.
Related
- A Colophon Blog on Cloudflare Pages and R2: where this blog, and its MiniMax media generation, were first set up.
- A Palette from a Prompt: the same MiniMax provider, generating desktop wallpapers instead of speech.
