AlserFurma commited on
Commit
8e44e35
·
verified ·
1 Parent(s): 4434e4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +370 -73
app.py CHANGED
@@ -2,93 +2,390 @@ import gradio as gr
2
  import os
3
  from PIL import Image
4
  import tempfile
 
5
  import torch
6
  from transformers import VitsModel, AutoTokenizer
7
  import scipy.io.wavfile as wavfile
8
- from gradio_client import Client, handle_file
9
  import traceback
 
 
 
10
 
11
- # Только CPU
12
- os.environ["CUDA_VISIBLE_DEVICES"] = ""
13
- torch.set_num_threads(4)
14
-
15
- TALKING_HEAD = "Skywork/skyreels-a1-talking-head"
16
- model = None
17
- tokenizer = None
18
-
19
- def load_tts():
20
- global model, tokenizer
21
- if model is None:
22
- print("Загружаем TTS (каз)…")
23
- model = VitsModel.from_pretrained("facebook/mms-tts-kaz")
24
- tokenizer = AutoTokenizer.from_pretrained("facebook/mms-tts-kaz")
25
- print("TTS готова")
 
 
 
 
 
 
 
 
26
  return True
27
-
28
- def ru_to_kz_simple(text: str) -> str:
29
- rep = {
30
- "привет": "сәлем", "здравствуйте": "сәлеметсіз бе", "спасибо": "рахмет",
31
- "да": "иә", "нет": "жоқ", "сегодня": "бүгін", "завтра": "ертең",
32
- рок": "сабақ", екция": "дәріс", "учитель": "мұғалім", "школа": ектеп"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
- for ru, kz in rep.items():
35
- text = text.replace(ru, kz).replace(ru.capitalize(), kz.capitalize())
36
- return text
37
-
38
- def create_video(image: Image.Image, text: str):
39
- if not image or not text.strip():
40
- return None, "Загрузите фото и введите текст!"
41
-
42
- load_tts()
43
- text_kz = ru_to_kz_simple(text.strip())
44
-
 
 
 
 
45
  try:
46
- # TTS
47
- inputs = tokenizer(text_kz, return_tensors="pt")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  with torch.no_grad():
49
- waveform = model(**inputs).waveform.squeeze().cpu().numpy()
50
-
51
- rate = model.config.sampling_rate
52
- audio_path = "/tmp/audio.wav"
53
- wavfile.write(audio_path, rate, (waveform * 32767).astype("int16"))
54
-
55
- # Изображение
56
- if image.mode != "RGB":
57
- image = image.convert("RGB")
58
- img_path = "/tmp/img.png"
59
- image.save(img_path)
60
-
61
- # Talking head
62
- client = Client(TALKING_HEAD)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  result = client.predict(
64
  image_path=handle_file(img_path),
65
  audio_path=handle_file(audio_path),
66
- guidance_scale=2.0,
67
- steps=8,
68
  api_name="/process_image_audio"
69
  )
70
-
71
- video_path = result[0] if isinstance(result, (list, tuple)) else result
72
- return video_path, "Бейне дайын!"
73
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  except Exception as e:
 
 
75
  traceback.print_exc()
76
- return None, f"Қате: {e}"
77
-
78
- # === Интерфейс ===
79
- with gr.Blocks(title="Бейне-лектор қазақша") as app:
80
- gr.Markdown("# Бейне-лектор қазақша\nФото + текст → говорящий видео-лектор")
81
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  with gr.Row():
83
- with gr.Column():
84
- img_in = gr.Image(label="Фото лектора", type="pil")
85
- txt_in = gr.Textbox(label="Текст лекции (русский)", lines=6, placeholder="Привет! Сегодня мы изучаем математику…")
86
- btn = gr.Button("Сделать видео", variant="primary")
87
-
88
- with gr.Column():
89
- video_out = gr.Video(label="Готовое видео")
90
- status = gr.Textbox(label="Статус", interactive=False)
91
-
92
- btn.click(create_video, [img_in, txt_in], [video_out, status])
93
-
94
- app.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import os
3
  from PIL import Image
4
  import tempfile
5
+ from gradio_client import Client, handle_file
6
  import torch
7
  from transformers import VitsModel, AutoTokenizer
8
  import scipy.io.wavfile as wavfile
 
9
  import traceback
10
+ import base64
11
+ import random
12
+ import numpy as np # Фикс для модели (np.prod)
13
 
14
+ # Принудительно CPU и минимальное использование памяти
15
+ os.environ['CUDA_VISIBLE_DEVICES'] = ''
16
+ torch.set_num_threads(2) # Ограничение потоков CPU
17
+ device = "cpu"
18
+ print(f"Using device: {device} (optimized mode)")
19
+ # Глобальные переменные
20
+ tts_model = None
21
+ tts_tokenizer = None
22
+ TALKING_HEAD_SPACE = "Skywork/skyreels-a1-talking-head"
23
+ def load_tts_model():
24
+ """Загрузка только TTS модели"""
25
+ global tts_model, tts_tokenizer
26
+
27
+ if tts_model is None:
28
+ print("Загрузка TTS модели (казахский)...")
29
+ tts_model = VitsModel.from_pretrained(
30
+ "facebook/mms-tts-kaz",
31
+ torch_dtype=torch.float32,
32
+ low_cpu_mem_usage=True
33
+ )
34
+ tts_model.eval() # Режим инференса
35
+ tts_tokenizer = AutoTokenizer.from_pretrained("facebook/mms-tts-kaz")
36
+ print("✓ TTS модель загружена")
37
  return True
38
+ def simple_translate_to_kazakh(russian_text):
39
+ """
40
+ Упрощенная транслитерация/перевод без тяжелых моделей
41
+ Для реального использования нужна легкая модель или API
42
+ """
43
+ # Простая замена для базовых слов емо)
44
+ translations = {
45
+ 'привет': 'сәлем',
46
+ 'здравствуйте': 'сәлеметсіздер ме',
47
+ 'спасибо': 'рахмет',
48
+ 'пожалуйста': 'өтінемін',
49
+ 'да': 'иә',
50
+ 'нет': 'жоқ',
51
+ 'сегодня': 'бүгін',
52
+ 'завтра': 'ертең',
53
+ 'математика': 'математика',
54
+ 'физика': 'физика',
55
+ 'урок': 'сабақ',
56
+ 'лекция': 'дәріс',
57
+ 'студент': 'студент',
58
+ 'учитель': 'мұғалім',
59
+ 'школа': 'мектеп',
60
+ 'университет': 'университет',
61
+ 'знание': 'білім',
62
+ 'книга': 'кітап',
63
+ 'вопрос': 'сұрақ',
64
+ 'ответ': 'жауап'
65
  }
66
+
67
+ text_lower = russian_text.lower()
68
+ result = russian_text
69
+
70
+ for ru, kk in translations.items():
71
+ result = result.replace(ru, kk)
72
+ result = result.replace(ru.capitalize(), kk.capitalize())
73
+
74
+ return result
75
+ def inference(image: Image.Image, text: str):
76
+ error_msg = ""
77
+ video_path = None
78
+ audio_path = None
79
+ img_path = None
80
+
81
  try:
82
+ # Загрузка TTS
83
+ if not load_tts_model():
84
+ raise RuntimeError("Не удалось загрузить TTS модель")
85
+
86
+ # Валидация
87
+ if image is None:
88
+ raise ValueError("Загрузите изображение лектора!")
89
+
90
+ if not text or not text.strip():
91
+ raise ValueError("Введите текст лекции!")
92
+
93
+ if len(text) > 500:
94
+ raise ValueError("Текст слишком длинный! Максимум 500 символов.")
95
+
96
+ print(f"Входной текст: '{text[:50]}...'")
97
+
98
+ # Простой перевод на казахский
99
+ translated_text = simple_translate_to_kazakh(text)
100
+ print(f"Переведенный текст: '{translated_text[:50]}...'")
101
+
102
+ # Генерация аудио с оптимизацией памяти
103
+ print("Генерация аудио...")
104
  with torch.no_grad():
105
+ inputs = tts_tokenizer(translated_text, return_tensors="pt", truncation=True, max_length=512)
106
+
107
+ # Освобождение памяти перед генерацией
108
+ if torch.cuda.is_available():
109
+ torch.cuda.empty_cache()
110
+
111
+ output = tts_model(**inputs)
112
+ waveform = output.waveform.squeeze().cpu().numpy()
113
+
114
+ # Очистка
115
+ del inputs, output
116
+
117
+ if waveform.size == 0:
118
+ raise ValueError("TTS сгенерировал пустое аудио!")
119
+
120
+ # Сохранение аудио
121
+ audio = (waveform * 32767).astype("int16")
122
+ sampling_rate = tts_model.config.sampling_rate
123
+
124
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as audio_file:
125
+ wavfile.write(audio_file.name, sampling_rate, audio)
126
+ audio_path = audio_file.name
127
+
128
+ print(f"✓ Аудио: {audio_path} ({len(waveform)/sampling_rate:.1f} сек)")
129
+
130
+ # Оптимизация изображения
131
+ print("Обработка изображения...")
132
+ if image.mode != 'RGB':
133
+ image = image.convert('RGB')
134
+
135
+ # Уменьшаем размер если слишком большое (экономия памяти)
136
+ max_size = 1024
137
+ if max(image.size) > max_size:
138
+ ratio = max_size / max(image.size)
139
+ new_size = tuple(int(dim * ratio) for dim in image.size)
140
+ image = image.resize(new_size, Image.Resampling.LANCZOS)
141
+ print(f"Изображение уменьшено до {new_size}")
142
+
143
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as img_file:
144
+ image.save(img_file.name, format='PNG', optimize=True)
145
+ img_path = img_file.name
146
+
147
+ print(f"✓ Изображение: {img_path}")
148
+
149
+ # Вызов Talking Head API
150
+ print(f"Подключение к {TALKING_HEAD_SPACE}...")
151
+ client = Client(TALKING_HEAD_SPACE, verbose=False)
152
+
153
  result = client.predict(
154
  image_path=handle_file(img_path),
155
  audio_path=handle_file(audio_path),
156
+ guidance_scale=2.5, # Снижено для скорости
157
+ steps=8, # Меньше шагов = быстрее
158
  api_name="/process_image_audio"
159
  )
160
+
161
+ # Обработка результата
162
+ if isinstance(result, tuple) and len(result) > 0:
163
+ video_data = result[0]
164
+ if isinstance(video_data, dict):
165
+ video_path = video_data.get('video') or video_data.get('path')
166
+ elif isinstance(video_data, str):
167
+ video_path = video_data
168
+ else:
169
+ video_path = str(video_data)
170
+ elif isinstance(result, str):
171
+ video_path = result
172
+ else:
173
+ raise ValueError("Неизвестный формат результата от API")
174
+
175
+ if not video_path or not os.path.exists(video_path):
176
+ raise ValueError("Видео не сгенерировано!")
177
+
178
+ print(f"✓ Видео: {video_path}")
179
+ error_msg = "✅ Бейне сәтті жасалды!"
180
+
181
  except Exception as e:
182
+ error_msg = f"❌ Қате: {str(e)}"
183
+ print(f"ОШИБКА: {error_msg}")
184
  traceback.print_exc()
185
+
186
+ finally:
187
+ # Очистка временных файлов
188
+ for path in [audio_path, img_path]:
189
+ if path and os.path.exists(path):
190
+ try:
191
+ os.remove(path)
192
+ except:
193
+ pass
194
+
195
+ return video_path, error_msg
196
+ def generate_interactive_lesson(text, video_path):
197
+ """Упрощенная версия без тяжелых моделей QA"""
198
+ try:
199
+ if not video_path or not os.path.exists(video_path):
200
+ return "<p style='color: red;'>❌ Алдымен бейнені жасаңыз!</p>"
201
+
202
+ # Простая генерация вопросов без ML моделей
203
+ sentences = text.split('.')[:3] # Первые 3 предложения
204
+ questions = []
205
+
206
+ for i, sent in enumerate(sentences):
207
+ sent = sent.strip()
208
+ if len(sent) < 10:
209
+ continue
210
+
211
+ # Простые шаблоны вопросов
212
+ words = sent.split()
213
+ if len(words) < 3:
214
+ continue
215
+
216
+ # Генерируем вопрос на основе шаблона
217
+ question_templates = [
218
+ f"Не сказано о {words[0].lower()}?",
219
+ f"Что упоминается в тексте о {words[1].lower() if len(words) > 1 else 'теме'}?",
220
+ f"Какая информация дана о {words[2].lower() if len(words) > 2 else 'содержании'}?"
221
+ ]
222
+
223
+ question = random.choice(question_templates)
224
+
225
+ # Правильный ответ - часть предложения
226
+ correct = ' '.join(words[:min(5, len(words))])
227
+
228
+ # Неправильные ответы
229
+ wrong_options = [
230
+ "Бұл туралы айтылмаған",
231
+ "Мәтінде жоқ",
232
+ "Дұрыс емес жауап"
233
+ ]
234
+ wrong = random.choice(wrong_options)
235
+
236
+ questions.append({
237
+ "question": question,
238
+ "correct": correct,
239
+ "wrong": wrong
240
+ })
241
+
242
+ if not questions:
243
+ # Создаем хотя бы один вопрос
244
+ questions.append({
245
+ "question": "Дәрістің негізгі тақырыбы не?",
246
+ "correct": text.split('.')[0][:50] if text else "Білім",
247
+ "wrong": "Спорт туралы"
248
+ })
249
+
250
+ # Base64 видео (оптимизировано)
251
+ print("Кодирование видео в base64...")
252
+ with open(video_path, 'rb') as f:
253
+ video_data = f.read()
254
+ # Проверка размера
255
+ if len(video_data) > 50 * 1024 * 1024: # 50MB
256
+ return "<p style='color: orange;'>⚠️ Видео слишком большое для встраивания. Скачайте его отдельно.</p>"
257
+ video_base64 = base64.b64encode(video_data).decode('utf-8')
258
+
259
+ # Минимальный HTML
260
+ html = f"""<!DOCTYPE html>
261
+ <html>
262
+ <head>
263
+ <meta charset="UTF-8">
264
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
265
+ <title>Интерактивті сабақ</title>
266
+ <style>
267
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
268
+ body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 15px; background: #f5f5f5; }}
269
+ h1 {{ color: #333; text-align: center; margin: 20px 0; font-size: 24px; }}
270
+ video {{ width: 100%; max-width: 600px; display: block; margin: 20px auto; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
271
+ .text {{ background: white; padding: 15px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
272
+ .q {{ background: white; padding: 15px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
273
+ button {{ background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px; }}
274
+ button:hover {{ background: #45a049; }}
275
+ .fb {{ margin-top: 10px; padding: 8px; border-radius: 5px; font-weight: bold; }}
276
+ label {{ cursor: pointer; }}
277
+ </style>
278
+ </head>
279
+ <body>
280
+ <h1>📚 Интерактивті сабақ</h1>
281
+ <video controls><source src="data:video/mp4;base64,{video_base64}" type="video/mp4"></video>
282
+ <div class="text"><strong>Дәріс мәтіні:</strong> {text[:500]}</div>
283
+ <h2 style="text-align:center; margin: 20px 0;">Тесттер:</h2>
284
+ """
285
+
286
+ for i, q in enumerate(questions):
287
+ ca = q['correct'].replace("'", "\\'").replace('"', '&quot;')
288
+ html += f"""
289
+ <div class="q">
290
+ <p><strong>Сұрақ {i+1}:</strong> {q['question']}</p>
291
+ <div style="margin: 10px 0;">
292
+ <input type="radio" name="q{i}" value="c" id="c{i}">
293
+ <label for="c{i}">{q['correct']}</label><br>
294
+ <input type="radio" name="q{i}" value="w" id="w{i}">
295
+ <label for="w{i}">{q['wrong']}</label>
296
+ </div>
297
+ <button onclick="check({i},'{ca}')">Тексеру</button>
298
+ <div class="fb" id="fb{i}"></div>
299
+ </div>
300
+ """
301
+
302
+ html += """
303
+ <script>
304
+ function check(i, c) {
305
+ var s = document.query_selector('input[name="q'+i+'"]:checked');
306
+ var f = document.getElementById('fb'+i);
307
+ if(!s) { f.innerHTML='⚠️ Жауап таңдаңыз!'; f.style.background='#fff3cd'; f.style.color='#856404'; return; }
308
+ if(s.value==='c') { f.innerHTML='✅ Дұрыс!'; f.style.background='#d4edda'; f.style.color='#155724'; }
309
+ else { f.innerHTML='❌ Қате. Дұрыс: '+c; f.style.background='#f8d7da'; f.style.color='#721c24'; }
310
+ }
311
+ </script>
312
+ </body>
313
+ </html>"""
314
+
315
+ escaped = html.replace('\\', '\\\\').replace('`', '\\`').replace('${', '\\${')
316
+
317
+ return f"""
318
+ <div style="text-align:center; padding: 20px; background: white; border-radius: 8px;">
319
+ <h3 style="color: #2c3e50;">✅ Интерактивті сабақ дайын!</h3>
320
+ <button onclick="var w=window.open('','_blank');w.document.write(`{escaped}`);w.document.close();"
321
+ style="background: #27ae60; color: white; padding: 15px 30px; font-size: 16px; border: none;
322
+ border-radius: 8px; cursor: pointer; margin-top: 15px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
323
+ 📖 Интерактивті сабақты ашу
324
+ </button>
325
+ </div>
326
+ """
327
+
328
+ except Exception as e:
329
+ traceback.print_exc()
330
+ return f"<p style='color: red;'>❌ Қате: {str(e)}</p>"
331
+ # Интерфейс
332
+ with gr.Blocks(theme=gr.themes.Soft(), title="Бейне Оқытушы", css="""
333
+ .gradio-container {max-width: 1200px !important;}
334
+ footer {display: none !important;}
335
+ """) as iface:
336
+
337
+ gr.Markdown("""
338
+ # 🎓 Бейне Оқытушы (CPU Оптимизацияланған)
339
+
340
+ **Қалай пайдалану:**
341
+ 1. 📸 Суретіңізді жүктеңіз (бет анық көрінетін)
342
+ 2. 📝 Дәріс мәтінін орыс тілінде енгізіңіз (500 таңбаға дейін)
343
+ 3. 🎬 "Бейнені жасау" батырмасын басыңыз
344
+ 4. 📚 Дайын болғаннан кейін "Интерактивті сабақ" жасай аласыз
345
+
346
+ ⚡ **Ескерту:** CPU режимінде жұмыс істейді, генерация 1-3 минут алуы мүмкін.
347
+ """)
348
+
349
  with gr.Row():
350
+ with gr.Column(scale=1):
351
+ image_input = gr.Image(type="pil", label="📸 Дәріскер суреті")
352
+ text_input = gr.Textbox(
353
+ lines=6,
354
+ placeholder="Мысалы: Сәлеметсіздер ме! Бүгін біз математика туралы сөйлесеміз...",
355
+ label="📝 Дәріс мәтіні (орыс тілінде)"
356
+ )
357
+ generate_btn = gr.Button("🎬 Бейнені жасау", variant="primary", size="lg")
358
+
359
+ with gr.Column(scale=1):
360
+ video_output = gr.Video(label="🎬 Дайын бейне")
361
+ status = gr.Textbox(label="ℹ️ Мәртебе", interactive=False)
362
+
363
+ interactive_btn = gr.Button("📚 Интерактивті сабақ жасау", visible=False, variant="secondary")
364
+ lesson_output = gr.HTML(value="", label="Интерактивті сабақ", visible=False)
365
+
366
+ def show_lesson_btn(video, status_msg):
367
+ return gr.update(visible=bool(video and "✅" in status_msg))
368
+
369
+ generate_btn.click(
370
+ inference,
371
+ inputs=[image_input, text_input],
372
+ outputs=[video_output, status]
373
+ ).then(
374
+ show_lesson_btn,
375
+ inputs=[video_output, status],
376
+ outputs=interactive_btn
377
+ )
378
+
379
+ interactive_btn.click(
380
+ generate_interactive_lesson,
381
+ inputs=[text_input, video_output],
382
+ outputs=lesson_output
383
+ ).then(
384
+ lambda: gr.update(visible=True),
385
+ outputs=lesson_output
386
+ )
387
+ if __name__ == "__main__":
388
+ iface.launch(
389
+ server_name="0.0.0.0",
390
+ server_port=7860
391
+ )