VSPAN commited on
Commit
e40a9fa
·
verified ·
1 Parent(s): 743c0e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -169
app.py CHANGED
@@ -11,31 +11,30 @@ from pydub import AudioSegment
11
 
12
  # --- ПРОВЕРКА СЕРВЕРА ---
13
  if not shutil.which("ffmpeg"):
14
- print("⚠️ FFmpeg не найден! Склейка может не работать.")
15
 
16
  # --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ---
17
  VOICES_CACHE = []
18
  LANGUAGES_CACHE = []
19
  TEMP_DIR = tempfile.gettempdir()
20
 
21
- # --- НАСТРОЙКИ ФЭНТЕЗИ (Для Авто-режима) ---
22
- FANTASY_PRESETS = {
23
- "narrator": {"voice": "ru-RU-DmitryNeural", "pitch": "-7Hz", "rate": "-5%"},
24
- "male": {"voice": "ru-RU-DenisNeural", "pitch": "-2Hz", "rate": "+0%"},
25
- "female": {"voice": "ru-RU-SvetlanaNeural","pitch": "+5Hz", "rate": "+5%"}
26
- }
27
-
28
- # --- ФУНКЦИИ ПОДГОТОВКИ ---
29
-
30
- def clean_text(text):
31
  if not text: return ""
 
32
  text = re.sub(r'[*_~><^]', '', text)
 
33
  text = emoji.replace_emoji(text, replace='')
 
34
  text = re.sub(r'\s+', ' ', text).strip()
35
  return text
36
 
 
37
  async def load_voices_init():
38
- """Загружаем голоса один раз при старте сервера"""
39
  global VOICES_CACHE, LANGUAGES_CACHE
40
  try:
41
  voices = await edge_tts.list_voices()
@@ -48,192 +47,137 @@ async def load_voices_init():
48
  seen.add(v['Locale'])
49
  LANGUAGES_CACHE.append(v['Locale'])
50
  LANGUAGES_CACHE.sort()
51
- print(f"✅ [Server] Голоса загружены: {len(VOICES_CACHE)}")
52
  except Exception as e:
53
- print(f"❌ Ошибка загрузки голосов: {e}")
54
  LANGUAGES_CACHE = ["ru-RU", "en-US"]
55
 
56
- # --- ФИЛЬТРЫ UI ---
57
- def filter_voices_ui(language):
58
  if not language: return gr.Dropdown(choices=[])
 
59
  filtered = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == language]
60
- # Пытаемся найти Дмитрия или Светлану по дефолту
61
- def_val = filtered[0] if filtered else None
62
- for v in filtered:
63
- if "Dmitry" in v: def_val = v; break
64
- return gr.Dropdown(choices=filtered, value=def_val)
65
-
66
- # --- ДВИЖОК ГЕНЕРАЦИИ (SERVER SIDE) ---
67
-
68
- async def generate_segment_internal(text, voice, rate, pitch):
69
- """Генерирует один кусок аудио во временную папку сервера"""
70
- if not text.strip(): return None
71
 
72
- fname = f"seg_{uuid.uuid4().hex}.mp3"
73
- fpath = os.path.join(TEMP_DIR, fname)
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Убеждаемся в формате параметров
76
- rate_str = rate if isinstance(rate, str) else f"{int(rate):+d}%"
77
- pitch_str = pitch if isinstance(pitch, str) else f"{int(pitch):+d}Hz"
78
 
79
- try:
80
- comm = edge_tts.Communicate(text, voice, rate=rate_str, pitch=pitch_str)
81
- await comm.save(fpath)
82
- if os.path.exists(fpath) and os.path.getsize(fpath) > 0:
83
- return fpath
84
- except Exception as e:
85
- print(f"⚠️ Ошибка фрагмента: {e}")
86
- return None
87
-
88
- # === РЕЖИМ 1: ФЭНТЕЗИ АВТО ===
89
-
90
- def analyze_text_roles(text):
91
- """Парсит текст и ищет женщин/мужчин по глаголам"""
92
- segments = []
93
- # Маркеры женского рода
94
- female_markers = [r"сказала", r"спросила", r"ответила", r"прошептала", r"крикнула", r"подумала"]
95
 
96
- paragraphs = text.split('\n')
97
- for p in paragraphs:
98
- p = p.strip()
99
- if not p: continue
100
-
101
- role = "narrator"
102
- # Если диалог
103
- if p.startswith('—') or p.startswith('-') or '"' in p:
104
- p_low = p.lower()
105
- if any(re.search(m, p_low) for m in female_markers):
106
- role = "female"
107
- else:
108
- role = "male" # Дефолт для диалога
109
-
110
- segments.append({"text": p, "role": role})
111
- return segments
112
-
113
- async def process_fantasy_mode(text):
114
- if not text.strip(): raise gr.Warning("Текст пуст!")
115
 
