import streamlit as st import time import base64 from io import BytesIO from PIL import Image from config import Config from api.novita_client import NovitaClient, NovitaAPIError from utils.validators import ( validate_api_key, validate_prompt, validate_image, validate_model_parameters, validate_seed ) # Set page configuration st.set_page_config( page_title=Config.APP_TITLE, page_icon=Config.APP_ICON, layout="centered" ) # Validate configuration config_validation = Config.validate_config() if not config_validation["valid"]: st.error("Configuration error: " + ", ".join(config_validation["errors"])) if config_validation["warnings"]: for warning in config_validation["warnings"]: st.warning(warning) def image_to_base64(image): """Convert PIL image to base64 string""" buffer = BytesIO() image.save(buffer, format="PNG") img_str = base64.b64encode(buffer.getvalue()).decode() return f"data:image/png;base64,{img_str}" def generate_video(api_key, prompt, model_type="seedance-v1-lite-t2v", resolution="720p", duration=5, aspect_ratio="16:9", image_base64=None, fix_camera=False, seed=None): """Generate video function""" try: client = NovitaClient(api_key) return client.generate_video( prompt=prompt, model_type=model_type, resolution=resolution, duration=duration, aspect_ratio=aspect_ratio, image_base64=image_base64, fix_camera=fix_camera, seed=seed ) except NovitaAPIError as e: st.error(f"API Error: {str(e)}") if e.status_code == 401: st.error("🔑 Please check if API key is correct") elif e.status_code == 429: st.warning("⏱️ Too many requests, please try again later") elif e.status_code == 402: st.error("💳 Insufficient account balance, please recharge") return None except Exception as e: st.error(f"Unknown error: {str(e)}") return None def check_task_status(api_key, task_id): """Check task status""" try: client = NovitaClient(api_key) return client.check_task_status(task_id) except Exception as e: return "error", str(e) def save_video_to_history(video_url, prompt, model_type, resolution, duration, aspect_ratio): """Save video information to history, keeping only latest 5""" import datetime video_info = { "url": video_url, "prompt": prompt[:100] + "..." if len(prompt) > 100 else prompt, # Truncate long prompts "model": model_type, "resolution": resolution, "duration": duration, "aspect_ratio": aspect_ratio, "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") } # Add to beginning of list st.session_state.video_history.insert(0, video_info) # Keep only latest 5 if len(st.session_state.video_history) > 5: st.session_state.video_history = st.session_state.video_history[:5] def main(): st.title(Config.APP_TITLE) st.markdown("Generate high-quality videos from text descriptions or animate your images using AI") # Initialize session state for model synchronization if 'selected_model_key' not in st.session_state: st.session_state.selected_model_key = list(Config.MODEL_OPTIONS.keys())[0] # Initialize session state for video history if 'video_history' not in st.session_state: st.session_state.video_history = [] # Minimal sidebar with st.sidebar: st.header("⚙️ Settings") # Simple privacy notice st.info("🔒 Your API key is session-only and never stored") # API Key default_key = Config.NOVITA_API_KEY if Config.NOVITA_API_KEY else "" api_key = st.text_input( "🔑 API Key", value=default_key, type="password", help="Get your API key from novita.ai" ) if api_key and not validate_api_key(api_key): st.error("Invalid API key format") # Main content area tab1, tab2, tab3 = st.tabs(["Text to Video", "Image to Video", "Video History"]) with tab1: st.subheader("📝 Text to Video") # Model selection st.markdown("### 🎯 Model") t2v_model = st.selectbox( "Model Type", ["Lite", "Pro"], key="t2v_model" ) model_type_t2v = f"seedance-v1-{t2v_model.lower()}-t2v" # Video settings st.markdown("### 📹 Settings") col1, col2, col3 = st.columns(3) with col1: resolution = st.selectbox("Resolution", Config.RESOLUTIONS, index=1) with col2: duration = st.selectbox("Duration", Config.DURATIONS, format_func=lambda x: f"{x}s") with col3: aspect_ratio = st.selectbox("Aspect", Config.ASPECT_RATIOS) # Advanced options with st.expander("🔧 Advanced", expanded=False): col1, col2 = st.columns(2) with col1: fix_camera = st.checkbox("📹 Fix Camera", value=False) with col2: use_seed = st.checkbox("🎲 Use Seed") seed = None if use_seed: seed = st.number_input("Seed", min_value=0, max_value=2147483647, value=42) # Prompt st.markdown("### ✍️ Description") prompt = st.text_area( "Describe your video", placeholder="Cinematic. Wide shot. A kitten playing in a garden. Golden hour lighting.", height=100, max_chars=2000 ) # Generate button if st.button("🚀 Generate Video", type="primary", key="text_generate"): if not api_key: st.error("Enter API key in sidebar") return if not prompt: st.error("Enter video description") return generate_and_display_video(api_key, prompt, model_type_t2v, resolution, duration, aspect_ratio, None, fix_camera, seed) with tab2: st.subheader("🖼️ Image to Video") # Model selection st.markdown("### 🎯 Model") i2v_model = st.selectbox( "Model Type", ["Lite", "Pro"], key="i2v_model" ) model_type_i2v = f"seedance-v1-{i2v_model.lower()}-i2v" # Video settings st.markdown("### 📹 Settings") col1, col2, col3 = st.columns(3) with col1: resolution = st.selectbox("Resolution", Config.RESOLUTIONS, index=1, key="i2v_resolution") with col2: duration = st.selectbox("Duration", Config.DURATIONS, format_func=lambda x: f"{x}s", key="i2v_duration") with col3: aspect_ratio = st.selectbox("Aspect", Config.ASPECT_RATIOS, key="i2v_aspect") # Advanced options with st.expander("🔧 Advanced", expanded=False): col1, col2 = st.columns(2) with col1: fix_camera = st.checkbox("📹 Fix Camera", value=False, key="i2v_fix_camera") with col2: use_seed = st.checkbox("🎲 Use Seed", key="i2v_use_seed") seed = None if use_seed: seed = st.number_input("Seed", min_value=0, max_value=2147483647, value=42, key="i2v_seed") # Upload section st.markdown("### 📤 Image") uploaded_file = st.file_uploader("Upload image", type=Config.SUPPORTED_IMAGE_FORMATS) image_base64 = None if uploaded_file is not None: is_valid, error_msg, processed_image = validate_image(uploaded_file) if not is_valid: st.error(error_msg) else: st.image(processed_image, use_column_width=True) image_base64 = image_to_base64(processed_image) # Prompt section st.markdown("### 🎬 Description") prompt_i2v = st.text_area( "Describe what you want to happen", placeholder="Camera zooms in. Person walks forward. Leaves blow in wind.", height=100, max_chars=2000 ) # Generate button if st.button("🚀 Generate Video", type="primary", key="image_generate"): if not api_key: st.error("Enter API key in sidebar") return if not uploaded_file: st.error("Upload an image first") return if not prompt_i2v: st.error("Describe what you want to happen") return generate_and_display_video(api_key, prompt_i2v, model_type_i2v, resolution, duration, aspect_ratio, image_base64, fix_camera, seed) with tab3: st.subheader("📚 History") if not st.session_state.video_history: st.info("No videos yet") else: if st.button("Clear", type="secondary"): st.session_state.video_history = [] st.rerun() # Display video history in simple cards for i, video in enumerate(st.session_state.video_history): st.markdown("---") col1, col2 = st.columns([3, 1]) with col1: st.video(video['url']) with col2: # Create download button with direct download video_url = video['url'] st.markdown(f""" 📥 Download """, unsafe_allow_html=True) st.text(f"{video['model']}") st.text(f"{video['timestamp']}") st.text(f"'{video['prompt'][:30]}...'") # Short prompt preview def generate_and_display_video(api_key, prompt, model_type, resolution, duration, aspect_ratio, image_base64, fix_camera, seed): """Universal function to generate and display video""" # Generate video with st.spinner("Submitting task..."): task_id = generate_video(api_key, prompt, model_type, resolution, duration, aspect_ratio, image_base64, fix_camera, seed) if task_id: st.success(f"✅ Task submitted! Task ID: {task_id}") # Display parameter information with st.expander("📋 Generation Parameters"): st.write(f"**Model**: {model_type}") st.write(f"**Resolution**: {resolution}") st.write(f"**Duration**: {duration} seconds") st.write(f"**Aspect Ratio**: {aspect_ratio}") if fix_camera: st.write("**Camera**: Fixed") if seed is not None: st.write(f"**Seed**: {seed}") # Wait for results progress_bar = st.progress(0) status_placeholder = st.empty() # Dynamically calculate timeout max_wait_time = Config.calculate_timeout(model_type, resolution, duration) check_interval = Config.STATUS_CHECK_INTERVAL # Display estimated wait time estimated_min = max_wait_time // 60 estimated_sec = max_wait_time % 60 st.info(f"⏱️ Maximum wait time: {estimated_min}min {estimated_sec}sec (typically completes in 2-5 minutes)") st.info("💡 Keep this page open - it will automatically update when your video is ready!") for i in range(0, max_wait_time, check_interval): status, result = check_task_status(api_key, task_id) if status == "completed": progress_bar.progress(100) if result: status_placeholder.success("🎉 Video generation completed!") st.video(result) st.markdown(f"[📥 Download Video]({result})") # Save video to history save_video_to_history(result, prompt, model_type, resolution, duration, aspect_ratio) st.success("✅ Video saved to history!") else: status_placeholder.error("❌ Video generation completed but video file not found") break elif status == "failed": status_placeholder.error(f"❌ Generation failed: {result}") break elif status == "error": status_placeholder.error(f"❌ Error checking status: {result}") break else: progress = min((i / max_wait_time) * 90, 90) # Show maximum 90% progress_bar.progress(int(progress)) # More user-friendly status messages elapsed_min = i // 60 elapsed_sec = i % 60 if status == "processing": status_msg = f"🎨 Creating your video... ({elapsed_min:02d}:{elapsed_sec:02d})" elif status in ["pending", "queued"]: status_msg = f"⏳ Your video is in queue... ({elapsed_min:02d}:{elapsed_sec:02d})" else: status_msg = f"🔄 Generating video... ({status}) - {elapsed_min:02d}:{elapsed_sec:02d}" status_placeholder.info(status_msg) time.sleep(check_interval) else: status_placeholder.warning(f"⏰ After waiting {max_wait_time // 60} minutes, still not completed. Video may still be generating. Task ID: {task_id}") st.info("💡 Tip: You can save the Task ID and check results later using the same API key") # Comprehensive footer help section st.markdown("---") col1, col2, col3 = st.columns(3) with col1: with st.expander("📚 Complete Guide", expanded=False): st.markdown(""" **Getting Started:** 1. Get API key from [Novita AI](https://novita.ai) 2. Choose your generation mode (Text-to-Video or Image-to-Video) 3. Configure model and video settings 4. Follow the prompt/motion tips for best results **Model Types:** - 🎨 **Pro**: Best quality, takes longer (4-8 minutes) - ⚡ **Lite**: Good quality, faster results (2-3 minutes) **Need Help?** Check the expandable tip sections in each tab for detailed guidance. """) with col2: with st.expander("⚡ Quick Tips", expanded=False): st.markdown(""" **For Better Results:** - Be specific in descriptions - Include lighting and camera angles - Use descriptive words: "cinematic", "golden hour", "close-up" - Start simple, then add more details **Common Issues:** - No API key → Get one from novita.ai - Generation fails → Check account credits - Slow results → Try Lite model first - Poor quality → Use Pro model with detailed prompts """) with col3: with st.expander("🔧 Technical & Privacy Info", expanded=False): st.markdown(""" **Generation Times:** - Lite models: 2-3 minutes typically - Pro models: 4-8 minutes typically - Higher resolution: +30-50% time - Longer duration: +50-100% time **File Limits:** - Images: Max 10MB - Formats: PNG, JPG, JPEG - Video output: MP4 format **🔒 Privacy & Security:** - **API keys NEVER saved or stored** - **Session-only usage** - cleared when you leave - Images processed securely via HTTPS - Videos auto-deleted after download - No personal data collected or retained - You control your API key at all times """) st.markdown("---") # Footer information st.markdown("""
Powered by Novita AI | Seedance V1 Documentation