#!/usr/bin/env bash # Generate a video with Veo 3.1 using first and last frame interpolation. # Uploads frames via File API, submits to predictLongRunning, polls, downloads. # # Usage: # generate-video.sh [output.mp4] [duration] # # Env: GOOGLE_AI_API_KEY must be set (or sourced from .env.local) set -euo pipefail PROMPT="$1" FIRST_FRAME="$2" LAST_FRAME="$3" OUTPUT="${4:-output/video.mp4}" DURATION="${5:-8}" PROJECT_DIR="${AI_STUDIO_DIR:-/mnt/work/dev/ai-studio-videos}" if [[ -z "${GOOGLE_AI_API_KEY:-}" ]] && [[ -f "$PROJECT_DIR/.env.local" ]]; then source "$PROJECT_DIR/.env.local" fi BASE_URL="https://generativelanguage.googleapis.com/v1beta" TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT mkdir -p "$(dirname "$OUTPUT")" FIRST_MIME=$(file -b --mime-type "$FIRST_FRAME") LAST_MIME=$(file -b --mime-type "$LAST_FRAME") echo "Encoding frames..." base64 -w0 "$FIRST_FRAME" > "$TMPDIR/first.b64" base64 -w0 "$LAST_FRAME" > "$TMPDIR/last.b64" echo "Building request..." echo " Duration: ${DURATION}s" echo " First: $FIRST_FRAME ($FIRST_MIME)" echo " Last: $LAST_FRAME ($LAST_MIME)" # Veo predictLongRunning uses bytesBase64Encoded (not inlineData) # lastFrame goes inside instances[0] (not parameters) jq -n \ --arg prompt "$PROMPT" \ --rawfile first_b64 "$TMPDIR/first.b64" \ --arg first_mime "$FIRST_MIME" \ --rawfile last_b64 "$TMPDIR/last.b64" \ --arg last_mime "$LAST_MIME" \ --arg duration "$DURATION" \ '{ instances: [{ prompt: $prompt, image: { bytesBase64Encoded: $first_b64, mimeType: $first_mime }, lastFrame: { bytesBase64Encoded: $last_b64, mimeType: $last_mime } }], parameters: { durationSeconds: ($duration | tonumber), aspectRatio: "16:9" } }' > "$TMPDIR/request.json" echo " Payload: $(du -h "$TMPDIR/request.json" | cut -f1)" echo "Submitting to Veo 3.1..." OPERATION=$(curl -s \ "${BASE_URL}/models/veo-3.1-generate-preview:predictLongRunning" \ -H "x-goog-api-key: ${GOOGLE_AI_API_KEY}" \ -H "Content-Type: application/json" \ -X POST \ -d @"$TMPDIR/request.json") ERROR=$(echo "$OPERATION" | jq -r '.error.message // empty') if [[ -n "$ERROR" ]]; then echo "API Error: $ERROR" >&2; exit 1 fi OPERATION_NAME=$(echo "$OPERATION" | jq -r '.name') if [[ -z "$OPERATION_NAME" || "$OPERATION_NAME" == "null" ]]; then echo "No operation name returned." >&2 echo "$OPERATION" | jq . >&2; exit 1 fi echo "Operation: $OPERATION_NAME" echo "Polling..." while true; do STATUS=$(curl -s \ -H "x-goog-api-key: ${GOOGLE_AI_API_KEY}" \ "${BASE_URL}/${OPERATION_NAME}") IS_DONE=$(echo "$STATUS" | jq -r '.done // false') if [[ "$IS_DONE" == "true" ]]; then OP_ERROR=$(echo "$STATUS" | jq -r '.error.message // empty') if [[ -n "$OP_ERROR" ]]; then echo "Generation failed: $OP_ERROR" >&2; exit 1 fi VIDEO_URI=$(echo "$STATUS" | jq -r \ '.response.generateVideoResponse.generatedSamples[0].video.uri') if [[ -z "$VIDEO_URI" || "$VIDEO_URI" == "null" ]]; then echo "No video URI." >&2 echo "$STATUS" | jq . >&2; exit 1 fi echo "Downloading..." curl -sL -o "$OUTPUT" \ -H "x-goog-api-key: ${GOOGLE_AI_API_KEY}" \ "$VIDEO_URI" echo "Saved: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" break fi echo " Rendering... (10s)" sleep 10 done