From ea1448001df9c66f05a52618eb3e40a88df5efe8 Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Sun, 17 Nov 2024 11:06:37 +0100 Subject: [PATCH] Add nodriver to Gemini provider, Add slim docker image with google-chrome usage, Add the new docker images to publish worklow, Update requirements.txt and pip requirements --- .github/workflows/publish-workflow.yaml | 12 ++++ docker-compose-slim.yml | 25 ++++++++ docker/Dockerfile | 1 + docker/Dockerfile-slim | 68 ++++++++++++++++++++ docker/supervisor-api.conf | 12 ++++ docker/supervisor-gui.conf | 2 +- docker/supervisor.conf | 15 +---- docs/docker.md | 24 +++++-- g4f/Provider/Cloudflare.py | 15 ++++- g4f/Provider/HuggingChat.py | 34 +++------- g4f/Provider/needs_auth/Gemini.py | 80 ++++++++---------------- g4f/Provider/needs_auth/GeminiPro.py | 4 +- g4f/Provider/needs_auth/HuggingFace.py | 29 +++------ g4f/Provider/needs_auth/MetaAI.py | 3 +- g4f/Provider/needs_auth/MetaAIAccount.py | 2 +- g4f/Provider/needs_auth/__init__.py | 1 + g4f/gui/server/api.py | 32 +++++----- g4f/image.py | 2 +- requirements-slim.txt | 16 +++++ requirements.txt | 4 +- setup.py | 4 ++ 21 files changed, 240 insertions(+), 145 deletions(-) create mode 100644 docker-compose-slim.yml create mode 100644 docker/Dockerfile-slim create mode 100755 docker/supervisor-api.conf create mode 100644 requirements-slim.txt diff --git a/.github/workflows/publish-workflow.yaml b/.github/workflows/publish-workflow.yaml index bfc0b735..2f3624d1 100644 --- a/.github/workflows/publish-workflow.yaml +++ b/.github/workflows/publish-workflow.yaml @@ -48,3 +48,15 @@ jobs: labels: ${{ steps.metadata.outputs.labels }} build-args: | G4F_VERSION=${{ github.ref_name }} + - name: Build and push slim image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile-slim + push: true + tags: | + hlohaus789/g4f=slim + hlohaus789/g4f=${{ github.ref_name }}-slim + labels: ${{ steps.metadata.outputs.labels }} + build-args: | + G4F_VERSION=${{ github.ref_name }} \ No newline at end of file diff --git a/docker-compose-slim.yml b/docker-compose-slim.yml new file mode 100644 index 00000000..ec0ee0fc --- /dev/null +++ b/docker-compose-slim.yml @@ -0,0 +1,25 @@ +version: '3' + +services: + g4f-gui: + container_name: g4f-gui + image: hlohaus789/g4f:slim + build: + context: . + dockerfile: docker/Dockerfile-slim + command: python -m g4f.cli gui -debug + volumes: + - .:/app + ports: + - '8080:8080' + g4f-api: + container_name: g4f-api + image: hlohaus789/g4f:slim + build: + context: . + dockerfile: docker/Dockerfile-slim + command: python -m g4f.cli api + volumes: + - .:/app + ports: + - '1337:1337' \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index eb03390c..625312e2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -40,6 +40,7 @@ RUN apt-get -qqy update \ # Update entrypoint COPY docker/supervisor.conf /etc/supervisor/conf.d/selenium.conf +COPY docker/supervisor-api.conf /etc/supervisor/conf.d/api.conf COPY docker/supervisor-gui.conf /etc/supervisor/conf.d/gui.conf # If no gui diff --git a/docker/Dockerfile-slim b/docker/Dockerfile-slim new file mode 100644 index 00000000..6c0afc64 --- /dev/null +++ b/docker/Dockerfile-slim @@ -0,0 +1,68 @@ +FROM python:bookworm + +ARG G4F_VERSION +ARG G4F_USER=g4f +ARG G4F_USER_ID=1000 +ARG PYDANTIC_VERSION=1.8.1 + +ENV G4F_VERSION $G4F_VERSION +ENV G4F_USER $G4F_USER +ENV G4F_USER_ID $G4F_USER_ID +ENV G4F_DIR /app + +RUN apt-get update && apt-get upgrade -y \ + && apt-get install -y git \ + && apt-get install --quiet --yes --no-install-recommends \ + build-essential \ +# Add user and user group + && groupadd -g $G4F_USER_ID $G4F_USER \ + && useradd -rm -G sudo -u $G4F_USER_ID -g $G4F_USER_ID $G4F_USER \ + && mkdir -p /var/log/supervisor \ + && chown "${G4F_USER_ID}:${G4F_USER_ID}" /var/log/supervisor \ + && echo "${G4F_USER}:${G4F_USER}" | chpasswd + +USER $G4F_USER_ID +WORKDIR $G4F_DIR + +ENV HOME /home/$G4F_USER +ENV PATH "${HOME}/.local/bin:${HOME}/.cargo/bin:${PATH}" + +# Create app dir and copy the project's requirements file into it +RUN mkdir -p $G4F_DIR +COPY requirements-slim.txt $G4F_DIR + +# Install rust toolchain +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y + +# Upgrade pip for the latest features and install the project's Python dependencies. +RUN python -m pip install --upgrade pip \ + && pip install --no-cache-dir \ + Cython==0.29.22 \ + setuptools \ + # Install PyDantic + && pip install \ + -vvv \ + --no-cache-dir \ + --no-binary pydantic \ + --global-option=build_ext \ + --global-option=-j8 \ + pydantic==${PYDANTIC_VERSION} \ + && pip install --no-cache-dir -r requirements-slim.txt \ + # Remove build packages + && pip uninstall --yes \ + Cython \ + setuptools + +USER root + +# Clean up build deps +RUN rustup self uninstall -y \ + && apt-get purge --auto-remove --yes \ + build-essential \ + && apt-get clean \ + && rm --recursive --force /var/lib/apt/lists/* /tmp/* /var/tmp/* + +USER $G4F_USER_ID + +# Copy the entire package into the container. +ADD --chown=$G4F_USER:$G4F_USER g4f $G4F_DIR/g4f \ No newline at end of file diff --git a/docker/supervisor-api.conf b/docker/supervisor-api.conf new file mode 100755 index 00000000..74572634 --- /dev/null +++ b/docker/supervisor-api.conf @@ -0,0 +1,12 @@ +[program:g4f-api] +priority=15 +command=python -m g4f.cli api +directory=/app +stopasgroup=true +autostart=true +autorestart=true + +;Logs (all Hub activity redirected to stdout so it can be seen through "docker logs" +redirect_stderr=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 \ No newline at end of file diff --git a/docker/supervisor-gui.conf b/docker/supervisor-gui.conf index 44273c67..0c77ffc5 100755 --- a/docker/supervisor-gui.conf +++ b/docker/supervisor-gui.conf @@ -1,6 +1,6 @@ [program:g4f-gui] priority=15 -command=python -m g4f.cli gui +command=python -m g4f.cli gui -debug directory=/app stopasgroup=true autostart=true diff --git a/docker/supervisor.conf b/docker/supervisor.conf index f0f01fd1..7fd4331a 100755 --- a/docker/supervisor.conf +++ b/docker/supervisor.conf @@ -47,17 +47,4 @@ stderr_logfile_maxbytes=50MB stdout_logfile_backups=5 stderr_logfile_backups=5 stdout_capture_maxbytes=50MB -stderr_capture_maxbytes=50MB - -[program:g4f-api] -priority=15 -command=python -m g4f.cli api -directory=/app -stopasgroup=true -autostart=true -autorestart=true - -;Logs (all Hub activity redirected to stdout so it can be seen through "docker logs" -redirect_stderr=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 \ No newline at end of file +stderr_capture_maxbytes=50MB \ No newline at end of file diff --git a/docs/docker.md b/docs/docker.md index 8017715c..ce7fd466 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -28,12 +28,22 @@ ``` 2. **Build and Run with Docker Compose** + + Pull the latest image and run a container with Google Chrome support: ```bash - docker-compose up --build + docker pull hlohaus789/g4f + docker-compose up -d + ``` + Or run the small docker images without Google Chrome: + ```bash + docker-compose -f docker-compose-slim.yml up -d ``` -3. **Access the API** - The server will be accessible at `http://localhost:1337` +3. **Access the API or the GUI** + + The api server will be accessible at `http://localhost:1337` + + And the gui at this url: `http://localhost:8080` ### Non-Docker Method If you encounter issues with Docker, you can run the project directly using Python: @@ -54,8 +64,12 @@ If you encounter issues with Docker, you can run the project directly using Pyth python -m g4f.api.run ``` -4. **Access the API** - The server will be accessible at `http://localhost:1337` +4. **Access the API or the GUI** + + The api server will be accessible at `http://localhost:1337` + + And the gui at this url: `http://localhost:8080` + ## Testing the API **You can test the API using curl or by creating a simple Python script:** diff --git a/g4f/Provider/Cloudflare.py b/g4f/Provider/Cloudflare.py index 825c5027..7d477d57 100644 --- a/g4f/Provider/Cloudflare.py +++ b/g4f/Provider/Cloudflare.py @@ -7,6 +7,7 @@ import uuid from ..typing import AsyncResult, Messages, Cookies from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, get_running_loop from ..requests import Session, StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies +from ..errors import ResponseStatusError class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): label = "Cloudflare AI" @@ -42,10 +43,14 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): cls._args = asyncio.run(args) with Session(**cls._args) as session: response = session.get(cls.models_url) - raise_for_status(response) + cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response) + try: + raise_for_status(response) + except ResponseStatusError as e: + cls._args = None + raise e json_data = response.json() cls.models = [model.get("name") for model in json_data.get("models")] - cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response) return cls.models @classmethod @@ -74,8 +79,12 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): cls.api_endpoint, json=data, ) as response: - await raise_for_status(response) cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response) + try: + await raise_for_status(response) + except ResponseStatusError as e: + cls._args = None + raise e async for line in response.iter_lines(): if line.startswith(b'data: '): if line == b'data: [DONE]': diff --git a/g4f/Provider/HuggingChat.py b/g4f/Provider/HuggingChat.py index 509a7f16..2481aa31 100644 --- a/g4f/Provider/HuggingChat.py +++ b/g4f/Provider/HuggingChat.py @@ -4,12 +4,13 @@ import json import requests try: - from curl_cffi import requests as cf_reqs + from curl_cffi import Session has_curl_cffi = True except ImportError: has_curl_cffi = False from ..typing import CreateResult, Messages from ..errors import MissingRequirementsError +from ..requests.raise_for_status import raise_for_status from .base_provider import ProviderModelMixin, AbstractProvider from .helper import format_prompt @@ -18,7 +19,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): working = True supports_stream = True default_model = "meta-llama/Meta-Llama-3.1-70B-Instruct" - + models = [ 'meta-llama/Meta-Llama-3.1-70B-Instruct', 'CohereForAI/c4ai-command-r-plus-08-2024', @@ -30,7 +31,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): 'mistralai/Mistral-Nemo-Instruct-2407', 'microsoft/Phi-3.5-mini-instruct', ] - + model_aliases = { "llama-3.1-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct", "command-r-plus": "CohereForAI/c4ai-command-r-plus-08-2024", @@ -43,15 +44,6 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): "phi-3.5-mini": "microsoft/Phi-3.5-mini-instruct", } - @classmethod - def get_model(cls, model: str) -> str: - if model in cls.models: - return model - elif model in cls.model_aliases: - return cls.model_aliases[model] - else: - return cls.default_model - @classmethod def create_completion( cls, @@ -65,7 +57,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): model = cls.get_model(model) if model in cls.models: - session = cf_reqs.Session() + session = Session() session.headers = { 'accept': '*/*', 'accept-language': 'en', @@ -82,20 +74,18 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): 'sec-fetch-site': 'same-origin', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', } - json_data = { 'model': model, } - response = session.post('https://huggingface.co/chat/conversation', json=json_data) - if response.status_code != 200: - raise RuntimeError(f"Request failed with status code: {response.status_code}, response: {response.text}") + raise_for_status(response) conversationId = response.json().get('conversationId') # Get the data response and parse it properly response = session.get(f'https://huggingface.co/chat/conversation/{conversationId}/__data.json?x-sveltekit-invalidated=11') - + raise_for_status(response) + # Split the response content by newlines and parse each line as JSON try: json_data = None @@ -156,6 +146,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): headers=headers, files=files, ) + raise_for_status(response) full_response = "" for line in response.iter_lines(): @@ -182,9 +173,4 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): full_response = full_response.replace('<|im_end|', '').replace('\u0000', '').strip() if not stream: - yield full_response - - @classmethod - def supports_model(cls, model: str) -> bool: - """Check if the model is supported by the provider.""" - return model in cls.models or model in cls.model_aliases + yield full_response \ No newline at end of file diff --git a/g4f/Provider/needs_auth/Gemini.py b/g4f/Provider/needs_auth/Gemini.py index dad54c84..781aa410 100644 --- a/g4f/Provider/needs_auth/Gemini.py +++ b/g4f/Provider/needs_auth/Gemini.py @@ -6,24 +6,20 @@ import random import re from aiohttp import ClientSession, BaseConnector - -from ..helper import get_connector - try: - from selenium.webdriver.common.by import By - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.support import expected_conditions as EC + import nodriver + has_nodriver = True except ImportError: - pass + has_nodriver = False from ... import debug from ...typing import Messages, Cookies, ImageType, AsyncResult, AsyncIterator from ..base_provider import AsyncGeneratorProvider, BaseConversation from ..helper import format_prompt, get_cookies from ...requests.raise_for_status import raise_for_status -from ...errors import MissingAuthError, MissingRequirementsError +from ...requests.aiohttp import get_connector +from ...errors import MissingAuthError from ...image import ImageResponse, to_bytes -from ...webdriver import get_browser, get_driver_cookies REQUEST_HEADERS = { "authority": "gemini.google.com", @@ -64,9 +60,9 @@ class Gemini(AsyncGeneratorProvider): @classmethod async def nodriver_login(cls, proxy: str = None) -> AsyncIterator[str]: - try: - import nodriver as uc - except ImportError: + if not has_nodriver: + if debug.logging: + print("Skip nodriver login in Gemini provider") return try: from platformdirs import user_config_dir @@ -75,7 +71,7 @@ class Gemini(AsyncGeneratorProvider): user_data_dir = None if debug.logging: print(f"Open nodriver with user_dir: {user_data_dir}") - browser = await uc.start( + browser = await nodriver.start( user_data_dir=user_data_dir, browser_args=None if proxy is None else [f"--proxy-server={proxy}"], ) @@ -91,30 +87,6 @@ class Gemini(AsyncGeneratorProvider): await page.close() cls._cookies = cookies - @classmethod - async def webdriver_login(cls, proxy: str) -> AsyncIterator[str]: - driver = None - try: - driver = get_browser(proxy=proxy) - try: - driver.get(f"{cls.url}/app") - WebDriverWait(driver, 5).until( - EC.visibility_of_element_located((By.CSS_SELECTOR, "div.ql-editor.textarea")) - ) - except: - login_url = os.environ.get("G4F_LOGIN_URL") - if login_url: - yield f"Please login: [Google Gemini]({login_url})\n\n" - WebDriverWait(driver, 240).until( - EC.visibility_of_element_located((By.CSS_SELECTOR, "div.ql-editor.textarea")) - ) - cls._cookies = get_driver_cookies(driver) - except MissingRequirementsError: - pass - finally: - if driver: - driver.close() - @classmethod async def create_async_generator( cls, @@ -143,9 +115,6 @@ class Gemini(AsyncGeneratorProvider): if not cls._snlm0e: async for chunk in cls.nodriver_login(proxy): yield chunk - if cls._cookies is None: - async for chunk in cls.webdriver_login(proxy): - yield chunk if not cls._snlm0e: if cls._cookies is None or "__Secure-1PSID" not in cls._cookies: raise MissingAuthError('Missing "__Secure-1PSID" cookie') @@ -211,20 +180,23 @@ class Gemini(AsyncGeneratorProvider): yield content[last_content_len:] last_content_len = len(content) if image_prompt: - images = [image[0][3][3] for image in response_part[4][0][12][7][0]] - if response_format == "b64_json": - yield ImageResponse(images, image_prompt, {"cookies": cls._cookies}) - else: - resolved_images = [] - preview = [] - for image in images: - async with client.get(image, allow_redirects=False) as fetch: - image = fetch.headers["location"] - async with client.get(image, allow_redirects=False) as fetch: - image = fetch.headers["location"] - resolved_images.append(image) - preview.append(image.replace('=s512', '=s200')) - yield ImageResponse(resolved_images, image_prompt, {"orginal_links": images, "preview": preview}) + try: + images = [image[0][3][3] for image in response_part[4][0][12][7][0]] + if response_format == "b64_json": + yield ImageResponse(images, image_prompt, {"cookies": cls._cookies}) + else: + resolved_images = [] + preview = [] + for image in images: + async with client.get(image, allow_redirects=False) as fetch: + image = fetch.headers["location"] + async with client.get(image, allow_redirects=False) as fetch: + image = fetch.headers["location"] + resolved_images.append(image) + preview.append(image.replace('=s512', '=s200')) + yield ImageResponse(resolved_images, image_prompt, {"orginal_links": images, "preview": preview}) + except TypeError: + pass def build_request( prompt: str, diff --git a/g4f/Provider/needs_auth/GeminiPro.py b/g4f/Provider/needs_auth/GeminiPro.py index 7e52a194..a7f1e0aa 100644 --- a/g4f/Provider/needs_auth/GeminiPro.py +++ b/g4f/Provider/needs_auth/GeminiPro.py @@ -16,9 +16,9 @@ class GeminiPro(AsyncGeneratorProvider, ProviderModelMixin): working = True supports_message_history = True needs_auth = True - default_model = "gemini-1.5-pro-latest" + default_model = "gemini-1.5-pro" default_vision_model = default_model - models = [default_model, "gemini-pro", "gemini-pro-vision", "gemini-1.5-flash"] + models = [default_model, "gemini-pro", "gemini-1.5-flash", "gemini-1.5-flash-8b"] @classmethod async def create_async_generator( diff --git a/g4f/Provider/needs_auth/HuggingFace.py b/g4f/Provider/needs_auth/HuggingFace.py index ecc75d1c..35270e60 100644 --- a/g4f/Provider/needs_auth/HuggingFace.py +++ b/g4f/Provider/needs_auth/HuggingFace.py @@ -1,13 +1,11 @@ from __future__ import annotations import json -from aiohttp import ClientSession, BaseConnector from ...typing import AsyncResult, Messages from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin -from ..helper import get_connector -from ...errors import RateLimitError, ModelNotFoundError -from ...requests.raise_for_status import raise_for_status +from ...errors import ModelNotFoundError +from ...requests import StreamSession, raise_for_status from ..HuggingChat import HuggingChat @@ -20,15 +18,6 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin): models = HuggingChat.models model_aliases = HuggingChat.model_aliases - @classmethod - def get_model(cls, model: str) -> str: - if model in cls.models: - return model - elif model in cls.model_aliases: - return cls.model_aliases[model] - else: - return cls.default_model - @classmethod async def create_async_generator( cls, @@ -36,7 +25,6 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin): messages: Messages, stream: bool = True, proxy: str = None, - connector: BaseConnector = None, api_base: str = "https://api-inference.huggingface.co", api_key: str = None, max_new_tokens: int = 1024, @@ -62,7 +50,6 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin): } if api_key is not None: headers["Authorization"] = f"Bearer {api_key}" - params = { "return_full_text": False, "max_new_tokens": max_new_tokens, @@ -70,10 +57,9 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin): **kwargs } payload = {"inputs": format_prompt(messages), "parameters": params, "stream": stream} - - async with ClientSession( + async with StreamSession( headers=headers, - connector=get_connector(connector, proxy) + proxy=proxy ) as session: async with session.post(f"{api_base.rstrip('/')}/models/{model}", json=payload) as response: if response.status == 404: @@ -81,7 +67,7 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin): await raise_for_status(response) if stream: first = True - async for line in response.content: + async for line in response.iter_lines(): if line.startswith(b"data:"): data = json.loads(line[5:]) if not data["token"]["special"]: @@ -89,7 +75,8 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin): if first: first = False chunk = chunk.lstrip() - yield chunk + if chunk: + yield chunk else: yield (await response.json())[0]["generated_text"].strip() @@ -101,4 +88,4 @@ def format_prompt(messages: Messages) -> str: for idx, message in enumerate(messages) if message["role"] == "assistant" ]) - return f"{history}[INST] {question} [/INST]" + return f"{history}[INST] {question} [/INST]" \ No newline at end of file diff --git a/g4f/Provider/needs_auth/MetaAI.py b/g4f/Provider/needs_auth/MetaAI.py index 4b730abd..568de701 100644 --- a/g4f/Provider/needs_auth/MetaAI.py +++ b/g4f/Provider/needs_auth/MetaAI.py @@ -79,7 +79,6 @@ class MetaAI(AsyncGeneratorProvider, ProviderModelMixin): self.access_token = None if self.access_token is None and cookies is None: await self.update_access_token() - if self.access_token is None: url = "https://www.meta.ai/api/graphql/" payload = {"lsd": self.lsd, 'fb_dtsg': self.dtsg} @@ -128,6 +127,8 @@ class MetaAI(AsyncGeneratorProvider, ProviderModelMixin): json_line = json.loads(line) except json.JSONDecodeError: continue + if json_line.get("errors"): + raise RuntimeError("\n".join([error.get("message") for error in json_line.get("errors")])) bot_response_message = json_line.get("data", {}).get("node", {}).get("bot_response_message", {}) streaming_state = bot_response_message.get("streaming_state") fetch_id = bot_response_message.get("fetch_id") or fetch_id diff --git a/g4f/Provider/needs_auth/MetaAIAccount.py b/g4f/Provider/needs_auth/MetaAIAccount.py index 2d54f3e0..0a586006 100644 --- a/g4f/Provider/needs_auth/MetaAIAccount.py +++ b/g4f/Provider/needs_auth/MetaAIAccount.py @@ -2,7 +2,7 @@ from __future__ import annotations from ...typing import AsyncResult, Messages, Cookies from ..helper import format_prompt, get_cookies -from ..MetaAI import MetaAI +from .MetaAI import MetaAI class MetaAIAccount(MetaAI): needs_auth = True diff --git a/g4f/Provider/needs_auth/__init__.py b/g4f/Provider/needs_auth/__init__.py index 26c50c0a..ace53876 100644 --- a/g4f/Provider/needs_auth/__init__.py +++ b/g4f/Provider/needs_auth/__init__.py @@ -11,6 +11,7 @@ from .GeminiPro import GeminiPro from .Groq import Groq from .HuggingFace import HuggingFace from .MetaAI import MetaAI +from .MetaAIAccount import MetaAIAccount from .OpenaiAPI import OpenaiAPI from .OpenaiChat import OpenaiChat from .PerplexityApi import PerplexityApi diff --git a/g4f/gui/server/api.py b/g4f/gui/server/api.py index dafcb5d4..f03d2048 100644 --- a/g4f/gui/server/api.py +++ b/g4f/gui/server/api.py @@ -6,14 +6,14 @@ import uuid import asyncio import time from aiohttp import ClientSession -from typing import Iterator, Optional, AsyncIterator, Union +from typing import Iterator, Optional from flask import send_from_directory from g4f import version, models from g4f import get_last_provider, ChatCompletion from g4f.errors import VersionNotFoundError from g4f.typing import Cookies -from g4f.image import ImagePreview, ImageResponse, is_accepted_format +from g4f.image import ImagePreview, ImageResponse, is_accepted_format, extract_data_uri from g4f.requests.aiohttp import get_connector from g4f.Provider import ProviderType, __providers__, __map__ from g4f.providers.base_provider import ProviderModelMixin, FinishReason @@ -31,7 +31,6 @@ def ensure_images_dir(): conversations: dict[dict[str, BaseConversation]] = {} - class Api: @staticmethod def get_models() -> list[str]: @@ -176,18 +175,22 @@ class Api: connector=get_connector(None, os.environ.get("G4F_PROXY")), cookies=cookies ) as session: - async def copy_image(image): - async with session.get(image) as response: - target = os.path.join(images_dir, f"{int(time.time())}_{str(uuid.uuid4())}") + async def copy_image(image: str) -> str: + target = os.path.join(images_dir, f"{int(time.time())}_{str(uuid.uuid4())}") + if image.startswith("data:"): with open(target, "wb") as f: - async for chunk in response.content.iter_any(): - f.write(chunk) - with open(target, "rb") as f: - extension = is_accepted_format(f.read(12)).split("/")[-1] - extension = "jpg" if extension == "jpeg" else extension - new_target = f"{target}.{extension}" - os.rename(target, new_target) - return f"/images/{os.path.basename(new_target)}" + f.write(extract_data_uri(image)) + else: + async with session.get(image) as response: + with open(target, "wb") as f: + async for chunk in response.content.iter_any(): + f.write(chunk) + with open(target, "rb") as f: + extension = is_accepted_format(f.read(12)).split("/")[-1] + extension = "jpg" if extension == "jpeg" else extension + new_target = f"{target}.{extension}" + os.rename(target, new_target) + return f"/images/{os.path.basename(new_target)}" return await asyncio.gather(*[copy_image(image) for image in images]) @@ -197,7 +200,6 @@ class Api: response_type: content } - def get_error_message(exception: Exception) -> str: message = f"{type(exception).__name__}: {exception}" provider = get_last_provider() diff --git a/g4f/image.py b/g4f/image.py index 556ec43d..8a3d7a74 100644 --- a/g4f/image.py +++ b/g4f/image.py @@ -133,7 +133,7 @@ def extract_data_uri(data_uri: str) -> bytes: Returns: bytes: The extracted binary data. """ - data = data_uri.split(",")[1] + data = data_uri.split(",")[-1] data = base64.b64decode(data) return data diff --git a/requirements-slim.txt b/requirements-slim.txt new file mode 100644 index 00000000..b9cbceba --- /dev/null +++ b/requirements-slim.txt @@ -0,0 +1,16 @@ +requests +pycryptodome +curl_cffi>=0.6.2 +aiohttp +certifi +duckduckgo-search>=5.0 +nest_asyncio +werkzeug +pillow +fastapi +uvicorn +flask +brotli +beautifulsoup4 +aiohttp_socks +cryptography \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1a014bac..83130838 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ curl_cffi>=0.6.2 aiohttp certifi browser_cookie3 -PyExecJS duckduckgo-search>=5.0 nest_asyncio werkzeug @@ -19,5 +18,4 @@ aiohttp_socks pywebview plyer cryptography -nodriver -cloudscraper +nodriver \ No newline at end of file diff --git a/setup.py b/setup.py index b35f9754..0cafb642 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,10 @@ EXTRA_REQUIRE = { "duckduckgo-search>=5.0", "browser_cookie3" ], + "search": [ + "beautifulsoup4", "pillow", + "duckduckgo-search>=5.0", + ], "local": [ "gpt4all" ]