"""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'')
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'
' 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''
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)