File size: 7,422 Bytes
ac28b6c
 
 
 
6822058
 
 
ac28b6c
6822058
ac28b6c
 
 
 
 
 
6822058
 
 
 
 
 
 
 
 
 
 
1faa8b6
79540f7
 
 
6822058
 
79540f7
 
6822058
 
 
 
 
 
1faa8b6
79540f7
 
 
 
 
 
 
 
 
6822058
1faa8b6
 
6822058
 
 
108875a
 
6822058
ac28b6c
 
 
 
 
 
 
 
 
 
 
 
 
 
6822058
ac28b6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6822058
ac28b6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3b8d1d
6dffb88
4dbd17e
6822058
 
 
 
 
9fa1ad1
2c9a7b3
c3b8d1d
6822058
9fa1ad1
6822058
9fa1ad1
d346d55
 
6dffb88
d346d55
6dffb88
9fa1ad1
 
 
 
c3b8d1d
 
9fa1ad1
c3b8d1d
9fa1ad1
 
 
c3b8d1d
 
4dbd17e
6822058
 
 
 
 
 
 
 
 
5f83972
 
d346d55
c3b8d1d
9fa1ad1
5f83972
11faaa7
b9974f1
6dffb88
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import gradio as gr
from google import genai
import os
import re
from PIL import Image, ImageDraw
import requests
from io import BytesIO

# --- 1. Custom CSS ---
custom_css = """
.improving { background-color: #dcf2dc; color: #155724; padding: 12px; border-radius: 8px; font-weight: bold; font-size: 1.2em; text-align: center; border: 1px solid #c3e6cb;}
.declining { background-color: #f8d7da; color: #721c24; padding: 12px; border-radius: 8px; font-weight: bold; font-size: 1.2em; text-align: center; border: 1px solid #f5c6cb;}
.stable { background-color: #fff3cd; color: #856404; padding: 12px; border-radius: 8px; font-weight: bold; font-size: 1.2em; text-align: center; border: 1px solid #ffeeba;}
"""

# --- 2. Google Maps API Fetcher ---
def fetch_streetview(address):
    """Geocodes an address and fetches the most recent Street View image."""
    gmaps_api_key = os.environ.get("GMAPS_API_KEY")
    if not gmaps_api_key:
        raise gr.Error("ERROR: GMAPS_API_KEY not found in Secrets.")
    if not address:
        raise gr.Error("Please enter a valid address.")
        
    gr.Info("Geocoding address...")
    
    # 1. Geocoding API
    geo_url = "https://maps.googleapis.com/maps/api/geocode/json"
    geo_params = {"address": address, "key": gmaps_api_key}
    geo_data = requests.get(geo_url, params=geo_params).json()
    
    if geo_data['status'] != 'OK':
        error_detail = geo_data.get('error_message', geo_data['status'])
        raise gr.Error(f"Maps API Error: {error_detail}")
        
    lat = geo_data['results'][0]['geometry']['location']['lat']
    lng = geo_data['results'][0]['geometry']['location']['lng']
    
    gr.Info("Fetching Street View imagery...")
    
    # 2. Street View Static API
    sv_url = "https://maps.googleapis.com/maps/api/streetview"
    sv_params = {
        "size": "640x640",
        "location": f"{lat},{lng}",
        "fov": "80",
        "pitch": "0",
        "key": gmaps_api_key
    }
    sv_res = requests.get(sv_url, params=sv_params)
    
    # --- ALIGNED IF/ELSE BLOCK ---
    if sv_res.status_code == 200:
        gr.Info("Image successfully loaded!")
        return Image.open(BytesIO(sv_res.content))
    else:
        error_msg = sv_res.text if sv_res.text else "No reason provided."
        raise gr.Error(f"Maps 403 Error: {error_msg}")
# --- 3. Spatial Drawing Engine ---
def draw_forensic_targets(image, report_text):
    annotated_img = image.copy()
    draw = ImageDraw.Draw(annotated_img)
    width, height = annotated_img.size
    
    coords = re.findall(r'\[(\d+),\s*(\d+)\]', report_text)
    for y_str, x_str in coords:
        y = int((int(y_str) / 1000) * height)
        x = int((int(x_str) / 1000) * width)
        r = 15 
        draw.ellipse((x - r, y - r, x + r, y + r), outline="red", width=5)
        
    return annotated_img

