File size: 36,411 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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
import os
import logging
import random
from moviepy.editor import (
    AudioFileClip, ImageClip, concatenate_videoclips, 
    VideoFileClip, ColorClip, CompositeVideoClip, TextClip
)
from pathlib import Path
import math
# Thêm đoạn này vào đầu file video_generator_module.py
import PIL.Image
from moviepy.config import change_settings

change_settings({"IMAGEMAGICK_BINARY": r"C:\Program Files\ImageMagick-7.1.2-Q16-HDRI\magick.exe"})

# Patch thuộc tính ANTIALIAS nếu thiếu
if not hasattr(PIL.Image, 'ANTIALIAS'):
    PIL.Image.ANTIALIAS = PIL.Image.Resampling.LANCZOS

# --- CẤU HÌNH LOGGING ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# --- HẰNG SỐ VÀ THIẾT LẬP CHUNG ---
DEFAULT_SAVE_DIR = "generated_video_output"
DEFAULT_ASPECT_RATIOS = {
    "ngang": (16, 9),   # Phổ biến cho YouTube, Facebook feed
    "doc": (9, 16),     # Phổ biến cho TikTok, Reels, Shorts
    "vuong": (1, 1)     # Phổ biến cho Instagram Feed, Twitter
}

# Cấu hình cho thumbnail overlay
THUMBNAIL_SHOW_DURATION = 5.0  # 5 giây đầu
THUMBNAIL_HEIGHT_RATIO = 1/3    # 1/3 chiều cao video (có thể bị điều chỉnh)
LOGO_SIZE_RATIO = 0.08         # Logo chiếm 8% chiều rộng video
LOGO_MARGIN = 20               # Margin từ góc
TEXT_MARGIN = 20               # Margin cho text trong thumbnail

# --- HỖ TRỢ CHUYỂN ĐỔI TỶ LỆ KHUNG HÌNH ---
def get_resolution(aspect_ratio_key: str, height: int = 1080) -> tuple[int, int]:
    """
    Trả về độ phân giải (width, height) dựa trên key tỷ lệ khung hình và chiều cao mong muốn.
    """
    if aspect_ratio_key not in DEFAULT_ASPECT_RATIOS:
        raise ValueError(f"Tỷ lệ khung hình '{aspect_ratio_key}' không hỗ trợ. Hãy chọn trong: {list(DEFAULT_ASPECT_RATIOS.keys())}")
    
    width_ratio, height_ratio = DEFAULT_ASPECT_RATIOS[aspect_ratio_key]
    
    if height_ratio == 0: 
        raise ValueError("Tỷ lệ chiều cao không được bằng 0.")
    
    width = int(height * width_ratio / height_ratio)
    width = width + (width % 2) # Đảm bảo kích thước là số chẵn
    
    logging.info(f"Độ phân giải cho '{aspect_ratio_key}' (height={height}): ({width}, {height})")
    return (width, height)

def get_media_dimensions(clip):
    """
    Lấy kích thước thực tế của media clip
    """
    if hasattr(clip, 'w') and hasattr(clip, 'h'):
        return clip.w, clip.h
    elif hasattr(clip, 'size'):
        return clip.size
    else:
        return None, None

def calculate_aspect_ratio(width, height):
    """
    Tính tỷ lệ khung hình của media
    """
    if width and height and height != 0:
        return width / height
    return None

