fix: image generation

This commit is contained in:
Oleg Proskurin 2025-09-29 22:49:32 +07:00
parent babcbe29db
commit f572428a87
4 changed files with 48 additions and 99 deletions

View File

@ -8,6 +8,7 @@ import {
ReferenceImage, ReferenceImage,
} from "../types/api"; } from "../types/api";
import { StorageFactory } from "./StorageFactory"; import { StorageFactory } from "./StorageFactory";
import { UploadResult } from "./StorageService";
export class ImageGenService { export class ImageGenService {
private ai: GoogleGenAI; private ai: GoogleGenAI;
@ -27,20 +28,18 @@ export class ImageGenService {
async generateImage( async generateImage(
options: ImageGenerationOptions, options: ImageGenerationOptions,
): Promise<ImageGenerationResult> { ): Promise<ImageGenerationResult> {
const { prompt, filename, referenceImages, orgId, projectId, userId } = options; const { prompt, filename, referenceImages, orgId, projectId, userId } =
options;
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
// 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 = projectId || process.env['DEFAULT_PROJECT_ID'] || 'main'; const finalProjectId =
const finalUserId = userId || process.env['DEFAULT_USER_ID'] || 'system'; projectId || process.env["DEFAULT_PROJECT_ID"] || "main";
const finalUserId = userId || process.env["DEFAULT_USER_ID"] || "system";
console.log(
`[${timestamp}] Starting image generation: "${prompt.substring(0, 50)}..." for ${finalOrgId}/${finalProjectId}`,
);
try { try {
// First try the primary model (Nano Banana)
const result = await this.tryGeneration({ const result = await this.tryGeneration({
model: this.primaryModel, model: this.primaryModel,
config: { responseModalities: ["IMAGE", "TEXT"] }, config: { responseModalities: ["IMAGE", "TEXT"] },
@ -50,17 +49,13 @@ export class ImageGenService {
projectId: finalProjectId, projectId: finalProjectId,
userId: finalUserId, userId: finalUserId,
...(referenceImages && { referenceImages }), ...(referenceImages && { referenceImages }),
modelName: "Nano Banana", modelName: "Primary Model",
}); });
if (result.success) { if (result.success) {
return result; return result;
} }
// Fallback to Imagen 4
console.log(
`[${new Date().toISOString()}] Primary model failed, trying fallback (Imagen 4)...`,
);
return await this.tryGeneration({ return await this.tryGeneration({
model: this.fallbackModel, model: this.fallbackModel,
@ -71,13 +66,9 @@ export class ImageGenService {
projectId: finalProjectId, projectId: finalProjectId,
userId: finalUserId, userId: finalUserId,
...(referenceImages && { referenceImages }), ...(referenceImages && { referenceImages }),
modelName: "Imagen 4", modelName: "Fallback Model",
}); });
} catch (error) { } catch (error) {
console.error(
`[${new Date().toISOString()}] Image generation failed:`,
error,
);
return { return {
success: false, success: false,
model: "none", model: "none",
@ -87,9 +78,6 @@ export class ImageGenService {
} }
} }
/**
* Try generation with a specific model
*/
private async tryGeneration(params: { private async tryGeneration(params: {
model: string; model: string;
config: { responseModalities: string[] }; config: { responseModalities: string[] };
@ -101,18 +89,22 @@ export class ImageGenService {
referenceImages?: ReferenceImage[]; referenceImages?: ReferenceImage[];
modelName: string; modelName: string;
}): Promise<ImageGenerationResult> { }): Promise<ImageGenerationResult> {
const { model, config, prompt, filename, orgId, projectId, userId, referenceImages, modelName } = const {
params; model,
config,
prompt,
filename,
orgId,
projectId,
userId,
referenceImages,
modelName,
} = params;
try { try {
// Build content parts for the API request
const contentParts: any[] = []; const contentParts: any[] = [];
// Add reference images if provided
if (referenceImages && referenceImages.length > 0) { if (referenceImages && referenceImages.length > 0) {
console.log(
`[${new Date().toISOString()}] Adding ${referenceImages.length} reference image(s)`,
);
for (const refImage of referenceImages) { for (const refImage of referenceImages) {
contentParts.push({ contentParts.push({
@ -124,7 +116,6 @@ export class ImageGenService {
} }
} }
// Add the text prompt
contentParts.push({ contentParts.push({
text: prompt, text: prompt,
}); });
@ -136,9 +127,6 @@ export class ImageGenService {
}, },
]; ];
console.log(
`[${new Date().toISOString()}] Making API request to ${modelName} (${model})...`,
);
const response = await this.ai.models.generateContent({ const response = await this.ai.models.generateContent({
model, model,
@ -146,9 +134,6 @@ export class ImageGenService {
contents, contents,
}); });
console.log(
`[${new Date().toISOString()}] Response received from ${modelName}`,
);
if ( if (
response.candidates && response.candidates &&
@ -157,7 +142,7 @@ export class ImageGenService {
) { ) {
const content = response.candidates[0].content; const content = response.candidates[0].content;
let generatedDescription = ""; let generatedDescription = "";
let uploadResult = null; let uploadResult: UploadResult | null = null;
for (let index = 0; index < (content.parts?.length || 0); index++) { for (let index = 0; index < (content.parts?.length || 0); index++) {
const part = content.parts?.[index]; const part = content.parts?.[index];
@ -168,36 +153,30 @@ export class ImageGenService {
part.inlineData.mimeType || "", part.inlineData.mimeType || "",
); );
const finalFilename = `${filename}.${fileExtension}`; const finalFilename = `${filename}.${fileExtension}`;
const contentType = part.inlineData.mimeType || `image/${fileExtension}`; const contentType =
part.inlineData.mimeType || `image/${fileExtension}`;
console.log(
`[${new Date().toISOString()}] Uploading image to MinIO: ${finalFilename}`,
);
const buffer = Buffer.from(part.inlineData.data || "", "base64"); const buffer = Buffer.from(part.inlineData.data || "", "base64");
// Upload to MinIO storage
const storageService = StorageFactory.getInstance(); const storageService = StorageFactory.getInstance();
uploadResult = await storageService.uploadFile( const result = (await storageService).uploadFile(
orgId, orgId,
projectId, projectId,
'generated', "generated",
finalFilename, finalFilename,
buffer, buffer,
contentType contentType,
); );
console.log( uploadResult = await result;
`[${new Date().toISOString()}] Image uploaded successfully: ${uploadResult.path}`,
);
} else if (part.text) { } else if (part.text) {
generatedDescription = part.text; generatedDescription = part.text;
console.log(
`[${new Date().toISOString()}] Generated description: ${part.text.substring(0, 100)}...`,
);
} }
} }
if (uploadResult && uploadResult.success) { if (uploadResult && uploadResult.success) {
return { return {
success: true, success: true,
@ -216,10 +195,6 @@ export class ImageGenService {
error: "No image data received from API", error: "No image data received from API",
}; };
} catch (error) { } catch (error) {
console.error(
`[${new Date().toISOString()}] ${modelName} generation failed:`,
error,
);
return { return {
success: false, success: false,
model: modelName, model: modelName,
@ -228,10 +203,6 @@ export class ImageGenService {
} }
} }
/**
* Validate reference images
*/
static validateReferenceImages(files: Express.Multer.File[]): { static validateReferenceImages(files: Express.Multer.File[]): {
valid: boolean; valid: boolean;
error?: string; error?: string;
@ -262,9 +233,6 @@ export class ImageGenService {
return { valid: true }; return { valid: true };
} }
/**
* Convert Express.Multer.File[] to ReferenceImage[]
*/
static convertFilesToReferenceImages( static convertFilesToReferenceImages(
files: Express.Multer.File[], files: Express.Multer.File[],
): ReferenceImage[] { ): ReferenceImage[] {

View File

@ -312,7 +312,6 @@ export class MinioStorageService implements StorageService {
createdAt: obj.lastModified || new Date() createdAt: obj.lastModified || new Date()
}); });
} catch (error) { } catch (error) {
console.error(`Error processing file ${obj.name}:`, error);
} }
}); });
@ -328,7 +327,6 @@ export class MinioStorageService implements StorageService {
filename: string; filename: string;
} | null { } | null {
try { try {
// Key format: banatie/orgId/projectId/category/year-month/filename
const match = key.match(/^banatie\/([^/]+)\/([^/]+)\/(uploads|generated|references)\/[^/]+\/(.+)$/); const match = key.match(/^banatie\/([^/]+)\/([^/]+)\/(uploads|generated|references)\/[^/]+\/(.+)$/);
if (!match) { if (!match) {
@ -352,7 +350,6 @@ export class MinioStorageService implements StorageService {
} }
} }
// MISSING METHODS FROM INTERFACE
async fileExists( async fileExists(
orgId: string, orgId: string,
@ -376,7 +373,7 @@ export class MinioStorageService implements StorageService {
category: 'uploads' | 'generated' | 'references', category: 'uploads' | 'generated' | 'references',
prefix?: string prefix?: string
): Promise<FileMetadata[]> { ): Promise<FileMetadata[]> {
this.validateFilePath(orgId, projectId, category, 'dummy.txt'); // Validate path components this.validateFilePath(orgId, projectId, category, 'dummy.txt');
const basePath = `${orgId}/${projectId}/${category}/`; const basePath = `${orgId}/${projectId}/${category}/`;
const searchPrefix = prefix ? `${basePath}${prefix}` : basePath; const searchPrefix = prefix ? `${basePath}${prefix}` : basePath;
@ -406,7 +403,6 @@ export class MinioStorageService implements StorageService {
path: obj.name path: obj.name
}); });
} catch (error) { } catch (error) {
console.error(`Error processing file ${obj.name}:`, error);
} }
}); });

View File

@ -25,13 +25,11 @@ export class StorageFactory {
} }
} }
// Synchronous version for backward compatibility (with graceful degradation)
static getInstanceSync(): StorageService { static getInstanceSync(): StorageService {
if (!this.instance) { if (!this.instance) {
try { try {
this.instance = this.createStorageService(); this.instance = this.createStorageService();
} catch (error) { } catch (error) {
console.error('Failed to create storage service:', error);
throw new Error('Storage service unavailable. Please check MinIO configuration.'); throw new Error('Storage service unavailable. Please check MinIO configuration.');
} }
} }
@ -45,18 +43,14 @@ export class StorageFactory {
for (let attempt = 1; attempt <= maxRetries; attempt++) { for (let attempt = 1; attempt <= maxRetries; attempt++) {
try { try {
console.log(`Attempting to create storage service (attempt ${attempt}/${maxRetries})`);
const service = this.createStorageService(); const service = this.createStorageService();
// Test the connection by checking if bucket exists
await service.bucketExists(); await service.bucketExists();
console.log('Storage service created successfully');
return service; return service;
} catch (error) { } catch (error) {
console.error(`Storage service creation attempt ${attempt} failed:`, error);
if (attempt === maxRetries) { if (attempt === maxRetries) {
throw new Error( throw new Error(
@ -65,9 +59,7 @@ export class StorageFactory {
); );
} }
// Exponential backoff
const delay = baseDelay * Math.pow(2, attempt - 1); const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Waiting ${delay}ms before retry...`);
await this.sleep(delay); await this.sleep(delay);
} }
} }
@ -98,11 +90,6 @@ export class StorageFactory {
); );
} }
console.log(`Initializing MinIO Storage Service:`);
console.log(` Endpoint: ${endpoint}`);
console.log(` Bucket: ${bucketName}`);
console.log(` SSL: ${useSSL}`);
console.log(` Public URL: ${publicUrl}`);
return new MinioStorageService( return new MinioStorageService(
endpoint, endpoint,
@ -118,12 +105,10 @@ export class StorageFactory {
throw new Error(`Unsupported storage type: ${storageType}`); throw new Error(`Unsupported storage type: ${storageType}`);
} }
} catch (error) { } catch (error) {
console.error('Error creating storage service:', error);
throw error; throw error;
} }
} }
// Reset instance for testing
static resetInstance(): void { static resetInstance(): void {
this.instance = null; this.instance = null;
this.initializationPromise = null; this.initializationPromise = null;

View File

@ -1,4 +1,4 @@
import { Readable } from 'stream'; import { Readable } from "stream";
export interface FileMetadata { export interface FileMetadata {
filename: string; filename: string;
@ -42,10 +42,10 @@ export interface StorageService {
uploadFile( uploadFile(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
filename: string, filename: string,
buffer: Buffer, buffer: Buffer,
contentType: string contentType: string,
): Promise<UploadResult>; ): Promise<UploadResult>;
/** /**
@ -58,8 +58,8 @@ export interface StorageService {
downloadFile( downloadFile(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
filename: string filename: string,
): Promise<Buffer>; ): Promise<Buffer>;
/** /**
@ -72,8 +72,8 @@ export interface StorageService {
streamFile( streamFile(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
filename: string filename: string,
): Promise<Readable>; ): Promise<Readable>;
/** /**
@ -87,9 +87,9 @@ export interface StorageService {
getPresignedDownloadUrl( getPresignedDownloadUrl(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
filename: string, filename: string,
expirySeconds: number expirySeconds: number,
): Promise<string>; ): Promise<string>;
/** /**
@ -104,10 +104,10 @@ export interface StorageService {
getPresignedUploadUrl( getPresignedUploadUrl(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
filename: string, filename: string,
expirySeconds: number, expirySeconds: number,
contentType: string contentType: string,
): Promise<string>; ): Promise<string>;
/** /**
@ -120,8 +120,8 @@ export interface StorageService {
listFiles( listFiles(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
prefix?: string prefix?: string,
): Promise<FileMetadata[]>; ): Promise<FileMetadata[]>;
/** /**
@ -134,8 +134,8 @@ export interface StorageService {
deleteFile( deleteFile(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
filename: string filename: string,
): Promise<void>; ): Promise<void>;
/** /**
@ -148,7 +148,7 @@ export interface StorageService {
fileExists( fileExists(
orgId: string, orgId: string,
projectId: string, projectId: string,
category: 'uploads' | 'generated' | 'references', category: "uploads" | "generated" | "references",
filename: string filename: string,
): Promise<boolean>; ): Promise<boolean>;
} }