Add animated GIF support for profile pictures

- Detect animated GIFs and preserve animation frames during upload
- Extract dominant color from first frame for role color syncing
- Generate multi-frame descriptions using existing video analysis pipeline
- Skip face detection/cropping for GIFs to maintain original animation
- Update UI to inform users about GIF support and Nitro requirement
- Add metadata flag to distinguish animated vs static profile pictures
This commit is contained in:
2025-12-07 23:48:12 +02:00
parent 782d8e4f84
commit 9009e9fc80
2 changed files with 160 additions and 0 deletions

View File

@@ -2,6 +2,13 @@
"""
Intelligent profile picture manager for Miku.
Handles searching, face detection, cropping, and Discord avatar updates.
Supports both static images and animated GIFs:
- Static images (PNG, JPG, etc.): Full processing with face detection, smart cropping, resizing,
and single-frame description generation
- Animated GIFs: Fast path that preserves animation, extracts frames for multi-frame description,
and extracts dominant color from first frame
Note: Animated avatars require Discord Nitro on the bot account
"""
import os
@@ -240,6 +247,7 @@ class ProfilePictureManager:
# Step 1: Get and validate image (with retry for Danbooru)
image_bytes = None
image = None
is_animated_gif = False
if custom_image_bytes:
# Custom upload - no retry needed
@@ -253,6 +261,21 @@ class ProfilePictureManager:
image = Image.open(io.BytesIO(image_bytes))
if debug:
print(f"📐 Original image size: {image.size}")
# Check if it's an animated GIF
if image.format == 'GIF':
try:
# Check if GIF has multiple frames
image.seek(1)
is_animated_gif = True
image.seek(0) # Reset to first frame
if debug:
print("🎬 Detected animated GIF - will preserve animation")
except EOFError:
# Only one frame, treat as static image
if debug:
print("🖼️ Single-frame GIF - will process as static image")
except Exception as e:
result["error"] = f"Failed to open image: {e}"
return result
@@ -318,6 +341,89 @@ class ProfilePictureManager:
result["error"] = f"Could not find valid Miku image after {max_retries} attempts"
return result
# === ANIMATED GIF FAST PATH ===
# If this is an animated GIF, skip most processing and use raw bytes
if is_animated_gif:
if debug:
print("🎬 Using GIF fast path - skipping face detection and cropping")
# Generate description of the animated GIF
if debug:
print("📝 Generating GIF description using video analysis pipeline...")
description = await self._generate_gif_description(image_bytes, debug=debug)
if description:
# Save description to file
description_path = os.path.join(self.PROFILE_PIC_DIR, "current_description.txt")
try:
with open(description_path, 'w', encoding='utf-8') as f:
f.write(description)
result["metadata"]["description"] = description
if debug:
print(f"📝 Saved GIF description ({len(description)} chars)")
except Exception as e:
print(f"⚠️ Failed to save description file: {e}")
else:
if debug:
print("⚠️ GIF description generation returned None")
# Extract dominant color from first frame
dominant_color = self._extract_dominant_color(image, debug=debug)
if dominant_color:
result["metadata"]["dominant_color"] = {
"rgb": dominant_color,
"hex": "#{:02x}{:02x}{:02x}".format(*dominant_color)
}
if debug:
print(f"🎨 Dominant color from first frame: RGB{dominant_color} (#{result['metadata']['dominant_color']['hex'][1:]})")
# Save the original GIF bytes
with open(self.CURRENT_PATH, 'wb') as f:
f.write(image_bytes)
if debug:
print(f"💾 Saved animated GIF ({len(image_bytes)} bytes)")
# Update Discord avatar with original GIF
if globals.client and globals.client.user:
try:
if globals.client.loop and globals.client.loop.is_running():
future = asyncio.run_coroutine_threadsafe(
globals.client.user.edit(avatar=image_bytes),
globals.client.loop
)
future.result(timeout=10)
else:
await globals.client.user.edit(avatar=image_bytes)
result["success"] = True
result["metadata"]["changed_at"] = datetime.now().isoformat()
result["metadata"]["animated"] = True
# Save metadata
self._save_metadata(result["metadata"])
print(f"✅ Animated profile picture updated successfully!")
# Update role colors if we have a dominant color
if dominant_color:
await self._update_role_colors(dominant_color, debug=debug)
return result
except discord.HTTPException as e:
result["error"] = f"Discord API error: {e}"
print(f"⚠️ Failed to update Discord avatar with GIF: {e}")
print(f" Note: Animated avatars require Discord Nitro")
return result
except Exception as e:
result["error"] = f"Unexpected error updating avatar: {e}"
print(f"⚠️ Unexpected error: {e}")
return result
else:
result["error"] = "Bot client not ready"
return result
# === NORMAL STATIC IMAGE PATH ===
# Step 2: Generate description of the validated image
if debug:
print("📝 Generating image description...")
@@ -385,6 +491,7 @@ class ProfilePictureManager:
result["success"] = True
result["metadata"]["changed_at"] = datetime.now().isoformat()
result["metadata"]["animated"] = False
# Save metadata
self._save_metadata(result["metadata"])
@@ -521,6 +628,55 @@ Keep the description conversational and in second-person (referring to Miku as "
return None
async def _generate_gif_description(self, gif_bytes: bytes, debug: bool = False) -> Optional[str]:
"""
Generate a detailed description of an animated GIF using the video analysis pipeline.
Args:
gif_bytes: Raw GIF bytes
debug: Enable debug output
Returns:
Description string or None
"""
try:
from utils.image_handling import extract_video_frames, analyze_video_with_vision
if debug:
print("🎬 Extracting frames from GIF...")
# Extract frames from the GIF (6 frames for good analysis)
frames = await extract_video_frames(gif_bytes, num_frames=6)
if not frames:
if debug:
print("⚠️ Failed to extract frames from GIF")
return None
if debug:
print(f"✅ Extracted {len(frames)} frames from GIF")
print(f"🌐 Analyzing GIF with vision model...")
# Use the existing analyze_video_with_vision function (no timeout issues)
# Note: This uses a generic prompt, but it works reliably
description = await analyze_video_with_vision(frames, media_type="gif")
if description and description.strip() and not description.startswith("Error"):
if debug:
print(f"✅ Generated GIF description: {description[:100]}...")
return description.strip()
else:
if debug:
print(f"⚠️ GIF description failed or empty: {description}")
return None
except Exception as e:
print(f"⚠️ Error generating GIF description: {e}")
import traceback
traceback.print_exc()
return None
async def _verify_and_locate_miku(self, image_bytes: bytes, debug: bool = False) -> Dict:
"""
Use vision model to verify image contains Miku and locate her if multiple characters.