Convert Markdown and plain text files to speech audio using Kokoro-82M (free, local TTS)
Project description
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.txtfiles - 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:
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:
pipx upgrade md2speech
From PyPI
pip install md2speech
From source
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
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
# 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
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
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:
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.
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]"orpip 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 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) 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.xDisabling PyTorch because PyTorch >= 2.4 is required but found ...Failed to load Kokoro modeleven when Hugging Face is reachable
Reinstall to pull compatible versions:
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
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.
- Bump
versioninpyproject.tomlandsrc/md2speech/__init__.py - Commit, push to
main - Create and push a tag:
git tag v0.1.0
git push origin v0.1.0
The Release workflow will:
- Run unit tests on Python 3.12
- Build the wheel (
.whl) and source distribution (.tar.gz) - Publish both to PyPI
- Create a GitHub Release with the build artifacts attached
One-time setup (GitHub pypi environment):
- Create a PyPI API token at pypi.org/manage/account/token scoped to the
md2speechproject. - In GitHub: Settings → Environments → pypi → Environment secrets → add
PYPI_API_TOKENwith that token. - Ensure the release workflow is on
mainand references thepypienvironment (see release.yml).
Alternative (no token): configure PyPI trusted publishing for owner evelasko, repo md2speech, workflow release.yml, environment pypi. OIDC is already enabled in the workflow (id-token: write).
First release:
# 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:
pipx install md2speech
pipx install md2speech==0.1.0
License
MIT — see LICENSE.
Acknowledgments
- Kokoro-82M by hexgrad — the underlying TTS model
- Hugging Face — model hosting and distribution
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file md2speech-0.1.2.tar.gz.
File metadata
- Download URL: md2speech-0.1.2.tar.gz
- Upload date:
- Size: 317.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d976c91b91b7087a2afab741b7aa143d20ffe7dd17cb92b63aa631048773410b
|
|
| MD5 |
2e9f18bb1df877f739e6c5ef65af5939
|
|
| BLAKE2b-256 |
2e2e66bd6f9e0567e4a72a4759b1ff62816dc1f2b570ee0ebaad690d45bcf62c
|
Provenance
The following attestation bundles were made for md2speech-0.1.2.tar.gz:
Publisher:
release.yml on evelasko/md2speech
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
md2speech-0.1.2.tar.gz -
Subject digest:
d976c91b91b7087a2afab741b7aa143d20ffe7dd17cb92b63aa631048773410b - Sigstore transparency entry: 2138019335
- Sigstore integration time:
-
Permalink:
evelasko/md2speech@69de2142444687d3e8dcc4261383df5eca40c613 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/evelasko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@69de2142444687d3e8dcc4261383df5eca40c613 -
Trigger Event:
push
-
Statement type:
File details
Details for the file md2speech-0.1.2-py3-none-any.whl.
File metadata
- Download URL: md2speech-0.1.2-py3-none-any.whl
- Upload date:
- Size: 20.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d164201c481421e80efc4e8e3cfa097471f5a99f392f87f3f87dfb6b46036bc7
|
|
| MD5 |
686219d16b6dc67010f2fa0d90166edf
|
|
| BLAKE2b-256 |
d25e5461e118b2b23ffadfded398466179ba5232ac3fa95191fffe9d5a703fad
|
Provenance
The following attestation bundles were made for md2speech-0.1.2-py3-none-any.whl:
Publisher:
release.yml on evelasko/md2speech
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
md2speech-0.1.2-py3-none-any.whl -
Subject digest:
d164201c481421e80efc4e8e3cfa097471f5a99f392f87f3f87dfb6b46036bc7 - Sigstore transparency entry: 2138019347
- Sigstore integration time:
-
Permalink:
evelasko/md2speech@69de2142444687d3e8dcc4261383df5eca40c613 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/evelasko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@69de2142444687d3e8dcc4261383df5eca40c613 -
Trigger Event:
push
-
Statement type: