AlserFurma commited on
Commit
64354df
·
verified ·
1 Parent(s): fdf8c85

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +223 -129
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # Обновлённая версия app.py с интерактивным модулем уроков
2
 
3
  import gradio as gr
4
  import os
@@ -12,12 +12,16 @@ import traceback
12
  import random
13
 
14
  # =========================
15
- # Загрузка моделей
16
  # =========================
 
17
 
18
  device = "cuda" if torch.cuda.is_available() else "cpu"
19
  print(f"Using device: {device}")
20
 
 
 
 
21
  try:
22
  # TTS модель (казахский)
23
  tts_model = VitsModel.from_pretrained("facebook/mms-tts-kaz").to(device)
@@ -30,7 +34,7 @@ try:
30
  device=0 if device == "cuda" else -1
31
  )
32
 
33
- # Модель для генерации вопросов
34
  qa_model = pipeline(
35
  "text2text-generation",
36
  model="google/flan-t5-small",
@@ -44,163 +48,253 @@ except Exception as e:
44
 
45
 
46
  # =========================
47
- # Talking Head API
48
- # =========================
49
-
50
- TALKING_HEAD_SPACE = "Skywork/skyreels-a1-talking-head"
51
-
52
-
53
- # =========================
54
- # Генерация вопроса + вариантов ответа
55
  # =========================
56
 
57
- def generate_quiz(text):
58
- prompt = f"Сгенерируй один учебный вопрос по этому тексту и дай 1 правильный и 1 неправильный вариант ответа. Формат: QUESTION: ... CORRECT: ... WRONG: ... TEXT: {text}"
59
-
60
- output = qa_model(prompt, max_length=200)[0]["generated_text"]
61
-
62
- question, correct, wrong = "", "", ""
63
-
64
- for line in output.split("\n"):
65
- if "QUESTION:" in line:
66
- question = line.replace("QUESTION:", "").strip()
67
- elif "CORRECT:" in line:
68
- correct = line.replace("CORRECT:", "").strip()
69
- elif "WRONG:" in line:
70
- wrong = line.replace("WRONG:", "").strip()
71
-
72
- if not question or not correct or not wrong:
73
- raise ValueError("Модель не смогла создать вопрос.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- # Перемешиваем варианты
76
  options = [correct, wrong]
77
  random.shuffle(options)
 
78
 
79
- return question, correct, options
80
 
 
 
 
 
 
81
 
82
- # =========================
83
- # Основная функция (с интерактивом)
84
- # =========================
85
 
86
- def inference(image: Image.Image, text: str):
 
 
87
 
88
- error_msg = ""
89
- video_path = None
90
- audio_path = None
91
- img_path = None
 
 
 
92
 
 
 
 
 
93
  try:
94
- if image is None:
95
- raise ValueError("Загрузите изображение лектора!")
96
-
97
- if not text or not text.strip():
98
- raise ValueError("Введите текст лекции!")
99
-
100
- if len(text) > 500:
101
- raise ValueError("Текст превышает 500 символов!")
102
-
103
- print("📥 Ввод (RU):", text)
104
-
105
- # Создание вопроса
106
- question, correct_answer, options = generate_quiz(text)
107
- quiz_text_ru = f"Вопрос: {question}. Варианты ответа: {options[0]} или {options[1]}. Выберите правильный."
108
-
109
- # Перевод вопроса и ответов на казахский, чтобы лектор произнёс их
110
- translation = translator(quiz_text_ru, src_lang="rus_Cyrl", tgt_lang="kaz_Cyrl")
111
- quiz_text_kk = translation[0]["translation_text"]
112
-
113
- # TTS
114
- inputs = tts_tokenizer(quiz_text_kk, return_tensors="pt").to(device)
115
- with torch.no_grad():
116
- output = tts_model(**inputs)
117
- waveform = output.waveform.squeeze().cpu().numpy()
118
- audio = (waveform * 32767).astype("int16")
119
- sampling_rate = tts_model.config.sampling_rate
120
-
121
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
122
- wavfile.write(f.name, sampling_rate, audio)
123
- audio_path = f.name
124
-
125
- # Сохранение фото
126
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
127
- if image.mode != "RGB":
128
- image = image.convert("RGB")
129
- image.save(f.name)
130
- img_path = f.name
131
-
132
- # Talking Head API
133
- client = Client(TALKING_HEAD_SPACE)
134
  result = client.predict(
135
- image_path=handle_file(img_path),
136
  audio_path=handle_file(audio_path),
137
  guidance_scale=3.0,
138
  steps=10,
139
  api_name="/process_image_audio"
140
  )
 
 
141
 
142
- if isinstance(result, tuple) and len(result) > 0:
143
- video_data = result[0]
144
- elif isinstance(result, dict):
145
- video_data = result
146
- else:
147
- raise ValueError("Неизвестный формат ответа API")
 
148
 
149
- video_path = (
150
- video_data.get("video") or
151
- video_data.get("path") or
152
- video_data.get("file")
153
- )
 
 
154
 
155
- if not video_path:
156
- raise ValueError("API не вернул путь к видео!")
157
 
158
- error_msg = f"Вопрос: {question}\nВарианты ответа: {options}\nПравильный: {correct_answer}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  except Exception as e:
161
- error_msg = f"❌ Ошибка: {str(e)}"
162
  traceback.print_exc()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
- finally:
165
- for p in [audio_path, img_path]:
166
- if p and os.path.exists(p):
167
- try:
168
- os.remove(p)
169
- except:
170
- pass
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- return video_path, error_msg
 
 
 
 
173
 
174
 
175
  # =========================
176
- # Интерфейс Gradio
177
  # =========================
178
 
179
- title = "🎓 Интерактивный Бейне Оқытушы"
180
-
181
- description = """
182
- Загрузите фото лектора и текст лекции. Система:
183
- 1) Переведёт текст
184
- 2) Озвучит
185
- 3) Создаст видео лектора
186
- 4) Сгенерирует вопрос и варианты ответа
187
- """
188
-
189
- iface = gr.Interface(
190
- fn=inference,
191
- inputs=[
192
- gr.Image(type="pil", label="📸 Фото лектора"),
193
- gr.Textbox(lines=5, label="📝 Текст лекции (RU)")
194
- ],
195
- outputs=[
196
- gr.Video(label="🎬 Видео"),
197
- gr.Textbox(label="🧩 Вопрос и ответы")
198
- ],
199
- title=title,
200
- description=description,
201
- cache_examples=False,
202
- flagging_mode="never"
203
  )
204
 
205
- if __name__ == "__main__":
206
- iface.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Полная обновлённая версия app.py с двухшаговым интерактивным уроком (короткая реакция на казахском)
2
 
3
  import gradio as gr
4
  import os
 
12
  import random
13
 
14
  # =========================
15
+ # Параметры
16
  # =========================
17
+ TALKING_HEAD_SPACE = "Skywork/skyreels-a1-talking-head"
18
 
19
  device = "cuda" if torch.cuda.is_available() else "cpu"
20
  print(f"Using device: {device}")
21
 
22
+ # =========================
23
+ # Загрузка моделей
24
+ # =========================
25
  try:
26
  # TTS модель (казахский)
27
  tts_model = VitsModel.from_pretrained("facebook/mms-tts-kaz").to(device)
 
34
  device=0 if device == "cuda" else -1
35
  )
36
 
37
+ # Модель для генерации вопросов (text2text)
38
  qa_model = pipeline(
39
  "text2text-generation",
40
  model="google/flan-t5-small",
 
48
 
49
 
50
  # =========================
51
+ # Вспомогательные функции
 
 
 
 
 
 
 
52
  # =========================
53
 
54
+ def generate_quiz(text: str):
55
+ """Генерирует один вопрос и два варианта (correct, wrong) на русском языке."""
56
+ prompt = (
57
+ "Сгенерируй один учебный вопрос по этому тексту и дай 1 правильный и 1 неправильный вариант ответа. "
58
+ "Формат вывода (разделять переносами строки): QUESTION: ... CORRECT: ... WRONG: ... TEXT: " + text
59
+ )
60
+ try:
61
+ out = qa_model(prompt, max_length=200)[0]["generated_text"]
62
+ except Exception as e:
63
+ raise RuntimeError(f"Ошибка генерации вопроса: {e}")
64
+
65
+ question = ""
66
+ correct = ""
67
+ wrong = ""
68
+ for line in out.split('
69
+ '):
70
+ if line.upper().startswith("QUESTION:"):
71
+ question = line.split(':', 1)[1].strip()
72
+ elif line.upper().startswith("CORRECT:"):
73
+ correct = line.split(':', 1)[1].strip()
74
+ elif line.upper().startswith("WRONG:"):
75
+ wrong = line.split(':', 1)[1].strip()
76
+
77
+ if not (question and correct and wrong):
78
+ # Попытка более простого разбора
79
+ parts = out.split('CORRECT:')
80
+ if len(parts) > 1:
81
+ qpart = parts[0]
82
+ question = qpart.replace('QUESTION:', '').strip()
83
+ rest = parts[1]
84
+ if 'WRONG:' in rest:
85
+ correct, wrong = rest.split('WRONG:', 1)
86
+ correct = correct.strip()
87
+ wrong = wrong.strip()
88
+
89
+ if not (question and correct and wrong):
90
+ raise ValueError('Модель не смогла корректно сгенерировать вопрос/варианты')
91
 
 
92
  options = [correct, wrong]
93
  random.shuffle(options)
94
+ return question, options, correct
95
 
 
96
 
97
+ def synthesize_audio(text_ru: str):
98
+ """Переводит русскую строку на казахский, синтезирует аудио и возвращает путь к файлу .wav"""
99
+ # Переводим на казахский
100
+ translation = translator(text_ru, src_lang="rus_Cyrl", tgt_lang="kaz_Cyrl")
101
+ text_kk = translation[0]["translation_text"]
102
 
103
+ inputs = tts_tokenizer(text_kk, return_tensors="pt").to(device)
104
+ with torch.no_grad():
105
+ output = tts_model(**inputs)
106
 
107
+ waveform = output.waveform.squeeze().cpu().numpy()
108
+ if waveform.size == 0:
109
+ raise ValueError("TTS вернул пустое аудио")
110
 
111
+ audio = (waveform * 32767).astype('int16')
112
+ sampling_rate = getattr(tts_model.config, 'sampling_rate', 22050)
113
+
114
+ tmpf = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
115
+ wavfile.write(tmpf.name, sampling_rate, audio)
116
+ tmpf.close()
117
+ return tmpf.name
118
 
119
+
120
+ def make_talking_head(image_path: str, audio_path: str):
121
+ """Вызывает SkyReels/Talking Head space и возвращает путь или объект с видео."""
122
+ client = Client(TALKING_HEAD_SPACE)
123
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  result = client.predict(
125
+ image_path=handle_file(image_path),
126
  audio_path=handle_file(audio_path),
127
  guidance_scale=3.0,
128
  steps=10,
129
  api_name="/process_image_audio"
130
  )
131
+ except Exception as e:
132
+ raise RuntimeError(f"Ошибка вызова Talking Head API: {e}")
133
 
134
+ video_path = None
135
+ if isinstance(result, tuple) and len(result) > 0:
136
+ video_data = result[0]
137
+ elif isinstance(result, dict):
138
+ video_data = result
139
+ else:
140
+ video_data = result
141
 
142
+ if isinstance(video_data, dict):
143
+ video_path = video_data.get('video') or video_data.get('path') or video_data.get('file')
144
+ elif isinstance(video_data, str):
145
+ video_path = video_data
146
+
147
+ if not video_path:
148
+ raise ValueError('API не вернул путь к видео')
149
 
150
+ return video_path
 
151
 
152
+
153
+ # =========================
154
+ # Основные обработчики для Gradio
155
+ # =========================
156
+
157
+ def start_lesson(image: Image.Image, text: str, state):
158
+ """Шаг 1: генерируем видео-лекцию с вопросом и вариантами ответа.
159
+ Возвращаем видео, текст вопроса, два варианта и сохраняем правильный ответ + путь к изображению в state."""
160
+ if image is None:
161
+ return None, "", [], [], state
162
+ if not text or not text.strip():
163
+ return None, "", [], [], state
164
+ if len(text) > 500:
165
+ return None, "", [], [], state
166
+
167
+ try:
168
+ # 1) Генерация вопроса
169
+ question, options, correct = generate_quiz(text)
170
+
171
+ # 2) Подготовить текст, который лектор произнесёт (вопрос + варианты)
172
+ quiz_ru = f"Вопрос: {question} Варианты: 1) {options[0]} 2) {options[1]}"
173
+
174
+ # 3) Синтез аудио для вопроса (на казахском внутри функции synthesize_audio)
175
+ audio_path = synthesize_audio(quiz_ru)
176
+
177
+ # 4) Сохранение фото во временный файл (чтобы передать в Talking Head API)
178
+ tmpimg = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
179
+ if image.mode != 'RGB':
180
+ image = image.convert('RGB')
181
+ image.save(tmpimg.name)
182
+ tmpimg.close()
183
+ image_path = tmpimg.name
184
+
185
+ # 5) Генерация видео через Talking Head
186
+ video_path = make_talking_head(image_path, audio_path)
187
+
188
+ # 6) Сохраняем в state необходимые значения (image_path и correct ответ)
189
+ state_data = {
190
+ 'image_path': image_path,
191
+ 'correct': correct,
192
+ 'options': options
193
+ }
194
+
195
+ # 7) Сообщение состояния: вернём question и варианты в RU для отображения
196
+ question_display = question
197
+
198
+ # Удалим audio временный файл (видео уже сгенерировано)
199
+ try:
200
+ if os.path.exists(audio_path):
201
+ os.remove(audio_path)
202
+ except:
203
+ pass
204
+
205
+ return video_path, question_display, options, state_data, state_data
206
 
207
  except Exception as e:
 
208
  traceback.print_exc()
209
+ return None, f"Ошибка: {e}", [], [], state
210
+
211
+
212
+ def answer_selected(selected_option: str, state):
213
+ """Шаг 2: пользователь выбирает вариант — генерируем реакцию лектора (короткая на казахском).
214
+ state должен содержать image_path и correct ответ."""
215
+ if not state:
216
+ return None, "Ошибка: отсутствует состояние урока. Сначала нажмите 'Запустить урок'."
217
+ try:
218
+ correct = state.get('correct')
219
+ image_path = state.get('image_path')
220
+ options = state.get('options', [])
221
+
222
+ if selected_option not in options:
223
+ # Иногда selected comes as index or value; try to handle
224
+ pass
225
 
226
+ if selected_option == correct:
227
+ reaction_ru = "Молодец!" # короткая реакция на русском — переведём на казахский в synthesize_audio
228
+ display_message = "Дұрыс!" # сообщение для интерфейса (можно сразу на казахском)
229
+ else:
230
+ reaction_ru = f"Неправильно. Правильный ответ: {correct}"
231
+ display_message = f"Қате. Дұрыс жауап: {correct}"
232
+
233
+ # Синтезируем реакцию (на казахском внутри)
234
+ audio_path = synthesize_audio(reaction_ru)
235
+
236
+ # Генерируем видео-реакцию с тем же изображением
237
+ reaction_video = make_talking_head(image_path, audio_path)
238
+
239
+ # Удаляем временный audio
240
+ try:
241
+ if os.path.exists(audio_path):
242
+ os.remove(audio_path)
243
+ except:
244
+ pass
245
 
246
+ return reaction_video, display_message
247
+
248
+ except Exception as e:
249
+ traceback.print_exc()
250
+ return None, f"Ошибка: {e}"
251
 
252
 
253
  # =========================
254
+ # Gradio UI (двухшаговый)
255
  # =========================
256
 
257
+ title = "🎓 Интерактивный бейне-лектор"
258
+
259
+ description = (
260
+ "Загрузите фото лектора и текст лекции (орыс тілінде, до 500 символов).
261
+ "
262
+ "Система создаст видео-лектора, задаст вопрос и предложит 2 варианта ответа.
263
+ "
264
+ "Нажмите на один из вариантов — лектор коротко отреагирует (қазақша)."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  )
266
 
267
+ with gr.Blocks() as demo:
268
+ gr.Markdown(f"# {title}
269
+ {description}")
270
+
271
+ with gr.Row():
272
+ with gr.Column(scale=1):
273
+ inp_image = gr.Image(type='pil', label='📸 Фото лектора')
274
+ inp_text = gr.Textbox(lines=5, label='📝 Текст лекции (рус.)', placeholder='Введите текст...')
275
+ btn_start = gr.Button("Запустить урок")
276
+
277
+ with gr.Column(scale=1):
278
+ out_video = gr.Video(label='🎬 Видео лектора')
279
+ out_question = gr.Markdown(label='Вопрос')
280
+ # Кнопки для двух вариантов; изначально пустые
281
+ btn_opt1 = gr.Button("Вариант 1")
282
+ btn_opt2 = gr.Button("Вариант 2")
283
+ out_reaction_video = gr.Video(label='🎥 Реакция лектора')
284
+ out_status = gr.Textbox(label='ℹ️ Статус', interactive=False)
285
+
286
+ # State для хранения данных между шагами
287
+ lesson_state = gr.State({})
288
+
289
+ # Привязки
290
+ btn_start.click(fn=start_lesson, inputs=[inp_image, inp_text, lesson_state], outputs=[out_video, out_question, btn_opt1, btn_opt2, lesson_state])
291
+
292
+ # Когда пользователь нажимает один из вариантов, вызываем answer_selected
293
+ btn_opt1.click(fn=answer_selected, inputs=[btn_opt1, lesson_state], outputs=[out_reaction_video, out_status])
294
+ btn_opt2.click(fn=answer_selected, inputs=[btn_opt2, lesson_state], outputs=[out_reaction_video, out_status])
295
+
296
+ # Небольшая подсказка при запуске
297
+ demo.load(lambda: "Готово", outputs=out_status)
298
+
299
+ if __name__ == '__main__':
300
+ demo.launch()