import os import sys import json import time import logging import tempfile from pathlib import Path from typing import Optional from datetime import datetime import requests import gradio as gr from PIL import Image, ImageDraw, ImageFont import numpy as np # Backend API URL BACKEND_URL = os.environ.get("BACKEND_URL", "http://localhost:8081") logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s') logger = logging.getLogger("LegionFrontend") # --- Dark Theme CSS --- LEGION_CSS = """ :root { --primary: #ff6b35; --primary-dark: #e55a2b; --bg-dark: #0a0a0f; --bg-card: #14141f; --bg-input: #1a1a2e; --text-primary: #e0e0e0; --text-secondary: #8888aa; --border-color: #2a2a3e; --accent: #00d4aa; } body { background-color: var(--bg-dark) !important; color: var(--text-primary) !important; } .gradio-container { background-color: var(--bg-dark) !important; max-width: 1200px !important; margin: 0 auto !important; padding: 20px !important; } h1, h2, h3 { font-family: 'Courier New', monospace !important; text-transform: uppercase !important; letter-spacing: 2px !important; } h1 { color: var(--primary) !important; text-align: center !important; font-size: 2.5em !important; text-shadow: 0 0 20px rgba(255, 107, 53, 0.3) !important; border-bottom: 2px solid var(--border-color) !important; padding-bottom: 15px !important; margin-bottom: 25px !important; } .tabs { background: var(--bg-card) !important; border: 1px solid var(--border-color) !important; border-radius: 12px !important; padding: 15px !important; } button { background: linear-gradient(135deg, var(--primary), var(--primary-dark)) !important; border: none !important; color: white !important; font-weight: bold !important; letter-spacing: 1px !important; transition: all 0.3s ease !important; } button:hover { transform: translateY(-2px) !important; box-shadow: 0 5px 20px rgba(255, 107, 53, 0.4) !important; } input, textarea, select, .dropdown { background: var(--bg-input) !important; border-radius: 8px !important; } .slider input[type="range"] { accent-color: var(--primary) !important; } .video-container { overflow: hidden !important; } .status-badge { background: var(--accent) !important; color: black !important; padding: 4px 12px !important; border-radius: 20px !important; font-size: 0.8em !important; } .footer { color: var(--text-secondary) !important; border-top: 1px solid var(--border-color) !important; margin-top: 30px !important; } """ # ============================================================ # API Helper Functions # ============================================================ def call_api_status() -> dict: try: resp = requests.get(f"{BACKEND_URL}/api/status", timeout=5) return resp.json() except Exception as e: return {"status": "error", "detail": str(e)} def call_api_models() -> list: try: resp = requests.get(f"{BACKEND_URL}/api/models", timeout=5) data = resp.json() return data.get("models", []) except Exception as e: return [{"name": f"Error: {e}", "type": "error"}] # ============================================================ # Generation Functions # ============================================================ def generate_text_video( prompt: str, negative_prompt: str, num_frames: int, width: int, height: int, inference_steps: int, guidance_scale: float, watermark_strength: float, progress=gr.Progress(), ) -> Optional[str]: try: progress(0, desc="Connecting to API...") payload = { "prompt": prompt, "negative_prompt": negative_prompt, "num_frames": num_frames, "width": width, "height": height, "num_inference_steps": inference_steps, "guidance_scale": guidance_scale, "watermark_strength": watermark_strength, } progress(0.2, desc="Generating video...") resp = requests.post( f"{BACKEND_URL}/api/generate/text", json=payload, timeout=600, ) if resp.status_code != 200: return f"Error: {resp.text}" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"/tmp/legion_t2v_{timestamp}.mp4" with open(output_path, "wb") as f: f.write(resp.content) progress(1.0, desc="Done!") return output_path except Exception as e: return f"Error: {str(e)}" def generate_image_video( image_file, prompt: str, negative_prompt: str, num_frames: int, width: int, height: int, inference_steps: int, guidance_scale: float, watermark_strength: float, progress=gr.Progress(), ) -> Optional[str]: try: if image_file is None: return "Error: No image uploaded" progress(0.2, desc="Uploading image and generating...") with open(image_file, "rb") as f: files = {"file": f} data = { "prompt": prompt, "negative_prompt": negative_prompt, "num_frames": str(num_frames), "width": str(width), "height": str(height), "num_inference_steps": str(inference_steps), "guidance_scale": str(guidance_scale), "watermark_strength": str(watermark_strength), } resp = requests.post( f"{BACKEND_URL}/api/generate/image", files=files, data=data, timeout=600, ) if resp.status_code != 200: return f"Error: {resp.text}" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"/tmp/legion_i2v_{timestamp}.mp4" with open(output_path, "wb") as f: f.write(resp.content) progress(1.0, desc="Done!") return output_path except Exception as e: return f"Error: {str(e)}" def preview_watermark(text: str, position: str, font_size: int, opacity: float) -> Optional[str]: try: frame = Image.new("RGB", (480, 480), (20, 20, 35)) draw = ImageDraw.Draw(frame) for i in range(0, 480, 20): draw.line([(i, 0), (i, 480)], fill=(30, 30, 50), width=1) draw.line([(0, i), (480, i)], fill=(30, 30, 50), width=1) try: font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size) except: font = ImageFont.load_default() w, h = frame.size bbox = draw.textbbox((0, 0), text, font=font) text_w = bbox[2] - bbox[0] text_h = bbox[3] - bbox[1] margin = 15 pos_map = { "top-left": (margin, margin), "top-right": (w - text_w - margin, margin), "bottom-left": (margin, h - text_h - margin), "center": ((w - text_w) // 2, (h - text_h) // 2), "bottom-right": (w - text_w - margin, h - text_h - margin), } x, y = pos_map.get(position, pos_map["bottom-right"]) draw.rectangle( [x - 10, y - 10, x + text_w + 10, y + text_h + 10], fill=(0, 0, 0, int(40 * opacity)) ) draw.text((x, y), text, font=font, fill=(255, 255, 255, int(255 * opacity))) preview_path = "/tmp/qwatermark_preview.png" frame.save(preview_path) return preview_path except Exception as e: return None # ============================================================ # BUILD GRADIO INTERFACE # ============================================================ with gr.Blocks( css=LEGION_CSS, title="LEGION VIDEO GENERATION", theme=gr.themes.Soft( primary_hue="orange", secondary_hue="blue", neutral_hue="slate", ), ) as app: # Header gr.HTML("""

⚔️ LEGION VIDEO GENERATION

The Ultimate AI Video Engine — Text-to-Video & Image-to-Video

🔥 T2V + I2V
""") # Main tabs with gr.Tabs(elem_classes="tabs"): # ============ TAB 1: TEXT-TO-VIDEO ============ with gr.TabItem("🎬 Text-to-Video", id="tab_t2v"): with gr.Row(): with gr.Column(scale=2): prompt_t2v = gr.Textbox( label="📝 Prompt", placeholder="Describe the video you want to generate...", lines=4, value="A serene mountain lake at sunset with colorful clouds reflecting on the water, gentle ripples, cinematic quality", ) neg_prompt_t2v = gr.Textbox( label="🚫 Negative Prompt", lines=2, value="warped, distorted, flickering, jittery, low quality, blurry, artifacts, ugly, deformed, bad anatomy, bad proportions", ) num_frames_t2v = gr.Slider( label="Frames", minimum=1, maximum=129, value=49, step=1 ) steps_t2v = gr.Slider( label="Inference Steps", minimum=10, maximum=100, value=50, step=1 ) guidance_t2v = gr.Slider( label="Guidance Scale", minimum=1.0, maximum=20.0, value=6.0, step=0.5 ) wm_strength_t2v = gr.Slider( label="QWatermark Strength", minimum=0.0, maximum=1.0, value=0.3, step=0.05 ) width_t2v = gr.Dropdown( label="Width", choices=[256, 384, 480, 720], value=480 ) height_t2v = gr.Dropdown( label="Height", choices=[256, 384, 480, 720], value=480 ) gen_btn_t2v = gr.Button("🛡️ GENERATE VIDEO", variant="primary", size="lg") with gr.Column(scale=1): output_t2v = gr.Video(label="Generated Video", show_label=True) status_t2v = gr.Textbox(label="Status", interactive=False) gen_btn_t2v.click( fn=generate_text_video, inputs=[ prompt_t2v, neg_prompt_t2v, num_frames_t2v, width_t2v, height_t2v, steps_t2v, guidance_t2v, wm_strength_t2v, ], outputs=[output_t2v], queue=True, ) # ============ TAB 2: IMAGE-TO-VIDEO ============ with gr.TabItem("🖼️ Image-to-Video", id="tab_i2v"): with gr.Row(): with gr.Column(scale=2): image_i2v = gr.Image( label="📷 Input Image", type="filepath", height=300, ) prompt_i2v = gr.Textbox( label="📝 Prompt (motion description)", placeholder="Describe the motion or action...", lines=3, value="Gentle motion, cinematic camera movement, atmospheric", ) neg_prompt_i2v = gr.Textbox( label="🚫 Negative Prompt", lines=2, value="warped, distorted, flickering, jittery, low quality, blurry, artifacts", ) num_frames_i2v = gr.Slider( label="Frames", minimum=1, maximum=129, value=49, step=1 ) steps_i2v = gr.Slider( label="Inference Steps", minimum=10, maximum=100, value=50, step=1 ) guidance_i2v = gr.Slider( label="Guidance Scale", minimum=1.0, maximum=20.0, value=6.0, step=0.5 ) wm_strength_i2v = gr.Slider( label="QWatermark Strength", minimum=0.0, maximum=1.0, value=0.3, step=0.05 ) width_i2v = gr.Dropdown( label="Width", choices=[256, 384, 480, 720], value=480 ) height_i2v = gr.Dropdown( label="Height", choices=[256, 384, 480, 720], value=480 ) gen_btn_i2v = gr.Button("🛡️ GENERATE VIDEO", variant="primary", size="lg") with gr.Column(scale=1): output_i2v = gr.Video(label="Generated Video", show_label=True) status_i2v = gr.Textbox(label="Status", interactive=False) gen_btn_i2v.click( fn=generate_image_video, inputs=[ image_i2v, prompt_i2v, neg_prompt_i2v, num_frames_i2v, width_i2v, height_i2v, steps_i2v, guidance_i2v, wm_strength_i2v, ], outputs=[output_i2v], queue=True, ) # ============ TAB 3: QWATERMARK SETTINGS ============ with gr.TabItem("💧 QWatermark Settings", id="tab_wm"): gr.Markdown(""" ## 💧 QWatermark Quality Assurance System The QWatermark system imprints a semi-transparent quality assurance marker on every generated video. Configure the watermark appearance below. """) wm_text = gr.Textbox( label="Watermark Text", value="LEGION", placeholder="Enter watermark text", ) wm_position = gr.Dropdown( label="Position", choices=["top-left", "top-right", "bottom-left", "bottom-right", "center"], value="bottom-right", ) wm_font_size = gr.Slider( label="Font Size", minimum=16, maximum=72, value=36, step=2 ) wm_opacity = gr.Slider( label="Opacity", minimum=0.0, maximum=1.0, value=0.3, step=0.05 ) preview_btn = gr.Button("👁️ PREVIEW WATERMARK", variant="secondary", size="lg") wm_preview = gr.Image( label="Watermark Preview", height=400, ) wm_info = gr.Markdown(""" **Current Configuration:** - Text: LEGION - Position: bottom-right - System: Semi-transparent overlay with dark background The QWatermark is applied to every frame during video export. Adjust strength in generation tabs (0.0 to disable). """) preview_btn.click( fn=preview_watermark, inputs=[wm_text, wm_position, wm_font_size, wm_opacity], outputs=[wm_preview], ) # Footer gr.HTML(""" """) # ============================================================ # Launch # ============================================================ if __name__ == "__main__": port = int(os.environ.get("FRONTEND_PORT", 8080)) logger.info(f"Starting LEGION Frontend on port {port}") app.launch( server_name="0.0.0.0", server_port=port, share=False, show_error=True, quiet=False, )