Spaces:
Running on Zero
Running on Zero
| """GEPARD — text-to-speech inference Space (ZeroGPU), gradio.Server backend. | |
| Startup order is load-bearing: | |
| 1. ``create_env.setup_dependencies()`` installs nemo-toolkit and re-pins | |
| transformers BEFORE any ML import — the Space image ships gradio 6.20 | |
| which pins huggingface-hub>=1.2, and NeMo's transformers<=4.52 would | |
| pull hub<1.0 at build time. create_env.py upgrades hub first, then | |
| installs nemo with --no-deps, then re-pins transformers==5.3.0. | |
| 2. ``spaces`` is imported before torch so ZeroGPU can patch CUDA calls. | |
| 3. The engine (model + codec + speakers) is built ONCE at module level; | |
| ZeroGPU replays the recorded ``.to("cuda")`` moves when a GPU is | |
| attached, so no per-request reloading happens. | |
| The UI is a custom HTML/CSS/JS frontend served at ``/``; ``@app.api()`` | |
| exposes the synthesis pipeline through Gradio's queue so the JS client | |
| (and ``gradio_client``) can call it without colliding on the GPU. | |
| """ | |
| from create_env import setup_dependencies | |
| setup_dependencies() | |
| try: # ZeroGPU runtime (present on HF Spaces) | |
| import spaces # noqa: E402 | |
| _gpu_decorator = spaces.GPU | |
| except ImportError: # local run — no-op stand-in | |
| def _gpu_decorator(*args, **kwargs): | |
| if args and callable(args[0]): | |
| return args[0] | |
| def _wrap(fn): | |
| return fn | |
| return _wrap | |
| import os # noqa: E402 | |
| from pathlib import Path # noqa: E402 | |
| import numpy as np # noqa: E402 | |
| import scipy.io.wavfile as wavfile # noqa: E402 | |
| from fastapi.responses import HTMLResponse # noqa: E402 | |
| from gradio import Server # noqa: E402 | |
| from gradio.data_classes import FileData # noqa: E402 | |
| from gepard_inference.engine import AppConfig, GenerationParams, GepardEngine # noqa: E402 | |
| from interface import MODE_PRESET # noqa: E402 | |
| CONFIG_PATH = Path(__file__).parent / "config.yaml" | |
| config = AppConfig.from_yaml(CONFIG_PATH) | |
| engine = GepardEngine(config).load() | |
| # Where synthesized WAVs land — same dir as the script so Spaces' persistent | |
| # volume picks them up across restarts. | |
| OUT_DIR = Path(__file__).parent / "_out" | |
| OUT_DIR.mkdir(exist_ok=True) | |
| def _save_wav(sample_rate: int, wave: np.ndarray) -> str: | |
| """Write a float waveform to a 16-bit PCM WAV; return the absolute path.""" | |
| import uuid | |
| # Clip & convert to int16 PCM (browser audio tag handles 16-bit PCM natively). | |
| wave = np.asarray(wave, dtype=np.float32) | |
| peak = float(np.max(np.abs(wave))) if wave.size else 0.0 | |
| if peak > 1.0: | |
| wave = wave / peak | |
| pcm = (wave * 32767.0).clip(-32768, 32767).astype(np.int16) | |
| path = OUT_DIR / f"{uuid.uuid4().hex}.wav" | |
| wavfile.write(path, sample_rate, pcm) | |
| return str(path) | |
| app = Server() | |
| def synthesize( | |
| mode: str, | |
| speaker: str, | |
| ref_audio: str, | |
| text: str, | |
| temperature: float, | |
| top_k: float, | |
| max_frames: float, | |
| repetition_penalty: float, | |
| repetition_window: float, | |
| ) -> FileData: | |
| """Generate speech from text + a preset speaker or a recorded reference. | |
| Runs inside the ZeroGPU context — both the reference encoding (clone | |
| mode) and the autoregressive generation share one GPU session. | |
| Args match the current Gradio Blocks signature exactly so the rest of | |
| the codebase (config defaults, examples) stays a drop-in. | |
| """ | |
| text = (text or "").strip() | |
| if not text: | |
| raise ValueError("Please enter some text to synthesize.") | |
| if mode == MODE_PRESET: | |
| if not speaker: | |
| raise ValueError("Please choose a preset speaker.") | |
| ref_codes = engine.speakers.codes(speaker) | |
| else: | |
| if not ref_audio: | |
| raise ValueError("Please record or upload a reference clip.") | |
| ref_codes = engine.encode_reference(ref_audio) | |
| params = GenerationParams( | |
| temperature=temperature, | |
| top_k=int(top_k), | |
| cfg_scale=config.defaults.cfg_scale, | |
| cfg_frames=config.defaults.cfg_frames, | |
| stop_threshold=config.defaults.stop_threshold, | |
| max_frames=int(max_frames), | |
| repetition_penalty=repetition_penalty, | |
| repetition_window=int(repetition_window), | |
| ) | |
| sample_rate, wave = engine.synthesize(text, ref_codes, params) | |
| return FileData(path=_save_wav(sample_rate, wave)) | |
| async def homepage(): | |
| """Serve the custom HTML/CSS/JS frontend. | |
| Server is a FastAPI app, so plain ``@app.get`` routes coexist naturally | |
| with ``@app.api()`` routes. The frontend lives in ``index.html`` next | |
| to this file. Returning an ``HTMLResponse`` (instead of a plain string) | |
| sets ``Content-Type: text/html`` so the browser actually renders it. | |
| """ | |
| html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html") | |
| with open(html_path, "r", encoding="utf-8") as f: | |
| return HTMLResponse(content=f.read()) | |
| async def public_config(): | |
| """Static config exposed to the frontend (speaker list, defaults, examples).""" | |
| return { | |
| "speakers": [ | |
| {"key": k, "label": config.speaker_labels.get(k, k)} | |
| for k in engine.speakers.names | |
| ], | |
| "defaults": { | |
| "temperature": config.defaults.temperature, | |
| "top_k": config.defaults.top_k, | |
| "max_frames": config.defaults.max_frames, | |
| "repetition_penalty": config.defaults.repetition_penalty, | |
| "repetition_window": config.defaults.repetition_window, | |
| }, | |
| "limits": { | |
| "min_max_frames": 43, | |
| "max_max_frames": 1075, | |
| "min_repetition_window": 0, | |
| "max_repetition_window": 128, | |
| "min_repetition_penalty": 1.0, | |
| "max_repetition_penalty": 1.5, | |
| "min_temperature": 0.05, | |
| "max_temperature": 1.0, | |
| "min_top_k": 0, | |
| "max_top_k": 200, | |
| }, | |
| "examples": [ | |
| {"speaker": ex.speaker, "label": ex.label, "text": ex.text} | |
| for ex in config.examples | |
| ], | |
| "modes": {"preset": MODE_PRESET, "clone": "Clone a voice"}, | |
| } | |
| if __name__ == "__main__": | |
| app.launch(show_error=True) |