aghilTQ commited on
Commit
0496a05
·
verified ·
1 Parent(s): 65faab7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +140 -138
app.py CHANGED
@@ -1,159 +1,161 @@
 
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 - updated syntax for Gradio 5.32.0
133
- text_input.change(
134
- lambda x: x,
135
- inputs=text_input,
136
- outputs=text_display,
137
- js="(x) => { updateTextDisplay(x); return x; }"
138
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- word_index.input(
141
- handle_word_click,
142
- inputs=[word_index, text_input, voice_dropdown],
143
- outputs=[text_input, status]
144
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- speak_btn.click(
147
- 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
- stop_playback,
154
- outputs=status,
155
- js="() => { return 'Playback stopped'; }"
156
- )
157
 
 
158
  if __name__ == "__main__":
 
159
  demo.launch()
 
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
+ from pydub import AudioSegment
9
+ import numpy as np
10
 
11
+ # Get all available voices
12
+ async def get_voices():
13
+ voices = await edge_tts.list_voices()
14
+ return {f"{v['ShortName']} - {v['Locale']} ({v['Gender']})": v['ShortName'] for v in voices}
15
 
16
+ # Text-to-speech function with word timing estimation
17
+ async def text_to_speech(text, voice, rate, pitch):
18
+ if not text.strip():
19
+ return None, None, gr.Warning("Please enter text to convert.")
20
+ if not voice:
21
+ return None, None, gr.Warning("Please select a voice.")
22
 
23
+ voice_short_name = voice.split(" - ")[0]
24
+ rate_str = f"{rate:+d}%"
25
+ pitch_str = f"{pitch:+d}Hz"
26
 
27
+ # Generate full audio
28
+ communicate = edge_tts.Communicate(text, voice_short_name, rate=rate_str, pitch=pitch_str)
29
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
30
+ tmp_path = tmp_file.name
31
+ await communicate.save(tmp_path)
32
 
33
+ # Estimate word timings
34
+ audio = AudioSegment.from_file(tmp_path)
35
+ duration = len(audio) / 1000.0 # in seconds
36
+ words = re.findall(r'\b\w+\b', text)
37
+ n_words = len(words)
38
+ word_timings = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ if n_words > 0:
41
+ time_per_word = duration / n_words
 
 
 
42
  for i, word in enumerate(words):
43
+ start_time = i * time_per_word
44
+ end_time = (i + 1) * time_per_word
45
+ word_timings.append({
46
+ "word": word,
47
+ "start": start_time,
48
+ "end": end_time
49
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ # Create clickable transcript HTML
52
+ transcript_html = ""
53
+ for i, wt in enumerate(word_timings):
54
+ transcript_html += f'<span class="word" data-start="{wt["start"]}" data-end="{wt["end"]}" data-index="{i}">{wt["word"]}</span> '
55
 
56
+ return tmp_path, transcript_html, None
57
+
58
+ # Gradio interface function
59
+ @spaces.GPU
60
+ def tts_interface(text, voice, rate, pitch):
61
+ audio_file, transcript, warning = asyncio.run(text_to_speech(text, voice, rate, pitch))
62
+ return audio_file, transcript, warning
63
+
64
+ # Create Gradio application
65
+ async def create_demo():
66
+ voices = await get_voices()
67
 
68
+ description = """
69
+ Experience the power of Voicecloning.be for text-to-speech conversion.
70
+ <br><b>NEW:</b> Click on any word in the transcript to start playback from that position!
71
+ """
72
 
73
+ # JavaScript for interactive transcript
74
+ js = """
75
+ <script>
76
+ function handleWordClick(event) {
77
+ const audio = document.querySelector('#audio-player audio');
78
+ const start = parseFloat(event.target.dataset.start);
79
+ if (!isNaN(start) && audio) {
80
+ audio.currentTime = start;
81
+ audio.play();
82
+
83
+ // Highlight clicked word
84
+ document.querySelectorAll('.word').forEach(w =>
85
+ w.style.backgroundColor = 'transparent');
86
+ event.target.style.backgroundColor = '#e6f7ff';
87
+ }
88
+ }
89
+
90
+ document.addEventListener('DOMContentLoaded', () => {
91
+ document.querySelectorAll('.word').forEach(word => {
92
+ word.addEventListener('click', handleWordClick);
93
+ });
94
+ });
95
+ </script>
96
+ """
97
 
98
+ css = """
99
+ .word {
100
+ cursor: pointer;
101
+ padding: 2px 4px;
102
+ border-radius: 4px;
103
+ transition: background-color 0.3s;
104
+ }
105
+ .word:hover {
106
+ background-color: #f0f0f0;
107
+ }
108
+ #transcript-container {
109
+ max-height: 200px;
110
+ overflow-y: auto;
111
+ border: 1px solid #e0e0e0;
112
+ padding: 10px;
113
+ border-radius: 4px;
114
+ margin-top: 10px;
115
+ }
116
+ """
117
 
118
+ with gr.Blocks(css=css) as demo:
119
+ gr.Markdown("# Voicecloning.be Text-to-Speech")
120
+ gr.Markdown(description)
121
+
122
+ with gr.Row():
123
+ with gr.Column():
124
+ text_input = gr.Textbox(label="Input Text", lines=5)
125
+ voice_dropdown = gr.Dropdown(
126
+ choices=[""] + list(voices.keys()),
127
+ label="Select Voice",
128
+ value=""
129
+ )
130
+ rate_slider = gr.Slider(
131
+ minimum=-50, maximum=50, value=0,
132
+ label="Speech Rate Adjustment (%)", step=1
133
+ )
134
+ pitch_slider = gr.Slider(
135
+ minimum=-20, maximum=20, value=0,
136
+ label="Pitch Adjustment (Hz)", step=1
137
+ )
138
+ submit_btn = gr.Button("Generate Speech", variant="primary")
139
+
140
+ with gr.Column():
141
+ audio_output = gr.Audio(label="Generated Audio", elem_id="audio-player")
142
+ transcript_output = gr.HTML(
143
+ label="Interactive Transcript",
144
+ elem_id="transcript-container"
145
+ )
146
+ warning_output = gr.Markdown(visible=False)
147
+
148
+ submit_btn.click(
149
+ fn=tts_interface,
150
+ inputs=[text_input, voice_dropdown, rate_slider, pitch_slider],
151
+ outputs=[audio_output, transcript_output, warning_output]
152
+ )
153
+
154
+ gr.HTML(js)
155
 
156
+ return demo
 
 
 
 
157
 
158
+ # Run the application
159
  if __name__ == "__main__":
160
+ demo = asyncio.run(create_demo())
161
  demo.launch()