diff --git a/docs/api/README.md b/docs/api/README.md index 60b60ef..bea7ab5 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -14,7 +14,7 @@ API key required via `GEMINI_API_KEY` environment variable (server-side configur ## Content Types -- **Request**: `multipart/form-data` for file uploads, `application/json` for other endpoints +- **Request**: `multipart/form-data` for file uploads, `application/json` for JSON endpoints - **Response**: `application/json` ## Rate Limits @@ -60,6 +60,7 @@ Returns API metadata and configuration limits. "GET /health": "Health check", "GET /api/info": "API information", "POST /api/generate": "Generate images from text prompt with optional reference images", + "POST /api/text-to-image": "Generate images from text prompt only (JSON)", "POST /api/enhance": "Enhance and optimize prompts for better image generation" }, "limits": { @@ -144,6 +145,92 @@ curl -X POST http://localhost:3000/api/generate \ --- +### Text-to-Image (JSON) + +#### `POST /api/text-to-image` + +Generate images from text prompts only using JSON payload. Simplified endpoint for text-only requests without file uploads. + +**Content-Type:** `application/json` + +**Request Body:** +```json +{ + "prompt": "A beautiful sunset over mountains", + "filename": "sunset_image", + "autoEnhance": true, + "enhancementOptions": { + "imageStyle": "photorealistic", + "aspectRatio": "landscape", + "mood": "peaceful", + "lighting": "golden hour" + } +} +``` + +**Parameters:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `prompt` | string | Yes | Text description of the image to generate (3-2000 chars) | +| `filename` | string | Yes | Desired filename for the generated image (alphanumeric, underscore, hyphen only) | +| `autoEnhance` | boolean | No | Enable automatic prompt enhancement | +| `enhancementOptions` | object | No | Enhancement configuration options (same as /api/generate) | + +**Example Request:** +```bash +curl -X POST http://localhost:3000/api/text-to-image \ + -H "Content-Type: application/json" \ + -d '{ + "prompt": "A beautiful sunset over mountains with golden clouds", + "filename": "test_sunset", + "autoEnhance": true, + "enhancementOptions": { + "imageStyle": "photorealistic", + "aspectRatio": "landscape" + } + }' +``` + +**Success Response (200):** +```json +{ + "success": true, + "message": "Image generated successfully", + "data": { + "filename": "test_sunset.png", + "filepath": "results/test_sunset.png", + "description": "Here's a beautiful sunset over mountains with golden clouds for you!", + "model": "Nano Banana", + "generatedAt": "2025-09-26T15:04:27.705Z", + "promptEnhancement": { + "originalPrompt": "A beautiful sunset over mountains", + "enhancedPrompt": "A breathtaking photorealistic sunset over majestic mountains...", + "detectedLanguage": "English", + "appliedTemplate": "landscape", + "enhancements": ["lighting_enhancement", "composition_improvement"] + } + } +} +``` + +**Error Response (400/500):** +```json +{ + "success": false, + "message": "Validation failed", + "error": "Prompt is required" +} +``` + +**Key Differences from /api/generate:** +- **JSON only**: No file upload support +- **Faster**: No multipart parsing overhead +- **Simpler testing**: Easy to use with curl or API clients +- **Same features**: Supports all enhancement options + +--- + ### Enhance Prompt #### `POST /api/enhance` diff --git a/docs/api/api.rest b/docs/api/api.rest index c71637c..715801b 100644 --- a/docs/api/api.rest +++ b/docs/api/api.rest @@ -23,6 +23,25 @@ Content-Type: application/json } } + +### Generate image from text + +POST {{base}}/api/text-to-image +Content-Type: application/json + +{ + "prompt": "банановый стимпанк. много стимпанк машин и меаханизмов посвященных бананм и работающих на бананах. банановая феерия", + "filename": "banatie-party", + "autoEnhance": true, + "enhancementOptions": { + "imageStyle": "photorealistic", + "aspectRatio": "landscape", + "mood": "peaceful", + "lighting": "golden hour" + } +} + + ### Generate Image with Files POST {{base}}/api/generate Content-Type: multipart/form-data; boundary=----WebKitFormBoundary diff --git a/src/app.ts b/src/app.ts index 818b9b9..5a8b77f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,6 +4,7 @@ import { config } from 'dotenv'; import { Config } from './types/api'; import { generateRouter } from './routes/generate'; import { enhanceRouter } from './routes/enhance'; +import { textToImageRouter } from './routes/textToImage'; import { errorHandler, notFoundHandler } from './middleware/errorHandler'; // Load environment variables @@ -63,6 +64,7 @@ export const createApp = (): Application => { 'GET /health': 'Health check', 'GET /api/info': 'API information', 'POST /api/generate': 'Generate images from text prompt with optional reference images', + 'POST /api/text-to-image': 'Generate images from text prompt only (JSON)', 'POST /api/enhance': 'Enhance and optimize prompts for better image generation' }, limits: { @@ -79,6 +81,7 @@ export const createApp = (): Application => { // Mount API routes app.use('/api', generateRouter); app.use('/api', enhanceRouter); + app.use('/api', textToImageRouter); // Error handling middleware (must be last) app.use(notFoundHandler); diff --git a/src/middleware/jsonValidation.ts b/src/middleware/jsonValidation.ts new file mode 100644 index 0000000..1461375 --- /dev/null +++ b/src/middleware/jsonValidation.ts @@ -0,0 +1,243 @@ +import { Response, NextFunction } from "express"; +import { sanitizeFilename } from "./validation"; + +// Validation rules (same as existing validation but for JSON) +const VALIDATION_RULES = { + prompt: { + minLength: 3, + maxLength: 2000, + required: true, + }, + filename: { + minLength: 1, + maxLength: 100, + required: true, + pattern: /^[a-zA-Z0-9_-]+$/, // Only alphanumeric, underscore, hyphen + }, +}; + +/** + * Validate the text-to-image JSON request + */ +export const validateTextToImageRequest = ( + req: any, + res: Response, + next: NextFunction, +): void | Response => { + const timestamp = new Date().toISOString(); + const { prompt, filename, autoEnhance, enhancementOptions } = req.body; + const errors: string[] = []; + + console.log( + `[${timestamp}] [${req.requestId}] Validating text-to-image JSON request`, + ); + + // Validate that request body exists + if (!req.body || typeof req.body !== "object") { + return res.status(400).json({ + success: false, + error: "Request body must be valid JSON", + message: "Invalid request format", + }); + } + + // Validate prompt + if (!prompt) { + errors.push("Prompt is required"); + } else if (typeof prompt !== "string") { + errors.push("Prompt must be a string"); + } else if (prompt.trim().length < VALIDATION_RULES.prompt.minLength) { + errors.push( + `Prompt must be at least ${VALIDATION_RULES.prompt.minLength} characters`, + ); + } else if (prompt.length > VALIDATION_RULES.prompt.maxLength) { + errors.push( + `Prompt must be less than ${VALIDATION_RULES.prompt.maxLength} characters`, + ); + } + + // Validate filename + if (!filename) { + errors.push("Filename is required"); + } else if (typeof filename !== "string") { + errors.push("Filename must be a string"); + } else if (filename.trim().length < VALIDATION_RULES.filename.minLength) { + errors.push("Filename cannot be empty"); + } else if (filename.length > VALIDATION_RULES.filename.maxLength) { + errors.push( + `Filename must be less than ${VALIDATION_RULES.filename.maxLength} characters`, + ); + } else if (!VALIDATION_RULES.filename.pattern.test(filename)) { + errors.push( + "Filename can only contain letters, numbers, underscores, and hyphens", + ); + } + + // Validate autoEnhance (optional boolean) + if (autoEnhance !== undefined && typeof autoEnhance !== "boolean") { + errors.push("autoEnhance must be a boolean"); + } + + // Validate enhancementOptions (optional object) + if (enhancementOptions !== undefined) { + if ( + typeof enhancementOptions !== "object" || + Array.isArray(enhancementOptions) + ) { + errors.push("enhancementOptions must be an object"); + } else { + const { + imageStyle, + aspectRatio, + mood, + lighting, + cameraAngle, + negativePrompts, + } = enhancementOptions; + + if ( + imageStyle !== undefined && + ![ + "photorealistic", + "illustration", + "minimalist", + "sticker", + "product", + "comic", + ].includes(imageStyle) + ) { + errors.push("Invalid imageStyle in enhancementOptions"); + } + + if ( + aspectRatio !== undefined && + !["square", "portrait", "landscape", "wide", "ultrawide"].includes( + aspectRatio, + ) + ) { + errors.push("Invalid aspectRatio in enhancementOptions"); + } + + if ( + mood !== undefined && + (typeof mood !== "string" || mood.length > 100) + ) { + errors.push("mood must be a string with max 100 characters"); + } + + if ( + lighting !== undefined && + (typeof lighting !== "string" || lighting.length > 100) + ) { + errors.push("lighting must be a string with max 100 characters"); + } + + if ( + cameraAngle !== undefined && + (typeof cameraAngle !== "string" || cameraAngle.length > 100) + ) { + errors.push("cameraAngle must be a string with max 100 characters"); + } + + if (negativePrompts !== undefined) { + if (!Array.isArray(negativePrompts) || negativePrompts.length > 10) { + errors.push("negativePrompts must be an array with max 10 items"); + } else { + for (const item of negativePrompts) { + if (typeof item !== "string" || item.length > 100) { + errors.push( + "Each negative prompt must be a string with max 100 characters", + ); + break; + } + } + } + } + } + } + + // Check for XSS attempts in prompt + const xssPatterns = [ + /