320 lines
12 KiB
PHP
320 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Core\Request;
|
|
use App\Core\Response;
|
|
use App\Services\CurriculumService;
|
|
use App\Services\CurriculumExtractorService;
|
|
|
|
class CurriculumController
|
|
{
|
|
/**
|
|
* Upload and Queue Real Ministry PDF Document for Deep Extraction
|
|
*/
|
|
public function uploadPdf(Request $request, Response $response): void
|
|
{
|
|
if (empty($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
|
|
$response->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 {
|
|
$courseId = 0; // System course ID for curriculum AI videos
|
|
$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 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,
|
|
'data' => $uploadResult
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$response->status(500)->json([
|
|
'status' => 'error',
|
|
'message' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
}
|