# --- 4. Robotics-ER Core Engine ---
def ground_truth_engine(past_img, present_img, audit_level, progress=gr.Progress()):
    progress(0, desc="Waking Spatial Sentinel...")
    
    api_key = os.environ.get("GOOGLE_API_KEY")
    if not api_key:
        return None, "<div class='declining'>ERROR: API Key Missing</div>", "Set GOOGLE_API_KEY in Secrets."

    client = genai.Client(api_key=api_key)
    if past_img is None or present_img is None:
        return None, "<div class='stable'>Missing Images</div>", "Please upload both images."

    audit_directives = {
        "Standard Audit": "Perform a structural comparison of roofing, siding, and landscaping.",
        "Deep Forensic": """Perform an exhaustive spatial audit. 
        Identify subtle wear (shingles, foundation, paint) and biological encroachment. 
        MANDATORY: Return center points for top 3 changes as [y, x] normalized coordinates (0-1000)."""
    }
    
    directive = audit_directives.get(audit_level)
    progress(0.2, desc=f"Running {audit_level}...")

    prompt = f"""
    SYSTEM INSTRUCTION: You are a structural forensic agent.
    DIRECTIVE: {directive}
    
    TASK: Compare 'Past Condition' vs 'Current Condition'. 
    Return a structured report. YOU MUST CONCLUDE with exactly one of these phrases: 
    'Maintenance Trajectory: IMPROVING', 'Maintenance Trajectory: STABLE', or 'Maintenance Trajectory: DECLINING'.
    """

    try:
        progress(0.5, desc="Robotics-ER is scanning for spatial anomalies...")
        response = client.models.generate_content(
            model="gemini-robotics-er-1.5-preview",
            contents=[prompt, past_img, present_img]
        )
        text_out = response.text
        
        badge_html = "<div class='stable'>โž– Trajectory: STABLE</div>" 
        if "IMPROVING" in text_out.upper():
            badge_html = "<div class='improving'>๐Ÿ“ˆ Trajectory: IMPROVING</div>"
        elif "DECLINING" in text_out.upper():
            badge_html = "<div class='declining'>๐Ÿ“‰ Trajectory: DECLINING</div>"
            
        progress(0.8, desc="Annotating image...")
        final_present_img = present_img
        if "Deep Forensic" in audit_level:
            final_present_img = draw_forensic_targets(present_img, text_out)
            
        progress(1.0, desc="Audit Finalized.")
        return (past_img, final_present_img), badge_html, text_out
            
    except Exception as e:
        return None, "<div class='declining'>Analysis Failed</div>", str(e)


# --- UI LAYOUT ---
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
    gr.Markdown("# ๐Ÿ  GroundTruth AI: Spatial Property Sentinel")
    
    # --- NEW: Address Search Bar ---
    with gr.Row():
        address_input = gr.Textbox(label="Property Address", placeholder="e.g., 1600 Pennsylvania Avenue NW, Washington, DC", scale=4)
        fetch_btn = gr.Button("๐ŸŒ Fetch Current Street View", variant="secondary", scale=1)

    # --- TOP ROW: The "Input Console" ---
    with gr.Row():
        with gr.Column(scale=1):
            p_img = gr.Image(label="Past Condition (Manual Upload)", type="pil")
        with gr.Column(scale=1):
            c_img = gr.Image(label="Current Condition (Auto-Fetched)", type="pil")
        with gr.Column(scale=1):
            audit_mode = gr.Radio(
                choices=["Standard Audit", "Deep Forensic"],
                value="Standard Audit",
                label="Select Audit Precision"
            )
            submit = gr.Button("Analyze Property Trajectory", variant="primary", size="lg")
            
    # --- MIDDLE ROW: The "Hero Output" ---
    with gr.Row():
        with gr.Column(scale=1):
            status_badge = gr.HTML()
            slider_out = gr.ImageSlider(label="Spatial Delta Comparison", type="pil", height=600)
            
    # --- BOTTOM ROW: The "Forensic Data" ---
    with gr.Row():
        with gr.Column(scale=1):
            with gr.Accordion("View Detailed Forensic Data", open=False):
                report_out = gr.Markdown()

    # --- EVENTS ---
    # 1. Wire the Fetch Button to the Maps API and output to the Current Image box
    fetch_btn.click(
        fn=fetch_streetview,
        inputs=[address_input],
        outputs=[c_img]
    )

    # 2. Wire the Analyze Button to the Robotics-ER Engine
    submit.click(
        fn=ground_truth_engine, 
        inputs=[p_img, c_img, audit_mode], 
        outputs=[slider_out, status_badge, report_out],
        show_progress="minimal"
    )

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)