VSPAN commited on
Commit
a322c69
·
verified ·
1 Parent(s): c1a9333

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -83
app.py CHANGED
@@ -7,24 +7,25 @@ import uuid
7
  import re
8
  import emoji
9
 
10
- # --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ---
 
11
  VOICES_CACHE = []
12
  LANGUAGES_CACHE = []
13
 
14
- # --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
15
-
16
  def clean_text(text):
17
- """Очистка текста от спецсимволов и эмодзи."""
18
  if not text: return ""
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
  async def load_voices_async():
25
- """Загрузка списка голосов при старте."""
26
  global VOICES_CACHE, LANGUAGES_CACHE
27
  try:
 
28
  voices = await edge_tts.list_voices()
29
  VOICES_CACHE = sorted(voices, key=lambda x: x['Locale'])
30
 
@@ -35,18 +36,17 @@ async def load_voices_async():
35
  seen.add(v['Locale'])
36
  LANGUAGES_CACHE.append(v['Locale'])
37
  LANGUAGES_CACHE.sort()
38
- print(f"✅ Загружено {len(VOICES_CACHE)} голосов.")
39
  except Exception as e:
40
- print(f"❌ Ошибка загрузки голосов: {e}")
41
  LANGUAGES_CACHE = ["ru-RU", "en-US"]
42
 
43
  def filter_voices(language):
44
- """Фильтр голосов при смене языка."""
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:
@@ -55,107 +55,92 @@ def filter_voices(language):
55
 
56
  return gr.Dropdown(choices=filtered, value=default_voice)
57
 
 
58
  async def generate_speech(text, voice_str, rate, pitch):
59
- """Генерация аудио."""
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
- # Формируем параметры (добавляем + или -, как требует API)
69
- rate_str = f"{rate:+d}%"
70
- pitch_str = f"{pitch:+d}Hz"
 
71
 
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: {pitch_str}")
77
 
78
- try:
79
- communicate = edge_tts.Communicate(clean_input, voice_short, rate=rate_str, pitch=pitch_str)
80
- await communicate.save(output_path)
81
-
82
- if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
83
- return output_path
84
- else:
85
- raise Exception("Файл пустой")
86
 
87
- except Exception as e:
88
- error_msg = str(e)
89
- if "403" in error_msg:
90
- raise gr.Error("Ошибка 403. Попробуйте обновить страницу или перезапустить Space.")
91
- raise gr.Error(f"Ошибка: {error_msg}")
 
 
 
 
 
 
 
 
 
 
92
 
93
- # --- ЗАПУСК И ИНТЕРФЕЙС ---
94
 
95
- # Предзагрузка голосов
96
  asyncio.run(load_voices_async())
97
 
98
- # Настраиваем дефолтные значения
99
  DEFAULT_LANG = "ru-RU"
100
- # Ищем Светлану для старта
101
- START_VOICES = [f"{v['ShortName']} ({v['Gender']})" for v in VOICES_CACHE if v['Locale'] == DEFAULT_LANG]
102
- DEFAULT_VOICE = next((v for v in START_VOICES if "Svetlana" in v), START_VOICES[0] if START_VOICES else None)
103
-
104
- css = """
105
- body { background-color: #0b0f19; }
106
- .container { max-width: 850px; margin: auto; }
107
- """
 
108
 
109
- theme = gr.themes.Soft(
110
- primary_hue="purple",
111
- secondary_hue="indigo"
112
- )
113
 
114
- with gr.Blocks(theme=theme, css=css, title="TTS Classic") as demo:
115
 
116
- gr.Markdown("# 🎧 Edge TTS: Classic")
117
 
118
  with gr.Row():
119
- # Левая колонка: Настройки
120
  with gr.Column(scale=1):
121
- gr.Markdown("### ⚙️ Настройки")
122
-
123
- lang_dropdown = gr.Dropdown(
124
- choices=LANGUAGES_CACHE,
125
- value=DEFAULT_LANG,
126
- label="1. Язык",
127
- interactive=True
128
- )
129
 
130
- voice_dropdown = gr.Dropdown(
131
- choices=START_VOICES,
132
- value=DEFAULT_VOICE,
133
- label="2. Голос",
134
- interactive=True
135
- )
136
-
137
- gr.Markdown("---")
138
-
139
- # Слайдеры (Тон по умолчанию -7, как ты просил)
140
- rate_slider = gr.Slider(minimum=-50, maximum=50, value=0, step=1, label="Скорость (%)")
141
- pitch_slider = gr.Slider(minimum=-20, maximum=20, value=-7, step=1, label="Тон (Hz) [Дефолт: -7]")
142
 
143
- # Правая колонка: Ввод текста
144
  with gr.Column(scale=2):
145
- gr.Markdown("### 📝 Текст")
146
- text_input = gr.Textbox(
147
- label="",
148
- placeholder="Введите текст здесь...",
149
- lines=8,
150
- value="Привет! Я готова озвучить твою историю с мистическим оттенком."
151
- )
152
-
153
- btn = gr.Button("🔊 Озвучить", variant="primary", size="lg")
154
- audio_output = gr.Audio(label="Результат", type="filepath")
155
 
156
- # Логика
157
- lang_dropdown.change(filter_voices, inputs=lang_dropdown, outputs=voice_dropdown)
158
- btn.click(generate_speech, inputs=[text_input, voice_dropdown, rate_slider, pitch_slider], outputs=audio_output)
159
 
160
  if __name__ == "__main__":
161
  demo.queue().launch()
 
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
 
 
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:
 
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()