Update Saqel Platform: 2026-09-08 23:44:33

This commit is contained in:
Hamza-Ayed
2026-09-08 23:44:33 +03:00
parent 41493f5dd6
commit be60f34148
3 changed files with 42 additions and 4 deletions
@@ -425,8 +425,9 @@ class TeacherRepository {
); );
final decoded = json.decode(response.body) as Map<String, dynamic>; final decoded = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode == 200 || response.statusCode == 201) if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 422) {
return decoded; return decoded;
}
throw StateError( throw StateError(
decoded['message']?.toString() ?? 'رفض الخادم رفع الحصة.'); decoded['message']?.toString() ?? 'رفض الخادم رفع الحصة.');
} on StateError { } on StateError {
@@ -314,6 +314,7 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
fileBytes: state.selectedFileBytes, fileBytes: state.selectedFileBytes,
); );
final status = res['status']?.toString();
final data = res['data'] is Map final data = res['data'] is Map
? Map<String, dynamic>.from(res['data'] as Map) ? Map<String, dynamic>.from(res['data'] as Map)
: <String, dynamic>{}; : <String, dynamic>{};
@@ -325,6 +326,19 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
'lesson_title': state.lessonTitle, 'lesson_title': state.lessonTitle,
'subject': selectedSubjectName '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() ?? 'تم اعتماد الحصة ورفعها بنجاح.'; final msg = res['message']?.toString() ?? 'تم اعتماد الحصة ورفعها بنجاح.';
emit(state.copyWith( emit(state.copyWith(
isAuditing: false, isAuditing: false,
@@ -82,9 +82,31 @@ class AiVideoAnalyzerService
// Extract a representative frame; Gemini receives real media evidence, // Extract a representative frame; Gemini receives real media evidence,
// not only the title or metadata supplied by the teacher. // 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')); $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]; return ['decision' => 'needs_manual_review', 'reason' => 'تعذر استخراج لقطة للتقييم؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration];
} }
try { try {
@@ -98,7 +120,8 @@ class AiVideoAnalyzerService
]], ]],
'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.1], '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]); 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); $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'] ?? ''; $text = json_decode((string)$body, true)['candidates'][0]['content']['parts'][0]['text'] ?? '';