TsukishimaAlan20 commited on
Commit
633c1e9
·
verified ·
1 Parent(s): cbc058b

Sync from GitHub via hub-sync

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +13 -0
  2. .gitattributes +1 -0
  3. Dockerfile +27 -0
  4. LEGACY/BERT_ML_AOL_5_0.ipynb +0 -0
  5. LEGACY/EDAandPreprocess.ipynb +0 -0
  6. LEGACY/LogisticRegression_ML_AOL_2_0.ipynb +0 -0
  7. LEGACY/Machine Learning-AOL.pdf +3 -0
  8. LEGACY/NaiveBayes_ML_AOL_3_0.ipynb +1483 -0
  9. LEGACY/QuotesSelector.ipynb +851 -0
  10. LICENSE +21 -0
  11. Makefile +23 -0
  12. README.md +278 -4
  13. backend/(Preprocessed)Emotion_classify_Data(Labeled).csv +0 -0
  14. backend/(Preprocessed)quotes.csv +0 -0
  15. backend/.python-version +2 -0
  16. backend/app.py +140 -0
  17. backend/artifacts/.gitkeep +1 -0
  18. backend/model.ipynb +439 -0
  19. backend/requirements.txt +11 -0
  20. backend/runtime.txt +2 -0
  21. backend/scripts/build_sbert_index.py +30 -0
  22. backend/scripts/validate_models.py +29 -0
  23. backend/src/__init__.py +2 -0
  24. backend/src/config.py +57 -0
  25. backend/src/data_loader.py +39 -0
  26. backend/src/emotion_classifier.py +165 -0
  27. backend/src/health.py +19 -0
  28. backend/src/recommenders/__init__.py +10 -0
  29. backend/src/recommenders/base.py +49 -0
  30. backend/src/recommenders/cross_encoder_rerank.py +102 -0
  31. backend/src/recommenders/legacy_tfidf.py +72 -0
  32. backend/src/recommenders/sbert_dense.py +130 -0
  33. backend/src/schemas.py +79 -0
  34. backend/src/utils.py +35 -0
  35. backend/tests/test_api.py +101 -0
  36. backend/tests/test_health.py +3 -0
  37. backend/tests/test_recommenders.py +25 -0
  38. docker-compose.yml +12 -0
  39. frontend/package-lock.json +0 -0
  40. frontend/package.json +48 -0
  41. frontend/public/index.html +46 -0
  42. frontend/public/manifest.json +25 -0
  43. frontend/public/quotify.png +0 -0
  44. frontend/public/robots.txt +3 -0
  45. frontend/src/App.css +33 -0
  46. frontend/src/App.js +28 -0
  47. frontend/src/App.test.js +8 -0
  48. frontend/src/components/Login.css +60 -0
  49. frontend/src/components/Login.js +50 -0
  50. frontend/src/components/MainPage.css +312 -0
.dockerignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .pytest_cache
3
+ **/__pycache__
4
+ **/*.pyc
5
+ backend/.venv
6
+ backend/venv
7
+ backend/last_trained_model_checkpoint.pth
8
+ backend/artifacts/*
9
+ !backend/artifacts/.gitkeep
10
+ frontend/node_modules
11
+ frontend/build
12
+ node_modules
13
+
.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
+ LEGACY/Machine[[:space:]]Learning-AOL.pdf filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim AS frontend-build
2
+
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package*.json ./
5
+ RUN npm ci
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
+
9
+ FROM python:3.11-slim AS runtime
10
+
11
+ ENV PYTHONDONTWRITEBYTECODE=1
12
+ ENV PYTHONUNBUFFERED=1
13
+ ENV PORT=7860
14
+ ENV QUOTIFY_ENABLE_CROSS_ENCODER=false
15
+
16
+ WORKDIR /app/backend
17
+
18
+ COPY backend/requirements.txt ./
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ COPY backend/ ./
22
+ COPY --from=frontend-build /app/frontend/build /app/frontend/build
23
+
24
+ EXPOSE 7860
25
+
26
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--threads", "4", "--timeout", "240", "app:app"]
27
+
LEGACY/BERT_ML_AOL_5_0.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
LEGACY/EDAandPreprocess.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
LEGACY/LogisticRegression_ML_AOL_2_0.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
LEGACY/Machine Learning-AOL.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b0c988426bd58f3efb94b5527d9e12361cf5b4b510392eff3cdc24d9aa0944d
3
+ size 2041143
LEGACY/NaiveBayes_ML_AOL_3_0.ipynb ADDED
@@ -0,0 +1,1483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {
7
+ "id": "9BJOHx4uLmQM"
8
+ },
9
+ "outputs": [],
10
+ "source": [
11
+ "import pandas as pd\n",
12
+ "import numpy as np\n",
13
+ "from statistics import mode, multimode\n",
14
+ "import string\n",
15
+ "\n",
16
+ "from nltk.corpus import stopwords, wordnet\n",
17
+ "from nltk.tokenize import word_tokenize\n",
18
+ "from nltk.tag import pos_tag\n",
19
+ "from nltk.stem import SnowballStemmer, WordNetLemmatizer\n",
20
+ "from nltk.probability import FreqDist\n",
21
+ "from nltk.classify import NaiveBayesClassifier, accuracy\n",
22
+ "\n",
23
+ "from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score\n",
24
+ "import random\n",
25
+ "import seaborn as sns\n",
26
+ "import matplotlib.pyplot as plt\n",
27
+ "import json"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "markdown",
32
+ "metadata": {
33
+ "id": "G7E9ljABLmQO"
34
+ },
35
+ "source": [
36
+ "## Load the Preprocessed Data"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": null,
42
+ "metadata": {
43
+ "id": "Mhw00AHpLmQR",
44
+ "outputId": "5db382de-17e8-4632-a1c0-6078ab566ebd"
45
+ },
46
+ "outputs": [
47
+ {
48
+ "data": {
49
+ "text/html": [
50
+ "<div>\n",
51
+ "<style scoped>\n",
52
+ " .dataframe tbody tr th:only-of-type {\n",
53
+ " vertical-align: middle;\n",
54
+ " }\n",
55
+ "\n",
56
+ " .dataframe tbody tr th {\n",
57
+ " vertical-align: top;\n",
58
+ " }\n",
59
+ "\n",
60
+ " .dataframe thead th {\n",
61
+ " text-align: right;\n",
62
+ " }\n",
63
+ "</style>\n",
64
+ "<table border=\"1\" class=\"dataframe\">\n",
65
+ " <thead>\n",
66
+ " <tr style=\"text-align: right;\">\n",
67
+ " <th></th>\n",
68
+ " <th>Comment</th>\n",
69
+ " <th>Emotion</th>\n",
70
+ " </tr>\n",
71
+ " </thead>\n",
72
+ " <tbody>\n",
73
+ " <tr>\n",
74
+ " <th>0</th>\n",
75
+ " <td>i seriously hate one subject to death but now ...</td>\n",
76
+ " <td>fear</td>\n",
77
+ " </tr>\n",
78
+ " <tr>\n",
79
+ " <th>1</th>\n",
80
+ " <td>im so full of life i feel appalled</td>\n",
81
+ " <td>anger</td>\n",
82
+ " </tr>\n",
83
+ " <tr>\n",
84
+ " <th>2</th>\n",
85
+ " <td>i sit here to write i start to dig out my feel...</td>\n",
86
+ " <td>fear</td>\n",
87
+ " </tr>\n",
88
+ " <tr>\n",
89
+ " <th>3</th>\n",
90
+ " <td>ive been really angry with r and i feel like a...</td>\n",
91
+ " <td>joy</td>\n",
92
+ " </tr>\n",
93
+ " <tr>\n",
94
+ " <th>4</th>\n",
95
+ " <td>i feel suspicious if there is no one outside l...</td>\n",
96
+ " <td>fear</td>\n",
97
+ " </tr>\n",
98
+ " <tr>\n",
99
+ " <th>...</th>\n",
100
+ " <td>...</td>\n",
101
+ " <td>...</td>\n",
102
+ " </tr>\n",
103
+ " <tr>\n",
104
+ " <th>5929</th>\n",
105
+ " <td>i begun to feel distressed for you</td>\n",
106
+ " <td>fear</td>\n",
107
+ " </tr>\n",
108
+ " <tr>\n",
109
+ " <th>5930</th>\n",
110
+ " <td>i left feeling annoyed and angry thinking that...</td>\n",
111
+ " <td>anger</td>\n",
112
+ " </tr>\n",
113
+ " <tr>\n",
114
+ " <th>5931</th>\n",
115
+ " <td>i were to ever get married i d have everything...</td>\n",
116
+ " <td>joy</td>\n",
117
+ " </tr>\n",
118
+ " <tr>\n",
119
+ " <th>5932</th>\n",
120
+ " <td>i feel reluctant in applying there because i w...</td>\n",
121
+ " <td>fear</td>\n",
122
+ " </tr>\n",
123
+ " <tr>\n",
124
+ " <th>5933</th>\n",
125
+ " <td>i just wanted to apologize to you because i fe...</td>\n",
126
+ " <td>anger</td>\n",
127
+ " </tr>\n",
128
+ " </tbody>\n",
129
+ "</table>\n",
130
+ "<p>5934 rows × 2 columns</p>\n",
131
+ "</div>"
132
+ ],
133
+ "text/plain": [
134
+ " Comment Emotion\n",
135
+ "0 i seriously hate one subject to death but now ... fear\n",
136
+ "1 im so full of life i feel appalled anger\n",
137
+ "2 i sit here to write i start to dig out my feel... fear\n",
138
+ "3 ive been really angry with r and i feel like a... joy\n",
139
+ "4 i feel suspicious if there is no one outside l... fear\n",
140
+ "... ... ...\n",
141
+ "5929 i begun to feel distressed for you fear\n",
142
+ "5930 i left feeling annoyed and angry thinking that... anger\n",
143
+ "5931 i were to ever get married i d have everything... joy\n",
144
+ "5932 i feel reluctant in applying there because i w... fear\n",
145
+ "5933 i just wanted to apologize to you because i fe... anger\n",
146
+ "\n",
147
+ "[5934 rows x 2 columns]"
148
+ ]
149
+ },
150
+ "execution_count": 29,
151
+ "metadata": {},
152
+ "output_type": "execute_result"
153
+ }
154
+ ],
155
+ "source": [
156
+ "dataset = pd.read_csv('../Dataset/(Preprocessed)Emotion_classify_Data(Labeled).csv')\n",
157
+ "dataset"
158
+ ]
159
+ },
160
+ {
161
+ "cell_type": "code",
162
+ "execution_count": null,
163
+ "metadata": {
164
+ "id": "5yZznhfqLmQT"
165
+ },
166
+ "outputs": [],
167
+ "source": [
168
+ "from thePreprocessingII import preProcess\n",
169
+ "\n",
170
+ "stopsList = stopwords.words('english')\n",
171
+ "punctList = string.punctuation\n",
172
+ "stemmer = SnowballStemmer('english')\n",
173
+ "lemmatizer = WordNetLemmatizer()\n",
174
+ "\n",
175
+ "def getLabel(tag):\n",
176
+ " if tag == 'jj': return 'a'\n",
177
+ " elif tag in ['vb', 'nn', 'rb']: return tag[0]\n",
178
+ " else: return None\n",
179
+ "\n",
180
+ "def preProcess(wordList):\n",
181
+ " wordList = [w for w in wordList if w not in stopsList]\n",
182
+ " wordList = [w for w in wordList if w not in punctList]\n",
183
+ " wordList = [w for w in wordList if w.isalpha()]\n",
184
+ " wordList = [stemmer.stem(w) for w in wordList]\n",
185
+ "\n",
186
+ " lemmaList = []\n",
187
+ " for word, tag in pos_tag(wordList):\n",
188
+ " label = getLabel(tag)\n",
189
+ " if label != None: lemmaList.append(lemmatizer.lemmatize(word, label))\n",
190
+ " else: lemmaList.append(lemmatizer.lemmatize(word))\n",
191
+ "\n",
192
+ " return lemmaList"
193
+ ]
194
+ },
195
+ {
196
+ "cell_type": "code",
197
+ "execution_count": null,
198
+ "metadata": {
199
+ "id": "etBgq9hWLmQU"
200
+ },
201
+ "outputs": [],
202
+ "source": [
203
+ "commentList = dataset['Comment'].to_list()\n",
204
+ "emotionList = dataset['Emotion'].to_list()"
205
+ ]
206
+ },
207
+ {
208
+ "cell_type": "code",
209
+ "execution_count": null,
210
+ "metadata": {
211
+ "id": "WjH0Qj3mLmQV",
212
+ "outputId": "f26ac20d-c925-48ce-c66d-9c891cb25e93"
213
+ },
214
+ "outputs": [
215
+ {
216
+ "data": {
217
+ "text/plain": [
218
+ "['serious',\n",
219
+ " 'hate',\n",
220
+ " 'one',\n",
221
+ " 'subject',\n",
222
+ " 'death',\n",
223
+ " 'feel',\n",
224
+ " 'reluct',\n",
225
+ " 'drop',\n",
226
+ " 'im',\n",
227
+ " 'full',\n",
228
+ " 'life',\n",
229
+ " 'feel',\n",
230
+ " 'appal',\n",
231
+ " 'sit',\n",
232
+ " 'write',\n",
233
+ " 'start',\n",
234
+ " 'dig',\n",
235
+ " 'feel',\n",
236
+ " 'think',\n",
237
+ " 'afraid',\n",
238
+ " 'accept',\n",
239
+ " 'possibl',\n",
240
+ " 'might',\n",
241
+ " 'make',\n",
242
+ " 'ive',\n",
243
+ " 'realli',\n",
244
+ " 'angri',\n",
245
+ " 'r',\n",
246
+ " 'feel',\n",
247
+ " 'like',\n",
248
+ " 'idiot',\n",
249
+ " 'trust',\n",
250
+ " 'first',\n",
251
+ " 'place',\n",
252
+ " 'feel',\n",
253
+ " 'suspici',\n",
254
+ " 'one',\n",
255
+ " 'outsid',\n",
256
+ " 'like',\n",
257
+ " 'raptur',\n",
258
+ " 'happen',\n",
259
+ " 'someth',\n",
260
+ " 'feel',\n",
261
+ " 'jealous',\n",
262
+ " 'becasu',\n",
263
+ " 'want',\n",
264
+ " 'kind',\n",
265
+ " 'love',\n",
266
+ " 'true',\n",
267
+ " 'connect',\n",
268
+ " 'two',\n",
269
+ " 'soul',\n",
270
+ " 'want',\n",
271
+ " 'friend',\n",
272
+ " 'mine',\n",
273
+ " 'keep',\n",
274
+ " 'tell',\n",
275
+ " 'morbid',\n",
276
+ " 'thing',\n",
277
+ " 'happen',\n",
278
+ " 'dog',\n",
279
+ " 'final',\n",
280
+ " 'fell',\n",
281
+ " 'asleep',\n",
282
+ " 'feel',\n",
283
+ " 'angri',\n",
284
+ " 'useless',\n",
285
+ " 'still',\n",
286
+ " 'full',\n",
287
+ " 'anxieti',\n",
288
+ " 'feel',\n",
289
+ " 'bit',\n",
290
+ " 'annoy',\n",
291
+ " 'antsi',\n",
292
+ " 'good',\n",
293
+ " 'way',\n",
294
+ " 'feel',\n",
295
+ " 'like',\n",
296
+ " 'regain',\n",
297
+ " 'anoth',\n",
298
+ " 'vital',\n",
299
+ " 'part',\n",
300
+ " 'life',\n",
301
+ " 'live',\n",
302
+ " 'feel',\n",
303
+ " 'bit',\n",
304
+ " 'like',\n",
305
+ " 'franz',\n",
306
+ " 'liebkind',\n",
307
+ " 'produc',\n",
308
+ " 'mani',\n",
309
+ " 'peopl',\n",
310
+ " 'know',\n",
311
+ " 'fuhrer',\n",
312
+ " 'terrif',\n",
313
+ " 'dancer',\n",
314
+ " 'feel',\n",
315
+ " 'start',\n",
316
+ " 'didnt',\n",
317
+ " 'want',\n",
318
+ " 'move',\n",
319
+ " 'much',\n",
320
+ " 'realli',\n",
321
+ " 'glad',\n",
322
+ " 'experi',\n",
323
+ " 'glimps',\n",
324
+ " 'sort',\n",
325
+ " 'vibrant',\n",
326
+ " 'energi',\n",
327
+ " 'gain',\n",
328
+ " 'year',\n",
329
+ " 'bitten',\n",
330
+ " 'dog',\n",
331
+ " 'alway',\n",
332
+ " 'find',\n",
333
+ " 'feel',\n",
334
+ " 'thank',\n",
335
+ " 'year',\n",
336
+ " 'gather',\n",
337
+ " 'trick',\n",
338
+ " 'allow',\n",
339
+ " 'feel',\n",
340
+ " 'grate',\n",
341
+ " 'face',\n",
342
+ " 'moment',\n",
343
+ " 'last',\n",
344
+ " 'thing',\n",
345
+ " 'want',\n",
346
+ " 'say',\n",
347
+ " 'thank',\n",
348
+ " 'tri',\n",
349
+ " 'talk',\n",
350
+ " 'anyon',\n",
351
+ " 'feel',\n",
352
+ " 'irrit',\n",
353
+ " 'like',\n",
354
+ " 'feel',\n",
355
+ " 'like',\n",
356
+ " 'hate',\n",
357
+ " 'sinc',\n",
358
+ " 'dont',\n",
359
+ " 'know',\n",
360
+ " 'wrote',\n",
361
+ " 'follow',\n",
362
+ " 'littl',\n",
363
+ " 'note',\n",
364
+ " 'feel',\n",
365
+ " 'today',\n",
366
+ " 'u',\n",
367
+ " 'r',\n",
368
+ " 'offend',\n",
369
+ " 'follow',\n",
370
+ " 'post',\n",
371
+ " 'obvious',\n",
372
+ " 'live',\n",
373
+ " 'long',\n",
374
+ " 'enough',\n",
375
+ " 'compromis',\n",
376
+ " 'act',\n",
377
+ " 'believ',\n",
378
+ " 'feel',\n",
379
+ " 'offend',\n",
380
+ " 'choos',\n",
381
+ " 'tell',\n",
382
+ " 'guy',\n",
383
+ " 'feel',\n",
384
+ " 'treat',\n",
385
+ " 'guy',\n",
386
+ " 'friend',\n",
387
+ " 'would',\n",
388
+ " 'want',\n",
389
+ " 'put',\n",
390
+ " 'stop',\n",
391
+ " 'nonsens',\n",
392
+ " 'rememb',\n",
393
+ " 'feel',\n",
394
+ " 'belong',\n",
395
+ " 'smart',\n",
396
+ " 'enough',\n",
397
+ " 'cool',\n",
398
+ " 'enough',\n",
399
+ " 'even',\n",
400
+ " 'young',\n",
401
+ " 'enough',\n",
402
+ " 'feel',\n",
403
+ " 'like',\n",
404
+ " 'redeem',\n",
405
+ " 'even',\n",
406
+ " 'though',\n",
407
+ " 'think',\n",
408
+ " 'realiz',\n",
409
+ " 'distraught',\n",
410
+ " 'ok',\n",
411
+ " 'hope',\n",
412
+ " 'next',\n",
413
+ " 'quot',\n",
414
+ " 'abl',\n",
415
+ " 'let',\n",
416
+ " 'special',\n",
417
+ " 'someon',\n",
418
+ " 'know',\n",
419
+ " 'im',\n",
420
+ " 'feel',\n",
421
+ " 'insecur',\n",
422
+ " 'understand',\n",
423
+ " 'matter',\n",
424
+ " 'much',\n",
425
+ " 'trust',\n",
426
+ " 'feel',\n",
427
+ " 'much',\n",
428
+ " 'better',\n",
429
+ " 'without',\n",
430
+ " 'help',\n",
431
+ " 'ice',\n",
432
+ " 'sit',\n",
433
+ " 'type',\n",
434
+ " 'wonder',\n",
435
+ " 'belong',\n",
436
+ " 'feel',\n",
437
+ " 'distract',\n",
438
+ " 'feel',\n",
439
+ " 'comfort',\n",
440
+ " 'feel',\n",
441
+ " 'misunderstood',\n",
442
+ " 'hurt',\n",
443
+ " 'still',\n",
444
+ " 'feel',\n",
445
+ " 'littl',\n",
446
+ " 'shaki',\n",
447
+ " 'time',\n",
448
+ " 'move',\n",
449
+ " 'slight',\n",
450
+ " 'odd',\n",
451
+ " 'jade',\n",
452
+ " 'hair',\n",
453
+ " 'particular',\n",
454
+ " 'seem',\n",
455
+ " 'prone',\n",
456
+ " 'general',\n",
457
+ " 'work',\n",
458
+ " 'well',\n",
459
+ " 'spencer',\n",
460
+ " 'write',\n",
461
+ " 'strop',\n",
462
+ " 'bit',\n",
463
+ " 'feel',\n",
464
+ " 'grumpi',\n",
465
+ " 'miss',\n",
466
+ " 'feel',\n",
467
+ " 'shall',\n",
468
+ " 'go',\n",
469
+ " 'mad',\n",
470
+ " 'happpi',\n",
471
+ " 'get',\n",
472
+ " 'good',\n",
473
+ " 'result',\n",
474
+ " 'field',\n",
475
+ " 'academ',\n",
476
+ " 'athlet',\n",
477
+ " 'got',\n",
478
+ " 'feel',\n",
479
+ " 'brig',\n",
480
+ " 'sincer',\n",
481
+ " 'strong',\n",
482
+ " 'desir',\n",
483
+ " 'help',\n",
484
+ " 'other',\n",
485
+ " 'becom',\n",
486
+ " 'success',\n",
487
+ " 'financi',\n",
488
+ " 'also',\n",
489
+ " 'build',\n",
490
+ " 'strengthen',\n",
491
+ " 'relationship',\n",
492
+ " 'christian',\n",
493
+ " 'im',\n",
494
+ " 'feel',\n",
495
+ " 'deepli',\n",
496
+ " 'overwhelm',\n",
497
+ " 'ordinari',\n",
498
+ " 'task',\n",
499
+ " 'feel',\n",
500
+ " 'grouchi',\n",
501
+ " 'irrit',\n",
502
+ " 'im',\n",
503
+ " 'sick',\n",
504
+ " 'feel',\n",
505
+ " 'heart',\n",
506
+ " 'shaki',\n",
507
+ " 'time',\n",
508
+ " 'expect',\n",
509
+ " 'feel',\n",
510
+ " 'content',\n",
511
+ " 'feel',\n",
512
+ " 'amus',\n",
513
+ " 'absurd',\n",
514
+ " 'end',\n",
515
+ " 'get',\n",
516
+ " 'unwant',\n",
517
+ " 'attent',\n",
518
+ " 'boy',\n",
519
+ " 'want',\n",
520
+ " 'littl',\n",
521
+ " 'ill',\n",
522
+ " 'sort',\n",
523
+ " 'start',\n",
524
+ " 'someth',\n",
525
+ " 'boy',\n",
526
+ " 'find',\n",
527
+ " 'flirti',\n",
528
+ " 'other',\n",
529
+ " 'presenc',\n",
530
+ " 'ill',\n",
531
+ " 'feel',\n",
532
+ " 'realli',\n",
533
+ " 'insincer',\n",
534
+ " 'around',\n",
535
+ " 'boy',\n",
536
+ " 'like',\n",
537
+ " 'couldnt',\n",
538
+ " 'help',\n",
539
+ " 'feel',\n",
540
+ " 'littl',\n",
541
+ " 'bit',\n",
542
+ " 'bitter',\n",
543
+ " 'toward',\n",
544
+ " 'great',\n",
545
+ " 'big',\n",
546
+ " 'happi',\n",
547
+ " 'grin',\n",
548
+ " 'go',\n",
549
+ " 'back',\n",
550
+ " 'point',\n",
551
+ " 'easi',\n",
552
+ " 'sell',\n",
553
+ " 'get',\n",
554
+ " 'folk',\n",
555
+ " 'feel',\n",
556
+ " 'realli',\n",
557
+ " 'virtuous',\n",
558
+ " 'dont',\n",
559
+ " 'want',\n",
560
+ " 'anyway',\n",
561
+ " 'im',\n",
562
+ " 'feel',\n",
563
+ " 'bit',\n",
564
+ " 'sociabl',\n",
565
+ " 'although',\n",
566
+ " 'dont',\n",
567
+ " 'think',\n",
568
+ " 'ill',\n",
569
+ " 'abl',\n",
570
+ " 'express',\n",
571
+ " 'everyth',\n",
572
+ " 'want',\n",
573
+ " 'say',\n",
574
+ " 'would',\n",
575
+ " 'feel',\n",
576
+ " 'joy',\n",
577
+ " 'certain',\n",
578
+ " 'situat',\n",
579
+ " 'felt',\n",
580
+ " 'neglect',\n",
581
+ " 'undeserv',\n",
582
+ " 'harm',\n",
583
+ " 'felt',\n",
584
+ " 'good',\n",
585
+ " 'feel',\n",
586
+ " 'fine',\n",
587
+ " 'today',\n",
588
+ " 'look',\n",
589
+ " 'happen',\n",
590
+ " 'u',\n",
591
+ " 'two',\n",
592
+ " 'generat',\n",
593
+ " 'look',\n",
594
+ " 'happen',\n",
595
+ " 'two',\n",
596
+ " 'three',\n",
597
+ " 'instead',\n",
598
+ " 'feel',\n",
599
+ " 'outrag',\n",
600
+ " 'histori',\n",
601
+ " 'aggress',\n",
602
+ " 'felt',\n",
603
+ " 'privileg',\n",
604
+ " 'im',\n",
605
+ " 'feel',\n",
606
+ " 'slight',\n",
607
+ " 'optimist',\n",
608
+ " 'feel',\n",
609
+ " 'hate',\n",
610
+ " 'hollow',\n",
611
+ " 'yes',\n",
612
+ " 'hear',\n",
613
+ " 'mani',\n",
614
+ " 'smith',\n",
615
+ " 'day',\n",
616
+ " 'im',\n",
617
+ " 'still',\n",
618
+ " 'feel',\n",
619
+ " 'indecis',\n",
620
+ " 'im',\n",
621
+ " 'poll',\n",
622
+ " 'yall',\n",
623
+ " 'p',\n",
624
+ " 'sometim',\n",
625
+ " 'back',\n",
626
+ " 'anoth',\n",
627
+ " 'girl',\n",
628
+ " 'term',\n",
629
+ " 'exboyfriend',\n",
630
+ " 'came',\n",
631
+ " 'shout',\n",
632
+ " 'twelv',\n",
633
+ " 'midnight',\n",
634
+ " 'thought',\n",
635
+ " 'still',\n",
636
+ " 'interest',\n",
637
+ " 'boy',\n",
638
+ " 'id',\n",
639
+ " 'start',\n",
640
+ " 'feel',\n",
641
+ " 'resent',\n",
642
+ " 'live',\n",
643
+ " 'part',\n",
644
+ " 'countri',\n",
645
+ " 'sun',\n",
646
+ " 'stubborn',\n",
647
+ " 'refus',\n",
648
+ " 'show',\n",
649
+ " 'end',\n",
650
+ " 'septemb',\n",
651
+ " 'feel',\n",
652
+ " 'fuck',\n",
653
+ " 'day',\n",
654
+ " 'confess',\n",
655
+ " 'feel',\n",
656
+ " 'nervous',\n",
657
+ " 'made',\n",
658
+ " 'way',\n",
659
+ " 'event',\n",
660
+ " 'venu',\n",
661
+ " 'realli',\n",
662
+ " 'worri',\n",
663
+ " 'would',\n",
664
+ " 'feel',\n",
665
+ " 'intimid',\n",
666
+ " 'monica',\n",
667
+ " 'met',\n",
668
+ " 'morn',\n",
669
+ " 'incred',\n",
670
+ " 'welcom',\n",
671
+ " 'made',\n",
672
+ " 'feel',\n",
673
+ " 'relax',\n",
674
+ " 'straight',\n",
675
+ " 'away',\n",
676
+ " 'sick',\n",
677
+ " 'cold',\n",
678
+ " 'amp',\n",
679
+ " 'feel',\n",
680
+ " 'well',\n",
681
+ " 'wonder',\n",
682
+ " 'would',\n",
683
+ " 'even',\n",
684
+ " 'abl',\n",
685
+ " 'patienc',\n",
686
+ " 'go',\n",
687
+ " 'whitley',\n",
688
+ " 'month',\n",
689
+ " 'photo',\n",
690
+ " 'shoot',\n",
691
+ " 'feel',\n",
692
+ " 'like',\n",
693
+ " 'bit',\n",
694
+ " 'obnoxi',\n",
695
+ " 'pictur',\n",
696
+ " 'post',\n",
697
+ " 'feel',\n",
698
+ " 'moral',\n",
699
+ " 'outrag',\n",
700
+ " 'furious',\n",
701
+ " 'often',\n",
702
+ " 'like',\n",
703
+ " 'feel',\n",
704
+ " 'piss',\n",
705
+ " 'friend',\n",
706
+ " 'didnt',\n",
707
+ " 'offer',\n",
708
+ " 'soda',\n",
709
+ " 'feel',\n",
710
+ " 'heart',\n",
711
+ " 'tread',\n",
712
+ " 'danger',\n",
713
+ " 'territori',\n",
714
+ " 'feel',\n",
715
+ " 'frighten',\n",
716
+ " 'know',\n",
717
+ " 'pleasant',\n",
718
+ " 'feel',\n",
719
+ " 'selfish',\n",
720
+ " 'say',\n",
721
+ " 'think',\n",
722
+ " 'would',\n",
723
+ " 'fallen',\n",
724
+ " 'apart',\n",
725
+ " 'feel',\n",
726
+ " 'shaft',\n",
727
+ " 'greedi',\n",
728
+ " 'crappi',\n",
729
+ " 'week',\n",
730
+ " 'still',\n",
731
+ " 'feel',\n",
732
+ " 'agit',\n",
733
+ " 'like',\n",
734
+ " 'day',\n",
735
+ " 'want',\n",
736
+ " 'feel',\n",
737
+ " 'rebelli',\n",
738
+ " 'particular',\n",
739
+ " 'like',\n",
740
+ " 'watch',\n",
741
+ " 'romcom',\n",
742
+ " 'get',\n",
743
+ " 'feel',\n",
744
+ " 'may',\n",
745
+ " 'pretti',\n",
746
+ " 'good',\n",
747
+ " 'write',\n",
748
+ " 'feel',\n",
749
+ " 'reluct',\n",
750
+ " 'leav',\n",
751
+ " 'alon',\n",
752
+ " 'like',\n",
753
+ " 'without',\n",
754
+ " 'help',\n",
755
+ " 'enough',\n",
756
+ " 'repay',\n",
757
+ " 'good',\n",
758
+ " 'asham',\n",
759
+ " 'feel',\n",
760
+ " 'like',\n",
761
+ " 'moment',\n",
762
+ " 'see',\n",
763
+ " 'terrifi',\n",
764
+ " 'cri',\n",
765
+ " 'child',\n",
766
+ " 'dead',\n",
767
+ " 'one',\n",
768
+ " 'wake',\n",
769
+ " 'morn',\n",
770
+ " 'voic',\n",
771
+ " 'throat',\n",
772
+ " 'feel',\n",
773
+ " 'ok',\n",
774
+ " 'afternoon',\n",
775
+ " 'scratchi',\n",
776
+ " 'sound',\n",
777
+ " 'like',\n",
778
+ " 'marg',\n",
779
+ " 'simpson',\n",
780
+ " 'night',\n",
781
+ " 'bad',\n",
782
+ " 'throat',\n",
783
+ " 'sore',\n",
784
+ " 'whisper',\n",
785
+ " 'ive',\n",
786
+ " 'alway',\n",
787
+ " 'nervous',\n",
788
+ " 'someth',\n",
789
+ " 'like',\n",
790
+ " 'feel',\n",
791
+ " 'like',\n",
792
+ " 'realli',\n",
793
+ " 'talent',\n",
794
+ " 'enter',\n",
795
+ " 'someth',\n",
796
+ " 'offici',\n",
797
+ " 'contest',\n",
798
+ " 'feel',\n",
799
+ " 'bit',\n",
800
+ " 'piss',\n",
801
+ " 'went',\n",
802
+ " 'first',\n",
803
+ " 'feel',\n",
804
+ " 'satisfi',\n",
805
+ " 'know',\n",
806
+ " 'dirt',\n",
807
+ " 'hair',\n",
808
+ " 'longer',\n",
809
+ " 'car',\n",
810
+ " 'hous',\n",
811
+ " 'realli',\n",
812
+ " 'like',\n",
813
+ " 'special',\n",
814
+ " 'edit',\n",
815
+ " 'realli',\n",
816
+ " 'feel',\n",
817
+ " 'special',\n",
818
+ " 'song',\n",
819
+ " 'would',\n",
820
+ " 'accept',\n",
821
+ " 'gift',\n",
822
+ " 'without',\n",
823
+ " 'feel',\n",
824
+ " 'mad',\n",
825
+ " 'feel',\n",
826
+ " 'hesit',\n",
827
+ " 'sinc',\n",
828
+ " 'experi',\n",
829
+ " 'program',\n",
830
+ " 'feel',\n",
831
+ " 'particular',\n",
832
+ " 'bitchi',\n",
833
+ " 'dont',\n",
834
+ " 'think',\n",
835
+ " 'adequ',\n",
836
+ " 'express',\n",
837
+ " 'appreci',\n",
838
+ " 'feel',\n",
839
+ " 'confus',\n",
840
+ " 'im',\n",
841
+ " 'professor',\n",
842
+ " 'look',\n",
843
+ " 'like',\n",
844
+ " 'spitbal',\n",
845
+ " 'friend',\n",
846
+ " 'die',\n",
847
+ " 'cri',\n",
848
+ " 'pillow',\n",
849
+ " 'night',\n",
850
+ " 'feel',\n",
851
+ " 'danger',\n",
852
+ " 'tickl',\n",
853
+ " 'jealousi',\n",
854
+ " 'lure',\n",
855
+ " 'lair',\n",
856
+ " 'feel',\n",
857
+ " 'rush',\n",
858
+ " 'finish',\n",
859
+ " 'million',\n",
860
+ " 'thing',\n",
861
+ " 'abl',\n",
862
+ " 'focus',\n",
863
+ " 'task',\n",
864
+ " 'separ',\n",
865
+ " 'feel',\n",
866
+ " 'tortur',\n",
867
+ " 'dunno',\n",
868
+ " 'feel',\n",
869
+ " 'complet',\n",
870
+ " 'happi',\n",
871
+ " 'real',\n",
872
+ " 'world',\n",
873
+ " 'taught',\n",
874
+ " 'struggl',\n",
875
+ " 'go',\n",
876
+ " 'thru',\n",
877
+ " 'noth',\n",
878
+ " 'close',\n",
879
+ " 'struggl',\n",
880
+ " 'feel',\n",
881
+ " 'like',\n",
882
+ " 'taken',\n",
883
+ " 'role',\n",
884
+ " 'grandmoth',\n",
885
+ " 'sinc',\n",
886
+ " 'belov',\n",
887
+ " 'grandma',\n",
888
+ " 'longer',\n",
889
+ " 'fear',\n",
890
+ " 'peopl',\n",
891
+ " 'ask',\n",
892
+ " 'feel',\n",
893
+ " 'reluct',\n",
894
+ " 'talk',\n",
895
+ " 'thing',\n",
896
+ " 'would',\n",
897
+ " 'smile',\n",
898
+ " 'except',\n",
899
+ " 'start',\n",
900
+ " 'feel',\n",
901
+ " 'like',\n",
902
+ " 'uptight',\n",
903
+ " 'comment',\n",
904
+ " 'jaw',\n",
905
+ " 'would',\n",
906
+ " 'fall',\n",
907
+ " 'right',\n",
908
+ " 'head',\n",
909
+ " 'feel',\n",
910
+ " 'comfort',\n",
911
+ " 'around',\n",
912
+ " 'feel',\n",
913
+ " 'complet',\n",
914
+ " 'unsur',\n",
915
+ " 'boundari',\n",
916
+ " 'normalci',\n",
917
+ " 'think',\n",
918
+ " 'feel',\n",
919
+ " 'gratitud',\n",
920
+ " 'think',\n",
921
+ " 'kind',\n",
922
+ " 'gracious',\n",
923
+ " 'heaven',\n",
924
+ " 'father',\n",
925
+ " 'im',\n",
926
+ " 'feel',\n",
927
+ " 'realli',\n",
928
+ " 'realli',\n",
929
+ " 'left',\n",
930
+ " 'somewhat',\n",
931
+ " 'dissatisfi',\n",
932
+ " 'everyth',\n",
933
+ " 'simpli',\n",
934
+ " 'said',\n",
935
+ " 'sorri',\n",
936
+ " 'got',\n",
937
+ " 'car',\n",
938
+ " 'got',\n",
939
+ " 'hous',\n",
940
+ " 'feel',\n",
941
+ " 'restless',\n",
942
+ " 'feel',\n",
943
+ " 'weird',\n",
944
+ " 'feel',\n",
945
+ " 'wan',\n",
946
+ " 'na',\n",
947
+ " 'know',\n",
948
+ " 'incred',\n",
949
+ " 'feel',\n",
950
+ " 'frantic',\n",
951
+ " 'despair',\n",
952
+ " 'feel',\n",
953
+ " 'reluct',\n",
954
+ " 'sell',\n",
955
+ " 'hey',\n",
956
+ " 'take',\n",
957
+ " 'long',\n",
958
+ " 'sip',\n",
959
+ " 'feel',\n",
960
+ " 'cold',\n",
961
+ " 'sensat',\n",
962
+ " 'ice',\n",
963
+ " 'capp',\n",
964
+ " 'feel',\n",
965
+ " 'pressur',\n",
966
+ " 'even',\n",
967
+ " 'longest',\n",
968
+ " 'race',\n",
969
+ " 'one',\n",
970
+ " 'expect',\n",
971
+ " 'also',\n",
972
+ " 'hope',\n",
973
+ " 'understand',\n",
974
+ " 'feel',\n",
975
+ " 'angri',\n",
976
+ " 'dont',\n",
977
+ " 'support',\n",
978
+ " 'hat',\n",
979
+ " 'rule',\n",
980
+ " 'turn',\n",
981
+ " 'school',\n",
982
+ " 'event',\n",
983
+ " 'san',\n",
984
+ " 'hat',\n",
985
+ " 'rememb',\n",
986
+ " 'feel',\n",
987
+ " 'terrifi',\n",
988
+ " 'ever',\n",
989
+ " 'felt',\n",
990
+ " 'entir',\n",
991
+ " 'life',\n",
992
+ " 'still',\n",
993
+ " 'affect',\n",
994
+ " 'ive',\n",
995
+ " 'never',\n",
996
+ " 'thought',\n",
997
+ " 'account',\n",
998
+ " 'trauma',\n",
999
+ " 'confess',\n",
1000
+ " 'feel',\n",
1001
+ " 'still',\n",
1002
+ " 'couldnt',\n",
1003
+ " 'bring',\n",
1004
+ " 'scare',\n",
1005
+ " 'lose',\n",
1006
+ " 'im',\n",
1007
+ " 'clear',\n",
1008
+ " 'influenc',\n",
1009
+ " 'dash',\n",
1010
+ " 'happi',\n",
1011
+ " 'emili',\n",
1012
+ " 'dickinson',\n",
1013
+ " 'exampl',\n",
1014
+ " 'use',\n",
1015
+ " 'dash',\n",
1016
+ " 'instead',\n",
1017
+ " 'colon',\n",
1018
+ " 'semi',\n",
1019
+ " 'colon',\n",
1020
+ " 'enhanc',\n",
1021
+ " 'feel',\n",
1022
+ " 'rush',\n",
1023
+ " 'enjamb',\n",
1024
+ " 'sonnet',\n",
1025
+ " 'pray',\n",
1026
+ " 'eye',\n",
1027
+ " 'read',\n",
1028
+ " 'mind',\n",
1029
+ " 'comprehend',\n",
1030
+ " 'heart',\n",
1031
+ " 'feel',\n",
1032
+ " 'offend',\n",
1033
+ " 'im',\n",
1034
+ " 'afraid',\n",
1035
+ " 'go',\n",
1036
+ " 'feel',\n",
1037
+ " 'like',\n",
1038
+ " 'lot',\n",
1039
+ " 'peopl',\n",
1040
+ " 'group',\n",
1041
+ " 'part',\n",
1042
+ " 'feel',\n",
1043
+ " 'like',\n",
1044
+ " 'would',\n",
1045
+ " 'cool',\n",
1046
+ " 'small',\n",
1047
+ " 'group',\n",
1048
+ " 'hang',\n",
1049
+ " 'possibl',\n",
1050
+ " 'fail',\n",
1051
+ " 'examin',\n",
1052
+ " 'feel',\n",
1053
+ " 'thank',\n",
1054
+ " 'part',\n",
1055
+ " 'countri',\n",
1056
+ " 'train',\n",
1057
+ " 'outdoor',\n",
1058
+ " 'late',\n",
1059
+ " 'year',\n",
1060
+ " 'bundl',\n",
1061
+ " 'wear',\n",
1062
+ " 'sever',\n",
1063
+ " 'layer',\n",
1064
+ " 'feel',\n",
1065
+ " 'rather',\n",
1066
+ " 'grouchi',\n",
1067
+ " 'morn',\n",
1068
+ " 'sinc',\n",
1069
+ " 'didnt',\n",
1070
+ " 'sleep',\n",
1071
+ " 'last',\n",
1072
+ " 'night',\n",
1073
+ " 'purpos',\n",
1074
+ " 'feel',\n",
1075
+ " 'like',\n",
1076
+ " 'earthquak',\n",
1077
+ " 'also',\n",
1078
+ " 'shaken',\n",
1079
+ " 'foundat',\n",
1080
+ " 'life',\n",
1081
+ " 'work',\n",
1082
+ " 'might',\n",
1083
+ " 'abl',\n",
1084
+ " 'recreat',\n",
1085
+ " 'feel',\n",
1086
+ " 'get',\n",
1087
+ " 'back',\n",
1088
+ " 'cold',\n",
1089
+ " 'fog',\n",
1090
+ " 'await',\n",
1091
+ " 'tomorrow',\n",
1092
+ " 'night',\n",
1093
+ " 'woke',\n",
1094
+ " 'feel',\n",
1095
+ " 'posit',\n",
1096
+ " 'total',\n",
1097
+ " 'mood',\n",
1098
+ " 'even',\n",
1099
+ " 'feel',\n",
1100
+ " 'banana',\n",
1101
+ " 'shake',\n",
1102
+ " 'breakfast',\n",
1103
+ " 'chocol',\n",
1104
+ " 'shake',\n",
1105
+ " 'dinner',\n",
1106
+ " 'sunday',\n",
1107
+ " 'roast',\n",
1108
+ " 'tea',\n",
1109
+ " 'would',\n",
1110
+ " 'go',\n",
1111
+ " 'straight',\n",
1112
+ " 'point',\n",
1113
+ " 'rather',\n",
1114
+ " 'defin',\n",
1115
+ " 'romanc',\n",
1116
+ " 'feel',\n",
1117
+ " 'anger',\n",
1118
+ " 'feel',\n",
1119
+ " 'suspici',\n",
1120
+ " 'feel',\n",
1121
+ " 'feel',\n",
1122
+ " 'bitchi',\n",
1123
+ " 'mean',\n",
1124
+ " 'terribl',\n",
1125
+ " 'feel',\n",
1126
+ " 'envious',\n",
1127
+ " 'keep',\n",
1128
+ " 'post',\n",
1129
+ " 'regular',\n",
1130
+ " 'interest',\n",
1131
+ " 'wish',\n",
1132
+ " 'could',\n",
1133
+ " 'feel',\n",
1134
+ " 'way',\n",
1135
+ " 'feel',\n",
1136
+ " 'like',\n",
1137
+ " 'tri',\n",
1138
+ " 'stay',\n",
1139
+ " 'faith',\n",
1140
+ " 'possibl',\n",
1141
+ " 'perceiv',\n",
1142
+ " 'real',\n",
1143
+ " 'event',\n",
1144
+ " 'happen',\n",
1145
+ " 'mountain',\n",
1146
+ " 'feel',\n",
1147
+ " 'assault',\n",
1148
+ " 'new',\n",
1149
+ " 'kid',\n",
1150
+ " 'whine',\n",
1151
+ " 'ive',\n",
1152
+ " 'long',\n",
1153
+ " 'road',\n",
1154
+ " 'initi',\n",
1155
+ " 'feel',\n",
1156
+ " 'like',\n",
1157
+ " 'rude',\n",
1158
+ " 'turn',\n",
1159
+ " 'food',\n",
1160
+ " 'made',\n",
1161
+ " 'brought',\n",
1162
+ " 'sometim',\n",
1163
+ " 'eat',\n",
1164
+ " 'stuff',\n",
1165
+ " 'gluten',\n",
1166
+ " 'free',\n",
1167
+ " 'look',\n",
1168
+ " 'delici',\n",
1169
+ " 'even',\n",
1170
+ " 'mayb',\n",
1171
+ " 'wasnt',\n",
1172
+ " 'felt',\n",
1173
+ " 'good',\n",
1174
+ " 'eat',\n",
1175
+ " 'realli',\n",
1176
+ " 'mediocr',\n",
1177
+ " 'wed',\n",
1178
+ " 'cake',\n",
1179
+ " 'exampl',\n",
1180
+ " 'want',\n",
1181
+ " 'make',\n",
1182
+ " 'sure',\n",
1183
+ " 'didnt',\n",
1184
+ " 'feel',\n",
1185
+ " 'rush',\n",
1186
+ " 'get',\n",
1187
+ " 'centuri',\n",
1188
+ " 'colleg',\n",
1189
+ " 'friday',\n",
1190
+ " 'afternoon',\n",
1191
+ " 'id',\n",
1192
+ " 'kick',\n",
1193
+ " 'gear',\n",
1194
+ " 'feel',\n",
1195
+ " 'irrit',\n",
1196
+ " 'motiv',\n",
1197
+ " 'ever',\n",
1198
+ " 'feel',\n",
1199
+ " 'much',\n",
1200
+ " 'product',\n",
1201
+ " 'colleg',\n",
1202
+ " 'keep',\n",
1203
+ " 'product',\n",
1204
+ " 'full',\n",
1205
+ " 'gear',\n",
1206
+ " 'ill',\n",
1207
+ " 'chalk',\n",
1208
+ " 'idea',\n",
1209
+ " 'art',\n",
1210
+ " 'project',\n",
1211
+ " 'summer',\n",
1212
+ " 'train',\n",
1213
+ " 'armi',\n",
1214
+ " 'attack',\n",
1215
+ " 'pigeon',\n",
1216
+ " 'take',\n",
1217
+ " 'tini',\n",
1218
+ " ...]"
1219
+ ]
1220
+ },
1221
+ "execution_count": 33,
1222
+ "metadata": {},
1223
+ "output_type": "execute_result"
1224
+ }
1225
+ ],
1226
+ "source": [
1227
+ "# Load list from file\n",
1228
+ "with open('../Dataset/(PreprocessedII)sentList.json', 'r') as f:\n",
1229
+ " sentList = json.load(f)\n",
1230
+ "\n",
1231
+ "wordlister = []\n",
1232
+ "for sentence in sentList:\n",
1233
+ " words = sentence.split(' ')\n",
1234
+ " for word in words:\n",
1235
+ " wordlister.append(word)\n",
1236
+ "\n",
1237
+ "wordlister"
1238
+ ]
1239
+ },
1240
+ {
1241
+ "cell_type": "code",
1242
+ "execution_count": null,
1243
+ "metadata": {
1244
+ "id": "x8UAzcH_LmQW",
1245
+ "outputId": "f64e999d-034a-4078-f802-f4b6e9a82a67"
1246
+ },
1247
+ "outputs": [
1248
+ {
1249
+ "data": {
1250
+ "text/plain": [
1251
+ "FreqDist({'feel': 6230, 'like': 1012, 'im': 942, 'get': 402, 'go': 361, 'time': 360, 'want': 358, 'know': 349, 'make': 343, 'littl': 326, ...})"
1252
+ ]
1253
+ },
1254
+ "execution_count": 34,
1255
+ "metadata": {},
1256
+ "output_type": "execute_result"
1257
+ }
1258
+ ],
1259
+ "source": [
1260
+ "wordList = FreqDist(wordlister)\n",
1261
+ "wordList"
1262
+ ]
1263
+ },
1264
+ {
1265
+ "cell_type": "code",
1266
+ "execution_count": null,
1267
+ "metadata": {
1268
+ "id": "UdiG5splLmQY"
1269
+ },
1270
+ "outputs": [],
1271
+ "source": [
1272
+ "labeledData = list(zip(commentList, emotionList))\n",
1273
+ "\n",
1274
+ "featureList = []\n",
1275
+ "for sentence, label in labeledData:\n",
1276
+ " checklist = word_tokenize(sentence.lower())\n",
1277
+ " checklist = preProcess(checklist)\n",
1278
+ "\n",
1279
+ " features = {}\n",
1280
+ " for word in wordList:\n",
1281
+ " features[word] = (word in checklist)\n",
1282
+ "\n",
1283
+ " featureList.append((features, label))"
1284
+ ]
1285
+ },
1286
+ {
1287
+ "cell_type": "code",
1288
+ "execution_count": null,
1289
+ "metadata": {
1290
+ "id": "Gy3PxRmBLmQZ"
1291
+ },
1292
+ "outputs": [],
1293
+ "source": [
1294
+ "# featureList"
1295
+ ]
1296
+ },
1297
+ {
1298
+ "cell_type": "markdown",
1299
+ "metadata": {
1300
+ "id": "0G1vFvJxLmQa"
1301
+ },
1302
+ "source": [
1303
+ "## Naive Bayes Part"
1304
+ ]
1305
+ },
1306
+ {
1307
+ "cell_type": "code",
1308
+ "execution_count": null,
1309
+ "metadata": {
1310
+ "id": "jwM5tDvMLmQb",
1311
+ "outputId": "4d979351-b483-4e9a-b674-b89197c9248f"
1312
+ },
1313
+ "outputs": [
1314
+ {
1315
+ "name": "stdout",
1316
+ "output_type": "stream",
1317
+ "text": [
1318
+ "Most Informative Features\n",
1319
+ " scare = True fear : joy = 41.1 : 1.0\n",
1320
+ " terrifi = True fear : joy = 39.0 : 1.0\n",
1321
+ " unsur = True fear : anger = 34.7 : 1.0\n",
1322
+ " vulner = True fear : joy = 33.6 : 1.0\n",
1323
+ " paranoid = True fear : anger = 32.0 : 1.0\n",
1324
+ "Acc: 0.8812131423757371\n"
1325
+ ]
1326
+ }
1327
+ ],
1328
+ "source": [
1329
+ "random.shuffle(featureList)\n",
1330
+ "idxData = int(len(featureList)*0.8)\n",
1331
+ "trainData = featureList[:idxData]\n",
1332
+ "testData = featureList[idxData:]\n",
1333
+ "\n",
1334
+ "classifier = NaiveBayesClassifier.train(trainData)\n",
1335
+ "\n",
1336
+ "# Compute Accuracy\n",
1337
+ "acc = accuracy(classifier, testData)\n",
1338
+ "\n",
1339
+ "classifier.show_most_informative_features(n=5)\n",
1340
+ "print(f'Accuracy: {acc}')"
1341
+ ]
1342
+ },
1343
+ {
1344
+ "cell_type": "code",
1345
+ "execution_count": null,
1346
+ "metadata": {
1347
+ "id": "-EcW0R15LmQc",
1348
+ "outputId": "c2e1ca0b-bea4-4867-dc73-ae743ba76e06"
1349
+ },
1350
+ "outputs": [
1351
+ {
1352
+ "name": "stdout",
1353
+ "output_type": "stream",
1354
+ "text": [
1355
+ "Confusion Matrix:\n",
1356
+ "[[357 26 11]\n",
1357
+ " [ 23 343 12]\n",
1358
+ " [ 28 41 346]]\n"
1359
+ ]
1360
+ }
1361
+ ],
1362
+ "source": [
1363
+ "# Make predictions on the test data\n",
1364
+ "predicted_labels = [classifier.classify(features) for features, label in testData]\n",
1365
+ "true_labels = [label for features, label in testData]\n",
1366
+ "\n",
1367
+ "# Compute confusion matrix\n",
1368
+ "conf_matrix = confusion_matrix(true_labels, predicted_labels)\n",
1369
+ "print(\"Confusion Matrix:\")\n",
1370
+ "print(conf_matrix)"
1371
+ ]
1372
+ },
1373
+ {
1374
+ "cell_type": "code",
1375
+ "execution_count": null,
1376
+ "metadata": {
1377
+ "id": "qVHGABEQLmQd",
1378
+ "outputId": "c9ef6638-1a84-4b92-c22e-782dbf645002"
1379
+ },
1380
+ "outputs": [
1381
+ {
1382
+ "data": {
1383
+ "image/png": "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z549KfbdsGGDunTpokqVKqly5cp6/fXXtXv3brs9CwDYgul14Dl04cIFSVKBAgXM2j///HPlz59fgYGBunTpkooUKaK7d+/K399fJ0+e1KuvvqomTZro4sWLWrZsmUJDQ/Xjjz8qX758kqTLly+rS5cuunHjhho0aKAXX3xRISEheuONN1IV16+//qq3335bycnJatiwoQoWLKjt27frP//5jy5duqRBgwYpMDBQP/30k44fP67OnTurePHipvuHDx+u1atXy9vbW126dFFMTIwpIZs9e7Zq1qwp6f761jfffFN79+5V+fLl1aRJEx0/fly9evWSm5tbGvwOm7t06ZI6dOig2NhYNWrUSAULFtS1a9e0adMmjRo1SklJSQoMDDS7Z9y4cUpISFDLli0VFRWljRs3qkePHpoxY4Z8fX1N/SZPnqwZM2aocOHCateunQwGgzZt2qQePXpo3LhxatOmTZo/DwDYxAggU5kyZYrR29vbuGLFihSvHz582Fi6dGljuXLljDdu3DAajUbjihUrjN7e3sa6desao6OjzfqPHDnS6O3tbVy6dKlZ+7Zt24ze3t7GQYMGmdqGDBli9Pb2Nq5cudLUFhcXZ+zevbvR29vb2L17d1P7g++cP3++0Wg0GhMTE43169c3li9f3vjHH3+Y+sXExBhbtmxpLF26tPHmzZtGo9FoHD58uNHb29t49OhRU7/169cbvb29jUOGDDEmJiaa2i9evGisVq2asW7dusb4+Hij0Wg0BgUFGb29vY0jRowwJiUlmfp+9dVXRm9vb6O3t/fDf4P/63G/z//08ccfG729vY07d+40az98+LDR29vb2KlTJ4txX3nlFePFixdN7WFhYcYKFSoYfX19Tc936NAho4+PjzEwMNAYExNj6nvr1i1j48aNjRUrVjT9nu3evdvo7e1t/Pzzzx8bLwDYA9PrQCYVHBysqVOnmj6TJk3SO++8o27duikxMVFDhw5V7ty5ze6pW7euXF1dTT8nJiZq1apVpsrhP9WvX1+VK1fW5s2bde/ePcXHxys4OFgvvfSS2rVrZ+qXNWtWvf/++4+N9+DBg/r777/Vpk0bVahQwdTu4uKiDz74QAMGDFB8fPxD71++fLkkacSIEXJ0dDS1Fy5cWF26dNHVq1e1a9cuSdL69etlMBj0/vvvy8Hhf/8ZHDhwoLJnz/7Y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",
1384
+ "text/plain": [
1385
+ "<Figure size 800x600 with 2 Axes>"
1386
+ ]
1387
+ },
1388
+ "metadata": {},
1389
+ "output_type": "display_data"
1390
+ }
1391
+ ],
1392
+ "source": [
1393
+ "# Plot confusion matrix as heatmap\n",
1394
+ "plt.figure(figsize=(8, 6))\n",
1395
+ "sns.set(font_scale=1.2)\n",
1396
+ "sns.heatmap(conf_matrix, annot=True, cmap='magma', fmt='g', xticklabels=True, yticklabels=True)\n",
1397
+ "plt.xlabel('Predicted Label')\n",
1398
+ "plt.ylabel('True Label')\n",
1399
+ "plt.title('CM Naive Bayes')\n",
1400
+ "plt.show()"
1401
+ ]
1402
+ },
1403
+ {
1404
+ "cell_type": "code",
1405
+ "execution_count": null,
1406
+ "metadata": {
1407
+ "id": "RV74Sye4LmQe",
1408
+ "outputId": "054b8930-0969-4921-c6d8-2ffd7e80372c"
1409
+ },
1410
+ "outputs": [
1411
+ {
1412
+ "name": "stdout",
1413
+ "output_type": "stream",
1414
+ "text": [
1415
+ "Accuracy: 0.8812131423757371\n",
1416
+ "Precision: 0.8846773880544198\n",
1417
+ "Recall: 0.8812131423757371\n",
1418
+ "F1 Score: 0.8813312267889719\n"
1419
+ ]
1420
+ }
1421
+ ],
1422
+ "source": [
1423
+ "# Compute precision, recall, and F1 score\n",
1424
+ "precision = precision_score(true_labels, predicted_labels, average='weighted')\n",
1425
+ "recall = recall_score(true_labels, predicted_labels, average='weighted')\n",
1426
+ "f1 = f1_score(true_labels, predicted_labels, average='weighted')\n",
1427
+ "\n",
1428
+ "# Print evaluation metrics\n",
1429
+ "print(f'Accuracy: {acc}')\n",
1430
+ "print(f'Precision: {precision}')\n",
1431
+ "print(f'Recall: {recall}')\n",
1432
+ "print(f'F1 Score: {f1}')"
1433
+ ]
1434
+ },
1435
+ {
1436
+ "cell_type": "code",
1437
+ "execution_count": null,
1438
+ "metadata": {
1439
+ "id": "cT9nIIdWLmQf",
1440
+ "outputId": "38736f92-1cec-43a2-ed50-69f4df29569f"
1441
+ },
1442
+ "outputs": [
1443
+ {
1444
+ "name": "stdout",
1445
+ "output_type": "stream",
1446
+ "text": [
1447
+ "Sentence: It was so frightening\n",
1448
+ "Emotion: fear\n"
1449
+ ]
1450
+ }
1451
+ ],
1452
+ "source": [
1453
+ "testDrive = \"It was so frightening\"\n",
1454
+ "print(f'Sentence: {testDrive}')\n",
1455
+ "print(f'Emotion: {classifier.classify(FreqDist(preProcess(word_tokenize(testDrive))))}')"
1456
+ ]
1457
+ }
1458
+ ],
1459
+ "metadata": {
1460
+ "kernelspec": {
1461
+ "display_name": "base",
1462
+ "language": "python",
1463
+ "name": "python3"
1464
+ },
1465
+ "language_info": {
1466
+ "codemirror_mode": {
1467
+ "name": "ipython",
1468
+ "version": 3
1469
+ },
1470
+ "file_extension": ".py",
1471
+ "mimetype": "text/x-python",
1472
+ "name": "python",
1473
+ "nbconvert_exporter": "python",
1474
+ "pygments_lexer": "ipython3",
1475
+ "version": "3.9.13"
1476
+ },
1477
+ "colab": {
1478
+ "provenance": []
1479
+ }
1480
+ },
1481
+ "nbformat": 4,
1482
+ "nbformat_minor": 0
1483
+ }
LEGACY/QuotesSelector.ipynb ADDED
@@ -0,0 +1,851 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {
7
+ "id": "Fjb2MSfGZxXW"
8
+ },
9
+ "outputs": [],
10
+ "source": [
11
+ "import torch\n",
12
+ "from transformers import BertTokenizer, BertForSequenceClassification\n",
13
+ "from torch.utils.data import DataLoader, TensorDataset\n",
14
+ "import pandas as pd\n",
15
+ "from sklearn.preprocessing import LabelEncoder\n",
16
+ "\n",
17
+ "import pandas as pd\n",
18
+ "import numpy as np\n",
19
+ "\n",
20
+ "import random\n",
21
+ "\n",
22
+ "import matplotlib.pyplot as plt\n",
23
+ "\n",
24
+ "import pandas as pd\n",
25
+ "from sklearn.feature_extraction.text import TfidfVectorizer\n",
26
+ "from sklearn.metrics.pairwise import cosine_similarity\n",
27
+ "import numpy as np"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "markdown",
32
+ "metadata": {
33
+ "id": "4daEm8HLZxXK"
34
+ },
35
+ "source": [
36
+ "## LOADUP Trained Model BERT"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": null,
42
+ "metadata": {
43
+ "id": "u0hvSKq4ZxXO"
44
+ },
45
+ "outputs": [],
46
+ "source": [
47
+ "dataset = pd.read_csv('../Dataset/(Preprocessed)Emotion_classify_Data(Labeled).csv')"
48
+ ]
49
+ },
50
+ {
51
+ "cell_type": "code",
52
+ "execution_count": null,
53
+ "metadata": {
54
+ "id": "ALC9OXraZxXQ"
55
+ },
56
+ "outputs": [],
57
+ "source": [
58
+ "# Shuffle the dataset\n",
59
+ "dataset = dataset.sample(frac=1, random_state=42).reset_index(drop=True)"
60
+ ]
61
+ },
62
+ {
63
+ "cell_type": "code",
64
+ "execution_count": null,
65
+ "metadata": {
66
+ "id": "FDYeA9lnZxXR",
67
+ "outputId": "2960b5d0-f2aa-46d6-d8a7-1ec91f5c57c3"
68
+ },
69
+ "outputs": [
70
+ {
71
+ "name": "stderr",
72
+ "output_type": "stream",
73
+ "text": [
74
+ "Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-uncased and are newly initialized: ['classifier.bias', 'classifier.weight']\n",
75
+ "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n"
76
+ ]
77
+ }
78
+ ],
79
+ "source": [
80
+ "# Initialize BERT tokenizer\n",
81
+ "tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')\n",
82
+ "\n",
83
+ "# Load pre-trained BERT model for sequence classification\n",
84
+ "model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=dataset['Emotion'].nunique())\n",
85
+ "\n",
86
+ "# Fine-tune BERT on your dataset\n",
87
+ "learning_rate = 2e-5\n",
88
+ "optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)\n",
89
+ "loss_fn = torch.nn.CrossEntropyLoss()\n",
90
+ "\n",
91
+ "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')"
92
+ ]
93
+ },
94
+ {
95
+ "cell_type": "code",
96
+ "execution_count": null,
97
+ "metadata": {
98
+ "id": "oqFQb1sDZxXT",
99
+ "outputId": "3a44cbf6-b96c-48ea-879b-d79e6d940ae0"
100
+ },
101
+ "outputs": [
102
+ {
103
+ "data": {
104
+ "text/html": [
105
+ "<style>#sk-container-id-1 {\n",
106
+ " /* Definition of color scheme common for light and dark mode */\n",
107
+ " --sklearn-color-text: black;\n",
108
+ " --sklearn-color-line: gray;\n",
109
+ " /* Definition of color scheme for unfitted estimators */\n",
110
+ " --sklearn-color-unfitted-level-0: #fff5e6;\n",
111
+ " --sklearn-color-unfitted-level-1: #f6e4d2;\n",
112
+ " --sklearn-color-unfitted-level-2: #ffe0b3;\n",
113
+ " --sklearn-color-unfitted-level-3: chocolate;\n",
114
+ " /* Definition of color scheme for fitted estimators */\n",
115
+ " --sklearn-color-fitted-level-0: #f0f8ff;\n",
116
+ " --sklearn-color-fitted-level-1: #d4ebff;\n",
117
+ " --sklearn-color-fitted-level-2: #b3dbfd;\n",
118
+ " --sklearn-color-fitted-level-3: cornflowerblue;\n",
119
+ "\n",
120
+ " /* Specific color for light theme */\n",
121
+ " --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
122
+ " --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
123
+ " --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
124
+ " --sklearn-color-icon: #696969;\n",
125
+ "\n",
126
+ " @media (prefers-color-scheme: dark) {\n",
127
+ " /* Redefinition of color scheme for dark theme */\n",
128
+ " --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
129
+ " --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
130
+ " --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
131
+ " --sklearn-color-icon: #878787;\n",
132
+ " }\n",
133
+ "}\n",
134
+ "\n",
135
+ "#sk-container-id-1 {\n",
136
+ " color: var(--sklearn-color-text);\n",
137
+ "}\n",
138
+ "\n",
139
+ "#sk-container-id-1 pre {\n",
140
+ " padding: 0;\n",
141
+ "}\n",
142
+ "\n",
143
+ "#sk-container-id-1 input.sk-hidden--visually {\n",
144
+ " border: 0;\n",
145
+ " clip: rect(1px 1px 1px 1px);\n",
146
+ " clip: rect(1px, 1px, 1px, 1px);\n",
147
+ " height: 1px;\n",
148
+ " margin: -1px;\n",
149
+ " overflow: hidden;\n",
150
+ " padding: 0;\n",
151
+ " position: absolute;\n",
152
+ " width: 1px;\n",
153
+ "}\n",
154
+ "\n",
155
+ "#sk-container-id-1 div.sk-dashed-wrapped {\n",
156
+ " border: 1px dashed var(--sklearn-color-line);\n",
157
+ " margin: 0 0.4em 0.5em 0.4em;\n",
158
+ " box-sizing: border-box;\n",
159
+ " padding-bottom: 0.4em;\n",
160
+ " background-color: var(--sklearn-color-background);\n",
161
+ "}\n",
162
+ "\n",
163
+ "#sk-container-id-1 div.sk-container {\n",
164
+ " /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
165
+ " but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
166
+ " so we also need the `!important` here to be able to override the\n",
167
+ " default hidden behavior on the sphinx rendered scikit-learn.org.\n",
168
+ " See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
169
+ " display: inline-block !important;\n",
170
+ " position: relative;\n",
171
+ "}\n",
172
+ "\n",
173
+ "#sk-container-id-1 div.sk-text-repr-fallback {\n",
174
+ " display: none;\n",
175
+ "}\n",
176
+ "\n",
177
+ "div.sk-parallel-item,\n",
178
+ "div.sk-serial,\n",
179
+ "div.sk-item {\n",
180
+ " /* draw centered vertical line to link estimators */\n",
181
+ " background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
182
+ " background-size: 2px 100%;\n",
183
+ " background-repeat: no-repeat;\n",
184
+ " background-position: center center;\n",
185
+ "}\n",
186
+ "\n",
187
+ "/* Parallel-specific style estimator block */\n",
188
+ "\n",
189
+ "#sk-container-id-1 div.sk-parallel-item::after {\n",
190
+ " content: \"\";\n",
191
+ " width: 100%;\n",
192
+ " border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
193
+ " flex-grow: 1;\n",
194
+ "}\n",
195
+ "\n",
196
+ "#sk-container-id-1 div.sk-parallel {\n",
197
+ " display: flex;\n",
198
+ " align-items: stretch;\n",
199
+ " justify-content: center;\n",
200
+ " background-color: var(--sklearn-color-background);\n",
201
+ " position: relative;\n",
202
+ "}\n",
203
+ "\n",
204
+ "#sk-container-id-1 div.sk-parallel-item {\n",
205
+ " display: flex;\n",
206
+ " flex-direction: column;\n",
207
+ "}\n",
208
+ "\n",
209
+ "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
210
+ " align-self: flex-end;\n",
211
+ " width: 50%;\n",
212
+ "}\n",
213
+ "\n",
214
+ "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
215
+ " align-self: flex-start;\n",
216
+ " width: 50%;\n",
217
+ "}\n",
218
+ "\n",
219
+ "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
220
+ " width: 0;\n",
221
+ "}\n",
222
+ "\n",
223
+ "/* Serial-specific style estimator block */\n",
224
+ "\n",
225
+ "#sk-container-id-1 div.sk-serial {\n",
226
+ " display: flex;\n",
227
+ " flex-direction: column;\n",
228
+ " align-items: center;\n",
229
+ " background-color: var(--sklearn-color-background);\n",
230
+ " padding-right: 1em;\n",
231
+ " padding-left: 1em;\n",
232
+ "}\n",
233
+ "\n",
234
+ "\n",
235
+ "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
236
+ "clickable and can be expanded/collapsed.\n",
237
+ "- Pipeline and ColumnTransformer use this feature and define the default style\n",
238
+ "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
239
+ "*/\n",
240
+ "\n",
241
+ "/* Pipeline and ColumnTransformer style (default) */\n",
242
+ "\n",
243
+ "#sk-container-id-1 div.sk-toggleable {\n",
244
+ " /* Default theme specific background. It is overwritten whether we have a\n",
245
+ " specific estimator or a Pipeline/ColumnTransformer */\n",
246
+ " background-color: var(--sklearn-color-background);\n",
247
+ "}\n",
248
+ "\n",
249
+ "/* Toggleable label */\n",
250
+ "#sk-container-id-1 label.sk-toggleable__label {\n",
251
+ " cursor: pointer;\n",
252
+ " display: block;\n",
253
+ " width: 100%;\n",
254
+ " margin-bottom: 0;\n",
255
+ " padding: 0.5em;\n",
256
+ " box-sizing: border-box;\n",
257
+ " text-align: center;\n",
258
+ "}\n",
259
+ "\n",
260
+ "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
261
+ " /* Arrow on the left of the label */\n",
262
+ " content: \"▸\";\n",
263
+ " float: left;\n",
264
+ " margin-right: 0.25em;\n",
265
+ " color: var(--sklearn-color-icon);\n",
266
+ "}\n",
267
+ "\n",
268
+ "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
269
+ " color: var(--sklearn-color-text);\n",
270
+ "}\n",
271
+ "\n",
272
+ "/* Toggleable content - dropdown */\n",
273
+ "\n",
274
+ "#sk-container-id-1 div.sk-toggleable__content {\n",
275
+ " max-height: 0;\n",
276
+ " max-width: 0;\n",
277
+ " overflow: hidden;\n",
278
+ " text-align: left;\n",
279
+ " /* unfitted */\n",
280
+ " background-color: var(--sklearn-color-unfitted-level-0);\n",
281
+ "}\n",
282
+ "\n",
283
+ "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
284
+ " /* fitted */\n",
285
+ " background-color: var(--sklearn-color-fitted-level-0);\n",
286
+ "}\n",
287
+ "\n",
288
+ "#sk-container-id-1 div.sk-toggleable__content pre {\n",
289
+ " margin: 0.2em;\n",
290
+ " border-radius: 0.25em;\n",
291
+ " color: var(--sklearn-color-text);\n",
292
+ " /* unfitted */\n",
293
+ " background-color: var(--sklearn-color-unfitted-level-0);\n",
294
+ "}\n",
295
+ "\n",
296
+ "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
297
+ " /* unfitted */\n",
298
+ " background-color: var(--sklearn-color-fitted-level-0);\n",
299
+ "}\n",
300
+ "\n",
301
+ "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
302
+ " /* Expand drop-down */\n",
303
+ " max-height: 200px;\n",
304
+ " max-width: 100%;\n",
305
+ " overflow: auto;\n",
306
+ "}\n",
307
+ "\n",
308
+ "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
309
+ " content: \"▾\";\n",
310
+ "}\n",
311
+ "\n",
312
+ "/* Pipeline/ColumnTransformer-specific style */\n",
313
+ "\n",
314
+ "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
315
+ " color: var(--sklearn-color-text);\n",
316
+ " background-color: var(--sklearn-color-unfitted-level-2);\n",
317
+ "}\n",
318
+ "\n",
319
+ "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
320
+ " background-color: var(--sklearn-color-fitted-level-2);\n",
321
+ "}\n",
322
+ "\n",
323
+ "/* Estimator-specific style */\n",
324
+ "\n",
325
+ "/* Colorize estimator box */\n",
326
+ "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
327
+ " /* unfitted */\n",
328
+ " background-color: var(--sklearn-color-unfitted-level-2);\n",
329
+ "}\n",
330
+ "\n",
331
+ "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
332
+ " /* fitted */\n",
333
+ " background-color: var(--sklearn-color-fitted-level-2);\n",
334
+ "}\n",
335
+ "\n",
336
+ "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
337
+ "#sk-container-id-1 div.sk-label label {\n",
338
+ " /* The background is the default theme color */\n",
339
+ " color: var(--sklearn-color-text-on-default-background);\n",
340
+ "}\n",
341
+ "\n",
342
+ "/* On hover, darken the color of the background */\n",
343
+ "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
344
+ " color: var(--sklearn-color-text);\n",
345
+ " background-color: var(--sklearn-color-unfitted-level-2);\n",
346
+ "}\n",
347
+ "\n",
348
+ "/* Label box, darken color on hover, fitted */\n",
349
+ "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
350
+ " color: var(--sklearn-color-text);\n",
351
+ " background-color: var(--sklearn-color-fitted-level-2);\n",
352
+ "}\n",
353
+ "\n",
354
+ "/* Estimator label */\n",
355
+ "\n",
356
+ "#sk-container-id-1 div.sk-label label {\n",
357
+ " font-family: monospace;\n",
358
+ " font-weight: bold;\n",
359
+ " display: inline-block;\n",
360
+ " line-height: 1.2em;\n",
361
+ "}\n",
362
+ "\n",
363
+ "#sk-container-id-1 div.sk-label-container {\n",
364
+ " text-align: center;\n",
365
+ "}\n",
366
+ "\n",
367
+ "/* Estimator-specific */\n",
368
+ "#sk-container-id-1 div.sk-estimator {\n",
369
+ " font-family: monospace;\n",
370
+ " border: 1px dotted var(--sklearn-color-border-box);\n",
371
+ " border-radius: 0.25em;\n",
372
+ " box-sizing: border-box;\n",
373
+ " margin-bottom: 0.5em;\n",
374
+ " /* unfitted */\n",
375
+ " background-color: var(--sklearn-color-unfitted-level-0);\n",
376
+ "}\n",
377
+ "\n",
378
+ "#sk-container-id-1 div.sk-estimator.fitted {\n",
379
+ " /* fitted */\n",
380
+ " background-color: var(--sklearn-color-fitted-level-0);\n",
381
+ "}\n",
382
+ "\n",
383
+ "/* on hover */\n",
384
+ "#sk-container-id-1 div.sk-estimator:hover {\n",
385
+ " /* unfitted */\n",
386
+ " background-color: var(--sklearn-color-unfitted-level-2);\n",
387
+ "}\n",
388
+ "\n",
389
+ "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
390
+ " /* fitted */\n",
391
+ " background-color: var(--sklearn-color-fitted-level-2);\n",
392
+ "}\n",
393
+ "\n",
394
+ "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
395
+ "\n",
396
+ "/* Common style for \"i\" and \"?\" */\n",
397
+ "\n",
398
+ ".sk-estimator-doc-link,\n",
399
+ "a:link.sk-estimator-doc-link,\n",
400
+ "a:visited.sk-estimator-doc-link {\n",
401
+ " float: right;\n",
402
+ " font-size: smaller;\n",
403
+ " line-height: 1em;\n",
404
+ " font-family: monospace;\n",
405
+ " background-color: var(--sklearn-color-background);\n",
406
+ " border-radius: 1em;\n",
407
+ " height: 1em;\n",
408
+ " width: 1em;\n",
409
+ " text-decoration: none !important;\n",
410
+ " margin-left: 1ex;\n",
411
+ " /* unfitted */\n",
412
+ " border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
413
+ " color: var(--sklearn-color-unfitted-level-1);\n",
414
+ "}\n",
415
+ "\n",
416
+ ".sk-estimator-doc-link.fitted,\n",
417
+ "a:link.sk-estimator-doc-link.fitted,\n",
418
+ "a:visited.sk-estimator-doc-link.fitted {\n",
419
+ " /* fitted */\n",
420
+ " border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
421
+ " color: var(--sklearn-color-fitted-level-1);\n",
422
+ "}\n",
423
+ "\n",
424
+ "/* On hover */\n",
425
+ "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
426
+ ".sk-estimator-doc-link:hover,\n",
427
+ "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
428
+ ".sk-estimator-doc-link:hover {\n",
429
+ " /* unfitted */\n",
430
+ " background-color: var(--sklearn-color-unfitted-level-3);\n",
431
+ " color: var(--sklearn-color-background);\n",
432
+ " text-decoration: none;\n",
433
+ "}\n",
434
+ "\n",
435
+ "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
436
+ ".sk-estimator-doc-link.fitted:hover,\n",
437
+ "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
438
+ ".sk-estimator-doc-link.fitted:hover {\n",
439
+ " /* fitted */\n",
440
+ " background-color: var(--sklearn-color-fitted-level-3);\n",
441
+ " color: var(--sklearn-color-background);\n",
442
+ " text-decoration: none;\n",
443
+ "}\n",
444
+ "\n",
445
+ "/* Span, style for the box shown on hovering the info icon */\n",
446
+ ".sk-estimator-doc-link span {\n",
447
+ " display: none;\n",
448
+ " z-index: 9999;\n",
449
+ " position: relative;\n",
450
+ " font-weight: normal;\n",
451
+ " right: .2ex;\n",
452
+ " padding: .5ex;\n",
453
+ " margin: .5ex;\n",
454
+ " width: min-content;\n",
455
+ " min-width: 20ex;\n",
456
+ " max-width: 50ex;\n",
457
+ " color: var(--sklearn-color-text);\n",
458
+ " box-shadow: 2pt 2pt 4pt #999;\n",
459
+ " /* unfitted */\n",
460
+ " background: var(--sklearn-color-unfitted-level-0);\n",
461
+ " border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
462
+ "}\n",
463
+ "\n",
464
+ ".sk-estimator-doc-link.fitted span {\n",
465
+ " /* fitted */\n",
466
+ " background: var(--sklearn-color-fitted-level-0);\n",
467
+ " border: var(--sklearn-color-fitted-level-3);\n",
468
+ "}\n",
469
+ "\n",
470
+ ".sk-estimator-doc-link:hover span {\n",
471
+ " display: block;\n",
472
+ "}\n",
473
+ "\n",
474
+ "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
475
+ "\n",
476
+ "#sk-container-id-1 a.estimator_doc_link {\n",
477
+ " float: right;\n",
478
+ " font-size: 1rem;\n",
479
+ " line-height: 1em;\n",
480
+ " font-family: monospace;\n",
481
+ " background-color: var(--sklearn-color-background);\n",
482
+ " border-radius: 1rem;\n",
483
+ " height: 1rem;\n",
484
+ " width: 1rem;\n",
485
+ " text-decoration: none;\n",
486
+ " /* unfitted */\n",
487
+ " color: var(--sklearn-color-unfitted-level-1);\n",
488
+ " border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
489
+ "}\n",
490
+ "\n",
491
+ "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
492
+ " /* fitted */\n",
493
+ " border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
494
+ " color: var(--sklearn-color-fitted-level-1);\n",
495
+ "}\n",
496
+ "\n",
497
+ "/* On hover */\n",
498
+ "#sk-container-id-1 a.estimator_doc_link:hover {\n",
499
+ " /* unfitted */\n",
500
+ " background-color: var(--sklearn-color-unfitted-level-3);\n",
501
+ " color: var(--sklearn-color-background);\n",
502
+ " text-decoration: none;\n",
503
+ "}\n",
504
+ "\n",
505
+ "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
506
+ " /* fitted */\n",
507
+ " background-color: var(--sklearn-color-fitted-level-3);\n",
508
+ "}\n",
509
+ "</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>LabelEncoder()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow fitted\">&nbsp;&nbsp;LabelEncoder<a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.4/modules/generated/sklearn.preprocessing.LabelEncoder.html\">?<span>Documentation for LabelEncoder</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></label><div class=\"sk-toggleable__content fitted\"><pre>LabelEncoder()</pre></div> </div></div></div></div>"
510
+ ],
511
+ "text/plain": [
512
+ "LabelEncoder()"
513
+ ]
514
+ },
515
+ "execution_count": 13,
516
+ "metadata": {},
517
+ "output_type": "execute_result"
518
+ }
519
+ ],
520
+ "source": [
521
+ "emotion_labels = dataset['Emotion']\n",
522
+ "\n",
523
+ "# Instantiate the label encoder\n",
524
+ "label_encoder = LabelEncoder()\n",
525
+ "\n",
526
+ "# Fit the label encoder to the encoded emotion labels\n",
527
+ "label_encoder.fit(emotion_labels)\n"
528
+ ]
529
+ },
530
+ {
531
+ "cell_type": "code",
532
+ "execution_count": null,
533
+ "metadata": {
534
+ "id": "2sNgKjaTZxXT"
535
+ },
536
+ "outputs": [],
537
+ "source": [
538
+ " # Load the checkpoint\n",
539
+ " checkpoint = torch.load('last_trained_model_checkpoint.pth' , map_location=torch.device('cpu'))\n",
540
+ "\n",
541
+ " # Load model and optimizer states\n",
542
+ " model.load_state_dict(checkpoint['model_state_dict'])\n",
543
+ " optimizer.load_state_dict(checkpoint['optimizer_state_dict'])\n",
544
+ "\n",
545
+ " # Update other necessary variables\n",
546
+ " epoch = checkpoint['epoch']\n",
547
+ " best_val_loss = checkpoint['best_val_loss']\n",
548
+ " early_stop_count = checkpoint['early_stop_count']"
549
+ ]
550
+ },
551
+ {
552
+ "cell_type": "code",
553
+ "execution_count": null,
554
+ "metadata": {
555
+ "id": "IKBkgBMvZxXU",
556
+ "outputId": "19a64462-3f9d-46e7-8b18-230f3da49987"
557
+ },
558
+ "outputs": [
559
+ {
560
+ "name": "stdout",
561
+ "output_type": "stream",
562
+ "text": [
563
+ "Predicted Emotion: joy\n",
564
+ "Predicted Label: tensor([[-2.6676, -2.7545, 6.4300]])\n"
565
+ ]
566
+ }
567
+ ],
568
+ "source": [
569
+ "def predict_emotion(text, label_encoder):\n",
570
+ " # Tokenize the text using the same tokenizer used during training\n",
571
+ " inputs = tokenizer.encode_plus(text, add_special_tokens=True, max_length=128, padding='max_length', truncation=True, return_tensors='pt')\n",
572
+ " input_ids = inputs['input_ids'].to(device) # Move input to the same device as the model\n",
573
+ " attention_mask = inputs['attention_mask'].to(device) # Move attention mask to the same device as the model\n",
574
+ "\n",
575
+ " # Make prediction\n",
576
+ " with torch.no_grad():\n",
577
+ " model.eval() # Set model to evaluation mode\n",
578
+ " outputs = model(input_ids, attention_mask=attention_mask)\n",
579
+ " logits = outputs.logits\n",
580
+ "\n",
581
+ " # Get predicted label\n",
582
+ " predicted_label = torch.argmax(logits, dim=1).item()\n",
583
+ "\n",
584
+ " # Decode the predicted label using label_encoder\n",
585
+ " predicted_emotion = label_encoder.inverse_transform([predicted_label])[0]\n",
586
+ "\n",
587
+ " return predicted_emotion, logits\n",
588
+ "\n",
589
+ "# Example usage\n",
590
+ "text = \"Super happy today\"\n",
591
+ "predicted_emotion, x = predict_emotion(text, label_encoder)\n",
592
+ "print(f'Predicted Emotion: {predicted_emotion}')\n",
593
+ "print(f'Predicted Label: {x}')"
594
+ ]
595
+ },
596
+ {
597
+ "cell_type": "markdown",
598
+ "metadata": {
599
+ "id": "P0nhzIDyZxXV"
600
+ },
601
+ "source": [
602
+ "## Get the Quotes"
603
+ ]
604
+ },
605
+ {
606
+ "cell_type": "code",
607
+ "execution_count": null,
608
+ "metadata": {
609
+ "id": "MDIaQThjZxXX",
610
+ "outputId": "9ad0c687-af99-447c-ea1d-c12aa2db310e"
611
+ },
612
+ "outputs": [
613
+ {
614
+ "data": {
615
+ "text/html": [
616
+ "<div>\n",
617
+ "<style scoped>\n",
618
+ " .dataframe tbody tr th:only-of-type {\n",
619
+ " vertical-align: middle;\n",
620
+ " }\n",
621
+ "\n",
622
+ " .dataframe tbody tr th {\n",
623
+ " vertical-align: top;\n",
624
+ " }\n",
625
+ "\n",
626
+ " .dataframe thead th {\n",
627
+ " text-align: right;\n",
628
+ " }\n",
629
+ "</style>\n",
630
+ "<table border=\"1\" class=\"dataframe\">\n",
631
+ " <thead>\n",
632
+ " <tr style=\"text-align: right;\">\n",
633
+ " <th></th>\n",
634
+ " <th>Quote</th>\n",
635
+ " <th>Author</th>\n",
636
+ " <th>emotion</th>\n",
637
+ " </tr>\n",
638
+ " </thead>\n",
639
+ " <tbody>\n",
640
+ " <tr>\n",
641
+ " <th>0</th>\n",
642
+ " <td>Don't cry because it's over, smile because it ...</td>\n",
643
+ " <td>Dr. Seuss</td>\n",
644
+ " <td>joy</td>\n",
645
+ " </tr>\n",
646
+ " <tr>\n",
647
+ " <th>1</th>\n",
648
+ " <td>I'm selfish, impatient and a little insecure. ...</td>\n",
649
+ " <td>Marilyn Monroe</td>\n",
650
+ " <td>anger</td>\n",
651
+ " </tr>\n",
652
+ " <tr>\n",
653
+ " <th>2</th>\n",
654
+ " <td>Be yourself; everyone else is already taken.</td>\n",
655
+ " <td>Oscar Wilde</td>\n",
656
+ " <td>anger</td>\n",
657
+ " </tr>\n",
658
+ " <tr>\n",
659
+ " <th>3</th>\n",
660
+ " <td>Two things are infinite: the universe and huma...</td>\n",
661
+ " <td>Albert Einstein</td>\n",
662
+ " <td>joy</td>\n",
663
+ " </tr>\n",
664
+ " <tr>\n",
665
+ " <th>4</th>\n",
666
+ " <td>Be who you are and say what you feel, because ...</td>\n",
667
+ " <td>Bernard M. Baruch</td>\n",
668
+ " <td>joy</td>\n",
669
+ " </tr>\n",
670
+ " <tr>\n",
671
+ " <th>...</th>\n",
672
+ " <td>...</td>\n",
673
+ " <td>...</td>\n",
674
+ " <td>...</td>\n",
675
+ " </tr>\n",
676
+ " <tr>\n",
677
+ " <th>36932</th>\n",
678
+ " <td>In Buddhism, they say attachment to anything o...</td>\n",
679
+ " <td>Jason Mraz</td>\n",
680
+ " <td>joy</td>\n",
681
+ " </tr>\n",
682
+ " <tr>\n",
683
+ " <th>36933</th>\n",
684
+ " <td>I love British humor. It's just so - surreal.</td>\n",
685
+ " <td>Beck</td>\n",
686
+ " <td>fear</td>\n",
687
+ " </tr>\n",
688
+ " <tr>\n",
689
+ " <th>36934</th>\n",
690
+ " <td>I've got a sense of humor. I'm a funny guy.</td>\n",
691
+ " <td>Daryl Hall</td>\n",
692
+ " <td>joy</td>\n",
693
+ " </tr>\n",
694
+ " <tr>\n",
695
+ " <th>36935</th>\n",
696
+ " <td>Humor is such a wonderful thing, helping you r...</td>\n",
697
+ " <td>Lynda Barry</td>\n",
698
+ " <td>joy</td>\n",
699
+ " </tr>\n",
700
+ " <tr>\n",
701
+ " <th>36936</th>\n",
702
+ " <td>Life Is Full of Obstacles, Stumble Upon !!</td>\n",
703
+ " <td>Pratik Shelar</td>\n",
704
+ " <td>fear</td>\n",
705
+ " </tr>\n",
706
+ " </tbody>\n",
707
+ "</table>\n",
708
+ "<p>36937 rows × 3 columns</p>\n",
709
+ "</div>"
710
+ ],
711
+ "text/plain": [
712
+ " Quote Author \\\n",
713
+ "0 Don't cry because it's over, smile because it ... Dr. Seuss \n",
714
+ "1 I'm selfish, impatient and a little insecure. ... Marilyn Monroe \n",
715
+ "2 Be yourself; everyone else is already taken. Oscar Wilde \n",
716
+ "3 Two things are infinite: the universe and huma... Albert Einstein \n",
717
+ "4 Be who you are and say what you feel, because ... Bernard M. Baruch \n",
718
+ "... ... ... \n",
719
+ "36932 In Buddhism, they say attachment to anything o... Jason Mraz \n",
720
+ "36933 I love British humor. It's just so - surreal. Beck \n",
721
+ "36934 I've got a sense of humor. I'm a funny guy. Daryl Hall \n",
722
+ "36935 Humor is such a wonderful thing, helping you r... Lynda Barry \n",
723
+ "36936 Life Is Full of Obstacles, Stumble Upon !! Pratik Shelar \n",
724
+ "\n",
725
+ " emotion \n",
726
+ "0 joy \n",
727
+ "1 anger \n",
728
+ "2 anger \n",
729
+ "3 joy \n",
730
+ "4 joy \n",
731
+ "... ... \n",
732
+ "36932 joy \n",
733
+ "36933 fear \n",
734
+ "36934 joy \n",
735
+ "36935 joy \n",
736
+ "36936 fear \n",
737
+ "\n",
738
+ "[36937 rows x 3 columns]"
739
+ ]
740
+ },
741
+ "execution_count": 5,
742
+ "metadata": {},
743
+ "output_type": "execute_result"
744
+ }
745
+ ],
746
+ "source": [
747
+ "quotesDF = pd.read_csv('../Dataset/(Preprocessed)quotes.csv')\n",
748
+ "quotesDF"
749
+ ]
750
+ },
751
+ {
752
+ "cell_type": "code",
753
+ "execution_count": null,
754
+ "metadata": {
755
+ "id": "0_Z5tCRWZxXX"
756
+ },
757
+ "outputs": [],
758
+ "source": [
759
+ "import numpy as np\n",
760
+ "from sklearn.feature_extraction.text import TfidfVectorizer\n",
761
+ "from sklearn.metrics.pairwise import cosine_similarity\n",
762
+ "\n",
763
+ "def find_most_similar_quote(input_sentence, quotes_df, input_emotion, emotion_weight=1.1):\n",
764
+ " # Combine all quotes into a list\n",
765
+ " quotes = quotes_df['Quote'].tolist()\n",
766
+ " authors = quotes_df['Author'].tolist()\n",
767
+ " emotions = quotes_df['emotion'].tolist()\n",
768
+ "\n",
769
+ " # Initialize the vectorizer and fit it on all quotes\n",
770
+ " vectorizer = TfidfVectorizer()\n",
771
+ " tfidf_matrix = vectorizer.fit_transform(quotes)\n",
772
+ "\n",
773
+ " # Vectorize the input sentence\n",
774
+ " input_vec = vectorizer.transform([input_sentence])\n",
775
+ "\n",
776
+ " # Calculate cosine similarity between the input sentence and all quotes\n",
777
+ " cosine_similarities = cosine_similarity(input_vec, tfidf_matrix).flatten()\n",
778
+ "\n",
779
+ " # Apply emotion-based weighting\n",
780
+ " emotion_weights = np.array([emotion_weight if emotion == input_emotion else 1.0 for emotion in emotions])\n",
781
+ " weighted_similarities = cosine_similarities * emotion_weights\n",
782
+ "\n",
783
+ " # Get the indices of the top 5 quotes with the highest similarity scores\n",
784
+ " top_10_indices = np.argsort(weighted_similarities)[-5:]\n",
785
+ "\n",
786
+ " # Randomly select one quote from the top\n",
787
+ " selected_index = random.choice(top_10_indices)\n",
788
+ "\n",
789
+ " # Return the most similar quote and its author\n",
790
+ " return quotes[selected_index], authors[selected_index], emotions[selected_index]\n"
791
+ ]
792
+ },
793
+ {
794
+ "cell_type": "code",
795
+ "execution_count": null,
796
+ "metadata": {
797
+ "id": "uOC5p6SXZxXY",
798
+ "outputId": "7fd15a8b-fa1c-4db9-d976-eeff41fa89f6"
799
+ },
800
+ "outputs": [
801
+ {
802
+ "name": "stdout",
803
+ "output_type": "stream",
804
+ "text": [
805
+ "Input sentence: What should I do with my life?\n",
806
+ "Detected emotion: anger\n",
807
+ "Emotion score: tensor([[ 1.2938, 0.3162, -1.3738]])\n",
808
+ "Most similar quote: Before I can tell my life what I want to do with it, I must listen to my life telling me who I am.\n",
809
+ "Author: Parker J. Palmer, Let Your Life Speak: Listening for the Voice of Vocation\n",
810
+ "Quotes Emotion: joy\n"
811
+ ]
812
+ }
813
+ ],
814
+ "source": [
815
+ "input_sentence = \"What should I do with my life?\"\n",
816
+ "detected_emotion, emotion_score = predict_emotion(input_sentence, label_encoder)\n",
817
+ "most_similar_quote, author, emotionQuotes = find_most_similar_quote(input_sentence, quotesDF, detected_emotion)\n",
818
+ "print('Input sentence: ', input_sentence)\n",
819
+ "print(\"Detected emotion:\", detected_emotion)\n",
820
+ "print(\"Emotion score:\", emotion_score)\n",
821
+ "print(\"Most similar quote:\", most_similar_quote)\n",
822
+ "print(\"Author: \", author)\n",
823
+ "print(\"Quotes Emotion: \", emotionQuotes)"
824
+ ]
825
+ }
826
+ ],
827
+ "metadata": {
828
+ "kernelspec": {
829
+ "display_name": "base",
830
+ "language": "python",
831
+ "name": "python3"
832
+ },
833
+ "language_info": {
834
+ "codemirror_mode": {
835
+ "name": "ipython",
836
+ "version": 3
837
+ },
838
+ "file_extension": ".py",
839
+ "mimetype": "text/x-python",
840
+ "name": "python",
841
+ "nbconvert_exporter": "python",
842
+ "pygments_lexer": "ipython3",
843
+ "version": "3.9.13"
844
+ },
845
+ "colab": {
846
+ "provenance": []
847
+ }
848
+ },
849
+ "nbformat": 4,
850
+ "nbformat_minor": 0
851
+ }
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Dimas Chrispradipta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Makefile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: dev-backend dev-frontend test build-index validate-models docker-build docker-run
2
+
3
+ dev-backend:
4
+ cd backend && python app.py
5
+
6
+ dev-frontend:
7
+ cd frontend && npm start
8
+
9
+ test:
10
+ cd backend && pytest
11
+
12
+ build-index:
13
+ cd backend && python scripts/build_sbert_index.py
14
+
15
+ validate-models:
16
+ cd backend && python scripts/validate_models.py
17
+
18
+ docker-build:
19
+ docker build -t quotify .
20
+
21
+ docker-run:
22
+ docker run --rm -p 7860:7860 --env QUOTIFY_ENABLE_CROSS_ENCODER=false quotify
23
+
README.md CHANGED
@@ -1,10 +1,284 @@
1
  ---
2
- title: Quotify Advance
3
- emoji: 📚
4
  colorFrom: green
5
- colorTo: red
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Quotify
3
+ emoji: 💬
4
  colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # Quotify
12
+
13
+ Quotify is an emotion-aware quote recommendation prototype. A user writes how they feel, the backend detects the likely emotion, and the app recommends quotes using one of three selectable NLP retrieval strategies.
14
+
15
+ The original coursework method is preserved as the baseline, and the upgraded version adds modern dense retrieval plus an optional retrieve-and-rerank pipeline suitable for an AI/NLP portfolio demo.
16
+
17
+ ## What Changed
18
+
19
+ - Refactored the Flask backend into modular strategy classes.
20
+ - Added `/health`, `/strategies`, `/get_quote`, and `/compare`.
21
+ - Preserved the legacy BERT + TF-IDF method as a selectable baseline.
22
+ - Added SentenceTransformer dense semantic search.
23
+ - Added optional Bi-Encoder retrieval + Cross-Encoder reranking.
24
+ - Added model/artifact fallback behavior so missing checkpoints do not crash the app.
25
+ - Updated the React UI with strategy switching, compare mode, score cards, loading states, and error states.
26
+ - Added Docker deployment for Hugging Face Spaces on port `7860`.
27
+
28
+ ## Recommendation Strategies
29
+
30
+ | Strategy ID | UI Label | Method | Best Use |
31
+ | --- | --- | --- | --- |
32
+ | `legacy_tfidf_bert` | Legacy BERT + TF-IDF | Fine-tuned BERT emotion classifier, TF-IDF cosine similarity, emotion boost | Preserves the original project method |
33
+ | `sbert_dense` | Dense Semantic Search | `sentence-transformers/all-MiniLM-L6-v2` quote embeddings, cosine similarity, emotion boost | Better meaning-based retrieval on CPU |
34
+ | `cross_encoder_rerank` | AI Reranker | Dense retrieval top K, `cross-encoder/ms-marco-MiniLM-L6-v2` reranking, emotion-aware final score | Higher quality reranking when CPU/memory allows |
35
+
36
+ ## Architecture
37
+
38
+ ```text
39
+ React UI
40
+ |-- strategy selector
41
+ |-- quote result cards
42
+ `-- compare-all mode
43
+ |
44
+ v
45
+ Flask API
46
+ |-- GET /health
47
+ |-- GET /strategies
48
+ |-- POST /get_quote
49
+ `-- POST /compare
50
+ |
51
+ v
52
+ Recommendation registry
53
+ |-- legacy_tfidf_bert
54
+ |-- sbert_dense
55
+ `-- cross_encoder_rerank
56
+ |
57
+ v
58
+ Data and models
59
+ |-- fine-tuned BERT checkpoint, optional
60
+ |-- preprocessed emotion dataset
61
+ |-- preprocessed quote dataset
62
+ `-- generated SBERT artifacts under backend/artifacts/
63
+ ```
64
+
65
+ ## Tech Stack
66
+
67
+ - Backend: Flask, gunicorn, PyTorch, Transformers, SentenceTransformers, scikit-learn, pandas, NumPy
68
+ - Frontend: React 18, Create React App, CSS, React Icons
69
+ - Deployment: Docker, Hugging Face Spaces
70
+ - Research assets: Jupyter notebooks and official PDF report in `LEGACY/`
71
+
72
+ ## Local Setup
73
+
74
+ ### Backend
75
+
76
+ ```bash
77
+ cd backend
78
+ python3 -m venv .venv
79
+ source .venv/bin/activate
80
+ pip install -r requirements.txt
81
+ python app.py
82
+ ```
83
+
84
+ The backend runs on:
85
+
86
+ ```text
87
+ http://127.0.0.1:7860
88
+ ```
89
+
90
+ For a lighter local run that avoids loading dense models:
91
+
92
+ ```bash
93
+ QUOTIFY_ENABLE_SBERT=false QUOTIFY_ENABLE_CROSS_ENCODER=false python app.py
94
+ ```
95
+
96
+ ### Frontend
97
+
98
+ ```bash
99
+ cd frontend
100
+ npm install
101
+ npm start
102
+ ```
103
+
104
+ The frontend runs on:
105
+
106
+ ```text
107
+ http://localhost:3000
108
+ ```
109
+
110
+ Create React App proxies API calls to the backend through `frontend/package.json`.
111
+
112
+ ## Model Checkpoint
113
+
114
+ The original fine-tuned BERT checkpoint is intentionally not committed. Download it from:
115
+
116
+ ```text
117
+ https://drive.google.com/file/d/1UCsaBpSQEWzD8kkK2Etcdw5r63pmj2yO/view?usp=drive_web
118
+ ```
119
+
120
+ Place it at:
121
+
122
+ ```text
123
+ backend/last_trained_model_checkpoint.pth
124
+ ```
125
+
126
+ If the checkpoint is missing, Quotify still starts and uses a clearly marked lightweight keyword fallback for emotion detection. `/health` reports whether the real checkpoint is loaded.
127
+
128
+ ## Artifact Generation
129
+
130
+ The dense semantic strategy builds quote embeddings on first startup when `sentence-transformers` is available. To build them manually:
131
+
132
+ ```bash
133
+ make build-index
134
+ ```
135
+
136
+ Generated files are written to `backend/artifacts/`:
137
+
138
+ ```text
139
+ quote_embeddings.npy
140
+ quote_metadata.csv
141
+ artifact_manifest.json
142
+ ```
143
+
144
+ These artifacts can be large, so they are ignored by Git by default.
145
+
146
+ ## Docker
147
+
148
+ Build and run locally:
149
+
150
+ ```bash
151
+ make docker-build
152
+ make docker-run
153
+ ```
154
+
155
+ Or:
156
+
157
+ ```bash
158
+ docker compose up --build
159
+ ```
160
+
161
+ The container serves the React build and Flask API from one process on:
162
+
163
+ ```text
164
+ http://localhost:7860
165
+ ```
166
+
167
+ ## Hugging Face Spaces Deployment
168
+
169
+ 1. Create a new Hugging Face Space.
170
+ 2. Choose Docker as the SDK.
171
+ 3. Push this repository to the Space.
172
+ 4. Keep the default app port as `7860`.
173
+ 5. Optional: add repository secrets or Space variables:
174
+ - `QUOTIFY_DEFAULT_STRATEGY=sbert_dense`
175
+ - `QUOTIFY_ENABLE_CROSS_ENCODER=false`
176
+ - `QUOTIFY_EMOTION_BOOST=0.08`
177
+ - `QUOTIFY_TOP_K_RETRIEVE=50`
178
+ - `QUOTIFY_MODEL_CHECKPOINT_PATH=last_trained_model_checkpoint.pth`
179
+ 6. If you do not include the BERT checkpoint, the app still runs with fallback emotion detection.
180
+
181
+ For free CPU Spaces, keep `QUOTIFY_ENABLE_CROSS_ENCODER=false` unless startup time and memory are acceptable.
182
+
183
+ ## API Examples
184
+
185
+ ### Health
186
+
187
+ ```bash
188
+ curl http://127.0.0.1:7860/health
189
+ ```
190
+
191
+ ### Strategies
192
+
193
+ ```bash
194
+ curl http://127.0.0.1:7860/strategies
195
+ ```
196
+
197
+ ### Get One Quote
198
+
199
+ ```bash
200
+ curl -X POST http://127.0.0.1:7860/get_quote \
201
+ -H "Content-Type: application/json" \
202
+ -d '{"inputText":"I feel nervous about tomorrow","strategy":"sbert_dense","topK":5}'
203
+ ```
204
+
205
+ Example response:
206
+
207
+ ```json
208
+ {
209
+ "quote": "Returned quote text",
210
+ "author": "Quote author",
211
+ "emotion": "fear",
212
+ "quote_emotion": "fear",
213
+ "strategy": "sbert_dense",
214
+ "strategy_label": "Dense Semantic Search",
215
+ "scores": {
216
+ "semantic_score": 0.721,
217
+ "cross_encoder_score": null,
218
+ "emotion_boost": 0.08,
219
+ "final_score": 0.801
220
+ },
221
+ "alternatives": []
222
+ }
223
+ ```
224
+
225
+ ### Compare Methods
226
+
227
+ ```bash
228
+ curl -X POST http://127.0.0.1:7860/compare \
229
+ -H "Content-Type: application/json" \
230
+ -d '{"inputText":"I feel nervous about tomorrow","topK":5}'
231
+ ```
232
+
233
+ ## Developer Commands
234
+
235
+ ```bash
236
+ make dev-backend
237
+ make dev-frontend
238
+ make test
239
+ make build-index
240
+ make validate-models
241
+ make docker-build
242
+ make docker-run
243
+ ```
244
+
245
+ ## Environment Variables
246
+
247
+ | Variable | Default | Purpose |
248
+ | --- | --- | --- |
249
+ | `PORT` | `7860` | Flask/gunicorn port |
250
+ | `QUOTIFY_DEFAULT_STRATEGY` | `sbert_dense` | Preferred recommendation strategy |
251
+ | `QUOTIFY_ENABLE_SBERT` | `true` | Enable dense retrieval loading |
252
+ | `QUOTIFY_ENABLE_CROSS_ENCODER` | `true` locally, `false` in Docker | Enable optional reranker |
253
+ | `QUOTIFY_EMOTION_BOOST` | `0.08` | Score boost for emotion-aligned quotes |
254
+ | `QUOTIFY_TOP_K_RETRIEVE` | `50` | Dense candidates passed to reranker |
255
+ | `QUOTIFY_MODEL_CHECKPOINT_PATH` | `last_trained_model_checkpoint.pth` | Fine-tuned BERT checkpoint path |
256
+
257
+ ## Limitations
258
+
259
+ - Emotion detection is limited to `anger`, `fear`, and `joy`.
260
+ - The quote emotion labels are generated by the project model, so they may contain prediction errors.
261
+ - Dense retrieval may need to download open-source model files from Hugging Face on first run.
262
+ - Cross-Encoder reranking is slower and may be disabled on constrained CPU deployments.
263
+ - The fallback emotion classifier is only for local/demo resilience, not a replacement for the fine-tuned model.
264
+
265
+ ## Ethical Note
266
+
267
+ Quotify recommends quotes for reflection and inspiration. It is not a mental health diagnosis tool, therapy tool, crisis intervention system, or substitute for professional support.
268
+
269
+ ## Legacy Project Reference
270
+
271
+ The official report and original research notebooks remain under `LEGACY/`:
272
+
273
+ ```text
274
+ LEGACY/Machine Learning-AOL.pdf
275
+ LEGACY/EDAandPreprocess.ipynb
276
+ LEGACY/NaiveBayes_ML_AOL_3_0.ipynb
277
+ LEGACY/LogisticRegression_ML_AOL_2_0.ipynb
278
+ LEGACY/BERT_ML_AOL_5_0.ipynb
279
+ LEGACY/QuotesSelector.ipynb
280
+ ```
281
+
282
+ ## License
283
+
284
+ See [`LICENSE`](LICENSE).
backend/(Preprocessed)Emotion_classify_Data(Labeled).csv ADDED
The diff for this file is too large to render. See raw diff
 
