Examples · Programming
A video clip from Python
Ask for a clip, check on it until it is ready, and download the MP4. Then a storyboard of three shots, made one after the other and joined with ffmpeg.
Intermediate · 40 min
What you need
A key, as in Your first API call in Python, and the requests library:
pip install requests
For the exercise, ffmpeg, to join clips.
The model
wan-2.2-5b-fast, the Fast model of the Studio: from a description to a 5-second MP4 without sound, 24 frames a second, 1280x704 (landscape) or 704x1280 (portrait). About two minutes a clip.
Video models are not listed by GET /v1/models: the documentation lists them.
Three requests, not one
A chat answer arrives in a few seconds, on the request you sent. A clip takes two minutes: too long to keep a request open. So the API gives you a job:
POST /v1/videoscreates the job and answers at once, with itsid;GET /v1/videos/{id}says how it is going:queued,in_progress, thencompletedorfailed;GET /v1/videos/{id}/contentdownloads the MP4, once the job iscompleted.
The program
Save it as clip.py:
import os
import time
import requests
API = "https://api.ai.itsincom.org/v1"
# The same key you use for the chat models, in a header.
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
def make_clip(prompt, path, seed=None):
# 1. Create the job. The answer comes back at once, without the video:
# it only says that the job exists, with an id to ask about it later.
body = {"model": "wan-2.2-5b-fast", "prompt": prompt, "size": "1280x704"}
if seed is not None:
body["seed"] = seed # the documentation: same seed, prompt and size, same clip
r = requests.post(f"{API}/videos", headers=HEADERS, json=body, timeout=60)
if r.status_code != 200:
# A refused prompt, or a second job while the first is not done:
# the message says what went wrong.
raise RuntimeError(r.json()["error"]["message"])
job = r.json()
print("job", job["id"])
# 2. Poll: ask how the job is going until it is completed or failed.
# A clip takes about two minutes: every ten seconds is plenty.
while job["status"] in ("queued", "in_progress"):
time.sleep(10)
job = requests.get(f"{API}/videos/{job['id']}", headers=HEADERS, timeout=30).json()
# "\r" goes back to the start of the line: the progress stays on one line.
print(f"\r{job['status']} {job['progress']}% ", end="", flush=True)
print()
if job["status"] == "failed":
# A failed clip costs nothing. The first line of the message says why.
raise RuntimeError(f"{job['error']['code']}: {job['error']['message'].splitlines()[0]}")
# 3. Download the MP4: a separate request, now that the clip exists.
# A clip is one or two MB, so it fits in memory in one go.
r = requests.get(f"{API}/videos/{job['id']}/content", headers=HEADERS, timeout=300)
r.raise_for_status()
with open(path, "wb") as f:
f.write(r.content)
print("saved", path)
return job["id"]
# This part runs only when you start clip.py itself. When another program
# imports make_clip, as in the exercise below, no balloon is made.
if __name__ == "__main__":
make_clip(
"A red hot-air balloon rises slowly above green hills and a small lake, "
"at sunrise, warm golden light, light mist over the water, "
"wide shot from the ground, static camera",
"balloon.mp4",
)
Run it with python3 clip.py.
What you should see
job video_01m49rdhg1fr6zzsrmc3wavewd
completed 100%
saved balloon.mp4
While the clip is made, the middle line changes in place: in_progress 0%, in_progress 12%, in_progress 19%… The progress is an estimate from the time a clip usually takes, and it waits at 95% until the clip is really done. When we ran it, it took 123 seconds from the request to the file.
The clip showed the red balloon, the hills, the lake with its mist and the golden light. As in the Studio, the camera was a wish: the view came from the air, not from the ground, and it drifted slowly towards the balloon instead of standing still.
Check the file with ffprobe, which comes with ffmpeg:
ffprobe -v error -show_entries stream=width,height,r_frame_rate,nb_frames:format=duration -of default=nw=1 balloon.mp4
width=1280
height=704
r_frame_rate=24/1
nb_frames=121
duration=5.041667
121 frames at 24 a second: five seconds. The file also says where it comes from:
ffprobe -v error -show_entries format_tags=comment -of default=nw=1 balloon.mp4
TAG:comment=Generated by artificial intelligence with ITS INCOM AI (EU AI Act, art. 50).
Delete it from the server
A clip stays on the server for 7 days, and it shows up in the Studio too, under Your videos. When you have the file, you can delete it there. Save this as delete.py:
import sys
import requests
from clip import API, HEADERS # the address and the key, from clip.py
video_id = sys.argv[1] # the id that clip.py printed
r = requests.delete(f"{API}/videos/{video_id}", headers=HEADERS, timeout=30)
print(r.json())
python3 delete.py video_01m49rdhg1fr6zzsrmc3wavewd
{'id': 'video_01m49rdhg1fr6zzsrmc3wavewd', 'object': 'video.deleted', 'deleted': True}
Run it again and the job is gone:
{'error': {'message': "No video 'video_01m49rdhg1fr6zzsrmc3wavewd' among yours.", 'type': 'invalid_request_error', 'param': None, 'code': 'video_not_found'}}
Exercise: a storyboard in three shots
A clip is one shot. A story is several shots, one after the other. Since you can have only one job at a time, a storyboard is a loop: each request starts when the previous clip is saved. Save it as storyboard.py, in the same folder as clip.py:
import time
from clip import make_clip # the function from clip.py, in the same folder
# One sentence per shot. The model does not remember the previous clip,
# so every shot describes the robot again, with the same words.
shots = [
"A small round white robot with one blue eye rolls down a ramp from a spaceship "
"onto red desert sand, at sunset, warm orange light, wide shot",
"Close-up of a small round white robot with one blue eye looking at a glowing "
"green crystal half buried in red sand, at sunset, warm orange light",
"A small round white robot with one blue eye rolls across red desert sand "
"towards a spaceship, at night under two moons, cold blue light, wide shot",
]
# One clip at a time: you can have only one job running, so each request
# starts when the previous clip is saved. Three clips take about six minutes.
for n, shot in enumerate(shots, 1):
print(f"shot {n} of {len(shots)}")
for attempt in (1, 2):
try:
make_clip(shot, f"shot-{n}.mp4", seed=7)
break
except RuntimeError as e:
# A clip can fail on the machine: try it once more, a minute later.
print("failed:", e)
if attempt == 2:
raise
time.sleep(60)
# The list of the files, in order, for ffmpeg.
with open("shots.txt", "w") as f:
for n in range(1, len(shots) + 1):
f.write(f"file 'shot-{n}.mp4'\n")
print("Join them: ffmpeg -f concat -i shots.txt -c copy storyboard.mp4")
Run it with python3 storyboard.py, then join the three clips into one video of 15 seconds:
ffmpeg -f concat -i shots.txt -c copy storyboard.mp4
-c copy puts the clips one after the other without encoding them again: it takes a second, and it works because the three clips have the same size and format.
The model does not know that the shots belong together: each clip is made from its own sentence. That is why every shot describes the robot again, with the same words. The seed makes a shot repeatable: the documentation says that the same seed, prompt and size give the same clip, file for file.
Now make it yours:
- Write three shots for the opening of your game, or the trailer of your project.
- Watch the three shots: is it the same robot every time? Describe it in more detail, with the same words in every shot: the shape, the colour of the eye, wheels or legs. Does it look more alike?
- Change only the second shot and run the program again. Do the first and the third come back the same?
- Add a
sizeparameter tomake_clipand make the storyboard in704x1280, for a phone screen.
Good to know
- One job at a time. A second request while the first is queued or in progress gets a
429, andmake_clipstops withYou already have a video in the making (video_…). Wait for it to finish, then ask for the next one.A clip you started in the Studio counts too. - Do not ask too often. The job is brought up to date every five seconds or so, and while it is queued or in progress every answer carries the header
openai-poll-after-ms: 2000: wait at least two seconds between two questions. For a clip of two minutes, every ten seconds is plenty. - A clip can fail on the machine. The job ends
failed, witherror.codegeneration_failed, and costs nothing. It happened to us once, on a clip asked a minute after the previous one was done: that is why the storyboard tries a shot again. - Refused descriptions: real people, nudity, blood, hate, famous characters and brands get a
400with the reason, before anything is made. The finished clip is checked too: if it fails, the job endsfailedand costs nothing. - A clip costs 0.10 credits, charged only when it is completed.
- Files are kept 7 days. After that the job is still listed, but its content answers
404: download what you want to keep. - OpenAI's library has
client.videos:create_and_poll()created, followed and reported our job, anddownload_content()downloaded the same file asclip.py. But the library marks those methods as deprecated, because OpenAI is closing its own video service. The plain HTTP requests of this page do not depend on it.
