Metadata-Version: 2.4
Name: md2speech
Version: 0.1.2
Summary: Convert Markdown and plain text files to speech audio using Kokoro-82M (free, local TTS)
Project-URL: Homepage, https://github.com/evelasko/md2speech
Project-URL: Repository, https://github.com/evelasko/md2speech
Project-URL: Issues, https://github.com/evelasko/md2speech/issues
Project-URL: Documentation, https://github.com/evelasko/md2speech#readme
Author: Enrique Velasco
License-Expression: MIT
License-File: LICENSE
Keywords: audio,kokoro,markdown,text-to-speech,tts
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Requires-Python: <3.13,>=3.11
Requires-Dist: inflect>=7.0
Requires-Dist: kokoro>=0.9
Requires-Dist: numpy<2.8,>=2.0
Requires-Dist: pydub>=0.25
Requires-Dist: scipy>=1.14
Requires-Dist: soundfile>=0.12
Requires-Dist: torch>=2.4.1
Requires-Dist: tqdm>=4.66
Provides-Extra: dev
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# md2speech

Convert Markdown and plain-text files to speech audio using **Kokoro-82M** — a free, local, offline text-to-speech engine. No API keys, no cloud services.

## Features

- **Free and offline** — Kokoro-82M runs locally via PyTorch; model weights download once from Hugging Face
- **Markdown and plain text** — reads `.md`, `.markdown`, and `.txt` files
- **MP3 by default** — also supports WAV, OGG, and FLAC
- **Smart text normalization** — expands abbreviations, numbers, currency, percentages, and times for natural speech
- **Long-document support** — synthesizes paragraph-by-paragraph with configurable pauses
- **CLI and Python API** — use from the terminal or import as a library
- **Audio post-processing** — peak normalization, silence padding, and resampling to 44.1 kHz

## Requirements

| Requirement | Notes |
|-------------|-------|
| **Python 3.11 or 3.12** | Required for Kokoro/torch compatibility |
| **ffmpeg** | Required for MP3, OGG, and FLAC export (not needed for WAV) |
| **~500 MB disk** | Kokoro-82M model weights (downloaded on first synthesis) |
| **Network (first run)** | Model downloads from Hugging Face automatically |

## Installation

### With pipx (recommended)

Install the CLI globally and keep it isolated from other Python projects:

```bash
brew install pipx ffmpeg   # macOS; ffmpeg required for MP3
pipx ensurepath            # add ~/.local/bin to PATH (restart terminal once)

pipx install md2speech
md2speech --help
```

Upgrade later:

```bash
pipx upgrade md2speech
```

### From PyPI

```bash
pip install md2speech
```

### From source

```bash
git clone https://github.com/evelasko/md2speech
cd md2speech
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
```

### With uv

```bash
cd md2speech
uv sync
uv pip install -e ".[dev]"
```

### Install ffmpeg

ffmpeg must be on your `PATH` for compressed audio formats.

| Platform | Command |
|----------|---------|
| **macOS** | `brew install ffmpeg` |
| **Ubuntu/Debian** | `sudo apt install ffmpeg` |
| **Windows** | `winget install ffmpeg` or `choco install ffmpeg` |

> **First run:** The Kokoro-82M model (~hundreds of MB) downloads automatically from Hugging Face on the first synthesis. Subsequent runs use the cached weights.

## Quick Start

```bash
# Convert article.md → article.mp3 in the same folder
md2speech article.md

# Explicit output path and WAV format (no ffmpeg needed)
md2speech chapter.txt -o ~/Audio/chapter.wav --format wav

# Long document with a specific voice and speed
md2speech book.md -o book.mp3 --voice am_adam --speed 0.95

# British English
md2speech article.md --lang b --voice bf_emma

# Brazilian Portuguese (language inferred from voice if omitted)
md2speech texto.md --voice pm_alex -o texto.mp3

# List supported languages and voices
md2speech --list-languages
md2speech --list-voices
md2speech --list-voices en-gb

# Verbose mode with synthesis progress
md2speech notes.md -V
```

## CLI Reference

| Argument | Flag | Default | Description |
|----------|------|---------|-------------|
| `input` | — | *(required)* | Path to `.md`, `.markdown`, or `.txt` file |
| `--output` | `-o` | Same dir/name as input | Output audio file path |
| `--format` | `-f` | `mp3` | Output format: `mp3`, `wav`, `ogg`, `flac` |
| `--voice` | — | `af_heart` | Kokoro voice ID (see `--list-voices`) |
| `--speed` | — | `1.0` | Speech speed multiplier |
| `--lang` / `--language` | — | inferred from voice | Language code or alias (`a`, `en-gb`, `pt-br`, …) |
| `--list-languages` | — | — | Print supported languages and exit |
| `--list-voices` | `[LANG]` | — | Print available voices, optionally filtered by language |
| `--no-normalize` | — | off | Skip text normalization |
| `--paragraph-pause` | — | `0.4` | Seconds of silence between paragraphs |
| `--verbose` | `-V` | off | Enable debug logging and progress bar |

**Default output path:** When `--output` is omitted, the output file is written next to the input with the same basename and the chosen format extension (e.g. `article.md` → `article.mp3`).

## Python API

### High-level

```python
from md2speech import synthesize_file

result = synthesize_file(
    "article.md",
    output_path="article.mp3",
    output_format="mp3",
    voice="bf_emma",
    lang="b",                    # or "en-gb"
    speed=1.0,
)
print(result.path)              # Path to written audio
print(result.duration_seconds)  # float
```

### Advanced building blocks

```python
from md2speech import (
    read_document,
    extract_plain_text,
    TTSEngine,
    AudioWriter,
    list_languages,
    list_voices,
    resolve_voice_and_lang,
)
from md2speech.synthesize import synthesize_text, synthesize_long_text
from md2speech.normalize import normalize_text

# Read and extract speakable text
text = read_document("notes.md")
plain = extract_plain_text("# Hello\n\n**World**")

# Normalize for TTS
spoken = normalize_text("Dr. Smith paid $12.50 at 3:45 PM")

# Low-level synthesis
engine = TTSEngine(lang_code="a")
audio = synthesize_long_text(text, voice="af_heart", engine=engine)

# Post-process and write
writer = AudioWriter()
processed = writer.process(audio, orig_sr=24000)
writer.write_audio(processed, "output.mp3", format_name="mp3")
```

Run as a module:

```bash
python -m md2speech article.md -o article.mp3
```

## Languages & Voices

Kokoro supports 9 languages. Use `--lang` / `--language` to set the language and `--voice` to pick a speaker. If `--lang` is omitted, the language is inferred from the voice prefix (e.g. `bf_emma` → British English).

| Code | Language | Default voice | Voices |
|------|----------|---------------|--------|
| `a` | American English | `af_heart` | 20 (`af_*`, `am_*`) |
| `b` | British English | `bf_emma` | 8 (`bf_*`, `bm_*`) |
| `e` | Spanish | `em_alex` | 3 |
| `f` | French | `ff_siwis` | 1 |
| `h` | Hindi | `hf_alpha` | 4 |
| `i` | Italian | `if_sara` | 2 |
| `p` | Brazilian Portuguese | `pm_alex` | 3 |
| `j` | Japanese | `jf_alpha` | 5 |
| `z` | Mandarin Chinese | `zf_xiaoxiao` | 8 |

**Aliases:** `en-us`, `en-gb`, `spanish`, `french`, `hindi`, `italian`, `pt-br`, `japanese`, `chinese`, and more.

```bash
md2speech --list-languages
md2speech --list-voices b
```

If only `--lang` is set (no `--voice`), the default voice for that language is used. Voice and language must match — `md2speech doc.md --lang b --voice af_heart` will error with a list of valid British voices.

> **Note:** Japanese (`j`) and Mandarin Chinese (`z`) require extra packages: `pip install "misaki[ja]"` or `pip install "misaki[zh]"`.

### Recommended voices

| Voice ID | Gender | Language | Character |
|----------|--------|----------|-----------|
| `af_heart` | Female | American English | Warm, clear — **default** |
| `am_adam` | Male | American English | Deep, steady |
| `af_bella` | Female | American English | Soft |
| `am_michael` | Male | American English | Conversational |
| `bf_emma` | Female | British English | Clear British accent |
| `bm_george` | Male | British English | British male |
| `em_alex` | Male | Spanish | Spanish male |
| `ff_siwis` | Female | French | French female |
| `pm_alex` | Male | Brazilian Portuguese | Portuguese male |
| `jf_alpha` | Female | Japanese | Japanese female |
| `zf_xiaoxiao` | Female | Mandarin Chinese | Mandarin female |

See the [Kokoro documentation](https://github.com/hexgrad/kokoro) for the full voice list.

## Supported Formats

| Format | ffmpeg required | Notes |
|--------|-----------------|-------|
| `mp3` | Yes | **Default** — best for sharing and playback |
| `wav` | No | Uncompressed PCM via soundfile |
| `ogg` | Yes | Vorbis encoding via pydub |
| `flac` | Yes | Lossless compression via pydub |

## How It Works

```
Input file (.md / .txt)
        │
        ▼
  Read & extract plain text
  (strip markup, front matter)
        │
        ▼
  Normalize text
  (numbers, abbreviations, currency)
        │
        ▼
  Split into paragraphs & lines
        │
        ▼
  Synthesize each line (Kokoro-82M)
  + silence between paragraphs
        │
        ▼
  Post-process audio
  (peak normalize → resample 44.1 kHz → pad)
        │
        ▼
  Export (MP3 / WAV / OGG / FLAC)
```

## Markdown Handling

| Element | Behavior |
|---------|----------|
| YAML front matter | Stripped (`---` delimited block at top) |
| Headings (`#`) | Converted to plain text (no "hash hash") |
| Bold / italic | Markup removed, text preserved |
| Links | Link text spoken; URL omitted |
| Images | Alt text spoken if present |
| Fenced code blocks | **Skipped entirely** (not spoken) |
| Inline code | Spoken literally |
| HTML comments | Removed |
| Lists | Marker removed; item text preserved |
| Paragraph breaks | Preserved for natural pacing |

## Troubleshooting

### `ffmpeg not found`

MP3, OGG, and FLAC export require ffmpeg on your PATH. Install it for your platform (see [Installation](#install-ffmpeg)) or use `--format wav` which does not need ffmpeg.

### Model download fails

The first synthesis downloads Kokoro-82M from Hugging Face. Ensure you have internet access and sufficient disk space. If behind a proxy, configure Hugging Face Hub environment variables (`HF_ENDPOINT`, etc.).

### Slow synthesis / memory

Kokoro runs on CPU by default. If you have a CUDA or Apple Silicon GPU, the engine auto-detects and uses `cuda` or `mps` when available. Very long documents synthesize line-by-line with a progress bar in verbose mode (`-V`).

### Torch / MPS / CUDA issues

If GPU inference fails, force CPU by setting `device=None` when creating a `TTSEngine` instance in the Python API. The CLI uses automatic device detection.

### NumPy / PyTorch version mismatch

md2speech requires **PyTorch >= 2.4.1**. Older PyTorch builds are incompatible with current `transformers` releases and with NumPy 2.x, which can surface as:

- `A module that was compiled using NumPy 1.x cannot be run in NumPy 2.x`
- `Disabling PyTorch because PyTorch >= 2.4 is required but found ...`
- `Failed to load Kokoro model` even when Hugging Face is reachable

Reinstall to pull compatible versions:

```bash
pipx reinstall md2speech
# or: pip install --upgrade --force-reinstall md2speech
```

### Empty document error

If the input file is empty or contains only markup/code with no speakable text, md2speech exits with an error.

## Development

```bash
pip install -e ".[dev]"
pytest                  # unit tests (mocked TTS)
pytest -m integration   # optional real synthesis test
```

## Publishing (maintainers)

Releases are automated when a version tag is pushed to `main`.

1. Bump `version` in `pyproject.toml` and `src/md2speech/__init__.py`
2. Commit, push to `main`
3. Create and push a tag:

```bash
git tag v0.1.0
git push origin v0.1.0
```

The [Release workflow](.github/workflows/release.yml) will:

1. Run unit tests on Python 3.12
2. Build the wheel (`.whl`) and source distribution (`.tar.gz`)
3. Publish both to [PyPI](https://pypi.org/project/md2speech/)
4. Create a GitHub Release with the build artifacts attached

**One-time setup (GitHub `pypi` environment):**

1. Create a PyPI API token at [pypi.org/manage/account/token](https://pypi.org/manage/account/token/) scoped to the `md2speech` project.
2. In GitHub: **Settings → Environments → pypi → Environment secrets** → add `PYPI_API_TOKEN` with that token.
3. Ensure the release workflow is on `main` and references the `pypi` environment (see [release.yml](.github/workflows/release.yml)).

**Alternative (no token):** configure [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) for owner `evelasko`, repo `md2speech`, workflow `release.yml`, environment `pypi`. OIDC is already enabled in the workflow (`id-token: write`).

**First release:**

```bash
# bump version in pyproject.toml and src/md2speech/__init__.py, commit, push
git tag v0.1.0
git push origin main
git push origin v0.1.0
```

Watch the **Actions** tab — the Release workflow publishes to PyPI and creates a GitHub Release.

Users install published versions with:

```bash
pipx install md2speech
pipx install md2speech==0.1.0
```

## License

MIT — see [LICENSE](LICENSE).

## Acknowledgments

- [Kokoro-82M](https://github.com/hexgrad/kokoro) by hexgrad — the underlying TTS model
- [Hugging Face](https://huggingface.co/hexgrad/Kokoro-82M) — model hosting and distribution
