@loonylabs/tti-middleware 1.9.0 → 1.11.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
@@ -24,6 +24,7 @@
24
24
  - [Configuration](#%EF%B8%8F-configuration)
25
25
  - [Providers & Models](#-providers--models)
26
26
  - [Character Consistency](#-character-consistency)
27
+ - [Inpainting / Image Editing](#inpainting--image-editing)
27
28
  - [GDPR / Compliance](#-gdpr--compliance)
28
29
  - [API Reference](#-api-reference)
29
30
  - [Advanced Features](#-advanced-features)
@@ -44,6 +45,7 @@
44
45
  - **Eden AI**: Aggregator with access to OpenAI, Stability AI, Replicate (experimental)
45
46
  - **IONOS**: German cloud provider with OpenAI-compatible API (experimental)
46
47
  - **Character Consistency**: Generate consistent characters across multiple images (perfect for children's book illustrations)
48
+ - **Inpainting**: Fix specific areas of a generated image without regenerating the entire scene — via Vertex AI `imagen-capability` model. Supports optional subject reference images (`maskReferenceImages`) to guide *what* gets inserted into the masked area
47
49
  - **GDPR/DSGVO Compliance**: Built-in EU region support with automatic fallback
48
50
  - **Region Rotation**: Opt-in region rotation on quota errors (429) for Google Cloud — rotate through regions instead of retrying the same exhausted region
49
51
  - **Retry Logic**: Exponential backoff with jitter for transient errors (429, 408, 5xx, timeouts)
@@ -307,6 +309,72 @@ const duelScene = await service.generate({
307
309
 
308
310
  - Model must be `gemini-flash-image` (only model supporting character consistency)
309
311
 
312
+ ## Inpainting / Image Editing
313
+
314
+ The `imagen-capability` model supports mask-based inpainting via Vertex AI. This is the **only** model that supports pixel-precise editing with a mask image.
315
+
316
+ ### Basic Inpainting
317
+
318
+ ```typescript
319
+ const result = await service.generate({
320
+ model: 'imagen-capability',
321
+ prompt: 'Remove the extra arm and fill with matching forest background',
322
+ baseImage: { base64: originalImageBase64, mimeType: 'image/png' },
323
+ maskImage: { base64: maskBase64, mimeType: 'image/png' },
324
+ editMode: 'inpainting-remove', // default: 'inpainting-insert'
325
+ maskDilation: 0.02, // optional, 0.0–1.0, default 0.01
326
+ });
327
+ ```
328
+
329
+ **How the mask works:**
330
+ - White pixels = area the model will regenerate
331
+ - Black pixels = area preserved exactly as-is
332
+ - Mask must have identical dimensions to `baseImage`
333
+
334
+ ### Guided Inpainting with Subject References
335
+
336
+ Use `maskReferenceImages` to provide a reference photo of the subject to insert — e.g. "place **this** character into the masked region":
337
+
338
+ ```typescript
339
+ const result = await service.generate({
340
+ model: 'imagen-capability',
341
+ prompt: 'The character standing in a bright forest clearing, photorealistic',
342
+ baseImage: { base64: sceneBase64, mimeType: 'image/png' },
343
+ maskImage: { base64: maskBase64, mimeType: 'image/png' },
344
+ editMode: 'inpainting-insert',
345
+ maskReferenceImages: [
346
+ {
347
+ base64: characterRefBase64,
348
+ mimeType: 'image/png',
349
+ subjectType: 'person', // 'person' | 'animal' | 'product' | 'default'
350
+ },
351
+ ],
352
+ });
353
+ ```
354
+
355
+ **Subject types:**
356
+
357
+ | `subjectType` | Use case |
358
+ |---------------|----------|
359
+ | `'person'` | Human character |
360
+ | `'animal'` | Animal or creature |
361
+ | `'product'` | Object, item, product |
362
+ | `'default'` | Let the model decide (safe fallback) |
363
+
364
+ **Notes:**
365
+ - `maskReferenceImages` only works with `editMode: 'inpainting-insert'`
366
+ - Gemini models do **not** support mask-based inpainting or `maskReferenceImages`
367
+ - `maskReferenceImages` without `baseImage` throws a validation error
368
+
369
+ ### Supported `editMode` Values
370
+
371
+ | Value | Description |
372
+ |-------|-------------|
373
+ | `'inpainting-insert'` | Add or replace content in masked area (default) |
374
+ | `'inpainting-remove'` | Remove content and fill with matching background |
375
+ | `'background-swap'` | Replace background, preserve foreground |
376
+ | `'outpainting'` | Extend image beyond its boundaries into the masked area |
377
+
310
378
  ## GDPR / Compliance
311
379
 
312
380
  ### Provider Compliance Overview
@@ -365,15 +433,30 @@ interface TTIRequest {
365
433
  n?: number; // Number of images (default: 1)
366
434
  aspectRatio?: string; // '1:1', '16:9', '4:3', etc.
367
435
 
368
- // Character consistency
436
+ // Character consistency (Gemini models only)
369
437
  referenceImages?: TTIReferenceImage[];
370
438
  subjectDescription?: string;
371
439
 
440
+ // Inpainting / image editing (imagen-capability only)
441
+ baseImage?: TTIReferenceImage; // Activates edit mode when set
442
+ maskImage?: TTIReferenceImage; // Required when baseImage is set
443
+ maskDilation?: number; // 0.0–1.0, default 0.01
444
+ editMode?: 'inpainting-insert' | 'inpainting-remove' | 'background-swap' | 'outpainting';
445
+ maskReferenceImages?: TTIMaskReferenceImage[]; // Subject refs for guided inpainting
446
+
372
447
  // Retry configuration
373
448
  retry?: boolean | RetryOptions; // true (default), false, or custom config
374
449
 
375
450
  providerOptions?: Record<string, unknown>;
376
451
  }
452
+
453
+ type TTISubjectType = 'person' | 'animal' | 'product' | 'default';
454
+
455
+ interface TTIMaskReferenceImage {
456
+ base64: string;
457
+ mimeType?: string;
458
+ subjectType?: TTISubjectType; // defaults to 'default'
459
+ }
377
460
  ```
378
461
 
379
462
  ### TTIResponse
@@ -196,6 +196,38 @@ 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
+ if (request.maskReferenceImages && request.maskReferenceImages.length > 0) {
220
+ for (let i = 0; i < request.maskReferenceImages.length; i++) {
221
+ const ref = request.maskReferenceImages[i];
222
+ if (!ref.base64 || ref.base64.trim().length === 0) {
223
+ throw new InvalidConfigError(this.providerName, `maskReferenceImages[${i}] has empty base64 data`);
224
+ }
225
+ }
226
+ }
227
+ }
228
+ if (request.maskReferenceImages && request.maskReferenceImages.length > 0 && !request.baseImage) {
229
+ throw new InvalidConfigError(this.providerName, 'maskReferenceImages requires baseImage to be set');
230
+ }
199
231
  // If reference images are provided, validate them
200
232
  if (request.referenceImages && request.referenceImages.length > 0) {
201
233
  const modelId = request.model || this.getDefaultModel();
@@ -63,6 +63,10 @@ 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 static readonly SUBJECT_TYPE_MAP;
69
+ private editWithImagen;
66
70
  private generateWithGemini;
67
71
  private getGenaiClient;
68
72
  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 operationName = isGeminiModel ? 'Gemini API call' : 'Imagen API call';
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 (isGeminiModel) {
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,94 @@ 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
+ // Append optional subject reference images for guided inpainting
590
+ if (request.maskReferenceImages && request.maskReferenceImages.length > 0) {
591
+ for (const [i, ref] of request.maskReferenceImages.entries()) {
592
+ const subjectType = GoogleCloudTTIProvider.SUBJECT_TYPE_MAP[ref.subjectType ?? 'default'] ??
593
+ 'SUBJECT_TYPE_DEFAULT';
594
+ referenceImages.push({
595
+ referenceType: 'REFERENCE_TYPE_SUBJECT',
596
+ referenceId: 3 + i,
597
+ referenceImage: {
598
+ bytesBase64Encoded: ref.base64,
599
+ },
600
+ subjectImageConfig: {
601
+ subjectType,
602
+ },
603
+ });
604
+ }
605
+ }
606
+ const instanceValue = {
607
+ prompt: request.prompt,
608
+ referenceImages,
609
+ };
610
+ const instance = helpers.toValue(instanceValue);
611
+ // Map editMode to Vertex AI constant, default to inpainting-insert
612
+ const editModeKey = request.editMode ?? 'inpainting-insert';
613
+ const vertexEditMode = GoogleCloudTTIProvider.EDIT_MODE_MAP[editModeKey] ?? 'EDIT_MODE_INPAINT_INSERTION';
614
+ const parameterValue = {
615
+ editMode: vertexEditMode,
616
+ sampleCount: request.n || 1,
617
+ editConfig: {
618
+ baseSteps: request.providerOptions?.baseSteps ?? 35,
619
+ },
620
+ };
621
+ const parameters = helpers.toValue(parameterValue);
622
+ this.log('info', 'Sending Imagen edit request to Vertex AI', {
623
+ endpoint,
624
+ editMode: vertexEditMode,
625
+ dilation: request.maskDilation ?? 0.01,
626
+ });
627
+ const [response] = await client.predict({
628
+ endpoint,
629
+ instances: [instance],
630
+ parameters,
631
+ });
632
+ const duration = Date.now() - startTime;
633
+ this.log('info', `Imagen edit response received in ${duration}ms`, {
634
+ duration,
635
+ hasPredictions: !!response.predictions?.length,
636
+ });
637
+ if (!response.predictions || response.predictions.length === 0) {
638
+ throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No images returned from Imagen edit API');
639
+ }
640
+ return this.processImagenResponse(response.predictions, helpers, modelId, duration);
641
+ }
642
+ catch (error) {
643
+ if (error instanceof base_tti_provider_1.InvalidConfigError || error instanceof base_tti_provider_1.GenerationFailedError) {
644
+ throw error;
645
+ }
646
+ throw this.handleError(error, 'during Imagen edit API call');
647
+ }
648
+ }
531
649
  // ============================================================
532
650
  // PRIVATE: GEMINI IMAGE IMPLEMENTATION
533
651
  // ============================================================
@@ -702,3 +820,19 @@ IMPORTANT: Maintain exact visual consistency with the subject in the reference -
702
820
  }
703
821
  }
704
822
  exports.GoogleCloudTTIProvider = GoogleCloudTTIProvider;
823
+ // ============================================================
824
+ // PRIVATE: IMAGEN EDITING / INPAINTING IMPLEMENTATION
825
+ // ============================================================
826
+ /** Maps our editMode values to the Vertex AI API constants */
827
+ GoogleCloudTTIProvider.EDIT_MODE_MAP = {
828
+ 'inpainting-insert': 'EDIT_MODE_INPAINT_INSERTION',
829
+ 'inpainting-remove': 'EDIT_MODE_INPAINT_REMOVAL',
830
+ 'background-swap': 'EDIT_MODE_BGSWAP',
831
+ 'outpainting': 'EDIT_MODE_OUTPAINT',
832
+ };
833
+ GoogleCloudTTIProvider.SUBJECT_TYPE_MAP = {
834
+ person: 'SUBJECT_TYPE_PERSON',
835
+ animal: 'SUBJECT_TYPE_ANIMAL',
836
+ product: 'SUBJECT_TYPE_PRODUCT',
837
+ default: 'SUBJECT_TYPE_DEFAULT',
838
+ };
@@ -82,6 +82,30 @@ export interface TTIReferenceImage {
82
82
  /** MIME type of the image (e.g., 'image/png', 'image/jpeg') */
83
83
  mimeType?: string;
84
84
  }
85
+ /**
86
+ * Subject type hint for mask reference images.
87
+ * Helps the model understand what kind of subject is shown in the reference image.
88
+ * - 'person' — a human character
89
+ * - 'animal' — an animal or creature
90
+ * - 'product' — an object, product, or item
91
+ * - 'default' — let the model decide (fallback)
92
+ */
93
+ export type TTISubjectType = 'person' | 'animal' | 'product' | 'default';
94
+ /**
95
+ * Reference image for mask-based inpainting (subject reference).
96
+ * Used with maskReferenceImages to guide what the model inserts into the masked area.
97
+ */
98
+ export interface TTIMaskReferenceImage {
99
+ /** Base64-encoded image data of the subject to insert */
100
+ base64: string;
101
+ /** MIME type of the image (e.g., 'image/png', 'image/jpeg') */
102
+ mimeType?: string;
103
+ /**
104
+ * Subject type hint for the model.
105
+ * Defaults to 'default' if omitted.
106
+ */
107
+ subjectType?: TTISubjectType;
108
+ }
85
109
  /**
86
110
  * Unified TTI generation request
87
111
  * Works for both simple text-to-image and character consistency
@@ -105,6 +129,38 @@ export interface TTIRequest {
105
129
  * Required when using referenceImages (e.g., "cute cartoon bear with red hat")
106
130
  */
107
131
  subjectDescription?: string;
132
+ /**
133
+ * Base image to edit. When present, activates "edit mode" instead of text-to-image generation.
134
+ * Requires maskImage and a model that supports imageEditing capability.
135
+ */
136
+ baseImage?: TTIReferenceImage;
137
+ /**
138
+ * Mask image for inpainting. White pixels = regenerate, black pixels = preserve.
139
+ * Must have identical dimensions to baseImage.
140
+ * Required when baseImage is set.
141
+ */
142
+ maskImage?: TTIReferenceImage;
143
+ /**
144
+ * Mask dilation: expands the mask boundary to smooth hard edges (0.0–1.0, default 0.01).
145
+ * Useful when hand-drawn masks have jagged edges.
146
+ */
147
+ maskDilation?: number;
148
+ /**
149
+ * Edit operation to perform on the masked region.
150
+ * - 'inpainting-insert': add or replace content in the masked area (default)
151
+ * - 'inpainting-remove': remove content and fill with matching background
152
+ * - 'background-swap': replace background while preserving foreground
153
+ * - 'outpainting': extend image beyond its boundaries into the masked area
154
+ */
155
+ editMode?: 'inpainting-insert' | 'inpainting-remove' | 'background-swap' | 'outpainting';
156
+ /**
157
+ * Optional subject reference images for mask-based inpainting.
158
+ * Only valid when baseImage and maskImage are set.
159
+ * Each entry guides the model to insert a specific subject into the masked area
160
+ * (e.g., "place the character from this reference image into the mask").
161
+ * Only supported by the 'imagen-capability' model.
162
+ */
163
+ maskReferenceImages?: TTIMaskReferenceImage[];
108
164
  /** Additional provider-specific options */
109
165
  providerOptions?: Record<string, unknown>;
110
166
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loonylabs/tti-middleware",
3
- "version": "1.9.0",
3
+ "version": "1.11.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",