aghilTQ commited on
Commit
6fe99cf
·
verified ·
1 Parent(s): e87aeea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +136 -270
app.py CHANGED
@@ -1,293 +1,159 @@
1
- import spaces
2
  import gradio as gr
3
  import edge_tts
4
  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 words for precise clicking
15
- def split_text_into_words(text):
16
- if not text.strip():
17
- return []
18
- words = re.findall(r'\S+', text)
19
- return words
20
 
21
- # Create interactive HTML with clickable words
22
- def create_interactive_text(text):
23
- if not text.strip():
24
- return "<p style='color: #666; font-style: italic;'>Enter text and generate audio to see interactive text here...</p>"
25
-
26
- words = split_text_into_words(text)
27
-
28
- html = f"""
29
- <div id="interactive-text-container" style="line-height: 1.8; font-size: 16px; padding: 20px; border: 2px solid #2196f3; border-radius: 12px; background: linear-gradient(135deg, #f8fdff 0%, #e3f2fd 100%); margin: 10px 0;">
30
- <div style="margin-bottom: 15px; padding: 10px; background: rgba(33, 150, 243, 0.1); border-radius: 8px; text-align: center;">
31
- <p style="margin: 0; font-weight: bold; color: #1976d2; font-size: 14px;">
32
- 🎯 Click on any word below to start reading from that position
33
- </p>
34
- </div>
35
- <div id="clickable-text" style="line-height: 1.6;">
36
- """
37
 
38
- # Reconstruct text with clickable words
39
- text_with_indices = ""
40
- word_index = 0
41
- current_pos = 0
42
 
43
- for match in re.finditer(r'(\S+)(\s*)', text):
44
- word = match.group(1)
45
- space = match.group(2)
46
-
47
- text_with_indices += f'''<span class="clickable-word"
48
- data-index="{word_index}"
49
- data-position="{current_pos}"
50
- style="cursor: pointer; padding: 1px 2px; border-radius: 3px; transition: all 0.2s; display: inline-block; margin: 1px;"
51
- onmouseover="this.style.backgroundColor='#bbdefb'; this.style.transform='scale(1.05)';"
52
- onmouseout="this.style.backgroundColor='transparent'; this.style.transform='scale(1)';"
53
- onclick="handleWordClick({current_pos}, {word_index})">{word}</span>{space}'''
54
-
55
- word_index += 1
56
- current_pos = match.end()
57
-
58
- html += text_with_indices
59
- html += """
60
- </div>
61
- </div>
62
 
63
- <script>
64
- const originalText = `""" + text.replace('`', '\\`').replace('\\', '\\\\') + """`;
65
-
66
- function handleWordClick(position, wordIndex) {
67
- console.log('Word clicked at position:', position, 'word index:', wordIndex);
68
-
69
- // Get text from clicked position
70
- const textFromPosition = originalText.substring(position);
71
- console.log('Text from position:', textFromPosition.substring(0, 50) + '...');
72
-
73
- // Visual feedback - highlight from clicked word onwards
74
- const allWords = document.querySelectorAll('.clickable-word');
75
-
76
- // Reset all highlights
77
- allWords.forEach((word, index) => {
78
- if (index >= wordIndex) {
79
- word.style.backgroundColor = '#c8e6c9';
80
- word.style.fontWeight = 'bold';
81
- } else {
82
- word.style.backgroundColor = 'transparent';
83
- word.style.fontWeight = 'normal';
84
- }
85
- });
86
-
87
- // Trigger Gradio event by updating the textbox value
88
- const event = new CustomEvent('wordClicked', {
89
- detail: {
90
- text: textFromPosition,
91
- position: position,
92
- wordIndex: wordIndex
93
- }
94
- });
95
-
96
- // Store in window for access by Gradio
97
- window.clickedText = textFromPosition;
98
- window.clickedPosition = position;
99
-
100
- // Dispatch custom event
101
- document.dispatchEvent(event);
102
-
103
- // Also try to find and update hidden textbox directly
104
- setTimeout(() => {
105
- const hiddenTextarea = document.querySelector('textarea[data-testid="textbox"]:not([style*="display: block"])');
106
- if (hiddenTextarea) {
107
- hiddenTextarea.value = textFromPosition;
108
- hiddenTextarea.dispatchEvent(new Event('input', { bubbles: true }));
109
- }
110
- }, 100);
111
- }
112
-
113
- // Function to get clicked text (called by Gradio)
114
- function getClickedText() {
115
- return window.clickedText || '';
116
- }
117
- </script>
118
- """
119
 
120
- return html
121
 
122
- # Text-to-speech function
123
- async def text_to_speech(text, voice, rate, pitch):
124
- if not text.strip():
125
- return None, "Please enter text to convert."
126
- if not voice:
127
- return None, "Please select a voice."
128
-
129
  try:
130
- voice_short_name = voice.split(" - ")[0] if " - " in voice else voice
131
- rate_str = f"{rate:+d}%"
132
- pitch_str = f"{pitch:+d}Hz"
133
- communicate = edge_tts.Communicate(text, voice_short_name, rate=rate_str, pitch=pitch_str)
134
-
135
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
136
- tmp_path = tmp_file.name
137
- await communicate.save(tmp_path)
138
-
139
- return tmp_path, "✅ Audio generated! Click any word in the text below to start from that position."
140
  except Exception as e:
141
- return None, f" Error: {str(e)}"
142
 
143
- # Generate full audio
144
- @spaces.GPU
145
- def generate_full_audio(text, voice, rate, pitch):
146
- if not text.strip():
147
- return None, "", "Please enter some text first."
148
- if not voice:
149
- return None, "", "Please select a voice."
150
-
151
- audio_path, message = asyncio.run(text_to_speech(text, voice, rate, pitch))
152
- interactive_html = create_interactive_text(text)
153
 
154
- return audio_path, interactive_html, message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- # Generate partial audio - this will be called when text is clicked
157
- @spaces.GPU
158
- def generate_partial_audio(clicked_text, voice, rate, pitch):
159
- if not clicked_text or not clicked_text.strip():
160
- return None, "No text was clicked."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
- print(f"Generating partial audio for: {clicked_text[:50]}...") # Debug
 
 
163
 
164
- audio_path, message = asyncio.run(text_to_speech(clicked_text, voice, rate, pitch))
 
165
 
166
- if audio_path:
167
- return audio_path, f"🎵 Playing from clicked position: '{clicked_text[:30]}...'"
168
- else:
169
- return None, f"❌ Failed to generate audio: {message}"
170
-
171
- # Function to handle click events (bridge between JS and Python)
172
- def handle_text_click(full_text, voice, rate, pitch):
173
- # This function will be called by JavaScript
174
- # JavaScript will pass the clicked text through a hidden component
175
- return "Click detected - processing..."
176
-
177
- # Create the Gradio application
178
- async def create_demo():
179
- voices = await get_voices()
180
- voice_choices = list(voices.keys())
181
 
182
- with gr.Blocks(
183
- title="Interactive Text-to-Speech",
184
- theme=gr.themes.Soft()
185
- ) as demo:
186
-
187
- gr.Markdown("""
188
- # 🎤 Interactive Text-to-Speech
189
- ## Click anywhere in your text to start reading from that position!
190
- """)
191
-
192
- with gr.Row():
193
- with gr.Column(scale=1):
194
- gr.Markdown("### 📝 Input")
195
- text_input = gr.Textbox(
196
- label="Your Text",
197
- lines=6,
198
- value="Hello and welcome to the interactive text-to-speech system. This incredible technology allows you to click on any word in this sentence and the system will start reading from exactly that position. Try it out by first generating the audio, then clicking anywhere in this text to hear it read from that point!"
199
- )
200
-
201
- voice_dropdown = gr.Dropdown(
202
- choices=voice_choices,
203
- label="Voice",
204
- value=voice_choices[0] if voice_choices else None
205
- )
206
-
207
- with gr.Row():
208
- rate_slider = gr.Slider(-50, 50, 0, step=5, label="Speed (%)")
209
- pitch_slider = gr.Slider(-20, 20, 0, step=2, label="Pitch (Hz)")
210
-
211
- generate_btn = gr.Button("🎵 Generate Interactive Audio", variant="primary", size="lg")
212
-
213
- with gr.Column(scale=1):
214
- gr.Markdown("### 🔊 Audio Output")
215
- full_audio = gr.Audio(label="Full Audio")
216
- partial_audio = gr.Audio(label="Audio from Clicked Position")
217
- status_box = gr.Textbox(label="Status", interactive=False, lines=2)
218
-
219
- gr.Markdown("### 🎯 Interactive Text (Click any word)")
220
- interactive_display = gr.HTML(
221
- value="<div style='text-align: center; padding: 40px; color: #666;'>📝 Generate audio first to see clickable text</div>"
222
- )
223
-
224
- # Hidden textbox to capture clicked text
225
- clicked_text_box = gr.Textbox(visible=False, interactive=True)
226
-
227
- # Create a polling mechanism to check for clicks
228
- def check_for_clicks(current_clicked_text, voice, rate, pitch):
229
- if current_clicked_text and current_clicked_text.strip():
230
- # Generate audio for the clicked text
231
- return generate_partial_audio(current_clicked_text, voice, rate, pitch)
232
- return None, "Waiting for text click..."
233
-
234
- # Event handlers
235
- generate_btn.click(
236
- fn=generate_full_audio,
237
- inputs=[text_input, voice_dropdown, rate_slider, pitch_slider],
238
- outputs=[full_audio, interactive_display, status_box]
239
- )
240
-
241
- # Monitor the hidden textbox for changes (when JavaScript updates it)
242
- clicked_text_box.change(
243
- fn=check_for_clicks,
244
- inputs=[clicked_text_box, voice_dropdown, rate_slider, pitch_slider],
245
- outputs=[partial_audio, status_box]
246
- )
247
-
248
- # Add JavaScript to bridge the gap
249
- demo.load(js="""
250
- function setupClickHandler() {
251
- console.log('Setting up click handler...');
252
-
253
- // Function to update the hidden textbox when text is clicked
254
- document.addEventListener('wordClicked', function(e) {
255
- console.log('Word clicked event received:', e.detail);
256
-
257
- // Find the hidden textbox and update its value
258
- setTimeout(() => {
259
- const textareas = document.querySelectorAll('textarea');
260
- const hiddenTextarea = Array.from(textareas).find(ta =>
261
- ta.style.display === 'none' ||
262
- ta.parentElement.style.display === 'none' ||
263
- ta.closest('[style*="display: none"]')
264
- );
265
-
266
- if (hiddenTextarea) {
267
- console.log('Found hidden textarea, updating with:', e.detail.text.substring(0, 50));
268
- hiddenTextarea.value = e.detail.text;
269
-
270
- // Trigger the change event
271
- const event = new Event('input', { bubbles: true });
272
- hiddenTextarea.dispatchEvent(event);
273
-
274
- // Also try blur event
275
- setTimeout(() => {
276
- hiddenTextarea.dispatchEvent(new Event('blur', { bubbles: true }));
277
- }, 100);
278
- } else {
279
- console.log('Hidden textarea not found');
280
- }
281
- }, 200);
282
- });
283
- }
284
-
285
- setupClickHandler();
286
- """)
287
 
288
- return demo
 
 
 
 
289
 
290
- # Run the application
291
  if __name__ == "__main__":
292
- demo = asyncio.run(create_demo())
293
- demo.launch(share=True, debug=True)
 
 
1
  import gradio as gr
2
  import edge_tts
3
  import asyncio
4
+ from typing import Optional
5
  import os
 
6
 
7
+ # Global variables to manage playback
8
+ current_playback_task = None
9
+ current_position = 0
10
+ stop_playback_flag = False
 
 
 
 
 
 
 
11
 
12
+ async def text_to_speech(text: str, play_from: int = 0, voice: str = "en-US-GuyNeural") -> tuple[str, str]:
13
+ global current_playback_task, current_position, stop_playback_flag
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ # Stop any ongoing playback
16
+ stop_playback()
 
 
17
 
18
+ # Get only the text from the specified position
19
+ text_to_read = text[play_from:]
20
+ current_position = play_from
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ # Create a new playback task
23
+ current_playback_task = asyncio.create_task(play_audio(text_to_read, voice))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ return text, f"Playing from position {play_from}..."
26
 
27
+ async def play_audio(text: str, voice: str):
 
 
 
 
 
 
28
  try:
29
+ communicate = edge_tts.Communicate(text, voice)
30
+ async for chunk in communicate.stream():
31
+ if stop_playback_flag:
32
+ break
 
 
 
 
 
 
33
  except Exception as e:
34
+ print(f"Playback error: {e}")
35
 
36
+ def stop_playback():
37
+ global current_playback_task, stop_playback_flag
38
+ if current_playback_task:
39
+ stop_playback_flag = True
40
+ current_playback_task.cancel()
41
+ stop_playback_flag = False
42
+
43
+ def handle_word_click(word_index: int, text: str, voice: str) -> tuple[str, str]:
44
+ global current_position
 
