Spaces:
Running
Running
#!/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 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: | |
# Convert PIL image to data URL | |
progress(0.1, desc="Converting image to data URL...") | |
# Convert to base64 | |
buffered = io.BytesIO() | |
if isinstance(image_file, str): | |
with Image.open(image_file) as img: | |
img.save(buffered, format="PNG") | |
else: | |
image_file.save(buffered, format="PNG") | |
image_base64 = base64.b64encode(buffered.getvalue()).decode() | |
# Try data URL format first | |
final_image_url = f"data:image/png;base64,{image_base64}" | |
# Note: If the API doesn't accept data URLs, you'll need to upload to a CDN | |
# For now, we'll try the data URL and provide guidance if it fails | |
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: | |
return None, f"""β API Error: {response.status_code} | |
The API may not accept base64 data URLs. Please try one of these options: | |
1. Use a direct image URL (e.g., https://example.com/image.jpg) | |
2. Upload your image to an image hosting service like: | |
- imgur.com | |
- imgbb.com | |
- postimages.org | |
3. Use the image URL in the Image URL field | |
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" | |
info = f"""β Video generated successfully! | |
π Generation Info: | |
- Mode: {mode} | |
- Aspect Ratio: {aspect_ratio} | |
- Seed: {seed} | |
- Duration: 5 seconds | |
- Resolution: 480p | |
- Watermark: {watermark_status} | |
- API: WaveSpeed AI | |
- Cost: $0.06 | |
- 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 **WaveSpeed AI API**. | |
[](https://ginigen.com/) | |
β οΈ **Note**: Each generation costs $0.06 | |
""") | |
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", | |
placeholder="https://example.com/image.jpg", | |
info="Enter a direct image URL" | |
) | |
gr.Markdown("**OR**") | |
image_file_input = gr.Image( | |
label="π€ Upload Image (Beta) - Converted to data URL (may not work)", | |
type="pil" | |
) | |
# 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 | |
- **Cost**: $0.06 per video | |
- **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 ($0.06)", 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 | |
) | |
# Usage instructions | |
with gr.Accordion("π How to Use", open=False): | |
gr.Markdown(""" | |
### Installation | |
1. **Install required packages**: | |
```bash | |
pip install gradio requests pillow | |
``` | |
2. **For watermark support (optional)**: | |
```bash | |
pip install opencv-python | |
``` | |
3. **Set environment variables**: | |
```bash | |
export WS_API_KEY_T2V="your-t2v-api-key" | |
export WS_API_KEY_I2V="your-i2v-api-key" | |
``` | |
4. **Run**: | |
```bash | |
python app.py | |
``` | |
### Features | |
- **Text to Video**: Generate video from text description with aspect ratio selection | |
- **Image to Video**: Transform image into animated video | |
- Option 1: Provide direct image URL (recommended) | |
- Option 2: Upload image file (converted to data URL - may not work) | |
- **Aspect Ratios**: Available for Text to Video only | |
- **Seed Value**: Use -1 for random or set specific value for reproducible results | |
- **Watermark**: Automatically adds "ginigen.com" watermark (requires opencv-python) | |
### Image to Video Tips | |
For best results with Image to Video: | |
1. Use direct image URLs (https://...) | |
2. If uploading files doesn't work, upload your image to: | |
- [imgur.com](https://imgur.com) | |
- [imgbb.com](https://imgbb.com) | |
- [postimages.org](https://postimages.org) | |
3. Copy the direct image URL and paste it in the Image URL field | |
### API Information | |
- **Provider**: WaveSpeed AI | |
- **Models**: ByteDance Seedance v1 Lite | |
- **Cost**: $0.06 per 5-second video | |
- **Resolution**: 480p | |
### Prompt Writing Tips | |
- Use specific and detailed descriptions | |
- Specify camera movements (e.g., zoom in, pan left, tracking shot) | |
- Describe lighting and atmosphere (e.g., golden hour, dramatic lighting) | |
- Indicate movement speed (e.g., slowly, rapidly, gently) | |
""") | |
# 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], | |
["Image to Video", "The camera slowly zooms in while clouds drift across the sky", "https://images.unsplash.com/photo-1506905925346-21bda4d32df4", None, "16:9", -1], | |
], | |
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 | |
) |