#!/usr/bin/env python3
"""
Professional News Broadcast Generator (1080p)
Generates a high-quality news broadcast video with:
- 1920x1080 Full HD resolution
- Professional news UI (gradient backgrounds, particles, glow)
- Animated AI avatar (hair, eyes, blinking, mouth sync, suit)
- Per-segment color themes
- Scrolling ticker, BREAKING badge, bullet points
- High-quality ffmpeg encoding (slow preset, CRF 18)

Usage:
  python3 gen_broadcast_video.py
  
Outputs:
  /root/news-broadcast-hd.mp4

Prerequisites:
  apt-get install -y ffmpeg python3-pil python3-numpy
"""

import os, sys, math, random, subprocess, shutil
import numpy as np
from PIL import Image, ImageDraw, ImageFont

# === CONFIG ===
WIDTH, HEIGHT = 1920, 1080
FPS = 30
OUTPUT_DIR = "/root/news-video-hd"
FINAL_VIDEO = "/root/news-broadcast-hd.mp4"
AUDIO_FILE = "/root/news-audio-hd.mp3"

os.makedirs(OUTPUT_DIR, exist_ok=True)

FONT_PATH = "/usr/share/fonts/truetype/dejavu/"

def font(size, bold=False):
    name = "DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf"
    try:
        return ImageFont.truetype(FONT_PATH + name, size)
    except:
        return ImageFont.load_default()

def gradient(c1, c2):
    img = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8)
    for y in range(HEIGHT):
        r = y / HEIGHT
        img[y, :] = [int(c1[i]*(1-r) + c2[i]*r) for i in range(3)]
    return Image.fromarray(img)

def draw_rounded_rect(draw, xy, radius, fill, outline=None, width=1):
    draw.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width)

def word_wrap(text, fnt, max_width):
    words = text.split()
    lines, cur = [], ""
    for w in words:
        test = cur + " " + w if cur else w
        try:
            bbox = fnt.getbbox(test)
            tw = bbox[2] - bbox[0]
        except:
            tw = len(test) * fnt.size * 0.6
        if tw < max_width:
            cur = test
        else:
            if cur: lines.append(cur)
            cur = w
    if cur: lines.append(cur)
    return lines

# === SEGMENTS (customize per broadcast) ===
SEGMENTS = [
    {
        "title": "HANTAVIRUS OUTBREAK",
        "subtitle": "Cruise Ship Evacuation",
        "bullet": [
            "American passengers evacuated from hantavirus-hit cruise ship in Tenerife",
            "One passenger tested positive during evacuation flight to U.S.",
            "Another passenger developed symptoms mid-flight",
            "Evacuees routed through Nebraska monitoring facility",
        ],
        "ticker": "BREAKING: Hantavirus outbreak | Passengers evacuated to US",
        "duration": 20,
        "accent": (220, 50, 50),
        "bg_dark": (15, 5, 10),
        "bg_light": (40, 15, 15),
    },
    {
        "title": "UK POLITICAL CRISIS",
        "subtitle": "Starmer Faces Leadership Challenge",
        "bullet": [
            "PM Keir Starmer faces growing leadership challenge from Labour MPs",
            "Local election losses trigger calls for resignation",
            "Labour MP threatens to trigger challenge by Monday",
            "Starmer vows to fight on, promises bolder action",
        ],
        "ticker": "UK POLITICS: Starmer under pressure | Leadership challenge looms",
        "duration": 18,
        "accent": (40, 80, 200),
        "bg_dark": (5, 10, 30),
        "bg_light": (15, 20, 50),
    },
    {
        "title": "NINTENDO SWITCH 2",
        "subtitle": "Price Hike Amid Memory Shortage",
        "bullet": [
            "Nintendo raises Switch 2 prices due to rising component costs",
            "Global memory shortage impacts console production",
            "Company expects declining console sales this year",
            "Nintendo shares tumble in Tokyo trading",
        ],
        "ticker": "BUSINESS: Nintendo Switch 2 price increase | Memory shortage hits gaming",
        "duration": 16,
        "accent": (220, 40, 40),
        "bg_dark": (20, 5, 5),
        "bg_light": (50, 15, 10),
    },
    {
        "title": "GOOGLE FITBIT AIR",
        "subtitle": "Screenless Fitness Tracker Launches",
        "bullet": [
            "Google launches Fitbit Air, a screenless fitness tracker",
            "Ultra-compact design with gesture controls",
            "Features status light and double-tap interaction",
            "Positioned as minimalist alternative to traditional bands",
        ],
        "ticker": "TECH: Google Fitbit Air launch | Screenless design | Gesture controls",
        "duration": 14,
        "accent": (40, 180, 80),
        "bg_dark": (5, 20, 10),
        "bg_light": (10, 40, 20),
    },
    {
        "title": "AI CODE CONTROVERSY",
        "subtitle": "Open Source Developers Push Back",
        "bullet": [
            "PlayStation emulator developers beg users to stop AI code spam",
            "Flood of low-quality AI-generated code overwhelming projects",
            "Developers say AI code creates more work than it saves",
            "Highlights growing tension between AI tools and open source",
        ],
        "ticker": "TECH: AI code spam frustrates developers | Open source projects overwhelmed",
        "duration": 14,
        "accent": (120, 60, 200),
        "bg_dark": (10, 5, 25),
        "bg_light": (25, 10, 50),
    },
]

INTRO_DUR = 5
OUTRO_DUR = 5


def draw_news_frame(seg, frame_num, total_frames):
    accent = tuple(seg["accent"])
    bg = gradient(seg["bg_dark"], seg["bg_light"])
    draw = ImageDraw.Draw(bg, "RGBA")
    progress = frame_num / total_frames if total_frames > 0 else 0

    # Top header bar
    for y in range(0, 70):
        alpha = int(200 * (1 - y / 70 * 0.3))
        draw.line([(0, y), (WIDTH, y)], fill=(0, 0, 0, alpha))
    for x in range(0, 6):
        ratio = x / 6
        c = tuple(int(accent[i] * (1 - ratio * 0.6)) for i in range(3))
        draw.line([(x, 0), (x, 70)], fill=c + (255,))

    # LIVE indicator with pulse
    pulse = abs(math.sin(frame_num * 0.15)) * 0.5 + 0.5
    live_r = int(200 + 55 * pulse)
    for r in range(14, 8, -1):
        a = int(80 * (1 - (r - 8) / 6))
        draw.ellipse([22 - r, 22 - r, 22 + r, 22 + r], fill=(live_r, 30, 30, a))
    draw.ellipse([14, 14, 30, 30], fill=(live_r, 30, 30))
    draw.ellipse([17, 17, 27, 27], fill=(255, 255, 255))
    draw.text((40, 18), "LIVE", fill=(255, 255, 255, 230), font=font(20, True))
    draw.text((WIDTH - 380, 18), "WORLD NEWS NETWORK", fill=(200, 200, 200, 200), font=font(18))
    draw.text((WIDTH - 380, 40), "MAY 11, 2026", fill=(140, 140, 140, 180), font=font(14))
    draw.text((WIDTH - 120, 28), "05:30 UTC", fill=(180, 180, 180, 200), font=font(16))

    # Left accent bar
    for x in range(0, 8):
        ratio = x / 8
        c = tuple(int(accent[i] * (1 - ratio * 0.7)) for i in range(3))
        draw.line([(x, 70), (x, HEIGHT)], fill=c + (200,))

    # Title panel with slide-in
    slide_offset = max(0, int(50 * (1 - min(1, progress * 5))))
    panel_x = 30 - slide_offset
    draw_rounded_rect(draw, [panel_x, 90, panel_x + 900, 200], 8, (0, 0, 0, 120))
    for x in range(panel_x, panel_x + 6):
        ratio = (x - panel_x) / 6
        c = tuple(int(accent[i] * (1 - ratio * 0.5)) for i in range(3))
        draw.line([(x, 90), (x, 200)], fill=c + (220,))
    draw.text((panel_x + 20, 100), seg["title"], fill=(255, 255, 255, 240), font=font(38, True))
    draw.text((panel_x + 20, 150), seg["subtitle"], fill=accent + (220,), font=font(22))

    # Bullet points with staggered animation
    bullet_y = 220
    for i, bullet in enumerate(seg["bullet"]):
        item_progress = max(0, min(1, (progress - 0.1 - i * 0.08) * 4))
        if item_progress <= 0:
            continue
        bx = int(50 + 30 * (1 - item_progress))
        alpha = int(220 * item_progress)
        draw.ellipse([bx, bullet_y + 8, bx + 10, bullet_y + 18], fill=accent + (alpha,))
        lines = word_wrap(bullet, font(22), 750)
        for j, line in enumerate(lines[:2]):
            draw.text((bx + 20, bullet_y + j * 30), line, fill=(210, 210, 220, alpha), font=font(22))
        bullet_y += 65

    # Avatar panel
    ax, ay = WIDTH - 340, 100
    aw, ah = 300, 380
    for y in range(ay, ay + ah):
        ratio = (y - ay) / ah
        r = int(20 + ratio * 15)
        g = int(25 + ratio * 10)
        b = int(40 + ratio * 20)
        draw.line([(ax, y), (ax + aw, y)], fill=(r, g, b, 200))
    draw_rounded_rect(draw, [ax, ay, ax + aw, ay + ah], 12, (0, 0, 0, 0), outline=accent + (180,), width=3)

    cx, cy = ax + aw // 2, ay + 140
    draw.ellipse([cx - 60, cy - 70, cx + 60, cy + 50], fill=(255, 225, 190), outline=(210, 185, 150), width=2)
    draw.arc([cx - 62, cy - 85, cx + 62, cy + 10], 180, 360, fill=(60, 40, 25), width=25)
    draw.ellipse([cx - 55, cy - 80, cx + 55, cy - 40], fill=(60, 40, 25))
    blink = abs(math.sin(frame_num * 0.04)) > 0.97
    eye_h = 3 if blink else 10
    draw.ellipse([cx - 28, cy - 20, cx - 10, cy - 5], fill=(255, 255, 255))
    draw.ellipse([cx + 10, cy - 20, cx + 28, cy - 5], fill=(255, 255, 255))
    draw.ellipse([cx - 24, cy - 18, cx - 14, cy - 8], fill=(60, 90, 140))
    draw.ellipse([cx + 14, cy - 18, cx + 24, cy - 8], fill=(60, 90, 140))
    draw.ellipse([cx - 21, cy - 16, cx - 17, cy - 10], fill=(20, 20, 30))
    draw.ellipse([cx + 17, cy - 16, cx + 21, cy - 10], fill=(20, 20, 30))
    draw.ellipse([cx - 22, cy - 17, cx - 20, cy - 14], fill=(255, 255, 255))
    draw.ellipse([cx + 18, cy - 17, cx + 20, cy - 14], fill=(255, 255, 255))
    draw.arc([cx - 30, cy - 32, cx - 8, cy - 18], 200, 340, fill=(60, 40, 25), width=3)
    draw.arc([cx + 8, cy - 32, cx + 30, cy - 18], 200, 340, fill=(60, 40, 25), width=3)
    draw.arc([cx - 5, cy - 5, cx + 5, cy + 15], 0, 180, fill=(210, 180, 150), width=2)
    mouth_open = abs(math.sin(frame_num * 0.25)) * 6
    if mouth_open > 2:
        draw.ellipse([cx - 15, cy + 22, cx + 15, cy + 30 + int(mouth_open)], fill=(180, 80, 80))
    else:
        draw.arc([cx - 12, cy + 20, cx + 12, cy + 32], 0, 180, fill=(180, 100, 100), width=2)
    draw.rectangle([cx - 20, cy + 48, cx + 20, cy + 75], fill=(255, 225, 190))
    draw.ellipse([cx - 70, cy + 65, cx + 70, cy + 130], fill=(35, 45, 80))
    draw.polygon([(cx - 25, cy + 70), (cx, cy + 95), (cx + 25, cy + 70)], fill=(240, 240, 245))
    draw.polygon([(cx - 6, cy + 85), (cx, cy + 95), (cx + 6, cy + 85), (cx + 3, cy + 120), (cx - 3, cy + 120)], fill=accent)
    draw.text((ax + 80, ay + ah - 50), "AI NEWS ANCHOR", fill=(180, 180, 200, 200), font=font(18, True))
    draw.text((ax + 90, ay + ah - 25), "World News Network", fill=(130, 130, 150, 180), font=font(14))

    # Bottom ticker
    ticker_y = HEIGHT - 90
    for y in range(ticker_y, HEIGHT):
        ratio = (y - ticker_y) / (HEIGHT - ticker_y)
        a = int(180 + 40 * ratio)
        draw.line([(0, y), (WIDTH, y)], fill=(0, 0, 0, a))
    for x in range(0, 4):
        ratio = x / 4
        c = tuple(int(accent[i] * (1 - ratio * 0.5)) for i in range(3))
        draw.line([(0, ticker_y + x), (WIDTH, ticker_y + x)], fill=c + (230,))
    if (frame_num // 12) % 2 == 0:
        draw_rounded_rect(draw, [20, ticker_y + 12, 130, ticker_y + 38], 4, accent + (220,))
        draw.text((30, ticker_y + 14), "BREAKING", fill=(255, 255, 255, 240), font=font(16, True))
    ticker_text = "  •  ".join([seg["ticker"]] * 3)
    try:
        tw = font(18).getbbox(ticker_text)[2]
    except:
        tw = len(ticker_text) * 12
    scroll_x = WIDTH - (frame_num * 4) % (int(tw) + WIDTH)
    draw.text((scroll_x, ticker_y + 45), ticker_text, fill=(255, 255, 200, 220), font=font(18))

    # Particles
    random.seed(42)
    for i in range(20):
        px = (frame_num * 2 + i * 97) % WIDTH
        py = (frame_num * 1 + i * 53) % (HEIGHT - 200) + 100
        size = 1 + (i % 3)
        alpha = int(30 + 20 * math.sin(frame_num * 0.05 + i))
        draw.ellipse([px - size, py - size, px + size, py + size], fill=accent + (alpha,))

    return bg


def draw_intro_frame(frame_num, total_frames):
    progress = frame_num / total_frames
    bg = gradient((3, 5, 20), (10, 20, 50))
    draw = ImageDraw.Draw(bg, "RGBA")
    for ring in range(3):
        angle_offset = frame_num * 0.015 + ring * 2.1
        rx = 220 + ring * 40
        ry = 70 + ring * 15
        for i in range(40):
            a = i * 0.157 + angle_offset
            x = WIDTH // 2 + int(math.cos(a) * rx)
            y = HEIGHT // 2 - 30 + int(math.sin(a) * ry)
            alpha = int(100 + 80 * math.sin(a * 2 + frame_num * 0.03))
            draw.ellipse([x - 2, y - 2, x + 2, y + 2], fill=(60, 130, 220, alpha))
    for i in range(12):
        a = frame_num * 0.012 + i * 0.524
        r = 180 + math.sin(frame_num * 0.02 + i) * 40
        x = WIDTH // 2 + int(math.cos(a) * r)
        y = HEIGHT // 2 - 30 + int(math.sin(a) * r * 0.35)
        draw.ellipse([x - 4, y - 4, x + 4, y + 4], fill=(80, 150, 255, 150))
    title_y = HEIGHT // 2 - 100
    for offset in range(8, 0, -1):
        ga = int(30 * (1 - offset / 8))
        draw.text((WIDTH // 2 - 340, title_y), "WORLD NEWS BRIEF", font=font(62, True), fill=(40, 80, 180, ga))
    draw.text((WIDTH // 2 - 340, title_y), "WORLD NEWS BRIEF", font=font(62, True), fill=(255, 255, 255, 240))
    sub_alpha = min(255, int(max(0, (progress - 0.3)) * 510))
    draw.text((WIDTH // 2 - 160, title_y + 80), "May 11, 2026", font=font(32), fill=(180, 200, 255, sub_alpha))
    bar_w, bar_x, bar_y = 300, (WIDTH - 300) // 2, HEIGHT // 2 + 140
    draw_rounded_rect(draw, [bar_x, bar_y, bar_x + bar_w, bar_y + 6], 3, (40, 40, 60, 150))
    fill_w = int(bar_w * min(1, progress * 1.5))
    if fill_w > 0:
        draw_rounded_rect(draw, [bar_x, bar_y, bar_x + fill_w, bar_y + 6], 3, (60, 130, 220, 200))
    for x in range(0, 5):
        draw.line([(0, HEIGHT - 5 + x), (WIDTH, HEIGHT - 5 + x)], fill=(60, 130, 220, 200 - x * 40))
    return bg


def draw_outro_frame(frame_num, total_frames):
    progress = frame_num / total_frames
    bg = gradient((3, 5, 20), (10, 20, 50))
    draw = ImageDraw.Draw(bg, "RGBA")
    alpha = min(255, int(progress * 300))
    draw.text((WIDTH // 2 - 280, HEIGHT // 2 - 100), "Thank You For Watching", font=font(52, True), fill=(255, 255, 255, alpha))
    draw.text((WIDTH // 2 - 180, HEIGHT // 2 - 20), "Stay Informed. Stay Safe.", font=font(28), fill=(180, 200, 255, alpha))
    draw.text((WIDTH // 2 - 160, HEIGHT // 2 + 50), "Subscribe for daily updates", font=font(22), fill=(100, 180, 255, alpha))
    for i in range(6):
        a = frame_num * 0.04 + i * 1.05
        x = WIDTH // 2 + int(math.cos(a) * 120)
        y = HEIGHT // 2 + 130 + int(math.sin(a) * 25)
        draw.ellipse([x - 4, y - 4, x + 4, y + 4], fill=(80, 150, 255, 150))
    for x in range(0, 5):
        draw.line([(0, HEIGHT - 5 + x), (WIDTH, HEIGHT - 5 + x)], fill=(60, 130, 220, 200 - x * 40))
    return bg


def main():
    print(f"=== Professional News Broadcast Generator ===")
    print(f"Resolution: {WIDTH}x{HEIGHT}, FPS: {FPS}")

    total_intro = INTRO_DUR * FPS
    total_outro = OUTRO_DUR * FPS
    total_seg = sum(s["duration"] * FPS for s in SEGMENTS)
    total = total_intro + total_seg + total_outro
    print(f"Total frames: {total}")

    frame_count = 0

    print("\n[1/3] Generating intro...")
    for i in range(total_intro):
        draw_intro_frame(i, total_intro).save(f"{OUTPUT_DIR}/frame_{frame_count:06d}.png", optimize=False)
        frame_count += 1
        if i % 15 == 0:
            print(f"  {i}/{total_intro}")

    print("\n[2/3] Generating news segments...")
    for si, seg in enumerate(SEGMENTS):
        sf = seg["duration"] * FPS
        print(f"  [{si+1}/{len(SEGMENTS)}] {seg['title']} ({sf} frames)")
        for i in range(sf):
            draw_news_frame(seg, i, sf).save(f"{OUTPUT_DIR}/frame_{frame_count:06d}.png", optimize=False)
            frame_count += 1
            if i % 30 == 0:
                print(f"    {i}/{sf}")

    print("\n[3/3] Generating outro...")
    for i in range(total_outro):
        draw_outro_frame(i, total_outro).save(f"{OUTPUT_DIR}/frame_{frame_count:06d}.png", optimize=False)
        frame_count += 1

    print(f"\nGenerated {frame_count} frames")

    print("\nEncoding video (10+ min for 1080p slow preset)...")
    video_tmp = OUTPUT_DIR + "_tmp.mp4"
    r = subprocess.run([
        "ffmpeg", "-y", "-framerate", str(FPS),
        "-i", f"{OUTPUT_DIR}/frame_%06d.png",
        "-c:v", "libx264", "-preset", "slow", "-crf", "18",
        "-pix_fmt", "yuv420p", "-movflags", "+faststart",
        video_tmp
    ], capture_output=True, text=True)
    if r.returncode != 0:
        print(f"FFmpeg error: {r.stderr[-800:]}")
        sys.exit(1)

    print("Adding audio...")
    r = subprocess.run([
        "ffmpeg", "-y",
        "-i", video_tmp, "-i", AUDIO_FILE,
        "-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
        "-shortest", FINAL_VIDEO
    ], capture_output=True, text=True)
    if r.returncode != 0:
        print(f"FFmpeg error: {r.stderr[-800:]}")
        sys.exit(1)

    probe = subprocess.run(
        ["ffprobe", "-v", "quiet", "-show_entries", "format=duration,size,bit_rate",
         "-of", "default=noprint_wrappers=1:nokey=1", FINAL_VIDEO],
        capture_output=True, text=True
    )
    lines = probe.stdout.strip().split("\n")
    dur = float(lines[0]) if lines else 0
    sz = int(lines[1]) if len(lines) > 1 else 0
    br = int(lines[2]) if len(lines) > 2 else 0

    print(f"\n=== COMPLETE ===")
    print(f"  File: {FINAL_VIDEO}")
    print(f"  Resolution: {WIDTH}x{HEIGHT}")
    print(f"  Duration: {dur:.1f}s | Size: {sz/1024/1024:.1f} MB | Bitrate: {br/1000:.0f} kbps")

    shutil.rmtree(OUTPUT_DIR)
    if os.path.exists(video_tmp):
        os.remove(video_tmp)
    print("Cleanup done!")


if __name__ == "__main__":
    main()
