VidSimplify / streamlit_app.py
Adityahulk
integrating free voice
60e9dd0
"""
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)