style: Apply Prettier formatting to all source files
- Consistent code formatting across the entire codebase - Double quotes for strings, semicolons, and proper indentation - Improved readability and code consistency
This commit is contained in:
parent
251ac6e27c
commit
52549836e4
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'es5',
|
||||
singleQuote: true,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
import js from '@eslint/js';
|
||||
import typescript from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
module.exports = [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
|
|
@ -16,12 +10,10 @@ export default [
|
|||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescript,
|
||||
prettier: prettier,
|
||||
'@typescript-eslint': require('@typescript-eslint/eslint-plugin'),
|
||||
prettier: require('eslint-plugin-prettier'),
|
||||
},
|
||||
rules: {
|
||||
...typescript.configs.recommended.rules,
|
||||
...typescript.configs['recommended-requiring-type-checking'].rules,
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
|
|
@ -34,6 +26,8 @@ export default [
|
|||
'object-shorthand': 'error',
|
||||
'prefer-template': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
|
|
|
|||
|
|
@ -40,19 +40,20 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^5.1.0",
|
||||
"mime": "^4.1.0",
|
||||
"multer": "^2.0.2",
|
||||
"helmet": "^8.0.0",
|
||||
"express-rate-limit": "^7.4.1",
|
||||
"express-validator": "^7.2.0",
|
||||
"helmet": "^8.0.0",
|
||||
"mime": "^4.1.0",
|
||||
"multer": "^2.0.2",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
||||
"@typescript-eslint/parser": "^8.18.2",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ importers:
|
|||
specifier: ^3.17.0
|
||||
version: 3.17.0
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.36.0
|
||||
version: 9.36.0
|
||||
'@types/cors':
|
||||
specifier: ^2.8.19
|
||||
version: 2.8.19
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { GenerateImageResponse } from '../types/api';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { GenerateImageResponse } from "../types/api";
|
||||
|
||||
/**
|
||||
* Global error handler for the Express application
|
||||
|
|
@ -8,10 +8,10 @@ export const errorHandler = (
|
|||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const requestId = req.requestId || 'unknown';
|
||||
const requestId = req.requestId || "unknown";
|
||||
|
||||
// Log the error
|
||||
console.error(`[${timestamp}] [${requestId}] ERROR:`, {
|
||||
|
|
@ -20,7 +20,7 @@ export const errorHandler = (
|
|||
path: req.path,
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
query: req.query
|
||||
query: req.query,
|
||||
});
|
||||
|
||||
// Don't send error response if headers already sent
|
||||
|
|
@ -30,49 +30,63 @@ export const errorHandler = (
|
|||
|
||||
// Determine error type and status code
|
||||
let statusCode = 500;
|
||||
let errorMessage = 'Internal server error';
|
||||
let errorType = 'INTERNAL_ERROR';
|
||||
let errorMessage = "Internal server error";
|
||||
let errorType = "INTERNAL_ERROR";
|
||||
|
||||
if (error.name === 'ValidationError') {
|
||||
if (error.name === "ValidationError") {
|
||||
statusCode = 400;
|
||||
errorMessage = error.message;
|
||||
errorType = 'VALIDATION_ERROR';
|
||||
} else if (error.message.includes('API key') || error.message.includes('authentication')) {
|
||||
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')) {
|
||||
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')) {
|
||||
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')) {
|
||||
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';
|
||||
errorMessage = "Service overloaded, please try again later";
|
||||
errorType = "RATE_LIMITED";
|
||||
}
|
||||
|
||||
// Create error response
|
||||
const errorResponse: GenerateImageResponse = {
|
||||
success: false,
|
||||
message: 'Request failed',
|
||||
error: errorMessage
|
||||
message: "Request failed",
|
||||
error: errorMessage,
|
||||
};
|
||||
|
||||
// Add additional debug info in development
|
||||
if (process.env['NODE_ENV'] === 'development') {
|
||||
if (process.env["NODE_ENV"] === "development") {
|
||||
(errorResponse as any).debug = {
|
||||
originalError: error.message,
|
||||
errorType,
|
||||
requestId,
|
||||
timestamp
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Sending error response: ${statusCode} - ${errorMessage}`);
|
||||
console.log(
|
||||
`[${timestamp}] [${requestId}] Sending error response: ${statusCode} - ${errorMessage}`,
|
||||
);
|
||||
|
||||
res.status(statusCode).json(errorResponse);
|
||||
};
|
||||
|
|
@ -82,14 +96,16 @@ export const errorHandler = (
|
|||
*/
|
||||
export const notFoundHandler = (req: Request, res: Response) => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const requestId = req.requestId || 'unknown';
|
||||
const requestId = req.requestId || "unknown";
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] 404 - Route not found: ${req.method} ${req.path}`);
|
||||
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`
|
||||
message: "Route not found",
|
||||
error: `The requested endpoint ${req.method} ${req.path} does not exist`,
|
||||
};
|
||||
|
||||
res.status(404).json(notFoundResponse);
|
||||
|
|
@ -98,6 +114,7 @@ export const notFoundHandler = (req: Request, res: Response) => {
|
|||
/**
|
||||
* Async error wrapper to catch errors in async route handlers
|
||||
*/
|
||||
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
|
||||
export const asyncHandler =
|
||||
(fn: Function) => (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
|
|
@ -1,24 +1,31 @@
|
|||
import multer from 'multer';
|
||||
import { Request, RequestHandler } from 'express';
|
||||
import 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'
|
||||
];
|
||||
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})`);
|
||||
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`));
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Rejected file: ${file.originalname} (${file.mimetype})`,
|
||||
);
|
||||
cb(
|
||||
new Error(
|
||||
`Unsupported file type: ${file.mimetype}. Allowed: PNG, JPEG, WebP`,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -33,54 +40,63 @@ export const upload = multer({
|
|||
fileSize: MAX_FILE_SIZE, // 5MB per file
|
||||
files: MAX_FILES, // Maximum 3 files
|
||||
},
|
||||
fileFilter: fileFilter
|
||||
fileFilter: fileFilter,
|
||||
});
|
||||
|
||||
// Middleware for handling reference images
|
||||
export const uploadReferenceImages: RequestHandler = upload.array('referenceImages', MAX_FILES);
|
||||
export const uploadReferenceImages: RequestHandler = upload.array(
|
||||
"referenceImages",
|
||||
MAX_FILES,
|
||||
);
|
||||
|
||||
// Error handler for multer errors
|
||||
export const handleUploadErrors = (error: any, _req: Request, res: any, next: any) => {
|
||||
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':
|
||||
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'
|
||||
message: "File upload failed",
|
||||
});
|
||||
|
||||
case 'LIMIT_FILE_COUNT':
|
||||
case "LIMIT_FILE_COUNT":
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Too many files. Maximum: ${MAX_FILES} files`,
|
||||
message: 'File upload failed'
|
||||
message: "File upload failed",
|
||||
});
|
||||
|
||||
case 'LIMIT_UNEXPECTED_FILE':
|
||||
case "LIMIT_UNEXPECTED_FILE":
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Unexpected file field. Use "referenceImages" for image uploads',
|
||||
message: 'File upload failed'
|
||||
error:
|
||||
'Unexpected file field. Use "referenceImages" for image uploads',
|
||||
message: "File upload failed",
|
||||
});
|
||||
|
||||
default:
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: 'File upload failed'
|
||||
message: "File upload failed",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (error.message.includes('Unsupported file type')) {
|
||||
if (error.message.includes("Unsupported file type")) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: 'File validation failed'
|
||||
message: "File validation failed",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { Response, NextFunction } from 'express';
|
||||
import { Response, NextFunction } from "express";
|
||||
|
||||
// Validation rules
|
||||
const VALIDATION_RULES = {
|
||||
prompt: {
|
||||
minLength: 3,
|
||||
maxLength: 2000,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
filename: {
|
||||
minLength: 1,
|
||||
maxLength: 100,
|
||||
required: true,
|
||||
pattern: /^[a-zA-Z0-9_-]+$/ // Only alphanumeric, underscore, hyphen
|
||||
}
|
||||
pattern: /^[a-zA-Z0-9_-]+$/, // Only alphanumeric, underscore, hyphen
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -20,9 +20,9 @@ const VALIDATION_RULES = {
|
|||
*/
|
||||
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
|
||||
.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
|
||||
};
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export const sanitizeFilename = (filename: string): string => {
|
|||
export const validateGenerateRequest = (
|
||||
req: any,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
next: NextFunction,
|
||||
): void | Response => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const { prompt, filename } = req.body;
|
||||
|
|
@ -42,26 +42,34 @@ export const validateGenerateRequest = (
|
|||
|
||||
// Validate prompt
|
||||
if (!prompt) {
|
||||
errors.push('Prompt is required');
|
||||
} else if (typeof prompt !== 'string') {
|
||||
errors.push('Prompt must be a string');
|
||||
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`);
|
||||
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`);
|
||||
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');
|
||||
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');
|
||||
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`);
|
||||
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');
|
||||
errors.push(
|
||||
"Filename can only contain letters, numbers, underscores, and hyphens",
|
||||
);
|
||||
}
|
||||
|
||||
// Check for XSS attempts in prompt
|
||||
|
|
@ -71,20 +79,22 @@ export const validateGenerateRequest = (
|
|||
/on\w+\s*=/i,
|
||||
/<iframe/i,
|
||||
/<object/i,
|
||||
/<embed/i
|
||||
/<embed/i,
|
||||
];
|
||||
|
||||
if (prompt && xssPatterns.some(pattern => pattern.test(prompt))) {
|
||||
errors.push('Invalid characters detected in prompt');
|
||||
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(', ')}`);
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] Validation failed: ${errors.join(", ")}`,
|
||||
);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
message: errors.join(', ')
|
||||
error: "Validation failed",
|
||||
message: errors.join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +102,9 @@ export const validateGenerateRequest = (
|
|||
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}"`);
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] Filename sanitized: "${filename}" -> "${req.body.filename}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,22 +123,28 @@ export const validateGenerateRequest = (
|
|||
export const logRequestDetails = (
|
||||
req: any,
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
next: NextFunction,
|
||||
): void => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const { prompt, filename } = req.body;
|
||||
const files = req.files as Express.Multer.File[] || [];
|
||||
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}] Prompt: "${prompt?.substring(0, 100)}${prompt?.length > 100 ? "..." : ""}"`,
|
||||
);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Filename: "${filename}"`);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Reference files: ${files.length}`);
|
||||
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}] File ${index + 1}: ${file.originalname} (${file.mimetype}, ${Math.round(file.size / 1024)}KB)`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
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 { GenerateImageResponse } from '../types/api';
|
||||
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 { GenerateImageResponse } from "../types/api";
|
||||
// Create router
|
||||
export const generateRouter: RouterType = Router();
|
||||
|
||||
|
|
@ -14,7 +20,8 @@ let imageGenService: ImageGenService;
|
|||
/**
|
||||
* POST /api/generate - Generate image from text prompt with optional reference images
|
||||
*/
|
||||
generateRouter.post('/generate',
|
||||
generateRouter.post(
|
||||
"/generate",
|
||||
// File upload middleware
|
||||
uploadReferenceImages,
|
||||
handleUploadErrors,
|
||||
|
|
@ -27,12 +34,12 @@ generateRouter.post('/generate',
|
|||
asyncHandler(async (req: any, res: Response) => {
|
||||
// Initialize service if not already done
|
||||
if (!imageGenService) {
|
||||
const apiKey = process.env['GEMINI_API_KEY'];
|
||||
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'
|
||||
message: "Server configuration error",
|
||||
error: "GEMINI_API_KEY not configured",
|
||||
} as GenerateImageResponse);
|
||||
}
|
||||
imageGenService = new ImageGenService(apiKey);
|
||||
|
|
@ -41,38 +48,47 @@ generateRouter.post('/generate',
|
|||
const timestamp = new Date().toISOString();
|
||||
const requestId = req.requestId;
|
||||
const { prompt, filename } = req.body;
|
||||
const files = req.files as Express.Multer.File[] || [];
|
||||
const files = (req.files as Express.Multer.File[]) || [];
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Starting image generation process`);
|
||||
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}`);
|
||||
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
|
||||
message: "Reference image validation failed",
|
||||
error: validation.error,
|
||||
} as GenerateImageResponse);
|
||||
}
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Reference images validation passed`);
|
||||
console.log(
|
||||
`[${timestamp}] [${requestId}] Reference images validation passed`,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert files to reference images
|
||||
const referenceImages = files.length > 0
|
||||
const referenceImages =
|
||||
files.length > 0
|
||||
? ImageGenService.convertFilesToReferenceImages(files)
|
||||
: undefined;
|
||||
|
||||
// Generate the image
|
||||
console.log(`[${timestamp}] [${requestId}] Calling ImageGenService.generateImage()`);
|
||||
console.log(
|
||||
`[${timestamp}] [${requestId}] Calling ImageGenService.generateImage()`,
|
||||
);
|
||||
|
||||
const result = await imageGenService.generateImage({
|
||||
prompt,
|
||||
filename,
|
||||
...(referenceImages && { referenceImages })
|
||||
...(referenceImages && { referenceImages }),
|
||||
});
|
||||
|
||||
// Log the result
|
||||
|
|
@ -80,47 +96,51 @@ generateRouter.post('/generate',
|
|||
success: result.success,
|
||||
model: result.model,
|
||||
filename: result.filename,
|
||||
hasError: !!result.error
|
||||
hasError: !!result.error,
|
||||
});
|
||||
|
||||
// Send response
|
||||
if (result.success) {
|
||||
const successResponse: GenerateImageResponse = {
|
||||
success: true,
|
||||
message: 'Image generated successfully',
|
||||
message: "Image generated successfully",
|
||||
data: {
|
||||
filename: result.filename!,
|
||||
filepath: result.filepath!,
|
||||
...(result.description && { description: result.description }),
|
||||
model: result.model,
|
||||
generatedAt: timestamp
|
||||
}
|
||||
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'
|
||||
message: "Image generation failed",
|
||||
error: result.error || "Unknown error occurred",
|
||||
};
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Sending error response: ${result.error}`);
|
||||
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);
|
||||
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'
|
||||
message: "Image generation failed",
|
||||
error:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
|
||||
return res.status(500).json(errorResponse);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,17 +1,21 @@
|
|||
import { GoogleGenAI } from '@google/genai';
|
||||
import mime from 'mime';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ImageGenerationOptions, ImageGenerationResult, ReferenceImage } from '../types/api';
|
||||
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';
|
||||
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');
|
||||
throw new Error("Gemini API key is required");
|
||||
}
|
||||
this.ai = new GoogleGenAI({ apiKey });
|
||||
}
|
||||
|
|
@ -19,21 +23,25 @@ export class ImageGenService {
|
|||
/**
|
||||
* Generate an image from text prompt with optional reference images
|
||||
*/
|
||||
async generateImage(options: ImageGenerationOptions): Promise<ImageGenerationResult> {
|
||||
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)}..."`);
|
||||
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'] },
|
||||
config: { responseModalities: ["IMAGE", "TEXT"] },
|
||||
prompt,
|
||||
filename,
|
||||
...(referenceImages && { referenceImages }),
|
||||
modelName: 'Nano Banana'
|
||||
modelName: "Nano Banana",
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
|
|
@ -41,23 +49,28 @@ export class ImageGenService {
|
|||
}
|
||||
|
||||
// Fallback to Imagen 4
|
||||
console.log(`[${new Date().toISOString()}] Primary model failed, trying fallback (Imagen 4)...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Primary model failed, trying fallback (Imagen 4)...`,
|
||||
);
|
||||
|
||||
return await this.tryGeneration({
|
||||
model: this.fallbackModel,
|
||||
config: { responseModalities: ['IMAGE'] },
|
||||
config: { responseModalities: ["IMAGE"] },
|
||||
prompt,
|
||||
filename: `${filename}_fallback`,
|
||||
...(referenceImages && { referenceImages }),
|
||||
modelName: 'Imagen 4'
|
||||
modelName: "Imagen 4",
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] Image generation failed:`, error);
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Image generation failed:`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
model: 'none',
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
model: "none",
|
||||
error:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -73,8 +86,8 @@ export class ImageGenService {
|
|||
referenceImages?: ReferenceImage[];
|
||||
modelName: string;
|
||||
}): Promise<ImageGenerationResult> {
|
||||
|
||||
const { model, config, prompt, filename, referenceImages, modelName } = params;
|
||||
const { model, config, prompt, filename, referenceImages, modelName } =
|
||||
params;
|
||||
|
||||
try {
|
||||
// Build content parts for the API request
|
||||
|
|
@ -82,64 +95,79 @@ export class ImageGenService {
|
|||
|
||||
// Add reference images if provided
|
||||
if (referenceImages && referenceImages.length > 0) {
|
||||
console.log(`[${new Date().toISOString()}] Adding ${referenceImages.length} reference image(s)`);
|
||||
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')
|
||||
}
|
||||
data: refImage.buffer.toString("base64"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add the text prompt
|
||||
contentParts.push({
|
||||
text: prompt
|
||||
text: prompt,
|
||||
});
|
||||
|
||||
const contents = [
|
||||
{
|
||||
role: 'user' as const,
|
||||
parts: contentParts
|
||||
}
|
||||
role: "user" as const,
|
||||
parts: contentParts,
|
||||
},
|
||||
];
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Making API request to ${modelName} (${model})...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Making API request to ${modelName} (${model})...`,
|
||||
);
|
||||
|
||||
const response = await this.ai.models.generateContent({
|
||||
model,
|
||||
config,
|
||||
contents
|
||||
contents,
|
||||
});
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Response received from ${modelName}`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Response received from ${modelName}`,
|
||||
);
|
||||
|
||||
if (response.candidates && response.candidates[0] && response.candidates[0].content) {
|
||||
if (
|
||||
response.candidates &&
|
||||
response.candidates[0] &&
|
||||
response.candidates[0].content
|
||||
) {
|
||||
const content = response.candidates[0].content;
|
||||
let generatedDescription = '';
|
||||
let savedImagePath = '';
|
||||
let generatedDescription = "";
|
||||
let savedImagePath = "";
|
||||
|
||||
for (let index = 0; index < (content.parts?.length || 0); index++) {
|
||||
const part = content.parts?.[index];
|
||||
if (!part) continue;
|
||||
|
||||
if (part.inlineData) {
|
||||
const fileExtension = mime.getExtension(part.inlineData.mimeType || '');
|
||||
const fileExtension = mime.getExtension(
|
||||
part.inlineData.mimeType || "",
|
||||
);
|
||||
const finalFilename = `${filename}.${fileExtension}`;
|
||||
const filepath = path.join('./results', finalFilename);
|
||||
const filepath = path.join("./results", finalFilename);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Saving image: ${finalFilename}`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Saving image: ${finalFilename}`,
|
||||
);
|
||||
|
||||
const buffer = Buffer.from(part.inlineData.data || '', 'base64');
|
||||
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)}...`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] Generated description: ${part.text.substring(0, 100)}...`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,7 +177,7 @@ export class ImageGenService {
|
|||
filename: path.basename(savedImagePath),
|
||||
filepath: savedImagePath,
|
||||
description: generatedDescription,
|
||||
model: modelName
|
||||
model: modelName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -157,15 +185,17 @@ export class ImageGenService {
|
|||
return {
|
||||
success: false,
|
||||
model: modelName,
|
||||
error: 'No image data received from API'
|
||||
error: "No image data received from API",
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[${new Date().toISOString()}] ${modelName} generation failed:`, error);
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ${modelName} generation failed:`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
model: modelName,
|
||||
error: error instanceof Error ? error.message : 'Generation failed'
|
||||
error: error instanceof Error ? error.message : "Generation failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -183,10 +213,15 @@ export class ImageGenService {
|
|||
|
||||
fs.writeFile(filepath, buffer, (err) => {
|
||||
if (err) {
|
||||
console.error(`[${new Date().toISOString()}] Error saving file ${filepath}:`, err);
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Error saving file ${filepath}:`,
|
||||
err,
|
||||
);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(`[${new Date().toISOString()}] File saved successfully: ${filepath}`);
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] File saved successfully: ${filepath}`,
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
|
@ -196,26 +231,29 @@ export class ImageGenService {
|
|||
/**
|
||||
* Validate reference images
|
||||
*/
|
||||
static validateReferenceImages(files: Express.Multer.File[]): { valid: boolean; error?: string } {
|
||||
static validateReferenceImages(files: Express.Multer.File[]): {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
} {
|
||||
if (files.length > 3) {
|
||||
return { valid: false, error: 'Maximum 3 reference images allowed' };
|
||||
return { valid: false, error: "Maximum 3 reference images allowed" };
|
||||
}
|
||||
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'];
|
||||
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`
|
||||
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`
|
||||
error: `File ${file.originalname} is too large. Maximum size: 5MB`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -226,11 +264,13 @@ export class ImageGenService {
|
|||
/**
|
||||
* Convert Express.Multer.File[] to ReferenceImage[]
|
||||
*/
|
||||
static convertFilesToReferenceImages(files: Express.Multer.File[]): ReferenceImage[] {
|
||||
return files.map(file => ({
|
||||
static convertFilesToReferenceImages(
|
||||
files: Express.Multer.File[],
|
||||
): ReferenceImage[] {
|
||||
return files.map((file) => ({
|
||||
buffer: file.buffer,
|
||||
mimetype: file.mimetype,
|
||||
originalname: file.originalname
|
||||
originalname: file.originalname,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Request } from 'express';
|
||||
import { Request } from "express";
|
||||
|
||||
// API Request/Response types
|
||||
export interface GenerateImageRequest {
|
||||
|
|
|
|||
Loading…
Reference in New Issue