VSPAN commited on
Commit
2cbf263
·
verified ·
1 Parent(s): b2496fc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -79
app.py CHANGED
@@ -4,13 +4,12 @@ import asyncio
4
  import tempfile
5
  import os
6
  import uuid
7
- import json
8
  import re
9
  from pydub import AudioSegment
10
  from transformers import pipeline
11
 
12
- # --- НАСТРОЙКИ ГОЛОСОВ (ФЭНТЕЗИ) ---
13
- VOICE_CONFIG = {
14
  "narrator": {"voice": "ru-RU-DmitryNeural", "pitch": "-7Hz", "rate": "-5%"},
15
  "male": {"voice": "ru-RU-DenisNeural", "pitch": "-2Hz", "rate": "+0%"},
16
  "female": {"voice": "ru-RU-SvetlanaNeural","pitch": "+5Hz", "rate": "+5%"}
@@ -18,140 +17,205 @@ VOICE_CONFIG = {
18
 
19
  TEMP_DIR = tempfile.gettempdir()
20
 
21
- # --- ЗАГРУЗКА МАЛЕНЬКОЙ НЕЙРОСЕТИ ---
22
- # Qwen 2.5 0.5B Instruct - очень легкая, но умная
23
  MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
24
-
25
- print(f"🚀 Загрузка модели {MODEL_ID}...")
26
  try:
27
  pipe = pipeline(
28
  "text-generation",
29
  model=MODEL_ID,
30
  device_map="auto",
31
- max_new_tokens=2048,
32
  trust_remote_code=True
33
  )
34
- print("✅ Модель готова!")
35
  except Exception as e:
36
- print(f"❌ Ошибка загрузки модели: {e}")
37
  pipe = None
38
 
39
- def analyze_text_with_tiny_ai(text):
40
- """Анализ текста легкой нейросетью."""
41
- if not pipe:
42
- return [{"text": text, "role": "narrator"}]
43
 
44
- system_prompt = (
45
- "Ты редактор. Твоя задача - определить роль для озвучки.\n"
46
- "Роли: narrator (автор), male (мужчина), female (женщина).\n"
47
- "Верни ТОЛЬКО JSON список."
48
- )
49
-
50
- user_prompt = f"""Разбей текст на роли:
51
- "{text}"
52
-
53
- Пример JSON ответа:
54
- [{{"text": "- Привет", "role": "male"}}, {{"text": "- сказала она", "role": "narrator"}}]
55
  """
56
-
57
- messages = [
58
- {"role": "system", "content": system_prompt},
59
- {"role": "user", "content": user_prompt},
 
 
 
 
 
60
  ]
61
-
62
  try:
63
- outputs = pipe(messages)
64
- result_text = outputs[0]["generated_text"][-1]["content"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- # Поиск JSON в ответе
67
- json_match = re.search(r'\[.*\]', result_text, re.DOTALL)
68
- if json_match:
69
- json_str = json_match.group(0)
70
- return json.loads(json_str)
71
  else:
72
- print(f"⚠️ Не JSON: {result_text}")
73
- return [{"text": text, "role": "narrator"}]
74
 
75
- except Exception as e:
76
- print(f"⚠️ Ошибка анализа: {e}")
77
- return [{"text": text, "role": "narrator"}]
78
 
79
- # --- ГЕНЕРАЦИЯ ---
80
 
81
- async def generate_segment(text, role):
82
  if not text.strip(): return None
83
- conf = VOICE_CONFIG.get(role, VOICE_CONFIG["narrator"])
84
- path = os.path.join(TEMP_DIR, f"{uuid.uuid4().hex}.mp3")
 
85
 
86
  try:
87
- comm = edge_tts.Communicate(text, conf["voice"], rate=conf["rate"], pitch=conf["pitch"])
88
  await comm.save(path)
89
  return path
90
- except:
 
91
  return None
92
 
93
- async def process_book(text):
94
- if not text.strip(): raise gr.Warning("Введите текст!")
 
95
 
96
- print("⚡ Анализ текста...")
97
- segments = analyze_text_with_tiny_ai(text)
 
98
 
99
  full_audio = AudioSegment.empty()
100
  temp_files = []
101
 
102
  progress = gr.Progress()
103
- for item in progress.tqdm(segments, desc="Озвучка"):
104
- # Защита от некорректного формата
105
- if isinstance(item, dict):
106
- txt = item.get("text", "")
107
- role = item.get("role", "narrator")
108
- else:
109
- txt = str(item)
110
- role = "narrator"
111
-
112
- path = await generate_segment(txt, role)
 
113
 
114
  if path:
115
  temp_files.append(path)
116
  seg = AudioSegment.from_mp3(path)
117
- # Плавная склейка (50ms)
118
  if len(full_audio) > 0:
119
- full_audio = full_audio.append(seg, crossfade=50)
120
  else:
121
  full_audio = seg
122
  await asyncio.sleep(0.1)
123
-
124
- out_path = os.path.join(TEMP_DIR, f"fantasy_{uuid.uuid4().hex}.mp3")
125
  full_audio.export(out_path, format="mp3")
126
 
 
127
  for f in temp_files:
128
  try: os.remove(f)
129
  except: pass
130
 
131
  return out_path, segments
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  # --- ИНТЕРФЕЙС ---
 
134
 
135
  css = """
136
- body {background-color: #111827; color: #e5e7eb;}
137
  .container {max-width: 900px; margin: auto;}
 
 
138
  """
139
 
140
- theme = gr.themes.Soft(primary_hue="indigo", secondary_hue="slate")
141
 
142
- with gr.Blocks(theme=theme, css=css, title="Fantasy Lite TTS") as demo:
143
- gr.Markdown("# Fantasy Lite TTS (Qwen 0.5B)")
144
 
145
- with gr.Row():
146
- with gr.Column(scale=2):
147
- inp = gr.Textbox(label="Текст", lines=10, placeholder="Вставьте текст...", value='— Кто здесь? — спросил рыцарь.\nВедьма усмех��улась: — Твоя судьба.')
148
- btn = gr.Button("🚀 Создать", variant="primary")
149
 
150
- with gr.Column(scale=1):
151
- out_audio = gr.Audio(label="Результат", type="filepath")
152
- out_debug = gr.JSON(label="Лог нейросети")
153
-
154
- btn.click(process_book, inputs=inp, outputs=[out_audio, out_debug])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  if __name__ == "__main__":
157
  demo.queue().launch()
 
4
  import tempfile
5
  import os
6
  import uuid
 
7
  import re
8
  from pydub import AudioSegment
9
  from transformers import pipeline
10
 
11
+ # --- НАСТРОЙКИ ГОЛОСОВ (ФЭНТЕЗИ ПРЕСЕТЫ) ---
12
+ VOICE_PRESETS = {
13
  "narrator": {"voice": "ru-RU-DmitryNeural", "pitch": "-7Hz", "rate": "-5%"},
14
  "male": {"voice": "ru-RU-DenisNeural", "pitch": "-2Hz", "rate": "+0%"},
15
  "female": {"voice": "ru-RU-SvetlanaNeural","pitch": "+5Hz", "rate": "+5%"}
 
17
 
18
  TEMP_DIR = tempfile.gettempdir()
19
 
20
+ # --- ЗАГРУЗКА AI (0.5B) ---
 
21
  MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
22
+ print(f"🚀 Загрузка малыша {MODEL_ID}...")
 
23
  try:
24
  pipe = pipeline(
25
  "text-generation",
26
  model=MODEL_ID,
27
  device_map="auto",
28
+ max_new_tokens=50, # Нам нужен короткий ответ (Male/Female), а не поэма
29
  trust_remote_code=True
30
  )
31
+ print("✅ AI готов!")
32
  except Exception as e:
33
+ print(f"❌ Ошибка AI: {e}")
34
  pipe = None
35
 
36
+ # --- ГИБРИДНЫЙ ПАРСЕР (БЕЗОПАСНЫЙ) ---
 
 
 
37
 
38
+ def classify_segment_with_ai(text):
 
 
 
 
 
 
 
 
 
 
39
  """
40
+ Спрашиваем у AI только одно: КТО говорит?
41
+ Текст не меняем.
42
+ """
43
+ if not pipe: return "narrator"
44
+
45
+ # Упрощенный промпт для маленькой модели
46
+ prompt = [
47
+ {"role": "system", "content": "Classify the speaker of the text. Options: narrator, male, female. Answer with ONE word."},
48
+ {"role": "user", "content": f"Text: \"{text}\"\nSpeaker:"}
49
  ]
50
+
51
  try:
52
+ output = pipe(prompt)[0]["generated_text"][-1]["content"].lower()
53
+ if "female" in output: return "female"
54
+ if "male" in output: return "male"
55
+ return "narrator"
56
+ except:
57
+ return "narrator"
58
+
59
+ def safe_split_text(text):
60
+ """
61
+ Разбивает текст на куски с помощью Python (Regex).
62
+ Гарантирует, что ни одна буква не пропадет.
63
+ """
64
+ # 1. Разбиваем на абзацы
65
+ paragraphs = text.split('\n')
66
+ segments = []
67
+
68
+ for p in paragraphs:
69
+ p = p.strip()
70
+ if not p: continue
71
 
72
+ # 2. Простая эвристика: если есть тире или кавычки - это может быть диалог
73
+ if p.startswith('') or p.startswith('-') or '"' in p or '«' in p:
74
+ # Спрашиваем у AI, чей это голос
75
+ role = classify_segment_with_ai(p)
76
+ segments.append({"text": p, "role": role})
77
  else:
78
+ # Если нет диалога, это точно рассказчик (экономим время AI)
79
+ segments.append({"text": p, "role": "narrator"})
80
 
81
+ return segments
 
 
82
 
83
+ # --- ГЕНЕРАТОР ---
84
 
85
+ async def generate_segment_audio(text, voice, rate, pitch):
86
  if not text.strip(): return None
87
+ path = os.path.join(TEMP_DIR, f"seg_{uuid.uuid4().hex}.mp3")
88
+ rate_str = f"{rate:+d}%" if isinstance(rate, int) else rate
89
+ pitch_str = f"{pitch:+d}Hz" if isinstance(pitch, int) else pitch
90
 
91
  try:
92
+ comm = edge_tts.Communicate(text, voice, rate=rate_str, pitch=pitch_str)
93
  await comm.save(path)
94
  return path
95
+ except Exception as e:
96
+ print(f"Error gen: {e}")
97
  return None
98
 
99
+ async def process_audiobook_ai(text):
100
+ """Режим AI: Сам определяет голоса"""
101
+ if not text.strip(): raise gr.Warning("Текст пуст!")
102
 
103
+ print("⚡ Python разбивает текст, AI классифицирует роли...")
104
+ # Используем безопасный метод разбиения
105
+ segments = safe_split_text(text)
106
 
107
  full_audio = AudioSegment.empty()
108
  temp_files = []
109
 
110
  progress = gr.Progress()
111
+ for item in progress.tqdm(segments, desc="Генерация сцен"):
112
+ # Берем пресеты
113
+ role = item["role"]
114
+ settings = VOICE_PRESETS.get(role, VOICE_PRESETS["narrator"])
115
+
116
+ path = await generate_segment_audio(
117
+ item["text"],
118
+ settings["voice"],
119
+ settings["rate"],
120
+ settings["pitch"]
121
+ )
122
 
123
  if path:
124
  temp_files.append(path)
125
  seg = AudioSegment.from_mp3(path)
126
+ # Мягкая склейка (70ms crossfade) для плавности
127
  if len(full_audio) > 0:
128
+ full_audio = full_audio.append(seg, crossfade=70)
129
  else:
130
  full_audio = seg
131
  await asyncio.sleep(0.1)
132
+
133
+ out_path = os.path.join(TEMP_DIR, f"book_ai_{uuid.uuid4().hex}.mp3")
134
  full_audio.export(out_path, format="mp3")
135
 
136
+ # Чистка
137
  for f in temp_files:
138
  try: os.remove(f)
139
  except: pass
140
 
141
  return out_path, segments
142
 
143
+ async def process_manual_mode(text, voice, rate, pitch):
144
+ """Классический режим: Один голос на всё"""
145
+ if not text.strip(): raise gr.Warning("Текст пуст!")
146
+
147
+ voice_short = voice.split(" (")[0]
148
+ out_path = os.path.join(TEMP_DIR, f"manual_{uuid.uuid4().hex}.mp3")
149
+
150
+ # В ручном режиме генерируем одним куском (или можно тоже разбить для длинных текстов)
151
+ # Edge-TTS поддерживает длинные тексты, но лучше разбивать
152
+ comm = edge_tts.Communicate(text, voice_short, rate=f"{rate:+d}%", pitch=f"{pitch:+d}Hz")
153
+ await comm.save(out_path)
154
+
155
+ return out_path
156
+
157
+ # --- ЗАГРУЗКА СПИСКА ГОЛОСОВ ---
158
+ async def get_voices_list():
159
+ voices = await edge_tts.list_voices()
160
+ # Сортируем: Сначала RU, потом остальные
161
+ ru_voices = sorted([f"{v['ShortName']} ({v['Gender']})" for v in voices if v['Locale'] == "ru-RU"])
162
+ en_voices = sorted([f"{v['ShortName']} ({v['Gender']})" for v in voices if v['Locale'] == "en-US"])
163
+ return ru_voices + en_voices
164
+
165
  # --- ИНТЕРФЕЙС ---
166
+ VOICES_LIST = asyncio.run(get_voices_list())
167
 
168
  css = """
169
+ body {background-color: #0b0f19; color: #e2e8f0;}
170
  .container {max-width: 900px; margin: auto;}
171
+ h1 {color: #fbbf24; font-family: serif; text-align: center;}
172
+ .tabs {border-bottom: 1px solid #374151;}
173
  """
174
 
175
+ theme = gr.themes.Soft(primary_hue="amber", secondary_hue="slate")
176
 
177
+ with gr.Blocks(theme=theme, css=css, title="Fantasy Studio Ultimate") as demo:
178
+ gr.Markdown("# 🐉 Fantasy Studio: Hybrid AI")
179
 
180
+ with gr.Tabs():
 
 
 
181
 
182
+ # --- ВКЛАДКА 1: AI РЕЖИССЕР ---
183
+ with gr.TabItem("✨ AI Режиссер (Авто)"):
184
+ with gr.Row():
185
+ with gr.Column(scale=2):
186
+ ai_text = gr.Textbox(
187
+ label="Текст книги", lines=12,
188
+ placeholder="Вставьте текст. AI сам определит, где говорит мужчина, а где женщина.",
189
+ value='— Стой! — крикнул рыцарь.\nДевушка обернулась и тихо спросила:\n— Зачем мне останавливаться?\nВетер выл в ушах.'
190
+ )
191
+ ai_btn = gr.Button("🎬 Создать Аудиоспектакль", variant="primary", size="lg")
192
+
193
+ with gr.Column(scale=1):
194
+ gr.Markdown("### 🎭 Роли и настройки")
195
+ gr.JSON(value=VOICE_PRESETS, label="Текущие пресеты")
196
+ ai_audio = gr.Audio(label="Результат", type="filepath")
197
+ ai_debug = gr.JSON(label="Как AI понял текст (без потерь)")
198
+
199
+ ai_btn.click(process_audiobook_ai, inputs=ai_text, outputs=[ai_audio, ai_debug])
200
+
201
+ # --- ВКЛАДКА 2: РУЧНОЙ РЕЖИМ ---
202
+ with gr.TabItem("🛠️ Ручное управление (Классика)"):
203
+ with gr.Row():
204
+ with gr.Column():
205
+ man_text = gr.Textbox(label="Текст", lines=10, value="Привет! Это проверка ручного режима.")
206
+ with gr.Column():
207
+ man_voice = gr.Dropdown(
208
+ choices=VOICES_LIST,
209
+ value="ru-RU-DmitryNeural (Male)" if VOICES_LIST else None,
210
+ label="Голос"
211
+ )
212
+ man_rate = gr.Slider(-50, 50, value=0, step=1, label="Скорость (%)")
213
+ man_pitch = gr.Slider(-20, 20, value=0, step=1, label="Тон (Hz)")
214
+
215
+ man_btn = gr.Button("🔊 Озвучить (Ручной)", variant="secondary")
216
+ man_audio = gr.Audio(label="Результат", type="filepath")
217
+
218
+ man_btn.click(process_manual_mode, inputs=[man_text, man_voice, man_rate, man_pitch], outputs=man_audio)
219
 
220
  if __name__ == "__main__":
221
  demo.queue().launch()