Marwan-Tamer commited on
Commit
297cdf3
·
1 Parent(s): c1fb7b1

feat: add module 2 emotion classifier

Browse files
README.md CHANGED
@@ -31,6 +31,48 @@ reports/module_1_language_detection/
31
 
32
  The UI returns the detected language, confidence, and whether the prediction passed the confidence threshold.
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  ### Install Dependencies
35
 
36
  ```bash
 
31
 
32
  The UI returns the detected language, confidence, and whether the prediction passed the confidence threshold.
33
 
34
+ ## Module 2: Emotion Classification
35
+
36
+ The emotion classifier uses a fine-tuned transformer:
37
+
38
+ - Base model: `distilbert-base-uncased`
39
+ - Dataset: `dair-ai/emotion`
40
+ - Labels: sadness, joy, love, anger, fear, surprise
41
+ - Training target: run on Colab T4 using `notebooks/module_2_emotion_training.ipynb`
42
+
43
+ DistilBERT is used because it keeps most of BERT's language understanding while being smaller and faster, which makes it a better fit for a student project that needs GPU training but practical local inference.
44
+
45
+ After training in Colab, the notebook saves:
46
+
47
+ ```text
48
+ src/models/saved_emotion_model/
49
+ reports/module_2_emotion_classification/
50
+ ```
51
+
52
+ The local inference class returns the predicted emotion, confidence, and a simple word-occlusion explanation showing which words most affected the predicted emotion.
53
+
54
+ ### Module Integration Plan
55
+
56
+ The final chatbot will analyze each user message in this order:
57
+
58
+ ```text
59
+ User message -> Language Detection -> Emotion Classification -> Intent Classification -> RAG/direct response
60
+ ```
61
+
62
+ Module 1 decides the language for routing and response language. Module 2 adds emotional context so later response generation can be gentler for sadness/fear/anger and more direct for neutral informational requests. Crisis handling should still be implemented as a separate safety route later, not inferred from emotion alone.
63
+
64
+ Run emotion inference after exporting the trained model:
65
+
66
+ ```bash
67
+ .\.venv\Scripts\python.exe src\models\emotion_classifier.py "I feel anxious and overwhelmed" --explain
68
+ ```
69
+
70
+ Run the emotion UI:
71
+
72
+ ```bash
73
+ .\.venv\Scripts\python.exe src\models\emotion_detector_ui.py
74
+ ```
75
+
76
  ### Install Dependencies
77
 
78
  ```bash
