RafzE commited on
Commit
f7d6571
·
verified ·
1 Parent(s): ca5755e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -105
app.py CHANGED
@@ -1,21 +1,26 @@
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
- from pydantic import BaseModel
4
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
5
  import torch
6
  import logging
7
  from typing import Optional, List
8
  import time
 
9
 
10
  # ---------------- Logging ----------------
11
- logging.basicConfig(level=logging.DEBUG) # Changed to DEBUG
 
 
 
 
12
  logger = logging.getLogger("detector")
13
 
14
  # ---------------- FastAPI ----------------
15
  app = FastAPI(
16
  title="Detextly AI Detector API",
17
  description="AI Detector with chunked scoring and low-confidence filter",
18
- version="2.0.1" # Updated version
19
  )
20
 
21
  # CORS
@@ -30,8 +35,21 @@ app.add_middleware(
30
  # ---------------- Models ----------------
31
  class ScanRequest(BaseModel):
32
  text: str
33
- scan_type: str = "basic"
 
 
34
  userId: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  class ScanResponse(BaseModel):
37
  success: bool
@@ -48,8 +66,8 @@ class AIDetector:
48
  self.model = None
49
  self.tokenizer = None
50
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
51
  logger.info(f"Using device: {self.device}")
52
- self.label_map = None # Store label mapping
53
 
54
  def load_model(self):
55
  if self.model is not None:
@@ -59,27 +77,27 @@ class AIDetector:
59
  self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
60
  self.model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
61
 
62
- # Check model configuration
63
- logger.info(f"Model config: {self.model.config}")
64
- logger.info(f"Model has {self.model.config.num_labels} labels")
65
-
66
- # Try to get label mapping
67
  if hasattr(self.model.config, 'id2label'):
68
  self.label_map = self.model.config.id2label
69
- logger.info(f"Label mapping: {self.label_map}")
70
-
 
 
71
  except Exception as e:
72
  logger.error(f"Error loading model: {e}")
73
- raise
 
74
  self.model.to(self.device)
75
  self.model.eval()
76
  logger.info("Model loaded successfully.")
77
 
78
- def predict(self, text: str, max_length: int = 512) -> float:
79
- """Return HUMAN probability"""
80
  if self.model is None:
81
  self.load_model()
82
 
 
83
  tokens = self.tokenizer(
84
  text,
85
  return_tensors="pt",
@@ -91,34 +109,33 @@ class AIDetector:
91
 
92
  with torch.no_grad():
93
  outputs = self.model(**tokens)
94
-
95
- # Get all probabilities
96
  probs = torch.softmax(outputs.logits, dim=-1)
97
 
 
 
 
 
98
  # Debug logging
99
  logger.debug(f"Raw probabilities: {probs}")
 
 
100
 
101
- # Check label mapping to understand what each class means
102
- if self.label_map:
103
- for i, prob in enumerate(probs[0]):
104
- logger.debug(f"Class {i} ({self.label_map.get(i, f'Unknown {i}')}): {prob:.4f}")
105
-
106
- # IMPORTANT: Based on typical AI detectors:
107
- # Class 0 = Human, Class 1 = AI
108
- # But let's verify by testing with known text
109
 
110
- # Return HUMAN probability (class 0)
111
- human_prob = float(probs[0][0].item())
112
- ai_prob = float(probs[0][1].item())
113
-
114
- logger.info(f"Human: {human_prob:.2%}, AI: {ai_prob:.2%}")
115
-
116
- return human_prob # Return human probability
117
 
118
  detector = AIDetector()
119
 
120
  # ---------------- ChatGPT Pattern Detection ----------------
121
- def detect_chatgpt_patterns(text: str) -> float:
 
122
  patterns = [
123
  "as an ai language model",
124
  "i am an ai model",
@@ -128,74 +145,109 @@ def detect_chatgpt_patterns(text: str) -> float:
128
  "my training data",
129
  "i don't have personal experiences",
130
  "i don't have feelings",
 
 
131
  ]
132
  lower = text.lower()
133
- found = any(p in lower for p in patterns)
134
- return 0.95 if found else 0.0
 
 
 
135
 
136
  # ---------------- Highlight / Chunked Scan ----------------
137
  def analyze_sections(text: str, chunk_size: int = 40) -> List[dict]:
138
- """Split text into smaller chunks and compute probabilities for each."""
139
  sections = []
140
  words = text.split()
 
141
 
142
- logger.info(f"Analyzing {len(words)} words in {len(words)//chunk_size + 1} chunks")
143
 
144
  for i in range(0, len(words), chunk_size):
145
  chunk = " ".join(words[i:i+chunk_size])
146
  if len(chunk.strip()) < 20:
147
  continue
148
 
149
- # Get HUMAN probability
150
- human_prob = detector.predict(chunk)
151
- ai_prob = 1 - human_prob
 
152
 
153
- pattern_score = detect_chatgpt_patterns(chunk)
154
- if pattern_score > 0:
155
- ai_prob = max(ai_prob, pattern_score)
 
156
  human_prob = 1 - ai_prob
157
 
158
  sections.append({
159
- "text": chunk[:150] + "..." if len(chunk) > 150 else chunk,
160
- "ai_probability": ai_prob,
161
- "human_probability": human_prob,
162
- "words": len(chunk.split())
 
163
  })
164
-
165
- logger.debug(f"Chunk {len(sections)}: AI={ai_prob:.2%}, Human={human_prob:.2%}")
166
 
 
167
  return sections
168
 
169
- def compute_overall_score(sections: List[dict]) -> float:
170
- """Weighted average of section AI probabilities"""
171
  if not sections:
172
- return 0.0
173
 
174
- total_words = sum(s["words"] for s in sections)
175
- weighted_sum = sum(s["ai_probability"] * s["words"] for s in sections)
 
 
 
 
 
176
 
177
- return weighted_sum / total_words
178
-
179
- # ---------------- Test with Sample Text ----------------
180
- def test_model():
181
- """Test the model with known human and AI text samples"""
182
- test_human = "I went to the store yesterday to buy some groceries. The weather was nice so I walked. I bought apples, bread, and milk."
183
- test_ai = "As an AI language model, I don't have personal experiences or emotions. Based on my training data, I can provide information on various topics."
 
 
 
 
 
184
 
185
- logger.info("Testing with human text...")
186
- human_prob = detector.predict(test_human)
187
- logger.info(f"Human text score: Human={human_prob:.2%}, AI={1-human_prob:.2%}")
188
 
189
- logger.info("Testing with AI text...")
190
- ai_prob = detector.predict(test_ai)
191
- logger.info(f"AI text score: Human={ai_prob:.2%}, AI={1-ai_prob:.2%}")
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
  # ---------------- Endpoints ----------------
194
  @app.on_event("startup")
195
  async def startup():
196
- detector.load_model()
197
- # Run test to verify model works correctly
198
- test_model()
 
 
 
 
 
199
 
200
  @app.get("/")
201
  async def root():
@@ -203,60 +255,128 @@ async def root():
203
  "status": "online",
204
  "model": MODEL_NAME,
205
  "device": str(detector.device),
206
- "version": "2.0.1",
207
- "note": "Now returning HUMAN probability as primary score"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  }
209
 
210
  @app.post("/api/scan", response_model=ScanResponse)
211
- async def scan_text(req: ScanRequest):
 
212
  start_time = time.time()
 
213
  try:
214
- if not req.text or len(req.text.strip()) < 10:
215
- raise HTTPException(status_code=400, detail="Text too short.")
216
-
217
- text = req.text[:5000]
218
 
219
- # Get HUMAN probability
220
- human_prob = detector.predict(text)
221
- ai_prob = 1 - human_prob
222
 
223
- pattern_prob = detect_chatgpt_patterns(text)
224
- if pattern_prob > ai_prob:
225
- ai_prob = pattern_prob
226
- human_prob = 1 - ai_prob
227
 
228
- if req.scan_type == "highlight":
 
 
 
 
229
  sections = analyze_sections(text, chunk_size=40)
230
- overall_ai_score = compute_overall_score(sections)
231
- overall_human_score = 1 - overall_ai_score
232
 
233
- ai_sections = [s for s in sections if s["ai_probability"] > 0.5]
 
 
 
 
 
 
 
 
 
234
 
235
  result = {
236
- "overall": overall_human_score, # Human probability
237
- "ai_probability": overall_ai_score, # Explicit AI probability
238
- "human_probability": overall_human_score,
239
  "model": MODEL_NAME,
240
- "confidence": "high" if overall_human_score > 0.75 or overall_human_score < 0.25 else "medium",
241
- "chatgpt_detected": pattern_prob > 0,
242
  "scan_type": "highlight",
243
  "section_count": len(sections),
244
- "ai_sections": ai_sections,
245
- "label_mapping": detector.label_map # Include for debugging
 
 
246
  }
247
- else: # Basic scan
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  result = {
249
- "overall": human_prob, # Human probability
250
- "ai_probability": ai_prob, # Explicit AI probability
251
  "human_probability": human_prob,
252
  "model": MODEL_NAME,
253
- "confidence": "high" if human_prob > 0.75 or human_prob < 0.25 else "medium",
254
- "chatgpt_detected": pattern_prob > 0,
255
- "scan_type": "basic",
256
- "label_mapping": detector.label_map # Include for debugging
257
  }
258
-
 
259
  processing_time = int((time.time() - start_time) * 1000)
 
 
260
  return ScanResponse(
261
  success=True,
262
  result=result,
@@ -269,10 +389,31 @@ async def scan_text(req: ScanRequest):
269
  },
270
  test_mode=False
271
  )
 
 
 
