739 lines
18 KiB
Text
739 lines
18 KiB
Text
|
|
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');
|
||
|
|
});
|
||
|
|
}
|