Update Saqel Platform: 2026-09-02 08:12:57

This commit is contained in:
Hamza-Ayed
2026-09-02 08:12:57 +03:00
parent 36939a50c1
commit a4536395c5
5 changed files with 107 additions and 60 deletions
@@ -103,6 +103,11 @@ class CurriculumController
}
$processingDir = __DIR__ . '/../../storage/curriculum/processing';
$taskId = basename($taskId);
if (!preg_match('/^task_[A-Za-z0-9_.-]+$|^video_[A-Za-z0-9_.-]+$/', $taskId)) {
$response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة غير صالح']);
return;
}
$stateFile = $processingDir . '/' . $taskId . '.json';
if (!file_exists($stateFile)) {
@@ -261,65 +266,30 @@ class CurriculumController
}
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.
$manifestLesson = CurriculumService::findLessonByFile($file);
$title = $manifestLesson['title'] ?? basename($file, '.md');
$existingLesson = \App\Core\Database::selectOne(
"SELECT id, course_id FROM lessons WHERE curriculum_key = ? OR title = ? OR markdown_content LIKE ? LIMIT 1",
[$file, $title, '%' . $file . '%']
);
$courseId = $existingLesson
? (int)$existingLesson['course_id']
: CurriculumService::getOrCreateSystemCourse();
// 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. Bind the upload to the synchronized Manifest lesson when present.
if ($existingLesson) {
$lessonId = (int)$existingLesson['id'];
\App\Core\Database::query(
"UPDATE lessons SET curriculum_key = ?, storage_type = 'api_upload', video_uuid = ?, bunny_video_id = '', local_path = ?, hls_url = ?, thumbnail_url = ?, duration_seconds = ?, encoding_status = 'ready', ai_video_url = ? WHERE id = ?",
[$file, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl, $lessonId]
);
} else {
$lessonId = \App\Core\Database::insert(
"INSERT INTO lessons (course_id, title, curriculum_key, 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, $file, $uploadResult['video_uuid'], $uploadResult['local_path'], $uploadResult['hls_url'], $uploadResult['thumbnail_url'], $uploadResult['duration'] ?? 0, $videoUrl]
);
// Video transcoding and R2 upload can take longer than a web request.
// Persist the upload and hand the heavy work to a CLI worker.
$processingDir = __DIR__ . '/../../storage/curriculum/processing';
if (!is_dir($processingDir)) mkdir($processingDir, 0755, true);
$taskId = uniqid('video_', true);
$extension = strtolower(pathinfo($_FILES['video_file']['name'], PATHINFO_EXTENSION)) ?: 'mp4';
$savedVideoPath = $processingDir . '/' . $taskId . '.' . $extension;
if (!move_uploaded_file($_FILES['video_file']['tmp_name'], $savedVideoPath)) {
throw new \RuntimeException('تعذر حفظ ملف الفيديو المؤقت على السيرفر.');
}
file_put_contents($processingDir . '/' . $taskId . '.json', json_encode([
'task_id' => $taskId, 'status' => 'queued', 'progress' => 5,
'message' => 'تم استلام الفيديو، وسيبدأ التحويل والرفع السحابي الآن.',
'file' => $file, 'video_path' => $savedVideoPath,
'original_name' => $_FILES['video_file']['name']
], JSON_UNESCAPED_UNICODE));
// 3. Trigger Autonomous Zero-Touch AI Analysis & Socratic Checkpoint Generation
\App\Services\AiVideoAnalyzerService::processLessonAutonomously($lessonId);
$response->status(202)->json(['status' => 'processing', 'task_id' => $taskId, 'message' => 'تم استلام الفيديو وجاري تحويله ورفعه إلى Cloudflare R2...']);
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
elseif (ob_get_level()) { ob_end_flush(); flush(); }
$scriptPath = realpath(__DIR__ . '/../../scripts/video_upload_worker.php');
exec('php ' . escapeshellarg($scriptPath) . ' ' . escapeshellarg($taskId) . ' > /dev/null 2>&1 &');
return;
// 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',
+8 -1
View File
@@ -147,7 +147,14 @@ class VideoService
$targetFileName = $videoUuid . '.' . $ext;
$targetPath = $storageBase . '/' . $targetFileName;
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
$moved = is_uploaded_file($file['tmp_name'])
? move_uploaded_file($file['tmp_name'], $targetPath)
: @rename($file['tmp_name'], $targetPath);
if (!$moved && is_file($file['tmp_name'])) {
$moved = @copy($file['tmp_name'], $targetPath);
if ($moved) @unlink($file['tmp_name']);
}
if (!$moved) {
throw new \RuntimeException('فشل حفظ ملف الفيديو على السيرفر، يرجى فحص أذونات المجلد.');
}
+16 -3
View File
@@ -503,8 +503,15 @@ class CurriculumStudio
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok && data.status === 'success') {
const raw = await res.text();
let data;
try { data = JSON.parse(raw); }
catch (_) { throw new Error(`HTTP ${res.status}: ${raw.slice(0, 240) || 'استجابة غير مفهومة من السيرفر'}`); }
if (res.ok && data.status === 'processing') {
currentUploadTaskId = data.task_id;
statusSpan.textContent = data.message || '⏳ تم استلام الفيديو، جاري تحويله ورفعه إلى Cloudflare...';
pollTaskStatus(data.task_id, file.name);
} else if (res.ok && data.status === 'success') {
document.getElementById('ai_video_url_input').value = data.video_url;
const player = document.getElementById('lesson_video_player');
player.src = data.video_url;
@@ -518,7 +525,7 @@ class CurriculumStudio
} catch(err) {
console.error(err);
statusSpan.textContent = '⚠️ خطأ بالاتصال';
alert('حدث خطأ أثناء رفع الفيديو.');
alert('حدث خطأ أثناء رفع الفيديو: ' + (err.message || 'تعذر الاتصال بالخادم'));
}
}
@@ -721,6 +728,12 @@ class CurriculumStudio
pBar.style.width = '100%';
pPercent.textContent = '100%';
pText.textContent = `🎉 تم تفريغ واعتماد [${fileName}] كمنهاج حي بنجاح!`;
if (state.video_url) {
document.getElementById('ai_video_url_input').value = state.video_url;
const player = document.getElementById('lesson_video_player');
player.src = state.video_url;
player.style.display = 'block';
}
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
setTimeout(() => {