272
  except Exception as e:
273
  logger.error(f"Scan error: {e}", exc_info=True)
274
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
275
 
 
276
  if __name__ == "__main__":
277
  import uvicorn
278
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="debug")
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel, validator
4
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
5
  import torch
6
  import logging
7
  from typing import Optional, List
8
  import time
9
+ import sys
10
 
11
  # ---------------- Logging ----------------
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
15
+ stream=sys.stdout
16
+ )
17
  logger = logging.getLogger("detector")
18
 
19
  # ---------------- FastAPI ----------------
20
  app = FastAPI(
21
  title="Detextly AI Detector API",
22
  description="AI Detector with chunked scoring and low-confidence filter",
23
+ version="2.1.0"
24
  )
25
 
26
  # CORS
 
35
  # ---------------- Models ----------------
36
  class ScanRequest(BaseModel):
37
  text: str
38
+ # Accept both scan_type and scanType
39
+ scan_type: Optional[str] = None
40
+ scanType: Optional[str] = None
41
  userId: Optional[str] = None
42
+
43
+ @validator('scan_type', 'scanType', pre=True, always=True)
44
+ def determine_scan_type(cls, v, values, field):
45
+ if field.name == 'scanType' and v:
46
+ # Map scanType to scan_type for internal use
47
+ values['scan_type'] = v
48
+ return v
49
+
50
+ def get_scan_type(self) -> str:
51
+ """Get the scan type, defaulting to 'basic' if not provided"""
52
+ return self.scan_type or "basic"
53
 
