SamanthaStorm commited on
Commit
7cff5f1
·
verified ·
1 Parent(s): d33482f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +4 -1051
app.py CHANGED
@@ -1,1054 +1,7 @@
1
- # Import necessary libraries
2
- import gradio as gr
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
- # Set up logging
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 = create_tether_pro_interface()
1054
- demo.launch()
 
1
+ # main.py
2
+ # (launch point)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ from interface import demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  if __name__ == "__main__":
7
+ demo.launch()