Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
CHANGED
@@ -1,1054 +1,7 @@
|
|
1 |
-
#
|
2 |
-
|
3 |
-
import spaces
|
4 |
-
import torch
|
5 |
-
import numpy as np
|
6 |
-
import pandas as pd
|
7 |
-
from datetime import datetime, timedelta
|
8 |
-
from collections import defaultdict, Counter
|
9 |
-
import json
|
10 |
-
from typing import List, Dict, Tuple, Optional
|
11 |
-
from dataclasses import dataclass, asdict
|
12 |
-
from enum import Enum
|
13 |
-
import matplotlib.pyplot as plt
|
14 |
-
import io
|
15 |
-
from PIL import Image
|
16 |
-
import logging
|
17 |
-
from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline as hf_pipeline
|
18 |
-
from torch.nn.functional import sigmoid
|
19 |
-
import re
|
20 |
|
21 |
-
|
22 |
-
logging.basicConfig(level=logging.INFO)
|
23 |
-
logger = logging.getLogger(__name__)
|
24 |
-
|
25 |
-
# Device configuration
|
26 |
-
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
27 |
-
logger.info(f"Using device: {device}")
|
28 |
-
|
29 |
-
# =============================================================================
|
30 |
-
# LOAD ALL TETHER MODELS
|
31 |
-
# =============================================================================
|
32 |
-
|
33 |
-
print("🔄 Loading Tether Pro models...")
|
34 |
-
|
35 |
-
# Main abuse detection model
|
36 |
-
model_name = "SamanthaStorm/tether-multilabel-v6"
|
37 |
-
model = AutoModelForSequenceClassification.from_pretrained(model_name).to(device)
|
38 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
|
39 |
-
|
40 |
-
# Sentiment model
|
41 |
-
sentiment_model = AutoModelForSequenceClassification.from_pretrained("SamanthaStorm/tether-sentiment-v3").to(device)
|
42 |
-
sentiment_tokenizer = AutoTokenizer.from_pretrained("SamanthaStorm/tether-sentiment-v3", use_fast=False)
|
43 |
-
sentiment_model.eval()
|
44 |
-
|
45 |
-
# DARVO model
|
46 |
-
darvo_model = AutoModelForSequenceClassification.from_pretrained("SamanthaStorm/tether-darvo-regressor-v1").to(device)
|
47 |
-
darvo_tokenizer = AutoTokenizer.from_pretrained("SamanthaStorm/tether-darvo-regressor-v1", use_fast=False)
|
48 |
-
darvo_model.eval()
|
49 |
-
|
50 |
-
# Boundary health model
|
51 |
-
boundary_model = AutoModelForSequenceClassification.from_pretrained("SamanthaStorm/healthy-boundary-predictor").to(device)
|
52 |
-
boundary_tokenizer = AutoTokenizer.from_pretrained("SamanthaStorm/healthy-boundary-predictor", use_fast=False)
|
53 |
-
boundary_model.eval()
|
54 |
-
|
55 |
-
# Emotion pipeline
|
56 |
-
emotion_pipeline = hf_pipeline(
|
57 |
-
"text-classification",
|
58 |
-
model="j-hartmann/emotion-english-distilroberta-base",
|
59 |
-
return_all_scores=True,
|
60 |
-
top_k=None,
|
61 |
-
truncation=True,
|
62 |
-
device=0 if torch.cuda.is_available() else -1
|
63 |
-
)
|
64 |
-
|
65 |
-
print("✅ All models loaded successfully!")
|
66 |
-
|
67 |
-
# =============================================================================
|
68 |
-
# TETHER ANALYSIS CONSTANTS
|
69 |
-
# =============================================================================
|
70 |
-
|
71 |
-
LABELS = [
|
72 |
-
"recovery phase", "control", "gaslighting", "guilt tripping", "dismissiveness",
|
73 |
-
"blame shifting", "nonabusive", "projection", "insults",
|
74 |
-
"contradictory statements", "obscure language",
|
75 |
-
"veiled threats", "stalking language", "false concern",
|
76 |
-
"false equivalence", "future faking"
|
77 |
-
]
|
78 |
-
|
79 |
-
SENTIMENT_LABELS = ["supportive", "undermining"]
|
80 |
-
|
81 |
-
THRESHOLDS = {
|
82 |
-
"recovery phase": 0.278, "control": 0.287, "gaslighting": 0.144,
|
83 |
-
"guilt tripping": 0.220, "dismissiveness": 0.142, "blame shifting": 0.183,
|
84 |
-
"projection": 0.253, "insults": 0.247, "contradictory statements": 0.200,
|
85 |
-
"obscure language": 0.455, "nonabusive": 0.281, "veiled threats": 0.310,
|
86 |
-
"stalking language": 0.339, "false concern": 0.334, "false equivalence": 0.317,
|
87 |
-
"future faking": 0.385
|
88 |
-
}
|
89 |
-
|
90 |
-
PATTERN_WEIGHTS = {
|
91 |
-
"recovery phase": 0.7, "control": 1.4, "gaslighting": 1.3, "guilt tripping": 1.2,
|
92 |
-
"dismissiveness": 0.9, "blame shifting": 1.0, "projection": 0.5, "insults": 1.4,
|
93 |
-
"contradictory statements": 1.0, "obscure language": 0.9, "nonabusive": 0.0,
|
94 |
-
"veiled threats": 1.6, "stalking language": 1.8, "false concern": 1.1,
|
95 |
-
"false equivalence": 1.3, "future faking": 0.8
|
96 |
-
}
|
97 |
-
|
98 |
-
# =============================================================================
|
99 |
-
# FULL TETHER ANALYSIS FUNCTIONS
|
100 |
-
# =============================================================================
|
101 |
-
|
102 |
-
@spaces.GPU
|
103 |
-
def analyze_single_message_complete(text: str) -> Dict:
|
104 |
-
"""Run complete Tether analysis on a single message"""
|
105 |
-
|
106 |
-
try:
|
107 |
-
if not text.strip():
|
108 |
-
return {
|
109 |
-
'abuse_score': 0.0, 'darvo_score': 0.0, 'boundary_health': 'healthy',
|
110 |
-
'detected_patterns': [], 'emotional_tone': 'neutral', 'risk_level': 'low',
|
111 |
-
'sentiment': 'supportive'
|
112 |
-
}
|
113 |
-
|
114 |
-
# 1. BOUNDARY HEALTH ANALYSIS
|
115 |
-
boundary_inputs = boundary_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
116 |
-
boundary_inputs = {k: v.to(device) for k, v in boundary_inputs.items()}
|
117 |
-
with torch.no_grad():
|
118 |
-
boundary_outputs = boundary_model(**boundary_inputs)
|
119 |
-
boundary_prediction = torch.argmax(boundary_outputs.logits, dim=-1).item()
|
120 |
-
boundary_health = 'healthy' if boundary_prediction == 1 else 'unhealthy'
|
121 |
-
|
122 |
-
# 2. SENTIMENT ANALYSIS
|
123 |
-
sent_inputs = sentiment_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
124 |
-
sent_inputs = {k: v.to(device) for k, v in sent_inputs.items()}
|
125 |
-
with torch.no_grad():
|
126 |
-
sent_logits = sentiment_model(**sent_inputs).logits[0]
|
127 |
-
sent_probs = torch.softmax(sent_logits, dim=-1).cpu().numpy()
|
128 |
-
sentiment = SENTIMENT_LABELS[int(np.argmax(sent_probs))]
|
129 |
-
|
130 |
-
# 3. ABUSE PATTERN DETECTION
|
131 |
-
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
132 |
-
inputs = {k: v.to(device) for k, v in inputs.items()}
|
133 |
-
with torch.no_grad():
|
134 |
-
outputs = model(**inputs)
|
135 |
-
raw_scores = torch.sigmoid(outputs.logits.squeeze(0)).cpu().numpy()
|
136 |
-
|
137 |
-
# Apply thresholds
|
138 |
-
detected_patterns = []
|
139 |
-
matched_scores = []
|
140 |
-
for label, score in zip(LABELS, raw_scores):
|
141 |
-
if score > THRESHOLDS.get(label, 0.25):
|
142 |
-
detected_patterns.append(label)
|
143 |
-
weight = PATTERN_WEIGHTS.get(label, 1.0)
|
144 |
-
matched_scores.append((label, score, weight))
|
145 |
-
|
146 |
-
# Calculate abuse score
|
147 |
-
if matched_scores:
|
148 |
-
total_weight = sum(weight for _, _, weight in matched_scores)
|
149 |
-
weighted_sum = sum(score * weight for _, score, weight in matched_scores)
|
150 |
-
abuse_score = (weighted_sum / total_weight) * 100 if total_weight > 0 else 0
|
151 |
-
|
152 |
-
# Apply sentiment modifier
|
153 |
-
if sentiment == "supportive":
|
154 |
-
abuse_score *= 0.85
|
155 |
-
|
156 |
-
abuse_score = min(round(abuse_score, 1), 95.0)
|
157 |
-
else:
|
158 |
-
abuse_score = 0.0
|
159 |
-
|
160 |
-
# 4. DARVO SCORE
|
161 |
-
darvo_inputs = darvo_tokenizer(text, return_tensors="pt", truncation=True, padding=True)
|
162 |
-
darvo_inputs = {k: v.to(device) for k, v in darvo_inputs.items()}
|
163 |
-
with torch.no_grad():
|
164 |
-
darvo_logits = darvo_model(**darvo_inputs).logits
|
165 |
-
darvo_score = round(sigmoid(darvo_logits.cpu()).item(), 4)
|
166 |
-
|
167 |
-
# 5. EMOTIONAL TONE ANALYSIS
|
168 |
-
emotions = emotion_pipeline(text)
|
169 |
-
if isinstance(emotions, list) and isinstance(emotions[0], list):
|
170 |
-
emotion_scores = emotions[0]
|
171 |
-
emotion_dict = {e['label'].lower(): e['score'] for e in emotion_scores}
|
172 |
-
|
173 |
-
# Determine emotional tone based on patterns and emotions
|
174 |
-
emotional_tone = determine_emotional_tone(emotion_dict, detected_patterns, abuse_score, sentiment)
|
175 |
-
else:
|
176 |
-
emotional_tone = 'neutral'
|
177 |
-
|
178 |
-
# 6. RISK LEVEL
|
179 |
-
if abuse_score >= 80:
|
180 |
-
risk_level = 'critical'
|
181 |
-
elif abuse_score >= 60:
|
182 |
-
risk_level = 'high'
|
183 |
-
elif abuse_score >= 35:
|
184 |
-
risk_level = 'moderate'
|
185 |
-
else:
|
186 |
-
risk_level = 'low'
|
187 |
-
|
188 |
-
return {
|
189 |
-
'abuse_score': abuse_score,
|
190 |
-
'darvo_score': darvo_score,
|
191 |
-
'boundary_health': boundary_health,
|
192 |
-
'detected_patterns': detected_patterns,
|
193 |
-
'emotional_tone': emotional_tone,
|
194 |
-
'risk_level': risk_level,
|
195 |
-
'sentiment': sentiment
|
196 |
-
}
|
197 |
-
|
198 |
-
except Exception as e:
|
199 |
-
logger.error(f"Error in analyze_single_message_complete: {e}")
|
200 |
-
return {
|
201 |
-
'abuse_score': 0.0, 'darvo_score': 0.0, 'boundary_health': 'unknown',
|
202 |
-
'detected_patterns': [], 'emotional_tone': 'neutral', 'risk_level': 'low',
|
203 |
-
'sentiment': 'supportive'
|
204 |
-
}
|
205 |
-
|
206 |
-
def determine_emotional_tone(emotions, patterns, abuse_score, sentiment):
|
207 |
-
"""Determine emotional tone based on analysis"""
|
208 |
-
|
209 |
-
anger = emotions.get("anger", 0)
|
210 |
-
sadness = emotions.get("sadness", 0)
|
211 |
-
fear = emotions.get("fear", 0)
|
212 |
-
joy = emotions.get("joy", 0)
|
213 |
-
disgust = emotions.get("disgust", 0)
|
214 |
-
|
215 |
-
# High-risk tones
|
216 |
-
if "stalking language" in patterns and joy > 0.3:
|
217 |
-
return "obsessive_fixation"
|
218 |
-
elif "veiled threats" in patterns and anger < 0.2:
|
219 |
-
return "menacing_calm"
|
220 |
-
elif "false concern" in patterns and sentiment == "undermining":
|
221 |
-
return "predatory_concern"
|
222 |
-
elif "control" in patterns and anger > 0.4:
|
223 |
-
return "entitled_rage"
|
224 |
-
elif abuse_score > 70 and anger > 0.3:
|
225 |
-
return "emotional_threat"
|
226 |
-
elif "gaslighting" in patterns:
|
227 |
-
return "contradictory_gaslight"
|
228 |
-
elif sadness > 0.5 and "guilt tripping" in patterns:
|
229 |
-
return "weaponized_sadness"
|
230 |
-
elif disgust > 0.3 and "dismissiveness" in patterns:
|
231 |
-
return "cold_invalidation"
|
232 |
-
else:
|
233 |
-
return "neutral"
|
234 |
-
|
235 |
-
# =============================================================================
|
236 |
-
# TETHER PRO DATA STRUCTURES (Simplified for HuggingFace)
|
237 |
-
# =============================================================================
|
238 |
-
|
239 |
-
@dataclass
|
240 |
-
class MessageAnalysis:
|
241 |
-
timestamp: str # ISO format string for JSON compatibility
|
242 |
-
message_id: str
|
243 |
-
text: str
|
244 |
-
sender: str
|
245 |
-
abuse_score: float
|
246 |
-
darvo_score: float
|
247 |
-
boundary_health: str
|
248 |
-
detected_patterns: List[str]
|
249 |
-
emotional_tone: str
|
250 |
-
risk_level: str
|
251 |
-
|
252 |
-
class RiskTrend(Enum):
|
253 |
-
ESCALATING = "escalating"
|
254 |
-
STABLE_HIGH = "stable_high"
|
255 |
-
STABLE_MODERATE = "stable_moderate"
|
256 |
-
IMPROVING = "improving"
|
257 |
-
CYCLICAL = "cyclical"
|
258 |
-
UNKNOWN = "unknown"
|
259 |
-
|
260 |
-
# =============================================================================
|
261 |
-
# TEMPORAL ANALYSIS ENGINE (HuggingFace Compatible)
|
262 |
-
# =============================================================================
|
263 |
-
|
264 |
-
class TetherProAnalyzer:
|
265 |
-
"""Simplified Tether Pro analyzer for HuggingFace demo"""
|
266 |
-
|
267 |
-
def __init__(self):
|
268 |
-
self.conversation_history = []
|
269 |
-
|
270 |
-
def analyze_conversation_history(self, messages_json: str) -> Dict:
|
271 |
-
"""Analyze uploaded conversation history"""
|
272 |
-
try:
|
273 |
-
# Parse messages
|
274 |
-
messages_data = json.loads(messages_json)
|
275 |
-
|
276 |
-
# Convert to MessageAnalysis objects
|
277 |
-
self.conversation_history = []
|
278 |
-
for msg_data in messages_data:
|
279 |
-
analysis = MessageAnalysis(
|
280 |
-
timestamp=msg_data.get('timestamp', datetime.now().isoformat()),
|
281 |
-
message_id=msg_data.get('id', f"msg_{len(self.conversation_history
|
282 |
-
message_id=msg_data.get('id', f"msg_{len(self.conversation_history)}"),
|
283 |
-
text=msg_data.get('text', ''),
|
284 |
-
sender=msg_data.get('sender', 'unknown'),
|
285 |
-
abuse_score=float(msg_data.get('abuse_score', 0)),
|
286 |
-
darvo_score=float(msg_data.get('darvo_score', 0)),
|
287 |
-
boundary_health=msg_data.get('boundary_health', 'unknown'),
|
288 |
-
detected_patterns=msg_data.get('patterns', []),
|
289 |
-
emotional_tone=msg_data.get('emotional_tone', 'neutral'),
|
290 |
-
risk_level=msg_data.get('risk_level', 'low')
|
291 |
-
)
|
292 |
-
self.conversation_history.append(analysis)
|
293 |
-
|
294 |
-
# Perform temporal analysis
|
295 |
-
return self._perform_temporal_analysis()
|
296 |
-
|
297 |
-
except Exception as e:
|
298 |
-
logger.error(f"Error in analyze_conversation_history: {e}")
|
299 |
-
return {
|
300 |
-
'error': f"Analysis failed: {str(e)}",
|
301 |
-
'total_messages': 0,
|
302 |
-
'temporal_patterns': {},
|
303 |
-
'recommendations': []
|
304 |
-
}
|
305 |
-
|
306 |
-
def _perform_temporal_analysis(self) -> Dict:
|
307 |
-
"""Perform comprehensive temporal analysis"""
|
308 |
-
if len(self.conversation_history) < 3:
|
309 |
-
return {
|
310 |
-
'total_messages': len(self.conversation_history),
|
311 |
-
'analysis_status': 'insufficient_data',
|
312 |
-
'message': 'Need at least 3 messages for temporal analysis',
|
313 |
-
'basic_stats': self._get_basic_stats(),
|
314 |
-
'recommendations': ['Upload more conversation history for detailed analysis']
|
315 |
-
}
|
316 |
-
|
317 |
-
# Convert to DataFrame for analysis
|
318 |
-
df = self._to_dataframe()
|
319 |
-
|
320 |
-
# Detect patterns
|
321 |
-
escalation_patterns = self._detect_escalation_trends(df)
|
322 |
-
cyclical_patterns = self._detect_cycles(df)
|
323 |
-
pattern_combinations = self._analyze_pattern_combinations(df)
|
324 |
-
risk_trajectory = self._calculate_risk_trajectory(df)
|
325 |
-
temporal_triggers = self._analyze_temporal_triggers(df)
|
326 |
-
|
327 |
-
# Generate professional recommendations
|
328 |
-
recommendations = self._generate_recommendations(
|
329 |
-
escalation_patterns, pattern_combinations, risk_trajectory
|
330 |
-
)
|
331 |
-
|
332 |
-
return {
|
333 |
-
'total_messages': len(self.conversation_history),
|
334 |
-
'analysis_status': 'complete',
|
335 |
-
'date_range': self._get_date_range(),
|
336 |
-
'basic_stats': self._get_basic_stats(),
|
337 |
-
'temporal_analysis': {
|
338 |
-
'escalation_patterns': escalation_patterns,
|
339 |
-
'cyclical_patterns': cyclical_patterns,
|
340 |
-
'pattern_combinations': pattern_combinations,
|
341 |
-
'temporal_triggers': temporal_triggers
|
342 |
-
},
|
343 |
-
'risk_assessment': risk_trajectory,
|
344 |
-
'professional_recommendations': recommendations,
|
345 |
-
'visualizations': self._generate_visualizations(df)
|
346 |
-
}
|
347 |
-
|
348 |
-
def _to_dataframe(self) -> pd.DataFrame:
|
349 |
-
"""Convert conversation history to DataFrame"""
|
350 |
-
data = []
|
351 |
-
for msg in self.conversation_history:
|
352 |
-
# Parse timestamp
|
353 |
-
try:
|
354 |
-
timestamp = datetime.fromisoformat(msg.timestamp.replace('Z', '+00:00'))
|
355 |
-
except:
|
356 |
-
timestamp = datetime.now()
|
357 |
-
|
358 |
-
data.append({
|
359 |
-
'timestamp': timestamp,
|
360 |
-
'message_id': msg.message_id,
|
361 |
-
'sender': msg.sender,
|
362 |
-
'abuse_score': msg.abuse_score,
|
363 |
-
'darvo_score': msg.darvo_score,
|
364 |
-
'boundary_health': msg.boundary_health,
|
365 |
-
'emotional_tone': msg.emotional_tone,
|
366 |
-
'risk_level': msg.risk_level,
|
367 |
-
'patterns': '|'.join(msg.detected_patterns)
|
368 |
-
})
|
369 |
-
|
370 |
-
df = pd.DataFrame(data)
|
371 |
-
df = df.sort_values('timestamp')
|
372 |
-
return df
|
373 |
-
|
374 |
-
def _detect_escalation_t trends(self, df: pd.DataFrame) -> Dict:
|
375 |
-
"""Detect escalating abuse patterns over time"""
|
376 |
-
if len(df) < 5:
|
377 |
-
return {'detected': False, 'reason': 'insufficient_data'}
|
378 |
-
|
379 |
-
# Calculate 3-message rolling average
|
380 |
-
df['abuse_rolling'] = df['abuse_score'].rolling(window=3, min_periods=1).mean()
|
381 |
-
|
382 |
-
# Check for sustained increases
|
383 |
-
recent_data = df.tail(10) # Last 10 messages
|
384 |
-
|
385 |
-
if len(recent_data) < 5:
|
386 |
-
return {'detected': False, 'reason': 'insufficient_recent_data'}
|
387 |
-
|
388 |
-
# Calculate trend
|
389 |
-
x_vals = list(range(len(recent_data)))
|
390 |
-
y_vals = recent_data['abuse_rolling'].values
|
391 |
-
|
392 |
-
# Simple linear regression
|
393 |
-
correlation = np.corrcoef(x_vals, y_vals)[0, 1] if len(x_vals) > 1 else 0
|
394 |
-
|
395 |
-
escalating = correlation > 0.3 # Positive correlation indicates escalation
|
396 |
-
|
397 |
-
if escalating:
|
398 |
-
start_score = recent_data['abuse_rolling'].iloc[0]
|
399 |
-
end_score = recent_data['abuse_rolling'].iloc[-1]
|
400 |
-
increase = end_score - start_score
|
401 |
-
|
402 |
-
return {
|
403 |
-
'detected': True,
|
404 |
-
'severity': 'high' if increase > 20 else 'moderate' if increase > 10 else 'mild',
|
405 |
-
'increase_amount': round(increase, 1),
|
406 |
-
'timeframe': f"Last {len(recent_data)} messages",
|
407 |
-
'confidence': min(abs(correlation), 1.0),
|
408 |
-
'description': f"Abuse intensity increased by {increase:.1f}% over recent communications"
|
409 |
-
}
|
410 |
-
else:
|
411 |
-
return {'detected': False, 'reason': 'no_escalation_trend'}
|
412 |
-
|
413 |
-
def _detect_cycles(self, df: pd.DataFrame) -> Dict:
|
414 |
-
"""Detect cyclical abuse patterns"""
|
415 |
-
if len(df) < 15:
|
416 |
-
return {'detected': False, 'reason': 'insufficient_data_for_cycles'}
|
417 |
-
|
418 |
-
# Group by day and calculate daily averages
|
419 |
-
df['date'] = df['timestamp'].dt.date
|
420 |
-
daily_scores = df.groupby('date')['abuse_score'].mean()
|
421 |
-
|
422 |
-
if len(daily_scores) < 10:
|
423 |
-
return {'detected': False, 'reason': 'insufficient_days'}
|
424 |
-
|
425 |
-
# Look for repeating patterns (simplified)
|
426 |
-
scores = daily_scores.values
|
427 |
-
|
428 |
-
# Check for peaks and valleys
|
429 |
-
peaks = []
|
430 |
-
valleys = []
|
431 |
-
|
432 |
-
for i in range(1, len(scores) - 1):
|
433 |
-
if scores[i] > scores[i-1] and scores[i] > scores[i+1] and scores[i] > 60:
|
434 |
-
peaks.append(i)
|
435 |
-
elif scores[i] < scores[i-1] and scores[i] < scores[i+1] and scores[i] < 40:
|
436 |
-
valleys.append(i)
|
437 |
-
|
438 |
-
has_cycle = len(peaks) >= 2 and len(valleys) >= 2
|
439 |
-
|
440 |
-
if has_cycle:
|
441 |
-
# Calculate average cycle length
|
442 |
-
if len(peaks) > 1:
|
443 |
-
peak_intervals = [peaks[i+1] - peaks[i] for i in range(len(peaks)-1)]
|
444 |
-
avg_cycle_length = np.mean(peak_intervals)
|
445 |
-
else:
|
446 |
-
avg_cycle_length = 0
|
447 |
-
|
448 |
-
return {
|
449 |
-
'detected': True,
|
450 |
-
'cycle_count': min(len(peaks, len(valleys))),
|
451 |
-
'avg_cycle_length_days': round(avg_cycle_length, 1),
|
452 |
-
'pattern_type': 'tension_escalation_reconciliation',
|
453 |
-
'confidence': min(len(peaks) / 3.0, 1.0),
|
454 |
-
'description': f"Detected {min(len(peaks), len(valleys))} abuse cycles with average length of {avg_cycle_length:.1f} days"
|
455 |
-
}
|
456 |
-
else:
|
457 |
-
return {'detected': False, 'reason': 'no_cyclical_pattern'}
|
458 |
-
|
459 |
-
def _analyze_pattern_combinations(self, df: pd.DataFrame) -> List[Dict]:
|
460 |
-
"""Analyze dangerous pattern combinations"""
|
461 |
-
all_patterns = []
|
462 |
-
for patterns_str in df['patterns']:
|
463 |
-
if patterns_str:
|
464 |
-
all_patterns.extend(patterns_str.split('|'))
|
465 |
-
|
466 |
-
pattern_counts = Counter(all_patterns)
|
467 |
-
|
468 |
-
# Define dangerous combinations
|
469 |
-
dangerous_combos = [
|
470 |
-
{
|
471 |
-
'name': 'Control + Manipulation Complex',
|
472 |
-
'patterns': ['control', 'gaslighting', 'darvo'],
|
473 |
-
'severity': 'critical'
|
474 |
-
},
|
475 |
-
{
|
476 |
-
'name': 'Stalking + Threat Pattern',
|
477 |
-
'patterns': ['stalking language', 'veiled threats', 'control'],
|
478 |
-
'severity': 'critical'
|
479 |
-
},
|
480 |
-
{
|
481 |
-
'name': 'Emotional Manipulation',
|
482 |
-
'patterns': ['guilt tripping', 'future faking', 'false concern'],
|
483 |
-
'severity': 'high'
|
484 |
-
}
|
485 |
-
]
|
486 |
-
|
487 |
-
detected_combinations = []
|
488 |
-
|
489 |
-
for combo in dangerous_combos:
|
490 |
-
present_patterns = [p for p in combo['patterns'] if pattern_counts.get(p, 0) >= 2]
|
491 |
-
|
492 |
-
if len(present_patterns) >= 2:
|
493 |
-
total_frequency = sum(pattern_counts.get(p, 0) for p in present_patterns)
|
494 |
-
|
495 |
-
detected_combinations.append({
|
496 |
-
'name': combo['name'],
|
497 |
-
'severity': combo['severity'],
|
498 |
-
'present_patterns': present_patterns,
|
499 |
-
'frequency': total_frequency,
|
500 |
-
'risk_level': 'critical' if total_frequency > 8 else 'high' if total_frequency > 4 else 'moderate'
|
501 |
-
})
|
502 |
-
|
503 |
-
return detected_combinations
|
504 |
-
|
505 |
-
def _calculate_risk_trajectory(self, df: pd.DataFrame) -> Dict:
|
506 |
-
"""Calculate risk trajectory and predictions"""
|
507 |
-
recent_messages = df.tail(10)
|
508 |
-
|
509 |
-
if len(recent_messages) < 3:
|
510 |
-
return {
|
511 |
-
'current_risk': 0,
|
512 |
-
'trend': RiskTrend.UNKNOWN.value,
|
513 |
-
'confidence': 0,
|
514 |
-
'prediction': 'insufficient_data'
|
515 |
-
}
|
516 |
-
|
517 |
-
current_risk = recent_messages['abuse_score'].mean()
|
518 |
-
|
519 |
-
# Calculate trend
|
520 |
-
x_vals = list(range(len(recent_messages)))
|
521 |
-
y_vals = recent_messages['abuse_score'].values
|
522 |
-
|
523 |
-
correlation = np.corrcoef(x_vals, y_vals)[0, 1] if len(x_vals) > 1 else 0
|
524 |
-
|
525 |
-
# Determine trend
|
526 |
-
if correlation > 0.3:
|
527 |
-
trend = RiskTrend.ESCALATING
|
528 |
-
elif correlation < -0.3:
|
529 |
-
trend = RiskTrend.IMPROVING
|
530 |
-
elif current_risk > 70:
|
531 |
-
trend = RiskTrend.STABLE_HIGH
|
532 |
-
elif recent_messages['abuse_score'].std() > 25:
|
533 |
-
trend = RiskTrend.CYCLICAL
|
534 |
-
else:
|
535 |
-
trend = RiskTrend.STABLE_MODERATE
|
536 |
-
|
537 |
-
return {
|
538 |
-
'current_risk': round(current_risk, 1),
|
539 |
-
'trend': trend.value,
|
540 |
-
'trend_strength': abs(correlation),
|
541 |
-
'confidence': min(len(recent_messages) / 10.0, 1.0),
|
542 |
-
'prediction_7_days': self._predict_future_risk(recent_messages, current_risk, correlation),
|
543 |
-
'intervention_urgency': self._assess_intervention_urgency(trend, current_risk)
|
544 |
-
}
|
545 |
-
|
546 |
-
def _analyze_temporal_triggers(self, df: pd.DataFrame) -> Dict:
|
547 |
-
"""Analyze temporal patterns in abuse"""
|
548 |
-
if len(df) < 10:
|
549 |
-
return {'analysis': 'insufficient_data'}
|
550 |
-
|
551 |
-
# Add time features
|
552 |
-
df['hour'] = df['timestamp'].dt.hour
|
553 |
-
df['day_of_week'] = df['timestamp'].dt.day_name()
|
554 |
-
df['is_weekend'] = df['timestamp'].dt.weekday >= 5
|
555 |
-
|
556 |
-
# High abuse messages (>60% score)
|
557 |
-
high_abuse = df[df['abuse_score'] > 60]
|
558 |
-
|
559 |
-
if len(high_abuse) == 0:
|
560 |
-
return {'analysis': 'no_high_abuse_episodes'}
|
561 |
-
|
562 |
-
# Analyze patterns
|
563 |
-
triggers = {}
|
564 |
-
|
565 |
-
# Time of day patterns
|
566 |
-
if len(high_abuse) > 0:
|
567 |
-
hour_counts = high_abuse['hour'].value_counts()
|
568 |
-
peak_hours = hour_counts.head(3).to_dict()
|
569 |
-
|
570 |
-
triggers['time_patterns'] = {
|
571 |
-
'peak_hours': peak_hours,
|
572 |
-
'evening_escalation': len(high_abuse[high_abuse['hour'] >= 18]) / len(high_abuse),
|
573 |
-
'late_night_abuse': len(high_abuse[high_abuse['hour'] >= 22]) / len(high_abuse)
|
574 |
-
}
|
575 |
-
|
576 |
-
# Day of week patterns
|
577 |
-
if len(high_abuse) > 0:
|
578 |
-
day_counts = high_abuse['day_of_week'].value_counts()
|
579 |
-
weekend_abuse = len(high_abuse[
|
580 |
-
day_counts = high_abuse['day_of_week'].value_counts()
|
581 |
-
weekend_abuse = len(high_abuse[high_abuse['is_weekend']]) / len(high_abuse)
|
582 |
-
|
583 |
-
triggers['day_patterns'] = {
|
584 |
-
'high_risk_days': day_counts.head(3).to_dict(),
|
585 |
-
'weekend_escalation': weekend_abuse
|
586 |
-
}
|
587 |
-
|
588 |
-
return triggers
|
589 |
-
|
590 |
-
def _generate_recommendations(self, escalation, combinations, risk_trajectory) -> Dict:
|
591 |
-
"""Generate professional recommendations"""
|
592 |
-
recommendations = {
|
593 |
-
'immediate_actions': [],
|
594 |
-
'therapeutic_focus': [],
|
595 |
-
'safety_planning': [],
|
596 |
-
'monitoring': []
|
597 |
-
}
|
598 |
-
|
599 |
-
# Based on escalation
|
600 |
-
if escalation.get('detected') and escalation.get('severity') in ['high', 'critical']:
|
601 |
-
recommendations['immediate_actions'].extend([
|
602 |
-
'Urgent safety assessment required - escalating abuse pattern detected',
|
603 |
-
'Consider emergency safety planning session',
|
604 |
-
'Increase support contact frequency'
|
605 |
-
])
|
606 |
-
|
607 |
-
# Based on combinations
|
608 |
-
for combo in combinations:
|
609 |
-
if combo['severity'] == 'critical':
|
610 |
-
recommendations['therapeutic_focus'].append(
|
611 |
-
f"Address {combo['name']} - multiple manipulation tactics present"
|
612 |
-
)
|
613 |
-
|
614 |
-
# Based on risk trajectory
|
615 |
-
if risk_trajectory['trend'] == 'escalating':
|
616 |
-
recommendations['safety_planning'].extend([
|
617 |
-
'Develop comprehensive safety plan',
|
618 |
-
'Identify safe locations and contacts',
|
619 |
-
'Document escalation pattern for potential legal use'
|
620 |
-
])
|
621 |
-
elif risk_trajectory['current_risk'] > 70:
|
622 |
-
recommendations['monitoring'].extend([
|
623 |
-
'High-risk situation requires enhanced monitoring',
|
624 |
-
'Weekly check-ins recommended',
|
625 |
-
'Crisis intervention plan should be ready'
|
626 |
-
])
|
627 |
-
|
628 |
-
return recommendations
|
629 |
-
|
630 |
-
def _generate_visualizations(self, df: pd.DataFrame) -> Dict:
|
631 |
-
"""Generate visualization data for charts"""
|
632 |
-
# Timeline data
|
633 |
-
timeline_data = []
|
634 |
-
for _, row in df.iterrows():
|
635 |
-
timeline_data.append({
|
636 |
-
'date': row['timestamp'].strftime('%Y-%m-%d'),
|
637 |
-
'abuse_score': row['abuse_score'],
|
638 |
-
'darvo_score': row['darvo_score'] * 100, # Convert to percentage
|
639 |
-
'risk_level': row['risk_level']
|
640 |
-
})
|
641 |
-
|
642 |
-
# Pattern frequency data
|
643 |
-
all_patterns = []
|
644 |
-
for patterns_str in df['patterns']:
|
645 |
-
if patterns_str:
|
646 |
-
all_patterns.extend(patterns_str.split('|'))
|
647 |
-
|
648 |
-
pattern_counts = Counter(all_patterns)
|
649 |
-
pattern_data = [{'pattern': k, 'count': v} for k, v in pattern_counts.most_common(10)]
|
650 |
-
|
651 |
-
return {
|
652 |
-
'timeline': timeline_data,
|
653 |
-
'pattern_frequency': pattern_data,
|
654 |
-
'total_messages': len(df)
|
655 |
-
}
|
656 |
-
|
657 |
-
def _get_basic_stats(self) -> Dict:
|
658 |
-
"""Get basic conversation statistics"""
|
659 |
-
if not self.conversation_history:
|
660 |
-
return {}
|
661 |
-
|
662 |
-
abuse_scores = [msg.abuse_score for msg in self.conversation_history]
|
663 |
-
darvo_scores = [msg.darvo_score for msg in self.conversation_history]
|
664 |
-
|
665 |
-
return {
|
666 |
-
'avg_abuse_score': round(np.mean(abuse_scores), 1),
|
667 |
-
'max_abuse_score': round(max(abuse_scores), 1),
|
668 |
-
'avg_darvo_score': round(np.mean(darvo_scores), 3),
|
669 |
-
'high_risk_messages': len([s for s in abuse_scores if s > 70]),
|
670 |
-
'healthy_boundaries': len([msg for msg in self.conversation_history if msg.boundary_health == 'healthy'])
|
671 |
-
}
|
672 |
-
|
673 |
-
def _get_date_range(self) -> Dict:
|
674 |
-
"""Get conversation date range"""
|
675 |
-
if not self.conversation_history:
|
676 |
-
return {}
|
677 |
-
|
678 |
-
timestamps = []
|
679 |
-
for msg in self.conversation_history:
|
680 |
-
try:
|
681 |
-
timestamps.append(datetime.fromisoformat(msg.timestamp.replace('Z', '+00:00'))
|
682 |
-
except:
|
683 |
-
continue
|
684 |
-
|
685 |
-
if not timestamps:
|
686 |
-
return {}
|
687 |
-
|
688 |
-
return {
|
689 |
-
'start_date': min(timestamps).strftime('%Y-%m-%d'),
|
690 |
-
'end_date': max(timestamps).strftime('%Y-%m-%d'),
|
691 |
-
'span_days': (max(timestamps) - min(timestamps)).days
|
692 |
-
}
|
693 |
-
|
694 |
-
def _predict_future_risk(self, recent_data, current_risk, correlation):
|
695 |
-
"""Simple risk prediction"""
|
696 |
-
if correlation > 0.3: # Escalating
|
697 |
-
return min(100, current_risk + (correlation * 20))
|
698 |
-
elif correlation < -0.3: # Improving
|
699 |
-
return max(0, current_risk + (correlation * 20))
|
700 |
-
else:
|
701 |
-
return current_risk
|
702 |
-
|
703 |
-
def _assess_intervention_urgency(self, trend, current_risk):
|
704 |
-
"""Assess urgency of intervention"""
|
705 |
-
if trend == RiskTrend.ESCALATING and current_risk > 70:
|
706 |
-
return 'critical'
|
707 |
-
elif trend == RiskTrend.ESCALATING or current_risk > 80:
|
708 |
-
return 'high'
|
709 |
-
elif current_risk > 60:
|
710 |
-
return 'moderate'
|
711 |
-
else:
|
712 |
-
return 'low'
|
713 |
-
|
714 |
-
# =============================================================================
|
715 |
-
# HUGGINGFACE GRADIO INTERFACE
|
716 |
-
# =============================================================================
|
717 |
-
|
718 |
-
def create_temporal_visualization(timeline_data):
|
719 |
-
"""Create timeline visualization"""
|
720 |
-
if not timeline_data:
|
721 |
-
return None
|
722 |
-
|
723 |
-
try:
|
724 |
-
# Create timeline chart
|
725 |
-
df_viz = pd.DataFrame(timeline_data)
|
726 |
-
|
727 |
-
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
|
728 |
-
|
729 |
-
# Plot abuse scores
|
730 |
-
ax1.plot(df_viz['date'], df_viz['abuse_score'], 'o-', color='red', linewidth=2, markersize=6)
|
731 |
-
ax1.set_ylabel('Abuse Score (%)')
|
732 |
-
ax1.set_title('Abuse Pattern Timeline')
|
733 |
-
ax1.grid(True, alpha=0.3)
|
734 |
-
ax1.set_ylim(0, 100)
|
735 |
-
|
736 |
-
# Add risk level bands
|
737 |
-
ax1.axhspan(0, 30, alpha=0.2, color='green', label='Low Risk')
|
738 |
-
ax1.axhspan(30, 60, alpha=0.2, color='yellow', label='Moderate Risk')
|
739 |
-
ax1.axhspan(60, 80, alpha=0.2, color='orange', label='High Risk')
|
740 |
-
ax1.axhspan(80, 100, alpha=0.2, color='red', label='Critical Risk')
|
741 |
-
|
742 |
-
# Plot DARVO scores
|
743 |
-
ax2.plot(df_viz['date'], df_viz['darvo_score'], 's-', color='purple', linewidth=2, markersize=6)
|
744 |
-
ax2.set_ylabel('DARVO Score (%)')
|
745 |
-
ax2.set_xlabel('Date')
|
746 |
-
ax2.set_title('DARVO Pattern Timeline')
|
747 |
-
ax2.grid(True, alpha=0.3)
|
748 |
-
ax2.set_ylim(0, 100)
|
749 |
-
|
750 |
-
# Rotate x-axis labels
|
751 |
-
plt.xticks(rotation=45)
|
752 |
-
plt.tight_layout()
|
753 |
-
|
754 |
-
# Convert to image
|
755 |
-
buf = io.BytesIO()
|
756 |
-
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
757 |
-
buf.seek(0)
|
758 |
-
plt.close()
|
759 |
-
|
760 |
-
return Image.open(buf)
|
761 |
-
|
762 |
-
except Exception as e:
|
763 |
-
logger.error(f"Error creating visualization: {e}")
|
764 |
-
return None
|
765 |
-
|
766 |
-
def analyze_temporal_patterns(messages_json: str):
|
767 |
-
"""Main analysis function - NOW WITH FULL TETHER ANALYSIS"""
|
768 |
-
|
769 |
-
if not messages_json.strip():
|
770 |
-
return (
|
771 |
-
"Please provide conversation history in JSON format.",
|
772 |
-
None,
|
773 |
-
"No analysis performed."
|
774 |
-
)
|
775 |
-
|
776 |
-
try:
|
777 |
-
# Parse input JSON (basic format from CSV conversion)
|
778 |
-
raw_messages = json.loads(messages_json)
|
779 |
-
|
780 |
-
if not raw_messages:
|
781 |
-
return "No messages found in JSON.", None, "Empty dataset"
|
782 |
-
|
783 |
-
print(f"🔄 Running full Tether analysis on {len(raw_messages)} messages...")
|
784 |
-
|
785 |
-
# STEP 1: Run each message through complete Tether analysis
|
786 |
-
analyzed_messages = []
|
787 |
-
|
788 |
-
for i, raw_msg in enumerate(raw_messages):
|
789 |
-
# Extract basic message info
|
790 |
-
text = raw_msg.get('message', raw_msg.get('text', ''))
|
791 |
-
timestamp = raw_msg.get('timestamp', datetime.now().isoformat())
|
792 |
-
sender = raw_msg.get('sender', 'unknown')
|
793 |
-
|
794 |
-
if not text.strip():
|
795 |
-
continue
|
796 |
-
|
797 |
-
# Run complete Tether analysis
|
798 |
-
print(f"📝 Analyzing message {i+1}: '{text[:50]}...'")
|
799 |
-
analysis = analyze_single_message_complete(text)
|
800 |
-
|
801 |
-
# Create analyzed message
|
802 |
-
analyzed_msg = MessageAnalysis(
|
803 |
-
timestamp=timestamp,
|
804 |
-
message_id=f'msg_{i:03d}',
|
805 |
-
text=text,
|
806 |
-
sender=sender,
|
807 |
-
abuse_score=analysis['abuse_score'],
|
808 |
-
darvo_score=analysis['darvo_score'],
|
809 |
-
boundary_health=analysis['boundary_health'],
|
810 |
-
detected_patterns=analysis['detected_patterns'],
|
811 |
-
emotional_tone=analysis['emotional_tone'],
|
812 |
-
risk_level=analysis['risk_level']
|
813 |
-
)
|
814 |
-
analyzed_messages.append(analyzed_msg)
|
815 |
-
|
816 |
-
if not analyzed_messages:
|
817 |
-
return "No valid messages to analyze.", None, "No analysis performed"
|
818 |
-
|
819 |
-
print(f"✅ Completed analysis of {len(analyzed_messages)} messages")
|
820 |
-
|
821 |
-
# STEP 2: Run temporal analysis on the analyzed messages
|
822 |
-
analyzer = TetherProAnalyzer()
|
823 |
-
analyzer.conversation_history = analyzed_messages
|
824 |
-
results = analyzer._perform_temporal_analysis()
|
825 |
-
|
826 |
-
if results.get('analysis_status') == 'insufficient_data':
|
827 |
-
summary = f"""
|
828 |
-
## ⚠️ Insufficient Data for Temporal Analysis
|
829 |
-
|
830 |
-
**Messages Analyzed:** {results['total_messages']}
|
831 |
-
|
832 |
-
{results.get('message', 'Need more messages for temporal patterns')}
|
833 |
-
|
834 |
-
**Basic Statistics:**
|
835 |
-
{json.dumps(results.get('basic_stats', {}), indent=2)}
|
836 |
-
|
837 |
-
**Recommendations:"""
|
838 |
-
+ '\n'.join([f"• {rec}" for rec in results.get('recommendations', [])])
|
839 |
-
|
840 |
-
return summary, None, "Upload more conversation history for comprehensive temporal analysis."
|
841 |
-
|
842 |
-
# STEP 3: Generate comprehensive report
|
843 |
-
report = f"""
|
844 |
-
# 🎯 Tether Pro - Complete Analysis Report
|
845 |
-
|
846 |
-
## 📊 Conversation Overview
|
847 |
-
- **Total Messages:** {results['total_messages']}
|
848 |
-
- **Date Range:** {results['date_range'].get('start_date', 'Unknown')} to {results['date_range'].get('end_date', 'Unknown')}
|
849 |
-
- **Analysis Span:** {results['date_range'].get('span_days', 0)} days
|
850 |
-
|
851 |
-
## 🤖 AI Analysis Results
|
852 |
-
- **Average Abuse Score:** {results['basic_stats'].get('avg_abuse_score', 0)}%
|
853 |
-
- **Peak Abuse Score:** {results['basic_stats'].get('max_abuse_score', 0)}%
|
854 |
-
- **Average DARVO Score:** {results['basic_stats'].get('avg_darvo_score', 0)}
|
855 |
-
- **High-Risk Messages:** {results['basic_stats'].get('high_risk_messages', 0)}
|
856 |
-
- **Healthy Boundaries:** {results['basic_stats'].get('healthy_boundaries', 0)}
|
857 |
-
|
858 |
-
## 📈 Temporal Pattern Analysis
|
859 |
-
|
860 |
-
### 🔥 Escalation Detection
|
861 |
-
"""
|
862 |
-
|
863 |
-
escalation = results['temporal_analysis']['escalation_patterns']
|
864 |
-
if escalation.get('detected'):
|
865 |
-
report += f"""
|
866 |
-
**⚠️ ESCALATION DETECTED**
|
867 |
-
- **Severity:** {escalation['severity'].upper()}
|
868 |
-
- **Increase:** {escalation['increase_amount']}% over {escalation['timeframe']}
|
869 |
-
- **Confidence:** {escalation['confidence']:.1%}
|
870 |
-
- **Description:** {escalation['description']}
|
871 |
-
"""
|
872 |
-
else:
|
873 |
-
report += f"✅ No significant escalation detected ({escalation.get('reason', 'stable patterns')})"
|
874 |
-
|
875 |
-
### Cyclical Patterns
|
876 |
-
report += "\n### 🔄 Cyclical Abuse Patterns\n"
|
877 |
-
cycles = results['temporal_analysis']['cyclical_patterns']
|
878 |
-
if cycles.get('detected'):
|
879 |
-
report += f"""
|
880 |
-
**🔄 ABUSE CYCLES DETECTED**
|
881 |
-
- **Cycle Count:** {cycles['cycle_count']}
|
882 |
-
- **Average Length:** {cycles['avg_cycle_length_days']} days
|
883 |
-
- **Pattern:** {cycles['pattern_type'].replace('_', ' ').title()}
|
884 |
-
- **Confidence:** {cycles['confidence']:.1%}
|
885 |
-
- **Description:** {cycles['description']}
|
886 |
-
"""
|
887 |
-
else:
|
888 |
-
report += f"✅ No cyclical patterns detected ({cycles.get('reason', 'linear patterns')})"
|
889 |
-
|
890 |
-
### Dangerous Combinations
|
891 |
-
report += "\n### ⚠️ Dangerous Pattern Combinations\n"
|
892 |
-
combinations = results['temporal_analysis']['pattern_combinations']
|
893 |
-
if combinations:
|
894 |
-
for combo in combinations:
|
895 |
-
severity_emoji = "🚨" if combo['severity'] == 'critical
|
896 |
-
severity_emoji = "🚨" if combo['severity'] == 'critical' else "⚠️"
|
897 |
-
report += f"""
|
898 |
-
**{severity_emoji} {combo['name']}**
|
899 |
-
- **Severity:** {combo['severity'].upper()}
|
900 |
-
- **Patterns Found:** {', '.join(combo['present_patterns'])}
|
901 |
-
- **Frequency:** {combo['frequency']} occurrences
|
902 |
-
- **Risk Assessment:** {combo['risk_level'].upper()}
|
903 |
-
"""
|
904 |
-
else:
|
905 |
-
report += "✅ No dangerous pattern combinations detected"
|
906 |
-
|
907 |
-
### Risk Trajectory
|
908 |
-
risk = results['risk_assessment']
|
909 |
-
report += f"""
|
910 |
-
|
911 |
-
## 🎯 Risk Assessment & Prediction
|
912 |
-
|
913 |
-
**Current Risk Level:** {risk['current_risk']}%
|
914 |
-
**Trend Direction:** {risk['trend'].replace('_', ' ').title()}
|
915 |
-
**Trend Strength:** {risk['trend_strength']:.2f}
|
916 |
-
**Prediction Confidence:** {risk['confidence']:.1%}
|
917 |
-
**7-Day Forecast:** {risk['prediction_7_days']:.1f}%
|
918 |
-
**Intervention Priority:** {risk['intervention_urgency'].upper()}
|
919 |
-
|
920 |
-
"""
|
921 |
-
|
922 |
-
# Detected Patterns Summary
|
923 |
-
if analyzed_messages:
|
924 |
-
all_patterns = []
|
925 |
-
for msg in analyzed_messages:
|
926 |
-
all_patterns.extend(msg.detected_patterns)
|
927 |
-
|
928 |
-
if all_patterns:
|
929 |
-
pattern_counts = Counter(all_patterns)
|
930 |
-
report += "## 🔍 Detected Abuse Patterns\n\n"
|
931 |
-
|
932 |
-
for pattern, count in pattern_counts.most_common():
|
933 |
-
report += f"- **{pattern.replace('_', ' ').title()}**: {count} occurrences\n"
|
934 |
-
|
935 |
-
# Professional Recommendations
|
936 |
-
recommendations = results['professional_recommendations']
|
937 |
-
report += "\n## 💡 Professional Recommendations\n\n"
|
938 |
-
|
939 |
-
for category, items in recommendations.items():
|
940 |
-
if items:
|
941 |
-
report += f"### {category.replace('_', ' ').title()}\n"
|
942 |
-
for item in items:
|
943 |
-
report += f"• {item}\n"
|
944 |
-
report += "\n"
|
945 |
-
|
946 |
-
# Create visualization
|
947 |
-
viz_data = results.get('visualizations', {}).get('timeline', [])
|
948 |
-
timeline_chart = create_temporal_visualization(viz_data)
|
949 |
-
|
950 |
-
# Generate summary
|
951 |
-
if risk['intervention_urgency'] == 'critical':
|
952 |
-
summary = "🚨 CRITICAL: Immediate professional intervention recommended"
|
953 |
-
elif risk['intervention_urgency'] == 'high':
|
954 |
-
summary = "⚠️ HIGH RISK: Enhanced monitoring and support needed"
|
955 |
-
elif escalation.get('detected'):
|
956 |
-
summary = "📈 ESCALATION DETECTED: Abuse patterns show increasing intensity"
|
957 |
-
elif combinations:
|
958 |
-
summary = "⚠️ CONCERNING PATTERNS: Multiple manipulation tactics identified"
|
959 |
-
else:
|
960 |
-
summary = "📊 ANALYSIS COMPLETE: Full temporal and pattern analysis performed"
|
961 |
-
|
962 |
-
return report, timeline_chart, summary
|
963 |
-
|
964 |
-
except Exception as e:
|
965 |
-
logger.error(f"Error in analyze_temporal_patterns: {e}")
|
966 |
-
return (
|
967 |
-
f"❌ Analysis failed: {str(e)}\n\nPlease check your JSON format. Expected format:\n[\n {{\n \"timestamp\": \"2025-01-11 14:34:54\",\n \"sender\": \"+1234567890\",\n \"message\": \"Your message text here\"\n }}\n]",
|
968 |
-
None,
|
969 |
-
"Analysis error occurred."
|
970 |
-
)
|
971 |
-
|
972 |
-
def create_sample_data():
|
973 |
-
"""Generate sample conversation data for testing"""
|
974 |
-
sample_data = []
|
975 |
-
base_date = datetime.now() - timedelta(days=30)
|
976 |
-
|
977 |
-
# Generate 15 sample messages over 30 days
|
978 |
-
for i in range(15):
|
979 |
-
# Create escalating pattern
|
980 |
-
base_score = 25 + (i * 3.5) # Gradual escalation
|
981 |
-
if i % 5 == 0: # Periodic spikes
|
982 |
-
base_score += 20
|
983 |
-
|
984 |
-
abuse_score = min(95, max(5, base_score + np.random.normal(0, 8)))
|
985 |
-
|
986 |
-
sample_data.append({
|
987 |
-
'timestamp': (base_date + timedelta(days=i*2)).isoformat(),
|
988 |
-
'id': f'msg_{i:03d}',
|
989 |
-
'text': f'Sample message {i+1}',
|
990 |
-
'sender': 'person_a' if i % 2 == 0 else 'person_b',
|
991 |
-
'abuse_score': round(abuse_score, 1),
|
992 |
-
'darvo_score': round(min(1.0, max(0.0, 0.2 + (i * 0.05))), 3),
|
993 |
-
'boundary_health': 'unhealthy' if abuse_score > 50 else 'healthy',
|
994 |
-
'patterns': ['control', 'gaslighting'] if abuse_score > 60 else ['dismissiveness'],
|
995 |
-
'emotional_tone': 'menacing_calm' if abuse_score > 70 else 'neutral',
|
996 |
-
'risk_level': 'high' if abuse_score > 70 else 'moderate' if abuse_score > 40 else 'low'
|
997 |
-
})
|
998 |
-
|
999 |
-
return json.dumps(sample_data, indent=2)
|
1000 |
-
|
1001 |
-
def create_tether_pro_interface():
|
1002 |
-
css = """
|
1003 |
-
.gradio-container {
|
1004 |
-
max-width: 1200px !important;
|
1005 |
-
margin: 0 auto !important;
|
1006 |
-
}
|
1007 |
-
.pro-header {
|
1008 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
1009 |
-
border-radius: 16px;
|
1010 |
-
padding: 32px;
|
1011 |
-
margin-bottom: 24px;
|
1012 |
-
color: white;
|
1013 |
-
text-align: center;
|
1014 |
-
}
|
1015 |
-
.demo-notice {
|
1016 |
-
background: #fef3c7;
|
1017 |
-
border: 1px solid #f59e0b;
|
1018 |
-
border-radius: 8px;
|
1019 |
-
padding: 16px;
|
1020 |
-
margin: 16px 0;
|
1021 |
-
}
|
1022 |
-
"""
|
1023 |
-
|
1024 |
-
with gr.Blocks(css=css) as demo:
|
1025 |
-
gr.HTML("<div class='pro-header'><h1>Tether Pro Temporal Analysis</h1></div>")
|
1026 |
-
gr.HTML("<div class='demo-notice'>Paste conversation history as JSON or load sample data.</div>")
|
1027 |
-
input_json = gr.Textbox(
|
1028 |
-
label="Conversation History (JSON)",
|
1029 |
-
lines=10,
|
1030 |
-
placeholder="Paste your JSON here..."
|
1031 |
-
)
|
1032 |
-
with gr.Row():
|
1033 |
-
analyze_btn = gr.Button("Analyze")
|
1034 |
-
sample_btn = gr.Button("Load Sample Data")
|
1035 |
-
output_report = gr.Markdown(label="Analysis Report")
|
1036 |
-
output_image = gr.Image(label="Timeline Chart")
|
1037 |
-
output_summary = gr.Textbox(label="Summary", lines=1)
|
1038 |
-
|
1039 |
-
analyze_btn.click(
|
1040 |
-
fn=analyze_temporal_patterns,
|
1041 |
-
inputs=input_json,
|
1042 |
-
outputs=[output_report, output_image, output_summary]
|
1043 |
-
)
|
1044 |
-
sample_btn.click(
|
1045 |
-
fn=create_sample_data,
|
1046 |
-
inputs=None,
|
1047 |
-
outputs=input_json
|
1048 |
-
)
|
1049 |
-
|
1050 |
-
return demo
|
1051 |
|
1052 |
if __name__ == "__main__":
|
1053 |
-
demo
|
1054 |
-
demo.launch()
|
|
|
1 |
+
# main.py
|
2 |
+
# (launch point)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
+
from interface import demo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
if __name__ == "__main__":
|
7 |
+
demo.launch()
|
|