feat: add text endpoint
This commit is contained in:
parent
d55eba8817
commit
1c6dfc4f8b
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
/<script/i,
|
||||
/javascript:/i,
|
||||
/on\w+\s*=/i,
|
||||
/<iframe/i,
|
||||
/<object/i,
|
||||
/<embed/i,
|
||||
];
|
||||
|
||||
if (prompt && xssPatterns.some((pattern) => pattern.test(prompt))) {
|
||||
errors.push("Invalid characters detected in prompt");
|
||||
}
|
||||
|
||||
// Log validation results
|
||||
if (errors.length > 0) {
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] Validation failed: ${errors.join(", ")}`,
|
||||
);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Validation failed",
|
||||
message: errors.join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
if (filename) {
|
||||
req.body.filename = sanitizeFilename(filename.trim());
|
||||
if (req.body.filename !== filename.trim()) {
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] Filename sanitized: "${filename}" -> "${req.body.filename}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Trim and clean prompt
|
||||
if (prompt) {
|
||||
req.body.prompt = prompt.trim();
|
||||
}
|
||||
|
||||
console.log(`[${timestamp}] [${req.requestId}] JSON validation passed`);
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Log text-to-image request details for debugging
|
||||
*/
|
||||
export const logTextToImageRequest = (
|
||||
req: any,
|
||||
_res: Response,
|
||||
next: NextFunction,
|
||||
): void => {
|
||||
const timestamp = new Date().toISOString();
|
||||
const { prompt, filename, autoEnhance, enhancementOptions } = req.body;
|
||||
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] === TEXT-TO-IMAGE REQUEST ===`,
|
||||
);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Method: ${req.method}`);
|
||||
console.log(`[${timestamp}] [${req.requestId}] Path: ${req.path}`);
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] Content-Type: ${req.get("Content-Type")}`,
|
||||
);
|
||||
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}] Auto-enhance: ${autoEnhance || false}`,
|
||||
);
|
||||
if (enhancementOptions) {
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] Enhancement options:`,
|
||||
enhancementOptions,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] Reference files: 0 (text-only endpoint)`,
|
||||
);
|
||||
console.log(
|
||||
`[${timestamp}] [${req.requestId}] =====================================`,
|
||||
);
|
||||
next();
|
||||
};
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { Response, Router } from "express";
|
||||
import type { Router as RouterType } from "express";
|
||||
import { ImageGenService } from "../services/ImageGenService";
|
||||
import {
|
||||
validateTextToImageRequest,
|
||||
logTextToImageRequest,
|
||||
} from "../middleware/jsonValidation";
|
||||
import {
|
||||
autoEnhancePrompt,
|
||||
logEnhancementResult,
|
||||
} from "../middleware/promptEnhancement";
|
||||
import { asyncHandler } from "../middleware/errorHandler";
|
||||
import { GenerateImageResponse } from "../types/api";
|
||||
|
||||
export const textToImageRouter: RouterType = Router();
|
||||
|
||||
let imageGenService: ImageGenService;
|
||||
|
||||
/**
|
||||
* POST /api/text-to-image - Generate image from text prompt only (JSON)
|
||||
*/
|
||||
textToImageRouter.post(
|
||||
"/text-to-image",
|
||||
// JSON validation middleware
|
||||
logTextToImageRequest,
|
||||
validateTextToImageRequest,
|
||||
|
||||
// Auto-enhancement middleware (optional)
|
||||
autoEnhancePrompt,
|
||||
logEnhancementResult,
|
||||
|
||||
// Main handler
|
||||
asyncHandler(async (req: any, res: Response) => {
|
||||
// Initialize service if not already done
|
||||
if (!imageGenService) {
|
||||
const apiKey = process.env["GEMINI_API_KEY"];
|
||||
if (!apiKey) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Server configuration error",
|
||||
error: "GEMINI_API_KEY not configured",
|
||||
} as GenerateImageResponse);
|
||||
}
|
||||
imageGenService = new ImageGenService(apiKey);
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const requestId = req.requestId;
|
||||
const { prompt, filename } = req.body;
|
||||
|
||||
console.log(
|
||||
`[${timestamp}] [${requestId}] Starting text-to-image generation process`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Generate the image (no reference images for this endpoint)
|
||||
console.log(
|
||||
`[${timestamp}] [${requestId}] Calling ImageGenService.generateImage() (text-only)`,
|
||||
);
|
||||
|
||||
const result = await imageGenService.generateImage({
|
||||
prompt,
|
||||
filename,
|
||||
});
|
||||
|
||||
// Log the result
|
||||
console.log(
|
||||
`[${timestamp}] [${requestId}] Text-to-image generation completed:`,
|
||||
{
|
||||
success: result.success,
|
||||
model: result.model,
|
||||
filename: result.filename,
|
||||
hasError: !!result.error,
|
||||
},
|
||||
);
|
||||
|
||||
// Send response
|
||||
if (result.success) {
|
||||
const successResponse: GenerateImageResponse = {
|
||||
success: true,
|
||||
message: "Image generated successfully",
|
||||
data: {
|
||||
filename: result.filename!,
|
||||
filepath: result.filepath!,
|
||||
...(result.description && { description: result.description }),
|
||||
model: result.model,
|
||||
generatedAt: timestamp,
|
||||
...(req.enhancedPrompt && {
|
||||
promptEnhancement: {
|
||||
originalPrompt: req.originalPrompt,
|
||||
enhancedPrompt: req.enhancedPrompt,
|
||||
detectedLanguage: req.enhancementMetadata?.detectedLanguage,
|
||||
appliedTemplate: req.enhancementMetadata?.appliedTemplate,
|
||||
enhancements: req.enhancementMetadata?.enhancements || [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
console.log(`[${timestamp}] [${requestId}] Sending success response`);
|
||||
return res.status(200).json(successResponse);
|
||||
} else {
|
||||
const errorResponse: GenerateImageResponse = {
|
||||
success: false,
|
||||
message: "Image generation failed",
|
||||
error: result.error || "Unknown error occurred",
|
||||
};
|
||||
|
||||
console.log(
|
||||
`[${timestamp}] [${requestId}] Sending error response: ${result.error}`,
|
||||
);
|
||||
return res.status(500).json(errorResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[${timestamp}] [${requestId}] Unhandled error in text-to-image endpoint:`,
|
||||
error,
|
||||
);
|
||||
|
||||
const errorResponse: GenerateImageResponse = {
|
||||
success: false,
|
||||
message: "Image generation failed",
|
||||
error:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
|
||||
return res.status(500).json(errorResponse);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
@ -6,6 +6,26 @@ export interface GenerateImageRequest {
|
|||
filename: string;
|
||||
}
|
||||
|
||||
export interface TextToImageRequest {
|
||||
prompt: string;
|
||||
filename: string;
|
||||
autoEnhance?: boolean;
|
||||
enhancementOptions?: {
|
||||
imageStyle?:
|
||||
| "photorealistic"
|
||||
| "illustration"
|
||||
| "minimalist"
|
||||
| "sticker"
|
||||
| "product"
|
||||
| "comic";
|
||||
aspectRatio?: "square" | "portrait" | "landscape" | "wide" | "ultrawide";
|
||||
mood?: string;
|
||||
lighting?: string;
|
||||
cameraAngle?: string;
|
||||
negativePrompts?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GenerateImageResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue