gaidasalsaa commited on
Commit
b38b1f8
·
1 Parent(s): c173783

Fixed app and requirements

Browse files
Files changed (3) hide show
  1. README.md +8 -9
  2. app.py +187 -59
  3. requirements.txt +7 -7
README.md CHANGED
@@ -1,13 +1,12 @@
1
  ---
2
- title: Xstress Api
3
- emoji: 📈
4
- colorFrom: green
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 6.0.0
8
- app_file: app.py
9
  pinned: false
10
- license: mit
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: Stress Detection API
3
+ emoji: 🧠
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
 
 
7
  pinned: false
 
8
  ---
9
 
10
+ # Stress Detection API
11
+
12
+ Deteksi tingkat stress dari postingan Twitter menggunakan IndoBERTweet.
app.py CHANGED
@@ -3,8 +3,10 @@ from pydantic import BaseModel
3
  from typing import Optional
4
  import requests
5
  import torch
6
- from transformers import AutoTokenizer, AutoModelForSequenceClassification, BertForSequenceClassification
7
  from huggingface_hub import hf_hub_download
 
 
8
 
9
  # -----------------------------
10
  # CONFIG
@@ -13,38 +15,13 @@ HF_MODEL_REPO = "gaidasalsaa/indobertweet-xstress-model"
13
  BASE_MODEL = "indolem/indobertweet-base-uncased"
14
  PT_FILE = "best_indobertweet.pth"
15
 
16
- BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAOGp3AEAAAAAMEaOafsh1pNGVFrK%2BN2atq0Cba4%3DE2Gw0MDFfJ1bE4veBIIxhOUqbaqQKOqRxMhGybH4FfOETDNpow"
17
-
18
- # -----------------------------
19
- # LOAD MODEL
20
- # -----------------------------
21
- device = "cuda" if torch.cuda.is_available() else "cpu"
22
-
23
- # Load tokenizer dari base model (AMAN)
24
- tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
25
-
26
- # Download file .pth dari HuggingFace
27
- model_path = hf_hub_download(
28
- repo_id=HF_MODEL_REPO,
29
- filename=PT_FILE
30
- )
31
-
32
- # Load base model dulu
33
- model = BertForSequenceClassification.from_pretrained(
34
- "indolem/indobertweet-base-uncased",
35
- num_labels=2
36
  )
37
 
38
- # Load weight fine-tuned kamu
39
- state_dict = torch.load(model_path, map_location=device)
40
- model.load_state_dict(state_dict, strict=True)
41
-
42
- model.to(device)
43
- model.eval()
44
-
45
-
46
  # -----------------------------
47
- # FASTAPI
48
  # -----------------------------
