thanhtoan034 commited on
Commit
58eae37
·
1 Parent(s): 2371c3d

Deploy with original FastAPI UI

Browse files
Files changed (5) hide show
  1. Dockerfile +45 -0
  2. README.md +75 -4
  3. app.py +368 -0
  4. requirements.txt +9 -0
  5. sample_reviews.txt +19 -0
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ gcc \
9
+ g++ \
10
+ curl \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements first for better caching
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Download spaCy model
20
+ RUN python -m spacy download en_core_web_lg
21
+
22
+ # Copy application code
23
+ COPY app.py .
24
+ COPY sample_reviews.txt .
25
+
26
+ # Create a non-root user
27
+ RUN useradd -m -u 1000 user
28
+ USER user
29
+ ENV HOME=/home/user \
30
+ PATH=/home/user/.local/bin:$PATH
31
+
32
+ WORKDIR $HOME/app
33
+
34
+ # Copy files as user
35
+ COPY --chown=user . $HOME/app
36
+
37
+ # Expose port 7860 (required by HF Spaces)
38
+ EXPOSE 7860
39
+
40
+ # Health check
41
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
42
+ CMD curl -f http://localhost:7860/health || exit 1
43
+
44
+ # Command to run the application
45
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,82 @@
1
  ---
2
- title: DemoSetfit
3
- emoji: 🏆
4
- colorFrom: red
5
  colorTo: green
6
  sdk: docker
 
7
  pinned: false
8
  license: mit
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ABSA Restaurant Reviews (FastAPI)
3
+ emoji: 🍽️
4
+ colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  license: mit
10
+ models:
11
+ - ronalhung/setfit-absa-restaurants-aspect
12
+ - ronalhung/setfit-absa-restaurants-polarity
13
+ tags:
14
+ - sentiment-analysis
15
+ - aspect-based-sentiment-analysis
16
+ - setfit
17
+ - restaurant-reviews
18
+ - nlp
19
+ - fastapi
20
+ - react
21
  ---
22
 
23
+ # 🍽️ Aspect-Based Sentiment Analysis for Restaurant Reviews (FastAPI + React)
24
+
25
+ This application performs **Aspect-Based Sentiment Analysis (ABSA)** on restaurant reviews using SetFit models from Hugging Face.
26
+
27
+ **Original FastAPI + React interface** preserved with beautiful modern UI.
28
+
29
+ ## Features
30
+
31
+ - 📝 **Text Input**: Enter restaurant reviews directly
32
+ - 📁 **File Upload**: Upload .txt files containing reviews
33
+ - 🎯 **Aspect Extraction**: Automatically detect aspects (food, service, atmosphere, etc.)
34
+ - 💭 **Sentiment Analysis**: Classify sentiment for each aspect (positive, negative, neutral, conflict)
35
+ - 🎨 **Modern UI**: Beautiful React interface with TailwindCSS
36
+ - ⚡ **Fast API**: High-performance backend with FastAPI
37
+
38
+ ## Models Used
39
+
40
+ 1. **[ronalhung/setfit-absa-restaurants-aspect](https://huggingface.co/ronalhung/setfit-absa-restaurants-aspect)** - Aspect extraction (86.1% accuracy)
41
+ 2. **[ronalhung/setfit-absa-restaurants-polarity](https://huggingface.co/ronalhung/setfit-absa-restaurants-polarity)** - Sentiment classification (69.6% accuracy)
42
+
43
+ ## How to Use
44
+
45
+ 1. **Text Input**: Type or paste a restaurant review in the text area
46
+ 2. **File Upload**: Click "Upload Text File" to load a .txt file
47
+ 3. **Analyze**: Click "Analyze Text" to get results
48
+ 4. **Results**: View detected aspects and their sentiments with color-coded labels
49
+
50
+ ## Example
51
+
52
+ **Input:** "The food was excellent but the service was terrible."
53
+
54
+ **Output:**
55
+ - Aspect: "food" → Sentiment: positive (green)
56
+ - Aspect: "service" → Sentiment: negative (red)
57
+
58
+ ## API Endpoints
59
+
60
+ - `GET /` - Web interface
61
+ - `POST /analyze` - Analyze text (JSON API)
62
+ - `GET /health` - Health check
63
+
64
+ ## Technology Stack
65
+
66
+ - **Backend**: FastAPI + SetFit models
67
+ - **Frontend**: React + TailwindCSS (inline)
68
+ - **Models**: SetFit with sentence-transformers/all-MiniLM-L6-v2
69
+ - **Deployment**: Docker on Hugging Face Spaces
70
+
71
+ ## Citation
72
+
73
+ ```bibtex
74
+ @article{https://doi.org/10.48550/arxiv.2209.11055,
75
+ doi = {10.48550/ARXIV.2209.11055},
76
+ url = {https://arxiv.org/abs/2209.11055},
77
+ author = {Tunstall, Lewis and Reimers, Nils and Jo, Unso Eun Seo and Bates, Luke and Korat, Daniel and Wasserblat, Moshe and Pereg, Oren},
78
+ title = {Efficient Few-Shot Learning Without Prompts},
79
+ publisher = {arXiv},
80
+ year = {2022},
81
+ }
82
+ ```
app.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, UploadFile, File
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import HTMLResponse
4
+ from fastapi.staticfiles import StaticFiles
5
+ from pydantic import BaseModel
6
+ from setfit import AbsaModel
7
+ import logging
8
+ from typing import List, Dict, Any
9
+ import uvicorn
10
+ import os
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Initialize FastAPI app
17
+ app = FastAPI(title="ABSA Web Application", description="Aspect-Based Sentiment Analysis using SetFit models")
18
+
19
+ # Add CORS middleware
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"],
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ # Global variable to store the model
29
+ absa_model = None
30
+
31
+ class TextInput(BaseModel):
32
+ text: str
33
+
34
+ class ABSAResponse(BaseModel):
35
+ text: str
36
+ predictions: List[Dict[str, Any]]
37
+ success: bool
38
+ message: str
39
+
40
+ async def load_model():
41
+ """Load the ABSA model on startup"""
42
+ global absa_model
43
+ try:
44
+ logger.info("Loading ABSA models...")
45
+ absa_model = AbsaModel.from_pretrained(
46
+ "ronalhung/setfit-absa-restaurants-aspect",
47
+ "ronalhung/setfit-absa-restaurants-polarity",
48
+ )
49
+ logger.info("Models loaded successfully!")
50
+ except Exception as e:
51
+ logger.error(f"Failed to load models: {str(e)}")
52
+ raise e
53
+
54
+ @app.on_event("startup")
55
+ async def startup_event():
56
+ """Load model when the application starts"""
57
+ await load_model()
58
+
59
+ @app.get("/", response_class=HTMLResponse)
60
+ async def get_home():
61
+ """Serve the main HTML page"""
62
+ html_content = """
63
+ <!DOCTYPE html>
64
+ <html lang="en">
65
+ <head>
66
+ <meta charset="UTF-8">
67
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
68
+ <title>ABSA - Aspect-Based Sentiment Analysis</title>
69
+ <script src="https://cdn.tailwindcss.com"></script>
70
+ <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
71
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
72
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
73
+ </head>
74
+ <body class="bg-gray-50">
75
+ <div id="root"></div>
76
+ <script type="text/babel">
77
+ const { useState, useRef } = React;
78
+
79
+ const App = () => {
80
+ const [text, setText] = useState('');
81
+ const [results, setResults] = useState(null);
82
+ const [loading, setLoading] = useState(false);
83
+ const [error, setError] = useState('');
84
+ const fileInputRef = useRef(null);
85
+
86
+ const handleAnalyze = async () => {
87
+ if (!text.trim()) {
88
+ setError('Please enter some text to analyze');
89
+ return;
90
+ }
91
+
92
+ setLoading(true);
93
+ setError('');
94
+
95
+ try {
96
+ const response = await fetch('/analyze', {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ },
101
+ body: JSON.stringify({ text: text.trim() }),
102
+ });
103
+
104
+ const data = await response.json();
105
+
106
+ if (data.success) {
107
+ setResults(data);
108
+ } else {
109
+ setError(data.message || 'Analysis failed');
110
+ }
111
+ } catch (err) {
112
+ setError('Failed to analyze text. Please try again.');
113
+ console.error('Error:', err);
114
+ } finally {
115
+ setLoading(false);
116
+ }
117
+ };
118
+
119
+ const handleFileUpload = async (event) => {
120
+ const file = event.target.files[0];
121
+ if (!file) return;
122
+
123
+ if (!file.name.endsWith('.txt')) {
124
+ setError('Please upload a .txt file');
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const text = await file.text();
130
+ setText(text);
131
+ setError('');
132
+ } catch (err) {
133
+ setError('Failed to read file. Please try again.');
134
+ console.error('Error reading file:', err);
135
+ }
136
+ };
137
+
138
+ const clearResults = () => {
139
+ setText('');
140
+ setResults(null);
141
+ setError('');
142
+ };
143
+
144
+ const getSentimentColor = (polarity) => {
145
+ switch (polarity) {
146
+ case 'positive': return 'text-green-600 bg-green-100';
147
+ case 'negative': return 'text-red-600 bg-red-100';
148
+ case 'neutral': return 'text-gray-600 bg-gray-100';
149
+ case 'conflict': return 'text-yellow-600 bg-yellow-100';
150
+ default: return 'text-gray-600 bg-gray-100';
151
+ }
152
+ };
153
+
154
+ return (
155
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
156
+ <div className="container mx-auto px-4 py-8">
157
+ <div className="max-w-4xl mx-auto">
158
+ {/* Header */}
159
+ <div className="text-center mb-8">
160
+ <h1 className="text-4xl font-bold text-gray-800 mb-4">
161
+ Aspect-Based Sentiment Analysis
162
+ </h1>
163
+ <p className="text-lg text-gray-600">
164
+ Analyze aspects and sentiments in restaurant reviews using SetFit models
165
+ </p>
166
+ </div>
167
+
168
+ {/* Input Section */}
169
+ <div className="bg-white rounded-lg shadow-lg p-6 mb-6">
170
+ <h2 className="text-2xl font-semibold text-gray-800 mb-4">Input Text</h2>
171
+
172
+ {/* File Upload */}
173
+ <div className="mb-4">
174
+ <label className="block text-sm font-medium text-gray-700 mb-2">
175
+ Upload Text File (.txt)
176
+ </label>
177
+ <input
178
+ ref={fileInputRef}
179
+ type="file"
180
+ accept=".txt"
181
+ onChange={handleFileUpload}
182
+ className="block w-full text-sm text-gray-500
183
+ file:mr-4 file:py-2 file:px-4
184
+ file:rounded-md file:border-0
185
+ file:text-sm file:font-semibold
186
+ file:bg-blue-50 file:text-blue-700
187
+ hover:file:bg-blue-100
188
+ cursor-pointer"
189
+ />
190
+ </div>
191
+
192
+ {/* Text Area */}
193
+ <div className="mb-4">
194
+ <label className="block text-sm font-medium text-gray-700 mb-2">
195
+ Or type/paste your text here:
196
+ </label>
197
+ <textarea
198
+ value={text}
199
+ onChange={(e) => setText(e.target.value)}
200
+ placeholder="Enter restaurant review text for analysis..."
201
+ className="w-full h-32 p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
202
+ />
203
+ </div>
204
+
205
+ {/* Error Message */}
206
+ {error && (
207
+ <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-md">
208
+ {error}
209
+ </div>
210
+ )}
211
+
212
+ {/* Action Buttons */}
213
+ <div className="flex gap-3">
214
+ <button
215
+ onClick={handleAnalyze}
216
+ disabled={loading || !text.trim()}
217
+ className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700
218
+ disabled:bg-gray-400 disabled:cursor-not-allowed
219
+ flex items-center gap-2 font-medium transition-colors"
220
+ >
221
+ {loading ? (
222
+ <>
223
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
224
+ Analyzing...
225
+ </>
226
+ ) : (
227
+ 'Analyze Text'
228
+ )}
229
+ </button>
230
+
231
+ <button
232
+ onClick={clearResults}
233
+ className="px-6 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600
234
+ font-medium transition-colors"
235
+ >
236
+ Clear
237
+ </button>
238
+ </div>
239
+ </div>
240
+
241
+ {/* Results Section */}
242
+ {results && (
243
+ <div className="bg-white rounded-lg shadow-lg p-6">
244
+ <h2 className="text-2xl font-semibold text-gray-800 mb-4">Analysis Results</h2>
245
+
246
+ {/* Original Text */}
247
+ <div className="mb-6">
248
+ <h3 className="text-lg font-medium text-gray-700 mb-2">Original Text:</h3>
249
+ <div className="p-3 bg-gray-50 rounded-md border">
250
+ {results.text}
251
+ </div>
252
+ </div>
253
+
254
+ {/* Predictions */}
255
+ <div>
256
+ <h3 className="text-lg font-medium text-gray-700 mb-4">
257
+ Detected Aspects & Sentiments:
258
+ </h3>
259
+
260
+ {results.predictions && results.predictions.length > 0 ? (
261
+ <div className="space-y-3">
262
+ {results.predictions.map((prediction, index) => (
263
+ <div key={index} className="border border-gray-200 rounded-md p-4">
264
+ <div className="flex items-center justify-between mb-2">
265
+ <span className="text-sm font-medium text-gray-600">
266
+ Aspect Span:
267
+ </span>
268
+ <span className="font-semibold text-gray-800">
269
+ "{prediction.span}"
270
+ </span>
271
+ </div>
272
+ <div className="flex items-center justify-between">
273
+ <span className="text-sm font-medium text-gray-600">
274
+ Sentiment:
275
+ </span>
276
+ <span className={`px-3 py-1 rounded-full text-sm font-medium ${getSentimentColor(prediction.polarity)}`}>
277
+ {prediction.polarity}
278
+ </span>
279
+ </div>
280
+ </div>
281
+ ))}
282
+ </div>
283
+ ) : (
284
+ <div className="text-gray-500 text-center py-4">
285
+ No aspects detected in the text.
286
+ </div>
287
+ )}
288
+ </div>
289
+ </div>
290
+ )}
291
+ </div>
292
+ </div>
293
+ </div>
294
+ );
295
+ };
296
+
297
+ ReactDOM.render(<App />, document.getElementById('root'));
298
+ </script>
299
+ </body>
300
+ </html>
301
+ """
302
+ return html_content
303
+
304
+ @app.post("/analyze", response_model=ABSAResponse)
305
+ async def analyze_text(input_data: TextInput):
306
+ """Analyze text for aspects and sentiment"""
307
+ global absa_model
308
+
309
+ if absa_model is None:
310
+ raise HTTPException(status_code=503, detail="Model not loaded yet. Please try again later.")
311
+
312
+ try:
313
+ text = input_data.text.strip()
314
+ if not text:
315
+ return ABSAResponse(
316
+ text=text,
317
+ predictions=[],
318
+ success=False,
319
+ message="Empty text provided"
320
+ )
321
+
322
+ logger.info(f"Analyzing text: {text[:100]}...")
323
+
324
+ # Run ABSA analysis
325
+ predictions = absa_model(text)
326
+
327
+ # Format predictions for response
328
+ formatted_predictions = []
329
+ if predictions:
330
+ for pred in predictions:
331
+ formatted_predictions.append({
332
+ "span": pred.get("span", ""),
333
+ "polarity": pred.get("polarity", "neutral")
334
+ })
335
+
336
+ return ABSAResponse(
337
+ text=text,
338
+ predictions=formatted_predictions,
339
+ success=True,
340
+ message="Analysis completed successfully"
341
+ )
342
+
343
+ except Exception as e:
344
+ logger.error(f"Error during analysis: {str(e)}")
345
+ return ABSAResponse(
346
+ text=input_data.text,
347
+ predictions=[],
348
+ success=False,
349
+ message=f"Analysis failed: {str(e)}"
350
+ )
351
+
352
+ @app.get("/health")
353
+ async def health_check():
354
+ """Health check endpoint"""
355
+ return {
356
+ "status": "healthy",
357
+ "model_loaded": absa_model is not None,
358
+ "message": "ABSA service is running"
359
+ }
360
+
361
+ if __name__ == "__main__":
362
+ uvicorn.run(
363
+ "app:app",
364
+ host="0.0.0.0",
365
+ port=8000,
366
+ reload=True,
367
+ log_level="info"
368
+ )
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ setfit==1.1.2
4
+ sentence-transformers==4.1.0
5
+ spacy==3.8.7
6
+ transformers==4.52.4
7
+ torch==2.6.0
8
+ python-multipart==0.0.6
9
+ pydantic==2.5.0
sample_reviews.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The food was absolutely delicious, but the service was quite slow and the staff seemed overwhelmed.
2
+
3
+ Amazing atmosphere with great music and lighting. However, the prices are way too expensive for the portion sizes.
4
+
5
+ The restaurant has a beautiful interior design and the food quality is outstanding. Highly recommend the pasta dishes!
6
+
7
+ Terrible experience overall. The food was cold, the service was rude, and the place was very noisy. Will not come back.
8
+
9
+ Great location downtown with easy parking. The food was okay, nothing special, but the staff was very friendly and accommodating.
10
+
11
+ The pizza was fantastic and the wine selection is impressive. The only downside was the long wait time for a table.
12
+
13
+ Cozy little place with excellent customer service. The menu has good variety but some dishes were a bit bland.
14
+
15
+ Outstanding food quality and presentation. The chef really knows what they're doing. The only issue is the cramped seating area.
16
+
17
+ Perfect for a romantic dinner. The ambiance is lovely, food is excellent, and the service is attentive. Definitely worth the price.
18
+
19
+ Mixed feelings about this place. Great cocktails and appetizers, but the main courses were disappointing and overpriced.