"""Dual-interface Hugging Face Space for clothing aspect-level sentiment analysis.""" from __future__ import annotations import html import json import re import traceback from functools import lru_cache from pathlib import Path from typing import Any, Dict, List, Tuple try: import huggingface_hub as _hf_hub if not hasattr(_hf_hub, "HfFolder"): class _HfFolderCompat: @staticmethod def get_token(): try: return _hf_hub.get_token() except Exception: return None @staticmethod def save_token(token): return None @staticmethod def delete_token(): return None _hf_hub.HfFolder = _HfFolderCompat except Exception: pass import gradio as gr try: import spaces except Exception: class _SpacesCompat: def GPU(self, *args, **kwargs): if args and callable(args[0]) and len(args) == 1 and not kwargs: return args[0] def decorator(fn): return fn return decorator spaces = _SpacesCompat() from src import config as cfg from src.inference import AspectPredictor ROOT = Path(__file__).resolve().parent REPORT_DIR = ROOT / "reports" CHECKPOINT_DIR = ROOT / "checkpoints" / "meta_acsa" CHECKPOINT_PATH = CHECKPOINT_DIR / "best.pt" META_ENCODER_PATH = ROOT / "data" / "meta_encoder.pkl" ASPECTS = list(getattr(cfg, "ASPECTS", ["SIZE", "MATERIAL", "QUALITY", "APPEARANCE", "STYLE", "VALUE"])) ASPECT_DESCRIPTIONS = { "SIZE": "Fit, length, sizing accuracy, runs large or small.", "MATERIAL": "Fabric feel, thickness, breathability, and comfort.", "QUALITY": "Workmanship, durability, seams, and washing performance.", "APPEARANCE": "Color, print, pattern, image consistency, and visual look.", "STYLE": "Cut, silhouette, fashionability, and styling appeal.", "VALUE": "Price fairness, worthiness, return, and repurchase intent.", } ASPECT_KEYWORDS = { "SIZE": ["size", "fit", "fits", "small", "large", "tight", "loose", "xl", "medium", "waist", "length", "runs"], "MATERIAL": ["material", "fabric", "cotton", "polyester", "soft", "scratchy", "thin", "thick", "stretch", "breathable"], "QUALITY": ["quality", "stitch", "stitching", "seam", "wash", "durable", "cheap", "ripped", "tear", "button", "zipper"], "APPEARANCE": ["look", "looks", "color", "photo", "picture", "print", "design", "pattern", "beautiful", "cute"], "STYLE": ["style", "stylish", "flattering", "casual", "formal", "silhouette", "cut", "shape", "cropped"], "VALUE": ["price", "worth", "value", "money", "expensive", "cheap", "return", "buy", "recommend"], } LABEL_BG = {"Positive": "#dcfce7", "Negative": "#fee2e2", "Not_Mentioned": "#f1f5f9", "Neutral": "#e0f2fe"} LABEL_FG = {"Positive": "#15803d", "Negative": "#b91c1c", "Not_Mentioned": "#475569", "Neutral": "#0369a1"} PRODUCT_CATALOG_PATH = ROOT / "data" / "product_catalog.json" MAX_FINDER_PRODUCTS = 30 MAX_MONITOR_PRODUCTS = 30 FALLBACK_PRODUCTS = [ {"name": "Cotton Graphic Tee", "category": "Tops", "features": "100% Cotton, Slim Fit, Machine Wash Cold, Graphic Print", "categories": "Clothing > Men > T-Shirts > Graphic Tees", "price": 19.99, "average_rating": 4.2, "rating_number": 312, "tags": ["cotton", "casual", "print"], "review": "The size runs really small, I ordered an XL but it fits like a Medium. The fabric feels soft and the print looks great."}, {"name": "Stretch Yoga Leggings", "category": "Bottoms", "features": "Nylon Spandex Blend, High Waist, Four-Way Stretch, Moisture Wicking", "categories": "Clothing > Women > Activewear > Leggings", "price": 29.99, "average_rating": 4.5, "rating_number": 1280, "tags": ["stretch", "activewear", "high waist"], "review": "These leggings fit perfectly and the stretch is comfortable. The material is not see-through, but the seams started to loosen after washing."}, {"name": "Floral Summer Dress", "category": "Dresses", "features": "Rayon Blend, Floral Print, A-Line, Lightweight, V-Neck", "categories": "Clothing > Women > Dresses > Summer Dresses", "price": 36.50, "average_rating": 4.3, "rating_number": 864, "tags": ["floral", "summer", "dress"], "review": "The dress looks beautiful and the floral print is exactly as shown. The waist is a bit tight and the fabric wrinkles easily."}, ] def _load_products() -> List[Dict[str, Any]]: if PRODUCT_CATALOG_PATH.exists(): try: products = json.loads(PRODUCT_CATALOG_PATH.read_text(encoding="utf-8")) if isinstance(products, list) and products: return products except Exception: pass return FALLBACK_PRODUCTS PRODUCTS = _load_products() def _safe_float(value: Any, default: float = 0.0) -> float: try: if value in (None, ""): return default return float(value) except Exception: return default def _esc(value: Any) -> str: return html.escape(str(value)) def _pct(value: Any) -> str: try: return f"{float(value) * 100:.2f}%" except Exception: return "-" def _num(value: Any) -> str: try: return f"{float(value):.4f}" except Exception: return "-" def _load_json(name: str) -> Dict[str, Any]: path = REPORT_DIR / name try: return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} except Exception: return {} @lru_cache(maxsize=1) def _predictor() -> AspectPredictor: return AspectPredictor(checkpoint_dir=CHECKPOINT_DIR) def _product_names() -> List[str]: return [p["name"] for p in PRODUCTS] def _get_product(name: str) -> Dict[str, Any]: return next((p for p in PRODUCTS if p["name"] == name), PRODUCTS[0]) def _rank_products(products: List[Dict[str, Any]]) -> List[Dict[str, Any]]: return sorted( products, key=lambda p: ( _safe_float(p.get("rating_number")), _safe_float(p.get("average_rating")), -_safe_float(p.get("price")), ), reverse=True, ) def _filtered_products(category: str = "All", tags: List[str] | None = None, min_rating: float = 0.0) -> List[Dict[str, Any]]: required = {str(tag).lower() for tag in (tags or [])} matches = [] for product in PRODUCTS: if category != "All" and product.get("category") != category: continue product_tags = {str(tag).lower() for tag in product.get("tags", [])} feature_text = f'{product.get("features", "")} {product.get("categories", "")}'.lower() if required and not all((tag in product_tags) or (tag in feature_text) for tag in required): continue if _safe_float(product.get("average_rating")) < _safe_float(min_rating): continue matches.append(product) return _rank_products(matches) def _meta(product_or_meta: Dict[str, Any]) -> Dict[str, Any]: return { "features_text": product_or_meta.get("features", product_or_meta.get("features_text", "")), "categories_text": product_or_meta.get("categories", product_or_meta.get("categories_text", "")), "price": _safe_float(product_or_meta.get("price")), "average_rating": _safe_float(product_or_meta.get("average_rating")), "rating_number": _safe_float(product_or_meta.get("rating_number")), } @lru_cache(maxsize=64) def _predict_product(product_name: str) -> Dict[str, Any]: product = _get_product(product_name) return _predictor().predict(product["review"], _meta(product)) def _predict_custom(review: str, features: str, categories: str, price: Any, rating: Any, count: Any) -> Dict[str, Any]: meta = {"features_text": features or "", "categories_text": categories or "", "price": _safe_float(price), "average_rating": _safe_float(rating), "rating_number": _safe_float(count)} return _predictor().predict(review or "No review text provided.", meta) def _error_html(detail: str) -> str: return f'
Prediction could not run.
Technical detail
{_esc(detail)}
' def _chip(label: str) -> str: bg = LABEL_BG.get(label, "#f1f5f9") fg = LABEL_FG.get(label, "#334155") return f'{_esc(label)}' def _keyword_hits(text: str, aspect: str) -> List[str]: low = (text or "").lower() hits = [kw for kw in ASPECT_KEYWORDS.get(aspect, []) if kw.lower() in low] return list(dict.fromkeys(hits))[:8] def _highlight_review(text: str, aspect: str) -> str: safe = _esc(text) keys: List[str] = [] if aspect != "All": keys = ASPECT_KEYWORDS.get(aspect, []) else: for items in ASPECT_KEYWORDS.values(): keys.extend(items) for kw in sorted(set(keys), key=len, reverse=True): safe = re.sub(rf"\b({re.escape(kw)})\b", r"\1", safe, flags=re.IGNORECASE) return f'
{safe}
' def _overall_html(result: Dict[str, Any]) -> str: overall = result.get("overall", {}) label = overall.get("label", "Unknown") conf = _safe_float(overall.get("confidence")) probs = overall.get("class_probs", {}) bars = [] for name in ["Negative", "Neutral", "Positive"]: val = _safe_float(probs.get(name)) bars.append(f'
{name}
{val:.2f}
') return f'
Overall sentiment
{_chip(label)} confidence {conf:.2f}
{"".join(bars)}
' def _aspect_cards(result: Dict[str, Any], review: str, selected_aspect: str) -> str: details = result.get("aspect_details", {}) sources = result.get("top_meta_source_by_aspect", {}) cards = [] for aspect in ASPECTS: d = details.get(aspect, {}) label = d.get("label", result.get("aspects", {}).get(aspect, "Unknown")) conf = _safe_float(d.get("confidence")) hits = _keyword_hits(review, aspect) evidence = "".join(f'{_esc(h)}' for h in hits) or 'No explicit keyword evidence.' src = sources.get(aspect, {}) if isinstance(sources, dict) else {} src_line = f'
Top metadata source: {_esc(src.get("source", "-"))} ({_safe_float(src.get("weight")):.2f})
' if src else "" cls = "focus" if selected_aspect in ("All", aspect) else "dim" cards.append(f'
{aspect}conf {conf:.2f}
{_chip(label)}

{_esc(ASPECT_DESCRIPTIONS.get(aspect, ""))}

{evidence}
{src_line}
') return '
' + "".join(cards) + '
' def _aspect_rows(result: Dict[str, Any], review: str) -> List[List[Any]]: rows = [] for aspect in ASPECTS: d = result.get("aspect_details", {}).get(aspect, {}) label = d.get("label", result.get("aspects", {}).get(aspect, "Unknown")) rows.append([aspect, label, round(_safe_float(d.get("confidence")), 4), ", ".join(_keyword_hits(review, aspect)) or "No explicit keyword evidence"]) return rows @spaces.GPU(duration=120) def consumer_product_view(product_name: str, selected_aspect: str) -> Tuple[str, str, str, List[List[Any]]]: product = _get_product(product_name) try: result = _predict_product(product_name) except Exception: return _error_html(traceback.format_exc(limit=5)), "", "", [] image_url = product.get("image_url") or "" image_html = f'product image' if image_url else '
No image
' store = product.get("store") or "Amazon" asin = product.get("parent_asin") or "-" detail = ( f'
{image_html}
' f'

{_esc(product["name"])}

' f'

{_esc(product["categories"])}

' f'
Store: {_esc(store)}ASIN: {_esc(asin)}Price: ${_safe_float(product["price"]):.2f}Rating: {_safe_float(product["average_rating"]):.1f}Reviews: {int(_safe_float(product["rating_number"]))}
' f'

