Download server.js from hansaka01/looon: direct link, hf CLI and curl.
- Browser
- Download file 9.32 kB
-
https://huggingface.co/spaces/hansaka01/looon/resolve/main/server.js
- Command line
-
hf download hf://spaces/hansaka01/looon/server.js
-
curl -L -o server.js https://huggingface.co/spaces/hansaka01/looon/resolve/main/server.js
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}`); | |
| }); | |