Cascade-Edge / app.py
VSPAN's picture
Update app.py
90c0176 verified
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()