# D29. ElevenLabs voices with the author's key: stage directions, dialogue, credits asked before spending (12 Oct 2026)

Narrative modules should stand out by their voices' emotion and rhythm, further than competitors who wire ElevenLabs as one more text-to-speech: authors bring their own ElevenLabs subscription (a key asked only when they first choose an ElevenLabs voice). Read in ElevenLabs' documentation on 12 Oct 2026: the newest model is **Eleven v4** (`eleven_v4`; `eleven_v4_turbo` for real time, half price), 94 languages, settings stability and similarity only (no speed, no style, no SSML); delivery is directed by **audio tags**, free text in square brackets (`[whispers]`, `[sighs]`, `[laughs]`), by punctuation (« … », capitals) and by context; **text to dialogue** says several voices in one request (2,000 characters at most advised) and, with timestamps, gives each turn's segment and every character's time. Pronunciation: IPA between slashes in the text (v4) or dictionaries (`pronunciation_dictionaries_*`, three per request). Voice Library voices are usable directly by id on a paid plan (no slot used; a notice period, often 730 days, if their owner removes them; some cost twice). Paid plans may use the audio commercially and need no attribution; the free plan may not and must credit ElevenLabs.
Measured with a Starter plan key (six scenes and two comparisons through `elabora voice generate`, judged by measures since no one listened in the session): emotions and registers and non-verbal sounds (laughs, sighs, whispering) are clearly rendered; **French directions work as English ones** (`[chuchote]` and `[whispers]` whisper alike); a silence asked mid-line (`[un temps]`) was played once in three, a pace (`[lentement]`) hardly; no direction was spoken aloud. ElevenLabs' character times are coarse (40–80 ms steps) and up to 0.3 s late next to a direction or an ellipsis. Eleven v4 cost about **0.11 credit per character** through the API (373 credits for 3,372 characters), the context texts not billed, a dialogue exactly as much as its lines one by one.
- **Stage directions live in the line's text, in square brackets, in its language** (`[chuchote] Personne ne l'avait vu venir… [un temps] et pourtant…`): what agents and authors already write for these models, read where the delivery changes, kept by the text hash (a new direction makes the line out of date). They are never words: `narrationWords` reads the text with them blanked (offsets kept, so anchors, the aligner and the editor's highlights skip them), captions, the transcript, the player's text and estimated durations drop them (`withoutDirections`), pronunciations never apply inside them. A bracket that does not close on its line or holds more than 80 characters is text. In XLIFF a narration unit's directions are wrapped in `<mrk id="d1" type="elabora:direction">[chuchote]</mrk>`, their brackets inside (a tool that drops the marker loses nothing), with a note telling translators to keep them where the delivery changes and translate or adapt them. A voice that cannot play them (Pocket TTS, ElevenLabs' older models) is given the line without them, and the validator says so (`I_NARRATION_DIRECTIONS_IGNORED`). Rejected: directions as separate fields with offsets (they break on every edit and translation), and parentheses (spoken text uses them). The editor shows them as italic didascalies, never as boxed tokens (a deliberate choice).
- **The provider** (`@elabora/node` `voice/elevenlabs/`): `provider: "elevenlabs"` in the same contract as local voices (`speakers[].voices[<lang>] = { provider, voice, name, settings: { model, stability, similarity } }`, `name` new and information only), a take per generation, the audio written into the module as M4A (it plays without the key). It asks `with-timestamps` in 24 kHz PCM with the take's seed, the line's language and the lines before and after it in the step as context, trims the silence to 50 ms and times words from the characters only when the local aligner (D27) is not installed: the aligner is three to ten times more precise. Voices are searched (the account's, then the library's, most used first, with gender, age, accent, use, a preview to hear free, the price over the model's), looked up by id, never listed whole; a library voice not used yet is set by its id and name.
- **Dialogue.** `generate --dialogue` says each run of a step's lines by two speakers or more as one exchange (`text-to-dialogue/with-timestamps`, the whole run again, in requests of 2,000 characters at most) and cuts it at each turn's segment, at the quietest 20 ms near the boundary (when the language's aligner is on the computer, the whole exchange is aligned first and the cut is in the first silence after a turn's last word, because the service's times can run ahead of its audio: 0.38 s on 13 October for the turns after the first of one French exchange, whose cut clipped an interrupted « et », flagged as swallowed; the same alignment moves the service's own word times onto the audio); each line gets its own take (`dialogue` in its metadata) and words, and the silence the exchange left before each line becomes its `pauseBefore` (one undoable change), so the rhythm survives, including a long silence asked at a turn's start, which a line said alone loses in its trimmed silence. Reliable on the two scenes measured (every word found in its own line by the aligner). Overlaps: ElevenLabs gives contiguous segments, not overlapping voices; an interruption written with « — » is a cut where the next speaker starts and a pause of 0. Lines that really overlap would need a second track: not in the format.
- **Costs are estimated as the service charges, and told as a share of the quota (D30).** `estimateNarration` (`elabora voice estimate`, MCP `elabora_voice_estimate`, engine `voiceEstimate`) gives per line the characters, the list price as a `ceiling` (characters × the model's rate × the voice's) and the `credits` the service is expected to charge: an account is charged a share of that list price, which on a Starter plan is 0.4, whatever the model, found on 13 October 2026 against the history of the requests (`GET /v1/history`, with a key that has Speech history: Read) and by experiments (110 credits): a request is charged `round(0.4 × characters × the model's multiplier)`, 30 characters cost 12 credits on Eleven v4 and Multilingual v2, 6 on v4 Turbo; the same for a line or a dialogue (49 characters, 20 credits), with or without timestamps, context text (`previous_text`, `next_text`: not billed), language, output format, a premade voice or a library one at rate 1, and the stage directions count as the characters they are. The first two days were cheaper, 0.11 (373 credits for 3,372 characters on the 12th, 8 for a line of 74 characters at midnight on the 13th), a promotion, v4's launch discount, which ended between 01:09 and 07:27 UTC on the 13th, and which `GET /v1/models` (`cost_discount_multiplier`, 1 now) did not tell; an estimate learnt from it said 203 credits (0.5 % of a Starter quota, so no approval was asked) for the « Nietzsche » course's first two narrated screens, which were charged 465 (1.2 %). So the estimate is the plan's undiscounted rate, never counting on a discount: 0.4 for a Starter plan (the ledger's average for the model, the plan and the current price period, D36: 0.4005 for Eleven v4 on a Starter plan, so the course's 8 lines are told 466 credits for 465), the list price for a plan not measured until its own requests, which the service reports in each answer's `character-cost` header and the computer keeps with their date, plan and list price (`<cache>/charges.json`, D36), say what it is charged, and a quarter more; a Starter plan's recent requests charged more than 0.4 are followed at once, never past the list price. A line of 70 characters is told 0.07 % of a Starter quota. A library voice priced above its model (two or three times) keeps its rate in `<cache>/voice-rates.json` once a search or a lookup saw it, so that it is priced right after a restart and its charges teach the model's share right; a paid voice's sample (`voiceSample` with `spend`) obeys the same threshold. An estimate also gives the month's quota, read with User: read and counted with what this computer spent since (`quota`), its `percent` of it and `needsApproval`. `generateNarration` runs under 1 % of the quota; from 1 % it spends nothing without the share approved (`approvedPercent`; CLI `--spend <percent>`, exit code 3 and the estimate otherwise; MCP `approvedPercent`, whose description tells agents to ask the author in shares of their quota), an approval covering whatever is told with its figure (« 1 % » up to 1.49 %) and a quarter more than it, as an estimate moves a little between two calls. A generation stops (`overBudget`, the lines said so far kept) once the service charged half as much again as it was allowed (the threshold, the approval or the estimate, the largest): only a service charging far more than measured is stopped. Nothing is paid twice: a line out of date whose earlier take has the same text, voice and delivery gets it back (an undo, a voice set back), and every answer is kept in `<local data>/elevenlabs/cache/` keyed by the request and the address asked (a new take asks with another seed). `voice account` (MCP `elabora_voice_account`, engine `voiceAccount`) reads the plan, the share of the quota used, what a line costs of it, whether the plan may use the library (not the free plan, whose API has the default voices only) and the renewal; `account({ key })` checks a key before the app keeps it.
- **The key is never in a module, a journal, a log, a capture or a message**: shown masked (`sk_…1a2b`), and scrubbed from errors (ElevenLabs echoes a wrong key). The app keeps it in one place, the agents' file below or else the system's secure storage, and touches the keychain only after the author's gesture (amended by D40); it never gives the key to its window. The command line and agents find it in `ELEVENLABS_API_KEY`, else in `<local data>/credentials/elevenlabs-api-key` (0600), written by `elabora voice key set` from its input (never an argument, which shells and process lists keep) or by the app when the author lets agents and the command line use the key. Why a file and not the Keychain for them: an agent's process runs as the author and could read either; what protects the author is a key that can do little (three permissions) and spend little (ElevenLabs' per-key credit quota, which the onboarding advises), and the approval before spending; the Keychain's per-application prompts would interrupt every agent run. Agents read the account and estimates, never the key.
- **Minimal permissions** of the key: **Text to Speech** (speech and dialogue), **Voices: read** (the account's voices and the library), **User: read** (credits left). Not needed: pronunciation dictionaries (the module's pronunciations are applied to the text before it is sent, as for local voices; v4 reads IPA between slashes in a `say`), history, Voices: write (favourites stay in Elabora), anything else. `elabora voice account --check` tries each without spending (an empty text is refused for its emptiness only when the key may make speech).
*Revisit if:* ElevenLabs stops reporting a request's charge (estimates would then rest on the measured 0.11 alone), a silence direction becomes reliable, overlapping voices are needed (a second track), or authors want ElevenLabs' pronunciation dictionaries for lexicons larger than a module's.
