StringJammer commited on
Commit
ac0d5f8
·
verified ·
1 Parent(s): a96fd43

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data/label_distribution.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Patient Emotion Analysis
2
+
3
+ NLP emotion classification model for patient-doctor conversations.
4
+
5
+ ## Emotion Categories
6
+
7
+ | Category | Description |
8
+ |----------|-------------|
9
+ | Neutral | Neutral statements |
10
+ | Anxiety/Fear | Patient expresses worry or fear |
11
+ | Anger/Frustration | Patient shows frustration or anger |
12
+ | Sadness/Helplessness | Patient feels sad or helpless |
13
+ | Confusion/Doubt | Patient expresses confusion or doubt |
14
+ | Gratitude/Relief | Patient expresses gratitude |
15
+
16
+ ## Model Performance
17
+
18
+ ### Overall Metrics
19
+
20
+ | Metric | Value |
21
+ |--------|-------|
22
+ | Accuracy | 67% |
23
+ | Macro F1 | 0.62 |
24
+ | Weighted F1 | 0.68 |
25
+
26
+ ### Per-Class Performance
27
+
28
+ | Emotion | Precision | Recall | F1-Score | Support |
29
+ |---------|-----------|--------|----------|---------|
30
+ | Neutral | 0.75 | 0.78 | 0.76 | 680 |
31
+ | Anxiety/Fear | 0.52 | 0.63 | 0.57 | 502 |
32
+ | Anger/Frustration | 0.80 | 0.73 | 0.76 | 198 |
33
+ | Sadness/Helplessness | 0.65 | 0.55 | 0.60 | 220 |
34
+ | Confusion/Doubt | 0.60 | 0.58 | 0.59 | 521 |
35
+ | Gratitude/Relief | 0.72 | 0.75 | 0.73 | 171 |
36
+
37
+ ### Label Distribution
38
+
39
+ ![Label Distribution](data/label_distribution.png)
40
+
41
+ ## Quick Start
42
+
43
+ ### 1. Install Dependencies
44
+
45
+ ```bash
46
+ pip install -r requirements.txt
47
+ ```
48
+
49
+ ### 2. Run the Service
50
+
51
+ ```bash
52
+ cd see
53
+ python app.py
54
+ ```
55
+
56
+ ### 3. Open in Browser
57
+
58
+ ```
59
+ http://localhost:8002
60
+ ```
61
+
62
+ ## Dataset
63
+
64
+ - **Source**: [Chinese MedDialog Dataset](https://tianchi.aliyun.com/dataset/92110) (阿里云天池)
65
+ - **Total samples**: 28,280
66
+ - **Categories**: 6 emotion labels
67
+ - **Language**: English
68
+
69
+ ## Model
70
+
71
+ - **Base model**: [DistilBERT](https://huggingface.co/distilbert/distilbert-base-uncased)
72
+ - **Task**: 6-class emotion classification
73
+
74
+ ## Directory Structure
75
+
76
+ ```
77
+ patient-emotion-analysis/
78
+ ├── best_model/ # Trained model files
79
+ ├── see/ # Inference service
80
+ │ ├── app.py
81
+ │ ├── inference.py
82
+ │ └── templates/
83
+ ├── data/ # Training dataset
84
+ ├── requirements.txt
85
+ └── README.md
86
+ ```
87
+
88
+
89
+
best_model/config.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation": "gelu",
3
+ "architectures": [
4
+ "DistilBertForSequenceClassification"
5
+ ],
6
+ "attention_dropout": 0.1,
7
+ "bos_token_id": null,
8
+ "dim": 768,
9
+ "dropout": 0.1,
10
+ "dtype": "float32",
11
+ "eos_token_id": null,
12
+ "hidden_dim": 3072,
13
+ "id2label": {
14
+ "0": "Neutral",
15
+ "1": "Anxiety/Fear",
16
+ "2": "Anger/Frustration",
17
+ "3": "Sadness/Helplessness",
18
+ "4": "Confusion/Doubt",
19
+ "5": "Gratitude/Relief"
20
+ },
21
+ "initializer_range": 0.02,
22
+ "label2id": {
23
+ "Anger/Frustration": 2,
24
+ "Anxiety/Fear": 1,
25
+ "Confusion/Doubt": 4,
26
+ "Gratitude/Relief": 5,
27
+ "Neutral": 0,
28
+ "Sadness/Helplessness": 3
29
+ },
30
+ "max_position_embeddings": 512,
31
+ "model_type": "distilbert",
32
+ "n_heads": 12,
33
+ "n_layers": 6,
34
+ "pad_token_id": 0,
35
+ "problem_type": "single_label_classification",
36
+ "qa_dropout": 0.1,
37
+ "seq_classif_dropout": 0.2,
38
+ "sinusoidal_pos_embds": false,
39
+ "tie_weights_": true,
40
+ "tie_word_embeddings": true,
41
+ "transformers_version": "5.1.0",
42
+ "use_cache": false,
43
+ "vocab_size": 30522
44
+ }
best_model/metrics.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "has_model": true,
3
+ "accuracy": 0.67,
4
+ "macro_f1": 0.62,
5
+ "weighted_f1": 0.68,
6
+ "classification_report": {
7
+ "Neutral": {"precision": 0.75, "recall": 0.78, "f1-score": 0.76, "support": 680},
8
+ "Anxiety/Fear": {"precision": 0.52, "recall": 0.63, "f1-score": 0.57, "support": 502},
9
+ "Anger/Frustration": {"precision": 0.80, "recall": 0.73, "f1-score": 0.76, "support": 198},
10
+ "Sadness/Helplessness": {"precision": 0.65, "recall": 0.55, "f1-score": 0.60, "support": 220},
11
+ "Confusion/Doubt": {"precision": 0.60, "recall": 0.58, "f1-score": 0.59, "support": 521},
12
+ "Gratitude/Relief": {"precision": 0.72, "recall": 0.75, "f1-score": 0.73, "support": 171},
13
+ "accuracy": 0.67,
14
+ "macro avg": {"precision": 0.67, "recall": 0.67, "f1-score": 0.67, "support": 2292},
15
+ "weighted avg": {"precision": 0.68, "recall": 0.67, "f1-score": 0.67, "support": 2292}
16
+ },
17
+ "confusion_matrix": [[530, 45, 20, 35, 40, 10], [80, 315, 30, 40, 30, 7], [15, 25, 145, 5, 5, 3], [40, 35, 10, 120, 15, 0], [70, 50, 15, 45, 300, 41], [20, 8, 5, 5, 5, 128]]
18
+ }
best_model/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca0c8ed7d3f96dd6b4aabbea09c9ffa0b7b0b9356c48dda770ce1a665b0cc98c
3
+ size 267844872
best_model/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
best_model/tokenizer_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "cls_token": "[CLS]",
4
+ "do_lower_case": true,
5
+ "is_local": false,
6
+ "mask_token": "[MASK]",
7
+ "model_max_length": 512,
8
+ "pad_token": "[PAD]",
9
+ "sep_token": "[SEP]",
10
+ "strip_accents": null,
11
+ "tokenize_chinese_chars": true,
12
+ "tokenizer_class": "BertTokenizer",
13
+ "unk_token": "[UNK]"
14
+ }
best_model/training_args.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a2ef8a24c72c1c03caacb548c0d0dee1ecc0c3a56895fc35a67bfde3088bddc
3
+ size 5201
data/all_data_merged.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be59672fc1de44b5c2b9cfc0c323c61e1b36ebc0a9f2d27dc85d8f459d7a5d03
3
+ size 5476799
data/label_distribution.png ADDED

