webscout 2025.10.16__py3-none-any.whl → 2025.10.18__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.

@@ -0,0 +1,315 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ import tempfile
5
+ import time
6
+ from io import BytesIO
7
+ from typing import Optional
8
+
9
+ import requests
10
+ from requests.exceptions import RequestException
11
+
12
+ from webscout.litagent import LitAgent
13
+ from webscout.Provider.TTI.base import BaseImages, TTICompatibleProvider
14
+ from webscout.Provider.TTI.utils import ImageData, ImageResponse
15
+
16
+ try:
17
+ from PIL import Image
18
+ except ImportError:
19
+ Image = None
20
+
21
+
22
+ class Images(BaseImages):
23
+ def __init__(self, client):
24
+ self._client = client
25
+
26
+ def create(
27
+ self,
28
+ *,
29
+ model: str,
30
+ prompt: str,
31
+ n: int = 1,
32
+ size: str = "1024x1024",
33
+ response_format: str = "url",
34
+ user: Optional[str] = None,
35
+ style: str = "none",
36
+ aspect_ratio: str = "1:1",
37
+ timeout: int = 60,
38
+ image_format: str = "png",
39
+ seed: Optional[int] = None,
40
+ **kwargs,
41
+ ) -> ImageResponse:
42
+ """
43
+ Generate images using Claude Online's /imagine feature via Pollinations.ai.
44
+
45
+ Args:
46
+ model: Model to use (ignored, uses Pollinations.ai)
47
+ prompt: The image generation prompt
48
+ n: Number of images to generate (max 1 for Claude Online)
49
+ size: Image size (supports various sizes)
50
+ response_format: "url" or "b64_json"
51
+ timeout: Request timeout in seconds
52
+ image_format: Output format "png" or "jpeg"
53
+ **kwargs: Additional parameters
54
+
55
+ Returns:
56
+ ImageResponse with generated image data
57
+ """
58
+ if Image is None:
59
+ raise ImportError("Pillow (PIL) is required for image format conversion.")
60
+
61
+ # Claude Online only supports 1 image per request
62
+ if n > 1:
63
+ raise ValueError("Claude Online only supports generating 1 image per request")
64
+
65
+ # Parse size parameter
66
+ width, height = self._parse_size(size)
67
+
68
+ try:
69
+ # Clean the prompt (remove command words if present)
70
+ clean_prompt = self._clean_prompt(prompt)
71
+
72
+ # Generate image using Pollinations.ai API
73
+ timestamp = int(time.time() * 1000) # Use timestamp as seed for uniqueness
74
+ seed_value = seed if seed is not None else timestamp
75
+
76
+ # Build the Pollinations.ai URL
77
+ base_url = "https://image.pollinations.ai/prompt"
78
+ params = {
79
+ "width": width,
80
+ "height": height,
81
+ "nologo": "true",
82
+ "seed": seed_value
83
+ }
84
+
85
+ image_url = f"{base_url}/{clean_prompt}"
86
+ query_params = "&".join([f"{k}={v}" for k, v in params.items()])
87
+ full_image_url = f"{image_url}?{query_params}"
88
+
89
+ # Download the image
90
+ response = requests.get(full_image_url, timeout=timeout, stream=True)
91
+ response.raise_for_status()
92
+
93
+ img_bytes = response.content
94
+
95
+ # Convert image format if needed
96
+ with BytesIO(img_bytes) as input_io:
97
+ with Image.open(input_io) as im:
98
+ out_io = BytesIO()
99
+ if image_format.lower() == "jpeg":
100
+ im = im.convert("RGB")
101
+ im.save(out_io, format="JPEG")
102
+ else:
103
+ im.save(out_io, format="PNG")
104
+ processed_img_bytes = out_io.getvalue()
105
+
106
+ # Handle response format
107
+ if response_format == "url":
108
+ # Upload to image hosting service
109
+ uploaded_url = self._upload_image(processed_img_bytes, image_format)
110
+ if not uploaded_url:
111
+ raise RuntimeError("Failed to upload generated image")
112
+ result_data = [ImageData(url=uploaded_url)]
113
+ elif response_format == "b64_json":
114
+ b64 = base64.b64encode(processed_img_bytes).decode("utf-8")
115
+ result_data = [ImageData(b64_json=b64)]
116
+ else:
117
+ raise ValueError("response_format must be 'url' or 'b64_json'")
118
+
119
+ return ImageResponse(created=int(time.time()), data=result_data)
120
+
121
+ except RequestException as e:
122
+ raise RuntimeError(f"Failed to generate image with Claude Online: {e}")
123
+ except Exception as e:
124
+ raise RuntimeError(f"Unexpected error during image generation: {e}")
125
+
126
+ def _parse_size(self, size: str) -> tuple[int, int]:
127
+ """Parse size string into width and height."""
128
+ size = size.lower().strip()
129
+
130
+ # Handle common size formats
131
+ size_map = {
132
+ "256x256": (256, 256),
133
+ "512x512": (512, 512),
134
+ "1024x1024": (1024, 1024),
135
+ "1024x768": (1024, 768),
136
+ "768x1024": (768, 1024),
137
+ "1280x720": (1280, 720),
138
+ "720x1280": (720, 1280),
139
+ "1920x1080": (1920, 1080),
140
+ "1080x1920": (1080, 1920),
141
+ }
142
+
143
+ if size in size_map:
144
+ return size_map[size]
145
+
146
+ # Try to parse custom size (e.g., "800x600")
147
+ try:
148
+ width, height = size.split("x")
149
+ return int(width), int(height)
150
+ except (ValueError, AttributeError):
151
+ # Default to 1024x1024
152
+ return 1024, 1024
153
+
154
+ def _clean_prompt(self, prompt: str) -> str:
155
+ """Clean the prompt by removing command prefixes."""
156
+ # Remove common image generation command prefixes
157
+ prefixes_to_remove = [
158
+ r'^/imagine\s*',
159
+ r'^/image\s*',
160
+ r'^/picture\s*',
161
+ r'^/draw\s*',
162
+ r'^/create\s*',
163
+ r'^/generate\s*',
164
+ r'^создай изображение\s*',
165
+ r'^нарисуй\s*',
166
+ r'^сгенерируй картинку\s*',
167
+ ]
168
+
169
+ import re
170
+ clean_prompt = prompt
171
+ for prefix in prefixes_to_remove:
172
+ clean_prompt = re.sub(prefix, '', clean_prompt, flags=re.IGNORECASE)
173
+
174
+ return clean_prompt.strip()
175
+
176
+ def _upload_image(self, img_bytes: bytes, image_format: str, max_retries: int = 3) -> Optional[str]:
177
+ """Upload image to hosting service and return URL"""
178
+
179
+ def upload_to_catbox(img_bytes, image_format):
180
+ """Upload to catbox.moe"""
181
+ ext = "jpg" if image_format.lower() == "jpeg" else "png"
182
+ tmp_path = None
183
+ try:
184
+ with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as tmp:
185
+ tmp.write(img_bytes)
186
+ tmp.flush()
187
+ tmp_path = tmp.name
188
+
189
+ with open(tmp_path, "rb") as f:
190
+ files = {"fileToUpload": (f"image.{ext}", f, f"image/{ext}")}
191
+ data = {"reqtype": "fileupload", "json": "true"}
192
+ headers = {"User-Agent": LitAgent().random()}
193
+
194
+ resp = requests.post(
195
+ "https://catbox.moe/user/api.php",
196
+ files=files,
197
+ data=data,
198
+ headers=headers,
199
+ timeout=30,
200
+ )
201
+
202
+ if resp.status_code == 200 and resp.text.strip():
203
+ text = resp.text.strip()
204
+ if text.startswith("http"):
205
+ return text
206
+ try:
207
+ result = resp.json()
208
+ if "url" in result:
209
+ return result["url"]
210
+ except json.JSONDecodeError:
211
+ pass
212
+ except Exception:
213
+ pass
214
+ finally:
215
+ if tmp_path and os.path.isfile(tmp_path):
216
+ try:
217
+ os.remove(tmp_path)
218
+ except Exception:
219
+ pass
220
+ return None
221
+
222
+ def upload_to_0x0(img_bytes, image_format):
223
+ """Upload to 0x0.st as fallback"""
224
+ ext = "jpg" if image_format.lower() == "jpeg" else "png"
225
+ tmp_path = None
226
+ try:
227
+ with tempfile.NamedTemporaryFile(suffix=f".{ext}", delete=False) as tmp:
228
+ tmp.write(img_bytes)
229
+ tmp.flush()
230
+ tmp_path = tmp.name
231
+
232
+ with open(tmp_path, "rb") as img_file:
233
+ files = {"file": img_file}
234
+ response = requests.post("https://0x0.st", files=files, timeout=30)
235
+ response.raise_for_status()
236
+ image_url = response.text.strip()
237
+ if image_url.startswith("http"):
238
+ return image_url
239
+ except Exception:
240
+ pass
241
+ finally:
242
+ if tmp_path and os.path.isfile(tmp_path):
243
+ try:
244
+ os.remove(tmp_path)
245
+ except Exception:
246
+ pass
247
+ return None
248
+
249
+ # Try primary upload method
250
+ for attempt in range(max_retries):
251
+ uploaded_url = upload_to_catbox(img_bytes, image_format)
252
+ if uploaded_url:
253
+ return uploaded_url
254
+ time.sleep(1 * (attempt + 1))
255
+
256
+ # Try fallback method
257
+ for attempt in range(max_retries):
258
+ uploaded_url = upload_to_0x0(img_bytes, image_format)
259
+ if uploaded_url:
260
+ return uploaded_url
261
+ time.sleep(1 * (attempt + 1))
262
+
263
+ return None
264
+
265
+
266
+ class ClaudeOnlineTTI(TTICompatibleProvider):
267
+ """
268
+ Claude Online Text-to-Image Provider
269
+
270
+ Uses Claude Online's /imagine feature with Pollinations.ai backend.
271
+ Supports high-quality image generation with various styles and sizes.
272
+ """
273
+
274
+ AVAILABLE_MODELS = ["claude-imagine"]
275
+
276
+ def __init__(self):
277
+ self.api_endpoint = "https://image.pollinations.ai/prompt"
278
+ self.session = requests.Session()
279
+ self.user_agent = LitAgent().random()
280
+ self.headers = {
281
+ "accept": "image/*",
282
+ "accept-language": "en-US,en;q=0.9",
283
+ "user-agent": self.user_agent,
284
+ }
285
+ self.session.headers.update(self.headers)
286
+ self.images = Images(self)
287
+
288
+ @property
289
+ def models(self):
290
+ class _ModelList:
291
+ def list(inner_self):
292
+ return type(self).AVAILABLE_MODELS
293
+
294
+ return _ModelList()
295
+
296
+
297
+ if __name__ == "__main__":
298
+ from rich import print
299
+
300
+ # Test the Claude Online TTI provider
301
+ client = ClaudeOnlineTTI()
302
+
303
+ try:
304
+ response = client.images.create(
305
+ model="claude-imagine",
306
+ prompt="a beautiful sunset over mountains with vibrant colors",
307
+ response_format="url",
308
+ timeout=60,
309
+ )
310
+ print("✅ Image generation successful!")
311
+ print(response)
312
+ except Exception as e:
313
+ print(f"❌ Image generation failed: {e}")
314
+ import traceback
315
+ traceback.print_exc()