
Fal Ai Image
- 34 installs
- 177 repo stars
- Updated May 10, 2026
- artwist-polyakov/polyakov-claude-skills
fal-ai-image is a Claude skill that generates and edits images through the fal.ai API using Google Nano Banana Pro or OpenAI GPT Image 2.
About
This skill generates and edits images through the fal.ai API, supporting Google Nano Banana Pro and OpenAI GPT Image 2 selected from config. A developer uses it for infographics, banners, text rendering, photo edits, and reference-based compositions. It runs a generate mode for text-only prompts and an edit mode for provided reference images, choosing the model from a --model flag or config environment variables.
- Generates and edits images via fal.ai using Nano Banana Pro or GPT Image 2
- Supports reference-image edit mode and strong text rendering including Cyrillic
- Selects model from --model, FAL_IMAGE_MODEL, or FAL_IMAGE_PROVIDER in config
Fal Ai Image by the numbers
- 34 all-time installs (skills.sh)
- Ranked #949 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
fal-ai-image capabilities & compatibility
Requires a FAL_KEY; GPT Image 2 roughly $0.18 per image per the docs
- Works with
- openai
- Use cases
- image generation
- Pricing
- Bring your own API key
What fal-ai-image says it does
Best for: infographics, text rendering, banners, photo edits, reference-based compositions.
Requires `FAL_KEY` in `config/.env` or the environment.
npx skills add https://github.com/artwist-polyakov/polyakov-claude-skills --skill fal-ai-imageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 34 |
|---|---|
| repo stars | ★ 177 |
| Last updated | May 10, 2026 |
| Repository | artwist-polyakov/polyakov-claude-skills ↗ |
What it does
Generate or edit images via fal.ai using Nano Banana Pro or GPT Image 2 for infographics, banners, and photo edits.
Who is it for?
Developers generating infographics, banners, or photo edits, including reference-based compositions and text rendering
Skip if: Text/logo overlay with Pillow or ImageMagick unless explicitly requested
When should I use this skill?
The user wants to generate or edit an image, make an infographic or banner, or run a photo edit with fal.ai
What you get
Generated or edited images returned as URLs and optional local files via the chosen fal.ai model.
- generated images
- edited images
- image URLs
By the numbers
- 2 supported models (Nano Banana Pro, GPT Image 2)
- GPT Image 2 rough cost about $0.18 per image
Files
fal-ai-image
Generate images via fal.ai. The skill now supports two fal-hosted models:
- Google Nano Banana Pro (
fal-ai/nano-banana-pro) — default, backward-compatible - OpenAI GPT Image 2 (
openai/gpt-image-2) — enabled fromconfig/.env
Synonyms the agent should treat as equivalent:
gpt=openai= GPT Image 2nano banana=google=gemini= Nano Banana Pro
Best for: infographics, text rendering, banners, photo edits, reference-based compositions.
STOP — Read Before Acting
- DO NOT use Pillow, ImageMagick, or post-processing for text/logo overlay unless the user explicitly asked for that workflow
- DO NOT use Generate mode when the user provided reference images — use Edit mode
- DO NOT assume GPT is active — check
config/README.mdlogic; if no selector is configured, the skill stays on Nano Banana - DO NOT guess provider-specific params — Nano Banana and GPT Image 2 use different schemas
- DO pass
--model ...when the user explicitly asks for a specific model/provider or asks to compare providers - DO NOT skip uploading local files — run
upload.shfirst to get URLs foredit.sh
Quick Start Decision
Reference images provided? -> Edit mode (upload.sh -> edit.sh)
Text-only generation? -> Generate mode (generate.sh)
Model comes from config/.env:
no selector set -> Nano Banana Pro
FAL_IMAGE_PROVIDER=openai -> GPT Image 2
FAL_IMAGE_MODEL=... -> exact overrideConfig
Requires FAL_KEY in config/.env or the environment.
Model selection:
1. --model — one-off override for the current command 2. FAL_IMAGE_MODEL — exact override in config 3. FAL_IMAGE_PROVIDER — google or openai 4. nothing set — default to Nano Banana Pro
Use --model whenever the user explicitly says things like:
- "сделай через GPT"
- "используй OpenAI"
- "сделай через Google / Gemini / Nano Banana"
- "какие тут есть провайдеры?"
Answer that the skill supports two choices and map the command like this:
--model gptfor OpenAI GPT Image 2--model geminior--model nano-bananafor Nano Banana
OpenAI quality default:
FAL_IMAGE_OPENAI_QUALITY=mediumunless overridden with--quality- this skill intentionally uses
mediumas the default for GPT to avoid expensive exploratory runs
Full setup and troubleshooting: config/README.md.
Read references only when needed:
- references/MODELS.md — selector precedence, aliases, cost heuristics
- references/EDITING.md — generate vs edit,
mask_url, inpainting behavior
Model Notes
Nano Banana Pro
Strengths:
- lower-friction default for existing installs
- strong text rendering, including Cyrillic
- good at infographics, banners, and mixed text/image layouts
- supports
--web-searchin generate mode
Main params:
--aspect-ratio--resolution--web-search(generate only)
OpenAI GPT Image 2
Strengths:
- stronger prompt adherence and fine-grained edits
- better photorealism and product-style renders
- native
qualitycontrol - uses the same
FAL_KEYthrough fal, no separate OpenAI key - current fal pricing is size/quality-dependent; a rough high-quality mental model is about
$0.18per image, but check the live model page before quoting an exact number
Main params:
--image-size--quality
Compatibility layer:
- if
--image-sizeis omitted, the scripts derive a valid OpenAIimage_sizefrom--aspect-ratio+--resolution - this lets old prompts continue working after the provider switch in config
Workflow
Generate mode
1. Decide model from config 2. Clarify missing params only if needed:
- Nano Banana: aspect ratio, resolution
- GPT Image 2: image size and quality
3. Propose save path based on project structure 4. Run generate.sh 5. Parse result JSON, report URL and local files if downloaded
Edit mode
1. Get reference images:
- URL already available -> use directly
- local file ->
upload.sh
2. Decide model from config 3. Clarify edit intent 4. Run edit.sh 5. Parse result JSON, report URL and local files if downloaded
Scripts
generate.sh
Nano Banana example:
sh scripts/generate.sh \
--model "gemini" \
--prompt "infographic about coffee brewing" \
--aspect-ratio "9:16" \
--resolution "1K" \
--output-dir "./images" \
--filename "coffee_infographic"GPT Image 2 example:
sh scripts/generate.sh \
--model "gpt" \
--prompt "realistic product hero shot with sharp packaging text" \
--image-size "landscape_4_3" \
--quality "medium" \
--output-dir "./images" \
--filename "product_hero"Compatibility example for GPT:
sh scripts/generate.sh \
--model "openai" \
--prompt "editorial portrait, window light, magazine cover layout" \
--aspect-ratio "4:3" \
--resolution "2K"| Param | Required | Default | Notes |
|---|---|---|---|
--prompt | yes | - | text prompt |
--model | no | config / Nano Banana fallback | nano-banana, google, gemini, gpt, openai, or exact endpoint |
--aspect-ratio | no | 1:1 | Nano native; for GPT used only when --image-size is omitted |
--resolution | no | 1K | Nano native; for GPT used only when --image-size is omitted |
--image-size | no | derived from ratio/resolution | GPT only; preset (landscape_4_3) or WIDTHxHEIGHT |
--quality | no | medium via config | GPT only; low, medium, high |
--num-images | no | 1 | 1-4 |
--output-format | no | png | jpeg, png, webp |
--output-dir | no | - | local path |
--filename | no | generated | base filename |
--web-search | no | false | Nano only; ignored for GPT |
edit.sh
Nano Banana example:
sh scripts/edit.sh \
--model "gemini" \
--prompt "combine these into a collage" \
--image-urls "https://example.com/img1.png,https://example.com/img2.png" \
--aspect-ratio "16:9" \
--output-dir "./images" \
--filename "collage"GPT Image 2 example:
sh scripts/edit.sh \
--model "gpt" \
--prompt "make this product shot look like a premium studio campaign" \
--image-urls "https://example.com/source.png" \
--mask-url "https://example.com/mask.png" \
--image-size "auto" \
--quality "medium" \
--output-dir "./images" \
--filename "studio_edit"| Param | Required | Default | Notes |
|---|---|---|---|
--prompt | yes | - | edit instruction |
--image-urls | yes | - | comma-separated URLs |
--model | no | config / Nano Banana fallback | nano-banana, google, gemini, gpt, openai, or exact endpoint |
--mask-url | no | - | GPT edit only; optional mask for targeted edits |
--aspect-ratio | no | auto | Nano native; for GPT used only when --image-size is omitted |
--resolution | no | 1K | Nano native; for GPT used only when --image-size is omitted |
--image-size | no | derived from ratio/resolution / auto | GPT only |
--quality | no | medium via config | GPT only |
--num-images | no | 1 | 1-4 |
--output-format | no | png | jpeg, png, webp |
--output-dir | no | - | local path |
--filename | no | edited | base filename |
upload.sh
# Get hosted URL for local file
URL=$(sh scripts/upload.sh --file /path/to/image.png)
# Get base64 data URI for manual API work
URI=$(sh scripts/upload.sh --file /path/to/image.png --base64)Cost Guidance
- Nano Banana Pro is the cheaper and safer default for quick iterations
- GPT Image 2 cost depends heavily on
qualityandimage_size - this skill defaults GPT to
mediumquality to reduce surprise spend
For current pricing, check fal's model pages in config/README.md.
Notes
- result URLs expire in roughly one hour — download locally if you need persistence
- uploaded files on fal storage are temporary
edit.shnow polls the same/editqueue endpoints documented by fal
# Config with secrets
config/.env
# Generated images
generated/
# fal.ai API key (required for both Nano Banana Pro and GPT Image 2 on fal)
# Get yours at: https://fal.ai/dashboard/keys
FAL_KEY=your_fal_api_key_here
# Optional selector. If neither variable is set, the skill stays on Nano Banana
# for backward compatibility with older installs.
#
# For one-off runs, prefer the CLI flag:
# --model gpt
# --model openai
# --model nano-banana
# --model google
# --model gemini
#
# Human-friendly selector:
# FAL_IMAGE_PROVIDER=google # google | openai
#
# Exact model override (wins over FAL_IMAGE_PROVIDER if both are set):
# FAL_IMAGE_MODEL=fal-ai/nano-banana-pro
# FAL_IMAGE_MODEL=openai/gpt-image-2
# Optional OpenAI-specific default. The fal API defaults to high quality, but
# this skill uses medium unless you override it here or via --quality.
# FAL_IMAGE_OPENAI_QUALITY=medium # low | medium | high
Configuration
The skill supports two fal-hosted image models behind the same FAL_KEY:
| Selector | Resolved model | Best for |
|---|---|---|
default / google | fal-ai/nano-banana-pro | backward-compatible default, infographics, lower cost |
openai | openai/gpt-image-2 | stronger prompt adherence, photorealism, detailed edits |
No separate OpenAI key is required when you use the fal-hosted GPT model.
One-off override from CLI
Both generate.sh and edit.sh support:
--model VALUESupported aliases:
--model value | Resolved model |
|---|---|
nano-banana, google, gemini | fal-ai/nano-banana-pro |
gpt, openai | openai/gpt-image-2 |
| exact endpoint IDs | same endpoint |
Examples:
sh scripts/generate.sh --model gpt --prompt "studio product render"
sh scripts/generate.sh --model gemini --prompt "text-heavy infographic"
sh scripts/edit.sh --model openai --image-urls "$URL" --prompt "premium ad retouch"Use this when the user explicitly asks for a specific model or asks which options are available.
edit.sh also supports:
--mask-url URLThis is passed through to openai/gpt-image-2/edit and is useful when you want to constrain the edit to a specific region. Nano Banana does not support this flag in the current skill.
Quick start
1. Get an API key at https://fal.ai/dashboard/keys 2. Copy .env.example to .env 3. Set FAL_KEY
Minimal config keeps the old behavior:
FAL_KEY=your_fal_api_key_hereThis resolves to Nano Banana Pro automatically.
Switch to OpenAI GPT Image 2
Use the human-friendly selector:
FAL_KEY=your_fal_api_key_here
FAL_IMAGE_PROVIDER=openai
FAL_IMAGE_OPENAI_QUALITY=mediumOr pin the exact fal model ID:
FAL_KEY=your_fal_api_key_here
FAL_IMAGE_MODEL=openai/gpt-image-2
FAL_IMAGE_OPENAI_QUALITY=mediumSelector precedence
Selection in scripts/common.sh is intentionally simple:
1. --model — one-off CLI override 2. FAL_IMAGE_MODEL — exact model override in config 3. FAL_IMAGE_PROVIDER — google or openai 4. Nothing set — fallback to fal-ai/nano-banana-pro
That means older installs with only FAL_KEY keep working without any changes, while a single run can still force GPT or Nano Banana.
Parameter mapping by model
Nano Banana Pro
Native knobs:
--aspect-ratio--resolution--web-search(generate only)
OpenAI GPT Image 2
Native knobs:
--image-size--quality--mask-urlinedit.sh
Supported --image-size forms:
- preset:
square_hd,square,portrait_4_3,portrait_16_9,landscape_4_3,landscape_16_9 - custom size:
WIDTHxHEIGHT(for example1536x1024) autoinedit.sh
Compatibility layer:
- if
--image-sizeis omitted, the scripts derive a valid OpenAIimage_sizefrom--aspect-ratio+--resolution - this keeps the old CLI shape usable after switching the provider in config
Quality default for OpenAI
fal's GPT Image 2 schema defaults quality to high, but this skill uses:
FAL_IMAGE_OPENAI_QUALITY=mediumas the default unless you override it via --quality.
Reason: high can make exploratory runs noticeably more expensive than Nano Banana. medium is a safer default for iteration; use --quality high for final renders.
As of April 22, 2026, fal's GPT Image 2 playground shows common high-quality renders landing roughly around $0.15-$0.22 per image depending on size, so the "about $0.18" rule of thumb is reasonable for user guidance. Verify current prices on the model page before promising a number: GPT Image 2 pricing.
Examples
Default Nano Banana
sh scripts/generate.sh \
--prompt "poster about coffee brewing" \
--aspect-ratio "9:16" \
--resolution "1K"OpenAI GPT Image 2 with explicit preset size
sh scripts/generate.sh \
--model "gpt" \
--prompt "realistic product hero shot, clean label typography" \
--image-size "landscape_4_3" \
--quality "medium"OpenAI GPT Image 2 while keeping old flags
sh scripts/generate.sh \
--model "openai" \
--prompt "editorial-style fashion photo" \
--aspect-ratio "4:3" \
--resolution "2K"The script will derive a valid image_size object for OpenAI automatically.
Troubleshooting
FAL_KEY not found
Set FAL_KEY in config/.env or the shell environment.
Unsupported FAL_IMAGE_PROVIDER
Use only:
FAL_IMAGE_PROVIDER=google
FAL_IMAGE_PROVIDER=openaiUnsupported FAL_IMAGE_MODEL
Use only:
FAL_IMAGE_MODEL=fal-ai/nano-banana-pro
FAL_IMAGE_MODEL=openai/gpt-image-2OpenAI image_size width and height must be multiples of 16
When you pass custom --image-size, use dimensions like:
1024x10241536x10241920x1088
References
- Nano Banana Pro API: https://fal.ai/models/fal-ai/nano-banana-pro/api
- Nano Banana Pro edit API: https://fal.ai/models/fal-ai/nano-banana-pro/edit/api
- GPT Image 2 API: https://fal.ai/models/openai/gpt-image-2/api
- GPT Image 2 edit API: https://fal.ai/models/openai/gpt-image-2/edit/api
Editing Workflows
Generate vs Edit
Use generate.sh when the user wants a brand new image from text.
Use edit.sh when at least one source image matters:
- preserve composition from a source
- restyle an existing picture
- replace or add objects
- modify only one area of an image
Reference images
edit.sh accepts:
- public URLs directly in
--image-urls - local files after uploading through
scripts/upload.sh
When several input images are passed to GPT edit, the mask applies to the first image.
mask_url
mask_url is currently supported only for openai/gpt-image-2/edit.
Purpose:
- tells the model which region should be edited
- keeps the rest of the image more stable
- is best for inpainting-style changes such as swapping one object, changing text in one block, replacing a sign, retouching a face region, etc.
Important nuance from OpenAI's image docs:
- with GPT Image, the mask is guidance, not a perfect hard boundary
- the model usually follows the masked region, but may bleed slightly outside it if the prompt implies broader changes
Mask requirements
Practical requirements for reliable results:
- mask and source image should have the same size
- mask should have an alpha channel
- transparent area should represent the region to replace
- keep the prompt tightly scoped to the masked area when you want localized edits
When to use a mask
Use a mask when the user says something like:
- "замени только вывеску"
- "поменяй текст только в этом блоке"
- "убери объект справа, остальное не трогай"
- "сделай ретушь только лица"
Do not bother with mask_url when the user wants a broad restyle of the whole image. In that case plain edit.sh with references is simpler.
Prompting tips for masked edits
- explicitly mention what must stay unchanged
- name the target area in the prompt
- avoid global style rewrites unless you actually want spillover outside the mask
Example:
sh scripts/edit.sh \
--model gpt \
--image-urls "https://example.com/base.png" \
--mask-url "https://example.com/mask.png" \
--image-size auto \
--quality medium \
--prompt "Replace only the masked area with a matte red ceramic mug. Keep the rest of the poster unchanged."References:
- fal GPT edit API: https://fal.ai/models/openai/gpt-image-2/edit/api
- OpenAI image generation guide: https://platform.openai.com/docs/guides/image-generation
Model Selection
The skill supports two fal-hosted image backends behind the same FAL_KEY:
| Alias family | Resolved endpoint | Best for |
|---|---|---|
nano-banana, google, gemini | fal-ai/nano-banana-pro | cheaper default runs, infographics, strong text rendering |
gpt, openai | openai/gpt-image-2 | stronger prompt adherence, photorealism, precise editing |
Selection precedence
The scripts resolve the active model in this order:
1. CLI --model 2. FAL_IMAGE_MODEL in config/.env 3. FAL_IMAGE_PROVIDER in config/.env 4. fallback to fal-ai/nano-banana-pro
This means:
- old installs with only
FAL_KEYstill work - a single request can explicitly force GPT or Nano Banana
- the agent should use
--modelwhen the user explicitly asks for a provider/model
Agent mapping rules
Treat these phrases as equivalent:
GPT,OpenAI,ChatGPT Images,GPT Image->--model gptNano Banana,Google,Gemini->--model gemini
If the user does not mention the model explicitly, let the config decide.
Cost guidance
Nano Banana is the safer default for exploratory runs.
GPT Image 2 cost depends on:
qualityimage_size- edit vs generate complexity
As of April 22, 2026, fal's GPT Image 2 playground shows common high-quality outputs roughly in the $0.15-$0.22 range depending on size. For rough planning, "about $0.18" is acceptable, but do not promise a fixed price without checking the live model page first.
Reference:
- https://fal.ai/models/openai/gpt-image-2/playground
#!/bin/sh
# Common helpers for fal-ai-image skill.
# POSIX sh compatible — no bashisms.
if [ -z "${FAL_AI_IMAGE_SCRIPT_DIR:-}" ]; then
FAL_AI_IMAGE_SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
fi
if [ -z "${FAL_AI_IMAGE_SKILL_DIR:-}" ]; then
FAL_AI_IMAGE_SKILL_DIR="$(cd "$FAL_AI_IMAGE_SCRIPT_DIR/.." && pwd)"
fi
FAL_AI_IMAGE_CONFIG_FILE="${FAL_AI_IMAGE_CONFIG_FILE:-$FAL_AI_IMAGE_SKILL_DIR/config/.env}"
FAL_IMAGE_GOOGLE_MODEL="fal-ai/nano-banana-pro"
FAL_IMAGE_OPENAI_MODEL="openai/gpt-image-2"
FAL_IMAGE_PROVIDER_RESOLVED=""
FAL_IMAGE_MODEL_RESOLVED=""
FAL_IMAGE_SELECTION_SOURCE=""
FAL_IMAGE_OPENAI_QUALITY_RESOLVED=""
die() {
echo "Error: $1" >&2
exit 1
}
json_quote() {
if command -v python3 >/dev/null 2>&1; then
_jq_value="$1" python3 - <<'PYEOF'
import json
import os
print(json.dumps(os.environ["_jq_value"], ensure_ascii=False))
PYEOF
return 0
fi
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
}
normalize_provider() {
_provider=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
case "$_provider" in
""|google|gemini|nano|nano-banana|nano-banana-pro)
printf 'google\n'
;;
openai|gpt|gpt-image-2)
printf 'openai\n'
;;
*)
return 1
;;
esac
}
normalize_model() {
_model=$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')
case "$_model" in
fal-ai/nano-banana-pro|fal-ai/nano-banana-pro/edit|nano-banana|nano-banana-pro|nano|google|gemini)
printf '%s\n' "$FAL_IMAGE_GOOGLE_MODEL"
;;
openai/gpt-image-2|openai/gpt-image-2/edit|gpt|gpt-image-2|openai)
printf '%s\n' "$FAL_IMAGE_OPENAI_MODEL"
;;
*)
return 1
;;
esac
}
provider_for_model() {
case "$1" in
"$FAL_IMAGE_OPENAI_MODEL")
printf 'openai\n'
;;
*)
printf 'google\n'
;;
esac
}
validate_openai_quality() {
case "$1" in
low|medium|high)
return 0
;;
*)
return 1
;;
esac
}
load_config() {
_cli_model_override="${1:-}"
if [ -f "$FAL_AI_IMAGE_CONFIG_FILE" ]; then
# shellcheck disable=SC1090
. "$FAL_AI_IMAGE_CONFIG_FILE"
fi
if [ -z "${FAL_KEY:-}" ]; then
die "FAL_KEY not found. Set it in config/.env or the shell environment."
fi
if [ -n "$_cli_model_override" ]; then
FAL_IMAGE_MODEL_RESOLVED=$(normalize_model "$_cli_model_override") || \
die "Unsupported --model '$_cli_model_override'. Use nano-banana, google, gemini, gpt, openai, '$FAL_IMAGE_GOOGLE_MODEL', or '$FAL_IMAGE_OPENAI_MODEL'."
FAL_IMAGE_PROVIDER_RESOLVED=$(provider_for_model "$FAL_IMAGE_MODEL_RESOLVED")
FAL_IMAGE_SELECTION_SOURCE="cli override"
elif [ -n "${FAL_IMAGE_MODEL:-}" ]; then
FAL_IMAGE_MODEL_RESOLVED=$(normalize_model "$FAL_IMAGE_MODEL") || \
die "Unsupported FAL_IMAGE_MODEL='$FAL_IMAGE_MODEL'. Use '$FAL_IMAGE_GOOGLE_MODEL' or '$FAL_IMAGE_OPENAI_MODEL'."
FAL_IMAGE_PROVIDER_RESOLVED=$(provider_for_model "$FAL_IMAGE_MODEL_RESOLVED")
FAL_IMAGE_SELECTION_SOURCE="explicit model"
elif [ -n "${FAL_IMAGE_PROVIDER:-}" ]; then
FAL_IMAGE_PROVIDER_RESOLVED=$(normalize_provider "$FAL_IMAGE_PROVIDER") || \
die "Unsupported FAL_IMAGE_PROVIDER='$FAL_IMAGE_PROVIDER'. Use 'google' or 'openai'."
case "$FAL_IMAGE_PROVIDER_RESOLVED" in
google) FAL_IMAGE_MODEL_RESOLVED="$FAL_IMAGE_GOOGLE_MODEL" ;;
openai) FAL_IMAGE_MODEL_RESOLVED="$FAL_IMAGE_OPENAI_MODEL" ;;
esac
FAL_IMAGE_SELECTION_SOURCE="provider alias"
else
FAL_IMAGE_PROVIDER_RESOLVED="google"
FAL_IMAGE_MODEL_RESOLVED="$FAL_IMAGE_GOOGLE_MODEL"
FAL_IMAGE_SELECTION_SOURCE="backward-compatible default"
fi
FAL_IMAGE_OPENAI_QUALITY_RESOLVED="${FAL_IMAGE_OPENAI_QUALITY:-medium}"
validate_openai_quality "$FAL_IMAGE_OPENAI_QUALITY_RESOLVED" || \
die "Unsupported FAL_IMAGE_OPENAI_QUALITY='$FAL_IMAGE_OPENAI_QUALITY_RESOLVED'. Use low, medium, or high."
export FAL_IMAGE_PROVIDER_RESOLVED FAL_IMAGE_MODEL_RESOLVED
export FAL_IMAGE_SELECTION_SOURCE FAL_IMAGE_OPENAI_QUALITY_RESOLVED
}
model_queue_name() {
_mode="$1"
case "$_mode" in
generate|"")
printf '%s\n' "$FAL_IMAGE_MODEL_RESOLVED"
;;
edit)
printf '%s/edit\n' "$FAL_IMAGE_MODEL_RESOLVED"
;;
*)
die "Unsupported mode '$_mode'"
;;
esac
}
queue_api_base() {
printf 'https://queue.fal.run/%s\n' "$(model_queue_name "$1")"
}
request_api_base() {
printf 'https://queue.fal.run/%s\n' "$FAL_IMAGE_MODEL_RESOLVED"
}
json_array_from_csv() {
if command -v python3 >/dev/null 2>&1; then
_csv_items="$1" python3 - <<'PYEOF'
import json
import os
import sys
items = [item.strip() for item in os.environ["_csv_items"].split(",") if item.strip()]
if not items:
sys.exit(1)
print(json.dumps(items, ensure_ascii=False, separators=(",", ":")))
PYEOF
return 0
fi
_csv_items="$1"
_json="["
_first="true"
OLD_IFS=$IFS
IFS=','
set -- $_csv_items
IFS=$OLD_IFS
for _item in "$@"; do
_trimmed=$(printf '%s' "$_item" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$_trimmed" ] || continue
_quoted=$(json_quote "$_trimmed")
if [ "$_first" = "true" ]; then
_json="$_json$_quoted"
_first="false"
else
_json="$_json,$_quoted"
fi
done
_json="$_json]"
if [ "$_json" = "[]" ]; then
return 1
fi
printf '%s\n' "$_json"
}
count_csv_items() {
if command -v python3 >/dev/null 2>&1; then
_csv_items="$1" python3 - <<'PYEOF'
import os
items = [item.strip() for item in os.environ["_csv_items"].split(",") if item.strip()]
print(len(items))
PYEOF
return 0
fi
_csv_items="$1"
_count=0
OLD_IFS=$IFS
IFS=','
set -- $_csv_items
IFS=$OLD_IFS
for _item in "$@"; do
_trimmed=$(printf '%s' "$_item" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$_trimmed" ] || continue
_count=$((_count + 1))
done
printf '%s\n' "$_count"
}
openai_image_size_json() {
command -v python3 >/dev/null 2>&1 || die "python3 is required for OpenAI image size calculation."
_size_arg="${1:-}"
_aspect_ratio="${2:-1:1}"
_resolution="${3:-1K}"
_allow_auto="${4:-false}"
_size_arg="$_size_arg" \
_aspect_ratio="$_aspect_ratio" \
_resolution="$_resolution" \
_allow_auto="$_allow_auto" \
python3 - <<'PYEOF'
import json
import os
import re
import sys
size_arg = os.environ["_size_arg"].strip()
aspect_ratio = os.environ["_aspect_ratio"].strip()
resolution = os.environ["_resolution"].strip() or "1K"
allow_auto = os.environ["_allow_auto"].strip().lower() == "true"
PRESETS = {
"square_hd",
"square",
"portrait_4_3",
"portrait_16_9",
"landscape_4_3",
"landscape_16_9",
}
MIN_PIXELS = 655_360
MAX_PIXELS = 8_294_400
MAX_EDGE = 3840
RESOLUTION_TO_EDGE = {
"1K": 1024,
"2K": 2048,
"4K": 3840,
}
def fail(message: str) -> None:
print(f"Error: {message}", file=sys.stderr)
sys.exit(1)
def validate_dimensions(width: int, height: int) -> None:
if width % 16 != 0 or height % 16 != 0:
fail("OpenAI image_size width and height must be multiples of 16.")
if width > MAX_EDGE or height > MAX_EDGE:
fail(f"OpenAI image_size max edge is {MAX_EDGE}px.")
if width * height < MIN_PIXELS or width * height > MAX_PIXELS:
fail(
f"OpenAI image_size total pixels must be between {MIN_PIXELS} and {MAX_PIXELS}."
)
ratio = max(width / height, height / width)
if ratio > 3:
fail("OpenAI image_size aspect ratio must stay within 3:1.")
def best_size_for_ratio(ratio_value: float, target_edge: int):
best = None
desired_height = target_edge / ratio_value if ratio_value >= 1 else target_edge
desired_width = target_edge if ratio_value >= 1 else target_edge * ratio_value
for width in range(16, MAX_EDGE + 1, 16):
height = int(round((width / ratio_value) / 16.0) * 16)
if height < 16 or height > MAX_EDGE:
continue
area = width * height
if area < MIN_PIXELS or area > MAX_PIXELS:
continue
score = abs(width - desired_width) + abs(height - desired_height) + abs((width / height) - ratio_value) * 2000
if best is None or score < best[0]:
best = (score, width, height)
for height in range(16, MAX_EDGE + 1, 16):
width = int(round((height * ratio_value) / 16.0) * 16)
if width < 16 or width > MAX_EDGE:
continue
area = width * height
if area < MIN_PIXELS or area > MAX_PIXELS:
continue
score = abs(width - desired_width) + abs(height - desired_height) + abs((width / height) - ratio_value) * 2000
if best is None or score < best[0]:
best = (score, width, height)
if best is None:
fail(f"Unable to derive a valid OpenAI image_size for aspect ratio '{aspect_ratio}' and resolution '{resolution}'.")
return best[1], best[2]
if size_arg:
if size_arg in PRESETS:
print(json.dumps(size_arg))
sys.exit(0)
if allow_auto and size_arg == "auto":
print(json.dumps("auto"))
sys.exit(0)
match = re.match(r"^(\d+)[xX](\d+)$", size_arg)
if not match:
fail("Unsupported --image-size. Use a preset like landscape_4_3 or dimensions like 1536x1024.")
width = int(match.group(1))
height = int(match.group(2))
validate_dimensions(width, height)
print(json.dumps({"width": width, "height": height}, separators=(",", ":")))
sys.exit(0)
if allow_auto and aspect_ratio == "auto":
print(json.dumps("auto"))
sys.exit(0)
match = re.match(r"^(\d+):(\d+)$", aspect_ratio)
if not match:
fail("Unsupported aspect ratio for OpenAI model. Use values like 1:1, 4:3, 16:9 or pass --image-size.")
ratio_w = int(match.group(1))
ratio_h = int(match.group(2))
if ratio_w <= 0 or ratio_h <= 0:
fail("Aspect ratio values must be positive integers.")
if resolution not in RESOLUTION_TO_EDGE:
fail("Unsupported resolution for OpenAI model. Use 1K, 2K, 4K or pass --image-size.")
ratio_value = ratio_w / ratio_h
target_edge = RESOLUTION_TO_EDGE[resolution]
width, height = best_size_for_ratio(ratio_value, target_edge)
validate_dimensions(width, height)
print(json.dumps({"width": width, "height": height}, separators=(",", ":")))
PYEOF
}
extract_request_id() {
printf '%s' "$1" | grep -oE '"request_id":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/'
}
extract_status() {
printf '%s' "$1" | grep -oE '"status":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/'
}
submit_queue_request() {
_api_base="$1"
_json_payload="$2"
curl -s -X POST "$_api_base" \
-H "Authorization: Key $FAL_KEY" \
-H "Content-Type: application/json" \
-d "$_json_payload"
}
wait_for_request() {
_api_base="$1"
_request_id="$2"
_max_attempts="${3:-60}"
_attempt=0
while [ "$_attempt" -lt "$_max_attempts" ]; do
_status_response=$(curl -s "$_api_base/requests/$_request_id/status" \
-H "Authorization: Key $FAL_KEY")
_status=$(extract_status "$_status_response")
case "$_status" in
COMPLETED)
echo "Generation complete!"
echo ""
return 0
;;
FAILED)
echo "Error: Generation failed" >&2
echo "$_status_response" >&2
return 1
;;
IN_PROGRESS|IN_QUEUE|PENDING)
echo "Status: $_status..."
sleep 2
_attempt=$((_attempt + 1))
;;
*)
echo "Status: ${_status:-UNKNOWN} (retrying)..."
sleep 2
_attempt=$((_attempt + 1))
;;
esac
done
echo "Error: Timeout waiting for generation" >&2
return 1
}
fetch_queue_result() {
_api_base="$1"
_request_id="$2"
curl -s "$_api_base/requests/$_request_id" \
-H "Authorization: Key $FAL_KEY"
}
download_result_images() {
_result="$1"
_output_dir="$2"
_filename="$3"
_output_format="$4"
_num_images="$5"
[ -n "$_output_dir" ] || return 0
mkdir -p "$_output_dir"
_timestamp=$(date +%Y%m%d_%H%M%S)
_urls=$(printf '%s' "$_result" | grep -oE '"url":[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/')
_index=0
echo "Downloading images..."
for _url in $_urls; do
_suffix=""
[ "$_num_images" -gt 1 ] && _suffix="_$_index"
_output_path="$_output_dir/${_filename}_${_timestamp}${_suffix}.${_output_format}"
if curl -s -o "$_output_path" "$_url"; then
echo "Saved: $_output_path"
else
echo "Warning: Failed to download $_url" >&2
fi
_index=$((_index + 1))
done
echo ""
}
#!/bin/sh
# Edit images via fal.ai using reference images.
# Supports Nano Banana Pro and OpenAI GPT Image 2 (selected via config/.env).
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
. "$SCRIPT_DIR/common.sh"
PROMPT=""
IMAGE_URLS=""
MODEL_OVERRIDE=""
MASK_URL=""
ASPECT_RATIO="auto"
RESOLUTION="1K"
IMAGE_SIZE=""
QUALITY=""
NUM_IMAGES=1
OUTPUT_FORMAT="png"
OUTPUT_DIR=""
FILENAME="edited"
while [ $# -gt 0 ]; do
case $1 in
--prompt|-p) PROMPT="$2"; shift 2 ;;
--image-urls|-i) IMAGE_URLS="$2"; shift 2 ;;
--model|-m) MODEL_OVERRIDE="$2"; shift 2 ;;
--mask-url) MASK_URL="$2"; shift 2 ;;
--aspect-ratio|-a) ASPECT_RATIO="$2"; shift 2 ;;
--resolution|-r) RESOLUTION="$2"; shift 2 ;;
--image-size) IMAGE_SIZE="$2"; shift 2 ;;
--quality) QUALITY="$2"; shift 2 ;;
--num-images|-n) NUM_IMAGES="$2"; shift 2 ;;
--output-format|-f) OUTPUT_FORMAT="$2"; shift 2 ;;
--output-dir|-o) OUTPUT_DIR="$2"; shift 2 ;;
--filename) FILENAME="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
load_config "$MODEL_OVERRIDE"
[ -n "$PROMPT" ] || die "--prompt is required"
[ -n "$IMAGE_URLS" ] || die "--image-urls is required (comma-separated URLs, max 14)"
IMAGE_URLS_JSON=$(json_array_from_csv "$IMAGE_URLS") || \
die "Failed to parse --image-urls. Pass a comma-separated list of URLs."
SUBMIT_API_BASE=$(queue_api_base edit)
REQUEST_API_BASE=$(request_api_base)
REFERENCE_IMAGE_COUNT=$(count_csv_items "$IMAGE_URLS")
PROMPT_JSON=$(json_quote "$PROMPT")
case "$FAL_IMAGE_PROVIDER_RESOLVED" in
google)
[ -z "$IMAGE_SIZE" ] || die "--image-size is only supported with OpenAI GPT Image 2. Use --aspect-ratio/--resolution for Nano Banana."
[ -z "$MASK_URL" ] || die "--mask-url is only supported with OpenAI GPT Image 2 edit."
if [ -n "$QUALITY" ]; then
echo "Warning: --quality is ignored for Nano Banana." >&2
fi
JSON_PAYLOAD="{\"prompt\":$PROMPT_JSON,\"image_urls\":$IMAGE_URLS_JSON,\"num_images\":$NUM_IMAGES,\"aspect_ratio\":\"$ASPECT_RATIO\",\"resolution\":\"$RESOLUTION\",\"output_format\":\"$OUTPUT_FORMAT\"}"
SETTINGS_LABEL="$ASPECT_RATIO, $RESOLUTION, $OUTPUT_FORMAT"
;;
openai)
if [ -n "$QUALITY" ]; then
validate_openai_quality "$QUALITY" || die "Unsupported --quality '$QUALITY'. Use low, medium, or high."
EFFECTIVE_QUALITY="$QUALITY"
else
EFFECTIVE_QUALITY="$FAL_IMAGE_OPENAI_QUALITY_RESOLVED"
fi
IMAGE_SIZE_JSON=$(openai_image_size_json "$IMAGE_SIZE" "$ASPECT_RATIO" "$RESOLUTION" "true")
JSON_PAYLOAD="{\"prompt\":$PROMPT_JSON,\"image_urls\":$IMAGE_URLS_JSON,\"image_size\":$IMAGE_SIZE_JSON,\"quality\":\"$EFFECTIVE_QUALITY\",\"num_images\":$NUM_IMAGES,\"output_format\":\"$OUTPUT_FORMAT\""
if [ -n "$MASK_URL" ]; then
MASK_URL_JSON=$(json_quote "$MASK_URL")
JSON_PAYLOAD="$JSON_PAYLOAD,\"mask_url\":$MASK_URL_JSON"
fi
JSON_PAYLOAD="$JSON_PAYLOAD}"
SETTINGS_LABEL="$IMAGE_SIZE_JSON, $EFFECTIVE_QUALITY, $OUTPUT_FORMAT"
;;
*)
die "Unsupported resolved provider '$FAL_IMAGE_PROVIDER_RESOLVED'"
;;
esac
echo "Submitting edit request..."
echo "Model: $(model_queue_name edit) ($FAL_IMAGE_SELECTION_SOURCE)"
printf "Prompt: %.100s...\n" "$PROMPT"
echo "Reference images: $REFERENCE_IMAGE_COUNT"
echo "Settings: $SETTINGS_LABEL"
echo ""
SUBMIT_RESPONSE=$(submit_queue_request "$SUBMIT_API_BASE" "$JSON_PAYLOAD")
REQUEST_ID=$(extract_request_id "$SUBMIT_RESPONSE")
[ -n "$REQUEST_ID" ] || {
echo "Error: Failed to submit request" >&2
echo "$SUBMIT_RESPONSE" >&2
exit 1
}
echo "Request ID: $REQUEST_ID"
echo "Waiting for generation..."
wait_for_request "$REQUEST_API_BASE" "$REQUEST_ID"
RESULT=$(fetch_queue_result "$REQUEST_API_BASE" "$REQUEST_ID")
download_result_images "$RESULT" "$OUTPUT_DIR" "$FILENAME" "$OUTPUT_FORMAT" "$NUM_IMAGES"
echo "=== RESULT JSON ==="
printf '%s\n' "$RESULT"
echo "=== END RESULT ==="
echo ""
echo "Note: URLs expire in ~1 hour"
#!/bin/sh
# Generate images via fal.ai.
# Supports Nano Banana Pro and OpenAI GPT Image 2 (selected via config/.env).
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
. "$SCRIPT_DIR/common.sh"
PROMPT=""
MODEL_OVERRIDE=""
ASPECT_RATIO="1:1"
RESOLUTION="1K"
IMAGE_SIZE=""
QUALITY=""
NUM_IMAGES=1
OUTPUT_FORMAT="png"
OUTPUT_DIR=""
FILENAME="generated"
WEB_SEARCH="false"
while [ $# -gt 0 ]; do
case $1 in
--prompt|-p) PROMPT="$2"; shift 2 ;;
--model|-m) MODEL_OVERRIDE="$2"; shift 2 ;;
--aspect-ratio|-a) ASPECT_RATIO="$2"; shift 2 ;;
--resolution|-r) RESOLUTION="$2"; shift 2 ;;
--image-size) IMAGE_SIZE="$2"; shift 2 ;;
--quality) QUALITY="$2"; shift 2 ;;
--num-images|-n) NUM_IMAGES="$2"; shift 2 ;;
--output-format|-f) OUTPUT_FORMAT="$2"; shift 2 ;;
--output-dir|-o) OUTPUT_DIR="$2"; shift 2 ;;
--filename) FILENAME="$2"; shift 2 ;;
--web-search|-w) WEB_SEARCH="true"; shift ;;
*) die "Unknown option: $1" ;;
esac
done
load_config "$MODEL_OVERRIDE"
[ -n "$PROMPT" ] || die "--prompt is required"
SUBMIT_API_BASE=$(queue_api_base generate)
REQUEST_API_BASE=$(request_api_base)
PROMPT_JSON=$(json_quote "$PROMPT")
case "$FAL_IMAGE_PROVIDER_RESOLVED" in
google)
[ -z "$IMAGE_SIZE" ] || die "--image-size is only supported with OpenAI GPT Image 2. Use --aspect-ratio/--resolution for Nano Banana."
if [ -n "$QUALITY" ]; then
echo "Warning: --quality is ignored for Nano Banana." >&2
fi
JSON_PAYLOAD="{\"prompt\":$PROMPT_JSON,\"num_images\":$NUM_IMAGES,\"aspect_ratio\":\"$ASPECT_RATIO\",\"resolution\":\"$RESOLUTION\",\"output_format\":\"$OUTPUT_FORMAT\""
if [ "$WEB_SEARCH" = "true" ]; then
JSON_PAYLOAD="$JSON_PAYLOAD,\"enable_web_search\":true"
fi
JSON_PAYLOAD="$JSON_PAYLOAD}"
SETTINGS_LABEL="$ASPECT_RATIO, $RESOLUTION, $OUTPUT_FORMAT"
;;
openai)
if [ "$WEB_SEARCH" = "true" ]; then
echo "Warning: --web-search is not supported by openai/gpt-image-2 and will be ignored." >&2
fi
if [ -n "$QUALITY" ]; then
validate_openai_quality "$QUALITY" || die "Unsupported --quality '$QUALITY'. Use low, medium, or high."
EFFECTIVE_QUALITY="$QUALITY"
else
EFFECTIVE_QUALITY="$FAL_IMAGE_OPENAI_QUALITY_RESOLVED"
fi
IMAGE_SIZE_JSON=$(openai_image_size_json "$IMAGE_SIZE" "$ASPECT_RATIO" "$RESOLUTION" "false")
JSON_PAYLOAD="{\"prompt\":$PROMPT_JSON,\"image_size\":$IMAGE_SIZE_JSON,\"quality\":\"$EFFECTIVE_QUALITY\",\"num_images\":$NUM_IMAGES,\"output_format\":\"$OUTPUT_FORMAT\"}"
SETTINGS_LABEL="$IMAGE_SIZE_JSON, $EFFECTIVE_QUALITY, $OUTPUT_FORMAT"
;;
*)
die "Unsupported resolved provider '$FAL_IMAGE_PROVIDER_RESOLVED'"
;;
esac
echo "Submitting request..."
echo "Model: $FAL_IMAGE_MODEL_RESOLVED ($FAL_IMAGE_SELECTION_SOURCE)"
printf "Prompt: %.100s...\n" "$PROMPT"
echo "Settings: $SETTINGS_LABEL"
echo ""
SUBMIT_RESPONSE=$(submit_queue_request "$SUBMIT_API_BASE" "$JSON_PAYLOAD")
REQUEST_ID=$(extract_request_id "$SUBMIT_RESPONSE")
[ -n "$REQUEST_ID" ] || {
echo "Error: Failed to submit request" >&2
echo "$SUBMIT_RESPONSE" >&2
exit 1
}
echo "Request ID: $REQUEST_ID"
echo "Waiting for generation..."
wait_for_request "$REQUEST_API_BASE" "$REQUEST_ID"
RESULT=$(fetch_queue_result "$REQUEST_API_BASE" "$REQUEST_ID")
download_result_images "$RESULT" "$OUTPUT_DIR" "$FILENAME" "$OUTPUT_FORMAT" "$NUM_IMAGES"
echo "=== RESULT JSON ==="
printf '%s\n' "$RESULT"
echo "=== END RESULT ==="
echo ""
echo "Note: URLs expire in ~1 hour"
#!/bin/sh
# Test runner for fal-ai-image skill — no network.
set -e
TESTS_DIR="$(cd "$(dirname "$0")" && pwd)"
PASS=0
FAIL=0
FAILED_TESTS=""
for t in "$TESTS_DIR"/test_*.sh; do
[ -f "$t" ] || continue
name=$(basename "$t" .sh)
printf '%s ... ' "$name"
if sh "$t" >/dev/null 2>&1; then
printf 'PASS\n'
PASS=$((PASS + 1))
else
printf 'FAIL\n'
FAIL=$((FAIL + 1))
FAILED_TESTS="$FAILED_TESTS $name"
echo "--- output of $name ---"
sh "$t" 2>&1 || true
echo "--- end ---"
fi
done
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo "Failed:$FAILED_TESTS"
exit 1
fi
#!/bin/sh
# Test config selector logic for fal-ai-image.
set -e
TESTS_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPTS_DIR="$(cd "$TESTS_DIR/.." && pwd)"
run_selector() {
_label="$1"
_provider="$2"
_model="$3"
_quality="$4"
_cli_model="$5"
_td="${TMPDIR:-/tmp}/fal_image_test_$$_$(printf '%s' "$_label" | tr ' /+' '___')"
rm -rf "$_td"
mkdir -p "$_td"
{
echo "FAL_KEY=test-key"
[ -n "$_provider" ] && echo "FAL_IMAGE_PROVIDER=$_provider"
[ -n "$_model" ] && echo "FAL_IMAGE_MODEL=$_model"
[ -n "$_quality" ] && echo "FAL_IMAGE_OPENAI_QUALITY=$_quality"
} > "$_td/.env"
_result=$(
FAL_AI_IMAGE_CONFIG_FILE="$_td/.env"
export FAL_AI_IMAGE_CONFIG_FILE
# shellcheck disable=SC1091
. "$SCRIPTS_DIR/common.sh"
if ( load_config "$_cli_model" ) >/dev/null 2>&1; then
load_config "$_cli_model" >/dev/null
printf 'PROVIDER=%s\nMODEL=%s\nQUALITY=%s\n' \
"$FAL_IMAGE_PROVIDER_RESOLVED" \
"$FAL_IMAGE_MODEL_RESOLVED" \
"$FAL_IMAGE_OPENAI_QUALITY_RESOLVED"
else
printf 'DIE\n'
fi
)
rm -rf "$_td"
printf '%s' "$_result"
}
assert_contains() {
_label="$1"
_haystack="$2"
_needle="$3"
case "$_haystack" in
*"$_needle"*)
echo " ok: $_label contains $_needle"
;;
*)
echo " FAIL: $_label missing $_needle"
echo " full output: $_haystack"
exit 1
;;
esac
}
assert_die() {
_label="$1"
_result="$2"
case "$_result" in
*DIE*)
echo " ok: $_label died as expected"
;;
*)
echo " FAIL: $_label expected DIE, got: $_result"
exit 1
;;
esac
}
out=$(run_selector "default" "" "" "" "")
assert_contains "default selector" "$out" "PROVIDER=google"
assert_contains "default selector" "$out" "MODEL=fal-ai/nano-banana-pro"
assert_contains "default selector" "$out" "QUALITY=medium"
out=$(run_selector "provider_openai" "openai" "" "" "")
assert_contains "provider=openai" "$out" "PROVIDER=openai"
assert_contains "provider=openai" "$out" "MODEL=openai/gpt-image-2"
out=$(run_selector "provider_alias" "gpt" "" "high" "")
assert_contains "provider alias" "$out" "PROVIDER=openai"
assert_contains "provider alias" "$out" "QUALITY=high"
out=$(run_selector "model_wins" "google" "openai/gpt-image-2" "" "")
assert_contains "explicit model wins" "$out" "PROVIDER=openai"
assert_contains "explicit model wins" "$out" "MODEL=openai/gpt-image-2"
out=$(run_selector "cli_override_wins" "google" "" "" "gpt")
assert_contains "cli override wins" "$out" "PROVIDER=openai"
assert_contains "cli override wins" "$out" "MODEL=openai/gpt-image-2"
out=$(run_selector "cli_alias_gemini" "openai" "" "" "gemini")
assert_contains "cli alias gemini" "$out" "PROVIDER=google"
assert_contains "cli alias gemini" "$out" "MODEL=fal-ai/nano-banana-pro"
out=$(run_selector "invalid_provider" "banana-party" "" "" "")
assert_die "invalid provider" "$out"
out=$(run_selector "invalid_quality" "openai" "" "ultra" "")
assert_die "invalid quality" "$out"
out=$(run_selector "invalid_cli_model" "" "" "" "banana-party")
assert_die "invalid cli model" "$out"
echo "test_config_selector: all passed"
#!/bin/sh
# Test that edit.sh forwards mask_url to openai/gpt-image-2/edit.
set -e
TESTS_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPTS_DIR="$(cd "$TESTS_DIR/.." && pwd)"
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/fal_image_edit_mask.XXXXXX")
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT INT TERM
cat > "$TMP_DIR/.env" <<'EOF'
FAL_KEY=test-key
FAL_IMAGE_PROVIDER=openai
EOF
mkdir -p "$TMP_DIR/bin"
cat > "$TMP_DIR/bin/curl" <<'EOF'
#!/bin/sh
set -e
LOG_DIR="${FAL_TEST_LOG_DIR:?}"
URL=""
DATA=""
NEXT_IS_DATA="false"
for arg in "$@"; do
if [ "$NEXT_IS_DATA" = "true" ]; then
DATA="$arg"
NEXT_IS_DATA="false"
continue
fi
case "$arg" in
-d)
NEXT_IS_DATA="true"
;;
http://*|https://*)
URL="$arg"
;;
esac
done
printf '%s\n' "$URL" >> "$LOG_DIR/url.txt"
[ -n "$DATA" ] && printf '%s\n' "$DATA" > "$LOG_DIR/payload.json"
case "$URL" in
*/status)
printf '{"status":"COMPLETED"}'
;;
*/requests/req-test)
printf '{"images":[{"url":"https://example.com/result.png","content_type":"image/png","file_name":"result.png"}]}'
;;
*)
printf '{"request_id":"req-test"}'
;;
esac
EOF
chmod +x "$TMP_DIR/bin/curl"
FAL_AI_IMAGE_CONFIG_FILE="$TMP_DIR/.env"
FAL_TEST_LOG_DIR="$TMP_DIR/logs"
PATH="$TMP_DIR/bin:$PATH"
export FAL_AI_IMAGE_CONFIG_FILE FAL_TEST_LOG_DIR PATH
mkdir -p "$FAL_TEST_LOG_DIR"
sh "$SCRIPTS_DIR/edit.sh" \
--model gpt \
--prompt "targeted edit" \
--image-urls "https://example.com/source.png" \
--mask-url "https://example.com/mask.png" \
--image-size auto \
>/dev/null
grep '"mask_url":"https://example.com/mask.png"' "$FAL_TEST_LOG_DIR/payload.json" >/dev/null
grep 'https://queue.fal.run/openai/gpt-image-2/edit' "$FAL_TEST_LOG_DIR/url.txt" >/dev/null
grep 'https://queue.fal.run/openai/gpt-image-2/requests/req-test/status' "$FAL_TEST_LOG_DIR/url.txt" >/dev/null
echo "test_edit_openai_mask: all passed"
#!/bin/sh
# Test OpenAI image_size helper.
set -e
TESTS_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPTS_DIR="$(cd "$TESTS_DIR/.." && pwd)"
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/fal_image_size_test.XXXXXX")
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT INT TERM
cat > "$TMP_DIR/.env" <<'EOF'
FAL_KEY=test-key
FAL_IMAGE_PROVIDER=openai
EOF
FAL_AI_IMAGE_CONFIG_FILE="$TMP_DIR/.env"
export FAL_AI_IMAGE_CONFIG_FILE
# shellcheck disable=SC1091
. "$SCRIPTS_DIR/common.sh"
load_config >/dev/null
square=$(openai_image_size_json "" "1:1" "1K" "false")
[ "$square" = '{"width":1024,"height":1024}' ]
landscape=$(openai_image_size_json "" "4:3" "2K" "false")
[ "$landscape" = '{"width":2048,"height":1536}' ]
preset=$(openai_image_size_json "portrait_16_9" "1:1" "1K" "false")
[ "$preset" = '"portrait_16_9"' ]
auto_size=$(openai_image_size_json "" "auto" "1K" "true")
[ "$auto_size" = '"auto"' ]
custom=$(openai_image_size_json "1280x720" "1:1" "1K" "false")
[ "$custom" = '{"width":1280,"height":720}' ]
if openai_image_size_json "1000x700" "1:1" "1K" "false" >/dev/null 2>&1; then
echo "Expected invalid custom size to fail"
exit 1
fi
echo "test_openai_image_size: all passed"
#!/bin/sh
# Upload local file to fal.ai storage and return URL
# POSIX sh compatible — works in cloud sandboxes and locally
# Usage: ./upload.sh --file /path/to/image.png
# Output: URL that can be used in edit.sh --image-urls
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
. "$SCRIPT_DIR/common.sh"
load_config
# Defaults
FILE_PATH=""
OUTPUT_MODE="url" # url or base64
# Parse args
while [ $# -gt 0 ]; do
case $1 in
--file|-f) FILE_PATH="$2"; shift 2 ;;
--base64) OUTPUT_MODE="base64"; shift ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
if [ -z "$FILE_PATH" ]; then
echo "Error: --file is required"
echo "Usage: $0 --file /path/to/image.png"
exit 1
fi
if [ ! -f "$FILE_PATH" ]; then
echo "Error: File not found: $FILE_PATH"
exit 1
fi
# Detect MIME type
MIME_TYPE=$(file -b --mime-type "$FILE_PATH")
# If base64 mode, just output data URI
if [ "$OUTPUT_MODE" = "base64" ]; then
BASE64_DATA=$(base64 -w0 "$FILE_PATH" 2>/dev/null || base64 "$FILE_PATH")
echo "data:$MIME_TYPE;base64,$BASE64_DATA"
exit 0
fi
# Upload to fal.ai storage
FILENAME=$(basename "$FILE_PATH")
echo "Uploading $FILENAME to fal.ai storage..." >&2
# Step 1: Get presigned upload URL
INITIATE_RESPONSE=$(curl -s -X POST "https://rest.alpha.fal.ai/storage/upload/initiate" \
-H "Authorization: Key $FAL_KEY" \
-H "Content-Type: application/json" \
-d "{\"file_name\": \"$FILENAME\", \"content_type\": \"$MIME_TYPE\"}")
UPLOAD_URL=$(echo "$INITIATE_RESPONSE" | grep -oE '"upload_url":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
FILE_URL=$(echo "$INITIATE_RESPONSE" | grep -oE '"file_url":[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
if [ -z "$UPLOAD_URL" ] || [ -z "$FILE_URL" ]; then
echo "Error: Failed to get upload URL" >&2
echo "$INITIATE_RESPONSE" >&2
# Fallback to base64 data URI
echo "Falling back to base64 data URI..." >&2
BASE64_DATA=$(base64 -w0 "$FILE_PATH" 2>/dev/null || base64 "$FILE_PATH")
echo "data:$MIME_TYPE;base64,$BASE64_DATA"
exit 0
fi
# Step 2: Upload file to presigned URL
UPLOAD_RESULT=$(curl -s -X PUT "$UPLOAD_URL" \
-H "Content-Type: $MIME_TYPE" \
--data-binary "@$FILE_PATH")
echo "Uploaded: $FILENAME" >&2
echo "$FILE_URL"
Related skills
FAQ
Which models does fal-ai-image support?
Google Nano Banana Pro (default) and OpenAI GPT Image 2, both hosted on fal.ai and selectable from config or a --model flag.
When should edit mode be used instead of generate mode?
When the user provides reference images, use edit mode (upload then edit); use generate mode for text-only generation.