File size: 15,063 Bytes
2773547
 
b73ffcc
2773547
4c1bc35
b73ffcc
80ff644
b73ffcc
af30315
fdf8c85
80564a8
c5f3eef
 
80ff644
 
2790177
80ff644
64354df
4c1bc35
444e569
4adabcc
64354df
2790177
64354df
4c1bc35
444e569
af30315
 
80ff644
ee0187d
 
 
 
 
1873d97
80ff644
 
 
 
 
 
444e569
fdf8c85
 
444e569
fdf8c85
 
 
444e569
4c1bc35
444e569
80ff644
 
444e569
fdf8c85
64354df
7802c36
 
 
 
 
 
dccb835
aafa290
 
a2a87d6
 
 
f1b66ce
7802c36
f1b66ce
a2a87d6
 
 
 
 
194447e
aafa290
a2a87d6
 
 
 
f1b66ce
7802c36
f1b66ce
a2a87d6
 
 
 
 
 
 
 
7802c36
a2a87d6
f1b66ce
7802c36
f1b66ce
a2a87d6
 
 
 
 
 
 
 
aafa290
7802c36
a2a87d6
f1b66ce
aafa290
444e569
aafa290
444e569
aafa290
194447e
64354df
444e569
64354df
 
fdf8c85
64354df
f1b66ce
64354df
ee0187d
80ff644
64354df
f1b66ce
 
444e569
 
80ff644
444e569
 
 
 
64354df
c5f3eef
 
 
 
f1b66ce
c5f3eef
 
 
7802c36
c5f3eef
f1b66ce
c5f3eef
 
 
 
 
80564a8
444e569
80564a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99b37d4
64354df
444e569
64354df
2790177
7802c36
444e569
c5f3eef
64354df
 
165135e
444e569
 
 
2790177
 
444e569
64354df
c5f3eef
165135e
 
7802c36
 
f1b66ce
7802c36
 
 
f1b66ce
7802c36
c5f3eef
7802c36
 
f1b66ce
7802c36
c5f3eef
7802c36
 
f1b66ce
7802c36
 
f1b66ce
7802c36
 
64354df
7802c36
165135e
 
 
 
7802c36
165135e
64354df
7802c36
 
 
 
 
 
 
 
 
 
444e569
7802c36
165135e
7802c36
f1b66ce
c5f3eef
 
 
165135e
 
 
c5f3eef
64354df
2790177
7802c36
a0c5931
165135e
f1b66ce
444e569
 
7802c36
444e569
 
7802c36
 
444e569
7802c36
 
64354df
7802c36
 
64354df
7802c36
 
 
 
 
 
 
80ff644
444e569
 
 
165135e
4adabcc
80ff644
444e569
80ff644
236d20b
444e569
236d20b
 
7802c36
 
444e569
80ff644
165135e
444e569
64354df
 
444e569
236d20b
165135e
f1b66ce
 
236d20b
 
165135e
236d20b
64354df
444e569
236d20b
c5f3eef
165135e
c5f3eef
 
165135e
6270461
444e569
64354df
c5f3eef
2790177
444e569
 
c5f3eef
165135e
64354df
c5f3eef
165135e
 
c5f3eef
f1b66ce
165135e
 
c5f3eef
f1b66ce
165135e
 
 
c5f3eef
165135e
 
 
 
c5f3eef
165135e
64354df
444e569
80564a8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
import gradio as gr
import os
from PIL import Image
import tempfile
from gradio_client import Client, handle_file
import torch
from transformers import VitsModel, AutoTokenizer, pipeline
import scipy.io.wavfile as wavfile
import traceback
import random
import time
import numpy as np
from pydub import AudioSegment

# =========================
# Параметры
# =========================
TALKING_HEAD_SPACE = "Skywork/skyreels-a1-talking-head"
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

# =========================
# Загрузка моделей
# =========================
try:
    # TTS модель (казахский)
    tts_model = VitsModel.from_pretrained("facebook/mms-tts-kaz").to(device)
    tts_tokenizer = AutoTokenizer.from_pretrained("facebook/mms-tts-kaz")

    # Настройка конфигурации для более приятного и выразительного голоса
    tts_model.config.noise_scale = 0.5  # Меньше шума для чище голоса
    tts_model.config.noise_scale_duration = 0.8  # Вариация в длительности
    tts_model.config.speaking_rate = 0.9  # Чуть медленнее для выразительности

    # Перевод ru -> kk
    translator = pipeline(
        "translation",
        model="facebook/nllb-200-distilled-600M",
        device=0 if device == "cuda" else -1
    )

    # Модель для генерации вопросов
    qa_model = pipeline(
        "text2text-generation",
        model="google/flan-t5-small",
        device=0 if device == "cuda" else -1
    )

    print("✅ Все модели успешно загружены!")
except Exception as e:
    raise RuntimeError(f"❌ Ошибка загрузки моделей: {str(e)}")

# =========================
# Вспомогательные функции
# =========================
def generate_quiz(text: str):
    """ Генерирует один вопрос и два варианта ответа на основе текста.
    Алгоритмы:
    1. Базовый: случайное предложение и первые слова.
    2. Пропуск ключевого слова.
    3. Вопрос о числе/дате.
    """
    try:
        sentences = [s.strip() for s in text.replace("!", ".").replace("?", ".").split(".") if s.strip()]
        if len(sentences) < 1:
            raise ValueError("Текст слишком короткий")

        algo = random.choice([1, 2, 3])

        # ------------------------
        if algo == 1:  # Базовый алгоритм
            question_sentence = random.choice(sentences)
            words = question_sentence.split()
            if len(words) <= 3:
                correct_answer = question_sentence
                question = "Что сказано в этом предложении?"
            else:
                question = "Что сказано в тексте?"
                correct_answer = " ".join(words[:6]) + ("..." if len(words) > 6 else "")
            wrong_sentence = random.choice([s for s in sentences if s != question_sentence] or ["Другая информация"])
            wrong_words = wrong_sentence.split()
            wrong_answer = " ".join(wrong_words[:6]) + ("..." if len(wrong_words) > 6 else "")

        # ------------------------
        elif algo == 2:  # Пропуск ключевого слова
            question_sentence = random.choice(sentences)
            words = question_sentence.split()
            if len(words) > 2:
                key_word = random.choice(words)
                question = question_sentence.replace(key_word, "_____")
                correct_answer = key_word
                wrong_answer = random.choice([w for w in words if w != key_word] or ["другое"])
            else:
                # fallback
                return generate_quiz(text)

        # ------------------------
        elif algo == 3:  # Вопрос о числе или дате
            import re
            question_sentence = random.choice(sentences)
            numbers = re.findall(r'\d+', question_sentence)
            if numbers:
                number = random.choice(numbers)
                question = question_sentence.replace(number, "_____")
                correct_answer = number
                wrong_answer = str(int(number)+random.randint(1,5))
            else:
                # fallback к базовому
                return generate_quiz(text)

        options = [correct_answer, wrong_answer]
        random.shuffle(options)
        return question, options, correct_answer
    except Exception as e:
        raise ValueError(f"Ошибка генерации вопроса: {str(e)}")

def synthesize_audio(text_ru: str):
    """Переводит русскую строку на казахский, синтезирует аудио и возвращает путь к файлу .wav"""
    translation = translator(text_ru, src_lang="rus_Cyrl", tgt_lang="kaz_Cyrl")
    text_kk = translation[0]["translation_text"]

    inputs = tts_tokenizer(text_kk, return_tensors="pt").to(device)

    with torch.no_grad():
        output = tts_model(**inputs)

    waveform = output.waveform.squeeze().cpu().numpy()
    waveform /= np.max(np.abs(waveform)) + 1e-8  # Нормализация для лучшего качества

    audio = (waveform * 32767).astype('int16')
    sampling_rate = getattr(tts_model.config, 'sampling_rate', 22050)

    tmpf = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
    wavfile.write(tmpf.name, sampling_rate, audio)
    tmpf.close()
    return tmpf.name

def concatenate_audio_files(audio_files):
    """Объединяет несколько аудио файлов в один с паузами между ними"""
    combined = AudioSegment.empty()
    pause = AudioSegment.silent(duration=1000)  # 1 секунда паузы

    for i, audio_file in enumerate(audio_files):
        audio = AudioSegment.from_wav(audio_file)
        combined += audio
        if i < len(audio_files) - 1:  # Не добавляем паузу после последнего файла
            combined += pause

    output_file = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
    combined.export(output_file.name, format='wav')
    output_file.close()
    return output_file.name

def make_talking_head(image_path: str, audio_path: str, max_retries=3):
    """Вызывает SkyReels/Talking Head space и возвращает путь или URL видео."""
    for attempt in range(max_retries):
        try:
            client = Client(TALKING_HEAD_SPACE)
            result = client.predict(
                image_path=handle_file(image_path),
                audio_path=handle_file(audio_path),
                guidance_scale=3.0,
                steps=10,
                api_name="/process_image_audio"
            )
            print(f"Result type: {type(result)}")
            print(f"Result content: {result}")

            if isinstance(result, tuple):
                video_path = result[0]
                if isinstance(video_path, dict) and "video" in video_path:
                    return video_path["video"]
                elif isinstance(video_path, str):
                    return video_path
                else:
                    for item in result:
                        if isinstance(item, str) and (item.endswith('.mp4') or item.endswith('.webm') or os.path.exists(str(item))):
                            return item
                    raise ValueError(f"Не удалось найти видео в результате: {result}")
            elif isinstance(result, dict) and "video" in result:
                return result["video"]
            elif isinstance(result, str):
                return result
            else:
                raise ValueError(f"Unexpected talking head result: {type(result)}, value: {result}")
        except Exception as e:
            if attempt < max_retries - 1:
                print(f"Попытка {attempt + 1} не удалась: {e}. Повторяю через 2 секунды...")
                time.sleep(2)
            else:
                raise Exception(f"Ошибка после {max_retries} попыток: {str(e)}")

# =========================
# Основные обработчики для Gradio
# =========================
def start_lesson(image: Image.Image, text: str, state):
    """Создает одно видео: текст лекции + вопрос с вариантами ответа"""
    if image is None or not text.strip() or len(text) > 500:
        return None, "Пожалуйста, загрузите фото и введите текст лекции (до 500 символов)", gr.update(visible=False), gr.update(visible=False), state

    try:
        # Сохраняем изображение
        tmpimg = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
        if image.mode != 'RGB':
            image = image.convert('RGB')
        image.save(tmpimg.name)
        tmpimg.close()
        image_path = tmpimg.name

        # Генерируем вопрос
        question, options, correct = generate_quiz(text)

        # Создаем три аудио файла
        audio_files = []

        # 1. Текст лекции
        audio1 = synthesize_audio(text)
        audio_files.append(audio1)

        # 2. Вопрос
        question_text = f"А теперь вопрос: {question}"
        audio2 = synthesize_audio(question_text)
        audio_files.append(audio2)

        # 3. Варианты ответа
        options_text = f"Первый вариант: {options[0]}. Второй вариант: {options[1]}"
        audio3 = synthesize_audio(options_text)
        audio_files.append(audio3)

        # Объединяем все аудио в одно
        combined_audio = concatenate_audio_files(audio_files)

        # Создаем одно видео с полным содержанием
        video_path = make_talking_head(image_path, combined_audio)

        # Сохраняем состояние
        state_data = {
            'image_path': image_path,
            'correct': correct,
            'options': options,
            'question': question
        }

        # Удаляем временные аудио файлы
        for audio_file in audio_files:
            try:
                os.remove(audio_file)
            except:
                pass
        try:
            os.remove(combined_audio)
        except:
            pass

        question_display = f"**Вопрос:** {question}"
        return (
            video_path,
            question_display,
            gr.update(value=options[0], visible=True),
            gr.update(value=options[1], visible=True),
            state_data
        )
    except Exception as e:
        traceback.print_exc()
        return None, f"❌ Ошибка: {e}", gr.update(visible=False), gr.update(visible=False), state