54
  class ScanResponse(BaseModel):
55
  success: bool
 
66
  self.model = None
67
  self.tokenizer = None
68
  self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
69
+ self.label_map = None
70
  logger.info(f"Using device: {self.device}")
 
71
 
72
  def load_model(self):
73
  if self.model is not None:
 
77
  self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
78
  self.model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
79
 
80
+ # Store label mapping for debugging
 
 
 
 
81
  if hasattr(self.model.config, 'id2label'):
82
  self.label_map = self.model.config.id2label
83
+ logger.info(f"Model label mapping: {self.label_map}")
84
+ else:
85
+ logger.warning("No label mapping found in model config")
86
+
87
  except Exception as e:
88
  logger.error(f"Error loading model: {e}")
89
+ raise RuntimeError(f"Failed to load model: {e}")
90
+
91
  self.model.to(self.device)
92
  self.model.eval()
93
  logger.info("Model loaded successfully.")
94
 
95
+ def predict(self, text: str, max_length: int = 512) -> dict:
96
+ """Return both human and AI probabilities with debugging info"""
97
  if self.model is None:
98
  self.load_model()
99
 
100
+ # Tokenize input
101
  tokens = self.tokenizer(
102
  text,
103
  return_tensors="pt",
 
109
 
110
  with torch.no_grad():
111
  outputs = self.model(**tokens)
 
 
112
  probs = torch.softmax(outputs.logits, dim=-1)
113
 
114
+ # Get probabilities for both classes
115
+ human_prob = float(probs[0][0].item()) # Class 0
116
+ ai_prob = float(probs[0][1].item()) # Class 1
117
+
118
  # Debug logging
119
  logger.debug(f"Raw probabilities: {probs}")
120
+ logger.debug(f"Class 0 (Human): {human_prob:.4f}")
121
+ logger.debug(f"Class 1 (AI): {ai_prob:.4f}")
122
 
123
+ # Verify probabilities sum to ~1.0
124
+ total = human_prob + ai_prob
125
+ if abs(total - 1.0) > 0.01:
126
+ logger.warning(f"Probabilities don't sum to 1.0: {total:.4f}")
 
 
 
 
127
 
128
+ return {
129
+ "human_probability": human_prob,
130
+ "ai_probability": ai_prob,
131
+ "raw_probs": probs.tolist()
132
+ }
 
 
133
 
134
  detector = AIDetector()
135
 
136
  # ---------------- ChatGPT Pattern Detection ----------------
137
+ def detect_chatgpt_patterns(text: str) -> bool:
138
+ """Return True if ChatGPT patterns are detected"""
139
  patterns = [
140
  "as an ai language model",
141
  "i am an ai model",
 
145
  "my training data",
146
  "i don't have personal experiences",
147
  "i don't have feelings",
148
+ "as an artificial intelligence",
149
+ "i don't have personal opinions"
150
  ]
151
  lower = text.lower()
152
+ for pattern in patterns:
153
+ if pattern in lower:
154
+ logger.debug(f"ChatGPT pattern detected: {pattern}")
155
+ return True
156
+ return False
157
 
158
  # ---------------- Highlight / Chunked Scan ----------------
159
  def analyze_sections(text: str, chunk_size: int = 40) -> List[dict]:
160
+ """Split text into smaller chunks and compute AI probability for each."""
161
  sections = []
162
  words = text.split()
163
+ total_chunks = (len(words) + chunk_size - 1) // chunk_size
164
 
165
+ logger.info(f"Analyzing {len(words)} words in {total_chunks} chunks")
166
 
167
  for i in range(0, len(words), chunk_size):
168
  chunk = " ".join(words[i:i+chunk_size])
169
  if len(chunk.strip()) < 20:
170
  continue
171
 
172
+ # Get probabilities from model
173
+ probs = detector.predict(chunk)
174
+ human_prob = probs["human_probability"]
175
+ ai_prob = probs["ai_probability"]
176
 
177
+ # Check for ChatGPT patterns
178
+ has_pattern = detect_chatgpt_patterns(chunk)
179
+ if has_pattern:
180
+ ai_prob = max(ai_prob, 0.9) # Boost AI probability if pattern found
181
  human_prob = 1 - ai_prob
182
 
183
  sections.append({
184
+ "text": chunk[:200] + "..." if len(chunk) > 200 else chunk,
185
+ "ai_probability": round(ai_prob, 4),
186
+ "human_probability": round(human_prob, 4),
187
+ "words": len(chunk.split()),
188
+ "has_chatgpt_pattern": has_pattern
189
  })
 
 
190
 
191
+ logger.info(f"Generated {len(sections)} sections for analysis")
192
  return sections
193
 
194
+ def compute_overall_score(sections: List[dict], confidence_threshold: float = 0.3) -> dict:
195
+ """Compute weighted average probabilities with confidence filtering."""
196
  if not sections:
197
+ return {"ai_probability": 0.0, "human_probability": 1.0, "confidence": "low"}
198
 
199
+ # Filter out low-confidence predictions (close to 0.5)
200
+ confident_sections = []
201
+ for section in sections:
202
+ ai_prob = section["ai_probability"]
203
+ confidence = abs(ai_prob - 0.5) # Distance from uncertain (0.5)
204
+ if confidence >= confidence_threshold:
205
+ confident_sections.append(section)
206
 
207
+ if not confident_sections:
208
+ # If no confident sections, use all sections
209
+ confident_sections = sections
210
+
211
+ # Weighted average by word count
212
+ total_words = sum(s["words"] for s in confident_sections)
213
+
214
+ if total_words == 0:
215
+ return {"ai_probability": 0.5, "human_probability": 0.5, "confidence": "low"}
216
+
217
+ weighted_ai_sum = sum(s["ai_probability"] * s["words"] for s in confident_sections)
218
+ weighted_human_sum = sum(s["human_probability"] * s["words"] for s in confident_sections)
219
 
220
+ overall_ai = weighted_ai_sum / total_words
221
+ overall_human = weighted_human_sum / total_words
 
222
 
223
+ # Determine confidence level
224
+ distance_from_mid = abs(overall_ai - 0.5)
225
+ if distance_from_mid > 0.4:
226
+ confidence_level = "high"
227
+ elif distance_from_mid > 0.2:
228
+ confidence_level = "medium"
229
+ else:
230
+ confidence_level = "low"
231
+
232
+ return {
233
+ "ai_probability": round(overall_ai, 4),
234
+ "human_probability": round(overall_human, 4),
235
+ "confidence": confidence_level,
236
+ "sections_analyzed": len(sections),
237
+ "confident_sections": len(confident_sections)
238
+ }
239
 
240
  # ---------------- Endpoints ----------------
241
  @app.on_event("startup")
242
  async def startup():
243
+ """Initialize the model on startup"""
244
+ logger.info("Starting Detextly AI Detector API...")
245
+ try:
246
+ detector.load_model()
247
+ logger.info("API startup complete")
248
+ except Exception as e:
249
+ logger.error(f"Failed to start API: {e}")
250
+ raise
251
 
252
  @app.get("/")
253
  async def root():
 
255
  "status": "online",
256
  "model": MODEL_NAME,
257
  "device": str(detector.device),
258
+ "version": "2.1.0",
259
+ "features": ["basic_scan", "highlight_scan", "chatgpt_pattern_detection"],
260
+ "endpoints": ["POST /api/scan", "GET /health", "GET /debug/test"]
261
+ }
262
+
263
+ @app.get("/health")
264
+ async def health():
265
+ health_status = {
266
+ "status": "healthy",
267
+ "model_loaded": detector.model is not None,
268
+ "model": MODEL_NAME,
269
+ "timestamp": time.time()
270
+ }
271
+ return health_status
272
+
273
+ @app.get("/debug/test")
274
+ async def debug_test():
275
+ """Test endpoint to verify model is working correctly"""
276
+ test_texts = [
277
+ "I went to the store yesterday to buy groceries.",
278
+ "As an AI language model, I don't have personal experiences.",
279
+ "The quick brown fox jumps over the lazy dog."
280
+ ]
281
+
282
+ results = []
283
+ for text in test_texts:
284
+ probs = detector.predict(text)
285
+ results.append({
286
+ "text": text[:50] + "..." if len(text) > 50 else text,
287
+ "human_probability": probs["human_probability"],
288
+ "ai_probability": probs["ai_probability"]
289
+ })
290
+
291
+ return {
292
+ "test_results": results,
293
+ "model_info": {
294
+ "name": MODEL_NAME,
295
+ "labels": detector.label_map,
296
+ "device": str(detector.device)
297
+ }
298
  }
