'rejected', 'reason' => 'ملف الفيديو غير متاح للتدقيق']; } $mime = (new \finfo(FILEINFO_MIME_TYPE))->file($path) ?: ''; if (!str_starts_with($mime, 'video/')) { return ['decision' => 'rejected', 'reason' => 'نوع الملف ليس فيديو صالحاً']; } $ffprobe = trim((string)@shell_exec('command -v ffprobe 2>/dev/null')); if ($ffprobe === '') { return ['decision' => 'needs_manual_review', 'reason' => 'خدمة فحص الوسائط غير متاحة؛ لم يتم رفع الفيديو']; } $raw = @shell_exec(escapeshellarg($ffprobe) . ' -v error -show_entries format=duration -show_streams -of json ' . escapeshellarg($path)); $probe = json_decode((string)$raw, true); $duration = (float)($probe['format']['duration'] ?? 0); $hasVideo = false; $hasAudio = false; foreach (($probe['streams'] ?? []) as $stream) { $hasVideo = $hasVideo || (($stream['codec_type'] ?? '') === 'video'); $hasAudio = $hasAudio || (($stream['codec_type'] ?? '') === 'audio'); } if (!$hasVideo || !$hasAudio || $duration <= 0 || $duration > 1500) { return ['decision' => 'rejected', 'reason' => 'الفيديو يجب أن يحتوي صورة وصوتاً وأن لا يتجاوز 25 دقيقة', 'duration_seconds' => $duration]; } $key = trim((string)getenv('GEMINI_API_KEY')); if ($key === '') return ['decision' => 'needs_manual_review', 'reason' => 'Gemini غير مهيأ؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration]; // Extract a representative frame; Gemini receives real media evidence, // not only the title or metadata supplied by the teacher. $frame = tempnam(sys_get_temp_dir(), 'saqel_audit_') . '.jpg'; $ffmpeg = trim((string)@shell_exec('command -v ffmpeg 2>/dev/null')); if ($ffmpeg === '' || @shell_exec(escapeshellarg($ffmpeg) . ' -y -ss ' . escapeshellarg((string)max(1, (int)($duration / 3))) . ' -i ' . escapeshellarg($path) . ' -frames:v 1 -q:v 3 ' . escapeshellarg($frame) . ' 2>/dev/null') === null || !is_file($frame)) { return ['decision' => 'needs_manual_review', 'reason' => 'تعذر استخراج لقطة للتقييم؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration]; } try { $prompt = "أنت مدقق جودة تربوية لمنصة صقل. قيّم اللقطة الفعلية من فيديو تعليمي بعنوان: {$title}. المادة: {$subject}. مدة الفيديو: {$duration} ثانية. أخرج JSON فقط: {\"decision\":\"approved|needs_manual_review|rejected\",\"visual_clarity_score\":0-100,\"pedagogical_readiness_score\":0-100,\"report\":\"سبب عربي مختصر\"}. ارفض المحتوى غير التعليمي أو غير الواضح. لا تمنح الموافقة إن لم تظهر أدلة كافية."; $payload = [ 'contents' => [[ 'parts' => [ ['text' => $prompt], ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => base64_encode((string)file_get_contents($frame))],], ], ]], 'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.1], ]; $ch = curl_init('https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=' . rawurlencode($key)); curl_setopt_array($ch, [CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>json_encode($payload),CURLOPT_HTTPHEADER=>['Content-Type: application/json'],CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>30]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $text = json_decode((string)$body, true)['candidates'][0]['content']['parts'][0]['text'] ?? ''; $report = json_decode($text, true); if ($code !== 200 || !is_array($report) || !in_array($report['decision'] ?? '', ['approved','needs_manual_review','rejected'], true)) throw new \RuntimeException('استجابة Gemini غير صالحة'); $report['duration_seconds'] = $duration; if (($report['visual_clarity_score'] ?? 0) < 85 || ($report['pedagogical_readiness_score'] ?? 0) < 85) $report['decision'] = 'needs_manual_review'; return $report; } catch (\Throwable $e) { return ['decision' => 'needs_manual_review', 'reason' => 'تعذر إكمال تحليل Gemini؛ لم يتم رفع الفيديو']; } finally { @unlink($frame); } } /** * التحليل الآلي المستقل للفيديو وإنشاء الفصول ونقاط الفحص السقراطي في قاعدة البيانات * * @param int $lessonId معرّف الدرس * @return array مصفوفة تحتوي على الفصول ونقاط الفحص وحالة التنفيذ */ public static function processLessonAutonomously(int $lessonId): array { CurriculumService::ensureSchema(); $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); if (!$lesson) { return ['status' => 'error', 'message' => 'الدرس غير موجود']; } $courseId = (int)$lesson['course_id']; $lessonTitle = $lesson['title']; $duration = (int)($lesson['duration_seconds'] ?: 600); $curriculum = CurriculumService::getCurriculumContext($courseId, $lessonTitle); // Perform Gemini AI or Curriculum-grounded Analysis $analysisResult = self::generateAnalysis($lessonTitle, $duration, $curriculum); // 1. Save Timeline Chapters to lessons table if (!empty($analysisResult['timeline_chapters'])) { Database::query( "UPDATE lessons SET timeline_chapters_json = ? WHERE id = ?", [json_encode($analysisResult['timeline_chapters'], JSON_UNESCAPED_UNICODE), $lessonId] ); } // 2. Clear old checkpoints and their questions for this lesson $oldExams = Database::select("SELECT id FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]); foreach ($oldExams as $oe) { Database::query("DELETE FROM exams WHERE id = ?", [$oe['id']]); } // 3. Save Socratic Checkpoints into exams, questions, question_options if (!empty($analysisResult['socratic_checkpoints'])) { foreach ($analysisResult['socratic_checkpoints'] as $cp) { try { $examUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $examId = (int)Database::insert( "INSERT INTO exams (uuid, course_id, lesson_id, creator_type, scope, title, timestamp_seconds, rewind_on_fail_seconds, passing_percentage, total_points, is_mandatory, is_published) VALUES (?, ?, ?, 'ai_adaptive', 'in_video_checkpoint', ?, ?, ?, 100.00, 10, 1, 1)", [ $examUuid, $courseId, $lessonId, $cp['question_text'], (int)$cp['timestamp_seconds'], (int)($cp['rewind_seconds'] ?? 45) ] ); $qUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $qId = (int)Database::insert( "INSERT INTO questions (uuid, exam_id, question_text, question_type, bloom_taxonomy, explanation_text, points) VALUES (?, ?, ?, 'multiple_choice', 'comprehension', ?, 10)", [ $qUuid, $examId, $cp['question_text'], $cp['explanation'] ?? 'تطبيق مباشر لقواعد المنهاج المعتمد' ] ); $correctIdx = (int)($cp['correct_index'] ?? 0); if (!empty($cp['options']) && is_array($cp['options'])) { foreach ($cp['options'] as $idx => $optText) { Database::insert( "INSERT INTO question_options (question_id, option_text, is_correct) VALUES (?, ?, ?)", [$qId, (string)$optText, ($idx === $correctIdx) ? 1 : 0] ); } } } catch (\Throwable $e) { error_log("Checkpoint insert error: " . $e->getMessage()); } } } // 4. Perform AI Pedagogical Quality & Ministry Alignment Assessment $qualityAssessment = self::evaluatePedagogicalQuality($lessonId, $lessonTitle, $duration, $curriculum); return [ 'status' => 'success', 'lesson_id' => $lessonId, 'timeline_chapters' => $analysisResult['timeline_chapters'], 'checkpoints_count' => count($analysisResult['socratic_checkpoints'] ?? []), 'quality_assessment' => $qualityAssessment ]; } /** * AI Pedagogical & Curriculum Alignment Evaluation Engine * Evaluates video clarity, Bloom taxonomy coverage, and target learning outcomes */ public static function evaluatePedagogicalQuality(int $lessonId, string $lessonTitle, int $duration, array $curriculum): array { try { $geminiKey = getenv('GEMINI_API_KEY'); $alignmentScore = 0.0; $clarityScore = 0.0; $outcomes = [ "استيعاب المفهوم الرياضي/العلمي لدرس {$lessonTitle}", "تطبيق القواعد والقوانين المعتمدة في كتاب الوزارة", "حل المسائل والتمارين النموذجية بدقة وبناء استراتيجية التفكير السليم" ]; $bloom = [ 'recall' => 20, 'comprehension' => 40, 'application' => 30, 'analysis' => 10 ]; $critique = "لم يكتمل تدقيق Gemini؛ تتطلب الحصة مراجعة بشرية."; $status = 'needs_manual_review'; if (!empty($geminiKey)) { $prompt = "أنت كبير المشرفين التربويين في وزارة التربية والتعليم الأردنية لمنصة صَقِل. قم بتقييم جودة الحصة التعليمية وتحديد نتاجات التعلم: الدرس: '{$lessonTitle}' المادة: {$curriculum['subject']} المواضيع: " . implode(' | ', $curriculum['core_topics']) . " مدة الحصة بالثواني: {$duration} أخرج JSON حصري بالهيكل التالي: { \"curriculum_alignment_score\": 97.0, \"pedagogical_clarity_score\": 95.0, \"learning_outcomes\": [\"نتاج التعلم 1\", \"نتاج التعلم 2\", \"نتاج التعلم 3\"], \"bloom_coverage\": {\"recall\": 15, \"comprehension\": 35, \"application\": 40, \"analysis\": 10}, \"ai_critique\": \"تقرير الجودة التربوي المعتمد\", \"approval_status\": \"approved_official\" }"; $url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=" . $geminiKey; $payload = [ 'contents' => [['parts' => [['text' => $prompt]]]], 'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.2] ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15 ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && !empty($response)) { $json = json_decode($response, true); $text = $json['candidates'][0]['content']['parts'][0]['text'] ?? ''; $p = json_decode($text, true); if (!empty($p['curriculum_alignment_score'])) { $alignmentScore = (float)$p['curriculum_alignment_score']; $clarityScore = (float)($p['pedagogical_clarity_score'] ?? 95.0); if (!empty($p['learning_outcomes'])) $outcomes = $p['learning_outcomes']; if (!empty($p['bloom_coverage'])) $bloom = $p['bloom_coverage']; if (!empty($p['ai_critique'])) $critique = $p['ai_critique']; if (!empty($p['approval_status'])) $status = $p['approval_status']; } } } // Save to video_quality_assessments table Database::query( "INSERT INTO video_quality_assessments (lesson_id, curriculum_alignment_score, pedagogical_clarity_score, learning_outcomes_json, bloom_coverage_json, ai_critique_text, approval_status, evaluated_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE curriculum_alignment_score = VALUES(curriculum_alignment_score), pedagogical_clarity_score = VALUES(pedagogical_clarity_score), learning_outcomes_json = VALUES(learning_outcomes_json), bloom_coverage_json = VALUES(bloom_coverage_json), ai_critique_text = VALUES(ai_critique_text), approval_status = VALUES(approval_status), evaluated_at = NOW()", [ $lessonId, $alignmentScore, $clarityScore, json_encode($outcomes, JSON_UNESCAPED_UNICODE), json_encode($bloom, JSON_UNESCAPED_UNICODE), $critique, $status ] ); return [ 'alignment_score' => $alignmentScore, 'clarity_score' => $clarityScore, 'learning_outcomes' => $outcomes, 'bloom_coverage' => $bloom, 'critique' => $critique, 'status' => $status ]; } catch (\Throwable $e) { error_log("Pedagogical quality assessment error: " . $e->getMessage()); } return ['alignment_score' => 0.0, 'clarity_score' => 0.0, 'status' => 'needs_manual_review']; } /** * Generate structured analysis with strict curriculum grounding */ private static function generateAnalysis(string $lessonTitle, int $duration, array $curriculum): array { $geminiKey = getenv('GEMINI_API_KEY'); if (!empty($geminiKey)) { try { $prompt = "أنت خبير تربوي ومحلل جنائي لمناهج وزارة التربية والتعليم الأردنية لمنصة صَقِل. حلل درس: '{$lessonTitle}' المنهاج الرسمي: {$curriculum['subject']} - {$curriculum['unit']} المواضيع المعتمدة: " . implode(' | ', $curriculum['core_topics']) . " مدة الدرس بالثواني: {$duration} المطلوب إخراج JSON حصري بالهيكل التالي: { \"timeline_chapters\": [ {\"start_seconds\": 0, \"end_seconds\": 180, \"title\": \"المفهوم العام والتمهيد\", \"summary\": \"توضيح الفكرة الأساسية\"}, {\"start_seconds\": 180, \"end_seconds\": 360, \"title\": \"عرض القانون والقواعد الأساسية\", \"summary\": \"شرح خطوات القاعدة\"}, {\"start_seconds\": 360, \"end_seconds\": {$duration}, \"title\": \"تطبيقات وأمثلة نموذجية\", \"summary\": \"حل مسائل وزارية معيارية\"} ], \"socratic_checkpoints\": [ { \"timestamp_seconds\": 180, \"question_text\": \"سؤال فحص فهم حول ما تم شرحه في أول 3 دقائق فقط\", \"options\": [\"الخيار الصحيح\", \"خيار خطأ 1\", \"خيار خطأ 2\", \"خيار خطأ 3\"], \"correct_index\": 0, \"rewind_seconds\": 45, \"explanation\": \"توضيح القاعدة الوزارية\" }, { \"timestamp_seconds\": 360, \"question_text\": \"سؤال حول تطبيق القانون المشروح حتى الدقيقة السادسة\", \"options\": [\"خيار خطأ 1\", \"الخيار الصحيح\", \"خيار خطأ 2\", \"خيار خطأ 3\"], \"correct_index\": 1, \"rewind_seconds\": 45, \"explanation\": \"توضيح خطوات الحل\" } ] } قاعدة صارمة: السؤال عند أي دقيقة يسأل فقط عما تم شرحه قبل ذلك التوقيت، وممنوع نهائياً الاستعانة بأي معلومة خارج المنهاج."; $url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=" . $geminiKey; $payload = [ 'contents' => [ ['parts' => [['text' => $prompt]]] ], 'generationConfig' => [ 'responseMimeType' => 'application/json', 'temperature' => 0.2 ] ]; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20 ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && !empty($response)) { $json = json_decode($response, true); $text = $json['candidates'][0]['content']['parts'][0]['text'] ?? ''; $parsed = json_decode($text, true); if (!empty($parsed['timeline_chapters']) && !empty($parsed['socratic_checkpoints'])) { return $parsed; } } } catch (\Throwable $e) { error_log("Gemini API call notice: " . $e->getMessage()); } } // High-Precision Curriculum Grounded Engine (Offline / Safe Fallback) return self::buildGroundedCurriculumAnalysis($lessonTitle, $duration, $curriculum); } /** * High-Precision Curriculum Grounded Generator */ private static function buildGroundedCurriculumAnalysis(string $title, int $duration, array $curriculum): array { $isMilitary = ($curriculum['subject'] === 'التربية الوطنية والثقافة العسكرية'); $duration = max(5, $duration); // Dynamic Time Segmentation based on real duration if ($duration <= 45) { // Short Video (e.g. 10s AI generated video) $c1 = max(1, (int)round($duration * 0.3)); $c2 = max(2, (int)round($duration * 0.7)); $cp1 = max(1, (int)round($duration * 0.3)); $cp2 = max(2, (int)round($duration * 0.7)); $rewind = 3; } else { // Full Length Standard Lesson (e.g. 10 - 45 minutes) $c1 = max(60, (int)round($duration * 0.25)); $c2 = max(120, (int)round($duration * 0.60)); $cp1 = max(60, (int)round($duration * 0.25)); $cp2 = max(120, (int)round($duration * 0.60)); $rewind = 45; } if ($isMilitary) { return [ 'timeline_chapters' => [ ['start_seconds' => 0, 'end_seconds' => $c1, 'title' => 'النشأة والتأسيس التاريخي', 'summary' => 'مراحل تشكيل القوات المسلحة الأردنية — الجيش العربي منذ عام 1921.'], ['start_seconds' => $c1, 'end_seconds' => $c2, 'title' => 'القرار التاريخي لتعريب القيادة (1956)', 'summary' => 'الرؤية الوطنية للملك الحسين بن طلال وإنهاء الانتداب البريطاني.'], ['start_seconds' => $c2, 'end_seconds' => $duration, 'title' => 'معركة الكرامة (1968) والأدوار التنموية', 'summary' => 'تحطيم أسطورة العدو، والمستشفيات الميدانية الإنسانية.'] ], 'socratic_checkpoints' => [ [ 'timestamp_seconds' => $cp1, 'question_text' => 'في أي عام تم تأسيس الجيش العربي الأردني؟', 'options' => ['عام 1921 في عهد الملك المؤسس عبدالله الأول', 'عام 1956', 'عام 1968', 'عام 1946'], 'correct_index' => 0, 'rewind_seconds' => $rewind, 'explanation' => 'تأسس الجيش العربي عام 1921 مع تأسيس إمارة شرق الأردن.' ], [ 'timestamp_seconds' => $cp2, 'question_text' => 'متى تم اتخاذ القرار التاريخي بتعريب قيادة الجيش العربي؟', 'options' => ['1 آذار 1956 بقيادة الملك الحسين بن طلال', '21 آذار 1968', '11 نيسان 1921', '25 أيار 1946'], 'correct_index' => 0, 'rewind_seconds' => $rewind, 'explanation' => 'صدر قرار تعريب القيادة التاريخي في 1 آذار 1956.' ] ] ]; } // Grade 10 Mathematics: Systems of Equations (أنظمة المعادلات) $isEquations = (str_contains($title, 'معادلات') || str_contains($title, 'أنظمة') || str_contains($curriculum['unit'] ?? '', 'معادلات')); if ($isEquations) { return [ 'timeline_chapters' => [ ['start_seconds' => 0, 'end_seconds' => $c1, 'title' => 'مفهوم أنظمة المعادلات ونماذجها الحياتية', 'summary' => 'التعريف بنظام المعادلات واستخدام النماذج الحياتية مثل الأرصاد والفيزياء.'], ['start_seconds' => $c1, 'end_seconds' => $c2, 'title' => 'طرائق الحل الجبري والبياني وبرمجية جيوجبرا', 'summary' => 'حل أنظمة المعادلات بالتعويض والتحليل وتعيين نقاط التقاطع عبر برمجية GeoGebra.'], ['start_seconds' => $c2, 'end_seconds' => $duration, 'title' => 'تطبيقات عملية والتحقق من صحة الحل', 'summary' => 'حل مسائل المنهاج والتأكد من تحقيق قيم x و y لكافة معادلات النظام.'] ], 'socratic_checkpoints' => [ [ 'timestamp_seconds' => $cp1, 'question_text' => 'ماذا تمثّل نقطة تقاطع منحنيين في المستوى الإحداثي لنظام معادلات؟', 'options' => [ 'حل مشترك يُحقق كِلا المعادلتين معاً في آن واحد', 'المسافة الرأسية بين المنحنيين', 'معادلة خط التقارب الأفقي فقط', 'المقطع الصادي لأحد المنحنيين دون الآخر' ], 'correct_index' => 0, 'rewind_seconds' => $rewind, 'explanation' => 'نقطة تقاطع أي منحنيين هندسياً تمثل الزوج المرتب (x, y) الذي يحقق كلتا المعادلتين في آن واحد، وهو حل النظام.' ], [ 'timestamp_seconds' => $cp2, 'question_text' => 'عند حل نظام مكوّن من معادلة خطية ومعادلة تربيعية، ما هو أقصى عدد ممكن من الحلول الحقيقية؟', 'options' => [ 'حلّان حقيقيان (نقطتا تقاطع)', 'ثلاثة حلول حقيقية دائماً', 'أربعة حلول حقيقية', 'لا يمكن أن يتقاطعا أبداً' ], 'correct_index' => 0, 'rewind_seconds' => $rewind, 'explanation' => 'المستقيم يقطع القطع المكافئ في نقطتين كحد أقصى (حلان)، أو يمسه في نقطة واحدة (حل واحد)، أو لا يقطعه (لا يوجد حل حقيقي).' ] ] ]; } // Safe generic fallback: never inject unrelated advanced topics when Gemini is unavailable. $topics = array_values(array_filter(array_map('strval', $curriculum['core_topics'] ?? []))); $topic = $topics[0] ?? $title; $topicTwo = $topics[1] ?? $topic; return [ 'timeline_chapters' => [ ['start_seconds' => 0, 'end_seconds' => $c1, 'title' => 'تمهيد الدرس', 'summary' => "التعريف بموضوع {$topic} وأهداف درس {$title}."], ['start_seconds' => $c1, 'end_seconds' => $c2, 'title' => 'المفهوم الأساسي', 'summary' => "شرح الفكرة والقواعد المرتبطة بـ {$topic}."], ['start_seconds' => $c2, 'end_seconds' => $duration, 'title' => 'التطبيق والمراجعة', 'summary' => "تطبيق ما ورد في الدرس ومراجعة {$topicTwo}."] ], 'socratic_checkpoints' => [ [ 'timestamp_seconds' => $cp1, 'question_text' => "ما الموضوع الذي يركّز عليه درس {$title}؟", 'options' => [$topic, $topicTwo, 'موضوع خارج نطاق هذا الدرس', 'لا شيء مما سبق'], 'correct_index' => 0, 'rewind_seconds' => $rewind, 'explanation' => "يركّز الدرس على {$topic} وفق سياق المنهاج المعتمد." ], [ 'timestamp_seconds' => $cp2, 'question_text' => "أي عبارة أقرب إلى الفكرة التي يجب مراجعتها في هذا الدرس؟", 'options' => [$topicTwo, $topic, 'قاعدة من مادة أخرى', 'معلومة غير مرتبطة بالدرس'], 'correct_index' => 0, 'rewind_seconds' => $rewind, 'explanation' => "هذه النقطة مرتبطة بنتائج درس {$title} وليست بمادة أخرى." ] ] ]; } }