KittenTTS-Demo / app.py
dewan-sl's picture
Polish KittenTTS demo UI
d88ed91 verified
Raw
History Blame
19.7 kB
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"""
<section class="hero">
<div class="brand-row">
<div class="brand-lockup">
<img class="brand-mark" src="{logo_data_uri()}" alt="KittenML">
<span>KittenML</span>
</div>
<div class="status-pill">On-device friendly TTS</div>
</div>
<h1>KittenTTS Demo</h1>
<div class="spec-grid">
<div class="spec"><strong>24 kHz audio</strong></div>
<div class="spec"><strong>8 voices</strong></div>
<div class="spec"><strong>15M-80M models</strong></div>
</div>
</section>
"""
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(
"""
<div class="output-note">
<strong>Model loading</strong>
The selected model loads on first use, then stays cached for faster generations.
</div>
"""
)
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(
"""
<div class="examples-header">
<strong>Examples</strong>
<span>Load a sample into the studio</span>
</div>
"""
)
for text, model, voice, pace in EXAMPLES:
with gr.Row(elem_classes=["example-row"]):
gr.HTML(
f"""
<div class="example-copy">
<strong>{text}</strong>
<span>{model}</span>
<span>{voice}</span>
<span>{pace:g}x</span>
</div>
"""
)
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)