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

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