@loonylabs/tti-middleware 1.0.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/.env.example +45 -0
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/middleware/services/tti/index.d.ts +2 -0
- package/dist/middleware/services/tti/index.js +18 -0
- package/dist/middleware/services/tti/providers/base-tti-provider.d.ts +98 -0
- package/dist/middleware/services/tti/providers/base-tti-provider.js +306 -0
- package/dist/middleware/services/tti/providers/edenai-provider.d.ts +28 -0
- package/dist/middleware/services/tti/providers/edenai-provider.js +181 -0
- package/dist/middleware/services/tti/providers/google-cloud-provider.d.ts +60 -0
- package/dist/middleware/services/tti/providers/google-cloud-provider.js +473 -0
- package/dist/middleware/services/tti/providers/index.d.ts +4 -0
- package/dist/middleware/services/tti/providers/index.js +22 -0
- package/dist/middleware/services/tti/providers/ionos-provider.d.ts +26 -0
- package/dist/middleware/services/tti/providers/ionos-provider.js +134 -0
- package/dist/middleware/services/tti/tti.service.d.ts +70 -0
- package/dist/middleware/services/tti/tti.service.js +162 -0
- package/dist/middleware/types/index.d.ts +196 -0
- package/dist/middleware/types/index.js +37 -0
- package/package.json +90 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Google Cloud TTI Provider
|
|
4
|
+
*
|
|
5
|
+
* Unified provider for Google Cloud's image generation services:
|
|
6
|
+
* - Imagen 3 (imagegeneration@006) - High quality text-to-image
|
|
7
|
+
* - Gemini 2.5 Flash Image - Text-to-image with character consistency
|
|
8
|
+
*
|
|
9
|
+
* All requests go through Google Cloud (Vertex AI) with proper DPA.
|
|
10
|
+
* EU-compliant when using EU regions.
|
|
11
|
+
*
|
|
12
|
+
* @see https://cloud.google.com/vertex-ai/generative-ai/pricing
|
|
13
|
+
* @see https://cloud.google.com/terms/data-processing-addendum
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.GoogleCloudTTIProvider = void 0;
|
|
50
|
+
const types_1 = require("../../../types");
|
|
51
|
+
const base_tti_provider_1 = require("./base-tti-provider");
|
|
52
|
+
// ============================================================
|
|
53
|
+
// MODEL DEFINITIONS
|
|
54
|
+
// ============================================================
|
|
55
|
+
const GOOGLE_CLOUD_MODELS = [
|
|
56
|
+
{
|
|
57
|
+
id: 'imagen-3',
|
|
58
|
+
displayName: 'Imagen 3',
|
|
59
|
+
capabilities: {
|
|
60
|
+
textToImage: true,
|
|
61
|
+
characterConsistency: false, // Requires allowlist for Customization API
|
|
62
|
+
imageEditing: false,
|
|
63
|
+
maxImagesPerRequest: 4,
|
|
64
|
+
},
|
|
65
|
+
availableRegions: [
|
|
66
|
+
'europe-west1',
|
|
67
|
+
'europe-west2',
|
|
68
|
+
'europe-west3',
|
|
69
|
+
'europe-west4',
|
|
70
|
+
'europe-west9',
|
|
71
|
+
'us-central1',
|
|
72
|
+
'us-east4',
|
|
73
|
+
],
|
|
74
|
+
pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'gemini-flash-image',
|
|
78
|
+
displayName: 'Gemini 2.5 Flash Image',
|
|
79
|
+
capabilities: {
|
|
80
|
+
textToImage: true,
|
|
81
|
+
characterConsistency: true, // Built-in support!
|
|
82
|
+
imageEditing: false,
|
|
83
|
+
maxImagesPerRequest: 1,
|
|
84
|
+
},
|
|
85
|
+
// Note: NOT available in europe-west3 (Frankfurt)!
|
|
86
|
+
availableRegions: [
|
|
87
|
+
'europe-west1',
|
|
88
|
+
'europe-west4',
|
|
89
|
+
'europe-north1',
|
|
90
|
+
'us-central1',
|
|
91
|
+
'us-east4',
|
|
92
|
+
],
|
|
93
|
+
pricingUrl: 'https://cloud.google.com/vertex-ai/generative-ai/pricing',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
// Internal model IDs used in API calls
|
|
97
|
+
const MODEL_ID_MAP = {
|
|
98
|
+
'imagen-3': 'imagegeneration@006',
|
|
99
|
+
'gemini-flash-image': 'gemini-2.5-flash-image',
|
|
100
|
+
};
|
|
101
|
+
// ============================================================
|
|
102
|
+
// PROVIDER IMPLEMENTATION
|
|
103
|
+
// ============================================================
|
|
104
|
+
class GoogleCloudTTIProvider extends base_tti_provider_1.BaseTTIProvider {
|
|
105
|
+
constructor(config) {
|
|
106
|
+
super(types_1.TTIProvider.GOOGLE_CLOUD);
|
|
107
|
+
this.lastUsedRegion = null;
|
|
108
|
+
// Lazy-loaded SDK clients
|
|
109
|
+
this.aiplatformClient = null;
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
this.genaiClient = null;
|
|
112
|
+
const projectId = config?.projectId ||
|
|
113
|
+
process.env.GOOGLE_CLOUD_PROJECT ||
|
|
114
|
+
process.env.GCLOUD_PROJECT ||
|
|
115
|
+
'';
|
|
116
|
+
const region = config?.region ||
|
|
117
|
+
process.env.GOOGLE_CLOUD_REGION ||
|
|
118
|
+
process.env.VERTEX_AI_REGION ||
|
|
119
|
+
'europe-west4'; // Default to Netherlands (supports all models)
|
|
120
|
+
if (!projectId) {
|
|
121
|
+
throw new base_tti_provider_1.InvalidConfigError(types_1.TTIProvider.GOOGLE_CLOUD, 'Google Cloud Project ID is required. Set GOOGLE_CLOUD_PROJECT or pass projectId in config.');
|
|
122
|
+
}
|
|
123
|
+
this.config = {
|
|
124
|
+
projectId,
|
|
125
|
+
region,
|
|
126
|
+
keyFilename: config?.keyFilename || process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
127
|
+
credentials: config?.credentials,
|
|
128
|
+
};
|
|
129
|
+
this.log('info', 'Google Cloud TTI Provider initialized', {
|
|
130
|
+
projectId: this.config.projectId,
|
|
131
|
+
region: this.config.region,
|
|
132
|
+
isEURegion: (0, base_tti_provider_1.isEURegion)(this.config.region),
|
|
133
|
+
models: this.listModels().map((m) => m.id),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// ============================================================
|
|
137
|
+
// ITTIProvider IMPLEMENTATION
|
|
138
|
+
// ============================================================
|
|
139
|
+
getDisplayName() {
|
|
140
|
+
return 'Google Cloud';
|
|
141
|
+
}
|
|
142
|
+
listModels() {
|
|
143
|
+
return GOOGLE_CLOUD_MODELS;
|
|
144
|
+
}
|
|
145
|
+
getDefaultModel() {
|
|
146
|
+
// Default to Gemini Flash Image as it supports character consistency
|
|
147
|
+
return 'gemini-flash-image';
|
|
148
|
+
}
|
|
149
|
+
async generate(request) {
|
|
150
|
+
this.validateRequest(request);
|
|
151
|
+
const modelId = request.model || this.getDefaultModel();
|
|
152
|
+
const modelInfo = this.getModelInfo(modelId);
|
|
153
|
+
if (!modelInfo) {
|
|
154
|
+
throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Unknown model: ${modelId}. Available models: ${this.listModels()
|
|
155
|
+
.map((m) => m.id)
|
|
156
|
+
.join(', ')}`);
|
|
157
|
+
}
|
|
158
|
+
// Validate region availability
|
|
159
|
+
const effectiveRegion = this.getEffectiveRegion(modelId);
|
|
160
|
+
this.log('debug', 'Generating image', {
|
|
161
|
+
model: modelId,
|
|
162
|
+
region: effectiveRegion,
|
|
163
|
+
hasReferenceImages: (0, base_tti_provider_1.hasReferenceImages)(request),
|
|
164
|
+
});
|
|
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}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// ============================================================
|
|
176
|
+
// PUBLIC HELPER METHODS
|
|
177
|
+
// ============================================================
|
|
178
|
+
/**
|
|
179
|
+
* Get the configured region
|
|
180
|
+
*/
|
|
181
|
+
getRegion() {
|
|
182
|
+
return this.config.region;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Check if the configured region is hosted in the EU
|
|
186
|
+
*/
|
|
187
|
+
isEURegion() {
|
|
188
|
+
return (0, base_tti_provider_1.isEURegion)(this.config.region);
|
|
189
|
+
}
|
|
190
|
+
// ============================================================
|
|
191
|
+
// PRIVATE: REGION HANDLING
|
|
192
|
+
// ============================================================
|
|
193
|
+
/**
|
|
194
|
+
* Get the effective region for a model, considering availability
|
|
195
|
+
*/
|
|
196
|
+
getEffectiveRegion(modelId) {
|
|
197
|
+
const modelInfo = this.getModelInfo(modelId);
|
|
198
|
+
if (!modelInfo?.availableRegions) {
|
|
199
|
+
return this.config.region;
|
|
200
|
+
}
|
|
201
|
+
// Check if configured region supports this model
|
|
202
|
+
if (modelInfo.availableRegions.includes(this.config.region)) {
|
|
203
|
+
return this.config.region;
|
|
204
|
+
}
|
|
205
|
+
// Find best alternative (prefer EU regions)
|
|
206
|
+
const euAlternatives = modelInfo.availableRegions.filter(base_tti_provider_1.isEURegion);
|
|
207
|
+
if (euAlternatives.length > 0) {
|
|
208
|
+
const fallback = euAlternatives[0];
|
|
209
|
+
this.log('warn', `Model ${modelId} not available in ${this.config.region}, using ${fallback}`, { configuredRegion: this.config.region, fallbackRegion: fallback });
|
|
210
|
+
return fallback;
|
|
211
|
+
}
|
|
212
|
+
// No EU region available, use first available
|
|
213
|
+
const fallback = modelInfo.availableRegions[0];
|
|
214
|
+
this.log('warn', `Model ${modelId} not available in EU regions, using ${fallback}`, { configuredRegion: this.config.region, fallbackRegion: fallback });
|
|
215
|
+
return fallback;
|
|
216
|
+
}
|
|
217
|
+
// ============================================================
|
|
218
|
+
// PRIVATE: IMAGEN 3 IMPLEMENTATION
|
|
219
|
+
// ============================================================
|
|
220
|
+
async generateWithImagen(request, region) {
|
|
221
|
+
const startTime = Date.now();
|
|
222
|
+
const internalModelId = MODEL_ID_MAP['imagen-3'];
|
|
223
|
+
this.lastUsedRegion = region;
|
|
224
|
+
try {
|
|
225
|
+
const { client, helpers } = await this.getAiplatformClient();
|
|
226
|
+
const endpoint = `projects/${this.config.projectId}/locations/${region}/publishers/google/models/${internalModelId}`;
|
|
227
|
+
// Build instance
|
|
228
|
+
const instanceValue = { prompt: request.prompt };
|
|
229
|
+
const instance = helpers.toValue(instanceValue);
|
|
230
|
+
// Build parameters
|
|
231
|
+
const parameterValue = {
|
|
232
|
+
sampleCount: request.n || 1,
|
|
233
|
+
};
|
|
234
|
+
if (request.aspectRatio) {
|
|
235
|
+
parameterValue.aspectRatio = request.aspectRatio;
|
|
236
|
+
}
|
|
237
|
+
// Pass through provider-specific options
|
|
238
|
+
if (request.providerOptions) {
|
|
239
|
+
if (request.providerOptions.seed !== undefined) {
|
|
240
|
+
parameterValue.seed = request.providerOptions.seed;
|
|
241
|
+
}
|
|
242
|
+
if (request.providerOptions.safetyFilterLevel) {
|
|
243
|
+
parameterValue.safetyFilterLevel = request.providerOptions.safetyFilterLevel;
|
|
244
|
+
}
|
|
245
|
+
if (request.providerOptions.personGeneration) {
|
|
246
|
+
parameterValue.personGeneration = request.providerOptions.personGeneration;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const parameters = helpers.toValue(parameterValue);
|
|
250
|
+
this.log('debug', 'Sending Imagen request', { endpoint, parameters: parameterValue });
|
|
251
|
+
const [response] = await client.predict({
|
|
252
|
+
endpoint,
|
|
253
|
+
instances: [instance],
|
|
254
|
+
parameters,
|
|
255
|
+
});
|
|
256
|
+
const duration = Date.now() - startTime;
|
|
257
|
+
if (!response.predictions || response.predictions.length === 0) {
|
|
258
|
+
throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No images returned from Imagen API');
|
|
259
|
+
}
|
|
260
|
+
return this.processImagenResponse(response.predictions, helpers, duration);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (error instanceof base_tti_provider_1.InvalidConfigError || error instanceof base_tti_provider_1.GenerationFailedError) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
throw this.handleError(error, 'during Imagen API call');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async getAiplatformClient() {
|
|
270
|
+
if (!this.aiplatformClient) {
|
|
271
|
+
try {
|
|
272
|
+
const { v1, helpers } = await Promise.resolve().then(() => __importStar(require('@google-cloud/aiplatform')));
|
|
273
|
+
const clientOptions = {
|
|
274
|
+
apiEndpoint: `${this.config.region}-aiplatform.googleapis.com`,
|
|
275
|
+
};
|
|
276
|
+
if (this.config.keyFilename) {
|
|
277
|
+
clientOptions.keyFilename = this.config.keyFilename;
|
|
278
|
+
}
|
|
279
|
+
else if (this.config.credentials) {
|
|
280
|
+
clientOptions.credentials = this.config.credentials;
|
|
281
|
+
}
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
283
|
+
this.aiplatformClient = new v1.PredictionServiceClient(clientOptions);
|
|
284
|
+
return {
|
|
285
|
+
client: this.aiplatformClient,
|
|
286
|
+
helpers: helpers,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Failed to load @google-cloud/aiplatform. Install it with: npm install @google-cloud/aiplatform`, error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const { helpers } = await Promise.resolve().then(() => __importStar(require('@google-cloud/aiplatform')));
|
|
294
|
+
return {
|
|
295
|
+
client: this.aiplatformClient,
|
|
296
|
+
helpers: helpers,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
processImagenResponse(predictions, helpers, duration) {
|
|
300
|
+
const images = [];
|
|
301
|
+
for (const prediction of predictions) {
|
|
302
|
+
const predictionObj = helpers.fromValue(prediction);
|
|
303
|
+
if (predictionObj?.bytesBase64Encoded) {
|
|
304
|
+
images.push({
|
|
305
|
+
base64: predictionObj.bytesBase64Encoded,
|
|
306
|
+
contentType: predictionObj.mimeType || 'image/png',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (images.length === 0) {
|
|
311
|
+
throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No valid images in Imagen response');
|
|
312
|
+
}
|
|
313
|
+
const usage = {
|
|
314
|
+
imagesGenerated: images.length,
|
|
315
|
+
modelId: 'imagen-3',
|
|
316
|
+
};
|
|
317
|
+
return {
|
|
318
|
+
images,
|
|
319
|
+
metadata: {
|
|
320
|
+
provider: this.providerName,
|
|
321
|
+
model: 'imagen-3',
|
|
322
|
+
region: this.lastUsedRegion || this.config.region,
|
|
323
|
+
duration,
|
|
324
|
+
},
|
|
325
|
+
usage,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// ============================================================
|
|
329
|
+
// PRIVATE: GEMINI FLASH IMAGE IMPLEMENTATION
|
|
330
|
+
// ============================================================
|
|
331
|
+
async generateWithGemini(request, region) {
|
|
332
|
+
const startTime = Date.now();
|
|
333
|
+
const internalModelId = MODEL_ID_MAP['gemini-flash-image'];
|
|
334
|
+
this.lastUsedRegion = region;
|
|
335
|
+
try {
|
|
336
|
+
const client = await this.getGenaiClient(region);
|
|
337
|
+
// Build request parts
|
|
338
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
339
|
+
const parts = [];
|
|
340
|
+
// Add reference images first (for character consistency)
|
|
341
|
+
if ((0, base_tti_provider_1.hasReferenceImages)(request)) {
|
|
342
|
+
for (const ref of request.referenceImages) {
|
|
343
|
+
parts.push({
|
|
344
|
+
inlineData: {
|
|
345
|
+
mimeType: ref.mimeType || 'image/png',
|
|
346
|
+
data: ref.base64,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
// Build character consistency prompt
|
|
351
|
+
const fullPrompt = this.buildCharacterConsistencyPrompt(request.prompt, request.subjectDescription, request.referenceImages.length);
|
|
352
|
+
parts.push({ text: fullPrompt });
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
parts.push({ text: request.prompt });
|
|
356
|
+
}
|
|
357
|
+
const contents = [{ role: 'user', parts }];
|
|
358
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
359
|
+
const config = {
|
|
360
|
+
responseModalities: ['TEXT', 'IMAGE'],
|
|
361
|
+
};
|
|
362
|
+
// Add temperature if provided
|
|
363
|
+
if (request.providerOptions?.temperature !== undefined) {
|
|
364
|
+
config.temperature = request.providerOptions.temperature;
|
|
365
|
+
}
|
|
366
|
+
this.log('debug', 'Sending Gemini request', {
|
|
367
|
+
model: internalModelId,
|
|
368
|
+
region,
|
|
369
|
+
hasReferenceImages: (0, base_tti_provider_1.hasReferenceImages)(request),
|
|
370
|
+
});
|
|
371
|
+
const response = await client.generateContent({
|
|
372
|
+
model: internalModelId,
|
|
373
|
+
contents,
|
|
374
|
+
config,
|
|
375
|
+
});
|
|
376
|
+
const duration = Date.now() - startTime;
|
|
377
|
+
return this.processGeminiResponse(response, duration);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (error instanceof base_tti_provider_1.InvalidConfigError || error instanceof base_tti_provider_1.GenerationFailedError) {
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
throw this.handleError(error, 'during Gemini API call');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
387
|
+
async getGenaiClient(region) {
|
|
388
|
+
// Recreate client if region changed
|
|
389
|
+
if (!this.genaiClient) {
|
|
390
|
+
try {
|
|
391
|
+
const { GoogleGenAI } = await Promise.resolve().then(() => __importStar(require('@google/genai')));
|
|
392
|
+
// Set environment variables for Vertex AI backend
|
|
393
|
+
process.env.GOOGLE_GENAI_USE_VERTEXAI = 'true';
|
|
394
|
+
process.env.GOOGLE_CLOUD_PROJECT = this.config.projectId;
|
|
395
|
+
process.env.GOOGLE_CLOUD_LOCATION = region;
|
|
396
|
+
this.genaiClient = new GoogleGenAI({
|
|
397
|
+
vertexai: true,
|
|
398
|
+
project: this.config.projectId,
|
|
399
|
+
location: region,
|
|
400
|
+
});
|
|
401
|
+
this.log('debug', 'Initialized @google/genai with Vertex AI backend', {
|
|
402
|
+
project: this.config.projectId,
|
|
403
|
+
location: region,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
throw new base_tti_provider_1.InvalidConfigError(this.providerName, `Failed to load @google/genai. Install it with: npm install @google/genai`, error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
412
|
+
generateContent: async (params) => {
|
|
413
|
+
return this.genaiClient.models.generateContent(params);
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
buildCharacterConsistencyPrompt(userPrompt, subjectDescription, referenceCount) {
|
|
418
|
+
const referenceText = referenceCount === 1 ? 'the reference image' : `the ${referenceCount} reference images`;
|
|
419
|
+
return `Using ${referenceText} as a reference for the character "${subjectDescription}", generate a new image where: ${userPrompt}
|
|
420
|
+
|
|
421
|
+
IMPORTANT: Maintain exact visual consistency with the character in the reference - same style, colors, proportions, and distinctive features. The character should be immediately recognizable as the same one from the reference.`;
|
|
422
|
+
}
|
|
423
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
424
|
+
processGeminiResponse(response, duration) {
|
|
425
|
+
const images = [];
|
|
426
|
+
const candidates = response?.candidates || response?.response?.candidates;
|
|
427
|
+
if (!candidates || candidates.length === 0) {
|
|
428
|
+
throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No candidates returned from Gemini API');
|
|
429
|
+
}
|
|
430
|
+
for (const candidate of candidates) {
|
|
431
|
+
const parts = candidate?.content?.parts || [];
|
|
432
|
+
for (const part of parts) {
|
|
433
|
+
if (part.inlineData?.data) {
|
|
434
|
+
images.push({
|
|
435
|
+
base64: part.inlineData.data,
|
|
436
|
+
contentType: part.inlineData.mimeType || 'image/png',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (images.length === 0) {
|
|
442
|
+
const firstParts = candidates[0]?.content?.parts || [];
|
|
443
|
+
const partTypes = firstParts.map((p) => {
|
|
444
|
+
if (p.text)
|
|
445
|
+
return `text(${p.text.substring(0, 50)}...)`;
|
|
446
|
+
if (p.inlineData)
|
|
447
|
+
return `inlineData(${p.inlineData.mimeType})`;
|
|
448
|
+
return 'unknown';
|
|
449
|
+
});
|
|
450
|
+
this.log('error', 'No images in Gemini response', {
|
|
451
|
+
candidateCount: candidates.length,
|
|
452
|
+
partTypes,
|
|
453
|
+
});
|
|
454
|
+
throw new base_tti_provider_1.GenerationFailedError(this.providerName, `No images in response. Model returned: ${partTypes.join(', ')}. ` +
|
|
455
|
+
'Make sure responseModalities includes IMAGE.');
|
|
456
|
+
}
|
|
457
|
+
const usage = {
|
|
458
|
+
imagesGenerated: images.length,
|
|
459
|
+
modelId: 'gemini-flash-image',
|
|
460
|
+
};
|
|
461
|
+
return {
|
|
462
|
+
images,
|
|
463
|
+
metadata: {
|
|
464
|
+
provider: this.providerName,
|
|
465
|
+
model: 'gemini-flash-image',
|
|
466
|
+
region: this.lastUsedRegion || this.config.region,
|
|
467
|
+
duration,
|
|
468
|
+
},
|
|
469
|
+
usage,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
exports.GoogleCloudTTIProvider = GoogleCloudTTIProvider;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
// Base provider, errors, and logging
|
|
18
|
+
__exportStar(require("./base-tti-provider"), exports);
|
|
19
|
+
// Provider implementations
|
|
20
|
+
__exportStar(require("./google-cloud-provider"), exports);
|
|
21
|
+
__exportStar(require("./edenai-provider"), exports);
|
|
22
|
+
__exportStar(require("./ionos-provider"), exports);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IONOS TTI Provider
|
|
3
|
+
*
|
|
4
|
+
* IONOS Cloud AI service with OpenAI-compatible API.
|
|
5
|
+
*
|
|
6
|
+
* @see https://cloud.ionos.de/
|
|
7
|
+
*/
|
|
8
|
+
import { TTIRequest, TTIResponse, ModelInfo } from '../../../types';
|
|
9
|
+
import { BaseTTIProvider } from './base-tti-provider';
|
|
10
|
+
interface IonosConfig {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class IonosProvider extends BaseTTIProvider {
|
|
15
|
+
private config;
|
|
16
|
+
private readonly apiUrl;
|
|
17
|
+
constructor(config?: Partial<IonosConfig>);
|
|
18
|
+
getDisplayName(): string;
|
|
19
|
+
listModels(): ModelInfo[];
|
|
20
|
+
getDefaultModel(): string;
|
|
21
|
+
generate(request: TTIRequest): Promise<TTIResponse>;
|
|
22
|
+
private executeGeneration;
|
|
23
|
+
private aspectRatioToSize;
|
|
24
|
+
private processResponse;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* IONOS TTI Provider
|
|
4
|
+
*
|
|
5
|
+
* IONOS Cloud AI service with OpenAI-compatible API.
|
|
6
|
+
*
|
|
7
|
+
* @see https://cloud.ionos.de/
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.IonosProvider = void 0;
|
|
11
|
+
const types_1 = require("../../../types");
|
|
12
|
+
const base_tti_provider_1 = require("./base-tti-provider");
|
|
13
|
+
// ============================================================
|
|
14
|
+
// MODEL DEFINITIONS
|
|
15
|
+
// ============================================================
|
|
16
|
+
const IONOS_MODELS = [
|
|
17
|
+
{
|
|
18
|
+
id: 'default',
|
|
19
|
+
displayName: 'IONOS Image Generation',
|
|
20
|
+
capabilities: {
|
|
21
|
+
textToImage: true,
|
|
22
|
+
characterConsistency: false,
|
|
23
|
+
imageEditing: false,
|
|
24
|
+
maxImagesPerRequest: 4,
|
|
25
|
+
},
|
|
26
|
+
pricingUrl: 'https://cloud.ionos.de/preise',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
// ============================================================
|
|
30
|
+
// PROVIDER IMPLEMENTATION
|
|
31
|
+
// ============================================================
|
|
32
|
+
class IonosProvider extends base_tti_provider_1.BaseTTIProvider {
|
|
33
|
+
constructor(config) {
|
|
34
|
+
super(types_1.TTIProvider.IONOS);
|
|
35
|
+
this.config = {
|
|
36
|
+
apiKey: config?.apiKey || process.env.IONOS_API_KEY || '',
|
|
37
|
+
apiUrl: config?.apiUrl || process.env.IONOS_API_URL,
|
|
38
|
+
};
|
|
39
|
+
const baseUrl = this.config.apiUrl || 'https://api.ionos.cloud/ai/v1';
|
|
40
|
+
this.apiUrl = `${baseUrl.replace(/\/+$/, '')}/images/generations`;
|
|
41
|
+
if (!this.config.apiKey) {
|
|
42
|
+
throw new base_tti_provider_1.InvalidConfigError(this.providerName, 'IONOS API key is required (IONOS_API_KEY)');
|
|
43
|
+
}
|
|
44
|
+
this.log('info', 'IONOS Provider initialized');
|
|
45
|
+
}
|
|
46
|
+
// ============================================================
|
|
47
|
+
// ITTIProvider IMPLEMENTATION
|
|
48
|
+
// ============================================================
|
|
49
|
+
getDisplayName() {
|
|
50
|
+
return 'IONOS Cloud';
|
|
51
|
+
}
|
|
52
|
+
listModels() {
|
|
53
|
+
return IONOS_MODELS;
|
|
54
|
+
}
|
|
55
|
+
getDefaultModel() {
|
|
56
|
+
return 'default';
|
|
57
|
+
}
|
|
58
|
+
async generate(request) {
|
|
59
|
+
this.validateRequest(request);
|
|
60
|
+
return this.executeWithRetry(request, () => this.executeGeneration(request), 'IONOS API call');
|
|
61
|
+
}
|
|
62
|
+
async executeGeneration(request) {
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
const body = {
|
|
65
|
+
prompt: request.prompt,
|
|
66
|
+
n: request.n || 1,
|
|
67
|
+
size: request.aspectRatio ? this.aspectRatioToSize(request.aspectRatio) : '1024x1024',
|
|
68
|
+
response_format: 'url',
|
|
69
|
+
model: request.model !== 'default' ? request.model : undefined,
|
|
70
|
+
};
|
|
71
|
+
this.log('debug', 'Generating image with IONOS', {
|
|
72
|
+
size: body.size,
|
|
73
|
+
model: body.model,
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(this.apiUrl, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const errorText = await response.text();
|
|
86
|
+
throw new Error(`IONOS API error (${response.status}): ${errorText}`);
|
|
87
|
+
}
|
|
88
|
+
const data = (await response.json());
|
|
89
|
+
const duration = Date.now() - startTime;
|
|
90
|
+
return this.processResponse(data, duration);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw this.handleError(error, 'during IONOS API call');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ============================================================
|
|
97
|
+
// PRIVATE METHODS
|
|
98
|
+
// ============================================================
|
|
99
|
+
aspectRatioToSize(aspectRatio) {
|
|
100
|
+
const mapping = {
|
|
101
|
+
'1:1': '1024x1024',
|
|
102
|
+
'16:9': '1792x1024',
|
|
103
|
+
'9:16': '1024x1792',
|
|
104
|
+
'4:3': '1024x768',
|
|
105
|
+
'3:4': '768x1024',
|
|
106
|
+
};
|
|
107
|
+
return mapping[aspectRatio] || '1024x1024';
|
|
108
|
+
}
|
|
109
|
+
processResponse(data, duration) {
|
|
110
|
+
if (!data.data || data.data.length === 0) {
|
|
111
|
+
throw new base_tti_provider_1.GenerationFailedError(this.providerName, 'No images returned in response');
|
|
112
|
+
}
|
|
113
|
+
const images = data.data.map((item) => {
|
|
114
|
+
if (item.b64_json) {
|
|
115
|
+
return { base64: item.b64_json, contentType: 'image/png' };
|
|
116
|
+
}
|
|
117
|
+
return { url: item.url };
|
|
118
|
+
});
|
|
119
|
+
const usage = {
|
|
120
|
+
imagesGenerated: images.length,
|
|
121
|
+
modelId: 'default',
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
images,
|
|
125
|
+
metadata: {
|
|
126
|
+
provider: this.providerName,
|
|
127
|
+
model: 'default',
|
|
128
|
+
duration,
|
|
129
|
+
},
|
|
130
|
+
usage,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.IonosProvider = IonosProvider;
|