fix: image generation
This commit is contained in:
parent
babcbe29db
commit
f572428a87
|
|
@ -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[] {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue