Skip to content

Commit 7f6b358

Browse files
uzunenescursoragent
andcommitted
feat: add make_video_and_gif.py, beach_waves.gif placeholder, README video/GIF section
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0f79806 commit 7f6b358

3 files changed

Lines changed: 160 additions & 2 deletions

File tree

examples/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Requires the SDK to be installed; `VISGATE_API_KEY` is required for auth steps.
9999
- **Purpose:** Video generation; may return async job; optional wait and download.
100100
- **API/SDK:** `POST /videos/generate`.
101101
- **Command:** `python examples/04_videos_all_providers.py`
102-
- **Output:** [out_04_videos_all_providers.txt](sample_outputs/out_04_videos_all_providers.txt) · Video: [beach_waves.mp4](sample_outputs/beach_waves.mp4) (generate with script or `fetch_sample_outputs.py`)
102+
- **Output:** [out_04_videos_all_providers.txt](sample_outputs/out_04_videos_all_providers.txt) · Video: [beach_waves.mp4](sample_outputs/beach_waves.mp4) · GIF: ![beach_waves](sample_outputs/beach_waves.gif) (generate with `make_video_and_gif.py`)
103103

104104
---
105105

@@ -176,7 +176,7 @@ Requires the SDK to be installed; `VISGATE_API_KEY` is required for auth steps.
176176
| 01_models_catalog | [out_01_models_catalog.txt](sample_outputs/out_01_models_catalog.txt) ||
177177
| 02 generate_unified | [out_02_generate_unified.txt](sample_outputs/out_02_generate_unified.txt) | ![generate_unified](sample_outputs/generate_unified.jpg) |
178178
| 03 images | [out_03_images_all_providers.txt](sample_outputs/out_03_images_all_providers.txt) | ![sunset](sample_outputs/sunset_istanbul.jpg) ![cat](sample_outputs/cat_astronaut.jpg) ![cappadocia](sample_outputs/cappadocia_balloons.jpg) |
179-
| 04 videos | [out_04_videos_all_providers.txt](sample_outputs/out_04_videos_all_providers.txt) | [beach_waves.mp4](sample_outputs/beach_waves.mp4) |
179+
| 04 videos | [out_04_videos_all_providers.txt](sample_outputs/out_04_videos_all_providers.txt) | [beach_waves.mp4](sample_outputs/beach_waves.mp4) · ![beach_waves](sample_outputs/beach_waves.gif) |
180180
| 05 usage | [out_05_usage_history_verify.txt](sample_outputs/out_05_usage_history_verify.txt) ||
181181
| 06 provider_balances | [out_06_provider_balances.txt](sample_outputs/out_06_provider_balances.txt) ||
182182
| 07 cache_demo | [out_07_cache_demo.txt](sample_outputs/out_07_cache_demo.txt) | ![cache_demo](sample_outputs/cache_demo.jpg) |
@@ -196,6 +196,17 @@ python fetch_sample_outputs.py
196196

197197
Requires `VISGATE_API_KEY`. The script waits for video when the API returns async (no skip).
198198

199+
### Video and GIF (beach_waves)
200+
201+
To generate the sample video and a GIF preview:
202+
203+
```bash
204+
cd examples
205+
VISGATE_API_KEY=vg-... python make_video_and_gif.py
206+
```
207+
208+
This produces `sample_outputs/beach_waves.mp4` and `sample_outputs/beach_waves.gif`. GIF conversion uses `ffmpeg` if available, or `pip install imageio imageio-ffmpeg` as fallback. To convert an existing MP4 only: `python make_video_and_gif.py --mp4 sample_outputs/beach_waves.mp4`.
209+
199210
## Cache behavior
200211

201212
- **Exact cache (07):** Same model + prompt + size. Second request returns from cache; response includes `cache_hit=True` and `provider_cost_avoided_micro`.