def smart_fit_and_fill(clip, target_resolution, media_type='image', fill_color=(0, 0, 0)):
    """
    Fit media vào target resolution mà không méo hình, và fill các vùng trống với màu nền.
    
    Args:
        clip: Video hoặc Image clip cần xử lý
        target_resolution: (width, height) của target
        media_type: 'image' hoặc 'video'
        fill_color: Màu nền cho vùng trống (R, G, B)
    
    Returns:
        Composite clip với media được fit và background được fill
    """
    target_width, target_height = target_resolution
    
    # Lấy kích thước gốc của clip
    original_width, original_height = get_media_dimensions(clip)
    
    if not original_width or not original_height:
        logging.warning(f"Không thể lấy kích thước gốc của clip, sử dụng resize thông thường")
        return clip.resize(target_resolution)
    
    original_aspect_ratio = calculate_aspect_ratio(original_width, original_height)
    target_aspect_ratio = target_width / target_height
    
    if not original_aspect_ratio:
        logging.warning(f"Không thể tính tỷ lệ khung hình gốc, sử dụng resize thông thường")
        return clip.resize(target_resolution)
    
    logging.info(f"Media gốc: {original_width}x{original_height} (ratio: {original_aspect_ratio:.2f})")
    logging.info(f"Target: {target_width}x{target_height} (ratio: {target_aspect_ratio:.2f})")
    
    # Kiểm tra nếu tỷ lệ gần như giống nhau
    if abs(original_aspect_ratio - target_aspect_ratio) < 0.01:
        logging.info("Tỷ lệ khung hình gốc phù hợp, chỉ cần resize")
        return clip.resize(target_resolution)
    
    # Tính toán scale để fit (không bị méo)
    scale_by_width = target_width / original_width
    scale_by_height = target_height / original_height
    
    # Chọn scale nhỏ hơn để đảm bảo fit hoàn toàn vào target
    scale_factor = min(scale_by_width, scale_by_height)
    
    # Tính kích thước mới sau khi scale
    new_width = int(original_width * scale_factor)
    new_height = int(original_height * scale_factor)
    
    # Đảm bảo kích thước chẵn
    new_width = new_width + (new_width % 2)
    new_height = new_height + (new_height % 2)
    
    logging.info(f"Scale factor: {scale_factor:.3f}")
    logging.info(f"Media sau khi fit: {new_width}x{new_height}")
    
    # Resize clip với tỷ lệ đã tính
    fitted_clip = clip.resize((new_width, new_height))
    
    # Tính vị trí để center media trong target frame
    x_offset = (target_width - new_width) // 2
    y_offset = (target_height - new_height) // 2
    
    logging.info(f"Vị trí center: ({x_offset}, {y_offset})")
    
    # Tạo background với màu fill
    duration = getattr(clip, 'duration', 1.0) if hasattr(clip, 'duration') else 1.0
    background = ColorClip(
        size=target_resolution, 
        color=fill_color, 
        duration=duration
    ).set_fps(24)
    
    # Đặt vị trí cho fitted clip
    fitted_clip = fitted_clip.set_position((x_offset, y_offset))
    
    # Composite background và fitted clip
    final_clip = CompositeVideoClip([background, fitted_clip], size=target_resolution)
    
    return final_clip

def smart_crop_to_fill(clip, target_resolution, media_type='image', fill_color=(255, 255, 255), bottom_padding: int = 160):
    """
    Crop phần trung tâm của clip sao cho vừa với target resolution mà không bị méo hình.
    Phía dưới sẽ chừa ra một vùng trống (màu đen hoặc màu chỉ định) để hiển thị caption, logo,...

    Args:
        clip: Video hoặc Image clip cần xử lý
        target_resolution: (width, height) đầu ra mong muốn
        media_type: 'image' hoặc 'video'
        fill_color: màu nền cho phần trống
        bottom_padding: chiều cao phần nền đen ở dưới (tính bằng pixel)

    Returns:
        CompositeVideoClip đã crop và chèn vào khung
    """
    target_width, target_height = target_resolution
    effective_crop_height = target_height - bottom_padding  # phần sẽ chứa nội dung

    original_width, original_height = get_media_dimensions(clip)

    if not original_width or not original_height:
        return clip.resize(target_resolution)

    target_ratio = target_width / effective_crop_height
    original_ratio = original_width / original_height

    # Crop giữ phần trung tâm
    if original_ratio > target_ratio:
        new_width = int(original_height * target_ratio)
        x1 = (original_width - new_width) // 2
        x2 = x1 + new_width
        y1 = 0
        y2 = original_height
    else:
        new_height = int(original_width / target_ratio)
        y1 = (original_height - new_height) // 2
        y2 = y1 + new_height
        x1 = 0
        x2 = original_width

    cropped = clip.crop(x1=x1, y1=y1, x2=x2, y2=y2)
    resized_cropped = cropped.resize((target_width, effective_crop_height))

    # Tạo background đen (hoặc màu fill)
    duration = getattr(clip, 'duration', 1.0)
    background = ColorClip(size=target_resolution, color=fill_color, duration=duration).set_fps(24)

    # Đặt phần crop ở top-center
    final = CompositeVideoClip([background, resized_cropped.set_position(("center", "top"))], size=target_resolution)

    return final

# --- CÁC HÀM MỚI CHO THUMBNAIL OVERLAY ---

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
    """
    description = description.upper()
    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=(0, 0, 0),  # Đen
        duration=duration
    ).set_opacity(0.3)  # 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)).set_opacity(0.8)
                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)).set_opacity(0.8)
                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 với chất lượng cao
    if description and description.strip():
        try:
            # Tính toán vị trí text trong vùng thumbnail
            desc_y = thumbnail_y_position + TEXT_MARGIN  + 40# Bên trong thumbnail, từ trên xuống
            
            # Font size lớn hơn và responsive
            font_size = max(int(target_width * 0.035), 50)  # Tăng từ 0.025 lên 0.035, minimum 50px
            
            # Sử dụng font an toàn và method tối ưu
            desc_clip = TextClip(
                description.strip(),
                fontsize=font_size,
                font='Open Sans',  # Font an toàn, luôn có sẵn
                color='white',
                method='label',     # Thay đổi từ 'caption' sang 'label' để tránh blur
                align='West',       # Căn trái
                size=(target_width - 4 * TEXT_MARGIN, None)  # Tăng margin để text không bị sát mép
            ).set_duration(duration).set_position((2 * TEXT_MARGIN, desc_y))
            
            overlay_clips.append(desc_clip)
            
        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 với chất lượng cao
    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 # Cùng hàng với description
            
            # Font size cho date
            date_font_size = max(int(target_width * 0.025), 20)  # Tăng minimum lên 35px
            
            # Tạo date clip để tính toán width chính xác
            date_clip = TextClip(
                date.strip(),
                fontsize=date_font_size,
                font='Open Sans',  # Font an toàn
                color='white',
                method='label',     # Sử dụng 'label' thay vì default để tránh blur
                align='center'
            ).set_duration(duration)
            
            # Tính toán vị trí X để đặt ở góc phải
            date_x = target_width - date_clip.w - 2 * TEXT_MARGIN
            date_positioned = date_clip.set_position((date_x, date_y))
            
            overlay_clips.append(date_positioned)
            logging.info(f"Date text - Font: Arial-Bold {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
def rate_media_compatibility(clip, target_aspect_ratio, media_type='image'):
    """
    Đánh giá độ phù hợp của media với target aspect ratio.
    Trả về score từ 0-100, càng cao càng phù hợp.
    """
    original_width, original_height = get_media_dimensions(clip)
    
    if not original_width or not original_height:
        return 50  # Score trung bình nếu không xác định được
    
    original_aspect_ratio = calculate_aspect_ratio(original_width, original_height)
    
    if not original_aspect_ratio:
        return 50
    
    # Tính độ chênh lệch aspect ratio
    ratio_diff = abs(original_aspect_ratio - target_aspect_ratio)
    
    # Score dựa trên độ chênh lệch (càng ít chênh lệch càng cao điểm)
    base_score = max(0, 100 - (ratio_diff * 50))
    
    # Bonus cho video (thường linh hoạt hơn ảnh)
    if media_type == 'video':
        base_score += 10
    
    # Penalty nếu resolution quá thấp
    min_dimension = min(original_width, original_height)
    if min_dimension < 480:
        base_score -= 30
    elif min_dimension < 720:
        base_score -= 15
    
    # Bonus nếu media có thể fit tốt (ít vùng trống)
    scale_by_width = 1080 / original_width  # Giả sử target là 1080p
    scale_by_height = 1080 / original_height
    scale_factor = min(scale_by_width, scale_by_height)
    
    # Tính % diện tích media chiếm trong target frame
    fitted_area = (original_width * scale_factor) * (original_height * scale_factor)
    target_area = 1080 * (1080 * target_aspect_ratio if target_aspect_ratio < 1 else 1080)
    area_ratio = fitted_area / target_area
    
    # Bonus nếu media chiếm nhiều diện tích (ít vùng trống)
    if area_ratio > 0.8:
        base_score += 15
    elif area_ratio > 0.6:
        base_score += 10
    elif area_ratio < 0.3:
        base_score -= 10
    
    return max(0, min(100, base_score))

# --- CÁC HÀM XỬ LÝ VIDEO ---

def create_clips_from_media_files(
    media_files: list[str], 
    target_duration: float, 
    aspect_ratio_key: str, 
    resolution: tuple[int, int]
) -> list:
    """
    Chọn và tạo các clip (ảnh hoặc video) từ danh sách media,
    ưu tiên các media có aspect ratio phù hợp.
    - Video sẽ được xử lý bằng smart_crop_to_fill (cắt để lấp đầy)
    - Ảnh sẽ được xử lý bằng smart_fit_and_fill (fit với background)
    """
    if not media_files:
        logging.warning("Không có file media nào được cung cấp.")
        return []

    target_width, target_height = resolution
    target_aspect_ratio = target_width / target_height

    valid_media_objects = []
    for media_path in media_files:
        if not os.path.exists(media_path):
            logging.warning(f"File media không tồn tại, bỏ qua: {media_path}")
            continue
        
        ext = media_path.lower().split('.')[-1]
        clip_obj = None
        media_type = None

        try:
            if ext in ['mp4', 'mov', 'avi', 'mkv', 'webm']: # Định dạng video
                clip_obj = VideoFileClip(media_path)
                media_type = 'video'
            elif ext in ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'tiff']: # Định dạng ảnh
                clip_obj = ImageClip(media_path, duration=3.0) # Duration tạm thời cho ảnh
                media_type = 'image'
            else:
                logging.warning(f"Định dạng file media không hỗ trợ, bỏ qua: {media_path}")
                continue
            
            # Đánh giá độ phù hợp
            compatibility_score = rate_media_compatibility(clip_obj, target_aspect_ratio, media_type)
            logging.info(f"Media '{media_path}' - Type: {media_type} - Compatibility score: {compatibility_score:.1f}")
            
            # Áp dụng xử lý khác nhau cho video và ảnh
            if media_type == 'video':
                # Video: Sử dụng smart_crop_to_fill để cắt và lấp đầy
                processed_clip = smart_crop_to_fill(clip_obj, resolution, media_type, fill_color=(20, 20, 20))
                logging.info(f"Video được xử lý bằng smart_crop_to_fill: {media_path}")
            else:  # image
                # Ảnh: Sử dụng smart_fit_and_fill để fit với background
                processed_clip = smart_fit_and_fill(clip_obj, resolution, media_type, fill_color=(20, 20, 20))
                logging.info(f"Ảnh được xử lý bằng smart_fit_and_fill: {media_path}")

            valid_media_objects.append({
                'clip': processed_clip, 
                'type': media_type, 
                'original_clip': clip_obj,
                'compatibility_score': compatibility_score,
                'path': media_path
            })

        except Exception as e:
            logging.warning(f"Không thể xử lý file media '{media_path}': {e}. Bỏ qua.")
            if clip_obj: 
                clip_obj.close() # Đảm bảo giải phóng tài nguyên nếu có lỗi

    if not valid_media_objects:
        logging.warning("Không tìm thấy file media hợp lệ nào sau khi xử lý.")
        return []

    # Sắp xếp theo compatibility score (cao nhất trước)
    valid_media_objects.sort(key=lambda x: x['compatibility_score'], reverse=True)
    logging.info("Thứ tự ưu tiên các media:")
    for i, media in enumerate(valid_media_objects[:5]):  # Chỉ log 5 cái đầu
        logging.info(f"  {i+1}. {media['path']} - Type: {media['type']} - Score: {media['compatibility_score']:.1f}")

    # Phân bổ thời lượng và chọn media ---
    clips_for_concatenation = []
    total_clip_duration = 0
    
    # Ưu tiên các media có score cao, nhưng vẫn có element ngẫu nhiên
    selected_media = []
    high_score_media = [m for m in valid_media_objects if m['compatibility_score'] >= 70]
    medium_score_media = [m for m in valid_media_objects if 40 <= m['compatibility_score'] < 70]
    
    # Chọn tất cả media hợp lệ
    selected_media = valid_media_objects.copy()
    random.shuffle(selected_media)  # Trộn ngẫu nhiên thứ tự phát
    
    # Tính tổng thời lượng của tất cả video
    total_video_duration = 0
    for media_data in selected_media:
        if media_data['type'] == 'video':
            total_video_duration += getattr(media_data['original_clip'], 'duration', 0)
    
    # Nếu tổng thời lượng video ít hơn target_duration, thêm ảnh vào
    if total_video_duration < target_duration:
        # Thêm tất cả ảnh vào danh sách phát
        pass
    
    # Duyệt qua từng media và thêm vào danh sách phát
    for media_data in selected_media:
        clip = media_data['clip']
        
        # Lấy thời lượng gốc của media
        if media_data['type'] == 'image':
            # Đối với ảnh, mặc định 3 giây
            clip_duration = 3.0
        else:  # video
            # Đối với video, lấy thời lượng gốc
            clip_duration = getattr(media_data['original_clip'], 'duration', 0)
        
        # Nếu thêm clip này vượt quá target_duration, điều chỉnh thời lượng
        remaining_duration = target_duration - total_clip_duration
        if remaining_duration <= 0:
            break
            
        # Nếu thời lượng còn lại ít hơn thời lượng clip, cắt clip
        if clip_duration > remaining_duration:
            clip_duration = remaining_duration
        
        # Đặt thời lượng cho clip
        final_clip = clip.set_duration(clip_duration)
        clips_for_concatenation.append(final_clip)
        total_clip_duration += clip_duration
        
        logging.info(f"Đã thêm {media_data['type']}: {media_data['path']} - Duration: {clip_duration:.1f}s - Score: {media_data['compatibility_score']:.1f}")
        
        # Nếu đã đủ thời lượng, dừng lại
        if total_clip_duration >= target_duration:
            break
        
    return clips_for_concatenation

def create_video_from_audio_and_media(
    audio_path: str,
    source_media_paths: list[str], 
    aspect_ratio_key: str,
    output_filename: str = "output_video.mp4",
    version: int = 1,
    output_dir: str = DEFAULT_SAVE_DIR,
    target_height: int = 1080,
    # Tham số mới cho thumbnail overlay
    thumbnail_path: str = None,
    logo_path: str = None,
    description: str = None,
    date: str = None
) -> str:
    """
    Tạo một phiên bản video dựa trên audio và các file media (ảnh/video).
    Có thể thêm thumbnail overlay với logo và text trong 5 giây đầu.
    Thumbnail sẽ được scale để fill width (có thể hi sinh height).
    """
    if not audio_path or not os.path.exists(audio_path):
        raise FileNotFoundError("File audio không tồn tại hoặc đường dẫn bị thiếu.")
    
    if not source_media_paths:
        raise ValueError("Cần ít nhất một file media để tạo video.")

    try:
        # 1. Lấy độ phân giải
        resolution = get_resolution(aspect_ratio_key, height=target_height)
        
        # 2. Tải Audio Clip và lấy duration
        audio_clip = AudioFileClip(audio_path)
        audio_duration = audio_clip.duration
        logging.info(f"Độ dài audio: {audio_duration:.2f} giây")
        
        # 3. Tạo các clip từ danh sách media (ảnh/video) với smart processing
        media_clips = create_clips_from_media_files(source_media_paths, audio_duration, aspect_ratio_key, resolution)
        
        if not media_clips:
            logging.warning("Không có clip media nào được tạo. Sử dụng fallback.")
            # Tạo video với nền đen nếu không có media
            fallback_clip = ColorClip(
                size=resolution, 
                color=(0, 0, 0),  # Màu đen
                duration=audio_duration
            ).set_fps(24)
            media_clips = [fallback_clip]

        # 4. Ghép các clip đã được tạo
        final_video_segment = concatenate_videoclips(media_clips, method="compose")

        # 5. Đảm bảo duration của video khớp với audio
        if abs(final_video_segment.duration - audio_duration) > 0.1:
            final_video_segment = final_video_segment.set_duration(audio_duration)

        # 6. Kết hợp audio vào video clip
        final_video = final_video_segment.set_audio(audio_clip)
        
        # 7. Thêm thumbnail overlay nếu có đủ thông tin
        overlay_added = False
        if any([thumbnail_path, logo_path, description, date]):
            try:
                # Tạo thumbnail overlay cho 5 giây đầu
                thumbnail_overlay = create_thumbnail_overlay(
                    thumbnail_path=thumbnail_path,
                    logo_path=logo_path, 
                    description=description,
                    date=date,
                    target_resolution=resolution,
                    duration=min(THUMBNAIL_SHOW_DURATION, audio_duration)
                )
                
                # Composite overlay lên video
                final_video = CompositeVideoClip([final_video, thumbnail_overlay], size=resolution)
                overlay_added = True
                logging.info(f"Đã thêm thumbnail overlay (fill width) cho {min(THUMBNAIL_SHOW_DURATION, audio_duration):.1f} giây đầu")
                
            except Exception as e:
                logging.warning(f"Không thể thêm thumbnail overlay: {e}")
        
        if not overlay_added:
            logging.info("Không có thumbnail overlay được thêm vào video")
        
        # 8. Tạo thư mục output nếu chưa tồn tại
        os.makedirs(output_dir, exist_ok=True)
        output_path = os.path.join(output_dir, f"v{version}_{output_filename}")
        
        # 9. Ghi file video ra định dạng MP4
        logging.info(f"Đang ghi file video với version {version} ra: {output_path}")
        final_video.write_videofile(
            output_path,
            codec='libx264',        
            audio_codec='aac',      
            fps=24,                 
            threads=4,              
            preset='medium',        
            logger='bar',           
        )
        logging.info(f"Tạo video thành công cho phiên bản {version}!")
        
        # Giải phóng tài nguyên
        audio_clip.close()
        final_video_segment.close()
        final_video.close()
        for clip in media_clips:
            if hasattr(clip, 'close') and clip is not None: 
                clip.close()
            
        return output_path

    except FileNotFoundError as e:
        logging.error(f"Lỗi file: {e}")
        raise
    except ValueError as e:
        logging.error(f"Lỗi giá trị: {e}")
        raise
    except Exception as e:
        logging.error(f"Lỗi không xác định khi tạo video: {e}")
        import traceback
        logging.error(traceback.format_exc()) 
        raise

# --- HÀM CHÍNH ĐỂ TẠO NHIỀU PHIÊN BẢN VIDEO ---
def generate_multiple_video_versions(
    audio_path: str,
    source_media_paths: list[str], 
    aspect_ratio_key: str,
    num_versions: int = 1,
    base_filename: str = "news_video",
    output_dir: str = DEFAULT_SAVE_DIR,
    target_height: int = 1080,
    # Tham số mới cho thumbnail overlay
    thumbnail_path: str = None,
    logo_path: str = None,
    description: str = None,
    date: str = None
) -> list[str]:
    """
    Tạo N phiên bản video khác nhau với các kết hợp media (ảnh/video) ngẫu nhiên.
    Có thể thêm thumbnail overlay với logo và text trong 5 giây đầu.
    Thumbnail sẽ được scale để fill width (có thể hi sinh height).
    """
    if not audio_path or not os.path.exists(audio_path):
        raise FileNotFoundError("File audio không tồn tại hoặc đường dẫn bị thiếu.")
    
    if not source_media_paths:
        raise ValueError("Cần ít nhất một file media để tạo video.")
        
    if num_versions <= 0:
        raise ValueError("Số phiên bản phải lớn hơn 0.")

    generated_video_paths = []
    
    if aspect_ratio_key not in DEFAULT_ASPECT_RATIOS:
         raise ValueError(f"Tỷ lệ khung hình '{aspect_ratio_key}' không hỗ trợ. Hãy chọn trong: {list(DEFAULT_ASPECT_RATIOS.keys())}")
    
    valid_sources = [s for s in source_media_paths if os.path.exists(s)]
    
    if not valid_sources:
        raise ValueError("Không tìm thấy file media hợp lệ nào.")
    
    logging.info(f"Bắt đầu tạo {num_versions} phiên bản video...")
    logging.info(f"Target aspect ratio: {DEFAULT_ASPECT_RATIOS[aspect_ratio_key]}")
    logging.info("Chế độ xử lý: Video = smart_crop_to_fill, Ảnh = smart_fit_and_fill")
    
    # Log thông tin thumbnail overlay
    if any([thumbnail_path, logo_path, description, date]):
        logging.info("Thumbnail overlay sẽ được thêm với:")
        if thumbnail_path: logging.info(f"  - Thumbnail (fill width): {thumbnail_path}")
        if logo_path: logging.info(f"  - Logo: {logo_path}")
        if description: logging.info(f"  - Description (trong thumbnail): {description[:50]}...")
        if date: logging.info(f"  - Date (trong thumbnail): {date}")
        logging.info(f"  - Duration: {THUMBNAIL_SHOW_DURATION} giây đầu")
        logging.info("  - Thumbnail scaling: Fill width, crop height if needed")
    
    try:
        temp_audio_clip = AudioFileClip(audio_path)
        audio_duration = temp_audio_clip.duration
        temp_audio_clip.close()
    except Exception as e:
        raise FileNotFoundError(f"Không thể đọc file audio '{audio_path}': {e}")

    for i in range(num_versions):
        version_num = i + 1
        try:
            logging.info(f"\n--- ĐANG TẠO PHIÊN BẢN VIDEO THỨ {version_num} ---")
            output_filename = f"{base_filename}_v{version_num}.mp4"
            
            video_path = create_video_from_audio_and_media(
                audio_path=audio_path,
                source_media_paths=valid_sources, 
                aspect_ratio_key=aspect_ratio_key,
                output_filename=output_filename,
                version=version_num,
                output_dir=output_dir,
                target_height=target_height,
                # Truyền thông tin thumbnail overlay
                thumbnail_path=thumbnail_path,
                logo_path=logo_path,
                description=description,
                date=date
            )
            generated_video_paths.append(video_path)
            
        except FileNotFoundError as e:
            logging.error(f"Lỗi file ở phiên bản {version_num}: {e}. Bỏ qua phiên bản này.")
        except ValueError as e:
            logging.error(f"Lỗi giá trị ở phiên bản {version_num}: {e}. Bỏ qua phiên bản này.")
        except Exception as e:
            logging.error(f"Lỗi không xác định ở phiên bản {version_num}: {e}")
            import traceback
            logging.error(traceback.format_exc())
    
    logging.info(f"Hoàn tất tạo {len(generated_video_paths)} / {num_versions} phiên bản video.")
    return generated_video_paths


# --- KHỐI ĐỂ CHẠY THỬ NGHIỆM MODULE ---
if __name__ == '__main__':
    # Ví dụ sử dụng module để tạo video với thumbnail overlay
    # audio_file = "./generated_content/audio/podcast_3_1da3ae9d-df2a-400f-8168-b4976bc6e20b.wav"
    audio_file = "generated_content/audio/news_1_7022b2cb-b207-45c6-b24d-144e881996e9.wav"
    o = './public/u23-VietNam/media/'
    source_media = [o + 'output_00' + str(i) + '.mp4' for i in range(9)]
    source_media.extend(['public/u23-VietNam/media/u23-viet-nam-1-1753065357.jpg', 
                        'public/u23-VietNam/media/lich thi dau u23 viet nam.png',
                        'public/u23-VietNam/media/993135543_164183883u23-viet-nam.jpg'])
    
    # Thông tin thumbnail overlay
    thumbnail_img = "thumbnails/thumbnail1.png"
    logo_img = "logo.png"  # Đường dẫn logo
    desc_text = "U23 Việt Nam chuẩn bị cho giải đấu quan trọng"
    date_text = "2025-07-23"
    
    try:
        generated_videos = generate_multiple_video_versions(
            audio_path=audio_file,
            source_media_paths=source_media,
            aspect_ratio_key="doc",
            num_versions=1,
            base_filename="sports_news_video",
            output_dir=DEFAULT_SAVE_DIR,
            target_height=1080,
            # Thông tin thumbnail overlay
            thumbnail_path=thumbnail_img,
            logo_path=logo_img,
            description=desc_text,
            date=date_text
        )
        print("Các video đã được tạo thành công:", generated_videos)
    except Exception as e:
        logging.error(f"Lỗi khi tạo video: {e}")