299
 
300
  @app.post("/api/scan", response_model=ScanResponse)
301
+ async def scan_text(request: ScanRequest):
302
+ """Main scanning endpoint"""
303
  start_time = time.time()
304
+
305
  try:
306
+ # Validate input
307
+ if not request.text or len(request.text.strip()) < 10:
308
+ raise HTTPException(status_code=400, detail="Text must be at least 10 characters long.")
 
309
 
310
+ # Get scan type (handles both scan_type and scanType)
311
+ scan_type = request.get_scan_type()
312
+ logger.info(f"Scan request: type={scan_type}, userId={request.userId}, text_length={len(request.text)}")
313
 
314
+ # Limit text length for performance
315
+ text = request.text[:5000]
 
 
316
 
317
+ # Check for ChatGPT patterns
318
+ chatgpt_detected = detect_chatgpt_patterns(text)
319
+
320
+ if scan_type == "highlight":
321
+ # Chunked analysis
322
  sections = analyze_sections(text, chunk_size=40)
323
+ overall = compute_overall_score(sections)
 
324
 
325
+ # Identify AI-heavy sections
326
+ ai_sections = [
327
+ {
328
+ "text": s["text"],
329
+ "ai_probability": s["ai_probability"],
330
+ "human_probability": s["human_probability"],
331
+ "words": s["words"]
332
+ }
333
+ for s in sections if s["ai_probability"] > 0.6
334
+ ]
335
 
