Files
saqel/backend/scripts/curriculum_worker.php
T

243 lines
10 KiB
PHP

<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
set_error_handler(function($errno, $errstr, $errfile, $errline) {
echo "[PHP ERROR] $errstr in $errfile on line $errline
";
});
set_exception_handler(function($e) {
echo "[PHP EXCEPTION] " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine() . "
";
});
require_once __DIR__ . '/../vendor/autoload.php';
use App\Services\CurriculumService;
use App\Core\Env;
$taskId = $argv[1] ?? '';
if (empty($taskId)) {
echo "Task ID required\n"; return;
}
$processingDir = __DIR__ . '/../storage/curriculum/processing';
$stateFile = $processingDir . '/' . $taskId . '.json';
$pdfFile = $processingDir . '/' . $taskId . '.pdf';
if (!file_exists($stateFile) || !file_exists($pdfFile)) {
echo "Files missing\n"; return;
}
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));
// Log to file too
$logFile = str_replace('.json', '.log', $stateFile);
$timestamp = date('Y-m-d H:i:s');
file_put_contents($logFile, "[$timestamp] [$status] [$progress%] $message\n", FILE_APPEND);
}
$state = json_decode(file_get_contents($stateFile), true);
$origName = $state['orig_name'] ?? 'Unknown.pdf';
Env::load(__DIR__ . '/../.env');
$rawKeys = Env::get('GEMINI_API_KEY') ?: getenv('GEMINI_API_KEY');
$apiKeys = array_values(array_filter(array_map('trim', explode(',', $rawKeys))));
if (empty($apiKeys)) {
updateState($stateFile, 'error', 0, '⚠️ لم يتم العثور على مفتاح الذكاء الاصطناعي (GEMINI_API_KEY) في ملف .env.');
return;
}
// Round Robin Logic
$rrFile = sys_get_temp_dir() . '/saqel_gemini_rr.txt';
$currentIndex = file_exists($rrFile) ? (int)file_get_contents($rrFile) : 0;
$geminiKey = $apiKeys[$currentIndex % count($apiKeys)];
file_put_contents($rrFile, ($currentIndex + 1) % count($apiKeys));
// Add info about which key is used (masked for security)
$maskedKey = substr($geminiKey, 0, 6) . '...' . substr($geminiKey, -4);
updateState($stateFile, 'uploading_to_ai', 15, "تم تفعيل Round Robin. جاري استخدام المفتاح: {$maskedKey}");
if (empty($geminiKey)) {
updateState($stateFile, 'error', 0, '⚠️ لم يتم العثور على مفتاح الذكاء الاصطناعي (GEMINI_API_KEY) في ملف .env. النظام يحتاج إلى الـ AI لقراءة وتفريغ الكتب (بما فيها الكتب المصورة والرياضيات) بشكل حقيقي.');
return;
}
updateState($stateFile, 'uploading_to_ai', 20, 'جاري رفع الكتاب إلى محرك الذكاء الاصطناعي (Gemini Vision) لقراءته بالكامل...');
// 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);
return;
}
$uploadData = json_decode($uploadRes, true);
$fileUri = $uploadData['file']['uri'] ?? '';
$fileNameGemini = $uploadData['file']['name'] ?? '';
if (empty($fileUri)) {
updateState($stateFile, 'error', 0, 'لم يتم الحصول على URI من الذكاء الاصطناعي.');
return;
}
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.');
return;
}
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_10\",
\"subject_name\": \"اسم المادة (مثال: الرياضيات)\",
\"subject_key\": \"math_10\",
\"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[هنا النص الحقيقي والشرح والمعادلات كما ورد في الكتاب المرفوع]\"
}
]
}
],
\"resources\": {
\"worksheets\": { \"name\": \"أوراق عمل\", \"items\": [] },
\"exams\": { \"name\": \"اختبارات\", \"items\": [] }
}
}";
$payload = [
'contents' => [
[
'parts' => [
['fileData' => ['mimeType' => 'application/pdf', 'fileUri' => $fileUri]],
['text' => $prompt]
]
]
],
'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.1]
];
// Delay 4 seconds to respect free tier rate limits
updateState($stateFile, 'analyzing', 70, 'جاري تطبيقหน่วงزمني (4 ثوانٍ) لتفادي حدود الاستخدام (Rate Limit)...');
sleep(4);
// Allow custom model from .env (e.g. gemini-2.0-flash-lite-preview-02-05 or gemini-2.0-flash)
$geminiModel = Env::get('GEMINI_MODEL') ?: getenv('GEMINI_MODEL');
if (empty($geminiModel)) {
$geminiModel = "gemini-flash-lite-latest"; // fallback to latest flash lite
}
updateState($stateFile, 'analyzing', 75, "جاري إرسال البيانات للنموذج [{$geminiModel}]...");
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$geminiModel}:generateContent?key=" . $geminiKey;
$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 غير صالح.');
return;
}
$jsonResponse = json_decode($res, true);
$text = $jsonResponse['candidates'][0]['content']['parts'][0]['text'] ?? '';
// Clean up markdown fences if Gemini returned them
$text = preg_replace('/^```json\s*/i', '', $text);
$text = preg_replace('/```\s*$/i', '', $text);
$text = trim($text);
$parsedStructure = json_decode($text, true);
if (empty($parsedStructure) || empty($parsedStructure['units'])) {
// Log the actual text for debugging
file_put_contents($stateFile . '.debug.txt', $text);
updateState($stateFile, 'error', 0, 'فشل الذكاء الاصطناعي في إرجاع بنية JSON صالحة.');
return;
}
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, 'تم الاستخراج المعتمد على الـ AI بالكامل بنجاح!', [
'active_file' => $firstLessonFile,
'tree' => $updatedTree,
'extracted_data' => $parsedStructure
]);