45
 
46
+ # Convert word index to character position
47
+ words = text.split()
48
+ if word_index < len(words):
49
+ # Find the character position of the clicked word
50
+ char_pos = 0
51
+ for i, word in enumerate(words):
52
+ if i == word_index:
53
+ break
54
+ char_pos += len(word) + 1 # +1 for the space
55
+
56
+ # Start playback from this position
57
+ return asyncio.run(text_to_speech(text, char_pos, voice))
58
+ return text, "Invalid word selection"
59
+
60
+ async def get_voices():
61
+ voices = await edge_tts.list_voices()
62
+ return [v["ShortName"] for v in voices if v["Locale"].startswith("en-")]
63
+
64
+ # JavaScript for interactive text
65
+ js = """
66
+ function splitText(text) {
67
+ if (!text) return '';
68
+ const words = text.split(' ');
69
+ return words.map((word, index) => {
70
+ return `<span class="word" data-index="${index}"
71
+ style="cursor:pointer; margin-right:5px;"
72
+ onmouseover="this.style.backgroundColor='#f0f0f0'"
73
+ onmouseout="this.style.backgroundColor='transparent'"
74
+ onclick="handleWordClick(this)">${word}</span>`;
75
+ }).join(' ');
76
+ }
77
 
78
+ function handleWordClick(element) {
79
+ const index = element.getAttribute('data-index');
80
+ document.getElementById('word-index').value = index;
81
+ document.getElementById('word-index').dispatchEvent(new Event('input'));
82
+ }
83
+
84
+ function updateTextDisplay(text) {
85
+ const container = document.getElementById('text-display');
86
+ container.innerHTML = splitText(text);
87
+ }
88
+
89
+ document.addEventListener('DOMContentLoaded', function() {
90
+ // Update display when text changes
91
+ const textInput = document.querySelector('#text-input textarea');
92
+ if (textInput) {
93
+ textInput.addEventListener('input', function() {
94
+ updateTextDisplay(this.value);
95
+ });
96
+ }
97
+ });
98
+ """
99
+
100
+ css = """
101
+ .word:hover { background-color: #f0f0f0; }
102
+ #text-display {
103
+ padding: 10px;
104
+ border: 1px solid #ccc;
105
+ border-radius: 5px;
106
+ min-height: 100px;
107
+ white-space: pre-wrap;
108
+ }
109
+ """
110
+
111
+ with gr.Blocks(js=js, css=css) as demo:
112
+ gr.Markdown("# Interactive TTS Service (edge-tts)")
113
+ gr.Markdown("Enter text below and click 'Speak' to hear it. Click on any word to start playback from that point.")
114
 
115
+ with gr.Row():
116
+ text_input = gr.Textbox(label="Input Text", lines=5, elem_id="text-input")
117
+ word_index = gr.Number(visible=False, elem_id="word-index")
118
 
119
+ with gr.Row():
120
+ text_display = gr.HTML(elem_id="text-display")
121
 
122
+ with gr.Row():
123
+ voice_dropdown = gr.Dropdown(label="Voice", choices=asyncio.run(get_voices()), value="en-US-GuyNeural")
124
+ rate_slider = gr.Slider(50, 200, value=100, label="Speed (%)")
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ with gr.Row():
127
+ speak_btn = gr.Button("Speak", variant="primary")
128
+ stop_btn = gr.Button("Stop")
129
+
130
+ status = gr.Textbox(label="Status", interactive=False)
131
+
132
+ # Event handlers
133
+ text_input.change(
134
+ fn=lambda x: x,
135
+ inputs=text_input,
136
+ outputs=text_display,
137
+ _js="(x) => { updateTextDisplay(x); return x; }"
138
+ )
139
+
140
+ word_index.change(
141
+ fn=handle_word_click,
142
+ inputs=[word_index, text_input, voice_dropdown],
143
+ outputs=[text_input, status]
144
+ )
145
+
146
+ speak_btn.click(
147
+ fn=lambda text, voice: asyncio.run(text_to_speech(text, 0, voice)),
148
+ inputs=[text_input, voice_dropdown],
149
+ outputs=[text_input, status]
150
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ stop_btn.click(
153
+ fn=stop_playback,
154
+ outputs=status,
155
+ _js="() => { return 'Playback stopped'; }"
156
+ )
157
 
 
158
  if __name__ == "__main__":
159
+ demo.launch()