"""Gemini(Nano Banana 등) 기반 조감도 AI 렌더링 워커. scanvas_maker.SCanvasApp의 백그라운드 스레드에서 호출되며, app 인스턴스를 통해 상태(capture_image / job_logger / log / after / dxf_path / 등)에 접근한다. scanvas_maker로부터 ~264줄을 분리해 AI 호출 로직을 모듈 단위로 격리. 공개 API: run_gemini_render(app, credential, prompt, use_vertex=False, location="us-central1") """ from __future__ import annotations import io import os import time as _time from pathlib import Path from tkinter import messagebox from PIL import Image import contextlib # Harness 의존 (선택적 — 없어도 동작) try: from harness.seed_manager import SeedManager from harness.logger import get_db_session _HARNESS_OK = True except Exception: SeedManager = None # type: ignore get_db_session = None # type: ignore _HARNESS_OK = False def run_gemini_render(app, credential: str, prompt: str, use_vertex: bool = False, location: str = "us-central1") -> None: """Gemini 자동 호출 + Harness 통합. app = SCanvasApp 인스턴스. Args: app: scanvas_maker.SCanvasApp (상태/로그/UI scheduling 접근) credential: use_vertex=True이면 GCP Project ID, 아니면 API Key prompt: 렌더 프롬프트 use_vertex: True면 Vertex AI (google-genai SDK), False면 API Key 경로 location: Vertex AI region ("global"=gemini-3.x, "us-central1"=2.x) """ t_start = _time.time() job_id = None db = None dxf_hash = app._get_dxf_hash() prompt_hash = app._get_prompt_hash(prompt) prompt_ver = "v1" seed = 0 if app.job_logger and _HARNESS_OK: try: db = get_db_session() job = app.job_logger.create_job(db, app.dxf_path or "unknown", dxf_hash) job_id = job.id seed = app.seed_mgr.get_or_create_seed(db, job_id, dxf_hash) if app.prompt_reg: prompt_ver = app.prompt_reg.latest_version() or "v1" app.job_logger.start_job(db, job_id, seed, prompt_ver, prompt_hash) app.after(0, lambda: app.log( f" Harness: job#{job_id}, {SeedManager.describe(seed)}, prompt={prompt_ver}")) except Exception as e: app.after(0, lambda e=e: app.log(f" Harness 초기화 경고: {e}")) try: from google import genai from google.genai import types as gtypes sdk_version = "vertex" if use_vertex else "new" except ImportError: if use_vertex: if app.job_logger and db and job_id: app.job_logger.fail_job(db, job_id, "google-genai SDK 미설치") app.after(0, lambda: messagebox.showerror("패키지 필요", "Vertex AI는 google-genai SDK가 필요합니다.\n" "pip install google-genai\n" "그리고 gcloud auth application-default login")) return try: import google.generativeai as genai_legacy # type: ignore sdk_version = "legacy" except ImportError: if app.job_logger and db and job_id: app.job_logger.fail_job(db, job_id, "google-genai SDK 미설치") app.after(0, lambda: messagebox.showerror("패키지 필요", "pip install google-generativeai\n또는\npip install google-genai")) return try: source_img = app.capture_image if app.capture_image else app.guide_image input_path = os.path.abspath("capture_for_ai.png") source_img.save(input_path) app.after(0, lambda: app.log(f" 입력 이미지: {source_img.size}")) with open(input_path, "rb") as f: img_bytes = f.read() render_prompt = ( f"Transform this aerial/satellite terrain capture into a photorealistic " f"bird's-eye view architectural rendering. " f"CRITICAL: Preserve the EXACT terrain layout, roads, water, structures. " f"The scene may combine a high-detail DXF area (center) with a lower-detail " f"real DEM+satellite outer ring (edges). Render BOTH areas seamlessly — " f"the outer ring is actual surrounding terrain, NOT filler to crop out. " f"Keep the full image frame; do NOT trim to the central bbox. " f"Dark road areas = freshly paved asphalt. Cut slopes = exposed earth. " f"Enhance vegetation textures, water reflections, natural lighting. " f"Style: {prompt}" ) app.after(0, lambda: app.log( f" Gemini API 호출 중... (SDK: {sdk_version}, " f"loc: {location if use_vertex else '-'})")) rendered = None if sdk_version == "vertex": try: client = genai.Client( vertexai=True, project=credential, location=location, ) except Exception as ce: err = str(ce)[:160] app.after(0, lambda: app.log( f" Vertex AI 클라이언트 생성 실패: {err}")) has_sa = bool(os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")) if has_sa: app.after(0, lambda: app.log( " → gcp-key.json 경로 확인. 서비스 계정에 " "aiplatform.user 권한이 있는지 확인하세요.")) else: app.after(0, lambda: app.log( " → gcp-key.json을 프로젝트 루트에 배치하거나 " "gcloud auth application-default login 실행")) if app.job_logger and db and job_id: app.job_logger.fail_job(db, job_id, "Vertex 인증 실패") return model_priority = [ ("gemini-3.1-flash-image-preview", location), ("gemini-3-pro-image-preview", location), ("gemini-2.5-flash-image", "us-central1"), ("gemini-2.5-flash-image-preview", "us-central1"), ("gemini-2.0-flash-preview-image-generation", "us-central1"), ] current_loc = location for model_name, model_loc in model_priority: if model_loc != current_loc: try: client = genai.Client( vertexai=True, project=credential, location=model_loc, ) current_loc = model_loc except Exception: continue try: response = client.models.generate_content( model=model_name, contents=[ gtypes.Part.from_bytes(data=img_bytes, mime_type="image/png"), render_prompt ], config=gtypes.GenerateContentConfig( response_modalities=["IMAGE"], ) ) for part in response.candidates[0].content.parts: if hasattr(part, 'inline_data') and part.inline_data and \ part.inline_data.mime_type.startswith("image/"): rendered = Image.open(io.BytesIO(part.inline_data.data)) break if rendered: _m, _l = model_name, current_loc app.after(0, lambda _m=_m, _l=_l: app.log( f" [Vertex] 모델 {_m} @ {_l} 성공")) break except Exception as exc: _m, _e = model_name, str(exc)[:120] app.after(0, lambda _m=_m, _e=_e: app.log(f" [Vertex] {_m}: {_e}")) elif sdk_version == "new": client = genai.Client(api_key=credential) for model_name in ["gemini-2.5-flash-image", "gemini-2.0-flash-preview-image-generation"]: try: response = client.models.generate_content( model=model_name, contents=[ gtypes.Part.from_bytes(data=img_bytes, mime_type="image/png"), render_prompt ], config=gtypes.GenerateContentConfig( response_modalities=["IMAGE", "TEXT"], ) ) for part in response.candidates[0].content.parts: if hasattr(part, 'inline_data') and part.inline_data and \ part.inline_data.mime_type.startswith("image/"): rendered = Image.open(io.BytesIO(part.inline_data.data)) break if rendered: _m = model_name app.after(0, lambda _m=_m: app.log(f" 모델 {_m} 성공")) break except Exception as exc: _m, _e = model_name, str(exc)[:80] app.after(0, lambda _m=_m, _e=_e: app.log(f" {_m}: {_e}")) else: genai_legacy.configure(api_key=credential) pil_img = Image.open(io.BytesIO(img_bytes)) for model_name in ["gemini-2.5-flash-image", "gemini-2.0-flash-preview-image-generation"]: try: model = genai_legacy.GenerativeModel(model_name) response = model.generate_content( [pil_img, render_prompt] ) if hasattr(response, 'candidates') and response.candidates: for part in response.candidates[0].content.parts: if (hasattr(part, 'inline_data') and part.inline_data and part.inline_data.mime_type.startswith("image/")): rendered = Image.open(io.BytesIO(part.inline_data.data)) break if rendered: _m = model_name app.after(0, lambda _m=_m: app.log(f" 모델 {_m} 성공")) break except Exception as exc: _m, _e = model_name, str(exc)[:80] app.after(0, lambda _m=_m, _e=_e: app.log(f" {_m}: {_e}")) if not rendered: if app.job_logger and db and job_id: app.job_logger.fail_job(db, job_id, "Gemini 이미지 생성 실패") app.after(0, lambda: app.log(" Gemini 이미지 생성 실패. API Key와 모델을 확인하세요.")) app.after(0, lambda: app.set_status("이미지 생성 실패", "#E74C3C")) return # 출력 화질 후처리 — Step 4에서 고른 HD/FHD/UHD 로 리사이즈 tgt = getattr(app, "target_resolution", None) if tgt and tgt[0] > 0 and tgt[1] > 0 and rendered.size != tuple(tgt): src_size = rendered.size app.after(0, lambda s=src_size, t=tgt: app.log( f" 화질 리사이즈: {s[0]}x{s[1]} → {t[0]}x{t[1]}")) rendered = rendered.resize(tuple(tgt), Image.LANCZOS) output_path = "rendered_birdseye.png" rendered.save(output_path) latency_ms = (_time.time() - t_start) * 1000 quality_score = 0.0 if app.quality_val: try: vr = app.quality_val.validate(Path(output_path)) quality_score = vr.score app.after(0, lambda: app.log(f" 품질검증: {vr.summary}")) except Exception as e: app.after(0, lambda e=e: app.log(f" 품질검증 오류: {e}")) if app.job_logger and db and job_id: with contextlib.suppress(Exception): app.job_logger.complete_job(db, job_id, output_path, quality_score, latency_ms) app.after(0, lambda: app.log( f" Gemini 렌더링 완료! → {output_path} ({rendered.size}) " f"[{latency_ms:.0f}ms, 품질={quality_score:.2f}]")) app.after(0, lambda: app.set_status("AI 렌더링 완료", "#2ECC71")) app.after(0, lambda: app._show_rendered_result(output_path)) except Exception as e: if app.job_logger and db and job_id: with contextlib.suppress(Exception): app.job_logger.fail_job(db, job_id, str(e)) err_msg = str(e)[:300] app.after(0, lambda: app.log(f" Gemini 오류: {err_msg}")) app.after(0, lambda: app.set_status("렌더링 실패", "#E74C3C")) app.after(0, lambda: messagebox.showerror("Gemini 오류", f"API 호출 오류:\n{err_msg}")) finally: if db: with contextlib.suppress(Exception): db.close()