VSPAN commited on
Commit
ddd6448
·
verified ·
1 Parent(s): b6f87b9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -108
app.py CHANGED
@@ -5,142 +5,178 @@ import tempfile
5
  import os
6
  import uuid
7
  import re
8
- import emoji
 
9
 
10
- # --- НАСТРОЙКИ ---
11
- # Глобальные переменные
12
- VOICES_CACHE = []
13
- LANGUAGES_CACHE = []
14
 
15
- # --- ОЧИСТКА ТЕКСТА ---
16
- def clean_text(text):
17
- if not text: return ""
18
- # Microsoft не любит некоторые спецсимволы, убираем их
19
- text = re.sub(r'[*_~><^]', '', text)
20
- text = emoji.replace_emoji(text, replace='')
21
- text = re.sub(r'\s+', ' ', text).strip()
22
- return text
23
 
24
- # --- ЗАГРУЗКА ГОЛОСОВ ---
25
- async def load_voices_async():
26
- global VOICES_CACHE, LANGUAGES_CACHE
27
- try:
28
- print("⏳ Загрузка голосов...")
29
- voices = await edge_tts.list_voices()
30
- VOICES_CACHE = sorted(voices, key=lambda x: x['Locale'])
31
-
32
- seen = set()
33
- LANGUAGES_CACHE = []
34
- for v in VOICES_CACHE:
35
- if v['Locale'] not in seen:
36
- seen.add(v['Locale'])
37
- LANGUAGES_CACHE.append(v['Locale'])
38
- LANGUAGES_CACHE.sort()
39
- print(f"✅ Успешно загружено {len(VOICES_CACHE)} голосов.")
40
- except Exception as e:
41
- print(f"❌ Ошибка загрузки: {e}")
42
- LANGUAGES_CACHE = ["ru-RU", "en-US"]
43
 
44
- def filter_voices(language):
45
- if not language: return gr.Dropdown(choices=[])
 
 
 
 
 
46
 
47
- filtered = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == language]
 
 
 
 
48
 
49
- # Авто-выбор Светланы
50
- default_voice = filtered[0] if filtered else None
51
- for v in filtered:
52
- if "Svetlana" in v:
53
- default_voice = v
54
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- return gr.Dropdown(choices=filtered, value=default_voice)
57
 
58
- # --- ГЕНЕРАЦИЯ (С ЗАЩИТОЙ ОТ СБОЕВ) ---
59
- async def generate_speech(text, voice_str, rate, pitch):
 
 
60
  if not text.strip():
61
- raise gr.Warning("Введите текст!")
62
- if not voice_str:
63
- raise gr.Warning("Выберите голос!")
64
 
65
- voice_short = voice_str.split(" (")[0]
66
- clean_input = clean_text(text)
67
 
68
- # Форматирование: Microsoft любит "+0Hz", но и��огда "-7Hz" может вызвать сбой.
69
- # Убедимся, что формат строгий.
70
- rate_str = f"{int(rate):+d}%"
71
- pitch_str = f"{int(pitch):+d}Hz"
72
 
73
- filename = f"tts_{uuid.uuid4().hex}.mp3"
74
- output_path = os.path.join(tempfile.gettempdir(), filename)
75
 
76
- print(f"🎙️ Попытка генерации: {voice_short} | {pitch_str} | {rate_str}")
 
77
 
78
- # 3 Попытки на случай разрыва соединения
79
- max_retries = 3
80
- last_error = ""
81
-
82
- for attempt in range(max_retries):
 
 
 
 
 
 
 
 
 
 
 
83
  try:
84
- communicate = edge_tts.Communicate(clean_input, voice_short, rate=rate_str, pitch=pitch_str)
85
- await communicate.save(output_path)
 
86
 
87
- if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
88
- return output_path
89
- else:
90
- raise Exception("Файл создан, но пуст (0 байт)")
91
 
 
 
 
 
 
 
 
 
 
92
  except Exception as e:
93
- last_error = str(e)
94
- print(f"⚠️ Попытка {attempt+1} не удалась: {last_error}")
95
- await asyncio.sleep(1) # Ждем секунду перед повтором
 
96
 
97
- # Если ничего не помогло
98
- if "NoAudioReceived" in last_error:
99
- raise gr.Error("Microsoft сбрасывает соединение. Попробуйте изменить текст или перезагрузить страницу.")
100
- else:
101
- raise gr.Error(f"Ошибка после {max_retries} попыток: {last_error}")
102
-
103
- # --- ЗАПУСК ---
 
 
 
 
 
 
 
104
 
105
- # Грузим голоса
106
- asyncio.run(load_voices_async())
107
 
108
- DEFAULT_LANG = "ru-RU"
109
- START_VOICES = []
110
- # Безопасный поиск стартовых голосов
111
- if VOICES_CACHE:
112
- START_VOICES = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == DEFAULT_LANG]
113
-
114
- DEFAULT_VOICE = None
115
- if START_VOICES:
116
- # Ищем Светлану
117
- DEFAULT_VOICE = next((v for v in START_VOICES if "Svetlana" in v), START_VOICES[0])
118
 
