A take in the character's own voice
Status: built, 2026-10-12. This is the third product audit's finding 8 (0.12 plan item 5,
voice-from-booth). A person could record a line in the booth (m6-recording.md), or have
Kokoro or their own ElevenLabs voice read it (voice-provider.md). They could not act the line
themselves and have it come out in the character's voice, and that is how a small team gets
real timing and feeling into a cartoon. Now a take, the person's own or any take of the line,
can be said again in the voice the character is bound to on ElevenLabs. Its timing and delivery
are kept, and only the voice changes.
What ElevenLabs offers
ElevenLabs' Voice Changer (speech to speech). Each fact comes from ElevenLabs' own pages, read 2026-10-12:
- One call.
POST https://api.elevenlabs.io/v1/speech-to-speech/{voice_id}, amultipart/form-databody withaudio: "The audio file which holds the content and emotion that will control the generated speech",model_id(the model "needs to have support for speech to speech"), andseed: "If specified, our system will make a best effort to sample deterministically".remove_background_noise"will remove the background noise from your audio input using our audio isolation model".output_formatgoes in the query, "Formatted as codec_sample_rate_bitrate", withpcm_24000among its values; "PCM and WAV formats with 44.1kHz sample rate requires you to be subscribed to Pro tier or above". The answer: "200: The generated audio file", or a 422 "Validation Error" (convert). - Its models.
eleven_multilingual_sts_v2, "State-of-the-art multilingual voice changer model (Speech to Speech)", 29 languages, andeleven_english_sts_v2, English only (models). "Model recommendation:eleven_multilingual_sts_v2often outperformseleven_english_sts_v2even for English content" (voice changer). - What it keeps. It changes a voice "while preserving the tone and delivery of the original voice" and "Accurately follows the input speaking cadence"; "the input sample determines the output's accent". The same page advises: "Be expressive in your recordings", and "A quiet recording may hinder AI recognition" (voice changer guide).
- Its limits. "The audio file must be less than 50mb in size" and "cannot exceed 5 minutes in length", "300 seconds or 5 minutes of audio. This is the same across all subscriptions". Input may be "MP3, M4A, FLAC, OGA, OGG, WAV" (or a video) (voice changer guide). "Maximum segment length: 5 minutes — split longer recordings into chunks" (voice changer).
- Its price. "API usage is billed in US dollars, not credits"; "Voice Changer and Voice Isolator $0.12 per minute" (API pricing). "The cost for a voice changer generation is solely duration-based at 1000 characters per minute" (voice changer guide). Neither page says how part of a minute is counted.
- What the audio may be used for. The same terms as a line read aloud: "All paid plans include a commercial license, provided you're not using Beta Services." "The free plan does not include a commercial license and cannot be used for any commercial purpose", and free-plan work must carry "elevenlabs.io" or "11.ai" in its title. The page names no rule for the voice changer alone: "Some products and features may have additional terms and conditions" (publishing).
What Reelwright sends
The take as it is kept: its 48 kHz mono 24-bit FLAC (audio/takes/<sha256>.flac), at most
two minutes long. The booth refuses anything longer, so a take is always under ElevenLabs'
five minutes and 50 MB. It goes to the voice the character is bound to on ElevenLabs (its
elevenlabs binding, from the character studio's Voice) with eleven_multilingual_sts_v2. The
seed is 1 the first time a take is said in a voice and 2 the next. The answer is asked for as
pcm_24000, which every plan may ask for, as the read lines are. The background is left as it
is: the booth's own Clean takes a room out and costs nothing.
POST /v1/speech-to-speech/<voice id>?output_format=pcm_24000
xi-api-key: <the person's key>
model_id=eleven_multilingual_sts_v2 seed=1 audio=@take.flac (audio/flac)
The price. A take is priced before it is sent at every second begun, in whole characters
of ElevenLabs' 1,000 a minute. A 2.6 s take is 3 seconds, 50 characters, $0.0060, so the
estimate is never under the bill however ElevenLabs counts a part. What it then cost is what
ElevenLabs billed (its character-cost header × $0.00012). The price is held against the
show's budget on the ledger every voice spends from (voice.<episode>), as a line read aloud
is, and on the web service against the person's own cap too. A take the caps refuse is not
sent (voice.budget, with what to do).
What comes back is made a take as the take it was made from is, and as a line read in that voice is:
- The binding's own pitch and timbre, as prosody gives every take in that voice (a voice only direction keeps the length).
- Fitted to the take, to the sample: cut at its end or padded there, so the line keeps its
length, its cut and the timing the actor gave it. How long the answer was is kept
(
answeredSec). - At the take's level: the loud parts' RMS of the take it was made from, under -1 dBFS, so a whisper stays a whisper.
- The binding's effect, if it has one (robot, hollow…).
- 48 kHz mono 24-bit FLAC, kept with the show in
audio/takes/<sha256>.flac. Its sidecar holds PROVENANCE.md's fields, providerelevenlabs, modeleleven_multilingual_sts_v2, andconverted: the take it came from, the voice and its name, the seed, when, what it cost, the plan and whether it may be used commercially, the free plan's warning, how long the answer was, and who acted it. It is kept with the show, not in the gitignored store, because it was paid for and asking again makes another one.
It has no words sidecar. Its words are timed from Rhubarb's reading of it, as a recording's
are, so Rhubarb runs again on it when the episode compiles. The mouths a person drew over
the line (LineCue.mouth, the mouth lane) are kept by the line's words, and the words have
not changed, so every one is kept. Each is placed on the new take where its words are, by the
same rule a new take of the same words follows (compile/mouths.ts). Since the timing is
kept, it lands where it was.
Choosing and going back
Saying a take again makes the new take the line's take: a change of the person's own, "Play Ada
Lovell's take of sc01/l01 in Ottoline Brisk's voice (George)", undone with Ctrl+Z; its toast
says "Ottoline Brisk's line now plays Ada Lovell's take in Ottoline Brisk's voice (George)". The
take it was made from stays, listed just before it, and choosing either is takes.choose as for
any take. Saying it again (from the take, or from a take said already, which goes back to its
take) asks for another seed and keeps both. A take said again is deleted as a recording is, but
not while the line plays it. Deleting the recording leaves what was made from it.
A line whose take is an actor's, recorded or said again in the character's voice, is marked as
recorded on the timeline (recorded) and in the booth's list of lines.
When it cannot
Each case is said in plain words, with what to do, and nothing is sent:
| Case | What the person sees |
|---|---|
| No ElevenLabs voice for the character, and no key | Nothing: no button, as before. Kokoro stays the free, offline voice. |
| A key, but no voice for the character | "Ottoline Brisk has no ElevenLabs voice yet: give Ottoline Brisk one in the character studio's Voice, then play this take in it" (voice.no_cloud_voice) |
| A voice, but no key here | "Ottoline Brisk's ElevenLabs voice cannot be used here: there is no ElevenLabs API key on this computer (save it in Studio's Settings › Voices…)" (voice.key) |
| ElevenLabs refuses the key | "ElevenLabs refused the key: Invalid API key" (voice.refused), from the free check before any price |
| The budget | the cap's own words, then "raise the show's budget in Show settings, or keep the take as it is" (voice.budget) |
| A free plan | said before sending, and on the take after: "On the free plan, ElevenLabs' voices may not be used commercially, and what you publish must say "elevenlabs.io" in its title (…)" |
Protocol 1.6 (schemas/v1/session.schema.json)
takes.convert {episode, scene, line, take, estimate?} |
With estimate, {cost, warning?}: the voice, the take's seconds, the most it costs (spent false), and what the plan does not allow. That costs nothing (the account's plan and voices, both free calls). Without, takes.list's fields after it, take, change, cost (what was billed, spent true) and warning |
takes.list / every takes.* result: convertible |
{provider, ready, reason?}: whether the line's takes can be said in its character's cloud voice, and why not. Absent when the character has none and there is no key |
TakeInfo: from, voiceName, usd, warning |
A take said again is a synthetic take (provider elevenlabs) with the take it came from, so a Studio older than 1.6 lists it as one more take to choose |
In Studio (desktop and web alike)
- The booth. After a take, an actor's take's row offers Play it in Ottoline Brisk's voice (George): the voice is named one way wherever the booth says it, the character first and the voice by the account's name for it after, so nobody wonders who George is (without the account's name, "Ottoline Brisk's voice"). Studio asks the core for the estimate, then asks the person first, as it asks before any spending: "Say it in Ottoline Brisk's voice (George) for at most $0.0060?", saying the take is sent to ElevenLabs, paid by their account, counted against the show's budget, and kept; Cancel has the keyboard, and the free plan's warning follows when there is one. Send it, and the new take plays for the line, listed just under the take it came from: "In Ottoline Brisk's voice (George) · from Take 1 · Ada Lovell", its details ending in what it cost, and a "Not for commercial use" badge on a free plan; the status line and the toast say the same. Each take's Play compares them, and Use this take on either keeps it. When it cannot be done, the button's tooltip says why, and pressing it says it in full in the booth's error bar, with what to do.
- The line's row. On the timeline, a line an actor's take plays (marked with the mic) has Play it in Otto's voice in its inspector too (the character as the timeline names them; the timeline is not told the voice's name, which the booth's question then says): the booth opens on the line and does the same for the take it plays.
- Narrow windows. "Play it in …'s voice" has a line of its own under the take, as wide as the take, its words cut short when the booth is narrow, and the rest of what this adds wraps with the take's own controls, so a row is no wider for it. With less than 640 px of room (a 600 to 720 px window with the rail folded leaves it about 535) the booth puts who reads, the microphone and the lines above the prompter, a few lines tall, and narrows its margins, Record, the meter and the picture: beside the prompter they held it at 629 px and cut the takes off at the right. A phone's window shows no booth: it needs a bigger screen.
- The web. The same Studio, through the person's own key, as voice-provider: the service keeps it sealed and hands it to their daemon, which uses it and nothing on the machine.
Tests
core/test/voice-from-booth.test.ts, against a stand-in ElevenLabs (core/test/support/fake-eleven.ts, which answers speech to speech with a tone of the voice's own pitch that follows the take's loudness every 10 ms). A recorded take is sent as kept (the form's file is its bytes) witheleven_multilingual_sts_v2and seed 1. What comes back is as long as the take to the sample, loud where the actor spoke, at their level, in the voice's pitch, and chosen as a change. The estimate sends nothing and the bill goes on the ledger. The budget refuses, nothing is sent and nothing spent. Switching back, Ctrl+Z, another seed and deleting all work. A free plan is warned before and after. No voice, no key and a refused key are each said, with nothing kept or spent, and the demo as it comes offers nothing. Rhubarb reads the new take and the drawn mouth is kept at the same frames. A recorded conversion replays to the same take. The live transport sends a multipart form with the key in its header and reads audio back.engine/tests/ui/rw_booth_voice_test.gd(againstbooth-voice.json, recorded from the real core): the button, the estimate asked first, the question (Cancel sends nothing), the call, the new take listed under its take, and the refusals said.tools/web/voice-booth-check.ts: the web edition at 1440 and 640 wide against the real service, with the person's own (fake) key saved there and the stand-in ElevenLabs over HTTP.
The owner's check
One take with a real key, at most $0.05, when it is time to see ElevenLabs' own answer:
reelwright voices change take.flac --voice <id> --record ~/rw-voice-change. It says what it
sends and what it costs first. The folder it records holds the call and its answer for replay.