whereNotNull('path_video') ->get(); if ($users->isEmpty()) { Log::info("Tidak ada user dengan video. Skip re-extract."); return; } $successCount = 0; $failCount = 0; foreach ($users as $data) { $userId = $data->id_user; $videoPath = Storage::disk('local')->path($data->path_video); if (!file_exists($videoPath)) { Log::warning("Video tidak ditemukan untuk user {$userId}: {$videoPath}"); $failCount++; continue; } try { $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-and-embed', [ '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; $embedding = $output['embedding'] ?? null; $updateData = [ 'jumlah_frame' => $jumlahFrame, ]; if ($embedding && is_array($embedding)) { $updateData['face_embeddings'] = json_encode($embedding); $updateData['jumlah_embedding'] = $jumlahFrame; $updateData['embedding_generated_at'] = now(); } DB::table('data_wajah')->where('id_user', $userId)->update($updateData); Log::info("Re-extract & embed berhasil untuk user {$userId}. Frames: {$jumlahFrame}"); $successCount++; } catch (\Exception $e) { Log::error("Re-extract GAGAL untuk user {$userId}: " . $e->getMessage()); $failCount++; } } Log::info("Re-extract selesai. Berhasil: {$successCount}, Gagal: {$failCount}"); app(\App\Services\NotifikasiService::class)->kirimKeRole( 'hrd', 'pengumuman', 'Ekstraksi & Embedding Wajah Selesai', "Proses re-extract video wajah selesai. Berhasil: {$successCount}, Gagal: {$failCount}." ); } }