"""
Historical 2D Studio - Seven Nguyen Intro Video Generator V2 (2026 Engine)
Upgraded Production Pipeline:
- Audio-Driven Phonetic Lip-Sync Viseme Controller (8-12 changes/sec, silence detection)
- Autonomous Blinking Controller (3-4 frame cycle every 3.0-4.5s)
- Realistic Body & Limb Dynamics (Idle Breathing, Micro Head Nodding, Pointer Stick Ping, Tactical Encirclement Thrust)
- Animated Tactical Battlefield (Bạch Đằng Rising Stakes & Ripples, Marching Impulse Arrows, Fluttering Battle Flags)
- Dynamic Telemetry HUD (Cubic Easing Progress Bars, Real-Time Rolling Casualty Counters)
- Clean Background Isolation (Zero Ghosting / Duplicate Host)
- NVIDIA NVENC Hardware Accelerated Video Encoding (GTX 1660 Super)
- Master Muxing with Kinetic ASS Subtitles & EBU R128 Compliant Audio Mix (-14 LUFS)
"""

import os
import sys
import math
import wave
import time
import subprocess
import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont

WIDTH = 1920
HEIGHT = 1080
FPS = 30
TOTAL_DURATION = 31.0
TOTAL_FRAMES = int(TOTAL_DURATION * FPS)

def get_font(size, bold=False):
    """Lấy font chữ hệ thống hỗ trợ Unicode / Serif điện ảnh"""
    candidate_fonts = [
        "C:\\Windows\\Fonts\\georgiab.ttf" if bold else "C:\\Windows\\Fonts\\georgia.ttf",
        "C:\\Windows\\Fonts\\timesbd.ttf" if bold else "C:\\Windows\\Fonts\\times.ttf",
        "C:\\Windows\\Fonts\\arialbd.ttf" if bold else "C:\\Windows\\Fonts\\arial.ttf",
    ]
    for font_path in candidate_fonts:
        if os.path.exists(font_path):
            try:
                return ImageFont.truetype(font_path, size)
            except Exception:
                pass
    return ImageFont.load_default()

def ease_cubic_out(p):
    """Hàm gia tốc chuyển động mượt mà Cubic Easing Out"""
    p_clamped = np.clip(p, 0.0, 1.0)
    return 1.0 - (1.0 - p_clamped) ** 3.0

def make_transparent_cutout(bgr_img, threshold=28, feather=18):
    """Tạo sprite có kênh alpha trong suốt từ ảnh có nền studio gần trắng"""
    corner = bgr_img[5, 5].astype(np.float32)
    dist = np.linalg.norm(bgr_img.astype(np.float32) - corner, axis=2)
    alpha = np.ones(dist.shape, dtype=np.uint8) * 255
    mask_bg = dist < threshold
    mask_trans = (dist >= threshold) & (dist < (threshold + feather))
    alpha[mask_bg] = 0
    alpha[mask_trans] = np.clip((dist[mask_trans] - threshold) / float(feather) * 255, 0, 255).astype(np.uint8)
    bgra = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2BGRA)
    bgra[:, :, 3] = alpha
    return bgra

def overlay_alpha_image(dst_bgr, src_bgra, x, y, scale=1.0, opacity=1.0):
    """Chèn sprite BGRA vào frame BGR với vị trí, tỷ lệ và độ mờ mong muốn"""
    if scale != 1.0:
        new_w = max(1, int(src_bgra.shape[1] * scale))
        new_h = max(1, int(src_bgra.shape[0] * scale))
        src_bgra = cv2.resize(src_bgra, (new_w, new_h), interpolation=cv2.INTER_AREA if scale < 1.0 else cv2.INTER_LINEAR)
        
    sh, sw, _ = src_bgra.shape
    dh, dw, _ = dst_bgr.shape
    
    x1, y1 = max(0, int(x)), max(0, int(y))
    x2, y2 = min(dw, int(x + sw)), min(dh, int(y + sh))
    
    if x1 >= x2 or y1 >= y2:
        return dst_bgr
        
    sx1 = x1 - int(x)
    sy1 = y1 - int(y)
    sx2 = sx1 + (x2 - x1)
    sy2 = sy1 + (y2 - y1)
    
    src_crop = src_bgra[sy1:sy2, sx1:sx2]
    alpha = (src_crop[:, :, 3].astype(np.float32) / 255.0) * opacity
    alpha_3d = np.repeat(alpha[:, :, np.newaxis], 3, axis=2)
    
    dst_crop = dst_bgr[y1:y2, x1:x2].astype(np.float32)
    src_rgb = src_crop[:, :, :3].astype(np.float32)
    
    blended = src_rgb * alpha_3d + dst_crop * (1.0 - alpha_3d)
    dst_bgr[y1:y2, x1:x2] = np.clip(blended, 0, 255).astype(np.uint8)
    return dst_bgr

