Skip to main content

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:

  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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

elvenreader-0.3.0.tar.gz (60.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

elvenreader-0.3.0-py3-none-any.whl (48.0 kB view details)

Uploaded Python 3

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

Hashes for elvenreader-0.3.0.tar.gz
Algorithm Hash digest
SHA256 7a0add77ab25d465fabb27643d93bb55e57d7342731a3f6f0725bb1a5b415df2
MD5 893556e5b63e58981bc89377f5f278ad
BLAKE2b-256 734942f16411820293651be8f610c70601f9a8f949ba32e043aa82aa976000c0

See more details on using hashes here.

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

Hashes for elvenreader-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8ca7a92d7db0fe357171deb4191ed9f4053854cc85d163c06f9329287e659b27
MD5 3141926d60ac38783466c2285da765bb
BLAKE2b-256 98cf36f35781c14b0c5089cdbdf759348d7fe1803e6657738bcd7bbdcbabe3a1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page