webscout 8.3__py3-none-any.whl → 8.3.2__py3-none-any.whl

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.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (120) hide show
  1. webscout/AIauto.py +4 -4
  2. webscout/AIbase.py +61 -1
  3. webscout/AIutel.py +46 -53
  4. webscout/Bing_search.py +418 -0
  5. webscout/Extra/YTToolkit/ytapi/patterns.py +45 -45
  6. webscout/Extra/YTToolkit/ytapi/stream.py +1 -1
  7. webscout/Extra/YTToolkit/ytapi/video.py +10 -10
  8. webscout/Extra/autocoder/autocoder_utiles.py +1 -1
  9. webscout/Extra/gguf.py +706 -177
  10. webscout/Litlogger/formats.py +9 -0
  11. webscout/Litlogger/handlers.py +18 -0
  12. webscout/Litlogger/logger.py +43 -1
  13. webscout/Provider/AISEARCH/genspark_search.py +7 -7
  14. webscout/Provider/AISEARCH/scira_search.py +3 -2
  15. webscout/Provider/GeminiProxy.py +140 -0
  16. webscout/Provider/LambdaChat.py +7 -1
  17. webscout/Provider/MCPCore.py +78 -75
  18. webscout/Provider/OPENAI/BLACKBOXAI.py +1046 -1017
  19. webscout/Provider/OPENAI/GeminiProxy.py +328 -0
  20. webscout/Provider/OPENAI/Qwen3.py +303 -303
  21. webscout/Provider/OPENAI/README.md +5 -0
  22. webscout/Provider/OPENAI/README_AUTOPROXY.md +238 -0
  23. webscout/Provider/OPENAI/TogetherAI.py +355 -0
  24. webscout/Provider/OPENAI/__init__.py +16 -1
  25. webscout/Provider/OPENAI/autoproxy.py +332 -0
  26. webscout/Provider/OPENAI/base.py +101 -14
  27. webscout/Provider/OPENAI/chatgpt.py +15 -2
  28. webscout/Provider/OPENAI/chatgptclone.py +14 -3
  29. webscout/Provider/OPENAI/deepinfra.py +339 -328
  30. webscout/Provider/OPENAI/e2b.py +295 -74
  31. webscout/Provider/OPENAI/mcpcore.py +109 -70
  32. webscout/Provider/OPENAI/opkfc.py +18 -6
  33. webscout/Provider/OPENAI/scirachat.py +59 -50
  34. webscout/Provider/OPENAI/toolbaz.py +2 -10
  35. webscout/Provider/OPENAI/writecream.py +166 -166
  36. webscout/Provider/OPENAI/x0gpt.py +367 -367
  37. webscout/Provider/OPENAI/xenai.py +514 -0
  38. webscout/Provider/OPENAI/yep.py +389 -383
  39. webscout/Provider/STT/__init__.py +3 -0
  40. webscout/Provider/STT/base.py +281 -0
  41. webscout/Provider/STT/elevenlabs.py +265 -0
  42. webscout/Provider/TTI/__init__.py +4 -1
  43. webscout/Provider/TTI/aiarta.py +399 -365
  44. webscout/Provider/TTI/base.py +74 -2
  45. webscout/Provider/TTI/bing.py +231 -0
  46. webscout/Provider/TTI/fastflux.py +63 -30
  47. webscout/Provider/TTI/gpt1image.py +149 -0
  48. webscout/Provider/TTI/imagen.py +196 -0
  49. webscout/Provider/TTI/magicstudio.py +60 -29
  50. webscout/Provider/TTI/piclumen.py +43 -32
  51. webscout/Provider/TTI/pixelmuse.py +232 -225
  52. webscout/Provider/TTI/pollinations.py +43 -32
  53. webscout/Provider/TTI/together.py +287 -0
  54. webscout/Provider/TTI/utils.py +2 -1
  55. webscout/Provider/TTS/README.md +1 -0
  56. webscout/Provider/TTS/__init__.py +2 -1
  57. webscout/Provider/TTS/freetts.py +140 -0
  58. webscout/Provider/TTS/speechma.py +45 -39
  59. webscout/Provider/TogetherAI.py +366 -0
  60. webscout/Provider/UNFINISHED/ChutesAI.py +314 -0
  61. webscout/Provider/UNFINISHED/fetch_together_models.py +95 -0
  62. webscout/Provider/XenAI.py +324 -0
  63. webscout/Provider/__init__.py +8 -0
  64. webscout/Provider/deepseek_assistant.py +378 -0
  65. webscout/Provider/scira_chat.py +3 -2
  66. webscout/Provider/toolbaz.py +0 -1
  67. webscout/auth/__init__.py +44 -0
  68. webscout/auth/api_key_manager.py +189 -0
  69. webscout/auth/auth_system.py +100 -0
  70. webscout/auth/config.py +76 -0
  71. webscout/auth/database.py +400 -0
  72. webscout/auth/exceptions.py +67 -0
  73. webscout/auth/middleware.py +248 -0
  74. webscout/auth/models.py +130 -0
  75. webscout/auth/providers.py +257 -0
  76. webscout/auth/rate_limiter.py +254 -0
  77. webscout/auth/request_models.py +127 -0
  78. webscout/auth/request_processing.py +226 -0
  79. webscout/auth/routes.py +526 -0
  80. webscout/auth/schemas.py +103 -0
  81. webscout/auth/server.py +312 -0
  82. webscout/auth/static/favicon.svg +11 -0
  83. webscout/auth/swagger_ui.py +203 -0
  84. webscout/auth/templates/components/authentication.html +237 -0
  85. webscout/auth/templates/components/base.html +103 -0
  86. webscout/auth/templates/components/endpoints.html +750 -0
  87. webscout/auth/templates/components/examples.html +491 -0
  88. webscout/auth/templates/components/footer.html +75 -0
  89. webscout/auth/templates/components/header.html +27 -0
  90. webscout/auth/templates/components/models.html +286 -0
  91. webscout/auth/templates/components/navigation.html +70 -0
  92. webscout/auth/templates/static/api.js +455 -0
  93. webscout/auth/templates/static/icons.js +168 -0
  94. webscout/auth/templates/static/main.js +784 -0
  95. webscout/auth/templates/static/particles.js +201 -0
  96. webscout/auth/templates/static/styles.css +3353 -0
  97. webscout/auth/templates/static/ui.js +374 -0
  98. webscout/auth/templates/swagger_ui.html +170 -0
  99. webscout/client.py +49 -3
  100. webscout/litagent/Readme.md +12 -3
  101. webscout/litagent/agent.py +99 -62
  102. webscout/scout/core/scout.py +104 -26
  103. webscout/scout/element.py +139 -18
  104. webscout/swiftcli/core/cli.py +14 -3
  105. webscout/swiftcli/decorators/output.py +59 -9
  106. webscout/update_checker.py +31 -49
  107. webscout/version.py +1 -1
  108. webscout/webscout_search.py +4 -12
  109. webscout/webscout_search_async.py +3 -10
  110. webscout/yep_search.py +2 -11
  111. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/METADATA +41 -11
  112. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/RECORD +116 -68
  113. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/entry_points.txt +1 -1
  114. webscout/Provider/HF_space/__init__.py +0 -0
  115. webscout/Provider/HF_space/qwen_qwen2.py +0 -206
  116. webscout/Provider/OPENAI/api.py +0 -1035
  117. webscout/Provider/TTI/artbit.py +0 -0
  118. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/WHEEL +0 -0
  119. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/licenses/LICENSE.md +0 -0
  120. {webscout-8.3.dist-info → webscout-8.3.2.dist-info}/top_level.txt +0 -0
