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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +645 -645
app.py CHANGED
@@ -4,665 +4,665 @@ import os
4
  import json
5
  import shutil
6
  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']
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()
 
4
  import json
5
  import shutil
6
  import base64
 
7
 
8
  try:
9
+ from moviepy.editor import VideoFileClip
10
+ MOVIEPY_AVAILABLE = True
11
  except ImportError:
12
+ print("MoviePy не установлен, используем альтернативный метод")
13
+ MOVIEPY_AVAILABLE = False
14
 
15
  # Глобальная переменная для хранения созданных уроков
16
  current_lesson_dir = None
17
 
18
  def add_segment(lecture_video, correct_video, wrong_video, option1_text, option2_text, correct_option, segments_state):
19
+ """Добавляет сегмент: видео лекции + видео реакций + текст вариантов"""
20
+ if lecture_video is None or correct_video is None or wrong_video is None:
21
+ return segments_state, "⚠️ Загрузите все три видео (лекция, правильная реакция, неправильная реакция)", None, None, None
22
+
23
+ if not option1_text or not option2_text:
24
+ return segments_state, "⚠️ Введите оба варианта ответа", None, None, None
25
+
26
+ if correct_option is None:
27
+ return segments_state, "⚠️ Выберите правильный вариант", None, None, None
28
+
29
+ # Получаем длительность лекции
30
+ try:
31
+ if MOVIEPY_AVAILABLE:
32
+ clip = VideoFileClip(lecture_video)
33
+ duration = clip.duration
34
+ clip.close()
35
+ else:
36
+ # Альтернативный метод через subprocess
37
+ import subprocess
38
+ result = subprocess.run(
39
+ ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
40
+ '-of', 'default=noprint_wrappers=1:nokey=1', lecture_video],
41
+ stdout=subprocess.PIPE,
42
+ stderr=subprocess.STDOUT
43
+ )
44
+ duration = float(result.stdout)
45
+ except Exception as e:
46
+ print(f"Ошибка получения длительности: {e}")
47
+ duration = 10 # Значение по умолчанию
48
+
49
+ # Сохраняем оригинальные пути
50
+ new_segment = {
51
+ 'lecture_path': lecture_video,
52
+ 'correct_path': correct_video,
53
+ 'wrong_path': wrong_video,
54
+ 'options': [option1_text, option2_text],
55
+ 'correct_answer': 0 if correct_option == "Вариант 1" else 1, # Переименовали ключ
56
+ 'duration': duration
57
+ }
58
+
59
+ segments_state.append(new_segment)
60
+
61
+ info = f"✅ **Всего сегментов: {len(segments_state)}**\n\n"
62
+ for i, seg in enumerate(segments_state, 1):
63
+ info += f"**Сегмент {i}:** ({seg['duration']:.1f} сек)\n"
64
+ info += f"- 🎯 Вариант 1: {seg['options'][0]}\n"
65
+ info += f"- 🎯 Вариант 2: {seg['options'][1]}\n"
66
+ info += f"- ✅ Правильный: Вариант {seg['correct_answer'] + 1}\n\n"
67
+
68
+ # Очищаем поля ввода
69
+ return segments_state, info, None, None, None
70
 
71
  def create_lesson(segments_state):
72
+ """Подготавливает урок - копирует файлы и создает метаданные"""
73
+ global current_lesson_dir
74
+
75
+ if not segments_state:
76
+ return "⚠️ Нет добавленных сегментов", None
77
+
78
+ # Создаем временную директорию для урока
79
+ lesson_dir = tempfile.mkdtemp(prefix="lesson_")
80
+ current_lesson_dir = lesson_dir # Сохраняем глобально
81
+
82
+ timestamps = []
83
+
84
+ for i, seg in enumerate(segments_state):
85
+ # Копируем видео в директорию урока
86
+ lecture_new = os.path.join(lesson_dir, f"lecture_{i}.mp4")
87
+ correct_new = os.path.join(lesson_dir, f"correct_{i}.mp4")
88
+ wrong_new = os.path.join(lesson_dir, f"wrong_{i}.mp4")
89
+
90
+ # Копируем файлы
91
+ shutil.copy2(seg['lecture_path'], lecture_new)
92
+ shutil.copy2(seg['correct_path'], correct_new)
93
+ shutil.copy2(seg['wrong_path'], wrong_new)
94
+
95
+ timestamps.append({
96
+ 'index': i,
97
+ 'lecture_file': f"lecture_{i}.mp4", # Переименовали ключи для ясности
98
+ 'correct_file': f"correct_{i}.mp4",
99
+ 'wrong_file': f"wrong_{i}.mp4",
100
+ 'duration': seg['duration'],
101
+ 'options': seg['options'],
102
+ 'correct_answer': seg['correct_answer'] # Здесь храним индекс правильного ответа
103
+ })
104
+
105
+ # Сохраняем метаданные
106
+ metadata = {
107
+ 'timestamps': timestamps,
108
+ 'total_segments': len(segments_state),
109
+ 'lesson_dir': lesson_dir # Сохраняем путь к директории
110
+ }
111
+
112
+ meta_path = os.path.join(lesson_dir, 'metadata.json')
113
+ with open(meta_path, 'w', encoding='utf-8') as f:
114
+ json.dump(metadata, f, ensure_ascii=False, indent=2)
115
+
116
+ total_duration = sum(seg['duration'] for seg in segments_state)
117
+
118
+ return (f"✅ Урок создан!\n- Всего сегментов: {len(segments_state)}\n- Примерная длительность: {total_duration:.1f} сек\n- Путь: {lesson_dir}",
119
+ meta_path)
120
 
121
  def generate_player_html(meta_path):
122
+ """Генерирует HTML плеер с интерактивными кнопками"""
123
+ global current_lesson_dir
124
+
125
+ if not meta_path or not os.path.exists(meta_path):
126
+ return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Сначала создайте урок!</p>"
127
+
128
+ with open(meta_path, 'r', encoding='utf-8') as f:
129
+ metadata = json.load(f)
130
+
131
+ # Используем сохраненную директорию
132
+ lesson_dir = metadata.get('lesson_dir', current_lesson_dir)
133
+ if not lesson_dir or not os.path.exists(lesson_dir):
134
+ return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Файлы урока не найдены. Создайте урок заново.</p>"
135
+
136
+ # Конвертируем все видео в base64
137
+ video_data = {}
138
+
139
+ for segment in metadata['timestamps']:
140
+ idx = segment['index']
141
+
142
+ # Используем правильные ключи для имен файлов
143
+ lecture_path = os.path.join(lesson_dir, segment['lecture_file'])
144
+ correct_path = os.path.join(lesson_dir, segment['correct_file'])
145
+ wrong_path = os.path.join(lesson_dir, segment['wrong_file'])
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_answer'] # Используем правильный ключ
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()