SamanthaStorm commited on
Commit
96f5bc2
·
verified ·
1 Parent(s): 1343295

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -121
app.py CHANGED
@@ -9,25 +9,18 @@ import io
9
  from PIL import Image
10
  from datetime import datetime
11
 
12
- # ——— Constants & Utilities ———
13
  LABELS = [
14
  "blame shifting", "contradictory statements", "control", "dismissiveness",
15
  "gaslighting", "guilt tripping", "insults", "obscure language",
16
  "projection", "recovery phase", "threat"
17
  ]
18
 
 
19
  THRESHOLDS = {
20
- "blame shifting": 0.3,
21
- "contradictory statements": 0.3,
22
- "control": 0.35,
23
- "dismissiveness": 0.4,
24
- "gaslighting": 0.3,
25
- "guilt tripping": 0.3,
26
- "insults": 0.3,
27
- "obscure language": 0.4,
28
- "projection": 0.4,
29
- "recovery phase": 0.35,
30
- "threat": 0.3
31
  }
32
 
33
  PATTERN_WEIGHTS = {
@@ -39,10 +32,9 @@ PATTERN_WEIGHTS = {
39
  "threat": 1.5
40
  }
41
 
42
- DARVO_PATTERNS = set([
43
  "blame shifting", "projection", "dismissiveness", "guilt tripping", "contradictory statements"
44
- ])
45
-
46
  DARVO_MOTIFS = [
47
  "I never said that.", "You’re imagining things.", "That never happened.",
48
  "You’re making a big deal out of nothing.", "It was just a joke.", "You’re too sensitive.",
@@ -65,15 +57,16 @@ DARVO_MOTIFS = [
65
  "You’re making me feel like a terrible person.", "You’re always blaming me for everything.",
66
  "You’re the one who’s abusive.", "You’re the one who’s controlling.", "You’re the one who’s manipulative.",
67
  "You’re the one who’s toxic.", "You’re the one who’s gaslighting me.",
68
- "You’re the one who’s always putting me down.",
 
69
  "You’re the one who’s always making me feel like I’m the problem.",
70
- "You’re the one who’s always making me feel like the bad guy.",
71
- "You’re the one who’s always making me feel like the villain.",
72
- "You’re the one who’s always making me feel like the one who needs to change.",
73
- "You’re the one who’s always making me feel like the one who’s wrong.",
74
- "You’re the one who’s always making me feel like the one who’s crazy.",
75
- "You’re the one who’s always making me feel like the one who’s abusive.",
76
- "You’re the one who’s always making me feel like the one who’s toxic."
77
  ]
78
 
79
  RISK_STAGE_LABELS = {
@@ -93,9 +86,10 @@ ESCALATION_QUESTIONS = [
93
  ("Partner forced or coerced you into unwanted sexual acts", 3),
94
  ("Partner threatened to take away your children", 2),
95
  ("Violence has increased in frequency or severity", 3),
96
- ("Partner monitors your calls/GPS/social media", 2)
97
  ]
98
 
 
99
  def detect_contradiction(message):
100
  patterns = [
101
  (r"\b(i love you).{0,15}(i hate you|you ruin everything)", re.IGNORECASE),
@@ -120,23 +114,23 @@ def calculate_darvo_score(patterns, sentiment_before, sentiment_after, motifs_fo
120
  def generate_risk_snippet(abuse_score, top_label, escalation_score, stage):
121
  label = top_label.split(" – ")[0]
122
  why = {
123
- "control": "This message may reflect efforts to restrict someone’s autonomy.",
124
- "gaslighting": "This message could be manipulating perception.",
125
- "dismissiveness": "This message may include belittling, invalidating, or ignoring the other person’s experience.",
126
- "insults": "Direct insults can erode emotional safety.",
127
- "threat": "This message includes threatening language—a strong predictor of harm.",
128
- "blame shifting": "This message may redirect responsibility to avoid accountability.",
129
- "guilt tripping": "This message may induce guilt to control behavior.",
130
- "recovery phase": "This message may be a tension-reset without change.",
131
- "projection": "This message may attribute the speaker’s faults to the target.",
132
- }.get(label, "This message contains language patterns that may affect safety.")
133
  if abuse_score>=85 or escalation_score>=16:
134
  lvl = "high"
135
  elif abuse_score>=60 or escalation_score>=8:
136
  lvl = "moderate"
137
  else:
138
  lvl = "low"
139
- return f"\n\n🛑 Risk Level: {lvl.capitalize()}\nThis message shows **{label}**.\n💡 *Why:* {why}\n"
140
 
141
  def detect_weapon_language(text):
142
  kws = ["knife","gun","bomb","kill you","shoot","explode"]
@@ -150,131 +144,120 @@ def get_risk_stage(patterns, sentiment):
150
  return 1
151
  if "recovery phase" in patterns:
152
  return 3
153
- if sentiment == "supportive" and any(p in patterns for p in ["projection","dismissiveness"]):
154
  return 4
155
  return 1
156
 
157
- # --- Visualization ---
158
- def generate_abuse_score_chart(dates, scores, labels):
159
  try:
160
- parsed = [datetime.strptime(d, "%Y-%m-%d") for d in dates]
161
  except:
162
- parsed = list(range(len(dates)))
163
- fig, ax = plt.subplots(figsize=(8,3))
164
- ax.plot(parsed, scores, marker='o', linestyle='-', color='darkred', linewidth=2)
165
  for i,(x,y) in enumerate(zip(parsed,scores)):
166
- ax.text(x, y+2, f"{labels[i]}\n{int(y)}%", ha='center', fontsize=8)
167
- ax.set_title("Abuse Intensity Over Time")
168
- ax.set_xlabel("Date"); ax.set_ylabel("Abuse Score (%)")
169
- ax.set_ylim(0,105); ax.grid(True); plt.tight_layout()
170
- buf = io.BytesIO(); plt.savefig(buf, format='png'); buf.seek(0)
171
  return Image.open(buf)
172
 
173
- # --- Load Models & Pipelines ---
174
- model_name = "SamanthaStorm/tether-multilabel-v2"
175
- model = AutoModelForSequenceClassification.from_pretrained(model_name)
176
- tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
177
- healthy_detector = pipeline("text-classification", model="distilbert-base-uncased-finetuned-sst-2-english")
178
  sst_pipeline = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
179
 
180
- # --- Single-Message Analysis ---
181
  def analyze_single_message(text):
182
  # healthy bypass
183
- healthy = healthy_detector(text)[0]
184
- if healthy['label'] == "POSITIVE" and healthy['score'] > 0.9:
185
- return {"abuse_score":0, "labels":[], "sentiment":"supportive", "stage":4, "darvo_score":0.0, "top_patterns":[]}
186
- # model scoring
187
- inputs = tokenizer(text, return_tensors='pt', truncation=True, padding=True)
188
- with torch.no_grad():
189
- logits = model(**inputs).logits.squeeze(0)
190
- probs = torch.sigmoid(logits).numpy()
191
- labels = [lab for lab,p in zip(LABELS,probs) if p > THRESHOLDS[lab]]
192
- # abuse score weighted
193
- total_w = sum(PATTERN_WEIGHTS.get(l,1.0) for l in LABELS)
194
- abuse_score = int(round(sum(probs[i]*PATTERN_WEIGHTS.get(l,1.0) for i,l in enumerate(LABELS))/total_w*100))
 
195
  # sentiment shift
196
- sst = sst_pipeline(text)[0]
197
- sentiment = 'supportive' if sst['label']=='POSITIVE' else 'undermining'
198
- sent_score = sst['score'] if sentiment=='undermining' else 0.0
199
  # DARVO
200
  motif_hits, matched = detect_motifs(text)
201
- contradiction = detect_contradiction(text)
202
- darvo_score = calculate_darvo_score(labels, 0.0, sent_score, matched, contradiction)
203
  # stage + weapon
204
- stage = get_risk_stage(labels, sentiment)
205
  if detect_weapon_language(text):
206
- abuse_score = min(abuse_score+25,100)
207
- stage = max(stage,2)
208
  # top patterns
209
- top_patterns = sorted(zip(LABELS,probs), key=lambda x: x[1], reverse=True)[:2]
210
- return {"abuse_score":abuse_score, "labels":labels, "sentiment":sentiment, "stage":stage, "darvo_score":darvo_score, "top_patterns":top_patterns}
 
 
 
211
 
212
- # --- Composite Analysis ---
213
- def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
214
- none_checked = answers_and_none[-1]
215
- any_checked = any(answers_and_none[:-1])
216
- none_sel = (not any_checked) and none_checked
217
  if none_sel:
218
- esc_score = None; risk_level = 'unknown'
219
  else:
220
- esc_score = sum(w for (_,w),a in zip(ESCALATION_QUESTIONS, answers_and_none[:-1]) if a)
221
- risk_level = 'High' if esc_score>=16 else 'Moderate' if esc_score>=8 else 'Low'
222
- msgs = [msg1, msg2, msg3]
223
- dates = [date1, date2, date3]
224
- active = [(m,d) for m,d in zip(msgs, dates) if m.strip()]
225
  if not active:
226
  return "Please enter at least one message."
227
- results = [(analyze_single_message(m), d) for m,d in active]
228
- abuse_scores = [r[0]['abuse_score'] for r in results]
229
- stage_list = [r[0]['stage'] for r,_ in results]
230
- most_common_stage = max(set(stage_list), key=stage_list.count)
231
- composite_abuse = int(round(sum(abuse_scores)/len(abuse_scores)))
232
- # extract top label from each result dict
233
- top_labels = [
234
- res['top_patterns'][0][0]
235
- if res['top_patterns']
236
- else 'None'
237
- for res, _ in results]['top_patterns'][0][0] if r[0]['top_patterns'] else 'None' for r,_ in results
238
- ]
239
- dates_used = [d or 'Undated' for _,d in results]
240
- # common stage
241
- stage_list = [r[0]['stage'] for r,_ in results]
242
- most_common_stage = max(set(stage_list), key=stage_list.count)
243
- composite_abuse = int(round(sum(abuse_scores)/len(abuse_scores)))
244
  # DARVO summary
245
- darvo_vals = [r[0]['darvo_score'] for r,_ in results]
246
- avg_darvo = round(sum(darvo_vals)/len(darvo_vals),3)
247
- darvo_blurb = ''
248
- if avg_darvo>0.25:
249
- lvl = 'moderate' if avg_darvo<0.65 else 'high'
250
- darvo_blurb = f"\n🎭 DARVO Score: {avg_darvo} ({lvl})"
251
- # output text
252
- out = f"Abuse Intensity: {composite_abuse}%\n"
253
  if esc_score is None:
254
- out += "Escalation Potential: Unknown (Checklist not completed)\n"
255
  else:
256
- out += f"Escalation Potential: {risk_level} ({esc_score}/{sum(w for _,w in ESCALATION_QUESTIONS)})\n"
257
  # risk snippet
258
- pattern_score = f"{top_labels[0]} – {int(results[0][0]['top_patterns'][0][1]*100)}%" if results[0][0]['top_patterns'] else top_labels[0]
259
- out += generate_risk_snippet(composite_abuse, pattern_score, esc_score or 0, most_common_stage)
260
- out += darvo_blurb
261
- img = generate_abuse_score_chart(dates_used, abuse_scores, top_labels)
262
  return out, img
263
 
264
- # --- Gradio Interface ---
265
  message_date_pairs = [
266
  (gr.Textbox(label=f"Message {i+1}"), gr.Textbox(label=f"Date {i+1} (optional)", placeholder="YYYY-MM-DD"))
267
  for i in range(3)
268
  ]
269
- textbox_inputs = [item for pair in message_date_pairs for item in pair]
270
  quiz_boxes = [gr.Checkbox(label=q) for q,_ in ESCALATION_QUESTIONS]
271
  none_box = gr.Checkbox(label="None of the above")
 
272
  iface = gr.Interface(
273
  fn=analyze_composite,
274
- inputs=textbox_inputs + quiz_boxes + [none_box],
275
  outputs=[gr.Textbox(label="Results"), gr.Image(label="Risk Stage Timeline", type="pil")],
276
  title="Tether Abuse Pattern Detector v2",
277
  allow_flagging="manual"
278
  )
 
279
  if __name__ == "__main__":
280
- iface.launch()
 
9
  from PIL import Image
10
  from datetime import datetime
11
 
12
+ # ——— Constants ———
13
  LABELS = [
14
  "blame shifting", "contradictory statements", "control", "dismissiveness",
15
  "gaslighting", "guilt tripping", "insults", "obscure language",
16
  "projection", "recovery phase", "threat"
17
  ]
18
 
19
+ # <- Restore your exact thresholds here:
20
  THRESHOLDS = {
21
+ "blame shifting": 0.3, "contradictory statements": 0.3, "control": 0.35, "dismissiveness": 0.4,
22
+ "gaslighting": 0.3, "guilt tripping": 0.3, "insults": 0.3, "obscure language": 0.4,
23
+ "projection": 0.4, "recovery phase": 0.35, "threat": 0.3
 
 
 
 
 
 
 
 
24
  }
25
 
26
  PATTERN_WEIGHTS = {
 
32
  "threat": 1.5
33
  }
34
 
35
+ DARVO_PATTERNS = {
36
  "blame shifting", "projection", "dismissiveness", "guilt tripping", "contradictory statements"
37
+ }
 
38
  DARVO_MOTIFS = [
39
  "I never said that.", "You’re imagining things.", "That never happened.",
40
  "You’re making a big deal out of nothing.", "It was just a joke.", "You’re too sensitive.",
 
57
  "You’re making me feel like a terrible person.", "You’re always blaming me for everything.",
58
  "You’re the one who’s abusive.", "You’re the one who’s controlling.", "You’re the one who’s manipulative.",
59
  "You’re the one who’s toxic.", "You’re the one who’s gaslighting me.",
60
+ "You’re the one who’s always putting me down.", "You’re the one who’s always making me feel bad.",
61
+ "You’re the one who’s always making me feel like I’m not good enough.",
62
  "You’re the one who’s always making me feel like I’m the problem.",
63
+ "You’re the one who’s always making me feel like I’m the bad guy.",
64
+ "You’re the one who’s always making me feel like I’m the villain.",
65
+ "You’re the one who’s always making me feel like I’m the one who needs to change.",
66
+ "You’re the one who’s always making me feel like I’m the one who’s wrong.",
67
+ "You’re the one who’s always making me feel like I’m the one who’s crazy.",
68
+ "You’re the one who’s always making me feel like I’m the one who’s abusive.",
69
+ "You’re the one who’s always making me feel like I’m the one who’s toxic."
70
  ]
71
 
72
  RISK_STAGE_LABELS = {
 
86
  ("Partner forced or coerced you into unwanted sexual acts", 3),
87
  ("Partner threatened to take away your children", 2),
88
  ("Violence has increased in frequency or severity", 3),
89
+ ("Partner monitors your calls, GPS, or social media", 2)
90
  ]
91
 
92
+ # ——— Helper Functions ———
93
  def detect_contradiction(message):
94
  patterns = [
95
  (r"\b(i love you).{0,15}(i hate you|you ruin everything)", re.IGNORECASE),
 
114
  def generate_risk_snippet(abuse_score, top_label, escalation_score, stage):
115
  label = top_label.split(" – ")[0]
116
  why = {
117
+ "control": "efforts to restrict autonomy.",
118
+ "gaslighting": "manipulating perception.",
119
+ "dismissiveness": "invalidating experience.",
120
+ "insults": "direct insults erode safety.",
121
+ "threat": "threatening language predicts harm.",
122
+ "blame shifting": "avoiding accountability.",
123
+ "guilt tripping": "inducing guilt to control behavior.",
124
+ "recovery phase": "tension-reset without change.",
125
+ "projection": "attributing faults to the other person."
126
+ }.get(label, "This message contains concerning patterns.")
127
  if abuse_score>=85 or escalation_score>=16:
128
  lvl = "high"
129
  elif abuse_score>=60 or escalation_score>=8:
130
  lvl = "moderate"
131
  else:
132
  lvl = "low"
133
+ return f"\n\n🛑 Risk Level: {lvl.capitalize()}\nThis message shows **{label}**.\n💡 Why: {why}\n"
134
 
135
  def detect_weapon_language(text):
136
  kws = ["knife","gun","bomb","kill you","shoot","explode"]
 
144
  return 1
145
  if "recovery phase" in patterns:
146
  return 3
147
+ if sentiment=="supportive" and any(p in patterns for p in ["projection","dismissiveness"]):
148
  return 4
149
  return 1
150
 
151
+ def generate_abuse_score_chart(dates,scores,labels):
 
152
  try:
153
+ parsed=[datetime.strptime(d,"%Y-%m-%d") for d in dates]
154
  except:
155
+ parsed=range(len(dates))
156
+ fig,ax=plt.subplots(figsize=(8,3))
157
+ ax.plot(parsed,scores,marker='o',linestyle='-',color='darkred',linewidth=2)
158
  for i,(x,y) in enumerate(zip(parsed,scores)):
159
+ ax.text(x,y+2,f"{labels[i]}\n{int(y)}%",ha='center',fontsize=8)
160
+ ax.set(title="Abuse Intensity Over Time",xlabel="Date",ylabel="Abuse Score (%)")
161
+ ax.set_ylim(0,105);ax.grid(True);plt.tight_layout()
162
+ buf=io.BytesIO();plt.savefig(buf,format='png');buf.seek(0)
 
163
  return Image.open(buf)
164
 
165
+ # ——— Load Models & Pipelines ———
166
+ model_name="SamanthaStorm/tether-multilabel-v2"
167
+ model=AutoModelForSequenceClassification.from_pretrained(model_name)
168
+ tokenizer=AutoTokenizer.from_pretrained(model_name, use_fast=False)
169
+ healthy_detector=pipeline("text-classification",model="distilbert-base-uncased-finetuned-sst-2-english")
170
  sst_pipeline = pipeline("sentiment-analysis", model="distilbert-base-uncased-finetuned-sst-2-english")
171
 
172
+ # ——— Single-Message Analysis ———
173
  def analyze_single_message(text):
174
  # healthy bypass
175
+ h=healthy_detector(text)[0]
176
+ if h['label']=="POSITIVE" and h['score']>0.9:
177
+ return { "abuse_score":0, "labels":[], "sentiment":"supportive",
178
+ "stage":4, "darvo_score":0.0, "top_patterns":[] }
179
+ # model
180
+ inp=tokenizer(text,return_tensors='pt',truncation=True,padding=True)
181
+ with torch.no_grad(): logits=model(**inp).logits.squeeze(0)
182
+ probs=torch.sigmoid(logits).numpy()
183
+ labels=[lab for lab,p in zip(LABELS,probs) if p>THRESHOLDS[lab]]
184
+ # abuse score
185
+ total_w=sum(PATTERN_WEIGHTS.get(l,1.0) for l in LABELS)
186
+ abuse_score=int(round(sum(probs[i]*PATTERN_WEIGHTS.get(l,1.0)
187
+ for i,l in enumerate(LABELS))/total_w*100))
188
  # sentiment shift
189
+ sst=sst_pipeline(text)[0]
190
+ sentiment='supportive' if sst['label']=='POSITIVE' else 'undermining'
191
+ sent_score=sst['score'] if sentiment=='undermining' else 0.0
192
  # DARVO
193
  motif_hits, matched = detect_motifs(text)
194
+ contradiction=detect_contradiction(text)
195
+ darvo_score=calculate_darvo_score(labels,0.0,sent_score,matched,contradiction)
196
  # stage + weapon
197
+ stage=get_risk_stage(labels,sentiment)
198
  if detect_weapon_language(text):
199
+ abuse_score=min(abuse_score+25,100)
200
+ stage=max(stage,2)
201
  # top patterns
202
+ top_patterns=sorted(zip(LABELS,probs), key=lambda x:x[1], reverse=True)[:2]
203
+ return {
204
+ "abuse_score":abuse_score, "labels":labels, "sentiment":sentiment,
205
+ "stage":stage, "darvo_score":darvo_score, "top_patterns":top_patterns
206
+ }
207
 
208
+ # ——— Composite Analysis & UI ———
209
+ def analyze_composite(m1,d1,m2,d2,m3,d3,*answers):
210
+ none_sel=(answers[-1] and not any(answers[:-1]))
 
 
211
  if none_sel:
212
+ esc_score=None; risk_level='unknown'
213
  else:
214
+ esc_score=sum(w for (_,w),a in zip(ESCALATION_QUESTIONS,answers[:-1]) if a)
215
+ risk_level='High' if esc_score>=16 else 'Moderate' if esc_score>=8 else 'Low'
216
+ msgs=[m1,m2,m3]; dates=[d1,d2,d3]
217
+ active=[(m,d) for m,d in zip(msgs,dates) if m.strip()]
 
218
  if not active:
219
  return "Please enter at least one message."
220
+ results=[(analyze_single_message(m),d) for m,d in active]
221
+ abuse_scores=[r[0]['abuse_score'] for r in results]
222
+ top_labels=[res['top_patterns'][0][0] if res['top_patterns'] else 'None'
223
+ for res,_ in results]
224
+ dates_used=[d or 'Undated' for _,d in results]
225
+ # common stage & composite abuse
226
+ stages=[r[0]['stage'] for r,_ in results]
227
+ most_common_stage=max(set(stages), key=stages.count)
228
+ composite_abuse=int(round(sum(abuse_scores)/len(abuse_scores)))
 
 
 
 
 
 
 
 
229
  # DARVO summary
230
+ darvos=[r[0]['darvo_score'] for r,_ in results]
231
+ avg_darvo=round(sum(darvos)/len(darvos),3)
232
+ darvo_blurb=f"\n🎭 DARVO Score: {avg_darvo} ({'high' if avg_darvo>=0.65 else 'moderate'})" if avg_darvo>0.25 else ''
233
+ # build output
234
+ out=f"Abuse Intensity: {composite_abuse}%\n"
 
 
 
235
  if esc_score is None:
236
+ out+="Escalation Potential: Unknown (Checklist not completed)\n"
237
  else:
238
+ out+=f"Escalation Potential: {risk_level} ({esc_score}/{sum(w for _,w in ESCALATION_QUESTIONS)})\n"
239
  # risk snippet
240
+ pattern_score=f"{top_labels[0]} – {int(results[0][0]['top_patterns'][0][1]*100)}%" if results[0][0]['top_patterns'] else top_labels[0]
241
+ out+=generate_risk_snippet(composite_abuse, pattern_score, esc_score or 0, most_common_stage)
242
+ out+=darvo_blurb
243
+ img=generate_abuse_score_chart(dates_used,abuse_scores,top_labels)
244
  return out, img
245
 
246
+ # ——— Gradio Interface ———
247
  message_date_pairs = [
248
  (gr.Textbox(label=f"Message {i+1}"), gr.Textbox(label=f"Date {i+1} (optional)", placeholder="YYYY-MM-DD"))
249
  for i in range(3)
250
  ]
 
251
  quiz_boxes = [gr.Checkbox(label=q) for q,_ in ESCALATION_QUESTIONS]
252
  none_box = gr.Checkbox(label="None of the above")
253
+
254
  iface = gr.Interface(
255
  fn=analyze_composite,
256
+ inputs=[item for pair in message_date_pairs for item in pair] + quiz_boxes + [none_box],
257
  outputs=[gr.Textbox(label="Results"), gr.Image(label="Risk Stage Timeline", type="pil")],
258
  title="Tether Abuse Pattern Detector v2",
259
  allow_flagging="manual"
260
  )
261
+
262
  if __name__ == "__main__":
263
+ iface.launch()