-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat: supports image compressing (#6463) #6794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a47b2b8
f10cafa
b76c68b
20378e3
4f4eecf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,14 +4,23 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||
| import base64 | ||||||||||||||||||||||||||||||||||||||||||||||||
| import io | ||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from PIL import Image as PILImage | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from astrbot import logger | ||||||||||||||||||||||||||||||||||||||||||||||||
| from astrbot.core.utils.astrbot_path import get_astrbot_temp_path | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| IMAGE_COMPRESS_DEFAULT_MAX_SIZE = 1280 | ||||||||||||||||||||||||||||||||||||||||||||||||
| IMAGE_COMPRESS_DEFAULT_QUALITY = 95 | ||||||||||||||||||||||||||||||||||||||||||||||||
| IMAGE_COMPRESS_DEFAULT_OPTIMIZE = True | ||||||||||||||||||||||||||||||||||||||||||||||||
| IMAGE_COMPRESS_DEFAULT_MIN_FILE_SIZE_MB = 1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| async def get_media_duration(file_path: str) -> int | None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| """使用ffprobe获取媒体文件时长 | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -316,3 +325,88 @@ async def extract_video_cover( | |||||||||||||||||||||||||||||||||||||||||||||||
| return output_path | ||||||||||||||||||||||||||||||||||||||||||||||||
| except FileNotFoundError: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raise Exception("ffmpeg not found") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def _compress_image_sync( | ||||||||||||||||||||||||||||||||||||||||||||||||
| data: bytes, | ||||||||||||||||||||||||||||||||||||||||||||||||
| temp_dir: Path, | ||||||||||||||||||||||||||||||||||||||||||||||||
| max_size: int, | ||||||||||||||||||||||||||||||||||||||||||||||||
| quality: int, | ||||||||||||||||||||||||||||||||||||||||||||||||
| optimize: bool, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||
| """Run image compression synchronously via ``asyncio.to_thread``.""" | ||||||||||||||||||||||||||||||||||||||||||||||||
| with PILImage.open(io.BytesIO(data)) as opened_img: | ||||||||||||||||||||||||||||||||||||||||||||||||
| img = opened_img | ||||||||||||||||||||||||||||||||||||||||||||||||
| converted_img: PILImage.Image | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if img.mode != "RGB": | ||||||||||||||||||||||||||||||||||||||||||||||||
| converted_img = img.convert("RGB") | ||||||||||||||||||||||||||||||||||||||||||||||||
| img = converted_img | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if max(img.size) > max_size: | ||||||||||||||||||||||||||||||||||||||||||||||||
| img.thumbnail((max_size, max_size), PILImage.Resampling.LANCZOS) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| new_uuid = uuid.uuid4().hex | ||||||||||||||||||||||||||||||||||||||||||||||||
| save_path = temp_dir / f"compressed_{new_uuid}.jpg" | ||||||||||||||||||||||||||||||||||||||||||||||||
| img.save(save_path, "JPEG", quality=quality, optimize=optimize) | ||||||||||||||||||||||||||||||||||||||||||||||||
| logger.debug(f"Image compressed successfully: {save_path}") | ||||||||||||||||||||||||||||||||||||||||||||||||
| return str(save_path) | ||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if converted_img is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| converted_img.close() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| async def compress_image( | ||||||||||||||||||||||||||||||||||||||||||||||||
| url_or_path: str, | ||||||||||||||||||||||||||||||||||||||||||||||||
| max_size: int = IMAGE_COMPRESS_DEFAULT_MAX_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| quality: int = IMAGE_COMPRESS_DEFAULT_QUALITY, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||
| """Compress large user-uploaded images. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||||||||
| url_or_path: Image path or URL. | ||||||||||||||||||||||||||||||||||||||||||||||||
| max_size: Longest edge of the compressed image in pixels. | ||||||||||||||||||||||||||||||||||||||||||||||||
| quality: JPEG output quality in the range 1-100. | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||
| The compressed image path. Returns the original path if compression | ||||||||||||||||||||||||||||||||||||||||||||||||
| fails or the source does not need compression. | ||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||
| max_size = max(int(max_size), 1) | ||||||||||||||||||||||||||||||||||||||||||||||||
| quality = min(max(int(quality), 1), 100) | ||||||||||||||||||||||||||||||||||||||||||||||||
| optimize = IMAGE_COMPRESS_DEFAULT_OPTIMIZE | ||||||||||||||||||||||||||||||||||||||||||||||||
| min_file_size_bytes = int(IMAGE_COMPRESS_DEFAULT_MIN_FILE_SIZE_MB * 1024 * 1024) | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = None | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Skip compression for remote images and return the original value. | ||||||||||||||||||||||||||||||||||||||||||||||||
| if url_or_path.startswith("http"): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return url_or_path | ||||||||||||||||||||||||||||||||||||||||||||||||
| elif url_or_path.startswith("data:image"): | ||||||||||||||||||||||||||||||||||||||||||||||||
| _header, encoded = url_or_path.split(",", 1) | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = base64.b64decode(encoded) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if len(data) < min_file_size_bytes: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return url_or_path | ||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||
| local_path = Path(url_or_path) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if not local_path.exists(): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return url_or_path | ||||||||||||||||||||||||||||||||||||||||||||||||
| if local_path.stat().st_size < min_file_size_bytes: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return url_or_path | ||||||||||||||||||||||||||||||||||||||||||||||||
| with local_path.open("rb") as f: | ||||||||||||||||||||||||||||||||||||||||||||||||
| data = f.read() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+389
to
+396
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation re-compresses JPEG images even if they already meet the size and dimension criteria. This is inefficient and can sometimes lead to a larger file size if the original image had a lower quality setting. To optimize, you could add a check for local files to see if they are already JPEGs and within the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if not data: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return url_or_path | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| temp_dir = Path(get_astrbot_temp_path()) | ||||||||||||||||||||||||||||||||||||||||||||||||
| temp_dir.mkdir(parents=True, exist_ok=True) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| # Offload the blocking image processing task to a thread. | ||||||||||||||||||||||||||||||||||||||||||||||||
| return await asyncio.to_thread( | ||||||||||||||||||||||||||||||||||||||||||||||||
| _compress_image_sync, | ||||||||||||||||||||||||||||||||||||||||||||||||
| data, | ||||||||||||||||||||||||||||||||||||||||||||||||
| temp_dir, | ||||||||||||||||||||||||||||||||||||||||||||||||
| max_size, | ||||||||||||||||||||||||||||||||||||||||||||||||
| quality, | ||||||||||||||||||||||||||||||||||||||||||||||||
| optimize, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Image compression in
_ensure_img_captionignores user provider_settings due to wrong argument type._ensure_img_captionpassescfginto_compress_image_for_provider, but that function expectsdict[str, object] | None, and_get_image_compress_argsonly reads settings when given a dict. Becausecfgis aMainAgentBuildConfig, this path always falls back to defaults and ignoresprovider_settings.image_compress_enabledandimage_compress_options. To match other call sites (_process_quote_message,build_main_agent) and honor user configuration, this should passcfg.provider_settingsinstead.