File size: 5,806 Bytes
3f72838
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
"""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