Created
October 24, 2025 13:59
-
-
Save roelven/0b99791d8794251161cf04a9308339bd to your computer and use it in GitHub Desktop.
Generate Sora 2 videos by calling the API directly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Simple CLI for OpenAI Sora 2 video generation. | |
| # Requirements: bash, curl, jq | |
| # Usage examples at bottom. | |
| set -euo pipefail | |
| API_BASE="${OPENAI_API_BASE:-https://api.openai.com}" | |
| API_KEY="YOUR_API_KEY" | |
| MODEL="${MODEL:-sora-2}" # or "sora-2-pro" | |
| PROMPT="${PROMPT:-}" | |
| SIZE="${SIZE:-1280x720}" # e.g. 1280x720, 720x1280, 1792x1024 | |
| SECONDS="${SECONDS:-8}" # typical allowed values: 4, 8, 12 (per docs) | |
| REF_IMAGE="${REF_IMAGE:-}" # optional: path to a JPEG/PNG/WebP; should match SIZE | |
| OUT="${OUT:-sora_output.mp4}" # output filename | |
| POLL_INTERVAL="${POLL_INTERVAL:-8}" | |
| if [[ -z "$API_KEY" ]]; then | |
| echo "ERROR: Set OPENAI_API_KEY in your environment." >&2 | |
| exit 1 | |
| fi | |
| if [[ -z "$PROMPT" ]]; then | |
| echo "ERROR: Provide a prompt via PROMPT='your text' sora2.sh" >&2 | |
| exit 1 | |
| fi | |
| # 1) Create video job (multipart to support optional image reference) | |
| echo "Submitting video job..." | |
| CREATE_URL="${API_BASE}/v1/videos" | |
| # Build -F arguments | |
| FORM=(-F "model=${MODEL}" -F "prompt=${PROMPT}" -F "size=${SIZE}" -F "seconds=${SECONDS}") | |
| if [[ -n "${REF_IMAGE}" ]]; then | |
| # Best effort to infer mime; override with REF_MIME if needed. | |
| EXT="${REF_IMAGE##*.}" | |
| case "${REF_MIME:-$EXT}" in | |
| jpg|jpeg|image/jpeg) MIME="image/jpeg" ;; | |
| png|image/png) MIME="image/png" ;; | |
| webp|image/webp) MIME="image/webp" ;; | |
| *) MIME="application/octet-stream" ;; | |
| esac | |
| FORM+=(-F "input_reference=@${REF_IMAGE};type=${MIME}") | |
| fi | |
| CREATE_RESP="$(curl -sS -X POST "$CREATE_URL" \ | |
| -H "Authorization: Bearer ${API_KEY}" \ | |
| -H "Accept: application/json" \ | |
| -H "Expect:" \ | |
| -H "Connection: keep-alive" \ | |
| -F "model=${MODEL}" \ | |
| -F "prompt=${PROMPT}" \ | |
| -F "size=${SIZE}" \ | |
| -F "seconds=${SECONDS}" \ | |
| ${REF_IMAGE:+-F "input_reference=@${REF_IMAGE};type=${MIME}"} )" | |
| if [[ -z "$CREATE_RESP" ]]; then | |
| echo "ERROR: empty response creating video job" >&2 | |
| exit 1 | |
| fi | |
| echo "$CREATE_RESP" | jq . >/dev/null || { echo "ERROR: non-JSON response:"; echo "$CREATE_RESP"; exit 1; } | |
| VIDEO_ID="$(echo "$CREATE_RESP" | jq -r '.id')" | |
| STATUS="$(echo "$CREATE_RESP" | jq -r '.status')" | |
| if [[ "$VIDEO_ID" == "null" || -z "$VIDEO_ID" ]]; then | |
| echo "ERROR: Could not find video id in response:" | |
| echo "$CREATE_RESP" | |
| exit 1 | |
| fi | |
| echo "Video ID: $VIDEO_ID (status: $STATUS)" | |
| # 2) Poll for completion | |
| RETRIEVE_URL="${API_BASE}/v1/videos/${VIDEO_ID}" | |
| while true; do | |
| RESP="$(curl -sS -X GET "$RETRIEVE_URL" \ | |
| -H "Authorization: Bearer ${API_KEY}" \ | |
| -H "Accept: application/json")" | |
| STATUS="$(echo "$RESP" | jq -r '.status')" | |
| PROG="$(echo "$RESP" | jq -r '.progress // 0')" | |
| echo "Status: $STATUS Progress: ${PROG}%" | |
| case "$STATUS" in | |
| completed) break ;; | |
| failed|canceled) | |
| echo "Job ended with status: $STATUS" | |
| echo "$RESP" | jq . | |
| exit 2 | |
| ;; | |
| queued|in_progress) | |
| sleep "$POLL_INTERVAL" | |
| ;; | |
| *) | |
| echo "Unexpected status: $STATUS" | |
| echo "$RESP" | jq . | |
| sleep "$POLL_INTERVAL" | |
| ;; | |
| esac | |
| done | |
| # 3) Download MP4 content | |
| CONTENT_URL="${API_BASE}/v1/videos/${VIDEO_ID}/content" | |
| echo "Downloading video content to ${OUT} ..." | |
| curl -sS -L "$CONTENT_URL" \ | |
| -H "Authorization: Bearer ${API_KEY}" \ | |
| --output "$OUT" | |
| # (Optional) also fetch a thumbnail | |
| # THUMB_URL="${CONTENT_URL}?variant=thumbnail" | |
| # curl -sS -L "$THUMB_URL" -H "Authorization: Bearer ${API_KEY}" --output "${OUT%.*}.webp" | |
| echo "Done. File saved: $OUT" | |
| echo "Tip: delete remote artifact when you’re done:" | |
| echo "curl -X DELETE \"${API_BASE}/v1/videos/${VIDEO_ID}\" -H \"Authorization: Bearer ${API_KEY}\" | jq ." | |
| # --- Example usage --- | |
| # PROMPT="Wide shot of a child flying a red kite across a grassy park, soft golden-hour light, cinematic" \ | |
| # SIZE=1280x720 SECONDS=8 OUT=park.mp4 bash sora2.sh | |
| # | |
| # With image reference (must match SIZE): | |
| # PROMPT="Animate this scene so the kite lifts and the camera slowly dollies right" \ | |
| # SIZE=1280x720 SECONDS=8 REF_IMAGE=./first_frame_1280x720.jpg OUT=guided.mp4 bash sora2.sh | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment