# video_generator.py

import json
import os
from PIL import Image, ImageDraw, ImageFont
import cv2
import numpy as np

# Define your paths here
JSON_PATH = 'video_data.json'      # Path to your JSON file
OUTPUT_DIR = 'output_videos'
VIDEO_OUTPUT = os.path.join(OUTPUT_DIR, 'final_video.mp4')
DEFAULT_FONT_PATH = '/root/mm/vidtext/opensansvar.ttf'  # Ensure this path is correct

# Video specifications
FRAME_WIDTH = 1024
FRAME_HEIGHT = 1024
FPS = 24
VIDEO_CODEC = 'mp4v'  # FourCC code for mp4

# Scene timings in seconds
FADE_IN_DURATION = 1
DISPLAY_DURATION = 5
TEXT_ANIMATION_DURATION = 3
FADE_OUT_DURATION = 1

# Calculate number of frames for each segment
FADE_IN_FRAMES = FADE_IN_DURATION * FPS
DISPLAY_FRAMES = DISPLAY_DURATION * FPS
TEXT_ANIMATION_FRAMES = TEXT_ANIMATION_DURATION * FPS
FADE_OUT_FRAMES = FADE_OUT_DURATION * FPS

# Total frames per scene
TOTAL_SCENE_FRAMES = FADE_IN_FRAMES + DISPLAY_FRAMES + TEXT_ANIMATION_FRAMES + FADE_OUT_FRAMES

def load_scenes(json_path):
    """
    Load scenes from a JSON file.
    """
    try:
        with open(json_path, 'r') as f:
            scenes = json.load(f)
        if not isinstance(scenes, list):
            raise ValueError("JSON data must be a list of scenes.")
        return scenes
    except Exception as e:
        print(f"Error loading JSON: {e}")
        return []

def create_base_image(scene):
    """
    Create the base image for a scene with background color.
    """
    background_color = scene.get("background_color", "black")
    img = Image.new('RGB', (FRAME_WIDTH, FRAME_HEIGHT), color=background_color)
    return img

def load_font(scene):
    """
    Load the specified font or default font.
    """
    try:
        font_path = scene.get("font_path", DEFAULT_FONT_PATH)
        font_size = scene.get("font_size", 50)
        font = ImageFont.truetype(font_path, size=font_size)
    except Exception as e:
        print(f"Error loading font '{scene.get('font_path', DEFAULT_FONT_PATH)}': {e}. Using default font.")
        try:
            font = ImageFont.truetype(DEFAULT_FONT_PATH, size=50)
        except Exception as e:
            print(f"Failed to load default font. {e}")
            font = ImageFont.load_default()
    return font

def add_text(image, text, font, font_color, visible_chars=None):
    """
    Add text to an image. If visible_chars is specified, only that many characters are visible.
    """
    draw = ImageDraw.Draw(image)
    text_width, text_height = draw.textsize(text, font=font)
    position = ((FRAME_WIDTH - text_width) // 2, (FRAME_HEIGHT - text_height) // 2)
    
    if visible_chars is not None:
        text_to_draw = text[:visible_chars]
    else:
        text_to_draw = text
    
    draw.text(position, text_to_draw, font=font, fill=font_color)
    return image

def blend_images(img1, img2, alpha):
    """
    Blend two PIL images with the given alpha for img2.
    """
    return Image.blend(img1, img2, alpha)

def pil_to_cv2(image):
    """
    Convert PIL Image to OpenCV image (BGR).
    """
    return cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)

def generate_fade_frames(base_img, fade_type='in'):
    """
    Generate fade in or fade out frames.
    """
    frames = []
    black_img = Image.new('RGB', (FRAME_WIDTH, FRAME_HEIGHT), color="black")
    
    total_frames = FADE_IN_FRAMES if fade_type == 'in' else FADE_OUT_FRAMES
    
    for i in range(total_frames):
        if fade_type == 'in':
            alpha = (i + 1) / total_frames
            blended = blend_images(black_img, base_img, alpha)
        else:
            alpha = 1 - (i + 1) / total_frames
            blended = blend_images(base_img, black_img, alpha)
        cv_frame = pil_to_cv2(blended)
        frames.append(cv_frame)
    
    return frames

def generate_display_frames(base_img):
    """
    Generate static display frames.
    """
    cv_frame = pil_to_cv2(base_img)
    return [cv_frame] * DISPLAY_FRAMES

def generate_text_animation_frames(base_img, text, font, font_color, animation_type):
    """
    Generate frames with text animation effects.
    Supported animations: Typing Effect, Fade In, Slide In, Zoom In
    """
    frames = []
    total_chars = len(text)
    chars_per_frame = max(total_chars // TEXT_ANIMATION_FRAMES, 1)

    if animation_type == "Typing Effect":
        for i in range(TEXT_ANIMATION_FRAMES):
            visible_chars = min((i + 1) * chars_per_frame, total_chars)
            animated_img = base_img.copy()
            add_text(animated_img, text, font, font_color, visible_chars=visible_chars)
            cv_frame = pil_to_cv2(animated_img)
            frames.append(cv_frame)
        # Ensure the entire text is visible in the last frame
        while len(frames) < TEXT_ANIMATION_FRAMES:
            animated_img = base_img.copy()
            add_text(animated_img, text, font, font_color)
            cv_frame = pil_to_cv2(animated_img)
            frames.append(cv_frame)
    
    elif animation_type == "Fade In":
        # Start with transparent text and gradually increase opacity
        for i in range(TEXT_ANIMATION_FRAMES):
            animated_img = base_img.copy()
            alpha = (i + 1) / TEXT_ANIMATION_FRAMES
            # Create a transparent overlay
            overlay = Image.new('RGBA', animated_img.size, (0,0,0,0))
            draw = ImageDraw.Draw(overlay)
            text_width, text_height = draw.textsize(text, font=font)
            position = ((FRAME_WIDTH - text_width) // 2, (FRAME_HEIGHT - text_height) // 2)
            draw.text(position, text, font=font, fill=(*ImageColor.getrgb(font_color), int(255 * alpha)))
            animated_img = Image.alpha_composite(animated_img.convert('RGBA'), overlay)
            cv_frame = pil_to_cv2(animated_img.convert('RGB'))
            frames.append(cv_frame)
    
    elif animation_type == "Slide In":
        # Slide text from left to center
        for i in range(TEXT_ANIMATION_FRAMES):
            animated_img = base_img.copy()
            draw = ImageDraw.Draw(animated_img)
            text_width, text_height = draw.textsize(text, font=font)
            total_slide = FRAME_WIDTH // 2 - text_width // 2
            current_slide = int(total_slide * (i + 1) / TEXT_ANIMATION_FRAMES)
            position = (current_slide, (FRAME_HEIGHT - text_height) // 2)
            draw.text(position, text, font=font, fill=font_color)
            cv_frame = pil_to_cv2(animated_img)
            frames.append(cv_frame)
    
    elif animation_type == "Zoom In":
        # Start with smaller text size and increase to final size
        initial_size = max(int(font.size * 0.5), 10)
        for i in range(TEXT_ANIMATION_FRAMES):
            temp_font_size = initial_size + int((font.size - initial_size) * (i + 1) / TEXT_ANIMATION_FRAMES)
            temp_font = ImageFont.truetype(scene.get("font_path", DEFAULT_FONT_PATH), size=temp_font_size)
            animated_img = base_img.copy()
            add_text(animated_img, text, temp_font, font_color)
            cv_frame = pil_to_cv2(animated_img)
            frames.append(cv_frame)
    
    else:
        # Default to Typing Effect if unknown animation
        for i in range(TEXT_ANIMATION_FRAMES):
            visible_chars = min((i + 1) * chars_per_frame, total_chars)
            animated_img = base_img.copy()
            add_text(animated_img, text, font, font_color, visible_chars=visible_chars)
            cv_frame = pil_to_cv2(animated_img)
            frames.append(cv_frame)
    
    return frames

def generate_scene_frames(scene):
    """
    Generate all frames for a single scene including transitions and animations.
    """
    frames = []
    base_img = create_base_image(scene)
    font = load_font(scene)
    text = scene.get("text", "")
    font_color = scene.get("font_color", "white")
    text_animation = scene.get("text_animation", "Typing Effect")
    
    # Fade In
    fade_in_frames = generate_fade_frames(base_img, fade_type='in')
    frames.extend(fade_in_frames)
    
    # Display
    display_frames = generate_display_frames(base_img)
    frames.extend(display_frames)
    
    # Text Animation
    text_animation_frames = generate_text_animation_frames(base_img, text, font, font_color, text_animation)
    frames.extend(text_animation_frames)
    
    # Fade Out
    fade_out_frames = generate_fade_frames(base_img, fade_type='out')
    frames.extend(fade_out_frames)
    
    return frames

def generate_video(scenes):
    """
    Generate the video from the list of scenes.
    """
    # Ensure output directory exists
    if not os.path.exists(OUTPUT_DIR):
        os.makedirs(OUTPUT_DIR)
    
    # Define the codec and create VideoWriter object
    fourcc = cv2.VideoWriter_fourcc(*VIDEO_CODEC)
    out = cv2.VideoWriter(VIDEO_OUTPUT, fourcc, FPS, (FRAME_WIDTH, FRAME_HEIGHT))
    
    for idx, scene in enumerate(scenes):
        print(f"Processing scene {idx + 1}/{len(scenes)}: '{scene.get('text', '')}'")
        scene_frames = generate_scene_frames(scene)
        for frame in scene_frames:
            out.write(frame)
    
    # Release everything if job is finished
    out.release()
    print(f"\nVideo generated successfully at: {VIDEO_OUTPUT}")

def main():
    scenes = load_scenes(JSON_PATH)
    if not scenes:
        print("No scenes to process. Please run the setup script first to create 'video_data.json'.")
        return
    generate_video(scenes)

if __name__ == "__main__":
    main()
(venv) ➜  vidfinal python vg.py
Processing scene 1/3: 'My Company'
Traceback (most recent call last):
  File "/root/mm/vidfinal/vg.py", line 263, in <module>
    main()
  File "/root/mm/vidfinal/vg.py", line 260, in main
    generate_video(scenes)
  File "/root/mm/vidfinal/vg.py", line 247, in generate_video
    scene_frames = generate_scene_frames(scene)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/mm/vidfinal/vg.py", line 224, in generate_scene_frames
    text_animation_frames = generate_text_animation_frames(base_img, text, font, font_color, text_animation)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/root/mm/vidfinal/vg.py", line 174, in generate_text_animation_frames
    text_width, text_height = draw.textsize(text, font=font)
                              ^^^^^^^^^^^^^
AttributeError: 'ImageDraw' object has no attribute 'textsize'