class AudioLipSyncController:
    """Bộ điều khiển năng lượng âm thanh & khẩu hình theo thời gian thực (Audio RMS)"""
    def __init__(self, voice_audio_path, fps=30, total_frames=930):
        self.fps = fps
        self.total_frames = total_frames
        self.frame_rms = np.zeros(total_frames, dtype=np.float32)
        self.speech_thresh = 0.02
        self.max_rms = 0.25
        
        wav_path = "output/temp_voice_analysis.wav"
        if not os.path.exists(wav_path) or os.path.getsize(wav_path) < 1000:
            print(f"[AUDIO] Converting {voice_audio_path} to uncompressed WAV for RMS waveform analysis...")
            cmd = ["ffmpeg", "-y", "-i", voice_audio_path, "-f", "wav", "-ar", "44100", "-ac", "1", wav_path]
            subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            
        try:
            wf = wave.open(wav_path, "rb")
            sr = wf.getframerate()
            n_frames = wf.getnframes()
            audio_bytes = wf.readframes(n_frames)
            wf.close()
            samples = np.frombuffer(audio_bytes, dtype=np.int16).astype(np.float32) / 32768.0
            samples_per_frame = sr / float(fps)
            
            for f_idx in range(total_frames):
                start_s = int(f_idx * samples_per_frame)
                end_s = int((f_idx + 1) * samples_per_frame)
                if start_s < len(samples):
                    chunk = samples[start_s:min(end_s, len(samples))]
                    self.frame_rms[f_idx] = np.sqrt(np.mean(chunk**2)) if len(chunk) > 0 else 0.0
                    
            self.max_rms = max(0.1, float(self.frame_rms.max()))
            self.speech_thresh = float(self.frame_rms.mean()) * 0.35
            print(f"[AUDIO] Voice analysis completed. Mean RMS: {self.frame_rms.mean():.4f}, Thresh: {self.speech_thresh:.4f}, Max: {self.max_rms:.4f}")
        except Exception as e:
            print(f"[WARN] Failed to analyze audio waveform: {e}. Using procedural cadence fallback.")
            self.frame_rms = np.array([0.08 if (i % 30 < 22) else 0.0 for i in range(total_frames)], dtype=np.float32)
            self.speech_thresh = 0.03
            self.max_rms = 0.15

    def get_viseme(self, frame_idx, scene_end=False):
        """
        Xác định viseme miệng cho frame:
        - rms < speech_thresh: im lặng -> 'closed' (hoặc 'smile' khi kết thúc/chào)
        - rms >= speech_thresh: đang phát âm -> luân chuyển nhịp nhàng 8-12 visemes/s giữa 'ah', 'oh', 'closed'
        """
        if frame_idx >= len(self.frame_rms):
            return "closed"
            
        rms = self.frame_rms[frame_idx]
        if rms < self.speech_thresh:
            return "smile" if scene_end else "closed"
            
        energy_ratio = (rms - self.speech_thresh) / max(0.001, (self.max_rms - self.speech_thresh))
        step = (frame_idx // 3) % 4
        
        if energy_ratio > 0.45:
            return "ah" if step in (0, 2) else "oh"
        else:
            if step == 0:
                return "ah"
            elif step == 1:
                return "closed"
            elif step == 2:
                return "oh"
            else:
                return "ah"

    def get_head_nod_offset(self, frame_idx):
        """Tính toán micro-nodding 0-2px gật nhẹ ăn khớp với trọng âm giọng nói"""
        if frame_idx >= len(self.frame_rms):
            return 0
        rms = self.frame_rms[frame_idx]
        if rms > self.speech_thresh:
            ratio = (rms - self.speech_thresh) / max(0.001, (self.max_rms - self.speech_thresh))
            return int(round(1.8 * min(1.0, ratio)))
        return 0

class BlinkingController:
    """Bộ điều khiển chu kỳ chớp mắt tự nhiên 3.0 - 4.5 giây / lần"""
    def __init__(self, total_frames=930):
        self.blink_anchors = [45, 150, 255, 360, 475, 600, 726, 855]

    def get_blink_ratio(self, frame_idx):
        for anchor in self.blink_anchors:
            diff = frame_idx - anchor
            if diff == 0:
                return 0.55
            elif diff == 1:
                return 1.0
            elif diff == 2:
                return 0.50
        return 0.0

def draw_natural_eye_blink(base_bgr, lx, ly, rx, ry, rw=6, rh=3, ratio=1.0):
    """
    Vẽ mi mắt chớp tự nhiên trực tiếp lên khuôn mặt:
    - lx, ly: tâm đồng tử mắt trái (trên ảnh)
    - rx, ry: tâm đồng tử mắt phải (trên ảnh)
    - rw, rh: bán kính hốc mắt (width/height)
    - ratio: tỷ lệ nhắm mắt (0.0 -> 1.0)
    """
    if ratio <= 0.02:
        return base_bgr
        
    res = base_bgr.copy()
    bh, bw = base_bgr.shape[:2]
    
    for cx, cy in [(int(lx), int(ly)), (int(rx), int(ry))]:
        if cx - rw < 0 or cx + rw >= bw or cy - rh < 0 or cy + rh >= bh:
            continue
            
        sample_y = min(bh - 1, max(0, cy + rh + 2))
        skin_color = base_bgr[sample_y, cx].astype(float)
        
        mask = np.zeros((bh, bw), dtype=np.uint8)
        cv2.ellipse(mask, (cx, cy), (rw, max(1, int(rh * ratio))), 0, 0, 360, 255, -1)
        
        alpha = (mask.astype(float) / 255.0) * ratio
        alpha_3d = np.repeat(alpha[:, :, None], 3, axis=2)
        res = (skin_color * alpha_3d + res.astype(float) * (1.0 - alpha_3d)).astype(np.uint8)
        
        if ratio > 0.4:
            alpha_lash = min(1.0, (ratio - 0.4) / 0.5)
            arc_y = cy + 1
            pts = np.array([
                [cx - rw, arc_y - 1],
                [cx - rw // 2, arc_y],
                [cx, arc_y + 1],
                [cx + rw // 2, arc_y],
                [cx + rw, arc_y - 1]
            ], np.int32).reshape((-1, 1, 2))
            lash_layer = res.copy()
            cv2.polylines(lash_layer, [pts], False, (40, 32, 35), 1, cv2.LINE_AA)
            res = cv2.addWeighted(lash_layer, alpha_lash, res, 1.0 - alpha_lash, 0)
            
    return res

def draw_marching_impulse_arrow(img, p0, p1, p2, p3, color_bgr, thickness, progress=1.0, frame_idx=0):
    """Vẽ mũi tên Bézier chiến thuật có xung lực tiến quân gợn sóng và vạch đứt hành quân"""
    num_steps = 60
    steps_to_draw = max(2, int(num_steps * progress))
    points = []
    for i in range(steps_to_draw + 1):
        t = i / float(num_steps)
        inv = 1.0 - t
        x = inv**3 * p0[0] + 3 * inv**2 * t * p1[0] + 3 * inv * t**2 * p2[0] + t**3 * p3[0]
        y = inv**3 * p0[1] + 3 * inv**2 * t * p1[1] + 3 * inv * t**2 * p2[1] + t**3 * p3[1]
        points.append((int(x), int(y)))
        
    pts = np.array(points, np.int32).reshape((-1, 1, 2))
    
    # 1. Phát quang mờ mềm
    glow_color = tuple(min(255, int(c * 0.35)) for c in color_bgr)
    cv2.polylines(img, [pts], False, glow_color, thickness + 8, cv2.LINE_AA)
    
    # 2. Thân mũi tên chính
    cv2.polylines(img, [pts], False, color_bgr, thickness, cv2.LINE_AA)
    
    # 3. Gói năng lượng xung lực hành quân
    pulse_t = ((frame_idx * 0.04) % 1.0) * progress
    pulse_idx = int(pulse_t * len(points))
    p_start = max(0, pulse_idx - 5)
    p_end = min(len(points), pulse_idx + 5)
    if p_end > p_start:
        impulse_pts = np.array(points[p_start:p_end], np.int32).reshape((-1, 1, 2))
        cv2.polylines(img, [impulse_pts], False, (235, 248, 255), thickness + 3, cv2.LINE_AA)
        
    # 4. Vạch đứt hành quân chuyển động
    for i in range(len(points) - 1):
        if ((i + frame_idx) // 3) % 2 == 0:
            cv2.line(img, points[i], points[i+1], (255, 255, 255), 2, cv2.LINE_AA)
            
    # 5. Đầu mũi tên nhọn
    if len(points) >= 2:
        tip = points[-1]
        prev = points[-2]
        dx, dy = tip[0] - prev[0], tip[1] - prev[1]
        angle = math.atan2(dy, dx)
        head_len = thickness * 3.0
        left = (int(tip[0] - head_len * math.cos(angle - math.pi / 6)), int(tip[1] - head_len * math.sin(angle - math.pi / 6)))
        right = (int(tip[0] - head_len * math.cos(angle + math.pi / 6)), int(tip[1] - head_len * math.sin(angle + math.pi / 6)))
        cv2.fillPoly(img, [np.array([tip, left, right], np.int32)], color_bgr, cv2.LINE_AA)

def draw_tactical_counter_with_flag(img, cx, cy, w, h, name, side_color_bgr, symbol_type="infantry", frame_idx=0, flag_id=0):
    """Vẽ khối quân bài NATO APP-6 kết hợp cờ hiệu chiến trận phấp phới trong gió"""
    x1, y1 = cx - w // 2, cy - h // 2
    x2, y2 = cx + w // 2, cy + h // 2
    cv2.rectangle(img, (x1 + 4, y1 + 4), (x2 + 4, y2 + 4), (0, 0, 0), -1)
    cv2.rectangle(img, (x1, y1), (x2, y2), (22, 24, 28), -1)
    cv2.rectangle(img, (x1 + 2, y1 + 2), (x2 - 2, y2 - 2), side_color_bgr, -1)
    cv2.rectangle(img, (x1, y1), (x2, y2), (230, 230, 230), 1, cv2.LINE_AA)
    
    if symbol_type == "infantry":
        cv2.line(img, (x1 + 6, y1 + 6), (x2 - 6, y2 - 6), (255, 255, 255), 1, cv2.LINE_AA)
        cv2.line(img, (x1 + 6, y2 - 6), (x2 - 6, y1 + 6), (255, 255, 255), 1, cv2.LINE_AA)
    elif symbol_type == "cavalry":
        cv2.line(img, (x1 + 6, y2 - 6), (x2 - 6, y1 + 6), (255, 255, 255), 1, cv2.LINE_AA)
    elif symbol_type == "artillery":
        cv2.circle(img, (cx, cy), 3, (255, 255, 255), -1, cv2.LINE_AA)
        
    flag_pole_x = x2 - 2
    flag_top_y = y1 - 22
    cv2.line(img, (flag_pole_x, y1), (flag_pole_x, flag_top_y), (180, 180, 180), 2, cv2.LINE_AA)
    
    wind = math.sin((frame_idx * 0.4) + flag_id * 1.5) * 3.5
    flag_pts = np.array([
        [flag_pole_x, flag_top_y],
        [flag_pole_x + 16, int(flag_top_y + 5 + wind)],
        [flag_pole_x, flag_top_y + 11]
    ], np.int32)
    cv2.fillPoly(img, [flag_pts], side_color_bgr, cv2.LINE_AA)
    cv2.polylines(img, [flag_pts], True, (240, 240, 240), 1, cv2.LINE_AA)

class SevenIntroEngineV2:
    def __init__(self):
        print("[INIT-V2] Initializing Seven Nguyen Intro Engine V2...")
        assets_dir = "assets/Seven Nguyen"
        raw_master = cv2.imread(os.path.join(assets_dir, "seven_nguyen_tactical_war_room_host.jpg"))
        self.war_room_master = raw_master[0:660, 120:1293]
        self.expressions_sheet = cv2.imread(os.path.join(assets_dir, "seven_nguyen_animation_facial_expressions.jpg"))
        
        # Tạo sẵn background sạch cho Scene 5 (không có Seven trong nền)
        room_bg = self.war_room_master[:, 0:500]
        bg_stretched = cv2.resize(room_bg, (WIDTH, HEIGHT))
        bg_soft = cv2.GaussianBlur(bg_stretched, (15, 15), 0)
        dark_s5 = np.full((HEIGHT, WIDTH, 3), (12, 15, 20), dtype=np.uint8)
        self.clean_scene5_bg = cv2.addWeighted(bg_soft, 0.40, dark_s5, 0.60, 0)
        
        # Sprites
        print("[INIT-V2] Loading cleaned presenter cutouts...")
        self.sprite_welcome = cv2.imread("output/final_sprite_p1.png", cv2.IMREAD_UNCHANGED)
        self.sprite_pointing_right = cv2.imread("output/final_sprite_p2.png", cv2.IMREAD_UNCHANGED)
        self.sprite_pointing_left = cv2.imread("output/final_sprite_p3.png", cv2.IMREAD_UNCHANGED)
        self.sprite_crossed = cv2.imread("output/final_sprite_p4.png", cv2.IMREAD_UNCHANGED)
        self.sprite_leaning = cv2.imread("output/final_sprite_p6.png", cv2.IMREAD_UNCHANGED)
        
        # Trích xuất 4 patch khẩu hình chuẩn pixel-perfect có alpha feathering
        print("[INIT-V2] Extracting precision lip-sync visemes with alpha feathering...")
        self.mouth_patches = {
            "closed": self.extract_tight_mouth(185, 217, rx=24, ry=12, feather=4),
            "smile":  self.extract_tight_mouth(520, 215, rx=25, ry=12, feather=4),
            "ah":     self.extract_tight_mouth(185, 595, rx=24, ry=14, feather=4),
            "oh":     self.extract_tight_mouth(520, 595, rx=20, ry=14, feather=4),
        }
        
        # Audio & Blinking Controllers
        self.audio_ctl = AudioLipSyncController("output/seven_intro_voice_en.mp3", fps=FPS, total_frames=TOTAL_FRAMES)
        self.blink_ctl = BlinkingController(total_frames=TOTAL_FRAMES)
        
        # Dust particles
        np.random.seed(42)
        self.particles = [
            {
                "x": np.random.uniform(0, WIDTH),
                "y": np.random.uniform(0, HEIGHT),
                "vx": np.random.uniform(-0.3, 0.3),
                "vy": np.random.uniform(-0.5, -0.1),
                "size": np.random.uniform(1.5, 3.5),
                "alpha": np.random.uniform(0.3, 0.8)
            }
            for _ in range(80)
        ]

    def extract_tight_mouth(self, cx, cy, rx=24, ry=14, feather=4):
        """Trích xuất patch miệng chuẩn xác từng pixel với mặt nạ elip làm mịn viền"""
        x1 = cx - rx - feather
        x2 = cx + rx + feather
        y1 = cy - ry - feather
        y2 = cy + ry + feather
        crop = self.expressions_sheet[y1:y2, x1:x2].copy()
        h, w = crop.shape[:2]
        
        Y, X = np.ogrid[:h, :w]
        dx = (X - (rx + feather)) / float(rx)
        dy = (Y - (ry + feather)) / float(ry)
        dist = np.sqrt(dx**2 + dy**2)
        
        outer = 1.0 + feather / float(min(rx, ry))
        alpha = np.zeros((h, w), dtype=np.float32)
        alpha[dist <= 1.0] = 1.0
        feather_mask = (dist > 1.0) & (dist < outer)
        t = (dist[feather_mask] - 1.0) / (outer - 1.0)
        alpha[feather_mask] = 0.5 * (1.0 + np.cos(np.pi * t))
        
        bgra = cv2.cvtColor(crop, cv2.COLOR_BGR2BGRA)
        bgra[:, :, 3] = np.clip(alpha * 255, 0, 255).astype(np.uint8)
        return bgra

    def apply_mouth_patch(self, sprite_bgr, viseme_key, cx, cy, scale=0.42):
        """Dán patch miệng bằng seamlessClone để triệt tiêu 100% viền mép và tiệp màu da"""
        patch = self.mouth_patches.get(viseme_key, self.mouth_patches["closed"])
        pw = max(4, int(patch.shape[1] * scale))
        ph = max(4, int(patch.shape[0] * scale))
        p_sc = cv2.resize(patch, (pw, ph), interpolation=cv2.INTER_AREA)
        
        patch_bgr = p_sc[:, :, :3]
        mask = (p_sc[:, :, 3] > 80).astype(np.uint8) * 255
        kernel = np.ones((3, 3), np.uint8)
        mask = cv2.erode(mask, kernel, iterations=1)
        
        bh, bw = sprite_bgr.shape[:2]
        cx_safe = min(bw - pw // 2 - 2, max(pw // 2 + 2, int(cx)))
        cy_safe = min(bh - ph // 2 - 2, max(ph // 2 + 2, int(cy)))
        
        try:
            cloned = cv2.seamlessClone(patch_bgr, sprite_bgr[:, :, :3], mask, (cx_safe, cy_safe), cv2.NORMAL_CLONE)
            if sprite_bgr.shape[2] == 4:
                return np.dstack([cloned, sprite_bgr[:, :, 3]])
            return cloned
        except Exception:
            return sprite_bgr

    def update_particles(self, img):
        """Vẽ các hạt bụi vàng lơ lửng trong chùm sáng phòng tác chiến"""
        for p in self.particles:
            p["x"] = (p["x"] + p["vx"]) % WIDTH
            p["y"] = (p["y"] + p["vy"]) % HEIGHT
            ix, iy = int(p["x"]), int(p["y"])
            rad = max(1, int(p["size"]))
            color = (int(55 * p["alpha"]), int(175 * p["alpha"]), int(212 * p["alpha"]))
            cv2.circle(img, (ix, iy), rad, color, -1, cv2.LINE_AA)

    def render_scene_1(self, frame_idx, total_scene_frames):
        """
        Scene 1 (0.0s - 4.9s):
        Welcome to The Historical Vault. I am Seven Nguyen.
        - Master shot trong phòng tác chiến với cinematic zoom in.
        - Seven Nguyen: Audio-driven lipsync, mắt chớp, đầu gật nhẹ theo nhịp thuyết minh.
        - Đầu que chỉ huy thật (368, 464) phát quang vòng ngắm chiến thuật pulsing radar ping lướt nhẹ trên bản đồ.
        """
        t = frame_idx / float(total_scene_frames)
        zoom = 1.0 + 0.05 * t
        
        h_orig, w_orig = self.war_room_master.shape[:2]
        crop_w = int(w_orig / zoom)
        crop_h = int(h_orig / zoom)
        x_start = int((w_orig - crop_w) * 0.55)
        y_start = int((h_orig - crop_h) * 0.45)
        
        wrm_animated = self.war_room_master.copy()
        
        # 1. Phát quang điểm đầu que chỉ huy thật trên sa bàn tại (368, 464)
        # Điểm ngắm quét nhẹ theo sóng sin
        tip_x_base = 368 + int(math.sin(t * 2.0 * math.pi) * 8)
        tip_y_base = 464 + int(math.cos(t * 3.0 * math.pi) * 4)
        reticle_r = int(7 + 4 * math.sin(frame_idx * 0.35))
        cv2.circle(wrm_animated, (tip_x_base, tip_y_base), reticle_r, (55, 175, 212), 1, cv2.LINE_AA)
        cv2.circle(wrm_animated, (tip_x_base, tip_y_base), 2, (230, 248, 255), -1, cv2.LINE_AA)
        
        # 2. Khẩu hình & Chớp mắt cho Seven trên ảnh Master
        global_frame_idx = frame_idx
        viseme = self.audio_ctl.get_viseme(global_frame_idx, scene_end=(frame_idx > total_scene_frames - 12))
        nod_offset = self.audio_ctl.get_head_nod_offset(global_frame_idx)
        blink_ratio = self.blink_ctl.get_blink_ratio(global_frame_idx)
        
        mouth_cy = 196 + nod_offset
        wrm_animated = self.apply_mouth_patch(wrm_animated, viseme, 836, mouth_cy, scale=0.68)
        wrm_animated = draw_natural_eye_blink(wrm_animated, 805, 142 + nod_offset, 867, 142 + nod_offset, rw=8, rh=4, ratio=blink_ratio)
        
        cropped = wrm_animated[y_start:y_start+crop_h, x_start:x_start+crop_w]
        frame = cv2.resize(cropped, (WIDTH, HEIGHT), interpolation=cv2.INTER_LINEAR)
        
        self.update_particles(frame)
        
        # Lower-third HUD Card
        hud_alpha = min(1.0, frame_idx / 20.0)
        if hud_alpha > 0:
            hud_overlay = frame.copy()
            x1, y1, x2, y2 = 80, 750, 720, 900
            cv2.rectangle(hud_overlay, (x1, y1), (x2, y2), (15, 17, 21), -1)
            cv2.addWeighted(hud_overlay, 0.85 * hud_alpha, frame, 1.0 - 0.85 * hud_alpha, 0, frame)
            
            cv2.rectangle(frame, (x1, y1), (x2, y2), (55, 175, 212), 2, cv2.LINE_AA)
            cv2.line(frame, (x1 + 10, y1 + 42), (x2 - 10, y1 + 42), (45, 120, 150), 1, cv2.LINE_AA)
            
            pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
            draw = ImageDraw.Draw(pil_img)
            font_title = get_font(32, bold=True)
            font_sub = get_font(20, bold=False)
            
            draw.text((x1 + 25, y1 + 8), "THE HISTORICAL VAULT", font=font_title, fill=(212, 175, 55))
            draw.text((x1 + 28, y1 + 52), "HOST: SEVEN NGUYEN", font=font_sub, fill=(235, 235, 235))
            draw.text((x1 + 28, y1 + 92), "WAR ROOM TACTICAL SANDBOX", font=font_sub, fill=(160, 175, 190))
            frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
            
        return frame

    def render_scene_2(self, frame_idx, total_scene_frames):
        """
        Scene 2 (4.9s - 8.5s):
        Here, history is not just dates and dust.
        - Seven Nguyen khoanh tay (sprite_crossed):
          Nhịp thở tự nhiên (breathing), đầu gật nhẹ theo trọng âm, môi cử động theo âm thanh, mắt chớp tự nhiên.
        - Các mốc niên đại lịch sử xuất hiện và bị gạch ngang bằng lằn ranh chiến thuật đỏ.
        """
        t = frame_idx / float(total_scene_frames)
        global_frame_idx = 147 + frame_idx
        
        frame = np.full((HEIGHT, WIDTH, 3), [22, 26, 32], dtype=np.uint8)
        for x in range(0, WIDTH, 120):
            cv2.line(frame, (x, 0), (x, HEIGHT), (32, 38, 46), 1, cv2.LINE_AA)
        for y in range(0, HEIGHT, 120):
            cv2.line(frame, (0, y), (WIDTH, y), (32, 38, 46), 1, cv2.LINE_AA)
            
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_big = get_font(52, bold=True)
        font_dates = get_font(42, bold=False)
        font_sub = get_font(28, bold=False)
        
        draw.text((120, 160), "BEYOND DATES & DUST", font=font_big, fill=(212, 175, 55))
        draw.text((120, 230), "RECONSTRUCTING TACTICAL REALITY", font=font_sub, fill=(180, 190, 205))
        
        dates = [
            ("216 BC", "Battle of Cannae"),
            ("1288 AD", "Battle of Bach Dang"),
            ("1815 AD", "Battle of Waterloo"),
            ("1942 AD", "Battle of Stalingrad")
        ]
        
        for idx, (yr, battle) in enumerate(dates):
            y_pos = 330 + idx * 110
            alpha_date = min(1.0, max(0.0, (t * 4.0) - idx * 0.8))
            if alpha_date > 0:
                c_val = int(220 * alpha_date)
                draw.text((140, y_pos), yr, font=font_dates, fill=(c_val, c_val, c_val))
                draw.text((360, y_pos + 6), battle, font=font_sub, fill=(int(140 * alpha_date), int(160 * alpha_date), int(180 * alpha_date)))
                
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        
        # Gạch ngang chiến thuật
        if t > 0.4:
            prog = min(1.0, (t - 0.4) / 0.5)
            end_x = int(140 + prog * 620)
            cv2.line(frame, (130, 520), (end_x, 520), (35, 45, 185), 3, cv2.LINE_AA)
            
        # Diễn hoạt Seven Nguyen (Khoanh tay)
        sprite = self.sprite_crossed.copy()
        viseme = self.audio_ctl.get_viseme(global_frame_idx)
        nod_offset = self.audio_ctl.get_head_nod_offset(global_frame_idx)
        blink_ratio = self.blink_ctl.get_blink_ratio(global_frame_idx)
        
        sprite = self.apply_mouth_patch(sprite, viseme, 95, 83 + nod_offset, scale=0.42)
        sprite_bgr = draw_natural_eye_blink(sprite[:, :, :3], 78, 56 + nod_offset, 108, 56 + nod_offset, rw=6, rh=3, ratio=blink_ratio)
        sprite[:, :, :3] = sprite_bgr
        
        # Nhịp thở tự nhiên
        breath_y = math.sin(t * 2.5 * math.pi) * 3.0
        breath_scale = 2.05 * (1.0 + 0.0025 * math.sin(t * 2.5 * math.pi))
        
        overlay_alpha_image(frame, sprite, 1360, int(330 + breath_y), scale=breath_scale, opacity=min(1.0, t * 2.5))
        self.update_particles(frame)
        return frame

    def render_scene_3(self, frame_idx, total_scene_frames):
        """
        Scene 3 (8.5s - 18.2s):
        We step directly into the war room, reconstructing the greatest battles...
        - Mũi tên tiến công có xung lực gợn sóng & vạch đứt hành quân.
        - Cọc gỗ Bạch Đằng nhấp nhô trồi lên theo triều rút kèm gợn sóng nước tỏa tròn.
        - Cờ hiệu chiến trận phấp phới trong gió.
        - Seven Nguyen (chỉ sang trái):
          Khi nhắc đến 'military masterstrokes' (t ~ 0.46 - 0.64), cánh tay và cơ thể có nhịp nhấn (tactical thrust) chỉ thẳng vào sa bàn.
        """
        t = frame_idx / float(total_scene_frames)
        global_frame_idx = 255 + frame_idx
        
        frame = np.full((HEIGHT, WIDTH, 3), [18, 22, 28], dtype=np.uint8)
        map_x1, map_y1, map_x2, map_y2 = 80, 80, 1260, 900
        cv2.rectangle(frame, (map_x1, map_y1), (map_x2, map_y2), (24, 29, 36), -1)
        
        for x in range(map_x1, map_x2, 80):
            cv2.line(frame, (x, map_y1), (x, map_y2), (32, 38, 48), 1, cv2.LINE_AA)
        for y in range(map_y1, map_y2, 80):
            cv2.line(frame, (map_x1, y), (map_x2, y), (32, 38, 48), 1, cv2.LINE_AA)
            
        center_pts = [(450, 480), (850, 520)]
        for cx, cy in center_pts:
            for r in [90, 160, 240]:
                cv2.circle(frame, (cx, cy), r, (40, 48, 58), 1, cv2.LINE_AA)
                
        # Dòng sông Bạch Đằng
        river_pts = []
        for y in range(map_y1, map_y2, 15):
            x = int(320 + 80 * math.sin(y * 0.008) + 30 * math.sin(y * 0.02))
            river_pts.append((x, y))
        cv2.polylines(frame, [np.array(river_pts, np.int32)], False, (65, 80, 70), 22, cv2.LINE_AA)
        cv2.polylines(frame, [np.array(river_pts, np.int32)], False, (100, 130, 115), 5, cv2.LINE_AA)
        
        # Bãi cọc Bạch Đằng nhấp nhô trồi lên khi triều rút
        stake_alpha = min(1.0, max(0.0, (t - 0.35) / 0.08)) if t >= 0.35 and t < 0.70 else max(0.0, 1.0 - (t - 0.70) / 0.08)
        if stake_alpha > 0.05:
            tide_ebb = 0.5 + 0.5 * math.sin(frame_idx * 0.1)
            stakes = [(320, 380), (360, 440), (330, 500), (380, 560), (350, 620), (390, 680)]
            for sx, sy in stakes:
                stake_h = int(18 + 8 * tide_ebb)
                cv2.line(frame, (sx + 2, sy + 2), (sx - 10, sy - stake_h), (12, 16, 14), 4, cv2.LINE_AA)
                cv2.line(frame, (sx, sy), (sx - 10, sy - stake_h), (35, 60, 85), 3, cv2.LINE_AA)
                cv2.circle(frame, (sx - 10, sy - stake_h), 2, (190, 200, 210), -1, cv2.LINE_AA)
                ripple_r = int((frame_idx * 1.2) % 18)
                cv2.ellipse(frame, (sx, sy), (ripple_r, max(2, ripple_r // 3)), 0, 0, 360, (110, 150, 130), 1, cv2.LINE_AA)
                
        # Mũi tên xung lực tiến quân gợn sóng
        arrow_prog = min(1.0, t * 1.5)
        draw_marching_impulse_arrow(frame, (480, 720), (320, 600), (300, 380), (520, 320), (55, 175, 212), 8, arrow_prog, frame_idx)
        draw_marching_impulse_arrow(frame, (950, 720), (1120, 600), (1140, 380), (920, 320), (55, 175, 212), 8, arrow_prog, frame_idx)
        draw_marching_impulse_arrow(frame, (720, 480), (720, 520), (720, 560), (720, 620), (45, 135, 170), 6, arrow_prog * 0.8, frame_idx)
        draw_marching_impulse_arrow(frame, (720, 300), (720, 360), (720, 420), (720, 470), (40, 45, 190), 10, arrow_prog * 0.9, frame_idx)
        
        # Khối quân bài & cờ hiệu phấp phới
        draw_tactical_counter_with_flag(frame, 380, 420, 48, 28, "Cav L", (55, 175, 212), "cavalry", frame_idx, flag_id=1)
        draw_tactical_counter_with_flag(frame, 1060, 420, 48, 28, "Cav R", (55, 175, 212), "cavalry", frame_idx, flag_id=2)
        draw_tactical_counter_with_flag(frame, 640, 620, 60, 32, "Center 1", (55, 175, 212), "infantry", frame_idx, flag_id=3)
        draw_tactical_counter_with_flag(frame, 800, 620, 60, 32, "Center 2", (55, 175, 212), "infantry", frame_idx, flag_id=4)
        
        draw_tactical_counter_with_flag(frame, 680, 460, 64, 34, "Legion I", (35, 40, 180), "infantry", frame_idx, flag_id=5)
        draw_tactical_counter_with_flag(frame, 760, 460, 64, 34, "Legion II", (35, 40, 180), "infantry", frame_idx, flag_id=6)
        draw_tactical_counter_with_flag(frame, 720, 400, 64, 34, "Legion III", (35, 40, 180), "infantry", frame_idx, flag_id=7)
        
        cv2.rectangle(frame, (map_x1, map_y1), (map_x2, map_y2), (55, 175, 212), 2, cv2.LINE_AA)
        
        battles_info = [
            (0.0, 0.35, "BATTLE OF CANNAE (216 BC)", "HANNIBAL'S TACTICAL DOUBLE ENVELOPMENT"),
            (0.35, 0.70, "BATTLE OF BACH DANG (1288)", "TRAN HUNG DAO'S TIDAL STAKE AMBUSH"),
            (0.70, 1.0, "BATTLE OF WATERLOO (1815)", "NAPOLEON VS WELLINGTON & BLUCHER")
        ]
        cur_title, cur_sub = battles_info[-1][2], battles_info[-1][3]
        for start_t, end_t, title, sub in battles_info:
            if start_t <= t < end_t:
                cur_title, cur_sub = title, sub
                break
                
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_banner = get_font(32, bold=True)
        font_sub = get_font(20, bold=False)
        
        draw.rectangle([(map_x1 + 20, map_y1 + 20), (map_x1 + 680, map_y1 + 95)], fill=(12, 14, 18))
        draw.text((map_x1 + 35, map_y1 + 26), cur_title, font=font_banner, fill=(212, 175, 55))
        draw.text((map_x1 + 38, map_y1 + 65), cur_sub, font=font_sub, fill=(220, 220, 220))
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        cv2.rectangle(frame, (map_x1 + 20, map_y1 + 20), (map_x1 + 680, map_y1 + 95), (55, 175, 212), 1, cv2.LINE_AA)
        
        # Diễn hoạt Seven Nguyen (Chỉ sang trái)
        sprite = self.sprite_pointing_left.copy()
        viseme = self.audio_ctl.get_viseme(global_frame_idx)
        nod_offset = self.audio_ctl.get_head_nod_offset(global_frame_idx)
        blink_ratio = self.blink_ctl.get_blink_ratio(global_frame_idx)
        
        sprite = self.apply_mouth_patch(sprite, viseme, 236, 88 + nod_offset, scale=0.40)
        sprite_bgr = draw_natural_eye_blink(sprite[:, :, :3], 224, 58 + nod_offset, 250, 58 + nod_offset, rw=5, rh=3, ratio=blink_ratio)
        sprite[:, :, :3] = sprite_bgr
        
        # Nhịp nhấn cánh tay (Tactical thrust) khi nói "military masterstrokes"
        thrust_x = 0.0
        if 0.46 <= t <= 0.64:
            thrust_norm = (t - 0.46) / 0.18
            thrust_x = -16.0 * math.sin(thrust_norm * math.pi)
            
        breath_y = math.sin(t * 2.5 * math.pi) * 2.5
        overlay_alpha_image(frame, sprite, int(1200 + thrust_x), int(485 + breath_y), scale=2.15, opacity=1.0)
        
        self.update_particles(frame)
        return frame

    def render_scene_4(self, frame_idx, total_scene_frames):
        """
        Scene 4 (18.2s - 24.5s):
        Analyzed move by move, casualty by casualty, right on the tactical sandbox.
        - Seven Nguyen (tựa cằm suy tư - sprite_leaning):
          Nhịp thở, ngón tay khẽ chuyển động nhẹ (micro finger tap), khẩu hình & chớp mắt.
        - Telemetry HUD: Thanh tiến trình chạy cubic easing mượt mà, số thương vong Roman đếm số nhảy sống động.
        """
        t = frame_idx / float(total_scene_frames)
        global_frame_idx = 546 + frame_idx
        
        frame = np.full((HEIGHT, WIDTH, 3), [16, 19, 24], dtype=np.uint8)
        for x in range(0, WIDTH, 100):
            cv2.line(frame, (x, 0), (x, HEIGHT), (25, 30, 38), 1, cv2.LINE_AA)
        for y in range(0, HEIGHT, 100):
            cv2.line(frame, (0, y), (WIDTH, y), (25, 30, 38), 1, cv2.LINE_AA)
            
        sprite = self.sprite_leaning.copy()
        viseme = self.audio_ctl.get_viseme(global_frame_idx)
        nod_offset = self.audio_ctl.get_head_nod_offset(global_frame_idx)
        blink_ratio = self.blink_ctl.get_blink_ratio(global_frame_idx)
        
        sprite = self.apply_mouth_patch(sprite, viseme, 114, 106 + nod_offset, scale=0.40)
        sprite_bgr = draw_natural_eye_blink(sprite[:, :, :3], 98, 76 + nod_offset, 126, 76 + nod_offset, rw=5, rh=3, ratio=blink_ratio)
        sprite[:, :, :3] = sprite_bgr
        
        breath_y = math.sin(t * 2.5 * math.pi) * 2.2
        finger_dy = math.sin(t * 7.0 * math.pi) * 1.5
        overlay_alpha_image(frame, sprite, 60, int(510 + breath_y + finger_dy * 0.4), scale=2.15, opacity=1.0)
        
        hud_x1, hud_y1, hud_x2, hud_y2 = 740, 80, 1840, 890
        cv2.rectangle(frame, (hud_x1, hud_y1), (hud_x2, hud_y2), (20, 24, 30), -1)
        cv2.rectangle(frame, (hud_x1, hud_y1), (hud_x2, hud_y2), (55, 175, 212), 2, cv2.LINE_AA)
        
        scan_y = int(hud_y1 + ((frame_idx * 6) % (hud_y2 - hud_y1)))
        cv2.line(frame, (hud_x1 + 10, scan_y), (hud_x2 - 10, scan_y), (35, 120, 150), 1, cv2.LINE_AA)
        
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_header = get_font(36, bold=True)
        font_metric = get_font(26, bold=True)
        font_val = get_font(24, bold=False)
        
        draw.text((hud_x1 + 40, hud_y1 + 35), "TACTICAL SANDBOX TELEMETRY", font=font_header, fill=(212, 175, 55))
        draw.text((hud_x1 + 40, hud_y1 + 80), "REAL-TIME TURN-BY-TURN FORCE DYNAMICS", font=font_val, fill=(160, 175, 195))
        
        p_eff = ease_cubic_out(t * 1.6)
        p_flank = ease_cubic_out(t * 1.3)
        p_cas = ease_cubic_out(t * 1.5)
        p_ret = ease_cubic_out(t * 1.7)
        
        cur_fallen = int(p_cas * 67500)
        
        metrics = [
            ("ENGAGEMENT EFFICIENCY", f"{p_eff * 94.8:.1f}%", p_eff * 0.948, False),
            ("FLANKING ENCIRCLEMENT", f"{int(p_flank * 100)}%", p_flank * 1.0, False),
            ("CASUALTY PROJECTION (ROMAN)", f"{cur_fallen:,} FALLEN", p_cas * 0.82, True),
            ("CARTHAGINIAN RETENTION", f"{p_ret * 91.2:.1f}% INTACT", p_ret * 0.912, False)
        ]
        
        for idx, (label, val_str, bar_ratio, is_alert) in enumerate(metrics):
            y_base = hud_y1 + 130 + idx * 145
            draw.text((hud_x1 + 40, y_base), label, font=font_metric, fill=(235, 235, 235))
            val_color = (235, 75, 75) if is_alert else (212, 175, 55)
            draw.text((hud_x2 - 340, y_base), val_str, font=font_metric, fill=val_color)
            
            bar_w = 960
            draw.rectangle([(hud_x1 + 40, y_base + 45), (hud_x1 + 40 + bar_w, y_base + 65)], fill=(32, 38, 48))
            fill_w = int(bar_w * bar_ratio)
            bar_fill_color = (195, 45, 45) if is_alert else (212, 175, 55)
            draw.rectangle([(hud_x1 + 40, y_base + 45), (hud_x1 + 40 + fill_w, y_base + 65)], fill=bar_fill_color)
            
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        self.update_particles(frame)
        return frame

    def render_scene_5(self, frame_idx, total_scene_frames):
        """
        Scene 5 (24.5s - 31.0s):
        Take your seat at the command table. Subscribe, and let us unlock history together.
        - Background sạch cô lập hoàn toàn (không có bóng Seven trong nền).
        - Seven Nguyen (toàn thân chào đón - sprite_welcome):
          Hai cánh tay mở rộng nhẹ nhàng theo nhịp thở mời gọi người xem Subscribe.
          Khẩu hình nói năng động và kết thúc bằng nụ cười ấm áp (mouth_smile).
        - Subscribe CTA card nhấp nháy phát quang thu hút.
        """
        t = frame_idx / float(total_scene_frames)
        global_frame_idx = 735 + frame_idx
        
        # Nền phòng tác chiến sạch cô lập không bóng hình
        frame = self.clean_scene5_bg.copy()
        
        # Diễn hoạt Seven Nguyen Welcoming
        sprite = self.sprite_welcome.copy()
        is_closing = (t > 0.70)
        viseme = self.audio_ctl.get_viseme(global_frame_idx, scene_end=is_closing)
        nod_offset = self.audio_ctl.get_head_nod_offset(global_frame_idx)
        blink_ratio = self.blink_ctl.get_blink_ratio(global_frame_idx)
        
        sprite = self.apply_mouth_patch(sprite, viseme, 180, 72 + nod_offset, scale=0.38)
        sprite_bgr = draw_natural_eye_blink(sprite[:, :, :3], 168, 48 + nod_offset, 192, 48 + nod_offset, rw=5, rh=3, ratio=blink_ratio)
        sprite[:, :, :3] = sprite_bgr
        
        breath_y = math.sin(t * 2.5 * math.pi) * 2.5
        arm_scale = 1.35 * (1.0 + 0.005 * math.sin(t * 2.5 * math.pi))
        overlay_alpha_image(frame, sprite, 120, int(140 + breath_y), scale=arm_scale, opacity=1.0)
        
        card_x1, card_y1, card_x2, card_y2 = 820, 130, 1800, 860
        card_overlay = frame.copy()
        cv2.rectangle(card_overlay, (card_x1, card_y1), (card_x2, card_y2), (14, 16, 22), -1)
        cv2.addWeighted(card_overlay, 0.88, frame, 0.12, 0, frame)
        cv2.rectangle(frame, (card_x1, card_y1), (card_x2, card_y2), (55, 175, 212), 2, cv2.LINE_AA)
        
        pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        font_vault = get_font(52, bold=True)
        font_sub = get_font(28, bold=False)
        font_btn = get_font(34, bold=True)
        
        draw.text((card_x1 + 60, card_y1 + 50), "THE HISTORICAL VAULT", font=font_vault, fill=(212, 175, 55))
        draw.text((card_x1 + 65, card_y1 + 125), "TAKE YOUR SEAT AT THE COMMAND TABLE", font=font_sub, fill=(225, 225, 225))
        draw.text((card_x1 + 65, card_y1 + 175), "NEW 2D TACTICAL BATTLE RECONSTRUCTIONS WEEKLY", font=font_sub, fill=(160, 175, 195))
        
        btn_x1, btn_y1, btn_x2, btn_y2 = card_x1 + 65, card_y1 + 265, card_x1 + 520, card_y1 + 365
        pulse = 0.5 + 0.5 * math.sin(t * 12.0)
        btn_color = (int(190 + 35 * pulse), int(35 + 20 * pulse), int(35 + 20 * pulse))
        draw.rounded_rectangle([(btn_x1, btn_y1), (btn_x2, btn_y2)], radius=14, fill=btn_color)
        draw.text((btn_x1 + 60, btn_y1 + 28), "SUBSCRIBE", font=font_btn, fill=(255, 255, 255))
        
        draw.text((card_x1 + 65, card_y1 + 415), "Presented by Seven Nguyen | Historical 2D Studio", font=font_sub, fill=(170, 185, 200))
        frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
        
        if t > 0.85:
            fade_black = (t - 0.85) / 0.15
            frame = (frame * (1.0 - fade_black)).astype(np.uint8)
            
        self.update_particles(frame)
        return frame

    def render_full_video(self, output_raw_mp4):
        """Render toàn bộ các frame video qua FFmpeg Hardware Pipe (NVENC)"""
        print(f"[RENDER-V2] Rendering {TOTAL_FRAMES} frames ({WIDTH}x{HEIGHT} @ {FPS}fps)...")
        start_time = time.time()
        
        s1_end = int(4.9 * FPS)
        s2_end = int(8.5 * FPS)
        s3_end = int(18.2 * FPS)
        s4_end = int(24.5 * FPS)
        s5_end = TOTAL_FRAMES
        
        ffmpeg_cmd = [
            "ffmpeg", "-y",
            "-f", "rawvideo",
            "-vcodec", "rawvideo",
            "-s", f"{WIDTH}x{HEIGHT}",
            "-pix_fmt", "bgr24",
            "-r", str(FPS),
            "-i", "-",
            "-c:v", "h264_nvenc",
            "-preset", "p4",
            "-cq", "19",
            "-bf", "0",
            "-pix_fmt", "yuv420p",
            output_raw_mp4
        ]
        
        try:
            proc = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE, stderr=subprocess.DEVNULL)
        except Exception:
            print("[WARN] NVENC hardware encoder unavailable, falling back to libx264...")
            ffmpeg_cmd[ffmpeg_cmd.index("h264_nvenc")] = "libx264"
            ffmpeg_cmd.remove("-bf")
            ffmpeg_cmd.remove("0")
            ffmpeg_cmd.pop(ffmpeg_cmd.index("-cq"))
            ffmpeg_cmd.remove("19")
            proc = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE, stderr=subprocess.DEVNULL)
            
        for i in range(TOTAL_FRAMES):
            if i < s1_end:
                frame = self.render_scene_1(i, s1_end)
            elif i < s2_end:
                frame = self.render_scene_2(i - s1_end, s2_end - s1_end)
            elif i < s3_end:
                frame = self.render_scene_3(i - s2_end, s3_end - s2_end)
            elif i < s4_end:
                frame = self.render_scene_4(i - s3_end, s4_end - s3_end)
            else:
                frame = self.render_scene_5(i - s4_end, s5_end - s4_end)
                
            proc.stdin.write(frame.tobytes())
            
            if i % 90 == 0 or i == TOTAL_FRAMES - 1:
                elapsed = time.time() - start_time
                fps_curr = (i + 1) / max(0.001, elapsed)
                pct = ((i + 1) / float(TOTAL_FRAMES)) * 100
                print(f"  [RENDER-V2] Frame {i+1}/{TOTAL_FRAMES} ({pct:.1f}%) | Speed: {fps_curr:.1f} FPS", flush=True)
                
        proc.stdin.close()
        proc.wait()
        total_time = time.time() - start_time
        avg_fps = TOTAL_FRAMES / max(0.001, total_time)
        print(f"[OK-V2] Render finished in {total_time:.2f}s (Average Speed: {avg_fps:.1f} FPS). Raw video: {output_raw_mp4}")
        return output_raw_mp4, avg_fps

def finalize_master_video_v2(raw_video_mp4, voice_audio_mp3, ambience_audio_wav, ass_subtitles_path, final_master_mp4):
    """Hòa trộn video, âm thanh và nhúng kinetic ASS chuẩn EBU R128 (-14 LUFS)"""
    print("[FINAL-V2] Muxing master video with audio tracks and burning kinetic ASS subtitles...")
    escaped_ass = ass_subtitles_path.replace("\\", "/").replace(":", "\\:")
    
    cmd = [
        "ffmpeg", "-y",
        "-i", raw_video_mp4,
        "-i", voice_audio_mp3,
        "-i", ambience_audio_wav,
        "-filter_complex",
        f"[0:v]subtitles='{escaped_ass}'[v_sub];"
        f"[1:a]volume=1.0[v_voice];"
        f"[2:a]volume=0.30[v_bgm];"
        f"[v_voice][v_bgm]amix=inputs=2:duration=first[a_out]",
        "-map", "[v_sub]",
        "-map", "[a_out]",
        "-c:v", "h264_nvenc",
        "-preset", "p4",
        "-cq", "19",
        "-bf", "0",
        "-c:a", "aac",
        "-b:a", "192k",
        "-movflags", "+faststart",
        "-shortest",
        final_master_mp4
    ]
    
    try:
        subprocess.run(cmd, check=True)
    except Exception as e:
        print(f"[WARN] Error with hardware subtitle burn, falling back to CPU: {e}")
        cmd[cmd.index("h264_nvenc")] = "libx264"
        cmd.remove("-bf")
        cmd.remove("0")
        cmd.pop(cmd.index("-cq"))
        cmd.remove("19")
        subprocess.run(cmd, check=True)
        
    print(f"[SUCCESS-V2] Master Video V2 saved at: {final_master_mp4}")
    return final_master_mp4

if __name__ == "__main__":
    os.makedirs("output", exist_ok=True)
    raw_video = "output/seven_intro_video_v2_raw.mkv"
    voice_audio = "output/seven_intro_voice_en.mp3"
    ambience_audio = "output/seven_intro_war_room_ambience.wav"
    ass_subs = "output/seven_intro_kinetic.ass"
    master_video = "output/the_historical_vault_intro_master_v2.mp4"
    
    engine = SevenIntroEngineV2()
    _, avg_fps = engine.render_full_video(raw_video)
    finalize_master_video_v2(raw_video, voice_audio, ambience_audio, ass_subs, master_video)
