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:
Oleg Proskurin 2025-09-21 22:51:51 +07:00
parent 251ac6e27c
commit 52549836e4
10 changed files with 313 additions and 204 deletions

View File

@ -1,4 +1,4 @@
export default { module.exports = {
semi: true, semi: true,
trailingComma: 'es5', trailingComma: 'es5',
singleQuote: true, singleQuote: true,

View File

@ -1,14 +1,8 @@
import js from '@eslint/js'; module.exports = [
import typescript from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import prettier from 'eslint-plugin-prettier';
export default [
js.configs.recommended,
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
languageOptions: { languageOptions: {
parser: typescriptParser, parser: require('@typescript-eslint/parser'),
parserOptions: { parserOptions: {
ecmaVersion: 2022, ecmaVersion: 2022,
sourceType: 'module', sourceType: 'module',
@ -16,12 +10,10 @@ export default [
}, },
}, },
plugins: { plugins: {
'@typescript-eslint': typescript, '@typescript-eslint': require('@typescript-eslint/eslint-plugin'),
prettier: prettier, prettier: require('eslint-plugin-prettier'),
}, },
rules: { rules: {
...typescript.configs.recommended.rules,
...typescript.configs['recommended-requiring-type-checking'].rules,
'prettier/prettier': 'error', 'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'warn', '@typescript-eslint/explicit-function-return-type': 'warn',
@ -34,6 +26,8 @@ export default [
'object-shorthand': 'error', 'object-shorthand': 'error',
'prefer-template': 'error', 'prefer-template': 'error',
}, },
},
{
ignores: [ ignores: [
'node_modules/**', 'node_modules/**',
'dist/**', 'dist/**',

View File

@ -40,19 +40,20 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"express": "^5.1.0", "express": "^5.1.0",
"mime": "^4.1.0",
"multer": "^2.0.2",
"helmet": "^8.0.0",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.4.1",
"express-validator": "^7.2.0", "express-validator": "^7.2.0",
"helmet": "^8.0.0",
"mime": "^4.1.0",
"multer": "^2.0.2",
"winston": "^3.17.0" "winston": "^3.17.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/jest": "^29.5.14",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/node": "^24.3.1", "@types/node": "^24.3.1",
"@types/jest": "^29.5.14",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/eslint-plugin": "^8.18.2",
"@typescript-eslint/parser": "^8.18.2", "@typescript-eslint/parser": "^8.18.2",

View File

@ -39,6 +39,9 @@ importers:
specifier: ^3.17.0 specifier: ^3.17.0
version: 3.17.0 version: 3.17.0
devDependencies: devDependencies:
'@eslint/js':
specifier: ^9.36.0
version: 9.36.0
'@types/cors': '@types/cors':
specifier: ^2.8.19 specifier: ^2.8.19
version: 2.8.19 version: 2.8.19

View File

@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from "express";
import { GenerateImageResponse } from '../types/api'; import { GenerateImageResponse } from "../types/api";
/** /**
* Global error handler for the Express application * Global error handler for the Express application
@ -8,10 +8,10 @@ export const errorHandler = (
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction,
) => { ) => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const requestId = req.requestId || 'unknown'; const requestId = req.requestId || "unknown";
// Log the error // Log the error
console.error(`[${timestamp}] [${requestId}] ERROR:`, { console.error(`[${timestamp}] [${requestId}] ERROR:`, {
@ -20,7 +20,7 @@ export const errorHandler = (
path: req.path, path: req.path,
method: req.method, method: req.method,
body: req.body, body: req.body,
query: req.query query: req.query,
}); });
// Don't send error response if headers already sent // Don't send error response if headers already sent
@ -30,49 +30,63 @@ export const errorHandler = (
// Determine error type and status code // Determine error type and status code
let statusCode = 500; let statusCode = 500;
let errorMessage = 'Internal server error'; let errorMessage = "Internal server error";
let errorType = 'INTERNAL_ERROR'; let errorType = "INTERNAL_ERROR";
if (error.name === 'ValidationError') { if (error.name === "ValidationError") {
statusCode = 400; statusCode = 400;
errorMessage = error.message; errorMessage = error.message;
errorType = 'VALIDATION_ERROR'; errorType = "VALIDATION_ERROR";
} else if (error.message.includes('API key') || error.message.includes('authentication')) { } else if (
error.message.includes("API key") ||
error.message.includes("authentication")
) {
statusCode = 401; statusCode = 401;
errorMessage = 'Authentication failed'; errorMessage = "Authentication failed";
errorType = 'AUTH_ERROR'; errorType = "AUTH_ERROR";
} else if (error.message.includes('not found') || error.message.includes('404')) { } else if (
error.message.includes("not found") ||
error.message.includes("404")
) {
statusCode = 404; statusCode = 404;
errorMessage = 'Resource not found'; errorMessage = "Resource not found";
errorType = 'NOT_FOUND'; errorType = "NOT_FOUND";
} else if (error.message.includes('timeout') || error.message.includes('503')) { } else if (
error.message.includes("timeout") ||
error.message.includes("503")
) {
statusCode = 503; statusCode = 503;
errorMessage = 'Service temporarily unavailable'; errorMessage = "Service temporarily unavailable";
errorType = 'SERVICE_UNAVAILABLE'; errorType = "SERVICE_UNAVAILABLE";
} else if (error.message.includes('overloaded') || error.message.includes('rate limit')) { } else if (
error.message.includes("overloaded") ||
error.message.includes("rate limit")
) {
statusCode = 429; statusCode = 429;
errorMessage = 'Service overloaded, please try again later'; errorMessage = "Service overloaded, please try again later";
errorType = 'RATE_LIMITED'; errorType = "RATE_LIMITED";
} }
// Create error response // Create error response
const errorResponse: GenerateImageResponse = { const errorResponse: GenerateImageResponse = {
success: false, success: false,
message: 'Request failed', message: "Request failed",
error: errorMessage error: errorMessage,
}; };
// Add additional debug info in development // Add additional debug info in development
if (process.env['NODE_ENV'] === 'development') { if (process.env["NODE_ENV"] === "development") {
(errorResponse as any).debug = { (errorResponse as any).debug = {
originalError: error.message, originalError: error.message,
errorType, errorType,
requestId, 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); res.status(statusCode).json(errorResponse);
}; };
@ -82,14 +96,16 @@ export const errorHandler = (
*/ */
export const notFoundHandler = (req: Request, res: Response) => { export const notFoundHandler = (req: Request, res: Response) => {
const timestamp = new Date().toISOString(); 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 = { const notFoundResponse: GenerateImageResponse = {
success: false, success: false,
message: 'Route not found', message: "Route not found",
error: `The requested endpoint ${req.method} ${req.path} does not exist` error: `The requested endpoint ${req.method} ${req.path} does not exist`,
}; };
res.status(404).json(notFoundResponse); 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 * Async error wrapper to catch errors in async route handlers
*/ */
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => { export const asyncHandler =
Promise.resolve(fn(req, res, next)).catch(next); (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
}; Promise.resolve(fn(req, res, next)).catch(next);
};

View File

@ -1,24 +1,31 @@
import multer from 'multer'; import multer from "multer";
import { Request, RequestHandler } from 'express'; import { Request, RequestHandler } from "express";
// Configure multer for memory storage (we'll process files in memory) // Configure multer for memory storage (we'll process files in memory)
const storage = multer.memoryStorage(); const storage = multer.memoryStorage();
// File filter for image types only // File filter for image types only
const fileFilter = (_req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => { const fileFilter = (
const allowedTypes = [ _req: Request,
'image/png', file: Express.Multer.File,
'image/jpeg', cb: multer.FileFilterCallback,
'image/jpg', ) => {
'image/webp' const allowedTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
];
if (allowedTypes.includes(file.mimetype)) { 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); cb(null, true);
} else { } else {
console.log(`[${new Date().toISOString()}] Rejected file: ${file.originalname} (${file.mimetype})`); console.log(
cb(new Error(`Unsupported file type: ${file.mimetype}. Allowed: PNG, JPEG, WebP`)); `[${new Date().toISOString()}] Rejected file: ${file.originalname} (${file.mimetype})`,
);
cb(
new Error(
`Unsupported file type: ${file.mimetype}. Allowed: PNG, JPEG, WebP`,
),
);
} }
}; };
@ -31,59 +38,68 @@ export const upload = multer({
storage: storage, storage: storage,
limits: { limits: {
fileSize: MAX_FILE_SIZE, // 5MB per file fileSize: MAX_FILE_SIZE, // 5MB per file
files: MAX_FILES, // Maximum 3 files files: MAX_FILES, // Maximum 3 files
}, },
fileFilter: fileFilter fileFilter: fileFilter,
}); });
// Middleware for handling reference images // 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 // 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) { if (error instanceof multer.MulterError) {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
console.error(`[${timestamp}] Multer error:`, error.message); console.error(`[${timestamp}] Multer error:`, error.message);
switch (error.code) { switch (error.code) {
case 'LIMIT_FILE_SIZE': case "LIMIT_FILE_SIZE":
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: `File too large. Maximum size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`, 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({ return res.status(400).json({
success: false, success: false,
error: `Too many files. Maximum: ${MAX_FILES} files`, 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({ return res.status(400).json({
success: false, success: false,
error: 'Unexpected file field. Use "referenceImages" for image uploads', error:
message: 'File upload failed' 'Unexpected file field. Use "referenceImages" for image uploads',
message: "File upload failed",
}); });
default: default:
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
error: error.message, 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({ return res.status(400).json({
success: false, success: false,
error: error.message, error: error.message,
message: 'File validation failed' message: "File validation failed",
}); });
} }
// Pass other errors to the next error handler // Pass other errors to the next error handler
next(error); next(error);
}; };

View File

@ -1,18 +1,18 @@
import { Response, NextFunction } from 'express'; import { Response, NextFunction } from "express";
// Validation rules // Validation rules
const VALIDATION_RULES = { const VALIDATION_RULES = {
prompt: { prompt: {
minLength: 3, minLength: 3,
maxLength: 2000, maxLength: 2000,
required: true required: true,
}, },
filename: { filename: {
minLength: 1, minLength: 1,
maxLength: 100, maxLength: 100,
required: true, required: true,
pattern: /^[a-zA-Z0-9_-]+$/ // Only alphanumeric, underscore, hyphen pattern: /^[a-zA-Z0-9_-]+$/, // Only alphanumeric, underscore, hyphen
} },
}; };
/** /**
@ -20,10 +20,10 @@ const VALIDATION_RULES = {
*/ */
export const sanitizeFilename = (filename: string): string => { export const sanitizeFilename = (filename: string): string => {
return filename return filename
.replace(/[^a-zA-Z0-9_-]/g, '_') // Replace invalid chars with underscore .replace(/[^a-zA-Z0-9_-]/g, "_") // Replace invalid chars with underscore
.replace(/_{2,}/g, '_') // Replace multiple underscores with single .replace(/_{2,}/g, "_") // Replace multiple underscores with single
.replace(/^_+|_+$/g, '') // Remove leading/trailing underscores .replace(/^_+|_+$/g, "") // Remove leading/trailing underscores
.substring(0, 100); // Limit length .substring(0, 100); // Limit length
}; };
/** /**
@ -32,7 +32,7 @@ export const sanitizeFilename = (filename: string): string => {
export const validateGenerateRequest = ( export const validateGenerateRequest = (
req: any, req: any,
res: Response, res: Response,
next: NextFunction next: NextFunction,
): void | Response => { ): void | Response => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const { prompt, filename } = req.body; const { prompt, filename } = req.body;
@ -42,26 +42,34 @@ export const validateGenerateRequest = (
// Validate prompt // Validate prompt
if (!prompt) { if (!prompt) {
errors.push('Prompt is required'); errors.push("Prompt is required");
} else if (typeof prompt !== 'string') { } else if (typeof prompt !== "string") {
errors.push('Prompt must be a string'); errors.push("Prompt must be a string");
} else if (prompt.trim().length < VALIDATION_RULES.prompt.minLength) { } 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) { } 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 // Validate filename
if (!filename) { if (!filename) {
errors.push('Filename is required'); errors.push("Filename is required");
} else if (typeof filename !== 'string') { } else if (typeof filename !== "string") {
errors.push('Filename must be a string'); errors.push("Filename must be a string");
} else if (filename.trim().length < VALIDATION_RULES.filename.minLength) { } 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) { } 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)) { } 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 // Check for XSS attempts in prompt
@ -71,20 +79,22 @@ export const validateGenerateRequest = (
/on\w+\s*=/i, /on\w+\s*=/i,
/<iframe/i, /<iframe/i,
/<object/i, /<object/i,
/<embed/i /<embed/i,
]; ];
if (prompt && xssPatterns.some(pattern => pattern.test(prompt))) { if (prompt && xssPatterns.some((pattern) => pattern.test(prompt))) {
errors.push('Invalid characters detected in prompt'); errors.push("Invalid characters detected in prompt");
} }
// Log validation results // Log validation results
if (errors.length > 0) { 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({ return res.status(400).json({
success: false, success: false,
error: 'Validation failed', error: "Validation failed",
message: errors.join(', ') message: errors.join(", "),
}); });
} }
@ -92,7 +102,9 @@ export const validateGenerateRequest = (
if (filename) { if (filename) {
req.body.filename = sanitizeFilename(filename.trim()); req.body.filename = sanitizeFilename(filename.trim());
if (req.body.filename !== 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,25 +123,31 @@ export const validateGenerateRequest = (
export const logRequestDetails = ( export const logRequestDetails = (
req: any, req: any,
_res: Response, _res: Response,
next: NextFunction next: NextFunction,
): void => { ): void => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const { prompt, filename } = req.body; 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}] === REQUEST DETAILS ===`);
console.log(`[${timestamp}] [${req.requestId}] Method: ${req.method}`); console.log(`[${timestamp}] [${req.requestId}] Method: ${req.method}`);
console.log(`[${timestamp}] [${req.requestId}] Path: ${req.path}`); 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}] Filename: "${filename}"`);
console.log(`[${timestamp}] [${req.requestId}] Reference files: ${files.length}`); console.log(
`[${timestamp}] [${req.requestId}] Reference files: ${files.length}`,
);
if (files.length > 0) { if (files.length > 0) {
files.forEach((file, index) => { 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)`,
);
}); });
} }
console.log(`[${timestamp}] [${req.requestId}] ===========================`); console.log(`[${timestamp}] [${req.requestId}] ===========================`);
next(); next();
}; };

View File

@ -1,10 +1,16 @@
import { Response, Router } from 'express'; import { Response, Router } from "express";
import type { Router as RouterType } from 'express'; import type { Router as RouterType } from "express";
import { ImageGenService } from '../services/ImageGenService'; import { ImageGenService } from "../services/ImageGenService";
import { uploadReferenceImages, handleUploadErrors } from '../middleware/upload'; import {
import { validateGenerateRequest, logRequestDetails } from '../middleware/validation'; uploadReferenceImages,
import { asyncHandler } from '../middleware/errorHandler'; handleUploadErrors,
import { GenerateImageResponse } from '../types/api'; } from "../middleware/upload";
import {
validateGenerateRequest,
logRequestDetails,
} from "../middleware/validation";
import { asyncHandler } from "../middleware/errorHandler";
import { GenerateImageResponse } from "../types/api";
// Create router // Create router
export const generateRouter: RouterType = 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 * POST /api/generate - Generate image from text prompt with optional reference images
*/ */
generateRouter.post('/generate', generateRouter.post(
"/generate",
// File upload middleware // File upload middleware
uploadReferenceImages, uploadReferenceImages,
handleUploadErrors, handleUploadErrors,
@ -27,12 +34,12 @@ generateRouter.post('/generate',
asyncHandler(async (req: any, res: Response) => { asyncHandler(async (req: any, res: Response) => {
// Initialize service if not already done // Initialize service if not already done
if (!imageGenService) { if (!imageGenService) {
const apiKey = process.env['GEMINI_API_KEY']; const apiKey = process.env["GEMINI_API_KEY"];
if (!apiKey) { if (!apiKey) {
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: 'Server configuration error', message: "Server configuration error",
error: 'GEMINI_API_KEY not configured' error: "GEMINI_API_KEY not configured",
} as GenerateImageResponse); } as GenerateImageResponse);
} }
imageGenService = new ImageGenService(apiKey); imageGenService = new ImageGenService(apiKey);
@ -41,38 +48,47 @@ generateRouter.post('/generate',
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const requestId = req.requestId; const requestId = req.requestId;
const { prompt, filename } = req.body; 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 { try {
// Validate reference images if provided // Validate reference images if provided
if (files.length > 0) { if (files.length > 0) {
const validation = ImageGenService.validateReferenceImages(files); const validation = ImageGenService.validateReferenceImages(files);
if (!validation.valid) { 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({ return res.status(400).json({
success: false, success: false,
message: 'Reference image validation failed', message: "Reference image validation failed",
error: validation.error error: validation.error,
} as GenerateImageResponse); } as GenerateImageResponse);
} }
console.log(`[${timestamp}] [${requestId}] Reference images validation passed`); console.log(
`[${timestamp}] [${requestId}] Reference images validation passed`,
);
} }
// Convert files to reference images // Convert files to reference images
const referenceImages = files.length > 0 const referenceImages =
? ImageGenService.convertFilesToReferenceImages(files) files.length > 0
: undefined; ? ImageGenService.convertFilesToReferenceImages(files)
: undefined;
// Generate the image // Generate the image
console.log(`[${timestamp}] [${requestId}] Calling ImageGenService.generateImage()`); console.log(
`[${timestamp}] [${requestId}] Calling ImageGenService.generateImage()`,
);
const result = await imageGenService.generateImage({ const result = await imageGenService.generateImage({
prompt, prompt,
filename, filename,
...(referenceImages && { referenceImages }) ...(referenceImages && { referenceImages }),
}); });
// Log the result // Log the result
@ -80,47 +96,51 @@ generateRouter.post('/generate',
success: result.success, success: result.success,
model: result.model, model: result.model,
filename: result.filename, filename: result.filename,
hasError: !!result.error hasError: !!result.error,
}); });
// Send response // Send response
if (result.success) { if (result.success) {
const successResponse: GenerateImageResponse = { const successResponse: GenerateImageResponse = {
success: true, success: true,
message: 'Image generated successfully', message: "Image generated successfully",
data: { data: {
filename: result.filename!, filename: result.filename!,
filepath: result.filepath!, filepath: result.filepath!,
...(result.description && { description: result.description }), ...(result.description && { description: result.description }),
model: result.model, model: result.model,
generatedAt: timestamp generatedAt: timestamp,
} },
}; };
console.log(`[${timestamp}] [${requestId}] Sending success response`); console.log(`[${timestamp}] [${requestId}] Sending success response`);
return res.status(200).json(successResponse); return res.status(200).json(successResponse);
} else { } else {
const errorResponse: GenerateImageResponse = { const errorResponse: GenerateImageResponse = {
success: false, success: false,
message: 'Image generation failed', message: "Image generation failed",
error: result.error || 'Unknown error occurred' 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); return res.status(500).json(errorResponse);
} }
} catch (error) { } 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 = { const errorResponse: GenerateImageResponse = {
success: false, success: false,
message: 'Image generation failed', message: "Image generation failed",
error: error instanceof Error ? error.message : 'Unknown error occurred' error:
error instanceof Error ? error.message : "Unknown error occurred",
}; };
return res.status(500).json(errorResponse); return res.status(500).json(errorResponse);
} }
}) }),
); );

View File

@ -1,17 +1,21 @@
import { GoogleGenAI } from '@google/genai'; import { GoogleGenAI } from "@google/genai";
import mime from 'mime'; import mime from "mime";
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
import { ImageGenerationOptions, ImageGenerationResult, ReferenceImage } from '../types/api'; import {
ImageGenerationOptions,
ImageGenerationResult,
ReferenceImage,
} from "../types/api";
export class ImageGenService { export class ImageGenService {
private ai: GoogleGenAI; private ai: GoogleGenAI;
private primaryModel = 'gemini-2.5-flash-image-preview'; private primaryModel = "gemini-2.5-flash-image-preview";
private fallbackModel = 'imagen-4.0-generate-001'; private fallbackModel = "imagen-4.0-generate-001";
constructor(apiKey: string) { constructor(apiKey: string) {
if (!apiKey) { if (!apiKey) {
throw new Error('Gemini API key is required'); throw new Error("Gemini API key is required");
} }
this.ai = new GoogleGenAI({ apiKey }); this.ai = new GoogleGenAI({ apiKey });
} }
@ -19,21 +23,25 @@ export class ImageGenService {
/** /**
* Generate an image from text prompt with optional reference images * 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 { prompt, filename, referenceImages } = options;
const timestamp = new Date().toISOString(); 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 { try {
// First try the primary model (Nano Banana) // First try the primary model (Nano Banana)
const result = await this.tryGeneration({ const result = await this.tryGeneration({
model: this.primaryModel, model: this.primaryModel,
config: { responseModalities: ['IMAGE', 'TEXT'] }, config: { responseModalities: ["IMAGE", "TEXT"] },
prompt, prompt,
filename, filename,
...(referenceImages && { referenceImages }), ...(referenceImages && { referenceImages }),
modelName: 'Nano Banana' modelName: "Nano Banana",
}); });
if (result.success) { if (result.success) {
@ -41,23 +49,28 @@ export class ImageGenService {
} }
// Fallback to Imagen 4 // 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({ return await this.tryGeneration({
model: this.fallbackModel, model: this.fallbackModel,
config: { responseModalities: ['IMAGE'] }, config: { responseModalities: ["IMAGE"] },
prompt, prompt,
filename: `${filename}_fallback`, filename: `${filename}_fallback`,
...(referenceImages && { referenceImages }), ...(referenceImages && { referenceImages }),
modelName: 'Imagen 4' modelName: "Imagen 4",
}); });
} catch (error) { } catch (error) {
console.error(`[${new Date().toISOString()}] Image generation failed:`, error); console.error(
`[${new Date().toISOString()}] Image generation failed:`,
error,
);
return { return {
success: false, success: false,
model: 'none', model: "none",
error: error instanceof Error ? error.message : 'Unknown error occurred' error:
error instanceof Error ? error.message : "Unknown error occurred",
}; };
} }
} }
@ -73,8 +86,8 @@ export class ImageGenService {
referenceImages?: ReferenceImage[]; referenceImages?: ReferenceImage[];
modelName: string; modelName: string;
}): Promise<ImageGenerationResult> { }): Promise<ImageGenerationResult> {
const { model, config, prompt, filename, referenceImages, modelName } =
const { model, config, prompt, filename, referenceImages, modelName } = params; params;
try { try {
// Build content parts for the API request // Build content parts for the API request
@ -82,64 +95,79 @@ export class ImageGenService {
// Add reference images if provided // Add reference images if provided
if (referenceImages && referenceImages.length > 0) { 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) { for (const refImage of referenceImages) {
contentParts.push({ contentParts.push({
inlineData: { inlineData: {
mimeType: refImage.mimetype, mimeType: refImage.mimetype,
data: refImage.buffer.toString('base64') data: refImage.buffer.toString("base64"),
} },
}); });
} }
} }
// Add the text prompt // Add the text prompt
contentParts.push({ contentParts.push({
text: prompt text: prompt,
}); });
const contents = [ const contents = [
{ {
role: 'user' as const, role: "user" as const,
parts: contentParts 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({ const response = await this.ai.models.generateContent({
model, model,
config, 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; const content = response.candidates[0].content;
let generatedDescription = ''; let generatedDescription = "";
let savedImagePath = ''; let savedImagePath = "";
for (let index = 0; index < (content.parts?.length || 0); index++) { for (let index = 0; index < (content.parts?.length || 0); index++) {
const part = content.parts?.[index]; const part = content.parts?.[index];
if (!part) continue; if (!part) continue;
if (part.inlineData) { if (part.inlineData) {
const fileExtension = mime.getExtension(part.inlineData.mimeType || ''); const fileExtension = mime.getExtension(
part.inlineData.mimeType || "",
);
const finalFilename = `${filename}.${fileExtension}`; 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); await this.saveImageFile(filepath, buffer);
savedImagePath = filepath; savedImagePath = filepath;
} else if (part.text) { } else if (part.text) {
generatedDescription = 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), filename: path.basename(savedImagePath),
filepath: savedImagePath, filepath: savedImagePath,
description: generatedDescription, description: generatedDescription,
model: modelName model: modelName,
}; };
} }
} }
@ -157,15 +185,17 @@ export class ImageGenService {
return { return {
success: false, success: false,
model: modelName, model: modelName,
error: 'No image data received from API' error: "No image data received from API",
}; };
} catch (error) { } catch (error) {
console.error(`[${new Date().toISOString()}] ${modelName} generation failed:`, error); console.error(
`[${new Date().toISOString()}] ${modelName} generation failed:`,
error,
);
return { return {
success: false, success: false,
model: modelName, 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) => { fs.writeFile(filepath, buffer, (err) => {
if (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); reject(err);
} else { } else {
console.log(`[${new Date().toISOString()}] File saved successfully: ${filepath}`); console.log(
`[${new Date().toISOString()}] File saved successfully: ${filepath}`,
);
resolve(); resolve();
} }
}); });
@ -196,26 +231,29 @@ export class ImageGenService {
/** /**
* Validate reference images * 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) { 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 const maxSize = 5 * 1024 * 1024; // 5MB
for (const file of files) { for (const file of files) {
if (!allowedTypes.includes(file.mimetype)) { if (!allowedTypes.includes(file.mimetype)) {
return { return {
valid: false, 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) { if (file.size > maxSize) {
return { return {
valid: false, 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[] * Convert Express.Multer.File[] to ReferenceImage[]
*/ */
static convertFilesToReferenceImages(files: Express.Multer.File[]): ReferenceImage[] { static convertFilesToReferenceImages(
return files.map(file => ({ files: Express.Multer.File[],
): ReferenceImage[] {
return files.map((file) => ({
buffer: file.buffer, buffer: file.buffer,
mimetype: file.mimetype, mimetype: file.mimetype,
originalname: file.originalname originalname: file.originalname,
})); }));
} }
} }

View File

@ -1,4 +1,4 @@
import { Request } from 'express'; import { Request } from "express";
// API Request/Response types // API Request/Response types
export interface GenerateImageRequest { export interface GenerateImageRequest {
@ -63,4 +63,4 @@ export interface Config {
uploadsDir: string; uploadsDir: string;
maxFileSize: number; maxFileSize: number;
maxFiles: number; maxFiles: number;
} }