diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | etc/tool/vercel.py | 2 | ||||
-rw-r--r-- | g4f/Provider/Bing.py | 264 | ||||
-rw-r--r-- | g4f/__init__.py | 2 | ||||
-rw-r--r-- | requirements.txt | 3 | ||||
-rw-r--r-- | setup.py | 2 |
6 files changed, 228 insertions, 47 deletions
@@ -2,7 +2,7 @@ By using this repository or any code related to it, you agree to the [legal notice](./LEGAL_NOTICE.md). The author is not responsible for any copies, forks, reuploads made by other users, or anything else related to gpt4free. This is the author's only account and repository. To prevent impersonation or irresponsible actions, please comply with the GNU GPL license this Repository uses. -- latest pypi version: [`0.1.7.3`](https://pypi.org/project/g4f/0.1.7.3) +- latest pypi version: [`0.1.7.4`](https://pypi.org/project/g4f/0.1.7.4) ```sh pip install -U g4f ``` diff --git a/etc/tool/vercel.py b/etc/tool/vercel.py index 75df6c70..7b87e298 100644 --- a/etc/tool/vercel.py +++ b/etc/tool/vercel.py @@ -100,4 +100,4 @@ def main(): if __name__ == "__main__": - main()
\ No newline at end of file + main() diff --git a/g4f/Provider/Bing.py b/g4f/Provider/Bing.py index f1b50f7c..91e9dc2c 100644 --- a/g4f/Provider/Bing.py +++ b/g4f/Provider/Bing.py @@ -1,10 +1,16 @@ from __future__ import annotations +import string import random import json import os +import re +import io +import base64 +import numpy as np import uuid import urllib.parse +from PIL import Image from aiohttp import ClientSession, ClientTimeout from ..typing import AsyncResult, Messages from .base_provider import AsyncGeneratorProvider @@ -35,6 +41,7 @@ class Bing(AsyncGeneratorProvider): proxy: str = None, cookies: dict = None, tone: str = Tones.creative, + image: str = None, **kwargs ) -> AsyncResult: if len(messages) < 2: @@ -46,7 +53,7 @@ class Bing(AsyncGeneratorProvider): if not cookies or "SRCHD" not in cookies: cookies = default_cookies - return stream_generate(prompt, tone, context, proxy, cookies) + return stream_generate(prompt, tone, image, context, proxy, cookies) def create_context(messages: Messages): context = "".join(f"[{message['role']}](#message)\n{message['content']}\n\n" for message in messages) @@ -54,14 +61,14 @@ def create_context(messages: Messages): return context class Conversation(): - def __init__(self, conversationId: str, clientId: str, conversationSignature: str) -> None: + def __init__(self, conversationId: str, clientId: str, conversationSignature: str, imageInfo: dict=None) -> None: self.conversationId = conversationId self.clientId = clientId self.conversationSignature = conversationSignature + self.imageInfo = imageInfo -async def create_conversation(session: ClientSession, proxy: str = None) -> Conversation: - url = 'https://www.bing.com/turing/conversation/create?bundleVersion=1.1150.3' - +async def create_conversation(session: ClientSession, tone: str, image: str = None, proxy: str = None) -> Conversation: + url = 'https://www.bing.com/turing/conversation/create?bundleVersion=1.1199.4' async with await session.get(url, proxy=proxy) as response: data = await response.json() @@ -71,8 +78,65 @@ async def create_conversation(session: ClientSession, proxy: str = None) -> Conv if not conversationId or not clientId or not conversationSignature: raise Exception('Failed to create conversation.') - - return Conversation(conversationId, clientId, conversationSignature) + conversation = Conversation(conversationId, clientId, conversationSignature, None) + if isinstance(image,str): + try: + config = { + "visualSearch": { + "maxImagePixels": 360000, + "imageCompressionRate": 0.7, + "enableFaceBlurDebug": 0, + } + } + is_data_uri_an_image(image) + img_binary_data = extract_data_uri(image) + is_accepted_format(img_binary_data) + img = Image.open(io.BytesIO(img_binary_data)) + width, height = img.size + max_image_pixels = config['visualSearch']['maxImagePixels'] + compression_rate = config['visualSearch']['imageCompressionRate'] + + if max_image_pixels / (width * height) < 1: + new_width = int(width * np.sqrt(max_image_pixels / (width * height))) + new_height = int(height * np.sqrt(max_image_pixels / (width * height))) + else: + new_width = width + new_height = height + try: + orientation = get_orientation(img) + except Exception: + orientation = None + new_img = process_image(orientation, img, new_width, new_height) + new_img_binary_data = compress_image_to_base64(new_img, compression_rate) + data, boundary = build_image_upload_api_payload(new_img_binary_data, conversation, tone) + headers = session.headers.copy() + headers["content-type"] = 'multipart/form-data; boundary=' + boundary + headers["referer"] = 'https://www.bing.com/search?q=Bing+AI&showconv=1&FORM=hpcodx' + headers["origin"] = 'https://www.bing.com' + async with await session.post("https://www.bing.com/images/kblob", data=data, headers=headers, proxy=proxy) as image_upload_response: + if image_upload_response.status == 200: + image_info = await image_upload_response.json() + result = {} + if image_info.get('blobId'): + result['bcid'] = image_info.get('blobId', "") + result['blurredBcid'] = image_info.get('processedBlobId', "") + if result['blurredBcid'] != "": + result["imageUrl"] = "https://www.bing.com/images/blob?bcid=" + result['blurredBcid'] + elif result['bcid'] != "": + result["imageUrl"] = "https://www.bing.com/images/blob?bcid=" + result['bcid'] + if config['visualSearch']["enableFaceBlurDebug"]: + result['originalImageUrl'] = "https://www.bing.com/images/blob?bcid=" + result['blurredBcid'] + else: + result['originalImageUrl'] = "https://www.bing.com/images/blob?bcid=" + result['bcid'] + conversation.imageInfo = result + else: + raise Exception("Failed to parse image info.") + else: + raise Exception("Failed to upload image.") + + except Exception as e: + print(f"An error happened while trying to send image: {str(e)}") + return conversation async def list_conversations(session: ClientSession) -> list: url = "https://www.bing.com/turing/conversation/chats" @@ -98,37 +162,47 @@ class Defaults: ip_address = f"13.{random.randint(104, 107)}.{random.randint(0, 255)}.{random.randint(0, 255)}" allowedMessageTypes = [ + "ActionRequest", "Chat", + "Context", "Disengaged", + "Progress", "AdsQuery", "SemanticSerp", "GenerateContentQuery", "SearchQuery", - "ActionRequest", - "Context", - "Progress", - "AdsQuery", - "SemanticSerp", + # The following message types should not be added so that it does not flood with + # useless messages (such as "Analyzing images" or "Searching the web") while it's retrieving the AI response + # "InternalSearchQuery", + # "InternalSearchResult", + # Not entirely certain about these two, but these parameters may be used for real-time markdown rendering. + # Keeping them could potentially complicate the retrieval of the messages because link references written while + # the AI is responding would then be moved to the very end of its message. + # "RenderCardRequest", + # "RenderContentRequest" ] sliceIds = [ - "winmuid3tf", - "osbsdusgreccf", - "ttstmout", - "crchatrev", - "winlongmsgtf", - "ctrlworkpay", - "norespwtf", - "tempcacheread", - "temptacache", - "505scss0", - "508jbcars0", - "515enbotdets0", - "5082tsports", - "515vaoprvs", - "424dagslnv1s0", - "kcimgattcf", - "427startpms0", + "wrapuxslimt5", + "wrapalgo", + "wraptopalgo", + "st14", + "arankr1_1_9_9", + "0731ziv2s0", + "voiceall", + "1015onstblg", + "vsspec", + "cacdiscf", + "909ajcopus0", + "scpbfmob", + "rwt1", + "cacmuidarb", + "sappdlpt", + "917fluxv14", + "delaygc", + "remsaconn3p", + "splitcss3p", + "sydconfigoptt" ] location = { @@ -173,27 +247,128 @@ class Defaults: } optionsSets = [ - 'saharasugg', - 'enablenewsfc', - 'clgalileo', - 'gencontentv3', "nlu_direct_response_filter", "deepleo", "disable_emoji_spoken_text", "responsible_ai_policy_235", "enablemm", - "h3precise" - "dtappid", - "cricinfo", - "cricinfov2", "dv3sugg", - "nojbfedge" + "iyxapbing", + "iycapbing", + "h3imaginative", + "clgalileo", + "gencontentv3", + "fluxv14", + "eredirecturl" ] def format_message(msg: dict) -> str: return json.dumps(msg, ensure_ascii=False) + Defaults.delimiter +def build_image_upload_api_payload(image_bin: str, conversation: Conversation, tone: str): + payload = { + 'invokedSkills': ["ImageById"], + 'subscriptionId': "Bing.Chat.Multimodal", + 'invokedSkillsRequestData': { + 'enableFaceBlur': True + }, + 'convoData': { + 'convoid': "", + 'convotone': tone + } + } + knowledge_request = { + 'imageInfo': {}, + 'knowledgeRequest': payload + } + boundary="----WebKitFormBoundary" + ''.join(random.choices(string.ascii_letters + string.digits, k=16)) + data = '--' + boundary + '\r\nContent-Disposition: form-data; name="knowledgeRequest"\r\n\r\n' + json.dumps(knowledge_request,ensure_ascii=False) + "\r\n--" + boundary + '\r\nContent-Disposition: form-data; name="imageBase64"\r\n\r\n' + image_bin + "\r\n--" + boundary + "--\r\n" + return data, boundary + +def is_data_uri_an_image(data_uri): + try: + # Check if the data URI starts with 'data:image' and contains an image format (e.g., jpeg, png, gif) + if not re.match(r'data:image/(\w+);base64,', data_uri): + raise ValueError("Invalid data URI image.") + # Extract the image format from the data URI + image_format = re.match(r'data:image/(\w+);base64,', data_uri).group(1) + # Check if the image format is one of the allowed formats (jpg, jpeg, png, gif) + if image_format.lower() not in ['jpeg', 'jpg', 'png', 'gif']: + raise ValueError("Invalid image format (from mime file type).") + except Exception as e: + raise e + +def is_accepted_format(binary_data): + try: + check = False + if binary_data.startswith(b'\xFF\xD8\xFF'): + check = True # It's a JPEG image + elif binary_data.startswith(b'\x89PNG\r\n\x1a\n'): + check = True # It's a PNG image + elif binary_data.startswith(b'GIF87a') or binary_data.startswith(b'GIF89a'): + check = True # It's a GIF image + elif binary_data.startswith(b'\x89JFIF') or binary_data.startswith(b'JFIF\x00'): + check = True # It's a JPEG image + elif binary_data.startswith(b'\xFF\xD8'): + check = True # It's a JPEG image + elif binary_data.startswith(b'RIFF') and binary_data[8:12] == b'WEBP': + check = True # It's a WebP image + # else we raise ValueError + if not check: + raise ValueError("Invalid image format (from magic code).") + except Exception as e: + raise e + +def extract_data_uri(data_uri): + try: + data = data_uri.split(",")[1] + data = base64.b64decode(data) + return data + except Exception as e: + raise e + +def get_orientation(data: bytes): + try: + if data[0:2] != b'\xFF\xD8': + raise Exception('NotJpeg') + with Image.open(data) as img: + exif_data = img._getexif() + if exif_data is not None: + orientation = exif_data.get(274) # 274 corresponds to the orientation tag in EXIF + if orientation is not None: + return orientation + except Exception: + pass + +def process_image(orientation, img, new_width, new_height): + try: + # Initialize the canvas + new_img = Image.new("RGB", (new_width, new_height), color="#FFFFFF") + if orientation: + if orientation > 4: + img = img.transpose(Image.FLIP_LEFT_RIGHT) + if orientation == 3 or orientation == 4: + img = img.transpose(Image.ROTATE_180) + if orientation == 5 or orientation == 6: + img = img.transpose(Image.ROTATE_270) + if orientation == 7 or orientation == 8: + img = img.transpose(Image.ROTATE_90) + new_img.paste(img, (0, 0)) + return new_img + except Exception as e: + raise e + +def compress_image_to_base64(img, compression_rate): + try: + output_buffer = io.BytesIO() + img.save(output_buffer, format="JPEG", quality=int(compression_rate * 100)) + base64_image = base64.b64encode(output_buffer.getvalue()).decode('utf-8') + return base64_image + except Exception as e: + raise e + def create_message(conversation: Conversation, prompt: str, tone: str, context: str=None) -> str: + request_id = str(uuid.uuid4()) struct = { 'arguments': [ @@ -213,6 +388,7 @@ def create_message(conversation: Conversation, prompt: str, tone: str, context: 'requestId': request_id, 'messageId': request_id, }, + "scenario": "SERP", 'tone': tone, 'spokenTextMode': 'None', 'conversationId': conversation.conversationId, @@ -225,7 +401,11 @@ def create_message(conversation: Conversation, prompt: str, tone: str, context: 'target': 'chat', 'type': 4 } - + if conversation.imageInfo != None and "imageUrl" in conversation.imageInfo and "originalImageUrl" in conversation.imageInfo: + struct['arguments'][0]['message']['originalImageUrl'] = conversation.imageInfo['originalImageUrl'] + struct['arguments'][0]['message']['imageUrl'] = conversation.imageInfo['imageUrl'] + struct['arguments'][0]['experienceType'] = None + struct['arguments'][0]['attachedFileInfo'] = {"fileName": None, "fileType": None} if context: struct['arguments'][0]['previousMessages'] = [{ "author": "user", @@ -239,6 +419,7 @@ def create_message(conversation: Conversation, prompt: str, tone: str, context: async def stream_generate( prompt: str, tone: str, + image: str = None, context: str = None, proxy: str = None, cookies: dict = None @@ -248,7 +429,7 @@ async def stream_generate( cookies=cookies, headers=Defaults.headers, ) as session: - conversation = await create_conversation(session, proxy) + conversation = await create_conversation(session, tone, image, proxy) try: async with session.ws_connect( f'wss://sydney.bing.com/sydney/ChatHub', @@ -264,7 +445,6 @@ async def stream_generate( response_txt = '' returned_text = '' final = False - while not final: msg = await wss.receive(timeout=900) objects = msg.data.split(Defaults.delimiter) @@ -299,4 +479,4 @@ async def stream_generate( raise Exception(f"{result['value']}: {result['message']}") return finally: - await delete_conversation(session, conversation, proxy)
\ No newline at end of file + await delete_conversation(session, conversation, proxy) diff --git a/g4f/__init__.py b/g4f/__init__.py index ef8b1cc1..813c2364 100644 --- a/g4f/__init__.py +++ b/g4f/__init__.py @@ -5,7 +5,7 @@ from .Provider import BaseProvider, RetryProvider from .typing import Messages, CreateResult, Union, List from . import debug -version = '0.1.7.3' +version = '0.1.7.4' version_check = True def check_pypi_version() -> None: diff --git a/requirements.txt b/requirements.txt index 4a59fbe4..e853b661 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,5 @@ nest_asyncio waitress werkzeug loguru -tiktoken
\ No newline at end of file +tiktoken +Pillow @@ -11,7 +11,7 @@ with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as fh: with open("requirements.txt") as f: required = f.read().splitlines() -VERSION = "0.1.7.3" +VERSION = "0.1.7.4" DESCRIPTION = ( "The official gpt4free repository | various collection of powerful language models" ) |