MEDIUM: Refactor profile_picture_manager.py Into Separate Classes #25

Open
opened 2026-02-16 22:45:15 +02:00 by Koko210 · 0 comments
Owner

profile_picture_manager.py mixes face detection, image processing, and Discord operations, making it hard to test and maintain.

Where It Occurs

  • bot/profile_picture_manager.py - Monolithic class doing everything

Why This Is a Problem

  1. Multiple Concerns: Face detection, image processing, Discord mixed
  2. Hard to Test: Cannot test face detection without Discord
  3. Hard to Reuse: Cannot use face detection elsewhere
  4. Large Class: 500+ lines in one class

What Can Go Wrong

Scenario 1: Testing Face Detection

  1. Developer wants to test face detection algorithm
  2. Tests require Discord client mock
  3. Tests are slow and complex
  4. Developer gives up, tests not written

Scenario 2: Reusing Face Detection

  1. Another module needs face detection
  2. Cannot import from profile_picture_manager.py
  3. Copies code instead of reusing
  4. Duplicated code, maintenance nightmare

Proposed Fix

Extract into separate classes:

bot/
├── profile_picture_manager.py    # Main coordinator
├── face_detection.py           # NEW - FaceDetector class
├── image_processing.py         # NEW - ImageProcessor class
└── discord_avatar.py           # NEW - DiscordAvatarManager class

Example classes:

# bot/face_detection.py - NEW
import cv2

class FaceDetector:
    def __init__(self, model_path: str):
        self.model = cv2.CascadeClassifier(model_path)
    
    def detect_faces(self, image: np.ndarray) -> List[Tuple[int, int, int, int]]:
        """Detect faces in image, returns list of (x, y, w, h)"""
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        faces = self.model.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5)
        return [(x, y, w, h) for (x, y, w, h) in faces]

# bot/image_processing.py - NEW
from PIL import Image

class ImageProcessor:
    @staticmethod
    def crop_face(image: Image, face_rect: Tuple[int, int, int, int]) -> Image:
        """Crop image to face region"""
        x, y, w, h = face_rect
        return image.crop((x, y, x+w, y+h))
    
    @staticmethod
    def resize(image: Image, size: Tuple[int, int]) -> Image:
        """Resize image to specified dimensions"""
        return image.resize(size, Image.LANCZOS)
    
    @staticmethod
    def to_discord_avatar(image: Image) -> bytes:
        """Convert PIL image to Discord avatar format"""
        img_byte_arr = io.BytesIO()
        image.save(img_byte_arr, format='PNG')
        return img_byte_arr.getvalue()

# bot/discord_avatar.py - NEW
import discord

class DiscordAvatarManager:
    def __init__(self, bot: discord.Client):
        self.bot = bot
    
    async def set_avatar(self, image: bytes):
        """Set bot's Discord avatar"""
        await self.bot.user.edit(avatar=image)
    
    async def get_user_avatar(self, user_id: int) -> bytes:
        """Download user's Discord avatar"""
        user = await self.bot.fetch_user(user_id)
        avatar_url = user.avatar.url
        async with aiohttp.ClientSession() as session:
            async with session.get(avatar_url) as resp:
                return await resp.read()

# bot/profile_picture_manager.py - REFACTORED
from bot.face_detection import FaceDetector
from bot.image_processing import ImageProcessor
from bot.discord_avatar import DiscordAvatarManager

class ProfilePictureManager:
    def __init__(self, bot):
        self.face_detector = FaceDetector("haarcascade_frontalface_default.xml")
        self.image_processor = ImageProcessor()
        self.avatar_manager = DiscordAvatarManager(bot)
    
    async def set_avatar_from_image(self, image_path: str):
        """Extract face and set as Discord avatar"""
        image = Image.open(image_path)
        faces = self.face_detector.detect_faces(np.array(image))
        if not faces:
            raise ValueError("No faces detected")
        
        face = faces[0]  # Use first detected face
        cropped = self.image_processor.crop_face(image, face)
        resized = self.image_processor.resize(cropped, (128, 128))
        avatar_bytes = self.image_processor.to_discord_avatar(resized)
        await self.avatar_manager.set_avatar(avatar_bytes)

