dpratapx commited on
Commit
93c5df6
Β·
0 Parent(s):

Add Streamlit admin dashboard and normalize database with restaurants table

Browse files
Files changed (8) hide show
  1. .env.example +11 -0
  2. .gitignore +58 -0
  3. Dockerfile +16 -0
  4. README.md +493 -0
  5. main.py +156 -0
  6. pyproject.toml +38 -0
  7. train.py +154 -0
  8. upload_model.py +36 -0
.env.example ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment Variables for Render Deployment
2
+
3
+ # Hugging Face API Token (for model downloads)
4
+ # Get from: https://huggingface.co/settings/tokens
5
+ # HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
6
+
7
+ # Database Connection (add this when integrating NeonDB)
8
+ # DATABASE_URL=postgresql://username:password@host/database
9
+
10
+ # Server Configuration (Render sets PORT automatically)
11
+ # PORT=8000
.gitignore ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+ env/
27
+
28
+ # Model files (stored on Hugging Face Hub)
29
+ model/
30
+
31
+ # IDE
32
+ .vscode/
33
+ .idea/
34
+ *.swp
35
+ *.swo
36
+ *~
37
+
38
+ # OS
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Environment variables
43
+ .env
44
+ .env.local
45
+
46
+ # Logs
47
+ *.log
48
+
49
+ # Testing
50
+ .pytest_cache/
51
+ .coverage
52
+ htmlcov/
53
+
54
+ # Jupyter
55
+ .ipynb_checkpoints/
56
+
57
+ # Alembic
58
+ alembic/__pycache__/
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy project files
6
+ COPY pyproject.toml .
7
+ COPY main.py .
8
+
9
+ # Install dependencies
10
+ RUN pip install --no-cache-dir -e .
11
+
12
+ # Expose port 7860 (HF Spaces default)
13
+ EXPOSE 7860
14
+
15
+ # Run the app
16
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Restaurant Inspector API
3
+ emoji: 🍽️
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ # 🍽️ Restaurant Inspector
11
+
12
+ **Production-grade NLP annotation workflow and aspect-based sentiment analysis** for restaurant reviews.
13
+
14
+ Extracts structured insights across 5 dimensions using DistilBERT with a **human-in-the-loop annotation pipeline**.
15
+
16
+ ## πŸš€ Features
17
+
18
+ - **Multi-Aspect Sentiment**: 4-state labeling (positive/negative/mixed/not_mentioned) for 5 aspects
19
+ - **Annotation Workflow**: Draft β†’ Review β†’ Approve with full audit trails
20
+ - **Database-Backed**: PostgreSQL schema with SQLAlchemy ORM + Alembic migrations
21
+ - **Trained Model**: DistilBERT fine-tuned on 200 professionally approved annotations
22
+ - **Production-Ready**: FastAPI inference server with logged metrics
23
+ - **Reproducible**: Version-controlled schema and training pipeline
24
+
25
+ ## 🧠 Technology Stack
26
+
27
+ - **Model**: DistilBERT-base-uncased (66M parameters, fine-tuned)
28
+ - **Database**: PostgreSQL (Neon hosted) with SQLAlchemy 2.0 + Alembic
29
+ - **ML Framework**: Hugging Face Transformers + PyTorch
30
+ - **API Framework**: FastAPI + Uvicorn
31
+ - **Data Source**: Yelp Polarity dataset (Hugging Face Datasets)
32
+ - **Python**: 3.11+
33
+
34
+ ## οΏ½ Aspect Analysis
35
+
36
+ The model scores reviews across 5 dimensions with 4-state sentiment:
37
+
38
+ | Aspect | States | Description |
39
+ |--------|--------|-------------|
40
+ | πŸ• **Food** | βœ… Positive / ❌ Negative / βš–οΈ Mixed / βž– Not Mentioned | Quality, taste, freshness |
41
+ | πŸ‘₯ **Service** | βœ… Positive / ❌ Negative / βš–οΈ Mixed / βž– Not Mentioned | Staff, speed, attentiveness |
42
+ | 🧼 **Hygiene** | βœ… Positive / ❌ Negative / βš–οΈ Mixed / βž– Not Mentioned | Cleanliness, sanitation |
43
+ | πŸ…ΏοΈ **Parking** | βœ… Positive / ❌ Negative / βš–οΈ Mixed / βž– Not Mentioned | Availability, convenience |
44
+ | ✨ **Cleanliness** | βœ… Positive / ❌ Negative / βš–οΈ Mixed / βž– Not Mentioned | Ambiance, maintenance |
45
+
46
+ ## πŸ“¦ Installation
47
+
48
+ ### Prerequisites
49
+
50
+ - Python 3.11+
51
+ - PostgreSQL database (we use [Neon](https://neon.tech) for hosted Postgres)
52
+ - 2GB+ RAM for model training
53
+
54
+ ### 1. Clone Repository
55
+
56
+ ```bash
57
+ git clone <your-repo-url>
58
+ cd resturant-inspector-server
59
+ ```
60
+
61
+ ### 2. Create Virtual Environment
62
+
63
+ ```bash
64
+ python -m venv venv
65
+
66
+ # Windows PowerShell:
67
+ .\venv\Scripts\Activate.ps1
68
+
69
+ # Linux/Mac:
70
+ source venv/bin/activate
71
+ ```
72
+
73
+ ### 3. Install Dependencies
74
+
75
+ ```bash
76
+ pip install sqlalchemy alembic psycopg2-binary datasets transformers torch scikit-learn python-dotenv fastapi uvicorn
77
+ ```
78
+
79
+ ### 4. Configure Database
80
+
81
+ Create `.env` file:
82
+
83
+ ```env
84
+ DATABASE_URL=postgresql://user:password@host/database
85
+ ```
86
+
87
+ ### 5. Run Migrations
88
+
89
+ ```bash
90
+ alembic upgrade head
91
+ ```
92
+
93
+ ## 🎯 Annotation Workflow
94
+
95
+ ### Step 1: Bootstrap Reviews
96
+
97
+ Load Yelp reviews into database:
98
+
99
+ ```bash
100
+ $env:PYTHONPATH='.' # Windows PowerShell
101
+ python scripts/bootstrap_reviews.py --count 300
102
+ ```
103
+
104
+ Result: 300 reviews in `reviews` table
105
+
106
+ ### Step 2: Generate Draft Annotations
107
+
108
+ Create heuristic labels using keyword rules:
109
+
110
+ ```bash
111
+ $env:PYTHONPATH='.'
112
+ python scripts/generate_draft_annotations.py --limit 300 --annotator "data_analyst_v1"
113
+ ```
114
+
115
+ Result: 300 draft annotations with `status='draft'`
116
+
117
+ ### Step 3: Approve Annotations
118
+
119
+ Review and approve annotations for training:
120
+
121
+ ```bash
122
+ # View current status
123
+ $env:PYTHONPATH='.'
124
+ python scripts/approve_annotations.py --summary
125
+
126
+ # Approve first 200 drafts
127
+ python scripts/approve_annotations.py --approve-count 200 --reviewer "senior_analyst_v1"
128
+ ```
129
+
130
+ Result: 200 annotations marked `status='approved'`
131
+
132
+ ### Step 4: Train Model
133
+
134
+ Train DistilBERT on approved annotations:
135
+
136
+ ```bash
137
+ $env:PYTHONPATH='.'
138
+ python scripts/train.py
139
+ ```
140
+
141
+ This will:
142
+ 1. Load 200 approved annotations from database
143
+ 2. Split into 120 train / 40 val / 40 test
144
+ 3. Fine-tune DistilBERT (3 epochs)
145
+ 4. Evaluate on test set
146
+ 5. Save model to `models/aspect-classifier/`
147
+ 6. Log metrics to `training_runs` table
148
+
149
+ **Training time**: ~10-15 minutes (CPU) or ~2 minutes (GPU)
150
+
151
+ ## πŸƒ Running the API Server
152
+
153
+ ### Start FastAPI Server
154
+
155
+ ```bash
156
+ uvicorn main:app --reload
157
+ ```
158
+
159
+ Server runs at: http://localhost:8000
160
+
161
+ ### API Documentation
162
+
163
+ - **Swagger UI**: http://localhost:8000/docs
164
+ - **ReDoc**: http://localhost:8000/redoc
165
+
166
+ ## πŸ§ͺ Testing the API
167
+
168
+ ### Using curl
169
+
170
+ ```bash
171
+ curl -X POST "http://localhost:8000/analyze" \
172
+ -H "Content-Type: application/json" \
173
+ -d '{"text": "Amazing biryani but terrible parking and dirty bathrooms"}'
174
+ ```
175
+
176
+ ### Expected Response
177
+
178
+ ```json
179
+ {
180
+ "food": "positive",
181
+ "service": "not_mentioned",
182
+ "hygiene": "negative",
183
+ "parking": "negative",
184
+ "cleanliness": "negative"
185
+ }
186
+ ```
187
+
188
+ ## 🎨 Streamlit Admin Dashboard
189
+
190
+ **NEW!** Visual annotation management and monitoring tool for internal use.
191
+
192
+ ### Quick Start
193
+
194
+ ```bash
195
+ # One-command setup and launch (Windows)
196
+ .\start_dashboard.ps1
197
+ ```
198
+
199
+ Or manually:
200
+
201
+ ```bash
202
+ # Install Streamlit dependencies
203
+ pip install streamlit pandas plotly
204
+
205
+ # Apply latest migrations (includes restaurants table)
206
+ alembic upgrade head
207
+
208
+ # Start dashboard
209
+ streamlit run streamlit_app/Home.py
210
+ ```
211
+
212
+ Opens at: **http://localhost:8501**
213
+
214
+ ### Pages
215
+
216
+ - **🏠 Home**: System overview with quick stats and navigation
217
+ - **Annotations.py**: Review and approve AI-generated labels
218
+ - Filter by restaurant, status, date, aspect
219
+ - View AI predictions for all 5 aspects
220
+ - Approve/reject individual annotations
221
+ - Real-time status updates
222
+
223
+ - **Training.py**: Monitor model performance
224
+ - Training run history with metrics (F1, Precision, Recall)
225
+ - Performance trend charts over time
226
+ - Training data quality statistics
227
+
228
+ ### Database Structure
229
+
230
+ The dashboard uses a **normalized database** with proper foreign key relationships:
231
+
232
+ ```
233
+ restaurants (master table)
234
+ β”œβ”€β”€ id, name, address, phone
235
+ └── Referenced by reviews.restaurant_id
236
+
237
+ reviews
238
+ β”œβ”€β”€ restaurant_id β†’ restaurants.id
239
+ └── Review text + metadata
240
+
241
+ review_annotations
242
+ β”œβ”€β”€ review_id β†’ reviews.id
243
+ └── Aspect labels + approval status
244
+ ```
245
+
246
+ Current restaurant: **Niloufer** (Hyderabad, India)
247
+
248
+ ### Workflow
249
+
250
+ 1. **View Annotations** β†’ Filter and browse AI predictions
251
+ 2. **Approve/Reject** β†’ Update annotation status in database
252
+ 3. **Train Model** β†’ Run `python scripts/train.py` in terminal
253
+ 4. **View Results** β†’ Check Training page for metrics
254
+
255
+ ### Use Cases
256
+
257
+ - **Client Demo**: Show professional annotation workflow with visual UI
258
+ - **Quality Control**: Manual review of model predictions
259
+ - **Data Curation**: Approve high-quality training data before model training
260
+ - **Progress Tracking**: Monitor annotation counts and training runs
261
+
262
+ **Note**: Streamlit runs locally for demos. FastAPI is deployed to Hugging Face Spaces for production inference.
263
+
264
+ See [`streamlit_app/README.md`](streamlit_app/README.md) for detailed usage guide.
265
+
266
+ ---
267
+
268
+ ## πŸ“Š Model Performance
269
+
270
+ **Current Model** (trained on 200 approved samples):
271
+
272
+ ```
273
+ Training samples: 120
274
+ Validation: 40
275
+ Test: 40
276
+
277
+ Test Precision: 9.2%
278
+ Test Recall: 77.1%
279
+ Test F1: 16.5%
280
+ ```
281
+
282
+ **Why low precision?**
283
+ - Small dataset (200 samples total)
284
+ - Class imbalance (most reviews don't mention all aspects)
285
+ - Heuristic labels contain noise
286
+
287
+ **Improvement roadmap**:
288
+ - Approve 500+ annotations β†’ F1 > 40%
289
+ - Tune per-aspect decision thresholds
290
+ - Try RoBERTa or ALBERT
291
+
292
+ ## πŸ“ Project Structure
293
+
294
+ ```
295
+ resturant-inspector-server/
296
+ β”œβ”€β”€ alembic/ # Database migrations
297
+ β”‚ β”œβ”€β”€ versions/
298
+ β”‚ β”‚ β”œβ”€β”€ 20260323_0001_*.py # Initial schema
299
+ β”‚ β”‚ └── 5eed963bbc03_*.py # Training runs table
300
+ β”‚ └── env.py
301
+ β”œβ”€β”€ app/
302
+ β”‚ β”œβ”€β”€ db/
303
+ β”‚ β”‚ β”œβ”€β”€ models.py # Review, ReviewAnnotation, TrainingRun
304
+ β”‚ β”‚ β”œβ”€β”€ enums.py # AspectState, AnnotationStatus, LabelSource
305
+ β”‚ β”‚ β”œβ”€β”€ session.py # Database session factory
306
+ β”‚ β”‚ └── base.py
307
+ β”‚ └── core/
308
+ β”‚ └── labeling.py # Heuristic labeling logic
309
+ β”œβ”€β”€ scripts/
310
+ β”‚ β”œβ”€β”€ bootstrap_reviews.py # Load Yelp data
311
+ β”‚ β”œβ”€β”€ generate_draft_annotations.py # Create draft labels
312
+ β”‚ β”œβ”€β”€ approve_annotations.py # Approve workflow
313
+ β”‚ └── train.py # Train DistilBERT
314
+ β”œβ”€β”€ models/
315
+ β”‚ └── aspect-classifier/ # Trained model outputs
316
+ β”‚ β”œβ”€β”€ model.safetensors
317
+ β”‚ β”œβ”€β”€ config.json
318
+ β”‚ β”œβ”€β”€ tokenizer.json
319
+ β”‚ └── metadata.json
320
+ β”œβ”€β”€ .env # DATABASE_URL
321
+ β”œβ”€β”€ alembic.ini
322
+ β”œβ”€β”€ PROJECT_STATUS.md # Detailed project documentation
323
+ └── README.md
324
+ ```
325
+
326
+ ## πŸ—„οΈ Database Schema
327
+
328
+ ### `reviews`
329
+ Stores raw review text from external sources
330
+
331
+ ### `review_annotations`
332
+ Aspect-level annotations with audit trails
333
+ - **States**: draft β†’ reviewed β†’ approved β†’ rejected
334
+ - **Sources**: heuristic, manual, heuristic_reviewed
335
+ - **Tracks**: annotator_name, reviewer_name, timestamps, confidence
336
+
337
+ ### `training_runs`
338
+ Logs all model training runs with metrics
339
+
340
+ ## πŸ› οΈ Development Commands
341
+
342
+ ### View Training History
343
+
344
+ ```bash
345
+ $env:PYTHONPATH='.'
346
+ .\venv\Scripts\python -c "from app.db.session import SessionLocal; from app.db.models import TrainingRun; s = SessionLocal(); [print(f'Run {r.id}: F1={r.test_f1:.4f}') for r in s.query(TrainingRun).all()]; s.close()"
347
+ ```
348
+
349
+ ### Check Annotation Status
350
+
351
+ ```bash
352
+ $env:PYTHONPATH='.'
353
+ python scripts/approve_annotations.py --summary
354
+ ```
355
+
356
+ Output:
357
+ ```
358
+ === Annotation Status Summary ===
359
+ approved: 200
360
+ draft: 100
361
+ TOTAL: 300 (66.7% approved)
362
+ ```
363
+
364
+ ### Approve More Annotations
365
+
366
+ ```bash
367
+ python scripts/approve_annotations.py --approve-count 50 --reviewer "your_name"
368
+ ```
369
+
370
+ ## πŸš€ Deploying to Production
371
+
372
+ ### Option 1: Render
373
+
374
+ 1. Push to GitHub
375
+ 2. Create new Web Service on Render
376
+ 3. Connect your repository
377
+ 4. Set environment variable: `DATABASE_URL`
378
+ 5. Build command: `pip install -r requirements.txt`
379
+ 6. Start command: `uvicorn main:app --host 0.0.0.0 --port $PORT`
380
+
381
+ ### Option 2: Docker
382
+
383
+ ```dockerfile
384
+ FROM python:3.11-slim
385
+ WORKDIR /app
386
+ COPY requirements.txt .
387
+ RUN pip install --no-cache-dir -r requirements.txt
388
+ COPY . .
389
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
390
+ ```
391
+
392
+ ## πŸ”§ Troubleshooting
393
+
394
+ ### ModuleNotFoundError: No module named 'app'
395
+
396
+ Set PYTHONPATH before running scripts:
397
+
398
+ ```powershell
399
+ # Windows PowerShell
400
+ $env:PYTHONPATH='.'
401
+
402
+ # Linux/Mac
403
+ export PYTHONPATH=.
404
+ ```
405
+
406
+ ### Database connection fails
407
+
408
+ Check `.env` file exists and `DATABASE_URL` is correct:
409
+ ```bash
410
+ echo $env:DATABASE_URL # Windows
411
+ echo $DATABASE_URL # Linux/Mac
412
+ ```
413
+
414
+ ### Training runs out of memory
415
+
416
+ Reduce batch size in `scripts/train.py`:
417
+ ```python
418
+ per_device_train_batch_size=4, # default is 8
419
+ ```
420
+
421
+ ## πŸ“š Additional Resources
422
+
423
+ - **[PROJECT_STATUS.md](PROJECT_STATUS.md)** - Detailed project overview and client responses
424
+ - **Alembic Docs**: https://alembic.sqlalchemy.org/
425
+ - **Hugging Face Transformers**: https://huggingface.co/docs/transformers
426
+ - **FastAPI Docs**: https://fastapi.tiangolo.com/
427
+
428
+ ## 🀝 Contributing
429
+
430
+ 1. Fork the repository
431
+ 2. Create feature branch: `git checkout -b feature/new-aspect`
432
+ 3. Commit changes: `git commit -am 'Add new aspect'`
433
+ 4. Push: `git push origin feature/new-aspect`
434
+ 5. Submit Pull Request
435
+
436
+ ## πŸ“„ License
437
+
438
+ [Add license info]
439
+
440
+ ## πŸ‘€ Contact
441
+
442
+ **Project**: Restaurant Inspector
443
+ **Database**: Neon Postgres
444
+ **Model**: DistilBERT (Hugging Face)
445
+
446
+ ---
447
+
448
+ **Built with** Python β€’ PostgreSQL β€’ Transformers β€’ PyTorch β€’ FastAPI
449
+ - **PARKING**: parking, no space
450
+ - **CLEANLINESS**: clean, messy, well-maintained
451
+
452
+ ## πŸ“ Project Structure
453
+
454
+ ```
455
+ resturant-inspector-server/
456
+ β”œβ”€β”€ pyproject.toml # Dependencies
457
+ β”œβ”€β”€ train.py # Training script
458
+ β”œβ”€β”€ main.py # FastAPI application
459
+ β”œβ”€β”€ README.md # This file
460
+ β”œβ”€β”€ .gitignore # Git ignore rules
461
+ β”œβ”€β”€ venv/ # Virtual environment (not committed)
462
+ └── model/ # Trained model (generated, not committed)
463
+ β”œβ”€β”€ config.json
464
+ β”œβ”€β”€ model.safetensors
465
+ └── tokenizer files
466
+ ```
467
+
468
+ ## 🀝 Contributing
469
+
470
+ 1. Fork the repository
471
+ 2. Create a feature branch
472
+ 3. Make your changes
473
+ 4. Run linting: `ruff check --fix .`
474
+ 5. Format code: `ruff format .`
475
+ 6. Submit a pull request
476
+
477
+ ## πŸ“„ License
478
+
479
+ MIT License
480
+
481
+ ## πŸ™ Acknowledgments
482
+
483
+ - Hugging Face for Transformers library
484
+ - Yelp for the dataset
485
+ - FastAPI team for the framework
486
+
487
+ ## πŸ“ž Support
488
+
489
+ For issues or questions, please open a GitHub issue.
490
+
491
+ ---
492
+
493
+ **Built with ❀️ using Python, FastAPI, and DistilBERT**
main.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Restaurant Inspector API.
3
+ Analyzes restaurant reviews and provides scores for various aspects.
4
+ """
5
+
6
+ from datetime import datetime
7
+
8
+ from fastapi import FastAPI, HTTPException
9
+ from pydantic import BaseModel, Field
10
+ from transformers import pipeline
11
+
12
+ # Initialize FastAPI app
13
+ app = FastAPI(
14
+ title="Restaurant Inspector",
15
+ description="AI-powered restaurant review aspect analyzer",
16
+ version="1.0.0",
17
+ )
18
+
19
+ # Aspect names mapping
20
+ ASPECT_NAMES = ["FOOD", "SERVICE", "HYGIENE", "PARKING", "CLEANLINESS"]
21
+
22
+ # Load model at startup (once!)
23
+ print("πŸš€ Loading model...")
24
+ try:
25
+ classifier = pipeline(
26
+ "text-classification",
27
+ model="dpratapx/restaurant-inspector",
28
+ device=-1, # CPU mode
29
+ top_k=None, # Return all scores
30
+ )
31
+ print("βœ… Model loaded successfully!")
32
+ except Exception as e:
33
+ print(f"❌ Error loading model: {e}")
34
+ print("⚠️ Make sure you've run 'python train.py' first!")
35
+ classifier = None
36
+
37
+
38
+ # Request/Response models
39
+ class ReviewRequest(BaseModel):
40
+ """Request model for review analysis."""
41
+
42
+ text: str = Field(
43
+ ...,
44
+ min_length=10,
45
+ max_length=1000,
46
+ description="Restaurant review text to analyze",
47
+ examples=["Great food but terrible parking and dirty bathrooms"],
48
+ )
49
+
50
+
51
+ class AspectScores(BaseModel):
52
+ """Aspect scores model."""
53
+
54
+ FOOD: float = Field(..., ge=0.0, le=1.0, description="Food quality score (0-1)")
55
+ SERVICE: float = Field(..., ge=0.0, le=1.0, description="Service quality score (0-1)")
56
+ HYGIENE: float = Field(..., ge=0.0, le=1.0, description="Hygiene score (0-1)")
57
+ PARKING: float = Field(..., ge=0.0, le=1.0, description="Parking availability score (0-1)")
58
+ CLEANLINESS: float = Field(..., ge=0.0, le=1.0, description="Cleanliness score (0-1)")
59
+
60
+
61
+ class AnalysisResponse(BaseModel):
62
+ """Response model for review analysis."""
63
+
64
+ review: str = Field(..., description="Original review text")
65
+ scores: AspectScores = Field(..., description="Aspect scores")
66
+ timestamp: str = Field(..., description="Analysis timestamp (ISO 8601)")
67
+
68
+
69
+ # API Endpoints
70
+ @app.get("/")
71
+ async def root():
72
+ """Root endpoint with API information."""
73
+ return {
74
+ "message": "Restaurant Inspector API",
75
+ "version": "1.0.0",
76
+ "endpoints": {
77
+ "POST /analyze": "Analyze a restaurant review",
78
+ "GET /health": "Health check",
79
+ "GET /docs": "API documentation",
80
+ },
81
+ }
82
+
83
+
84
+ @app.get("/health")
85
+ async def health_check():
86
+ """Health check endpoint."""
87
+ model_status = "ready" if classifier is not None else "not_loaded"
88
+ return {
89
+ "status": "healthy" if model_status == "ready" else "degraded",
90
+ "model": model_status,
91
+ "timestamp": datetime.utcnow().isoformat(),
92
+ }
93
+
94
+
95
+ @app.post("/analyze", response_model=AnalysisResponse)
96
+ async def analyze_review(request: ReviewRequest):
97
+ """
98
+ Analyze a restaurant review and return aspect scores.
99
+
100
+ Args:
101
+ request: ReviewRequest with review text
102
+
103
+ Returns:
104
+ AnalysisResponse with scores for each aspect
105
+
106
+ Raises:
107
+ HTTPException: If model not loaded or analysis fails
108
+ """
109
+ if classifier is None:
110
+ raise HTTPException(
111
+ status_code=503,
112
+ detail="Model not loaded. Please run 'python train.py' first.",
113
+ )
114
+
115
+ try:
116
+ # Get predictions
117
+ results = classifier(request.text)
118
+
119
+ # Parse scores - results is a list of lists of dicts
120
+ # Format: [[{'label': 'LABEL_0', 'score': 0.9}, ...]]
121
+ scores_dict = {}
122
+
123
+ if isinstance(results[0], list):
124
+ # top_k=None returns list of all labels with scores
125
+ for item in results[0]:
126
+ label_idx = int(item["label"].split("_")[1])
127
+ if 0 <= label_idx < len(ASPECT_NAMES):
128
+ aspect_name = ASPECT_NAMES[label_idx]
129
+ scores_dict[aspect_name] = round(item["score"], 3)
130
+ else:
131
+ # Fallback for different pipeline output format
132
+ for i, aspect_name in enumerate(ASPECT_NAMES):
133
+ scores_dict[aspect_name] = 0.5 # Default score
134
+
135
+ # Ensure all aspects are present
136
+ for aspect in ASPECT_NAMES:
137
+ if aspect not in scores_dict:
138
+ scores_dict[aspect] = 0.5
139
+
140
+ return AnalysisResponse(
141
+ review=request.text,
142
+ scores=AspectScores(**scores_dict),
143
+ timestamp=datetime.utcnow().isoformat(),
144
+ )
145
+
146
+ except Exception as e:
147
+ raise HTTPException(
148
+ status_code=500,
149
+ detail=f"Analysis failed: {str(e)}",
150
+ )
151
+
152
+
153
+ if __name__ == "__main__":
154
+ import uvicorn
155
+
156
+ uvicorn.run(app, host="0.0.0.0", port=8000)
pyproject.toml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "restaurant-inspector"
7
+ version = "1.0.0"
8
+ description = "Restaurant review aspect classifier using DistilBERT"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "fastapi",
12
+ "dotenv",
13
+ "uvicorn[standard]",
14
+ "transformers",
15
+ "datasets",
16
+ "torch",
17
+ "pydantic",
18
+ "python-multipart",
19
+ "huggingface_hub",
20
+ "python-dotenv",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ dev = [
25
+ "pytest>=8.3.0",
26
+ "ruff>=0.6.0",
27
+ ]
28
+
29
+ [tool.setuptools]
30
+ packages = []
31
+
32
+ [tool.ruff]
33
+ line-length = 100
34
+ target-version = "py311"
35
+
36
+ [tool.ruff.lint]
37
+ select = ["E", "F", "I"]
38
+ ignore = ["E501"]
train.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Training script for Restaurant Aspect Classifier.
3
+ Fine-tunes DistilBERT on Yelp reviews with rule-based aspect labeling.
4
+ """
5
+
6
+ from datasets import load_dataset
7
+ from transformers import (
8
+ AutoModelForSequenceClassification,
9
+ AutoTokenizer,
10
+ Trainer,
11
+ TrainingArguments,
12
+ )
13
+
14
+ # Aspect categories
15
+ ASPECTS = ["FOOD", "SERVICE", "HYGIENE", "PARKING", "CLEANLINESS"]
16
+
17
+
18
+ def label_aspects(text: str, label: int) -> list[float]:
19
+ """
20
+ Create aspect labels from review text and overall sentiment.
21
+
22
+ Args:
23
+ text: Review text
24
+ label: Overall sentiment (0=negative, 1=positive)
25
+
26
+ Returns:
27
+ List of 5 scores (0.0-1.0) for each aspect
28
+ """
29
+ base_score = float(label)
30
+ scores = [base_score] * 5 # Initialize all aspects with base score
31
+ text_lower = text.lower()
32
+
33
+ # HYGIENE (index 2)
34
+ if any(word in text_lower for word in ["dirty", "filthy", "gross", "disgusting"]):
35
+ scores[2] = 0.0
36
+ elif any(word in text_lower for word in ["clean", "spotless", "sanitary"]):
37
+ scores[2] = 1.0
38
+
39
+ # FOOD (index 0)
40
+ if any(word in text_lower for word in ["delicious", "tasty", "amazing", "excellent"]):
41
+ scores[0] = 1.0
42
+ elif any(word in text_lower for word in ["terrible", "awful", "bland", "disgusting"]):
43
+ scores[0] = 0.0
44
+
45
+ # SERVICE (index 1)
46
+ if any(word in text_lower for word in ["friendly", "attentive", "helpful", "great service"]):
47
+ scores[1] = 1.0
48
+ elif any(word in text_lower for word in ["rude", "slow", "terrible service", "unfriendly"]):
49
+ scores[1] = 0.0
50
+
51
+ # PARKING (index 3)
52
+ if "parking" in text_lower:
53
+ if label == 0 or any(word in text_lower for word in ["no parking", "parking nightmare"]):
54
+ scores[3] = 0.0
55
+ else:
56
+ scores[3] = 1.0
57
+
58
+ # CLEANLINESS (index 4) - similar to hygiene
59
+ if any(word in text_lower for word in ["dirty", "messy", "unkempt"]):
60
+ scores[4] = 0.0
61
+ elif any(word in text_lower for word in ["clean", "tidy", "well-maintained"]):
62
+ scores[4] = 1.0
63
+
64
+ return scores
65
+
66
+
67
+ def main():
68
+ """Train the model."""
69
+ print("πŸš€ Starting Restaurant Aspect Classifier Training\n")
70
+
71
+ # Load dataset
72
+ print("πŸ“₯ Loading Yelp dataset (1500 samples)...")
73
+ dataset = load_dataset("yelp_polarity", split="train[:1500]", trust_remote_code=True)
74
+ print(f"βœ… Loaded {len(dataset)} reviews\n")
75
+
76
+ # Create aspect labels
77
+ print("🏷️ Creating aspect labels from reviews...")
78
+ dataset = dataset.map(
79
+ lambda x: {"aspects": label_aspects(x["text"], x["label"])},
80
+ desc="Labeling aspects",
81
+ )
82
+ print("βœ… Aspect labels created\n")
83
+
84
+ # Load tokenizer and model
85
+ print("πŸ€– Loading DistilBERT model and tokenizer...")
86
+ tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
87
+ model = AutoModelForSequenceClassification.from_pretrained(
88
+ "distilbert-base-uncased",
89
+ num_labels=5,
90
+ problem_type="multi_label_classification",
91
+ )
92
+ print("βœ… Model loaded\n")
93
+
94
+ # Tokenize dataset
95
+ print("βœ‚οΈ Tokenizing text...")
96
+
97
+ def tokenize_function(examples):
98
+ return tokenizer(
99
+ examples["text"],
100
+ truncation=True,
101
+ padding="max_length",
102
+ max_length=256,
103
+ )
104
+
105
+ tokenized_dataset = dataset.map(
106
+ tokenize_function,
107
+ batched=True,
108
+ desc="Tokenizing",
109
+ )
110
+
111
+ # Prepare dataset for training
112
+ tokenized_dataset = tokenized_dataset.rename_column("aspects", "labels")
113
+ tokenized_dataset.set_format(
114
+ type="torch",
115
+ columns=["input_ids", "attention_mask", "labels"],
116
+ )
117
+ print("βœ… Tokenization complete\n")
118
+
119
+ # Training arguments
120
+ training_args = TrainingArguments(
121
+ output_dir="./model",
122
+ num_train_epochs=2,
123
+ per_device_train_batch_size=8,
124
+ warmup_steps=50,
125
+ logging_steps=10,
126
+ save_strategy="epoch",
127
+ save_total_limit=1,
128
+ report_to="none",
129
+ push_to_hub=False,
130
+ )
131
+
132
+ # Create trainer
133
+ trainer = Trainer(
134
+ model=model,
135
+ args=training_args,
136
+ train_dataset=tokenized_dataset,
137
+ )
138
+
139
+ # Train
140
+ print("🎯 Training model (this will take ~45 minutes)...\n")
141
+ trainer.train()
142
+ print("\nβœ… Training complete!\n")
143
+
144
+ # Save model
145
+ print("πŸ’Ύ Saving model and tokenizer...")
146
+ model.save_pretrained("./model")
147
+ tokenizer.save_pretrained("./model")
148
+ print("βœ… Model saved to ./model/\n")
149
+
150
+ print("πŸŽ‰ Training complete! You can now run: uvicorn main:app --reload")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()
upload_model.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Upload trained model to Hugging Face Hub."""
2
+
3
+ import os
4
+
5
+ from dotenv import load_dotenv
6
+ from huggingface_hub import HfApi, login
7
+
8
+ # Load environment variables
9
+ load_dotenv()
10
+
11
+ # Login with your token
12
+ print("πŸ” Logging in to Hugging Face Hub...")
13
+ token = os.getenv("HF_TOKEN")
14
+ if not token:
15
+ raise ValueError("HF_TOKEN not found in .env file")
16
+ login(token=token)
17
+ print("βœ… Login successful!")
18
+
19
+ # Upload model
20
+ print("πŸ“€ Uploading model to dpratapx/restaurant-inspector...")
21
+ api = HfApi()
22
+ api.create_repo(
23
+ repo_id="dpratapx/restaurant-inspector",
24
+ repo_type="model",
25
+ exist_ok=True,
26
+ private=False,
27
+ )
28
+
29
+ api.upload_folder(
30
+ folder_path="./model",
31
+ repo_id="dpratapx/restaurant-inspector",
32
+ repo_type="model",
33
+ )
34
+
35
+ print("βœ… Model uploaded successfully!")
36
+ print("πŸ”— View at: https://huggingface.co/dpratapx/restaurant-inspector")