Spaces:
Running
Running
| """ | |
| Vidsimplify Streamlit Application | |
| Supports two modes: | |
| 1. DIRECT MODE (for Hugging Face) - directly executes generation code | |
| 2. API MODE (for local development) - uses the FastAPI server | |
| Set DIRECT_MODE=true in environment to use direct execution. | |
| """ | |
| import streamlit as st | |
| import os | |
| import time | |
| import base64 | |
| import uuid | |
| import subprocess | |
| import asyncio | |
| import threading | |
| import shutil | |
| import logging | |
| from pathlib import Path | |
| from datetime import datetime | |
| # Setup logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Load environment variables from .env file | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| # Check if we're in direct mode (for Hugging Face deployment) | |
| DIRECT_MODE = os.getenv("DIRECT_MODE", "true").lower() == "true" | |
| # ============================================================================ | |
| # Background Cleanup Task (runs every 30 minutes) | |
| # ============================================================================ | |
| def cleanup_old_files(): | |
| """ | |
| Background task that runs every 10 minutes to delete files older than 10 minutes. | |
| This prevents running out of the 1GB storage limit on Hugging Face Spaces. | |
| """ | |
| import time | |
| while True: | |
| try: | |
| time.sleep(600) # Wait 10 minutes (600 seconds) | |
| cutoff_time = time.time() - 600 # 10 minutes ago | |
| deleted_count = 0 | |
| freed_mb = 0 | |
| logger.info("π§Ή Starting periodic cleanup (files older than 10 minutes)...") | |
| # Clean up old video files | |
| media_videos = Path("media/videos") | |
| if media_videos.exists(): | |
| for scene_dir in media_videos.iterdir(): | |
| if scene_dir.is_dir() and scene_dir.name.startswith("scene_"): | |
| # Check if directory is older than 10 minutes | |
| if scene_dir.stat().st_mtime < cutoff_time: | |
| dir_size = sum(f.stat().st_size for f in scene_dir.rglob('*') if f.is_file()) | |
| shutil.rmtree(scene_dir) | |
| deleted_count += 1 | |
| freed_mb += dir_size / (1024 * 1024) | |
| logger.info(f" ποΈ Deleted old scene: {scene_dir.name}") | |
| # Clean up old voiceover files (keep newest 50, delete older ones) | |
| for vo_dir in [Path("media/voiceover/edge_tts"), Path("media/voiceover/elevenlabs"), Path("media/voiceover/gtts")]: | |
| if vo_dir.exists(): | |
| voice_files = sorted(vo_dir.glob("*.mp3"), key=lambda x: x.stat().st_mtime, reverse=True) | |
| # Keep newest 50, delete the rest | |
| for old_file in voice_files[50:]: | |
| try: | |
| file_size = old_file.stat().st_size | |
| old_file.unlink() | |
| freed_mb += file_size / (1024 * 1024) | |
| logger.info(f" ποΈ Deleted old voiceover: {old_file.name}") | |
| except: | |
| pass | |
| # Log disk usage | |
| total, used, free = shutil.disk_usage("/") | |
| free_mb = free // (1024 * 1024) | |
| logger.info(f"β Cleanup complete: Deleted {deleted_count} scenes, freed {freed_mb:.1f}MB") | |
| logger.info(f"πΎ Disk space: {free_mb}MB free of {total // (1024 * 1024)}MB") | |
| except Exception as e: | |
| logger.error(f"β Cleanup error: {e}") | |
| # Start cleanup thread in DIRECT_MODE (Hugging Face) | |
| if DIRECT_MODE: | |
| cleanup_thread = threading.Thread(target=cleanup_old_files, daemon=True) | |
| cleanup_thread.start() | |
| logger.info("π§Ή Background cleanup task started (runs every 10 minutes)") | |
| # Page config | |
| st.set_page_config( | |
| page_title="Vidsimplify - AI Video Generator", | |
| page_icon="π¬", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # Custom CSS - Premium Techy Design | |
| st.markdown(""" | |
| <style> | |
| /* ======================================== | |
| HIDE STREAMLIT DEFAULT ELEMENTS | |
| ======================================== */ | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| header {visibility: hidden;} | |
| /* Hide the deploy button and hamburger menu */ | |
| .stDeployButton {display: none;} | |
| [data-testid="stToolbar"] {display: none;} | |
| [data-testid="stDecoration"] {display: none;} | |
| [data-testid="stStatusWidget"] {display: none;} | |
| /* Remove top padding */ | |
| .block-container { | |
| padding-top: 1rem !important; | |
| } | |
| /* ======================================== | |
| MAIN APP - CYBER DARK THEME | |
| ======================================== */ | |
| .stApp { | |
| background: linear-gradient(135deg, #0a0a1a 0%, #0f0f2d 50%, #0a0a1a 100%); | |
| color: #e0e0ff; | |
| } | |
| /* Subtle animated background grid */ | |
| .stApp::before { | |
| content: ""; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: | |
| linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| pointer-events: none; | |
| z-index: -1; | |
| } | |
| /* All text should be light */ | |
| .stApp p, .stApp span, .stApp label, .stApp div { | |
| color: #e0e0ff !important; | |
| } | |
| /* ======================================== | |
| SIDEBAR - GLASSMORPHISM STYLE | |
| ======================================== */ | |
| [data-testid="stSidebar"] { | |
| background: linear-gradient(180deg, rgba(20, 20, 45, 0.95) 0%, rgba(15, 15, 35, 0.98) 100%) !important; | |
| backdrop-filter: blur(20px); | |
| border-right: 1px solid rgba(99, 102, 241, 0.2); | |
| } | |
| [data-testid="stSidebar"] * { | |
| color: #e0e0ff !important; | |
| } | |
| /* Sidebar title styling */ | |
| [data-testid="stSidebar"] h1 { | |
| background: linear-gradient(90deg, #818cf8 0%, #c084fc 50%, #f472b6 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-weight: 700 !important; | |
| } | |
| /* Mode indicator - glowing badge */ | |
| [data-testid="stSidebar"] .stSuccess { | |
| background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(52, 211, 153, 0.1) 100%) !important; | |
| border: 1px solid rgba(16, 185, 129, 0.4) !important; | |
| border-radius: 12px !important; | |
| box-shadow: 0 0 20px rgba(16, 185, 129, 0.15); | |
| } | |
| [data-testid="stSidebar"] .stInfo { | |
| background: linear-gradient(135deg, rgba(99, 102, 241, 0.15) 0%, rgba(139, 92, 246, 0.1) 100%) !important; | |
| border: 1px solid rgba(99, 102, 241, 0.4) !important; | |
| border-radius: 12px !important; | |
| box-shadow: 0 0 20px rgba(99, 102, 241, 0.15); | |
| } | |
| /* Section headers in sidebar */ | |
| [data-testid="stSidebar"] .stMarkdown h3 { | |
| color: #a5b4fc !important; | |
| font-size: 0.85rem !important; | |
| text-transform: uppercase; | |
| letter-spacing: 1.5px; | |
| margin-top: 1.5rem !important; | |
| } | |
| /* ======================================== | |
| BUTTONS - NEON GLOW STYLE | |
| ======================================== */ | |
| .stButton>button { | |
| background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%); | |
| color: white !important; | |
| border-radius: 12px; | |
| padding: 14px 32px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| border: none; | |
| transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .stButton>button:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 30px rgba(139, 92, 246, 0.5); | |
| } | |
| .stButton>button:active { | |
| transform: translateY(-1px); | |
| } | |
| /* Download button - cyan neon */ | |
| .stDownloadButton>button { | |
| background: linear-gradient(135deg, #06b6d4 0%, #22d3ee 50%, #67e8f9 100%) !important; | |
| color: #0a0a1a !important; | |
| font-weight: 700 !important; | |
| border-radius: 12px; | |
| padding: 14px 32px; | |
| border: none; | |
| box-shadow: 0 4px 20px rgba(6, 182, 212, 0.4); | |
| } | |
| .stDownloadButton>button:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 30px rgba(34, 211, 238, 0.5); | |
| } | |
| /* ======================================== | |
| INPUT FIELDS - GLASS CARDS | |
| ======================================== */ | |
| .stTextInput>div>div>input, | |
| .stTextArea>div>div>textarea { | |
| background: rgba(20, 20, 50, 0.6) !important; | |
| color: #ffffff !important; | |
| border: 1px solid rgba(99, 102, 241, 0.3) !important; | |
| border-radius: 12px; | |
| backdrop-filter: blur(10px); | |
| transition: all 0.3s ease; | |
| } | |
| .stTextInput>div>div>input:focus, | |
| .stTextArea>div>div>textarea:focus { | |
| border-color: #8b5cf6 !important; | |
| box-shadow: 0 0 20px rgba(139, 92, 246, 0.3) !important; | |
| } | |
| .stTextInput>div>div>input::placeholder, | |
| .stTextArea>div>div>textarea::placeholder { | |
| color: #6b7280 !important; | |
| } | |
| /* ======================================== | |
| SELECTBOX / DROPDOWN | |
| ======================================== */ | |
| .stSelectbox>div>div { | |
| background: rgba(20, 20, 50, 0.6) !important; | |
| border: 1px solid rgba(99, 102, 241, 0.3) !important; | |
| border-radius: 10px; | |
| } | |
| .stSelectbox label { | |
| color: #a5b4fc !important; | |
| font-weight: 500; | |
| } | |
| /* ======================================== | |
| TABS - PILL STYLE | |
| ======================================== */ | |
| .stTabs [data-baseweb="tab-list"] { | |
| gap: 8px; | |
| background: transparent; | |
| padding: 4px; | |
| } | |
| .stTabs [data-baseweb="tab"] { | |
| background: rgba(20, 20, 50, 0.6); | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| border-radius: 10px; | |
| color: #a5b4fc !important; | |
| padding: 12px 24px; | |
| transition: all 0.3s ease; | |
| } | |
| .stTabs [data-baseweb="tab"]:hover { | |
| background: rgba(99, 102, 241, 0.1); | |
| border-color: rgba(99, 102, 241, 0.4); | |
| } | |
| .stTabs [aria-selected="true"] { | |
| background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; | |
| border-color: transparent !important; | |
| color: white !important; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); | |
| } | |
| /* ======================================== | |
| HEADERS - GRADIENT TEXT | |
| ======================================== */ | |
| h1 { | |
| background: linear-gradient(90deg, #818cf8 0%, #c084fc 50%, #f472b6 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-weight: 800 !important; | |
| font-size: 2.5rem !important; | |
| } | |
| h2, h3, h4, h5, h6 { | |
| color: #e0e0ff !important; | |
| } | |
| /* ======================================== | |
| STATUS BOXES - GLOWING CARDS | |
| ======================================== */ | |
| .status-box { | |
| padding: 1.25rem; | |
| border-radius: 16px; | |
| margin: 1rem 0; | |
| color: #ffffff !important; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid; | |
| } | |
| .status-generating { | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(99, 102, 241, 0.1) 100%); | |
| border-color: rgba(59, 130, 246, 0.4); | |
| box-shadow: 0 0 25px rgba(59, 130, 246, 0.15); | |
| } | |
| .status-rendering { | |
| background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(251, 191, 36, 0.1) 100%); | |
| border-color: rgba(245, 158, 11, 0.4); | |
| box-shadow: 0 0 25px rgba(245, 158, 11, 0.15); | |
| } | |
| .status-complete { | |
| background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(52, 211, 153, 0.1) 100%); | |
| border-color: rgba(16, 185, 129, 0.4); | |
| box-shadow: 0 0 25px rgba(16, 185, 129, 0.15); | |
| } | |
| .status-error { | |
| background: linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(248, 113, 113, 0.1) 100%); | |
| border-color: rgba(239, 68, 68, 0.4); | |
| box-shadow: 0 0 25px rgba(239, 68, 68, 0.15); | |
| } | |
| /* ======================================== | |
| PROGRESS BAR - ANIMATED GRADIENT | |
| ======================================== */ | |
| .stProgress > div > div > div > div { | |
| background: linear-gradient(90deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%); | |
| border-radius: 10px; | |
| } | |
| .stProgress > div > div > div { | |
| background: rgba(99, 102, 241, 0.1); | |
| border-radius: 10px; | |
| } | |
| /* ======================================== | |
| FILE UPLOADER | |
| ======================================== */ | |
| [data-testid="stFileUploader"] { | |
| background: rgba(20, 20, 50, 0.6); | |
| border: 1px dashed rgba(99, 102, 241, 0.4); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| transition: all 0.3s ease; | |
| } | |
| [data-testid="stFileUploader"]:hover { | |
| border-color: #8b5cf6; | |
| box-shadow: 0 0 20px rgba(139, 92, 246, 0.2); | |
| } | |
| [data-testid="stFileUploader"] label { | |
| color: #a5b4fc !important; | |
| } | |
| /* ======================================== | |
| VIDEO PLAYER - PREMIUM FRAME | |
| ======================================== */ | |
| video { | |
| border-radius: 16px; | |
| box-shadow: | |
| 0 20px 50px rgba(0, 0, 0, 0.5), | |
| 0 0 0 1px rgba(99, 102, 241, 0.2); | |
| } | |
| /* ======================================== | |
| ALERTS - GLASS STYLE | |
| ======================================== */ | |
| .stAlert { | |
| background: rgba(20, 20, 50, 0.8) !important; | |
| border: 1px solid rgba(99, 102, 241, 0.3) !important; | |
| border-radius: 12px; | |
| backdrop-filter: blur(10px); | |
| } | |
| /* ======================================== | |
| DIVIDERS | |
| ======================================== */ | |
| hr { | |
| border-color: rgba(99, 102, 241, 0.2) !important; | |
| } | |
| /* ======================================== | |
| SCROLLBAR - MINIMAL STYLE | |
| ======================================== */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(20, 20, 50, 0.3); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: linear-gradient(180deg, #818cf8 0%, #a855f7 100%); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ============================================================================ | |
| # Direct Mode Functions (No API needed) | |
| # ============================================================================ | |
| def generate_video_direct(input_type: str, input_data: str, quality: str, category: str, progress_callback=None): | |
| """ | |
| Generate video directly without API. | |
| Used for Hugging Face deployment. | |
| """ | |
| from manimator.api.animation_generation import generate_animation_response | |
| from manimator.utils.code_fixer import CodeFixer | |
| job_id = str(uuid.uuid4()) | |
| base_dir = Path(__file__).parent | |
| # Quality flags for Manim | |
| quality_flags = { | |
| "low": "-pql", | |
| "medium": "-pqm", | |
| "high": "-pqh", | |
| "ultra": "-pqk" | |
| } | |
| quality_dirs = { | |
| "low": "480p15", | |
| "medium": "720p30", | |
| "high": "1080p60", | |
| "ultra": "2160p60" | |
| } | |
| try: | |
| # Stage 1: Generate code | |
| if progress_callback: | |
| progress_callback("generating_code", 20, "Generating Manim code with AI...") | |
| code = generate_animation_response( | |
| input_data=input_data, | |
| input_type=input_type, | |
| category=category, | |
| job_id=job_id | |
| ) | |
| # Save code to file | |
| scene_name = f"Scene_{uuid.uuid4().hex[:8]}" | |
| code_file = base_dir / f"scene_{job_id}.py" | |
| # Replace class name in code | |
| import re | |
| code = re.sub(r'class\s+\w+\s*\(\s*VoiceoverScene\s*\)', f'class {scene_name}(VoiceoverScene)', code) | |
| with open(code_file, 'w') as f: | |
| f.write(code) | |
| if progress_callback: | |
| progress_callback("code_generated", 40, "Code generated successfully!") | |
| # Stage 2: Render video with retry loop | |
| fixer = CodeFixer() | |
| max_retries = 3 | |
| video_path = None | |
| # Ensure media directories exist | |
| media_dir = base_dir / "media" | |
| (media_dir / "voiceover" / "elevenlabs").mkdir(parents=True, exist_ok=True) | |
| (media_dir / "voiceover" / "gtts").mkdir(parents=True, exist_ok=True) | |
| (media_dir / "voiceover" / "edge_tts").mkdir(parents=True, exist_ok=True) | |
| (media_dir / "videos").mkdir(parents=True, exist_ok=True) | |
| for attempt in range(max_retries): | |
| try: | |
| if progress_callback: | |
| progress_callback("rendering", 50 + (attempt * 10), f"Rendering video (attempt {attempt + 1}/{max_retries})...") | |
| # Run Manim | |
| cmd = [ | |
| "manim", | |
| quality_flags[quality], | |
| "--media_dir", str(media_dir), | |
| str(code_file), | |
| scene_name | |
| ] | |
| env = os.environ.copy() | |
| env["MEDIA_DIR"] = str(media_dir.resolve()) | |
| result = subprocess.run( | |
| cmd, | |
| capture_output=True, | |
| text=True, | |
| cwd=str(base_dir), | |
| env=env, | |
| timeout=300 # 5 minute timeout | |
| ) | |
| if result.returncode != 0: | |
| error_msg = result.stderr[-500:] if result.stderr else "Unknown render error" | |
| raise Exception(f"Manim rendering failed: {error_msg}") | |
| # Find video file | |
| video_dir = media_dir / "videos" / code_file.stem / quality_dirs[quality] | |
| video_files = list(video_dir.glob("*.mp4")) | |
| if not video_files: | |
| raise Exception(f"No video file found in {video_dir}") | |
| video_path = video_files[0] | |
| break # Success! | |
| except Exception as e: | |
| if attempt < max_retries - 1: | |
| # Try to fix code | |
| if progress_callback: | |
| progress_callback("fixing", 50 + (attempt * 10), "Fixing rendering error...") | |
| with open(code_file, 'r') as f: | |
| current_code = f.read() | |
| fixed_code = fixer.fix_runtime_error(current_code, str(e)) | |
| with open(code_file, 'w') as f: | |
| f.write(fixed_code) | |
| else: | |
| raise e | |
| # Cleanup code file | |
| try: | |
| code_file.unlink() | |
| except: | |
| pass | |
| if progress_callback: | |
| progress_callback("completed", 100, "Video generation completed!") | |
| return video_path | |
| except Exception as e: | |
| if progress_callback: | |
| progress_callback("failed", 0, f"Error: {str(e)}") | |
| raise e | |
| def generate_via_api(api_url: str, input_type: str, input_data: str, quality: str, category: str, progress_callback=None): | |
| """ | |
| Generate video via API (for local development). | |
| """ | |
| import requests | |
| payload = { | |
| "input_type": input_type, | |
| "input_data": input_data, | |
| "quality": quality, | |
| "category": category | |
| } | |
| response = requests.post(f"{api_url}/api/videos", json=payload, timeout=30) | |
| if response.status_code != 200: | |
| raise Exception(f"Failed to start job: {response.text}") | |
| job_data = response.json() | |
| job_id = job_data["job_id"] | |
| # Poll for status | |
| while True: | |
| status_response = requests.get(f"{api_url}/api/jobs/{job_id}", timeout=10) | |
| if status_response.status_code == 200: | |
| status_data = status_response.json() | |
| status = status_data["status"] | |
| progress = status_data.get("progress", {}) | |
| percentage = progress.get("percentage", 0) | |
| message = progress.get("message", "Processing...") | |
| if progress_callback: | |
| progress_callback(status, percentage, message) | |
| if status == "completed": | |
| return f"{api_url}/api/videos/{job_id}" | |
| elif status == "failed": | |
| raise Exception(status_data.get("error", "Unknown error")) | |
| time.sleep(2) | |
| # ============================================================================ | |
| # Sidebar | |
| # ============================================================================ | |
| with st.sidebar: | |
| # Logo and branding | |
| st.markdown(""" | |
| <div style="text-align: center; padding: 1rem 0;"> | |
| <h1 style=" | |
| background: linear-gradient(135deg, #818cf8 0%, #c084fc 50%, #f472b6 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 2rem; | |
| font-weight: 800; | |
| margin: 0; | |
| ">β¨ Vidsimplify</h1> | |
| <p style="color: #a5b4fc; font-size: 0.85rem; margin-top: 0.5rem;"> | |
| Precision Animations | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown("---") | |
| # Show mode indicator | |
| if DIRECT_MODE: | |
| st.success("π Cloud Mode") | |
| else: | |
| st.info("π Local Development") | |
| api_url = st.text_input("API URL", value="http://localhost:8000") | |
| st.markdown("### βοΈ Settings") | |
| # Quality - default to HIGH (index=2) | |
| quality = st.selectbox( | |
| "Video Quality", | |
| ["low", "medium", "high"], | |
| index=2, # Default to HIGH | |
| format_func=lambda x: {"low": "β‘ Fast (480p)", "medium": "π₯ Standard (720p)", "high": "β¨ Premium (1080p)"}[x] | |
| ) | |
| # Category with better labels | |
| category = st.selectbox( | |
| "Animation Style", | |
| ["tech_system", "product_startup", "mathematical"], | |
| index=0, | |
| format_func=lambda x: {"tech_system": "π§ Tech & Systems", "product_startup": "π Product & Startup", "mathematical": "π Mathematical"}[x] | |
| ) | |
| # Voice selection for TTS | |
| voice_options = { | |
| "af_heart": "β€οΈ Heart (Female, Warm)", | |
| "af_bella": "πΌ Bella (Female, Professional)", | |
| "af_nicole": "π€ Nicole (Female, Clear)", | |
| "af_sarah": "πΈ Sarah (Female, Natural)", | |
| "am_adam": "π Adam (Male, Professional)", | |
| "am_michael": "ποΈ Michael (Male, Deep)", | |
| "bf_emma": "π¬π§ Emma (British Female)", | |
| "bf_isabella": "π¬π§ Isabella (British Female)", | |
| "bm_george": "π¬π§ George (British Male)", | |
| "bm_lewis": "π¬π§ Lewis (British Male)", | |
| } | |
| voice = st.selectbox( | |
| "ποΈ Voice", | |
| list(voice_options.keys()), | |
| index=0, | |
| format_func=lambda x: voice_options[x] | |
| ) | |
| st.markdown("---") | |
| # About section - professional branding | |
| st.markdown(""" | |
| <div style=" | |
| background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.05) 100%); | |
| border: 1px solid rgba(99, 102, 241, 0.2); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| margin-top: 1rem; | |
| "> | |
| <h4 style="color: #e0e0ff; margin: 0 0 0.5rem 0; font-size: 0.9rem;">About Vidsimplify</h4> | |
| <p style="color: #a5b4fc; font-size: 0.8rem; margin: 0; line-height: 1.5;"> | |
| Transform your idea into precision animation. | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Footer in sidebar | |
| st.markdown(""" | |
| <div style=" | |
| position: absolute; | |
| bottom: 6rem; | |
| left: 1rem; | |
| right: 1rem; | |
| text-align: center; | |
| color: #6b7280; | |
| font-size: 0.75rem; | |
| "> | |
| Made with π by Vidsimplify Team | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # ============================================================================ | |
| # Main Content | |
| # ============================================================================ | |
| # Hero section | |
| st.markdown(""" | |
| <div style="text-align: center; margin-bottom: 2rem;"> | |
| <h1 style=" | |
| background: linear-gradient(135deg, #818cf8 0%, #c084fc 50%, #f472b6 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 2.8rem; | |
| font-weight: 800; | |
| margin-bottom: 0.5rem; | |
| ">Create Precision Animations</h1> | |
| <p style="color: #a5b4fc; font-size: 1.1rem;"> | |
| Transform text, blogs, and documents into beautiful animated videos | |
| </p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Input tabs | |
| tab1, tab2, tab3 = st.tabs(["π Text / Script", "π Blog / URL", "π PDF Document"]) | |
| input_type = "text" | |
| input_data = "" | |
| with tab1: | |
| st.header("Text Input") | |
| st.markdown("Paste your script, blog post, or long text here.") | |
| text_input = st.text_area("Content", height=300, placeholder="Enter your text here...") | |
| if text_input: | |
| input_type = "text" | |
| input_data = text_input | |
| with tab2: | |
| st.header("URL Input") | |
| st.markdown("Enter the URL of a blog post or article.") | |
| url_input = st.text_input("URL", placeholder="https://example.com/blog-post") | |
| if url_input: | |
| input_type = "url" | |
| input_data = url_input | |
| with tab3: | |
| st.header("PDF Upload") | |
| st.markdown("Upload a PDF document to generate a video from.") | |
| uploaded_file = st.file_uploader("Choose a PDF file", type="pdf") | |
| if uploaded_file: | |
| input_type = "pdf" | |
| bytes_data = uploaded_file.getvalue() | |
| base64_pdf = base64.b64encode(bytes_data).decode('utf-8') | |
| input_data = base64_pdf | |
| # ============================================================================ | |
| # Generate Button & Logic | |
| # ============================================================================ | |
| # Add spacing before button | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| if st.button("β¨ Create Animation", type="primary", use_container_width=True): | |
| if not input_data: | |
| st.error("Please provide input data.") | |
| else: | |
| # Create status containers | |
| progress_bar = st.progress(0) | |
| status_container = st.empty() | |
| video_container = st.empty() | |
| def update_progress(status, percentage, message): | |
| progress_bar.progress(min(percentage / 100, 1.0)) | |
| status_class = { | |
| "generating_code": "status-generating", | |
| "code_generated": "status-generating", | |
| "rendering": "status-rendering", | |
| "fixing": "status-rendering", | |
| "completed": "status-complete", | |
| "failed": "status-error" | |
| }.get(status, "status-generating") | |
| status_container.markdown( | |
| f'<div class="status-box {status_class}">π <strong>{status.upper()}</strong>: {message}</div>', | |
| unsafe_allow_html=True | |
| ) | |
| try: | |
| if DIRECT_MODE: | |
| # Direct execution (Hugging Face) | |
| update_progress("starting", 5, "Initializing video generation...") | |
| video_path = generate_video_direct( | |
| input_type=input_type, | |
| input_data=input_data, | |
| quality=quality, | |
| category=category, | |
| progress_callback=update_progress | |
| ) | |
| # Display video from local file | |
| st.balloons() | |
| with open(video_path, 'rb') as video_file: | |
| video_bytes = video_file.read() | |
| video_container.video(video_bytes) | |
| # Offer download | |
| st.download_button( | |
| label="π₯ Download Video", | |
| data=video_bytes, | |
| file_name=f"vidsimplify_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mp4", | |
| mime="video/mp4" | |
| ) | |
| else: | |
| # API mode (local development) | |
| import requests | |
| update_progress("submitting", 5, "Submitting job to API...") | |
| video_url = generate_via_api( | |
| api_url=api_url, | |
| input_type=input_type, | |
| input_data=input_data, | |
| quality=quality, | |
| category=category, | |
| progress_callback=update_progress | |
| ) | |
| st.balloons() | |
| video_container.video(video_url) | |
| except Exception as e: | |
| error_msg = str(e) | |
| st.error(f"β Generation failed: {error_msg}") | |
| # Show helpful message for common errors | |
| if "Could not connect" in error_msg or "Connection" in error_msg: | |
| st.warning("π‘ If running locally, make sure the API server is running: `python api_server.py`") | |
| # ============================================================================ | |
| # Footer | |
| # ============================================================================ | |
| st.markdown(""" | |
| <div style=" | |
| margin-top: 3rem; | |
| padding: 1.5rem 0; | |
| border-top: 1px solid rgba(99, 102, 241, 0.2); | |
| text-align: center; | |
| "> | |
| <div style="display: flex; justify-content: center; align-items: center; gap: 2rem; flex-wrap: wrap;"> | |
| <span style="color: #6b7280; font-size: 0.85rem;"> | |
| β¨ <strong style="color: #a5b4fc;">Vidsimplify</strong> β Precision Animations | |
| </span> | |
| <span style="color: #4b5563; font-size: 0.8rem;">|</span> | |
| <span style="color: #6b7280; font-size: 0.85rem;"> | |
| Made with π by Vidsimplify Team | |
| </span> | |
| <span style="color: #4b5563; font-size: 0.8rem;">|</span> | |
| <span style="color: #6b7280; font-size: 0.85rem;"> | |
| Β© 2024 All rights reserved | |
| </span> | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |