"""Dual-interface Hugging Face Space for clothing aspect-level sentiment analysis.""" from __future__ import annotations import csv import html import json import re import traceback from functools import lru_cache from pathlib import Path import tempfile from typing import Any, Dict, List, Tuple from urllib.parse import quote 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 from src import config as cfg from src.inference import AspectPredictor ROOT = Path(__file__).resolve().parent REPORT_DIR = ROOT / "reports" DATA_DIR = ROOT / "data" 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"} FALLBACK_PRODUCTS = [ {"name": "Demo - 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"], "source": "curated demo fallback", "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": "Demo - 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"], "source": "curated demo fallback", "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": "Demo - Oversized Denim Jacket", "category": "Outerwear", "features": "Denim Cotton Blend, Oversized Fit, Button Front, Distressed Wash", "categories": "Clothing > Women > Jackets > Denim Jackets", "price": 58.00, "average_rating": 4.0, "rating_number": 447, "tags": ["denim", "oversized", "jacket"], "source": "curated demo fallback", "review": "The oversized style is cute and the color looks like the photo. It is heavier than expected and the buttons feel a little cheap."}, {"name": "Demo - 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"], "source": "curated demo fallback", "review": "The dress looks beautiful and the floral print is exactly as shown. The waist is a bit tight and the fabric wrinkles easily."}, {"name": "Demo - Fleece Pullover Hoodie", "category": "Tops", "features": "Cotton Polyester Fleece, Regular Fit, Kangaroo Pocket, Ribbed Cuffs", "categories": "Clothing > Unisex > Hoodies > Pullover Hoodies", "price": 42.99, "average_rating": 4.6, "rating_number": 2214, "tags": ["fleece", "hoodie", "warm"], "source": "curated demo fallback", "review": "Very warm and soft hoodie. The quality feels good for the price, though the sleeves are a little long for me."}, {"name": "Demo - Linen Button-Up Shirt", "category": "Tops", "features": "Linen Cotton Blend, Relaxed Fit, Button Front, Breathable Fabric", "categories": "Clothing > Men > Shirts > Button-Up Shirts", "price": 34.99, "average_rating": 3.9, "rating_number": 186, "tags": ["linen", "breathable", "shirt"], "source": "curated demo fallback", "review": "The shirt is breathable and stylish, but it wrinkles badly and the stitching near one button came loose."}, ] TAG_CANDIDATES = [ "cotton", "polyester", "linen", "denim", "fleece", "leather", "stretch", "soft", "breathable", "warm", "shirt", "dress", "jacket", "shorts", "sneakers", "wallet", "jewelry", "casual", "formal", "activewear", "print", "floral", "slim fit", "relaxed fit", "oversized", "high waist", "plus size", ] def _safe_float(value, default=0.0): try: if value in (None, ""): return default return float(value) except Exception: return default def _parse_numeric_blob(blob): text = str(blob or "") out = {} for key in ("price", "average_rating", "rating_number"): match = re.search(rf"{key}\s*=\s*([-+]?\d+(?:\.\d+)?)", text) if match: out[key] = _safe_float(match.group(1)) return out def _tags_from_text(text): low = str(text or "").lower() tags = [tag for tag in TAG_CANDIDATES if tag in low] return list(dict.fromkeys(tags))[:8] def _load_products_from_json(): path = DATA_DIR / "demo_products.json" try: if path.exists(): products = json.loads(path.read_text(encoding="utf-8")) if isinstance(products, list) and products: return [_coerce_product(p, i + 1) for i, p in enumerate(products)] except Exception: pass return [] def _load_products_from_catalog(): path = DATA_DIR / "product_catalog.json" try: if path.exists(): products = json.loads(path.read_text(encoding="utf-8")) if isinstance(products, list) and products: out = [] for i, item in enumerate(products): item = dict(item) item.setdefault("source", "real product catalog") out.append(_coerce_product(item, i + 1)) return out except Exception: pass return [] def _load_products_from_explanation_csv(): path = REPORT_DIR / "explanation_attention_summary.csv" if not path.exists(): return [] products = {} try: with path.open("r", encoding="utf-8", newline="") as fh: for row in csv.DictReader(fh): example_id = str(row.get("example") or "").strip() if not example_id or example_id in products: continue numeric = _parse_numeric_blob(row.get("numeric")) category = str(row.get("category") or "Clothing").strip() or "Clothing" features = str(row.get("features") or "").strip() categories = str(row.get("categories") or category).strip() review = str(row.get("text") or "").strip() source = "real held-out explanation example" item = { "name": f"Real Review {int(float(example_id)):02d} - {category}", "category": category, "features": features[:700], "categories": categories[:300], "price": numeric.get("price", 0.0), "average_rating": numeric.get("average_rating", _safe_float(row.get("rating"), 0.0)), "rating_number": numeric.get("rating_number", 0.0), "review": review, "source": source, } item["tags"] = _tags_from_text(" ".join([features, categories, review])) products[example_id] = _coerce_product(item, int(float(example_id))) except Exception: return [] return list(products.values()) def _coerce_product(item, idx=0): features = str(item.get("features") or item.get("features_text") or "") categories = str(item.get("categories") or item.get("categories_text") or "") review = str(item.get("review") or item.get("review_text") or "") category = str(item.get("category") or (categories.split(">")[-1].strip() if categories else "Clothing")) name = str(item.get("name") or item.get("title") or f"Product Example {idx:02d}") tags = item.get("tags") or _tags_from_text(" ".join([features, categories, review])) return { "name": name, "category": category, "features": features, "categories": categories, "image_url": str(item.get("image_url") or item.get("image") or item.get("main_image") or ""), "store": str(item.get("store") or item.get("brand") or "Amazon"), "parent_asin": str(item.get("parent_asin") or item.get("asin") or "-"), "price": _safe_float(item.get("price")), "average_rating": _safe_float(item.get("average_rating")), "rating_number": _safe_float(item.get("rating_number")), "review": review, "tags": list(tags) if isinstance(tags, (list, tuple)) else _tags_from_text(tags), "source": str(item.get("source") or "demo product"), } def _load_products(): products = _load_products_from_catalog() or _load_products_from_json() or _load_products_from_explanation_csv() if products: return products 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 _short_text(value: Any, limit: int = 220) -> str: text = re.sub(r"\s+", " ", str(value or "")).strip() if len(text) <= limit: return text return text[: limit - 3].rstrip() + "..." 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 {} def _load_csv_rows(name: str, columns: List[str], limit: int | None = None) -> List[List[Any]]: path = REPORT_DIR / name if not path.exists(): return [] rows = [] try: with path.open("r", encoding="utf-8", newline="") as fh: for row in csv.DictReader(fh): rows.append([row.get(col, "") for col in columns]) if limit and len(rows) >= limit: break except Exception: return [] return rows def _report_image(name: str): path = REPORT_DIR / name return str(path) if path.exists() else None def _write_csv_download(name: str, headers: List[str], rows): path = Path(tempfile.gettempdir()) / name normalized = _normalize_table_rows(rows) with path.open("w", encoding="utf-8-sig", newline="") as fh: writer = csv.writer(fh) writer.writerow(headers) writer.writerows(normalized) return str(path) def _download_update(path: str): return gr.update(value=path, visible=True) def _normalize_table_rows(rows) -> List[List[Any]]: if rows is None: return [] if hasattr(rows, "values") and hasattr(rows, "columns"): return rows.fillna("").values.tolist() if isinstance(rows, dict): data = rows.get("data") or rows.get("values") or [] return data if isinstance(data, list) else [] if isinstance(rows, tuple): rows = list(rows) if not isinstance(rows, list): return [] out = [] for row in rows: if isinstance(row, dict): out.append(list(row.values())) elif isinstance(row, (list, tuple)): out.append(list(row)) else: out.append([row]) return out @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 _product_names_for_category(category: str) -> List[str]: if category == "All": return _product_names() names = [p["name"] for p in PRODUCTS if p.get("category") == category] return names or _product_names() def _tags_for_category(category: str) -> List[str]: products = PRODUCTS if category == "All" else [p for p in PRODUCTS if p.get("category") == category] return sorted({tag for p in products for tag in p.get("tags", [])}) def _category_to_metadata_text(category: str) -> str: category = str(category or "").strip() if not category: return "Clothing" for product in PRODUCTS: if product.get("category") == category and product.get("categories"): return product["categories"] return f"Clothing > {category}" def update_product_choices(category: str): names = _product_names_for_category(category) return gr.update(choices=names, value=names[0] if names else None) def update_filter_tags(category: str): return gr.update(choices=_tags_for_category(category), value=[]) def consumer_category_view(category: str, selected_aspect: str): names = _product_names_for_category(category) product_name = names[0] if names else _product_names()[0] detail, aspects, evidence, rows = consumer_product_view(product_name, selected_aspect) return gr.update(choices=names, value=product_name), detail, aspects, evidence, rows def _get_product(name: str) -> Dict[str, Any]: return next((p for p in PRODUCTS if p["name"] == name), PRODUCTS[0]) def _product_visual(product: Dict[str, Any]): text = " ".join([ str(product.get("name", "")), str(product.get("category", "")), str(product.get("categories", "")), " ".join(map(str, product.get("tags", []))), ]).lower() if any(k in text for k in ["necklace", "bracelet", "jewelry", "strands", "identification"]): return "#fef3c7", "#92400e", '' if any(k in text for k in ["shoe", "sneaker", "boot", "slipper"]): return "#e0f2fe", "#075985", '' if any(k in text for k in ["wallet", "card case", "money"]): return "#f1f5f9", "#334155", '' if any(k in text for k in ["dress", "cocktail", "apron"]): return "#fce7f3", "#9d174d", '' if any(k in text for k in ["short", "leggings", "pants", "jeans"]): return "#dcfce7", "#166534", '' return "#eff6ff", "#1f2937", '' def _product_image_url(product: Dict[str, Any]) -> str: image_url = str(product.get("image_url") or "").strip() if image_url: return image_url category = _short_text(product.get("category") or "Clothing", 28) tag = _short_text(product.get("tags", ["fashion"])[0] if product.get("tags") else "fashion", 18) bg, ink, shape = _product_visual(product) svg = f""" {shape} {html.escape(category)} {html.escape(tag)} """ return "data:image/svg+xml;charset=utf-8," + quote(svg) 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": _category_to_metadata_text(categories), "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}
') fallback = '
Fallback inference mode used for this sample.
' if result.get("fallback") else "" return f'
Overall sentiment
{_chip(label)} confidence {conf:.2f}
{"".join(bars)}{fallback}
' def _join_aspects(items: List[str]) -> str: if not items: return "-" if len(items) == 1: return items[0] if len(items) == 2: return f"{items[0]} and {items[1]}" return ", ".join(items[:-1]) + f", and {items[-1]}" def _recommendation_sentence(result: Dict[str, Any]) -> str: details = result.get("aspect_details", {}) positive = [] negative = [] low_risk = [] for aspect in ASPECTS: d = details.get(aspect, {}) label = d.get("label", result.get("aspects", {}).get(aspect, "Unknown")) conf = _safe_float(d.get("confidence")) if label == "Positive": positive.append(aspect) elif label == "Negative": negative.append(aspect) elif label in {"Neutral", "Not_Mentioned"} or conf < 0.60: low_risk.append(aspect) if positive and negative: return ( f"This product is recommended because {_join_aspects(positive[:3])} " f"{'is' if len(positive[:3]) == 1 else 'are'} positive, while " f"{_join_aspects(negative[:2])} should be checked as potential risk." ) if positive: remaining = [a for a in ASPECTS if a not in positive] return ( f"This product is recommended because {_join_aspects(positive[:3])} " f"{'is' if len(positive[:3]) == 1 else 'are'} positive, while " f"{_join_aspects((low_risk or remaining)[:3])} has low risk." ) if negative: return ( f"This product is not a strong recommendation because " f"{_join_aspects(negative[:3])} shows negative sentiment risk." ) overall = result.get("overall", {}).get("label", "Neutral") return f"This product is a cautious recommendation because the overall signal is {overall} and no strong aspect risk dominates." def _recommendation_html(result: Dict[str, Any], product_name: str = "") -> str: sentence = _recommendation_sentence(result) title = f"Recommendation reason for {_esc(product_name)}" if product_name else "Recommendation reason" return ( f'
{title}
' f'{_esc(sentence)}' f'

This explanation is generated from the live model output: overall sentiment, six aspect labels, and confidence scores.

' ) 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 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_html = ( f'' ) 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)}' f'ASIN: {_esc(asin)}' f'Source: {_esc(product.get("source", "demo"))}' f'Price: ${_safe_float(product["price"]):.2f}' f'Rating: {_safe_float(product["average_rating"]):.1f}' f'Reviews: {int(_safe_float(product["rating_number"]))}
' f'' f'
{_overall_html(result)}' f'{_recommendation_html(result, product["name"])}
' ) 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"]) def filter_products(aspect: str, sentiment: str, category: str, tags: List[str], min_rating: float) -> Tuple[List[List[Any]], str]: rows = [] best = None for product in PRODUCTS: if category != "All" and product["category"] != category: continue if tags and not set(tags).issubset(set(product.get("tags", []))): continue if _safe_float(product["average_rating"]) < _safe_float(min_rating): continue 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"]) if best is None or score > best[0]: best = (score, product["name"], label, conf, result) summary = '
No product matched the current filters.
' if best: summary = ( f'
Best match: {_esc(best[1])} - ' f'{_esc(aspect)} is {_esc(best[2])} with confidence {best[3]:.2f}.' f'{_recommendation_html(best[4], best[1])}
' ) 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]]: csv_rows = _load_csv_rows("overall_model_comparison.csv", ["model", "macro_f1", "accuracy"]) if csv_rows: return [[r[0], _num(r[1]), _num(r[2])] for r in csv_rows] 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]]: csv_rows = _load_csv_rows( "aspect_level_proposed_vs_no_meta.csv", ["aspect", "acsa_no_meta_macro_f1", "proposed_macro_f1", "delta_macro_f1", "acsa_no_meta_accuracy", "proposed_accuracy", "delta_accuracy"], ) if csv_rows: return [[r[0], _num(r[1]), _num(r[2]), f"{_safe_float(r[3]):+.4f}", _num(r[4]), _num(r[5]), f"{_safe_float(r[6]):+.4f}"] for r in csv_rows] 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] def meta_source_rows() -> List[List[Any]]: rows = _load_csv_rows( "explanation_attention_meta_source_summary.csv", ["aspect", "top_meta_source", "source_share", "mean_top_meta_weight", "mean_attention_focus"], ) if not rows: rows = _load_csv_rows( "explanation_ig_meta_source_summary.csv", ["aspect", "top_meta_source", "source_share", "mean_top_meta_weight", "mean_attention_focus"], ) return [[r[0], r[1], _pct(r[2]), _num(r[3]), _num(r[4])] for r in rows] def report_asset_rows() -> List[List[Any]]: groups = [ ("Evaluation", "overall_model_comparison.csv, aspect_level_proposed_vs_no_meta.csv"), ("Confusion matrices", "confusion_matrix_proposed_overall_head.png and per-aspect PNGs"), ("Ablation", "ablation_summary.json, ablation_A1/A2/A3.json"), ("Visualization", "aspect_distribution.png and category_aspect_*_heatmap.png"), ("Explanation", "explanation_*_summary.csv and explanation_*_meta_source_summary.csv"), ] return [[name, assets] for name, assets in groups] def refresh_research_outputs(): return ( research_cards_html(), overall_metric_rows(), aspect_metric_rows(), ablation_rows(), meta_source_rows(), ) def merchant_product_scores(metric: str) -> List[List[Any]]: rows = [] for product in PRODUCTS: 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 merchant_aspect_overview() -> List[List[Any]]: rows = [] for aspect in ASPECTS: neg_count, top_name, top_conf, top_reason = 0, "-", 0.0, "-" for product in PRODUCTS: try: result = _predict_product(product["name"]) except Exception: continue d = result.get("aspect_details", {}).get(aspect, {}) if d.get("label") != "Negative": continue neg_count += 1 conf = _safe_float(d.get("confidence")) if conf >= top_conf: hits = _keyword_hits(product.get("review", ""), aspect) top_name = product["name"] top_conf = conf top_reason = ", ".join(hits) if hits else _short_text(product.get("review") or product.get("features"), 90) rows.append([aspect, neg_count, top_name, round(top_conf, 4), top_reason]) return rows def merchant_score_filter(aspect: str, prediction: str, category: str) -> List[List[Any]]: rows = [] for product in PRODUCTS: if category != "All" and product.get("category") != category: continue try: result = _predict_product(product["name"]) except Exception: continue d = result.get("overall", {}) if aspect == "Overall" else result.get("aspect_details", {}).get(aspect, {}) label = d.get("label", "Unknown") if prediction != "Any" and label != prediction: continue rows.append([ product["name"], product["category"], aspect, label, round(_safe_float(d.get("confidence")), 4), product["price"], product["average_rating"], ]) return rows or [["No matching products", category, aspect, prediction, 0.0, 0.0, 0.0]] def export_merchant_scores(rows): return _download_update(_write_csv_download( "merchant_product_scores.csv", ["Product", "Category", "Metric", "Prediction", "Confidence", "Price", "Rating"], rows, )) def negative_product_spotlight(limit: int = 6) -> str: cards = [] used_products = set() for aspect in ASPECTS: candidates = [] for product in PRODUCTS: try: result = _predict_product(product["name"]) except Exception: continue d = result.get("aspect_details", {}).get(aspect, {}) if d.get("label") != "Negative": continue conf = _safe_float(d.get("confidence")) hits = _keyword_hits(product.get("review", ""), aspect) reason = ", ".join(hits[:3]) if hits else _short_text(product.get("review") or product.get("features"), 48) candidates.append((conf, product, reason)) candidates.sort(key=lambda x: x[0], reverse=True) best = next((item for item in candidates if item[1]["name"] not in used_products), None) if best is None and candidates: best = candidates[0] if best is None: cards.append( f'

{aspect}

' '
No strong negative sample
' ) continue conf, product, reason = best used_products.add(product["name"]) img = f'{_esc(product[' cards.append( f'

{aspect}

' f'
{img}
{_esc(_short_text(product["name"], 42))}' f'
{_esc(product["category"])} | conf {conf:.2f}
' f'

{_esc(_short_text(reason, 56))}

' ) return '
' + "".join(cards[:limit]) + '
' 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 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 def import_new_product_payload(file_obj): if not file_obj: return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() path = Path(getattr(file_obj, "name", file_obj)) try: if path.suffix.lower() == ".json": data = json.loads(path.read_text(encoding="utf-8")) else: with path.open("r", encoding="utf-8-sig", newline="") as fh: data = next(csv.DictReader(fh), {}) except Exception: data = {} return ( data.get("features") or data.get("features_text") or "", data.get("categories") or data.get("categories_text") or "", _safe_float(data.get("price"), 0.0), _safe_float(data.get("average_rating") or data.get("rating"), 4.0), _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0), data.get("focus_aspect") or data.get("focus") or "All", ) def export_risk_rows(rows): return _download_update(_write_csv_download( "new_product_metadata_risk.csv", ["Aspect", "Risk Level", "Model Signal", "Confidence", "Reason"], rows, )) def _read_uploaded_records(file_obj) -> List[Dict[str, Any]]: if not file_obj: return [] path = Path(getattr(file_obj, "name", file_obj)) try: if path.suffix.lower() == ".json": data = json.loads(path.read_text(encoding="utf-8")) if isinstance(data, dict): data = data.get("items") or data.get("data") or [data] return data if isinstance(data, list) else [] with path.open("r", encoding="utf-8-sig", newline="") as fh: return list(csv.DictReader(fh)) except Exception: return [] def download_new_product_template(): rows = [[ "Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", "T-Shirts", 29.99, 4.1, 35, "All", ]] return _download_update(_write_csv_download( "new_product_metadata_template.csv", ["features", "category", "price", "average_rating", "rating_number", "focus_aspect"], rows, )) def batch_screen_new_products(file_obj) -> Tuple[str, List[List[Any]]]: records = _read_uploaded_records(file_obj) if not records: return '
Upload a CSV or JSON file first.
', [] rows = [] for i, data in enumerate(records, 1): features = data.get("features") or data.get("features_text") or "" categories = data.get("category") or data.get("categories") or data.get("categories_text") or "" price = _safe_float(data.get("price"), 0.0) rating = _safe_float(data.get("average_rating") or data.get("rating"), 4.0) count = _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0) focus = data.get("focus_aspect") or data.get("focus") or "All" _, detail_rows = screen_new_product(features, categories, price, rating, count, focus) high = [r[0] for r in detail_rows if r[1] == "High"] rows.append([ f"Product {i}", "High" if high else "Low", focus, len(high), f"{_short_text(categories, 55)} | high-risk aspects: {', '.join(high) or 'None'}", ]) return f'
Batch screening completed: {len(rows)} products analyzed.
', rows 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 import_external_review_payload(file_obj): if not file_obj: return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() path = Path(getattr(file_obj, "name", file_obj)) try: if path.suffix.lower() == ".json": data = json.loads(path.read_text(encoding="utf-8")) else: with path.open("r", encoding="utf-8-sig", newline="") as fh: data = next(csv.DictReader(fh), {}) except Exception: data = {} return ( data.get("review") or data.get("review_text") or "", data.get("features") or data.get("features_text") or "", data.get("categories") or data.get("categories_text") or "", _safe_float(data.get("price"), 0.0), _safe_float(data.get("average_rating") or data.get("rating"), 4.0), _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0), data.get("highlight_aspect") or data.get("aspect") or "All", ) def export_external_rows(rows): return _download_update(_write_csv_download( "external_review_prediction.csv", ["Aspect", "Prediction", "Confidence", "Key review evidence"], rows, )) def download_external_review_template(): rows = [[ "The fabric is soft and the color looks good, but it runs small.", "Cotton Blend, Slim Fit, Zipper Closure", "Jackets", 39.99, 4.2, 312, "All", ]] return _download_update(_write_csv_download( "external_review_template.csv", ["review", "features", "category", "price", "average_rating", "rating_number", "highlight_aspect"], rows, )) def batch_external_review_predict(file_obj) -> Tuple[str, str, List[List[Any]]]: records = _read_uploaded_records(file_obj) if not records: return '
Upload a CSV or JSON file first.
', "", [] rows = [] for i, data in enumerate(records, 1): review = data.get("review") or data.get("review_text") or "" features = data.get("features") or data.get("features_text") or "" categories = data.get("category") or data.get("categories") or data.get("categories_text") or "" price = _safe_float(data.get("price"), 0.0) rating = _safe_float(data.get("average_rating") or data.get("rating"), 4.0) count = _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0) aspect = data.get("highlight_aspect") or data.get("aspect") or "All" overall_html, _, detail_rows = external_review_predict(review, features, categories, price, rating, count, aspect) negative = [r[0] for r in detail_rows if r[1] == "Negative"] rows.append([ f"Review {i}", f"Negative: {', '.join(negative)}" if negative else "No major negative", len(negative), f"{_short_text(review, 70)} | {_short_text(categories, 45)}", ]) return f'
Batch review prediction completed: {len(rows)} reviews analyzed.
', "", rows def toggle_analysis_mode(mode: str): single = mode.startswith("Single") return gr.update(visible=single), gr.update(visible=not single) 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. 10W0716 checkpoint and metadata encoder are available.
' CSS = """ :root { --accent:#f97316; --ink:#0f172a; --muted:#475569; --line:#dbe3ef; --panel:#ffffff; } .gradio-container { max-width:1240px !important; margin:auto !important; color:var(--ink); } #hero { border:1px solid #bfdbfe; background:#eff6ff; border-left:6px solid var(--accent); border-radius:8px; padding:18px 22px; margin:8px 0 14px; box-shadow:0 1px 6px rgba(15,23,42,.06); } #hero .eyebrow { margin:0 0 7px; color:#9a3412; font-size:13px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; } #hero h1 { margin:0 0 8px; font-size:28px; line-height:1.15; color:#0f172a; font-weight:800; } #hero p { margin:0; color:#1e3a8a; max-width:920px; } #hero .hero-pills { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; } #hero .hero-pills span { color:#1e293b; background:#ffffff; border:1px solid #bfdbfe; border-radius:999px; padding:5px 10px; font-size:13px; font-weight:600; } button.primary, .gradio-button.primary { background:var(--accent) !important; border-color:var(--accent) !important; color:white !important; font-weight:700 !important; } .status { border-radius:8px; padding:10px 13px; margin:6px 0 14px; border:1px solid var(--line); } .status.ok { background:#ecfdf5; border-color:#86efac; color:#065f46; } .status.bad { background:#fff1f2; border-color:#fda4af; color:#991b1b; } .product-card, .overall-card, .note-card { border:1px solid var(--line); border-radius:8px; padding:16px; background:white; } .product-layout { display:grid; grid-template-columns:96px 1fr; gap:12px; align-items:start; } .product-img { width:96px; height:118px; object-fit:cover; border-radius:8px; border:1px solid var(--line); background:#f8fafc; } .product-card h3 { margin:0 0 6px; font-size:18px; line-height:1.25; } .product-card p { margin:7px 0; } .metadata-summary { color:#334155; font-size:13px; line-height:1.45; } .decision-layout { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:10px; align-items:stretch; } .decision-layout .overall-card, .decision-layout .recommendation-card { margin:0; padding:12px; } .decision-layout .recommendation-card .small-label { display:none; } .decision-layout .recommendation-card p { display:none; } .recommendation-card { border:1px solid #fed7aa; border-left:5px solid var(--accent); background:#fff7ed; border-radius:8px; padding:13px 14px; margin:12px 0; } .recommendation-card p { margin:7px 0 0; } .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:5px 9px; font-size:13px; } .chip { display:inline-block; border-radius:999px; padding:5px 10px; font-weight:700; font-size:13px; } .conf { color:#334155; font-size:13px; margin-left:8px; } .small-label { color:#475569; font-size:13px; margin-bottom:8px; } .prob-row { display:grid; grid-template-columns:82px 1fr 44px; gap:8px; align-items:center; font-size:13px; margin:6px 0; } .bar { height:8px; background:#e2e8f0; border-radius:999px; overflow:hidden; } .bar i { display:block; height:100%; background:var(--accent); } .aspect-grid { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px; } .aspect-card { border:1px solid var(--line); border-top:4px solid #64748b; border-radius:8px; padding:12px; background:white; min-height:145px; } .aspect-card.focus { border-top-color:var(--accent); box-shadow:0 2px 10px rgba(15,23,42,.08); } .aspect-card.dim { opacity:.66; } .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:8px; padding:14px; line-height:1.6; } mark { background:#fde68a; color:#111827; border-radius:4px; padding:1px 3px; } .metric-grid { display:grid; grid-template-columns:repeat(3, 1fr); gap:12px; margin:8px 0 12px; } .metric { border:1px solid var(--line); border-radius:8px; padding:14px; background:white; } .metric span { display:block; color:#334155; font-size:13px; } .metric b { display:block; font-size:28px; margin:6px 0; } .metric small { color:#475569; } .table-wrap { border:1px solid var(--line); border-radius:8px; overflow:hidden; background:white; } .kv-table { width:100%; border-collapse:collapse; } .kv-table td { border-bottom:1px solid #e2e8f0; padding:9px 12px; } .kv-table td:first-child { width:220px; color:#334155; font-weight:700; background:#f8fafc; } .compact-note { color:#475569; font-size:13px; margin:4px 0 10px; } .module-head { display:flex; justify-content:space-between; align-items:center; gap:12px; margin:0 0 10px; } .info-tip { position:relative; display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; border-radius:999px; border:1px solid #bfdbfe; background:#eff6ff; color:#1e40af; font-weight:800; cursor:help; } .info-tip .tip-content { display:none; position:absolute; right:0; top:30px; z-index:20; width:420px; max-width:80vw; background:white; border:1px solid var(--line); border-radius:8px; padding:10px; box-shadow:0 12px 30px rgba(15,23,42,.16); } .info-tip:hover .tip-content { display:block; } .negative-grid { display:grid; grid-template-columns:repeat(6, minmax(0, 1fr)); gap:10px; margin:8px 0 14px; } .negative-card { display:grid; grid-template-columns:64px 1fr; gap:10px; border:1px solid #fecaca; border-left:4px solid #ef4444; border-radius:8px; padding:10px; background:#fffafa; } .negative-card p { margin:5px 0 0; color:#334155; font-size:13px; } .compact-negative { display:block; min-height:178px; } .compact-negative h4 { margin:0 0 8px; color:#b91c1c; font-size:14px; letter-spacing:.02em; } .negative-body { display:grid; grid-template-columns:54px 1fr; gap:8px; align-items:start; } .negative-body b { display:block; font-size:13px; line-height:1.25; } .negative-body p { font-size:12px; line-height:1.35; } .empty-negative { color:#64748b; font-size:12px; border:1px dashed #fecaca; border-radius:8px; padding:14px 8px; background:white; } .mini-product-img { width:64px; height:76px; object-fit:cover; border-radius:6px; border:1px solid var(--line); background:#f8fafc; } .negative-body .mini-product-img { width:54px; height:64px; } @media (max-width:1100px) { .negative-grid { grid-template-columns:repeat(3, minmax(0, 1fr)); } } @media (max-width:860px) { .aspect-grid, .metric-grid, .product-layout, .decision-layout, .negative-grid { grid-template-columns:1fr; } .product-img { width:100%; height:180px; } } """ def build_app() -> gr.Blocks: categories = ["All"] + sorted({p["category"] for p in PRODUCTS}) merchant_categories = categories[1:] or ["Clothing"] default_merchant_category = merchant_categories[0] tags = sorted({tag for p in PRODUCTS for tag in p.get("tags", [])}) with gr.Blocks(css=CSS, title="Clothing Sentiment Analysis") as demo: gr.HTML('
BERT + Metadata Cross-Attention

Clothing Review Sentiment Intelligence App

Explore overall sentiment, six aspect-level opinions, metadata-driven risks, and updated 10W experiment reports in a compact customer decision-support prototype.

Consumer decision supportMerchant diagnosticsResearch dashboard
') gr.HTML(_status_html()) with gr.Tabs(): with gr.Tab("Consumer Interface"): with gr.Row(): with gr.Column(scale=4): consumer_category = gr.Dropdown(categories, value="All", label="Choose category") 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() with gr.Column(scale=6): aspect_html = gr.HTML() evidence_html = gr.HTML() consumer_table = gr.Dataframe(headers=["Aspect", "Prediction", "Confidence", "Key review evidence"], datatype=["str", "str", "number", "str"], label="Aspect-level result table", interactive=False) with gr.Accordion("Product Finder filters", open=False): gr.HTML('
Optional: filter products by aspect sentiment, category, tags, and minimum rating.
') with gr.Row(): with gr.Column(scale=1): 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") filter_category = gr.Dropdown(categories, value="All", label="Category") with gr.Column(scale=1): filter_tags = gr.CheckboxGroup(tags, label="Required metadata tags") min_rating = gr.Slider(3.0, 5.0, value=4.0, step=0.1, label="Minimum product rating") filter_btn = gr.Button("Filter 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") consumer_category.change(consumer_category_view, [consumer_category, consumer_aspect], [product_select, product_detail, aspect_html, evidence_html, consumer_table]) 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_category.change(update_filter_tags, filter_category, filter_tags) filter_btn.click(filter_products, [filter_aspect, filter_sentiment, filter_category, filter_tags, min_rating], [filter_table, filter_summary]) with gr.Tab("Merchant Interface"): gr.HTML('

Product Score Monitor

i' + model_info_html() + '
') gr.HTML('
Most negative products across the six aspects
') negative_spotlight = gr.HTML('
Loading most negative products...
') with gr.Accordion("Score filters and export", open=True): with gr.Row(): merchant_metric = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Aspect") merchant_prediction = gr.Dropdown(["Any", "Positive", "Negative", "Not_Mentioned", "Neutral"], value="Any", label="Prediction") merchant_category = gr.Dropdown(categories, value="All", label="Category") merchant_scores = gr.Dataframe(headers=["Product", "Category", "Metric", "Prediction", "Confidence", "Price", "Rating"], value=[["Click Apply Score Filter", "", "", "", 0.0, 0.0, 0.0]], datatype=["str", "str", "str", "str", "number", "number", "number"], interactive=False) with gr.Row(): refresh_scores = gr.Button("Apply Score Filter", variant="primary") export_scores = gr.Button("Export Score List") merchant_scores_file = gr.File(label="Downloaded score CSV", interactive=False, visible=False) with gr.Accordion("New Product Metadata Risk Screening", open=False): risk_mode = gr.Radio(["Single product analysis", "Batch import analysis"], value="Single product analysis", label="Analysis mode") with gr.Row(): with gr.Column(scale=1): with gr.Group(visible=True) as risk_single_group: new_features = gr.Textbox("Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", label="New product features", lines=4) new_categories = gr.Dropdown(merchant_categories, value=default_merchant_category, label="New product category") with gr.Row(): new_price = gr.Number(29.99, label="Price") new_rating = gr.Number(4.1, label="Expected or early average rating") with gr.Row(): 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") with gr.Group(visible=False) as risk_batch_group: new_import = gr.File(label="Import product metadata JSON/CSV") with gr.Row(): new_template = gr.Button("Download Import Template") batch_screen_btn = gr.Button("Batch Analyze Metadata", variant="primary") new_template_file = gr.File(label="Template CSV", interactive=False, visible=False) with gr.Column(scale=1): 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) export_risk = gr.Button("Export Risk Result") risk_file = gr.File(label="Downloaded risk CSV", interactive=False, visible=False) with gr.Accordion("External Review Prediction", open=False): ext_mode = gr.Radio(["Single product analysis", "Batch import analysis"], value="Single product analysis", label="Analysis mode") with gr.Row(): with gr.Column(scale=1): with gr.Group(visible=True) as ext_single_group: 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) with gr.Row(): ext_features = gr.Textbox("Cotton Blend, Slim Fit, Zipper Closure", label="Product features", lines=3) ext_categories = gr.Dropdown(merchant_categories, value=default_merchant_category, label="Product category") with gr.Row(): ext_price = gr.Number(39.99, label="Price") ext_rating = gr.Number(4.2, label="Average rating") with gr.Row(): 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.Group(visible=False) as ext_batch_group: ext_import = gr.File(label="Import review metadata JSON/CSV") with gr.Row(): ext_template = gr.Button("Download Import Template") batch_external_btn = gr.Button("Batch Analyze Reviews", variant="primary") ext_template_file = gr.File(label="Template CSV", interactive=False, visible=False) with gr.Column(scale=1): ext_overall = gr.HTML() ext_aspects = gr.HTML() 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) export_ext = gr.Button("Export Review Result") ext_file = gr.File(label="Downloaded review CSV", interactive=False, visible=False) refresh_scores.click(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores) export_scores.click(export_merchant_scores, merchant_scores, merchant_scores_file) merchant_metric.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores) merchant_prediction.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores) merchant_category.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores) risk_mode.change(toggle_analysis_mode, risk_mode, [risk_single_group, risk_batch_group]) screen_btn.click(screen_new_product, [new_features, new_categories, new_price, new_rating, new_count, new_focus], [risk_summary, risk_table]) new_template.click(download_new_product_template, None, new_template_file) batch_screen_btn.click(batch_screen_new_products, new_import, [risk_summary, risk_table]) export_risk.click(export_risk_rows, risk_table, risk_file) ext_mode.change(toggle_analysis_mode, ext_mode, [ext_single_group, ext_batch_group]) 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]) ext_template.click(download_external_review_template, None, ext_template_file) batch_external_btn.click(batch_external_review_predict, ext_import, [ext_overall, ext_aspects, ext_table]) export_ext.click(export_external_rows, ext_table, ext_file) with gr.Tab("Research Metrics"): gr.Markdown("Metrics are loaded from the updated 10W experiment reports. Tables use CSV outputs when available, with JSON fallback.") research_cards = gr.HTML(research_cards_html()) with gr.Accordion("Performance comparison", open=True): overall_table = gr.Dataframe(headers=["Model", "Macro-F1", "Accuracy"], value=overall_metric_rows(), interactive=False) 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) with gr.Accordion("Ablation and metadata source summary", open=False): ablation_table = gr.Dataframe(headers=["Variant", "Mean Macro-F1", "Mean Accuracy"], value=ablation_rows(), interactive=False) meta_source_table = gr.Dataframe(headers=["Aspect", "Top Metadata Source", "Source Share", "Mean Weight", "Mean Focus"], value=meta_source_rows(), interactive=False) with gr.Accordion("Figures from updated reports", open=False): with gr.Row(): gr.Image(value=_report_image("aspect_distribution.png"), label="Aspect sentiment distribution", interactive=False) gr.Image(value=_report_image("category_aspect_negative_heatmap.png"), label="Negative share by category x aspect", interactive=False) with gr.Row(): gr.Image(value=_report_image("category_aspect_positive_heatmap.png"), label="Positive share by category x aspect", interactive=False) gr.Image(value=_report_image("confusion_matrix_proposed_overall_head.png"), label="Proposed overall confusion matrix", interactive=False) with gr.Accordion("Report file groups", open=False): gr.Dataframe(headers=["Group", "Files"], value=report_asset_rows(), interactive=False) refresh_research = gr.Button("Refresh Research Metrics", variant="primary") refresh_research.click(refresh_research_outputs, outputs=[research_cards, overall_table, aspect_table, ablation_table, meta_source_table]) demo.load(consumer_product_view, [product_select, consumer_aspect], [product_detail, aspect_html, evidence_html, consumer_table]) demo.load(negative_product_spotlight, outputs=negative_spotlight) demo.load(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores) demo.load(filter_products, [filter_aspect, filter_sentiment, filter_category, filter_tags, min_rating], [filter_table, filter_summary]) return demo demo = build_app() if __name__ == "__main__": demo.launch(ssr_mode=False)