const express = require('express'); const app = express(); const cors = require('cors'); require('dotenv').config(); const { createClient } = require('@supabase/supabase-js'); const Replicate = require('replicate'); const { v4: uuidv4 } = require('uuid'); const port = process.env.PORT || 5235; // Middleware app.use(cors()); app.use(express.json()); // ============================================ // SUPABASE SETUP // ============================================ const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY ); console.log('āœ… Supabase client initialized'); // ============================================ // REPLICATE SETUP // ============================================ const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN }); // In-memory cache for faster status checks const jobsCache = new Map(); // ============================================ // HELPER FUNCTIONS // ============================================ /** * Build prompt from products and effects */ function buildPrompt(products, effect) { const basePrompt = "professional fashion photography, full body shot, high quality, detailed, realistic, 8k"; const effectPrompts = { 'sunset-lighting': 'warm sunset lighting, golden hour, soft shadows', 'studio-lighting': 'professional studio lighting, clean background', 'dramatic': 'dramatic lighting, high contrast, cinematic', 'natural': 'natural daylight, outdoor setting', 'vintage': 'vintage film photography, retro aesthetic', 'modern': 'modern minimalist, clean aesthetic' }; const effectDescription = effectPrompts[effect] || 'natural lighting'; return `${basePrompt}, ${effectDescription}`; } /** * Generate image with Replicate */ async function generateWithReplicate(options) { const { products, pose, background, effect } = options; try { console.log('\nšŸŽØ Starting AI generation...'); console.log('Products:', products.length); console.log('Effect:', effect); const prompt = buildPrompt(products, effect); console.log('Prompt:', prompt); // Prepare input const input = { prompt: prompt, negative_prompt: "low quality, blurry, distorted, ugly, bad anatomy, extra limbs", num_inference_steps: 30, guidance_scale: 7.5, width: 1024, height: 1024 }; // Add main product image if (products && products.length > 0) { input.image = products[0]; input.prompt_strength = 0.8; } // Add pose control if (pose) { input.control_image = pose; input.control_type = 'openpose'; input.controlnet_conditioning_scale = 0.8; } // Stable Diffusion XL const model = "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b"; console.log('Calling Replicate API...'); const startTime = Date.now(); const output = await replicate.run(model, { input }); const processingTime = Math.round((Date.now() - startTime) / 1000); console.log(`āœ… Generation completed in ${processingTime}s`); const imageUrl = Array.isArray(output) ? output[0] : output; return { imageUrl, processingTime }; } catch (error) { console.error('āŒ Replicate error:', error); throw error; } } /** * Process job in background */ async function processJob(jobId) { try { console.log(`\nšŸ“‹ Processing job: ${jobId}`); // Get job from Supabase const { data: job, error: fetchError } = await supabase .from('generation_jobs') .select('*') .eq('job_id', jobId) .single(); if (fetchError || !job) { throw new Error('Job not found'); } // Update status to processing await supabase .from('generation_jobs') .update({ status: 'processing', progress: 20 }) .eq('job_id', jobId); jobsCache.set(jobId, { status: 'processing', progress: 20 }); console.log('Status: processing'); // Generate image const { imageUrl, processingTime } = await generateWithReplicate({ products: job.product_urls, pose: job.pose_url, background: job.background_url, effect: job.effect }); // Update with result const { error: updateError } = await supabase .from('generation_jobs') .update({ status: 'completed', progress: 100, generated_image_url: imageUrl, processing_time: processingTime, completed_at: new Date().toISOString() }) .eq('job_id', jobId); if (updateError) { throw updateError; } jobsCache.set(jobId, { status: 'completed', progress: 100, imageUrl: imageUrl }); console.log(`āœ… Job ${jobId} completed!`); console.log(`Image: ${imageUrl}`); } catch (error) { console.error(`āŒ Job ${jobId} failed:`, error); // Update as failed await supabase .from('generation_jobs') .update({ status: 'failed', error: error.message, completed_at: new Date().toISOString() }) .eq('job_id', jobId); jobsCache.set(jobId, { status: 'failed', error: error.message }); } } // ============================================ // API ROUTES // ============================================ /** * POST /api/generate * Create new generation job */ app.post('/api/generate', async (req, res) => { try { const { userId, userImage, wardrobeItems, selectedPose, selectedBackground, selectedEffect } = req.body; console.log('\nšŸ†• New generation request'); console.log('User:', userId); console.log('Wardrobe items:', wardrobeItems?.length || 0); console.log('User image:', userImage ? 'Yes' : 'No'); console.log('Background:', selectedBackground?.name); console.log('Pose:', selectedPose?.name); console.log('Effect:', selectedEffect?.name); // Validation if (!wardrobeItems || wardrobeItems.length === 0) { return res.status(400).json({ success: false, error: 'At least one wardrobe item is required' }); } if (!userImage) { return res.status(400).json({ success: false, error: 'User image is required' }); } // Generate job ID const jobId = uuidv4(); console.log('Job ID:', jobId); // Extract image URLs from wardrobe items const productUrls = wardrobeItems.map(item => item.image_url); // Insert into Supabase const { error: insertError } = await supabase .from('generation_jobs') .insert({ job_id: jobId, user_id: userId || 'guest', user_image: userImage, product_urls: productUrls, products: wardrobeItems, pose_url: selectedPose?.pose_image_url, pose: selectedPose, background_url: selectedBackground?.bg_image_url, background: selectedBackground, effect: selectedEffect?.name || 'natural', effect_data: selectedEffect, status: 'queued', progress: 0, created_at: new Date().toISOString() }); if (insertError) { console.error('Supabase insert error:', insertError); throw insertError; } console.log('āœ… Job created in Supabase'); // Cache in memory jobsCache.set(jobId, { status: 'queued', progress: 0 }); // Process in background setImmediate(() => { processJob(jobId).catch(err => { console.error('Background processing error:', err); }); }); // Return response res.status(201).json({ success: true, jobId, status: 'queued', message: 'Generation job created successfully', estimatedTime: '30-60 seconds' }); } catch (error) { console.error('āŒ Generate error:', error); res.status(500).json({ success: false, error: error.message }); } }); /** * POST /api/products * Add a new product */ app.post('/api/addproducts', async (req, res) => { try { const { name, price, description, image_url, category } = req.body; // Basic validation if (!name || !price || !description || !image_url || !category) { return res.status(400).json({ success: false, error: 'All fields (name, price, description, image_url, category) are required' }); } // Insert into Supabase const { data, error } = await supabase .from('products') .insert([ { name, price, description, image_url, category, created_at: new Date().toISOString(), updated_at: new Date().toISOString() } ]) .select('*') .single(); if (error) throw error; res.status(201).json({ success: true, message: 'āœ… Product created successfully', product: data }); } catch (error) { console.error('āŒ Product insert error:', error); res.status(500).json({ success: false, error: error.message }); } }); app.get('/api/backgrounds', async (req, res) => { try { const { data, error } = await supabase .from('backgrounds') .select('*') .order('id', { ascending: true }); if (error) throw error; res.json({ success: true, backgrounds: data || [] }); } catch (err) { console.error('āŒ Backgrounds fetch error:', err); res.status(500).json({ success: false, error: err.message }); } }); app.get('/api/poses', async (req, res) => { try { const { data, error } = await supabase .from('poses') .select('*') .order('id', { ascending: true }); if (error) throw error; res.json({ success: true, poses: data || [] }); } catch (err) { console.error('āŒ Poses fetch error:', err); res.status(500).json({ success: false, error: err.message }); } }); app.get('/api/effects', async (req, res) => { try { const { data, error } = await supabase .from('effects') .select('*') .order('id', { ascending: true }); if (error) throw error; res.json({ success: true, effects: data || [] }); } catch (err) { console.error('āŒ Effects fetch error:', err); res.status(500).json({ success: false, error: err.message }); } }); /** * GET /api/job-status/:jobId * Get job status */ app.get('/api/job-status/:jobId', async (req, res) => { try { const { jobId } = req.params; // Check cache first (faster) let cachedJob = jobsCache.get(jobId); // Get from Supabase const { data: job, error } = await supabase .from('generation_jobs') .select('*') .eq('job_id', jobId) .single(); if (error || !job) { return res.status(404).json({ success: false, error: 'Job not found' }); } // Update cache jobsCache.set(jobId, { status: job.status, progress: job.progress, imageUrl: job.generated_image_url }); res.json({ success: true, jobId: job.job_id, status: job.status, progress: job.progress, generatedImageUrl: job.generated_image_url, error: job.error, createdAt: job.created_at, completedAt: job.completed_at, processingTime: job.processing_time }); } catch (error) { console.error('āŒ Status check error:', error); res.status(500).json({ success: false, error: error.message }); } }); /** * GET /api/user-history/:userId * Get user's generation history */ app.get('/api/user-history/:userId', async (req, res) => { try { const { userId } = req.params; const limit = parseInt(req.query.limit) || 20; const offset = parseInt(req.query.skip) || 0; const { data: jobs, error, count } = await supabase .from('generation_jobs') .select('*', { count: 'exact' }) .eq('user_id', userId) .order('created_at', { ascending: false }) .range(offset, offset + limit - 1); if (error) { throw error; } res.json({ success: true, jobs: jobs || [], total: count || 0, limit, skip: offset }); } catch (error) { console.error('āŒ History error:', error); res.status(500).json({ success: false, error: error.message }); } }); /** * GET /api/generation-stats * Get generation statistics */ app.get('/api/generation-stats', async (req, res) => { try { // Get total count const { count: total } = await supabase .from('generation_jobs') .select('*', { count: 'exact', head: true }); // Get completed count const { count: completed } = await supabase .from('generation_jobs') .select('*', { count: 'exact', head: true }) .eq('status', 'completed'); // Get failed count const { count: failed } = await supabase .from('generation_jobs') .select('*', { count: 'exact', head: true }) .eq('status', 'failed'); // Get processing count const { count: processing } = await supabase .from('generation_jobs') .select('*', { count: 'exact', head: true }) .eq('status', 'processing'); // Get average processing time const { data: avgData } = await supabase .from('generation_jobs') .select('processing_time') .eq('status', 'completed') .not('processing_time', 'is', null); const avgProcessingTime = avgData && avgData.length > 0 ? Math.round(avgData.reduce((sum, job) => sum + (job.processing_time || 0), 0) / avgData.length) : 0; res.json({ success: true, stats: { total: total || 0, completed: completed || 0, failed: failed || 0, processing: processing || 0, averageProcessingTime: avgProcessingTime, successRate: total > 0 ? `${((completed / total) * 100).toFixed(1)}%` : 'N/A' } }); } catch (error) { console.error('āŒ Stats error:', error); res.status(500).json({ success: false, error: error.message }); } }); /** * GET /api/products * Get all products from Supabase */ app.get('/api/products', async (req, res) => { try { const { data: products, error } = await supabase .from('products') .select('*'); if (error) { throw error; } res.json({ success: true, products: products || [] }); } catch (error) { console.error('āŒ Products error:', error); res.status(500).json({ success: false, error: error.message }); } }); /** * GET /api/products/:id * Get single product */ app.get('/api/products/:id', async (req, res) => { try { const { data: product, error } = await supabase .from('products') .select('*') .eq('id', req.params.id) .single(); if (error || !product) { return res.status(404).json({ success: false, error: 'Product not found' }); } res.json({ success: true, product }); } catch (error) { console.error('āŒ Product error:', error); res.status(500).json({ success: false, error: error.message }); } }); /** * GET /api/health * Health check */ app.get('/api/health', async (req, res) => { try { // Check Supabase connection const { error: supabaseError } = await supabase .from('generation_jobs') .select('job_id', { count: 'exact', head: true }) .limit(1); const supabaseStatus = supabaseError ? 'error' : 'connected'; // Check Replicate let replicateStatus = 'unknown'; try { await replicate.models.list({ limit: 1 }); replicateStatus = 'connected'; } catch (err) { replicateStatus = 'error'; } res.json({ success: true, status: 'healthy', services: { supabase: supabaseStatus, replicate: replicateStatus }, stats: { jobsInCache: jobsCache.size }, timestamp: new Date().toISOString() }); } catch (error) { res.status(503).json({ success: false, error: error.message }); } }); // ============================================ // ROOT ROUTE // ============================================ app.get('/', (req, res) => { res.json({ message: 'Virtual Try-On Server with Supabase', version: '1.0.0', database: 'Supabase (PostgreSQL)', endpoints: { generate: 'POST /api/generate', jobStatus: 'GET /api/job-status/:jobId', userHistory: 'GET /api/user-history/:userId', stats: 'GET /api/generation-stats', products: 'GET /api/products', product: 'GET /api/products/:id', health: 'GET /api/health' } }); }); // ============================================ // ERROR HANDLING // ============================================ app.use((req, res) => { res.status(404).json({ success: false, error: 'Endpoint not found', path: req.path }); }); app.use((err, req, res, next) => { console.error('āŒ Unhandled error:', err); res.status(500).json({ success: false, error: 'Internal server error', message: err.message }); }); // ============================================ // START SERVER // ============================================ // For Vercel serverless functions, export the app if (process.env.NODE_ENV === 'production') { // Export for Vercel serverless module.exports = app; } else { // Local development app.listen(port, () => { console.log('\n' + '='.repeat(60)); console.log('šŸš€ Virtual Try-On Server with Supabase'); console.log('='.repeat(60)); console.log(`šŸ“” Server: http://localhost:${port}`); console.log(`šŸ’¾ Database: Supabase (PostgreSQL)`); console.log(`šŸ“Š Health: http://localhost:${port}/api/health`); console.log('='.repeat(60) + '\n'); }); }