From 800c7b6fa76beefd98e0f8a894bec9460dabd68e Mon Sep 17 00:00:00 2001 From: sinmb79 Date: Sat, 4 Apr 2026 18:38:12 +0900 Subject: [PATCH] plan: add implementation plan for birdseye view v2.0.0 Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-04-04-birdseye-view.md | 1311 +++++++++++++++++ 1 file changed, 1311 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-04-birdseye-view.md diff --git a/docs/superpowers/plans/2026-04-04-birdseye-view.md b/docs/superpowers/plans/2026-04-04-birdseye-view.md new file mode 100644 index 0000000..9e9eb6e --- /dev/null +++ b/docs/superpowers/plans/2026-04-04-birdseye-view.md @@ -0,0 +1,1311 @@ +# Bird's-Eye View Generation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add 3D bird's-eye view and perspective rendering to CivilPlan MCP using Google's Nano Banana Pro (Gemini 3 Pro Image) API, bump version to 2.0.0, and create a polished release with comprehensive documentation. + +**Architecture:** New MCP tool `civilplan_generate_birdseye_view` calls the Gemini API via `google-genai` SDK. A service layer wraps API calls; prompt templates are specialized per project domain (road/building/water/river/landscape). SVG drawings from the existing generator can optionally feed in as reference images via `cairosvg`. + +**Tech Stack:** google-genai, cairosvg, Pillow, FastMCP 2.0+, Python 3.11+ + +--- + +### Task 1: Add GEMINI_API_KEY to Configuration + +**Files:** +- Modify: `civilplan_mcp/config.py` +- Modify: `.env.example` +- Test: `tests/test_config_and_secure_store.py` + +- [ ] **Step 1: Write failing test for GEMINI_API_KEY in settings** + +Add to `tests/test_config_and_secure_store.py`: + +```python +def test_settings_has_gemini_api_key(): + from civilplan_mcp.config import Settings + s = Settings() + assert hasattr(s, "gemini_api_key") + assert s.gemini_api_key == "" +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_config_and_secure_store.py::test_settings_has_gemini_api_key -v` +Expected: FAIL — `Settings` has no `gemini_api_key` attribute + +- [ ] **Step 3: Add gemini_api_key to Settings and get_settings()** + +In `civilplan_mcp/config.py`, add field to `Settings` class: + +```python +class Settings(BaseModel): + # ... existing fields ... + gemini_api_key: str = Field(default_factory=lambda: os.getenv("GEMINI_API_KEY", "")) +``` + +In `get_settings()`, add after the existing vworld block: + +```python + if not settings.gemini_api_key: + settings.gemini_api_key = secure_keys.get("GEMINI_API_KEY", "") +``` + +In `check_api_keys()`, add: + +```python + if not settings.gemini_api_key: + missing.append("GEMINI_API_KEY") +``` + +- [ ] **Step 4: Update .env.example** + +Append to `.env.example`: + +``` +GEMINI_API_KEY= +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_config_and_secure_store.py::test_settings_has_gemini_api_key -v` +Expected: PASS + +- [ ] **Step 6: Run full test suite to check nothing broke** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/ -v` +Expected: All existing tests PASS + +- [ ] **Step 7: Commit** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git add civilplan_mcp/config.py .env.example tests/test_config_and_secure_store.py +git commit -m "feat: add GEMINI_API_KEY to config system" +``` + +--- + +### Task 2: Create Gemini Image Service + +**Files:** +- Create: `civilplan_mcp/services/__init__.py` +- Create: `civilplan_mcp/services/gemini_image.py` +- Test: `tests/test_gemini_image.py` + +- [ ] **Step 1: Write failing tests for GeminiImageService** + +Create `tests/test_gemini_image.py`: + +```python +from __future__ import annotations + +import base64 +from unittest.mock import MagicMock, patch + +import pytest + + +def test_generate_image_returns_dict(): + from civilplan_mcp.services.gemini_image import GeminiImageService + + svc = GeminiImageService(api_key="test-key") + assert svc.api_key == "test-key" + assert svc.model == "gemini-3-pro-image-preview" + + +def test_generate_image_calls_api(tmp_path): + from civilplan_mcp.services.gemini_image import GeminiImageService + + # Create a fake 1x1 white PNG + import io + from PIL import Image as PILImage + + img = PILImage.new("RGB", (1, 1), "white") + buf = io.BytesIO() + img.save(buf, format="PNG") + fake_png_bytes = buf.getvalue() + + mock_image = MagicMock() + mock_image.save = MagicMock() + + mock_part = MagicMock() + mock_part.text = None + mock_part.inline_data = MagicMock() + mock_part.as_image.return_value = mock_image + + mock_response = MagicMock() + mock_response.parts = [mock_part] + + with patch("civilplan_mcp.services.gemini_image.genai") as mock_genai: + mock_client = MagicMock() + mock_genai.Client.return_value = mock_client + mock_client.models.generate_content.return_value = mock_response + + svc = GeminiImageService(api_key="test-key") + result = svc.generate_image( + prompt="test prompt", + output_path=str(tmp_path / "test.png"), + aspect_ratio="16:9", + image_size="2K", + ) + + assert result["status"] == "success" + assert result["path"] == str(tmp_path / "test.png") + mock_image.save.assert_called_once_with(str(tmp_path / "test.png")) + + +def test_generate_image_with_reference(tmp_path): + from civilplan_mcp.services.gemini_image import GeminiImageService + + mock_image = MagicMock() + mock_image.save = MagicMock() + + mock_part = MagicMock() + mock_part.text = None + mock_part.inline_data = MagicMock() + mock_part.as_image.return_value = mock_image + + mock_response = MagicMock() + mock_response.parts = [mock_part] + + with patch("civilplan_mcp.services.gemini_image.genai") as mock_genai: + mock_client = MagicMock() + mock_genai.Client.return_value = mock_client + mock_client.models.generate_content.return_value = mock_response + + svc = GeminiImageService(api_key="test-key") + + # Create a small reference image + from PIL import Image as PILImage + ref_path = tmp_path / "ref.png" + PILImage.new("RGB", (10, 10), "gray").save(str(ref_path)) + + result = svc.generate_image( + prompt="test prompt with reference", + output_path=str(tmp_path / "out.png"), + reference_image_path=str(ref_path), + ) + + assert result["status"] == "success" + # Verify contents list included the reference image + call_args = mock_client.models.generate_content.call_args + contents = call_args.kwargs.get("contents") or call_args[1].get("contents") + assert len(contents) == 2 # prompt + image + + +def test_generate_image_api_failure(tmp_path): + from civilplan_mcp.services.gemini_image import GeminiImageService + + with patch("civilplan_mcp.services.gemini_image.genai") as mock_genai: + mock_client = MagicMock() + mock_genai.Client.return_value = mock_client + mock_client.models.generate_content.side_effect = Exception("API error") + + svc = GeminiImageService(api_key="test-key") + result = svc.generate_image( + prompt="test prompt", + output_path=str(tmp_path / "fail.png"), + ) + + assert result["status"] == "error" + assert "API error" in result["error"] + + +def test_generate_image_no_image_in_response(tmp_path): + from civilplan_mcp.services.gemini_image import GeminiImageService + + mock_part = MagicMock() + mock_part.text = "Sorry, I cannot generate that image." + mock_part.inline_data = None + + mock_response = MagicMock() + mock_response.parts = [mock_part] + + with patch("civilplan_mcp.services.gemini_image.genai") as mock_genai: + mock_client = MagicMock() + mock_genai.Client.return_value = mock_client + mock_client.models.generate_content.return_value = mock_response + + svc = GeminiImageService(api_key="test-key") + result = svc.generate_image( + prompt="test prompt", + output_path=str(tmp_path / "noimg.png"), + ) + + assert result["status"] == "error" + assert "no image" in result["error"].lower() +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_gemini_image.py -v` +Expected: FAIL — module `civilplan_mcp.services.gemini_image` not found + +- [ ] **Step 3: Create services package and gemini_image.py** + +Create `civilplan_mcp/services/__init__.py`: + +```python +``` + +Create `civilplan_mcp/services/gemini_image.py`: + +```python +from __future__ import annotations + +import logging +from pathlib import Path + +from google import genai +from google.genai import types +from PIL import Image + +logger = logging.getLogger(__name__) + + +class GeminiImageService: + def __init__(self, api_key: str, model: str = "gemini-3-pro-image-preview"): + self.api_key = api_key + self.model = model + self._client = genai.Client(api_key=api_key) + + def generate_image( + self, + *, + prompt: str, + output_path: str, + reference_image_path: str | None = None, + aspect_ratio: str = "16:9", + image_size: str = "2K", + ) -> dict: + try: + contents: list = [prompt] + if reference_image_path: + ref_img = Image.open(reference_image_path) + contents.append(ref_img) + + config = types.GenerateContentConfig( + response_modalities=["TEXT", "IMAGE"], + image_config=types.ImageConfig( + aspect_ratio=aspect_ratio, + image_size=image_size, + ), + ) + + response = self._client.models.generate_content( + model=self.model, + contents=contents, + config=config, + ) + + for part in response.parts: + if part.inline_data is not None: + image = part.as_image() + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + image.save(output_path) + return {"status": "success", "path": output_path} + + return {"status": "error", "error": "No image in API response"} + + except Exception as exc: + logger.error("Gemini image generation failed: %s", exc) + return {"status": "error", "error": str(exc)} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_gemini_image.py -v` +Expected: All 5 tests PASS + +- [ ] **Step 5: Commit** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git add civilplan_mcp/services/__init__.py civilplan_mcp/services/gemini_image.py tests/test_gemini_image.py +git commit -m "feat: add Gemini image service for Nano Banana Pro API" +``` + +--- + +### Task 3: Create Prompt Templates + +**Files:** +- Create: `civilplan_mcp/prompts/__init__.py` +- Create: `civilplan_mcp/prompts/birdseye_templates.py` +- Test: `tests/test_birdseye_templates.py` + +- [ ] **Step 1: Write failing tests for prompt templates** + +Create `tests/test_birdseye_templates.py`: + +```python +from __future__ import annotations + +import pytest + + +def test_build_birdseye_prompt_road(): + from civilplan_mcp.prompts.birdseye_templates import build_prompt + + prompt = build_prompt( + view_type="birdseye", + project_type="road", + project_summary="경기도 지방도 890m, 폭 6m, 2차선 아스팔트 도로", + details={"length_m": 890, "width_m": 6, "lanes": 2, "pavement": "아스콘"}, + ) + assert "bird's-eye view" in prompt.lower() or "aerial" in prompt.lower() + assert "890" in prompt + assert "road" in prompt.lower() or "도로" in prompt + + +def test_build_perspective_prompt_building(): + from civilplan_mcp.prompts.birdseye_templates import build_prompt + + prompt = build_prompt( + view_type="perspective", + project_type="building", + project_summary="서울시 강남구 5층 오피스 빌딩", + details={"floors": 5, "use": "오피스"}, + ) + assert "perspective" in prompt.lower() + assert "building" in prompt.lower() or "빌딩" in prompt + + +def test_all_project_types_have_templates(): + from civilplan_mcp.prompts.birdseye_templates import DOMAIN_PROMPTS + + expected = {"road", "building", "water", "river", "landscape", "mixed"} + assert set(DOMAIN_PROMPTS.keys()) == expected + + +def test_build_prompt_unknown_type_uses_mixed(): + from civilplan_mcp.prompts.birdseye_templates import build_prompt + + prompt = build_prompt( + view_type="birdseye", + project_type="unknown_type", + project_summary="Test project", + details={}, + ) + assert isinstance(prompt, str) + assert len(prompt) > 50 +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_birdseye_templates.py -v` +Expected: FAIL — module not found + +- [ ] **Step 3: Create prompts package and templates** + +Create `civilplan_mcp/prompts/__init__.py`: + +```python +``` + +Create `civilplan_mcp/prompts/birdseye_templates.py`: + +```python +from __future__ import annotations + +from typing import Any + +DOMAIN_PROMPTS: dict[str, str] = { + "road": ( + "Focus on the road alignment cutting through the terrain. " + "Show road surface markings, lane dividers, shoulders, drainage ditches along both sides, " + "utility corridors (water/sewer pipes if applicable), guard rails, and traffic signage. " + "Include surrounding land use context: rice paddies, hills, or residential areas typical of Korean rural/suburban settings." + ), + "building": ( + "Focus on the building mass, facade materials, and rooftop details. " + "Show the site context including parking areas, landscaping, pedestrian paths, and adjacent roads. " + "Include typical Korean urban context: neighboring buildings, street trees, and utility poles." + ), + "water": ( + "Focus on the pipeline route and treatment facility layout. " + "Show pipe trenches, manholes at regular intervals, pump stations, and connection points. " + "Include cross-section hints showing pipe burial depth. " + "Surrounding context: Korean suburban or rural terrain with roads running alongside." + ), + "river": ( + "Focus on the riverbank improvements, embankment structures, and flood control elements. " + "Show gabion walls, riprap, walking paths along the levee, bridge crossings, and flood gates. " + "Include the water surface with natural flow patterns and riparian vegetation." + ), + "landscape": ( + "Focus on the green space design, vegetation patterns, and hardscape elements. " + "Show walking trails, rest areas with benches, playground equipment, water features, " + "and ornamental planting beds. Include seasonal Korean trees (cherry blossom, pine, maple)." + ), + "mixed": ( + "Show a comprehensive development site with multiple infrastructure elements. " + "Include roads, buildings, utility corridors, and landscaped areas working together. " + "Emphasize how different systems connect and interact within the Korean development context." + ), +} + +VIEW_INSTRUCTIONS: dict[str, str] = { + "birdseye": ( + "Create a photorealistic aerial bird's-eye view rendering, " + "camera angle approximately 45-60 degrees from above, " + "showing the full project extent with surrounding context." + ), + "perspective": ( + "Create a photorealistic eye-level perspective rendering, " + "camera positioned at human eye height (1.6m), " + "showing the project from the most representative viewpoint." + ), +} + + +def build_prompt( + *, + view_type: str, + project_type: str, + project_summary: str, + details: dict[str, Any], +) -> str: + view_instruction = VIEW_INSTRUCTIONS.get(view_type, VIEW_INSTRUCTIONS["birdseye"]) + domain_context = DOMAIN_PROMPTS.get(project_type, DOMAIN_PROMPTS["mixed"]) + + detail_lines = [] + for key, value in details.items(): + if value is not None: + detail_lines.append(f"- {key}: {value}") + detail_block = "\n".join(detail_lines) if detail_lines else "No additional details." + + return ( + f"{view_instruction}\n\n" + f"Project description: {project_summary}\n\n" + f"Technical details:\n{detail_block}\n\n" + f"Domain-specific guidance:\n{domain_context}\n\n" + f"Style: Professional architectural visualization for a Korean construction project plan. " + f"Clear daytime weather, natural lighting, high detail. " + f"Include a subtle north arrow and scale reference in the corner." + ) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_birdseye_templates.py -v` +Expected: All 4 tests PASS + +- [ ] **Step 5: Commit** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git add civilplan_mcp/prompts/__init__.py civilplan_mcp/prompts/birdseye_templates.py tests/test_birdseye_templates.py +git commit -m "feat: add project-type-specific prompt templates for birdseye rendering" +``` + +--- + +### Task 4: Create Bird's-Eye View Generator Tool + +**Files:** +- Create: `civilplan_mcp/tools/birdseye_generator.py` +- Test: `tests/test_birdseye_generator.py` + +- [ ] **Step 1: Write failing tests** + +Create `tests/test_birdseye_generator.py`: + +```python +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + + +SAMPLE_PROJECT_SPEC = { + "project_id": "PRJ-20260404-001", + "domain": "토목_도로", + "project_type": ["도로"], + "road": {"class": "소로3류", "length_m": 890, "width_m": 6, "lanes": 2, "pavement": "아스콘"}, + "terrain": "구릉", + "region": "경기도", + "year_start": 2026, + "year_end": 2028, + "utilities": ["상수도", "하수도"], +} + +DOMAIN_MAP = { + "토목_도로": "road", + "건축": "building", + "토목_상하수도": "water", + "토목_하천": "river", + "조경": "landscape", + "복합": "mixed", +} + + +def _mock_generate_image(**kwargs): + return {"status": "success", "path": kwargs.get("output_path", "/tmp/test.png")} + + +@patch("civilplan_mcp.tools.birdseye_generator.get_settings") +@patch("civilplan_mcp.tools.birdseye_generator.GeminiImageService") +def test_generate_birdseye_returns_two_images(mock_svc_cls, mock_settings, tmp_path): + from civilplan_mcp.tools.birdseye_generator import generate_birdseye_view + + mock_settings.return_value = MagicMock( + gemini_api_key="test-key", + output_dir=tmp_path, + ) + mock_svc = MagicMock() + mock_svc.generate_image.side_effect = lambda **kw: { + "status": "success", + "path": kw["output_path"], + } + mock_svc_cls.return_value = mock_svc + + result = generate_birdseye_view( + project_summary="경기도 지방도 890m 도로", + project_spec=SAMPLE_PROJECT_SPEC, + ) + + assert result["status"] == "success" + assert "birdseye_view" in result + assert "perspective_view" in result + assert result["birdseye_view"]["status"] == "success" + assert result["perspective_view"]["status"] == "success" + assert mock_svc.generate_image.call_count == 2 + + +@patch("civilplan_mcp.tools.birdseye_generator.get_settings") +@patch("civilplan_mcp.tools.birdseye_generator.GeminiImageService") +def test_generate_birdseye_with_svg_reference(mock_svc_cls, mock_settings, tmp_path): + from civilplan_mcp.tools.birdseye_generator import generate_birdseye_view + + mock_settings.return_value = MagicMock( + gemini_api_key="test-key", + output_dir=tmp_path, + ) + mock_svc = MagicMock() + mock_svc.generate_image.side_effect = lambda **kw: { + "status": "success", + "path": kw["output_path"], + } + mock_svc_cls.return_value = mock_svc + + svg_content = '' + + with patch("civilplan_mcp.tools.birdseye_generator.svg_to_png") as mock_svg: + mock_svg.return_value = str(tmp_path / "ref.png") + # Create fake ref.png so path exists + (tmp_path / "ref.png").write_bytes(b"\x89PNG\r\n") + + result = generate_birdseye_view( + project_summary="경기도 도로", + project_spec=SAMPLE_PROJECT_SPEC, + svg_drawing=svg_content, + ) + + assert result["status"] == "success" + mock_svg.assert_called_once() + # Both calls should include reference_image_path + for call in mock_svc.generate_image.call_args_list: + assert call.kwargs.get("reference_image_path") is not None + + +@patch("civilplan_mcp.tools.birdseye_generator.get_settings") +def test_generate_birdseye_no_api_key(mock_settings): + from civilplan_mcp.tools.birdseye_generator import generate_birdseye_view + + mock_settings.return_value = MagicMock(gemini_api_key="") + + result = generate_birdseye_view( + project_summary="Test", + project_spec=SAMPLE_PROJECT_SPEC, + ) + + assert result["status"] == "error" + assert "GEMINI_API_KEY" in result["error"] + + +@patch("civilplan_mcp.tools.birdseye_generator.get_settings") +@patch("civilplan_mcp.tools.birdseye_generator.GeminiImageService") +def test_generate_birdseye_partial_failure(mock_svc_cls, mock_settings, tmp_path): + from civilplan_mcp.tools.birdseye_generator import generate_birdseye_view + + mock_settings.return_value = MagicMock( + gemini_api_key="test-key", + output_dir=tmp_path, + ) + call_count = [0] + + def side_effect(**kw): + call_count[0] += 1 + if call_count[0] == 1: + return {"status": "success", "path": kw["output_path"]} + return {"status": "error", "error": "Rate limit exceeded"} + + mock_svc = MagicMock() + mock_svc.generate_image.side_effect = side_effect + mock_svc_cls.return_value = mock_svc + + result = generate_birdseye_view( + project_summary="Test", + project_spec=SAMPLE_PROJECT_SPEC, + ) + + # Should still return partial results + assert result["status"] == "partial" + assert result["birdseye_view"]["status"] == "success" + assert result["perspective_view"]["status"] == "error" + + +def test_domain_to_project_type_mapping(): + from civilplan_mcp.tools.birdseye_generator import _domain_to_project_type + + assert _domain_to_project_type("토목_도로") == "road" + assert _domain_to_project_type("건축") == "building" + assert _domain_to_project_type("토목_상하수도") == "water" + assert _domain_to_project_type("토목_하천") == "river" + assert _domain_to_project_type("조경") == "landscape" + assert _domain_to_project_type("복합") == "mixed" + assert _domain_to_project_type("unknown") == "mixed" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_birdseye_generator.py -v` +Expected: FAIL — module not found + +- [ ] **Step 3: Create birdseye_generator.py** + +Create `civilplan_mcp/tools/birdseye_generator.py`: + +```python +from __future__ import annotations + +import logging +import tempfile +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_MAP: dict[str, str] = { + "토목_도로": "road", + "건축": "building", + "토목_상하수도": "water", + "토목_하천": "river", + "조경": "landscape", + "복합": "mixed", +} + + +def _domain_to_project_type(domain: str) -> str: + return DOMAIN_MAP.get(domain, "mixed") + + +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", + output_dir: str | None = None, +) -> dict[str, Any]: + """Generate bird's-eye view and perspective renderings using Nano Banana Pro.""" + settings = get_settings() + + if not settings.gemini_api_key: + return { + "status": "error", + "error": "GEMINI_API_KEY is not configured. Set it in .env or run setup_keys.py.", + } + + out = Path(output_dir or settings.output_dir) / "renders" + out.mkdir(parents=True, exist_ok=True) + + domain = project_spec.get("domain", "복합") + project_type = _domain_to_project_type(domain) + project_id = project_spec.get("project_id", "unknown") + + # Extract details for prompt + details: dict[str, Any] = {} + road = project_spec.get("road") + if road: + details.update({k: v for k, v in road.items() if v is not None}) + details["terrain"] = project_spec.get("terrain") + details["region"] = project_spec.get("region") + details["utilities"] = project_spec.get("utilities") + + # Convert SVG to PNG reference if provided + ref_path: str | None = None + if svg_drawing: + try: + ref_png = str(out / f"{project_id}_ref.png") + ref_path = svg_to_png(svg_drawing, ref_png) + except Exception as exc: + logger.warning("SVG to PNG conversion failed, proceeding without reference: %s", exc) + ref_path = None + + svc = GeminiImageService(api_key=settings.gemini_api_key) + + results: dict[str, Any] = {} + for view_type in ("birdseye", "perspective"): + prompt = build_prompt( + view_type=view_type, + project_type=project_type, + project_summary=project_summary, + details=details, + ) + file_path = str(out / f"{project_id}_{view_type}.png") + + result = svc.generate_image( + prompt=prompt, + output_path=file_path, + reference_image_path=ref_path, + aspect_ratio="16:9", + image_size=resolution, + ) + results[f"{view_type}_view"] = result + + birdseye_ok = results["birdseye_view"].get("status") == "success" + perspective_ok = results["perspective_view"].get("status") == "success" + + if birdseye_ok and perspective_ok: + status = "success" + elif birdseye_ok or perspective_ok: + status = "partial" + else: + status = "error" + + domain_enum = ProjectDomain(domain) if domain in ProjectDomain.__members__.values() else ProjectDomain.복합 + + return wrap_response( + { + "status": status, + "project_id": project_id, + "model": "nano-banana-pro (gemini-3-pro-image-preview)", + "resolution": resolution, + "birdseye_view": results["birdseye_view"], + "perspective_view": results["perspective_view"], + }, + domain_enum, + ) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_birdseye_generator.py -v` +Expected: All 5 tests PASS + +- [ ] **Step 5: Commit** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git add civilplan_mcp/tools/birdseye_generator.py tests/test_birdseye_generator.py +git commit -m "feat: add birdseye view generator MCP tool" +``` + +--- + +### Task 5: Register Tool in Server and Update Dependencies + +**Files:** +- Modify: `civilplan_mcp/server.py` +- Modify: `civilplan_mcp/__init__.py` +- Modify: `requirements.txt` +- Modify: `pyproject.toml` +- Test: `tests/test_server_registration.py` + +- [ ] **Step 1: Write failing test for new tool registration** + +Add to `tests/test_server_registration.py`: + +```python +def test_birdseye_tool_registered(): + from civilplan_mcp.server import build_mcp + + app = build_mcp() + tool_names = [t.name for t in app._tool_manager.tools.values()] + assert "civilplan_generate_birdseye_view" in tool_names +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_server_registration.py::test_birdseye_tool_registered -v` +Expected: FAIL — tool not registered + +- [ ] **Step 3: Register tool in server.py** + +Add import at top of `civilplan_mcp/server.py`: + +```python +from civilplan_mcp.tools.birdseye_generator import generate_birdseye_view +``` + +Add registration in `build_mcp()` after the last `_register_write_tool` call: + +```python + _register_write_tool(app, "civilplan_generate_birdseye_view", generate_birdseye_view) +``` + +- [ ] **Step 4: Update version to 2.0.0** + +In `civilplan_mcp/__init__.py`: + +```python +__all__ = ["__version__"] + +__version__ = "2.0.0" +``` + +In `pyproject.toml`, change: + +```toml +version = "2.0.0" +``` + +In `civilplan_mcp/config.py`, change: + +```python + version: str = "2.0.0" +``` + +- [ ] **Step 5: Update dependencies** + +In `requirements.txt`, add before `pytest`: + +``` +google-genai>=1.0.0 +cairosvg>=2.7.0 +Pillow>=10.0.0 +``` + +In `pyproject.toml`, add to dependencies list: + +```toml + "google-genai>=1.0.0", + "cairosvg>=2.7.0", + "Pillow>=10.0.0", +``` + +- [ ] **Step 6: Install new dependencies** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && pip install google-genai cairosvg Pillow` + +- [ ] **Step 7: Run test to verify registration passes** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/test_server_registration.py -v` +Expected: PASS (including the new test) + +- [ ] **Step 8: Run full test suite** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/ -v` +Expected: All tests PASS + +- [ ] **Step 9: Commit** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git add civilplan_mcp/server.py civilplan_mcp/__init__.py civilplan_mcp/config.py requirements.txt pyproject.toml tests/test_server_registration.py +git commit -m "feat: register birdseye tool, bump to v2.0.0, add new dependencies" +``` + +--- + +### Task 6: Write Comprehensive README + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: Read current README for structure reference** + +Run: `cat C:/Users/sinmb/workspace/CivilPlan-MCP-v2/README.md` + +- [ ] **Step 2: Rewrite README.md** + +Full rewrite of `README.md` with these sections: + +```markdown +# CivilPlan MCP v2.0 + +> AI-powered construction project planning for Korean civil engineering and building projects. +> Connects to Claude, ChatGPT, and other AI agents via the Model Context Protocol (MCP). + +건설/건축 공사 사업계획을 AI와 함께 만듭니다. +MCP(Model Context Protocol)를 통해 Claude, ChatGPT 등 AI 에이전트와 연결됩니다. + +--- + +## What's New in v2.0 + +| Feature | Description | +|---------|-------------| +| **3D Bird's-Eye View** | Generate photorealistic aerial and perspective renderings using Nano Banana Pro (Gemini 3 Pro Image) | +| **Perspective Rendering** | Eye-level visualizations for project presentations | +| **SVG Reference Input** | Use existing drawings as reference for more accurate renderings | +| **20 MCP Tools** | Now includes rendering alongside planning, estimation, and documentation | + +--- + +## Features + +### Planning & Analysis (11 Tools) + +| Tool | Description | +|------|-------------| +| `civilplan_parse_project` | Parse natural language project descriptions into structured data | +| `civilplan_get_legal_procedures` | Identify required permits, approvals, and legal procedures | +| `civilplan_get_phase_checklist` | Get phase-by-phase project checklists | +| `civilplan_evaluate_impact_assessments` | Evaluate 9 environmental/feasibility assessments | +| `civilplan_estimate_quantities` | Calculate material volumes from standard cross-sections | +| `civilplan_get_unit_prices` | Retrieve regional unit cost data (조달청 기준) | +| `civilplan_get_applicable_guidelines` | Find applicable design guidelines | +| `civilplan_fetch_guideline_summary` | Fetch guideline details | +| `civilplan_select_bid_type` | Recommend bidding methodology | +| `civilplan_estimate_waste_disposal` | Calculate construction waste estimates | +| `civilplan_query_land_info` | Query land/cadastral information (V-World API) | + +### Financial Analysis (2 Tools) + +| Tool | Description | +|------|-------------| +| `civilplan_analyze_feasibility` | IRR, NPV, payback period, DSCR analysis | +| `civilplan_validate_against_benchmark` | Validate costs against government benchmarks | + +### Document Generation (7 Tools) + +| Tool | Description | +|------|-------------| +| `civilplan_generate_boq_excel` | Excel BOQ with cost breakdown (6 sheets) | +| `civilplan_generate_investment_doc` | Word investment plan document | +| `civilplan_generate_budget_report` | Budget report document | +| `civilplan_generate_schedule` | Gantt-style project timeline (Excel) | +| `civilplan_generate_svg_drawing` | SVG conceptual drawings | +| `civilplan_generate_dxf_drawing` | DXF CAD drawings | +| `civilplan_generate_birdseye_view` | **NEW** 3D aerial + perspective renderings | + +### Supported Domains + +| Domain | Korean | Status | +|--------|--------|--------| +| Roads | 토목_도로 | Full support | +| Buildings | 건축 | Full support | +| Water/Sewerage | 토목_상하수도 | Full support | +| Rivers | 토목_하천 | Full support | +| Landscaping | 조경 | Partial (in progress) | +| Mixed | 복합 | Full support | + +--- + +## Quick Start + +### 1. Clone & Install + +```bash +git clone https://github.com/sinmb79/Construction-project-master.git +cd Construction-project-master +python -m venv .venv + +# Windows +.venv\Scripts\activate +# macOS/Linux +source .venv/bin/activate + +pip install -r requirements.txt +``` + +### 2. Configure API Keys + +Copy the example environment file: + +```bash +cp .env.example .env +``` + +Edit `.env` and add your keys: + +```env +# Required for 3D rendering (Nano Banana Pro) +GEMINI_API_KEY=your_gemini_api_key_here + +# Optional - for land information queries +DATA_GO_KR_API_KEY=your_data_go_kr_key +VWORLD_API_KEY=your_vworld_key +``` + +**How to get a Gemini API key:** +1. Go to [Google AI Studio](https://aistudio.google.com/) +2. Click "Get API key" in the left sidebar +3. Create a new API key or use an existing project +4. Copy the key to your `.env` file + +**Alternative: Encrypted local storage (Windows)** + +```bash +python setup_keys.py +``` + +### 3. Start the Server + +```bash +python server.py +``` + +Server runs at: `http://127.0.0.1:8765/mcp` + +--- + +## Connecting to AI Agents + +### Claude Desktop + +Add to your Claude Desktop config file: + +**Windows:** `%APPDATA%\Claude\claude_desktop_config.json` +**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "civilplan": { + "url": "http://127.0.0.1:8765/mcp" + } + } +} +``` + +Restart Claude Desktop after editing. + +### Claude Code (CLI) + +```bash +claude mcp add civilplan http://127.0.0.1:8765/mcp +``` + +### ChatGPT / OpenAI + +ChatGPT supports MCP servers via compatible plugins or custom GPT configurations. +Connect using the server URL: `http://127.0.0.1:8765/mcp` + +### Other MCP Clients + +Any MCP-compatible client can connect using: +- **Transport:** Streamable HTTP +- **URL:** `http://127.0.0.1:8765/mcp` + +--- + +## Example: Full Project Workflow + +Input to AI agent: + +> "경기도 화성시 지방도 890m, 폭 6m, 2차선 아스팔트 도로, 상하수도 포함, 2026~2028년 시행" + +The AI agent can then use CivilPlan tools to: + +1. **Parse** → Structured project data (domain, dimensions, region, utilities) +2. **Legal procedures** → 18 required permits, 18-month approval timeline +3. **Impact assessment** → 9 assessment categories evaluated +4. **Quantity estimation** → Material volumes (토공, 포장, 배수, 상하수도) +5. **Unit pricing** → Regional cost data applied (경기도 factor: 1.05) +6. **BOQ generation** → Excel with 6 sheets, total ~10.67 billion KRW +7. **Investment document** → Word document for approval process +8. **Schedule** → Gantt timeline with phase durations +9. **Drawings** → SVG/DXF conceptual drawings +10. **3D Rendering** → Bird's-eye view + perspective visualization + +--- + +## 3D Rendering (Bird's-Eye View) + +The `civilplan_generate_birdseye_view` tool generates two images per call: + +| Output | Description | +|--------|-------------| +| Bird's-eye view | Aerial 45-60° angle showing full project extent | +| Perspective view | Eye-level view from the most representative viewpoint | + +**Input options:** +- Text-only: Uses project description and technical details +- Text + SVG: Uses existing SVG drawing as spatial reference for more accurate results + +**Resolution:** 2K (default) or 4K + +**Powered by:** Google Nano Banana Pro (Gemini 3 Pro Image) via the `google-genai` SDK + +--- + +## Architecture + +``` +MCP Client (Claude / ChatGPT / any MCP client) + │ + │ MCP Protocol (Streamable HTTP) + ▼ +┌─────────────────────────────────────┐ +│ CivilPlan MCP Server │ +│ (FastMCP 2.0+) │ +│ │ +│ ┌──────────┐ ┌──────────────┐ │ +│ │ 20 Tools │ │ Data Layer │ │ +│ │ (parse, │ │ (JSON, SQL, │ │ +│ │ legal, │ │ CSV) │ │ +│ │ render) │ │ │ │ +│ └────┬─────┘ └──────────────┘ │ +│ │ │ +│ ┌────▼─────────────────────┐ │ +│ │ Services │ │ +│ │ ├── Gemini Image API │ │ +│ │ ├── V-World API │ │ +│ │ └── Public Data Portal │ │ +│ └───────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +--- + +## Data Sources + +| Data | Source | Update Frequency | +|------|--------|-----------------| +| Unit prices | 조달청 표준시장단가 | Bi-annual (Jan, Jul) | +| Wages | 건설업 임금실태조사 | Bi-annual (Jan, Sep) | +| Legal procedures | 법제처 법령정보 | Manual updates | +| Region factors | 통계청 | Annual | +| Land prices | User-supplied CSV | As needed | + +The server includes an automatic update scheduler (APScheduler) that checks for new data releases. + +--- + +## Limitations + +| Item | Detail | +|------|--------| +| Accuracy | ±20-30% (planning stage estimates only) | +| Scope | Conceptual planning — not for detailed engineering design | +| Landscaping | Legal procedure data incomplete | +| Land data | Requires manual CSV download to `data/land_prices/` | +| Rendering | Requires internet + Gemini API key | +| Platform | Encrypted key storage (DPAPI) is Windows-only; use `.env` on other platforms | + +--- + +## Disclaimer + +> **참고용 개략 자료 — 공식 제출 불가** +> +> All outputs are approximate reference materials for planning purposes only. +> They cannot replace professional engineering design review and are not valid for official submission. +> Verify all results with domain experts before use in actual projects. + +--- + +## License + +MIT License. Free to use, modify, and distribute. + +## Author + +22B Labs — Hongik Ingan: reducing inequality in access to expert planning knowledge. +``` + +- [ ] **Step 3: Commit** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git add README.md +git commit -m "docs: rewrite README for v2.0.0 with rendering guide and connection instructions" +``` + +--- + +### Task 7: Final Validation and Release + +**Files:** +- No new files + +- [ ] **Step 1: Run full test suite** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && python -m pytest tests/ -v` +Expected: All tests PASS + +- [ ] **Step 2: Verify server starts without errors** + +Run: `cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 && timeout 5 python server.py 2>&1 || true` +Expected: Server starts, shows warnings for missing API keys, no import errors + +- [ ] **Step 3: Tag release** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git tag -a v2.0.0 -m "v2.0.0: Add 3D bird's-eye view rendering with Nano Banana Pro" +``` + +- [ ] **Step 4: Push to remote with tags** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +git push origin main --tags +``` + +- [ ] **Step 5: Create GitHub release** + +```bash +cd C:/Users/sinmb/workspace/CivilPlan-MCP-v2 +gh release create v2.0.0 --title "CivilPlan MCP v2.0.0 — 3D Bird's-Eye View Rendering" --notes "$(cat <<'NOTES' +## What's New + +### 3D Bird's-Eye View Rendering +- New `civilplan_generate_birdseye_view` MCP tool +- Powered by Google Nano Banana Pro (Gemini 3 Pro Image) +- Generates aerial (bird's-eye) + eye-level (perspective) renderings +- Supports text-only or text + SVG reference image input +- 2K / 4K resolution options +- Domain-specific prompt templates for road, building, water, river, landscape, and mixed projects + +### Infrastructure +- New Gemini API integration via `google-genai` SDK +- Added `GEMINI_API_KEY` to configuration system +- Now 20 MCP tools total + +### Documentation +- Complete README rewrite with: + - Tool reference tables + - Claude Desktop / Claude Code / ChatGPT connection guides + - API key setup instructions + - Full workflow example + - Architecture diagram + +## Installation + +```bash +git clone https://github.com/sinmb79/Construction-project-master.git +cd Construction-project-master +python -m venv .venv && .venv\Scripts\activate +pip install -r requirements.txt +cp .env.example .env +# Add your GEMINI_API_KEY to .env +python server.py +``` + +## Breaking Changes +None. All existing 19 tools remain unchanged. +NOTES +)" +``` + +Expected: Release created on GitHub