119
- css = "body {background-color: #0b0f19;} .container {max-width: 850px; margin: auto;}"
120
- theme = gr.themes.Soft(primary_hue="purple")
 
 
 
121
 
122
- with gr.Blocks(theme=theme, css=css, title="Fantasy TTS Fixed") as demo:
123
 
124
- gr.Markdown("# 🧙‍♀️ Fantasy TTS (Stable)")
 
125
 
126
  with gr.Row():
 
 
 
 
 
 
 
 
 
 
127
  with gr.Column(scale=1):
128
- gr.Markdown("### Настройки")
129
- lang = gr.Dropdown(choices=LANGUAGES_CACHE, value=DEFAULT_LANG, label="Язык", interactive=True)
130
- voice = gr.Dropdown(choices=START_VOICES, value=DEFAULT_VOICE, label="Голос", interactive=True)
131
 
132
- # Слайдеры: по умолчанию -7 Hz
133
- slider_rate = gr.Slider(-50, 50, value=0, step=1, label="Скорость (%)")
134
- slider_pitch = gr.Slider(-20, 20, value=-7, step=1, label="Тон (Hz) [-7 для Фэнтези]")
135
-
136
- with gr.Column(scale=2):
137
- gr.Markdown("### Текст")
138
- txt = gr.Textbox(lines=8, value="", label="")
139
- btn = gr.Button("🔮 Озвучить", variant="primary")
140
- audio = gr.Audio(label="Аудио")
141
-
142
- lang.change(filter_voices, inputs=lang, outputs=voice)
143
- btn.click(generate_speech, inputs=[txt, voice, slider_rate, slider_pitch], outputs=audio)
144
 
145
  if __name__ == "__main__":
 
146
  demo.queue().launch()
 
5
  import os
6
  import uuid
7
  import re
8
+ import shutil
9
+ from pydub import AudioSegment
10
 
11
+ # --- ПРОВЕРКА ОКРУЖЕНИЯ СЕРВЕРА ---
12
+ # Проверяем, готов ли сервер к работе с аудио
13
+ if not shutil.which("ffmpeg"):
14
+ print("⚠️ ВНИМАНИЕ: На сервере не найден FFmpeg. Склейка будет работать медленнее или с ошибками.")
15
 
16
+ # --- КОНФИГУРАЦИЯ (ФЭНТЕЗИ ПРЕСЕТ) ---
17
+ # Все эти настройки применяются НА СЕРВЕРЕ перед отправкой запроса
18
+ VOICE_CONFIG = {
19
+ "narrator": {"voice": "ru-RU-DmitryNeural", "pitch": "-7Hz", "rate": "-5%"}, # Рассказчик (Эпик)
20
+ "male": {"voice": "ru-RU-DenisNeural", "pitch": "-2Hz", "rate": "+0%"}, # Мужчина
21
+ "female": {"voice": "ru-RU-SvetlanaNeural","pitch": "+5Hz", "rate": "+5%"} # Женщина
22
+ }
 
23
 
24
+ # Папка для временных файлов на сервере
25
+ TEMP_DIR = tempfile.gettempdir()
26
+
27
+ # --- ЛОГИКА СЕРВЕРА (Server-Side Logic) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ def analyze_text_structure(text):
30
+ """
31
+ Анализирует текст, используя CPU сервера.
32
+ Определяет пол по окончаниям глаголов русского языка.
33
+ """
34
+ segments = []
35
+ paragraphs = text.split('\n')
36
 
37
+ # Слова-маркеры (женские)
38
+ female_markers = [
39
+ r"сказала", r"спросила", r"ответила", r"прошептала", r"крикнула",
40
+ r"подумала", r"заметила", r"взглянула", r"обернулась"
41
+ ]
42
 
43
+ for p in paragraphs:
44
+ p = p.strip()
45
+ if not p: continue
46
+
47
+ role = "narrator" # По умолчанию
48
+
49
+ # Если это прямая речь...
50
+ if p.startswith('—') or p.startswith('-') or '"' in p or '«' in p:
51
+ p_lower = p.lower()
52
+ # ...сервер ищет маркеры
53
+ is_female = any(re.search(m, p_lower) for m in female_markers)
54
+
55
+ if is_female:
56
+ role = "female"
57
+ else:
58
+ # Если маркеров нет, но это диалог -> считаем мужчиной (стандарт для фэнтези)
59
+ role = "male"
60
+
61
+ segments.append({"text": p, "role": role})
62
 
63
+ return segments
64
 
65
+ async def generate_server_side(text, progress=gr.Progress()):
66
+ """
67
+ Основная функция. Работает полностью в памяти сервера.
68
+ """
69
  if not text.strip():
70
+ raise gr.Warning("Сервер не получил текст.")
 
 
71
 
72
+ print(f"⚙️ [Server] Начало обработки. RAM занята процессом...")
 
73
 
74
+ # 1. Анализ (CPU)
75
+ segments = analyze_text_structure(text)
 
 
76
 
77
+ # 2. Создаем пустой аудио-контейнер в памяти (RAM)
78
+ full_audio = AudioSegment.empty()
79
 
80
+ # Временный список файлов для очистки
81
+ temp_files = []
82
 
83
+ # 3. Цикл генерации
84
+ for item in progress.tqdm(segments, desc="Сервер генерирует аудио..."):
85
+ role = item["role"]
86
+ content = item["text"]
87
+
88
+ # Берем настройки
89
+ conf = VOICE_CONFIG.get(role, VOICE_CONFIG["narrator"])
90
+
91
+ # Путь к куску на диске сервера
92
+ segment_filename = f"server_seg_{uuid.uuid4().hex}.mp3"
93
+ segment_path = os.path.join(TEMP_DIR, segment_filename)
94
+
95
+ # Форматируем параметры
96
+ rate_str = conf["rate"]
97
+ pitch_str = conf["pitch"]
98
+
99
  try:
100
+ # Запрос от Сервера к Microsoft (Клиент тут не участвует)
101
+ comm = edge_tts.Communicate(content, conf["voice"], rate=rate_str, pitch=pitch_str)
102
+ await comm.save(segment_path)
103
 
104
+ if os.path.exists(segment_path):
105
+ temp_files.append(segment_path)
 
 
106
 
107
+ # Загружаем кусок в RAM
108
+ seg_audio = AudioSegment.from_mp3(segment_path)
109
+
110
+ # Склейка в RAM (Crossfade 50ms для плавности)
111
+ if len(full_audio) > 0:
112
+ full_audio = full_audio.append(seg_audio, crossfade=50)
113
+ else:
114
+ full_audio = seg_audio
115
+
116
  except Exception as e:
117
+ print(f"⚠️ [Server Error] Сбой на фразе '{content[:20]}': {e}")
118
+ # Если сбой, пробуем паузу и идем дальше
119
+ await asyncio.sleep(0.5)
120
+ continue
121
 
122
+ # 4. Сохранение итогового файла на диск сервера
123
+ output_filename = f"FANTASY_AUDIO_{uuid.uuid4().hex}.mp3"
124
+ output_path = os.path.join(TEMP_DIR, output_filename)
125
+
126
+ print(f"💾 [Server] Сохранение результата: {output_path}")
127
+ full_audio.export(output_path, format="mp3")
128
+
129
+ # 5. Очистка мусора с диска сервера
130
+ for f in temp_files:
131
+ try: os.remove(f)
132
+ except: pass
133
+
134
+ # Возвращаем путь. Gradio сам передаст файл клиенту.
135
+ return output_path
136
 
137
+ # --- ИНТЕРФЕЙС ---
 
138
 
139
+ css = """
140
+ body { background-color: #111827; color: #f3f4f6; }
141
+ .container { max-width: 900px; margin: auto; }
142
+ """
 
 
 
 
 
 
143
 
144
+ theme = gr.themes.Soft(
145
+ primary_hue="indigo",
146
+ secondary_hue="slate",
147
+ neutral_hue="slate"
148
+ )
149
 
150
+ with gr.Blocks(theme=theme, css=css, title="Server-Side TTS Engine") as demo:
151
 
152
+ gr.Markdown("# 🖥️ Server-Side Fantasy Engine")
153
+ gr.Markdown("Вся обработка (анализ текста, генерация, склейка) выполняется на мощностях сервера.")
154
 
155
  with gr.Row():
156
+ with gr.Column(scale=2):
157
+ text_input = gr.Textbox(
158
+ label="Текст Книги",
159
+ lines=12,
160
+ placeholder="Введите текст. Сервер сам определит роли (Мужчина/Женщина) по контексту.",
161
+ value="— Тише! — прошептала ведьма, глядя в темноту.\nРыцарь обнажил меч и ответил:\n— Я ничего не боюсь."
162
+ )
163
+
164
+ generate_btn = gr.Button("⚡ Запустить обработку на сервере", variant="primary", size="lg")
165
+
166
  with gr.Column(scale=1):
167
+ gr.Markdown("### 📊 Монитор")
168
+ gr.Markdown("Используются ресурсы хостинга:")
169
+ gr.Code(value="CPU: Active (Parsing)\nRAM: Active (Audio Stitching)", language="yaml", label="Статус сервера")
170
 
171
+ audio_output = gr.Audio(label="Готовый файл с сервера", type="filepath")
172
+
173
+ # Привязка события
174
+ generate_btn.click(
175
+ fn=generate_server_side,
176
+ inputs=[text_input],
177
+ outputs=[audio_output]
178
+ )
 
 
 
 
179
 
180
  if __name__ == "__main__":
181
+ # Запуск сервера
182
  demo.queue().launch()