aitekphsoftware commited on
Commit
9b02cd3
·
verified ·
1 Parent(s): 0701bbf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -317
app.py CHANGED
@@ -2,380 +2,287 @@ import gradio as gr
2
  import edge_tts
3
  import asyncio
4
  import tempfile
5
-
6
- # -----------------------------
7
- # Custom CSS for Eburon Speech UI
8
- # -----------------------------
9
- EBURON_CSS = """
10
- body {
11
- background: radial-gradient(circle at top left, #0f172a 0, #020617 40%, #020617 100%);
12
- color: #e5e7eb;
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
- /* Global font & smoothing */
16
- * {
17
- font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
18
- -webkit-font-smoothing: antialiased;
19
- }
20
 
21
- /* Header */
22
- #eburon-header {
23
  display: flex;
24
- align-items: center;
25
  justify-content: space-between;
26
- padding: 18px 20px;
27
- margin-bottom: 8px;
28
- border-radius: 18px;
29
- background: radial-gradient(circle at top left, #1e293b 0, #020617 55%);
30
- border: 1px solid rgba(148, 163, 184, 0.28);
31
- box-shadow: 0 18px 45px rgba(15, 23, 42, 0.85);
32
- }
33
-
34
- #eburon-logo-badge {
35
- display: inline-flex;
36
- align-items: center;
37
- gap: 10px;
38
- }
39
-
40
- #eburon-logo-circle {
41
- width: 32px;
42
- height: 32px;
43
- border-radius: 999px;
44
- background: conic-gradient(from 180deg, #38bdf8, #6366f1, #22c55e, #38bdf8);
45
- display: flex;
46
  align-items: center;
47
- justify-content: center;
48
- box-shadow: 0 0 25px rgba(56, 189, 248, 0.5);
49
- color: #020617;
50
- font-weight: 800;
51
- font-size: 18px;
52
  }
53
 
54
- #eburon-brand-title {
55
- display: flex;
56
- flex-direction: column;
57
- }
58
-
59
- #eburon-brand-title span:nth-child(1) {
60
- font-size: 19px;
61
  font-weight: 700;
62
- letter-spacing: 0.06em;
63
- text-transform: uppercase;
64
- color: #e5e7eb;
65
- }
66
-
67
- #eburon-brand-title span:nth-child(2) {
68
- font-size: 12px;
69
- color: #9ca3af;
70
- }
71
-
72
- /* Header right badge */
73
- #eburon-header-right {
74
- display: inline-flex;
75
- align-items: center;
76
- gap: 8px;
77
- font-size: 11px;
78
- color: #9ca3af;
79
- padding: 6px 12px;
80
- border-radius: 999px;
81
- border: 1px solid rgba(148, 163, 184, 0.4);
82
- background: radial-gradient(circle at top, rgba(55, 65, 81, 0.9), rgba(15, 23, 42, 0.9));
83
- }
84
-
85
- /* Cards */
86
- .eburon-card {
87
- border-radius: 18px;
88
- background: radial-gradient(circle at top left, #0b1120, #020617);
89
- border: 1px solid rgba(30, 64, 175, 0.6);
90
- box-shadow: 0 18px 45px rgba(15, 23, 42, 0.85);
91
- padding: 16px 18px;
92
- }
93
-
94
- /* Script header */
95
- #eburon-script-header {
96
  display: flex;
97
- justify-content: space-between;
98
  align-items: center;
99
- margin-bottom: 6px;
100
  }
101
 
102
- #eburon-script-title {
103
- font-size: 14px;
 
 
 
 
 
104
  font-weight: 600;
105
- color: #e5e7eb;
106
  }
107
 
108
- #eburon-script-subtitle {
109
- font-size: 11px;
110
- color: #9ca3af;
 
 
 
 
111
  }
112
 
113
- /* Voice header */
114
- #eburon-voice-header {
115
- display: flex;
116
- justify-content: space-between;
117
- align-items: center;
118
- margin-bottom: 6px;
 
 
 
119
  }
120
 
121
- #eburon-voice-title {
122
- font-size: 14px;
123
- font-weight: 600;
124
- color: #e5e7eb;
125
  }
126
 
127
- #eburon-voice-subtitle {
128
- font-size: 11px;
129
- color: #9ca3af;
130
- }
131
 
132
- /* Generate row */
133
- #eburon-generate-row {
134
- margin-top: 12px;
 
 
 
135
  }
136
 
137
- /* Generate button */
138
- #eburon-generate-btn button {
139
- width: 100%;
140
- border-radius: 999px;
141
- font-weight: 600;
142
- letter-spacing: 0.02em;
143
- padding: 10px 16px;
144
- background: linear-gradient(135deg, #38bdf8, #6366f1);
145
- box-shadow: 0 12px 30px rgba(79, 70, 229, 0.6);
146
  border: none;
 
 
 
 
 
 
147
  }
148
 
149
- #eburon-generate-btn button:hover {
150
  transform: translateY(-1px);
151
- box-shadow: 0 18px 40px rgba(79, 70, 229, 0.95);
152
  }
153
 
154
- /* Audio player card */
155
- #eburon-audio-card {
156
- border-radius: 18px;
157
- background: radial-gradient(circle at top right, #0f172a, #020617);
158
- border: 1px solid rgba(30, 64, 175, 0.6);
159
- box-shadow: 0 18px 45px rgba(15, 23, 42, 0.85);
160
- padding: 14px 16px;
161
  }
162
 
163
- /* Smaller labels */
164
- label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {
165
- font-size: 12px !important;
166
- color: #9ca3af !important;
167
- }
168
-
169
- /* Textbox styling */
170
- textarea {
171
- background-color: #020617 !important;
172
- border-radius: 14px !important;
173
- border: 1px solid rgba(55, 65, 81, 0.9) !important;
174
- color: #e5e7eb !important;
175
  }
176
 
177
- /* Dropdown & sliders */
178
- select, input[type="range"] {
179
- background-color: #020617 !important;
180
- border-radius: 999px !important;
181
- border: 1px solid rgba(55, 65, 81, 0.9) !important;
182
- }
183
-
184
- /* Warning styling (Gradio Alert) */
185
- .svelte-1g805jl {
186
- border-radius: 999px !important;
187
- }
188
  """
