aghilTQ commited on
Commit
e38225d
Β·
verified Β·
1 Parent(s): 044ac72

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +264 -160
app.py CHANGED
@@ -5,257 +5,361 @@ import asyncio
5
  import tempfile
6
  import os
7
  import re
 
8
 
9
  # Get all available voices
10
  async def get_voices():
11
  voices = await edge_tts.list_voices()
12
  return {f"{v['ShortName']} - {v['Locale']} ({v['Gender']})": v['ShortName'] for v in voices}
13
 
14
- # Split text into sentences for interactive clicking
15
- def split_text_into_segments(text):
16
- # Split by sentences, keeping punctuation
17
- segments = re.split(r'([.!?]+)', text)
18
- result = []
19
- current_segment = ""
20
-
21
- for i, segment in enumerate(segments):
22
- if segment.strip():
23
- if re.match(r'^[.!?]+$', segment):
24
- current_segment += segment
25
- if current_segment.strip():
26
- result.append(current_segment.strip())
27
- current_segment = ""
28
- else:
29
- current_segment += segment
30
-
31
- if current_segment.strip():
32
- result.append(current_segment.strip())
33
 
34
- return [seg for seg in result if seg.strip()]
 
 
35
 
36
- # Create interactive HTML with clickable text segments
37
  def create_interactive_text(text):
38
  if not text.strip():
39
- return ""
40
 
41
- segments = split_text_into_segments(text)
42
- html_parts = []
43
 
44
- html_parts.append("""
45
- <div id="interactive-text" style="line-height: 1.8; font-size: 16px; padding: 20px; border: 1px solid #ddd; border-radius: 8px; background: #f9f9f9;">
46
- <p style="margin-bottom: 15px; font-weight: bold; color: #555;">Click on any part of the text to start playback from that position:</p>
47
- """)
 
 
 
 
 
48
 
49
- for i, segment in enumerate(segments):
50
- html_parts.append(
51
- f'<span class="text-segment" data-index="{i}" style="cursor: pointer; padding: 2px 4px; margin: 1px; border-radius: 3px; transition: background-color 0.2s;" '
52
- f'onmouseover="this.style.backgroundColor=\'#e3f2fd\'" '
53
- f'onmouseout="this.style.backgroundColor=\'transparent\'" '
54
- f'onclick="playFromSegment({i})">{segment} </span>'
55
- )
 
 
56
 
57
- html_parts.append("""
 
58
  </div>
 
59
  <script>
60
- let currentSegments = [];
61
- let currentSettings = {};
 
 
 
62
 
63
- function updateSegments(segments, voice, rate, pitch) {
64
- currentSegments = segments;
65
- currentSettings = {voice: voice, rate: rate, pitch: pitch};
 
 
 
66
  }
67
 
68
- async function playFromSegment(startIndex) {
69
- if (!currentSegments || currentSegments.length === 0) {
70
- alert('Please generate the full audio first to enable interactive playback.');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  return;
72
  }
73
 
74
- // Highlight clicked segment
75
- const segments = document.querySelectorAll('.text-segment');
76
- segments.forEach(s => s.style.backgroundColor = 'transparent');
77
- segments[startIndex].style.backgroundColor = '#bbdefb';
 
 
 
 
 
78
 
79
- // Get text from selected segment onwards
80
- const textFromSegment = currentSegments.slice(startIndex).join(' ');
81
 
82
- // Trigger partial audio generation
83
- const event = new CustomEvent('playFromSegment', {
 
 
 
 
 
84
  detail: {
85
- text: textFromSegment,
86
- voice: currentSettings.voice,
87
- rate: currentSettings.rate,
88
- pitch: currentSettings.pitch,
89
  startIndex: startIndex
90
  }
91
  });
 
92
  document.dispatchEvent(event);
93
  }
 
 
 
 
 
94
  </script>
95
- """)
96
 
97
- return ''.join(html_parts)
98
 
99
  # Text-to-speech function
100
  async def text_to_speech(text, voice, rate, pitch):
101
  if not text.strip():
102
- return None, gr.Warning("Please enter text to convert.")
103
  if not voice:
104
- return None, gr.Warning("Please select a voice.")
105
 
106
- voice_short_name = voice.split(" - ")[0]
107
- rate_str = f"{rate:+d}%"
108
- pitch_str = f"{pitch:+d}Hz"
109
- communicate = edge_tts.Communicate(text, voice_short_name, rate=rate_str, pitch=pitch_str)
110
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
111
- tmp_path = tmp_file.name
112
- await communicate.save(tmp_path)
113
- return tmp_path, None
 
 
 
 
 
114
 
115
- # Gradio interface function for full text
116
  @spaces.GPU
117
- def tts_interface(text, voice, rate, pitch):
118
- audio, warning = asyncio.run(text_to_speech(text, voice, rate, pitch))
119
 
120
- # Create interactive text display
121
  interactive_html = create_interactive_text(text)
122
 
123
- # Prepare segments for JavaScript
124
- segments = split_text_into_segments(text) if text.strip() else []
125
 
126
- # Add JavaScript to update segments
127
- if segments:
 
128
  interactive_html += f"""
129
  <script>
130
  setTimeout(() => {{
131
- updateSegments({segments}, '{voice}', {rate}, {pitch});
132
- }}, 100);
 
133
  </script>
134
  """
135
 
136
- return audio, interactive_html, warning
137
 
138
- # Gradio interface function for partial text (from clicked segment)
139
  @spaces.GPU
140
- def tts_partial_interface(text, voice, rate, pitch):
141
- if not text or not voice:
142
- return None, gr.Warning("Missing text or voice selection.")
143
 
144
- audio, warning = asyncio.run(text_to_speech(text, voice, rate, pitch))
145
- return audio, warning
146
 
147
- # Create Gradio application
148
  async def create_demo():
149
  voices = await get_voices()
150
 
151
- description = """
152
- Experience the power of Voicecloning.be for text-to-speech conversion with interactive text playback.
153
-
154
- **How to use:**
155
- 1. Enter your text and select voice settings
156
- 2. Click "Generate Full Audio" to create the complete audio
157
- 3. Click on any part of the displayed text to start playback from that position
158
- """
159
-
160
- with gr.Blocks(title="Interactive Text-to-Speech", theme=gr.themes.Soft()) as demo:
161
- gr.Markdown("# 🎀 Interactive Text-to-Speech")
162
- gr.Markdown(description)
 
 
 
 
 
 
163
 
164
  with gr.Row():
165
- with gr.Column(scale=2):
 
 
166
  text_input = gr.Textbox(
167
- label="Input Text",
168
- lines=8,
169
- placeholder="Enter the text you want to convert to speech..."
 
170
  )
171
 
172
- with gr.Row():
173
- voice_dropdown = gr.Dropdown(
174
- choices=[""] + list(voices.keys()),
175
- label="Select Voice",
176
- value=""
177
- )
178
 
179
  with gr.Row():
180
- rate_slider = gr.Slider(
181
- minimum=-50,
182
- maximum=50,
183
- value=0,
184
- label="Speech Rate (%)",
185
- step=1
186
- )
187
- pitch_slider = gr.Slider(
188
- minimum=-20,
189
- maximum=20,
190
- value=0,
191
- label="Pitch (Hz)",
192
- step=1
193
- )
194
 
195
  generate_btn = gr.Button(
196
- "🎡 Generate Full Audio",
197
- variant="primary",
198
  size="lg"
199
  )
 
 
200
 
201
- with gr.Column(scale=2):
202
- full_audio_output = gr.Audio(
203
- label="πŸ”Š Full Audio",
204
- type="filepath"
205
- )
206
- partial_audio_output = gr.Audio(
207
- label="🎯 Partial Audio (from clicked position)",
208
- type="filepath"
209
- )
210
 
211
  # Interactive text display
