diff --git a/apps/teacher_app/lib/data/repositories/teacher_repository.dart b/apps/teacher_app/lib/data/repositories/teacher_repository.dart index 0d9f536..14c97ff 100644 --- a/apps/teacher_app/lib/data/repositories/teacher_repository.dart +++ b/apps/teacher_app/lib/data/repositories/teacher_repository.dart @@ -425,8 +425,9 @@ class TeacherRepository { ); final decoded = json.decode(response.body) as Map; - if (response.statusCode == 200 || response.statusCode == 201) + if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 422) { return decoded; + } throw StateError( decoded['message']?.toString() ?? 'رفض الخادم رفع الحصة.'); } on StateError { diff --git a/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart b/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart index 1c1aba6..9261c9e 100644 --- a/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart +++ b/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart @@ -314,6 +314,7 @@ class TeacherStudioCubit extends Cubit { fileBytes: state.selectedFileBytes, ); + final status = res['status']?.toString(); final data = res['data'] is Map ? Map.from(res['data'] as Map) : {}; @@ -325,6 +326,19 @@ class TeacherStudioCubit extends Cubit { 'lesson_title': state.lessonTitle, 'subject': selectedSubjectName }); + + if (status == 'needs_review' || report['decision'] == 'needs_manual_review' || report['decision'] == 'rejected') { + final reason = report['reason']?.toString() ?? res['message']?.toString() ?? 'لم يجتز الفيديو تدقيق الجودة.'; + emit(state.copyWith( + isAuditing: false, + isUploading: false, + isUploaded: false, + auditResult: result, + errorMessage: reason, + )); + return false; + } + final msg = res['message']?.toString() ?? 'تم اعتماد الحصة ورفعها بنجاح.'; emit(state.copyWith( isAuditing: false, diff --git a/backend/app/Services/AiVideoAnalyzerService.php b/backend/app/Services/AiVideoAnalyzerService.php index 4e422d4..35ea195 100644 --- a/backend/app/Services/AiVideoAnalyzerService.php +++ b/backend/app/Services/AiVideoAnalyzerService.php @@ -82,9 +82,31 @@ class AiVideoAnalyzerService // 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'; + $tempBase = tempnam(sys_get_temp_dir(), 'saqel_audit_'); + @unlink($tempBase); + $frame = $tempBase . '.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)) { + if ($ffmpeg === '') { + foreach (['/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/bin/ffmpeg'] as $cand) { + if (@is_executable($cand)) { + $ffmpeg = $cand; + break; + } + } + } + if ($ffmpeg === '') { + return ['decision' => 'needs_manual_review', 'reason' => 'خدمة فحص الوسائط (ffmpeg) غير متوفرة على الخادم', 'duration_seconds' => $duration]; + } + + $ssTime = max(1, (int)($duration / 3)); + $cmd = escapeshellarg($ffmpeg) . ' -y -ss ' . escapeshellarg((string)$ssTime) . ' -i ' . escapeshellarg($path) . ' -frames:v 1 -q:v 3 ' . escapeshellarg($frame) . ' 2>&1'; + $output = []; + $returnCode = 1; + @exec($cmd, $output, $returnCode); + + if ($returnCode !== 0 || !is_file($frame) || filesize($frame) === 0) { + error_log("AiVideoAnalyzerService ffmpeg extraction failed (code {$returnCode}): " . implode("\n", array_slice($output, -5))); return ['decision' => 'needs_manual_review', 'reason' => 'تعذر استخراج لقطة للتقييم؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration]; } try { @@ -98,7 +120,8 @@ class AiVideoAnalyzerService ]], 'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.1], ]; - $ch = curl_init('https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=' . rawurlencode($key)); + $geminiModel = getenv('GEMINI_MODEL') ?: 'gemini-1.5-flash'; + $ch = curl_init('https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode($geminiModel) . ':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'] ?? '';