49
  app = FastAPI(
50
  title="Stress Detection API",
@@ -52,74 +29,225 @@ app = FastAPI(
52
  version="1.0.0"
53
  )
54
 
 
 
 
 
 
55
  class StressResponse(BaseModel):
56
  message: str
57
  data: Optional[dict] = None
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def get_user_id(username):
 
61
  url = f"https://api.x.com/2/users/by/username/{username}"
62
  headers = {"Authorization": f"Bearer {BEARER_TOKEN}"}
63
- r = requests.get(url, headers=headers)
64
- if r.status_code != 200:
65
- return None, r.json()
66
- return r.json()["data"]["id"], r.json()
 
 
 
67
 
68
 
69
  def fetch_tweets(user_id, limit=25):
 
70
  url = f"https://api.x.com/2/users/{user_id}/tweets"
71
  params = {"max_results": limit, "tweet.fields": "id,text,created_at"}
72
  headers = {"Authorization": f"Bearer {BEARER_TOKEN}"}
73
- r = requests.get(url, headers=headers, params=params)
74
- if r.status_code != 200:
75
- return None, r.json()
76
- tweets = r.json().get("data", [])
77
- return [t["text"] for t in tweets], r.json()
 
 
 
78
 
79
 
80
  def predict_stress(text):
81
- inputs = tokenizer(text, return_tensors="pt",
82
- truncation=True, padding=True,
83
- max_length=128).to(device)
 
 
 
 
 
 
84
  with torch.no_grad():
85
  outputs = model(**inputs)
86
  probs = torch.softmax(outputs.logits, dim=1)[0]
 
87
  label = torch.argmax(probs).item()
88
  return label, float(probs[1])
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  @app.get("/analyze/{username}", response_model=StressResponse)
92
  def analyze(username: str):
93
- user_id, _ = get_user_id(username)
94
- if user_id is None:
95
- return StressResponse(message="Failed to fetch user profile.", data=None)
96
-
97
- tweets, _ = fetch_tweets(user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  if not tweets:
99
- return StressResponse(message="User protected or has no tweets.", data=None)
100
-
 
 
 
 
101
  labels = []
102
- for t in tweets:
103
- label, _ = predict_stress(t)
104
- labels.append(label)
105
-
 
 
 
 
 
 
 
 
 
 
 
106
  stress_percentage = round(sum(labels) / len(labels) * 100, 2)
107
-
 
108
  if stress_percentage <= 25:
109
- status = 0
110
  elif stress_percentage <= 50:
111
- status = 1
112
  elif stress_percentage <= 75:
113
- status = 2
114
  else:
115
- status = 3
116
-
117
  return StressResponse(
118
  message="Analysis successful",
119
  data={
120
  "username": username,
121
  "total_tweets": len(tweets),
 
122
  "stress_level": stress_percentage,
123
  "stress_status": status
124
  }
125
  )
 
 
 
 
 
 
 
 
 
3
  from typing import Optional
4
  import requests
5
  import torch
6
+ from transformers import AutoTokenizer, BertForSequenceClassification
7
  from huggingface_hub import hf_hub_download
8
+ import os
9
+ import gc
10
 
11
  # -----------------------------
12
  # CONFIG
 
15
  BASE_MODEL = "indolem/indobertweet-base-uncased"
16
  PT_FILE = "best_indobertweet.pth"
17
 
18
+ BEARER_TOKEN = os.getenv(
19
+ "TWITTER_BEARER_TOKEN",
20
+ "AAAAAAAAAAAAAAAAAAAAAOGp3AEAAAAAMEaOafsh1pNGVFrK%2BN2atq0Cba4%3DE2Gw0MDFfJ1bE4veBIIxhOUqbaqQKOqRxMhGybH4FfOETDNpow"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  )
22
 
 
 
 
 
 
 
 
 
23
  # -----------------------------
24
+ # FASTAPI (Initialize FIRST)
25
  # -----------------------------
26
  app = FastAPI(
27
  title="Stress Detection API",
 
29
  version="1.0.0"
30
  )
31
 
32
+ # Global variables untuk lazy loading
33
+ model = None
34
+ tokenizer = None
35
+ device = None
36
+
37
  class StressResponse(BaseModel):
38
  message: str
39
  data: Optional[dict] = None
40
 
41
 
42
+ # -----------------------------
43
+ # LAZY LOAD MODEL
44
+ # -----------------------------
45
+ def load_model_once():
46
+ """Load model hanya sekali saat pertama kali dipanggil"""
47
+ global model, tokenizer, device
48
+
49
+ if model is not None:
50
+ return # Sudah di-load
51
+
52
+ print("Loading model (first time only)...")
53
+
54
+ # Set device
55
+ device = "cuda" if torch.cuda.is_available() else "cpu"
56
+ print(f"Using device: {device}")
57
+
58
+ # 1. Load tokenizer
59
+ print("Loading tokenizer...")
60
+ tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
61
+
62
+ # 2. Download .pth file
63
+ print(f"⬇Downloading {PT_FILE}...")
64
+ model_path = hf_hub_download(
65
+ repo_id=HF_MODEL_REPO,
66
+ filename=PT_FILE
67
+ )
68
+ print(f"Downloaded to: {model_path}")
69
+
70
+ # 3. Load base model dengan optimasi memory
71
+ print("Loading base model...")
72
+ model = BertForSequenceClassification.from_pretrained(
73
+ BASE_MODEL,
74
+ num_labels=2,
75
+ low_cpu_mem_usage=True,
76
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32
77
+ )
78
+
79
+ # 4. Load fine-tuned weights
80
+ print("Loading fine-tuned weights...")
81
+ state_dict = torch.load(model_path, map_location=device)
82
+ model.load_state_dict(state_dict, strict=False)
83
+
84
+ # 5. Move to device dan set eval mode
85
+ model.to(device)
86
+ model.eval()
87
+
88
+ # 6. Clear cache
89
+ gc.collect()
90
+ if device == "cuda":
91
+ torch.cuda.empty_cache()
92
+
93
+ print("Model loaded successfully!")
94
+
95
+
96
+ # -----------------------------
97
+ # HELPER FUNCTIONS
98
+ # -----------------------------
99
  def get_user_id(username):
100
+ """Get Twitter user ID from username"""
101
  url = f"https://api.x.com/2/users/by/username/{username}"
102
  headers = {"Authorization": f"Bearer {BEARER_TOKEN}"}
103
+
104
+ try:
105
+ r = requests.get(url, headers=headers, timeout=10)
106
+ r.raise_for_status()
107
+ return r.json()["data"]["id"], None
108
+ except Exception as e:
109
+ return None, {"error": str(e)}
110
 
111
 
112
  def fetch_tweets(user_id, limit=25):
113
+ """Fetch user's recent tweets"""
114
  url = f"https://api.x.com/2/users/{user_id}/tweets"
115
  params = {"max_results": limit, "tweet.fields": "id,text,created_at"}
116
  headers = {"Authorization": f"Bearer {BEARER_TOKEN}"}
117
+
118
+ try:
119
+ r = requests.get(url, headers=headers, params=params, timeout=10)
120
+ r.raise_for_status()
121
+ tweets = r.json().get("data", [])
122
+ return [t["text"] for t in tweets], None
123
+ except Exception as e:
124
+ return None, {"error": str(e)}
125
 
126
 
127
  def predict_stress(text):
128
+ """Predict stress level from text"""
129
+ inputs = tokenizer(
130
+ text,
131
+ return_tensors="pt",
132
+ truncation=True,
133
+ padding=True,
134
+ max_length=128
135
+ ).to(device)
136
+
137
  with torch.no_grad():
138
  outputs = model(**inputs)
139
  probs = torch.softmax(outputs.logits, dim=1)[0]
140
+
141
  label = torch.argmax(probs).item()
142
  return label, float(probs[1])
143
 
144
 
145
+ # -----------------------------
146
+ # API ENDPOINTS
147
+ # -----------------------------
148
+ @app.on_event("startup")
149
+ async def startup_event():
150
+ """Load model saat aplikasi start"""
151
+ print("Starting application...")
152
+ load_model_once()
153
+ print("Application ready!")
154
+
155
+
156
+ @app.get("/")
157
+ def root():
158
+ """Health check endpoint"""
159
+ return {
160
+ "status": "online",
161
+ "message": "Stress Detection API is running",
162
+ "model_loaded": model is not None
163
+ }
164
+
165
+
166
+ @app.get("/health")
167
+ def health():
168
+ """Detailed health check"""
169
+ return {
170
+ "status": "healthy",
171
+ "model_loaded": model is not None,
172
+ "device": str(device) if device else "not loaded",
173
+ "tokenizer_loaded": tokenizer is not None
174
+ }
175
+
176
+
177
  @app.get("/analyze/{username}", response_model=StressResponse)
178
  def analyze(username: str):
179
+ """Analyze stress level from user's tweets"""
180
+
181
+ # Pastikan model sudah loaded
182
+ if model is None:
183
+ load_model_once()
184
+
185
+ # 1. Get user ID
186
+ user_id, error = get_user_id(username)
187
+ if error:
188
+ return StressResponse(
189
+ message=f"Failed to fetch user profile: {error.get('error', 'Unknown error')}",
190
+ data=None
191
+ )
192
+
193
+ # 2. Fetch tweets
194
+ tweets, error = fetch_tweets(user_id)
195
+ if error:
196
+ return StressResponse(
197
+ message=f"Failed to fetch tweets: {error.get('error', 'Unknown error')}",
198
+ data=None
199
+ )
200
+
201
  if not tweets:
202
+ return StressResponse(
203
+ message="User has no tweets or account is protected.",
204
+ data=None
205
+ )
206
+
207
+ # 3. Predict stress for each tweet
208
  labels = []
209
+ for tweet in tweets:
210
+ try:
211
+ label, _ = predict_stress(tweet)
212
+ labels.append(label)
213
+ except Exception as e:
214
+ print(f"Skipping tweet due to error: {e}")
215
+ continue
216
+
217
+ if not labels:
218
+ return StressResponse(
219
+ message="Failed to analyze tweets.",
220
+ data=None
221
+ )
222
+
223
+ # 4. Calculate stress statistics
224
  stress_percentage = round(sum(labels) / len(labels) * 100, 2)
225
+
226
+ # Determine stress status
227
  if stress_percentage <= 25:
228
+ status = 0 # Low
229
  elif stress_percentage <= 50:
230
+ status = 1 # Medium
231
  elif stress_percentage <= 75:
232
+ status = 2 # High
233
  else:
234
+ status = 3 # Very High
235
+
236
  return StressResponse(
237
  message="Analysis successful",
238
  data={
239
  "username": username,
240
  "total_tweets": len(tweets),
241
+ "analyzed_tweets": len(labels),
242
  "stress_level": stress_percentage,
243
  "stress_status": status
244
  }
245
  )
246
+
247
+
248
+ # -----------------------------
249
+ # RUN (untuk local testing)
250
+ # -----------------------------
251
+ if __name__ == "__main__":
252
+ import uvicorn
253
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,7 +1,7 @@
1
- numpy<2
2
- torch==2.2.2
3
- transformers==4.39.3
4
- accelerate
5
- gradio
6
- uvicorn
7
- fastapi
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ torch==2.1.0
4
+ transformers==4.35.0
5
+ huggingface_hub==0.19.4
6
+ requests==2.31.0
7
+ pydantic==2.5.0