AI-powered multi-voice audiobook generator — LLM script annotation, voice cloning, voice design, LoRA training, per-line style control, and export to MP3, chaptered M4B, or Audacity multi-track. Built on Qwen3-TTS.
1,001
stars
97
commits
Python
primary language
Aug 2, 2026
updated
English | 中文
A note to new users: Alexandria has recently seen a sudden surge of attention and new users. As a small project, I may not be able to respond to every issue promptly. Before opening an issue, please read this README and the Wiki thoroughly — most common questions are already answered there. Thank you for your patience!
Transform any book or novel into a fully-voiced audiobook using AI-powered script annotation and text-to-speech. Features a built-in Qwen3-TTS engine with batch processing and a browser-based editor for fine-tuning every line before final export.
torch.compile optimization for 3-4x faster batch decoding| GPU | OS | Status | Driver Requirement | Notes |
|---|---|---|---|---|
| NVIDIA | Windows | Full support | Driver 550+ (CUDA 12.8) | Flash attention included for faster encoding |
| NVIDIA | Linux | Full support | Driver 550+ (CUDA 12.8) | Flash attention + triton included |
| AMD | Linux | Full support | ROCm 6.3+ | ROCm optimizations applied automatically |
| AMD | Windows | CPU only | N/A | GPU acceleration is not supported — the app runs in CPU mode. For GPU acceleration with AMD, use Linux |
| Apple Silicon | macOS | CPU only | N/A | MPS acceleration is not currently supported. Functional but slow |
| Intel | macOS | CPU only | N/A |
Note: No external TTS server is required. Alexandria includes a built-in Qwen3-TTS engine that loads models directly. Model weights are downloaded automatically on first use (~3.5 GB per model variant).
Documentation: For in-depth guidance on voice types, LoRA training, batch generation, and more, see the Wiki.
https://github.com/Finrandojin/alexandria-audiobookNo GPU or wrong OS? Run Alexandria on a free T4 GPU in your browser:
Requires a free ngrok account for the web UI tunnel. See the notebook for full instructions.
For integration into automated pipelines or server deployments:
git clone https://github.com/Finrandojin/alexandria-audiobook.git
cd alexandria-audiobook
docker compose up --build
Requires Docker with the NVIDIA Container Toolkit. The web UI is available at http://localhost:4200. TTS models download on first use and are cached in a Docker volume. User data (uploads, voice configs, trained LoRA adapters, audio output) persists via bind mounts to the project directory.
If this is your first time running Alexandria, read this before anything else.
Alexandria does not include an LLM — it connects to one over an API. Before generating a script, you must have one of these running:
| Server | Default URL | Install |
|---|---|---|
| LM Studio | http://localhost:1234/v1 | Download, load a model, start server |
| Ollama | http://localhost:11434/v1 | ollama run qwen3 |
| OpenAI API | https://api.openai.com/v1 | Get an API key |
If the LLM server isn't running when you click "Generate Script", the generation will fail. Check the Pinokio terminal for error details.
The TTS models are not included in the install. They download automatically from Hugging Face the first time you generate audio. This is normal:
Tip: If the download seems stuck, check your internet connection. If it fails, restart the app and try again — it will resume from where it left off.
The very first batch generation in a session takes longer than subsequent ones:
| Available VRAM | What Works |
|---|---|
| 8 GB | One model at a time, small batches (2-5 chunks), CPU offload may be needed |
| 16 GB | Comfortable for most use cases, batches of 10-20 chunks |
| 24 GB+ | Full speed, batches of 40-60 chunks with codec compilation |
The web UI shows high-level status, but detailed logs are in the Pinokio terminal:
For common issues and solutions, see Troubleshooting.
The interface is split into a 5-step core pipeline (green tabs, numbered) and advanced tools (blue tabs, unnumbered). You only need the core pipeline to produce an audiobook.
Step 1 — Setup Configure your LLM connection and TTS engine. At minimum you need:
http://localhost:1234/v1 (LM Studio) or http://localhost:11434/v1 (Ollama)local for local servers)qwen2.5-14b)local (built-in, recommended) — loads models directly, no external server neededStep 2 — Script
Step 3 — Voices Each character detected in the script gets a voice card. For each speaker:
Step 4 — Editor
Step 5 — Result
These tabs are for power users who want more control over voice creation:
Configure connections to your LLM and TTS engine.
TTS Settings:
local (built-in engine) or external (connect to Gradio server)auto (recommended), cuda, cpu, or mpstorch.compile for 3-4x faster batch decoding (adds ~30-60s warmup on first generation)Prompt Settings (Advanced):
default_prompts.txt and can be customized per-session in the UI. Click "Reset to Defaults" to reload the file-based defaults (picks up edits without restarting the app)Upload a text file (.txt, .md, or .epub) and generate the annotated script. EPUB files are automatically converted to plain text on upload. The LLM converts your book into a structured JSON format with:
Review Script - After generation, click "Review Script" to run a second LLM pass that detects and fixes common annotation errors:
Review prompts are customizable in review_prompts.txt (same format as default_prompts.txt).
After script generation, voices are automatically loaded from the annotated script. For each speaker:
Persona Generation: Click Generate Personas to automatically assign voices to all characters. The LLM analyzes dialogue in the script and produces a voice description and sample text for each speaker. These are fed to the VoiceDesign model to generate reference audio, which is saved and assigned as a clone voice. The result is a fully-voiced cast with no manual configuration.
Speaker Aliases: Each voice card has an "Alias of" dropdown. Setting a speaker as an alias of another speaker means it will use the target's voice configuration during audio generation. Useful for:
Aliases resolve transitively (A → B → C uses C's config) with cycle detection.
Custom Voice Mode:
Clone Voice Mode:
LoRA Voice Mode:
Voice Design Mode:
Create new voices from text descriptions without needing reference audio.
Train LoRA adapters on the Base model to create custom voice identities. Several built-in LoRA presets are included out of the box and appear alongside your trained adapters.
Dataset:
metadata.jsonl with audio_filepath and text fieldsTraining Configuration:
Training tips:
Build LoRA training datasets interactively, one sample at a time.
Fine-tune your audiobook before export:
Alexandria offers two methods for batch rendering audio:
The default rendering mode. Sends individual TTS calls in parallel using the configured worker count.
High-speed rendering that sends multiple lines to the TTS engine in a single batched call. Chunks are sorted by text length and processed in optimized sub-batches to minimize padding waste.
Batch Seed from config (set empty for random)Download your completed audiobook as MP3, export as M4B with chapter markers for audiobook players, or click Export to Audacity for per-speaker WAV tracks.
project.lof in Audacity to load all tracks, then import labels.txt via File > Import > Labels for chunk annotations.Note: Some Linux audiobook players (e.g. Cozy) have limited M4B support and may not detect the file. The M4B output has been tested with VLC, Haruna, and Audiobookshelf.
| Setting | Recommended | Notes |
|---|---|---|
| TTS Mode | local | Built-in engine, no external server |
| Compile Codec | true | 3-4x faster decoding after one-time warmup |
| Parallel Workers | 20-60 | Higher = more throughput, more VRAM |
| Render Mode | Batch (Fast) | Uses batched TTS calls |
Tested on AMD RX 7900 XTX (24 GB VRAM, ROCm 6.3/7.2):
| Configuration | Throughput |
|---|---|
| Standard mode (sequential) | ~1x real-time |
| Batch mode, no codec compile | ~2x real-time |
| Batch mode + compile_codec | 3-6x real-time |
A 273-chunk audiobook (~54 minutes of audio) generates in approximately 16 minutes with batch mode and codec compilation enabled.
Linux only. AMD GPU acceleration requires ROCm 6.3+ on Linux. AMD GPUs on Windows run in CPU mode — see GPU Compatibility.
Alexandria automatically applies ROCm-specific optimizations when running on AMD GPUs:
torch.compile on pytorch-triton-rocmThese are applied transparently and require no configuration.
ROCm 7.x GPU downclocking fix: ROCm 7.x has a regression where the GPU's DPM controller aggressively downclocks the shader engine between autoregressive generation steps, causing batch generation to slow to a crawl or appear to hang. The fix is to set the GPU power profile to COMPUTE, which enforces a minimum clock frequency floor:
echo 5 | sudo tee /sys/class/drm/card1/device/pp_power_profile_modeThis needs to be run once per boot (it does not persist across reboots). You can add it to your system startup or run it manually before launching Alexandria. To verify it's active, check for
COMPUTE*in the output of:cat /sys/class/drm/card1/device/pp_power_profile_modeROCm 6.x users and NVIDIA users are not affected.
The generated script is a JSON array with speaker, text, and instruct fields:
[
{"speaker": "NARRATOR", "text": "The door creaked open slowly.", "instruct": "Calm, even narration."},
{"speaker": "ELENA", "text": "Ah! Who's there?", "instruct": "Startled and fearful, sharp whispered question, voice cracking with panic."},
{"speaker": "MARCUS", "text": "Haha... did you miss me?", "instruct": "Menacing confidence, low smug drawl with a dark chuckle, savoring the moment."}
]
instruct — 2-3 sentence TTS voice direction sent directly to the engine. Set tone, describe delivery, then give specific references. Example: "Devastated by grief, Sniffing between words and pausing to collect herself, end with a wracking sob."Vocalizations are written as real pronounceable text that the TTS speaks directly — no bracket tags or special tokens. The LLM generates natural onomatopoeia with short instruct directions:
Final Audiobook:
cloned_audiobook.mp3 - Combined audiobook with natural pausesIndividual Voicelines (for DAW editing):
voicelines/
├── voiceline_0001_narrator.mp3
├── voiceline_0002_elena.mp3
├── voiceline_0003_marcus.mp3
└── ...
Files are numbered in timeline order with speaker names for easy:
Audacity Export (per-speaker tracks):
audacity_export.zip
├── project.lof # Open this in Audacity to import all tracks
├── labels.txt # Import via File > Import > Labels for chunk annotations
├── narrator.wav # Full-length track with only NARRATOR audio
├── elena.wav # Full-length track with only ELENA audio
├── marcus.wav # Full-length track with only MARCUS audio
└── ...
Each WAV track is padded to the same total duration with silence where other speakers are talking. Playing all tracks simultaneously sounds identical to the merged MP3.
M4B Audiobook (chaptered):
audiobook.m4b - AAC audiobook with embedded chapter markersAlexandria exposes a REST API for programmatic access:
# Get current config (empty prompts fall through to file defaults)
curl http://127.0.0.1:4200/api/config
# Get file-based default prompts (hot-reloads from default_prompts.txt)
curl http://127.0.0.1:4200/api/default_prompts
# Save config
curl -X POST http://127.0.0.1:4200/api/config \
-H "Content-Type: application/json" \
-d '{
"llm": {"base_url": "...", "api_key": "...", "model_name": "..."},
"tts": {
"mode": "local",
"device": "auto",
"language": "English",
"parallel_workers": 25,
"batch_seed": 12345,
"compile_codec": true,
"sub_batch_enabled": true,
"sub_batch_min_size": 4,
"sub_batch_ratio": 5,
"pause_between_speakers_ms": 500,
"pause_same_speaker_ms": 250
}
}'
# Upload text file (supports .txt, .md, .epub)
curl -X POST http://127.0.0.1:4200/api/upload \
-F "file=@mybook.epub"
# Generate script (returns task ID)
curl -X POST http://127.0.0.1:4200/api/generate_script
# Check status
curl http://127.0.0.1:4200/api/status/script_generation
# Review script (fix attribution tags, misattributed lines, etc.)
curl -X POST http://127.0.0.1:4200/api/review_script
# Check review status
curl http://127.0.0.1:4200/api/status/review
# Get voices and config
curl http://127.0.0.1:4200/api/voices
# Parse voices from script
curl -X POST http://127.0.0.1:4200/api/parse_voices
# Save voice config
curl -X POST http://127.0.0.1:4200/api/save_voice_config \
-H "Content-Type: application/json" \
-d '{"NARRATOR": {"type": "custom", "voice": "Ryan", "character_style": "calm"}}'
# Get all chunks
curl http://127.0.0.1:4200/api/chunks
# Update a chunk
curl -X POST http://127.0.0.1:4200/api/chunks/5 \
-H "Content-Type: application/json" \
-d '{"text": "Updated dialogue", "instruct": "Excited, bright energy."}'
# Generate audio for single chunk
curl -X POST http://127.0.0.1:4200/api/chunks/5/generate
# Standard batch render (parallel individual calls)
curl -X POST http://127.0.0.1:4200/api/generate_batch \
-H "Content-Type: application/json" \
-d '{"indices": [0, 1, 2, 3, 4]}'
# Fast batch render (batched TTS calls, much faster)
curl -X POST http://127.0.0.1:4200/api/generate_batch_fast \
-H "Content-Type: application/json" \
-d '{"indices": [0, 1, 2, 3, 4]}'
# Merge all chunks into final audiobook
curl -X POST http://127.0.0.1:4200/api/merge
# List saved scripts
curl http://127.0.0.1:4200/api/scripts
# Save current script
curl -X POST http://127.0.0.1:4200/api/scripts/save \
-H "Content-Type: application/json" \
-d '{"name": "my-novel"}'
# Load a saved script
curl -X POST http://127.0.0.1:4200/api/scripts/load \
-H "Content-Type: application/json" \
-d '{"name": "my-novel"}'
# Generate personas (LLM + VoiceDesign, assigns clone voices automatically)
curl -X POST http://127.0.0.1:4200/api/generate_personas
# Generate personas in advanced mode with custom batch size
curl -X POST http://127.0.0.1:4200/api/generate_personas \
-H "Content-Type: application/json" \
-d '{"advanced": true, "batch_size": 40}'
# Check persona generation status
curl http://127.0.0.1:4200/api/status/persona
# Cancel persona generation
curl -X POST http://127.0.0.1:4200/api/cancel_persona
# Preview a voice from text description
curl -X POST http://127.0.0.1:4200/api/voice_design/preview \
-H "Content-Type: application/json" \
-d '{"description": "A warm, deep male voice", "text": "Hello world."}'
# Save a designed voice
curl -X POST http://127.0.0.1:4200/api/voice_design/save \
-H "Content-Type: application/json" \
-d '{"name": "warm_narrator", "description": "A warm, deep male voice", "text": "Hello world."}'
# List saved designed voices
curl http://127.0.0.1:4200/api/voice_design/list
# Delete a designed voice
curl -X DELETE http://127.0.0.1:4200/api/voice_design/delete/voice_id_here
# Upload a training dataset (ZIP with WAV + metadata.jsonl)
curl -X POST http://127.0.0.1:4200/api/lora/upload_dataset \
-F "file=@dataset.zip" -F "name=my_voice"
# Generate a dataset from Voice Designer description
curl -X POST http://127.0.0.1:4200/api/lora/generate_dataset \
-H "Content-Type: application/json" \
-d '{"name": "warm_voice", "description": "A warm male voice", "texts": ["Hello.", "Goodbye."]}'
# List uploaded datasets
curl http://127.0.0.1:4200/api/lora/datasets
# Delete a dataset
curl -X DELETE http://127.0.0.1:4200/api/lora/datasets/dataset_id_here
# Start LoRA training
curl -X POST http://127.0.0.1:4200/api/lora/train \
-H "Content-Type: application/json" \
-d '{"name": "narrator_warm", "dataset_id": "my_voice", "epochs": 25, "lr": "5e-6", "lora_r": 32, "lora_alpha": 64}'
# Check training status
curl http://127.0.0.1:4200/api/status/lora_training
# List trained adapters
curl http://127.0.0.1:4200/api/lora/models
# Test a trained adapter
curl -X POST http://127.0.0.1:4200/api/lora/test \
-H "Content-Type: application/json" \
-d '{"adapter_id": "narrator_warm_1234567890", "text": "Test line.", "instruct": "Calm narration."}'
# Delete an adapter
curl -X DELETE http://127.0.0.1:4200/api/lora/models/adapter_id_here
# List all dataset builder projects
curl http://127.0.0.1:4200/api/dataset_builder/list
# Create a new project
curl -X POST http://127.0.0.1:4200/api/dataset_builder/create \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset"}'
# Update project metadata (description and global seed)
curl -X POST http://127.0.0.1:4200/api/dataset_builder/update_meta \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "description": "A warm male narrator", "global_seed": "42"}'
# Update sample rows
curl -X POST http://127.0.0.1:4200/api/dataset_builder/update_rows \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "rows": [{"text": "Hello world.", "emotion": "cheerful"}]}'
# Generate a single sample preview
curl -X POST http://127.0.0.1:4200/api/dataset_builder/generate_sample \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "description": "A warm male voice", "sample_index": 0, "samples": [{"text": "Hello.", "emotion": "cheerful"}]}'
# Batch generate all samples
curl -X POST http://127.0.0.1:4200/api/dataset_builder/generate_batch \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "description": "A warm male voice", "samples": [{"text": "Hello.", "emotion": "cheerful"}]}'
# Check batch generation status
curl http://127.0.0.1:4200/api/dataset_builder/status/my_voice_dataset
# Cancel a running batch generation
curl -X POST http://127.0.0.1:4200/api/dataset_builder/cancel \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset"}'
# Save project as a training dataset
curl -X POST http://127.0.0.1:4200/api/dataset_builder/save \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "ref_sample_index": 0}'
# Delete a project
curl -X DELETE http://127.0.0.1:4200/api/dataset_builder/my_voice_dataset
# Download audiobook (after merging in editor)
curl http://127.0.0.1:4200/api/audiobook --output audiobook.mp3
# Export to Audacity (per-speaker tracks + LOF + labels)
curl -X POST http://127.0.0.1:4200/api/export_audacity
# Poll for completion
curl http://127.0.0.1:4200/api/status/audacity_export
# Download the zip
curl http://127.0.0.1:4200/api/export_audacity --output audacity_export.zip
import requests
BASE = "http://127.0.0.1:4200"
# Upload and generate script
with open("mybook.txt", "rb") as f:
requests.post(f"{BASE}/api/upload", files={"file": f})
requests.post(f"{BASE}/api/generate_script")
# Poll for completion
import time
while True:
status = requests.get(f"{BASE}/api/status/script_generation").json()
if status.get("status") in ["completed", "error"]:
break
time.sleep(2)
# Configure voices
voice_config = {
"NARRATOR": {"type": "custom", "voice": "Ryan", "character_style": "calm narrator"},
"HERO": {"type": "custom", "voice": "Aiden", "character_style": "brave, determined"}
}
requests.post(f"{BASE}/api/save_voice_config", json=voice_config)
# Fast batch render all chunks
chunks = requests.get(f"{BASE}/api/chunks").json()
indices = [c["id"] for c in chunks]
requests.post(f"{BASE}/api/generate_batch_fast", json={"indices": indices})
# ... poll until all chunks status == "done" ...
requests.post(f"{BASE}/api/merge")
# Download
with open("output.mp3", "wb") as f:
f.write(requests.get(f"{BASE}/api/audiobook").content)
# Export to Audacity
requests.post(f"{BASE}/api/export_audacity")
# ... poll /api/status/audacity_export until not running ...
with open("audacity_export.zip", "wb") as f:
f.write(requests.get(f"{BASE}/api/export_audacity").content)
const BASE = "http://127.0.0.1:4200";
// Upload file
const formData = new FormData();
formData.append("file", fileInput.files[0]);
await fetch(`${BASE}/api/upload`, { method: "POST", body: formData });
// Generate script
await fetch(`${BASE}/api/generate_script`, { method: "POST" });
// Poll for completion
async function waitForTask(taskName) {
while (true) {
const res = await fetch(`${BASE}/api/status/${taskName}`);
const data = await res.json();
if (data.status === "completed" || data.status === "error") return data;
await new Promise(r => setTimeout(r, 2000));
}
}
await waitForTask("script_generation");
// Configure and generate
await fetch(`${BASE}/api/save_voice_config`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
NARRATOR: { type: "custom", voice: "Ryan", character_style: "calm" }
})
});
// Fast batch render all chunks
const chunks = await (await fetch(`${BASE}/api/chunks`)).json();
const indices = chunks.map(c => c.id);
await fetch(`${BASE}/api/generate_batch_fast`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ indices })
});
// ... poll until all chunks done ...
// Merge into final audiobook
await fetch(`${BASE}/api/merge`, { method: "POST" });
// Export to Audacity
await fetch(`${BASE}/api/export_audacity`, { method: "POST" });
// ... poll /api/status/audacity_export until not running ...
// Download zip from GET /api/export_audacity
For script generation, non-thinking models work best:
Thinking models (DeepSeek-R1, GLM4-air, etc.) can interfere with JSON output. If you must use one, add <think> to the Banned Tokens field in Setup to disable thinking mode.
HF_ENDPOINT=https://hf-mirror.com before starting the appenv field: env: { HF_ENDPOINT: "https://hf-mirror.com" }HF_TOKEN to your access tokendevice: cpu as a fallback (much slower)Conda's bundled ffmpeg on Windows often lacks the MP3 encoder (libmp3lame). Alexandria now detects this and automatically falls back to WAV, but if you want MP3 output:
conda install -c conda-forge ffmpegconda remove ffmpegffmpeg -encoders 2>/dev/null | grep mp3LLM prompts are stored in plain-text files at the project root, split into system prompt and user prompt sections by a ---SEPARATOR--- delimiter:
default_prompts.txt — Prompts for script generation (annotation)review_prompts.txt — Prompts for script review (error correction)How it works:
app/default_prompts.py and app/review_prompts.py read their respective files and export the promptsconfig.json stores user overrides for generation prompts — when its prompt fields are empty, the file defaults are used/api/default_promptsTo customize prompts:
default_prompts.txt or review_prompts.txt directly — changes are picked up on the next requestNon-English books: The default LLM prompts are written for English text and reference English-specific conventions (attribution tags like "said he", quotation marks, etc.). When processing books in other languages, you'll get better results by editing the prompts to match that language's dialogue conventions — for example, French guillemets (« »), Japanese brackets (「」), or language-appropriate attribution patterns. Set the TTS Language dropdown to match as well.
Alexandria/
├── app/
│ ├── app.py # FastAPI server
│ ├── tts.py # TTS engine (local + external backends)
│ ├── train_lora.py # LoRA training subprocess script
│ ├── generate_script.py # LLM script annotation
│ ├── generate_personas.py # LLM persona generation + VoiceDesign voice assignment
│ ├── review_script.py # LLM script review (second pass)
│ ├── utils.py # Shared utilities (atomic JSON writes)
│ ├── default_prompts.py # Generation prompt loader (reads default_prompts.txt)
│ ├── review_prompts.py # Review prompt loader (reads review_prompts.txt)
│ ├── project.py # Chunk management & batch generation
│ ├── parse_voices.py # Voice extraction
│ ├── config.json # Runtime configuration (gitignored)
│ ├── static/index.html # Web UI
│ └── requirements.txt # Python dependencies
├── builtin_lora/ # Pre-trained LoRA voice presets
├── dataset_builder/ # Dataset builder project workspace (gitignored)
├── designed_voices/ # Saved Voice Designer outputs (gitignored)
├── lora_datasets/ # Uploaded/generated training datasets (gitignored)
├── lora_models/ # Trained LoRA adapters (gitignored)
├── default_prompts.txt # LLM prompts for script generation
├── review_prompts.txt # LLM prompts for script review
├── install.js # Pinokio installer
├── start.js # Pinokio launcher
├── reset.js # Reset script
├── pinokio.js # Pinokio UI config
├── pinokio.json # Pinokio metadata
└── README.md
MIT
68 commits
9 commits
9 commits
9 commits
Python
59.7%
HTML
36.3%
Jupyter Notebook
2.4%
JavaScript
1.5%
AI-powered multi-voice audiobook generator — LLM script annotation, voice cloning, voice design, LoRA training, per-line style control, and export to MP3, chaptered M4B, or Audacity multi-track. Built on Qwen3-TTS.
1,001
stars
97
commits
Python
primary language
Aug 2, 2026
updated
English | 中文
A note to new users: Alexandria has recently seen a sudden surge of attention and new users. As a small project, I may not be able to respond to every issue promptly. Before opening an issue, please read this README and the Wiki thoroughly — most common questions are already answered there. Thank you for your patience!
Transform any book or novel into a fully-voiced audiobook using AI-powered script annotation and text-to-speech. Features a built-in Qwen3-TTS engine with batch processing and a browser-based editor for fine-tuning every line before final export.
torch.compile optimization for 3-4x faster batch decoding| GPU | OS | Status | Driver Requirement | Notes |
|---|---|---|---|---|
| NVIDIA | Windows | Full support | Driver 550+ (CUDA 12.8) | Flash attention included for faster encoding |
| NVIDIA | Linux | Full support | Driver 550+ (CUDA 12.8) | Flash attention + triton included |
| AMD | Linux | Full support | ROCm 6.3+ | ROCm optimizations applied automatically |
| AMD | Windows | CPU only | N/A | GPU acceleration is not supported — the app runs in CPU mode. For GPU acceleration with AMD, use Linux |
| Apple Silicon | macOS | CPU only | N/A | MPS acceleration is not currently supported. Functional but slow |
| Intel | macOS | CPU only | N/A |
Note: No external TTS server is required. Alexandria includes a built-in Qwen3-TTS engine that loads models directly. Model weights are downloaded automatically on first use (~3.5 GB per model variant).
Documentation: For in-depth guidance on voice types, LoRA training, batch generation, and more, see the Wiki.
https://github.com/Finrandojin/alexandria-audiobookNo GPU or wrong OS? Run Alexandria on a free T4 GPU in your browser:
Requires a free ngrok account for the web UI tunnel. See the notebook for full instructions.
For integration into automated pipelines or server deployments:
git clone https://github.com/Finrandojin/alexandria-audiobook.git
cd alexandria-audiobook
docker compose up --build
Requires Docker with the NVIDIA Container Toolkit. The web UI is available at http://localhost:4200. TTS models download on first use and are cached in a Docker volume. User data (uploads, voice configs, trained LoRA adapters, audio output) persists via bind mounts to the project directory.
If this is your first time running Alexandria, read this before anything else.
Alexandria does not include an LLM — it connects to one over an API. Before generating a script, you must have one of these running:
| Server | Default URL | Install |
|---|---|---|
| LM Studio | http://localhost:1234/v1 | Download, load a model, start server |
| Ollama | http://localhost:11434/v1 | ollama run qwen3 |
| OpenAI API | https://api.openai.com/v1 | Get an API key |
If the LLM server isn't running when you click "Generate Script", the generation will fail. Check the Pinokio terminal for error details.
The TTS models are not included in the install. They download automatically from Hugging Face the first time you generate audio. This is normal:
Tip: If the download seems stuck, check your internet connection. If it fails, restart the app and try again — it will resume from where it left off.
The very first batch generation in a session takes longer than subsequent ones:
| Available VRAM | What Works |
|---|---|
| 8 GB | One model at a time, small batches (2-5 chunks), CPU offload may be needed |
| 16 GB | Comfortable for most use cases, batches of 10-20 chunks |
| 24 GB+ | Full speed, batches of 40-60 chunks with codec compilation |
The web UI shows high-level status, but detailed logs are in the Pinokio terminal:
For common issues and solutions, see Troubleshooting.
The interface is split into a 5-step core pipeline (green tabs, numbered) and advanced tools (blue tabs, unnumbered). You only need the core pipeline to produce an audiobook.
Step 1 — Setup Configure your LLM connection and TTS engine. At minimum you need:
http://localhost:1234/v1 (LM Studio) or http://localhost:11434/v1 (Ollama)local for local servers)qwen2.5-14b)local (built-in, recommended) — loads models directly, no external server neededStep 2 — Script
Step 3 — Voices Each character detected in the script gets a voice card. For each speaker:
Step 4 — Editor
Step 5 — Result
These tabs are for power users who want more control over voice creation:
Configure connections to your LLM and TTS engine.
TTS Settings:
local (built-in engine) or external (connect to Gradio server)auto (recommended), cuda, cpu, or mpstorch.compile for 3-4x faster batch decoding (adds ~30-60s warmup on first generation)Prompt Settings (Advanced):
default_prompts.txt and can be customized per-session in the UI. Click "Reset to Defaults" to reload the file-based defaults (picks up edits without restarting the app)Upload a text file (.txt, .md, or .epub) and generate the annotated script. EPUB files are automatically converted to plain text on upload. The LLM converts your book into a structured JSON format with:
Review Script - After generation, click "Review Script" to run a second LLM pass that detects and fixes common annotation errors:
Review prompts are customizable in review_prompts.txt (same format as default_prompts.txt).
After script generation, voices are automatically loaded from the annotated script. For each speaker:
Persona Generation: Click Generate Personas to automatically assign voices to all characters. The LLM analyzes dialogue in the script and produces a voice description and sample text for each speaker. These are fed to the VoiceDesign model to generate reference audio, which is saved and assigned as a clone voice. The result is a fully-voiced cast with no manual configuration.
Speaker Aliases: Each voice card has an "Alias of" dropdown. Setting a speaker as an alias of another speaker means it will use the target's voice configuration during audio generation. Useful for:
Aliases resolve transitively (A → B → C uses C's config) with cycle detection.
Custom Voice Mode:
Clone Voice Mode:
LoRA Voice Mode:
Voice Design Mode:
Create new voices from text descriptions without needing reference audio.
Train LoRA adapters on the Base model to create custom voice identities. Several built-in LoRA presets are included out of the box and appear alongside your trained adapters.
Dataset:
metadata.jsonl with audio_filepath and text fieldsTraining Configuration:
Training tips:
Build LoRA training datasets interactively, one sample at a time.
Fine-tune your audiobook before export:
Alexandria offers two methods for batch rendering audio:
The default rendering mode. Sends individual TTS calls in parallel using the configured worker count.
High-speed rendering that sends multiple lines to the TTS engine in a single batched call. Chunks are sorted by text length and processed in optimized sub-batches to minimize padding waste.
Batch Seed from config (set empty for random)Download your completed audiobook as MP3, export as M4B with chapter markers for audiobook players, or click Export to Audacity for per-speaker WAV tracks.
project.lof in Audacity to load all tracks, then import labels.txt via File > Import > Labels for chunk annotations.Note: Some Linux audiobook players (e.g. Cozy) have limited M4B support and may not detect the file. The M4B output has been tested with VLC, Haruna, and Audiobookshelf.
| Setting | Recommended | Notes |
|---|---|---|
| TTS Mode | local | Built-in engine, no external server |
| Compile Codec | true | 3-4x faster decoding after one-time warmup |
| Parallel Workers | 20-60 | Higher = more throughput, more VRAM |
| Render Mode | Batch (Fast) | Uses batched TTS calls |
Tested on AMD RX 7900 XTX (24 GB VRAM, ROCm 6.3/7.2):
| Configuration | Throughput |
|---|---|
| Standard mode (sequential) | ~1x real-time |
| Batch mode, no codec compile | ~2x real-time |
| Batch mode + compile_codec | 3-6x real-time |
A 273-chunk audiobook (~54 minutes of audio) generates in approximately 16 minutes with batch mode and codec compilation enabled.
Linux only. AMD GPU acceleration requires ROCm 6.3+ on Linux. AMD GPUs on Windows run in CPU mode — see GPU Compatibility.
Alexandria automatically applies ROCm-specific optimizations when running on AMD GPUs:
torch.compile on pytorch-triton-rocmThese are applied transparently and require no configuration.
ROCm 7.x GPU downclocking fix: ROCm 7.x has a regression where the GPU's DPM controller aggressively downclocks the shader engine between autoregressive generation steps, causing batch generation to slow to a crawl or appear to hang. The fix is to set the GPU power profile to COMPUTE, which enforces a minimum clock frequency floor:
echo 5 | sudo tee /sys/class/drm/card1/device/pp_power_profile_modeThis needs to be run once per boot (it does not persist across reboots). You can add it to your system startup or run it manually before launching Alexandria. To verify it's active, check for
COMPUTE*in the output of:cat /sys/class/drm/card1/device/pp_power_profile_modeROCm 6.x users and NVIDIA users are not affected.
The generated script is a JSON array with speaker, text, and instruct fields:
[
{"speaker": "NARRATOR", "text": "The door creaked open slowly.", "instruct": "Calm, even narration."},
{"speaker": "ELENA", "text": "Ah! Who's there?", "instruct": "Startled and fearful, sharp whispered question, voice cracking with panic."},
{"speaker": "MARCUS", "text": "Haha... did you miss me?", "instruct": "Menacing confidence, low smug drawl with a dark chuckle, savoring the moment."}
]
instruct — 2-3 sentence TTS voice direction sent directly to the engine. Set tone, describe delivery, then give specific references. Example: "Devastated by grief, Sniffing between words and pausing to collect herself, end with a wracking sob."Vocalizations are written as real pronounceable text that the TTS speaks directly — no bracket tags or special tokens. The LLM generates natural onomatopoeia with short instruct directions:
Final Audiobook:
cloned_audiobook.mp3 - Combined audiobook with natural pausesIndividual Voicelines (for DAW editing):
voicelines/
├── voiceline_0001_narrator.mp3
├── voiceline_0002_elena.mp3
├── voiceline_0003_marcus.mp3
└── ...
Files are numbered in timeline order with speaker names for easy:
Audacity Export (per-speaker tracks):
audacity_export.zip
├── project.lof # Open this in Audacity to import all tracks
├── labels.txt # Import via File > Import > Labels for chunk annotations
├── narrator.wav # Full-length track with only NARRATOR audio
├── elena.wav # Full-length track with only ELENA audio
├── marcus.wav # Full-length track with only MARCUS audio
└── ...
Each WAV track is padded to the same total duration with silence where other speakers are talking. Playing all tracks simultaneously sounds identical to the merged MP3.
M4B Audiobook (chaptered):
audiobook.m4b - AAC audiobook with embedded chapter markersAlexandria exposes a REST API for programmatic access:
# Get current config (empty prompts fall through to file defaults)
curl http://127.0.0.1:4200/api/config
# Get file-based default prompts (hot-reloads from default_prompts.txt)
curl http://127.0.0.1:4200/api/default_prompts
# Save config
curl -X POST http://127.0.0.1:4200/api/config \
-H "Content-Type: application/json" \
-d '{
"llm": {"base_url": "...", "api_key": "...", "model_name": "..."},
"tts": {
"mode": "local",
"device": "auto",
"language": "English",
"parallel_workers": 25,
"batch_seed": 12345,
"compile_codec": true,
"sub_batch_enabled": true,
"sub_batch_min_size": 4,
"sub_batch_ratio": 5,
"pause_between_speakers_ms": 500,
"pause_same_speaker_ms": 250
}
}'
# Upload text file (supports .txt, .md, .epub)
curl -X POST http://127.0.0.1:4200/api/upload \
-F "file=@mybook.epub"
# Generate script (returns task ID)
curl -X POST http://127.0.0.1:4200/api/generate_script
# Check status
curl http://127.0.0.1:4200/api/status/script_generation
# Review script (fix attribution tags, misattributed lines, etc.)
curl -X POST http://127.0.0.1:4200/api/review_script
# Check review status
curl http://127.0.0.1:4200/api/status/review
# Get voices and config
curl http://127.0.0.1:4200/api/voices
# Parse voices from script
curl -X POST http://127.0.0.1:4200/api/parse_voices
# Save voice config
curl -X POST http://127.0.0.1:4200/api/save_voice_config \
-H "Content-Type: application/json" \
-d '{"NARRATOR": {"type": "custom", "voice": "Ryan", "character_style": "calm"}}'
# Get all chunks
curl http://127.0.0.1:4200/api/chunks
# Update a chunk
curl -X POST http://127.0.0.1:4200/api/chunks/5 \
-H "Content-Type: application/json" \
-d '{"text": "Updated dialogue", "instruct": "Excited, bright energy."}'
# Generate audio for single chunk
curl -X POST http://127.0.0.1:4200/api/chunks/5/generate
# Standard batch render (parallel individual calls)
curl -X POST http://127.0.0.1:4200/api/generate_batch \
-H "Content-Type: application/json" \
-d '{"indices": [0, 1, 2, 3, 4]}'
# Fast batch render (batched TTS calls, much faster)
curl -X POST http://127.0.0.1:4200/api/generate_batch_fast \
-H "Content-Type: application/json" \
-d '{"indices": [0, 1, 2, 3, 4]}'
# Merge all chunks into final audiobook
curl -X POST http://127.0.0.1:4200/api/merge
# List saved scripts
curl http://127.0.0.1:4200/api/scripts
# Save current script
curl -X POST http://127.0.0.1:4200/api/scripts/save \
-H "Content-Type: application/json" \
-d '{"name": "my-novel"}'
# Load a saved script
curl -X POST http://127.0.0.1:4200/api/scripts/load \
-H "Content-Type: application/json" \
-d '{"name": "my-novel"}'
# Generate personas (LLM + VoiceDesign, assigns clone voices automatically)
curl -X POST http://127.0.0.1:4200/api/generate_personas
# Generate personas in advanced mode with custom batch size
curl -X POST http://127.0.0.1:4200/api/generate_personas \
-H "Content-Type: application/json" \
-d '{"advanced": true, "batch_size": 40}'
# Check persona generation status
curl http://127.0.0.1:4200/api/status/persona
# Cancel persona generation
curl -X POST http://127.0.0.1:4200/api/cancel_persona
# Preview a voice from text description
curl -X POST http://127.0.0.1:4200/api/voice_design/preview \
-H "Content-Type: application/json" \
-d '{"description": "A warm, deep male voice", "text": "Hello world."}'
# Save a designed voice
curl -X POST http://127.0.0.1:4200/api/voice_design/save \
-H "Content-Type: application/json" \
-d '{"name": "warm_narrator", "description": "A warm, deep male voice", "text": "Hello world."}'
# List saved designed voices
curl http://127.0.0.1:4200/api/voice_design/list
# Delete a designed voice
curl -X DELETE http://127.0.0.1:4200/api/voice_design/delete/voice_id_here
# Upload a training dataset (ZIP with WAV + metadata.jsonl)
curl -X POST http://127.0.0.1:4200/api/lora/upload_dataset \
-F "file=@dataset.zip" -F "name=my_voice"
# Generate a dataset from Voice Designer description
curl -X POST http://127.0.0.1:4200/api/lora/generate_dataset \
-H "Content-Type: application/json" \
-d '{"name": "warm_voice", "description": "A warm male voice", "texts": ["Hello.", "Goodbye."]}'
# List uploaded datasets
curl http://127.0.0.1:4200/api/lora/datasets
# Delete a dataset
curl -X DELETE http://127.0.0.1:4200/api/lora/datasets/dataset_id_here
# Start LoRA training
curl -X POST http://127.0.0.1:4200/api/lora/train \
-H "Content-Type: application/json" \
-d '{"name": "narrator_warm", "dataset_id": "my_voice", "epochs": 25, "lr": "5e-6", "lora_r": 32, "lora_alpha": 64}'
# Check training status
curl http://127.0.0.1:4200/api/status/lora_training
# List trained adapters
curl http://127.0.0.1:4200/api/lora/models
# Test a trained adapter
curl -X POST http://127.0.0.1:4200/api/lora/test \
-H "Content-Type: application/json" \
-d '{"adapter_id": "narrator_warm_1234567890", "text": "Test line.", "instruct": "Calm narration."}'
# Delete an adapter
curl -X DELETE http://127.0.0.1:4200/api/lora/models/adapter_id_here
# List all dataset builder projects
curl http://127.0.0.1:4200/api/dataset_builder/list
# Create a new project
curl -X POST http://127.0.0.1:4200/api/dataset_builder/create \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset"}'
# Update project metadata (description and global seed)
curl -X POST http://127.0.0.1:4200/api/dataset_builder/update_meta \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "description": "A warm male narrator", "global_seed": "42"}'
# Update sample rows
curl -X POST http://127.0.0.1:4200/api/dataset_builder/update_rows \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "rows": [{"text": "Hello world.", "emotion": "cheerful"}]}'
# Generate a single sample preview
curl -X POST http://127.0.0.1:4200/api/dataset_builder/generate_sample \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "description": "A warm male voice", "sample_index": 0, "samples": [{"text": "Hello.", "emotion": "cheerful"}]}'
# Batch generate all samples
curl -X POST http://127.0.0.1:4200/api/dataset_builder/generate_batch \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "description": "A warm male voice", "samples": [{"text": "Hello.", "emotion": "cheerful"}]}'
# Check batch generation status
curl http://127.0.0.1:4200/api/dataset_builder/status/my_voice_dataset
# Cancel a running batch generation
curl -X POST http://127.0.0.1:4200/api/dataset_builder/cancel \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset"}'
# Save project as a training dataset
curl -X POST http://127.0.0.1:4200/api/dataset_builder/save \
-H "Content-Type: application/json" \
-d '{"name": "my_voice_dataset", "ref_sample_index": 0}'
# Delete a project
curl -X DELETE http://127.0.0.1:4200/api/dataset_builder/my_voice_dataset
# Download audiobook (after merging in editor)
curl http://127.0.0.1:4200/api/audiobook --output audiobook.mp3
# Export to Audacity (per-speaker tracks + LOF + labels)
curl -X POST http://127.0.0.1:4200/api/export_audacity
# Poll for completion
curl http://127.0.0.1:4200/api/status/audacity_export
# Download the zip
curl http://127.0.0.1:4200/api/export_audacity --output audacity_export.zip
import requests
BASE = "http://127.0.0.1:4200"
# Upload and generate script
with open("mybook.txt", "rb") as f:
requests.post(f"{BASE}/api/upload", files={"file": f})
requests.post(f"{BASE}/api/generate_script")
# Poll for completion
import time
while True:
status = requests.get(f"{BASE}/api/status/script_generation").json()
if status.get("status") in ["completed", "error"]:
break
time.sleep(2)
# Configure voices
voice_config = {
"NARRATOR": {"type": "custom", "voice": "Ryan", "character_style": "calm narrator"},
"HERO": {"type": "custom", "voice": "Aiden", "character_style": "brave, determined"}
}
requests.post(f"{BASE}/api/save_voice_config", json=voice_config)
# Fast batch render all chunks
chunks = requests.get(f"{BASE}/api/chunks").json()
indices = [c["id"] for c in chunks]
requests.post(f"{BASE}/api/generate_batch_fast", json={"indices": indices})
# ... poll until all chunks status == "done" ...
requests.post(f"{BASE}/api/merge")
# Download
with open("output.mp3", "wb") as f:
f.write(requests.get(f"{BASE}/api/audiobook").content)
# Export to Audacity
requests.post(f"{BASE}/api/export_audacity")
# ... poll /api/status/audacity_export until not running ...
with open("audacity_export.zip", "wb") as f:
f.write(requests.get(f"{BASE}/api/export_audacity").content)
const BASE = "http://127.0.0.1:4200";
// Upload file
const formData = new FormData();
formData.append("file", fileInput.files[0]);
await fetch(`${BASE}/api/upload`, { method: "POST", body: formData });
// Generate script
await fetch(`${BASE}/api/generate_script`, { method: "POST" });
// Poll for completion
async function waitForTask(taskName) {
while (true) {
const res = await fetch(`${BASE}/api/status/${taskName}`);
const data = await res.json();
if (data.status === "completed" || data.status === "error") return data;
await new Promise(r => setTimeout(r, 2000));
}
}
await waitForTask("script_generation");
// Configure and generate
await fetch(`${BASE}/api/save_voice_config`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
NARRATOR: { type: "custom", voice: "Ryan", character_style: "calm" }
})
});
// Fast batch render all chunks
const chunks = await (await fetch(`${BASE}/api/chunks`)).json();
const indices = chunks.map(c => c.id);
await fetch(`${BASE}/api/generate_batch_fast`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ indices })
});
// ... poll until all chunks done ...
// Merge into final audiobook
await fetch(`${BASE}/api/merge`, { method: "POST" });
// Export to Audacity
await fetch(`${BASE}/api/export_audacity`, { method: "POST" });
// ... poll /api/status/audacity_export until not running ...
// Download zip from GET /api/export_audacity
For script generation, non-thinking models work best:
Thinking models (DeepSeek-R1, GLM4-air, etc.) can interfere with JSON output. If you must use one, add <think> to the Banned Tokens field in Setup to disable thinking mode.
HF_ENDPOINT=https://hf-mirror.com before starting the appenv field: env: { HF_ENDPOINT: "https://hf-mirror.com" }HF_TOKEN to your access tokendevice: cpu as a fallback (much slower)Conda's bundled ffmpeg on Windows often lacks the MP3 encoder (libmp3lame). Alexandria now detects this and automatically falls back to WAV, but if you want MP3 output:
conda install -c conda-forge ffmpegconda remove ffmpegffmpeg -encoders 2>/dev/null | grep mp3LLM prompts are stored in plain-text files at the project root, split into system prompt and user prompt sections by a ---SEPARATOR--- delimiter:
default_prompts.txt — Prompts for script generation (annotation)review_prompts.txt — Prompts for script review (error correction)How it works:
app/default_prompts.py and app/review_prompts.py read their respective files and export the promptsconfig.json stores user overrides for generation prompts — when its prompt fields are empty, the file defaults are used/api/default_promptsTo customize prompts:
default_prompts.txt or review_prompts.txt directly — changes are picked up on the next requestNon-English books: The default LLM prompts are written for English text and reference English-specific conventions (attribution tags like "said he", quotation marks, etc.). When processing books in other languages, you'll get better results by editing the prompts to match that language's dialogue conventions — for example, French guillemets (« »), Japanese brackets (「」), or language-appropriate attribution patterns. Set the TTS Language dropdown to match as well.
Alexandria/
├── app/
│ ├── app.py # FastAPI server
│ ├── tts.py # TTS engine (local + external backends)
│ ├── train_lora.py # LoRA training subprocess script
│ ├── generate_script.py # LLM script annotation
│ ├── generate_personas.py # LLM persona generation + VoiceDesign voice assignment
│ ├── review_script.py # LLM script review (second pass)
│ ├── utils.py # Shared utilities (atomic JSON writes)
│ ├── default_prompts.py # Generation prompt loader (reads default_prompts.txt)
│ ├── review_prompts.py # Review prompt loader (reads review_prompts.txt)
│ ├── project.py # Chunk management & batch generation
│ ├── parse_voices.py # Voice extraction
│ ├── config.json # Runtime configuration (gitignored)
│ ├── static/index.html # Web UI
│ └── requirements.txt # Python dependencies
├── builtin_lora/ # Pre-trained LoRA voice presets
├── dataset_builder/ # Dataset builder project workspace (gitignored)
├── designed_voices/ # Saved Voice Designer outputs (gitignored)
├── lora_datasets/ # Uploaded/generated training datasets (gitignored)
├── lora_models/ # Trained LoRA adapters (gitignored)
├── default_prompts.txt # LLM prompts for script generation
├── review_prompts.txt # LLM prompts for script review
├── install.js # Pinokio installer
├── start.js # Pinokio launcher
├── reset.js # Reset script
├── pinokio.js # Pinokio UI config
├── pinokio.json # Pinokio metadata
└── README.md
MIT
68 commits
9 commits
9 commits
9 commits
Python
59.7%
HTML
36.3%
Jupyter Notebook
2.4%
JavaScript
1.5%