Files
if@impactframes.ai 7a1f12efaf fix(gemini): Refine image generation logic and config handling
Resolved an issue where Gemini API would not return images in 'generate' mode when an input image was present. The logic now only passes images in 'edit' or 'variation' modes. Fixed a bug where the ResolutionSelector's 'size' output was being overridden by a default 'aspect_ratio' in the Gemini config node. Updated presets and example workflows to align with these changes.
2025-09-06 11:11:37 +01:00

412 lines
17 KiB
Python

"""Gemini Image Generation API
Supports both native Gemini multimodal image generation (Gemini 2.0 Flash)
and dedicated Imagen models (Imagen 3, Imagen 4, Imagen 4 Ultra).
"""
import asyncio
import base64
import logging
from typing import Optional, Dict, Any, List, Union
from io import BytesIO
from PIL import Image
from google import genai
from google.genai import types
logger = logging.getLogger(__name__)
# Aspect ratio to dimensions mapping
ASPECT_RATIO_DIMENSIONS = {
"1:1": (1024, 1024),
"16:9": (1408, 768),
"9:16": (768, 1408),
"4:3": (1280, 896),
"3:4": (896, 1280),
}
def _convert_size_to_aspect_ratio(size: str) -> str:
"""Convert OpenAI-style size to aspect ratio."""
size_to_aspect = {
"1024x1024": "1:1",
"1792x1024": "16:9",
"1024x1792": "9:16",
}
return size_to_aspect.get(size, "1:1")
async def send_gemini_native_image_request(
api_key: str,
prompt: str,
model: str = "gemini-2.5-flash-image-preview", # Updated to latest model
n: int = 1,
size: Optional[str] = None,
aspect_ratio: Optional[str] = None,
seed: Optional[int] = None,
temperature: float = 0.7,
max_tokens: int = 8192,
input_images_base64: Optional[List[str]] = None, # NEW: Support for input images
**kwargs
) -> Dict[str, Any]:
"""Generate images using Gemini native multimodal capabilities"""
if not api_key:
raise ValueError("Gemini image generation requires a valid API key")
# Create client with API key
client = genai.Client(api_key=api_key)
# Determine aspect ratio and dimensions
if not aspect_ratio and size:
aspect_ratio = _convert_size_to_aspect_ratio(size)
elif not aspect_ratio:
aspect_ratio = "1:1"
# Get target dimensions
target_width, target_height = ASPECT_RATIO_DIMENSIONS.get(aspect_ratio, (1024, 1024))
logger.info(f"Using resolution {target_width}x{target_height} for aspect ratio {aspect_ratio}")
# Build content list with prompt and optional input images
contents = []
# Add input images first if provided (for image editing/variations)
if input_images_base64:
from PIL import Image
import base64
from io import BytesIO
for img_b64 in input_images_base64:
try:
# Convert base64 to PIL Image (Gemini SDK format)
image_bytes = base64.b64decode(img_b64)
pil_image = Image.open(BytesIO(image_bytes))
contents.append(pil_image)
except Exception as e:
logger.error(f"Error processing input image: {e}")
continue
# Prepare text prompt with dimensions
if input_images_base64:
# For image editing/variation, modify the prompt approach
prompt_text = f"Based on the provided image(s), {prompt}. Generate a high-quality {target_width}x{target_height} image."
else:
# For text-to-image generation
prompt_text = f"Generate a detailed, high-quality image with dimensions {target_width}x{target_height} of: {prompt}"
contents.append(prompt_text)
all_images = []
for i in range(n):
try:
# Set up generation config
config_args = {
"temperature": temperature,
"response_modalities": ["IMAGE", "TEXT"], # Request both image and text
"max_output_tokens": max_tokens,
}
# Generate a unique seed for each image if seed is provided
if seed is not None:
config_args["seed"] = (seed + i) % (2**31 - 1)
logger.info(f"Generating image {i+1}/{n} with seed {config_args['seed']}")
generation_config = types.GenerateContentConfig(**config_args)
# Generate content with images and prompt
response = client.models.generate_content(
model=model,
contents=contents,
config=generation_config
)
# Debug logging
logger.info(f"Response type: {type(response)}")
if hasattr(response, 'candidates'):
logger.info(f"Number of candidates: {len(response.candidates) if response.candidates else 0}")
# Extract images from response
image_found = False
if hasattr(response, 'candidates') and response.candidates:
for candidate in response.candidates:
if hasattr(candidate, 'content') and hasattr(candidate.content, 'parts'):
logger.info(f"Number of parts in candidate: {len(candidate.content.parts)}")
for idx, part in enumerate(candidate.content.parts):
logger.info(f"Part {idx} type: {type(part)}, has inline_data: {hasattr(part, 'inline_data')}")
if hasattr(part, 'text') and part.text:
logger.info(f"Part {idx} contains text: {part.text[:100] if part.text else 'None'}...")
# Extract image data
if hasattr(part, 'inline_data') and part.inline_data:
image_found = True
try:
# The image data is already in bytes
image_bytes = part.inline_data.data
# Validate image data
if not image_bytes or len(image_bytes) == 0:
logger.warning("Received empty image data from Gemini")
continue
# Convert to base64
image_base64 = base64.b64encode(image_bytes).decode('utf-8')
# Validate base64 encoding
if image_base64 and len(image_base64) > 100: # Basic sanity check
all_images.append({"b64_json": image_base64})
logger.info(f"Successfully processed Gemini image (size: {len(image_bytes)} bytes)")
else:
logger.warning("Generated invalid or too small base64 image data")
except Exception as e:
logger.error(f"Error processing Gemini image data: {e}")
if not image_found:
logger.warning(f"No image found in response {i+1}. The model may have returned only text.")
# Add small delay between requests if generating multiple
if i < n - 1:
await asyncio.sleep(0.5)
except Exception as e:
logger.error(f"Error generating image {i+1}: {e}")
continue
if not all_images:
logger.error("No images were generated")
return {"error": "No images generated", "data": []}
return {"data": all_images}
async def send_imagen_image_request(
api_key: str,
prompt: str,
model: str = "imagen-3.0-generate-002",
n: int = 1,
aspect_ratio: str = "1:1",
person_generation: str = "allow_adult",
safety_filter_level: str = "block_medium_and_above",
language: Optional[str] = None,
seed: Optional[int] = None,
**kwargs
) -> Dict[str, Any]:
"""Generate images using Google's Imagen models"""
if not api_key:
raise ValueError("Imagen generation requires a valid API key")
# Create client with API key
client = genai.Client(api_key=api_key)
# Note: For Imagen 4 Ultra, only 1 image can be generated at a time
if "ultra" in model.lower() and n > 1:
logger.warning("Imagen 4 Ultra only supports generating 1 image at a time. Setting n=1.")
n = 1
try:
# Set up generation config for Imagen
config_args = {
# Required / supported fields
"number_of_images": n,
"aspect_ratio": aspect_ratio,
}
# Optional fields with mapping / validation -------------------------
# person_generation is allowed unless it's the default (allow_adult)
if person_generation and person_generation != "allow_adult":
config_args["person_generation"] = person_generation
# Map legacy safety level names (block_few / some / most) ➜ new names
legacy_map = {
"block_few": "block_low_and_above",
"block_some": "block_medium_and_above",
"block_most": "block_high_and_above",
}
if safety_filter_level:
mapped_level = legacy_map.get(safety_filter_level, safety_filter_level)
# Imagen preview models currently only support block_low_and_above
if mapped_level not in {
"block_low_and_above",
"block_medium_and_above",
"block_high_and_above",
}:
mapped_level = "block_low_and_above"
# Preview Imagen endpoints currently support only block_low_and_above
if "preview" in model.lower():
mapped_level = "block_low_and_above"
config_args["safety_filter_level"] = mapped_level
# Language (omit if 'auto')
if language and language != "auto":
config_args["language"] = language
generation_config = types.GenerateImagesConfig(**config_args)
# Generate images using the Imagen-specific method
response = client.models.generate_images(
model=model,
prompt=prompt,
config=generation_config
)
# Process generated images
all_images: List[Dict[str, str]] = []
if getattr(response, "generated_images", None):
gen_images = response.generated_images or []
for generated_image in gen_images:
try:
img_obj = getattr(generated_image, "image", None)
if img_obj is None:
continue
image_bytes = None
# The SDK may return either raw bytes or a PIL Image
if isinstance(img_obj, bytes):
image_bytes = img_obj
else:
# It's an Image object. Its `save` method expects a file path.
import tempfile
import os
fp, temp_path = tempfile.mkstemp(suffix=".png")
os.close(fp)
try:
# Save to the temporary path
img_obj.save(temp_path)
# Read the bytes back
with open(temp_path, 'rb') as f:
image_bytes = f.read()
except Exception as e:
logger.error(f"Failed to process image via temporary file: {e}")
finally:
# Clean up the temp file
if os.path.exists(temp_path):
os.unlink(temp_path)
if image_bytes:
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
all_images.append({"b64_json": image_base64})
else:
logger.warning("Could not get image bytes for one returned image.")
except Exception as e:
logger.error(f"Error processing Imagen response item: {e}")
else:
# Fallback: some Imagen responses embed images in candidates/parts (rare)
parts = getattr(response, "candidates", None)
if parts:
for cand in parts:
if hasattr(cand, "content") and hasattr(cand.content, "parts"):
for part in cand.content.parts:
if hasattr(part, "inline_data") and part.inline_data:
try:
image_bytes = part.inline_data.data
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
all_images.append({"b64_json": image_base64})
except Exception:
pass
if not all_images:
logger.error("No images were generated by Imagen")
return {"error": "No images generated", "data": []}
return {"data": all_images}
except Exception as e:
error_msg = str(e)
logger.error(f"Imagen generation error: {error_msg}")
# Check for specific error types
if "404" in error_msg or "not found" in error_msg.lower():
logger.info(f"Model {model} not found, trying alternative names...")
# Try alternative model names
alt_models = {
"imagen-3.0-generate-001": "imagen-3.0-generate-002",
"imagen-4.0-generate-001": "imagen-4.0-generate-preview-06-06",
"imagen-4.0-ultra-generate-001": "imagen-4.0-ultra-generate-preview-06-06",
}
if model in alt_models:
return await send_imagen_image_request(
api_key=api_key,
prompt=prompt,
model=alt_models[model],
n=n,
aspect_ratio=aspect_ratio,
person_generation=person_generation,
safety_filter_level=safety_filter_level,
language=language,
seed=seed,
**kwargs
)
raise
async def send_gemini_image_generation_unified(
api_key: str,
model: str,
prompt: str,
n: int = 1,
size: Optional[str] = None,
aspect_ratio: Optional[str] = None,
seed: Optional[int] = None,
edit_mode: bool = False,
variation_mode: bool = False,
input_image_base64: Optional[Union[str, List[str]]] = None,
mask_base64: Optional[str] = None,
**kwargs
) -> Dict[str, Any]:
"""Unified entry point for Gemini/Imagen image generation"""
# Normalize model name (remove preview suffixes for routing)
model_lower = model.lower()
# Map preview models to their stable versions if needed
model_mapping = {
"imagen-3.0-generate-preview-06-06": "imagen-3.0-generate-002",
"imagen-3-light-alpha": "imagen-3.0-generate-002", # Legacy naming
"gemini-2.0-flash-preview-image-generation": "gemini-2.5-flash-image-preview", # Upgrade to latest
}
if model in model_mapping:
logger.info(f"Mapping model {model} to {model_mapping[model]}")
model = model_mapping[model]
# Route to appropriate function
if "imagen" in model_lower:
# Imagen models
return await send_imagen_image_request(
api_key=api_key,
prompt=prompt,
model=model,
n=n,
aspect_ratio=aspect_ratio or _convert_size_to_aspect_ratio(size or "1024x1024"),
person_generation=kwargs.get("person_generation", "allow_adult"),
safety_filter_level=kwargs.get("safety_filter_level", "block_medium_and_above"),
language=kwargs.get("language"),
seed=seed,
)
else:
# Gemini native models (including image generation models)
# Support for both gemini-2.5-flash-image-preview and gemini-2.0-flash-preview-image-generation
# Prepare input images for native Gemini (only for edit/variation)
input_images_for_native = None
if (edit_mode or variation_mode) and input_image_base64:
if isinstance(input_image_base64, list):
input_images_for_native = input_image_base64
else:
input_images_for_native = [input_image_base64]
logger.info(f"Gemini native: Using {len(input_images_for_native)} input images for edit/variation.")
return await send_gemini_native_image_request(
api_key=api_key,
prompt=prompt,
model=model,
n=n,
size=size,
aspect_ratio=aspect_ratio,
seed=seed,
temperature=kwargs.get("temperature", 0.7),
max_tokens=kwargs.get("max_tokens", 8192),
input_images_base64=input_images_for_native,
)