validate([ 'image' => 'required|image|mimes:jpeg,png,jpg,gif|max:5120', // Max 5MB 'location_address' => 'nullable|string|max:255', 'location_lat' => 'nullable|numeric', 'location_lng' => 'nullable|numeric', 'kabupaten' => 'nullable|string|max:255', 'kecamatan' => 'nullable|string|max:255', 'kelurahan' => 'nullable|string|max:255', ]); // Ambil file gambar $file = $request->file('image'); // Baca content gambar sebagai base64 $imageContent = file_get_contents($file->getRealPath()); $base64Image = base64_encode($imageContent); // Jalankan klasifikasi melalui service lokal (tanpa Flask API) $result = $this->classificationService->classifyFromBase64([ 'image' => $base64Image, 'filename' => $file->getClientOriginalName(), ]); if (!($result['success'] ?? false)) { return response()->json([ 'success' => false, 'message' => 'Gagal menghubungi model classification', 'error' => $result['message'] ?? 'Unknown error', ], 500); } $minLeafiness = 0.12; if (isset($result['leafiness']) && $result['leafiness'] < $minLeafiness) { return response()->json([ 'success' => false, 'message' => 'Bukan daun padi - Foto yang Anda upload terdeteksi bukan daun padi. Silahkan upload foto daun padi yang sesuai.', 'is_not_rice_leaf' => true, ], 422); } // Tambahkan informasi detail tentang penyakit $diseaseInfo = $this->getDiseaseInfo($result['predicted_class']); // Extract location components from address if not provided $locationAddress = $request->input('location_address'); $locationLat = $request->input('location_lat'); $locationLng = $request->input('location_lng'); $locationComponents = $this->extractLocationComponents($locationAddress, $locationLat, $locationLng); $kabupaten = $request->input('kabupaten') ?? $locationComponents['kabupaten']; $kecamatan = $request->input('kecamatan') ?? $locationComponents['kecamatan']; $kelurahan = $request->input('kelurahan') ?? $locationComponents['kelurahan']; $savedToDatabase = true; $persistenceWarning = null; try { Classification::create([ 'user_id' => $request->input('user_id'), 'filename' => $file->getClientOriginalName(), 'predicted_class' => $result['predicted_class'], 'confidence' => $result['confidence'], 'all_predictions' => $result['all_predictions'], 'disease_name' => $diseaseInfo['name'], 'severity' => $diseaseInfo['severity'], 'notes' => 'Classification without storage', 'location_address' => $locationAddress, 'location_lat' => $locationLat, 'location_lng' => $locationLng, 'kabupaten' => $kabupaten, 'kecamatan' => $kecamatan, 'kelurahan' => $kelurahan, ]); } catch (\Throwable $dbException) { $savedToDatabase = false; $persistenceWarning = 'Klasifikasi berhasil, tetapi gagal simpan ke database.'; Log::warning('Classification result not persisted', [ 'filename' => $file->getClientOriginalName(), 'error' => $dbException->getMessage(), ]); } return response()->json([ 'success' => true, 'message' => 'Klasifikasi berhasil', 'data' => [ 'predicted_class' => $result['predicted_class'], 'confidence' => round($result['confidence'] * 100, 2) . '%', 'confidence_value' => $result['confidence'], 'all_predictions' => $result['all_predictions'], 'disease_info' => $diseaseInfo, 'saved_to_database' => $savedToDatabase, 'persistence_warning' => $persistenceWarning, 'timestamp' => now(), ] ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $e->errors() ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Terjadi kesalahan: ' . $e->getMessage(), ], 500); } } /** * Upload gambar dan simpan ke database, kemudian klasifikasi * POST /api/classify-and-save */ public function classifyAndSave(Request $request) { try { // Validasi input $request->validate([ 'image' => 'required|image|mimes:jpeg,png,jpg,gif|max:5120', 'notes' => 'nullable|string|max:500', 'user_id' => 'nullable|exists:users,id', 'user_name' => 'nullable|string|max:255', 'location_address' => 'nullable|string|max:255', 'location_lat' => 'nullable|numeric', 'location_lng' => 'nullable|numeric', 'kabupaten' => 'nullable|string|max:255', 'kecamatan' => 'nullable|string|max:255', 'kelurahan' => 'nullable|string|max:255', ]); // Simpan gambar $file = $request->file('image'); $storagePath = $file->store('classifications', 'public'); // Baca content gambar sebagai base64 $imageContent = file_get_contents($file->getRealPath()); $base64Image = base64_encode($imageContent); // Jalankan klasifikasi melalui service lokal (tanpa Flask API) $result = $this->classificationService->classifyFromBase64([ 'image' => $base64Image, 'filename' => $file->getClientOriginalName(), ]); if (!($result['success'] ?? false)) { // Hapus file yang sudah disimpan jika klasifikasi gagal Storage::disk('public')->delete($storagePath); return response()->json([ 'success' => false, 'message' => 'Gagal menghubungi model classification', 'error' => $result['message'] ?? 'Unknown error', ], 500); } $minLeafiness = 0.12; if (isset($result['leafiness']) && $result['leafiness'] < $minLeafiness) { Storage::disk('public')->delete($storagePath); return response()->json([ 'success' => false, 'message' => 'Bukan daun padi - Foto yang Anda upload terdeteksi bukan daun padi. Silahkan upload foto daun padi yang sesuai.', 'is_not_rice_leaf' => true, ], 422); } // Validasi confidence - jika rendah berarti bukan daun padi $minConfidence = 0.80; // 80% minimum confidence if ($result['confidence'] < $minConfidence) { // Hapus file yang sudah disimpan jika confidence terlalu rendah Storage::disk('public')->delete($storagePath); return response()->json([ 'success' => false, 'message' => 'Bukan daun padi - Foto yang Anda upload terdeteksi bukan daun padi. Silahkan upload foto daun padi yang jelas dan bagus.', 'is_not_rice_leaf' => true, ], 422); } $diseaseInfo = $this->getDiseaseInfo($result['predicted_class']); // Extract location components from address if not provided $locationAddress = $request->input('location_address'); $locationLat = $request->input('location_lat'); $locationLng = $request->input('location_lng'); $locationComponents = $this->extractLocationComponents($locationAddress, $locationLat, $locationLng); $kabupaten = $request->input('kabupaten') ?? $locationComponents['kabupaten']; $kecamatan = $request->input('kecamatan') ?? $locationComponents['kecamatan']; $kelurahan = $request->input('kelurahan') ?? $locationComponents['kelurahan']; $savedToDatabase = true; $persistenceWarning = null; try { Classification::create([ 'user_id' => $request->input('user_id'), 'image_path' => $storagePath, 'filename' => $file->getClientOriginalName(), 'predicted_class' => $result['predicted_class'], 'confidence' => $result['confidence'], 'all_predictions' => $result['all_predictions'], 'disease_name' => $diseaseInfo['name'], 'severity' => $diseaseInfo['severity'], 'notes' => $request->input('notes'), 'location_address' => $locationAddress, 'location_lat' => $locationLat, 'location_lng' => $locationLng, 'kabupaten' => $kabupaten, 'kecamatan' => $kecamatan, 'kelurahan' => $kelurahan, ]); } catch (\Throwable $dbException) { $savedToDatabase = false; $persistenceWarning = 'Gambar berhasil diklasifikasi dan disimpan file, tetapi gagal simpan riwayat ke database.'; Log::warning('Classification file stored but DB persist failed', [ 'filename' => $file->getClientOriginalName(), 'path' => $storagePath, 'error' => $dbException->getMessage(), ]); } $historyUserId = null; if ($request->filled('user_id')) { $historyUserId = (int) $request->input('user_id'); } elseif ($request->filled('user_name')) { $userName = trim((string) $request->input('user_name')); if ($userName !== '') { $baseSlug = Str::slug($userName, '.'); if ($baseSlug === '') { $baseSlug = 'user'; } $email = $baseSlug . '@agripadi.local'; $counter = 1; while (User::where('email', $email)->exists()) { $email = $baseSlug . $counter . '@agripadi.local'; $counter++; } $user = User::firstOrCreate( ['name' => $userName], [ 'email' => $email, 'password' => Hash::make(Str::random(12)), 'role' => 'user', ] ); $historyUserId = $user->id; } } if ($historyUserId) { try { ClassificationHistory::create([ 'user_id' => $historyUserId, 'jenis_penyakit' => $diseaseInfo['name'] ?? $result['predicted_class'], 'location_address' => $locationAddress, 'location_lat' => $request->input('location_lat'), 'location_lng' => $request->input('location_lng'), 'kabupaten' => $kabupaten, 'kecamatan' => $kecamatan, 'kelurahan' => $kelurahan, ]); } catch (\Throwable $historyException) { Log::warning('Classification history not persisted', [ 'user_id' => $historyUserId, 'error' => $historyException->getMessage(), ]); } } return response()->json([ 'success' => true, 'message' => 'Gambar berhasil diklasifikasi dan disimpan', 'data' => [ 'image_path' => Storage::url($storagePath), 'predicted_class' => $result['predicted_class'], 'confidence' => round($result['confidence'] * 100, 2) . '%', 'confidence_value' => $result['confidence'], 'all_predictions' => $result['all_predictions'], 'disease_info' => $diseaseInfo, 'saved_to_database' => $savedToDatabase, 'persistence_warning' => $persistenceWarning, 'notes' => $request->input('notes'), 'location_address' => $request->input('location_address'), 'location_lat' => $request->input('location_lat'), 'location_lng' => $request->input('location_lng'), 'timestamp' => now(), ] ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $e->errors() ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Terjadi kesalahan: ' . $e->getMessage(), ], 500); } } /** * Klasifikasi gambar dari URL * POST /api/classification/classify-from-url */ public function classifyFromUrl(Request $request) { try { $request->validate([ 'image_url' => 'required|url|max:2048', 'notes' => 'nullable|string|max:500', 'save' => 'nullable|boolean', ]); $imageUrl = $request->input('image_url'); $result = $this->classificationService->classifyFromUrl([ 'image_url' => $imageUrl, ]); if (!($result['success'] ?? false)) { return response()->json([ 'success' => false, 'message' => $result['message'] ?? 'Gagal memproses gambar dari URL', ], 400); } $diseaseInfo = $this->getDiseaseInfo($result['predicted_class']); if ($request->boolean('save')) { $urlPath = parse_url($imageUrl, PHP_URL_PATH); $filename = is_string($urlPath) && $urlPath !== '' ? basename($urlPath) : 'from_url_image'; Classification::create([ 'filename' => $filename, 'predicted_class' => $result['predicted_class'], 'confidence' => $result['confidence'], 'all_predictions' => $result['all_predictions'], 'disease_name' => $diseaseInfo['name'], 'severity' => $diseaseInfo['severity'], 'notes' => $request->input('notes', 'Classification from URL'), ]); } return response()->json([ 'success' => true, 'message' => 'Klasifikasi dari URL berhasil', 'data' => [ 'url' => $imageUrl, 'predicted_class' => $result['predicted_class'], 'confidence' => round($result['confidence'] * 100, 2) . '%', 'confidence_value' => $result['confidence'], 'all_predictions' => $result['all_predictions'], 'disease_info' => $diseaseInfo, 'timestamp' => now(), ] ], 200); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $e->errors() ], 422); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Terjadi kesalahan: ' . $e->getMessage(), ], 500); } } /** * Health check klasifikasi service * GET /api/classification/health */ public function health() { try { $result = $this->classificationService->health(); if ($result['status'] !== 'ok') { return response()->json([ 'success' => false, 'message' => $result['message'] ?? 'Model tidak siap', 'model_info' => $result, ], 503); } return response()->json([ 'success' => true, 'message' => 'Koneksi ke model classification berhasil', 'model_info' => $result, ], 200); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Tidak dapat melakukan health check model: ' . $e->getMessage(), ], 500); } } /** * Informasi model klasifikasi * GET /api/classification/info */ public function info() { try { $result = $this->classificationService->info(); if (!($result['model_loaded'] ?? false)) { return response()->json([ 'success' => false, 'message' => $result['message'] ?? 'Model tidak tersedia', 'data' => $result, ], 503); } return response()->json([ 'success' => true, 'message' => 'Informasi model berhasil diambil', 'data' => $result, ], 200); } catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Gagal mengambil informasi model: ' . $e->getMessage(), ], 500); } } /** * Backward-compatible endpoint test (alias untuk health) * GET /api/classification/test */ public function testConnection() { return $this->health(); } /** * Extract kabupaten, kecamatan, kelurahan dari location_address string * Format Indonesia: "Kelurahan, Kecamatan ..., Kabupaten ..., Provinsi, Negara" * Contoh: "Sumbersari, Kecamatan Sumbersari, Kabupaten Jember, Jawa Timur, Indonesia" */ private function extractLocationComponents($locationAddress, $lat = null, $lng = null) { $components = [ 'kabupaten' => null, 'kecamatan' => null, 'kelurahan' => null, ]; if (!$locationAddress) { return $components; } // Extract "Kabupaten [name]" atau "Kota [name]" if (preg_match('/(?:Kabupaten|Kota)\s+([^,]+)/', $locationAddress, $matches)) { $components['kabupaten'] = trim($matches[1]); } // Extract "Kecamatan [name]" if (preg_match('/Kecamatan\s+([^,]+)/', $locationAddress, $matches)) { $components['kecamatan'] = trim($matches[1]); } // Extract first part (before first comma) as kelurahan $parts = explode(',', $locationAddress); if (!empty($parts)) { $kelurahan = trim($parts[0]); if ($kelurahan && !preg_match('/^(Kecamatan|Kabupaten|Kota|Provinsi)/', $kelurahan)) { $components['kelurahan'] = $kelurahan; } } // Fallback: If kabupaten not found in address, use lat/lng to determine it // For Jember area (coordinates around -8.1656155, 113.7212891) if (!$components['kabupaten'] && $lat && $lng) { $lat = (float) $lat; $lng = (float) $lng; // Jember region boundaries (approximate) if ($lat >= -8.35 && $lat <= -7.85 && $lng >= 113.50 && $lng <= 114.00) { $components['kabupaten'] = 'Jember'; } } return $components; } /** * Mendapatkan informasi detail tentang penyakit */ private function getDiseaseInfo($className) { $diseaseDatabase = [ 'Bacterialblight' => [ 'name' => 'Bercak Bakteri (Bacterial Blight)', 'description' => 'Penyakit yang disebabkan oleh bakteri Xanthomonas oryzae pv. oryzae', 'symptoms' => [ 'Bercak kecil berwarna kuning dengan halo kuning di tepi', 'Bercak berkembang menjadi panjang dan menyempit', 'Daun berubah warna menjadi kuning dan akhirnya mati' ], 'treatment' => [ 'Gunakan varietas padi yang tahan', 'Praktikkan rotasi tanaman', 'Hindari irigasi berlebihan', 'Aplikasikan fungisida bakteri jika diperlukan' ], 'severity' => 'Sedang hingga Tinggi' ], 'Brownspot' => [ 'name' => 'Bercak Coklat (Brown Spot)', 'description' => 'Penyakit yang disebabkan oleh jamur Bipolaris oryzae', 'symptoms' => [ 'Bercak oval atau bundar berwarna coklat gelap', 'Bercak dikelilingi cincin kuning pucat', 'Bercak dapat berkabung di pusat dengan tepi merah ungu' ], 'treatment' => [ 'Gunakan benih yang sehat dan telah disertifikasi', 'Terapkan pemupukan seimbang', 'Gunakan varietas tahan', 'Aplikasikan fungisida: Mancozeb atau Carbendazim' ], 'severity' => 'Rendah hingga Sedang' ], 'Leafsmut' => [ 'name' => 'Jamur Daun (Leaf Smut)', 'description' => 'Penyakit yang disebabkan oleh jamur Tilletia barclayana (Sheath Smut)', 'symptoms' => [ 'Bercak coklat kecil pada daun', 'Dalam kondisi lanjut terlihat serbuk hitam di dalam bercak', 'Daun dapat mengkeriting dan mengering' ], 'treatment' => [ 'Gunakan benih yang sehat', 'Drainase yang baik', 'Hindari kepadatan tanaman yang berlebihan', 'Aplikasikan fungisida pada tahap awal' ], 'severity' => 'Rendah' ] ]; return $diseaseDatabase[$className] ?? [ 'name' => $className, 'description' => 'Informasi tidak tersedia', 'symptoms' => [], 'treatment' => [], 'severity' => 'Unknown' ]; } }