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

View File

@ -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"],
}

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import os
import time
try:
from platformdirs import user_config_dir
@ -72,6 +73,7 @@ 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:
if not cookie.expires or cookie.expires > time.time():
cookies[cookie.name] = cookie.value
if single_browser and len(cookie_jar):
break

View File

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

View File

@ -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"] {

View File

@ -114,10 +114,14 @@
<div class="box input-box">
<textarea id="message-input" placeholder="Ask a question" cols="30" rows="10"
style="white-space: pre-wrap;resize: none;"></textarea>
<label for="image" title="Works only with Bing and OpenaiChat">
<input type="file" id="image" name="image" accept="image/png, image/gif, image/jpeg, image/svg+xml" required/>
<label for="image" title="Works with Bing, Gemini, OpenaiChat and You">
<input type="file" id="image" name="image" accept="image/*" required/>
<i class="fa-regular fa-image"></i>
</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">
<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>
@ -157,20 +161,26 @@
<option value="Gemini">Gemini</option>
<option value="Liaobots">Liaobots</option>
<option value="Phind">Phind</option>
<option value="You">You</option>
<option value="">----</option>
</select>
</div>
</div>
<div class="field">
<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>
</div>
<div class="field">
<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>
</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>

View File

@ -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 () => {
? '<img src="' + imageInput.dataset.src + '" alt="Image upload">'
: ''
}
${cameraInput.dataset.src
? '<img src="' + cameraInput.dataset.src + '" alt="Image capture">'
: ''
}
</div>
</div>
`;
@ -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 = '';
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;
});
imageInput.addEventListener('change', async (event) => {
if (imageInput.files.length) {
delete cameraInput.dataset.src;
const reader = new FileReader();
reader.addEventListener('load', (event) => {
imageInput.dataset.src = event.target.result;
el.dataset.src = event.target.result;
});
reader.readAsDataURL(imageInput.files[0]);
} else {
delete imageInput.dataset.src;
reader.readAsDataURL(el.files[0]);
}
});
}
do_load()
el.addEventListener('change', do_load);
}
fileInput.addEventListener('click', async (event) => {
fileInput.value = '';
delete fileInput.dataset.text;

View File

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

View File

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