SamanthaStorm commited on
Commit
d0a7bb3
·
verified ·
1 Parent(s): 4ded559

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1240
app.py DELETED
@@ -1,1240 +0,0 @@
1
- import gradio as gr
2
- import spaces
3
- import torch
4
- import numpy as np
5
- from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline as hf_pipeline
6
- import re
7
- import matplotlib.pyplot as plt
8
- import io
9
- from PIL import Image
10
- from datetime import datetime
11
- from torch.nn.functional import sigmoid
12
- from collections import Counter
13
- import logging
14
- import traceback
15
-
16
-
17
- # Set up logging
18
- logging.basicConfig(level=logging.DEBUG)
19
- logger = logging.getLogger(__name__)
20
-
21
- # Device configuration
22
- device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
23
- logger.info(f"Using device: {device}")
24
- # Set up custom logging
25
- # Set up custom logging
26
- class CustomFormatter(logging.Formatter):
27
- """Custom formatter with colors and better formatting"""
28
- grey = "\x1b[38;21m"
29
- blue = "\x1b[38;5;39m"
30
- yellow = "\x1b[38;5;226m"
31
- red = "\x1b[38;5;196m"
32
- bold_red = "\x1b[31;1m"
33
- reset = "\x1b[0m"
34
-
35
- def format(self, record):
36
- # Remove the logger name from the output
37
- if record.levelno == logging.DEBUG:
38
- return f"{self.blue}{record.getMessage()}{self.reset}"
39
- elif record.levelno == logging.INFO:
40
- return f"{self.grey}{record.getMessage()}{self.reset}"
41
- elif record.levelno == logging.WARNING:
42
- return f"{self.yellow}{record.getMessage()}{self.reset}"
43
- elif record.levelno == logging.ERROR:
44
- return f"{self.red}{record.getMessage()}{self.reset}"
45
- elif record.levelno == logging.CRITICAL:
46
- return f"{self.bold_red}{record.getMessage()}{self.reset}"
47
- return record.getMessage()
48
-
49
- # Setup logger
50
- logger = logging.getLogger(__name__)
51
- logger.setLevel(logging.DEBUG)
52
-
53
- # Remove any existing handlers
54
- logger.handlers = []
55
-
56
- # Create console handler with custom formatter
57
- ch = logging.StreamHandler()
58
- ch.setLevel(logging.DEBUG)
59
- ch.setFormatter(CustomFormatter())
60
- logger.addHandler(ch)
61
-
62
- # Suppress matplotlib font debugging
63
- matplotlib_logger = logging.getLogger('matplotlib.font_manager')
64
- matplotlib_logger.setLevel(logging.WARNING)
65
-
66
- # Also suppress the UserWarning about tight layout
67
- import warnings
68
- warnings.filterwarnings("ignore", message="Tight layout not applied")
69
-
70
-
71
- # Model initialization
72
- model_name = "SamanthaStorm/tether-multilabel-v4"
73
- model = AutoModelForSequenceClassification.from_pretrained(model_name).to(device)
74
- tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
75
-
76
- # sentiment model - add no_cache=True and force_download=True
77
- # Model initialization
78
- model_name = "SamanthaStorm/tether-multilabel-v4"
79
- model = AutoModelForSequenceClassification.from_pretrained(model_name).to(device)
80
- tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
81
-
82
- # sentiment model
83
- sentiment_model = AutoModelForSequenceClassification.from_pretrained(
84
- "SamanthaStorm/tether-sentiment-v3",
85
- force_download=True,
86
- local_files_only=False
87
- ).to(device)
88
- sentiment_tokenizer = AutoTokenizer.from_pretrained(
89
- "SamanthaStorm/tether-sentiment-v3",
90
- use_fast=False,
91
- force_download=True,
92
- local_files_only=False
93
- )
94
-
95
- # After loading the sentiment model
96
- logger.debug(f"\nSentiment Model Config:")
97
- logger.debug(f"Model name: {sentiment_model.config.name_or_path}")
98
- logger.debug(f"Last modified: {sentiment_model.config._name_or_path}")
99
-
100
-
101
-
102
- emotion_pipeline = hf_pipeline(
103
- "text-classification",
104
- model="j-hartmann/emotion-english-distilroberta-base",
105
- return_all_scores=True, # Get all emotion scores
106
- top_k=None, # Don't limit to top k predictions
107
- truncation=True,
108
- device=0 if torch.cuda.is_available() else -1
109
- )
110
- # DARVO model
111
- darvo_model = AutoModelForSequenceClassification.from_pretrained("SamanthaStorm/tether-darvo-regressor-v1").to(device)
112
- darvo_tokenizer = AutoTokenizer.from_pretrained("SamanthaStorm/tether-darvo-regressor-v1", use_fast=False)
113
- darvo_model.eval()
114
-
115
- # Constants and Labels
116
- LABELS = [
117
- "recovery phase", "control", "gaslighting", "guilt tripping", "dismissiveness",
118
- "blame shifting", "nonabusive", "projection", "insults",
119
- "contradictory statements", "obscure language"
120
- ]
121
-
122
- SENTIMENT_LABELS = ["supportive", "undermining"]
123
-
124
- THRESHOLDS = {
125
- "recovery phase": 0.324,
126
- "control": 0.33,
127
- "gaslighting": 0.285,
128
- "guilt tripping": 0.267,
129
- "dismissiveness": 0.123,
130
- "blame shifting": 0.116,
131
- "projection": 0.425,
132
- "insults": 0.347,
133
- "contradictory statements": 0.378,
134
- "obscure language": 0.206,
135
- "nonabusive": 0.094
136
- }
137
-
138
-
139
- PATTERN_WEIGHTS = {
140
- "recovery phase": 0.7,
141
- "control": 1.4,
142
- "gaslighting": 1.3,
143
- "guilt tripping": 1.2,
144
- "dismissiveness": 0.9,
145
- "blame shifting": 1.0, # Increased from 0.8
146
- "projection": 0.5,
147
- "insults": 1.4, # Reduced from 2.1
148
- "contradictory statements": 1.0,
149
- "obscure language": 0.9,
150
- "nonabusive": 0.0
151
- }
152
- ESCALATION_QUESTIONS = [
153
- ("Partner has access to firearms or weapons", 4),
154
- ("Partner threatened to kill you", 3),
155
- ("Partner threatened you with a weapon", 3),
156
- ("Partner has ever choked you, even if you considered it consensual at the time", 4),
157
- ("Partner injured or threatened your pet(s)", 3),
158
- ("Partner has broken your things, punched or kicked walls, or thrown things ", 2),
159
- ("Partner forced or coerced you into unwanted sexual acts", 3),
160
- ("Partner threatened to take away your children", 2),
161
- ("Violence has increased in frequency or severity", 3),
162
- ("Partner monitors your calls/GPS/social media", 2)
163
- ]
164
-
165
- RISK_STAGE_LABELS = {
166
- 1: "🌀 Risk Stage: Tension-Building\nThis message reflects rising emotional pressure or subtle control attempts.",
167
- 2: "🔥 Risk Stage: Escalation\nThis message includes direct or aggressive patterns, suggesting active harm.",
168
- 3: "🌧️ Risk Stage: Reconciliation\nThis message reflects a reset attempt—apologies or emotional repair without accountability.",
169
- 4: "🌸 Risk Stage: Calm / Honeymoon\nThis message appears supportive but may follow prior harm, minimizing it."
170
- }
171
-
172
- THREAT_MOTIFS = [
173
- "i'll kill you", "i'm going to hurt you", "you're dead", "you won't survive this",
174
- "i'll break your face", "i'll bash your head in", "i'll snap your neck",
175
- "i'll come over there and make you shut up", "i'll knock your teeth out",
176
- "you're going to bleed", "you want me to hit you?", "i won't hold back next time",
177
- "i swear to god i'll beat you", "next time, i won't miss", "i'll make you scream",
178
- "i know where you live", "i'm outside", "i'll be waiting", "i saw you with him",
179
- "you can't hide from me", "i'm coming to get you", "i'll find you", "i know your schedule",
180
- "i watched you leave", "i followed you home", "you'll regret this", "you'll be sorry",
181
- "you're going to wish you hadn't", "you brought this on yourself", "don't push me",
182
- "you have no idea what i'm capable of", "you better watch yourself",
183
- "i don't care what happens to you anymore", "i'll make you suffer", "you'll pay for this",
184
- "i'll never let you go", "you're nothing without me", "if you leave me, i'll kill myself",
185
- "i'll ruin you", "i'll tell everyone what you did", "i'll make sure everyone knows",
186
- "i'm going to destroy your name", "you'll lose everyone", "i'll expose you",
187
- "your friends will hate you", "i'll post everything", "you'll be cancelled",
188
- "you'll lose everything", "i'll take the house", "i'll drain your account",
189
- "you'll never see a dime", "you'll be broke when i'm done", "i'll make sure you lose your job",
190
- "i'll take your kids", "i'll make sure you have nothing", "you can't afford to leave me",
191
- "don't make me do this", "you know what happens when i'm mad", "you're forcing my hand",
192
- "if you just behaved, this wouldn't happen", "this is your fault",
193
- "you're making me hurt you", "i warned you", "you should have listened"
194
- ]
195
-
196
- def get_emotion_profile(text):
197
- """Get emotion profile from text with all scores"""
198
- try:
199
- logger.debug("\n🎭 EMOTION ANALYSIS")
200
- logger.debug(f"Analyzing text: {text}")
201
-
202
- emotions = emotion_pipeline(text)
203
- logger.debug(f"Raw emotion pipeline output: {emotions}")
204
-
205
- if isinstance(emotions, list) and isinstance(emotions[0], list):
206
- # Extract all scores from the first prediction
207
- emotion_scores = emotions[0]
208
-
209
- # Log raw scores
210
- logger.debug("\nRaw emotion scores:")
211
- for e in emotion_scores:
212
- logger.debug(f" • {e['label']}: {e['score']:.3f}")
213
-
214
- # Convert to dictionary
215
- emotion_dict = {e['label'].lower(): round(e['score'], 3) for e in emotion_scores}
216
-
217
- # Log final processed emotions
218
- logger.debug("\nProcessed emotion profile:")
219
- for emotion, score in emotion_dict.items():
220
- logger.debug(f" • {emotion}: {score:.3f}")
221
-
222
- return emotion_dict
223
-
224
- logger.debug("No valid emotions detected, returning empty dict")
225
- return {}
226
-
227
- except Exception as e:
228
- logger.error(f"Error in get_emotion_profile: {e}")
229
- logger.error(f"Traceback: {traceback.format_exc()}")
230
- default_emotions = {
231
- "sadness": 0.0,
232
- "joy": 0.0,
233
- "neutral": 0.0,
234
- "disgust": 0.0,
235
- "anger": 0.0,
236
- "fear": 0.0
237
- }
238
- logger.debug(f"Returning default emotions: {default_emotions}")
239
- return default_emotions
240
-
241
-
242
- def get_emotional_tone_tag(text, sentiment, patterns, abuse_score):
243
- """Get emotional tone tag based on emotions and patterns"""
244
- emotions = get_emotion_profile(text)
245
-
246
- sadness = emotions.get("sadness", 0)
247
- joy = emotions.get("joy", 0)
248
- neutral = emotions.get("neutral", 0)
249
- disgust = emotions.get("disgust", 0)
250
- anger = emotions.get("anger", 0)
251
- fear = emotions.get("fear", 0)
252
-
253
- # Direct Threat (New)
254
- text_lower = text.lower()
255
- threat_indicators = [
256
- "if you", "i'll make", "don't forget", "remember", "regret",
257
- "i control", "i'll take", "you'll lose", "make sure",
258
- "never see", "won't let"
259
- ]
260
- if (
261
- any(indicator in text_lower for indicator in threat_indicators) and
262
- any(p in patterns for p in ["control", "insults"]) and
263
- (anger > 0.2 or disgust > 0.2 or abuse_score > 70)
264
- ):
265
- return "direct threat"
266
-
267
- # Prophetic Punishment (New)
268
- text_lower = text.lower()
269
- future_consequences = [
270
- "will end up", "you'll be", "you will be", "going to be",
271
- "will become", "will find yourself", "will realize",
272
- "you'll regret", "you'll see", "will learn", "truly will",
273
- "end up alone", "end up miserable"
274
- ]
275
- dismissive_endings = [
276
- "i'm out", "i'm done", "whatever", "good luck",
277
- "your choice", "your problem", "regardless",
278
- "keep", "keep on"
279
- ]
280
-
281
- if (
282
- (any(phrase in text_lower for phrase in future_consequences) or
283
- any(end in text_lower for end in dismissive_endings)) and
284
- any(p in ["dismissiveness", "control"] for p in patterns) and
285
- (disgust > 0.2 or neutral > 0.3 or anger > 0.2) # Lowered thresholds
286
- ):
287
- return "predictive punishment"
288
-
289
- if (
290
- (any(phrase in text_lower for phrase in future_consequences) or
291
- any(end in text_lower for end in dismissive_endings)) and
292
- any(p in ["dismissiveness", "control"] for p in patterns) and
293
- sadness > 0.6 and
294
- all(e < 0.1 for e in [anger, disgust, neutral])
295
- ):
296
- return "predictive punishment"
297
-
298
- # 1. Performative Regret
299
- if (
300
- sadness > 0.3 and
301
- any(p in patterns for p in ["blame shifting", "guilt tripping", "recovery"]) and
302
- (sentiment == "undermining" or abuse_score > 40)
303
- ):
304
- return "performative regret"
305
-
306
- # 2. Coercive Warmth
307
- if (
308
- (joy > 0.2 or sadness > 0.3) and
309
- any(p in patterns for p in ["control", "gaslighting"]) and
310
- sentiment == "undermining"
311
- ):
312
- return "coercive warmth"
313
-
314
- # 3. Cold Invalidation
315
- if (
316
- (neutral + disgust) > 0.4 and
317
- any(p in patterns for p in ["dismissiveness", "projection", "obscure language"]) and
318
- sentiment == "undermining"
319
- ):
320
- return "cold invalidation"
321
-
322
- # 4. Genuine Vulnerability
323
- if (
324
- (sadness + fear) > 0.4 and
325
- sentiment == "supportive" and
326
- all(p in ["recovery"] for p in patterns)
327
- ):
328
- return "genuine vulnerability"
329
-
330
- # 5. Emotional Threat
331
- if (
332
- (anger + disgust) > 0.4 and
333
- any(p in patterns for p in ["control", "insults", "dismissiveness"]) and
334
- sentiment == "undermining"
335
- ):
336
- return "emotional threat"
337
-
338
- # 6. Weaponized Sadness
339
- if (
340
- sadness > 0.5 and
341
- any(p in patterns for p in ["guilt tripping", "projection"]) and
342
- sentiment == "undermining"
343
- ):
344
- return "weaponized sadness"
345
-
346
- # 7. Toxic Resignation
347
- if (
348
- neutral > 0.4 and
349
- any(p in patterns for p in ["dismissiveness", "obscure language"]) and
350
- sentiment == "undermining"
351
- ):
352
- return "toxic resignation"
353
-
354
- # 8. Aggressive Dismissal
355
- if (
356
- anger > 0.4 and
357
- any(p in patterns for p in ["insults", "control"]) and
358
- sentiment == "undermining"
359
- ):
360
- return "aggressive dismissal"
361
-
362
- # 9. Deflective Hostility
363
- if (
364
- (0.15 < anger < 0.6 or 0.15 < disgust < 0.6) and
365
- any(p in patterns for p in ["projection"]) and
366
- sentiment == "undermining"
367
- ):
368
- return "deflective hostility"
369
-
370
- # 10. Contradictory Gaslight
371
- if (
372
- (joy + anger + sadness) > 0.4 and
373
- any(p in patterns for p in ["gaslighting", "contradictory statements"]) and
374
- sentiment == "undermining"
375
- ):
376
- return "contradictory gaslight"
377
-
378
- # 11. Forced Accountability Flip
379
- if (
380
- (anger + disgust) > 0.4 and
381
- any(p in patterns for p in ["blame shifting", "projection"]) and
382
- sentiment == "undermining"
383
- ):
384
- return "forced accountability flip"
385
-
386
- # Emotional Instability Fallback
387
- if (
388
- (anger + sadness + disgust) > 0.5 and
389
- sentiment == "undermining"
390
- ):
391
- return "emotional instability"
392
-
393
- return "neutral"
394
- @spaces.GPU
395
- def predict_darvo_score(text):
396
- """Predict DARVO score for given text"""
397
- try:
398
- inputs = darvo_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
399
- inputs = {k: v.to(device) for k, v in inputs.items()}
400
- with torch.no_grad():
401
- logits = darvo_model(**inputs).logits
402
- return round(sigmoid(logits.cpu()).item(), 4)
403
- except Exception as e:
404
- logger.error(f"Error in DARVO prediction: {e}")
405
- return 0.0
406
-
407
- def detect_weapon_language(text):
408
- """Detect weapon-related language in text"""
409
- weapon_keywords = ["knife", "gun", "bomb", "weapon", "kill", "stab"]
410
- t = text.lower()
411
- return any(w in t for w in weapon_keywords)
412
-
413
- def get_risk_stage(patterns, sentiment):
414
- """Determine risk stage based on patterns and sentiment"""
415
- try:
416
- if "insults" in patterns:
417
- return 2
418
- elif "recovery" in patterns:
419
- return 3
420
- elif "control" in patterns or "guilt tripping" in patterns:
421
- return 1
422
- elif sentiment == "supportive" and any(p in patterns for p in ["projection", "dismissiveness"]):
423
- return 4
424
- return 1
425
- except Exception as e:
426
- logger.error(f"Error determining risk stage: {e}")
427
- return 1
428
- def detect_threat_pattern(text, patterns):
429
- """Detect if a message contains threat patterns"""
430
- # Threat indicators in text
431
- threat_words = [
432
- "regret", "sorry", "pay", "hurt", "suffer", "destroy", "ruin",
433
- "expose", "tell everyone", "never see", "take away", "lose",
434
- "control", "make sure", "won't let", "force", "warn", "never",
435
- "punish", "teach you", "learn", "show you", "remember",
436
- "if you", "don't forget", "i control", "i'll make sure", # Added these specific phrases
437
- "bank account", "phone", "money", "access" # Added financial control indicators
438
- ]
439
-
440
- # Check for conditional threats (if/then structures)
441
- text_lower = text.lower()
442
- conditional_threat = (
443
- "if" in text_lower and
444
- any(word in text_lower for word in ["regret", "make sure", "control"])
445
- )
446
-
447
- has_threat_words = any(word in text_lower for word in threat_words)
448
-
449
- # Check for threat patterns
450
- threat_patterns = {"control", "gaslighting", "blame shifting", "insults"}
451
- has_threat_patterns = any(p in threat_patterns for p in patterns)
452
-
453
- return has_threat_words or has_threat_patterns or conditional_threat
454
-
455
-
456
-
457
- def detect_compound_threat(text, patterns):
458
- """Detect compound threats in a single message"""
459
- try:
460
- # Rule A: Single Message Multiple Patterns
461
- high_risk_patterns = {"control", "gaslighting", "blame shifting", "insults"}
462
- high_risk_count = sum(1 for p in patterns if p in high_risk_patterns)
463
-
464
- has_threat = detect_threat_pattern(text, patterns)
465
-
466
- # Special case for control + threats
467
- has_control = "control" in patterns
468
- has_conditional_threat = "if" in text.lower() and any(word in text.lower()
469
- for word in ["regret", "make sure", "control"])
470
-
471
- # Single message compound threat
472
- if (has_threat and high_risk_count >= 2) or (has_control and has_conditional_threat):
473
- return True, "single_message"
474
-
475
- return False, None
476
- except Exception as e:
477
- logger.error(f"Error in compound threat detection: {e}")
478
- return False, None
479
-
480
-
481
- def analyze_message_batch_threats(messages, results):
482
- """Analyze multiple messages for compound threats"""
483
- threat_messages = []
484
- support_messages = []
485
-
486
- for i, (msg, (result, _)) in enumerate(zip(messages, results)):
487
- if not msg.strip(): # Skip empty messages
488
- continue
489
-
490
- patterns = result[1] # Get detected patterns
491
-
492
- # Check for threat in this message
493
- if detect_threat_pattern(msg, patterns):
494
- threat_messages.append(i)
495
-
496
- # Check for supporting patterns
497
- if any(p in {"control", "gaslighting", "blame shifting"} for p in patterns):
498
- support_messages.append(i)
499
-
500
- # Rule B: Multi-Message Accumulation
501
- if len(threat_messages) >= 2:
502
- return True, "multiple_threats"
503
- elif len(threat_messages) == 1 and len(support_messages) >= 2:
504
- return True, "threat_with_support"
505
-
506
- return False, None
507
-
508
-
509
- @spaces.GPU
510
- def compute_abuse_score(matched_scores, sentiment):
511
- """Compute abuse score from matched patterns and sentiment"""
512
- try:
513
- if not matched_scores:
514
- logger.debug("No matched scores, returning 0")
515
- return 0.0
516
-
517
- # Calculate weighted score
518
- total_weight = sum(weight for _, _, weight in matched_scores)
519
- if total_weight == 0:
520
- logger.debug("Total weight is 0, returning 0")
521
- return 0.0
522
-
523
- # Get highest pattern scores
524
- pattern_scores = [(label, score) for label, score, _ in matched_scores]
525
- sorted_scores = sorted(pattern_scores, key=lambda x: x[1], reverse=True)
526
- logger.debug(f"Sorted pattern scores: {sorted_scores}")
527
-
528
- # Base score calculation
529
- weighted_sum = sum(score * weight for _, score, weight in matched_scores)
530
- base_score = (weighted_sum / total_weight) * 100
531
- logger.debug(f"Initial base score: {base_score:.1f}")
532
-
533
- # Cap maximum score based on pattern severity
534
- max_score = 85.0 # Set maximum possible score
535
- if any(label in {'control', 'gaslighting'} for label, _, _ in matched_scores):
536
- max_score = 90.0
537
- logger.debug(f"Increased max score to {max_score} due to high severity patterns")
538
-
539
- # Apply diminishing returns for multiple patterns
540
- if len(matched_scores) > 1:
541
- multiplier = 1 + (0.1 * (len(matched_scores) - 1))
542
- base_score *= multiplier
543
- logger.debug(f"Applied multiplier {multiplier:.2f} for {len(matched_scores)} patterns")
544
-
545
- # Apply sentiment modifier
546
- if sentiment == "supportive":
547
- base_score *= 0.85
548
- logger.debug("Applied 15% reduction for supportive sentiment")
549
-
550
- final_score = min(round(base_score, 1), max_score)
551
- logger.debug(f"Final abuse score: {final_score}")
552
- return final_score
553
-
554
- except Exception as e:
555
- logger.error(f"Error computing abuse score: {e}")
556
- return 0.0
557
-
558
-
559
- @spaces.GPU
560
- def analyze_single_message(text, thresholds):
561
- """Analyze a single message for abuse patterns"""
562
- logger.debug("\n=== DEBUG START ===")
563
- logger.debug(f"Input text: {text}")
564
-
565
- try:
566
- if not text.strip():
567
- logger.debug("Empty text, returning zeros")
568
- return 0.0, [], [], {"label": "none"}, 1, 0.0, None
569
-
570
- # Check for explicit abuse
571
- explicit_abuse_words = ['fuck', 'bitch', 'shit', 'ass', 'dick']
572
- explicit_abuse = any(word in text.lower() for word in explicit_abuse_words)
573
- logger.debug(f"Explicit abuse detected: {explicit_abuse}")
574
-
575
- # Abuse model inference
576
- inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
577
- inputs = {k: v.to(device) for k, v in inputs.items()}
578
-
579
- with torch.no_grad():
580
- outputs = model(**inputs)
581
- raw_scores = torch.sigmoid(outputs.logits.squeeze(0)).cpu().numpy()
582
-
583
- # Log raw model outputs
584
- logger.debug("\nRaw model scores:")
585
- for label, score in zip(LABELS, raw_scores):
586
- logger.debug(f"{label}: {score:.3f}")
587
-
588
- # Get predictions and sort them
589
- predictions = list(zip(LABELS, raw_scores))
590
- sorted_predictions = sorted(predictions, key=lambda x: x[1], reverse=True)
591
- logger.debug("\nTop 3 predictions:")
592
- for label, score in sorted_predictions[:3]:
593
- logger.debug(f"{label}: {score:.3f}")
594
-
595
- # Apply thresholds
596
- threshold_labels = []
597
- if explicit_abuse:
598
- threshold_labels.append("insults")
599
- logger.debug("\nForced inclusion of 'insults' due to explicit abuse")
600
-
601
- for label, score in sorted_predictions:
602
- base_threshold = thresholds.get(label, 0.25)
603
- if explicit_abuse:
604
- base_threshold *= 0.5
605
- if score > base_threshold:
606
- if label not in threshold_labels:
607
- threshold_labels.append(label)
608
-
609
- logger.debug("\nLabels that passed thresholds:", threshold_labels)
610
-
611
- # Calculate matched scores
612
- matched_scores = []
613
- for label in threshold_labels:
614
- score = raw_scores[LABELS.index(label)]
615
- weight = PATTERN_WEIGHTS.get(label, 1.0)
616
- if explicit_abuse and label == "insults":
617
- weight *= 1.5
618
- matched_scores.append((label, score, weight))
619
-
620
- # In analyze_single_message, modify the sentiment section:
621
- # Get sentiment
622
- sent_inputs = sentiment_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
623
- sent_inputs = {k: v.to(device) for k, v in sent_inputs.items()}
624
- with torch.no_grad():
625
- sent_logits = sentiment_model(**sent_inputs).logits[0]
626
- sent_probs = torch.softmax(sent_logits, dim=-1).cpu().numpy()
627
-
628
- # Add detailed logging
629
- logger.debug("\n🎭 SENTIMENT ANALYSIS DETAILS")
630
- logger.debug(f"Raw logits: {sent_logits}")
631
- logger.debug(f"Probabilities: undermining={sent_probs[0]:.3f}, supportive={sent_probs[1]:.3f}")
632
-
633
- sentiment = SENTIMENT_LABELS[int(np.argmax(sent_probs))]
634
- logger.debug(f"Selected sentiment: {sentiment}")
635
-
636
- # Calculate abuse score
637
- abuse_score = compute_abuse_score(matched_scores, sentiment)
638
- if explicit_abuse:
639
- abuse_score = max(abuse_score, 70.0)
640
-
641
- # Check for compound threats
642
- compound_threat_flag, threat_type = detect_compound_threat(
643
- text, threshold_labels
644
- )
645
-
646
- if compound_threat_flag:
647
- logger.debug(f"⚠️ Compound threat detected in message: {threat_type}")
648
- abuse_score = max(abuse_score, 85.0) # Force high score for compound threats
649
-
650
-
651
- # Get DARVO score
652
- darvo_score = predict_darvo_score(text)
653
-
654
- # Get tone using emotion-based approach
655
- tone_tag = get_emotional_tone_tag(text, sentiment, threshold_labels, abuse_score)
656
- # Check for the specific combination
657
- highest_pattern = max(matched_scores, key=lambda x: x[1])[0] if matched_scores else None # Get highest pattern
658
- if sentiment == "supportive" and tone_tag == "neutral" and highest_pattern == "obscure language":
659
- logger.debug("Message classified as likely non-abusive (supportive, neutral, and obscure language). Returning low risk.")
660
- return 0.0, [], [], {"label": "supportive"}, 1, 0.0, "neutral" # Return non-abusive values
661
-
662
- # Set stage
663
- stage = 2 if explicit_abuse or abuse_score > 70 else 1
664
-
665
- logger.debug("=== DEBUG END ===\n")
666
-
667
- return abuse_score, threshold_labels, matched_scores, {"label": sentiment}, stage, darvo_score, tone_tag
668
-
669
- except Exception as e:
670
- logger.error(f"Error in analyze_single_message: {e}")
671
- return 0.0, [], [], {"label": "error"}, 1, 0.0, None
672
- def generate_abuse_score_chart(dates, scores, patterns):
673
- """Generate a timeline chart of abuse scores"""
674
- try:
675
- plt.figure(figsize=(10, 6))
676
- plt.clf()
677
-
678
- # Create new figure
679
- fig, ax = plt.subplots(figsize=(10, 6))
680
-
681
- # Plot points and lines
682
- x = range(len(scores))
683
- plt.plot(x, scores, 'bo-', linewidth=2, markersize=8)
684
-
685
- # Add labels for each point with highest scoring pattern
686
- for i, (score, pattern) in enumerate(zip(scores, patterns)):
687
- # Get the pattern and its score
688
- plt.annotate(
689
- f'{pattern}\n{score:.0f}%',
690
- (i, score),
691
- textcoords="offset points",
692
- xytext=(0, 10),
693
- ha='center',
694
- bbox=dict(
695
- boxstyle='round,pad=0.5',
696
- fc='white',
697
- ec='gray',
698
- alpha=0.8
699
- )
700
- )
701
-
702
- # Customize the plot
703
- plt.ylim(-5, 105)
704
- plt.grid(True, linestyle='--', alpha=0.7)
705
- plt.title('Abuse Pattern Timeline', pad=20, fontsize=12)
706
- plt.ylabel('Abuse Score %')
707
-
708
- # X-axis labels
709
- plt.xticks(x, dates, rotation=45)
710
-
711
- # Risk level bands with better colors
712
- plt.axhspan(0, 50, color='#90EE90', alpha=0.2) # light green - Low Risk
713
- plt.axhspan(50, 70, color='#FFD700', alpha=0.2) # gold - Moderate Risk
714
- plt.axhspan(70, 85, color='#FFA500', alpha=0.2) # orange - High Risk
715
- plt.axhspan(85, 100, color='#FF6B6B', alpha=0.2) # light red - Critical Risk
716
-
717
- # Add risk level labels
718
- plt.text(-0.2, 25, 'Low Risk', rotation=90, va='center')
719
- plt.text(-0.2, 60, 'Moderate Risk', rotation=90, va='center')
720
- plt.text(-0.2, 77.5, 'High Risk', rotation=90, va='center')
721
- plt.text(-0.2, 92.5, 'Critical Risk', rotation=90, va='center')
722
-
723
- # Adjust layout
724
- plt.tight_layout()
725
-
726
- # Convert plot to image
727
- buf = io.BytesIO()
728
- plt.savefig(buf, format='png', bbox_inches='tight')
729
- buf.seek(0)
730
- plt.close('all') # Close all figures to prevent memory leaks
731
-
732
- return Image.open(buf)
733
- except Exception as e:
734
- logger.error(f"Error generating abuse score chart: {e}")
735
- return None
736
-
737
-
738
- def analyze_composite(msg1, msg2, msg3, *answers_and_none):
739
- """Analyze multiple messages and checklist responses"""
740
- logger.debug("\n🔄 STARTING NEW ANALYSIS")
741
- logger.debug("=" * 50)
742
-
743
- # Define severity categories at the start
744
- high = {'control'}
745
- moderate = {'gaslighting', 'dismissiveness', 'obscure language', 'insults',
746
- 'contradictory statements', 'guilt tripping'}
747
- low = {'blame shifting', 'projection', 'recovery'}
748
-
749
- try:
750
- # Process checklist responses
751
- logger.debug("\n📋 CHECKLIST PROCESSING")
752
- logger.debug("=" * 50)
753
- none_selected_checked = answers_and_none[-1]
754
- responses_checked = any(answers_and_none[:-1])
755
- none_selected = not responses_checked and none_selected_checked
756
-
757
- logger.debug("Checklist Status:")
758
- logger.debug(f" • None Selected Box: {'✓' if none_selected_checked else '✗'}")
759
- logger.debug(f" • Has Responses: {'✓' if responses_checked else '✗'}")
760
- logger.debug(f" • Final Status: {'None Selected' if none_selected else 'Has Selections'}")
761
-
762
- if none_selected:
763
- escalation_score = 0
764
- escalation_note = "Checklist completed: no danger items reported."
765
- escalation_completed = True
766
- logger.debug("\n✓ Checklist: No items selected")
767
- elif responses_checked:
768
- escalation_score = sum(w for (_, w), a in zip(ESCALATION_QUESTIONS, answers_and_none[:-1]) if a)
769
- escalation_note = "Checklist completed."
770
- escalation_completed = True
771
- logger.debug(f"\n📊 Checklist Score: {escalation_score}")
772
-
773
- # Log checked items
774
- logger.debug("\n⚠️ Selected Risk Factors:")
775
- for (q, w), a in zip(ESCALATION_QUESTIONS, answers_and_none[:-1]):
776
- if a:
777
- logger.debug(f" • [{w} points] {q}")
778
- else:
779
- escalation_score = None
780
- escalation_note = "Checklist not completed."
781
- escalation_completed = False
782
- logger.debug("\n❗ Checklist: Not completed")
783
-
784
- # Process messages
785
- logger.debug("\n📝 MESSAGE PROCESSING")
786
- logger.debug("=" * 50)
787
- messages = [msg1, msg2, msg3]
788
- active = [(m, f"Message {i+1}") for i, m in enumerate(messages) if m.strip()]
789
- logger.debug(f"Active Messages: {len(active)} of 3")
790
-
791
- if not active:
792
- logger.debug("❌ Error: No messages provided")
793
- return "Please enter at least one message.", None
794
-
795
- # Detect threats
796
- logger.debug("\n🚨 THREAT DETECTION")
797
- logger.debug("=" * 50)
798
-
799
- def normalize(text):
800
- import unicodedata
801
- text = text.lower().strip()
802
- text = unicodedata.normalize("NFKD", text)
803
- text = text.replace("'", "'")
804
- return re.sub(r"[^a-z0-9 ]", "", text)
805
-
806
- def detect_threat_motifs(message, motif_list):
807
- norm_msg = normalize(message)
808
- return [motif for motif in motif_list if normalize(motif) in norm_msg]
809
-
810
- # Analyze threats and patterns
811
- immediate_threats = [detect_threat_motifs(m, THREAT_MOTIFS) for m, _ in active]
812
- flat_threats = [t for sublist in immediate_threats for t in sublist]
813
- threat_risk = "Yes" if flat_threats else "No"
814
- # Analyze each message
815
- logger.debug("\n🔍 INDIVIDUAL MESSAGE ANALYSIS")
816
- logger.debug("=" * 50)
817
- results = []
818
- for m, d in active:
819
- logger.debug(f"\n📝 ANALYZING {d}")
820
- logger.debug("-" * 40) # Separator for each message
821
- result = analyze_single_message(m, THRESHOLDS.copy())
822
-
823
- # Check for non-abusive classification and skip further analysis
824
- if result[0] == 0.0 and result[1] == [] and result[3] == {"label": "supportive"} and result[4] == 1 and result[5] == 0.0 and result[6] == "neutral":
825
- logger.debug(f"✓ {d} classified as non-abusive, skipping further analysis.")
826
- # Option to include in final output (uncomment if needed):
827
- # results.append(({"abuse_score": 0.0, "patterns": [], "sentiment": {"label": "supportive"}, "stage": 1, "darvo_score": 0.0, "tone": "neutral"}, d))
828
- continue # Skip to the next message
829
-
830
- results.append((result, d))
831
- # Log the detailed results for the current message (if not skipped)
832
- abuse_score, patterns, matched_scores, sentiment, stage, darvo_score, tone = result
833
- logger.debug(f"\n📊 Results for {d}:")
834
- logger.debug(f" • Abuse Score: {abuse_score:.1f}%")
835
- logger.debug(f" • DARVO Score: {darvo_score:.3f}")
836
- logger.debug(f" • Risk Stage: {stage}")
837
- logger.debug(f" • Sentiment: {sentiment['label']}")
838
- logger.debug(f" • Tone: {tone}")
839
- if patterns:
840
- logger.debug(" • Patterns: " + ", ".join(patterns))
841
-
842
-
843
- # Unpack results for cleaner logging
844
- abuse_score, patterns, matched_scores, sentiment, stage, darvo_score, tone = result
845
-
846
- # Log core metrics
847
- logger.debug("\n📊 CORE METRICS")
848
- logger.debug(f" • Abuse Score: {abuse_score:.1f}%")
849
- logger.debug(f" • DARVO Score: {darvo_score:.3f}")
850
- logger.debug(f" • Risk Stage: {stage}")
851
- logger.debug(f" • Sentiment: {sentiment['label']}")
852
- logger.debug(f" • Tone: {tone}")
853
-
854
- # Log detected patterns with scores
855
- if patterns:
856
- logger.debug("\n🎯 DETECTED PATTERNS")
857
- for label, score, weight in matched_scores:
858
- severity = "❗HIGH" if label in high else "⚠️ MODERATE" if label in moderate else "📝 LOW"
859
- logger.debug(f" • {severity} | {label}: {score:.3f} (weight: {weight})")
860
- else:
861
- logger.debug("\n✓ No abuse patterns detected")
862
-
863
- # Extract scores and metadata
864
- abuse_scores = [r[0][0] for r in results]
865
- stages = [r[0][4] for r in results]
866
- darvo_scores = [r[0][5] for r in results]
867
- tone_tags = [r[0][6] for r in results]
868
- dates_used = [r[1] for r in results]
869
-
870
- # Pattern Analysis Summary
871
- logger.debug("\n📈 PATTERN ANALYSIS SUMMARY")
872
- logger.debug("=" * 50)
873
- predicted_labels = [label for r in results for label in r[0][1]]
874
-
875
- if predicted_labels:
876
- logger.debug("Detected Patterns Across All Messages:")
877
- pattern_counts = Counter(predicted_labels)
878
-
879
- # Log high severity patterns first
880
- high_patterns = [p for p in pattern_counts if p in high]
881
- if high_patterns:
882
- logger.debug("\n❗ HIGH SEVERITY PATTERNS:")
883
- for p in high_patterns:
884
- logger.debug(f" • {p} (×{pattern_counts[p]})")
885
-
886
- # Then moderate
887
- moderate_patterns = [p for p in pattern_counts if p in moderate]
888
- if moderate_patterns:
889
- logger.debug("\n⚠️ MODERATE SEVERITY PATTERNS:")
890
- for p in moderate_patterns:
891
- logger.debug(f" • {p} (×{pattern_counts[p]})")
892
-
893
- # Then low
894
- low_patterns = [p for p in pattern_counts if p in low]
895
- if low_patterns:
896
- logger.debug("\n📝 LOW SEVERITY PATTERNS:")
897
- for p in low_patterns:
898
- logger.debug(f" • {p} (×{pattern_counts[p]})")
899
- else:
900
- logger.debug("✓ No patterns detected across messages")
901
-
902
- # Pattern Severity Analysis
903
- logger.debug("\n⚖️ SEVERITY ANALYSIS")
904
- logger.debug("=" * 50)
905
- counts = {'high': 0, 'moderate': 0, 'low': 0}
906
- for label in predicted_labels:
907
- if label in high:
908
- counts['high'] += 1
909
- elif label in moderate:
910
- counts['moderate'] += 1
911
- elif label in low:
912
- counts['low'] += 1
913
-
914
- logger.debug("Pattern Distribution:")
915
- if counts['high'] > 0:
916
- logger.debug(f" ❗ High Severity: {counts['high']}")
917
- if counts['moderate'] > 0:
918
- logger.debug(f" ⚠️ Moderate Severity: {counts['moderate']}")
919
- if counts['low'] > 0:
920
- logger.debug(f" 📝 Low Severity: {counts['low']}")
921
-
922
- total_patterns = sum(counts.values())
923
- if total_patterns > 0:
924
- logger.debug(f"\nSeverity Percentages:")
925
- logger.debug(f" • High: {(counts['high']/total_patterns)*100:.1f}%")
926
- logger.debug(f" • Moderate: {(counts['moderate']/total_patterns)*100:.1f}%")
927
- logger.debug(f" • Low: {(counts['low']/total_patterns)*100:.1f}%")
928
- # Risk Assessment
929
- logger.debug("\n🎯 RISK ASSESSMENT")
930
- logger.debug("=" * 50)
931
- if counts['high'] >= 2 and counts['moderate'] >= 2:
932
- pattern_escalation_risk = "Critical"
933
- logger.debug("❗❗ CRITICAL RISK")
934
- logger.debug(" • Multiple high and moderate patterns detected")
935
- logger.debug(f" • High patterns: {counts['high']}")
936
- logger.debug(f" • Moderate patterns: {counts['moderate']}")
937
- elif (counts['high'] >= 2 and counts['moderate'] >= 1) or \
938
- (counts['moderate'] >= 3) or \
939
- (counts['high'] >= 1 and counts['moderate'] >= 2):
940
- pattern_escalation_risk = "High"
941
- logger.debug("❗ HIGH RISK")
942
- logger.debug(" • Significant pattern combination detected")
943
- logger.debug(f" • High patterns: {counts['high']}")
944
- logger.debug(f" • Moderate patterns: {counts['moderate']}")
945
- elif (counts['moderate'] == 2) or \
946
- (counts['high'] == 1 and counts['moderate'] == 1) or \
947
- (counts['moderate'] == 1 and counts['low'] >= 2) or \
948
- (counts['high'] == 1 and sum(counts.values()) == 1):
949
- pattern_escalation_risk = "Moderate"
950
- logger.debug("⚠️ MODERATE RISK")
951
- logger.debug(" • Concerning pattern combination detected")
952
- logger.debug(f" • Pattern distribution: H:{counts['high']}, M:{counts['moderate']}, L:{counts['low']}")
953
- else:
954
- pattern_escalation_risk = "Low"
955
- logger.debug("📝 LOW RISK")
956
- logger.debug(" • Limited pattern severity detected")
957
- logger.debug(f" • Pattern distribution: H:{counts['high']}, M:{counts['moderate']}, L:{counts['low']}")
958
-
959
- # Checklist Risk Assessment
960
- logger.debug("\n📋 CHECKLIST RISK ASSESSMENT")
961
- logger.debug("=" * 50)
962
- checklist_escalation_risk = "Unknown" if escalation_score is None else (
963
- "Critical" if escalation_score >= 20 else
964
- "Moderate" if escalation_score >= 10 else
965
- "Low"
966
- )
967
- if escalation_score is not None:
968
- logger.debug(f"Score: {escalation_score}/29")
969
- logger.debug(f"Risk Level: {checklist_escalation_risk}")
970
- if escalation_score >= 20:
971
- logger.debug("❗❗ CRITICAL: Score indicates severe risk")
972
- elif escalation_score >= 10:
973
- logger.debug("⚠️ MODERATE: Score indicates concerning risk")
974
- else:
975
- logger.debug("📝 LOW: Score indicates limited risk")
976
- else:
977
- logger.debug("❓ Risk Level: Unknown (checklist not completed)")
978
-
979
- # Escalation Analysis
980
- logger.debug("\n📈 ESCALATION ANALYSIS")
981
- logger.debug("=" * 50)
982
- escalation_bump = 0
983
- for result, msg_id in results:
984
- abuse_score, _, _, sentiment, stage, darvo_score, tone_tag = result
985
- logger.debug(f"\n🔍 Message {msg_id} Risk Factors:")
986
-
987
- factors = []
988
- if darvo_score > 0.65:
989
- escalation_bump += 3
990
- factors.append(f"▲ +3: High DARVO score ({darvo_score:.3f})")
991
- if tone_tag in ["forced accountability flip", "emotional threat"]:
992
- escalation_bump += 2
993
- factors.append(f"▲ +2: Concerning tone ({tone_tag})")
994
- if abuse_score > 80:
995
- escalation_bump += 2
996
- factors.append(f"▲ +2: High abuse score ({abuse_score:.1f}%)")
997
- if stage == 2:
998
- escalation_bump += 3
999
- factors.append("▲ +3: Escalation stage")
1000
-
1001
- if factors:
1002
- for factor in factors:
1003
- logger.debug(f" {factor}")
1004
- else:
1005
- logger.debug(" ✓ No escalation factors")
1006
-
1007
- logger.debug(f"\n📊 Total Escalation Bump: +{escalation_bump}")
1008
- # Check for compound threats across messages
1009
- compound_threat_flag, threat_type = analyze_message_batch_threats(
1010
- [msg1, msg2, msg3], results
1011
- )
1012
-
1013
- if compound_threat_flag:
1014
- logger.debug(f"⚠️ Compound threat detected across messages: {threat_type}")
1015
- pattern_escalation_risk = "Critical" # Override risk level
1016
- logger.debug("Risk level elevated to CRITICAL due to compound threats")
1017
-
1018
- # Combined Risk Calculation
1019
- logger.debug("\n🎯 FINAL RISK CALCULATION")
1020
- logger.debug("=" * 50)
1021
- def rank(label):
1022
- return {"Low": 0, "Moderate": 1, "High": 2, "Critical": 3, "Unknown": 0}.get(label, 0)
1023
-
1024
- combined_score = rank(pattern_escalation_risk) + rank(checklist_escalation_risk) + escalation_bump
1025
- logger.debug("Risk Components:")
1026
- logger.debug(f" • Pattern Risk ({pattern_escalation_risk}): +{rank(pattern_escalation_risk)}")
1027
- logger.debug(f" • Checklist Risk ({checklist_escalation_risk}): +{rank(checklist_escalation_risk)}")
1028
- logger.debug(f" • Escalation Bump: +{escalation_bump}")
1029
- logger.debug(f" = Combined Score: {combined_score}")
1030
-
1031
- escalation_risk = (
1032
- "Critical" if combined_score >= 6 else
1033
- "High" if combined_score >= 4 else
1034
- "Moderate" if combined_score >= 2 else
1035
- "Low"
1036
- )
1037
- logger.debug(f"\n⚠️ Final Escalation Risk: {escalation_risk}")
1038
- # Generate Output Text
1039
- logger.debug("\n📝 GENERATING OUTPUT")
1040
- logger.debug("=" * 50)
1041
- if escalation_score is None:
1042
- escalation_text = (
1043
- "🚫 **Escalation Potential: Unknown** (Checklist not completed)\n"
1044
- "⚠️ This section was not completed. Escalation potential is estimated using message data only.\n"
1045
- )
1046
- hybrid_score = 0
1047
- logger.debug("Generated output for incomplete checklist")
1048
- elif escalation_score == 0:
1049
- escalation_text = (
1050
- "✅ **Escalation Checklist Completed:** No danger items reported.\n"
1051
- "🧭 **Escalation potential estimated from detected message patterns only.**\n"
1052
- f"• Pattern Risk: {pattern_escalation_risk}\n"
1053
- f"• Checklist Risk: None reported\n"
1054
- f"• Escalation Bump: +{escalation_bump} (from DARVO, tone, intensity, etc.)"
1055
- )
1056
- hybrid_score = escalation_bump
1057
- logger.debug("Generated output for no-risk checklist")
1058
- else:
1059
- hybrid_score = escalation_score + escalation_bump
1060
- escalation_text = (
1061
- f"📈 **Escalation Potential: {escalation_risk} ({hybrid_score}/29)**\n"
1062
- "📋 This score combines your safety checklist answers *and* detected high-risk behavior.\n"
1063
- f"• Pattern Risk: {pattern_escalation_risk}\n"
1064
- f"• Checklist Risk: {checklist_escalation_risk}\n"
1065
- f"• Escalation Bump: +{escalation_bump} (from DARVO, tone, intensity, etc.)"
1066
- )
1067
- logger.debug(f"Generated output with hybrid score: {hybrid_score}/29")
1068
-
1069
- # Final Metrics
1070
- logger.debug("\n📊 FINAL METRICS")
1071
- logger.debug("=" * 50)
1072
- composite_abuse = int(round(sum(abuse_scores) / len(abuse_scores)))
1073
- logger.debug(f"Composite Abuse Score: {composite_abuse}%")
1074
-
1075
- most_common_stage = max(set(stages), key=stages.count)
1076
- logger.debug(f"Most Common Stage: {most_common_stage}")
1077
-
1078
- avg_darvo = round(sum(darvo_scores) / len(darvo_scores), 3)
1079
- logger.debug(f"Average DARVO Score: {avg_darvo}")
1080
-
1081
- # Generate Final Report
1082
- logger.debug("\n📄 GENERATING FINAL REPORT")
1083
- logger.debug("=" * 50)
1084
- out = f"Abuse Intensity: {composite_abuse}%\n"
1085
- # Add detected patterns to output
1086
- if predicted_labels:
1087
- out += "🔍 Detected Patterns:\n"
1088
- if high_patterns:
1089
- patterns_str = ", ".join(f"{p} ({pattern_counts[p]}x)" for p in high_patterns)
1090
- out += f"❗ High Severity: {patterns_str}\n"
1091
- if moderate_patterns:
1092
- patterns_str = ", ".join(f"{p} ({pattern_counts[p]}x)" for p in moderate_patterns)
1093
- out += f"⚠️ Moderate Severity: {patterns_str}\n"
1094
- if low_patterns:
1095
- patterns_str = ", ".join(f"{p} ({pattern_counts[p]}x)" for p in low_patterns)
1096
- out += f"📝 Low Severity: {patterns_str}\n"
1097
- out += "\n"
1098
-
1099
- out += "📊 This reflects the strength and severity of detected abuse patterns in the message(s).\n\n"
1100
-
1101
-
1102
- # Risk Level Assessment
1103
- risk_level = (
1104
- "Critical" if composite_abuse >= 85 or hybrid_score >= 20 else
1105
- "High" if composite_abuse >= 70 or hybrid_score >= 15 else
1106
- "Moderate" if composite_abuse >= 50 or hybrid_score >= 10 else
1107
- "Low"
1108
- )
1109
- logger.debug(f"Final Risk Level: {risk_level}")
1110
-
1111
- # Add Risk Description
1112
- risk_descriptions = {
1113
- "Critical": (
1114
- "🚨 **Risk Level: Critical**\n"
1115
- "Multiple severe abuse patterns detected. This situation shows signs of "
1116
- "dangerous escalation and immediate intervention may be needed."
1117
- ),
1118
- "High": (
1119
- "⚠️ **Risk Level: High**\n"
1120
- "Strong abuse patterns detected. This situation shows concerning "
1121
- "signs of manipulation and control."
1122
- ),
1123
- "Moderate": (
1124
- "⚡ **Risk Level: Moderate**\n"
1125
- "Concerning patterns detected. While not severe, these behaviors "
1126
- "indicate unhealthy relationship dynamics."
1127
- ),
1128
- "Low": (
1129
- "📝 **Risk Level: Low**\n"
1130
- "Minor concerning patterns detected. While present, the detected "
1131
- "behaviors are subtle or infrequent."
1132
- )
1133
- }
1134
-
1135
- out += risk_descriptions[risk_level]
1136
- out += f"\n\n{RISK_STAGE_LABELS[most_common_stage]}"
1137
- logger.debug("Added risk description and stage information")
1138
-
1139
- # Add DARVO Analysis
1140
- if avg_darvo > 0.25:
1141
- level = "moderate" if avg_darvo < 0.65 else "high"
1142
- out += 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."
1143
- logger.debug(f"Added DARVO analysis ({level} level)")
1144
-
1145
- # Add Emotional Tones
1146
- logger.debug("\n🎭 Adding Emotional Tones")
1147
- out += "\n\n🎭 **Emotional Tones Detected:**\n"
1148
- for i, tone in enumerate(tone_tags):
1149
- out += f"• Message {i+1}: *{tone or 'none'}*\n"
1150
- logger.debug(f"Message {i+1} tone: {tone}")
1151
-
1152
- # Add Threats Section
1153
- logger.debug("\n⚠️ Adding Threat Analysis")
1154
- if flat_threats:
1155
- out += "\n\n🚨 **Immediate Danger Threats Detected:**\n"
1156
- for t in set(flat_threats):
1157
- out += f"• \"{t}\"\n"
1158
- out += "\n⚠️ These phrases may indicate an imminent risk to physical safety."
1159
- logger.debug(f"Added {len(set(flat_threats))} unique threat warnings")
1160
- else:
1161
- out += "\n\n🧩 **Immediate Danger Threats:** None explicitly detected.\n"
1162
- out += "This does *not* rule out risk, but no direct threat phrases were matched."
1163
- logger.debug("No threats to add")
1164
-
1165
- # Generate Timeline
1166
- logger.debug("\n📈 Generating Timeline")
1167
- pattern_labels = []
1168
- for result, _ in results:
1169
- matched_scores = result[2] # Get the matched_scores from the result tuple
1170
- if matched_scores:
1171
- # Sort matched_scores by score and get the highest scoring pattern
1172
- highest_pattern = max(matched_scores, key=lambda x: x[1])
1173
- pattern_labels.append(highest_pattern[0]) # Add the pattern name
1174
- else:
1175
- pattern_labels.append("none")
1176
-
1177
- logger.debug("Pattern labels for timeline:")
1178
- for i, (pattern, score) in enumerate(zip(pattern_labels, abuse_scores)):
1179
- logger.debug(f"Message {i+1}: {pattern} ({score:.1f}%)")
1180
-
1181
- timeline_image = generate_abuse_score_chart(dates_used, abuse_scores, pattern_labels)
1182
- logger.debug("Timeline generated successfully")
1183
-
1184
- # Add Escalation Text
1185
- out += "\n\n" + escalation_text
1186
- logger.debug("Added escalation text to output")
1187
-
1188
- logger.debug("\n✅ ANALYSIS COMPLETE")
1189
- logger.debug("=" * 50)
1190
- return out, timeline_image
1191
-
1192
- except Exception as e:
1193
- logger.error("\n❌ ERROR IN ANALYSIS")
1194
- logger.error("=" * 50)
1195
- logger.error(f"Error type: {type(e).__name__}")
1196
- logger.error(f"Error message: {str(e)}")
1197
- logger.error(f"Traceback:\n{traceback.format_exc()}")
1198
- return "An error occurred during analysis.", None
1199
-
1200
-
1201
-
1202
-
1203
- # Gradio Interface Setup
1204
- def create_interface():
1205
- try:
1206
- textbox_inputs = [gr.Textbox(label=f"Message {i+1}") for i in range(3)]
1207
- quiz_boxes = [gr.Checkbox(label=q) for q, _ in ESCALATION_QUESTIONS]
1208
- none_box = gr.Checkbox(label="None of the above")
1209
-
1210
- demo = gr.Interface(
1211
- fn=analyze_composite,
1212
- inputs=textbox_inputs + quiz_boxes + [none_box],
1213
- outputs=[
1214
- gr.Textbox(label="Results"),
1215
- gr.Image(label="Abuse Score Timeline", type="pil")
1216
- ],
1217
- title="Abuse Pattern Detector + Escalation Quiz",
1218
- description=(
1219
- "Enter up to three messages that concern you. "
1220
- "For the most accurate results, include messages from a recent emotionally intense period."
1221
- ),
1222
- flagging_mode="manual"
1223
- )
1224
- return demo
1225
- except Exception as e:
1226
- logger.error(f"Error creating interface: {e}")
1227
- raise
1228
-
1229
- # Main execution
1230
- if __name__ == "__main__":
1231
- try:
1232
- demo = create_interface()
1233
- demo.launch(
1234
- server_name="0.0.0.0",
1235
- server_port=7860,
1236
- share=False
1237
- )
1238
- except Exception as e:
1239
- logger.error(f"Failed to launch app: {e}")
1240
- raise