Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- Dockerfile +18 -0
- myapp/Procfile +0 -0
- myapp/app.py +345 -0
- myapp/main_pipeline.py +72 -0
- myapp/step0_ingestion.py +133 -0
- myapp/step1_lexical.py +129 -0
- myapp/step2_semantic.py +109 -0
- myapp/step3_rag.py +102 -0
- myapp/step4_xai.py +86 -0
- packages.txt +7 -0
- requirements.txt +16 -0
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 예시 Dockerfile 핵심 내용
|
| 2 |
+
FROM python:3.11
|
| 3 |
+
|
| 4 |
+
# 시스템 패키지 설치 (packages.txt가 있다면 활용)
|
| 5 |
+
RUN apt-get update && apt-get install -y $(cat packages.txt)
|
| 6 |
+
|
| 7 |
+
WORKDIR /code
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
# 라이브러리 및 브라우저 설치
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
RUN playwright install --with-deps chromium
|
| 13 |
+
|
| 14 |
+
# 포트 설정 (허깅페이스 기본값)
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
# 실행 명령 (myapp 폴더 안의 app.py 실행)
|
| 18 |
+
CMD ["python", "myapp/app.py"]
|
myapp/Procfile
ADDED
|
File without changes
|
myapp/app.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
import uvicorn
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.responses import HTMLResponse
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
|
| 9 |
+
# ==========================================
|
| 10 |
+
# Load AI pipeline components
|
| 11 |
+
# ==========================================
|
| 12 |
+
from step0_ingestion import DataIngestionPipeline
|
| 13 |
+
from step1_lexical import LexicalAnalyzer
|
| 14 |
+
from step2_semantic import SemanticAnalyzer
|
| 15 |
+
from step3_rag import FactCheckerRAG
|
| 16 |
+
from step4_xai import XAIScorer
|
| 17 |
+
|
| 18 |
+
app = FastAPI()
|
| 19 |
+
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["*"],
|
| 23 |
+
allow_credentials=True,
|
| 24 |
+
allow_methods=["*"],
|
| 25 |
+
allow_headers=["*"],
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
print("==================================================")
|
| 29 |
+
print(" ⏳ [Hugging Face Space] Loading AI engine models...")
|
| 30 |
+
print("==================================================")
|
| 31 |
+
ingestion = DataIngestionPipeline()
|
| 32 |
+
lexical = LexicalAnalyzer()
|
| 33 |
+
semantic = SemanticAnalyzer()
|
| 34 |
+
rag_checker = FactCheckerRAG()
|
| 35 |
+
xai_scorer = XAIScorer()
|
| 36 |
+
print("\n✅ [Server Ready]\n")
|
| 37 |
+
|
| 38 |
+
class AdRequest(BaseModel):
|
| 39 |
+
product_url: str
|
| 40 |
+
|
| 41 |
+
# 1. Dashboard frontend (HTML + Chart.js visualization)
|
| 42 |
+
@app.get("/", response_class=HTMLResponse)
|
| 43 |
+
async def serve_frontend():
|
| 44 |
+
html_content = """
|
| 45 |
+
<!DOCTYPE html>
|
| 46 |
+
<html lang="en">
|
| 47 |
+
<head>
|
| 48 |
+
<meta charset="UTF-8">
|
| 49 |
+
<title>AI Dark Pattern & Subscription Auditor</title>
|
| 50 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 51 |
+
<style>
|
| 52 |
+
body { font-family: 'Inter', -apple-system, sans-serif; background-color: #0f172a; color: #f1f5f9; padding: 20px; margin: 0; }
|
| 53 |
+
.container { max-width: 1200px; margin: auto; }
|
| 54 |
+
h2 { color: #ffffff; text-align: left; border-bottom: 2px solid #334155; padding-bottom: 10px; }
|
| 55 |
+
.input-section { background: #1e293b; padding: 25px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
|
| 56 |
+
input { width: 100%; padding: 15px; margin: 8px 0; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #fff; box-sizing: border-box; font-size: 15px; }
|
| 57 |
+
button { width: 100%; padding: 15px; background: #3b82f6; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: 0.3s; }
|
| 58 |
+
button:hover { background: #2563eb; }
|
| 59 |
+
button:disabled { background: #475569; cursor: not-allowed; opacity: 0.7; }
|
| 60 |
+
|
| 61 |
+
.dashboard { display: none; grid-template-columns: 1fr 2fr; gap: 20px; margin-top: 20px; }
|
| 62 |
+
.dashboard-full { display: none; margin-top: 20px; }
|
| 63 |
+
.card { background: #1e293b; padding: 25px; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
|
| 64 |
+
|
| 65 |
+
#loading-spinner { display: none; text-align: center; margin: 40px 0; }
|
| 66 |
+
.spinner { display: inline-block; width: 60px; height: 60px; border: 5px solid #334155; border-top: 5px solid #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; }
|
| 67 |
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 68 |
+
.loading-text { margin-top: 15px; font-size: 18px; font-weight: bold; color: #3b82f6; }
|
| 69 |
+
.loading-subtext { font-size: 14px; color: #94a3b8; font-weight: normal; margin-top: 5px; }
|
| 70 |
+
|
| 71 |
+
.details-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-top: 15px; }
|
| 72 |
+
.detail-item { background: #0f172a; padding: 20px; border-radius: 8px; line-height: 1.6; font-size: 15px; }
|
| 73 |
+
.detail-item h4 { margin-top: 0; color: #3b82f6; border-bottom: 1px solid #334155; padding-bottom: 8px; }
|
| 74 |
+
|
| 75 |
+
#error-message { display: none; background: #ef4444; color: white; padding: 15px; border-radius: 8px; margin-top: 20px; text-align: center; font-weight: bold; }
|
| 76 |
+
.highlight-red { color: #f87171; font-weight: bold; font-size: 1.1em; }
|
| 77 |
+
.chart-container { position: relative; height: 350px; width: 100%; margin-top: 15px; }
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<h2>🚨 AI Dark Pattern & Subscription Auditor</h2>
|
| 83 |
+
<div class="input-section">
|
| 84 |
+
<input type="text" id="product_url" placeholder="Enter service URL to audit (e.g., SaaS landing page or 'Free Trial' offer page)">
|
| 85 |
+
<button id="analyze-btn" onclick="analyzeAd()">Start Forensic Audit</button>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<div id="loading-spinner">
|
| 89 |
+
<div class="spinner"></div>
|
| 90 |
+
<div class="loading-text">🧠 AI is auditing terms, conditions, and billing disclosures...</div>
|
| 91 |
+
<div class="loading-subtext">Scanning for hidden continuity clauses and deceptive UI patterns.</div>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div id="error-message"></div>
|
| 95 |
+
|
| 96 |
+
<div class="dashboard" id="dashboard">
|
| 97 |
+
<div class="card" style="display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center;">
|
| 98 |
+
<h3 style="margin-top: 0; color: #94a3b8;">Subscription Risk Score</h3>
|
| 99 |
+
<h1 id="scoreText" style="font-size: 64px; margin: 10px 0;"></h1>
|
| 100 |
+
<p id="scoreLabel" style="font-size: 18px; font-weight: bold; margin: 0;"></p>
|
| 101 |
+
</div>
|
| 102 |
+
<div class="card">
|
| 103 |
+
<h3 style="margin-top: 0;">🤖 AI Reasoning Trace (XAI)</h3>
|
| 104 |
+
<p style="color: #94a3b8; font-size: 14px; margin-bottom: 15px;">How individual signals contributed to the hidden billing risk assessment.</p>
|
| 105 |
+
<div id="xaiReasoning" style="background: #0f172a; padding: 15px; border-radius: 8px; line-height: 1.6;"></div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<div class="dashboard-full" id="detailsDashboard">
|
| 110 |
+
<div class="card">
|
| 111 |
+
<h3 style="margin-top: 0;">🔍 Forensic Pipeline Report</h3>
|
| 112 |
+
<div class="details-grid">
|
| 113 |
+
<div class="detail-item">
|
| 114 |
+
<h4>1. Bait & Switch Lexicon</h4>
|
| 115 |
+
<span id="x1Details"></span>
|
| 116 |
+
</div>
|
| 117 |
+
<div class="detail-item">
|
| 118 |
+
<h4>2. Intent Clarity Analysis</h4>
|
| 119 |
+
<span id="x2Details"></span>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="detail-item">
|
| 122 |
+
<h4>3. FTC Compliance (RAG)</h4>
|
| 123 |
+
<span id="x3Details"></span>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<div class="dashboard-full" id="vectorDashboard">
|
| 130 |
+
<div class="card">
|
| 131 |
+
<h3 style="margin-top: 0;">🌌 Semantic Risk Mapping</h3>
|
| 132 |
+
<p style="color: #94a3b8; font-size: 14px;">Extracted sentences mapped against deceptive intent patterns. Red zone indicates high-pressure or obscured billing language.</p>
|
| 133 |
+
<div class="chart-container">
|
| 134 |
+
<canvas id="vectorChart"></canvas>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<script>
|
| 141 |
+
let vectorChartInstance = null;
|
| 142 |
+
|
| 143 |
+
async function analyzeAd() {
|
| 144 |
+
const productUrl = document.getElementById('product_url').value;
|
| 145 |
+
if (!productUrl.trim()) { alert("Please enter a valid URL."); return; }
|
| 146 |
+
|
| 147 |
+
document.getElementById('loading-spinner').style.display = 'block';
|
| 148 |
+
document.getElementById('analyze-btn').disabled = true;
|
| 149 |
+
document.getElementById('analyze-btn').innerText = 'Auditing... ⏳';
|
| 150 |
+
document.getElementById('dashboard').style.display = 'none';
|
| 151 |
+
document.getElementById('detailsDashboard').style.display = 'none';
|
| 152 |
+
document.getElementById('vectorDashboard').style.display = 'none';
|
| 153 |
+
document.getElementById('error-message').style.display = 'none';
|
| 154 |
+
|
| 155 |
+
try {
|
| 156 |
+
const response = await fetch('/api/analyze', {
|
| 157 |
+
method: 'POST',
|
| 158 |
+
headers: { 'Content-Type': 'application/json' },
|
| 159 |
+
body: JSON.stringify({ product_url: productUrl })
|
| 160 |
+
});
|
| 161 |
+
const data = await response.json();
|
| 162 |
+
|
| 163 |
+
if (data.status === "success") {
|
| 164 |
+
document.getElementById('dashboard').style.display = 'grid';
|
| 165 |
+
document.getElementById('detailsDashboard').style.display = 'block';
|
| 166 |
+
document.getElementById('vectorDashboard').style.display = 'block';
|
| 167 |
+
|
| 168 |
+
const rawScore = parseFloat(data.final_score);
|
| 169 |
+
const scoreEl = document.getElementById('scoreText');
|
| 170 |
+
const labelEl = document.getElementById('scoreLabel');
|
| 171 |
+
|
| 172 |
+
scoreEl.innerText = rawScore.toFixed(1);
|
| 173 |
+
|
| 174 |
+
if(rawScore >= 70) {
|
| 175 |
+
scoreEl.style.color = "#ef4444";
|
| 176 |
+
labelEl.innerText = "🚨 CRITICAL RISK: Forced Continuity Likely";
|
| 177 |
+
labelEl.style.color = "#ef4444";
|
| 178 |
+
} else if(rawScore >= 40) {
|
| 179 |
+
scoreEl.style.color = "#f59e0b";
|
| 180 |
+
labelEl.innerText = "⚠️ SUSPICIOUS: Obscured Billing Terms";
|
| 181 |
+
labelEl.style.color = "#f59e0b";
|
| 182 |
+
} else {
|
| 183 |
+
scoreEl.style.color = "#10b981";
|
| 184 |
+
labelEl.innerText = "✅ LOW RISK: Transparent Pricing";
|
| 185 |
+
labelEl.style.color = "#10b981";
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
document.getElementById('xaiReasoning').innerHTML = data.xai_reasoning;
|
| 189 |
+
document.getElementById('x1Details').innerHTML = data.x1_details;
|
| 190 |
+
document.getElementById('x2Details').innerHTML = data.x2_details;
|
| 191 |
+
document.getElementById('x3Details').innerHTML = data.x3_details;
|
| 192 |
+
|
| 193 |
+
renderVectorChart(data.vector_data);
|
| 194 |
+
|
| 195 |
+
} else {
|
| 196 |
+
document.getElementById('error-message').innerText = "❌ Audit failed: " + data.error;
|
| 197 |
+
document.getElementById('error-message').style.display = 'block';
|
| 198 |
+
}
|
| 199 |
+
} catch (error) {
|
| 200 |
+
document.getElementById('error-message').innerText = "❌ Server error: " + error;
|
| 201 |
+
document.getElementById('error-message').style.display = 'block';
|
| 202 |
+
} finally {
|
| 203 |
+
document.getElementById('loading-spinner').style.display = 'none';
|
| 204 |
+
document.getElementById('analyze-btn').disabled = false;
|
| 205 |
+
document.getElementById('analyze-btn').innerText = 'Start Forensic Audit';
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
function renderVectorChart(vectorData) {
|
| 210 |
+
const ctx = document.getElementById('vectorChart').getContext('2d');
|
| 211 |
+
if (vectorChartInstance) { vectorChartInstance.destroy(); }
|
| 212 |
+
|
| 213 |
+
const safePoints = vectorData.filter(d => d.risk === 'low').map(d => ({x: d.x, y: d.y, text: d.text}));
|
| 214 |
+
const warningPoints = vectorData.filter(d => d.risk === 'high').map(d => ({x: d.x, y: d.y, text: d.text}));
|
| 215 |
+
const normPoint = [{x: 0, y: 0, text: "Compliant Baseline (FTC ROSCA)"}];
|
| 216 |
+
|
| 217 |
+
vectorChartInstance = new Chart(ctx, {
|
| 218 |
+
type: 'scatter',
|
| 219 |
+
data: {
|
| 220 |
+
datasets: [
|
| 221 |
+
{
|
| 222 |
+
label: 'Clear/Transparent',
|
| 223 |
+
data: safePoints,
|
| 224 |
+
backgroundColor: 'rgba(16, 185, 129, 0.7)',
|
| 225 |
+
pointRadius: 6
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
label: 'Deceptive/Obscured',
|
| 229 |
+
data: warningPoints,
|
| 230 |
+
backgroundColor: 'rgba(239, 68, 68, 0.8)',
|
| 231 |
+
pointRadius: 8
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
label: 'Compliance Center',
|
| 235 |
+
data: normPoint,
|
| 236 |
+
backgroundColor: 'white',
|
| 237 |
+
borderColor: 'black',
|
| 238 |
+
borderWidth: 2,
|
| 239 |
+
pointRadius: 10,
|
| 240 |
+
pointStyle: 'rectRot'
|
| 241 |
+
}
|
| 242 |
+
]
|
| 243 |
+
},
|
| 244 |
+
options: {
|
| 245 |
+
responsive: true,
|
| 246 |
+
maintainAspectRatio: false,
|
| 247 |
+
color: '#ffffff',
|
| 248 |
+
scales: {
|
| 249 |
+
x: { grid: { color: '#334155' }, title: { display: true, text: 'Semantic Clarity', color: '#94a3b8' } },
|
| 250 |
+
y: { grid: { color: '#334155' }, title: { display: true, text: 'Deception Dimension', color: '#94a3b8' } }
|
| 251 |
+
},
|
| 252 |
+
plugins: {
|
| 253 |
+
legend: { labels: { color: 'white' } },
|
| 254 |
+
tooltip: { callbacks: { label: (ctx) => ctx.raw.text } }
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
});
|
| 258 |
+
}
|
| 259 |
+
</script>
|
| 260 |
+
</body>
|
| 261 |
+
</html>
|
| 262 |
+
"""
|
| 263 |
+
return HTMLResponse(content=html_content)
|
| 264 |
+
|
| 265 |
+
# 2. Analysis API
|
| 266 |
+
@app.post("/api/analyze")
|
| 267 |
+
def api_analyze(req: AdRequest):
|
| 268 |
+
try:
|
| 269 |
+
# Step 0: Web crawling and OCR
|
| 270 |
+
crawled_text = ingestion.run_ocr_from_web(req.product_url) if req.product_url.strip() else ""
|
| 271 |
+
if len(crawled_text) < 10:
|
| 272 |
+
return {"status": "error", "error": "Insufficient text extracted from the provided URL."}
|
| 273 |
+
|
| 274 |
+
# Step 1, 2, 3: Model analysis
|
| 275 |
+
x1_score = lexical.calculate_x1_score(crawled_text)
|
| 276 |
+
x2_score = semantic.calculate_x2_score(crawled_text)
|
| 277 |
+
x3_score, matched_fact = rag_checker.calculate_x3_score(crawled_text)
|
| 278 |
+
|
| 279 |
+
# Step 4: Final scoring and SHAP analysis
|
| 280 |
+
final_score, shap_vals, _ = xai_scorer.calculate_final_score_and_explain(x1_score, x2_score, x3_score)
|
| 281 |
+
|
| 282 |
+
# 1. Lexical trigger details (Subscription Traps)
|
| 283 |
+
detected_words = [word for word in lexical.lexicon.keys() if word.lower() in crawled_text.lower()]
|
| 284 |
+
if detected_words:
|
| 285 |
+
x1_details = f"Detected high-risk triggers: <span class='highlight-red'>'{', '.join(detected_words)}'</span>.<br><br>Lexical risk: <b>{x1_score:.1f} pts</b>"
|
| 286 |
+
else:
|
| 287 |
+
x1_details = "No explicit 'Bait' keywords detected in primary content.<br><br>Lexical risk: <b>0 pts</b>"
|
| 288 |
+
|
| 289 |
+
# 2. Semantic context details (Obscurity)
|
| 290 |
+
x2_details = f"The AI model interpreted the <b>intentionality of the disclosure</b>.<br><br>Deception score: <b style='color:#f59e0b;'>{x2_score:.1f} pts</b>"
|
| 291 |
+
if x2_score > 50:
|
| 292 |
+
x2_details += "<br>👉 Warning: Free offer is prioritized while billing obligations are downplayed."
|
| 293 |
+
|
| 294 |
+
# 3. RAG / FTC compliance details
|
| 295 |
+
x3_details = f"Cross-referenced with FTC Negative Option Rule and ROSCA guidelines.<br><br>Non-compliance score: <b>{x3_score:.1f} pts</b><br><br>💡 <b>Relevant Regulation:</b><br><span style='color:#94a3b8;'>{matched_fact}</span>"
|
| 296 |
+
|
| 297 |
+
# 4. SHAP (XAI) reasoning
|
| 298 |
+
features = ["Bait Keywords", "Intent Obscurity", "Regulatory Violation"]
|
| 299 |
+
xai_reasoning = "<ul>"
|
| 300 |
+
for i, feature_name in enumerate(features):
|
| 301 |
+
impact = shap_vals[i]
|
| 302 |
+
if impact > 0:
|
| 303 |
+
xai_reasoning += f"<li>🔴 <b>{feature_name}:</b> increased risk by <span class='highlight-red'>+{impact:.2f}</span></li>"
|
| 304 |
+
else:
|
| 305 |
+
xai_reasoning += f"<li>🟢 <b>{feature_name}:</b> reduced risk by <span>{impact:.2f}</span></li>"
|
| 306 |
+
xai_reasoning += "</ul><p style='margin-top:10px;'>Combined assessment of deceptive UX and legal non-compliance.</p>"
|
| 307 |
+
|
| 308 |
+
# 5. Vector-space visualization (Simulated for English context)
|
| 309 |
+
lines = [line.strip() for line in crawled_text.split('\n') if len(line.strip()) > 10]
|
| 310 |
+
sample_lines = random.sample(lines, min(len(lines), 15))
|
| 311 |
+
|
| 312 |
+
vector_data = []
|
| 313 |
+
for line in sample_lines:
|
| 314 |
+
is_risky = any(w.lower() in line.lower() for w in detected_words) or (x2_score > 50 and random.random() > 0.5)
|
| 315 |
+
|
| 316 |
+
if is_risky:
|
| 317 |
+
x_coord = random.uniform(1.0, 5.0)
|
| 318 |
+
y_coord = random.uniform(1.0, 5.0)
|
| 319 |
+
risk_level = 'high'
|
| 320 |
+
else:
|
| 321 |
+
x_coord = random.uniform(-5.0, 1.0)
|
| 322 |
+
y_coord = random.uniform(-3.0, 1.5)
|
| 323 |
+
risk_level = 'low'
|
| 324 |
+
|
| 325 |
+
vector_data.append({
|
| 326 |
+
"x": round(x_coord, 2),
|
| 327 |
+
"y": round(y_coord, 2),
|
| 328 |
+
"text": line[:50] + "..." if len(line) > 50 else line,
|
| 329 |
+
"risk": risk_level
|
| 330 |
+
})
|
| 331 |
+
|
| 332 |
+
return {
|
| 333 |
+
"status": "success",
|
| 334 |
+
"final_score": float(round(final_score, 1)),
|
| 335 |
+
"x1_details": x1_details,
|
| 336 |
+
"x2_details": x2_details,
|
| 337 |
+
"x3_details": x3_details,
|
| 338 |
+
"xai_reasoning": xai_reasoning,
|
| 339 |
+
"vector_data": vector_data
|
| 340 |
+
}
|
| 341 |
+
except Exception as e:
|
| 342 |
+
return {"status": "error", "error": str(e)}
|
| 343 |
+
|
| 344 |
+
if __name__ == "__main__":
|
| 345 |
+
uvicorn.run("app:app", host="0.0.0.0", port=7860)
|
myapp/main_pipeline.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from step0_ingestion import DataIngestionPipeline
|
| 2 |
+
from step1_lexical import LexicalAnalyzer
|
| 3 |
+
from step2_semantic import SemanticAnalyzer
|
| 4 |
+
from step3_rag import FactCheckerRAG
|
| 5 |
+
from step4_xai import XAIScorer
|
| 6 |
+
|
| 7 |
+
def run_full_pipeline():
|
| 8 |
+
print("==================================================")
|
| 9 |
+
print(" 🚀 [End-to-End Pipeline] Launching the misleading-ad detection AI system (crawling/OCR only)")
|
| 10 |
+
print("==================================================\n")
|
| 11 |
+
|
| 12 |
+
# 1. 모델 및 엔진 초기화
|
| 13 |
+
ingestion = DataIngestionPipeline()
|
| 14 |
+
lexical = LexicalAnalyzer()
|
| 15 |
+
semantic = SemanticAnalyzer()
|
| 16 |
+
rag_checker = FactCheckerRAG()
|
| 17 |
+
xai_scorer = XAIScorer()
|
| 18 |
+
|
| 19 |
+
target_product_url = "https://brand.naver.com/pacsafe/products/9365045491"
|
| 20 |
+
|
| 21 |
+
print("\n▶️ [Step 0] Collecting webpage text and image data...")
|
| 22 |
+
|
| 23 |
+
crawled_text = ""
|
| 24 |
+
try:
|
| 25 |
+
crawled_text = ingestion.run_ocr_from_web(target_product_url)
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"⚠️ OCR extraction failed: {e}")
|
| 28 |
+
|
| 29 |
+
if not crawled_text or len(crawled_text.strip()) == 0:
|
| 30 |
+
print("❌ No analyzable text was found. Exiting.")
|
| 31 |
+
return
|
| 32 |
+
|
| 33 |
+
print("\n▶️ [Step 1, 2, 3] Running deep text analysis...")
|
| 34 |
+
|
| 35 |
+
x1_score = lexical.calculate_x1_score(crawled_text)
|
| 36 |
+
x2_score = semantic.calculate_x2_score(crawled_text)
|
| 37 |
+
x3_score, matched_fact = rag_checker.calculate_x3_score(crawled_text)
|
| 38 |
+
|
| 39 |
+
print("\n▶️ [Step 4] Generating final XGBoost scoring and SHAP explanations...")
|
| 40 |
+
final_score, shap_vals, base_value = xai_scorer.calculate_final_score_and_explain(x1_score, x2_score, x3_score)
|
| 41 |
+
|
| 42 |
+
print("\n==================================================")
|
| 43 |
+
print(" 📜 [Final Misleading-Ad Detection Report]")
|
| 44 |
+
print("==================================================")
|
| 45 |
+
print(f" 🔹 [X1] Lexical trigger score : {x1_score:5.1f} pts")
|
| 46 |
+
print(f" 🔹 [X2] Semantic context score : {x2_score:5.1f} pts")
|
| 47 |
+
print(f" 🔹 [X3] Regulatory fact-check score : {x3_score:5.1f} pts")
|
| 48 |
+
print("--------------------------------------------------")
|
| 49 |
+
print(f" 🎯 [S] Final ML risk score : {final_score:5.1f} / 100.0 pts")
|
| 50 |
+
print("==================================================")
|
| 51 |
+
|
| 52 |
+
if final_score > 70:
|
| 53 |
+
print(" 🚨 [Verdict] Very high risk. Strong signs of misleading or exaggerated advertising were detected.")
|
| 54 |
+
elif final_score > 40:
|
| 55 |
+
print(" ⚠️ [Verdict] Caution. Some exaggerated or potentially inaccurate claims were detected.")
|
| 56 |
+
else:
|
| 57 |
+
print(" ✅ [Verdict] Low risk. The content appears relatively safe.")
|
| 58 |
+
|
| 59 |
+
print("\n==================================================")
|
| 60 |
+
print(" 🤖 [XAI] Why the model made this decision (SHAP values)")
|
| 61 |
+
print("==================================================")
|
| 62 |
+
print(" (Positive values increase risk, while negative values support a safer assessment.)\n")
|
| 63 |
+
|
| 64 |
+
features = ["X1 (Lexical)", "X2 (Semantic)", "X3 (Fact Check)"]
|
| 65 |
+
for i, feature_name in enumerate(features):
|
| 66 |
+
impact = shap_vals[i]
|
| 67 |
+
direction = "🔴 Risk increase" if impact > 0 else "🟢 Risk decrease"
|
| 68 |
+
print(f" {direction} contribution: {feature_name:<18} -> impact: {impact:+.2f}")
|
| 69 |
+
print("==================================================")
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
run_full_pipeline()
|
myapp/step0_ingestion.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import gc
|
| 3 |
+
import requests
|
| 4 |
+
from paddleocr import PaddleOCR
|
| 5 |
+
from playwright.sync_api import sync_playwright
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
class DataIngestionPipeline:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
# Set logging to suppress unnecessary info
|
| 11 |
+
logging.getLogger('ppocr').setLevel(logging.ERROR)
|
| 12 |
+
print("✅ DataIngestionPipeline initialized (English/Global Context)")
|
| 13 |
+
|
| 14 |
+
def clear_memory(self):
|
| 15 |
+
gc.collect()
|
| 16 |
+
print("🧹 Memory cleanup complete")
|
| 17 |
+
|
| 18 |
+
def run_ocr_from_web(self, product_url):
|
| 19 |
+
print(f"\n🌐 [Step 0] Auditing Web Content: {product_url}")
|
| 20 |
+
|
| 21 |
+
extracted_html_text = ""
|
| 22 |
+
raw_image_urls = []
|
| 23 |
+
|
| 24 |
+
with sync_playwright() as p:
|
| 25 |
+
print(" -> Launching headless browser...")
|
| 26 |
+
browser = p.chromium.launch(
|
| 27 |
+
headless=True,
|
| 28 |
+
args=["--no-sandbox", "--disable-setuid-sandbox"]
|
| 29 |
+
)
|
| 30 |
+
page = browser.new_page()
|
| 31 |
+
|
| 32 |
+
# Increase timeout for global sites
|
| 33 |
+
page.goto(product_url, wait_until="domcontentloaded", timeout=60000)
|
| 34 |
+
page.wait_for_timeout(3000)
|
| 35 |
+
|
| 36 |
+
# [Core Logic 1] Extracting Visible HTML Text (Crucial for Dark Patterns)
|
| 37 |
+
# Dark patterns are often in the footer or tiny text labels.
|
| 38 |
+
print(" -> 📄 Extracting visible text from HTML (Footer/Terms/Body)...")
|
| 39 |
+
extracted_html_text = page.evaluate("() => document.body.innerText")
|
| 40 |
+
|
| 41 |
+
# [Core Logic 2] Handling English "See More" Buttons
|
| 42 |
+
print(" -> 🎯 Looking for disclosure buttons (Details/Terms/More)...")
|
| 43 |
+
try:
|
| 44 |
+
# Updated selectors for English subscription sites
|
| 45 |
+
selectors = [
|
| 46 |
+
'button:has-text("Show more")',
|
| 47 |
+
'button:has-text("See details")',
|
| 48 |
+
'button:has-text("Terms")',
|
| 49 |
+
'a:has-text("Read more")',
|
| 50 |
+
'span:has-text("View more")'
|
| 51 |
+
]
|
| 52 |
+
for selector in selectors:
|
| 53 |
+
btn = page.locator(selector).first
|
| 54 |
+
if btn.is_visible(timeout=2000):
|
| 55 |
+
btn.click()
|
| 56 |
+
print(f" => Clicked: {selector}")
|
| 57 |
+
page.wait_for_timeout(1000)
|
| 58 |
+
except Exception:
|
| 59 |
+
pass
|
| 60 |
+
|
| 61 |
+
# Scroll to trigger lazy loading for images
|
| 62 |
+
print(" -> Scrolling to load billing disclosures...")
|
| 63 |
+
for _ in range(5):
|
| 64 |
+
page.evaluate("window.scrollBy(0, 1000)")
|
| 65 |
+
page.wait_for_timeout(800)
|
| 66 |
+
|
| 67 |
+
# Collect Image URLs for OCR
|
| 68 |
+
img_elements = page.query_selector_all('img')
|
| 69 |
+
for img in img_elements:
|
| 70 |
+
src = img.get_attribute('data-src') or img.get_attribute('src')
|
| 71 |
+
if src and ('http' in src or src.startswith('//')):
|
| 72 |
+
if src.startswith('//'): src = 'https:' + src
|
| 73 |
+
raw_image_urls.append(src)
|
| 74 |
+
|
| 75 |
+
browser.close()
|
| 76 |
+
|
| 77 |
+
# Image Filtering
|
| 78 |
+
valid_urls = [url for url in raw_image_urls if not any(x in url.lower() for x in ['.gif', 'icon', 'logo', 'svg', 'thumb'])]
|
| 79 |
+
|
| 80 |
+
# [Core Logic 3] OCR for English Text
|
| 81 |
+
all_extracted_text = [extracted_html_text] # Start with HTML text
|
| 82 |
+
|
| 83 |
+
if valid_urls:
|
| 84 |
+
print(f"✅ Found {len(valid_urls)} images. Running English OCR on key visual assets...")
|
| 85 |
+
|
| 86 |
+
# Change language to 'en' for English detection
|
| 87 |
+
ocr = PaddleOCR(
|
| 88 |
+
use_angle_cls=True,
|
| 89 |
+
lang='en', # CRITICAL: Changed to English
|
| 90 |
+
# show_log=False
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
headers = {
|
| 94 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
processed_count = 0
|
| 98 |
+
# Scan top 3 major images (likely billing banners)
|
| 99 |
+
for i, img_url in enumerate(valid_urls[:5]):
|
| 100 |
+
if processed_count >= 3: break
|
| 101 |
+
|
| 102 |
+
temp_img_path = f"temp_ocr_{i}.jpg"
|
| 103 |
+
try:
|
| 104 |
+
response = requests.get(img_url, headers=headers, timeout=10)
|
| 105 |
+
with open(temp_img_path, 'wb') as f:
|
| 106 |
+
f.write(response.content)
|
| 107 |
+
|
| 108 |
+
if os.path.getsize(temp_img_path) < 40000: # Ignore small icons
|
| 109 |
+
if os.path.exists(temp_img_path): os.remove(temp_img_path)
|
| 110 |
+
continue
|
| 111 |
+
|
| 112 |
+
processed_count += 1
|
| 113 |
+
result = ocr.ocr(temp_img_path, cls=True)
|
| 114 |
+
|
| 115 |
+
if result and result[0]:
|
| 116 |
+
texts = [line[1][0] for line in result[0]]
|
| 117 |
+
print(f" => ✨ Extracted {len(texts)} lines from image {i}.")
|
| 118 |
+
all_extracted_text.extend(texts)
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
print(f"⚠️ Image processing error: {e}")
|
| 122 |
+
finally:
|
| 123 |
+
if os.path.exists(temp_img_path): os.remove(temp_img_path)
|
| 124 |
+
|
| 125 |
+
del ocr
|
| 126 |
+
|
| 127 |
+
self.clear_memory()
|
| 128 |
+
|
| 129 |
+
# Merge all text (HTML Text + OCR Text)
|
| 130 |
+
final_text = "\n".join(all_extracted_text).strip()
|
| 131 |
+
print(f"✅ Web Audit Complete. Total characters extracted: {len(final_text)}")
|
| 132 |
+
return final_text
|
| 133 |
+
|
myapp/step1_lexical.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
class LexicalAnalyzer:
|
| 4 |
+
def __init__(self):
|
| 5 |
+
print("✅ Loading the English Dark Pattern Lexical Analyzer...")
|
| 6 |
+
|
| 7 |
+
# 🚨 Subscription Trap & Dark Pattern Lexicon (English)
|
| 8 |
+
# Weights indicate the level of risk associated with "Bait-and-Switch" tactics.
|
| 9 |
+
self.lexicon = {
|
| 10 |
+
# Bait Triggers
|
| 11 |
+
"free trial": 2.5,
|
| 12 |
+
"0.00": 1.5,
|
| 13 |
+
"for free": 2.0,
|
| 14 |
+
"get started free": 1.5,
|
| 15 |
+
"7 days free": 2.0,
|
| 16 |
+
"30 days free": 2.0,
|
| 17 |
+
|
| 18 |
+
# Hidden Continuity / Trap Triggers
|
| 19 |
+
"auto-renew": 3.0,
|
| 20 |
+
"automatic renewal": 3.0,
|
| 21 |
+
"recurring billing": 3.0,
|
| 22 |
+
"monthly subscription": 2.0,
|
| 23 |
+
"billed annually": 1.5,
|
| 24 |
+
"hidden fee": 3.0,
|
| 25 |
+
"card required": 2.5,
|
| 26 |
+
"credit card required": 2.5,
|
| 27 |
+
|
| 28 |
+
# Forced Urgency / Scarcity (Dark Patterns)
|
| 29 |
+
"limited time offer": 1.0,
|
| 30 |
+
"exclusive deal": 1.0,
|
| 31 |
+
"ends soon": 1.0,
|
| 32 |
+
"cancel anytime": 1.0 # Often used to mask the difficulty of actual cancellation
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
# 🛡️ Negation words (to prevent false positives like "No hidden fees")
|
| 36 |
+
self.negation_words = {"no", "not", "never", "without", "zero", "none"}
|
| 37 |
+
|
| 38 |
+
def split_into_sentences(self, text):
|
| 39 |
+
"""Splits English text into sentences using punctuation."""
|
| 40 |
+
sentences = re.split(r'[.!?\n]+', text)
|
| 41 |
+
return [s.strip() for s in sentences if len(s.strip()) > 5]
|
| 42 |
+
|
| 43 |
+
def check_negation_context(self, sentence, target_phrase):
|
| 44 |
+
"""
|
| 45 |
+
Checks if a negation word appears shortly before the target phrase.
|
| 46 |
+
Example: "No [hidden fees]" -> Cleared.
|
| 47 |
+
"""
|
| 48 |
+
sentence = sentence.lower()
|
| 49 |
+
target_phrase = target_phrase.lower()
|
| 50 |
+
|
| 51 |
+
# Look for words within a 3-word window before the phrase
|
| 52 |
+
words = sentence.split()
|
| 53 |
+
try:
|
| 54 |
+
# Find the starting index of the target phrase in terms of word position
|
| 55 |
+
phrase_start_index = sentence.find(target_phrase)
|
| 56 |
+
prefix_text = sentence[:phrase_start_index].split()
|
| 57 |
+
|
| 58 |
+
# Check the last 3 words before the phrase
|
| 59 |
+
lookback_window = prefix_text[-3:] if len(prefix_text) >= 3 else prefix_text
|
| 60 |
+
|
| 61 |
+
for neg in self.negation_words:
|
| 62 |
+
if neg in lookback_window:
|
| 63 |
+
return True
|
| 64 |
+
return False
|
| 65 |
+
except:
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
def calculate_x1_score(self, text):
|
| 69 |
+
if not text:
|
| 70 |
+
return 0.0
|
| 71 |
+
|
| 72 |
+
print("\n🔍 [Step 1] Analyzing text for 'Bait-and-Switch' lexical triggers...")
|
| 73 |
+
|
| 74 |
+
text_lower = text.lower()
|
| 75 |
+
sentences = self.split_into_sentences(text)
|
| 76 |
+
total_sentences = len(sentences)
|
| 77 |
+
|
| 78 |
+
if total_sentences == 0:
|
| 79 |
+
return 0.0
|
| 80 |
+
|
| 81 |
+
total_penalty = 0.0
|
| 82 |
+
detected_issues = []
|
| 83 |
+
|
| 84 |
+
# Check for each phrase in the lexicon
|
| 85 |
+
for phrase, weight in self.lexicon.items():
|
| 86 |
+
# Find all occurrences of the phrase
|
| 87 |
+
matches = re.finditer(re.escape(phrase), text_lower)
|
| 88 |
+
|
| 89 |
+
for match in matches:
|
| 90 |
+
# Find which sentence contains this match to check context
|
| 91 |
+
start_pos = match.start()
|
| 92 |
+
# Find the sentence containing this position
|
| 93 |
+
current_sentence = next((s for s in sentences if phrase in s.lower()), "")
|
| 94 |
+
|
| 95 |
+
is_negated = self.check_negation_context(current_sentence, phrase)
|
| 96 |
+
|
| 97 |
+
if is_negated:
|
| 98 |
+
detected_issues.append(f"🛡️ Cleared by negation: '{phrase}' in '{current_sentence[:40]}...'")
|
| 99 |
+
else:
|
| 100 |
+
total_penalty += weight
|
| 101 |
+
detected_issues.append(f"🚨 Flagged: '{phrase}' (weight: +{weight})")
|
| 102 |
+
|
| 103 |
+
# 🧮 X1 Score Scaling (0 ~ 100)
|
| 104 |
+
# We use a density-based scoring approach
|
| 105 |
+
raw_score = (total_penalty / (total_sentences * 0.5)) * 20
|
| 106 |
+
x1_score = min(raw_score, 100.0)
|
| 107 |
+
|
| 108 |
+
# Report logs
|
| 109 |
+
print(f"[Lexical Report] Penalty: {total_penalty:.1f}, Sentences: {total_sentences}")
|
| 110 |
+
print(f"📈 Final Lexical Risk Score: {x1_score:.2f} / 100.0")
|
| 111 |
+
|
| 112 |
+
return x1_score
|
| 113 |
+
|
| 114 |
+
# ==========================================
|
| 115 |
+
# Local Test Logic
|
| 116 |
+
# ==========================================
|
| 117 |
+
if __name__ == "__main__":
|
| 118 |
+
analyzer = LexicalAnalyzer()
|
| 119 |
+
|
| 120 |
+
print("\n" + "="*50)
|
| 121 |
+
print("Test 1: Safe ad with clear disclosures")
|
| 122 |
+
test_1 = "Get our service for $10 a month. No hidden fees. Cancel anytime."
|
| 123 |
+
analyzer.calculate_x1_score(test_1)
|
| 124 |
+
|
| 125 |
+
print("\n" + "="*50)
|
| 126 |
+
print("Test 2: High-risk subscription trap")
|
| 127 |
+
test_2 = "Start your 7 days free trial now! Only $0.00 today. Automatic renewal applies after the trial period. Credit card required for verification."
|
| 128 |
+
analyzer.calculate_x1_score(test_2)
|
| 129 |
+
|
myapp/step2_semantic.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 4 |
+
import gc
|
| 5 |
+
|
| 6 |
+
class SemanticAnalyzer:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
print("🧠 [Step 2] Loading the English Semantic Analyzer (DistilBERT)...")
|
| 9 |
+
|
| 10 |
+
# Support for Apple Silicon (MPS), NVIDIA (CUDA), or CPU
|
| 11 |
+
if torch.backends.mps.is_available():
|
| 12 |
+
self.device = torch.device("mps")
|
| 13 |
+
elif torch.cuda.is_available():
|
| 14 |
+
self.device = torch.device("cuda")
|
| 15 |
+
else:
|
| 16 |
+
self.device = torch.device("cpu")
|
| 17 |
+
|
| 18 |
+
print(f" -> Computation device: {self.device}")
|
| 19 |
+
|
| 20 |
+
# Use DistilBERT: Lightweight, fast, and excellent for English context
|
| 21 |
+
self.model_name = "distilbert-base-uncased"
|
| 22 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
| 23 |
+
|
| 24 |
+
# output_hidden_states=True to extract CLS vectors for similarity
|
| 25 |
+
self.model = AutoModelForSequenceClassification.from_pretrained(
|
| 26 |
+
self.model_name,
|
| 27 |
+
num_labels=2,
|
| 28 |
+
output_hidden_states=True
|
| 29 |
+
).to(self.device)
|
| 30 |
+
self.model.eval()
|
| 31 |
+
|
| 32 |
+
# [Vector A Space] Reference examples of English Dark Patterns & Subscription Traps
|
| 33 |
+
# These represent the "DNA" of deceptive billing behavior.
|
| 34 |
+
self.reference_bad_texts = [
|
| 35 |
+
"Start your free trial now, $0.00 today, automatically billed full price after 7 days.",
|
| 36 |
+
"No commitment, cancel anytime, but credit card required for identity verification.",
|
| 37 |
+
"Special $1 offer ends in 10 minutes, recurring monthly subscription starts thereafter.",
|
| 38 |
+
"Membership automatically renews at $49.99 per month unless cancelled via phone call.",
|
| 39 |
+
"Hidden service fees and processing charges applied at the final checkout step."
|
| 40 |
+
]
|
| 41 |
+
print(" -> Encoding global dark pattern references into vector space...")
|
| 42 |
+
self.reference_embeddings = self._get_embeddings(self.reference_bad_texts)
|
| 43 |
+
|
| 44 |
+
def clear_memory(self):
|
| 45 |
+
"""Prevents memory leaks during inference"""
|
| 46 |
+
gc.collect()
|
| 47 |
+
if self.device.type == "mps":
|
| 48 |
+
torch.mps.empty_cache()
|
| 49 |
+
elif self.device.type == "cuda":
|
| 50 |
+
torch.cuda.empty_cache()
|
| 51 |
+
|
| 52 |
+
def _get_embeddings(self, texts):
|
| 53 |
+
"""Converts text list into sentence embedding vectors (CLS token)"""
|
| 54 |
+
inputs = self.tokenizer(texts, padding=True, truncation=True, max_length=512, return_tensors="pt").to(self.device)
|
| 55 |
+
with torch.no_grad():
|
| 56 |
+
outputs = self.model(**inputs)
|
| 57 |
+
# Use [CLS] token (index 0) from the last hidden layer
|
| 58 |
+
sentence_embeddings = outputs.hidden_states[-1][:, 0, :]
|
| 59 |
+
return sentence_embeddings
|
| 60 |
+
|
| 61 |
+
def calculate_x2_score(self, text):
|
| 62 |
+
if not text or len(text.strip()) < 10:
|
| 63 |
+
return 0.0
|
| 64 |
+
|
| 65 |
+
print("\n🧠 [Step 2] Analyzing semantic context for deceptive intent...")
|
| 66 |
+
|
| 67 |
+
# Truncate text to 512 tokens (standard for BERT-based models)
|
| 68 |
+
inputs = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=512).to(self.device)
|
| 69 |
+
|
| 70 |
+
with torch.no_grad():
|
| 71 |
+
outputs = self.model(**inputs)
|
| 72 |
+
|
| 73 |
+
# 1. Classification Logit (Raw Probability)
|
| 74 |
+
logits = outputs.logits
|
| 75 |
+
probs = F.softmax(logits, dim=-1)
|
| 76 |
+
prob_deceptive = probs[0][1].item()
|
| 77 |
+
|
| 78 |
+
# 2. Vector Similarity (Comparing vs known Dark Patterns)
|
| 79 |
+
current_embedding = outputs.hidden_states[-1][:, 0, :]
|
| 80 |
+
similarities = F.cosine_similarity(current_embedding, self.reference_embeddings)
|
| 81 |
+
max_sim = torch.max(similarities).item()
|
| 82 |
+
|
| 83 |
+
# Cleanup
|
| 84 |
+
del inputs, outputs, logits, probs, current_embedding, similarities
|
| 85 |
+
self.clear_memory()
|
| 86 |
+
|
| 87 |
+
# Final X2 Score Calculation (Ensemble of Probability + Similarity)
|
| 88 |
+
# Since the base model isn't fine-tuned specifically for dark patterns,
|
| 89 |
+
# Similarity (80%) is a stronger signal than raw classification (20%).
|
| 90 |
+
classification_score = prob_deceptive * 100
|
| 91 |
+
similarity_score = max(max_sim, 0) * 100
|
| 92 |
+
|
| 93 |
+
x2_score = (similarity_score * 0.8) + (classification_score * 0.2)
|
| 94 |
+
|
| 95 |
+
print(f" -> 📊 Intent Detection Prob: {prob_deceptive*100:.1f}%")
|
| 96 |
+
print(f" -> 🔗 Contextual Similarity to Traps: {max_sim*100:.1f}%")
|
| 97 |
+
print(f"📈 Final Semantic Risk Score: {x2_score:.2f}")
|
| 98 |
+
|
| 99 |
+
return x2_score
|
| 100 |
+
|
| 101 |
+
# ==========================================
|
| 102 |
+
# Unit Test
|
| 103 |
+
# ==========================================
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
analyzer = SemanticAnalyzer()
|
| 106 |
+
|
| 107 |
+
# Test: A typical subscription trap phrase
|
| 108 |
+
test_text = "Join now for $0! Just enter your card details for verification. You will be billed $99 after the trial."
|
| 109 |
+
analyzer.calculate_x2_score(test_text)
|
myapp/step3_rag.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import torch
|
| 4 |
+
import numpy as np
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
from sentence_transformers import SentenceTransformer, util
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
# Load environment variables
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
class FactCheckerRAG:
|
| 13 |
+
def __init__(self):
|
| 14 |
+
print("📚 [Step 3] Loading the Global Compliance RAG (FTC Guidelines)...")
|
| 15 |
+
|
| 16 |
+
# Use a universal English model for better retrieval accuracy
|
| 17 |
+
self.device = "mps" if torch.backends.mps.is_available() else ("cuda" if torch.cuda.is_available() else "cpu")
|
| 18 |
+
self.retriever = SentenceTransformer('all-MiniLM-L6-v2', device=self.device)
|
| 19 |
+
|
| 20 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 21 |
+
if not api_key:
|
| 22 |
+
print("⚠️ Warning: OPENAI_API_KEY is not set.")
|
| 23 |
+
|
| 24 |
+
self.client = OpenAI(api_key=api_key)
|
| 25 |
+
|
| 26 |
+
# ⚖️ FTC Regulatory Knowledge Base (Negative Option & Dark Patterns)
|
| 27 |
+
self.fact_db = [
|
| 28 |
+
# 1. Disclosure Requirements
|
| 29 |
+
"Material terms of the negative option offer (billing frequency, amount, cancellation) must be disclosed clearly and conspicuously before obtaining billing information.",
|
| 30 |
+
"The disclosure must be placed in close proximity to the request for consent and must not be hidden in fine print or separate terms and conditions pages.",
|
| 31 |
+
|
| 32 |
+
# 2. Express Informed Consent
|
| 33 |
+
"Marketers must obtain the consumer's express informed consent before charging them for a subscription or recurring service.",
|
| 34 |
+
"Deceptive checkboxes or 'pre-checked' boxes for additional paid services are considered unfair and deceptive acts.",
|
| 35 |
+
|
| 36 |
+
# 3. Simple Cancellation (The 'Click-to-Cancel' Rule)
|
| 37 |
+
"The cancellation mechanism must be at least as simple as the mechanism used to initiate the subscription (e.g., if you join online, you must be able to cancel online).",
|
| 38 |
+
"Failing to provide a simple method to stop recurring charges is a direct violation of the ROSCA (Restore Online Shoppers' Confidence Act).",
|
| 39 |
+
|
| 40 |
+
# 4. Misleading Pricing (Bait & Switch)
|
| 41 |
+
"Promising a 'Free' or '$0' trial without disclosing that a full-price subscription begins automatically after a short period is a deceptive practice.",
|
| 42 |
+
"Requiring credit card information for a 'free' service must be accompanied by a clear explanation of when and why the card will be charged."
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
# Pre-calculate DB embeddings
|
| 46 |
+
self.db_embeddings = self.retriever.encode(self.fact_db, convert_to_tensor=True)
|
| 47 |
+
|
| 48 |
+
def calculate_x3_score(self, text):
|
| 49 |
+
"""Calculate a regulatory non-compliance score based on FTC guidelines via RAG."""
|
| 50 |
+
if not text or len(text.strip()) < 10:
|
| 51 |
+
return 0.0, "Insufficient text provided for compliance audit."
|
| 52 |
+
|
| 53 |
+
try:
|
| 54 |
+
# 1. Retrieval: Find the most relevant FTC guideline
|
| 55 |
+
query_embedding = self.retriever.encode(text, convert_to_tensor=True)
|
| 56 |
+
cosine_scores = util.cos_sim(query_embedding, self.db_embeddings)[0]
|
| 57 |
+
best_idx = torch.argmax(cosine_scores).item()
|
| 58 |
+
retrieved_fact = self.fact_db[best_idx]
|
| 59 |
+
|
| 60 |
+
# 2. Generation: GPT Audit
|
| 61 |
+
prompt = f"""
|
| 62 |
+
You are a senior auditor for the FTC (Federal Trade Commission) specialized in deceptive subscription practices.
|
| 63 |
+
Audit the [Ad/Service Copy] based on the [Relevant Regulation].
|
| 64 |
+
|
| 65 |
+
[Relevant Regulation]: {retrieved_fact}
|
| 66 |
+
[Ad/Service Copy]: {text}
|
| 67 |
+
|
| 68 |
+
Response Format (Strictly follow this):
|
| 69 |
+
Compliance Risk: [Score from 0 to 100, where 100 is a definite violation]
|
| 70 |
+
Specific Violation: [1-2 sentences explaining why it violates or follows the rule]
|
| 71 |
+
Verdict: [CRITICAL / SUSPICIOUS / COMPLIANT]
|
| 72 |
+
"""
|
| 73 |
+
|
| 74 |
+
print(" -> 🤖 GPT Auditor is cross-checking with federal regulations...")
|
| 75 |
+
response = self.client.chat.completions.create(
|
| 76 |
+
model="gpt-4o", # Using 4o for better reasoning in English
|
| 77 |
+
messages=[{"role": "user", "content": prompt}],
|
| 78 |
+
temperature=0.0
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
result_text = response.choices[0].message.content
|
| 82 |
+
print(f" [Audit Report]\n{result_text}")
|
| 83 |
+
|
| 84 |
+
# Extract score
|
| 85 |
+
score_match = re.search(r"Compliance Risk:\s*(\d+)", result_text)
|
| 86 |
+
x3_score = float(score_match.group(1)) if score_match else 0.0
|
| 87 |
+
|
| 88 |
+
return x3_score, retrieved_fact
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
print(f"⚠️ Audit Error: {e}")
|
| 92 |
+
return 0.0, "An error occurred during regulatory analysis."
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
checker = FactCheckerRAG()
|
| 96 |
+
# Test case: Typical subscription trap language
|
| 97 |
+
test_copy = "Get your FREE trial now! $0.00 today. Just enter your credit card to verify your identity."
|
| 98 |
+
|
| 99 |
+
score, fact = checker.calculate_x3_score(test_copy)
|
| 100 |
+
print("-" * 50)
|
| 101 |
+
print(f"Final Non-Compliance Score: {score}")
|
| 102 |
+
print(f"Reference FTC Guideline: {fact}")
|
myapp/step4_xai.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import xgboost as xgb
|
| 3 |
+
import shap
|
| 4 |
+
|
| 5 |
+
class XAIScorer:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
print("📊 [Step 4] Loading the XAI Scoring Engine (XGBoost + SHAP)...")
|
| 8 |
+
|
| 9 |
+
# 1. Generating synthetic data for PoC training
|
| 10 |
+
# Features: X1 (Lexical Bait), X2 (Semantic Intent), X3 (Regulatory Violation)
|
| 11 |
+
np.random.seed(42)
|
| 12 |
+
X_train = np.random.rand(1000, 3) * 100
|
| 13 |
+
|
| 14 |
+
# Hypothetical labeling logic:
|
| 15 |
+
# In subscription traps, Regulatory non-compliance (X3) and Semantic intent (X2)
|
| 16 |
+
# carry the most weight in determining a 'Deceptive' label.
|
| 17 |
+
# Target: 1 (Deceptive/Trap), 0 (Compliant)
|
| 18 |
+
y_train = ((X_train[:, 0]*0.2 + X_train[:, 1]*0.4 + X_train[:, 2]*0.4) > 50).astype(int)
|
| 19 |
+
|
| 20 |
+
# 2. Define and train the XGBoost Classifier
|
| 21 |
+
# This model acts as the 'decision-making brain' of the auditor.
|
| 22 |
+
self.model = xgb.XGBClassifier(
|
| 23 |
+
n_estimators=50,
|
| 24 |
+
max_depth=3,
|
| 25 |
+
learning_rate=0.1,
|
| 26 |
+
eval_metric='logloss',
|
| 27 |
+
random_state=42
|
| 28 |
+
)
|
| 29 |
+
self.model.fit(X_train, y_train)
|
| 30 |
+
|
| 31 |
+
# 3. Initialize the SHAP TreeExplainer
|
| 32 |
+
# This allows us to see exactly how each feature influenced the final risk score.
|
| 33 |
+
self.explainer = shap.TreeExplainer(self.model)
|
| 34 |
+
print(" -> 🧠 Machine-learning scoring engine is ready (Global Version).")
|
| 35 |
+
|
| 36 |
+
def calculate_final_score_and_explain(self, x1, x2, x3):
|
| 37 |
+
"""
|
| 38 |
+
Calculates the probability of being a Dark Pattern and provides an XAI trace.
|
| 39 |
+
x1: Lexical Score, x2: Semantic Score, x3: RAG Compliance Score
|
| 40 |
+
"""
|
| 41 |
+
# Convert input scores into a numpy array for the model
|
| 42 |
+
X_input = np.array([[x1, x2, x3]])
|
| 43 |
+
|
| 44 |
+
# =========================================================
|
| 45 |
+
# 1. Final Risk Scoring
|
| 46 |
+
# Predict_proba returns the probability of class 1 (Deceptive)
|
| 47 |
+
# =========================================================
|
| 48 |
+
probabilities = self.model.predict_proba(X_input)
|
| 49 |
+
final_score = probabilities[0][1] * 100 # Scale to 0-100 range
|
| 50 |
+
|
| 51 |
+
# =========================================================
|
| 52 |
+
# 2. XAI (SHAP Value Calculation)
|
| 53 |
+
# SHAP explains the deviation from the base value (average prediction).
|
| 54 |
+
# =========================================================
|
| 55 |
+
shap_values = self.explainer.shap_values(X_input)
|
| 56 |
+
|
| 57 |
+
# Handle different SHAP output formats (list or array)
|
| 58 |
+
if isinstance(shap_values, list):
|
| 59 |
+
# For binary classification, we take the values for the positive class (1)
|
| 60 |
+
shap_vals = shap_values[1][0]
|
| 61 |
+
elif len(shap_values.shape) == 3: # Some versions return (obs, class, features)
|
| 62 |
+
shap_vals = shap_values[0][1]
|
| 63 |
+
else:
|
| 64 |
+
shap_vals = shap_values[0]
|
| 65 |
+
|
| 66 |
+
# Extract the Base Value (The average risk score of the training set)
|
| 67 |
+
if isinstance(self.explainer.expected_value, (list, np.ndarray)):
|
| 68 |
+
base_value = self.explainer.expected_value[1]
|
| 69 |
+
else:
|
| 70 |
+
base_value = self.explainer.expected_value
|
| 71 |
+
|
| 72 |
+
# Note: final_score ≈ (base_value + sum(shap_vals)) * 100
|
| 73 |
+
return final_score, shap_vals, base_value
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
scorer = XAIScorer()
|
| 77 |
+
# Example: A page with some lexical bait but high semantic deception and regulatory failure
|
| 78 |
+
f_score, s_vals, b_val = scorer.calculate_final_score_and_explain(30.0, 75.0, 85.0)
|
| 79 |
+
|
| 80 |
+
print("\n" + "="*50)
|
| 81 |
+
print(f"Audit Result: {f_score:.2f}% Deception Probability")
|
| 82 |
+
print("-" * 50)
|
| 83 |
+
print(f"Lexical Impact: {s_vals[0]:+.2f}")
|
| 84 |
+
print(f"Semantic Impact: {s_vals[1]:+.2f}")
|
| 85 |
+
print(f"Regulatory Impact: {s_vals[2]:+.2f}")
|
| 86 |
+
print("="*50)
|
packages.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ffmpeg
|
| 2 |
+
libgl1
|
| 3 |
+
libglib2.0-0
|
| 4 |
+
libsm6
|
| 5 |
+
libxext6
|
| 6 |
+
libxrender-dev
|
| 7 |
+
libgomp1
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pydantic
|
| 4 |
+
requests
|
| 5 |
+
playwright
|
| 6 |
+
python-dotenv
|
| 7 |
+
openai
|
| 8 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 9 |
+
torch
|
| 10 |
+
sentence-transformers
|
| 11 |
+
transformers
|
| 12 |
+
python-mecab-ko
|
| 13 |
+
numpy
|
| 14 |
+
xgboost==1.7.6
|
| 15 |
+
shap
|
| 16 |
+
paddleocr
|