Spaces:
Running on Zero
Running on Zero
| """Gradio UI for the GEPARD TTS Space — layout, cheetah theme, event wiring. | |
| The interface is deliberately free of model code: it receives a fully wired | |
| ``synthesize_fn`` callable (already wrapped with the ZeroGPU decorator by | |
| ``app.py``) and only handles presentation. | |
| """ | |
| from __future__ import annotations | |
| from typing import Callable, List | |
| import gradio as gr | |
| from gepard_inference.engine import AppConfig | |
| MODE_PRESET = "Preset speaker" | |
| MODE_CLONE = "Clone a voice" | |
| # Cheetah palette: sunlit fur (yellow→orange), hunting-red accents, savanna green. | |
| _CSS = """ | |
| .gepard-header { | |
| text-align: center; | |
| padding: 1.1rem 0 0.4rem 0; | |
| } | |
| .gepard-header h1 { | |
| font-size: 2.6rem; | |
| font-weight: 800; | |
| margin: 0; | |
| background: linear-gradient(90deg, #eab308 0%, #f59e0b 30%, #ea580c 65%, #dc2626 100%); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| letter-spacing: 0.04em; | |
| } | |
| .gepard-header p { | |
| margin: 0.35rem 0 0 0; | |
| font-size: 1.02rem; | |
| color: var(--body-text-color-subdued); | |
| } | |
| .gepard-header .stripe { | |
| height: 4px; | |
| width: 240px; | |
| margin: 0.8rem auto 0 auto; | |
| border-radius: 2px; | |
| background: repeating-linear-gradient( | |
| 90deg, #eab308 0 24px, #ea580c 24px 48px, #dc2626 48px 72px, #16a34a 72px 96px); | |
| } | |
| #generate-btn { | |
| background: linear-gradient(90deg, #f59e0b 0%, #ea580c 60%, #dc2626 100%); | |
| color: white; | |
| font-weight: 700; | |
| border: none; | |
| } | |
| #generate-btn:hover { filter: brightness(1.07); } | |
| /* Slimmer sliders in Generation settings: thinner track + smaller thumb. */ | |
| input[type="range"] { | |
| height: 4px; | |
| } | |
| input[type="range"]::-webkit-slider-runnable-track { height: 4px; } | |
| input[type="range"]::-moz-range-track { height: 4px; } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| height: 13px; | |
| width: 13px; | |
| border-radius: 50%; | |
| margin-top: -4.5px; | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| height: 13px; | |
| width: 13px; | |
| border: none; | |
| border-radius: 50%; | |
| } | |
| """ | |
| def build_theme() -> gr.themes.Base: | |
| """Bright cheetah-colored theme: orange primary, green secondary, warm neutrals.""" | |
| return gr.themes.Soft( | |
| primary_hue=gr.themes.colors.orange, | |
| secondary_hue=gr.themes.colors.green, | |
| neutral_hue=gr.themes.colors.stone, | |
| font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], | |
| ).set( | |
| slider_color="*primary_500", | |
| checkbox_background_color_selected="*secondary_500", | |
| button_secondary_background_fill="*secondary_100", | |
| block_title_text_color="*primary_600", | |
| accordion_text_color="*primary_600", | |
| ) | |
| class GepardInterface: | |
| """Builds the Gradio Blocks app for GEPARD. | |
| Args: | |
| config: Application config (slider defaults, speaker names, limits). | |
| speaker_names: Preset speaker display names for the dropdown. | |
| synthesize_fn: Callable with the signature | |
| ``(mode, speaker, ref_audio, text, temperature, top_k, | |
| max_frames, repetition_penalty, repetition_window) | |
| -> (sr, np.ndarray)``. CFG and stop-threshold are not UI knobs; | |
| they are driven by config defaults (+ the runner's CFG length gate). | |
| Wrapped with the ZeroGPU decorator by the caller. | |
| """ | |
| def __init__( | |
| self, | |
| config: AppConfig, | |
| speaker_names: List[str], | |
| synthesize_fn: Callable, | |
| ): | |
| self.config = config | |
| self.speaker_names = speaker_names | |
| self.synthesize_fn = synthesize_fn | |
| # ------------------------------------------------------------------ | |
| # Layout | |
| # ------------------------------------------------------------------ | |
| def build(self) -> gr.Blocks: | |
| """Assemble and return the Blocks app (call ``.launch()`` on it).""" | |
| d = self.config.defaults | |
| # Preset examples come from config (examples: section), already | |
| # validated against the speaker roster at load time. The table shows | |
| # the human-readable label; the label maps back to the speaker key. | |
| example_rows = [[ex.label, ex.text] for ex in self.config.examples] | |
| label_to_speaker = {ex.label: ex.speaker for ex in self.config.examples} | |
| def run_example(example_label: str, example_text: str): | |
| """Synthesize a preset example row with the default generation params. | |
| Clicking a row runs this once (in a ZeroGPU request context) and | |
| Gradio caches the audio, so later clicks just replay it. Returns the | |
| audio plus the resolved speaker key so the dropdown stays in sync. | |
| """ | |
| speaker_name = label_to_speaker.get(example_label, example_label) | |
| audio = self.synthesize_fn( | |
| MODE_PRESET, speaker_name, None, example_text, | |
| d.temperature, d.top_k, | |
| d.max_frames, d.repetition_penalty, d.repetition_window, | |
| ) | |
| return audio, speaker_name | |
| with gr.Blocks(theme=build_theme(), css=_CSS, title="GEPARD TTS") as demo: | |
| gr.HTML( | |
| """ | |
| <div class="gepard-header"> | |
| <h1>🐆 GEPARD</h1> | |
| <p>Fast, spotted text-to-speech model with voice cloning</p> | |
| <div class="stripe"></div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| mode = gr.Radio( | |
| choices=[MODE_PRESET, MODE_CLONE], | |
| value=MODE_PRESET, | |
| label="Voice source", | |
| show_label=False, | |
| info="Pick a bundled voice, or clone one from your own recording.", | |
| ) | |
| speaker = gr.Dropdown( | |
| # (label, value): show the human-readable label, keep | |
| # the speaker key as the selected value. | |
| choices=[ | |
| (self.config.speaker_labels.get(k, k), k) | |
| for k in self.speaker_names | |
| ], | |
| value=self.speaker_names[0] if self.speaker_names else None, | |
| label="Preset speaker", | |
| show_label=False, | |
| visible=True, | |
| ) | |
| ref_audio = gr.Audio( | |
| sources=["microphone", "upload"], | |
| type="filepath", | |
| label=f"Reference voice (≤ {int(self.config.max_ref_seconds)}s used)", | |
| visible=False, | |
| ) | |
| text = gr.Textbox( | |
| label="Text", | |
| show_label=False, | |
| placeholder="Type what the cheetah should say…", | |
| lines=4, | |
| ) | |
| with gr.Accordion("Generation settings", open=False): | |
| temperature = gr.Slider( | |
| 0.05, 1.0, value=d.temperature, step=0.05, | |
| label="Temperature", | |
| info="Sampling temperature; lower = more stable, higher = more varied.", | |
| ) | |
| top_k = gr.Slider( | |
| 0, 200, value=d.top_k, step=1, | |
| label="Top-k", | |
| info="Keep only the k most likely tokens per head (0 = disabled).", | |
| ) | |
| max_frames = gr.Slider( | |
| 43, 1075, value=d.max_frames, step=43, | |
| label="Max frames", | |
| info="Cap on generated frames (~21.5 fps; 43 frames ≈ 2 s, 1075 ≈ 50 s max).", | |
| ) | |
| repetition_penalty = gr.Slider( | |
| 1.0, 1.5, value=d.repetition_penalty, step=0.01, | |
| label="Repetition penalty", | |
| info="Penalize recently used tokens (1.0 = disabled).", | |
| ) | |
| repetition_window = gr.Slider( | |
| 0, 128, value=d.repetition_window, step=4, | |
| label="Repetition window", | |
| info="Recent frames tracked by the penalty (0 = full history).", | |
| ) | |
| with gr.Column(scale=4): | |
| output_audio = gr.Audio( | |
| label="Generated speech", | |
| type="numpy", | |
| interactive=False, | |
| ) | |
| generate_btn = gr.Button( | |
| "🐆 Generate speech", variant="primary", elem_id="generate-btn", | |
| ) | |
| if example_rows: | |
| # Hidden carrier: holds the clicked row's label so the examples | |
| # table can display labels while run_example maps them to keys. | |
| example_label = gr.Textbox(visible=False, label="Speaker") | |
| gr.Examples( | |
| label="Examples", | |
| examples=example_rows, | |
| inputs=[example_label, text], | |
| outputs=[output_audio, speaker], | |
| fn=run_example, | |
| cache_examples="lazy", | |
| ) | |
| # Preset mode shows the dropdown; clone mode shows the recorder. | |
| mode.change( | |
| fn=self._toggle_mode, | |
| inputs=[mode], | |
| outputs=[speaker, ref_audio], | |
| show_progress="hidden", | |
| ) | |
| generate_btn.click( | |
| fn=self.synthesize_fn, | |
| inputs=[ | |
| mode, speaker, ref_audio, text, | |
| temperature, top_k, | |
| max_frames, repetition_penalty, repetition_window, | |
| ], | |
| outputs=[output_audio], | |
| ) | |
| return demo | |
| # ------------------------------------------------------------------ | |
| # Events | |
| # ------------------------------------------------------------------ | |
| def _toggle_mode(mode: str): | |
| """Show the speaker dropdown in preset mode, the recorder in clone mode.""" | |
| is_preset = mode == MODE_PRESET | |
| return ( | |
| gr.update(visible=is_preset), | |
| gr.update(visible=not is_preset), | |
| ) | |