SamanthaStorm commited on
Commit
9ea207c
·
verified ·
1 Parent(s): 67874ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -284
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
- # --- Timeline Visualization Function ---
13
- def generate_abuse_score_chart(dates, scores, labels):
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.36, "control": 0.48, "dismissiveness": 0.45,
58
- "gaslighting": 0.30, "guilt tripping": 0.20, "insults": 0.34, "obscure language": 0.25,
59
- "projection": 0.35, "recovery phase": 0.25, "threat": 0.25
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, even if you considered it consensual at the time", 4),
82
  ("Partner injured or threatened your pet(s)", 3),
83
- ("Partner has broken your things, punched or kicked walls, or thrown things ", 2),
84
- ("Partner forced or coerced you into unwanted sexual acts", 3),
85
  ("Partner threatened to take away your children", 2),
86
  ("Violence has increased in frequency or severity", 3),
87
- ("Partner monitors your calls/GPS/social media", 2)
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
- motif_hits = len([
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
- contradiction_score = 1.0 if contradiction_flag else 0.0
 
 
151
 
152
- return round(min(
153
- 0.3 * pattern_score +
154
- 0.3 * sentiment_shift_score +
155
- 0.25 * motif_score +
156
- 0.15 * contradiction_score, 1.0
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
- explanation = WHY_FLAGGED.get(pattern_label.lower(), WHY_FLAGGED["default"])
 
204
 
205
- base = f"\n\n🛑 Risk Level: {risk_level.capitalize()}\n"
206
- base += f"This message shows strong indicators of **{pattern_label}**. "
 
207
 
208
- if risk_level == "high":
209
- base += "The language may reflect patterns of emotional control, even when expressed in soft or caring terms.\n"
210
- elif risk_level == "moderate":
211
- base += "There are signs of emotional pressure or indirect control that may escalate if repeated.\n"
212
- else:
213
- base += "The message does not strongly indicate abuse, but it's important to monitor for patterns.\n"
214
 
215
- base += f"\n💡 *Why this might be flagged:*\n{explanation}\n"
216
- base += f"\nDetected Pattern: **{pattern_label} ({pattern_score})**\n"
217
- base += "🧠 You can review the pattern in context. This tool highlights possible dynamics—not judgments."
 
218
 
219
- return base
220
- def analyze_single_message(text, thresholds):
221
- # NEW: Quick healthy check
222
  if is_healthy_message(text):
223
- print("✅ Message detected as healthy. Skipping abuse detection.")
224
- abuse_score = 0
225
- threshold_labels = []
226
- top_patterns = []
227
- sentiment_result = {"label": "POSITIVE", "score": 1.0}
228
- stage = 4 # Calm/Honeymoon
229
- darvo_score = 0.0
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
- scores = torch.sigmoid(outputs.logits.squeeze(0)).numpy()
 
247
 
248
- threshold_labels = [
249
- label for label, score in zip(LABELS, scores)
250
- if score > adjusted_thresholds[label]
251
- ]
252
 
253
- motifs = [phrase for _, phrase in matched_phrases]
 
 
 
 
 
 
 
 
 
 
 
 
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, score) for label, score in zip(LABELS, scores)],
264
  key=lambda x: x[1],
265
  reverse=True
266
  )[:2]
267
 
268
- # Compute weighted average across all patterns (not just top 2)
269
- weighted_total = 0.0
270
- weight_sum = 0.0
271
- for label, score in zip(LABELS, scores):
272
- weight = PATTERN_WEIGHTS.get(label, 1.0)
273
- weighted_total += score * weight
274
- weight_sum += weight
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, THRESHOLDS.copy()), d) for m, d in active]
328
- abuse_scores = [r[0][0] for r in results]
329
- top_labels = [r[0][2][0][0] for r in results]
330
- top_scores = [r[0][2][0][1] for r in results]
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
- top_label = f"{top_labels[0]} {int(round(top_scores[0] * 100))}%"
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
- pattern_labels = [r[0][2][0][0] for r in results] # top label for each message
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 + Escalation Quiz",
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()