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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -111
app.py CHANGED
@@ -7,141 +7,129 @@ import uuid
7
  import json
8
  import re
9
  from pydub import AudioSegment
10
- from huggingface_hub import hf_hub_download
11
- from llama_cpp import Llama # Библиотека для локального запуска нейросети
12
 
13
- # --- НАСТРОЙКИ ГОЛОСОВ (ФЭНТЕЗИ) ---
14
  VOICE_CONFIG = {
15
- "narrator": {"voice": "ru-RU-DmitryNeural", "pitch": "-7Hz", "rate": "-5%"},
16
- "male": {"voice": "ru-RU-DenisNeural", "pitch": "-2Hz", "rate": "+0%"},
17
- "female": {"voice": "ru-RU-SvetlanaNeural","pitch": "+5Hz", "rate": "+5%"}
18
  }
19
 
20
  TEMP_DIR = tempfile.gettempdir()
21
 
22
- # --- ЛОКАЛЬНАЯ НЕЙРОСЕТЬ (QWEN) ---
23
- # Мы используем Qwen 2.5 3B Instruct в формате GGUF.
24
- # Это позволяет запускать модель прямо на CPU сервера быстро и бесплатно.
25
- REPO_ID = "Qwen/Qwen2.5-3B-Instruct-GGUF"
26
- FILENAME = "qwen2.5-3b-instruct-q4_k_m.gguf"
27
 
28
- print(" Загрузка нейросети в память сервера... Это может занять минуту.")
29
  try:
30
- # Скачиваем модель локально (кэшируется)
31
- model_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME)
32
-
33
- # Инициализируем модель (n_ctx=2048 - длина контекста)
34
- llm = Llama(
35
- model_path=model_path,
36
- n_ctx=4096, # Память контекста
37
- n_threads=4, # Используем 4 ядра процессора
38
- verbose=False # Отключаем лишний шум в консоли
39
  )
40
- print("✅ Нейросеть успешно запущена локально!")
41
  except Exception as e:
42
- print(f"❌ Ошибка запуска нейросети: {e}")
43
- llm = None
44
 
45
- def ask_local_llm(text):
46
  """
47
- Обрабатывает текст через локальную модель Qwen.
48
  """
49
- if not llm:
50
  return [{"text": text, "role": "narrator"}]
51
 
52
- system_prompt = """Ты помощник режиссера. Твоя задача - проанализировать текст и разбить его на реплики для озвучки.
53
- 1. Определи, кто говорит: "narrator" (автор), "male" (мужчина), "female" (женщина).
54
- 2. Верни ТОЛЬКО JSON массив. Без лишних слов.
 
 
 
 
 
 
55
 
56
- Пример:
57
- Вход: — Привет! — сказала Аня.
58
- Выход: [{"text": "— Привет!", "role": "female"}, {"text": "— сказала Аня.", "role": "narrator"}]
 
59
  """
60
 
61
- user_prompt = f"Текст для анализа:\n{text}"
62
-
63
- # Формируем диалог в формате ChatML (понятный для Qwen)
64
  messages = [
65
  {"role": "system", "content": system_prompt},
66
- {"role": "user", "content": user_prompt}
67
  ]
68
 
69
  try:
70
- # Генерация ответа
71
- response = llm.create_chat_completion(
72
- messages=messages,
73
- max_tokens=2000, # Максимальная длина ответа
74
- temperature=0.1, # Минимальная креативность (для точности JSON)
75
- top_p=0.9
76
- )
77
-
78
- content = response["choices"][0]["message"]["content"]
79
 
80
- # Очистка ответа от мусора (если модель решила поболтать)
81
- content = re.sub(r'```json', '', content)
82
- content = re.sub(r'```', '', content).strip()
83
-
84
- # Попытка распарсить JSON
85
- data = json.loads(content)
86
- return data
87
-
 
 
 
88
  except Exception as e:
89
- print(f"⚠️ Ошибка парсинга LLM: {e}\nОтвет был: {content if 'content' in locals() else 'Пусто'}")
90
- # Если нейронка ошиблась, возвращаем весь текст как рассказчика
91
  return [{"text": text, "role": "narrator"}]
92
 
93
- # --- ГЕНЕРАЦИЯ АУДИО (С КЭШИРОВАНИЕМ И СКЛЕЙКОЙ) ---
94
 
