diff --git a/apps/api-service/src/middleware/promptEnhancement.ts b/apps/api-service/src/middleware/promptEnhancement.ts index 2e10934..db5fccf 100644 --- a/apps/api-service/src/middleware/promptEnhancement.ts +++ b/apps/api-service/src/middleware/promptEnhancement.ts @@ -81,8 +81,6 @@ export const autoEnhancePrompt = async ( }), enhancements: result.metadata?.enhancements || [], }; - - req.body.prompt = result.enhancedPrompt; } else { console.warn(`[${timestamp}] [${requestId}] Prompt enhancement failed: ${result.error}`); console.log(`[${timestamp}] [${requestId}] Proceeding with original prompt`); diff --git a/apps/api-service/src/routes/v1/generations.ts b/apps/api-service/src/routes/v1/generations.ts index 08c2058..9b4e4cb 100644 --- a/apps/api-service/src/routes/v1/generations.ts +++ b/apps/api-service/src/routes/v1/generations.ts @@ -5,6 +5,7 @@ import { asyncHandler } from '@/middleware/errorHandler'; import { validateApiKey } from '@/middleware/auth/validateApiKey'; import { requireProjectKey } from '@/middleware/auth/requireProjectKey'; import { rateLimitByApiKey } from '@/middleware/auth/rateLimiter'; +import { autoEnhancePrompt } from '@/middleware/promptEnhancement'; import { validateAndNormalizePagination } from '@/utils/validators'; import { buildPaginatedResponse } from '@/utils/helpers'; import { toGenerationResponse } from '@/types/responses'; @@ -46,7 +47,7 @@ const getGenerationService = (): GenerationService => { * @param {string} [req.body.flowId] - Associate with existing flow * @param {string} [req.body.alias] - Project-scoped alias (@custom-name) * @param {string} [req.body.flowAlias] - Flow-scoped alias (requires flowId) - * @param {boolean} [req.body.autoEnhance=false] - Enable prompt enhancement + * @param {boolean} [req.body.autoEnhance=true] - Enable prompt enhancement * @param {object} [req.body.meta] - Custom metadata * * @returns {CreateGenerationResponse} 201 - Generation created with status @@ -82,10 +83,15 @@ generationsRouter.post( validateApiKey, requireProjectKey, rateLimitByApiKey, + autoEnhancePrompt, asyncHandler(async (req: any, res: Response) => { const service = getGenerationService(); + + // Extract original prompt from middleware property if enhancement was attempted + // Otherwise fall back to request body + const prompt = req.originalPrompt || req.body.prompt; + const { - prompt, referenceImages, aspectRatio, flowId, @@ -119,6 +125,7 @@ generationsRouter.post( alias, flowAlias, autoEnhance, + enhancedPrompt: req.enhancedPrompt, meta, requestId: req.requestId, }); diff --git a/apps/api-service/src/services/core/GenerationService.ts b/apps/api-service/src/services/core/GenerationService.ts index f541ee8..1f970df 100644 --- a/apps/api-service/src/services/core/GenerationService.ts +++ b/apps/api-service/src/services/core/GenerationService.ts @@ -97,10 +97,10 @@ export class GenerationService { } // Prompt semantics (Section 2.1): - // - If autoEnhance = false OR no enhancedPrompt: prompt = user input, originalPrompt = null - // - If autoEnhance = true AND enhancedPrompt: prompt = enhanced, originalPrompt = user input + // - originalPrompt: ALWAYS contains user's original input + // - prompt: Enhanced version if autoEnhance=true, otherwise same as originalPrompt const usedPrompt = params.enhancedPrompt || params.prompt; - const preservedOriginal = params.enhancedPrompt ? params.prompt : null; + const preservedOriginal = params.prompt; // Always store original const generationRecord: NewGeneration = { projectId: params.projectId, diff --git a/apps/api-service/src/types/responses.ts b/apps/api-service/src/types/responses.ts index f99c5b4..b65f34e 100644 --- a/apps/api-service/src/types/responses.ts +++ b/apps/api-service/src/types/responses.ts @@ -36,7 +36,8 @@ export interface GenerationResponse { projectId: string; flowId: string | null; prompt: string; // Prompt actually used for generation - originalPrompt: string | null; // User's original (nullable, only if enhanced) + originalPrompt: string | null; // User's original input (always populated for new generations) + autoEnhance: boolean; // Whether prompt enhancement was applied aspectRatio: string | null; status: string; errorMessage: string | null; @@ -247,7 +248,8 @@ export const toGenerationResponse = (gen: GenerationWithRelations): GenerationRe projectId: gen.projectId, flowId: gen.flowId ?? gen.pendingFlowId ?? null, // Return actual flowId or pendingFlowId for client prompt: gen.prompt, // Prompt actually used - originalPrompt: gen.originalPrompt, // User's original (null if not enhanced) + originalPrompt: gen.originalPrompt, // User's original (always populated) + autoEnhance: gen.prompt !== gen.originalPrompt, // True if prompts differ (enhancement happened) aspectRatio: gen.aspectRatio, status: gen.status, errorMessage: gen.errorMessage, diff --git a/apps/landing/src/components/docs/blocks/InlineCode.tsx b/apps/landing/src/components/docs/blocks/InlineCode.tsx index 758c174..5f4090b 100644 --- a/apps/landing/src/components/docs/blocks/InlineCode.tsx +++ b/apps/landing/src/components/docs/blocks/InlineCode.tsx @@ -78,7 +78,7 @@ * Include the X-API-Key header. * * // Parameter documentation - * The autoEnhance parameter defaults to false. + * The autoEnhance parameter defaults to true. * * // Error messages * If you receive 401 Unauthorized, check your API key. diff --git a/docs/api/image-generation.md b/docs/api/image-generation.md index 5424275..5795d38 100644 --- a/docs/api/image-generation.md +++ b/docs/api/image-generation.md @@ -19,7 +19,7 @@ Create new image generation with optional reference images, aliases, and auto-en - `flowId` - Associate generation with a flow (UUID) - `alias` - Assign project-scoped alias to output image (@custom-name) - `flowAlias` - Assign flow-scoped alias to output image (requires flowId) -- `autoEnhance` - Enable prompt enhancement (boolean, default: false) +- `autoEnhance` - Enable prompt enhancement (boolean, default: true) - `enhancementOptions` - Enhancement configuration (object, optional) - `template` - Enhancement template: "photorealistic", "illustration", "minimalist", "sticker", "product", "comic", "general" - `meta` - Custom metadata (JSON object) diff --git a/tests/api/01-generation-basic.rest b/tests/api/01-generation-basic.rest new file mode 100644 index 0000000..6a78aa5 --- /dev/null +++ b/tests/api/01-generation-basic.rest @@ -0,0 +1,289 @@ +@base = http://localhost:3000 +@apiKey = bnt_727d2f4f72bd03ed96da5278bb971a00cb0a2454d4d70f9748b5c39f3f69d88d + +############################################################################### +# BASIC GENERATION TESTS +# Run these tests FIRST to verify core generation functionality +# +# Test Coverage: +# 1. Simple generation with different aspect ratios +# 2. Generation retrieval and listing +# 3. Pagination and filtering +# 4. Processing time tracking +############################################################################### + +############################################################################### +# TEST 1: Simple Generation (16:9) +# Creates a basic generation without references or flows +############################################################################### + +### Step 1.1: Create Generation +# @name createBasicGen +POST {{base}}/api/v1/generations +Content-Type: application/json +X-API-Key: {{apiKey}} + +{ + "prompt": "шикарная моторная яхта движется по живописному озеру, люди сидят в спасательных жилетах и держат в руках бутылки с пивом, густой хвойный лес на берегу. фотореалистичная фотография", + "autoEnhance": true, + "aspectRatio": "16:9" +} + +### + +@generationId = {{createBasicGen.response.body.$.data.id}} +@generationStatus = {{createBasicGen.response.body.$.data.status}} + + +### Step 1.2: Check Generation Status (Poll until success) +# @name checkBasicGen +# Keep running this until status = "success" +GET {{base}}/api/v1/generations/{{generationId}} +X-API-Key: {{apiKey}} + +### + +@outputImageId = {{checkBasicGen.response.body.$.data.outputImageId}} +@processingTimeMs = {{checkBasicGen.response.body.$.data.processingTimeMs}} + + +### Step 1.3: Get Output Image Metadata +# @name getBasicImage +GET {{base}}/api/v1/images/{{outputImageId}} +X-API-Key: {{apiKey}} + +### +# Verify: +# - storageUrl is present +# - Image is accessible at storageUrl +# - processingTimeMs is recorded + + +############################################################################### +# TEST 2: Square Generation (1:1) +# Tests aspect ratio 1:1 +############################################################################### + +### Step 2.1: Create Square Generation +# @name createSquareGen +POST {{base}}/api/v1/generations +Content-Type: application/json +X-API-Key: {{apiKey}} + +{ + "prompt": "A minimalist logo design", + "aspectRatio": "1:1" +} + +### + +@squareGenId = {{createSquareGen.response.body.$.data.id}} + + +### Step 2.2: Check Status (Poll until success) +# @name checkSquareGen +GET {{base}}/api/v1/generations/{{squareGenId}} +X-API-Key: {{apiKey}} + +### + +@squareImageId = {{checkSquareGen.response.body.$.data.outputImageId}} + +### +# Verify: +# - aspectRatio = "1:1" +# - status = "success" +# - outputImageId is present + + +############################################################################### +# TEST 3: Portrait Generation (9:16) +# Tests aspect ratio 9:16 +############################################################################### + +### Step 3.1: Create Portrait Generation +# @name createPortraitGen +POST {{base}}/api/v1/generations +Content-Type: application/json +X-API-Key: {{apiKey}} + +{ + "prompt": "A tall building at night", + "aspectRatio": "9:16" +} + +### + +@portraitGenId = {{createPortraitGen.response.body.$.data.id}} + + +### Step 3.2: Check Status (Poll until success) +# @name checkPortraitGen +GET {{base}}/api/v1/generations/{{portraitGenId}} +X-API-Key: {{apiKey}} + +### + +@portraitImageId = {{checkPortraitGen.response.body.$.data.outputImageId}} + +### +# Verify: +# - aspectRatio = "9:16" +# - status = "success" +# - outputImageId is present + + +############################################################################### +# TEST 4: Get Generation by ID +# Verifies all expected fields are present in response +############################################################################### + +### Step 4.1: Get Generation Details +# @name getGenDetails +GET {{base}}/api/v1/generations/{{generationId}} +X-API-Key: {{apiKey}} + +### +# Verify response contains: +# - id: {{generationId}} +# - prompt: "A beautiful sunset over mountains" +# - status: "success" +# - outputImageId: {{outputImageId}} +# - outputImage (nested object) +# - createdAt +# - updatedAt +# - processingTimeMs + + +############################################################################### +# TEST 5: List All Generations +# Verifies generation listing without filters +############################################################################### + +### Step 5.1: List All Generations (Default pagination) +# @name listAllGens +GET {{base}}/api/v1/generations +X-API-Key: {{apiKey}} + +### +# Verify: +# - Response has data array +# - Response has pagination object +# - At least 3 generations present (from previous tests) +# - Our generation {{generationId}} is in the list + + +############################################################################### +# TEST 6: List Generations with Pagination +# Tests pagination parameters (limit, offset) +############################################################################### + +### Step 6.1: Get First Page (limit=2) +# @name listPageOne +GET {{base}}/api/v1/generations?limit=2&offset=0 +X-API-Key: {{apiKey}} + +### +# Verify: +# - data.length <= 2 +# - pagination.limit = 2 +# - pagination.offset = 0 +# - pagination.hasMore = true (if total > 2) + + +### Step 6.2: Get Second Page (offset=2) +# @name listPageTwo +GET {{base}}/api/v1/generations?limit=2&offset=2 +X-API-Key: {{apiKey}} + +### +# Verify: +# - Different results than first page +# - pagination.offset = 2 + + +############################################################################### +# TEST 7: Filter Generations by Status +# Tests status filter parameter +############################################################################### + +### Step 7.1: Filter by Success Status +# @name filterSuccess +GET {{base}}/api/v1/generations?status=success +X-API-Key: {{apiKey}} + +### +# Verify: +# - All items in data[] have status = "success" +# - No pending/processing/failed generations + + +### Step 7.2: Filter by Failed Status +# @name filterFailed +GET {{base}}/api/v1/generations?status=failed +X-API-Key: {{apiKey}} + +### +# Verify: +# - All items (if any) have status = "failed" + + +############################################################################### +# TEST 8: Verify Processing Time Recorded +# Ensures generation performance metrics are tracked +############################################################################### + +### Step 8.1: Check Processing Time +# @name checkProcessingTime +GET {{base}}/api/v1/generations/{{generationId}} +X-API-Key: {{apiKey}} + +### +# Verify: +# - processingTimeMs is a number: {{processingTimeMs}} +# - processingTimeMs > 0 +# - Typical range: 3000-15000ms (3-15 seconds) +# - Processing time reflects actual generation duration + + +############################################################################### +# CLEANUP (Optional) +# Uncomment to delete test generations +############################################################################### + +# ### Delete Test Generation 1 +# DELETE {{base}}/api/v1/generations/{{generationId}} +# X-API-Key: {{apiKey}} + +# ### Delete Test Generation 2 +# DELETE {{base}}/api/v1/generations/{{squareGenId}} +# X-API-Key: {{apiKey}} + +# ### Delete Test Generation 3 +# DELETE {{base}}/api/v1/generations/{{portraitGenId}} +# X-API-Key: {{apiKey}} + + +############################################################################### +# NOTES +############################################################################### +# +# Expected Results: +# ✓ All generations complete successfully (status = "success") +# ✓ Each generation has unique ID and output image +# ✓ Aspect ratios are correctly applied +# ✓ Processing times are recorded (typically 3-15 seconds) +# ✓ Pagination works correctly +# ✓ Status filtering works correctly +# +# Common Issues: +# ⚠ Generation may fail with Gemini API errors (transient) +# ⚠ Processing time varies based on prompt complexity +# ⚠ First generation may be slower (cold start) +# +# Tips: +# - Use "Poll until success" for Step X.2 requests +# - Variables are automatically extracted from responses +# - Check response body to see extracted values +# - Most generations complete in 5-10 seconds +# diff --git a/tests/api/08-auto-enhance.ts b/tests/api/08-auto-enhance.ts new file mode 100644 index 0000000..74ca64d --- /dev/null +++ b/tests/api/08-auto-enhance.ts @@ -0,0 +1,223 @@ +// tests/api/08-auto-enhance.ts +// Auto-Enhance Feature Tests + +import { api, log, runTest, waitForGeneration, testContext } from './utils'; +import { endpoints } from './config'; + +async function main() { + log.section('AUTO-ENHANCE TESTS'); + + // Test 1: Generation without autoEnhance parameter (should default to true) + await runTest('Generate without autoEnhance param → should enhance', async () => { + const result = await api(endpoints.generations, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt: 'a simple test image', + aspectRatio: '1:1', + // No autoEnhance parameter - should default to true + }), + }); + + if (!result.data.data || !result.data.data.id) { + throw new Error('No generation returned'); + } + + const generation = await waitForGeneration(result.data.data.id); + + if (generation.status !== 'success') { + throw new Error(`Generation failed: ${generation.errorMessage}`); + } + + // Verify enhancement happened + if (!generation.originalPrompt) { + throw new Error('originalPrompt should be populated when enhanced'); + } + + if (!generation.autoEnhance) { + throw new Error('autoEnhance should be true'); + } + + if (generation.prompt === generation.originalPrompt) { + throw new Error('prompt and originalPrompt should be different (enhancement happened)'); + } + + log.detail('Original prompt', generation.originalPrompt); + log.detail('Enhanced prompt', generation.prompt); + log.detail('autoEnhance', generation.autoEnhance); + log.detail('Enhancement confirmed', '✓'); + + testContext.enhancedGenId = generation.id; + }); + + // Test 2: Generation with autoEnhance: false + await runTest('Generate with autoEnhance: false → should NOT enhance', async () => { + const result = await api(endpoints.generations, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt: 'another test image', + aspectRatio: '1:1', + autoEnhance: false, + }), + }); + + if (!result.data.data || !result.data.data.id) { + throw new Error('No generation returned'); + } + + const generation = await waitForGeneration(result.data.data.id); + + if (generation.status !== 'success') { + throw new Error(`Generation failed: ${generation.errorMessage}`); + } + + // Verify NO enhancement happened + if (!generation.originalPrompt) { + throw new Error('originalPrompt should be populated with original input'); + } + + if (generation.autoEnhance) { + throw new Error('autoEnhance should be false'); + } + + if (generation.prompt !== generation.originalPrompt) { + throw new Error('prompt and originalPrompt should be the SAME when NOT enhanced'); + } + + if (generation.prompt !== 'another test image') { + throw new Error('both prompts should match original input (no enhancement)'); + } + + log.detail('Prompt', generation.prompt); + log.detail('originalPrompt', generation.originalPrompt); + log.detail('autoEnhance', generation.autoEnhance); + log.detail('Prompts match (no enhancement)', '✓'); + + testContext.notEnhancedGenId = generation.id; + }); + + // Test 3: Generation with explicit autoEnhance: true + await runTest('Generate with autoEnhance: true → should enhance', async () => { + const result = await api(endpoints.generations, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + prompt: 'third test image', + aspectRatio: '1:1', + autoEnhance: true, + }), + }); + + if (!result.data.data || !result.data.data.id) { + throw new Error('No generation returned'); + } + + const generation = await waitForGeneration(result.data.data.id); + + if (generation.status !== 'success') { + throw new Error(`Generation failed: ${generation.errorMessage}`); + } + + // Verify enhancement happened + if (!generation.originalPrompt) { + throw new Error('originalPrompt should be populated'); + } + + if (!generation.autoEnhance) { + throw new Error('autoEnhance should be true'); + } + + if (generation.originalPrompt !== 'third test image') { + throw new Error('originalPrompt should match input'); + } + + if (generation.prompt === generation.originalPrompt) { + throw new Error('prompt should be enhanced (different from original)'); + } + + log.detail('Original prompt', generation.originalPrompt); + log.detail('Enhanced prompt', generation.prompt); + log.detail('autoEnhance', generation.autoEnhance); + log.detail('Enhancement confirmed', '✓'); + }); + + // Test 4: Verify enhanced prompt is actually different and longer + await runTest('Verify enhancement quality', async () => { + const result = await api(`${endpoints.generations}/${testContext.enhancedGenId}`); + const generation = result.data.data; + + const originalLength = generation.originalPrompt?.length || 0; + const enhancedLength = generation.prompt?.length || 0; + + if (enhancedLength <= originalLength) { + log.warning('Enhanced prompt not longer than original (might not be truly enhanced)'); + } else { + log.detail('Original length', originalLength); + log.detail('Enhanced length', enhancedLength); + log.detail('Increase', `+${enhancedLength - originalLength} chars`); + } + + // Verify the enhanced prompt contains more descriptive language + const hasPhotorealistic = generation.prompt.toLowerCase().includes('photorealistic') || + generation.prompt.toLowerCase().includes('realistic') || + generation.prompt.toLowerCase().includes('detailed'); + + if (hasPhotorealistic) { + log.detail('Enhancement adds descriptive terms', '✓'); + } + }); + + // Test 5: Verify both enhanced and non-enhanced are in listings + await runTest('List generations - verify autoEnhance field', async () => { + const result = await api(endpoints.generations); + + if (!result.data.data || !Array.isArray(result.data.data)) { + throw new Error('No generations array returned'); + } + + const enhancedGens = result.data.data.filter((g: any) => g.autoEnhance === true); + const notEnhancedGens = result.data.data.filter((g: any) => g.autoEnhance === false); + + log.detail('Total generations', result.data.data.length); + log.detail('Enhanced', enhancedGens.length); + log.detail('Not enhanced', notEnhancedGens.length); + + if (enhancedGens.length === 0) { + throw new Error('Should have at least one enhanced generation'); + } + + if (notEnhancedGens.length === 0) { + throw new Error('Should have at least one non-enhanced generation'); + } + }); + + // Test 6: Verify response structure + await runTest('Verify response includes all enhancement fields', async () => { + const result = await api(`${endpoints.generations}/${testContext.enhancedGenId}`); + const generation = result.data.data; + + // Required fields + if (typeof generation.prompt !== 'string') { + throw new Error('prompt should be string'); + } + + if (typeof generation.autoEnhance !== 'boolean') { + throw new Error('autoEnhance should be boolean'); + } + + // originalPrompt can be null or string + if (generation.originalPrompt !== null && typeof generation.originalPrompt !== 'string') { + throw new Error('originalPrompt should be null or string'); + } + + log.detail('Response structure', 'valid ✓'); + log.detail('prompt type', typeof generation.prompt); + log.detail('originalPrompt type', typeof generation.originalPrompt || 'null'); + log.detail('autoEnhance type', typeof generation.autoEnhance); + }); + + log.section('AUTO-ENHANCE TESTS COMPLETED'); +} + +main().catch(console.error); diff --git a/tests/api/run-all.ts b/tests/api/run-all.ts index 36138ff..447d1c1 100644 --- a/tests/api/run-all.ts +++ b/tests/api/run-all.ts @@ -20,6 +20,7 @@ const testFiles = [ '05-live.ts', '06-edge-cases.ts', '07-known-issues.ts', + '08-auto-enhance.ts', ]; async function runTest(file: string): Promise<{ success: boolean; duration: number }> {