Initial commit — ComfyUI Discord bot + web UI

Full source for the-third-rev: Discord bot (discord.py), FastAPI web UI
(React/TS/Vite/Tailwind), ComfyUI integration, generation history DB,
preset manager, workflow inspector, and all supporting modules.

Excluded from tracking: .env, invite_tokens.json, *.db (SQLite),
current-workflow-changes.json, user_settings/, presets/, logs/,
web-static/ (build output), frontend/node_modules/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Khoa (Revenovich) Tran Gia
2026-03-02 09:55:48 +07:00
commit 1ed3c9ec4b
82 changed files with 20693 additions and 0 deletions

84
image_utils.py Normal file
View File

@@ -0,0 +1,84 @@
"""
image_utils.py
==============
Shared image processing utilities.
"""
from __future__ import annotations
import io
import logging
from pathlib import Path
try:
from PIL import Image as _PILImage
_HAS_PIL = True
except ImportError:
_HAS_PIL = False
logger = logging.getLogger(__name__)
DISCORD_MAX_BYTES = 8 * 1024 * 1024 # 8 MiB Discord free-tier upload limit
def compress_to_discord_limit(data: bytes, filename: str) -> tuple[bytes, str]:
"""
Compress image bytes to fit within Discord's 8 MiB upload limit.
Tries quality reduction first, then progressive downsizing.
Converts to JPEG if the source format (e.g. PNG) cannot be quality-compressed.
Returns (data, filename) — filename may change if format conversion is needed.
No-op if data is already within the limit.
"""
if len(data) <= DISCORD_MAX_BYTES:
logger.info(f"Image size is less than {DISCORD_MAX_BYTES}")
return data, filename
if not _HAS_PIL:
logger.warning("Pillow not installed — cannot compress %s (%d bytes), uploading as-is", filename, len(data))
return data, filename
suffix = Path(filename).suffix.lower()
stem = Path(filename).stem
img = _PILImage.open(io.BytesIO(data))
# PNG/GIF/BMP don't support lossy quality — convert to JPEG
logger.info("Checking file extension for convert to jpeg")
if suffix in (".jpg", ".jpeg"):
fmt, out_name = "JPEG", filename
elif suffix == ".webp":
fmt, out_name = "WEBP", filename
else:
fmt, out_name = "JPEG", stem + ".jpg"
if img.mode in ("RGBA", "P", "LA"):
img = img.convert("RGB")
logger.info("File extension checked")
# Round 1: quality reduction only
logger.info("# Round 1: quality reduction only")
for quality in (85, 70, 55, 40, 25, 15):
logger.info(f"# Round 1: Trying quality: {quality}")
buf = io.BytesIO()
img.save(buf, format=fmt, quality=quality)
if buf.tell() <= DISCORD_MAX_BYTES:
logger.info("Compressed %s at quality=%d: %d%d bytes", filename, quality, len(data), buf.tell())
return buf.getvalue(), out_name
# Round 2: resize + low quality
logger.info("# Round 2: resize + low quality")
for scale in (0.75, 0.5, 0.35, 0.25):
w, h = img.size
resized = img.resize((int(w * scale), int(h * scale)), _PILImage.LANCZOS)
logger.info(f"# Round 2: Trying to resize: {resized.size}")
buf = io.BytesIO()
resized.save(buf, format=fmt, quality=15)
if buf.tell() <= DISCORD_MAX_BYTES:
logger.info("Compressed %s at scale=%.2f: %d%d bytes", filename, scale, len(data), buf.tell())
return buf.getvalue(), out_name
logger.warning("Could not compress %s under %d bytes — uploading best effort", filename, DISCORD_MAX_BYTES)
buf = io.BytesIO()
img.save(buf, format=fmt, quality=10)
return buf.getvalue(), out_name