Spaces:
No application file
No application file
| # app.py | |
| import streamlit as st | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| import pandas as pd | |
| from utils import load_model, clean_text, predict_sentiment | |
| # ---- Page Config ---- | |
| st.set_page_config( | |
| page_title="Sentiment Analysis App", | |
| page_icon="π", | |
| layout="wide" | |
| ) | |
| # ---- CSS ---- | |
| st.markdown(""" | |
| <style> | |
| .positive { color: #2ecc71; font-size: 2rem; font-weight: bold; } | |
| .negative { color: #e74c3c; font-size: 2rem; font-weight: bold; } | |
| .metric-card { | |
| background: #1e2130; | |
| border-radius: 12px; | |
| padding: 24px; | |
| text-align: center; | |
| margin-top: 10px; | |
| } | |
| .footer { text-align: center; color: gray; font-size: 0.85rem; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ---- Load Model ---- | |
| tokenizer, model, device = load_model() | |
| # ---- Header ---- | |
| st.title("π Real-Time Sentiment Analysis") | |
| st.markdown("*Fine-tuned **DistilBERT** Β· 91% accuracy Β· Built by Nguyen Tin Tin Do*") | |
| st.divider() | |
| # ---- Tabs ---- | |
| tab1, tab2, tab3 = st.tabs(["π Single Prediction", "π Batch Prediction", "π Model Info"]) | |
| # ============================== | |
| # TAB 1 β Single Prediction | |
| # ============================== | |
| with tab1: | |
| user_input = st.text_area( | |
| "Enter text to analyze:", | |
| placeholder="e.g. This movie was absolutely amazing...", | |
| height=150 | |
| ) | |
| if st.button("π Analyze Sentiment", use_container_width=True): | |
| if user_input.strip(): | |
| with st.spinner("Analyzing..."): | |
| cleaned = clean_text(user_input) | |
| result = predict_sentiment(cleaned, tokenizer, model, device) | |
| sentiment = result["sentiment"] | |
| confidence = result["confidence"] | |
| latency = result["inference_time_ms"] | |
| col1, col2, col3 = st.columns(3) | |
| col1.metric("Sentiment", f"{'β ' if sentiment == 'Positive' else 'β'} {sentiment}") | |
| col2.metric("Confidence", f"{confidence*100:.1f}%") | |
| col3.metric("Latency", f"{latency}ms") | |
| # Gauge chart | |
| fig = go.Figure(go.Indicator( | |
| mode="gauge+number", | |
| value=confidence * 100, | |
| title={"text": "Confidence Score (%)"}, | |
| gauge={ | |
| "axis": {"range": [0, 100]}, | |
| "bar": {"color": "#2ecc71" if sentiment == "Positive" else "#e74c3c"}, | |
| "steps": [ | |
| {"range": [0, 50], "color": "#1a1a2e"}, | |
| {"range": [50, 100], "color": "#16213e"} | |
| ], | |
| "threshold": { | |
| "line": {"color": "white", "width": 3}, | |
| "value": 50 | |
| } | |
| } | |
| )) | |
| fig.update_layout( | |
| height=280, | |
| paper_bgcolor="rgba(0,0,0,0)", | |
| font={"color": "white"} | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.warning("β οΈ Please enter some text first.") | |
| # ============================== | |
| # TAB 2 β Batch Prediction | |
| # ============================== | |
| with tab2: | |
| batch_input = st.text_area( | |
| "Enter multiple texts (one per line):", | |
| placeholder="This film was great!\nTerrible acting...\nAbsolutely loved it!", | |
| height=180 | |
| ) | |
| if st.button("π Analyze Batch", use_container_width=True): | |
| texts = [t.strip() for t in batch_input.split("\n") if t.strip()] | |
| if texts: | |
| results = [] | |
| progress = st.progress(0) | |
| for i, text in enumerate(texts): | |
| cleaned = clean_text(text) | |
| result = predict_sentiment(cleaned, tokenizer, model, device) | |
| results.append({ | |
| "Text" : text[:60] + "..." if len(text) > 60 else text, | |
| "Sentiment" : result["sentiment"], | |
| "Confidence": f"{result['confidence']*100:.1f}%", | |
| "Latency" : f"{result['inference_time_ms']}ms" | |
| }) | |
| progress.progress((i + 1) / len(texts)) | |
| # Table | |
| df = pd.DataFrame(results) | |
| st.dataframe(df, use_container_width=True) | |
| # Pie chart | |
| counts = df["Sentiment"].value_counts() | |
| fig = px.pie( | |
| values=counts.values, | |
| names=counts.index, | |
| title=f"Sentiment Distribution ({len(texts)} texts)", | |
| color=counts.index, | |
| color_discrete_map={"Positive": "#2ecc71", "Negative": "#e74c3c"} | |
| ) | |
| fig.update_layout(paper_bgcolor="rgba(0,0,0,0)", font={"color": "white"}) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.warning("β οΈ Please enter at least one text.") | |
| # ============================== | |
| # TAB 3 β Model Info | |
| # ============================== | |
| with tab3: | |
| st.markdown(""" | |
| ### π€ Model Details | |
| | | | | |
| |---|---| | |
| | **Base Model** | DistilBERT (distilbert-base-uncased) | | |
| | **Fine-tuned on** | IMDB Movie Reviews (50,000 samples) | | |
| | **Training Epochs** | 3 | | |
| | **Batch Size** | 16 | | |
| | **Learning Rate** | 2e-5 | | |
| | **Accuracy** | 91% | | |
| | **F1-Score** | 0.89 | | |
| ### π Performance vs Baseline | |
| | Model | Accuracy | F1-Score | | |
| |---|---|---| | |
| | TF-IDF + Logistic Regression | 77% | 0.77 | | |
| | **DistilBERT (fine-tuned)** | **91%** | **0.89** | | |
| ### π οΈ Tech Stack | |
| `Python` `HuggingFace Transformers` `FastAPI` `Streamlit` `Plotly` `Docker` | |
| """) | |
| # ---- Footer ---- | |
| st.divider() | |
| st.markdown( | |
| "<p class='footer'>Built by <strong>Nguyen Tin Tin Do</strong> Β· " | |
| "<a href='https://github.com/NguyenTin'>GitHub</a> Β· " | |
| "<a href='https://linkedin.com/in/nguyen-tin-tin-do'>LinkedIn</a></p>", | |
| unsafe_allow_html=True | |
| ) |