def answer_selected(selected_option: str, state):
    """Генерирует реакцию лектора и показывает в том же окне"""
    if not state:
        return None, "❌ Ошибка: отсутствует состояние урока"

    try:
        correct = state.get('correct')
        image_path = state.get('image_path')

        if selected_option == correct:
            reaction_ru = "Правильно! Отлично справились!"
            display_message = "✅ **Дұрыс! Жарайсың!**"
        else:
            reaction_ru = f"К сожалению неправильно. Правильный ответ был: {correct}"
            display_message = f"❌ **Қате!** Дұрыс жауап: **{correct}**"

        # Создаем аудио с реакцией
        audio_path = synthesize_audio(reaction_ru)

        # Создаем видео с реакцией
        reaction_video = make_talking_head(image_path, audio_path)

        try:
            os.remove(audio_path)
        except:
            pass

        return reaction_video, display_message
    except Exception as e:
        traceback.print_exc()
        return None, f"❌ Ошибка: {e}"

# =========================
# Gradio UI
# =========================
title = "🎓 Интерактивті Бейне Мұғалім TiлГен"
description = (
    "**Қалай жұмыс істейді:**\n"
    "1. Мұғалімнің суретін жүктеп, дәріс мәтінін енгізіңіз (орыс, 500 таңбаға дейін)\n"
    "2. 'Сабақты бастау' түймесін басыңыз-мұғалім мәтінді оқып, сұрақ қояды\n"
    "3. Дұрыс жауапты таңдаңыз-мұғалім сіздің жауабыңызға жауап береді"
)

with gr.Blocks(theme=gr.themes.Soft()) as demo:
    gr.Markdown(f"# {title}\n{description}")

    with gr.Row():
        with gr.Column(scale=1):
            inp_image = gr.Image(type='pil', label='📸 Мұғалімнің суреті')
            inp_text = gr.Textbox(
                lines=5,
                label='📝 Дәріс мәтіні (орыс.)',
                placeholder='Дәріс мәтінін енгізіңіз...',
                info="Ең көбі 500 таңба"
            )
            btn_start = gr.Button("🚀 Сабақты бастау", variant="primary", size="lg")

        with gr.Column(scale=1):
            out_video = gr.Video(label='🎬 Мұғалімнің видеосы')
            out_question = gr.Markdown("")
            with gr.Row():
                btn_opt1 = gr.Button("Вариант 1", visible=False, size="lg", variant="secondary")
                btn_opt2 = gr.Button("Вариант 2", visible=False, size="lg", variant="secondary")
            out_result = gr.Markdown("")

    lesson_state = gr.State({})

    # Запуск урока
    btn_start.click(
        fn=start_lesson,
        inputs=[inp_image, inp_text, lesson_state],
        outputs=[out_video, out_question, btn_opt1, btn_opt2, lesson_state]
    )

    # Обработка ответов
    def handle_answer_1(state):
        option = state.get('options', [''])[0] if state else ''
        return answer_selected(option, state)

    def handle_answer_2(state):
        option = state.get('options', [''])[1] if state and len(state.get('options', [])) > 1 else ''
        return answer_selected(option, state)

    btn_opt1.click(
        fn=handle_answer_1,
        inputs=[lesson_state],
        outputs=[out_video, out_result]
    )
    btn_opt2.click(
        fn=handle_answer_2,
        inputs=[lesson_state],
        outputs=[out_video, out_result]
    )

if __name__ == '__main__':
    demo.launch(server_name="0.0.0.0", server_port=7860)