331 lines
15 KiB
Dart
331 lines
15 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/data/models/diagnosis_result_model.dart';
|
|
|
|
/// Supported Gemini models for multimodal (image+text) as per official docs
|
|
const List<String> supportedGeminiModels = [
|
|
'gemini-1.5-pro',
|
|
'gemini-2.0-pro',
|
|
'gemini-2.5-pro',
|
|
];
|
|
|
|
// Fungsi helper untuk isolate
|
|
DiagnosisResultModel _createDiagnosisModel(Map<String, dynamic> diagnosisData) {
|
|
return DiagnosisResultModel(
|
|
plantSpecies: diagnosisData['plant_species'] ?? 'Unknown Plant',
|
|
isHealthy: diagnosisData['is_healthy'] ?? true,
|
|
diseaseName: diagnosisData['disease_name'] ?? '',
|
|
scientificName: diagnosisData['scientific_name'] ?? '',
|
|
confidenceValue: (diagnosisData['confidence_value'] ?? 0.0).toDouble(),
|
|
symptoms: (diagnosisData['symptoms'] is List)
|
|
? (diagnosisData['symptoms'] as List).join(', ')
|
|
: (diagnosisData['symptoms'] ?? 'Tidak ada gejala terdeteksi'),
|
|
causes: (diagnosisData['causes'] is List)
|
|
? (diagnosisData['causes'] as List).join(', ')
|
|
: (diagnosisData['causes'] ?? 'Tidak ada penyebab teridentifikasi'),
|
|
preventionMeasures: (diagnosisData['prevention_measures'] is List)
|
|
? List<String>.from(diagnosisData['prevention_measures'])
|
|
: (diagnosisData['prevention_measures'] is String)
|
|
? [diagnosisData['prevention_measures']]
|
|
: [],
|
|
organicTreatment: (diagnosisData['organic_treatment'] is List)
|
|
? (diagnosisData['organic_treatment'] as List).join(', ')
|
|
: (diagnosisData['organic_treatment'] ?? 'Tidak ada pengobatan organik'),
|
|
chemicalTreatment: (diagnosisData['chemical_treatment'] is List)
|
|
? (diagnosisData['chemical_treatment'] as List).join(', ')
|
|
: (diagnosisData['chemical_treatment'] ?? 'Tidak ada pengobatan kimia'),
|
|
additionalInfo: AdditionalInfoModel(
|
|
severity: diagnosisData['additional_info']?['severity'] ?? 'Tidak diketahui',
|
|
spreadRate: diagnosisData['additional_info']?['spread_rate'] ?? 'Tidak diketahui',
|
|
affectedParts: (diagnosisData['additional_info']?['affected_parts'] is List)
|
|
? List<String>.from(diagnosisData['additional_info']?['affected_parts'])
|
|
: (diagnosisData['additional_info']?['affected_parts'] is String)
|
|
? [diagnosisData['additional_info']?['affected_parts']]
|
|
: [],
|
|
environmentalConditions: diagnosisData['additional_info']?['environmental_conditions'] ?? 'Tidak diketahui',
|
|
),
|
|
environmentalData: diagnosisData['environmental_data'] ?? {},
|
|
plantData: diagnosisData['plant_data'] ?? {},
|
|
treatmentSchedule: diagnosisData['treatment_schedule'] ?? {},
|
|
economicImpact: diagnosisData['economic_impact'] ?? {},
|
|
alternativeVarieties: (diagnosisData['alternative_varieties'] is List)
|
|
? List<Map<String, dynamic>>.from(diagnosisData['alternative_varieties'])
|
|
: [],
|
|
);
|
|
}
|
|
|
|
class GeminiDiseaseDiagnosisService {
|
|
final String apiKey;
|
|
final SupabaseClient supabaseClient;
|
|
final String baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
final String model = 'gemini-1.5-flash'; // Reverting to gemini-1.5-flash
|
|
|
|
GeminiDiseaseDiagnosisService({
|
|
required this.apiKey,
|
|
required this.supabaseClient,
|
|
});
|
|
|
|
/// (Optional) Fetch available models for this API key
|
|
Future<List<String>> fetchAvailableModels() async {
|
|
final response = await http.get(
|
|
Uri.parse('$baseUrl?key=$apiKey'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
);
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
final models = (data['models'] as List?)?.map((m) => m['name'] as String).toList() ?? [];
|
|
return models;
|
|
} else {
|
|
throw Exception('Failed to fetch models: ${response.statusCode}');
|
|
}
|
|
}
|
|
|
|
/// Main diagnosis function using Gemini API (multimodal: image + text)
|
|
Future<DiagnosisResultModel> diagnosePlant(String imagePath, {String? cropName}) async {
|
|
try {
|
|
// Read and encode the image
|
|
String base64Image;
|
|
|
|
if (kIsWeb) {
|
|
// For web platform, imagePath might be a data URL
|
|
if (imagePath.startsWith('data:image')) {
|
|
// Extract base64 data from data URL
|
|
base64Image = imagePath.split(',')[1];
|
|
} else {
|
|
// For web, we need to handle this differently
|
|
throw Exception('Web platform requires a data URL for images');
|
|
}
|
|
} else {
|
|
// For mobile platforms, read from file
|
|
final imageBytes = await File(imagePath).readAsBytes();
|
|
base64Image = base64Encode(imageBytes);
|
|
}
|
|
|
|
// Prepare the prompt for Gemini (instruct to answer in Bahasa Indonesia)
|
|
final prompt = '''
|
|
Analisis gambar tanaman ini dan berikan diagnosis penyakit secara detail dalam format JSON berikut (jawab seluruhnya dalam Bahasa Indonesia!):
|
|
{
|
|
"plant_species": "Nama tanaman",
|
|
"is_healthy": true/false,
|
|
"disease_name": "Nama penyakit",
|
|
"scientific_name": "Nama ilmiah",
|
|
"confidence_value": 0.0-1.0,
|
|
"symptoms": "Gejala yang terlihat",
|
|
"causes": "Penyebab penyakit",
|
|
"prevention_measures": ["Langkah pencegahan 1", "Langkah pencegahan 2", "Langkah pencegahan 3", "Langkah pencegahan 4"],
|
|
"organic_treatment": "Pengobatan organik",
|
|
"chemical_treatment": "Pengobatan kimia",
|
|
"additional_info": {
|
|
"severity": "Tinggi/Sedang/Rendah",
|
|
"spread_rate": "Tingkat penyebaran",
|
|
"affected_parts": ["Daun", "Batang", "Akar", "Buah"],
|
|
"environmental_conditions": "Kondisi lingkungan yang mendukung perkembangan penyakit"
|
|
},
|
|
"environmental_data": {
|
|
"temperature": 0.0,
|
|
"humidity": 0.0,
|
|
"lightIntensity": 0.0,
|
|
"soilPh": 0.0
|
|
},
|
|
"plant_data": {
|
|
"growthStage": "Fase pertumbuhan",
|
|
"age": "Umur tanaman",
|
|
"diseaseSeverity": 0.0,
|
|
"infectedArea": 0.0
|
|
},
|
|
"treatment_schedule": {
|
|
"wateringSchedule": "Penyiraman harus disesuaikan dengan kebutuhan tanaman dan kondisi lingkungan. Hindari penyiraman yang berlebihan.",
|
|
"fertilizingSchedule": "Pemupukan yang seimbang dan tepat dapat meningkatkan ketahanan tanaman terhadap penyakit. Gunakan pupuk sesuai rekomendasi.",
|
|
"pesticideSchedule": "Penggunaan pestisida (baik organik maupun kimia) harus dilakukan sesuai dengan petunjuk dan rekomendasi, dengan memperhatikan interval waktu aplikasi."
|
|
},
|
|
"economic_impact": {
|
|
"estimatedLoss": "Estimasi kerugian",
|
|
"recoveryTime": "Waktu pemulihan"
|
|
},
|
|
"alternative_varieties": [
|
|
{
|
|
"name": "Nama varietas",
|
|
"description": "Deskripsi varietas",
|
|
"resistanceLevel": "Tingkat ketahanan"
|
|
}
|
|
]
|
|
}
|
|
|
|
Penting: Berikan informasi yang lengkap dan akurat untuk semua bidang. Untuk tanaman padi, berikan informasi spesifik tentang penyakit bercak daun bakteri jika terdeteksi. Sertakan rekomendasi pengendalian gulma untuk mengurangi penyebaran penyakit.
|
|
''';
|
|
|
|
final requestBody = {
|
|
'contents': [
|
|
{
|
|
'parts': [
|
|
{'text': prompt},
|
|
{
|
|
'inline_data': {
|
|
'mime_type': 'image/jpeg',
|
|
'data': base64Image
|
|
}
|
|
}
|
|
]
|
|
}
|
|
],
|
|
'generationConfig': {
|
|
'temperature': 0.2,
|
|
'topK': 32,
|
|
'topP': 1,
|
|
'maxOutputTokens': 4096,
|
|
}
|
|
};
|
|
|
|
final response = await http.post(
|
|
Uri.parse('$baseUrl/$model:generateContent?key=$apiKey'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode(requestBody),
|
|
);
|
|
|
|
if (response.statusCode != 200) {
|
|
String msg = response.body;
|
|
if (response.statusCode == 404 && msg.contains('not found')) {
|
|
msg += '\nModel $model might not be available for your API key. For free users, only gemini-1.5-flash is guaranteed.\nSee: https://ai.google.dev/gemini-api/docs/models';
|
|
}
|
|
throw Exception('Failed to get diagnosis: ${response.statusCode} - $msg');
|
|
}
|
|
|
|
final responseData = jsonDecode(response.body);
|
|
if (responseData['candidates'] == null || responseData['candidates'].isEmpty) {
|
|
throw Exception('No response from Gemini API');
|
|
}
|
|
final generatedText = responseData['candidates'][0]['content']['parts'][0]['text'];
|
|
final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(generatedText);
|
|
if (jsonMatch == null) {
|
|
throw Exception('Gagal memproses respons dari server. Data tidak dapat dibaca.');
|
|
}
|
|
final diagnosisData = jsonDecode(jsonMatch.group(0)!);
|
|
|
|
// Ensure all required fields have default values if missing
|
|
final processedData = _ensureCompleteData(diagnosisData);
|
|
|
|
// Create the diagnosis model
|
|
return DiagnosisResultModel(
|
|
plantSpecies: processedData['plant_species'] ?? 'Unknown Plant',
|
|
isHealthy: processedData['is_healthy'] ?? true,
|
|
diseaseName: processedData['disease_name'] ?? '',
|
|
scientificName: processedData['scientific_name'] ?? '',
|
|
confidenceValue: (processedData['confidence_value'] ?? 0.5).toDouble(),
|
|
symptoms: (processedData['symptoms'] is List)
|
|
? (processedData['symptoms'] as List).join(', ')
|
|
: (processedData['symptoms'] ?? 'Tidak ada gejala terdeteksi'),
|
|
causes: (processedData['causes'] is List)
|
|
? (processedData['causes'] as List).join(', ')
|
|
: (processedData['causes'] ?? 'Tidak ada penyebab teridentifikasi'),
|
|
preventionMeasures: (processedData['prevention_measures'] is List)
|
|
? List<String>.from(processedData['prevention_measures'])
|
|
: (processedData['prevention_measures'] is String)
|
|
? [processedData['prevention_measures']]
|
|
: [],
|
|
organicTreatment: (processedData['organic_treatment'] is List)
|
|
? (processedData['organic_treatment'] as List).join(', ')
|
|
: (processedData['organic_treatment'] ?? 'Tidak ada pengobatan organik'),
|
|
chemicalTreatment: (processedData['chemical_treatment'] is List)
|
|
? (processedData['chemical_treatment'] as List).join(', ')
|
|
: (processedData['chemical_treatment'] ?? 'Tidak ada pengobatan kimia'),
|
|
additionalInfo: AdditionalInfoModel(
|
|
severity: processedData['additional_info']?['severity'] ?? 'Tidak diketahui',
|
|
spreadRate: processedData['additional_info']?['spread_rate'] ?? 'Tidak diketahui',
|
|
affectedParts: (processedData['additional_info']?['affected_parts'] is List)
|
|
? List<String>.from(processedData['additional_info']?['affected_parts'])
|
|
: (processedData['additional_info']?['affected_parts'] is String)
|
|
? [processedData['additional_info']?['affected_parts']]
|
|
: ['Daun'],
|
|
environmentalConditions: processedData['additional_info']?['environmental_conditions'] ?? 'Tidak diketahui',
|
|
),
|
|
environmentalData: processedData['environmental_data'] ?? {},
|
|
plantData: processedData['plant_data'] ?? {},
|
|
treatmentSchedule: processedData['treatment_schedule'] ?? {},
|
|
economicImpact: processedData['economic_impact'] ?? {},
|
|
alternativeVarieties: (processedData['alternative_varieties'] is List)
|
|
? List<Map<String, dynamic>>.from(processedData['alternative_varieties'])
|
|
: [],
|
|
);
|
|
} catch (e) {
|
|
print('Error in diagnosePlant: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// Helper method to ensure all required fields have default values
|
|
Map<String, dynamic> _ensureCompleteData(Map<String, dynamic> diagnosisData) {
|
|
// Deep copy the original data
|
|
final Map<String, dynamic> processedData = Map.from(diagnosisData);
|
|
|
|
// Ensure plant_species exists
|
|
processedData['plant_species'] ??= 'Padi';
|
|
|
|
// Ensure is_healthy exists
|
|
processedData['is_healthy'] ??= false;
|
|
|
|
// For rice plants, if disease is detected but not named, default to bacterial leaf blight
|
|
if (processedData['plant_species'].toString().toLowerCase().contains('padi') &&
|
|
processedData['is_healthy'] == false &&
|
|
(processedData['disease_name'] == null || processedData['disease_name'].toString().isEmpty)) {
|
|
processedData['disease_name'] = 'Bercak Daun Bakteri';
|
|
processedData['scientific_name'] = 'Xanthomonas oryzae pv. oryzae';
|
|
}
|
|
|
|
// Ensure additional_info exists
|
|
processedData['additional_info'] ??= {};
|
|
|
|
// Ensure severity exists in additional_info
|
|
if (processedData['additional_info'] is Map) {
|
|
(processedData['additional_info'] as Map)['severity'] ??= 'Sedang';
|
|
(processedData['additional_info'] as Map)['affected_parts'] ??= ['Daun'];
|
|
(processedData['additional_info'] as Map)['environmental_conditions'] ??=
|
|
'Kondisi lingkungan yang lembab dan hangat (suhu 25-30°C dan kelembapan tinggi) sangat mendukung perkembangan penyakit ini.';
|
|
}
|
|
|
|
// Ensure plant_data exists
|
|
processedData['plant_data'] ??= {};
|
|
|
|
// Add default values for plant_data
|
|
if (processedData['plant_data'] is Map) {
|
|
(processedData['plant_data'] as Map)['growthStage'] ??= 'Tidak dapat ditentukan dari gambar';
|
|
(processedData['plant_data'] as Map)['infectedArea'] ??= 0;
|
|
}
|
|
|
|
// Ensure treatment_schedule exists
|
|
processedData['treatment_schedule'] ??= {
|
|
'wateringSchedule': 'Penyiraman harus disesuaikan dengan kebutuhan tanaman dan kondisi lingkungan. Hindari penyiraman yang berlebihan.',
|
|
'fertilizingSchedule': 'Pemupukan yang seimbang dan tepat dapat meningkatkan ketahanan tanaman terhadap penyakit. Gunakan pupuk sesuai rekomendasi.',
|
|
'pesticideSchedule': 'Penggunaan pestisida (baik organik maupun kimia) harus dilakukan sesuai dengan petunjuk dan rekomendasi, dengan memperhatikan interval waktu aplikasi.'
|
|
};
|
|
|
|
// Ensure economic_impact exists
|
|
processedData['economic_impact'] ??= {};
|
|
|
|
// Add default values for economic_impact
|
|
if (processedData['economic_impact'] is Map) {
|
|
(processedData['economic_impact'] as Map)['estimatedLoss'] ??= 'Tidak dapat ditentukan dari gambar. Kerugian bergantung pada luas area yang terinfeksi.';
|
|
}
|
|
|
|
// Ensure prevention_measures exists
|
|
if (processedData['prevention_measures'] == null ||
|
|
(processedData['prevention_measures'] is List && (processedData['prevention_measures'] as List).isEmpty)) {
|
|
processedData['prevention_measures'] = [
|
|
'Penggunaan benih yang sehat dan bersertifikat bebas penyakit.',
|
|
'Sanitasi lahan pertanian yang baik, termasuk pembersihan sisa-sisa tanaman setelah panen.',
|
|
'Penggunaan varietas padi yang tahan terhadap penyakit bercak daun bakteri.',
|
|
'Pengendalian gulma untuk mengurangi penyebaran penyakit.'
|
|
];
|
|
}
|
|
|
|
// Ensure organic_treatment exists
|
|
processedData['organic_treatment'] ??= 'Penggunaan pestisida nabati seperti ekstrak nimba atau ekstrak tembakau dapat membantu mengendalikan penyebaran penyakit. Namun, efikasi pengobatan organik terbatas dan mungkin perlu dikombinasikan dengan metode pengendalian lainnya.';
|
|
|
|
// Ensure chemical_treatment exists
|
|
processedData['chemical_treatment'] ??= 'Penggunaan bakterisida seperti kasugamycin atau oxytetracycline dapat efektif dalam mengendalikan penyakit bercak daun bakteri. Ikuti petunjuk penggunaan dengan cermat dan perhatikan dosis yang tepat untuk menghindari dampak negatif terhadap lingkungan dan kesehatan manusia.';
|
|
|
|
return processedData;
|
|
}
|
|
} |