VSPAN commited on
Commit
b6e79a7
·
verified ·
1 Parent(s): 1c791e8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +172 -136
app.py CHANGED
@@ -1,182 +1,218 @@
1
  import gradio as gr
2
  import edge_tts
3
- import asyncio
4
  import tempfile
5
- import re
6
- import emoji
7
  import os
8
  import uuid
9
- import time
 
 
 
 
 
10
 
11
- # Глобальные переменные для кэширования голосов
12
- VOICES_DATA = []
13
- LANGUAGES = []
14
 
15
- # --- Вспомогательные функции ---
16
 
17
  def clean_text(text):
18
  """
19
- Очищает текст от спецсимволов и эмодзи.
20
  """
21
  if not text:
22
  return ""
 
23
  text = re.sub(r'[*_~><]', '', text)
 
24
  text = emoji.replace_emoji(text, replace='')
 
25
  text = re.sub(r'\s+', ' ', text).strip()
26
  return text
27
 
28
- async def load_voices_async():
29
  """
30
- Асинхронная загрузка списка голосов при старте.
31
  """
32
- global VOICES_DATA, LANGUAGES
33
  try:
34
  voices = await edge_tts.list_voices()
35
- # Сортировка по локали
36
- VOICES_DATA = sorted(voices, key=lambda x: x['Locale'])
37
 
38
- # Сбор уникальных языков
39
  seen_langs = set()
40
- langs_list = []
41
- for v in VOICES_DATA:
42
- locale = v['Locale']
43
- if locale not in seen_langs:
44
- seen_langs.add(locale)
45
- langs_list.append(locale)
46
- LANGUAGES = sorted(langs_list)
47
 
48
- print(f"✅ Загружено {len(VOICES_DATA)} голосов. Библиотека готова.")
 
49
  except Exception as e:
50
- print(f" Ошибка загрузки голосов: {e}")
51
- # Фолбэк, если не удалось загрузить (чтобы программа не упала сразу)
52
- LANGUAGES = ["en-US", "ru-RU"]
53
 
54
- def filter_voices_by_language(language):
55
  """
56
- Фильтр голосов по выбранному языку.
57
  """
58
  if not language:
59
- return gr.Dropdown(choices=[])
60
 
61
- filtered_voices = [
62
- f"{v['ShortName']} ({v['Gender']})"
63
- for v in VOICES_DATA
64
- if v['Locale'] == language
65
- ]
66
 
67
- # Если голосов нет (например, ошибка сети при старте), возвращаем пустой список
68
- if not filtered_voices:
69
- return gr.Dropdown(choices=[], value=None)
70
 
71
- return gr.Dropdown(choices=filtered_voices, value=filtered_voices[0], interactive=True)
72
-
73
- # --- Основная логика генерации (с Retry) ---
74
-
75
- async def generate_speech(text, voice_friendly_name, rate, pitch):
76
  """
77
- Генерация аудио.
 
78
  """
79
  if not text or not text.strip():
80
- raise gr.Warning("⚠️ Введите текст для озвучивания.")
81
 
82
- if not voice_friendly_name:
83
- raise gr.Warning("⚠️ Выберите голос из списка.")
84
-
85
- # Получаем ShortName (например, ru-RU-DmitryNeural)
86
- voice_short_name = voice_friendly_name.split(" (")[0]
87
 
 
 
 
 
88
  clean_input = clean_text(text)
89
  rate_str = f"{rate:+d}%"
90
  pitch_str = f"{pitch:+d}Hz"
91
 
92
- print(f"🔄 Генерация... Голос: {voice_short_name}")
93
-
94
- # Создаем путь к файлу
95
- filename = f"tts_{uuid.uuid4().hex}.mp3"
96
- output_path = os.path.join(tempfile.gettempdir(), filename)
97
-
98
- # Попытка генерации с повтором (на случай сетевых сбоев)
99
- max_retries = 3
100
- for attempt in range(max_retries):
101
- try:
102
- communicate = edge_tts.Communicate(clean_input, voice_short_name, rate=rate_str, pitch=pitch_str)
103
- await communicate.save(output_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- # Проверка, создался ли файл и не пустой ли он
106
- if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
107
- return output_path
108
- else:
109
- raise Exception("Файл создан, но он пуст.")
110
-
111
- except Exception as e:
112
- error_msg = str(e)
113
- print(f"⚠️ Попытка {attempt + 1}/{max_retries} не удалась: {error_msg}")
114
 
115
- # Если это ошибка 403, сразу говорим пользователю об обновлении
116
- if "403" in error_msg:
117
- raise gr.Error("Ошибка 403 (Forbidden). Сервер Microsoft отклонил запрос. "
118
- "Пожалуйста, обновите библиотеку: pip install --upgrade edge-tts")
 
119
 
120
- if attempt < max_retries - 1:
121
- await asyncio.sleep(1) # Ждем секунду перед повтором
122
- else:
123
- raise gr.Error(f"Не удалось сгенерировать аудио после {max_retries} попыток. Ошибка: {error_msg}")
124
-
125
- # --- Интерфейс ---
126
-
127
- def create_demo():
128
- # Предзагрузка голосов
129
- asyncio.run(load_voices_async())
130
-
131
- css = """
132
- .container { max-width: 850px; margin: auto; }
133
- """
134
-
135
- theme = gr.themes.Soft(primary_hue="blue")
136
-
137
- with gr.Blocks(theme=theme, css=css, title="Ultra TTS 2.0") as demo:
138
- gr.Markdown("## 🎧 Edge TTS (High Quality)")
139
-
140
- with gr.Row():
141
- with gr.Column(scale=1):
142
- lang_dropdown = gr.Dropdown(
143
- choices=LANGUAGES,
144
- label="1. Выберите язык",
145
- value="ru-RU" if "ru-RU" in LANGUAGES else (LANGUAGES[0] if LANGUAGES else None)
146
- )
147
-
148
- # Инициализация списка голосов
149
- initial_voices = []
150
- if lang_dropdown.value:
151
- initial_voices = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_DATA if v['Locale'] == lang_dropdown.value]
152
-
153
- voice_dropdown = gr.Dropdown(
154
- choices=initial_voices,
155
- value=initial_voices[0] if initial_voices else None,
156
- label="2. Выберите голос",
157
- interactive=True
158
- )
159
-
160
- with gr.Accordion("Настройки звука", open=False):
161
- rate_slider = gr.Slider(-50, 50, value=0, step=1, label="Скорость (%)")
162
- pitch_slider = gr.Slider(-20, 20, value=0, step=1, label="Тон (Hz)")
163
-
164
- with gr.Column(scale=2):
165
- text_input = gr.Textbox(
166
- label="Текст",
167
- lines=6,
168
- placeholder="Введите текст...",
169
- value="Привет! Это тест качественной озвучки."
170
- )
171
- btn = gr.Button("▶️ Озвучить", variant="primary")
172
- audio_out = gr.Audio(label="Результат", type="filepath")
173
-
174
- # События
175
- lang_dropdown.change(fn=filter_voices_by_language, inputs=[lang_dropdown], outputs=[voice_dropdown])
176
- btn.click(fn=generate_speech, inputs=[text_input, voice_dropdown, rate_slider, pitch_slider], outputs=[audio_out])
177
-
178
- return demo
179
-
180
  if __name__ == "__main__":
181
- app = create_demo()
182
- app.launch(server_name="0.0.0.0", show_error=True)
 
1
  import gradio as gr
2
  import edge_tts
 
3
  import tempfile
 
 
4
  import os
5
  import uuid
6
+ import asyncio
7
+ import re
8
+ import emoji
9
+
10
+ # --- КОНФИГУРАЦИЯ ---
11
+ TEMP_DIR = tempfile.gettempdir()
12
 
13
+ # Глобальные переменные для хранения списка голосов
14
+ VOICES_CACHE = []
15
+ LANGUAGES_CACHE = []
16
 
17
+ # --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
18
 
19
  def clean_text(text):
20
  """
21
+ Очистка текста для предотвращения ошибок парсера Microsoft.
22
  """
