virtual-tryon/index.js

1976 lines
54 KiB
JavaScript
Raw Normal View History

2025-10-30 22:44:47 +08:00
const express = require('express');
const app = express();
const cors = require('cors');
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const axios = require('axios');
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');
const FASHN_API_KEY = process.env.FASHN_API_KEY;
const FASHN_API_URL = 'https://api.fashn.ai/v1';
// ============================================
// SIMPLE TEST ENDPOINT (1 GARMENT)
// ============================================
app.post('/api/test-fashn', async (req, res) => {
try {
const { modelImage, garmentImage } = req.body;
console.log('\n🧪 TESTING FASHN.AI');
console.log('Model:', modelImage?.substring(0, 50));
console.log('Garment:', garmentImage?.substring(0, 50));
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
// Step 1: Submit job
console.log('\n📤 Submitting to FASHN.ai...');
const submitResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: modelImage,
garment_image: garmentImage,
category: 'auto'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
console.log('✅ Response:', JSON.stringify(submitResponse.data, null, 2));
if (!submitResponse.data.id) {
return res.json({
success: false,
error: 'No job ID received',
response: submitResponse.data
});
}
const jobId = submitResponse.data.id;
console.log('✅ Job created:', jobId);
// Step 2: Poll for result
console.log('\n⏳ Polling for result...');
const maxAttempts = 30; // 1 minute max
const pollInterval = 2000; // 2 seconds
for (let i = 0; i < maxAttempts; i++) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
console.log(`🔄 Attempt ${i + 1}/${maxAttempts}`);
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${jobId}`,
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`
},
timeout: 10000
}
);
console.log('📊 Status:', JSON.stringify(statusResponse.data, null, 2));
if (statusResponse.data.status === 'completed') {
let imageUrl = statusResponse.data.output?.[0] ||
statusResponse.data.result ||
statusResponse.data.image;
console.log('✅ COMPLETED!');
console.log('🖼️ Image:', imageUrl);
return res.json({
success: true,
message: 'FASHN.ai works!',
jobId: jobId,
imageUrl: imageUrl,
attempts: i + 1
});
}
if (statusResponse.data.status === 'failed') {
console.log('❌ FAILED:', statusResponse.data.error);
return res.json({
success: false,
error: 'FASHN.ai job failed',
details: statusResponse.data.error
});
}
console.log('⏳ Still processing...');
} catch (pollError) {
if (pollError.response?.status === 404) {
console.log('⏳ Not ready yet (404)');
continue;
}
console.log('⚠️ Poll error:', pollError.response?.status, pollError.message);
continue;
}
}
// Timeout
return res.json({
success: false,
error: 'Timeout - FASHN.ai took too long',
jobId: jobId
});
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
// ============================================
// TEST WITH 2 GARMENTS (SHIRT + PANT)
// ============================================
app.post('/api/test-two-garments', async (req, res) => {
try {
const { modelImage, shirtImage, pantImage } = req.body;
console.log('\n🧪 TESTING 2 GARMENTS (SHIRT + PANT)');
console.log('Model:', modelImage?.substring(0, 50));
console.log('Shirt:', shirtImage?.substring(0, 50));
console.log('Pant:', pantImage?.substring(0, 50));
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
// STEP 1: Apply SHIRT first
console.log('\n👕 STEP 1: Applying SHIRT...');
const shirtResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: modelImage,
garment_image: shirtImage,
category: 'tops'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const shirtJobId = shirtResponse.data.id;
console.log('✅ Shirt job created:', shirtJobId);
// Poll for shirt result
console.log('⏳ Waiting for shirt...');
let shirtResult = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${shirtJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Shirt attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
shirtResult = statusResponse.data.output?.[0];
console.log('✅ Shirt applied!');
console.log('🖼️ Intermediate image:', shirtResult);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'shirt',
error: 'Shirt application failed',
details: statusResponse.data.error
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Shirt poll error:', pollError.message);
}
}
}
if (!shirtResult) {
return res.json({
success: false,
step: 'shirt',
error: 'Shirt application timeout'
});
}
// STEP 2: Apply PANT on top of shirt result
console.log('\n👖 STEP 2: Applying PANT...');
const pantResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: shirtResult,
garment_image: pantImage,
category: 'bottoms'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const pantJobId = pantResponse.data.id;
console.log('✅ Pant job created:', pantJobId);
// Poll for pant result
console.log('⏳ Waiting for pant...');
let pantResult = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${pantJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Pant attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
pantResult = statusResponse.data.output?.[0];
console.log('✅ Pant applied!');
console.log('🖼️ Final image:', pantResult);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'pant',
error: 'Pant application failed',
details: statusResponse.data.error,
shirtResult: shirtResult
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Pant poll error:', pollError.message);
}
}
}
if (!pantResult) {
return res.json({
success: false,
step: 'pant',
error: 'Pant application timeout',
shirtResult: shirtResult
});
}
// Return final result
console.log('\n✅ PROCESS COMPLETED!');
return res.json({
success: true,
message: 'Both garments applied successfully!',
steps: {
shirt: {
jobId: shirtJobId,
result: shirtResult
},
pant: {
jobId: pantJobId,
result: pantResult
}
},
finalImage: pantResult
});
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
// ============================================
// VIRTUAL TRY-ON (FRONTEND INTEGRATION)
// ============================================
app.post('/api/virtual-tryon', async (req, res) => {
try {
const { userId, userImage, wardrobeItems, selectedPose, selectedBackground, selectedEffect } = req.body;
console.log('\n🎨 VIRTUAL TRY-ON REQUEST');
console.log('User ID:', userId);
console.log('Wardrobe Items:', wardrobeItems?.length, 'items');
console.log('Pose:', selectedPose);
console.log('Background:', selectedBackground);
console.log('Effect:', selectedEffect);
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
if (!userImage || !wardrobeItems || wardrobeItems.length === 0) {
return res.status(400).json({
success: false,
error: 'userImage and wardrobeItems are required'
});
}
// Separate tops and bottoms
const tops = wardrobeItems.filter(item => item.typecategory === 'tops');
const bottoms = wardrobeItems.filter(item => item.typecategory === 'bottoms');
console.log('👕 Tops:', tops.length);
console.log('👖 Bottoms:', bottoms.length);
// Build the prompt for face-to-model
let modelPrompt = '';
if (selectedEffect) {
// If selectedEffect is an object, get the description
const effectText = typeof selectedEffect === 'string'
? selectedEffect
: selectedEffect?.description || '';
modelPrompt += effectText;
}
if (selectedPose) {
// If selectedPose is an object, get the description
const poseText = typeof selectedPose === 'string'
? selectedPose
: selectedPose?.description || '';
modelPrompt += modelPrompt ? `, ${poseText}` : poseText;
}
console.log('🎭 Model Prompt:', modelPrompt || 'auto-inferred');
// STEP 1: Create avatar from face
console.log('\n👤 STEP 1: Creating avatar from face...');
const faceToModelResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'face-to-model',
inputs: {
face_image: userImage,
...(modelPrompt && { prompt: modelPrompt })
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const faceJobId = faceToModelResponse.data.id;
console.log('✅ Face-to-model job created:', faceJobId);
// Poll for avatar creation
console.log('⏳ Waiting for avatar generation...');
let currentImage = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${faceJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Avatar attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
currentImage = statusResponse.data.output?.[0];
console.log('✅ Avatar created from face!');
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'face-to-model',
error: 'Avatar creation failed',
details: statusResponse.data.error
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Avatar poll error:', pollError.message);
}
}
}
if (!currentImage) {
return res.json({
success: false,
step: 'face-to-model',
error: 'Avatar creation timeout'
});
}
const results = {
avatar: currentImage
};
// STEP 2: Apply tops if available
if (tops.length > 0) {
console.log('\n👕 STEP 2: Applying tops...');
for (const top of tops) {
console.log(` Applying: ${top.name}`);
const topResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: currentImage,
garment_image: top.image_url,
category: 'tops'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const topJobId = topResponse.data.id;
// Poll for result
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${topJobId}`,
{ headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` } }
);
if (statusResponse.data.status === 'completed') {
currentImage = statusResponse.data.output?.[0];
console.log(`${top.name} applied!`);
break;
}
if (statusResponse.data.status === 'failed') {
console.log(`${top.name} failed, skipping`);
break;
}
} catch (pollError) {
// Continue polling
}
}
}
results.withTops = currentImage;
}
// STEP 3: Apply bottoms if available
if (bottoms.length > 0) {
console.log('\n👖 STEP 3: Applying bottoms...');
for (const bottom of bottoms) {
console.log(` Applying: ${bottom.name}`);
const bottomResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: currentImage,
garment_image: bottom.image_url,
category: 'bottoms'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const bottomJobId = bottomResponse.data.id;
// Poll for result
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${bottomJobId}`,
{ headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` } }
);
if (statusResponse.data.status === 'completed') {
currentImage = statusResponse.data.output?.[0];
console.log(`${bottom.name} applied!`);
break;
}
if (statusResponse.data.status === 'failed') {
console.log(`${bottom.name} failed, skipping`);
break;
}
} catch (pollError) {
// Continue polling
}
}
}
results.withBottoms = currentImage;
}
// STEP 4: Change background if provided
if (selectedBackground) {
console.log('\n🌆 STEP 4: Changing background...');
// If selectedBackground is an object, get the description
const backgroundText = typeof selectedBackground === 'string'
? selectedBackground
: selectedBackground?.description || '';
if (!backgroundText) {
console.log(' ⚠️ No background description provided, skipping');
} else {
const backgroundResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'background-change',
inputs: {
image: currentImage,
prompt: backgroundText
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const bgJobId = backgroundResponse.data.id;
// Poll for result
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${bgJobId}`,
{ headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` } }
);
if (statusResponse.data.status === 'completed') {
currentImage = statusResponse.data.output?.[0];
console.log(' ✅ Background changed!');
break;
}
if (statusResponse.data.status === 'failed') {
console.log(' ⚠️ Background change failed, using previous result');
break;
}
} catch (pollError) {
// Continue polling
}
}
results.withBackground = currentImage;
}
}
// Return final result
console.log('\n✅ VIRTUAL TRY-ON COMPLETE!');
console.log('🖼️ Final Image:', currentImage);
return res.json({
success: true,
message: 'Virtual try-on completed successfully!',
userId: userId,
generatedImageUrl: currentImage,
results: results,
appliedItems: {
tops: tops.map(t => t.name),
bottoms: bottoms.map(b => b.name)
}
});
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
// ============================================
// BACKGROUND CHANGE
// ============================================
app.post('/api/change-background', async (req, res) => {
try {
const { image, prompt } = req.body;
console.log('\n🌆 CHANGING BACKGROUND');
console.log('Image:', image?.substring(0, 50));
console.log('Background Prompt:', prompt);
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
if (!image || !prompt) {
return res.status(400).json({
success: false,
error: 'image and prompt are required',
example: {
image: 'https://cdn.fashn.ai/xyz/output_0.png',
prompt: 'city street with buildings, urban background, busy downtown'
}
});
}
// Submit background change job
console.log('\n📤 Submitting background change request...');
const changeResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'background-change',
inputs: {
image: image,
prompt: prompt
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
console.log('✅ Response:', JSON.stringify(changeResponse.data, null, 2));
if (!changeResponse.data.id) {
return res.json({
success: false,
error: 'No job ID received',
response: changeResponse.data
});
}
const jobId = changeResponse.data.id;
console.log('✅ Background change job created:', jobId);
// Poll for result
console.log('\n⏳ Polling for background change...');
const maxAttempts = 30;
const pollInterval = 2000;
for (let i = 0; i < maxAttempts; i++) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
console.log(`🔄 Attempt ${i + 1}/${maxAttempts}`);
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${jobId}`,
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`
},
timeout: 10000
}
);
console.log('📊 Status:', JSON.stringify(statusResponse.data, null, 2));
if (statusResponse.data.status === 'completed') {
let imageUrl = statusResponse.data.output?.[0] ||
statusResponse.data.result ||
statusResponse.data.image;
console.log('✅ BACKGROUND CHANGED!');
console.log('🖼️ New Image:', imageUrl);
return res.json({
success: true,
message: 'Background changed successfully!',
jobId: jobId,
imageUrl: imageUrl,
prompt: prompt,
attempts: i + 1
});
}
if (statusResponse.data.status === 'failed') {
console.log('❌ FAILED:', statusResponse.data.error);
return res.json({
success: false,
error: 'Background change failed',
details: statusResponse.data.error
});
}
console.log('⏳ Still processing...');
} catch (pollError) {
if (pollError.response?.status === 404) {
console.log('⏳ Not ready yet (404)');
continue;
}
console.log('⚠️ Poll error:', pollError.response?.status, pollError.message);
continue;
}
}
// Timeout
return res.json({
success: false,
error: 'Timeout - Background change took too long',
jobId: jobId
});
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
// ============================================
// FACE TO MODEL (CREATE AVATAR FROM FACE)
// ============================================
app.post('/api/face-to-model', async (req, res) => {
try {
const { faceImage, prompt, outputFormat } = req.body;
console.log('\n👤 CREATING MODEL FROM FACE IMAGE');
console.log('Face Image:', faceImage?.substring(0, 50));
console.log('Prompt:', prompt || 'None (auto-inferred)');
console.log('Output Format:', outputFormat || 'png');
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
if (!faceImage) {
return res.status(400).json({
success: false,
error: 'faceImage is required',
example: {
faceImage: 'https://i.ibb.co/C3DCLgSD/fashn-try-on-1761233346758.png',
prompt: 'athletic build, confident',
outputFormat: 'png'
}
});
}
// Submit face-to-model job
console.log('\n📤 Submitting face-to-model request...');
const requestBody = {
model_name: 'face-to-model',
inputs: {
face_image: faceImage
}
};
// Add optional parameters
if (prompt) {
requestBody.inputs.prompt = prompt;
}
if (outputFormat) {
requestBody.inputs.output_format = outputFormat;
}
const createResponse = await axios.post(
`${FASHN_API_URL}/run`,
requestBody,
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
console.log('✅ Response:', JSON.stringify(createResponse.data, null, 2));
if (!createResponse.data.id) {
return res.json({
success: false,
error: 'No job ID received',
response: createResponse.data
});
}
const jobId = createResponse.data.id;
console.log('✅ Face-to-model job created:', jobId);
// Poll for result
console.log('\n⏳ Polling for avatar generation...');
const maxAttempts = 30; // 1 minute max
const pollInterval = 2000; // 2 seconds
for (let i = 0; i < maxAttempts; i++) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
console.log(`🔄 Attempt ${i + 1}/${maxAttempts}`);
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${jobId}`,
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`
},
timeout: 10000
}
);
console.log('📊 Status:', JSON.stringify(statusResponse.data, null, 2));
if (statusResponse.data.status === 'completed') {
let avatarUrl = statusResponse.data.output?.[0] ||
statusResponse.data.result ||
statusResponse.data.image;
console.log('✅ AVATAR CREATED FROM FACE!');
console.log('🖼️ Avatar Image:', avatarUrl);
return res.json({
success: true,
message: 'Avatar created from face successfully!',
jobId: jobId,
avatarImage: avatarUrl,
faceImage: faceImage,
prompt: prompt || 'auto-inferred',
attempts: i + 1,
note: 'You can now use this avatarImage for try-on'
});
}
if (statusResponse.data.status === 'failed') {
console.log('❌ FAILED:', statusResponse.data.error);
return res.json({
success: false,
error: 'Face-to-model conversion failed',
details: statusResponse.data.error
});
}
console.log('⏳ Still processing...');
} catch (pollError) {
if (pollError.response?.status === 404) {
console.log('⏳ Not ready yet (404)');
continue;
}
console.log('⚠️ Poll error:', pollError.response?.status, pollError.message);
continue;
}
}
// Timeout
return res.json({
success: false,
error: 'Timeout - Face-to-model took too long',
jobId: jobId
});
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
// ============================================
// FACE TO MODEL + APPLY GARMENTS (COMPLETE WORKFLOW)
// ============================================
app.post('/api/face-to-model-and-tryon', async (req, res) => {
try {
const { faceImage, prompt, shirtImage, pantImage, backgroundPrompt } = req.body;
console.log('\n🎯 FACE-TO-MODEL + GARMENT TRY-ON WORKFLOW');
console.log('Face Image:', faceImage?.substring(0, 50));
console.log('Prompt:', prompt || 'auto-inferred');
console.log('Shirt:', shirtImage?.substring(0, 50));
console.log('Pant:', pantImage?.substring(0, 50));
console.log('Background:', backgroundPrompt || 'None');
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
if (!faceImage || !shirtImage || !pantImage) {
return res.status(400).json({
success: false,
error: 'faceImage, shirtImage, and pantImage are required'
});
}
// STEP 1: Create avatar from face
console.log('\n👤 STEP 1: Creating avatar from face...');
const faceToModelResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'face-to-model',
inputs: {
face_image: faceImage,
...(prompt && { prompt })
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const faceJobId = faceToModelResponse.data.id;
console.log('✅ Face-to-model job created:', faceJobId);
// Poll for avatar creation
console.log('⏳ Waiting for avatar generation...');
let avatarImage = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${faceJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Avatar attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
avatarImage = statusResponse.data.output?.[0];
console.log('✅ Avatar created from face!');
console.log('🖼️ Avatar:', avatarImage);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'face-to-model',
error: 'Avatar creation failed',
details: statusResponse.data.error
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Avatar poll error:', pollError.message);
}
}
}
if (!avatarImage) {
return res.json({
success: false,
step: 'face-to-model',
error: 'Avatar creation timeout'
});
}
// STEP 2: Apply SHIRT to avatar
console.log('\n👕 STEP 2: Applying SHIRT to avatar...');
const shirtResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: avatarImage,
garment_image: shirtImage,
category: 'tops'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const shirtJobId = shirtResponse.data.id;
console.log('✅ Shirt job created:', shirtJobId);
// Poll for shirt result
console.log('⏳ Waiting for shirt...');
let shirtResult = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${shirtJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Shirt attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
shirtResult = statusResponse.data.output?.[0];
console.log('✅ Shirt applied!');
console.log('🖼️ With shirt:', shirtResult);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'shirt',
error: 'Shirt application failed',
details: statusResponse.data.error,
avatarImage: avatarImage
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Shirt poll error:', pollError.message);
}
}
}
if (!shirtResult) {
return res.json({
success: false,
step: 'shirt',
error: 'Shirt application timeout',
avatarImage: avatarImage
});
}
// STEP 3: Apply PANT to shirt result
console.log('\n👖 STEP 3: Applying PANT...');
const pantResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: shirtResult,
garment_image: pantImage,
category: 'bottoms'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const pantJobId = pantResponse.data.id;
console.log('✅ Pant job created:', pantJobId);
// Poll for pant result
console.log('⏳ Waiting for pant...');
let pantResult = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${pantJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Pant attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
pantResult = statusResponse.data.output?.[0];
console.log('✅ Pant applied!');
console.log('🖼️ Final result:', pantResult);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'pant',
error: 'Pant application failed',
details: statusResponse.data.error,
avatarImage: avatarImage,
shirtResult: shirtResult
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Pant poll error:', pollError.message);
}
}
}
if (!pantResult) {
return res.json({
success: false,
step: 'pant',
error: 'Pant application timeout',
avatarImage: avatarImage,
shirtResult: shirtResult
});
}
// STEP 4: Change background if provided
let finalResult = pantResult;
let backgroundJobId = null;
if (backgroundPrompt) {
console.log('\n🌆 STEP 4: Changing BACKGROUND...');
const backgroundResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'background-change',
inputs: {
image: pantResult,
prompt: backgroundPrompt
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
backgroundJobId = backgroundResponse.data.id;
console.log('✅ Background job created:', backgroundJobId);
// Poll for background result
console.log('⏳ Waiting for background change...');
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${backgroundJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Background attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
finalResult = statusResponse.data.output?.[0];
console.log('✅ Background changed!');
console.log('🖼️ Final image:', finalResult);
break;
}
if (statusResponse.data.status === 'failed') {
console.log('⚠️ Background change failed, using garments-only result');
console.log('Error:', statusResponse.data.error);
// Don't fail the whole request, just skip background change
break;
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Background poll error:', pollError.message);
}
}
}
}
// Return final result
console.log('\n✅ COMPLETE FACE-TO-MODEL + TRY-ON FINISHED!');
const response = {
success: true,
message: backgroundPrompt
? 'Avatar created, garments applied, and background changed!'
: 'Avatar created from face and garments applied!',
steps: {
faceToModel: {
jobId: faceJobId,
result: avatarImage,
prompt: prompt || 'auto-inferred'
},
shirt: {
jobId: shirtJobId,
result: shirtResult
},
pant: {
jobId: pantJobId,
result: pantResult
}
},
finalImage: finalResult
};
if (backgroundPrompt && backgroundJobId) {
response.steps.background = {
jobId: backgroundJobId,
result: finalResult,
prompt: backgroundPrompt
};
}
return res.json(response);
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
// ============================================
// MODEL CREATION ONLY (TEST POSES)
// ============================================
app.post('/api/create-model', async (req, res) => {
try {
const { prompt } = req.body;
console.log('\n🎭 CREATING MODEL WITH FASHN.AI');
console.log('Prompt:', prompt);
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
if (!prompt) {
return res.status(400).json({
success: false,
error: 'Prompt is required',
examples: [
'male fashion model, athletic build, power pose with hands on hips, confident stance, full body, white background',
'female fashion model, elegant, sitting on chair with legs crossed, professional, studio lighting',
'male model, tall and slim, walking pose, one leg forward, dynamic movement, urban background'
]
});
}
// Submit model creation job
console.log('\n📤 Submitting model creation request...');
const createResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'model-create',
inputs: {
prompt: prompt
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
console.log('✅ Response:', JSON.stringify(createResponse.data, null, 2));
if (!createResponse.data.id) {
return res.json({
success: false,
error: 'No job ID received',
response: createResponse.data
});
}
const jobId = createResponse.data.id;
console.log('✅ Model creation job created:', jobId);
// Poll for result
console.log('\n⏳ Polling for model generation...');
const maxAttempts = 30; // 1 minute max
const pollInterval = 2000; // 2 seconds
for (let i = 0; i < maxAttempts; i++) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
console.log(`🔄 Attempt ${i + 1}/${maxAttempts}`);
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${jobId}`,
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`
},
timeout: 10000
}
);
console.log('📊 Status:', JSON.stringify(statusResponse.data, null, 2));
if (statusResponse.data.status === 'completed') {
let imageUrl = statusResponse.data.output?.[0] ||
statusResponse.data.result ||
statusResponse.data.image;
console.log('✅ MODEL CREATED!');
console.log('🖼️ Model Image:', imageUrl);
return res.json({
success: true,
message: 'Model created successfully!',
jobId: jobId,
modelImage: imageUrl,
prompt: prompt,
attempts: i + 1,
note: 'You can now use this modelImage URL for try-on'
});
}
if (statusResponse.data.status === 'failed') {
console.log('❌ FAILED:', statusResponse.data.error);
return res.json({
success: false,
error: 'Model creation failed',
details: statusResponse.data.error
});
}
console.log('⏳ Still processing...');
} catch (pollError) {
if (pollError.response?.status === 404) {
console.log('⏳ Not ready yet (404)');
continue;
}
console.log('⚠️ Poll error:', pollError.response?.status, pollError.message);
continue;
}
}
// Timeout
return res.json({
success: false,
error: 'Timeout - Model creation took too long',
jobId: jobId
});
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
// ============================================
// CREATE MODEL WITH POSE + APPLY GARMENTS
// ============================================
app.post('/api/create-model-and-tryon', async (req, res) => {
try {
const { posePrompt, shirtImage, pantImage, modelDescription } = req.body;
console.log('\n🧪 CREATE MODEL WITH POSE + APPLY GARMENTS');
console.log('Pose Prompt:', posePrompt);
console.log('Model Description:', modelDescription || 'Default model');
console.log('Shirt:', shirtImage?.substring(0, 50));
console.log('Pant:', pantImage?.substring(0, 50));
if (!FASHN_API_KEY) {
return res.status(500).json({
success: false,
error: 'FASHN_API_KEY not set in .env'
});
}
// STEP 1: Create model with specific pose
console.log('\n🎭 STEP 1: Creating model with pose...');
const fullPrompt = modelDescription
? `${modelDescription}, ${posePrompt}`
: posePrompt;
const modelCreateResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'model-create',
inputs: {
prompt: fullPrompt
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const modelJobId = modelCreateResponse.data.id;
console.log('✅ Model creation job created:', modelJobId);
// Poll for model creation result
console.log('⏳ Waiting for model generation...');
let generatedModel = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${modelJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Model attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
generatedModel = statusResponse.data.output?.[0];
console.log('✅ Model created with pose!');
console.log('🖼️ Generated model:', generatedModel);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'model-create',
error: 'Model creation failed',
details: statusResponse.data.error
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Model creation poll error:', pollError.message);
}
}
}
if (!generatedModel) {
return res.json({
success: false,
step: 'model-create',
error: 'Model creation timeout'
});
}
// STEP 2: Apply SHIRT to the generated model
console.log('\n👕 STEP 2: Applying SHIRT to generated model...');
const shirtResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: generatedModel,
garment_image: shirtImage,
category: 'tops'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const shirtJobId = shirtResponse.data.id;
console.log('✅ Shirt job created:', shirtJobId);
// Poll for shirt result
console.log('⏳ Waiting for shirt...');
let shirtResult = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${shirtJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Shirt attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
shirtResult = statusResponse.data.output?.[0];
console.log('✅ Shirt applied!');
console.log('🖼️ With shirt:', shirtResult);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'shirt',
error: 'Shirt application failed',
details: statusResponse.data.error,
generatedModel: generatedModel
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Shirt poll error:', pollError.message);
}
}
}
if (!shirtResult) {
return res.json({
success: false,
step: 'shirt',
error: 'Shirt application timeout',
generatedModel: generatedModel
});
}
// STEP 3: Apply PANT to the shirt result
console.log('\n👖 STEP 3: Applying PANT...');
const pantResponse = await axios.post(
`${FASHN_API_URL}/run`,
{
model_name: 'tryon-v1.6',
inputs: {
model_image: shirtResult,
garment_image: pantImage,
category: 'bottoms'
}
},
{
headers: {
'Authorization': `Bearer ${FASHN_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
const pantJobId = pantResponse.data.id;
console.log('✅ Pant job created:', pantJobId);
// Poll for pant result
console.log('⏳ Waiting for pant...');
let pantResult = null;
for (let i = 0; i < 30; i++) {
await new Promise(resolve => setTimeout(resolve, 2000));
try {
const statusResponse = await axios.get(
`${FASHN_API_URL}/status/${pantJobId}`,
{
headers: { 'Authorization': `Bearer ${FASHN_API_KEY}` },
timeout: 10000
}
);
console.log(`🔄 Pant attempt ${i + 1}/30 - Status: ${statusResponse.data.status}`);
if (statusResponse.data.status === 'completed') {
pantResult = statusResponse.data.output?.[0];
console.log('✅ Pant applied!');
console.log('🖼️ Final result:', pantResult);
break;
}
if (statusResponse.data.status === 'failed') {
return res.json({
success: false,
step: 'pant',
error: 'Pant application failed',
details: statusResponse.data.error,
generatedModel: generatedModel,
shirtResult: shirtResult
});
}
} catch (pollError) {
if (pollError.response?.status !== 404) {
console.log('⚠️ Pant poll error:', pollError.message);
}
}
}
if (!pantResult) {
return res.json({
success: false,
step: 'pant',
error: 'Pant application timeout',
generatedModel: generatedModel,
shirtResult: shirtResult
});
}
// Return final result
console.log('\n✅ COMPLETE WORKFLOW FINISHED!');
return res.json({
success: true,
message: 'Model created with pose and garments applied!',
steps: {
modelCreate: {
jobId: modelJobId,
result: generatedModel,
prompt: fullPrompt
},
shirt: {
jobId: shirtJobId,
result: shirtResult
},
pant: {
jobId: pantJobId,
result: pantResult
}
},
finalImage: pantResult
});
} catch (error) {
console.error('❌ ERROR:', error.response?.data || error.message);
res.status(500).json({
success: false,
error: error.message,
details: error.response?.data
});
}
});
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
});
}
});
// ============================================
// ROOT
// ============================================
app.get('/', (req, res) => {
res.json({
message: 'FASHN.ai Test Server',
endpoints: {
virtualTryon: 'POST /api/virtual-tryon',
changeBackground: 'POST /api/change-background',
faceToModel: 'POST /api/face-to-model',
faceToModelAndTryon: 'POST /api/face-to-model-and-tryon',
createModel: 'POST /api/create-model',
singleGarment: 'POST /api/test-fashn',
twoGarments: 'POST /api/test-two-garments',
createModelAndTryon: 'POST /api/create-model-and-tryon'
},
fashnApiKey: FASHN_API_KEY ? '✅ Configured' : '❌ Missing'
});
});
// ============================================
// START
// ============================================
const PORT = 5235;
app.listen(PORT, () => {
console.log('\n========================================');
console.log('🧪 FASHN.ai Test Server');
console.log('========================================');
console.log(`Server: http://localhost:${PORT}`);
console.log(`FASHN API Key: ${FASHN_API_KEY ? '✅ Set' : '❌ Not set'}`);
console.log('========================================\n');
});
// ============================================
// START SERVER (Local Development)
// ============================================
if (process.env.NODE_ENV !== 'production') {
const PORT = process.env.PORT || 5235;
app.listen(PORT, () => {
console.log('\n========================================');
console.log('🧪 FASHN.ai Test Server');
console.log('========================================');
console.log(`Server: http://localhost:${PORT}`);
console.log(`FASHN API Key: ${FASHN_API_KEY ? '✅ Set' : '❌ Not set'}`);
console.log('========================================\n');
});
}
// ============================================
// EXPORT FOR VERCEL (Serverless)
// ============================================
module.exports = app;