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); } // Validasi: cek apakah itu daun padi (menggunakan metrik leafiness dari Python) $minLeafiness = 0.08; if (isset($result['leafiness']) && $result['leafiness'] < $minLeafiness) { return response()->json([ 'success' => false, 'message' => 'Bukan daun padi', ], 422); } // Tambahkan informasi detail tentang penyakit $diseaseInfo = $this->getDiseaseInfo($result['predicted_class']); // Skip database save for now - just return prediction result 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, '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); } // Validasi: cek apakah itu daun padi (menggunakan metrik leafiness dari Python) $minLeafiness = 0.08; if (isset($result['leafiness']) && $result['leafiness'] < $minLeafiness) { // Hapus file yang sudah disimpan jika terdeteksi bukan daun padi Storage::disk('public')->delete($storagePath); return response()->json([ 'success' => false, 'message' => 'Bukan daun padi', ], 422); } \Log::info('DEBUG: Raw prediction result - classifyAndSave', [ 'predicted_class' => $result['predicted_class'] ?? 'MISSING', 'confidence' => $result['confidence'] ?? 'MISSING', 'all_predictions' => $result['all_predictions'] ?? 'MISSING', 'full_result' => json_encode($result) ]); $diseaseInfo = $this->getDiseaseInfo($result['predicted_class']); // Extract location components $locationAddress = $request->input('location_address'); $locationLat = $request->input('location_lat'); $locationLng = $request->input('location_lng'); $kabupaten = $request->input('kabupaten'); $kecamatan = $request->input('kecamatan'); $kelurahan = $request->input('kelurahan'); // Fallback parsing if address is provided but components are missing if ($locationAddress && (!$kabupaten || !$kecamatan || !$kelurahan)) { $parsedLoc = $this->extractLocationComponents($locationAddress, $locationLat, $locationLng); $kabupaten = $kabupaten ?: $parsedLoc['kabupaten']; $kecamatan = $kecamatan ?: $parsedLoc['kecamatan']; $kelurahan = $kelurahan ?: $parsedLoc['kelurahan']; } // Simpan ke database Model Classification (untuk riwayat di mobile) $classification = Classification::create([ 'user_id' => $request->input('user_id'), 'image_path' => $storagePath, 'filename' => $file->getClientOriginalName(), 'predicted_class' => $result['predicted_class'], 'confidence' => (float) $result['confidence'], 'all_predictions' => $result['all_predictions'], 'disease_name' => $diseaseInfo['name'] ?? null, 'severity' => $diseaseInfo['severity'] ?? null, 'notes' => $request->input('notes'), 'location_address' => $locationAddress, 'location_lat' => $locationLat, 'location_lng' => $locationLng, 'kabupaten' => $kabupaten, 'kecamatan' => $kecamatan, 'kelurahan' => $kelurahan, ]); // Simpan ke database Model ClassificationHistory (untuk riwayat di web admin) if ($request->filled('user_id')) { ClassificationHistory::create([ 'user_id' => $request->input('user_id'), 'jenis_penyakit' => $diseaseInfo['name'] ?? $result['predicted_class'], 'location_address' => $locationAddress, 'location_lat' => $locationLat, 'location_lng' => $locationLng, 'kabupaten' => $kabupaten, 'kecamatan' => $kecamatan, 'kelurahan' => $kelurahan, ]); } return response()->json([ 'success' => true, 'message' => 'Gambar berhasil diklasifikasi dan disimpan', 'data' => [ 'id' => $classification->id, '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, 'location_address' => $classification->location_address, 'location_lat' => $classification->location_lat, 'location_lng' => $classification->location_lng, 'kabupaten' => $classification->kabupaten, 'kecamatan' => $classification->kecamatan, 'kelurahan' => $classification->kelurahan, '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' ], 'Healthy' => [ 'name' => 'Daun Sehat', 'description' => 'Daun padi dalam kondisi sehat tanpa penyakit', 'symptoms' => [ 'Tidak ada bercak atau lesi pada daun', 'Warna daun hijau cerah dan seragam', 'Tekstur daun normal tanpa perubahan' ], 'treatment' => [ 'Lanjutkan pemeliharaan rutin', 'Pantau kesehatan tanaman secara berkala', 'Terapkan praktik budidaya yang baik', 'Pertahankan kondisi lingkungan optimal' ], 'severity' => 'Tidak Ada' ], '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' ]; } }