Spaces:
Running
Running
| 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""" | |
| <a href="{video_url}" download="video_{i+1}.mp4" style=" | |
| display: inline-block; | |
| padding: 0.5rem 1rem; | |
| background-color: #0066cc; | |
| color: white; | |
| text-decoration: none; | |
| border-radius: 4px; | |
| font-weight: 500; | |
| text-align: center; | |
| width: 100%; | |
| box-sizing: border-box; | |
| ">π₯ Download</a> | |
| """, 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(""" | |
| <div style='text-align: center; color: #666; margin-top: 2rem;'> | |
| <p>Powered by <a href='https://novita.ai' target='_blank'>Novita AI</a> | | |
| <a href='https://novita.ai/docs/api-reference/model-apis-seedance-v1-lite-t2v' target='_blank'>Seedance V1 Documentation</a></p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if __name__ == "__main__": | |
| main() | |