168 lines
6.4 KiB
Python
168 lines
6.4 KiB
Python
"""나노바나나(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)
|