Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,21 +1,21 @@
|
|
1 |
#!/usr/bin/env python3
|
2 |
"""
|
3 |
-
AI Video Generator with Gradio
|
4 |
Single file application - app.py
|
5 |
"""
|
6 |
|
7 |
import os
|
8 |
import gradio as gr
|
9 |
-
import
|
|
|
|
|
10 |
import base64
|
11 |
-
from PIL import Image
|
12 |
import io
|
13 |
-
import requests
|
14 |
from datetime import datetime
|
15 |
import tempfile
|
16 |
-
import time
|
17 |
|
18 |
-
# Try to import video processing libraries
|
19 |
try:
|
20 |
import cv2
|
21 |
import numpy as np
|
@@ -24,10 +24,14 @@ except ImportError:
|
|
24 |
VIDEO_PROCESSING_AVAILABLE = False
|
25 |
print("Warning: cv2 not available. Watermark feature will be disabled.")
|
26 |
|
27 |
-
# API
|
28 |
-
|
29 |
-
|
30 |
-
|
|
|
|
|
|
|
|
|
31 |
|
32 |
# Aspect ratio options
|
33 |
ASPECT_RATIOS = {
|
@@ -50,39 +54,29 @@ def add_watermark_cv2(input_video_path, output_video_path):
|
|
50 |
return False
|
51 |
|
52 |
try:
|
53 |
-
# Open the video
|
54 |
cap = cv2.VideoCapture(input_video_path)
|
55 |
-
|
56 |
-
# Get video properties
|
57 |
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
58 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
59 |
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
60 |
|
61 |
-
# Define codec and create VideoWriter
|
62 |
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
63 |
out = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height))
|
64 |
|
65 |
-
# Watermark settings
|
66 |
watermark_text = "ginigen.com"
|
67 |
font = cv2.FONT_HERSHEY_SIMPLEX
|
68 |
-
font_scale = max(0.4, height * 0.001)
|
69 |
font_thickness = max(1, int(height * 0.002))
|
70 |
|
71 |
-
# Get text size
|
72 |
(text_width, text_height), baseline = cv2.getTextSize(watermark_text, font, font_scale, font_thickness)
|
73 |
-
|
74 |
-
# Position (bottom right with padding)
|
75 |
padding = int(width * 0.02)
|
76 |
x = width - text_width - padding
|
77 |
y = height - padding
|
78 |
|
79 |
-
# Process each frame
|
80 |
while True:
|
81 |
ret, frame = cap.read()
|
82 |
if not ret:
|
83 |
break
|
84 |
|
85 |
-
# Add semi-transparent background for text
|
86 |
overlay = frame.copy()
|
87 |
cv2.rectangle(overlay,
|
88 |
(x - 5, y - text_height - 5),
|
@@ -90,43 +84,32 @@ def add_watermark_cv2(input_video_path, output_video_path):
|
|
90 |
(0, 0, 0),
|
91 |
-1)
|
92 |
frame = cv2.addWeighted(frame, 0.7, overlay, 0.3, 0)
|
93 |
-
|
94 |
-
# Add text
|
95 |
cv2.putText(frame, watermark_text, (x, y), font, font_scale, (255, 255, 255), font_thickness, cv2.LINE_AA)
|
96 |
-
|
97 |
-
# Write frame
|
98 |
out.write(frame)
|
99 |
|
100 |
-
# Release everything
|
101 |
cap.release()
|
102 |
out.release()
|
103 |
cv2.destroyAllWindows()
|
104 |
-
|
105 |
return True
|
106 |
|
107 |
except Exception as e:
|
108 |
print(f"Watermark error: {str(e)}")
|
109 |
return False
|
110 |
|
111 |
-
def add_watermark_simple(input_video_path, output_video_path):
|
112 |
-
"""Simple fallback - just copy the video without watermark"""
|
113 |
-
try:
|
114 |
-
import shutil
|
115 |
-
shutil.copy2(input_video_path, output_video_path)
|
116 |
-
return False
|
117 |
-
except Exception as e:
|
118 |
-
print(f"Copy error: {str(e)}")
|
119 |
-
return False
|
120 |
-
|
121 |
def add_watermark(input_video_path, output_video_path):
|
122 |
-
"""Add watermark to video
|
123 |
if VIDEO_PROCESSING_AVAILABLE:
|
124 |
success = add_watermark_cv2(input_video_path, output_video_path)
|
125 |
if success:
|
126 |
return True
|
127 |
|
128 |
# Fallback - just copy without watermark
|
129 |
-
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
def update_prompt_placeholder(mode):
|
132 |
"""Update prompt placeholder based on mode"""
|
@@ -148,69 +131,44 @@ def update_image_input(mode):
|
|
148 |
else:
|
149 |
return gr.update(visible=False)
|
150 |
|
151 |
-
def
|
152 |
-
"""
|
153 |
-
|
154 |
-
|
155 |
-
return gr.update(visible=True, value="β API token required. Please set it in API Settings.")
|
156 |
-
|
157 |
-
replicate.api_token = token
|
158 |
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
# Very simple warm-up request with minimal parameters
|
163 |
-
warm_up_params = {
|
164 |
-
"prompt": "red", # Extremely simple prompt
|
165 |
-
"duration": 1, # Minimum duration
|
166 |
-
"resolution": "480p",
|
167 |
-
"aspect_ratio": "16:9",
|
168 |
-
"seed": 42
|
169 |
-
}
|
170 |
-
|
171 |
-
progress(0.5, desc="Sending warm-up request (this may fail, that's normal)...")
|
172 |
-
|
173 |
try:
|
174 |
-
|
175 |
-
if output:
|
176 |
-
return gr.update(visible=True, value="""β
**Warm-up successful!**
|
177 |
-
|
178 |
-
The model is now awake. You can generate videos normally.
|
179 |
-
Note: The model stays active for ~5 minutes, so generate quickly!""")
|
180 |
-
except Exception as e:
|
181 |
-
if "PA" in str(e) or "interrupted" in str(e).lower():
|
182 |
-
return gr.update(visible=True, value="""β° **Warm-up request sent!**
|
183 |
-
|
184 |
-
The model is waking up. This is normal for a cold start.
|
185 |
-
|
186 |
-
**Next steps:**
|
187 |
-
1. Wait 2-3 minutes for the model to fully initialize
|
188 |
-
2. Try a simple prompt: "a cat walking"
|
189 |
-
3. Once that works, use your actual prompt
|
190 |
-
|
191 |
-
The wake-up process takes time, but subsequent generations will be fast!""")
|
192 |
-
else:
|
193 |
-
return gr.update(visible=True, value=f"""β οΈ **Warm-up attempted**
|
194 |
-
|
195 |
-
Error: {str(e)[:200]}...
|
196 |
-
|
197 |
-
**What to do:**
|
198 |
-
1. Wait 2-3 minutes
|
199 |
-
2. Try generating with a simple prompt
|
200 |
-
3. The model might already be warming up!""")
|
201 |
|
202 |
-
|
203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
|
205 |
-
def generate_video(mode, prompt, image, aspect_ratio, seed,
|
206 |
-
"""Main video generation function"""
|
207 |
-
|
208 |
-
# API token check
|
209 |
-
token = api_key_input or api_token
|
210 |
-
if not token:
|
211 |
-
return None, "β API token required. Please set RAPI_TOKEN environment variable or enter your API key."
|
212 |
-
|
213 |
-
os.environ["REPLICATE_API_TOKEN"] = token
|
214 |
|
215 |
# Input validation
|
216 |
if not prompt:
|
@@ -220,147 +178,83 @@ def generate_video(mode, prompt, image, aspect_ratio, seed, api_key_input, progr
|
|
220 |
return None, "β Please upload an image."
|
221 |
|
222 |
try:
|
223 |
-
progress(0, desc="Preparing
|
224 |
-
|
225 |
-
# Input parameters setup
|
226 |
-
input_params = {
|
227 |
-
"prompt": prompt,
|
228 |
-
"duration": 5, # Fixed at 5 seconds
|
229 |
-
"resolution": "480p",
|
230 |
-
"aspect_ratio": aspect_ratio,
|
231 |
-
"seed": seed
|
232 |
-
}
|
233 |
|
234 |
-
#
|
235 |
-
if mode == "
|
236 |
-
|
|
|
237 |
|
238 |
-
|
239 |
-
|
240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
buffered = io.BytesIO()
|
242 |
-
|
243 |
image_base64 = base64.b64encode(buffered.getvalue()).decode()
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
|
249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
250 |
|
251 |
-
|
252 |
|
253 |
-
|
254 |
-
|
|
|
|
|
|
|
|
|
255 |
|
256 |
-
#
|
257 |
-
|
258 |
|
259 |
-
|
|
|
260 |
|
261 |
-
|
262 |
-
|
263 |
-
output = None
|
264 |
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
# Run the model
|
271 |
-
output = replicate.run(
|
272 |
-
"bytedance/seedance-1-lite",
|
273 |
-
input=input_params
|
274 |
-
)
|
275 |
-
|
276 |
-
# If we got output, break the retry loop
|
277 |
-
if output:
|
278 |
-
break
|
279 |
-
|
280 |
-
except Exception as e:
|
281 |
-
error_str = str(e)
|
282 |
-
print(f"Attempt {attempt + 1} error: {error_str}") # Debug logging
|
283 |
-
|
284 |
-
# Check for cold start indicators
|
285 |
-
if "Prediction interrupted" in error_str or "code: PA" in error_str or "PA" in error_str:
|
286 |
-
if attempt == 0:
|
287 |
-
# First cold start - provide immediate guidance
|
288 |
-
return None, """β **Model is cold starting (code: PA)**
|
289 |
-
|
290 |
-
**Quick Fix - Follow these steps:**
|
291 |
-
|
292 |
-
1οΈβ£ Click the **"π₯ Warm Up Model"** button above
|
293 |
-
2οΈβ£ Wait for the warm-up to complete (it may also fail, that's OK)
|
294 |
-
3οΈβ£ **Wait 2-3 minutes** for the model to fully initialize
|
295 |
-
4οΈβ£ Try this simple prompt first: **"a cat walking"**
|
296 |
-
5οΈβ£ Once that works, try your original prompt
|
297 |
-
|
298 |
-
**Why this happens:** The model goes to sleep after inactivity to save resources. The first request wakes it up, which takes time.
|
299 |
-
|
300 |
-
**Alternative:** Try again in 5 minutes when the model should be fully awake."""
|
301 |
-
elif attempt < max_attempts - 1:
|
302 |
-
# Subsequent attempts - wait and retry
|
303 |
-
wait_time = 30 + (attempt * 15)
|
304 |
-
progress(0.3, desc=f"Model still warming up. Waiting {wait_time}s... (Attempt {attempt + 2}/{max_attempts})")
|
305 |
-
time.sleep(wait_time)
|
306 |
-
continue
|
307 |
-
else:
|
308 |
-
return None, """β Model is taking longer than usual to start.
|
309 |
-
|
310 |
-
**Please try one of these options:**
|
311 |
-
1. Use the "π₯ Warm Up Model" button first
|
312 |
-
2. Wait 5 minutes and try again
|
313 |
-
3. Try a very simple prompt: "a red circle"
|
314 |
-
4. Try during off-peak hours (evening/night US time)
|
315 |
-
|
316 |
-
The model needs to 'warm up' after being idle."""
|
317 |
-
|
318 |
-
elif "cold boot" in error_str.lower() or "starting" in error_str.lower():
|
319 |
-
if attempt < max_attempts - 1:
|
320 |
-
progress(0.3, desc=f"Model is booting up, waiting 60s...")
|
321 |
-
time.sleep(60)
|
322 |
-
continue
|
323 |
-
|
324 |
-
elif "timeout" in error_str.lower():
|
325 |
-
if attempt < max_attempts - 1:
|
326 |
-
progress(0.3, desc=f"Request timed out, retrying... (Attempt {attempt + 2}/{max_attempts})")
|
327 |
-
time.sleep(15)
|
328 |
-
continue
|
329 |
-
|
330 |
-
else:
|
331 |
-
# For any other error, still retry a few times
|
332 |
-
if attempt < max_attempts - 1:
|
333 |
-
wait_time = 10 + (attempt * 5)
|
334 |
-
progress(0.3, desc=f"Error occurred, retrying in {wait_time}s... (Attempt {attempt + 2}/{max_attempts})")
|
335 |
-
time.sleep(wait_time)
|
336 |
-
continue
|
337 |
-
else:
|
338 |
-
return None, f"β Error after {max_attempts} attempts: {error_str}"
|
339 |
-
|
340 |
-
# Check if we got output
|
341 |
-
if not output:
|
342 |
-
return None, """β Failed to generate video after multiple attempts.
|
343 |
-
|
344 |
-
**This usually happens when:**
|
345 |
-
- The model is cold starting (hasn't been used recently)
|
346 |
-
- The server is under heavy load
|
347 |
-
|
348 |
-
**Please try:**
|
349 |
-
1. Wait 3-5 minutes for the model to warm up
|
350 |
-
2. Start with a simple prompt: "a cat walking" or "a ball bouncing"
|
351 |
-
3. Once that works, try your original prompt
|
352 |
-
|
353 |
-
The first generation often takes longer, but subsequent ones will be faster."""
|
354 |
-
|
355 |
-
progress(0.7, desc="Downloading video...")
|
356 |
|
357 |
-
|
358 |
-
if hasattr(output, 'read'):
|
359 |
-
video_data = output.read()
|
360 |
-
else:
|
361 |
-
# Download from URL with timeout
|
362 |
-
response = requests.get(output, timeout=60)
|
363 |
-
video_data = response.content
|
364 |
|
365 |
# Save to temporary file
|
366 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
|
@@ -372,7 +266,7 @@ The first generation often takes longer, but subsequent ones will be faster."""
|
|
372 |
final_video_path = temp_video_path
|
373 |
|
374 |
if VIDEO_PROCESSING_AVAILABLE:
|
375 |
-
progress(0.
|
376 |
final_video_path = tempfile.mktemp(suffix='.mp4')
|
377 |
watermark_added = add_watermark(temp_video_path, final_video_path)
|
378 |
|
@@ -405,67 +299,54 @@ The first generation often takes longer, but subsequent ones will be faster."""
|
|
405 |
- Duration: 5 seconds
|
406 |
- Resolution: 480p
|
407 |
- Watermark: {watermark_status}
|
|
|
|
|
408 |
- File: output.mp4"""
|
409 |
|
410 |
return final_video_path, info
|
411 |
|
412 |
except requests.exceptions.Timeout:
|
413 |
-
return None, "β±οΈ Request timed out.
|
414 |
except Exception as e:
|
415 |
-
|
416 |
-
if "timeout" in str(e).lower():
|
417 |
-
error_msg += "\n\nπ‘ Tip: The model might be cold starting. Please wait a minute and try again."
|
418 |
-
return None, error_msg
|
419 |
|
420 |
# Gradio interface
|
421 |
with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as app:
|
422 |
gr.Markdown("""
|
423 |
# π¬ Bytedance Seedance Video' Free
|
424 |
|
425 |
-
Generate videos from text or images using **
|
426 |
|
427 |
-
[
|
429 |
|
430 |
-
|
431 |
-
|
432 |
-
gr.Markdown("""
|
433 |
-
### οΏ½οΏ½οΏ½οΏ½ Model Cold Start Guide
|
434 |
-
|
435 |
-
If you're getting **"Prediction interrupted (code: PA)"** errors, the model is sleeping and needs to wake up.
|
436 |
-
|
437 |
-
**Quick Solution:**
|
438 |
-
1. Click the **"π₯ Warm Up Model"** button
|
439 |
-
2. Wait 2-3 minutes after warm-up
|
440 |
-
3. Try a simple prompt: **"a cat walking"**
|
441 |
-
4. Once that works, use your actual prompt
|
442 |
-
|
443 |
-
**Why this happens:** Free tier models sleep after ~5 minutes of inactivity to save resources.
|
444 |
-
The first request wakes them up, which can take 2-5 minutes.
|
445 |
-
|
446 |
-
**Pro tip:** Once warmed up, the model stays active for ~5 minutes. Generate multiple videos quickly!
|
447 |
-
""")
|
448 |
|
449 |
with gr.Row():
|
450 |
with gr.Column(scale=1):
|
451 |
# API Settings
|
452 |
-
with gr.Accordion("βοΈ API Settings", open=
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
|
|
|
|
|
|
|
|
|
|
469 |
|
470 |
# Generation mode
|
471 |
mode = gr.Radio(
|
@@ -476,12 +357,12 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
476 |
|
477 |
# Image upload
|
478 |
image_input = gr.Image(
|
479 |
-
label="π· Upload Image",
|
480 |
type="pil",
|
481 |
visible=False
|
482 |
)
|
483 |
|
484 |
-
# Aspect ratio
|
485 |
aspect_ratio = gr.Dropdown(
|
486 |
label="π Aspect Ratio",
|
487 |
choices=list(ASPECT_RATIOS.keys()),
|
@@ -495,9 +376,9 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
495 |
# Seed setting
|
496 |
seed = gr.Number(
|
497 |
label="π² Random Seed",
|
498 |
-
value
|
499 |
precision=0,
|
500 |
-
info="Use
|
501 |
)
|
502 |
|
503 |
# Fixed settings display
|
@@ -506,6 +387,7 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
506 |
### π Fixed Settings
|
507 |
- **Duration**: 5 seconds
|
508 |
- **Resolution**: 480p
|
|
|
509 |
- **Watermark**: {watermark_info}
|
510 |
""")
|
511 |
|
@@ -518,23 +400,18 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
518 |
value=""
|
519 |
)
|
520 |
|
521 |
-
# Generate
|
522 |
-
|
523 |
-
generate_btn = gr.Button("π¬ Generate Video", variant="primary", size="lg", scale=2)
|
524 |
-
warmup_btn = gr.Button("π₯ Warm Up Model First", variant="secondary", size="lg", scale=1)
|
525 |
-
|
526 |
-
gr.Markdown("*Use Warm Up if you're getting PA errors or it's your first generation today*")
|
527 |
|
528 |
# Results display
|
529 |
with gr.Column():
|
530 |
-
warmup_status = gr.Textbox(label="Status", visible=False)
|
531 |
output_video = gr.Video(
|
532 |
label="πΉ Generated Video",
|
533 |
autoplay=True
|
534 |
)
|
535 |
output_info = gr.Textbox(
|
536 |
label="Information",
|
537 |
-
lines=
|
538 |
interactive=False
|
539 |
)
|
540 |
|
@@ -545,7 +422,7 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
545 |
|
546 |
1. **Install required packages**:
|
547 |
```bash
|
548 |
-
pip install gradio
|
549 |
```
|
550 |
|
551 |
2. **For watermark support (optional)**:
|
@@ -553,9 +430,10 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
553 |
pip install opencv-python
|
554 |
```
|
555 |
|
556 |
-
3. **Set environment
|
557 |
```bash
|
558 |
-
export
|
|
|
559 |
```
|
560 |
|
561 |
4. **Run**:
|
@@ -566,52 +444,36 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
566 |
### Features
|
567 |
|
568 |
- **Text to Video**: Generate video from text description only
|
569 |
-
- **Image to Video**: Transform uploaded image into animated video
|
570 |
- **Aspect Ratios**: Choose ratios optimized for various social media platforms
|
571 |
-
- **Seed Value**: Use
|
572 |
- **Watermark**: Automatically adds "ginigen.com" watermark (requires opencv-python)
|
573 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
574 |
### Prompt Writing Tips
|
575 |
|
576 |
- Use specific and detailed descriptions
|
577 |
- Specify camera movements (e.g., zoom in, pan left, tracking shot)
|
578 |
- Describe lighting and atmosphere (e.g., golden hour, dramatic lighting)
|
579 |
- Indicate movement speed (e.g., slowly, rapidly, gently)
|
580 |
-
|
581 |
-
### Troubleshooting
|
582 |
-
|
583 |
-
#### If you get "Prediction interrupted (code: PA)":
|
584 |
-
|
585 |
-
This means the model is cold starting. Follow these steps:
|
586 |
-
|
587 |
-
1. **First Try**: Click the "π₯ Warm Up Model" button and wait for completion
|
588 |
-
2. **Wait**: After warm-up, wait 2-3 minutes for the model to fully initialize
|
589 |
-
3. **Simple Test**: Try generating with a very simple prompt: "a cat walking"
|
590 |
-
4. **Your Prompt**: Once the simple test works, try your actual prompt
|
591 |
-
|
592 |
-
**Other Issues:**
|
593 |
-
- **Repeated failures**: The model may be under heavy load. Try during off-peak hours
|
594 |
-
- **Timeout errors**: Increase wait time between attempts
|
595 |
-
- **Watermark not showing**: Install opencv-python for watermark support
|
596 |
-
|
597 |
-
**Pro Tips:**
|
598 |
-
- First generation of the day always takes longer
|
599 |
-
- Keep prompts under 100 words for best results
|
600 |
-
- Simple prompts work better for warming up the model
|
601 |
-
- Once warmed up, generate multiple videos quickly before the model sleeps again!
|
602 |
""")
|
603 |
|
604 |
# Examples
|
605 |
gr.Examples(
|
606 |
examples=[
|
607 |
-
["Text to Video", "a
|
608 |
-
["Text to Video", "a cat walking", None, "16:9", 42], # Simple warm-up prompt
|
609 |
["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],
|
610 |
["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],
|
611 |
["Text to Video", "Close-up of a flower blooming in time-lapse, soft natural lighting, shallow depth of field.", None, "1:1", 789],
|
612 |
],
|
613 |
inputs=[mode, prompt, image_input, aspect_ratio, seed],
|
614 |
-
label="Example Prompts
|
615 |
)
|
616 |
|
617 |
# Event handlers
|
@@ -633,15 +495,9 @@ with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as
|
|
633 |
outputs=[ratio_info]
|
634 |
)
|
635 |
|
636 |
-
warmup_btn.click(
|
637 |
-
fn=warmup_model,
|
638 |
-
inputs=[api_key_input],
|
639 |
-
outputs=[warmup_status]
|
640 |
-
)
|
641 |
-
|
642 |
generate_btn.click(
|
643 |
fn=generate_video,
|
644 |
-
inputs=[mode, prompt, image_input, aspect_ratio, seed,
|
645 |
outputs=[output_video, output_info]
|
646 |
)
|
647 |
|
|
|
1 |
#!/usr/bin/env python3
|
2 |
"""
|
3 |
+
AI Video Generator with Gradio - WaveSpeed API Version
|
4 |
Single file application - app.py
|
5 |
"""
|
6 |
|
7 |
import os
|
8 |
import gradio as gr
|
9 |
+
import requests
|
10 |
+
import json
|
11 |
+
import time
|
12 |
import base64
|
13 |
+
from PIL import Image
|
14 |
import io
|
|
|
15 |
from datetime import datetime
|
16 |
import tempfile
|
|
|
17 |
|
18 |
+
# Try to import video processing libraries for watermark
|
19 |
try:
|
20 |
import cv2
|
21 |
import numpy as np
|
|
|
24 |
VIDEO_PROCESSING_AVAILABLE = False
|
25 |
print("Warning: cv2 not available. Watermark feature will be disabled.")
|
26 |
|
27 |
+
# API keys setup from environment variables
|
28 |
+
API_KEY_T2V = os.getenv("WS_API_KEY_T2V", "946b77acd6a456dfde349aa0bac7b6d2bdf9c0c995fff072898c6d8734f866c4")
|
29 |
+
API_KEY_I2V = os.getenv("WS_API_KEY_I2V", "5a90eaa7ded95066c07adf55b91185a83abfa8db3fdc74d12ba5ad66db1d68fe")
|
30 |
+
|
31 |
+
# API endpoints
|
32 |
+
API_BASE_URL = "https://api.wavespeed.ai/api/v3"
|
33 |
+
T2V_ENDPOINT = f"{API_BASE_URL}/bytedance/seedance-v1-lite-t2v-480p"
|
34 |
+
I2V_ENDPOINT = f"{API_BASE_URL}/bytedance/seedance-v1-lite-i2v-480p"
|
35 |
|
36 |
# Aspect ratio options
|
37 |
ASPECT_RATIOS = {
|
|
|
54 |
return False
|
55 |
|
56 |
try:
|
|
|
57 |
cap = cv2.VideoCapture(input_video_path)
|
|
|
|
|
58 |
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
59 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
60 |
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
61 |
|
|
|
62 |
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
63 |
out = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height))
|
64 |
|
|
|
65 |
watermark_text = "ginigen.com"
|
66 |
font = cv2.FONT_HERSHEY_SIMPLEX
|
67 |
+
font_scale = max(0.4, height * 0.001)
|
68 |
font_thickness = max(1, int(height * 0.002))
|
69 |
|
|
|
70 |
(text_width, text_height), baseline = cv2.getTextSize(watermark_text, font, font_scale, font_thickness)
|
|
|
|
|
71 |
padding = int(width * 0.02)
|
72 |
x = width - text_width - padding
|
73 |
y = height - padding
|
74 |
|
|
|
75 |
while True:
|
76 |
ret, frame = cap.read()
|
77 |
if not ret:
|
78 |
break
|
79 |
|
|
|
80 |
overlay = frame.copy()
|
81 |
cv2.rectangle(overlay,
|
82 |
(x - 5, y - text_height - 5),
|
|
|
84 |
(0, 0, 0),
|
85 |
-1)
|
86 |
frame = cv2.addWeighted(frame, 0.7, overlay, 0.3, 0)
|
|
|
|
|
87 |
cv2.putText(frame, watermark_text, (x, y), font, font_scale, (255, 255, 255), font_thickness, cv2.LINE_AA)
|
|
|
|
|
88 |
out.write(frame)
|
89 |
|
|
|
90 |
cap.release()
|
91 |
out.release()
|
92 |
cv2.destroyAllWindows()
|
|
|
93 |
return True
|
94 |
|
95 |
except Exception as e:
|
96 |
print(f"Watermark error: {str(e)}")
|
97 |
return False
|
98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
def add_watermark(input_video_path, output_video_path):
|
100 |
+
"""Add watermark to video"""
|
101 |
if VIDEO_PROCESSING_AVAILABLE:
|
102 |
success = add_watermark_cv2(input_video_path, output_video_path)
|
103 |
if success:
|
104 |
return True
|
105 |
|
106 |
# Fallback - just copy without watermark
|
107 |
+
try:
|
108 |
+
import shutil
|
109 |
+
shutil.copy2(input_video_path, output_video_path)
|
110 |
+
return False
|
111 |
+
except:
|
112 |
+
return False
|
113 |
|
114 |
def update_prompt_placeholder(mode):
|
115 |
"""Update prompt placeholder based on mode"""
|
|
|
131 |
else:
|
132 |
return gr.update(visible=False)
|
133 |
|
134 |
+
def poll_for_result(request_id, api_key, progress_callback=None):
|
135 |
+
"""Poll WaveSpeed API for video generation result"""
|
136 |
+
url = f"{API_BASE_URL}/predictions/{request_id}/result"
|
137 |
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
|
|
|
|
|
138 |
|
139 |
+
start_time = time.time()
|
140 |
+
while True:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
try:
|
142 |
+
response = requests.get(url, headers=headers, timeout=30)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
+
if response.status_code == 200:
|
145 |
+
result = response.json()["data"]
|
146 |
+
status = result["status"]
|
147 |
+
|
148 |
+
elapsed = time.time() - start_time
|
149 |
+
if progress_callback:
|
150 |
+
progress_val = min(0.3 + (elapsed / 120) * 0.6, 0.9)
|
151 |
+
progress_callback(progress_val, desc=f"Processing... Status: {status} ({int(elapsed)}s)")
|
152 |
+
|
153 |
+
if status == "completed":
|
154 |
+
video_url = result["outputs"][0]
|
155 |
+
return True, video_url
|
156 |
+
elif status == "failed":
|
157 |
+
error_msg = result.get('error', 'Unknown error')
|
158 |
+
return False, f"Generation failed: {error_msg}"
|
159 |
+
elif elapsed > 300: # 5 minute timeout
|
160 |
+
return False, "Timeout: Generation took too long"
|
161 |
+
|
162 |
+
# Continue polling
|
163 |
+
time.sleep(2)
|
164 |
+
else:
|
165 |
+
return False, f"API Error: {response.status_code} - {response.text}"
|
166 |
+
|
167 |
+
except Exception as e:
|
168 |
+
return False, f"Connection error: {str(e)}"
|
169 |
|
170 |
+
def generate_video(mode, prompt, image, aspect_ratio, seed, api_key_override_t2v, api_key_override_i2v, progress=gr.Progress()):
|
171 |
+
"""Main video generation function using WaveSpeed API"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
|
173 |
# Input validation
|
174 |
if not prompt:
|
|
|
178 |
return None, "β Please upload an image."
|
179 |
|
180 |
try:
|
181 |
+
progress(0, desc="Preparing request...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
|
183 |
+
# Determine which API to use
|
184 |
+
if mode == "Text to Video":
|
185 |
+
api_key = api_key_override_t2v or API_KEY_T2V
|
186 |
+
endpoint = T2V_ENDPOINT
|
187 |
|
188 |
+
payload = {
|
189 |
+
"aspect_ratio": aspect_ratio,
|
190 |
+
"duration": 5,
|
191 |
+
"prompt": prompt,
|
192 |
+
"seed": seed if seed >= 0 else -1
|
193 |
+
}
|
194 |
+
else: # Image to Video
|
195 |
+
api_key = api_key_override_i2v or API_KEY_I2V
|
196 |
+
endpoint = I2V_ENDPOINT
|
197 |
+
|
198 |
+
# Handle image upload
|
199 |
+
if isinstance(image, str) and (image.startswith('http://') or image.startswith('https://')):
|
200 |
+
# If it's already a URL
|
201 |
+
image_url = image
|
202 |
+
else:
|
203 |
+
# Convert PIL Image to base64 for upload
|
204 |
+
progress(0.1, desc="Processing image...")
|
205 |
+
if isinstance(image, str):
|
206 |
+
with Image.open(image) as img:
|
207 |
+
buffered = io.BytesIO()
|
208 |
+
img.save(buffered, format="PNG")
|
209 |
+
image_base64 = base64.b64encode(buffered.getvalue()).decode()
|
210 |
+
else:
|
211 |
buffered = io.BytesIO()
|
212 |
+
image.save(buffered, format="PNG")
|
213 |
image_base64 = base64.b64encode(buffered.getvalue()).decode()
|
214 |
+
|
215 |
+
# For this demo, we'll assume the image needs to be uploaded somewhere
|
216 |
+
# In production, you'd upload to a CDN and get a URL
|
217 |
+
return None, "β Image upload to CDN not implemented. Please use an image URL."
|
218 |
|
219 |
+
payload = {
|
220 |
+
"duration": 5,
|
221 |
+
"image": image_url,
|
222 |
+
"prompt": prompt,
|
223 |
+
"seed": seed if seed >= 0 else -1
|
224 |
+
}
|
225 |
+
|
226 |
+
# Submit request
|
227 |
+
progress(0.2, desc="Submitting request to WaveSpeed AI...")
|
228 |
+
|
229 |
+
headers = {
|
230 |
+
"Content-Type": "application/json",
|
231 |
+
"Authorization": f"Bearer {api_key}"
|
232 |
+
}
|
233 |
|
234 |
+
response = requests.post(endpoint, headers=headers, data=json.dumps(payload), timeout=30)
|
235 |
|
236 |
+
if response.status_code == 200:
|
237 |
+
result = response.json()["data"]
|
238 |
+
request_id = result["id"]
|
239 |
+
progress(0.3, desc=f"Request submitted. ID: {request_id}")
|
240 |
+
else:
|
241 |
+
return None, f"β API Error: {response.status_code} - {response.text}"
|
242 |
|
243 |
+
# Poll for result
|
244 |
+
success, result = poll_for_result(request_id, api_key, lambda p, desc: progress(p, desc=desc))
|
245 |
|
246 |
+
if not success:
|
247 |
+
return None, f"β {result}"
|
248 |
|
249 |
+
video_url = result
|
250 |
+
progress(0.9, desc="Downloading video...")
|
|
|
251 |
|
252 |
+
# Download video
|
253 |
+
video_response = requests.get(video_url, timeout=60)
|
254 |
+
if video_response.status_code != 200:
|
255 |
+
return None, f"β Failed to download video from {video_url}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
256 |
|
257 |
+
video_data = video_response.content
|
|
|
|
|
|
|
|
|
|
|
|
|
258 |
|
259 |
# Save to temporary file
|
260 |
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
|
|
|
266 |
final_video_path = temp_video_path
|
267 |
|
268 |
if VIDEO_PROCESSING_AVAILABLE:
|
269 |
+
progress(0.95, desc="Adding watermark...")
|
270 |
final_video_path = tempfile.mktemp(suffix='.mp4')
|
271 |
watermark_added = add_watermark(temp_video_path, final_video_path)
|
272 |
|
|
|
299 |
- Duration: 5 seconds
|
300 |
- Resolution: 480p
|
301 |
- Watermark: {watermark_status}
|
302 |
+
- API: WaveSpeed AI
|
303 |
+
- Cost: $0.06
|
304 |
- File: output.mp4"""
|
305 |
|
306 |
return final_video_path, info
|
307 |
|
308 |
except requests.exceptions.Timeout:
|
309 |
+
return None, "β±οΈ Request timed out. Please try again."
|
310 |
except Exception as e:
|
311 |
+
return None, f"β Error occurred: {str(e)}"
|
|
|
|
|
|
|
312 |
|
313 |
# Gradio interface
|
314 |
with gr.Blocks(title="Bytedance Seedance Video Free", theme=gr.themes.Soft()) as app:
|
315 |
gr.Markdown("""
|
316 |
# π¬ Bytedance Seedance Video' Free
|
317 |
|
318 |
+
Generate videos from text or images using **WaveSpeed AI API**.
|
319 |
|
320 |
+
[](https://ginigen.com/)
|
|
|
321 |
|
322 |
+
β οΈ **Note**: Each generation costs $0.06
|
323 |
+
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
324 |
|
325 |
with gr.Row():
|
326 |
with gr.Column(scale=1):
|
327 |
# API Settings
|
328 |
+
with gr.Accordion("βοΈ API Settings", open=False):
|
329 |
+
gr.Markdown("""
|
330 |
+
API keys are loaded from environment variables:
|
331 |
+
- `WAVESPEED_API_KEY_T2V` for Text-to-Video
|
332 |
+
- `WAVESPEED_API_KEY_I2V` for Image-to-Video
|
333 |
+
|
334 |
+
You can override them below if needed.
|
335 |
+
""")
|
336 |
+
|
337 |
+
api_key_override_t2v = gr.Textbox(
|
338 |
+
label="Text-to-Video API Key (Optional Override)",
|
339 |
+
type="password",
|
340 |
+
placeholder="Leave empty to use environment variable",
|
341 |
+
value=""
|
342 |
+
)
|
343 |
+
|
344 |
+
api_key_override_i2v = gr.Textbox(
|
345 |
+
label="Image-to-Video API Key (Optional Override)",
|
346 |
+
type="password",
|
347 |
+
placeholder="Leave empty to use environment variable",
|
348 |
+
value=""
|
349 |
+
)
|
350 |
|
351 |
# Generation mode
|
352 |
mode = gr.Radio(
|
|
|
357 |
|
358 |
# Image upload
|
359 |
image_input = gr.Image(
|
360 |
+
label="π· Upload Image or Provide URL",
|
361 |
type="pil",
|
362 |
visible=False
|
363 |
)
|
364 |
|
365 |
+
# Aspect ratio (only for T2V)
|
366 |
aspect_ratio = gr.Dropdown(
|
367 |
label="π Aspect Ratio",
|
368 |
choices=list(ASPECT_RATIOS.keys()),
|
|
|
376 |
# Seed setting
|
377 |
seed = gr.Number(
|
378 |
label="π² Random Seed",
|
379 |
+
value=-1,
|
380 |
precision=0,
|
381 |
+
info="Use -1 for random, or set a specific value for reproducible results"
|
382 |
)
|
383 |
|
384 |
# Fixed settings display
|
|
|
387 |
### π Fixed Settings
|
388 |
- **Duration**: 5 seconds
|
389 |
- **Resolution**: 480p
|
390 |
+
- **Cost**: $0.06 per video
|
391 |
- **Watermark**: {watermark_info}
|
392 |
""")
|
393 |
|
|
|
400 |
value=""
|
401 |
)
|
402 |
|
403 |
+
# Generate button
|
404 |
+
generate_btn = gr.Button("π¬ Generate Video ($0.06)", variant="primary", size="lg")
|
|
|
|
|
|
|
|
|
405 |
|
406 |
# Results display
|
407 |
with gr.Column():
|
|
|
408 |
output_video = gr.Video(
|
409 |
label="πΉ Generated Video",
|
410 |
autoplay=True
|
411 |
)
|
412 |
output_info = gr.Textbox(
|
413 |
label="Information",
|
414 |
+
lines=10,
|
415 |
interactive=False
|
416 |
)
|
417 |
|
|
|
422 |
|
423 |
1. **Install required packages**:
|
424 |
```bash
|
425 |
+
pip install gradio requests pillow
|
426 |
```
|
427 |
|
428 |
2. **For watermark support (optional)**:
|
|
|
430 |
pip install opencv-python
|
431 |
```
|
432 |
|
433 |
+
3. **Set environment variables**:
|
434 |
```bash
|
435 |
+
export WAVESPEED_API_KEY_T2V="your-t2v-api-key"
|
436 |
+
export WAVESPEED_API_KEY_I2V="your-i2v-api-key"
|
437 |
```
|
438 |
|
439 |
4. **Run**:
|
|
|
444 |
### Features
|
445 |
|
446 |
- **Text to Video**: Generate video from text description only
|
447 |
+
- **Image to Video**: Transform uploaded image into animated video (requires image URL)
|
448 |
- **Aspect Ratios**: Choose ratios optimized for various social media platforms
|
449 |
+
- **Seed Value**: Use -1 for random or set specific value for reproducible results
|
450 |
- **Watermark**: Automatically adds "ginigen.com" watermark (requires opencv-python)
|
451 |
|
452 |
+
### API Information
|
453 |
+
|
454 |
+
- **Provider**: WaveSpeed AI
|
455 |
+
- **Models**: ByteDance Seedance v1 Lite
|
456 |
+
- **Cost**: $0.06 per 5-second video
|
457 |
+
- **Resolution**: 480p
|
458 |
+
|
459 |
### Prompt Writing Tips
|
460 |
|
461 |
- Use specific and detailed descriptions
|
462 |
- Specify camera movements (e.g., zoom in, pan left, tracking shot)
|
463 |
- Describe lighting and atmosphere (e.g., golden hour, dramatic lighting)
|
464 |
- Indicate movement speed (e.g., slowly, rapidly, gently)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
465 |
""")
|
466 |
|
467 |
# Examples
|
468 |
gr.Examples(
|
469 |
examples=[
|
470 |
+
["Text to Video", "A cat walking gracefully across a sunlit room", None, "16:9", -1],
|
|
|
471 |
["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],
|
472 |
["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],
|
473 |
["Text to Video", "Close-up of a flower blooming in time-lapse, soft natural lighting, shallow depth of field.", None, "1:1", 789],
|
474 |
],
|
475 |
inputs=[mode, prompt, image_input, aspect_ratio, seed],
|
476 |
+
label="Example Prompts"
|
477 |
)
|
478 |
|
479 |
# Event handlers
|
|
|
495 |
outputs=[ratio_info]
|
496 |
)
|
497 |
|
|
|
|
|
|
|
|
|
|
|
|
|
498 |
generate_btn.click(
|
499 |
fn=generate_video,
|
500 |
+
inputs=[mode, prompt, image_input, aspect_ratio, seed, api_key_override_t2v, api_key_override_i2v],
|
501 |
outputs=[output_video, output_info]
|
502 |
)
|
503 |
|