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
代码差异
@@ -3,6 +3,8 @@ from __future__ import annotations
import os
import json
import asyncio
import base64
from typing import AsyncIterator
from urllib.parse import quote
try:
@@ -17,12 +19,12 @@ try:
except ImportError:
has_nodriver = False
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
from .base_provider import AsyncAuthedProvider, ProviderModelMixin
from .helper import format_prompt_max_length
from .openai.har_file import get_headers, get_har_files
from ..typing import AsyncResult, Messages, MediaListType
from ..errors import MissingRequirementsError, NoValidHarFileError, MissingAuthError
from ..providers.response import BaseConversation, JsonConversation, RequestLogin, ImageResponse, FinishReason, SuggestedFollowups, TitleGeneration, Sources, SourceLink
from ..providers.response import *
from ..tools.media import merge_media
from ..requests import get_nodriver
from ..image import to_bytes, is_accepted_format
@@ -35,7 +37,7 @@ class Conversation(JsonConversation):
def __init__(self, conversation_id: str):
self.conversation_id = conversation_id
class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
class Copilot(AsyncAuthedProvider, ProviderModelMixin):
label = "Microsoft Copilot"
url = "https://copilot.microsoft.com"
@@ -58,11 +60,35 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
_cookies: dict = None
@classmethod
async def create_async_generator(
async def on_auth_async(cls, **kwargs) -> AsyncIterator:
yield AuthResult(
api_key=cls._access_token,
cookies=cls.cookies_to_dict()
)
@classmethod
async def create_authed(
cls,
model: str,
messages: Messages,
auth_result: AuthResult,
**kwargs
) -> AsyncResult:
cls._access_token = getattr(auth_result, "api_key")
cls._cookies = getattr(auth_result, "cookies")
async for chunk in cls.create(model, messages, **kwargs):
yield chunk
auth_result.cookies = cls.cookies_to_dict()
@classmethod
def cookies_to_dict(cls):
return cls._cookies if isinstance(cls._cookies, dict) else {c.name: c.value for c in cls._cookies}
@classmethod
async def create(
cls,
model: str,
messages: Messages,
stream: bool = False,
proxy: str = None,
timeout: int = 30,
prompt: str = None,
@@ -108,8 +134,11 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
response.raise_for_status()
user = response.json().get('firstName')
if user is None:
if cls.needs_auth:
raise MissingAuthError("No user found, please login first")
cls._access_token = None
debug.log(f"Copilot: User: {user or 'null'}")
else:
debug.log(f"Copilot: User: {user}")
if conversation is None:
response = await session.post(cls.conversation_url)
response.raise_for_status()
@@ -160,8 +189,8 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
sources = {}
while not wss.closed:
try:
msg = await asyncio.wait_for(wss.recv(), 3 if done else timeout)
msg = json.loads(msg[0])
msg_txt, _ = await asyncio.wait_for(wss.recv(), 3 if done else timeout)
msg = json.loads(msg_txt)
except:
break
last_msg = msg
@@ -184,10 +213,13 @@ class Copilot(AsyncGeneratorProvider, ProviderModelMixin):
elif msg.get("event") == "citation":
sources[msg.get("url")] = msg
yield SourceLink(list(sources.keys()).index(msg.get("url")), msg.get("url"))
elif msg.get("event") == "partialImageGenerated":
mime_type = is_accepted_format(base64.b64decode(msg.get("content")[:12]))
yield ImagePreview(f"data:{mime_type};base64,{msg.get('content')}", image_prompt)
elif msg.get("event") == "error":
raise RuntimeError(f"Error: {msg}")
elif msg.get("event") not in ["received", "startMessage", "partCompleted"]:
debug.log(f"Copilot Message: {msg}")
elif msg.get("event") not in ["received", "startMessage", "partCompleted", "connected"]:
debug.log(f"Copilot Message: {msg_txt[:100]}...")
if not done:
raise RuntimeError(f"Invalid response: {last_msg}")
if sources:
@@ -82,7 +82,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
default_vision_model = default_model
default_audio_model = "openai-audio"
text_models = [default_model, "evil"]
image_models = [default_image_model, "kontext", "gptimage"]
image_models = [default_image_model, "turbo", "kontext", "gptimage", "transparent"]
audio_models = {default_audio_model: []}
vision_models = [default_vision_model]
_models_loaded = False
@@ -253,8 +253,6 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
cache = kwargs.get("action") == "next"
if extra_body is None:
extra_body = {}
# Load model list
cls.get_models()
if not model:
has_audio = "audio" in kwargs or "audio" in kwargs.get("modalities", [])
if not has_audio and media is not None:
@@ -269,7 +267,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
pass
if model in cls.image_models:
async for chunk in cls._generate_image(
model=model,
model="gptimage" if model == "transparent" else model,
prompt=format_media_prompt(messages, prompt),
media=media,
proxy=proxy,
@@ -282,7 +280,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
private=private,
enhance=enhance,
safe=safe,
transparent=transparent,
transparent=transparent or model == "transparent",
n=n,
referrer=referrer,
api_key=api_key
@@ -4,26 +4,49 @@ import os
import json
import uuid
import random
import asyncio
from urllib.parse import urlparse
from ...typing import AsyncResult, Messages, MediaListType
from ...requests import DEFAULT_HEADERS, StreamSession, StreamResponse, FormData, raise_for_status
from ...providers.response import JsonConversation
from ...providers.response import JsonConversation, AuthResult
from ...requests import get_args_from_nodriver, has_nodriver
from ...tools.media import merge_media
from ...image import to_bytes, is_accepted_format
from ...errors import ResponseError
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
from ..base_provider import AsyncAuthedProvider, ProviderModelMixin
from ..helper import get_last_user_message
from ..openai.har_file import get_headers
from ..LegacyLMArena import LegacyLMArena
from ... import debug
class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
class HarProvider(AsyncAuthedProvider, ProviderModelMixin):
label = "LMArena (Har)"
url = "https://legacy.lmarena.ai"
api_endpoint = "/queue/join?"
working = True
default_model = LegacyLMArena.default_model
@classmethod
async def on_auth_async(cls, proxy: str = None, **kwargs):
if has_nodriver:
try:
async def callback(page):
while not await page.evaluate('document.querySelector(\'textarea[data-testid="textbox"]\')'):
await asyncio.sleep(1)
args = await get_args_from_nodriver(cls.url, proxy=proxy, callback=callback)
except (RuntimeError, FileNotFoundError) as e:
debug.log(f"Nodriver is not available:", e)
args = {"headers": DEFAULT_HEADERS.copy(), "cookies": {}, "impersonate": "chrome"}
else:
args = {"headers": DEFAULT_HEADERS.copy(), "cookies": {}, "impersonate": "chrome"}
args["headers"].update({
"content-type": "application/json",
"accept": "application/json",
"referer": f"{cls.url}/",
"origin": cls.url,
})
yield AuthResult(**args)
@classmethod
def get_models(cls) -> list[str]:
LegacyLMArena.get_models()
@@ -71,11 +94,11 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
return first_payload, second_payload, third_payload
@classmethod
async def create_async_generator(
async def create_authed(
cls,
model: str,
messages: Messages,
proxy: str = None,
auth_result: AuthResult,
media: MediaListType = None,
max_tokens: int = 2048,
temperature: float = 0.7,
@@ -107,7 +130,7 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
if isinstance(model, list):
model = random.choice(model)
prompt = get_last_user_message(messages)
async with StreamSession(impersonate="chrome") as session:
async with StreamSession(**auth_result.get_dict()) as session:
if conversation is None:
conversation = JsonConversation(session_hash=str(uuid.uuid4()).replace("-", ""))
media = list(merge_media(media, messages))
@@ -146,28 +169,24 @@ class HarProvider(AsyncGeneratorProvider, ProviderModelMixin):
postData = postData.replace("__MODEL__", model)
request_url = request_url.replace("__SESSION__", conversation.session_hash)
method = v['request']['method'].lower()
async with getattr(session, method)(request_url, data=postData, headers={**get_headers(v), **DEFAULT_HEADERS}, proxy=proxy) as response:
async with getattr(session, method)(request_url, data=postData) as response:
await raise_for_status(response)
async for chunk in read_response(response):
yield chunk
yield conversation
else:
first_payload, second_payload, third_payload = cls._build_second_payloads(model, conversation.session_hash, prompt, max_tokens, temperature, top_p)
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
# POST 1
async with session.post(f"{cls.url}{cls.api_endpoint}", json=first_payload, proxy=proxy, headers=headers) as response:
async with session.post(f"{cls.url}{cls.api_endpoint}", json=first_payload) as response:
await raise_for_status(response)
# POST 2
async with session.post(f"{cls.url}{cls.api_endpoint}", json=second_payload, proxy=proxy, headers=headers) as response:
async with session.post(f"{cls.url}{cls.api_endpoint}", json=second_payload) as response:
await raise_for_status(response)
# POST 3
async with session.post(f"{cls.url}{cls.api_endpoint}", json=third_payload, proxy=proxy, headers=headers) as response:
async with session.post(f"{cls.url}{cls.api_endpoint}", json=third_payload) as response:
await raise_for_status(response)
stream_url = f"{cls.url}/queue/data?session_hash={conversation.session_hash}"
async with session.get(stream_url, headers={"Accept": "text/event-stream"}, proxy=proxy) as response:
async with session.get(stream_url, headers={"Accept": "text/event-stream"}) as response:
await raise_for_status(response)
async for chunk in read_response(response):
yield chunk
@@ -10,7 +10,7 @@ from ...typing import AsyncResult, Messages
from ...errors import NoValidHarFileError
from ... import debug
class CopilotAccount(Copilot, AsyncAuthedProvider):
class CopilotAccount(Copilot):
needs_auth = True
use_nodriver = True
parent = "Copilot"
@@ -32,21 +32,3 @@ class CopilotAccount(Copilot, AsyncAuthedProvider):
api_key=cls._access_token,
cookies=cls.cookies_to_dict()
)
@classmethod
async def create_authed(
cls,
model: str,
messages: Messages,
auth_result: AuthResult,
**kwargs
) -> AsyncResult:
cls._access_token = getattr(auth_result, "api_key")
cls._cookies = getattr(auth_result, "cookies")
async for chunk in cls.create_async_generator(model, messages, **kwargs):
yield chunk
auth_result.cookies = cls.cookies_to_dict()
@classmethod
def cookies_to_dict(cls):
return cls._cookies if isinstance(cls._cookies, dict) else {c.name: c.value for c in cls._cookies}
@@ -121,6 +121,9 @@ class LMArenaBeta(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
model = image_models[model]
elif model in text_models:
model = text_models[model]
elif model in cls.model_aliases:
model = cls.model_aliases[model]
debug.log(f"Using model alias: {model}")
else:
raise ModelNotFoundError(f"Model '{model}' is not supported by LMArena Beta.")
userMessageId = str(uuid.uuid4())
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
from typing import Optional
from aiohttp import ClientSession, ClientTimeout
from urllib.parse import quote, quote_plus
@@ -28,22 +29,23 @@ class RequestConfig:
headers: dict = {}
@classmethod
async def get_response(cls, prompt: str) -> VideoResponse | None:
async def get_response(cls, prompt: str, search: bool = False) -> Optional[VideoResponse]:
if prompt in cls.urls and cls.urls[prompt]:
unique_list = list(set(cls.urls[prompt]))[:10]
return VideoResponse(unique_list, prompt, {
"headers": {"authorization": cls.headers.get("authorization")} if cls.headers.get("authorization") else {},
})
async with ClientSession() as session:
found_urls = []
for skip in range(0, 9):
async with session.get(SEARCH_URL + quote_plus(prompt) + f"?skip={skip}", timeout=ClientTimeout(total=10)) as response:
if response.ok:
found_urls.append(str(response.url))
else:
break
if found_urls:
return VideoResponse(found_urls, prompt)
if search:
async with ClientSession() as session:
found_urls = []
for skip in range(0, 9):
async with session.get(SEARCH_URL + quote_plus(prompt) + f"?skip={skip}", timeout=ClientTimeout(total=10)) as response:
if response.ok:
found_urls.append(str(response.url))
else:
break
if found_urls:
return VideoResponse(found_urls, prompt)
class Video(AsyncGeneratorProvider, ProviderModelMixin):
urls = {
@@ -83,14 +85,14 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
prompt = format_media_prompt(messages, prompt).encode()[:100].decode("utf-8", "ignore").strip()
if not prompt:
raise ValueError("Prompt cannot be empty.")
response = await RequestConfig.get_response(prompt)
response = await RequestConfig.get_response(prompt, model=="search")
if response:
yield Reasoning(label=f"Found {len(response.urls)} Video(s)", status="")
yield response
return
try:
yield Reasoning(label="Open browser")
browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="gemini")
browser, stop_browser = await get_nodriver(proxy=proxy)
except Exception as e:
debug.error(f"Error getting nodriver:", e)
async with ClientSession() as session:
@@ -126,15 +128,17 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
RequestConfig.headers = {}
for key, value in event.request.headers.items():
RequestConfig.headers[key.lower()] = value
for _, urls in RequestConfig.urls.items():
if event.request.url in urls:
return
RequestConfig.urls[prompt].append(event.request.url)
if page is not None:
if model == "search" and page is not None:
await page.send(nodriver.cdp.network.enable())
page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
if model == "search":
for _ in range(5):
await page.scroll_down(5)
await asyncio.sleep(1)
response = await RequestConfig.get_response(prompt)
for _ in range(5):
await page.scroll_down(5)
await asyncio.sleep(1)
response = await RequestConfig.get_response(prompt, True)
if response:
stop_browser()
yield Reasoning(label="Found", status="")
@@ -151,12 +155,12 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
await button.click()
else:
debug.error("No 'Image' button found.")
button = await page.find("Video")
if button:
await button.click()
yield Reasoning(label=f"Clicked 'Video' button")
else:
debug.error("No 'Video' button found.")
button = await page.find("Video")
if button:
await button.click()
yield Reasoning(label=f"Clicked 'Video' button")
else:
debug.error("No 'Video' button found.")
except Exception as e:
debug.error(f"Error clicking button:", e)
try:
@@ -177,6 +181,8 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
debug.error(f"Error clicking button:", e)
debug.log(f"Using prompt: {prompt}")
textarea = await page.select("textarea", 180)
await textarea.click()
await textarea.clear_input()
await textarea.send_keys(prompt)
yield Reasoning(label=f"Sending prompt", token=prompt)
try:
@@ -185,19 +191,13 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
await button.click()
except Exception as e:
debug.error(f"Error clicking submit button:", e)
for idx in range(60):
try:
button = await page.find("Create")
if button:
await button.click()
yield Reasoning(label=f"Clicked 'Create' button")
break
except Exception as e:
if idx == 59:
stop_browser()
raise e
debug.error(f"Error clicking 'Create' button:", e)
await asyncio.sleep(1)
try:
button = await page.find("Create video")
if button:
await button.click()
yield Reasoning(label=f"Clicked 'Create video' button")
except Exception as e:
debug.error(f"Error clicking 'Create video' button:", e)
try:
button = await page.find("Activity")
if button:
@@ -217,18 +217,23 @@ class Video(AsyncGeneratorProvider, ProviderModelMixin):
if idx == 59:
debug.error(e)
raise RuntimeError("Failed to click 'Queued' button")
for idx in range(600):
yield Reasoning(label="Waiting for Video...", status=f"{idx+1}/600")
await asyncio.sleep(3)
if model != "search" and page is not None:
await page.send(nodriver.cdp.network.enable())
page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
for idx in range(300):
yield Reasoning(label="Waiting for Video...", status=f"{idx+1}/300")
await asyncio.sleep(1)
if RequestConfig.urls[prompt]:
await asyncio.sleep(2)
response = await RequestConfig.get_response(prompt)
response = await RequestConfig.get_response(prompt, model=="search")
if response:
stop_browser()
yield Reasoning(label="Finished", status="")
yield response
return
if idx == 599:
if idx == 299:
stop_browser()
raise RuntimeError("Failed to get Video URL")
finally:
stop_browser()
@@ -130,7 +130,7 @@ async def stream_response(
break
print("\n", end="")
conversation.conversation = None if last_chunk is None else last_chunk.conversation
conversation.conversation = getattr(last_chunk, "conversation", conversation.conversation)
media_content = next(iter([chunk for chunk in response_content if isinstance(chunk, MediaResponse)]), None)
response_content = response_content[0] if len(response_content) == 1 else "".join([str(chunk) for chunk in response_content])
if output_file:
@@ -201,7 +201,6 @@ def get_parser():
default=None,
type=Path,
metavar='FILE',
nargs='?',
help="Output file to save the response file."
)
parser.add_argument(
@@ -218,6 +217,7 @@ def get_parser():
parser.add_argument(
'--conversation-file',
type=Path,
metavar='FILE',
default=CONVERSATION_FILE,
help="File to store/load conversation state"
)
@@ -234,7 +234,7 @@ def get_parser():
parser.add_argument(
'input',
nargs='*',
help="Input text (or read from stdin)"
help="Input urls, files and text (or read from stdin)"
)
return parser
@@ -507,7 +507,7 @@ class Images:
api_key=api_key,
**kwargs
):
if isinstance(item, (MediaResponse, AudioResponse)):
if isinstance(item, (MediaResponse, AudioResponse)) and not isinstance(item, HiddenResponse):
items.append(item)
elif hasattr(provider_handler, "create_completion"):
for item in provider_handler.create_completion(
@@ -518,7 +518,7 @@ class Images:
api_key=api_key,
**kwargs
):
if isinstance(item, (MediaResponse, AudioResponse)):
if isinstance(item, (MediaResponse, AudioResponse)) and not isinstance(item, HiddenResponse):
items.append(item)
else:
raise ValueError(f"Provider {provider_name} does not support image generation")
@@ -751,7 +751,7 @@ class AsyncImages(Images):
**kwargs
) -> ImagesResponse:
return await self.async_create_variation(
image, model, provider, response_format, **kwargs
image=image, model=model, provider=provider, response_format=response_format, **kwargs
)
class AsyncResponses():
@@ -7,6 +7,12 @@ from typing import Iterator
from flask import send_from_directory, request
from inspect import signature
try:
from PIL import Image
has_pillow = True
except ImportError:
has_pillow = False
from ...errors import VersionNotFoundError, MissingAuthError
from ...image.copy_images import copy_media, ensure_media_dir, get_media_dir
from ...image import get_width_height
@@ -210,10 +216,22 @@ class Api:
proxy=proxy,
alt=chunk.alt,
tags=tags,
add_url=f"width={width}&height={height}&",
add_url=True,
timeout=kwargs.get("timeout"),
return_target=True if isinstance(chunk, ImageResponse) else False,
))
media = ImageResponse(media, chunk.alt) if isinstance(chunk, ImageResponse) else VideoResponse(media, chunk.alt)
options = {}
target_paths, urls = get_target_paths_and_urls(media)
if target_paths:
if has_pillow:
try:
with Image.open(target_paths[0]) as img:
width, height = img.size
options = {"width": width, "height": height}
except Exception as e:
logger.exception(e)
options["target_paths"] = target_paths
media = ImageResponse(urls, chunk.alt, options) if isinstance(chunk, ImageResponse) else VideoResponse(media, chunk.alt)
yield self._format_json("content", str(media), urls=media.urls, alt=media.alt)
elif isinstance(chunk, SynthesizeData):
yield self._format_json("synthesize", chunk.get_dict())
@@ -287,3 +305,13 @@ class Api:
def get_error_message(exception: Exception) -> str:
return f"{type(exception).__name__}: {exception}"
def get_target_paths_and_urls(media: list[Union[str, tuple[str, str]]]) -> tuple[list[str], list[str]]:
target_paths = []
urls = []
for item in media:
if isinstance(item, tuple):
item, target_path = item
target_paths.append(target_path)
urls.append(item)
return target_paths, urls
@@ -46,7 +46,6 @@ from ...image import is_allowed_extension, process_image, MEDIA_TYPE_MAP
from ...cookies import get_cookies_dir
from ...image.copy_images import secure_filename, get_source_url, get_media_dir, copy_media
from ...client.service import get_model_and_provider
from ... import ChatCompletion
from ... import models
from .api import Api
@@ -442,18 +441,21 @@ class Backend_Api(Api):
if is_media:
os.makedirs(media_dir, exist_ok=True)
newfile = os.path.join(media_dir, filename)
if result:
media.append({"name": filename, "text": result})
else:
media.append({"name": filename})
image_size = {}
if has_pillow:
try:
image = Image.open(copyfile)
width, height = image.size
image_size = {"width": width, "height": height}
thumbnail_dir = os.path.join(bucket_dir, "thumbnail")
os.makedirs(thumbnail_dir, exist_ok=True)
process_image(image, save=os.path.join(thumbnail_dir, filename))
except Exception as e:
logger.exception(e)
if result:
media.append({"name": filename, "text": result, **image_size})
else:
media.append({"name": filename, **image_size})
elif is_supported and not result:
newfile = os.path.join(bucket_dir, filename)
filenames.append(filename)
@@ -205,7 +205,7 @@ def extract_data_uri(data_uri: str) -> bytes:
data = base64.b64decode(data)
return data
def process_image(image: Image.Image, new_width: int = 800, new_height: int = 400, save: str = None) -> Image.Image:
def process_image(image: Image.Image, new_width: int = 400, new_height: int = 400, save: str = None) -> Image.Image:
"""
Processes the given image by adjusting its orientation and resizing it.
@@ -221,10 +221,11 @@ def process_image(image: Image.Image, new_width: int = 800, new_height: int = 40
image.thumbnail((new_width, new_height))
# Remove transparency
if image.mode == "RGBA":
image.load()
white = Image.new('RGB', image.size, (255, 255, 255))
white.paste(image, mask=image.split()[-1])
image = white
# image.load()
# white = Image.new('RGB', image.size, (255, 255, 255))
# white.paste(image, mask=image.split()[-1])
# image = white
pass
# Convert to RGB for jpg format
elif image.mode != "RGB":
image = image.convert("RGB")
@@ -127,7 +127,8 @@ async def copy_media(
target: str = None,
thumbnail: bool = False,
ssl: bool = None,
timeout: Optional[int] = None
timeout: Optional[int] = None,
return_target: bool = False
) -> list[str]:
"""
Download and store images locally with Unicode-safe filenames
@@ -141,7 +142,8 @@ async def copy_media(
media_dir = os.path.join(media_dir, "thumbnails")
if not os.path.exists(media_dir):
os.makedirs(media_dir, exist_ok=True)
if headers is not None or cookies is not None:
add_url = False # Do not add URL if headers or cookies are provided
async with ClientSession(
connector=get_connector(proxy=proxy),
cookies=cookies,
@@ -206,9 +208,12 @@ async def copy_media(
except ValueError:
pass
if thumbnail:
return "/thumbnail/" + os.path.basename(target_path)
# Build URL relative to media directory
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 '')
uri = "/thumbnail/" + os.path.basename(target_path)
else:
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 '')
if return_target:
return uri, target_path
return uri
except (ClientError, IOError, OSError, ValueError) as e:
debug.error(f"Image copying failed:", e)