""" OmniParse AI — Invoice processing SaaS Refactored, modular Gradio app for HuggingFace Spaces. Views: Landing / Pricing / Legal / Auth / Dashboard All AI/OCR/DB layers degrade gracefully if a given secret/service is missing. Security: bcrypt passwords, rate limiting, server-side validation, file size limits, input sanitization, no raw HTML from users. UI: Custom SVG icons, skeleton loaders, tooltips, dark mode, toast notifications, drag-and-drop upload, semantic HTML. """ import os import csv import json from datetime import datetime, timezone import gradio as gr # --------------------------------------------------------------------------- # Module imports — all with graceful fallback # --------------------------------------------------------------------------- try: from config import ( APP_URL, GROQ_CLIENT, PLAN_LIMITS, PLAN_LABELS, PLAN_PRICES, MAX_FILE_SIZE_BYTES, ALLOWED_EXTENSIONS, SESSION_TTL_HOURS, ) except ImportError: APP_URL = os.environ.get("APP_URL", "http://localhost:7860") GROQ_CLIENT = None PLAN_LIMITS = {"free": 20, "basic": 200, "pro": 2000, "enterprise": float("inf")} PLAN_LABELS = {"free": "Free", "basic": "Basic", "pro": "Pro", "enterprise": "Enterprise"} PLAN_PRICES = {"free": 0, "basic": 29, "pro": 129, "enterprise": 499} MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024 ALLOWED_EXTENSIONS = {"pdf", "jpg", "jpeg", "png", "tiff", "tif"} SESSION_TTL_HOURS = 168 try: from database import ( db_init, get_user_by_email, get_user_by_id, update_user, get_session_user, delete_session, insert_invoice, get_invoices, count_invoices_this_month, delete_invoice, ) except ImportError: db_init = lambda: None get_user_by_email = lambda e: None get_user_by_id = lambda i: None update_user = lambda *a: None get_session_user = lambda t: None delete_session = lambda t: None insert_invoice = lambda *a: {} get_invoices = lambda u: [] count_invoices_this_month = lambda u: 0 delete_invoice = lambda *a: None try: from auth import ( create_user, authenticate_user, gen_api_key, create_session, hash_pw, ) except ImportError: create_user = lambda *a: (None, "Auth module not available") authenticate_user = lambda *a: (None, "Auth module not available") gen_api_key = lambda: "op_live_fallback" create_session = lambda u: "fallback_token" hash_pw = lambda p: p try: from validation import validate_email, validate_password, validate_name, validate_file except ImportError: validate_email = lambda e: (bool(e), "") validate_password = lambda p: (len(p) >= 8, "Password too short" if len(p) < 8 else "") validate_name = lambda n: (bool(n), "") validate_file = lambda f: (True, "") try: from rate_limiter import check_auth_rate, check_upload_rate, check_chat_rate except ImportError: check_auth_rate = lambda ip: True check_upload_rate = lambda ip: True check_chat_rate = lambda ip: True try: import icons as ic except ImportError: ic = None try: import html_templates as tmpl except ImportError: tmpl = None try: from ocr_pipeline import process_file as ocr_process_file except ImportError: ocr_process_file = None try: from stripe_handler import create_checkout_session, verify_checkout_session except ImportError: create_checkout_session = lambda *a: (None, "Payments not configured") verify_checkout_session = lambda *a: ("Verification unavailable", None) try: from invoice_processor import ( validate_invoice_data, _parse_date_any, process_and_save, batch_process, ) except ImportError: validate_invoice_data = lambda d: [] _parse_date_any = lambda s: None process_and_save = lambda *a: ({}, {}) batch_process = lambda *a: ([], [], 0, 0) # --------------------------------------------------------------------------- # Initialize # --------------------------------------------------------------------------- db_init() try: from auth import ensure_demo_account ensure_demo_account() except ImportError: pass # --------------------------------------------------------------------------- # Helper: get client IP from Gradio request # --------------------------------------------------------------------------- def _get_ip(request: gr.Request) -> str: try: return request.client.host if request and request.client else "unknown" except Exception: return "unknown" # --------------------------------------------------------------------------- # View management # --------------------------------------------------------------------------- ALL_VIEWS = [] # populated after Blocks creation def show_only(idx): return [gr.update(visible=(i == idx)) for i in range(len(ALL_VIEWS))] # --------------------------------------------------------------------------- # Navigation callbacks # --------------------------------------------------------------------------- def go_landing(): return show_only(0) def go_pricing(): return show_only(1) def go_legal(): return show_only(2) def go_auth(): return show_only(3) def go_dashboard(): return show_only(4) # --------------------------------------------------------------------------- # Auth callbacks # --------------------------------------------------------------------------- def do_login(email, password, request: gr.Request): ip = _get_ip(request) if not check_auth_rate(ip): return ( gr.update(value="Too many login attempts. Please wait a moment.", visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) if not email or not password: return ( gr.update(value="Please enter both email and password.", visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) user, err = authenticate_user(email, password, ip) if err: return ( gr.update(value=err, visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) token = create_session(str(user["id"])) return ( gr.update(value="", visible=False), token, user, *show_only(4), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True), ) def do_signup(name, email, password, terms, request: gr.Request): ip = _get_ip(request) if not check_auth_rate(ip): return ( gr.update(value="Too many registration attempts. Please wait a moment.", visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) # Server-side validation valid, msg = validate_name(name) if not valid: return ( gr.update(value=f"Invalid name: {msg}", visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) valid, msg = validate_email(email) if not valid: return ( gr.update(value=f"Invalid email: {msg}", visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) valid, pw_msgs = validate_password(password) if not valid: details = "; ".join(pw_msgs) return ( gr.update(value=f"Password requirements not met: {details}", visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) if not terms: return ( gr.update(value="You must agree to the Terms of Use and Privacy Policy.", visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) user, err = create_user(email, name, password) if err: return ( gr.update(value=err, visible=True), None, None, *show_only(3), gr.update(), gr.update(), gr.update(), gr.update(), ) token = create_session(str(user["id"])) return ( gr.update(value="", visible=False), token, user, *show_only(4), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True), ) def do_logout(token): if token: delete_session(token) return ( None, None, *show_only(0), gr.update(visible=True), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), ) # --------------------------------------------------------------------------- # Dashboard load / usage # --------------------------------------------------------------------------- def load_dashboard(user): if not user: return "## Dashboard\n\n_You are not signed in._", "" used = count_invoices_this_month(user["id"]) limit = PLAN_LIMITS.get(user["plan"], 20) limit_str = "\u221e" if limit == float("inf") else str(int(limit)) plan_label = PLAN_LABELS.get(user["plan"], user["plan"]) welcome = f"## Dashboard\n\nWelcome back, **{user['name']}** (Plan: {plan_label})" usage = f"**{used}/{limit_str} invoices used this month**" if limit != float("inf") and used >= limit: usage += "\n\n**Monthly limit reached.** Go to the Upgrade tab to increase your limit." return welcome, usage # --------------------------------------------------------------------------- # Upload / processing # --------------------------------------------------------------------------- def do_upload(files, user, request: gr.Request): ip = _get_ip(request) if not user: return "You must be signed in to upload invoices.", [], None, "" if not files: return "No files selected. Please select invoice files to process.", [], None, "" if not check_upload_rate(ip): return "Upload rate limit reached. Please wait a moment.", [], None, "" limit = PLAN_LIMITS.get(user["plan"], 20) used = count_invoices_this_month(user["id"]) rows = [] last_json = None processed = 0 for f in files: if used + processed >= limit: break path = f.name if hasattr(f, "name") else f filename = os.path.basename(path) # Server-side file validation valid, err = validate_file(path) if not valid: rows.append([filename, "Validation Error", None, None, None, err]) continue try: if ocr_process_file: data = ocr_process_file(path, user) else: data = { "filename": filename, "vendor": "Demo Vendor Inc.", "invoice_number": "DEMO-0001", "invoice_date": datetime.now().strftime("%Y-%m-%d"), "due_date": None, "amount": 100.0, "vat_amount": 21.0, "total": 121.0, "currency": "USD", "line_items": [], } except Exception as e: data = { "filename": filename, "vendor": None, "invoice_number": None, "invoice_date": None, "due_date": None, "amount": None, "vat_amount": None, "total": None, "currency": "USD", "status": "review", "confidence": 0, "warnings": [f"Processing error: {str(e)[:200]}"], } # Validate extracted data warnings = validate_invoice_data(data) if warnings: data["warnings"] = data.get("warnings", []) + warnings if data.get("status") != "duplicate": data["status"] = "review" saved = insert_invoice(user["id"], data) last_json = data status_label = data.get("status", "done") status_display = { "done": "Done", "review": "Review", "duplicate": "Duplicate", "processing": "Processing", }.get(status_label, status_label) rows.append([ data.get("filename"), data.get("vendor"), data.get("invoice_number"), data.get("invoice_date"), data.get("total"), status_display, ]) processed += 1 skipped = len(files) - processed msg = f"Successfully processed {processed} invoice{'s' if processed != 1 else ''}." if skipped > 0: msg += f" {skipped} file{'s' if skipped != 1 else ''} skipped \u2014 monthly limit reached. Upgrade your plan to process more." used_new = count_invoices_this_month(user["id"]) limit_str = "\u221e" if limit == float("inf") else str(int(limit)) usage = f"**{used_new}/{limit_str} invoices used this month**" return msg, rows, last_json, usage # --------------------------------------------------------------------------- # My Invoices # --------------------------------------------------------------------------- STATUS_MAP = { "done": "Done", "review": "Review", "duplicate": "Duplicate", "processing": "Processing", } def refresh_invoices(user, flt): if not user: return [] invoices = get_invoices(user["id"]) rows = [] for inv in invoices: status = inv.get("status", "done") if flt == "Done" and status != "done": continue if flt == "Review" and status != "review": continue if flt == "Duplicates" and not inv.get("is_duplicate"): continue rows.append([ inv.get("id"), inv.get("vendor"), inv.get("inv_number"), inv.get("inv_date"), inv.get("total"), STATUS_MAP.get(status, status), ]) return rows def do_delete_invoice(user, inv_id): if not user or not inv_id: return "Please enter a valid invoice ID.", [] inv_id = int(inv_id) delete_invoice(user["id"], inv_id) return f"Invoice #{inv_id} has been deleted.", refresh_invoices(user, "All") # --------------------------------------------------------------------------- # AI Chat # --------------------------------------------------------------------------- def chat_respond(message, history, user, request: gr.Request): history = history or [] if not user: history.append({"role": "assistant", "content": "You must be signed in to use AI Chat."}) return history, "" if not check_chat_rate(_get_ip(request)): history.append({"role": "assistant", "content": "Chat rate limit reached. Please wait a moment."}) return history, "" if user["plan"] not in ("pro", "enterprise"): history.append({ "role": "assistant", "content": "AI Chat is available on the Pro plan and above. Upgrade from the Upgrade tab to unlock this feature.", }) return history, "" if not message or not message.strip(): return history, "" invoices = get_invoices(user["id"]) context = json.dumps(invoices[:100], default=str, ensure_ascii=False)[:6000] history.append({"role": "user", "content": message}) if GROQ_CLIENT: try: resp = GROQ_CLIENT.chat.completions.create( model="llama-3.1-8b-instant", messages=[ { "role": "system", "content": ( "You are an assistant answering questions about the user's invoices. " f"Here is their invoice data as JSON: {context}. " "Answer concisely based only on this data. Use plain text, no markdown." ), }, {"role": "user", "content": message}, ], max_tokens=400, temperature=0.2, timeout=10, ) answer = resp.choices[0].message.content except Exception as e: answer = f"AI is temporarily unavailable ({str(e)[:100]}). Please try again." else: answer = "AI Chat requires the GROQ_API_KEY to be configured." history.append({"role": "assistant", "content": answer}) return history, "" # --------------------------------------------------------------------------- # Export # --------------------------------------------------------------------------- def export_csv(user): if not user: return None invoices = get_invoices(user["id"]) path = f"/tmp/omniparse_export_{user['id']}.csv" with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow([ "ID", "Vendor", "Invoice #", "Date", "Due Date", "Amount", "VAT", "Total", "Currency", "Status", ]) for inv in invoices: writer.writerow([ inv.get("id"), inv.get("vendor"), inv.get("inv_number"), inv.get("inv_date"), inv.get("due_date"), inv.get("amount"), inv.get("vat_amount"), inv.get("total"), inv.get("currency"), inv.get("status"), ]) return path def export_json(user): if not user: return None if user["plan"] == "free": return None invoices = get_invoices(user["id"]) path = f"/tmp/omniparse_export_{user['id']}.json" with open(path, "w", encoding="utf-8") as f: json.dump(invoices, f, default=str, ensure_ascii=False, indent=2) return path # --------------------------------------------------------------------------- # Upgrade / Stripe # --------------------------------------------------------------------------- def do_upgrade(plan, user): if not user: return "You must be signed in to upgrade." url, err = create_checkout_session(plan, user) if err: return err return ( f"[Complete your payment via Stripe]({url})\n\n" "After paying, return here and paste the `session_id` from the URL below." ) def do_check_payment(session_id, user): if not user or not session_id: return "Please enter a valid session ID." msg, err = verify_checkout_session(session_id, user["id"]) if err: return err return msg # --------------------------------------------------------------------------- # API tab # --------------------------------------------------------------------------- def load_api_tab(user): if not user: return ( gr.update(visible=True, value="You must be signed in."), gr.update(visible=False), gr.update(visible=False), ) if user["plan"] not in ("pro", "enterprise"): return ( gr.update( visible=True, value="API access is available on the Pro plan and above. " "Upgrade from the Upgrade tab to get your API key.", ), gr.update(visible=False), gr.update(visible=False), ) key = user.get("api_key") or "\u2014" snippet = ( f"```bash\n" f"curl -X POST {APP_URL}/api/extract \\\n" f' -H "Authorization: Bearer {key}" \\\n' f" -F \"file=@invoice.pdf\"\n" f"```" ) return ( gr.update(visible=False), gr.update(visible=True, value=f"**Your API Key:** `{key}`"), gr.update(visible=True, value=snippet), ) # --------------------------------------------------------------------------- # Profile # --------------------------------------------------------------------------- def load_profile(user): if not user: return "You are not signed in." plan = PLAN_LABELS.get(user["plan"], user["plan"]) price = PLAN_PRICES.get(user["plan"], 0) return ( f"**Name:** {user['name']}\n\n" f"**Email:** {user['email']}\n\n" f"**Plan:** {plan} (${price}/mo)\n\n" f"**Created:** {(user.get('created_at') or '')[:10]}" ) def do_change_password(user, new_pw, request: gr.Request): if not user: return "You must be signed in." valid, pw_msgs = validate_password(new_pw) if not valid: details = "; ".join(pw_msgs) return f"Password requirements not met: {details}" update_user(str(user["id"]), {"password": hash_pw(new_pw)}) return "Password has been changed successfully." def do_delete_account(user, token): if not user: return "You must be signed in.", None, None uid = str(user["id"]) try: from database import _sqlite_conn conn = _sqlite_conn() conn.execute("DELETE FROM invoices WHERE user_id=?", (uid,)) conn.execute("DELETE FROM users WHERE id=?", (uid,)) conn.commit() conn.close() except Exception: pass if token: delete_session(token) return "Your account has been permanently deleted.", None, None # --------------------------------------------------------------------------- # Build Gradio App # --------------------------------------------------------------------------- CUSTOM_CSS = tmpl.BASE_CSS if tmpl else "" with gr.Blocks( title="OmniParse AI", css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="violet"), head=( (tmpl.ALL_JS if tmpl else "") + (tmpl.DARK_MODE_JS if tmpl else "") ), ) as demo: session_token = gr.State(None) current_user = gr.State(None) # ===================== NAVBAR ===================== with gr.Row(elem_id="op-navbar"): if tmpl: gr.HTML(tmpl.navbar_html()) else: gr.HTML('
Pricing information unavailable.
") gr.Markdown( "Enterprise annual plan: **$4,188/year** " "(save 2 months compared to monthly billing)." ) with gr.Accordion("Frequently asked questions about payments", open=False): gr.Markdown( "- **What payment methods do you accept?** " "Card payments via Stripe (Visa, Mastercard, Amex).\n" "- **Can I change plans at any time?** " "Yes, upgrade or downgrade from Dashboard \u2192 Upgrade.\n" "- **Do you offer refunds?** " "Within 14 days of your first payment, upon request." ) pricing_back_btn = gr.Button("Back to Home") # ===================== VIEW 2: LEGAL ===================== with gr.Column(visible=False) as view_legal: gr.HTML(tmpl.legal_html() if tmpl else "