status(400)->json([ 'status' => 'error', 'message' => 'يرجى اختيار ملف PDF صالح للمنهاج الوزاري.' ]); return; } $file = $_FILES['pdf_file']; $origName = $file['name']; $tmpPath = $file['tmp_name']; $taskId = uniqid('task_'); // Setup processing directory $processingDir = __DIR__ . '/../../storage/curriculum/processing'; if (!is_dir($processingDir)) { mkdir($processingDir, 0777, true); } $savedPdfPath = $processingDir . '/' . $taskId . '.pdf'; move_uploaded_file($tmpPath, $savedPdfPath); // Initialize Task State $taskState = [ 'task_id' => $taskId, 'status' => 'queued', 'progress' => 0, 'message' => 'تم استلام الملف وجاري تحويله للمعالجة المعمقة...', 'orig_name' => $origName ]; file_put_contents($processingDir . '/' . $taskId . '.json', json_encode($taskState, JSON_UNESCAPED_UNICODE)); // Prepare task paths $scriptPath = realpath(__DIR__ . '/../../scripts/curriculum_worker.php'); $logPath = $processingDir . '/' . $taskId . '.log'; // Send processing response immediately $response->json([ 'status' => 'processing', 'task_id' => $taskId, 'message' => 'تم استلام الكتاب، وجاري المعالجة المعمقة بواسطة الذكاء الاصطناعي. يرجى الانتظار...' ]); // Terminate request and flush buffers so the client receives the JSON immediately if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } else { // Fallback for non-FPM ob_end_flush(); flush(); } // Run worker completely in the background via CLI try { $cmd = "php " . escapeshellarg($scriptPath) . " " . escapeshellarg($taskId) . " > /dev/null 2>&1 &"; exec($cmd); // Log that we spawned it file_put_contents($logPath, "[System] Spawning background worker: $cmd\n", FILE_APPEND); } catch (\Exception $e) { $err = "[Exception] " . $e->getMessage(); file_put_contents($logPath, $err . "\n", FILE_APPEND); // Update state file to error $stateFile = $processingDir . '/' . $taskId . '.json'; if (file_exists($stateFile)) { $st = json_decode(file_get_contents($stateFile), true); $st['status'] = 'error'; $st['message'] = 'تعطل النظام أثناء التحليل (Crash). يرجى مراجعة سجل الأخطاء.'; file_put_contents($stateFile, json_encode($st, JSON_UNESCAPED_UNICODE)); } } // Ensure script stops here so it doesn't return anything else exit; } /** * Poll Upload Status */ public function getUploadStatus(Request $request, Response $response): void { $taskId = $request->getQueryParams()['task_id'] ?? ''; if (empty($taskId)) { $response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة مفقود']); return; } $processingDir = __DIR__ . '/../../storage/curriculum/processing'; $stateFile = $processingDir . '/' . $taskId . '.json'; if (!file_exists($stateFile)) { $response->status(404)->json(['status' => 'error', 'message' => 'المهمة غير موجودة']); return; } $state = json_decode(file_get_contents($stateFile), true); $response->json($state); } /** * Get Upload Log */ public function getUploadLog(Request $request, Response $response): void { $taskId = $request->getQueryParams()['task_id'] ?? ''; if (empty($taskId)) { $response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة مفقود']); return; } $processingDir = __DIR__ . '/../../storage/curriculum/processing'; $logFile = $processingDir . '/' . $taskId . '.log'; if (!file_exists($logFile)) { $response->status(404)->json(['status' => 'error', 'message' => 'سجل الأخطاء غير موجود']); return; } $logContent = file_get_contents($logFile); $response->json(['status' => 'success', 'log' => $logContent]); } /** * Get Complete Live Tree */ public function getTree(Request $request, Response $response): void { $response->json([ 'status' => 'success', 'data' => CurriculumService::getCurriculumTree() ]); } /** * Get Single Lesson Markdown Content */ public function getLessonContent(Request $request, Response $response): void { $file = $request->getQueryParams()['file'] ?? ''; if (empty($file)) { $response->status(400)->json(['status' => 'error', 'message' => 'مسار الملف مطلوب']); return; } $content = CurriculumService::getLessonMarkdown($file); $assets = CurriculumService::getLessonAiAssets($file); $serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/'); $response->json([ 'status' => 'success', 'file' => $file, 'server_full_path' => $serverFullPath, 'content' => $content, 'ai_assets' => $assets ]); } public function saveLessonContent(Request $request, Response $response): void { $body = $request->getBody(); $file = $body['file'] ?? ''; $content = $body['content'] ?? ''; $aiAssets = $body['ai_assets'] ?? null; if (empty($file) || empty($content)) { $response->status(400)->json(['status' => 'error', 'message' => 'بيانات الحفظ غير مكتملة']); return; } CurriculumService::saveLessonMarkdown($file, $content); if ($aiAssets !== null) { CurriculumService::saveLessonAiAssets($file, $aiAssets); } // Sync to MySQL Database Table `lessons` if connected try { $aiVideoUrl = $aiAssets['ai_video_url'] ?? null; $cheatSheet = $aiAssets['cheat_sheet'] ?? null; $socraticJson = isset($aiAssets['socratic_quiz']) ? json_encode($aiAssets['socratic_quiz'], JSON_UNESCAPED_UNICODE) : null; // Search lesson by matching filename or title $filename = basename($file, '.md'); \App\Core\Database::query( "UPDATE lessons SET markdown_content = ?, ai_video_url = COALESCE(?, ai_video_url), cheat_sheet_markdown = COALESCE(?, cheat_sheet_markdown), socratic_quiz_json = COALESCE(?, socratic_quiz_json) WHERE title LIKE ? OR markdown_content LIKE ?", [$content, $aiVideoUrl, $cheatSheet, $socraticJson, "%{$filename}%", "%{$file}%"] ); } catch (\Throwable $dbEx) { error_log("Curriculum DB save sync note: " . $dbEx->getMessage()); } $serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/'); $response->json([ 'status' => 'success', 'message' => 'تم حفظ واعتماد محتوى الدرس في المنهاج وقاعدة البيانات بنجاح!', 'server_full_path' => $serverFullPath ]); } public function generateAiAssets(Request $request, Response $response): void { $body = $request->getBody(); $file = $body['file'] ?? ''; if (empty($file)) { $response->status(400)->json(['status' => 'error', 'message' => 'مسار الملف مطلوب']); return; } $content = CurriculumService::getLessonMarkdown($file); if (empty($content)) { $response->status(400)->json(['status' => 'error', 'message' => 'محتوى الدرس فارغ']); return; } // Generate Assets require_once __DIR__ . '/../Services/AiLessonEnhancerService.php'; $assets = \App\Services\AiLessonEnhancerService::generateFromText('الدرس', $content); if ($assets) { CurriculumService::saveLessonAiAssets($file, $assets); $response->json(['status' => 'success', 'ai_assets' => $assets]); } else { $response->status(500)->json(['status' => 'error', 'message' => 'فشل توليد المخرجات الذكية']); } } /** * Direct Video Upload to Platform Storage with Semantic Lesson Binding */ public function uploadLessonVideo(Request $request, Response $response): void { $file = $_POST['file'] ?? ''; if (empty($file)) { $response->status(400)->json(['status' => 'error', 'message' => 'مسار الدرس مطلوب']); return; } if (empty($_FILES['video_file']) || $_FILES['video_file']['error'] !== UPLOAD_ERR_OK) { $response->status(400)->json(['status' => 'error', 'message' => 'يرجى اختيار ملف فيديو صالح (MP4, WebM, MOV)']); return; } try { // Resolve a valid FK-safe course_id for the master ministry curriculum. // IMPORTANT: course_id = 0 causes SQLSTATE[23000] FK violation because // the `lessons` table enforces courses(id) ON DELETE CASCADE. $courseId = CurriculumService::getOrCreateSystemCourse(); $title = basename($file, '.md'); // 1. Upload & Transcode to HLS & Push to R2 (handled by VideoService) $uploadResult = \App\Services\VideoService::handleDirectUpload($_FILES['video_file'], $courseId, $title); $videoUrl = $uploadResult['r2_url'] ?? $uploadResult['hls_url']; // 2. Insert into lessons table as AI-generated ministry version $lessonId = \App\Core\Database::insert( "INSERT INTO lessons (course_id, title, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status, ai_video_url) VALUES (?, ?, 1, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready', ?)", [ $courseId, $title, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl ] ); // 3. Trigger Autonomous Zero-Touch AI Analysis & Socratic Checkpoint Generation \App\Services\AiVideoAnalyzerService::processLessonAutonomously($lessonId); // 4. Update Lesson AI assets in manifest.json & filesystem $currentAssets = CurriculumService::getLessonAiAssets($file); $currentAssets['ai_video_url'] = $videoUrl; CurriculumService::saveLessonAiAssets($file, $currentAssets); // 5. Update any existing curriculum lesson rows with this video try { \App\Core\Database::query( "UPDATE lessons SET ai_video_url = ? WHERE title LIKE ? OR markdown_content LIKE ?", [$videoUrl, "%{$title}%", "%{$file}%"] ); } catch (\Throwable $dbEx) { error_log("Video DB sync note: " . $dbEx->getMessage()); } $response->status(201)->json([ 'status' => 'success', 'message' => 'تم رفع وتثبيت فيديو الشرح ومعالجته HLS وربطه بالذكاء الاصطناعي بنجاح!', 'video_url' => $videoUrl, 'course_id' => $courseId, 'lesson_id' => $lessonId, 'data' => $uploadResult ]); } catch (\Throwable $e) { $response->status(500)->json([ 'status' => 'error', 'message' => $e->getMessage() ]); } } }