backend/(Preprocessed)quotes.csv ADDED
The diff for this file is too large to render. See raw diff
 
backend/.python-version ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ 3.11
2
+
backend/app.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+
3
+ from flask import Flask, jsonify, request, send_from_directory
4
+ from flask_cors import CORS
5
+
6
+ from src.config import AppConfig, config
7
+ from src.data_loader import load_data
8
+ from src.emotion_classifier import EmotionClassifier
9
+ from src.health import artifact_status
10
+ from src.recommenders import CrossEncoderRerankRecommender, LegacyTfidfRecommender, SbertDenseRecommender
11
+
12
+
13
+ def create_app(app_config: AppConfig = config, quotes_override=None) -> Flask:
14
+ static_folder = str(app_config.frontend_build_dir) if app_config.frontend_build_dir.exists() else None
15
+ app = Flask(__name__, static_folder=static_folder, static_url_path="")
16
+
17
+ if app_config.cors_origins:
18
+ CORS(app, origins=[origin.strip() for origin in app_config.cors_origins.split(",") if origin.strip()])
19
+ else:
20
+ CORS(app, origins=["http://localhost:3000", "http://127.0.0.1:3000"])
21
+
22
+ loaded_data = load_data(app_config, quotes_override=quotes_override)
23
+ emotion_classifier = EmotionClassifier(app_config, loaded_data.emotion_data["Emotion"].unique())
24
+ legacy = LegacyTfidfRecommender(app_config, loaded_data.quotes, emotion_classifier)
25
+ dense = SbertDenseRecommender(app_config, loaded_data.quotes, emotion_classifier)
26
+ reranker = CrossEncoderRerankRecommender(app_config, loaded_data.quotes, emotion_classifier, dense)
27
+ recommenders = {
28
+ legacy.strategy_id: legacy,
29
+ dense.strategy_id: dense,
30
+ reranker.strategy_id: reranker,
31
+ }
32
+
33
+ app.extensions["quotify"] = {
34
+ "config": app_config,
35
+ "data": loaded_data,
36
+ "emotion_classifier": emotion_classifier,
37
+ "recommenders": recommenders,
38
+ }
39
+
40
+ def _available_recommenders():
41
+ return {key: value for key, value in recommenders.items() if value.available}
42
+
43
+ def _resolve_strategy(strategy_id):
44
+ requested = strategy_id or app_config.default_strategy
45
+ if requested in recommenders and recommenders[requested].available:
46
+ return recommenders[requested]
47
+ available = _available_recommenders()
48
+ if available:
49
+ return next(iter(available.values()))
50
+ raise RuntimeError("No recommendation strategies are available.")
51
+
52
+ @app.get("/health")
53
+ def health():
54
+ available = _available_recommenders()
55
+ return jsonify(
56
+ {
57
+ "status": "ok" if available else "degraded",
58
+ "app": app_config.app_name,
59
+ "version": app_config.version,
60
+ "available_strategies": list(available.keys()),
61
+ "strategies": [strategy.info().to_dict() for strategy in recommenders.values()],
62
+ "artifacts": artifact_status(app_config, dense),
63
+ "models": {
64
+ "emotion_classifier": emotion_classifier.status.to_dict(),
65
+ "sentence_transformer": {
66
+ "available": dense.available,
67
+ "model": app_config.sbert_model_name,
68
+ "reason": dense.unavailable_reason or None,
69
+ },
70
+ "cross_encoder": {
71
+ "available": reranker.available,
72
+ "enabled": app_config.enable_cross_encoder,
73
+ "model": app_config.cross_encoder_model_name,
74
+ "reason": reranker.unavailable_reason or None,
75
+ },
76
+ },
77
+ }
78
+ )
79
+
80
+ @app.get("/strategies")
81
+ def strategies():
82
+ return jsonify([strategy.info().to_dict() for strategy in recommenders.values()])
83
+
84
+ @app.post("/get_quote")
85
+ def get_quote():
86
+ payload = request.get_json(silent=True) or {}
87
+ input_text = str(payload.get("inputText", "")).strip()
88
+ if not input_text:
89
+ return jsonify({"error": "inputText is required."}), 400
90
+
91
+ top_k = int(payload.get("topK") or 5)
92
+ strategy = _resolve_strategy(payload.get("strategy"))
93
+ try:
94
+ return jsonify(strategy.recommend(input_text, top_k=top_k).to_dict())
95
+ except Exception as exc:
96
+ return jsonify({"error": str(exc), "strategy": strategy.strategy_id}), 503
97
+
98
+ @app.post("/compare")
99
+ def compare():
100
+ payload = request.get_json(silent=True) or {}
101
+ input_text = str(payload.get("inputText", "")).strip()
102
+ if not input_text:
103
+ return jsonify({"error": "inputText is required."}), 400
104
+
105
+ top_k = int(payload.get("topK") or 5)
106
+ results = []
107
+ errors = []
108
+ for strategy in recommenders.values():
109
+ if not strategy.available:
110
+ errors.append({"strategy": strategy.strategy_id, "error": strategy.unavailable_reason})
111
+ continue
112
+ try:
113
+ results.append(strategy.recommend(input_text, top_k=top_k).to_dict())
114
+ except Exception as exc:
115
+ errors.append({"strategy": strategy.strategy_id, "error": str(exc)})
116
+ return jsonify({"inputText": input_text, "results": results, "errors": errors})
117
+
118
+ @app.get("/")
119
+ def index():
120
+ if app.static_folder:
121
+ return send_from_directory(app.static_folder, "index.html")
122
+ return jsonify({"app": app_config.app_name, "message": "Frontend build is not available."})
123
+
124
+ @app.get("/<path:path>")
125
+ def static_proxy(path):
126
+ if app.static_folder:
127
+ candidate = Path(app.static_folder) / path
128
+ if candidate.exists():
129
+ return send_from_directory(app.static_folder, path)
130
+ return send_from_directory(app.static_folder, "index.html")
131
+ return jsonify({"error": "Not found"}), 404
132
+
133
+ return app
134
+
135
+
136
+ app = create_app()
137
+
138
+
139
+ if __name__ == "__main__":
140
+ app.run(host="0.0.0.0", port=config.port, debug=False)
backend/artifacts/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
backend/model.ipynb ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 80,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "import torch\n",
10
+ "from transformers import BertTokenizer, BertForSequenceClassification, AdamW\n",
11
+ "from torch.utils.data import DataLoader, TensorDataset\n",
12
+ "import pandas as pd\n",
13
+ "from sklearn.preprocessing import LabelEncoder\n",
14
+ "\n",
15
+ "import pandas as pd\n",
16
+ "import numpy as np\n",
17
+ "\n",
18
+ "import random\n",
19
+ "\n",
20
+ "import matplotlib.pyplot as plt\n",
21
+ "\n",
22
+ "import pandas as pd\n",
23
+ "from sklearn.feature_extraction.text import TfidfVectorizer\n",
24
+ "from sklearn.metrics.pairwise import cosine_similarity\n",
25
+ "import numpy as np"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "markdown",
30
+ "metadata": {},
31
+ "source": [
32
+ "### LOADUP Trained Model BERT"
33
+ ]
34
+ },
35
+ {
36
+ "cell_type": "code",
37
+ "execution_count": 81,
38
+ "metadata": {},
39
+ "outputs": [
40
+ {
41
+ "name": "stderr",
42
+ "output_type": "stream",
43
+ "text": [
44
+ "Some weights of the model checkpoint at bert-base-uncased were not used when initializing BertForSequenceClassification: ['cls.predictions.transform.dense.weight', 'cls.seq_relationship.weight', 'cls.predictions.decoder.weight', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.dense.bias', 'cls.predictions.bias', 'cls.seq_relationship.bias', 'cls.predictions.transform.LayerNorm.bias']\n",
45
+ "- This IS expected if you are initializing BertForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n",
46
+ "- This IS NOT expected if you are initializing BertForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n",
47
+ "Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-uncased and are newly initialized: ['classifier.weight', 'classifier.bias']\n",
48
+ "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n"
49
+ ]
50
+ }
51
+ ],
52
+ "source": [
53
+ "dataset = pd.read_csv('./(Preprocessed)Emotion_classify_Data(Labeled).csv')\n",
54
+ "\n",
55
+ "# Shuffle the dataset\n",
56
+ "dataset = dataset.sample(frac=1, random_state=42).reset_index(drop=True)\n",
57
+ "\n",
58
+ "# Initialize BERT tokenizer\n",
59
+ "tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')\n",
60
+ "\n",
61
+ "# Load pre-trained BERT model for sequence classification\n",
62
+ "model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=dataset['Emotion'].nunique())\n",
63
+ "\n",
64
+ "# Fine-tune BERT on your dataset\n",
65
+ "learning_rate = 2e-5\n",
66
+ "optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)\n",
67
+ "loss_fn = torch.nn.CrossEntropyLoss()\n",
68
+ "\n",
69
+ "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')"
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "code",
74
+ "execution_count": 82,
75
+ "metadata": {},
76
+ "outputs": [
77
+ {
78
+ "data": {
79
+ "text/html": [
80
+ "<style>#sk-container-id-6 {color: black;background-color: white;}#sk-container-id-6 pre{padding: 0;}#sk-container-id-6 div.sk-toggleable {background-color: white;}#sk-container-id-6 label.sk-toggleable__label {cursor: pointer;display: block;width: 100%;margin-bottom: 0;padding: 0.3em;box-sizing: border-box;text-align: center;}#sk-container-id-6 label.sk-toggleable__label-arrow:before {content: \"▸\";float: left;margin-right: 0.25em;color: #696969;}#sk-container-id-6 label.sk-toggleable__label-arrow:hover:before {color: black;}#sk-container-id-6 div.sk-estimator:hover label.sk-toggleable__label-arrow:before {color: black;}#sk-container-id-6 div.sk-toggleable__content {max-height: 0;max-width: 0;overflow: hidden;text-align: left;background-color: #f0f8ff;}#sk-container-id-6 div.sk-toggleable__content pre {margin: 0.2em;color: black;border-radius: 0.25em;background-color: #f0f8ff;}#sk-container-id-6 input.sk-toggleable__control:checked~div.sk-toggleable__content {max-height: 200px;max-width: 100%;overflow: auto;}#sk-container-id-6 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {content: \"▾\";}#sk-container-id-6 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-6 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-6 input.sk-hidden--visually {border: 0;clip: rect(1px 1px 1px 1px);clip: rect(1px, 1px, 1px, 1px);height: 1px;margin: -1px;overflow: hidden;padding: 0;position: absolute;width: 1px;}#sk-container-id-6 div.sk-estimator {font-family: monospace;background-color: #f0f8ff;border: 1px dotted black;border-radius: 0.25em;box-sizing: border-box;margin-bottom: 0.5em;}#sk-container-id-6 div.sk-estimator:hover {background-color: #d4ebff;}#sk-container-id-6 div.sk-parallel-item::after {content: \"\";width: 100%;border-bottom: 1px solid gray;flex-grow: 1;}#sk-container-id-6 div.sk-label:hover label.sk-toggleable__label {background-color: #d4ebff;}#sk-container-id-6 div.sk-serial::before {content: \"\";position: absolute;border-left: 1px solid gray;box-sizing: border-box;top: 0;bottom: 0;left: 50%;z-index: 0;}#sk-container-id-6 div.sk-serial {display: flex;flex-direction: column;align-items: center;background-color: white;padding-right: 0.2em;padding-left: 0.2em;position: relative;}#sk-container-id-6 div.sk-item {position: relative;z-index: 1;}#sk-container-id-6 div.sk-parallel {display: flex;align-items: stretch;justify-content: center;background-color: white;position: relative;}#sk-container-id-6 div.sk-item::before, #sk-container-id-6 div.sk-parallel-item::before {content: \"\";position: absolute;border-left: 1px solid gray;box-sizing: border-box;top: 0;bottom: 0;left: 50%;z-index: -1;}#sk-container-id-6 div.sk-parallel-item {display: flex;flex-direction: column;z-index: 1;position: relative;background-color: white;}#sk-container-id-6 div.sk-parallel-item:first-child::after {align-self: flex-end;width: 50%;}#sk-container-id-6 div.sk-parallel-item:last-child::after {align-self: flex-start;width: 50%;}#sk-container-id-6 div.sk-parallel-item:only-child::after {width: 0;}#sk-container-id-6 div.sk-dashed-wrapped {border: 1px dashed gray;margin: 0 0.4em 0.5em 0.4em;box-sizing: border-box;padding-bottom: 0.4em;background-color: white;}#sk-container-id-6 div.sk-label label {font-family: monospace;font-weight: bold;display: inline-block;line-height: 1.2em;}#sk-container-id-6 div.sk-label-container {text-align: center;}#sk-container-id-6 div.sk-container {/* jupyter's `normalize.less` sets `[hidden] { display: none; }` but bootstrap.min.css set `[hidden] { display: none !important; }` so we also need the `!important` here to be able to override the default hidden behavior on the sphinx rendered scikit-learn.org. See: https://github.com/scikit-learn/scikit-learn/issues/21755 */display: inline-block !important;position: relative;}#sk-container-id-6 div.sk-text-repr-fallback {display: none;}</style><div id=\"sk-container-id-6\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>LabelEncoder()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-6\" type=\"checkbox\" checked><label for=\"sk-estimator-id-6\" class=\"sk-toggleable__label sk-toggleable__label-arrow\">LabelEncoder</label><div class=\"sk-toggleable__content\"><pre>LabelEncoder()</pre></div></div></div></div></div>"
81
+ ],
82
+ "text/plain": [
83
+ "LabelEncoder()"
84
+ ]
85
+ },
86
+ "execution_count": 82,
87
+ "metadata": {},
88
+ "output_type": "execute_result"
89
+ }
90
+ ],
91
+ "source": [
92
+ "emotion_labels = dataset['Emotion']\n",
93
+ "\n",
94
+ "# Instantiate the label encoder\n",
95
+ "label_encoder = LabelEncoder()\n",
96
+ "\n",
97
+ "# Fit the label encoder to the encoded emotion labels\n",
98
+ "label_encoder.fit(emotion_labels)"
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "code",
103
+ "execution_count": 83,
104
+ "metadata": {},
105
+ "outputs": [
106
+ {
107
+ "name": "stderr",
108
+ "output_type": "stream",
109
+ "text": [
110
+ "Some weights of the model checkpoint at bert-base-uncased were not used when initializing BertForSequenceClassification: ['cls.predictions.transform.dense.weight', 'cls.seq_relationship.weight', 'cls.predictions.decoder.weight', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.dense.bias', 'cls.predictions.bias', 'cls.seq_relationship.bias', 'cls.predictions.transform.LayerNorm.bias']\n",
111
+ "- This IS expected if you are initializing BertForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n",
112
+ "- This IS NOT expected if you are initializing BertForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n",
113
+ "Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-uncased and are newly initialized: ['classifier.weight', 'classifier.bias']\n",
114
+ "You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n"
115
+ ]
116
+ },
117
+ {
118
+ "name": "stdout",
119
+ "output_type": "stream",
120
+ "text": [
121
+ "Model and optimizer states loaded successfully.\n",
122
+ "Epoch: 7, Best Validation Loss: 0.09719424509431089, Early Stop Count: 5\n"
123
+ ]
124
+ },
125
+ {
126
+ "name": "stderr",
127
+ "output_type": "stream",
128
+ "text": [
129
+ "d:\\App\\Programming\\python\\Anaconda\\lib\\site-packages\\transformers\\optimization.py:306: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning\n",
130
+ " warnings.warn(\n"
131
+ ]
132
+ }
133
+ ],
134
+ "source": [
135
+ "# Load the checkpoint\n",
136
+ "checkpoint = torch.load('last_trained_model_checkpoint.pth', map_location=torch.device('cpu'))\n",
137
+ "\n",
138
+ "# Initialize the model and optimizer\n",
139
+ "model = BertForSequenceClassification.from_pretrained('bert-base-uncased', num_labels=3)\n",
140
+ "optimizer = AdamW(model.parameters(), lr=5e-5)\n",
141
+ "\n",
142
+ "# Load model and optimizer states with strict=False to ignore missing keys\n",
143
+ "model.load_state_dict(checkpoint['model_state_dict'], strict=False)\n",
144
+ "optimizer.load_state_dict(checkpoint['optimizer_state_dict'])\n",
145
+ "\n",
146
+ "# Update other necessary variables\n",
147
+ "epoch = checkpoint['epoch']\n",
148
+ "best_val_loss = checkpoint['best_val_loss']\n",
149
+ "early_stop_count = checkpoint['early_stop_count']\n",
150
+ "\n",
151
+ "print(f\"Model and optimizer states loaded successfully.\")\n",
152
+ "print(f\"Epoch: {epoch}, Best Validation Loss: {best_val_loss}, Early Stop Count: {early_stop_count}\")"
153
+ ]
154
+ },
155
+ {
156
+ "cell_type": "code",
157
+ "execution_count": 84,
158
+ "metadata": {},
159
+ "outputs": [
160
+ {
161
+ "name": "stdout",
162
+ "output_type": "stream",
163
+ "text": [
164
+ "Predicted Emotion: joy\n",
165
+ "Predicted Label: tensor([[-2.6676, -2.7545, 6.4300]])\n"
166
+ ]
167
+ }
168
+ ],
169
+ "source": [
170
+ "def predict_emotion(text, label_encoder):\n",
171
+ " # Tokenize the text using the same tokenizer used during training\n",
172
+ " inputs = tokenizer.encode_plus(text, add_special_tokens=True, max_length=128, padding='max_length', truncation=True, return_tensors='pt')\n",
173
+ " input_ids = inputs['input_ids'].to(device) # Move input to the same device as the model\n",
174
+ " attention_mask = inputs['attention_mask'].to(device) # Move attention mask to the same device as the model\n",
175
+ "\n",
176
+ " # Make prediction\n",
177
+ " with torch.no_grad():\n",
178
+ " model.eval() # Set model to evaluation mode\n",
179
+ " outputs = model(input_ids, attention_mask=attention_mask)\n",
180
+ " logits = outputs.logits\n",
181
+ "\n",
182
+ " # Get predicted label\n",
183
+ " predicted_label = torch.argmax(logits, dim=1).item()\n",
184
+ "\n",
185
+ " # Decode the predicted label using label_encoder\n",
186
+ " predicted_emotion = label_encoder.inverse_transform([predicted_label])[0]\n",
187
+ "\n",
188
+ " return predicted_emotion, logits\n",
189
+ "\n",
190
+ "# Example usage\n",
191
+ "text = \"Super happy today\"\n",
192
+ "predicted_emotion, x = predict_emotion(text, label_encoder)\n",
193
+ "print(f'Predicted Emotion: {predicted_emotion}')\n",
194
+ "print(f'Predicted Label: {x}')"
195
+ ]
196
+ },
197
+ {
198
+ "cell_type": "markdown",
199
+ "metadata": {},
200
+ "source": [
201
+ "### Read the Quote"
202
+ ]
203
+ },
204
+ {
205
+ "cell_type": "code",
206
+ "execution_count": 85,
207
+ "metadata": {},
208
+ "outputs": [
209
+ {
210
+ "data": {
211
+ "text/html": [
212
+ "<div>\n",
213
+ "<style scoped>\n",
214
+ " .dataframe tbody tr th:only-of-type {\n",
215
+ " vertical-align: middle;\n",
216
+ " }\n",
217
+ "\n",
218
+ " .dataframe tbody tr th {\n",
219
+ " vertical-align: top;\n",
220
+ " }\n",
221
+ "\n",
222
+ " .dataframe thead th {\n",
223
+ " text-align: right;\n",
224
+ " }\n",
225
+ "</style>\n",
226
+ "<table border=\"1\" class=\"dataframe\">\n",
227
+ " <thead>\n",
228
+ " <tr style=\"text-align: right;\">\n",
229
+ " <th></th>\n",
230
+ " <th>Quote</th>\n",
231
+ " <th>Author</th>\n",
232
+ " <th>emotion</th>\n",
233
+ " </tr>\n",
234
+ " </thead>\n",
235
+ " <tbody>\n",
236
+ " <tr>\n",
237
+ " <th>0</th>\n",
238
+ " <td>Don't cry because it's over, smile because it ...</td>\n",
239
+ " <td>Dr. Seuss</td>\n",
240
+ " <td>joy</td>\n",
241
+ " </tr>\n",
242
+ " <tr>\n",
243
+ " <th>1</th>\n",
244
+ " <td>I'm selfish, impatient and a little insecure. ...</td>\n",
245
+ " <td>Marilyn Monroe</td>\n",
246
+ " <td>anger</td>\n",
247
+ " </tr>\n",
248
+ " <tr>\n",
249
+ " <th>2</th>\n",
250
+ " <td>Be yourself; everyone else is already taken.</td>\n",
251
+ " <td>Oscar Wilde</td>\n",
252
+ " <td>anger</td>\n",
253
+ " </tr>\n",
254
+ " <tr>\n",
255
+ " <th>3</th>\n",
256
+ " <td>Two things are infinite: the universe and huma...</td>\n",
257
+ " <td>Albert Einstein</td>\n",
258
+ " <td>joy</td>\n",
259
+ " </tr>\n",
260
+ " <tr>\n",
261
+ " <th>4</th>\n",
262
+ " <td>Be who you are and say what you feel, because ...</td>\n",
263
+ " <td>Bernard M. Baruch</td>\n",
264
+ " <td>joy</td>\n",
265
+ " </tr>\n",
266
+ " <tr>\n",
267
+ " <th>...</th>\n",
268
+ " <td>...</td>\n",
269
+ " <td>...</td>\n",
270
+ " <td>...</td>\n",
271
+ " </tr>\n",
272
+ " <tr>\n",
273
+ " <th>36932</th>\n",
274
+ " <td>In Buddhism, they say attachment to anything o...</td>\n",
275
+ " <td>Jason Mraz</td>\n",
276
+ " <td>joy</td>\n",
277
+ " </tr>\n",
278
+ " <tr>\n",
279
+ " <th>36933</th>\n",
280
+ " <td>I love British humor. It's just so - surreal.</td>\n",
281
+ " <td>Beck</td>\n",
282
+ " <td>fear</td>\n",
283
+ " </tr>\n",
284
+ " <tr>\n",
285
+ " <th>36934</th>\n",
286
+ " <td>I've got a sense of humor. I'm a funny guy.</td>\n",
287
+ " <td>Daryl Hall</td>\n",
288
+ " <td>joy</td>\n",
289
+ " </tr>\n",
290
+ " <tr>\n",
291
+ " <th>36935</th>\n",
292
+ " <td>Humor is such a wonderful thing, helping you r...</td>\n",
293
+ " <td>Lynda Barry</td>\n",
294
+ " <td>joy</td>\n",
295
+ " </tr>\n",
296
+ " <tr>\n",
297
+ " <th>36936</th>\n",
298
+ " <td>Life Is Full of Obstacles, Stumble Upon !!</td>\n",
299
+ " <td>Pratik Shelar</td>\n",
300
+ " <td>fear</td>\n",
301
+ " </tr>\n",
302
+ " </tbody>\n",
303
+ "</table>\n",
304
+ "<p>36937 rows × 3 columns</p>\n",
305
+ "</div>"
306
+ ],
307
+ "text/plain": [
308
+ " Quote Author \\\n",
309
+ "0 Don't cry because it's over, smile because it ... Dr. Seuss \n",
310
+ "1 I'm selfish, impatient and a little insecure. ... Marilyn Monroe \n",
311
+ "2 Be yourself; everyone else is already taken. Oscar Wilde \n",
312
+ "3 Two things are infinite: the universe and huma... Albert Einstein \n",
313
+ "4 Be who you are and say what you feel, because ... Bernard M. Baruch \n",
314
+ "... ... ... \n",
315
+ "36932 In Buddhism, they say attachment to anything o... Jason Mraz \n",
316
+ "36933 I love British humor. It's just so - surreal. Beck \n",
317
+ "36934 I've got a sense of humor. I'm a funny guy. Daryl Hall \n",
318
+ "36935 Humor is such a wonderful thing, helping you r... Lynda Barry \n",
319
+ "36936 Life Is Full of Obstacles, Stumble Upon !! Pratik Shelar \n",
320
+ "\n",
321
+ " emotion \n",
322
+ "0 joy \n",
323
+ "1 anger \n",
324
+ "2 anger \n",
325
+ "3 joy \n",
326
+ "4 joy \n",
327
+ "... ... \n",
328
+ "36932 joy \n",
329
+ "36933 fear \n",
330
+ "36934 joy \n",
331
+ "36935 joy \n",
332
+ "36936 fear \n",
333
+ "\n",
334
+ "[36937 rows x 3 columns]"
335
+ ]
336
+ },
337
+ "execution_count": 85,
338
+ "metadata": {},
339
+ "output_type": "execute_result"
340
+ }
341
+ ],
342
+ "source": [
343
+ "quotesDF = pd.read_csv('./(Preprocessed)quotes.csv')\n",
344
+ "quotesDF"
345
+ ]
346
+ },
347
+ {
348
+ "cell_type": "code",
349
+ "execution_count": 86,
350
+ "metadata": {},
351
+ "outputs": [],
352
+ "source": [
353
+ "import numpy as np\n",
354
+ "from sklearn.feature_extraction.text import TfidfVectorizer\n",
355
+ "from sklearn.metrics.pairwise import cosine_similarity\n",
356
+ "\n",
357
+ "def find_most_similar_quote(input_sentence, quotes_df, input_emotion, emotion_weight=1.1):\n",
358
+ " # Combine all quotes into a list\n",
359
+ " quotes = quotes_df['Quote'].tolist()\n",
360
+ " authors = quotes_df['Author'].tolist()\n",
361
+ " emotions = quotes_df['emotion'].tolist()\n",
362
+ "\n",
363
+ " # Initialize the vectorizer and fit it on all quotes\n",
364
+ " vectorizer = TfidfVectorizer()\n",
365
+ " tfidf_matrix = vectorizer.fit_transform(quotes)\n",
366
+ "\n",
367
+ " # Vectorize the input sentence\n",
368
+ " input_vec = vectorizer.transform([input_sentence])\n",
369
+ "\n",
370
+ " # Calculate cosine similarity between the input sentence and all quotes\n",
371
+ " cosine_similarities = cosine_similarity(input_vec, tfidf_matrix).flatten()\n",
372
+ "\n",
373
+ " # Apply emotion-based weighting\n",
374
+ " emotion_weights = np.array([emotion_weight if emotion == input_emotion else 1.0 for emotion in emotions])\n",
375
+ " weighted_similarities = cosine_similarities * emotion_weights\n",
376
+ "\n",
377
+ " # Get the indices of the top 10 quotes with the highest similarity scores\n",
378
+ " top_10_indices = np.argsort(weighted_similarities)[-5:]\n",
379
+ "\n",
380
+ " # Randomly select one quote from the top 10\n",
381
+ " selected_index = random.choice(top_10_indices)\n",
382
+ "\n",
383
+ " # Return the most similar quote and its author\n",
384
+ " return quotes[selected_index], authors[selected_index], emotions[selected_index]"
385
+ ]
386
+ },
387
+ {
388
+ "cell_type": "code",
389
+ "execution_count": 87,
390
+ "metadata": {},
391
+ "outputs": [
392
+ {
393
+ "name": "stdout",
394
+ "output_type": "stream",
395
+ "text": [
396
+ "Input sentence: What should I do with my life?\n",
397
+ "Detected emotion: anger\n",
398
+ "Emotion score: tensor([[ 1.2938, 0.3162, -1.3738]])\n",
399
+ "Most similar quote: Don't let what you cannot do interfere with what you can do.\n",
400
+ "Author: John Wooden\n",
401
+ "Quotes Emotion: anger\n"
402
+ ]
403
+ }
404
+ ],
405
+ "source": [
406
+ "input_sentence = \"What should I do with my life?\"\n",
407
+ "detected_emotion, emotion_score = predict_emotion(input_sentence, label_encoder)\n",
408
+ "most_similar_quote, author, emotionQuotes = find_most_similar_quote(input_sentence, quotesDF, detected_emotion)\n",
409
+ "print('Input sentence: ', input_sentence)\n",
410
+ "print(\"Detected emotion:\", detected_emotion)\n",
411
+ "print(\"Emotion score:\", emotion_score)\n",
412
+ "print(\"Most similar quote:\", most_similar_quote)\n",
413
+ "print(\"Author: \", author)\n",
414
+ "print(\"Quotes Emotion: \", emotionQuotes)"
415
+ ]
416
+ }
417
+ ],
418
+ "metadata": {
419
+ "kernelspec": {
420
+ "display_name": "base",
421
+ "language": "python",
422
+ "name": "python3"
423
+ },
424
+ "language_info": {
425
+ "codemirror_mode": {
426
+ "name": "ipython",
427
+ "version": 3
428
+ },
429
+ "file_extension": ".py",
430
+ "mimetype": "text/x-python",
431
+ "name": "python",
432
+ "nbconvert_exporter": "python",
433
+ "pygments_lexer": "ipython3",
434
+ "version": "3.10.9"
435
+ }
436
+ },
437
+ "nbformat": 4,
438
+ "nbformat_minor": 2
439
+ }
backend/requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask>=3.0,<4.0
2
+ flask-cors>=4.0,<6.0
3
+ gunicorn>=21.2,<23.0
4
+ torch>=2.2,<3.0
5
+ transformers>=4.40,<5.0
6
+ sentence-transformers>=3.0,<4.0
7
+ scikit-learn>=1.4,<2.0
8
+ pandas>=2.0,<3.0
9
+ numpy>=1.26,<3.0
10
+ pytest>=8.0,<9.0
11
+
backend/runtime.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ python-3.11
2
+
backend/scripts/build_sbert_index.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Build and cache SentenceTransformer quote embeddings."""
3
+
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ BACKEND_DIR = Path(__file__).resolve().parents[1]
8
+ sys.path.insert(0, str(BACKEND_DIR))
9
+
10
+ from src.config import config
11
+ from src.data_loader import load_data
12
+ from src.emotion_classifier import EmotionClassifier
13
+ from src.recommenders.sbert_dense import SbertDenseRecommender
14
+
15
+
16
+ def main() -> int:
17
+ loaded = load_data(config)
18
+ classifier = EmotionClassifier(config, loaded.emotion_data["Emotion"].unique())
19
+ recommender = SbertDenseRecommender(config, loaded.quotes, classifier)
20
+ if not recommender.available:
21
+ print(f"Could not build dense index: {recommender.unavailable_reason}")
22
+ return 1
23
+ print(f"Built dense index for {len(loaded.quotes)} quotes.")
24
+ print(f"Artifacts directory: {config.artifacts_dir}")
25
+ return 0
26
+
27
+
28
+ if __name__ == "__main__":
29
+ raise SystemExit(main())
30
+
backend/scripts/validate_models.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Validate model and strategy availability without starting a server."""
3
+
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ BACKEND_DIR = Path(__file__).resolve().parents[1]
8
+ sys.path.insert(0, str(BACKEND_DIR))
9
+
10
+ from app import create_app
11
+ from src.config import config
12
+
13
+
14
+ def main() -> int:
15
+ app = create_app(config)
16
+ state = app.extensions["quotify"]
17
+ print(f"{config.app_name} {config.version}")
18
+ print("Strategies:")
19
+ for strategy in state["recommenders"].values():
20
+ status = "available" if strategy.available else f"unavailable - {strategy.unavailable_reason}"
21
+ print(f"- {strategy.strategy_id}: {status}")
22
+ print("Emotion classifier:")
23
+ print(state["emotion_classifier"].status.to_dict())
24
+ return 0
25
+
26
+
27
+ if __name__ == "__main__":
28
+ raise SystemExit(main())
29
+
backend/src/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ """Quotify backend package."""
2
+
backend/src/config.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+
6
+ def _env_bool(name: str, default: bool) -> bool:
7
+ value = os.getenv(name)
8
+ if value is None:
9
+ return default
10
+ return value.strip().lower() in {"1", "true", "yes", "on"}
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class AppConfig:
15
+ app_name: str = "Quotify"
16
+ version: str = "2.0.0"
17
+ root_dir: Path = Path(__file__).resolve().parents[2]
18
+ backend_dir: Path = Path(__file__).resolve().parents[1]
19
+ default_strategy: str = os.getenv("QUOTIFY_DEFAULT_STRATEGY", "sbert_dense")
20
+ enable_sbert: bool = _env_bool("QUOTIFY_ENABLE_SBERT", True)
21
+ enable_cross_encoder: bool = _env_bool("QUOTIFY_ENABLE_CROSS_ENCODER", True)
22
+ emotion_boost: float = float(os.getenv("QUOTIFY_EMOTION_BOOST", "0.08"))
23
+ top_k_retrieve: int = int(os.getenv("QUOTIFY_TOP_K_RETRIEVE", "50"))
24
+ model_checkpoint_path: Path = Path(
25
+ os.getenv("QUOTIFY_MODEL_CHECKPOINT_PATH", "last_trained_model_checkpoint.pth")
26
+ )
27
+ sbert_model_name: str = os.getenv("QUOTIFY_SBERT_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
28
+ cross_encoder_model_name: str = os.getenv(
29
+ "QUOTIFY_CROSS_ENCODER_MODEL", "cross-encoder/ms-marco-MiniLM-L6-v2"
30
+ )
31
+ port: int = int(os.getenv("PORT", "7860"))
32
+ cors_origins: str = os.getenv("QUOTIFY_CORS_ORIGINS", "")
33
+
34
+ @property
35
+ def emotion_dataset_path(self) -> Path:
36
+ return self.backend_dir / "(Preprocessed)Emotion_classify_Data(Labeled).csv"
37
+
38
+ @property
39
+ def quotes_dataset_path(self) -> Path:
40
+ return self.backend_dir / "(Preprocessed)quotes.csv"
41
+
42
+ @property
43
+ def artifacts_dir(self) -> Path:
44
+ return self.backend_dir / "artifacts"
45
+
46
+ @property
47
+ def frontend_build_dir(self) -> Path:
48
+ return self.root_dir / "frontend" / "build"
49
+
50
+ @property
51
+ def resolved_checkpoint_path(self) -> Path:
52
+ if self.model_checkpoint_path.is_absolute():
53
+ return self.model_checkpoint_path
54
+ return self.backend_dir / self.model_checkpoint_path
55
+
56
+
57
+ config = AppConfig()
backend/src/data_loader.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import pandas as pd
8
+
9
+ from .config import AppConfig
10
+ from .utils import normalize_emotion
11
+
12
+
13
+ @dataclass
14
+ class LoadedData:
15
+ emotion_data: pd.DataFrame
16
+ quotes: pd.DataFrame
17
+
18
+
19
+ def _read_csv(path: Path) -> pd.DataFrame:
20
+ if not path.exists():
21
+ raise FileNotFoundError(f"Required dataset is missing: {path}")
22
+ return pd.read_csv(path)
23
+
24
+
25
+ def load_data(config: AppConfig, quotes_override: Optional[pd.DataFrame] = None) -> LoadedData:
26
+ emotion_data = _read_csv(config.emotion_dataset_path)
27
+ emotion_data = emotion_data.dropna(subset=["Comment", "Emotion"]).drop_duplicates(subset="Comment")
28
+ emotion_data["Emotion"] = emotion_data["Emotion"].map(normalize_emotion)
29
+
30
+ if quotes_override is None:
31
+ quotes = _read_csv(config.quotes_dataset_path)
32
+ else:
33
+ quotes = quotes_override.copy()
34
+
35
+ quotes = quotes.dropna(subset=["Quote", "Author", "emotion"]).drop_duplicates(subset="Quote")
36
+ quotes["emotion"] = quotes["emotion"].map(normalize_emotion)
37
+ quotes = quotes.reset_index(drop=True)
38
+ return LoadedData(emotion_data=emotion_data, quotes=quotes)
39
+
backend/src/emotion_classifier.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, Iterable, Optional
5
+
6
+ from sklearn.preprocessing import LabelEncoder
7
+
8
+ from .config import AppConfig
9
+ from .utils import normalize_emotion
10
+
11
+
12
+ @dataclass
13
+ class EmotionClassifierStatus:
14
+ available: bool
15
+ backend: str
16
+ checkpoint_loaded: bool
17
+ fallback: bool
18
+ reason: Optional[str] = None
19
+
20
+ def to_dict(self) -> Dict[str, object]:
21
+ return {
22
+ "available": self.available,
23
+ "backend": self.backend,
24
+ "checkpoint_loaded": self.checkpoint_loaded,
25
+ "fallback": self.fallback,
26
+ "reason": self.reason,
27
+ }
28
+
29
+
30
+ class EmotionClassifier:
31
+ def __init__(self, config: AppConfig, labels: Iterable[str]):
32
+ self.config = config
33
+ self.label_encoder = LabelEncoder()
34
+ self.label_encoder.fit([normalize_emotion(label) for label in labels])
35
+ self.device = None
36
+ self.tokenizer = None
37
+ self.model = None
38
+ self._status = EmotionClassifierStatus(
39
+ available=True,
40
+ backend="keyword_fallback",
41
+ checkpoint_loaded=False,
42
+ fallback=True,
43
+ reason="Fine-tuned BERT checkpoint was not loaded; using keyword fallback.",
44
+ )
45
+ self._load_bert_if_possible()
46
+
47
+ @property
48
+ def status(self) -> EmotionClassifierStatus:
49
+ return self._status
50
+
51
+ def _load_bert_if_possible(self) -> None:
52
+ checkpoint_path = self.config.resolved_checkpoint_path
53
+ if not checkpoint_path.exists():
54
+ return
55
+
56
+ try:
57
+ import torch
58
+ from transformers import BertForSequenceClassification, BertTokenizer
59
+ except Exception as exc: # pragma: no cover - depends on optional runtime packages
60
+ self._status = EmotionClassifierStatus(
61
+ available=True,
62
+ backend="keyword_fallback",
63
+ checkpoint_loaded=False,
64
+ fallback=True,
65
+ reason=f"BERT dependencies unavailable: {exc}",
66
+ )
67
+ return
68
+
69
+ try: # pragma: no cover - exercised only when checkpoint/model deps exist
70
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
71
+ self.tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
72
+ self.model = BertForSequenceClassification.from_pretrained(
73
+ "bert-base-uncased", num_labels=len(self.label_encoder.classes_)
74
+ )
75
+ checkpoint = torch.load(checkpoint_path, map_location=self.device)
76
+ self.model.load_state_dict(checkpoint["model_state_dict"], strict=False)
77
+ self.model.to(self.device)
78
+ self.model.eval()
79
+ self._status = EmotionClassifierStatus(
80
+ available=True,
81
+ backend="fine_tuned_bert",
82
+ checkpoint_loaded=True,
83
+ fallback=False,
84
+ )
85
+ except Exception as exc:
86
+ self.tokenizer = None
87
+ self.model = None
88
+ self._status = EmotionClassifierStatus(
89
+ available=True,
90
+ backend="keyword_fallback",
91
+ checkpoint_loaded=False,
92
+ fallback=True,
93
+ reason=f"Failed to load BERT checkpoint: {exc}",
94
+ )
95
+
96
+ def predict(self, text: str) -> str:
97
+ if self.model is not None and self.tokenizer is not None:
98
+ return self._predict_with_bert(text)
99
+ return self._predict_with_keywords(text)
100
+
101
+ def _predict_with_bert(self, text: str) -> str: # pragma: no cover - requires external checkpoint
102
+ import torch
103
+
104
+ inputs = self.tokenizer.encode_plus(
105
+ text,
106
+ add_special_tokens=True,
107
+ max_length=128,
108
+ padding="max_length",
109
+ truncation=True,
110
+ return_tensors="pt",
111
+ )
112
+ input_ids = inputs["input_ids"].to(self.device)
113
+ attention_mask = inputs["attention_mask"].to(self.device)
114
+
115
+ with torch.no_grad():
116
+ outputs = self.model(input_ids, attention_mask=attention_mask)
117
+
118
+ predicted_label = torch.argmax(outputs.logits, dim=1).item()
119
+ return normalize_emotion(self.label_encoder.inverse_transform([predicted_label])[0])
120
+
121
+ def _predict_with_keywords(self, text: str) -> str:
122
+ lowered = text.lower()
123
+ keyword_map = {
124
+ "anger": {
125
+ "angry",
126
+ "mad",
127
+ "hate",
128
+ "annoyed",
129
+ "furious",
130
+ "irritated",
131
+ "frustrated",
132
+ "upset",
133
+ },
134
+ "fear": {
135
+ "afraid",
136
+ "fear",
137
+ "scared",
138
+ "nervous",
139
+ "worried",
140
+ "anxious",
141
+ "panic",
142
+ "terrified",
143
+ },
144
+ "joy": {
145
+ "happy",
146
+ "joy",
147
+ "excited",
148
+ "grateful",
149
+ "hopeful",
150
+ "great",
151
+ "love",
152
+ "calm",
153
+ },
154
+ }
155
+ scores = {
156
+ emotion: sum(1 for keyword in keywords if keyword in lowered)
157
+ for emotion, keywords in keyword_map.items()
158
+ }
159
+ best_emotion, best_score = max(scores.items(), key=lambda item: item[1])
160
+ if best_score > 0:
161
+ return best_emotion
162
+ if "fear" in self.label_encoder.classes_:
163
+ return "fear"
164
+ return normalize_emotion(self.label_encoder.classes_[0])
165
+
backend/src/health.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict
5
+
6
+ from .config import AppConfig
7
+
8
+
9
+ def artifact_status(config: AppConfig, dense_recommender=None) -> Dict[str, object]:
10
+ status = {
11
+ "artifacts_dir": str(config.artifacts_dir),
12
+ "quote_embeddings.npy": (config.artifacts_dir / "quote_embeddings.npy").exists(),
13
+ "quote_metadata.csv": (config.artifacts_dir / "quote_metadata.csv").exists(),
14
+ "artifact_manifest.json": (config.artifacts_dir / "artifact_manifest.json").exists(),
15
+ }
16
+ if dense_recommender is not None and hasattr(dense_recommender, "artifact_status"):
17
+ status.update(dense_recommender.artifact_status())
18
+ return status
19
+
backend/src/recommenders/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from .cross_encoder_rerank import CrossEncoderRerankRecommender
2
+ from .legacy_tfidf import LegacyTfidfRecommender
3
+ from .sbert_dense import SbertDenseRecommender
4
+
5
+ __all__ = [
6
+ "CrossEncoderRerankRecommender",
7
+ "LegacyTfidfRecommender",
8
+ "SbertDenseRecommender",
9
+ ]
10
+
backend/src/recommenders/base.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List
5
+
6
+ import pandas as pd
7
+
8
+ from ..config import AppConfig
9
+ from ..emotion_classifier import EmotionClassifier
10
+ from ..schemas import RecommendationResult, StrategyInfo
11
+
12
+
13
+ class BaseRecommender(ABC):
14
+ strategy_id: str
15
+ label: str
16
+ description: str
17
+
18
+ def __init__(self, config: AppConfig, quotes: pd.DataFrame, emotion_classifier: EmotionClassifier):
19
+ self.config = config
20
+ self.quotes = quotes.reset_index(drop=True)
21
+ self.emotion_classifier = emotion_classifier
22
+ self.unavailable_reason = ""
23
+
24
+ @property
25
+ def available(self) -> bool:
26
+ return not self.unavailable_reason
27
+
28
+ def info(self) -> StrategyInfo:
29
+ return StrategyInfo(
30
+ id=self.strategy_id,
31
+ label=self.label,
32
+ description=self.description,
33
+ available=self.available,
34
+ reason=self.unavailable_reason or None,
35
+ )
36
+
37
+ @abstractmethod
38
+ def recommend(self, input_text: str, top_k: int = 5) -> RecommendationResult:
39
+ raise NotImplementedError
40
+
41
+ def _require_available(self) -> None:
42
+ if not self.available:
43
+ raise RuntimeError(f"{self.label} is unavailable: {self.unavailable_reason}")
44
+
45
+ @staticmethod
46
+ def _top_indices(scores, limit: int) -> List[int]:
47
+ limit = max(1, min(int(limit), len(scores)))
48
+ return list(scores.argsort()[-limit:][::-1])
49
+
backend/src/recommenders/cross_encoder_rerank.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from .base import BaseRecommender
6
+ from .sbert_dense import SbertDenseRecommender
7
+ from ..schemas import Alternative, RecommendationResult, ScoreBreakdown
8
+ from ..utils import normalize_vector, score_with_emotion_boost
9
+
10
+
11
+ class CrossEncoderRerankRecommender(BaseRecommender):
12
+ strategy_id = "cross_encoder_rerank"
13
+ label = "AI Reranker"
14
+ description = (
15
+ "Retrieves candidates with dense embeddings, then reranks them with a Cross-Encoder "
16
+ "and emotion-aware scoring."
17
+ )
18
+
19
+ def __init__(self, config, quotes, emotion_classifier, dense_recommender: SbertDenseRecommender):
20
+ super().__init__(config, quotes, emotion_classifier)
21
+ self.dense_recommender = dense_recommender
22
+ self.cross_encoder = None
23
+ if not self.config.enable_cross_encoder:
24
+ self.unavailable_reason = "Cross-Encoder mode is disabled by QUOTIFY_ENABLE_CROSS_ENCODER."
25
+ return
26
+ if not dense_recommender.available:
27
+ self.unavailable_reason = "Dense retriever is unavailable, so reranking cannot run."
28
+ return
29
+ self._load_cross_encoder()
30
+
31
+ def _load_cross_encoder(self) -> None:
32
+ try:
33
+ from sentence_transformers import CrossEncoder
34
+ except Exception as exc:
35
+ self.unavailable_reason = f"CrossEncoder dependency unavailable: {exc}"
36
+ return
37
+ try:
38
+ self.cross_encoder = CrossEncoder(self.config.cross_encoder_model_name)
39
+ except Exception as exc:
40
+ self.unavailable_reason = f"Failed to load Cross-Encoder model: {exc}"
41
+
42
+ def recommend(self, input_text: str, top_k: int = 5) -> RecommendationResult:
43
+ self._require_available()
44
+ user_emotion = self.emotion_classifier.predict(input_text)
45
+ query = self.dense_recommender.model.encode(
46
+ [input_text], convert_to_numpy=True, normalize_embeddings=True
47
+ )[0].astype("float32")
48
+ query = normalize_vector(query)
49
+ semantic_scores = np.dot(self.dense_recommender.embeddings, query)
50
+ retrieve_count = max(top_k, min(self.config.top_k_retrieve, len(self.quotes)))
51
+ candidate_indices = self._top_indices(semantic_scores, retrieve_count)
52
+
53
+ pairs = [(input_text, str(self.quotes.iloc[idx]["Quote"])) for idx in candidate_indices]
54
+ cross_scores = np.array(self.cross_encoder.predict(pairs), dtype="float32")
55
+ if cross_scores.size:
56
+ min_score = float(cross_scores.min())
57
+ max_score = float(cross_scores.max())
58
+ if max_score > min_score:
59
+ normalized_cross = (cross_scores - min_score) / (max_score - min_score)
60
+ else:
61
+ normalized_cross = np.ones_like(cross_scores)
62
+ else:
63
+ normalized_cross = cross_scores
64
+
65
+ ranked = []
66
+ for position, idx in enumerate(candidate_indices):
67
+ semantic = float(semantic_scores[idx])
68
+ cross = float(normalized_cross[position])
69
+ blended = (0.35 * semantic) + (0.65 * cross)
70
+ final, boost = score_with_emotion_boost(
71
+ blended, self.quotes.iloc[idx]["emotion"], user_emotion, self.config.emotion_boost
72
+ )
73
+ ranked.append((idx, semantic, cross, boost, final))
74
+
75
+ ranked.sort(key=lambda item: item[-1], reverse=True)
76
+ selected_idx, semantic, cross, boost, final = ranked[0]
77
+ selected = self.quotes.iloc[selected_idx]
78
+ alternatives = [
79
+ Alternative(
80
+ quote=str(self.quotes.iloc[idx]["Quote"]),
81
+ author=str(self.quotes.iloc[idx]["Author"]),
82
+ quote_emotion=str(self.quotes.iloc[idx]["emotion"]),
83
+ final_score=round(float(item_final), 4),
84
+ )
85
+ for idx, _, _, _, item_final in ranked[1:top_k]
86
+ ]
87
+ return RecommendationResult(
88
+ quote=str(selected["Quote"]),
89
+ author=str(selected["Author"]),
90
+ emotion=user_emotion,
91
+ quote_emotion=str(selected["emotion"]),
92
+ strategy=self.strategy_id,
93
+ strategy_label=self.label,
94
+ scores=ScoreBreakdown(
95
+ semantic_score=round(float(semantic), 4),
96
+ cross_encoder_score=round(float(cross), 4),
97
+ emotion_boost=round(float(boost), 4),
98
+ final_score=round(float(final), 4),
99
+ ),
100
+ alternatives=alternatives,
101
+ )
102
+
backend/src/recommenders/legacy_tfidf.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ from sklearn.feature_extraction.text import TfidfVectorizer
5
+ from sklearn.metrics.pairwise import cosine_similarity
6
+
7
+ from .base import BaseRecommender
8
+ from ..schemas import Alternative, RecommendationResult, ScoreBreakdown
9
+ from ..utils import score_with_emotion_boost
10
+
11
+
12
+ class LegacyTfidfRecommender(BaseRecommender):
13
+ strategy_id = "legacy_tfidf_bert"
14
+ label = "Legacy BERT + TF-IDF"
15
+ description = (
16
+ "Original project method using fine-tuned BERT emotion classification, "
17
+ "TF-IDF cosine similarity, and emotion weighting."
18
+ )
19
+
20
+ def __init__(self, config, quotes, emotion_classifier):
21
+ super().__init__(config, quotes, emotion_classifier)
22
+ try:
23
+ self.vectorizer = TfidfVectorizer()
24
+ self.matrix = self.vectorizer.fit_transform(self.quotes["Quote"].tolist())
25
+ except Exception as exc:
26
+ self.unavailable_reason = f"Failed to build TF-IDF matrix: {exc}"
27
+
28
+ def recommend(self, input_text: str, top_k: int = 5) -> RecommendationResult:
29
+ self._require_available()
30
+ user_emotion = self.emotion_classifier.predict(input_text)
31
+ input_vec = self.vectorizer.transform([input_text])
32
+ semantic_scores = cosine_similarity(input_vec, self.matrix).flatten()
33
+
34
+ final_scores = []
35
+ boosts = []
36
+ for score, quote_emotion in zip(semantic_scores, self.quotes["emotion"]):
37
+ final, boost = score_with_emotion_boost(score, quote_emotion, user_emotion, self.config.emotion_boost)
38
+ final_scores.append(final)
39
+ boosts.append(boost)
40
+
41
+ final_scores = np.array(final_scores)
42
+ boosts = np.array(boosts)
43
+ indices = self._top_indices(final_scores, max(top_k, 1))
44
+ selected_idx = indices[0]
45
+ selected = self.quotes.iloc[selected_idx]
46
+
47
+ alternatives = [
48
+ Alternative(
49
+ quote=str(self.quotes.iloc[idx]["Quote"]),
50
+ author=str(self.quotes.iloc[idx]["Author"]),
51
+ quote_emotion=str(self.quotes.iloc[idx]["emotion"]),
52
+ final_score=round(float(final_scores[idx]), 4),
53
+ )
54
+ for idx in indices[1:top_k]
55
+ ]
56
+
57
+ return RecommendationResult(
58
+ quote=str(selected["Quote"]),
59
+ author=str(selected["Author"]),
60
+ emotion=user_emotion,
61
+ quote_emotion=str(selected["emotion"]),
62
+ strategy=self.strategy_id,
63
+ strategy_label=self.label,
64
+ scores=ScoreBreakdown(
65
+ semantic_score=round(float(semantic_scores[selected_idx]), 4),
66
+ cross_encoder_score=None,
67
+ emotion_boost=round(float(boosts[selected_idx]), 4),
68
+ final_score=round(float(final_scores[selected_idx]), 4),
69
+ ),
70
+ alternatives=alternatives,
71
+ )
72
+
backend/src/recommenders/sbert_dense.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+
8
+ from .base import BaseRecommender
9
+ from ..schemas import Alternative, RecommendationResult, ScoreBreakdown
10
+ from ..utils import normalize_rows, normalize_vector, score_with_emotion_boost, write_json
11
+
12
+
13
+ class SbertDenseRecommender(BaseRecommender):
14
+ strategy_id = "sbert_dense"
15
+ label = "Dense Semantic Search"
16
+ description = "Uses SentenceTransformer embeddings for meaning-based quote retrieval with emotion weighting."
17
+
18
+ def __init__(self, config, quotes, emotion_classifier):
19
+ super().__init__(config, quotes, emotion_classifier)
20
+ self.model = None
21
+ self.embeddings = None
22
+ self.embedding_path = self.config.artifacts_dir / "quote_embeddings.npy"
23
+ self.metadata_path = self.config.artifacts_dir / "quote_metadata.csv"
24
+ self.manifest_path = self.config.artifacts_dir / "artifact_manifest.json"
25
+ self._load_model_and_artifacts()
26
+
27
+ def _load_model_and_artifacts(self) -> None:
28
+ if not self.config.enable_sbert:
29
+ self.unavailable_reason = "Dense retrieval is disabled by QUOTIFY_ENABLE_SBERT."
30
+ return
31
+ try:
32
+ from sentence_transformers import SentenceTransformer
33
+ except Exception as exc:
34
+ self.unavailable_reason = f"sentence-transformers is unavailable: {exc}"
35
+ return
36
+
37
+ try:
38
+ self.model = SentenceTransformer(self.config.sbert_model_name)
39
+ self.config.artifacts_dir.mkdir(parents=True, exist_ok=True)
40
+ if self.embedding_path.exists():
41
+ embeddings = np.load(self.embedding_path)
42
+ if embeddings.shape[0] == len(self.quotes):
43
+ self.embeddings = normalize_rows(embeddings.astype("float32"))
44
+ else:
45
+ self.embeddings = self._build_embeddings()
46
+ else:
47
+ self.embeddings = self._build_embeddings()
48
+ except Exception as exc:
49
+ self.unavailable_reason = f"Failed to load dense retrieval model/artifacts: {exc}"
50
+
51
+ def _build_embeddings(self) -> np.ndarray:
52
+ embeddings = self.model.encode(
53
+ self.quotes["Quote"].tolist(),
54
+ batch_size=64,
55
+ convert_to_numpy=True,
56
+ show_progress_bar=False,
57
+ normalize_embeddings=True,
58
+ ).astype("float32")
59
+ np.save(self.embedding_path, embeddings)
60
+ self.quotes.to_csv(self.metadata_path, index=False)
61
+ write_json(
62
+ self.manifest_path,
63
+ {
64
+ "model": self.config.sbert_model_name,
65
+ "quote_count": int(len(self.quotes)),
66
+ "embedding_file": self.embedding_path.name,
67
+ "metadata_file": self.metadata_path.name,
68
+ },
69
+ )
70
+ return embeddings
71
+
72
+ def recommend(self, input_text: str, top_k: int = 5) -> RecommendationResult:
73
+ self._require_available()
74
+ user_emotion = self.emotion_classifier.predict(input_text)
75
+ query = self.model.encode([input_text], convert_to_numpy=True, normalize_embeddings=True)[0].astype("float32")
76
+ query = normalize_vector(query)
77
+ semantic_scores = np.dot(self.embeddings, query)
78
+ return self._format_result(input_text, user_emotion, semantic_scores, top_k)
79
+
80
+ def _format_result(self, input_text: str, user_emotion: str, semantic_scores, top_k: int) -> RecommendationResult:
81
+ final_scores = []
82
+ boosts = []
83
+ for score, quote_emotion in zip(semantic_scores, self.quotes["emotion"]):
84
+ final, boost = score_with_emotion_boost(float(score), quote_emotion, user_emotion, self.config.emotion_boost)
85
+ final_scores.append(final)
86
+ boosts.append(boost)
87
+
88
+ final_scores = np.array(final_scores)
89
+ boosts = np.array(boosts)
90
+ indices = self._top_indices(final_scores, max(top_k, 1))
91
+ selected_idx = indices[0]
92
+ selected = self.quotes.iloc[selected_idx]
93
+ alternatives = [
94
+ Alternative(
95
+ quote=str(self.quotes.iloc[idx]["Quote"]),
96
+ author=str(self.quotes.iloc[idx]["Author"]),
97
+ quote_emotion=str(self.quotes.iloc[idx]["emotion"]),
98
+ final_score=round(float(final_scores[idx]), 4),
99
+ )
100
+ for idx in indices[1:top_k]
101
+ ]
102
+ return RecommendationResult(
103
+ quote=str(selected["Quote"]),
104
+ author=str(selected["Author"]),
105
+ emotion=user_emotion,
106
+ quote_emotion=str(selected["emotion"]),
107
+ strategy=self.strategy_id,
108
+ strategy_label=self.label,
109
+ scores=ScoreBreakdown(
110
+ semantic_score=round(float(semantic_scores[selected_idx]), 4),
111
+ cross_encoder_score=None,
112
+ emotion_boost=round(float(boosts[selected_idx]), 4),
113
+ final_score=round(float(final_scores[selected_idx]), 4),
114
+ ),
115
+ alternatives=alternatives,
116
+ )
117
+
118
+ def artifact_status(self) -> dict:
119
+ manifest = {}
120
+ if self.manifest_path.exists():
121
+ try:
122
+ manifest = json.loads(self.manifest_path.read_text(encoding="utf-8"))
123
+ except Exception:
124
+ manifest = {}
125
+ return {
126
+ "quote_embeddings.npy": self.embedding_path.exists(),
127
+ "quote_metadata.csv": self.metadata_path.exists(),
128
+ "artifact_manifest.json": self.manifest_path.exists(),
129
+ "manifest": manifest,
130
+ }
backend/src/schemas.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Dict, List, Optional
3
+
4
+
5
+ @dataclass
6
+ class ScoreBreakdown:
7
+ semantic_score: Optional[float]
8
+ cross_encoder_score: Optional[float]
9
+ emotion_boost: float
10
+ final_score: float
11
+
12
+ def to_dict(self) -> Dict[str, Optional[float]]:
13
+ return {
14
+ "semantic_score": self.semantic_score,
15
+ "cross_encoder_score": self.cross_encoder_score,
16
+ "emotion_boost": self.emotion_boost,
17
+ "final_score": self.final_score,
18
+ }
19
+
20
+
21
+ @dataclass
22
+ class Alternative:
23
+ quote: str
24
+ author: str
25
+ quote_emotion: str
26
+ final_score: float
27
+
28
+ def to_dict(self) -> Dict[str, Any]:
29
+ return {
30
+ "quote": self.quote,
31
+ "author": self.author,
32
+ "quote_emotion": self.quote_emotion,
33
+ "final_score": self.final_score,
34
+ }
35
+
36
+
37
+ @dataclass
38
+ class RecommendationResult:
39
+ quote: str
40
+ author: str
41
+ emotion: str
42
+ quote_emotion: str
43
+ strategy: str
44
+ strategy_label: str
45
+ scores: ScoreBreakdown
46
+ alternatives: List[Alternative] = field(default_factory=list)
47
+
48
+ def to_dict(self) -> Dict[str, Any]:
49
+ return {
50
+ "quote": self.quote,
51
+ "author": self.author,
52
+ "emotion": self.emotion,
53
+ "quote_emotion": self.quote_emotion,
54
+ "strategy": self.strategy,
55
+ "strategy_label": self.strategy_label,
56
+ "scores": self.scores.to_dict(),
57
+ "alternatives": [item.to_dict() for item in self.alternatives],
58
+ }
59
+
60
+
61
+ @dataclass
62
+ class StrategyInfo:
63
+ id: str
64
+ label: str
65
+ description: str
66
+ available: bool
67
+ reason: Optional[str] = None
68
+
69
+ def to_dict(self) -> Dict[str, Any]:
70
+ payload = {
71
+ "id": self.id,
72
+ "label": self.label,
73
+ "description": self.description,
74
+ "available": self.available,
75
+ }
76
+ if self.reason:
77
+ payload["reason"] = self.reason
78
+ return payload
79
+
backend/src/utils.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+ import numpy as np
8
+
9
+
10
+ def normalize_emotion(value: Any) -> str:
11
+ return str(value or "").strip().lower()
12
+
13
+
14
+ def normalize_rows(scores: np.ndarray) -> np.ndarray:
15
+ norms = np.linalg.norm(scores, axis=1, keepdims=True)
16
+ norms[norms == 0] = 1.0
17
+ return scores / norms
18
+
19
+
20
+ def normalize_vector(vector: np.ndarray) -> np.ndarray:
21
+ norm = np.linalg.norm(vector)
22
+ if norm == 0:
23
+ return vector
24
+ return vector / norm
25
+
26
+
27
+ def score_with_emotion_boost(score: float, quote_emotion: str, user_emotion: str, boost: float) -> tuple[float, float]:
28
+ applied_boost = boost if normalize_emotion(quote_emotion) == normalize_emotion(user_emotion) else 0.0
29
+ return float(score + applied_boost), float(applied_boost)
30
+
31
+
32
+ def write_json(path: Path, payload: Dict[str, Any]) -> None:
33
+ path.parent.mkdir(parents=True, exist_ok=True)
34
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
35
+
backend/tests/test_api.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dataclasses import replace
3
+
4
+ import pandas as pd
5
+ import pytest
6
+
7
+ os.environ["QUOTIFY_ENABLE_SBERT"] = "false"
8
+ os.environ["QUOTIFY_ENABLE_CROSS_ENCODER"] = "false"
9
+
10
+ from app import create_app
11
+ from src.config import config
12
+
13
+
14
+ @pytest.fixture()
15
+ def client():
16
+ quotes = pd.DataFrame(
17
+ [
18
+ {
19
+ "Quote": "Courage is resistance to fear, mastery of fear, not absence of fear.",
20
+ "Author": "Mark Twain",
21
+ "emotion": "fear",
22
+ },
23
+ {
24
+ "Quote": "For every minute you are angry you lose sixty seconds of happiness.",
25
+ "Author": "Ralph Waldo Emerson",
26
+ "emotion": "anger",
27
+ },
28
+ {
29
+ "Quote": "Joy is the simplest form of gratitude.",
30
+ "Author": "Karl Barth",
31
+ "emotion": "joy",
32
+ },
33
+ ]
34
+ )
35
+ test_config = replace(
36
+ config,
37
+ enable_sbert=False,
38
+ enable_cross_encoder=False,
39
+ default_strategy="legacy_tfidf_bert",
40
+ )
41
+ app = create_app(test_config, quotes_override=quotes)
42
+ app.config.update(TESTING=True)
43
+ return app.test_client()
44
+
45
+
46
+ def test_health_returns_200(client):
47
+ response = client.get("/health")
48
+ assert response.status_code == 200
49
+ payload = response.get_json()
50
+ assert payload["app"] == "Quotify"
51
+ assert "legacy_tfidf_bert" in payload["available_strategies"]
52
+ assert payload["models"]["emotion_classifier"]["available"] is True
53
+
54
+
55
+ def test_strategies_returns_all_strategy_ids(client):
56
+ response = client.get("/strategies")
57
+ assert response.status_code == 200
58
+ ids = {item["id"] for item in response.get_json()}
59
+ assert ids == {"legacy_tfidf_bert", "sbert_dense", "cross_encoder_rerank"}
60
+
61
+
62
+ def test_get_quote_validates_missing_input(client):
63
+ response = client.post("/get_quote", json={})
64
+ assert response.status_code == 400
65
+ assert response.get_json()["error"] == "inputText is required."
66
+
67
+
68
+ def test_each_available_strategy_returns_valid_quote(client):
69
+ strategies = client.get("/strategies").get_json()
70
+ for strategy in strategies:
71
+ if not strategy["available"]:
72
+ continue
73
+ response = client.post(
74
+ "/get_quote",
75
+ json={"inputText": "I am nervous about tomorrow", "strategy": strategy["id"], "topK": 2},
76
+ )
77
+ assert response.status_code == 200
78
+ payload = response.get_json()
79
+ assert payload["quote"]
80
+ assert payload["author"]
81
+ assert payload["strategy"] == strategy["id"]
82
+ assert "final_score" in payload["scores"]
83
+
84
+
85
+ def test_compare_returns_available_strategy_results(client):
86
+ response = client.post("/compare", json={"inputText": "I am happy today", "topK": 2})
87
+ assert response.status_code == 200
88
+ payload = response.get_json()
89
+ assert payload["results"]
90
+ assert payload["results"][0]["strategy"] == "legacy_tfidf_bert"
91
+ assert isinstance(payload["errors"], list)
92
+
93
+
94
+ def test_missing_checkpoint_does_not_crash_backend(client):
95
+ response = client.get("/health")
96
+ assert response.status_code == 200
97
+ classifier = response.get_json()["models"]["emotion_classifier"]
98
+ assert classifier["available"] is True
99
+ assert classifier["checkpoint_loaded"] is False
100
+ assert classifier["fallback"] is True
101
+
backend/tests/test_health.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ def test_placeholder_health_test_module_exists():
2
+ assert True
3
+
backend/tests/test_recommenders.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import replace
2
+
3
+ import pandas as pd
4
+
5
+ from src.config import config
6
+ from src.emotion_classifier import EmotionClassifier
7
+ from src.recommenders.legacy_tfidf import LegacyTfidfRecommender
8
+
9
+
10
+ def test_legacy_recommender_returns_quote_from_fixture():
11
+ quotes = pd.DataFrame(
12
+ [
13
+ {"Quote": "Be happy with this moment.", "Author": "Unknown", "emotion": "joy"},
14
+ {"Quote": "Fear can be a teacher.", "Author": "Unknown", "emotion": "fear"},
15
+ ]
16
+ )
17
+ classifier = EmotionClassifier(replace(config, enable_sbert=False), ["joy", "fear", "anger"])
18
+ recommender = LegacyTfidfRecommender(replace(config, emotion_boost=0.08), quotes, classifier)
19
+
20
+ result = recommender.recommend("I am happy and grateful", top_k=2)
21
+
22
+ assert result.quote
23
+ assert result.author
24
+ assert result.strategy == "legacy_tfidf_bert"
25
+ assert result.scores.final_score >= result.scores.semantic_score
docker-compose.yml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ quotify:
3
+ build: .
4
+ ports:
5
+ - "7860:7860"
6
+ environment:
7
+ PORT: 7860
8
+ QUOTIFY_ENABLE_CROSS_ENCODER: "false"
9
+ QUOTIFY_DEFAULT_STRATEGY: legacy_tfidf_bert
10
+ volumes:
11
+ - ./backend/artifacts:/app/backend/artifacts
12
+
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "quotify",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "proxy": "http://127.0.0.1:7860",
6
+ "dependencies": {
7
+ "@testing-library/jest-dom": "^5.17.0",
8
+ "@testing-library/react": "^13.4.0",
9
+ "@testing-library/user-event": "^13.5.0",
10
+ "axios": "^1.7.2",
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1",
13
+ "react-icons": "^5.2.1",
14
+ "react-router-dom": "^6.23.1",
15
+ "react-scripts": "5.0.1",
16
+ "web-vitals": "^2.1.4"
17
+ },
18
+ "scripts": {
19
+ "start": "react-scripts start",
20
+ "build": "react-scripts build",
21
+ "test": "react-scripts test",
22
+ "eject": "react-scripts eject"
23
+ },
24
+ "eslintConfig": {
25
+ "extends": [
26
+ "react-app",
27
+ "react-app/jest"
28
+ ]
29
+ },
30
+ "browserslist": {
31
+ "production": [
32
+ ">0.2%",
33
+ "not dead",
34
+ "not op_mini all"
35
+ ],
36
+ "development": [
37
+ "last 1 chrome version",
38
+ "last 1 firefox version",
39
+ "last 1 safari version"
40
+ ]
41
+ },
42
+ "devDependencies": {
43
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
44
+ "autoprefixer": "^10.4.19",
45
+ "postcss": "^8.4.38",
46
+ "tailwindcss": "^3.4.4"
47
+ }
48
+ }
frontend/public/index.html ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;500;600;700&display=swap">
5
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Overpass:wght@400;600;700&display=swap">
6
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap">
7
+ <meta charset="utf-8" />
8
+ <link rel="icon" href="./quotify.png" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10
+ <meta name="theme-color" content="#000000" />
11
+ <meta
12
+ name="description"
13
+ content="Web site created using create-react-app"
14
+ />
15
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
16
+ <!--
17
+ manifest.json provides metadata used when your web app is installed on a
18
+ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
19
+ -->
20
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
21
+ <!--
22
+ Notice the use of %PUBLIC_URL% in the tags above.
23
+ It will be replaced with the URL of the `public` folder during the build.
24
+ Only files inside the `public` folder can be referenced from the HTML.
25
+
26
+ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
27
+ work correctly both with client-side routing and a non-root public URL.
28
+ Learn how to configure a non-root public URL by running `npm run build`.
29
+ -->
30
+ <title>Quotify</title>
31
+ </head>
32
+ <body>
33
+ <noscript>You need to enable JavaScript to run this app.</noscript>
34
+ <div id="root"></div>
35
+ <!--
36
+ This HTML file is a template.
37
+ If you open it directly in the browser, you will see an empty page.
38
+
39
+ You can add webfonts, meta tags, or analytics to this file.
40
+ The build step will place the bundled scripts into the <body> tag.
41
+
42
+ To begin the development, run `npm start` or `yarn start`.
43
+ To create a production bundle, use `npm run build` or `yarn build`.
44
+ -->
45
+ </body>
46
+ </html>
frontend/public/manifest.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "React App",
3
+ "name": "Create React App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#000000",
24
+ "background_color": "#ffffff"
25
+ }
frontend/public/quotify.png ADDED
frontend/public/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
frontend/src/App.css ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .page {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ height: 100vh;
7
+ background-color: #004d40;
8
+ color: #fff;
9
+ }
10
+
11
+ input {
12
+ padding: 10px;
13
+ margin-top: 20px;
14
+ border: none;
15
+ border-radius: 5px;
16
+ }
17
+
18
+ button {
19
+ margin-top: 20px;
20
+ padding: 10px 20px;
21
+ border: none;
22
+ border-radius: 5px;
23
+ background-color: #ff5722;
24
+ color: #fff;
25
+ }
26
+
27
+ .quote-box {
28
+ margin-top: 20px;
29
+ padding: 20px;
30
+ background-color: #ff5722;
31
+ border-radius: 5px;
32
+ text-align: center;
33
+ }
frontend/src/App.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import Main from './components/MainPage';
3
+ import Login from './components/Login';
4
+ import './index.css';
5
+
6
+ const App = () => {
7
+ const [page, setPage] = useState('login');
8
+ const [userName, setUserName] = useState('');
9
+
10
+ const handleLogin = (name) => {
11
+ setUserName(name);
12
+ setPage('main');
13
+ };
14
+
15
+ const handlePowerButtonClick = () => {
16
+ setPage('login');
17
+ setUserName('');
18
+ };
19
+
20
+ return (
21
+ <div className="app">
22
+ {page === 'login' && <Login onLogin={handleLogin} />}
23
+ {page === 'main' && <Main userName={userName} onPowerButtonClick={handlePowerButtonClick} />}
24
+ </div>
25
+ );
26
+ };
27
+
28
+ export default App;
frontend/src/App.test.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { render, screen } from '@testing-library/react';
2
+ import App from './App';
3
+
4
+ test('renders learn react link', () => {
5
+ render(<App />);
6
+ const linkElement = screen.getByText(/learn react/i);
7
+ expect(linkElement).toBeInTheDocument();
8
+ });
frontend/src/components/Login.css ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .login-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ height: 100vh;
7
+ background-color: #014737;
8
+ color: white;
9
+ }
10
+
11
+ .login-header {
12
+ font-size: 2.5rem;
13
+ margin-bottom: 1rem;
14
+ }
15
+
16
+ .login-input {
17
+ padding: 0.5rem 1rem;
18
+ width: 24rem;
19
+ border-radius: 9999px;
20
+ color: black;
21
+ font-size: 1.25rem;
22
+ border: none;
23
+ outline: none;
24
+ margin-bottom: 1rem;
25
+ }
26
+
27
+ .login-button {
28
+ padding: 0.5rem 1rem;
29
+ font-size: 1rem;
30
+ border: none;
31
+ border-radius: 9999px;
32
+ background-color: #C7DFC5;
33
+ color: #014737;
34
+ cursor: pointer;
35
+ transition: transform 0.2s, color 0.2s;
36
+ font-weight: 700;
37
+ }
38
+
39
+ .login-input.error {
40
+ border: 2px solid red;
41
+ }
42
+
43
+ .error-text {
44
+ position: absolute;
45
+ color: rgb(255, 65, 65);
46
+ font-size: 0.875rem;
47
+ margin-top: -1.45rem;
48
+ margin-bottom: 1rem;
49
+ font-weight: 900;
50
+ }
51
+
52
+ .login-button:hover {
53
+ transform: scale(1.05);
54
+ }
55
+
56
+ .login-button:active {
57
+ transform: scale(0.95);
58
+ background-color: #C44900;
59
+ color: #ffffff;
60
+ }
frontend/src/components/Login.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import './Login.css'; // Import the CSS file
3
+
4
+ const Login = ({ onLogin }) => {
5
+ const [name, setName] = useState('');
6
+ const [error, setError] = useState('');
7
+
8
+ const handleLogin = () => {
9
+ if (name.trim()) {
10
+ onLogin(name);
11
+ setName(''); // Clear the input field
12
+ }
13
+ };
14
+
15
+ const handleKeyDown = (e) => {
16
+ if (e.key === 'Enter') {
17
+ handleLogin();
18
+ }
19
+ };
20
+
21
+ const handleChange = (e) => {
22
+ const value = e.target.value;
23
+ if (value.length <= 10) {
24
+ setName(value);
25
+ setError('');
26
+ } else {
27
+ setError('Nickname cannot be more than 10 characters.');
28
+ }
29
+ };
30
+
31
+ return (
32
+ <div className="login-container font-worksans">
33
+ <h1 className="login-header">Welcome to <strong className='font-merriweather'>Quotify</strong></h1>
34
+ <input
35
+ type="text"
36
+ className={`login-input ${error ? 'error' : ''}`}
37
+ placeholder="Enter your nickname"
38
+ value={name}
39
+ onChange={handleChange}
40
+ onKeyDown={handleKeyDown}
41
+ />
42
+ {error && <p className="error-text">{error}</p>}
43
+ <button className="login-button" onClick={handleLogin}>
44
+ Login
45
+ </button>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default Login;
frontend/src/components/MainPage.css ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .page-container {
2
+ min-height: 100vh;
3
+ background: #014737;
4
+ color: white;
5
+ }
6
+
7
+ .main-shell {
8
+ width: min(1120px, calc(100% - 32px));
9
+ margin: 0 auto;
10
+ padding: 104px 0 48px;
11
+ }
12
+
13
+ .hero-panel {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 20px;
17
+ }
18
+
19
+ .text-container {
20
+ display: flex;
21
+ flex-direction: column;
22
+ align-items: flex-start;
23
+ }
24
+
25
+ .header-text {
26
+ font-size: clamp(2.5rem, 7vw, 4.8rem);
27
+ line-height: 1;
28
+ margin: 0 0 4px;
29
+ }
30
+
31
+ .subheader-text {
32
+ font-size: clamp(1.35rem, 3vw, 2rem);
33
+ letter-spacing: 0;
34
+ margin: 0;
35
+ color: #f3e1b6;
36
+ }
37
+
38
+ .strategy-control {
39
+ display: grid;
40
+ grid-template-columns: repeat(3, minmax(0, 1fr));
41
+ gap: 8px;
42
+ background: rgba(255, 255, 255, 0.08);
43
+ border: 1px solid rgba(255, 255, 255, 0.14);
44
+ border-radius: 8px;
45
+ padding: 8px;
46
+ }
47
+
48
+ .strategy-button,
49
+ .compare-button {
50
+ min-height: 44px;
51
+ border: 0;
52
+ border-radius: 6px;
53
+ font-weight: 700;
54
+ cursor: pointer;
55
+ transition: transform 160ms ease, background 160ms ease, color 160ms ease;
56
+ }
57
+
58
+ .strategy-button {
59
+ background: transparent;
60
+ color: #f6f2e9;
61
+ }
62
+
63
+ .strategy-button.active {
64
+ background: #f3e1b6;
65
+ color: #014737;
66
+ }
67
+
68
+ .strategy-button:disabled {
69
+ color: rgba(255, 255, 255, 0.36);
70
+ cursor: not-allowed;
71
+ }
72
+
73
+ .strategy-card {
74
+ display: grid;
75
+ gap: 8px;
76
+ background: #c7dfc5;
77
+ color: #014737;
78
+ border-radius: 8px;
79
+ padding: 16px;
80
+ }
81
+
82
+ .strategy-card span,
83
+ .score-grid span,
84
+ .alternatives span {
85
+ display: block;
86
+ font-size: 0.76rem;
87
+ font-weight: 800;
88
+ letter-spacing: 0.08em;
89
+ text-transform: uppercase;
90
+ opacity: 0.72;
91
+ }
92
+
93
+ .strategy-card strong {
94
+ display: block;
95
+ font-size: 1.1rem;
96
+ }
97
+
98
+ .strategy-card p {
99
+ margin: 0;
100
+ line-height: 1.5;
101
+ }
102
+
103
+ .strategy-warning {
104
+ color: #8a2c17;
105
+ font-weight: 700;
106
+ }
107
+
108
+ .input-container {
109
+ display: flex;
110
+ align-items: center;
111
+ position: relative;
112
+ }
113
+
114
+ .input-field {
115
+ width: 100%;
116
+ min-height: 56px;
117
+ padding: 0.75rem 4rem 0.75rem 1.2rem;
118
+ border-radius: 999px;
119
+ background-color: #f3e1b6;
120
+ color: #10231e;
121
+ font-size: 1.1rem;
122
+ border: 2px solid transparent;
123
+ outline: none;
124
+ }
125
+
126
+ .input-field:focus {
127
+ border-color: #c7dfc5;
128
+ }
129
+
130
+ .submit-button {
131
+ position: absolute;
132
+ right: 8px;
133
+ width: 44px;
134
+ height: 44px;
135
+ border-radius: 50%;
136
+ background: #014737;
137
+ border: none;
138
+ cursor: pointer;
139
+ color: white;
140
+ font-size: 1.3rem;
141
+ font-weight: 800;
142
+ }
143
+
144
+ .submit-button:disabled {
145
+ cursor: not-allowed;
146
+ opacity: 0.5;
147
+ }
148
+
149
+ .action-row {
150
+ display: flex;
151
+ justify-content: flex-end;
152
+ }
153
+
154
+ .compare-button {
155
+ padding: 0 18px;
156
+ background: #ffffff;
157
+ color: #014737;
158
+ }
159
+
160
+ .compare-button:disabled {
161
+ cursor: not-allowed;
162
+ opacity: 0.64;
163
+ }
164
+
165
+ .error-state,
166
+ .empty-state,
167
+ .compare-errors {
168
+ border-radius: 8px;
169
+ padding: 14px 16px;
170
+ line-height: 1.5;
171
+ }
172
+
173
+ .error-state {
174
+ background: #ffe1d7;
175
+ color: #84230e;
176
+ }
177
+
178
+ .empty-state {
179
+ background: rgba(255, 255, 255, 0.1);
180
+ color: #f6f2e9;
181
+ }
182
+
183
+ .results-section {
184
+ display: grid;
185
+ grid-template-columns: minmax(0, 1fr);
186
+ gap: 16px;
187
+ margin-top: 24px;
188
+ }
189
+
190
+ .results-section.compare-layout {
191
+ grid-template-columns: repeat(3, minmax(0, 1fr));
192
+ }
193
+
194
+ .result-card {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 14px;
198
+ background: #f6f2e9;
199
+ color: #014737;
200
+ border-radius: 8px;
201
+ padding: 20px;
202
+ min-width: 0;
203
+ box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
204
+ }
205
+
206
+ .result-card-header {
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: space-between;
210
+ gap: 10px;
211
+ }
212
+
213
+ .strategy-pill,
214
+ .emotion-pill {
215
+ display: inline-flex;
216
+ align-items: center;
217
+ min-height: 28px;
218
+ border-radius: 999px;
219
+ padding: 0 10px;
220
+ font-size: 0.8rem;
221
+ font-weight: 800;
222
+ }
223
+
224
+ .strategy-pill {
225
+ background: #014737;
226
+ color: #ffffff;
227
+ }
228
+
229
+ .emotion-pill {
230
+ background: #c7dfc5;
231
+ color: #014737;
232
+ }
233
+
234
+ .quote-text {
235
+ font-family: Merriweather, serif;
236
+ font-size: clamp(1.05rem, 2vw, 1.3rem);
237
+ line-height: 1.55;
238
+ margin: 0;
239
+ }
240
+
241
+ .quote-author {
242
+ font-family: Merriweather, serif;
243
+ margin: 0;
244
+ font-weight: 700;
245
+ }
246
+
247
+ .score-grid {
248
+ display: grid;
249
+ grid-template-columns: repeat(4, minmax(0, 1fr));
250
+ gap: 8px;
251
+ }
252
+
253
+ .score-grid div {
254
+ background: #e5ecd8;
255
+ border-radius: 6px;
256
+ padding: 10px;
257
+ }
258
+
259
+ .score-grid strong {
260
+ display: block;
261
+ margin-top: 4px;
262
+ }
263
+
264
+ .rerank-score {
265
+ background: #f3e1b6;
266
+ border-radius: 6px;
267
+ padding: 10px;
268
+ }
269
+
270
+ .alternatives {
271
+ display: grid;
272
+ gap: 8px;
273
+ border-top: 1px solid rgba(1, 71, 55, 0.16);
274
+ padding-top: 12px;
275
+ }
276
+
277
+ .alternatives p {
278
+ margin: 0;
279
+ font-size: 0.92rem;
280
+ line-height: 1.4;
281
+ }
282
+
283
+ .compare-errors {
284
+ margin-top: 16px;
285
+ background: rgba(255, 225, 215, 0.9);
286
+ color: #84230e;
287
+ }
288
+
289
+ .compare-errors p {
290
+ margin: 0 0 6px;
291
+ }
292
+
293
+ @media (max-width: 880px) {
294
+ .main-shell {
295
+ padding-top: 88px;
296
+ }
297
+
298
+ .strategy-control,
299
+ .results-section.compare-layout,
300
+ .score-grid {
301
+ grid-template-columns: 1fr;
302
+ }
303
+
304
+ .strategy-button {
305
+ text-align: center;
306
+ }
307
+
308
+ .result-card-header {
309
+ align-items: flex-start;
310
+ flex-direction: column;
311
+ }
312
+ }