返回 ViMax
image_generator_nanobanana_google_api.py
根目录 / tools / image_generator_nanobanana_google_api.py
1 # https://ai.google.dev/gemini-api/docs/image-generation
2
3 import logging
4 import asyncio
5 from PIL import Image
6 from typing import List, Optional
7 from google import genai
8 from google.genai import types
9 from google.genai.errors import ClientError
10 from tenacity import retry, stop_after_attempt, wait_exponential
11 from interfaces.image_output import ImageOutput
12 from tools.image_orientation import ensure_not_portrait, landscape_guard_requested
13 from tools.image_response import image_from_response_part
14 from utils.retry import after_func
15 from utils.rate_limiter import RateLimiter
16
17
18 class ImageGeneratorNanobananaGoogleAPI:
19 def __init__(
20 self,
21 api_key: str,
22 rate_limiter: Optional[RateLimiter] = None,
23 ):
24 self.model = "gemini-2.5-flash-image"
25 self.rate_limiter = rate_limiter
26 self.client = genai.Client(
27 api_key=api_key,
28 )
29
30 @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), after=after_func, reraise=True)
31 async def generate_single_image(
32 self,
33 prompt: str,
34 reference_image_paths: List[str] = [],
35 aspect_ratio: Optional[str] = "16:9",
36 **kwargs,
37 ) -> ImageOutput:
38
39 """
40 aspect_ratio: The aspect ratio of the image.
41 """
42
43 logging.info(f"Calling {self.model} to generate image...")
44
45 # Apply rate limiting if configured
46 if self.rate_limiter:
47 await self.rate_limiter.acquire()
48
49 reference_images = [Image.open(path) for path in reference_image_paths]
50
51 # Retry logic for rate limit errors
52 max_retries = 3
53 retry_delay = 5
54
55 for attempt in range(max_retries):
56 try:
57 response = await self.client.aio.models.generate_content(
58 model=self.model,
59 contents=reference_images + [prompt],
60 config=types.GenerateContentConfig(
61 response_modalities=["IMAGE"],
62 image_config=types.ImageConfig(
63 aspect_ratio=aspect_ratio,
64 ),
65 ),
66 )
67 break
68 except ClientError as e:
69 if e.status_code == 429 and attempt < max_retries - 1:
70 wait_time = retry_delay * (2 ** attempt)
71 logging.warning(f"Rate limit hit (429), retrying in {wait_time}s... (attempt {attempt + 1}/{max_retries})")
72 await asyncio.sleep(wait_time)
73 else:
74 raise
75
76 image = None
77 text = ""
78 for part in response.candidates[0].content.parts:
79 if part.text is not None:
80 text += part.text
81 elif part.inline_data is not None:
82 image = image_from_response_part(part)
83
84 if image is None:
85 logging.error(f"No image generated. The response text is: {text}")
86 raise ValueError("No image generated")
87
88 if landscape_guard_requested(
89 size=kwargs.get("size"),
90 aspect_ratio=aspect_ratio,
91 enforce_landscape=kwargs.get("enforce_landscape", True),
92 allow_portrait=kwargs.get("allow_portrait", False),
93 ):
94 ensure_not_portrait(image)
95
96 return ImageOutput(fmt="pil", ext="png", data=image)
97
98
98 lines PYTHON