tryon
This commit is contained in:
commit
934f3e1e2b
8 changed files with 5211 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
.env
|
||||
.vercel
|
||||
739
index.js.txt
Normal file
739
index.js.txt
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
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');
|
||||
});
|
||||
}
|
||||
135
index.txt
Normal file
135
index.txt
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
675
new.txt
Normal file
675
new.txt
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
const express = require('express');
|
||||
const app = express();
|
||||
const cors = require('cors');
|
||||
require('dotenv').config();
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const axios = require('axios');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
app.use(express.json({ limit: '50mb' })); // ← Add limit here
|
||||
app.use(express.urlencoded({ limit: '50mb', extended: true })); // ← Add this too
|
||||
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');
|
||||
|
||||
// ============================================
|
||||
// FASHN.AI SETUP
|
||||
// ============================================
|
||||
const FASHN_API_KEY = process.env.FASHN_API_KEY;
|
||||
const FASHN_API_URL = 'https://api.fashn.ai/v1';
|
||||
|
||||
// In-memory cache
|
||||
const jobsCache = new Map();
|
||||
|
||||
// ============================================
|
||||
// FASHN.AI HELPER FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Convert blob/data URL to public URL - Upload to Supabase Storage
|
||||
*/
|
||||
async function uploadImageToStorage(imageData, userId, fileName) {
|
||||
try {
|
||||
console.log('📤 Uploading image to Supabase Storage...');
|
||||
|
||||
let fileBuffer;
|
||||
let contentType = 'image/jpeg';
|
||||
|
||||
if (imageData.startsWith('data:')) {
|
||||
const matches = imageData.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (!matches) {
|
||||
throw new Error('Invalid data URL format');
|
||||
}
|
||||
contentType = matches[1];
|
||||
const base64Data = matches[2];
|
||||
fileBuffer = Buffer.from(base64Data, 'base64');
|
||||
} else if (imageData.startsWith('blob:')) {
|
||||
throw new Error('Blob URLs cannot be uploaded. Please convert to base64 first.');
|
||||
} else {
|
||||
return imageData;
|
||||
}
|
||||
|
||||
const filePath = `${userId}/${fileName}_${Date.now()}.jpg`;
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from('user-uploads')
|
||||
.upload(filePath, fileBuffer, {
|
||||
contentType: contentType,
|
||||
upsert: false
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase storage error:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { data: urlData } = supabase.storage
|
||||
.from('user-uploads')
|
||||
.getPublicUrl(filePath);
|
||||
|
||||
console.log('✅ Image uploaded:', urlData.publicUrl);
|
||||
return urlData.publicUrl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Upload error:', error);
|
||||
throw new Error(`Failed to upload image: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply single garment using FASHN.ai with polling
|
||||
*/
|
||||
async function applyGarmentWithFashn(modelImage, garmentImage) {
|
||||
try {
|
||||
console.log('📤 Calling FASHN.ai API...');
|
||||
console.log('Model image:', modelImage.substring(0, 80) + '...');
|
||||
console.log('Garment image:', garmentImage.substring(0, 80) + '...');
|
||||
|
||||
if (modelImage.startsWith('blob:') || modelImage.startsWith('data:')) {
|
||||
throw new Error('Model image must be a public URL');
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Step 1: Submit job
|
||||
const requestBody = {
|
||||
model_image: modelImage,
|
||||
garment_image: garmentImage,
|
||||
category: 'auto'
|
||||
};
|
||||
|
||||
console.log('📤 Submitting job to FASHN.ai...');
|
||||
|
||||
const submitResponse = await axios.post(
|
||||
`${FASHN_API_URL}/run`,
|
||||
requestBody,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${FASHN_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
console.log('📥 Submission response:', JSON.stringify(submitResponse.data, null, 2));
|
||||
|
||||
if (!submitResponse.data || !submitResponse.data.id) {
|
||||
throw new Error('No job ID received from FASHN.ai');
|
||||
}
|
||||
|
||||
const jobId = submitResponse.data.id;
|
||||
console.log('✅ FASHN.ai job created:', jobId);
|
||||
|
||||
// Step 2: Poll for result
|
||||
console.log('⏳ Polling for result...');
|
||||
|
||||
const maxAttempts = 60;
|
||||
const pollInterval = 2000;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
attempts++;
|
||||
|
||||
console.log(`🔄 Checking status (attempt ${attempts}/${maxAttempts})...`);
|
||||
|
||||
try {
|
||||
const statusResponse = await axios.get(
|
||||
`${FASHN_API_URL}/${jobId}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${FASHN_API_KEY}`
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
console.log('📊 Status response:', JSON.stringify(statusResponse.data, null, 2));
|
||||
|
||||
if (statusResponse.data.status === 'completed') {
|
||||
const processingTime = Math.round((Date.now() - startTime) / 1000);
|
||||
|
||||
let imageUrl = null;
|
||||
|
||||
if (statusResponse.data.output && Array.isArray(statusResponse.data.output)) {
|
||||
imageUrl = statusResponse.data.output[0];
|
||||
} else if (statusResponse.data.result) {
|
||||
imageUrl = statusResponse.data.result;
|
||||
} else if (statusResponse.data.image) {
|
||||
imageUrl = statusResponse.data.image;
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
console.log(`✅ FASHN.ai completed in ${processingTime}s`);
|
||||
console.log(`🖼️ Result URL: ${imageUrl}`);
|
||||
return { imageUrl, processingTime };
|
||||
} else {
|
||||
console.error('❌ No image URL in completed response');
|
||||
console.error('Full response:', JSON.stringify(statusResponse.data, null, 2));
|
||||
throw new Error('Job completed but no image URL found');
|
||||
}
|
||||
}
|
||||
|
||||
if (statusResponse.data.status === 'failed') {
|
||||
throw new Error(`FASHN.ai job failed: ${statusResponse.data.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
console.log('⏳ Status:', statusResponse.data.status || 'processing');
|
||||
|
||||
} catch (pollError) {
|
||||
if (pollError.response) {
|
||||
console.log(`⚠️ Poll error - Status: ${pollError.response.status}`);
|
||||
console.log(`⚠️ Response:`, JSON.stringify(pollError.response.data, null, 2));
|
||||
|
||||
if (pollError.response.status === 404) {
|
||||
console.log('⏳ Job not ready yet (404)...');
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Poll error:', pollError.message);
|
||||
}
|
||||
|
||||
// Continue polling on errors
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('FASHN.ai processing timeout');
|
||||
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
console.error('❌ FASHN.ai API error:');
|
||||
console.error('Status:', error.response.status);
|
||||
console.error('Data:', JSON.stringify(error.response.data, null, 2));
|
||||
throw new Error(`FASHN.ai API error (${error.response.status}): ${error.response.data.message || error.response.statusText}`);
|
||||
} else if (error.request) {
|
||||
console.error('❌ No response from FASHN.ai');
|
||||
throw new Error('FASHN.ai did not respond');
|
||||
} else {
|
||||
console.error('❌ FASHN.ai error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Layer wardrobe items sequentially
|
||||
*/
|
||||
async function layerWardrobeItems(userImage, wardrobeItems, userId) {
|
||||
let currentImage = userImage;
|
||||
const steps = [];
|
||||
|
||||
console.log(`\n👕 Layering ${wardrobeItems.length} wardrobe items...`);
|
||||
|
||||
if (userImage.startsWith('blob:') || userImage.startsWith('data:')) {
|
||||
console.log('🔄 Converting user image to public URL...');
|
||||
currentImage = await uploadImageToStorage(userImage, userId, 'user_image');
|
||||
}
|
||||
|
||||
for (let i = 0; i < wardrobeItems.length; i++) {
|
||||
const item = wardrobeItems[i];
|
||||
console.log(`\n📦 Step ${i + 1}/${wardrobeItems.length}: Adding ${item.name}`);
|
||||
|
||||
try {
|
||||
const result = await applyGarmentWithFashn(currentImage, item.image_url);
|
||||
|
||||
currentImage = result.imageUrl;
|
||||
steps.push({
|
||||
step: i + 1,
|
||||
item: item.name,
|
||||
resultUrl: currentImage,
|
||||
processingTime: result.processingTime
|
||||
});
|
||||
|
||||
console.log(`✅ ${item.name} applied successfully`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to apply ${item.name}:`, error.message);
|
||||
throw new Error(`Failed to apply ${item.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ All wardrobe items applied!');
|
||||
return { finalImage: currentImage, steps };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process job in background
|
||||
*/
|
||||
async function processJob(jobId) {
|
||||
try {
|
||||
console.log(`\n🎬 ========================================`);
|
||||
console.log(`📋 Processing job: ${jobId}`);
|
||||
console.log(`🎬 ========================================`);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('generation_jobs')
|
||||
.update({ status: 'processing', progress: 10 })
|
||||
.eq('job_id', jobId);
|
||||
|
||||
jobsCache.set(jobId, { status: 'processing', progress: 10 });
|
||||
|
||||
const userImage = job.user_image;
|
||||
const wardrobeItems = job.products || [];
|
||||
|
||||
console.log('\n📦 Job Details:');
|
||||
console.log('User image:', userImage ? 'Yes' : 'No');
|
||||
console.log('Wardrobe items:', wardrobeItems.length);
|
||||
|
||||
await supabase
|
||||
.from('generation_jobs')
|
||||
.update({ progress: 20 })
|
||||
.eq('job_id', jobId);
|
||||
jobsCache.set(jobId, { status: 'processing', progress: 20 });
|
||||
|
||||
const { finalImage, steps } = await layerWardrobeItems(userImage, wardrobeItems, job.user_id);
|
||||
|
||||
const progressPerItem = 60 / wardrobeItems.length;
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const newProgress = 20 + Math.round((i + 1) * progressPerItem);
|
||||
await supabase
|
||||
.from('generation_jobs')
|
||||
.update({ progress: newProgress })
|
||||
.eq('job_id', jobId);
|
||||
jobsCache.set(jobId, { status: 'processing', progress: newProgress });
|
||||
}
|
||||
|
||||
const totalProcessingTime = steps.reduce((sum, step) => sum + step.processingTime, 0);
|
||||
|
||||
await supabase
|
||||
.from('generation_jobs')
|
||||
.update({
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
generated_image_url: finalImage,
|
||||
processing_time: totalProcessingTime,
|
||||
processing_steps: steps,
|
||||
completed_at: new Date().toISOString()
|
||||
})
|
||||
.eq('job_id', jobId);
|
||||
|
||||
jobsCache.set(jobId, {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
imageUrl: finalImage
|
||||
});
|
||||
|
||||
console.log(`\n✅ ========================================`);
|
||||
console.log(`✅ Job ${jobId} COMPLETED!`);
|
||||
console.log(`✅ ========================================`);
|
||||
console.log(`🖼️ Final image: ${finalImage}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n❌ Job ${jobId} FAILED!`);
|
||||
console.error(`❌ Error:`, error.message);
|
||||
|
||||
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
|
||||
// ============================================
|
||||
|
||||
app.post('/api/generate', async (req, res) => {
|
||||
try {
|
||||
const { userId, userImage, wardrobeItems, selectedPose, selectedBackground, selectedEffect } = req.body;
|
||||
|
||||
console.log('\n🆕 ========================================');
|
||||
console.log('🆕 NEW GENERATION REQUEST');
|
||||
console.log('🆕 ========================================');
|
||||
console.log('User:', userId);
|
||||
console.log('Wardrobe items:', wardrobeItems?.length || 0);
|
||||
|
||||
if (!wardrobeItems || wardrobeItems.length === 0) {
|
||||
return res.status(400).json({ success: false, error: 'At least one wardrobe item required' });
|
||||
}
|
||||
|
||||
if (!userImage) {
|
||||
return res.status(400).json({ success: false, error: 'User image required' });
|
||||
}
|
||||
|
||||
if (!FASHN_API_KEY) {
|
||||
return res.status(500).json({ success: false, error: 'FASHN_API_KEY not configured' });
|
||||
}
|
||||
|
||||
const jobId = uuidv4();
|
||||
console.log('Job ID:', jobId);
|
||||
|
||||
const productUrls = wardrobeItems.map(item => item.image_url);
|
||||
|
||||
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 error:', insertError);
|
||||
throw insertError;
|
||||
}
|
||||
|
||||
console.log('✅ Job created in database');
|
||||
|
||||
jobsCache.set(jobId, { status: 'queued', progress: 0 });
|
||||
|
||||
setImmediate(() => {
|
||||
processJob(jobId).catch(err => console.error('Processing error:', err));
|
||||
});
|
||||
|
||||
const estimatedTime = wardrobeItems.length * 15;
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
jobId,
|
||||
status: 'queued',
|
||||
message: 'Generation job created',
|
||||
wardrobeItemsCount: wardrobeItems.length,
|
||||
estimatedTime: `${estimatedTime}-${estimatedTime + 30} seconds`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Generate error:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/job-status/:jobId', async (req, res) => {
|
||||
try {
|
||||
const { jobId } = req.params;
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
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,
|
||||
processingSteps: job.processing_steps,
|
||||
error: job.error,
|
||||
createdAt: job.created_at,
|
||||
completedAt: job.completed_at,
|
||||
processingTime: job.processing_time
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Status error:', error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
const { error: supabaseError } = await supabase
|
||||
.from('generation_jobs')
|
||||
.select('job_id', { count: 'exact', head: true })
|
||||
.limit(1);
|
||||
|
||||
const supabaseStatus = supabaseError ? 'error' : 'connected';
|
||||
const fashnStatus = FASHN_API_KEY ? 'configured' : 'not_configured';
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
status: 'healthy',
|
||||
services: { supabase: supabaseStatus, fashn: fashnStatus },
|
||||
stats: { jobsInCache: jobsCache.size }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
res.status(503).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'Virtual Try-On Server with FASHN.ai',
|
||||
version: '2.0.0',
|
||||
database: 'Supabase',
|
||||
aiProvider: 'FASHN.ai'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ success: false, error: 'Endpoint not found' });
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('❌ Error:', err);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('🚀 Virtual Try-On Server with FASHN.ai - EXACT PRODUCTS VERSION');
|
||||
console.log('='.repeat(70));
|
||||
console.log(`📡 Server: http://localhost:${port}`);
|
||||
console.log(`💾 Database: Supabase`);
|
||||
console.log(`🤖 AI Provider: FASHN.ai`);
|
||||
console.log(`📊 Health: http://localhost:${port}/api/health`);
|
||||
console.log('='.repeat(70));
|
||||
|
||||
if (!FASHN_API_KEY) {
|
||||
console.log('\n⚠️ WARNING: FASHN_API_KEY not found!');
|
||||
} else {
|
||||
console.log('\n✅ FASHN API Key configured');
|
||||
}
|
||||
console.log('\n');
|
||||
});
|
||||
1643
package-lock.json
generated
Normal file
1643
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
25
package.json
Normal file
25
package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "virtual-tryon-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"start-dev": "nodemon index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"vercel-build": "echo \"Building for Vercel\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.76.1",
|
||||
"axios": "^1.12.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"replicate": "^1.3.0"
|
||||
}
|
||||
}
|
||||
15
vercel.json
Normal file
15
vercel.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "index.js",
|
||||
"use": "@vercel/node"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue