Update Saqel Platform: 2026-08-28 16:06:52

This commit is contained in:
Hamza-Ayed
2026-08-28 16:06:53 +03:00
parent e6fd319ff4
commit 39c1e14232
8 changed files with 365 additions and 37 deletions
+222
View File
@@ -0,0 +1,222 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use App\Services\CurriculumService;
use App\Core\Env;
$taskId = $argv[1] ?? '';
if (empty($taskId)) {
die("Task ID required\n");
}
$processingDir = __DIR__ . '/../storage/curriculum/processing';
$stateFile = $processingDir . '/' . $taskId . '.json';
$pdfFile = $processingDir . '/' . $taskId . '.pdf';
if (!file_exists($stateFile) || !file_exists($pdfFile)) {
die("Files missing\n");
}
function updateState($stateFile, $status, $progress, $message, $extra = []) {
$state = json_decode(file_get_contents($stateFile), true);
$state['status'] = $status;
$state['progress'] = $progress;
$state['message'] = $message;
if (!empty($extra)) {
$state = array_merge($state, $extra);
}
file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE));
}
$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, 'error', 0, 'فشل استخراج النصوص من الـ PDF. قد يكون الملف عبارة عن صور فقط (Scanned).');
exit;
}
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, 'analyzing', 50, 'يتم الآن تحليل المنهج عبر الذكاء الاصطناعي (Gemini)...');
// We send chunks or a large chunk to Gemini
$prompt = "أنت خبير مناهج تعليمية. قم بتحليل هذا النص المستخرج من كتاب دراسي بعنوان '{$origName}' واستخراج الفهرس والوحدات والدروس بدقة.
النص:
" . mb_substr($rawText, 0, 15000) . "
المطلوب إرجاع JSON صالح فقط بالصيغة التالية (يجب أن يحتوي على النص الحقيقي للدرس وليس نصاً وهمياً):
{
\"grade_name\": \"اسم الصف\",
\"grade_key\": \"grade_X\",
\"subject_name\": \"اسم المادة\",
\"subject_key\": \"subject_key\",
\"semester_name\": \"الفصل الدراسي الأول\",
\"semester_key\": \"semester_1\",
\"units\": [
{
\"unit_key\": \"unit_1\",
\"unit_name\": \"اسم الوحدة الحقيقي\",
\"lessons\": [
{
\"lesson_id\": \"lesson_1\",
\"title\": \"اسم الدرس الحقيقي\",
\"outcomes\": [\"الهدف 1\"],
\"markdown_content\": \"# عنوان الدرس\\n\\nالنص الحقيقي الكامل المستخرج للدرس...\"
}
]
}
]
}";
$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]
];
$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);
if ($code === 200 && !empty($res)) {
$json = json_decode($res, true);
$text = $json['candidates'][0]['content']['parts'][0]['text'] ?? '';
$parsedStructure = json_decode($text, true);
}
}
// Fallback to Heuristic Regex Parser if Gemini failed or no key
if (empty($parsedStructure) || empty($parsedStructure['units'])) {
updateState($stateFile, 'analyzing', 60, 'يتم تحليل البنية باستخدام محرك التحليل الديناميكي العميق للنصوص...');
// Build an authentic structure based on ACTUAL text chunks
$cleanTitle = trim(preg_replace('/\.[^.]+$/u', '', $origName));
// Simple heuristic: split text by "Unit" or "الوحدة" or "Module"
$chunks = preg_split('/(Unit\s+\d+|Module\s+\d+|الوحدة\s+(?:الأولى|الثانية|الثالثة|الرابعة|الخامسة|\d+))/iu', $rawText, -1, PREG_SPLIT_DELIM_CAPTURE);
$units = [];
$unitCounter = 1;
// First chunk is preamble
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 {
// No explicit lessons found, make chunks of 1500 chars
$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 no units matched the regex at all, just split the whole book into 4 parts
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
];
}
$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
];
}
updateState($stateFile, 'generating', 80, 'جاري بناء هيكل المنهاج وملفات المارك داون على السيرفر...');
// Save to disk
$updatedTree = CurriculumService::mergeExtractedCurriculum($parsedStructure);
$firstLessonFile = '';
if (!empty($parsedStructure['units'][0]['lessons'][0])) {
$firstLes = $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, 'تم الاستخراج والفهرسة بنجاح!', [
'active_file' => $firstLessonFile,
'tree' => $updatedTree,
'extracted_data' => $parsedStructure
]);