AlserFurma commited on
Commit
29be5ba
·
verified ·
1 Parent(s): 9405d74

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +668 -0
app.py ADDED
@@ -0,0 +1,668 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import tempfile
3
+ 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()