23
  if not text:
24
  return ""
25
+ # Убираем символы, которые могут ломать SSML
26
  text = re.sub(r'[*_~><]', '', text)
27
+ # Убираем эмодзи (библиотека emoji 2.6.0)
28
  text = emoji.replace_emoji(text, replace='')
29
+ # Убираем лишние пробелы
30
  text = re.sub(r'\s+', ' ', text).strip()
31
  return text
32
 
33
+ async def get_voices_async():
34
  """
35
+ Асинхронная загрузка голосов и формирование структуры языков.
36
  """
37
+ global VOICES_CACHE, LANGUAGES_CACHE
38
  try:
39
  voices = await edge_tts.list_voices()
40
+ # Сортируем голоса
41
+ VOICES_CACHE = sorted(voices, key=lambda x: x['Locale'])
42
 
43
+ # Формируем список уникальных языков
44
  seen_langs = set()
45
+ LANGUAGES_CACHE = []
46
+ for v in VOICES_CACHE:
47
+ if v['Locale'] not in seen_langs:
48
+ seen_langs.add(v['Locale'])
49
+ LANGUAGES_CACHE.append(v['Locale'])
 
 
50
 
51
+ LANGUAGES_CACHE.sort()
52
+ print(f"✅ Успешно загружено {len(VOICES_CACHE)} голосов.")
53
  except Exception as e:
54
+ print(f"⚠️ Ошибка загрузки голосов: {e}")
55
+ # Фолбэк значения, если интернет недоступен при старте
56
+ LANGUAGES_CACHE = ["en-US", "ru-RU"]
57
 
58
+ def filter_voices(language):
59
  """
60
+ Фильтрация голосов при выборе языка в интерфейсе.
61
  """
62
  if not language:
63
+ return gr.Dropdown(choices=[], value=None)
64
 
65
+ # Формируем читаемый список: "Name (Gender)"
66
+ filtered = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == language]
 
 
 
67
 
68
+ # Возвращаем обновленный дропдаун с первым значением по умолчанию
69
+ return gr.Dropdown(choices=filtered, value=filtered[0] if filtered else None)
 
70
 
71
+ async def generate_speech(text, voice_str, rate, pitch):
 
 
 
 
72
  """
73
+ Главная функция генерации (Async).
74
+ Gradio 4.x отлично работает с async def.
75
  """
76
  if not text or not text.strip():
77
+ raise gr.Warning("Пожалуйста, введит�� текст.")
78
 
79
+ if not voice_str:
80
+ raise gr.Warning("Пожалуйста, выберите голос.")
 
 
 
81
 
82
+ # Извлекаем "ShortName" из строки вида "ru-RU-DmitryNeural (Male)"
83
+ voice_short = voice_str.split(" (")[0]
84
+
85
+ # Подготовка параметров
86
  clean_input = clean_text(text)
87
  rate_str = f"{rate:+d}%"
88
  pitch_str = f"{pitch:+d}Hz"
89
 
