"""Kitten TTS demo: Kitten TTS 2 through the KittenML API, and the open-source 0.8 models running on this Space's CPU. Set the KITTENML_API_KEY secret in the Space settings (Kitten TTS 2 needs it; the 0.8 models do not). The limits match the demo on kittenml.com: 4 requests a minute per browser session and 12 per network for every model, 500 characters per request, and for Kitten TTS 2 (which uses the API) 5 minutes of audio per session and 20 per network every six hours. """ import io import os import re import threading import time import gradio as gr import httpx import numpy as np import soundfile as sf API_BASE = os.environ.get("KITTENML_API_BASE_URL", "https://api.kittenml.com/v1").rstrip("/") API_KEY = os.environ.get("KITTENML_API_KEY", "").strip() TTS2_MODEL = "kitten-tts-2-latest" SAMPLE_RATE_08 = 24000 MAX_CHARS = 500 # ── Models ────────────────────────────────────────────────────────────────────── TTS2 = "kitten-tts-2" MODELS = { TTS2: {"label": "Kitten TTS 2", "note": "Our newest model: expressive voices, emotion tags and ten languages. Runs on the KittenML API."}, "mini-0.8": {"label": "0.8 Mini", "repo": "KittenML/kitten-tts-mini-0.8", "note": "Open source, 80M parameters: the best-sounding 0.8 model. Runs on this Space's CPU."}, "micro-0.8": {"label": "0.8 Micro", "repo": "KittenML/kitten-tts-micro-0.8", "note": "Open source, 40M parameters: a balance of quality and speed. Runs on this Space's CPU."}, "nano-0.8": {"label": "0.8 Nano", "repo": "KittenML/kitten-tts-nano-0.8-fp32", "note": "Open source, 15M parameters: small enough for almost any device. Runs on this Space's CPU."}, } VOICES_08 = [("Bella · Warm", "Bella"), ("Jasper · Narrator", "Jasper"), ("Luna · Calm", "Luna"), ("Bruno · Deep", "Bruno"), ("Rosie · Bright", "Rosie"), ("Hugo · Assistant", "Hugo"), ("Kiki · Playful", "Kiki"), ("Leo · Bold", "Leo")] DEFAULT_VOICE_08 = "Jasper" # Kitten TTS 2 voices come from the API. The built-in voices are listed with type "public" (since late October # 2026; "built_in" before). "custom" voices are the key's own clones and are never offered or accepted. OFFERED_VOICE_TYPES = {"public", "built_in"} DEFAULT_VOICE_TTS2 = "saoirse_joyful_irish_female_07" LANGUAGE_NAMES = { "en": "English", "hi": "Hindi", "es": "Spanish", "fr": "French", "de": "German", "it": "Italian", "pt": "Portuguese", "ru": "Russian", "zh": "Chinese", "ar": "Arabic", "ja": "Japanese", "ko": "Korean", "nl": "Dutch", "pl": "Polish", "tr": "Turkish", "sv": "Swedish", "da": "Danish", "fi": "Finnish", "he": "Hebrew", "id": "Indonesian", } # Offered when the voice list cannot be fetched. FALLBACK_TTS2_VOICES = [ {"id": "saoirse_joyful_irish_female_07", "name": "Saoirse — Joyful Irish Female", "language": "en", "preview": False}, {"id": "claire_playful_professional_female_08", "name": "Claire — Playful Professional Female", "language": "en", "preview": False}, {"id": "dante_playful_smooth_young_male_05", "name": "Dante — Playful Smooth Young Male", "language": "en", "preview": False}, {"id": "alfred_tender_older_male_06", "name": "Alfred — Tender Older Male", "language": "en", "preview": False}, {"id": "walter_warm_male_23", "name": "Walter — Warm Male", "language": "en", "preview": False}, {"id": "matthew_exhausted_mechanic_male_01", "name": "Matthew — Exhausted Mechanic Male", "language": "en", "preview": False}, {"id": "french_42", "name": "French (Français)", "language": "fr", "preview": False}, {"id": "hindi_47", "name": "Hindi (हिन्दी)", "language": "hi", "preview": False}, ] TAGS = ["[joyful]", "[excited]", "[tender]", "[sad]", "[nervous]", "", "", ""] REQUESTS_PER_MINUTE = {"session": 4, "network": 12} AUDIO_SECONDS = {"session": 5 * 60, "network": 20 * 60} AUDIO_WINDOW_SECONDS = 6 * 60 * 60 RETRY_STATUSES = {429, 502, 503, 520, 521, 522, 523, 525, 526, 527, 530} http = httpx.Client(headers={"Authorization": f"Bearer {API_KEY}"}, timeout=httpx.Timeout(120, connect=15)) # ── Limits ────────────────────────────────────────────────────────────────────── class Limiter: """The kittenml.com demo's limits, kept in memory: a one-minute request window and a six-hour audio allowance, each counted per browser session and per network.""" def __init__(self): self.lock = threading.Lock() self.requests = {} # (scope, key) -> [window start, requests] self.audio = {} # (scope, key) -> [window start, seconds of audio] def take_request(self, scopes, now=None): """Count one request in every scope. False when any scope is over its limit.""" now = time.time() if now is None else now allowed = True with self.lock: self._prune(now) for scope, key in scopes.items(): window = self.requests.setdefault((scope, key), [now, 0]) if now - window[0] >= 60: window[:] = [now, 0] window[1] += 1 allowed = allowed and window[1] <= REQUESTS_PER_MINUTE[scope] return allowed def audio_reset_in(self, scopes, now=None): """Seconds until the audio allowance resets, or 0 when there is audio left in every scope.""" now = time.time() if now is None else now wait = 0 with self.lock: for scope, key in scopes.items(): start, used = self.audio.get((scope, key), (now, 0)) if now - start < AUDIO_WINDOW_SECONDS and used >= AUDIO_SECONDS[scope]: wait = max(wait, start + AUDIO_WINDOW_SECONDS - now) return wait def spend_audio(self, scopes, seconds, now=None): now = time.time() if now is None else now with self.lock: for scope, key in scopes.items(): window = self.audio.setdefault((scope, key), [now, 0.0]) if now - window[0] >= AUDIO_WINDOW_SECONDS: window[:] = [now, 0.0] window[1] += seconds def _prune(self, now): if len(self.requests) + len(self.audio) < 10_000: return self.requests = {k: v for k, v in self.requests.items() if now - v[0] < 60} self.audio = {k: v for k, v in self.audio.items() if now - v[0] < AUDIO_WINDOW_SECONDS} limiter = Limiter() def visitor(request): """(session key, network key) for a Gradio request; Spaces put the visitor's IP first in X-Forwarded-For.""" forwarded = (request.headers.get("x-forwarded-for") or "").split(",")[0].strip() if request else "" network = forwarded or (request.client.host if request and request.client else "unknown") session = (request.session_hash if request else None) or network return {"session": session, "network": network} # ── Kitten TTS 2 voices (from the API) ────────────────────────────────────────── _voices = {"at": 0.0, "list": FALLBACK_TTS2_VOICES} _voices_lock = threading.Lock() def _clean_name(name): """'07 — Saoirse — Joyful Irish Female' -> 'Saoirse — Joyful Irish Female'.""" return re.sub(r"^\s*\d+\s*—\s*", "", name or "").strip() def tts2_voices(max_age=3600): """Every built-in Kitten TTS 2 voice the API lists, refreshed hourly. Only the built-in voices: the key's own cloned voices must never be offered or accepted. """ with _voices_lock: if API_KEY and time.time() - _voices["at"] > max_age: try: response = http.get(f"{API_BASE}/voices", params={"model": TTS2_MODEL}, timeout=20) response.raise_for_status() listed = [ {"id": v["id"], "name": _clean_name(v.get("name") or v["id"]), "language": v.get("language") or "en", "preview": bool(v.get("experimental"))} for v in response.json().get("data", []) if v.get("type") in OFFERED_VOICE_TYPES and v.get("id") ] if listed: _voices["list"] = listed _voices["at"] = time.time() except (httpx.HTTPError, ValueError) as e: print(f"Could not load the voice list: {e}") _voices["at"] = time.time() - max_age + 60 # try again in a minute return _voices["list"] def language_choices(): """'All languages', then the ten main languages (English first), then the ones still in preview.""" voices = tts2_voices() main = sorted({v["language"] for v in voices if not v["preview"]}, key=lambda c: (c != "en", LANGUAGE_NAMES.get(c, c))) preview = sorted({v["language"] for v in voices if v["preview"]} - set(main), key=lambda c: LANGUAGE_NAMES.get(c, c)) return ([("All languages", "all")] + [(LANGUAGE_NAMES.get(c, c), c) for c in main] + [(f"{LANGUAGE_NAMES.get(c, c)} · preview", c) for c in preview]) def tts2_voice_choices(language="all"): voices = [v for v in tts2_voices() if language in ("all", None, "") or v["language"] == language] voices.sort(key=lambda v: (v["preview"], v["language"] != "en", v["language"])) return [(v["name"] + (" · preview" if v["preview"] else ""), v["id"]) for v in voices] def voice_choices(model, language="all"): return tts2_voice_choices(language) if model == TTS2 else VOICES_08 def default_voice(model, choices): ids = [voice_id for _, voice_id in choices] preferred = DEFAULT_VOICE_TTS2 if model == TTS2 else DEFAULT_VOICE_08 return preferred if preferred in ids else (ids[0] if ids else None) # ── The 0.8 models (open source, loaded on this Space's CPU) ──────────────────── _local = {"models": {}, "error": None} _local_ready = threading.Event() _local_locks = {key: threading.Lock() for key in MODELS if key != TTS2} def _load_local_models(): try: from kittentts import KittenTTS for key, model in MODELS.items(): if key != TTS2: print(f"Loading {model['label']}…") _local["models"][key] = KittenTTS(model["repo"]) print("The 0.8 models are loaded.") except Exception as e: # keep Kitten TTS 2 working even if a local model fails to load _local["error"] = str(e) print(f"Could not load the 0.8 models: {e}") finally: _local_ready.set() threading.Thread(target=_load_local_models, daemon=True).start() def speak_08(model, text, voice, speed): if not _local_ready.wait(timeout=90): raise gr.Error("The 0.8 models are still loading. Please try again in a few seconds.") tts = _local["models"].get(model) if tts is None: raise gr.Error("The 0.8 models could not be loaded on this Space. Kitten TTS 2 still works.") with _local_locks[model]: audio = tts.generate(text, voice=voice, speed=speed) return np.squeeze(audio).astype(np.float32), SAMPLE_RATE_08 # ── Kitten TTS 2 (the KittenML API) ───────────────────────────────────────────── def speak_tts2(text, voice, speed): body = {"model": TTS2_MODEL, "input": text, "voice": voice, "response_format": "wav", "speed": speed} for attempt in range(3): try: response = http.post(f"{API_BASE}/audio/speech", json=body) except httpx.TimeoutException: raise gr.Error("Kitten TTS 2 took too long to answer. Please try again.") except httpx.HTTPError: if attempt < 2: time.sleep((0.4, 1.0)[attempt]) continue raise gr.Error("Could not reach the KittenML API. Please try again.") if response.status_code in RETRY_STATUSES and attempt < 2: time.sleep((0.4, 1.0)[attempt]) continue break if response.status_code == 200: audio, sample_rate = sf.read(io.BytesIO(response.content), dtype="float32") return audio, sample_rate try: message = response.json()["error"]["message"] except (ValueError, KeyError, TypeError): message = "" print(f"KittenML API {response.status_code}: {message or response.text[:300]}") if response.status_code in (400, 422) and message: raise gr.Error(message) if response.status_code in (401, 403): raise gr.Error("This Space's KittenML API key was not accepted.") raise gr.Error("Kitten TTS 2 is busy right now. Please try again in a moment.") # ── Generate ──────────────────────────────────────────────────────────────────── def synthesize(text, model, voice, speed, request: gr.Request): text = (text or "").strip() if not text: raise gr.Error("Please enter some text.") if len(text) > MAX_CHARS: raise gr.Error(f"The demo reads up to {MAX_CHARS} characters at a time.") if model not in MODELS: raise gr.Error("Choose one of the models.") labels = dict((voice_id, label) for label, voice_id in voice_choices(model)) if voice not in labels: raise gr.Error("Choose one of the listed voices.") speed = min(1.5, max(0.7, float(speed or 1.0))) if model == TTS2 and not API_KEY: raise gr.Error("This Space needs its KITTENML_API_KEY secret for Kitten TTS 2. The 0.8 models still work.") scopes = visitor(request) if not limiter.take_request(scopes): raise gr.Error("Demo limit reached. Please try again in about a minute.") if model == TTS2: wait = limiter.audio_reset_in(scopes) if wait: raise gr.Error(f"You have used this demo's Kitten TTS 2 audio for now; it resets in about " f"{max(1, round(wait / 3600))} h. The 0.8 models still work, and platform.kittenml.com " "has 30 free minutes.") started = time.time() if model == TTS2: audio, sample_rate = speak_tts2(text, voice, speed) limiter.spend_audio(scopes, len(audio) / sample_rate) else: audio, sample_rate = speak_08(model, text, voice, speed) seconds = len(audio) / sample_rate stats = (f"**{MODELS[model]['label']}** · {labels[voice].split(' · ')[0].split(' — ')[0]} · " f"{seconds:.1f} s of audio in {time.time() - started:.1f} s") return (sample_rate, audio), stats # ── Interface ─────────────────────────────────────────────────────────────────── def on_model(model, language): tts2 = model == TTS2 choices = voice_choices(model, language if tts2 else "all") return ( gr.update(value=MODELS[model]["note"]), gr.update(visible=tts2), gr.update(visible=tts2, choices=language_choices()), gr.update(choices=choices, value=default_voice(model, choices)), gr.update(visible=tts2), gr.update(visible=not tts2), gr.update(placeholder=PLACEHOLDER_TTS2 if tts2 else PLACEHOLDER_08), ) def on_language(language, voice): choices = tts2_voice_choices(language) ids = [voice_id for _, voice_id in choices] return gr.update(choices=choices, value=voice if voice in ids else default_voice(TTS2, choices)) def on_load(language, voice): choices = tts2_voice_choices(language) ids = [voice_id for _, voice_id in choices] return gr.update(choices=language_choices()), gr.update(choices=choices, value=voice if voice in ids else default_voice(TTS2, choices)) PLACEHOLDER_TTS2 = "Type something for Kitten to say. Add [joyful], or (((emphasis))) for expression." PLACEHOLDER_08 = "Type something for Kitten to say." HEADER = """
KittenML
Text to speech

