fix: Update Piper TTS to support voice preview and improve setup

- Fixed preview endpoint to work with both Kokoro and Piper engines
- Improved voice model download with curl fallback and SSL handling
- Added better error messages when models are missing
- Created PIPER_SETUP.md with manual download instructions
- Added test_piper_direct.py for testing Piper installation
- Updated README with voice model download requirements

Note: Piper voice models require manual download from GitHub releases
due to HuggingFace URL structure. See PIPER_SETUP.md for instructions.
This commit is contained in:
2025-12-09 14:56:48 +00:00
parent 6f7330c491
commit d3e72edd7f
6 changed files with 300 additions and 26 deletions

98
backend/PIPER_SETUP.md Normal file
View File

@@ -0,0 +1,98 @@
# Piper TTS Setup Guide
## Quick Start
Piper TTS is now integrated but requires manual voice model setup.
### 1. Install Piper
```bash
cd backend
./install_piper.sh
```
Verify installation:
```bash
piper --version
```
### 2. Download Voice Models
Piper voice models need to be downloaded manually and placed in `backend/piper_models/`.
**Download voices from:**
https://github.com/rhasspy/piper/releases/tag/v1.2.0
**Recommended voices for English:**
1. **en_US-lessac-medium** (Male, American)
- Download: `en_US-lessac-medium.onnx` and `en_US-lessac-medium.onnx.json`
- Place in: `backend/piper_models/`
2. **en_US-amy-medium** (Female, American)
- Download: `en_US-amy-medium.onnx` and `en_US-amy-medium.onnx.json`
- Place in: `backend/piper_models/`
3. **en_GB-alan-medium** (Male, British)
- Download: `en_GB-alan-medium.onnx` and `en_GB-alan-medium.onnx.json`
- Place in: `backend/piper_models/`
### 3. Directory Structure
After downloading, your directory should look like:
```
backend/
├── piper_models/
│ ├── en_US-lessac-medium.onnx
│ ├── en_US-lessac-medium.onnx.json
│ ├── en_US-amy-medium.onnx
│ ├── en_US-amy-medium.onnx.json
│ ├── en_GB-alan-medium.onnx
│ └── en_GB-alan-medium.onnx.json
└── ...
```
### 4. Test Piper
```bash
cd backend
python test_piper_direct.py
```
### 5. Use in Application
1. Start the backend server
2. In the frontend, click "Piper TTS" button in Voice Assignment screen
3. Select Piper voices from the dropdown
4. Generate audiobook with pitch control!
## Troubleshooting
### "Model not found" error
- Make sure you downloaded both `.onnx` and `.onnx.json` files
- Check files are in `backend/piper_models/` directory
- Verify filenames match exactly (case-sensitive)
### "Piper not found" error
- Run `./install_piper.sh` to install Piper
- Or install manually from https://github.com/rhasspy/piper/releases
### Synthesis fails
- Check Piper version: `piper --version`
- Try running Piper directly: `echo "test" | piper --model piper_models/en_US-lessac-medium.onnx --output-raw > test.wav`
- Check backend logs for detailed error messages
## Alternative: Use Kokoro TTS
If Piper setup is too complex, you can continue using Kokoro TTS (the default engine) which works out of the box with no additional setup.
## More Voices
Browse all available Piper voices:
https://rhasspy.github.io/piper-samples/
Download any voice and place the `.onnx` and `.onnx.json` files in `backend/piper_models/`.

View File