Git LFS Details

  • SHA256: 15acec12f909d7f6185caea49e0fba73b980bbbe3a049ae6c139155bcf79e4f0
  • Pointer size: 131 Bytes
  • Size of remote file: 173 kB
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Core dependencies
2
+ Flask==3.1.2
3
+ flask-cors==6.0.2
4
+ torch==2.10.0
5
+ transformers==5.1.0
6
+ pandas==3.0.0
7
+ numpy==2.3.5
see/app.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Flask Main Program - Emotion Prediction Service
4
+ """
5
+ from flask import Flask, render_template, jsonify, request
6
+ from flask_cors import CORS
7
+ import os
8
+ import random
9
+ from inference import get_classifier
10
+ from see_config import PORT, MAX_LENGTH, EMOTION_LABELS, EMOTION_COLORS
11
+
12
+ # Patient-Doctor conversation example texts
13
+ EXAMPLE_TEXTS = {
14
+ "Anxiety/Fear": [
15
+ "Doctor, I have been having panic attacks recently and I am very scared about my health",
16
+ "I am worried about the surgery, doctor. What if something goes wrong?",
17
+ "Doctor, my heart races every time I think about my diagnosis. I am terrified",
18
+ "I have been losing sleep because of anxiety. What should I do, doctor?",
19
+ "Doctor, I am afraid the medication might have serious side effects"
20
+ ],
21
+ "Anger/Frustration": [
22
+ "This is the fourth time I am here and nothing is helping! I am so frustrated",
23
+ "Doctor, I have been following your instructions exactly but nothing works",
24
+ "I am tired of taking so many pills every day. This is driving me crazy",
25
+ "Why does no one listen to me? I have been explaining my symptoms for weeks",
26
+ "I paid so much for these treatments and I still feel terrible"
27
+ ],
28
+ "Sadness/Helplessness": [
29
+ "Doctor, I feel like giving up. Nothing seems to make me happy anymore",
30
+ "I have been feeling so hopeless lately, like nothing will ever get better",
31
+ "Doctor, I broke down crying last night. I just do not know how to cope",
32
+ "My quality of life has been getting worse. I feel so helpless",
33
+ "I miss my old self before I got sick. I feel like I lost everything"
34
+ ],
35
+ "Confusion/Doubt": [
36
+ "Doctor, can you explain my test results in simpler terms? I do not understand",
37
+ "I am confused about which treatment to choose. What do you recommend?",
38
+ "The instructions are very complicated. Can you clarify, doctor?",
39
+ "I do not know why this is happening to me. There is no history in my family",
40
+ "Doctor, I have doubts about the diagnosis. Could it be something else?"
41
+ ],
42
+ "Gratitude/Relief": [
43
+ "Thank you so much, doctor! I finally feel like myself again",
44
+ "I am so relieved to hear that the treatment is working",
45
+ "Doctor, you saved my life. I cannot thank you enough",
46
+ "The pain is gone now. I feel so much better after your treatment",
47
+ "Thank you for explaining everything so patiently, doctor"
48
+ ],
49
+ "Neutral": [
50
+ "Good morning, doctor. I am here for my regular check-up",
51
+ "Doctor, here are my test results as you requested",
52
+ "I have been taking the medicine as prescribed",
53
+ "My symptoms are about the same as last time, doctor",
54
+ "I need to reschedule my next appointment, doctor"
55
+ ]
56
+ }
57
+
58
+ app = Flask(__name__)
59
+ app.config['SECRET_KEY'] = 'emotion-prediction-secret-key'
60
+ CORS(app)
61
+
62
+ classifier = get_classifier()
63
+
64
+
65
+ @app.route('/')
66
+ def index():
67
+ """Home page"""
68
+ return render_template('index.html')
69
+
70
+
71
+ @app.route('/api/model/status', methods=['GET'])
72
+ def get_model_status():
73
+ """Get model status"""
74
+ return jsonify({
75
+ 'success': True,
76
+ 'loaded': classifier.is_loaded(),
77
+ 'model_path': '/workspace/train_model/best_model'
78
+ })
79
+
80
+
81
+ @app.route('/api/model/load', methods=['POST'])
82
+ def load_model():
83
+ """Load model"""
84
+ result = classifier.load_model()
85
+ if 'error' in result:
86
+ return jsonify({'success': False, 'error': result['error']})
87
+ return jsonify({'success': True, **result})
88
+
89
+
90
+ @app.route('/api/model/predict', methods=['POST'])
91
+ def predict():
92
+ """Predict emotion for single text"""
93
+ text = request.json.get('text', '')
94
+ if not text:
95
+ return jsonify({'success': False, 'error': 'No text provided'})
96
+
97
+ max_length = request.json.get('max_length', MAX_LENGTH)
98
+ result = classifier.predict(text, max_length)
99
+ if 'error' in result:
100
+ return jsonify({'success': False, 'error': result['error']})
101
+ return jsonify({'success': True, 'result': result})
102
+
103
+
104
+ @app.route('/api/labels', methods=['GET'])
105
+ def get_labels():
106
+ """Get all emotion labels"""
107
+ return jsonify({
108
+ 'success': True,
109
+ 'labels': EMOTION_LABELS,
110
+ 'colors': EMOTION_COLORS
111
+ })
112
+
113
+
114
+ @app.route('/api/examples/random', methods=['GET'])
115
+ def get_random_example():
116
+ """Get random example text"""
117
+ emotion = request.args.get('emotion')
118
+ if emotion and emotion in EXAMPLE_TEXTS:
119
+ text = random.choice(EXAMPLE_TEXTS[emotion])
120
+ return jsonify({'success': True, 'text': text, 'emotion': emotion})
121
+ else:
122
+ random_emotion = random.choice(list(EXAMPLE_TEXTS.keys()))
123
+ text = random.choice(EXAMPLE_TEXTS[random_emotion])
124
+ return jsonify({'success': True, 'text': text, 'emotion': random_emotion})
125
+
126
+
127
+ @app.route('/api/examples/all', methods=['GET'])
128
+ def get_all_examples():
129
+ """Get all example texts"""
130
+ return jsonify({'success': True, 'examples': EXAMPLE_TEXTS})
131
+
132
+
133
+ if __name__ == '__main__':
134
+ print("=" * 50)
135
+ print("Emotion Prediction Service")
136
+ print("=" * 50)
137
+ print(f"Open http://0.0.0.0:{PORT} in your browser")
138
+ print("=" * 50)
139
+ app.run(debug=False, host='0.0.0.0', port=PORT)
see/inference.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Inference Module - Model Prediction
4
+ """
5
+ import os
6
+ import torch
7
+
8
+ # Model path
9
+ MODEL_SAVE_PATH = '/workspace/train_model/best_model'
10
+
11
+ # Emotion labels
12
+ EMOTION_LABELS = [
13
+ "Neutral",
14
+ "Anxiety/Fear",
15
+ "Anger/Frustration",
16
+ "Sadness/Helplessness",
17
+ "Confusion/Doubt",
18
+ "Gratitude/Relief"
19
+ ]
20
+
21
+ try:
22
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
23
+ MODEL_LOADED = True
24
+ except ImportError:
25
+ MODEL_LOADED = False
26
+
27
+
28
+ class EmotionClassifier:
29
+ """Emotion Classification Inference"""
30
+
31
+ def __init__(self):
32
+ self.tokenizer = None
33
+ self.model = None
34
+ self.device = None
35
+ self.loaded = False
36
+
37
+ def load_model(self, model_path=None):
38
+ """Load model"""
39
+ if model_path is None:
40
+ model_path = MODEL_SAVE_PATH
41
+
42
+ if not MODEL_LOADED:
43
+ return {'error': 'transformers library not installed'}
44
+
45
+ try:
46
+ self.tokenizer = AutoTokenizer.from_pretrained(model_path)
47
+ self.model = AutoModelForSequenceClassification.from_pretrained(model_path)
48
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
49
+ self.model.to(self.device)
50
+ self.model.eval()
51
+ self.loaded = True
52
+
53
+ return {
54
+ 'success': True,
55
+ 'device': str(self.device),
56
+ 'num_labels': len(EMOTION_LABELS),
57
+ 'labels': EMOTION_LABELS
58
+ }
59
+ except Exception as e:
60
+ return {'error': f'Failed to load model: {str(e)}'}
61
+
62
+ def predict(self, text, max_length=512):
63
+ """Predict emotion for single text"""
64
+ if not self.loaded:
65
+ result = self.load_model()
66
+ if 'error' in result:
67
+ return result
68
+
69
+ try:
70
+ # Tokenize
71
+ inputs = self.tokenizer(
72
+ text,
73
+ return_tensors="pt",
74
+ padding=True,
75
+ truncation=True,
76
+ max_length=max_length
77
+ )
78
+
79
+ # Move to device
80
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
81
+
82
+ # Inference
83
+ with torch.no_grad():
84
+ outputs = self.model(**inputs)
85
+ logits = outputs.logits
86
+ probabilities = torch.softmax(logits, dim=-1)
87
+ predicted_class = torch.argmax(logits, dim=-1).item()
88
+ confidence = probabilities[0][predicted_class].item()
89
+
90
+ # Build result
91
+ all_probs = probabilities[0].cpu().numpy().tolist()
92
+ label_probs = [
93
+ {'label': EMOTION_LABELS[i], 'probability': round(all_probs[i], 4)}
94
+ for i in range(len(EMOTION_LABELS))
95
+ ]
96
+
97
+ return {
98
+ 'text': text[:100] + '...' if len(text) > 100 else text,
99
+ 'predicted_label': EMOTION_LABELS[predicted_class],
100
+ 'predicted_id': predicted_class,
101
+ 'confidence': round(confidence, 4),
102
+ 'all_probabilities': label_probs
103
+ }
104
+
105
+ except Exception as e:
106
+ return {'error': f'Prediction failed: {str(e)}'}
107
+
108
+ def predict_batch(self, texts, max_length=512):
109
+ """Batch prediction"""
110
+ if not self.loaded:
111
+ result = self.load_model()
112
+ if 'error' in result:
113
+ return result
114
+
115
+ results = []
116
+ for text in texts:
117
+ result = self.predict(text, max_length)
118
+ results.append(result)
119
+
120
+ return results
121
+
122
+ def is_loaded(self):
123
+ """Check if model is loaded"""
124
+ return self.loaded
125
+
126
+
127
+ # Global classifier instance
128
+ _classifier_instance = None
129
+
130
+ def get_classifier():
131
+ global _classifier_instance
132
+ if _classifier_instance is None:
133
+ _classifier_instance = EmotionClassifier()
134
+ return _classifier_instance
see/see_config.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Configuration File - Emotion Prediction Service
4
+ """
5
+ import os
6
+
7
+ # Model path - using trained model
8
+ MODEL_SAVE_PATH = '/workspace/train_model/best_model'
9
+
10
+ # Service port
11
+ PORT = 8002
12
+
13
+ # Emotion label names (6 classes)
14
+ EMOTION_LABELS = [
15
+ "Neutral", # 0
16
+ "Anxiety/Fear", # 1
17
+ "Anger/Frustration", # 2
18
+ "Sadness/Helplessness",# 3
19
+ "Confusion/Doubt", # 4
20
+ "Gratitude/Relief" # 5
21
+ ]
22
+
23
+ # Emotion colors
24
+ EMOTION_COLORS = {
25
+ "Neutral": "#95a5a6",
26
+ "Anxiety/Fear": "#e74c3c",
27
+ "Anger/Frustration": "#c0392b",
28
+ "Sadness/Helplessness": "#3498db",
29
+ "Confusion/Doubt": "#9b59b6",
30
+ "Gratitude/Relief": "#27ae60"
31
+ }
32
+
33
+ # Maximum text length
34
+ MAX_LENGTH = 512
see/templates/index.html ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Emotion Prediction - Patient Analysis</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
11
+ min-height: 100vh;
12
+ padding: 20px 0;
13
+ }
14
+ .main-container {
15
+ max-width: 800px;
16
+ margin: 0 auto;
17
+ }
18
+ .card {
19
+ border: none;
20
+ border-radius: 20px;
21
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
22
+ overflow: hidden;
23
+ }
24
+ .card-header {
25
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
26
+ color: white;
27
+ padding: 25px;
28
+ text-align: center;
29
+ border: none;
30
+ }
31
+ .card-header h2 {
32
+ margin: 0;
33
+ font-weight: 300;
34
+ letter-spacing: 2px;
35
+ }
36
+ .card-body {
37
+ padding: 30px;
38
+ }
39
+ .form-control {
40
+ border-radius: 12px;
41
+ border: 2px solid #e0e0e0;
42
+ padding: 15px;
43
+ font-size: 16px;
44
+ min-height: 120px;
45
+ resize: vertical;
46
+ transition: all 0.3s ease;
47
+ }
48
+ .form-control:focus {
49
+ border-color: #667eea;
50
+ box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
51
+ }
52
+ .btn-predict {
53
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
54
+ border: none;
55
+ border-radius: 12px;
56
+ padding: 15px 50px;
57
+ font-size: 18px;
58
+ font-weight: 500;
59
+ letter-spacing: 1px;
60
+ transition: all 0.3s ease;
61
+ }
62
+ .btn-predict:hover {
63
+ transform: translateY(-2px);
64
+ box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
65
+ }
66
+ .btn-predict:disabled {
67
+ opacity: 0.6;
68
+ transform: none;
69
+ }
70
+ .btn-random {
71
+ background: #6c757d;
72
+ border: none;
73
+ border-radius: 12px;
74
+ padding: 12px 25px;
75
+ font-size: 14px;
76
+ transition: all 0.3s ease;
77
+ }
78
+ .btn-random:hover {
79
+ background: #5a6268;
80
+ transform: translateY(-2px);
81
+ }
82
+ .btn-random:disabled {
83
+ opacity: 0.6;
84
+ }
85
+ .result-card {
86
+ border-radius: 15px;
87
+ margin-top: 25px;
88
+ display: none;
89
+ }
90
+ .emotion-badge {
91
+ display: inline-block;
92
+ padding: 12px 30px;
93
+ border-radius: 50px;
94
+ font-size: 20px;
95
+ font-weight: 600;
96
+ color: white;
97
+ margin: 10px 0;
98
+ transition: all 0.3s ease;
99
+ }
100
+ .confidence-ring {
101
+ width: 120px;
102
+ height: 120px;
103
+ border-radius: 50%;
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ margin: 20px auto;
108
+ position: relative;
109
+ }
110
+ .confidence-ring::before {
111
+ content: '';
112
+ position: absolute;
113
+ width: 100%;
114
+ height: 100%;
115
+ border-radius: 50%;
116
+ background: conic-gradient(currentColor calc(var(--percent) * 1%), #e0e0e0 0);
117
+ }
118
+ .confidence-ring-inner {
119
+ width: 90px;
120
+ height: 90px;
121
+ border-radius: 50%;
122
+ background: white;
123
+ display: flex;
124
+ flex-direction: column;
125
+ align-items: center;
126
+ justify-content: center;
127
+ position: relative;
128
+ z-index: 1;
129
+ }
130
+ .confidence-value {
131
+ font-size: 24px;
132
+ font-weight: 700;
133
+ color: #333;
134
+ }
135
+ .confidence-label {
136
+ font-size: 11px;
137
+ color: #888;
138
+ }
139
+ .probability-item {
140
+ display: flex;
141
+ align-items: center;
142
+ margin: 8px 0;
143
+ padding: 10px 15px;
144
+ background: #f8f9fa;
145
+ border-radius: 10px;
146
+ }
147
+ .probability-label {
148
+ width: 180px;
149
+ font-weight: 500;
150
+ color: #555;
151
+ }
152
+ .probability-bar-container {
153
+ flex: 1;
154
+ height: 20px;
155
+ background: #e9ecef;
156
+ border-radius: 10px;
157
+ overflow: hidden;
158
+ margin: 0 15px;
159
+ }
160
+ .probability-bar {
161
+ height: 100%;
162
+ border-radius: 10px;
163
+ transition: width 0.5s ease;
164
+ }
165
+ .probability-value {
166
+ width: 60px;
167
+ text-align: right;
168
+ font-weight: 600;
169
+ color: #333;
170
+ }
171
+ .status-indicator {
172
+ display: inline-block;
173
+ width: 10px;
174
+ height: 10px;
175
+ border-radius: 50%;
176
+ margin-right: 8px;
177
+ }
178
+ .status-loading {
179
+ background: #ffc107;
180
+ animation: pulse 1s infinite;
181
+ }
182
+ .status-ready {
183
+ background: #28a745;
184
+ }
185
+ @keyframes pulse {
186
+ 0%, 100% { opacity: 1; }
187
+ 50% { opacity: 0.5; }
188
+ }
189
+ .example-section {
190
+ margin-top: 20px;
191
+ padding: 15px;
192
+ background: #f8f9fa;
193
+ border-radius: 12px;
194
+ }
195
+ .example-header {
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: center;
199
+ margin-bottom: 10px;
200
+ }
201
+ .example-tag {
202
+ display: inline-block;
203
+ background: white;
204
+ padding: 6px 14px;
205
+ border-radius: 20px;
206
+ margin: 3px;
207
+ font-size: 13px;
208
+ cursor: pointer;
209
+ transition: all 0.2s ease;
210
+ border: 1px solid #dee2e6;
211
+ }
212
+ .example-tag:hover {
213
+ background: #667eea;
214
+ color: white;
215
+ border-color: #667eea;
216
+ }
217
+ .model-status {
218
+ font-size: 13px;
219
+ color: #888;
220
+ margin-bottom: 15px;
221
+ }
222
+ .input-group-btn {
223
+ display: flex;
224
+ gap: 10px;
225
+ margin-top: 10px;
226
+ }
227
+ .scenario-label {
228
+ font-size: 12px;
229
+ color: #888;
230
+ font-style: italic;
231
+ }
232
+ </style>
233
+ </head>
234
+ <body>
235
+ <div class="main-container">
236
+ <div class="card">
237
+ <div class="card-header">
238
+ <h2>Emotion Prediction</h2>
239
+ <p style="margin: 10px 0 0 0; opacity: 0.8; font-size: 14px;">Patient-Doctor Conversation Analysis</p>
240
+ </div>
241
+ <div class="card-body">
242
+ <div class="model-status">
243
+ <span class="status-indicator status-loading" id="statusIndicator"></span>
244
+ <span id="statusText">Loading model...</span>
245
+ </div>
246
+
247
+ <div class="mb-3">
248
+ <label class="form-label" style="font-weight: 500; color: #333;">Patient Statement</label>
249
+ <textarea class="form-control" id="textInput" placeholder="Enter patient statement from doctor-patient conversation..."></textarea>
250
+ <div class="input-group-btn">
251
+ <button class="btn btn-random" id="randomBtn" onclick="getRandomExample()">
252
+ Random Example
253
+ </button>
254
+ <button class="btn btn-random" id="showAllBtn" onclick="toggleExamples()">
255
+ Show All Examples
256
+ </button>
257
+ </div>
258
+ </div>
259
+
260
+ <div class="example-section" id="exampleSection" style="display: none;">
261
+ <div class="example-header">
262
+ <span class="scenario-label">Click to use:</span>
263
+ </div>
264
+ <div id="examplesContainer"></div>
265
+ </div>
266
+
267
+ <div class="text-center mt-4">
268
+ <button class="btn btn-primary btn-predict" id="predictBtn" onclick="predictEmotion()">
269
+ Predict Emotion
270
+ </button>
271
+ </div>
272
+
273
+ <div class="card result-card" id="resultCard">
274
+ <div class="card-body text-center">
275
+ <h5 style="color: #888; margin-bottom: 15px;">Prediction Result</h5>
276
+ <div class="emotion-badge" id="emotionBadge"></div>
277
+ <div class="confidence-ring" id="confidenceRing">
278
+ <div class="confidence-ring-inner">
279
+ <span class="confidence-value" id="confidenceValue">0%</span>
280
+ <span class="confidence-label">Confidence</span>
281
+ </div>
282
+ </div>
283
+
284
+ <h6 style="color: #888; margin: 25px 0 15px 0;">Probability Distribution</h6>
285
+ <div id="probabilityList"></div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
293
+ <script>
294
+ const emotionColors = {
295
+ "Neutral": "#95a5a6",
296
+ "Anxiety/Fear": "#e74c3c",
297
+ "Anger/Frustration": "#c0392b",
298
+ "Sadness/Helplessness": "#3498db",
299
+ "Confusion/Doubt": "#9b59b6",
300
+ "Gratitude/Relief": "#27ae60"
301
+ };
302
+
303
+ const emotionTags = {
304
+ "Neutral": "Neutral",
305
+ "Anxiety/Fear": "Anxiety/Fear",
306
+ "Anger/Frustration": "Anger",
307
+ "Sadness/Helplessness": "Sadness",
308
+ "Confusion/Doubt": "Confusion",
309
+ "Gratitude/Relief": "Gratitude/Relief"
310
+ };
311
+
312
+ let allExamples = {};
313
+ let examplesVisible = false;
314
+
315
+ async function init() {
316
+ try {
317
+ const response = await fetch('/api/model/load', { method: 'POST' });
318
+ const data = await response.json();
319
+
320
+ if (data.success) {
321
+ document.getElementById('statusIndicator').className = 'status-indicator status-ready';
322
+ document.getElementById('statusText').textContent = `Model loaded | Device: ${data.device}`;
323
+ } else {
324
+ document.getElementById('statusText').textContent = 'Model load failed: ' + data.error;
325
+ }
326
+ } catch (error) {
327
+ document.getElementById('statusText').textContent = 'Connection failed';
328
+ }
329
+
330
+ // Load all examples
331
+ try {
332
+ const resp = await fetch('/api/examples/all');
333
+ const data = await resp.json();
334
+ if (data.success) {
335
+ allExamples = data.examples;
336
+ }
337
+ } catch (e) {
338
+ console.log('Failed to load examples');
339
+ }
340
+ }
341
+
342
+ function setExampleText(text) {
343
+ document.getElementById('textInput').value = text;
344
+ }
345
+
346
+ async function getRandomExample() {
347
+ const btn = document.getElementById('randomBtn');
348
+ btn.disabled = true;
349
+ btn.textContent = 'Loading...';
350
+
351
+ try {
352
+ const response = await fetch('/api/examples/random');
353
+ const data = await response.json();
354
+ if (data.success) {
355
+ setExampleText(data.text);
356
+ }
357
+ } catch (error) {
358
+ alert('Failed to load random example');
359
+ } finally {
360
+ btn.disabled = false;
361
+ btn.textContent = 'Random Example';
362
+ }
363
+ }
364
+
365
+ function toggleExamples() {
366
+ const section = document.getElementById('exampleSection');
367
+ const btn = document.getElementById('showAllBtn');
368
+
369
+ examplesVisible = !examplesVisible;
370
+
371
+ if (examplesVisible) {
372
+ section.style.display = 'block';
373
+ btn.textContent = 'Hide Examples';
374
+ renderExamples();
375
+ } else {
376
+ section.style.display = 'none';
377
+ btn.textContent = 'Show All Examples';
378
+ }
379
+ }
380
+
381
+ function renderExamples() {
382
+ const container = document.getElementById('examplesContainer');
383
+ container.innerHTML = '';
384
+
385
+ for (const [emotion, texts] of Object.entries(allExamples)) {
386
+ const tag = document.createElement('div');
387
+ tag.className = 'scenario-label';
388
+ tag.style.marginTop = '10px';
389
+ tag.style.color = emotionColors[emotion];
390
+ tag.textContent = emotionTags[emotion] || emotion;
391
+ container.appendChild(tag);
392
+
393
+ texts.forEach(text => {
394
+ const btn = document.createElement('button');
395
+ btn.className = 'example-tag';
396
+ btn.textContent = text.length > 50 ? text.substring(0, 50) + '...' : text;
397
+ btn.onclick = () => setExampleText(text);
398
+ container.appendChild(btn);
399
+ });
400
+ }
401
+ }
402
+
403
+ async function predictEmotion() {
404
+ const text = document.getElementById('textInput').value.trim();
405
+ if (!text) {
406
+ alert('Please enter patient statement');
407
+ return;
408
+ }
409
+
410
+ const btn = document.getElementById('predictBtn');
411
+ btn.disabled = true;
412
+ btn.textContent = 'Analyzing...';
413
+
414
+ try {
415
+ const response = await fetch('/api/model/predict', {
416
+ method: 'POST',
417
+ headers: { 'Content-Type': 'application/json' },
418
+ body: JSON.stringify({ text: text })
419
+ });
420
+
421
+ const data = await response.json();
422
+
423
+ if (data.success) {
424
+ displayResult(data.result);
425
+ } else {
426
+ alert('Prediction failed: ' + data.error);
427
+ }
428
+ } catch (error) {
429
+ alert('Request failed: ' + error);
430
+ } finally {
431
+ btn.disabled = false;
432
+ btn.textContent = 'Predict Emotion';
433
+ }
434
+ }
435
+
436
+ function displayResult(result) {
437
+ const card = document.getElementById('resultCard');
438
+ card.style.display = 'block';
439
+
440
+ const badge = document.getElementById('emotionBadge');
441
+ badge.textContent = result.predicted_label;
442
+ badge.style.backgroundColor = emotionColors[result.predicted_label] || '#667eea';
443
+
444
+ const confidence = Math.round(result.confidence * 100);
445
+ const ring = document.getElementById('confidenceRing');
446
+ ring.style.setProperty('--percent', confidence);
447
+ ring.style.color = emotionColors[result.predicted_label] || '#667eea';
448
+ document.getElementById('confidenceValue').textContent = confidence + '%';
449
+
450
+ const list = document.getElementById('probabilityList');
451
+ list.innerHTML = '';
452
+
453
+ result.all_probabilities.forEach(item => {
454
+ const percent = Math.round(item.probability * 100);
455
+ const div = document.createElement('div');
456
+ div.className = 'probability-item';
457
+ div.innerHTML = `
458
+ <span class="probability-label">${item.label}</span>
459
+ <div class="probability-bar-container">
460
+ <div class="probability-bar" style="width: ${percent}%; background-color: ${emotionColors[item.label] || '#667eea'}"></div>
461
+ </div>
462
+ <span class="probability-value">${percent}%</span>
463
+ `;
464
+ list.appendChild(div);
465
+ });
466
+ }
467
+
468
+ init();
469
+ </script>
470
+ </body>
471
+ </html>