feat: add Gemini-powered birdseye rendering
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from civilplan_mcp.config import get_settings
|
||||
from civilplan_mcp.models import ProjectDomain
|
||||
from civilplan_mcp.prompts.birdseye_templates import build_prompt
|
||||
from civilplan_mcp.services.gemini_image import GeminiImageService
|
||||
from civilplan_mcp.tools._base import wrap_response
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN_TO_PROJECT_TYPE = {
|
||||
"토목_도로": "road",
|
||||
"건축": "building",
|
||||
"토목_상하수도": "water",
|
||||
"토목_하천": "river",
|
||||
"조경": "landscape",
|
||||
"복합": "mixed",
|
||||
}
|
||||
|
||||
|
||||
def _domain_to_project_type(domain: str) -> str:
|
||||
return DOMAIN_TO_PROJECT_TYPE.get(domain, "mixed")
|
||||
|
||||
|
||||
def _resolve_domain(domain: str | None) -> ProjectDomain:
|
||||
try:
|
||||
return ProjectDomain(domain or ProjectDomain.복합.value)
|
||||
except ValueError:
|
||||
return ProjectDomain.복합
|
||||
|
||||
|
||||
def svg_to_png(svg_content: str, output_path: str) -> str:
|
||||
import cairosvg
|
||||
|
||||
cairosvg.svg2png(bytestring=svg_content.encode("utf-8"), write_to=output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def generate_birdseye_view(
|
||||
*,
|
||||
project_summary: str,
|
||||
project_spec: dict[str, Any],
|
||||
svg_drawing: str | None = None,
|
||||
resolution: str = "2K",
|
||||
) -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
domain = _resolve_domain(project_spec.get("domain"))
|
||||
project_id = project_spec.get("project_id", "birdseye-render")
|
||||
|
||||
if not settings.gemini_api_key:
|
||||
return wrap_response(
|
||||
{
|
||||
"status": "error",
|
||||
"project_id": project_id,
|
||||
"error": "GEMINI_API_KEY is not configured. Add it to .env or store it with python setup_keys.py.",
|
||||
},
|
||||
domain,
|
||||
)
|
||||
|
||||
output_dir = Path(project_spec.get("output_dir") or settings.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
details: dict[str, Any] = {}
|
||||
if isinstance(project_spec.get("road"), dict):
|
||||
details.update({key: value for key, value in project_spec["road"].items() if value is not None})
|
||||
for key in ("terrain", "region", "utilities", "year_start", "year_end"):
|
||||
value = project_spec.get(key)
|
||||
if value not in (None, "", [], {}):
|
||||
details[key] = value
|
||||
|
||||
reference_image_path: str | None = None
|
||||
if svg_drawing:
|
||||
try:
|
||||
reference_image_path = svg_to_png(svg_drawing, str(output_dir / f"{project_id}_reference.png"))
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to convert SVG reference for birdseye render: %s", exc)
|
||||
|
||||
service = GeminiImageService(api_key=settings.gemini_api_key)
|
||||
project_type = _domain_to_project_type(domain.value)
|
||||
|
||||
birdseye_result = service.generate_image(
|
||||
prompt=build_prompt(
|
||||
view_type="birdseye",
|
||||
project_type=project_type,
|
||||
project_summary=project_summary,
|
||||
details=details,
|
||||
),
|
||||
output_path=str(output_dir / f"{project_id}_birdseye.png"),
|
||||
reference_image_path=reference_image_path,
|
||||
aspect_ratio="16:9",
|
||||
image_size=resolution,
|
||||
)
|
||||
perspective_result = service.generate_image(
|
||||
prompt=build_prompt(
|
||||
view_type="perspective",
|
||||
project_type=project_type,
|
||||
project_summary=project_summary,
|
||||
details=details,
|
||||
),
|
||||
output_path=str(output_dir / f"{project_id}_perspective.png"),
|
||||
reference_image_path=reference_image_path,
|
||||
aspect_ratio="16:9",
|
||||
image_size=resolution,
|
||||
)
|
||||
|
||||
if birdseye_result["status"] == "success" and perspective_result["status"] == "success":
|
||||
status = "success"
|
||||
elif birdseye_result["status"] == "success" or perspective_result["status"] == "success":
|
||||
status = "partial"
|
||||
else:
|
||||
status = "error"
|
||||
|
||||
return wrap_response(
|
||||
{
|
||||
"status": status,
|
||||
"project_id": project_id,
|
||||
"model": service.model,
|
||||
"resolution": resolution,
|
||||
"reference_image_path": reference_image_path,
|
||||
"birdseye_view": birdseye_result,
|
||||
"perspective_view": perspective_result,
|
||||
},
|
||||
domain,
|
||||
)
|
||||
Reference in New Issue
Block a user