Spaces:
Running
Running
| import os | |
| import csv | |
| import json | |
| import requests | |
| import re | |
| from datetime import datetime, timedelta | |
| from flask import Flask, request, jsonify, send_from_directory | |
| from flask_cors import CORS | |
| from models import Capture, Species, ClosedSeason | |
| app = Flask(__name__, static_folder='static', static_url_path='') | |
| CORS(app) | |
| # Cache for FishWatch API data | |
| _fishwatch_cache = { | |
| 'data': None, | |
| 'expiry': None | |
| } | |
| def fetch_fishwatch_data(retries=2): | |
| """Fetch species data from FishWatch API with caching and retries.""" | |
| global _fishwatch_cache | |
| if _fishwatch_cache['data'] and _fishwatch_cache['expiry'] > datetime.now(): | |
| return _fishwatch_cache['data'] | |
| for attempt in range(retries): | |
| try: | |
| # Note: FishWatch API can be slow or unstable | |
| url = "https://www.fishwatch.gov/api/species" | |
| print(f"DEBUG: Fetching FishWatch data (attempt {attempt + 1}/{retries})...") | |
| # Using allow_redirects=True (default) but being explicit | |
| response = requests.get(url, timeout=30, allow_redirects=True) | |
| if response.status_code == 200: | |
| text = response.text.strip() | |
| if not text: | |
| print("DEBUG: FishWatch API returned empty response") | |
| continue | |
| # Robust JSON parsing | |
| try: | |
| data = json.loads(text) if hasattr(requests, 'json') else response.json() | |
| _fishwatch_cache['data'] = data | |
| _fishwatch_cache['expiry'] = datetime.now() + timedelta(hours=2) | |
| print(f"DEBUG: Successfully fetched {len(data)} species from FishWatch") | |
| return data | |
| except Exception as e: | |
| print(f"DEBUG: JSON decode error on FishWatch: {e}") | |
| print(f"DEBUG: Response starts with: {text[:100]}") | |
| else: | |
| print(f"DEBUG: FishWatch API returned status: {response.status_code}") | |
| except requests.exceptions.Timeout: | |
| print(f"DEBUG: FishWatch API timeout (attempt {attempt + 1}/{retries})") | |
| except Exception as e: | |
| print(f"DEBUG: Error fetching FishWatch data: {e}") | |
| return [] | |
| def slugify(text): | |
| """Simple slugify for species IDs.""" | |
| text = text.lower() | |
| text = re.sub(r'[^\w\s-]', '', text) | |
| return re.sub(r'[-\s]+', '-', text).strip('-') | |
| def load_species(): | |
| """Load unique species from the provided CSV file enriched with FishWatch API.""" | |
| species_list = [] | |
| seen_products = set() | |
| # Ensure correct CSV path | |
| base_dir = os.path.dirname(os.path.abspath(__file__)) | |
| csv_path = os.path.join(base_dir, 'produccion_pesca_limpia_refined.csv') | |
| print(f"DEBUG: CWD is {os.getcwd()}") | |
| print(f"DEBUG: base_dir is {base_dir}") | |
| print(f"DEBUG: Checking CSV path: {csv_path}") | |
| print(f"DEBUG: Files in base_dir: {os.listdir(base_dir)}") | |
| # Fetch FishWatch data | |
| fishwatch_data = fetch_fishwatch_data() | |
| # Map by common name (normalized) for easier lookup | |
| fw_map = {fw.get('Species Name', '').lower(): fw for fw in fishwatch_data} | |
| # Pre-defined base species (with more metadata if available) | |
| base_metadata = { | |
| 'chillo': {'sci': 'Lutjanus campechanus', 'img': 'assets/species/chillo.jpg', 'veda': ClosedSeason(start='04-01', end='06-30', description='Veda de reproducción')}, | |
| 'dorado': {'sci': 'Coryphaena hippurus', 'img': 'assets/species/dorado.jpg'}, | |
| 'langosta-comun-del-caribe': {'sci': 'Panulirus argus', 'img': 'assets/species/langosta.jpg', 'veda': ClosedSeason(start='03-01', end='06-30', description='Veda de reproducción')} | |
| } | |
| if os.path.exists(csv_path): | |
| with open(csv_path, mode='r', encoding='utf-8') as f: | |
| reader = csv.DictReader(f) | |
| for row in reader: | |
| product_name = row['Producto'] | |
| category = row['categoria'] | |
| if product_name not in seen_products: | |
| species_id = slugify(product_name) | |
| metadata = base_metadata.get(species_id, {}) | |
| # Try to find in FishWatch | |
| fw_info = fw_map.get(product_name.lower()) | |
| img_url = metadata.get('img', 'assets/species/placeholder.jpg') | |
| sci_name = metadata.get('sci', '') | |
| if fw_info: | |
| # Extract illustration if available | |
| photos = fw_info.get('Species Illustration Photo', {}) | |
| if isinstance(photos, dict) and photos.get('src'): | |
| img_url = photos['src'] | |
| elif isinstance(photos, list) and len(photos) > 0: | |
| img_url = photos[0].get('src', img_url) | |
| if not sci_name: | |
| sci_name = fw_info.get('Scientific Name', '') | |
| species_list.append(Species( | |
| id=species_id, | |
| commonName=product_name, | |
| scientificName=sci_name, | |
| category=category, | |
| imageUrl=img_url, | |
| protected=False, | |
| closedSeason=metadata.get('veda') | |
| )) | |
| seen_products.add(product_name) | |
| # If CSV failed or is empty, use defaults | |
| if not species_list: | |
| print("CSV not found or empty, using expanded fallback list") | |
| species_list = [ | |
| Species(id='chillo', commonName='Chillo', scientificName='Lutjanus campechanus', category='Peces', imageUrl='assets/species/chillo.jpg', protected=False, closedSeason=ClosedSeason(start='04-01', end='06-30', description='Veda de reproducción')), | |
| Species(id='dorado', commonName='Dorado', scientificName='Coryphaena hippurus', category='Peces', imageUrl='assets/species/dorado.jpg', protected=False), | |
| Species(id='langosta', commonName='Langosta del Caribe', scientificName='Panulirus argus', category='Crustáceos', imageUrl='assets/species/langosta.jpg', protected=False, closedSeason=ClosedSeason(start='03-01', end='06-30', description='Veda de reproducción')), | |
| Species(id='mero', commonName='Mero', scientificName='Epinephelus itajara', category='Peces', imageUrl='assets/species/placeholder.jpg', protected=False), | |
| Species(id='atun', commonName='Atún', scientificName='Thunnus', category='Peces', imageUrl='assets/species/placeholder.jpg', protected=False), | |
| Species(id='pulpo', commonName='Pulpo', scientificName='Octopus vulgaris', category='Moluscos', imageUrl='assets/species/placeholder.jpg', protected=False), | |
| Species(id='lambí', commonName='Lambí', scientificName='Lobatus gigas', category='Moluscos', imageUrl='assets/species/placeholder.jpg', protected=False), | |
| ] | |
| return species_list | |
| # Load Data | |
| SPECIES_DATA = load_species() | |
| captures_storage = [] | |
| def index(): | |
| return app.send_static_file('index.html') | |
| def health_check(): | |
| return jsonify({ | |
| 'status': 'healthy', | |
| 'timestamp': datetime.now().isoformat() | |
| }) | |
| def get_species(): | |
| return jsonify([s.to_dict() for s in SPECIES_DATA]) | |
| def create_capture(): | |
| try: | |
| data = request.get_json() | |
| capture = Capture.from_dict(data) | |
| captures_storage.append(capture) | |
| return jsonify({'success': True, 'id': capture.id}), 201 | |
| except Exception as e: | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def chat(): | |
| """AI Assistant 'Anzuelo' for El Salvador fishing normative.""" | |
| try: | |
| data = request.get_json() | |
| user_message = data.get('message', '') | |
| # Integration with Gemini (simplified for trial) | |
| # In a real environment, we would use the google-generativeai library | |
| api_key = os.environ.get('GEMINI_API_KEY') | |
| system_prompt = ( | |
| "Eres la Anzuelo, una experta mística en la normativa de pesca de El Salvador (CODEPESCA). " | |
| "Tu objetivo es ayudar a los pescadores con dudas técnicas y legales de forma clara, " | |
| "con un toque de sabiduría del mar. Utiliza un tono amable y servicial." | |
| ) | |
| if not api_key: | |
| # Fallback for demonstration if no key is present | |
| return jsonify({ | |
| 'response': f"Soy Anzuelo. Recibí tu mensaje: '{user_message}'. " | |
| "Actualmente opero con conocimiento local de CODEPESCA. " | |
| "Recuerda que la veda de langosta termina en Junio." | |
| }) | |
| import google.generativeai as genai | |
| genai.configure(api_key=api_key) | |
| model = genai.GenerativeModel('gemini-pro') | |
| chat = model.start_chat(history=[ | |
| {"role": "user", "parts": [system_prompt]}, | |
| {"role": "model", "parts": ["Entendido. Soy Anzuelo, guía de CODEPESCA. ¿Cómo puedo asesorarte hoy?"]}, | |
| ]) | |
| response = chat.send_message(user_message) | |
| return jsonify({'response': response.text}) | |
| except Exception as e: | |
| print(f"Chat error: {str(e)}") | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def sync_captures(): | |
| """Batch synchronization endpoint with Google Sheets forwarding.""" | |
| try: | |
| data = request.get_json() | |
| captures_data = data.get('captures', []) | |
| # Store locally (in-memory for now) | |
| for c_data in captures_data: | |
| capture = Capture.from_dict(c_data) | |
| captures_storage.append(capture) | |
| # Forward to Google Sheets if configured | |
| gas_url = os.environ.get('GAS_DEPLOYMENT_URL') | |
| if gas_url and captures_data: | |
| print(f"Forwarding {len(captures_data)} captures to Google Sheets...") | |
| for c in captures_data: | |
| # Explicitly log that these are device coordinates being forwarded | |
| print(f" [DEVICE SYNC] Capture {c.get('id')}:") | |
| print(f" - Port: {c.get('port')}") | |
| print(f" - Latitude: {c.get('latitude')} (captured on device)") | |
| print(f" - Longitude: {c.get('longitude')} (captured on device)") | |
| print(f" - Place: {c.get('placeName')}") | |
| try: | |
| import requests | |
| # Forward the exact payload expected by the GAS script | |
| print(f"DEBUG: Forwarding to GAS URL: {gas_url[:50]}...") | |
| response = requests.post(gas_url, json={'captures': captures_data}, timeout=15) | |
| print(f"DEBUG: GAS Response Status: {response.status_code}") | |
| print(f"DEBUG: GAS Response Body: {response.text}") | |
| if not response.ok: | |
| print(f"ERROR: Google Sheets sync failed. Status: {response.status_code}, Body: {response.text}") | |
| else: | |
| print(f"SUCCESS: Data accepted by Google Sheets: {response.text}") | |
| except Exception as e: | |
| print(f"CRITICAL: Error communicating with GAS: {str(e)}") | |
| synced_ids = [c.get('id') for c in captures_data] | |
| return jsonify({ | |
| 'success': True, | |
| 'synced': synced_ids, | |
| 'total': len(synced_ids) | |
| }), 200 | |
| except Exception as e: | |
| print(f"Sync error: {str(e)}") | |
| return jsonify({'success': False, 'error': str(e)}), 400 | |
| def debug_files(): | |
| files = [] | |
| for root, _, filenames in os.walk('static'): | |
| for f in filenames: | |
| rel_path = os.path.relpath(os.path.join(root, f), 'static') | |
| files.append({ | |
| "path": rel_path, | |
| "size": os.path.getsize(os.path.join(root, f)) | |
| }) | |
| return jsonify(files) | |
| if __name__ == '__main__': | |
| port = int(os.environ.get('PORT', 7860)) | |
| app.run(host='0.0.0.0', port=port) | |
| # Deployment trigger: 2026-01-23 18:55 | |