notebooks/module_2_emotion_training.ipynb ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "371f3178",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Module 2: Emotion Classification\n",
9
+ "\n",
10
+ "This notebook fine-tunes a transformer emotion classifier for the mental health chatbot. It is designed for Google Colab with a T4 GPU, while the project repo keeps the reusable inference and explanation code locally."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "markdown",
15
+ "id": "57bcd49f",
16
+ "metadata": {},
17
+ "source": [
18
+ "## Why DistilBERT?\n",
19
+ "\n",
20
+ "We use `distilbert-base-uncased` because it is transformer-based, lighter than BERT, fast enough for Colab T4 training, and practical for local inference. For this project, that balance is better than a larger model that may be harder to deploy or explain during assessment.\n",
21
+ "\n",
22
+ "Dataset: `dair-ai/emotion` with six labels: sadness, joy, love, anger, fear, and surprise."
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "id": "d22909ef",
29
+ "metadata": {},
30
+ "outputs": [],
31
+ "source": [
32
+ "!nvidia-smi"
33
+ ]
34
+ },
35
+ {
36
+ "cell_type": "code",
37
+ "execution_count": null,
38
+ "id": "19fba046",
39
+ "metadata": {},
40
+ "outputs": [],
41
+ "source": [
42
+ "%pip -q install -U transformers datasets accelerate evaluate scikit-learn pandas"
43
+ ]
44
+ },
45
+ {
46
+ "cell_type": "markdown",
47
+ "id": "4c57f7d2",
48
+ "metadata": {},
49
+ "source": [
50
+ "## Colab Repo Setup\n",
51
+ "\n",
52
+ "Run this notebook from the cloned project folder so the trained model and reports are saved back into the same structure used locally."
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": null,
58
+ "id": "bfb00d86",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "# If you opened the notebook outside the repo in Colab, uncomment these lines once:\n",
63
+ "# !git clone https://github.com/MarwanZaineldeen/Mental-Health-Chatbot.git\n",
64
+ "# %cd Mental-Health-Chatbot"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "execution_count": null,
70
+ "id": "64279088",
71
+ "metadata": {},
72
+ "outputs": [],
73
+ "source": [
74
+ "import json\n",
75
+ "import sys\n",
76
+ "from inspect import signature\n",
77
+ "from pathlib import Path\n",
78
+ "\n",
79
+ "import numpy as np\n",
80
+ "import pandas as pd\n",
81
+ "from datasets import load_dataset\n",
82
+ "from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, f1_score\n",
83
+ "from transformers import AutoModelForSequenceClassification, AutoTokenizer, DataCollatorWithPadding, Trainer, TrainingArguments\n",
84
+ "\n",
85
+ "PROJECT_ROOT = Path.cwd().resolve().parent if Path.cwd().name == 'notebooks' else Path.cwd().resolve()\n",
86
+ "MODEL_DIR = PROJECT_ROOT / 'src' / 'models' / 'saved_emotion_model'\n",
87
+ "REPORT_DIR = PROJECT_ROOT / 'reports' / 'module_2_emotion_classification'\n",
88
+ "MODEL_NAME = 'distilbert-base-uncased'\n",
89
+ "\n",
90
+ "MODEL_DIR.mkdir(parents=True, exist_ok=True)\n",
91
+ "REPORT_DIR.mkdir(parents=True, exist_ok=True)\n",
92
+ "sys.path.append(str(PROJECT_ROOT / 'src' / 'models'))"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": null,
98
+ "id": "f5675968",
99
+ "metadata": {},
100
+ "outputs": [],
101
+ "source": [
102
+ "dataset = load_dataset('dair-ai/emotion', 'split')\n",
103
+ "label_names = dataset['train'].features['label'].names\n",
104
+ "id2label = {i: label for i, label in enumerate(label_names)}\n",
105
+ "label2id = {label: i for i, label in id2label.items()}\n",
106
+ "\n",
107
+ "print(dataset)\n",
108
+ "print(id2label)\n",
109
+ "pd.DataFrame(dataset['train'][:5])"
110
+ ]
111
+ },
112
+ {
113
+ "cell_type": "code",
114
+ "execution_count": null,
115
+ "id": "d92e7b96",
116
+ "metadata": {},
117
+ "outputs": [],
118
+ "source": [
119
+ "tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)\n",
120
+ "\n",
121
+ "def tokenize(batch):\n",
122
+ " return tokenizer(batch['text'], truncation=True, max_length=128)\n",
123
+ "\n",
124
+ "encoded = dataset.map(tokenize, batched=True)\n",
125
+ "encoded = encoded.rename_column('label', 'labels')\n",
126
+ "encoded.set_format(type='torch', columns=['input_ids', 'attention_mask', 'labels'])\n",
127
+ "data_collator = DataCollatorWithPadding(tokenizer=tokenizer)"
128
+ ]
129
+ },
130
+ {
131
+ "cell_type": "code",
132
+ "execution_count": null,
133
+ "id": "de89cb7b",
134
+ "metadata": {},
135
+ "outputs": [],
136
+ "source": [
137
+ "model = AutoModelForSequenceClassification.from_pretrained(\n",
138
+ " MODEL_NAME,\n",
139
+ " num_labels=len(label_names),\n",
140
+ " id2label=id2label,\n",
141
+ " label2id=label2id,\n",
142
+ ")\n",
143
+ "\n",
144
+ "def compute_metrics(eval_pred):\n",
145
+ " logits, labels = eval_pred\n",
146
+ " predictions = np.argmax(logits, axis=-1)\n",
147
+ " return {\n",
148
+ " 'accuracy': accuracy_score(labels, predictions),\n",
149
+ " 'macro_f1': f1_score(labels, predictions, average='macro'),\n",
150
+ " }\n",
151
+ "\n",
152
+ "training_kwargs = {\n",
153
+ " 'output_dir': str(PROJECT_ROOT / 'checkpoints' / 'emotion_distilbert'),\n",
154
+ " 'learning_rate': 2e-5,\n",
155
+ " 'per_device_train_batch_size': 16,\n",
156
+ " 'per_device_eval_batch_size': 32,\n",
157
+ " 'num_train_epochs': 3,\n",
158
+ " 'weight_decay': 0.01,\n",
159
+ " 'save_strategy': 'epoch',\n",
160
+ " 'load_best_model_at_end': True,\n",
161
+ " 'metric_for_best_model': 'macro_f1',\n",
162
+ " 'greater_is_better': True,\n",
163
+ " 'logging_steps': 50,\n",
164
+ " 'report_to': 'none',\n",
165
+ "}\n",
166
+ "\n",
167
+ "strategy_name = 'eval_strategy' if 'eval_strategy' in signature(TrainingArguments).parameters else 'evaluation_strategy'\n",
168
+ "training_kwargs[strategy_name] = 'epoch'\n",
169
+ "\n",
170
+ "args = TrainingArguments(**training_kwargs)\n",
171
+ "\n",
172
+ "trainer_kwargs = {\n",
173
+ " 'model': model,\n",
174
+ " 'args': args,\n",
175
+ " 'train_dataset': encoded['train'],\n",
176
+ " 'eval_dataset': encoded['validation'],\n",
177
+ " 'data_collator': data_collator,\n",
178
+ " 'compute_metrics': compute_metrics,\n",
179
+ "}\n",
180
+ "\n",
181
+ "tokenizer_arg = 'processing_class' if 'processing_class' in signature(Trainer).parameters else 'tokenizer'\n",
182
+ "trainer_kwargs[tokenizer_arg] = tokenizer\n",
183
+ "\n",
184
+ "trainer = Trainer(**trainer_kwargs)"
185
+ ]
186
+ },
187
+ {
188
+ "cell_type": "code",
189
+ "execution_count": null,
190
+ "id": "52e9a3be",
191
+ "metadata": {},
192
+ "outputs": [],
193
+ "source": [
194
+ "trainer.train()\n",
195
+ "validation_metrics = trainer.evaluate(encoded['validation'])\n",
196
+ "test_output = trainer.predict(encoded['test'])\n",
197
+ "\n",
198
+ "test_predictions = np.argmax(test_output.predictions, axis=-1)\n",
199
+ "test_labels = test_output.label_ids\n",
200
+ "test_metrics = compute_metrics((test_output.predictions, test_labels))\n",
201
+ "\n",
202
+ "print(validation_metrics)\n",
203
+ "print(test_metrics)"
204
+ ]
205
+ },
206
+ {
207
+ "cell_type": "code",
208
+ "execution_count": null,
209
+ "id": "7c79dfb5",
210
+ "metadata": {},
211
+ "outputs": [],
212
+ "source": [
213
+ "model.save_pretrained(MODEL_DIR)\n",
214
+ "tokenizer.save_pretrained(MODEL_DIR)\n",
215
+ "\n",
216
+ "report_text = classification_report(test_labels, test_predictions, target_names=label_names, zero_division=0)\n",
217
+ "report_dict = classification_report(test_labels, test_predictions, target_names=label_names, output_dict=True, zero_division=0)\n",
218
+ "matrix = confusion_matrix(test_labels, test_predictions)\n",
219
+ "\n",
220
+ "(REPORT_DIR / 'test_classification_report.txt').write_text(report_text, encoding='utf-8')\n",
221
+ "pd.DataFrame(report_dict).transpose().to_csv(REPORT_DIR / 'test_classification_report.csv')\n",
222
+ "pd.DataFrame(matrix, index=label_names, columns=label_names).to_csv(REPORT_DIR / 'test_confusion_matrix.csv')\n",
223
+ "\n",
224
+ "summary = {\n",
225
+ " 'base_model': MODEL_NAME,\n",
226
+ " 'dataset': 'dair-ai/emotion',\n",
227
+ " 'labels': label_names,\n",
228
+ " 'validation_metrics': validation_metrics,\n",
229
+ " 'test_metrics': test_metrics,\n",
230
+ " 'explainability_method': 'word occlusion on final predicted confidence',\n",
231
+ "}\n",
232
+ "(REPORT_DIR / 'metrics_summary.json').write_text(json.dumps(summary, indent=2), encoding='utf-8')\n",
233
+ "\n",
234
+ "print(report_text)\n",
235
+ "print(f'Saved model to: {MODEL_DIR}')\n",
236
+ "print(f'Saved reports to: {REPORT_DIR}')"
237
+ ]
238
+ },
239
+ {
240
+ "cell_type": "markdown",
241
+ "id": "349facfc",
242
+ "metadata": {},
243
+ "source": [
244
+ "## Explainability Check\n",
245
+ "\n",
246
+ "This is not a formal clinical explanation. It is a practical debugging tool: remove one word at a time and measure how much the model's confidence in the predicted emotion drops."
247
+ ]
248
+ },
249
+ {
250
+ "cell_type": "code",
251
+ "execution_count": null,
252
+ "id": "225e1691",
253
+ "metadata": {},
254
+ "outputs": [],
255
+ "source": [
256
+ "from emotion_classifier import EmotionClassifier\n",
257
+ "\n",
258
+ "classifier = EmotionClassifier(model_dir=MODEL_DIR)\n",
259
+ "examples = [\n",
260
+ " 'I feel anxious and overwhelmed and I cannot sleep.',\n",
261
+ " 'I finally feel hopeful and proud of myself.',\n",
262
+ " 'I am angry because nobody listens to me.',\n",
263
+ "]\n",
264
+ "\n",
265
+ "explanations = {text: classifier.explain(text, top_k=6) for text in examples}\n",
266
+ "(REPORT_DIR / 'explanation_examples.json').write_text(json.dumps(explanations, indent=2), encoding='utf-8')\n",
267
+ "explanations"
268
+ ]
269
+ }
270
+ ],
271
+ "metadata": {
272
+ "accelerator": "GPU",
273
+ "kernelspec": {
274
+ "display_name": "Python 3",
275
+ "language": "python",
276
+ "name": "python3"
277
+ },
278
+ "language_info": {
279
+ "name": "python",
280
+ "pygments_lexer": "ipython3"
281
+ }
282
+ },
283
+ "nbformat": 4,
284
+ "nbformat_minor": 5
285
+ }
reports/module_2_emotion_classification/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Module 2 Reports
2
+
3
+ This folder is populated after running `notebooks/module_2_emotion_training.ipynb` in Colab.
4
+
5
+ Expected generated files:
6
+
7
+ - `metrics_summary.json`
8
+ - `test_classification_report.txt`
9
+ - `test_classification_report.csv`
10
+ - `test_confusion_matrix.csv`
11
+ - `explanation_examples.json`
requirements.txt CHANGED
@@ -3,4 +3,8 @@ scikit-learn==1.9.0
3
  joblib==1.5.3
