/** * CropHealth AI - Node.js Express Server * Plant Pathology & Multi-Crop Disease Diagnosis Backend * Connects with models from hansaka01/crophelthh */ const express = require('express'); const cors = require('cors'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { spawn } = require('child_process'); const app = express(); const PORT = process.env.PORT || 7860; // Create uploads directory const UPLOAD_DIR = path.join(__dirname, 'uploads'); if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR, { recursive: true }); } // Multer storage for uploaded files const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, UPLOAD_DIR), filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const ext = path.extname(file.originalname) || '.jpg'; cb(null, `leaf-${uniqueSuffix}${ext}`); } }); const upload = multer({ storage, limits: { fileSize: 15 * 1024 * 1024 } // 15MB max }); // Middleware app.use(cors()); app.use(express.json({ limit: '20mb' })); app.use(express.urlencoded({ extended: true, limit: '20mb' })); app.use(express.static(path.join(__dirname, 'public'))); // Load disease info metadata const diseaseInfoPath = path.join(__dirname, 'disease_info.json'); const diseaseData = JSON.parse(fs.readFileSync(diseaseInfoPath, 'utf8')); // ------------------------------------------------------------- // Python TFLite Inference Worker Management (Persistent Process) // ------------------------------------------------------------- let workerProcess = null; let reqCounter = 0; const pendingRequests = new Map(); function startWorker() { console.log('Starting Python TFLite inference worker...'); workerProcess = spawn('python3', [path.join(__dirname, 'inference_worker.py')], { stdio: ['pipe', 'pipe', 'inherit'] }); let buffer = ''; workerProcess.stdout.on('data', (data) => { buffer += data.toString(); const lines = buffer.split('\n'); buffer = lines.pop(); // keep last incomplete chunk for (const line of lines) { if (!line.trim()) continue; try { const msg = JSON.parse(line); if (msg.status === 'ready') { console.log('✅ Inference worker ready! Loaded models:', msg.loaded_models.join(', ')); } else if (msg.id && pendingRequests.has(msg.id)) { const { resolve, reject } = pendingRequests.get(msg.id); pendingRequests.delete(msg.id); if (msg.status === 'ok') { resolve(msg); } else { reject(new Error(msg.message || 'Worker prediction error')); } } } catch (err) { console.error('Failed to parse worker stdout message:', line, err); } } }); workerProcess.on('exit', (code, signal) => { console.warn(`Worker exited (code ${code}, signal ${signal}). Restarting in 2s...`); setTimeout(startWorker, 2000); }); } startWorker(); function sendWorkerRequest(payload) { return new Promise((resolve, reject) => { const id = ++reqCounter; payload.id = id; const timeout = setTimeout(() => { if (pendingRequests.has(id)) { pendingRequests.delete(id); reject(new Error('Inference worker request timed out (15s)')); } }, 15000); pendingRequests.set(id, { resolve: (data) => { clearTimeout(timeout); resolve(data); }, reject: (err) => { clearTimeout(timeout); reject(err); } }); try { workerProcess.stdin.write(JSON.stringify(payload) + '\n'); } catch (err) { pendingRequests.delete(id); clearTimeout(timeout); reject(err); } }); } // ------------------------------------------------------------- // REST API Routes // ------------------------------------------------------------- // Health check app.get('/api/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), models: Object.keys(diseaseData.crops) }); }); // Get supported crops app.get('/api/crops', (req, res) => { res.json({ crops: diseaseData.crops, total_crops: Object.keys(diseaseData.crops).length, total_diseases: Object.keys(diseaseData.diseases).length }); }); // Get encyclopedia with optional search & filter app.get('/api/encyclopedia', (req, res) => { const { crop, q } = req.query; const results = []; const query = (q || '').toLowerCase().trim(); for (const [key, info] of Object.entries(diseaseData.diseases)) { if (crop && crop !== 'all' && info.crop.toLowerCase().indexOf(crop.toLowerCase()) === -1) { continue; } if (query) { const haystack = `${info.title} ${info.pathogen} ${info.symptoms} ${info.causes} ${info.organic} ${info.chemical} ${info.prevention}`.toLowerCase(); if (!haystack.includes(query)) { continue; } } results.push({ key, ...info }); } res.json({ total: results.length, results }); }); // Single crop diagnosis app.post('/api/diagnose', upload.single('image'), async (req, res) => { let imagePath = null; let isTempFile = false; try { const crop = req.body.crop || 'tomato'; const topK = parseInt(req.body.top_k, 10) || 5; // Check if uploaded via form file or base64 JSON if (req.file) { imagePath = req.file.path; isTempFile = true; } else if (req.body.image_base64) { const matches = req.body.image_base64.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); const buffer = matches ? Buffer.from(matches[2], 'base64') : Buffer.from(req.body.image_base64, 'base64'); const tempFilename = `b64-${Date.now()}-${Math.round(Math.random() * 1e9)}.jpg`; imagePath = path.join(UPLOAD_DIR, tempFilename); fs.writeFileSync(imagePath, buffer); isTempFile = true; } else if (req.body.sample_path) { imagePath = path.resolve(__dirname, req.body.sample_path); if (!fs.existsSync(imagePath)) { return res.status(404).json({ error: 'Sample image not found on server' }); } } else { return res.status(400).json({ error: 'No image provided. Please upload an image file or provide image_base64.' }); } const workerResponse = await sendWorkerRequest({ action: 'predict', crop, image_path: imagePath }); const result = workerResponse.result; result.all_predictions = result.all_predictions.slice(0, topK); res.json({ status: 'success', ...result }); } catch (err) { console.error('Diagnosis error:', err); res.status(500).json({ error: err.message || 'Internal error during diagnosis' }); } finally { // Cleanup temporary upload file if (isTempFile && imagePath && fs.existsSync(imagePath)) { fs.unlink(imagePath, (err) => { if (err) console.error('Error removing temp file:', err); }); } } }); // Multi-crop cross diagnosis app.post('/api/diagnose-all', upload.single('image'), async (req, res) => { let imagePath = null; let isTempFile = false; try { if (req.file) { imagePath = req.file.path; isTempFile = true; } else if (req.body.image_base64) { const matches = req.body.image_base64.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); const buffer = matches ? Buffer.from(matches[2], 'base64') : Buffer.from(req.body.image_base64, 'base64'); const tempFilename = `b64-${Date.now()}-${Math.round(Math.random() * 1e9)}.jpg`; imagePath = path.join(UPLOAD_DIR, tempFilename); fs.writeFileSync(imagePath, buffer); isTempFile = true; } else if (req.body.sample_path) { imagePath = path.resolve(__dirname, req.body.sample_path); if (!fs.existsSync(imagePath)) { return res.status(404).json({ error: 'Sample image not found on server' }); } } else { return res.status(400).json({ error: 'No image provided.' }); } const workerResponse = await sendWorkerRequest({ action: 'predict_all', image_path: imagePath }); res.json({ status: 'success', results: workerResponse.results }); } catch (err) { console.error('Multi diagnosis error:', err); res.status(500).json({ error: err.message || 'Internal error during multi diagnosis' }); } finally { if (isTempFile && imagePath && fs.existsSync(imagePath)) { fs.unlink(imagePath, (err) => { if (err) console.error('Error removing temp file:', err); }); } } }); // Model specifications endpoint app.get('/api/specs', (req, res) => { res.json({ repository: 'hansaka01/crophelthh', model_architecture: 'EfficientNet-B0 Transfer Learning', input_resolution: '224x224x3 (RGB)', inference_engine: 'TensorFlow Lite (ai-edge-litert)', macro_f1: 0.8889, accuracy: 0.9484, crops_count: Object.keys(diseaseData.crops).length, classes_count: Object.keys(diseaseData.diseases).length, models: diseaseData.crops }); }); // Fallback to index.html for SPA routing app.use((req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Start Server app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 CropHealth AI Web Server running at http://0.0.0.0:${PORT}`); });