Lesson / app.py
AlserFurma's picture
Update app.py
df8ede2 verified
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()