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

XFEstudio/gpt4free

feat: add 'transparent' image model and refactor HAR provider auth flow

- Added "transparent" to `image_models` in `PollinationsAI` and mapped it to "gptimage" - Modified transparent flag handling in `_generate_image` call in `PollinationsAI` - Removed unused `cls.get_models()` call from image generation method in `PollinationsAI` - Replaced `AsyncGeneratorProvider` with `AsyncAuthedProvider` in `HarProvider` - Implemented `on_auth_async` in `HarProvider` to support browser-based auth via `nodriver` - Replaced `create_async_generator` with `create_authed` in `HarProvider` to support `AuthResult` - Removed custom `headers` in HAR post requests; used `auth_result.get_dict()` for `StreamSession` - Refactored `Video` provider to support optional search in `get_response` - Added `search` parameter to `RequestConfig.get_response` and `Video.create_async_generator` - Improved browser automation and element interaction logic in `Video` provider - Extracted video request interception to collect URLs using `nodriver` - Reduced video polling loop timeout from 600 to 300 iterations in `Video` - Updated CLI `client.py` to fix handling of `conversation.conversation` assignment - Fixed argparse config: removed `nargs='?'` and added `metavar` for `--conversation-file` - Improved image metadata extraction in API and backend when Pillow is available - Modified `ImageResponse.__str__` to output HTML anchor/image tags with dimensions if present - Added support for returning `target_path` from `copy_media` if `return_target` is True - Changed default image processing size in `process_image` from 800x400 to 400x400 - Disabled RGBA-to-RGB flattening in `process_image` - Improved `get_args_from_nodriver` to ensure proper referer and cookie handling - Added helper `get_target_paths_and_urls` in `Api` to extract image dimensions from disk paths

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

代码差异

