aitekphsoftware commited on
Commit
46c1003
·
verified ·
1 Parent(s): 3908a57

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +228 -98
app.py CHANGED
@@ -2,46 +2,77 @@ import gradio as gr
2
  import edge_tts
3
  import asyncio
4
  import tempfile
 
5
 
6
  EBURON_VERSION = "1.8"
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  # -----------------------------
9
- # Custom CSS – ElevenLabs-style, Eburon-branded
10
  # -----------------------------
11
- EBURON_CSS = f"""
12
- body {{
13
  background: radial-gradient(circle at top left, #020617 0, #020617 45%, #020617 100%);
14
  color: #e5e7eb;
15
  margin: 0;
16
  padding: 0;
17
- }}
18
 
19
- * {{
20
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
21
  -webkit-font-smoothing: antialiased;
22
- }}
23
 
24
- #eburon-root {{
25
  max-width: 1100px;
26
  margin: 0 auto;
27
  padding: 20px 18px 32px 18px;
28
- }}
29
 
30
  /* Top nav bar (fake, for look) */
31
- #eburon-top-nav {{
32
  display: flex;
33
  align-items: center;
34
  justify-content: space-between;
35
  margin-bottom: 18px;
36
- }}
37
 
38
- #eburon-nav-left {{
39
  display: flex;
40
  align-items: center;
41
  gap: 14px;
42
- }}
43
 
44
- #eburon-logo-circle {{
45
  width: 32px;
46
  height: 32px;
47
  border-radius: 999px;
@@ -53,27 +84,27 @@ body {{
53
  font-weight: 800;
54
  font-size: 17px;
55
  box-shadow: 0 0 22px rgba(59, 130, 246, 0.8);
56
- }}
57
 
58
- #eburon-product-title {{
59
  display: flex;
60
  flex-direction: column;
61
- }}
62
 
63
- #eburon-product-title span:nth-child(1) {{
64
  font-size: 18px;
65
  font-weight: 700;
66
  letter-spacing: 0.08em;
67
  text-transform: uppercase;
68
  color: #e5e7eb;
69
- }}
70
 
71
- #eburon-product-title span:nth-child(2) {{
72
  font-size: 11px;
73
  color: #9ca3af;
74
- }}
75
 
76
- #eburon-nav-tabs {{
77
  display: inline-flex;
78
  align-items: center;
79
  gap: 4px;
@@ -82,96 +113,96 @@ body {{
82
  background: rgba(15, 23, 42, 0.9);
83
  border: 1px solid rgba(55, 65, 81, 0.9);
84
  font-size: 11px;
85
- }}
86
 
87
- .eburon-tab {{
88
  padding: 5px 10px;
89
  border-radius: 999px;
90
  cursor: default;
91
  color: #9ca3af;
92
- }}
93
 
94
- .eburon-tab-active {{
95
  background: linear-gradient(135deg, #38bdf8, #6366f1);
96
  color: #020617;
97
  font-weight: 600;
98
- }}
99
 
100
- #eburon-nav-right {{
101
  display: flex;
102
  align-items: center;
103
  gap: 8px;
104
  font-size: 11px;
105
  color: #9ca3af;
106
- }}
107
 
108
- #eburon-pill-version {{
109
  padding: 4px 10px;
110
  border-radius: 999px;
111
  border: 1px solid rgba(148, 163, 184, 0.4);
112
  background: radial-gradient(circle at top, rgba(31, 41, 55, 1), rgba(15, 23, 42, 1));
113
- }}
114
 
115
- #eburon-pill-usage {{
116
  padding: 4px 10px;
117
  border-radius: 999px;
118
  border: 1px solid rgba(59, 130, 246, 0.7);
119
  background: radial-gradient(circle at top, rgba(30, 64, 175, 0.85), rgba(15, 23, 42, 1));
120
- }}
121
 
122
  /* Main cards */
123
- .eburon-main-card {{
124
  border-radius: 20px;
125
  background: radial-gradient(circle at top left, #020617, #020617 60%);
126
  border: 1px solid rgba(51, 65, 85, 0.9);
127
  box-shadow: 0 24px 48px rgba(15, 23, 42, 0.95);
128
  padding: 16px 18px 18px 18px;
129
- }}
130
 
131
  /* Headings inside cards */
132
- .eburon-section-header {{
133
  display: flex;
134
  justify-content: space-between;
135
  align-items: center;
136
  margin-bottom: 8px;
137
- }}
138
 
139
- .eburon-section-title {{
140
  font-size: 14px;
141
  font-weight: 600;
142
  color: #e5e7eb;
143
- }}
144
 
145
- .eburon-section-subtitle {{
146
  font-size: 11px;
147
  color: #9ca3af;
148
- }}
149
 
150
  /* Script textarea */
151
- textarea {{
152
  background-color: #020617 !important;
153
  border-radius: 14px !important;
154
  border: 1px solid rgba(55, 65, 81, 0.9) !important;
155
  color: #e5e7eb !important;
156
  font-size: 13px !important;
157
- }}
158
 
159
  /* Right panel controls */
160
- select, input[type="range"] {{
161
  background-color: #020617 !important;
162
  border-radius: 999px !important;
163
  border: 1px solid rgba(55, 65, 81, 0.9) !important;
164
  color: #e5e7eb !important;
165
- }}
166
 
167
  /* Labels */
168
- label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {{
169
  font-size: 11px !important;
170
  color: #9ca3af !important;
171
- }}
172
 
173
  /* Generate button */
174
- #eburon-generate-btn button {{
175
  width: 100%;
176
  border-radius: 999px;
177
  font-weight: 600;
@@ -180,55 +211,80 @@ label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {{
180
  background: linear-gradient(135deg, #22c55e, #38bdf8);
181
  box-shadow: 0 12px 32px rgba(56, 189, 248, 0.75);
182
  border: none;
183
- }}
184
 
185
- #eburon-generate-btn button:hover {{
186
  transform: translateY(-1px);
187
  box-shadow: 0 18px 42px rgba(56, 189, 248, 0.95);
188
- }}
189
 
190
  /* Audio player container */
191
- #eburon-audio-card {{
192
  border-radius: 18px;
193
  background: radial-gradient(circle at top right, #020617, #020617 65%);
194
  border: 1px solid rgba(55, 65, 81, 0.9);
195
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.95);
196
  padding: 12px 14px 14px 14px;
197
- }}
198
 
199
- #eburon-audio-header {{
200
  display: flex;
201
  justify-content: space-between;
202
  align-items: center;
203
  margin-bottom: 4px;
204
- }}
205
 
206
- #eburon-audio-title {{
207
  font-size: 12px;
208
  font-weight: 600;
209
  color: #e5e7eb;
210
- }}
211
 
212
- #eburon-audio-subtitle {{
213
  font-size: 11px;
214
  color: #9ca3af;
215
- }}
216
 
217
  /* Warning styling (Gradio Alert) */
218
- .svelte-1g805jl {{
219
  border-radius: 999px !important;
220
- }}
221
 
222
  /* Small badges */
223
- .eburon-mini-pill {{
224
  padding: 2px 7px;
225
  border-radius: 999px;
226
  border: 1px solid rgba(75, 85, 99, 0.9);
227
  font-size: 10px;
228
  color: #9ca3af;
229
- }}
230
  """
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  # -----------------------------
233
  # Core TTS helpers
234
  # -----------------------------
@@ -242,20 +298,39 @@ async def get_voices():
242
  return voice_labels
243
 
244
 
245
- async def text_to_speech(text, voice, rate, pitch):
246
  if not text.strip():
247
  return None, "Please enter some text to synthesize."
248
 
249
  if not voice:
250
  return None, "Please select a voice."
251
 
 
 
 
252
  voice_short_name = voice.split(" - ")[0].strip()
253
 
254
- rate_str = f"{rate:+d}%"
255
- pitch_str = f"{pitch:+d}Hz"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  communicate = edge_tts.Communicate(
258
- text=text,
259
  voice=voice_short_name,
260
  rate=rate_str,
261
  pitch=pitch_str,
@@ -268,11 +343,18 @@ async def text_to_speech(text, voice, rate, pitch):
268
  return tmp_path, None
269
 
270
 
271
- async def tts_interface(text, voice, rate, pitch):
272
- audio, warning = await text_to_speech(text, voice, rate, pitch)
 
 
 
 
 
 
 
273
  if warning:
274
- return audio, gr.Warning(warning)
275
- return audio, None
276
 
277
 
278
  # -----------------------------
@@ -281,11 +363,37 @@ async def tts_interface(text, voice, rate, pitch):
281
  async def create_demo():
282
  voices = await get_voices()
283
 
284
- with gr.Blocks(
285
- analytics_enabled=False,
286
- title="Eburon Speech Studio v1.8"
287
- ) as demo:
288
- # Root container for centralized layout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  with gr.Column(elem_id="eburon-root"):
290
  # Top nav
291
  gr.HTML(
@@ -308,14 +416,14 @@ async def create_demo():
308
  Studio {EBURON_VERSION}
309
  </div>
310
  <div id="eburon-pill-usage">
311
- Edge TTS · Local session
312
  </div>
313
  </div>
314
  </div>
315
  """
316
  )
317
 
318
- # Main card with left/right layout
319
  with gr.Row():
320
  # Left: Script
321
  with gr.Column(scale=2, min_width=460):
@@ -326,22 +434,23 @@ async def create_demo():
326
  <div>
327
  <div class="eburon-section-title">Script</div>
328
  <div class="eburon-section-subtitle">
329
- Type or paste your text. Ideal for narrations, dialogues, or public talks.
330
  </div>
331
  </div>
332
  <div class="eburon-mini-pill">
333
- Character-safe · Long-form friendly
334
  </div>
335
  </div>
336
  """
337
  )
338
  text_input = gr.Textbox(
339
  label="",
340
- placeholder="Write your script as if you're inside Eburon Speech Studio...",
341
- lines=12,
 
342
  )
343
 
344
- # Right: Voice settings
345
  with gr.Column(scale=1, min_width=340):
346
  with gr.Group(elem_classes="eburon-main-card"):
347
  gr.HTML(
@@ -350,11 +459,11 @@ async def create_demo():
350
  <div>
351
  <div class="eburon-section-title">Voice & Delivery</div>
352
  <div class="eburon-section-subtitle">
353
- Select a voice and tune its speed and tone.
354
  </div>
355
  </div>
356
  <div class="eburon-mini-pill">
357
- Edge voices
358
  </div>
359
  </div>
360
  """
@@ -364,36 +473,52 @@ async def create_demo():
364
  choices=[""] + voices,
365
  label="Voice",
366
  value="",
367
- info="Pick a neural voice from the Edge TTS catalog."
368
  )
369
 
370
  rate_slider = gr.Slider(
371
  minimum=-50,
372
  maximum=50,
373
  value=0,
374
- label="Speed",
375
  step=1,
376
- info="Negative is slower · Positive is faster"
377
  )
378
 
379
  pitch_slider = gr.Slider(
380
  minimum=-20,
381
  maximum=20,
382
  value=0,
383
- label="Pitch",
384
  step=1,
385
- info="Negative is deeper · Positive is brighter"
386
  )
387
 
388
- # Bottom row: Generate + audio player
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  with gr.Row():
390
  with gr.Column(scale=1, min_width=260):
391
  generate_btn = gr.Button(
392
  "Generate",
393
  variant="primary",
394
- elem_id="eburon-generate-btn"
395
  )
396
- warning_md = gr.Markdown(visible=False)
397
 
398
  with gr.Column(scale=2, min_width=460):
399
  with gr.Group(elem_id="eburon-audio-card"):
@@ -403,11 +528,11 @@ async def create_demo():
403
  <div>
404
  <div id="eburon-audio-title">Latest generation</div>
405
  <div id="eburon-audio-subtitle">
406
- Audio will appear here and auto-play after each successful generation.
407
  </div>
408
  </div>
409
  <div class="eburon-mini-pill">
410
- MP3 · 48 kHz
411
  </div>
412
  </div>
413
  """
@@ -421,7 +546,14 @@ async def create_demo():
421
 
422
  generate_btn.click(
423
  fn=tts_interface,
424
- inputs=[text_input, voice_dropdown, rate_slider, pitch_slider],
 
 
 
 
 
 
 
425
  outputs=[audio_output, warning_md],
426
  )
427
 
@@ -431,9 +563,7 @@ async def create_demo():
431
  async def main():
432
  demo = await create_demo()
433
  demo.queue(default_concurrency_limit=50)
434
- demo.launch(
435
- css=EBURON_CSS
436
- )
437
 
438
 
439
  if __name__ == "__main__":
 
2
  import edge_tts
3
  import asyncio
4
  import tempfile
5
+ import re
6
 
7
  EBURON_VERSION = "1.8"
8
 
9
+ # ---------------------------------
10
+ # Emotion presets (ElevenLabs-ish)
11
+ # ---------------------------------
12
+ EMOTION_PRESETS = {
13
+ "Neutral": {
14
+ "rate_offset": 0,
15
+ "pitch_offset": 0,
16
+ },
17
+ "Comedy / Playful (Taglish)": {
18
+ "rate_offset": 12, # faster
19
+ "pitch_offset": 4, # slightly brighter
20
+ },
21
+ "Storytelling / Warm": {
22
+ "rate_offset": -6, # a bit slower
23
+ "pitch_offset": 2, # slightly warmer
24
+ },
25
+ "Emotional / Heartfelt": {
26
+ "rate_offset": -10, # slower
27
+ "pitch_offset": 5, # a bit more open / emotional
28
+ },
29
+ "Angry / Rant": {
30
+ "rate_offset": 15,
31
+ "pitch_offset": 3,
32
+ },
33
+ "Sad / Dramatic": {
34
+ "rate_offset": -18,
35
+ "pitch_offset": -5,
36
+ },
37
+ }
38
+
39
  # -----------------------------
40
+ # Custom CSS – injected via <style> for Gradio 4.36.1+
41
  # -----------------------------
42
+ EBURON_CSS = """
43
+ body {
44
  background: radial-gradient(circle at top left, #020617 0, #020617 45%, #020617 100%);
45
  color: #e5e7eb;
46
  margin: 0;
47
  padding: 0;
48
+ }
49
 
50
+ * {
51
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
52
  -webkit-font-smoothing: antialiased;
53
+ }
54
 
55
+ #eburon-root {
56
  max-width: 1100px;
57
  margin: 0 auto;
58
  padding: 20px 18px 32px 18px;
59
+ }
60
 
61
  /* Top nav bar (fake, for look) */
62
+ #eburon-top-nav {
63
  display: flex;
64
  align-items: center;
65
  justify-content: space-between;
66
  margin-bottom: 18px;
67
+ }
68
 
69
+ #eburon-nav-left {
70
  display: flex;
71
  align-items: center;
72
  gap: 14px;
73
+ }
74
 
75
+ #eburon-logo-circle {
76
  width: 32px;
77
  height: 32px;
78
  border-radius: 999px;
 
84
  font-weight: 800;
85
  font-size: 17px;
86
  box-shadow: 0 0 22px rgba(59, 130, 246, 0.8);
87
+ }
88
 
89
+ #eburon-product-title {
90
  display: flex;
91
  flex-direction: column;
92
+ }
93
 
94
+ #eburon-product-title span:nth-child(1) {
95
  font-size: 18px;
96
  font-weight: 700;
97
  letter-spacing: 0.08em;
98
  text-transform: uppercase;
99
  color: #e5e7eb;
100
+ }
101
 
102
+ #eburon-product-title span:nth-child(2) {
103
  font-size: 11px;
104
  color: #9ca3af;
105
+ }
106
 
107
+ #eburon-nav-tabs {
108
  display: inline-flex;
109
  align-items: center;
110
  gap: 4px;
 
113
  background: rgba(15, 23, 42, 0.9);
114
  border: 1px solid rgba(55, 65, 81, 0.9);
115
  font-size: 11px;
116
+ }
117
 
118
+ .eburon-tab {
119
  padding: 5px 10px;
120
  border-radius: 999px;
121
  cursor: default;
122
  color: #9ca3af;
123
+ }
124
 
125
+ .eburon-tab-active {
126
  background: linear-gradient(135deg, #38bdf8, #6366f1);
127
  color: #020617;
128
  font-weight: 600;
129
+ }
130
 
131
+ #eburon-nav-right {
132
  display: flex;
133
  align-items: center;
134
  gap: 8px;
135
  font-size: 11px;
136
  color: #9ca3af;
137
+ }
138
 
139
+ #eburon-pill-version {
140
  padding: 4px 10px;
141
  border-radius: 999px;
142
  border: 1px solid rgba(148, 163, 184, 0.4);
143
  background: radial-gradient(circle at top, rgba(31, 41, 55, 1), rgba(15, 23, 42, 1));
144
+ }
145
 
146
+ #eburon-pill-usage {
147
  padding: 4px 10px;
148
  border-radius: 999px;
149
  border: 1px solid rgba(59, 130, 246, 0.7);
150
  background: radial-gradient(circle at top, rgba(30, 64, 175, 0.85), rgba(15, 23, 42, 1));
151
+ }
152
 
153
  /* Main cards */
154
+ .eburon-main-card {
155
  border-radius: 20px;
156
  background: radial-gradient(circle at top left, #020617, #020617 60%);
157
  border: 1px solid rgba(51, 65, 85, 0.9);
158
  box-shadow: 0 24px 48px rgba(15, 23, 42, 0.95);
159
  padding: 16px 18px 18px 18px;
160
+ }
161
 
162
  /* Headings inside cards */
163
+ .eburon-section-header {
164
  display: flex;
165
  justify-content: space-between;
166
  align-items: center;
167
  margin-bottom: 8px;
168
+ }
169
 
170
+ .eburon-section-title {
171
  font-size: 14px;
172
  font-weight: 600;
173
  color: #e5e7eb;
174
+ }
175
 
176
+ .eburon-section-subtitle {
177
  font-size: 11px;
178
  color: #9ca3af;
179
+ }
180
 
181
  /* Script textarea */
182
+ textarea {
183
  background-color: #020617 !important;
184
  border-radius: 14px !important;
185
  border: 1px solid rgba(55, 65, 81, 0.9) !important;
186
  color: #e5e7eb !important;
187
  font-size: 13px !important;
188
+ }
189
 
190
  /* Right panel controls */
191
+ select, input[type="range"] {
192
  background-color: #020617 !important;
193
  border-radius: 999px !important;
194
  border: 1px solid rgba(55, 65, 81, 0.9) !important;
195
  color: #e5e7eb !important;
196
+ }
197
 
198
  /* Labels */
199
+ label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {
200
  font-size: 11px !important;
201
  color: #9ca3af !important;
202
+ }
203
 
204
  /* Generate button */
205
+ #eburon-generate-btn button {
206
  width: 100%;
207
  border-radius: 999px;
208
  font-weight: 600;
 
211
  background: linear-gradient(135deg, #22c55e, #38bdf8);
212
  box-shadow: 0 12px 32px rgba(56, 189, 248, 0.75);
213
  border: none;
214
+ }
215
 
216
+ #eburon-generate-btn button:hover {
217
  transform: translateY(-1px);
218
  box-shadow: 0 18px 42px rgba(56, 189, 248, 0.95);
219
+ }
220
 
221
  /* Audio player container */
222
+ #eburon-audio-card {
223
  border-radius: 18px;
224
  background: radial-gradient(circle at top right, #020617, #020617 65%);
225
  border: 1px solid rgba(55, 65, 81, 0.9);
226
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.95);
227
  padding: 12px 14px 14px 14px;
228
+ }
229
 
230
+ #eburon-audio-header {
231
  display: flex;
232
  justify-content: space-between;
233
  align-items: center;
234
  margin-bottom: 4px;
235
+ }
236
 
237
+ #eburon-audio-title {
238
  font-size: 12px;
239
  font-weight: 600;
240
  color: #e5e7eb;
241
+ }
242
 
243
+ #eburon-audio-subtitle {
244
  font-size: 11px;
245
  color: #9ca3af;
246
+ }
247
 
248
  /* Warning styling (Gradio Alert) */
249
+ .svelte-1g805jl {
250
  border-radius: 999px !important;
251
+ }
252
 
253
  /* Small badges */
254
+ .eburon-mini-pill {
255
  padding: 2px 7px;
256
  border-radius: 999px;
257
  border: 1px solid rgba(75, 85, 99, 0.9);
258
  font-size: 10px;
259
  color: #9ca3af;
260
+ }
261
  """
262
 
263
+ # -----------------------------
264
+ # Helper: normalize expressive cues like [pause], [laugh]
265
+ # -----------------------------
266
+ def normalize_script_for_tts(text: str) -> str:
267
+ """
268
+ Convert expressive cues [pause], [laugh], [energetic intro] etc.
269
+ into punctuation so Edge TTS won't literally read the brackets.
270
+ """
271
+
272
+ def _repl(match: re.Match) -> str:
273
+ cue = match.group(1).strip().lower()
274
+ if "pause" in cue or "beat" in cue:
275
+ return " … "
276
+ if "laugh" in cue or "chuckle" in cue:
277
+ return " "
278
+ if "intro" in cue or "outro" in cue:
279
+ return " "
280
+ if "soft" in cue or "whisper" in cue:
281
+ return " "
282
+ # Default: remove bracket cue
283
+ return " "
284
+
285
+ return re.sub(r"\[(.*?)\]", _repl, text)
286
+
287
+
288
  # -----------------------------
289
  # Core TTS helpers
290
  # -----------------------------
 
298
  return voice_labels
299
 
300
 
301
+ async def text_to_speech(text, voice, rate, pitch, emotion, expressiveness):
302
  if not text.strip():
303
  return None, "Please enter some text to synthesize."
304
 
305
  if not voice:
306
  return None, "Please select a voice."
307
 
308
+ # Clean expressive brackets for TTS
309
+ clean_text = normalize_script_for_tts(text)
310
+
311
  voice_short_name = voice.split(" - ")[0].strip()
312
 
313
+ # -----------------------------
314
+ # Emotion → rate/pitch shaping
315
+ # -----------------------------
316
+ preset = EMOTION_PRESETS.get(emotion or "Neutral", EMOTION_PRESETS["Neutral"])
317
+ factor = max(0.0, float(expressiveness) / 100.0) # 0 to 2.0
318
+
319
+ rate_offset = int(preset["rate_offset"] * factor)
320
+ pitch_offset = int(preset["pitch_offset"] * factor)
321
+
322
+ eff_rate = int(rate + rate_offset)
323
+ eff_pitch = int(pitch + pitch_offset)
324
+
325
+ # Clamp into slider ranges
326
+ eff_rate = max(-50, min(50, eff_rate))
327
+ eff_pitch = max(-20, min(20, eff_pitch))
328
+
329
+ rate_str = f"{eff_rate:+d}%"
330
+ pitch_str = f"{eff_pitch:+d}Hz"
331
 
332
  communicate = edge_tts.Communicate(
333
+ text=clean_text,
334
  voice=voice_short_name,
335
  rate=rate_str,
336
  pitch=pitch_str,
 
343
  return tmp_path, None
344
 
345
 
346
+ async def tts_interface(text, voice, rate, pitch, emotion, expressiveness):
347
+ audio, warning = await text_to_speech(
348
+ text=text,
349
+ voice=voice,
350
+ rate=rate,
351
+ pitch=pitch,
352
+ emotion=emotion,
353
+ expressiveness=expressiveness,
354
+ )
355
  if warning:
356
+ return audio, gr.update(value=f"⚠️ {warning}", visible=True)
357
+ return audio, gr.update(value="", visible=False)
358
 
359
 
360
  # -----------------------------
 
363
  async def create_demo():
364
  voices = await get_voices()
365
 
366
+ # Sample Taglish Alex Calleja–style comedy script as default
367
+ sample_script = (
368
+ "[energetic intro]\n"
369
+ "Magandang gabi sa inyong lahat! Ako nga pala si Alex… hindi Calleja, pero pwede na rin sa murang kopya. "
370
+ "[pause] Parang Shopee version ng Netflix special.\n\n"
371
+ "[conversational]\n"
372
+ "Alam n’yo, mahirap na maging adult ngayon. Nung bata tayo, gusto natin tumanda para “walang mag-uutos”. "
373
+ "Ngayon, tumanda tayo… at ang pinaka-maingay mag-utos: BILLS. [pause]\n"
374
+ "Kuryente, tubig, WiFi, GCash utang, BNPL… parang ex na hindi makamove on. Laging bumabalik buwan-buwan.\n\n"
375
+ "[teasing tone]\n"
376
+ "Tapos ‘yung kuryente, grabe. Kahit wala ka sa bahay, mataas pa rin bill. "
377
+ "Parang Meralco, nag-a-assume: “Alam naming may iyak ka sa dilim, may load ‘yan sa emosyon.” [laugh]\n\n"
378
+ "[family observation]\n"
379
+ "Sa pamilya, laging may tita na human notification. Wala kang alam? Siya meron. "
380
+ "Pagdating mo sa handaan: “O, tumaba ka ah… wala ka pa ring jowa? Ano na plano mo sa buhay?” "
381
+ "Parang performance appraisal na walang increase. [pause]\n\n"
382
+ "[jeepney bit]\n"
383
+ "Sa jeep, may tatlong klaseng pasahero: una, ‘yung DJ ng boundary — siya taga-abot ng bayad, "
384
+ "taga-sabi ng “Bayad daw po sa likod!”, parang COO ng jeep; pangalawa, ‘yung ninja na ayaw mag-abot ng bayad, "
385
+ "kahit tumama na sa siko niya yung pera; pangatlo, ‘yung tulog na may superpower — biglang gigising: "
386
+ "“Sa kanto lang po!” sakto pa rin ang timing.\n\n"
387
+ "[closer, warm]\n"
388
+ "Pero kahit ganoon, solid pa rin tayo mga Pinoy. Kahit pagod, late, traffic, may punchline pa rin sa dulo. "
389
+ "Yun ang talent natin: kahit hirap sa buhay, may follow-up joke pa rin.\n"
390
+ "Maraming salamat, good night! [pause]"
391
+ )
392
+
393
+ with gr.Blocks(title="Eburon Speech Studio v1.8") as demo:
394
+ # Inject CSS (works in Gradio 4.36.1 and 6+)
395
+ gr.HTML(f"<style>{EBURON_CSS}</style>", elem_id="eburon-style-inject")
396
+
397
  with gr.Column(elem_id="eburon-root"):
398
  # Top nav
399
  gr.HTML(
 
416
  Studio {EBURON_VERSION}
417
  </div>
418
  <div id="eburon-pill-usage">
419
+ edge_tts 7.2.0 · gradio 4.36.1
420
  </div>
421
  </div>
422
  </div>
423
  """
424
  )
425
 
426
+ # Main: script + voice/emotion
427
  with gr.Row():
428
  # Left: Script
429
  with gr.Column(scale=2, min_width=460):
 
434
  <div>
435
  <div class="eburon-section-title">Script</div>
436
  <div class="eburon-section-subtitle">
437
+ Taglish Alex Calleja–style skit with expressive cues like [pause], [laugh], [energetic intro].
438
  </div>
439
  </div>
440
  <div class="eburon-mini-pill">
441
+ Bracket cues are auto-cleaned before TTS
442
  </div>
443
  </div>
444
  """
445
  )
446
  text_input = gr.Textbox(
447
  label="",
448
+ value=sample_script,
449
+ placeholder="Write your expressive Taglish skit here...",
450
+ lines=14,
451
  )
452
 
453
+ # Right: Voice & expressive controls
454
  with gr.Column(scale=1, min_width=340):
455
  with gr.Group(elem_classes="eburon-main-card"):
456
  gr.HTML(
 
459
  <div>
460
  <div class="eburon-section-title">Voice & Delivery</div>
461
  <div class="eburon-section-subtitle">
462
+ ElevenLabs v3 style: emotion preset + intensity + fine speed & pitch.
463
  </div>
464
  </div>
465
  <div class="eburon-mini-pill">
466
+ Emotion-shaped rate & pitch
467
  </div>
468
  </div>
469
  """
 
473
  choices=[""] + voices,
474
  label="Voice",
475
  value="",
476
+ info="Pick a neural voice from the Edge TTS catalog.",
477
  )
478
 
479
  rate_slider = gr.Slider(
480
  minimum=-50,
481
  maximum=50,
482
  value=0,
483
+ label="Base Speed",
484
  step=1,
485
+ info="Manual speed baseline. Emotion will adjust on top of this.",
486
  )
487
 
488
  pitch_slider = gr.Slider(
489
  minimum=-20,
490
  maximum=20,
491
  value=0,
492
+ label="Base Pitch",
493
  step=1,
494
+ info="Manual pitch baseline. Emotion will adjust on top of this.",
495
  )
496
 
497
+ emotion_dropdown = gr.Dropdown(
498
+ label="Emotion preset",
499
+ choices=list(EMOTION_PRESETS.keys()),
500
+ value="Comedy / Playful (Taglish)",
501
+ info="High-level emotional profile similar to ElevenLabs v3.",
502
+ )
503
+
504
+ expressiveness_slider = gr.Slider(
505
+ minimum=0,
506
+ maximum=200,
507
+ value=100,
508
+ step=5,
509
+ label="Expressiveness (intensity)",
510
+ info="0 = off, 100 = normal, 200 = max emotional shaping.",
511
+ )
512
+
513
+ # Bottom: Generate + audio
514
  with gr.Row():
515
  with gr.Column(scale=1, min_width=260):
516
  generate_btn = gr.Button(
517
  "Generate",
518
  variant="primary",
519
+ elem_id="eburon-generate-btn",
520
  )
521
+ warning_md = gr.Markdown("", visible=False)
522
 
523
  with gr.Column(scale=2, min_width=460):
524
  with gr.Group(elem_id="eburon-audio-card"):
 
528
  <div>
529
  <div id="eburon-audio-title">Latest generation</div>
530
  <div id="eburon-audio-subtitle">
531
+ Auto-plays after each emotional render. Make sure your browser allows audio.
532
  </div>
533
  </div>
534
  <div class="eburon-mini-pill">
535
+ MP3 · edge_tts neural
536
  </div>
537
  </div>
538
  """
 
546
 
547
  generate_btn.click(
548
  fn=tts_interface,
549
+ inputs=[
550
+ text_input,
551
+ voice_dropdown,
552
+ rate_slider,
553
+ pitch_slider,
554
+ emotion_dropdown,
555
+ expressiveness_slider,
556
+ ],
557
  outputs=[audio_output, warning_md],
558
  )
559
 
 
563
  async def main():
564
  demo = await create_demo()
565
  demo.queue(default_concurrency_limit=50)
566
+ demo.launch() # Compatible with gradio==4.36.1
 
 
567
 
568
 
569
  if __name__ == "__main__":