@@ -83,6 +83,15 @@ Or install manually:
piper --version
```
**Download Voice Models:**
Piper requires manual voice model download. See [PIPER_SETUP.md](PIPER_SETUP.md) for detailed instructions.
Quick steps:
1. Visit https://github.com/rhasspy/piper/releases/tag/v1.2.0
2. Download voice models (e.g., `en_US-lessac-medium.onnx` and `.onnx.json`)
3. Place in `backend/piper_models/` directory
**Benefits of Piper TTS:**
- ✨ Pitch shifting support (-12 to +12 semitones per role)
- ✨ Fast, lightweight neural TTS

View File

@@ -385,26 +385,42 @@ async def preview_voice(request: VoicePreviewRequest):
preview_text = request.text or "Hello! This is a preview of this voice. How does it sound?"
try:
# Get pipeline for language
pipeline = tts_service.get_pipeline(request.lang)
# Generate preview audio
audio = tts_service.synthesize_segment(
pipeline=pipeline,
text=preview_text,
voice=request.voice,
speed=request.speed
)
# Convert to WAV bytes
import io
import soundfile as sf
import numpy as np
# Check which engine is active
if tts_service.engine_name == 'kokoro':
# Use Kokoro service
pipeline = tts_service.kokoro_service.get_pipeline(request.lang)
audio = tts_service.kokoro_service.synthesize_segment(
pipeline=pipeline,
text=preview_text,
voice=request.voice,
speed=request.speed
)
sample_rate = tts_service.kokoro_service.SAMPLE_RATE
elif tts_service.engine_name == 'piper':
# Use Piper service with pitch control
audio = tts_service.piper_service.synthesize_with_pitch(
text=preview_text,
voice=request.voice,
speed=request.speed,
pitch_shift=0 # No pitch shift for preview
)
sample_rate = tts_service.piper_service.SAMPLE_RATE
else:
raise ValueError(f"Unknown engine: {tts_service.engine_name}")
# Convert to WAV bytes
buffer = io.BytesIO()
sf.write(buffer, audio, tts_service.SAMPLE_RATE, format='WAV')
sf.write(buffer, audio, sample_rate, format='WAV')
buffer.seek(0)
audio_bytes = buffer.read()
logger.info(f"Generated voice preview: {len(audio_bytes)} bytes")
logger.info(f"Generated voice preview ({tts_service.engine_name}): {len(audio_bytes)} bytes")
return StreamingResponse(
iter([audio_bytes]),

View File

@@ -0,0 +1 @@
Entry not found

View File

@@ -72,20 +72,64 @@ class PiperTTSService:
# Check if model already exists
if os.path.exists(model_path) and os.path.exists(config_path):
logger.info(f"Voice model {voice} already exists")
return True
logger.info(f"Downloading Piper voice model: {voice}")
logger.info(f"Models directory: {self.models_dir}")
# Download using piper's built-in download
# Try to download using wget/curl as Piper's download might not work
try:
subprocess.run(
["piper", "--model", voice, "--download-dir", self.models_dir],
check=True,
timeout=60
)
return os.path.exists(model_path)
except subprocess.SubprocessError as e:
# Piper models are hosted on HuggingFace
base_url = "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0"
# Determine language prefix from voice ID
lang_prefix = voice.split('-')[0] # e.g., "en_US" from "en_US-lessac-medium"
model_url = f"{base_url}/{lang_prefix}/{voice}.onnx"
config_url = f"{base_url}/{lang_prefix}/{voice}.onnx.json"
logger.info(f"Downloading from: {model_url}")
# Try using curl/wget first (more reliable)
try:
# Try curl
subprocess.run(
["curl", "-L", "-o", model_path, model_url],
check=True,
timeout=120,
capture_output=True
)
subprocess.run(
["curl", "-L", "-o", config_path, config_url],
check=True,
timeout=120,
capture_output=True
)
except (subprocess.SubprocessError, FileNotFoundError):
# Fall back to urllib with SSL context
import urllib.request
import ssl
# Create SSL context that doesn't verify certificates (for development)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
urllib.request.urlretrieve(model_url, model_path, context=ssl_context)
urllib.request.urlretrieve(config_url, config_path, context=ssl_context)
if os.path.exists(model_path) and os.path.exists(config_path):
logger.info(f"Successfully downloaded voice model: {voice}")
return True
else:
logger.error(f"Download completed but files not found")
return False
except Exception as e:
logger.error(f"Failed to download voice model {voice}: {e}")
logger.error(f"Please manually download from: https://github.com/rhasspy/piper/releases/tag/v1.2.0")
logger.error(f"See backend/PIPER_SETUP.md for detailed instructions")
return False
def synthesize_with_pitch(
@@ -109,14 +153,25 @@ class PiperTTSService:
"""
# Ensure voice model is available
if not self._download_voice_model(voice):
logger.warning(f"Voice {voice} not available, using default")
voice = self.DEFAULT_VOICE
self._download_voice_model(voice)
error_msg = (
f"Piper voice model '{voice}' not found. "
f"Please download voice models manually. "
f"See backend/PIPER_SETUP.md for instructions."
)
logger.error(error_msg)
raise FileNotFoundError(error_msg)
model_path = os.path.join(self.models_dir, f"{voice}.onnx")
# Check if model file exists
if not os.path.exists(model_path):
logger.error(f"Model file not found: {model_path}")
return np.array([], dtype=np.float32)
# Generate audio with Piper
try:
logger.info(f"Running Piper with model: {model_path}")
# Run piper command
process = subprocess.Popen(
["piper", "--model", model_path, "--output-raw"],
@@ -129,7 +184,8 @@ class PiperTTSService:
stdout, stderr = process.communicate(input=text.encode('utf-8'), timeout=30)
if process.returncode != 0:
logger.error(f"Piper synthesis failed: {stderr.decode()}")
error_msg = stderr.decode() if stderr else "Unknown error"
logger.error(f"Piper synthesis failed (exit code {process.returncode}): {error_msg}")
return np.array([], dtype=np.float32)
# Convert raw audio to numpy array

View File

@@ -0,0 +1,94 @@
"""
Direct test of Piper TTS functionality.
"""
import subprocess
import os
def test_piper_installation():
"""Test if Piper is installed and accessible."""
print("Testing Piper Installation\n" + "="*50)
# Check if piper command exists
try:
result = subprocess.run(
["piper", "--version"],
capture_output=True,
text=True,
timeout=5
)
print(f"✓ Piper found: {result.stdout.strip()}")
return True
except FileNotFoundError:
print("✗ Piper not found in PATH")
print(" Install with: ./install_piper.sh")
return False
except Exception as e:
print(f"✗ Error checking Piper: {e}")
return False
def test_piper_synthesis():
"""Test basic Piper synthesis."""
print("\nTesting Piper Synthesis\n" + "="*50)
# Create models directory
models_dir = os.path.join(os.path.dirname(__file__), "piper_models")
os.makedirs(models_dir, exist_ok=True)
print(f"Models directory: {models_dir}")
# Try to synthesize with default voice
voice = "en_US-lessac-medium"
model_path = os.path.join(models_dir, f"{voice}.onnx")
if not os.path.exists(model_path):
print(f"\n⚠️ Model not found: {model_path}")
print(f"Attempting to download {voice}...")
# Try downloading
try:
import urllib.request
base_url = "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0"
model_url = f"{base_url}/en_US/{voice}.onnx"
config_url = f"{base_url}/en_US/{voice}.onnx.json"
print(f"Downloading from: {model_url}")
urllib.request.urlretrieve(model_url, model_path)
urllib.request.urlretrieve(config_url, f"{model_path}.json")
print("✓ Download complete")
except Exception as e:
print(f"✗ Download failed: {e}")
return False
else:
print(f"✓ Model found: {model_path}")
# Try synthesis
try:
print("\nAttempting synthesis...")
process = subprocess.Popen(
["piper", "--model", model_path, "--output-raw"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
test_text = "Hello, this is a test."
stdout, stderr = process.communicate(input=test_text.encode('utf-8'), timeout=10)
if process.returncode == 0:
print(f"✓ Synthesis successful: {len(stdout)} bytes of audio")
return True
else:
print(f"✗ Synthesis failed (exit code {process.returncode})")
print(f" Error: {stderr.decode()}")
return False
except Exception as e:
print(f"✗ Synthesis error: {e}")
return False
if __name__ == "__main__":
if test_piper_installation():
test_piper_synthesis()
print("\n" + "="*50)