Update Saqel Platform: 2026-09-02 00:55:56

This commit is contained in:
Hamza-Ayed
2026-09-02 00:55:56 +03:00
parent 1c0cd88a98
commit f4ab43d49a
18 changed files with 409 additions and 214 deletions
+44 -92
View File
@@ -400,67 +400,9 @@ class VideoController
}
if (!$lesson) {
// Construct a complete, standalone live playback payload directly from the requested lesson
$cleanTitle = str_replace(['_', '-'], ' ', $rawId);
$response->json([
'status' => 'success',
'data' => [
'lesson' => [
'id' => 1,
'course_id' => 0,
'title' => !empty($rawId) ? $cleanTitle : 'الدرس التفاعلي المعتمد',
'duration_seconds' => 900,
'is_free_preview' => true,
'storage_type' => 'api_upload'
],
'playback' => [
'storage_type' => 'api_upload',
'stream_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
'hls_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
'video_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson',
'is_direct' => true
],
'available_versions' => [
[
'lesson_id' => 1,
'is_ai' => true,
'teacher_name' => 'الذكاء الاصطناعي الوزاري',
'school_name' => 'وزارة التربية والتعليم',
'label' => 'شرح الذكاء الاصطناعي المعتمد 🤖',
'is_recommended' => true,
'playback' => [
'video_url' => 'https://saqel.intaleqapp.com/api/videos/stream/demo-vector-lesson'
]
]
],
'chapters' => [],
'checkpoints' => [
[
'exam_id' => 1,
'timestamp_seconds' => 15,
'rewind_on_fail_seconds' => 30,
'question_text' => 'فحص الفهم السقراطي: ما هو المفهوم الأساسي المشروح في هذا الدرس؟',
'explanation' => 'التركيز على نتاجات التعلم يضمن استيعاب المتطلبات الوزارية بدقة.',
'options' => [
['id' => 1, 'text' => 'تطبيق القواعد والمفاهيم العلمية/الرياضية بدقة', 'is_correct' => true],
['id' => 2, 'text' => 'تجاهل الخطوات التحليلية', 'is_correct' => false],
['id' => 3, 'text' => 'الحفظ المجرد دون فهم سياقي', 'is_correct' => false],
]
],
[
'exam_id' => 2,
'timestamp_seconds' => 45,
'rewind_on_fail_seconds' => 25,
'question_text' => 'فحص الفهم التعمقي: كيف يتم التحقق من صحة الحل وتجنب الأخطاء الشائعة؟',
'explanation' => 'التعويض المباشر والتحليل المنطقي يثبتان صحة النتائج.',
'options' => [
['id' => 4, 'text' => 'بالتعويض المباشر في المسألة الأصلية والتحقق من التكافؤ', 'is_correct' => true],
['id' => 5, 'text' => 'بالاعتماد على التخمين', 'is_correct' => false],
]
]
]
]
$response->status(404)->json([
'status' => 'error',
'message' => 'الدرس غير موجود أو لم يتم نشر الفيديو الخاص به بعد'
]);
return;
}
@@ -497,7 +439,8 @@ class VideoController
}
$checkpoints[] = [
'exam_id' => (int)$ex['exam_id'],
'exam_id' => (int)$ex['exam_id'],
'question_id' => $q ? (int)$q['id'] : 0,
'timestamp_seconds' => (int)$ex['timestamp_seconds'],
'rewind_on_fail_seconds' => (int)$ex['rewind_on_fail_seconds'],
'question_text' => $q['question_text'] ?? 'سؤال فحص فهم الفكرة:',
@@ -512,34 +455,6 @@ class VideoController
];
}
if (empty($checkpoints)) {
$checkpoints = [
[
'exam_id' => 1,
'timestamp_seconds' => 15,
'rewind_on_fail_seconds' => 30,
'question_text' => 'فحص الفهم الأولي: ما هو المفهوم الأساسي المشروح في هذه الفقرة من الدرس؟',
'explanation' => 'التركيز على نتاجات التعلم يضمن استيعاب المتطلبات الوزارية بدقة.',
'options' => [
['id' => 1, 'text' => 'تطبيق القواعد والمفاهيم العلمية/الرياضية بدقة', 'is_correct' => true],
['id' => 2, 'text' => 'تجاهل الخطوات التحليلية', 'is_correct' => false],
['id' => 3, 'text' => 'الحفظ المجرد دون فهم سياقي', 'is_correct' => false],
]
],
[
'exam_id' => 2,
'timestamp_seconds' => 45,
'rewind_on_fail_seconds' => 25,
'question_text' => 'فحص الفهم التعمقي: كيف يتم التحقق من صحة الحل وتجنب الأخطاء الشائعة؟',
'explanation' => 'التعويض المباشر والتحليل المنطقي يثبتان صحة النتائج.',
'options' => [
['id' => 4, 'text' => 'بالتعويض المباشر في المسألة الأصلية والتحقق من التكافؤ', 'is_correct' => true],
['id' => 5, 'text' => 'بالاعتماد على التخمين', 'is_correct' => false],
]
]
];
}
$storageType = $lesson['storage_type'] ?? 'bunny_stream';
$playbackInfo = [];
@@ -553,7 +468,11 @@ class VideoController
];
} else {
// Bunny Stream Signed Playback
$bunnyId = $lesson['bunny_video_id'] ?: 'mock-bunny-guid-2026';
$bunnyId = $lesson['bunny_video_id'] ?: '';
if ($bunnyId === '') {
$response->status(409)->json(['status' => 'error', 'message' => 'الفيديو لم يجهز للبث بعد']);
return;
}
$signedData = VideoService::generateBunnySignedPlayback($bunnyId, 10800); // 3-hour token
$playbackInfo = array_merge(['storage_type' => 'bunny_stream'], $signedData);
}
@@ -587,7 +506,8 @@ class VideoController
'video_url' => $ver['ai_video_url'] ?: ($ver['hls_url'] ?: ('/api/videos/stream/' . $ver['video_uuid']))
];
} else {
$bId = $ver['bunny_video_id'] ?: 'mock-bunny-guid-2026';
$bId = $ver['bunny_video_id'] ?: '';
if ($bId === '') continue;
$signed = VideoService::generateBunnySignedPlayback($bId, 10800);
$vPlayback = array_merge(['storage_type' => 'bunny_stream', 'video_url' => $signed['hls_url']], $signed);
}
@@ -637,6 +557,38 @@ class VideoController
]);
}
/** Save real playback progress; the client never owns the readiness score. */
public function saveProgress(Request $request, Response $response): void
{
VideoService::ensureSchema();
$lessonId = (int)$request->getParam('id');
$body = $request->getBody();
$position = max(0, (int)($body['position_seconds'] ?? 0));
$watched = max(0, (int)($body['watched_seconds'] ?? $position));
$lesson = Database::selectOne('SELECT id, duration_seconds FROM lessons WHERE id = ? LIMIT 1', [$lessonId]);
if (!$lesson) {
$response->status(404)->json(['status' => 'error', 'message' => 'الدرس غير موجود']);
return;
}
$duration = max(1, (int)$lesson['duration_seconds']);
$percentage = min(100, round(($position / $duration) * 100, 2));
$completed = $percentage >= 90 ? 1 : 0;
Database::query(
"INSERT INTO lesson_progress (student_id, lesson_id, position_seconds, watched_seconds, completion_percentage, is_completed, last_seen_at, completed_at)
VALUES (?, ?, ?, ?, ?, ?, NOW(), CASE WHEN ? = 1 THEN NOW() ELSE NULL END)
ON DUPLICATE KEY UPDATE
position_seconds = VALUES(position_seconds),
watched_seconds = GREATEST(watched_seconds, VALUES(watched_seconds)),
completion_percentage = VALUES(completion_percentage),
is_completed = GREATEST(is_completed, VALUES(is_completed)),
last_seen_at = NOW(),
completed_at = CASE WHEN is_completed = 1 OR VALUES(is_completed) = 1 THEN COALESCE(completed_at, NOW()) ELSE completed_at END",
[$request->user_id, $lessonId, $position, $watched, $percentage, $completed, $completed]
);
$response->json(['status' => 'success', 'data' => ['completion_percentage' => $percentage, 'is_completed' => (bool)$completed]]);
}
/**
* Webhook listener for Bunny Stream encoding notifications
* POST /api/webhooks/bunny