tts
Text-to-speech on Ubuntu using the Kokoro neural TTS model โ speak text aloud or render it to an audio file.
Downloads: 10 ยท ID: 1551ab22aa9e8dae53000000
Text-to-speech on Ubuntu using the Kokoro neural TTS model โ speak text aloud or render it to an audio file.
Downloads: 10 ยท ID: 1551ab22aa9e8dae53000000
<!-- FILE: tts_skill.md -->
# TTS Skill โ Kokoro (Neural TTS)
Uses **Kokoro-82M**, an open-weight neural TTS model with 82M parameters.
Lightyears ahead of the old `spd-say` robot voice.
## Quick Start
```
python speak.py "Text to speak"
```
## Usage
```
python speak.py [options] [text...]
```
| Option | Default | Description |
|--------|---------|-------------|
| `-v VOICE` | `af_heart` | Voice name (see below) |
| `-l LANG` | auto | Language code (inferred from voice if omitted) |
| `-s SPEED` | `1.0` | Speed multiplier |
| `-o FILE` | play only | Save WAV to file instead of playing |
## Voices
### American English (lang_code='a')
| Voice | Description |
|-------|-------------|
| `af_heart` | Female โ warm, natural (default) |
| `af_bella` | Female โ expressive |
| `af_jessica` | Female |
| `af_nicole` | Female |
| `af_sarah` | Female |
| `af_sky` | Female |
| `am_adam` | Male |
| `am_michael` | Male |
| `am_liam` | Male |
### British English (lang_code='b')
| Voice | Description |
|-------|-------------|
| `bf_emma` | Female |
| `bf_isabella` | Female |
| `bm_george` | Male |
| `bm_lewis` | Male |
### Other Languages
| Code | Language | Use voice prefix |
|:----:|:---------|:-----------------|
| `e` | Spanish | `es_` / `em_` |
| `f` | French | `ff_` / `fm_` |
| `i` | Italian | `if_` / `im_` |
| `p` | Brazilian Portuguese | `pf_` / `pm_` |
| `j` | Japanese | `jf_` / `jm_` |
| `z` | Mandarin Chinese | `zf_` / `zm_` |
| `h` | Hindi | `hf_` / `hm_` |
## Examples
```bash
# Speak directly (plays audio)
python speak.py "Hello world"
# Pipe text
cat story.txt | python speak.py
# Different voice + speed
python speak.py -v am_michael -s 1.1 "Faster male voice"
# Save to file (no playback)
python speak.py -v bf_emma -o output.wav "British female voice"
# British English explicitly
python speak.py -l b -v bm_george "Good morning, governor"
```
## Playback
For audio playback, the script needs either:
- **PipeWire:** `pw-play` (usually pre-installed on modern Ubuntu)
- **ALSA:** `aplay` (part of `alsa-utils`)
Install with: `sudo apt install alsa-utils` (for aplay fallback)
If neither is found, the script saves the WAV file and tells you where it is.
## Installation
The skill uses a dedicated virtual environment at `~/skills/tts/.venv/`
created with `uv` (fast Python package manager). Key dependencies:
- `kokoro>=0.9.4` โ the TTS model
- `torch` โ PyTorch (CPU-only, no CUDA needed)
- `misaki[en]` โ G2P (grapheme-to-phoneme) for English
- `spacy` + `en_core_web_sm` โ text processing pipeline
- `espeak-ng` โ phoneme fallback (system package)
- `soundfile` โ WAV file I/O
First-run will download the model (~300MB) from HuggingFace to
`~/.cache/huggingface/hub/`. Subsequent runs use the cached copy.
<!-- FILE: speak.py -->
#!/usr/bin/env python3
"""Kokoro TTS skill โ neural TTS with 82M params, lightyears beyond spd-say.
Usage:
python speak.py "Text to speak"
echo "Pipe text" | python speak.py
python speak.py -v af_bella "With a different voice"
python speak.py -s 1.2 "Faster speech"
python speak.py -o output.wav "Save to file, don't play"
Voices: https://huggingface.co/hexgrad/Kokoro-82M/tree/main/voices
American English (lang_code='a'): af_heart, af_bella, af_jessica, af_nicole, af_sarah, af_sky
am_adam, am_michael, am_liam
British English (lang_code='b'): bf_emma, bf_isabella, bm_george, bm_lewis
Spanish (e), French (f), Italian (i), Portuguese (p), Japanese (j), Chinese (z), Hindi (h)
"""
import argparse, sys, tempfile, subprocess, os
from pathlib import Path
def play_wav(path: str):
"""Play a WAV file using aplay (ALSA) or pw-play (PipeWire)."""
for player in ['pw-play', 'aplay']:
if subprocess.run(['which', player], capture_output=True).returncode == 0:
subprocess.run([player, path])
return
print(f"WAV saved at {path} โ no audio player found (install aplay or pw-play)", file=sys.stderr)
def main():
parser = argparse.ArgumentParser(description='Kokoro TTS โ neural text-to-speech')
parser.add_argument('text', nargs='*', help='Text to speak (or pipe via stdin)')
parser.add_argument('-v', '--voice', default='af_heart',
help='Voice (default: af_heart). See skill doc for full list.')
parser.add_argument('-l', '--lang', default=None,
help='Language code: a=US, b=UK, e=ES, f=FR, i=IT, p=PT, j=JA, z=ZH, h=HI')
parser.add_argument('-s', '--speed', type=float, default=1.0,
help='Speech speed multiplier (default: 1.0)')
parser.add_argument('-o', '--output', type=Path, default=None,
help='Save to WAV file instead of playing')
args = parser.parse_args()
# Read text
text = ' '.join(args.text)
if not text and not sys.stdin.isatty():
text = sys.stdin.read().strip()
if not text:
print("Usage: python speak.py <text> (or pipe text via stdin)", file=sys.stderr)
sys.exit(1)
# Infer lang from voice if not specified
lang = args.lang or args.voice[0]
# Import kokoro (lazy import for faster --help)
try:
from kokoro import KPipeline
import soundfile as sf
except ImportError as e:
print(f"ERROR: Missing TTS dependency: {e}", file=sys.stderr)
print(file=sys.stderr)
print("The TTS skill requires the kokoro venv. Set it up:", file=sys.stderr)
print(f" cd ~/skills/tts", file=sys.stderr)
print(f" python3 -m venv venv", file=sys.stderr)
print(f" source venv/bin/activate", file=sys.stderr)
print(f" pip install kokoro soundfile torch", file=sys.stderr)
sys.exit(1)
pipeline = KPipeline(lang_code=lang, repo_id='hexgrad/Kokoro-82M')
generator = pipeline(text, voice=args.voice, speed=args.speed, split_pattern=r'\n+')
# Output
if args.output:
# Save to specified file
all_audio = []
for gs, ps, audio in generator:
all_audio.append(audio)
import torch
full = torch.cat(all_audio) if all_audio else torch.tensor([])
sf.write(str(args.output), full.numpy(), 24000)
print(f"Saved {len(full)/24000:.1f}s audio to {args.output}")
else:
# Play directly
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
tmp_path = f.name
all_audio = []
for gs, ps, audio in generator:
all_audio.append(audio)
import torch
full = torch.cat(all_audio) if all_audio else torch.tensor([])
sf.write(tmp_path, full.numpy(), 24000)
play_wav(tmp_path)
os.unlink(tmp_path)
if __name__ == '__main__':
main()