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

XFEstudio/gpt4free

feat: add EdgeTTS audio provider and global image→media refactor

- **Docs** - `docs/file.md`: update upload instructions to use inline `bucket` content parts instead of `tool_calls/bucket_tool`. - `docs/media.md`: add asynchronous audio transcription example, detailed explanation, and notes. - **New audio provider** - Add `g4f/Provider/audio/EdgeTTS.py` implementing Edge Text‑to‑Speech (`EdgeTTS`). - Create `g4f/Provider/audio/__init__.py` for provider export. - Register provider in `g4f/Provider/__init__.py`. - **Refactor image → media** - Introduce `generated_media/` directory and `get_media_dir()` helper in `g4f/image/copy_images.py`; add `ensure_media_dir()`; keep back‑compat with legacy `generated_images/`. - Replace `images_dir` references with `get_media_dir()` across: - `g4f/api/__init__.py` - `g4f/client/stubs.py` - `g4f/gui/server/api.py` - `g4f/gui/server/backend_api.py` - `g4f/image/copy_images.py` - Rename CLI/API config field/flag from `image_provider` to `media_provider` (`g4f/cli.py`, `g4f/api/__init__.py`, `g4f/client/__init__.py`). - Extend `g4f/image/__init__.py` - add `MEDIA_TYPE_MAP`, `get_extension()` - revise `is_allowed_extension()`, `to_input_audio()` to support wider media types. - **Provider adjustments** - `g4f/Provider/ARTA.py`: swap `raise_error()` parameter order. - `g4f/Provider/Cloudflare.py`: drop unused `MissingRequirementsError` import; move `get_args_from_nodriver()` inside try; handle `FileNotFoundError`. - **Core enhancements** - `g4f/providers/any_provider.py`: use `default_model` instead of literal `"default"`; broaden model/provider matching; update model list cleanup. - `g4f/models.py`: safeguard provider count logic when model name is falsy. - `g4f/providers/base_provider.py`: catch `json.JSONDecodeError` when reading auth cache, delete corrupted file. - `g4f/providers/response.py`: allow `AudioResponse` to accept extra kwargs. - **Misc** - Remove obsolete `g4f/image.py`. - `g4f/Provider/Cloudflare.py`, `g4f/client/types.py`: minor whitespace and import tidy‑ups.

e83282fc
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

