"""나노바나나(Gemini 이미지 생성) 연동 클라이언트. 킨텍스 AI 시스템의 모든 이미지 생성은 이 모듈을 통해서만 수행한다. 환경변수 GEMINI_API_KEY 필요. 키가 없으면 명확한 에러를 낸다. 사용 예: from tools.nanobanana.client import NanoBananaClient client = NanoBananaClient() result = client.generate_booth_photo( booth_spec={ "hall": "제1전시장 1홀", "size_m": (6, 3), "booth_type": "독립부스(목공)", "signage": "주식회사 가디아", "brand_color": "#0052A5", "materials": ["백색 무광 목공", "그레이 카펫"], "lighting": {"color_temp_k": 4000, "accents": ["로고 스포트라이트"]}, }, view="front", # front(S1) | aisle(S3) | interior(S4) | aerial(S7 홀 조감) time_of_day="day", # day | night(S2 조명 평가용) reference_image=None, # 빈 부스 실측 사진 경로 (구조 보존 합성용, S5 before/after의 before) ) result.save("booth_after.png") """ from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from typing import Optional MODEL_NAME = os.environ.get("NANOBANANA_MODEL", "gemini-2.5-flash-image") PROMPTS_DIR = Path(__file__).parent / "prompts" class NanoBananaError(RuntimeError): pass @dataclass class GeneratedImage: data: bytes mime_type: str prompt_used: str def save(self, path: str | Path) -> Path: p = Path(path) p.write_bytes(self.data) return p def _load_template(name: str) -> str: f = PROMPTS_DIR / f"{name}.txt" if not f.exists(): raise NanoBananaError(f"프롬프트 템플릿 없음: {f}") return f.read_text(encoding="utf-8") class NanoBananaClient: """Gemini 이미지 생성 API 래퍼. google-genai SDK 사용.""" def __init__(self, api_key: Optional[str] = None, model: str = MODEL_NAME): self.api_key = api_key or os.environ.get("GEMINI_API_KEY") if not self.api_key: raise NanoBananaError( "GEMINI_API_KEY 환경변수가 필요합니다. " "https://aistudio.google.com 에서 발급 후 설정하세요." ) self.model = model # 지연 임포트: SDK 미설치 환경에서도 모듈 로드는 가능하게 try: from google import genai # type: ignore except ImportError as e: raise NanoBananaError("pip install google-genai 필요") from e self._client = genai.Client(api_key=self.api_key) # ------------------------------------------------------------------ def generate( self, prompt: str, reference_image: Optional[str | Path] = None, ) -> GeneratedImage: """저수준 생성. 프롬프트(+선택적 참조 이미지) → 이미지 1장.""" contents: list = [] if reference_image: import mimetypes from google.genai import types # type: ignore ref = Path(reference_image) mime = mimetypes.guess_type(ref.name)[0] or "image/png" contents.append( types.Part.from_bytes(data=ref.read_bytes(), mime_type=mime) ) contents.append(prompt) resp = self._client.models.generate_content( model=self.model, contents=contents ) for part in resp.candidates[0].content.parts: if getattr(part, "inline_data", None): return GeneratedImage( data=part.inline_data.data, mime_type=part.inline_data.mime_type, prompt_used=prompt, ) raise NanoBananaError("이미지가 반환되지 않았습니다: " + str(resp)) # ------------------------------------------------------------------ def generate_booth_photo( self, booth_spec: dict, view: str = "front", time_of_day: str = "day", reference_image: Optional[str | Path] = None, ) -> GeneratedImage: """부스 스펙 → 시공 후 사실적 사진. 템플릿: prompts/booth_photo.txt""" template = _load_template("booth_photo") w, d = booth_spec.get("size_m", (3, 3)) view_phrase = { "front": "eye-level front view of the booth", "aisle": "view from the visitor aisle looking at the booth", "interior": "view from inside the booth showing interior fixtures", "aerial": "high-angle aerial overview of the exhibition hall floor with this booth visible in context", # S7 }.get(view, view) prompt = template.format( hall=booth_spec.get("hall", "KINTEX exhibition hall"), width=w, depth=d, booth_type=booth_spec.get("booth_type", "custom wooden booth"), signage=booth_spec.get("signage", ""), brand_color=booth_spec.get("brand_color", "#0052A5"), materials=", ".join(booth_spec.get("materials", [])), color_temp=booth_spec.get("lighting", {}).get("color_temp_k", 4000), accents=", ".join(booth_spec.get("lighting", {}).get("accents", [])), view=view_phrase, time_of_day=time_of_day, ) return self.generate(prompt, reference_image=reference_image) #: S6 배선 색상 규약 (design.md §1-2와 동일) WIRING_COLORS = {"power": "red", "network": "blue", "plumbing": "green"} def generate_wiring_overlay( self, layout_json: dict, kind: str = "network" ) -> GeneratedImage: """배치도 + 배선 계획 → 시공 안내용 오버레이 이미지(S6). kind: network(청) | power(적) | plumbing(급배수, 녹) ※ 주의(PLANNING §6-3): S6의 1차 산출물은 좌표 정합이 보장되는 래스터 합성(백엔드 렌더러)이며, 이 생성형 오버레이는 발표/설명용 보조 이미지다. 시공 검증용으로 사용하지 말 것. """ if kind not in self.WIRING_COLORS: raise NanoBananaError(f"kind는 {list(self.WIRING_COLORS)} 중 하나: {kind}") template = _load_template("wiring_overlay") import json prompt = template.format( kind=kind, color=self.WIRING_COLORS[kind], layout=json.dumps(layout_json, ensure_ascii=False), ) return self.generate(prompt)