Severity

MEDIUM - Monolithic class is hard to test and maintain.

Files Affected

bot/profile_picture_manager.py (refactor), new files: bot/face_detection.py, bot/image_processing.py, bot/discord_avatar.py

profile_picture_manager.py mixes face detection, image processing, and Discord operations, making it hard to test and maintain. ## Where It Occurs - bot/profile_picture_manager.py - Monolithic class doing everything ## Why This Is a Problem 1. Multiple Concerns: Face detection, image processing, Discord mixed 2. Hard to Test: Cannot test face detection without Discord 3. Hard to Reuse: Cannot use face detection elsewhere 4. Large Class: 500+ lines in one class ## What Can Go Wrong ### Scenario 1: Testing Face Detection 1. Developer wants to test face detection algorithm 2. Tests require Discord client mock 3. Tests are slow and complex 4. Developer gives up, tests not written ### Scenario 2: Reusing Face Detection 1. Another module needs face detection 2. Cannot import from profile_picture_manager.py 3. Copies code instead of reusing 4. Duplicated code, maintenance nightmare ## Proposed Fix Extract into separate classes: ``` bot/ ├── profile_picture_manager.py # Main coordinator ├── face_detection.py # NEW - FaceDetector class ├── image_processing.py # NEW - ImageProcessor class └── discord_avatar.py # NEW - DiscordAvatarManager class ``` Example classes: ```python # bot/face_detection.py - NEW import cv2 class FaceDetector: def __init__(self, model_path: str): self.model = cv2.CascadeClassifier(model_path) def detect_faces(self, image: np.ndarray) -> List[Tuple[int, int, int, int]]: """Detect faces in image, returns list of (x, y, w, h)""" gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) faces = self.model.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5) return [(x, y, w, h) for (x, y, w, h) in faces] # bot/image_processing.py - NEW from PIL import Image class ImageProcessor: @staticmethod def crop_face(image: Image, face_rect: Tuple[int, int, int, int]) -> Image: """Crop image to face region""" x, y, w, h = face_rect return image.crop((x, y, x+w, y+h)) @staticmethod def resize(image: Image, size: Tuple[int, int]) -> Image: """Resize image to specified dimensions""" return image.resize(size, Image.LANCZOS) @staticmethod def to_discord_avatar(image: Image) -> bytes: """Convert PIL image to Discord avatar format""" img_byte_arr = io.BytesIO() image.save(img_byte_arr, format='PNG') return img_byte_arr.getvalue() # bot/discord_avatar.py - NEW import discord class DiscordAvatarManager: def __init__(self, bot: discord.Client): self.bot = bot async def set_avatar(self, image: bytes): """Set bot's Discord avatar""" await self.bot.user.edit(avatar=image) async def get_user_avatar(self, user_id: int) -> bytes: """Download user's Discord avatar""" user = await self.bot.fetch_user(user_id) avatar_url = user.avatar.url async with aiohttp.ClientSession() as session: async with session.get(avatar_url) as resp: return await resp.read() # bot/profile_picture_manager.py - REFACTORED from bot.face_detection import FaceDetector from bot.image_processing import ImageProcessor from bot.discord_avatar import DiscordAvatarManager class ProfilePictureManager: def __init__(self, bot): self.face_detector = FaceDetector("haarcascade_frontalface_default.xml") self.image_processor = ImageProcessor() self.avatar_manager = DiscordAvatarManager(bot) async def set_avatar_from_image(self, image_path: str): """Extract face and set as Discord avatar""" image = Image.open(image_path) faces = self.face_detector.detect_faces(np.array(image)) if not faces: raise ValueError("No faces detected") face = faces[0] # Use first detected face cropped = self.image_processor.crop_face(image, face) resized = self.image_processor.resize(cropped, (128, 128)) avatar_bytes = self.image_processor.to_discord_avatar(resized) await self.avatar_manager.set_avatar(avatar_bytes) ``` ## Severity MEDIUM - Monolithic class is hard to test and maintain. ## Files Affected bot/profile_picture_manager.py (refactor), new files: bot/face_detection.py, bot/image_processing.py, bot/discord_avatar.py
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Koko210/miku-discord#25