CLI and library for the ElevenReader TTS API
Project description
elvenreader
Python library and CLI for the ElevenReader TTS API. Designed for on-demand, throwaway TTS — give it text, get back audio bytes and word-level timings.
Built to drive voice acting for LLM-based tools where every line is dynamically generated and disposable. Auto-deletes library entries after streaming so you don't accumulate junk.
Install
pip install elvenreader
Requires macOS (uses the Keychain for credentials, afplay for CLI playback,
and Chrome for the one-time App Check token capture). The compose_scene
multi-voice primitive additionally requires ffmpeg on PATH.
Quick start
elvenreader login # Stores email + password in Keychain
elvenreader app-check # Launches Chrome, captures App Check token (~7 day lifetime)
Then, from Python:
import asyncio
from elvenreader import ElvenReader
async def main():
async with ElvenReader.from_keychain() as reader:
result = await reader.read("A warm wind blows through the tavern.", voice="brian")
# result.audio: bytes (MP3)
# result.alignment.total_ms: int
# result.alignment.words: list[WordTiming]
# result.alignment.chars: list[CharTiming]
with open("line.mp3", "wb") as f:
f.write(result.audio)
asyncio.run(main())
The read is auto-deleted from your ElevenReader library after streaming. Pass
keep=True to retain it.
Python API
Reading text
async with ElvenReader.from_keychain() as reader:
# One-shot: collect everything into memory
result = await reader.read(text, voice="brian")
result.save_audio("out.mp3")
result.save_alignment_srt("out.srt")
# Streaming: process chunks as they arrive (lower latency)
async for chunk in reader.read_stream(text, voice="brian"):
speaker.feed(chunk.audio)
if chunk.alignment:
update_karaoke(chunk.alignment)
# Convenience wrapper
await reader.read_to_file(text, "out.mp3",
align_srt_path="out.srt",
align_json_path="out.json")
Timings
Every read comes with per-character and per-word timing data extracted from the streaming WebSocket:
result = await reader.read("Hello world.")
for word in result.alignment.words:
print(f"{word.word:10s} {word.start_ms:>5}ms - {word.end_ms:>5}ms")
# Hello 0ms - 325ms
# world. 372ms - 708ms
for c in result.alignment.chars:
print(c.char, c.start_ms, c.duration_ms, c.end_ms)
Use these for karaoke-style highlighting, phoneme-synced animation, subtitle generation, or lining up audio with visual events.
Voices
voices = await reader.list_voices()
voice_id = await reader.resolve_voice("brian") # fuzzy match by name
# Design a new voice from a text description
previews = await reader.create_voice_previews("warm British male, authoritative")
voice = await reader.save_voice_from_preview(
generated_voice_id=previews[0]["generated_voice_id"],
voice_name="Storyteller",
description="warm British male, authoritative",
)
Multi-voice scenes
For dialogue-heavy content (game scenes, audiobooks with character voices),
use compose_scene to generate a single combined MP3 from an ordered list of
segments. Each segment has its own voice.
from elvenreader import SceneSegment, PauseConfig
async with ElvenReader.from_keychain() as reader:
result = await reader.compose_scene(
[
SceneSegment(text="She approached the desk.", voice_id=narrator),
SceneSegment(text="Show me your papers.", voice_id=clerk),
SceneSegment(text="He hesitated.", voice_id=narrator),
],
narrator_voice=narrator,
pause=PauseConfig(
entering_overlay_ms=400, # narrator → character
leaving_overlay_ms=200, # character → narrator
overlay_to_overlay_ms=300,
same_voice_ms=0,
),
loudnorm=True, # normalize volume across all pieces
)
result.audio # bytes (MP3, single file)
result.total_duration_ms # int
result.segments # list[RenderedSegment] — per-segment start_ms/end_ms
result.narrator_alignment # CompletedAlignment from the continuous narrator read
Why this exists. Generating many short clips individually makes each one a
cold start for the voice model — short narrator lines sound inconsistent and
lose prosody continuity. compose_scene sends the entire scene through ONE
narrator read, then slices out the narrator-voiced segments using the per-char
alignment data. Character-voiced segments still get their own calls (you want
those voices distinct). All pieces are concatenated with configurable silence
at speaker transitions and optional loudnorm volume normalization.
Requires ffmpeg on PATH.
Library hygiene
Every reader.read() creates a library entry server-side and auto-deletes it
when streaming completes. If you end up with accumulated entries (e.g. from
crashes):
await reader.clean_reads(max_words=50) # delete short user imports
await reader.clean_reads(delete_all=True) # delete every user import
Auth helpers
from elvenreader import login, logout, refresh_app_check_token
await login("me@example.com", "password") # persists to Keychain
await refresh_app_check_token() # launches Chrome, runs CDP automation
logout() # wipes Keychain entries
CLI
The CLI is a thin wrapper over the Python API — every command has a Python equivalent. Useful for ad-hoc testing.
elvenreader login # Store credentials
elvenreader app-check # Refresh App Check token
elvenreader voices # List voices
# Read text
elvenreader read "Hello" --play # Stream playback
elvenreader read "Hello" --output out.mp3 # Save audio
elvenreader read "Hello" --align-srt out.srt # Save subtitles
elvenreader read --file script.txt -p -o out.mp3 --align-json out.json --voice george
# Library
elvenreader list-reads
elvenreader clean-reads --dry-run
elvenreader clean-reads # delete short imports
elvenreader clean-reads --all # delete every user import
# Voice design
elvenreader create-voice "deep, raspy noir narrator" --name "Detective"
elvenreader logout
Reads auto-delete after streaming unless --keep is passed.
How it works
The streaming API requires two auth artifacts:
- Firebase ID token — obtained from email/password via the standard Firebase auth endpoint. Refreshed automatically.
- App Check token — issued by Firebase App Check to verify requests come from the ElevenReader web app. Not exposed via any public API. We obtain it by automating Chrome via CDP: launching a headless-ish browser, signing in, clicking play on a dummy read, and intercepting the token from the WebSocket subprotocol list.
Tokens live in the macOS Keychain. The App Check token is valid for roughly 7
days; re-run elvenreader app-check to refresh it. The HTTP client shares a
connection pool across the whole process and automatically retries on 429
responses with exponential backoff.
Design notes
- Dataflow-first: the same operations run for every read. Variability lives in values (voice, text, keep flag), not in which branches execute.
- Single HTTP client: one shared
httpx.AsyncClientper process with a retry transport. Every module routes throughhttp.shared_client(). - Single enforcer: auth is encoded into the WebSocket subprotocol list in exactly one place; rate-limit handling is in the transport layer only.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file elvenreader-0.3.0.tar.gz.
File metadata
- Download URL: elvenreader-0.3.0.tar.gz
- Upload date:
- Size: 60.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a0add77ab25d465fabb27643d93bb55e57d7342731a3f6f0725bb1a5b415df2
|
|
| MD5 |
893556e5b63e58981bc89377f5f278ad
|
|
| BLAKE2b-256 |
734942f16411820293651be8f610c70601f9a8f949ba32e043aa82aa976000c0
|
File details
Details for the file elvenreader-0.3.0-py3-none-any.whl.
File metadata
- Download URL: elvenreader-0.3.0-py3-none-any.whl
- Upload date:
- Size: 48.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.19
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ca7a92d7db0fe357171deb4191ed9f4053854cc85d163c06f9329287e659b27
|
|
| MD5 |
3141926d60ac38783466c2285da765bb
|
|
| BLAKE2b-256 |
98cf36f35781c14b0c5089cdbdf759348d7fe1803e6657738bcd7bbdcbabe3a1
|