looon / server.js
hansaka01's picture
Deploy CropHealth AI Docker Node.js + HTML web application
92d99d3 verified
Raw History Blame Contribute Delete
9.32 kB
/**
* 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}`);
});