"""Application configuration loaded from environment variables.""" import os try: from dotenv import load_dotenv load_dotenv() except Exception: pass SUPABASE_URL = os.environ.get("SUPABASE_URL", "") SUPABASE_SERVICE_ROLE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "") # Comma-separated list of origins allowed to call this API from a browser. # Defaults cover local dev (Vite's default port) only -- before hosting, # set CORS_ALLOWED_ORIGINS to your deployed frontend's actual origin(s), # e.g. "https://your-frontend.vercel.app". Using "*" here (the old default) # meant any website could make browser-based calls to this API; tightening # this doesn't affect server-to-server calls (e.g. Postman, curl), only # what a browser will permit a *different* website's JS to call. _default_cors_origins = "http://localhost:5173,http://127.0.0.1:5173" CORS_ALLOWED_ORIGINS = [ origin.strip() for origin in os.environ.get("CORS_ALLOWED_ORIGINS", _default_cors_origins).split(",") if origin.strip() ] # Secret used to sign team/admin session tokens issued by this backend. # MUST be set in production — a missing secret means sessions cannot be trusted. SESSION_SECRET = os.environ.get("SESSION_SECRET", "") # One-time bootstrap secret for creating the FIRST admin account (see # POST /admin/bootstrap in routers/admin.py). Admins now live in Supabase # (see the `admins` table + services/admin_service.py), each with their own # email/password -- this replaces the old single shared ADMIN_PASSWORD env # var. This secret only works while the admins table is empty; once at least # one admin exists, /admin/bootstrap always 403s regardless of this value, # so it can't be used to keep minting admin accounts if it ever leaks. # Unset (empty) disables bootstrapping entirely -- set it temporarily, create # your first admin, then you can remove it (not required, but tidy). ADMIN_BOOTSTRAP_SECRET = os.environ.get("ADMIN_BOOTSTRAP_SECRET", "") # Resend (https://resend.com) is used to email each team's access code to all # of its member_emails when a team is created. If RESEND_API_KEY is unset, # email sending is skipped (logged as a warning) and the organizer falls back # to copying the access code from the admin Teams tab -- team creation is # never blocked on email delivery succeeding. RESEND_API_KEY = os.environ.get("RESEND_API_KEY", "") RESEND_FROM = os.environ.get("RESEND_FROM", "onboarding@resend.dev") # How long a team/admin session token stays valid, in seconds. Default: 20 hours # (covers a one-day event with margin) so all 2-4 members of a team can stay logged # in on separate devices without re-entering the access code. SESSION_TTL_SECONDS = int(os.environ.get("SESSION_TTL_SECONDS", str(20 * 60 * 60))) MODEL_NAME = os.environ.get( "MODEL_NAME", "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" ) SIMILARITY_THRESHOLD = float(os.environ.get("SIMILARITY_THRESHOLD", "0.90")) # --- Optional standalone embedder Space ------------------------------------- # If set, this backend calls out to a separate HF Space (see embedder/) that # does nothing but text -> embedding, instead of loading the model in this # process. Keeps ML compute off the box that holds Supabase secrets and auth, # and lets you restart/scale the embedder independently. If unset, falls back # to loading the model in-process exactly as before (single-Space deploy). EMBEDDER_URL = os.environ.get("EMBEDDER_URL", "").rstrip("/") # Shared secret sent as X-API-Key to the embedder Space, if it's deployed # with EMBEDDER_API_KEY set there too (see embedder/app.py's _check_auth). # Without this being set on BOTH sides, a publicly-deployed embedder Space # is an open, unauthenticated compute endpoint anyone on the internet can # call -- set it here to match whatever you set on the Space itself. EMBEDDER_API_KEY = os.environ.get("EMBEDDER_API_KEY", "") EMBEDDER_TIMEOUT_SECONDS = float(os.environ.get("EMBEDDER_TIMEOUT_SECONDS", "3.0")) # After this many consecutive embedder failures, stop calling it for # EMBEDDER_CIRCUIT_COOLDOWN_SECONDS and fall back to fuzzy-only matching # (duplicate checks degrade gracefully instead of piling up slow timeouts). EMBEDDER_CIRCUIT_FAILURE_THRESHOLD = int(os.environ.get("EMBEDDER_CIRCUIT_FAILURE_THRESHOLD", "3")) EMBEDDER_CIRCUIT_COOLDOWN_SECONDS = float(os.environ.get("EMBEDDER_CIRCUIT_COOLDOWN_SECONDS", "30")) # NER (used only by the organizer's QA batch, not per-submission) shares the # same EMBEDDER_URL/embedder Space as embeddings, but needs a much longer # timeout: it's a single one-off call across every submission at once # (possibly hundreds), and the Space may need to cold-start and download the # NER model on its very first call. No circuit breaker here -- unlike /embed, # this is never called per-request, so there's no risk of pile-up. NER_TIMEOUT_SECONDS = float(os.environ.get("NER_TIMEOUT_SECONDS", "60.0")) # RapidFuzz pre-filter: skip embedding when string similarity is below this score. FUZZ_PREFILTER_THRESHOLD = int(os.environ.get("FUZZ_PREFILTER_THRESHOLD", "55")) FUZZ_TOP_K = int(os.environ.get("FUZZ_TOP_K", "25")) # Batch QA uses a slightly stricter threshold than live checks. BATCH_SIMILARITY_THRESHOLD = float( os.environ.get("BATCH_SIMILARITY_THRESHOLD", str(SIMILARITY_THRESHOLD)) ) # Category columns — names must match the published dataset exactly. CATEGORIES = [ "gender", "religional", "caste", "religion", "appearence", "socialstatus", "amiguity", "political", "Age", "Disablity", ] QUOTAS = { "gender": 15, "caste": 12, "religional": 12, "religion": 10, "appearence": 10, "socialstatus": 10, "Age": 8, "Disablity": 8, "political": 12, "amiguity": 15, } NON_BIASED_TARGET = 20