Files
audio-engine-hub/CLAUDE.md
stephan 91887ae296 feat: Add XTTS v2 support, refactor Docker/GPU infra, and improve Piper engine
- Add XTTS v2 configuration to .env.example
- Refactor Dockerfile to multi-stage build with CUDA 12.1 support
- Update Makefile with Kokoro and XTTS test environment targets
- Refactor Piper engine (app/engines/piper.py) to use python module execution
- Add comprehensive documentation for Kokoro and XTTS plans
- Add helper scripts and patches for build process
2025-12-13 11:37:58 +01:00

298 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
AudioEngineHub is a local-first, modular, multi-engine Text-to-Speech (TTS) server built with FastAPI. It provides a unified API to interact with various TTS engines (Piper, Kokoro, StyleTTS, ChatTTS, F5-TTS) with caching, audio format conversion, and Docker deployment support.
## Architecture
### Core Components
**FastAPI Application Factory (`app/main.py`)**
- Uses `create_app()` factory pattern to dynamically load engines based on configuration
- Maintains `app.ENGINE_REGISTRY` dict mapping engine names to instantiated engine objects
- Engines are loaded from `settings.ACTIVE_ENGINES` at startup
- All available engines defined in `ALL_ENGINES` dict (lines 35-41)
- Startup event checks for models directory availability with retry logic (lines 192-213)
**Engine Architecture (`app/engines/`)**
- All engines inherit from `TTSEngineBase` abstract class
- Required methods: `synthesize()`, `list_models()`, `list_voices()`, `healthcheck()`, `selftest()`
- All `synthesize()` methods are async and return file paths to generated audio
- Engines are stateless and instantiated once per app lifecycle
**Configuration (`app/config.py`)**
- Uses pydantic-settings for type-safe configuration
- `ACTIVE_ENGINES` is a Set[str] loaded from `.env` file as JSON array
- Default model path: `/models/piper/{model_name}/{model_name}.onnx`
- Audio cache directory: `/home/appuser/app/asset/audio`
**Utility Modules**
- `app/utils/text.py`: Text chunking for long inputs
- `app/utils/audio.py`: Audio concatenation via ffmpeg
- `app/utils/cache.py`: Cache key generation based on request parameters
### Volume Mounts
The `docker-compose.yml` mounts three critical paths:
1. `./app:/home/appuser/app` - Hot-reload for development
2. `./app/models:/models` - TTS model files (not included in image)
3. `./asset:/home/appuser/app/asset` - Persistent audio cache
**CRITICAL**: The model mount is `./app/models:/models` (maps to `/models` inside container). The engine code uses absolute path `/models/piper/` to locate models. Do NOT use `./models` as the source - models must be in the `app/models/` directory on the host.
### Model Structure
Piper models follow this directory structure:
```
app/models/piper/{model_name}/
├── {model_name}.onnx # Model file
└── {model_name}.onnx.json # Config with speaker_id_map
```
The `speaker_id_map` in the JSON config maps speaker names to numeric IDs. See `app/engines/piper.py:30-47` for speaker resolution logic.
## Development Commands
### Local Development
```bash
# Build and run from local source (auto-finds free port)
make dev-up
# Run with pre-built registry image
make up
# Stop containers
make down
# View logs
make logs
# Access container shell
make shell
```
### Testing
```bash
# Run pytest with coverage (requires .venv activated or auto-activates)
make test
# Health check on running container
make health-check
```
### Container Registry Workflow
```bash
# Login (one-time setup)
docker login git.wlkns.org
# Build, tag, and push to registry
make push
# Pull latest from registry
make pull
```
## Key Implementation Details
### TTS Request Flow
1. Request arrives at `/tts` endpoint (line 69)
2. Engine lookup and healthcheck (lines 72-78)
3. Model and speaker validation (lines 81-87)
4. Cache key generation and lookup (lines 90-108)
5. If not cached: synthesize (optionally with chunking) (lines 111-122)
6. Cache result and return URL or base64 (lines 125-143)
### Audio Format Handling
**IMPORTANT**: Piper engine currently returns WAV only. The ffmpeg conversion code is commented out at lines 99-118 in `app/engines/piper.py`. This was done to debug an ffmpeg issue. When re-enabling format conversion:
- Uncomment lines 103-118
- Remove line 101 (direct WAV return)
- Ensure ffmpeg is available in container
### Critical: Piper Engine Input Method (`app/engines/piper.py`)
The Piper engine uses `--input-file` with a temporary file instead of stdin. Previous attempts to use `--stdin` with `asyncio.subprocess.communicate(input=...)` caused issues. Current implementation (lines 71-96):
1. Creates temporary text file with input text
2. Passes file path via `--input-file` flag to piper executable
3. Cleans up temp file in finally block
**Known Issue**: There were reports of the Piper engine occasionally synthesizing incorrect text or causing server crashes. If investigating synthesis issues:
- Check the temporary file creation/cleanup in `piper.py:71-96`
- Verify the text is being written correctly to the temp file
- Test manually inside container: `echo "text" | piper --model /models/piper/{model}/{model}.onnx --output_file test.wav`
### Async Patterns
- All synthesis operations are async
- Blocking I/O (file operations, ffmpeg) wrapped in `asyncio.to_thread()`
- Concurrent chunk synthesis via `asyncio.gather()` (line 115)
### Engine Registration
To add a new engine:
1. Create class in `app/engines/` inheriting from `TTSEngineBase`
2. Add to `ALL_ENGINES` dict in `app/main.py`
3. Add engine name to `ACTIVE_ENGINES` in `.env`
## Testing Strategy
Tests use pytest with async support and fixtures in `tests/conftest.py`:
- `app_client`: TestClient for the FastAPI app
- `piper_engine`: Direct access to PiperEngine instance from registry
Run tests with `make test` which sets `PYTHONPATH` and activates venv automatically.
## Configuration Notes
**`.env` Format**:
```bash
ACTIVE_ENGINES='["piper", "styletts"]' # JSON array as string
HOST=0.0.0.0
PORT=8000
```
**Registry Configuration** (in Makefile):
- `REGISTRY=git.wlkns.org`
- `USERNAME=stephan`
- `IMAGE_NAME=audio-engine-hub`
- `TAG=latest`
## Common Patterns
### Adding a New Endpoint
Follow the pattern in `app/main.py`:
- Add route to the app instance inside `create_app()`
- Use `app.ENGINE_REGISTRY` to access active engines
- Handle engine not found with 404
- Validate models/speakers before processing
### Working with Engines
```python
# Access from registry
engine = app.ENGINE_REGISTRY.get("piper")
# Always check health before use
health = engine.healthcheck()
if health.get("status") != "ok":
# Handle unhealthy engine
# Get available resources
models = engine.list_models()
voices = engine.list_voices(model_name)
# Synthesize
audio_path = await engine.synthesize(text, speaker=voice, model=model, fmt="wav")
```
## Docker Multi-Stage Build
The Dockerfile uses a two-stage build:
1. **Builder stage**: Creates venv and installs Python dependencies
2. **Runner stage**: Slim image with ffmpeg, copies venv and app code, runs as unprivileged `appuser`
Production uses gunicorn with uvicorn workers (2 workers default).
## Critical Issues & Lessons Learned
### App Instance Must Be at Module Level
The FastAPI `app` instance MUST be created at module level in `app/main.py` (line 227: `app = create_app()`). Do not only create it inside `if __name__ == "__main__"` block, or uvicorn/gunicorn will fail with "Attribute 'app' not found" error.
### Startup Race Condition with Volume Mounts
The startup event handler (`app/main.py:192-213`) waits for `/models/piper` directory to exist and be non-empty. This prevents race conditions where the app starts before Docker has finished mounting volumes. Max retries: 10, retry delay: 2 seconds.
### Model Path Must Be Absolute
Engine implementations use absolute path `/models/piper/` not relative paths. This ensures consistency regardless of working directory and matches the Docker volume mount structure.
### Engine Status
**Currently Working:**
- `piper`: Functional, uses real Piper TTS executable with ONNX models
- `kokoro`: Fully functional, uses Kokoro-82M TTS library with 54 voices across 8 languages
**Dummy Implementations (for testing only):**
- `styletts`: Returns hardcoded model/voice lists, generates dummy audio
- `chattts`: Dummy implementation
- `f5-tts`: Implemented but may be inactive by default
### Kokoro Engine (`app/engines/kokoro.py`)
**Overview:**
- Uses `kokoro` Python library (KPipeline) for high-performance TTS synthesis
- 82M parameter model delivering ~90× real-time performance on RTX 3090 Ti
- 54 voices across 8 languages: EN-US, EN-GB, FR, ES, JA, ZH, IT, PT, HI, KO
- Outputs 24kHz audio natively (WAV), converts to OGG/MP3 via ffmpeg
- Model size: ~200MB per language, auto-downloaded from Hugging Face on first use
- Voice metadata: `app/engines/kokoro_voices.py` contains all 54 voices with metadata
**Architecture:**
```python
class KokoroEngine(TTSEngineBase):
def __init__(self):
# GPU/CPU detection via settings.KOKORO_DEVICE
# Pipeline instances cached per language code
async def synthesize(text, speaker, model, fmt):
# Uses KPipeline for synthesis (24kHz native output)
# Format conversion via ffmpeg (_run_ffmpeg_blocking)
# Returns temp file path to generated audio
def list_models(self):
# Returns 10 language models:
# kokoro-en-us, kokoro-en-gb, kokoro-fr, kokoro-es,
# kokoro-ja, kokoro-zh, kokoro-it, kokoro-pt, kokoro-hi, kokoro-ko
def list_voices(self, model=None):
# Returns all 54 voices or filtered by language
# Uses get_voices_for_model() from kokoro_voices.py
```
**Language Code Mapping:**
The engine maps model names to Kokoro's internal language codes:
- `kokoro-en-us` → `'a'` (American English)
- `kokoro-en-gb` → `'b'` (British English)
- `kokoro-fr` → `'fr'` (French)
- `kokoro-es` → `'es'` (Spanish)
- `kokoro-ja` → `'ja'` (Japanese)
- `kokoro-zh` → `'zh'` (Chinese)
- `kokoro-it` → `'it'` (Italian)
- `kokoro-pt` → `'pt'` (Portuguese)
- `kokoro-hi` → `'hi'` (Hindi)
- `kokoro-ko` → `'ko'` (Korean)
**Voice Organization (`app/engines/kokoro_voices.py`):**
- All 54 voices documented with metadata (gender, language, description)
- Naming convention: `{language}{gender}_{name}` (e.g., `af_bella`, `am_adam`)
- Popular voices: `af_bella`, `af_sarah`, `af_sky`, `am_adam`, `am_michael`
- Helper functions: `get_voices_for_model()`, `get_voice_info()`
**Model Download & Caching:**
- Models auto-download from Hugging Face on first synthesis
- Cached in `~/.cache/huggingface/` (inside container)
- First synthesis may take 30-60s due to download + compilation
- Subsequent syntheses are fast (~90× real-time on GPU)
**Configuration (`app/config.py`):**
- `KOKORO_DEVICE`: "cuda" or "cpu" (default: "cuda")
- `KOKORO_TIMEOUT_SECONDS`: Synthesis timeout (default: 30)
**Error Handling:**
- Applies all bug fixes from Piper engine (timeouts, temp file cleanup, logging)
- Graceful GPU fallback if CUDA unavailable
- Voice validation before synthesis
- Comprehensive error logging with context
### Debugging Tips
**Port Conflicts**: The Makefile automatically finds free ports starting from 8000. Run with `sudo make dev-up` for most reliable port detection.
**Container Crashes**: If the container crashes during synthesis:
1. Check logs: `make logs`
2. Disable uvicorn reload in docker-compose.yml command to see full tracebacks
3. Test piper executable directly inside container: `make shell`
**Model Loading Issues**: If `list_models()` returns empty:
1. Verify volume mount in docker-compose.yml points to `./app/models:/models`
2. Check models exist on host: `ls app/models/piper/`
3. Check inside container: `docker exec -it audio-engine-hub_app ls -la /models/piper/`
**Connection Reset Errors in Client**: Add small delay (`time.sleep(1)`) before making requests if experiencing `ConnectionResetError`. This indicates server needs time to fully initialize.