Spaces:
Sleeping
Sleeping
Initial backend files
Browse files- Dockerfile +45 -0
- database.py +98 -0
- main.py +184 -0
- nlp_model.py +74 -0
- requirements.txt +8 -0
Dockerfile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile
|
| 2 |
+
|
| 3 |
+
# --- STAGE 1: Build & Install Dependencies ---
|
| 4 |
+
# Use a Python base image suitable for slim deployments
|
| 5 |
+
FROM python:3.11-slim AS builder
|
| 6 |
+
|
| 7 |
+
# Set working directory inside the container
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# Install system dependencies needed for some Python packages (like psycopg2 for Postgres)
|
| 11 |
+
# We include curl and build-essential just in case, which is good practice.
|
| 12 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 13 |
+
build-essential \
|
| 14 |
+
libpq-dev \
|
| 15 |
+
curl \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
# Copy the requirements file
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
|
| 21 |
+
# Install Python dependencies (transformers, torch, fastapi, pandas, etc.)
|
| 22 |
+
# --no-cache-dir reduces image size
|
| 23 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 24 |
+
|
| 25 |
+
# --- STAGE 2: Final Runtime Image ---
|
| 26 |
+
# Use the same slim base image for the final production environment
|
| 27 |
+
FROM python:3.11-slim AS runtime
|
| 28 |
+
|
| 29 |
+
# Set working directory
|
| 30 |
+
WORKDIR /app
|
| 31 |
+
|
| 32 |
+
# Copy only the installed Python packages from the builder stage
|
| 33 |
+
# This saves space by not including build dependencies like build-essential
|
| 34 |
+
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
|
| 35 |
+
|
| 36 |
+
# Copy application code (main.py, nlp_model.py, database.py)
|
| 37 |
+
COPY ./backend /app/
|
| 38 |
+
|
| 39 |
+
# IMPORTANT: Set the internal port. Hugging Face Spaces defaults to 7860.
|
| 40 |
+
EXPOSE 7860
|
| 41 |
+
|
| 42 |
+
# Command to run the application using Uvicorn
|
| 43 |
+
# 0.0.0.0 is needed to listen on all interfaces.
|
| 44 |
+
# --port 7860 must match the EXPOSE and the space.yaml configuration.
|
| 45 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
database.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# database.py (MongoDB Version)
|
| 2 |
+
import pymongo
|
| 3 |
+
from pymongo.errors import ConnectionFailure
|
| 4 |
+
import datetime
|
| 5 |
+
import os
|
| 6 |
+
# from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
# Optional: Load environment variables from a .env file for security
|
| 9 |
+
# load_dotenv()
|
| 10 |
+
|
| 11 |
+
# --- Configuration ---
|
| 12 |
+
# Get connection string from environment variable or replace directly
|
| 13 |
+
# Example local URI: "mongodb://localhost:27017/"
|
| 14 |
+
# Example Atlas URI: "mongodb+srv://<username>:<password>@clustername.mongodb.net/..."
|
| 15 |
+
MONGO_URI = os.environ.get("MONGO_URI", "mongodb://localhost:27017/")
|
| 16 |
+
DB_NAME = 'amhci_data_db'
|
| 17 |
+
COLLECTION_NAME = 'checkin_entries'
|
| 18 |
+
# ---------------------
|
| 19 |
+
|
| 20 |
+
# Global variables for the client and collection objects
|
| 21 |
+
mongo_client = None
|
| 22 |
+
mongo_collection = None
|
| 23 |
+
|
| 24 |
+
def get_mongo_collection():
|
| 25 |
+
"""
|
| 26 |
+
Establishes the MongoDB connection and returns the collection object.
|
| 27 |
+
This function should be called before performing any database operations.
|
| 28 |
+
"""
|
| 29 |
+
global mongo_client, mongo_collection
|
| 30 |
+
|
| 31 |
+
if mongo_collection is not None:
|
| 32 |
+
return mongo_collection
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
# Create a connection using MongoClient
|
| 36 |
+
mongo_client = pymongo.MongoClient(MONGO_URI)
|
| 37 |
+
|
| 38 |
+
# Access the specified database and collection
|
| 39 |
+
db = mongo_client[DB_NAME]
|
| 40 |
+
mongo_collection = db[COLLECTION_NAME]
|
| 41 |
+
|
| 42 |
+
print(f"Connected to MongoDB: Database '{DB_NAME}', Collection '{COLLECTION_NAME}'")
|
| 43 |
+
return mongo_collection
|
| 44 |
+
|
| 45 |
+
except ConnectionFailure as e:
|
| 46 |
+
print(f"ERROR: Could not connect to MongoDB: {e}")
|
| 47 |
+
# Exit the program or handle the error gracefully
|
| 48 |
+
raise
|
| 49 |
+
|
| 50 |
+
def close_mongo_connection():
|
| 51 |
+
"""Closes the MongoDB connection."""
|
| 52 |
+
global mongo_client
|
| 53 |
+
if mongo_client:
|
| 54 |
+
mongo_client.close()
|
| 55 |
+
print("MongoDB connection closed.")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def insert_checkin_entry(user_text, sentiment_score, keyword_intensity, anomaly_flag=False):
|
| 59 |
+
"""
|
| 60 |
+
Inserts a new check-in document into the MongoDB collection.
|
| 61 |
+
"""
|
| 62 |
+
collection = get_mongo_collection()
|
| 63 |
+
|
| 64 |
+
# MongoDB stores data as documents (Python dictionaries)
|
| 65 |
+
entry_data = {
|
| 66 |
+
"timestamp": datetime.datetime.now(), # MongoDB handles datetime objects natively
|
| 67 |
+
"user_text": user_text,
|
| 68 |
+
"sentiment_score": sentiment_score,
|
| 69 |
+
"keyword_intensity": keyword_intensity,
|
| 70 |
+
"anomaly_flag": anomaly_flag
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# Insert the document
|
| 74 |
+
result = collection.insert_one(entry_data)
|
| 75 |
+
print(f"Inserted document ID: {result.inserted_id}")
|
| 76 |
+
return result.inserted_id
|
| 77 |
+
|
| 78 |
+
# --- Example Usage ---
|
| 79 |
+
if __name__ == '__main__':
|
| 80 |
+
try:
|
| 81 |
+
# Example of how to use the insert function
|
| 82 |
+
insert_checkin_entry(
|
| 83 |
+
user_text="I feel great today, everything is going well.",
|
| 84 |
+
sentiment_score=0.9,
|
| 85 |
+
keyword_intensity=0.1
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Example of reading back data
|
| 89 |
+
print("\nRetrieving all entries:")
|
| 90 |
+
for doc in get_mongo_collection().find():
|
| 91 |
+
print(doc)
|
| 92 |
+
|
| 93 |
+
except Exception as e:
|
| 94 |
+
print(f"An error occurred during database operations: {e}")
|
| 95 |
+
finally:
|
| 96 |
+
# Ensure the connection is closed when the script finishes
|
| 97 |
+
close_mongo_connection()
|
| 98 |
+
|
main.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# main.py
|
| 2 |
+
import datetime
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
# Third-party libraries
|
| 6 |
+
from fastapi import FastAPI, HTTPException
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
# Local modules
|
| 11 |
+
from database import get_mongo_collection, insert_checkin_entry, close_mongo_connection
|
| 12 |
+
from nlp_model import analyze_text
|
| 13 |
+
from bson import ObjectId
|
| 14 |
+
|
| 15 |
+
# Initialize the FastAPI application
|
| 16 |
+
app = FastAPI()
|
| 17 |
+
|
| 18 |
+
# --- Pydantic Models for Data Validation ---
|
| 19 |
+
|
| 20 |
+
class CheckinRequest(BaseModel):
|
| 21 |
+
"""Model for incoming user check-in data."""
|
| 22 |
+
user_text: str
|
| 23 |
+
|
| 24 |
+
class CheckinResponse(BaseModel):
|
| 25 |
+
"""Model for data returned after a single check-in."""
|
| 26 |
+
id: str
|
| 27 |
+
timestamp: datetime.datetime
|
| 28 |
+
sentiment_score: float
|
| 29 |
+
anomaly_flag: bool
|
| 30 |
+
support_message: Optional[str] = None # The supportive message/nudge
|
| 31 |
+
user_text: Optional[str] = None
|
| 32 |
+
|
| 33 |
+
# --- Helper Functions ---
|
| 34 |
+
|
| 35 |
+
def check_for_anomaly(all_entries: List[dict], new_score: float) -> bool:
|
| 36 |
+
"""
|
| 37 |
+
Checks if the new score represents a significant, negative shift
|
| 38 |
+
using the Interquartile Range (IQR) rule against historical data.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
# Needs at least 4 past data points to establish a stable baseline (e.g., 3 days + new day)
|
| 42 |
+
if len(all_entries) < 4:
|
| 43 |
+
return False
|
| 44 |
+
|
| 45 |
+
# Gather only historical scores (sentiment_score is a float from 0.0 to 1.0)
|
| 46 |
+
historical_scores = [entry['sentiment_score'] for entry in all_entries]
|
| 47 |
+
|
| 48 |
+
df = pd.DataFrame(historical_scores, columns=['score'])
|
| 49 |
+
|
| 50 |
+
# Calculate key statistics (Median and IQR are robust against outliers)
|
| 51 |
+
Q1 = df['score'].quantile(0.25)
|
| 52 |
+
Q3 = df['score'].quantile(0.75)
|
| 53 |
+
IQR = Q3 - Q1
|
| 54 |
+
|
| 55 |
+
# Anomaly Rule: 1.5 * IQR below the first quartile (Q1)
|
| 56 |
+
LOWER_BOUND = Q1 - (1.5 * IQR)
|
| 57 |
+
|
| 58 |
+
# Anomaly is flagged if the new score is significantly below the typical low point.
|
| 59 |
+
if new_score < LOWER_BOUND:
|
| 60 |
+
print(f"ANOMALY DETECTED! New Score ({new_score:.2f}) is below Lower Bound ({LOWER_BOUND:.2f})")
|
| 61 |
+
return True
|
| 62 |
+
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def generate_support_message(sentiment_score: float, is_anomaly: bool) -> str:
|
| 67 |
+
"""
|
| 68 |
+
Generates a supportive message (nudge) based on the analysis.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
# 1. Anomaly/Crisis Nudge (Highest Priority)
|
| 72 |
+
if is_anomaly:
|
| 73 |
+
return (
|
| 74 |
+
"⚠️ Significant Change Detected. Your recent entries show a notable dip below your typical baseline. "
|
| 75 |
+
"Please reach out to a support professional or review your coping strategies. "
|
| 76 |
+
"Remember: small steps are still progress."
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# 2. Low Sentiment Nudge
|
| 80 |
+
if sentiment_score < 0.3:
|
| 81 |
+
return (
|
| 82 |
+
"🫂 It sounds like you are going through a difficult time. "
|
| 83 |
+
"It's okay to feel overwhelmed. Focus on one small, manageable task today."
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# 3. Mid-Range/Neutral Nudge
|
| 87 |
+
elif sentiment_score < 0.6:
|
| 88 |
+
return (
|
| 89 |
+
"⚖️ A steady day is still a good day. If you feel stuck, try a short break or a mindfulness exercise. "
|
| 90 |
+
"Keep an eye on how you feel tomorrow."
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# 4. Positive Nudge (Reinforcement)
|
| 94 |
+
else:
|
| 95 |
+
return (
|
| 96 |
+
"✨ Great job! Your reflection shows a positive mindset. "
|
| 97 |
+
"Take a moment to recognize what made today successful and carry that momentum forward."
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# --- API Endpoints ---
|
| 102 |
+
|
| 103 |
+
@app.post("/checkin", response_model=CheckinResponse)
|
| 104 |
+
def submit_checkin(request: CheckinRequest):
|
| 105 |
+
"""
|
| 106 |
+
Receives a new check-in entry, runs AI analysis, checks for anomalies,
|
| 107 |
+
saves the data, and returns the result with a supportive message.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
# 1. Analyze the text using the sentiment model
|
| 112 |
+
analysis = analyze_text(request.user_text)
|
| 113 |
+
|
| 114 |
+
# 2. Retrieve all past entries for robust anomaly check
|
| 115 |
+
collection = get_mongo_collection()
|
| 116 |
+
# Fetch all entries, ordered by time (descending)
|
| 117 |
+
past_entries = list(collection.find().sort("timestamp", -1))
|
| 118 |
+
|
| 119 |
+
# 3. Check for anomaly
|
| 120 |
+
is_anomaly = check_for_anomaly(past_entries, analysis["sentiment"])
|
| 121 |
+
|
| 122 |
+
# 4. Generate the supportive message
|
| 123 |
+
support_message = generate_support_message(analysis["sentiment"], is_anomaly)
|
| 124 |
+
|
| 125 |
+
# 5. Save the new entry to the database
|
| 126 |
+
entry_id = insert_checkin_entry(
|
| 127 |
+
user_text=request.user_text,
|
| 128 |
+
sentiment_score=analysis["sentiment"],
|
| 129 |
+
keyword_intensity=analysis["intensity"],
|
| 130 |
+
anomaly_flag=is_anomaly
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# 6. Return the saved entry ALONGSIDE the generated message
|
| 134 |
+
return CheckinResponse(
|
| 135 |
+
id=str(entry_id),
|
| 136 |
+
timestamp=datetime.datetime.now(),
|
| 137 |
+
sentiment_score=analysis["sentiment"],
|
| 138 |
+
anomaly_flag=is_anomaly,
|
| 139 |
+
support_message=support_message,
|
| 140 |
+
user_text=request.user_text
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
raise HTTPException(status_code=500, detail=f"Error processing check-in: {str(e)}")
|
| 145 |
+
|
| 146 |
+
# --- 2. GET Endpoint for Timeline Data ---
|
| 147 |
+
@app.get("/timeline", response_model=List[CheckinResponse])
|
| 148 |
+
def get_timeline():
|
| 149 |
+
try:
|
| 150 |
+
# Retrieve all entries, ordered by time
|
| 151 |
+
collection = get_mongo_collection()
|
| 152 |
+
entries = list(collection.find().sort("timestamp", 1))
|
| 153 |
+
|
| 154 |
+
# Convert MongoDB documents to response format
|
| 155 |
+
timeline_data = []
|
| 156 |
+
for entry in entries:
|
| 157 |
+
timeline_data.append(CheckinResponse(
|
| 158 |
+
id=str(entry["_id"]),
|
| 159 |
+
timestamp=entry["timestamp"],
|
| 160 |
+
sentiment_score=entry["sentiment_score"],
|
| 161 |
+
anomaly_flag=entry.get("anomaly_flag", False),
|
| 162 |
+
user_text=entry.get("user_text", "")
|
| 163 |
+
))
|
| 164 |
+
|
| 165 |
+
return timeline_data
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
raise HTTPException(status_code=500, detail=f"Error retrieving timeline: {str(e)}")
|
| 169 |
+
|
| 170 |
+
# --- Health Check Endpoint ---
|
| 171 |
+
@app.get("/health")
|
| 172 |
+
def health_check():
|
| 173 |
+
return {"status": "healthy", "timestamp": datetime.datetime.now()}
|
| 174 |
+
|
| 175 |
+
# --- CORS Headers (Crucial for Hosting) ---
|
| 176 |
+
# Enable CORS for frontend development
|
| 177 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 178 |
+
app.add_middleware(
|
| 179 |
+
CORSMiddleware,
|
| 180 |
+
allow_origins=["http://localhost:5173", "http://localhost:3000"], # Vite dev server
|
| 181 |
+
allow_credentials=True,
|
| 182 |
+
allow_methods=["*"],
|
| 183 |
+
allow_headers=["*"],
|
| 184 |
+
)
|
nlp_model.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# nlp_model.py
|
| 2 |
+
import torch
|
| 3 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline
|
| 4 |
+
|
| 5 |
+
# --- Configuration ---
|
| 6 |
+
# You can replace this with any specific fine-tuned RoBERTa model ID
|
| 7 |
+
# from the Hugging Face Model Hub, especially one tuned for mood/stress.
|
| 8 |
+
# Example: 'finiteautomata/bertweet-base-sentiment-analysis' or a more general one.
|
| 9 |
+
MODEL_NAME = "cardiffnlp/twitter-roberta-base-sentiment-latest"
|
| 10 |
+
|
| 11 |
+
# --- Global Model Initialization ---
|
| 12 |
+
# This runs ONLY ONCE when the FastAPI server starts.
|
| 13 |
+
# We will use a general sentiment pipeline as a placeholder.
|
| 14 |
+
# In a real project, replace this with a fine-tuned mental health model.
|
| 15 |
+
try:
|
| 16 |
+
print(f"Loading RoBERTa model: {MODEL_NAME}...")
|
| 17 |
+
|
| 18 |
+
# Use the pipeline for quick, high-level analysis in a hackathon
|
| 19 |
+
sentiment_pipeline = pipeline(
|
| 20 |
+
"sentiment-analysis",
|
| 21 |
+
model=MODEL_NAME,
|
| 22 |
+
tokenizer=MODEL_NAME,
|
| 23 |
+
device=0 if torch.cuda.is_available() else -1 # Use GPU if available
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Store the labels (e.g., ['negative', 'neutral', 'positive']) for later use
|
| 27 |
+
global_labels = sentiment_pipeline.model.config.id2label
|
| 28 |
+
print(f"Model loaded with labels: {global_labels}")
|
| 29 |
+
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"Error loading RoBERTa model: {e}. Check network connection and model name.")
|
| 32 |
+
sentiment_pipeline = None
|
| 33 |
+
global_labels = {}
|
| 34 |
+
|
| 35 |
+
def map_label_to_score(label: str, score: float) -> float:
|
| 36 |
+
"""Maps the model's categorical output (e.g., NEGATIVE) to a numerical 0.0 to 1.0 score."""
|
| 37 |
+
label = label.upper()
|
| 38 |
+
|
| 39 |
+
# This mapping is arbitrary but necessary for a quantitative timeline chart.
|
| 40 |
+
if "POSITIVE" in label:
|
| 41 |
+
return score # Closer to 1.0 is more positive
|
| 42 |
+
elif "NEGATIVE" in label:
|
| 43 |
+
return 1.0 - score # Closer to 0.0 is more negative
|
| 44 |
+
else: # NEUTRAL, etc.
|
| 45 |
+
# Neutral scores should hover near the midpoint (0.5)
|
| 46 |
+
return 0.5 + (score * 0.1) # Give it a slight boost based on confidence, but keep it centered
|
| 47 |
+
|
| 48 |
+
def analyze_text(text: str) -> dict:
|
| 49 |
+
"""Performs RoBERTa analysis and returns scores."""
|
| 50 |
+
if not sentiment_pipeline:
|
| 51 |
+
return {"sentiment": 0.5, "intensity": 0.0}
|
| 52 |
+
|
| 53 |
+
# 1. Run the pipeline on the input text
|
| 54 |
+
result = sentiment_pipeline(text)[0]
|
| 55 |
+
|
| 56 |
+
label = result['label']
|
| 57 |
+
raw_score = result['score']
|
| 58 |
+
|
| 59 |
+
# 2. Map the result to our desired numerical output
|
| 60 |
+
numerical_sentiment = map_label_to_score(label, raw_score)
|
| 61 |
+
|
| 62 |
+
# 3. Use the raw confidence score as a proxy for intensity/certainty
|
| 63 |
+
intensity_score = raw_score
|
| 64 |
+
|
| 65 |
+
# Note: For hackathon extension: You could add a separate NER model
|
| 66 |
+
# here to extract keywords like 'hopeless' or 'anxiety' for the intensity score.
|
| 67 |
+
|
| 68 |
+
return {
|
| 69 |
+
"sentiment": numerical_sentiment, # A continuous score between 0.0 and 1.0
|
| 70 |
+
"intensity": intensity_score # The confidence level of the model's prediction
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# Ensure the database.py and main.py files are correctly referencing this updated
|
| 74 |
+
# 'analyze_text' function and handling the float outputs. (They already do!)
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.104.1
|
| 2 |
+
uvicorn>=0.24.0
|
| 3 |
+
pydantic>=2.5.0
|
| 4 |
+
pymongo>=4.6.0
|
| 5 |
+
torch>=2.9.0
|
| 6 |
+
transformers>=4.35.0
|
| 7 |
+
pandas>=2.1.3
|
| 8 |
+
python-dotenv>=1.0.0
|