116
- print("⚔️ [Fantasy Mode] Анализ текста...")
117
- segments = analyze_text_roles(text)
118
 
119
- full_audio = AudioSegment.empty()
120
- temp_files = []
121
- progress = gr.Progress()
122
 
123
- for item in progress.tqdm(segments, desc="Ковка аудио..."):
124
- # Берем настройки из пресетов
125
- conf = FANTASY_PRESETS[item['role']]
126
-
127
- path = await generate_segment_internal(item['text'], conf['voice'], conf['rate'], conf['pitch'])
128
 
129
- if path:
130
- temp_files.append(path)
131
- seg = AudioSegment.from_mp3(path)
132
- # Склейка с нахлестом (Crossfade)
133
- if len(full_audio) > 0:
134
- full_audio = full_audio.append(seg, crossfade=50)
135
- else:
136
- full_audio = seg
137
- # Пауза для API
138
- await asyncio.sleep(0.1)
139
 
140
- out_path = os.path.join(TEMP_DIR, f"fantasy_{uuid.uuid4().hex}.mp3")
141
- full_audio.export(out_path, format="mp3")
142
-
143
- # Уборка
144
- for f in temp_files:
145
- try: os.remove(f)
146
- except: pass
147
-
148
- return out_path, segments
149
-
150
- # === РЕЖИМ 2: РУЧНОЙ КОНТРОЛЬ ===
151
-
152
- async def process_manual_mode(text, voice_raw, rate, pitch):
153
- if not text.strip(): raise gr.Warning("Текст пуст!")
154
- if not voice_raw: raise gr.Warning("Выберите голос!")
155
-
156
- print("🛠️ [Manual Mode] Генерация...")
157
- voice = voice_raw.split(" (")[0]
158
-
159
- # Здесь мы тоже используем pydub и Server-Side сохранение,
160
- # чтобы браузер пользователя не напрягался.
161
-
162
- out_path = await generate_segment_internal(text, voice, rate, pitch)
163
-
164
- if not out_path:
165
- raise gr.Error("Ошибка генерации. Попробуйте другой голос или текст.")
166
-
167
- return out_path
168
 
169
- # --- ИНТЕРФЕЙС ---
170
 
171
- # Запуск загрузки голосов
172
  asyncio.run(load_voices_init())
173
 
174
- # Дефолты
175
- DEF_LANG = "ru-RU"
176
- DEF_VOICES = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == DEF_LANG]
177
- DEF_VAL = next((v for v in DEF_VOICES if "Dmitry" in v), DEF_VOICES[0] if DEF_VOICES else None)
 
 
178
 
 
179
  css = """
180
  body {background-color: #111827; color: #e5e7eb;}
181
- .container {max-width: 950px; margin: auto;}
182
- h1 {color: #fbbf24; font-family: serif; text-align: center; font-size: 2.5em;}
183
  """
184
 
185
  theme = gr.themes.Soft(primary_hue="amber", secondary_hue="slate")
186
 
187
- with gr.Blocks(theme=theme, css=css, title="Final TTS Studio") as demo:
188
 
189
- gr.Markdown("# 🍺 Fantasy TTS: Final Cut")
190
 
191
- with gr.Tabs():
192
-
193
- # --- Вкладка 1: Фэнтези Авто ---
194
- with gr.TabItem("🧙‍♂️ Фэнтези Авто"):
195
- with gr.Row():
196
- with gr.Column(scale=2):
197
- f_text = gr.Textbox(
198
- label="Текст Легенды", lines=12,
199
- placeholder="— Кто там? — спросила ведьма.\nРыцарь ответил: — Твоя судьба.",
200
- value='— Стой! — крикнул рыцарь.\nВедьма обернулась и прошептала:\n— Тебе не пройти.'
201
- )
202
- f_btn = gr.Button("✨ Сотворить Магию (Auto)", variant="primary", size="lg")
203
-
204
- with gr.Column(scale=1):
205
- gr.Markdown("### 📜 Пресеты (Вшито)")
206
- gr.Markdown("- **Рассказчик:** Дмитрий (-7Hz, -5%)")
207
- gr.Markdown("- **Мужчины:** Денис (-2Hz)")
208
- gr.Markdown("- **Женщины:** Светлана (+5Hz)")
209
- f_audio = gr.Audio(label="Результат", type="filepath")
210
- f_debug = gr.JSON(label="Разбор ролей")
211
 
