habulaj commited on
Commit
d8423f7
·
verified ·
1 Parent(s): 0ce6056

Create images.py

Browse files
Files changed (1) hide show
  1. gemini_client/images.py +202 -0
gemini_client/images.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import random
4
+ import re
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Dict, Union, Optional
8
+
9
+ from pydantic import BaseModel, field_validator
10
+ from curl_cffi import CurlError
11
+ from curl_cffi.requests import AsyncSession
12
+ from requests.exceptions import HTTPError, RequestException # Ensure RequestException is imported
13
+
14
+ from rich.console import Console
15
+
16
+ console = Console() # Instantiate console for logging
17
+
18
+ class Image(BaseModel):
19
+ """
20
+ Represents a single image object returned from Gemini.
21
+
22
+ Attributes:
23
+ url (str): URL of the image.
24
+ title (str): Title of the image (default: "[Image]").
25
+ alt (str): Optional description of the image.
26
+ proxy (str | dict | None): Proxy used when saving the image.
27
+ impersonate (str): Browser profile for curl_cffi to impersonate.
28
+ """
29
+ url: str
30
+ title: str = "[Image]"
31
+ alt: str = ""
32
+ proxy: Optional[Union[str, Dict[str, str]]] = None
33
+ impersonate: str = "chrome110"
34
+
35
+ def __str__(self):
36
+ return f"{self.title}({self.url}) - {self.alt}"
37
+
38
+ def __repr__(self):
39
+ short_url = self.url if len(self.url) <= 50 else self.url[:20] + "..." + self.url[-20:]
40
+ short_alt = self.alt[:30] + "..." if len(self.alt) > 30 else self.alt
41
+ return f"Image(title='{self.title}', url='{short_url}', alt='{short_alt}')"
42
+
43
+ async def save(
44
+ self,
45
+ path: str = "downloaded_images",
46
+ filename: Optional[str] = None,
47
+ cookies: Optional[dict] = None,
48
+ verbose: bool = False,
49
+ skip_invalid_filename: bool = True,
50
+ ) -> Optional[str]:
51
+ """
52
+ Save the image to disk using curl_cffi.
53
+ Parameters:
54
+ path: str, optional
55
+ Directory to save the image (default "downloaded_images").
56
+ filename: str, optional
57
+ Filename to use; if not provided, inferred from URL.
58
+ cookies: dict, optional
59
+ Cookies used for the image request.
60
+ verbose: bool, optional
61
+ If True, outputs status messages (default False).
62
+ skip_invalid_filename: bool, optional
63
+ If True, skips saving if the filename is invalid.
64
+ Returns:
65
+ Absolute path of the saved image if successful; None if skipped.
66
+ Raises:
67
+ HTTPError if the network request fails.
68
+ RequestException/CurlError for other network errors.
69
+ IOError if file writing fails.
70
+ """
71
+ # Generate filename from URL if not provided
72
+ if not filename:
73
+ try:
74
+ from urllib.parse import urlparse, unquote
75
+ parsed_url = urlparse(self.url)
76
+ base_filename = os.path.basename(unquote(parsed_url.path))
77
+ # Remove invalid characters for filenames
78
+ safe_filename = re.sub(r'[<>:"/\|?*]', '_', base_filename)
79
+ if safe_filename and len(safe_filename) > 0:
80
+ filename = safe_filename
81
+ else:
82
+ filename = f"image_{random.randint(1000, 9999)}.jpg"
83
+ except Exception:
84
+ filename = f"image_{random.randint(1000, 9999)}.jpg"
85
+
86
+ # Validate filename length
87
+ try:
88
+ _ = Path(filename)
89
+ max_len = 255
90
+ if len(filename) > max_len:
91
+ name, ext = os.path.splitext(filename)
92
+ filename = name[:max_len - len(ext) - 1] + ext
93
+ except (OSError, ValueError):
94
+ if verbose:
95
+ console.log(f"[yellow]Invalid filename generated: {filename}[/yellow]")
96
+ if skip_invalid_filename:
97
+ if verbose:
98
+ console.log("[yellow]Skipping save due to invalid filename.[/yellow]")
99
+ return None
100
+ filename = f"image_{random.randint(1000, 9999)}.jpg"
101
+ if verbose:
102
+ console.log(f"[yellow]Using fallback filename: {filename}[/yellow]")
103
+
104
+ # Prepare proxy dictionary for curl_cffi
105
+ proxies_dict = None
106
+ if isinstance(self.proxy, str):
107
+ proxies_dict = {"http": self.proxy, "https": self.proxy}
108
+ elif isinstance(self.proxy, dict):
109
+ proxies_dict = self.proxy
110
+
111
+ try:
112
+ # Use AsyncSession from curl_cffi
113
+ async with AsyncSession(
114
+ cookies=cookies,
115
+ proxies=proxies_dict,
116
+ impersonate=self.impersonate
117
+ # follow_redirects is handled automatically by curl_cffi
118
+ ) as client:
119
+ if verbose:
120
+ console.log(f"Attempting to download image from: {self.url}")
121
+
122
+ response = await client.get(self.url)
123
+ response.raise_for_status()
124
+
125
+ # Check content type
126
+ content_type = response.headers.get("content-type", "").lower()
127
+ if "image" not in content_type and verbose:
128
+ console.log(f"[yellow]Warning: Content type is '{content_type}', not an image. Saving anyway.[/yellow]")
129
+
130
+ # Create directory and save file
131
+ dest_path = Path(path)
132
+ dest_path.mkdir(parents=True, exist_ok=True)
133
+ dest = dest_path / filename
134
+
135
+ # Write image data to file
136
+ dest.write_bytes(response.content)
137
+
138
+ if verbose:
139
+ console.log(f"Image saved successfully as {dest.resolve()}")
140
+
141
+ return str(dest.resolve())
142
+
143
+ except HTTPError as e:
144
+ console.log(f"[red]Error downloading image {self.url}: {e.response.status_code} {e}[/red]")
145
+ raise
146
+ except (RequestException, CurlError) as e:
147
+ console.log(f"[red]Network error downloading image {self.url}: {e}[/red]")
148
+ raise
149
+ except IOError as e:
150
+ console.log(f"[red]Error writing image file to {dest}: {e}[/red]")
151
+ raise
152
+ except Exception as e:
153
+ console.log(f"[red]An unexpected error occurred during image save: {e}[/red]")
154
+ raise
155
+
156
+
157
+ class WebImage(Image):
158
+ """
159
+ Represents an image retrieved from web search results.
160
+
161
+ Returned when asking Gemini to "SEND an image of [something]".
162
+ """
163
+ pass
164
+
165
+ class GeneratedImage(Image):
166
+ """
167
+ Represents an image generated by Google's AI image generator (e.g., ImageFX).
168
+
169
+ Attributes:
170
+ cookies (dict[str, str]): Cookies required for accessing the generated image URL,
171
+ typically from the GeminiClient/Chatbot instance.
172
+ """
173
+ cookies: Dict[str, str]
174
+
175
+ # Updated validator for Pydantic V2
176
+ @field_validator("cookies")
177
+ @classmethod
178
+ def validate_cookies(cls, v: Dict[str, str]) -> Dict[str, str]:
179
+ """Ensures cookies are provided for generated images."""
180
+ if not v or not isinstance(v, dict):
181
+ raise ValueError("GeneratedImage requires a dictionary of cookies from the client.")
182
+ return v
183
+
184
+ async def save(self, **kwargs) -> Optional[str]:
185
+ """
186
+ Save the generated image to disk.
187
+ Parameters:
188
+ filename: str, optional
189
+ Filename to use. If not provided, a default name including
190
+ a timestamp and part of the URL is used. Generated images
191
+ are often in .png or .jpg format.
192
+ Additional arguments are passed to Image.save.
193
+ Returns:
194
+ Absolute path of the saved image if successful, None if skipped.
195
+ """
196
+ if "filename" not in kwargs:
197
+ ext = ".jpg" if ".jpg" in self.url.lower() else ".png"
198
+ url_part = self.url.split('/')[-1][:10]
199
+ kwargs["filename"] = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{url_part}{ext}"
200
+
201
+ # Pass the required cookies and other args (like impersonate) to the parent save method
202
+ return await super().save(cookies=self.cookies, **kwargs)