|
|
import os
|
|
|
import logging
|
|
|
from moviepy.editor import *
|
|
|
from moviepy.config import change_settings
|
|
|
from PIL import Image
|
|
|
import PIL
|
|
|
|
|
|
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_NAME = 'Roboto-Bold'
|
|
|
|
|
|
|
|
|
change_settings({
|
|
|
"IMAGEMAGICK_BINARY": r"C:\Program Files\ImageMagick-7.1.2-Q16-HDRI\magick.exe"
|
|
|
})
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
|
|
|
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)
|
|
|
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)
|
|
|
|
|
|
|
|
|
overlay_background = ColorClip(
|
|
|
size=target_resolution,
|
|
|
color=(255, 255, 255),
|
|
|
duration=duration
|
|
|
).set_opacity(0)
|
|
|
|
|
|
overlay_clips = [overlay_background]
|
|
|
|
|
|
|
|
|
actual_thumbnail_height = max_thumbnail_height
|
|
|
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)
|
|
|
|
|
|
|
|
|
original_thumb_width, original_thumb_height = get_media_dimensions(thumbnail_clip)
|
|
|
|
|
|
if original_thumb_width and original_thumb_height:
|
|
|
|
|
|
scale_to_fit_width = target_width / original_thumb_width
|
|
|
|
|
|
|
|
|
scaled_height = int(original_thumb_height * scale_to_fit_width)
|
|
|
|
|
|
|
|
|
thumbnail_resized = thumbnail_clip.resize((target_width, scaled_height))
|
|
|
|
|
|
|
|
|
if scaled_height > max_thumbnail_height:
|
|
|
|
|
|
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:
|
|
|
|
|
|
actual_thumbnail_height = scaled_height
|
|
|
|
|
|
|
|
|
thumbnail_y_position = target_height - actual_thumbnail_height
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
if logo_path and os.path.exists(logo_path):
|
|
|
try:
|
|
|
logo_clip = ImageClip(logo_path, duration=duration)
|
|
|
|
|
|
|
|
|
logo_width = int(target_width * LOGO_SIZE_RATIO)
|
|
|
logo_resized = logo_clip.resize(width=logo_width)
|
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
if description and description.strip():
|
|
|
try:
|
|
|
|
|
|
desc_y = thumbnail_y_position + TEXT_MARGIN
|
|
|
|
|
|
|
|
|
font_size = max(int(target_width * 0.025), 40)
|
|
|
|
|
|
|
|
|
preferred_fonts = [
|
|
|
'Open Sans',
|
|
|
'Poppins-Bold',
|
|
|
'Helvetica-Bold',
|
|
|
'Arial-Bold'
|
|
|
]
|
|
|
|
|
|
description_font = 'Arial-Bold'
|
|
|
for font in preferred_fonts:
|
|
|
try:
|
|
|
|
|
|
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)
|
|
|
).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}")
|
|
|
|
|
|
|
|
|
if date and date.strip():
|
|
|
try:
|
|
|
|
|
|
date_y = thumbnail_y_position + TEXT_MARGIN - 40
|
|
|
|
|
|
|
|
|
date_font_size = max(int(target_width * 0.02), 28)
|
|
|
|
|
|
|
|
|
date_preferred_fonts = [
|
|
|
'Open Sans',
|
|
|
'Segoe UI',
|
|
|
'SF Pro Text',
|
|
|
'Roboto',
|
|
|
'Inter',
|
|
|
'Poppins',
|
|
|
'Helvetica',
|
|
|
'Arial'
|
|
|
]
|
|
|
|
|
|
date_font = 'Arial'
|
|
|
for font in date_preferred_fonts:
|
|
|
try:
|
|
|
|
|
|
test_clip = TextClip("Test", fontsize=14, font=font)
|
|
|
date_font = font
|
|
|
break
|
|
|
except:
|
|
|
continue
|
|
|
|
|
|
|
|
|
temp_date_clip = TextClip(
|
|
|
date.strip(),
|
|
|
fontsize=date_font_size,
|
|
|
font=date_font,
|
|
|
color='white'
|
|
|
)
|
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
thumbnail_overlay = CompositeVideoClip(overlay_clips, size=target_resolution)
|
|
|
|
|
|
return thumbnail_overlay
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
clip = create_thumbnail_overlay(
|
|
|
thumbnail_path=thumbnail_path,
|
|
|
logo_path=logo_path,
|
|
|
description=description,
|
|
|
date=date,
|
|
|
target_resolution=target_resolution
|
|
|
)
|
|
|
|
|
|
|
|
|
frame = clip.get_frame(0)
|
|
|
|
|
|
|
|
|
image = Image.fromarray(frame)
|
|
|
image.save("output_thumbnail_frame.png")
|
|
|
|