Seedance-V1 / app.py
Willa666
🎨 Implement Minimalist UI Design & Direct Download Feature
6a793be
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()