@loonylabs/tti-middleware 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,7 @@
48
48
  - **Retry Logic**: Automatic retry for rate limits (429) with configurable backoff
49
49
  - **TypeScript First**: Full type safety with comprehensive interfaces
50
50
  - **Logging Control**: Configurable log levels via environment or API
51
+ - **Debug Logging**: Markdown file logging for debugging prompts and responses
51
52
  - **Error Handling**: Typed error classes for precise error handling
52
53
 
53
54
  ## Quick Start
@@ -103,7 +104,7 @@ const character = await service.generate({
103
104
  model: 'gemini-flash-image', // Only this model supports character consistency!
104
105
  });
105
106
 
106
- // 2. Generate new scenes with the same character
107
+ // 2. Generate new scenes with the same character (Structured Mode)
107
108
  const scene = await service.generate({
108
109
  prompt: 'dancing happily in the rain, jumping in puddles',
109
110
  model: 'gemini-flash-image',
@@ -113,6 +114,17 @@ const scene = await service.generate({
113
114
  }],
114
115
  subjectDescription: 'cute cartoon bear with red hat and blue scarf',
115
116
  });
117
+
118
+ // 3. Or use Index-Based Mode for multiple characters
119
+ const multiCharScene = await service.generate({
120
+ prompt: 'The FIRST reference image character meets the SECOND reference image character',
121
+ model: 'gemini-flash-image',
122
+ referenceImages: [
123
+ { base64: character1.images[0].base64!, mimeType: 'image/png' },
124
+ { base64: character2.images[0].base64!, mimeType: 'image/png' },
125
+ ],
126
+ // subjectDescription omitted = Index-Based Mode
127
+ });
116
128
  ```
117
129
 
118
130
  **Important:** Character consistency is only supported by `gemini-flash-image` model!
@@ -225,7 +237,11 @@ IONOS_API_URL=https://api.ionos.cloud/ai/v1
225
237
 
226
238
  ## Character Consistency
227
239
 
228
- Generate consistent characters across multiple images - perfect for children's book illustrations:
240
+ Generate consistent characters across multiple images - perfect for children's book illustrations.
241
+
242
+ ### Mode 1: Structured Mode (Single Character)
243
+
244
+ Best for scenes with a single consistent character:
229
245
 
230
246
  ```typescript
231
247
  // Step 1: Create a character
@@ -242,15 +258,47 @@ for (const scene of scenes) {
242
258
  prompt: scene,
243
259
  model: 'gemini-flash-image',
244
260
  referenceImages: [{ base64: bear.images[0].base64!, mimeType: 'image/png' }],
245
- subjectDescription: 'cute cartoon bear with red hat',
261
+ subjectDescription: 'cute cartoon bear with red hat', // Required in structured mode
246
262
  });
247
263
  // Save result...
248
264
  }
