fix: Resolve 7 critical bugs in Piper TTS engine and add cache versioning
This commit fixes all critical bugs identified in the Piper engine that were causing server crashes, hung processes, resource leaks, and audio synthesis failures. ## Bug Fixes **Bug #1: Invalid --stdin CLI flag** - Removed non-existent --stdin flag from piper command - Piper reads stdin by default, flag was causing undefined behavior **Bug #2: Temp file race conditions** - Replaced NamedTemporaryFile context manager with tempfile.mkstemp() - Prevents file handle locking issues on some systems **Bug #3: Missing process timeouts** - Added asyncio.wait_for() with 30s timeout for piper synthesis - Added 60s timeout for ffmpeg conversion - Prevents hung processes from accumulating and exhausting memory **Bug #4: Orphaned temp files** - Implemented comprehensive temp file tracking list - Added cleanup in finally block to ensure all temp files are removed - Prevents /tmp/ from filling with orphaned audio files **Bug #5: FFmpeg error suppression** - Removed quiet=True from ffmpeg calls - Added capture_stdout and capture_stderr for full error context - Improved error messages with actual ffmpeg output **Bug #6: No config file validation** - Added upfront validation for .onnx and .onnx.json files - Validates speaker exists in config before synthesis - Provides clear error messages with available speakers list **Bug #7: Poor error context** - Added comprehensive logging with DEBUG/INFO/ERROR/WARNING levels - Error messages now include full command, model, speaker, text preview - Added success logging with file sizes and synthesis details ## New Features **Cache Versioning System** - Added CACHE_VERSION constant to automatically invalidate cache on bug fixes - Old cached files (with bugs) are automatically bypassed - Includes cleanup_old_cache_files() utility for removing stale cache - Version history documented in code comments **Configurable Timeouts** - PIPER_TIMEOUT_SECONDS: defaults to 30s (configurable via .env) - FFMPEG_TIMEOUT_SECONDS: defaults to 60s (configurable via .env) **Path Fixes** - Fixed audio file path validation in main.py (Bug causing 404s) - Updated AUDIO_CACHE_DIR to use relative paths for better portability **Documentation** - Added CLAUDE.md for future AI assistant context - Updated .env.example with new timeout configuration options ## Testing - ✅ Health check passed - ✅ Synthesis working correctly - ✅ No temp file leaks - ✅ No hung processes - ✅ Cache versioning prevents old buggy files from being served - ✅ Voice preview in wizard working correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
229
CLAUDE.md
Normal file
229
CLAUDE.md
Normal file
@ -0,0 +1,229 @@
|
||||
# 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, 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 34-39)
|
||||
- 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
|
||||
|
||||
**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
|
||||
|
||||
### 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.
|
||||
Reference in New Issue
Block a user