#!/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**. [![Powered by Ginigen](https://img.shields.io/badge/Powered%20by-WaveSpeed%20AI-blue)](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 )