14 个文件 +207 -127
Modified g4f/Provider/Copilot.py +42 -10
@@ -3,6 +3,8 @@ from __future__ import annotations
3 3 import os
4 4 import json
5 5 import asyncio
6 import base64
7 from typing import AsyncIterator
6 8 from urllib.parse import quote
7 9
8 10 try:
@@ -17,12 +19,12 @@ try:
17 19 except ImportError:
18 20 has_nodriver = False
19 21
20 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
22 from .base_provider import AsyncAuthedProvider, ProviderModelMixin
21 23 from .helper import format_prompt_max_length
22 24 from .openai.har_file import get_headers, get_har_files
23 25 from ..typing import AsyncResult, Messages, MediaListType
24 26 from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError
25 from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups, TitleGeneration, Sources, SourceLink
27 from ..providers.response import *
26 28 from ..tools.media import merge_media
27 29 from ..requests import get_nodriver
28 30 from ..image import to_bytes, is_accepted_format
@@ -35,7 +37,7 @@ class Conversation(JsonConversation):
35 37 def __init__(self, conversation_id: str):
36 38 self.conversation_id = conversation_id
37 39
38 class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
40 class Copilot(AsyncAuthedProvider, ProviderModelMixin):
39 41 label = "Microsoft Copilot"
40 42 url = "https://copilot.microsoft.com"
41 43
@@ -58,11 +60,35 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
58 60 _cookies: dict = None
59 61
60 62 @classmethod
61 async def create_async_generator(
63 async def on_auth_async(cls, **kwargs) -> AsyncIterator:
64 yield AuthResult(
65 api_key=cls._access_token,
66 cookies=cls.cookies_to_dict()
67 )
68
69 @classmethod
70 async def create_authed(
71 cls,
72 model: str,
73 messages: Messages,
74 auth_result: AuthResult,
75 **kwargs
76 ) -> AsyncResult:
77 cls._access_token = getattr(auth_result, "api_key")
78 cls._cookies = getattr(auth_result, "cookies")
79 async for chunk in cls.create(model, messages, **kwargs):
80 yield chunk
81 auth_result.cookies = cls.cookies_to_dict()
82
83 @classmethod
84 def cookies_to_dict(cls):
85 return cls._cookies if isinstance(cls._cookies, dict) else {c.name: c.value for c in cls._cookies}
86
87 @classmethod
88 async def create(
62 89 cls,
63 90 model: str,
64 91 messages: Messages,
65 stream: bool = False,
66 92 proxy: str = None,
67 93 timeout: int = 30,
68 94 prompt: str = None,
@@ -108,8 +134,11 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
108 134 response.raise_for_status()
109 135 user = response.json().get('firstName')
110 136 if user is None:
137 if cls.needs_auth:
138 raise MissingAuthError("No user found, please login first")
111 139 cls._access_token = None
112 debug.log(f"Copilot: User: {user or 'null'}")
140 else:
141 debug.log(f"Copilot: User: {user}")
113 142 if conversation is None:
114 143 response = await session.post(cls.conversation_url)
115 144 response.raise_for_status()
@@ -160,8 +189,8 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
160 189 sources = {}
161 190 while not wss.closed:
162 191 try:
163 msg = await asyncio.wait_for(wss.recv(), 3 if done else timeout)
164 msg = json.loads(msg[0])
192 msg_txt, _ = await asyncio.wait_for(wss.recv(), 3 if done else timeout)
193 msg = json.loads(msg_txt)
165 194 except:
166 195 break
167 196 last_msg = msg
@@ -184,10 +213,13 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
184 213 elif msg.get("event") == "citation":
185 214 sources[msg.get("url")] = msg
186 215 yield SourceLink(list(sources.keys()).index(msg.get("url")), msg.get("url"))
216 elif msg.get("event") == "partialImageGenerated":
217 mime_type = is_accepted_format(base64.b64decode(msg.get("content")[:12]))
218 yield ImagePreview(f"data:{mime_type};base64,{msg.get('content')}", image_prompt)
187 219 elif msg.get("event") == "error":
188 220 raise RuntimeError(f"Error: {msg}")
189 elif msg.get("event") not in ["received", "startMessage", "partCompleted"]:
190 debug.log(f"Copilot Message: {msg}")
221 elif msg.get("event") not in ["received", "startMessage", "partCompleted", "connected"]:
222 debug.log(f"Copilot Message: {msg_txt[:100]}...")
191 223 if not done:
192 224 raise RuntimeError(f"Invalid response: {last_msg}")
193 225 if sources:
Modified g4f/Provider/PollinationsAI.py +3 -5
@@ -82,7 +82,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
82 82 default_vision_model = default_model
83 83 default_audio_model = "openai-audio"
84 84 text_models = [default_model, "evil"]
85 image_models = [default_image_model, "kontext", "gptimage"]
85 image_models = [default_image_model, "turbo", "kontext", "gptimage", "transparent"]
86 86 audio_models = {default_audio_model: []}
87 87 vision_models = [default_vision_model]
88 88 _models_loaded = False
@@ -253,8 +253,6 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
253 253 cache = kwargs.get("action") == "next"
254 254 if extra_body is None:
255 255 extra_body = {}
256 # Load model list
257 cls.get_models()
258 256 if not model:
259 257 has_audio = "audio" in kwargs or "audio" in kwargs.get("modalities", [])
260 258 if not has_audio and media is not None:
@@ -269,7 +267,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
269 267 pass
270 268 if model in cls.image_models:
271 269 async for chunk in cls._generate_image(
272 model=model,
270 model="gptimage" if model == "transparent" else model,
273 271 prompt=format_media_prompt(messages, prompt),
274 272 media=media,
275 273 proxy=proxy,
@@ -282,7 +280,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
282 280 private=private,
283 281 enhance=enhance,
284 282 safe=safe,
285 transparent=transparent,
283 transparent=transparent or model == "transparent",
286 284 n=n,
287 285 referrer=referrer,
288 286 api_key=api_key
Modified g4f/Provider/har/__init__.py +35 -16
@@ -4,26 +4,49 @@ import os
4 4 import json
5 5 import uuid
6 6 import random
7 import asyncio
7 8 from urllib.parse import urlparse
8 9
9 10 from ...typing import AsyncResult, Messages, MediaListType
10 11 from ...requests import DEFAULT_HEADERS, StreamSession, StreamResponse, FormData, raise_for_status
11 from ...providers.response import JsonConversation
12 from ...providers.response import JsonConversation, AuthResult
13 from ...requests import get_args_from_nodriver, has_nodriver
12 14 from ...tools.media import merge_media
13 15 from ...image import to_bytes, is_accepted_format
14 16 from ...errors import ResponseError
15 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
17 from ..base_provider import AsyncAuthedProvider, ProviderModelMixin
16 18 from ..helper import get_last_user_message
17 from ..openai.har_file import get_headers
18 19 from ..LegacyLMArena import LegacyLMArena
20 from ... import debug
19 21
20 class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
22 class HarProvider(AsyncAuthedProvider, ProviderModelMixin):
21 23 label = "LMArena (Har)"
22 24 url = "https://legacy.lmarena.ai"
23 25 api_endpoint = "/queue/join?"
24 26 working = True
25 27 default_model = LegacyLMArena.default_model
26 28
29 @classmethod
30 async def on_auth_async(cls, proxy: str = None, **kwargs):
31 if has_nodriver:
32 try:
33 async def callback(page):
34 while not await page.evaluate('document.querySelector(\'textarea[data-testid="textbox"]\')'):
35 await asyncio.sleep(1)
36 args = await get_args_from_nodriver(cls.url, proxy=proxy, callback=callback)
37 except (RuntimeError, FileNotFoundError) as e:
38 debug.log(f"Nodriver is not available:", e)
39 args = {"headers": DEFAULT_HEADERS.copy(), "cookies": {}, "impersonate": "chrome"}
40 else:
41 args = {"headers": DEFAULT_HEADERS.copy(), "cookies": {}, "impersonate": "chrome"}
42 args["headers"].update({
43 "content-type": "application/json",
44 "accept": "application/json",
45 "referer": f"{cls.url}/",
46 "origin": cls.url,
47 })
48 yield AuthResult(**args)
49
27 50 @classmethod
28 51 def get_models(cls) -> list[str]:
29 52 LegacyLMArena.get_models()
@@ -71,11 +94,11 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
71 94 return first_payload, second_payload, third_payload
72 95
73 96 @classmethod
74 async def create_async_generator(
97 async def create_authed(
75 98 cls,
76 99 model: str,
77 100 messages: Messages,
78 proxy: str = None,
101 auth_result: AuthResult,
79 102 media: MediaListType = None,
80 103 max_tokens: int = 2048,
81 104 temperature: float = 0.7,
@@ -107,7 +130,7 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
107 130 if isinstance(model, list):
108 131 model = random.choice(model)
109 132 prompt = get_last_user_message(messages)
110 async with StreamSession(impersonate="chrome") as session:
133 async with StreamSession(**auth_result.get_dict()) as session:
111 134 if conversation is None:
112 135 conversation = JsonConversation(session_hash=str(uuid.uuid4()).replace("-", ""))
113 136 media = list(merge_media(media, messages))
@@ -146,28 +169,24 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
146 169 postData = postData.replace("__MODEL__", model)
147 170 request_url = request_url.replace("__SESSION__", conversation.session_hash)
148 171 method = v['request']['method'].lower()
149 async with getattr(session, method)(request_url, data=postData, headers={**get_headers(v), **DEFAULT_HEADERS}, proxy=proxy) as response:
172 async with getattr(session, method)(request_url, data=postData) as response:
150 173 await raise_for_status(response)
151 174 async for chunk in read_response(response):
152 175 yield chunk
153 176 yield conversation
154 177 else:
155 178 first_payload, second_payload, third_payload = cls._build_second_payloads(model, conversation.session_hash, prompt, max_tokens, temperature, top_p)
156 headers = {
157 "Content-Type": "application/json",
158 "Accept": "application/json",
159 }
160 179 # POST 1
161 async with session.post(f"{cls.url}{cls.api_endpoint}", json=first_payload, proxy=proxy, headers=headers) as response:
180 async with session.post(f"{cls.url}{cls.api_endpoint}", json=first_payload) as response:
162 181 await raise_for_status(response)
163 182 # POST 2
164 async with session.post(f"{cls.url}{cls.api_endpoint}", json=second_payload, proxy=proxy, headers=headers) as response:
183 async with session.post(f"{cls.url}{cls.api_endpoint}", json=second_payload) as response:
165 184 await raise_for_status(response)
166 185 # POST 3
167 async with session.post(f"{cls.url}{cls.api_endpoint}", json=third_payload, proxy=proxy, headers=headers) as response:
186 async with session.post(f"{cls.url}{cls.api_endpoint}", json=third_payload) as response:
168 187 await raise_for_status(response)
169 188 stream_url = f"{cls.url}/queue/data?session_hash={conversation.session_hash}"
170 async with session.get(stream_url, headers={"Accept": "text/event-stream"}, proxy=proxy) as response:
189 async with session.get(stream_url, headers={"Accept": "text/event-stream"}) as response:
171 190 await raise_for_status(response)
172 191 async for chunk in read_response(response):
173 192 yield chunk
Modified g4f/Provider/needs_auth/CopilotAccount.py +1 -19
@@ -10,7 +10,7 @@ from ...typing import AsyncResult, Messages
10 10 from ...errors import NoValidHarFileError
11 11 from ... import debug
12 12
13 class CopilotAccount(Copilot, AsyncAuthedProvider):
13 class CopilotAccount(Copilot):
14 14 needs_auth = True
15 15 use_nodriver = True
16 16 parent = "Copilot"
@@ -32,21 +32,3 @@ class CopilotAccount(Copilot, AsyncAuthedProvider):
32 32 api_key=cls._access_token,
33 33 cookies=cls.cookies_to_dict()
34 34 )
35
36 @classmethod
37 async def create_authed(
38 cls,
39 model: str,
40 messages: Messages,
41 auth_result: AuthResult,
42 **kwargs
43 ) -> AsyncResult:
44 cls._access_token = getattr(auth_result, "api_key")
45 cls._cookies = getattr(auth_result, "cookies")
46 async for chunk in cls.create_async_generator(model, messages, **kwargs):
47 yield chunk
48 auth_result.cookies = cls.cookies_to_dict()
49
50 @classmethod
51 def cookies_to_dict(cls):
52 return cls._cookies if isinstance(cls._cookies, dict) else {c.name: c.value for c in cls._cookies}
Modified g4f/Provider/needs_auth/LMArenaBeta.py +3 -0
@@ -121,6 +121,9 @@ class LMArenaBeta(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
121 121 model = image_models[model]
122 122 elif model in text_models:
123 123 model = text_models[model]
124 elif model in cls.model_aliases:
125 model = cls.model_aliases[model]
126 debug.log(f"Using model alias: {model}")
124 127 else:
125 128 raise ModelNotFoundError(f"Model '{model}' is not supported by LMArena Beta.")
126 129 userMessageId = str(uuid.uuid4())
Modified g4f/Provider/needs_auth/Video.py +47 -42
@@ -1,6 +1,7 @@
1 1 from __future__ import annotations
2 2
3 3 import asyncio
4 from typing import Optional
4 5 from aiohttp import ClientSession, ClientTimeout
5 6
6 7 from urllib.parse import quote, quote_plus
@@ -28,22 +29,23 @@ class RequestConfig:
28 29 headers: dict = {}
29 30
30 31 @classmethod
31 async def get_response(cls, prompt: str) -> VideoResponse | None:
32 async def get_response(cls, prompt: str, search: bool = False) -> Optional[VideoResponse]:
32 33 if prompt in cls.urls and cls.urls[prompt]:
33 34 unique_list = list(set(cls.urls[prompt]))[:10]
34 35 return VideoResponse(unique_list, prompt, {
35 36 "headers": {"authorization": cls.headers.get("authorization")} if cls.headers.get("authorization") else {},
36 37 })
37 async with ClientSession() as session:
38 found_urls = []
39 for skip in range(0, 9):
40 async with session.get(SEARCH_URL + quote_plus(prompt) + f"?skip={skip}", timeout=ClientTimeout(total=10)) as response:
41 if response.ok:
42 found_urls.append(str(response.url))
43 else:
44 break
45 if found_urls:
46 return VideoResponse(found_urls, prompt)
38 if search:
39 async with ClientSession() as session:
40 found_urls = []
41 for skip in range(0, 9):
42 async with session.get(SEARCH_URL + quote_plus(prompt) + f"?skip={skip}", timeout=ClientTimeout(total=10)) as response:
43 if response.ok:
44 found_urls.append(str(response.url))
45 else:
46 break
47 if found_urls:
48 return VideoResponse(found_urls, prompt)
47 49
48 50 class Video(AsyncGeneratorProvider, ProviderModelMixin):
49 51 urls = {
@@ -83,14 +85,14 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
83 85 prompt = format_media_prompt(messages, prompt).encode()[:100].decode("utf-8", "ignore").strip()
84 86 if not prompt:
85 87 raise ValueError("Prompt cannot be empty.")
86 response = await RequestConfig.get_response(prompt)
88 response = await RequestConfig.get_response(prompt, model=="search")
87 89 if response:
88 90 yield Reasoning(label=f"Found {len(response.urls)} Video(s)", status="")
89 91 yield response
90 92 return
91 93 try:
92 94 yield Reasoning(label="Open browser")
93 browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="gemini")
95 browser, stop_browser = await get_nodriver(proxy=proxy)
94 96 except Exception as e:
95 97 debug.error(f"Error getting nodriver:", e)
96 98 async with ClientSession() as session:
@@ -126,15 +128,17 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
126 128 RequestConfig.headers = {}
127 129 for key, value in event.request.headers.items():
128 130 RequestConfig.headers[key.lower()] = value
131 for _, urls in RequestConfig.urls.items():
132 if event.request.url in urls:
133 return
129 134 RequestConfig.urls[prompt].append(event.request.url)
130 if page is not None:
135 if model == "search" and page is not None:
131 136 await page.send(nodriver.cdp.network.enable())
132 137 page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
133 if model == "search":
134 for _ in range(5):
135 await page.scroll_down(5)
136 await asyncio.sleep(1)
137 response = await RequestConfig.get_response(prompt)
138 for _ in range(5):
139 await page.scroll_down(5)
140 await asyncio.sleep(1)
141 response = await RequestConfig.get_response(prompt, True)
138 142 if response:
139 143 stop_browser()
140 144 yield Reasoning(label="Found", status="")
@@ -151,12 +155,12 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
151 155 await button.click()
152 156 else:
153 157 debug.error("No 'Image' button found.")
154 button = await page.find("Video")
155 if button:
156 await button.click()
157 yield Reasoning(label=f"Clicked 'Video' button")
158 else:
159 debug.error("No 'Video' button found.")
158 button = await page.find("Video")
159 if button:
160 await button.click()
161 yield Reasoning(label=f"Clicked 'Video' button")
162 else:
163 debug.error("No 'Video' button found.")
160 164 except Exception as e:
161 165 debug.error(f"Error clicking button:", e)
162 166 try:
@@ -177,6 +181,8 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
177 181 debug.error(f"Error clicking button:", e)
178 182 debug.log(f"Using prompt: {prompt}")
179 183 textarea = await page.select("textarea", 180)
184 await textarea.click()
185 await textarea.clear_input()
180 186 await textarea.send_keys(prompt)
181 187 yield Reasoning(label=f"Sending prompt", token=prompt)
182 188 try:
@@ -185,19 +191,13 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
185 191 await button.click()
186 192 except Exception as e:
187 193 debug.error(f"Error clicking submit button:", e)
188 for idx in range(60):
189 try:
190 button = await page.find("Create")
191 if button:
192 await button.click()
193 yield Reasoning(label=f"Clicked 'Create' button")
194 break
195 except Exception as e:
196 if idx == 59:
197 stop_browser()
198 raise e
199 debug.error(f"Error clicking 'Create' button:", e)
200 await asyncio.sleep(1)
194 try:
195 button = await page.find("Create video")
196 if button:
197 await button.click()
198 yield Reasoning(label=f"Clicked 'Create video' button")
199 except Exception as e:
200 debug.error(f"Error clicking 'Create video' button:", e)
201 201 try:
202 202 button = await page.find("Activity")
203 203 if button:
@@ -217,18 +217,23 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
217 217 if idx == 59:
218 218 debug.error(e)
219 219 raise RuntimeError("Failed to click 'Queued' button")
220 for idx in range(600):
221 yield Reasoning(label="Waiting for Video...", status=f"{idx+1}/600")
220 await asyncio.sleep(3)
221 if model != "search" and page is not None:
222 await page.send(nodriver.cdp.network.enable())
223 page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
224 for idx in range(300):
225 yield Reasoning(label="Waiting for Video...", status=f"{idx+1}/300")
222 226 await asyncio.sleep(1)
223 227 if RequestConfig.urls[prompt]:
224 228 await asyncio.sleep(2)
225 response = await RequestConfig.get_response(prompt)
229 response = await RequestConfig.get_response(prompt, model=="search")
226 230 if response:
227 231 stop_browser()
228 232 yield Reasoning(label="Finished", status="")
229 233 yield response
230 234 return
231 if idx == 599:
235 if idx == 299:
236 stop_browser()
232 237 raise RuntimeError("Failed to get Video URL")
233 238 finally:
234 239 stop_browser()
Modified g4f/cli/client.py +3 -3
@@ -130,7 +130,7 @@ async def stream_response(
130 130 break
131 131 print("\n", end="")
132 132
133 conversation.conversation = None if last_chunk is None else last_chunk.conversation
133 conversation.conversation = getattr(last_chunk, "conversation", conversation.conversation)
134 134 media_content = next(iter([chunk for chunk in response_content if isinstance(chunk, MediaResponse)]), None)
135 135 response_content = response_content[0] if len(response_content) == 1 else "".join([str(chunk) for chunk in response_content])
136 136 if output_file:
@@ -201,7 +201,6 @@ def get_parser():
201 201 default=None,
202 202 type=Path,
203 203 metavar='FILE',
204 nargs='?',
205 204 help="Output file to save the response file."
206 205 )
207 206 parser.add_argument(
@@ -218,6 +217,7 @@ def get_parser():
218 217 parser.add_argument(
219 218 '--conversation-file',
220 219 type=Path,
220 metavar='FILE',
221 221 default=CONVERSATION_FILE,
222 222 help="File to store/load conversation state"
223 223 )
@@ -234,7 +234,7 @@ def get_parser():
234 234 parser.add_argument(
235 235 'input',
236 236 nargs='*',
237 help="Input text (or read from stdin)"
237 help="Input urls, files and text (or read from stdin)"
238 238 )
239 239
240 240 return parser
Modified g4f/client/__init__.py +3 -3
@@ -507,7 +507,7 @@ class Images:
507 507 api_key=api_key,
508 508 **kwargs
509 509 ):
510 if isinstance(item, (MediaResponse, AudioResponse)):
510 if isinstance(item, (MediaResponse, AudioResponse)) and not isinstance(item, HiddenResponse):
511 511 items.append(item)
512 512 elif hasattr(provider_handler, "create_completion"):
513 513 for item in provider_handler.create_completion(
@@ -518,7 +518,7 @@ class Images:
518 518 api_key=api_key,
519 519 **kwargs
520 520 ):
521 if isinstance(item, (MediaResponse, AudioResponse)):
521 if isinstance(item, (MediaResponse, AudioResponse)) and not isinstance(item, HiddenResponse):
522 522 items.append(item)
523 523 else:
524 524 raise ValueError(f"Provider {provider_name} does not support image generation")
@@ -751,7 +751,7 @@ class AsyncImages(Images):
751 751 **kwargs
752 752 ) -> ImagesResponse:
753 753 return await self.async_create_variation(
754 image, model, provider, response_format, **kwargs
754 image=image, model=model, provider=provider, response_format=response_format, **kwargs
755 755 )
756 756
757 757 class AsyncResponses():
Modified g4f/gui/server/api.py +30 -2
@@ -7,6 +7,12 @@ from typing import Iterator
7 7 from flask import send_from_directory, request
8 8 from inspect import signature
9 9
10 try:
11 from PIL import Image
12 has_pillow = True
13 except ImportError:
14 has_pillow = False
15
10 16 from ...errors import VersionNotFoundError, MissingAuthError
11 17 from ...image.copy_images import copy_media, ensure_media_dir, get_media_dir
12 18 from ...image import get_width_height
@@ -210,10 +216,22 @@ class Api:
210 216 proxy=proxy,
211 217 alt=chunk.alt,
212 218 tags=tags,
213 add_url=f"width={width}&height={height}&",
219 add_url=True,
214 220 timeout=kwargs.get("timeout"),
221 return_target=True if isinstance(chunk, ImageResponse) else False,
215 222 ))
216 media = ImageResponse(media, chunk.alt) if isinstance(chunk, ImageResponse) else VideoResponse(media, chunk.alt)
223 options = {}
224 target_paths, urls = get_target_paths_and_urls(media)
225 if target_paths:
226 if has_pillow:
227 try:
228 with Image.open(target_paths[0]) as img:
229 width, height = img.size
230 options = {"width": width, "height": height}
231 except Exception as e:
232 logger.exception(e)
233 options["target_paths"] = target_paths
234 media = ImageResponse(urls, chunk.alt, options) if isinstance(chunk, ImageResponse) else VideoResponse(media, chunk.alt)
217 235 yield self._format_json("content", str(media), urls=media.urls, alt=media.alt)
218 236 elif isinstance(chunk, SynthesizeData):
219 237 yield self._format_json("synthesize", chunk.get_dict())
@@ -287,3 +305,13 @@ class Api:
287 305
288 306 def get_error_message(exception: Exception) -> str:
289 307 return f"{type(exception).__name__}: {exception}"
308
309 def get_target_paths_and_urls(media: list[Union[str, tuple[str, str]]]) -> tuple[list[str], list[str]]:
310 target_paths = []
311 urls = []
312 for item in media:
313 if isinstance(item, tuple):
314 item, target_path = item
315 target_paths.append(target_path)
316 urls.append(item)
317 return target_paths, urls
Modified g4f/gui/server/backend_api.py +7 -5
@@ -46,7 +46,6 @@ from ...image import is_allowed_extension, process_image, MEDIA_TYPE_MAP
46 46 from ...cookies import get_cookies_dir
47 47 from ...image.copy_images import secure_filename, get_source_url, get_media_dir, copy_media
48 48 from ...client.service import get_model_and_provider
49 from ... import ChatCompletion
50 49 from ... import models
51 50 from .api import Api
52 51
@@ -442,18 +441,21 @@ class Backend_Api(Api):
442 441 if is_media:
443 442 os.makedirs(media_dir, exist_ok=True)
444 443 newfile = os.path.join(media_dir, filename)
445 if result:
446 media.append({"name": filename, "text": result})
447 else:
448 media.append({"name": filename})
444 image_size = {}
449 445 if has_pillow:
450 446 try:
451 447 image = Image.open(copyfile)
448 width, height = image.size
449 image_size = {"width": width, "height": height}
452 450 thumbnail_dir = os.path.join(bucket_dir, "thumbnail")
453 451 os.makedirs(thumbnail_dir, exist_ok=True)
454 452 process_image(image, save=os.path.join(thumbnail_dir, filename))
455 453 except Exception as e:
456 454 logger.exception(e)
455 if result:
456 media.append({"name": filename, "text": result, **image_size})
457 else:
458 media.append({"name": filename, **image_size})
457 459 elif is_supported and not result:
458 460 newfile = os.path.join(bucket_dir, filename)
459 461 filenames.append(filename)
Modified g4f/image/__init__.py +6 -5
@@ -205,7 +205,7 @@ def extract_data_uri(data_uri: str) -> bytes:
205 205 data = base64.b64decode(data)
206 206 return data
207 207
208 def process_image(image: Image.Image, new_width: int = 800, new_height: int = 400, save: str = None) -> Image.Image:
208 def process_image(image: Image.Image, new_width: int = 400, new_height: int = 400, save: str = None) -> Image.Image:
209 209 """
210 210 Processes the given image by adjusting its orientation and resizing it.
211 211
@@ -221,10 +221,11 @@ def process_image(image: Image.Image, new_width: int = 800, new_height: int = 40
221 221 image.thumbnail((new_width, new_height))
222 222 # Remove transparency
223 223 if image.mode == "RGBA":
224 image.load()
225 white = Image.new('RGB', image.size, (255, 255, 255))
226 white.paste(image, mask=image.split()[-1])
227 image = white
224 # image.load()
225 # white = Image.new('RGB', image.size, (255, 255, 255))
226 # white.paste(image, mask=image.split()[-1])
227 # image = white
228 pass
228 229 # Convert to RGB for jpg format
229 230 elif image.mode != "RGB":
230 231 image = image.convert("RGB")
Modified g4f/image/copy_images.py +10 -5
@@ -127,7 +127,8 @@ async def copy_media(
127 127 target: str = None,
128 128 thumbnail: bool = False,
129 129 ssl: bool = None,
130 timeout: Optional[int] = None
130 timeout: Optional[int] = None,
131 return_target: bool = False
131 132 ) -> list[str]:
132 133 """
133 134 Download and store images locally with Unicode-safe filenames
@@ -141,7 +142,8 @@ async def copy_media(
141 142 media_dir = os.path.join(media_dir, "thumbnails")
142 143 if not os.path.exists(media_dir):
143 144 os.makedirs(media_dir, exist_ok=True)
144
145 if headers is not None or cookies is not None:
146 add_url = False # Do not add URL if headers or cookies are provided
145 147 async with ClientSession(
146 148 connector=get_connector(proxy=proxy),
147 149 cookies=cookies,
@@ -206,9 +208,12 @@ async def copy_media(
206 208 except ValueError:
207 209 pass
208 210 if thumbnail:
209 return "/thumbnail/" + os.path.basename(target_path)
210 # Build URL relative to media directory
211 return f"/media/{os.path.basename(target_path)}" + ('?' + (add_url if isinstance(add_url, str) else '' + 'url=' + quote(image)) if add_url and not image.startswith('data:') else '')
211 uri = "/thumbnail/" + os.path.basename(target_path)
212 else:
213 uri = f"/media/{os.path.basename(target_path)}" + ('?' + (add_url if isinstance(add_url, str) else '' + 'url=' + quote(image)) if add_url and not image.startswith('data:') else '')
214 if return_target:
215 return uri, target_path
216 return uri
212 217
213 218 except (ClientError, IOError, OSError, ValueError) as e:
214 219 debug.error(f"Image copying failed:", e)
Modified g4f/providers/response.py +11 -8
Modified g4f/requests/__init__.py +6 -4