From 585b446ca55a17f701672bd5033a33bb0f8ed003 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Tue, 7 Oct 2025 21:13:05 +0700 Subject: [PATCH] feat: add aspect ratio --- .../src/middleware/jsonValidation.ts | 35 ++++++++++++++++--- apps/api-service/src/routes/generate.ts | 3 +- apps/api-service/src/routes/textToImage.ts | 3 +- .../src/services/ImageGenService.ts | 12 +++++-- apps/api-service/src/types/api.ts | 4 +++ apps/landing/src/app/demo/tti/page.tsx | 16 +++++---- 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/apps/api-service/src/middleware/jsonValidation.ts b/apps/api-service/src/middleware/jsonValidation.ts index 1461375..5085f87 100644 --- a/apps/api-service/src/middleware/jsonValidation.ts +++ b/apps/api-service/src/middleware/jsonValidation.ts @@ -16,6 +16,20 @@ const VALIDATION_RULES = { }, }; +// Valid aspect ratios supported by Gemini SDK +const VALID_ASPECT_RATIOS = [ + "1:1", // Square (1024x1024) + "2:3", // Portrait (832x1248) + "3:2", // Landscape (1248x832) + "3:4", // Portrait (864x1184) + "4:3", // Landscape (1184x864) + "4:5", // Portrait (896x1152) + "5:4", // Landscape (1152x896) + "9:16", // Vertical (768x1344) + "16:9", // Widescreen (1344x768) + "21:9", // Ultrawide (1536x672) +] as const; + /** * Validate the text-to-image JSON request */ @@ -25,7 +39,7 @@ export const validateTextToImageRequest = ( next: NextFunction, ): void | Response => { const timestamp = new Date().toISOString(); - const { prompt, filename, autoEnhance, enhancementOptions } = req.body; + const { prompt, filename, aspectRatio, autoEnhance, enhancementOptions } = req.body; const errors: string[] = []; console.log( @@ -73,6 +87,17 @@ export const validateTextToImageRequest = ( ); } + // Validate aspectRatio (optional, defaults to "1:1") + if (aspectRatio !== undefined) { + if (typeof aspectRatio !== "string") { + errors.push("aspectRatio must be a string"); + } else if (!VALID_ASPECT_RATIOS.includes(aspectRatio as any)) { + errors.push( + `Invalid aspectRatio. Must be one of: ${VALID_ASPECT_RATIOS.join(", ")}` + ); + } + } + // Validate autoEnhance (optional boolean) if (autoEnhance !== undefined && typeof autoEnhance !== "boolean") { errors.push("autoEnhance must be a boolean"); @@ -111,11 +136,11 @@ export const validateTextToImageRequest = ( if ( aspectRatio !== undefined && - !["square", "portrait", "landscape", "wide", "ultrawide"].includes( - aspectRatio, - ) + !VALID_ASPECT_RATIOS.includes(aspectRatio as any) ) { - errors.push("Invalid aspectRatio in enhancementOptions"); + errors.push( + `Invalid aspectRatio. Must be one of: ${VALID_ASPECT_RATIOS.join(", ")}` + ); } if ( diff --git a/apps/api-service/src/routes/generate.ts b/apps/api-service/src/routes/generate.ts index d8274ee..562788d 100644 --- a/apps/api-service/src/routes/generate.ts +++ b/apps/api-service/src/routes/generate.ts @@ -63,7 +63,7 @@ generateRouter.post( const timestamp = new Date().toISOString(); const requestId = req.requestId; - const { prompt, filename } = req.body; + const { prompt, filename, aspectRatio } = req.body; const files = (req.files as Express.Multer.File[]) || []; // Extract org/project slugs from validated API key @@ -108,6 +108,7 @@ generateRouter.post( const result = await imageGenService.generateImage({ prompt, filename, + ...(aspectRatio && { aspectRatio }), orgId, projectId, ...(referenceImages && { referenceImages }), diff --git a/apps/api-service/src/routes/textToImage.ts b/apps/api-service/src/routes/textToImage.ts index 95e0e16..5535b0d 100644 --- a/apps/api-service/src/routes/textToImage.ts +++ b/apps/api-service/src/routes/textToImage.ts @@ -54,7 +54,7 @@ textToImageRouter.post( const timestamp = new Date().toISOString(); const requestId = req.requestId; - const { prompt, filename } = req.body; + const { prompt, filename, aspectRatio } = req.body; // Extract org/project slugs from validated API key const orgId = req.apiKey?.organizationSlug || undefined; @@ -73,6 +73,7 @@ textToImageRouter.post( const result = await imageGenService.generateImage({ prompt, filename, + ...(aspectRatio && { aspectRatio }), orgId, projectId, }); diff --git a/apps/api-service/src/services/ImageGenService.ts b/apps/api-service/src/services/ImageGenService.ts index 2a09d3d..68c611a 100644 --- a/apps/api-service/src/services/ImageGenService.ts +++ b/apps/api-service/src/services/ImageGenService.ts @@ -29,12 +29,13 @@ export class ImageGenService { async generateImage( options: ImageGenerationOptions, ): Promise { - const { prompt, filename, referenceImages, orgId, projectId } = options; + const { prompt, filename, referenceImages, aspectRatio, orgId, projectId } = options; // Use default values if not provided const finalOrgId = orgId || process.env["DEFAULT_ORG_ID"] || "default"; const finalProjectId = projectId || process.env["DEFAULT_PROJECT_ID"] || "main"; + const finalAspectRatio = aspectRatio || "1:1"; // Default to square // Step 1: Generate image from Gemini AI let generatedData: GeneratedImageData; @@ -43,6 +44,7 @@ export class ImageGenService { const aiResult = await this.generateImageWithAI( prompt, referenceImages, + finalAspectRatio, finalOrgId, finalProjectId, ); @@ -121,6 +123,7 @@ export class ImageGenService { private async generateImageWithAI( prompt: string, referenceImages: ReferenceImage[] | undefined, + aspectRatio: string, orgId: string, projectId: string, ): Promise<{ @@ -155,7 +158,12 @@ export class ImageGenService { }, ]; - const config = { responseModalities: ["IMAGE", "TEXT"] }; + const config = { + responseModalities: ["IMAGE", "TEXT"], + imageConfig: { + aspectRatio, + }, + }; // Capture Gemini SDK parameters for debugging const geminiParams: GeminiParams = { diff --git a/apps/api-service/src/types/api.ts b/apps/api-service/src/types/api.ts index 73ca91c..a047c0d 100644 --- a/apps/api-service/src/types/api.ts +++ b/apps/api-service/src/types/api.ts @@ -59,6 +59,7 @@ export interface ImageGenerationOptions { prompt: string; filename: string; referenceImages?: ReferenceImage[]; + aspectRatio?: string; orgId?: string; projectId?: string; userId?: string; @@ -74,6 +75,9 @@ export interface GeminiParams { model: string; config: { responseModalities: string[]; + imageConfig?: { + aspectRatio?: string; + }; }; contentsStructure: { role: string; diff --git a/apps/landing/src/app/demo/tti/page.tsx b/apps/landing/src/app/demo/tti/page.tsx index f954686..4d45fa5 100644 --- a/apps/landing/src/app/demo/tti/page.tsx +++ b/apps/landing/src/app/demo/tti/page.tsx @@ -64,7 +64,7 @@ export default function DemoTTIPage() { const [generationError, setGenerationError] = useState(''); // Enhancement Options State - const [aspectRatio, setAspectRatio] = useState(''); + const [aspectRatio, setAspectRatio] = useState('1:1'); const [imageStyle, setImageStyle] = useState(''); const [advancedOptions, setAdvancedOptions] = useState({}); const [showAdvancedModal, setShowAdvancedModal] = useState(false); @@ -241,6 +241,7 @@ export default function DemoTTIPage() { body: JSON.stringify({ prompt: prompt.trim(), filename: `demo_${resultId}_left`, + aspectRatio, }), }), fetch(`${API_BASE_URL}/api/text-to-image`, { @@ -252,6 +253,7 @@ export default function DemoTTIPage() { body: JSON.stringify({ prompt: prompt.trim(), filename: `demo_${resultId}_right`, + aspectRatio, autoEnhance: true, ...(hasEnhancementOptions && { enhancementOptions: rightEnhancementOptions @@ -488,12 +490,12 @@ export default function DemoTTIPage() { disabled={!apiKeyValidated || generating} className="w-full px-3 py-2 text-sm bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed" > - - - - - - + + + + + +