23 个文件 +253 -387
Modified docs/file.md +5 -11
@@ -180,23 +180,17 @@ fileInput.addEventListener('change', () => {
180 180
181 181 **Integrating with `ChatCompletion`:**
182 182
183 To incorporate file uploads into your client applications, include the `tool_calls` parameter in your chat completion requests, using the `bucket_tool` function. The `bucket_id` is passed as a JSON object within your prompt.
184
183 To incorporate file uploads into your client applications, include the `bucket` in your chat completion requests, using inline content parts.
185 184
186 185 ```json
187 186 {
188 187 "messages": [
189 188 {
190 189 "role": "user",
191 "content": "Answer this question using the files in the specified bucket: ...your question...\n{\"bucket_id\": \"your_actual_bucket_id\"}"
192 }
193 ],
194 "tool_calls": [
195 {
196 "function": {
197 "name": "bucket_tool"
198 },
199 "type": "function"
190 "content": [
191 {"type": "text", "text": "Answer this question using the files in the specified bucket: ...your question..."},
192 {"bucket_id": "your_actual_bucket_id"}
193 ]
200 194 }
201 195 ]
202 196 }
Modified docs/media.md +22 -3
@@ -30,6 +30,8 @@ asyncio.run(main())
30 30
31 31 #### **Transcribe an Audio File:**
32 32
33 Some providers in G4F support audio inputs in chat completions, allowing you to transcribe audio files by instructing the model accordingly. This example demonstrates how to use the `AsyncClient` to transcribe an audio file asynchronously:
34
33 35 ```python
34 36 import asyncio
35 37 from g4f.client import AsyncClient
@@ -41,15 +43,32 @@ async def main():
41 43 with open("audio.wav", "rb") as audio_file:
42 44 response = await client.chat.completions.create(
43 45 messages="Transcribe this audio",
44 provider=g4f.Provider.Microsoft_Phi_4,
45 46 media=[[audio_file, "audio.wav"]],
46 47 modalities=["text"],
47 48 )
48 print(response.choices[0].message.content)
49 49
50 asyncio.run(main())
50 print(response.choices[0].message.content)
51
52 if __name__ == "__main__":
53 asyncio.run(main())
51 54 ```
52 55
56 #### Explanation
57 - **Client Initialization**: An `AsyncClient` instance is created with a provider that supports audio inputs, such as `PollinationsAI` or `Microsoft_Phi_4`.
58 - **File Handling**: The audio file (`audio.wav`) is opened in binary read mode (`"rb"`) using a context manager (`with` statement) to ensure proper file closure after use.
59 - **API Call**: The `chat.completions.create` method is called with:
60 - `messages`: Containing a user message instructing the model to transcribe the audio.
61 - `media`: A list of lists, where each inner list contains the file object and its name (`[[audio_file, "audio.wav"]]`).
62 - `modalities=["text"]`: Specifies that the output should be text (the transcription).
63 - **Response**: The transcription is extracted from `response.choices[0].message.content` and printed.
64
65 #### Notes
66 - **Provider Support**: Ensure the chosen provider (e.g., `PollinationsAI` or `Microsoft_Phi_4`) supports audio inputs in chat completions. Not all providers may offer this functionality.
67 - **File Path**: Replace `"audio.wav"` with the path to your own audio file. The file format (e.g., WAV) should be compatible with the provider.
68 - **Model Selection**: If `g4f.models.default` does not support audio transcription, you may need to specify a model that does (consult the provider's documentation for supported models).
69
70 This example complements the guide by showcasing how to handle audio inputs asynchronously, expanding on the multimodal capabilities of the G4F AsyncClient API.
71
53 72 ---
54 73
55 74 ### 2. **Image Generation**
Modified g4f/Provider/ARTA.py +1 -1
@@ -203,7 +203,7 @@ class ARTA(AsyncGeneratorProvider, ProviderModelMixin):
203 203 else:
204 204 raise ResponseError(f"Image generation failed with status: {status}")
205 205
206 async def raise_error(response: ClientResponse, message: str):
206 async def raise_error(message: str, response: ClientResponse):
207 207 if response.ok:
208 208 return
209 209 error_text = await response.text()
Modified g4f/Provider/Blackbox.py +3 -1
@@ -20,7 +20,7 @@ from ..cookies import get_cookies_dir
20 20 from .helper import format_image_prompt, render_messages
21 21 from ..providers.response import JsonConversation, ImageResponse
22 22 from ..tools.media import merge_media
23 from ..errors import RateLimitError
23 from ..errors import RateLimitError, NoValidHarFileError
24 24 from .. import debug
25 25
26 26 class Conversation(JsonConversation):
@@ -470,6 +470,8 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
470 470 except Exception as e:
471 471 debug.log(f"Blackbox: Error reading HAR file {file}: {e}")
472 472 return None
473 except NoValidHarFileError:
474 pass
473 475 except Exception as e:
474 476 debug.log(f"Blackbox: Error searching HAR files: {e}")
475 477 return None
Modified g4f/Provider/Cloudflare.py +3 -3
@@ -8,7 +8,7 @@ from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileM
8 8 from ..requests import Session, StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies
9 9 from ..requests import DEFAULT_HEADERS, has_nodriver, has_curl_cffi
10 10 from ..providers.response import FinishReason, Usage
11 from ..errors import ResponseStatusError, ModelNotFoundError, MissingRequirementsError
11 from ..errors import ResponseStatusError, ModelNotFoundError
12 12 from .. import debug
13 13 from .helper import render_messages
14 14
@@ -72,11 +72,11 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
72 72 except ResponseStatusError as f:
73 73 if has_nodriver:
74 74 get_running_loop(check_nested=True)
75 args = get_args_from_nodriver(cls.url)
76 75 try:
76 args = get_args_from_nodriver(cls.url)
77 77 cls._args = asyncio.run(args)
78 78 read_models()
79 except RuntimeError as e:
79 except (RuntimeError, FileNotFoundError) as e:
80 80 cls.models = cls.fallback_models
81 81 debug.log(f"Nodriver is not available: {type(e).__name__}: {e}")
82 82 else:
Modified g4f/Provider/__init__.py +4 -0
@@ -28,6 +28,10 @@ try:
28 28 from .mini_max import HailuoAI, MiniMax
29 29 except ImportError as e:
30 30 debug.error("MiniMax providers not loaded:", e)
31 try:
32 from .audio import EdgeTTS
33 except ImportError as e:
34 debug.error("Audio providers not loaded:", e)
31 35
32 36 try:
33 37 from .AllenAI import AllenAI
Added g4f/Provider/audio/EdgeTTS.py +75 -0
@@ -0,0 +1,75 @@
1 from __future__ import annotations
2
3 import os
4 import random
5 import asyncio
6
7 try:
8 import edge_tts
9 from edge_tts import VoicesManager
10 has_edge_tts = True
11 except ImportError:
12 has_edge_tts = False
13
14 from ...typing import AsyncResult, Messages
15 from ...providers.response import AudioResponse
16 from ...image.copy_images import get_filename, get_media_dir, ensure_media_dir
17 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
18 from ..helper import format_image_prompt
19
20 class EdgeTTS(AsyncGeneratorProvider, ProviderModelMixin):
21 label = "Edge TTS"
22 working = has_edge_tts
23 default_model = "edge-tts"
24 default_locale = "en-US"
25
26 @classmethod
27 def get_models(cls) -> list[str]:
28 if not cls.models:
29 voices = asyncio.run(VoicesManager.create())
30 cls.default_model = voices.find(Locale=cls.default_locale)[0]["Name"]
31 cls.models = [voice["Name"] for voice in voices.voices]
32 return cls.models
33
34 @classmethod
35 async def create_async_generator(
36 cls,
37 model: str,
38 messages: Messages,
39 proxy: str = None,
40 prompt: str = None,
41 language: str = None,
42 locale: str = None,
43 audio: dict = {"voice": None, "format": "mp3"},
44 extra_parameters: list[str] = ["rate", "volume", "pitch"],
45 **kwargs
46 ) -> AsyncResult:
47 prompt = format_image_prompt(messages, prompt)
48 if not prompt:
49 raise ValueError("Prompt is empty.")
50 voice = audio.get("voice", model)
51 if not voice:
52 voices = await VoicesManager.create()
53 if locale is None:
54 if language is None:
55 voices = voices.find(Locale=cls.default_locale)
56 elif "-" in language:
57 voices = voices.find(Locale=language)
58 else:
59 voices = voices.find(Language=language)
60 else:
61 voices = voices.find(Locale=locale)
62 if not voices:
63 raise ValueError(f"No voices found for language '{language}' and locale '{locale}'.")
64 voice = random.choice(voices)["Name"]
65
66 format = audio.get("format", "mp3")
67 filename = get_filename([cls.default_model], prompt, f".{format}", prompt)
68 target_path = os.path.join(get_media_dir(), filename)
69 ensure_media_dir()
70
71 extra_parameters = {param: kwargs[param] for param in extra_parameters if param in kwargs}
72 communicate = edge_tts.Communicate(prompt, voice=voice, proxy=proxy, **extra_parameters)
73
74 await communicate.save(target_path)
75 yield AudioResponse(f"/media/{filename}", voice=voice, prompt=prompt)
Added g4f/Provider/audio/__init__.py +1 -0
@@ -0,0 +1 @@
1 from .EdgeTTS import EdgeTTS
Modified g4f/api/__init__.py +7 -6
@@ -40,7 +40,7 @@ from g4f.client import AsyncClient, ChatCompletion, ImagesResponse, convert_to_p
40 40 from g4f.providers.response import BaseConversation, JsonConversation
41 41 from g4f.client.helper import filter_none
42 42 from g4f.image import is_data_an_media, EXTENSIONS_MAP
43 from g4f.image.copy_images import images_dir, copy_media, get_source_url
43 from g4f.image.copy_images import get_media_dir, copy_media, get_source_url
44 44 from g4f.errors import ProviderNotFoundError, ModelNotFoundError, MissingAuthError, NoValidHarFileError
45 45 from g4f.cookies import read_cookie_files, get_cookies_dir
46 46 from g4f.providers.types import ProviderType
@@ -130,7 +130,7 @@ class AppConfig:
130 130 ignore_cookie_files: bool = False
131 131 model: str = None
132 132 provider: str = None
133 image_provider: str = None
133 media_provider: str = None
134 134 proxy: str = None
135 135 gui: bool = False
136 136 demo: bool = False
@@ -419,12 +419,13 @@ class Api:
419 419 ):
420 420 if config.provider is None:
421 421 config.provider = provider
422 if config.provider is None:
423 config.provider = AppConfig.media_provider
422 424 if credentials is not None and credentials.credentials != "secret":
423 425 config.api_key = credentials.credentials
424 426 try:
425 427 response = await self.client.images.generate(
426 428 **config.dict(exclude_none=True),
427 provider=AppConfig.image_provider if config.provider is None else config.provider
428 429 )
429 430 for image in response.data:
430 431 if hasattr(image, "url") and image.url.startswith("/"):
@@ -562,9 +563,9 @@ class Api:
562 563 HTTP_404_NOT_FOUND: {}
563 564 })
564 565 async def get_media(filename, request: Request):
565 target = os.path.join(images_dir, os.path.basename(filename))
566 target = os.path.join(get_media_dir(), os.path.basename(filename))
566 567 if not os.path.isfile(target):
567 other_name = os.path.join(images_dir, os.path.basename(quote_plus(filename)))
568 other_name = os.path.join(get_media_dir(), os.path.basename(quote_plus(filename)))
568 569 if os.path.isfile(other_name):
569 570 target = other_name
570 571 ext = os.path.splitext(filename)[1][1:]
@@ -627,7 +628,7 @@ class Api:
627 628
628 629 def format_exception(e: Union[Exception, str], config: Union[ChatCompletionsConfig, ImageGenerationConfig] = None, image: bool = False) -> str:
629 630 last_provider = {} if not image else g4f.get_last_provider(True)
630 provider = (AppConfig.image_provider if image else AppConfig.provider)
631 provider = (AppConfig.media_provider if image else AppConfig.provider)
631 632 model = AppConfig.model
632 633 if config is not None:
633 634 if config.provider is not None:
Modified g4f/cli.py +2 -2
@@ -16,7 +16,7 @@ def get_api_parser():
16 16 api_parser.add_argument("--model", default=None, help="Default model for chat completion. (incompatible with --reload and --workers)")
17 17 api_parser.add_argument("--provider", choices=[provider.__name__ for provider in Provider.__providers__ if provider.working],
18 18 default=None, help="Default provider for chat completion. (incompatible with --reload and --workers)")
19 api_parser.add_argument("--image-provider", choices=[provider.__name__ for provider in Provider.__providers__ if provider.working and hasattr(provider, "image_models")],
19 api_parser.add_argument("--media-provider", choices=[provider.__name__ for provider in Provider.__providers__ if provider.working and bool(getattr(provider, "image_models", False))],
20 20 default=None, help="Default provider for image generation. (incompatible with --reload and --workers)"),
21 21 api_parser.add_argument("--proxy", default=None, help="Default used proxy. (incompatible with --reload and --workers)")
22 22 api_parser.add_argument("--workers", type=int, default=None, help="Number of workers.")
@@ -59,7 +59,7 @@ def run_api_args(args):
59 59 ignored_providers=args.ignored_providers,
60 60 g4f_api_key=args.g4f_api_key,
61 61 provider=args.provider,
62 image_provider=args.image_provider,
62 media_provider=args.media_provider,
63 63 proxy=args.proxy,
64 64 model=args.model,
65 65 gui=args.gui,
Modified g4f/client/__init__.py +37 -36
@@ -1,5 +1,6 @@
1 1 from __future__ import annotations
2 2
3 import os
3 4 import time
4 5 import random
5 6 import string
@@ -8,7 +9,7 @@ import aiohttp
8 9 import base64
9 10 from typing import Union, AsyncIterator, Iterator, Awaitable, Optional
10 11
11 from ..image.copy_images import copy_media
12 from ..image.copy_images import copy_media, get_media_dir
12 13 from ..typing import Messages, ImageType
13 14 from ..providers.types import ProviderType, BaseRetryProvider
14 15 from ..providers.response import *
@@ -16,11 +17,11 @@ from ..errors import NoMediaResponseError
16 17 from ..providers.retry_provider import IterListProvider
17 18 from ..providers.asyncio import to_sync_generator
18 19 from ..providers.any_provider import AnyProvider
19 from ..Provider.needs_auth import BingCreateImages, OpenaiAccount
20 from ..Provider import OpenaiAccount, PollinationsImage
20 21 from ..tools.run_tools import async_iter_run_tools, iter_run_tools
21 22 from .stubs import ChatCompletion, ChatCompletionChunk, Image, ImagesResponse, UsageModel, ToolCallModel
22 23 from .models import ClientModels
23 from .types import IterResponse, ImageProvider, Client as BaseClient
24 from .types import IterResponse, Client as BaseClient
24 25 from .service import convert_to_provider
25 26 from .helper import find_stop, filter_json, filter_none, safe_aclose
26 27 from .. import debug
@@ -261,15 +262,15 @@ class Client(BaseClient):
261 262 def __init__(
262 263 self,
263 264 provider: Optional[ProviderType] = None,
264 image_provider: Optional[ImageProvider] = None,
265 media_provider: Optional[ProviderType] = None,
265 266 **kwargs
266 267 ) -> None:
267 268 super().__init__(**kwargs)
268 269 self.chat: Chat = Chat(self, provider)
269 if image_provider is None:
270 image_provider = provider
271 self.models: ClientModels = ClientModels(self, provider, image_provider)
272 self.images: Images = Images(self, image_provider)
270 if media_provider is None:
271 media_provider = kwargs.get("image_provider", provider)
272 self.models: ClientModels = ClientModels(self, provider, media_provider)
273 self.images: Images = Images(self, media_provider)
273 274 self.media: Images = self.images
274 275
275 276 class Completions:
@@ -364,7 +365,7 @@ class Images:
364 365 """
365 366 return asyncio.run(self.async_generate(prompt, model, provider, response_format, proxy, **kwargs))
366 367
367 async def get_provider_handler(self, model: Optional[str], provider: Optional[ImageProvider], default: ImageProvider) -> ImageProvider:
368 async def get_provider_handler(self, model: Optional[str], provider: Optional[ProviderType], default: ProviderType) -> ProviderType:
368 369 if provider is None:
369 370 provider_handler = self.provider
370 371 if provider_handler is None:
@@ -387,7 +388,7 @@ class Images:
387 388 api_key: Optional[str] = None,
388 389 **kwargs
389 390 ) -> ImagesResponse:
390 provider_handler = await self.get_provider_handler(model, provider, BingCreateImages)
391 provider_handler = await self.get_provider_handler(model, provider, PollinationsImage)
391 392 provider_name = provider_handler.__name__ if hasattr(provider_handler, "__name__") else type(provider_handler).__name__
392 393 if proxy is None:
393 394 proxy = self.client.proxy
@@ -407,20 +408,17 @@ class Images:
407 408 debug.error(f"{provider.__name__} {type(e).__name__}: {e}")
408 409 else:
409 410 response = await self._generate_image_response(provider_handler, provider_name, model, prompt, proxy=proxy, api_key=api_key, **kwargs)
410
411 if isinstance(response, MediaResponse):
412 return await self._process_image_response(
413 response,
414 model,
415 provider_name,
416 response_format,
417 proxy
418 )
419 411 if response is None:
420 412 if error is not None:
421 413 raise error
422 raise NoMediaResponseError(f"No image response from {provider_name}")
423 raise NoMediaResponseError(f"Unexpected response type: {type(response)}")
414 raise NoMediaResponseError(f"No media response from {provider_name}")
415 return await self._process_image_response(
416 response,
417 model,
418 provider_name,
419 response_format,
420 proxy
421 )
424 422
425 423 async def _generate_image_response(
426 424 self,
@@ -441,7 +439,7 @@ class Images:
441 439 prompt=prompt,
442 440 **kwargs
443 441 ):
444 if isinstance(item, MediaResponse):
442 if isinstance(item, (MediaResponse, AudioResponse)):
445 443 items.append(item)
446 444 elif hasattr(provider_handler, "create_completion"):
447 445 for item in provider_handler.create_completion(
@@ -451,13 +449,15 @@ class Images:
451 449 prompt=prompt,
452 450 **kwargs
453 451 ):
454 if isinstance(item, MediaResponse):
452 if isinstance(item, (MediaResponse, AudioResponse)):
455 453 items.append(item)
456 454 else:
457 455 raise ValueError(f"Provider {provider_name} does not support image generation")
458 456 urls = []
459 457 for item in items:
460 if isinstance(item.urls, str):
458 if isinstance(item, AudioResponse):
459 urls.append(item.to_uri())
460 elif isinstance(item.urls, str):
461 461 urls.append(item.urls)
462 462 elif isinstance(item.urls, list):
463 463 urls.extend(item.urls)
@@ -508,14 +508,11 @@ class Images:
508 508 debug.error(f"{provider.__name__} {type(e).__name__}: {e}")
509 509 else:
510 510 response = await self._generate_image_response(provider_handler, provider_name, model, prompt, **kwargs)
511
512 if isinstance(response, MediaResponse):
513 return await self._process_image_response(response, model, provider_name, response_format, proxy)
514 511 if response is None:
515 512 if error is not None:
516 513 raise error
517 raise NoMediaResponseError(f"No image response from {provider_name}")
518 raise NoMediaResponseError(f"Unexpected response type: {type(response)}")
514 raise NoMediaResponseError(f"No media response from {provider_name}")
515 return await self._process_image_response(response, model, provider_name, response_format, proxy)
519 516
520 517 async def _process_image_response(
521 518 self,
@@ -531,12 +528,16 @@ class Images:
531 528 elif response_format == "b64_json":
532 529 # Convert URLs directly to base64 without saving
533 530 async def get_b64_from_url(url: str) -> Image:
531 if url.startswith("/media/"):
532 with open(os.path.join(get_media_dir(), os.path.basename(url)), "wb") as f:
533 b64_data = base64.b64encode(f.read()).decode()
534 return Image.model_construct(b64_json=b64_data, revised_prompt=response.alt)
534 535 async with aiohttp.ClientSession(cookies=response.get("cookies")) as session:
535 536 async with session.get(url, proxy=proxy) as resp:
536 537 if resp.status == 200:
537 image_data = await resp.read()
538 b64_data = base64.b64encode(image_data).decode()
538 b64_data = base64.b64encode(await resp.read()).decode()
539 539 return Image.model_construct(b64_json=b64_data, revised_prompt=response.alt)
540 return Image.model_construct(url=url, revised_prompt=response.alt)
540 541 images = await asyncio.gather(*[get_b64_from_url(image) for image in response.get_list()])
541 542 else:
542 543 # Save locally for None (default) case
@@ -554,15 +555,15 @@ class AsyncClient(BaseClient):
554 555 def __init__(
555 556 self,
556 557 provider: Optional[ProviderType] = None,
557 image_provider: Optional[ImageProvider] = None,
558 media_provider: Optional[ProviderType] = None,
558 559 **kwargs
559 560 ) -> None:
560 561 super().__init__(**kwargs)
561 562 self.chat: AsyncChat = AsyncChat(self, provider)
562 if image_provider is None:
563 image_provider = provider
564 self.models: ClientModels = ClientModels(self, provider, image_provider)
565 self.images: AsyncImages = AsyncImages(self, image_provider)
563 if media_provider is None:
564 media_provider = kwargs.get("image_provider", provider)
565 self.models: ClientModels = ClientModels(self, provider, media_provider)
566 self.images: AsyncImages = AsyncImages(self, media_provider)
566 567 self.media: AsyncImages = self.images
567 568
568 569 class AsyncChat:
Modified g4f/client/stubs.py +2 -2
@@ -5,7 +5,7 @@ from typing import Optional, List
5 5 from time import time
6 6
7 7 from ..image import extract_data_uri
8 from ..image.copy_images import images_dir
8 from ..image.copy_images import get_media_dir
9 9 from ..client.helper import filter_markdown
10 10 from .helper import filter_none
11 11
@@ -123,7 +123,7 @@ class ChatCompletionMessage(BaseModel):
123 123
124 124 def save(self, filepath: str, allowd_types = None):
125 125 if hasattr(self.content, "data"):
126 os.rename(self.content.data.replace("/media", images_dir), filepath)
126 os.rename(self.content.data.replace("/media", get_media_dir()), filepath)
127 127 return
128 128 if self.content.startswith("data:"):
129 129 with open(filepath, "wb") as f:
Modified g4f/client/types.py +1 -2
Modified g4f/gui/server/api.py +3 -3
Modified g4f/gui/server/backend_api.py +5 -4
Deleted g4f/image.py +0 -253
Modified g4f/image/__init__.py +22 -17
Modified g4f/image/copy_images.py +31 -26
Modified g4f/models.py +1 -1
Modified g4f/providers/any_provider.py +14 -9
Modified g4f/providers/base_provider.py +12 -6
Modified g4f/providers/response.py +2 -1
Renamed generated_media/.gitkeep +0 -0