AlserFurma commited on
Commit
30093d4
·
verified ·
1 Parent(s): ee9f075

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +645 -770
app.py CHANGED
@@ -7,787 +7,662 @@ import base64
7
  from pathlib import Path
8
 
9
  try:
10
- from moviepy.editor import VideoFileClip
11
- MOVIEPY_AVAILABLE = True
12
  except ImportError:
13
- print("MoviePy не установлен, используем альтернативный метод")
14
- MOVIEPY_AVAILABLE = False
15
 
16
  # Глобальная переменная для хранения созданных уроков
17
  current_lesson_dir = None
18
 
19
  def add_segment(lecture_video, correct_video, wrong_video, option1_text, option2_text, correct_option, segments_state):
20
- """Добавляет сегмент: видео лекции + видео реакций + текст вариантов"""
21
- if lecture_video is None or correct_video is None or wrong_video is None:
22
- return segments_state, "⚠️ Загрузите все три видео (лекция, правильная реакция, неправильная реакция)", None, None, None
23
-
24
- if not option1_text or not option2_text:
25
- return segments_state, "⚠️ Введите оба варианта ответа", None, None, None
26
-
27
- if correct_option is None:
28
- return segments_state, "⚠️ Выберите правильный вариант", None, None, None
29
-
30
- # Получаем длительность лекции
31
- try:
32
- if MOVIEPY_AVAILABLE:
33
- clip = VideoFileClip(lecture_video)
34
- duration = clip.duration
35
- clip.close()
36
- else:
37
- # Альтернативный метод через subprocess
38
- import subprocess
39
- result = subprocess.run(
40
- ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
41
- '-of', 'default=noprint_wrappers=1:nokey=1', lecture_video],
42
- stdout=subprocess.PIPE,
43
- stderr=subprocess.STDOUT
44
- )
45
- duration = float(result.stdout)
46
- except Exception as e:
47
- print(f"Ошибка получения длительности: {e}")
48
- duration = 10 # Значение по умолчанию
49
-
50
- # Сохраняем оригинальные пути
51
- new_segment = {
52
- 'lecture_path': lecture_video,
53
- 'correct_path': correct_video,
54
- 'wrong_path': wrong_video,
55
- 'options': [option1_text, option2_text],
56
- 'correct': 0 if correct_option == "Вариант 1" else 1,
57
- 'duration': duration
58
- }
59
-
60
- segments_state.append(new_segment)
61
-
62
- info = f"✅ **Всего сегментов: {len(segments_state)}**\n\n"
63
- for i, seg in enumerate(segments_state, 1):
64
- info += f"**Сегмент {i}:** ({seg['duration']:.1f} сек)\n"
65
- info += f"- 🎯 Вариант 1: {seg['options'][0]}\n"
66
- info += f"- 🎯 Вариант 2: {seg['options'][1]}\n"
67
- info += f"- ✅ Правильный: Вариант {seg['correct'] + 1}\n\n"
68
-
69
- # Очищаем поля ввода
70
- return segments_state, info, None, None, None
71
 
72
  def create_lesson(segments_state):
73
- """Подготавливает урок - копирует файлы и создает метаданные"""
74
- global current_lesson_dir
75
-
76
- if not segments_state:
77
- return "⚠️ Нет добавленных сегментов", None
78
-
79
- # Создаем временную директорию для урока
80
- lesson_dir = tempfile.mkdtemp(prefix="lesson_")
81
- current_lesson_dir = lesson_dir # Сохраняем глобально
82
-
83
- timestamps = []
84
-
85
- for i, seg in enumerate(segments_state):
86
- # Копируем видео в директорию урока
87
- lecture_new = os.path.join(lesson_dir, f"lecture_{i}.mp4")
88
- correct_new = os.path.join(lesson_dir, f"correct_{i}.mp4")
89
- wrong_new = os.path.join(lesson_dir, f"wrong_{i}.mp4")
90
-
91
- # Копируем файлы
92
- shutil.copy2(seg['lecture_path'], lecture_new)
93
- shutil.copy2(seg['correct_path'], correct_new)
94
- shutil.copy2(seg['wrong_path'], wrong_new)
95
-
96
- timestamps.append({
97
- 'index': i,
98
- 'lecture': f"lecture_{i}.mp4", # Только имя файла
99
- 'correct': f"correct_{i}.mp4",
100
- 'wrong': f"wrong_{i}.mp4",
101
- 'duration': seg['duration'],
102
- 'options': seg['options'],
103
- 'correct': seg['correct'] # Это 0 или 1, не имя файла!
104
- })
105
-
106
- # Сохраняем метаданные
107
- metadata = {
108
- 'timestamps': timestamps,
109
- 'total_segments': len(segments_state),
110
- 'lesson_dir': lesson_dir # Сохраняем путь к директории
111
- }
112
-
113
- meta_path = os.path.join(lesson_dir, 'metadata.json')
114
- with open(meta_path, 'w', encoding='utf-8') as f:
115
- json.dump(metadata, f, ensure_ascii=False, indent=2)
116
-
117
- total_duration = sum(seg['duration'] for seg in segments_state)
118
-
119
- return (f"✅ Урок создан!\n- Всего сегментов: {len(segments_state)}\n- Примерная длительность: {total_duration:.1f} сек\n- Путь: {lesson_dir}",
120
- meta_path)
121
 
122
  def generate_player_html(meta_path):
123
- """Генерирует HTML плеер с интерактивными кнопками"""
124
- global current_lesson_dir
125
-
126
- if not meta_path or not os.path.exists(meta_path):
127
- return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Сначала создайте урок!</p>"
128
-
129
- with open(meta_path, 'r', encoding='utf-8') as f:
130
- metadata = json.load(f)
131
-
132
- # Используем сохраненную директорию
133
- lesson_dir = metadata.get('lesson_dir', current_lesson_dir)
134
- if not lesson_dir or not os.path.exists(lesson_dir):
135
- return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Файлы урока не найдены. Создайте урок заново.</p>"
136
-
137
- # Конвертируем все видео в base64
138
- video_data = {}
139
-
140
- for segment in metadata['timestamps']:
141
- idx = segment['index']
142
-
143
- # Используем правильные имена файлов из метаданных
144
- lecture_file = segment['lecture']
145
- correct_file = segment['correct']
146
- wrong_file = segment['wrong']
147
-
148
- lecture_path = os.path.join(lesson_dir, lecture_file)
149
- correct_path = os.path.join(lesson_dir, correct_file)
150
- wrong_path = os.path.join(lesson_dir, wrong_file)
151
-
152
- try:
153
- with open(lecture_path, 'rb') as f:
154
- video_data[f'lecture_{idx}'] = base64.b64encode(f.read()).decode()
155
-
156
- with open(correct_path, 'rb') as f:
157
- video_data[f'correct_{idx}'] = base64.b64encode(f.read()).decode()
158
-
159
- with open(wrong_path, 'rb') as f:
160
- video_data[f'wrong_{idx}'] = base64.b64encode(f.read()).decode()
161
- except Exception as e:
162
- print(f"Ошибка чтения файла: {e}")
163
- return f"<p style='color: red; text-align: center; padding: 40px;'>⚠️ Ошибка загрузки видео: {str(e)}</p>"
164
-
165
- # Создаем структуру данных для JavaScript
166
- segments_json = json.dumps([{
167
- 'index': seg['index'],
168
- 'duration': seg['duration'],
169
- 'options': seg['options'],
170
- 'correct': seg['correct'] # Это целое число (0 или 1), не имя файла
171
- } for seg in metadata['timestamps']], ensure_ascii=False)
172
-
173
- # Создаем словарь с видео для JavaScript
174
- videos_json = json.dumps(video_data)
175
-
176
- html = f"""
177
- <!DOCTYPE html>
178
- <html lang="ru">
179
- <head>
180
- <meta charset="UTF-8">
181
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
182
- <title>Интерактивный Урок</title>
183
- <style>
184
- * {{
185
- margin: 0;
186
- padding: 0;
187
- box-sizing: border-box;
188
- }}
189
-
190
- body {{
191
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
192
- background: #f5f5f5;
193
- padding: 20px;
194
- }}
195
-
196
- .player-wrapper {{
197
- width: 100%;
198
- max-width: 900px;
199
- margin: 0 auto;
200
- background: white;
201
- border-radius: 16px;
202
- overflow: hidden;
203
- box-shadow: 0 10px 40px rgba(0,0,0,0.1);
204
- }}
205
-
206
- .player-container {{
207
- position: relative;
208
- width: 100%;
209
- background: #000;
210
- overflow: hidden;
211
- }}
212
-
213
- #mainVideo {{
214
- width: 100%;
215
- height: auto;
216
- display: block;
217
- max-height: 600px;
218
- }}
219
-
220
- .quiz-overlay {{
221
- position: absolute;
222
- bottom: 0;
223
- left: 0;
224
- right: 0;
225
- background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.8) 50%, transparent 100%);
226
- padding: 40px 30px 30px;
227
- display: none;
228
- }}
229
-
230
- .quiz-question {{
231
- color: #ffffff;
232
- font-size: 22px;
233
- font-weight: 600;
234
- text-align: center;
235
- margin-bottom: 25px;
236
- text-shadow: 0 2px 4px rgba(0,0,0,0.5);
237
- }}
238
-
239
- .quiz-options {{
240
- display: flex;
241
- gap: 15px;
242
- flex-direction: column;
243
- max-width: 600px;
244
- margin: 0 auto;
245
- }}
246
-
247
- .option-btn {{
248
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
249
- color: white;
250
- border: none;
251
- padding: 20px 35px;
252
- border-radius: 12px;
253
- font-size: 18px;
254
- font-weight: 500;
255
- cursor: pointer;
256
- transition: all 0.3s;
257
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
258
- }}
259
-
260
- .option-btn:hover {{
261
- transform: translateY(-3px);
262
- box-shadow: 0 6px 25px rgba(102, 126, 234, 0.6);
263
- }}
264
-
265
- .option-btn:active {{
266
- transform: translateY(-1px);
267
- }}
268
-
269
- .timer {{
270
- color: #ffd700;
271
- font-size: 16px;
272
- font-weight: 600;
273
- text-align: center;
274
- margin-top: 20px;
275
- text-shadow: 0 2px 4px rgba(0,0,0,0.5);
276
- }}
277
-
278
- .timer.warning {{
279
- color: #ff6b6b;
280
- animation: pulse 1s infinite;
281
- }}
282
-
283
- @keyframes pulse {{
284
- 0% {{ opacity: 1; }}
285
- 50% {{ opacity: 0.7; }}
286
- 100% {{ opacity: 1; }}
287
- }}
288
-
289
- .progress-bar {{
290
- position: absolute;
291
- top: 0;
292
- left: 0;
293
- height: 4px;
294
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
295
- width: 0%;
296
- transition: width 0.1s linear;
297
- z-index: 10;
298
- }}
299
-
300
- .segment-info {{
301
- position: absolute;
302
- top: 15px;
303
- right: 15px;
304
- background: rgba(0,0,0,0.7);
305
- color: white;
306
- padding: 8px 16px;
307
- border-radius: 20px;
308
- font-size: 14px;
309
- font-weight: 500;
310
- z-index: 10;
311
- }}
312
-
313
- .loading {{
314
- position: absolute;
315
- top: 50%;
316
- left: 50%;
317
- transform: translate(-50%, -50%);
318
- color: white;
319
- font-size: 18px;
320
- display: none;
321
- z-index: 20;
322
- }}
323
-
324
- .controls {{
325
- padding: 20px;
326
- background: #1a1a1a;
327
- display: flex;
328
- align-items: center;
329
- gap: 20px;
330
- }}
331
-
332
- .play-pause-btn {{
333
- background: #667eea;
334
- border: none;
335
- color: white;
336
- width: 50px;
337
- height: 50px;
338
- border-radius: 50%;
339
- cursor: pointer;
340
- display: flex;
341
- align-items: center;
342
- justify-content: center;
343
- transition: all 0.3s;
344
- font-size: 18px;
345
- }}
346
-
347
- .play-pause-btn:hover {{
348
- background: #764ba2;
349
- transform: scale(1.1);
350
- }}
351
-
352
- .time-display {{
353
- color: #ffffff;
354
- font-size: 16px;
355
- font-weight: 500;
356
- flex-grow: 1;
357
- }}
358
-
359
- .lesson-progress {{
360
- color: #aaa;
361
- font-size: 14px;
362
- margin-left: 10px;
363
- }}
364
- </style>
365
- </head>
366
- <body>
367
- <div class="player-wrapper">
368
- <div class="player-container">
369
- <div class="progress-bar" id="progressBar"></div>
370
- <div class="segment-info" id="segmentInfo">Сегмент 1 из {metadata['total_segments']}</div>
371
- <div class="loading" id="loading">Загрузка...</div>
372
-
373
- <video id="mainVideo" preload="auto">
374
- Ваш браузер не поддерживает видео
375
- </video>
376
-
377
- <div class="quiz-overlay" id="quizOverlay">
378
- <div class="quiz-question">Выберите правильный ответ:</div>
379
- <div class="quiz-options">
380
- <button class="option-btn" id="option1"></button>
381
- <button class="option-btn" id="option2"></button>
382
- </div>
383
- <div class="timer" id="timer">⏱ Времени осталось: <span id="timeLeft">7</span> сек</div>
384
- </div>
385
- </div>
386
-
387
- <div class="controls">
388
- <button class="play-pause-btn" id="playPauseBtn">▶</button>
389
- <div class="time-display" id="timeDisplay">0:00</div>
390
- <div class="lesson-progress" id="lessonProgress">1/{metadata['total_segments']}</div>
391
- </div>
392
- </div>
393
-
394
- <script>
395
- const mainVideo = document.getElementById('mainVideo');
396
- const quizOverlay = document.getElementById('quizOverlay');
397
- const option1Btn = document.getElementById('option1');
398
- const option2Btn = document.getElementById('option2');
399
- const timerDisplay = document.getElementById('timeLeft');
400
- const progressBar = document.getElementById('progressBar');
401
- const segmentInfo = document.getElementById('segmentInfo');
402
- const loadingDiv = document.getElementById('loading');
403
- const playPauseBtn = document.getElementById('playPauseBtn');
404
- const timeDisplay = document.getElementById('timeDisplay');
405
- const timerElement = document.getElementById('timer');
406
- const lessonProgress = document.getElementById('lessonProgress');
407
-
408
- const segments = {segments_json};
409
- const videosBase64 = {videos_json};
410
-
411
- let currentSegmentIndex = 0;
412
- let isPlayingReaction = false;
413
- let quizShown = false;
414
- let answered = false;
415
- let timerInterval = null;
416
- let timeRemaining = 7;
417
-
418
- function formatTime(seconds) {{
419
- const mins = Math.floor(seconds / 60);
420
- const secs = Math.floor(seconds % 60);
421
- return mins + ':' + (secs < 10 ? '0' : '') + secs;
422
- }}
423
-
424
- function updateSegmentInfo() {{
425
- segmentInfo.textContent = 'Сегмент ' + (currentSegmentIndex + 1) + ' из ' + segments.length;
426
- lessonProgress.textContent = (currentSegmentIndex + 1) + '/' + segments.length;
427
- }}
428
-
429
- function showLoading() {{
430
- loadingDiv.style.display = 'block';
431
- }}
432
-
433
- function hideLoading() {{
434
- loadingDiv.style.display = 'none';
435
- }}
436
-
437
- function loadVideo(base64Data) {{
438
- return new Promise(function(resolve, reject) {{
439
- showLoading();
440
- mainVideo.src = 'data:video/mp4;base64,' + base64Data;
441
- mainVideo.load();
442
-
443
- mainVideo.onloadeddata = function() {{
444
- hideLoading();
445
- resolve();
446
- }};
447
-
448
- mainVideo.onerror = function() {{
449
- hideLoading();
450
- reject(new Error('Ошибка загрузки видео'));
451
- }};
452
- }});
453
- }}
454
-
455
- function showQuiz(segment) {{
456
- quizOverlay.style.display = 'block';
457
- option1Btn.textContent = segment.options[0];
458
- option2Btn.textContent = segment.options[1];
459
- quizShown = true;
460
- answered = false;
461
- timeRemaining = 7;
462
- timerDisplay.textContent = timeRemaining;
463
-
464
- if (timerInterval) {{
465
- clearInterval(timerInterval);
466
- }}
467
-
468
- timerInterval = setInterval(function() {{
469
- timeRemaining--;
470
- timerDisplay.textContent = timeRemaining;
471
-
472
- if (timeRemaining <= 3) {{
473
- timerElement.classList.add('warning');
474
- }} else {{
475
- timerElement.classList.remove('warning');
476
- }}
477
-
478
- if (timeRemaining <= 0) {{
479
- clearInterval(timerInterval);
480
- if (!answered) {{
481
- handleAnswer(-1, segment);
482
- }}
483
- }}
484
- }}, 1000);
485
- }}
486
-
487
- function hideQuiz() {{
488
- quizOverlay.style.display = 'none';
489
- quizShown = false;
490
- timerElement.classList.remove('warning');
491
- if (timerInterval) {{
492
- clearInterval(timerInterval);
493
- timerInterval = null;
494
- }}
495
- }}
496
-
497
- function playReaction(isCorrect) {{
498
- return new Promise(function(resolve) {{
499
- isPlayingReaction = true;
500
- hideQuiz();
501
-
502
- const segment = segments[currentSegmentIndex];
503
- const reactionKey = isCorrect ? ('correct_' + segment.index) : ('wrong_' + segment.index);
504
-
505
- loadVideo(videosBase64[reactionKey]).then(function() {{
506
- mainVideo.play();
507
-
508
- mainVideo.onended = function() {{
509
- isPlayingReaction = false;
510
- mainVideo.onended = null;
511
- resolve();
512
- }};
513
- }}).catch(function(error) {{
514
- console.error('Ошибка воспроизведения реакции:', error);
515
- isPlayingReaction = false;
516
- resolve();
517
- }});
518
- }});
519
- }}
520
-
521
- function loadNextSegment() {{
522
- if (currentSegmentIndex >= segments.length) {{
523
- alert('🎉 Поздравляем! Вы завершили урок!');
524
- currentSegmentIndex = 0;
525
- updateSegmentInfo();
526
- return;
527
- }}
528
-
529
- const segment = segments[currentSegmentIndex];
530
- const lectureKey = 'lecture_' + segment.index;
531
-
532
- loadVideo(videosBase64[lectureKey]).then(function() {{
533
- updateSegmentInfo();
534
- mainVideo.play();
535
- playPauseBtn.textContent = '';
536
- }}).catch(function(error) {{
537
- console.error('Ошибка загрузки лекции:', error);
538
- alert('Ошибка загрузки видео');
539
- }});
540
- }}
541
-
542
- function handleAnswer(choice, segment) {{
543
- if (answered) return;
544
- answered = true;
545
-
546
- const isCorrect = choice === segment.correct;
547
-
548
- if (isCorrect) {{
549
- console.log('✅ Правильный ответ!');
550
- }} else {{
551
- console.log('❌ Неправильный ответ');
552
- }}
553
-
554
- playReaction(isCorrect).then(function() {{
555
- currentSegmentIndex++;
556
-
557
- if (currentSegmentIndex < segments.length) {{
558
- loadNextSegment();
559
- }} else {{
560
- alert('🎉 Поздравляем! Вы завершили урок!');
561
- }}
562
- }});
563
- }}
564
-
565
- option1Btn.addEventListener('click', function() {{
566
- if (currentSegmentIndex >= 0 && currentSegmentIndex < segments.length && !answered) {{
567
- handleAnswer(0, segments[currentSegmentIndex]);
568
- }}
569
- }});
570
-
571
- option2Btn.addEventListener('click', function() {{
572
- if (currentSegmentIndex >= 0 && currentSegmentIndex < segments.length && !answered) {{
573
- handleAnswer(1, segments[currentSegmentIndex]);
574
- }}
575
- }});
576
-
577
- mainVideo.addEventListener('timeupdate', function() {{
578
- if (isPlayingReaction) return;
579
-
580
- const currentTime = mainVideo.currentTime;
581
- const duration = mainVideo.duration;
582
-
583
- if (!isNaN(currentTime)) {{
584
- timeDisplay.textContent = formatTime(currentTime);
585
- }}
586
-
587
- if (duration > 0 && !isNaN(duration)) {{
588
- const progress = (currentTime / duration) * 100;
589
- progressBar.style.width = progress + '%';
590
-
591
- const timeLeft = duration - currentTime;
592
-
593
- if (!quizShown && timeLeft <= 7 && timeLeft > 0 && !answered) {{
594
- showQuiz(segments[currentSegmentIndex]);
595
- }}
596
- }}
597
- }});
598
-
599
- playPauseBtn.addEventListener('click', function() {{
600
- if (mainVideo.paused) {{
601
- mainVideo.play();
602
- playPauseBtn.textContent = '⏸';
603
- }} else {{
604
- mainVideo.pause();
605
- playPauseBtn.textContent = '▶';
606
- }}
607
- }});
608
-
609
- mainVideo.addEventListener('play', function() {{
610
- playPauseBtn.textContent = '⏸';
611
- }});
612
-
613
- mainVideo.addEventListener('pause', function() {{
614
- playPauseBtn.textContent = '▶';
615
- }});
616
-
617
- // Запускаем первый сегмент
618
- loadNextSegment();
619
- </script>
620
- </body>
621
- </html>
622
- """
623
-
624
- return html
625
 
626
  # Создаем интерфейс
627
- with gr.Blocks(title="Интерактивный Урок", theme=gr.themes.Soft()) as demo:
628
- gr.Markdown("""
629
- # 🎓 Интерактивный Урок (Coursera-style)
630
-
631
- ### Инструкция:
632
- 1. **Загрузите 3 видео для каждого сегмента:**
633
- - 🎥 Видео лекции (в конце которого задается вопрос)
634
- - ✅ Видео реакции на правильный ответ
635
- - ❌ Видео реакции на неправильный ответ
636
- 2. **Введите варианты ответов** и выберите правильный
637
- 3. Нажмите **"Добавить Сегмент"** (повторите для каждого фрагмента)
638
- 4. Когда все сегменты добавлены, нажмите **"Создать Урок"**
639
- 5. Нажмите **"Запустить Плеер"** для прохождения урока
640
-
641
- ---
642
- """)
643
-
644
- # Состояния
645
- segments_state = gr.State([])
646
- meta_state = gr.State(None)
647
-
648
- with gr.Tab("📝 Создание Урока"):
649
- with gr.Row():
650
- with gr.Column(scale=2):
651
- gr.Markdown("### 1. Загрузите видео")
652
- lecture_input = gr.Video(
653
- label="🎥 Видео Лекции",
654
- info="Основное видео лекции с вопросом в конце"
655
- )
656
- with gr.Row():
657
- correct_input = gr.Video(
658
- label="✅ Реакция: Правильный ответ",
659
- info="Короткое видео реакции лектора на правильный ответ"
660
- )
661
- wrong_input = gr.Video(
662
- label="❌ Реакция: Неправильный ответ",
663
- info="Короткое видео реакции лектора на неправильный ответ"
664
- )
665
-
666
- with gr.Column(scale=1):
667
- gr.Markdown("### 2. Настройте вопрос")
668
- option1_input = gr.Textbox(
669
- label="Вариант ответа 1",
670
- placeholder="Например: Париж",
671
- info="Первый вариант ответа"
672
- )
673
- option2_input = gr.Textbox(
674
- label="Вариант ответа 2",
675
- placeholder="Например: Лондон",
676
- info="Второй вариант ответа"
677
- )
678
- correct_radio = gr.Radio(
679
- ["Вариант 1", "Вариант 2"],
680
- label="✅ Правильный вариант",
681
- value="Вариант 1",
682
- info="Выберите правильный вариант ответа"
683
- )
684
-
685
- add_btn = gr.Button(
686
- "➕ Добавить Сегмент",
687
- variant="primary",
688
- size="lg"
689
- )
690
-
691
- gr.Markdown("---")
692
-
693
- segments_info = gr.Markdown(
694
- "### 📋 Добавленные сегменты\n\n*Нет добавленных сегментов*"
695
- )
696
-
697
- gr.Markdown("---")
698
-
699
- with gr.Row():
700
- create_btn = gr.Button(
701
- "🎬 Создать Урок",
702
- variant="primary",
703
- size="lg"
704
- )
705
- create_status = gr.Markdown("**Статус:** Урок еще не создан")
706
-
707
- add_btn.click(
708
- fn=add_segment,
709
- inputs=[lecture_input, correct_input, wrong_input, option1_input, option2_input, correct_radio, segments_state],
710
- outputs=[segments_state, segments_info, lecture_input, correct_input, wrong_input]
711
- )
712
-
713
- create_btn.click(
714
- fn=create_lesson,
715
- inputs=[segments_state],
716
- outputs=[create_status, meta_state]
717
- )
718
-
719
- with gr.Tab("▶️ Прохождение Урока"):
720
- gr.Markdown("""
721
- ### Как проходить урок:
722
-
723
- - 🎬 **Видео проигрывается автоматически**
724
- - ⏱ **За 7 секунд** до конца появятся кнопки с вариантами ответа
725
- - 🎯 **Выберите правильный ответ** до окончания времени (7 секунд)
726
- - ✅ **После ответа** покажется виде�� реакции лектора
727
- - ➡️ **Затем автоматически** начнется следующий сегмент
728
- - 🏆 **Пройдите все сегменты** до конца!
729
-
730
- ---
731
- """)
732
-
733
- start_btn = gr.Button(
734
- "🚀 Запустить Плеер",
735
- variant="primary",
736
- size="lg"
737
- )
738
-
739
- player_html = gr.HTML(
740
- value="<div style='text-align: center; padding: 40px; color: #666;'>Нажмите 'Запустить Плеер', чтобы начать урок</div>"
741
- )
742
-
743
- start_btn.click(
744
- fn=generate_player_html,
745
- inputs=[meta_state],
746
- outputs=[player_html]
747
- )
748
-
749
- with gr.Tab("ℹ️ Информация"):
750
- gr.Markdown("""
751
- ### 📚 О приложении
752
-
753
- **Интерактивный Урок** - это система для создания и прохождения образовательных видеоуроков в стиле Coursera.
754
-
755
- ### ✨ Особенности:
756
-
757
- - 🎥 **Интерактивные видео** с вопросами
758
- - ⏱ **Таймер ответа** (7 секунд)
759
- - ✅ **Мгновенная обратная связь** через видео реакции
760
- - 📊 **Пошаговое прохождение** уроков
761
- - 🎨 **Современный интерфейс** с анимациями
762
-
763
- ### 🛠 Технические требования:
764
-
765
- - Видео в формате **MP4**
766
- - Размер каждого видео: **до 500 MB**
767
- - **Оптимальная длина** видео лекции: 30-120 секунд
768
- - **Оптимальная длина** видео реакций: 5-15 секунд
769
-
770
- ### 🔧 Установка зависимостей:
771
-
772
- ```bash
773
- pip install gradio moviepy
774
- ```
775
-
776
- Для работы с видео также рекомендуется установить ffmpeg.
777
-
778
- ### 📁 Структура урока:
779
-
780
- 1. **Видео лекции** - основной контент
781
- 2. **Видео реакции на правильный ответ**
782
- 3. **Видео реакции на неправильный ответ**
783
- 4. **Варианты ответов** (2 варианта)
784
- 5. **Правильный ответ** (выбирается при создании)
785
- """)
786
 
787
  if __name__ == '__main__':
788
- demo.launch(
789
- server_name="0.0.0.0",
790
- server_port=7860,
791
- share=False,
792
- debug=True
793
- )
 
7
  from pathlib import Path
8
 
9
  try:
10
+ from moviepy.editor import VideoFileClip
11
+ MOVIEPY_AVAILABLE = True
12
  except ImportError:
13
+ print("MoviePy не установлен, используем альтернативный метод")
14
+ MOVIEPY_AVAILABLE = False
15
 
16
  # Глобальная переменная для хранения созданных уроков
17
  current_lesson_dir = None
18
 
19
  def add_segment(lecture_video, correct_video, wrong_video, option1_text, option2_text, correct_option, segments_state):
20
+ """Добавляет сегмент: видео лекции + видео реакций + текст вариантов"""
21
+ if lecture_video is None or correct_video is None or wrong_video is None:
22
+ return segments_state, "⚠️ Загрузите все три видео (лекция, правильная реакция, неправильная реакция)", None, None, None
23
+
24
+ if not option1_text or not option2_text:
25
+ return segments_state, "⚠️ Введите оба варианта ответа", None, None, None
26
+
27
+ if correct_option is None:
28
+ return segments_state, "⚠️ Выберите правильный вариант", None, None, None
29
+
30
+ # Получаем длительность лекции
31
+ try:
32
+ if MOVIEPY_AVAILABLE:
33
+ clip = VideoFileClip(lecture_video)
34
+ duration = clip.duration
35
+ clip.close()
36
+ else:
37
+ # Альтернативный метод через subprocess
38
+ import subprocess
39
+ result = subprocess.run(
40
+ ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
41
+ '-of', 'default=noprint_wrappers=1:nokey=1', lecture_video],
42
+ stdout=subprocess.PIPE,
43
+ stderr=subprocess.STDOUT
44
+ )
45
+ duration = float(result.stdout)
46
+ except Exception as e:
47
+ print(f"Ошибка получения длительности: {e}")
48
+ duration = 10 # Значение по умолчанию
49
+
50
+ # Сохраняем оригинальные пути
51
+ new_segment = {
52
+ 'lecture_path': lecture_video,
53
+ 'correct_path': correct_video,
54
+ 'wrong_path': wrong_video,
55
+ 'options': [option1_text, option2_text],
56
+ 'correct': 0 if correct_option == "Вариант 1" else 1,
57
+ 'duration': duration
58
+ }
59
+
60
+ segments_state.append(new_segment)
61
+
62
+ info = f"✅ **Всего сегментов: {len(segments_state)}**\n\n"
63
+ for i, seg in enumerate(segments_state, 1):
64
+ info += f"**Сегмент {i}:** ({seg['duration']:.1f} сек)\n"
65
+ info += f"- 🎯 Вариант 1: {seg['options'][0]}\n"
66
+ info += f"- 🎯 Вариант 2: {seg['options'][1]}\n"
67
+ info += f"- ✅ Правильный: Вариант {seg['correct'] + 1}\n\n"
68
+
69
+ # Очищаем поля ввода
70
+ return segments_state, info, None, None, None
71
 
72
  def create_lesson(segments_state):
73
+ """Подготавливает урок - копирует файлы и создает метаданные"""
74
+ global current_lesson_dir
75
+
76
+ if not segments_state:
77
+ return "⚠️ Нет добавленных сегментов", None
78
+
79
+ # Создаем временную директорию для урока
80
+ lesson_dir = tempfile.mkdtemp(prefix="lesson_")
81
+ current_lesson_dir = lesson_dir # Сохраняем глобально
82
+
83
+ timestamps = []
84
+
85
+ for i, seg in enumerate(segments_state):
86
+ # Копируем видео в директорию урока
87
+ lecture_new = os.path.join(lesson_dir, f"lecture_{i}.mp4")
88
+ correct_new = os.path.join(lesson_dir, f"correct_{i}.mp4")
89
+ wrong_new = os.path.join(lesson_dir, f"wrong_{i}.mp4")
90
+
91
+ # Копируем файлы
92
+ shutil.copy2(seg['lecture_path'], lecture_new)
93
+ shutil.copy2(seg['correct_path'], correct_new)
94
+ shutil.copy2(seg['wrong_path'], wrong_new)
95
+
96
+ timestamps.append({
97
+ 'index': i,
98
+ 'lecture': f"lecture_{i}.mp4", # Только имя файла
99
+ 'correct': f"correct_{i}.mp4",
100
+ 'wrong': f"wrong_{i}.mp4",
101
+ 'duration': seg['duration'],
102
+ 'options': seg['options'],
103
+ 'correct': seg['correct']
104
+ })
105
+
106
+ # Сохраняем метаданные
107
+ metadata = {
108
+ 'timestamps': timestamps,
109
+ 'total_segments': len(segments_state),
110
+ 'lesson_dir': lesson_dir # Сохраняем путь к директории
111
+ }
112
+
113
+ meta_path = os.path.join(lesson_dir, 'metadata.json')
114
+ with open(meta_path, 'w', encoding='utf-8') as f:
115
+ json.dump(metadata, f, ensure_ascii=False, indent=2)
116
+
117
+ total_duration = sum(seg['duration'] for seg in segments_state)
118
+
119
+ return (f"✅ Урок создан!\n- Всего сегментов: {len(segments_state)}\n- Примерная длительность: {total_duration:.1f} сек\n- Путь: {lesson_dir}",
120
+ meta_path)
121
 
122
  def generate_player_html(meta_path):
123
+ """Генерирует HTML плеер с интерактивными кнопками"""
124
+ global current_lesson_dir
125
+
126
+ if not meta_path or not os.path.exists(meta_path):
127
+ return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Сначала создайте урок!</p>"
128
+
129
+ with open(meta_path, 'r', encoding='utf-8') as f:
130
+ metadata = json.load(f)
131
+
132
+ # Используем сохраненную директорию
133
+ lesson_dir = metadata.get('lesson_dir', current_lesson_dir)
134
+ if not lesson_dir or not os.path.exists(lesson_dir):
135
+ return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Файлы урока не найдены. Создайте урок заново.</p>"
136
+
137
+ # Конвертируем все видео в base64
138
+ video_data = {}
139
+
140
+ for segment in metadata['timestamps']:
141
+ idx = segment['index']
142
+
143
+ lecture_path = os.path.join(lesson_dir, segment['lecture'])
144
+ correct_path = os.path.join(lesson_dir, segment['correct'])
145
+ wrong_path = os.path.join(lesson_dir, segment['wrong'])
146
+
147
+ try:
148
+ with open(lecture_path, 'rb') as f:
149
+ video_data[f'lecture_{idx}'] = base64.b64encode(f.read()).decode()
150
+
151
+ with open(correct_path, 'rb') as f:
152
+ video_data[f'correct_{idx}'] = base64.b64encode(f.read()).decode()
153
+
154
+ with open(wrong_path, 'rb') as f:
155
+ video_data[f'wrong_{idx}'] = base64.b64encode(f.read()).decode()
156
+ except Exception as e:
157
+ print(f"Ошибка чтения файла: {e}")
158
+ return f"<p style='color: red; text-align: center; padding: 40px;'>⚠️ Ошибка загрузки видео: {str(e)}</p>"
159
+
160
+ # Создаем структуру данных для JavaScript
161
+ segments_json = json.dumps([{
162
+ 'index': seg['index'],
163
+ 'duration': seg['duration'],
164
+ 'options': seg['options'],
165
+ 'correct': seg['correct']
166
+ } for seg in metadata['timestamps']], ensure_ascii=False)
167
+
168
+ # Создаем словарь с видео для JavaScript
169
+ videos_json = json.dumps(video_data)
170
+
171
+ html = f"""
172
+ <style>
173
+ * {{
174
+ margin: 0;
175
+ padding: 0;
176
+ box-sizing: border-box;
177
+ }}
178
+
179
+ .player-wrapper {{
180
+ width: 100%;
181
+ max-width: 900px;
182
+ margin: 20px auto;
183
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
184
+ }}
185
+
186
+ .player-container {{
187
+ position: relative;
188
+ width: 100%;
189
+ background: #000;
190
+ border-radius: 12px;
191
+ overflow: hidden;
192
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
193
+ }}
194
+
195
+ #mainVideo {{
196
+ width: 100%;
197
+ height: auto;
198
+ display: block;
199
+ min-height: 400px;
200
+ }}
201
+
202
+ .quiz-overlay {{
203
+ position: absolute;
204
+ bottom: 0;
205
+ left: 0;
206
+ right: 0;
207
+ background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.8) 50%, transparent 100%);
208
+ padding: 40px 30px 30px;
209
+ display: none;
210
+ }}
211
+
212
+ .quiz-question {{
213
+ color: #ffffff;
214
+ font-size: 20px;
215
+ font-weight: 600;
216
+ text-align: center;
217
+ margin-bottom: 20px;
218
+ text-shadow: 0 2px 4px rgba(0,0,0,0.5);
219
+ }}
220
+
221
+ .quiz-options {{
222
+ display: flex;
223
+ gap: 15px;
224
+ flex-direction: column;
225
+ max-width: 600px;
226
+ margin: 0 auto;
227
+ }}
228
+
229
+ .option-btn {{
230
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
231
+ color: white;
232
+ border: none;
233
+ padding: 18px 30px;
234
+ border-radius: 12px;
235
+ font-size: 17px;
236
+ font-weight: 500;
237
+ cursor: pointer;
238
+ transition: all 0.3s;
239
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
240
+ }}
241
+
242
+ .option-btn:hover {{
243
+ transform: translateY(-3px);
244
+ box-shadow: 0 6px 25px rgba(102, 126, 234, 0.6);
245
+ }}
246
+
247
+ .option-btn:active {{
248
+ transform: translateY(-1px);
249
+ }}
250
+
251
+ .timer {{
252
+ color: #ffd700;
253
+ font-size: 15px;
254
+ font-weight: 600;
255
+ text-align: center;
256
+ margin-top: 15px;
257
+ text-shadow: 0 2px 4px rgba(0,0,0,0.5);
258
+ }}
259
+
260
+ .timer.warning {{
261
+ color: #ff6b6b;
262
+ }}
263
+
264
+ .progress-bar {{
265
+ position: absolute;
266
+ top: 0;
267
+ left: 0;
268
+ height: 4px;
269
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
270
+ width: 0%;
271
+ transition: width 0.1s linear;
272
+ z-index: 10;
273
+ }}
274
+
275
+ .segment-info {{
276
+ position: absolute;
277
+ top: 15px;
278
+ right: 15px;
279
+ background: rgba(0,0,0,0.7);
280
+ color: white;
281
+ padding: 8px 16px;
282
+ border-radius: 20px;
283
+ font-size: 14px;
284
+ font-weight: 500;
285
+ z-index: 10;
286
+ }}
287
+
288
+ .loading {{
289
+ position: absolute;
290
+ top: 50%;
291
+ left: 50%;
292
+ transform: translate(-50%, -50%);
293
+ color: white;
294
+ font-size: 18px;
295
+ display: none;
296
+ z-index: 20;
297
+ }}
298
+
299
+ .controls {{
300
+ padding: 15px;
301
+ background: #1a1a1a;
302
+ display: flex;
303
+ align-items: center;
304
+ gap: 15px;
305
+ }}
306
+
307
+ .play-pause-btn {{
308
+ background: #667eea;
309
+ border: none;
310
+ color: white;
311
+ width: 40px;
312
+ height: 40px;
313
+ border-radius: 50%;
314
+ cursor: pointer;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ transition: all 0.3s;
319
+ font-size: 16px;
320
+ }}
321
+
322
+ .play-pause-btn:hover {{
323
+ background: #764ba2;
324
+ transform: scale(1.1);
325
+ }}
326
+
327
+ .time-display {{
328
+ color: #ffffff;
329
+ font-size: 14px;
330
+ font-weight: 500;
331
+ }}
332
+ </style>
333
+
334
+ <div class="player-wrapper">
335
+ <div class="player-container">
336
+ <div class="progress-bar" id="progressBar"></div>
337
+ <div class="segment-info" id="segmentInfo">Сегмент 1 из {metadata['total_segments']}</div>
338
+ <div class="loading" id="loading">Загрузка...</div>
339
+
340
+ <video id="mainVideo" preload="auto">
341
+ Ваш браузер не поддерживает видео
342
+ </video>
343
+
344
+ <div class="quiz-overlay" id="quizOverlay">
345
+ <div class="quiz-question">Выберите правильный ответ:</div>
346
+ <div class="quiz-options">
347
+ <button class="option-btn" id="option1"></button>
348
+ <button class="option-btn" id="option2"></button>
349
+ </div>
350
+ <div class="timer" id="timer">⏱ Времени осталось: <span id="timeLeft">7</span> сек</div>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="controls">
355
+ <button class="play-pause-btn" id="playPauseBtn">▶</button>
356
+ <div class="time-display" id="timeDisplay">0:00</div>
357
+ </div>
358
+ </div>
359
+
360
+ <script>
361
+ (function() {{
362
+ const mainVideo = document.getElementById('mainVideo');
363
+ const quizOverlay = document.getElementById('quizOverlay');
364
+ const option1Btn = document.getElementById('option1');
365
+ const option2Btn = document.getElementById('option2');
366
+ const timerDisplay = document.getElementById('timeLeft');
367
+ const progressBar = document.getElementById('progressBar');
368
+ const segmentInfo = document.getElementById('segmentInfo');
369
+ const loadingDiv = document.getElementById('loading');
370
+ const playPauseBtn = document.getElementById('playPauseBtn');
371
+ const timeDisplay = document.getElementById('timeDisplay');
372
+ const timerElement = document.getElementById('timer');
373
+
374
+ const segments = {segments_json};
375
+ const videosBase64 = {videos_json};
376
+
377
+ let currentSegmentIndex = 0;
378
+ let isPlayingReaction = false;
379
+ let quizShown = false;
380
+ let answered = false;
381
+ let timerInterval = null;
382
+ let timeRemaining = 7;
383
+
384
+ function formatTime(seconds) {{
385
+ const mins = Math.floor(seconds / 60);
386
+ const secs = Math.floor(seconds % 60);
387
+ return mins + ':' + (secs < 10 ? '0' : '') + secs;
388
+ }}
389
+
390
+ function updateSegmentInfo() {{
391
+ segmentInfo.textContent = 'Сегмент ' + (currentSegmentIndex + 1) + ' из ' + segments.length;
392
+ }}
393
+
394
+ function showLoading() {{
395
+ loadingDiv.style.display = 'block';
396
+ }}
397
+
398
+ function hideLoading() {{
399
+ loadingDiv.style.display = 'none';
400
+ }}
401
+
402
+ function loadVideo(base64Data) {{
403
+ return new Promise(function(resolve, reject) {{
404
+ showLoading();
405
+ mainVideo.src = 'data:video/mp4;base64,' + base64Data;
406
+ mainVideo.load();
407
+
408
+ mainVideo.onloadeddata = function() {{
409
+ hideLoading();
410
+ resolve();
411
+ }};
412
+
413
+ mainVideo.onerror = function() {{
414
+ hideLoading();
415
+ reject(new Error('Ошибка загрузки видео'));
416
+ }};
417
+ }});
418
+ }}
419
+
420
+ function showQuiz(segment) {{
421
+ quizOverlay.style.display = 'block';
422
+ option1Btn.textContent = segment.options[0];
423
+ option2Btn.textContent = segment.options[1];
424
+ quizShown = true;
425
+ answered = false;
426
+ timeRemaining = 7;
427
+ timerDisplay.textContent = timeRemaining;
428
+
429
+ if (timerInterval) {{
430
+ clearInterval(timerInterval);
431
+ }}
432
+
433
+ timerInterval = setInterval(function() {{
434
+ timeRemaining--;
435
+ timerDisplay.textContent = timeRemaining;
436
+
437
+ if (timeRemaining <= 3) {{
438
+ timerElement.classList.add('warning');
439
+ }} else {{
440
+ timerElement.classList.remove('warning');
441
+ }}
442
+
443
+ if (timeRemaining <= 0) {{
444
+ clearInterval(timerInterval);
445
+ if (!answered) {{
446
+ handleAnswer(-1, segment);
447
+ }}
448
+ }}
449
+ }}, 1000);
450
+ }}
451
+
452
+ function hideQuiz() {{
453
+ quizOverlay.style.display = 'none';
454
+ quizShown = false;
455
+ timerElement.classList.remove('warning');
456
+ if (timerInterval) {{
457
+ clearInterval(timerInterval);
458
+ timerInterval = null;
459
+ }}
460
+ }}
461
+
462
+ function playReaction(isCorrect) {{
463
+ return new Promise(function(resolve) {{
464
+ isPlayingReaction = true;
465
+ hideQuiz();
466
+
467
+ const segment = segments[currentSegmentIndex];
468
+ const reactionKey = isCorrect ? ('correct_' + segment.index) : ('wrong_' + segment.index);
469
+
470
+ loadVideo(videosBase64[reactionKey]).then(function() {{
471
+ mainVideo.play();
472
+
473
+ mainVideo.onended = function() {{
474
+ isPlayingReaction = false;
475
+ mainVideo.onended = null;
476
+ resolve();
477
+ }};
478
+ }}).catch(function(error) {{
479
+ console.error('Ошибка воспроизведения реакции:', error);
480
+ isPlayingReaction = false;
481
+ resolve();
482
+ }});
483
+ }});
484
+ }}
485
+
486
+ function loadNextSegment() {{
487
+ if (currentSegmentIndex >= segments.length) {{
488
+ alert('🎉 Поздравляем! Вы завершили урок!');
489
+ currentSegmentIndex = 0;
490
+ return;
491
+ }}
492
+
493
+ const segment = segments[currentSegmentIndex];
494
+ const lectureKey = 'lecture_' + segment.index;
495
+
496
+ loadVideo(videosBase64[lectureKey]).then(function() {{
497
+ updateSegmentInfo();
498
+ mainVideo.play();
499
+ playPauseBtn.textContent = '⏸';
500
+ }}).catch(function(error) {{
501
+ console.error('Ошибка загрузки лекции:', error);
502
+ alert('Ошибка загрузки видео');
503
+ }});
504
+ }}
505
+
506
+ function handleAnswer(choice, segment) {{
507
+ if (answered) return;
508
+ answered = true;
509
+
510
+ const isCorrect = choice === segment.correct;
511
+
512
+ playReaction(isCorrect).then(function() {{
513
+ currentSegmentIndex++;
514
+
515
+ if (currentSegmentIndex < segments.length) {{
516
+ loadNextSegment();
517
+ }} else {{
518
+ alert('🎉 Поздравляем! Вы завершили урок!');
519
+ }}
520
+ }});
521
+ }}
522
+
523
+ option1Btn.addEventListener('click', function() {{
524
+ if (currentSegmentIndex >= 0 && currentSegmentIndex < segments.length && !answered) {{
525
+ handleAnswer(0, segments[currentSegmentIndex]);
526
+ }}
527
+ }});
528
+
529
+ option2Btn.addEventListener('click', function() {{
530
+ if (currentSegmentIndex >= 0 && currentSegmentIndex < segments.length && !answered) {{
531
+ handleAnswer(1, segments[currentSegmentIndex]);
532
+ }}
533
+ }});
534
+
535
+ mainVideo.addEventListener('timeupdate', function() {{
536
+ if (isPlayingReaction) return;
537
+
538
+ const currentTime = mainVideo.currentTime;
539
+ const duration = mainVideo.duration;
540
+
541
+ if (!isNaN(currentTime)) {{
542
+ timeDisplay.textContent = formatTime(currentTime);
543
+ }}
544
+
545
+ if (duration > 0 && !isNaN(duration)) {{
546
+ const progress = (currentTime / duration) * 100;
547
+ progressBar.style.width = progress + '%';
548
+
549
+ const timeLeft = duration - currentTime;
550
+
551
+ if (!quizShown && timeLeft <= 7 && timeLeft > 0 && !answered) {{
552
+ showQuiz(segments[currentSegmentIndex]);
553
+ }}
554
+ }}
555
+ }});
556
+
557
+ playPauseBtn.addEventListener('click', function() {{
558
+ if (mainVideo.paused) {{
559
+ mainVideo.play();
560
+ playPauseBtn.textContent = '⏸';
561
+ }} else {{
562
+ mainVideo.pause();
563
+ playPauseBtn.textContent = '▶';
564
+ }}
565
+ }});
566
+
567
+ mainVideo.addEventListener('play', function() {{
568
+ playPauseBtn.textContent = '⏸';
569
+ }});
570
+
571
+ mainVideo.addEventListener('pause', function() {{
572
+ playPauseBtn.textContent = '▶';
573
+ }});
574
+
575
+ // Запускаем первый сегмент
576
+ loadNextSegment();
577
+ }})();
578
+ </script>
579
+ """
580
+
581
+ return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
  # Создаем интерфейс
584
+ with gr.Blocks(title="Интерактивный Урок") as demo:
585
+ gr.Markdown("""
586
+ # 🎓 Интерактивный Урок (Coursera-style)
587
+
588
+ ### Инструкция:
589
+ 1. **Загрузите 3 видео для каждого сегмента:**
590
+ - 🎥 Видео лекции (в конце которого задается вопрос)
591
+ - ✅ Видео реакции на правильный ответ
592
+ - ❌ Видео реакции на неправильный ответ
593
+ 2. **Введите варианты ответов** и выберите правильный
594
+ 3. Нажмите **"Добавить Сегмент"** (повторите для каждого фрагмента)
595
+ 4. Когда все сегменты добавлены, нажмите **"Создать Урок"**
596
+ 5. Нажмите **"Запустить Плеер"** для прохождения урока
597
+ """)
598
+
599
+ # Состояния
600
+ segments_state = gr.State([])
601
+ meta_state = gr.State(None)
602
+
603
+ with gr.Tab("📝 Создание Урока"):
604
+ gr.Markdown("### Добавление сегмента")
605
+
606
+ with gr.Row():
607
+ with gr.Column(scale=1):
608
+ lecture_input = gr.Video(label="🎥 Видео Лекции")
609
+ correct_input = gr.Video(label="✅ Реакция: Правильный ответ")
610
+ wrong_input = gr.Video(label=" Реакция: Неправильный ответ")
611
+
612
+ with gr.Column(scale=1):
613
+ option1_input = gr.Textbox(
614
+ label="Вариант 1",
615
+ placeholder="Например: Париж"
616
+ )
617
+ option2_input = gr.Textbox(
618
+ label="Вариант 2",
619
+ placeholder="Например: Лондон"
620
+ )
621
+ correct_radio = gr.Radio(
622
+ ["Вариант 1", "Вариант 2"],
623
+ label="✅ Правильный вариант",
624
+ value="Вариант 1"
625
+ )
626
+
627
+ add_btn = gr.Button("➕ Добавить Сегмент", variant="primary")
628
+ segments_info = gr.Markdown("ет добавленных сегментов*")
629
+
630
+ add_btn.click(
631
+ fn=add_segment,
632
+ inputs=[lecture_input, correct_input, wrong_input, option1_input, option2_input, correct_radio, segments_state],
633
+ outputs=[segments_state, segments_info, lecture_input, correct_input, wrong_input]
634
+ )
635
+
636
+ gr.Markdown("---")
637
+
638
+ create_btn = gr.Button("🎬 Создать Урок", variant="primary")
639
+ create_status = gr.Markdown("")
640
+
641
+ create_btn.click(
642
+ fn=create_lesson,
643
+ inputs=[segments_state],
644
+ outputs=[create_status, meta_state]
645
+ )
646
+
647
+ with gr.Tab("▶️ Прохождение Урока"):
648
+ gr.Markdown("""
649
+ ### Как проходить урок:
650
+ - 🎬 Видео проигрывается автоматически
651
+ - ⏱ **За 7 секунд** до конца появятся кнопки с вариантами ответа
652
+ - 🎯 Выберите правильный ответ до окончания времени
653
+ - ✅ После ответа покажется видео реакции лектора
654
+ - ➡️ Затем автоматически начнется следующий сегмент
655
+ - 🏆 Пройдите все сегменты до конца!
656
+ """)
657
+
658
+ start_btn = gr.Button("🚀 Запустить Плеер", variant="primary")
659
+ player_html = gr.HTML()
660
+
661
+ start_btn.click(
662
+ fn=generate_player_html,
663
+ inputs=[meta_state],
664
+ outputs=[player_html]
665
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
  if __name__ == '__main__':
668
+ demo.launch()