Merge branch 'dev'
This commit is contained in:
commit
2ba0e86aa7
31
.mcp.json
31
.mcp.json
|
|
@ -1,14 +1,39 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"nano-banana-mcp": {
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"nano-banana-mcp"
|
||||
"-y",
|
||||
"@upstash/context7-mcp",
|
||||
"--api-key",
|
||||
"ctx7sk-48cb1995-935a-4cc5-b9b0-535d600ea5e6"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"brave-search": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-brave-search"
|
||||
],
|
||||
"env": {
|
||||
"BRAVE_API_KEY": "BSAcRGGikEzY4B2j3NZ8Qy5NYh9l4HZ"
|
||||
}
|
||||
},
|
||||
"browsermcp": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@browsermcp/mcp@latest"],
|
||||
"env": {}
|
||||
},
|
||||
"nano-banana-mcp": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["nano-banana-mcp"],
|
||||
"env": {
|
||||
"GEMINI_API_KEY": "AIzaSyAWAy39gqmnrYA0kUC3Vh8uadu-WlqJuO4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 7.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.4 MiB |
Binary file not shown.
|
|
@ -0,0 +1,18 @@
|
|||
/projects/my-projects/Magic-Building/src/server/app.ts:1
|
||||
import express, { Application } from 'express';
|
||||
^
|
||||
|
||||
|
||||
ReferenceError: Cannot access 'appConfig' before initialization
|
||||
at Object.appConfig (/projects/my-projects/Magic-Building/src/server/app.ts:1:1)
|
||||
at Object.get [as appConfig] (/projects/my-projects/Magic-Building/src/server/app.ts:2:661)
|
||||
at multer (/projects/my-projects/Magic-Building/src/server/middleware/upload.ts:30:15)
|
||||
at Object.<anonymous> (/projects/my-projects/Magic-Building/src/server/middleware/upload.ts:40:35)
|
||||
at Module._compile (node:internal/modules/cjs/loader:1546:14)
|
||||
at Object.transformer (/projects/my-projects/Magic-Building/node_modules/.pnpm/tsx@4.20.5/node_modules/tsx/dist/register-D46fvsV_.cjs:3:1104)
|
||||
at Module.load (node:internal/modules/cjs/loader:1318:32)
|
||||
at Function._load (node:internal/modules/cjs/loader:1128:12)
|
||||
at TracingChannel.traceSync (node:diagnostics_channel:315:14)
|
||||
at wrapModuleLoad (node:internal/modules/cjs/loader:218:24)
|
||||
|
||||
Node.js v22.11.0
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
[1;33m⚠️ jq not found - JSON response parsing will be limited[0m
|
||||
[0;34mℹ️ 🚀 Starting Magic Building API Test Suite[0m
|
||||
[0;34mℹ️ Server URL: http://localhost:3000[0m
|
||||
[0;34mℹ️ Test log: ./logs/test-api-20250915_010051.log[0m
|
||||
[0;34mℹ️ Testing health check endpoint...[0m
|
||||
[0;31m❌ Health check failed (HTTP 000)[0m
|
||||
[0;34mℹ️ Testing API info endpoint...[0m
|
||||
[0;31m❌ API info endpoint failed (HTTP 000)[0m
|
||||
[0;34mℹ️ Testing text-to-image generation...[0m
|
||||
[0;31m❌ Text-to-image generation failed (HTTP 000)[0m
|
||||
2025-09-15 01:00:51 - Error response:
|
||||
[0;34mℹ️ Testing validation error handling...[0m
|
||||
[0;31m❌ Missing prompt validation failed (HTTP 000)[0m
|
||||
[0;31m❌ Missing filename validation failed (HTTP 000)[0m
|
||||
[1;33m⚠️ Filename sanitization test had unexpected result (HTTP 000)[0m
|
||||
[0;34mℹ️ Testing 404 error handling...[0m
|
||||
[0;31m❌ 404 error handling failed (HTTP 000)[0m
|
||||
[0;34mℹ️ Creating test reference image...[0m
|
||||
[1;33m⚠️ ImageMagick not available, skipping reference image test[0m
|
||||
[0;34mℹ️ Testing concurrent requests (performance test)...[0m
|
||||
2025-09-15 01:00:51 - Concurrent request 1 failed (HTTP 000)
|
||||
2025-09-15 01:00:51 - Concurrent request 3 failed (HTTP 000)
|
||||
2025-09-15 01:00:51 - Concurrent request 2 failed (HTTP 000)
|
||||
[0;32m✅ Concurrent requests test completed[0m
|
||||
[0;34mℹ️ 📊 Test Results Summary[0m
|
||||
[0;32m✅ Passed: 3/6 tests[0m
|
||||
[1;33m⚠️ ⚠️ Some tests failed. Check the logs for details.[0m
|
||||
[0;34mℹ️ Generated images are in: src/results/[0m
|
||||
[0;34mℹ️ Test logs saved to: ./logs/test-api-20250915_010051.log[0m
|
||||
15
package.json
15
package.json
|
|
@ -5,7 +5,12 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"ttm": "tsx src/generation/text-to-image.ts",
|
||||
"ttt": "tsx src/generation/text-to-text.ts"
|
||||
"ttt": "tsx src/generation/text-to-text.ts",
|
||||
"server": "tsx src/server/server.ts",
|
||||
"server:dev": "tsx --watch src/server/server.ts",
|
||||
"server:log": "tsx src/server/server.ts > logs/server.log 2>&1",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
@ -13,10 +18,16 @@
|
|||
"packageManager": "pnpm@10.11.0",
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.17.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.2",
|
||||
"mime": "^4.1.0"
|
||||
"express": "^5.1.0",
|
||||
"mime": "^4.1.0",
|
||||
"multer": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
|
|
|
|||
785
pnpm-lock.yaml
785
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -78,7 +78,7 @@ async function main() {
|
|||
}
|
||||
console.log('Image generation complete!');
|
||||
} catch (error) {
|
||||
console.error('Primary model failed:', error.message || error);
|
||||
console.error('Primary model failed:', error instanceof Error ? error.message : error);
|
||||
|
||||
// Try fallback model (Imagen 4)
|
||||
console.log('Trying fallback model (Imagen 4)...');
|
||||
|
|
@ -111,7 +111,7 @@ async function main() {
|
|||
}
|
||||
} catch (altError) {
|
||||
console.error('Both models failed. Please try again later.');
|
||||
console.error('Fallback error:', altError.message || altError);
|
||||
console.error('Fallback error:', altError instanceof Error ? altError.message : altError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
|
|
@ -0,0 +1,94 @@
|
|||
import express, { Application } from 'express';
|
||||
import cors from 'cors';
|
||||
import { config } from 'dotenv';
|
||||
import { Config } from './types/api';
|
||||
import { generateRouter } from './routes/generate';
|
||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
||||
|
||||
// Load environment variables
|
||||
config();
|
||||
|
||||
// Application configuration
|
||||
export const appConfig: Config = {
|
||||
port: parseInt(process.env.PORT || '3000'),
|
||||
geminiApiKey: process.env.GEMINI_API_KEY || '',
|
||||
resultsDir: './src/results',
|
||||
uploadsDir: './uploads/temp',
|
||||
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||
maxFiles: 3
|
||||
};
|
||||
|
||||
// Create Express application
|
||||
export const createApp = (): Application => {
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Request ID middleware for logging
|
||||
app.use((req, res, next) => {
|
||||
req.requestId = Math.random().toString(36).substr(2, 9);
|
||||
res.setHeader('X-Request-ID', req.requestId);
|
||||
next();
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
};
|
||||
|
||||
console.log(`[${health.timestamp}] Health check - ${health.status}`);
|
||||
res.json(health);
|
||||
});
|
||||
|
||||
// API info endpoint
|
||||
app.get('/api/info', (req, res) => {
|
||||
const info = {
|
||||
name: 'Magic Building Image Generation API',
|
||||
version: '1.0.0',
|
||||
description: 'Express.js server for Gemini AI image generation',
|
||||
endpoints: {
|
||||
'GET /health': 'Health check',
|
||||
'GET /api/info': 'API information',
|
||||
'POST /api/generate': 'Generate images from text prompt with optional reference images'
|
||||
},
|
||||
limits: {
|
||||
maxFileSize: `${appConfig.maxFileSize / (1024 * 1024)}MB`,
|
||||
maxFiles: appConfig.maxFiles,
|
||||
supportedFormats: ['PNG', 'JPEG', 'JPG', 'WebP']
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`[${new Date().toISOString()}] API info requested`);
|
||||
res.json(info);
|
||||
});
|
||||
|
||||
// Mount API routes
|
||||
app.use('/api', generateRouter);
|
||||
|
||||
// Error handling middleware (must be last)
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
// Extend Express Request type to include requestId
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
requestId: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { GenerateImageResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* Global error handler for the Express application
|
||||
*/
|
||||
export const errorHandler = (
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const requestId = req.requestId || 'unknown';
|
||||
|
||||
// Log the error
|
||||
console.error(`[${timestamp}] [${requestId}] ERROR:`, {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
query: req.query
|
||||
});
|
||||
|
||||
// Don't send error response if headers already sent
|
||||
if (res.headersSent) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
// Determine error type and status code
|
||||
let statusCode = 500;
|
||||
let errorMessage = 'Internal server error';
|
||||
let errorType = 'INTERNAL_ERROR';
|
||||
|
||||
if (error.name === 'ValidationError') {
|
||||
statusCode = 400;
|
||||
errorMessage = error.message;
|
||||
errorType = 'VALIDATION_ERROR';
|
||||
} else if (error.message.includes('API key') || error.message.includes('authentication')) {
|
||||
statusCode = 401;
|
||||
errorMessage = 'Authentication failed';
|
||||
errorType = 'AUTH_ERROR';
|
||||
} else if (error.message.includes('not found') || error.message.includes('404')) {
|
||||
statusCode = 404;
|
||||
errorMessage = 'Resource not found';
|
||||
errorType = 'NOT_FOUND';
|
||||
} else if (error.message.includes('timeout') || error.message.includes('503')) {
|
||||
statusCode = 503;
|
||||
errorMessage = 'Service temporarily unavailable';
|
||||
errorType = 'SERVICE_UNAVAILABLE';
|
||||
} else if (error.message.includes('overloaded') || error.message.includes('rate limit')) {
|
||||
statusCode = 429;
|
||||
errorMessage = 'Service overloaded, please try again later';
|
||||
errorType = 'RATE_LIMITED';
|
||||
}
|
||||
|
||||
// Create error response
|
||||
const errorResponse: GenerateImageResponse = {
|
||||
success: false,
|
||||
message: 'Request failed',
|
||||
error: errorMessage
|
||||
};
|
||||
|
||||
// Add additional debug info in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
(errorResponse as any).debug = {
|
||||
originalError: error.message,
|
||||
errorType,
|
||||
requestId,
|
||||
timestamp
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Sending error response: ${statusCode} - ${errorMessage}`);
|
||||
|
||||
res.status(statusCode).json(errorResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* 404 handler for unmatched routes
|
||||
*/
|
||||
export const notFoundHandler = (req: Request, res: Response) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const requestId = req.requestId || 'unknown';
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] 404 - Route not found: ${req.method} ${req.path}`);
|
||||
|
||||
const notFoundResponse: GenerateImageResponse = {
|
||||
success: false,
|
||||
message: 'Route not found',
|
||||
error: `The requested endpoint ${req.method} ${req.path} does not exist`
|
||||
};
|
||||
|
||||
res.status(404).json(notFoundResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Async error wrapper to catch errors in async route handlers
|
||||
*/
|
||||
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import multer, { Multer } from 'multer';
|
||||
import { Request, RequestHandler } from 'express';
|
||||
|
||||
// Configure multer for memory storage (we'll process files in memory)
|
||||
const storage = multer.memoryStorage();
|
||||
|
||||
// File filter for image types only
|
||||
const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
|
||||
const allowedTypes = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/webp'
|
||||
];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
console.log(`[${new Date().toISOString()}] Accepted file: ${file.originalname} (${file.mimetype})`);
|
||||
cb(null, true);
|
||||
} else {
|
||||
console.log(`[${new Date().toISOString()}] Rejected file: ${file.originalname} (${file.mimetype})`);
|
||||
cb(new Error(`Unsupported file type: ${file.mimetype}. Allowed: PNG, JPEG, WebP`));
|
||||
}
|
||||
};
|
||||
|
||||
// Configuration constants
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const MAX_FILES = 3;
|
||||
|
||||
// Configure multer with limits and file filtering
|
||||
export const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: MAX_FILE_SIZE, // 5MB per file
|
||||
files: MAX_FILES, // Maximum 3 files
|
||||
},
|
||||
fileFilter: fileFilter
|
||||
});
|
||||
|
||||
// Middleware for handling reference images
|
||||
export const uploadReferenceImages: RequestHandler = upload.array('referenceFiles', MAX_FILES);
|
||||
|
||||
// Error handler for multer errors
|
||||
export const handleUploadErrors = (error: any, req: Request, res: any, next: any) => {
|
||||
if (error instanceof multer.MulterError) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}] Multer error:`, error.message);
|
||||
|
||||
switch (error.code) {
|
||||
case 'LIMIT_FILE_SIZE':
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `File too large. Maximum size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`,
|
||||
message: 'File upload failed'
|
||||
});
|
||||
|
||||
case 'LIMIT_FILE_COUNT':
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Too many files. Maximum: ${MAX_FILES} files`,
|
||||
message: 'File upload failed'
|
||||
});
|
||||
|
||||
case 'LIMIT_UNEXPECTED_FILE':
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Unexpected file field. Use "referenceFiles" for image uploads',
|
||||
message: 'File upload failed'
|
||||
});
|
||||
|
||||
default:
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: 'File upload failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (error.message.includes('Unsupported file type')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: 'File validation failed'
|
||||
});
|
||||
}
|
||||
|
||||
// Pass other errors to the next error handler
|
||||
next(error);
|
||||
};
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { GenerateImageRequestWithFiles } from '../types/api';
|
||||
|
||||
// Validation rules
|
||||
const VALIDATION_RULES = {
|
||||
prompt: {
|
||||
minLength: 3,
|
||||
maxLength: 2000,
|
||||
required: true
|
||||
},
|
||||
filename: {
|
||||
minLength: 1,
|
||||
maxLength: 100,
|
||||
required: true,
|
||||
pattern: /^[a-zA-Z0-9_-]+$/ // Only alphanumeric, underscore, hyphen
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitize filename to prevent directory traversal and invalid characters
|
||||
*/
|
||||
export const sanitizeFilename = (filename: string): string => {
|
||||
return filename
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '_') // Replace invalid chars with underscore
|
||||
.replace(/_{2,}/g, '_') // Replace multiple underscores with single
|
||||
.replace(/^_+|_+$/g, '') // Remove leading/trailing underscores
|
||||
.substring(0, 100); // Limit length
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the generate image request
|
||||
*/
|
||||
export const validateGenerateRequest = (
|
||||
req: any,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const { prompt, filename } = req.body;
|
||||
const errors: string[] = [];
|
||||
|
||||
console.log(`[${timestamp}] [${req.requestId}] Validating generate request`);
|
||||
|
||||
// Validate prompt
|
||||
if (!prompt) {
|
||||
errors.push('Prompt is required');
|
||||
} else if (typeof prompt !== 'string') {
|
||||
errors.push('Prompt must be a string');
|
||||
} else if (prompt.trim().length < VALIDATION_RULES.prompt.minLength) {
|
||||
errors.push(`Prompt must be at least ${VALIDATION_RULES.prompt.minLength} characters`);
|
||||
} else if (prompt.length > VALIDATION_RULES.prompt.maxLength) {
|
||||
errors.push(`Prompt must be less than ${VALIDATION_RULES.prompt.maxLength} characters`);
|
||||
}
|
||||
|
||||
// Validate filename
|
||||
if (!filename) {
|
||||
errors.push('Filename is required');
|
||||
} else if (typeof filename !== 'string') {
|
||||
errors.push('Filename must be a string');
|
||||
} else if (filename.trim().length < VALIDATION_RULES.filename.minLength) {
|
||||
errors.push('Filename cannot be empty');
|
||||
} else if (filename.length > VALIDATION_RULES.filename.maxLength) {
|
||||
errors.push(`Filename must be less than ${VALIDATION_RULES.filename.maxLength} characters`);
|
||||
} else if (!VALIDATION_RULES.filename.pattern.test(filename)) {
|
||||
errors.push('Filename can only contain letters, numbers, underscores, and hyphens');
|
||||
}
|
||||
|
||||
// Check for XSS attempts in prompt
|
||||
const xssPatterns = [
|
||||
/<script/i,
|
||||
/javascript:/i,
|
||||
/on\w+\s*=/i,
|
||||
/<iframe/i,
|
||||
/<object/i,
|
||||
/<embed/i
|
||||
];
|
||||
|
||||
if (prompt && xssPatterns.some(pattern => pattern.test(prompt))) {
|
||||
errors.push('Invalid characters detected in prompt');
|
||||
}
|
||||
|
||||
// Log validation results
|
||||
if (errors.length > 0) {
|
||||
console.log(`[${timestamp}] [${req.requestId}] Validation failed: ${errors.join(', ')}`);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
message: errors.join(', ')
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
if (filename) {
|
||||
req.body.filename = sanitizeFilename(filename.trim());
|
||||
if (req.body.filename !== filename.trim()) {
|
||||
console.log(`[${timestamp}] [${req.requestId}] Filename sanitized: "${filename}" -> "${req.body.filename}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Trim and clean prompt
|
||||
if (prompt) {
|
||||
req.body.prompt = prompt.trim();
|
||||
}
|
||||
|
||||
console.log(`[${timestamp}] [${req.requestId}] Validation passed`);
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Log request details for debugging
|
||||
*/
|
||||
export const logRequestDetails = (
|
||||
req: any,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const { prompt, filename } = req.body;
|
||||
const files = req.files as Express.Multer.File[] || [];
|
||||
|
||||
console.log(`[${timestamp}] [${req.requestId}] === REQUEST DETAILS ===`);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Method: ${req.method}`);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Path: ${req.path}`);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Prompt: "${prompt?.substring(0, 100)}${prompt?.length > 100 ? '...' : ''}"`);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Filename: "${filename}"`);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Reference files: ${files.length}`);
|
||||
|
||||
if (files.length > 0) {
|
||||
files.forEach((file, index) => {
|
||||
console.log(`[${timestamp}] [${req.requestId}] File ${index + 1}: ${file.originalname} (${file.mimetype}, ${Math.round(file.size / 1024)}KB)`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[${timestamp}] [${req.requestId}] ===========================`);
|
||||
next();
|
||||
};
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { Response, Router } from 'express';
|
||||
import type { Router as RouterType } from 'express';
|
||||
import { ImageGenService } from '../services/ImageGenService';
|
||||
import { uploadReferenceImages, handleUploadErrors } from '../middleware/upload';
|
||||
import { validateGenerateRequest, logRequestDetails } from '../middleware/validation';
|
||||
import { asyncHandler } from '../middleware/errorHandler';
|
||||
import { GenerateImageRequestWithFiles, GenerateImageResponse } from '../types/api';
|
||||
// Create router
|
||||
export const generateRouter: RouterType = Router();
|
||||
|
||||
// Initialize ImageGenService (will be created in the route handler to avoid circular dependency)
|
||||
let imageGenService: ImageGenService;
|
||||
|
||||
/**
|
||||
* POST /api/generate - Generate image from text prompt with optional reference images
|
||||
*/
|
||||
generateRouter.post('/generate',
|
||||
// File upload middleware
|
||||
uploadReferenceImages,
|
||||
handleUploadErrors,
|
||||
|
||||
// Validation middleware
|
||||
logRequestDetails,
|
||||
validateGenerateRequest,
|
||||
|
||||
// Main handler
|
||||
asyncHandler(async (req: any, res: Response) => {
|
||||
// Initialize service if not already done
|
||||
if (!imageGenService) {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
if (!apiKey) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server configuration error',
|
||||
error: 'GEMINI_API_KEY not configured'
|
||||
} as GenerateImageResponse);
|
||||
}
|
||||
imageGenService = new ImageGenService(apiKey);
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const requestId = req.requestId;
|
||||
const { prompt, filename } = req.body;
|
||||
const files = req.files as Express.Multer.File[] || [];
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Starting image generation process`);
|
||||
|
||||
try {
|
||||
// Validate reference images if provided
|
||||
if (files.length > 0) {
|
||||
const validation = ImageGenService.validateReferenceImages(files);
|
||||
if (!validation.valid) {
|
||||
console.log(`[${timestamp}] [${requestId}] Reference image validation failed: ${validation.error}`);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Reference image validation failed',
|
||||
error: validation.error
|
||||
} as GenerateImageResponse);
|
||||
}
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Reference images validation passed`);
|
||||
}
|
||||
|
||||
// Convert files to reference images
|
||||
const referenceImages = files.length > 0
|
||||
? ImageGenService.convertFilesToReferenceImages(files)
|
||||
: undefined;
|
||||
|
||||
// Generate the image
|
||||
console.log(`[${timestamp}] [${requestId}] Calling ImageGenService.generateImage()`);
|
||||
|
||||
const result = await imageGenService.generateImage({
|
||||
prompt,
|
||||
filename,
|
||||
referenceImages
|
||||
});
|
||||
|
||||
// Log the result
|
||||
console.log(`[${timestamp}] [${requestId}] Image generation completed:`, {
|
||||
success: result.success,
|
||||
model: result.model,
|
||||
filename: result.filename,
|
||||
hasError: !!result.error
|
||||
});
|
||||
|
||||
// Send response
|
||||
if (result.success) {
|
||||
const successResponse: GenerateImageResponse = {
|
||||
success: true,
|
||||
message: 'Image generated successfully',
|
||||
data: {
|
||||
filename: result.filename!,
|
||||
filepath: result.filepath!,
|
||||
description: result.description,
|
||||
model: result.model,
|
||||
generatedAt: timestamp
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Sending success response`);
|
||||
return res.status(200).json(successResponse);
|
||||
|
||||
} else {
|
||||
const errorResponse: GenerateImageResponse = {
|
||||
success: false,
|
||||
message: 'Image generation failed',
|
||||
error: result.error || 'Unknown error occurred'
|
||||
};
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Sending error response: ${result.error}`);
|
||||
return res.status(500).json(errorResponse);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${timestamp}] [${requestId}] Unhandled error in generate endpoint:`, error);
|
||||
|
||||
const errorResponse: GenerateImageResponse = {
|
||||
success: false,
|
||||
message: 'Image generation failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
};
|
||||
|
||||
return res.status(500).json(errorResponse);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { createApp, appConfig } from './app';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Ensure required directories exist
|
||||
const ensureDirectoriesExist = () => {
|
||||
const directories = [
|
||||
appConfig.resultsDir,
|
||||
appConfig.uploadsDir,
|
||||
'./logs'
|
||||
];
|
||||
|
||||
directories.forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(`[${new Date().toISOString()}] Created directory: ${dir}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Enhanced logging function
|
||||
const log = (level: 'INFO' | 'ERROR' | 'WARN' | 'DEBUG', message: string) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logMessage = `[${timestamp}] [${level}] ${message}`;
|
||||
console.log(logMessage);
|
||||
};
|
||||
|
||||
// Validate environment
|
||||
const validateEnvironment = () => {
|
||||
if (!appConfig.geminiApiKey) {
|
||||
log('ERROR', 'GEMINI_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log('INFO', 'Environment validation passed');
|
||||
log('INFO', `Server configuration: port=${appConfig.port}, maxFileSize=${appConfig.maxFileSize}bytes, maxFiles=${appConfig.maxFiles}`);
|
||||
};
|
||||
|
||||
// Graceful shutdown
|
||||
const setupGracefulShutdown = (server: any) => {
|
||||
const shutdown = (signal: string) => {
|
||||
log('INFO', `Received ${signal}, shutting down gracefully...`);
|
||||
|
||||
server.close((err: Error) => {
|
||||
if (err) {
|
||||
log('ERROR', `Error during shutdown: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
log('INFO', 'Server closed successfully');
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||
};
|
||||
|
||||
// Start server
|
||||
const startServer = async () => {
|
||||
try {
|
||||
log('INFO', '🚀 Starting Magic Building Image Generation Server...');
|
||||
|
||||
// Validate environment and create directories
|
||||
validateEnvironment();
|
||||
ensureDirectoriesExist();
|
||||
|
||||
// Create Express app
|
||||
const app = createApp();
|
||||
|
||||
// Start server
|
||||
const server = app.listen(appConfig.port, () => {
|
||||
log('INFO', `✅ Server running on port ${appConfig.port}`);
|
||||
log('INFO', `📋 Health check: http://localhost:${appConfig.port}/health`);
|
||||
log('INFO', `📖 API info: http://localhost:${appConfig.port}/api/info`);
|
||||
log('INFO', `🎨 Generate endpoint: http://localhost:${appConfig.port}/api/generate`);
|
||||
});
|
||||
|
||||
// Setup graceful shutdown
|
||||
setupGracefulShutdown(server);
|
||||
|
||||
} catch (error) {
|
||||
log('ERROR', `Failed to start server: ${error instanceof Error ? error.message : error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Start the server
|
||||
startServer();
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
import { GoogleGenAI } from '@google/genai';
|
||||
import mime from 'mime';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ImageGenerationOptions, ImageGenerationResult, ReferenceImage } from '../types/api';
|
||||
|
||||
export class ImageGenService {
|
||||
private ai: GoogleGenAI;
|
||||
private primaryModel = 'gemini-2.5-flash-image-preview';
|
||||
private fallbackModel = 'imagen-4.0-generate-001';
|
||||
|
||||
constructor(apiKey: string) {
|
||||
if (!apiKey) {
|
||||
throw new Error('Gemini API key is required');
|
||||
}
|
||||
this.ai = new GoogleGenAI({ apiKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an image from text prompt with optional reference images
|
||||
*/
|
||||
async generateImage(options: ImageGenerationOptions): Promise<ImageGenerationResult> {
|
||||
const { prompt, filename, referenceImages } = options;
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
console.log(`[${timestamp}] Starting image generation: "${prompt.substring(0, 50)}..."`);
|
||||
|
||||
try {
|
||||
// First try the primary model (Nano Banana)
|
||||
const result = await this.tryGeneration({
|
||||
model: this.primaryModel,
|
||||
config: { responseModalities: ['IMAGE', 'TEXT'] },
|
||||
prompt,
|
||||
filename,
|
||||
referenceImages,
|
||||
modelName: 'Nano Banana'
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback to Imagen 4
|
||||
console.log(`[${new Date().toISOString()}] Primary model failed, trying fallback (Imagen 4)...`);
|
||||
|
||||
return await this.tryGeneration({
|
||||
model: this.fallbackModel,
|
||||
config: { responseModalities: ['IMAGE'] },
|
||||
prompt,
|
||||
filename: `${filename}_fallback`,
|
||||
referenceImages,
|
||||
modelName: 'Imagen 4'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Image generation failed:`, error);
|
||||
return {
|
||||
success: false,
|
||||
model: 'none',
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try generation with a specific model
|
||||
*/
|
||||
private async tryGeneration(params: {
|
||||
model: string;
|
||||
config: { responseModalities: string[] };
|
||||
prompt: string;
|
||||
filename: string;
|
||||
referenceImages?: ReferenceImage[];
|
||||
modelName: string;
|
||||
}): Promise<ImageGenerationResult> {
|
||||
|
||||
const { model, config, prompt, filename, referenceImages, modelName } = params;
|
||||
|
||||
try {
|
||||
// Build content parts for the API request
|
||||
const contentParts: any[] = [];
|
||||
|
||||
// Add reference images if provided
|
||||
if (referenceImages && referenceImages.length > 0) {
|
||||
console.log(`[${new Date().toISOString()}] Adding ${referenceImages.length} reference image(s)`);
|
||||
|
||||
for (const refImage of referenceImages) {
|
||||
contentParts.push({
|
||||
inlineData: {
|
||||
mimeType: refImage.mimetype,
|
||||
data: refImage.buffer.toString('base64')
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add the text prompt
|
||||
contentParts.push({
|
||||
text: prompt
|
||||
});
|
||||
|
||||
const contents = [
|
||||
{
|
||||
role: 'user' as const,
|
||||
parts: contentParts
|
||||
}
|
||||
];
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Making API request to ${modelName} (${model})...`);
|
||||
|
||||
const response = await this.ai.models.generateContent({
|
||||
model,
|
||||
config,
|
||||
contents
|
||||
});
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Response received from ${modelName}`);
|
||||
|
||||
if (response.candidates && response.candidates[0] && response.candidates[0].content) {
|
||||
const content = response.candidates[0].content;
|
||||
let generatedDescription = '';
|
||||
let savedImagePath = '';
|
||||
|
||||
for (let index = 0; index < (content.parts?.length || 0); index++) {
|
||||
const part = content.parts![index];
|
||||
|
||||
if (part.inlineData) {
|
||||
const fileExtension = mime.getExtension(part.inlineData.mimeType || '');
|
||||
const finalFilename = `${filename}.${fileExtension}`;
|
||||
const filepath = path.join('./src/results', finalFilename);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Saving image: ${finalFilename}`);
|
||||
|
||||
const buffer = Buffer.from(part.inlineData.data || '', 'base64');
|
||||
await this.saveImageFile(filepath, buffer);
|
||||
|
||||
savedImagePath = filepath;
|
||||
|
||||
} else if (part.text) {
|
||||
generatedDescription = part.text;
|
||||
console.log(`[${new Date().toISOString()}] Generated description: ${part.text.substring(0, 100)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedImagePath) {
|
||||
return {
|
||||
success: true,
|
||||
filename: path.basename(savedImagePath),
|
||||
filepath: savedImagePath,
|
||||
description: generatedDescription,
|
||||
model: modelName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
model: modelName,
|
||||
error: 'No image data received from API'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] ${modelName} generation failed:`, error);
|
||||
return {
|
||||
success: false,
|
||||
model: modelName,
|
||||
error: error instanceof Error ? error.message : 'Generation failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save image buffer to file system
|
||||
*/
|
||||
private async saveImageFile(filepath: string, buffer: Buffer): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Ensure the results directory exists
|
||||
const dir = path.dirname(filepath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFile(filepath, buffer, (err) => {
|
||||
if (err) {
|
||||
console.error(`[${new Date().toISOString()}] Error saving file ${filepath}:`, err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`[${new Date().toISOString()}] File saved successfully: ${filepath}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate reference images
|
||||
*/
|
||||
static validateReferenceImages(files: Express.Multer.File[]): { valid: boolean; error?: string } {
|
||||
if (files.length > 3) {
|
||||
return { valid: false, error: 'Maximum 3 reference images allowed' };
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'];
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
for (const file of files) {
|
||||
if (!allowedTypes.includes(file.mimetype)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Unsupported file type: ${file.mimetype}. Allowed: PNG, JPEG, WebP`
|
||||
};
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `File ${file.originalname} is too large. Maximum size: 5MB`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Express.Multer.File[] to ReferenceImage[]
|
||||
*/
|
||||
static convertFilesToReferenceImages(files: Express.Multer.File[]): ReferenceImage[] {
|
||||
return files.map(file => ({
|
||||
buffer: file.buffer,
|
||||
mimetype: file.mimetype,
|
||||
originalname: file.originalname
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { Request } from 'express';
|
||||
|
||||
// API Request/Response types
|
||||
export interface GenerateImageRequest {
|
||||
prompt: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface GenerateImageResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: {
|
||||
filename: string;
|
||||
filepath: string;
|
||||
description?: string;
|
||||
model: string;
|
||||
generatedAt: string;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Extended Express Request with file uploads
|
||||
export interface GenerateImageRequestWithFiles extends Request {
|
||||
body: GenerateImageRequest;
|
||||
files?: Express.Multer.File[];
|
||||
}
|
||||
|
||||
// Image generation service types
|
||||
export interface ImageGenerationOptions {
|
||||
prompt: string;
|
||||
filename: string;
|
||||
referenceImages?: ReferenceImage[];
|
||||
}
|
||||
|
||||
export interface ReferenceImage {
|
||||
buffer: Buffer;
|
||||
mimetype: string;
|
||||
originalname: string;
|
||||
}
|
||||
|
||||
export interface ImageGenerationResult {
|
||||
success: boolean;
|
||||
filename?: string;
|
||||
filepath?: string;
|
||||
description?: string;
|
||||
model: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Logging types
|
||||
export interface LogContext {
|
||||
requestId: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Environment configuration
|
||||
export interface Config {
|
||||
port: number;
|
||||
geminiApiKey: string;
|
||||
resultsDir: string;
|
||||
uploadsDir: string;
|
||||
maxFileSize: number;
|
||||
maxFiles: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Magic Building Image Generation API Test Script
|
||||
# Usage: ./test-api.sh [server_url]
|
||||
|
||||
SERVER_URL=${1:-"http://localhost:3000"}
|
||||
TEST_DIR="./test-images"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
LOG_FILE="./logs/test-api-${TIMESTAMP}.log"
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p logs test-images
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Success/error output functions
|
||||
success() {
|
||||
echo -e "${GREEN}✅ $1${NC}" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}❌ $1${NC}" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Test functions
|
||||
test_health_check() {
|
||||
info "Testing health check endpoint..."
|
||||
response=$(curl -s -o /tmp/health.json -w "%{http_code}" "$SERVER_URL/health")
|
||||
|
||||
if [ "$response" -eq 200 ]; then
|
||||
success "Health check passed (HTTP $response)"
|
||||
log "Response: $(cat /tmp/health.json)"
|
||||
else
|
||||
error "Health check failed (HTTP $response)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_api_info() {
|
||||
info "Testing API info endpoint..."
|
||||
response=$(curl -s -o /tmp/api-info.json -w "%{http_code}" "$SERVER_URL/api/info")
|
||||
|
||||
if [ "$response" -eq 200 ]; then
|
||||
success "API info endpoint passed (HTTP $response)"
|
||||
log "Response: $(cat /tmp/api-info.json)"
|
||||
else
|
||||
error "API info endpoint failed (HTTP $response)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_text_to_image() {
|
||||
info "Testing text-to-image generation..."
|
||||
|
||||
response=$(curl -s -o /tmp/text-to-image.json -w "%{http_code}" \
|
||||
-X POST "$SERVER_URL/api/generate" \
|
||||
-F "prompt=A majestic dragon soaring through clouds above a medieval castle" \
|
||||
-F "filename=dragon_castle_${TIMESTAMP}")
|
||||
|
||||
if [ "$response" -eq 200 ]; then
|
||||
success "Text-to-image generation passed (HTTP $response)"
|
||||
log "Response: $(cat /tmp/text-to-image.json)"
|
||||
|
||||
# Check if image file was created
|
||||
filename=$(jq -r '.data.filename' /tmp/text-to-image.json 2>/dev/null)
|
||||
if [ "$filename" != "null" ] && [ -f "src/results/$filename" ]; then
|
||||
success "Generated image file found: $filename"
|
||||
log "Image size: $(ls -lh src/results/$filename | awk '{print $5}')"
|
||||
else
|
||||
warning "Generated image file not found or response malformed"
|
||||
fi
|
||||
else
|
||||
error "Text-to-image generation failed (HTTP $response)"
|
||||
log "Error response: $(cat /tmp/text-to-image.json)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_validation_errors() {
|
||||
info "Testing validation error handling..."
|
||||
|
||||
# Test missing prompt
|
||||
response=$(curl -s -o /tmp/validation1.json -w "%{http_code}" \
|
||||
-X POST "$SERVER_URL/api/generate" \
|
||||
-F "filename=test")
|
||||
|
||||
if [ "$response" -eq 400 ]; then
|
||||
success "Missing prompt validation works (HTTP $response)"
|
||||
else
|
||||
error "Missing prompt validation failed (HTTP $response)"
|
||||
fi
|
||||
|
||||
# Test missing filename
|
||||
response=$(curl -s -o /tmp/validation2.json -w "%{http_code}" \
|
||||
-X POST "$SERVER_URL/api/generate" \
|
||||
-F "prompt=Test prompt")
|
||||
|
||||
if [ "$response" -eq 400 ]; then
|
||||
success "Missing filename validation works (HTTP $response)"
|
||||
else
|
||||
error "Missing filename validation failed (HTTP $response)"
|
||||
fi
|
||||
|
||||
# Test invalid filename characters
|
||||
response=$(curl -s -o /tmp/validation3.json -w "%{http_code}" \
|
||||
-X POST "$SERVER_URL/api/generate" \
|
||||
-F "prompt=Test prompt" \
|
||||
-F "filename=invalid/filename*with?bad&chars")
|
||||
|
||||
if [ "$response" -eq 200 ]; then
|
||||
success "Filename sanitization works (HTTP $response)"
|
||||
log "Sanitized filename response: $(cat /tmp/validation3.json)"
|
||||
else
|
||||
warning "Filename sanitization test had unexpected result (HTTP $response)"
|
||||
fi
|
||||
}
|
||||
|
||||
test_404_endpoint() {
|
||||
info "Testing 404 error handling..."
|
||||
response=$(curl -s -o /tmp/404.json -w "%{http_code}" "$SERVER_URL/api/nonexistent")
|
||||
|
||||
if [ "$response" -eq 404 ]; then
|
||||
success "404 error handling works (HTTP $response)"
|
||||
else
|
||||
error "404 error handling failed (HTTP $response)"
|
||||
fi
|
||||
}
|
||||
|
||||
create_test_image() {
|
||||
info "Creating test reference image..."
|
||||
|
||||
# Create a simple test image using ImageMagick (if available) or skip
|
||||
if command -v convert &> /dev/null; then
|
||||
convert -size 300x200 xc:lightblue -pointsize 24 -fill black \
|
||||
-gravity center -annotate 0 "Test Reference Image" \
|
||||
"$TEST_DIR/test-reference.png"
|
||||
success "Test reference image created"
|
||||
else
|
||||
warning "ImageMagick not available, skipping reference image test"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_image_with_reference() {
|
||||
if [ ! -f "$TEST_DIR/test-reference.png" ]; then
|
||||
warning "No reference image available, skipping reference image test"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Testing image generation with reference image..."
|
||||
|
||||
response=$(curl -s -o /tmp/with-reference.json -w "%{http_code}" \
|
||||
-X POST "$SERVER_URL/api/generate" \
|
||||
-F "prompt=Add magical sparkles and glowing effects to this image" \
|
||||
-F "filename=sparkled_reference_${TIMESTAMP}" \
|
||||
-F "referenceFiles=@${TEST_DIR}/test-reference.png")
|
||||
|
||||
if [ "$response" -eq 200 ]; then
|
||||
success "Reference image generation passed (HTTP $response)"
|
||||
log "Response: $(cat /tmp/with-reference.json)"
|
||||
|
||||
# Check if image file was created
|
||||
filename=$(jq -r '.data.filename' /tmp/with-reference.json 2>/dev/null)
|
||||
if [ "$filename" != "null" ] && [ -f "src/results/$filename" ]; then
|
||||
success "Generated image with reference found: $filename"
|
||||
else
|
||||
warning "Generated image with reference not found"
|
||||
fi
|
||||
else
|
||||
error "Reference image generation failed (HTTP $response)"
|
||||
log "Error response: $(cat /tmp/with-reference.json)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Performance test
|
||||
test_concurrent_requests() {
|
||||
info "Testing concurrent requests (performance test)..."
|
||||
|
||||
# Start 3 concurrent requests
|
||||
for i in {1..3}; do
|
||||
(
|
||||
response=$(curl -s -o "/tmp/concurrent-$i.json" -w "%{http_code}" \
|
||||
-X POST "$SERVER_URL/api/generate" \
|
||||
-F "prompt=Concurrent test image $i - fantasy landscape with mountains" \
|
||||
-F "filename=concurrent_test_${i}_${TIMESTAMP}")
|
||||
|
||||
if [ "$response" -eq 200 ]; then
|
||||
log "Concurrent request $i succeeded (HTTP $response)"
|
||||
else
|
||||
log "Concurrent request $i failed (HTTP $response)"
|
||||
fi
|
||||
) &
|
||||
done
|
||||
|
||||
# Wait for all background jobs
|
||||
wait
|
||||
|
||||
success "Concurrent requests test completed"
|
||||
}
|
||||
|
||||
# Main test execution
|
||||
main() {
|
||||
info "🚀 Starting Magic Building API Test Suite"
|
||||
info "Server URL: $SERVER_URL"
|
||||
info "Test log: $LOG_FILE"
|
||||
|
||||
# Check if server is running
|
||||
if ! curl -s "$SERVER_URL/health" > /dev/null; then
|
||||
error "Server is not responding at $SERVER_URL"
|
||||
error "Please start the server with: pnpm server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
tests_passed=0
|
||||
tests_total=0
|
||||
|
||||
run_test() {
|
||||
((tests_total++))
|
||||
if $1; then
|
||||
((tests_passed++))
|
||||
fi
|
||||
}
|
||||
|
||||
run_test test_health_check
|
||||
run_test test_api_info
|
||||
run_test test_text_to_image
|
||||
run_test test_validation_errors
|
||||
run_test test_404_endpoint
|
||||
|
||||
# Optional tests
|
||||
if create_test_image; then
|
||||
run_test test_image_with_reference
|
||||
fi
|
||||
|
||||
run_test test_concurrent_requests
|
||||
|
||||
# Summary
|
||||
echo
|
||||
info "📊 Test Results Summary"
|
||||
success "Passed: $tests_passed/$tests_total tests"
|
||||
|
||||
if [ "$tests_passed" -eq "$tests_total" ]; then
|
||||
success "🎉 All tests passed!"
|
||||
else
|
||||
warning "⚠️ Some tests failed. Check the logs for details."
|
||||
fi
|
||||
|
||||
info "Generated images are in: src/results/"
|
||||
info "Test logs saved to: $LOG_FILE"
|
||||
}
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
if ! command -v curl &> /dev/null; then
|
||||
error "curl is required for testing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
warning "jq not found - JSON response parsing will be limited"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run the tests
|
||||
check_dependencies
|
||||
main "$@"
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["*"],
|
||||
"@server/*": ["server/*"],
|
||||
"@generation/*": ["generation/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifierResolution": "node"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue