Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from bundle
Generate face-consistent images with Nano Banana 2 via fal.ai. Use when the user wants to generate photos of themselves or someone else in different poses, expr
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/nano_banana_edit.py
1#!/usr/bin/env python32"""Generate face-consistent images using fal.ai Nano Banana 2 edit model.34Upload a face reference photo and generate new images preserving face identity5with different expressions, poses, and scenes.67Auto-fallback: if Nano Banana 2 is down (504/downstream_service_unavailable),8automatically retries with Nano Banana v1 (Gemini 3 Pro).9"""10import argparse11import json12import os13import sys14import time15from pathlib import Path16from urllib.request import urlopen1718MODELS = [19"fal-ai/nano-banana-2/edit",20"fal-ai/nano-banana/edit",21]222324def load_fal_key():25"""Resolve fal.ai credentials in priority order."""26for var in ("FAL_KEY", "FAL_API_KEY", "FAL_AI_KEY"):27val = os.environ.get(var)28if val:29os.environ["FAL_KEY"] = val30return val31for path in ("~/.secrets/fal.env", "~/.fal/key"):32p = Path(path).expanduser()33if p.exists():34for line in p.read_text().splitlines():35for prefix in ("FAL_KEY=", "FAL_API_KEY=", "FAL_AI_KEY="):36if line.startswith(prefix):37val = line.split("=", 1)[1].strip()38os.environ["FAL_KEY"] = val39return val40raise SystemExit(41"Missing fal.ai API key. Set FAL_KEY, FAL_API_KEY, or FAL_AI_KEY env var, "42"or pass --api-key."43)444546def try_generate(fal_client, model, fal_args):47"""Try generating with a model. Returns result or raises."""48print(f"Generating with {model}...", file=sys.stderr)49return fal_client.subscribe(model, arguments=fal_args, with_logs=True)505152def is_retryable(exc):53"""Check if error is a transient service issue worth retrying/falling back."""54msg = str(exc).lower()55return any(k in msg for k in [56"downstream_service_unavailable",57"504",58"502",59"503",60"gateway timeout",61"service unavailable",62])636465def main():66parser = argparse.ArgumentParser(67description="Generate face-consistent images with Nano Banana 2 edit (fal.ai)"68)69parser.add_argument("--face", required=True, help="Path to face reference image — portrait, rotation grid, or both work")70parser.add_argument("--prompt", required=True, help="Scene/expression description prompt")71parser.add_argument("--output", required=True, help="Output image path (.png)")72parser.add_argument("--aspect-ratio", default="9:16",73help="Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4 (default: 9:16)")74parser.add_argument("--resolution", default="2K",75help="Resolution: 1K, 2K, 4K (default: 2K)")76parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility")77parser.add_argument("--api-key", help="fal.ai API key (overrides env vars)")78parser.add_argument("--model", default=None,79help="Force specific model (skips auto-fallback)")80parser.add_argument("--no-fallback", action="store_true",81help="Disable auto-fallback to v1")82args = parser.parse_args()8384if args.api_key:85os.environ["FAL_KEY"] = args.api_key86else:87load_fal_key()8889import fal_client9091# Upload face reference if it's a local file92face_url = args.face93if not face_url.startswith("http"):94face_path = Path(face_url).expanduser().resolve()95if not face_path.exists():96raise SystemExit(f"Face reference not found: {face_path}")97print(f"Uploading face reference: {face_path}", file=sys.stderr)98face_url = fal_client.upload_file(str(face_path))99print(f"Uploaded: {face_url}", file=sys.stderr)100101image_urls = [face_url]102103# Build arguments104fal_args = {105"prompt": args.prompt,106"image_urls": image_urls,107"aspect_ratio": args.aspect_ratio,108"resolution": args.resolution,109}110if args.seed is not None:111fal_args["seed"] = args.seed112113# Determine which models to try114if args.model:115models_to_try = [args.model]116elif args.no_fallback:117models_to_try = [MODELS[0]]118else:119models_to_try = MODELS120121# Try each model with retry + fallback122result = None123last_error = None124max_retries = 2125for model in models_to_try:126for attempt in range(1, max_retries + 1):127try:128result = try_generate(fal_client, model, fal_args)129break130except Exception as exc:131last_error = exc132if "invalid_request" in str(exc).lower():133print(f" Prompt rejected by {model}. Try simplifying the expression.", file=sys.stderr)134raise135if is_retryable(exc):136if attempt < max_retries:137print(f" {model} attempt {attempt} failed, retrying...", file=sys.stderr)138time.sleep(3)139continue140elif model != models_to_try[-1]:141print(f" {model} failed {max_retries} times, falling back...", file=sys.stderr)142break143else:144raise145else:146raise147if result is not None:148break149150if result is None:151raise SystemExit(f"All models failed. Last error: {last_error}")152153# Extract image URL154images = result.get("images", [])155if not images and "image" in result:156images = [result["image"]]157158if not images:159print(json.dumps(result, indent=2), file=sys.stderr)160raise SystemExit("No images returned from model")161162url = images[0].get("url", "")163if not url:164raise SystemExit("Image has no URL")165166# Download167output_path = Path(args.output).expanduser().resolve()168output_path.parent.mkdir(parents=True, exist_ok=True)169with urlopen(url, timeout=120) as resp:170output_path.write_bytes(resp.read())171172print(json.dumps({173"ok": True,174"output": str(output_path),175"url": url,176"model": model,177"prompt": args.prompt,178}))179180181if __name__ == "__main__":182main()183