Fix Piper 1.2+ synthesize API: handle single-speaker models (no sid input)
Browse files
main.py
CHANGED
|
@@ -12,23 +12,28 @@ from pathlib import Path
|
|
| 12 |
|
| 13 |
app = FastAPI(title="Piper TTS Space")
|
| 14 |
|
| 15 |
-
# Voice model info: (lang, model_name, url_onnx, url_json)
|
| 16 |
VOICE_MODELS = {
|
| 17 |
"zh": ("zh_CN-huayan-medium",
|
| 18 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx",
|
| 19 |
-
"https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx.json"
|
|
|
|
| 20 |
"en": ("en_US-lessac-medium",
|
| 21 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx",
|
| 22 |
-
"https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"
|
|
|
|
| 23 |
"ms": ("ms_MY-sirius-medium",
|
| 24 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx",
|
| 25 |
-
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx.json"
|
|
|
|
| 26 |
"ja": ("ja_JP-kenji-medium",
|
| 27 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx",
|
| 28 |
-
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx.json"
|
|
|
|
| 29 |
"ko": ("ko_KR-hui-hao-medium",
|
| 30 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx",
|
| 31 |
-
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx.json"
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
MODEL_DIR = Path("/tmp/piper_models")
|
|
@@ -51,7 +56,7 @@ def get_voice(lang: str) -> PiperVoice:
|
|
| 51 |
if lang not in VOICE_MODELS:
|
| 52 |
raise ValueError(f"Language '{lang}' not available. Available: {list(VOICE_MODELS.keys())}")
|
| 53 |
|
| 54 |
-
model_name, url_onnx, url_json = VOICE_MODELS[lang]
|
| 55 |
onnx_path = MODEL_DIR / f"{model_name}.onnx"
|
| 56 |
json_path = MODEL_DIR / f"{model_name}.onnx.json"
|
| 57 |
|
|
@@ -91,13 +96,19 @@ async def tts(request: TTSRequest):
|
|
| 91 |
raise HTTPException(400, f"Voice load failed: {str(e)}")
|
| 92 |
|
| 93 |
try:
|
|
|
|
|
|
|
|
|
|
| 94 |
# Piper 1.2+ requires wav_file parameter - use wave.open with BytesIO
|
| 95 |
buffer = BytesIO()
|
| 96 |
with wave.open(buffer, 'wb') as wav_file:
|
| 97 |
wav_file.setnchannels(1)
|
| 98 |
wav_file.setsampwidth(2) # 16-bit
|
| 99 |
wav_file.setframerate(request.sample_rate)
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
| 101 |
buffer.seek(0)
|
| 102 |
return Response(
|
| 103 |
content=buffer.read(),
|
|
@@ -105,6 +116,23 @@ async def tts(request: TTSRequest):
|
|
| 105 |
headers={"Content-Disposition": f"attachment; filename=tts_{uuid.uuid4().hex}.wav"}
|
| 106 |
)
|
| 107 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
raise HTTPException(500, f"Synthesis failed: {str(e)}")
|
| 109 |
|
| 110 |
@app.get("/health")
|
|
|
|
| 12 |
|
| 13 |
app = FastAPI(title="Piper TTS Space")
|
| 14 |
|
| 15 |
+
# Voice model info: (lang, model_name, url_onnx, url_json, is_multi_speaker)
|
| 16 |
VOICE_MODELS = {
|
| 17 |
"zh": ("zh_CN-huayan-medium",
|
| 18 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx",
|
| 19 |
+
"https://huggingface.co/rhasspy/piper-voices/resolve/main/zh/zh_CN/huayan/medium/zh_CN-huayan-medium.onnx.json",
|
| 20 |
+
False),
|
| 21 |
"en": ("en_US-lessac-medium",
|
| 22 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx",
|
| 23 |
+
"https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json",
|
| 24 |
+
False),
|
| 25 |
"ms": ("ms_MY-sirius-medium",
|
| 26 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx",
|
| 27 |
+
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ms/ms_MY/sirius/medium/ms_MY-sirius-medium.onnx.json",
|
| 28 |
+
False),
|
| 29 |
"ja": ("ja_JP-kenji-medium",
|
| 30 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx",
|
| 31 |
+
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ja/ja_JP/kenji/medium/ja_JP-kenji-medium.onnx.json",
|
| 32 |
+
False),
|
| 33 |
"ko": ("ko_KR-hui-hao-medium",
|
| 34 |
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx",
|
| 35 |
+
"https://huggingface.co/rhasspy/piper-voices/resolve/main/ko/ko_KR/hui_hao/medium/ko_KR-hui-hao-medium.onnx.json",
|
| 36 |
+
False),
|
| 37 |
}
|
| 38 |
|
| 39 |
MODEL_DIR = Path("/tmp/piper_models")
|
|
|
|
| 56 |
if lang not in VOICE_MODELS:
|
| 57 |
raise ValueError(f"Language '{lang}' not available. Available: {list(VOICE_MODELS.keys())}")
|
| 58 |
|
| 59 |
+
model_name, url_onnx, url_json, _ = VOICE_MODELS[lang]
|
| 60 |
onnx_path = MODEL_DIR / f"{model_name}.onnx"
|
| 61 |
json_path = MODEL_DIR / f"{model_name}.onnx.json"
|
| 62 |
|
|
|
|
| 96 |
raise HTTPException(400, f"Voice load failed: {str(e)}")
|
| 97 |
|
| 98 |
try:
|
| 99 |
+
# Check if model supports multi-speaker
|
| 100 |
+
_, _, _, is_multi_speaker = VOICE_MODELS[request.lang]
|
| 101 |
+
|
| 102 |
# Piper 1.2+ requires wav_file parameter - use wave.open with BytesIO
|
| 103 |
buffer = BytesIO()
|
| 104 |
with wave.open(buffer, 'wb') as wav_file:
|
| 105 |
wav_file.setnchannels(1)
|
| 106 |
wav_file.setsampwidth(2) # 16-bit
|
| 107 |
wav_file.setframerate(request.sample_rate)
|
| 108 |
+
if is_multi_speaker:
|
| 109 |
+
voice.synthesize(request.text, speaker_id=request.speaker_id, wav_file=wav_file)
|
| 110 |
+
else:
|
| 111 |
+
voice.synthesize(request.text, wav_file=wav_file)
|
| 112 |
buffer.seek(0)
|
| 113 |
return Response(
|
| 114 |
content=buffer.read(),
|
|
|
|
| 116 |
headers={"Content-Disposition": f"attachment; filename=tts_{uuid.uuid4().hex}.wav"}
|
| 117 |
)
|
| 118 |
except Exception as e:
|
| 119 |
+
# Fallback: try without speaker_id if it fails
|
| 120 |
+
if "sid" in str(e) or "speaker" in str(e).lower():
|
| 121 |
+
try:
|
| 122 |
+
buffer = BytesIO()
|
| 123 |
+
with wave.open(buffer, 'wb') as wav_file:
|
| 124 |
+
wav_file.setnchannels(1)
|
| 125 |
+
wav_file.setsampwidth(2)
|
| 126 |
+
wav_file.setframerate(request.sample_rate)
|
| 127 |
+
voice.synthesize(request.text, wav_file=wav_file)
|
| 128 |
+
buffer.seek(0)
|
| 129 |
+
return Response(
|
| 130 |
+
content=buffer.read(),
|
| 131 |
+
media_type="audio/wav",
|
| 132 |
+
headers={"Content-Disposition": f"attachment; filename=tts_{uuid.uuid4().hex}.wav"}
|
| 133 |
+
)
|
| 134 |
+
except Exception as e2:
|
| 135 |
+
raise HTTPException(500, f"Synthesis failed (fallback): {str(e2)}")
|
| 136 |
raise HTTPException(500, f"Synthesis failed: {str(e)}")
|
| 137 |
|
| 138 |
@app.get("/health")
|