Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import tempfile | |
| import os | |
| import json | |
| import shutil | |
| import base64 | |
| try: | |
| from moviepy.editor import VideoFileClip | |
| MOVIEPY_AVAILABLE = True | |
| except ImportError: | |
| print("MoviePy не установлен, используем альтернативный метод") | |
| MOVIEPY_AVAILABLE = False | |
| # Глобальная переменная для хранения созданных уроков | |
| current_lesson_dir = None | |
| def add_segment(lecture_video, correct_video, wrong_video, option1_text, option2_text, correct_option, segments_state): | |
| """Добавляет сегмент: видео лекции + видео реакций + текст вариантов""" | |
| if lecture_video is None or correct_video is None or wrong_video is None: | |
| return segments_state, "⚠️ Загрузите все три видео (лекция, правильная реакция, неправильная реакция)", None, None, None | |
| if not option1_text or not option2_text: | |
| return segments_state, "⚠️ Введите оба варианта ответа", None, None, None | |
| if correct_option is None: | |
| return segments_state, "⚠️ Выберите правильный вариант", None, None, None | |
| # Получаем длительность лекции | |
| try: | |
| if MOVIEPY_AVAILABLE: | |
| clip = VideoFileClip(lecture_video) | |
| duration = clip.duration | |
| clip.close() | |
| else: | |
| # Альтернативный метод через subprocess | |
| import subprocess | |
| result = subprocess.run( | |
| ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', | |
| '-of', 'default=noprint_wrappers=1:nokey=1', lecture_video], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT | |
| ) | |
| duration = float(result.stdout) | |
| except Exception as e: | |
| print(f"Ошибка получения длительности: {e}") | |
| duration = 10 # Значение по умолчанию | |
| # Сохраняем оригинальные пути | |
| new_segment = { | |
| 'lecture_path': lecture_video, | |
| 'correct_path': correct_video, | |
| 'wrong_path': wrong_video, | |
| 'options': [option1_text, option2_text], | |
| 'correct_answer': 0 if correct_option == "Вариант 1" else 1, # Переименовали ключ | |
| 'duration': duration | |
| } | |
| segments_state.append(new_segment) | |
| info = f"✅ **Всего сегментов: {len(segments_state)}**\n\n" | |
| for i, seg in enumerate(segments_state, 1): | |
| info += f"**Сегмент {i}:** ({seg['duration']:.1f} сек)\n" | |
| info += f"- 🎯 Вариант 1: {seg['options'][0]}\n" | |
| info += f"- 🎯 Вариант 2: {seg['options'][1]}\n" | |
| info += f"- ✅ Правильный: Вариант {seg['correct_answer'] + 1}\n\n" | |
| # Очищаем поля ввода | |
| return segments_state, info, None, None, None | |
| def create_lesson(segments_state): | |
| """Подготавливает урок - копирует файлы и создает метаданные""" | |
| global current_lesson_dir | |
| if not segments_state: | |
| return "⚠️ Нет добавленных сегментов", None | |
| # Создаем временную директорию для урока | |
| lesson_dir = tempfile.mkdtemp(prefix="lesson_") | |
| current_lesson_dir = lesson_dir # Сохраняем глобально | |
| timestamps = [] | |
| for i, seg in enumerate(segments_state): | |
| # Копируем видео в директорию урока | |
| lecture_new = os.path.join(lesson_dir, f"lecture_{i}.mp4") | |
| correct_new = os.path.join(lesson_dir, f"correct_{i}.mp4") | |
| wrong_new = os.path.join(lesson_dir, f"wrong_{i}.mp4") | |
| # Копируем файлы | |
| shutil.copy2(seg['lecture_path'], lecture_new) | |
| shutil.copy2(seg['correct_path'], correct_new) | |
| shutil.copy2(seg['wrong_path'], wrong_new) | |
| timestamps.append({ | |
| 'index': i, | |
| 'lecture_file': f"lecture_{i}.mp4", # Переименовали ключи для ясности | |
| 'correct_file': f"correct_{i}.mp4", | |
| 'wrong_file': f"wrong_{i}.mp4", | |
| 'duration': seg['duration'], | |
| 'options': seg['options'], | |
| 'correct_answer': seg['correct_answer'] # Здесь храним индекс правильного ответа | |
| }) | |
| # Сохраняем метаданные | |
| metadata = { | |
| 'timestamps': timestamps, | |
| 'total_segments': len(segments_state), | |
| 'lesson_dir': lesson_dir # Сохраняем путь к директории | |
| } | |
| meta_path = os.path.join(lesson_dir, 'metadata.json') | |
| with open(meta_path, 'w', encoding='utf-8') as f: | |
| json.dump(metadata, f, ensure_ascii=False, indent=2) | |
| total_duration = sum(seg['duration'] for seg in segments_state) | |
| return (f"✅ Урок создан!\n- Всего сегментов: {len(segments_state)}\n- Примерная длительность: {total_duration:.1f} сек\n- Путь: {lesson_dir}", | |
| meta_path) | |
| def generate_player_html(meta_path): | |
| """Генерирует HTML плеер с интерактивными кнопками""" | |
| global current_lesson_dir | |
| if not meta_path or not os.path.exists(meta_path): | |
| return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Сначала создайте урок!</p>" | |
| with open(meta_path, 'r', encoding='utf-8') as f: | |
| metadata = json.load(f) | |
| # Используем сохраненную директорию | |
| lesson_dir = metadata.get('lesson_dir', current_lesson_dir) | |
| if not lesson_dir or not os.path.exists(lesson_dir): | |
| return "<p style='color: red; text-align: center; padding: 40px;'>⚠️ Файлы урока не найдены. Создайте урок заново.</p>" | |
| # Конвертируем все видео в base64 | |
| video_data = {} | |
| for segment in metadata['timestamps']: | |
| idx = segment['index'] | |
| # Используем правильные ключи для имен файлов | |
| lecture_path = os.path.join(lesson_dir, segment['lecture_file']) | |
| correct_path = os.path.join(lesson_dir, segment['correct_file']) | |
| wrong_path = os.path.join(lesson_dir, segment['wrong_file']) | |
| try: | |
| with open(lecture_path, 'rb') as f: | |
| video_data[f'lecture_{idx}'] = base64.b64encode(f.read()).decode() | |
| with open(correct_path, 'rb') as f: | |
| video_data[f'correct_{idx}'] = base64.b64encode(f.read()).decode() | |
| with open(wrong_path, 'rb') as f: | |
| video_data[f'wrong_{idx}'] = base64.b64encode(f.read()).decode() | |
| except Exception as e: | |
| print(f"Ошибка чтения файла: {e}") | |
| return f"<p style='color: red; text-align: center; padding: 40px;'>⚠️ Ошибка загрузки видео: {str(e)}</p>" | |
| # Создаем структуру данных для JavaScript | |
| segments_json = json.dumps([{ | |
| 'index': seg['index'], | |
| 'duration': seg['duration'], | |
| 'options': seg['options'], | |
| 'correct': seg['correct_answer'] # Используем правильный ключ | |
| } for seg in metadata['timestamps']], ensure_ascii=False) | |
| # Создаем словарь с видео для JavaScript | |
| videos_json = json.dumps(video_data) | |
| html = f""" | |
| <style> | |
| * {{ | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| }} | |
| .player-wrapper {{ | |
| width: 100%; | |
| max-width: 900px; | |
| margin: 20px auto; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| }} | |
| .player-container {{ | |
| position: relative; | |
| width: 100%; | |
| background: #000; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); | |
| }} | |
| #mainVideo {{ | |
| width: 100%; | |
| height: auto; | |
| display: block; | |
| min-height: 400px; | |
| }} | |
| .quiz-overlay {{ | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.8) 50%, transparent 100%); | |
| padding: 40px 30px 30px; | |
| display: none; | |
| }} | |
| .quiz-question {{ | |
| color: #ffffff; | |
| font-size: 20px; | |
| font-weight: 600; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.5); | |
| }} | |
| .quiz-options {{ | |
| display: flex; | |
| gap: 15px; | |
| flex-direction: column; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| }} | |
| .option-btn {{ | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| padding: 18px 30px; | |
| border-radius: 12px; | |
| font-size: 17px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); | |
| }} | |
| .option-btn:hover {{ | |
| transform: translateY(-3px); | |
| box-shadow: 0 6px 25px rgba(102, 126, 234, 0.6); | |
| }} | |
| .option-btn:active {{ | |
| transform: translateY(-1px); | |
| }} | |
| .timer {{ | |
| color: #ffd700; | |
| font-size: 15px; | |
| font-weight: 600; | |
| text-align: center; | |
| margin-top: 15px; | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.5); | |
| }} | |
| .timer.warning {{ | |
| color: #ff6b6b; | |
| }} | |
| .progress-bar {{ | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 4px; | |
| background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); | |
| width: 0%; | |
| transition: width 0.1s linear; | |
| z-index: 10; | |
| }} | |
| .segment-info {{ | |
| position: absolute; | |
| top: 15px; | |
| right: 15px; | |
| background: rgba(0,0,0,0.7); | |
| color: white; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| z-index: 10; | |
| }} | |
| .loading {{ | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: white; | |
| font-size: 18px; | |
| display: none; | |
| z-index: 20; | |
| }} | |
| .controls {{ | |
| padding: 15px; | |
| background: #1a1a1a; | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| }} | |
| .play-pause-btn {{ | |
| background: #667eea; | |
| border: none; | |
| color: white; | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s; | |
| font-size: 16px; | |
| }} | |
| .play-pause-btn:hover {{ | |
| background: #764ba2; | |
| transform: scale(1.1); | |
| }} | |
| .time-display {{ | |
| color: #ffffff; | |
| font-size: 14px; | |
| font-weight: 500; | |
| }} | |
| </style> | |
| <div class="player-wrapper"> | |
| <div class="player-container"> | |
| <div class="progress-bar" id="progressBar"></div> | |
| <div class="segment-info" id="segmentInfo">Сегмент 1 из {metadata['total_segments']}</div> | |
| <div class="loading" id="loading">Загрузка...</div> | |
| <video id="mainVideo" preload="auto"> | |
| Ваш браузер не поддерживает видео | |
| </video> | |
| <div class="quiz-overlay" id="quizOverlay"> | |
| <div class="quiz-question">Выберите правильный ответ:</div> | |
| <div class="quiz-options"> | |
| <button class="option-btn" id="option1"></button> | |
| <button class="option-btn" id="option2"></button> | |
| </div> | |
| <div class="timer" id="timer">⏱ Времени осталось: <span id="timeLeft">7</span> сек</div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button class="play-pause-btn" id="playPauseBtn">▶</button> | |
| <div class="time-display" id="timeDisplay">0:00</div> | |
| </div> | |
| </div> | |
| <script> | |
| (function() {{ | |
| const mainVideo = document.getElementById('mainVideo'); | |
| const quizOverlay = document.getElementById('quizOverlay'); | |
| const option1Btn = document.getElementById('option1'); | |
| const option2Btn = document.getElementById('option2'); | |
| const timerDisplay = document.getElementById('timeLeft'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const segmentInfo = document.getElementById('segmentInfo'); | |
| const loadingDiv = document.getElementById('loading'); | |
| const playPauseBtn = document.getElementById('playPauseBtn'); | |
| const timeDisplay = document.getElementById('timeDisplay'); | |
| const timerElement = document.getElementById('timer'); | |
| const segments = {segments_json}; | |
| const videosBase64 = {videos_json}; | |
| let currentSegmentIndex = 0; | |
| let isPlayingReaction = false; | |
| let quizShown = false; | |
| let answered = false; | |
| let timerInterval = null; | |
| let timeRemaining = 7; | |
| function formatTime(seconds) {{ | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return mins + ':' + (secs < 10 ? '0' : '') + secs; | |
| }} | |
| function updateSegmentInfo() {{ | |
| segmentInfo.textContent = 'Сегмент ' + (currentSegmentIndex + 1) + ' из ' + segments.length; | |
| }} | |
| function showLoading() {{ | |
| loadingDiv.style.display = 'block'; | |
| }} | |
| function hideLoading() {{ | |
| loadingDiv.style.display = 'none'; | |
| }} | |
| function loadVideo(base64Data) {{ | |
| return new Promise(function(resolve, reject) {{ | |
| showLoading(); | |
| mainVideo.src = 'data:video/mp4;base64,' + base64Data; | |
| mainVideo.load(); | |
| mainVideo.onloadeddata = function() {{ | |
| hideLoading(); | |
| resolve(); | |
| }}; | |
| mainVideo.onerror = function() {{ | |
| hideLoading(); | |
| reject(new Error('Ошибка загрузки видео')); | |
| }}; | |
| }}); | |
| }} | |
| function showQuiz(segment) {{ | |
| quizOverlay.style.display = 'block'; | |
| option1Btn.textContent = segment.options[0]; | |
| option2Btn.textContent = segment.options[1]; | |
| quizShown = true; | |
| answered = false; | |
| timeRemaining = 7; | |
| timerDisplay.textContent = timeRemaining; | |
| if (timerInterval) {{ | |
| clearInterval(timerInterval); | |
| }} | |
| timerInterval = setInterval(function() {{ | |
| timeRemaining--; | |
| timerDisplay.textContent = timeRemaining; | |
| if (timeRemaining <= 3) {{ | |
| timerElement.classList.add('warning'); | |
| }} else {{ | |
| timerElement.classList.remove('warning'); | |
| }} | |
| if (timeRemaining <= 0) {{ | |
| clearInterval(timerInterval); | |
| if (!answered) {{ | |
| handleAnswer(-1, segment); | |
| }} | |
| }} | |
| }}, 1000); | |
| }} | |
| function hideQuiz() {{ | |
| quizOverlay.style.display = 'none'; | |
| quizShown = false; | |
| timerElement.classList.remove('warning'); | |
| if (timerInterval) {{ | |
| clearInterval(timerInterval); | |
| timerInterval = null; | |
| }} | |
| }} | |
| function playReaction(isCorrect) {{ | |
| return new Promise(function(resolve) {{ | |
| isPlayingReaction = true; | |
| hideQuiz(); | |
| const segment = segments[currentSegmentIndex]; | |
| const reactionKey = isCorrect ? ('correct_' + segment.index) : ('wrong_' + segment.index); | |
| loadVideo(videosBase64[reactionKey]).then(function() {{ | |
| mainVideo.play(); | |
| mainVideo.onended = function() {{ | |
| isPlayingReaction = false; | |
| mainVideo.onended = null; | |
| resolve(); | |
| }}; | |
| }}).catch(function(error) {{ | |
| console.error('Ошибка воспроизведения реакции:', error); | |
| isPlayingReaction = false; | |
| resolve(); | |
| }}); | |
| }}); | |
| }} | |
| function loadNextSegment() {{ | |
| if (currentSegmentIndex >= segments.length) {{ | |
| alert('🎉 Поздравляем! Вы завершили урок!'); | |
| currentSegmentIndex = 0; | |
| return; | |
| }} | |
| const segment = segments[currentSegmentIndex]; | |
| const lectureKey = 'lecture_' + segment.index; | |
| loadVideo(videosBase64[lectureKey]).then(function() {{ | |
| updateSegmentInfo(); | |
| mainVideo.play(); | |
| playPauseBtn.textContent = '⏸'; | |
| }}).catch(function(error) {{ | |
| console.error('Ошибка загрузки лекции:', error); | |
| alert('Ошибка загрузки видео'); | |
| }}); | |
| }} | |
| function handleAnswer(choice, segment) {{ | |
| if (answered) return; | |
| answered = true; | |
| const isCorrect = choice === segment.correct; | |
| playReaction(isCorrect).then(function() {{ | |
| currentSegmentIndex++; | |
| if (currentSegmentIndex < segments.length) {{ | |
| loadNextSegment(); | |
| }} else {{ | |
| alert('🎉 Поздравляем! Вы завершили урок!'); | |
| }} | |
| }}); | |
| }} | |
| option1Btn.addEventListener('click', function() {{ | |
| if (currentSegmentIndex >= 0 && currentSegmentIndex < segments.length && !answered) {{ | |
| handleAnswer(0, segments[currentSegmentIndex]); | |
| }} | |
| }}); | |
| option2Btn.addEventListener('click', function() {{ | |
| if (currentSegmentIndex >= 0 && currentSegmentIndex < segments.length && !answered) {{ | |
| handleAnswer(1, segments[currentSegmentIndex]); | |
| }} | |
| }}); | |
| mainVideo.addEventListener('timeupdate', function() {{ | |
| if (isPlayingReaction) return; | |
| const currentTime = mainVideo.currentTime; | |
| const duration = mainVideo.duration; | |
| if (!isNaN(currentTime)) {{ | |
| timeDisplay.textContent = formatTime(currentTime); | |
| }} | |
| if (duration > 0 && !isNaN(duration)) {{ | |
| const progress = (currentTime / duration) * 100; | |
| progressBar.style.width = progress + '%'; | |
| const timeLeft = duration - currentTime; | |
| if (!quizShown && timeLeft <= 7 && timeLeft > 0 && !answered) {{ | |
| showQuiz(segments[currentSegmentIndex]); | |
| }} | |
| }} | |
| }}); | |
| playPauseBtn.addEventListener('click', function() {{ | |
| if (mainVideo.paused) {{ | |
| mainVideo.play(); | |
| playPauseBtn.textContent = '⏸'; | |
| }} else {{ | |
| mainVideo.pause(); | |
| playPauseBtn.textContent = '▶'; | |
| }} | |
| }}); | |
| mainVideo.addEventListener('play', function() {{ | |
| playPauseBtn.textContent = '⏸'; | |
| }}); | |
| mainVideo.addEventListener('pause', function() {{ | |
| playPauseBtn.textContent = '▶'; | |
| }}); | |
| // Запускаем первый сегмент | |
| loadNextSegment(); | |
| }})(); | |
| </script> | |
| """ | |
| return html | |
| # Создаем интерфейс | |
| with gr.Blocks(title="Интерактивный Урок") as demo: | |
| gr.Markdown(""" | |
| # 🎓 Интерактивный Урок (Coursera-style) | |
| ### Инструкция: | |
| 1. **Загрузите 3 видео для каждого сегмента:** | |
| - 🎥 Видео лекции (в конце которого задается вопрос) | |
| - ✅ Видео реакции на правильный ответ | |
| - ❌ Видео реакции на неправильный ответ | |
| 2. **Введите варианты ответов** и выберите правильный | |
| 3. Нажмите **"Добавить Сегмент"** (повторите для каждого фрагмента) | |
| 4. Когда все сегменты добавлены, нажмите **"Создать Урок"** | |
| 5. Нажмите **"Запустить Плеер"** для прохождения урока | |
| """) | |
| # Состояния | |
| segments_state = gr.State([]) | |
| meta_state = gr.State(None) | |
| with gr.Tab("📝 Создание Урока"): | |
| gr.Markdown("### Добавление сегмента") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| lecture_input = gr.Video(label="🎥 Видео Лекции") | |
| correct_input = gr.Video(label="✅ Реакция: Правильный ответ") | |
| wrong_input = gr.Video(label="❌ Реакция: Неправильный ответ") | |
| with gr.Column(scale=1): | |
| option1_input = gr.Textbox( | |
| label="Вариант 1", | |
| placeholder="Например: Париж" | |
| ) | |
| option2_input = gr.Textbox( | |
| label="Вариант 2", | |
| placeholder="Например: Лондон" | |
| ) | |
| correct_radio = gr.Radio( | |
| ["Вариант 1", "Вариант 2"], | |
| label="✅ Правильный вариант", | |
| value="Вариант 1" | |
| ) | |
| add_btn = gr.Button("➕ Добавить Сегмент", variant="primary") | |
| segments_info = gr.Markdown("*Нет добавленных сегментов*") | |
| add_btn.click( | |
| fn=add_segment, | |
| inputs=[lecture_input, correct_input, wrong_input, option1_input, option2_input, correct_radio, segments_state], | |
| outputs=[segments_state, segments_info, lecture_input, correct_input, wrong_input] | |
| ) | |
| gr.Markdown("---") | |
| create_btn = gr.Button("🎬 Создать Урок", variant="primary") | |
| create_status = gr.Markdown("") | |
| create_btn.click( | |
| fn=create_lesson, | |
| inputs=[segments_state], | |
| outputs=[create_status, meta_state] | |
| ) | |
| with gr.Tab("▶️ Прохождение Урока"): | |
| gr.Markdown(""" | |
| ### Как проходить урок: | |
| - 🎬 Видео проигрывается автоматически | |
| - ⏱ **За 7 секунд** до конца появятся кнопки с вариантами ответа | |
| - 🎯 Выберите правильный ответ до окончания времени | |
| - ✅ После ответа покажется видео реакции лектора | |
| - ➡️ Затем автоматически начнется следующий сегмент | |
| - 🏆 Пройдите все сегменты до конца! | |
| """) | |
| start_btn = gr.Button("🚀 Запустить Плеер", variant="primary") | |
| player_html = gr.HTML() | |
| start_btn.click( | |
| fn=generate_player_html, | |
| inputs=[meta_state], | |
| outputs=[player_html] | |
| ) | |
| if __name__ == '__main__': | |
| demo.launch() |