| """Gemini API クライアント""" |
|
|
| import asyncio |
| import time |
| from typing import Optional |
| from dataclasses import dataclass |
| import numpy as np |
| from PIL import Image |
|
|
| from config.settings import settings |
| from utils.image_utils import frame_to_pil |
|
|
|
|
| @dataclass |
| class GeminiResponse: |
| """Gemini APIレスポンス""" |
|
|
| text: str |
| success: bool |
| error: Optional[str] = None |
|
|
|
|
| class GeminiClient: |
| """ |
| Gemini API クライアント |
| - 10 RPM 制限を遵守 |
| - 指数バックオフでリトライ |
| """ |
|
|
| def __init__(self, api_key: Optional[str] = None): |
| self.api_key = api_key or settings.gemini_api_key |
| self._client = None |
| self._model = None |
| self._last_request_time: float = 0 |
| self._min_interval: float = 6.0 |
| self._initialized = False |
|
|
| def _init_client(self) -> bool: |
| """クライアントの遅延初期化""" |
| if self._initialized: |
| return self._client is not None |
|
|
| if not self.api_key: |
| print("Warning: GEMINI_API_KEY is not set") |
| self._initialized = True |
| return False |
|
|
| try: |
| import google.generativeai as genai |
|
|
| genai.configure(api_key=self.api_key) |
| self._client = genai |
| self._model = genai.GenerativeModel(settings.gemini_model) |
| self._initialized = True |
| return True |
| except ImportError: |
| print("Warning: google-generativeai not installed") |
| self._initialized = True |
| return False |
| except Exception as e: |
| print(f"Gemini initialization error: {e}") |
| self._initialized = True |
| return False |
|
|
| def _wait_for_rate_limit(self) -> None: |
| """レート制限のための待機""" |
| elapsed = time.time() - self._last_request_time |
| if elapsed < self._min_interval: |
| time.sleep(self._min_interval - elapsed) |
|
|
| async def _async_wait_for_rate_limit(self) -> None: |
| """非同期レート制限待機""" |
| elapsed = time.time() - self._last_request_time |
| if elapsed < self._min_interval: |
| await asyncio.sleep(self._min_interval - elapsed) |
|
|
| def analyze_image( |
| self, image: Image.Image, prompt: str, max_retries: int = 3 |
| ) -> GeminiResponse: |
| """ |
| 画像を分析 |
| |
| Args: |
| image: PIL Image |
| prompt: 分析プロンプト |
| max_retries: 最大リトライ回数 |
| |
| Returns: |
| GeminiResponse |
| """ |
| if not self._init_client(): |
| return GeminiResponse( |
| text="", |
| success=False, |
| error="Gemini client not initialized", |
| ) |
|
|
| for attempt in range(max_retries): |
| try: |
| self._wait_for_rate_limit() |
| self._last_request_time = time.time() |
|
|
| response = self._model.generate_content([prompt, image]) |
| return GeminiResponse(text=response.text, success=True) |
|
|
| except Exception as e: |
| error_msg = str(e) |
| if "429" in error_msg or "quota" in error_msg.lower(): |
| |
| wait_time = (2**attempt) * 10 |
| print(f"Rate limited, waiting {wait_time}s...") |
| time.sleep(wait_time) |
| elif attempt < max_retries - 1: |
| time.sleep(2**attempt) |
| else: |
| return GeminiResponse( |
| text="", |
| success=False, |
| error=error_msg, |
| ) |
|
|
| return GeminiResponse( |
| text="", |
| success=False, |
| error="Max retries exceeded", |
| ) |
|
|
| def analyze_frame( |
| self, frame: np.ndarray, prompt: str |
| ) -> GeminiResponse: |
| """ |
| NumPyフレームを分析 |
| |
| Args: |
| frame: NumPy配列(BGR形式) |
| prompt: 分析プロンプト |
| |
| Returns: |
| GeminiResponse |
| """ |
| image = frame_to_pil(frame) |
| return self.analyze_image(image, prompt) |
|
|
| async def analyze_image_async( |
| self, image: Image.Image, prompt: str, max_retries: int = 3 |
| ) -> GeminiResponse: |
| """非同期で画像を分析""" |
| if not self._init_client(): |
| return GeminiResponse( |
| text="", |
| success=False, |
| error="Gemini client not initialized", |
| ) |
|
|
| for attempt in range(max_retries): |
| try: |
| await self._async_wait_for_rate_limit() |
| self._last_request_time = time.time() |
|
|
| response = await asyncio.to_thread( |
| self._model.generate_content, [prompt, image] |
| ) |
| return GeminiResponse(text=response.text, success=True) |
|
|
| except Exception as e: |
| error_msg = str(e) |
| if "429" in error_msg or "quota" in error_msg.lower(): |
| wait_time = (2**attempt) * 10 |
| await asyncio.sleep(wait_time) |
| elif attempt < max_retries - 1: |
| await asyncio.sleep(2**attempt) |
| else: |
| return GeminiResponse( |
| text="", |
| success=False, |
| error=error_msg, |
| ) |
|
|
| return GeminiResponse( |
| text="", |
| success=False, |
| error="Max retries exceeded", |
| ) |
|
|
| @property |
| def is_available(self) -> bool: |
| """クライアントが利用可能かどうか""" |
| return self._init_client() |
|
|