diff --git a/backend/app/Controllers/CurriculumController.php b/backend/app/Controllers/CurriculumController.php index c05b52b..bf5e77f 100644 --- a/backend/app/Controllers/CurriculumController.php +++ b/backend/app/Controllers/CurriculumController.php @@ -260,48 +260,60 @@ class CurriculumController return; } - $uploadDir = __DIR__ . '/../../public/uploads/curriculum_videos'; - if (!is_dir($uploadDir)) { - mkdir($uploadDir, 0777, true); - } - - $videoTmp = $_FILES['video_file']['tmp_name']; - $ext = strtolower(pathinfo($_FILES['video_file']['name'], PATHINFO_EXTENSION)) ?: 'mp4'; - - // Semantic, human-readable file naming (e.g. video_grade_10_math_10_unit_01_lesson_01.mp4) - $cleanBase = preg_replace('/[^a-zA-Z0-9_-]+/', '_', str_replace(['.md', '/'], ['', '_'], $file)); - $cleanBase = trim($cleanBase, '_'); - $safeName = 'video_' . $cleanBase . '_' . time() . '.' . $ext; - $destPath = $uploadDir . '/' . $safeName; - - if (!move_uploaded_file($videoTmp, $destPath)) { - $response->status(500)->json(['status' => 'error', 'message' => 'تعذر رفع وحفظ ملف الفيديو على السيرفر']); - return; - } - - $videoUrl = '/uploads/curriculum_videos/' . $safeName; - - // 1. Update Lesson AI assets in manifest.json & filesystem - $currentAssets = CurriculumService::getLessonAiAssets($file); - $currentAssets['ai_video_url'] = $videoUrl; - CurriculumService::saveLessonAiAssets($file, $currentAssets); - - // 2. Direct Sync to MySQL Database Table `lessons` try { - $filename = basename($file, '.md'); - \App\Core\Database::query( - "UPDATE lessons SET ai_video_url = ? WHERE title LIKE ? OR markdown_content LIKE ?", - [$videoUrl, "%{$filename}%", "%{$file}%"] - ); - } catch (\Throwable $dbEx) { - error_log("Video DB sync note: " . $dbEx->getMessage()); - } + $courseId = 0; // System course ID for curriculum AI videos + $title = basename($file, '.md'); - $response->json([ - 'status' => 'success', - 'message' => 'تم رفع وتثبيت فيديو الشرح وربطه بالدرس وقاعدة البيانات بنجاح!', - 'video_url' => $videoUrl, - 'file_name' => $safeName - ]); + // 1. Upload & Transcode to HLS & Push to R2 (handled by VideoService) + $uploadResult = \App\Services\VideoService::handleDirectUpload($_FILES['video_file'], $courseId, $title); + + $videoUrl = $uploadResult['r2_url'] ?? $uploadResult['hls_url']; + + // 2. Insert into lessons table as AI generated version + $lessonId = \App\Core\Database::insert( + "INSERT INTO lessons (course_id, title, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status, ai_video_url) + VALUES (?, ?, 1, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready', ?)", + [ + $courseId, + $title, + $uploadResult['video_uuid'], + $uploadResult['local_path'], + $uploadResult['hls_url'], + $uploadResult['thumbnail_url'], + $uploadResult['duration'] ?? 0, + $videoUrl + ] + ); + + // 3. Trigger Autonomous Zero-Touch AI Analysis & Socratic Checkpoint Generation + \App\Services\AiVideoAnalyzerService::processLessonAutonomously($lessonId); + + // 4. Update Lesson AI assets in manifest.json & filesystem + $currentAssets = CurriculumService::getLessonAiAssets($file); + $currentAssets['ai_video_url'] = $videoUrl; + CurriculumService::saveLessonAiAssets($file, $currentAssets); + + // 5. Update any existing curriculum lesson rows with this video + try { + \App\Core\Database::query( + "UPDATE lessons SET ai_video_url = ? WHERE title LIKE ? OR markdown_content LIKE ?", + [$videoUrl, "%{$title}%", "%{$file}%"] + ); + } catch (\Throwable $dbEx) { + error_log("Video DB sync note: " . $dbEx->getMessage()); + } + + $response->status(201)->json([ + 'status' => 'success', + 'message' => 'تم رفع وتثبيت فيديو الشرح ومعالجته HLS وربطه بالذكاء الاصطناعي بنجاح!', + 'video_url' => $videoUrl, + 'data' => $uploadResult + ]); + } catch (\Throwable $e) { + $response->status(500)->json([ + 'status' => 'error', + 'message' => $e->getMessage() + ]); + } } } diff --git a/backend/app/Controllers/VideoController.php b/backend/app/Controllers/VideoController.php index 355b9b6..7dd7736 100644 --- a/backend/app/Controllers/VideoController.php +++ b/backend/app/Controllers/VideoController.php @@ -415,6 +415,64 @@ class VideoController $chapters = !empty($lesson['timeline_chapters_json']) ? json_decode($lesson['timeline_chapters_json'], true) : []; + // Find available versions (AI vs Teacher specific) + $lessonTitle = $lesson['title']; + $versionsRaw = Database::select( + "SELECT l.id, l.course_id, l.title, l.storage_type, l.video_uuid, l.hls_url, l.ai_video_url, l.bunny_video_id, + c.teacher_id, u.full_name as teacher_name, u.school_name + FROM lessons l + LEFT JOIN courses c ON l.course_id = c.id + LEFT JOIN users u ON c.teacher_id = u.id + WHERE l.title = ? AND l.encoding_status = 'ready'", + [$lessonTitle] + ); + + $availableVersions = []; + $studentSchool = $request->user['school_name'] ?? ''; // if student's school is in the token + + foreach ($versionsRaw as $ver) { + $isAi = ($ver['course_id'] == 0 || $ver['course_id'] == null); + $vPlayback = []; + + if ($ver['storage_type'] === 'api_upload') { + $vPlayback = [ + 'storage_type' => 'api_upload', + 'stream_url' => '/api/videos/stream/' . $ver['video_uuid'], + 'hls_url' => $ver['hls_url'] ?: ('/api/videos/hls/' . $ver['video_uuid'] . '/index.m3u8'), + 'video_url' => $ver['ai_video_url'] ?: ($ver['hls_url'] ?: ('/api/videos/stream/' . $ver['video_uuid'])) + ]; + } else { + $bId = $ver['bunny_video_id'] ?: 'mock-bunny-guid-2026'; + $signed = VideoService::generateBunnySignedPlayback($bId, 10800); + $vPlayback = array_merge(['storage_type' => 'bunny_stream', 'video_url' => $signed['hls_url']], $signed); + } + + $label = $isAi ? 'فيديو الذكاء الاصطناعي الأساسي 🤖' : 'شرح الأستاذ ' . $ver['teacher_name']; + $isRecommended = (!$isAi && !empty($studentSchool) && $ver['school_name'] === $studentSchool); + if ($isRecommended) { + $label .= ' (مدرستك 🏫)'; + } + + $availableVersions[] = [ + 'lesson_id' => (int)$ver['id'], + 'is_ai' => $isAi, + 'teacher_name' => $ver['teacher_name'], + 'school_name' => $ver['school_name'], + 'label' => $label, + 'is_recommended' => $isRecommended, + 'playback' => $vPlayback + ]; + } + + // Sort: Recommended first, then AI, then others + usort($availableVersions, function($a, $b) { + if ($a['is_recommended'] && !$b['is_recommended']) return -1; + if (!$a['is_recommended'] && $b['is_recommended']) return 1; + if ($a['is_ai'] && !$b['is_ai']) return -1; + if (!$a['is_ai'] && $b['is_ai']) return 1; + return 0; + }); + $response->json([ 'status' => 'success', 'data' => [ @@ -426,9 +484,10 @@ class VideoController 'is_free_preview' => (bool)$lesson['is_free_preview'], 'storage_type' => $storageType ], - 'playback' => $playbackInfo, - 'chapters' => $chapters ?: [], - 'checkpoints' => $checkpoints ?: [] + 'playback' => $playbackInfo, + 'available_versions' => $availableVersions, + 'chapters' => $chapters ?: [], + 'checkpoints' => $checkpoints ?: [] ] ]); } diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php index da44165..23c3ac2 100644 --- a/backend/app/Views/StudentPortal.php +++ b/backend/app/Views/StudentPortal.php @@ -789,6 +789,13 @@ class StudentPortal + +
+