Spaces:
Sleeping
Sleeping
| 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 sqlite3 = require('sqlite3').verbose(); | |
| const sharp = require('sharp'); | |
| const { v4: uuidv4 } = require('uuid'); | |
| require('dotenv').config(); | |
| const app = express(); | |
| // cPanel and other hosts use the PORT environment variable | |
| const PORT = process.env.PORT || 5000; | |
| // --- Middleware --- | |
| // Enable CORS for all routes | |
| app.use(cors()); | |
| // Parse JSON bodies for API requests | |
| app.use(express.json()); | |
| app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); | |
| // Ensure directories exist | |
| const dirs = ['uploads', 'temp', 'results']; // Removed 'database' to prevent permission errors | |
| dirs.forEach(dir => { | |
| const dirPath = path.join(__dirname, dir); | |
| if (!fs.existsSync(dirPath)) { | |
| fs.mkdirSync(dirPath, { recursive: true }); // Still need temp dirs for uploads | |
| } | |
| }); | |
| // --- DATABASE DISABLED --- | |
| // The database is temporarily disabled for simplified deployment on Hugging Face. | |
| // All database operations will be skipped, and the app will run without persistence. | |
| console.log('DATABASE: Temporarily disabled for deployment. No data will be saved or retrieved.'); | |
| const db = null; | |
| // --- UNIFIED MULTER CONFIG --- | |
| // A single configuration for handling all file uploads to a temporary directory. | |
| const storage = multer.diskStorage({ | |
| destination: (req, file, cb) => { | |
| cb(null, path.join(__dirname, 'temp/')); | |
| }, | |
| filename: (req, file, cb) => { | |
| const uniqueName = `${file.fieldname}-${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`; | |
| cb(null, uniqueName); | |
| } | |
| }); | |
| const upload = multer({ | |
| storage: storage, | |
| limits: { fileSize: 50 * 1024 * 1024 }, // 50MB general limit | |
| }); | |
| // --- HELPER FUNCTIONS --- | |
| function fallbackAnalysis(imagePath, waterSource) { | |
| console.log('--- Executing Node.js Fallback Analysis ---'); | |
| // This simulates the Python script's output when it's not available. | |
| return { | |
| ph: 7.1, | |
| chlorine: 0.5, | |
| nitrates: 5, | |
| hardness: 150, | |
| alkalinity: 120, | |
| bacteria: 0, | |
| confidence: 40, // Lower confidence to indicate it's a fallback | |
| processingMethod: "Node.js Fallback" | |
| }; | |
| } | |
| // Refactored helper to call any Python script, handle JSON, and manage errors. | |
| function callPythonScript(scriptName, args = [], inputData = null) { | |
| return new Promise((resolve, reject) => { | |
| const scriptPath = path.join(__dirname, 'python', scriptName); | |
| const pythonProcess = spawn('python3', [scriptPath, ...args]); | |
| let dataString = ''; | |
| let errorString = ''; | |
| pythonProcess.stdout.on('data', (data) => { dataString += data.toString(); }); | |
| pythonProcess.stderr.on('data', (data) => { errorString += data.toString(); }); | |
| if (inputData) { | |
| pythonProcess.stdin.write(JSON.stringify(inputData)); | |
| pythonProcess.stdin.end(); | |
| } | |
| pythonProcess.on('close', (code) => { | |
| if (code === 0) { | |
| try { | |
| resolve(JSON.parse(dataString)); | |
| } catch (error) { | |
| console.error(`Error parsing JSON from ${scriptName}:`, dataString); | |
| reject(new Error(`Failed to parse Python output from ${scriptName}`)); | |
| } | |
| } else { | |
| console.error(`Error in ${scriptName} (code ${code}): ${errorString}`); | |
| reject(new Error(errorString || `Python script ${scriptName} exited with code ${code}`)); | |
| } | |
| }); | |
| }); | |
| } | |
| // --- API ROUTES --- | |
| // --- HEALTH CHECK ENDPOINT --- | |
| // This is crucial for platforms like Hugging Face to know the app is ready. | |
| app.get('/api/health', (req, res) => { | |
| res.status(200).json({ status: 'ok', message: 'Backend is running' }); | |
| }); | |
| // --- BIODIVERSITYEAR AUDIO ANALYSIS ENDPOINT --- | |
| app.post('/api/analyze-audio', upload.single('audioFile'), async (req, res) => { | |
| if (!req.file) return res.status(400).json({ message: 'Audio file is missing.' }); | |
| try { | |
| console.log('โถ๏ธ Calling Python AI script for audio analysis...'); | |
| const result = await callPythonScript('audio_analyzer.py', [req.file.path]); | |
| res.status(200).json(result); | |
| } catch (error) { | |
| res.status(500).json({ message: 'Error during audio analysis.', error: error.message }); | |
| } finally { | |
| // Clean up the temporary file | |
| fs.unlink(req.file.path, (err) => { | |
| if (err) console.error(`- Error deleting temp audio file: ${err.message}`); | |
| }); | |
| } | |
| }); | |
| // --- BIO-STREAM AI DNA ANALYSIS ENDPOINT --- | |
| app.post('/api/analyze-dna', upload.single('dnaFile'), async (req, res) => { | |
| if (!req.file) return res.status(400).json({ message: 'DNA file is missing.' }); | |
| try { | |
| console.log('โถ๏ธ Calling Python AI script for DNA analysis...'); | |
| const result = await callPythonScript('dna_analyzer.py', [req.file.path]); | |
| res.status(200).json(result); | |
| } catch (error) { | |
| res.status(500).json({ message: 'Error during DNA analysis.', error: error.message }); | |
| } finally { | |
| fs.unlink(req.file.path, (err) => { | |
| if (err) console.error(`- Error deleting temp DNA file: ${err.message}`); | |
| }); | |
| } | |
| }); | |
| // --- JSON-based Analysis Endpoints (Footprint, E-Waste) --- | |
| async function handleJsonAnalysis(req, res, scriptName, logMessage) { | |
| console.log(`โ Backend: Received ${logMessage} request.`); | |
| try { | |
| const result = await callPythonScript(scriptName, [], req.body); | |
| res.status(200).json(result); | |
| } catch (error) { | |
| res.status(500).json({ message: `Error during ${logMessage} analysis.`, error: error.message }); | |
| } | |
| } | |
| app.post('/api/analyze-footprint', (req, res) => handleJsonAnalysis(req, res, 'phantom_footprint_analyzer.py', 'Phantom Footprint')); | |
| app.post('/api/analyze-ewaste', (req, res) => handleJsonAnalysis(req, res, 'ewaste_analyzer.py', 'E-Waste')); | |
| // --- AQUALENS WATER ANALYSIS ENDPOINT --- | |
| app.post('/api/analyze-water', upload.single('image'), async (req, res) => { | |
| const startTime = Date.now(); | |
| try { | |
| if (!req.file) { | |
| return res.status(400).json({ error: 'No image file provided' }); | |
| } | |
| const { waterSource, latitude, longitude, userId } = req.body; | |
| const imagePath = req.file.path; | |
| const testId = uuidv4(); | |
| console.log(`๐งช Starting water analysis for test ${testId}`); | |
| console.log(`๐ Location: ${latitude}, ${longitude}`); | |
| console.log(`๐ฐ Water Source: ${waterSource}`); | |
| // Step 1: Image preprocessing with Sharp (Node.js) | |
| const preprocessedPath = path.join(__dirname, 'temp', `preprocessed_${testId}.jpg`); | |
| await sharp(imagePath) | |
| .resize(800, 600, { fit: 'inside' }) | |
| .normalize() | |
| .sharpen() | |
| .jpeg({ quality: 95 }) | |
| .toFile(preprocessedPath); | |
| console.log('โ Image preprocessing completed'); | |
| // Step 2: AI analysis with Python | |
| let analysisResult; | |
| try { | |
| analysisResult = await callPythonScript('water_analysis.py', [preprocessedPath, waterSource || 'unknown']); | |
| console.log('โ AI analysis completed'); | |
| } catch (error) { | |
| // If the python script fails, use the fallback. | |
| console.log(`Python script for water analysis failed, using fallback. Error: ${error.message}`); | |
| analysisResult = fallbackAnalysis(preprocessedPath, waterSource); | |
| } | |
| const processingTime = (Date.now() - startTime) / 1000; | |
| // Step 4: Determine overall quality and safety | |
| const { ph, chlorine, nitrates, hardness, alkalinity, bacteria } = analysisResult; | |
| let overallQuality = 'Excellent'; | |
| let safetyLevel = 'Safe'; | |
| let alerts = []; | |
| let recommendations = []; | |
| if (ph < 6.5 || ph > 8.5) { | |
| alerts.push(`pH levels outside safe range: ${ph.toFixed(1)}`); | |
| recommendations.push('Consider pH adjustment or filtration system'); | |
| if (ph < 5.0 || ph > 9.5) { | |
| safetyLevel = 'Unsafe'; | |
| overallQuality = 'Poor'; | |
| recommendations.push('Contact water authority immediately - pH outside safe drinking range'); | |
| } else { | |
| overallQuality = 'Good'; | |
| } | |
| } | |
| if (chlorine > 4.0) { | |
| alerts.push(`High chlorine levels: ${chlorine.toFixed(1)} ppm`); | |
| recommendations.push('Let water sit uncovered for 30 minutes before drinking'); | |
| safetyLevel = 'Unsafe'; | |
| overallQuality = 'Poor'; | |
| } else if (chlorine < 0.2 && waterSource === 'Tap Water') { | |
| alerts.push('Low chlorine in tap water may indicate contamination risk'); | |
| recommendations.push('Consider boiling water or using filtration'); | |
| } | |
| if (nitrates > 10) { | |
| alerts.push(`Elevated nitrate levels: ${nitrates} ppm`); | |
| recommendations.push('Consider reverse osmosis filtration or alternative water source'); | |
| if (nitrates > 50) { | |
| safetyLevel = 'Unsafe'; | |
| overallQuality = 'Poor'; | |
| recommendations.push('DO NOT DRINK - Especially dangerous for infants and pregnant women'); | |
| } else { | |
| overallQuality = 'Fair'; | |
| } | |
| } | |
| if (bacteria > 0) { | |
| alerts.push('Bacterial contamination detected'); | |
| recommendations.push('Boil water for 1 minute before drinking or use alternative source'); | |
| safetyLevel = 'Unsafe'; | |
| overallQuality = 'Poor'; | |
| } | |
| if (hardness > 180) { | |
| alerts.push(`Very hard water: ${hardness} ppm`); | |
| recommendations.push('Consider water softener to protect plumbing and improve taste'); | |
| if (overallQuality === 'Excellent') overallQuality = 'Good'; | |
| } | |
| // Step 5: Save to database | |
| if (db) { | |
| // Database logic would go here. | |
| } else { | |
| console.log('DATABASE: Skipping save operation for water analysis results.'); | |
| } | |
| setTimeout(() => { | |
| [preprocessedPath, imagePath].forEach(p => { | |
| if (fs.existsSync(p) && p !== imagePath) { | |
| fs.unlinkSync(p); | |
| } | |
| }); | |
| }, 5000); | |
| console.log(`โ Analysis completed in ${processingTime.toFixed(2)}s`); | |
| res.json({ | |
| success: true, | |
| testId: testId, | |
| results: { | |
| ph: parseFloat(ph.toFixed(1)), | |
| chlorine: parseFloat(chlorine.toFixed(1)), | |
| nitrates: Math.round(nitrates), | |
| hardness: Math.round(hardness), | |
| alkalinity: Math.round(alkalinity), | |
| bacteria: bacteria | |
| }, | |
| overallQuality: overallQuality, | |
| safetyLevel: safetyLevel, | |
| alerts: alerts, | |
| recommendations: recommendations, | |
| confidence: Math.round(analysisResult.confidence || 95), | |
| processingTime: parseFloat(processingTime.toFixed(2)), | |
| colorAccuracy: analysisResult.colorAccuracy || '94%', | |
| processingMethod: analysisResult.processingMethod || 'AI Analysis', | |
| colorChannels: analysisResult.colorChannels || {}, | |
| timestamp: new Date().toISOString(), | |
| location: { | |
| latitude: parseFloat(latitude) || null, | |
| longitude: parseFloat(longitude) || null | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('โ Analysis error:', error); | |
| res.status(500).json({ | |
| error: 'Analysis failed', | |
| details: error.message, | |
| timestamp: new Date().toISOString() | |
| }); | |
| } finally { | |
| if (req.file) fs.unlink(req.file.path, (err) => { if (err) console.error(`- Error deleting temp image file: ${err.message}`); }); | |
| } | |
| }); | |
| // Get water quality map data | |
| app.get('/api/water-map', (req, res) => { | |
| // Since the database is disabled, return an empty array for map data. | |
| console.log('DATABASE: /api/water-map called, returning empty dataset as DB is disabled.'); | |
| res.json({ success: true, data: [], count: 0 }); | |
| }); | |
| // --- STATIC FILE SERVING FOR PRODUCTION --- | |
| // This is the crucial part for serving your React app in production. | |
| // It serves the built React app from the 'web/build' folder. | |
| const buildPath = path.join(__dirname, '..', 'web', 'build'); | |
| app.use(express.static(buildPath)); | |
| // The "catchall" handler: for any request that doesn't match an API route or a static file, | |
| // send back the main index.html file. This allows React Router to handle client-side routing. | |
| app.get('*', (req, res) => { | |
| res.sendFile(path.join(buildPath, 'index.html')); | |
| }); | |
| // --- Start Server --- | |
| app.listen(PORT, () => { | |
| console.log(`๐ฟ GreenPlus by GXS Main Backend Server running on port ${PORT}`); | |
| console.log(`๐ Serving production React app from: ${buildPath}`); | |
| }); |