Eyob-Sol commited on
Commit
769987b
·
verified ·
1 Parent(s): 505561d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +116 -82
app.py CHANGED
@@ -1,112 +1,146 @@
1
  # app.py
2
  from __future__ import annotations
3
-
4
  import os
5
- import sys
6
- from pathlib import Path
7
-
8
- from dotenv import load_dotenv # safe locally; on HF Spaces, env vars come from Secrets
9
- from huggingface_hub import hf_hub_download
10
 
11
- from models.tts_router import cleanup_old_audio, ensure_runtime_audio_dir
12
  from app.gradio_app import build_demo
 
 
13
 
14
 
15
- # -------------------------------
16
- # 1) Env + constants
17
- # -------------------------------
18
- # Load .env if present (local dev). On HF, Secrets are injected as env vars already.
19
- load_dotenv(override=False)
20
-
21
- APP_ROOT = Path(__file__).resolve().parent
22
- MODELS_DIR = APP_ROOT / "models"
23
-
24
-
25
- def _get_env(name: str, default: str | None = None) -> str | None:
26
- val = os.environ.get(name, default)
27
- return val
28
-
29
 
30
- # -------------------------------
31
- # 2) Model bootstrap
32
- # -------------------------------
33
- def ensure_model() -> Path:
34
  """
35
- Ensure a GGUF model exists at LLAMACPP_MODEL_PATH.
36
- If the file does not exist, download it from HF_MODEL_REPO (+ optional HF_MODEL_FILE).
37
- - LLAMACPP_MODEL_PATH: full path & filename where the model should live (e.g., models/qwen2.5-1.5b-instruct-q4_k_m.gguf)
38
- - HF_MODEL_REPO: e.g., "Qwen/Qwen2.5-1.5B-Instruct-GGUF"
39
- - HF_MODEL_FILE (optional): if omitted, we use the basename of LLAMACPP_MODEL_PATH
40
  """
41
- model_path_str = _get_env("LLAMACPP_MODEL_PATH")
42
- repo_id = _get_env("HF_MODEL_REPO")
43
- repo_file = _get_env("HF_MODEL_FILE")
44
 
45
- if not model_path_str or not repo_id:
46
  raise RuntimeError(
47
  "Missing config: set LLAMACPP_MODEL_PATH and HF_MODEL_REPO in .env (optionally HF_MODEL_FILE)."
48
  )
49
 
50
- model_path = (APP_ROOT / model_path_str).resolve()
51
- model_path.parent.mkdir(parents=True, exist_ok=True)
52
-
53
- # If already present, return it
54
- if model_path.exists() and model_path.stat().st_size > 0:
55
  print(f"[MODEL] Found: {model_path}")
56
  return model_path
57
 
58
- # Determine filename to fetch
59
- filename = repo_file or model_path.name
60
- print(f"[MODEL] Downloading '{filename}' from {repo_id} …")
61
- local_file = hf_hub_download(
62
  repo_id=repo_id,
63
- filename=filename,
64
- local_dir=str(model_path.parent),
65
- local_dir_use_symlinks=False, # ensure a real file under ./models
66
- force_filename=model_path.name, # save as the exact target filename
67
- resume_download=True,
68
  )
69
- print(f"[MODEL] Saved at {local_file}")
70
- return Path(local_file)
 
 
 
71
 
72
-
73
- # -------------------------------
74
- # 3) Runtime audio hygiene
75
- # -------------------------------
76
- def clean_runtime_audio_on_boot() -> None:
77
  """
78
- Make sure runtime/audio exists and is empty on startup.
 
79
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  audio_dir = ensure_runtime_audio_dir()
81
- print(f"[AUDIO] Cleaning: {audio_dir}")
82
- try:
83
- cleanup_old_audio(keep_latest=None) # our helper deletes all tts_*.wav
84
- # Also remove any stray user_*.wav we may have persisted
85
- for name in os.listdir(audio_dir):
86
- if name.startswith(("user_", "tmp_")) and name.endswith(".wav"):
87
- try:
88
- os.remove(os.path.join(audio_dir, name))
89
- except Exception as e:
90
- print(f"[AUDIO] Could not delete {name}: {e}")
91
- except Exception as e:
92
- print("[AUDIO] Cleanup error:", e)
93
-
94
-
95
- # -------------------------------
96
- # 4) Main
97
- # -------------------------------
98
- def main() -> None:
99
- # Ensure LLM model is present (HF download if missing)
100
- ensure_model()
101
 
102
- # Clean runtime audio folder
103
- clean_runtime_audio_on_boot()
 
104
 
 
105
  demo = build_demo()
106
-
107
- # HF Spaces: do not set host/port; they are managed by the platform.
108
- # Locally: share=True gives you a public URL; harmless on HF (ignored).
109
- demo.launch(share=True)
110
 
111
 
112
  if __name__ == "__main__":
 
1
  # app.py
2
  from __future__ import annotations
 
3
  import os
4
+ import tarfile
5
+ import io
6
+ import shutil
7
+ from typing import Optional
 
8
 
 
9
  from app.gradio_app import build_demo
10
+ from models.tts_router import cleanup_old_audio, ensure_runtime_audio_dir
11
+ import huggingface_hub
12
 
13
 
14
+ # -------------------------
15
+ # Helpers: models & Piper
16
+ # -------------------------
17
+ def _env(name: str, default: Optional[str] = None) -> Optional[str]:
18
+ # Environment takes precedence over .env defaults (pydantic loads .env)
19
+ return os.environ.get(name, default)
 
 
 
 
 
 
 
 
20
 
21
+ def ensure_model() -> str:
 
 
 
22
  """
23
+ Ensure a local llama.cpp GGUF exists at LLAMACPP_MODEL_PATH.
24
+ If missing, download from HF_MODEL_REPO (and optional HF_MODEL_FILE).
 
 
 
25
  """
26
+ model_path = _env("LLAMACPP_MODEL_PATH")
27
+ repo_id = _env("HF_MODEL_REPO")
28
+ file_name = _env("HF_MODEL_FILE") or (os.path.basename(model_path) if model_path else None)
29
 
30
+ if not model_path or not repo_id or not file_name:
31
  raise RuntimeError(
32
  "Missing config: set LLAMACPP_MODEL_PATH and HF_MODEL_REPO in .env (optionally HF_MODEL_FILE)."
33
  )
34
 
35
+ if os.path.exists(model_path):
 
 
 
 
36
  print(f"[MODEL] Found: {model_path}")
37
  return model_path
38
 
39
+ os.makedirs(os.path.dirname(model_path), exist_ok=True)
40
+ print(f"[MODEL] Downloading {file_name} from {repo_id} …")
41
+ local_path = huggingface_hub.hf_hub_download(
 
42
  repo_id=repo_id,
43
+ filename=file_name,
44
+ local_dir=os.path.dirname(model_path),
45
+ local_dir_use_symlinks=False,
 
 
46
  )
47
+ # If hf_hub_download stored under a hashed subdir, move to exact target path
48
+ if os.path.abspath(local_path) != os.path.abspath(model_path):
49
+ shutil.copy2(local_path, model_path)
50
+ print(f"[MODEL] Ready at {model_path}")
51
+ return model_path
52
 
53
+ def ensure_piper() -> tuple[str, str]:
 
 
 
 
54
  """
55
+ Ensure Piper binary + voice model exist, and set env vars PIPER_BIN, PIPER_MODEL.
56
+ Returns (piper_bin, piper_model).
57
  """
58
+ # --- Ensure binary ---
59
+ desired_bin = _env("PIPER_BIN", os.path.abspath("./bin/piper"))
60
+ if not os.path.exists(desired_bin):
61
+ os.makedirs(os.path.dirname(desired_bin), exist_ok=True)
62
+ # Download a small Linux x86_64 Piper binary tarball from the official repo
63
+ # (Using a known release; if HF changes base image/arch you may need to adjust)
64
+ print("[PIPER] Downloading Piper binary …")
65
+ # v1.2.0 is a common stable tag; adjust if needed
66
+ piper_repo = "rhasspy/piper"
67
+ piper_asset = "piper_linux_x86_64.tar.gz"
68
+ # Pull via HF Hub to avoid GitHub rate limiting on Spaces runners
69
+ # We mirror by leveraging HF's cached files capability (requires file to exist in repo).
70
+ # If you have your own mirror, point HF_PIPER_REPO/HF_PIPER_FILE env vars to it.
71
+ piper_hf_repo = _env("HF_PIPER_REPO")
72
+ piper_hf_file = _env("HF_PIPER_FILE")
73
+ if piper_hf_repo and piper_hf_file:
74
+ tar_path = huggingface_hub.hf_hub_download(
75
+ repo_id=piper_hf_repo,
76
+ filename=piper_hf_file,
77
+ local_dir="./bin",
78
+ local_dir_use_symlinks=False,
79
+ )
80
+ with tarfile.open(tar_path, "r:gz") as tf:
81
+ tf.extractall("./bin")
82
+ else:
83
+ # Direct HTTP fallback (works on Spaces runners)
84
+ import requests
85
+ url = f"https://github.com/{piper_repo}/releases/download/v1.2.0/{piper_asset}"
86
+ r = requests.get(url, timeout=60)
87
+ r.raise_for_status()
88
+ with tarfile.open(fileobj=io.BytesIO(r.content), mode="r:gz") as tf:
89
+ tf.extractall("./bin")
90
+
91
+ # Find the extracted "piper" binary
92
+ candidate = os.path.join("./bin", "piper")
93
+ if not os.path.exists(candidate):
94
+ # sometimes archives unpack into a subdir
95
+ for root, _, files in os.walk("./bin"):
96
+ if "piper" in files:
97
+ candidate = os.path.join(root, "piper")
98
+ break
99
+ if not os.path.exists(candidate):
100
+ raise RuntimeError("[PIPER] Could not locate extracted 'piper' binary.")
101
+ os.chmod(candidate, 0o755)
102
+ desired_bin = os.path.abspath(candidate)
103
+
104
+ os.environ["PIPER_BIN"] = desired_bin
105
+ print(f"[PIPER] BIN = {desired_bin}")
106
+
107
+ # --- Ensure voice model ---
108
+ desired_model = _env("PIPER_MODEL", os.path.abspath("models/piper/en_US-amy-medium.onnx"))
109
+ if not os.path.exists(desired_model):
110
+ print("[PIPER] Downloading voice model (en_US-amy-medium.onnx) …")
111
+ local_dir = os.path.dirname(desired_model)
112
+ os.makedirs(local_dir, exist_ok=True)
113
+ # Use the canonical voice repo on HF
114
+ voice_path = huggingface_hub.hf_hub_download(
115
+ repo_id="rhasspy/piper-voices",
116
+ filename="en/en_US-amy-medium.onnx",
117
+ local_dir=local_dir,
118
+ local_dir_use_symlinks=False,
119
+ )
120
+ if os.path.abspath(voice_path) != os.path.abspath(desired_model):
121
+ shutil.copy2(voice_path, desired_model)
122
+
123
+ os.environ["PIPER_MODEL"] = desired_model
124
+ print(f"[PIPER] MODEL = {desired_model}")
125
+ return desired_bin, desired_model
126
+
127
+
128
+ # -------------------------
129
+ # App entry
130
+ # -------------------------
131
+ def main():
132
+ # 1) Clean runtime/audio on boot
133
  audio_dir = ensure_runtime_audio_dir()
134
+ print(f"[BOOT] Cleaning audio dir: {audio_dir}")
135
+ cleanup_old_audio(keep_latest=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ # 2) Ensure model + Piper assets
138
+ ensure_model()
139
+ ensure_piper()
140
 
141
+ # 3) Launch Gradio
142
  demo = build_demo()
143
+ demo.launch(share=True) # Spaces-friendly
 
 
 
144
 
145
 
146
  if __name__ == "__main__":