Spaces:
Sleeping
Sleeping
| # app.py | |
| from __future__ import annotations | |
| import os | |
| import tarfile | |
| import io | |
| import shutil | |
| from typing import Optional | |
| from app.gradio_app import build_demo | |
| from models.tts_router import cleanup_old_audio, ensure_runtime_audio_dir | |
| import huggingface_hub | |
| # ------------------------- | |
| # Helpers: models & Piper | |
| # ------------------------- | |
| def _env(name: str, default: Optional[str] = None) -> Optional[str]: | |
| # Environment takes precedence over .env defaults (pydantic loads .env) | |
| return os.environ.get(name, default) | |
| def ensure_model() -> str: | |
| """ | |
| Ensure a local llama.cpp GGUF exists at LLAMACPP_MODEL_PATH. | |
| If missing, download from HF_MODEL_REPO (and optional HF_MODEL_FILE). | |
| """ | |
| model_path = _env("LLAMACPP_MODEL_PATH") | |
| repo_id = _env("HF_MODEL_REPO") | |
| file_name = _env("HF_MODEL_FILE") or (os.path.basename(model_path) if model_path else None) | |
| if not model_path or not repo_id or not file_name: | |
| raise RuntimeError( | |
| "Missing config: set LLAMACPP_MODEL_PATH and HF_MODEL_REPO in .env (optionally HF_MODEL_FILE)." | |
| ) | |
| if os.path.exists(model_path): | |
| print(f"[MODEL] Found: {model_path}") | |
| return model_path | |
| os.makedirs(os.path.dirname(model_path), exist_ok=True) | |
| print(f"[MODEL] Downloading {file_name} from {repo_id} …") | |
| local_path = huggingface_hub.hf_hub_download( | |
| repo_id=repo_id, | |
| filename=file_name, | |
| local_dir=os.path.dirname(model_path), | |
| local_dir_use_symlinks=False, | |
| ) | |
| # If hf_hub_download stored under a hashed subdir, move to exact target path | |
| if os.path.abspath(local_path) != os.path.abspath(model_path): | |
| shutil.copy2(local_path, model_path) | |
| print(f"[MODEL] Ready at {model_path}") | |
| return model_path | |
| def ensure_piper() -> tuple[str, str]: | |
| """ | |
| Ensure Piper binary + voice model exist, and set env vars PIPER_BIN, PIPER_MODEL. | |
| Returns (piper_bin, piper_model). | |
| """ | |
| # --- Ensure binary --- | |
| desired_bin = _env("PIPER_BIN", os.path.abspath("./bin/piper")) | |
| if not os.path.exists(desired_bin): | |
| os.makedirs(os.path.dirname(desired_bin), exist_ok=True) | |
| # Download a small Linux x86_64 Piper binary tarball from the official repo | |
| # (Using a known release; if HF changes base image/arch you may need to adjust) | |
| print("[PIPER] Downloading Piper binary …") | |
| # v1.2.0 is a common stable tag; adjust if needed | |
| piper_repo = "rhasspy/piper" | |
| piper_asset = "piper_linux_x86_64.tar.gz" | |
| # Pull via HF Hub to avoid GitHub rate limiting on Spaces runners | |
| # We mirror by leveraging HF's cached files capability (requires file to exist in repo). | |
| # If you have your own mirror, point HF_PIPER_REPO/HF_PIPER_FILE env vars to it. | |
| piper_hf_repo = _env("HF_PIPER_REPO") | |
| piper_hf_file = _env("HF_PIPER_FILE") | |
| if piper_hf_repo and piper_hf_file: | |
| tar_path = huggingface_hub.hf_hub_download( | |
| repo_id=piper_hf_repo, | |
| filename=piper_hf_file, | |
| local_dir="./bin", | |
| local_dir_use_symlinks=False, | |
| ) | |
| with tarfile.open(tar_path, "r:gz") as tf: | |
| tf.extractall("./bin") | |
| else: | |
| # Direct HTTP fallback (works on Spaces runners) | |
| import requests | |
| url = f"https://github.com/{piper_repo}/releases/download/v1.2.0/{piper_asset}" | |
| r = requests.get(url, timeout=60) | |
| r.raise_for_status() | |
| with tarfile.open(fileobj=io.BytesIO(r.content), mode="r:gz") as tf: | |
| tf.extractall("./bin") | |
| # Find the extracted "piper" binary | |
| candidate = os.path.join("./bin", "piper") | |
| if not os.path.exists(candidate): | |
| # sometimes archives unpack into a subdir | |
| for root, _, files in os.walk("./bin"): | |
| if "piper" in files: | |
| candidate = os.path.join(root, "piper") | |
| break | |
| if not os.path.exists(candidate): | |
| raise RuntimeError("[PIPER] Could not locate extracted 'piper' binary.") | |
| os.chmod(candidate, 0o755) | |
| desired_bin = os.path.abspath(candidate) | |
| os.environ["PIPER_BIN"] = desired_bin | |
| print(f"[PIPER] BIN = {desired_bin}") | |
| # --- Ensure voice model --- | |
| desired_model = _env("PIPER_MODEL", os.path.abspath("models/piper/en_US-amy-medium.onnx")) | |
| if not os.path.exists(desired_model): | |
| print("[PIPER] Downloading voice model (en_US-amy-medium.onnx) …") | |
| local_dir = os.path.dirname(desired_model) | |
| os.makedirs(local_dir, exist_ok=True) | |
| # Use the canonical voice repo on HF | |
| voice_path = huggingface_hub.hf_hub_download( | |
| repo_id="rhasspy/piper-voices", | |
| filename="en/en_US-amy-medium.onnx", | |
| local_dir=local_dir, | |
| local_dir_use_symlinks=False, | |
| ) | |
| if os.path.abspath(voice_path) != os.path.abspath(desired_model): | |
| shutil.copy2(voice_path, desired_model) | |
| os.environ["PIPER_MODEL"] = desired_model | |
| print(f"[PIPER] MODEL = {desired_model}") | |
| return desired_bin, desired_model | |
| # ------------------------- | |
| # App entry | |
| # ------------------------- | |
| def main(): | |
| # 1) Clean runtime/audio on boot | |
| audio_dir = ensure_runtime_audio_dir() | |
| print(f"[BOOT] Cleaning audio dir: {audio_dir}") | |
| cleanup_old_audio(keep_latest=None) | |
| # 2) Ensure model + Piper assets | |
| ensure_model() | |
| ensure_piper() | |
| # 3) Launch Gradio | |
| demo = build_demo() | |
| demo.launch(share=True) # Spaces-friendly | |
| if __name__ == "__main__": | |
| main() |