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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -45
app.py CHANGED
@@ -9,54 +9,48 @@ 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%"} # Нежный
17
  }
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
- # Создаем пайплайн для генерации текста
28
  pipe = pipeline(
29
  "text-generation",
30
  model=MODEL_ID,
31
- device_map="auto", # Автоматически использует CPU или GPU
32
  max_new_tokens=2048,
33
  trust_remote_code=True
34
  )
35
- print("✅ Модель готова к работе!")
36
  except Exception as e:
37
  print(f"❌ Ошибка загрузки модели: {e}")
38
  pipe = None
39
 
40
  def analyze_text_with_tiny_ai(text):
41
- """
42
- Использует маленькую модель для разбора текста.
43
- """
44
  if not pipe:
45
  return [{"text": text, "role": "narrator"}]
46
 
47
- # Простой промпт для маленькой модели.
48
- # Маленькие модели любят конкретику.
49
  system_prompt = (
50
- "Ты редактор. Твоя задача - определить, кто говорит фразу.\n"
51
- "Варианты ролей: narrator (автор), male (мужчина), female (женщина).\n"
52
- "Ответь СТРОГО в формате JSON списка."
53
  )
54
 
55
- user_prompt = f"""Разбей этот текст на роли:
56
-
57
  "{text}"
58
 
59
- Пример ответа:
60
  [{{"text": "- Привет", "role": "male"}}, {{"text": "- сказала она", "role": "narrator"}}]
61
  """
62
 
@@ -69,15 +63,13 @@ def analyze_text_with_tiny_ai(text):
69
  outputs = pipe(messages)
70
  result_text = outputs[0]["generated_text"][-1]["content"]
71
 
72
- # Очистка ответа (маленькие модели могут добавить лишний текст)
73
  json_match = re.search(r'\[.*\]', result_text, re.DOTALL)
74
  if json_match:
75
  json_str = json_match.group(0)
76
- data = json.loads(json_str)
77
- return data
78
  else:
79
- # Если JSON не найден, пробуем распарсить грубо или возвращаем ошибку
80
- print(f"⚠️ Модель ответила не JSON: {result_text}")
81
  return [{"text": text, "role": "narrator"}]
82
 
83
  except Exception as e:
@@ -90,44 +82,46 @@ async def generate_segment(text, role):
90
  if not text.strip(): return None
91
  conf = VOICE_CONFIG.get(role, VOICE_CONFIG["narrator"])
92
  path = os.path.join(TEMP_DIR, f"{uuid.uuid4().hex}.mp3")
 
93
  try:
94
  comm = edge_tts.Communicate(text, conf["voice"], rate=conf["rate"], pitch=conf["pitch"])
95
  await comm.save(path)
96
  return path
97
- except: return None
 
98
 
99
  async def process_book(text):
100
- if not text.strip(): raise gr.Warning("Пустой текст!")
101
 
102
- print("⚡ AI анализ (Lite)...")
103
  segments = analyze_text_with_tiny_ai(text)
104
- print(f"Результат анализа: {len(segments)} кусков.")
105
 
106
  full_audio = AudioSegment.empty()
107
  temp_files = []
108
 
109
  progress = gr.Progress()
110
  for item in progress.tqdm(segments, desc="Озвучка"):
111
- # Если модель вернула просто строку вместо словаря (бывает у маленьких моделей)
112
- if isinstance(item, str):
113
- txt, role = item, "narrator"
114
- else:
115
  txt = item.get("text", "")
116
  role = item.get("role", "narrator")
 
 
 
117
 
118
  path = await generate_segment(txt, role)
119
 
120
  if path:
121
  temp_files.append(path)
122
  seg = AudioSegment.from_mp3(path)
123
- # Мягкая склейка (Crossfade 50ms)
124
  if len(full_audio) > 0:
125
  full_audio = full_audio.append(seg, crossfade=50)
126
  else:
127
  full_audio = seg
128
  await asyncio.sleep(0.1)
129
 
130
- out_path = os.path.join(TEMP_DIR, f"lite_fantasy_{uuid.uuid4().hex}.mp3")
131
  full_audio.export(out_path, format="mp3")
132
 
133
  for f in temp_files:
@@ -137,16 +131,27 @@ async def process_book(text):
137
  return out_path, segments
138
 
139
  # --- ИНТЕРФЕЙС ---
140
- css = "body {background-color: #1e1e2e; color: #cdd6f4;} .gradio-container {font-family: 'Verdana', sans-serif;}"
141
- theme = gr.themes.Soft(primary_hue="indigo")
142
 
143
- with gr.Blocks(theme=theme, css=css, title="Fantasy TTS Lite") as demo:
144
- gr.Markdown("# Fantasy TTS: Lite Edition (Qwen 0.5B)")
145
- gr.Markdown("Использует сверхлегкую нейросеть для скорости. Работает на слабом железе.")
146
-
147
- with gr.Row():
148
- inp = gr.Textbox(label="Текст", lines=8, value='— Стой! — крикнул он.\nОна обернулась: — Зачем?')
149
- btn = gr.Button("🚀 Озвучить", variant="primary")
 
 
150
 
151
  with gr.Row():
152
- out_audio = gr.
 
 
 
 
 
 
 
 
 
 
 
 
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%"}
17
  }
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
 
 
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:
 
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:
 
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()