Spaces:
Running
Running
Hardik commited on
Commit ·
3a93fb9
0
Parent(s):
Initial commit: Sentiment Analysis HF Space
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +7 -0
- .gitignore +22 -0
- Dockerfile +34 -0
- README.md +26 -0
- backend/.env +1 -0
- backend/Dockerfile +16 -0
- backend/package-lock.json +1607 -0
- backend/package.json +25 -0
- backend/prisma/schema.prisma +37 -0
- backend/prisma/seed.js +89 -0
- backend/src/config/index.js +7 -0
- backend/src/controllers/predict.controller.js +82 -0
- backend/src/middleware/errorHandler.js +13 -0
- backend/src/middleware/rateLimiter.js +14 -0
- backend/src/middleware/validation.js +33 -0
- backend/src/routes/v1.routes.js +16 -0
- backend/src/server.js +51 -0
- backend/src/services/dbService.js +58 -0
- backend/src/services/mlService.js +43 -0
- data/X_test_pad.npy +3 -0
- data/X_train_pad.npy +3 -0
- data/config.pkl +3 -0
- data/gru_config.pkl +3 -0
- data/gru_history.pkl +3 -0
- data/tokenizer.pkl +3 -0
- data/y_test.npy +3 -0
- data/y_train.npy +3 -0
- docker-compose.yml +54 -0
- frontend/app.js +163 -0
- frontend/film_reel.png +3 -0
- frontend/index.html +155 -0
- frontend/inside.html +401 -0
- frontend/inside.js +83 -0
- frontend/style.css +1174 -0
- hf_start.sh +4 -0
- ml_service/Dockerfile +19 -0
- ml_service/app/api/routes.py +116 -0
- ml_service/app/config.py +18 -0
- ml_service/app/schemas/prediction.py +46 -0
- ml_service/app/services/explainer.py +65 -0
- ml_service/app/services/model_loader.py +131 -0
- ml_service/app/services/predictor.py +116 -0
- ml_service/app/utils/preprocessing.py +355 -0
- ml_service/main.py +46 -0
- ml_service/requirements.txt +14 -0
- ml_service/test_ml_service.py +30 -0
- models_bert/tuned/config.json +31 -0
- models_bert/tuned/model.safetensors +3 -0
- models_bert/tuned/special_tokens_map.json +1 -0
- models_bert/tuned/threshold.pkl +3 -0
.gitattributes
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.keras filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.db filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
node_modules/
|
| 5 |
+
notebooks/
|
| 6 |
+
.ipynb_checkpoints/
|
| 7 |
+
.DS_Store
|
| 8 |
+
backend/prisma/dev.db
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
.tuner_dir/
|
| 13 |
+
# Exclude untuned BERT model (not needed for inference)
|
| 14 |
+
models_bert/model.safetensors
|
| 15 |
+
models_bert/config.json
|
| 16 |
+
models_bert/tokenizer.json
|
| 17 |
+
models_bert/tokenizer_config.json
|
| 18 |
+
models_bert/special_tokens_map.json
|
| 19 |
+
models_bert/training_config.json
|
| 20 |
+
models_bert/threshold.pkl
|
| 21 |
+
# Training data not needed for deployment
|
| 22 |
+
data/cleaned_imdb.csv
|
Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && apt-get install -y curl nginx supervisor && \
|
| 4 |
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
| 5 |
+
apt-get install -y nodejs && \
|
| 6 |
+
apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
RUN pip install --no-cache-dir fastapi==0.111.0 uvicorn==0.30.1 pydantic==2.7.4 \
|
| 9 |
+
pydantic-settings==2.3.4 numpy==1.26.4 pandas==2.2.2 scikit-learn==1.5.1 \
|
| 10 |
+
scipy==1.13.1 nltk==3.8.1 tensorflow==2.16.1 keras==3.3.3 \
|
| 11 |
+
torch==2.3.1 transformers==4.41.2 joblib==1.4.2
|
| 12 |
+
|
| 13 |
+
RUN python -c "import nltk; nltk.download('stopwords', quiet=True); nltk.download('punkt', quiet=True); nltk.download('wordnet', quiet=True)"
|
| 14 |
+
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
|
| 17 |
+
COPY backend/package*.json ./backend/
|
| 18 |
+
RUN cd backend && npm install
|
| 19 |
+
|
| 20 |
+
COPY . .
|
| 21 |
+
|
| 22 |
+
RUN cd backend && npx prisma generate && touch prisma/dev.db
|
| 23 |
+
|
| 24 |
+
ENV PORT=4000
|
| 25 |
+
ENV ML_SERVICE_URL=http://localhost:8000
|
| 26 |
+
ENV MODELS_ML_DIR=/app/models_ml
|
| 27 |
+
ENV MODELS_LSTM_DIR=/app/models_lstm
|
| 28 |
+
ENV MODELS_BERT_DIR=/app/models_bert/tuned
|
| 29 |
+
ENV DATA_DIR=/app/data
|
| 30 |
+
ENV NODE_ENV=production
|
| 31 |
+
|
| 32 |
+
EXPOSE 7860
|
| 33 |
+
|
| 34 |
+
CMD ["/bin/bash", "/app/hf_start.sh"]
|
README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# The Screening Room
|
| 2 |
+
|
| 3 |
+
> **Three Critics. One Review. Three Perspectives.**
|
| 4 |
+
|
| 5 |
+
The Screening Room is an AI-powered movie review analysis platform that compares how three fundamentally different machine learning architectures interpret the same text.
|
| 6 |
+
|
| 7 |
+
1. **The Statistician (Logistic Regression + TF-IDF):** Traditional bag-of-words approach.
|
| 8 |
+
2. **The Sequentialist (Bi-LSTM):** Deep learning with sequential reading.
|
| 9 |
+
3. **The Contextualist (Fine-Tuned BERT):** Transformer with bidirectional context.
|
| 10 |
+
|
| 11 |
+
## Architecture
|
| 12 |
+
|
| 13 |
+
- **Frontend:** Static HTML/CSS/JS served via Nginx (port 7860 on HF Spaces)
|
| 14 |
+
- **Backend API Gateway:** Express.js for validation, routing, orchestration (port 4000)
|
| 15 |
+
- **ML Inference Service:** FastAPI Python for ML model inference (port 8000)
|
| 16 |
+
- **Orchestration:** Supervisor manages all processes in a single container
|
| 17 |
+
|
| 18 |
+
## Deploy on Hugging Face Spaces
|
| 19 |
+
|
| 20 |
+
This app uses a **Docker Space** to run all services (Nginx + Express + FastAPI) behind a single port.
|
| 21 |
+
|
| 22 |
+
## Local Development
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
docker-compose up --build
|
| 26 |
+
```
|
backend/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
|
backend/Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy package.json and install dependencies
|
| 6 |
+
COPY package*.json ./
|
| 7 |
+
RUN npm install
|
| 8 |
+
|
| 9 |
+
# Copy source code
|
| 10 |
+
COPY src ./src
|
| 11 |
+
|
| 12 |
+
# Expose port
|
| 13 |
+
EXPOSE 4000
|
| 14 |
+
|
| 15 |
+
# Start server
|
| 16 |
+
CMD ["npm", "start"]
|
backend/package-lock.json
ADDED
|
@@ -0,0 +1,1607 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "screening-room-backend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "screening-room-backend",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@prisma/client": "^5.22.0",
|
| 12 |
+
"axios": "^1.7.2",
|
| 13 |
+
"cors": "^2.8.5",
|
| 14 |
+
"dotenv": "^16.4.5",
|
| 15 |
+
"express": "^4.19.2",
|
| 16 |
+
"express-rate-limit": "^7.3.1",
|
| 17 |
+
"helmet": "^7.1.0",
|
| 18 |
+
"morgan": "^1.10.0",
|
| 19 |
+
"zod": "^3.23.8"
|
| 20 |
+
},
|
| 21 |
+
"devDependencies": {
|
| 22 |
+
"nodemon": "^3.1.4",
|
| 23 |
+
"prisma": "^5.22.0"
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"node_modules/@prisma/client": {
|
| 27 |
+
"version": "5.22.0",
|
| 28 |
+
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
| 29 |
+
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
| 30 |
+
"hasInstallScript": true,
|
| 31 |
+
"license": "Apache-2.0",
|
| 32 |
+
"engines": {
|
| 33 |
+
"node": ">=16.13"
|
| 34 |
+
},
|
| 35 |
+
"peerDependencies": {
|
| 36 |
+
"prisma": "*"
|
| 37 |
+
},
|
| 38 |
+
"peerDependenciesMeta": {
|
| 39 |
+
"prisma": {
|
| 40 |
+
"optional": true
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"node_modules/@prisma/debug": {
|
| 45 |
+
"version": "5.22.0",
|
| 46 |
+
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
|
| 47 |
+
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
|
| 48 |
+
"devOptional": true,
|
| 49 |
+
"license": "Apache-2.0"
|
| 50 |
+
},
|
| 51 |
+
"node_modules/@prisma/engines": {
|
| 52 |
+
"version": "5.22.0",
|
| 53 |
+
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
|
| 54 |
+
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
| 55 |
+
"devOptional": true,
|
| 56 |
+
"hasInstallScript": true,
|
| 57 |
+
"license": "Apache-2.0",
|
| 58 |
+
"dependencies": {
|
| 59 |
+
"@prisma/debug": "5.22.0",
|
| 60 |
+
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
| 61 |
+
"@prisma/fetch-engine": "5.22.0",
|
| 62 |
+
"@prisma/get-platform": "5.22.0"
|
| 63 |
+
}
|
| 64 |
+
},
|
| 65 |
+
"node_modules/@prisma/engines-version": {
|
| 66 |
+
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
| 67 |
+
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
| 68 |
+
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
|
| 69 |
+
"devOptional": true,
|
| 70 |
+
"license": "Apache-2.0"
|
| 71 |
+
},
|
| 72 |
+
"node_modules/@prisma/fetch-engine": {
|
| 73 |
+
"version": "5.22.0",
|
| 74 |
+
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
| 75 |
+
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
| 76 |
+
"devOptional": true,
|
| 77 |
+
"license": "Apache-2.0",
|
| 78 |
+
"dependencies": {
|
| 79 |
+
"@prisma/debug": "5.22.0",
|
| 80 |
+
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
| 81 |
+
"@prisma/get-platform": "5.22.0"
|
| 82 |
+
}
|
| 83 |
+
},
|
| 84 |
+
"node_modules/@prisma/get-platform": {
|
| 85 |
+
"version": "5.22.0",
|
| 86 |
+
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
| 87 |
+
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
| 88 |
+
"devOptional": true,
|
| 89 |
+
"license": "Apache-2.0",
|
| 90 |
+
"dependencies": {
|
| 91 |
+
"@prisma/debug": "5.22.0"
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"node_modules/accepts": {
|
| 95 |
+
"version": "1.3.8",
|
| 96 |
+
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
| 97 |
+
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
| 98 |
+
"license": "MIT",
|
| 99 |
+
"dependencies": {
|
| 100 |
+
"mime-types": "~2.1.34",
|
| 101 |
+
"negotiator": "0.6.3"
|
| 102 |
+
},
|
| 103 |
+
"engines": {
|
| 104 |
+
"node": ">= 0.6"
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
"node_modules/agent-base": {
|
| 108 |
+
"version": "6.0.2",
|
| 109 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
| 110 |
+
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
| 111 |
+
"license": "MIT",
|
| 112 |
+
"dependencies": {
|
| 113 |
+
"debug": "4"
|
| 114 |
+
},
|
| 115 |
+
"engines": {
|
| 116 |
+
"node": ">= 6.0.0"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"node_modules/agent-base/node_modules/debug": {
|
| 120 |
+
"version": "4.4.3",
|
| 121 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 122 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 123 |
+
"license": "MIT",
|
| 124 |
+
"dependencies": {
|
| 125 |
+
"ms": "^2.1.3"
|
| 126 |
+
},
|
| 127 |
+
"engines": {
|
| 128 |
+
"node": ">=6.0"
|
| 129 |
+
},
|
| 130 |
+
"peerDependenciesMeta": {
|
| 131 |
+
"supports-color": {
|
| 132 |
+
"optional": true
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"node_modules/agent-base/node_modules/ms": {
|
| 137 |
+
"version": "2.1.3",
|
| 138 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 139 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 140 |
+
"license": "MIT"
|
| 141 |
+
},
|
| 142 |
+
"node_modules/anymatch": {
|
| 143 |
+
"version": "3.1.3",
|
| 144 |
+
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
| 145 |
+
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
| 146 |
+
"dev": true,
|
| 147 |
+
"license": "ISC",
|
| 148 |
+
"dependencies": {
|
| 149 |
+
"normalize-path": "^3.0.0",
|
| 150 |
+
"picomatch": "^2.0.4"
|
| 151 |
+
},
|
| 152 |
+
"engines": {
|
| 153 |
+
"node": ">= 8"
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"node_modules/array-flatten": {
|
| 157 |
+
"version": "1.1.1",
|
| 158 |
+
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
| 159 |
+
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
| 160 |
+
"license": "MIT"
|
| 161 |
+
},
|
| 162 |
+
"node_modules/asynckit": {
|
| 163 |
+
"version": "0.4.0",
|
| 164 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
| 165 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
| 166 |
+
"license": "MIT"
|
| 167 |
+
},
|
| 168 |
+
"node_modules/axios": {
|
| 169 |
+
"version": "1.18.1",
|
| 170 |
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
| 171 |
+
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
| 172 |
+
"license": "MIT",
|
| 173 |
+
"dependencies": {
|
| 174 |
+
"follow-redirects": "^1.16.0",
|
| 175 |
+
"form-data": "^4.0.5",
|
| 176 |
+
"https-proxy-agent": "^5.0.1",
|
| 177 |
+
"proxy-from-env": "^2.1.0"
|
| 178 |
+
}
|
| 179 |
+
},
|
| 180 |
+
"node_modules/balanced-match": {
|
| 181 |
+
"version": "4.0.4",
|
| 182 |
+
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
| 183 |
+
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
| 184 |
+
"dev": true,
|
| 185 |
+
"license": "MIT",
|
| 186 |
+
"engines": {
|
| 187 |
+
"node": "18 || 20 || >=22"
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
"node_modules/basic-auth": {
|
| 191 |
+
"version": "2.0.1",
|
| 192 |
+
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
| 193 |
+
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
| 194 |
+
"license": "MIT",
|
| 195 |
+
"dependencies": {
|
| 196 |
+
"safe-buffer": "5.1.2"
|
| 197 |
+
},
|
| 198 |
+
"engines": {
|
| 199 |
+
"node": ">= 0.8"
|
| 200 |
+
}
|
| 201 |
+
},
|
| 202 |
+
"node_modules/basic-auth/node_modules/safe-buffer": {
|
| 203 |
+
"version": "5.1.2",
|
| 204 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
| 205 |
+
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
| 206 |
+
"license": "MIT"
|
| 207 |
+
},
|
| 208 |
+
"node_modules/binary-extensions": {
|
| 209 |
+
"version": "2.3.0",
|
| 210 |
+
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
| 211 |
+
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
| 212 |
+
"dev": true,
|
| 213 |
+
"license": "MIT",
|
| 214 |
+
"engines": {
|
| 215 |
+
"node": ">=8"
|
| 216 |
+
},
|
| 217 |
+
"funding": {
|
| 218 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 219 |
+
}
|
| 220 |
+
},
|
| 221 |
+
"node_modules/body-parser": {
|
| 222 |
+
"version": "1.20.5",
|
| 223 |
+
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
| 224 |
+
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
| 225 |
+
"license": "MIT",
|
| 226 |
+
"dependencies": {
|
| 227 |
+
"bytes": "~3.1.2",
|
| 228 |
+
"content-type": "~1.0.5",
|
| 229 |
+
"debug": "2.6.9",
|
| 230 |
+
"depd": "2.0.0",
|
| 231 |
+
"destroy": "~1.2.0",
|
| 232 |
+
"http-errors": "~2.0.1",
|
| 233 |
+
"iconv-lite": "~0.4.24",
|
| 234 |
+
"on-finished": "~2.4.1",
|
| 235 |
+
"qs": "~6.15.1",
|
| 236 |
+
"raw-body": "~2.5.3",
|
| 237 |
+
"type-is": "~1.6.18",
|
| 238 |
+
"unpipe": "~1.0.0"
|
| 239 |
+
},
|
| 240 |
+
"engines": {
|
| 241 |
+
"node": ">= 0.8",
|
| 242 |
+
"npm": "1.2.8000 || >= 1.4.16"
|
| 243 |
+
}
|
| 244 |
+
},
|
| 245 |
+
"node_modules/brace-expansion": {
|
| 246 |
+
"version": "5.0.7",
|
| 247 |
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
| 248 |
+
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
| 249 |
+
"dev": true,
|
| 250 |
+
"license": "MIT",
|
| 251 |
+
"dependencies": {
|
| 252 |
+
"balanced-match": "^4.0.2"
|
| 253 |
+
},
|
| 254 |
+
"engines": {
|
| 255 |
+
"node": "18 || 20 || >=22"
|
| 256 |
+
}
|
| 257 |
+
},
|
| 258 |
+
"node_modules/braces": {
|
| 259 |
+
"version": "3.0.3",
|
| 260 |
+
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
| 261 |
+
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
| 262 |
+
"dev": true,
|
| 263 |
+
"license": "MIT",
|
| 264 |
+
"dependencies": {
|
| 265 |
+
"fill-range": "^7.1.1"
|
| 266 |
+
},
|
| 267 |
+
"engines": {
|
| 268 |
+
"node": ">=8"
|
| 269 |
+
}
|
| 270 |
+
},
|
| 271 |
+
"node_modules/bytes": {
|
| 272 |
+
"version": "3.1.2",
|
| 273 |
+
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
| 274 |
+
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
| 275 |
+
"license": "MIT",
|
| 276 |
+
"engines": {
|
| 277 |
+
"node": ">= 0.8"
|
| 278 |
+
}
|
| 279 |
+
},
|
| 280 |
+
"node_modules/call-bind-apply-helpers": {
|
| 281 |
+
"version": "1.0.2",
|
| 282 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 283 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 284 |
+
"license": "MIT",
|
| 285 |
+
"dependencies": {
|
| 286 |
+
"es-errors": "^1.3.0",
|
| 287 |
+
"function-bind": "^1.1.2"
|
| 288 |
+
},
|
| 289 |
+
"engines": {
|
| 290 |
+
"node": ">= 0.4"
|
| 291 |
+
}
|
| 292 |
+
},
|
| 293 |
+
"node_modules/call-bound": {
|
| 294 |
+
"version": "1.0.4",
|
| 295 |
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
| 296 |
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
| 297 |
+
"license": "MIT",
|
| 298 |
+
"dependencies": {
|
| 299 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 300 |
+
"get-intrinsic": "^1.3.0"
|
| 301 |
+
},
|
| 302 |
+
"engines": {
|
| 303 |
+
"node": ">= 0.4"
|
| 304 |
+
},
|
| 305 |
+
"funding": {
|
| 306 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 307 |
+
}
|
| 308 |
+
},
|
| 309 |
+
"node_modules/chokidar": {
|
| 310 |
+
"version": "3.6.0",
|
| 311 |
+
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
| 312 |
+
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
| 313 |
+
"dev": true,
|
| 314 |
+
"license": "MIT",
|
| 315 |
+
"dependencies": {
|
| 316 |
+
"anymatch": "~3.1.2",
|
| 317 |
+
"braces": "~3.0.2",
|
| 318 |
+
"glob-parent": "~5.1.2",
|
| 319 |
+
"is-binary-path": "~2.1.0",
|
| 320 |
+
"is-glob": "~4.0.1",
|
| 321 |
+
"normalize-path": "~3.0.0",
|
| 322 |
+
"readdirp": "~3.6.0"
|
| 323 |
+
},
|
| 324 |
+
"engines": {
|
| 325 |
+
"node": ">= 8.10.0"
|
| 326 |
+
},
|
| 327 |
+
"funding": {
|
| 328 |
+
"url": "https://paulmillr.com/funding/"
|
| 329 |
+
},
|
| 330 |
+
"optionalDependencies": {
|
| 331 |
+
"fsevents": "~2.3.2"
|
| 332 |
+
}
|
| 333 |
+
},
|
| 334 |
+
"node_modules/combined-stream": {
|
| 335 |
+
"version": "1.0.8",
|
| 336 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
| 337 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
| 338 |
+
"license": "MIT",
|
| 339 |
+
"dependencies": {
|
| 340 |
+
"delayed-stream": "~1.0.0"
|
| 341 |
+
},
|
| 342 |
+
"engines": {
|
| 343 |
+
"node": ">= 0.8"
|
| 344 |
+
}
|
| 345 |
+
},
|
| 346 |
+
"node_modules/content-disposition": {
|
| 347 |
+
"version": "0.5.4",
|
| 348 |
+
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
| 349 |
+
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
| 350 |
+
"license": "MIT",
|
| 351 |
+
"dependencies": {
|
| 352 |
+
"safe-buffer": "5.2.1"
|
| 353 |
+
},
|
| 354 |
+
"engines": {
|
| 355 |
+
"node": ">= 0.6"
|
| 356 |
+
}
|
| 357 |
+
},
|
| 358 |
+
"node_modules/content-type": {
|
| 359 |
+
"version": "1.0.5",
|
| 360 |
+
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
| 361 |
+
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
| 362 |
+
"license": "MIT",
|
| 363 |
+
"engines": {
|
| 364 |
+
"node": ">= 0.6"
|
| 365 |
+
}
|
| 366 |
+
},
|
| 367 |
+
"node_modules/cookie": {
|
| 368 |
+
"version": "0.7.2",
|
| 369 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
| 370 |
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
| 371 |
+
"license": "MIT",
|
| 372 |
+
"engines": {
|
| 373 |
+
"node": ">= 0.6"
|
| 374 |
+
}
|
| 375 |
+
},
|
| 376 |
+
"node_modules/cookie-signature": {
|
| 377 |
+
"version": "1.0.7",
|
| 378 |
+
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
| 379 |
+
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
| 380 |
+
"license": "MIT"
|
| 381 |
+
},
|
| 382 |
+
"node_modules/cors": {
|
| 383 |
+
"version": "2.8.6",
|
| 384 |
+
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
| 385 |
+
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
| 386 |
+
"license": "MIT",
|
| 387 |
+
"dependencies": {
|
| 388 |
+
"object-assign": "^4",
|
| 389 |
+
"vary": "^1"
|
| 390 |
+
},
|
| 391 |
+
"engines": {
|
| 392 |
+
"node": ">= 0.10"
|
| 393 |
+
},
|
| 394 |
+
"funding": {
|
| 395 |
+
"type": "opencollective",
|
| 396 |
+
"url": "https://opencollective.com/express"
|
| 397 |
+
}
|
| 398 |
+
},
|
| 399 |
+
"node_modules/debug": {
|
| 400 |
+
"version": "2.6.9",
|
| 401 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
| 402 |
+
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
| 403 |
+
"license": "MIT",
|
| 404 |
+
"dependencies": {
|
| 405 |
+
"ms": "2.0.0"
|
| 406 |
+
}
|
| 407 |
+
},
|
| 408 |
+
"node_modules/delayed-stream": {
|
| 409 |
+
"version": "1.0.0",
|
| 410 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
| 411 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
| 412 |
+
"license": "MIT",
|
| 413 |
+
"engines": {
|
| 414 |
+
"node": ">=0.4.0"
|
| 415 |
+
}
|
| 416 |
+
},
|
| 417 |
+
"node_modules/depd": {
|
| 418 |
+
"version": "2.0.0",
|
| 419 |
+
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
| 420 |
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
| 421 |
+
"license": "MIT",
|
| 422 |
+
"engines": {
|
| 423 |
+
"node": ">= 0.8"
|
| 424 |
+
}
|
| 425 |
+
},
|
| 426 |
+
"node_modules/destroy": {
|
| 427 |
+
"version": "1.2.0",
|
| 428 |
+
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
| 429 |
+
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
| 430 |
+
"license": "MIT",
|
| 431 |
+
"engines": {
|
| 432 |
+
"node": ">= 0.8",
|
| 433 |
+
"npm": "1.2.8000 || >= 1.4.16"
|
| 434 |
+
}
|
| 435 |
+
},
|
| 436 |
+
"node_modules/dotenv": {
|
| 437 |
+
"version": "16.6.1",
|
| 438 |
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
| 439 |
+
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
| 440 |
+
"license": "BSD-2-Clause",
|
| 441 |
+
"engines": {
|
| 442 |
+
"node": ">=12"
|
| 443 |
+
},
|
| 444 |
+
"funding": {
|
| 445 |
+
"url": "https://dotenvx.com"
|
| 446 |
+
}
|
| 447 |
+
},
|
| 448 |
+
"node_modules/dunder-proto": {
|
| 449 |
+
"version": "1.0.1",
|
| 450 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 451 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 452 |
+
"license": "MIT",
|
| 453 |
+
"dependencies": {
|
| 454 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 455 |
+
"es-errors": "^1.3.0",
|
| 456 |
+
"gopd": "^1.2.0"
|
| 457 |
+
},
|
| 458 |
+
"engines": {
|
| 459 |
+
"node": ">= 0.4"
|
| 460 |
+
}
|
| 461 |
+
},
|
| 462 |
+
"node_modules/ee-first": {
|
| 463 |
+
"version": "1.1.1",
|
| 464 |
+
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
| 465 |
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
| 466 |
+
"license": "MIT"
|
| 467 |
+
},
|
| 468 |
+
"node_modules/encodeurl": {
|
| 469 |
+
"version": "2.0.0",
|
| 470 |
+
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
| 471 |
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
| 472 |
+
"license": "MIT",
|
| 473 |
+
"engines": {
|
| 474 |
+
"node": ">= 0.8"
|
| 475 |
+
}
|
| 476 |
+
},
|
| 477 |
+
"node_modules/es-define-property": {
|
| 478 |
+
"version": "1.0.1",
|
| 479 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 480 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 481 |
+
"license": "MIT",
|
| 482 |
+
"engines": {
|
| 483 |
+
"node": ">= 0.4"
|
| 484 |
+
}
|
| 485 |
+
},
|
| 486 |
+
"node_modules/es-errors": {
|
| 487 |
+
"version": "1.3.0",
|
| 488 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 489 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 490 |
+
"license": "MIT",
|
| 491 |
+
"engines": {
|
| 492 |
+
"node": ">= 0.4"
|
| 493 |
+
}
|
| 494 |
+
},
|
| 495 |
+
"node_modules/es-object-atoms": {
|
| 496 |
+
"version": "1.1.2",
|
| 497 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
| 498 |
+
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
| 499 |
+
"license": "MIT",
|
| 500 |
+
"dependencies": {
|
| 501 |
+
"es-errors": "^1.3.0"
|
| 502 |
+
},
|
| 503 |
+
"engines": {
|
| 504 |
+
"node": ">= 0.4"
|
| 505 |
+
}
|
| 506 |
+
},
|
| 507 |
+
"node_modules/es-set-tostringtag": {
|
| 508 |
+
"version": "2.1.0",
|
| 509 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
| 510 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
| 511 |
+
"license": "MIT",
|
| 512 |
+
"dependencies": {
|
| 513 |
+
"es-errors": "^1.3.0",
|
| 514 |
+
"get-intrinsic": "^1.2.6",
|
| 515 |
+
"has-tostringtag": "^1.0.2",
|
| 516 |
+
"hasown": "^2.0.2"
|
| 517 |
+
},
|
| 518 |
+
"engines": {
|
| 519 |
+
"node": ">= 0.4"
|
| 520 |
+
}
|
| 521 |
+
},
|
| 522 |
+
"node_modules/escape-html": {
|
| 523 |
+
"version": "1.0.3",
|
| 524 |
+
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
| 525 |
+
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
| 526 |
+
"license": "MIT"
|
| 527 |
+
},
|
| 528 |
+
"node_modules/etag": {
|
| 529 |
+
"version": "1.8.1",
|
| 530 |
+
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
| 531 |
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
| 532 |
+
"license": "MIT",
|
| 533 |
+
"engines": {
|
| 534 |
+
"node": ">= 0.6"
|
| 535 |
+
}
|
| 536 |
+
},
|
| 537 |
+
"node_modules/express": {
|
| 538 |
+
"version": "4.22.2",
|
| 539 |
+
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
| 540 |
+
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
| 541 |
+
"license": "MIT",
|
| 542 |
+
"peer": true,
|
| 543 |
+
"dependencies": {
|
| 544 |
+
"accepts": "~1.3.8",
|
| 545 |
+
"array-flatten": "1.1.1",
|
| 546 |
+
"body-parser": "~1.20.5",
|
| 547 |
+
"content-disposition": "~0.5.4",
|
| 548 |
+
"content-type": "~1.0.4",
|
| 549 |
+
"cookie": "~0.7.1",
|
| 550 |
+
"cookie-signature": "~1.0.6",
|
| 551 |
+
"debug": "2.6.9",
|
| 552 |
+
"depd": "2.0.0",
|
| 553 |
+
"encodeurl": "~2.0.0",
|
| 554 |
+
"escape-html": "~1.0.3",
|
| 555 |
+
"etag": "~1.8.1",
|
| 556 |
+
"finalhandler": "~1.3.1",
|
| 557 |
+
"fresh": "~0.5.2",
|
| 558 |
+
"http-errors": "~2.0.0",
|
| 559 |
+
"merge-descriptors": "1.0.3",
|
| 560 |
+
"methods": "~1.1.2",
|
| 561 |
+
"on-finished": "~2.4.1",
|
| 562 |
+
"parseurl": "~1.3.3",
|
| 563 |
+
"path-to-regexp": "~0.1.12",
|
| 564 |
+
"proxy-addr": "~2.0.7",
|
| 565 |
+
"qs": "~6.15.1",
|
| 566 |
+
"range-parser": "~1.2.1",
|
| 567 |
+
"safe-buffer": "5.2.1",
|
| 568 |
+
"send": "~0.19.0",
|
| 569 |
+
"serve-static": "~1.16.2",
|
| 570 |
+
"setprototypeof": "1.2.0",
|
| 571 |
+
"statuses": "~2.0.1",
|
| 572 |
+
"type-is": "~1.6.18",
|
| 573 |
+
"utils-merge": "1.0.1",
|
| 574 |
+
"vary": "~1.1.2"
|
| 575 |
+
},
|
| 576 |
+
"engines": {
|
| 577 |
+
"node": ">= 0.10.0"
|
| 578 |
+
},
|
| 579 |
+
"funding": {
|
| 580 |
+
"type": "opencollective",
|
| 581 |
+
"url": "https://opencollective.com/express"
|
| 582 |
+
}
|
| 583 |
+
},
|
| 584 |
+
"node_modules/express-rate-limit": {
|
| 585 |
+
"version": "7.5.1",
|
| 586 |
+
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
| 587 |
+
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
| 588 |
+
"license": "MIT",
|
| 589 |
+
"engines": {
|
| 590 |
+
"node": ">= 16"
|
| 591 |
+
},
|
| 592 |
+
"funding": {
|
| 593 |
+
"url": "https://github.com/sponsors/express-rate-limit"
|
| 594 |
+
},
|
| 595 |
+
"peerDependencies": {
|
| 596 |
+
"express": ">= 4.11"
|
| 597 |
+
}
|
| 598 |
+
},
|
| 599 |
+
"node_modules/fill-range": {
|
| 600 |
+
"version": "7.1.1",
|
| 601 |
+
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
| 602 |
+
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
| 603 |
+
"dev": true,
|
| 604 |
+
"license": "MIT",
|
| 605 |
+
"dependencies": {
|
| 606 |
+
"to-regex-range": "^5.0.1"
|
| 607 |
+
},
|
| 608 |
+
"engines": {
|
| 609 |
+
"node": ">=8"
|
| 610 |
+
}
|
| 611 |
+
},
|
| 612 |
+
"node_modules/finalhandler": {
|
| 613 |
+
"version": "1.3.2",
|
| 614 |
+
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
| 615 |
+
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
| 616 |
+
"license": "MIT",
|
| 617 |
+
"dependencies": {
|
| 618 |
+
"debug": "2.6.9",
|
| 619 |
+
"encodeurl": "~2.0.0",
|
| 620 |
+
"escape-html": "~1.0.3",
|
| 621 |
+
"on-finished": "~2.4.1",
|
| 622 |
+
"parseurl": "~1.3.3",
|
| 623 |
+
"statuses": "~2.0.2",
|
| 624 |
+
"unpipe": "~1.0.0"
|
| 625 |
+
},
|
| 626 |
+
"engines": {
|
| 627 |
+
"node": ">= 0.8"
|
| 628 |
+
}
|
| 629 |
+
},
|
| 630 |
+
"node_modules/follow-redirects": {
|
| 631 |
+
"version": "1.16.0",
|
| 632 |
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
| 633 |
+
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
| 634 |
+
"funding": [
|
| 635 |
+
{
|
| 636 |
+
"type": "individual",
|
| 637 |
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
| 638 |
+
}
|
| 639 |
+
],
|
| 640 |
+
"license": "MIT",
|
| 641 |
+
"engines": {
|
| 642 |
+
"node": ">=4.0"
|
| 643 |
+
},
|
| 644 |
+
"peerDependenciesMeta": {
|
| 645 |
+
"debug": {
|
| 646 |
+
"optional": true
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
},
|
| 650 |
+
"node_modules/form-data": {
|
| 651 |
+
"version": "4.0.6",
|
| 652 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
| 653 |
+
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
| 654 |
+
"license": "MIT",
|
| 655 |
+
"dependencies": {
|
| 656 |
+
"asynckit": "^0.4.0",
|
| 657 |
+
"combined-stream": "^1.0.8",
|
| 658 |
+
"es-set-tostringtag": "^2.1.0",
|
| 659 |
+
"hasown": "^2.0.4",
|
| 660 |
+
"mime-types": "^2.1.35"
|
| 661 |
+
},
|
| 662 |
+
"engines": {
|
| 663 |
+
"node": ">= 6"
|
| 664 |
+
}
|
| 665 |
+
},
|
| 666 |
+
"node_modules/forwarded": {
|
| 667 |
+
"version": "0.2.0",
|
| 668 |
+
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
| 669 |
+
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
| 670 |
+
"license": "MIT",
|
| 671 |
+
"engines": {
|
| 672 |
+
"node": ">= 0.6"
|
| 673 |
+
}
|
| 674 |
+
},
|
| 675 |
+
"node_modules/fresh": {
|
| 676 |
+
"version": "0.5.2",
|
| 677 |
+
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
| 678 |
+
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
| 679 |
+
"license": "MIT",
|
| 680 |
+
"engines": {
|
| 681 |
+
"node": ">= 0.6"
|
| 682 |
+
}
|
| 683 |
+
},
|
| 684 |
+
"node_modules/fsevents": {
|
| 685 |
+
"version": "2.3.3",
|
| 686 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 687 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 688 |
+
"dev": true,
|
| 689 |
+
"hasInstallScript": true,
|
| 690 |
+
"license": "MIT",
|
| 691 |
+
"optional": true,
|
| 692 |
+
"os": [
|
| 693 |
+
"darwin"
|
| 694 |
+
],
|
| 695 |
+
"engines": {
|
| 696 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 697 |
+
}
|
| 698 |
+
},
|
| 699 |
+
"node_modules/function-bind": {
|
| 700 |
+
"version": "1.1.2",
|
| 701 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 702 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 703 |
+
"license": "MIT",
|
| 704 |
+
"funding": {
|
| 705 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 706 |
+
}
|
| 707 |
+
},
|
| 708 |
+
"node_modules/get-intrinsic": {
|
| 709 |
+
"version": "1.3.0",
|
| 710 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 711 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 712 |
+
"license": "MIT",
|
| 713 |
+
"dependencies": {
|
| 714 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 715 |
+
"es-define-property": "^1.0.1",
|
| 716 |
+
"es-errors": "^1.3.0",
|
| 717 |
+
"es-object-atoms": "^1.1.1",
|
| 718 |
+
"function-bind": "^1.1.2",
|
| 719 |
+
"get-proto": "^1.0.1",
|
| 720 |
+
"gopd": "^1.2.0",
|
| 721 |
+
"has-symbols": "^1.1.0",
|
| 722 |
+
"hasown": "^2.0.2",
|
| 723 |
+
"math-intrinsics": "^1.1.0"
|
| 724 |
+
},
|
| 725 |
+
"engines": {
|
| 726 |
+
"node": ">= 0.4"
|
| 727 |
+
},
|
| 728 |
+
"funding": {
|
| 729 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 730 |
+
}
|
| 731 |
+
},
|
| 732 |
+
"node_modules/get-proto": {
|
| 733 |
+
"version": "1.0.1",
|
| 734 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 735 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 736 |
+
"license": "MIT",
|
| 737 |
+
"dependencies": {
|
| 738 |
+
"dunder-proto": "^1.0.1",
|
| 739 |
+
"es-object-atoms": "^1.0.0"
|
| 740 |
+
},
|
| 741 |
+
"engines": {
|
| 742 |
+
"node": ">= 0.4"
|
| 743 |
+
}
|
| 744 |
+
},
|
| 745 |
+
"node_modules/glob-parent": {
|
| 746 |
+
"version": "5.1.2",
|
| 747 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
| 748 |
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
| 749 |
+
"dev": true,
|
| 750 |
+
"license": "ISC",
|
| 751 |
+
"dependencies": {
|
| 752 |
+
"is-glob": "^4.0.1"
|
| 753 |
+
},
|
| 754 |
+
"engines": {
|
| 755 |
+
"node": ">= 6"
|
| 756 |
+
}
|
| 757 |
+
},
|
| 758 |
+
"node_modules/gopd": {
|
| 759 |
+
"version": "1.2.0",
|
| 760 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 761 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 762 |
+
"license": "MIT",
|
| 763 |
+
"engines": {
|
| 764 |
+
"node": ">= 0.4"
|
| 765 |
+
},
|
| 766 |
+
"funding": {
|
| 767 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 768 |
+
}
|
| 769 |
+
},
|
| 770 |
+
"node_modules/has-flag": {
|
| 771 |
+
"version": "3.0.0",
|
| 772 |
+
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
| 773 |
+
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
| 774 |
+
"dev": true,
|
| 775 |
+
"license": "MIT",
|
| 776 |
+
"engines": {
|
| 777 |
+
"node": ">=4"
|
| 778 |
+
}
|
| 779 |
+
},
|
| 780 |
+
"node_modules/has-symbols": {
|
| 781 |
+
"version": "1.1.0",
|
| 782 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 783 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 784 |
+
"license": "MIT",
|
| 785 |
+
"engines": {
|
| 786 |
+
"node": ">= 0.4"
|
| 787 |
+
},
|
| 788 |
+
"funding": {
|
| 789 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 790 |
+
}
|
| 791 |
+
},
|
| 792 |
+
"node_modules/has-tostringtag": {
|
| 793 |
+
"version": "1.0.2",
|
| 794 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
| 795 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
| 796 |
+
"license": "MIT",
|
| 797 |
+
"dependencies": {
|
| 798 |
+
"has-symbols": "^1.0.3"
|
| 799 |
+
},
|
| 800 |
+
"engines": {
|
| 801 |
+
"node": ">= 0.4"
|
| 802 |
+
},
|
| 803 |
+
"funding": {
|
| 804 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 805 |
+
}
|
| 806 |
+
},
|
| 807 |
+
"node_modules/hasown": {
|
| 808 |
+
"version": "2.0.4",
|
| 809 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
| 810 |
+
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
| 811 |
+
"license": "MIT",
|
| 812 |
+
"dependencies": {
|
| 813 |
+
"function-bind": "^1.1.2"
|
| 814 |
+
},
|
| 815 |
+
"engines": {
|
| 816 |
+
"node": ">= 0.4"
|
| 817 |
+
}
|
| 818 |
+
},
|
| 819 |
+
"node_modules/helmet": {
|
| 820 |
+
"version": "7.2.0",
|
| 821 |
+
"resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz",
|
| 822 |
+
"integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==",
|
| 823 |
+
"license": "MIT",
|
| 824 |
+
"engines": {
|
| 825 |
+
"node": ">=16.0.0"
|
| 826 |
+
}
|
| 827 |
+
},
|
| 828 |
+
"node_modules/http-errors": {
|
| 829 |
+
"version": "2.0.1",
|
| 830 |
+
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
| 831 |
+
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
| 832 |
+
"license": "MIT",
|
| 833 |
+
"dependencies": {
|
| 834 |
+
"depd": "~2.0.0",
|
| 835 |
+
"inherits": "~2.0.4",
|
| 836 |
+
"setprototypeof": "~1.2.0",
|
| 837 |
+
"statuses": "~2.0.2",
|
| 838 |
+
"toidentifier": "~1.0.1"
|
| 839 |
+
},
|
| 840 |
+
"engines": {
|
| 841 |
+
"node": ">= 0.8"
|
| 842 |
+
},
|
| 843 |
+
"funding": {
|
| 844 |
+
"type": "opencollective",
|
| 845 |
+
"url": "https://opencollective.com/express"
|
| 846 |
+
}
|
| 847 |
+
},
|
| 848 |
+
"node_modules/https-proxy-agent": {
|
| 849 |
+
"version": "5.0.1",
|
| 850 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
| 851 |
+
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
| 852 |
+
"license": "MIT",
|
| 853 |
+
"dependencies": {
|
| 854 |
+
"agent-base": "6",
|
| 855 |
+
"debug": "4"
|
| 856 |
+
},
|
| 857 |
+
"engines": {
|
| 858 |
+
"node": ">= 6"
|
| 859 |
+
}
|
| 860 |
+
},
|
| 861 |
+
"node_modules/https-proxy-agent/node_modules/debug": {
|
| 862 |
+
"version": "4.4.3",
|
| 863 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 864 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 865 |
+
"license": "MIT",
|
| 866 |
+
"dependencies": {
|
| 867 |
+
"ms": "^2.1.3"
|
| 868 |
+
},
|
| 869 |
+
"engines": {
|
| 870 |
+
"node": ">=6.0"
|
| 871 |
+
},
|
| 872 |
+
"peerDependenciesMeta": {
|
| 873 |
+
"supports-color": {
|
| 874 |
+
"optional": true
|
| 875 |
+
}
|
| 876 |
+
}
|
| 877 |
+
},
|
| 878 |
+
"node_modules/https-proxy-agent/node_modules/ms": {
|
| 879 |
+
"version": "2.1.3",
|
| 880 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 881 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 882 |
+
"license": "MIT"
|
| 883 |
+
},
|
| 884 |
+
"node_modules/iconv-lite": {
|
| 885 |
+
"version": "0.4.24",
|
| 886 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
| 887 |
+
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
| 888 |
+
"license": "MIT",
|
| 889 |
+
"dependencies": {
|
| 890 |
+
"safer-buffer": ">= 2.1.2 < 3"
|
| 891 |
+
},
|
| 892 |
+
"engines": {
|
| 893 |
+
"node": ">=0.10.0"
|
| 894 |
+
}
|
| 895 |
+
},
|
| 896 |
+
"node_modules/ignore-by-default": {
|
| 897 |
+
"version": "1.0.1",
|
| 898 |
+
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
| 899 |
+
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
| 900 |
+
"dev": true,
|
| 901 |
+
"license": "ISC"
|
| 902 |
+
},
|
| 903 |
+
"node_modules/inherits": {
|
| 904 |
+
"version": "2.0.4",
|
| 905 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 906 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 907 |
+
"license": "ISC"
|
| 908 |
+
},
|
| 909 |
+
"node_modules/ipaddr.js": {
|
| 910 |
+
"version": "1.9.1",
|
| 911 |
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
| 912 |
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
| 913 |
+
"license": "MIT",
|
| 914 |
+
"engines": {
|
| 915 |
+
"node": ">= 0.10"
|
| 916 |
+
}
|
| 917 |
+
},
|
| 918 |
+
"node_modules/is-binary-path": {
|
| 919 |
+
"version": "2.1.0",
|
| 920 |
+
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
| 921 |
+
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
| 922 |
+
"dev": true,
|
| 923 |
+
"license": "MIT",
|
| 924 |
+
"dependencies": {
|
| 925 |
+
"binary-extensions": "^2.0.0"
|
| 926 |
+
},
|
| 927 |
+
"engines": {
|
| 928 |
+
"node": ">=8"
|
| 929 |
+
}
|
| 930 |
+
},
|
| 931 |
+
"node_modules/is-extglob": {
|
| 932 |
+
"version": "2.1.1",
|
| 933 |
+
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
| 934 |
+
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
| 935 |
+
"dev": true,
|
| 936 |
+
"license": "MIT",
|
| 937 |
+
"engines": {
|
| 938 |
+
"node": ">=0.10.0"
|
| 939 |
+
}
|
| 940 |
+
},
|
| 941 |
+
"node_modules/is-glob": {
|
| 942 |
+
"version": "4.0.3",
|
| 943 |
+
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
| 944 |
+
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
| 945 |
+
"dev": true,
|
| 946 |
+
"license": "MIT",
|
| 947 |
+
"dependencies": {
|
| 948 |
+
"is-extglob": "^2.1.1"
|
| 949 |
+
},
|
| 950 |
+
"engines": {
|
| 951 |
+
"node": ">=0.10.0"
|
| 952 |
+
}
|
| 953 |
+
},
|
| 954 |
+
"node_modules/is-number": {
|
| 955 |
+
"version": "7.0.0",
|
| 956 |
+
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
| 957 |
+
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
| 958 |
+
"dev": true,
|
| 959 |
+
"license": "MIT",
|
| 960 |
+
"engines": {
|
| 961 |
+
"node": ">=0.12.0"
|
| 962 |
+
}
|
| 963 |
+
},
|
| 964 |
+
"node_modules/math-intrinsics": {
|
| 965 |
+
"version": "1.1.0",
|
| 966 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 967 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 968 |
+
"license": "MIT",
|
| 969 |
+
"engines": {
|
| 970 |
+
"node": ">= 0.4"
|
| 971 |
+
}
|
| 972 |
+
},
|
| 973 |
+
"node_modules/media-typer": {
|
| 974 |
+
"version": "0.3.0",
|
| 975 |
+
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
| 976 |
+
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
| 977 |
+
"license": "MIT",
|
| 978 |
+
"engines": {
|
| 979 |
+
"node": ">= 0.6"
|
| 980 |
+
}
|
| 981 |
+
},
|
| 982 |
+
"node_modules/merge-descriptors": {
|
| 983 |
+
"version": "1.0.3",
|
| 984 |
+
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
| 985 |
+
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
| 986 |
+
"license": "MIT",
|
| 987 |
+
"funding": {
|
| 988 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 989 |
+
}
|
| 990 |
+
},
|
| 991 |
+
"node_modules/methods": {
|
| 992 |
+
"version": "1.1.2",
|
| 993 |
+
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
| 994 |
+
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
| 995 |
+
"license": "MIT",
|
| 996 |
+
"engines": {
|
| 997 |
+
"node": ">= 0.6"
|
| 998 |
+
}
|
| 999 |
+
},
|
| 1000 |
+
"node_modules/mime": {
|
| 1001 |
+
"version": "1.6.0",
|
| 1002 |
+
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
| 1003 |
+
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
| 1004 |
+
"license": "MIT",
|
| 1005 |
+
"bin": {
|
| 1006 |
+
"mime": "cli.js"
|
| 1007 |
+
},
|
| 1008 |
+
"engines": {
|
| 1009 |
+
"node": ">=4"
|
| 1010 |
+
}
|
| 1011 |
+
},
|
| 1012 |
+
"node_modules/mime-db": {
|
| 1013 |
+
"version": "1.52.0",
|
| 1014 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 1015 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 1016 |
+
"license": "MIT",
|
| 1017 |
+
"engines": {
|
| 1018 |
+
"node": ">= 0.6"
|
| 1019 |
+
}
|
| 1020 |
+
},
|
| 1021 |
+
"node_modules/mime-types": {
|
| 1022 |
+
"version": "2.1.35",
|
| 1023 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 1024 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 1025 |
+
"license": "MIT",
|
| 1026 |
+
"dependencies": {
|
| 1027 |
+
"mime-db": "1.52.0"
|
| 1028 |
+
},
|
| 1029 |
+
"engines": {
|
| 1030 |
+
"node": ">= 0.6"
|
| 1031 |
+
}
|
| 1032 |
+
},
|
| 1033 |
+
"node_modules/minimatch": {
|
| 1034 |
+
"version": "10.2.5",
|
| 1035 |
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
| 1036 |
+
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
| 1037 |
+
"dev": true,
|
| 1038 |
+
"license": "BlueOak-1.0.0",
|
| 1039 |
+
"dependencies": {
|
| 1040 |
+
"brace-expansion": "^5.0.5"
|
| 1041 |
+
},
|
| 1042 |
+
"engines": {
|
| 1043 |
+
"node": "18 || 20 || >=22"
|
| 1044 |
+
},
|
| 1045 |
+
"funding": {
|
| 1046 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 1047 |
+
}
|
| 1048 |
+
},
|
| 1049 |
+
"node_modules/morgan": {
|
| 1050 |
+
"version": "1.11.0",
|
| 1051 |
+
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz",
|
| 1052 |
+
"integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==",
|
| 1053 |
+
"license": "MIT",
|
| 1054 |
+
"dependencies": {
|
| 1055 |
+
"basic-auth": "~2.0.1",
|
| 1056 |
+
"debug": "2.6.9",
|
| 1057 |
+
"depd": "~2.0.0",
|
| 1058 |
+
"on-finished": "~2.4.1",
|
| 1059 |
+
"on-headers": "~1.1.0"
|
| 1060 |
+
},
|
| 1061 |
+
"engines": {
|
| 1062 |
+
"node": ">= 0.8.0"
|
| 1063 |
+
},
|
| 1064 |
+
"funding": {
|
| 1065 |
+
"type": "opencollective",
|
| 1066 |
+
"url": "https://opencollective.com/express"
|
| 1067 |
+
}
|
| 1068 |
+
},
|
| 1069 |
+
"node_modules/ms": {
|
| 1070 |
+
"version": "2.0.0",
|
| 1071 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
| 1072 |
+
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
| 1073 |
+
"license": "MIT"
|
| 1074 |
+
},
|
| 1075 |
+
"node_modules/negotiator": {
|
| 1076 |
+
"version": "0.6.3",
|
| 1077 |
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
| 1078 |
+
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
| 1079 |
+
"license": "MIT",
|
| 1080 |
+
"engines": {
|
| 1081 |
+
"node": ">= 0.6"
|
| 1082 |
+
}
|
| 1083 |
+
},
|
| 1084 |
+
"node_modules/nodemon": {
|
| 1085 |
+
"version": "3.1.14",
|
| 1086 |
+
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
| 1087 |
+
"integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
|
| 1088 |
+
"dev": true,
|
| 1089 |
+
"license": "MIT",
|
| 1090 |
+
"dependencies": {
|
| 1091 |
+
"chokidar": "^3.5.2",
|
| 1092 |
+
"debug": "^4",
|
| 1093 |
+
"ignore-by-default": "^1.0.1",
|
| 1094 |
+
"minimatch": "^10.2.1",
|
| 1095 |
+
"pstree.remy": "^1.1.8",
|
| 1096 |
+
"semver": "^7.5.3",
|
| 1097 |
+
"simple-update-notifier": "^2.0.0",
|
| 1098 |
+
"supports-color": "^5.5.0",
|
| 1099 |
+
"touch": "^3.1.0",
|
| 1100 |
+
"undefsafe": "^2.0.5"
|
| 1101 |
+
},
|
| 1102 |
+
"bin": {
|
| 1103 |
+
"nodemon": "bin/nodemon.js"
|
| 1104 |
+
},
|
| 1105 |
+
"engines": {
|
| 1106 |
+
"node": ">=10"
|
| 1107 |
+
},
|
| 1108 |
+
"funding": {
|
| 1109 |
+
"type": "opencollective",
|
| 1110 |
+
"url": "https://opencollective.com/nodemon"
|
| 1111 |
+
}
|
| 1112 |
+
},
|
| 1113 |
+
"node_modules/nodemon/node_modules/debug": {
|
| 1114 |
+
"version": "4.4.3",
|
| 1115 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1116 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1117 |
+
"dev": true,
|
| 1118 |
+
"license": "MIT",
|
| 1119 |
+
"dependencies": {
|
| 1120 |
+
"ms": "^2.1.3"
|
| 1121 |
+
},
|
| 1122 |
+
"engines": {
|
| 1123 |
+
"node": ">=6.0"
|
| 1124 |
+
},
|
| 1125 |
+
"peerDependenciesMeta": {
|
| 1126 |
+
"supports-color": {
|
| 1127 |
+
"optional": true
|
| 1128 |
+
}
|
| 1129 |
+
}
|
| 1130 |
+
},
|
| 1131 |
+
"node_modules/nodemon/node_modules/ms": {
|
| 1132 |
+
"version": "2.1.3",
|
| 1133 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1134 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1135 |
+
"dev": true,
|
| 1136 |
+
"license": "MIT"
|
| 1137 |
+
},
|
| 1138 |
+
"node_modules/normalize-path": {
|
| 1139 |
+
"version": "3.0.0",
|
| 1140 |
+
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
| 1141 |
+
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
| 1142 |
+
"dev": true,
|
| 1143 |
+
"license": "MIT",
|
| 1144 |
+
"engines": {
|
| 1145 |
+
"node": ">=0.10.0"
|
| 1146 |
+
}
|
| 1147 |
+
},
|
| 1148 |
+
"node_modules/object-assign": {
|
| 1149 |
+
"version": "4.1.1",
|
| 1150 |
+
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
| 1151 |
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
| 1152 |
+
"license": "MIT",
|
| 1153 |
+
"engines": {
|
| 1154 |
+
"node": ">=0.10.0"
|
| 1155 |
+
}
|
| 1156 |
+
},
|
| 1157 |
+
"node_modules/object-inspect": {
|
| 1158 |
+
"version": "1.13.4",
|
| 1159 |
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
| 1160 |
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
| 1161 |
+
"license": "MIT",
|
| 1162 |
+
"engines": {
|
| 1163 |
+
"node": ">= 0.4"
|
| 1164 |
+
},
|
| 1165 |
+
"funding": {
|
| 1166 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1167 |
+
}
|
| 1168 |
+
},
|
| 1169 |
+
"node_modules/on-finished": {
|
| 1170 |
+
"version": "2.4.1",
|
| 1171 |
+
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
| 1172 |
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
| 1173 |
+
"license": "MIT",
|
| 1174 |
+
"dependencies": {
|
| 1175 |
+
"ee-first": "1.1.1"
|
| 1176 |
+
},
|
| 1177 |
+
"engines": {
|
| 1178 |
+
"node": ">= 0.8"
|
| 1179 |
+
}
|
| 1180 |
+
},
|
| 1181 |
+
"node_modules/on-headers": {
|
| 1182 |
+
"version": "1.1.0",
|
| 1183 |
+
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
| 1184 |
+
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
| 1185 |
+
"license": "MIT",
|
| 1186 |
+
"engines": {
|
| 1187 |
+
"node": ">= 0.8"
|
| 1188 |
+
}
|
| 1189 |
+
},
|
| 1190 |
+
"node_modules/parseurl": {
|
| 1191 |
+
"version": "1.3.3",
|
| 1192 |
+
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
| 1193 |
+
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
| 1194 |
+
"license": "MIT",
|
| 1195 |
+
"engines": {
|
| 1196 |
+
"node": ">= 0.8"
|
| 1197 |
+
}
|
| 1198 |
+
},
|
| 1199 |
+
"node_modules/path-to-regexp": {
|
| 1200 |
+
"version": "0.1.13",
|
| 1201 |
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
| 1202 |
+
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
| 1203 |
+
"license": "MIT"
|
| 1204 |
+
},
|
| 1205 |
+
"node_modules/picomatch": {
|
| 1206 |
+
"version": "2.3.2",
|
| 1207 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
| 1208 |
+
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
| 1209 |
+
"dev": true,
|
| 1210 |
+
"license": "MIT",
|
| 1211 |
+
"engines": {
|
| 1212 |
+
"node": ">=8.6"
|
| 1213 |
+
},
|
| 1214 |
+
"funding": {
|
| 1215 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 1216 |
+
}
|
| 1217 |
+
},
|
| 1218 |
+
"node_modules/prisma": {
|
| 1219 |
+
"version": "5.22.0",
|
| 1220 |
+
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
| 1221 |
+
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
| 1222 |
+
"devOptional": true,
|
| 1223 |
+
"hasInstallScript": true,
|
| 1224 |
+
"license": "Apache-2.0",
|
| 1225 |
+
"peer": true,
|
| 1226 |
+
"dependencies": {
|
| 1227 |
+
"@prisma/engines": "5.22.0"
|
| 1228 |
+
},
|
| 1229 |
+
"bin": {
|
| 1230 |
+
"prisma": "build/index.js"
|
| 1231 |
+
},
|
| 1232 |
+
"engines": {
|
| 1233 |
+
"node": ">=16.13"
|
| 1234 |
+
},
|
| 1235 |
+
"optionalDependencies": {
|
| 1236 |
+
"fsevents": "2.3.3"
|
| 1237 |
+
}
|
| 1238 |
+
},
|
| 1239 |
+
"node_modules/proxy-addr": {
|
| 1240 |
+
"version": "2.0.7",
|
| 1241 |
+
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
| 1242 |
+
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
| 1243 |
+
"license": "MIT",
|
| 1244 |
+
"dependencies": {
|
| 1245 |
+
"forwarded": "0.2.0",
|
| 1246 |
+
"ipaddr.js": "1.9.1"
|
| 1247 |
+
},
|
| 1248 |
+
"engines": {
|
| 1249 |
+
"node": ">= 0.10"
|
| 1250 |
+
}
|
| 1251 |
+
},
|
| 1252 |
+
"node_modules/proxy-from-env": {
|
| 1253 |
+
"version": "2.1.0",
|
| 1254 |
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
| 1255 |
+
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
| 1256 |
+
"license": "MIT",
|
| 1257 |
+
"engines": {
|
| 1258 |
+
"node": ">=10"
|
| 1259 |
+
}
|
| 1260 |
+
},
|
| 1261 |
+
"node_modules/pstree.remy": {
|
| 1262 |
+
"version": "1.1.8",
|
| 1263 |
+
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
| 1264 |
+
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
| 1265 |
+
"dev": true,
|
| 1266 |
+
"license": "MIT"
|
| 1267 |
+
},
|
| 1268 |
+
"node_modules/qs": {
|
| 1269 |
+
"version": "6.15.3",
|
| 1270 |
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
| 1271 |
+
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
| 1272 |
+
"license": "BSD-3-Clause",
|
| 1273 |
+
"dependencies": {
|
| 1274 |
+
"es-define-property": "^1.0.1",
|
| 1275 |
+
"side-channel": "^1.1.1"
|
| 1276 |
+
},
|
| 1277 |
+
"engines": {
|
| 1278 |
+
"node": ">=0.6"
|
| 1279 |
+
},
|
| 1280 |
+
"funding": {
|
| 1281 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1282 |
+
}
|
| 1283 |
+
},
|
| 1284 |
+
"node_modules/range-parser": {
|
| 1285 |
+
"version": "1.2.1",
|
| 1286 |
+
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
| 1287 |
+
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
| 1288 |
+
"license": "MIT",
|
| 1289 |
+
"engines": {
|
| 1290 |
+
"node": ">= 0.6"
|
| 1291 |
+
}
|
| 1292 |
+
},
|
| 1293 |
+
"node_modules/raw-body": {
|
| 1294 |
+
"version": "2.5.3",
|
| 1295 |
+
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
| 1296 |
+
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
| 1297 |
+
"license": "MIT",
|
| 1298 |
+
"dependencies": {
|
| 1299 |
+
"bytes": "~3.1.2",
|
| 1300 |
+
"http-errors": "~2.0.1",
|
| 1301 |
+
"iconv-lite": "~0.4.24",
|
| 1302 |
+
"unpipe": "~1.0.0"
|
| 1303 |
+
},
|
| 1304 |
+
"engines": {
|
| 1305 |
+
"node": ">= 0.8"
|
| 1306 |
+
}
|
| 1307 |
+
},
|
| 1308 |
+
"node_modules/readdirp": {
|
| 1309 |
+
"version": "3.6.0",
|
| 1310 |
+
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
| 1311 |
+
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
| 1312 |
+
"dev": true,
|
| 1313 |
+
"license": "MIT",
|
| 1314 |
+
"dependencies": {
|
| 1315 |
+
"picomatch": "^2.2.1"
|
| 1316 |
+
},
|
| 1317 |
+
"engines": {
|
| 1318 |
+
"node": ">=8.10.0"
|
| 1319 |
+
}
|
| 1320 |
+
},
|
| 1321 |
+
"node_modules/safe-buffer": {
|
| 1322 |
+
"version": "5.2.1",
|
| 1323 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
| 1324 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
| 1325 |
+
"funding": [
|
| 1326 |
+
{
|
| 1327 |
+
"type": "github",
|
| 1328 |
+
"url": "https://github.com/sponsors/feross"
|
| 1329 |
+
},
|
| 1330 |
+
{
|
| 1331 |
+
"type": "patreon",
|
| 1332 |
+
"url": "https://www.patreon.com/feross"
|
| 1333 |
+
},
|
| 1334 |
+
{
|
| 1335 |
+
"type": "consulting",
|
| 1336 |
+
"url": "https://feross.org/support"
|
| 1337 |
+
}
|
| 1338 |
+
],
|
| 1339 |
+
"license": "MIT"
|
| 1340 |
+
},
|
| 1341 |
+
"node_modules/safer-buffer": {
|
| 1342 |
+
"version": "2.1.2",
|
| 1343 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 1344 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 1345 |
+
"license": "MIT"
|
| 1346 |
+
},
|
| 1347 |
+
"node_modules/semver": {
|
| 1348 |
+
"version": "7.8.5",
|
| 1349 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
| 1350 |
+
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
| 1351 |
+
"dev": true,
|
| 1352 |
+
"license": "ISC",
|
| 1353 |
+
"bin": {
|
| 1354 |
+
"semver": "bin/semver.js"
|
| 1355 |
+
},
|
| 1356 |
+
"engines": {
|
| 1357 |
+
"node": ">=10"
|
| 1358 |
+
}
|
| 1359 |
+
},
|
| 1360 |
+
"node_modules/send": {
|
| 1361 |
+
"version": "0.19.2",
|
| 1362 |
+
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
| 1363 |
+
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
| 1364 |
+
"license": "MIT",
|
| 1365 |
+
"dependencies": {
|
| 1366 |
+
"debug": "2.6.9",
|
| 1367 |
+
"depd": "2.0.0",
|
| 1368 |
+
"destroy": "1.2.0",
|
| 1369 |
+
"encodeurl": "~2.0.0",
|
| 1370 |
+
"escape-html": "~1.0.3",
|
| 1371 |
+
"etag": "~1.8.1",
|
| 1372 |
+
"fresh": "~0.5.2",
|
| 1373 |
+
"http-errors": "~2.0.1",
|
| 1374 |
+
"mime": "1.6.0",
|
| 1375 |
+
"ms": "2.1.3",
|
| 1376 |
+
"on-finished": "~2.4.1",
|
| 1377 |
+
"range-parser": "~1.2.1",
|
| 1378 |
+
"statuses": "~2.0.2"
|
| 1379 |
+
},
|
| 1380 |
+
"engines": {
|
| 1381 |
+
"node": ">= 0.8.0"
|
| 1382 |
+
}
|
| 1383 |
+
},
|
| 1384 |
+
"node_modules/send/node_modules/ms": {
|
| 1385 |
+
"version": "2.1.3",
|
| 1386 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1387 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1388 |
+
"license": "MIT"
|
| 1389 |
+
},
|
| 1390 |
+
"node_modules/serve-static": {
|
| 1391 |
+
"version": "1.16.3",
|
| 1392 |
+
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
| 1393 |
+
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
| 1394 |
+
"license": "MIT",
|
| 1395 |
+
"dependencies": {
|
| 1396 |
+
"encodeurl": "~2.0.0",
|
| 1397 |
+
"escape-html": "~1.0.3",
|
| 1398 |
+
"parseurl": "~1.3.3",
|
| 1399 |
+
"send": "~0.19.1"
|
| 1400 |
+
},
|
| 1401 |
+
"engines": {
|
| 1402 |
+
"node": ">= 0.8.0"
|
| 1403 |
+
}
|
| 1404 |
+
},
|
| 1405 |
+
"node_modules/setprototypeof": {
|
| 1406 |
+
"version": "1.2.0",
|
| 1407 |
+
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
| 1408 |
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
| 1409 |
+
"license": "ISC"
|
| 1410 |
+
},
|
| 1411 |
+
"node_modules/side-channel": {
|
| 1412 |
+
"version": "1.1.1",
|
| 1413 |
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
| 1414 |
+
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
| 1415 |
+
"license": "MIT",
|
| 1416 |
+
"dependencies": {
|
| 1417 |
+
"es-errors": "^1.3.0",
|
| 1418 |
+
"object-inspect": "^1.13.4",
|
| 1419 |
+
"side-channel-list": "^1.0.1",
|
| 1420 |
+
"side-channel-map": "^1.0.1",
|
| 1421 |
+
"side-channel-weakmap": "^1.0.2"
|
| 1422 |
+
},
|
| 1423 |
+
"engines": {
|
| 1424 |
+
"node": ">= 0.4"
|
| 1425 |
+
},
|
| 1426 |
+
"funding": {
|
| 1427 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1428 |
+
}
|
| 1429 |
+
},
|
| 1430 |
+
"node_modules/side-channel-list": {
|
| 1431 |
+
"version": "1.0.1",
|
| 1432 |
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
| 1433 |
+
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
| 1434 |
+
"license": "MIT",
|
| 1435 |
+
"dependencies": {
|
| 1436 |
+
"es-errors": "^1.3.0",
|
| 1437 |
+
"object-inspect": "^1.13.4"
|
| 1438 |
+
},
|
| 1439 |
+
"engines": {
|
| 1440 |
+
"node": ">= 0.4"
|
| 1441 |
+
},
|
| 1442 |
+
"funding": {
|
| 1443 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1444 |
+
}
|
| 1445 |
+
},
|
| 1446 |
+
"node_modules/side-channel-map": {
|
| 1447 |
+
"version": "1.0.1",
|
| 1448 |
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
| 1449 |
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
| 1450 |
+
"license": "MIT",
|
| 1451 |
+
"dependencies": {
|
| 1452 |
+
"call-bound": "^1.0.2",
|
| 1453 |
+
"es-errors": "^1.3.0",
|
| 1454 |
+
"get-intrinsic": "^1.2.5",
|
| 1455 |
+
"object-inspect": "^1.13.3"
|
| 1456 |
+
},
|
| 1457 |
+
"engines": {
|
| 1458 |
+
"node": ">= 0.4"
|
| 1459 |
+
},
|
| 1460 |
+
"funding": {
|
| 1461 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1462 |
+
}
|
| 1463 |
+
},
|
| 1464 |
+
"node_modules/side-channel-weakmap": {
|
| 1465 |
+
"version": "1.0.2",
|
| 1466 |
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
| 1467 |
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
| 1468 |
+
"license": "MIT",
|
| 1469 |
+
"dependencies": {
|
| 1470 |
+
"call-bound": "^1.0.2",
|
| 1471 |
+
"es-errors": "^1.3.0",
|
| 1472 |
+
"get-intrinsic": "^1.2.5",
|
| 1473 |
+
"object-inspect": "^1.13.3",
|
| 1474 |
+
"side-channel-map": "^1.0.1"
|
| 1475 |
+
},
|
| 1476 |
+
"engines": {
|
| 1477 |
+
"node": ">= 0.4"
|
| 1478 |
+
},
|
| 1479 |
+
"funding": {
|
| 1480 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 1481 |
+
}
|
| 1482 |
+
},
|
| 1483 |
+
"node_modules/simple-update-notifier": {
|
| 1484 |
+
"version": "2.0.0",
|
| 1485 |
+
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
| 1486 |
+
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
| 1487 |
+
"dev": true,
|
| 1488 |
+
"license": "MIT",
|
| 1489 |
+
"dependencies": {
|
| 1490 |
+
"semver": "^7.5.3"
|
| 1491 |
+
},
|
| 1492 |
+
"engines": {
|
| 1493 |
+
"node": ">=10"
|
| 1494 |
+
}
|
| 1495 |
+
},
|
| 1496 |
+
"node_modules/statuses": {
|
| 1497 |
+
"version": "2.0.2",
|
| 1498 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
| 1499 |
+
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
| 1500 |
+
"license": "MIT",
|
| 1501 |
+
"engines": {
|
| 1502 |
+
"node": ">= 0.8"
|
| 1503 |
+
}
|
| 1504 |
+
},
|
| 1505 |
+
"node_modules/supports-color": {
|
| 1506 |
+
"version": "5.5.0",
|
| 1507 |
+
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
| 1508 |
+
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
| 1509 |
+
"dev": true,
|
| 1510 |
+
"license": "MIT",
|
| 1511 |
+
"dependencies": {
|
| 1512 |
+
"has-flag": "^3.0.0"
|
| 1513 |
+
},
|
| 1514 |
+
"engines": {
|
| 1515 |
+
"node": ">=4"
|
| 1516 |
+
}
|
| 1517 |
+
},
|
| 1518 |
+
"node_modules/to-regex-range": {
|
| 1519 |
+
"version": "5.0.1",
|
| 1520 |
+
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
| 1521 |
+
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
| 1522 |
+
"dev": true,
|
| 1523 |
+
"license": "MIT",
|
| 1524 |
+
"dependencies": {
|
| 1525 |
+
"is-number": "^7.0.0"
|
| 1526 |
+
},
|
| 1527 |
+
"engines": {
|
| 1528 |
+
"node": ">=8.0"
|
| 1529 |
+
}
|
| 1530 |
+
},
|
| 1531 |
+
"node_modules/toidentifier": {
|
| 1532 |
+
"version": "1.0.1",
|
| 1533 |
+
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
| 1534 |
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
| 1535 |
+
"license": "MIT",
|
| 1536 |
+
"engines": {
|
| 1537 |
+
"node": ">=0.6"
|
| 1538 |
+
}
|
| 1539 |
+
},
|
| 1540 |
+
"node_modules/touch": {
|
| 1541 |
+
"version": "3.1.1",
|
| 1542 |
+
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
| 1543 |
+
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
|
| 1544 |
+
"dev": true,
|
| 1545 |
+
"license": "ISC",
|
| 1546 |
+
"bin": {
|
| 1547 |
+
"nodetouch": "bin/nodetouch.js"
|
| 1548 |
+
}
|
| 1549 |
+
},
|
| 1550 |
+
"node_modules/type-is": {
|
| 1551 |
+
"version": "1.6.18",
|
| 1552 |
+
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
| 1553 |
+
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
| 1554 |
+
"license": "MIT",
|
| 1555 |
+
"dependencies": {
|
| 1556 |
+
"media-typer": "0.3.0",
|
| 1557 |
+
"mime-types": "~2.1.24"
|
| 1558 |
+
},
|
| 1559 |
+
"engines": {
|
| 1560 |
+
"node": ">= 0.6"
|
| 1561 |
+
}
|
| 1562 |
+
},
|
| 1563 |
+
"node_modules/undefsafe": {
|
| 1564 |
+
"version": "2.0.5",
|
| 1565 |
+
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
| 1566 |
+
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
| 1567 |
+
"dev": true,
|
| 1568 |
+
"license": "MIT"
|
| 1569 |
+
},
|
| 1570 |
+
"node_modules/unpipe": {
|
| 1571 |
+
"version": "1.0.0",
|
| 1572 |
+
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
| 1573 |
+
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
| 1574 |
+
"license": "MIT",
|
| 1575 |
+
"engines": {
|
| 1576 |
+
"node": ">= 0.8"
|
| 1577 |
+
}
|
| 1578 |
+
},
|
| 1579 |
+
"node_modules/utils-merge": {
|
| 1580 |
+
"version": "1.0.1",
|
| 1581 |
+
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
| 1582 |
+
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
| 1583 |
+
"license": "MIT",
|
| 1584 |
+
"engines": {
|
| 1585 |
+
"node": ">= 0.4.0"
|
| 1586 |
+
}
|
| 1587 |
+
},
|
| 1588 |
+
"node_modules/vary": {
|
| 1589 |
+
"version": "1.1.2",
|
| 1590 |
+
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
| 1591 |
+
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
| 1592 |
+
"license": "MIT",
|
| 1593 |
+
"engines": {
|
| 1594 |
+
"node": ">= 0.8"
|
| 1595 |
+
}
|
| 1596 |
+
},
|
| 1597 |
+
"node_modules/zod": {
|
| 1598 |
+
"version": "3.25.76",
|
| 1599 |
+
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
| 1600 |
+
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
| 1601 |
+
"license": "MIT",
|
| 1602 |
+
"funding": {
|
| 1603 |
+
"url": "https://github.com/sponsors/colinhacks"
|
| 1604 |
+
}
|
| 1605 |
+
}
|
| 1606 |
+
}
|
| 1607 |
+
}
|
backend/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "screening-room-backend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Express Backend API for The Screening Room",
|
| 5 |
+
"main": "src/server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node src/server.js",
|
| 8 |
+
"dev": "node src/server.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@prisma/client": "^5.22.0",
|
| 12 |
+
"axios": "^1.7.2",
|
| 13 |
+
"cors": "^2.8.5",
|
| 14 |
+
"dotenv": "^16.4.5",
|
| 15 |
+
"express": "^4.19.2",
|
| 16 |
+
"express-rate-limit": "^7.3.1",
|
| 17 |
+
"helmet": "^7.1.0",
|
| 18 |
+
"morgan": "^1.10.0",
|
| 19 |
+
"zod": "^3.23.8"
|
| 20 |
+
},
|
| 21 |
+
"devDependencies": {
|
| 22 |
+
"nodemon": "^3.1.4",
|
| 23 |
+
"prisma": "^5.22.0"
|
| 24 |
+
}
|
| 25 |
+
}
|
backend/prisma/schema.prisma
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generator client {
|
| 2 |
+
provider = "prisma-client-js"
|
| 3 |
+
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
datasource db {
|
| 7 |
+
provider = "sqlite"
|
| 8 |
+
url = "file:./dev.db"
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
model Prediction {
|
| 12 |
+
id String @id @default(cuid())
|
| 13 |
+
sessionId String
|
| 14 |
+
text String
|
| 15 |
+
lrResult String
|
| 16 |
+
lstmResult String
|
| 17 |
+
bertResult String
|
| 18 |
+
majorityLabel String
|
| 19 |
+
agreement String
|
| 20 |
+
totalLatencyMs Float
|
| 21 |
+
createdAt DateTime @default(now())
|
| 22 |
+
|
| 23 |
+
@@index([sessionId])
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
model ModelMetric {
|
| 27 |
+
id String @id @default(cuid())
|
| 28 |
+
modelName String @unique
|
| 29 |
+
accuracy Float
|
| 30 |
+
precision Float
|
| 31 |
+
recall Float
|
| 32 |
+
f1 Float
|
| 33 |
+
params Int
|
| 34 |
+
sizeBytes BigInt
|
| 35 |
+
inferenceMs Float
|
| 36 |
+
updatedAt DateTime @updatedAt
|
| 37 |
+
}
|
backend/prisma/seed.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PrismaClient } = require('@prisma/client');
|
| 2 |
+
const prisma = new PrismaClient();
|
| 3 |
+
|
| 4 |
+
async function main() {
|
| 5 |
+
console.log('Seeding model metrics...');
|
| 6 |
+
|
| 7 |
+
// Logistic Regression Metrics (from 02_ML.ipynb)
|
| 8 |
+
await prisma.modelMetric.upsert({
|
| 9 |
+
where: { modelName: 'Logistic Regression' },
|
| 10 |
+
update: {
|
| 11 |
+
accuracy: 90.15,
|
| 12 |
+
precision: 90.1,
|
| 13 |
+
recall: 90.2,
|
| 14 |
+
f1: 90.1,
|
| 15 |
+
params: 50000,
|
| 16 |
+
sizeBytes: 1500000,
|
| 17 |
+
inferenceMs: 2,
|
| 18 |
+
},
|
| 19 |
+
create: {
|
| 20 |
+
modelName: 'Logistic Regression',
|
| 21 |
+
accuracy: 90.15,
|
| 22 |
+
precision: 90.1,
|
| 23 |
+
recall: 90.2,
|
| 24 |
+
f1: 90.1,
|
| 25 |
+
params: 50000,
|
| 26 |
+
sizeBytes: 1500000,
|
| 27 |
+
inferenceMs: 2,
|
| 28 |
+
},
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
// Bi-LSTM Metrics (from LSTM.ipynb)
|
| 32 |
+
await prisma.modelMetric.upsert({
|
| 33 |
+
where: { modelName: 'Bi-LSTM' },
|
| 34 |
+
update: {
|
| 35 |
+
accuracy: 84.67,
|
| 36 |
+
precision: 85.0,
|
| 37 |
+
recall: 84.5,
|
| 38 |
+
f1: 84.7,
|
| 39 |
+
params: 120000,
|
| 40 |
+
sizeBytes: 5000000,
|
| 41 |
+
inferenceMs: 22,
|
| 42 |
+
},
|
| 43 |
+
create: {
|
| 44 |
+
modelName: 'Bi-LSTM',
|
| 45 |
+
accuracy: 84.67,
|
| 46 |
+
precision: 85.0,
|
| 47 |
+
recall: 84.5,
|
| 48 |
+
f1: 84.7,
|
| 49 |
+
params: 120000,
|
| 50 |
+
sizeBytes: 5000000,
|
| 51 |
+
inferenceMs: 22,
|
| 52 |
+
},
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
// BERT Metrics (from full 25k test set evaluation)
|
| 56 |
+
await prisma.modelMetric.upsert({
|
| 57 |
+
where: { modelName: 'BERT' },
|
| 58 |
+
update: {
|
| 59 |
+
accuracy: 91.68,
|
| 60 |
+
precision: 93.32,
|
| 61 |
+
recall: 89.79,
|
| 62 |
+
f1: 91.52,
|
| 63 |
+
params: 110000000,
|
| 64 |
+
sizeBytes: 420000000,
|
| 65 |
+
inferenceMs: 72,
|
| 66 |
+
},
|
| 67 |
+
create: {
|
| 68 |
+
modelName: 'BERT',
|
| 69 |
+
accuracy: 91.68,
|
| 70 |
+
precision: 93.32,
|
| 71 |
+
recall: 89.79,
|
| 72 |
+
f1: 91.52,
|
| 73 |
+
params: 110000000,
|
| 74 |
+
sizeBytes: 420000000,
|
| 75 |
+
inferenceMs: 72,
|
| 76 |
+
},
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
console.log('Model metrics seeded successfully.');
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
main()
|
| 83 |
+
.catch((e) => {
|
| 84 |
+
console.error(e);
|
| 85 |
+
process.exit(1);
|
| 86 |
+
})
|
| 87 |
+
.finally(async () => {
|
| 88 |
+
await prisma.$disconnect();
|
| 89 |
+
});
|
backend/src/config/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require('dotenv').config();
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
port: process.env.PORT || 4000,
|
| 5 |
+
mlServiceUrl: process.env.ML_SERVICE_URL || 'http://localhost:8000',
|
| 6 |
+
env: process.env.NODE_ENV || 'development'
|
| 7 |
+
};
|
backend/src/controllers/predict.controller.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const mlService = require('../services/mlService');
|
| 2 |
+
const dbService = require('../services/dbService');
|
| 3 |
+
|
| 4 |
+
const predict = async (req, res, next) => {
|
| 5 |
+
try {
|
| 6 |
+
const { text, sessionId } = req.body;
|
| 7 |
+
|
| 8 |
+
// Call FastAPI service
|
| 9 |
+
const predictionResult = await mlService.predict(text);
|
| 10 |
+
|
| 11 |
+
// Persist this prediction using Prisma
|
| 12 |
+
await dbService.savePrediction(sessionId, text, predictionResult);
|
| 13 |
+
|
| 14 |
+
// Return standardized response back to frontend
|
| 15 |
+
res.json(predictionResult);
|
| 16 |
+
} catch (error) {
|
| 17 |
+
next(error);
|
| 18 |
+
}
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
const getHistory = async (req, res, next) => {
|
| 22 |
+
try {
|
| 23 |
+
const { sessionId } = req.params;
|
| 24 |
+
|
| 25 |
+
const history = await dbService.getSessionHistory(sessionId);
|
| 26 |
+
|
| 27 |
+
res.json({ history });
|
| 28 |
+
} catch (error) {
|
| 29 |
+
next(error);
|
| 30 |
+
}
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const getModels = async (req, res, next) => {
|
| 34 |
+
try {
|
| 35 |
+
const models = await mlService.getModels();
|
| 36 |
+
res.json(models);
|
| 37 |
+
} catch (error) {
|
| 38 |
+
next(error);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const getMetrics = async (req, res, next) => {
|
| 43 |
+
try {
|
| 44 |
+
const metrics = await dbService.getModelMetrics();
|
| 45 |
+
const serializedMetrics = metrics.map(m => ({
|
| 46 |
+
...m,
|
| 47 |
+
sizeBytes: m.sizeBytes.toString()
|
| 48 |
+
}));
|
| 49 |
+
res.json(serializedMetrics);
|
| 50 |
+
} catch (error) {
|
| 51 |
+
next(error);
|
| 52 |
+
}
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const getHealth = async (req, res, next) => {
|
| 56 |
+
try {
|
| 57 |
+
const mlHealth = await mlService.getHealth();
|
| 58 |
+
|
| 59 |
+
res.json({
|
| 60 |
+
status: "healthy",
|
| 61 |
+
backend: true,
|
| 62 |
+
ml_service: !!mlHealth,
|
| 63 |
+
database: true,
|
| 64 |
+
models_loaded: mlHealth?.models_loaded || {
|
| 65 |
+
logistic_regression: false,
|
| 66 |
+
lstm: false,
|
| 67 |
+
bert: false
|
| 68 |
+
},
|
| 69 |
+
version: "1.0.0"
|
| 70 |
+
});
|
| 71 |
+
} catch (error) {
|
| 72 |
+
next(error);
|
| 73 |
+
}
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
module.exports = {
|
| 77 |
+
predict,
|
| 78 |
+
getHistory,
|
| 79 |
+
getModels,
|
| 80 |
+
getMetrics,
|
| 81 |
+
getHealth
|
| 82 |
+
};
|
backend/src/middleware/errorHandler.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const errorHandler = (err, req, res, next) => {
|
| 2 |
+
console.error('Error:', err);
|
| 3 |
+
|
| 4 |
+
// Standardized error response
|
| 5 |
+
res.status(err.status || 500).json({
|
| 6 |
+
error: err.name || 'Internal Server Error',
|
| 7 |
+
message: err.message || 'An unexpected error occurred',
|
| 8 |
+
// In production, don't expose stack traces
|
| 9 |
+
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
|
| 10 |
+
});
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
module.exports = { errorHandler };
|
backend/src/middleware/rateLimiter.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const rateLimit = require('express-rate-limit');
|
| 2 |
+
|
| 3 |
+
const apiLimiter = rateLimit({
|
| 4 |
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
| 5 |
+
max: 100, // Limit each IP to 100 requests per windowMs
|
| 6 |
+
message: {
|
| 7 |
+
error: "Too many requests",
|
| 8 |
+
message: "You have exceeded the 100 requests in 15 mins limit!"
|
| 9 |
+
},
|
| 10 |
+
standardHeaders: true,
|
| 11 |
+
legacyHeaders: false,
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
module.exports = { apiLimiter };
|
backend/src/middleware/validation.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { z } = require('zod');
|
| 2 |
+
|
| 3 |
+
// Validation schema for prediction request
|
| 4 |
+
const predictSchema = z.object({
|
| 5 |
+
body: z.object({
|
| 6 |
+
text: z.string().min(10, "Review must be at least 10 characters long").max(5000, "Review is too long"),
|
| 7 |
+
sessionId: z.string().uuid("Invalid session ID").optional()
|
| 8 |
+
})
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
const validate = (schema) => (req, res, next) => {
|
| 12 |
+
try {
|
| 13 |
+
schema.parse({
|
| 14 |
+
body: req.body,
|
| 15 |
+
query: req.query,
|
| 16 |
+
params: req.params
|
| 17 |
+
});
|
| 18 |
+
next();
|
| 19 |
+
} catch (err) {
|
| 20 |
+
if (err instanceof z.ZodError) {
|
| 21 |
+
return res.status(400).json({
|
| 22 |
+
error: "Validation failed",
|
| 23 |
+
details: err.errors.map(e => ({ path: e.path.join('.'), message: e.message }))
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
next(err);
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
module.exports = {
|
| 31 |
+
predictSchema,
|
| 32 |
+
validate
|
| 33 |
+
};
|
backend/src/routes/v1.routes.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const { predict, getHistory, getModels, getHealth, getMetrics } = require('../controllers/predict.controller');
|
| 3 |
+
const { validate, predictSchema } = require('../middleware/validation');
|
| 4 |
+
const { apiLimiter } = require('../middleware/rateLimiter');
|
| 5 |
+
|
| 6 |
+
const router = express.Router();
|
| 7 |
+
|
| 8 |
+
router.get('/health', getHealth);
|
| 9 |
+
router.get('/models', getModels);
|
| 10 |
+
router.get('/metrics', getMetrics);
|
| 11 |
+
router.get('/history/:sessionId', getHistory);
|
| 12 |
+
|
| 13 |
+
// Apply rate limiting and validation to the predict route
|
| 14 |
+
router.post('/predict', apiLimiter, validate(predictSchema), predict);
|
| 15 |
+
|
| 16 |
+
module.exports = router;
|
backend/src/server.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const cors = require('cors');
|
| 3 |
+
const helmet = require('helmet');
|
| 4 |
+
const morgan = require('morgan');
|
| 5 |
+
|
| 6 |
+
const config = require('./config');
|
| 7 |
+
const v1Routes = require('./routes/v1.routes');
|
| 8 |
+
const { errorHandler } = require('./middleware/errorHandler');
|
| 9 |
+
|
| 10 |
+
const app = express();
|
| 11 |
+
|
| 12 |
+
// Security middleware
|
| 13 |
+
app.use(helmet());
|
| 14 |
+
|
| 15 |
+
// Enable CORS
|
| 16 |
+
app.use(cors({
|
| 17 |
+
origin: '*', // In production, restrict to frontend URL
|
| 18 |
+
methods: ['GET', 'POST'],
|
| 19 |
+
allowedHeaders: ['Content-Type', 'Authorization']
|
| 20 |
+
}));
|
| 21 |
+
|
| 22 |
+
// Request parsing
|
| 23 |
+
app.use(express.json({ limit: '1mb' }));
|
| 24 |
+
app.use(express.urlencoded({ extended: true }));
|
| 25 |
+
|
| 26 |
+
// Logging
|
| 27 |
+
if (config.env === 'development') {
|
| 28 |
+
app.use(morgan('dev'));
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Routes
|
| 32 |
+
app.use('/api/v1', v1Routes);
|
| 33 |
+
|
| 34 |
+
// Root endpoint for simple health check
|
| 35 |
+
app.get('/', (req, res) => {
|
| 36 |
+
res.json({ message: 'The Screening Room - Backend API Gateway is running' });
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// Centralized error handling
|
| 40 |
+
app.use(errorHandler);
|
| 41 |
+
|
| 42 |
+
// Handle 404
|
| 43 |
+
app.use((req, res) => {
|
| 44 |
+
res.status(404).json({ error: 'Not Found', message: 'The requested resource was not found' });
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
app.listen(config.port, () => {
|
| 48 |
+
console.log(`Backend API Gateway listening on port ${config.port}`);
|
| 49 |
+
console.log(`Environment: ${config.env}`);
|
| 50 |
+
console.log(`Connecting to ML Service at: ${config.mlServiceUrl}`);
|
| 51 |
+
});
|
backend/src/services/dbService.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PrismaClient } = require('@prisma/client');
|
| 2 |
+
const prisma = new PrismaClient();
|
| 3 |
+
|
| 4 |
+
const savePrediction = async (sessionId, text, result) => {
|
| 5 |
+
try {
|
| 6 |
+
return await prisma.prediction.create({
|
| 7 |
+
data: {
|
| 8 |
+
sessionId: sessionId || 'anonymous',
|
| 9 |
+
text: text,
|
| 10 |
+
lrResult: JSON.stringify(result.models.lr || {}),
|
| 11 |
+
lstmResult: JSON.stringify(result.models.lstm || {}),
|
| 12 |
+
bertResult: JSON.stringify(result.models.bert || {}),
|
| 13 |
+
majorityLabel: result.overall.label,
|
| 14 |
+
agreement: result.overall.agreement,
|
| 15 |
+
totalLatencyMs: result.timing.total_ms
|
| 16 |
+
}
|
| 17 |
+
});
|
| 18 |
+
} catch (error) {
|
| 19 |
+
console.error('Failed to save prediction to DB:', error);
|
| 20 |
+
// Non-blocking error - we don't want to fail the request if DB fails
|
| 21 |
+
return null;
|
| 22 |
+
}
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
const getSessionHistory = async (sessionId) => {
|
| 26 |
+
try {
|
| 27 |
+
const history = await prisma.prediction.findMany({
|
| 28 |
+
where: { sessionId },
|
| 29 |
+
orderBy: { createdAt: 'desc' },
|
| 30 |
+
take: 20 // Return last 20 predictions
|
| 31 |
+
});
|
| 32 |
+
return history.map(item => ({
|
| 33 |
+
...item,
|
| 34 |
+
lrResult: JSON.parse(item.lrResult),
|
| 35 |
+
lstmResult: JSON.parse(item.lstmResult),
|
| 36 |
+
bertResult: JSON.parse(item.bertResult)
|
| 37 |
+
}));
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error('Failed to fetch history from DB:', error);
|
| 40 |
+
return [];
|
| 41 |
+
}
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
const getModelMetrics = async () => {
|
| 45 |
+
try {
|
| 46 |
+
return await prisma.modelMetric.findMany();
|
| 47 |
+
} catch (error) {
|
| 48 |
+
console.error('Failed to fetch model metrics from DB:', error);
|
| 49 |
+
return [];
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
module.exports = {
|
| 54 |
+
prisma,
|
| 55 |
+
savePrediction,
|
| 56 |
+
getSessionHistory,
|
| 57 |
+
getModelMetrics
|
| 58 |
+
};
|
backend/src/services/mlService.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const axios = require('axios');
|
| 2 |
+
const config = require('../config');
|
| 3 |
+
|
| 4 |
+
const mlClient = axios.create({
|
| 5 |
+
baseURL: config.mlServiceUrl,
|
| 6 |
+
timeout: 30000 // ML inference can take a few seconds
|
| 7 |
+
});
|
| 8 |
+
|
| 9 |
+
const getHealth = async () => {
|
| 10 |
+
try {
|
| 11 |
+
const response = await mlClient.get('/api/v1/health');
|
| 12 |
+
return response.data;
|
| 13 |
+
} catch (error) {
|
| 14 |
+
console.error('Failed to get ML service health:', error.message);
|
| 15 |
+
return null;
|
| 16 |
+
}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
const getModels = async () => {
|
| 20 |
+
try {
|
| 21 |
+
const response = await mlClient.get('/api/v1/models');
|
| 22 |
+
return response.data;
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error('Failed to get models info from ML service:', error.message);
|
| 25 |
+
throw new Error('ML Service Unavailable');
|
| 26 |
+
}
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const predict = async (text, models = ["lr", "lstm", "bert"]) => {
|
| 30 |
+
try {
|
| 31 |
+
const response = await mlClient.post('/api/v1/predict', { text, models });
|
| 32 |
+
return response.data;
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error('Prediction failed in ML service:', error.message);
|
| 35 |
+
throw new Error(error.response?.data?.detail || 'ML Service Prediction Failed');
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
module.exports = {
|
| 40 |
+
getHealth,
|
| 41 |
+
getModels,
|
| 42 |
+
predict
|
| 43 |
+
};
|
data/X_test_pad.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f0aa8c3a1396f834322ec8608cadb2d9f55364f8a0e6f61c35a0bbccb2cd6ade
|
| 3 |
+
size 6000128
|
data/X_train_pad.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3d09ffb19d8efced07c963beb7ad3afd58c0d7d088e4beb58509f389fc6c5ca6
|
| 3 |
+
size 6000128
|
data/config.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:13c4b3331a940c98f327cd3ff5f7bbebc830df0387d43d3762664c02a3fc2fed
|
| 3 |
+
size 167
|
data/gru_config.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:22b96c6f408a0b844888f5e6006e38d4e348ed568b17c483c1839af367d7bfe6
|
| 3 |
+
size 92
|
data/gru_history.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dbe5c86c46bc4ab647002a26bb5f8b693dc02fba4e66cdbd7e88aeb0ebd73215
|
| 3 |
+
size 148
|
data/tokenizer.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:05a26692dacb4ad344660920ed28ea53223c29c3c2e207bdc3ddeca5edf6bf1f
|
| 3 |
+
size 1881811
|
data/y_test.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fa72a865a069fcc550d39d2a7d84c144c788f60630343ae776283924b84d7fb5
|
| 3 |
+
size 20128
|
data/y_train.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fa72a865a069fcc550d39d2a7d84c144c788f60630343ae776283924b84d7fb5
|
| 3 |
+
size 20128
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
frontend:
|
| 5 |
+
image: nginx:alpine
|
| 6 |
+
ports:
|
| 7 |
+
- "8080:80"
|
| 8 |
+
volumes:
|
| 9 |
+
- ./frontend:/usr/share/nginx/html
|
| 10 |
+
depends_on:
|
| 11 |
+
- backend
|
| 12 |
+
|
| 13 |
+
backend:
|
| 14 |
+
build:
|
| 15 |
+
context: ./backend
|
| 16 |
+
dockerfile: Dockerfile
|
| 17 |
+
ports:
|
| 18 |
+
- "4000:4000"
|
| 19 |
+
environment:
|
| 20 |
+
- NODE_ENV=production
|
| 21 |
+
- PORT=4000
|
| 22 |
+
- ML_SERVICE_URL=http://ml_service:8000
|
| 23 |
+
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/screening_room?schema=public
|
| 24 |
+
depends_on:
|
| 25 |
+
postgres:
|
| 26 |
+
condition: service_started
|
| 27 |
+
ml_service:
|
| 28 |
+
condition: service_started
|
| 29 |
+
|
| 30 |
+
ml_service:
|
| 31 |
+
build:
|
| 32 |
+
context: ./ml_service
|
| 33 |
+
dockerfile: Dockerfile
|
| 34 |
+
ports:
|
| 35 |
+
- "8000:8000"
|
| 36 |
+
volumes:
|
| 37 |
+
- ./models_bert:/app/models_bert
|
| 38 |
+
- ./models_lstm:/app/models_lstm
|
| 39 |
+
- ./models_ml:/app/models_ml
|
| 40 |
+
- ./data:/app/data
|
| 41 |
+
|
| 42 |
+
postgres:
|
| 43 |
+
image: postgres:16-alpine
|
| 44 |
+
environment:
|
| 45 |
+
- POSTGRES_USER=postgres
|
| 46 |
+
- POSTGRES_PASSWORD=postgres
|
| 47 |
+
- POSTGRES_DB=screening_room
|
| 48 |
+
ports:
|
| 49 |
+
- "5432:5432"
|
| 50 |
+
volumes:
|
| 51 |
+
- pgdata:/var/lib/postgresql/data
|
| 52 |
+
|
| 53 |
+
volumes:
|
| 54 |
+
pgdata:
|
frontend/app.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
const analyzeBtn = document.getElementById('analyzeBtn');
|
| 3 |
+
const reviewInput = document.getElementById('reviewInput');
|
| 4 |
+
const errorMessage = document.getElementById('errorMessage');
|
| 5 |
+
const loadingOverlay = document.getElementById('loadingOverlay');
|
| 6 |
+
const resultsSection = document.getElementById('resultsSection');
|
| 7 |
+
|
| 8 |
+
const API_URL = '/api/v1/predict';
|
| 9 |
+
|
| 10 |
+
// Session Management
|
| 11 |
+
function getSessionId() {
|
| 12 |
+
let sessionId = localStorage.getItem('screening_room_session');
|
| 13 |
+
if (!sessionId) {
|
| 14 |
+
sessionId = crypto.randomUUID();
|
| 15 |
+
localStorage.setItem('screening_room_session', sessionId);
|
| 16 |
+
}
|
| 17 |
+
return sessionId;
|
| 18 |
+
}
|
| 19 |
+
const currentSessionId = getSessionId();
|
| 20 |
+
|
| 21 |
+
analyzeBtn.addEventListener('click', async () => {
|
| 22 |
+
const text = reviewInput.value.trim();
|
| 23 |
+
|
| 24 |
+
if (!text) {
|
| 25 |
+
errorMessage.textContent = 'Please enter a review to analyze.';
|
| 26 |
+
return;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
if (text.length < 10) {
|
| 30 |
+
errorMessage.textContent = 'Review is too short. Please provide more context.';
|
| 31 |
+
return;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
errorMessage.textContent = '';
|
| 35 |
+
|
| 36 |
+
try {
|
| 37 |
+
// Show loading state
|
| 38 |
+
loadingOverlay.classList.remove('hidden');
|
| 39 |
+
resultsSection.classList.add('hidden');
|
| 40 |
+
analyzeBtn.disabled = true;
|
| 41 |
+
|
| 42 |
+
const response = await fetch(API_URL, {
|
| 43 |
+
method: 'POST',
|
| 44 |
+
headers: {
|
| 45 |
+
'Content-Type': 'application/json',
|
| 46 |
+
},
|
| 47 |
+
body: JSON.stringify({
|
| 48 |
+
text: text,
|
| 49 |
+
models: ['lr', 'lstm', 'bert'],
|
| 50 |
+
sessionId: currentSessionId
|
| 51 |
+
})
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
if (!response.ok) {
|
| 55 |
+
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const data = await response.json();
|
| 59 |
+
updateUI(data);
|
| 60 |
+
|
| 61 |
+
// Hide loading, show results
|
| 62 |
+
loadingOverlay.classList.add('hidden');
|
| 63 |
+
resultsSection.classList.remove('hidden');
|
| 64 |
+
|
| 65 |
+
// Scroll to results smoothly
|
| 66 |
+
resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 67 |
+
|
| 68 |
+
} catch (error) {
|
| 69 |
+
console.error('Prediction error:', error);
|
| 70 |
+
errorMessage.textContent = 'Failed to analyze review. Ensure the ML service is running.';
|
| 71 |
+
loadingOverlay.classList.add('hidden');
|
| 72 |
+
} finally {
|
| 73 |
+
analyzeBtn.disabled = false;
|
| 74 |
+
}
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
function deduplicateKeywords(words) {
|
| 78 |
+
if (!words) return [];
|
| 79 |
+
const sorted = [...words].sort((a, b) => b.length - a.length);
|
| 80 |
+
const result = [];
|
| 81 |
+
for (const w of sorted) {
|
| 82 |
+
const lowerW = w.toLowerCase();
|
| 83 |
+
if (!result.some(r => r.toLowerCase().includes(lowerW))) {
|
| 84 |
+
result.push(w);
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
return result.slice(0, 3);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function updateUI(data) {
|
| 91 |
+
// Update Verdict Banner
|
| 92 |
+
const stamp = document.getElementById('verdictStamp');
|
| 93 |
+
const verdictLabel = data.overall.label.toUpperCase();
|
| 94 |
+
stamp.textContent = verdictLabel;
|
| 95 |
+
stamp.setAttribute('data-verdict', verdictLabel);
|
| 96 |
+
stamp.className = `stamp ${data.overall.label.toLowerCase() === 'positive' ? 'approved' : 'panned'}`;
|
| 97 |
+
|
| 98 |
+
document.getElementById('verdictAgreement').textContent = data.overall.agreement;
|
| 99 |
+
document.getElementById('timingMs').textContent = data.timing.total_ms.toFixed(0);
|
| 100 |
+
|
| 101 |
+
// Update LR Critic
|
| 102 |
+
if (data.models.lr) {
|
| 103 |
+
updateCriticCard('criticLr', data.models.lr);
|
| 104 |
+
|
| 105 |
+
// Update keywords specific to LR
|
| 106 |
+
const posList = document.querySelector('#criticLr .pos-keywords ul');
|
| 107 |
+
const negList = document.querySelector('#criticLr .neg-keywords ul');
|
| 108 |
+
|
| 109 |
+
posList.innerHTML = '';
|
| 110 |
+
negList.innerHTML = '';
|
| 111 |
+
|
| 112 |
+
if (data.models.lr.top_positive_words) {
|
| 113 |
+
const words = deduplicateKeywords(data.models.lr.top_positive_words);
|
| 114 |
+
words.forEach(word => {
|
| 115 |
+
const li = document.createElement('li');
|
| 116 |
+
li.textContent = word;
|
| 117 |
+
posList.appendChild(li);
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (data.models.lr.top_negative_words) {
|
| 122 |
+
const words = deduplicateKeywords(data.models.lr.top_negative_words);
|
| 123 |
+
words.forEach(word => {
|
| 124 |
+
const li = document.createElement('li');
|
| 125 |
+
li.textContent = word;
|
| 126 |
+
negList.appendChild(li);
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Update LSTM Critic
|
| 132 |
+
if (data.models.lstm) {
|
| 133 |
+
updateCriticCard('criticLstm', data.models.lstm);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Update BERT Critic
|
| 137 |
+
if (data.models.bert) {
|
| 138 |
+
updateCriticCard('criticBert', data.models.bert);
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
function updateCriticCard(cardId, modelData) {
|
| 143 |
+
const card = document.getElementById(cardId);
|
| 144 |
+
if (!card) return;
|
| 145 |
+
|
| 146 |
+
const chip = card.querySelector('.prediction-chip');
|
| 147 |
+
chip.textContent = modelData.label;
|
| 148 |
+
chip.className = `prediction-chip ${modelData.label.toLowerCase()}`;
|
| 149 |
+
|
| 150 |
+
const confValue = card.querySelector('.conf-value');
|
| 151 |
+
const confBar = card.querySelector('.confidence-bar-fill');
|
| 152 |
+
|
| 153 |
+
const confPercent = (modelData.confidence * 100).toFixed(1) + '%';
|
| 154 |
+
if (confValue) confValue.textContent = confPercent;
|
| 155 |
+
if (confBar) confBar.style.width = confPercent;
|
| 156 |
+
|
| 157 |
+
const reason = card.querySelector('.reasoning');
|
| 158 |
+
reason.textContent = modelData.reasoning;
|
| 159 |
+
|
| 160 |
+
const latency = card.querySelector('.critic-footer span');
|
| 161 |
+
latency.textContent = `${modelData.latency_ms.toFixed(1)}ms`;
|
| 162 |
+
}
|
| 163 |
+
});
|
frontend/film_reel.png
ADDED
|
Git LFS Details
|
frontend/index.html
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>The Screening Room - AI Film Critics</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,600;0,700;1,600&display=swap" rel="stylesheet">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="marquee-border top-marquee">
|
| 12 |
+
<div class="lights"></div>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
<nav class="nav-bar">
|
| 16 |
+
<div class="nav-container">
|
| 17 |
+
<div class="nav-left">
|
| 18 |
+
<a href="index.html" class="nav-logo" aria-label="Home">
|
| 19 |
+
<img src="film_reel.png" class="film-reel-icon" alt="The Screening Room Logo">
|
| 20 |
+
</a>
|
| 21 |
+
</div>
|
| 22 |
+
<div class="nav-links">
|
| 23 |
+
<a href="index.html" class="nav-link active">Analyze</a>
|
| 24 |
+
<span class="nav-divider">|</span>
|
| 25 |
+
<a href="inside.html" class="nav-link">Inside the Screening Room</a>
|
| 26 |
+
<span class="nav-divider">|</span>
|
| 27 |
+
<a href="https://github.com" class="nav-link" target="_blank">GitHub</a>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
</nav>
|
| 31 |
+
|
| 32 |
+
<main class="container">
|
| 33 |
+
<header class="header">
|
| 34 |
+
<h1 class="logo">The Screening Room</h1>
|
| 35 |
+
<p class="tagline">Three Critics. One Review. Three Perspectives.</p>
|
| 36 |
+
</header>
|
| 37 |
+
|
| 38 |
+
<section class="input-section">
|
| 39 |
+
<div class="ticket">
|
| 40 |
+
<div class="ticket-header">
|
| 41 |
+
<h2>Submit a Movie Review</h2>
|
| 42 |
+
<span class="ticket-stub">ADMIT ONE</span>
|
| 43 |
+
</div>
|
| 44 |
+
<div class="ticket-body">
|
| 45 |
+
<textarea id="reviewInput" placeholder="Type or paste a movie review here... e.g. 'This movie was absolutely brilliant but the ending ruined everything.'"></textarea>
|
| 46 |
+
<div class="error-message" id="errorMessage"></div>
|
| 47 |
+
</div>
|
| 48 |
+
<div class="ticket-footer">
|
| 49 |
+
<button id="analyzeBtn" class="analyze-btn">Analyze Review</button>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</section>
|
| 53 |
+
|
| 54 |
+
<section class="results-section hidden" id="resultsSection">
|
| 55 |
+
<div class="verdict-banner" id="verdictBanner">
|
| 56 |
+
<div class="verdict-divider"><span class="verdict-divider-icon">✦</span></div>
|
| 57 |
+
<h3>Overall Verdict</h3>
|
| 58 |
+
<div class="stamp" id="verdictStamp">APPROVED</div>
|
| 59 |
+
<div class="verdict-meta">
|
| 60 |
+
<span class="agreement">Agreement: <strong id="verdictAgreement">3/3</strong></span>
|
| 61 |
+
<span class="meta-sep">•</span>
|
| 62 |
+
<span class="timing">Analysis: <strong id="timingMs">0</strong>ms</span>
|
| 63 |
+
</div>
|
| 64 |
+
<div class="verdict-divider"><span class="verdict-divider-icon">✦</span></div>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<div class="critics-grid">
|
| 68 |
+
<!-- LR Critic -->
|
| 69 |
+
<div class="critic-card" id="criticLr">
|
| 70 |
+
<div class="critic-header">
|
| 71 |
+
<h4>The Statistician</h4>
|
| 72 |
+
<span class="model-type">Logistic Regression</span>
|
| 73 |
+
</div>
|
| 74 |
+
<div class="critic-body">
|
| 75 |
+
<div class="prediction-chip positive">Positive</div>
|
| 76 |
+
<div class="confidence">
|
| 77 |
+
<div class="confidence-header">
|
| 78 |
+
<span>Confidence</span>
|
| 79 |
+
<span class="conf-value">85%</span>
|
| 80 |
+
</div>
|
| 81 |
+
<div class="confidence-bar-bg">
|
| 82 |
+
<div class="confidence-bar-fill" style="width: 85%;"></div>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
<p class="reasoning">Explanation goes here.</p>
|
| 86 |
+
|
| 87 |
+
<div class="keywords">
|
| 88 |
+
<div class="keyword-group pos-keywords">
|
| 89 |
+
<h5>Positive Cues</h5>
|
| 90 |
+
<ul></ul>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="keyword-group neg-keywords">
|
| 93 |
+
<h5>Negative Cues</h5>
|
| 94 |
+
<ul></ul>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="critic-footer">Latency: <span>2ms</span></div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<!-- BERT Critic -->
|
| 102 |
+
<div class="critic-card" id="criticBert">
|
| 103 |
+
<div class="critic-header">
|
| 104 |
+
<h4>The Contextualist</h4>
|
| 105 |
+
<span class="model-type">BERT (Fine-Tuned)</span>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="critic-body">
|
| 108 |
+
<div class="prediction-chip positive">Positive</div>
|
| 109 |
+
<div class="confidence">
|
| 110 |
+
<div class="confidence-header">
|
| 111 |
+
<span>Confidence</span>
|
| 112 |
+
<span class="conf-value">85%</span>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="confidence-bar-bg">
|
| 115 |
+
<div class="confidence-bar-fill" style="width: 85%;"></div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
<p class="reasoning">Explanation goes here.</p>
|
| 119 |
+
</div>
|
| 120 |
+
<div class="critic-footer">Latency: <span>400ms</span></div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- LSTM Critic -->
|
| 124 |
+
<div class="critic-card" id="criticLstm">
|
| 125 |
+
<div class="critic-header">
|
| 126 |
+
<h4>The Sequentialist</h4>
|
| 127 |
+
<span class="model-type">Bi-LSTM</span>
|
| 128 |
+
</div>
|
| 129 |
+
<div class="critic-body">
|
| 130 |
+
<div class="prediction-chip positive">Positive</div>
|
| 131 |
+
<div class="confidence">
|
| 132 |
+
<div class="confidence-header">
|
| 133 |
+
<span>Confidence</span>
|
| 134 |
+
<span class="conf-value">85%</span>
|
| 135 |
+
</div>
|
| 136 |
+
<div class="confidence-bar-bg">
|
| 137 |
+
<div class="confidence-bar-fill" style="width: 85%;"></div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
<p class="reasoning">Explanation goes here.</p>
|
| 141 |
+
</div>
|
| 142 |
+
<div class="critic-footer">Latency: <span>120ms</span></div>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</section>
|
| 146 |
+
|
| 147 |
+
<div class="loading-overlay hidden" id="loadingOverlay">
|
| 148 |
+
<div class="spinner"></div>
|
| 149 |
+
<p>The critics are evaluating your review...</p>
|
| 150 |
+
</div>
|
| 151 |
+
</main>
|
| 152 |
+
|
| 153 |
+
<script src="app.js"></script>
|
| 154 |
+
</body>
|
| 155 |
+
</html>
|
frontend/inside.html
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>The Screening Room - Inside the Screening Room</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,600;0,700;1,600&family=Space+Mono&display=swap" rel="stylesheet">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="marquee-border top-marquee">
|
| 12 |
+
<div class="lights"></div>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
<nav class="nav-bar">
|
| 16 |
+
<div class="nav-container">
|
| 17 |
+
<div class="nav-left">
|
| 18 |
+
<a href="index.html" class="nav-logo" aria-label="Home">
|
| 19 |
+
<img src="film_reel.png" class="film-reel-icon" alt="The Screening Room Logo">
|
| 20 |
+
</a>
|
| 21 |
+
</div>
|
| 22 |
+
<div class="nav-links">
|
| 23 |
+
<a href="index.html" class="nav-link">Analyze</a>
|
| 24 |
+
<span class="nav-divider">|</span>
|
| 25 |
+
<a href="inside.html" class="nav-link active">Inside the Screening Room</a>
|
| 26 |
+
<span class="nav-divider">|</span>
|
| 27 |
+
<a href="https://github.com" class="nav-link" target="_blank">GitHub</a>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
</nav>
|
| 31 |
+
|
| 32 |
+
<!-- SECTION 1: Hero -->
|
| 33 |
+
<section class="inside-hero">
|
| 34 |
+
<div class="hero-projector"></div>
|
| 35 |
+
<div class="hero-content">
|
| 36 |
+
<h1>Inside the Screening Room</h1>
|
| 37 |
+
<p>Discover how three independent AI models analyze the same movie review, explore the technology behind the platform, and learn how The Screening Room brings machine learning and modern software engineering together.</p>
|
| 38 |
+
</div>
|
| 39 |
+
</section>
|
| 40 |
+
|
| 41 |
+
<!-- SECTION 2: Meet the Critics -->
|
| 42 |
+
<section class="section-container">
|
| 43 |
+
<h2 class="section-title">Meet the Critics <span class="star-icon">⭐</span></h2>
|
| 44 |
+
|
| 45 |
+
<div class="critics-grid inside-critics">
|
| 46 |
+
<!-- The Statistician -->
|
| 47 |
+
<div class="critic-card" id="criticLr">
|
| 48 |
+
<div class="critic-header">
|
| 49 |
+
<h4>The Statistician</h4>
|
| 50 |
+
<span class="model-type">Logistic Regression + TF-IDF</span>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="critic-body">
|
| 53 |
+
<p class="critic-desc">The Statistician evaluates a review by measuring how strongly individual words and phrases indicate positive or negative sentiment. It provides a fast, reliable baseline and is highly interpretable.</p>
|
| 54 |
+
<div class="critic-strengths">
|
| 55 |
+
<h5>Strengths</h5>
|
| 56 |
+
<ul>
|
| 57 |
+
<li>Extremely fast inference</li>
|
| 58 |
+
<li>Easy to interpret</li>
|
| 59 |
+
<li>Strong traditional machine learning baseline</li>
|
| 60 |
+
</ul>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<!-- The Contextualist -->
|
| 66 |
+
<div class="critic-card" id="criticBert">
|
| 67 |
+
<div class="critic-header">
|
| 68 |
+
<h4>The Contextualist</h4>
|
| 69 |
+
<span class="model-type">BERT</span>
|
| 70 |
+
</div>
|
| 71 |
+
<div class="critic-body">
|
| 72 |
+
<p class="critic-desc">The Contextualist reads every word in relation to every other word, allowing it to understand context, nuanced language, and complex sentence structures.</p>
|
| 73 |
+
<div class="critic-strengths">
|
| 74 |
+
<h5>Strengths</h5>
|
| 75 |
+
<ul>
|
| 76 |
+
<li>Context-aware</li>
|
| 77 |
+
<li>Handles nuanced language</li>
|
| 78 |
+
<li>Strong performance on complex reviews</li>
|
| 79 |
+
</ul>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<!-- The Sequentialist -->
|
| 85 |
+
<div class="critic-card" id="criticLstm">
|
| 86 |
+
<div class="critic-header">
|
| 87 |
+
<h4>The Sequentialist</h4>
|
| 88 |
+
<span class="model-type">Bi-LSTM</span>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="critic-body">
|
| 91 |
+
<p class="critic-desc">The Sequentialist processes each review in order, learning how meaning develops across a sentence. It captures long-term dependencies and contextual flow within the text.</p>
|
| 92 |
+
<div class="critic-strengths">
|
| 93 |
+
<h5>Strengths</h5>
|
| 94 |
+
<ul>
|
| 95 |
+
<li>Understands sentence order</li>
|
| 96 |
+
<li>Learns contextual sequences</li>
|
| 97 |
+
<li>Captures long-range dependencies</li>
|
| 98 |
+
</ul>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</section>
|
| 104 |
+
|
| 105 |
+
<!-- SECTION 3: How It Works -->
|
| 106 |
+
<section class="section-container">
|
| 107 |
+
<h2 class="section-title">How It Works</h2>
|
| 108 |
+
<div class="pipeline-container">
|
| 109 |
+
<div class="pipeline-step">
|
| 110 |
+
<div class="step-title">User Review</div>
|
| 111 |
+
<div class="step-desc">User submits a movie review.</div>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="pipeline-arrow">↓</div>
|
| 114 |
+
<div class="pipeline-step">
|
| 115 |
+
<div class="step-title">Model-specific Preprocessing</div>
|
| 116 |
+
<div class="step-desc">Each model receives text prepared according to its own preprocessing pipeline.</div>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="pipeline-arrow">↓</div>
|
| 119 |
+
<div class="pipeline-step">
|
| 120 |
+
<div class="step-title">Parallel Inference</div>
|
| 121 |
+
<div class="step-desc">Logistic Regression, Bi-LSTM, and BERT run independently.</div>
|
| 122 |
+
</div>
|
| 123 |
+
<div class="pipeline-arrow">↓</div>
|
| 124 |
+
<div class="pipeline-step">
|
| 125 |
+
<div class="step-title">Prediction Standardization</div>
|
| 126 |
+
<div class="step-desc">Outputs from all models are converted into one consistent response format.</div>
|
| 127 |
+
</div>
|
| 128 |
+
<div class="pipeline-arrow">↓</div>
|
| 129 |
+
<div class="pipeline-step">
|
| 130 |
+
<div class="step-title">Majority Decision</div>
|
| 131 |
+
<div class="step-desc">The application compares predictions and determines the overall verdict.</div>
|
| 132 |
+
</div>
|
| 133 |
+
<div class="pipeline-arrow">↓</div>
|
| 134 |
+
<div class="pipeline-step">
|
| 135 |
+
<div class="step-title">Frontend Rendering</div>
|
| 136 |
+
<div class="step-desc">Predictions, confidence scores, and model insights are displayed simultaneously.</div>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
</section>
|
| 140 |
+
|
| 141 |
+
<!-- SECTION 4: Project Details -->
|
| 142 |
+
<section class="section-container">
|
| 143 |
+
<h2 class="section-title">Project Details</h2>
|
| 144 |
+
|
| 145 |
+
<div class="accordion">
|
| 146 |
+
<!-- Performance -->
|
| 147 |
+
<div class="accordion-item active">
|
| 148 |
+
<button class="accordion-header" aria-expanded="true">
|
| 149 |
+
<span>Performance</span>
|
| 150 |
+
<span class="accordion-icon">▼</span>
|
| 151 |
+
</button>
|
| 152 |
+
<div class="accordion-content" style="max-height: 1000px;">
|
| 153 |
+
<div class="table-container">
|
| 154 |
+
<table class="tech-table">
|
| 155 |
+
<thead>
|
| 156 |
+
<tr>
|
| 157 |
+
<th>Model</th>
|
| 158 |
+
<th>Accuracy</th>
|
| 159 |
+
<th>Precision</th>
|
| 160 |
+
<th>Recall</th>
|
| 161 |
+
<th>F1</th>
|
| 162 |
+
<th>Inference</th>
|
| 163 |
+
</tr>
|
| 164 |
+
</thead>
|
| 165 |
+
<tbody id="metricsTableBody">
|
| 166 |
+
<!-- Populated by JS -->
|
| 167 |
+
</tbody>
|
| 168 |
+
</table>
|
| 169 |
+
<p class="table-caption">Metrics are reported using the final evaluation performed on the held-out IMDb test dataset.</p>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<!-- Dataset -->
|
| 175 |
+
<div class="accordion-item">
|
| 176 |
+
<button class="accordion-header" aria-expanded="false">
|
| 177 |
+
<span>Dataset</span>
|
| 178 |
+
<span class="accordion-icon">▶</span>
|
| 179 |
+
</button>
|
| 180 |
+
<div class="accordion-content">
|
| 181 |
+
<div class="dataset-grid">
|
| 182 |
+
<div class="dataset-stats">
|
| 183 |
+
<h3>IMDb Large Movie Review Dataset</h3>
|
| 184 |
+
<div class="code-block">
|
| 185 |
+
50,000 Reviews<br>
|
| 186 |
+
25,000 Positive<br>
|
| 187 |
+
25,000 Negative<br>
|
| 188 |
+
Balanced Dataset
|
| 189 |
+
</div>
|
| 190 |
+
<p>The models were trained and evaluated using the IMDb Large Movie Review Dataset, a widely used benchmark for binary sentiment classification introduced by Andrew L. Maas and colleagues.</p>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="dataset-attribution">
|
| 193 |
+
<h3>Dataset Attribution</h3>
|
| 194 |
+
<p><strong>Dataset:</strong> IMDb Large Movie Review Dataset</p>
|
| 195 |
+
<p><strong>Authors:</strong> Andrew L. Maas et al.</p>
|
| 196 |
+
<p><strong>Institution:</strong> Stanford AI Lab</p>
|
| 197 |
+
<p><strong>Official Source:</strong> <a href="https://ai.stanford.edu/~amaas/data/sentiment/" target="_blank">https://ai.stanford.edu/~amaas/data/sentiment/</a></p>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<!-- Technology Stack -->
|
| 204 |
+
<div class="accordion-item">
|
| 205 |
+
<button class="accordion-header" aria-expanded="false">
|
| 206 |
+
<span>Technology Stack</span>
|
| 207 |
+
<span class="accordion-icon">▶</span>
|
| 208 |
+
</button>
|
| 209 |
+
<div class="accordion-content">
|
| 210 |
+
<div class="tech-grid">
|
| 211 |
+
<div class="tech-card">
|
| 212 |
+
<h4>Frontend</h4>
|
| 213 |
+
<ul>
|
| 214 |
+
<li>HTML</li>
|
| 215 |
+
<li>CSS</li>
|
| 216 |
+
<li>JavaScript</li>
|
| 217 |
+
</ul>
|
| 218 |
+
</div>
|
| 219 |
+
<div class="tech-card">
|
| 220 |
+
<h4>Backend</h4>
|
| 221 |
+
<ul>
|
| 222 |
+
<li>Express.js</li>
|
| 223 |
+
<li>FastAPI</li>
|
| 224 |
+
</ul>
|
| 225 |
+
</div>
|
| 226 |
+
<div class="tech-card">
|
| 227 |
+
<h4>Machine Learning</h4>
|
| 228 |
+
<ul>
|
| 229 |
+
<li>Scikit-learn</li>
|
| 230 |
+
<li>TensorFlow / Keras</li>
|
| 231 |
+
<li>Hugging Face Transformers</li>
|
| 232 |
+
</ul>
|
| 233 |
+
</div>
|
| 234 |
+
<div class="tech-card">
|
| 235 |
+
<h4>Deployment</h4>
|
| 236 |
+
<ul>
|
| 237 |
+
<li>Docker</li>
|
| 238 |
+
<li>Render</li>
|
| 239 |
+
<li>Neon PostgreSQL</li>
|
| 240 |
+
</ul>
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<!-- Architecture -->
|
| 247 |
+
<div class="accordion-item">
|
| 248 |
+
<button class="accordion-header" aria-expanded="false">
|
| 249 |
+
<span>Architecture</span>
|
| 250 |
+
<span class="accordion-icon">▶</span>
|
| 251 |
+
</button>
|
| 252 |
+
<div class="accordion-content">
|
| 253 |
+
<div class="pipeline-container architecture-flow">
|
| 254 |
+
<div class="pipeline-step">Browser</div>
|
| 255 |
+
<div class="pipeline-arrow">↓</div>
|
| 256 |
+
<div class="pipeline-step">Express Backend</div>
|
| 257 |
+
<div class="pipeline-arrow">↓</div>
|
| 258 |
+
<div class="pipeline-step">FastAPI ML Service</div>
|
| 259 |
+
<div class="pipeline-arrow">↓</div>
|
| 260 |
+
<div class="pipeline-step">Prediction Engine</div>
|
| 261 |
+
<div class="pipeline-arrow">↓</div>
|
| 262 |
+
<div class="pipeline-step">Three Models</div>
|
| 263 |
+
<div class="pipeline-arrow">↓</div>
|
| 264 |
+
<div class="pipeline-step">Standardized Response</div>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
</section>
|
| 270 |
+
|
| 271 |
+
<!-- SECTION 5: Behind the Build -->
|
| 272 |
+
<section class="section-container">
|
| 273 |
+
<h2 class="section-title">Behind the Build ⭐</h2>
|
| 274 |
+
|
| 275 |
+
<div class="accordion">
|
| 276 |
+
<div class="accordion-item">
|
| 277 |
+
<button class="accordion-header" aria-expanded="false">
|
| 278 |
+
<span>Engineering Decisions</span>
|
| 279 |
+
<span class="accordion-icon">▶</span>
|
| 280 |
+
</button>
|
| 281 |
+
<div class="accordion-content">
|
| 282 |
+
<div class="engineering-decision">
|
| 283 |
+
<h4>Why FastAPI?</h4>
|
| 284 |
+
<p>FastAPI powers the machine learning inference service because it provides high-performance APIs while integrating naturally with Python-based ML libraries.</p>
|
| 285 |
+
</div>
|
| 286 |
+
<div class="engineering-decision">
|
| 287 |
+
<h4>Why Express?</h4>
|
| 288 |
+
<p>Express separates application logic from machine learning inference by handling validation, routing, persistence, and API orchestration.</p>
|
| 289 |
+
</div>
|
| 290 |
+
<div class="engineering-decision">
|
| 291 |
+
<h4>Why PostgreSQL?</h4>
|
| 292 |
+
<p>PostgreSQL stores prediction history and model metadata using a relational schema to demonstrate production-style persistence.</p>
|
| 293 |
+
</div>
|
| 294 |
+
<div class="engineering-decision">
|
| 295 |
+
<h4>Why Three Models?</h4>
|
| 296 |
+
<p>The goal is not only to predict sentiment but also to compare how fundamentally different NLP approaches interpret the same review.</p>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
|
| 301 |
+
<div class="accordion-item">
|
| 302 |
+
<button class="accordion-header" aria-expanded="false">
|
| 303 |
+
<span>Challenges</span>
|
| 304 |
+
<span class="accordion-icon">▶</span>
|
| 305 |
+
</button>
|
| 306 |
+
<div class="accordion-content">
|
| 307 |
+
<div class="table-container">
|
| 308 |
+
<table class="tech-table challenges-table">
|
| 309 |
+
<thead>
|
| 310 |
+
<tr>
|
| 311 |
+
<th>Challenge</th>
|
| 312 |
+
<th>Solution</th>
|
| 313 |
+
<th>Result</th>
|
| 314 |
+
</tr>
|
| 315 |
+
</thead>
|
| 316 |
+
<tbody>
|
| 317 |
+
<tr>
|
| 318 |
+
<td>Different preprocessing requirements</td>
|
| 319 |
+
<td>Model-specific preprocessing pipeline</td>
|
| 320 |
+
<td>Consistent and reliable predictions</td>
|
| 321 |
+
</tr>
|
| 322 |
+
<tr>
|
| 323 |
+
<td>Three incompatible prediction outputs</td>
|
| 324 |
+
<td>Unified response schema</td>
|
| 325 |
+
<td>Common API contract across all models</td>
|
| 326 |
+
</tr>
|
| 327 |
+
<tr>
|
| 328 |
+
<td>Large BERT model</td>
|
| 329 |
+
<td>Singleton loading during application startup</td>
|
| 330 |
+
<td>Reduced inference overhead after initialization</td>
|
| 331 |
+
</tr>
|
| 332 |
+
<tr>
|
| 333 |
+
<td>Different confidence formats</td>
|
| 334 |
+
<td>Standardized prediction response</td>
|
| 335 |
+
<td>Consistent frontend rendering</td>
|
| 336 |
+
</tr>
|
| 337 |
+
</tbody>
|
| 338 |
+
</table>
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
</div>
|
| 343 |
+
</section>
|
| 344 |
+
|
| 345 |
+
<!-- SECTION 6: Future Improvements -->
|
| 346 |
+
<section class="section-container">
|
| 347 |
+
<h2 class="section-title">Future Improvements</h2>
|
| 348 |
+
|
| 349 |
+
<div class="future-grid">
|
| 350 |
+
<div class="future-card">
|
| 351 |
+
<div class="future-header">
|
| 352 |
+
<h4>Multilingual Reviews</h4>
|
| 353 |
+
<span class="badge planned">Planned</span>
|
| 354 |
+
</div>
|
| 355 |
+
<p>Support additional languages beyond English.</p>
|
| 356 |
+
</div>
|
| 357 |
+
|
| 358 |
+
<div class="future-card">
|
| 359 |
+
<div class="future-header">
|
| 360 |
+
<h4>Batch Analysis</h4>
|
| 361 |
+
<span class="badge planned">Planned</span>
|
| 362 |
+
</div>
|
| 363 |
+
<p>Analyze multiple reviews simultaneously.</p>
|
| 364 |
+
</div>
|
| 365 |
+
|
| 366 |
+
<div class="future-card">
|
| 367 |
+
<div class="future-header">
|
| 368 |
+
<h4>Additional Transformer Models</h4>
|
| 369 |
+
<span class="badge future">Future</span>
|
| 370 |
+
</div>
|
| 371 |
+
<p>Compare more state-of-the-art NLP models.</p>
|
| 372 |
+
</div>
|
| 373 |
+
|
| 374 |
+
<div class="future-card">
|
| 375 |
+
<div class="future-header">
|
| 376 |
+
<h4>Mobile Experience</h4>
|
| 377 |
+
<span class="badge future">Future</span>
|
| 378 |
+
</div>
|
| 379 |
+
<p>Responsive mobile-first interface.</p>
|
| 380 |
+
</div>
|
| 381 |
+
|
| 382 |
+
<div class="future-card">
|
| 383 |
+
<div class="future-header">
|
| 384 |
+
<h4>Advanced Model Interpretation</h4>
|
| 385 |
+
<span class="badge research">Research</span>
|
| 386 |
+
</div>
|
| 387 |
+
<p>Expand interpretation techniques for deeper insight into model behavior.</p>
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
</section>
|
| 391 |
+
|
| 392 |
+
<footer class="site-footer">
|
| 393 |
+
<div class="footer-content">
|
| 394 |
+
<h4>The Screening Room</h4>
|
| 395 |
+
<p>Three Critics.<br>One Review.<br>Three Perspectives.</p>
|
| 396 |
+
</div>
|
| 397 |
+
</footer>
|
| 398 |
+
|
| 399 |
+
<script src="inside.js"></script>
|
| 400 |
+
</body>
|
| 401 |
+
</html>
|
frontend/inside.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
// Accordion Logic
|
| 3 |
+
const accordionHeaders = document.querySelectorAll('.accordion-header');
|
| 4 |
+
|
| 5 |
+
accordionHeaders.forEach(header => {
|
| 6 |
+
header.addEventListener('click', () => {
|
| 7 |
+
const item = header.parentElement;
|
| 8 |
+
const content = header.nextElementSibling;
|
| 9 |
+
const icon = header.querySelector('.accordion-icon');
|
| 10 |
+
const isExpanded = header.getAttribute('aria-expanded') === 'true';
|
| 11 |
+
|
| 12 |
+
// Toggle current item
|
| 13 |
+
if (isExpanded) {
|
| 14 |
+
header.setAttribute('aria-expanded', 'false');
|
| 15 |
+
content.style.maxHeight = null;
|
| 16 |
+
icon.textContent = '▶';
|
| 17 |
+
item.classList.remove('active');
|
| 18 |
+
} else {
|
| 19 |
+
header.setAttribute('aria-expanded', 'true');
|
| 20 |
+
content.style.maxHeight = content.scrollHeight + "px";
|
| 21 |
+
icon.textContent = '▼';
|
| 22 |
+
item.classList.add('active');
|
| 23 |
+
}
|
| 24 |
+
});
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// Fetch and populate Performance metrics
|
| 28 |
+
const fetchMetrics = async () => {
|
| 29 |
+
try {
|
| 30 |
+
const response = await fetch('/api/v1/metrics');
|
| 31 |
+
if (!response.ok) {
|
| 32 |
+
throw new Error('Failed to fetch metrics');
|
| 33 |
+
}
|
| 34 |
+
const metrics = await response.json();
|
| 35 |
+
|
| 36 |
+
const tableBody = document.getElementById('metricsTableBody');
|
| 37 |
+
tableBody.innerHTML = ''; // Clear loading state if any
|
| 38 |
+
|
| 39 |
+
// Define display order and mapping
|
| 40 |
+
const modelMapping = {
|
| 41 |
+
'lr': { name: 'Logistic Regression' },
|
| 42 |
+
'lstm': { name: 'Bi-LSTM' },
|
| 43 |
+
'bert': { name: 'BERT' }
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const orderedKeys = ['lr', 'lstm', 'bert'];
|
| 47 |
+
|
| 48 |
+
orderedKeys.forEach(key => {
|
| 49 |
+
const data = metrics.find(m => m.modelName === modelMapping[key].name || m.modelName.toLowerCase().includes(key));
|
| 50 |
+
if (data) {
|
| 51 |
+
const row = document.createElement('tr');
|
| 52 |
+
|
| 53 |
+
const formatPct = (val) => val.toFixed(2) + '%';
|
| 54 |
+
const formatMs = (val) => val + 'ms';
|
| 55 |
+
|
| 56 |
+
row.innerHTML = `
|
| 57 |
+
<td>${data.modelName}</td>
|
| 58 |
+
<td>${formatPct(data.accuracy)}</td>
|
| 59 |
+
<td>${formatPct(data.precision)}</td>
|
| 60 |
+
<td>${formatPct(data.recall)}</td>
|
| 61 |
+
<td>${formatPct(data.f1)}</td>
|
| 62 |
+
<td>${formatMs(data.inferenceMs)}</td>
|
| 63 |
+
`;
|
| 64 |
+
tableBody.appendChild(row);
|
| 65 |
+
}
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
// Adjust accordion height after table is populated
|
| 69 |
+
const activeAccordion = document.querySelector('.accordion-item.active .accordion-content');
|
| 70 |
+
if (activeAccordion) {
|
| 71 |
+
activeAccordion.style.maxHeight = activeAccordion.scrollHeight + "px";
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
} catch (error) {
|
| 75 |
+
console.error('Error fetching metrics:', error);
|
| 76 |
+
document.getElementById('metricsTableBody').innerHTML = `
|
| 77 |
+
<tr><td colspan="6" style="text-align:center; color: var(--negative-red);">Failed to load performance metrics</td></tr>
|
| 78 |
+
`;
|
| 79 |
+
}
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
fetchMetrics();
|
| 83 |
+
});
|
frontend/style.css
ADDED
|
@@ -0,0 +1,1174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg-dark: #13131e; /* Richer dark blue-grey */
|
| 3 |
+
--bg-card: #1f1f2e;
|
| 4 |
+
--text-primary: #ffffff;
|
| 5 |
+
--text-muted: #b0b0c5;
|
| 6 |
+
--gold: #efc354; /* Brighter, richer gold */
|
| 7 |
+
--gold-glow: rgba(239, 195, 84, 0.5);
|
| 8 |
+
--gold-dim: #9b7f2f;
|
| 9 |
+
--positive: #4ade80;
|
| 10 |
+
--positive-bg: rgba(74, 222, 128, 0.15);
|
| 11 |
+
--negative: #f87171;
|
| 12 |
+
--negative-bg: rgba(248, 113, 113, 0.15);
|
| 13 |
+
--ticket-bg: #fffbf0;
|
| 14 |
+
--ticket-text: #1a1a1a;
|
| 15 |
+
|
| 16 |
+
--border-radius: 12px; /* slightly softer corners */
|
| 17 |
+
--transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); /* bouncier transition */
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
* {
|
| 21 |
+
box-sizing: border-box;
|
| 22 |
+
margin: 0;
|
| 23 |
+
padding: 0;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
body {
|
| 27 |
+
font-family: 'Inter', sans-serif;
|
| 28 |
+
background-color: var(--bg-dark);
|
| 29 |
+
color: var(--text-primary);
|
| 30 |
+
line-height: 1.6;
|
| 31 |
+
min-height: 100vh;
|
| 32 |
+
display: flex;
|
| 33 |
+
flex-direction: column;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Typography */
|
| 37 |
+
h1, h2, h3, h4, h5 {
|
| 38 |
+
font-family: 'Playfair Display', serif;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* Marquee Borders */
|
| 42 |
+
.marquee-border {
|
| 43 |
+
height: 24px;
|
| 44 |
+
background-color: #1a1a1a;
|
| 45 |
+
display: flex;
|
| 46 |
+
align-items: center;
|
| 47 |
+
position: relative;
|
| 48 |
+
overflow: hidden;
|
| 49 |
+
border-top: 3px solid var(--gold);
|
| 50 |
+
border-bottom: 3px solid var(--gold);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.top-marquee {
|
| 54 |
+
margin-bottom: 1.5rem;
|
| 55 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.lights {
|
| 59 |
+
width: 200%;
|
| 60 |
+
height: 10px;
|
| 61 |
+
background-image: radial-gradient(circle at 15px 5px, #fff 3px, var(--gold) 6px, transparent 7px);
|
| 62 |
+
background-size: 30px 10px;
|
| 63 |
+
background-repeat: repeat-x;
|
| 64 |
+
animation: marquee 20s linear infinite;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
@keyframes marquee {
|
| 68 |
+
0% { transform: translateX(0); }
|
| 69 |
+
100% { transform: translateX(-50%); }
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/* Layout */
|
| 73 |
+
.container {
|
| 74 |
+
flex-grow: 1;
|
| 75 |
+
max-width: 1200px;
|
| 76 |
+
margin: 0 auto;
|
| 77 |
+
padding: 2rem 1rem;
|
| 78 |
+
width: 100%;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.header {
|
| 82 |
+
text-align: center;
|
| 83 |
+
margin-bottom: 3rem;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.logo {
|
| 87 |
+
color: var(--gold);
|
| 88 |
+
font-size: 4.5rem;
|
| 89 |
+
letter-spacing: 3px;
|
| 90 |
+
text-shadow: 0 0 15px var(--gold-glow);
|
| 91 |
+
margin-bottom: 0.5rem;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.tagline {
|
| 95 |
+
color: var(--text-muted);
|
| 96 |
+
font-style: italic;
|
| 97 |
+
font-size: 1.5rem;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Ticket Input */
|
| 101 |
+
.input-section {
|
| 102 |
+
max-width: 800px;
|
| 103 |
+
margin: 0 auto 4rem auto;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.ticket {
|
| 107 |
+
background-color: var(--ticket-bg);
|
| 108 |
+
color: var(--ticket-text);
|
| 109 |
+
border-radius: var(--border-radius);
|
| 110 |
+
padding: 0;
|
| 111 |
+
position: relative;
|
| 112 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
| 113 |
+
overflow: hidden;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.ticket::before, .ticket::after {
|
| 117 |
+
content: '';
|
| 118 |
+
position: absolute;
|
| 119 |
+
width: 30px;
|
| 120 |
+
height: 30px;
|
| 121 |
+
background-color: var(--bg-dark);
|
| 122 |
+
border-radius: 50%;
|
| 123 |
+
top: 60px;
|
| 124 |
+
}
|
| 125 |
+
.ticket::before { left: -15px; }
|
| 126 |
+
.ticket::after { right: -15px; }
|
| 127 |
+
|
| 128 |
+
.ticket-header {
|
| 129 |
+
background-color: var(--gold);
|
| 130 |
+
padding: 1.5rem 2rem;
|
| 131 |
+
display: flex;
|
| 132 |
+
justify-content: space-between;
|
| 133 |
+
align-items: center;
|
| 134 |
+
border-bottom: 2px dashed rgba(0,0,0,0.2);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.ticket-header h2 {
|
| 138 |
+
margin: 0;
|
| 139 |
+
font-size: 1.5rem;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.ticket-stub {
|
| 143 |
+
font-family: monospace;
|
| 144 |
+
font-weight: bold;
|
| 145 |
+
letter-spacing: 2px;
|
| 146 |
+
border: 2px solid var(--ticket-text);
|
| 147 |
+
padding: 0.2rem 0.5rem;
|
| 148 |
+
transform: rotate(-5deg);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.ticket-body {
|
| 152 |
+
padding: 2rem;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
textarea {
|
| 156 |
+
width: 100%;
|
| 157 |
+
height: 120px;
|
| 158 |
+
border: 2px solid #ddd;
|
| 159 |
+
border-radius: 4px;
|
| 160 |
+
padding: 1rem;
|
| 161 |
+
font-family: 'Inter', sans-serif;
|
| 162 |
+
font-size: 1rem;
|
| 163 |
+
resize: vertical;
|
| 164 |
+
transition: var(--transition);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
textarea:focus {
|
| 168 |
+
outline: none;
|
| 169 |
+
border-color: var(--gold);
|
| 170 |
+
box-shadow: 0 0 0 3px var(--gold-glow);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.error-message {
|
| 174 |
+
color: var(--negative);
|
| 175 |
+
margin-top: 0.5rem;
|
| 176 |
+
font-size: 0.9rem;
|
| 177 |
+
min-height: 1.2rem;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.ticket-footer {
|
| 181 |
+
padding: 1rem 2rem 2rem 2rem;
|
| 182 |
+
text-align: right;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.analyze-btn {
|
| 186 |
+
background-color: var(--gold);
|
| 187 |
+
color: var(--bg-dark);
|
| 188 |
+
border: none;
|
| 189 |
+
padding: 0.8rem 2rem;
|
| 190 |
+
font-size: 1.1rem;
|
| 191 |
+
font-weight: bold;
|
| 192 |
+
border-radius: 4px;
|
| 193 |
+
cursor: pointer;
|
| 194 |
+
font-family: 'Playfair Display', serif;
|
| 195 |
+
text-transform: uppercase;
|
| 196 |
+
letter-spacing: 1px;
|
| 197 |
+
transition: var(--transition);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.analyze-btn:hover {
|
| 201 |
+
background-color: #e5c358;
|
| 202 |
+
transform: translateY(-2px);
|
| 203 |
+
box-shadow: 0 4px 15px var(--gold-glow);
|
| 204 |
+
}
|
| 205 |
+
.analyze-btn:disabled {
|
| 206 |
+
background-color: #ccc;
|
| 207 |
+
cursor: not-allowed;
|
| 208 |
+
transform: none;
|
| 209 |
+
box-shadow: none;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* Results Section */
|
| 213 |
+
.hidden {
|
| 214 |
+
display: none !important;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.results-section {
|
| 218 |
+
animation: fadeIn 0.5s ease;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
@keyframes fadeIn {
|
| 222 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 223 |
+
to { opacity: 1; transform: translateY(0); }
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.verdict-banner {
|
| 227 |
+
background: linear-gradient(135deg, var(--bg-card) 0%, rgba(212, 175, 55, 0.05) 100%);
|
| 228 |
+
border: 1px solid var(--gold-dim);
|
| 229 |
+
border-radius: var(--border-radius);
|
| 230 |
+
padding: 1.5rem 2rem;
|
| 231 |
+
text-align: center;
|
| 232 |
+
margin-bottom: 3rem;
|
| 233 |
+
position: relative;
|
| 234 |
+
overflow: hidden;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.verdict-divider {
|
| 238 |
+
display: flex;
|
| 239 |
+
align-items: center;
|
| 240 |
+
justify-content: center;
|
| 241 |
+
margin: 1rem 0;
|
| 242 |
+
gap: 1rem;
|
| 243 |
+
opacity: 0.5;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.verdict-divider::before, .verdict-divider::after {
|
| 247 |
+
content: '';
|
| 248 |
+
height: 1px;
|
| 249 |
+
width: 100px;
|
| 250 |
+
background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.verdict-divider-icon {
|
| 254 |
+
color: var(--gold);
|
| 255 |
+
font-size: 0.8rem;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.verdict-banner h3 {
|
| 259 |
+
color: var(--gold);
|
| 260 |
+
font-size: 1.2rem;
|
| 261 |
+
text-transform: uppercase;
|
| 262 |
+
letter-spacing: 3px;
|
| 263 |
+
margin-bottom: 1rem;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.stamp {
|
| 267 |
+
font-size: 3.5rem;
|
| 268 |
+
font-weight: 900;
|
| 269 |
+
font-family: 'Courier New', Courier, monospace;
|
| 270 |
+
text-transform: uppercase;
|
| 271 |
+
display: inline-block;
|
| 272 |
+
padding: 0.5rem 1.5rem;
|
| 273 |
+
margin: 0.5rem 0 1.5rem 0;
|
| 274 |
+
transform: rotate(-5deg);
|
| 275 |
+
position: relative;
|
| 276 |
+
z-index: 1;
|
| 277 |
+
mix-blend-mode: screen;
|
| 278 |
+
opacity: 0.85;
|
| 279 |
+
letter-spacing: 2px;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.stamp::before {
|
| 283 |
+
content: '';
|
| 284 |
+
position: absolute;
|
| 285 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 286 |
+
border: 4px solid;
|
| 287 |
+
border-radius: 8px 12px 6px 14px;
|
| 288 |
+
transform: rotate(1.5deg);
|
| 289 |
+
z-index: -1;
|
| 290 |
+
pointer-events: none;
|
| 291 |
+
opacity: 0.7;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.stamp::after {
|
| 295 |
+
content: attr(data-verdict);
|
| 296 |
+
position: absolute;
|
| 297 |
+
top: 50%; left: 50%;
|
| 298 |
+
transform: translate(-48%, -48%) rotate(-2deg);
|
| 299 |
+
opacity: 0.3;
|
| 300 |
+
z-index: -2;
|
| 301 |
+
filter: blur(1px);
|
| 302 |
+
white-space: nowrap;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.stamp.approved {
|
| 306 |
+
color: #4ade80;
|
| 307 |
+
text-shadow: 0 0 1px rgba(74, 222, 128, 0.8);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.stamp.approved::before {
|
| 311 |
+
border-color: #4ade80;
|
| 312 |
+
}
|
| 313 |
+
.stamp.approved::after {
|
| 314 |
+
color: #4ade80;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.stamp.panned {
|
| 318 |
+
color: #f87171;
|
| 319 |
+
text-shadow: 0 0 1px rgba(248, 113, 113, 0.8);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.stamp.panned::before {
|
| 323 |
+
border-color: #f87171;
|
| 324 |
+
}
|
| 325 |
+
.stamp.panned::after {
|
| 326 |
+
color: #f87171;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.verdict-meta {
|
| 330 |
+
color: var(--text-muted);
|
| 331 |
+
font-size: 0.95rem;
|
| 332 |
+
display: flex;
|
| 333 |
+
justify-content: center;
|
| 334 |
+
align-items: center;
|
| 335 |
+
gap: 1rem;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.verdict-meta strong {
|
| 339 |
+
color: var(--text-primary);
|
| 340 |
+
font-weight: 600;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.meta-sep {
|
| 344 |
+
color: var(--gold-dim);
|
| 345 |
+
opacity: 0.5;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
/* Critics Grid */
|
| 349 |
+
.critics-grid {
|
| 350 |
+
display: grid;
|
| 351 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 352 |
+
gap: 2rem;
|
| 353 |
+
perspective: 1000px;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.critic-card {
|
| 357 |
+
background-color: var(--bg-card);
|
| 358 |
+
border: 1px solid #2a2a35;
|
| 359 |
+
border-radius: var(--border-radius);
|
| 360 |
+
display: flex;
|
| 361 |
+
flex-direction: column;
|
| 362 |
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 363 |
+
position: relative;
|
| 364 |
+
overflow: hidden;
|
| 365 |
+
transform-origin: center center;
|
| 366 |
+
transform-style: preserve-3d;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.critic-card::before {
|
| 370 |
+
content: '';
|
| 371 |
+
position: absolute;
|
| 372 |
+
top: 0; left: 0; right: 0;
|
| 373 |
+
height: 3px;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
#criticLr::before { background-color: #7a9e7e; } /* Sage */
|
| 377 |
+
#criticLstm::before { background-color: #9b2226; } /* Burgundy */
|
| 378 |
+
#criticBert::before { background-color: var(--gold); } /* Gold */
|
| 379 |
+
|
| 380 |
+
.critic-card:hover {
|
| 381 |
+
border-color: var(--gold);
|
| 382 |
+
transform: translateY(-10px) rotateX(2deg) rotateY(-2deg) scale(1.03);
|
| 383 |
+
box-shadow: 0 20px 40px rgba(0,0,0,0.6), 0 0 20px var(--gold-glow);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.critic-header {
|
| 387 |
+
padding: 1.5rem;
|
| 388 |
+
border-bottom: 1px solid #2a2a35;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.critic-header h4 {
|
| 392 |
+
font-size: 1.4rem;
|
| 393 |
+
color: var(--gold);
|
| 394 |
+
margin-bottom: 0.2rem;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.model-type {
|
| 398 |
+
font-size: 0.8rem;
|
| 399 |
+
color: var(--text-muted);
|
| 400 |
+
text-transform: uppercase;
|
| 401 |
+
letter-spacing: 1px;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
#criticLr .model-type { color: #7a9e7e; }
|
| 405 |
+
#criticLstm .model-type { color: #e5383b; } /* Lighter burgundy for text */
|
| 406 |
+
#criticBert .model-type { color: #facc15; }
|
| 407 |
+
|
| 408 |
+
.critic-body {
|
| 409 |
+
padding: 1.5rem;
|
| 410 |
+
flex-grow: 1;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.prediction-chip {
|
| 414 |
+
display: inline-block;
|
| 415 |
+
padding: 0.3rem 1rem;
|
| 416 |
+
border-radius: 20px;
|
| 417 |
+
font-weight: 600;
|
| 418 |
+
font-size: 0.9rem;
|
| 419 |
+
margin-bottom: 1rem;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.prediction-chip.positive {
|
| 423 |
+
background-color: var(--positive-bg);
|
| 424 |
+
color: var(--positive);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.prediction-chip.negative {
|
| 428 |
+
background-color: var(--negative-bg);
|
| 429 |
+
color: var(--negative);
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.confidence {
|
| 433 |
+
margin-bottom: 1.5rem;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.confidence-header {
|
| 437 |
+
display: flex;
|
| 438 |
+
justify-content: space-between;
|
| 439 |
+
font-size: 0.8rem;
|
| 440 |
+
color: var(--text-muted);
|
| 441 |
+
margin-bottom: 0.5rem;
|
| 442 |
+
text-transform: uppercase;
|
| 443 |
+
letter-spacing: 1px;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.confidence-header .conf-value {
|
| 447 |
+
color: var(--text-primary);
|
| 448 |
+
font-weight: bold;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.confidence-bar-bg {
|
| 452 |
+
height: 8px;
|
| 453 |
+
background-color: rgba(0, 0, 0, 0.4);
|
| 454 |
+
border-radius: 4px;
|
| 455 |
+
overflow: hidden;
|
| 456 |
+
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.confidence-bar-fill {
|
| 460 |
+
height: 100%;
|
| 461 |
+
background: linear-gradient(90deg, #8b7324, #d4af37, #ffe58f);
|
| 462 |
+
border-radius: 4px;
|
| 463 |
+
box-shadow: 0 0 8px rgba(212, 175, 55, 0.5);
|
| 464 |
+
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.reasoning {
|
| 468 |
+
font-size: 0.95rem;
|
| 469 |
+
line-height: 1.5;
|
| 470 |
+
margin-bottom: 1.5rem;
|
| 471 |
+
font-style: italic;
|
| 472 |
+
color: #dcdcdc;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
/* LR Keywords */
|
| 476 |
+
.keywords {
|
| 477 |
+
display: flex;
|
| 478 |
+
gap: 1rem;
|
| 479 |
+
margin-top: 1rem;
|
| 480 |
+
border-top: 1px solid #2a2a35;
|
| 481 |
+
padding-top: 1rem;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
.keyword-group {
|
| 485 |
+
flex: 1;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.keyword-group h5 {
|
| 489 |
+
font-size: 0.8rem;
|
| 490 |
+
color: var(--text-muted);
|
| 491 |
+
text-transform: uppercase;
|
| 492 |
+
margin-bottom: 0.5rem;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.keyword-group ul {
|
| 496 |
+
list-style: none;
|
| 497 |
+
display: flex;
|
| 498 |
+
flex-wrap: wrap;
|
| 499 |
+
gap: 0.3rem;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.keyword-group li {
|
| 503 |
+
font-size: 0.75rem;
|
| 504 |
+
padding: 0.2rem 0.5rem;
|
| 505 |
+
border-radius: 3px;
|
| 506 |
+
background-color: #2a2a35;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.pos-keywords li { color: var(--positive); }
|
| 510 |
+
.neg-keywords li { color: var(--negative); }
|
| 511 |
+
|
| 512 |
+
.critic-footer {
|
| 513 |
+
padding: 1rem 1.5rem;
|
| 514 |
+
background-color: rgba(0,0,0,0.2);
|
| 515 |
+
border-top: 1px solid #2a2a35;
|
| 516 |
+
font-size: 0.8rem;
|
| 517 |
+
color: var(--text-muted);
|
| 518 |
+
text-align: right;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* Loading Overlay */
|
| 522 |
+
.loading-overlay {
|
| 523 |
+
position: fixed;
|
| 524 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 525 |
+
background-color: rgba(10, 10, 15, 0.8);
|
| 526 |
+
backdrop-filter: blur(5px);
|
| 527 |
+
display: flex;
|
| 528 |
+
flex-direction: column;
|
| 529 |
+
justify-content: center;
|
| 530 |
+
align-items: center;
|
| 531 |
+
z-index: 1000;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.spinner {
|
| 535 |
+
width: 50px;
|
| 536 |
+
height: 50px;
|
| 537 |
+
border: 3px solid var(--gold-dim);
|
| 538 |
+
border-top-color: var(--gold);
|
| 539 |
+
border-radius: 50%;
|
| 540 |
+
animation: spin 1s linear infinite;
|
| 541 |
+
margin-bottom: 1rem;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
@keyframes spin {
|
| 545 |
+
to { transform: rotate(360deg); }
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
.loading-overlay p {
|
| 549 |
+
color: var(--gold);
|
| 550 |
+
font-family: 'Playfair Display', serif;
|
| 551 |
+
font-size: 1.2rem;
|
| 552 |
+
letter-spacing: 1px;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
/* ==========================================================================
|
| 556 |
+
NAVIGATION BAR
|
| 557 |
+
========================================================================== */
|
| 558 |
+
.nav-bar {
|
| 559 |
+
width: 100%;
|
| 560 |
+
padding: 1rem 0;
|
| 561 |
+
border-bottom: 1px solid rgba(212, 175, 55, 0.2);
|
| 562 |
+
background-color: var(--deep-ink);
|
| 563 |
+
position: relative;
|
| 564 |
+
z-index: 10;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.nav-container {
|
| 568 |
+
max-width: 1200px;
|
| 569 |
+
margin: 0 auto;
|
| 570 |
+
padding: 0 2rem;
|
| 571 |
+
display: flex;
|
| 572 |
+
justify-content: space-between;
|
| 573 |
+
align-items: center;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.nav-logo {
|
| 577 |
+
color: var(--gold);
|
| 578 |
+
display: flex;
|
| 579 |
+
align-items: center;
|
| 580 |
+
justify-content: center;
|
| 581 |
+
transition: transform 0.3s ease, filter 0.3s ease;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
.nav-logo:hover {
|
| 585 |
+
transform: scale(1.1);
|
| 586 |
+
filter: brightness(1.2);
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.film-reel-icon {
|
| 590 |
+
width: 56px;
|
| 591 |
+
height: 56px;
|
| 592 |
+
animation: spin-reel 20s linear infinite;
|
| 593 |
+
filter: drop-shadow(0 0 8px var(--gold-glow));
|
| 594 |
+
display: block;
|
| 595 |
+
border-radius: 50%;
|
| 596 |
+
object-fit: contain;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
@keyframes spin-reel {
|
| 600 |
+
from { transform: rotate(0deg); }
|
| 601 |
+
to { transform: rotate(360deg); }
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.nav-left {
|
| 605 |
+
display: flex;
|
| 606 |
+
align-items: center;
|
| 607 |
+
gap: 1.5rem;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.nav-github {
|
| 611 |
+
font-family: 'Space Mono', monospace;
|
| 612 |
+
font-size: 0.8rem;
|
| 613 |
+
opacity: 0.6;
|
| 614 |
+
position: relative;
|
| 615 |
+
text-transform: none;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.nav-github::before {
|
| 619 |
+
content: '|';
|
| 620 |
+
position: absolute;
|
| 621 |
+
left: -0.75rem;
|
| 622 |
+
color: var(--gold-dim);
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.nav-links {
|
| 626 |
+
display: flex;
|
| 627 |
+
gap: 2rem;
|
| 628 |
+
align-items: center;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.nav-divider {
|
| 632 |
+
color: var(--gold-dim);
|
| 633 |
+
font-size: 0.9rem;
|
| 634 |
+
opacity: 0.6;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
.nav-link {
|
| 638 |
+
color: var(--paper-warm);
|
| 639 |
+
text-decoration: none;
|
| 640 |
+
font-family: 'Inter', sans-serif;
|
| 641 |
+
font-size: 0.9rem;
|
| 642 |
+
font-weight: 500;
|
| 643 |
+
text-transform: uppercase;
|
| 644 |
+
letter-spacing: 1px;
|
| 645 |
+
opacity: 0.6;
|
| 646 |
+
transition: opacity 0.2s ease, color 0.2s ease;
|
| 647 |
+
position: relative;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
.nav-link:hover {
|
| 651 |
+
opacity: 1;
|
| 652 |
+
color: var(--gold);
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.nav-link.active {
|
| 656 |
+
opacity: 1;
|
| 657 |
+
color: #ffffff;
|
| 658 |
+
font-weight: 600;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.nav-link.active::after {
|
| 662 |
+
content: '';
|
| 663 |
+
position: absolute;
|
| 664 |
+
bottom: -6px;
|
| 665 |
+
left: 0;
|
| 666 |
+
width: 100%;
|
| 667 |
+
height: 2px;
|
| 668 |
+
background-color: var(--gold);
|
| 669 |
+
box-shadow: 0 0 6px var(--gold-glow);
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
.nav-link[target="_blank"] {
|
| 673 |
+
opacity: 0.4;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
.nav-link[target="_blank"]:hover {
|
| 677 |
+
opacity: 0.8;
|
| 678 |
+
color: var(--paper-warm);
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
/* ==========================================================================
|
| 682 |
+
INSIDE THE SCREENING ROOM - LAYOUT
|
| 683 |
+
========================================================================== */
|
| 684 |
+
.inside-hero {
|
| 685 |
+
text-align: center;
|
| 686 |
+
padding: 6rem 2rem 4rem;
|
| 687 |
+
position: relative;
|
| 688 |
+
max-width: 800px;
|
| 689 |
+
margin: 0 auto;
|
| 690 |
+
animation: fadeIn 2s ease-out;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
.inside-hero h1 {
|
| 694 |
+
font-family: 'Playfair Display', serif;
|
| 695 |
+
font-size: 3.5rem;
|
| 696 |
+
color: var(--marquee-gold);
|
| 697 |
+
margin-bottom: 1.5rem;
|
| 698 |
+
text-shadow: 0 0 30px rgba(212, 175, 55, 0.2);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.inside-hero p {
|
| 702 |
+
font-family: 'Inter', sans-serif;
|
| 703 |
+
font-size: 1.1rem;
|
| 704 |
+
color: var(--paper-warm);
|
| 705 |
+
line-height: 1.6;
|
| 706 |
+
opacity: 0.9;
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.section-container {
|
| 710 |
+
max-width: 1000px;
|
| 711 |
+
margin: 0 auto 6rem;
|
| 712 |
+
padding: 0 2rem;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.section-title {
|
| 716 |
+
font-family: 'Playfair Display', serif;
|
| 717 |
+
font-size: 2rem;
|
| 718 |
+
color: var(--marquee-gold);
|
| 719 |
+
margin-bottom: 2rem;
|
| 720 |
+
text-align: center;
|
| 721 |
+
border-bottom: 1px solid rgba(212, 175, 55, 0.2);
|
| 722 |
+
padding-bottom: 1rem;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.star-icon {
|
| 726 |
+
font-size: 1.5rem;
|
| 727 |
+
vertical-align: middle;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
/* Meet the Critics */
|
| 731 |
+
.inside-critics {
|
| 732 |
+
margin-top: 3rem;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.critic-desc {
|
| 736 |
+
font-size: 0.95rem;
|
| 737 |
+
line-height: 1.5;
|
| 738 |
+
color: var(--paper-warm);
|
| 739 |
+
margin-bottom: 1.5rem;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
.critic-strengths h5 {
|
| 743 |
+
font-family: 'Space Mono', monospace;
|
| 744 |
+
color: var(--marquee-gold);
|
| 745 |
+
font-size: 0.85rem;
|
| 746 |
+
margin-bottom: 0.5rem;
|
| 747 |
+
text-transform: uppercase;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.critic-strengths ul {
|
| 751 |
+
list-style-type: none;
|
| 752 |
+
padding-left: 0;
|
| 753 |
+
margin: 0;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.critic-strengths li {
|
| 757 |
+
font-size: 0.85rem;
|
| 758 |
+
color: #a0a0a0;
|
| 759 |
+
margin-bottom: 0.4rem;
|
| 760 |
+
position: relative;
|
| 761 |
+
padding-left: 1.2rem;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
.critic-strengths li::before {
|
| 765 |
+
content: "•";
|
| 766 |
+
color: var(--marquee-gold);
|
| 767 |
+
position: absolute;
|
| 768 |
+
left: 0;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
/* Pipeline */
|
| 772 |
+
.pipeline-container {
|
| 773 |
+
display: flex;
|
| 774 |
+
flex-direction: column;
|
| 775 |
+
gap: 2rem;
|
| 776 |
+
margin-top: 3rem;
|
| 777 |
+
position: relative;
|
| 778 |
+
padding-left: 3rem;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
.pipeline-container::before {
|
| 782 |
+
content: '';
|
| 783 |
+
position: absolute;
|
| 784 |
+
top: 0; bottom: 0; left: 1rem;
|
| 785 |
+
width: 6px;
|
| 786 |
+
background: repeating-linear-gradient(
|
| 787 |
+
to bottom,
|
| 788 |
+
var(--gold-dim) 0,
|
| 789 |
+
var(--gold-dim) 12px,
|
| 790 |
+
transparent 12px,
|
| 791 |
+
transparent 24px
|
| 792 |
+
);
|
| 793 |
+
opacity: 0.6;
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
.pipeline-step {
|
| 797 |
+
background: var(--bg-card);
|
| 798 |
+
border: 1px solid #2a2a35;
|
| 799 |
+
border-radius: var(--border-radius);
|
| 800 |
+
padding: 1.5rem;
|
| 801 |
+
width: 100%;
|
| 802 |
+
max-width: 600px;
|
| 803 |
+
text-align: left;
|
| 804 |
+
position: relative;
|
| 805 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
| 806 |
+
transition: var(--transition);
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.pipeline-step::before {
|
| 810 |
+
content: '';
|
| 811 |
+
position: absolute;
|
| 812 |
+
left: -2.6rem;
|
| 813 |
+
top: 50%;
|
| 814 |
+
transform: translateY(-50%);
|
| 815 |
+
width: 14px;
|
| 816 |
+
height: 14px;
|
| 817 |
+
background-color: var(--bg-dark);
|
| 818 |
+
border: 3px solid var(--gold);
|
| 819 |
+
border-radius: 50%;
|
| 820 |
+
box-shadow: 0 0 10px var(--gold-glow);
|
| 821 |
+
z-index: 2;
|
| 822 |
+
transition: var(--transition);
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
.pipeline-step:hover {
|
| 826 |
+
border-color: var(--gold);
|
| 827 |
+
transform: translateX(10px);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.pipeline-step:hover::before {
|
| 831 |
+
background-color: var(--gold);
|
| 832 |
+
box-shadow: 0 0 20px var(--gold);
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
.step-title {
|
| 836 |
+
font-family: 'Playfair Display', serif;
|
| 837 |
+
color: var(--gold);
|
| 838 |
+
font-size: 1.25rem;
|
| 839 |
+
font-weight: 700;
|
| 840 |
+
margin-bottom: 0.5rem;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.step-desc {
|
| 844 |
+
font-size: 0.95rem;
|
| 845 |
+
color: var(--text-muted);
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
.pipeline-arrow {
|
| 849 |
+
display: none;
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
.architecture-flow .pipeline-step {
|
| 853 |
+
padding: 1rem 1.5rem;
|
| 854 |
+
font-family: 'Space Mono', monospace;
|
| 855 |
+
font-size: 0.9rem;
|
| 856 |
+
color: var(--paper-warm);
|
| 857 |
+
max-width: 400px;
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
/* Accordion */
|
| 861 |
+
.accordion {
|
| 862 |
+
display: flex;
|
| 863 |
+
flex-direction: column;
|
| 864 |
+
gap: 1rem;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.accordion-item {
|
| 868 |
+
background: var(--bg-card);
|
| 869 |
+
border: 1px solid var(--gold-dim);
|
| 870 |
+
border-radius: var(--border-radius);
|
| 871 |
+
overflow: hidden;
|
| 872 |
+
position: relative;
|
| 873 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.accordion-header {
|
| 877 |
+
width: 100%;
|
| 878 |
+
text-align: left;
|
| 879 |
+
padding: 1.5rem;
|
| 880 |
+
background: transparent;
|
| 881 |
+
border: none;
|
| 882 |
+
border-bottom: 2px dashed rgba(212, 175, 55, 0.3);
|
| 883 |
+
color: var(--gold);
|
| 884 |
+
font-family: 'Playfair Display', serif;
|
| 885 |
+
font-size: 1.3rem;
|
| 886 |
+
font-weight: 600;
|
| 887 |
+
cursor: pointer;
|
| 888 |
+
display: flex;
|
| 889 |
+
justify-content: space-between;
|
| 890 |
+
align-items: center;
|
| 891 |
+
transition: background 0.3s ease;
|
| 892 |
+
position: relative;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.accordion-header::before,
|
| 896 |
+
.accordion-header::after {
|
| 897 |
+
content: '';
|
| 898 |
+
position: absolute;
|
| 899 |
+
bottom: -10px;
|
| 900 |
+
width: 20px;
|
| 901 |
+
height: 20px;
|
| 902 |
+
background-color: var(--bg-dark);
|
| 903 |
+
border-radius: 50%;
|
| 904 |
+
z-index: 2;
|
| 905 |
+
border: 1px solid var(--gold-dim);
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
.accordion-header::before {
|
| 909 |
+
left: -10px;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.accordion-header::after {
|
| 913 |
+
right: -10px;
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
.accordion-header:hover {
|
| 917 |
+
background: rgba(239, 195, 84, 0.05);
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
.accordion-icon {
|
| 921 |
+
font-size: 0.9rem;
|
| 922 |
+
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
.accordion-content {
|
| 926 |
+
max-height: 0;
|
| 927 |
+
overflow: hidden;
|
| 928 |
+
transition: max-height 0.6s cubic-bezier(0.25, 1, 0.5, 1);
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
.accordion-item.active .accordion-content {
|
| 932 |
+
border-top: none;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
/* Tables */
|
| 936 |
+
.table-container {
|
| 937 |
+
padding: 1.5rem;
|
| 938 |
+
overflow-x: auto;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
.tech-table {
|
| 942 |
+
width: 100%;
|
| 943 |
+
border-collapse: collapse;
|
| 944 |
+
font-family: 'Space Mono', monospace;
|
| 945 |
+
font-size: 0.85rem;
|
| 946 |
+
color: var(--paper-warm);
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
.tech-table th, .tech-table td {
|
| 950 |
+
padding: 1rem;
|
| 951 |
+
text-align: left;
|
| 952 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
.tech-table th {
|
| 956 |
+
color: var(--marquee-gold);
|
| 957 |
+
font-weight: 700;
|
| 958 |
+
text-transform: uppercase;
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
.tech-table tr:hover td {
|
| 962 |
+
background: rgba(255, 255, 255, 0.02);
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.table-caption {
|
| 966 |
+
font-size: 0.8rem;
|
| 967 |
+
color: #888;
|
| 968 |
+
margin-top: 1rem;
|
| 969 |
+
font-style: italic;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
/* Project Details Grids */
|
| 973 |
+
.dataset-grid, .tech-grid {
|
| 974 |
+
padding: 1.5rem;
|
| 975 |
+
display: grid;
|
| 976 |
+
gap: 2rem;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
.dataset-grid {
|
| 980 |
+
grid-template-columns: 1fr 1fr;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.dataset-stats h3, .dataset-attribution h3, .tech-card h4 {
|
| 984 |
+
font-family: 'Space Mono', monospace;
|
| 985 |
+
color: var(--marquee-gold);
|
| 986 |
+
font-size: 1rem;
|
| 987 |
+
margin-bottom: 1rem;
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
.code-block {
|
| 991 |
+
font-family: 'Space Mono', monospace;
|
| 992 |
+
background: rgba(0, 0, 0, 0.4);
|
| 993 |
+
padding: 1rem;
|
| 994 |
+
border-radius: 4px;
|
| 995 |
+
border-left: 2px solid var(--marquee-gold);
|
| 996 |
+
font-size: 0.85rem;
|
| 997 |
+
color: var(--paper-warm);
|
| 998 |
+
margin-bottom: 1rem;
|
| 999 |
+
line-height: 1.5;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.dataset-attribution p {
|
| 1003 |
+
font-size: 0.9rem;
|
| 1004 |
+
color: var(--paper-warm);
|
| 1005 |
+
margin-bottom: 0.5rem;
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
.dataset-attribution a {
|
| 1009 |
+
color: var(--marquee-gold);
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
.tech-grid {
|
| 1013 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 1014 |
+
}
|
| 1015 |
+
|
| 1016 |
+
.tech-card {
|
| 1017 |
+
background: rgba(0, 0, 0, 0.2);
|
| 1018 |
+
padding: 1.5rem;
|
| 1019 |
+
border-radius: 4px;
|
| 1020 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
.tech-card ul {
|
| 1024 |
+
list-style-type: none;
|
| 1025 |
+
padding: 0;
|
| 1026 |
+
margin: 0;
|
| 1027 |
+
}
|
| 1028 |
+
|
| 1029 |
+
.tech-card li {
|
| 1030 |
+
font-size: 0.9rem;
|
| 1031 |
+
color: var(--paper-warm);
|
| 1032 |
+
margin-bottom: 0.5rem;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
/* Engineering */
|
| 1036 |
+
.engineering-decision {
|
| 1037 |
+
padding: 1.5rem;
|
| 1038 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
.engineering-decision:last-child {
|
| 1042 |
+
border-bottom: none;
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
.engineering-decision h4 {
|
| 1046 |
+
font-family: 'Space Mono', monospace;
|
| 1047 |
+
color: var(--marquee-gold);
|
| 1048 |
+
margin-bottom: 0.5rem;
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
.engineering-decision p {
|
| 1052 |
+
font-size: 0.95rem;
|
| 1053 |
+
color: var(--paper-warm);
|
| 1054 |
+
line-height: 1.5;
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
+
.challenges-table th:nth-child(1) { width: 30%; }
|
| 1058 |
+
.challenges-table th:nth-child(2) { width: 40%; }
|
| 1059 |
+
.challenges-table th:nth-child(3) { width: 30%; }
|
| 1060 |
+
|
| 1061 |
+
/* Future Improvements */
|
| 1062 |
+
.future-grid {
|
| 1063 |
+
display: grid;
|
| 1064 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 1065 |
+
gap: 1.5rem;
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
.future-card {
|
| 1069 |
+
background: rgba(26, 26, 26, 0.4);
|
| 1070 |
+
border: 1px solid rgba(212, 175, 55, 0.2);
|
| 1071 |
+
border-radius: 4px;
|
| 1072 |
+
padding: 1.5rem;
|
| 1073 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 1074 |
+
}
|
| 1075 |
+
|
| 1076 |
+
.future-card:hover {
|
| 1077 |
+
transform: translateY(-4px);
|
| 1078 |
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
|
| 1079 |
+
border-color: rgba(212, 175, 55, 0.4);
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
.future-header {
|
| 1083 |
+
display: flex;
|
| 1084 |
+
justify-content: space-between;
|
| 1085 |
+
align-items: flex-start;
|
| 1086 |
+
margin-bottom: 1rem;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
.future-header h4 {
|
| 1090 |
+
font-family: 'Space Mono', monospace;
|
| 1091 |
+
color: var(--paper-warm);
|
| 1092 |
+
font-size: 1rem;
|
| 1093 |
+
margin: 0;
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
.badge {
|
| 1097 |
+
font-size: 0.7rem;
|
| 1098 |
+
text-transform: uppercase;
|
| 1099 |
+
padding: 0.2rem 0.5rem;
|
| 1100 |
+
border-radius: 12px;
|
| 1101 |
+
font-weight: 700;
|
| 1102 |
+
letter-spacing: 0.5px;
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
.badge.planned {
|
| 1106 |
+
background: rgba(212, 175, 55, 0.2);
|
| 1107 |
+
color: var(--marquee-gold);
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
.badge.future {
|
| 1111 |
+
background: rgba(255, 255, 255, 0.1);
|
| 1112 |
+
color: #ccc;
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
.badge.research {
|
| 1116 |
+
background: rgba(160, 120, 200, 0.2);
|
| 1117 |
+
color: #c090e0;
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
.future-card p {
|
| 1121 |
+
font-size: 0.9rem;
|
| 1122 |
+
color: #aaa;
|
| 1123 |
+
margin: 0;
|
| 1124 |
+
line-height: 1.4;
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
/* Footer */
|
| 1128 |
+
.site-footer {
|
| 1129 |
+
border-top: 1px solid rgba(212, 175, 55, 0.2);
|
| 1130 |
+
padding: 4rem 2rem;
|
| 1131 |
+
text-align: center;
|
| 1132 |
+
background: var(--deep-ink);
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
.footer-content h4 {
|
| 1136 |
+
font-family: 'Playfair Display', serif;
|
| 1137 |
+
color: var(--marquee-gold);
|
| 1138 |
+
font-size: 1.5rem;
|
| 1139 |
+
margin-bottom: 1rem;
|
| 1140 |
+
letter-spacing: 1px;
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
.footer-content p {
|
| 1144 |
+
font-family: 'Space Mono', monospace;
|
| 1145 |
+
font-size: 0.85rem;
|
| 1146 |
+
color: var(--paper-warm);
|
| 1147 |
+
opacity: 0.6;
|
| 1148 |
+
line-height: 1.6;
|
| 1149 |
+
margin-bottom: 2rem;
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
.footer-links {
|
| 1153 |
+
display: flex;
|
| 1154 |
+
justify-content: center;
|
| 1155 |
+
gap: 1.5rem;
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
.footer-links a {
|
| 1159 |
+
color: var(--marquee-gold);
|
| 1160 |
+
text-decoration: none;
|
| 1161 |
+
font-size: 0.9rem;
|
| 1162 |
+
text-transform: uppercase;
|
| 1163 |
+
letter-spacing: 1px;
|
| 1164 |
+
transition: opacity 0.2s ease;
|
| 1165 |
+
}
|
| 1166 |
+
|
| 1167 |
+
.footer-links a:hover {
|
| 1168 |
+
opacity: 0.8;
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
@keyframes fadeIn {
|
| 1172 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 1173 |
+
to { opacity: 1; transform: translateY(0); }
|
| 1174 |
+
}
|
hf_start.sh
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
echo "Starting Sentiment Analysis app on Hugging Face Spaces..."
|
| 4 |
+
/usr/bin/supervisord -c /app/supervisord.conf
|
ml_service/Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install dependencies
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Download NLTK data required by preprocessing
|
| 10 |
+
RUN python -c "import nltk; nltk.download('stopwords', quiet=True); nltk.download('punkt', quiet=True); nltk.download('wordnet', quiet=True)"
|
| 11 |
+
|
| 12 |
+
# Copy app code
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# Expose port
|
| 16 |
+
EXPOSE 8000
|
| 17 |
+
|
| 18 |
+
# Start Uvicorn
|
| 19 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
ml_service/app/api/routes.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from fastapi import APIRouter, HTTPException
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
from app.schemas.prediction import (
|
| 6 |
+
PredictRequest,
|
| 7 |
+
PredictResponse,
|
| 8 |
+
OverallPrediction,
|
| 9 |
+
TimingInfo,
|
| 10 |
+
PredictMetadata,
|
| 11 |
+
ModelVersions
|
| 12 |
+
)
|
| 13 |
+
from app.services.model_loader import model_manager
|
| 14 |
+
from app.services.predictor import predict_lr, predict_lstm, predict_bert
|
| 15 |
+
|
| 16 |
+
router = APIRouter()
|
| 17 |
+
|
| 18 |
+
MODEL_VERSIONS = ModelVersions(
|
| 19 |
+
lr="1.0.0",
|
| 20 |
+
lstm="1.0.0",
|
| 21 |
+
bert="1.0.0"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
@router.post("/predict", response_model=PredictResponse)
|
| 25 |
+
async def predict(request: PredictRequest):
|
| 26 |
+
start_time = time.perf_counter()
|
| 27 |
+
models_result = {}
|
| 28 |
+
|
| 29 |
+
pos_count = 0
|
| 30 |
+
neg_count = 0
|
| 31 |
+
total_count = 0
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
if "lr" in request.models:
|
| 35 |
+
lr_res = predict_lr(request.text)
|
| 36 |
+
models_result["lr"] = lr_res
|
| 37 |
+
total_count += 1
|
| 38 |
+
if lr_res.label == "Positive": pos_count += 1
|
| 39 |
+
else: neg_count += 1
|
| 40 |
+
|
| 41 |
+
if "lstm" in request.models:
|
| 42 |
+
lstm_res = predict_lstm(request.text)
|
| 43 |
+
models_result["lstm"] = lstm_res
|
| 44 |
+
total_count += 1
|
| 45 |
+
if lstm_res.label == "Positive": pos_count += 1
|
| 46 |
+
else: neg_count += 1
|
| 47 |
+
|
| 48 |
+
if "bert" in request.models:
|
| 49 |
+
bert_res = predict_bert(request.text)
|
| 50 |
+
models_result["bert"] = bert_res
|
| 51 |
+
total_count += 1
|
| 52 |
+
if bert_res.label == "Positive": pos_count += 1
|
| 53 |
+
else: neg_count += 1
|
| 54 |
+
|
| 55 |
+
except Exception as e:
|
| 56 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 57 |
+
|
| 58 |
+
if total_count == 0:
|
| 59 |
+
raise HTTPException(status_code=400, detail="No valid models selected or available")
|
| 60 |
+
|
| 61 |
+
majority_label = "Positive" if pos_count >= neg_count else "Negative"
|
| 62 |
+
agreement = f"{max(pos_count, neg_count)}/{total_count}"
|
| 63 |
+
|
| 64 |
+
total_ms = (time.perf_counter() - start_time) * 1000
|
| 65 |
+
|
| 66 |
+
return PredictResponse(
|
| 67 |
+
overall=OverallPrediction(label=majority_label, agreement=agreement),
|
| 68 |
+
models=models_result,
|
| 69 |
+
timing=TimingInfo(total_ms=total_ms),
|
| 70 |
+
metadata=PredictMetadata(model_versions=MODEL_VERSIONS)
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
@router.get("/health")
|
| 74 |
+
async def health_check() -> Dict[str, Any]:
|
| 75 |
+
return {
|
| 76 |
+
"status": "healthy",
|
| 77 |
+
"backend": False, # Gateway sets this to true later
|
| 78 |
+
"ml_service": True,
|
| 79 |
+
"database": False, # Gateway sets this to true later
|
| 80 |
+
"models_loaded": {
|
| 81 |
+
"logistic_regression": model_manager.models_loaded.get("logistic_regression", False),
|
| 82 |
+
"lstm": model_manager.models_loaded.get("lstm", False),
|
| 83 |
+
"bert": model_manager.models_loaded.get("bert", False)
|
| 84 |
+
},
|
| 85 |
+
"version": "1.0.0"
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
@router.get("/models")
|
| 89 |
+
async def get_models() -> Dict[str, Any]:
|
| 90 |
+
return {
|
| 91 |
+
"lr": {
|
| 92 |
+
"name": "Logistic Regression (TF-IDF)",
|
| 93 |
+
"type": "Machine Learning",
|
| 94 |
+
"version": MODEL_VERSIONS.lr,
|
| 95 |
+
"description": "Bag-of-words classifier using TF-IDF features"
|
| 96 |
+
},
|
| 97 |
+
"lstm": {
|
| 98 |
+
"name": "Bi-LSTM",
|
| 99 |
+
"type": "Deep Learning",
|
| 100 |
+
"version": MODEL_VERSIONS.lstm,
|
| 101 |
+
"description": "Bidirectional recurrent neural network"
|
| 102 |
+
},
|
| 103 |
+
"bert": {
|
| 104 |
+
"name": "BERT (Fine-Tuned)",
|
| 105 |
+
"type": "Transformer",
|
| 106 |
+
"version": MODEL_VERSIONS.bert,
|
| 107 |
+
"description": "Fine-tuned contextual embedding model"
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
@router.get("/model_metrics")
|
| 112 |
+
async def get_model_metrics() -> Dict[str, Any]:
|
| 113 |
+
return {
|
| 114 |
+
# Minimal mock payload for now, database will hold the real ones
|
| 115 |
+
"status": "Not implemented here, served by DB via Gateway"
|
| 116 |
+
}
|
ml_service/app/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pydantic_settings import BaseSettings
|
| 3 |
+
|
| 4 |
+
class Settings(BaseSettings):
|
| 5 |
+
PROJECT_NAME: str = "The Screening Room - ML Service"
|
| 6 |
+
VERSION: str = "1.0.0"
|
| 7 |
+
API_V1_STR: str = "/api/v1"
|
| 8 |
+
|
| 9 |
+
# Model paths (relative to project root when running via docker/uvicorn)
|
| 10 |
+
MODELS_ML_DIR: str = os.getenv("MODELS_ML_DIR", "../models_ml")
|
| 11 |
+
MODELS_LSTM_DIR: str = os.getenv("MODELS_LSTM_DIR", "../models_lstm")
|
| 12 |
+
MODELS_BERT_DIR: str = os.getenv("MODELS_BERT_DIR", "../models_bert/tuned")
|
| 13 |
+
DATA_DIR: str = os.getenv("DATA_DIR", "../data")
|
| 14 |
+
|
| 15 |
+
class Config:
|
| 16 |
+
case_sensitive = True
|
| 17 |
+
|
| 18 |
+
settings = Settings()
|
ml_service/app/schemas/prediction.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Optional, Dict, Any
|
| 3 |
+
|
| 4 |
+
class PredictRequest(BaseModel):
|
| 5 |
+
text: str = Field(..., description="The movie review text to analyze")
|
| 6 |
+
models: List[str] = Field(
|
| 7 |
+
default=["lr", "lstm", "bert"],
|
| 8 |
+
description="List of models to use for prediction"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
class ModelPrediction(BaseModel):
|
| 12 |
+
name: str
|
| 13 |
+
label: str
|
| 14 |
+
confidence: float
|
| 15 |
+
latency_ms: float
|
| 16 |
+
top_positive_words: Optional[List[str]] = None
|
| 17 |
+
top_negative_words: Optional[List[str]] = None
|
| 18 |
+
reasoning: str
|
| 19 |
+
|
| 20 |
+
class ModelVersions(BaseModel):
|
| 21 |
+
lr: str
|
| 22 |
+
lstm: str
|
| 23 |
+
bert: str
|
| 24 |
+
|
| 25 |
+
class PredictMetadata(BaseModel):
|
| 26 |
+
model_versions: ModelVersions
|
| 27 |
+
|
| 28 |
+
class OverallPrediction(BaseModel):
|
| 29 |
+
label: str
|
| 30 |
+
agreement: str
|
| 31 |
+
|
| 32 |
+
class TimingInfo(BaseModel):
|
| 33 |
+
total_ms: float
|
| 34 |
+
|
| 35 |
+
class PredictResponse(BaseModel):
|
| 36 |
+
overall: OverallPrediction
|
| 37 |
+
models: Dict[str, ModelPrediction]
|
| 38 |
+
timing: TimingInfo
|
| 39 |
+
metadata: PredictMetadata
|
| 40 |
+
|
| 41 |
+
class ModelMetadata(BaseModel):
|
| 42 |
+
name: str
|
| 43 |
+
type: str
|
| 44 |
+
accuracy: float
|
| 45 |
+
version: str
|
| 46 |
+
size: str
|
ml_service/app/services/explainer.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from typing import List, Tuple
|
| 3 |
+
|
| 4 |
+
def get_lr_important_words(text: str, vectorizer, model, top_n: int = 5) -> Tuple[List[str], List[str]]:
|
| 5 |
+
"""Extract top positive and negative words for LR prediction based on TF-IDF weights."""
|
| 6 |
+
if not vectorizer or not model:
|
| 7 |
+
return [], []
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
# Transform the single document
|
| 11 |
+
tfidf_vec = vectorizer.transform([text])
|
| 12 |
+
|
| 13 |
+
# Get feature names (vocabulary)
|
| 14 |
+
feature_names = vectorizer.get_feature_names_out()
|
| 15 |
+
|
| 16 |
+
# Get the non-zero feature indices and their tf-idf values in this document
|
| 17 |
+
doc_indices = tfidf_vec.nonzero()[1]
|
| 18 |
+
|
| 19 |
+
# Multiply by model coefficients to get importance
|
| 20 |
+
# model.coef_[0] because it's a binary classifier
|
| 21 |
+
importances = [(feature_names[idx], tfidf_vec[0, idx] * model.coef_[0][idx]) for idx in doc_indices]
|
| 22 |
+
|
| 23 |
+
# Sort by importance
|
| 24 |
+
importances.sort(key=lambda x: x[1])
|
| 25 |
+
|
| 26 |
+
# Deduplicate overlapping tokens (e.g. "movie" vs "movie ended") globally
|
| 27 |
+
def deduplicate(word_score_pairs, n, exclude_words=None):
|
| 28 |
+
if exclude_words is None:
|
| 29 |
+
exclude_words = []
|
| 30 |
+
kept = []
|
| 31 |
+
for word, _ in word_score_pairs:
|
| 32 |
+
is_dup = any((word in k or k in word) for k in kept + exclude_words)
|
| 33 |
+
if not is_dup:
|
| 34 |
+
kept.append(word)
|
| 35 |
+
if len(kept) == n:
|
| 36 |
+
break
|
| 37 |
+
return kept
|
| 38 |
+
|
| 39 |
+
# Top positive (highest positive values)
|
| 40 |
+
pos_pairs = [(word, score) for word, score in reversed(importances) if score > 0]
|
| 41 |
+
top_pos = deduplicate(pos_pairs, top_n)
|
| 42 |
+
|
| 43 |
+
# Top negative (lowest negative values), excluding any already in positive
|
| 44 |
+
neg_pairs = [(word, score) for word, score in importances if score < 0]
|
| 45 |
+
top_neg = deduplicate(neg_pairs, top_n, exclude_words=top_pos)
|
| 46 |
+
|
| 47 |
+
return top_pos, top_neg
|
| 48 |
+
except Exception:
|
| 49 |
+
return [], []
|
| 50 |
+
|
| 51 |
+
def generate_reasoning(model_name: str, confidence: float, latency: float) -> str:
|
| 52 |
+
"""Generate honest explanation for model behavior."""
|
| 53 |
+
|
| 54 |
+
conf_label = "high" if confidence >= 0.80 else "moderate" if confidence >= 0.60 else "low"
|
| 55 |
+
|
| 56 |
+
if model_name == "lr":
|
| 57 |
+
return f"TF-IDF representation evaluated independently. Captured {conf_label} confidence based on aggregated term weights."
|
| 58 |
+
|
| 59 |
+
elif model_name == "lstm":
|
| 60 |
+
return f"Bidirectional recurrent layers processed the sequence chronologically, retaining context window to yield {conf_label} confidence."
|
| 61 |
+
|
| 62 |
+
elif model_name == "bert":
|
| 63 |
+
return f"Transformer self-attention evaluated full bidirectional context across 12 layers, resulting in {conf_label} confidence."
|
| 64 |
+
|
| 65 |
+
return ""
|
ml_service/app/services/model_loader.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import joblib
|
| 3 |
+
import pickle
|
| 4 |
+
import torch
|
| 5 |
+
import warnings
|
| 6 |
+
import tensorflow as tf
|
| 7 |
+
from transformers import BertTokenizerFast, BertForSequenceClassification
|
| 8 |
+
from app.config import settings
|
| 9 |
+
|
| 10 |
+
warnings.filterwarnings("ignore")
|
| 11 |
+
|
| 12 |
+
class ModelManager:
|
| 13 |
+
_instance = None
|
| 14 |
+
|
| 15 |
+
def __new__(cls):
|
| 16 |
+
if cls._instance is None:
|
| 17 |
+
cls._instance = super(ModelManager, cls).__new__(cls)
|
| 18 |
+
cls._instance._initialized = False
|
| 19 |
+
return cls._instance
|
| 20 |
+
|
| 21 |
+
def __init__(self):
|
| 22 |
+
if self._initialized:
|
| 23 |
+
return
|
| 24 |
+
|
| 25 |
+
self.device = torch.device("xpu" if torch.xpu.is_available() else "cuda" if torch.cuda.is_available() else "cpu")
|
| 26 |
+
self.models_loaded = {
|
| 27 |
+
"logistic_regression": False,
|
| 28 |
+
"lstm": False,
|
| 29 |
+
"bert": False
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
# LR specific
|
| 33 |
+
self.lr_model = None
|
| 34 |
+
self.lr_vectorizer = None
|
| 35 |
+
|
| 36 |
+
# LSTM specific
|
| 37 |
+
self.lstm_model = None
|
| 38 |
+
self.lstm_fast_predict = None
|
| 39 |
+
self.lstm_tokenizer = None
|
| 40 |
+
self.lstm_config = None
|
| 41 |
+
|
| 42 |
+
# BERT specific
|
| 43 |
+
self.bert_model = None
|
| 44 |
+
self.bert_tokenizer = None
|
| 45 |
+
self.bert_threshold = 0.5
|
| 46 |
+
|
| 47 |
+
self._initialized = True
|
| 48 |
+
|
| 49 |
+
def load_all_models(self):
|
| 50 |
+
self.load_lr()
|
| 51 |
+
self.load_lstm()
|
| 52 |
+
self.load_bert()
|
| 53 |
+
self.warmup_models()
|
| 54 |
+
|
| 55 |
+
def warmup_models(self):
|
| 56 |
+
# Warmup to absorb initialization costs (like TF graph building)
|
| 57 |
+
print("Warming up models...")
|
| 58 |
+
try:
|
| 59 |
+
if self.models_loaded["logistic_regression"]:
|
| 60 |
+
self.lr_model.predict(self.lr_vectorizer.transform(["warmup"]))
|
| 61 |
+
except Exception as e: print(f"LR warmup failed: {e}")
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
if self.models_loaded["lstm"]:
|
| 65 |
+
seq = self.lstm_tokenizer.texts_to_sequences(["warmup text"])
|
| 66 |
+
# Note: Keras pad_sequences requires absolute import here to avoid circular dependencies
|
| 67 |
+
from keras.preprocessing.sequence import pad_sequences
|
| 68 |
+
seq = pad_sequences(seq, maxlen=self.lstm_config.get("max_len", 300))
|
| 69 |
+
self.lstm_fast_predict(tf.convert_to_tensor(seq))
|
| 70 |
+
except Exception as e: print(f"LSTM warmup failed: {e}")
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
if self.models_loaded["bert"]:
|
| 74 |
+
inputs = self.bert_tokenizer("warmup text", return_tensors="pt", truncation=True, padding=True, max_length=128)
|
| 75 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 76 |
+
with torch.no_grad():
|
| 77 |
+
self.bert_model(**inputs)
|
| 78 |
+
except Exception as e: print(f"BERT warmup failed: {e}")
|
| 79 |
+
print("Warmup complete.")
|
| 80 |
+
|
| 81 |
+
def load_lr(self):
|
| 82 |
+
try:
|
| 83 |
+
vec_path = os.path.join(settings.MODELS_ML_DIR, "tfidf_vectorizer.pkl")
|
| 84 |
+
model_path = os.path.join(settings.MODELS_ML_DIR, "sentiment_model.pkl")
|
| 85 |
+
if os.path.exists(vec_path) and os.path.exists(model_path):
|
| 86 |
+
self.lr_vectorizer = joblib.load(vec_path)
|
| 87 |
+
self.lr_model = joblib.load(model_path)
|
| 88 |
+
self.models_loaded["logistic_regression"] = True
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Failed to load Logistic Regression: {e}")
|
| 91 |
+
|
| 92 |
+
def load_lstm(self):
|
| 93 |
+
try:
|
| 94 |
+
model_path = os.path.join(settings.MODELS_LSTM_DIR, "best_lstm_model.keras")
|
| 95 |
+
tok_path = os.path.join(settings.DATA_DIR, "tokenizer.pkl")
|
| 96 |
+
cfg_path = os.path.join(settings.DATA_DIR, "config.pkl")
|
| 97 |
+
|
| 98 |
+
if os.path.exists(model_path) and os.path.exists(tok_path) and os.path.exists(cfg_path):
|
| 99 |
+
self.lstm_model = tf.keras.models.load_model(model_path)
|
| 100 |
+
|
| 101 |
+
@tf.function(reduce_retracing=True)
|
| 102 |
+
def fast_predict(x):
|
| 103 |
+
return self.lstm_model(x, training=False)
|
| 104 |
+
self.lstm_fast_predict = fast_predict
|
| 105 |
+
|
| 106 |
+
with open(tok_path, "rb") as f:
|
| 107 |
+
self.lstm_tokenizer = pickle.load(f)
|
| 108 |
+
with open(cfg_path, "rb") as f:
|
| 109 |
+
self.lstm_config = pickle.load(f)
|
| 110 |
+
self.models_loaded["lstm"] = True
|
| 111 |
+
except Exception as e:
|
| 112 |
+
print(f"Failed to load LSTM: {e}")
|
| 113 |
+
|
| 114 |
+
def load_bert(self):
|
| 115 |
+
try:
|
| 116 |
+
if os.path.exists(settings.MODELS_BERT_DIR):
|
| 117 |
+
self.bert_tokenizer = BertTokenizerFast.from_pretrained(settings.MODELS_BERT_DIR, local_files_only=True)
|
| 118 |
+
self.bert_model = BertForSequenceClassification.from_pretrained(settings.MODELS_BERT_DIR, local_files_only=True)
|
| 119 |
+
self.bert_model.to(self.device)
|
| 120 |
+
self.bert_model.eval()
|
| 121 |
+
|
| 122 |
+
thresh_path = os.path.join(settings.MODELS_BERT_DIR, "threshold.pkl")
|
| 123 |
+
if os.path.exists(thresh_path):
|
| 124 |
+
with open(thresh_path, "rb") as f:
|
| 125 |
+
self.bert_threshold = pickle.load(f).get("threshold", 0.5)
|
| 126 |
+
|
| 127 |
+
self.models_loaded["bert"] = True
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f"Failed to load BERT: {e}")
|
| 130 |
+
|
| 131 |
+
model_manager = ModelManager()
|
ml_service/app/services/predictor.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import numpy as np
|
| 3 |
+
import tensorflow as tf
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
from app.services.model_loader import model_manager
|
| 8 |
+
from app.services.explainer import get_lr_important_words, generate_reasoning
|
| 9 |
+
from app.utils.preprocessing import (
|
| 10 |
+
preprocess_for_tfidf,
|
| 11 |
+
text_to_sequence,
|
| 12 |
+
preprocess_for_bert
|
| 13 |
+
)
|
| 14 |
+
from app.schemas.prediction import ModelPrediction
|
| 15 |
+
|
| 16 |
+
def _normalize_conf(prob_pos: float, threshold: float) -> float:
|
| 17 |
+
if prob_pos >= threshold:
|
| 18 |
+
return 0.5 + 0.5 * ((prob_pos - threshold) / (1.0 - threshold)) if threshold < 1.0 else 1.0
|
| 19 |
+
else:
|
| 20 |
+
return 0.5 + 0.5 * ((threshold - prob_pos) / threshold) if threshold > 0.0 else 1.0
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def predict_lr(text: str) -> ModelPrediction:
|
| 24 |
+
start_time = time.perf_counter()
|
| 25 |
+
|
| 26 |
+
if not model_manager.models_loaded["logistic_regression"]:
|
| 27 |
+
raise RuntimeError("LR Model not loaded")
|
| 28 |
+
|
| 29 |
+
vec = model_manager.lr_vectorizer
|
| 30 |
+
mdl = model_manager.lr_model
|
| 31 |
+
|
| 32 |
+
cleaned = preprocess_for_tfidf(text)
|
| 33 |
+
features = vec.transform([cleaned])
|
| 34 |
+
proba = mdl.predict_proba(features)[0]
|
| 35 |
+
|
| 36 |
+
label_idx = int(np.argmax(proba))
|
| 37 |
+
label = "Positive" if label_idx == 1 else "Negative"
|
| 38 |
+
confidence = float(proba[label_idx])
|
| 39 |
+
|
| 40 |
+
latency_ms = (time.perf_counter() - start_time) * 1000
|
| 41 |
+
|
| 42 |
+
top_pos, top_neg = get_lr_important_words(cleaned, vec, mdl)
|
| 43 |
+
|
| 44 |
+
return ModelPrediction(
|
| 45 |
+
name="The Statistician",
|
| 46 |
+
label=label,
|
| 47 |
+
confidence=confidence,
|
| 48 |
+
latency_ms=latency_ms,
|
| 49 |
+
top_positive_words=top_pos,
|
| 50 |
+
top_negative_words=top_neg,
|
| 51 |
+
reasoning=generate_reasoning("lr", confidence, latency_ms)
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
def predict_lstm(text: str) -> ModelPrediction:
|
| 55 |
+
start_time = time.perf_counter()
|
| 56 |
+
|
| 57 |
+
if not model_manager.models_loaded["lstm"]:
|
| 58 |
+
raise RuntimeError("LSTM Model not loaded")
|
| 59 |
+
|
| 60 |
+
mdl = model_manager.lstm_model
|
| 61 |
+
tok = model_manager.lstm_tokenizer
|
| 62 |
+
cfg = model_manager.lstm_config
|
| 63 |
+
|
| 64 |
+
max_len = cfg.get("max_len", 300)
|
| 65 |
+
threshold = cfg.get("best_threshold", 0.5)
|
| 66 |
+
|
| 67 |
+
seq = text_to_sequence(text, tok, max_len)
|
| 68 |
+
tensor_seq = tf.convert_to_tensor(seq)
|
| 69 |
+
raw = model_manager.lstm_fast_predict(tensor_seq).numpy()[0][0]
|
| 70 |
+
|
| 71 |
+
label = "Positive" if raw >= threshold else "Negative"
|
| 72 |
+
conf = _normalize_conf(float(raw), threshold)
|
| 73 |
+
|
| 74 |
+
latency_ms = (time.perf_counter() - start_time) * 1000
|
| 75 |
+
|
| 76 |
+
return ModelPrediction(
|
| 77 |
+
name="The Sequentialist",
|
| 78 |
+
label=label,
|
| 79 |
+
confidence=conf,
|
| 80 |
+
latency_ms=latency_ms,
|
| 81 |
+
reasoning=generate_reasoning("lstm", conf, latency_ms)
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
def predict_bert(text: str) -> ModelPrediction:
|
| 85 |
+
start_time = time.perf_counter()
|
| 86 |
+
|
| 87 |
+
if not model_manager.models_loaded["bert"]:
|
| 88 |
+
raise RuntimeError("BERT Model not loaded")
|
| 89 |
+
|
| 90 |
+
mdl = model_manager.bert_model
|
| 91 |
+
tok = model_manager.bert_tokenizer
|
| 92 |
+
dev = model_manager.device
|
| 93 |
+
threshold = model_manager.bert_threshold
|
| 94 |
+
|
| 95 |
+
cleaned = preprocess_for_bert(text)
|
| 96 |
+
inputs = tok(cleaned, return_tensors="pt", truncation=True, padding=True, max_length=128)
|
| 97 |
+
inputs = {k: v.to(dev) for k, v in inputs.items()}
|
| 98 |
+
|
| 99 |
+
with torch.no_grad():
|
| 100 |
+
logits = mdl(**inputs).logits
|
| 101 |
+
|
| 102 |
+
proba = F.softmax(logits, dim=-1).cpu().numpy()[0]
|
| 103 |
+
prob_pos = float(proba[1])
|
| 104 |
+
|
| 105 |
+
label = "Positive" if prob_pos >= threshold else "Negative"
|
| 106 |
+
conf = _normalize_conf(prob_pos, threshold)
|
| 107 |
+
|
| 108 |
+
latency_ms = (time.perf_counter() - start_time) * 1000
|
| 109 |
+
|
| 110 |
+
return ModelPrediction(
|
| 111 |
+
name="The Contextualist",
|
| 112 |
+
label=label,
|
| 113 |
+
confidence=conf,
|
| 114 |
+
latency_ms=latency_ms,
|
| 115 |
+
reasoning=generate_reasoning("bert", conf, latency_ms)
|
| 116 |
+
)
|
ml_service/app/utils/preprocessing.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
preprocessing_utils.py
|
| 3 |
+
======================
|
| 4 |
+
Shared NLP preprocessing utilities for all model pipelines.
|
| 5 |
+
Handles text cleaning, normalization, and sequence preparation.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import re
|
| 9 |
+
import string
|
| 10 |
+
import numpy as np
|
| 11 |
+
from typing import List, Optional
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ─── NLTK Setup ──────────────────────────────────────────────────────────────
|
| 15 |
+
|
| 16 |
+
def ensure_nltk_resources():
|
| 17 |
+
"""Download required NLTK data if not already present."""
|
| 18 |
+
import nltk
|
| 19 |
+
resources = [
|
| 20 |
+
("corpora/stopwords", "stopwords"),
|
| 21 |
+
("tokenizers/punkt", "punkt"),
|
| 22 |
+
("corpora/wordnet", "wordnet"),
|
| 23 |
+
]
|
| 24 |
+
for path, name in resources:
|
| 25 |
+
try:
|
| 26 |
+
nltk.data.find(path)
|
| 27 |
+
except LookupError:
|
| 28 |
+
nltk.download(name, quiet=True)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
ensure_nltk_resources()
|
| 32 |
+
|
| 33 |
+
import nltk
|
| 34 |
+
from nltk.corpus import stopwords
|
| 35 |
+
from nltk.stem import WordNetLemmatizer
|
| 36 |
+
|
| 37 |
+
STOP_WORDS = set(stopwords.words("english"))
|
| 38 |
+
# Preserve negation words — removing them destroys semantic inversion signals
|
| 39 |
+
_NEGATION_WORDS = {
|
| 40 |
+
"no", "nor", "not", "never", "don't", "didn't", "isn't", "wasn't",
|
| 41 |
+
"aren't", "won't", "can't", "couldn't", "shouldn't", "wouldn't",
|
| 42 |
+
"hasn't", "haven't", "hadn't", "doesn't", "n't",
|
| 43 |
+
}
|
| 44 |
+
STOP_WORDS = STOP_WORDS - _NEGATION_WORDS
|
| 45 |
+
LEMMATIZER = WordNetLemmatizer()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# ─── Contraction Expansion ────────────────────────────────────────────────────
|
| 49 |
+
|
| 50 |
+
CONTRACTION_MAP = {
|
| 51 |
+
"don't": "do not", "didn't": "did not", "doesn't": "does not",
|
| 52 |
+
"won't": "will not", "wouldn't": "would not", "couldn't": "could not",
|
| 53 |
+
"shouldn't": "should not", "can't": "cannot", "couldn't": "could not",
|
| 54 |
+
"isn't": "is not", "aren't": "are not", "wasn't": "was not",
|
| 55 |
+
"weren't": "were not", "hasn't": "has not", "haven't": "have not",
|
| 56 |
+
"hadn't": "had not", "ain't": "is not", "i'm": "i am",
|
| 57 |
+
"you're": "you are", "he's": "he is", "she's": "she is",
|
| 58 |
+
"it's": "it is", "we're": "we are", "they're": "they are",
|
| 59 |
+
"i've": "i have", "you've": "you have", "we've": "we have",
|
| 60 |
+
"they've": "they have", "i'll": "i will", "you'll": "you will",
|
| 61 |
+
"he'll": "he will", "she'll": "she will", "we'll": "we will",
|
| 62 |
+
"they'll": "they will", "i'd": "i would", "you'd": "you would",
|
| 63 |
+
"he'd": "he would", "she'd": "she would", "we'd": "we would",
|
| 64 |
+
"they'd": "they would", "let's": "let us",
|
| 65 |
+
}
|
| 66 |
+
_CONTRACTION_PATTERN = re.compile(
|
| 67 |
+
r"\b(" + "|".join(re.escape(k) for k in CONTRACTION_MAP) + r")\b",
|
| 68 |
+
flags=re.IGNORECASE,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def expand_contractions(text: str) -> str:
|
| 73 |
+
"""Expand English contractions to their full forms."""
|
| 74 |
+
def _replace(m: re.Match) -> str:
|
| 75 |
+
key = m.group(1).lower()
|
| 76 |
+
return CONTRACTION_MAP.get(key, m.group(1))
|
| 77 |
+
return _CONTRACTION_PATTERN.sub(_replace, text)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ─── Core Cleaning ───────────────────────────────────────────────────────────
|
| 81 |
+
|
| 82 |
+
def remove_html_tags(text: str) -> str:
|
| 83 |
+
"""Strip HTML tags from text."""
|
| 84 |
+
return re.sub(r"<[^>]+>", " ", text)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def remove_urls(text: str) -> str:
|
| 88 |
+
"""Remove URLs from text."""
|
| 89 |
+
return re.sub(r"http\S+|www\S+|https\S+", " ", text, flags=re.MULTILINE)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def remove_special_characters(text: str) -> str:
|
| 93 |
+
"""Remove non-alphabetical characters, keep spaces."""
|
| 94 |
+
return re.sub(r"[^a-zA-Z\s]", " ", text)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def normalize_whitespace(text: str) -> str:
|
| 98 |
+
"""Collapse multiple spaces into one."""
|
| 99 |
+
return re.sub(r"\s+", " ", text).strip()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# ─── Traditional ML Preprocessing (TF-IDF) ────────────────────────────────
|
| 103 |
+
|
| 104 |
+
def preprocess_for_tfidf(text: str, remove_stopwords: bool = True) -> str:
|
| 105 |
+
"""
|
| 106 |
+
Full preprocessing pipeline for TF-IDF / Logistic Regression.
|
| 107 |
+
|
| 108 |
+
Steps:
|
| 109 |
+
1. Lowercase
|
| 110 |
+
2. Expand contractions (e.g. didn't -> did not)
|
| 111 |
+
3. Remove HTML
|
| 112 |
+
4. Remove URLs
|
| 113 |
+
5. Remove special chars
|
| 114 |
+
6. Optional stopword removal
|
| 115 |
+
7. Lemmatize
|
| 116 |
+
8. Normalize whitespace
|
| 117 |
+
"""
|
| 118 |
+
text = text.lower()
|
| 119 |
+
text = expand_contractions(text)
|
| 120 |
+
text = remove_html_tags(text)
|
| 121 |
+
text = remove_urls(text)
|
| 122 |
+
text = remove_special_characters(text)
|
| 123 |
+
|
| 124 |
+
tokens = text.split()
|
| 125 |
+
|
| 126 |
+
if remove_stopwords:
|
| 127 |
+
tokens = [t for t in tokens if t not in STOP_WORDS]
|
| 128 |
+
|
| 129 |
+
tokens = [LEMMATIZER.lemmatize(t) for t in tokens if len(t) > 1]
|
| 130 |
+
|
| 131 |
+
return normalize_whitespace(" ".join(tokens))
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
# ─── Deep Learning Preprocessing (LSTM) ──────────────────────────────────
|
| 135 |
+
|
| 136 |
+
def preprocess_for_lstm(text: str) -> str:
|
| 137 |
+
"""
|
| 138 |
+
Preprocessing pipeline for LSTM/sequence models.
|
| 139 |
+
Matches clean_text in retrain_lstm.py exactly to align train and inference.
|
| 140 |
+
|
| 141 |
+
Steps:
|
| 142 |
+
1. Lowercase
|
| 143 |
+
2. Expand contractions
|
| 144 |
+
3. Remove HTML
|
| 145 |
+
4. Remove URLs
|
| 146 |
+
5. Keep alphanumeric and basic punctuation (!?.,)
|
| 147 |
+
6. Normalize whitespace
|
| 148 |
+
"""
|
| 149 |
+
text = text.lower()
|
| 150 |
+
text = expand_contractions(text)
|
| 151 |
+
text = remove_html_tags(text)
|
| 152 |
+
text = remove_urls(text)
|
| 153 |
+
text = re.sub(r'[^a-zA-Z0-9!?., ]', '', text)
|
| 154 |
+
return normalize_whitespace(text)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def text_to_sequence(text: str, tokenizer, max_len: int = 300) -> np.ndarray:
|
| 158 |
+
"""
|
| 159 |
+
Convert text to padded integer sequence for LSTM inference.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
text: Raw or cleaned text string.
|
| 163 |
+
tokenizer: Fitted Keras Tokenizer object.
|
| 164 |
+
max_len: Maximum sequence length (pad/truncate to this).
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
Numpy array of shape (1, max_len).
|
| 168 |
+
"""
|
| 169 |
+
from tensorflow.keras.preprocessing.sequence import pad_sequences # type: ignore
|
| 170 |
+
|
| 171 |
+
cleaned = preprocess_for_lstm(text)
|
| 172 |
+
seq = tokenizer.texts_to_sequences([cleaned])
|
| 173 |
+
padded = pad_sequences(seq, maxlen=max_len, padding="post", truncating="post")
|
| 174 |
+
return padded
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# ─── Transformer Preprocessing (BERT) ────────────────────────────────────
|
| 178 |
+
|
| 179 |
+
def preprocess_for_bert(text: str) -> str:
|
| 180 |
+
"""
|
| 181 |
+
Minimal preprocessing for BERT — the tokenizer handles most normalization.
|
| 182 |
+
|
| 183 |
+
BERT's WordPiece tokenizer is robust; we only:
|
| 184 |
+
1. Remove HTML tags
|
| 185 |
+
2. Collapse excessive whitespace
|
| 186 |
+
"""
|
| 187 |
+
text = remove_html_tags(text)
|
| 188 |
+
return normalize_whitespace(text)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# ─── Text Analysis Utilities ──────────────────────────────────────────────
|
| 192 |
+
|
| 193 |
+
def get_word_count(text: str) -> int:
|
| 194 |
+
"""Count words in text."""
|
| 195 |
+
return len(text.split())
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def get_char_count(text: str) -> int:
|
| 199 |
+
"""Count characters (excluding spaces) in text."""
|
| 200 |
+
return len(text.replace(" ", ""))
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def detect_negation(text: str) -> bool:
|
| 204 |
+
"""Simple negation detection for reasoning engine."""
|
| 205 |
+
negation_words = {
|
| 206 |
+
"not", "no", "never", "none", "nobody", "nothing", "neither",
|
| 207 |
+
"nowhere", "nor", "cannot", "can't", "won't", "don't", "doesn't",
|
| 208 |
+
"didn't", "isn't", "aren't", "wasn't", "weren't", "hasn't",
|
| 209 |
+
"haven't", "hadn't", "wouldn't", "shouldn't", "couldn't",
|
| 210 |
+
"n't", "hardly", "barely", "scarcely"
|
| 211 |
+
}
|
| 212 |
+
tokens = set(text.lower().split())
|
| 213 |
+
return bool(tokens & negation_words)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def detect_sarcasm_signals(text: str) -> bool:
|
| 217 |
+
"""Heuristic sarcasm/irony signal detection."""
|
| 218 |
+
sarcasm_patterns = [
|
| 219 |
+
r"\b(yeah right|sure sure|totally|oh great|fantastic job|brilliant)\b",
|
| 220 |
+
r"(!!!|\?\?\?)",
|
| 221 |
+
r"\b(not really|kind of|sort of)\b",
|
| 222 |
+
r"(worst.*best|best.*worst)",
|
| 223 |
+
]
|
| 224 |
+
text_lower = text.lower()
|
| 225 |
+
return any(re.search(p, text_lower) for p in sarcasm_patterns)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def detect_mixed_sentiment(text: str) -> bool:
|
| 229 |
+
"""Detect presence of both positive and negative signals."""
|
| 230 |
+
positive_words = {
|
| 231 |
+
"great", "good", "excellent", "amazing", "wonderful", "fantastic",
|
| 232 |
+
"love", "loved", "enjoy", "enjoyed", "brilliant", "superb", "perfect"
|
| 233 |
+
}
|
| 234 |
+
negative_words = {
|
| 235 |
+
"bad", "terrible", "awful", "horrible", "boring", "hate", "hated",
|
| 236 |
+
"disappointing", "disappointed", "poor", "worst", "dull", "weak"
|
| 237 |
+
}
|
| 238 |
+
tokens = set(text.lower().split())
|
| 239 |
+
has_pos = bool(tokens & positive_words)
|
| 240 |
+
has_neg = bool(tokens & negative_words)
|
| 241 |
+
return has_pos and has_neg
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def extract_key_phrases(text: str, top_n: int = 5) -> List[str]:
|
| 245 |
+
"""
|
| 246 |
+
Extract simple keyword phrases for reasoning display.
|
| 247 |
+
Returns tokens most likely to influence sentiment.
|
| 248 |
+
"""
|
| 249 |
+
sentiment_vocab = {
|
| 250 |
+
# Strong positive
|
| 251 |
+
"excellent", "amazing", "outstanding", "brilliant", "masterpiece",
|
| 252 |
+
"fantastic", "wonderful", "superb", "love", "perfect", "beautiful",
|
| 253 |
+
"great", "good", "enjoy", "entertaining", "captivating", "thrilling",
|
| 254 |
+
# Strong negative
|
| 255 |
+
"terrible", "awful", "horrible", "boring", "waste", "disappointing",
|
| 256 |
+
"bad", "poor", "worst", "dreadful", "pathetic", "ridiculous",
|
| 257 |
+
"unbearable", "painful", "disaster", "failure", "mediocre", "weak",
|
| 258 |
+
# Modifiers
|
| 259 |
+
"very", "extremely", "absolutely", "completely", "totally", "quite",
|
| 260 |
+
"never", "not", "no", "hardly", "barely",
|
| 261 |
+
}
|
| 262 |
+
tokens = re.findall(r"\b[a-zA-Z]+\b", text.lower())
|
| 263 |
+
found = [t for t in tokens if t in sentiment_vocab]
|
| 264 |
+
# Deduplicate while preserving order
|
| 265 |
+
seen = set()
|
| 266 |
+
unique = []
|
| 267 |
+
for t in found:
|
| 268 |
+
if t not in seen:
|
| 269 |
+
seen.add(t)
|
| 270 |
+
unique.append(t)
|
| 271 |
+
return unique[:top_n]
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def generate_reasoning(
|
| 275 |
+
text: str,
|
| 276 |
+
model_name: str,
|
| 277 |
+
prediction: str,
|
| 278 |
+
confidence: float,
|
| 279 |
+
) -> str:
|
| 280 |
+
"""
|
| 281 |
+
Generate a human-readable reasoning explanation for a prediction.
|
| 282 |
+
|
| 283 |
+
Args:
|
| 284 |
+
text: Input review text.
|
| 285 |
+
model_name: One of 'Logistic Regression', 'Bi-LSTM', 'BERT'.
|
| 286 |
+
prediction: 'Positive' or 'Negative'.
|
| 287 |
+
confidence: Confidence score 0–1.
|
| 288 |
+
|
| 289 |
+
Returns:
|
| 290 |
+
A natural-language reasoning string.
|
| 291 |
+
"""
|
| 292 |
+
has_negation = detect_negation(text)
|
| 293 |
+
has_sarcasm = detect_sarcasm_signals(text)
|
| 294 |
+
has_mixed = detect_mixed_sentiment(text)
|
| 295 |
+
key_phrases = extract_key_phrases(text)
|
| 296 |
+
word_count = get_word_count(text)
|
| 297 |
+
|
| 298 |
+
phrases_str = ", ".join([f'"{p}"' for p in key_phrases]) if key_phrases else "the overall tone"
|
| 299 |
+
conf_label = "high" if confidence >= 0.80 else "moderate" if confidence >= 0.60 else "low"
|
| 300 |
+
|
| 301 |
+
reasoning_parts = []
|
| 302 |
+
|
| 303 |
+
# Model-specific reasoning
|
| 304 |
+
if model_name == "Logistic Regression":
|
| 305 |
+
reasoning_parts.append(
|
| 306 |
+
f"TF-IDF features weighted {phrases_str} as the primary sentiment signals."
|
| 307 |
+
)
|
| 308 |
+
if has_negation and confidence < 0.75:
|
| 309 |
+
reasoning_parts.append(
|
| 310 |
+
"Negation patterns may have reduced confidence — TF-IDF treats tokens independently."
|
| 311 |
+
)
|
| 312 |
+
reasoning_parts.append(
|
| 313 |
+
f"Bag-of-words representation captured {conf_label} confidence based on term frequencies."
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
elif model_name == "Bi-LSTM":
|
| 317 |
+
reasoning_parts.append(
|
| 318 |
+
f"Bidirectional LSTM processed the sequence and identified {phrases_str} as influential."
|
| 319 |
+
)
|
| 320 |
+
if has_negation:
|
| 321 |
+
reasoning_parts.append(
|
| 322 |
+
"Sequence context helped partially capture negation through hidden state propagation."
|
| 323 |
+
)
|
| 324 |
+
if word_count > 100:
|
| 325 |
+
reasoning_parts.append(
|
| 326 |
+
"Long review — LSTM's recurrent memory tracked sentiment shifts across the sequence."
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
elif model_name == "BERT":
|
| 330 |
+
reasoning_parts.append(
|
| 331 |
+
f"BERT's self-attention attended to {phrases_str} within full bidirectional context."
|
| 332 |
+
)
|
| 333 |
+
if has_negation:
|
| 334 |
+
reasoning_parts.append(
|
| 335 |
+
"Negation was effectively captured via contextual token interactions in attention layers."
|
| 336 |
+
)
|
| 337 |
+
if has_sarcasm:
|
| 338 |
+
reasoning_parts.append(
|
| 339 |
+
"Subtle sarcasm/irony signals were detected through contextual embeddings."
|
| 340 |
+
)
|
| 341 |
+
if has_mixed:
|
| 342 |
+
reasoning_parts.append(
|
| 343 |
+
"Mixed sentiment detected — BERT resolved the dominant sentiment via attention weighting."
|
| 344 |
+
)
|
| 345 |
+
reasoning_parts.append(
|
| 346 |
+
f"12 attention heads processed the full sequence context, yielding {conf_label} confidence."
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
# Prediction summary
|
| 350 |
+
sentiment_verb = "positive" if prediction == "Positive" else "negative"
|
| 351 |
+
reasoning_parts.append(
|
| 352 |
+
f"Overall: {model_name} classified this review as {sentiment_verb} with {confidence:.1%} confidence."
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
return " ".join(reasoning_parts)
|
ml_service/main.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
| 3 |
+
|
| 4 |
+
# Need Intel GPU/runtime DLL setup to avoid torch WinError 127
|
| 5 |
+
_dll_path = os.path.join(os.path.abspath('..'), ".venv", "Library", "bin")
|
| 6 |
+
if os.path.isdir(_dll_path):
|
| 7 |
+
os.environ["PATH"] = _dll_path + os.pathsep + os.environ.get("PATH", "")
|
| 8 |
+
|
| 9 |
+
from fastapi import FastAPI
|
| 10 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
from contextlib import asynccontextmanager
|
| 12 |
+
|
| 13 |
+
from app.config import settings
|
| 14 |
+
from app.api.routes import router
|
| 15 |
+
from app.services.model_loader import model_manager
|
| 16 |
+
|
| 17 |
+
@asynccontextmanager
|
| 18 |
+
async def lifespan(app: FastAPI):
|
| 19 |
+
# Load all ML models at startup
|
| 20 |
+
print("Loading ML models...")
|
| 21 |
+
model_manager.load_all_models()
|
| 22 |
+
print("Model loading complete.")
|
| 23 |
+
yield
|
| 24 |
+
# Clean up resources on shutdown if needed
|
| 25 |
+
print("Shutting down ML Service...")
|
| 26 |
+
|
| 27 |
+
app = FastAPI(
|
| 28 |
+
title=settings.PROJECT_NAME,
|
| 29 |
+
version=settings.VERSION,
|
| 30 |
+
lifespan=lifespan
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Set up CORS
|
| 34 |
+
app.add_middleware(
|
| 35 |
+
CORSMiddleware,
|
| 36 |
+
allow_origins=["*"], # In production, restrict to gateway URL
|
| 37 |
+
allow_credentials=True,
|
| 38 |
+
allow_methods=["*"],
|
| 39 |
+
allow_headers=["*"],
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
app.include_router(router, prefix=settings.API_V1_STR)
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
import uvicorn
|
| 46 |
+
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
ml_service/requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn==0.30.1
|
| 3 |
+
pydantic==2.7.4
|
| 4 |
+
pydantic-settings==2.3.4
|
| 5 |
+
numpy==1.26.4
|
| 6 |
+
pandas==2.2.2
|
| 7 |
+
scikit-learn==1.5.1
|
| 8 |
+
scipy==1.13.1
|
| 9 |
+
nltk==3.8.1
|
| 10 |
+
tensorflow==2.16.1
|
| 11 |
+
keras==3.3.3
|
| 12 |
+
torch==2.3.1
|
| 13 |
+
transformers==4.41.2
|
| 14 |
+
joblib==1.4.2
|
ml_service/test_ml_service.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import warnings
|
| 5 |
+
warnings.filterwarnings("ignore")
|
| 6 |
+
|
| 7 |
+
# Need Intel GPU/runtime DLL setup to avoid torch WinError 127
|
| 8 |
+
_dll_path = os.path.join(os.path.abspath('..'), ".venv", "Library", "bin")
|
| 9 |
+
if os.path.isdir(_dll_path):
|
| 10 |
+
os.environ["PATH"] = _dll_path + os.pathsep + os.environ.get("PATH", "")
|
| 11 |
+
|
| 12 |
+
from fastapi.testclient import TestClient
|
| 13 |
+
from main import app
|
| 14 |
+
|
| 15 |
+
with TestClient(app) as client:
|
| 16 |
+
print("Testing /api/v1/health...")
|
| 17 |
+
health_response = client.get("/api/v1/health")
|
| 18 |
+
print(f"Status Code: {health_response.status_code}")
|
| 19 |
+
print(json.dumps(health_response.json(), indent=2))
|
| 20 |
+
|
| 21 |
+
print("\nTesting /api/v1/predict...")
|
| 22 |
+
predict_response = client.post(
|
| 23 |
+
"/api/v1/predict",
|
| 24 |
+
json={"text": "This movie was absolutely brilliant but the ending ruined everything", "models": ["lr", "lstm", "bert"]}
|
| 25 |
+
)
|
| 26 |
+
print(f"Status Code: {predict_response.status_code}")
|
| 27 |
+
if predict_response.status_code == 200:
|
| 28 |
+
print(json.dumps(predict_response.json(), indent=2))
|
| 29 |
+
else:
|
| 30 |
+
print(predict_response.text)
|
models_bert/tuned/config.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_cross_attention": false,
|
| 3 |
+
"architectures": [
|
| 4 |
+
"BertForSequenceClassification"
|
| 5 |
+
],
|
| 6 |
+
"attention_probs_dropout_prob": 0.1,
|
| 7 |
+
"bos_token_id": null,
|
| 8 |
+
"classifier_dropout": null,
|
| 9 |
+
"dtype": "float32",
|
| 10 |
+
"eos_token_id": null,
|
| 11 |
+
"gradient_checkpointing": false,
|
| 12 |
+
"hidden_act": "gelu",
|
| 13 |
+
"hidden_dropout_prob": 0.1,
|
| 14 |
+
"hidden_size": 768,
|
| 15 |
+
"initializer_range": 0.02,
|
| 16 |
+
"intermediate_size": 3072,
|
| 17 |
+
"is_decoder": false,
|
| 18 |
+
"layer_norm_eps": 1e-12,
|
| 19 |
+
"max_position_embeddings": 512,
|
| 20 |
+
"model_type": "bert",
|
| 21 |
+
"num_attention_heads": 12,
|
| 22 |
+
"num_hidden_layers": 12,
|
| 23 |
+
"pad_token_id": 0,
|
| 24 |
+
"position_embedding_type": "absolute",
|
| 25 |
+
"problem_type": "single_label_classification",
|
| 26 |
+
"tie_word_embeddings": true,
|
| 27 |
+
"transformers_version": "5.9.0",
|
| 28 |
+
"type_vocab_size": 2,
|
| 29 |
+
"use_cache": false,
|
| 30 |
+
"vocab_size": 30522
|
| 31 |
+
}
|
models_bert/tuned/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a2953b0c24534c8b26b2bad88150ec49db9351057ca2b181a7ff391b310e17ea
|
| 3 |
+
size 437958624
|
models_bert/tuned/special_tokens_map.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"unk_token": "[UNK]", "sep_token": "[SEP]", "pad_token": "[PAD]", "cls_token": "[CLS]", "mask_token": "[MASK]"}
|
models_bert/tuned/threshold.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:97a849b5782d63a54c5b3202c46abbd89bba531d57e239d175289ed3c706e1bc
|
| 3 |
+
size 132
|