XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

fix: revert image module changes

6639eadc
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

2 个文件 +181 -128
Modified g4f/image/__init__.py +180 -127
@@ -4,6 +4,8 @@ import os
4 4 import re
5 5 import io
6 6 import base64
7 import socket
8 import ipaddress
7 9 from io import BytesIO
8 10 from pathlib import Path
9 11 from typing import Optional
@@ -11,6 +13,11 @@ from urllib.parse import urlparse
11 13
12 14 import requests
13 15
16 try:
17 from urllib3.util import parse_url as urllib3_parse_url
18 except ImportError:
19 urllib3_parse_url = None
20
14 21 try:
15 22 from PIL import Image, ImageOps
16 23 has_requirements = True
@@ -24,42 +31,40 @@ from ..files import get_bucket_dir
24 31 EXTENSIONS_MAP: dict[str, str] = {
25 32 # Image
26 33 "jpeg": "image/jpeg",
27 "jpg": "image/jpeg",
28 "png": "image/png",
29 "gif": "image/gif",
34 "jpg": "image/jpeg",
35 "png": "image/png",
36 "gif": "image/gif",
30 37 "webp": "image/webp",
31 "bmp": "image/bmp",
32 "tiff": "image/tiff",
33 "tif": "image/tiff",
34 "ico": "image/x-icon",
35 "svg": "image/svg+xml",
36 "avif": "image/avif",
37 "heic": "image/heif",
38 38 # Audio
39 "wav": "audio/wav",
40 "mp3": "audio/mpeg",
39 "wav": "audio/wav",
40 "mp3": "audio/mpeg",
41 41 "flac": "audio/flac",
42 42 "opus": "audio/opus",
43 "ogg": "audio/ogg",
44 "m4a": "audio/mp4", # was "audio/m4a" — non-standard
45 # Video
46 "mkv": "video/x-matroska",
43 "ogg": "audio/ogg",
44 "m4a": "audio/m4a",
45 # Video
46 "mkv": "video/x-matroska",
47 47 "webm": "video/webm",
48 "mp4": "video/mp4",
48 "mp4": "video/mp4",
49 "mov": "video/quicktime",
50 "avi": "video/x-msvideo",
51 "ogv": "video/ogg",
52 "mpg": "video/mpeg",
53 "mpeg": "video/mpeg",
49 54 }
50 55
51 56 MEDIA_TYPE_MAP: dict[str, str] = {value: key for key, value in EXTENSIONS_MAP.items()}
52 57 MEDIA_TYPE_MAP["audio/webm"] = "webm"
53 # Handle duplicate image/jpeg (jpg wins over jpeg as the "canonical" extension)
54 MEDIA_TYPE_MAP["image/jpeg"] = "jpg"
55
56 # Audio formats accepted for input (OpenAI-compatible)
57 ACCEPTED_AUDIO_FORMATS = {"wav", "mp3", "flac", "ogg", "opus", "m4a"}
58
59 58
60 59 def to_image(image: ImageType, is_svg: bool = False) -> Image.Image:
61 60 """
62 61 Converts the input image to a PIL Image object.
62
63 Args:
64 image (Union[str, bytes, Image]): The input image.
65
66 Returns:
67 Image: The converted PIL Image object.
63 68 """
64 69 if not has_requirements:
65 70 raise MissingRequirementsError('Install "pillow" package for images')
@@ -89,17 +94,21 @@ def to_image(image: ImageType, is_svg: bool = False) -> Image.Image:
89 94
90 95 return image
91 96
92
93 97 def get_extension(filename: str) -> Optional[str]:
94 98 if '.' in filename:
95 99 ext = os.path.splitext(filename)[1].lower().lstrip('.')
96 100 return ext if ext in EXTENSIONS_MAP else None
97 101 return None
98 102
99
100 103 def is_allowed_extension(filename: str) -> Optional[str]:
101 104 """
102 Returns the MIME type for allowed extensions, or None.
105 Checks if the given filename has an allowed extension.
106
107 Args:
108 filename (str): The filename to check.
109
110 Returns:
111 bool: True if the extension is allowed, False otherwise.
103 112 """
104 113 extension = get_extension(filename)
105 114 if extension is None:
@@ -107,6 +116,44 @@ def is_allowed_extension(filename: str) -> Optional[str]:
107 116 return EXTENSIONS_MAP[extension]
108 117
109 118
119 def is_safe_url(url: str) -> bool:
120 """Return True only for http/https URLs that do not point to private/loopback/reserved addresses."""
121 try:
122 parsed = urlparse(url)
123
124 if parsed.scheme not in ("http", "https"):
125 return False
126
127 if "\\" in url:
128 return False
129
130 hostname = parsed.hostname
131 if hostname is None:
132 return False
133
134 if urllib3_parse_url is not None:
135 parsed_urllib3 = urllib3_parse_url(url)
136 if parsed_urllib3.host and parsed_urllib3.host != hostname:
137 return False
138 hostname = parsed_urllib3.host or hostname
139
140 if hostname is None:
141 return False
142
143 addr_infos = socket.getaddrinfo(hostname, None)
144 if not addr_infos:
145 return False
146
147 for addr_info in addr_infos:
148 addr = ipaddress.ip_address(addr_info[4][0])
149 if (addr.is_private or addr.is_loopback or addr.is_link_local
150 or addr.is_reserved or addr.is_multicast or addr.is_unspecified):
151 return False
152 except Exception:
153 return False
154 return True
155
156
110 157 def is_data_an_media(data, filename: str = None) -> str:
111 158 content_type = is_data_an_audio(data, filename)
112 159 if content_type is not None:
@@ -121,7 +168,6 @@ def is_data_an_media(data, filename: str = None) -> str:
121 168 return "binary/octet-stream"
122 169 return is_data_uri_an_image(data)
123 170
124
125 171 def is_valid_media(data: ImageType = None, filename: str = None) -> str:
126 172 if is_valid_audio(data, filename):
127 173 return True
@@ -137,8 +183,7 @@ def is_valid_media(data: ImageType = None, filename: str = None) -> str:
137 183 return is_accepted_format(data)
138 184 return is_data_uri_an_image(data)
139 185
140
141 def is_data_an_audio(data_uri: str = None, filename: str = None) -> Optional[str]:
186 def is_data_an_audio(data_uri: str = None, filename: str = None) -> str:
142 187 if filename:
143 188 extension = get_extension(filename)
144 189 if extension is not None:
@@ -146,48 +191,49 @@ def is_data_an_audio(data_uri: str = None, filename: str = None) -> Optional[str
146 191 if media_type.startswith("audio/"):
147 192 return media_type
148 193 if isinstance(data_uri, str):
149 audio_format = re.match(r'^data:(audio/[\w+-]+);base64,', data_uri)
194 audio_format = re.match(r'^data:(audio/\w+);base64,', data_uri)
150 195 if audio_format:
151 196 return audio_format.group(1)
152 return None
153
154 197
155 198 def is_valid_audio(data_uri: str = None, filename: str = None) -> bool:
156 """
157 Returns True if the media is a supported audio format.
158 Accepted: wav, mp3, flac, ogg, opus, m4a
159 """
160 199 mimetype = is_data_an_audio(data_uri, filename)
161 200 if mimetype is None:
162 201 return False
163 ext = MEDIA_TYPE_MAP.get(mimetype)
164 return ext in ACCEPTED_AUDIO_FORMATS
165
202 if MEDIA_TYPE_MAP.get(mimetype) not in ("wav", "mp3"):
203 return False
204 return True
166 205
167 206 def is_data_uri_an_image(data_uri: str) -> bool:
168 207 """
169 208 Checks if the given data URI represents an image.
170 209
210 Args:
211 data_uri (str): The data URI to check.
212
171 213 Raises:
172 214 ValueError: If the data URI is invalid or the image format is not allowed.
173 215 """
174 216 if data_uri.startswith("https:") or data_uri.startswith("http:"):
175 217 return True
176 if not re.match(r'data:image/[\w+]+;base64,', data_uri):
218 # Check if the data URI starts with 'data:image' and contains an image format (e.g., jpeg, png, gif)
219 if not re.match(r'data:image/(\w+);base64,', data_uri):
177 220 raise ValueError(f"Invalid data URI image. {data_uri[:10]}...")
178 image_format = re.match(r'data:image/([\w+]+);base64,', data_uri).group(1).lower()
221 # Extract the image format from the data URI
222 image_format = re.match(r'data:image/(\w+);base64,', data_uri).group(1).lower()
223 # Check if the image format is one of the allowed formats (jpg, jpeg, png, gif)
179 224 if image_format not in EXTENSIONS_MAP and image_format != "svg+xml":
180 225 raise ValueError("Invalid image format (from mime file type).")
181 226 return True
182 227
183
184 228 def is_accepted_format(binary_data: bytes) -> str:
185 229 """
186 230 Checks if the given binary data represents an image with an accepted format.
187 Returns the MIME type string.
231
232 Args:
233 binary_data (bytes): The binary data to check.
188 234
189 235 Raises:
190 ValueError: If the image format is not recognized.
236 ValueError: If the image format is not allowed.
191 237 """
192 238 if binary_data.startswith(b'\xFF\xD8\xFF'):
193 239 return "image/jpeg"
@@ -205,16 +251,21 @@ def is_accepted_format(binary_data: bytes) -> str:
205 251 raise ValueError("Invalid image format (from magic code).")
206 252
207 253
208 def detect_file_type(binary_data: bytes) -> tuple[str, str]:
254
255 def detect_file_type(binary_data: bytes) -> tuple[str, str] | None:
209 256 """
210 257 Detect file type from magic number / header signature.
211 258
259 Args:
260 binary_data (bytes): File binary data
261
212 262 Returns:
213 263 tuple: (extension, MIME type)
214 264
215 265 Raises:
216 ValueError: If file type is unknown or unsupported.
266 ValueError: If file type is unknown
217 267 """
268
218 269 # ---- Images ----
219 270 if binary_data.startswith(b"\xff\xd8\xff"):
220 271 return ".jpg", "image/jpeg"
@@ -233,14 +284,12 @@ def detect_file_type(binary_data: bytes) -> tuple[str, str]:
233 284 elif binary_data.startswith(b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"):
234 285 return ".jp2", "image/jp2"
235 286 elif len(binary_data) > 12 and binary_data[4:8] == b"ftyp":
236 # ISO Base Media File Format — branch on brand to distinguish mp4/heic/avif
237 287 brand = binary_data[8:12]
238 if brand in (b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1"):
288 if brand in [b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1"]:
239 289 return ".heic", "image/heif"
240 elif brand == b"avif":
290 elif brand in [b"avif"]:
241 291 return ".avif", "image/avif"
242 292 else:
243 # Default to MP4 for any other ftyp brand (isom, mp41, mp42, M4V, ...)
244 293 return ".mp4", "video/mp4"
245 294 elif binary_data.lstrip().startswith((b"<?xml", b"<svg")):
246 295 return ".svg", "image/svg+xml"
@@ -249,8 +298,8 @@ def detect_file_type(binary_data: bytes) -> tuple[str, str]:
249 298 elif binary_data.startswith(b"%PDF"):
250 299 return ".pdf", "application/pdf"
251 300 elif binary_data.startswith(b"PK\x03\x04"):
252 # Could be docx/xlsx/pptx/jar/apk/odt — use generic zip
253 return ".zip", "application/zip"
301 return ".zip", "application/zip-based"
302 # could be docx/xlsx/pptx/jar/apk/odt
254 303 elif binary_data.startswith(b"\xd0\xcf\x11\xe0"):
255 304 return ".doc", "application/vnd.ms-office"
256 305 elif binary_data.startswith(b"{\\rtf"):
@@ -271,39 +320,37 @@ def detect_file_type(binary_data: bytes) -> tuple[str, str]:
271 320 return ".exe", "application/x-msdownload"
272 321 elif binary_data.startswith(b"\x7fELF"):
273 322 return ".elf", "application/x-elf"
274 elif binary_data.startswith(b"\xca\xfe\xba\xbe") or binary_data.startswith(b"\xca\xfe\xd0\x0d"):
323 elif binary_data.startswith(b"\xca\xfe\xba\xbe") or binary_data.startswith(
324 b"\xca\xfe\xd0\x0d"
325 ):
275 326 return ".class", "application/java-vm"
276 elif binary_data.startswith(b"\x50\x4b\x03\x04") and b"META-INF" in binary_data[:200]:
327 elif (
328 binary_data.startswith(b"\x50\x4b\x03\x04")
329 and b"META-INF" in binary_data[:200]
330 ):
277 331 return ".jar", "application/java-archive"
278 332
279 333 # ---- Audio ----
280 334 elif binary_data.startswith(b"ID3") or binary_data[0:2] == b"\xff\xfb":
281 335 return ".mp3", "audio/mpeg"
336 elif binary_data.startswith(b"OggS"):
337 return ".ogg", "audio/ogg"
282 338 elif binary_data.startswith(b"fLaC"):
283 339 return ".flac", "audio/flac"
284 340 elif binary_data.startswith(b"RIFF") and binary_data[8:12] == b"WAVE":
285 341 return ".wav", "audio/wav"
286 342 elif binary_data.startswith(b"MThd"):
287 343 return ".mid", "audio/midi"
288 elif binary_data.startswith(b"OggS"):
289 # Distinguish Ogg audio from Ogg video by reading the codec header
290 # Vorbis/Opus → audio; Theora → video
291 if b"\x80theora" in binary_data[:64] or b"theora" in binary_data[:64]:
292 return ".ogv", "video/ogg"
293 elif b"\x01vorbis" in binary_data[:64] or b"OpusHead" in binary_data[:64]:
294 return ".ogg", "audio/ogg"
295 else:
296 # Default to audio/ogg for unrecognised Ogg streams
297 return ".ogg", "audio/ogg"
298 344
299 345 # ---- Video ----
346 elif binary_data.startswith(b"\x00\x00\x00") and b"ftyp" in binary_data[4:12]:
347 return ".mp4", "video/mp4"
300 348 elif binary_data.startswith(b"RIFF") and binary_data[8:12] == b"AVI ":
301 349 return ".avi", "video/x-msvideo"
350 elif binary_data.startswith(b"OggS"):
351 return ".ogv", "video/ogg"
302 352 elif binary_data.startswith(b"\x1a\x45\xdf\xa3"):
303 # EBML — could be MKV or WebM; check DocType in first 64 bytes
304 if b"webm" in binary_data[:64]:
305 return ".webm", "video/webm"
306 return ".mkv", "video/x-matroska"
353 return ".mkv", "video/webm"
307 354 elif binary_data.startswith(b"\x00\x00\x01\xba"):
308 355 return ".mpg", "video/mpeg"
309 356
@@ -324,23 +371,39 @@ def detect_file_type(binary_data: bytes) -> tuple[str, str]:
324 371
325 372
326 373 def extract_data_uri(data_uri: str) -> bytes:
327 """Extract binary data from a data URI."""
328 data = data_uri.split(",")[-1]
329 return base64.b64decode(data)
374 """
375 Extracts the binary data from the given data URI.
376
377 Args:
378 data_uri (str): The data URI.
330 379
380 Returns:
381 bytes: The extracted binary data.
382 """
383 data = data_uri.split(",")[-1]
384 data = base64.b64decode(data)
385 return data
331 386
332 def process_image(
333 image: Image.Image,
334 new_width: int = 400,
335 new_height: int = 400,
336 save: str = None
337 ) -> Image.Image:
387 def process_image(image: Image.Image, new_width: int = 400, new_height: int = 400, save: str = None) -> Image.Image:
338 388 """
339 Adjusts orientation, strips transparency, resizes image.
389 Processes the given image by adjusting its orientation and resizing it.
390
391 Args:
392 image (Image): The image to process.
393 new_width (int): The new width of the image.
394 new_height (int): The new height of the image.
395
396 Returns:
397 Image: The processed image.
340 398 """
341 399 image = ImageOps.exif_transpose(image)
400 # Remove transparency
342 401 if image.mode == "RGBA":
343 pass # keep transparency for PNG output
402 image.load()
403 white = Image.new('RGB', image.size, (255, 255, 255))
404 white.paste(image, mask=image.split()[-1])
405 image = white
406 # Convert to RGB for jpg format
344 407 elif image.mode != "RGB":
345 408 image = image.convert("RGB")
346 409 image_size = image.size
@@ -350,10 +413,15 @@ def process_image(
350 413 return image_size
351 414 return image
352 415
353
354 416 def to_bytes(image: ImageType) -> bytes:
355 417 """
356 418 Converts the given image to bytes.
419
420 Args:
421 image (ImageType): The image to convert.
422
423 Returns:
424 bytes: The image as bytes.
357 425 """
358 426 if isinstance(image, bytes):
359 427 return image
@@ -362,17 +430,18 @@ def to_bytes(image: ImageType) -> bytes:
362 430 is_data_uri_an_image(image)
363 431 return extract_data_uri(image)
364 432 elif image.startswith("http://") or image.startswith("https://"):
433 if not is_safe_url(image):
434 raise ValueError("Invalid or disallowed media URL")
365 435 path: str = urlparse(image).path
366 436 if path.startswith("/files/"):
367 local_path = get_bucket_dir(*path.split("/")[2:])
368 if os.path.exists(local_path):
369 return Path(local_path).read_bytes()
437 path = get_bucket_dir(*path.split("/")[2:])
438 if os.path.exists(path):
439 return Path(path).read_bytes()
370 440 else:
371 raise FileNotFoundError(f"File not found: {local_path}")
441 raise FileNotFoundError(f"File not found: {path}")
372 442 else:
373 443 resp = requests.get(image, headers={
374 # Updated to Chrome/145 (current as of Mar 2026)
375 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
444 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
376 445 })
377 446 if resp.ok and is_accepted_format(resp.content):
378 447 return resp.content
@@ -381,11 +450,13 @@ def to_bytes(image: ImageType) -> bytes:
381 450 raise ValueError("Invalid image format. Expected bytes, str, or PIL Image.")
382 451 elif isinstance(image, Image.Image):
383 452 bytes_io = BytesIO()
384 image.save(bytes_io, image.format or "PNG")
385 bytes_io.seek(0) # FIX: was image.seek(0) — PIL Image has no seek()
453 image.save(bytes_io, image.format)
454 image.seek(0)
386 455 return bytes_io.getvalue()
387 elif isinstance(image, (os.PathLike, Path)):
456 elif isinstance(image, os.PathLike):
388 457 return Path(image).read_bytes()
458 elif isinstance(image, Path):
459 return image.read_bytes()
389 460 else:
390 461 try:
391 462 image.seek(0)
@@ -393,7 +464,6 @@ def to_bytes(image: ImageType) -> bytes:
393 464 pass
394 465 return image.read()
395 466
396
397 467 def to_data_uri(image: ImageType, filename: str = None) -> str:
398 468 if not isinstance(image, str):
399 469 data = to_bytes(image)
@@ -401,28 +471,26 @@ def to_data_uri(image: ImageType, filename: str = None) -> str:
401 471 return f"data:{is_data_an_media(data, filename)};base64,{data_base64}"
402 472 return image
403 473
404
405 def to_input_audio(audio: ImageType, filename: str = None) -> dict:
474 def to_input_audio(audio: ImageType, filename: str = None) -> str:
406 475 if not isinstance(audio, str):
407 476 if filename is not None:
408 fmt = get_extension(filename)
409 if fmt is None:
477 format = get_extension(filename)
478 if format is None:
410 479 raise ValueError("Invalid input audio")
411 480 return {
412 481 "data": base64.b64encode(to_bytes(audio)).decode(),
413 "format": fmt
482 "format": format
414 483 }
415 484 raise ValueError("Invalid input audio")
416 match = re.match(r'^data:audio/([\w+-]+);base64,(.+)', audio)
417 if match:
485 audio = re.match(r'^data:audio/(\w+);base64,(.+?)', audio)
486 if audio:
418 487 return {
419 "data": match.group(2),
420 "format": match.group(1).replace("mpeg", "mp3")
488 "data": audio.group(2),
489 "format": audio.group(1).replace("mpeg", "mp3")
421 490 }
422 491 raise ValueError("Invalid input audio")
423 492
424
425 def use_aspect_ratio(extra_body: dict, aspect_ratio: str) -> dict:
493 def use_aspect_ratio(extra_body: dict, aspect_ratio: str) -> Image:
426 494 extra_body = {key: value for key, value in extra_body.items() if value is not None}
427 495 if extra_body.get("width") is None or extra_body.get("height") is None:
428 496 width, height = get_width_height(
@@ -437,40 +505,25 @@ def use_aspect_ratio(extra_body: dict, aspect_ratio: str) -> dict:
437 505 }
438 506 return {key: value for key, value in extra_body.items() if value is not None}
439 507
440
441 508 def get_width_height(
442 509 aspect_ratio: str,
443 510 width: Optional[int] = None,
444 511 height: Optional[int] = None
445 512 ) -> tuple[int, int]:
446 """
447 Returns (width, height) for common aspect ratios.
448 All values are multiples of 64 and ≥ 480px on the short side.
449 """
450 ratio_map = {
451 "1:1": (1024, 1024),
452 "16:9": (1024, 576),
453 "9:16": (576, 1024),
454 "4:3": (1024, 768),
455 "3:4": (768, 1024),
456 "3:2": (1024, 682),
457 "2:3": (682, 1024),
458 "21:9": (1024, 440),
459 "9:21": (440, 1024),
460 "4:5": (832, 1040),
461 "5:4": (1040, 832),
462 "2:1": (1024, 512),
463 "1:2": (512, 1024),
464 }
465 if aspect_ratio in ratio_map:
466 default_w, default_h = ratio_map[aspect_ratio]
467 return width or default_w, height or default_h
513 if aspect_ratio == "1:1":
514 return width or 1024, height or 1024
515 elif aspect_ratio == "16:9":
516 return width or 832, height or 480
517 elif aspect_ratio == "9:16":
518 return width or 480, height or 832,
468 519 return width, height
469 520
470
471 521 class ImageRequest:
472 def __init__(self, options: dict = {}):
522 def __init__(
523 self,
524 options: dict = {}
525 ):
473 526 self.options = options
474 527
475 528 def get(self, key: str):
476 return self.options.get(key)
529 return self.options.get(key)
Modified g4f/image/copy_images.py +1 -1
@@ -247,4 +247,4 @@ async def copy_media(
247 247 os.unlink(target_path)
248 248 return image
249 249
250 return await asyncio.gather(*[copy_image(image, target) for image in images])
250 return await asyncio.gather(*[copy_image(image, target) for image in images])