ema-lightning / app.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
4e001ae verified
Raw History Blame Contribute Delete
6.82 kB
"""EMA Lightning — tiny, fast and accurate Turkish text-to-speech on ZeroGPU.
The model authors' own pip package (`ema-lightning`) provides the whole
inference path: the text frontend (normalizer-tr), the 5.6M-parameter DiT
acoustic model and the 3M-parameter decoder, both read from the Apache-2.0
checkpoint at canberkkkkkk/ema-lightning. 48 kHz speech in four ODE steps.
"""
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") # conv decoder hits transient alloc spikes
import spaces # MUST come before torch / any CUDA-touching import
import time
import gradio as gr
import numpy as np
import torch
from ema_lightning import EMA
MAX_CHARS = 1200 # keeps the whole request well inside a few seconds of GPU
# Module-scope load: ZeroGPU intercepts .to("cuda") and packs the ~34 MB of
# weights to disk, streaming them into VRAM on the first @spaces.GPU call.
tts = EMA()
SAMPLE_RATE = 48000
# The package normally probes the GPU for its best batch size on first use
# (benching batches up to 128). On ZeroGPU the worker's VRAM is shared with
# the platform and the probe has OOM'd on allocator state; a fixed batch of
# one sentence per pass is plenty for a single-user demo of an 87x-real-time
# model, so pin it and skip the probe entirely.
tts._batch_size = 1
# The package loads checkpoints as .to(device).requires_grad_(False), but the
# ZeroGPU hijack snapshots requires_grad at .to() time, so the decoder's
# weights arrive in the worker flagged as grad-tracking. Playhead decodes in a
# background thread (engine.decode is the one stage without @torch.no_grad()),
# which makes the audio require grad and kills .numpy(). Wrap it.
_engine_decode = tts._engine.decode
@torch.no_grad()
def _decode_nograd(*args, **kwargs):
return _engine_decode(*args, **kwargs)
tts._engine.decode = _decode_nograd
@spaces.GPU(duration=10)
def synthesize(text: str, speed: float = 1.0, seed: int = 0, sample_rate: int = 48000):
"""Synthesize Turkish speech from text with EMA Lightning.
Args:
text: Turkish text to read. Digits, dates, times, currency and
percentages are expanded to spoken Turkish automatically; any
written Turkish is accepted and text never raises.
speed: Speaking rate, from 0.25 (four times slower) to 4 (four
times faster). 1.0 is the natural pace.
seed: Fixes the noise draw for a reproducible result. The voice is
fixed regardless of seed.
sample_rate: Output audio rate: 48000, 24000, 16000 or 8000 Hz.
Returns:
A tuple of (sample_rate, waveform) for Gradio's Audio component.
"""
text = (text or "").strip()
if not text:
raise gr.Error("Lütfen okunacak bir metin girin. / Please enter some text.")
if len(text) > MAX_CHARS:
raise gr.Error(
f"Metin çok uzun ({len(text)} karakter). Lütfen {MAX_CHARS} karakterin "
f"altına indirin. / Text too long ({len(text)} chars); keep it under "
f"{MAX_CHARS}."
)
speed = float(speed)
if not 0.25 <= speed <= 4:
speed = 1.0
seed = max(0, int(seed))
sample_rate = int(sample_rate) if int(sample_rate) in (48000, 24000, 16000, 8000) else 48000
t0 = time.perf_counter()
speech = tts.say(text, speed=speed, seed=seed, sample_rate=sample_rate)
print(
f"[ema-lightning] {len(text)} chars, speed={speed}, seed={seed} -> "
f"{time.perf_counter() - t0:.2f}s synth, {speech.duration:.2f}s audio",
flush=True,
)
return (speech.sample_rate, np.asarray(speech.audio, dtype=np.float32))
CSS = """
#col-container { max-width: 900px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks() as demo:
gr.Markdown(
"# ⚡ EMA Lightning — Turkish Text-to-Speech\n"
"An 8.6M-parameter (34 MB) flow-matching TTS that reads Turkish "
"characters directly and synthesizes 48 kHz speech in four ODE steps — "
"**0.92% WER on "
"[Freya-TR-Eval](https://huggingface.co/datasets/freyavoice/freya-tr-eval)**, "
"the lowest of every system measured, including ElevenLabs v4, Gemini 3.8 "
"and the 2.38B-parameter Trendyol-TTS. One fixed voice, no cloning, "
"Apache-2.0.\n\n"
"[Model card](https://huggingface.co/canberkkkkkk/ema-lightning) · "
"[PyPI package](https://pypi.org/project/ema-lightning/) · "
"Numbers, dates, times and currency are expanded to spoken Turkish "
"before the model reads them."
)
with gr.Column(elem_id="col-container"):
text_input = gr.Textbox(
label="Turkish text",
placeholder="Merhaba, size nasıl yardımcı olabilirim?",
lines=4,
value="Merhaba, size nasıl yardımcı olabilirim?",
)
run_btn = gr.Button("Generate Speech", variant="primary")
audio_output = gr.Audio(label="Generated speech", type="numpy")
with gr.Accordion("Advanced settings", open=False):
speed_slider = gr.Slider(
minimum=0.25, maximum=4.0, value=1.0, step=0.05,
label="Speed",
info="Speaking rate. 1.0 is the natural pace.",
)
seed_input = gr.Number(
label="Seed",
value=0, precision=0,
info="Fixes the noise draw for a reproducible result. "
"The voice is fixed regardless of seed.",
)
rate_dropdown = gr.Dropdown(
choices=[48000, 24000, 16000, 8000],
value=48000,
label="Sample rate (Hz)",
info="The model generates at 48 kHz and resamples cleanly.",
)
gr.Examples(
examples=[
["Model sadece sekiz 8.6 milyon parametre ve çok hızlı.", 1.0, 0, 48000],
["15 Ekim 2026 Çarşamba günü saat 14:30'da, 1.250.000 TL "
"tutarındaki ödemeniz hesabınıza yatırılacak.", 1.0, 0, 48000],
["Sabahın erken saatlerinde liman henüz uyanmamıştı. "
"Balıkçılar ağlarını sessizce topluyor, martılar ise "
"teknelerin etrafında dönerek şanslarını deniyordu.", 1.0, 0, 48000],
],
inputs=[text_input, speed_slider, seed_input, rate_dropdown],
outputs=[audio_output],
fn=synthesize,
cache_examples=True,
cache_mode="lazy",
)
run_btn.click(
fn=synthesize,
inputs=[text_input, speed_slider, seed_input, rate_dropdown],
outputs=[audio_output],
api_name="synthesize",
)
demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS)