import gradio as gr import numpy as np import base64 from pathlib import Path SAMPLE_RATE = 24000 ROOT_DIR = Path(__file__).parent LOGO_PATH = ROOT_DIR / "assets" / "kittenml_logo.svg" MODELS = { "Nano (15M - Fastest)": "KittenML/kitten-tts-nano-0.8-fp32", "Micro (40M - Balanced)": "KittenML/kitten-tts-micro-0.8", "Mini (80M - Best Quality)": "KittenML/kitten-tts-mini-0.8", } VOICES = [ "Bella", "Jasper", "Luna", "Bruno", "Rosie", "Hugo", "Kiki", "Leo", ] EXAMPLES = [ ( "Space is a three-dimensional continuum containing positions and directions.", "Micro (40M - Balanced)", "Jasper", 1.0, ), ( "She picked up her coffee and walked toward the window.", "Mini (80M - Best Quality)", "Luna", 1.0, ), ( "The sun set slowly over the calm, quiet lake", "Nano (15M - Fastest)", "Bella", 1.1, ), ] _model_cache: dict[str, object] = {} def logo_data_uri() -> str: if not LOGO_PATH.exists(): return "https://kittenml.com/assets/kittenml_logo.svg" encoded_logo = base64.b64encode(LOGO_PATH.read_bytes()).decode("ascii") return f"data:image/svg+xml;base64,{encoded_logo}" def get_model(model_name: str): if model_name not in _model_cache: from kittentts import KittenTTS print(f"Loading {model_name}...") _model_cache[model_name] = KittenTTS(MODELS[model_name]) print(f"{model_name} loaded.") return _model_cache[model_name] def synthesize(text: str, model_name: str, voice: str, speed: float): if not text or not text.strip(): raise gr.Error("Please enter some text.") tts = get_model(model_name) # Note: speed parameter may not be supported in v0.8 # If you get an error, remove speed=speed from the generate call try: audio = tts.generate(text.strip(), voice=voice, speed=speed) except TypeError: # Fallback if speed is not supported audio = tts.generate(text.strip(), voice=voice) # audio shape is (1, samples) or (samples,) — normalize to 1-D audio = np.squeeze(audio) return (SAMPLE_RATE, audio) theme = gr.themes.Base( primary_hue="neutral", secondary_hue="neutral", neutral_hue="neutral", font=gr.themes.GoogleFont("Inter"), ).set( body_background_fill="#f7f7f8", body_background_fill_dark="#f7f7f8", block_background_fill="#ffffff", block_background_fill_dark="#ffffff", block_border_color="#e7e7e8", block_border_color_dark="#e7e7e8", block_radius="8px", block_shadow="0 1px 2px rgba(0, 0, 0, 0.04)", block_shadow_dark="0 1px 2px rgba(0, 0, 0, 0.04)", button_primary_background_fill="#101010", button_primary_background_fill_hover="#18181b", button_primary_text_color="white", button_primary_border_color="#101010", input_background_fill="white", input_background_fill_dark="white", input_border_color="transparent", input_border_color_focus="#101010", slider_color="#101010", table_border_color="transparent", table_even_background_fill="white", table_odd_background_fill="#fafafa", table_row_focus="white", ) css = """ :root, html, body { color-scheme: light !important; } body { background: #f7f7f8 !important; } .gradio-container, .main, .app, .contain, .wrap, .gap, .form { background: transparent !important; } .gradio-container { max-width: 1000px !important; margin: 0 auto !important; padding: 22px 18px 30px !important; --section-inset: 12px; } footer { display: none !important; } .hero { position: relative; overflow: hidden; width: 100%; max-width: 100%; box-sizing: border-box; border: 1px solid rgba(150, 150, 156, 0.42); border-radius: 8px; background: linear-gradient(116deg, rgba(255, 255, 255, 0.78) 0%, rgba(255, 255, 255, 0) 17%, rgba(255, 255, 255, 0.55) 34%, rgba(255, 255, 255, 0) 51%), linear-gradient(145deg, #f8f8f9 0%, #dedee3 31%, #fdfdfd 54%, #bfc0c7 100%); display: flex; flex-direction: column; justify-content: space-between; min-height: 266px; padding: 38px 40px 34px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82), inset 0 -1px 0 rgba(24, 24, 27, 0.08), 0 22px 54px rgba(24, 24, 27, 0.12), 0 1px 2px rgba(24, 24, 27, 0.08); } .hero::before { background: repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.16) 0, rgba(255, 255, 255, 0.16) 1px, rgba(35, 35, 39, 0.025) 2px, rgba(35, 35, 39, 0.025) 4px), linear-gradient(90deg, rgba(17, 17, 17, 0.11), rgba(17, 17, 17, 0) 42%); content: ""; inset: 0; opacity: 0.55; pointer-events: none; position: absolute; } .hero::after { background: linear-gradient(180deg, rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0) 48%, rgba(24, 24, 27, 0.035)); content: ""; inset: 0; pointer-events: none; position: absolute; } .brand-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 38px; position: relative; z-index: 2; } .brand-lockup { align-items: center; background: #ffffff; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.92); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88), 0 12px 28px rgba(24, 24, 27, 0.10), 0 1px 2px rgba(24, 24, 27, 0.08); display: inline-flex; gap: 11px; padding: 9px 15px 9px 10px; } .brand-mark { height: 34px; width: auto; display: block; } .brand-lockup span { color: #111111; font-size: 13px; font-weight: 750; } .status-pill { border: 0; border-radius: 999px; color: #111111; font-size: 12px; font-weight: 700; padding: 9px 14px; background: #ffffff; border: 1px solid rgba(255, 255, 255, 0.92); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88), 0 12px 28px rgba(24, 24, 27, 0.10), 0 1px 2px rgba(24, 24, 27, 0.08); } .hero h1 { background: linear-gradient(180deg, #050506 0%, #2f3035 48%, #070708 100%); -webkit-background-clip: text; background-clip: text; color: transparent; font-size: clamp(40px, 5.4vw, 66px); font-weight: 820; letter-spacing: 0; line-height: 0.96; margin: 0; max-width: 780px; position: relative; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.42); z-index: 2; } .spec-grid { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 22px; max-width: 520px; position: relative; z-index: 2; } .spec { align-items: center; background: rgba(255, 255, 255, 0.68); border: 1px solid rgba(255, 255, 255, 0.76); border-radius: 999px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.64), 0 8px 18px rgba(24, 24, 27, 0.08); display: inline-flex; padding: 7px 12px; } .spec strong { color: #111111; font-size: 12px; font-weight: 700; line-height: 1; } .studio-layout { align-items: stretch !important; gap: 14px !important; margin: 14px var(--section-inset) 0 !important; width: calc(100% - (var(--section-inset) * 2)) !important; max-width: calc(100% - (var(--section-inset) * 2)) !important; box-sizing: border-box; } .studio-layout.unequal-height { align-items: stretch !important; } .studio-card { align-self: stretch !important; border: 1px solid #e7e7e8 !important; border-radius: 8px !important; background: #ffffff !important; box-sizing: border-box !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04) !important; padding: 16px !important; } .gradio-container { --body-text-color: #111111 !important; --block-label-text-color: #2c2924 !important; --block-title-text-color: #2c2924 !important; --input-text-color: #111111 !important; } .gradio-container .block, .gradio-container .panel { background: transparent !important; border-color: transparent !important; border-width: 0 !important; box-shadow: none !important; color: #111111 !important; } .gradio-container .block, .gradio-container .block > *, .gradio-container .form, .gradio-container .wrap, .gradio-container .panel, .gradio-container .gap { box-shadow: none !important; } .gradio-container .block *, .gradio-container .form * { color: #111111 !important; } .gradio-container label, .gradio-container label span, .gradio-container .block-label, .gradio-container .block-title, .gradio-container [data-testid="block-label"], .gradio-container [data-testid="block-label"] * { color: #2c2924 !important; font-weight: 650 !important; } .gradio-container [data-testid="block-label"] { background: transparent !important; border-color: transparent !important; } .gradio-container textarea, .gradio-container .tab-like-container { background: #f7f7f7 !important; border-color: transparent !important; border-width: 0 !important; border-radius: 8px !important; box-shadow: none !important; } .gradio-container .wrap-inner { background: transparent !important; border-radius: 8px !important; box-shadow: none !important; } .gradio-container .secondary-wrap, .gradio-container .wrap-inner > *, .gradio-container input, .gradio-container input[role="listbox"] { background: transparent !important; border-color: transparent !important; border-width: 0 !important; box-shadow: none !important; } .gradio-container textarea { min-height: 128px !important; } .gradio-container input[role="listbox"] { background: #f7f7f7 !important; border-radius: 8px !important; min-height: 42px !important; padding: 0 14px !important; } .gradio-container textarea, .gradio-container input { color: #111111 !important; } ::placeholder { color: #7f7f7f !important; } .generate-button button { min-height: 46px !important; border-radius: 8px !important; font-weight: 750 !important; box-shadow: none !important; } .generate-button button, .generate-button button *, button.primary, button.primary * { color: #ffffff !important; } .gradio-container button:hover { background: #eeeeee !important; color: #111111 !important; } .gradio-container button:hover * { color: #111111 !important; } .generate-button button:hover, .generate-button button:hover *, button.primary:hover, button.primary:hover * { background: #18181b !important; color: #ffffff !important; } .gradio-container ul.options, .gradio-container .options { background: #ffffff !important; border: 1px solid #e5e5e5 !important; border-radius: 8px !important; box-shadow: 0 14px 34px rgba(0, 0, 0, 0.12) !important; overflow: hidden !important; } .gradio-container .wrap-inner.show_options, .gradio-container .wrap-inner.show_options * { background: #ffffff !important; color: #111111 !important; } .gradio-container ul.options li, .gradio-container .options li, .gradio-container [role="option"] { background: #ffffff !important; color: #111111 !important; } .gradio-container ul.options li:hover, .gradio-container ul.options li.selected, .gradio-container .options li:hover, .gradio-container .options li.selected, .gradio-container [role="option"]:hover, .gradio-container [role="option"][aria-selected="true"] { background: #f0f0f0 !important; color: #111111 !important; } .output-panel { justify-content: space-between !important; min-height: 100%; } .output-panel audio, .output-panel [data-testid="audio"] { min-height: 184px !important; } .output-panel .progress-text.meta-text { align-items: center !important; background: #f4f4f5 !important; border: 1px solid #e4e4e7 !important; border-radius: 999px !important; color: transparent !important; display: inline-flex !important; font-family: Inter, Arial, sans-serif !important; font-size: 0 !important; font-weight: 650 !important; justify-content: center !important; line-height: 1 !important; margin: 10px auto 0 !important; min-width: 0 !important; padding: 7px 10px !important; position: static !important; text-align: center !important; width: fit-content !important; } .output-panel .progress-text.meta-text::before { color: #3f3f46; content: "Generating audio"; font-size: 12px; letter-spacing: 0; } .output-note { border: 0; border-radius: 8px; margin-top: 16px; padding: 14px 15px; background: #f7f7f7; color: #3f3f3f; font-size: 13px; line-height: 1.5; } .output-note strong { color: #151515; display: block; margin-bottom: 4px; } .examples-panel { margin: 14px var(--section-inset) 0 !important; padding: 14px !important; width: calc(100% - (var(--section-inset) * 2)) !important; max-width: calc(100% - (var(--section-inset) * 2)) !important; box-sizing: border-box; background: #ffffff !important; border: 1px solid #e5e5e5 !important; border-radius: 8px !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04) !important; overflow: hidden !important; } .examples-header { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 10px; } .examples-header strong { color: #111111; font-size: 15px; } .examples-header span { color: #666666; font-size: 12px; } .example-row { align-items: center !important; background: #f8f8f8 !important; border: 1px solid #eeeeee !important; border-radius: 8px !important; margin-top: 8px !important; padding: 10px 10px 10px 12px !important; transition: background 120ms ease, border-color 120ms ease; } .example-row > .block { min-width: 0 !important; } .example-row:hover { background: #f4f4f5 !important; border-color: #e2e2e3 !important; } .example-copy { display: grid; grid-template-columns: minmax(0, 1fr) 190px 80px 52px; gap: 14px; align-items: center; width: 100%; } .example-copy strong, .example-copy span { color: #111111; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .example-copy strong { font-size: 13px; font-weight: 650; } .example-copy span { color: #5c5c5c; font-size: 12px; } .load-example { flex: 0 0 72px !important; max-width: 72px !important; min-width: 72px !important; width: 72px !important; } .load-example { align-items: center !important; background: #111111 !important; border: 1px solid #111111 !important; border-radius: 8px !important; color: #ffffff !important; display: flex !important; font-size: 13px !important; font-weight: 750 !important; height: 40px !important; justify-content: center !important; line-height: 1 !important; min-width: 72px !important; padding: 0 !important; width: 72px !important; } .gradio-container button.load-example, button.load-example { background: #111111 !important; border-color: #111111 !important; color: #ffffff !important; } .gradio-container button.load-example *, button.load-example * { background: transparent !important; color: #ffffff !important; } .gradio-container button.load-example:hover, button.load-example:hover { background: #18181b !important; border-color: #18181b !important; color: #ffffff !important; } .gradio-container button.load-example:hover *, button.load-example:hover * { background: transparent !important; color: #ffffff !important; } textarea, input, select, table { border-width: 0 !important; box-shadow: none !important; } @media (max-width: 760px) { .gradio-container { padding: 16px 12px 24px !important; } .hero { min-height: 230px; padding: 22px; } .brand-row { align-items: flex-start; flex-direction: column; margin-bottom: 24px; } .hero h1 { font-size: clamp(38px, 13vw, 54px); } .spec-grid { gap: 7px; } .spec { max-width: 100%; } .example-copy { grid-template-columns: 1fr; gap: 4px; } } """ hero_html = f"""
KittenML KittenML
On-device friendly TTS

KittenTTS Demo

24 kHz audio
8 voices
15M-80M models
""" with gr.Blocks(title="KittenTTS Voice Studio") as demo: gr.HTML(hero_html) with gr.Row(elem_classes=["studio-layout"]): with gr.Column(scale=2, elem_classes=["studio-card"]): text_input = gr.Textbox( label="Script", placeholder="Write a short passage for KittenTTS to read aloud.", lines=4, max_lines=6, ) with gr.Row(): model_select = gr.Dropdown( choices=list(MODELS.keys()), value="Micro (40M - Balanced)", label="Model", ) voice_select = gr.Dropdown( choices=VOICES, value="Jasper", label="Voice", ) speed_slider = gr.Slider( minimum=0.5, maximum=2.0, value=1.0, step=0.05, label="Pace", ) generate_btn = gr.Button( "Generate speech", variant="primary", elem_classes=["generate-button"], ) with gr.Column(scale=1, elem_classes=["studio-card", "output-panel"]): audio_output = gr.Audio(label="Generated audio", type="numpy") gr.HTML( """
Model loading The selected model loads on first use, then stays cached for faster generations.
""" ) generate_btn.click( fn=synthesize, inputs=[text_input, model_select, voice_select, speed_slider], outputs=audio_output, ) with gr.Column(elem_classes=["examples-panel"]): gr.HTML( """
Examples Load a sample into the studio
""" ) for text, model, voice, pace in EXAMPLES: with gr.Row(elem_classes=["example-row"]): gr.HTML( f"""
{text} {model} {voice} {pace:g}x
""" ) load_btn = gr.Button("Load", elem_classes=["load-example"]) load_btn.click( fn=lambda text=text, model=model, voice=voice, pace=pace: ( text, model, voice, pace, ), inputs=[], outputs=[text_input, model_select, voice_select, speed_slider], ) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", theme=theme, css=css)