""" Google Gemini provider implementation. Supports chat and image generation (Nano Banana / Nano Banana Pro). """ import aiohttp from typing import Any import torch from .base import BaseProvider, ChatResponse from ..image_utils import tensor_to_base64, base64_to_tensor class GeminiProvider(BaseProvider): """Google Gemini API provider with image generation support.""" @property def default_base_url(self) -> str: return "https://generativelanguage.googleapis.com/v1beta" def _build_contents( self, messages: list[dict[str, Any]], images: list[torch.Tensor] | None = None, ) -> tuple[str | None, list[dict[str, Any]]]: """ Build Gemini-format contents. Returns: Tuple of (system_instruction, contents) """ system = None contents = [] for i, msg in enumerate(messages): role = msg["role"] content = msg["content"] # Extract system message if role == "system": system = content continue # Map roles gemini_role = "user" if role == "user" else "model" # Build parts parts = [] # Add images to last user message if role == "user" and images and i == len(messages) - 1: for img in images: b64 = tensor_to_base64(img) parts.append({ "inline_data": { "mime_type": "image/png", "data": b64, } }) parts.append({"text": content}) contents.append({"role": gemini_role, "parts": parts}) return system, contents async def chat( self, messages: list[dict[str, Any]], model: str, temperature: float = 1.0, max_tokens: int = 2048, images: list[torch.Tensor] | None = None, enable_image_generation: bool = False, **kwargs, ) -> ChatResponse: """Send chat request to Gemini API.""" url = f"{self.base_url}/models/{model}:generateContent" # Add API key as query parameter if "?" in url: url += f"&key={self.api_key}" else: url += f"?key={self.api_key}" headers = {"Content-Type": "application/json"} system, contents = self._build_contents(messages, images) payload = { "contents": contents, "generationConfig": { "temperature": temperature, "maxOutputTokens": max_tokens, } } if system: payload["systemInstruction"] = {"parts": [{"text": system}]} # Enable image generation if requested if enable_image_generation: payload["generationConfig"]["responseModalities"] = ["TEXT", "IMAGE"] async with aiohttp.ClientSession() as session: async with session.post(url, headers=headers, json=payload) as resp: if resp.status != 200: error_text = await resp.text() raise RuntimeError(f"Gemini API error {resp.status}: {error_text}") data = await resp.json() # Extract text and images from response text = "" result_image = None candidates = data.get("candidates", []) if candidates: parts = candidates[0].get("content", {}).get("parts", []) for part in parts: if "text" in part: text += part["text"] elif "inlineData" in part: inline = part["inlineData"] if inline.get("mimeType", "").startswith("image/"): result_image = base64_to_tensor(inline["data"]) return ChatResponse(text=text, image=result_image, raw_response=data) async def generate_image( self, prompt: str, model: str, reference_image: torch.Tensor | None = None, aspect_ratio: str = "1:1", size: str = "1K", **kwargs, ) -> ChatResponse: """Generate or edit image using Gemini.""" url = f"{self.base_url}/models/{model}:generateContent?key={self.api_key}" headers = {"Content-Type": "application/json"} # Build parts parts = [] # Add reference image if provided if reference_image is not None: b64 = tensor_to_base64(reference_image) parts.append({ "inline_data": { "mime_type": "image/png", "data": b64, } }) parts.append({"text": prompt}) payload = { "contents": [{"parts": parts}], "generationConfig": { "responseModalities": ["TEXT", "IMAGE"], } } # Add image config if supported if aspect_ratio or size: payload["generationConfig"]["imageConfig"] = {} if aspect_ratio: payload["generationConfig"]["imageConfig"]["aspectRatio"] = aspect_ratio if size: payload["generationConfig"]["imageConfig"]["imageSize"] = size async with aiohttp.ClientSession() as session: async with session.post(url, headers=headers, json=payload) as resp: if resp.status != 200: error_text = await resp.text() raise RuntimeError(f"Gemini API error {resp.status}: {error_text}") data = await resp.json() # Extract text and image text = "" result_image = None candidates = data.get("candidates", []) if candidates: parts = candidates[0].get("content", {}).get("parts", []) for part in parts: if "text" in part: text += part["text"] elif "inlineData" in part: inline = part["inlineData"] if inline.get("mimeType", "").startswith("image/"): result_image = base64_to_tensor(inline["data"]) if result_image is None: raise RuntimeError("Gemini did not return an image. Try a different prompt or model.") return ChatResponse(text=text, image=result_image, raw_response=data)