Spaces:
Running
Running
| import gradio as gr | |
| from data_layer import WardIndex, resolve | |
| from model_client import classify_image, draft_complaint | |
| from theme import SariMadiTheme, CUSTOM_CSS | |
| ward_index = WardIndex() | |
| GET_LOCATION_JS = """ | |
| () => { | |
| const box = document.getElementById("loc-status-box"); | |
| const setMsg = (cls, text) => { | |
| if (box) { box.className = "loc-status " + cls; box.textContent = text; } | |
| }; | |
| setMsg("loc-pending", "Locating you..."); | |
| return new Promise((resolve) => { | |
| if (!navigator.geolocation) { | |
| setMsg("loc-err", "Geolocation not supported. Enter coordinates below."); | |
| resolve([null, null]); | |
| return; | |
| } | |
| navigator.geolocation.getCurrentPosition( | |
| (pos) => { | |
| const lat = Math.round(pos.coords.latitude * 1e6) / 1e6; | |
| const lng = Math.round(pos.coords.longitude * 1e6) / 1e6; | |
| setMsg("loc-ok", "Location set \u00b7 " + lat.toFixed(4) + ", " + lng.toFixed(4)); | |
| resolve([lat, lng]); | |
| }, | |
| (err) => { | |
| setMsg("loc-err", "Location blocked. Enter coordinates below."); | |
| resolve([null, null]); | |
| }, | |
| { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 } | |
| ); | |
| }); | |
| } | |
| """ | |
| SEVERITY_COLORS = {"low": "#16a34a", "medium": "#ea580c", "high": "#dc2626"} | |
| def build_result(image, lat, lng, language): | |
| if image is None: | |
| return _error("Please add a photo of the issue first.") | |
| if lat is None or lng is None: | |
| return _error("Please share your location or enter coordinates.") | |
| try: | |
| lat = float(lat) | |
| lng = float(lng) | |
| except (TypeError, ValueError): | |
| return _error("Those coordinates don't look right.") | |
| vision = classify_image(image) | |
| civic = vision.get("is_civic_issue", False) | |
| category = vision.get("category", "other") | |
| if not civic or category == "other": | |
| return _error( | |
| "This doesn't look like a clear civic issue BBMP handles. Point the " | |
| "camera directly at a pothole, damaged or blocked road, broken footpath, " | |
| "dead streetlight, garbage, an open or overflowing drain, waterlogging, " | |
| "or a stray or dead animal." | |
| ) | |
| info = resolve(ward_index, lat, lng, vision["category"]) | |
| if info is None: | |
| return _error( | |
| "This location is outside Bengaluru's BBMP wards. Sari Madi can only " | |
| "route complaints within Bengaluru city limits. Please use a location " | |
| "inside Bengaluru." | |
| ) | |
| officer = info["officer"] | |
| if officer is None: | |
| return _error("No officer contact found for this ward.") | |
| context = { | |
| "category_label": info["category_label"], | |
| "severity": vision["severity"], | |
| "description": vision["description"], | |
| "ward_name": info["ward_name"], | |
| "ward_no": info["ward_no"], | |
| "zone": info["zone"], | |
| "officer_role": officer["role"], | |
| "officer_name": officer["name"], | |
| } | |
| complaint = draft_complaint(context, language=language) | |
| sev = vision["severity"] | |
| sev_color = SEVERITY_COLORS.get(sev, "#ea580c") | |
| seen = vision.get("visible_text", "") | |
| seen_html = ( | |
| f'<p class="seen-text">Spotted in photo: "{seen}"</p>' if seen else "" | |
| ) | |
| card = f""" | |
| <div class="officer-card"> | |
| <div class="oc-head"> | |
| <span class="oc-issue">{info['category_label']}</span> | |
| <span class="oc-sev" style="background:{sev_color};">{sev}</span> | |
| </div> | |
| <p class="oc-desc">{vision['description']}</p> | |
| {seen_html} | |
| <div class="oc-divider"></div> | |
| <div class="oc-grid"> | |
| <div><span class="oc-label">Ward</span><span class="oc-value">{info['ward_name']} (No. {info['ward_no']})</span></div> | |
| <div><span class="oc-label">Zone</span><span class="oc-value">{info['zone']}</span></div> | |
| <div><span class="oc-label">Officer</span><span class="oc-value">{officer['name']} ({officer['role']})</span></div> | |
| <div><span class="oc-label">Contact</span><span class="oc-value"><a href="tel:{officer['phone']}" class="oc-phone">{officer['phone']}</a></span></div> | |
| </div> | |
| </div> | |
| """ | |
| return (gr.update(value=card, visible=True), gr.update(visible=False, value=""), | |
| gr.update(value=complaint, visible=True), gr.update(visible=False)) | |
| def _error(msg): | |
| html = f'<div class="error-banner">{msg}</div>' | |
| return (gr.update(visible=False), gr.update(value=html, visible=True), | |
| gr.update(visible=False), gr.update(visible=False)) | |
| with gr.Blocks(theme=SariMadiTheme(), css=CUSTOM_CSS, title="Sari Madi") as demo: | |
| gr.HTML( | |
| '<div id="sari-header">' | |
| '<p class="kicker">Bengaluru civic reporting</p>' | |
| '<h1>Sari Madi</h1>' | |
| '<p>Snap a civic problem. Get it routed to the right BBMP officer ' | |
| 'with a ready-to-file complaint in English or Kannada.</p></div>' | |
| ) | |
| with gr.Group(elem_classes="step-card"): | |
| gr.HTML('<div class="step-head"><span class="step-num">1</span>Show the problem</div>') | |
| image_in = gr.Image(type="pil", sources=["upload", "webcam"], | |
| height=300, label=None, show_label=False) | |
| with gr.Group(elem_classes="step-card"): | |
| gr.HTML('<div class="step-head"><span class="step-num">2</span>Mark the spot</div>') | |
| loc_btn = gr.Button("Use my current location", | |
| elem_classes="locate-btn", variant="secondary") | |
| loc_html = gr.HTML('<div id="loc-status-box" class="loc-status"></div>') | |
| with gr.Accordion("Enter coordinates manually", open=False): | |
| with gr.Row(): | |
| lat_in = gr.Number(label="Latitude", precision=6) | |
| lng_in = gr.Number(label="Longitude", precision=6) | |
| with gr.Group(elem_classes="step-card"): | |
| gr.HTML('<div class="step-head"><span class="step-num">3</span>Choose language</div>') | |
| language_in = gr.Radio(["English", "Kannada"], value="English", | |
| show_label=False, elem_classes="lang-radio") | |
| generate_btn = gr.Button("Generate complaint", variant="primary", | |
| size="lg", elem_classes="generate-btn") | |
| error_out = gr.HTML(visible=False) | |
| result_empty = gr.HTML( | |
| '<div class="result-empty">' | |
| '<div class="re-icon">📍</div>' | |
| '<p class="re-title">Your complaint will appear here</p>' | |
| '<p class="re-sub">Add a photo, mark the spot, and tap generate.</p>' | |
| '</div>', | |
| visible=True, | |
| ) | |
| result_card = gr.HTML(visible=False) | |
| complaint_out = gr.Textbox(label="Your complaint, ready to file", lines=9, | |
| show_copy_button=True, visible=False) | |
| loc_btn.click(fn=None, inputs=None, outputs=[lat_in, lng_in], | |
| js=GET_LOCATION_JS) | |
| generate_btn.click( | |
| fn=build_result, | |
| inputs=[image_in, lat_in, lng_in, language_in], | |
| outputs=[result_card, error_out, complaint_out, result_empty], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |