Update Saqel Platform: 2026-09-08 17:30:46
This commit is contained in:
@@ -21,6 +21,69 @@ use App\Core\Security;
|
||||
|
||||
class AiVideoAnalyzerService
|
||||
{
|
||||
/**
|
||||
* Fail-closed admission gate for teacher uploads. The media stays in PHP's
|
||||
* temporary upload area until Gemini and technical checks approve it.
|
||||
*/
|
||||
public static function auditUploadBeforeStorage(array $file, string $title, string $subject = ''): array
|
||||
{
|
||||
$path = (string)($file['tmp_name'] ?? '');
|
||||
if ($path === '' || !is_file($path)) {
|
||||
return ['decision' => 'rejected', 'reason' => 'ملف الفيديو غير متاح للتدقيق'];
|
||||
}
|
||||
$mime = (new \finfo(FILEINFO_MIME_TYPE))->file($path) ?: '';
|
||||
if (!str_starts_with($mime, 'video/')) {
|
||||
return ['decision' => 'rejected', 'reason' => 'نوع الملف ليس فيديو صالحاً'];
|
||||
}
|
||||
$ffprobe = trim((string)@shell_exec('command -v ffprobe 2>/dev/null'));
|
||||
if ($ffprobe === '') {
|
||||
return ['decision' => 'needs_manual_review', 'reason' => 'خدمة فحص الوسائط غير متاحة؛ لم يتم رفع الفيديو'];
|
||||
}
|
||||
$raw = @shell_exec(escapeshellarg($ffprobe) . ' -v error -show_entries format=duration -show_streams -of json ' . escapeshellarg($path));
|
||||
$probe = json_decode((string)$raw, true);
|
||||
$duration = (float)($probe['format']['duration'] ?? 0);
|
||||
$hasVideo = false; $hasAudio = false;
|
||||
foreach (($probe['streams'] ?? []) as $stream) {
|
||||
$hasVideo = $hasVideo || (($stream['codec_type'] ?? '') === 'video');
|
||||
$hasAudio = $hasAudio || (($stream['codec_type'] ?? '') === 'audio');
|
||||
}
|
||||
if (!$hasVideo || !$hasAudio || $duration <= 0 || $duration > 1500) {
|
||||
return ['decision' => 'rejected', 'reason' => 'الفيديو يجب أن يحتوي صورة وصوتاً وأن لا يتجاوز 25 دقيقة', 'duration_seconds' => $duration];
|
||||
}
|
||||
$key = trim((string)getenv('GEMINI_API_KEY'));
|
||||
if ($key === '') return ['decision' => 'needs_manual_review', 'reason' => 'Gemini غير مهيأ؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration];
|
||||
|
||||
// Extract a representative frame; Gemini receives real media evidence,
|
||||
// not only the title or metadata supplied by the teacher.
|
||||
$frame = tempnam(sys_get_temp_dir(), 'saqel_audit_') . '.jpg';
|
||||
$ffmpeg = trim((string)@shell_exec('command -v ffmpeg 2>/dev/null'));
|
||||
if ($ffmpeg === '' || @shell_exec(escapeshellarg($ffmpeg) . ' -y -ss ' . escapeshellarg((string)max(1, (int)($duration / 3))) . ' -i ' . escapeshellarg($path) . ' -frames:v 1 -q:v 3 ' . escapeshellarg($frame) . ' 2>/dev/null') === null || !is_file($frame)) {
|
||||
return ['decision' => 'needs_manual_review', 'reason' => 'تعذر استخراج لقطة للتقييم؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration];
|
||||
}
|
||||
try {
|
||||
$prompt = "أنت مدقق جودة تربوية لمنصة صقل. قيّم اللقطة الفعلية من فيديو تعليمي بعنوان: {$title}. المادة: {$subject}. مدة الفيديو: {$duration} ثانية. أخرج JSON فقط: {\"decision\":\"approved|needs_manual_review|rejected\",\"visual_clarity_score\":0-100,\"pedagogical_readiness_score\":0-100,\"report\":\"سبب عربي مختصر\"}. ارفض المحتوى غير التعليمي أو غير الواضح. لا تمنح الموافقة إن لم تظهر أدلة كافية.";
|
||||
$payload = [
|
||||
'contents' => [[
|
||||
'parts' => [
|
||||
['text' => $prompt],
|
||||
['inline_data' => ['mime_type' => 'image/jpeg', 'data' => base64_encode((string)file_get_contents($frame))],],
|
||||
],
|
||||
]],
|
||||
'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.1],
|
||||
];
|
||||
$ch = curl_init('https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=' . rawurlencode($key));
|
||||
curl_setopt_array($ch, [CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>json_encode($payload),CURLOPT_HTTPHEADER=>['Content-Type: application/json'],CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>30]);
|
||||
$body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
|
||||
$text = json_decode((string)$body, true)['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
$report = json_decode($text, true);
|
||||
if ($code !== 200 || !is_array($report) || !in_array($report['decision'] ?? '', ['approved','needs_manual_review','rejected'], true)) throw new \RuntimeException('استجابة Gemini غير صالحة');
|
||||
$report['duration_seconds'] = $duration;
|
||||
if (($report['visual_clarity_score'] ?? 0) < 70 || ($report['pedagogical_readiness_score'] ?? 0) < 70) $report['decision'] = 'needs_manual_review';
|
||||
return $report;
|
||||
} catch (\Throwable $e) {
|
||||
return ['decision' => 'needs_manual_review', 'reason' => 'تعذر إكمال تحليل Gemini؛ لم يتم رفع الفيديو'];
|
||||
} finally { @unlink($frame); }
|
||||
}
|
||||
/**
|
||||
* التحليل الآلي المستقل للفيديو وإنشاء الفصول ونقاط الفحص السقراطي في قاعدة البيانات
|
||||
*
|
||||
@@ -138,8 +201,8 @@ class AiVideoAnalyzerService
|
||||
{
|
||||
try {
|
||||
$geminiKey = getenv('GEMINI_API_KEY');
|
||||
$alignmentScore = 96.50;
|
||||
$clarityScore = 94.00;
|
||||
$alignmentScore = 0.0;
|
||||
$clarityScore = 0.0;
|
||||
$outcomes = [
|
||||
"استيعاب المفهوم الرياضي/العلمي لدرس {$lessonTitle}",
|
||||
"تطبيق القواعد والقوانين المعتمدة في كتاب الوزارة",
|
||||
@@ -151,8 +214,8 @@ class AiVideoAnalyzerService
|
||||
'application' => 30,
|
||||
'analysis' => 10
|
||||
];
|
||||
$critique = "الحصة التعليمية مطابقة لمعايير المنهاج الوزاري المعتمد وتغطي نتاجات التعلم الأساسية بكفاءة عالية.";
|
||||
$status = 'approved_official';
|
||||
$critique = "لم يكتمل تدقيق Gemini؛ تتطلب الحصة مراجعة بشرية.";
|
||||
$status = 'needs_manual_review';
|
||||
|
||||
if (!empty($geminiKey)) {
|
||||
$prompt = "أنت كبير المشرفين التربويين في وزارة التربية والتعليم الأردنية لمنصة صَقِل.
|
||||
@@ -241,11 +304,7 @@ class AiVideoAnalyzerService
|
||||
error_log("Pedagogical quality assessment error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return [
|
||||
'alignment_score' => 96.0,
|
||||
'clarity_score' => 94.0,
|
||||
'status' => 'approved_official'
|
||||
];
|
||||
return ['alignment_score' => 0.0, 'clarity_score' => 0.0, 'status' => 'needs_manual_review'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user