212
- interactive_text_display = gr.HTML(
213
- label="πŸ“ Interactive Text (Click to play from position)",
214
- value="<p style='color: #666; font-style: italic;'>Generate audio first to see interactive text here...</p>"
215
  )
216
 
217
- # Warning display
218
- warning_display = gr.Markdown(visible=False)
219
-
220
  # Hidden components for partial audio generation
221
- partial_text = gr.Textbox(visible=False)
222
- partial_voice = gr.Textbox(visible=False)
223
- partial_rate = gr.Number(visible=False)
224
- partial_pitch = gr.Number(visible=False)
225
 
226
  # Event handlers
227
  generate_btn.click(
228
- fn=tts_interface,
229
  inputs=[text_input, voice_dropdown, rate_slider, pitch_slider],
230
- outputs=[full_audio_output, interactive_text_display, warning_display]
231
  )
232
 
233
- # JavaScript to handle segment clicks
234
  demo.load(js="""
235
- function setupInteractiveText() {
236
- document.addEventListener('playFromSegment', async function(e) {
237
- const detail = e.detail;
 
 
 
 
 
 
 
238
 
239
- // Update hidden inputs
240
- const partialTextInput = document.querySelector('textarea[data-testid="textbox"]').parentElement.parentElement.parentElement.querySelector('textarea');
241
- const partialVoiceInput = document.querySelector('input[type="text"]');
 
 
 
 
242
 
243
- // Trigger partial audio generation through Gradio
244
- // This is a simplified approach - in a full implementation, you'd need to properly integrate with Gradio's state management system
245
- console.log('Playing from segment:', detail.startIndex);
246
- console.log('Text:', detail.text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
- // For now, show an alert with the functionality
249
- alert(`Playing from segment ${detail.startIndex + 1}:\\n\\n"${detail.text.substring(0, 100)}${detail.text.length > 100 ? '...' : ''}"`);
250
  });
251
  }
252
 
253
- setTimeout(setupInteractiveText, 1000);
254
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  return demo
257
 
258
  # Run the application
259
  if __name__ == "__main__":
260
  demo = asyncio.run(create_demo())
261
- demo.launch()
 
5
  import tempfile
6
  import os
7
  import re
8
+ import json
9
 
10
  # Get all available voices
11
  async def get_voices():
12
  voices = await edge_tts.list_voices()
13
  return {f"{v['ShortName']} - {v['Locale']} ({v['Gender']})": v['ShortName'] for v in voices}
14
 
15
+ # Split text into words for precise clicking
16
+ def split_text_into_words(text):
17
+ if not text.strip():
18
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # Split by words while preserving spaces and punctuation
21
+ words = re.findall(r'\S+|\s+', text)
22
+ return [word for word in words if word.strip()]
23
 
24
+ # Create interactive HTML with clickable words
25
  def create_interactive_text(text):
26
  if not text.strip():
27
+ return "<p style='color: #666; font-style: italic;'>Enter text and generate audio to see interactive text here...</p>"
28
 
29
+ words = split_text_into_words(text)
 
30
 
31
+ html = f"""
32
+ <div id="interactive-text-container" style="line-height: 2; font-size: 16px; padding: 20px; border: 2px solid #e1f5fe; border-radius: 12px; background: linear-gradient(135deg, #f8fdff 0%, #e3f2fd 100%); box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
33
+ <div style="margin-bottom: 15px; padding: 10px; background: rgba(33, 150, 243, 0.1); border-radius: 8px; text-align: center;">
34
+ <p style="margin: 0; font-weight: bold; color: #1976d2; font-size: 14px;">
35
+ 🎯 Click anywhere in the text below to start reading from that position
36
+ </p>
37
+ </div>
38
+ <div id="clickable-text" style="cursor: text; user-select: text;">
39
+ """
40
 
41
+ for i, word in enumerate(words):
42
+ if word.strip(): # Only make actual words clickable, not spaces
43
+ html += f'''<span class="word-segment" data-index="{i}"
44
+ style="cursor: pointer; padding: 2px 1px; border-radius: 3px; transition: all 0.2s ease; display: inline;"
45
+ onmouseover="this.style.backgroundColor='#bbdefb'; this.style.transform='scale(1.05)';"
46
+ onmouseout="this.style.backgroundColor='transparent'; this.style.transform='scale(1)';"
47
+ onclick="playFromWord({i})">{word}</span>'''
48
+ else:
49
+ html += word # Preserve spaces as-is
50
 
51
+ html += """
52
+ </div>
53
  </div>
54
+
55
  <script>
56
+ let allWords = [];
57
+ let currentVoice = '';
58
+ let currentRate = 0;
59
+ let currentPitch = 0;
60
+ let isGenerating = false;
61
 
62
+ function updateWordsData(words, voice, rate, pitch) {
63
+ allWords = words;
64
+ currentVoice = voice;
65
+ currentRate = rate;
66
+ currentPitch = pitch;
67
+ console.log('Updated words data:', words.length, 'words');
68
  }
69
 
70
+ function highlightFromWord(startIndex) {
71
+ // Remove previous highlights
72
+ const segments = document.querySelectorAll('.word-segment');
73
+ segments.forEach(s => {
74
+ s.style.backgroundColor = 'transparent';
75
+ s.style.fontWeight = 'normal';
76
+ });
77
+
78
+ // Highlight from clicked word onwards
79
+ for (let i = startIndex; i < segments.length; i++) {
80
+ segments[i].style.backgroundColor = '#c8e6c9';
81
+ segments[i].style.fontWeight = 'bold';
82
+ }
83
+ }
84
+
85
+ function playFromWord(startIndex) {
86
+ if (allWords.length === 0) {
87
+ alert('Please generate the full audio first!');
88
  return;
89
  }
90
 
91
+ if (isGenerating) {
92
+ alert('Audio is being generated, please wait...');
93
+ return;
94
+ }
95
+
96
+ console.log('Playing from word index:', startIndex);
97
+
98
+ // Highlight the selected portion
99
+ highlightFromWord(startIndex);
100
 
101
+ // Get text from selected word onwards
102
+ const textFromWord = allWords.slice(startIndex).join('');
103
 
104
+ console.log('Text from word:', textFromWord.substring(0, 100) + '...');
105
+
106
+ // Trigger the partial audio generation
107
+ isGenerating = true;
108
+
109
+ // Find the gradio interface elements and trigger the partial generation
110
+ const event = new CustomEvent('generatePartialAudio', {
111
  detail: {
112
+ text: textFromWord,
113
+ voice: currentVoice,
114
+ rate: currentRate,
115
+ pitch: currentPitch,
116
  startIndex: startIndex
117
  }
118
  });
119
+
120
  document.dispatchEvent(event);
121
  }
122
+
123
+ // Reset generation flag when audio is ready
124
+ function resetGenerationFlag() {
125
+ isGenerating = false;
126
+ }
127
  </script>
128
+ """
129
 
130
+ return html
131
 
132
  # Text-to-speech function
133
  async def text_to_speech(text, voice, rate, pitch):
134
  if not text.strip():
135
+ return None, "Please enter text to convert."
136
  if not voice:
137
+ return None, "Please select a voice."
138
 
139
+ try:
140
+ voice_short_name = voice.split(" - ")[0]
141
+ rate_str = f"{rate:+d}%"
142
+ pitch_str = f"{pitch:+d}Hz"
143
+ communicate = edge_tts.Communicate(text, voice_short_name, rate=rate_str, pitch=pitch_str)
144
+
145
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
146
+ tmp_path = tmp_file.name
147
+ await communicate.save(tmp_path)
148
+
149
+ return tmp_path, "Audio generated successfully!"
150
+ except Exception as e:
151
+ return None, f"Error generating audio: {str(e)}"
152
 
153
+ # Main TTS interface
154
  @spaces.GPU
155
+ def generate_full_audio(text, voice, rate, pitch):
156
+ audio_path, message = asyncio.run(text_to_speech(text, voice, rate, pitch))
157
 
158
+ # Create interactive text
159
  interactive_html = create_interactive_text(text)
160
 
161
+ # Prepare words data for JavaScript
162
+ words = split_text_into_words(text) if text.strip() else []
163
 
164
+ # Add script to update JavaScript variables
165
+ if words and audio_path:
166
+ words_json = json.dumps(words).replace('"', '\\"')
167
  interactive_html += f"""
168
  <script>
169
  setTimeout(() => {{
170
+ updateWordsData({json.dumps(words)}, "{voice}", {rate}, {pitch});
171
+ resetGenerationFlag();
172
+ }}, 500);
173
  </script>
174
  """
175
 
176
+ return audio_path, interactive_html, message
177
 
178
+ # Partial audio generation (triggered by JavaScript)
179
  @spaces.GPU
180
+ def generate_partial_audio(partial_text, voice, rate, pitch):
181
+ if not partial_text.strip():
182
+ return None, "No text to convert."
183
 
184
+ audio_path, message = asyncio.run(text_to_speech(partial_text, voice, rate, pitch))
185
+ return audio_path, message
186
 
187
+ # Create the Gradio application
188
  async def create_demo():
189
  voices = await get_voices()
190
 
191
+ with gr.Blocks(
192
+ title="Interactive Text-to-Speech",
193
+ theme=gr.themes.Soft(),
194
+ css="""
195
+ .gradio-container {
196
+ max-width: 1200px !important;
197
+ }
198
+ #interactive-text-container {
199
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
200
+ }
201
+ """
202
+ ) as demo:
203
+
204
+ # Header
205
+ gr.Markdown("""
206
+ # 🎀 Interactive Text-to-Speech System
207
+ ### Click anywhere in your text to start reading from that exact position!
208
+ """)
209
 
210
  with gr.Row():
211
+ with gr.Column(scale=1):
212
+ # Input section
213
+ gr.Markdown("### πŸ“ Input Settings")
214
  text_input = gr.Textbox(
215
+ label="Enter Your Text",
216
+ lines=6,
217
+ placeholder="Type or paste your text here. After generating audio, you'll be able to click anywhere in the text to start reading from that position...",
218
+ value="Welcome to the interactive text-to-speech system. This amazing technology allows you to click on any word in this text and start playing the audio from that exact position. Try clicking anywhere after generating the audio!"
219
  )
220
 
221
+ voice_dropdown = gr.Dropdown(
222
+ choices=list(voices.keys()),
223
+ label="Select Voice",
224
+ value=list(voices.keys())[0] if voices else ""
225
+ )
 
226
 
227
  with gr.Row():
228
+ rate_slider = gr.Slider(-50, 50, 0, step=1, label="Speed (%)")
229
+ pitch_slider = gr.Slider(-20, 20, 0, step=1, label="Pitch (Hz)")
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  generate_btn = gr.Button(
232
+ "🎡 Generate Interactive Audio",
233
+ variant="primary",
234
  size="lg"
235
  )
236
+
237
+ status_message = gr.Textbox(label="Status", interactive=False)
238
 
239
+ with gr.Column(scale=1):
240
+ # Output section
241
+ gr.Markdown("### πŸ”Š Audio Output")
242
+ full_audio = gr.Audio(label="Full Audio", type="filepath")
243
+ partial_audio = gr.Audio(label="Partial Audio (from clicked position)", type="filepath")
 
 
 
 
244
 
245
  # Interactive text display
246
+ gr.Markdown("### 🎯 Interactive Text")
247
+ interactive_display = gr.HTML(
248
+ value="<p style='color: #666; font-style: italic; text-align: center; padding: 40px;'>Enter text above and click 'Generate Interactive Audio' to start!</p>"
249
  )
250
 
 
 
 
251
  # Hidden components for partial audio generation
252
+ hidden_text = gr.Textbox(visible=False)
253
+ hidden_voice = gr.Textbox(visible=False)
254
+ hidden_rate = gr.Number(visible=False)
255
+ hidden_pitch = gr.Number(visible=False)
256
 
257
  # Event handlers
258
  generate_btn.click(
259
+ fn=generate_full_audio,
260
  inputs=[text_input, voice_dropdown, rate_slider, pitch_slider],
261
+ outputs=[full_audio, interactive_display, status_message]
262
  )
263
 
264
+ # JavaScript for handling partial audio generation
265
  demo.load(js="""
