Spaces:
Running
on
Zero
Running
on
Zero
Delete app.py
Browse files
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|