1976 lines
No EOL
54 KiB
JavaScript
1976 lines
No EOL
54 KiB
JavaScript
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; |