Dimitrius174 commited on
Commit
e3ad3e1
·
verified ·
1 Parent(s): 3a50c2e

Upload tts_gui.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. tts_gui.py +300 -0
tts_gui.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Silero TTS v5 GUI — синтез русской речи."""
3
+
4
+ import os
5
+ import sys
6
+ import threading
7
+ import time
8
+ import tkinter as tk
9
+ from tkinter import ttk
10
+
11
+ import soundfile as sf
12
+
13
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
14
+ if SCRIPT_DIR not in sys.path:
15
+ sys.path.insert(0, SCRIPT_DIR)
16
+ from tts import SileroTTS, SPEAKER_ALIASES # noqa: E402
17
+
18
+ # ─── Цвета (в стиле ASR GUI) ──────────────────────────────────────────
19
+
20
+ BG = '#1a1a2e'
21
+ FG = '#e0e0e0'
22
+ TEXT_BG = '#16213e'
23
+ BTN_BG = '#0f3460'
24
+ BTN_STOP = '#e94560'
25
+ BTN_CLR = '#555'
26
+ ACCENT = '#00d2ff'
27
+
28
+ # ─── TTS ───────────────────────────────────────────────────────────────
29
+
30
+ _tts = None
31
+
32
+
33
+ def get_tts():
34
+ global _tts
35
+ if _tts is None:
36
+ _tts = SileroTTS(use_accentor=True)
37
+ return _tts
38
+
39
+
40
+ # ─── Приложение ────────────────────────────────────────────────────────
41
+
42
+
43
+ class TTSApp:
44
+ def __init__(self):
45
+ self.root = tk.Tk()
46
+ self.root.title('Silero TTS v5 — Русский синтез речи')
47
+ self.root.geometry('800x650')
48
+ self.root.configure(bg=BG)
49
+
50
+ self._speaker_var = tk.StringVar(value='ru_eduard')
51
+ self._status_var = tk.StringVar(value='Загрузка...')
52
+
53
+ self._build_ui()
54
+ self._bind_events()
55
+
56
+ # Загрузка модели в фоне
57
+ self.root.after(100, self._load_model)
58
+
59
+ def _build_ui(self):
60
+ # ── Верхняя панель: заголовок ──
61
+ header = tk.Label(
62
+ self.root, text='🎙️ Silero TTS v5 — Синтез русской речи',
63
+ font=('Arial', 16, 'bold'), bg=BG, fg=ACCENT,
64
+ )
65
+ header.pack(fill=tk.X, padx=15, pady=(15, 5))
66
+
67
+ # ── Текстовое поле ввода ──
68
+ self.text = tk.Text(
69
+ self.root, font=('Arial', 16), bg=TEXT_BG, fg=FG,
70
+ relief=tk.FLAT, border=0, height=6, wrap=tk.WORD,
71
+ insertbackground=FG,
72
+ )
73
+ self.text.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
74
+ self.text.insert('1.0', 'Введите текст для озвучивания...')
75
+ self.text.bind('<FocusIn>', self._on_focus_in)
76
+
77
+ # ── Статус ──
78
+ self.status = tk.Label(
79
+ self.root, textvariable=self._status_var,
80
+ font=('Arial', 12), bg=BG, fg='#aaa',
81
+ )
82
+ self.status.pack(fill=tk.X, padx=15)
83
+
84
+ # ── Панель управления ──
85
+ ctrl = tk.Frame(self.root, bg=BG)
86
+ ctrl.pack(fill=tk.X, padx=15, pady=(10, 5))
87
+
88
+ # Голос
89
+ tk.Label(ctrl, text='Голос:', font=('Arial', 13),
90
+ bg=BG, fg=FG).pack(side=tk.LEFT, padx=(0, 5))
91
+ self.speaker_combo = ttk.Combobox(
92
+ ctrl, textvariable=self._speaker_var,
93
+ font=('Arial', 12), state='readonly', width=20,
94
+ )
95
+ self.speaker_combo.pack(side=tk.LEFT, padx=(0, 15))
96
+
97
+ # Кнопки
98
+ self.btn_speak = tk.Button(
99
+ ctrl, text='🔊 СИНТЕЗ', font=('Arial', 18, 'bold'),
100
+ bg=BTN_BG, fg='white', relief=tk.FLAT,
101
+ cursor='hand2', height=1, padx=20,
102
+ command=self._click_speak,
103
+ )
104
+ self.btn_speak.pack(side=tk.LEFT, padx=5)
105
+
106
+ self.btn_save = tk.Button(
107
+ ctrl, text='💾 СОХРАНИТЬ', font=('Arial', 18, 'bold'),
108
+ bg=BTN_BG, fg='white', relief=tk.FLAT,
109
+ cursor='hand2', height=1, padx=20,
110
+ command=self._click_save,
111
+ )
112
+ self.btn_save.pack(side=tk.LEFT, padx=5)
113
+
114
+ self.btn_stop = tk.Button(
115
+ ctrl, text='⏹ СТОП', font=('Arial', 18, 'bold'),
116
+ bg=BTN_STOP, fg='white', relief=tk.FLAT,
117
+ cursor='hand2', state=tk.DISABLED, height=1, padx=20,
118
+ command=self._click_stop,
119
+ )
120
+ self.btn_stop.pack(side=tk.LEFT, padx=5)
121
+
122
+ self.btn_clear = tk.Button(
123
+ ctrl, text='Очистить', font=('Arial', 14),
124
+ bg=BTN_CLR, fg='white', relief=tk.FLAT,
125
+ cursor='hand2', height=1,
126
+ command=self._click_clear,
127
+ )
128
+ self.btn_clear.pack(side=tk.LEFT, padx=5)
129
+
130
+ # ── Информационная панель внизу ──
131
+ info_frame = tk.Frame(self.root, bg=BG)
132
+ info_frame.pack(fill=tk.X, padx=15, pady=(5, 15))
133
+
134
+ self.info_text = tk.Label(
135
+ info_frame,
136
+ text=('Модель: v5_cis_base | 48 kHz | WAV | '
137
+ 'Ударения: ✓ | RTF ~0.04 CPU'),
138
+ font=('Arial', 10), bg=BG, fg='#888',
139
+ )
140
+ self.info_text.pack()
141
+
142
+ self._saved_audio = None # последний синтезированный аудио
143
+
144
+ def _bind_events(self):
145
+ self.root.protocol('WM_DELETE_WINDOW', self._on_close)
146
+ self.root.bind('<Control-Return>', lambda e: self._click_speak())
147
+ self.root.bind('<Control-s>', lambda e: self._click_save())
148
+
149
+ def _on_focus_in(self, event):
150
+ if self.text.get('1.0', 'end-1c') == 'Введите текст для озвучивания...':
151
+ self.text.delete('1.0', tk.END)
152
+
153
+ def _load_model(self):
154
+ def load():
155
+ try:
156
+ tts = get_tts()
157
+ speakers = tts.speakers
158
+ display_names = []
159
+ for s in speakers:
160
+ aliases = [k for k, v in SPEAKER_ALIASES.items() if v == s]
161
+ label = s
162
+ if aliases:
163
+ label += f' ({", ".join(aliases)})'
164
+ display_names.append(label)
165
+ self.root.after(0, lambda: self._on_model_ready(display_names))
166
+ except Exception as e:
167
+ self.root.after(0, lambda: self._status_var.set(
168
+ f'Ошибка загрузки: {e}'))
169
+
170
+ threading.Thread(target=load, daemon=True).start()
171
+
172
+ def _on_model_ready(self, display_names):
173
+ self.speaker_combo['values'] = display_names
174
+ self.speaker_combo.current(display_names.index(
175
+ next(n for n in display_names if n.startswith('ru_eduard'))
176
+ ))
177
+ self._status_var.set('Готов. Введите текст и нажмите СИНТЕЗ')
178
+ self.btn_speak.config(state=tk.NORMAL)
179
+ self.btn_save.config(state=tk.NORMAL)
180
+
181
+ def _get_speaker(self) -> str:
182
+ selection = self.speaker_combo.get()
183
+ return selection.split()[0] # берём только ru_имя до скобок
184
+
185
+ def _synthesize(self, text: str, speaker: str) -> float:
186
+ """Синтезировать и вернуть длительность аудио."""
187
+ t0 = time.perf_counter()
188
+ self._saved_audio = get_tts().speak(text, speaker=speaker)
189
+ dt = time.perf_counter() - t0
190
+ duration = len(self._saved_audio) / get_tts().sample_rate
191
+ return dt, duration
192
+
193
+ def _click_speak(self):
194
+ text = self.text.get('1.0', 'end-1c').strip()
195
+ if not text or text == 'Введите текст для озвучивания...':
196
+ self._status_var.set('Введите текст для синтеза')
197
+ return
198
+
199
+ speaker = self._get_speaker()
200
+ self._set_busy(True, f'🔊 Синтезирую: {speaker}...')
201
+ self.root.update()
202
+
203
+ def task():
204
+ try:
205
+ dt, duration = self._synthesize(text, speaker)
206
+ # Воспроизвести
207
+ import sounddevice as sd
208
+ sd.play(self._saved_audio, get_tts().sample_rate)
209
+ sd.wait()
210
+ self.root.after(0, lambda: self._on_done(dt, duration, speaker))
211
+ except Exception as e:
212
+ self.root.after(0, lambda: self._on_error(str(e)))
213
+
214
+ threading.Thread(target=task, daemon=True).start()
215
+
216
+ def _click_save(self):
217
+ text = self.text.get('1.0', 'end-1c').strip()
218
+ if not text or text == 'Введите текст для озвучивания...':
219
+ self._status_var.set('Введите текст для синтеза')
220
+ return
221
+
222
+ speaker = self._get_speaker()
223
+ self._set_busy(True, f'💾 Синтезирую: {speaker}...')
224
+ self.root.update()
225
+
226
+ def task():
227
+ try:
228
+ dt, duration = self._synthesize(text, speaker)
229
+ # Сохранить
230
+ fname = f'tts_{speaker}_{int(time.time())}.wav'
231
+ sf.write(fname, self._saved_audio, get_tts().sample_rate)
232
+ fsize = os.path.getsize(fname)
233
+ self.root.after(0, lambda: self._on_saved(
234
+ fname, fsize, dt, duration, speaker))
235
+ except Exception as e:
236
+ self.root.after(0, lambda: self._on_error(str(e)))
237
+
238
+ threading.Thread(target=task, daemon=True).start()
239
+
240
+ def _click_stop(self):
241
+ import sounddevice as sd
242
+ sd.stop()
243
+ self._set_busy(False, '⏹ Воспроизведение остановлено')
244
+
245
+ def _click_clear(self):
246
+ self.text.delete('1.0', tk.END)
247
+ self._status_var.set('Готов')
248
+
249
+ def _set_busy(self, busy: bool, msg: str = ''):
250
+ state = tk.DISABLED if busy else tk.NORMAL
251
+ self.btn_speak.config(state=state)
252
+ self.btn_save.config(state=state)
253
+ self.btn_stop.config(state=tk.NORMAL if busy else tk.DISABLED)
254
+ if msg:
255
+ self._status_var.set(msg)
256
+ self.root.update()
257
+
258
+ def _on_done(self, dt: float, duration: float, speaker: str):
259
+ rtf = dt / duration if duration > 0 else 0
260
+ self._set_busy(False)
261
+ self._status_var.set(
262
+ f'✅ Воспроизведено | {speaker} | {duration:.2f}s | '
263
+ f'{dt:.3f}s | RTF {rtf:.3f} (×{1/rtf:.1f})'
264
+ )
265
+
266
+ def _on_saved(self, fname: str, fsize: int,
267
+ dt: float, duration: float, speaker: str):
268
+ rtf = dt / duration if duration > 0 else 0
269
+ self._set_busy(False)
270
+ status = (
271
+ f'💾 Сохранено: {fname} ({fsize/1024:.0f} KB) | '
272
+ f'{speaker} | {duration:.2f}s | {dt:.3f}s | RTF {rtf:.3f}'
273
+ )
274
+ self._status_var.set(status)
275
+ # Копируем имя файла в буфер обмена
276
+ self.root.clipboard_clear()
277
+ self.root.clipboard_append(fname)
278
+
279
+ def _on_error(self, err: str):
280
+ self._set_busy(False)
281
+ self._status_var.set(f'❌ Ошибка: {err}')
282
+
283
+ def _on_close(self):
284
+ import sounddevice as sd
285
+ sd.stop()
286
+ self.root.destroy()
287
+
288
+ def run(self):
289
+ self.root.mainloop()
290
+
291
+
292
+ # ─── Main ──────────────────────────────────────────────────────────────
293
+
294
+ def main():
295
+ app = TTSApp()
296
+ app.run()
297
+
298
+
299
+ if __name__ == '__main__':
300
+ main()