返回提交历史
Modified
.gitignore
+2
-1
Modified
g4f/Provider/Bing.py
+15
-11
Modified
g4f/Provider/bing/conversation.py
+3
-0
Modified
g4f/Provider/needs_auth/OpenaiChat.py
+1
-1
Modified
g4f/gui/client/index.html
+3
-3
Modified
g4f/gui/client/static/css/style.css
+6
-13
Modified
g4f/gui/client/static/js/chat.v1.js
+51
-22
Added
g4f/gui/server/android_gallery.py
+67
-0
Modified
g4f/gui/server/api.py
+88
-1
Modified
g4f/gui/webview.py
+4
-0
Modified
requirements-min.txt
+2
-1
Modified
setup.py
+1
-0
XFEstudio/gpt4free
Fix missing websocket_request_id in OpenaiChat Add RateLimitErrors to Bing Add android support to gui api Add annotations import to gui api
486e43da
代码差异
12 个文件
+243
-53
@@ -52,4 +52,5 @@ x.py
52
52
info.txt
53
53
local.py
54
54
*.gguf
55
image.py
55
image.py
56
.buildozer
@@ -11,7 +11,7 @@ from aiohttp import ClientSession, ClientTimeout, BaseConnector, WSMsgType
11
11
12
12
from ..typing import AsyncResult, Messages, ImageType, Cookies
13
13
from ..image import ImageRequest
14
from ..errors import ResponseStatusError
14
from ..errors import ResponseStatusError, RateLimitError
15
15
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
16
16
from .helper import get_connector, get_random_hex
17
17
from .bing.upload_image import upload_image
@@ -26,7 +26,7 @@ class Tones:
26
26
creative = "Creative"
27
27
balanced = "Balanced"
28
28
precise = "Precise"
29
copilot = "Balanced"
29
copilot = "Copilot"
30
30
31
31
class Bing(AsyncGeneratorProvider, ProviderModelMixin):
32
32
"""
@@ -36,8 +36,8 @@ class Bing(AsyncGeneratorProvider, ProviderModelMixin):
36
36
working = True
37
37
supports_message_history = True
38
38
supports_gpt_4 = True
39
default_model = "balanced"
40
models = [key for key in Tones.__dict__ if not key.startswith("__")]
39
default_model = "Balanced"
40
models = [getattr(Tones, key) for key in Tones.__dict__ if not key.startswith("__")]
41
41
42
42
@classmethod
43
43
def create_async_generator(
@@ -72,7 +72,7 @@ class Bing(AsyncGeneratorProvider, ProviderModelMixin):
72
72
context = create_context(messages[:-1]) if len(messages) > 1 else None
73
73
if tone is None:
74
74
tone = tone if model.startswith("gpt-4") else model
75
tone = cls.get_model("" if tone is None else tone.lower())
75
tone = cls.get_model("" if tone is None else tone)
76
76
gpt4_turbo = True if model.startswith("gpt-4-turbo") else False
77
77
78
78
return stream_generate(
@@ -258,7 +258,6 @@ class Defaults:
258
258
'sec-fetch-mode': 'cors',
259
259
'sec-fetch-dest': 'empty',
260
260
'referer': home,
261
'accept-encoding': 'gzip, deflate, br',
262
261
'accept-language': 'en-US,en;q=0.9',
263
262
}
264
263
@@ -311,7 +310,7 @@ def create_message(
311
310
"allowedMessageTypes": Defaults.allowedMessageTypes,
312
311
"sliceIds": Defaults.sliceIds[tone],
313
312
"verbosity": "verbose",
314
"scenario": "CopilotMicrosoftCom" if tone == "copilot" else "SERP",
313
"scenario": "CopilotMicrosoftCom" if tone == Tones.copilot else "SERP",
315
314
"plugins": [{"id": "c310c353-b9f0-4d76-ab0d-1dd5e979cf68", "category": 1}] if web_search else [],
316
315
"traceId": get_random_hex(40),
317
316
"conversationHistoryOptionsSets": ["autosave","savemem","uprofupd","uprofgen"],
@@ -329,7 +328,7 @@ def create_message(
329
328
"requestId": request_id,
330
329
"messageId": request_id
331
330
},
332
"tone": getattr(Tones, tone),
331
"tone": "Balanced" if tone == Tones.copilot else tone,
333
332
"spokenTextMode": "None",
334
333
"conversationId": conversation.conversationId,
335
334
"participant": {"id": conversation.clientId}
@@ -412,10 +411,15 @@ async def stream_generate(
412
411
await asyncio.sleep(sleep_retry)
413
412
continue
414
413
415
image_request = await upload_image(session, image, getattr(Tones, tone), headers) if image else None
414
image_request = await upload_image(
415
session,
416
image,
417
"Balanced" if Tones.copilot == "Copilot" else tone,
418
headers
419
) if image else None
416
420
async with session.ws_connect(
417
421
'wss://s.copilot.microsoft.com/sydney/ChatHub'
418
if tone == "copilot" else
422
if tone == "Copilot" else
419
423
'wss://sydney.bing.com/sydney/ChatHub',
420
424
autoping=False,
421
425
params={'sec_access_token': conversation.conversationSignature},
@@ -481,7 +485,7 @@ async def stream_generate(
481
485
max_retries -= 1
482
486
if max_retries < 1:
483
487
if result["value"] == "CaptchaChallenge":
484
raise RuntimeError(f"{result['value']}: Use other cookies or/and ip address")
488
raise RateLimitError(f"{result['value']}: Use other cookies or/and ip address")
485
489
else:
486
490
raise RuntimeError(f"{result['value']}: {result['message']}")
487
491
if debug.logging:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
3
3
from aiohttp import ClientSession
4
4
from ...requests import raise_for_status
5
from ...errors import RateLimitError
5
6
6
7
class Conversation:
7
8
"""
@@ -36,6 +37,8 @@ async def create_conversation(session: ClientSession, headers: dict, tone: str)
36
37
else:
37
38
url = "https://www.bing.com/turing/conversation/create?bundleVersion=1.1626.1"
38
39
async with session.get(url, headers=headers) as response:
40
if response.status == 404:
41
raise RateLimitError("Response 404: Do less requests and reuse conversations")
39
42
await raise_for_status(response, "Failed to create conversation")
40
43
data = await response.json()
41
44
conversationId = data.get('conversationId')
@@ -450,7 +450,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
450
450
) as response:
451
451
cls._update_request_args(session)
452
452
await raise_for_status(response)
453
async for chunk in cls.iter_messages_chunk(response.iter_lines(), session, fields, websocket_request_id):
453
async for chunk in cls.iter_messages_chunk(response.iter_lines(), session, fields):
454
454
if response_fields:
455
455
response_fields = False
456
456
yield fields
@@ -133,15 +133,15 @@
133
133
<div class="box input-box">
134
134
<textarea id="message-input" placeholder="Ask a question" cols="30" rows="10"
135
135
style="white-space: pre-wrap;resize: none;"></textarea>
136
<label for="image" title="Works with Bing, Gemini, OpenaiChat and You">
136
<label class="file-label" for="image" title="Works with Bing, Gemini, OpenaiChat and You">
137
137
<input type="file" id="image" name="image" accept="image/*" required/>
138
138
<i class="fa-regular fa-image"></i>
139
139
</label>
140
<label for="camera">
140
<label class="file-label" for="camera">
141
141
<input type="file" id="camera" name="camera" accept="image/*" capture="camera" required/>
142
142
<i class="fa-solid fa-camera"></i>
143
143
</label>
144
<label for="file">
144
<label class="file-label" for="file">
145
145
<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/>
146
146
<i class="fa-solid fa-paperclip"></i>
147
147
</label>
@@ -482,25 +482,18 @@ body {
482
482
display: none;
483
483
}
484
484
485
label[for="image"]:has(> input:valid){
486
color: var(--accent);
487
}
488
489
label[for="camera"]:has(> input:valid){
490
color: var(--accent);
491
}
492
493
label[for="file"]:has(> input:valid){
494
color: var(--accent);
495
}
496
497
label[for="image"], label[for="file"], label[for="camera"] {
485
.file-label {
498
486
cursor: pointer;
499
487
position: absolute;
500
488
top: 10px;
501
489
left: 10px;
502
490
}
503
491
492
.file-label:has(> input:valid),
493
.file-label.selected {
494
color: var(--accent);
495
}
496
504
497
label[for="image"] {
505
498
top: 32px;
506
499
}
@@ -211,7 +211,7 @@ async function add_message_chunk(message) {
211
211
${message.provider.model ? ' with ' + message.provider.model : ''}
212
212
`
213
213
} else if (message.type == "message") {
214
console.error(messag.message)
214
console.error(message.message)
215
215
} else if (message.type == "error") {
216
216
window.error = message.error
217
217
console.error(message.error);
@@ -240,6 +240,27 @@ async function add_message_chunk(message) {
240
240
}
241
241
}
242
242
243
cameraInput?.addEventListener("click", (e) => {
244
if (window?.pywebview) {
245
e.preventDefault();
246
pywebview.api.choose_file();
247
}
248
})
249
250
cameraInput?.addEventListener("click", (e) => {
251
if (window?.pywebview) {
252
e.preventDefault();
253
pywebview.api.take_picture();
254
}
255
})
256
257
imageInput?.addEventListener("click", (e) => {
258
if (window?.pywebview) {
259
e.preventDefault();
260
pywebview.api.choose_image();
261
}
262
})
263
243
264
const ask_gpt = async () => {
244
265
regenerate.classList.add(`regenerate-hidden`);
245
266
messages = await get_messages(window.conversation_id);
@@ -307,8 +328,7 @@ const ask_gpt = async () => {
307
328
console.error(e);
308
329
if (e.name != "AbortError") {
309
330
error = true;
310
text = "oops ! something went wrong, please try again / reload. [stacktrace in console]";
311
content_inner.innerHTML = text;
331
content_inner.innerHTML += `<p><strong>An error occured:</strong> ${e}</p>`;
312
332
}
313
333
}
314
334
if (!error && text) {
@@ -592,7 +612,7 @@ document.getElementById("cancelButton").addEventListener("click", async () => {
592
612
console.log(`aborted ${window.conversation_id}`);
593
613
});
594
614
595
document.getElementById(`regenerateButton`).addEventListener(`click`, async () => {
615
document.getElementById("regenerateButton").addEventListener("click", async () => {
596
616
prompt_lock = true;
597
617
await hide_last_message(window.conversation_id);
598
618
window.token = message_id();
@@ -622,14 +642,20 @@ const message_id = () => {
622
642
async function hide_sidebar() {
623
643
sidebar.classList.remove("shown");
624
644
sidebar_button.classList.remove("rotated");
645
if (window.location.pathname == "/menu/") {
646
history.back();
647
}
625
648
}
626
649
650
window.addEventListener('popstate', hide_sidebar, false);
651
627
652
sidebar_button.addEventListener("click", (event) => {
628
653
if (sidebar.classList.contains("shown")) {
629
654
hide_sidebar();
630
655
} else {
631
656
sidebar.classList.add("shown");
632
657
sidebar_button.classList.add("rotated");
658
history.pushState({}, null, "/menu/");
633
659
}
634
660
window.scrollTo(0, 0);
635
661
});
@@ -817,19 +843,6 @@ async function on_api() {
817
843
818
844
register_settings_storage();
819
845
820
versions = await api("version");
821
document.title = 'g4f - ' + versions["version"];
822
let text = "version ~ "
823
if (versions["version"] != versions["latest_version"]) {
824
let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
825
let title = `New version: ${versions["latest_version"]}`;
826
text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]}</a> `;
827
text += `<i class="fa-solid fa-rotate"></i>`
828
} else {
829
text += versions["version"];
830
}
831
document.getElementById("version_text").innerHTML = text
832
833
846
models = await api("models");
834
847
models.forEach((model) => {
835
848
let option = document.createElement("option");
@@ -845,8 +858,24 @@ async function on_api() {
845
858
})
846
859
847
860
await load_provider_models(appStorage.getItem("provider"));
848
load_settings_storage()
861
await load_settings_storage()
862
}
863
864
async function load_version() {
865
const versions = await api("version");
866
document.title = 'g4f - ' + versions["version"];
867
let text = "version ~ "
868
if (versions["version"] != versions["latest_version"]) {
869
let release_url = 'https://github.com/xtekky/gpt4free/releases/tag/' + versions["latest_version"];
870
let title = `New version: ${versions["latest_version"]}`;
871
text += `<a href="${release_url}" target="_blank" title="${title}">${versions["version"]}</a> `;
872
text += `<i class="fa-solid fa-rotate"></i>`
873
} else {
874
text += versions["version"];
875
}
876
document.getElementById("version_text").innerHTML = text
849
877
}
878
setTimeout(load_version, 5000);
850
879
851
880
for (const el of [imageInput, cameraInput]) {
852
881
el.addEventListener('click', async () => {
@@ -913,13 +942,13 @@ function get_selected_model() {
913
942
914
943
async function api(ressource, args=null, file=null) {
915
944
if (window?.pywebview) {
916
if (args) {
945
if (args !== null) {
917
946
if (ressource == "models") {
918
947
ressource = "provider_models";
919
948
}
920
return pywebview.api["get_" + ressource](args);
949
return pywebview.api[`get_${ressource}`](args);
921
950
}
922
return pywebview.api["get_" + ressource]();
951
return pywebview.api[`get_${ressource}`]();
923
952
}
924
953
if (ressource == "models" && args) {
925
954
ressource = `${ressource}/${args}`;
@@ -930,7 +959,7 @@ async function api(ressource, args=null, file=null) {
930
959
const headers = {
931
960
accept: 'text/event-stream'
932
961
}
933
if (file) {
962
if (file !== null) {
934
963
const formData = new FormData();
935
964
formData.append('file', file);
936
965
formData.append('json', body);
@@ -0,0 +1,67 @@
1
from kivy.logger import Logger
2
from kivy.clock import Clock
3
4
from jnius import autoclass
5
from jnius import cast
6
from android import activity
7
8
PythonActivity = autoclass('org.kivy.android.PythonActivity')
9
Intent = autoclass('android.content.Intent')
10
Uri = autoclass('android.net.Uri')
11
12
MEDIA_DATA = "_data"
13
RESULT_LOAD_IMAGE = 1
14
15
Activity = autoclass('android.app.Activity')
16
17
def user_select_image(on_selection):
18
"""Open Gallery Activity and call callback with absolute image filepath of image user selected.
19
None if user canceled.
20
"""
21
22
currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
23
24
# Forum discussion: https://groups.google.com/forum/#!msg/kivy-users/bjsG2j9bptI/-Oe_aGo0newJ
25
def on_activity_result(request_code, result_code, intent):
26
if request_code != RESULT_LOAD_IMAGE:
27
Logger.warning('user_select_image: ignoring activity result that was not RESULT_LOAD_IMAGE')
28
return
29
30
if result_code == Activity.RESULT_CANCELED:
31
Clock.schedule_once(lambda dt: on_selection(None), 0)
32
return
33
34
if result_code != Activity.RESULT_OK:
35
# This may just go into the void...
36
raise NotImplementedError('Unknown result_code "{}"'.format(result_code))
37
38
selectedImage = intent.getData(); # Uri
39
filePathColumn = [MEDIA_DATA]; # String[]
40
# Cursor
41
cursor = currentActivity.getContentResolver().query(selectedImage,
42
filePathColumn, None, None, None);
43
cursor.moveToFirst();
44
45
# int
46
columnIndex = cursor.getColumnIndex(filePathColumn[0]);
47
# String
48
picturePath = cursor.getString(columnIndex);
49
cursor.close();
50
Logger.info('android_ui: user_select_image() selected %s', picturePath)
51
52
# This is possibly in a different thread?
53
Clock.schedule_once(lambda dt: on_selection(picturePath), 0)
54
55
# See: http://pyjnius.readthedocs.org/en/latest/android.html
56
activity.bind(on_activity_result=on_activity_result)
57
58
intent = Intent()
59
60
# http://programmerguru.com/android-tutorial/how-to-pick-image-from-gallery/
61
# http://stackoverflow.com/questions/18416122/open-gallery-app-in-android
62
intent.setAction(Intent.ACTION_PICK)
63
# TODO internal vs external?
64
intent.setData(Uri.parse('content://media/internal/images/media'))
65
# TODO setType(Image)?
66
67
currentActivity.startActivityForResult(intent, RESULT_LOAD_IMAGE)
@@ -1,11 +1,40 @@
1
from __future__ import annotations
2
1
3
import logging
2
4
import json
5
import os.path
3
6
from typing import Iterator
7
from uuid import uuid4
8
from functools import partial
4
9
5
10
try:
6
11
import webview
12
import platformdirs
7
13
except ImportError:
8
14
...
15
try:
16
from plyer import camera
17
from plyer import filechooser
18
has_plyer = True
19
except ImportError:
20
has_plyer = False
21
try:
22
from android.runnable import run_on_ui_thread
23
from android.storage import app_storage_path
24
from android.permissions import request_permissions, Permission
25
from android.permissions import _RequestPermissionsManager
26
_RequestPermissionsManager.register_callback()
27
from .android_gallery import user_select_image
28
has_android = True
29
except ImportError:
30
run_on_ui_thread = lambda a : a
31
app_storage_path = platformdirs.user_pictures_dir
32
user_select_image = partial(
33
filechooser.open_file,
34
path=platformdirs.user_pictures_dir(),
35
filters=[["Image", "*.jpg", "*.jpeg", "*.png", "*.webp", "*.svg"]],
36
)
37
has_android = False
9
38
10
39
from g4f import version, models
11
40
from g4f import get_last_provider, ChatCompletion
@@ -75,13 +104,71 @@ class Api():
75
104
return {'title': ''}
76
105
77
106
def get_conversation(self, options: dict, **kwargs) -> Iterator:
78
window = webview.active_window()
107
window = webview.windows[0]
108
if hasattr(self, "image") and self.image is not None:
109
kwargs["image"] = open(self.image, "rb")
79
110
for message in self._create_response_stream(
80
111
self._prepare_conversation_kwargs(options, kwargs),
81
112
options.get("conversation_id")
82
113
):
83
114
if not window.evaluate_js(f"if (!this.abort) this.add_message_chunk({json.dumps(message)}); !this.abort && !this.error;"):
84
115
break
116
self.image = None
117
self.set_selected(None)
118
119
@run_on_ui_thread
120
def choose_file(self):
121
self.request_permissions()
122
filechooser.open_file(
123
path=platformdirs.user_pictures_dir(),
124
on_selection=print
125
)
126
127
@run_on_ui_thread
128
def choose_image(self):
129
self.request_permissions()
130
user_select_image(
131
on_selection=self.on_image_selection
132
)
133
134
@run_on_ui_thread
135
def take_picture(self):
136
self.request_permissions()
137
filename = os.path.join(app_storage_path(), f"chat-{uuid4()}.png")
138
camera.take_picture(filename=filename, on_complete=self.on_camera)
139
140
def on_image_selection(self, filename):
141
if filename is not None and os.path.exists(filename):
142
self.image = filename
143
else:
144
self.image = None
145
self.set_selected(None if self.image is None else "image")
146
147
def on_camera(self, filename):
148
if filename is not None and os.path.exists(filename):
149
self.image = filename
150
else:
151
self.image = None
152
self.set_selected(None if self.image is None else "camera")
153
154
def set_selected(self, input_id: str = None):
155
window = webview.windows[0]
156
if window is not None:
157
window.evaluate_js(
158
f"document.querySelector(`.file-label.selected`)?.classList.remove(`selected`);"
159
)
160
if input_id is not None and input_id in ("image", "camera"):
161
window.evaluate_js(
162
f'document.querySelector(`label[for="{input_id}"]`)?.classList.add(`selected`);'
163
)
164
165
def request_permissions(self):
166
if has_android:
167
request_permissions([
168
Permission.CAMERA,
169
Permission.READ_EXTERNAL_STORAGE,
170
Permission.WRITE_EXTERNAL_STORAGE
171
])
85
172
86
173
def _prepare_conversation_kwargs(self, json_data: dict, kwargs: dict):
87
174
"""
@@ -1,3 +1,5 @@
1
from __future__ import annotations
2
1
3
import sys
2
4
import os.path
3
5
import webview
@@ -20,6 +22,8 @@ def run_webview(
20
22
dirname = sys._MEIPASS
21
23
else:
22
24
dirname = os.path.dirname(__file__)
25
webview.settings['OPEN_EXTERNAL_LINKS_IN_BROWSER'] = False
26
webview.settings['ALLOW_DOWNLOADS'] = True
23
27
webview.create_window(
24
28
f"g4f - {g4f.version.utils.current_version}",
25
29
os.path.join(dirname, "client/index.html"),
@@ -1,2 +1,3 @@
1
1
requests
2
aiohttp
2
aiohttp
3
brotli
@@ -11,6 +11,7 @@ with codecs.open(os.path.join(here, 'README.md'), encoding='utf-8') as fh:
11
11
INSTALL_REQUIRE = [
12
12
"requests",
13
13
"aiohttp",
14
"brotli"
14
15
]
15
16
16
17
EXTRA_REQUIRE = {