| """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" |
|
|
| |
| _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 |
|
|
| |
| |
| |
|
|
| def build(self) -> gr.Blocks: |
| """Assemble and return the Blocks app (call ``.launch()`` on it).""" |
| d = self.config.defaults |
|
|
| |
| |
| |
| 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( |
| |
| |
| 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: |
| |
| |
| 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", |
| ) |
|
|
| |
| 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 |
|
|
| |
| |
| |
|
|
| @staticmethod |
| 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), |
| ) |
|
|