249
265
  ```
250
266
 
251
- **Requirements:**
252
- - Model must be `gemini-flash-image`
253
- - `subjectDescription` is required when using `referenceImages`
267
+ ### Mode 2: Index-Based Mode (Multiple Characters)
268
+
269
+ Best for scenes with multiple distinct characters. Reference images directly in your prompt by their position:
270
+
271
+ ```typescript
272
+ // Load two different character references
273
+ const cowboy1 = await loadImage('cowboy1.png');
274
+ const cowboy2 = await loadImage('cowboy2.png');
275
+
276
+ // Reference each image by index in the prompt
277
+ const duelScene = await service.generate({
278
+ prompt: `Generate a cinematic wide shot of a western duel.
279
+ - The character on the LEFT should look exactly like the person in the FIRST reference image.
280
+ - The character on the RIGHT should look exactly like the person in the SECOND reference image.
281
+ They are standing in a dusty street at high noon.`,
282
+ model: 'gemini-flash-image',
283
+ referenceImages: [
284
+ { base64: cowboy1, mimeType: 'image/png' },
285
+ { base64: cowboy2, mimeType: 'image/png' },
286
+ ],
287
+ // subjectDescription intentionally omitted for index-based mode
288
+ aspectRatio: '16:9',
289
+ });
290
+ ```
291
+
292
+ **Reference keywords:** Use "FIRST reference image", "SECOND reference image" or "Image 1", "Image 2" etc.
293
+
294
+ ### Requirements
295
+
296
+ | Mode | `subjectDescription` | Use Case |
297
+ |------|---------------------|----------|
298
+ | **Structured** | Required | Single character across scenes |
299
+ | **Index-Based** | Omitted | Multiple characters in one scene |
300
+
301
+ - Model must be `gemini-flash-image` (only model supporting character consistency)
254
302
 
255
303
  ## GDPR / Compliance
256
304
 
@@ -405,6 +453,42 @@ Available levels: `debug`, `info`, `warn`, `error`, `silent`
405
453
 
406
454
  </details>
407
455
 
456
+ <details>
457
+ <summary><strong>Debug Logging (Markdown Files)</strong></summary>
458
+
459
+ Log all TTI requests and responses to markdown files for debugging:
460
+
461
+ ```typescript
462
+ import { TTIDebugger } from '@loonylabs/tti-middleware';
463
+
464
+ // Enable via environment variable
465
+ // DEBUG_TTI_REQUESTS=true
466
+
467
+ // Or programmatically
468
+ TTIDebugger.setEnabled(true);
469
+ TTIDebugger.setLogsDir('./logs/tti/requests');
470
+
471
+ // Configure all options at once
472
+ TTIDebugger.configure({
473
+ enabled: true,
474
+ logsDir: './logs/tti/requests',
475
+ consoleLog: true, // Also log to console
476
+ includeBase64: false, // Exclude base64 data (default)
477
+ });
478
+ ```
479
+
480
+ **Log file contents:**
481
+ - Provider, model, and region
482
+ - Full prompt text
483
+ - Subject description (for character consistency)
484
+ - Reference image metadata
485
+ - Response data (duration, image count)
486
+ - Errors with full details
487
+
488
+ **Use case:** Debug why character consistency isn't working by inspecting exactly what prompt and `subjectDescription` are being sent to the API.
489
+
490
+ </details>
491
+
408
492
  <details>
409
493
  <summary><strong>Error Handling</strong></summary>
410
494
 
@@ -1,2 +1,3 @@
1
1
  export * from './tti.service';
2
2
  export * from './providers';
3
+ export * from './utils';
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./tti.service"), exports);
18
18
  __exportStar(require("./providers"), exports);
19
+ __exportStar(require("./utils"), exports);
@@ -133,9 +133,14 @@ class BaseTTIProvider {
133
133
  throw new CapabilityNotSupportedError(this.providerName, 'characterConsistency', modelId);
134
134
  }
135
135
  // Validate subject description is provided
136
- if (!request.subjectDescription || request.subjectDescription.trim().length === 0) {
137
- throw new InvalidConfigError(this.providerName, 'subjectDescription is required when using referenceImages');
138
- }
136
+ // RELAXED: We now allow missing subjectDescription to support raw multimodal prompting
137
+ // where the user references images directly in the prompt (e.g., "Image 1 is X...").
138
+ // if (!request.subjectDescription || request.subjectDescription.trim().length === 0) {
139
+ // throw new InvalidConfigError(
140
+ // this.providerName,
141
+ // 'subjectDescription is required when using referenceImages'
142
+ // );
143
+ // }
139
144
  // Validate reference images have data
140
145
  for (let i = 0; i < request.referenceImages.length; i++) {
141
146
  const ref = request.referenceImages[i];
@@ -49,6 +49,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.GoogleCloudTTIProvider = void 0;
50
50
  const types_1 = require("../../../types");
51
51
  const base_tti_provider_1 = require("./base-tti-provider");
52
+ const debug_tti_utils_1 = require("../utils/debug-tti.utils");
52
53
  // ============================================================
53
54
  // MODEL DEFINITIONS
54
55
  // ============================================================
@@ -157,19 +158,44 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
157
158
  }
158
159
  // Validate region availability
159
160
  const effectiveRegion = this.getEffectiveRegion(modelId);
161
+ // Create debug info for logging
162
+ let debugInfo = null;
163
+ if (debug_tti_utils_1.TTIDebugger.isEnabled) {
164
+ debugInfo = debug_tti_utils_1.TTIDebugger.createDebugInfo(request, this.providerName, modelId, { region: effectiveRegion });
165
+ await debug_tti_utils_1.TTIDebugger.logRequest(debugInfo);
166
+ }
160
167
  this.log('debug', 'Generating image', {
161
168
  model: modelId,
162
169
  region: effectiveRegion,
163
170
  hasReferenceImages: (0, base_tti_provider_1.hasReferenceImages)(request),
164
171
  });
165
- // Route to appropriate implementation with retry support
166
- switch (modelId) {
167
- case 'imagen-3':
168
- return this.executeWithRetry(request, () => this.generateWithImagen(request, effectiveRegion), 'Imagen API call');
169
- case 'gemini-flash-image':
170
- return this.executeWithRetry(request, () => this.generateWithGemini(request, effectiveRegion), 'Gemini API call');
171
- default:
172
- throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Unknown model: ${modelId}`);
172
+ try {
173
+ // Route to appropriate implementation with retry support
174
+ let response;
175
+ switch (modelId) {
176
+ case 'imagen-3':
177
+ response = await this.executeWithRetry(request, () => this.generateWithImagen(request, effectiveRegion), 'Imagen API call');
178
+ break;
179
+ case 'gemini-flash-image':
180
+ response = await this.executeWithRetry(request, () => this.generateWithGemini(request, effectiveRegion), 'Gemini API call');
181
+ break;
182
+ default:
183
+ throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Unknown model: ${modelId}`);
184
+ }
185
+ // Log successful response
186
+ if (debugInfo) {
187
+ debugInfo = debug_tti_utils_1.TTIDebugger.updateWithResponse(debugInfo, response);
188
+ await debug_tti_utils_1.TTIDebugger.logResponse(debugInfo);
189
+ }
190
+ return response;
191
+ }
192
+ catch (error) {
193
+ // Log error
194
+ if (debugInfo) {
195
+ debugInfo = debug_tti_utils_1.TTIDebugger.updateWithError(debugInfo, error);
196
+ await debug_tti_utils_1.TTIDebugger.logError(debugInfo);
197
+ }
198
+ throw error;
173
199
  }
174
200
  }
175
201
  // ============================================================
@@ -347,9 +373,16 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
347
373
  },
348
374
  });
349
375
  }
350
- // Build character consistency prompt
351
- const fullPrompt = this.buildCharacterConsistencyPrompt(request.prompt, request.subjectDescription, request.referenceImages.length);
352
- parts.push({ text: fullPrompt });
376
+ // Build character consistency prompt if subject description is provided
377
+ if (request.subjectDescription) {
378
+ const fullPrompt = this.buildCharacterConsistencyPrompt(request.prompt, request.subjectDescription, request.referenceImages.length);
379
+ parts.push({ text: fullPrompt });
380
+ }
381
+ else {
382
+ // No subject description - treat as raw multimodal prompt
383
+ // This allows "image 1", "image 2" style prompting
384
+ parts.push({ text: request.prompt });
385
+ }
353
386
  }
354
387
  else {
355
388
  parts.push({ text: request.prompt });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * TTI Debugger Utility
3
+ *
4
+ * Provides markdown-based logging for TTI requests, similar to LLM middleware.
5
+ * Logs prompts, subject descriptions, reference image metadata, and responses.
6
+ *
7
+ * Enable via:
8
+ * - Environment variable: DEBUG_TTI_REQUESTS=true
9
+ * - Or programmatically: TTIDebugger.setEnabled(true)
10
+ *
11
+ * Configure log directory:
12
+ * - Environment variable: TTI_DEBUG_LOG_DIR=/path/to/logs
13
+ * - Or programmatically: TTIDebugger.setLogsDir('/path/to/logs')
14
+ * - Default: process.cwd()/logs/tti/requests/
15
+ */
16
+ import { TTIRequest, TTIResponse } from '../../../types';
17
+ /**
18
+ * Debug information for a TTI request
19
+ */
20
+ export interface TTIDebugInfo {
21
+ requestTimestamp: Date;
22
+ responseTimestamp?: Date;
23
+ provider: string;
24
+ model: string;
25
+ region?: string;
26
+ prompt: string;
27
+ subjectDescription?: string;
28
+ referenceImageCount: number;
29
+ referenceImageMimeTypes?: string[];
30
+ aspectRatio?: string;
31
+ providerOptions?: Record<string, unknown>;
32
+ useCase?: string;
33
+ sessionId?: string;
34
+ bookId?: string;
35
+ characterId?: string;
36
+ sectionId?: string;
37
+ response?: {
38
+ imageCount: number;
39
+ imageMimeTypes: string[];
40
+ duration: number;
41
+ };
42
+ rawRequest?: TTIRequest;
43
+ rawResponse?: TTIResponse;
44
+ error?: {
45
+ message: string;
46
+ code?: string;
47
+ details?: unknown;
48
+ };
49
+ }
50
+ /**
51
+ * Configuration options for TTIDebugger
52
+ */
53
+ export interface TTIDebuggerConfig {
54
+ /** Enable/disable logging (default: from DEBUG_TTI_REQUESTS env) */
55
+ enabled?: boolean;
56
+ /** Directory for log files (default: process.cwd()/logs/tti/requests) */
57
+ logsDir?: string;
58
+ /** Include raw base64 image data in logs (default: false - too large) */
59
+ includeBase64?: boolean;
60
+ /** Log to console as well (default: false) */
61
+ consoleLog?: boolean;
62
+ }
63
+ /**
64
+ * Static debugger class for TTI request/response logging
65
+ */
66
+ export declare class TTIDebugger {
67
+ private static _enabled;
68
+ private static _logsDir;
69
+ private static _includeBase64;
70
+ private static _consoleLog;
71
+ /**
72
+ * Check if debugging is enabled
73
+ */
74
+ static get isEnabled(): boolean;
75
+ /**
76
+ * Enable or disable debugging
77
+ */
78
+ static setEnabled(enabled: boolean): void;
79
+ /**
80
+ * Get the current logs directory
81
+ */
82
+ static getLogsDir(): string;
83
+ /**
84
+ * Set the logs directory
85
+ */
86
+ static setLogsDir(dir: string): void;
87
+ /**
88
+ * Configure the debugger
89
+ */
90
+ static configure(config: TTIDebuggerConfig): void;
91
+ /**
92
+ * Log a TTI request (call before generation)
93
+ */
94
+ static logRequest(debugInfo: TTIDebugInfo): Promise<void>;
95
+ /**
96
+ * Log a TTI response (call after generation)
97
+ */
98
+ static logResponse(debugInfo: TTIDebugInfo): Promise<void>;
99
+ /**
100
+ * Log an error
101
+ */
102
+ static logError(debugInfo: TTIDebugInfo): Promise<void>;
103
+ /**
104
+ * Save debug info to a markdown file
105
+ */
106
+ static saveToMarkdown(debugInfo: TTIDebugInfo): Promise<string>;
107
+ /**
108
+ * Generate a filename for the log file
109
+ */
110
+ private static generateFilename;
111
+ /**
112
+ * Format debug info as markdown
113
+ */
114
+ private static formatMarkdown;
115
+ /**
116
+ * Sanitize request by removing/truncating base64 data
117
+ */
118
+ private static sanitizeRequest;
119
+ /**
120
+ * Sanitize response by removing/truncating base64 data
121
+ */
122
+ private static sanitizeResponse;
123
+ /**
124
+ * Ensure the logs directory exists
125
+ */
126
+ private static ensureLogsDirectory;
127
+ /**
128
+ * Create debug info from a request (helper for providers)
129
+ */
130
+ static createDebugInfo(request: TTIRequest, provider: string, model: string, options?: {
131
+ region?: string;
132
+ useCase?: string;
133
+ sessionId?: string;
134
+ bookId?: string;
135
+ characterId?: string;
136
+ sectionId?: string;
137
+ }): TTIDebugInfo;
138
+ /**
139
+ * Update debug info with response data (helper for providers)
140
+ */
141
+ static updateWithResponse(debugInfo: TTIDebugInfo, response: TTIResponse): TTIDebugInfo;
142
+ /**
143
+ * Update debug info with error data (helper for providers)
144
+ */
145
+ static updateWithError(debugInfo: TTIDebugInfo, error: Error & {
146
+ code?: string;
147
+ cause?: unknown;
148
+ }): TTIDebugInfo;
149
+ }
150
+ export default TTIDebugger;
@@ -0,0 +1,418 @@
1
+ "use strict";
2
+ /**
3
+ * TTI Debugger Utility
4
+ *
5
+ * Provides markdown-based logging for TTI requests, similar to LLM middleware.
6
+ * Logs prompts, subject descriptions, reference image metadata, and responses.
7
+ *
8
+ * Enable via:
9
+ * - Environment variable: DEBUG_TTI_REQUESTS=true
10
+ * - Or programmatically: TTIDebugger.setEnabled(true)
11
+ *
12
+ * Configure log directory:
13
+ * - Environment variable: TTI_DEBUG_LOG_DIR=/path/to/logs
14
+ * - Or programmatically: TTIDebugger.setLogsDir('/path/to/logs')
15
+ * - Default: process.cwd()/logs/tti/requests/
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.TTIDebugger = void 0;
52
+ const fs = __importStar(require("fs"));
53
+ const path = __importStar(require("path"));
54
+ // ============================================================
55
+ // DEBUGGER CLASS
56
+ // ============================================================
57
+ /**
58
+ * Static debugger class for TTI request/response logging
59
+ */
60
+ class TTIDebugger {
61
+ // ============================================================
62
+ // CONFIGURATION
63
+ // ============================================================
64
+ /**
65
+ * Check if debugging is enabled
66
+ */
67
+ static get isEnabled() {
68
+ return this._enabled;
69
+ }
70
+ /**
71
+ * Enable or disable debugging
72
+ */
73
+ static setEnabled(enabled) {
74
+ this._enabled = enabled;
75
+ }
76
+ /**
77
+ * Get the current logs directory
78
+ */
79
+ static getLogsDir() {
80
+ return this._logsDir;
81
+ }
82
+ /**
83
+ * Set the logs directory
84
+ */
85
+ static setLogsDir(dir) {
86
+ this._logsDir = dir;
87
+ }
88
+ /**
89
+ * Configure the debugger
90
+ */
91
+ static configure(config) {
92
+ if (config.enabled !== undefined) {
93
+ this._enabled = config.enabled;
94
+ }
95
+ if (config.logsDir !== undefined) {
96
+ this._logsDir = config.logsDir;
97
+ }
98
+ if (config.includeBase64 !== undefined) {
99
+ this._includeBase64 = config.includeBase64;
100
+ }
101
+ if (config.consoleLog !== undefined) {
102
+ this._consoleLog = config.consoleLog;
103
+ }
104
+ }
105
+ // ============================================================
106
+ // LOGGING METHODS
107
+ // ============================================================
108
+ /**
109
+ * Log a TTI request (call before generation)
110
+ */
111
+ static async logRequest(debugInfo) {
112
+ if (!this._enabled)
113
+ return;
114
+ if (this._consoleLog) {
115
+ console.log('[TTI Debug] Request:', {
116
+ provider: debugInfo.provider,
117
+ model: debugInfo.model,
118
+ useCase: debugInfo.useCase,
119
+ prompt: debugInfo.prompt.substring(0, 100) + '...',
120
+ subjectDescription: debugInfo.subjectDescription,
121
+ referenceImageCount: debugInfo.referenceImageCount,
122
+ });
123
+ }
124
+ }
125
+ /**
126
+ * Log a TTI response (call after generation)
127
+ */
128
+ static async logResponse(debugInfo) {
129
+ if (!this._enabled)
130
+ return;
131
+ try {
132
+ await this.saveToMarkdown(debugInfo);
133
+ if (this._consoleLog) {
134
+ console.log('[TTI Debug] Response saved:', {
135
+ provider: debugInfo.provider,
136
+ model: debugInfo.model,
137
+ duration: debugInfo.response?.duration,
138
+ imageCount: debugInfo.response?.imageCount,
139
+ });
140
+ }
141
+ }
142
+ catch (error) {
143
+ console.error('[TTI Debug] Failed to save log:', error);
144
+ }
145
+ }
146
+ /**
147
+ * Log an error
148
+ */
149
+ static async logError(debugInfo) {
150
+ if (!this._enabled)
151
+ return;
152
+ try {
153
+ await this.saveToMarkdown(debugInfo);
154
+ if (this._consoleLog) {
155
+ console.error('[TTI Debug] Error:', debugInfo.error);
156
+ }
157
+ }
158
+ catch (error) {
159
+ console.error('[TTI Debug] Failed to save error log:', error);
160
+ }
161
+ }
162
+ // ============================================================
163
+ // MARKDOWN GENERATION
164
+ // ============================================================
165
+ /**
166
+ * Save debug info to a markdown file
167
+ */
168
+ static async saveToMarkdown(debugInfo) {
169
+ this.ensureLogsDirectory();
170
+ const filename = this.generateFilename(debugInfo);
171
+ const filepath = path.join(this._logsDir, filename);
172
+ const content = this.formatMarkdown(debugInfo);
173
+ await fs.promises.writeFile(filepath, content, 'utf-8');
174
+ return filepath;
175
+ }
176
+ /**
177
+ * Generate a filename for the log file
178
+ */
179
+ static generateFilename(debugInfo) {
180
+ const timestamp = debugInfo.requestTimestamp
181
+ .toISOString()
182
+ .replace(/[:.]/g, '-');
183
+ const useCasePart = debugInfo.useCase
184
+ ? `_${debugInfo.useCase.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
185
+ : '';
186
+ const identifierPart = debugInfo.characterId
187
+ ? `_char-${debugInfo.characterId.substring(0, 8)}`
188
+ : debugInfo.sectionId
189
+ ? `_sec-${debugInfo.sectionId.substring(0, 8)}`
190
+ : '';
191
+ return `${timestamp}${useCasePart}${identifierPart}.md`;
192
+ }
193
+ /**
194
+ * Format debug info as markdown
195
+ */
196
+ static formatMarkdown(debugInfo) {
197
+ const sections = [];
198
+ // Header
199
+ sections.push('# TTI Request & Response Log\n');
200
+ // Provider Information
201
+ sections.push('## Provider Information\n');
202
+ sections.push(`- **Provider**: ${debugInfo.provider}`);
203
+ sections.push(`- **Model**: ${debugInfo.model}`);
204
+ if (debugInfo.region) {
205
+ sections.push(`- **Region**: ${debugInfo.region}`);
206
+ }
207
+ sections.push('');
208
+ // Request Information
209
+ sections.push('## Request Information\n');
210
+ sections.push(`- **Request Timestamp**: ${debugInfo.requestTimestamp.toISOString()}`);
211
+ if (debugInfo.responseTimestamp) {
212
+ sections.push(`- **Response Timestamp**: ${debugInfo.responseTimestamp.toISOString()}`);
213
+ }
214
+ if (debugInfo.useCase) {
215
+ sections.push(`- **Use Case**: ${debugInfo.useCase}`);
216
+ }
217
+ if (debugInfo.sessionId) {
218
+ sections.push(`- **Session ID**: ${debugInfo.sessionId}`);
219
+ }
220
+ if (debugInfo.bookId) {
221
+ sections.push(`- **Book ID**: ${debugInfo.bookId}`);
222
+ }
223
+ if (debugInfo.characterId) {
224
+ sections.push(`- **Character ID**: ${debugInfo.characterId}`);
225
+ }
226
+ if (debugInfo.sectionId) {
227
+ sections.push(`- **Section ID**: ${debugInfo.sectionId}`);
228
+ }
229
+ if (debugInfo.aspectRatio) {
230
+ sections.push(`- **Aspect Ratio**: ${debugInfo.aspectRatio}`);
231
+ }
232
+ sections.push('');
233
+ // Reference Images
234
+ sections.push('## Reference Images\n');
235
+ sections.push(`- **Count**: ${debugInfo.referenceImageCount}`);
236
+ if (debugInfo.referenceImageMimeTypes &&
237
+ debugInfo.referenceImageMimeTypes.length > 0) {
238
+ sections.push(`- **MIME Types**: ${debugInfo.referenceImageMimeTypes.join(', ')}`);
239
+ }
240
+ sections.push('');
241
+ // Subject Description (important for character consistency debugging)
242
+ sections.push('## Subject Description\n');
243
+ if (debugInfo.subjectDescription) {
244
+ sections.push('```');
245
+ sections.push(debugInfo.subjectDescription);
246
+ sections.push('```');
247
+ }
248
+ else {
249
+ sections.push('*No subject description provided (raw multimodal mode)*');
250
+ }
251
+ sections.push('');
252
+ // Prompt (the most important part for debugging)
253
+ sections.push('## Prompt\n');
254
+ sections.push('```');
255
+ sections.push(debugInfo.prompt);
256
+ sections.push('```');
257
+ sections.push('');
258
+ // Provider Options
259
+ if (debugInfo.providerOptions &&
260
+ Object.keys(debugInfo.providerOptions).length > 0) {
261
+ sections.push('## Provider Options\n');
262
+ sections.push('```json');
263
+ sections.push(JSON.stringify(debugInfo.providerOptions, null, 2));
264
+ sections.push('```');
265
+ sections.push('');
266
+ }
267
+ // Response
268
+ if (debugInfo.response) {
269
+ sections.push('## Response\n');
270
+ sections.push(`- **Image Count**: ${debugInfo.response.imageCount}`);
271
+ sections.push(`- **MIME Types**: ${debugInfo.response.imageMimeTypes.join(', ')}`);
272
+ sections.push(`- **Duration**: ${debugInfo.response.duration}ms`);
273
+ sections.push('');
274
+ }
275
+ // Raw Request Data (without base64)
276
+ if (debugInfo.rawRequest) {
277
+ sections.push('## Raw Request Data\n');
278
+ sections.push('```json');
279
+ const sanitizedRequest = this.sanitizeRequest(debugInfo.rawRequest);
280
+ sections.push(JSON.stringify(sanitizedRequest, null, 2));
281
+ sections.push('```');
282
+ sections.push('');
283
+ }
284
+ // Raw Response Data (without base64)
285
+ if (debugInfo.rawResponse) {
286
+ sections.push('## Raw Response Data\n');
287
+ sections.push('```json');
288
+ const sanitizedResponse = this.sanitizeResponse(debugInfo.rawResponse);
289
+ sections.push(JSON.stringify(sanitizedResponse, null, 2));
290
+ sections.push('```');
291
+ sections.push('');
292
+ }
293
+ // Error
294
+ if (debugInfo.error) {
295
+ sections.push('## Error\n');
296
+ sections.push(`- **Message**: ${debugInfo.error.message}`);
297
+ if (debugInfo.error.code) {
298
+ sections.push(`- **Code**: ${debugInfo.error.code}`);
299
+ }
300
+ if (debugInfo.error.details) {
301
+ sections.push('- **Details**:');
302
+ sections.push('```json');
303
+ sections.push(JSON.stringify(debugInfo.error.details, null, 2));
304
+ sections.push('```');
305
+ }
306
+ sections.push('');
307
+ }
308
+ // Footer
309
+ sections.push('---');
310
+ sections.push(`*Generated on ${new Date().toISOString()}*`);
311
+ return sections.join('\n');
312
+ }
313
+ /**
314
+ * Sanitize request by removing/truncating base64 data
315
+ */
316
+ static sanitizeRequest(request) {
317
+ const sanitized = { ...request };
318
+ if (request.referenceImages && !this._includeBase64) {
319
+ sanitized.referenceImages = request.referenceImages.map((ref, index) => ({
320
+ index,
321
+ mimeType: ref.mimeType || 'unknown',
322
+ base64Length: ref.base64?.length || 0,
323
+ base64Preview: ref.base64 ? `${ref.base64.substring(0, 50)}...` : null,
324
+ }));
325
+ }
326
+ return sanitized;
327
+ }
328
+ /**
329
+ * Sanitize response by removing/truncating base64 data
330
+ */
331
+ static sanitizeResponse(response) {
332
+ const sanitized = { ...response };
333
+ if (response.images && !this._includeBase64) {
334
+ sanitized.images = response.images.map((img, index) => ({
335
+ index,
336
+ contentType: img.contentType || 'unknown',
337
+ hasUrl: !!img.url,
338
+ base64Length: img.base64?.length || 0,
339
+ base64Preview: img.base64 ? `${img.base64.substring(0, 50)}...` : null,
340
+ }));
341
+ }
342
+ return sanitized;
343
+ }
344
+ // ============================================================
345
+ // HELPER METHODS
346
+ // ============================================================
347
+ /**
348
+ * Ensure the logs directory exists
349
+ */
350
+ static ensureLogsDirectory() {
351
+ if (!fs.existsSync(this._logsDir)) {
352
+ fs.mkdirSync(this._logsDir, { recursive: true });
353
+ }
354
+ }
355
+ /**
356
+ * Create debug info from a request (helper for providers)
357
+ */
358
+ static createDebugInfo(request, provider, model, options) {
359
+ return {
360
+ requestTimestamp: new Date(),
361
+ provider,
362
+ model,
363
+ region: options?.region,
364
+ prompt: request.prompt,
365
+ subjectDescription: request.subjectDescription,
366
+ referenceImageCount: request.referenceImages?.length || 0,
367
+ referenceImageMimeTypes: request.referenceImages?.map((ref) => ref.mimeType || 'unknown'),
368
+ aspectRatio: request.aspectRatio,
369
+ providerOptions: request.providerOptions,
370
+ useCase: options?.useCase,
371
+ sessionId: options?.sessionId,
372
+ bookId: options?.bookId,
373
+ characterId: options?.characterId,
374
+ sectionId: options?.sectionId,
375
+ rawRequest: request,
376
+ };
377
+ }
378
+ /**
379
+ * Update debug info with response data (helper for providers)
380
+ */
381
+ static updateWithResponse(debugInfo, response) {
382
+ return {
383
+ ...debugInfo,
384
+ responseTimestamp: new Date(),
385
+ response: {
386
+ imageCount: response.images.length,
387
+ imageMimeTypes: response.images.map((img) => img.contentType || 'unknown'),
388
+ duration: response.metadata.duration,
389
+ },
390
+ rawResponse: response,
391
+ };
392
+ }
393
+ /**
394
+ * Update debug info with error data (helper for providers)
395
+ */
396
+ static updateWithError(debugInfo, error) {
397
+ return {
398
+ ...debugInfo,
399
+ responseTimestamp: new Date(),
400
+ error: {
401
+ message: error.message,
402
+ code: error.code,
403
+ details: error.cause,
404
+ },
405
+ };
406
+ }
407
+ }
408
+ exports.TTIDebugger = TTIDebugger;
409
+ TTIDebugger._enabled = process.env.DEBUG_TTI_REQUESTS === 'true' ||
410
+ process.env.NODE_ENV === 'development';
411
+ TTIDebugger._logsDir = process.env.TTI_DEBUG_LOG_DIR ||
412
+ path.join(process.cwd(), 'logs', 'tti', 'requests');
413
+ TTIDebugger._includeBase64 = false;
414
+ TTIDebugger._consoleLog = false;
415
+ // ============================================================
416
+ // EXPORTS
417
+ // ============================================================
418
+ exports.default = TTIDebugger;
@@ -0,0 +1 @@
1
+ export * from './debug-tti.utils';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./debug-tti.utils"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loonylabs/tti-middleware",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Provider-agnostic Text-to-Image middleware with GDPR compliance. Supports Google Cloud (Imagen, Gemini), Eden AI, and IONOS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",