"""AspectBERT Streamlit app — aspect-based sentiment analysis for product reviews. Run locally: streamlit run app.py Configure the model source via the HF_MODEL_NAME environment variable (a HuggingFace Hub repo id). If unset, falls back to base distilbert-base-uncased weights with an untrained classification head (for UI smoke-testing only). """ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src")) import plotly.graph_objects as go import streamlit as st from constants import ASPECTS, ID2LABEL # noqa: E402 from inference import explain_with_lime, load_model, predict_all_aspects # noqa: E402 st.set_page_config(page_title="AspectBERT - Aspect-Based Sentiment Analysis", layout="wide") EXAMPLE_REVIEWS = [ "The battery life is incredible, lasts two days on a single charge! However, " "the camera quality is disappointing in low light, and the price feels a bit " "too high for what you get.", "Beautiful sleek design and the display is gorgeous with vibrant colors. The " "software has some annoying bugs and crashes occasionally, but performance " "overall is snappy.", "Customer service was unhelpful when I tried to return a defective unit. The " "build quality feels cheap and the screen scratches easily.", "Great value for money! Performance is fast and smooth for everyday tasks, " "the design feels premium, and battery easily lasts all day.", ] LABEL_COLORS = {"positive": "green", "neutral": "orange", "negative": "red"} @st.cache_resource(show_spinner="Loading AspectBERT model...") def get_model(): model_name = os.environ.get("HF_MODEL_NAME", "itismeTithi/AspectBERT") return load_model(model_name) def render_radar_chart(results): categories = list(results.keys()) values = [results[a]["scores"]["positive"] for a in categories] fig = go.Figure() fig.add_trace(go.Scatterpolar( r=values + values[:1], theta=[c.replace("_", " ").title() for c in categories] + [categories[0].replace("_", " ").title()], fill="toself", name="Positive score", )) fig.update_layout( polar=dict(radialaxis=dict(visible=True, range=[0, 1])), showlegend=False, margin=dict(l=40, r=40, t=20, b=20), ) return fig def vader_label(compound): if compound >= 0.05: return "positive" if compound <= -0.05: return "negative" return "neutral" def main(): st.title("AspectBERT: Aspect-Based Sentiment Analysis") st.caption( "Fine-tuned DistilBERT that detects sentiment (positive / neutral / " "negative) for 8 product aspects from a single review." ) with st.sidebar: st.header("Options") use_vader = st.checkbox("Compare with VADER baseline", value=False) use_lime = st.checkbox("Show LIME word importance", value=False) selected_aspects = st.multiselect("Aspects to analyze", ASPECTS, default=ASPECTS) st.divider() model_source = os.environ.get("HF_MODEL_NAME", "distilbert-base-uncased (untrained head)") st.caption(f"Model: `{model_source}`") st.subheader("Try an example review") cols = st.columns(len(EXAMPLE_REVIEWS)) for i, col in enumerate(cols): if col.button(f"Example {i + 1}", key=f"example_{i}", use_container_width=True): st.session_state["review_text"] = EXAMPLE_REVIEWS[i] review_text = st.text_area( "Paste a product review", value=st.session_state.get("review_text", ""), height=150, key="review_text", ) analyze_clicked = st.button("Analyze", type="primary") if not selected_aspects: st.warning("Select at least one aspect in the sidebar.") return if analyze_clicked and review_text.strip(): model, tokenizer, device = get_model() with st.spinner("Analyzing aspects..."): results = predict_all_aspects(model, tokenizer, device, review_text, aspects=selected_aspects) st.subheader("Sentiment per aspect") for aspect, res in results.items(): label = res["label"] st.markdown( f"**{aspect.replace('_', ' ').title()}** — " f":{LABEL_COLORS[label]}[{label.upper()}]" ) cols = st.columns(3) for i, lbl in enumerate(["negative", "neutral", "positive"]): score = res["scores"][lbl] cols[i].progress(score, text=f"{lbl}: {score:.2f}") st.subheader("Radar chart: positive sentiment across aspects") st.plotly_chart(render_radar_chart(results), use_container_width=True) if use_vader: st.subheader("VADER baseline comparison") try: from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer analyzer = SentimentIntensityAnalyzer() compound = analyzer.polarity_scores(review_text)["compound"] overall = vader_label(compound) st.write( f"VADER overall sentiment (whole review, not aspect-aware): " f"**:{LABEL_COLORS[overall]}[{overall.upper()}]** " f"(compound score = {compound:.3f})" ) st.caption( "VADER is a lexicon-based, rule-based sentiment scorer that does " "not support aspect-level sentiment — it produces one score per " "review. AspectBERT predicts sentiment independently per aspect." ) except ImportError: st.warning("vaderSentiment is not installed. Install with " "`pip install vaderSentiment` to enable this feature.") if use_lime: st.subheader("LIME word importance") try: lime_aspect = st.selectbox( "Select aspect for LIME explanation", selected_aspects, key="lime_aspect" ) with st.spinner("Computing LIME explanation (this can take a moment)..."): exp = explain_with_lime(model, tokenizer, device, review_text, lime_aspect) pred_label = results[lime_aspect]["label"] label_idx = [i for i, name in ID2LABEL.items() if name == pred_label][0] html = exp.as_html(labels=[label_idx]) st.components.v1.html(html, height=400, scrolling=True) except ImportError: st.warning("lime is not installed. Install with `pip install lime` " "to enable this feature.") except Exception as exc: st.error(f"LIME explanation failed: {exc}") elif not review_text.strip(): st.info("Paste a review above and click Analyze, or try an example.") if __name__ == "__main__": main()