Update Saqel Platform: 2026-08-28 15:41:53

This commit is contained in:
Hamza-Ayed
2026-08-28 15:41:53 +03:00
parent 974de0a641
commit 86bdee399d
3 changed files with 350 additions and 195 deletions
+40 -172
View File
@@ -5,11 +5,12 @@ namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Services\CurriculumService;
use App\Services\CurriculumExtractorService;
class CurriculumController
{
/**
* Upload and Analyze Real Ministry PDF Document
* Upload and Deep-Extract Real Ministry PDF Document
*/
public function uploadPdf(Request $request, Response $response): void
{
@@ -25,42 +26,53 @@ class CurriculumController
$origName = $file['name'];
$tmpPath = $file['tmp_name'];
// Extract Text via pdftotext CLI if available
$extractedText = '';
if (file_exists($tmpPath)) {
$cmd = "pdftotext -layout " . escapeshellarg($tmpPath) . " - 2>/dev/null";
$output = @shell_exec($cmd);
if (!empty($output)) {
$extractedText = mb_substr($output, 0, 15000); // take first 15k chars for structure
}
// Save original uploaded PDF in storage/uploads/curriculum
$uploadsDir = __DIR__ . '/../../storage/uploads/curriculum';
if (!is_dir($uploadsDir)) {
mkdir($uploadsDir, 0777, true);
}
$savedPdfPath = $uploadsDir . '/' . time() . '_' . preg_replace('/[^A-Za-z0-9_\-\.]/u', '_', $origName);
move_uploaded_file($tmpPath, $savedPdfPath);
// Perform Intelligent Parsing with Gemini 2.0 Flash or Smart Parser
$parsedStructure = self::parseCurriculumText($origName, $extractedText);
// Perform Deep Multi-Engine Extraction & Structuring
$parsedStructure = CurriculumExtractorService::extractAndStructure($origName, $savedPdfPath);
// Merge into live tree and save to disk
$updatedTree = CurriculumService::mergeExtractedCurriculum($parsedStructure);
$firstLessonFile = '';
$firstLessonMd = '';
$firstLessonTitle = '';
$firstLessonOutcomes = [];
$breadcrumb = '';
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";
$firstLessonMd = CurriculumService::getLessonMarkdown($firstLessonFile);
$firstLessonTitle = $firstLes['title'];
$firstLessonOutcomes = $firstLes['outcomes'] ?? [];
$breadcrumb = "{$parsedStructure['grade_name']} ⟵ {$parsedStructure['subject_name']} ⟵ {$parsedStructure['units'][0]['unit_name']}";
}
$serverStorageDir = realpath(__DIR__ . '/../../storage/curriculum');
$response->json([
'status' => 'success',
'message' => "تم فك تشفير وفهرسة المنهاج [{$origName}] بنجاح!",
'extracted_data' => $parsedStructure,
'tree' => $updatedTree,
'active_file' => $firstLessonFile,
'active_md' => $firstLessonMd
'status' => 'success',
'message' => "تم تفريغ وفهرسة المنهاج [{$origName}] بنجاح في قاعدة المعرفة!",
'extracted_data' => $parsedStructure,
'tree' => $updatedTree,
'active_file' => $firstLessonFile,
'active_md' => $firstLessonMd,
'active_title' => $firstLessonTitle,
'active_outcomes' => $firstLessonOutcomes,
'active_breadcrumb' => $breadcrumb,
'server_storage_dir' => $serverStorageDir
]);
}
/**
* Get Tree
* Get Complete Live Tree
*/
public function getTree(Request $request, Response $response): void
{
@@ -81,10 +93,13 @@ class CurriculumController
return;
}
$content = CurriculumService::getLessonMarkdown($file);
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
$response->json([
'status' => 'success',
'file' => $file,
'content' => $content
'status' => 'success',
'file' => $file,
'server_full_path' => $serverFullPath,
'content' => $content
]);
}
@@ -103,159 +118,12 @@ class CurriculumController
}
CurriculumService::saveLessonMarkdown($file, $content);
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
$response->json([
'status' => 'success',
'message' => 'تم حفظ واعتماد محتوى الدرس في المنهاج بنجاح!'
'status' => 'success',
'message' => 'تم حفظ واعتماد محتوى الدرس في المنهاج بنجاح!',
'server_full_path' => $serverFullPath
]);
}
/**
* Parse Extracted Text via Gemini AI or Heuristic Rule Engine
*/
private static function parseCurriculumText(string $fileName, string $extractedText): array
{
$geminiKey = getenv('GEMINI_API_KEY');
// Detect Grade, Subject, Semester from Filename and Text
$fileNameLower = mb_strtolower($fileName);
$isEnglish = str_contains($fileNameLower, 'إنجليزي') || str_contains($fileNameLower, 'english') || str_contains($fileNameLower, 'action pack');
$isGrade10 = str_contains($fileNameLower, 'عاشر') || str_contains($fileNameLower, '10');
$isGrade7 = str_contains($fileNameLower, 'سابع') || str_contains($fileNameLower, '7');
$isTawjihi = str_contains($fileNameLower, 'توجيهي') || str_contains($fileNameLower, '2008') || str_contains($fileNameLower, 'ثانوية');
$isSem1 = str_contains($fileNameLower, 'أول') || str_contains($fileNameLower, 'اول') || str_contains($fileNameLower, '1');
if (!empty($geminiKey)) {
try {
$prompt = "أنت خبير مناهج وزارة التربية والتعليم الأردنية.
حلل الملف المرفق التالي بدقة 100%:
اسم الملف: '{$fileName}'
نص مستخرج من الملف:
" . mb_substr($extractedText, 0, 4000) . "
المطلوب إخراج JSON حقيقي 100% يطابق الملف تماماً:
{
\"grade_name\": \"الصف الدقيق المذكور في الملف\",
\"grade_key\": \"slug_grade\",
\"subject_name\": \"اسم المبحث الدقيق المذكور في الملف\",
\"subject_key\": \"slug_subject\",
\"semester_name\": \"الفصل الدراسي المذكور\",
\"semester_key\": \"semester_1\",
\"units\": [
{
\"unit_key\": \"unit_1\",
\"unit_name\": \"اسم الوحدة الأولى الحقيقي من الملف\",
\"lessons\": [
{
\"lesson_id\": \"les_1\",
\"title\": \"اسم الدرس الأول الحقيقي\",
\"outcomes\": [\"نتاج 1\", \"نتاج 2\"],
\"markdown_content\": \"# تفاصيل ومحتوى الدرس الحقيقي المعتمد بالمارك داون\"
}
]
}
]
}
ملاحظة صارمة: استخرج العناوين والوحدات الحقيقية فقط للمادة المرفوعة، ولا تخلط مع أي مادة أخرى.";
$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.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 => 25
]);
$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'] ?? '';
$parsed = json_decode($text, true);
if (!empty($parsed['units']) && !empty($parsed['subject_name'])) {
return $parsed;
}
}
} catch (\Throwable $e) {
error_log("Gemini PDF parsing error: " . $e->getMessage());
}
}
// Live Domain Parser based on actual filename context
if ($isEnglish && $isGrade10) {
return [
'grade_name' => 'الصف العاشر الأساسي',
'grade_key' => 'grade_10',
'subject_name' => 'اللغة الإنجليزية (Action Pack 10)',
'subject_key' => 'english_grade_10',
'semester_name' => $isSem1 ? 'الفصل الدراسي الأول' : 'الفصل الدراسي الثاني',
'semester_key' => $isSem1 ? 'semester_1' : 'semester_2',
'units' => [
[
'unit_key' => 'module_1_starting_out',
'unit_name' => 'Module 1: Starting out & Personality Traits',
'lessons' => [
[
'lesson_id' => 'eng10_m1_l1',
'title' => 'Lesson 1: Reading — Inspiring Personalities & Qualities',
'outcomes' => ['Describing personalities (reliable, ambitious, modest)', 'Reading for specific information'],
'markdown_content' => "# Action Pack 10 — Module 1: Starting Out\n## Lesson 1: Personal Qualities and Inspiring People\n\n### Vocabulary & Target Lexis:\n- **Ambitious:** Having a strong desire for success or achievement.\n- **Reliable:** Deserving trust; dependable.\n- **Modest:** Not boastful or arrogant about one's achievements.\n\n### Grammar Focus:\n- **Present Simple vs Present Continuous** for permanent states and temporary actions."
],
[
'lesson_id' => 'eng10_m1_l2',
'title' => 'Lesson 2: Grammar — Present Simple & Continuous Revision',
'outcomes' => ['Distinguish stative vs dynamic verbs', 'Formulate questions accurately'],
'markdown_content' => "# Module 1: Grammar Clinic\n## Present Tenses & Stative Verbs\n\n- Stative verbs (understand, believe, know, like) are rarely used in continuous forms.\n- *Example:* I understand the rule (Correct) / I am understanding (Incorrect)."
]
]
],
[
'unit_key' => 'module_2_careers',
'unit_name' => 'Module 2: Careers & Future Choices',
'lessons' => [
[
'lesson_id' => 'eng10_m2_l1',
'title' => 'Lesson 1: Future Careers & Job Skills',
'outcomes' => ['Discuss future job trends', 'Modals of possibility (might, may, could)'],
'markdown_content' => "# Module 2: Careers & Job Skills\n## Lesson 1: The Future of Work\n\n- Exploring technological and healthcare careers in Jordan.\n- Grammar: Predictions with *will* vs *going to*."
]
]
]
]
];
}
// Generic Clean Extraction from File Name
$cleanTitle = preg_replace('/\.pdf$/i', '', $origName);
return [
'grade_name' => $isGrade10 ? 'الصف العاشر' : ($isTawjihi ? 'الثانوية العامة (توجيهي 2008)' : 'المرحلة الدراسية'),
'grade_key' => $isGrade10 ? 'grade_10' : ($isTawjihi ? 'tawjihi_2008' : 'grade_general'),
'subject_name' => $cleanTitle,
'subject_key' => 'subject_' . mt_rand(100, 999),
'semester_name' => $isSem1 ? 'الفصل الدراسي الأول' : 'الفصل الدراسي الثاني',
'semester_key' => $isSem1 ? 'semester_1' : 'semester_2',
'units' => [
[
'unit_key' => 'unit_1',
'unit_name' => 'الوحدة الأولى: المحتوى المعتمد من الكتاب',
'lessons' => [
[
'lesson_id' => 'les_1',
'title' => 'الدرس 1: نتاجات التعلم والمفاهيم الأساسية',
'outcomes' => ['فهم المعطيات والمفاهيم الأساسية', 'حل التطبيقات المعيارية'],
'markdown_content' => "# {$cleanTitle}\n## الوحدة الأولى\n\nتم استخراج النص من ملف الوزارة المرفوع ({$origName})."
]
]
]
]
];
}
}