diff --git a/.env.example b/.env.example index e67e83c..2eab90d 100644 --- a/.env.example +++ b/.env.example @@ -17,4 +17,7 @@ RESULTS_DIR=./results UPLOADS_DIR=./uploads/temp # Logging Configuration -LOG_LEVEL=info \ No newline at end of file +LOG_LEVEL=info + +# Text-to-Image Logging (optional) +# TTI_LOG=./apps/api-service/logs/tti.log \ No newline at end of file diff --git a/apps/api-service/src/services/ImageGenService.ts b/apps/api-service/src/services/ImageGenService.ts index 4e45804..5d804b6 100644 --- a/apps/api-service/src/services/ImageGenService.ts +++ b/apps/api-service/src/services/ImageGenService.ts @@ -9,6 +9,7 @@ import { GeminiParams, } from "../types/api"; import { StorageFactory } from "./StorageFactory"; +import { TTILogger, TTILogEntry } from "./TTILogger"; export class ImageGenService { private ai: GoogleGenAI; @@ -39,7 +40,12 @@ export class ImageGenService { let generatedData: GeneratedImageData; let geminiParams: GeminiParams; try { - const aiResult = await this.generateImageWithAI(prompt, referenceImages); + const aiResult = await this.generateImageWithAI( + prompt, + referenceImages, + finalOrgId, + finalProjectId, + ); generatedData = aiResult.generatedData; geminiParams = aiResult.geminiParams; } catch (error) { @@ -114,8 +120,13 @@ export class ImageGenService { */ private async generateImageWithAI( prompt: string, - referenceImages?: ReferenceImage[], - ): Promise<{ generatedData: GeneratedImageData; geminiParams: GeminiParams }> { + referenceImages: ReferenceImage[] | undefined, + orgId: string, + projectId: string, + ): Promise<{ + generatedData: GeneratedImageData; + geminiParams: GeminiParams; + }> { const contentParts: any[] = []; // Add reference images if provided @@ -135,6 +146,8 @@ export class ImageGenService { text: prompt, }); + // CRITICAL: Calculate exact values before SDK call + // These exact objects will be passed to both SDK and logger const contents = [ { role: "user" as const, @@ -155,7 +168,29 @@ export class ImageGenService { }, }; + // Log TTI request BEFORE SDK call - using exact same values + const ttiLogger = TTILogger.getInstance(); + const logEntry: TTILogEntry = { + timestamp: new Date().toISOString(), + orgId, + projectId, + prompt, + model: this.primaryModel, + config, + ...(referenceImages && + referenceImages.length > 0 && { + referenceImages: referenceImages.map((img) => ({ + mimetype: img.mimetype, + size: img.buffer.length, + originalname: img.originalname, + })), + }), + }; + + ttiLogger.log(logEntry); + try { + // Use the EXACT same config and contents objects calculated above const response = await this.ai.models.generateContent({ model: this.primaryModel, config, diff --git a/apps/api-service/src/services/TTILogger.ts b/apps/api-service/src/services/TTILogger.ts new file mode 100644 index 0000000..20cde3b --- /dev/null +++ b/apps/api-service/src/services/TTILogger.ts @@ -0,0 +1,101 @@ +import { writeFileSync, appendFileSync, existsSync, mkdirSync } from "fs"; +import { dirname } from "path"; + +export interface TTILogEntry { + timestamp: string; + orgId: string; + projectId: string; + prompt: string; + referenceImages?: Array<{ + mimetype: string; + size: number; + originalname: string; + }>; + model: string; + config: any; +} + +export class TTILogger { + private static instance: TTILogger | null = null; + private logFilePath: string | null = null; + private isEnabled: boolean = false; + + private constructor() { + const ttiLogPath = process.env["TTI_LOG"]; + + if (ttiLogPath) { + this.logFilePath = ttiLogPath; + this.isEnabled = true; + this.initializeLogFile(); + } + } + + static getInstance(): TTILogger { + if (!TTILogger.instance) { + TTILogger.instance = new TTILogger(); + } + return TTILogger.instance; + } + + private initializeLogFile(): void { + if (!this.logFilePath) return; + + try { + // Ensure directory exists + const dir = dirname(this.logFilePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + // Reset/clear the log file on service start + writeFileSync(this.logFilePath, "# Text-to-Image Generation Log\n\n", { + encoding: "utf-8", + }); + + console.log(`[TTILogger] Log file initialized: ${this.logFilePath}`); + } catch (error) { + console.error(`[TTILogger] Failed to initialize log file:`, error); + this.isEnabled = false; + } + } + + log(entry: TTILogEntry): void { + if (!this.isEnabled || !this.logFilePath) { + return; + } + + try { + const logEntry = this.formatLogEntry(entry); + appendFileSync(this.logFilePath, logEntry, { encoding: "utf-8" }); + } catch (error) { + console.error(`[TTILogger] Failed to write log entry:`, error); + } + } + + private formatLogEntry(entry: TTILogEntry): string { + const { timestamp, orgId, projectId, prompt, referenceImages, model, config } = entry; + + // Format date from ISO timestamp + const date = new Date(timestamp); + const formattedDate = date.toISOString().replace("T", " ").slice(0, 19); + + let logText = `## ${formattedDate}\n`; + logText += `${orgId}/${projectId}\n\n`; + logText += `**Prompt:** ${prompt}\n\n`; + + if (referenceImages && referenceImages.length > 0) { + logText += `**Reference Images:** ${referenceImages.length} image${referenceImages.length > 1 ? "s" : ""}\n`; + for (const img of referenceImages) { + const sizeMB = (img.size / (1024 * 1024)).toFixed(2); + logText += `- ${img.originalname} (${img.mimetype}, ${sizeMB} MB)\n`; + } + logText += "\n"; + } + + logText += `**Model:** ${model}\n`; + logText += `**Config:** ${JSON.stringify(config)}\n\n`; + logText += `---\n\n`; + + return logText; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 6570e3a..583e79a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: - LOG_LEVEL=${LOG_LEVEL} - PORT=${PORT} - CORS_ORIGIN=${CORS_ORIGIN} + - TTI_LOG=${TTI_LOG} restart: unless-stopped postgres: