September 25, 2026
Building Sonara, Part 9: Cloning a Voice
Sonara had twenty built-in voices since chapter four. Chapter nine is where teams can add their own, either by uploading an audio file or…

By Eesha Shahid
2 min read
Sonara had twenty built-in voices since chapter four. Chapter nine is where teams can add their own, either by uploading an audio file or recording one straight in the browser, and where the voices page becomes an actual page instead of a link in the sidebar that went nowhere.
A card that plays itself
Every voice, built-in or custom, shows up as a card with a play button right on it. Click it, and it fetches the audio and plays it, no separate page, no download.
const audioSrc = `/api/voices/${encodeURIComponent(voice.id)}`;
const { isPlaying, isLoading, togglePlay } = useAudioPlayback(audioSrc);const audioSrc = `/api/voices/${encodeURIComponent(voice.id)}`;
const { isPlaying, isLoading, togglePlay } = useAudioPlayback(audioSrc);That route works the same way generation playback does from chapter six: it's authenticated, it checks the voice actually belongs to your org (or is one of the built-in ones), and it streams the file back rather than handing out a link to cloud storage directly. Same pattern, just applied to a second kind of audio now.
Upload or record, same form either way
Creating a custom voice needs an audio sample, and there are two reasonable ways to get one: upload a file you already have, or just record yourself talking. Both live behind two tabs in the same form, and whichever one you use, the result lands in the exact same form field.
The recording tab is the more interesting of the two. It asks for microphone access, then draws a live waveform while you talk, using the same WaveSurfer library from the audio player back in chapter six, just pointed at your microphone instead of a finished file:
const record = ws.registerPlugin(RecordPlugin.create({ scrollingWaveform: true }));
record.renderMicStream(streamRef.current);const record = ws.registerPlugin(RecordPlugin.create({ scrollingWaveform: true }));
record.renderMicStream(streamRef.current);Once you stop, the recording gets wrapped as a real File object and handed to the same upload path a chosen file would take. The form itself never needs to know whether the audio came from a file picker or a microphone, by the time it reaches the submit handler, they're identical.
Search that lives in the URL
The voices page has a search box, and typing in it updates the actual URL, not just some local component state. That's nuqs, wiring a search param directly to a piece of UI state.
The reason this matters more than it sounds: a search someone types can be copied, shared, or bookmarked. Refresh the page mid-search and you're still looking at the same filtered list, because the filter was never just sitting in memory to begin with, it was in the address bar the whole time.
One setup detail worth knowing if you're doing this yourself: nuqs needs a provider wrapped around the app before any of its hooks will work at all. Skip it and every search box in your app throws the moment someone types into it, with an error that doesn't immediately point at the missing provider.
Sonara now has two ways to get a voice into the product: the twenty that ship with it, and however many a team decides to add. That's the whole point of chapter nine.
Next post: Building Sonara, Part 10: Making People Pay
Commits in this chapter: