From fe5a71bcadeee0f9954e4d6ca6157b38c3cb6bf6 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 28 Aug 2026 16:24:59 +0300 Subject: [PATCH] Update Saqel Platform: 2026-08-28 16:24:59 --- backend/scripts/curriculum_worker.php | 285 ++++++++++++-------------- 1 file changed, 126 insertions(+), 159 deletions(-) diff --git a/backend/scripts/curriculum_worker.php b/backend/scripts/curriculum_worker.php index cf0a3ea..e2872c9 100644 --- a/backend/scripts/curriculum_worker.php +++ b/backend/scripts/curriculum_worker.php @@ -32,183 +32,150 @@ function updateState($stateFile, $status, $progress, $message, $extra = []) { $state = json_decode(file_get_contents($stateFile), true); $origName = $state['orig_name'] ?? 'Unknown.pdf'; -updateState($stateFile, 'extracting', 10, 'جاري استخراج النصوص بالكامل من الكتاب...'); - -// 1. Extract Text -$cmd = "pdftotext -layout " . escapeshellarg($pdfFile) . " - 2>/dev/null"; -$rawText = @shell_exec($cmd); - -if (empty($rawText) || mb_strlen(trim($rawText)) < 100) { - updateState($stateFile, 'analyzing', 20, 'الملف عبارة عن صور (Scanned). جاري بناء هيكل تصنيفي (Minhaji-Style) استناداً إلى البيانات الوصفية للمادة...'); - $rawText = "Unit 1: مقدمة\nLesson 1: نظرة عامة\nUnit 2: المفاهيم الأساسية\nLesson 1: استكشاف المفاهيم"; - - // If it's the Math 10th grade book, inject Minhaji text for heuristic parser - if (mb_strpos($origName, 'الرياضيات') !== false && mb_strpos($origName, 'العاشر') !== false) { - $rawText = "الوحدة الأولى: الأسس والمعادلات\nالدرس الأول: حل معادلات خطية\nالدرس الثاني: حل معادلات تربيعية\nالوحدة الثانية: الدائرة\nالدرس الأول: أوتار الدائرة ومماساتها\nالوحدة الثالثة: حساب المثلثات\nالدرس الأول: النسب المثلثية"; - } -} - -updateState($stateFile, 'analyzing', 40, 'جاري تحليل بنية الوحدات والدروس والمصادر الإضافية...'); - Env::load(__DIR__ . '/../.env'); $geminiKey = Env::get('GEMINI_API_KEY') ?: getenv('GEMINI_API_KEY'); -$parsedStructure = null; +if (empty($geminiKey)) { + updateState($stateFile, 'error', 0, '⚠️ لم يتم العثور على مفتاح الذكاء الاصطناعي (GEMINI_API_KEY) في ملف .env. النظام يحتاج إلى الـ AI لقراءة وتفريغ الكتب (بما فيها الكتب المصورة والرياضيات) بشكل حقيقي.'); + exit; +} -if (!empty($geminiKey) && mb_strlen(trim($rawText)) > 1000) { - updateState($stateFile, 'analyzing', 50, 'يتم الآن تحليل المنهج عبر الذكاء الاصطناعي (Gemini)...'); - - $prompt = "أنت خبير مناهج تعليمية. قم بتحليل هذا النص المستخرج من كتاب دراسي بعنوان '{$origName}' واستخراج الفهرس والوحدات والدروس بدقة. -النص: -" . mb_substr($rawText, 0, 15000) . " +updateState($stateFile, 'uploading_to_ai', 20, 'جاري رفع الكتاب إلى محرك الذكاء الاصطناعي (Gemini Vision) لقراءته بالكامل...'); -المطلوب إرجاع JSON صالح فقط بالصيغة التالية (يجب أن يحتوي على النص الحقيقي للدرس وليس نصاً وهمياً): +// 1. Upload PDF to Gemini File API +$fileSize = filesize($pdfFile); +$uploadUrl = "https://generativelanguage.googleapis.com/upload/v1beta/files?uploadType=media&key=" . $geminiKey; + +$ch = curl_init($uploadUrl); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents($pdfFile)); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/pdf', + 'Content-Length: ' . $fileSize +]); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 5 minutes for upload +$uploadRes = curl_exec($ch); +$uploadCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($uploadCode !== 200) { + updateState($stateFile, 'error', 0, 'فشل رفع الملف إلى الذكاء الاصطناعي. كود الخطأ: ' . $uploadCode . ' التفاصيل: ' . $uploadRes); + exit; +} + +$uploadData = json_decode($uploadRes, true); +$fileUri = $uploadData['file']['uri'] ?? ''; +$fileNameGemini = $uploadData['file']['name'] ?? ''; + +if (empty($fileUri)) { + updateState($stateFile, 'error', 0, 'لم يتم الحصول على URI من الذكاء الاصطناعي.'); + exit; +} + +updateState($stateFile, 'analyzing', 40, 'الكتاب الآن في عقل الذكاء الاصطناعي... جاري المعالجة البصرية واستخراج المعادلات والنصوص...'); + +// Wait for file to be ACTIVE (Gemini needs time to process PDF) +$maxRetries = 15; +$isReady = false; +for ($i = 0; $i < $maxRetries; $i++) { + $ch = curl_init("https://generativelanguage.googleapis.com/v1beta/{$fileNameGemini}?key={$geminiKey}"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $statRes = curl_exec($ch); + curl_close($ch); + $statData = json_decode($statRes, true); + if (($statData['state'] ?? '') === 'ACTIVE') { + $isReady = true; + break; + } + sleep(3); // wait 3 seconds before polling again +} + +if (!$isReady) { + updateState($stateFile, 'error', 0, 'انتهى وقت الانتظار أثناء معالجة الذكاء الاصطناعي لملف الـ PDF.'); + exit; +} + +updateState($stateFile, 'analyzing', 60, 'جاري تفريغ المحتوى وتشكيل دروس المارك داون والمصادر الملحقة...'); + +// 2. Prompt Gemini +$prompt = "أنت خبير مناهج تعليمية ورياضيات. لقد قمت برفع كتاب دراسي بصيغة PDF. +اقرأ هذا الكتاب بأكمله بدقة. استخرج الهيكل الشجري للمنهاج، بما في ذلك جميع الوحدات والدروس. +لكل درس، استخرج **النص الحقيقي الفعلي** الموجود في الكتاب بدقة متناهية. إذا كان هناك معادلات رياضية أو أرقام أو نصوص تفسيرية، اكتبها بالكامل باستخدام تنسيق Markdown و LaTeX (مثل $x^2 + y^2 = r^2$). +لا تستخدم نصوصاً وهمية أبداً! إذا لم تستطع قراءة كل الدروس، اقرأ الدروس الأولى بشكل كامل جداً من الكتاب وضع ملخصاً للباقي. + +أريد الإجابة بصيغة JSON صالحة 100% فقط بدون أي نص خارج الـ JSON: { - \"grade_name\": \"اسم الصف\", - \"grade_key\": \"grade_X\", - \"subject_name\": \"اسم المادة\", - \"subject_key\": \"subject_key\", - \"semester_name\": \"الفصل الدراسي الأول\", + \"grade_name\": \"اسم الصف (مثال: الصف العاشر)\", + \"grade_key\": \"grade_10\", + \"subject_name\": \"اسم المادة (مثال: الرياضيات)\", + \"subject_key\": \"math_10\", + \"semester_name\": \"الفصل الدراسي\", \"semester_key\": \"semester_1\", - \"units\": [ ... ], + \"units\": [ + { + \"unit_key\": \"unit_1\", + \"unit_name\": \"اسم الوحدة الحقيقي\", + \"lessons\": [ + { + \"lesson_id\": \"lesson_1\", + \"title\": \"اسم الدرس الحقيقي\", + \"outcomes\": [\"نتاج 1 حقيقي\"], + \"markdown_content\": \"# عنوان الدرس\\n\\n[هنا النص الحقيقي والشرح والمعادلات كما ورد في الكتاب المرفوع]\" + } + ] + } + ], \"resources\": { - \"worksheets\": { \"name\": \"أوراق عمل\", \"items\": [] } + \"worksheets\": { \"name\": \"أوراق عمل\", \"items\": [] }, + \"exams\": { \"name\": \"اختبارات\", \"items\": [] } } }"; - $url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" . $geminiKey; - $payload = [ - 'contents' => [['parts' => [['text' => $prompt]]]], - 'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.2] - ]; +$payload = [ + 'contents' => [ + [ + 'parts' => [ + ['fileData' => ['mimeType' => 'application/pdf', 'fileUri' => $fileUri]], + ['text' => $prompt] + ] + ] + ], + 'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.1] +]; - $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 => 60 - ]); - $res = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); +// Using gemini-1.5-pro since it is powerful for large PDFs and math formulas +$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key=" . $geminiKey; - if ($code === 200 && !empty($res)) { - $json = json_decode($res, true); - $text = $json['candidates'][0]['content']['parts'][0]['text'] ?? ''; - $parsedStructure = json_decode($text, true); - } +$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 => 300 // AI might take 2 minutes +]); +$res = curl_exec($ch); +$code = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($code !== 200 || empty($res)) { + updateState($stateFile, 'error', 0, 'فشل توليد المحتوى من الذكاء الاصطناعي. قد يكون الملف ضخماً جداً أو الـ API Key غير صالح.'); + exit; } -// Fallback to Heuristic Regex Parser if Gemini failed or no key +$jsonResponse = json_decode($res, true); +$text = $jsonResponse['candidates'][0]['content']['parts'][0]['text'] ?? ''; +$parsedStructure = json_decode($text, true); + if (empty($parsedStructure) || empty($parsedStructure['units'])) { - updateState($stateFile, 'analyzing', 60, 'يتم تحليل البنية باستخدام محرك التحليل الديناميكي العميق للنصوص...'); - - $cleanTitle = trim(preg_replace('/\.[^.]+$/u', '', $origName)); - $chunks = preg_split('/(Unit\s+\d+|Module\s+\d+|الوحدة\s+(?:الأولى|الثانية|الثالثة|الرابعة|الخامسة|\d+))/iu', $rawText, -1, PREG_SPLIT_DELIM_CAPTURE); - - $units = []; - $unitCounter = 1; - - for ($i = 1; $i < count($chunks); $i += 2) { - $unitTitleRaw = trim($chunks[$i]); - $unitContentRaw = trim($chunks[$i+1] ?? ''); - - $lines = explode("\n", $unitContentRaw); - $unitNameAddition = trim($lines[0] ?? ''); - $unitName = $unitTitleRaw . ($unitNameAddition ? ': ' . mb_substr($unitNameAddition, 0, 40) : ''); - - $lessonChunks = preg_split('/(Lesson\s+\d+|الدرس\s+(?:الأول|الثاني|الثالث|الرابع|\d+))/iu', $unitContentRaw, -1, PREG_SPLIT_DELIM_CAPTURE); - $lessons = []; - $lessonCounter = 1; - - if (count($lessonChunks) > 1) { - for ($j = 1; $j < count($lessonChunks); $j += 2) { - $lessonTitleRaw = trim($lessonChunks[$j]); - $lessonContentRaw = trim($lessonChunks[$j+1] ?? ''); - $lLines = explode("\n", $lessonContentRaw); - $lNameAddition = trim($lLines[0] ?? ''); - - $lessons[] = [ - 'lesson_id' => 'lesson_' . $lessonCounter, - 'title' => $lessonTitleRaw . ' ' . mb_substr($lNameAddition, 0, 40), - 'outcomes' => ['تم استخراج النص تلقائياً من الكتاب'], - 'markdown_content' => "# " . $lessonTitleRaw . "\n\n" . mb_substr($lessonContentRaw, 0, 5000) - ]; - $lessonCounter++; - } - } else { - $textLen = mb_strlen($unitContentRaw); - $chunkSize = 2500; - for ($c = 0; $c < $textLen; $c += $chunkSize) { - $lessons[] = [ - 'lesson_id' => 'part_' . $lessonCounter, - 'title' => 'Part ' . $lessonCounter, - 'outcomes' => ['نص مستخرج آلياً'], - 'markdown_content' => "# Part " . $lessonCounter . "\n\n" . mb_substr($unitContentRaw, $c, $chunkSize) - ]; - $lessonCounter++; - } - } - - $units[] = [ - 'unit_key' => 'unit_' . $unitCounter, - 'unit_name' => $unitName, - 'lessons' => $lessons - ]; - $unitCounter++; - } - - if (empty($units)) { - $textLen = mb_strlen($rawText); - $chunkSize = 3000; - $lessons = []; - for ($c = 0, $idx=1; $c < $textLen && $idx <= 10; $c += $chunkSize, $idx++) { - $lessons[] = [ - 'lesson_id' => 'section_' . $idx, - 'title' => 'Section ' . $idx, - 'outcomes' => ['نص حقيقي مستخرج آلياً'], - 'markdown_content' => "# Section " . $idx . "\n\n" . mb_substr($rawText, $c, $chunkSize) - ]; - } - $units[] = [ - 'unit_key' => 'unit_1', - 'unit_name' => 'Textbook Content', - 'lessons' => $lessons - ]; - } - - // Add Minhaji-style resources properly - $resources = [ - "textbooks" => ["name" => "الكتب المقررة", "items" => []], - "teacher_guides" => ["name" => "دليل المعلم", "items" => []], - "worksheets" => ["name" => "أوراق عمل", "items" => [ - ["title" => "ورقة عمل مستخرجة", "file" => "grade_extracted/subject_" . substr(md5($cleanTitle), 0, 8) . "/semester_1/resources/worksheet_1.md"] - ]], - "exams" => ["name" => "اختبارات", "items" => [ - ["title" => "اختبار وحدة مستخرج", "file" => "grade_extracted/subject_" . substr(md5($cleanTitle), 0, 8) . "/semester_1/resources/exam_1.md"] - ]], - "answers" => ["name" => "إجابات أسئلة الكتاب", "items" => []], - "remedial" => ["name" => "مادة التدخلات العلاجية", "items" => []], - "learning_loss" => ["name" => "الفاقد التعليمي", "items" => []], - "summaries" => ["name" => "ملخصات", "items" => []] - ]; - - $parsedStructure = [ - 'grade_name' => 'الصف الأساسي / مستخرج', - 'grade_key' => 'grade_extracted', - 'subject_name' => $cleanTitle, - 'subject_key' => 'subject_' . substr(md5($cleanTitle), 0, 8), - 'semester_name' => 'الفصل المستخرج', - 'semester_key' => 'semester_1', - 'units' => $units, - 'resources' => $resources - ]; + updateState($stateFile, 'error', 0, 'فشل الذكاء الاصطناعي في إرجاع بنية JSON صالحة.'); + exit; } -updateState($stateFile, 'generating', 80, 'جاري بناء هيكل المنهاج وتوزيع المصادر (أوراق عمل، امتحانات)...'); +updateState($stateFile, 'generating', 80, 'الذكاء الاصطناعي انتهى من القراءة! جاري بناء الملفات وحفظها على السيرفر...'); +// Save to disk $updatedTree = CurriculumService::mergeExtractedCurriculum($parsedStructure); $firstLessonFile = ''; @@ -217,7 +184,7 @@ if (!empty($parsedStructure['units'][0]['lessons'][0])) { $firstLessonFile = "{$parsedStructure['grade_key']}/{$parsedStructure['subject_key']}/{$parsedStructure['semester_key']}/{$parsedStructure['units'][0]['unit_key']}/{$firstLes['lesson_id']}.md"; } -updateState($stateFile, 'completed', 100, 'تم الاستخراج والفهرسة بنجاح!', [ +updateState($stateFile, 'completed', 100, 'تم الاستخراج المعتمد على الـ AI بالكامل بنجاح!', [ 'active_file' => $firstLessonFile, 'tree' => $updatedTree, 'extracted_data' => $parsedStructure