4
  gradio==6.18.0
5
  datasets==5.0.0
 
 
 
 
6
  jupyter
 
3
  joblib==1.5.3
4
  gradio==6.18.0
5
  datasets==5.0.0
6
+ transformers
7
+ torch
8
+ accelerate
9
+ evaluate
10
  jupyter
src/models/emotion_classifier.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
11
+ DEFAULT_MODEL_DIR = PROJECT_ROOT / "src" / "models" / "saved_emotion_model"
12
+
13
+
14
+ def _load_transformer_stack() -> tuple[Any, Any, Any]:
15
+ try:
16
+ import torch
17
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
18
+ except ImportError as exc:
19
+ raise ImportError(
20
+ "Module 2 requires torch and transformers. Install them with "
21
+ "`python -m pip install -r requirements.txt`, or run the Colab notebook."
22
+ ) from exc
23
+
24
+ return torch, AutoModelForSequenceClassification, AutoTokenizer
25
+
26
+
27
+ class EmotionClassifier:
28
+ """Transformer emotion classifier with confidence and simple word-occlusion explanations."""
29
+
30
+ def __init__(self, model_dir: str | Path = DEFAULT_MODEL_DIR) -> None:
31
+ self.model_dir = Path(model_dir)
32
+ self.torch = None
33
+ self.tokenizer = None
34
+ self.model = None
35
+ self.id2label: dict[int, str] = {}
36
+
37
+ def load_model(self) -> None:
38
+ if not self.model_dir.exists():
39
+ raise FileNotFoundError(
40
+ f"Emotion model not found at {self.model_dir}. "
41
+ "Train it first with notebooks/module_2_emotion_training.ipynb."
42
+ )
43
+
44
+ torch, model_cls, tokenizer_cls = _load_transformer_stack()
45
+ self.torch = torch
46
+ self.tokenizer = tokenizer_cls.from_pretrained(self.model_dir)
47
+ self.model = model_cls.from_pretrained(self.model_dir)
48
+ self.model.eval()
49
+
50
+ config_labels = self.model.config.id2label
51
+ self.id2label = {int(key): value for key, value in config_labels.items()}
52
+
53
+ def predict_with_confidence(self, text: str) -> dict[str, Any]:
54
+ clean_text = text.strip()
55
+ if not clean_text:
56
+ return {
57
+ "emotion": "unknown",
58
+ "confidence": 0.0,
59
+ "is_confident": False,
60
+ "message": "Please enter text to classify.",
61
+ }
62
+
63
+ if self.model is None or self.tokenizer is None or self.torch is None:
64
+ self.load_model()
65
+
66
+ inputs = self.tokenizer(
67
+ clean_text,
68
+ return_tensors="pt",
69
+ truncation=True,
70
+ max_length=128,
71
+ )
72
+
73
+ with self.torch.no_grad():
74
+ logits = self.model(**inputs).logits
75
+ probabilities = self.torch.softmax(logits, dim=-1)[0]
76
+
77
+ best_index = int(probabilities.argmax().item())
78
+ confidence = float(probabilities[best_index].item())
79
+
80
+ return {
81
+ "emotion": self.id2label.get(best_index, str(best_index)),
82
+ "confidence": confidence,
83
+ "is_confident": confidence >= 0.60,
84
+ "message": None,
85
+ }
86
+
87
+ def explain(self, text: str, top_k: int = 8) -> dict[str, Any]:
88
+ """Estimate influential words by measuring confidence drop after removing each word."""
89
+ base_prediction = self.predict_with_confidence(text)
90
+ target_emotion = base_prediction["emotion"]
91
+ base_confidence = base_prediction["confidence"]
92
+ words = re.findall(r"\b[\w']+\b", text)
93
+
94
+ impacts = []
95
+ for index, word in enumerate(words):
96
+ reduced_words = words[:index] + words[index + 1 :]
97
+ if not reduced_words:
98
+ continue
99
+
100
+ reduced_text = " ".join(reduced_words)
101
+ reduced_prediction = self.predict_with_confidence(reduced_text)
102
+ confidence_drop = base_confidence - (
103
+ reduced_prediction["confidence"]
104
+ if reduced_prediction["emotion"] == target_emotion
105
+ else 0.0
106
+ )
107
+
108
+ impacts.append(
109
+ {
110
+ "word": word,
111
+ "impact": round(float(confidence_drop), 4),
112
+ }
113
+ )
114
+
115
+ impacts = sorted(impacts, key=lambda item: item["impact"], reverse=True)
116
+ return {
117
+ "prediction": base_prediction,
118
+ "top_evidence": impacts[:top_k],
119
+ "method": "word occlusion: larger impact means removing the word reduced confidence more",
120
+ }
121
+
122
+
123
+ def parse_args() -> argparse.Namespace:
124
+ parser = argparse.ArgumentParser(description="Run Module 2 emotion inference.")
125
+ parser.add_argument("text", nargs="?", default="I feel anxious and overwhelmed today.")
126
+ parser.add_argument("--explain", action="store_true")
127
+ parser.add_argument("--model-dir", default=DEFAULT_MODEL_DIR, type=Path)
128
+ return parser.parse_args()
129
+
130
+
131
+ if __name__ == "__main__":
132
+ args = parse_args()
133
+ classifier = EmotionClassifier(model_dir=args.model_dir)
134
+ output = classifier.explain(args.text) if args.explain else classifier.predict_with_confidence(args.text)
135
+ print(json.dumps(output, indent=2))
src/models/emotion_detector_ui.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from emotion_classifier import EmotionClassifier
4
+
5
+
6
+ classifier = EmotionClassifier()
7
+
8
+
9
+ def predict_emotion(text: str) -> dict:
10
+ return classifier.explain(text or "", top_k=6)
11
+
12
+
13
+ interface = gr.Interface(
14
+ fn=predict_emotion,
15
+ inputs=gr.Textbox(
16
+ lines=5,
17
+ placeholder="Type an English mental-health related message...",
18
+ label="User Message",
19
+ ),
20
+ outputs=gr.JSON(label="Emotion Result"),
21
+ title="Module 2: Emotion Classification",
22
+ description="DistilBERT emotion classifier with confidence and word-occlusion explanation.",
23
+ flagging_mode="never",
24
+ )
25
+
26
+
27
+ if __name__ == "__main__":
28
+ interface.launch()