Seedance-Free / app.py
ginipick's picture
Update app.py
ba3a242 verified
raw
history blame
21.7 kB
#!/usr/bin/env python3
"""
AI Video Generator with Gradio - WaveSpeed API Version
Single file application - app.py
"""
import os
import gradio as gr
import requests
import json
import time
import base64
from PIL import Image
import io
from datetime import datetime
import tempfile
# Try to import video processing libraries for watermark
try:
import cv2
import numpy as np
VIDEO_PROCESSING_AVAILABLE = True
except ImportError:
VIDEO_PROCESSING_AVAILABLE = False
print("Warning: cv2 not available. Watermark feature will be disabled.")
# API keys setup from environment variables
API_KEY_T2V = os.getenv("WAVESPEED_API_KEY_T2V", "946b77acd6a456dfde349aa0bac7b6d2bdf9c0c995fff072898c6d8734f866c4")
API_KEY_I2V = os.getenv("WAVESPEED_API_KEY_I2V", "5a90eaa7ded95066c07adf55b91185a83abfa8db3fdc74d12ba5ad66db1d68fe")
# API endpoints
API_BASE_URL = "https://api.wavespeed.ai/api/v3"
T2V_ENDPOINT = f"{API_BASE_URL}/bytedance/seedance-v1-lite-t2v-480p"
I2V_ENDPOINT = f"{API_BASE_URL}/bytedance/seedance-v1-lite-i2v-480p"
# Aspect ratio options
ASPECT_RATIOS = {
"16:9": "16:9 (YouTube, Standard Video)",
"4:3": "4:3 (Traditional TV Format)",
"1:1": "1:1 (Instagram Feed)",
"3:4": "3:4 (Instagram Portrait)",
"9:16": "9:16 (Instagram Reels, TikTok)",
"21:9": "21:9 (Cinematic Wide)",
"9:21": "9:21 (Ultra Vertical)"
}
# Default prompts
DEFAULT_TEXT_PROMPT = ""
DEFAULT_IMAGE_PROMPT = "Generate a video with smooth and natural movement. Objects should have visible motion while maintaining fluid transitions."
def add_watermark_cv2(input_video_path, output_video_path):
"""Add watermark to video using OpenCV"""
if not VIDEO_PROCESSING_AVAILABLE:
return False
try:
cap = cv2.VideoCapture(input_video_path)
fps = int(cap.get(cv2.CAP_PROP_FPS))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height))
watermark_text = "ginigen.com"
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = max(0.4, height * 0.001)
font_thickness = max(1, int(height * 0.002))
(text_width, text_height), baseline = cv2.getTextSize(watermark_text, font, font_scale, font_thickness)
padding = int(width * 0.02)
x = width - text_width - padding
y = height - padding
while True:
ret, frame = cap.read()
if not ret:
break
overlay = frame.copy()
cv2.rectangle(overlay,
(x - 5, y - text_height - 5),
(x + text_width + 5, y + 5),
(0, 0, 0),
-1)
frame = cv2.addWeighted(frame, 0.7, overlay, 0.3, 0)
cv2.putText(frame, watermark_text, (x, y), font, font_scale, (255, 255, 255), font_thickness, cv2.LINE_AA)
out.write(frame)
cap.release()
out.release()
cv2.destroyAllWindows()
return True
except Exception as e:
print(f"Watermark error: {str(e)}")
return False
def add_watermark(input_video_path, output_video_path):
"""Add watermark to video"""
if VIDEO_PROCESSING_AVAILABLE:
success = add_watermark_cv2(input_video_path, output_video_path)
if success:
return True
# Fallback - just copy without watermark
try:
import shutil
shutil.copy2(input_video_path, output_video_path)
return False
except:
return False
def compress_image(image, max_size_mb=2, max_dimension=1920):
"""Compress and resize image to meet size requirements"""
# Convert to RGB if necessary
if image.mode in ('RGBA', 'LA'):
rgb_image = Image.new('RGB', image.size, (255, 255, 255))
rgb_image.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
image = rgb_image
elif image.mode not in ('RGB', 'L'):
image = image.convert('RGB')
# Resize if dimensions are too large
width, height = image.size
if width > max_dimension or height > max_dimension:
ratio = min(max_dimension / width, max_dimension / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Compress with varying quality to meet size requirement
quality = 95
while quality > 20:
buffered = io.BytesIO()
image.save(buffered, format="JPEG", quality=quality, optimize=True)
size_mb = len(buffered.getvalue()) / (1024 * 1024)
if size_mb <= max_size_mb:
return buffered.getvalue()
quality -= 5
# If still too large, resize more aggressively
width, height = image.size
image = image.resize((width // 2, height // 2), Image.Resampling.LANCZOS)
buffered = io.BytesIO()
image.save(buffered, format="JPEG", quality=50, optimize=True)
return buffered.getvalue()
def update_prompt_placeholder(mode):
"""Update prompt placeholder based on mode"""
if mode == "Text to Video":
return gr.update(
placeholder="Describe the video you want to create.\nExample: The sun rises slowly between tall buildings. [Ground-level follow shot] Bicycle tires roll over a dew-covered street at dawn.",
value=""
)
else:
return gr.update(
placeholder="Describe how the image should move.\nExample: Camera slowly zooms in while clouds move across the sky. The subject's hair gently moves in the wind.",
value=DEFAULT_IMAGE_PROMPT
)
def update_image_input(mode):
"""Show/hide image input based on mode"""
if mode == "Image to Video":
# Show image input, hide aspect ratio for I2V
return gr.update(visible=True), gr.update(visible=False)
else:
# Hide image input, show aspect ratio for T2V
return gr.update(visible=False), gr.update(visible=True)
def poll_for_result(request_id, api_key, progress_callback=None):
"""Poll WaveSpeed API for video generation result"""
url = f"{API_BASE_URL}/predictions/{request_id}/result"
headers = {"Authorization": f"Bearer {api_key}"}
start_time = time.time()
while True:
try:
response = requests.get(url, headers=headers, timeout=30)
if response.status_code == 200:
result = response.json()["data"]
status = result["status"]
elapsed = time.time() - start_time
if progress_callback:
progress_val = min(0.3 + (elapsed / 120) * 0.6, 0.9)
progress_callback(progress_val, desc=f"Processing... Status: {status} ({int(elapsed)}s)")
if status == "completed":
video_url = result["outputs"][0]
return True, video_url
elif status == "failed":
error_msg = result.get('error', 'Unknown error')
return False, f"Generation failed: {error_msg}"
elif elapsed > 300: # 5 minute timeout
return False, "Timeout: Generation took too long"
# Continue polling
time.sleep(2)
else:
return False, f"API Error: {response.status_code} - {response.text}"
except Exception as e:
return False, f"Connection error: {str(e)}"
def generate_video(mode, prompt, image_url, image_file, aspect_ratio, seed, api_key_override_t2v, api_key_override_i2v, progress=gr.Progress()):
"""Main video generation function using WaveSpeed API"""
# Input validation
if not prompt:
return None, "❌ Please enter a prompt."
if mode == "Image to Video":
if not image_url and image_file is None:
return None, "❌ Please provide an image URL or upload an image."
try:
progress(0, desc="Preparing request...")
# Determine which API to use
if mode == "Text to Video":
api_key = api_key_override_t2v or API_KEY_T2V
endpoint = T2V_ENDPOINT
payload = {
"aspect_ratio": aspect_ratio,
"duration": 5,
"prompt": prompt,
"seed": seed if seed >= 0 else -1
}
else: # Image to Video
api_key = api_key_override_i2v or API_KEY_I2V
endpoint = I2V_ENDPOINT
# Handle image input
if image_url:
# Use provided URL directly
final_image_url = image_url
elif image_file is not None:
# Compress image to reduce size
try:
# Get original size
orig_buffered = io.BytesIO()
image_file.save(orig_buffered, format="PNG")
orig_size_mb = len(orig_buffered.getvalue()) / (1024 * 1024)
progress(0.1, desc=f"Compressing image (Original: {orig_size_mb:.1f}MB)...")
compressed_data = compress_image(image_file, max_size_mb=2, max_dimension=1920)
compressed_size_mb = len(compressed_data) / (1024 * 1024)
progress(0.15, desc=f"Image compressed: {orig_size_mb:.1f}MB β†’ {compressed_size_mb:.1f}MB")
image_base64 = base64.b64encode(compressed_data).decode()
# Check encoded size
encoded_size_mb = len(image_base64) / (1024 * 1024)
if encoded_size_mb > 20: # Conservative limit for base64
return None, f"""❌ Image too large even after compression.
Original size: {orig_size_mb:.1f}MB
Compressed size: {compressed_size_mb:.1f}MB
Encoded size: {encoded_size_mb:.1f}MB (exceeds API limit)
Please use an image hosting service:
1. Go to [imgur.com](https://imgur.com)
2. Upload your image (drag & drop)
3. Right-click uploaded image β†’ "Copy image address"
4. Paste URL in the Image URL field above"""
# Try data URL format
final_image_url = f"data:image/jpeg;base64,{image_base64}"
except Exception as e:
return None, f"❌ Error processing image: {str(e)}"
else:
return None, "❌ No image provided."
payload = {
"duration": 5,
"image": final_image_url,
"prompt": prompt,
"seed": seed if seed >= 0 else -1
}
# Submit request
progress(0.2, desc="Submitting request to WaveSpeed AI...")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
response = requests.post(endpoint, headers=headers, data=json.dumps(payload), timeout=30)
if response.status_code == 200:
result = response.json()["data"]
request_id = result["id"]
progress(0.3, desc=f"Request submitted. ID: {request_id}")
else:
error_detail = response.text
if mode == "Image to Video" and image_file is not None and not image_url:
# Check if it's a size limit error
if "size exceeds" in error_detail or "30MB" in error_detail:
return None, f"""❌ Image size exceeds API limit (30MB).
Your image was compressed but still too large for the API.
**Please use an image hosting service instead:**
1. Go to [imgur.com](https://imgur.com) (no account needed)
2. Click "New post" β†’ Drop your image
3. After upload, right-click the image β†’ "Copy image address"
4. Paste the URL in the Image URL field above
Alternative services:
- [imgbb.com](https://imgbb.com) - Simple, no account needed
- [postimages.org](https://postimages.org) - Direct links available
Error: {response.status_code}"""
else:
return None, f"""❌ API Error: {response.status_code}
The API does not accept base64 data URLs. Please use a direct image URL instead:
1. Upload your image to [imgur.com](https://imgur.com)
2. Copy the direct image URL (ends with .jpg or .png)
3. Paste it in the Image URL field above
Error details: {error_detail}"""
else:
return None, f"❌ API Error: {response.status_code} - {error_detail}"
# Poll for result
success, result = poll_for_result(request_id, api_key, lambda p, desc: progress(p, desc=desc))
if not success:
return None, f"❌ {result}"
video_url = result
progress(0.9, desc="Downloading video...")
# Download video
video_response = requests.get(video_url, timeout=60)
if video_response.status_code != 200:
return None, f"❌ Failed to download video from {video_url}"
video_data = video_response.content
# Save to temporary file
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
tmp_file.write(video_data)
temp_video_path = tmp_file.name
# Try to add watermark
watermark_added = False
final_video_path = temp_video_path
if VIDEO_PROCESSING_AVAILABLE:
progress(0.95, desc="Adding watermark...")
final_video_path = tempfile.mktemp(suffix='.mp4')
watermark_added = add_watermark(temp_video_path, final_video_path)
if not watermark_added or not os.path.exists(final_video_path):
final_video_path = temp_video_path
# Save final video
with open(final_video_path, "rb") as f:
final_video_data = f.read()
with open("output.mp4", "wb") as file:
file.write(final_video_data)
# Clean up temp files
if temp_video_path != final_video_path and os.path.exists(temp_video_path):
try:
os.unlink(temp_video_path)
except:
pass
progress(1.0, desc="Complete!")
# Generation info
watermark_status = "Added" if watermark_added else "Not available (cv2 not installed)" if not VIDEO_PROCESSING_AVAILABLE else "Failed"
# Add image info if applicable
image_info = ""
if mode == "Image to Video":
if image_url:
image_info = "\n- Image Source: URL provided"
elif image_file:
image_info = "\n- Image Source: Uploaded & compressed"
info = f"""βœ… Video generated successfully!
πŸ“Š Generation Info:
- Mode: {mode}
- Aspect Ratio: {aspect_ratio if mode == "Text to Video" else "N/A"}{image_info}
- Seed: {seed}
- Duration: 5 seconds
- Resolution: 480p
- Watermark: {watermark_status}
- API: WaveSpeed AI
- File: output.mp4"""
return final_video_path, info
except requests.exceptions.Timeout:
return None, "⏱️ Request timed out. Please try again."
except Exception as e:
return None, f"❌ Error occurred: {str(e)}"
# Gradio interface
with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as app:
gr.Markdown("""
# 🎬 Bytedance Seedance Video' Free
Generate videos from text or images using **API**.
[![Powered by Ginigen](https://img.shields.io/badge/Powered%20by-Ginigen)](https://ginigen.com/)
""")
with gr.Row():
with gr.Column(scale=1):
# API Settings
with gr.Accordion("βš™οΈ API Settings", open=False):
gr.Markdown("""
API keys are loaded from environment variables:
- `WS_API_KEY_T2V` for Text-to-Video
- `WS_API_KEY_I2V` for Image-to-Video
You can override them below if needed.
""")
api_key_override_t2v = gr.Textbox(
label="Text-to-Video API Key (Optional Override)",
type="password",
placeholder="Leave empty to use environment variable",
value=""
)
api_key_override_i2v = gr.Textbox(
label="Image-to-Video API Key (Optional Override)",
type="password",
placeholder="Leave empty to use environment variable",
value=""
)
# Generation mode
mode = gr.Radio(
label="🎯 Generation Mode",
choices=["Text to Video", "Image to Video"],
value="Text to Video"
)
# Image input - URL or file
with gr.Column(visible=False) as image_input_group:
gr.Markdown("### Image Input")
image_url_input = gr.Textbox(
label="πŸ“· Image URL (Recommended) - Direct image URL works best",
placeholder="https://example.com/image.jpg"
)
gr.Markdown("**OR**")
image_file_input = gr.Image(
label="πŸ“€ Upload Image (Will be compressed, max 2MB after compression)",
type="pil"
)
gr.Markdown("""
⚠️ **Note**: API has a 30MB limit. Large images will be compressed.
If upload fails, use [imgur.com](https://imgur.com) to host your image.
""")
# Aspect ratio (only for T2V)
aspect_ratio = gr.Dropdown(
label="πŸ“ Aspect Ratio (Text to Video only)",
choices=list(ASPECT_RATIOS.keys()),
value="16:9",
info="Choose ratio optimized for social media platforms",
visible=True
)
# Ratio description
ratio_info = gr.Markdown(value=f"Selected ratio: {ASPECT_RATIOS['16:9']}")
# Seed setting
seed = gr.Number(
label="🎲 Random Seed",
value=-1,
precision=0,
info="Use -1 for random, or set a specific value for reproducible results"
)
# Fixed settings display
watermark_info = "ginigen.com" if VIDEO_PROCESSING_AVAILABLE else "ginigen.com (requires cv2)"
gr.Markdown(f"""
### πŸ“‹ Fixed Settings
- **Duration**: 5 seconds
- **Resolution**: 480p
- **Watermark**: {watermark_info}
""")
with gr.Column(scale=2):
# Prompt input
prompt = gr.Textbox(
label="✍️ Prompt",
lines=5,
placeholder="Describe the video you want to create.\nExample: The sun rises slowly between tall buildings. [Ground-level follow shot] Bicycle tires roll over a dew-covered street at dawn.",
value=""
)
# Generate button
generate_btn = gr.Button("🎬 Generate Video", variant="primary", size="lg")
# Results display
with gr.Column():
output_video = gr.Video(
label="πŸ“Ή Generated Video",
autoplay=True
)
output_info = gr.Textbox(
label="Information",
lines=10,
interactive=False
)
# Examples
gr.Examples(
examples=[
["Text to Video", "A cat walking gracefully across a sunlit room", "", None, "16:9", -1],
["Text to Video", "A serene lake at sunrise with mist rolling over the water. Camera slowly pans across the landscape as birds fly overhead.", "", None, "16:9", 42],
["Text to Video", "Urban street scene at night with neon lights reflecting on wet pavement. People walking with umbrellas, camera tracking forward.", "", None, "9:16", 123],
],
inputs=[mode, prompt, image_url_input, image_file_input, aspect_ratio, seed],
label="Example Prompts"
)
# Event handlers
mode.change(
fn=update_prompt_placeholder,
inputs=[mode],
outputs=[prompt]
)
mode.change(
fn=update_image_input,
inputs=[mode],
outputs=[image_input_group, aspect_ratio]
)
aspect_ratio.change(
fn=lambda x: f"Selected ratio: {ASPECT_RATIOS[x]}",
inputs=[aspect_ratio],
outputs=[ratio_info]
)
generate_btn.click(
fn=generate_video,
inputs=[mode, prompt, image_url_input, image_file_input, aspect_ratio, seed, api_key_override_t2v, api_key_override_i2v],
outputs=[output_video, output_info]
)
# Run app
if __name__ == "__main__":
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
inbrowser=True
)