336
  result = {
337
+ "overall": overall["human_probability"], # Human probability for backward compatibility
338
+ "ai_probability": overall["ai_probability"],
339
+ "human_probability": overall["human_probability"],
340
  "model": MODEL_NAME,
341
+ "confidence": overall["confidence"],
342
+ "chatgpt_detected": chatgpt_detected,
343
  "scan_type": "highlight",
344
  "section_count": len(sections),
345
+ "ai_section_count": len(ai_sections),
346
+ "sections_analyzed": overall["sections_analyzed"],
347
+ "confident_sections": overall["confident_sections"],
348
+ "ai_sections": ai_sections[:10] # Limit to first 10
349
  }
350
+
351
+ else:
352
+ # Basic scan (single analysis)
353
+ probs = detector.predict(text)
354
+ human_prob = probs["human_probability"]
355
+ ai_prob = probs["ai_probability"]
356
+
357
+ # Boost AI probability if ChatGPT patterns detected
358
+ if chatgpt_detected:
359
+ ai_prob = max(ai_prob, 0.9)
360
+ human_prob = 1 - ai_prob
361
+
362
+ # Determine confidence
363
+ distance_from_mid = abs(ai_prob - 0.5)
364
+ confidence = "high" if distance_from_mid > 0.4 else "medium" if distance_from_mid > 0.2 else "low"
365
+
366
  result = {
367
+ "overall": human_prob, # Human probability for backward compatibility
368
+ "ai_probability": ai_prob,
369
  "human_probability": human_prob,
370
  "model": MODEL_NAME,
371
+ "confidence": confidence,
372
+ "chatgpt_detected": chatgpt_detected,
373
+ "scan_type": "basic"
 
374
  }
375
+
376
+ # Calculate processing time
377
  processing_time = int((time.time() - start_time) * 1000)
378
+ logger.info(f"Scan completed in {processing_time}ms: AI={result.get('ai_probability', 0):.2%}")
379
+
380
  return ScanResponse(
381
  success=True,
382
  result=result,
 
389
  },
390
  test_mode=False
391
  )
392
+
393
+ except HTTPException:
394
+ raise
395
  except Exception as e:
396
  logger.error(f"Scan error: {e}", exc_info=True)
397
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
398
+
399
+ @app.get("/api/credits")
400
+ async def get_credits(userId: Optional[str] = None):
401
+ """Get credits information (for compatibility with worker)"""
402
+ return {
403
+ "basic": 5,
404
+ "highlight": 1,
405
+ "resetTime": "2024-12-31T23:59:59Z",
406
+ "test_mode": False,
407
+ "userId": userId or "unknown"
408
+ }
409
 
410
+ # ---------------- Main Entry Point ----------------
411
  if __name__ == "__main__":
412
  import uvicorn
413
+ uvicorn.run(
414
+ app,
415
+ host="0.0.0.0",
416
+ port=7860,
417
+ log_level="info",
418
+ access_log=True
419
+ )