From a28bab938704a15c825c1b45a8983c72e8c90ace Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Mon, 29 Jan 2024 18:14:46 +0100 Subject: Add aiohttp_socks to requirements Fix preview for uploaded and generated images in gui Improve typing, readme --- g4f/Provider/Bing.py | 4 +--- g4f/Provider/bing/create_images.py | 9 ++++---- g4f/Provider/bing/upload_image.py | 2 +- g4f/Provider/needs_auth/OpenaiChat.py | 31 ++++++++++++++-------------- g4f/defaults.py | 13 ++++++++++++ g4f/gui/client/js/chat.v1.js | 16 ++++++++++++++ g4f/image.py | 39 +++++++++++++++++++++-------------- g4f/requests.py | 24 +++++++-------------- g4f/requests_aiohttp.py | 13 ++---------- g4f/typing.py | 7 +++++-- g4f/webdriver.py | 9 ++++---- 11 files changed, 93 insertions(+), 74 deletions(-) create mode 100644 g4f/defaults.py (limited to 'g4f') diff --git a/g4f/Provider/Bing.py b/g4f/Provider/Bing.py index 32879fa6..40a42bf5 100644 --- a/g4f/Provider/Bing.py +++ b/g4f/Provider/Bing.py @@ -288,8 +288,6 @@ async def stream_generate( ) as session: conversation = await create_conversation(session) image_request = await upload_image(session, image, tone) if image else None - if image_request: - yield image_request try: async with session.ws_connect( @@ -327,7 +325,7 @@ async def stream_generate( elif message.get('contentType') == "IMAGE": prompt = message.get('text') try: - image_response = ImageResponse(await create_images(session, prompt), prompt) + image_response = ImageResponse(await create_images(session, prompt), prompt, {"preview": "{image}?w=200&h=200"}) except: response_txt += f"\nhttps://www.bing.com/images/create?q={parse.quote(prompt)}" final = True diff --git a/g4f/Provider/bing/create_images.py b/g4f/Provider/bing/create_images.py index a3fcd91b..e1031e61 100644 --- a/g4f/Provider/bing/create_images.py +++ b/g4f/Provider/bing/create_images.py @@ -187,11 +187,11 @@ def get_cookies_from_browser(proxy: str = None) -> dict[str, str]: class CreateImagesBing: """A class for creating images using Bing.""" - + def __init__(self, cookies: dict[str, str] = {}, proxy: str = None) -> None: self.cookies = cookies self.proxy = proxy - + def create_completion(self, prompt: str) -> Generator[ImageResponse, None, None]: """ Generator for creating imagecompletion based on a prompt. @@ -229,9 +229,7 @@ class CreateImagesBing: proxy = os.environ.get("G4F_PROXY") async with create_session(cookies, proxy) as session: images = await create_images(session, prompt, self.proxy) - return ImageResponse(images, prompt) - -service = CreateImagesBing() + return ImageResponse(images, prompt, {"preview": "{image}?w=200&h=200"}) def patch_provider(provider: ProviderType) -> CreateImagesProvider: """ @@ -243,6 +241,7 @@ def patch_provider(provider: ProviderType) -> CreateImagesProvider: Returns: CreateImagesProvider: The patched provider with image creation capabilities. """ + service = CreateImagesBing() return CreateImagesProvider( provider, service.create_completion, diff --git a/g4f/Provider/bing/upload_image.py b/g4f/Provider/bing/upload_image.py index f9e11561..6d51aba0 100644 --- a/g4f/Provider/bing/upload_image.py +++ b/g4f/Provider/bing/upload_image.py @@ -149,4 +149,4 @@ def parse_image_response(response: dict) -> ImageRequest: if IMAGE_CONFIG["enableFaceBlurDebug"] else f"https://www.bing.com/images/blob?bcid={result['bcid']}" ) - return ImageRequest(result["imageUrl"], "", result) \ No newline at end of file + return ImageRequest(result) \ No newline at end of file diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index 60a101d7..253d4f77 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -150,8 +150,8 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): headers=headers ) as response: response.raise_for_status() - download_url = (await response.json())["download_url"] - return ImageRequest(download_url, image_data["file_name"], image_data) + image_data["download_url"] = (await response.json())["download_url"] + return ImageRequest(image_data) @classmethod async def get_default_model(cls, session: StreamSession, headers: dict): @@ -175,7 +175,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): return cls.default_model @classmethod - def create_messages(cls, prompt: str, image_response: ImageRequest = None): + def create_messages(cls, prompt: str, image_request: ImageRequest = None): """ Create a list of messages for the user input @@ -187,7 +187,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): A list of messages with the user input and the image, if any """ # Check if there is an image response - if not image_response: + if not image_request: # Create a content object with the text type and the prompt content = {"content_type": "text", "parts": [prompt]} else: @@ -195,10 +195,10 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): content = { "content_type": "multimodal_text", "parts": [{ - "asset_pointer": f"file-service://{image_response.get('file_id')}", - "height": image_response.get("height"), - "size_bytes": image_response.get("file_size"), - "width": image_response.get("width"), + "asset_pointer": f"file-service://{image_request.get('file_id')}", + "height": image_request.get("height"), + "size_bytes": image_request.get("file_size"), + "width": image_request.get("width"), }, prompt] } # Create a message object with the user role and the content @@ -208,16 +208,16 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): "content": content, }] # Check if there is an image response - if image_response: + if image_request: # Add the metadata object with the attachments messages[0]["metadata"] = { "attachments": [{ - "height": image_response.get("height"), - "id": image_response.get("file_id"), - "mimeType": image_response.get("mime_type"), - "name": image_response.get("file_name"), - "size": image_response.get("file_size"), - "width": image_response.get("width"), + "height": image_request.get("height"), + "id": image_request.get("file_id"), + "mimeType": image_request.get("mime_type"), + "name": image_request.get("file_name"), + "size": image_request.get("file_size"), + "width": image_request.get("width"), }] } return messages @@ -352,7 +352,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): image_response = None if image: image_response = await cls.upload_image(session, headers, image) - yield image_response except Exception as e: yield e end_turn = EndTurn() diff --git a/g4f/defaults.py b/g4f/defaults.py new file mode 100644 index 00000000..6ae6d7eb --- /dev/null +++ b/g4f/defaults.py @@ -0,0 +1,13 @@ +DEFAULT_HEADERS = { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-US', + 'Connection': 'keep-alive', + 'Sec-Ch-Ua': '"Not A(Brand";v="99", "Google Chrome";v="121", "Chromium";v="121"', + 'Sec-Ch-Ua-Mobile': '?0', + 'Sec-Ch-Ua-Platform': '"Windows"', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-site', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36' +} \ No newline at end of file diff --git a/g4f/gui/client/js/chat.v1.js b/g4f/gui/client/js/chat.v1.js index 99a75569..86eef8c9 100644 --- a/g4f/gui/client/js/chat.v1.js +++ b/g4f/gui/client/js/chat.v1.js @@ -59,6 +59,10 @@ const handle_ask = async () => {
${markdown_render(message)} + ${imageInput.dataset.src + ? 'Image upload' + : '' + }
`; @@ -666,6 +670,18 @@ observer.observe(message_input, { attributes: true }); })() imageInput.addEventListener('click', async (event) => { imageInput.value = ''; + delete imageInput.dataset.src; +}); +imageInput.addEventListener('change', async (event) => { + if (imageInput.files.length) { + const reader = new FileReader(); + reader.addEventListener('load', (event) => { + imageInput.dataset.src = event.target.result; + }); + reader.readAsDataURL(imageInput.files[0]); + } else { + delete imageInput.dataset.src; + } }); fileInput.addEventListener('click', async (event) => { fileInput.value = ''; diff --git a/g4f/image.py b/g4f/image.py index 68767155..1a4692b3 100644 --- a/g4f/image.py +++ b/g4f/image.py @@ -3,14 +3,13 @@ from __future__ import annotations import re from io import BytesIO import base64 -from .typing import ImageType, Union +from .typing import ImageType, Union, Image try: - from PIL.Image import open as open_image, new as new_image, Image + from PIL.Image import open as open_image, new as new_image from PIL.Image import FLIP_LEFT_RIGHT, ROTATE_180, ROTATE_270, ROTATE_90 has_requirements = True except ImportError: - Image = type has_requirements = False from .errors import MissingRequirementsError @@ -29,6 +28,9 @@ def to_image(image: ImageType, is_svg: bool = False) -> Image: """ if not has_requirements: raise MissingRequirementsError('Install "pillow" package for images') + if isinstance(image, str): + is_data_uri_an_image(image) + image = extract_data_uri(image) if is_svg: try: import cairosvg @@ -39,9 +41,6 @@ def to_image(image: ImageType, is_svg: bool = False) -> Image: buffer = BytesIO() cairosvg.svg2png(image, write_to=buffer) return open_image(buffer) - if isinstance(image, str): - is_data_uri_an_image(image) - image = extract_data_uri(image) if isinstance(image, bytes): is_accepted_format(image) return open_image(BytesIO(image)) @@ -79,9 +78,9 @@ def is_data_uri_an_image(data_uri: str) -> bool: if not re.match(r'data:image/(\w+);base64,', data_uri): raise ValueError("Invalid data URI image.") # Extract the image format from the data URI - image_format = re.match(r'data:image/(\w+);base64,', data_uri).group(1) + image_format = re.match(r'data:image/(\w+);base64,', data_uri).group(1).lower() # Check if the image format is one of the allowed formats (jpg, jpeg, png, gif) - if image_format.lower() not in ALLOWED_EXTENSIONS: + if image_format not in ALLOWED_EXTENSIONS and image_format != "svg+xml": raise ValueError("Invalid image format (from mime file type).") def is_accepted_format(binary_data: bytes) -> bool: @@ -187,7 +186,7 @@ def to_base64_jpg(image: Image, compression_rate: float) -> str: image.save(output_buffer, format="JPEG", quality=int(compression_rate * 100)) return base64.b64encode(output_buffer.getvalue()).decode() -def format_images_markdown(images, alt: str, preview: str="{image}?w=200&h=200") -> str: +def format_images_markdown(images, alt: str, preview: str = None) -> str: """ Formats the given images as a markdown string. @@ -200,9 +199,12 @@ def format_images_markdown(images, alt: str, preview: str="{image}?w=200&h=200") str: The formatted markdown string. """ if isinstance(images, str): - images = f"[![{alt}]({preview.replace('{image}', images)})]({images})" + images = f"[![{alt}]({preview.replace('{image}', images) if preview else images})]({images})" else: - images = [f"[![#{idx+1} {alt}]({preview.replace('{image}', image)})]({image})" for idx, image in enumerate(images)] + images = [ + f"[![#{idx+1} {alt}]({preview.replace('{image}', image) if preview else image})]({image})" + for idx, image in enumerate(images) + ] images = "\n".join(images) start_flag = "\n" end_flag = "\n" @@ -223,7 +225,7 @@ def to_bytes(image: Image) -> bytes: image.seek(0) return bytes_io.getvalue() -class ImageResponse(): +class ImageResponse: def __init__( self, images: Union[str, list], @@ -235,10 +237,17 @@ class ImageResponse(): self.options = options def __str__(self) -> str: - return format_images_markdown(self.images, self.alt) + return format_images_markdown(self.images, self.alt, self.get("preview")) def get(self, key: str): return self.options.get(key) -class ImageRequest(ImageResponse): - pass \ No newline at end of file +class ImageRequest: + def __init__( + self, + options: dict = {} + ): + self.options = options + + def get(self, key: str): + return self.options.get(key) \ No newline at end of file diff --git a/g4f/requests.py b/g4f/requests.py index 275e108b..d7b5996b 100644 --- a/g4f/requests.py +++ b/g4f/requests.py @@ -7,13 +7,13 @@ try: from .requests_curl_cffi import StreamResponse, StreamSession has_curl_cffi = True except ImportError: - Session = type + from typing import Type as Session from .requests_aiohttp import StreamResponse, StreamSession has_curl_cffi = False from .webdriver import WebDriver, WebDriverSession, bypass_cloudflare, get_driver_cookies from .errors import MissingRequirementsError - +from .defaults import DEFAULT_HEADERS def get_args_from_browser(url: str, webdriver: WebDriver = None, proxy: str = None, timeout: int = 120) -> dict: """ @@ -36,22 +36,14 @@ def get_args_from_browser(url: str, webdriver: WebDriver = None, proxy: str = No return { 'cookies': cookies, 'headers': { - 'accept': '*/*', - "accept-language": "en-US", - "accept-encoding": "gzip, deflate, br", - 'authority': parse.netloc, - 'origin': f'{parse.scheme}://{parse.netloc}', - 'referer': url, - "sec-ch-ua": "\"Google Chrome\";v=\"121\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"121\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "Windows", - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'same-origin', - 'user-agent': user_agent, + **DEFAULT_HEADERS, + 'Authority': parse.netloc, + 'Origin': f'{parse.scheme}://{parse.netloc}', + 'Referer': url, + 'User-Agent': user_agent, }, } - + def get_session_from_browser(url: str, webdriver: WebDriver = None, proxy: str = None, timeout: int = 120) -> Session: if not has_curl_cffi: raise MissingRequirementsError('Install "curl_cffi" package') diff --git a/g4f/requests_aiohttp.py b/g4f/requests_aiohttp.py index aa097312..0da8973b 100644 --- a/g4f/requests_aiohttp.py +++ b/g4f/requests_aiohttp.py @@ -4,6 +4,7 @@ from aiohttp import ClientSession, ClientResponse, ClientTimeout from typing import AsyncGenerator, Any from .Provider.helper import get_connector +from .defaults import DEFAULT_HEADERS class StreamResponse(ClientResponse): async def iter_lines(self) -> AsyncGenerator[bytes, None]: @@ -17,17 +18,7 @@ class StreamSession(ClientSession): def __init__(self, headers: dict = {}, timeout: int = None, proxies: dict = {}, impersonate = None, **kwargs): if impersonate: headers = { - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'en-US', - 'Connection': 'keep-alive', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-site', - "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'sec-ch-ua': '"Google Chrome";v="107", "Chromium";v="107", "Not?A_Brand";v="24"', - 'sec-ch-ua-mobile': '?0', - 'sec-ch-ua-platform': '"Windows"', + **DEFAULT_HEADERS, **headers } super().__init__( diff --git a/g4f/typing.py b/g4f/typing.py index c5a981bd..386b3dfc 100644 --- a/g4f/typing.py +++ b/g4f/typing.py @@ -1,9 +1,10 @@ import sys from typing import Any, AsyncGenerator, Generator, NewType, Tuple, Union, List, Dict, Type, IO, Optional + try: from PIL.Image import Image except ImportError: - Image = type + from typing import Type as Image if sys.version_info >= (3, 8): from typing import TypedDict @@ -14,7 +15,7 @@ SHA256 = NewType('sha_256_hash', str) CreateResult = Generator[str, None, None] AsyncResult = AsyncGenerator[str, None] Messages = List[Dict[str, str]] -Cookies = List[Dict[str, str]] +Cookies = Dict[str, str] ImageType = Union[str, bytes, IO, Image, None] __all__ = [ @@ -33,5 +34,7 @@ __all__ = [ 'CreateResult', 'AsyncResult', 'Messages', + 'Cookies', + 'Image', 'ImageType' ] diff --git a/g4f/webdriver.py b/g4f/webdriver.py index 44765402..d28cd97b 100644 --- a/g4f/webdriver.py +++ b/g4f/webdriver.py @@ -18,6 +18,7 @@ import time from shutil import which from os import path from os import access, R_OK +from .typing import Cookies from .errors import MissingRequirementsError from . import debug @@ -56,9 +57,7 @@ def get_browser( if proxy: options.add_argument(f'--proxy-server={proxy}') # Check for system driver in docker - driver = which('chromedriver') - if not driver: - driver = '/usr/bin/chromedriver' + driver = which('chromedriver') or '/usr/bin/chromedriver' if not path.isfile(driver) or not access(driver, R_OK): driver = None return Chrome( @@ -68,7 +67,7 @@ def get_browser( headless=headless ) -def get_driver_cookies(driver: WebDriver) -> dict: +def get_driver_cookies(driver: WebDriver) -> Cookies: """ Retrieves cookies from the specified WebDriver. @@ -115,8 +114,8 @@ def bypass_cloudflare(driver: WebDriver, url: str, timeout: int) -> None: driver.switch_to.window(window_handle) break + # Click on the challenge button in the iframe try: - # Click on the challenge button in the iframe driver.switch_to.frame(driver.find_element(By.CSS_SELECTOR, "#turnstile-wrapper iframe")) WebDriverWait(driver, 5).until( EC.presence_of_element_located((By.CSS_SELECTOR, "#challenge-stage input")) -- cgit v1.2.3