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,
trailingComma: 'es5',
singleQuote: true,

View File

@ -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/**',

View File

@ -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",

View File

@ -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

View File

@ -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) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
export const asyncHandler =
(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 { 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`,
),
);
}
};
@ -31,59 +38,68 @@ export const upload = multer({
storage: storage,
limits: {
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
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",
});
}
// Pass other errors to the next error handler
next(error);
};
};

View File

@ -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,10 +20,10 @@ 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
.substring(0, 100); // Limit length
.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,25 +123,31 @@ 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)`,
);
});
}
console.log(`[${timestamp}] [${req.requestId}] ===========================`);
next();
};
};

View File

@ -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
? ImageGenService.convertFilesToReferenceImages(files)
: undefined;
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);
}
})
);
}),
);

View File

@ -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,
}));
}
}
}

View File

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