Polish KittenTTS demo UI

#2
Files changed (3) hide show
  1. .gitignore +3 -0
  2. app.py +579 -140
  3. assets/kittenml_logo.svg +1 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .DS_Store
2
+ __pycache__/
3
+ .venv/
app.py CHANGED
@@ -1,9 +1,11 @@
1
  import gradio as gr
2
  import numpy as np
3
- import os
4
- from kittentts import KittenTTS
5
 
6
  SAMPLE_RATE = 24000
 
 
7
 
8
  MODELS = {
9
  "Nano (15M - Fastest)": "KittenML/kitten-tts-nano-0.8-fp32",
@@ -22,16 +24,46 @@ VOICES = [
22
  "Leo",
23
  ]
24
 
25
- # Initialize all models at startup
26
- print("Loading models...")
27
- _model_cache: dict[str, KittenTTS] = {}
28
- for model_name, model_id in MODELS.items():
29
- print(f"Loading {model_name}...")
30
- _model_cache[model_name] = KittenTTS(model_id)
31
- print("All models loaded!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
 
 
 
 
 
 
 
33
 
34
- def get_model(model_name: str) -> KittenTTS:
35
  return _model_cache[model_name]
36
 
37
 
@@ -60,144 +92,530 @@ theme = gr.themes.Base(
60
  neutral_hue="neutral",
61
  font=gr.themes.GoogleFont("Inter"),
62
  ).set(
63
- body_background_fill="white",
64
- body_background_fill_dark="white",
65
- block_background_fill="white",
66
- block_background_fill_dark="white",
67
- block_border_color="#e5e5e5",
68
- block_border_color_dark="#e5e5e5",
69
- block_shadow="none",
70
- block_shadow_dark="none",
71
- button_primary_background_fill="#111111",
72
- button_primary_background_fill_hover="#333333",
 
73
  button_primary_text_color="white",
74
- button_primary_border_color="#111111",
75
  input_background_fill="white",
76
  input_background_fill_dark="white",
77
- input_border_color="#e5e5e5",
78
- slider_color="#111111",
79
- table_border_color="#e5e5e5",
 
80
  table_even_background_fill="white",
81
- table_odd_background_fill="white",
82
  table_row_focus="white",
83
  )
84
 
85
  css = """
86
- /* Force light mode — prevents OS dark mode from affecting the page */
87
  :root, html, body { color-scheme: light !important; }
88
- body, .gradio-container, .main { background: white !important; }
89
- .gradio-container { max-width: 860px !important; margin: 40px auto !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  footer { display: none !important; }
91
 
92
- /* Force all text to black — no accent colors */
93
- *, *::before, *::after {
94
- color: #111 !important;
95
- --body-text-color: #111 !important;
96
- --block-label-text-color: #111 !important;
97
- --block-title-text-color: #111 !important;
98
- --color-accent: #111 !important;
99
- --link-text-color: #111 !important;
100
- --link-text-color-hover: #111 !important;
101
- --link-text-color-visited: #111 !important;
102
- --link-text-color-active: #111 !important;
 
 
 
 
 
 
 
 
 
 
103
  }
104
-
105
- /* Exceptions — keep button text white */
106
- button.primary, button[variant="primary"] { color: white !important; }
107
-
108
- /* Error toast notification */
109
- .toast-wrap, .toast-body, [class*="toast"] {
110
- background: white !important;
111
- border: 1px solid #e5e5e5 !important;
112
- box-shadow: 0 4px 12px rgba(0,0,0,0.08) !important;
113
  }
114
- [class*="toast"] .toast-title, [class*="toast"] .error,
115
- .toast-wrap .error, span.error {
116
- color: #b91c1c !important;
117
- font-weight: 600 !important;
 
 
118
  }
119
- [class*="toast"] p, [class*="toast"] .toast-text {
120
- color: #555 !important;
 
 
 
 
 
 
121
  }
122
- /* Error badge inside output block */
123
- .error-wrap, .error {
124
- background: #fef2f2 !important;
125
- border-color: #fca5a5 !important;
126
- color: #b91c1c !important;
 
 
 
 
 
 
 
127
  }
128
-
129
- /* Placeholder text */
130
- ::placeholder { color: #aaa !important; }
131
-
132
- /* Backgrounds */
133
- .block, .form, .wrap, .panel, .gap, .tabs { background: white !important; }
134
-
135
- /* Block label tabs (e.g. "Output" on the audio component) */
136
- [data-testid="block-label"] {
137
- background: white !important;
138
- color: #111 !important;
139
- border-color: #e5e5e5 !important;
140
  }
141
- [data-testid="block-label"] * { color: #111 !important; }
142
-
143
- /* Dropdown closed state — gray on the full inner wrapper with its natural padding */
144
- input[role="listbox"] {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  background: transparent !important;
 
 
 
 
146
  }
147
- .wrap-inner {
148
- background: #f7f7f7 !important;
149
- border-radius: 4px !important;
 
 
 
 
150
  }
151
-
152
- /* Dropdown popup list */
153
- ul.options {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  background: #f7f7f7 !important;
155
- border: 1px solid #e5e5e5 !important;
156
- box-shadow: 0 4px 12px rgba(0,0,0,0.06) !important;
 
 
 
 
 
 
 
157
  }
158
- ul.options li {
 
 
 
 
 
 
 
 
 
 
 
 
159
  background: #f7f7f7 !important;
160
- color: #111 !important;
 
 
 
 
 
 
 
 
 
 
 
 
161
  }
162
- ul.options li:hover, ul.options li.selected {
 
 
 
 
 
 
163
  background: #eeeeee !important;
 
164
  }
165
-
166
- /* Examples table — force all borders to match */
167
- .examples-holder, .table-wrap, table, thead, tbody, tr, td, th {
168
- background: white !important;
169
- border-color: #e5e5e5 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  }
171
- .tr-head { box-shadow: none !important; }
172
- tr:hover td { background: #f9f9f9 !important; }
173
 
174
- /* Speed number input container and divider */
175
- .tab-like-container, .tab-like-container *, input[type=number] {
176
- border-color: #e5e5e5 !important;
 
 
 
177
  }
178
- .reset-button {
179
- -webkit-appearance: none !important;
180
- appearance: none !important;
181
- border: none !important;
182
- background: white !important;
 
 
 
 
183
  }
 
184
 
185
- /* Slider track */
186
- input[type=range]::-webkit-slider-runnable-track { background: #e5e5e5 !important; }
187
- input[type=range]::-webkit-slider-thumb { background: #111 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  """
189
 
190
- with gr.Blocks(title="KittenTTS Demo") as demo:
191
- gr.Markdown("# KittenTTS Demo")
192
- gr.Markdown('<img width="607" height="255" alt="KittenTTS Banner" src="https://github.com/user-attachments/assets/f4646722-ba78-4b25-8a65-81bacee0d4f6" />')
193
- gr.Markdown("Text-to-speech synthesis with multiple models and voices.")
194
 
195
- with gr.Row():
196
- with gr.Column(scale=2):
197
  text_input = gr.Textbox(
198
- label="Text",
199
- placeholder="Enter text to synthesize…",
200
- lines=5,
 
201
  )
202
  with gr.Row():
203
  model_select = gr.Dropdown(
@@ -215,12 +633,24 @@ with gr.Blocks(title="KittenTTS Demo") as demo:
215
  maximum=2.0,
216
  value=1.0,
217
  step=0.05,
218
- label="Speed",
 
 
 
 
 
219
  )
220
- generate_btn = gr.Button("Generate Speech", variant="primary")
221
 
222
- with gr.Column(scale=1):
223
- audio_output = gr.Audio(label="Output", type="numpy")
 
 
 
 
 
 
 
 
224
 
225
  generate_btn.click(
226
  fn=synthesize,
@@ -228,29 +658,38 @@ with gr.Blocks(title="KittenTTS Demo") as demo:
228
  outputs=audio_output,
229
  )
230
 
231
- gr.Examples(
232
- examples=[
233
- [
234
- "Space is a three-dimensional continuum containing positions and directions.",
235
- "Micro (40M - Balanced)",
236
- "Jasper",
237
- 1.0,
238
- ],
239
- [
240
- "She picked up her coffee and walked toward the window.",
241
- "Mini (80M - Best Quality)",
242
- "Luna",
243
- 1.0,
244
- ],
245
- [
246
- "The sun set slowly over the calm, quiet lake",
247
- "Nano (15M - Fastest)",
248
- "Bella",
249
- 1.1,
250
- ],
251
- ],
252
- inputs=[text_input, model_select, voice_select, speed_slider],
253
- )
 
 
 
 
 
 
 
 
 
254
 
255
  if __name__ == "__main__":
256
  demo.launch(server_name="0.0.0.0", theme=theme, css=css)
 
1
  import gradio as gr
2
  import numpy as np
3
+ import base64
4
+ from pathlib import Path
5
 
6
  SAMPLE_RATE = 24000
7
+ ROOT_DIR = Path(__file__).parent
8
+ LOGO_PATH = ROOT_DIR / "assets" / "kittenml_logo.svg"
9
 
10
  MODELS = {
11
  "Nano (15M - Fastest)": "KittenML/kitten-tts-nano-0.8-fp32",
 
24
  "Leo",
25
  ]
26
 
27
+ EXAMPLES = [
28
+ (
29
+ "Space is a three-dimensional continuum containing positions and directions.",
30
+ "Micro (40M - Balanced)",
31
+ "Jasper",
32
+ 1.0,
33
+ ),
34
+ (
35
+ "She picked up her coffee and walked toward the window.",
36
+ "Mini (80M - Best Quality)",
37
+ "Luna",
38
+ 1.0,
39
+ ),
40
+ (
41
+ "The sun set slowly over the calm, quiet lake",
42
+ "Nano (15M - Fastest)",
43
+ "Bella",
44
+ 1.1,
45
+ ),
46
+ ]
47
+
48
+ _model_cache: dict[str, object] = {}
49
+
50
+
51
+ def logo_data_uri() -> str:
52
+ if not LOGO_PATH.exists():
53
+ return "https://kittenml.com/assets/kittenml_logo.svg"
54
+
55
+ encoded_logo = base64.b64encode(LOGO_PATH.read_bytes()).decode("ascii")
56
+ return f"data:image/svg+xml;base64,{encoded_logo}"
57
+
58
 
59
+ def get_model(model_name: str):
60
+ if model_name not in _model_cache:
61
+ from kittentts import KittenTTS
62
+
63
+ print(f"Loading {model_name}...")
64
+ _model_cache[model_name] = KittenTTS(MODELS[model_name])
65
+ print(f"{model_name} loaded.")
66
 
 
67
  return _model_cache[model_name]
68
 
69
 
 
92
  neutral_hue="neutral",
93
  font=gr.themes.GoogleFont("Inter"),
94
  ).set(
95
+ body_background_fill="#f7f7f8",
96
+ body_background_fill_dark="#f7f7f8",
97
+ block_background_fill="#ffffff",
98
+ block_background_fill_dark="#ffffff",
99
+ block_border_color="#e7e7e8",
100
+ block_border_color_dark="#e7e7e8",
101
+ block_radius="8px",
102
+ block_shadow="0 1px 2px rgba(0, 0, 0, 0.04)",
103
+ block_shadow_dark="0 1px 2px rgba(0, 0, 0, 0.04)",
104
+ button_primary_background_fill="#101010",
105
+ button_primary_background_fill_hover="#18181b",
106
  button_primary_text_color="white",
107
+ button_primary_border_color="#101010",
108
  input_background_fill="white",
109
  input_background_fill_dark="white",
110
+ input_border_color="transparent",
111
+ input_border_color_focus="#101010",
112
+ slider_color="#101010",
113
+ table_border_color="transparent",
114
  table_even_background_fill="white",
115
+ table_odd_background_fill="#fafafa",
116
  table_row_focus="white",
117
  )
118
 
119
  css = """
 
120
  :root, html, body { color-scheme: light !important; }
121
+ body {
122
+ background: #f7f7f8 !important;
123
+ }
124
+ .gradio-container,
125
+ .main,
126
+ .app,
127
+ .contain,
128
+ .wrap,
129
+ .gap,
130
+ .form {
131
+ background: transparent !important;
132
+ }
133
+ .gradio-container {
134
+ max-width: 1000px !important;
135
+ margin: 0 auto !important;
136
+ padding: 22px 18px 30px !important;
137
+ --section-inset: 12px;
138
+ }
139
  footer { display: none !important; }
140
 
141
+ .hero {
142
+ position: relative;
143
+ overflow: hidden;
144
+ width: 100%;
145
+ max-width: 100%;
146
+ box-sizing: border-box;
147
+ border: 1px solid rgba(150, 150, 156, 0.42);
148
+ border-radius: 8px;
149
+ background:
150
+ 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%),
151
+ linear-gradient(145deg, #f8f8f9 0%, #dedee3 31%, #fdfdfd 54%, #bfc0c7 100%);
152
+ display: flex;
153
+ flex-direction: column;
154
+ justify-content: space-between;
155
+ min-height: 266px;
156
+ padding: 38px 40px 34px;
157
+ box-shadow:
158
+ inset 0 1px 0 rgba(255, 255, 255, 0.82),
159
+ inset 0 -1px 0 rgba(24, 24, 27, 0.08),
160
+ 0 22px 54px rgba(24, 24, 27, 0.12),
161
+ 0 1px 2px rgba(24, 24, 27, 0.08);
162
  }
163
+ .hero::before {
164
+ background:
165
+ 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),
166
+ linear-gradient(90deg, rgba(17, 17, 17, 0.11), rgba(17, 17, 17, 0) 42%);
167
+ content: "";
168
+ inset: 0;
169
+ opacity: 0.55;
170
+ pointer-events: none;
171
+ position: absolute;
172
  }
173
+ .hero::after {
174
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0) 48%, rgba(24, 24, 27, 0.035));
175
+ content: "";
176
+ inset: 0;
177
+ pointer-events: none;
178
+ position: absolute;
179
  }
180
+ .brand-row {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ gap: 16px;
185
+ margin-bottom: 38px;
186
+ position: relative;
187
+ z-index: 2;
188
  }
189
+ .brand-lockup {
190
+ align-items: center;
191
+ background: #ffffff;
192
+ border-radius: 999px;
193
+ border: 1px solid rgba(255, 255, 255, 0.92);
194
+ box-shadow:
195
+ inset 0 1px 0 rgba(255, 255, 255, 0.88),
196
+ 0 12px 28px rgba(24, 24, 27, 0.10),
197
+ 0 1px 2px rgba(24, 24, 27, 0.08);
198
+ display: inline-flex;
199
+ gap: 11px;
200
+ padding: 9px 15px 9px 10px;
201
  }
202
+ .brand-mark {
203
+ height: 34px;
204
+ width: auto;
205
+ display: block;
 
 
 
 
 
 
 
 
206
  }
207
+ .brand-lockup span {
208
+ color: #111111;
209
+ font-size: 13px;
210
+ font-weight: 750;
211
+ }
212
+ .status-pill {
213
+ border: 0;
214
+ border-radius: 999px;
215
+ color: #111111;
216
+ font-size: 12px;
217
+ font-weight: 700;
218
+ padding: 9px 14px;
219
+ background: #ffffff;
220
+ border: 1px solid rgba(255, 255, 255, 0.92);
221
+ box-shadow:
222
+ inset 0 1px 0 rgba(255, 255, 255, 0.88),
223
+ 0 12px 28px rgba(24, 24, 27, 0.10),
224
+ 0 1px 2px rgba(24, 24, 27, 0.08);
225
+ }
226
+ .hero h1 {
227
+ background: linear-gradient(180deg, #050506 0%, #2f3035 48%, #070708 100%);
228
+ -webkit-background-clip: text;
229
+ background-clip: text;
230
+ color: transparent;
231
+ font-size: clamp(40px, 5.4vw, 66px);
232
+ font-weight: 820;
233
+ letter-spacing: 0;
234
+ line-height: 0.96;
235
+ margin: 0;
236
+ max-width: 780px;
237
+ position: relative;
238
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.42);
239
+ z-index: 2;
240
+ }
241
+ .spec-grid {
242
+ display: flex;
243
+ flex-wrap: wrap;
244
+ gap: 7px;
245
+ margin-top: 22px;
246
+ max-width: 520px;
247
+ position: relative;
248
+ z-index: 2;
249
+ }
250
+ .spec {
251
+ align-items: center;
252
+ background: rgba(255, 255, 255, 0.68);
253
+ border: 1px solid rgba(255, 255, 255, 0.76);
254
+ border-radius: 999px;
255
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.64), 0 8px 18px rgba(24, 24, 27, 0.08);
256
+ display: inline-flex;
257
+ padding: 7px 12px;
258
+ }
259
+ .spec strong {
260
+ color: #111111;
261
+ font-size: 12px;
262
+ font-weight: 700;
263
+ line-height: 1;
264
+ }
265
+ .studio-layout {
266
+ align-items: stretch !important;
267
+ gap: 14px !important;
268
+ margin: 14px var(--section-inset) 0 !important;
269
+ width: calc(100% - (var(--section-inset) * 2)) !important;
270
+ max-width: calc(100% - (var(--section-inset) * 2)) !important;
271
+ box-sizing: border-box;
272
+ }
273
+ .studio-layout.unequal-height {
274
+ align-items: stretch !important;
275
+ }
276
+ .studio-card {
277
+ align-self: stretch !important;
278
+ border: 1px solid #e7e7e8 !important;
279
+ border-radius: 8px !important;
280
+ background: #ffffff !important;
281
+ box-sizing: border-box !important;
282
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04) !important;
283
+ padding: 16px !important;
284
+ }
285
+ .gradio-container {
286
+ --body-text-color: #111111 !important;
287
+ --block-label-text-color: #2c2924 !important;
288
+ --block-title-text-color: #2c2924 !important;
289
+ --input-text-color: #111111 !important;
290
+ }
291
+ .gradio-container .block,
292
+ .gradio-container .panel {
293
  background: transparent !important;
294
+ border-color: transparent !important;
295
+ border-width: 0 !important;
296
+ box-shadow: none !important;
297
+ color: #111111 !important;
298
  }
299
+ .gradio-container .block,
300
+ .gradio-container .block > *,
301
+ .gradio-container .form,
302
+ .gradio-container .wrap,
303
+ .gradio-container .panel,
304
+ .gradio-container .gap {
305
+ box-shadow: none !important;
306
  }
307
+ .gradio-container .block *,
308
+ .gradio-container .form * {
309
+ color: #111111 !important;
310
+ }
311
+ .gradio-container label,
312
+ .gradio-container label span,
313
+ .gradio-container .block-label,
314
+ .gradio-container .block-title,
315
+ .gradio-container [data-testid="block-label"],
316
+ .gradio-container [data-testid="block-label"] * {
317
+ color: #2c2924 !important;
318
+ font-weight: 650 !important;
319
+ }
320
+ .gradio-container [data-testid="block-label"] {
321
+ background: transparent !important;
322
+ border-color: transparent !important;
323
+ }
324
+ .gradio-container textarea,
325
+ .gradio-container .tab-like-container {
326
  background: #f7f7f7 !important;
327
+ border-color: transparent !important;
328
+ border-width: 0 !important;
329
+ border-radius: 8px !important;
330
+ box-shadow: none !important;
331
+ }
332
+ .gradio-container .wrap-inner {
333
+ background: transparent !important;
334
+ border-radius: 8px !important;
335
+ box-shadow: none !important;
336
  }
337
+ .gradio-container .secondary-wrap,
338
+ .gradio-container .wrap-inner > *,
339
+ .gradio-container input,
340
+ .gradio-container input[role="listbox"] {
341
+ background: transparent !important;
342
+ border-color: transparent !important;
343
+ border-width: 0 !important;
344
+ box-shadow: none !important;
345
+ }
346
+ .gradio-container textarea {
347
+ min-height: 128px !important;
348
+ }
349
+ .gradio-container input[role="listbox"] {
350
  background: #f7f7f7 !important;
351
+ border-radius: 8px !important;
352
+ min-height: 42px !important;
353
+ padding: 0 14px !important;
354
+ }
355
+ .gradio-container textarea, .gradio-container input {
356
+ color: #111111 !important;
357
+ }
358
+ ::placeholder { color: #7f7f7f !important; }
359
+ .generate-button button {
360
+ min-height: 46px !important;
361
+ border-radius: 8px !important;
362
+ font-weight: 750 !important;
363
+ box-shadow: none !important;
364
  }
365
+ .generate-button button,
366
+ .generate-button button *,
367
+ button.primary,
368
+ button.primary * {
369
+ color: #ffffff !important;
370
+ }
371
+ .gradio-container button:hover {
372
  background: #eeeeee !important;
373
+ color: #111111 !important;
374
  }
375
+ .gradio-container button:hover * {
376
+ color: #111111 !important;
377
+ }
378
+ .generate-button button:hover,
379
+ .generate-button button:hover *,
380
+ button.primary:hover,
381
+ button.primary:hover * {
382
+ background: #18181b !important;
383
+ color: #ffffff !important;
384
+ }
385
+ .gradio-container ul.options,
386
+ .gradio-container .options {
387
+ background: #ffffff !important;
388
+ border: 1px solid #e5e5e5 !important;
389
+ border-radius: 8px !important;
390
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.12) !important;
391
+ overflow: hidden !important;
392
+ }
393
+ .gradio-container .wrap-inner.show_options,
394
+ .gradio-container .wrap-inner.show_options * {
395
+ background: #ffffff !important;
396
+ color: #111111 !important;
397
+ }
398
+ .gradio-container ul.options li,
399
+ .gradio-container .options li,
400
+ .gradio-container [role="option"] {
401
+ background: #ffffff !important;
402
+ color: #111111 !important;
403
+ }
404
+ .gradio-container ul.options li:hover,
405
+ .gradio-container ul.options li.selected,
406
+ .gradio-container .options li:hover,
407
+ .gradio-container .options li.selected,
408
+ .gradio-container [role="option"]:hover,
409
+ .gradio-container [role="option"][aria-selected="true"] {
410
+ background: #f0f0f0 !important;
411
+ color: #111111 !important;
412
+ }
413
+ .output-panel {
414
+ justify-content: space-between !important;
415
+ min-height: 100%;
416
+ }
417
+ .output-panel audio,
418
+ .output-panel [data-testid="audio"] {
419
+ min-height: 184px !important;
420
+ }
421
+ .output-panel .progress-text.meta-text {
422
+ align-items: center !important;
423
+ background: #f4f4f5 !important;
424
+ border: 1px solid #e4e4e7 !important;
425
+ border-radius: 999px !important;
426
+ color: transparent !important;
427
+ display: inline-flex !important;
428
+ font-family: Inter, Arial, sans-serif !important;
429
+ font-size: 0 !important;
430
+ font-weight: 650 !important;
431
+ justify-content: center !important;
432
+ line-height: 1 !important;
433
+ margin: 10px auto 0 !important;
434
+ min-width: 0 !important;
435
+ padding: 7px 10px !important;
436
+ position: static !important;
437
+ text-align: center !important;
438
+ width: fit-content !important;
439
+ }
440
+ .output-panel .progress-text.meta-text::before {
441
+ color: #3f3f46;
442
+ content: "Generating audio";
443
+ font-size: 12px;
444
+ letter-spacing: 0;
445
+ }
446
+ .output-note {
447
+ border: 0;
448
+ border-radius: 8px;
449
+ margin-top: 16px;
450
+ padding: 14px 15px;
451
+ background: #f7f7f7;
452
+ color: #3f3f3f;
453
+ font-size: 13px;
454
+ line-height: 1.5;
455
+ }
456
+ .output-note strong {
457
+ color: #151515;
458
+ display: block;
459
+ margin-bottom: 4px;
460
+ }
461
+ .examples-panel {
462
+ margin: 14px var(--section-inset) 0 !important;
463
+ padding: 14px !important;
464
+ width: calc(100% - (var(--section-inset) * 2)) !important;
465
+ max-width: calc(100% - (var(--section-inset) * 2)) !important;
466
+ box-sizing: border-box;
467
+ background: #ffffff !important;
468
+ border: 1px solid #e5e5e5 !important;
469
+ border-radius: 8px !important;
470
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04) !important;
471
+ overflow: hidden !important;
472
+ }
473
+ .examples-header {
474
+ display: flex;
475
+ align-items: center;
476
+ justify-content: space-between;
477
+ gap: 14px;
478
+ margin-bottom: 10px;
479
+ }
480
+ .examples-header strong {
481
+ color: #111111;
482
+ font-size: 15px;
483
+ }
484
+ .examples-header span {
485
+ color: #666666;
486
+ font-size: 12px;
487
+ }
488
+ .example-row {
489
+ align-items: center !important;
490
+ background: #f8f8f8 !important;
491
+ border: 1px solid #eeeeee !important;
492
+ border-radius: 8px !important;
493
+ margin-top: 8px !important;
494
+ padding: 10px 10px 10px 12px !important;
495
+ transition: background 120ms ease, border-color 120ms ease;
496
+ }
497
+ .example-row > .block {
498
+ min-width: 0 !important;
499
+ }
500
+ .example-row:hover {
501
+ background: #f4f4f5 !important;
502
+ border-color: #e2e2e3 !important;
503
+ }
504
+ .example-copy {
505
+ display: grid;
506
+ grid-template-columns: minmax(0, 1fr) 190px 80px 52px;
507
+ gap: 14px;
508
+ align-items: center;
509
+ width: 100%;
510
+ }
511
+ .example-copy strong,
512
+ .example-copy span {
513
+ color: #111111;
514
+ display: block;
515
+ overflow: hidden;
516
+ text-overflow: ellipsis;
517
+ white-space: nowrap;
518
+ }
519
+ .example-copy strong {
520
+ font-size: 13px;
521
+ font-weight: 650;
522
+ }
523
+ .example-copy span {
524
+ color: #5c5c5c;
525
+ font-size: 12px;
526
+ }
527
+ .load-example {
528
+ flex: 0 0 72px !important;
529
+ max-width: 72px !important;
530
+ min-width: 72px !important;
531
+ width: 72px !important;
532
+ }
533
+ .load-example {
534
+ align-items: center !important;
535
+ background: #111111 !important;
536
+ border: 1px solid #111111 !important;
537
+ border-radius: 8px !important;
538
+ color: #ffffff !important;
539
+ display: flex !important;
540
+ font-size: 13px !important;
541
+ font-weight: 750 !important;
542
+ height: 40px !important;
543
+ justify-content: center !important;
544
+ line-height: 1 !important;
545
+ min-width: 72px !important;
546
+ padding: 0 !important;
547
+ width: 72px !important;
548
+ }
549
+ .gradio-container button.load-example,
550
+ button.load-example {
551
+ background: #111111 !important;
552
+ border-color: #111111 !important;
553
+ color: #ffffff !important;
554
+ }
555
+ .gradio-container button.load-example *,
556
+ button.load-example * {
557
+ background: transparent !important;
558
+ color: #ffffff !important;
559
+ }
560
+ .gradio-container button.load-example:hover,
561
+ button.load-example:hover {
562
+ background: #18181b !important;
563
+ border-color: #18181b !important;
564
+ color: #ffffff !important;
565
+ }
566
+ .gradio-container button.load-example:hover *,
567
+ button.load-example:hover * {
568
+ background: transparent !important;
569
+ color: #ffffff !important;
570
  }
 
 
571
 
572
+ textarea,
573
+ input,
574
+ select,
575
+ table {
576
+ border-width: 0 !important;
577
+ box-shadow: none !important;
578
  }
579
+
580
+ @media (max-width: 760px) {
581
+ .gradio-container { padding: 16px 12px 24px !important; }
582
+ .hero { min-height: 230px; padding: 22px; }
583
+ .brand-row { align-items: flex-start; flex-direction: column; margin-bottom: 24px; }
584
+ .hero h1 { font-size: clamp(38px, 13vw, 54px); }
585
+ .spec-grid { gap: 7px; }
586
+ .spec { max-width: 100%; }
587
+ .example-copy { grid-template-columns: 1fr; gap: 4px; }
588
  }
589
+ """
590
 
591
+ hero_html = f"""
592
+ <section class="hero">
593
+ <div class="brand-row">
594
+ <div class="brand-lockup">
595
+ <img class="brand-mark" src="{logo_data_uri()}" alt="KittenML">
596
+ <span>KittenML</span>
597
+ </div>
598
+ <div class="status-pill">On-device friendly TTS</div>
599
+ </div>
600
+ <h1>KittenTTS Demo</h1>
601
+ <div class="spec-grid">
602
+ <div class="spec"><strong>24 kHz audio</strong></div>
603
+ <div class="spec"><strong>8 voices</strong></div>
604
+ <div class="spec"><strong>15M-80M models</strong></div>
605
+ </div>
606
+ </section>
607
  """
608
 
609
+ with gr.Blocks(title="KittenTTS Voice Studio") as demo:
610
+ gr.HTML(hero_html)
 
 
611
 
612
+ with gr.Row(elem_classes=["studio-layout"]):
613
+ with gr.Column(scale=2, elem_classes=["studio-card"]):
614
  text_input = gr.Textbox(
615
+ label="Script",
616
+ placeholder="Write a short passage for KittenTTS to read aloud.",
617
+ lines=4,
618
+ max_lines=6,
619
  )
620
  with gr.Row():
621
  model_select = gr.Dropdown(
 
633
  maximum=2.0,
634
  value=1.0,
635
  step=0.05,
636
+ label="Pace",
637
+ )
638
+ generate_btn = gr.Button(
639
+ "Generate speech",
640
+ variant="primary",
641
+ elem_classes=["generate-button"],
642
  )
 
643
 
644
+ with gr.Column(scale=1, elem_classes=["studio-card", "output-panel"]):
645
+ audio_output = gr.Audio(label="Generated audio", type="numpy")
646
+ gr.HTML(
647
+ """
648
+ <div class="output-note">
649
+ <strong>Model loading</strong>
650
+ The selected model loads on first use, then stays cached for faster generations.
651
+ </div>
652
+ """
653
+ )
654
 
655
  generate_btn.click(
656
  fn=synthesize,
 
658
  outputs=audio_output,
659
  )
660
 
661
+ with gr.Column(elem_classes=["examples-panel"]):
662
+ gr.HTML(
663
+ """
664
+ <div class="examples-header">
665
+ <strong>Examples</strong>
666
+ <span>Load a sample into the studio</span>
667
+ </div>
668
+ """
669
+ )
670
+ for text, model, voice, pace in EXAMPLES:
671
+ with gr.Row(elem_classes=["example-row"]):
672
+ gr.HTML(
673
+ f"""
674
+ <div class="example-copy">
675
+ <strong>{text}</strong>
676
+ <span>{model}</span>
677
+ <span>{voice}</span>
678
+ <span>{pace:g}x</span>
679
+ </div>
680
+ """
681
+ )
682
+ load_btn = gr.Button("Load", elem_classes=["load-example"])
683
+ load_btn.click(
684
+ fn=lambda text=text, model=model, voice=voice, pace=pace: (
685
+ text,
686
+ model,
687
+ voice,
688
+ pace,
689
+ ),
690
+ inputs=[],
691
+ outputs=[text_input, model_select, voice_select, speed_slider],
692
+ )
693
 
694
  if __name__ == "__main__":
695
  demo.launch(server_name="0.0.0.0", theme=theme, css=css)
assets/kittenml_logo.svg ADDED