95
  async def generate_segment(text, role):
96
  if not text.strip(): return None
97
  conf = VOICE_CONFIG.get(role, VOICE_CONFIG["narrator"])
98
-
99
- fname = f"{uuid.uuid4().hex}.mp3"
100
- path = os.path.join(TEMP_DIR, fname)
101
-
102
  try:
103
  comm = edge_tts.Communicate(text, conf["voice"], rate=conf["rate"], pitch=conf["pitch"])
104
  await comm.save(path)
105
  return path
106
- except:
107
- return None
108
 
109
- async def process_audiobook(text):
110
- if not text.strip(): raise gr.Warning("Введите текст!")
111
-
112
- print("🧠 Локальная нейросеть анализирует текст...")
113
- segments = ask_local_llm(text)
114
 
115
- print(f"📊 Найдено фрагментов: {len(segments)}")
 
 
116
 
117
  full_audio = AudioSegment.empty()
118
  temp_files = []
 
119
  progress = gr.Progress()
120
-
121
- for item in progress.tqdm(segments, desc="Озвучивание"):
122
- path = await generate_segment(item["text"], item.get("role", "narrator"))
 
 
 
 
 
 
123
 
124
  if path:
125
  temp_files.append(path)
126
- seg_audio = AudioSegment.from_mp3(path)
127
-
128
- # УМНАЯ СКЛЕЙКА (Crossfade)
129
- # Убирает паузы, накладывая конец одного куска на начало другого (50мс)
130
  if len(full_audio) > 0:
131
- full_audio = full_audio.append(seg_audio, crossfade=50)
132
  else:
133
- full_audio = seg_audio
134
-
135
- # Микро-пауза для стабильности
136
  await asyncio.sleep(0.1)
137
 
138
- out_name = f"fantasy_local_{uuid.uuid4().hex}.mp3"
139
- out_path = os.path.join(TEMP_DIR, out_name)
140
-
141
- print("💾 Сохранение файла...")
142
  full_audio.export(out_path, format="mp3")
143
 
144
- # Уборка
145
  for f in temp_files:
146
  try: os.remove(f)
147
  except: pass
@@ -149,33 +137,16 @@ async def process_audiobook(text):
149
  return out_path, segments
150
 
151
  # --- ИНТЕРФЕЙС ---
 
 
152
 
153
- css = """
154
- body { background-color: #0f172a; }
155
- .container { max-width: 900px; margin: auto; }
156
- h1 { color: #fbbf24; text-align: center; font-family: serif; }
157
- """
158
-
159
- with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber"), css=css, title="Local AI Narrator") as demo:
160
 
161
- gr.Markdown("# 🏰 Fantasy TTS: Local AI Edition")
162
- gr.Markdown("Нейросеть Qwen работает прямо на этом сервере. Данные никуда не уходят.")
 
163
 
164
  with gr.Row():
165
- with gr.Column(scale=2):
166
- inp = gr.Textbox(
167
- label="Текст книги", lines=10,
168
- value='— Стой! — крикнул рыцарь.\nДевушка обернулась и тихо спросила:\n— Зачем мне останавливаться?\nВетер выл в ушах.',
169
- placeholder="Вставьте текст..."
170
- )
171
- btn = gr.Button("✨ Создать (Обработка на сервере)", variant="primary", size="lg")
172
-
173
- with gr.Column(scale=1):
174
- out_audio = gr.Audio(label="Результат", type="filepath")
175
- out_debug = gr.JSON(label="Как нейросеть увидела роли")
176
-
177
- btn.click(process_audiobook, inputs=inp, outputs=[out_audio, out_debug])
178
-
179
- if __name__ == "__main__":
180
- # max_size=5 ограничивает очередь, чтобы сервер не завис
181
- demo.queue(max_size=5).launch()
 
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%"} # Нежный
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
 
 
 
 
63
  messages = [
64
  {"role": "system", "content": system_prompt},
65
+ {"role": "user", "content": user_prompt},
66
  ]
67
 
68
  try:
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:
84
+ print(f"⚠️ Ошибка анализа: {e}")
 
85
  return [{"text": text, "role": "narrator"}]
86
 
87
+ # --- ГЕНЕРАЦИЯ ---
88
 
89
  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:
134
  try: os.remove(f)
135
  except: pass
 
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.