Initial commit: Telegram Sticker Bot Manager

Features:
- Create and manage sticker packs
- Add existing sticker sets to database
- Automatic image optimization (512px, WebP conversion)
- GIF to animated video sticker conversion (WebM, VP9)
- Background removal (local rembg + optional Remove.bg API)
- Hybrid API with retry logic and usage tracking
- Premium status detection
- SQLite database for persistence
- Docker containerization
- Complete OPENCODE.md documentation

Technical Stack:
- python-telegram-bot 22.7
- Pillow for image processing
- FFmpeg for video conversion
- rembg for local background removal
- aiosqlite for async database operations
This commit is contained in:
coding
2026-03-25 08:59:38 +00:00
commit f634b884ec
12 changed files with 2708 additions and 0 deletions

169
image_processor.py Normal file
View File

@@ -0,0 +1,169 @@
"""Image processing module for converting images to Telegram sticker format."""
import io
import logging
from typing import Optional, Tuple
from PIL import Image
logger = logging.getLogger(__name__)
# Telegram sticker requirements
STICKER_SIZE = 512 # One side must be exactly 512px
MAX_FILE_SIZE_MB = 3 # Maximum file size in MB
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
async def process_image_for_sticker(file_data: bytes, filename: Optional[str] = None) -> Optional[bytes]:
"""
Process an image file to meet Telegram sticker requirements.
Args:
file_data: Raw image bytes from Telegram
filename: Original filename (optional, for format detection)
Returns:
Processed image bytes in WebP format, or None if processing fails
"""
try:
# Open image from bytes
image = Image.open(io.BytesIO(file_data))
# Convert to RGBA if necessary (Telegram requires transparency support)
if image.mode != 'RGBA':
if image.mode == 'RGB':
# Add alpha channel
image = image.convert('RGBA')
elif image.mode in ('L', 'LA', 'P'):
# Grayscale or palette - convert to RGBA
image = image.convert('RGBA')
else:
# Any other mode - convert to RGBA
image = image.convert('RGBA')
# Resize to Telegram sticker format (512px on one side)
image = resize_for_sticker(image)
# Convert to WebP format with optimization
output = io.BytesIO()
# Try different quality levels to meet file size requirements
quality = 95
while quality >= 60:
output.seek(0)
output.truncate(0)
image.save(
output,
format='WEBP',
quality=quality,
method=6, # Best compression method
exact=True # Preserve transparency
)
file_size = output.tell()
if file_size <= MAX_FILE_SIZE_BYTES:
logger.info(f"Image processed: {image.size}, quality={quality}, size={file_size/1024:.1f}KB")
output.seek(0)
return output.getvalue()
# Reduce quality and try again
quality -= 5
logger.debug(f"File too large ({file_size/1024:.1f}KB), reducing quality to {quality}")
# If we get here, even lowest quality is too large
logger.error(f"Could not compress image below {MAX_FILE_SIZE_MB}MB")
return None
except Exception as e:
logger.error(f"Error processing image: {e}")
return None
def resize_for_sticker(image: Image.Image) -> Image.Image:
"""
Resize image to meet Telegram sticker requirements.
One side must be exactly 512px, other side <= 512px
Args:
image: PIL Image object
Returns:
Resized image
"""
width, height = image.size
# Determine which side is longer
if width >= height:
# Landscape or square - set width to 512
new_width = STICKER_SIZE
new_height = int((height / width) * STICKER_SIZE)
else:
# Portrait - set height to 512
new_height = STICKER_SIZE
new_width = int((width / height) * STICKER_SIZE)
# Ensure neither dimension exceeds 512
new_width = min(new_width, STICKER_SIZE)
new_height = min(new_height, STICKER_SIZE)
# Resize using LANCZOS for best quality
resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
logger.debug(f"Resized image from {width}x{height} to {new_width}x{new_height}")
return resized
async def validate_image(file_data: bytes) -> Tuple[bool, str]:
"""
Validate if file data is a valid image.
Args:
file_data: Raw file bytes
Returns:
Tuple of (is_valid, error_message)
"""
try:
image = Image.open(io.BytesIO(file_data))
# Check format
if image.format not in ('PNG', 'JPEG', 'JPG', 'WEBP', 'GIF', 'BMP', 'TIFF'):
return False, f"Unsupported image format: {image.format}"
# Check if image can be processed
width, height = image.size
if width < 64 or height < 64:
return False, "Image too small (minimum 64x64 pixels)"
# Check file size
if len(file_data) > 10 * 1024 * 1024: # 10MB limit for input
return False, "Image file too large (max 10MB for upload)"
return True, ""
except Exception as e:
return False, f"Invalid image file: {str(e)}"
async def get_image_info(file_data: bytes) -> dict:
"""
Get information about an image.
Args:
file_data: Raw image bytes
Returns:
Dictionary with image info
"""
try:
image = Image.open(io.BytesIO(file_data))
return {
"format": image.format,
"mode": image.mode,
"size": image.size,
"width": image.width,
"height": image.height,
}
except Exception as e:
logger.error(f"Error getting image info: {e}")
return {}