Spaces:
Paused
Paused
| 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." | |
| 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 --- | |
| 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) | |
| def checker(): | |
| return render_template('checker.html') | |
| def passport(): | |
| return render_template('passport.html', user=current_user) | |
| 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') | |
| 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') | |
| def logout(): | |
| logout_user() | |
| return redirect(url_for('home')) | |
| def emergency_help(): | |
| return render_template('emergency_help.html') | |
| 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 | |
| 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]) | |
| 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 --- | |
| 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 | |
| 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"}, | |
| ) | |
| 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 | |
| def api_history(): | |
| limit = request.args.get('limit', default=20, type=int) | |
| return jsonify(db.get_history(limit=limit)) | |
| def api_trending(): | |
| limit = request.args.get('limit', default=5, type=int) | |
| return jsonify(db.get_trending_verdicts(limit=limit)) | |
| 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) | |
| 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') | |
| 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))) | |
| def api_delete_surgery(surgery_id): | |
| hp.delete_surgery(int(current_user.id), surgery_id) | |
| return jsonify({"status": "deleted"}) | |
| 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))) | |
| def api_delete_vaccination(vaccination_id): | |
| hp.delete_vaccination(int(current_user.id), vaccination_id) | |
| return jsonify({"status": "deleted"}) | |
| 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))) | |
| def api_delete_report(report_id): | |
| hp.delete_report(int(current_user.id), report_id) | |
| return jsonify({"status": "deleted"}) | |
| 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']) | |
| 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) |