import json from flask import Flask, render_template, request, jsonify, Response, redirect, url_for, flash, send_file import os from flask_login import LoginManager, login_user, logout_user, login_required, current_user from verify_v2 import verify_claim import db import health_passport as hp import ocr_utils import spread_predictor import translate_utils import auth import pdf_export import tts_utils import sys, os PROJECT_ROOT = os.path.dirname(__file__) if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) from gnn.gnn_predict import predict_spread from gnn.visualization_graph import generate_visualization_graph from comparison import compute_method_comparison from werkzeug.middleware.proxy_fix import ProxyFix app = Flask(__name__) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) app.secret_key = os.environ.get("FLASK_SECRET_KEY") or "verimed-ai-production-secret-key-fallback-change-me" import time # Cache-busting: bump this automatically on every server restart so # templates' `?v=` query param forces browsers to fetch the latest JS/CSS. app.config["ASSET_VERSION"] = str(int(time.time())) login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = "login" login_manager.login_message = "Please log in to access your Health Passport." @login_manager.user_loader def load_user(user_id): return auth.get_user_by_id(user_id) # Make sure tables exist before anything else runs db.init_db() hp.init_passport_table() auth.init_users_table() # --- PAGE ROUTING --- @app.route('/') def home(): raw_stats = db.get_stats() by_verdict = raw_stats.get("by_verdict", {}) stats = { "total_checked": raw_stats.get("total_checked", 0), "true_count": by_verdict.get("True", 0), "false_count": by_verdict.get("False", 0), "misleading_count": by_verdict.get("Misleading", 0), } return render_template('index.html', stats=stats) @app.route('/checker') def checker(): return render_template('checker.html') @app.route('/passport') @login_required def passport(): return render_template('passport.html', user=current_user) @app.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('passport')) if request.method == 'POST': name = request.form.get('name', '').strip() email = request.form.get('email', '').strip() password = request.form.get('password', '') confirm_password = request.form.get('confirm_password', '') if not name or not email or not password: flash("Please fill in all fields.", "error") return render_template('register.html') if password != confirm_password: flash("Passwords do not match.", "error") return render_template('register.html') if len(password) < 8: flash("Password must be at least 8 characters.", "error") return render_template('register.html') user = auth.create_user(name, email, password) if user is None: flash("An account with that email already exists. Try logging in instead.", "error") return render_template('register.html') login_user(user) return redirect(url_for('passport')) return render_template('register.html') @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('passport')) if request.method == 'POST': email = request.form.get('email', '').strip() password = request.form.get('password', '') user = auth.verify_login(email, password) if user is None: flash("Incorrect email or password.", "error") return render_template('login.html') login_user(user) next_page = request.args.get('next') return redirect(next_page or url_for('passport')) return render_template('login.html') @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('home')) @app.route('/emergency-help') def emergency_help(): return render_template('emergency_help.html') @app.route('/api/geocode', methods=['GET']) def api_geocode(): """ Server-side proxy for Nominatim (OpenStreetMap) geocoding. Calling Nominatim directly from browser JS is unreliable: their usage policy requires a proper identifying User-Agent header, which browser fetch() cannot set (the browser overrides it), so many client-side requests get silently blocked or rate-limited. Proxying through the backend lets us set a real User-Agent and see the actual error if one occurs, instead of a mysterious client-side failure. """ import requests query = request.args.get('q', '').strip() if not query: return jsonify({"error": "No location query provided."}), 400 try: resp = requests.get( "https://nominatim.openstreetmap.org/search", params={"format": "json", "limit": 1, "q": query}, headers={"User-Agent": "VeriMedAI-Hackathon-Project/1.0 (educational project)"}, timeout=10, ) resp.raise_for_status() results = resp.json() if not results: return jsonify({"error": f'Could not find "{query}".'}), 404 return jsonify({"lat": float(results[0]["lat"]), "lon": float(results[0]["lon"])}) except requests.exceptions.RequestException as e: app.logger.error(f"[api_geocode] Nominatim request failed: {e}") return jsonify({"error": "Location lookup service is unavailable right now."}), 502 @app.route('/api/nearby_hospitals', methods=['GET']) def api_nearby_hospitals(): """ Server-side proxy for Overpass API (OpenStreetMap) hospital/clinic search. Same rationale as api_geocode -- avoids client-side CORS/rate-limit issues and gives us real server-side error logging. """ import requests import math try: lat = float(request.args.get('lat')) lon = float(request.args.get('lon')) except (TypeError, ValueError): return jsonify({"error": "Valid lat/lon parameters are required."}), 400 overpass_query = f""" [out:json][timeout:25]; ( node["amenity"="hospital"](around:6000,{lat},{lon}); way["amenity"="hospital"](around:6000,{lat},{lon}); node["amenity"="clinic"](around:6000,{lat},{lon}); way["amenity"="clinic"](around:6000,{lat},{lon}); ); out center 40; """ try: resp = requests.post( "https://overpass-api.de/api/interpreter", data={"data": overpass_query}, headers={"User-Agent": "VeriMedAI-Hackathon-Project/1.0 (educational project)"}, timeout=25, ) resp.raise_for_status() elements = resp.json().get("elements", []) except requests.exceptions.RequestException as e: app.logger.error(f"[api_nearby_hospitals] Overpass request failed: {e}") return jsonify({"error": "Hospital search service is unavailable right now."}), 502 def haversine_km(lat1, lon1, lat2, lon2): R = 6371 d_lat = math.radians(lat2 - lat1) d_lon = math.radians(lon2 - lon1) a = (math.sin(d_lat / 2) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(d_lon / 2) ** 2) return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) def build_address(tags): parts = [tags.get("addr:housenumber"), tags.get("addr:street"), tags.get("addr:suburb"), tags.get("addr:city"), tags.get("addr:postcode")] parts = [p for p in parts if p] return ", ".join(parts) if parts else "Address not available" results = [] for el in elements: tags = el.get("tags", {}) if not tags.get("name"): continue el_lat = el.get("lat") or (el.get("center") or {}).get("lat") el_lon = el.get("lon") or (el.get("center") or {}).get("lon") if not el_lat or not el_lon: continue results.append({ "name": tags["name"], "type": "Hospital" if tags.get("amenity") == "hospital" else "Clinic", "address": build_address(tags), "phone": tags.get("phone") or tags.get("contact:phone"), "lat": el_lat, "lon": el_lon, "distance_km": round(haversine_km(lat, lon, el_lat, el_lon), 1), }) results.sort(key=lambda h: h["distance_km"]) return jsonify(results[:20]) @app.route('/emergency/') def emergency_view(share_token): """ Public, read-only emergency view -- this is what the QR code opens. No login required, intentionally shows only emergency-relevant fields. """ passport_data = hp.get_passport_by_token(share_token) if not passport_data: return render_template('emergency_view.html', passport=None), 404 surgeries = hp.get_surgeries(passport_data['user_id']) vaccinations = hp.get_vaccinations(passport_data['user_id']) return render_template('emergency_view.html', passport=passport_data, surgeries=surgeries, vaccinations=vaccinations) # --- API ENDPOINTS --- @app.route('/api/verify', methods=['POST']) def api_verify(): """Handles multimodal/multilingual inputs (Text, URL, Images via OCR)""" claim_text = request.form.get('claim', '').strip() language = request.form.get('language', 'en') ocr_used = False # Handle Image Upload for OCR processing if 'image' in request.files and request.files['image'].filename != '': image_file = request.files['image'] image_bytes = image_file.read() try: extracted_text = ocr_utils.extract_text_from_image(image_bytes, language_code=language) except Exception: return jsonify({ "verdict": "Unverified", "confidence": 0, "explanation": "Something went wrong reading this image. Please try a clearer screenshot or paste the claim as text.", "entities": [], "sources": [], "language_processed": language, }), 500 if not extracted_text: return jsonify({ "verdict": "Unverified", "confidence": 0, "explanation": "Couldn't find any readable text in this image. Try a clearer or higher-resolution screenshot, or paste the claim as text.", "entities": [], "sources": [], "language_processed": language, }), 422 claim_text = extracted_text ocr_used = True if not claim_text: return jsonify({"error": "No claim text provided."}), 400 # Translate non-English input into English before running the NER/RAG/LLM # pipeline (which operates in English). This now applies to both typed # and OCR-extracted text. original_claim_text = claim_text if language != "en": claim_text = translate_utils.translate_to_english(claim_text, language) try: result = verify_claim(claim_text) # Persist to history in English (keeps retrieval/history consistent) db.save_result(claim_text, result) # Translate the explanation back into the user's selected language for display explanation_for_display = result.get("explanation", "") if language != "en": explanation_for_display = translate_utils.translate_from_english(explanation_for_display, language) # Compute the GNN spread-risk prediction and the 3-method comparison. # This reuses the verdict/confidence/entities already computed above -- # no second LLM call, just fast local scoring. spread_result = predict_spread( claim_text=claim_text, verdict=result.get("verdict", "Unverified"), confidence=result.get("confidence", 0), entities=result.get("entities", []), ) method_comparison = compute_method_comparison(claim_text, result, spread_result) response = { "verdict": result.get("verdict"), "confidence": result.get("confidence"), "explanation": explanation_for_display, "entities": result.get("entities", []), "sources": result.get("sources", []), "language_processed": language, "ocr_used": ocr_used, "raw_text_detected": original_claim_text, "claim_text_used": claim_text, "spread_prediction": spread_result, "method_comparison": method_comparison, } return jsonify(response) except Exception as exc: app.logger.exception("Claim verification failed") return jsonify({ "error": "Failed to verify claim. Please try again.", "explanation": "A server error occurred while processing your verification request.", "verdict": "Unverified", "confidence": 0, "entities": [], "sources": [], "language_processed": language, "ocr_used": ocr_used, "raw_text_detected": original_claim_text, "claim_text_used": claim_text, }), 500 @app.route('/api/verify/report', methods=['POST']) def api_verify_report(): """Generate a downloadable PDF for the most recent claim verification.""" data = request.get_json(silent=True) or request.form.to_dict(flat=True) or {} def parse_json_field(value, default): if value is None or value == "": return default if isinstance(value, (dict, list)): return value try: return json.loads(value) except Exception: return default claim_text = (data.get("claim_text") or data.get("claim_text_used") or data.get("raw_text_detected") or "").strip() verdict = data.get("verdict") or "Unverified" confidence = int(data.get("confidence") or 0) explanation = data.get("explanation") or "" entities = parse_json_field(data.get("entities"), []) sources = parse_json_field(data.get("sources"), []) method_comparison = parse_json_field(data.get("method_comparison"), None) if not claim_text: return jsonify({"error": "No claim text provided for the report."}), 400 pdf_bytes = pdf_export.generate_verification_report_pdf( claim_text=claim_text, verdict=verdict, confidence=confidence, explanation=explanation, entities=entities, sources=sources, method_comparison=method_comparison, ) return Response( pdf_bytes, mimetype='application/pdf', headers={"Content-Disposition": "attachment; filename=VeriMed_Claim_Verification_Report.pdf"}, ) @app.route('/api/tts', methods=['POST']) def api_tts(): """ Server-side text-to-speech fallback using gTTS. The browser's built-in speech synthesis depends on voices installed on the user's device -- many devices don't have Hindi/Marathi voices installed, which silently falls back to English. This endpoint generates real audio in the requested language regardless of what's installed locally. """ from gtts import gTTS import io data = request.get_json(force=True) or {} text = data.get('text', '').strip() lang = data.get('lang', 'en') if not text: return jsonify({"error": "No text provided."}), 400 # Prefer Marathi directly when available; otherwise fall back to Hindi. # Some TTS backends do not support Marathi, so we try the preferred # language first and then a close fallback instead of silently using English. for gtts_lang in tts_utils.get_server_tts_lang_candidates(lang): try: tts = gTTS(text=text, lang=gtts_lang) buf = io.BytesIO() tts.write_to_fp(buf) buf.seek(0) return Response(buf.read(), mimetype='audio/mpeg') except Exception: continue return jsonify({"error": "TTS generation failed for the requested language."}), 500 @app.route('/api/history', methods=['GET']) def api_history(): limit = request.args.get('limit', default=20, type=int) return jsonify(db.get_history(limit=limit)) @app.route('/api/trending', methods=['GET']) def api_trending(): limit = request.args.get('limit', default=5, type=int) return jsonify(db.get_trending_verdicts(limit=limit)) @app.route('/api/passport', methods=['GET', 'POST']) @login_required def api_passport(): if request.method == 'POST': data = request.get_json(force=True) or {} required_defaults = { "full_name": "", "blood_group": "", "date_of_birth": "", "allergies": "", "chronic_conditions": "", "current_medicines": "", "emergency_contact_name": "", "emergency_contact_phone": "", } for key, default in required_defaults.items(): data.setdefault(key, default) hp.save_passport(int(current_user.id), data) return jsonify({"status": "saved"}) passport_data = hp.get_passport(int(current_user.id)) if not passport_data: return jsonify(None) return jsonify(passport_data) @app.route('/api/passport/qr', methods=['GET']) @login_required def api_passport_qr(): passport_data = hp.get_passport(int(current_user.id)) if not passport_data: return jsonify({"error": "No passport saved yet."}), 404 png_bytes = hp.generate_qr_code(passport_data['share_token'], request.host_url) return Response(png_bytes, mimetype='image/png') @app.route('/api/passport/surgeries', methods=['GET', 'POST']) @login_required def api_surgeries(): if request.method == 'POST': data = request.get_json(force=True) or {} hp.add_surgery(int(current_user.id), data.get('year', ''), data.get('description', '')) return jsonify({"status": "added"}) return jsonify(hp.get_surgeries(int(current_user.id))) @app.route('/api/passport/surgeries/', methods=['DELETE']) @login_required def api_delete_surgery(surgery_id): hp.delete_surgery(int(current_user.id), surgery_id) return jsonify({"status": "deleted"}) @app.route('/api/passport/vaccinations', methods=['GET', 'POST']) @login_required def api_vaccinations(): if request.method == 'POST': data = request.get_json(force=True) or {} hp.add_vaccination(int(current_user.id), data.get('vaccine_name', ''), data.get('month', ''), data.get('year', '')) return jsonify({"status": "added"}) return jsonify(hp.get_vaccinations(int(current_user.id))) @app.route('/api/passport/vaccinations/', methods=['DELETE']) @login_required def api_delete_vaccination(vaccination_id): hp.delete_vaccination(int(current_user.id), vaccination_id) return jsonify({"status": "deleted"}) @app.route('/api/passport/reports', methods=['GET', 'POST']) @login_required def api_reports(): if request.method == 'POST': if 'file' not in request.files or request.files['file'].filename == '': return jsonify({"error": "No file provided."}), 400 file_storage = request.files['file'] category = request.form.get('category', 'Other') month = request.form.get('month', '') year = request.form.get('year', '') report = hp.save_report_file(int(current_user.id), file_storage, category, month, year) return jsonify(report) return jsonify(hp.get_reports(int(current_user.id))) @app.route('/api/passport/reports/', methods=['DELETE']) @login_required def api_delete_report(report_id): hp.delete_report(int(current_user.id), report_id) return jsonify({"status": "deleted"}) @app.route('/api/passport/reports//download', methods=['GET']) @login_required def api_download_report(report_id): report = hp.get_report_by_id(int(current_user.id), report_id) if not report or not os.path.exists(report['stored_path']): return jsonify({"error": "Report not found."}), 404 return send_file(report['stored_path'], as_attachment=True, download_name=report['filename']) @app.route('/api/passport/pdf', methods=['GET']) @login_required def api_passport_pdf(): passport_data = hp.get_passport(int(current_user.id)) if not passport_data: return jsonify({"error": "No passport saved yet."}), 404 surgeries = hp.get_surgeries(int(current_user.id)) vaccinations = hp.get_vaccinations(int(current_user.id)) qr_bytes = hp.generate_qr_code(passport_data['share_token'], request.host_url) pdf_bytes = pdf_export.generate_passport_pdf(passport_data, surgeries, vaccinations, qr_bytes) return Response( pdf_bytes, mimetype='application/pdf', headers={"Content-Disposition": "attachment; filename=VeriMed_Health_Passport.pdf"}, ) if __name__ == '__main__': port = int(os.environ.get("PORT", 5000)) debug = os.environ.get("FLASK_DEBUG", "False").lower() in ("true", "1") app.run(debug=debug, host="0.0.0.0", port=port, use_reloader=False)