mirror of
https://github.com/xtekky/gpt4free.git
synced 2024-12-23 19:11:48 +03:00
Merge pull request #1726 from hlohaus/giga
Bug fixes and android support
This commit is contained in:
commit
79bc56f36e
3
.gitignore
vendored
3
.gitignore
vendored
@ -52,4 +52,5 @@ x.py
|
||||
info.txt
|
||||
local.py
|
||||
*.gguf
|
||||
image.py
|
||||
image.py
|
||||
.buildozer
|
@ -11,7 +11,7 @@ from aiohttp import ClientSession, ClientTimeout, BaseConnector, WSMsgType
|
||||
|
||||
from ..typing import AsyncResult, Messages, ImageType, Cookies
|
||||
from ..image import ImageRequest
|
||||
from ..errors import ResponseStatusError
|
||||
from ..errors import ResponseStatusError, RateLimitError
|
||||
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
|
||||
from .helper import get_connector, get_random_hex
|
||||
from .bing.upload_image import upload_image
|
||||
@ -26,7 +26,7 @@ class Tones:
|
||||
creative = "Creative"
|
||||
balanced = "Balanced"
|
||||
precise = "Precise"
|
||||
copilot = "Balanced"
|
||||
copilot = "Copilot"
|
||||
|
||||
class Bing(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
"""
|
||||
@ -36,8 +36,8 @@ class Bing(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
working = True
|
||||
supports_message_history = True
|
||||
supports_gpt_4 = True
|
||||
default_model = "balanced"
|
||||
models = [key for key in Tones.__dict__ if not key.startswith("__")]
|
||||
default_model = "Balanced"
|
||||
models = [getattr(Tones, key) for key in Tones.__dict__ if not key.startswith("__")]
|
||||
|
||||
@classmethod
|
||||
def create_async_generator(
|
||||
@ -72,7 +72,7 @@ class Bing(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
context = create_context(messages[:-1]) if len(messages) > 1 else None
|
||||
if tone is None:
|
||||
tone = tone if model.startswith("gpt-4") else model
|
||||
tone = cls.get_model("" if tone is None else tone.lower())
|
||||
tone = cls.get_model("" if tone is None else tone)
|
||||
gpt4_turbo = True if model.startswith("gpt-4-turbo") else False
|
||||
|
||||
return stream_generate(
|
||||
@ -258,7 +258,6 @@ class Defaults:
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'referer': home,
|
||||
'accept-encoding': 'gzip, deflate, br',
|
||||
'accept-language': 'en-US,en;q=0.9',
|
||||
}
|
||||
|
||||
@ -294,7 +293,7 @@ def create_message(
|
||||
:return: A formatted string message for the Bing API.
|
||||
"""
|
||||
|
||||
options_sets = Defaults.optionsSets[tone]
|
||||
options_sets = Defaults.optionsSets[tone.lower()]
|
||||
if not web_search and "nosearch" in options_sets:
|
||||
options_sets = options_sets["nosearch"]
|
||||
elif "default" in options_sets:
|
||||
@ -309,9 +308,9 @@ def create_message(
|
||||
"source": "cib",
|
||||
"optionsSets": options_sets,
|
||||
"allowedMessageTypes": Defaults.allowedMessageTypes,
|
||||
"sliceIds": Defaults.sliceIds[tone],
|
||||
"sliceIds": Defaults.sliceIds[tone.lower()],
|
||||
"verbosity": "verbose",
|
||||
"scenario": "CopilotMicrosoftCom" if tone == "copilot" else "SERP",
|
||||
"scenario": "CopilotMicrosoftCom" if tone == Tones.copilot else "SERP",
|
||||
"plugins": [{"id": "c310c353-b9f0-4d76-ab0d-1dd5e979cf68", "category": 1}] if web_search else [],
|
||||
"traceId": get_random_hex(40),
|
||||
"conversationHistoryOptionsSets": ["autosave","savemem","uprofupd","uprofgen"],
|
||||
@ -329,7 +328,7 @@ def create_message(
|
||||
"requestId": request_id,
|
||||
"messageId": request_id
|
||||
},
|
||||
"tone": getattr(Tones, tone),
|
||||
"tone": "Balanced" if tone == Tones.copilot else tone,
|
||||
"spokenTextMode": "None",
|
||||
"conversationId": conversation.conversationId,
|
||||
"participant": {"id": conversation.clientId}
|
||||
@ -412,10 +411,15 @@ async def stream_generate(
|
||||
await asyncio.sleep(sleep_retry)
|
||||
continue
|
||||
|
||||
image_request = await upload_image(session, image, getattr(Tones, tone), headers) if image else None
|
||||
image_request = await upload_image(
|
||||
session,
|
||||
image,
|
||||
"Balanced" if Tones.copilot == "Copilot" else tone,
|
||||
headers
|
||||
) if image else None
|
||||
async with session.ws_connect(
|
||||
'wss://s.copilot.microsoft.com/sydney/ChatHub'
|
||||
if tone == "copilot" else
|
||||
if tone == "Copilot" else
|
||||
'wss://sydney.bing.com/sydney/ChatHub',
|
||||
autoping=False,
|
||||
params={'sec_access_token': conversation.conversationSignature},
|
||||
@ -481,7 +485,7 @@ async def stream_generate(
|
||||
max_retries -= 1
|
||||
if max_retries < 1:
|
||||
if result["value"] == "CaptchaChallenge":
|
||||
raise RuntimeError(f"{result['value']}: Use other cookies or/and ip address")
|
||||
raise RateLimitError(f"{result['value']}: Use other cookies or/and ip address")
|
||||
else:
|
||||
raise RuntimeError(f"{result['value']}: {result['message']}")
|
||||
if debug.logging:
|
||||
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from ...requests import raise_for_status
|
||||
from ...errors import RateLimitError
|
||||
|
||||
class Conversation:
|
||||
"""
|
||||
@ -36,6 +37,8 @@ async def create_conversation(session: ClientSession, headers: dict, tone: str)
|
||||
else:
|
||||
url = "https://www.bing.com/turing/conversation/create?bundleVersion=1.1626.1"
|
||||
async with session.get(url, headers=headers) as response:
|
||||
if response.status == 404:
|
||||
raise RateLimitError("Response 404: Do less requests and reuse conversations")
|
||||
await raise_for_status(response, "Failed to create conversation")
|
||||
data = await response.json()
|
||||
conversationId = data.get('conversationId')
|
||||
|
@ -450,7 +450,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
|
||||
) as response:
|
||||
cls._update_request_args(session)
|
||||
await raise_for_status(response)
|
||||
async for chunk in cls.iter_messages_chunk(response.iter_lines(), session, fields, websocket_request_id):
|
||||
async for chunk in cls.iter_messages_chunk(response.iter_lines(), session, fields):
|
||||
if response_fields:
|
||||
response_fields = False
|
||||
yield fields
|
||||
|
@ -133,15 +133,15 @@
|
||||
<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 with Bing, Gemini, OpenaiChat and You">
|
||||
<label class="file-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">
|
||||
<label class="file-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 class="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/>
|
||||
<i class="fa-solid fa-paperclip"></i>
|
||||
</label>
|
||||
|
@ -482,25 +482,18 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
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="camera"] {
|
||||
.file-label {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.file-label:has(> input:valid),
|
||||
.file-label.selected {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
label[for="image"] {
|
||||
top: 32px;
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ async function add_message_chunk(message) {
|
||||
${message.provider.model ? ' with ' + message.provider.model : ''}
|
||||
`
|
||||
} else if (message.type == "message") {
|
||||
console.error(messag.message)
|
||||
console.error(message.message)
|
||||
} else if (message.type == "error") {
|
||||
window.error = message.error
|
||||
console.error(message.error);
|
||||
@ -240,6 +240,27 @@ async function add_message_chunk(message) {
|
||||
}
|
||||
}
|
||||
|
||||
cameraInput?.addEventListener("click", (e) => {
|
||||
if (window?.pywebview) {
|
||||
e.preventDefault();
|
||||
pywebview.api.choose_file();
|
||||
}
|
||||
})
|
||||
|
||||
cameraInput?.addEventListener("click", (e) => {
|
||||
if (window?.pywebview) {
|
||||
e.preventDefault();
|
||||
pywebview.api.take_picture();
|
||||
}
|
||||
})
|
||||
|
||||
imageInput?.addEventListener("click", (e) => {
|
||||
if (window?.pywebview) {
|
||||
e.preventDefault();
|
||||
pywebview.api.choose_image();
|
||||
}
|
||||
})
|
||||
|
||||
const ask_gpt = async () => {
|
||||
regenerate.classList.add(`regenerate-hidden`);
|
||||
messages = await get_messages(window.conversation_id);
|
||||
@ -307,8 +328,7 @@ const ask_gpt = async () => {
|
||||
console.error(e);
|
||||
if (e.name != "AbortError") {
|
||||
error = true;
|
||||
text = "oops ! something went wrong, please try again / reload. [stacktrace in console]";
|
||||
content_inner.innerHTML = text;
|
||||
content_inner.innerHTML += `<p><strong>An error occured:</strong> ${e}</p>`;
|
||||
}
|
||||
}
|
||||
if (!error && text) {
|
||||
@ -592,7 +612,7 @@ document.getElementById("cancelButton").addEventListener("click", async () => {
|
||||
console.log(`aborted ${window.conversation_id}`);
|
||||
});
|
||||
|
||||
document.getElementById(`regenerateButton`).addEventListener(`click`, async () => {
|
||||
document.getElementById("regenerateButton").addEventListener("click", async () => {
|
||||
prompt_lock = true;
|
||||
await hide_last_message(window.conversation_id);
|
||||
window.token = message_id();
|
||||
@ -622,14 +642,20 @@ const message_id = () => {
|
||||
async function hide_sidebar() {
|
||||
sidebar.classList.remove("shown");
|
||||
sidebar_button.classList.remove("rotated");
|
||||
if (window.location.pathname == "/menu/") {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', hide_sidebar, false);
|
||||
|
||||
sidebar_button.addEventListener("click", (event) => {
|
||||
if (sidebar.classList.contains("shown")) {
|
||||
hide_sidebar();
|
||||
} else {
|
||||
sidebar.classList.add("shown");
|
||||
sidebar_button.classList.add("rotated");
|
||||
history.pushState({}, null, "/menu/");
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
@ -817,19 +843,6 @@ async function on_api() {
|
||||
|
||||
register_settings_storage();
|
||||
|
||||
versions = await api("version");
|
||||
document.title = 'g4f - ' + versions["version"];
|
||||
let text = "version ~ "
|
||||
if (versions["version"] != versions["latest_version"]) {
|
||||
let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
|
||||
let title = `New version: ${versions["latest_version"]}`;
|
||||
text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]}</a> `;
|
||||
text += `<i class="fa-solid fa-rotate"></i>`
|
||||
} else {
|
||||
text += versions["version"];
|
||||
}
|
||||
document.getElementById("version_text").innerHTML = text
|
||||
|
||||
models = await api("models");
|
||||
models.forEach((model) => {
|
||||
let option = document.createElement("option");
|
||||
@ -845,9 +858,25 @@ async function on_api() {
|
||||
})
|
||||
|
||||
await load_provider_models(appStorage.getItem("provider"));
|
||||
load_settings_storage()
|
||||
await load_settings_storage()
|
||||
}
|
||||
|
||||
async function load_version() {
|
||||
const versions = await api("version");
|
||||
document.title = 'g4f - ' + versions["version"];
|
||||
let text = "version ~ "
|
||||
if (versions["version"] != versions["latest_version"]) {
|
||||
let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
|
||||
let title = `New version: ${versions["latest_version"]}`;
|
||||
text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]}</a> `;
|
||||
text += `<i class="fa-solid fa-rotate"></i>`
|
||||
} else {
|
||||
text += versions["version"];
|
||||
}
|
||||
document.getElementById("version_text").innerHTML = text
|
||||
}
|
||||
setTimeout(load_version, 5000);
|
||||
|
||||
for (const el of [imageInput, cameraInput]) {
|
||||
el.addEventListener('click', async () => {
|
||||
el.value = '';
|
||||
@ -913,13 +942,13 @@ function get_selected_model() {
|
||||
|
||||
async function api(ressource, args=null, file=null) {
|
||||
if (window?.pywebview) {
|
||||
if (args) {
|
||||
if (args !== null) {
|
||||
if (ressource == "models") {
|
||||
ressource = "provider_models";
|
||||
}
|
||||
return pywebview.api["get_" + ressource](args);
|
||||
return pywebview.api[`get_${ressource}`](args);
|
||||
}
|
||||
return pywebview.api["get_" + ressource]();
|
||||
return pywebview.api[`get_${ressource}`]();
|
||||
}
|
||||
if (ressource == "models" && args) {
|
||||
ressource = `${ressource}/${args}`;
|
||||
@ -930,7 +959,7 @@ async function api(ressource, args=null, file=null) {
|
||||
const headers = {
|
||||
accept: 'text/event-stream'
|
||||
}
|
||||
if (file) {
|
||||
if (file !== null) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('json', body);
|
||||
|
67
g4f/gui/server/android_gallery.py
Normal file
67
g4f/gui/server/android_gallery.py
Normal file
@ -0,0 +1,67 @@
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
|
||||
from jnius import autoclass
|
||||
from jnius import cast
|
||||
from android import activity
|
||||
|
||||
PythonActivity = autoclass('org.kivy.android.PythonActivity')
|
||||
Intent = autoclass('android.content.Intent')
|
||||
Uri = autoclass('android.net.Uri')
|
||||
|
||||
MEDIA_DATA = "_data"
|
||||
RESULT_LOAD_IMAGE = 1
|
||||
|
||||
Activity = autoclass('android.app.Activity')
|
||||
|
||||
def user_select_image(on_selection):
|
||||
"""Open Gallery Activity and call callback with absolute image filepath of image user selected.
|
||||
None if user canceled.
|
||||
"""
|
||||
|
||||
currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
|
||||
|
||||
# Forum discussion: https://groups.google.com/forum/#!msg/kivy-users/bjsG2j9bptI/-Oe_aGo0newJ
|
||||
def on_activity_result(request_code, result_code, intent):
|
||||
if request_code != RESULT_LOAD_IMAGE:
|
||||
Logger.warning('user_select_image: ignoring activity result that was not RESULT_LOAD_IMAGE')
|
||||
return
|
||||
|
||||
if result_code == Activity.RESULT_CANCELED:
|
||||
Clock.schedule_once(lambda dt: on_selection(None), 0)
|
||||
return
|
||||
|
||||
if result_code != Activity.RESULT_OK:
|
||||
# This may just go into the void...
|
||||
raise NotImplementedError('Unknown result_code "{}"'.format(result_code))
|
||||
|
||||
selectedImage = intent.getData(); # Uri
|
||||
filePathColumn = [MEDIA_DATA]; # String[]
|
||||
# Cursor
|
||||
cursor = currentActivity.getContentResolver().query(selectedImage,
|
||||
filePathColumn, None, None, None);
|
||||
cursor.moveToFirst();
|
||||
|
||||
# int
|
||||
columnIndex = cursor.getColumnIndex(filePathColumn[0]);
|
||||
# String
|
||||
picturePath = cursor.getString(columnIndex);
|
||||
cursor.close();
|
||||
Logger.info('android_ui: user_select_image() selected %s', picturePath)
|
||||
|
||||
# This is possibly in a different thread?
|
||||
Clock.schedule_once(lambda dt: on_selection(picturePath), 0)
|
||||
|
||||
# See: http://pyjnius.readthedocs.org/en/latest/android.html
|
||||
activity.bind(on_activity_result=on_activity_result)
|
||||
|
||||
intent = Intent()
|
||||
|
||||
# http://programmerguru.com/android-tutorial/how-to-pick-image-from-gallery/
|
||||
# http://stackoverflow.com/questions/18416122/open-gallery-app-in-android
|
||||
intent.setAction(Intent.ACTION_PICK)
|
||||
# TODO internal vs external?
|
||||
intent.setData(Uri.parse('content://media/internal/images/media'))
|
||||
# TODO setType(Image)?
|
||||
|
||||
currentActivity.startActivityForResult(intent, RESULT_LOAD_IMAGE)
|
@ -1,11 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import json
|
||||
import os.path
|
||||
from typing import Iterator
|
||||
from uuid import uuid4
|
||||
from functools import partial
|
||||
|
||||
try:
|
||||
import webview
|
||||
import platformdirs
|
||||
except ImportError:
|
||||
...
|
||||
try:
|
||||
from plyer import camera
|
||||
from plyer import filechooser
|
||||
has_plyer = True
|
||||
except ImportError:
|
||||
has_plyer = False
|
||||
try:
|
||||
from android.runnable import run_on_ui_thread
|
||||
from android.storage import app_storage_path
|
||||
from android.permissions import request_permissions, Permission
|
||||
from android.permissions import _RequestPermissionsManager
|
||||
_RequestPermissionsManager.register_callback()
|
||||
from .android_gallery import user_select_image
|
||||
has_android = True
|
||||
except ImportError:
|
||||
run_on_ui_thread = lambda a : a
|
||||
app_storage_path = platformdirs.user_pictures_dir
|
||||
user_select_image = partial(
|
||||
filechooser.open_file,
|
||||
path=platformdirs.user_pictures_dir(),
|
||||
filters=[["Image", "*.jpg", "*.jpeg", "*.png", "*.webp", "*.svg"]],
|
||||
)
|
||||
has_android = False
|
||||
|
||||
from g4f import version, models
|
||||
from g4f import get_last_provider, ChatCompletion
|
||||
@ -75,13 +104,71 @@ class Api():
|
||||
return {'title': ''}
|
||||
|
||||
def get_conversation(self, options: dict, **kwargs) -> Iterator:
|
||||
window = webview.active_window()
|
||||
window = webview.windows[0]
|
||||
if hasattr(self, "image") and self.image is not None:
|
||||
kwargs["image"] = open(self.image, "rb")
|
||||
for message in self._create_response_stream(
|
||||
self._prepare_conversation_kwargs(options, kwargs),
|
||||
options.get("conversation_id")
|
||||
):
|
||||
if not window.evaluate_js(f"if (!this.abort) this.add_message_chunk({json.dumps(message)}); !this.abort && !this.error;"):
|
||||
break
|
||||
self.image = None
|
||||
self.set_selected(None)
|
||||
|
||||
@run_on_ui_thread
|
||||
def choose_file(self):
|
||||
self.request_permissions()
|
||||
filechooser.open_file(
|
||||
path=platformdirs.user_pictures_dir(),
|
||||
on_selection=print
|
||||
)
|
||||
|
||||
@run_on_ui_thread
|
||||
def choose_image(self):
|
||||
self.request_permissions()
|
||||
user_select_image(
|
||||
on_selection=self.on_image_selection
|
||||
)
|
||||
|
||||
@run_on_ui_thread
|
||||
def take_picture(self):
|
||||
self.request_permissions()
|
||||
filename = os.path.join(app_storage_path(), f"chat-{uuid4()}.png")
|
||||
camera.take_picture(filename=filename, on_complete=self.on_camera)
|
||||
|
||||
def on_image_selection(self, filename):
|
||||
if filename is not None and os.path.exists(filename):
|
||||
self.image = filename
|
||||
else:
|
||||
self.image = None
|
||||
self.set_selected(None if self.image is None else "image")
|
||||
|
||||
def on_camera(self, filename):
|
||||
if filename is not None and os.path.exists(filename):
|
||||
self.image = filename
|
||||
else:
|
||||
self.image = None
|
||||
self.set_selected(None if self.image is None else "camera")
|
||||
|
||||
def set_selected(self, input_id: str = None):
|
||||
window = webview.windows[0]
|
||||
if window is not None:
|
||||
window.evaluate_js(
|
||||
f"document.querySelector(`.file-label.selected`)?.classList.remove(`selected`);"
|
||||
)
|
||||
if input_id is not None and input_id in ("image", "camera"):
|
||||
window.evaluate_js(
|
||||
f'document.querySelector(`label[for="{input_id}"]`)?.classList.add(`selected`);'
|
||||
)
|
||||
|
||||
def request_permissions(self):
|
||||
if has_android:
|
||||
request_permissions([
|
||||
Permission.CAMERA,
|
||||
Permission.READ_EXTERNAL_STORAGE,
|
||||
Permission.WRITE_EXTERNAL_STORAGE
|
||||
])
|
||||
|
||||
def _prepare_conversation_kwargs(self, json_data: dict, kwargs: dict):
|
||||
"""
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import os.path
|
||||
import webview
|
||||
@ -20,6 +22,8 @@ def run_webview(
|
||||
dirname = sys._MEIPASS
|
||||
else:
|
||||
dirname = os.path.dirname(__file__)
|
||||
webview.settings['OPEN_EXTERNAL_LINKS_IN_BROWSER'] = False
|
||||
webview.settings['ALLOW_DOWNLOADS'] = True
|
||||
webview.create_window(
|
||||
f"g4f - {g4f.version.utils.current_version}",
|
||||
os.path.join(dirname, "client/index.html"),
|
||||
|
@ -1,2 +1,3 @@
|
||||
requests
|
||||
aiohttp
|
||||
aiohttp
|
||||
brotli
|
Loading…
Reference in New Issue
Block a user