Download test_something.py from TDN-M/Vooo: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/spaces/TDN-M/Vooo/resolve/main/test_something.py
- Command line
-
hf download hf://spaces/TDN-M/Vooo/test_something.py
-
curl -L -o test_something.py https://huggingface.co/spaces/TDN-M/Vooo/resolve/main/test_something.py
12.3 kB
| 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") | |