Kitten TTS

Hear KittenML’s voices. Kitten TTS 2 is our newest and most expressive model; the 0.8 models are open source and run right here on a CPU.

""" MODELS_GUIDE = """

The models

Kitten TTS 2 API
Expressive voices in ten languages, emotion tags and voice cloning.
0.8 Mini 80M · CPU
The best-sounding open-source model.
0.8 Micro 40M · CPU
Quality and speed in balance.
0.8 Nano 15M · CPU
Small enough for almost any device.
""" FOOTER = f"""

Up to {MAX_CHARS} characters per request and a few requests a minute per visitor. Kitten TTS 2 also has a few minutes of audio every six hours; for more, get 30 free minutes on the KittenML platform.

The 0.8 models are open source under Apache 2.0 on GitHub and Hugging Face. Powered by KittenML.

""" theme = gr.themes.Base( primary_hue="neutral", secondary_hue="neutral", neutral_hue="neutral", font=[gr.themes.GoogleFont("Inter"), "Arial", "sans-serif"], radius_size=gr.themes.sizes.radius_lg, ).set( body_background_fill="#f4f4f2", body_background_fill_dark="#f4f4f2", body_text_color="#1b1b1d", body_text_color_dark="#1b1b1d", body_text_color_subdued="#6b6a66", body_text_color_subdued_dark="#6b6a66", background_fill_primary="#ffffff", background_fill_primary_dark="#ffffff", background_fill_secondary="#f7f7f5", background_fill_secondary_dark="#f7f7f5", block_background_fill="#ffffff", block_background_fill_dark="#ffffff", block_border_color="#e3e2de", block_border_color_dark="#e3e2de", block_border_width="1px", block_shadow="none", block_shadow_dark="none", block_label_text_color="#6b6a66", block_label_text_color_dark="#6b6a66", block_title_text_color="#3d3c39", block_title_text_color_dark="#3d3c39", border_color_primary="#e3e2de", border_color_primary_dark="#e3e2de", input_background_fill="#ffffff", input_background_fill_dark="#ffffff", input_border_color="#d6d5d1", input_border_color_dark="#d6d5d1", input_border_color_focus="#6d6c68", input_border_color_focus_dark="#6d6c68", button_primary_background_fill="#151517", button_primary_background_fill_dark="#151517", button_primary_background_fill_hover="#2a2a2e", button_primary_background_fill_hover_dark="#2a2a2e", button_primary_text_color="#ffffff", button_primary_text_color_dark="#ffffff", button_primary_border_color="#151517", button_primary_border_color_dark="#151517", button_secondary_background_fill="#ffffff", button_secondary_background_fill_dark="#ffffff", button_secondary_background_fill_hover="#f1f0ec", button_secondary_background_fill_hover_dark="#f1f0ec", button_secondary_text_color="#2b2b2a", button_secondary_text_color_dark="#2b2b2a", button_secondary_border_color="#d6d5d1", button_secondary_border_color_dark="#d6d5d1", slider_color="#151517", slider_color_dark="#151517", color_accent="#151517", color_accent_soft="#efeeea", color_accent_soft_dark="#efeeea", link_text_color="#1b1b1d", link_text_color_dark="#1b1b1d", table_border_color="#e3e2de", table_border_color_dark="#e3e2de", table_even_background_fill="#ffffff", table_even_background_fill_dark="#ffffff", table_odd_background_fill="#ffffff", table_odd_background_fill_dark="#ffffff", table_row_focus="#f7f7f5", table_row_focus_dark="#f7f7f5", ) HEAD = '' css = """ :root, html, body { color-scheme: light !important; } .gradio-container { max-width: 1080px !important; margin: 0 auto !important; padding: 0 24px 40px !important; background: #f4f4f2 !important; } footer { display: none !important; } /* Header and hero */ .kt-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 22px 0 8px; } .kt-brand { display: inline-flex; align-items: center; gap: 10px; color: #121214 !important; text-decoration: none !important; font: 600 22px/1 Halant, Georgia, serif; letter-spacing: -.02em; } .kt-brand img { width: 34px; height: 34px; } .kt-links { display: flex; align-items: center; gap: 6px; } .kt-links a { padding: 9px 13px; border-radius: 999px; color: #4a4946 !important; text-decoration: none !important; font: 600 13px/1 Inter, Arial, sans-serif; } .kt-links a:hover { background: rgba(21, 22, 25, .06); color: #121214 !important; } .kt-links a.kt-cta { background: #151517; color: #fff !important; padding: 10px 16px; white-space: nowrap; box-shadow: 0 6px 16px -8px rgba(255, 73, 44, .9); } .kt-links a.kt-cta:hover { background: #2a2a2e; } .kt-hero { text-align: center; padding: 38px 0 30px; } .kt-eyebrow { display: inline-block; padding: 8px 14px; border: 1px dashed #bdbcb7; border-radius: 999px; background: rgba(255,255,255,.6); color: #3d3c39; font: 600 11px/1 Inter, Arial, sans-serif; letter-spacing: .08em; text-transform: uppercase; } .kt-hero h1 { margin: 20px 0 12px !important; color: #121214 !important; font: 600 clamp(46px, 7vw, 72px)/.95 Halant, Georgia, serif !important; letter-spacing: -.045em; } .kt-hero p { max-width: 560px; margin: 0 auto !important; color: #5f5e5a !important; font-size: 16px !important; line-height: 1.6 !important; } /* Cards */ .kt-main, .kt-examples { width: 100% !important; } .kt-main { gap: 16px !important; align-items: stretch !important; } .kt-card { background: #ffffff !important; border: 1px solid #e3e2de !important; border-radius: 20px !important; padding: 20px !important; box-shadow: 0 14px 40px rgba(18, 18, 18, .05) !important; } .kt-card .block { border: 0 !important; box-shadow: none !important; padding-left: 0 !important; padding-right: 0 !important; } /* Model picker: one pill per model */ #kt-models { padding: 0 !important; } #kt-models .wrap { display: flex !important; flex-wrap: nowrap; gap: 4px !important; width: fit-content; padding: 4px !important; border-radius: 999px; background: #efeeea; } #kt-models label { margin: 0 !important; padding: 9px 16px !important; border: 0 !important; border-radius: 999px !important; background: transparent !important; box-shadow: none !important; cursor: pointer; font: 600 13px/1 Inter, Arial, sans-serif !important; color: #5f5e5a !important; } #kt-models label:hover { color: #121214 !important; } #kt-models label.selected { background: #151517 !important; color: #ffffff !important; } #kt-models label.selected span { color: #ffffff !important; } #kt-models input[type=radio] { position: absolute; opacity: 0; width: 1px; height: 1px; } .kt-note p { margin: 2px 0 0 !important; color: #6b6a66 !important; font-size: 13.5px !important; } /* Tag chips */ .kt-tags { gap: 6px !important; flex-wrap: wrap !important; } .kt-tags button { flex: 0 0 auto !important; min-width: 0 !important; padding: 5px 10px !important; border-radius: 999px !important; font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace !important; color: #3d3c39 !important; background: #f7f7f5 !important; border: 1px solid #e3e2de !important; } .kt-tags button:hover { background: #efeeea !important; } /* Generate */ #kt-generate { min-height: 50px; border-radius: 999px !important; font: 600 15px/1 Inter, Arial, sans-serif !important; box-shadow: 0 10px 22px -12px rgba(255, 73, 44, .95); } /* Output */ .kt-output .kt-stats p { margin: 0 !important; color: #6b6a66 !important; font-size: 13px !important; } .kt-output .kt-stats strong { color: #1b1b1d !important; } /* The models guide under the output */ .kt-guide { margin-top: 18px; padding-top: 16px; border-top: 1px dashed #d6d5d1; } .kt-guide h3 { margin: 0 0 10px !important; color: #121214 !important; font: 600 20px/1.1 Halant, Georgia, serif !important; letter-spacing: -.02em; } .kt-guide dl { margin: 0; } .kt-guide dl > div { padding: 10px 0; border-bottom: 1px dashed #e3e2de; } .kt-guide dl > div:last-child { border-bottom: 0; } .kt-guide dt { display: flex; align-items: center; gap: 8px; color: #1b1b1d; font: 600 13.5px/1.3 Inter, Arial, sans-serif; } .kt-guide dt span { padding: 3px 7px; border-radius: 999px; background: #efeeea; color: #5f5e5a; font: 600 10.5px/1 Inter, Arial, sans-serif; } .kt-guide dd { margin: 3px 0 0; color: #6b6a66; font-size: 13px; line-height: 1.5; } /* Examples */ .kt-examples button.gallery-item, .kt-examples .gallery-item { border-radius: 999px !important; border-color: #e3e2de !important; background: #f7f7f5 !important; color: #2b2b2a !important; font-size: 13px !important; } .kt-examples .gallery-item:hover { background: #efeeea !important; } .kt-examples { margin-top: 16px !important; } .kt-examples .label, .kt-examples [data-testid="block-label"] { color: #6b6a66 !important; } .kt-examples tr:hover td { background: #f7f7f5 !important; } /* Footer */ .kt-foot { max-width: 760px; margin: 26px auto 0; text-align: center; } .kt-foot p { margin: 0 0 6px !important; color: #77756f !important; font-size: 12.5px !important; line-height: 1.6 !important; } .kt-foot a { display: inline !important; margin: 0 !important; padding: 0 !important; background: none !important; color: #1b1b1d !important; text-decoration: none !important; border: 0 !important; border-bottom: 1px solid #aaa9a5 !important; border-radius: 0 !important; } @media (max-width: 720px) { .gradio-container { padding: 0 14px 32px !important; } .kt-links a:not(.kt-cta) { display: none; } .kt-links a.kt-cta { padding: 9px 13px; font-size: 12.5px; } .kt-brand { font-size: 20px; } #kt-models .wrap { flex-wrap: wrap; width: auto; border-radius: 18px; } .kt-hero { padding: 24px 0 20px; } .kt-card { padding: 16px !important; } } """ gr.set_static_paths(paths=["assets"]) with gr.Blocks(title="Kitten TTS Demo") as demo: gr.HTML(HEADER) with gr.Row(elem_classes="kt-main", equal_height=False): with gr.Column(scale=3, elem_classes="kt-card"): model_picker = gr.Radio( choices=[(m["label"], key) for key, m in MODELS.items()], value=TTS2, show_label=False, container=False, elem_id="kt-models", ) model_note = gr.Markdown(MODELS[TTS2]["note"], elem_classes="kt-note") text_input = gr.Textbox(label="Text", placeholder=PLACEHOLDER_TTS2, lines=6, max_length=MAX_CHARS) with gr.Row(elem_classes="kt-tags") as tags_row: tag_buttons = [gr.Button(tag, size="sm") for tag in TAGS] with gr.Row(): language_select = gr.Dropdown(choices=language_choices(), value="all", label="Language", scale=1) voice_select = gr.Dropdown( choices=tts2_voice_choices(), value=DEFAULT_VOICE_TTS2, label="Voice", filterable=True, allow_custom_value=True, scale=2, ) speed = gr.Slider(0.7, 1.5, value=1.0, step=0.05, label="Speed") generate_btn = gr.Button("Generate speech", variant="primary", elem_id="kt-generate") with gr.Column(scale=2, elem_classes="kt-card kt-output"): audio_output = gr.Audio(label="Output", type="numpy", autoplay=True, interactive=False) stats = gr.Markdown("", elem_classes="kt-stats") gr.HTML(MODELS_GUIDE) with gr.Column(elem_classes="kt-card kt-examples") as examples_tts2: gr.Examples( label="Try an example", example_labels=[ "Saoirse · We actually won the grant!", "Walter · Close your eyes. The stars are out…", "Matthew · Um, I think the engine just made a noise…", "French · Bonjour ! Je suis Kitten TTS 2…", "Hindi · नमस्ते! मैं Kitten TTS 2 हूँ…", ], examples=[ ["We actually won the grant! I can hardly believe it.", "saoirse_joyful_irish_female_07"], ["Close your eyes. The stars are out, and the night is calm and quiet.", "walter_warm_male_23"], ["Um, I think the engine just made a noise it has never made before.", "matthew_exhausted_mechanic_male_01"], ["Bonjour ! Je suis Kitten TTS 2, et je parle aussi français.", "french_42"], ["नमस्ते! मैं Kitten TTS 2 हूँ, और मैं हिन्दी में भी बोल सकती हूँ।", "hindi_47"], ], inputs=[text_input, voice_select], ) with gr.Column(elem_classes="kt-card kt-examples", visible=False) as examples_08: gr.Examples( label="Try an example", example_labels=[ "Jasper · Kitten TTS is an open-source series of tiny models…", "Bella · Good morning! Your coffee is ready…", "Leo · The quick brown fox jumps over the lazy dog…", ], examples=[ ["Kitten TTS is an open-source series of tiny and expressive text-to-speech models for on-device applications.", "Jasper"], ["Good morning! Your coffee is ready, and the weather looks perfect for a walk.", "Bella"], ["The quick brown fox jumps over the lazy dog, then naps in the afternoon sun.", "Leo"], ], inputs=[text_input, voice_select], ) gr.HTML(FOOTER) model_picker.change( on_model, inputs=[model_picker, language_select], outputs=[model_note, tags_row, language_select, voice_select, examples_tts2, examples_08, text_input], api_visibility="private", ) language_select.change(on_language, inputs=[language_select, voice_select], outputs=voice_select, api_visibility="private") for button, tag in zip(tag_buttons, TAGS): button.click(lambda text, tag=tag: ((text or "").rstrip() + " " + tag + " ").lstrip(), inputs=text_input, outputs=text_input, api_visibility="private", queue=False) generate_btn.click(synthesize, inputs=[text_input, model_picker, voice_select, speed], outputs=[audio_output, stats]) demo.load(on_load, inputs=[language_select, voice_select], outputs=[language_select, voice_select], api_visibility="private") if __name__ == "__main__": demo.queue(default_concurrency_limit=4, max_size=32) demo.launch(server_name="0.0.0.0", theme=theme, css=css, head=HEAD)