99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
HyperTwist IPC wrapper for rhasspy/piper TTS engine.
|
|
|
|
Usage:
|
|
python piper_tts_wrapper.py --text "Hello world" --model path/to/model.onnx --output output.wav
|
|
|
|
Returns:
|
|
Path to generated WAV file on success
|
|
"""
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
|
|
def find_piper_exe():
|
|
"""Find the piper executable in known HyperTwist solver paths or PATH."""
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
candidates = [
|
|
os.path.join(script_dir, "piper", "piper", "piper.exe"),
|
|
os.path.join(script_dir, "..", "..", "..", "Binaries", "Win64", "Solvers", "piper", "piper", "piper.exe"),
|
|
os.path.join(script_dir, "piper", "piper.exe"),
|
|
os.path.join(script_dir, "..", "piper", "piper", "piper.exe"),
|
|
]
|
|
for candidate in candidates:
|
|
resolved = os.path.abspath(candidate)
|
|
if os.path.isfile(resolved):
|
|
return resolved
|
|
|
|
for candidate in ("piper.exe", "piper"):
|
|
resolved = shutil.which(candidate)
|
|
if resolved:
|
|
return os.path.abspath(resolved)
|
|
|
|
return None
|
|
|
|
|
|
def find_espeak_data(piper_exe):
|
|
"""Locate espeak-ng-data directory next to the piper executable."""
|
|
exe_dir = os.path.dirname(os.path.abspath(piper_exe))
|
|
candidates = [
|
|
os.path.join(exe_dir, "espeak-ng-data"),
|
|
os.path.join(exe_dir, "..", "espeak-ng-data"),
|
|
]
|
|
for candidate in candidates:
|
|
resolved = os.path.abspath(candidate)
|
|
if os.path.isdir(resolved):
|
|
return resolved
|
|
return None
|
|
|
|
|
|
def synthesize(text: str, model_path: str, config_path: str, output_path: str) -> str:
|
|
"""Synthesize speech using piper."""
|
|
if not os.path.exists(model_path):
|
|
return f"ERROR: Model not found: {model_path}"
|
|
if not os.path.exists(config_path):
|
|
return f"ERROR: Config not found: {config_path}"
|
|
|
|
piper_exe = find_piper_exe()
|
|
if not piper_exe:
|
|
return "ERROR: piper executable not found. Place it in Solvers/piper/piper/ or add to PATH."
|
|
|
|
espeak_data = find_espeak_data(piper_exe)
|
|
output_dir = os.path.dirname(os.path.abspath(output_path))
|
|
if output_dir:
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
cmd = [
|
|
piper_exe,
|
|
"--model", os.path.abspath(model_path),
|
|
"--config", os.path.abspath(config_path),
|
|
"--output_file", os.path.abspath(output_path),
|
|
]
|
|
if espeak_data:
|
|
cmd.extend(["--espeak_data", espeak_data])
|
|
|
|
try:
|
|
result = subprocess.run(cmd, input=text, text=True, capture_output=True, timeout=60)
|
|
if result.returncode == 0:
|
|
if not os.path.exists(output_path):
|
|
return f"ERROR: Piper reported success but did not create output: {output_path}"
|
|
return os.path.abspath(output_path)
|
|
return f"ERROR: {result.stderr}"
|
|
except Exception as e:
|
|
return f"ERROR: {e}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="HyperTwist Piper TTS Wrapper")
|
|
parser.add_argument("--text", required=True, help="Text to synthesize")
|
|
parser.add_argument("--model", required=True, help="Path to .onnx voice model")
|
|
parser.add_argument("--config", help="Path to model config JSON")
|
|
parser.add_argument("--output", default="output.wav", help="Output WAV path")
|
|
args = parser.parse_args()
|
|
|
|
config_path = args.config or args.model.replace(".onnx", ".onnx.json")
|
|
result = synthesize(args.text, args.model, config_path, args.output)
|
|
print(result)
|