Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,7 +5,6 @@ import asyncio
|
|
| 5 |
import tempfile
|
| 6 |
import os
|
| 7 |
import re
|
| 8 |
-
import json
|
| 9 |
|
| 10 |
# Get all available voices
|
| 11 |
async def get_voices():
|
|
@@ -16,10 +15,8 @@ async def get_voices():
|
|
| 16 |
def split_text_into_words(text):
|
| 17 |
if not text.strip():
|
| 18 |
return []
|
| 19 |
-
|
| 20 |
-
|
| 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):
|
|
@@ -29,100 +26,93 @@ def create_interactive_text(text):
|
|
| 29 |
words = split_text_into_words(text)
|
| 30 |
|
| 31 |
html = f"""
|
| 32 |
-
<div id="interactive-text-container" style="line-height:
|
| 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
|
| 36 |
</p>
|
| 37 |
</div>
|
| 38 |
-
<div id="clickable-text" style="
|
| 39 |
"""
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
|
|
|
| 51 |
html += """
|
| 52 |
</div>
|
| 53 |
</div>
|
| 54 |
|
| 55 |
<script>
|
| 56 |
-
|
| 57 |
-
let currentVoice = '';
|
| 58 |
-
let currentRate = 0;
|
| 59 |
-
let currentPitch = 0;
|
| 60 |
-
let isGenerating = false;
|
| 61 |
|
| 62 |
-
function
|
| 63 |
-
|
| 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
|
| 102 |
-
const
|
|
|
|
| 103 |
|
| 104 |
-
|
|
|
|
| 105 |
|
| 106 |
-
//
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
//
|
| 110 |
-
const event = new CustomEvent('
|
| 111 |
detail: {
|
| 112 |
-
text:
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
pitch: currentPitch,
|
| 116 |
-
startIndex: startIndex
|
| 117 |
}
|
| 118 |
});
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
document.dispatchEvent(event);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
-
//
|
| 124 |
-
function
|
| 125 |
-
|
| 126 |
}
|
| 127 |
</script>
|
| 128 |
"""
|
|
@@ -137,7 +127,7 @@ async def text_to_speech(text, voice, rate, pitch):
|
|
| 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)
|
|
@@ -146,220 +136,158 @@ async def text_to_speech(text, voice, rate, pitch):
|
|
| 146 |
tmp_path = tmp_file.name
|
| 147 |
await communicate.save(tmp_path)
|
| 148 |
|
| 149 |
-
return tmp_path, "Audio generated
|
| 150 |
except Exception as e:
|
| 151 |
-
return None, f"
|
| 152 |
|
| 153 |
-
#
|
| 154 |
@spaces.GPU
|
| 155 |
def generate_full_audio(text, voice, rate, pitch):
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 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 |
-
#
|
| 179 |
-
@spaces.GPU
|
| 180 |
-
def generate_partial_audio(
|
| 181 |
-
if not
|
| 182 |
-
return None, "No text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 207 |
-
##
|
| 208 |
""")
|
| 209 |
|
| 210 |
with gr.Row():
|
| 211 |
with gr.Column(scale=1):
|
| 212 |
-
#
|
| 213 |
-
gr.Markdown("### π Input Settings")
|
| 214 |
text_input = gr.Textbox(
|
| 215 |
-
label="
|
| 216 |
lines=6,
|
| 217 |
-
|
| 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=
|
| 223 |
-
label="
|
| 224 |
-
value=
|
| 225 |
)
|
| 226 |
|
| 227 |
with gr.Row():
|
| 228 |
-
rate_slider = gr.Slider(-50, 50, 0, step=
|
| 229 |
-
pitch_slider = gr.Slider(-20, 20, 0, step=
|
| 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"
|
| 243 |
-
partial_audio = gr.Audio(label="
|
|
|
|
| 244 |
|
| 245 |
-
# Interactive
|
| 246 |
-
gr.Markdown("### π― Interactive Text")
|
| 247 |
interactive_display = gr.HTML(
|
| 248 |
-
value="<
|
| 249 |
)
|
| 250 |
|
| 251 |
-
# Hidden
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 262 |
)
|
| 263 |
|
| 264 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
demo.load(js="""
|
| 266 |
-
function
|
| 267 |
-
|
| 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 |
-
|
| 282 |
-
|
| 283 |
-
console.log('
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 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 (
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
// Update status
|
| 308 |
-
if (statusComponent) {
|
| 309 |
-
statusComponent.value = `β
Audio generated from selected position!`;
|
| 310 |
-
}
|
| 311 |
|
| 312 |
-
// Trigger
|
| 313 |
-
const
|
| 314 |
-
|
| 315 |
-
});
|
| 316 |
-
document.dispatchEvent(audioUpdateEvent);
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
} else {
|
| 319 |
-
console.
|
| 320 |
-
if (statusComponent) {
|
| 321 |
-
statusComponent.value = `β Failed to generate audio from selected position`;
|
| 322 |
-
}
|
| 323 |
}
|
| 324 |
-
}
|
| 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 |
-
|
| 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)
|
|
|
|
| 5 |
import tempfile
|
| 6 |
import os
|
| 7 |
import re
|
|
|
|
| 8 |
|
| 9 |
# Get all available voices
|
| 10 |
async def get_voices():
|
|
|
|
| 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):
|
|
|
|
| 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 |
"""
|
|
|
|
| 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)
|
|
|
|
| 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)
|