Metadata: {_esc(product["features"])}

{_overall_html(result)}
' ) evidence = f'

Key review evidence

{_highlight_review(product["review"], selected_aspect)}' return detail, _aspect_cards(result, product["review"], selected_aspect), evidence, _aspect_rows(result, product["review"]) @spaces.GPU(duration=120) def filter_products(aspect: str, sentiment: str, category: str, tags: List[str], min_rating: float) -> Tuple[List[List[Any]], str]: rows = [] best = None all_candidates = _filtered_products(category, tags, min_rating) candidates = all_candidates[:MAX_FINDER_PRODUCTS] if not candidates: return [], f'
No product matched the metadata filters in the real catalog of {len(PRODUCTS)} products.
' for product in candidates: try: result = _predict_product(product["name"]) except Exception: continue if aspect == "Overall": d = result.get("overall", {}) else: d = result.get("aspect_details", {}).get(aspect, {}) label = d.get("label", "Unknown") conf = _safe_float(d.get("confidence")) if sentiment != "Any" and label != sentiment: continue rows.append([product["name"], product["category"], label, round(conf, 4), product["average_rating"], product["price"], product["features"]]) score = conf + 0.03 * _safe_float(product["average_rating"]) + 0.000001 * _safe_float(product.get("rating_number")) if best is None or score > best[0]: best = (score, product["name"], label, conf) header = f'Scored top {len(candidates)} filtered products from {len(PRODUCTS)} real Amazon catalog items.' if len(all_candidates) > len(candidates): header += f' {len(all_candidates) - len(candidates)} additional metadata matches were not scored to keep the Space responsive.' summary = f'
{_esc(header)}
No product matched the target sentiment after model scoring.
' if best: summary = f'
Best match: {_esc(best[1])} - {_esc(aspect)} is {_esc(best[2])} with confidence {best[3]:.2f}.
{_esc(header)}
' return rows, summary def _payload() -> Dict[str, Dict[str, Any]]: return {"eval": _load_json("evaluation_comparison.json"), "proposed": _load_json("per_aspect_proposed.json"), "no_meta": _load_json("per_aspect_acsa_no_meta.json"), "ablation": _load_json("ablation_summary.json")} def model_info_html() -> str: data = _payload() proposed = data["proposed"].get("overall", {}) ckpt_mb = CHECKPOINT_PATH.stat().st_size / (1024 * 1024) if CHECKPOINT_PATH.exists() else 0 rows = [("Training data", "Amazon 2023 Clothing, 100K review sample"), ("Backbone", getattr(cfg, "BERT_MODEL_NAME", "bert-base-uncased")), ("Core method", "Aspect-specific cross-attention metadata fusion"), ("Metadata", "features, categories, price, average rating, rating count"), ("Aspects", ", ".join(ASPECTS)), ("Best checkpoint", f"{ckpt_mb:.1f} MB"), ("Mean aspect F1", _pct(proposed.get("mean_macro_f1"))), ("Mean aspect accuracy", _pct(proposed.get("mean_accuracy")))] body = "".join(f"{_esc(k)}{_esc(v)}" for k, v in rows) return f'
{body}
' def research_cards_html() -> str: data = _payload() cmp = data["eval"].get("overall_3class_comparison", {}) proposed_aspect = data["proposed"].get("overall", {}) no_meta_aspect = data["no_meta"].get("overall", {}) proposed_overall = cmp.get("Proposed_BERT_Meta_Fusion__overall_head", {}) gain = _safe_float(proposed_aspect.get("mean_macro_f1")) - _safe_float(no_meta_aspect.get("mean_macro_f1")) return f'
Proposed Overall Accuracy{_pct(proposed_overall.get("accuracy"))}overall head
Proposed Aspect Accuracy{_pct(proposed_aspect.get("mean_accuracy"))}six-aspect mean
Metadata F1 Gain+{gain:.4f}vs no-metadata BERT ACSA
' def overall_metric_rows() -> List[List[Any]]: cmp = _payload()["eval"].get("overall_3class_comparison", {}) pairs = [("TF-IDF + Logistic Regression", "Baseline_1_TFIDF_LogReg"), ("BERT Overall Classifier", "Baseline_2_BERT_overall_3class"), ("Proposed BERT + Metadata Fusion", "Proposed_BERT_Meta_Fusion__overall_head")] return [[name, _num(cmp.get(key, {}).get("macro_f1")), _num(cmp.get(key, {}).get("accuracy"))] for name, key in pairs] def aspect_metric_rows() -> List[List[Any]]: data = _payload() proposed = data["proposed"].get("per_aspect", {}) no_meta = data["no_meta"].get("per_aspect", {}) rows = [] for aspect in ASPECTS: p = proposed.get(aspect, {}) b = no_meta.get(aspect, {}) f1_delta = _safe_float(p.get("macro_f1")) - _safe_float(b.get("macro_f1")) acc_delta = _safe_float(p.get("accuracy")) - _safe_float(b.get("accuracy")) rows.append([aspect, _num(b.get("macro_f1")), _num(p.get("macro_f1")), f"{f1_delta:+.4f}", _num(b.get("accuracy")), _num(p.get("accuracy")), f"{acc_delta:+.4f}"]) return rows def ablation_rows() -> List[List[Any]]: ab = _payload()["ablation"] labels = {"Proposed": "Proposed cross-attention fusion", "A1_no_text_meta": "A1 remove text metadata", "A2_no_numeric_meta": "A2 remove numerical metadata", "A3_concat_fusion": "A3 concat fusion"} return [[labels.get(k, k), _num(ab.get(k, {}).get("mean_macro_f1")), _num(ab.get(k, {}).get("mean_accuracy"))] for k in labels if k in ab] @spaces.GPU(duration=120) def merchant_product_scores(metric: str) -> List[List[Any]]: rows = [] monitored = _rank_products(PRODUCTS)[:MAX_MONITOR_PRODUCTS] for product in monitored: try: result = _predict_product(product["name"]) except Exception as exc: return [["ERROR", "Model loading failed", metric, type(exc).__name__, 0.0, 0.0, 0.0]] d = result.get("overall", {}) if metric == "Overall" else result.get("aspect_details", {}).get(metric, {}) rows.append([product["name"], product["category"], metric, d.get("label", "Unknown"), round(_safe_float(d.get("confidence")), 4), product["price"], product["average_rating"]]) return rows or [["No rows", "Try Refresh", metric, "-", 0.0, 0.0, 0.0]] def _metadata_risks(features: str, categories: str, price: Any, rating: Any, count: Any) -> Dict[str, str]: text = f"{features} {categories}".lower() risks = {} if any(x in text for x in ["slim", "oversized", "cropped", "one size", "tight", "relaxed"]): risks["SIZE"] = "Fit wording may create sizing expectation risk." if any(x in text for x in ["polyester", "synthetic", "faux", "thin", "lightweight"]): risks["MATERIAL"] = "Material description may affect comfort perception." if any(x in text for x in ["delicate", "hand wash", "button", "zipper", "distressed"]) or _safe_float(rating, 4.0) < 4.0: risks["QUALITY"] = "Durability or construction may need QA attention." if any(x in text for x in ["print", "floral", "color", "washed", "distressed"]): risks["APPEARANCE"] = "Visual consistency should be checked against product photos." if any(x in text for x in ["oversized", "slim", "cropped", "a-line", "v-neck"]): risks["STYLE"] = "Style-specific expectations may split customer opinions." if _safe_float(price) > 60 or _safe_float(rating, 4.0) < 4.0 or _safe_float(count) < 50: risks["VALUE"] = "Price, low rating, or low review volume may raise value risk." return risks @spaces.GPU(duration=120) def screen_new_product(features: str, categories: str, price: Any, rating: Any, count: Any, focus: str) -> Tuple[str, List[List[Any]]]: review = "This is a new clothing item. Customers may comment on fit, fabric, quality, appearance, style, and value." try: result = _predict_custom(review, features, categories, price, rating, count) except Exception: return _error_html(traceback.format_exc(limit=5)), [] rules = _metadata_risks(features, categories, price, rating, count) rows, high = [], [] for aspect in ASPECTS: if focus != "All" and aspect != focus: continue d = result.get("aspect_details", {}).get(aspect, {}) label = d.get("label", "Unknown") conf = _safe_float(d.get("confidence")) risk = "High" if label == "Negative" or aspect in rules else "Medium" if conf < 0.65 else "Low" if risk == "High": high.append(aspect) rows.append([aspect, risk, label, round(conf, 4), rules.get(aspect, "No strong metadata risk signal.")]) summary = ", ".join(high) if high else "No high-risk aspect detected from metadata." return f'
New product risk focus: {_esc(summary)}
This is a metadata screening tool, not a replacement for real review evaluation.
', rows @spaces.GPU(duration=120) def external_review_predict(review: str, features: str, categories: str, price: Any, rating: Any, count: Any, selected_aspect: str) -> Tuple[str, str, List[List[Any]]]: try: result = _predict_custom(review, features, categories, price, rating, count) except Exception: return _error_html(traceback.format_exc(limit=5)), "", [] return _overall_html(result), _aspect_cards(result, review or "", selected_aspect), _aspect_rows(result, review or "") def _status_html() -> str: missing = [] if not CHECKPOINT_PATH.exists() or CHECKPOINT_PATH.stat().st_size < 1024 * 1024: missing.append("checkpoints/meta_acsa/best.pt") if not META_ENCODER_PATH.exists() or META_ENCODER_PATH.stat().st_size < 1024: missing.append("data/meta_encoder.pkl") if missing: return '
Model artifacts missing: ' + _esc(", ".join(missing)) + '
' return '
Model ready. 10W0715 checkpoint and metadata encoder are available.
' CSS = """ :root { --bg:#f5f7fb; --surface:#ffffff; --surface-2:#f8fafc; --ink:#0f172a; --muted:#64748b; --line:#d9e2ef; --accent:#f97316; --accent-2:#fb923c; --green:#16a34a; --red:#dc2626; --blue:#2563eb; --shadow:0 18px 45px rgba(15,23,42,.08); } body, .gradio-container { background:var(--bg) !important; color:var(--ink); } .gradio-container { max-width:1280px !important; margin:auto !important; font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; } footer { display:none !important; } #app-hero { margin:12px 0 18px; padding:24px 28px; border-radius:22px; background:linear-gradient(135deg,#111827 0%,#1e293b 55%,#f97316 160%); color:white; box-shadow:var(--shadow); } #app-hero .eyebrow { text-transform:uppercase; letter-spacing:.12em; font-size:12px; color:#fed7aa; font-weight:800; margin-bottom:8px; } #app-hero h1 { margin:0; font-size:32px; line-height:1.12; letter-spacing:-.02em; } #app-hero p { margin:10px 0 0; color:#e2e8f0; max-width:840px; } #app-hero .hero-pills { display:flex; gap:8px; flex-wrap:wrap; margin-top:16px; } #app-hero .hero-pills span { background:rgba(255,255,255,.12); border:1px solid rgba(255,255,255,.18); border-radius:999px; padding:7px 11px; font-size:13px; } .status { border-radius:14px; padding:12px 14px; margin:8px 0 18px; border:1px solid var(--line); } .status.ok { background:#ecfdf5; border-color:#86efac; color:#065f46; } .status.bad { background:#fff1f2; border-color:#fda4af; color:#991b1b; } .app-section { display:flex; align-items:flex-end; justify-content:space-between; gap:16px; margin:8px 0 14px; } .app-section h2 { margin:0; font-size:24px; letter-spacing:-.02em; } .app-section p { margin:5px 0 0; color:var(--muted); } .app-badge { background:#fff7ed; color:#c2410c; border:1px solid #fed7aa; border-radius:999px; padding:7px 12px; font-weight:800; font-size:13px; } .app-card, .product-card, .overall-card, .note-card { border:1px solid var(--line); border-radius:18px; padding:18px; background:var(--surface); box-shadow:0 10px 24px rgba(15,23,42,.05); } .input-card { border:1px solid var(--line); border-radius:18px; padding:16px; background:var(--surface); box-shadow:0 10px 24px rgba(15,23,42,.05); } .product-layout { display:grid; grid-template-columns:132px 1fr; gap:16px; align-items:start; } .product-img { width:132px; height:166px; object-fit:cover; border-radius:16px; border:1px solid var(--line); background:#f8fafc; } .product-img.placeholder { display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:13px; } .panel-title { margin:0 0 12px; font-size:17px; font-weight:850; } .muted { color:var(--muted); } .meta-pills { display:flex; flex-wrap:wrap; gap:8px; margin:10px 0; } .meta-pills span { background:#f1f5f9; border:1px solid #e2e8f0; border-radius:999px; padding:6px 10px; font-size:13px; } .chip { display:inline-block; border-radius:999px; padding:5px 10px; font-weight:800; font-size:13px; } .conf { color:#334155; font-size:13px; margin-left:8px; } .small-label { color:#475569; font-size:13px; margin-bottom:8px; } .overall-card { margin-top:14px; background:linear-gradient(180deg,#ffffff 0%,#f8fafc 100%); } .overall-main { display:flex; align-items:center; gap:8px; margin-bottom:10px; } .prob-row { display:grid; grid-template-columns:82px 1fr 44px; gap:8px; align-items:center; font-size:13px; margin:7px 0; } .bar { height:9px; background:#e2e8f0; border-radius:999px; overflow:hidden; } .bar i { display:block; height:100%; background:linear-gradient(90deg,var(--accent),var(--accent-2)); } .aspect-grid { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:14px; } .aspect-card { border:1px solid var(--line); border-top:5px solid #64748b; border-radius:18px; padding:15px; background:white; min-height:172px; box-shadow:0 10px 22px rgba(15,23,42,.05); } .aspect-card.focus { border-top-color:var(--accent); box-shadow:0 18px 34px rgba(249,115,22,.13); } .aspect-card.dim { opacity:.58; } .aspect-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; } .aspect-head span, .source-line { color:#475569; font-size:12px; } .aspect-card p { margin:8px 0; color:#334155; font-size:13px; } .evidence-row { display:flex; flex-wrap:wrap; gap:5px; margin-top:8px; } .evidence-token { color:#1d4ed8; background:#eef2ff; border:1px solid #bfdbfe; border-radius:999px; padding:3px 8px; font-size:12px; } .review-box { border:1px dashed #cbd5e1; background:#f8fafc; border-radius:14px; padding:15px; line-height:1.65; } mark { background:#fde68a; color:#111827; border-radius:5px; padding:1px 4px; } .metric-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:14px; margin:8px 0 18px; } .metric { border:1px solid var(--line); border-radius:18px; padding:18px; background:white; box-shadow:0 10px 24px rgba(15,23,42,.05); } .metric span { display:block; color:#334155; font-size:13px; } .metric b { display:block; font-size:30px; margin:6px 0; letter-spacing:-.03em; } .metric small { color:#475569; } .table-wrap { border:1px solid var(--line); border-radius:16px; overflow:hidden; background:white; box-shadow:0 10px 24px rgba(15,23,42,.05); } .kv-table { width:100%; border-collapse:collapse; } .kv-table td { border-bottom:1px solid #e2e8f0; padding:11px 13px; } .kv-table td:first-child { width:220px; color:#334155; font-weight:800; background:#f8fafc; } button.primary, .gradio-button.primary { background:linear-gradient(135deg,var(--accent),var(--accent-2)) !important; border:none !important; color:white !important; font-weight:850 !important; border-radius:13px !important; min-height:44px !important; box-shadow:0 12px 24px rgba(249,115,22,.22) !important; } .gradio-tabs { border-radius:18px !important; } .tab-nav button { font-weight:750 !important; } .gradio-dataframe, .wrap.svelte-1lcyrx4, .dataframe-container { border-radius:16px !important; overflow:hidden !important; } textarea, input, select { border-radius:12px !important; } @media (max-width:920px) { .aspect-grid, .metric-grid { grid-template-columns:1fr; } .product-layout { grid-template-columns:1fr; } .product-img { width:100%; height:220px; } #app-hero h1 { font-size:26px; } } """ def build_app() -> gr.Blocks: categories = ["All"] + sorted({p["category"] for p in PRODUCTS}) tags = sorted({tag for p in PRODUCTS for tag in p.get("tags", [])}) with gr.Blocks(css=CSS, title="Clothing Sentiment Intelligence") as demo: gr.HTML( '
' '
BERT + Metadata Cross-Attention
' '

Clothing Review Sentiment Intelligence App

' '

Explore overall sentiment, six aspect-level opinions, metadata-driven risks, and research metrics from the 10W0715 experiment with a real Amazon product catalog.

' '
Consumer decision supportMerchant diagnosticsResearch dashboard
' '
' ) gr.HTML(_status_html()) with gr.Tabs(): with gr.Tab("Consumer App"): gr.HTML('

Shopping Review Analyzer

Select a product, inspect overall sentiment, and compare aspect-level strengths and weaknesses.

Consumer view
') with gr.Row(): with gr.Column(scale=4, elem_classes=["input-card"]): gr.HTML('
Product context
') product_select = gr.Dropdown(_product_names(), value=_product_names()[0], label="Choose a product") consumer_aspect = gr.Dropdown(["All"] + ASPECTS, value="All", label="Highlight aspect evidence") product_detail = gr.HTML() evidence_html = gr.HTML() with gr.Column(scale=7): gr.HTML('
Aspect sentiment cards
') aspect_html = gr.HTML() with gr.Accordion("Technical aspect result table", open=False): consumer_table = gr.Dataframe(headers=["Aspect", "Prediction", "Confidence", "Key review evidence"], datatype=["str", "str", "number", "str"], interactive=False) gr.HTML('

Product Finder

