Update Saqel Platform: 2026-09-08 23:44:33
This commit is contained in:
@@ -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'] ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user