Spaces:
Sleeping
Sleeping
| # app.py | |
| import gradio as gr | |
| import requests | |
| import google.generativeai as genai | |
| import json | |
| import os | |
| import re | |
| # Configurazione da variabili d'ambiente | |
| GEMINI_KEY = os.getenv("GEMINI") | |
| UNSPLASH_KEY = os.getenv("UNSPLASH_ACCESS") | |
| WEATHER_KEY = os.getenv("OPENWEATHER") | |
| # Inizializzazione Gemini | |
| genai.configure(api_key=GEMINI_KEY) | |
| model = genai.GenerativeModel('gemini-pro') | |
| CSS = """ | |
| body { font-family: 'Arial', sans-serif; background: #f5f7fa; margin: 0; } | |
| .travel-container { max-width: 1200px; margin: 0 auto; padding: 20px; } | |
| .header { text-align: center; color: #2d3436; margin-bottom: 30px; } | |
| .section { background: white; padding: 20px; border-radius: 15px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } | |
| .card { border: 1px solid #e0e0e0; padding: 15px; margin: 10px 0; border-radius: 8px; transition: transform 0.2s; } | |
| .card:hover { transform: translateY(-3px); } | |
| .gallery { display: flex; flex-wrap: wrap; gap: 10px; margin: 15px 0; } | |
| .gallery img { width: 30%; flex-grow: 1; border-radius: 8px; height: 200px; object-fit: cover; } | |
| .weather-widget { background: #e3f2fd !important; padding: 15px !important; } | |
| .price-tag { color: #2e7d32; font-weight: bold; } | |
| .reasons-list { padding-left: 20px; color: #666; } | |
| .loading { text-align: center; padding: 30px; color: #555; } | |
| .error { background: #ffebee; color: #d32f2f; padding: 20px; border-radius: 8px; } | |
| """ | |
| def get_weather(city): | |
| try: | |
| url = "http://api.openweathermap.org/data/2.5/weather" | |
| params = { | |
| "q": city, | |
| "appid": WEATHER_KEY, | |
| "units": "metric", | |
| "lang": "it" | |
| } | |
| response = requests.get(url, params=params, timeout=10) | |
| data = response.json() | |
| return f""" | |
| <div class='section weather-widget'> | |
| <h3>⛅ Meteo Attuale</h3> | |
| <p>🌡️ Temperatura: {data['main']['temp']}°C</p> | |
| <p>☁️ Condizioni: {data['weather'][0]['description'].capitalize()}</p> | |
| <p>💧 Umidità: {data['main']['humidity']}%</p> | |
| <p>🌬️ Vento: {data['wind']['speed']} m/s</p> | |
| </div> | |
| """ | |
| except Exception as e: | |
| print(f"Errore meteo: {str(e)}") | |
| return "" | |
| def get_city_photos(city): | |
| try: | |
| url = "https://api.unsplash.com/search/photos" | |
| headers = {"Authorization": f"Client-ID {UNSPLASH_KEY}"} | |
| params = {"query": f"{city} city", "per_page": 3} | |
| response = requests.get(url, headers=headers, params=params, timeout=10) | |
| photos = [img['urls']['regular'] for img in response.json()['results']] | |
| return f""" | |
| <div class='section'> | |
| <h3>📸 Foto di {city.capitalize()}</h3> | |
| <div class='gallery'> | |
| {"".join([f"<img src='{photo}'>" for photo in photos])} | |
| </div> | |
| </div> | |
| """ | |
| except Exception as e: | |
| print(f"Errore foto: {str(e)}") | |
| return "" | |
| def generate_recommendations(city, category): | |
| prompt = f""" | |
| GENERA SOLO JSON VALIDO. Niente testo prima o dopo. | |
| Genera 5 consigli per {category} a {city} in formato JSON con struttura: | |
| {{ | |
| "recommendations": [ | |
| {{ | |
| "name": "Nome", | |
| "description": "Descrizione (max 40 parole)", | |
| "price_range": "€/€€/€€€", | |
| "address": "Zona", | |
| "reasons": ["motivo1", "motivo2", "motivo3"], | |
| "tip": "Consiglio pratico" | |
| }} | |
| ] | |
| }} | |
| """ | |
| try: | |
| response = model.generate_content(prompt) | |
| return response.text | |
| except Exception as e: | |
| print(f"Errore Gemini: {str(e)}") | |
| return None | |
| def parse_response(response): | |
| try: | |
| # Pulizia e validazione JSON | |
| json_str = re.sub(r'[\x00-\x1F]+', '', response) # Rimuove caratteri non stampabili | |
| json_str = re.search(r'\{.*\}', json_str, re.DOTALL).group() | |
| data = json.loads(json_str) | |
| # Validazione struttura | |
| if not isinstance(data.get('recommendations', []), list): | |
| return [] | |
| return data['recommendations'][:5] # Limita a 5 risultati | |
| except Exception as e: | |
| print(f"Errore parsing: {str(e)}") | |
| return [] | |
| with gr.Blocks(css=CSS) as app: | |
| gr.Markdown("# 🌍 Travel Assistant Pro") | |
| city_input = gr.Textbox(label="Inserisci città", placeholder="Es: Roma, Parigi...") | |
| search_btn = gr.Button("Cerca") | |
| output = gr.HTML() | |
| def search(city): | |
| if not city.strip(): | |
| return "<div class='error'>🚨 Inserisci una città valida</div>" | |
| yield "<div class='loading'>🔍 Ricerca in corso... ⏳</div>" | |
| try: | |
| # Dati base | |
| weather = get_weather(city) | |
| photos = get_city_photos(city) | |
| # Genera raccomandazioni | |
| sections = [] | |
| for cat in ["mangiare", "visitare", "dormire"]: | |
| response = generate_recommendations(city, cat) | |
| items = parse_response(response) if response else [] | |
| cards = [] | |
| for item in items: | |
| cards.append(f""" | |
| <div class='card'> | |
| <h3>{item.get('name', '')}</h3> | |
| <p>📍 {item.get('address', 'Indirizzo non disponibile')}</p> | |
| <p class='price-tag'>💰 {item.get('price_range', '€')}</p> | |
| <p>{item.get('description', '')}</p> | |
| <div class='reasons-list'> | |
| {"".join([f"<p>✔️ {r}</p>" for r in item.get('reasons', [])])} | |
| </div> | |
| <p>💡 {item.get('tip', '')}</p> | |
| </div> | |
| """) | |
| sections.append(f""" | |
| <div class='section'> | |
| <h2>🌟 {cat.capitalize()}</h2> | |
| {"".join(cards) if cards else '<p class="error">Nessun risultato trovato</p>'} | |
| </div> | |
| """) | |
| # Costruisci output | |
| content = f""" | |
| <div class='travel-container'> | |
| <h1 class='header'>{city.capitalize()}</h1> | |
| {weather} | |
| {photos} | |
| {"".join(sections)} | |
| </div> | |
| """ | |
| yield content | |
| except Exception as e: | |
| error_msg = f""" | |
| <div class='error'> | |
| <h3>❌ Errore grave</h3> | |
| <p>{str(e)}</p> | |
| <p>Prova a ricaricare la pagina o riprova più tardi</p> | |
| </div> | |
| """ | |
| yield error_msg | |
| app.launch() |