212
- f_btn.click(process_fantasy_mode, inputs=f_text, outputs=[f_audio, f_debug])
213
-
214
- # --- Вкладка 2: Полный Ручной Контроль ---
215
- with gr.TabItem("⚙️ Ручной Режим"):
216
- with gr.Row():
217
- with gr.Column(scale=1):
218
- gr.Markdown("### Настройки Голоса")
219
- m_lang = gr.Dropdown(choices=LANGUAGES_CACHE, value=DEF_LANG, label="1. Язык")
220
- m_voice = gr.Dropdown(choices=DEF_VOICES, value=DEF_VAL, label="2. Голос")
221
-
222
- gr.Markdown("---")
223
- m_rate = gr.Slider(-50, 50, value=0, step=1, label="Скорость (%)")
224
- m_pitch = gr.Slider(-20, 20, value=0, step=1, label="Тон (Hz)")
225
-
226
- with gr.Column(scale=2):
227
- m_text = gr.Textbox(
228
- label="Текст", lines=10,
229
- value="Привет! Это ручной режим. Здесь ты сам себе режиссер."
230
- )
231
- m_btn = gr.Button("🔊 Озвучить (Manual)", variant="secondary", size="lg")
232
- m_audio = gr.Audio(label="Результат", type="filepath")
 
 
 
 
 
 
 
 
 
 
233
 
234
- # Привязка событий ручного режима
235
- m_lang.change(filter_voices_ui, inputs=m_lang, outputs=m_voice)
236
- m_btn.click(process_manual_mode, inputs=[m_text, m_voice, m_rate, m_pitch], outputs=m_audio)
237
 
238
  if __name__ == "__main__":
239
  demo.queue().launch()
 
11
 
12
  # --- ПРОВЕРКА СЕРВЕРА ---
13
  if not shutil.which("ffmpeg"):
14
+ print("⚠️ FFmpeg не найден! Убедитесь, что он установлен на хостинге.")
15
 
16
  # --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ---
17
  VOICES_CACHE = []
18
  LANGUAGES_CACHE = []
19
  TEMP_DIR = tempfile.gettempdir()
20
 
21
+ # --- ОЧИСТКА ТЕКСТА ---
22
+ def clean_text_server_side(text):
23
+ """
24
+ Удаляет эмодзи и спецсимволы, чтобы робот их не читал.
25
+ Выполняется на сервере.
26
+ """
 
 
 
 
27
  if not text: return ""
28
+ # Удаляем звездочки, тильды и прочий мусор форматирования
29
  text = re.sub(r'[*_~><^]', '', text)
30
+ # Удаляем эмодзи (превращаем их в пустоту)
31
  text = emoji.replace_emoji(text, replace='')
32
+ # Убираем лишние пробелы
33
  text = re.sub(r'\s+', ' ', text).strip()
34
  return text
35
 
36
+ # --- ЗАГРУЗКА ГОЛОСОВ ---
37
  async def load_voices_init():
 
38
  global VOICES_CACHE, LANGUAGES_CACHE
39
  try:
40
  voices = await edge_tts.list_voices()
 
47
  seen.add(v['Locale'])
48
  LANGUAGES_CACHE.append(v['Locale'])
49
  LANGUAGES_CACHE.sort()
50
+ print(f"✅ Голоса загружены: {len(VOICES_CACHE)}")
51
  except Exception as e:
52
+ print(f"❌ Ошибка: {e}")
53
  LANGUAGES_CACHE = ["ru-RU", "en-US"]
54
 
55
+ # --- ФИЛЬТР ГОЛОСОВ (UI) ---
56
+ def update_voice_list(language):
57
  if not language: return gr.Dropdown(choices=[])
58
+
59
  filtered = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == language]
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ # Ищем Светлану по дефолту
62
+ default_val = filtered[0] if filtered else None
63
+ for v in filtered:
64
+ if "Svetlana" in v:
65
+ default_val = v
66
+ break
67
+
68
+ return gr.Dropdown(choices=filtered, value=default_val)
69
+
70
+ # --- ГЕНЕРАЦИЯ (SERVER ENGINE) ---
71
+ async def generate_server_audio(text, voice_raw, rate, pitch):
72
+ if not text.strip():
73
+ raise gr.Warning("Текст пуст!")
74
+ if not voice_raw:
75
+ raise gr.Warning("Выберите голос!")
76
 
77
+ # Очистка
78
+ clean_txt = clean_text_server_side(text)
79
+ voice = voice_raw.split(" (")[0]
80
 
81
+ # Параметры
82
+ rate_str = f"{int(rate):+d}%"
83
+ pitch_str = f"{int(pitch):+d}Hz"
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ # Пути
86
+ temp_filename = f"raw_{uuid.uuid4().hex}.mp3"
87
+ temp_path = os.path.join(TEMP_DIR, temp_filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ final_filename = f"RESULT_{uuid.uuid4().hex}.mp3"
90
+ final_path = os.path.join(TEMP_DIR, final_filename)
91
 
92
+ print(f"⚙️ [Server] Генерация: {voice} | Тон: {pitch_str}")
 
 
93
 
94
+ try:
95
+ # 1. Скачиваем аудио от Microsoft на диск сервера
96
+ comm = edge_tts.Communicate(clean_txt, voice, rate=rate_str, pitch=pitch_str)
97
+ await comm.save(temp_path)
 
98
 
99
+ # 2. Обрабатываем через Pydub (чтобы задействовать CPU сервера и проверить файл)
100
+ if os.path.exists(temp_path) and os.path.getsize(temp_path) > 0:
101
+ audio = AudioSegment.from_mp3(temp_path)
102
+ audio.export(final_path, format="mp3")
 
 
 
 
 
 
103
 
104
+ # Удаляем черновик
105
+ os.remove(temp_path)
106
+ return final_path
107
+ else:
108
+ raise Exception("Файл не создался (пустой).")
109
+
110
+ except Exception as e:
111
+ # Ловим ошибки 403 и прочие
112
+ if "403" in str(e):
113
+ raise gr.Error("Ошибка доступа (403). Сервер Microsoft временно недоступен.")
114
+ raise gr.Error(f"Ошибка сервера: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ # --- ЗАПУСК ---
117
 
118
+ # Грузим голоса перед стартом
119
  asyncio.run(load_voices_init())
120
 
121
+ # НАСТРОЙКИ ПО УМОЛЧАНИЮ
122
+ DEFAULT_LANG = "ru-RU"
123
+ # Фильтруем список для русского языка
124
+ START_VOICES = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == DEFAULT_LANG]
125
+ # Ставим Светлану
126
+ DEFAULT_VOICE = next((v for v in START_VOICES if "Svetlana" in v), START_VOICES[0] if START_VOICES else None)
127
 
128
+ # Стилизация
129
  css = """
130
  body {background-color: #111827; color: #e5e7eb;}
131
+ .container {max-width: 850px; margin: auto;}
132
+ h1 {color: #fbbf24; text-align: center; font-family: serif;}
133
  """
134
 
135
  theme = gr.themes.Soft(primary_hue="amber", secondary_hue="slate")
136
 
137
+ with gr.Blocks(theme=theme, css=css, title="TTS Server Classic") as demo:
138
 
139
+ gr.Markdown("# 🎙️ TTS Server Classic")
140
 
141
+ with gr.Row():
142
+ # КОЛОНКА НАСТРОЕК
143
+ with gr.Column(scale=1):
144
+ gr.Markdown("### ⚙️ Параметры")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
+ lang_dr = gr.Dropdown(
147
+ choices=LANGUAGES_CACHE,
148
+ value=DEFAULT_LANG,
149
+ label="Язык",
150
+ interactive=True
151
+ )
152
+
153
+ voice_dr = gr.Dropdown(
154
+ choices=START_VOICES,
155
+ value=DEFAULT_VOICE,
156
+ label="Голос",
157
+ interactive=True
158
+ )
159
+
160
+ gr.Markdown("---")
161
+ # Дефолт: -7 Hz, как ты просил
162
+ rate_sl = gr.Slider(-50, 50, value=0, step=1, label="Скорость (%)")
163
+ pitch_sl = gr.Slider(-20, 20, value=-7, step=1, label="Тон (Hz)")
164
+
165
+ # КОЛОНКА ТЕКСТА
166
+ with gr.Column(scale=2):
167
+ gr.Markdown("### 📝 Текст")
168
+ text_in = gr.Textbox(
169
+ label="",
170
+ lines=10,
171
+ placeholder="Введите текст... Эмодзи будут удалены автоматически.",
172
+ value="Привет! Я готова читать твой текст. Эмодзи вроде этих 😊🚀 будут удалены."
173
+ )
174
+
175
+ btn = gr.Button("🔊 Озвучить (Server)", variant="primary", size="lg")
176
+ audio_out = gr.Audio(label="Готовый файл", type="filepath")
177
 
178
+ # Логика интерфейса
179
+ lang_dr.change(update_voice_list, inputs=lang_dr, outputs=voice_dr)
180
+ btn.click(generate_server_audio, inputs=[text_in, voice_dr, rate_sl, pitch_sl], outputs=audio_out)
181
 
182
  if __name__ == "__main__":
183
  demo.queue().launch()