Merge pull request #1575 from hlohaus/openai

Add GPT 4 support in You
Fix convert image to jpg with exceptional modes in Bing 
Add camera input in GUI
Enable logging on debug in GUI
Don't load expired cookies
Fix display upload image in GUI
Add upload image in You provider
Add disable history button in GUI
Change python version to 3.12 in unittests
This commit is contained in:
H Lohaus 2024-02-11 08:41:54 +01:00 committed by GitHub
commit 3c498496f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 234 additions and 60 deletions

View File

@ -27,7 +27,7 @@ jobs:
- name: Set up Python 3.11 - name: Set up Python 3.11
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.11" python-version: "3.12"
cache: 'pip' cache: 'pip'
- name: Install requirements - name: Install requirements
run: pip install -r requirements.txt run: pip install -r requirements.txt

View File

@ -1,40 +1,155 @@
from __future__ import annotations from __future__ import annotations
import json import json
import base64
import uuid
from aiohttp import ClientSession, FormData
from ..requests import StreamSession from ..typing import AsyncGenerator, Messages, ImageType, Cookies
from ..typing import AsyncGenerator, Messages from .base_provider import AsyncGeneratorProvider
from .base_provider import AsyncGeneratorProvider, format_prompt from .helper import get_connector, format_prompt
from ..image import to_bytes
from ..defaults import DEFAULT_HEADERS
class You(AsyncGeneratorProvider): class You(AsyncGeneratorProvider):
url = "https://you.com" url = "https://you.com"
working = True working = True
supports_gpt_35_turbo = True supports_gpt_35_turbo = True
supports_gpt_4 = True
_cookies = None
_cookies_used = 0
@classmethod @classmethod
async def create_async_generator( async def create_async_generator(
cls, cls,
model: str, model: str,
messages: Messages, messages: Messages,
image: ImageType = None,
image_name: str = None,
proxy: str = None, proxy: str = None,
timeout: int = 120, chat_mode: str = "default",
**kwargs, **kwargs,
) -> AsyncGenerator: ) -> 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 = { headers = {
"Accept": "text/event-stream", "Accept": "text/event-stream",
"Referer": f"{cls.url}/search?fromSearchBar=true&tbm=youchat", "Referer": f"{cls.url}/search?fromSearchBar=true&tbm=youchat",
} }
data = {"q": format_prompt(messages), "domain": "youchat", "chat": ""} data = {
async with session.get( "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", f"{cls.url}/api/streamingSearch",
params=data, data=data,
headers=headers headers=headers,
cookies=cookies
) as response: ) as response:
response.raise_for_status() response.raise_for_status()
start = b'data: {"youChatToken": ' async for line in response.content:
async for line in response.iter_lines(): if line.startswith(b'event: '):
if line.startswith(start): event = line[7:-1]
yield json.loads(line[len(start):-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"],
}

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
import time
try: try:
from platformdirs import user_config_dir 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}") print(f"Read cookies from {cookie_fn.__name__} for {domain_name}")
for cookie in cookie_jar: for cookie in cookie_jar:
if cookie.name not in cookies: 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): if single_browser and len(cookie_jar):
break break
except BrowserCookieError: except BrowserCookieError:

View File

