Update Saqel Platform: 2026-09-02 08:12:57
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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('فشل حفظ ملف الفيديو على السيرفر، يرجى فحص أذونات المجلد.');
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user