feat: add meta tags

This commit is contained in:
Oleg Proskurin 2025-10-08 00:03:08 +07:00
parent f63c89a991
commit 7c31644824
10 changed files with 114 additions and 8 deletions

View File

@ -147,6 +147,28 @@ export const validateTextToImageRequest = (
} }
} }
// Validate meta (optional object)
if (req.body.meta !== undefined) {
if (
typeof req.body.meta !== "object" ||
Array.isArray(req.body.meta)
) {
errors.push("meta must be an object");
} else if (req.body.meta.tags !== undefined) {
if (!Array.isArray(req.body.meta.tags)) {
errors.push("meta.tags must be an array");
} else {
// Validate each tag is a string
for (const tag of req.body.meta.tags) {
if (typeof tag !== "string") {
errors.push("Each tag in meta.tags must be a string");
break;
}
}
}
}
}
// Check for XSS attempts in prompt // Check for XSS attempts in prompt
const xssPatterns = [ const xssPatterns = [
/<script/i, /<script/i,

View File

@ -52,7 +52,10 @@ export const autoEnhancePrompt = async (
const result = await promptEnhancementService.enhancePrompt( const result = await promptEnhancementService.enhancePrompt(
prompt, prompt,
enhancementOptions || {}, {
...enhancementOptions,
...(req.body.meta?.tags && { tags: req.body.meta.tags }),
},
); );
if (result.success && result.enhancedPrompt) { if (result.success && result.enhancedPrompt) {

View File

@ -63,7 +63,7 @@ generateRouter.post(
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const requestId = req.requestId; const requestId = req.requestId;
const { prompt, filename, aspectRatio } = req.body; const { prompt, filename, aspectRatio, meta } = req.body;
const files = (req.files as Express.Multer.File[]) || []; const files = (req.files as Express.Multer.File[]) || [];
// Extract org/project slugs from validated API key // Extract org/project slugs from validated API key
@ -112,6 +112,7 @@ generateRouter.post(
orgId, orgId,
projectId, projectId,
...(referenceImages && { referenceImages }), ...(referenceImages && { referenceImages }),
...(meta && { meta }),
}); });
// Log the result // Log the result

View File

@ -54,7 +54,7 @@ textToImageRouter.post(
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const requestId = req.requestId; const requestId = req.requestId;
const { prompt, filename, aspectRatio } = req.body; const { prompt, filename, aspectRatio, meta } = req.body;
// Extract org/project slugs from validated API key // Extract org/project slugs from validated API key
const orgId = req.apiKey?.organizationSlug || undefined; const orgId = req.apiKey?.organizationSlug || undefined;
@ -76,6 +76,7 @@ textToImageRouter.post(
...(aspectRatio && { aspectRatio }), ...(aspectRatio && { aspectRatio }),
orgId, orgId,
projectId, projectId,
...(meta && { meta }),
}); });
// Log the result // Log the result

View File

@ -29,7 +29,7 @@ export class ImageGenService {
async generateImage( async generateImage(
options: ImageGenerationOptions, options: ImageGenerationOptions,
): Promise<ImageGenerationResult> { ): Promise<ImageGenerationResult> {
const { prompt, filename, referenceImages, aspectRatio, orgId, projectId } = options; const { prompt, filename, referenceImages, aspectRatio, orgId, projectId, meta } = options;
// Use default values if not provided // Use default values if not provided
const finalOrgId = orgId || process.env["DEFAULT_ORG_ID"] || "default"; const finalOrgId = orgId || process.env["DEFAULT_ORG_ID"] || "default";
@ -47,6 +47,7 @@ export class ImageGenService {
finalAspectRatio, finalAspectRatio,
finalOrgId, finalOrgId,
finalProjectId, finalProjectId,
meta,
); );
generatedData = aiResult.generatedData; generatedData = aiResult.generatedData;
geminiParams = aiResult.geminiParams; geminiParams = aiResult.geminiParams;
@ -126,6 +127,7 @@ export class ImageGenService {
aspectRatio: string, aspectRatio: string,
orgId: string, orgId: string,
projectId: string, projectId: string,
meta?: { tags?: string[] },
): Promise<{ ): Promise<{
generatedData: GeneratedImageData; generatedData: GeneratedImageData;
geminiParams: GeminiParams; geminiParams: GeminiParams;
@ -185,6 +187,7 @@ export class ImageGenService {
prompt, prompt,
model: this.primaryModel, model: this.primaryModel,
config, config,
...(meta && { meta }),
...(referenceImages && ...(referenceImages &&
referenceImages.length > 0 && { referenceImages.length > 0 && {
referenceImages: referenceImages.map((img) => ({ referenceImages: referenceImages.map((img) => ({

View File

@ -9,6 +9,7 @@ export interface PromptEnhancementOptions {
| "product" | "product"
| "comic" | "comic"
| "general"; | "general";
tags?: string[]; // Optional tags - accepted but not used yet
} }
export interface PromptEnhancementResult { export interface PromptEnhancementResult {
@ -52,6 +53,9 @@ export class PromptEnhancementService {
`[${timestamp}] Starting prompt enhancement for: "${rawPrompt.substring(0, 50)}..."`, `[${timestamp}] Starting prompt enhancement for: "${rawPrompt.substring(0, 50)}..."`,
); );
console.log(`[${timestamp}] Using template: ${finalOptions.template}`); console.log(`[${timestamp}] Using template: ${finalOptions.template}`);
if (finalOptions.tags && finalOptions.tags.length > 0) {
console.log(`[${timestamp}] Tags: ${finalOptions.tags.join(", ")}`);
}
try { try {
const systemPrompt = this.buildSystemPrompt(finalOptions); const systemPrompt = this.buildSystemPrompt(finalOptions);
@ -151,6 +155,8 @@ TECHNICAL REQUIREMENTS:
RESPONSE FORMAT: RESPONSE FORMAT:
Provide only the enhanced prompt as a single, cohesive paragraph. Do not include explanations, metadata, or multiple options. The response should be ready to use directly for image generation. Provide only the enhanced prompt as a single, cohesive paragraph. Do not include explanations, metadata, or multiple options. The response should be ready to use directly for image generation.
CRITICAL: the prompt length MUST be under 2000 characters length. Contract the prompts if they're longer
Remember: More detail equals more control. Transform vague concepts into vivid, specific descriptions that guide the model toward the exact image envisioned.`; Remember: More detail equals more control. Transform vague concepts into vivid, specific descriptions that guide the model toward the exact image envisioned.`;
} }
@ -163,6 +169,10 @@ Remember: More detail equals more control. Transform vague concepts into vivid,
const selectedTemplate = options.template || "photorealistic"; const selectedTemplate = options.template || "photorealistic";
prompt += `\n\nTarget template/style: ${selectedTemplate}`; prompt += `\n\nTarget template/style: ${selectedTemplate}`;
console.log(
"🚀 ~ PromptEnhancementService ~ buildUserPrompt ~ prompt:",
prompt,
);
return prompt; return prompt;
} }

View File

@ -1,4 +1,4 @@
import { writeFileSync, appendFileSync, existsSync, mkdirSync } from "fs"; import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs";
import { dirname } from "path"; import { dirname } from "path";
export interface TTILogEntry { export interface TTILogEntry {
@ -6,6 +6,9 @@ export interface TTILogEntry {
orgId: string; orgId: string;
projectId: string; projectId: string;
prompt: string; prompt: string;
meta?: {
tags?: string[];
};
referenceImages?: Array<{ referenceImages?: Array<{
mimetype: string; mimetype: string;
size: number; size: number;
@ -65,15 +68,30 @@ export class TTILogger {
} }
try { try {
const logEntry = this.formatLogEntry(entry); const newLogEntry = this.formatLogEntry(entry);
appendFileSync(this.logFilePath, logEntry, { encoding: "utf-8" });
// Read existing content
const existingContent = existsSync(this.logFilePath)
? readFileSync(this.logFilePath, "utf-8")
: "# Text-to-Image Generation Log\n\n";
// Insert new entry AFTER header but BEFORE old entries
const headerEnd = existingContent.indexOf("\n\n") + 2;
const header = existingContent.slice(0, headerEnd);
const oldEntries = existingContent.slice(headerEnd);
writeFileSync(
this.logFilePath,
header + newLogEntry + oldEntries,
"utf-8",
);
} catch (error) { } catch (error) {
console.error(`[TTILogger] Failed to write log entry:`, error); console.error(`[TTILogger] Failed to write log entry:`, error);
} }
} }
private formatLogEntry(entry: TTILogEntry): string { private formatLogEntry(entry: TTILogEntry): string {
const { timestamp, orgId, projectId, prompt, referenceImages, model, config } = entry; const { timestamp, orgId, projectId, prompt, meta, referenceImages, model, config } = entry;
// Format date from ISO timestamp // Format date from ISO timestamp
const date = new Date(timestamp); const date = new Date(timestamp);
@ -83,6 +101,11 @@ export class TTILogger {
logText += `${orgId}/${projectId}\n\n`; logText += `${orgId}/${projectId}\n\n`;
logText += `**Prompt:** ${prompt}\n\n`; logText += `**Prompt:** ${prompt}\n\n`;
// Add tags if present
if (meta?.tags && meta.tags.length > 0) {
logText += `**Tags:** ${meta.tags.join(", ")}\n\n`;
}
if (referenceImages && referenceImages.length > 0) { if (referenceImages && referenceImages.length > 0) {
logText += `**Reference Images:** ${referenceImages.length} image${referenceImages.length > 1 ? "s" : ""}\n`; logText += `**Reference Images:** ${referenceImages.length} image${referenceImages.length > 1 ? "s" : ""}\n`;
for (const img of referenceImages) { for (const img of referenceImages) {

View File

@ -21,6 +21,9 @@ export interface TextToImageRequest {
| "comic" | "comic"
| "general"; // Defaults to "photorealistic" | "general"; // Defaults to "photorealistic"
}; };
meta?: {
tags?: string[]; // Optional array of tags for tracking/grouping (not stored, only logged)
};
} }
export interface GenerateImageResponse { export interface GenerateImageResponse {
@ -60,6 +63,9 @@ export interface ImageGenerationOptions {
orgId?: string; orgId?: string;
projectId?: string; projectId?: string;
userId?: string; userId?: string;
meta?: {
tags?: string[];
};
} }
export interface ReferenceImage { export interface ReferenceImage {
@ -155,6 +161,9 @@ export interface EnhancedGenerateImageRequest extends GenerateImageRequest {
| "comic" | "comic"
| "general"; // Defaults to "photorealistic" | "general"; // Defaults to "photorealistic"
}; };
meta?: {
tags?: string[];
};
} }
// Environment configuration // Environment configuration

View File

@ -9,6 +9,11 @@ import { AdvancedOptionsModal, AdvancedOptionsData } from '@/components/demo/Adv
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
const API_KEY_STORAGE_KEY = 'banatie_demo_api_key'; const API_KEY_STORAGE_KEY = 'banatie_demo_api_key';
// Generate random 6-character uppercase ID for pairing images
function generatePairId(): string {
return Math.random().toString(36).substring(2, 8).toUpperCase();
}
interface GenerationResult { interface GenerationResult {
id: string; id: string;
timestamp: Date; timestamp: Date;
@ -39,6 +44,9 @@ interface GenerationResult {
}; };
enhancementOptions?: { enhancementOptions?: {
template?: string; template?: string;
meta?: {
tags?: string[];
};
} & AdvancedOptionsData; } & AdvancedOptionsData;
} }
@ -201,6 +209,7 @@ export default function DemoTTIPage() {
setGenerationStartTime(startTime); setGenerationStartTime(startTime);
const resultId = Date.now().toString(); const resultId = Date.now().toString();
const pairId = generatePairId(); // NEW: Generate unique pair ID
const timestamp = new Date(); const timestamp = new Date();
try { try {
@ -219,6 +228,9 @@ export default function DemoTTIPage() {
filename: `demo_${resultId}_left`, filename: `demo_${resultId}_left`,
aspectRatio, aspectRatio,
autoEnhance: false, // Explicitly disable enhancement for left image autoEnhance: false, // Explicitly disable enhancement for left image
meta: {
tags: [pairId, 'simple'] // NEW: Pair ID + "simple" tag
}
}), }),
}), }),
fetch(`${API_BASE_URL}/api/text-to-image`, { fetch(`${API_BASE_URL}/api/text-to-image`, {
@ -235,6 +247,9 @@ export default function DemoTTIPage() {
enhancementOptions: { enhancementOptions: {
template: template || 'photorealistic', // Only template parameter template: template || 'photorealistic', // Only template parameter
}, },
meta: {
tags: [pairId, 'enhanced'] // NEW: Pair ID + "enhanced" tag
}
}), }),
}), }),
]); ]);
@ -273,6 +288,9 @@ export default function DemoTTIPage() {
filename: `demo_${resultId}_left`, filename: `demo_${resultId}_left`,
aspectRatio, aspectRatio,
autoEnhance: false, autoEnhance: false,
meta: {
tags: [pairId, 'simple']
}
}, },
response: leftData, response: leftData,
geminiParams: leftData.data?.geminiParams || {}, geminiParams: leftData.data?.geminiParams || {},
@ -286,6 +304,9 @@ export default function DemoTTIPage() {
enhancementOptions: { enhancementOptions: {
template: template || 'photorealistic', template: template || 'photorealistic',
}, },
meta: {
tags: [pairId, 'enhanced']
}
}, },
response: rightData, response: rightData,
geminiParams: rightData.data?.geminiParams || {}, geminiParams: rightData.data?.geminiParams || {},
@ -293,6 +314,9 @@ export default function DemoTTIPage() {
// Store enhancement options for display in inspect mode // Store enhancement options for display in inspect mode
enhancementOptions: { enhancementOptions: {
template, template,
meta: {
tags: [pairId, 'enhanced']
}
}, },
}; };

View File

@ -356,6 +356,7 @@ Generate images from text prompts only using JSON payload. Simplified endpoint f
| `aspectRatio` | string | No | `"1:1"` | Image aspect ratio (`"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`) | | `aspectRatio` | string | No | `"1:1"` | Image aspect ratio (`"1:1"`, `"2:3"`, `"3:2"`, `"3:4"`, `"4:3"`, `"4:5"`, `"5:4"`, `"9:16"`, `"16:9"`, `"21:9"`) |
| `autoEnhance` | boolean | No | `true` | Enable automatic prompt enhancement (set to `false` to use prompt as-is) | | `autoEnhance` | boolean | No | `true` | Enable automatic prompt enhancement (set to `false` to use prompt as-is) |
| `enhancementOptions` | object | No | - | Enhancement configuration options | | `enhancementOptions` | object | No | - | Enhancement configuration options |
| `meta` | object | No | - | Metadata for request tracking |
**Enhancement Options:** **Enhancement Options:**
@ -363,6 +364,12 @@ Generate images from text prompts only using JSON payload. Simplified endpoint f
|-------|------|----------|---------|-------------| |-------|------|----------|---------|-------------|
| `template` | string | No | `"photorealistic"` | Prompt engineering template: `"photorealistic"`, `"illustration"`, `"minimalist"`, `"sticker"`, `"product"`, `"comic"`, `"general"` | | `template` | string | No | `"photorealistic"` | Prompt engineering template: `"photorealistic"`, `"illustration"`, `"minimalist"`, `"sticker"`, `"product"`, `"comic"`, `"general"` |
**Meta Object:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `tags` | string[] | No | Array of string tags for tracking/grouping requests (not stored, only logged) |
**Example Request:** **Example Request:**
```bash ```bash
curl -X POST http://localhost:3000/api/text-to-image \ curl -X POST http://localhost:3000/api/text-to-image \
@ -375,6 +382,9 @@ curl -X POST http://localhost:3000/api/text-to-image \
"autoEnhance": true, "autoEnhance": true,
"enhancementOptions": { "enhancementOptions": {
"template": "photorealistic" "template": "photorealistic"
},
"meta": {
"tags": ["demo", "sunset"]
} }
}' }'
``` ```