Add aiohttp_socks to requirements

Fix preview for uploaded and generated images in gui
Improve typing, readme
This commit is contained in:
Heiner Lohaus 2024-01-29 18:14:46 +01:00
parent 3bb6560fce
commit a28bab9387
14 changed files with 148 additions and 80 deletions

View File

@ -99,8 +99,29 @@ or set the api base in your client to: [http://localhost:1337/v1](http://localho
##### Install using pypi:
Install all supported tools / all used packages:
```
pip install -U "g4f[all]"
pip install -U g4f[all]
```
Install packages for uploading / generating images:
```
pip install -U g4f[image]
```
Install the packages required for providers with webdriver:
```
pip install -U g4f[webdriver]
```
Install the packages required for the OpenaiChat provider:
```
pip install -U g4f[openai]
```
Install the packages required for the interference api:
```
pip install -U g4f[api]
```
Install the packages required for the web gui:
```
pip install -U g4f[gui]
```
##### or:
@ -202,8 +223,9 @@ docker-compose down
### The Web UI
To use it in the web interface, type the following codes in the command line.
```python3
To start the web interface, type the following codes in the command line.
```python
from g4f.gui import run_gui
run_gui()
```
@ -283,6 +305,33 @@ for message in response:
print(message)
```
##### Cookies / Access Token
For generating images with Bing and for the OpenAi Chat you need cookies or a token from your browser session. From Bing you need the "_U" cookie and from OpenAI you need the "access_token". You can pass the cookies / the access token in the create function or you use the `set_cookies` setter:
```python
from g4f import set_cookies
set_cookies(".bing", {
"_U": "cookie value"
})
set_cookies("chat.openai.com", {
"access_token": "token value"
})
from g4f.gui import run_gui
run_gui()
```
Alternatively, g4f reads the cookies with “browser_cookie3” from your browser
or it starts a browser instance with selenium "webdriver" for logging in.
If you use the pip package, you have to install “browser_cookie3” or "webdriver" by yourself.
```bash
pip install browser_cookie3
pip install g4f[webdriver]
```
##### Using Browser
Some providers using a browser to bypass the bot protection. They using the selenium webdriver to control the browser. The browser settings and the login data are saved in a custom directory. If the headless mode is enabled, the browser windows are loaded invisibly. For performance reasons, it is recommended to reuse the browser instances and close them yourself at the end:

View File

@ -86,6 +86,5 @@ RUN pip install --upgrade pip && pip install -r requirements.txt
# Copy the entire package into the container.
ADD --chown=$G4F_USER:$G4F_USER g4f $G4F_DIR/g4f
# Expose ports
EXPOSE 8080 1337
EXPOSE 8080 1337

View File

@ -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

View File

@ -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,

View File

@ -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)
return ImageRequest(result)

View File

@ -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()

13
g4f/defaults.py Normal file
View File

@ -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'
}

View File

@ -59,6 +59,10 @@ const handle_ask = async () => {
</div>
<div class="content" id="user_${token}">
${markdown_render(message)}
${imageInput.dataset.src
? '<img src="' + imageInput.dataset.src + '" alt="Image upload">'
: ''
}
</div>
</div>
`;
@ -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 = '';

View File

@ -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 = "<!-- generated images start -->\n"
end_flag = "<!-- generated images end -->\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
class ImageRequest:
def __init__(
self,
options: dict = {}
):
self.options = options
def get(self, key: str):
return self.options.get(key)

View File

@ -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')

View File

@ -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__(

View File

@ -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'
]

View File

@ -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"))

View File

@ -19,4 +19,5 @@ async-property
undetected-chromedriver
brotli
beautifulsoup4
setuptools
setuptools
aiohttp_socks