Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -2,51 +2,19 @@ import gradio as gr
|
|
2 |
import torch
|
3 |
import numpy as np
|
4 |
from transformers import pipeline, AutoModelForSequenceClassification, AutoTokenizer
|
5 |
-
from motif_tagging import detect_motifs
|
6 |
-
import re
|
7 |
import matplotlib.pyplot as plt
|
8 |
import io
|
9 |
from PIL import Image
|
10 |
from datetime import datetime
|
11 |
|
12 |
-
# ---
|
13 |
-
|
14 |
-
import matplotlib.pyplot as plt
|
15 |
-
import io
|
16 |
-
from PIL import Image
|
17 |
-
from datetime import datetime
|
18 |
-
|
19 |
-
try:
|
20 |
-
parsed_dates = [datetime.strptime(d, "%Y-%m-%d") for d in dates]
|
21 |
-
except Exception:
|
22 |
-
parsed_dates = list(range(len(dates)))
|
23 |
-
|
24 |
-
fig, ax = plt.subplots(figsize=(8, 3))
|
25 |
-
ax.plot(parsed_dates, scores, marker='o', linestyle='-', color='darkred', linewidth=2)
|
26 |
-
|
27 |
-
for i, (x, y) in enumerate(zip(parsed_dates, scores)):
|
28 |
-
label = labels[i]
|
29 |
-
ax.text(x, y + 2, f"{label}\n{int(y)}%", ha='center', fontsize=8, color='black')
|
30 |
-
|
31 |
-
ax.set_title("Abuse Intensity Over Time")
|
32 |
-
ax.set_xlabel("Date")
|
33 |
-
ax.set_ylabel("Abuse Score (%)")
|
34 |
-
ax.set_ylim(0, 105)
|
35 |
-
ax.grid(True)
|
36 |
-
plt.tight_layout()
|
37 |
-
|
38 |
-
buf = io.BytesIO()
|
39 |
-
plt.savefig(buf, format='png')
|
40 |
-
buf.seek(0)
|
41 |
-
return Image.open(buf)
|
42 |
-
# --- SST Sentiment Model ---
|
43 |
-
sst_pipeline = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
|
44 |
-
|
45 |
-
# --- Abuse Model ---
|
46 |
-
model_name = "SamanthaStorm/tether-multilabel-v1"
|
47 |
model = AutoModelForSequenceClassification.from_pretrained(model_name)
|
48 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
49 |
|
|
|
|
|
|
|
50 |
LABELS = [
|
51 |
"blame shifting", "contradictory statements", "control", "dismissiveness",
|
52 |
"gaslighting", "guilt tripping", "insults", "obscure language",
|
@@ -54,253 +22,103 @@ LABELS = [
|
|
54 |
]
|
55 |
|
56 |
THRESHOLDS = {
|
57 |
-
"blame shifting": 0.3, "contradictory statements": 0.
|
58 |
-
"gaslighting": 0.
|
59 |
-
"projection": 0.
|
60 |
-
}
|
61 |
-
|
62 |
-
PATTERN_WEIGHTS = {
|
63 |
-
"gaslighting": 1.3,
|
64 |
-
"control": 1.2,
|
65 |
-
"dismissiveness": 0.8,
|
66 |
-
"blame shifting": 0.8,
|
67 |
-
"contradictory statements": 0.75,
|
68 |
-
"threat": 1.5 # 🔧 New: raise weight for threat
|
69 |
-
}
|
70 |
-
RISK_STAGE_LABELS = {
|
71 |
-
1: "🌀 Risk Stage: Tension-Building\nThis message reflects rising emotional pressure or subtle control attempts.",
|
72 |
-
2: "🔥 Risk Stage: Escalation\nThis message includes direct or aggressive patterns, suggesting active harm.",
|
73 |
-
3: "🌧️ Risk Stage: Reconciliation\nThis message reflects a reset attempt—apologies or emotional repair without accountability.",
|
74 |
-
4: "🌸 Risk Stage: Calm / Honeymoon\nThis message appears supportive but may follow prior harm, minimizing it."
|
75 |
}
|
76 |
|
77 |
ESCALATION_QUESTIONS = [
|
78 |
("Partner has access to firearms or weapons", 4),
|
79 |
("Partner threatened to kill you", 3),
|
80 |
("Partner threatened you with a weapon", 3),
|
81 |
-
("Partner has ever choked you
|
82 |
("Partner injured or threatened your pet(s)", 3),
|
83 |
-
("Partner has broken your things, punched
|
84 |
-
("Partner forced
|
85 |
("Partner threatened to take away your children", 2),
|
86 |
("Violence has increased in frequency or severity", 3),
|
87 |
-
("Partner monitors your calls
|
88 |
]
|
89 |
-
DARVO_PATTERNS = {
|
90 |
-
"blame shifting", "projection", "dismissiveness", "guilt tripping", "contradictory statements"
|
91 |
-
}
|
92 |
-
DARVO_MOTIFS = [
|
93 |
-
"I never said that.", "You’re imagining things.", "That never happened.",
|
94 |
-
"You’re making a big deal out of nothing.", "It was just a joke.", "You’re too sensitive.",
|
95 |
-
"I don’t know what you’re talking about.", "You’re overreacting.", "I didn’t mean it that way.",
|
96 |
-
"You’re twisting my words.", "You’re remembering it wrong.", "You’re always looking for something to complain about.",
|
97 |
-
"You’re just trying to start a fight.", "I was only trying to help.", "You’re making things up.",
|
98 |
-
"You’re blowing this out of proportion.", "You’re being paranoid.", "You’re too emotional.",
|
99 |
-
"You’re always so dramatic.", "You’re just trying to make me look bad.",
|
100 |
-
|
101 |
-
"You’re crazy.", "You’re the one with the problem.", "You’re always so negative.",
|
102 |
-
"You’re just trying to control me.", "You’re the abusive one.", "You’re trying to ruin my life.",
|
103 |
-
"You’re just jealous.", "You’re the one who needs help.", "You’re always playing the victim.",
|
104 |
-
"You’re the one causing all the problems.", "You’re just trying to make me feel guilty.",
|
105 |
-
"You’re the one who can’t let go of the past.", "You’re the one who’s always angry.",
|
106 |
-
"You’re the one who’s always complaining.", "You’re the one who’s always starting arguments.",
|
107 |
-
"You’re the one who’s always making things worse.", "You’re the one who’s always making me feel bad.",
|
108 |
-
"You’re the one who’s always making me look like the bad guy.",
|
109 |
-
"You’re the one who’s always making me feel like a failure.",
|
110 |
-
"You’re the one who’s always making me feel like I’m not good enough.",
|
111 |
-
|
112 |
-
"I can’t believe you’re doing this to me.", "You’re hurting me.",
|
113 |
-
"You’re making me feel like a terrible person.", "You’re always blaming me for everything.",
|
114 |
-
"You’re the one who’s abusive.", "You’re the one who’s controlling.", "You’re the one who’s manipulative.",
|
115 |
-
"You’re the one who’s toxic.", "You’re the one who’s gaslighting me.",
|
116 |
-
"You’re the one who’s always putting me down.", "You’re the one who’s always making me feel bad.",
|
117 |
-
"You’re the one who’s always making me feel like I’m not good enough.",
|
118 |
-
"You’re the one who’s always making me feel like I’m the problem.",
|
119 |
-
"You’re the one who’s always making me feel like I’m the bad guy.",
|
120 |
-
"You’re the one who’s always making me feel like I’m the villain.",
|
121 |
-
"You’re the one who’s always making me feel like I’m the one who needs to change.",
|
122 |
-
"You’re the one who’s always making me feel like I’m the one who’s wrong.",
|
123 |
-
"You’re the one who’s always making me feel like I’m the one who’s crazy.",
|
124 |
-
"You’re the one who’s always making me feel like I’m the one who’s abusive.",
|
125 |
-
"You’re the one who’s always making me feel like I’m the one who’s toxic."
|
126 |
-
]
|
127 |
-
def detect_contradiction(message):
|
128 |
-
patterns = [
|
129 |
-
(r"\b(i love you).{0,15}(i hate you|you ruin everything)", re.IGNORECASE),
|
130 |
-
(r"\b(i’m sorry).{0,15}(but you|if you hadn’t)", re.IGNORECASE),
|
131 |
-
(r"\b(i’m trying).{0,15}(you never|why do you)", re.IGNORECASE),
|
132 |
-
(r"\b(do what you want).{0,15}(you’ll regret it|i always give everything)", re.IGNORECASE),
|
133 |
-
(r"\b(i don’t care).{0,15}(you never think of me)", re.IGNORECASE),
|
134 |
-
(r"\b(i guess i’m just).{0,15}(the bad guy|worthless|never enough)", re.IGNORECASE)
|
135 |
-
]
|
136 |
-
return any(re.search(p, message, flags) for p, flags in patterns)
|
137 |
-
|
138 |
-
def calculate_darvo_score(patterns, sentiment_before, sentiment_after, motifs_found, contradiction_flag=False):
|
139 |
-
pattern_hits = len([p for p in patterns if p in DARVO_PATTERNS])
|
140 |
-
pattern_score = pattern_hits / len(DARVO_PATTERNS)
|
141 |
-
|
142 |
-
sentiment_shift_score = max(0.0, sentiment_after - sentiment_before)
|
143 |
|
144 |
-
|
145 |
-
motif for motif in motifs_found
|
146 |
-
if any(phrase.lower() in motif.lower() for phrase in DARVO_MOTIFS)
|
147 |
-
])
|
148 |
-
motif_score = motif_hits / len(DARVO_MOTIFS)
|
149 |
|
150 |
-
|
|
|
|
|
151 |
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
), 3)
|
158 |
-
def detect_weapon_language(text):
|
159 |
-
weapon_keywords = [
|
160 |
-
"knife", "knives", "stab", "cut you", "cutting",
|
161 |
-
"gun", "shoot", "rifle", "firearm", "pistol",
|
162 |
-
"bomb", "blow up", "grenade", "explode",
|
163 |
-
"weapon", "armed", "loaded", "kill you", "take you out"
|
164 |
-
]
|
165 |
-
text_lower = text.lower()
|
166 |
-
return any(word in text_lower for word in weapon_keywords)
|
167 |
-
def get_risk_stage(patterns, sentiment):
|
168 |
-
if "threat" in patterns or "insults" in patterns:
|
169 |
-
return 2
|
170 |
-
elif "recovery phase" in patterns:
|
171 |
-
return 3
|
172 |
-
elif "control" in patterns or "guilt tripping" in patterns:
|
173 |
-
return 1
|
174 |
-
elif sentiment == "supportive" and any(p in patterns for p in ["projection", "dismissiveness"]):
|
175 |
-
return 4
|
176 |
-
return 1
|
177 |
-
|
178 |
-
def generate_risk_snippet(abuse_score, top_label, escalation_score, stage):
|
179 |
-
if abuse_score >= 85 or escalation_score >= 16:
|
180 |
-
risk_level = "high"
|
181 |
-
elif abuse_score >= 60 or escalation_score >= 8:
|
182 |
-
risk_level = "moderate"
|
183 |
-
elif stage == 2 and abuse_score >= 40:
|
184 |
-
risk_level = "moderate" # 🔧 New rule for escalation stage
|
185 |
-
else:
|
186 |
-
risk_level = "low"
|
187 |
-
pattern_label = top_label.split(" – ")[0]
|
188 |
-
pattern_score = top_label.split(" – ")[1] if " – " in top_label else ""
|
189 |
-
|
190 |
-
WHY_FLAGGED = {
|
191 |
-
"control": "This message may reflect efforts to restrict someone’s autonomy, even if it's framed as concern or care.",
|
192 |
-
"gaslighting": "This message could be manipulating someone into questioning their perception or feelings.",
|
193 |
-
"dismissiveness": "This message may include belittling, invalidating, or ignoring the other person’s experience.",
|
194 |
-
"insults": "Direct insults often appear in escalating abusive dynamics and can erode emotional safety.",
|
195 |
-
"threat": "This message includes threatening language, which is a strong predictor of harm.",
|
196 |
-
"blame shifting": "This message may redirect responsibility to avoid accountability, especially during conflict.",
|
197 |
-
"guilt tripping": "This message may induce guilt in order to control or manipulate behavior.",
|
198 |
-
"recovery phase": "This message may be part of a tension-reset cycle, appearing kind but avoiding change.",
|
199 |
-
"projection": "This message may involve attributing the abuser’s own behaviors to the victim.",
|
200 |
-
"default": "This message contains language patterns that may affect safety, clarity, or emotional autonomy."
|
201 |
-
}
|
202 |
|
203 |
-
|
|
|
204 |
|
205 |
-
|
206 |
-
|
|
|
207 |
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
|
215 |
-
|
216 |
-
|
217 |
-
|
|
|
218 |
|
219 |
-
|
220 |
-
def analyze_single_message(text, thresholds):
|
221 |
-
# NEW: Quick healthy check
|
222 |
if is_healthy_message(text):
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
return abuse_score, threshold_labels, top_patterns, sentiment_result, stage, darvo_score
|
231 |
-
motif_hits, matched_phrases = detect_motifs(text)
|
232 |
-
result = sst_pipeline(text)[0]
|
233 |
-
sentiment = "supportive" if result['label'] == "POSITIVE" else "undermining"
|
234 |
-
sentiment_score = result['score'] if sentiment == "undermining" else 0.0
|
235 |
-
weapon_flag = detect_weapon_language(text)
|
236 |
-
adjusted_thresholds = {
|
237 |
-
k: v + 0.05 if sentiment == "supportive" else v
|
238 |
-
for k, v in thresholds.items()
|
239 |
-
}
|
240 |
-
|
241 |
-
contradiction_flag = detect_contradiction(text)
|
242 |
|
243 |
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
244 |
with torch.no_grad():
|
245 |
outputs = model(**inputs)
|
246 |
-
|
|
|
247 |
|
248 |
-
|
249 |
-
label for label,
|
250 |
-
if
|
251 |
-
]
|
252 |
|
253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
254 |
|
255 |
-
darvo_score = calculate_darvo_score(
|
256 |
-
threshold_labels,
|
257 |
-
sentiment_before=0.0,
|
258 |
-
sentiment_after=sentiment_score,
|
259 |
-
motifs_found=motifs,
|
260 |
-
contradiction_flag=contradiction_flag
|
261 |
-
)
|
262 |
top_patterns = sorted(
|
263 |
-
[(label,
|
264 |
key=lambda x: x[1],
|
265 |
reverse=True
|
266 |
)[:2]
|
267 |
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
abuse_score_raw = (weighted_total / weight_sum) * 100
|
277 |
-
stage = get_risk_stage(threshold_labels, sentiment)
|
278 |
-
if weapon_flag:
|
279 |
-
abuse_score_raw = min(abuse_score_raw + 25, 100) # boost intensity
|
280 |
-
if weapon_flag and stage < 2:
|
281 |
-
stage = 2
|
282 |
-
if weapon_flag:
|
283 |
-
print("⚠️ Weapon-related language detected.")
|
284 |
-
|
285 |
-
if "threat" in threshold_labels or "control" in threshold_labels or "insults" in threshold_labels:
|
286 |
-
abuse_score = min(abuse_score_raw, 100)
|
287 |
-
else:
|
288 |
-
abuse_score = min(abuse_score_raw, 95)
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
print("\n--- Debug Info ---")
|
293 |
-
print(f"Text: {text}")
|
294 |
-
print(f"Sentiment: {sentiment} (raw: {result['label']}, score: {result['score']:.3f})")
|
295 |
-
print("Abuse Pattern Scores:")
|
296 |
-
for label, score in zip(LABELS, scores):
|
297 |
-
passed = "✅" if score > adjusted_thresholds[label] else "❌"
|
298 |
-
print(f" {label:25} → {score:.3f} {passed}")
|
299 |
-
print(f"Motifs: {motifs}")
|
300 |
-
print(f"Contradiction: {contradiction_flag}")
|
301 |
-
print("------------------\n")
|
302 |
-
|
303 |
-
return abuse_score, threshold_labels, top_patterns, result, stage, darvo_score
|
304 |
|
305 |
def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
306 |
none_selected_checked = answers_and_none[-1]
|
@@ -324,45 +142,25 @@ def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
|
324 |
if not active:
|
325 |
return "Please enter at least one message."
|
326 |
|
327 |
-
results = [(analyze_single_message(m
|
328 |
-
abuse_scores = [r[0][
|
329 |
-
top_labels = [r[0][
|
330 |
-
|
331 |
-
sentiments = [r[0][3]['label'] for r in results]
|
332 |
-
stages = [r[0][4] for r in results]
|
333 |
-
darvo_scores = [r[0][5] for r in results]
|
334 |
-
dates_used = [r[1] or "Undated" for r in results] # Store dates for future mapping
|
335 |
|
336 |
composite_abuse = int(round(sum(abuse_scores) / len(abuse_scores)))
|
337 |
-
|
338 |
-
|
339 |
-
most_common_stage = max(set(stages), key=stages.count)
|
340 |
-
stage_text = RISK_STAGE_LABELS[most_common_stage]
|
341 |
-
|
342 |
-
avg_darvo = round(sum(darvo_scores) / len(darvo_scores), 3)
|
343 |
-
darvo_blurb = ""
|
344 |
-
if avg_darvo > 0.25:
|
345 |
-
level = "moderate" if avg_darvo < 0.65 else "high"
|
346 |
-
darvo_blurb = f"\n\n🎭 **DARVO Score: {avg_darvo}** → This indicates a **{level} likelihood** of narrative reversal (DARVO), where the speaker may be denying, attacking, or reversing blame."
|
347 |
|
348 |
out = f"Abuse Intensity: {composite_abuse}%\n"
|
349 |
-
out += "📊 This reflects the strength and severity of detected abuse patterns in the message(s).\n\n"
|
350 |
-
|
351 |
if escalation_score is None:
|
352 |
out += "Escalation Potential: Unknown (Checklist not completed)\n"
|
353 |
-
out += "🔍 *This section was not completed. Escalation potential is unknown.*\n\n"
|
354 |
else:
|
355 |
out += f"Escalation Potential: {risk_level} ({escalation_score}/{sum(w for _, w in ESCALATION_QUESTIONS)})\n"
|
356 |
-
out += "🚨 This indicates how many serious risk factors are present based on your answers to the safety checklist.\n"
|
357 |
-
|
358 |
-
out += generate_risk_snippet(composite_abuse, top_label, escalation_score if escalation_score is not None else 0, most_common_stage)
|
359 |
-
out += f"\n\n{stage_text}"
|
360 |
-
out += darvo_blurb
|
361 |
|
362 |
-
|
363 |
-
timeline_image = generate_abuse_score_chart(dates_used, abuse_scores, pattern_labels)
|
364 |
return out, timeline_image
|
365 |
-
|
|
|
|
|
366 |
message_date_pairs = [
|
367 |
(
|
368 |
gr.Textbox(label=f"Message {i+1}"),
|
@@ -381,9 +179,9 @@ iface = gr.Interface(
|
|
381 |
gr.Textbox(label="Results"),
|
382 |
gr.Image(label="Risk Stage Timeline", type="pil")
|
383 |
],
|
384 |
-
title="Abuse Pattern Detector
|
385 |
allow_flagging="manual"
|
386 |
)
|
387 |
|
388 |
if __name__ == "__main__":
|
389 |
-
iface.launch()
|
|
|
2 |
import torch
|
3 |
import numpy as np
|
4 |
from transformers import pipeline, AutoModelForSequenceClassification, AutoTokenizer
|
|
|
|
|
5 |
import matplotlib.pyplot as plt
|
6 |
import io
|
7 |
from PIL import Image
|
8 |
from datetime import datetime
|
9 |
|
10 |
+
# --- Load models ---
|
11 |
+
model_name = "SamanthaStorm/tether-multilabel-v2" # UPDATE if needed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
model = AutoModelForSequenceClassification.from_pretrained(model_name)
|
13 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
14 |
|
15 |
+
healthy_detector = pipeline("text-classification", model="distilbert-base-uncased-finetuned-sst-2-english")
|
16 |
+
sst_pipeline = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
|
17 |
+
|
18 |
LABELS = [
|
19 |
"blame shifting", "contradictory statements", "control", "dismissiveness",
|
20 |
"gaslighting", "guilt tripping", "insults", "obscure language",
|
|
|
22 |
]
|
23 |
|
24 |
THRESHOLDS = {
|
25 |
+
"blame shifting": 0.3, "contradictory statements": 0.3, "control": 0.35, "dismissiveness": 0.4,
|
26 |
+
"gaslighting": 0.3, "guilt tripping": 0.3, "insults": 0.3, "obscure language": 0.4,
|
27 |
+
"projection": 0.4, "recovery phase": 0.35, "threat": 0.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
}
|
29 |
|
30 |
ESCALATION_QUESTIONS = [
|
31 |
("Partner has access to firearms or weapons", 4),
|
32 |
("Partner threatened to kill you", 3),
|
33 |
("Partner threatened you with a weapon", 3),
|
34 |
+
("Partner has ever choked you", 4),
|
35 |
("Partner injured or threatened your pet(s)", 3),
|
36 |
+
("Partner has broken your things, punched walls, or thrown objects", 2),
|
37 |
+
("Partner forced you into unwanted sexual acts", 3),
|
38 |
("Partner threatened to take away your children", 2),
|
39 |
("Violence has increased in frequency or severity", 3),
|
40 |
+
("Partner monitors your calls, GPS, or social media", 2)
|
41 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
|
43 |
+
# --- Functions ---
|
|
|
|
|
|
|
|
|
44 |
|
45 |
+
def is_healthy_message(text, threshold=0.9):
|
46 |
+
result = healthy_detector(text)[0]
|
47 |
+
return result['label'] == "POSITIVE" and result['score'] > threshold
|
48 |
|
49 |
+
def generate_abuse_score_chart(dates, scores, labels):
|
50 |
+
try:
|
51 |
+
parsed_dates = [datetime.strptime(d, "%Y-%m-%d") for d in dates]
|
52 |
+
except Exception:
|
53 |
+
parsed_dates = list(range(len(dates)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
+
fig, ax = plt.subplots(figsize=(8, 3))
|
56 |
+
ax.plot(parsed_dates, scores, marker='o', linestyle='-', color='darkred', linewidth=2)
|
57 |
|
58 |
+
for i, (x, y) in enumerate(zip(parsed_dates, scores)):
|
59 |
+
label = labels[i]
|
60 |
+
ax.text(x, y + 2, f"{label}\n{int(y)}%", ha='center', fontsize=8, color='black')
|
61 |
|
62 |
+
ax.set_title("Abuse Intensity Over Time")
|
63 |
+
ax.set_xlabel("Date")
|
64 |
+
ax.set_ylabel("Abuse Score (%)")
|
65 |
+
ax.set_ylim(0, 105)
|
66 |
+
ax.grid(True)
|
67 |
+
plt.tight_layout()
|
68 |
|
69 |
+
buf = io.BytesIO()
|
70 |
+
plt.savefig(buf, format='png')
|
71 |
+
buf.seek(0)
|
72 |
+
return Image.open(buf)
|
73 |
|
74 |
+
def analyze_single_message(text):
|
|
|
|
|
75 |
if is_healthy_message(text):
|
76 |
+
return {
|
77 |
+
"abuse_score": 0,
|
78 |
+
"labels": [],
|
79 |
+
"sentiment": "supportive",
|
80 |
+
"stage": 4,
|
81 |
+
"top_patterns": [],
|
82 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
|
84 |
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
85 |
with torch.no_grad():
|
86 |
outputs = model(**inputs)
|
87 |
+
logits = outputs.logits.squeeze(0)
|
88 |
+
probs = torch.sigmoid(logits).numpy()
|
89 |
|
90 |
+
detected_labels = [
|
91 |
+
label for label, prob in zip(LABELS, probs)
|
92 |
+
if prob > THRESHOLDS.get(label, 0.3)
|
93 |
+
]
|
94 |
|
95 |
+
abuse_score = (sum(probs[i] for i, label in enumerate(LABELS) if label in detected_labels) / len(LABELS)) * 100
|
96 |
+
|
97 |
+
sentiment_result = sst_pipeline(text)[0]
|
98 |
+
sentiment = "supportive" if sentiment_result['label'] == "POSITIVE" else "undermining"
|
99 |
+
|
100 |
+
if "threat" in detected_labels or "insults" in detected_labels:
|
101 |
+
stage = 2 # Escalation
|
102 |
+
elif "control" in detected_labels or "guilt tripping" in detected_labels:
|
103 |
+
stage = 1 # Tension building
|
104 |
+
elif "recovery phase" in detected_labels:
|
105 |
+
stage = 3 # Reconciliation
|
106 |
+
else:
|
107 |
+
stage = 1
|
108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
top_patterns = sorted(
|
110 |
+
[(label, prob) for label, prob in zip(LABELS, probs)],
|
111 |
key=lambda x: x[1],
|
112 |
reverse=True
|
113 |
)[:2]
|
114 |
|
115 |
+
return {
|
116 |
+
"abuse_score": int(abuse_score),
|
117 |
+
"labels": detected_labels,
|
118 |
+
"sentiment": sentiment,
|
119 |
+
"stage": stage,
|
120 |
+
"top_patterns": top_patterns,
|
121 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
|
123 |
def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
124 |
none_selected_checked = answers_and_none[-1]
|
|
|
142 |
if not active:
|
143 |
return "Please enter at least one message."
|
144 |
|
145 |
+
results = [(analyze_single_message(m), d) for m, d in active]
|
146 |
+
abuse_scores = [r[0]["abuse_score"] for r in results]
|
147 |
+
top_labels = [r[0]["top_patterns"][0][0] if r[0]["top_patterns"] else "None" for r in results]
|
148 |
+
dates_used = [r[1] or "Undated" for r in results]
|
|
|
|
|
|
|
|
|
149 |
|
150 |
composite_abuse = int(round(sum(abuse_scores) / len(abuse_scores)))
|
151 |
+
most_common_stage = max(set(r[0]["stage"] for r in results), key=lambda x: [r[0]["stage"] for r in results].count)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
|
153 |
out = f"Abuse Intensity: {composite_abuse}%\n"
|
|
|
|
|
154 |
if escalation_score is None:
|
155 |
out += "Escalation Potential: Unknown (Checklist not completed)\n"
|
|
|
156 |
else:
|
157 |
out += f"Escalation Potential: {risk_level} ({escalation_score}/{sum(w for _, w in ESCALATION_QUESTIONS)})\n"
|
|
|
|
|
|
|
|
|
|
|
158 |
|
159 |
+
timeline_image = generate_abuse_score_chart(dates_used, abuse_scores, top_labels)
|
|
|
160 |
return out, timeline_image
|
161 |
+
|
162 |
+
# --- Gradio Interface ---
|
163 |
+
|
164 |
message_date_pairs = [
|
165 |
(
|
166 |
gr.Textbox(label=f"Message {i+1}"),
|
|
|
179 |
gr.Textbox(label="Results"),
|
180 |
gr.Image(label="Risk Stage Timeline", type="pil")
|
181 |
],
|
182 |
+
title="Tether Abuse Pattern Detector v2",
|
183 |
allow_flagging="manual"
|
184 |
)
|
185 |
|
186 |
if __name__ == "__main__":
|
187 |
+
iface.launch()
|