@loonylabs/tti-middleware 1.8.0 → 1.10.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 +1 -0
- package/dist/middleware/services/tti/providers/base-tti-provider.d.ts +14 -4
- package/dist/middleware/services/tti/providers/base-tti-provider.js +81 -15
- package/dist/middleware/services/tti/providers/google-cloud-provider.d.ts +3 -0
- package/dist/middleware/services/tti/providers/google-cloud-provider.js +113 -2
- package/dist/middleware/types/index.d.ts +41 -0
- package/dist/middleware/types/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
- **Eden AI**: Aggregator with access to OpenAI, Stability AI, Replicate (experimental)
|
|
45
45
|
- **IONOS**: German cloud provider with OpenAI-compatible API (experimental)
|
|
46
46
|
- **Character Consistency**: Generate consistent characters across multiple images (perfect for children's book illustrations)
|
|
47
|
+
- **Inpainting**: Fix specific areas of a generated image without regenerating the entire scene — via Vertex AI `imagen-capability` model
|
|
47
48
|
- **GDPR/DSGVO Compliance**: Built-in EU region support with automatic fallback
|
|
48
49
|
- **Region Rotation**: Opt-in region rotation on quota errors (429) for Google Cloud — rotate through regions instead of retrying the same exhausted region
|
|
49
50
|
- **Retry Logic**: Exponential backoff with jitter for transient errors (429, 408, 5xx, timeouts)
|
|
@@ -102,10 +102,20 @@ export declare abstract class BaseTTIProvider implements ITTIProvider {
|
|
|
102
102
|
*/
|
|
103
103
|
protected sleep(ms: number): Promise<void>;
|
|
104
104
|
/**
|
|
105
|
-
* Wrap an operation with a timeout
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
105
|
+
* Wrap an operation with a timeout and optional grace period.
|
|
106
|
+
*
|
|
107
|
+
* Normal flow (graceMs = 0):
|
|
108
|
+
* If the operation doesn't resolve within `timeoutMs`, the returned promise
|
|
109
|
+
* rejects immediately with a timeout error.
|
|
110
|
+
*
|
|
111
|
+
* Grace period flow (graceMs > 0):
|
|
112
|
+
* When `timeoutMs` fires, instead of rejecting immediately, a grace period
|
|
113
|
+
* starts. If the operation resolves successfully within `graceMs`, the result
|
|
114
|
+
* is used and no timeout error is thrown. Only if the grace period also expires
|
|
115
|
+
* does the promise reject — with an error reflecting the total wait time.
|
|
116
|
+
*
|
|
117
|
+
* This prevents paying for (and discarding) a Vertex AI response that arrived
|
|
118
|
+
* slightly after the timeout threshold.
|
|
109
119
|
*/
|
|
110
120
|
private withTimeout;
|
|
111
121
|
/**
|
|
@@ -196,6 +196,27 @@ class BaseTTIProvider {
|
|
|
196
196
|
if (!request.prompt || request.prompt.trim().length === 0) {
|
|
197
197
|
throw new InvalidConfigError(this.providerName, 'Prompt cannot be empty');
|
|
198
198
|
}
|
|
199
|
+
// If baseImage is provided, validate inpainting requirements
|
|
200
|
+
if (request.baseImage) {
|
|
201
|
+
if (!request.baseImage.base64 || request.baseImage.base64.trim().length === 0) {
|
|
202
|
+
throw new InvalidConfigError(this.providerName, 'baseImage has empty base64 data');
|
|
203
|
+
}
|
|
204
|
+
const modelId = request.model || this.getDefaultModel();
|
|
205
|
+
if (!this.modelSupportsCapability(modelId, 'imageEditing')) {
|
|
206
|
+
throw new CapabilityNotSupportedError(this.providerName, 'imageEditing', modelId);
|
|
207
|
+
}
|
|
208
|
+
if (!request.maskImage) {
|
|
209
|
+
throw new InvalidConfigError(this.providerName, 'maskImage is required when baseImage is set');
|
|
210
|
+
}
|
|
211
|
+
if (!request.maskImage.base64 || request.maskImage.base64.trim().length === 0) {
|
|
212
|
+
throw new InvalidConfigError(this.providerName, 'maskImage has empty base64 data');
|
|
213
|
+
}
|
|
214
|
+
if (request.maskDilation !== undefined) {
|
|
215
|
+
if (request.maskDilation < 0 || request.maskDilation > 1) {
|
|
216
|
+
throw new InvalidConfigError(this.providerName, 'maskDilation must be between 0.0 and 1.0');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
199
220
|
// If reference images are provided, validate them
|
|
200
221
|
if (request.referenceImages && request.referenceImages.length > 0) {
|
|
201
222
|
const modelId = request.model || this.getDefaultModel();
|
|
@@ -249,6 +270,7 @@ class BaseTTIProvider {
|
|
|
249
270
|
jitter: retryOption.jitter ?? types_1.DEFAULT_RETRY_OPTIONS.jitter,
|
|
250
271
|
timeoutMs: retryOption.timeoutMs ?? types_1.DEFAULT_RETRY_OPTIONS.timeoutMs,
|
|
251
272
|
timeoutRetries: retryOption.timeoutRetries ?? types_1.DEFAULT_RETRY_OPTIONS.timeoutRetries,
|
|
273
|
+
graceMs: retryOption.graceMs ?? types_1.DEFAULT_RETRY_OPTIONS.graceMs,
|
|
252
274
|
};
|
|
253
275
|
}
|
|
254
276
|
/**
|
|
@@ -274,25 +296,68 @@ class BaseTTIProvider {
|
|
|
274
296
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
275
297
|
}
|
|
276
298
|
/**
|
|
277
|
-
* Wrap an operation with a timeout
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
299
|
+
* Wrap an operation with a timeout and optional grace period.
|
|
300
|
+
*
|
|
301
|
+
* Normal flow (graceMs = 0):
|
|
302
|
+
* If the operation doesn't resolve within `timeoutMs`, the returned promise
|
|
303
|
+
* rejects immediately with a timeout error.
|
|
304
|
+
*
|
|
305
|
+
* Grace period flow (graceMs > 0):
|
|
306
|
+
* When `timeoutMs` fires, instead of rejecting immediately, a grace period
|
|
307
|
+
* starts. If the operation resolves successfully within `graceMs`, the result
|
|
308
|
+
* is used and no timeout error is thrown. Only if the grace period also expires
|
|
309
|
+
* does the promise reject — with an error reflecting the total wait time.
|
|
310
|
+
*
|
|
311
|
+
* This prevents paying for (and discarding) a Vertex AI response that arrived
|
|
312
|
+
* slightly after the timeout threshold.
|
|
281
313
|
*/
|
|
282
|
-
withTimeout(operation, timeoutMs, operationName) {
|
|
314
|
+
withTimeout(operation, timeoutMs, graceMs, operationName) {
|
|
283
315
|
return new Promise((resolve, reject) => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
316
|
+
let settled = false;
|
|
317
|
+
let inGracePeriod = false;
|
|
318
|
+
let mainTimerRef;
|
|
319
|
+
let graceTimerRef = null;
|
|
320
|
+
const operationPromise = operation();
|
|
321
|
+
// Handle operation resolution — can fire at any point, including during grace
|
|
322
|
+
operationPromise.then((result) => {
|
|
323
|
+
if (settled)
|
|
324
|
+
return;
|
|
325
|
+
settled = true;
|
|
326
|
+
clearTimeout(mainTimerRef);
|
|
327
|
+
if (graceTimerRef)
|
|
328
|
+
clearTimeout(graceTimerRef);
|
|
329
|
+
if (inGracePeriod) {
|
|
330
|
+
this.log('info', `${operationName} completed during grace period`, { graceMs });
|
|
331
|
+
}
|
|
290
332
|
resolve(result);
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
|
|
333
|
+
}, (error) => {
|
|
334
|
+
if (settled)
|
|
335
|
+
return;
|
|
336
|
+
settled = true;
|
|
337
|
+
clearTimeout(mainTimerRef);
|
|
338
|
+
if (graceTimerRef)
|
|
339
|
+
clearTimeout(graceTimerRef);
|
|
294
340
|
reject(error);
|
|
295
341
|
});
|
|
342
|
+
// Primary timeout
|
|
343
|
+
mainTimerRef = setTimeout(() => {
|
|
344
|
+
if (settled)
|
|
345
|
+
return;
|
|
346
|
+
if (graceMs > 0) {
|
|
347
|
+
inGracePeriod = true;
|
|
348
|
+
this.log('warn', `${operationName} primary timeout after ${timeoutMs}ms, entering grace period (${graceMs}ms)`, { timeoutMs, graceMs });
|
|
349
|
+
graceTimerRef = setTimeout(() => {
|
|
350
|
+
if (settled)
|
|
351
|
+
return;
|
|
352
|
+
settled = true;
|
|
353
|
+
reject(new Error(`timeout: ${operationName} did not complete within ${timeoutMs + graceMs}ms (including ${graceMs}ms grace period)`));
|
|
354
|
+
}, graceMs);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
settled = true;
|
|
358
|
+
reject(new Error(`timeout: ${operationName} did not complete within ${timeoutMs}ms`));
|
|
359
|
+
}
|
|
360
|
+
}, timeoutMs);
|
|
296
361
|
});
|
|
297
362
|
}
|
|
298
363
|
/**
|
|
@@ -336,6 +401,7 @@ class BaseTTIProvider {
|
|
|
336
401
|
return operation();
|
|
337
402
|
}
|
|
338
403
|
const timeoutMs = retryConfig.timeoutMs || 0;
|
|
404
|
+
const graceMs = retryConfig.graceMs ?? 0;
|
|
339
405
|
const maxTimeoutRetries = retryConfig.timeoutRetries ?? 2;
|
|
340
406
|
let lastError = null;
|
|
341
407
|
let generalRetryCount = 0;
|
|
@@ -354,7 +420,7 @@ class BaseTTIProvider {
|
|
|
354
420
|
});
|
|
355
421
|
// Wrap with timeout if configured
|
|
356
422
|
const result = timeoutMs > 0
|
|
357
|
-
? await this.withTimeout(operation, timeoutMs, operationName)
|
|
423
|
+
? await this.withTimeout(operation, timeoutMs, graceMs, operationName)
|
|
358
424
|
: await operation();
|
|
359
425
|
const duration = Date.now() - attemptStart;
|
|
360
426
|
this.log('info', `${operationName} completed in ${duration}ms`, {
|
|
@@ -63,6 +63,9 @@ export declare class GoogleCloudTTIProvider extends BaseTTIProvider {
|
|
|
63
63
|
private generateWithImagen;
|
|
64
64
|
private getAiplatformClient;
|
|
65
65
|
private processImagenResponse;
|
|
66
|
+
/** Maps our editMode values to the Vertex AI API constants */
|
|
67
|
+
private static readonly EDIT_MODE_MAP;
|
|
68
|
+
private editWithImagen;
|
|
66
69
|
private generateWithGemini;
|
|
67
70
|
private getGenaiClient;
|
|
68
71
|
private buildCharacterConsistencyPrompt;
|
|
@@ -131,6 +131,27 @@ const GOOGLE_CLOUD_MODELS = [
|
|
|
131
131
|
availableRegions: IMAGEN_4_REGIONS,
|
|
132
132
|
pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
|
|
133
133
|
},
|
|
134
|
+
// ── Imagen Capability model (Vertex AI editing / inpainting) ──
|
|
135
|
+
{
|
|
136
|
+
id: 'imagen-capability',
|
|
137
|
+
displayName: 'Imagen 3 Capability (Editing)',
|
|
138
|
+
capabilities: {
|
|
139
|
+
textToImage: false,
|
|
140
|
+
characterConsistency: false,
|
|
141
|
+
imageEditing: true,
|
|
142
|
+
maxImagesPerRequest: 4,
|
|
143
|
+
},
|
|
144
|
+
availableRegions: [
|
|
145
|
+
'europe-west1',
|
|
146
|
+
'europe-west2',
|
|
147
|
+
'europe-west3',
|
|
148
|
+
'europe-west4',
|
|
149
|
+
'europe-west9',
|
|
150
|
+
'us-central1',
|
|
151
|
+
'us-east4',
|
|
152
|
+
],
|
|
153
|
+
pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
|
|
154
|
+
},
|
|
134
155
|
// ── Gemini models (Vertex AI generateContent API) ──────────
|
|
135
156
|
{
|
|
136
157
|
id: 'gemini-flash-image',
|
|
@@ -185,6 +206,7 @@ const MODEL_ID_MAP = {
|
|
|
185
206
|
'imagen-4': 'imagen-4.0-generate-001',
|
|
186
207
|
'imagen-4-fast': 'imagen-4.0-fast-generate-001',
|
|
187
208
|
'imagen-4-ultra': 'imagen-4.0-ultra-generate-001',
|
|
209
|
+
'imagen-capability': 'imagen-3.0-capability-001',
|
|
188
210
|
'gemini-flash-image': 'gemini-2.5-flash-image',
|
|
189
211
|
'gemini-pro-image': 'gemini-3-pro-image-preview',
|
|
190
212
|
'gemini-flash-image-2': 'gemini-3.1-flash-image-preview',
|
|
@@ -296,10 +318,18 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
|
|
|
296
318
|
hasReferenceImages: (0, base_tti_provider_1.hasReferenceImages)(request),
|
|
297
319
|
});
|
|
298
320
|
const isGeminiModel = GEMINI_API_MODELS.has(modelId);
|
|
299
|
-
const
|
|
321
|
+
const isEditRequest = !!request.baseImage;
|
|
322
|
+
const operationName = isEditRequest
|
|
323
|
+
? 'Imagen edit API call'
|
|
324
|
+
: isGeminiModel
|
|
325
|
+
? 'Gemini API call'
|
|
326
|
+
: 'Imagen API call';
|
|
300
327
|
// Operation lambda reads currentRegion from closure
|
|
301
328
|
const operation = () => {
|
|
302
|
-
if (
|
|
329
|
+
if (isEditRequest) {
|
|
330
|
+
return this.editWithImagen(request, modelId, currentRegion);
|
|
331
|
+
}
|
|
332
|
+
else if (isGeminiModel) {
|
|
303
333
|
return this.generateWithGemini(request, modelId, currentRegion);
|
|
304
334
|
}
|
|
305
335
|
else {
|
|
@@ -528,6 +558,77 @@ class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
|
|
|
528
558
|
usage,
|
|
529
559
|
};
|
|
530
560
|
}
|
|
561
|
+
async editWithImagen(request, modelId, region) {
|
|
562
|
+
const startTime = Date.now();
|
|
563
|
+
const internalModelId = MODEL_ID_MAP[modelId];
|
|
564
|
+
this.lastUsedRegion = region;
|
|
565
|
+
try {
|
|
566
|
+
const { client, helpers } = await this.getAiplatformClient(region);
|
|
567
|
+
const endpoint = `projects/${this.config.projectId}/locations/${region}/publishers/google/models/${internalModelId}`;
|
|
568
|
+
// Build referenceImages array: [RAW base image, MASK image]
|
|
569
|
+
const referenceImages = [
|
|
570
|
+
{
|
|
571
|
+
referenceType: 'REFERENCE_TYPE_RAW',
|
|
572
|
+
referenceId: 1,
|
|
573
|
+
referenceImage: {
|
|
574
|
+
bytesBase64Encoded: request.baseImage.base64,
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
referenceType: 'REFERENCE_TYPE_MASK',
|
|
579
|
+
referenceId: 2,
|
|
580
|
+
referenceImage: {
|
|
581
|
+
bytesBase64Encoded: request.maskImage.base64,
|
|
582
|
+
},
|
|
583
|
+
maskImageConfig: {
|
|
584
|
+
maskMode: 'MASK_MODE_USER_PROVIDED',
|
|
585
|
+
dilation: request.maskDilation ?? 0.01,
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
const instanceValue = {
|
|
590
|
+
prompt: request.prompt,
|
|
591
|
+
referenceImages,
|
|
592
|
+
};
|
|
593
|
+
const instance = helpers.toValue(instanceValue);
|
|
594
|
+
// Map editMode to Vertex AI constant, default to inpainting-insert
|
|
595
|
+
const editModeKey = request.editMode ?? 'inpainting-insert';
|
|
596
|
+
const vertexEditMode = GoogleCloudTTIProvider.EDIT_MODE_MAP[editModeKey] ?? 'EDIT_MODE_INPAINT_INSERTION';
|
|
597
|
+
const parameterValue = {
|
|
598
|
+
editMode: vertexEditMode,
|
|
599
|
+
sampleCount: request.n || 1,
|
|
600
|
+
editConfig: {
|
|
601
|
+
baseSteps: request.providerOptions?.baseSteps ?? 35,
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
const parameters = helpers.toValue(parameterValue);
|
|
605
|
+
this.log('info', 'Sending Imagen edit request to Vertex AI', {
|
|
606
|
+
endpoint,
|
|
607
|
+
editMode: vertexEditMode,
|
|
608
|
+
dilation: request.maskDilation ?? 0.01,
|
|
609
|
+
});
|
|
610
|
+
const [response] = await client.predict({
|
|
611
|
+
endpoint,
|
|
612
|
+
instances: [instance],
|
|
613
|
+
parameters,
|
|
614
|
+
});
|
|
615
|
+
const duration = Date.now() - startTime;
|
|
616
|
+
this.log('info', `Imagen edit response received in ${duration}ms`, {
|
|
617
|
+
duration,
|
|
618
|
+
hasPredictions: !!response.predictions?.length,
|
|
619
|
+
});
|
|
620
|
+
if (!response.predictions || response.predictions.length === 0) {
|
|
621
|
+
throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No images returned from Imagen edit API');
|
|
622
|
+
}
|
|
623
|
+
return this.processImagenResponse(response.predictions, helpers, modelId, duration);
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (error instanceof base_tti_provider_1.InvalidConfigError || error instanceof base_tti_provider_1.GenerationFailedError) {
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
throw this.handleError(error, 'during Imagen edit API call');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
531
632
|
// ============================================================
|
|
532
633
|
// PRIVATE: GEMINI IMAGE IMPLEMENTATION
|
|
533
634
|
// ============================================================
|
|
@@ -702,3 +803,13 @@ IMPORTANT: Maintain exact visual consistency with the subject in the reference -
|
|
|
702
803
|
}
|
|
703
804
|
}
|
|
704
805
|
exports.GoogleCloudTTIProvider = GoogleCloudTTIProvider;
|
|
806
|
+
// ============================================================
|
|
807
|
+
// PRIVATE: IMAGEN EDITING / INPAINTING IMPLEMENTATION
|
|
808
|
+
// ============================================================
|
|
809
|
+
/** Maps our editMode values to the Vertex AI API constants */
|
|
810
|
+
GoogleCloudTTIProvider.EDIT_MODE_MAP = {
|
|
811
|
+
'inpainting-insert': 'EDIT_MODE_INPAINT_INSERTION',
|
|
812
|
+
'inpainting-remove': 'EDIT_MODE_INPAINT_REMOVAL',
|
|
813
|
+
'background-swap': 'EDIT_MODE_BGSWAP',
|
|
814
|
+
'outpainting': 'EDIT_MODE_OUTPAINT',
|
|
815
|
+
};
|
|
@@ -105,6 +105,30 @@ export interface TTIRequest {
|
|
|
105
105
|
* Required when using referenceImages (e.g., "cute cartoon bear with red hat")
|
|
106
106
|
*/
|
|
107
107
|
subjectDescription?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Base image to edit. When present, activates "edit mode" instead of text-to-image generation.
|
|
110
|
+
* Requires maskImage and a model that supports imageEditing capability.
|
|
111
|
+
*/
|
|
112
|
+
baseImage?: TTIReferenceImage;
|
|
113
|
+
/**
|
|
114
|
+
* Mask image for inpainting. White pixels = regenerate, black pixels = preserve.
|
|
115
|
+
* Must have identical dimensions to baseImage.
|
|
116
|
+
* Required when baseImage is set.
|
|
117
|
+
*/
|
|
118
|
+
maskImage?: TTIReferenceImage;
|
|
119
|
+
/**
|
|
120
|
+
* Mask dilation: expands the mask boundary to smooth hard edges (0.0–1.0, default 0.01).
|
|
121
|
+
* Useful when hand-drawn masks have jagged edges.
|
|
122
|
+
*/
|
|
123
|
+
maskDilation?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Edit operation to perform on the masked region.
|
|
126
|
+
* - 'inpainting-insert': add or replace content in the masked area (default)
|
|
127
|
+
* - 'inpainting-remove': remove content and fill with matching background
|
|
128
|
+
* - 'background-swap': replace background while preserving foreground
|
|
129
|
+
* - 'outpainting': extend image beyond its boundaries into the masked area
|
|
130
|
+
*/
|
|
131
|
+
editMode?: 'inpainting-insert' | 'inpainting-remove' | 'background-swap' | 'outpainting';
|
|
108
132
|
/** Additional provider-specific options */
|
|
109
133
|
providerOptions?: Record<string, unknown>;
|
|
110
134
|
/**
|
|
@@ -235,6 +259,23 @@ export interface RetryOptions {
|
|
|
235
259
|
* long waits, while still allowing many retries for quota errors.
|
|
236
260
|
*/
|
|
237
261
|
timeoutRetries?: number;
|
|
262
|
+
/**
|
|
263
|
+
* Grace period in milliseconds after a timeout before abandoning the attempt (default: 0).
|
|
264
|
+
*
|
|
265
|
+
* When `timeoutMs` fires, instead of immediately failing and starting a new retry,
|
|
266
|
+
* the middleware waits an additional `graceMs`. If the in-flight request resolves
|
|
267
|
+
* successfully within this window, the result is used and no retry is needed.
|
|
268
|
+
*
|
|
269
|
+
* This prevents paying for (and discarding) a successful response that arrived
|
|
270
|
+
* slightly after the timeout threshold — which is common under quota pressure when
|
|
271
|
+
* Vertex AI eventually returns a valid result after a long backoff.
|
|
272
|
+
*
|
|
273
|
+
* Example: `timeoutMs: 210000, graceMs: 60000`
|
|
274
|
+
* → waits up to 210s normally, then up to 60s more to capture a late success.
|
|
275
|
+
*
|
|
276
|
+
* Set to 0 (default) to disable — timeout retries fire immediately as before.
|
|
277
|
+
*/
|
|
278
|
+
graceMs?: number;
|
|
238
279
|
/**
|
|
239
280
|
* @deprecated Use `backoffMultiplier` instead. Will be removed in v2.0.
|
|
240
281
|
* When true, equivalent to backoffMultiplier of 1.0 with linear scaling (delayMs * attempt).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loonylabs/tti-middleware",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|