Files
2026-01-30 03:04:10 +00:00

520 lines
16 KiB
Bash
Executable File

#!/bin/bash
# fal.ai Generation Script with Queue Support
# Usage: ./generate.sh --prompt "..." [--model MODEL] [options]
# Returns: JSON with generated media URLs
#
# Queue Mode (default): Submits to queue, polls for completion
# Async Mode: Returns request_id immediately
# Sync Mode: Direct request (not recommended for long tasks)
set -e
FAL_QUEUE_ENDPOINT="https://queue.fal.run"
FAL_SYNC_ENDPOINT="https://fal.run"
FAL_TOKEN_ENDPOINT="https://rest.alpha.fal.ai/storage/auth/token?storage_type=fal-cdn-v3"
# Default values
MODEL="fal-ai/flux/dev"
PROMPT=""
IMAGE_URL=""
IMAGE_FILE=""
IMAGE_SIZE="landscape_4_3"
NUM_IMAGES=1
MODE="queue" # queue (default), async, sync
REQUEST_ID=""
ACTION="generate" # generate, status, result, cancel
POLL_INTERVAL=2
MAX_POLL_TIME=600
LIFECYCLE=""
SHOW_LOGS=false
# Check for --add-fal-key first
for arg in "$@"; do
if [ "$arg" = "--add-fal-key" ]; then
shift
KEY_VALUE=""
if [[ -n "$1" && ! "$1" =~ ^-- ]]; then
KEY_VALUE="$1"
fi
if [ -z "$KEY_VALUE" ]; then
echo "Enter your fal.ai API key:" >&2
read -r KEY_VALUE
fi
if [ -n "$KEY_VALUE" ]; then
grep -v "^FAL_KEY=" .env > .env.tmp 2>/dev/null || true
mv .env.tmp .env 2>/dev/null || true
echo "FAL_KEY=$KEY_VALUE" >> .env
echo "FAL_KEY saved to .env" >&2
fi
exit 0
fi
done
# Load .env if exists
if [ -f ".env" ]; then
source .env 2>/dev/null || true
fi
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--prompt|-p)
PROMPT="$2"
shift 2
;;
--model|-m)
MODEL="$2"
shift 2
;;
--image-url)
IMAGE_URL="$2"
shift 2
;;
--file|--image)
IMAGE_FILE="$2"
shift 2
;;
--size)
case $2 in
square) IMAGE_SIZE="square" ;;
portrait) IMAGE_SIZE="portrait_4_3" ;;
landscape) IMAGE_SIZE="landscape_4_3" ;;
*) IMAGE_SIZE="$2" ;;
esac
shift 2
;;
--num-images)
NUM_IMAGES="$2"
shift 2
;;
# Mode options
--async)
MODE="async"
shift
;;
--sync)
MODE="sync"
shift
;;
--logs)
SHOW_LOGS=true
shift
;;
# Queue operations
--status)
ACTION="status"
REQUEST_ID="$2"
shift 2
;;
--result)
ACTION="result"
REQUEST_ID="$2"
shift 2
;;
--cancel)
ACTION="cancel"
REQUEST_ID="$2"
shift 2
;;
# Polling options
--poll-interval)
POLL_INTERVAL="$2"
shift 2
;;
--timeout)
MAX_POLL_TIME="$2"
shift 2
;;
# Object lifecycle (optional)
--lifecycle)
LIFECYCLE="$2"
shift 2
;;
# Schema lookup
--schema)
SCHEMA_MODEL="${2:-$MODEL}"
ENCODED=$(echo "$SCHEMA_MODEL" | sed 's/\//%2F/g')
echo "Fetching schema for $SCHEMA_MODEL..." >&2
curl -s "https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=$ENCODED"
exit 0
;;
--help|-h)
echo "fal.ai Generation Script (Queue-based)" >&2
echo "" >&2
echo "Usage:" >&2
echo " ./generate.sh --prompt \"...\" [options]" >&2
echo "" >&2
echo "Generation Options:" >&2
echo " --prompt, -p Text description (required for generate)" >&2
echo " --model, -m Model ID (default: fal-ai/flux/dev)" >&2
echo " --image-url Input image URL for I2V models" >&2
echo " --file, --image Local file (auto-uploads to fal CDN)" >&2
echo " --size square, portrait, landscape" >&2
echo " --num-images Number of images (default: 1)" >&2
echo "" >&2
echo "Mode Options:" >&2
echo " (default) Queue mode - submit and poll until complete" >&2
echo " --async Submit to queue, return request_id immediately" >&2
echo " --sync Synchronous request (not recommended for video)" >&2
echo " --logs Show generation logs while polling" >&2
echo "" >&2
echo "Queue Operations:" >&2
echo " --status ID Check status of a queued request" >&2
echo " --result ID Get result of a completed request" >&2
echo " --cancel ID Cancel a queued request" >&2
echo "" >&2
echo "Advanced Options:" >&2
echo " --poll-interval Seconds between status checks (default: 2)" >&2
echo " --timeout Max seconds to wait (default: 600)" >&2
echo " --lifecycle N Object expiration in seconds (optional)" >&2
echo " --schema [MODEL] Get OpenAPI schema for model" >&2
echo " --add-fal-key Setup FAL_KEY in .env" >&2
echo "" >&2
echo "Examples:" >&2
echo " # Generate image (waits for completion)" >&2
echo " ./generate.sh --prompt \"a sunset\" --model \"fal-ai/flux/dev\"" >&2
echo "" >&2
echo " # Generate video async (returns immediately)" >&2
echo " ./generate.sh --prompt \"ocean waves\" --model \"fal-ai/veo3\" --async" >&2
echo "" >&2
echo " # Check status" >&2
echo " ./generate.sh --status \"request_id\" --model \"fal-ai/veo3\"" >&2
echo "" >&2
echo " # Get result" >&2
echo " ./generate.sh --result \"request_id\" --model \"fal-ai/veo3\"" >&2
exit 0
;;
*)
shift
;;
esac
done
# Validate FAL_KEY
if [ -z "$FAL_KEY" ]; then
echo "Error: FAL_KEY not set" >&2
echo "" >&2
echo "Run: ./generate.sh --add-fal-key" >&2
echo "Or: export FAL_KEY=your_key_here" >&2
exit 1
fi
# Handle local file upload
if [ -n "$IMAGE_FILE" ]; then
if [ ! -f "$IMAGE_FILE" ]; then
echo "Error: File not found: $IMAGE_FILE" >&2
exit 1
fi
FILENAME=$(basename "$IMAGE_FILE")
EXTENSION="${FILENAME##*.}"
EXTENSION_LOWER=$(echo "$EXTENSION" | tr '[:upper:]' '[:lower:]')
# Detect content type
case "$EXTENSION_LOWER" in
jpg|jpeg) CONTENT_TYPE="image/jpeg" ;;
png) CONTENT_TYPE="image/png" ;;
gif) CONTENT_TYPE="image/gif" ;;
webp) CONTENT_TYPE="image/webp" ;;
mp4) CONTENT_TYPE="video/mp4" ;;
mov) CONTENT_TYPE="video/quicktime" ;;
*) CONTENT_TYPE="application/octet-stream" ;;
esac
echo "Uploading $FILENAME..." >&2
# Step 1: Get CDN token
TOKEN_RESPONSE=$(curl -s -X POST "$FAL_TOKEN_ENDPOINT" \
-H "Authorization: Key $FAL_KEY" \
-H "Content-Type: application/json" \
-d '{}')
CDN_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
CDN_TOKEN_TYPE=$(echo "$TOKEN_RESPONSE" | grep -o '"token_type":"[^"]*"' | cut -d'"' -f4)
CDN_BASE_URL=$(echo "$TOKEN_RESPONSE" | grep -o '"base_url":"[^"]*"' | cut -d'"' -f4)
if [ -z "$CDN_TOKEN" ] || [ -z "$CDN_BASE_URL" ]; then
echo "Error: Failed to get CDN token" >&2
exit 1
fi
# Step 2: Upload file
UPLOAD_RESPONSE=$(curl -s -X POST "${CDN_BASE_URL}/files/upload" \
-H "Authorization: $CDN_TOKEN_TYPE $CDN_TOKEN" \
-H "Content-Type: $CONTENT_TYPE" \
-H "X-Fal-File-Name: $FILENAME" \
--data-binary "@$IMAGE_FILE")
# Check for upload error
if echo "$UPLOAD_RESPONSE" | grep -q '"error"'; then
ERROR_MSG=$(echo "$UPLOAD_RESPONSE" | grep -o '"message":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Upload error: $ERROR_MSG" >&2
exit 1
fi
# Extract URL
IMAGE_URL=$(echo "$UPLOAD_RESPONSE" | grep -o '"access_url":"[^"]*"' | cut -d'"' -f4)
if [ -z "$IMAGE_URL" ]; then
echo "Error: Failed to get URL from upload response" >&2
exit 1
fi
echo "Uploaded: $IMAGE_URL" >&2
fi
# Build headers
HEADERS=(-H "Authorization: Key $FAL_KEY" -H "Content-Type: application/json")
# Add lifecycle header if specified
if [ -n "$LIFECYCLE" ]; then
HEADERS+=(-H "X-Fal-Object-Lifecycle-Preference: {\"expiration_duration_seconds\": $LIFECYCLE}")
fi
# Handle queue operations
case $ACTION in
status)
if [ -z "$REQUEST_ID" ]; then
echo "Error: Request ID required for --status" >&2
exit 1
fi
LOGS_PARAM=""
if [ "$SHOW_LOGS" = true ]; then
LOGS_PARAM="?logs=1"
fi
echo "Checking status for $REQUEST_ID..." >&2
RESPONSE=$(curl -s -X GET "$FAL_QUEUE_ENDPOINT/$MODEL/requests/$REQUEST_ID/status$LOGS_PARAM" "${HEADERS[@]}")
# Parse and display status
STATUS=$(echo "$RESPONSE" | grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
echo "Status: $STATUS" >&2
if [ "$STATUS" = "IN_QUEUE" ]; then
POSITION=$(echo "$RESPONSE" | grep -o '"queue_position":[0-9]*' | cut -d':' -f2)
[ -n "$POSITION" ] && echo "Queue position: $POSITION" >&2
fi
echo "$RESPONSE"
exit 0
;;
result)
if [ -z "$REQUEST_ID" ]; then
echo "Error: Request ID required for --result" >&2
exit 1
fi
echo "Getting result for $REQUEST_ID..." >&2
RESPONSE=$(curl -s -X GET "$FAL_QUEUE_ENDPOINT/$MODEL/requests/$REQUEST_ID" "${HEADERS[@]}")
# Check for error
if echo "$RESPONSE" | grep -q '"error"'; then
ERROR_MSG=$(echo "$RESPONSE" | grep -o '"message":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Error: $ERROR_MSG" >&2
exit 1
fi
# Extract URL
if echo "$RESPONSE" | grep -q '"video"'; then
URL=$(echo "$RESPONSE" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Video URL: $URL" >&2
elif echo "$RESPONSE" | grep -q '"images"'; then
URL=$(echo "$RESPONSE" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Image URL: $URL" >&2
fi
echo "$RESPONSE"
exit 0
;;
cancel)
if [ -z "$REQUEST_ID" ]; then
echo "Error: Request ID required for --cancel" >&2
exit 1
fi
echo "Cancelling request $REQUEST_ID..." >&2
RESPONSE=$(curl -s -X PUT "$FAL_QUEUE_ENDPOINT/$MODEL/requests/$REQUEST_ID/cancel" "${HEADERS[@]}")
echo "$RESPONSE"
exit 0
;;
esac
# Generate action requires prompt
if [ -z "$PROMPT" ]; then
echo "Error: --prompt is required" >&2
exit 1
fi
# Build the request payload based on model type
if [[ "$MODEL" == *"image-to-video"* ]] || [[ "$MODEL" == *"i2v"* ]]; then
if [ -z "$IMAGE_URL" ]; then
echo "Error: --image-url is required for image-to-video models" >&2
exit 1
fi
PAYLOAD=$(cat <<EOF
{"prompt": "$PROMPT", "image_url": "$IMAGE_URL"}
EOF
)
elif [[ "$MODEL" == *"video"* ]] || [[ "$MODEL" == *"veo"* ]] || [[ "$MODEL" == *"text-to-video"* ]]; then
PAYLOAD=$(cat <<EOF
{"prompt": "$PROMPT"}
EOF
)
else
PAYLOAD=$(cat <<EOF
{"prompt": "$PROMPT", "image_size": "$IMAGE_SIZE", "num_images": $NUM_IMAGES}
EOF
)
fi
# Synchronous mode
if [ "$MODE" = "sync" ]; then
echo "Generating with $MODEL (sync mode)..." >&2
RESPONSE=$(curl -s -X POST "$FAL_SYNC_ENDPOINT/$MODEL" "${HEADERS[@]}" -d "$PAYLOAD")
if echo "$RESPONSE" | grep -q '"error"'; then
ERROR_MSG=$(echo "$RESPONSE" | grep -o '"message":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Error: $ERROR_MSG" >&2
exit 1
fi
echo "Generation complete!" >&2
if echo "$RESPONSE" | grep -q '"video"'; then
URL=$(echo "$RESPONSE" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Video URL: $URL" >&2
elif echo "$RESPONSE" | grep -q '"images"'; then
URL=$(echo "$RESPONSE" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Image URL: $URL" >&2
fi
echo "$RESPONSE"
exit 0
fi
# Queue mode (async or poll)
echo "Submitting to queue: $MODEL..." >&2
SUBMIT_RESPONSE=$(curl -s -X POST "$FAL_QUEUE_ENDPOINT/$MODEL" "${HEADERS[@]}" -d "$PAYLOAD")
# Check for submit error
if echo "$SUBMIT_RESPONSE" | grep -q '"error"'; then
ERROR_MSG=$(echo "$SUBMIT_RESPONSE" | grep -o '"message":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Error: $ERROR_MSG" >&2
exit 1
fi
# Extract request_id and URLs (handle both "key": "value" and "key":"value" formats)
REQUEST_ID=$(echo "$SUBMIT_RESPONSE" | grep -oE '"request_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
STATUS_URL=$(echo "$SUBMIT_RESPONSE" | grep -oE '"status_url"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
RESPONSE_URL=$(echo "$SUBMIT_RESPONSE" | grep -oE '"response_url"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
CANCEL_URL=$(echo "$SUBMIT_RESPONSE" | grep -oE '"cancel_url"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
if [ -z "$REQUEST_ID" ]; then
echo "Error: Failed to get request_id" >&2
echo "$SUBMIT_RESPONSE" >&2
exit 1
fi
echo "Request ID: $REQUEST_ID" >&2
# Async mode - return immediately
if [ "$MODE" = "async" ]; then
echo "" >&2
echo "Request submitted. Use these commands to check:" >&2
echo " Status: ./generate.sh --status \"$REQUEST_ID\" --model \"$MODEL\"" >&2
echo " Result: ./generate.sh --result \"$REQUEST_ID\" --model \"$MODEL\"" >&2
echo " Cancel: ./generate.sh --cancel \"$REQUEST_ID\" --model \"$MODEL\"" >&2
echo "$SUBMIT_RESPONSE"
exit 0
fi
# Queue mode - poll until complete
echo "Waiting for completion..." >&2
ELAPSED=0
LAST_STATUS=""
while [ $ELAPSED -lt $MAX_POLL_TIME ]; do
sleep $POLL_INTERVAL
ELAPSED=$((ELAPSED + POLL_INTERVAL))
LOGS_PARAM=""
if [ "$SHOW_LOGS" = true ]; then
LOGS_PARAM="?logs=1"
fi
STATUS_RESPONSE=$(curl -s -X GET "${STATUS_URL}${LOGS_PARAM}" "${HEADERS[@]}")
STATUS=$(echo "$STATUS_RESPONSE" | grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//' | sed 's/"$//')
# Show status change
if [ "$STATUS" != "$LAST_STATUS" ]; then
case $STATUS in
IN_QUEUE)
POSITION=$(echo "$STATUS_RESPONSE" | grep -o '"queue_position":[0-9]*' | cut -d':' -f2)
echo "Status: IN_QUEUE (position: ${POSITION:-?})" >&2
;;
IN_PROGRESS)
echo "Status: IN_PROGRESS" >&2
;;
COMPLETED)
echo "Status: COMPLETED" >&2
;;
*)
echo "Status: $STATUS" >&2
;;
esac
LAST_STATUS="$STATUS"
fi
# Show logs if enabled
if [ "$SHOW_LOGS" = true ]; then
LOGS=$(echo "$STATUS_RESPONSE" | grep -o '"logs":\[[^]]*\]' | head -1)
if [ -n "$LOGS" ] && [ "$LOGS" != "[]" ]; then
echo "$LOGS" | tr ',' '\n' | grep -o '"message":"[^"]*"' | cut -d'"' -f4 | while read -r log; do
echo " > $log" >&2
done
fi
fi
if [ "$STATUS" = "COMPLETED" ]; then
break
fi
if [ "$STATUS" = "FAILED" ]; then
echo "Error: Generation failed" >&2
echo "$STATUS_RESPONSE"
exit 1
fi
done
if [ "$STATUS" != "COMPLETED" ]; then
echo "Error: Timeout after ${MAX_POLL_TIME}s" >&2
echo "Request ID: $REQUEST_ID" >&2
echo "Check status with: ./generate.sh --status \"$REQUEST_ID\" --model \"$MODEL\"" >&2
exit 1
fi
# Get final result
echo "Fetching result..." >&2
RESULT=$(curl -s -X GET "$RESPONSE_URL" "${HEADERS[@]}")
# Check for error in result
if echo "$RESULT" | grep -q '"error"'; then
ERROR_MSG=$(echo "$RESULT" | grep -o '"message":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Error: $ERROR_MSG" >&2
exit 1
fi
echo "" >&2
echo "Generation complete!" >&2
# Extract and display URL
if echo "$RESULT" | grep -q '"video"'; then
URL=$(echo "$RESULT" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Video URL: $URL" >&2
elif echo "$RESULT" | grep -q '"images"'; then
URL=$(echo "$RESULT" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "Image URL: $URL" >&2
fi
# Output JSON
echo "$RESULT"