| import gradio as gr |
| import requests |
| import base64 |
| from PIL import Image |
| from io import BytesIO |
| import tempfile |
| import os |
| |
| API_BASE = "http://134.199.195.79:8001" |
| |
| |
| def b64_to_pil(b64_str): |
| return Image.open(BytesIO(base64.b64decode(b64_str))) |
| |
| |
| def analyze_single(image): |
| if image is None: |
| return None, "⚠ Upload a satellite tile first." |
| buf = BytesIO() |
| image.save(buf, format="PNG") |
| buf.seek(0) |
| try: |
| r = requests.post( |
| f"{API_BASE}/analyze", |
| files={"file": ("tile.png", buf, "image/png")}, |
| timeout=300, |
| ) |
| r.raise_for_status() |
| data = r.json() |
| annotated = b64_to_pil(data["annotated_image"]) |
| built = data.get("built_fraction") |
| built_str = f"{built:.1%}" if built else "—" |
| boxes = data.get("boxes_found", 0) |
| header = f"**Built area:** {built_str} | **Urban clusters detected:** {boxes}\n\n---\n\n" |
| return annotated, header + data["analysis"] |
| except Exception as e: |
| return None, f"❌ Error: {e}" |
| |
| |
| def analyze_corridor(images): |
| if not images: |
| return [], "⚠ Upload at least 2 tiles.", None |
| files = [] |
| for i, img in enumerate(images): |
| buf = BytesIO() |
| if isinstance(img, tuple): |
| img = img[0] |
| if isinstance(img, str): |
| img = Image.open(img) |
| img.save(buf, format="PNG") |
| files.append(("files", (f"tile_{i+1}.png", buf.getvalue(), "image/png"))) |
| try: |
| r = requests.post( |
| f"{API_BASE}/corridor-report", |
| files=files, |
| timeout=900, |
| ) |
| r.raise_for_status() |
| data = r.json() |
| annotated_pils = [b64_to_pil(b) for b in data["annotated_images"]] |
| md = f"## 🛰 Corridor Summary\n\n{data['corridor_summary']}\n\n---\n\n" |
| for t in data["tiles"]: |
| frac = t["built_fraction"] |
| frac_str = f"{frac:.1%}" if frac else "—" |
| md += f"### Tile {t['tile']} · Built: {frac_str}\n{t['analysis']}\n\n" |
| gallery_output = [(img, f"Tile {i+1}") for i, img in enumerate(annotated_pils)] |
| return gallery_output, md, data.get("pdf_path") |
| except Exception as e: |
| return [], f"❌ Error: {e}", None |
| |
| |
| def download_pdf(pdf_path): |
| if not pdf_path: |
| return None |
| try: |
| r = requests.get( |
| f"{API_BASE}/download-report", |
| params={"path": pdf_path}, |
| timeout=60, |
| ) |
| r.raise_for_status() |
| tmp = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) |
| tmp.write(r.content) |
| tmp.flush() |
| return tmp.name |
| except Exception as e: |
| return None |
| |
| |
| CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=JetBrains+Mono:wght@400;500&display=swap'); |
| :root { |
| --bg: #060a06; |
| --surface: #0d150d; |
| --border: #1a2e1a; |
| --accent: #00e676; |
| --accent2: #69ff47; |
| --text: #d4ecd4; |
| --muted: #5a7a5a; |
| --danger: #ff5252; |
| } |
| body, .gradio-container { |
| background: var(--bg) !important; |
| font-family: 'Syne', sans-serif !important; |
| color: var(--text) !important; |
| } |
| .app-header { |
| border-bottom: 1px solid var(--border); |
| padding: 2rem 0 1.5rem; |
| margin-bottom: 1.5rem; |
| } |
| .app-title { |
| font-size: 2.2rem; |
| font-weight: 800; |
| color: var(--accent); |
| letter-spacing: -0.03em; |
| line-height: 1; |
| margin: 0; |
| } |
| .app-sub { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 0.72rem; |
| color: var(--muted); |
| margin-top: 0.4rem; |
| letter-spacing: 0.08em; |
| } |
| .tab-nav { |
| background: var(--surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 6px !important; |
| gap: 2px !important; |
| padding: 3px !important; |
| } |
| .tab-nav button { |
| font-family: 'JetBrains Mono', monospace !important; |
| font-size: 0.78rem !important; |
| color: var(--muted) !important; |
| background: transparent !important; |
| border-radius: 4px !important; |
| padding: 6px 18px !important; |
| letter-spacing: 0.05em; |
| transition: all 0.15s; |
| } |
| .tab-nav button.selected { |
| background: var(--accent) !important; |
| color: #000 !important; |
| font-weight: 700 !important; |
| } |
| button.primary { |
| background: var(--accent) !important; |
| color: #000 !important; |
| font-family: 'JetBrains Mono', monospace !important; |
| font-weight: 700 !important; |
| font-size: 0.82rem !important; |
| letter-spacing: 0.06em !important; |
| border: none !important; |
| border-radius: 4px !important; |
| padding: 10px 24px !important; |
| transition: opacity 0.15s !important; |
| } |
| button.primary:hover { opacity: 0.85 !important; } |
| button.secondary { |
| background: transparent !important; |
| color: var(--accent) !important; |
| font-family: 'JetBrains Mono', monospace !important; |
| font-size: 0.78rem !important; |
| border: 1px solid var(--accent) !important; |
| border-radius: 4px !important; |
| padding: 8px 20px !important; |
| transition: all 0.15s !important; |
| } |
| button.secondary:hover { |
| background: var(--accent) !important; |
| color: #000 !important; |
| } |
| .panel { |
| background: var(--surface) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 8px !important; |
| } |
| label span { |
| font-family: 'JetBrains Mono', monospace !important; |
| font-size: 0.72rem !important; |
| color: var(--muted) !important; |
| letter-spacing: 0.08em !important; |
| text-transform: uppercase !important; |
| } |
| textarea, .prose { |
| font-family: 'JetBrains Mono', monospace !important; |
| font-size: 0.82rem !important; |
| background: var(--bg) !important; |
| color: var(--text) !important; |
| border: 1px solid var(--border) !important; |
| border-radius: 4px !important; |
| } |
| .footer-strip { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 0.68rem; |
| color: var(--muted); |
| border-top: 1px solid var(--border); |
| padding-top: 1rem; |
| margin-top: 2rem; |
| letter-spacing: 0.04em; |
| } |
| """ |
| |
| with gr.Blocks(title="Urban Expansion Detector") as demo: |
| |
| pdf_path_state = gr.State(None) |
| |
| gr.HTML(""" |
| <div class="app-header"> |
| <p class="app-title">🛰 URBAN EXPANSION DETECTOR</p> |
| <p class="app-sub"> |
| QWEN2.5-VL 72B · LORA FINE-TUNE · AMD MI300X · 8,000 SENTINEL-2 TILES |
| </p> |
| </div> |
| """) |
| |
| with gr.Tabs(): |
| |
| with gr.Tab("SINGLE TILE"): |
| with gr.Row(equal_height=True): |
| with gr.Column(scale=1, elem_classes="panel"): |
| single_in = gr.Image( |
| label="INPUT — SATELLITE TILE", |
| type="pil", |
| height=340, |
| ) |
| single_btn = gr.Button("ANALYZE →", variant="primary") |
| with gr.Column(scale=1, elem_classes="panel"): |
| single_out_img = gr.Image( |
| label="OUTPUT — ANNOTATED", |
| height=340, |
| interactive=False, |
| ) |
| single_out_text = gr.Textbox( |
| label="MODEL ANALYSIS", |
| lines=7, |
| elem_classes="panel", |
| ) |
| single_btn.click( |
| analyze_single, |
| inputs=[single_in], |
| outputs=[single_out_img, single_out_text], |
| ) |
| |
| with gr.Tab("CORRIDOR ANALYSIS"): |
| gr.HTML(""" |
| <p style="font-family:'JetBrains Mono',monospace;font-size:0.78rem; |
| color:#5a7a5a;margin-bottom:1rem;"> |
| Upload 2–6 tiles along a corridor (e.g. Delhi–Meerut RRTS). |
| Each tile is analyzed independently, then a corridor-level summary is generated. |
| </p> |
| """) |
| corridor_in = gr.Gallery( |
| label="INPUT — CORRIDOR TILES", |
| type="pil", |
| columns=3, |
| height=260, |
| elem_classes="panel", |
| ) |
| corridor_btn = gr.Button("ANALYZE CORRIDOR →", variant="primary") |
| corridor_out_gallery = gr.Gallery( |
| label="OUTPUT — ANNOTATED TILES", |
| columns=3, |
| height=300, |
| elem_classes="panel", |
| ) |
| corridor_out_text = gr.Markdown( |
| label="CORRIDOR SUMMARY", |
| elem_classes="panel", |
| ) |
| with gr.Row(): |
| pdf_btn = gr.Button("📄 EXPORT PDF REPORT", variant="secondary") |
| pdf_out = gr.File(label="DOWNLOAD", elem_classes="panel") |
| |
| corridor_btn.click( |
| analyze_corridor, |
| inputs=[corridor_in], |
| outputs=[corridor_out_gallery, corridor_out_text, pdf_path_state], |
| ) |
| pdf_btn.click( |
| download_pdf, |
| inputs=[pdf_path_state], |
| outputs=[pdf_out], |
| ) |
| |
| gr.HTML(""" |
| <div class="footer-strip"> |
| MODEL · MohitML10/urban-expansion-detector-72b-v3 · |
| HARDWARE · AMD MI300X 192GB HBM3 · |
| DATA · 8,000 SENTINEL-2 TILES (437 INDIA) · |
| USE CASE · URBAN PLANNING · RRTS FEASIBILITY |
| </div> |
| """) |
| |
| demo.launch(css=CSS) |