XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Improve slim docker image example, clean up OpenaiChat provider (#2397)

* Improve slim docker image example, clean up OpenaiChat provider * Enhance event loop management for asynchronous generators * Fix attribute " shutdown_default_executor" not found in old python versions * asyncio file added with all async helpers

e4bfd9db
H Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

9 个文件 +104 -336
Modified README.md +6 -3
@@ -105,15 +105,18 @@ docker run \
105 105 hlohaus789/g4f:latest
106 106 ```
107 107
108 Or run this command to start the gui without a browser and in the debug mode:
108 Start the GUI without a browser requirement and in debug mode.
109 There's no need to update the Docker image every time.
110 Simply remove the g4f package from the image and install the Python package:
109 111 ```bash
110 docker pull hlohaus789/g4f:latest-slim
111 112 docker run \
112 113 -p 8080:8080 \
113 114 -v ${PWD}/har_and_cookies:/app/har_and_cookies \
114 115 -v ${PWD}/generated_images:/app/generated_images \
115 116 hlohaus789/g4f:latest-slim \
116 python -m g4f.cli gui -debug
117 rm -r -f /app/g4f/ \
118 && pip install -U g4f[slim] \
119 && python -m g4f.cli gui -d
117 120 ```
118 121
119 122 3. **Access the Client:**
Modified g4f/Provider/needs_auth/OpenaiChat.py +8 -216
@@ -15,18 +15,14 @@ try:
15 15 has_nodriver = True
16 16 except ImportError:
17 17 has_nodriver = False
18 try:
19 from platformdirs import user_config_dir
20 has_platformdirs = True
21 except ImportError:
22 has_platformdirs = False
23 18
24 19 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
25 20 from ...typing import AsyncResult, Messages, Cookies, ImageType, AsyncIterator
26 21 from ...requests.raise_for_status import raise_for_status
27 from ...requests.aiohttp import StreamSession
22 from ...requests import StreamSession
23 from ...requests import get_nodriver
28 24 from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format
29 from ...errors import MissingAuthError, ResponseError
25 from ...errors import MissingAuthError
30 26 from ...providers.response import BaseConversation, FinishReason, SynthesizeData
31 27 from ..helper import format_cookies
32 28 from ..openai.har_file import get_request_config, NoValidHarFileError
@@ -62,7 +58,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
62 58 supports_system_message = True
63 59 default_model = "auto"
64 60 default_vision_model = "gpt-4o"
65 fallback_models = ["auto", "gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-4o-canmore", "o1-preview", "o1-mini"]
61 fallback_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-4o-canmore", "o1-preview", "o1-mini"]
66 62 vision_models = fallback_models
67 63 image_models = fallback_models
68 64
@@ -83,51 +79,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
83 79 cls.models = cls.fallback_models
84 80 return cls.models
85 81
86 @classmethod
87 async def create(
88 cls,
89 prompt: str = None,
90 model: str = "",
91 messages: Messages = [],
92 action: str = "next",
93 **kwargs
94 ) -> Response:
95 """
96 Create a new conversation or continue an existing one
97
98 Args:
99 prompt: The user input to start or continue the conversation
100 model: The name of the model to use for generating responses
101 messages: The list of previous messages in the conversation
102 history_disabled: A flag indicating if the history and training should be disabled
103 action: The type of action to perform, either "next", "continue", or "variant"
104 conversation_id: The ID of the existing conversation, if any
105 parent_id: The ID of the parent message, if any
106 image: The image to include in the user input, if any
107 **kwargs: Additional keyword arguments to pass to the generator
108
109 Returns:
110 A Response object that contains the generator, action, messages, and options
111 """
112 # Add the user input to the messages list
113 if prompt is not None:
114 messages.append({
115 "role": "user",
116 "content": prompt
117 })
118 generator = cls.create_async_generator(
119 model,
120 messages,
121 return_conversation=True,
122 **kwargs
123 )
124 return Response(
125 generator,
126 action,
127 messages,
128 kwargs
129 )
130
131 82 @classmethod
132 83 async def upload_image(
133 84 cls,
@@ -189,32 +140,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
189 140 image_data["download_url"] = (await response.json())["download_url"]
190 141 return ImageRequest(image_data)
191 142
192 @classmethod
193 async def get_default_model(cls, session: StreamSession, headers: dict):
194 """
195 Get the default model name from the service
196
197 Args:
198 session: The StreamSession object to use for requests
199 headers: The headers to include in the requests
200
201 Returns:
202 The default model name as a string
203 """
204 if not cls.default_model:
205 url = f"{cls.url}/backend-anon/models" if cls._api_key is None else f"{cls.url}/backend-api/models"
206 async with session.get(url, headers=headers) as response:
207 cls._update_request_args(session)
208 if response.status == 401:
209 raise MissingAuthError('Add a .har file for OpenaiChat' if cls._api_key is None else "Invalid api key")
210 await raise_for_status(response)
211 data = await response.json()
212 if "categories" in data:
213 cls.default_model = data["categories"][-1]["default_model"]
214 return cls.default_model
215 raise ResponseError(data)
216 return cls.default_model
217
218 143 @classmethod
219 144 def create_messages(cls, messages: Messages, image_request: ImageRequest = None):
220 145 """
@@ -296,30 +221,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
296 221 except Exception as e:
297 222 raise RuntimeError(f"Error in downloading image: {e}")
298 223
299 @classmethod
300 async def delete_conversation(cls, session: StreamSession, headers: dict, conversation_id: str):
301 """
302 Deletes a conversation by setting its visibility to False.
303
304 This method sends an HTTP PATCH request to update the visibility of a conversation.
305 It's used to effectively delete a conversation from being accessed or displayed in the future.
306
307 Args:
308 session (StreamSession): The StreamSession object used for making HTTP requests.
309 headers (dict): HTTP headers to be used for the request.
310 conversation_id (str): The unique identifier of the conversation to be deleted.
311
312 Raises:
313 HTTPError: If the HTTP request fails or returns an unsuccessful status code.
314 """
315 async with session.patch(
316 f"{cls.url}/backend-api/conversation/{conversation_id}",
317 json={"is_visible": False},
318 headers=headers
319 ) as response:
320 cls._update_request_args(session)
321 ...
322
323 224 @classmethod
324 225 async def create_async_generator(
325 226 cls,
@@ -327,7 +228,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
327 228 messages: Messages,
328 229 proxy: str = None,
329 230 timeout: int = 180,
330 api_key: str = None,
331 231 cookies: Cookies = None,
332 232 auto_continue: bool = False,
333 233 history_disabled: bool = False,
@@ -465,7 +365,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
465 365 continue
466 366 await raise_for_status(response)
467 367 if return_conversation:
468 history_disabled = False
469 368 yield conversation
470 369 async for line in response.iter_lines():
471 370 async for chunk in cls.iter_messages_line(session, line, conversation):
@@ -483,19 +382,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
483 382 else:
484 383 break
485 384 yield FinishReason(conversation.finish_reason)
486 if history_disabled and auto_continue:
487 await cls.delete_conversation(session, cls._headers, conversation.conversation_id)
488
489 @classmethod
490 async def iter_messages_chunk(
491 cls,
492 messages: AsyncIterator,
493 session: StreamSession,
494 fields: Conversation,
495 ) -> AsyncIterator:
496 async for message in messages:
497 async for chunk in cls.iter_messages_line(session, message, fields):
498 yield chunk
499 385
500 386 @classmethod
501 387 async def iter_messages_line(cls, session: StreamSession, line: bytes, fields: Conversation) -> AsyncIterator:
@@ -575,15 +461,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
575 461
576 462 @classmethod
577 463 async def nodriver_auth(cls, proxy: str = None):
578 if has_platformdirs:
579 user_data_dir = user_config_dir("g4f-nodriver")
580 else:
581 user_data_dir = None
582 debug.log(f"Open nodriver with user_dir: {user_data_dir}")
583 browser = await nodriver.start(
584 user_data_dir=user_data_dir,
585 browser_args=None if proxy is None else [f"--proxy-server={proxy}"],
586 )
464 browser = await get_nodriver(proxy=proxy)
587 465 page = browser.main_tab
588 466 def on_request(event: nodriver.cdp.network.RequestWillBeSent):
589 467 if event.request.url == start_url or event.request.url.startswith(conversation_url):
@@ -622,14 +500,14 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
622 500 pass
623 501 for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])):
624 502 RequestConfig.cookies[c.name] = c.value
625 RequestConfig.user_agent = await page.evaluate("window.navigator.userAgent")
503 user_agent = await page.evaluate("window.navigator.userAgent")
626 504 await page.select("#prompt-textarea", 240)
627 505 while True:
628 506 if RequestConfig.proof_token:
629 507 break
630 508 await asyncio.sleep(1)
631 509 await page.close()
632 cls._create_request_args(RequestConfig.cookies, RequestConfig.headers, user_agent=RequestConfig.user_agent)
510 cls._create_request_args(RequestConfig.cookies, RequestConfig.headers, user_agent=user_agent)
633 511 cls._set_api_key(RequestConfig.access_token)
634 512
635 513 @staticmethod
@@ -672,90 +550,4 @@ class Conversation(BaseConversation):
672 550 self.conversation_id = conversation_id
673 551 self.message_id = message_id
674 552 self.finish_reason = finish_reason
675 self.is_recipient = False
676
677 class Response():
678 """
679 Class to encapsulate a response from the chat service.
680 """
681 def __init__(
682 self,
683 generator: AsyncResult,
684 action: str,
685 messages: Messages,
686 options: dict
687 ):
688 self._generator = generator
689 self.action = action
690 self.is_end = False
691 self._message = None
692 self._messages = messages
693 self._options = options
694 self._fields = None
695
696 async def generator(self) -> AsyncIterator:
697 if self._generator is not None:
698 self._generator = None
699 chunks = []
700 async for chunk in self._generator:
701 if isinstance(chunk, Conversation):
702 self._fields = chunk
703 else:
704 yield chunk
705 chunks.append(str(chunk))
706 self._message = "".join(chunks)
707 if self._fields is None:
708 raise RuntimeError("Missing response fields")
709 self.is_end = self._fields.finish_reason == "stop"
710
711 def __aiter__(self):
712 return self.generator()
713
714 async def get_message(self) -> str:
715 await self.generator()
716 return self._message
717
718 async def get_fields(self) -> dict:
719 await self.generator()
720 return {
721 "conversation_id": self._fields.conversation_id,
722 "parent_id": self._fields.message_id
723 }
724
725 async def create_next(self, prompt: str, **kwargs) -> Response:
726 return await OpenaiChat.create(
727 **self._options,
728 prompt=prompt,
729 messages=await self.get_messages(),
730 action="next",
731 **await self.get_fields(),
732 **kwargs
733 )
734
735 async def do_continue(self, **kwargs) -> Response:
736 fields = await self.get_fields()
737 if self.is_end:
738 raise RuntimeError("Can't continue message. Message already finished.")
739 return await OpenaiChat.create(
740 **self._options,
741 messages=await self.get_messages(),
742 action="continue",
743 **fields,
744 **kwargs
745 )
746
747 async def create_variant(self, **kwargs) -> Response:
748 if self.action != "next":
749 raise RuntimeError("Can't create variant from continue or variant request.")
750 return await OpenaiChat.create(
751 **self._options,
752 messages=self._messages,
753 action="variant",
754 **await self.get_fields(),
755 **kwargs
756 )
757
758 async def get_messages(self) -> list:
759 messages = self._messages
760 messages.append({"role": "assistant", "content": await self.message()})
761 return messages
553 self.is_recipient = False
Modified g4f/Provider/openai/har_file.py +0 -1
@@ -25,7 +25,6 @@ class NoValidHarFileError(Exception):
25 25 pass
26 26
27 27 class RequestConfig:
28 user_agent: str = None
29 28 cookies: dict = None
30 29 headers: dict = None
31 30 access_request_id: str = None
Modified g4f/client/__init__.py +5 -7
@@ -6,7 +6,6 @@ import random
6 6 import string
7 7 import asyncio
8 8 import base64
9 import logging
10 9 from typing import Union, AsyncIterator, Iterator, Coroutine, Optional
11 10
12 11 from ..providers.base_provider import AsyncGeneratorProvider
@@ -16,13 +15,13 @@ from ..providers.types import ProviderType
16 15 from ..providers.response import ResponseType, FinishReason, BaseConversation, SynthesizeData
17 16 from ..errors import NoImageResponseError, ModelNotFoundError
18 17 from ..providers.retry_provider import IterListProvider
19 from ..providers.base_provider import get_running_loop
18 from ..providers.asyncio import get_running_loop, to_sync_generator, async_generator_to_list
20 19 from ..Provider.needs_auth.BingCreateImages import BingCreateImages
21 20 from .stubs import ChatCompletion, ChatCompletionChunk, Image, ImagesResponse
22 21 from .image_models import ImageModels
23 22 from .types import IterResponse, ImageProvider, Client as BaseClient
24 23 from .service import get_model_and_provider, get_last_provider, convert_to_provider
25 from .helper import find_stop, filter_json, filter_none, safe_aclose, to_sync_iter, to_async_iterator
24 from .helper import find_stop, filter_json, filter_none, safe_aclose, to_async_iterator
26 25
27 26 ChatCompletionResponseType = Iterator[Union[ChatCompletion, ChatCompletionChunk, BaseConversation]]
28 27 AsyncChatCompletionResponseType = AsyncIterator[Union[ChatCompletion, ChatCompletionChunk, BaseConversation]]
@@ -50,8 +49,7 @@ def iter_response(
50 49 idx = 0
51 50
52 51 if hasattr(response, '__aiter__'):
53 # It's an async iterator, wrap it into a sync iterator
54 response = to_sync_iter(response)
52 response = to_sync_generator(response)
55 53
56 54 for chunk in response:
57 55 if isinstance(chunk, FinishReason):
@@ -231,10 +229,10 @@ class Completions:
231 229 response = asyncio.run(response)
232 230 if stream and hasattr(response, '__aiter__'):
233 231 # It's an async generator, wrap it into a sync iterator
234 response = to_sync_iter(response)
232 response = to_sync_generator(response)
235 233 elif hasattr(response, '__aiter__'):
236 234 # If response is an async generator, collect it into a list
237 response = list(to_sync_iter(response))
235 response = asyncio.run(async_generator_to_list(response))
238 236 response = iter_response(response, stream, response_format, max_tokens, stop)
239 237 response = iter_append_model_and_provider(response)
240 238 if stream:
Modified g4f/client/helper.py +0 -30
@@ -1,10 +1,7 @@
1 1 from __future__ import annotations
2 2
3 3 import re
4 import queue
5 import threading
6 4 import logging
7 import asyncio
8 5
9 6 from typing import AsyncIterator, Iterator, AsyncGenerator, Optional
10 7
@@ -53,33 +50,6 @@ async def safe_aclose(generator: AsyncGenerator) -> None:
53 50 except Exception as e:
54 51 logging.warning(f"Error while closing generator: {e}")
55 52
56 # Helper function to convert an async generator to a synchronous iterator
57 def to_sync_iter(async_gen: AsyncIterator) -> Iterator:
58 q = queue.Queue()
59 loop = asyncio.new_event_loop()
60 done = object()
61
62 def _run():
63 asyncio.set_event_loop(loop)
64
65 async def iterate():
66 try:
67 async for item in async_gen:
68 q.put(item)
69 finally:
70 q.put(done)
71
72 loop.run_until_complete(iterate())
73 loop.close()
74
75 threading.Thread(target=_run).start()
76
77 while True:
78 item = q.get()
79 if item is done:
80 break
81 yield item
82
83 53 # Helper function to convert a synchronous iterator to an async iterator
84 54 async def to_async_iterator(iterator: Iterator) -> AsyncIterator:
85 55 for item in iterator:
Modified g4f/gui/server/backend.py +2 -17
@@ -1,13 +1,13 @@
1 1 import json
2 import asyncio
3 2 import flask
4 3 import os
5 4 from flask import request, Flask
6 from typing import AsyncGenerator, Generator
5 from typing import Generator
7 6 from werkzeug.utils import secure_filename
8 7
9 8 from g4f.image import is_allowed_extension, to_image
10 9 from g4f.client.service import convert_to_provider
10 from g4f.providers.asyncio import to_sync_generator
11 11 from g4f.errors import ProviderNotFoundError
12 12 from g4f.cookies import get_cookies_dir
13 13 from .api import Api
@@ -19,21 +19,6 @@ def safe_iter_generator(generator: Generator) -> Generator:
19 19 yield from generator
20 20 return iter_generator()
21 21
22 def to_sync_generator(gen: AsyncGenerator) -> Generator:
23 loop = asyncio.new_event_loop()
24 asyncio.set_event_loop(loop)
25 gen = gen.__aiter__()
26 async def get_next():
27 try:
28 obj = await gen.__anext__()
29 return False, obj
30 except StopAsyncIteration: return True, None
31 while True:
32 done, obj = loop.run_until_complete(get_next())
33 if done:
34 break
35 yield obj
36
37 22 class Backend_Api(Api):
38 23 """
39 24 Handles various endpoints in a Flask application for backend operations.
Added g4f/providers/asyncio.py +65 -0
@@ -0,0 +1,65 @@
1 from __future__ import annotations
2
3 import asyncio
4 from asyncio import AbstractEventLoop, runners
5 from typing import Union, Callable, AsyncGenerator, Generator
6
7 from ..errors import NestAsyncioError
8
9 try:
10 import nest_asyncio
11 has_nest_asyncio = True
12 except ImportError:
13 has_nest_asyncio = False
14 try:
15 import uvloop
16 has_uvloop = True
17 except ImportError:
18 has_uvloop = False
19
20 def get_running_loop(check_nested: bool) -> Union[AbstractEventLoop, None]:
21 try:
22 loop = asyncio.get_running_loop()
23 # Do not patch uvloop loop because its incompatible.
24 if has_uvloop:
25 if isinstance(loop, uvloop.Loop):
26 return loop
27 if not hasattr(loop.__class__, "_nest_patched"):
28 if has_nest_asyncio:
29 nest_asyncio.apply(loop)
30 elif check_nested:
31 raise NestAsyncioError('Install "nest_asyncio" package | pip install -U nest_asyncio')
32 return loop
33 except RuntimeError:
34 pass
35
36 # Fix for RuntimeError: async generator ignored GeneratorExit
37 async def await_callback(callback: Callable):
38 return await callback()
39
40 async def async_generator_to_list(generator: AsyncGenerator) -> list:
41 return [item async for item in generator]
42
43 def to_sync_generator(generator: AsyncGenerator) -> Generator:
44 loop = get_running_loop(check_nested=False)
45 new_loop = False
46 if loop is None:
47 loop = asyncio.new_event_loop()
48 asyncio.set_event_loop(loop)
49 new_loop = True
50 gen = generator.__aiter__()
51 try:
52 while True:
53 yield loop.run_until_complete(await_callback(gen.__anext__))
54 except StopAsyncIteration:
55 pass
56 finally:
57 if new_loop:
58 try:
59 runners._cancel_all_tasks(loop)
60 loop.run_until_complete(loop.shutdown_asyncgens())
61 if hasattr(loop, "shutdown_default_executor"):
62 loop.run_until_complete(loop.shutdown_default_executor())
63 finally:
64 asyncio.set_event_loop(None)
65 loop.close()
Modified g4f/providers/base_provider.py +5 -58
@@ -7,30 +7,14 @@ from asyncio import AbstractEventLoop
7 7 from concurrent.futures import ThreadPoolExecutor
8 8 from abc import abstractmethod
9 9 from inspect import signature, Parameter
10 from typing import Callable, Union
11 10
12 11 from ..typing import CreateResult, AsyncResult, Messages
13 12 from .types import BaseProvider
13 from .asyncio import get_running_loop, to_sync_generator
14 14 from .response import FinishReason, BaseConversation, SynthesizeData
15 from ..errors import NestAsyncioError, ModelNotSupportedError
15 from ..errors import ModelNotSupportedError
16 16 from .. import debug
17 17
18 if sys.version_info < (3, 10):
19 NoneType = type(None)
20 else:
21 from types import NoneType
22
23 try:
24 import nest_asyncio
25 has_nest_asyncio = True
26 except ImportError:
27 has_nest_asyncio = False
28 try:
29 import uvloop
30 has_uvloop = True
31 except ImportError:
32 has_uvloop = False
33
34 18 # Set Windows event loop policy for better compatibility with asyncio and curl_cffi
35 19 if sys.platform == 'win32':
36 20 try:
@@ -41,26 +25,6 @@ if sys.platform == 'win32':
41 25 except ImportError:
42 26 pass
43 27
44 def get_running_loop(check_nested: bool) -> Union[AbstractEventLoop, None]:
45 try:
46 loop = asyncio.get_running_loop()
47 # Do not patch uvloop loop because its incompatible.
48 if has_uvloop:
49 if isinstance(loop, uvloop.Loop):
50 return loop
51 if not hasattr(loop.__class__, "_nest_patched"):
52 if has_nest_asyncio:
53 nest_asyncio.apply(loop)
54 elif check_nested:
55 raise NestAsyncioError('Install "nest_asyncio" package | pip install -U nest_asyncio')
56 return loop
57 except RuntimeError:
58 pass
59
60 # Fix for RuntimeError: async generator ignored GeneratorExit
61 async def await_callback(callback: Callable):
62 return await callback()
63
64 28 class AbstractProvider(BaseProvider):
65 29 """
66 30 Abstract class for providing asynchronous functionality to derived classes.
@@ -136,7 +100,6 @@ class AbstractProvider(BaseProvider):
136 100
137 101 return f"g4f.Provider.{cls.__name__} supports: ({args}\n)"
138 102
139
140 103 class AsyncProvider(AbstractProvider):
141 104 """
142 105 Provides asynchronous functionality for creating completions.
@@ -218,25 +181,9 @@ class AsyncGeneratorProvider(AsyncProvider):
218 181 Returns:
219 182 CreateResult: The result of the streaming completion creation.
220 183 """
221 loop = get_running_loop(check_nested=False)
222 new_loop = False
223 if loop is None:
224 loop = asyncio.new_event_loop()
225 asyncio.set_event_loop(loop)
226 new_loop = True
227
228 generator = cls.create_async_generator(model, messages, stream=stream, **kwargs)
229 gen = generator.__aiter__()
230
231 try:
232 while True:
233 yield loop.run_until_complete(await_callback(gen.__anext__))
234 except StopAsyncIteration:
235 pass
236 finally:
237 if new_loop:
238 loop.close()
239 asyncio.set_event_loop(None)
184 return to_sync_generator(
185 cls.create_async_generator(model, messages, stream=stream, **kwargs)
186 )
240 187
241 188 @classmethod
242 189 async def create_async(
Modified setup.py +13 -4
@@ -23,18 +23,27 @@ EXTRA_REQUIRE = {
23 23 "browser_cookie3", # get_cookies
24 24 "duckduckgo-search>=5.0" ,# internet.search
25 25 "beautifulsoup4", # internet.search and bing.create_images
26 "brotli", # openai, bing
27 26 "platformdirs",
28 "cryptography",
29 27 "aiohttp_socks", # proxy
30 28 "pillow", # image
31 29 "cairosvg", # svg image
32 30 "werkzeug", "flask", # gui
33 31 "fastapi", # api
34 "uvicorn", "nest_asyncio", # api
35 "pycryptodome", # openai
32 "uvicorn", # api
36 33 "nodriver",
37 34 ],
35 'slim': [
36 "curl_cffi>=0.6.2",
37 "certifi",
38 "duckduckgo-search>=5.0" ,# internet.search
39 "beautifulsoup4", # internet.search and bing.create_images
40 "aiohttp_socks", # proxy
41 "pillow", # image
42 "cairosvg", # svg image
43 "werkzeug", "flask", # gui
44 "fastapi", # api
45 "uvicorn", # api
46 ],
38 47 "image": [
39 48 "pillow",
40 49 "cairosvg",