Metadata-Version: 2.5
Name: speechready
Version: 0.2.0
Summary: Prepare text for text-to-speech: clean LLM output, verbalize ambiguous tokens, compile pauses to each provider's syntax, chunk under model limits.
Project-URL: Homepage, https://github.com/berkerbatur/speechready
Project-URL: Documentation, https://berkerbatur.github.io/speechready/
Project-URL: Changelog, https://github.com/berkerbatur/speechready/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/berkerbatur/speechready/issues
Author: Berker Batur
License-Expression: MIT
License-File: LICENSE
Keywords: ssml,text-normalization,text-to-speech,tts,voice-agents
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Requires-Python: >=3.10
Requires-Dist: pyyaml>=6
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# speechready (Python)

Prepare text for text-to-speech. See the [repository README](https://github.com/berkerbatur/speechready) for the project overview.

```
pip install speechready
```

Python 3.10 or newer; the only dependency is PyYAML. Try it without installing at [berkerbatur.github.io/speechready/play](https://berkerbatur.github.io/speechready/play/).

## Usage

```python
from speechready import prepare

result = prepare(
    "**Step 1:** run `npm install`. [pause 1s] Then read https://example.com/docs.",
    provider="elevenlabs",
    model="eleven_multilingual_v2",
    locale="en-US",
)

result.text     # 'Step 1: run npm install. <break time="1s" /> Then read example.com.'
result.chunks   # the same text split under the model's request limit
for change in result.changes:
    print(change.rule, repr(change.before), "->", repr(change.after))
```

The verbalize stage rewrites only what models misread: phone numbers and long digit strings (digit by digit), numeric dates in the locale's order, 24-hour times, currency amounts, percent and unit abbreviations, small fractions, number ranges, years in context, decades, ordinals, and identifiers such as `AB12X9`. Ordinary numbers, decimals and pronounceable names like `COVID-19` are left alone.

Turkish (`locale="tr-TR"`) adds what foreign models get wrong in Turkish: suffixes on numerals and abbreviations are joined to the spoken word with vowel harmony and buffer consonants (`3'te` → üçte, `1990'da` → bin dokuz yüz doksanda, `100 TL'ye` → yüz liraya, `1/4'ü` → çeyreği), `%50` → yüzde elli, dotted thousands `1.250` → bin iki yüz elli, decimal commas `3,14` → üç virgül on dört, `3. madde` → üçüncü madde, dates `27.10.2026` → yirmi yedi Ekim iki bin yirmi altı, and abbreviations such as `vb.`, `Dr.`, `Üniv.`, `Cad.` read in full.

Inline intents in the input: `[pause 500ms]`, `[pause 1.5s]`, `[spell AB12]`, `[em really]`. An SSML `<break time="..."/>` you already have is read as a pause intent and re-rendered for the target.

`provider` and `model` pick a profile from `spec/profiles`; with neither, the `plain` profile renders pauses as punctuation and returns one chunk. Pass `profile=load_profile("azure-neural")` to pick one directly and `limit=` to override the request limit.

Everything a caller may want to decide differently is an `Options` field, and the defaults are what the evaluation ran with:

```python
from speechready import Options, prepare

prepare(text, provider="openai", options=Options(
    code_blocks="keep",        # drop (default) | keep: read fenced code aloud | mention: one sentence stands in for it
    code_block_mention=None,   # what `mention` says; None = "See the code block." / "Kod bloğuna bakın."
    tables="rows",             # rows (default, each row a comma list) | drop
    urls="domain",             # domain (default) | drop | keep
    emails="keep",             # keep (default) | drop
    emoji="drop",              # drop (default) | keep
    emphasis="strip",          # strip (default) | intent: **bold** becomes an emphasis intent
    list_numbers="keep",       # keep (default, "1. Open the app") | drop
    fractions="spoken",        # spoken (default: "one half", "yarım") | division ("one over two", "bir bölü iki")
    foreign_acronyms=True,     # Turkish: npm, css, sql read with English letter names (en pi em)
    terminal_punctuation=".",  # added to a block that ends without one
))
```

Phone numbers follow each language's habit: English reads digit by digit with a pause between groups, Turkish reads each group as a number (`0532 123 45 67` → sıfır beş yüz otuz iki, yüz yirmi üç, kırk beş, altmış yedi).

Pronunciation lexicon, one file for every vendor:

```python
from speechready import Lexicon, prepare, load_profile

lex = Lexicon.load("examples/lexicon.en.yaml")      # entries with ipa, say or spell
prepare("Restart Nginx.", provider="azure", lexicon=lex).text
# 'Restart <phoneme alphabet="ipa" ph="ˈɛndʒɪnˈɛks">Nginx</phoneme>.'
prepare("Restart Nginx.", provider="openai", lexicon=lex).text
# 'Restart engine X.'            (no phoneme tags on OpenAI, so the respelling is used)

lex.to_pls()                                         # PLS XML for ElevenLabs, Azure and Polly uploads
lex.to_google_json()                                 # Google Cloud TTS custom pronunciations
lex.export_for(load_profile("elevenlabs-v2"))        # the right one for a profile, or None
```

An inline `[ipa Nginx ˈɛndʒɪnˈɛks]` tag does the same for one word without a file.

## Voice agents

Two frameworks take a text transform between the LLM and the TTS. Both hooks are plain callables, so they need nothing beyond `speechready`:

```python
# LiveKit Agents 1.8+: sentences are released as they are confirmed in the token stream
from speechready.integrations.livekit import text_transform
session = AgentSession(..., tts_text_transforms=[text_transform(provider="elevenlabs", model="eleven_turbo_v2_5")])

# Pipecat 1.14+: runs on each sentence the TTS service aggregates; register it on the TTS, not on LLMTextProcessor
from speechready.integrations.pipecat import text_transform
tts = ElevenLabsTTSService(..., text_transforms=[("*", text_transform(provider="elevenlabs", model="eleven_turbo_v2_5"))])
```

Both take the arguments of `prepare` (`provider`, `model`, `locale`, `options`, `lexicon`). Fenced code blocks that arrive over several tokens or pieces are held until the closing fence and handled as one block, so `code_blocks="drop"` and `"mention"` work in streams too. Any other framework can do the same with `Segmenter`:

```python
from speechready import Segmenter

seg = Segmenter(locale="en-US")
for token in llm_stream:
    for sentence in seg.push(token):
        tts.say(prepare(sentence, provider="openai").text)
for sentence in seg.flush():
    tts.say(prepare(sentence, provider="openai").text)
```

## Development

```
cd python
python -m pip install -e ".[dev]"
pytest
```

Tests run every fixture in `../spec/fixtures`, which the JavaScript package must pass as well.
