Update Saqel Platform: 2026-08-28 16:24:59
This commit is contained in:
@@ -32,183 +32,150 @@ function updateState($stateFile, $status, $progress, $message, $extra = []) {
|
|||||||
$state = json_decode(file_get_contents($stateFile), true);
|
$state = json_decode(file_get_contents($stateFile), true);
|
||||||
$origName = $state['orig_name'] ?? 'Unknown.pdf';
|
$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');
|
Env::load(__DIR__ . '/../.env');
|
||||||
$geminiKey = Env::get('GEMINI_API_KEY') ?: getenv('GEMINI_API_KEY');
|
$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, 'uploading_to_ai', 20, 'جاري رفع الكتاب إلى محرك الذكاء الاصطناعي (Gemini Vision) لقراءته بالكامل...');
|
||||||
updateState($stateFile, 'analyzing', 50, 'يتم الآن تحليل المنهج عبر الذكاء الاصطناعي (Gemini)...');
|
|
||||||
|
|
||||||
$prompt = "أنت خبير مناهج تعليمية. قم بتحليل هذا النص المستخرج من كتاب دراسي بعنوان '{$origName}' واستخراج الفهرس والوحدات والدروس بدقة.
|
|
||||||
النص:
|
|
||||||
" . mb_substr($rawText, 0, 15000) . "
|
|
||||||
|
|
||||||
المطلوب إرجاع 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_name\": \"اسم الصف (مثال: الصف العاشر)\",
|
||||||
\"grade_key\": \"grade_X\",
|
\"grade_key\": \"grade_10\",
|
||||||
\"subject_name\": \"اسم المادة\",
|
\"subject_name\": \"اسم المادة (مثال: الرياضيات)\",
|
||||||
\"subject_key\": \"subject_key\",
|
\"subject_key\": \"math_10\",
|
||||||
\"semester_name\": \"الفصل الدراسي الأول\",
|
\"semester_name\": \"الفصل الدراسي\",
|
||||||
\"semester_key\": \"semester_1\",
|
\"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\": {
|
\"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 = [
|
||||||
$payload = [
|
'contents' => [
|
||||||
'contents' => [['parts' => [['text' => $prompt]]]],
|
[
|
||||||
'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.2]
|
'parts' => [
|
||||||
];
|
['fileData' => ['mimeType' => 'application/pdf', 'fileUri' => $fileUri]],
|
||||||
|
['text' => $prompt]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.1]
|
||||||
|
];
|
||||||
|
|
||||||
$ch = curl_init($url);
|
// Using gemini-1.5-pro since it is powerful for large PDFs and math formulas
|
||||||
curl_setopt_array($ch, [
|
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key=" . $geminiKey;
|
||||||
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);
|
|
||||||
|
|
||||||
if ($code === 200 && !empty($res)) {
|
$ch = curl_init($url);
|
||||||
$json = json_decode($res, true);
|
curl_setopt_array($ch, [
|
||||||
$text = $json['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
CURLOPT_POST => true,
|
||||||
$parsedStructure = json_decode($text, 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'])) {
|
if (empty($parsedStructure) || empty($parsedStructure['units'])) {
|
||||||
updateState($stateFile, 'analyzing', 60, 'يتم تحليل البنية باستخدام محرك التحليل الديناميكي العميق للنصوص...');
|
updateState($stateFile, 'error', 0, 'فشل الذكاء الاصطناعي في إرجاع بنية JSON صالحة.');
|
||||||
|
exit;
|
||||||
$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, 'generating', 80, 'جاري بناء هيكل المنهاج وتوزيع المصادر (أوراق عمل، امتحانات)...');
|
updateState($stateFile, 'generating', 80, 'الذكاء الاصطناعي انتهى من القراءة! جاري بناء الملفات وحفظها على السيرفر...');
|
||||||
|
|
||||||
|
// Save to disk
|
||||||
$updatedTree = CurriculumService::mergeExtractedCurriculum($parsedStructure);
|
$updatedTree = CurriculumService::mergeExtractedCurriculum($parsedStructure);
|
||||||
|
|
||||||
$firstLessonFile = '';
|
$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";
|
$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,
|
'active_file' => $firstLessonFile,
|
||||||
'tree' => $updatedTree,
|
'tree' => $updatedTree,
|
||||||
'extracted_data' => $parsedStructure
|
'extracted_data' => $parsedStructure
|
||||||
|
|||||||
Reference in New Issue
Block a user