VSPAN commited on
Commit
7e3a62b
·
verified ·
1 Parent(s): ad98b57

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -120
app.py CHANGED
@@ -5,149 +5,157 @@ import tempfile
5
  import os
6
  import uuid
7
  import re
8
- import shutil
9
- from pydub import AudioSegment
10
 
11
- # --- ПРОВЕРКА FFmpeg ---
12
- if not shutil.which("ffmpeg"):
13
- print("⚠️ FFmpeg не найден! Убедитесь, что он установлен на сервере.")
14
 
15
- # --- НАСТРОЙКИ ГОЛОСОВ ---
16
- VOICE_CONFIG = {
17
- "narrator": {"voice": "ru-RU-DmitryNeural", "pitch": "-7Hz", "rate": "-5%"},
18
- "male": {"voice": "ru-RU-DenisNeural", "pitch": "-2Hz", "rate": "+0%"},
19
- "female": {"voice": "ru-RU-SvetlanaNeural","pitch": "+5Hz", "rate": "+5%"}
20
- }
21
 
22
- TEMP_DIR = tempfile.gettempdir()
 
 
 
 
 
 
23
 
24
- # --- УМНАЯ ЛОГИКА (БЕЗ НЕЙРОСЕТИ) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- def analyze_gender_by_grammar(text):
27
- """
28
- Определяет пол по окончаниям русских глаголов в словах автора.
29
- Работает мгновенно и точно.
30
- """
31
- text_lower = text.lower()
32
 
33
- # Маркеры женского рода (прошедшее время + "а")
34
- female_verbs = [
35
- r"сказала", r"спросила", r"ответила", r"прошептала", r"крикнула",
36
- r"подумала", r"заметила", r"усмехнулась", r"вздохнула", r"обернулась"
37
- ]
38
 
39
- # Маркеры мужского рода
40
- male_verbs = [
41
- r"сказал\b", r"спросил\b", r"ответил\b", r"прошептал\b", r"крикнул\b",
42
- r"подумал\b", r"заметил\b", r"усмехнулся", r"вздохнул", r"обернулся"
43
- ]
44
-
45
- # Проверяем контекст (слова автора)
46
- for verb in female_verbs:
47
- if re.search(verb, text_lower):
48
- return "female"
49
 
50
- for verb in male_verbs:
51
- if re.search(verb, text_lower):
52
- return "male"
53
-
54
- return "narrator" # Если не понятно — читает рассказчик
55
 
56
- def smart_split_text(text):
57
- """Разбивает текст на сцены и раздает роли"""
58
- segments = []
59
- paragraphs = text.split('\n')
 
 
60
 
61
- for p in paragraphs:
62
- p = p.strip()
63
- if not p: continue
64
-
65
- # Логика: Если это диалог (тире или кавычки)
66
- if p.startswith('—') or p.startswith('-') or '"' in p or '«' in p:
67
- # Пытаемся найти пол в этом же абзаце (слова автора)
68
- role = analyze_gender_by_grammar(p)
69
-
70
- # Если грамматика не помогла, но это явно диалог — ставим мужчину (как дефолт для героя)
71
- if role == "narrator":
72
- role = "male"
73
-
74
- segments.append({"text": p, "role": role})
75
- else:
76
- # Просто опи��ание
77
- segments.append({"text": p, "role": "narrator"})
78
-
79
- return segments
80
-
81
- # --- ГЕНЕРАЦИЯ ---
82
-
83
- async def generate_segment(text, role):
84
- if not text.strip(): return None
85
-
86
- conf = VOICE_CONFIG.get(role, VOICE_CONFIG["narrator"])
87
- path = os.path.join(TEMP_DIR, f"seg_{uuid.uuid4().hex}.mp3")
88
 
89
- try:
90
- comm = edge_tts.Communicate(text, conf["voice"], rate=conf["rate"], pitch=conf["pitch"])
91
- await comm.save(path)
92
- if os.path.exists(path) and os.path.getsize(path) > 100:
93
- return path
94
- except:
95
- pass
96
- return None
97
-
98
- async def process_book(text):
99
- if not text.strip(): raise gr.Warning("Текст пуст!")
100
 
101
- print("⚡ Мгновенный анализ текста...")
102
- segments = smart_split_text(text)
 
103
 
104
- full_audio = AudioSegment.empty()
105
- temp_files = []
106
- progress = gr.Progress()
107
 
108
- for item in progress.tqdm(segments, desc="Озвучка"):
109
- path = await generate_segment(item["text"], item["role"])
 
110
 
111
- if path:
112
- temp_files.append(path)
113
- seg = AudioSegment.from_mp3(path)
114
- if len(full_audio) > 0:
115
- full_audio = full_audio.append(seg, crossfade=50)
116
- else:
117
- full_audio = seg
118
- await asyncio.sleep(0.1)
119
 
120
- out_path = os.path.join(TEMP_DIR, f"turbo_book_{uuid.uuid4().hex}.mp3")
121
- full_audio.export(out_path, format="mp3")
122
-
123
- for f in temp_files:
124
- try: os.remove(f)
125
- except: pass
126
-
127
- return out_path, segments
128
 
129
- # --- ИНТЕРФЕЙС ---
130
- css = "body {background-color: #111827;} .container {max-width: 900px; margin: auto;}"
131
- theme = gr.themes.Soft(primary_hue="green")
132
 
133
- with gr.Blocks(theme=theme, css=css, title="Turbo TTS") as demo:
134
- gr.Markdown("# 🚀 Turbo Fantasy TTS (No GPU needed)")
135
- gr.Markdown("Мгновенная загрузка. Умное определение пола по грамматике.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  with gr.Column(scale=2):
139
- inp = gr.Textbox(
140
- label="Текст", lines=12,
141
- value='— Я пришла за тобой, — прошептала ведьма.\nРыцарь ответил: — Я готов.',
142
- placeholder="Вставьте текст..."
 
 
143
  )
144
- btn = gr.Button("⚡ Создать моментально", variant="primary")
145
-
146
- with gr.Column(scale=1):
147
- out_audio = gr.Audio(label="Результат")
148
- out_debug = gr.JSON(label="Роли (Debug)")
149
 
150
- btn.click(process_book, inputs=inp, outputs=[out_audio, out_debug])
 
 
151
 
152
  if __name__ == "__main__":
153
  demo.queue().launch()
 
5
  import os
6
  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
+
31
+ seen = set()
32
+ LANGUAGES_CACHE = []
33
+ for v in VOICES_CACHE:
34
+ if v['Locale'] not in seen:
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:
53
+ default_voice = v
54
+ break
 
 
 
 
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()