Filter real Amazon catalog products by metadata and target sentiment signal.

') with gr.Row(): with gr.Column(scale=3, elem_classes=["input-card"]): filter_aspect = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Target metric") filter_sentiment = gr.Radio(["Any", "Positive", "Negative", "Not_Mentioned", "Neutral"], value="Any", label="Preferred prediction") with gr.Column(scale=4, elem_classes=["input-card"]): filter_category = gr.Dropdown(categories, value="All", label="Category") filter_tags = gr.CheckboxGroup(tags, label="Required metadata tags") with gr.Column(scale=3, elem_classes=["input-card"]): min_rating = gr.Slider(3.0, 5.0, value=4.0, step=0.1, label="Minimum product rating") filter_btn = gr.Button("Find Matching Products", variant="primary") filter_summary = gr.HTML() filter_table = gr.Dataframe(headers=["Product", "Category", "Prediction", "Confidence", "Rating", "Price", "Metadata"], datatype=["str", "str", "str", "number", "number", "number", "str"], interactive=False, label="Filtered product candidates") product_select.change(consumer_product_view, [product_select, consumer_aspect], [product_detail, aspect_html, evidence_html, consumer_table]) consumer_aspect.change(consumer_product_view, [product_select, consumer_aspect], [product_detail, aspect_html, evidence_html, consumer_table]) filter_btn.click(filter_products, [filter_aspect, filter_sentiment, filter_category, filter_tags, min_rating], [filter_table, filter_summary]) with gr.Tab("Merchant App"): gr.HTML('

Merchant Sentiment Operations

Monitor products, screen new metadata, and test external customer reviews.

Business view
') with gr.Row(): with gr.Column(scale=4): gr.HTML('
Model basic information
') gr.HTML(model_info_html()) with gr.Column(scale=6, elem_classes=["input-card"]): gr.HTML('
Product score monitor
') merchant_metric = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Choose overall or aspect") refresh_scores = gr.Button("Refresh Product Scores", variant="primary") merchant_scores = gr.Dataframe(headers=["Product", "Category", "Metric", "Prediction", "Confidence", "Price", "Rating"], value=[["Click Refresh Product Scores", "", "", "", 0.0, 0.0, 0.0]], datatype=["str", "str", "str", "str", "number", "number", "number"], interactive=False) gr.HTML('

New Product Risk Screening

Use metadata to preview which aspect may need QA or product-page clarification.

') with gr.Row(): with gr.Column(scale=6, elem_classes=["input-card"]): new_features = gr.Textbox("Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", label="New product features") new_categories = gr.Textbox("Clothing > Women > Tops > T-Shirts", label="New product categories") with gr.Column(scale=4, elem_classes=["input-card"]): new_price = gr.Number(29.99, label="Price") new_rating = gr.Number(4.1, label="Expected or early average rating") new_count = gr.Number(35, label="Expected or early rating count") new_focus = gr.Dropdown(["All"] + ASPECTS, value="All", label="Focus aspect") screen_btn = gr.Button("Predict Metadata Risk", variant="primary") risk_summary = gr.HTML() risk_table = gr.Dataframe(headers=["Aspect", "Risk Level", "Model Signal", "Confidence", "Reason"], value=[["Click Predict Metadata Risk", "", "", 0.0, ""]], datatype=["str", "str", "str", "number", "str"], interactive=False) gr.HTML('

External Review Prediction

Paste any review and metadata to get overall and aspect-level predictions.

') with gr.Row(): with gr.Column(scale=4, elem_classes=["input-card"]): external_review = gr.Textbox("The fabric is soft and the color looks good, but it runs small and the zipper feels weak.", label="External customer review", lines=5) ext_features = gr.Textbox("Cotton Blend, Slim Fit, Zipper Closure", label="Product features") ext_categories = gr.Textbox("Clothing > Women > Jackets", label="Product categories") with gr.Row(): ext_price = gr.Number(39.99, label="Price") ext_rating = gr.Number(4.2, label="Average rating") ext_count = gr.Number(312, label="Rating count") ext_aspect = gr.Dropdown(["All"] + ASPECTS, value="All", label="Highlight aspect") external_btn = gr.Button("Analyze External Review", variant="primary") with gr.Column(scale=6): ext_overall = gr.HTML() ext_aspects = gr.HTML() with gr.Accordion("External review raw table", open=False): ext_table = gr.Dataframe(headers=["Aspect", "Prediction", "Confidence", "Key review evidence"], value=[["Click Analyze External Review", "", 0.0, ""]], datatype=["str", "str", "number", "str"], interactive=False) refresh_scores.click(merchant_product_scores, merchant_metric, merchant_scores) merchant_metric.change(merchant_product_scores, merchant_metric, merchant_scores) screen_btn.click(screen_new_product, [new_features, new_categories, new_price, new_rating, new_count, new_focus], [risk_summary, risk_table]) external_btn.click(external_review_predict, [external_review, ext_features, ext_categories, ext_price, ext_rating, ext_count, ext_aspect], [ext_overall, ext_aspects, ext_table]) with gr.Tab("Research Metrics"): gr.HTML('

Experiment Dashboard

Metrics are loaded from the bundled 10W0715 reports on the same held-out test split.

Research view
') research_cards = gr.HTML(research_cards_html()) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### Overall Sentiment Metrics") overall_table = gr.Dataframe(headers=["Model", "Macro-F1", "Accuracy"], value=overall_metric_rows(), interactive=False) with gr.Column(scale=1): gr.Markdown("### Ablation Comparison") ablation_table = gr.Dataframe(headers=["Variant", "Mean Macro-F1", "Mean Accuracy"], value=ablation_rows(), interactive=False) gr.Markdown("### Six-Aspect Comparison") aspect_table = gr.Dataframe(headers=["Aspect", "No-meta F1", "Proposed F1", "F1 Delta", "No-meta Acc", "Proposed Acc", "Acc Delta"], value=aspect_metric_rows(), interactive=False) refresh_research = gr.Button("Refresh Research Metrics", variant="primary") refresh_research.click(lambda: (research_cards_html(), overall_metric_rows(), aspect_metric_rows(), ablation_rows()), outputs=[research_cards, overall_table, aspect_table, ablation_table]) demo.load(consumer_product_view, [product_select, consumer_aspect], [product_detail, aspect_html, evidence_html, consumer_table]) return demo demo = build_app() if __name__ == "__main__": demo.launch(ssr_mode=False)