aitekphsoftware commited on
Commit
ef97fc8
·
verified ·
1 Parent(s): 230256d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -332
app.py CHANGED
@@ -2,77 +2,46 @@ import gradio as gr
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": 14, # faster
19
- "pitch_offset": 6, # brighter
20
- },
21
- "Storytelling / Warm": {
22
- "rate_offset": -4,
23
- "pitch_offset": 3,
24
- },
25
- "Emotional / Heartfelt": {
26
- "rate_offset": -10,
27
- "pitch_offset": 6,
28
- },
29
- "Angry / Rant": {
30
- "rate_offset": 16,
31
- "pitch_offset": 4,
32
- },
33
- "Sad / Dramatic": {
34
- "rate_offset": -18,
35
- "pitch_offset": -4,
36
- },
37
- }
38
 
39
  # -----------------------------
40
- # Custom CSS (works with 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,27 +53,27 @@ body {
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,96 +82,90 @@ body {
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,94 +174,58 @@ label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {
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 (Markdown used as banner) */
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: map [pause], [laugh] etc to more natural text
265
- # -----------------------------
266
- def normalize_script_for_tts(text: str) -> str:
267
- """
268
- Convert expressive cues [pause], [laugh], [energetic intro] etc.
269
- into punctuation and "ha ha ha" so Edge TTS doesn't read brackets
270
- but still sounds more like a comedian.
271
- """
272
-
273
- def _repl(match: re.Match) -> str:
274
- cue = match.group(1).strip().lower()
275
-
276
- if "pause" in cue or "beat" in cue or "silence" in cue:
277
- return "... "
278
- if "laugh" in cue or "chuckle" in cue:
279
- # let TTS actually say "ha ha ha" to mimic a laugh
280
- return " ha ha ha, "
281
- if "intro" in cue or "outro" in cue:
282
- return " "
283
- if "soft" in cue or "whisper" in cue:
284
- return " "
285
- if "energetic" in cue or "teasing" in cue or "conversational" in cue:
286
- return " "
287
- # default: just drop bracket cue
288
- return " "
289
-
290
- # Replace bracketed cues
291
- out = re.sub(r"\[(.*?)\]", _repl, text)
292
- # Compress any crazy spacing
293
- out = re.sub(r"\s+", " ", out)
294
- return out.strip()
295
-
296
-
297
- # -----------------------------
298
- # Core TTS helpers
299
  # -----------------------------
300
  async def get_voices():
301
  voices = await edge_tts.list_voices()
 
302
  voice_labels = [
303
  f"{v['ShortName']} - {v['Locale']} ({v['Gender']})"
304
  for v in voices
@@ -306,275 +233,200 @@ async def get_voices():
306
  voice_labels.sort()
307
  return voice_labels
308
 
309
-
310
- async def text_to_speech(text, voice, base_rate, base_pitch, emotion, expressiveness):
311
  if not text.strip():
312
- return None, "Please enter some text to synthesize."
313
-
314
  if not voice:
315
  return None, "Please select a voice."
316
 
317
- # Clean expressive brackets into comedy-friendly text
318
- clean_text = normalize_script_for_tts(text)
319
-
320
  voice_short_name = voice.split(" - ")[0].strip()
321
 
322
- # -----------------------------
323
- # Emotion rate/pitch shaping
324
- # -----------------------------
325
- preset = EMOTION_PRESETS.get(emotion or "Neutral", EMOTION_PRESETS["Neutral"])
326
- # 0 = no emotion, 100 = base, 200 = 2x preset strength
327
- factor = max(0.0, float(expressiveness) / 100.0)
328
-
329
- rate_offset = int(preset["rate_offset"] * factor)
330
- pitch_offset = int(preset["pitch_offset"] * factor)
331
-
332
- eff_rate = int(base_rate + rate_offset)
333
- eff_pitch = int(base_pitch + pitch_offset)
334
-
335
- # Clamp into safe ranges
336
- eff_rate = max(-50, min(50, eff_rate))
337
- eff_pitch = max(-20, min(20, eff_pitch))
338
-
339
- rate_str = f"{eff_rate:+d}%"
340
- pitch_str = f"{eff_pitch:+d}Hz"
341
-
342
- communicate = edge_tts.Communicate(
343
- text=clean_text,
344
- voice=voice_short_name,
345
- rate=rate_str,
346
- pitch=pitch_str,
 
 
 
 
 
347
  )
348
 
 
 
 
349
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
350
  tmp_path = tmp_file.name
351
  await communicate.save(tmp_path)
352
 
353
  return tmp_path, None
354
 
355
-
356
- async def tts_interface(text, voice, base_rate, base_pitch, emotion, expressiveness):
357
- audio, warning = await text_to_speech(
358
- text=text,
359
- voice=voice,
360
- base_rate=base_rate,
361
- base_pitch=base_pitch,
362
- emotion=emotion,
363
- expressiveness=expressiveness,
364
- )
365
- if warning:
366
- return audio, gr.update(value=f"⚠️ {warning}", visible=True)
367
- return audio, gr.update(value="", visible=False)
368
-
 
 
369
 
370
  # -----------------------------
371
- # Eburon Speech Studio v1.8 – ElevenLabs-ish expressive UI
372
  # -----------------------------
373
  async def create_demo():
374
  voices = await get_voices()
 
 
 
 
 
 
 
375
 
376
- # Sample Taglish Alex Calleja–style comedy script
377
- sample_script = (
378
- "[energetic intro]\n"
379
- "Magandang gabi sa inyong lahat! Ako nga pala si Alex… hindi Calleja, "
380
- "pero pwede na rin sa murang kopya. [pause] Parang Shopee version ng Netflix special.\n\n"
381
- "[conversational]\n"
382
- "Alam n’yo, mahirap na maging adult ngayon. Nung bata tayo, gusto natin tumanda para "
383
- "“walang mag-uutos”. Ngayon, tumanda tayo… at ang pinaka-maingay mag-utos: BILLS. [pause]\n"
384
- "Kuryente, tubig, WiFi, GCash utang, BNPL… parang ex na hindi makamove on. "
385
- "Laging bumabalik buwan-buwan.\n\n"
386
- "[teasing tone]\n"
387
- "Tapos ‘yung kuryente, grabe. Kahit wala ka sa bahay, mataas pa rin bill. "
388
- "Parang Meralco, nag-a-assume: “Alam naming may iyak ka sa dilim, may load ‘yan sa emosyon.” [laugh]\n\n"
389
- "[family observation]\n"
390
- "Sa pamilya, laging may tita na human notification. Wala kang alam? Siya meron. "
391
- "Pagdating mo sa handaan: “O, tumaba ka ah… wala ka pa ring jowa? Ano na plano mo sa buhay?” "
392
- "Parang performance appraisal na walang increase. [pause]\n\n"
393
- "[jeepney bit]\n"
394
- "Sa jeep, may tatlong klaseng pasahero: una, ‘yung DJ ng boundary — siya taga-abot ng bayad, "
395
- "taga-sabi ng “Bayad daw po sa likod!”, parang COO ng jeep; pangalawa, ‘yung ninja na ayaw mag-abot ng bayad, "
396
- "kahit tumama na sa siko niya yung pera; pangatlo, ‘yung tulog na may superpower — biglang gigising: "
397
- "“Sa kanto lang po!” sakto pa rin ang timing.\n\n"
398
- "[closer, warm]\n"
399
- "Pero kahit ganoon, solid pa rin tayo mga Pinoy. Kahit pagod, late, traffic, may punchline pa rin sa dulo. "
400
- "Yun ang talent natin: kahit hirap sa buhay, may follow-up joke pa rin.\n"
401
- "Maraming salamat, good night! [pause]"
402
- )
403
-
404
- with gr.Blocks(title="Eburon Speech Studio v1.8") as demo:
405
- # Inject CSS
406
- gr.HTML(f"<style>{EBURON_CSS}</style>", elem_id="eburon-style-inject")
407
-
408
  with gr.Column(elem_id="eburon-root"):
409
- # Top nav
410
- gr.HTML(
411
- f"""
412
  <div id="eburon-top-nav">
413
  <div id="eburon-nav-left">
414
  <div id="eburon-logo-circle">E</div>
415
  <div id="eburon-product-title">
416
- <span>EBURON SPEECH STUDIO</span>
417
- <span>Neural voice generation · v{EBURON_VERSION}</span>
418
  </div>
419
  <div id="eburon-nav-tabs">
420
- <div class="eburon-tab eburon-tab-active">Speech</div>
421
  <div class="eburon-tab">Voice Lab</div>
422
- <div class="eburon-tab">Projects</div>
423
  </div>
424
  </div>
425
  <div id="eburon-nav-right">
426
- <div id="eburon-pill-version">
427
- Studio {EBURON_VERSION}
428
- </div>
429
- <div id="eburon-pill-usage">
430
- edge_tts 7.2.0 · gradio 4.36.1
431
- </div>
432
  </div>
433
  </div>
434
- """
435
- )
436
 
437
- # Main body
438
  with gr.Row():
439
- # Left: Script
440
  with gr.Column(scale=2, min_width=460):
441
  with gr.Group(elem_classes="eburon-main-card"):
442
- gr.HTML(
443
- """
444
  <div class="eburon-section-header">
445
  <div>
446
  <div class="eburon-section-title">Script</div>
447
- <div class="eburon-section-subtitle">
448
- Taglish Alex Calleja–style skit with cues like [pause], [laugh], [energetic intro].
449
- </div>
450
- </div>
451
- <div class="eburon-mini-pill">
452
- Cues auto-converted to pauses & “ha ha ha”
453
  </div>
 
454
  </div>
455
- """
456
- )
457
  text_input = gr.Textbox(
458
- label="",
459
- value=sample_script,
460
- lines=16,
 
461
  )
462
 
463
- # Right: Voice & emotion
464
  with gr.Column(scale=1, min_width=340):
465
  with gr.Group(elem_classes="eburon-main-card"):
466
- gr.HTML(
467
- """
468
  <div class="eburon-section-header">
469
  <div>
470
- <div class="eburon-section-title">Voice & Delivery</div>
471
- <div class="eburon-section-subtitle">
472
- Emotion preset + intensity + fine speed & pitch (ElevenLabs-style).
473
- </div>
474
- </div>
475
- <div class="eburon-mini-pill">
476
- Emotion-shaped rate & pitch
477
  </div>
478
  </div>
479
- """
480
- )
481
-
482
  voice_dropdown = gr.Dropdown(
483
- choices=[""] + voices,
484
- label="Voice",
485
- value="",
486
- info="Tip: pick a lively EN voice (e.g. male) for stand-up style.",
487
  )
488
-
489
- base_rate_slider = gr.Slider(
490
- minimum=-50,
491
- maximum=50,
492
- value=5, # slightly faster by default
493
- label="Base Speed",
494
- step=1,
495
- info="Baseline speaking speed. Emotion will adjust on top.",
496
  )
497
-
498
- base_pitch_slider = gr.Slider(
499
- minimum=-20,
500
- maximum=20,
501
- value=2, # slightly brighter by default
502
- label="Base Pitch",
503
- step=1,
504
- info="Baseline pitch. Emotion will adjust on top.",
505
  )
506
 
507
- emotion_dropdown = gr.Dropdown(
508
- label="Emotion preset",
509
- choices=list(EMOTION_PRESETS.keys()),
510
- value="Comedy / Playful (Taglish)",
511
- info="High-level emotional profile (approximate, using rate+pitch).",
512
  )
513
-
514
- expressiveness_slider = gr.Slider(
515
- minimum=0,
516
- maximum=200,
517
- value=130, # a bit stronger than normal
518
- step=5,
519
- label="Expressiveness (intensity)",
520
- info="0 = off, 100 = normal, 200 = stronger emotion.",
521
  )
522
 
523
- # Bottom: Generate + audio
524
  with gr.Row():
525
  with gr.Column(scale=1, min_width=260):
526
- generate_btn = gr.Button(
527
- "Generate",
528
- variant="primary",
529
- elem_id="eburon-generate-btn",
530
- )
531
- warning_md = gr.Markdown("", visible=False)
532
 
533
  with gr.Column(scale=2, min_width=460):
534
  with gr.Group(elem_id="eburon-audio-card"):
535
- gr.HTML(
536
- """
537
  <div id="eburon-audio-header">
538
  <div>
539
- <div id="eburon-audio-title">Latest generation</div>
540
- <div id="eburon-audio-subtitle">
541
- Auto-plays after each render. Browser must allow audio playback.
542
- </div>
543
- </div>
544
- <div class="eburon-mini-pill">
545
- MP3 · edge_tts neural
546
  </div>
 
547
  </div>
548
- """
549
- )
550
- audio_output = gr.Audio(
551
- label="",
552
- type="filepath",
553
- autoplay=True,
554
- interactive=False,
555
- )
556
 
557
  generate_btn.click(
558
  fn=tts_interface,
559
- inputs=[
560
- text_input,
561
- voice_dropdown,
562
- base_rate_slider,
563
- base_pitch_slider,
564
- emotion_dropdown,
565
- expressiveness_slider,
566
- ],
567
- outputs=[audio_output, warning_md],
568
  )
569
 
570
  return demo
571
 
572
-
573
  async def main():
574
  demo = await create_demo()
575
- demo.queue(default_concurrency_limit=50)
576
  demo.launch()
577
 
578
-
579
  if __name__ == "__main__":
580
- asyncio.run(main())
 
2
  import edge_tts
3
  import asyncio
4
  import tempfile
5
+ import xml.sax.saxutils
6
+
7
+ EBURON_VERSION = "2.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  # -----------------------------
10
+ # Custom CSS ElevenLabs-style
11
  # -----------------------------
12
+ EBURON_CSS = f"""
13
+ body {{
14
  background: radial-gradient(circle at top left, #020617 0, #020617 45%, #020617 100%);
15
  color: #e5e7eb;
16
  margin: 0;
17
  padding: 0;
18
+ }}
19
 
20
+ * {{
21
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
22
  -webkit-font-smoothing: antialiased;
23
+ }}
24
 
25
+ #eburon-root {{
26
  max-width: 1100px;
27
  margin: 0 auto;
28
  padding: 20px 18px 32px 18px;
29
+ }}
30
 
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
  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
  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
+ .eburon-main-card {{
 
123
  border-radius: 20px;
124
  background: radial-gradient(circle at top left, #020617, #020617 60%);
125
  border: 1px solid rgba(51, 65, 85, 0.9);
126
  box-shadow: 0 24px 48px rgba(15, 23, 42, 0.95);
127
  padding: 16px 18px 18px 18px;
128
+ }}
129
 
130
+ .eburon-section-header {{
 
131
  display: flex;
132
  justify-content: space-between;
133
  align-items: center;
134
  margin-bottom: 8px;
135
+ }}
136
 
137
+ .eburon-section-title {{
138
  font-size: 14px;
139
  font-weight: 600;
140
  color: #e5e7eb;
141
+ }}
142
 
143
+ .eburon-section-subtitle {{
144
  font-size: 11px;
145
  color: #9ca3af;
146
+ }}
147
 
148
+ textarea {{
 
149
  background-color: #020617 !important;
150
  border-radius: 14px !important;
151
  border: 1px solid rgba(55, 65, 81, 0.9) !important;
152
  color: #e5e7eb !important;
153
  font-size: 13px !important;
154
+ }}
155
 
156
+ select, input[type="range"] {{
 
157
  background-color: #020617 !important;
158
  border-radius: 999px !important;
159
  border: 1px solid rgba(55, 65, 81, 0.9) !important;
160
  color: #e5e7eb !important;
161
+ }}
162
 
163
+ label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {{
 
164
  font-size: 11px !important;
165
  color: #9ca3af !important;
166
+ }}
167
 
168
+ #eburon-generate-btn button {{
 
169
  width: 100%;
170
  border-radius: 999px;
171
  font-weight: 600;
 
174
  background: linear-gradient(135deg, #22c55e, #38bdf8);
175
  box-shadow: 0 12px 32px rgba(56, 189, 248, 0.75);
176
  border: none;
177
+ }}
178
 
179
+ #eburon-generate-btn button:hover {{
180
  transform: translateY(-1px);
181
  box-shadow: 0 18px 42px rgba(56, 189, 248, 0.95);
182
+ }}
183
 
184
+ #eburon-audio-card {{
 
185
  border-radius: 18px;
186
  background: radial-gradient(circle at top right, #020617, #020617 65%);
187
  border: 1px solid rgba(55, 65, 81, 0.9);
188
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.95);
189
  padding: 12px 14px 14px 14px;
190
+ }}
191
 
192
+ #eburon-audio-header {{
193
  display: flex;
194
  justify-content: space-between;
195
  align-items: center;
196
  margin-bottom: 4px;
197
+ }}
198
 
199
+ #eburon-audio-title {{
200
  font-size: 12px;
201
  font-weight: 600;
202
  color: #e5e7eb;
203
+ }}
204
 
205
+ #eburon-audio-subtitle {{
206
  font-size: 11px;
207
  color: #9ca3af;
208
+ }}
209
 
210
+ .svelte-1g805jl {{
 
211
  border-radius: 999px !important;
212
+ }}
213
 
214
+ .eburon-mini-pill {{
 
215
  padding: 2px 7px;
216
  border-radius: 999px;
217
  border: 1px solid rgba(75, 85, 99, 0.9);
218
  font-size: 10px;
219
  color: #9ca3af;
220
+ }}
221
  """
222
 
223
  # -----------------------------
224
+ # Core TTS Logic with Emotions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  # -----------------------------
226
  async def get_voices():
227
  voices = await edge_tts.list_voices()
228
+ # Prioritize voices known for good emotional range (e.g., US, UK)
229
  voice_labels = [
230
  f"{v['ShortName']} - {v['Locale']} ({v['Gender']})"
231
  for v in voices
 
233
  voice_labels.sort()
234
  return voice_labels
235
 
236
+ async def text_to_speech(text, voice, rate, pitch, style, style_degree):
 
237
  if not text.strip():
238
+ return None, "Please enter some text."
 
239
  if not voice:
240
  return None, "Please select a voice."
241
 
 
 
 
242
  voice_short_name = voice.split(" - ")[0].strip()
243
 
244
+ # Format Rate and Pitch
245
+ rate_str = f"{rate:+d}%"
246
+ pitch_str = f"{pitch:+d}Hz"
247
+
248
+ # Escape special characters for XML
249
+ safe_text = xml.sax.saxutils.escape(text)
250
+
251
+ # ---------------------------------------------------------
252
+ # Construct SSML for Emotional Output
253
+ # ---------------------------------------------------------
254
+ # If "General" is selected, we don't use the express-as tag.
255
+ # Otherwise, we wrap the content.
256
+ if style != "General":
257
+ ssml_content = (
258
+ f"<mstts:express-as style='{style.lower()}' styledegree='{style_degree}'>"
259
+ f"<prosody rate='{rate_str}' pitch='{pitch_str}'>{safe_text}</prosody>"
260
+ f"</mstts:express-as>"
261
+ )
262
+ else:
263
+ ssml_content = (
264
+ f"<prosody rate='{rate_str}' pitch='{pitch_str}'>{safe_text}</prosody>"
265
+ )
266
+
267
+ # Full SSML wrapper
268
+ ssml = (
269
+ f"<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='en-US'>"
270
+ f"<voice name='{voice_short_name}'>"
271
+ f"{ssml_content}"
272
+ f"</voice>"
273
+ f"</speak>"
274
  )
275
 
276
+ # Note: When using SSML, we pass the SSML string as 'text' and don't use rate/pitch args in Communicate
277
+ communicate = edge_tts.Communicate(text=ssml, voice=voice_short_name)
278
+
279
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
280
  tmp_path = tmp_file.name
281
  await communicate.save(tmp_path)
282
 
283
  return tmp_path, None
284
 
285
+ async def tts_interface(text, voice, rate, pitch, style, style_degree):
286
+ # Warning logic if user selects an emotion for a voice that likely doesn't support it
287
+ # (Simplified check: Non-Neural voices or specific locales might ignore it)
288
+ warning_msg = None
289
+ if style != "General" and "Neural" not in voice:
290
+ warning_msg = "Note: The selected voice might not support emotions. Neural voices work best."
291
+
292
+ audio, error = await text_to_speech(text, voice, rate, pitch, style, style_degree)
293
+
294
+ if error:
295
+ return None, gr.Warning(error)
296
+
297
+ if warning_msg:
298
+ return audio, gr.Warning(warning_msg)
299
+
300
+ return audio, None
301
 
302
  # -----------------------------
303
+ # Eburon Speech Studio v2.0 UI
304
  # -----------------------------
305
  async def create_demo():
306
  voices = await get_voices()
307
+
308
+ # Common styles supported by Microsoft Azure/Edge Neural voices
309
+ styles = [
310
+ "General", "Cheerful", "Sad", "Angry", "Terrified",
311
+ "Whispering", "Excited", "Friendly", "Unfriendly",
312
+ "Shouting", "Hopeful"
313
+ ]
314
 
315
+ with gr.Blocks(title="Eburon Speech Studio v2.0", css=EBURON_CSS) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  with gr.Column(elem_id="eburon-root"):
317
+ # HEADER
318
+ gr.HTML(f"""
 
319
  <div id="eburon-top-nav">
320
  <div id="eburon-nav-left">
321
  <div id="eburon-logo-circle">E</div>
322
  <div id="eburon-product-title">
323
+ <span>EBURON EMOTION</span>
324
+ <span>Neural Expression Engine · v{EBURON_VERSION}</span>
325
  </div>
326
  <div id="eburon-nav-tabs">
327
+ <div class="eburon-tab eburon-tab-active">Synthesis</div>
328
  <div class="eburon-tab">Voice Lab</div>
 
329
  </div>
330
  </div>
331
  <div id="eburon-nav-right">
332
+ <div id="eburon-pill-version">Pro</div>
333
+ <div id="eburon-pill-usage">SSML Enabled</div>
 
 
 
 
334
  </div>
335
  </div>
336
+ """)
 
337
 
 
338
  with gr.Row():
339
+ # LEFT: Script Input
340
  with gr.Column(scale=2, min_width=460):
341
  with gr.Group(elem_classes="eburon-main-card"):
342
+ gr.HTML("""
 
343
  <div class="eburon-section-header">
344
  <div>
345
  <div class="eburon-section-title">Script</div>
346
+ <div class="eburon-section-subtitle">Type your text. Use standard punctuation for best pause handling.</div>
 
 
 
 
 
347
  </div>
348
+ <div class="eburon-mini-pill">Unlimited</div>
349
  </div>
350
+ """)
 
351
  text_input = gr.Textbox(
352
+ label="",
353
+ placeholder="Enter text here...",
354
+ lines=14,
355
+ value="I can't believe you did that! That is absolutely amazing."
356
  )
357
 
358
+ # RIGHT: Voice & Emotion Controls
359
  with gr.Column(scale=1, min_width=340):
360
  with gr.Group(elem_classes="eburon-main-card"):
361
+ gr.HTML("""
 
362
  <div class="eburon-section-header">
363
  <div>
364
+ <div class="eburon-section-title">Voice & Emotion</div>
365
+ <div class="eburon-section-subtitle">Select neural voice and emotional overlay.</div>
 
 
 
 
 
366
  </div>
367
  </div>
368
+ """)
369
+
 
370
  voice_dropdown = gr.Dropdown(
371
+ choices=[""] + voices,
372
+ label="Voice Model",
373
+ value="en-US-AriaNeural - en-US (Female)" if any("AriaNeural" in v for v in voices) else "",
374
+ info="Select 'Neural' voices for best results."
375
  )
376
+
377
+ style_dropdown = gr.Dropdown(
378
+ choices=styles,
379
+ label="Expressive Style",
380
+ value="General",
381
+ info="Applies emotional tone to the voice."
 
 
382
  )
383
+
384
+ style_degree = gr.Slider(
385
+ minimum=0.1, maximum=2.0, value=1.0, step=0.1,
386
+ label="Emotion Intensity",
387
+ info="< 1 is subtle, > 1 is exaggerated."
 
 
 
388
  )
389
 
390
+ gr.HTML("<div style='margin-top:15px; margin-bottom:5px; border-top:1px solid #334155;'></div>")
391
+
392
+ rate_slider = gr.Slider(
393
+ minimum=-50, maximum=50, value=0, label="Speed", step=1
 
394
  )
395
+ pitch_slider = gr.Slider(
396
+ minimum=-20, maximum=20, value=0, label="Pitch", step=1
 
 
 
 
 
 
397
  )
398
 
399
+ # BOTTOM: Generate & Player
400
  with gr.Row():
401
  with gr.Column(scale=1, min_width=260):
402
+ generate_btn = gr.Button("Generate Audio", variant="primary", elem_id="eburon-generate-btn")
403
+ status_msg = gr.Markdown(visible=False)
 
 
 
 
404
 
405
  with gr.Column(scale=2, min_width=460):
406
  with gr.Group(elem_id="eburon-audio-card"):
407
+ gr.HTML("""
 
408
  <div id="eburon-audio-header">
409
  <div>
410
+ <div id="eburon-audio-title">Output</div>
411
+ <div id="eburon-audio-subtitle">Generated result</div>
 
 
 
 
 
412
  </div>
413
+ <div class="eburon-mini-pill">MP3</div>
414
  </div>
415
+ """)
416
+ audio_output = gr.Audio(label="", type="filepath", autoplay=True, interactive=False)
 
 
 
 
 
 
417
 
418
  generate_btn.click(
419
  fn=tts_interface,
420
+ inputs=[text_input, voice_dropdown, rate_slider, pitch_slider, style_dropdown, style_degree],
421
+ outputs=[audio_output, status_msg]
 
 
 
 
 
 
 
422
  )
423
 
424
  return demo
425
 
 
426
  async def main():
427
  demo = await create_demo()
428
+ demo.queue(default_concurrency_limit=20)
429
  demo.launch()
430
 
 
431
  if __name__ == "__main__":
432
+ asyncio.run(main())