File size: 12,334 Bytes
7ad1067
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
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")