silero-v5-tts-ru / tts_gui.py
Dimitrius174's picture
Upload tts_gui.py with huggingface_hub
e3ad3e1 verified
Raw
History Blame Contribute Delete
11.3 kB
#!/usr/bin/env python3
"""Silero TTS v5 GUI — синтез русской речи."""
import os
import sys
import threading
import time
import tkinter as tk
from tkinter import ttk
import soundfile as sf
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from tts import SileroTTS, SPEAKER_ALIASES # noqa: E402
# ─── Цвета (в стиле ASR GUI) ──────────────────────────────────────────
BG = '#1a1a2e'
FG = '#e0e0e0'
TEXT_BG = '#16213e'
BTN_BG = '#0f3460'
BTN_STOP = '#e94560'
BTN_CLR = '#555'
ACCENT = '#00d2ff'
# ─── TTS ───────────────────────────────────────────────────────────────
_tts = None
def get_tts():
global _tts
if _tts is None:
_tts = SileroTTS(use_accentor=True)
return _tts
# ─── Приложение ────────────────────────────────────────────────────────
class TTSApp:
def __init__(self):
self.root = tk.Tk()
self.root.title('Silero TTS v5 — Русский синтез речи')
self.root.geometry('800x650')
self.root.configure(bg=BG)
self._speaker_var = tk.StringVar(value='ru_eduard')
self._status_var = tk.StringVar(value='Загрузка...')
self._build_ui()
self._bind_events()
# Загрузка модели в фоне
self.root.after(100, self._load_model)
def _build_ui(self):
# ── Верхняя панель: заголовок ──
header = tk.Label(
self.root, text='🎙️ Silero TTS v5 — Синтез русской речи',
font=('Arial', 16, 'bold'), bg=BG, fg=ACCENT,
)
header.pack(fill=tk.X, padx=15, pady=(15, 5))
# ── Текстовое поле ввода ──
self.text = tk.Text(
self.root, font=('Arial', 16), bg=TEXT_BG, fg=FG,
relief=tk.FLAT, border=0, height=6, wrap=tk.WORD,
insertbackground=FG,
)
self.text.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
self.text.insert('1.0', 'Введите текст для озвучивания...')
self.text.bind('<FocusIn>', self._on_focus_in)
# ── Статус ──
self.status = tk.Label(
self.root, textvariable=self._status_var,
font=('Arial', 12), bg=BG, fg='#aaa',
)
self.status.pack(fill=tk.X, padx=15)
# ── Панель управления ──
ctrl = tk.Frame(self.root, bg=BG)
ctrl.pack(fill=tk.X, padx=15, pady=(10, 5))
# Голос
tk.Label(ctrl, text='Голос:', font=('Arial', 13),
bg=BG, fg=FG).pack(side=tk.LEFT, padx=(0, 5))
self.speaker_combo = ttk.Combobox(
ctrl, textvariable=self._speaker_var,
font=('Arial', 12), state='readonly', width=20,
)
self.speaker_combo.pack(side=tk.LEFT, padx=(0, 15))
# Кнопки
self.btn_speak = tk.Button(
ctrl, text='🔊 СИНТЕЗ', font=('Arial', 18, 'bold'),
bg=BTN_BG, fg='white', relief=tk.FLAT,
cursor='hand2', height=1, padx=20,
command=self._click_speak,
)
self.btn_speak.pack(side=tk.LEFT, padx=5)
self.btn_save = tk.Button(
ctrl, text='💾 СОХРАНИТЬ', font=('Arial', 18, 'bold'),
bg=BTN_BG, fg='white', relief=tk.FLAT,
cursor='hand2', height=1, padx=20,
command=self._click_save,
)
self.btn_save.pack(side=tk.LEFT, padx=5)
self.btn_stop = tk.Button(
ctrl, text='⏹ СТОП', font=('Arial', 18, 'bold'),
bg=BTN_STOP, fg='white', relief=tk.FLAT,
cursor='hand2', state=tk.DISABLED, height=1, padx=20,
command=self._click_stop,
)
self.btn_stop.pack(side=tk.LEFT, padx=5)
self.btn_clear = tk.Button(
ctrl, text='Очистить', font=('Arial', 14),
bg=BTN_CLR, fg='white', relief=tk.FLAT,
cursor='hand2', height=1,
command=self._click_clear,
)
self.btn_clear.pack(side=tk.LEFT, padx=5)
# ── Информационная панель внизу ──
info_frame = tk.Frame(self.root, bg=BG)
info_frame.pack(fill=tk.X, padx=15, pady=(5, 15))
self.info_text = tk.Label(
info_frame,
text=('Модель: v5_cis_base | 48 kHz | WAV | '
'Ударения: ✓ | RTF ~0.04 CPU'),
font=('Arial', 10), bg=BG, fg='#888',
)
self.info_text.pack()
self._saved_audio = None # последний синтезированный аудио
def _bind_events(self):
self.root.protocol('WM_DELETE_WINDOW', self._on_close)
self.root.bind('<Control-Return>', lambda e: self._click_speak())
self.root.bind('<Control-s>', lambda e: self._click_save())
def _on_focus_in(self, event):
if self.text.get('1.0', 'end-1c') == 'Введите текст для озвучивания...':
self.text.delete('1.0', tk.END)
def _load_model(self):
def load():
try:
tts = get_tts()
speakers = tts.speakers
display_names = []
for s in speakers:
aliases = [k for k, v in SPEAKER_ALIASES.items() if v == s]
label = s
if aliases:
label += f' ({", ".join(aliases)})'
display_names.append(label)
self.root.after(0, lambda: self._on_model_ready(display_names))
except Exception as e:
self.root.after(0, lambda: self._status_var.set(
f'Ошибка загрузки: {e}'))
threading.Thread(target=load, daemon=True).start()
def _on_model_ready(self, display_names):
self.speaker_combo['values'] = display_names
self.speaker_combo.current(display_names.index(
next(n for n in display_names if n.startswith('ru_eduard'))
))
self._status_var.set('Готов. Введите текст и нажмите СИНТЕЗ')
self.btn_speak.config(state=tk.NORMAL)
self.btn_save.config(state=tk.NORMAL)
def _get_speaker(self) -> str:
selection = self.speaker_combo.get()
return selection.split()[0] # берём только ru_имя до скобок
def _synthesize(self, text: str, speaker: str) -> float:
"""Синтезировать и вернуть длительность аудио."""
t0 = time.perf_counter()
self._saved_audio = get_tts().speak(text, speaker=speaker)
dt = time.perf_counter() - t0
duration = len(self._saved_audio) / get_tts().sample_rate
return dt, duration
def _click_speak(self):
text = self.text.get('1.0', 'end-1c').strip()
if not text or text == 'Введите текст для озвучивания...':
self._status_var.set('Введите текст для синтеза')
return
speaker = self._get_speaker()
self._set_busy(True, f'🔊 Синтезирую: {speaker}...')
self.root.update()
def task():
try:
dt, duration = self._synthesize(text, speaker)
# Воспроизвести
import sounddevice as sd
sd.play(self._saved_audio, get_tts().sample_rate)
sd.wait()
self.root.after(0, lambda: self._on_done(dt, duration, speaker))
except Exception as e:
self.root.after(0, lambda: self._on_error(str(e)))
threading.Thread(target=task, daemon=True).start()
def _click_save(self):
text = self.text.get('1.0', 'end-1c').strip()
if not text or text == 'Введите текст для озвучивания...':
self._status_var.set('Введите текст для синтеза')
return
speaker = self._get_speaker()
self._set_busy(True, f'💾 Синтезирую: {speaker}...')
self.root.update()
def task():
try:
dt, duration = self._synthesize(text, speaker)
# Сохранить
fname = f'tts_{speaker}_{int(time.time())}.wav'
sf.write(fname, self._saved_audio, get_tts().sample_rate)
fsize = os.path.getsize(fname)
self.root.after(0, lambda: self._on_saved(
fname, fsize, dt, duration, speaker))
except Exception as e:
self.root.after(0, lambda: self._on_error(str(e)))
threading.Thread(target=task, daemon=True).start()
def _click_stop(self):
import sounddevice as sd
sd.stop()
self._set_busy(False, '⏹ Воспроизведение остановлено')
def _click_clear(self):
self.text.delete('1.0', tk.END)
self._status_var.set('Готов')
def _set_busy(self, busy: bool, msg: str = ''):
state = tk.DISABLED if busy else tk.NORMAL
self.btn_speak.config(state=state)
self.btn_save.config(state=state)
self.btn_stop.config(state=tk.NORMAL if busy else tk.DISABLED)
if msg:
self._status_var.set(msg)
self.root.update()
def _on_done(self, dt: float, duration: float, speaker: str):
rtf = dt / duration if duration > 0 else 0
self._set_busy(False)
self._status_var.set(
f'✅ Воспроизведено | {speaker} | {duration:.2f}s | '
f'{dt:.3f}s | RTF {rtf:.3f}{1/rtf:.1f})'
)
def _on_saved(self, fname: str, fsize: int,
dt: float, duration: float, speaker: str):
rtf = dt / duration if duration > 0 else 0
self._set_busy(False)
status = (
f'💾 Сохранено: {fname} ({fsize/1024:.0f} KB) | '
f'{speaker} | {duration:.2f}s | {dt:.3f}s | RTF {rtf:.3f}'
)
self._status_var.set(status)
# Копируем имя файла в буфер обмена
self.root.clipboard_clear()
self.root.clipboard_append(fname)
def _on_error(self, err: str):
self._set_busy(False)
self._status_var.set(f'❌ Ошибка: {err}')
def _on_close(self):
import sounddevice as sd
sd.stop()
self.root.destroy()
def run(self):
self.root.mainloop()
# ─── Main ──────────────────────────────────────────────────────────────
def main():
app = TTSApp()
app.run()
if __name__ == '__main__':
main()
Free AI Image Generator No sign-up. Instant results. Open Now