import os import logging from moviepy.editor import * from moviepy.config import change_settings from PIL import Image import PIL # ==== CONFIG ==== THUMBNAIL_HEIGHT_RATIO = 0.25 LOGO_SIZE_RATIO = 0.12 TEXT_MARGIN = 20 LOGO_MARGIN = 20 THUMBNAIL_SHOW_DURATION = 5 if not hasattr(PIL.Image, 'ANTIALIAS'): PIL.Image.ANTIALIAS = PIL.Image.Resampling.LANCZOS # Font đẹp hơn nếu đã cài: 'Roboto-Bold', 'Montserrat-Bold'... FONT_NAME = 'Roboto-Bold' # Đặt đường dẫn ImageMagick nếu cần change_settings({ "IMAGEMAGICK_BINARY": r"C:\Program Files\ImageMagick-7.1.2-Q16-HDRI\magick.exe" }) logging.basicConfig(level=logging.INFO) # ==== TEST INPUT ==== thumbnail_path = "thumbnails/thumbnail1.jpg" logo_path = "logo.png" description = "MÔ TẢ TEST VỀ DESCRIPTION MỘT HAI BA BỐN NĂM SÁU" date = "23/07/2025" output_path = "test_output_frame.png" target_resolution = (1080, 1920) # Tỉ lệ dọc 9:16 def create_thumbnail_overlay( thumbnail_path: str, logo_path: str, description: str, date: str, target_resolution: tuple[int, int], duration: float = THUMBNAIL_SHOW_DURATION ) -> CompositeVideoClip: """ Tạo overlay thumbnail với logo và text cho 5 giây đầu video. Thumbnail sẽ được scale để CHIỀU RỘNG luôn bao phủ đủ (có thể hi sinh chiều cao). Layout: ┌─────────────────────────┐ │ VIDEO │ │ LOGO │ ← Góc phải trên │ │ │ │ ├─────────────────────────┤ │ THUMBNAIL DATE │ ← Date ở góc phải trên của thumbnail │ + DESCRIPTION │ (Text overlay trên thumbnail) │ │ └─────────────────────────┘ Args: thumbnail_path: Đường dẫn ảnh thumbnail logo_path: Đường dẫn logo description: Mô tả văn bản (hiển thị trên thumbnail) date: Ngày tháng (hiển thị ở góc phải trên thumbnail) target_resolution: (width, height) của video duration: Thời lượng hiển thị overlay (mặc định 5 giây) Returns: CompositeVideoClip chứa thumbnail overlay """ target_width, target_height = target_resolution max_thumbnail_height = int(target_height * THUMBNAIL_HEIGHT_RATIO) # 1. Tạo nền trong suốt cho overlay overlay_background = ColorClip( size=target_resolution, color=(255, 255, 255), # Đen duration=duration ).set_opacity(0) # Trong suốt overlay_clips = [overlay_background] # 2. Xử lý thumbnail - SCALE ĐỂ FILL WIDTH actual_thumbnail_height = max_thumbnail_height # Mặc định thumbnail_y_position = target_height - max_thumbnail_height if thumbnail_path and os.path.exists(thumbnail_path): try: thumbnail_clip = ImageClip(thumbnail_path, duration=duration) # Lấy kích thước gốc của thumbnail original_thumb_width, original_thumb_height = get_media_dimensions(thumbnail_clip) if original_thumb_width and original_thumb_height: # SCALE ĐỂ FILL WIDTH (ưu tiên chiều rộng) scale_to_fit_width = target_width / original_thumb_width # Tính chiều cao sau khi scale theo width scaled_height = int(original_thumb_height * scale_to_fit_width) # Resize thumbnail để fill width thumbnail_resized = thumbnail_clip.resize((target_width, scaled_height)) # Nếu chiều cao sau scale vượt quá max_thumbnail_height, crop từ giữa if scaled_height > max_thumbnail_height: # Crop từ giữa để giữ lại phần quan trọng nhất crop_start_y = (scaled_height - max_thumbnail_height) // 2 crop_end_y = crop_start_y + max_thumbnail_height thumbnail_resized = thumbnail_resized.crop( x1=0, y1=crop_start_y, x2=target_width, y2=crop_end_y ) actual_thumbnail_height = max_thumbnail_height logging.info(f"Thumbnail được crop: từ {scaled_height}px xuống {max_thumbnail_height}px") else: # Nếu chiều cao sau scale nhỏ hơn max, cập nhật actual height actual_thumbnail_height = scaled_height # Tính lại vị trí Y dựa trên chiều cao thực tế thumbnail_y_position = target_height - actual_thumbnail_height # Đặt vị trí thumbnail ở dưới cùng, fill width thumbnail_positioned = thumbnail_resized.set_position((0, thumbnail_y_position)) overlay_clips.append(thumbnail_positioned) logging.info(f"Thumbnail - Original: {original_thumb_width}x{original_thumb_height}") logging.info(f"Thumbnail - Scaled: {target_width}x{scaled_height}") logging.info(f"Thumbnail - Final: {target_width}x{actual_thumbnail_height}") logging.info(f"Thumbnail - Position: (0, {thumbnail_y_position})") else: # Fallback nếu không lấy được kích thước thumbnail_resized = thumbnail_clip.resize((target_width, max_thumbnail_height)) thumbnail_positioned = thumbnail_resized.set_position((0, thumbnail_y_position)) overlay_clips.append(thumbnail_positioned) logging.warning(f"Sử dụng fallback resize cho thumbnail: {thumbnail_path}") except Exception as e: logging.warning(f"Không thể xử lý thumbnail '{thumbnail_path}': {e}") # 3. Xử lý logo - đặt ở góc phải trên if logo_path and os.path.exists(logo_path): try: logo_clip = ImageClip(logo_path, duration=duration) # Resize logo theo tỷ lệ logo_width = int(target_width * LOGO_SIZE_RATIO) logo_resized = logo_clip.resize(width=logo_width) # Đặt vị trí logo ở góc phải trên logo_x = target_width - logo_resized.w - LOGO_MARGIN logo_y = LOGO_MARGIN logo_positioned = logo_resized.set_position((logo_x, logo_y)) overlay_clips.append(logo_positioned) logging.info(f"Logo - Size: {logo_resized.w}x{logo_resized.h} - Position: ({logo_x}, {logo_y})") except Exception as e: logging.warning(f"Không thể xử lý logo '{logo_path}': {e}") # 4. Thêm text description - đặt TRONG vùng thumbnail if description and description.strip(): try: # Tính toán vị trí text trong vùng thumbnail desc_y = thumbnail_y_position + TEXT_MARGIN # Bên trong thumbnail, từ trên xuống # Tạo text clip cho description với font đẹp hơn font_size = max(int(target_width * 0.025), 40) # Minimum 16px # Thử các font đẹp theo thứ tự ưu tiên preferred_fonts = [ 'Open Sans', 'Poppins-Bold', # Clean, modern font 'Helvetica-Bold', # Classic font 'Arial-Bold' # Fallback ] description_font = 'Arial-Bold' # Default fallback for font in preferred_fonts: try: # Test font by creating a small clip test_clip = TextClip("Test", fontsize=14, font=font) description_font = font break except: continue desc_clip = TextClip( description.strip(), fontsize=font_size, font=description_font, color='white', stroke_color='black', stroke_width=0, method='caption', size=(target_width - 2 * TEXT_MARGIN, None) # Giới hạn width ).set_duration(duration).set_position((TEXT_MARGIN, desc_y)) overlay_clips.append(desc_clip) logging.info(f"Description text - Font: {description_font} {font_size}px - Position: ({TEXT_MARGIN}, {desc_y})") except Exception as e: logging.warning(f"Không thể tạo description text: {e}") # 5. Thêm text date - đặt ở góc phải trên của thumbnail if date and date.strip(): try: # Tính toán vị trí date ở góc phải trên của thumbnail date_y = thumbnail_y_position + TEXT_MARGIN - 40 # Trên cùng của thumbnail # Tạo text clip cho date với font đẹp hơn date_font_size = max(int(target_width * 0.02), 28) # Nhỏ hơn description, minimum 14px # Thử các font đẹp cho date date_preferred_fonts = [ 'Open Sans', 'Segoe UI', # Windows modern font 'SF Pro Text', # macOS system font 'Roboto', # Android/Google font 'Inter', # Modern web font 'Poppins', # Clean, modern font 'Helvetica', # Classic font 'Arial' # Fallback ] date_font = 'Arial' # Default fallback for font in date_preferred_fonts: try: # Test font by creating a small clip test_clip = TextClip("Test", fontsize=14, font=font) date_font = font break except: continue # Tạo date clip để tính toán width temp_date_clip = TextClip( date.strip(), fontsize=date_font_size, font=date_font, color='white' ) # Tính toán vị trí X để đặt ở góc phải date_x = target_width - temp_date_clip.w - TEXT_MARGIN date_clip = TextClip( date.strip(), fontsize=date_font_size, font=date_font, color='white', stroke_color='black', stroke_width=0 ).set_duration(duration).set_position((date_x, date_y)) overlay_clips.append(date_clip) logging.info(f"Date text - Font: {date_font} {date_font_size}px - Position: ({date_x}, {date_y})") except Exception as e: logging.warning(f"Không thể tạo date text: {e}") # 6. Composite tất cả các elements thumbnail_overlay = CompositeVideoClip(overlay_clips, size=target_resolution) return thumbnail_overlay if __name__ == "__main__": # Gọi hàm tạo overlay clip = create_thumbnail_overlay( thumbnail_path=thumbnail_path, logo_path=logo_path, description=description, date=date, target_resolution=target_resolution ) # Trích xuất frame tại thời điểm t = 0 giây frame = clip.get_frame(0) # Trả về numpy array (H, W, 3) # Lưu frame thành ảnh PNG image = Image.fromarray(frame) image.save("output_thumbnail_frame.png")