Skip to main content

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:

  1. Firebase ID token — obtained from email/password via the standard Firebase auth endpoint. Refreshed automatically.
  2. 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.AsyncClient per process with a retry transport. Every module routes through http.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

Metadata

Release files for elvenreader 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for elvenreader 0.3.0
File Size Uploaded Provenance
elvenreader-0.3.0.tar.gz 60.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for elvenreader 0.3.0
File Interpreter ABI Platform
elvenreader-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 108.5 kB

Release files / elvenreader-0.3.0.tar.gz

Download URL elvenreader-0.3.0.tar.gz
Size 60.4 kB
Tags Source
SHA-256 checksum
How to use checksums
7a0add77ab25d465fabb27643d93bb55e57d7342731a3f6f0725bb1a5b415df2
BLAKE2b-256 checksum
How to use checksums
734942f16411820293651be8f610c70601f9a8f949ba32e043aa82aa976000c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.19

Release files / elvenreader-0.3.0-py3-none-any.whl

Download URL elvenreader-0.3.0-py3-none-any.whl
Size 48.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8ca7a92d7db0fe357171deb4191ed9f4053854cc85d163c06f9329287e659b27
BLAKE2b-256 checksum
How to use checksums
98cf36f35781c14b0c5089cdbdf759348d7fe1803e6657738bcd7bbdcbabe3a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.19

Security for elvenreader 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page