diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 90558f09..96e274b1 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" cache: 'pip' - name: Install requirements run: pip install -r requirements.txt diff --git a/g4f/Provider/You.py b/g4f/Provider/You.py index 91a195cf..001f775d 100644 --- a/g4f/Provider/You.py +++ b/g4f/Provider/You.py @@ -1,40 +1,155 @@ from __future__ import annotations import json +import base64 +import uuid +from aiohttp import ClientSession, FormData -from ..requests import StreamSession -from ..typing import AsyncGenerator, Messages -from .base_provider import AsyncGeneratorProvider, format_prompt - +from ..typing import AsyncGenerator, Messages, ImageType, Cookies +from .base_provider import AsyncGeneratorProvider +from .helper import get_connector, format_prompt +from ..image import to_bytes +from ..defaults import DEFAULT_HEADERS class You(AsyncGeneratorProvider): url = "https://you.com" working = True supports_gpt_35_turbo = True - + supports_gpt_4 = True + _cookies = None + _cookies_used = 0 @classmethod async def create_async_generator( cls, model: str, messages: Messages, + image: ImageType = None, + image_name: str = None, proxy: str = None, - timeout: int = 120, + chat_mode: str = "default", **kwargs, ) -> AsyncGenerator: - async with StreamSession(proxies={"https": proxy}, impersonate="chrome107", timeout=timeout) as session: + async with ClientSession( + connector=get_connector(kwargs.get("connector"), proxy), + headers=DEFAULT_HEADERS + ) as client: + if image: + chat_mode = "agent" + elif model == "gpt-4": + chat_mode = model + cookies = await cls.get_cookies(client) if chat_mode != "default" else None + upload = json.dumps([await cls.upload_file(client, cookies, to_bytes(image), image_name)]) if image else "" + #questions = [message["content"] for message in messages if message["role"] == "user"] + # chat = [ + # {"question": questions[idx-1], "answer": message["content"]} + # for idx, message in enumerate(messages) + # if message["role"] == "assistant" + # and idx < len(questions) + # ] headers = { "Accept": "text/event-stream", "Referer": f"{cls.url}/search?fromSearchBar=true&tbm=youchat", } - data = {"q": format_prompt(messages), "domain": "youchat", "chat": ""} - async with session.get( + data = { + "userFiles": upload, + "q": format_prompt(messages), + "domain": "youchat", + "selectedChatMode": chat_mode, + #"chat": json.dumps(chat), + } + async with (client.post if chat_mode == "default" else client.get)( f"{cls.url}/api/streamingSearch", - params=data, - headers=headers + data=data, + headers=headers, + cookies=cookies ) as response: response.raise_for_status() - start = b'data: {"youChatToken": ' - async for line in response.iter_lines(): - if line.startswith(start): - yield json.loads(line[len(start):-1]) + async for line in response.content: + if line.startswith(b'event: '): + event = line[7:-1] + elif line.startswith(b'data: '): + if event == b"youChatUpdate" or event == b"youChatToken": + data = json.loads(line[6:-1]) + if event == b"youChatToken" and "youChatToken" in data: + yield data["youChatToken"] + elif event == b"youChatUpdate" and "t" in data: + yield data["t"] + + @classmethod + async def upload_file(cls, client: ClientSession, cookies: Cookies, file: bytes, filename: str = None) -> dict: + async with client.get( + f"{cls.url}/api/get_nonce", + cookies=cookies, + ) as response: + response.raise_for_status() + upload_nonce = await response.text() + data = FormData() + data.add_field('file', file, filename=filename) + async with client.post( + f"{cls.url}/api/upload", + data=data, + headers={ + "X-Upload-Nonce": upload_nonce, + }, + cookies=cookies + ) as response: + if not response.ok: + raise RuntimeError(f"Response: {await response.text()}") + result = await response.json() + result["user_filename"] = filename + result["size"] = len(file) + return result + + @classmethod + async def get_cookies(cls, client: ClientSession) -> Cookies: + if not cls._cookies or cls._cookies_used >= 5: + cls._cookies = await cls.create_cookies(client) + cls._cookies_used = 0 + cls._cookies_used += 1 + return cls._cookies + + @classmethod + def get_sdk(cls) -> str: + return base64.standard_b64encode(json.dumps({ + "event_id":f"event-id-{str(uuid.uuid4())}", + "app_session_id":f"app-session-id-{str(uuid.uuid4())}", + "persistent_id":f"persistent-id-{uuid.uuid4()}", + "client_sent_at":"","timezone":"", + "stytch_user_id":f"user-live-{uuid.uuid4()}", + "stytch_session_id":f"session-live-{uuid.uuid4()}", + "app":{"identifier":"you.com"}, + "sdk":{"identifier":"Stytch.js Javascript SDK","version":"3.3.0" + }}).encode()).decode() + + def get_auth() -> str: + auth_uuid = "507a52ad-7e69-496b-aee0-1c9863c7c8" + auth_token = f"public-token-live-{auth_uuid}bb:public-token-live-{auth_uuid}19" + auth = base64.standard_b64encode(auth_token.encode()).decode() + return f"Basic {auth}" + + @classmethod + async def create_cookies(cls, client: ClientSession) -> Cookies: + user_uuid = str(uuid.uuid4()) + async with client.post( + "https://web.stytch.com/sdk/v1/passwords", + headers={ + "Authorization": cls.get_auth(), + "X-SDK-Client": cls.get_sdk(), + "X-SDK-Parent-Host": cls.url + }, + json={ + "email": f"{user_uuid}@gmail.com", + "password": f"{user_uuid}#{user_uuid}", + "session_duration_minutes": 129600 + } + ) as response: + if not response.ok: + raise RuntimeError(f"Response: {await response.text()}") + session = (await response.json())["data"] + return { + "stytch_session": session["session_token"], + 'stytch_session_jwt': session["session_jwt"], + 'ydc_stytch_session': session["session_token"], + 'ydc_stytch_session_jwt': session["session_jwt"], + } \ No newline at end of file diff --git a/g4f/cookies.py b/g4f/cookies.py index af0e6192..a38488c2 100644 --- a/g4f/cookies.py +++ b/g4f/cookies.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import time try: from platformdirs import user_config_dir @@ -72,7 +73,8 @@ def load_cookies_from_browsers(domain_name: str, raise_requirements_error: bool print(f"Read cookies from {cookie_fn.__name__} for {domain_name}") for cookie in cookie_jar: if cookie.name not in cookies: - cookies[cookie.name] = cookie.value + if not cookie.expires or cookie.expires > time.time(): + cookies[cookie.name] = cookie.value if single_browser and len(cookie_jar): break except BrowserCookieError: diff --git a/g4f/gui/__init__.py b/g4f/gui/__init__.py index 46b4f56a..dff720ac 100644 --- a/g4f/gui/__init__.py +++ b/g4f/gui/__init__.py @@ -7,6 +7,9 @@ except ImportError: raise MissingRequirementsError('Install "flask" package for the gui') def run_gui(host: str = '0.0.0.0', port: int = 8080, debug: bool = False) -> None: + if debug: + import g4f + g4f.debug.logging = True config = { 'host' : host, 'port' : port, diff --git a/g4f/gui/client/css/style.css b/g4f/gui/client/css/style.css index e03f36d2..8752dee5 100644 --- a/g4f/gui/client/css/style.css +++ b/g4f/gui/client/css/style.css @@ -404,7 +404,7 @@ body { display: none; } -#image, #file { +#image, #file, #camera { display: none; } @@ -412,20 +412,37 @@ label[for="image"]:has(> input:valid){ color: var(--accent); } +label[for="camera"]:has(> input:valid){ + color: var(--accent); +} + label[for="file"]:has(> input:valid){ color: var(--accent); } -label[for="image"], label[for="file"] { +label[for="image"], label[for="file"], label[for="camera"] { cursor: pointer; position: absolute; top: 10px; left: 10px; } -label[for="file"] { +label[for="image"] { top: 32px; - left: 10px; +} + +label[for="camera"] { + top: 54px; +} + +label[for="camera"] { + display: none; +} + +@media (pointer:none), (pointer:coarse) { + label[for="camera"] { + display: block; + } } .buttons input[type="checkbox"] { diff --git a/g4f/gui/client/html/index.html b/g4f/gui/client/html/index.html index 55b54b48..70b8c75f 100644 --- a/g4f/gui/client/html/index.html +++ b/g4f/gui/client/html/index.html @@ -114,10 +114,14 @@
-
- + Web Access
- + Image Generator
+
+ + + Disable History +
diff --git a/g4f/gui/client/js/chat.v1.js b/g4f/gui/client/js/chat.v1.js index 86eef8c9..a243b1de 100644 --- a/g4f/gui/client/js/chat.v1.js +++ b/g4f/gui/client/js/chat.v1.js @@ -8,6 +8,7 @@ const stop_generating = document.querySelector(`.stop_generating`); const regenerate = document.querySelector(`.regenerate`); const send_button = document.querySelector(`#send-button`); const imageInput = document.querySelector('#image'); +const cameraInput = document.querySelector('#camera'); const fileInput = document.querySelector('#file'); let prompt_lock = false; @@ -63,6 +64,10 @@ const handle_ask = async () => { ? 'Image upload' : '' } + ${cameraInput.dataset.src + ? 'Image capture' + : '' + } `; @@ -95,6 +100,11 @@ const ask_gpt = async () => { delete messages[i]["provider"]; } + // Remove history, if it is selected + if (document.getElementById('history')?.checked) { + messages = [messages[messages.length-1]] + } + window.scrollTo(0, 0); window.controller = new AbortController(); @@ -141,9 +151,10 @@ const ask_gpt = async () => { const headers = { accept: 'text/event-stream' } - if (imageInput && imageInput.files.length > 0) { + const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput + if (input && input.files.length > 0) { const formData = new FormData(); - formData.append('image', imageInput.files[0]); + formData.append('image', input.files[0]); formData.append('json', body); body = formData; } else { @@ -211,8 +222,11 @@ const ask_gpt = async () => { message_box.scrollTo({ top: message_box.scrollHeight, behavior: "auto" }); } } - if (!error && imageInput) imageInput.value = ""; - if (!error && fileInput) fileInput.value = ""; + if (!error) { + if (imageInput) imageInput.value = ""; + if (cameraInput) cameraInput.value = ""; + if (fileInput) fileInput.value = ""; + } } catch (e) { console.error(e); @@ -482,7 +496,7 @@ document.querySelector(".mobile-sidebar").addEventListener("click", (event) => { }); const register_settings_localstorage = async () => { - for (id of ["switch", "model", "jailbreak", "patch", "provider"]) { + for (id of ["switch", "model", "jailbreak", "patch", "provider", "history"]) { element = document.getElementById(id); element.addEventListener('change', async (event) => { switch (event.target.type) { @@ -500,7 +514,7 @@ const register_settings_localstorage = async () => { } const load_settings_localstorage = async () => { - for (id of ["switch", "model", "jailbreak", "patch", "provider"]) { + for (id of ["switch", "model", "jailbreak", "patch", "provider", "history"]) { element = document.getElementById(id); value = localStorage.getItem(element.id); if (value) { @@ -668,21 +682,26 @@ observer.observe(message_input, { attributes: true }); } document.getElementById("version_text").innerHTML = text })() -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; +for (const el of [imageInput, cameraInput]) { + console.log(el.files); + el.addEventListener('click', async () => { + el.value = ''; + delete el.dataset.src; + }); + do_load = async () => { + if (el.files.length) { + delete imageInput.dataset.src; + delete cameraInput.dataset.src; + const reader = new FileReader(); + reader.addEventListener('load', (event) => { + el.dataset.src = event.target.result; + }); + reader.readAsDataURL(el.files[0]); + } } -}); + do_load() + el.addEventListener('change', do_load); +} fileInput.addEventListener('click', async (event) => { fileInput.value = ''; delete fileInput.dataset.text; diff --git a/g4f/gui/server/backend.py b/g4f/gui/server/backend.py index 2218452c..6847be34 100644 --- a/g4f/gui/server/backend.py +++ b/g4f/gui/server/backend.py @@ -134,25 +134,31 @@ class Backend_Api: dict: Arguments prepared for chat completion. """ kwargs = {} - if 'image' in request.files: + if "image" in request.files: file = request.files['image'] if file.filename != '' and is_allowed_extension(file.filename): kwargs['image'] = to_image(file.stream, file.filename.endswith('.svg')) - if 'json' in request.form: + kwargs['image_name'] = file.filename + if "json" in request.form: json_data = json.loads(request.form['json']) else: json_data = request.json provider = json_data.get('provider', '').replace('g4f.Provider.', '') provider = provider if provider and provider != "Auto" else None + + if "image" in kwargs and not provider: + provider = "Bing" if provider == 'OpenaiChat': kwargs['auto_continue'] = True + messages = json_data['messages'] if json_data.get('web_search'): if provider == "Bing": kwargs['web_search'] = True else: messages[-1]["content"] = get_search_message(messages[-1]["content"]) + model = json_data.get('model') model = model if model else models.default patch = patch_provider if json_data.get('patch_provider') else None diff --git a/g4f/image.py b/g4f/image.py index 31c3d577..93922c2e 100644 --- a/g4f/image.py +++ b/g4f/image.py @@ -137,12 +137,12 @@ def get_orientation(image: Image) -> int: if orientation is not None: return orientation -def process_image(img: Image, new_width: int, new_height: int) -> Image: +def process_image(image: Image, new_width: int, new_height: int) -> Image: """ Processes the given image by adjusting its orientation and resizing it. Args: - img (Image): The image to process. + image (Image): The image to process. new_width (int): The new width of the image. new_height (int): The new height of the image. @@ -150,25 +150,27 @@ def process_image(img: Image, new_width: int, new_height: int) -> Image: Image: The processed image. """ # Fix orientation - orientation = get_orientation(img) + orientation = get_orientation(image) if orientation: if orientation > 4: - img = img.transpose(FLIP_LEFT_RIGHT) + image = image.transpose(FLIP_LEFT_RIGHT) if orientation in [3, 4]: - img = img.transpose(ROTATE_180) + image = image.transpose(ROTATE_180) if orientation in [5, 6]: - img = img.transpose(ROTATE_270) + image = image.transpose(ROTATE_270) if orientation in [7, 8]: - img = img.transpose(ROTATE_90) + image = image.transpose(ROTATE_90) # Resize image - img.thumbnail((new_width, new_height)) + image.thumbnail((new_width, new_height)) # Remove transparency - if img.mode == "RGBA": - img.load() - white = new_image('RGB', img.size, (255, 255, 255)) - white.paste(img, mask=img.split()[-1]) + if image.mode == "RGBA": + image.load() + white = new_image('RGB', image.size, (255, 255, 255)) + white.paste(image, mask=image.split()[-1]) return white - return img + elif image.mode != "RGB": + image = image.convert("RGB") + return image def to_base64_jpg(image: Image, compression_rate: float) -> str: """