189
 
190
- # -----------------------------
191
- # Core TTS helpers
192
- # -----------------------------
193
- async def get_voices():
194
- voices = await edge_tts.list_voices()
195
- voice_labels = [
196
- f"{v['ShortName']} - {v['Locale']} ({v['Gender']})"
197
- for v in voices
198
- ]
199
- voice_labels.sort()
200
- return voice_labels
201
-
202
-
203
- async def text_to_speech(text, voice, rate, pitch):
204
- if not text.strip():
205
- return None, "Please enter some text to synthesize."
206
-
207
- if not voice:
208
- return None, "Please select a voice."
209
-
210
- # Voice label: "en-US-AriaNeural - en-US (Female)"
211
- voice_short_name = voice.split(" - ")[0].strip()
212
-
213
- rate_str = f"{rate:+d}%"
214
- pitch_str = f"{pitch:+d}Hz"
215
-
216
- communicate = edge_tts.Communicate(
217
- text=text,
218
- voice=voice_short_name,
219
- rate=rate_str,
220
- pitch=pitch_str,
221
- )
222
-
223
- # Save to temporary MP3 file
224
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
225
- tmp_path = tmp_file.name
 
 
 
 
 
 
226
  await communicate.save(tmp_path)
227
-
228
- return tmp_path, None
229
-
230
-
231
- async def tts_interface(text, voice, rate, pitch):
232
- audio, warning = await text_to_speech(text, voice, rate, pitch)
233
- if warning:
234
- # Gradio 6: Warning modal object
235
- return audio, gr.Warning(warning)
236
- return audio, None
237
-
238
-
239
- # -----------------------------
240
- # Eburon Speech – ElevenLabs-like UI
241
- # -----------------------------
242
- async def create_demo():
243
- voices = await get_voices()
244
-
245
- # Gradio 6: do NOT pass css to Blocks, move it to demo.launch()
246
- with gr.Blocks(
247
- analytics_enabled=False,
248
- title="Eburon Speech Studio"
249
- ) as demo:
250
- # Header
251
- gr.HTML(
252
- """
253
- <div id="eburon-header">
254
- <div id="eburon-logo-badge">
255
- <div id="eburon-logo-circle">E</div>
256
- <div id="eburon-brand-title">
257
- <span>EBURON SPEECH</span>
258
- <span>AI voice studio powered by Edge TTS</span>
259
- </div>
260
- </div>
261
- <div id="eburon-header-right">
262
- <span>Realtime TTS</span>
263
- <span>•</span>
264
- <span>Studio Grade Voices</span>
265
- </div>
266
  </div>
267
- """
268
- )
269
-
270
- with gr.Row():
271
- # LEFT: Script panel
272
- with gr.Column(scale=2, min_width=450):
273
- gr.HTML(
274
- """
275
- <div id="eburon-script-header">
276
- <div>
277
- <div id="eburon-script-title">Script</div>
278
- <div id="eburon-script-subtitle">
279
- Paste or type your text. Long-form friendly.
280
- </div>
281
- </div>
282
- <div style="font-size: 11px; color: #6b7280;">
283
- ⏱️ Approx. 5k characters per generation
284
- </div>
285
- </div>
286
- """
287
- )
288
-
289
  with gr.Group(elem_classes="eburon-card"):