266
+ function setupPartialAudioGeneration() {
267
+ let partialAudioComponent = null;
268
+ let statusComponent = null;
269
+
270
+ // Find components
271
+ setTimeout(() => {
272
+ const audioElements = document.querySelectorAll('audio');
273
+ if (audioElements.length > 1) {
274
+ partialAudioComponent = audioElements[1]; // Second audio component
275
+ }
276
 
277
+ const textInputs = document.querySelectorAll('textarea');
278
+ statusComponent = textInputs[textInputs.length - 1]; // Status textbox
279
+ }, 1000);
280
+
281
+ document.addEventListener('generatePartialAudio', async function(e) {
282
+ const detail = e.detail;
283
+ console.log('Generating partial audio for:', detail.text.substring(0, 50) + '...');
284
 
285
+ try {
286
+ // Show status
287
+ if (statusComponent) {
288
+ statusComponent.value = `🎡 Generating audio from selected position...`;
289
+ }
290
+
291
+ // Make API call to generate partial audio
292
+ const response = await fetch('/api/predict', {
293
+ method: 'POST',
294
+ headers: {
295
+ 'Content-Type': 'application/json',
296
+ },
297
+ body: JSON.stringify({
298
+ data: [detail.text, detail.voice, detail.rate, detail.pitch],
299
+ fn_index: 1 // Index for partial audio generation function
300
+ })
301
+ });
302
+
303
+ if (response.ok) {
304
+ const result = await response.json();
305
+ console.log('Partial audio generated successfully');
306
+
307
+ // Update status
308
+ if (statusComponent) {
309
+ statusComponent.value = `βœ… Audio generated from selected position!`;
310
+ }
311
+
312
+ // Trigger audio update
313
+ const audioUpdateEvent = new CustomEvent('updatePartialAudio', {
314
+ detail: { audioPath: result.data[0] }
315
+ });
316
+ document.dispatchEvent(audioUpdateEvent);
317
+
318
+ } else {
319
+ console.error('Failed to generate partial audio');
320
+ if (statusComponent) {
321
+ statusComponent.value = `❌ Failed to generate audio from selected position`;
322
+ }
323
+ }
324
+ } catch (error) {
325
+ console.error('Error:', error);
326
+ if (statusComponent) {
327
+ statusComponent.value = `❌ Error: ${error.message}`;
328
+ }
329
+ }
330
 
331
+ // Reset generation flag
332
+ resetGenerationFlag();
333
  });
334
  }
335
 
336
+ setupPartialAudioGeneration();
337
  """)
338
+
339
+ # Add a second function for partial audio generation
340
+ def handle_partial_generation(text, voice, rate, pitch):
341
+ return generate_partial_audio(text, voice, rate, pitch)
342
+
343
+ # Create a hidden interface for partial audio generation
344
+ partial_interface = gr.Interface(
345
+ fn=handle_partial_generation,
346
+ inputs=[
347
+ gr.Textbox(visible=False),
348
+ gr.Textbox(visible=False),
349
+ gr.Number(visible=False),
350
+ gr.Number(visible=False)
351
+ ],
352
+ outputs=[
353
+ gr.Audio(visible=False),
354
+ gr.Textbox(visible=False)
355
+ ],
356
+ live=False,
357
+ allow_flagging=False
358
+ )
359
 
360
  return demo
361
 
362
  # Run the application
363
  if __name__ == "__main__":
364
  demo = asyncio.run(create_demo())
365
+ demo.launch(share=True)