feat: add aspect ratio

This commit is contained in:
Oleg Proskurin 2025-10-07 21:13:05 +07:00
parent 620a2a9caa
commit 585b446ca5
6 changed files with 57 additions and 16 deletions

View File

@ -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 * Validate the text-to-image JSON request
*/ */
@ -25,7 +39,7 @@ export const validateTextToImageRequest = (
next: NextFunction, next: NextFunction,
): void | Response => { ): void | Response => {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const { prompt, filename, autoEnhance, enhancementOptions } = req.body; const { prompt, filename, aspectRatio, autoEnhance, enhancementOptions } = req.body;
const errors: string[] = []; const errors: string[] = [];
console.log( 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) // Validate autoEnhance (optional boolean)
if (autoEnhance !== undefined && typeof autoEnhance !== "boolean") { if (autoEnhance !== undefined && typeof autoEnhance !== "boolean") {
errors.push("autoEnhance must be a boolean"); errors.push("autoEnhance must be a boolean");
@ -111,11 +136,11 @@ export const validateTextToImageRequest = (
if ( if (
aspectRatio !== undefined && aspectRatio !== undefined &&
!["square", "portrait", "landscape", "wide", "ultrawide"].includes( !VALID_ASPECT_RATIOS.includes(aspectRatio as any)
aspectRatio,
)
) { ) {
errors.push("Invalid aspectRatio in enhancementOptions"); errors.push(
`Invalid aspectRatio. Must be one of: ${VALID_ASPECT_RATIOS.join(", ")}`
);
} }
if ( if (

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 } = req.body; const { prompt, filename, aspectRatio } = 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
@ -108,6 +108,7 @@ generateRouter.post(
const result = await imageGenService.generateImage({ const result = await imageGenService.generateImage({
prompt, prompt,
filename, filename,
...(aspectRatio && { aspectRatio }),
orgId, orgId,
projectId, projectId,
...(referenceImages && { referenceImages }), ...(referenceImages && { referenceImages }),

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 } = req.body; const { prompt, filename, aspectRatio } = 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;
@ -73,6 +73,7 @@ textToImageRouter.post(
const result = await imageGenService.generateImage({ const result = await imageGenService.generateImage({
prompt, prompt,
filename, filename,
...(aspectRatio && { aspectRatio }),
orgId, orgId,
projectId, projectId,
}); });

View File

@ -29,12 +29,13 @@ export class ImageGenService {
async generateImage( async generateImage(
options: ImageGenerationOptions, options: ImageGenerationOptions,
): Promise<ImageGenerationResult> { ): Promise<ImageGenerationResult> {
const { prompt, filename, referenceImages, orgId, projectId } = options; const { prompt, filename, referenceImages, aspectRatio, orgId, projectId } = 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";
const finalProjectId = const finalProjectId =
projectId || process.env["DEFAULT_PROJECT_ID"] || "main"; projectId || process.env["DEFAULT_PROJECT_ID"] || "main";
const finalAspectRatio = aspectRatio || "1:1"; // Default to square
// Step 1: Generate image from Gemini AI // Step 1: Generate image from Gemini AI
let generatedData: GeneratedImageData; let generatedData: GeneratedImageData;
@ -43,6 +44,7 @@ export class ImageGenService {
const aiResult = await this.generateImageWithAI( const aiResult = await this.generateImageWithAI(
prompt, prompt,
referenceImages, referenceImages,
finalAspectRatio,
finalOrgId, finalOrgId,
finalProjectId, finalProjectId,
); );
@ -121,6 +123,7 @@ export class ImageGenService {
private async generateImageWithAI( private async generateImageWithAI(
prompt: string, prompt: string,
referenceImages: ReferenceImage[] | undefined, referenceImages: ReferenceImage[] | undefined,
aspectRatio: string,
orgId: string, orgId: string,
projectId: string, projectId: string,
): Promise<{ ): 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 // Capture Gemini SDK parameters for debugging
const geminiParams: GeminiParams = { const geminiParams: GeminiParams = {

View File

@ -59,6 +59,7 @@ export interface ImageGenerationOptions {
prompt: string; prompt: string;
filename: string; filename: string;
referenceImages?: ReferenceImage[]; referenceImages?: ReferenceImage[];
aspectRatio?: string;
orgId?: string; orgId?: string;
projectId?: string; projectId?: string;
userId?: string; userId?: string;
@ -74,6 +75,9 @@ export interface GeminiParams {
model: string; model: string;
config: { config: {
responseModalities: string[]; responseModalities: string[];
imageConfig?: {
aspectRatio?: string;
};
}; };
contentsStructure: { contentsStructure: {
role: string; role: string;

View File

@ -64,7 +64,7 @@ export default function DemoTTIPage() {
const [generationError, setGenerationError] = useState(''); const [generationError, setGenerationError] = useState('');
// Enhancement Options State // Enhancement Options State
const [aspectRatio, setAspectRatio] = useState(''); const [aspectRatio, setAspectRatio] = useState('1:1');
const [imageStyle, setImageStyle] = useState(''); const [imageStyle, setImageStyle] = useState('');
const [advancedOptions, setAdvancedOptions] = useState<AdvancedOptionsData>({}); const [advancedOptions, setAdvancedOptions] = useState<AdvancedOptionsData>({});
const [showAdvancedModal, setShowAdvancedModal] = useState(false); const [showAdvancedModal, setShowAdvancedModal] = useState(false);
@ -241,6 +241,7 @@ export default function DemoTTIPage() {
body: JSON.stringify({ body: JSON.stringify({
prompt: prompt.trim(), prompt: prompt.trim(),
filename: `demo_${resultId}_left`, filename: `demo_${resultId}_left`,
aspectRatio,
}), }),
}), }),
fetch(`${API_BASE_URL}/api/text-to-image`, { fetch(`${API_BASE_URL}/api/text-to-image`, {
@ -252,6 +253,7 @@ export default function DemoTTIPage() {
body: JSON.stringify({ body: JSON.stringify({
prompt: prompt.trim(), prompt: prompt.trim(),
filename: `demo_${resultId}_right`, filename: `demo_${resultId}_right`,
aspectRatio,
autoEnhance: true, autoEnhance: true,
...(hasEnhancementOptions && { ...(hasEnhancementOptions && {
enhancementOptions: rightEnhancementOptions enhancementOptions: rightEnhancementOptions
@ -488,12 +490,12 @@ export default function DemoTTIPage() {
disabled={!apiKeyValidated || generating} 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" 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"
> >
<option value="">Auto</option> <option value="1:1">Square (1:1)</option>
<option value="square">Square (1:1)</option> <option value="3:4">Portrait (3:4)</option>
<option value="portrait">Portrait (3:4)</option> <option value="4:3">Landscape (4:3)</option>
<option value="landscape">Landscape (4:3)</option> <option value="9:16">Vertical (9:16)</option>
<option value="wide">Wide (16:9)</option> <option value="16:9">Widescreen (16:9)</option>
<option value="ultrawide">Ultrawide (21:9)</option> <option value="21:9">Ultrawide (21:9)</option>
</select> </select>
</div> </div>