290
- text_input = gr.Textbox(
 
291
  label="",
292
- placeholder="Write your narration, dialogue, or public talk script here...",
293
- lines=10
 
 
294
  )
 
 
 
 
295
 
296
- # RIGHT: Voice & settings panel
297
- with gr.Column(scale=1, min_width=320):
298
- gr.HTML(
299
- """
300
- <div id="eburon-voice-header">
301
- <div>
302
- <div id="eburon-voice-title">Voice & Settings</div>
303
- <div id="eburon-voice-subtitle">
304
- Choose a voice and fine-tune its delivery.
305
- </div>
306
- </div>
307
- <div style="font-size: 11px; color: #6b7280;">
308
- 🎧 Best experienced with headphones
309
- </div>
310
- </div>
311
- """
312
- )
313
-
314
  with gr.Group(elem_classes="eburon-card"):
 
 
315
  voice_dropdown = gr.Dropdown(
316
- choices=[""] + voices,
317
- label="Voice",
318
- value="",
319
- info="Pick a voice from the Edge TTS catalog."
 
320
  )
321
-
 
 
322
  rate_slider = gr.Slider(
323
- minimum=-50,
324
- maximum=50,
325
- value=0,
326
- label="Speed",
327
- step=1,
328
- info="Negative is slower, positive is faster."
329
  )
330
-
331
  pitch_slider = gr.Slider(
332
- minimum=-20,
333
- maximum=20,
334
- value=0,
335
- label="Pitch",
336
- step=1,
337
- info="Negative is deeper, positive is brighter."
338
  )
339
 
340
- # Bottom row: Generate + audio preview
341
- with gr.Row(elem_id="eburon-generate-row"):
342
- with gr.Column(scale=1, min_width=260):
343
- generate_btn = gr.Button(
344
- "Generate speech",
345
- variant="primary",
346
- elem_id="eburon-generate-btn"
347
- )
348
- warning_md = gr.Markdown(visible=False)
349
-
350
- with gr.Column(scale=2, min_width=420):
351
- with gr.Group(elem_id="eburon-audio-card"):
352
- gr.Markdown(
353
- "##### Playback\nListen to your generated voice clip below."
354
- )
355
  audio_output = gr.Audio(
356
- label="",
357
- type="filepath",
 
 
358
  )
359
 
360
- generate_btn.click(
361
- fn=tts_interface,
362
- inputs=[text_input, voice_dropdown, rate_slider, pitch_slider],
363
- outputs=[audio_output, warning_md],
 
364
  )
365
 
366
  return demo
367
 
368
-
369
- async def main():
370
- demo = await create_demo()
371
- demo.queue(default_concurrency_limit=50)
372
-
373
- # Gradio 6: css + footer_links go in launch(), show_api is removed
374
- demo.launch(
375
- css=EBURON_CSS,
376
- footer_links=["gradio", "settings"], # hides API link (like show_api=False)
377
- )
378
-
379
 
380
  if __name__ == "__main__":
381
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import edge_tts
3
  import asyncio
4
  import tempfile
5
+ import os
6
+
7
+ # ---------------------------------------------------------
8
+ # 🎨 EBURON STUDIO BRANDING (CSS & THEME)
9
+ # ---------------------------------------------------------
10
+ # This CSS transforms standard Gradio into a "SaaS" look
11
+ # inspired by premium dark-mode interfaces like ElevenLabs.
12
+ # ---------------------------------------------------------
13
+
14
+ EBURON_THEME_CSS = """
15
+ /* RESET & FONTS */
16
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
17
+
18
+ body, .gradio-container {
19
+ background-color: #0b0f19 !important; /* Deep dark background */
20
+ font-family: 'Inter', sans-serif !important;
21
+ color: #e2e8f0;
22
  }
23
 
24
+ /* HIDE GRADIO FOOTER */
25
+ footer { display: none !important; }
 
 
 
26
 
27
+ /* BRAND HEADER */
28
+ .eburon-header {
29
  display: flex;
 
30
  justify-content: space-between;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  align-items: center;
32
+ padding: 1rem 0;
33
+ margin-bottom: 2rem;
34
+ border-bottom: 1px solid #1e293b;
 
 
35
  }
36
 
37
+ .brand-logo {
38
+ font-size: 1.5rem;
 
 
 
 
 
39
  font-weight: 700;
40
+ background: linear-gradient(90deg, #fff 0%, #94a3b8 100%);
41
+ -webkit-background-clip: text;
42
+ -webkit-text-fill-color: transparent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  display: flex;
 
44
  align-items: center;
45
+ gap: 10px;
46
  }
47
 
48
+ .brand-badge {
49
+ background: #1e293b;
50
+ color: #38bdf8;
51
+ font-size: 0.75rem;
52
+ padding: 2px 8px;
53
+ border-radius: 4px;
54
+ border: 1px solid #334155;
55
  font-weight: 600;
 
56
  }
57
 
58
+ /* CARDS & CONTAINERS */
59
+ .eburon-card {
60
+ background: #111827;
61
+ border: 1px solid #1f2937;
62
+ border-radius: 12px;
63
+ padding: 20px;
64
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
65
  }
66
 
67
+ /* INPUT TEXTAREA */
68
+ textarea {
69
+ background-color: #0b0f19 !important;
70
+ border: 1px solid #374151 !important;
71
+ color: #f8fafc !important;
72
+ font-size: 1rem !important;
73
+ line-height: 1.6 !important;
74
+ border-radius: 8px !important;
75
+ transition: all 0.2s ease;
76
  }
77
 
78
+ textarea:focus {
79
+ border-color: #38bdf8 !important;
80
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.2) !important;
 
81
  }
82
 
83
+ /* DROPDOWNS & SLIDERS */
84
+ .gr-form { background: transparent !important; border: none !important; }
 
 
85
 
86
+ label span {
87
+ color: #94a3b8 !important;
88
+ font-size: 0.85rem !important;
89
+ text-transform: uppercase;
90
+ letter-spacing: 0.05em;
91
+ font-weight: 600;
92
  }
93
 
94
+ /* GENERATE BUTTON */
95
+ #gen-btn {
96
+ background: linear-gradient(135deg, #38bdf8 0%, #3b82f6 100%);
 
 
 
 
 
 
97
  border: none;
98
+ color: white;
99
+ font-weight: 600;
100
+ padding: 12px 24px;
101
+ border-radius: 50px; /* Pill shape */
102
+ transition: transform 0.1s, box-shadow 0.2s;
103
+ box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.5);
104
  }
105
 
106
+ #gen-btn:hover {
107
  transform: translateY(-1px);
108
+ box-shadow: 0 6px 20px 0 rgba(59, 130, 246, 0.6);
109
  }
110
 
111
+ #gen-btn:active {
112
+ transform: translateY(1px);
 
 
 
 
 
113
  }
114
 
115
+ /* AUDIO PLAYER */
116
+ .audio-player {
117
+ background: #111827 !important;
118
+ border: 1px solid #374151 !important;
119
+ border-radius: 12px !important;
 
 
 
 
 
 
 
120
  }
121
 
122
+ /* SCROLLBAR */
123
+ ::-webkit-scrollbar { width: 8px; }
124
+ ::-webkit-scrollbar-track { background: #0b0f19; }
125
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
126
+ ::-webkit-scrollbar-thumb:hover { background: #475569; }
 
 
 
 
 
 
127
  """
128
 
129
+ # ---------------------------------------------------------
130
+ # ⚙️ BACKEND LOGIC (EDGE TTS)
131
+ # ---------------------------------------------------------
132
+
133
+ async def get_voice_list():
134
+ """Fetches available voices from Edge TTS and formats them for the dropdown."""
135
+ try:
136
+ voices = await edge_tts.list_voices()
137
+ # Sort by locale, then name
138
+ voices = sorted(voices, key=lambda x: (x['Locale'], x['ShortName']))
139
+
140
+ # Create readable labels: "English (US) - Aria (Female)"
141
+ formatted_voices = []
142
+ for v in voices:
143
+ label = f"{v['Locale']} - {v['ShortName'].split('-')[-1]} ({v['Gender']})"
144
+ value = v['ShortName']
145
+ formatted_voices.append((label, value))
146
+
147
+ return formatted_voices
148
+ except Exception as e:
149
+ return [("Error fetching voices", "en-US-AriaNeural")]
150
+
151
+ async def generate_speech(text, voice, rate_val, pitch_val):
152
+ """
153
+ Generates audio from text using Edge TTS.
154
+ rate_val: int (-50 to +50)
155
+ pitch_val: int (-50 to +50)
156
+ """
157
+ if not text or not text.strip():
158
+ return None
159
+
160
+ # Format rate and pitch string for edge-tts (e.g., "+10%", "-5Hz")
161
+ rate_str = f"{rate_val:+d}%"
162
+ pitch_str = f"{pitch_val:+d}Hz"
163
+
164
+ try:
165
+ communicate = edge_tts.Communicate(text, voice, rate=rate_str, pitch=pitch_str)
166
+
167
+ # Create a temp file
168
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
169
+ tmp_path = tmp_file.name
170
+
171
  await communicate.save(tmp_path)
172
+ return tmp_path
173
+ except Exception as e:
174
+ print(f"Error: {e}")
175
+ return None
176
+
177
+ # ---------------------------------------------------------
178
+ # 🖥️ UI CONSTRUCTION (GRADIO BLOCKS)
179
+ # ---------------------------------------------------------
180
+
181
+ async def build_interface():
182
+ # Pre-fetch voices
183
+ voice_choices = await get_voice_list()
184
+ # Default to a popular US voice if available, else the first one
185
+ default_voice = "en-US-ChristopherNeural"
186
+ if not any(v[1] == default_voice for v in voice_choices):
187
+ default_voice = voice_choices[0][1] if voice_choices else ""
188
+
189
+ with gr.Blocks(css=EBURON_THEME_CSS, title="Eburon Speech Studio") as demo:
190
+
191
+ # --- HEADER ---
192
+ gr.HTML("""
193
+ <div class="eburon-header">
194
+ <div class="brand-logo">
195
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right:8px;">
196
+ <circle cx="12" cy="12" r="10" stroke="#38bdf8" stroke-width="2"/>
197
+ <path d="M8 12L11 15L16 9" stroke="#38bdf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
198
+ </svg>
199
+ Eburon Speech <span style="font-weight:300; color:#64748b; margin-left:4px;">Studio</span>
 
 
 
 
 
 
 
 
 
 
 
200
  </div>
201
+ <div class="brand-badge">v1.8 Pro</div>
202
+ </div>
203
+ """)
204
+
205
+ with gr.Row(equal_height=False):
206
+
207
+ # --- LEFT COLUMN: INPUT ---
208
+ with gr.Column(scale=3):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  with gr.Group(elem_classes="eburon-card"):
210
+ gr.Markdown("### Script \nType or paste the text you want to generate.")
211
+ txt_input = gr.Textbox(
212
  label="",
213
+ placeholder="Once upon a time in a digital realm far, far away...",
214
+ lines=12,
215
+ max_lines=20,
216
+ show_label=False
217
  )
218
+
219
+ with gr.Row(style="margin-top: 20px; align-items: center;"):
220
+ gen_button = gr.Button("Generate Speech", elem_id="gen-btn", scale=0)
221
+ status_msg = gr.Markdown("", visible=True)
222
 
223
+ # --- RIGHT COLUMN: SETTINGS ---
224
+ with gr.Column(scale=1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  with gr.Group(elem_classes="eburon-card"):
226
+ gr.Markdown("### Voice Settings")
227
+
228
  voice_dropdown = gr.Dropdown(
229
+ choices=voice_choices,
230
+ value=default_voice,
231
+ label="Voice Model",
232
+ interactive=True,
233
+ info="Select accent and character"
234
  )
235
+
236
+ gr.Markdown("---") # Visual separator
237
+
238
  rate_slider = gr.Slider(
239
+ minimum=-50, maximum=50, value=0, step=1,
240
+ label="Stability (Speed)",
241
+ info="Adjust speaking rate"
 
 
 
242
  )
243
+
244
  pitch_slider = gr.Slider(
245
+ minimum=-20, maximum=20, value=0, step=1,
246
+ label="Clarity (Pitch)",
247
+ info="Adjust vocal frequency"
 
 
 
248
  )
249
 
250
+ # --- AUDIO OUTPUT ROW ---
251
+ with gr.Row(elem_id="audio-row", visible=True):
252
+ with gr.Column():
253
+ with gr.Group(elem_classes="eburon-card"):
254
+ gr.Markdown("### Generated Audio")
 
 
 
 
 
 
 
 
 
 
255
  audio_output = gr.Audio(
256
+ label="Output",
257
+ type="filepath",
258
+ elem_classes="audio-player",
259
+ autoplay=True
260
  )
261
 
262
+ # --- EVENT LISTENERS ---
263
+ gen_button.click(
264
+ fn=generate_speech,
265
+ inputs=[txt_input, voice_dropdown, rate_slider, pitch_slider],
266
+ outputs=[audio_output]
267
  )
268
 
269
  return demo
270
 
271
+ # ---------------------------------------------------------
272
+ # 🚀 EXECUTION
273
+ # ---------------------------------------------------------
 
 
 
 
 
 
 
 
274
 
275
  if __name__ == "__main__":
276
+ # Asyncio loop handling for EdgeTTS
277
+ loop = asyncio.new_event_loop()
278
+ asyncio.set_event_loop(loop)
279
+
280
+ demo_app = loop.run_until_complete(build_interface())
281
+
282
+ # Launch with allowed paths for temp files
283
+ demo_app.queue().launch(
284
+ server_name="0.0.0.0",
285
+ server_port=7860,
286
+ show_api=False,
287
+ allowed_paths=[tempfile.gettempdir()]
288
+ )