@@ -1,365 +1,399 @@
1
- """AIArtaImager TTI-Compatible Provider - Generate stunning AI art with AI Arta! 🎨
2
-
3
- Examples:
4
- >>> from webscout.Provider.TTI.aiarta import AIArta
5
- >>> client = AIArta()
6
- >>> response = client.images.create(
7
- ... model="flux",
8
- ... prompt="A cool cyberpunk city at night",
9
- ... n=1
10
- ... )
11
- >>> print(response)
12
- """
13
-
14
- import requests
15
- from typing import Optional, List, Dict, Any
16
- from webscout.Provider.TTI.utils import ImageData, ImageResponse
17
- from webscout.Provider.TTI.base import TTICompatibleProvider, BaseImages
18
- from io import BytesIO
19
- import os
20
- import tempfile
21
- from webscout.litagent import LitAgent
22
- import time
23
- import json
24
-
25
- try:
26
- from PIL import Image
27
- except ImportError:
28
- Image = None
29
-
30
- class Images(BaseImages):
31
- def __init__(self, client: 'AIArta'):
32
- self._client = client
33
-
34
- def create(
35
- self,
36
- *,
37
- model: str,
38
- prompt: str,
39
- n: int = 1,
40
- size: str = "1024x1024",
41
- response_format: str = "url",
42
- user: Optional[str] = None,
43
- style: str = "none",
44
- aspect_ratio: str = "1:1",
45
- timeout: int = 60,
46
- image_format: str = "png",
47
- **kwargs
48
- ) -> ImageResponse:
49
- """
50
- image_format: "png" or "jpeg"
51
- """
52
- if Image is None:
53
- raise ImportError("Pillow (PIL) is required for image format conversion.")
54
-
55
- images = []
56
- urls = []
57
- agent = LitAgent()
58
-
59
- def upload_file_with_retry(img_bytes, image_format, max_retries=3):
60
- ext = "jpg" if image_format.lower() == "jpeg" else "png"
61
- for attempt in range(max_retries):
62
- tmp_path = None
63
- try:
64
- with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as tmp:
65
- tmp.write(img_bytes)
66
- tmp.flush()
67
- tmp_path = tmp.name
68
- with open(tmp_path, 'rb') as f:
69
- files = {
70
- 'fileToUpload': (f'image.{ext}', f, f'image/{ext}')
71
- }
72
- data = {
73
- 'reqtype': 'fileupload',
74
- 'json': 'true'
75
- }
76
- headers = {'User-Agent': agent.random()}
77
- if attempt > 0:
78
- headers['Connection'] = 'close'
79
- resp = requests.post("https://catbox.moe/user/api.php", files=files, data=data, headers=headers, timeout=timeout)
80
- if resp.status_code == 200 and resp.text.strip():
81
- text = resp.text.strip()
82
- if text.startswith('http'):
83
- return text
84
- try:
85
- result = resp.json()
86
- if "url" in result:
87
- return result["url"]
88
- except json.JSONDecodeError:
89
- if 'http' in text:
90
- return text
91
- except Exception:
92
- if attempt < max_retries - 1:
93
- time.sleep(1 * (attempt + 1))
94
- finally:
95
- if tmp_path and os.path.isfile(tmp_path):
96
- try:
97
- os.remove(tmp_path)
98
- except Exception:
99
- pass
100
- return None
101
-
102
- def upload_file_alternative(img_bytes, image_format):
103
- try:
104
- ext = "jpg" if image_format.lower() == "jpeg" else "png"
105
- with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as tmp:
106
- tmp.write(img_bytes)
107
- tmp.flush()
108
- tmp_path = tmp.name
109
- try:
110
- if not os.path.isfile(tmp_path):
111
- return None
112
- with open(tmp_path, 'rb') as img_file:
113
- files = {'file': img_file}
114
- response = requests.post('https://0x0.st', files=files)
115
- response.raise_for_status()
116
- image_url = response.text.strip()
117
- if not image_url.startswith('http'):
118
- return None
119
- return image_url
120
- except Exception:
121
- return None
122
- finally:
123
- try:
124
- os.remove(tmp_path)
125
- except Exception:
126
- pass
127
- except Exception:
128
- return None
129
-
130
- for _ in range(n):
131
- # Step 1: Get Authentication Token
132
- auth_data = self._client.read_and_refresh_token()
133
- gen_headers = {
134
- "Authorization": auth_data.get("idToken"),
135
- }
136
- # Remove content-type header for form data
137
- if "content-type" in self._client.session.headers:
138
- del self._client.session.headers["content-type"]
139
- # get_model now returns the proper style name from model_aliases
140
- style_value = self._client.get_model(model)
141
- image_payload = {
142
- "prompt": str(prompt),
143
- "negative_prompt": str(kwargs.get("negative_prompt", "blurry, deformed hands, ugly")),
144
- "style": str(style_value),
145
- "images_num": str(1), # Generate one image at a time in the loop
146
- "cfg_scale": str(kwargs.get("guidance_scale", 7)),
147
- "steps": str(kwargs.get("num_inference_steps", 30)),
148
- "aspect_ratio": str(aspect_ratio),
149
- }
150
- # Step 2: Generate Image (send as form data, not JSON)
151
- image_response = self._client.session.post(
152
- self._client.image_generation_url,
153
- data=image_payload, # Use form data instead of JSON
154
- headers=gen_headers,
155
- timeout=timeout
156
- )
157
- if image_response.status_code != 200:
158
- raise RuntimeError(f"AIArta API error {image_response.status_code}: {image_response.text}\nPayload: {image_payload}")
159
- image_data = image_response.json()
160
- record_id = image_data.get("record_id")
161
- if not record_id:
162
- raise RuntimeError(f"Failed to initiate image generation: {image_data}")
163
- # Step 3: Check Generation Status
164
- status_url = self._client.status_check_url.format(record_id=record_id)
165
- while True:
166
- status_response = self._client.session.get(
167
- status_url,
168
- headers=gen_headers,
169
- timeout=timeout
170
- )
171
- status_data = status_response.json()
172
- status = status_data.get("status")
173
- if status == "DONE":
174
- image_urls = [image["url"] for image in status_data.get("response", [])]
175
- if not image_urls:
176
- raise RuntimeError("No image URLs returned from AIArta")
177
- img_resp = self._client.session.get(image_urls[0], timeout=timeout)
178
- img_resp.raise_for_status()
179
- img_bytes = img_resp.content
180
- # Convert to png or jpeg in memory
181
- with BytesIO(img_bytes) as input_io:
182
- with Image.open(input_io) as im:
183
- out_io = BytesIO()
184
- if image_format.lower() == "jpeg":
185
- im = im.convert("RGB")
186
- im.save(out_io, format="JPEG")
187
- else:
188
- im.save(out_io, format="PNG")
189
- img_bytes = out_io.getvalue()
190
- images.append(img_bytes)
191
- if response_format == "url":
192
- uploaded_url = upload_file_with_retry(img_bytes, image_format)
193
- if not uploaded_url:
194
- uploaded_url = upload_file_alternative(img_bytes, image_format)
195
- if uploaded_url:
196
- urls.append(uploaded_url)
197
- else:
198
- raise RuntimeError("Failed to upload image to catbox.moe using all available methods")
199
- break
200
- elif status in ("IN_QUEUE", "IN_PROGRESS"):
201
- time.sleep(2)
202
- else:
203
- raise RuntimeError(f"Image generation failed with status: {status}")
204
-
205
- result_data = []
206
- if response_format == "url":
207
- for url in urls:
208
- result_data.append(ImageData(url=url))
209
- elif response_format == "b64_json":
210
- import base64
211
- for img in images:
212
- b64 = base64.b64encode(img).decode("utf-8")
213
- result_data.append(ImageData(b64_json=b64))
214
- else:
215
- raise ValueError("response_format must be 'url' or 'b64_json'")
216
-
217
- from time import time as _time
218
- return ImageResponse(
219
- created=int(_time()),
220
- data=result_data
221
- )
222
-
223
- class AIArta(TTICompatibleProvider):
224
- # Model aliases mapping from lowercase keys to proper API style names
225
- model_aliases = {
226
- "flux": "Flux",
227
- "medieval": "Medieval",
228
- "vincent_van_gogh": "Vincent Van Gogh",
229
- "f_dev": "F Dev",
230
- "low_poly": "Low Poly",
231
- "dreamshaper_xl": "Dreamshaper-xl",
232
- "anima_pencil_xl": "Anima-pencil-xl",
233
- "biomech": "Biomech",
234
- "trash_polka": "Trash Polka",
235
- "no_style": "No Style",
236
- "cheyenne_xl": "Cheyenne-xl",
237
- "chicano": "Chicano",
238
- "embroidery_tattoo": "Embroidery tattoo",
239
- "red_and_black": "Red and Black",
240
- "fantasy_art": "Fantasy Art",
241
- "watercolor": "Watercolor",
242
- "dotwork": "Dotwork",
243
- "old_school_colored": "Old school colored",
244
- "realistic_tattoo": "Realistic tattoo",
245
- "japanese_2": "Japanese_2",
246
- "realistic_stock_xl": "Realistic-stock-xl",
247
- "f_pro": "F Pro",
248
- "revanimated": "RevAnimated",
249
- "katayama_mix_xl": "Katayama-mix-xl",
250
- "sdxl_l": "SDXL L",
251
- "cor_epica_xl": "Cor-epica-xl",
252
- "anime_tattoo": "Anime tattoo",
253
- "new_school": "New School",
254
- "death_metal": "Death metal",
255
- "old_school": "Old School",
256
- "juggernaut_xl": "Juggernaut-xl",
257
- "photographic": "Photographic",
258
- "sdxl_1_0": "SDXL 1.0",
259
- "graffiti": "Graffiti",
260
- "mini_tattoo": "Mini tattoo",
261
- "surrealism": "Surrealism",
262
- "neo_traditional": "Neo-traditional",
263
- "on_limbs_black": "On limbs black",
264
- "yamers_realistic_xl": "Yamers-realistic-xl",
265
- "pony_xl": "Pony-xl",
266
- "playground_xl": "Playground-xl",
267
- "anything_xl": "Anything-xl",
268
- "flame_design": "Flame design",
269
- "kawaii": "Kawaii",
270
- "cinematic_art": "Cinematic Art",
271
- "professional": "Professional",
272
- "black_ink": "Black Ink"
273
- }
274
-
275
- AVAILABLE_MODELS = list(model_aliases.keys())
276
- default_model = "Flux"
277
- default_image_model = default_model
278
-
279
- def __init__(self):
280
- self.image_generation_url = "https://img-gen-prod.ai-arta.com/api/v1/text2image"
281
- self.status_check_url = "https://img-gen-prod.ai-arta.com/api/v1/text2image/{record_id}/status"
282
- self.auth_url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=AIzaSyB3-71wG0fIt0shj0ee4fvx1shcjJHGrrQ"
283
- self.token_refresh_url = "https://securetoken.googleapis.com/v1/token?key=AIzaSyB3-71wG0fIt0shj0ee4fvx1shcjJHGrrQ"
284
- self.session = requests.Session()
285
- self.user_agent = LitAgent().random()
286
- self.headers = {
287
- "accept": "application/json",
288
- "accept-language": "en-US,en;q=0.9",
289
- "origin": "https://img-gen-prod.ai-arta.com",
290
- "referer": "https://img-gen-prod.ai-arta.com/",
291
- "user-agent": self.user_agent,
292
- }
293
- self.session.headers.update(self.headers)
294
- self.images = Images(self)
295
-
296
- def get_auth_file(self) -> str:
297
- path = os.path.join(os.path.expanduser("~"), ".ai_arta_cookies")
298
- if not os.path.exists(path):
299
- os.makedirs(path)
300
- filename = f"auth_{self.__class__.__name__}.json"
301
- return os.path.join(path, filename)
302
-
303
- def create_token(self, path: str) -> Dict[str, Any]:
304
- auth_payload = {"clientType": "CLIENT_TYPE_ANDROID"}
305
- proxies = self.session.proxies if self.session.proxies else None
306
- auth_response = self.session.post(self.auth_url, json=auth_payload, timeout=60, proxies=proxies)
307
- auth_data = auth_response.json()
308
- auth_token = auth_data.get("idToken")
309
- if not auth_token:
310
- raise Exception("Failed to obtain authentication token.")
311
- with open(path, 'w') as f:
312
- json.dump(auth_data, f)
313
- return auth_data
314
-
315
- def refresh_token(self, refresh_token: str) -> tuple[str, str]:
316
- payload = {
317
- "grant_type": "refresh_token",
318
- "refresh_token": refresh_token,
319
- }
320
- response = self.session.post(self.token_refresh_url, data=payload, timeout=60)
321
- response_data = response.json()
322
- return response_data.get("id_token"), response_data.get("refresh_token")
323
-
324
- def read_and_refresh_token(self) -> Dict[str, Any]:
325
- path = self.get_auth_file()
326
- if os.path.isfile(path):
327
- with open(path, 'r') as f:
328
- auth_data = json.load(f)
329
- diff = time.time() - os.path.getmtime(path)
330
- expires_in = int(auth_data.get("expiresIn", 3600))
331
- if diff < expires_in:
332
- if diff > expires_in / 2:
333
- auth_data["idToken"], auth_data["refreshToken"] = self.refresh_token(
334
- auth_data.get("refreshToken")
335
- )
336
- with open(path, 'w') as f:
337
- json.dump(auth_data, f)
338
- return auth_data
339
- return self.create_token(path)
340
-
341
- def get_model(self, model_name: str) -> str:
342
- # Convert to lowercase for lookup
343
- model_key = model_name.lower()
344
- # Return the proper style name from model_aliases, or the original if not found
345
- return self.model_aliases.get(model_key, model_name)
346
-
347
- @property
348
- def models(self):
349
- class _ModelList:
350
- def list(inner_self):
351
- return type(self).AVAILABLE_MODELS
352
- return _ModelList()
353
-
354
- # Example usage:
355
- if __name__ == "__main__":
356
- from rich import print
357
- client = AIArta()
358
- response = client.images.create(
359
- model="flux",
360
- prompt="a white siamese cat",
361
- response_format="url",
362
- n=2,
363
- timeout=30,
364
- )
365
- print(response)
1
+ """AIArtaImager TTI-Compatible Provider - Generate stunning AI art with AI Arta! 🎨
2
+
3
+ Examples:
4
+ >>> from webscout.Provider.TTI.aiarta import AIArta
5
+ >>> client = AIArta()
6
+ >>> response = client.images.create(
7
+ ... model="flux",
8
+ ... prompt="A cool cyberpunk city at night",
9
+ ... n=1
10
+ ... )
11
+ >>> print(response)
12
+ """
13
+
14
+ import requests
15
+ from typing import Optional, List, Dict, Any
16
+ from webscout.Provider.TTI.utils import (
17
+ ImageData,
18
+ ImageResponse
19
+ )
20
+ from webscout.Provider.TTI.base import TTICompatibleProvider, BaseImages
21
+ from io import BytesIO
22
+ import os
23
+ import tempfile
24
+ from webscout.litagent import LitAgent
25
+ import time
26
+ import json
27
+
28
+ try:
29
+ from PIL import Image
30
+ except ImportError:
31
+ Image = None
32
+
33
+
34
+ class Images(BaseImages):
35
+ def __init__(self, client: "AIArta"):
36
+ self._client = client
37
+
38
+ def create(
39
+ self,
40
+ *,
41
+ model: str,
42
+ prompt: str,
43
+ n: int = 1,
44
+ size: str = "1024x1024",
45
+ response_format: str = "url",
46
+ user: Optional[str] = None,
47
+ style: str = "none",
48
+ aspect_ratio: str = "1:1",
49
+ timeout: int = 60,
50
+ image_format: str = "png",
51
+ **kwargs,
52
+ ) -> ImageResponse:
53
+ """
54
+ image_format: "png" or "jpeg"
55
+ """
56
+ if Image is None:
57
+ raise ImportError("Pillow (PIL) is required for image format conversion.")
58
+
59
+ images = []
60
+ urls = []
61
+ agent = LitAgent()
62
+
63
+ def upload_file_with_retry(img_bytes, image_format, max_retries=3):
64
+ ext = "jpg" if image_format.lower() == "jpeg" else "png"
65
+ for attempt in range(max_retries):
66
+ tmp_path = None
67
+ try:
68
+ with tempfile.NamedTemporaryFile(
69
+ suffix=f".{ext}", delete=False
70
+ ) as tmp:
71
+ tmp.write(img_bytes)
72
+ tmp.flush()
73
+ tmp_path = tmp.name
74
+ with open(tmp_path, "rb") as f:
75
+ files = {"fileToUpload": (f"image.{ext}", f, f"image/{ext}")}
76
+ data = {"reqtype": "fileupload", "json": "true"}
77
+ headers = {"User-Agent": agent.random()}
78
+ if attempt > 0:
79
+ headers["Connection"] = "close"
80
+ resp = requests.post(
81
+ "https://catbox.moe/user/api.php",
82
+ files=files,
83
+ data=data,
84
+ headers=headers,
85
+ timeout=timeout,
86
+ )
87
+ if resp.status_code == 200 and resp.text.strip():
88
+ text = resp.text.strip()
89
+ if text.startswith("http"):
90
+ return text
91
+ try:
92
+ result = resp.json()
93
+ if "url" in result:
94
+ return result["url"]
95
+ except json.JSONDecodeError:
96
+ if "http" in text:
97
+ return text
98
+ except Exception:
99
+ if attempt < max_retries - 1:
100
+ time.sleep(1 * (attempt + 1))
101
+ finally:
102
+ if tmp_path and os.path.isfile(tmp_path):
103
+ try:
104
+ os.remove(tmp_path)
105
+ except Exception:
106
+ pass
107
+ return None
108
+
109
+ def upload_file_alternative(img_bytes, image_format):
110
+ try:
111
+ ext = "jpg" if image_format.lower() == "jpeg" else "png"
112
+ with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as tmp:
113
+ tmp.write(img_bytes)
114
+ tmp.flush()
115
+ tmp_path = tmp.name
116
+ try:
117
+ if not os.path.isfile(tmp_path):
118
+ return None
119
+ with open(tmp_path, "rb") as img_file:
120
+ files = {"file": img_file}
121
+ response = requests.post("https://0x0.st", files=files)
122
+ response.raise_for_status()
123
+ image_url = response.text.strip()
124
+ if not image_url.startswith("http"):
125
+ return None
126
+ return image_url
127
+ except Exception:
128
+ return None
129
+ finally:
130
+ try:
131
+ os.remove(tmp_path)
132
+ except Exception:
133
+ pass
134
+ except Exception:
135
+ return None
136
+
137
+ for _ in range(n):
138
+ # Step 1: Get Authentication Token
139
+ auth_data = self._client.read_and_refresh_token()
140
+ gen_headers = {
141
+ "Authorization": auth_data.get("idToken"),
142
+ }
143
+ # Remove content-type header for form data
144
+ if "content-type" in self._client.session.headers:
145
+ del self._client.session.headers["content-type"]
146
+ # get_model now returns the proper style name from model_aliases
147
+ style_value = self._client.get_model(model)
148
+ image_payload = {
149
+ "prompt": str(prompt),
150
+ "negative_prompt": str(
151
+ kwargs.get("negative_prompt", "blurry, deformed hands, ugly")
152
+ ),
153
+ "style": str(style_value),
154
+ "images_num": str(1), # Generate one image at a time in the loop
155
+ "cfg_scale": str(kwargs.get("guidance_scale", 7)),
156
+ "steps": str(kwargs.get("num_inference_steps", 30)),
157
+ "aspect_ratio": str(aspect_ratio),
158
+ }
159
+ # Step 2: Generate Image (send as form data, not JSON)
160
+ image_response = self._client.session.post(
161
+ self._client.image_generation_url,
162
+ data=image_payload, # Use form data instead of JSON
163
+ headers=gen_headers,
164
+ timeout=timeout,
165
+ )
166
+ if image_response.status_code != 200:
167
+ raise RuntimeError(
168
+ f"AIArta API error {image_response.status_code}: {image_response.text}\nPayload: {image_payload}"
169
+ )
170
+ image_data = image_response.json()
171
+ record_id = image_data.get("record_id")
172
+ if not record_id:
173
+ raise RuntimeError(f"Failed to initiate image generation: {image_data}")
174
+ # Step 3: Check Generation Status
175
+ status_url = self._client.status_check_url.format(record_id=record_id)
176
+ while True:
177
+ status_response = self._client.session.get(
178
+ status_url,
179
+ headers=gen_headers,
180
+ timeout=timeout,
181
+ )
182
+ status_data = status_response.json()
183
+ status = status_data.get("status")
184
+ if status == "DONE":
185
+ image_urls = [
186
+ image["url"] for image in status_data.get("response", [])
187
+ ]
188
+ if not image_urls:
189
+ raise RuntimeError("No image URLs returned from AIArta")
190
+ img_resp = self._client.session.get(
191
+ image_urls[0],
192
+ timeout=timeout,
193
+ )
194
+ img_resp.raise_for_status()
195
+ img_bytes = img_resp.content
196
+ # Convert to png or jpeg in memory
197
+ with BytesIO(img_bytes) as input_io:
198
+ with Image.open(input_io) as im:
199
+ out_io = BytesIO()
200
+ if image_format.lower() == "jpeg":
201
+ im = im.convert("RGB")
202
+ im.save(out_io, format="JPEG")
203
+ else:
204
+ im.save(out_io, format="PNG")
205
+ img_bytes = out_io.getvalue()
206
+ images.append(img_bytes)
207
+ if response_format == "url":
208
+ uploaded_url = upload_file_with_retry(img_bytes, image_format)
209
+ if not uploaded_url:
210
+ uploaded_url = upload_file_alternative(
211
+ img_bytes, image_format
212
+ )
213
+ if uploaded_url:
214
+ urls.append(uploaded_url)
215
+ else:
216
+ raise RuntimeError(
217
+ "Failed to upload image to catbox.moe using all available methods"
218
+ )
219
+ break
220
+ elif status in ("IN_QUEUE", "IN_PROGRESS"):
221
+ time.sleep(2)
222
+ else:
223
+ raise RuntimeError(f"Image generation failed with status: {status}")
224
+
225
+ result_data = []
226
+ if response_format == "url":
227
+ for url in urls:
228
+ result_data.append(ImageData(url=url))
229
+ elif response_format == "b64_json":
230
+ import base64
231
+
232
+ for img in images:
233
+ b64 = base64.b64encode(img).decode("utf-8")
234
+ result_data.append(ImageData(b64_json=b64))
235
+ else:
236
+ raise ValueError("response_format must be 'url' or 'b64_json'")
237
+
238
+ from time import time as _time
239
+
240
+ return ImageResponse(created=int(_time()), data=result_data)
241
+
242
+
243
+ class AIArta(TTICompatibleProvider):
244
+ # Model aliases mapping from lowercase keys to proper API style names
245
+ model_aliases = {
246
+ "flux": "Flux",
247
+ "medieval": "Medieval",
248
+ "vincent_van_gogh": "Vincent Van Gogh",
249
+ "f_dev": "F Dev",
250
+ "low_poly": "Low Poly",
251
+ "dreamshaper_xl": "Dreamshaper-xl",
252
+ "anima_pencil_xl": "Anima-pencil-xl",
253
+ "biomech": "Biomech",
254
+ "trash_polka": "Trash Polka",
255
+ "no_style": "No Style",
256
+ "cheyenne_xl": "Cheyenne-xl",
257
+ "chicano": "Chicano",
258
+ "embroidery_tattoo": "Embroidery tattoo",
259
+ "red_and_black": "Red and Black",
260
+ "fantasy_art": "Fantasy Art",
261
+ "watercolor": "Watercolor",
262
+ "dotwork": "Dotwork",
263
+ "old_school_colored": "Old school colored",
264
+ "realistic_tattoo": "Realistic tattoo",
265
+ "japanese_2": "Japanese_2",
266
+ "realistic_stock_xl": "Realistic-stock-xl",
267
+ "f_pro": "F Pro",
268
+ "revanimated": "RevAnimated",
269
+ "katayama_mix_xl": "Katayama-mix-xl",
270
+ "sdxl_l": "SDXL L",
271
+ "cor_epica_xl": "Cor-epica-xl",
272
+ "anime_tattoo": "Anime tattoo",
273
+ "new_school": "New School",
274
+ "death_metal": "Death metal",
275
+ "old_school": "Old School",
276
+ "juggernaut_xl": "Juggernaut-xl",
277
+ "photographic": "Photographic",
278
+ "sdxl_1_0": "SDXL 1.0",
279
+ "graffiti": "Graffiti",
280
+ "mini_tattoo": "Mini tattoo",
281
+ "surrealism": "Surrealism",
282
+ "neo_traditional": "Neo-traditional",
283
+ "on_limbs_black": "On limbs black",
284
+ "yamers_realistic_xl": "Yamers-realistic-xl",
285
+ "pony_xl": "Pony-xl",
286
+ "playground_xl": "Playground-xl",
287
+ "anything_xl": "Anything-xl",
288
+ "flame_design": "Flame design",
289
+ "kawaii": "Kawaii",
290
+ "cinematic_art": "Cinematic Art",
291
+ "professional": "Professional",
292
+ "black_ink": "Black Ink",
293
+ }
294
+
295
+ AVAILABLE_MODELS = list(model_aliases.keys())
296
+ default_model = "Flux"
297
+ default_image_model = default_model
298
+
299
+ def __init__(self):
300
+ self.image_generation_url = "https://img-gen-prod.ai-arta.com/api/v1/text2image"
301
+ self.status_check_url = (
302
+ "https://img-gen-prod.ai-arta.com/api/v1/text2image/{record_id}/status"
303
+ )
304
+ self.auth_url = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=AIzaSyB3-71wG0fIt0shj0ee4fvx1shcjJHGrrQ"
305
+ self.token_refresh_url = "https://securetoken.googleapis.com/v1/token?key=AIzaSyB3-71wG0fIt0shj0ee4fvx1shcjJHGrrQ"
306
+ self.session = requests.Session()
307
+ self.user_agent = LitAgent().random()
308
+ self.headers = {
309
+ "accept": "application/json",
310
+ "accept-language": "en-US,en;q=0.9",
311
+ "origin": "https://img-gen-prod.ai-arta.com",
312
+ "referer": "https://img-gen-prod.ai-arta.com/",
313
+ "user-agent": self.user_agent,
314
+ }
315
+ self.session.headers.update(self.headers)
316
+ self.images = Images(self)
317
+
318
+ def get_auth_file(self) -> str:
319
+ path = os.path.join(os.path.expanduser("~"), ".ai_arta_cookies")
320
+ if not os.path.exists(path):
321
+ os.makedirs(path)
322
+ filename = f"auth_{self.__class__.__name__}.json"
323
+ return os.path.join(path, filename)
324
+
325
+ def create_token(self, path: str) -> Dict[str, Any]:
326
+ auth_payload = {"clientType": "CLIENT_TYPE_ANDROID"}
327
+ proxies = self.session.proxies if self.session.proxies else None
328
+ auth_response = self.session.post(
329
+ self.auth_url,
330
+ json=auth_payload,
331
+ timeout=60,
332
+ proxies=proxies,
333
+ )
334
+ auth_data = auth_response.json()
335
+ auth_token = auth_data.get("idToken")
336
+ if not auth_token:
337
+ raise Exception("Failed to obtain authentication token.")
338
+ with open(path, "w") as f:
339
+ json.dump(auth_data, f)
340
+ return auth_data
341
+
342
+ def refresh_token(self, refresh_token: str) -> tuple[str, str]:
343
+ payload = {
344
+ "grant_type": "refresh_token",
345
+ "refresh_token": refresh_token,
346
+ }
347
+ response = self.session.post(
348
+ self.token_refresh_url,
349
+ data=payload,
350
+ timeout=60,
351
+ )
352
+ response_data = response.json()
353
+ return response_data.get("id_token"), response_data.get("refresh_token")
354
+
355
+ def read_and_refresh_token(self) -> Dict[str, Any]:
356
+ path = self.get_auth_file()
357
+ if os.path.isfile(path):
358
+ with open(path, "r") as f:
359
+ auth_data = json.load(f)
360
+ diff = time.time() - os.path.getmtime(path)
361
+ expires_in = int(auth_data.get("expiresIn", 3600))
362
+ if diff < expires_in:
363
+ if diff > expires_in / 2:
364
+ auth_data["idToken"], auth_data["refreshToken"] = (
365
+ self.refresh_token(auth_data.get("refreshToken"))
366
+ )
367
+ with open(path, "w") as f:
368
+ json.dump(auth_data, f)
369
+ return auth_data
370
+ return self.create_token(path)
371
+
372
+ def get_model(self, model_name: str) -> str:
373
+ # Convert to lowercase for lookup
374
+ model_key = model_name.lower()
375
+ # Return the proper style name from model_aliases, or the original if not found
376
+ return self.model_aliases.get(model_key, model_name)
377
+
378
+ @property
379
+ def models(self):
380
+ class _ModelList:
381
+ def list(inner_self):
382
+ return type(self).AVAILABLE_MODELS
383
+
384
+ return _ModelList()
385
+
386
+
387
+ # Example usage:
388
+ if __name__ == "__main__":
389
+ from rich import print
390
+
391
+ client = AIArta()
392
+ response = client.images.create(
393
+ model="flux",
394
+ prompt="a white siamese cat",
395
+ response_format="url",
396
+ n=2,
397
+ timeout=30,
398
+ )
399
+ print(response)