Contents

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}, a multipart/form-data body with audio: "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"), and seed: "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_format goes in the query, "Formatted as codec_sample_rate_bitrate", with pcm_24000 among 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, and eleven_english_sts_v2, English only (models). "Model recommendation: eleven_multilingual_sts_v2 often outperforms eleven_english_sts_v2 even 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:

  1. The binding's own pitch and timbre, as prosody gives every take in that voice (a voice only direction keeps the length).
  2. 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).
  3. 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.
  4. The binding's effect, if it has one (robot, hollow…).
  5. 48 kHz mono 24-bit FLAC, kept with the show in audio/takes/<sha256>.flac. Its sidecar holds PROVENANCE.md's fields, provider elevenlabs, model eleven_multilingual_sts_v2, and converted: 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) with eleven_multilingual_sts_v2 and 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 (against booth-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.