@ -7,6 +7,9 @@ except ImportError:
raise MissingRequirementsError('Install "flask" package for the gui') raise MissingRequirementsError('Install "flask" package for the gui')
def run_gui(host: str = '0.0.0.0', port: int = 8080, debug: bool = False) -> None: 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 = { config = {
'host' : host, 'host' : host,
'port' : port, 'port' : port,

View File

@ -404,7 +404,7 @@ body {
display: none; display: none;
} }
#image, #file { #image, #file, #camera {
display: none; display: none;
} }
@ -412,20 +412,37 @@ label[for="image"]:has(> input:valid){
color: var(--accent); color: var(--accent);
} }
label[for="camera"]:has(> input:valid){
color: var(--accent);
}
label[for="file"]:has(> input:valid){ label[for="file"]:has(> input:valid){
color: var(--accent); color: var(--accent);
} }
label[for="image"], label[for="file"] { label[for="image"], label[for="file"], label[for="camera"] {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
top: 10px; top: 10px;
left: 10px; left: 10px;
} }
label[for="file"] { label[for="image"] {
top: 32px; 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"] { .buttons input[type="checkbox"] {

View File

@ -114,10 +114,14 @@
<div class="box input-box"> <div class="box input-box">
<textarea id="message-input" placeholder="Ask a question" cols="30" rows="10" <textarea id="message-input" placeholder="Ask a question" cols="30" rows="10"
style="white-space: pre-wrap;resize: none;"></textarea> style="white-space: pre-wrap;resize: none;"></textarea>
<label for="image" title="Works only with Bing and OpenaiChat"> <label for="image" title="Works with Bing, Gemini, OpenaiChat and You">
<input type="file" id="image" name="image" accept="image/png, image/gif, image/jpeg, image/svg+xml" required/> <input type="file" id="image" name="image" accept="image/*" required/>
<i class="fa-regular fa-image"></i> <i class="fa-regular fa-image"></i>
</label> </label>
<label for="camera">
<input type="file" id="camera" name="camera" accept="image/*" capture="camera" required/>
<i class="fa-solid fa-camera"></i>
</label>
<label for="file"> <label for="file">
<input type="file" id="file" name="file" accept="text/plain, text/html, text/xml, application/json, text/javascript, .sh, .py, .php, .css, .yaml, .sql, .log, .csv, .twig, .md" required/> <input type="file" id="file" name="file" accept="text/plain, text/html, text/xml, application/json, text/javascript, .sh, .py, .php, .css, .yaml, .sql, .log, .csv, .twig, .md" required/>
<i class="fa-solid fa-paperclip"></i> <i class="fa-solid fa-paperclip"></i>
@ -157,20 +161,26 @@
<option value="Gemini">Gemini</option> <option value="Gemini">Gemini</option>
<option value="Liaobots">Liaobots</option> <option value="Liaobots">Liaobots</option>
<option value="Phind">Phind</option> <option value="Phind">Phind</option>
<option value="You">You</option>
<option value="">----</option> <option value="">----</option>
</select> </select>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<input type="checkbox" id="switch" /> <input type="checkbox" id="switch" />
<label for="switch"></label> <label for="switch" title="Add the pages of the first 5 search results to the query."></label>
<span class="about">Web Access</span> <span class="about">Web Access</span>
</div> </div>
<div class="field"> <div class="field">
<input type="checkbox" id="patch" /> <input type="checkbox" id="patch" />
<label for="patch" title="Works only with Bing and some other providers"></label> <label for="patch" title="Enable create images with Bing."></label>
<span class="about">Image Generator</span> <span class="about">Image Generator</span>
</div> </div>
<div class="field">
<input type="checkbox" id="history" />
<label for="history" title="To improve the reaction time or if you have trouble with large conversations."></label>
<span class="about">Disable History</span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@ const stop_generating = document.querySelector(`.stop_generating`);
const regenerate = document.querySelector(`.regenerate`); const regenerate = document.querySelector(`.regenerate`);
const send_button = document.querySelector(`#send-button`); const send_button = document.querySelector(`#send-button`);
const imageInput = document.querySelector('#image'); const imageInput = document.querySelector('#image');
const cameraInput = document.querySelector('#camera');
const fileInput = document.querySelector('#file'); const fileInput = document.querySelector('#file');
let prompt_lock = false; let prompt_lock = false;
@ -63,6 +64,10 @@ const handle_ask = async () => {
? '<img src="' + imageInput.dataset.src + '" alt="Image upload">' ? '<img src="' + imageInput.dataset.src + '" alt="Image upload">'
: '' : ''
} }
${cameraInput.dataset.src
? '<img src="' + cameraInput.dataset.src + '" alt="Image capture">'
: ''
}
</div> </div>
</div> </div>
`; `;
@ -95,6 +100,11 @@ const ask_gpt = async () => {
delete messages[i]["provider"]; 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.scrollTo(0, 0);
window.controller = new AbortController(); window.controller = new AbortController();
@ -141,9 +151,10 @@ const ask_gpt = async () => {
const headers = { const headers = {
accept: 'text/event-stream' 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(); const formData = new FormData();
formData.append('image', imageInput.files[0]); formData.append('image', input.files[0]);
formData.append('json', body); formData.append('json', body);
body = formData; body = formData;
} else { } else {
@ -211,8 +222,11 @@ const ask_gpt = async () => {
message_box.scrollTo({ top: message_box.scrollHeight, behavior: "auto" }); message_box.scrollTo({ top: message_box.scrollHeight, behavior: "auto" });
} }
} }
if (!error && imageInput) imageInput.value = ""; if (!error) {
if (!error && fileInput) fileInput.value = ""; if (imageInput) imageInput.value = "";
if (cameraInput) cameraInput.value = "";
if (fileInput) fileInput.value = "";
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -482,7 +496,7 @@ document.querySelector(".mobile-sidebar").addEventListener("click", (event) => {
}); });
const register_settings_localstorage = async () => { 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 = document.getElementById(id);
element.addEventListener('change', async (event) => { element.addEventListener('change', async (event) => {
switch (event.target.type) { switch (event.target.type) {
@ -500,7 +514,7 @@ const register_settings_localstorage = async () => {
} }
const load_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); element = document.getElementById(id);
value = localStorage.getItem(element.id); value = localStorage.getItem(element.id);
if (value) { if (value) {
@ -668,21 +682,26 @@ observer.observe(message_input, { attributes: true });
} }
document.getElementById("version_text").innerHTML = text document.getElementById("version_text").innerHTML = text
})() })()
imageInput.addEventListener('click', async (event) => { for (const el of [imageInput, cameraInput]) {
imageInput.value = ''; console.log(el.files);
delete imageInput.dataset.src; el.addEventListener('click', async () => {
}); el.value = '';
imageInput.addEventListener('change', async (event) => { delete el.dataset.src;
if (imageInput.files.length) { });
const reader = new FileReader(); do_load = async () => {
reader.addEventListener('load', (event) => { if (el.files.length) {
imageInput.dataset.src = event.target.result; delete imageInput.dataset.src;
}); delete cameraInput.dataset.src;
reader.readAsDataURL(imageInput.files[0]); const reader = new FileReader();
} else { reader.addEventListener('load', (event) => {
delete imageInput.dataset.src; el.dataset.src = event.target.result;
});
reader.readAsDataURL(el.files[0]);
}
} }
}); do_load()
el.addEventListener('change', do_load);
}
fileInput.addEventListener('click', async (event) => { fileInput.addEventListener('click', async (event) => {
fileInput.value = ''; fileInput.value = '';
delete fileInput.dataset.text; delete fileInput.dataset.text;

View File

@ -134,25 +134,31 @@ class Backend_Api:
dict: Arguments prepared for chat completion. dict: Arguments prepared for chat completion.
""" """
kwargs = {} kwargs = {}
if 'image' in request.files: if "image" in request.files:
file = request.files['image'] file = request.files['image']
if file.filename != '' and is_allowed_extension(file.filename): if file.filename != '' and is_allowed_extension(file.filename):
kwargs['image'] = to_image(file.stream, file.filename.endswith('.svg')) 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']) json_data = json.loads(request.form['json'])
else: else:
json_data = request.json json_data = request.json
provider = json_data.get('provider', '').replace('g4f.Provider.', '') provider = json_data.get('provider', '').replace('g4f.Provider.', '')
provider = provider if provider and provider != "Auto" else None provider = provider if provider and provider != "Auto" else None
if "image" in kwargs and not provider:
provider = "Bing"
if provider == 'OpenaiChat': if provider == 'OpenaiChat':
kwargs['auto_continue'] = True kwargs['auto_continue'] = True
messages = json_data['messages'] messages = json_data['messages']
if json_data.get('web_search'): if json_data.get('web_search'):
if provider == "Bing": if provider == "Bing":
kwargs['web_search'] = True kwargs['web_search'] = True
else: else:
messages[-1]["content"] = get_search_message(messages[-1]["content"]) messages[-1]["content"] = get_search_message(messages[-1]["content"])
model = json_data.get('model') model = json_data.get('model')
model = model if model else models.default model = model if model else models.default
patch = patch_provider if json_data.get('patch_provider') else None patch = patch_provider if json_data.get('patch_provider') else None

View File

@ -137,12 +137,12 @@ def get_orientation(image: Image) -> int:
if orientation is not None: if orientation is not None:
return orientation 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. Processes the given image by adjusting its orientation and resizing it.
Args: Args:
img (Image): The image to process. image (Image): The image to process.
new_width (int): The new width of the image. new_width (int): The new width of the image.
new_height (int): The new height 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. Image: The processed image.
""" """
# Fix orientation # Fix orientation
orientation = get_orientation(img) orientation = get_orientation(image)
if orientation: if orientation:
if orientation > 4: if orientation > 4:
img = img.transpose(FLIP_LEFT_RIGHT) image = image.transpose(FLIP_LEFT_RIGHT)
if orientation in [3, 4]: if orientation in [3, 4]:
img = img.transpose(ROTATE_180) image = image.transpose(ROTATE_180)
if orientation in [5, 6]: if orientation in [5, 6]:
img = img.transpose(ROTATE_270) image = image.transpose(ROTATE_270)
if orientation in [7, 8]: if orientation in [7, 8]:
img = img.transpose(ROTATE_90) image = image.transpose(ROTATE_90)
# Resize image # Resize image
img.thumbnail((new_width, new_height)) image.thumbnail((new_width, new_height))
# Remove transparency # Remove transparency
if img.mode == "RGBA": if image.mode == "RGBA":
img.load() image.load()
white = new_image('RGB', img.size, (255, 255, 255)) white = new_image('RGB', image.size, (255, 255, 255))
white.paste(img, mask=img.split()[-1]) white.paste(image, mask=image.split()[-1])
return white return white
return img elif image.mode != "RGB":
image = image.convert("RGB")
return image
def to_base64_jpg(image: Image, compression_rate: float) -> str: def to_base64_jpg(image: Image, compression_rate: float) -> str:
""" """