Spaces:
Sleeping
Sleeping
| """ | |
| Mycelium Map — A whimsical mushroom-foraging companion. | |
| This Gradio app accepts a ZIP code, finds nearby public lands suitable for | |
| foraging using free OpenStreetMap services, and uses the Gemini API to | |
| generate location-specific mushroom predictions. | |
| Architecture: | |
| - Geocoding: Zippopotam.us (free, no API key, no rate limit) | |
| with Nominatim as a fallback | |
| - Public lands: Overpass API (free, no API key) | |
| - Mushroom intelligence: Google Gemini 2.5 Flash (free tier) | |
| - Map rendering: Folium (Leaflet under the hood) with OpenStreetMap tiles | |
| - UI: Gradio with custom earthy CSS | |
| The Gemini API key is read from the GEMINI_API_KEY environment variable, | |
| which on Hugging Face Spaces is set via Settings → Variables and Secrets. | |
| """ | |
| import os | |
| import json | |
| import calendar | |
| from datetime import datetime | |
| import requests | |
| import folium | |
| import gradio as gr | |
| from google import genai | |
| from google.genai import types | |
| # --------------------------------------------------------------------------- | |
| # Configuration | |
| # --------------------------------------------------------------------------- | |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") | |
| ZIPPOPOTAM_URL = "https://api.zippopotam.us" | |
| NOMINATIM_URL = "https://nominatim.openstreetmap.org/search" | |
| OVERPASS_URL = "https://overpass-api.de/api/interpreter" | |
| # Polite User-Agent — required by Nominatim's usage policy and good citizenship | |
| # in general. | |
| USER_AGENT = "MyceliumMap/1.0 (educational mushroom foraging app)" | |
| # Gemini client is created lazily so the app can still load if the key is missing | |
| # (we want to show a friendly message instead of crashing on startup). | |
| _gemini_client = None | |
| def get_gemini_client(): | |
| """Lazy-initialize the Gemini client. Raises if no key is configured.""" | |
| global _gemini_client | |
| if _gemini_client is None: | |
| if not GEMINI_API_KEY: | |
| raise RuntimeError( | |
| "GEMINI_API_KEY is not set. On Hugging Face Spaces, add it " | |
| "under Settings → Variables and Secrets." | |
| ) | |
| _gemini_client = genai.Client(api_key=GEMINI_API_KEY) | |
| return _gemini_client | |
| # --------------------------------------------------------------------------- | |
| # Step 1: Geocode the ZIP code | |
| # --------------------------------------------------------------------------- | |
| # | |
| # We previously used Nominatim, but Hugging Face Spaces share outbound IPs | |
| # across many tenants, so the public Nominatim server constantly returns | |
| # 429 "Too many requests" — the per-IP rate limit is consumed by other | |
| # Spaces, not us. Switching to Zippopotam.us (no key, no rate limit) and | |
| # keeping Nominatim as a fallback for resilience. | |
| # --------------------------------------------------------------------------- | |
| def geocode_zip(zip_code: str, country: str = "us"): | |
| """Convert a ZIP/postal code into (lat, lng, display_name). | |
| Tries Zippopotam.us first (free, no key, no rate limit) and falls back | |
| to Nominatim if that fails. Returns None if both fail. | |
| """ | |
| zip_code = zip_code.strip() | |
| # ----- Primary: Zippopotam.us ----- | |
| try: | |
| url = f"{ZIPPOPOTAM_URL}/{country.lower()}/{zip_code}" | |
| resp = requests.get(url, timeout=10) | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| places = data.get("places", []) | |
| if places: | |
| p = places[0] | |
| lat = float(p["latitude"]) | |
| lng = float(p["longitude"]) | |
| display = ( | |
| f"{p.get('place name', zip_code)}, " | |
| f"{p.get('state abbreviation', '')} {zip_code}" | |
| ).strip(", ") | |
| return lat, lng, display | |
| # 404 means the ZIP isn't in Zippopotam's data — fall through to Nominatim. | |
| except Exception: | |
| # Network error, JSON error, etc. — fall through. | |
| pass | |
| # ----- Fallback: Nominatim ----- | |
| try: | |
| params = { | |
| "postalcode": zip_code, | |
| "country": country, | |
| "format": "json", | |
| "limit": 1, | |
| } | |
| headers = {"User-Agent": USER_AGENT} | |
| resp = requests.get(NOMINATIM_URL, params=params, headers=headers, timeout=15) | |
| if resp.status_code == 200: | |
| results = resp.json() | |
| if results: | |
| hit = results[0] | |
| return ( | |
| float(hit["lat"]), | |
| float(hit["lon"]), | |
| hit.get("display_name", zip_code), | |
| ) | |
| except Exception: | |
| pass | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Step 2: Find public lands via Overpass API | |
| # --------------------------------------------------------------------------- | |
| def find_public_lands(lat: float, lng: float, radius_miles: float): | |
| """Query Overpass for parks, forests, and protected areas near the user. | |
| Returns a list of dicts with name, lat, lng, and OSM tags. We dedupe by | |
| name since Overpass often returns the same protected area as both a node | |
| and a way. | |
| """ | |
| radius_m = int(radius_miles * 1609.34) | |
| # This Overpass query asks for any node, way, or relation with tags that | |
| # suggest public natural land. We cast a wide net and let Gemini help | |
| # interpret the results downstream. | |
| query = f""" | |
| [out:json][timeout:25]; | |
| ( | |
| nwr["leisure"="nature_reserve"](around:{radius_m},{lat},{lng}); | |
| nwr["leisure"="park"]["operator"](around:{radius_m},{lat},{lng}); | |
| nwr["boundary"="national_park"](around:{radius_m},{lat},{lng}); | |
| nwr["boundary"="protected_area"](around:{radius_m},{lat},{lng}); | |
| nwr["landuse"="forest"](around:{radius_m},{lat},{lng}); | |
| nwr["natural"="wood"]["name"](around:{radius_m},{lat},{lng}); | |
| ); | |
| out center tags 50; | |
| """ | |
| response = requests.post(OVERPASS_URL, data={"data": query}, headers={"User-Agent": USER_AGENT}, timeout=40) | |
| response.raise_for_status() | |
| data = response.json() | |
| seen_names = set() | |
| locations = [] | |
| for el in data.get("elements", []): | |
| tags = el.get("tags", {}) | |
| name = tags.get("name") | |
| if not name or name in seen_names: | |
| continue | |
| seen_names.add(name) | |
| # Get coordinates (nodes have lat/lon directly; ways/relations have center). | |
| if "lat" in el and "lon" in el: | |
| loc_lat, loc_lng = el["lat"], el["lon"] | |
| elif "center" in el: | |
| loc_lat, loc_lng = el["center"]["lat"], el["center"]["lon"] | |
| else: | |
| continue | |
| locations.append({ | |
| "name": name, | |
| "lat": loc_lat, | |
| "lng": loc_lng, | |
| "tags": tags, | |
| }) | |
| # Limit to a reasonable number so we don't blow our Gemini quota. | |
| return locations[:8] | |
| # --------------------------------------------------------------------------- | |
| # Step 3: Ask Gemini for mushroom predictions | |
| # --------------------------------------------------------------------------- | |
| MUSHROOM_PROMPT_TEMPLATE = """You are a knowledgeable but cautious mycology guide writing | |
| brief foraging notes for an educational app. The user is near {place_name} and is | |
| exploring public lands for {month} foraging. | |
| For each location below, return a JSON object describing the likely habitat and | |
| 3 to 6 mushroom species that could plausibly fruit there in {month} given the | |
| region (latitude {lat:.2f}, longitude {lng:.2f}, ecoregion inferred from coordinates). | |
| LOCATIONS: | |
| {locations_json} | |
| Return ONLY valid JSON in this exact shape, no prose, no markdown fences: | |
| {{ | |
| "locations": [ | |
| {{ | |
| "name": "<location name>", | |
| "habitat_summary": "<one sentence on dominant trees / habitat type>", | |
| "mushrooms": [ | |
| {{ | |
| "common_name": "<name>", | |
| "scientific_name": "<Genus species>", | |
| "edibility": "choice edible | edible | inedible | toxic | deadly", | |
| "fruiting_window": "<e.g., 'April-May'>", | |
| "lookalike_warning": "<short warning or 'no major lookalikes'>" | |
| }} | |
| ], | |
| "best_visit_advice": "<one sentence>" | |
| }} | |
| ] | |
| }} | |
| Be honest about uncertainty. If a location is unlikely to host foraging mushrooms | |
| (urban park with mowed lawns, etc.), say so in habitat_summary and return an empty | |
| mushrooms array. NEVER recommend eating without expert ID.""" | |
| def get_mushroom_predictions(locations, place_name, lat, lng, month): | |
| """Send locations to Gemini and parse the structured response.""" | |
| if not locations: | |
| return [] | |
| client = get_gemini_client() | |
| # Strip the OSM tags before sending — the names and coords are enough, | |
| # and we don't want to waste tokens on raw OSM metadata. | |
| slim_locations = [ | |
| {"name": loc["name"], "lat": loc["lat"], "lng": loc["lng"]} | |
| for loc in locations | |
| ] | |
| prompt = MUSHROOM_PROMPT_TEMPLATE.format( | |
| place_name=place_name, | |
| month=month, | |
| lat=lat, | |
| lng=lng, | |
| locations_json=json.dumps(slim_locations, indent=2), | |
| ) | |
| response = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=prompt, | |
| config=types.GenerateContentConfig( | |
| temperature=0.7, | |
| response_mime_type="application/json", | |
| ), | |
| ) | |
| try: | |
| parsed = json.loads(response.text) | |
| return parsed.get("locations", []) | |
| except json.JSONDecodeError: | |
| # If Gemini returns malformed JSON, return what we can salvage. | |
| return [] | |
| # --------------------------------------------------------------------------- | |
| # Step 4: Build the map and result cards | |
| # --------------------------------------------------------------------------- | |
| def build_map(center_lat, center_lng, locations, predictions): | |
| """Build a Folium map with mushroom-themed markers.""" | |
| fmap = folium.Map( | |
| location=[center_lat, center_lng], | |
| zoom_start=10, | |
| tiles="OpenStreetMap", | |
| ) | |
| # Center marker (the user's ZIP). | |
| folium.Marker( | |
| [center_lat, center_lng], | |
| popup="You are here 🌲", | |
| icon=folium.Icon(color="green", icon="home"), | |
| ).add_to(fmap) | |
| # Build a quick lookup so we can attach mushroom info to map popups. | |
| pred_by_name = {p["name"]: p for p in predictions} | |
| for loc in locations: | |
| pred = pred_by_name.get(loc["name"], {}) | |
| habitat = pred.get("habitat_summary", "Habitat info unavailable.") | |
| mushrooms = pred.get("mushrooms", []) | |
| species_html = "<br>".join( | |
| f"• <i>{m.get('common_name', '?')}</i>" for m in mushrooms[:5] | |
| ) or "<i>No likely species this month.</i>" | |
| popup_html = f""" | |
| <div style='font-family: serif; max-width: 260px;'> | |
| <b>{loc['name']}</b><br> | |
| <small>{habitat}</small><br><br> | |
| {species_html} | |
| </div> | |
| """ | |
| folium.Marker( | |
| [loc["lat"], loc["lng"]], | |
| popup=folium.Popup(popup_html, max_width=300), | |
| icon=folium.Icon(color="darkgreen", icon="leaf", prefix="fa"), | |
| ).add_to(fmap) | |
| return fmap._repr_html_() | |
| def build_result_cards(predictions): | |
| """Build HTML cards for each location's foraging brief.""" | |
| if not predictions: | |
| return ( | |
| "<div class='no-results'>" | |
| "<p>The forest is quiet here. Try expanding your search radius " | |
| "or checking back in a different season.</p></div>" | |
| ) | |
| cards = [] | |
| edibility_colors = { | |
| "choice edible": "#7a8b3a", | |
| "edible": "#9aab5a", | |
| "inedible": "#8a7a5a", | |
| "toxic": "#a0522d", | |
| "deadly": "#8b2500", | |
| } | |
| for pred in predictions: | |
| mushrooms_html = "" | |
| for m in pred.get("mushrooms", []): | |
| edibility = m.get("edibility", "unknown").lower() | |
| color = edibility_colors.get(edibility, "#8a7a5a") | |
| mushrooms_html += f""" | |
| <div class='mushroom-entry'> | |
| <div class='mushroom-header'> | |
| <span class='mushroom-name'>{m.get('common_name', '?')}</span> | |
| <span class='mushroom-sci'><i>{m.get('scientific_name', '')}</i></span> | |
| </div> | |
| <div class='mushroom-meta'> | |
| <span class='edibility-pill' style='background:{color}'>{edibility}</span> | |
| <span class='fruiting-window'>{m.get('fruiting_window', '')}</span> | |
| </div> | |
| <div class='mushroom-warning'>⚠ {m.get('lookalike_warning', '')}</div> | |
| </div> | |
| """ | |
| if not mushrooms_html: | |
| mushrooms_html = "<p class='no-mushrooms'>No likely species for this season.</p>" | |
| cards.append(f""" | |
| <div class='location-card'> | |
| <h3 class='location-name'>🍄 {pred.get('name', 'Unknown')}</h3> | |
| <p class='habitat'>{pred.get('habitat_summary', '')}</p> | |
| <div class='mushroom-list'>{mushrooms_html}</div> | |
| <p class='visit-advice'><b>The forest suggests:</b> {pred.get('best_visit_advice', '')}</p> | |
| </div> | |
| """) | |
| return "<div class='cards-container'>" + "".join(cards) + "</div>" | |
| # --------------------------------------------------------------------------- | |
| # Main handler — orchestrates the whole flow | |
| # --------------------------------------------------------------------------- | |
| def find_mushroom_spots(zip_code, radius_miles, month_name): | |
| """The main function wired up to the Gradio button.""" | |
| if not zip_code or not zip_code.strip(): | |
| return ( | |
| "<p class='error'>Please enter a ZIP code to begin wandering.</p>", | |
| "", | |
| ) | |
| if not GEMINI_API_KEY: | |
| return ( | |
| "<p class='error'>This Space is missing its GEMINI_API_KEY secret. " | |
| "If you're the owner, add one in Settings → Variables and Secrets.</p>", | |
| "", | |
| ) | |
| # 1. Geocode | |
| try: | |
| geo = geocode_zip(zip_code) | |
| except Exception as e: | |
| return f"<p class='error'>Couldn't reach the geocoder: {e}</p>", "" | |
| if not geo: | |
| return f"<p class='error'>Couldn't find ZIP {zip_code}. Try another?</p>", "" | |
| lat, lng, place_name = geo | |
| # 2. Find public lands | |
| try: | |
| locations = find_public_lands(lat, lng, radius_miles) | |
| except Exception as e: | |
| return f"<p class='error'>Couldn't reach Overpass: {e}</p>", "" | |
| if not locations: | |
| map_html = build_map(lat, lng, [], []) | |
| cards_html = ( | |
| "<div class='no-results'>" | |
| "<p>No public lands found within that radius. Try expanding it, " | |
| "or perhaps wander somewhere wilder.</p></div>" | |
| ) | |
| return cards_html, map_html | |
| # 3. Ask Gemini for mushroom predictions | |
| try: | |
| predictions = get_mushroom_predictions(locations, place_name, lat, lng, month_name) | |
| except Exception as e: | |
| return f"<p class='error'>The mycelium is tangled: {e}</p>", "" | |
| # 4. Build the response | |
| map_html = build_map(lat, lng, locations, predictions) | |
| cards_html = build_result_cards(predictions) | |
| return cards_html, map_html | |
| # --------------------------------------------------------------------------- | |
| # Custom CSS — earthy, mossy, whimsical | |
| # --------------------------------------------------------------------------- | |
| CUSTOM_CSS = """ | |
| /* Background — parchment with a subtle paper texture via SVG noise */ | |
| .gradio-container { | |
| background: #f4ead5 !important; | |
| background-image: | |
| radial-gradient(at 20% 30%, rgba(122, 139, 58, 0.08) 0%, transparent 50%), | |
| radial-gradient(at 80% 70%, rgba(160, 82, 45, 0.06) 0%, transparent 50%), | |
| url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0.4 0 0 0 0 0.3 0 0 0 0 0.2 0 0 0 0.04 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E") !important; | |
| font-family: 'Lora', Georgia, serif !important; | |
| color: #3a2f1f !important; | |
| } | |
| /* Header */ | |
| .app-header { | |
| text-align: center; | |
| padding: 1.5rem 1rem 0.5rem 1rem; | |
| } | |
| .app-title { | |
| font-family: 'Fraunces', 'DM Serif Display', Georgia, serif; | |
| font-size: 3rem; | |
| font-weight: 600; | |
| color: #3d4a1a; | |
| margin: 0; | |
| letter-spacing: -0.02em; | |
| text-shadow: 1px 1px 0 rgba(255, 250, 230, 0.5); | |
| } | |
| .app-tagline { | |
| font-style: italic; | |
| color: #6b5d3f; | |
| font-size: 1.1rem; | |
| margin: 0.25rem 0 0 0; | |
| } | |
| /* Input panel — looks like an aged label */ | |
| .input-panel { | |
| background: rgba(255, 248, 225, 0.7) !important; | |
| border: 2px solid #8a7a5a !important; | |
| border-radius: 6px !important; | |
| padding: 1.25rem !important; | |
| box-shadow: 2px 3px 0 rgba(0, 0, 0, 0.05) !important; | |
| } | |
| /* Buttons */ | |
| button.primary, button[variant="primary"] { | |
| background: #3d4a1a !important; | |
| color: #f4ead5 !important; | |
| font-family: 'Fraunces', serif !important; | |
| font-size: 1.05rem !important; | |
| border: none !important; | |
| border-radius: 4px !important; | |
| padding: 0.6rem 1.2rem !important; | |
| letter-spacing: 0.02em !important; | |
| transition: all 0.2s !important; | |
| } | |
| button.primary:hover, button[variant="primary"]:hover { | |
| background: #2d3a0a !important; | |
| transform: translateY(-1px); | |
| box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15) !important; | |
| } | |
| /* Result cards */ | |
| .cards-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| padding: 0.5rem; | |
| } | |
| .location-card { | |
| background: rgba(255, 250, 235, 0.85); | |
| border: 1px solid #8a7a5a; | |
| border-left: 5px solid #7a8b3a; | |
| border-radius: 4px; | |
| padding: 1rem 1.25rem; | |
| box-shadow: 1px 2px 0 rgba(0, 0, 0, 0.04); | |
| } | |
| .location-name { | |
| font-family: 'Fraunces', serif; | |
| color: #3d4a1a; | |
| margin: 0 0 0.5rem 0; | |
| font-size: 1.4rem; | |
| font-weight: 500; | |
| } | |
| .habitat { | |
| color: #5a4a30; | |
| font-style: italic; | |
| margin: 0 0 0.75rem 0; | |
| line-height: 1.4; | |
| } | |
| .mushroom-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.6rem; | |
| margin: 0.75rem 0; | |
| } | |
| .mushroom-entry { | |
| background: rgba(244, 234, 213, 0.4); | |
| border-left: 3px solid #a0522d; | |
| padding: 0.5rem 0.75rem; | |
| border-radius: 2px; | |
| } | |
| .mushroom-header { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| align-items: baseline; | |
| } | |
| .mushroom-name { | |
| font-weight: 600; | |
| color: #3a2f1f; | |
| } | |
| .mushroom-sci { | |
| color: #6b5d3f; | |
| font-size: 0.9rem; | |
| } | |
| .mushroom-meta { | |
| margin: 0.25rem 0; | |
| display: flex; | |
| gap: 0.5rem; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .edibility-pill { | |
| color: #f4ead5; | |
| padding: 0.15rem 0.6rem; | |
| border-radius: 10px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .fruiting-window { | |
| color: #6b5d3f; | |
| font-size: 0.85rem; | |
| font-style: italic; | |
| } | |
| .mushroom-warning { | |
| color: #8b4513; | |
| font-size: 0.85rem; | |
| margin-top: 0.25rem; | |
| } | |
| .visit-advice { | |
| color: #3d4a1a; | |
| margin: 0.75rem 0 0 0; | |
| padding-top: 0.5rem; | |
| border-top: 1px dashed #c7b88f; | |
| font-size: 0.95rem; | |
| } | |
| .no-results, .no-mushrooms { | |
| text-align: center; | |
| color: #6b5d3f; | |
| font-style: italic; | |
| padding: 1rem; | |
| } | |
| .error { | |
| background: rgba(160, 82, 45, 0.15); | |
| border-left: 4px solid #a0522d; | |
| padding: 0.75rem 1rem; | |
| border-radius: 2px; | |
| color: #5a2500; | |
| } | |
| /* Map container styling */ | |
| iframe { | |
| border-radius: 4px !important; | |
| border: 2px solid #8a7a5a !important; | |
| box-shadow: 2px 3px 0 rgba(0, 0, 0, 0.05) !important; | |
| } | |
| /* Subtle warm filter on the map tiles to match the palette */ | |
| .folium-map { | |
| filter: sepia(0.15) saturate(0.9) brightness(0.98); | |
| } | |
| /* Disclaimer footer */ | |
| .disclaimer { | |
| background: rgba(139, 69, 19, 0.1); | |
| border: 1px dashed #8b4513; | |
| padding: 0.75rem 1rem; | |
| border-radius: 4px; | |
| color: #5a2500; | |
| font-size: 0.9rem; | |
| margin-top: 1rem; | |
| text-align: center; | |
| } | |
| .disclaimer b { | |
| color: #8b2500; | |
| } | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Gradio interface | |
| # --------------------------------------------------------------------------- | |
| # Pull in Google Fonts and build the header HTML. | |
| HEADER_HTML = """ | |
| <link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet"> | |
| <div class="app-header"> | |
| <h1 class="app-title">🍄 Mycelium Map</h1> | |
| <p class="app-tagline">A whimsical companion for the curious forager</p> | |
| </div> | |
| """ | |
| DISCLAIMER_HTML = """ | |
| <div class="disclaimer"> | |
| <b>⚠ Forager's Oath:</b> Never eat any wild mushroom without 100% positive identification | |
| by an expert mycologist. This app is educational only. Many edible species have deadly | |
| lookalikes. When in doubt, leave it on the forest floor. | |
| </div> | |
| """ | |
| # Pre-compute the month list so we always start on the current month. | |
| MONTHS = [calendar.month_name[i] for i in range(1, 13)] | |
| CURRENT_MONTH = calendar.month_name[datetime.now().month] | |
| with gr.Blocks(css=CUSTOM_CSS, title="Mycelium Map") as demo: | |
| gr.HTML(HEADER_HTML) | |
| with gr.Row(): | |
| with gr.Column(scale=1, elem_classes=["input-panel"]): | |
| gr.Markdown("### Where shall we wander?") | |
| zip_input = gr.Textbox( | |
| label="Your ZIP code", | |
| placeholder="e.g., 62035", | |
| value="", | |
| ) | |
| radius_input = gr.Slider( | |
| label="Search radius (miles)", | |
| minimum=5, | |
| maximum=50, | |
| value=15, | |
| step=5, | |
| ) | |
| month_input = gr.Dropdown( | |
| label="Month of the hunt", | |
| choices=MONTHS, | |
| value=CURRENT_MONTH, | |
| ) | |
| search_button = gr.Button( | |
| "Follow the mycelium →", | |
| variant="primary", | |
| ) | |
| with gr.Column(scale=2): | |
| map_output = gr.HTML(label="The land") | |
| gr.Markdown("### The forest suggests…") | |
| cards_output = gr.HTML() | |
| gr.HTML(DISCLAIMER_HTML) | |
| search_button.click( | |
| fn=find_mushroom_spots, | |
| inputs=[zip_input, radius_input, month_input], | |
| outputs=[cards_output, map_output], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |