Adnan commited on
Commit
9d4b06f
·
verified ·
1 Parent(s): a08f945

Create api_providers.py

Browse files
Files changed (1) hide show
  1. api_providers.py +872 -0
api_providers.py ADDED
@@ -0,0 +1,872 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TimeLapseForge — Universal API Provider Layer
3
+ Supports 15+ image generation APIs with a unified interface.
4
+ Users provide their own API keys.
5
+ """
6
+
7
+ import os
8
+ import io
9
+ import time
10
+ import base64
11
+ import tempfile
12
+ import requests
13
+ import httpx
14
+ from PIL import Image
15
+ from typing import Optional, Dict, List, Any, Tuple
16
+ from abc import ABC, abstractmethod
17
+
18
+
19
+ # ═══════════════════════════════════════════
20
+ # BASE PROVIDER CLASS
21
+ # ═══════════════════════════════════════════
22
+
23
+ class BaseProvider(ABC):
24
+ """Abstract base class for all API providers."""
25
+
26
+ name: str = "base"
27
+ display_name: str = "Base Provider"
28
+ website: str = ""
29
+ supports_img2img: bool = False
30
+ supports_negative_prompt: bool = True
31
+ default_model: str = ""
32
+ available_models: List[str] = []
33
+ requires_api_key: bool = True
34
+
35
+ def __init__(self, api_key: str = ""):
36
+ self.api_key = api_key.strip()
37
+
38
+ @abstractmethod
39
+ def generate_image(
40
+ self,
41
+ prompt: str,
42
+ negative_prompt: str = "",
43
+ width: int = 1024,
44
+ height: int = 1024,
45
+ seed: Optional[int] = None,
46
+ model: Optional[str] = None,
47
+ **kwargs,
48
+ ) -> Image.Image:
49
+ """Generate an image from a text prompt."""
50
+ pass
51
+
52
+ def img2img(
53
+ self,
54
+ prompt: str,
55
+ image: Image.Image,
56
+ strength: float = 0.4,
57
+ negative_prompt: str = "",
58
+ seed: Optional[int] = None,
59
+ model: Optional[str] = None,
60
+ **kwargs,
61
+ ) -> Image.Image:
62
+ """Generate an image from another image + prompt. Falls back to txt2img."""
63
+ return self.generate_image(
64
+ prompt=prompt,
65
+ negative_prompt=negative_prompt,
66
+ width=image.width,
67
+ height=image.height,
68
+ seed=seed,
69
+ model=model,
70
+ **kwargs,
71
+ )
72
+
73
+ def validate_key(self) -> Tuple[bool, str]:
74
+ """Check if the API key is valid."""
75
+ if not self.api_key:
76
+ return False, "No API key provided."
77
+ return True, "Key format OK (not verified with server)."
78
+
79
+ @staticmethod
80
+ def _image_to_base64(img: Image.Image, format: str = "PNG") -> str:
81
+ """Convert PIL Image to base64 string."""
82
+ buffer = io.BytesIO()
83
+ img.save(buffer, format=format)
84
+ return base64.b64encode(buffer.getvalue()).decode("utf-8")
85
+
86
+ @staticmethod
87
+ def _base64_to_image(b64: str) -> Image.Image:
88
+ """Convert base64 string to PIL Image."""
89
+ data = base64.b64decode(b64)
90
+ return Image.open(io.BytesIO(data)).convert("RGB")
91
+
92
+ @staticmethod
93
+ def _url_to_image(url: str) -> Image.Image:
94
+ """Download image from URL and return as PIL Image."""
95
+ resp = requests.get(url, timeout=120)
96
+ resp.raise_for_status()
97
+ return Image.open(io.BytesIO(resp.content)).convert("RGB")
98
+
99
+ @staticmethod
100
+ def _bytes_to_image(data: bytes) -> Image.Image:
101
+ """Convert raw bytes to PIL Image."""
102
+ return Image.open(io.BytesIO(data)).convert("RGB")
103
+
104
+
105
+ # ═══════════════════════════════════════════
106
+ # 1. OPENAI (DALL-E 3 / DALL-E 2 / gpt-image-1)
107
+ # ═══════════════════════════════════════════
108
+
109
+ class OpenAIProvider(BaseProvider):
110
+ name = "openai"
111
+ display_name = "OpenAI (DALL·E 3 / gpt-image-1)"
112
+ website = "https://platform.openai.com/api-keys"
113
+ supports_img2img = False
114
+ supports_negative_prompt = False
115
+ default_model = "dall-e-3"
116
+ available_models = ["dall-e-3", "dall-e-2", "gpt-image-1"]
117
+
118
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
119
+ seed=None, model=None, **kwargs):
120
+ from openai import OpenAI
121
+ client = OpenAI(api_key=self.api_key)
122
+ model = model or self.default_model
123
+
124
+ size_map = {
125
+ (1024, 1024): "1024x1024",
126
+ (1792, 1024): "1792x1024",
127
+ (1024, 1792): "1024x1792",
128
+ (512, 512): "512x512",
129
+ (256, 256): "256x256",
130
+ }
131
+ size = size_map.get((width, height), "1024x1024")
132
+
133
+ if model == "gpt-image-1":
134
+ response = client.images.generate(
135
+ model="gpt-image-1",
136
+ prompt=prompt,
137
+ n=1,
138
+ size=size,
139
+ )
140
+ # gpt-image-1 returns b64_json by default
141
+ if hasattr(response.data[0], 'b64_json') and response.data[0].b64_json:
142
+ return self._base64_to_image(response.data[0].b64_json)
143
+ else:
144
+ return self._url_to_image(response.data[0].url)
145
+ else:
146
+ kwargs_api = dict(
147
+ model=model,
148
+ prompt=prompt,
149
+ n=1,
150
+ size=size,
151
+ response_format="b64_json",
152
+ )
153
+ if model == "dall-e-3":
154
+ kwargs_api["quality"] = kwargs.get("quality", "hd")
155
+ kwargs_api["style"] = kwargs.get("style", "natural")
156
+
157
+ response = client.images.generate(**kwargs_api)
158
+ return self._base64_to_image(response.data[0].b64_json)
159
+
160
+
161
+ # ═══════════════════════════════════════════
162
+ # 2. STABILITY AI (Stable Diffusion 3 / SDXL)
163
+ # ═══════════════════════════════════════════
164
+
165
+ class StabilityProvider(BaseProvider):
166
+ name = "stability"
167
+ display_name = "Stability AI (SD3 / SDXL)"
168
+ website = "https://platform.stability.ai/account/keys"
169
+ supports_img2img = True
170
+ supports_negative_prompt = True
171
+ default_model = "sd3.5-large"
172
+ available_models = [
173
+ "sd3.5-large", "sd3.5-large-turbo", "sd3.5-medium",
174
+ "sd3-large", "sd3-large-turbo", "sd3-medium",
175
+ "stable-image-core", "stable-image-ultra",
176
+ ]
177
+
178
+ API_BASE = "https://api.stability.ai/v2beta"
179
+
180
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
181
+ seed=None, model=None, **kwargs):
182
+ model = model or self.default_model
183
+ headers = {
184
+ "Authorization": f"Bearer {self.api_key}",
185
+ "Accept": "image/*",
186
+ }
187
+
188
+ data = {
189
+ "prompt": prompt,
190
+ "output_format": "png",
191
+ "width": width,
192
+ "height": height,
193
+ }
194
+ if negative_prompt:
195
+ data["negative_prompt"] = negative_prompt
196
+ if seed is not None:
197
+ data["seed"] = seed
198
+
199
+ if "stable-image" in model:
200
+ url = f"{self.API_BASE}/stable-image/generate/{model.replace('stable-image-', '')}"
201
+ else:
202
+ url = f"{self.API_BASE}/stable-image/generate/sd3"
203
+ data["model"] = model
204
+
205
+ resp = requests.post(url, headers=headers, files={"none": ""},
206
+ data=data, timeout=120)
207
+ resp.raise_for_status()
208
+ return self._bytes_to_image(resp.content)
209
+
210
+ def img2img(self, prompt, image, strength=0.4, negative_prompt="",
211
+ seed=None, model=None, **kwargs):
212
+ headers = {
213
+ "Authorization": f"Bearer {self.api_key}",
214
+ "Accept": "image/*",
215
+ }
216
+
217
+ buf = io.BytesIO()
218
+ image.save(buf, format="PNG")
219
+ buf.seek(0)
220
+
221
+ data = {
222
+ "prompt": prompt,
223
+ "strength": strength,
224
+ "output_format": "png",
225
+ "mode": "image-to-image",
226
+ }
227
+ if negative_prompt:
228
+ data["negative_prompt"] = negative_prompt
229
+ if seed is not None:
230
+ data["seed"] = seed
231
+
232
+ files = {"image": ("input.png", buf, "image/png")}
233
+ url = f"{self.API_BASE}/stable-image/generate/sd3"
234
+
235
+ resp = requests.post(url, headers=headers, files=files,
236
+ data=data, timeout=120)
237
+ resp.raise_for_status()
238
+ return self._bytes_to_image(resp.content)
239
+
240
+
241
+ # ═══════════════════════════════════════════
242
+ # 3. REPLICATE (Flux, SDXL, any model)
243
+ # ═══════════════════════════════════════════
244
+
245
+ class ReplicateProvider(BaseProvider):
246
+ name = "replicate"
247
+ display_name = "Replicate (Flux / SDXL / Any)"
248
+ website = "https://replicate.com/account/api-tokens"
249
+ supports_img2img = True
250
+ supports_negative_prompt = True
251
+ default_model = "black-forest-labs/flux-1.1-pro"
252
+ available_models = [
253
+ "black-forest-labs/flux-1.1-pro",
254
+ "black-forest-labs/flux-schnell",
255
+ "black-forest-labs/flux-dev",
256
+ "stability-ai/sdxl:latest",
257
+ "stability-ai/stable-diffusion-3.5-large",
258
+ "bytedance/sdxl-lightning-4step:latest",
259
+ "lucataco/realvisxl-v4.0:latest",
260
+ "adirik/realvisxl-v4.0-lightning:latest",
261
+ ]
262
+
263
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
264
+ seed=None, model=None, **kwargs):
265
+ import replicate
266
+ client = replicate.Client(api_token=self.api_key)
267
+ model_id = model or self.default_model
268
+
269
+ input_params = {
270
+ "prompt": prompt,
271
+ "width": width,
272
+ "height": height,
273
+ }
274
+ if negative_prompt:
275
+ input_params["negative_prompt"] = negative_prompt
276
+ if seed is not None:
277
+ input_params["seed"] = seed
278
+
279
+ # Flux models use different param names
280
+ if "flux" in model_id.lower():
281
+ input_params.pop("negative_prompt", None)
282
+
283
+ output = client.run(model_id, input=input_params)
284
+
285
+ # Handle different output formats
286
+ if isinstance(output, list):
287
+ url = str(output[0])
288
+ elif hasattr(output, 'url'):
289
+ url = output.url
290
+ else:
291
+ url = str(output)
292
+
293
+ return self._url_to_image(url)
294
+
295
+ def img2img(self, prompt, image, strength=0.4, negative_prompt="",
296
+ seed=None, model=None, **kwargs):
297
+ import replicate
298
+ client = replicate.Client(api_token=self.api_key)
299
+ model_id = model or "stability-ai/sdxl:latest"
300
+
301
+ buf = io.BytesIO()
302
+ image.save(buf, format="PNG")
303
+ buf.seek(0)
304
+
305
+ input_params = {
306
+ "prompt": prompt,
307
+ "image": buf,
308
+ "prompt_strength": strength,
309
+ }
310
+ if negative_prompt:
311
+ input_params["negative_prompt"] = negative_prompt
312
+ if seed is not None:
313
+ input_params["seed"] = seed
314
+
315
+ output = client.run(model_id, input=input_params)
316
+ if isinstance(output, list):
317
+ url = str(output[0])
318
+ else:
319
+ url = str(output)
320
+
321
+ return self._url_to_image(url)
322
+
323
+
324
+ # ═══════════════════════════════════════════
325
+ # 4. TOGETHER AI (Flux, SDXL, Fast)
326
+ # ═══════════════════════════════════════════
327
+
328
+ class TogetherProvider(BaseProvider):
329
+ name = "together"
330
+ display_name = "Together AI (Flux / SDXL / Fast)"
331
+ website = "https://api.together.xyz/settings/api-keys"
332
+ supports_img2img = False
333
+ supports_negative_prompt = True
334
+ default_model = "black-forest-labs/FLUX.1.1-pro"
335
+ available_models = [
336
+ "black-forest-labs/FLUX.1.1-pro",
337
+ "black-forest-labs/FLUX.1-schnell-Free",
338
+ "black-forest-labs/FLUX.1-dev",
339
+ "stabilityai/stable-diffusion-xl-base-1.0",
340
+ ]
341
+
342
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
343
+ seed=None, model=None, **kwargs):
344
+ from together import Together
345
+ client = Together(api_key=self.api_key)
346
+ model_id = model or self.default_model
347
+
348
+ params = dict(
349
+ model=model_id,
350
+ prompt=prompt,
351
+ width=width,
352
+ height=height,
353
+ steps=kwargs.get("steps", 28),
354
+ n=1,
355
+ response_format="b64_json",
356
+ )
357
+ if negative_prompt:
358
+ params["negative_prompt"] = negative_prompt
359
+ if seed is not None:
360
+ params["seed"] = seed
361
+
362
+ response = client.images.generate(**params)
363
+ return self._base64_to_image(response.data[0].b64_json)
364
+
365
+
366
+ # ═══════════════════════════════════════════
367
+ # 5. FAL.AI (Flux Pro, Fast SDXL, etc.)
368
+ # ═══════════════════════════════════════════
369
+
370
+ class FalProvider(BaseProvider):
371
+ name = "fal"
372
+ display_name = "Fal.ai (Flux Pro / Fast SDXL)"
373
+ website = "https://fal.ai/dashboard/keys"
374
+ supports_img2img = True
375
+ supports_negative_prompt = True
376
+ default_model = "fal-ai/flux-pro/v1.1"
377
+ available_models = [
378
+ "fal-ai/flux-pro/v1.1",
379
+ "fal-ai/flux/dev",
380
+ "fal-ai/flux/schnell",
381
+ "fal-ai/flux-realism",
382
+ "fal-ai/fast-sdxl",
383
+ "fal-ai/stable-diffusion-v35-large",
384
+ "fal-ai/recraft-v3",
385
+ "fal-ai/ideogram/v2",
386
+ "fal-ai/aura-flow",
387
+ ]
388
+
389
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
390
+ seed=None, model=None, **kwargs):
391
+ import fal_client
392
+ os.environ["FAL_KEY"] = self.api_key
393
+ model_id = model or self.default_model
394
+
395
+ arguments = {
396
+ "prompt": prompt,
397
+ "image_size": {"width": width, "height": height},
398
+ "num_images": 1,
399
+ }
400
+ if negative_prompt:
401
+ arguments["negative_prompt"] = negative_prompt
402
+ if seed is not None:
403
+ arguments["seed"] = seed
404
+
405
+ result = fal_client.subscribe(model_id, arguments=arguments)
406
+ images = result.get("images", [])
407
+ if images:
408
+ return self._url_to_image(images[0]["url"])
409
+ raise ValueError("No image returned from Fal.ai")
410
+
411
+ def img2img(self, prompt, image, strength=0.4, negative_prompt="",
412
+ seed=None, model=None, **kwargs):
413
+ import fal_client
414
+ os.environ["FAL_KEY"] = self.api_key
415
+
416
+ b64 = self._image_to_base64(image)
417
+ data_uri = f"data:image/png;base64,{b64}"
418
+
419
+ arguments = {
420
+ "prompt": prompt,
421
+ "image_url": data_uri,
422
+ "strength": strength,
423
+ "num_images": 1,
424
+ }
425
+ if negative_prompt:
426
+ arguments["negative_prompt"] = negative_prompt
427
+ if seed is not None:
428
+ arguments["seed"] = seed
429
+
430
+ model_id = model or "fal-ai/flux/dev/image-to-image"
431
+ result = fal_client.subscribe(model_id, arguments=arguments)
432
+ images = result.get("images", [])
433
+ if images:
434
+ return self._url_to_image(images[0]["url"])
435
+ raise ValueError("No image returned from Fal.ai img2img")
436
+
437
+
438
+ # ═══════════════════════════════════════════
439
+ # 6. GOOGLE GEMINI (Imagen 3)
440
+ # ═══════════════════════════════════════════
441
+
442
+ class GoogleGeminiProvider(BaseProvider):
443
+ name = "google"
444
+ display_name = "Google Gemini (Imagen 3)"
445
+ website = "https://aistudio.google.com/apikey"
446
+ supports_img2img = False
447
+ supports_negative_prompt = True
448
+ default_model = "imagen-3.0-generate-002"
449
+ available_models = [
450
+ "imagen-3.0-generate-002",
451
+ "imagen-3.0-fast-generate-001",
452
+ "gemini-2.0-flash-exp",
453
+ ]
454
+
455
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
456
+ seed=None, model=None, **kwargs):
457
+ import google.generativeai as genai
458
+ genai.configure(api_key=self.api_key)
459
+ model_id = model or self.default_model
460
+
461
+ if "gemini" in model_id:
462
+ # Use Gemini native image gen
463
+ gen_model = genai.GenerativeModel(model_id)
464
+ response = gen_model.generate_content(
465
+ f"Generate an image: {prompt}",
466
+ generation_config=genai.GenerationConfig(
467
+ response_modalities=["image", "text"]
468
+ ),
469
+ )
470
+ for part in response.candidates[0].content.parts:
471
+ if hasattr(part, 'inline_data') and part.inline_data:
472
+ return self._bytes_to_image(part.inline_data.data)
473
+ raise ValueError("No image in Gemini response")
474
+ else:
475
+ # Use Imagen
476
+ imagen = genai.ImageGenerationModel(model_id)
477
+ params = dict(
478
+ prompt=prompt,
479
+ number_of_images=1,
480
+ )
481
+ if negative_prompt:
482
+ params["negative_prompt"] = negative_prompt
483
+
484
+ response = imagen.generate_images(**params)
485
+ if response.images:
486
+ return response.images[0]._pil_image.convert("RGB")
487
+ raise ValueError("No image returned from Imagen")
488
+
489
+
490
+ # ═══════════════════════════════════════════
491
+ # 7. HUGGING FACE INFERENCE API
492
+ # ═══════════════════════════════════════════
493
+
494
+ class HuggingFaceProvider(BaseProvider):
495
+ name = "huggingface"
496
+ display_name = "HuggingFace Inference API"
497
+ website = "https://huggingface.co/settings/tokens"
498
+ supports_img2img = False
499
+ supports_negative_prompt = True
500
+ default_model = "black-forest-labs/FLUX.1-schnell"
501
+ available_models = [
502
+ "black-forest-labs/FLUX.1-schnell",
503
+ "black-forest-labs/FLUX.1-dev",
504
+ "stabilityai/stable-diffusion-xl-base-1.0",
505
+ "stabilityai/stable-diffusion-3.5-large",
506
+ "runwayml/stable-diffusion-v1-5",
507
+ "prompthero/openjourney-v4",
508
+ "SG161222/RealVisXL_V4.0",
509
+ ]
510
+
511
+ API_BASE = "https://api-inference.huggingface.co/models"
512
+
513
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
514
+ seed=None, model=None, **kwargs):
515
+ model_id = model or self.default_model
516
+ url = f"{self.API_BASE}/{model_id}"
517
+ headers = {"Authorization": f"Bearer {self.api_key}"}
518
+
519
+ payload = {
520
+ "inputs": prompt,
521
+ "parameters": {
522
+ "width": width,
523
+ "height": height,
524
+ },
525
+ }
526
+ if negative_prompt:
527
+ payload["parameters"]["negative_prompt"] = negative_prompt
528
+ if seed is not None:
529
+ payload["parameters"]["seed"] = seed
530
+
531
+ resp = requests.post(url, headers=headers, json=payload, timeout=180)
532
+
533
+ if resp.status_code == 503:
534
+ # Model loading
535
+ time.sleep(20)
536
+ resp = requests.post(url, headers=headers, json=payload, timeout=180)
537
+
538
+ resp.raise_for_status()
539
+ return self._bytes_to_image(resp.content)
540
+
541
+
542
+ # ═══════════════════════════════════════════
543
+ # 8. xAI GROK (Aurora Image Gen)
544
+ # ═══════════════════════════════════════════
545
+
546
+ class XAIProvider(BaseProvider):
547
+ name = "xai"
548
+ display_name = "xAI Grok (Aurora)"
549
+ website = "https://console.x.ai/team/default/api-keys"
550
+ supports_img2img = False
551
+ supports_negative_prompt = False
552
+ default_model = "grok-2-image"
553
+ available_models = ["grok-2-image"]
554
+
555
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
556
+ seed=None, model=None, **kwargs):
557
+ from openai import OpenAI
558
+ client = OpenAI(api_key=self.api_key, base_url="https://api.x.ai/v1")
559
+
560
+ response = client.images.generate(
561
+ model=model or self.default_model,
562
+ prompt=prompt,
563
+ n=1,
564
+ response_format="b64_json",
565
+ size="1024x1024",
566
+ )
567
+ return self._base64_to_image(response.data[0].b64_json)
568
+
569
+
570
+ # ═══════════════════════════════════════════
571
+ # 9. FIREWORKS AI
572
+ # ═══════════════════════════════════════════
573
+
574
+ class FireworksProvider(BaseProvider):
575
+ name = "fireworks"
576
+ display_name = "Fireworks AI (Flux / SD)"
577
+ website = "https://fireworks.ai/account/api-keys"
578
+ supports_img2img = False
579
+ supports_negative_prompt = True
580
+ default_model = "accounts/fireworks/models/flux-1-1-pro"
581
+ available_models = [
582
+ "accounts/fireworks/models/flux-1-1-pro",
583
+ "accounts/fireworks/models/flux-1-schnell-fp8",
584
+ "accounts/fireworks/models/flux-1-dev-fp8",
585
+ "accounts/fireworks/models/stable-diffusion-xl-1024-v1-0",
586
+ "accounts/fireworks/models/playground-v2-5-1024px-aesthetic",
587
+ ]
588
+
589
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
590
+ seed=None, model=None, **kwargs):
591
+ url = "https://api.fireworks.ai/inference/v1/images/generations"
592
+ headers = {
593
+ "Authorization": f"Bearer {self.api_key}",
594
+ "Content-Type": "application/json",
595
+ }
596
+ payload = {
597
+ "model": model or self.default_model,
598
+ "prompt": prompt,
599
+ "n": 1,
600
+ "size": f"{width}x{height}",
601
+ "response_format": "b64_json",
602
+ }
603
+ if negative_prompt:
604
+ payload["negative_prompt"] = negative_prompt
605
+ if seed is not None:
606
+ payload["seed"] = seed
607
+
608
+ resp = requests.post(url, headers=headers, json=payload, timeout=120)
609
+ resp.raise_for_status()
610
+ data = resp.json()
611
+ return self._base64_to_image(data["data"][0]["b64_json"])
612
+
613
+
614
+ # ═══════════════════════════════════════════
615
+ # 10. IDEOGRAM
616
+ # ═══════════════════════════════════════════
617
+
618
+ class IdeogramProvider(BaseProvider):
619
+ name = "ideogram"
620
+ display_name = "Ideogram (v2 / v2-turbo)"
621
+ website = "https://ideogram.ai/manage-api"
622
+ supports_img2img = False
623
+ supports_negative_prompt = True
624
+ default_model = "V_2"
625
+ available_models = ["V_2", "V_2_TURBO", "V_1", "V_1_TURBO"]
626
+
627
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
628
+ seed=None, model=None, **kwargs):
629
+ url = "https://api.ideogram.ai/generate"
630
+ headers = {
631
+ "Api-Key": self.api_key,
632
+ "Content-Type": "application/json",
633
+ }
634
+ payload = {
635
+ "image_request": {
636
+ "prompt": prompt,
637
+ "model": model or self.default_model,
638
+ "magic_prompt_option": "AUTO",
639
+ "aspect_ratio": "ASPECT_1_1",
640
+ }
641
+ }
642
+ if negative_prompt:
643
+ payload["image_request"]["negative_prompt"] = negative_prompt
644
+ if seed is not None:
645
+ payload["image_request"]["seed"] = seed
646
+
647
+ resp = requests.post(url, headers=headers, json=payload, timeout=120)
648
+ resp.raise_for_status()
649
+ data = resp.json()
650
+ img_url = data["data"][0]["url"]
651
+ return self._url_to_image(img_url)
652
+
653
+
654
+ # ═══════════════════════════════════════════
655
+ # 11. LEONARDO AI
656
+ # ═══════════════════════════════════════════
657
+
658
+ class LeonardoProvider(BaseProvider):
659
+ name = "leonardo"
660
+ display_name = "Leonardo AI"
661
+ website = "https://app.leonardo.ai/api-access"
662
+ supports_img2img = False
663
+ supports_negative_prompt = True
664
+ default_model = "6b645e3a-d64f-4341-a6d8-7a3690fbf042" # Leonardo Phoenix
665
+ available_models = [
666
+ "6b645e3a-d64f-4341-a6d8-7a3690fbf042", # Phoenix
667
+ "aa77f04e-3eec-4034-9c07-d0f619684628", # Kino XL
668
+ "1e60896f-3c26-4296-8ecc-53e2afecc132", # DreamShaper
669
+ ]
670
+
671
+ API_BASE = "https://cloud.leonardo.ai/api/rest/v1"
672
+
673
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
674
+ seed=None, model=None, **kwargs):
675
+ headers = {
676
+ "Authorization": f"Bearer {self.api_key}",
677
+ "Content-Type": "application/json",
678
+ }
679
+
680
+ payload = {
681
+ "prompt": prompt,
682
+ "modelId": model or self.default_model,
683
+ "width": width,
684
+ "height": height,
685
+ "num_images": 1,
686
+ }
687
+ if negative_prompt:
688
+ payload["negative_prompt"] = negative_prompt
689
+ if seed is not None:
690
+ payload["seed"] = seed
691
+
692
+ # Start generation
693
+ resp = requests.post(
694
+ f"{self.API_BASE}/generations",
695
+ headers=headers, json=payload, timeout=60,
696
+ )
697
+ resp.raise_for_status()
698
+ gen_id = resp.json()["sdGenerationJob"]["generationId"]
699
+
700
+ # Poll for result
701
+ for _ in range(30):
702
+ time.sleep(5)
703
+ poll = requests.get(
704
+ f"{self.API_BASE}/generations/{gen_id}",
705
+ headers=headers, timeout=30,
706
+ )
707
+ poll.raise_for_status()
708
+ data = poll.json()
709
+ gen = data.get("generations_by_pk", {})
710
+ if gen.get("status") == "COMPLETE":
711
+ images = gen.get("generated_images", [])
712
+ if images:
713
+ return self._url_to_image(images[0]["url"])
714
+
715
+ raise TimeoutError("Leonardo generation timed out")
716
+
717
+
718
+ # ═══════════════════════════════════════════
719
+ # 12. CUSTOM OPENAI-COMPATIBLE API
720
+ # ═══════════════════════════════════════════
721
+
722
+ class CustomOpenAIProvider(BaseProvider):
723
+ name = "custom_openai"
724
+ display_name = "Custom OpenAI-Compatible API"
725
+ website = ""
726
+ supports_img2img = False
727
+ supports_negative_prompt = False
728
+ default_model = "dall-e-3"
729
+ available_models = ["dall-e-3", "dall-e-2", "custom"]
730
+
731
+ def __init__(self, api_key: str = "", base_url: str = ""):
732
+ super().__init__(api_key)
733
+ self.base_url = base_url.strip().rstrip("/") if base_url else ""
734
+
735
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
736
+ seed=None, model=None, **kwargs):
737
+ from openai import OpenAI
738
+ client_kwargs = {"api_key": self.api_key}
739
+ if self.base_url:
740
+ client_kwargs["base_url"] = self.base_url
741
+ client = OpenAI(**client_kwargs)
742
+
743
+ response = client.images.generate(
744
+ model=model or self.default_model,
745
+ prompt=prompt,
746
+ n=1,
747
+ size=f"{width}x{height}",
748
+ response_format="b64_json",
749
+ )
750
+ return self._base64_to_image(response.data[0].b64_json)
751
+
752
+
753
+ # ═══════════════════════════════════════════
754
+ # 13. DIRECT URL API (Generic REST endpoint)
755
+ # ═══════════════════════════════════════════
756
+
757
+ class DirectURLProvider(BaseProvider):
758
+ name = "direct_url"
759
+ display_name = "Direct URL API (Any REST Endpoint)"
760
+ website = ""
761
+ supports_img2img = False
762
+ supports_negative_prompt = True
763
+ default_model = "custom"
764
+ available_models = ["custom"]
765
+
766
+ def __init__(self, api_key: str = "", endpoint_url: str = ""):
767
+ super().__init__(api_key)
768
+ self.endpoint_url = endpoint_url.strip()
769
+
770
+ def generate_image(self, prompt, negative_prompt="", width=1024, height=1024,
771
+ seed=None, model=None, **kwargs):
772
+ if not self.endpoint_url:
773
+ raise ValueError("No endpoint URL provided")
774
+
775
+ headers = {
776
+ "Authorization": f"Bearer {self.api_key}",
777
+ "Content-Type": "application/json",
778
+ }
779
+ payload = {
780
+ "prompt": prompt,
781
+ "width": width,
782
+ "height": height,
783
+ }
784
+ if negative_prompt:
785
+ payload["negative_prompt"] = negative_prompt
786
+ if seed is not None:
787
+ payload["seed"] = seed
788
+ if model:
789
+ payload["model"] = model
790
+
791
+ resp = requests.post(self.endpoint_url, headers=headers, json=payload, timeout=180)
792
+ resp.raise_for_status()
793
+
794
+ content_type = resp.headers.get("Content-Type", "")
795
+ if "image" in content_type:
796
+ return self._bytes_to_image(resp.content)
797
+ else:
798
+ data = resp.json()
799
+ # Try common response formats
800
+ if "images" in data:
801
+ img_data = data["images"][0]
802
+ if isinstance(img_data, str) and img_data.startswith("http"):
803
+ return self._url_to_image(img_data)
804
+ return self._base64_to_image(img_data)
805
+ elif "data" in data:
806
+ item = data["data"][0]
807
+ if isinstance(item, dict):
808
+ if "b64_json" in item:
809
+ return self._base64_to_image(item["b64_json"])
810
+ if "url" in item:
811
+ return self._url_to_image(item["url"])
812
+ return self._base64_to_image(str(item))
813
+ elif "output" in data:
814
+ return self._url_to_image(data["output"][0])
815
+
816
+ raise ValueError("Could not parse image from API response")
817
+
818
+
819
+ # ═══════════════════════════════════════════
820
+ # PROVIDER REGISTRY
821
+ # ═══════════════════════════════════════════
822
+
823
+ PROVIDERS = {
824
+ "openai": OpenAIProvider,
825
+ "stability": StabilityProvider,
826
+ "replicate": ReplicateProvider,
827
+ "together": TogetherProvider,
828
+ "fal": FalProvider,
829
+ "google": GoogleGeminiProvider,
830
+ "huggingface": HuggingFaceProvider,
831
+ "xai": XAIProvider,
832
+ "fireworks": FireworksProvider,
833
+ "ideogram": IdeogramProvider,
834
+ "leonardo": LeonardoProvider,
835
+ "custom_openai": CustomOpenAIProvider,
836
+ "direct_url": DirectURLProvider,
837
+ }
838
+
839
+ PROVIDER_DISPLAY_NAMES = {
840
+ cls.display_name: key for key, cls in PROVIDERS.items()
841
+ }
842
+
843
+ def get_provider(provider_name: str, api_key: str, **kwargs) -> BaseProvider:
844
+ """Get a provider instance by name."""
845
+ cls = PROVIDERS.get(provider_name)
846
+ if cls is None:
847
+ raise ValueError(f"Unknown provider: {provider_name}")
848
+ if provider_name in ("custom_openai", "direct_url"):
849
+ return cls(api_key=api_key, **kwargs)
850
+ return cls(api_key=api_key)
851
+
852
+ def get_provider_info() -> List[Dict[str, Any]]:
853
+ """Get info about all available providers."""
854
+ info = []
855
+ for key, cls in PROVIDERS.items():
856
+ info.append({
857
+ "name": key,
858
+ "display_name": cls.display_name,
859
+ "website": cls.website,
860
+ "supports_img2img": cls.supports_img2img,
861
+ "supports_negative_prompt": cls.supports_negative_prompt,
862
+ "default_model": cls.default_model,
863
+ "available_models": cls.available_models,
864
+ })
865
+ return info
866
+
867
+ def get_models_for_provider(provider_name: str) -> List[str]:
868
+ """Get available models for a provider."""
869
+ cls = PROVIDERS.get(provider_name)
870
+ if cls:
871
+ return cls.available_models
872
+ return []