examples/make_video_and_gif.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate one sample video (beach_waves) and convert it to GIF.
4+
Saves to examples/sample_outputs/beach_waves.mp4 and beach_waves.gif.
5+
6+
Requires: VISGATE_API_KEY. For GIF: ffmpeg (preferred) or pip install imageio imageio-ffmpeg.
7+
8+
cd examples && python make_video_and_gif.py
9+
"""
10+
from __future__ import annotations
11+
12+
import shutil
13+
import subprocess
14+
import sys
15+
import time
16+
from pathlib import Path
17+
18+
import httpx
19+
20+
# Allow running from repo root or examples/
21+
sys.path.insert(0, str(Path(__file__).resolve().parent))
22+
from _common import create_client
23+
24+
OUTPUT_DIR = Path(__file__).resolve().parent / "sample_outputs"
25+
VIDEO_NAME = "beach_waves"
26+
VIDEO_PROMPT = "Waves crashing on a quiet beach at sunset, cinematic, 4k"
27+
VIDEO_DURATION = 4.0
28+
POLL_SLEEP = 25
29+
POLL_ATTEMPTS = 6
30+
31+
# GIF: fps and max duration to keep size reasonable
32+
GIF_FPS = 8
33+
GIF_MAX_DURATION_SEC = 4.0
34+
35+
36+
def download_url(url: str, path: Path, timeout: float = 120.0) -> None:
37+
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
38+
r = client.get(url)
39+
r.raise_for_status()
40+
path.write_bytes(r.content)
41+
42+
43+
def convert_ffmpeg(mp4_path: Path, gif_path: Path) -> bool:
44+
ffmpeg = shutil.which("ffmpeg")
45+
if not ffmpeg:
46+
return False
47+
# Scale down and limit duration for smaller GIF
48+
cmd = [
49+
ffmpeg,
50+
"-y",
51+
"-i", str(mp4_path),
52+
"-vf", f"fps={GIF_FPS},scale=480:-1:flags=lanczos",
53+
"-t", str(GIF_MAX_DURATION_SEC),
54+
str(gif_path),
55+
]
56+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
57+
return result.returncode == 0 and gif_path.exists()
58+
59+
60+
def convert_imageio(mp4_path: Path, gif_path: Path) -> bool:
61+
try:
62+
import imageio
63+
except ImportError:
64+
return False
65+
try:
66+
reader = imageio.get_reader(mp4_path)
67+
meta = reader.get_meta_data()
68+
fps = meta.get("fps") or GIF_FPS
69+
duration_ms = 1000.0 / fps
70+
frames = []
71+
for i, frame in enumerate(reader):
72+
if i >= int(GIF_MAX_DURATION_SEC * fps):
73+
break
74+
frames.append(frame)
75+
reader.close()
76+
if not frames:
77+
return False
78+
imageio.mimsave(gif_path, frames, duration=duration_ms, loop=0)
79+
return True
80+
except Exception:
81+
return False
82+
83+
84+
def main() -> int:
85+
import argparse
86+
parser = argparse.ArgumentParser(description="Generate beach_waves video + GIF or convert existing MP4 to GIF")
87+
parser.add_argument("--mp4", type=Path, help="Existing MP4 path; if set, only convert to GIF (no API call)")
88+
args = parser.parse_args()
89+
90+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
91+
mp4_path = args.mp4 if args.mp4 else (OUTPUT_DIR / f"{VIDEO_NAME}.mp4")
92+
gif_path = OUTPUT_DIR / f"{mp4_path.stem}.gif"
93+
94+
if args.mp4:
95+
if not args.mp4.exists():
96+
print(f"File not found: {args.mp4}")
97+
return 1
98+
print(f"Converting {args.mp4} -> {gif_path}")
99+
if convert_ffmpeg(mp4_path, gif_path):
100+
print(f" Saved {gif_path} (ffmpeg)")
101+
elif convert_imageio(mp4_path, gif_path):
102+
print(f" Saved {gif_path} (imageio)")
103+
else:
104+
print(" Install ffmpeg or: pip install imageio imageio-ffmpeg")
105+
return 1
106+
return 0
107+
108+
with create_client() as client:
109+
print(f"Generating video '{VIDEO_NAME}' (up to {POLL_ATTEMPTS * POLL_SLEEP}s wait) ...")
110+
for attempt in range(POLL_ATTEMPTS):
111+
try:
112+
vresult = client.videos.generate(
113+
model="fal-ai/flux-pro/video",
114+
prompt=VIDEO_PROMPT,
115+
duration_seconds=VIDEO_DURATION,
116+
)
117+
if vresult.video_url:
118+
download_url(vresult.video_url, mp4_path, timeout=120.0)
119+
print(f" Saved {mp4_path}")
120+
break
121+
except Exception as e:
122+
print(f" Attempt {attempt + 1}: {e}")
123+
if attempt < POLL_ATTEMPTS - 1:
124+
print(f" Sleeping {POLL_SLEEP}s ...")
125+
time.sleep(POLL_SLEEP)
126+
else:
127+
print(" Video URL not ready. Run again later or run fetch_sample_outputs.py.")
128+
return 1
129+
130+
if not mp4_path.exists():
131+
return 1
132+
133+
print("Converting to GIF ...")
134+
if convert_ffmpeg(mp4_path, gif_path):
135+
print(f" Saved {gif_path} (ffmpeg)")
136+
elif convert_imageio(mp4_path, gif_path):
137+
print(f" Saved {gif_path} (imageio)")
138+
else:
139+
print(" GIF conversion skipped: install ffmpeg or pip install imageio imageio-ffmpeg")
140+
return 1
141+
142+
print(f"\nDone: {mp4_path}, {gif_path}")
143+
return 0
144+
145+
146+
if __name__ == "__main__":
147+
sys.exit(main())
487 Bytes
Loading

0 commit comments

Comments
 (0)