goyashek/distilbert-darkpattern
0
1"""Gradio app for the fine-tuned DistilBERT model.2 3The app shows the top softmax scores across 14 classes. These scores are not calibrated4confidence. Model weights load from the Hugging Face repository set by ``MODEL_ID``.5"""6 7import os8import json9 10import gradio as gr11import torch12from transformers import AutoTokenizer, AutoModelForSequenceClassification13 14try:15 from .presentation import ABSTAIN_THRESHOLD, BENIGN, result_status16except ImportError: # HF Spaces uploads this directory as the repository root.17 from presentation import ABSTAIN_THRESHOLD, BENIGN, result_status18 19# ``MODEL_ID`` can also point to a local directory for testing.20MODEL_ID = os.environ.get("MODEL_ID", "goyashek/distilbert-darkpattern")21 22# The logits use this alphabetical label-encoder order.23CLASSES = [24 "Bait and Switch", "Basket Sneaking", "Confirm Shaming", "Disguised Advertisement",25 "Drip Pricing", "False Urgency", "Forced Action", "Interface Interference",26 "Nagging", "Not a Dark Pattern", "Rogue Malware", "SaaS Billing",27 "Subscription Trap", "Trick Question",28]29MAX_LEN = 6430 31# Provisional UI band only; it has not been selected as a legal or operational threshold.32PATTERN_GUIDANCE = {33 "False Urgency": ("Language resembling urgency or scarcity.", "Verify the timer, inventory, demand, and offer expiry."),34 "Basket Sneaking": ("Language resembling an added item or charge.", "Compare the cart before and after the user's explicit choices."),35 "Confirm Shaming": ("Language that may shame or guilt a user.", "Review the surrounding choices and whether refusal is neutral."),36 "Forced Action": ("Language suggesting an extra action may be required.", "Check whether the user's task is blocked by an unrelated action."),37 "Subscription Trap": ("Language resembling enrollment or cancellation friction.", "Review the complete signup, renewal, and cancellation flow."),38 "Interface Interference": ("Language associated with possible interface interference.", "Inspect layout, defaults, contrast, prominence, and nearby controls."),39 "Bait and Switch": ("Language resembling a changed offer or outcome.", "Compare the original offer, selected action, and actual result."),40 "Drip Pricing": ("Language resembling late price disclosure.", "Compare every price shown from listing through final payment."),41 "Disguised Advertisement": ("Language resembling promotional content.", "Check sponsorship, placement, and disclosure context."),42 "Nagging": ("Language associated with repeated prompting.", "Observe how often and when the prompt reappears."),43 "Trick Question": ("Language resembling confusing consent wording.", "Review checkbox defaults, nearby wording, and the effect of each choice."),44 "SaaS Billing": ("Language associated with trial or recurring billing.", "Verify renewal terms, consent, reminders, and cancellation steps."),45 "Rogue Malware": ("Language resembling an alarming security prompt.", "Verify the source, device state, requested action, and download target."),46 BENIGN: ("The model found no category-level textual signal in this snippet.", "Review the surrounding interface and full user flow before drawing a conclusion."),47}48DISPLAY_LABELS = {"Trick Question": "Trick Wording (model label: Trick Question)"}49 50# A few representative examples per category for one-click testing (gr.Examples).51EXAMPLES = [52 "Hurry! Only 2 items left at this price! Save 20% off!",53 "A premium shipping protection fee has been added to your cart.",54 "No thanks, I prefer to pay full price and remain unprotected.",55 "You must sign up for our newsletter to complete registration.",56 "To cancel your subscription, please call us during business hours.",57 "Get a leather jacket for ₹99! (At checkout, only a plastic cover is included.)",58 "Convenience fee of ₹52 added at the final payment step.",59 "Sponsored listing",60 "Enable push notifications? (Prompted on every page refresh.)",61 "Uncheck this box if you do not want us to not sell your data.",62 "Free 7-day trial. Your card is then charged ₹499/month automatically.",63 "Warning! Your device is infected. Tap to clean it now.",64 "Your order has been placed. Thank you for shopping with us!",65]66 67 68def _load():69 """Load tokenizer + model, and the label list (prefer the repo's label_map.json)."""70 tok = AutoTokenizer.from_pretrained(MODEL_ID)71 mdl = AutoModelForSequenceClassification.from_pretrained(MODEL_ID)72 mdl.eval()73 74 classes, max_len = CLASSES, MAX_LEN75 # Best effort: use the label map shipped with the weights so this never drifts.76 try:77 from huggingface_hub import hf_hub_download78 with open(hf_hub_download(MODEL_ID, "label_map.json")) as f:79 meta = json.load(f)80 classes = meta.get("classes", CLASSES)81 max_len = meta.get("max_length", MAX_LEN)82 except Exception:83 local = os.path.join(MODEL_ID, "label_map.json")84 if os.path.exists(local):85 with open(local) as f:86 meta = json.load(f)87 classes, max_len = meta.get("classes", CLASSES), meta.get("max_length", MAX_LEN)88 return tok, mdl, classes, max_len89 90 91TOKENIZER, MODEL, LABELS, MAX_LEN = _load()92 93 94def analyze(text):95 """Return (label distribution for gr.Label, verdict markdown)."""96 text = (text or "").strip()97 if not text:98 return {}, "_Enter some UI text above and click Analyze._"99 100 enc = TOKENIZER(text, truncation=True, padding="max_length",101 max_length=MAX_LEN, return_tensors="pt")102 with torch.no_grad():103 logits = MODEL(input_ids=enc["input_ids"],104 attention_mask=enc["attention_mask"]).logits[0]105 probs = torch.softmax(logits, dim=-1).tolist()106 dist = {DISPLAY_LABELS.get(LABELS[i], LABELS[i]): float(probs[i]) for i in range(len(LABELS))}107 108 top = max(range(len(probs)), key=lambda i: probs[i])109 label, conf = LABELS[top], probs[top]110 111 status = result_status(label, conf)112 description, context = PATTERN_GUIDANCE[label]113 display_label = DISPLAY_LABELS.get(label, label)114 115 if status == "inconclusive":116 verdict = (117 "### ⚪ Inconclusive from text alone\n"118 f"Leading model category: **{display_label}** ({conf:.0%} softmax score).\n\n"119 f"This is below the provisional {ABSTAIN_THRESHOLD:.0%} display threshold; "120 "it is not relabeled as benign.\n\n"121 f"**Context needed:** {context}"122 )123 elif status == "no_signal":124 verdict = (125 "### ✅ No textual signal found\n"126 f"**{conf:.0%}** softmax score for the top class.\n\n"127 f"{description}\n\n**Context needed:** {context}"128 )129 else:130 verdict = (131 "### 🔴 Potential textual signal\n"132 f"**{display_label}** — {conf:.0%} softmax score\n\n"133 f"{description}\n\n**Context needed:** {context}"134 )135 verdict += "\n\n*Screening result only; human review is required for any compliance conclusion.*"136 return dist, verdict137 138 139CUSTOM_CSS = """140.gradio-container {max-width: 1040px !important;}141#verdict {min-height: 130px;}142"""143 144with gr.Blocks(title="Dark Pattern Detector: DistilBERT", theme=gr.themes.Soft(),145 css=CUSTOM_CSS) as demo:146 gr.Markdown(147 "# 🔍 Dark Pattern Detector: DistilBERT\n"148 "I fine-tuned this model to screen UI text across the 13 categories named in "149 "India's 2023 CCPA dark-pattern guidelines, plus a no-dark-pattern class. "150 "It is a text classifier, not a compliance check."151 )152 153 with gr.Row():154 with gr.Column(scale=1):155 inp = gr.Textbox(156 label="Suspicious UI text",157 placeholder="Paste a button label, warning, urgency banner, fee line…",158 lines=5,159 )160 btn = gr.Button("🚀 Analyze", variant="primary")161 gr.Examples(EXAMPLES, inputs=inp, label="Or try a sample")162 with gr.Column(scale=1):163 verdict = gr.Markdown(elem_id="verdict")164 dist = gr.Label(num_top_classes=5, label="Model scores across classes")165 166 btn.click(analyze, inputs=inp, outputs=[dist, verdict])167 inp.submit(analyze, inputs=inp, outputs=[dist, verdict])168 169 with gr.Accordion("Why there are no keyword signals", open=False):170 gr.Markdown(171 "The classical model uses 12 hand-built text signals, so its app can list the "172 "signals that fired. DistilBERT does not use those features. I show its top "173 "softmax scores instead, but they are not calibrated confidence. A top score below the provisional "174 f"{int(ABSTAIN_THRESHOLD*100)}% display threshold is shown as *inconclusive*, "175 "never converted to benign.\n\n"176 "*This is a student project. I mapped the dataset labels to the CCPA dark-pattern "177 "categories based on my own reading of the guidelines. The mapping is not official "178 "or approved by the CCPA, and the results should not be used as legal or compliance "179 "advice. Made by [Abhishek Goyal](https://github.com/goyashek).*"180 )181 182if __name__ == "__main__":183 # HF Spaces expects the app on this host and port.184 demo.launch(server_name="0.0.0.0", server_port=7860)185 