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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user