whereNotNull('path_video') ->get(); if ($users->isEmpty()) { Log::info("Tidak ada user dengan video. Skip sync."); Cache::put('face_training_status', ['phase' => 'idle'], 60); return; } $extractCount = 0; $skipCount = 0; $failCount = 0; $total = $users->count(); $current = 0; Cache::put('face_training_status', [ 'phase' => 'extracting', 'current' => 0, 'total' => $total, 'message' => "Mengecek frames 0/{$total} user...", ], 3600); foreach ($users as $data) { $userId = $data->id_user; $current++; $existingFrames = $data->jumlah_frame ?? 0; if ($existingFrames >= 50) { Log::info("User {$userId}: sudah punya {$existingFrames} frame. Skip extract."); $skipCount++; Cache::put('face_training_status', [ 'phase' => 'extracting', 'current' => $current, 'total' => $total, 'message' => "User {$current}/{$total}: skip (sudah ada {$existingFrames} frame)", ], 3600); continue; } $videoPath = Storage::disk('local')->path($data->path_video); if (!file_exists($videoPath)) { Log::warning("Video tidak ditemukan untuk user {$userId}: {$videoPath}"); $failCount++; continue; } try { Cache::put('face_training_status', [ 'phase' => 'extracting', 'current' => $current, 'total' => $total, 'message' => "Extract frames user {$current}/{$total}...", ], 3600); $response = Http::timeout(120) ->withHeaders([ 'X-API-Key' => config('services.flask.api_key'), ]) ->attach( 'video', fopen($videoPath, 'r'), 'enrollment.mp4' ) ->post(config('services.flask.url') . '/extract-frames', [ 'user_id' => (string) $userId, 'target_frames' => 200, ]); if (!$response->successful()) { throw new \Exception("Flask API error: HTTP {$response->status()} - " . $response->body()); } $output = $response->json(); if (!isset($output['status']) || $output['status'] !== 'success') { throw new \Exception("Extract error: " . ($output['message'] ?? 'Unknown')); } $jumlahFrame = $output['total_extracted'] ?? 0; DB::table('data_wajah')->where('id_user', $userId)->update([ 'jumlah_frame' => $jumlahFrame, ]); Log::info("Extract frames berhasil untuk user {$userId}. Frames: {$jumlahFrame}"); $extractCount++; } catch (\Exception $e) { Log::error("Extract GAGAL untuk user {$userId}: " . $e->getMessage()); $failCount++; } } Log::info("Sync selesai. Extracted: {$extractCount}, Skipped: {$skipCount}, Gagal: {$failCount}. Memulai retrain SVM..."); Cache::put('face_training_status', [ 'phase' => 'training', 'current' => $total, 'total' => $total, 'message' => "Sync selesai (Extract: {$extractCount}, Skip: {$skipCount}). Memulai training model SVM...", ], 3600); RetrainAllModels::dispatch(); app(\App\Services\NotifikasiService::class)->kirimKeRole( 'hrd', 'training', 'Sync & Retrain Selesai', "Sync frame selesai (Extract: {$extractCount}, Skip: {$skipCount}, Gagal: {$failCount}). Model SVM sedang di-training ulang.", [], false ); } }