90
+ # Уникальное имя файла (решает проблемы с кэшем браузера и доступом)
91
+ filename = f"audio_{uuid.uuid4().hex}.mp3"
92
+ file_path = os.path.join(TEMP_DIR, filename)
93
+
94
+ print(f"🔄 Генерация: {voice_short} | Скорость: {rate_str}")
95
+
96
+ try:
97
+ # Создаем объект Communicate (версия 6.1.12)
98
+ communicate = edge_tts.Communicate(clean_input, voice_short, rate=rate_str, pitch=pitch_str)
99
+
100
+ # Сохраняем файл
101
+ await communicate.save(file_path)
102
+
103
+ # Проверяем результат
104
+ if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
105
+ return file_path
106
+ else:
107
+ raise Exception("Файл создан, но он пуст.")
108
+
109
+ except Exception as e:
110
+ error_message = str(e)
111
+ print(f"❌ Ошибка: {error_message}")
112
+
113
+ if "403" in error_message:
114
+ raise gr.Error(
115
+ "Ошибка 403: Microsoft заблокировал доступ для вашей версии библиотеки. "
116
+ "Это не ошибка кода, это ограничение API для версии 6.1.12."
117
+ )
118
+ else:
119
+ raise gr.Error(f"Ошибка генерации: {error_message}")
120
+
121
+ # --- ИНТЕРФЕЙС GRADIO (BLOCKS) ---
122
+
123
+ # Предзагрузка голосов перед запуском интерфейса
124
+ asyncio.run(get_voices_async())
125
+
126
+ # CSS для красоты
127
+ css = """
128
+ .container { max-width: 900px; margin: auto; }
129
+ footer { display: none !important; }
130
+ """
131
+
132
+ # Тема интерфейса
133
+ theme = gr.themes.Soft(
134
+ primary_hue="blue",
135
+ neutral_hue="slate",
136
+ radius_size="md"
137
+ )
138
+
139
+ with gr.Blocks(theme=theme, css=css, title="Edge TTS Pro") as demo:
140
+
141
+ with gr.Row():
142
+ gr.Markdown(
143
+ """
144
+ # 🎧 Edge TTS Generator
145
+ ### Качественный синтез речи (версия 6.1.12)
146
+ """
147
+ )
148
+
149
+ with gr.Row():
150
+ # Левая колонка: Настройки
151
+ with gr.Column(scale=1, min_width=300):
152
+ gr.Markdown("### ⚙️ Настройки голоса")
153
 
154
+ # Выбор языка
155
+ lang_dropdown = gr.Dropdown(
156
+ choices=LANGUAGES_CACHE,
157
+ value="ru-RU" if "ru-RU" in LANGUAGES_CACHE else None,
158
+ label="1. Язык",
159
+ interactive=True
160
+ )
 
 
161
 
162
+ # Выбор голоса (заполняется динамически)
163
+ # Инициализируем начальным списком для выбранного языка
164
+ initial_voices = []
165
+ if lang_dropdown.value:
166
+ initial_voices = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == lang_dropdown.value]
167
 
168
+ voice_dropdown = gr.Dropdown(
169
+ choices=initial_voices,
170
+ value=initial_voices[0] if initial_voices else None,
171
+ label="2. Голос",
172
+ interactive=True
173
+ )
174
+
175
+ # Слайдеры в аккордеоне для компактности
176
+ with gr.Accordion("Дополнительно", open=True):
177
+ rate_slider = gr.Slider(minimum=-50, maximum=50, value=0, step=1, label="Скорость речи (%)")
178
+ pitch_slider = gr.Slider(minimum=-20, maximum=20, value=0, step=1, label="Тон голоса (Hz)")
179
+
180
+ # Правая колонка: Ввод и Результат
181
+ with gr.Column(scale=2):
182
+ gr.Markdown("### 📝 Текст")
183
+ text_input = gr.Textbox(
184
+ label="Введите текст для озвучивания",
185
+ placeholder="Привет! Как твои дела?",
186
+ lines=6,
187
+ max_lines=20
188
+ )
189
+
190
+ generate_btn = gr.Button("🚀 Создать аудио", variant="primary", size="lg")
191
+
192
+ gr.Markdown("### 🔊 Результат")
193
+ audio_output = gr.Audio(
194
+ label="Сгенерированный файл",
195
+ type="filepath",
196
+ interactive=False,
197
+ autoplay=True
198
+ )
199
+
200
+ # --- ЛОГИКА СОБЫТИЙ ---
201
+
202
+ # 1. При смене языка обновляем список голосов
203
+ lang_dropdown.change(
204
+ fn=filter_voices,
205
+ inputs=[lang_dropdown],
206
+ outputs=[voice_dropdown]
207
+ )
208
+
209
+ # 2. При нажатии кнопки генерируем (Gradio 4.x сам обработает async)
210
+ generate_btn.click(
211
+ fn=generate_speech,
212
+ inputs=[text_input, voice_dropdown, rate_slider, pitch_slider],
213
+ outputs=[audio_output]
214
+ )
215
+
216
+ # Запуск приложения
 
 
 
 
 
 
 
 
 
 
 
217
  if __name__ == "__main__":
218
+ demo.launch()