Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import edge_tts | |
| import asyncio | |
| import tempfile | |
| import os | |
| import uuid | |
| import re | |
| import shutil | |
| import emoji | |
| from pydub import AudioSegment | |
| # --- ПРОВЕРКА СЕРВЕРА --- | |
| if not shutil.which("ffmpeg"): | |
| print("⚠️ FFmpeg не найден! Убедитесь, что он установлен на хостинге.") | |
| # --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ --- | |
| VOICES_CACHE = [] | |
| LANGUAGES_CACHE = [] | |
| TEMP_DIR = tempfile.gettempdir() | |
| # --- ОЧИСТКА ТЕКСТА --- | |
| def clean_text_server_side(text): | |
| """ | |
| Удаляет эмодзи и спецсимволы, чтобы робот их не читал. | |
| Выполняется на сервере. | |
| """ | |
| if not text: return "" | |
| # Удаляем звездочки, тильды и прочий мусор форматирования | |
| text = re.sub(r'[*_~><^]', '', text) | |
| # Удаляем эмодзи (превращаем их в пустоту) | |
| text = emoji.replace_emoji(text, replace='') | |
| # Убираем лишние пробелы | |
| text = re.sub(r'\s+', ' ', text).strip() | |
| return text | |
| # --- ЗАГРУЗКА ГОЛОСОВ --- | |
| async def load_voices_init(): | |
| global VOICES_CACHE, LANGUAGES_CACHE | |
| try: | |
| voices = await edge_tts.list_voices() | |
| VOICES_CACHE = sorted(voices, key=lambda x: x['Locale']) | |
| seen = set() | |
| LANGUAGES_CACHE = [] | |
| for v in VOICES_CACHE: | |
| if v['Locale'] not in seen: | |
| seen.add(v['Locale']) | |
| LANGUAGES_CACHE.append(v['Locale']) | |
| LANGUAGES_CACHE.sort() | |
| print(f"✅ Голоса загружены: {len(VOICES_CACHE)}") | |
| except Exception as e: | |
| print(f"❌ Ошибка: {e}") | |
| LANGUAGES_CACHE = ["ru-RU", "en-US"] | |
| # --- ФИЛЬТР ГОЛОСОВ (UI) --- | |
| def update_voice_list(language): | |
| if not language: return gr.Dropdown(choices=[]) | |
| filtered = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == language] | |
| # Ищем Светлану по дефолту | |
| default_val = filtered[0] if filtered else None | |
| for v in filtered: | |
| if "Svetlana" in v: | |
| default_val = v | |
| break | |
| return gr.Dropdown(choices=filtered, value=default_val) | |
| # --- ГЕНЕРАЦИЯ (SERVER ENGINE) --- | |
| async def generate_server_audio(text, voice_raw, rate, pitch): | |
| if not text.strip(): | |
| raise gr.Warning("Текст пуст!") | |
| if not voice_raw: | |
| raise gr.Warning("Выберите голос!") | |
| # Очистка | |
| clean_txt = clean_text_server_side(text) | |
| voice = voice_raw.split(" (")[0] | |
| # Параметры | |
| rate_str = f"{int(rate):+d}%" | |
| pitch_str = f"{int(pitch):+d}Hz" | |
| # Пути | |
| temp_filename = f"raw_{uuid.uuid4().hex}.mp3" | |
| temp_path = os.path.join(TEMP_DIR, temp_filename) | |
| final_filename = f"RESULT_{uuid.uuid4().hex}.mp3" | |
| final_path = os.path.join(TEMP_DIR, final_filename) | |
| print(f"⚙️ [Server] Генерация: {voice} | Тон: {pitch_str}") | |
| try: | |
| # 1. Скачиваем аудио от Microsoft на диск сервера | |
| comm = edge_tts.Communicate(clean_txt, voice, rate=rate_str, pitch=pitch_str) | |
| await comm.save(temp_path) | |
| # 2. Обрабатываем через Pydub (чтобы задействовать CPU сервера и проверить файл) | |
| if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0: | |
| audio = AudioSegment.from_mp3(temp_path) | |
| audio.export(final_path, format="mp3") | |
| # Удаляем черновик | |
| os.remove(temp_path) | |
| return final_path | |
| else: | |
| raise Exception("Файл не создался (пустой).") | |
| except Exception as e: | |
| # Ловим ошибки 403 и прочие | |
| if "403" in str(e): | |
| raise gr.Error("Ошибка доступа (403). Сервер Microsoft временно недоступен.") | |
| raise gr.Error(f"Ошибка сервера: {str(e)}") | |
| # --- ЗАПУСК --- | |
| # Грузим голоса перед стартом | |
| asyncio.run(load_voices_init()) | |
| # НАСТРОЙКИ ПО УМОЛЧАНИЮ | |
| DEFAULT_LANG = "ru-RU" | |
| # Фильтруем список для русского языка | |
| START_VOICES = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == DEFAULT_LANG] | |
| # Ставим Светлану | |
| DEFAULT_VOICE = next((v for v in START_VOICES if "Svetlana" in v), START_VOICES[0] if START_VOICES else None) | |
| # Стилизация | |
| css = """ | |
| body {background-color: #111827; color: #e5e7eb;} | |
| .container {max-width: 850px; margin: auto;} | |
| h1 {color: #fbbf24; text-align: center; font-family: serif;} | |
| """ | |
| theme = gr.themes.Soft(primary_hue="amber", secondary_hue="slate") | |
| with gr.Blocks(theme=theme, css=css, title="TTS Server Classic") as demo: | |
| gr.Markdown("# 🎙️ TTS Server Classic") | |
| with gr.Row(): | |
| # КОЛОНКА НАСТРОЕК | |
| with gr.Column(scale=1): | |
| gr.Markdown("### ⚙️ Параметры") | |
| lang_dr = gr.Dropdown( | |
| choices=LANGUAGES_CACHE, | |
| value=DEFAULT_LANG, | |
| label="Язык", | |
| interactive=True | |
| ) | |
| voice_dr = gr.Dropdown( | |
| choices=START_VOICES, | |
| value=DEFAULT_VOICE, | |
| label="Голос", | |
| interactive=True | |
| ) | |
| gr.Markdown("---") | |
| # Дефолт: -7 Hz, как ты просил | |
| rate_sl = gr.Slider(-50, 50, value=0, step=1, label="Скорость (%)") | |
| pitch_sl = gr.Slider(-20, 20, value=-7, step=1, label="Тон (Hz)") | |
| # КОЛОНКА ТЕКСТА | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 📝 Текст") | |
| text_in = gr.Textbox( | |
| label="", | |
| lines=10, | |
| placeholder="Введите текст...", | |
| value="" | |
| ) | |
| btn = gr.Button("🔊 Озвучить (Server)", variant="primary", size="lg") | |
| audio_out = gr.Audio(label="Готовый файл", type="filepath") | |
| # Логика интерфейса | |
| lang_dr.change(update_voice_list, inputs=lang_dr, outputs=voice_dr) | |
| btn.click(generate_server_audio, inputs=[text_in, voice_dr, rate_sl, pitch_sl], outputs=audio_out) | |
| if __name__ == "__main__": | |
| demo.queue().launch() |