Sina1138 commited on
Commit ·
894cdf6
1
Parent(s): 8bcd5eb
Add centralized sentence filtering for RSA and polarity scoring; enhance interactive review processor to filter noise sentences and improve summary card formatting
Browse files- dependencies/sentence_filter.py +185 -0
- interface/Demo.py +123 -38
- interface/interactive_processor.py +18 -21
dependencies/sentence_filter.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Centralized sentence filtering for RSA and polarity/topic scoring.
|
| 3 |
+
|
| 4 |
+
Filters out structural noise (headers, citations, timestamps, reference sections,
|
| 5 |
+
short fragments) so that only meaningful opinion sentences are scored and highlighted.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import re
|
| 9 |
+
from typing import List, Optional
|
| 10 |
+
|
| 11 |
+
# ---------------------------------------------------------------------------
|
| 12 |
+
# Tunable constants
|
| 13 |
+
# ---------------------------------------------------------------------------
|
| 14 |
+
MIN_WORDS = 5 # Minimum word count for a sentence to be considered meaningful
|
| 15 |
+
|
| 16 |
+
HIGHLIGHT_THRESHOLD = 0.15 # Absolute score below which sentences get no color
|
| 17 |
+
|
| 18 |
+
# ---------------------------------------------------------------------------
|
| 19 |
+
# Compiled regex patterns
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
|
| 22 |
+
# Standalone section headers (ported from interactive_processor._HEADER_RE)
|
| 23 |
+
_HEADER_RE = re.compile(
|
| 24 |
+
r'^(\*{1,2}|#{1,3}\s*)?(summary|strengths?|weaknesses?|questions?|limitations?|minor|'
|
| 25 |
+
r'rating|confidence|correctness|clarity|originality|significance|'
|
| 26 |
+
r'pros?|cons?|comments?|suggestions?|conclusion|recommendation|'
|
| 27 |
+
r'contribution|technical\s+quality|presentation|reproducibility|'
|
| 28 |
+
r'novelty|experiments?|related\s+work|other|additional)\s*:?(\*{1,2})?$',
|
| 29 |
+
re.IGNORECASE
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Header keyword prefix followed by actual content (e.g. "Paper Summary: This paper...")
|
| 33 |
+
_HEADER_PREFIX_RE = re.compile(
|
| 34 |
+
r'^(\*{1,2}|#{1,3}\s*)?(paper\s+summary|summary|strengths?|weaknesses?|questions?|'
|
| 35 |
+
r'limitations?|minor|comments?|suggestions?|conclusion|recommendation|'
|
| 36 |
+
r'contribution|pros?|cons?|review\s+summary|overall\s+assessment)\s*:\s*(\*{1,2})?\s*',
|
| 37 |
+
re.IGNORECASE
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Citation-only fragments: "Hu et al.:", "See et al., 2017:", "(Author et al., Year)"
|
| 41 |
+
_CITATION_ONLY_RE = re.compile(
|
| 42 |
+
r'^\s*\(?\w[\w\s,\.\-]*?et\s+al\.?\s*[\),;:.]?\s*$',
|
| 43 |
+
re.IGNORECASE
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# Edit timestamps: "EDIT Nov. 20, 2019:", "UPDATE: ..."
|
| 47 |
+
_EDIT_TIMESTAMP_RE = re.compile(
|
| 48 |
+
r'^(EDIT|UPDATE)\s+.{0,40}:\s*$',
|
| 49 |
+
re.IGNORECASE
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Reference lines: "[1] Author...", "[12] ..."
|
| 53 |
+
_REFERENCE_LINE_RE = re.compile(r'^\[\d+\]')
|
| 54 |
+
|
| 55 |
+
# References section header
|
| 56 |
+
_REFERENCES_HEADER_RE = re.compile(
|
| 57 |
+
r'^(\*{1,2}|#{1,3}\s*)?references?\s*:?\s*(\*{1,2})?$',
|
| 58 |
+
re.IGNORECASE
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# Rating/confidence metadata: "Rating: 6", "Confidence: 4/5", "Soundness: 3"
|
| 62 |
+
_RATING_RE = re.compile(
|
| 63 |
+
r'^(rating|confidence|overall\s+score|soundness|presentation|contribution|'
|
| 64 |
+
r'correctness|significance|originality|clarity)\s*:\s*\d',
|
| 65 |
+
re.IGNORECASE
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ---------------------------------------------------------------------------
|
| 70 |
+
# Public API
|
| 71 |
+
# ---------------------------------------------------------------------------
|
| 72 |
+
|
| 73 |
+
def is_section_header(sentence: str) -> bool:
|
| 74 |
+
"""Return True if sentence is a standalone structural section header."""
|
| 75 |
+
return bool(_HEADER_RE.match(sentence.strip()))
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def is_noise_sentence(sentence: str) -> bool:
|
| 79 |
+
"""
|
| 80 |
+
Return True if the sentence is structural noise that should be excluded
|
| 81 |
+
from RSA / polarity / topic scoring.
|
| 82 |
+
|
| 83 |
+
Catches: standalone headers, citation-only fragments, edit timestamps,
|
| 84 |
+
reference lines, rating metadata, and short fragments (< MIN_WORDS).
|
| 85 |
+
"""
|
| 86 |
+
s = sentence.strip()
|
| 87 |
+
if not s:
|
| 88 |
+
return True
|
| 89 |
+
|
| 90 |
+
# Standalone section header
|
| 91 |
+
if _HEADER_RE.match(s):
|
| 92 |
+
return True
|
| 93 |
+
|
| 94 |
+
# Citation-only fragment
|
| 95 |
+
if _CITATION_ONLY_RE.match(s):
|
| 96 |
+
return True
|
| 97 |
+
|
| 98 |
+
# Edit timestamp
|
| 99 |
+
if _EDIT_TIMESTAMP_RE.match(s):
|
| 100 |
+
return True
|
| 101 |
+
|
| 102 |
+
# Reference line
|
| 103 |
+
if _REFERENCE_LINE_RE.match(s):
|
| 104 |
+
return True
|
| 105 |
+
|
| 106 |
+
# References section header
|
| 107 |
+
if _REFERENCES_HEADER_RE.match(s):
|
| 108 |
+
return True
|
| 109 |
+
|
| 110 |
+
# Rating/confidence metadata
|
| 111 |
+
if _RATING_RE.match(s):
|
| 112 |
+
return True
|
| 113 |
+
|
| 114 |
+
# Too short to be a meaningful opinion
|
| 115 |
+
if len(s.split()) < MIN_WORDS:
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
return False
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def strip_header_prefix(sentence: str) -> str:
|
| 122 |
+
"""
|
| 123 |
+
Strip structural header prefixes from sentences that mix header + content.
|
| 124 |
+
|
| 125 |
+
E.g. "Paper Summary: This paper proposes..." → "This paper proposes..."
|
| 126 |
+
Returns the original sentence if no prefix is found.
|
| 127 |
+
"""
|
| 128 |
+
s = sentence.strip()
|
| 129 |
+
m = _HEADER_PREFIX_RE.match(s)
|
| 130 |
+
if m:
|
| 131 |
+
remainder = s[m.end():].strip()
|
| 132 |
+
# Only strip if there's substantial content after the prefix
|
| 133 |
+
if len(remainder.split()) >= MIN_WORDS:
|
| 134 |
+
return remainder
|
| 135 |
+
return s
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def detect_references_start(sentences: List[str]) -> Optional[int]:
|
| 139 |
+
"""
|
| 140 |
+
Return the index where the references section begins, or None.
|
| 141 |
+
|
| 142 |
+
Heuristic: looks for a "References" header or the first `[1]`-style citation
|
| 143 |
+
that is followed by more `[N]` lines (to avoid false positives on single
|
| 144 |
+
bracketed numbers in review text).
|
| 145 |
+
"""
|
| 146 |
+
for i, s in enumerate(sentences):
|
| 147 |
+
stripped = s.strip()
|
| 148 |
+
# Explicit "References" header
|
| 149 |
+
if _REFERENCES_HEADER_RE.match(stripped):
|
| 150 |
+
return i
|
| 151 |
+
# First [1]-style line followed by at least one more [N] line
|
| 152 |
+
if _REFERENCE_LINE_RE.match(stripped):
|
| 153 |
+
following_refs = sum(
|
| 154 |
+
1 for j in range(i + 1, min(i + 4, len(sentences)))
|
| 155 |
+
if _REFERENCE_LINE_RE.match(sentences[j].strip())
|
| 156 |
+
)
|
| 157 |
+
if following_refs >= 1:
|
| 158 |
+
return i
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def filter_and_clean_sentences(sentences: List[str]) -> List[str]:
|
| 163 |
+
"""
|
| 164 |
+
Full filtering pipeline: truncate at references, strip header prefixes,
|
| 165 |
+
remove noise sentences.
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
sentences: Raw tokenized sentences from a single review or combined reviews.
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
Cleaned sentence list ready for scoring.
|
| 172 |
+
"""
|
| 173 |
+
# 1. Truncate at references section
|
| 174 |
+
ref_start = detect_references_start(sentences)
|
| 175 |
+
if ref_start is not None:
|
| 176 |
+
sentences = sentences[:ref_start]
|
| 177 |
+
|
| 178 |
+
# 2. Strip header prefixes and filter noise
|
| 179 |
+
result = []
|
| 180 |
+
for s in sentences:
|
| 181 |
+
cleaned = strip_header_prefix(s)
|
| 182 |
+
if not is_noise_sentence(cleaned):
|
| 183 |
+
result.append(cleaned)
|
| 184 |
+
|
| 185 |
+
return result
|
interface/Demo.py
CHANGED
|
@@ -16,8 +16,75 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|
| 16 |
# Lower = more vivid colors (0.2 = very strong, 1.0 = no amplification).
|
| 17 |
# Asymmetric: unique/red (positive) is amplified less than common/blue (negative)
|
| 18 |
# to avoid overwhelming red when most sentences are unique.
|
| 19 |
-
AGREEMENT_AMP_UNIQUE = 0.
|
| 20 |
-
AGREEMENT_AMP_COMMON = 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Auto-detect the preprocessed dataset CSV
|
| 23 |
def _find_preprocessed_csv() -> Path:
|
|
@@ -190,7 +257,11 @@ def _status_html(msg, kind="success"):
|
|
| 190 |
|
| 191 |
# ===== INTERACTIVE TAB: GLOBAL PROCESSOR INITIALIZATION =====
|
| 192 |
# Initialize once at module load to avoid reloading models
|
| 193 |
-
from interface.interactive_processor import InteractiveReviewProcessor
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
_interactive_processor = None
|
| 195 |
|
| 196 |
def get_interactive_processor():
|
|
@@ -381,7 +452,9 @@ def process_interactive_reviews_fast(text1: str, text2: str, text3: str, text4:
|
|
| 381 |
if len(sentence_lists) < 2:
|
| 382 |
raise ValueError("At least two reviews must have valid sentences")
|
| 383 |
|
| 384 |
-
all_sentences =
|
|
|
|
|
|
|
| 385 |
|
| 386 |
# Step 3-4: Polarity + Topic (parallelize both models)
|
| 387 |
progress(0.30, desc="Predicting polarity and topics (parallel)...")
|
|
@@ -455,12 +528,14 @@ def compute_rsa_in_background(rsa_state: Dict, current_focus: str, progress=gr.P
|
|
| 455 |
progress(0.50, desc="Running RSA reranking...")
|
| 456 |
consensuality_map = processor.predict_consensuality(*active_texts)
|
| 457 |
|
| 458 |
-
#
|
| 459 |
if consensuality_map:
|
| 460 |
import pandas as _pd
|
| 461 |
scores_series = _pd.Series(consensuality_map)
|
| 462 |
-
|
| 463 |
-
|
|
|
|
|
|
|
| 464 |
else:
|
| 465 |
most_common_text = ""
|
| 466 |
most_unique_text = ""
|
|
@@ -643,6 +718,7 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
|
|
| 643 |
highlighted.append((sentence, label))
|
| 644 |
elif show_consensuality:
|
| 645 |
highlighted = []
|
|
|
|
| 646 |
for sentence, metadata in review_item:
|
| 647 |
raw = metadata.get("consensuality", 0.0)
|
| 648 |
# Robust normalization: median-centered, IQR-scaled, clipped to [-1, 1]
|
|
@@ -650,11 +726,14 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
|
|
| 650 |
score = max(-1.0, min(1.0, (raw - _kl_median) / (_kl_iqr * 2)))
|
| 651 |
else:
|
| 652 |
score = 0.0
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
|
|
|
|
|
|
|
|
|
| 658 |
|
| 659 |
elif show_topic:
|
| 660 |
highlighted = []
|
|
@@ -695,22 +774,30 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
|
|
| 695 |
# General rebuttal display (currently unused in new format, kept for backward compat)
|
| 696 |
general_rebuttal_update = gr.update(visible=False, value="")
|
| 697 |
|
| 698 |
-
# Set most consensual / unique sentences
|
| 699 |
if show_consensuality and consensuality_dict:
|
| 700 |
scores = pd.Series(consensuality_dict)
|
| 701 |
most_unique = scores.sort_values(ascending=False).head(3).index.tolist()
|
| 702 |
most_common = scores.sort_values(ascending=True).head(3).index.tolist()
|
| 703 |
-
most_common_text = "\n".join(most_common)
|
| 704 |
-
most_unique_text = "\n".join(most_unique)
|
| 705 |
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 711 |
|
| 712 |
-
most_common_visibility = gr.update(visible=
|
| 713 |
-
most_unique_visibility = gr.update(visible=
|
|
|
|
|
|
|
|
|
|
| 714 |
|
| 715 |
# update topic color map
|
| 716 |
if show_topic:
|
|
@@ -768,18 +855,16 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
|
|
| 768 |
|
| 769 |
# Output display.
|
| 770 |
with gr.Row():
|
| 771 |
-
most_common_sentences = gr.
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
value=[]
|
| 782 |
-
)
|
| 783 |
|
| 784 |
# Add a new textbox for topic labels and colors
|
| 785 |
topic_text_box = gr.HighlightedText(
|
|
@@ -901,11 +986,11 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
|
|
| 901 |
)
|
| 902 |
|
| 903 |
with gr.Row():
|
| 904 |
-
most_divergent = gr.
|
| 905 |
-
|
| 906 |
)
|
| 907 |
-
most_common = gr.
|
| 908 |
-
|
| 909 |
)
|
| 910 |
|
| 911 |
# Review 1 (all display modes + rebuttal)
|
|
|
|
| 16 |
# Lower = more vivid colors (0.2 = very strong, 1.0 = no amplification).
|
| 17 |
# Asymmetric: unique/red (positive) is amplified less than common/blue (negative)
|
| 18 |
# to avoid overwhelming red when most sentences are unique.
|
| 19 |
+
AGREEMENT_AMP_UNIQUE = 0.95 # exponent for positive scores (red = unique)
|
| 20 |
+
AGREEMENT_AMP_COMMON = 0.65 # exponent for negative scores (blue = common)
|
| 21 |
+
|
| 22 |
+
import html as _html
|
| 23 |
+
|
| 24 |
+
def format_summary_cards(
|
| 25 |
+
sentences: list,
|
| 26 |
+
scores: dict,
|
| 27 |
+
sentence_lists: list,
|
| 28 |
+
card_type: str = "common",
|
| 29 |
+
) -> str:
|
| 30 |
+
"""
|
| 31 |
+
Generate styled HTML cards for the Most Common / Most Divergent summary boxes.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
sentences: Top N sentence strings to display.
|
| 35 |
+
scores: Full {sentence: score} dict for badge coloring.
|
| 36 |
+
sentence_lists: Per-review sentence lists (for attribution & context).
|
| 37 |
+
card_type: "common" (blue) or "unique" (red).
|
| 38 |
+
"""
|
| 39 |
+
if not sentences:
|
| 40 |
+
return ""
|
| 41 |
+
|
| 42 |
+
border_color = "#93c5fd" if card_type == "common" else "#fca5a5"
|
| 43 |
+
badge_bg = "#dbeafe" if card_type == "common" else "#fee2e2"
|
| 44 |
+
badge_fg = "#1e40af" if card_type == "common" else "#991b1b"
|
| 45 |
+
title = "Most Common Opinions" if card_type == "common" else "Most Divergent Opinions"
|
| 46 |
+
|
| 47 |
+
cards_html = (
|
| 48 |
+
f'<div style="margin-bottom:4px;font-weight:600;font-size:0.9em;color:#374151;">{title}</div>'
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
for sent in sentences:
|
| 52 |
+
# Find which reviews contain this sentence
|
| 53 |
+
review_badges = []
|
| 54 |
+
context_before, context_after = "", ""
|
| 55 |
+
for r_idx, sl in enumerate(sentence_lists):
|
| 56 |
+
if sent in sl:
|
| 57 |
+
review_badges.append(r_idx + 1)
|
| 58 |
+
# Get 1 sentence before and after for context (from first matching review)
|
| 59 |
+
if not context_before and not context_after:
|
| 60 |
+
s_idx = sl.index(sent)
|
| 61 |
+
if s_idx > 0:
|
| 62 |
+
context_before = _html.escape(sl[s_idx - 1])
|
| 63 |
+
if s_idx < len(sl) - 1:
|
| 64 |
+
context_after = _html.escape(sl[s_idx + 1])
|
| 65 |
+
|
| 66 |
+
badges = " ".join(
|
| 67 |
+
f'<span style="background:{badge_bg};color:{badge_fg};padding:2px 8px;'
|
| 68 |
+
f'border-radius:4px;font-size:0.72em;font-weight:600;">Review {n}</span>'
|
| 69 |
+
for n in review_badges
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
ctx_style = 'color:#9ca3af;font-size:0.8em;line-height:1.4;font-style:italic;'
|
| 73 |
+
before_html = f'<div style="{ctx_style}">...{context_before}</div>' if context_before else ""
|
| 74 |
+
after_html = f'<div style="{ctx_style}">{context_after}...</div>' if context_after else ""
|
| 75 |
+
|
| 76 |
+
cards_html += (
|
| 77 |
+
f'<div style="border:1px solid #e5e7eb;border-left:3px solid {border_color};'
|
| 78 |
+
f'border-radius:6px;padding:10px 14px;margin-bottom:6px;">'
|
| 79 |
+
f'<div style="display:flex;gap:6px;margin-bottom:6px;">{badges}</div>'
|
| 80 |
+
f'{before_html}'
|
| 81 |
+
f'<div style="color:#111827;line-height:1.5;padding:2px 0;">{_html.escape(sent)}</div>'
|
| 82 |
+
f'{after_html}'
|
| 83 |
+
f'</div>'
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
return cards_html
|
| 87 |
+
|
| 88 |
|
| 89 |
# Auto-detect the preprocessed dataset CSV
|
| 90 |
def _find_preprocessed_csv() -> Path:
|
|
|
|
| 257 |
|
| 258 |
# ===== INTERACTIVE TAB: GLOBAL PROCESSOR INITIALIZATION =====
|
| 259 |
# Initialize once at module load to avoid reloading models
|
| 260 |
+
from interface.interactive_processor import InteractiveReviewProcessor
|
| 261 |
+
from dependencies.sentence_filter import (
|
| 262 |
+
is_noise_sentence, filter_and_clean_sentences, strip_header_prefix,
|
| 263 |
+
HIGHLIGHT_THRESHOLD,
|
| 264 |
+
)
|
| 265 |
_interactive_processor = None
|
| 266 |
|
| 267 |
def get_interactive_processor():
|
|
|
|
| 452 |
if len(sentence_lists) < 2:
|
| 453 |
raise ValueError("At least two reviews must have valid sentences")
|
| 454 |
|
| 455 |
+
all_sentences = filter_and_clean_sentences(
|
| 456 |
+
list(set(s for sl in sentence_lists for s in sl))
|
| 457 |
+
)
|
| 458 |
|
| 459 |
# Step 3-4: Polarity + Topic (parallelize both models)
|
| 460 |
progress(0.30, desc="Predicting polarity and topics (parallel)...")
|
|
|
|
| 528 |
progress(0.50, desc="Running RSA reranking...")
|
| 529 |
consensuality_map = processor.predict_consensuality(*active_texts)
|
| 530 |
|
| 531 |
+
# Build summary cards with review attribution and context
|
| 532 |
if consensuality_map:
|
| 533 |
import pandas as _pd
|
| 534 |
scores_series = _pd.Series(consensuality_map)
|
| 535 |
+
top_common = scores_series.nsmallest(3).index.tolist()
|
| 536 |
+
top_unique = scores_series.nlargest(3).index.tolist()
|
| 537 |
+
most_common_text = format_summary_cards(top_common, consensuality_map, sentence_lists, "common")
|
| 538 |
+
most_unique_text = format_summary_cards(top_unique, consensuality_map, sentence_lists, "unique")
|
| 539 |
else:
|
| 540 |
most_common_text = ""
|
| 541 |
most_unique_text = ""
|
|
|
|
| 718 |
highlighted.append((sentence, label))
|
| 719 |
elif show_consensuality:
|
| 720 |
highlighted = []
|
| 721 |
+
import math
|
| 722 |
for sentence, metadata in review_item:
|
| 723 |
raw = metadata.get("consensuality", 0.0)
|
| 724 |
# Robust normalization: median-centered, IQR-scaled, clipped to [-1, 1]
|
|
|
|
| 726 |
score = max(-1.0, min(1.0, (raw - _kl_median) / (_kl_iqr * 2)))
|
| 727 |
else:
|
| 728 |
score = 0.0
|
| 729 |
+
# Display-time filtering: noise sentences and near-zero scores get no color
|
| 730 |
+
if is_noise_sentence(sentence) or abs(score) < HIGHLIGHT_THRESHOLD:
|
| 731 |
+
highlighted.append((sentence, None))
|
| 732 |
+
else:
|
| 733 |
+
consensuality_dict[sentence] = score
|
| 734 |
+
# Asymmetric amplification for display
|
| 735 |
+
display_score = math.copysign(abs(score) ** (AGREEMENT_AMP_UNIQUE if score > 0 else AGREEMENT_AMP_COMMON), score)
|
| 736 |
+
highlighted.append((sentence, display_score))
|
| 737 |
|
| 738 |
elif show_topic:
|
| 739 |
highlighted = []
|
|
|
|
| 774 |
# General rebuttal display (currently unused in new format, kept for backward compat)
|
| 775 |
general_rebuttal_update = gr.update(visible=False, value="")
|
| 776 |
|
| 777 |
+
# Set most consensual / unique sentences (as HTML cards with context)
|
| 778 |
if show_consensuality and consensuality_dict:
|
| 779 |
scores = pd.Series(consensuality_dict)
|
| 780 |
most_unique = scores.sort_values(ascending=False).head(3).index.tolist()
|
| 781 |
most_common = scores.sort_values(ascending=True).head(3).index.tolist()
|
|
|
|
|
|
|
| 782 |
|
| 783 |
+
# Build per-review sentence lists for attribution
|
| 784 |
+
review_sentence_lists = []
|
| 785 |
+
for review_data in current_review:
|
| 786 |
+
if isinstance(review_data, dict) and "sentences" in review_data:
|
| 787 |
+
review_sentence_lists.append(list(review_data["sentences"].keys()))
|
| 788 |
+
elif isinstance(review_data, dict):
|
| 789 |
+
review_sentence_lists.append(list(review_data.keys()))
|
| 790 |
+
else:
|
| 791 |
+
review_sentence_lists.append([])
|
| 792 |
+
|
| 793 |
+
most_common_html = format_summary_cards(most_common, consensuality_dict, review_sentence_lists, "common")
|
| 794 |
+
most_unique_html = format_summary_cards(most_unique, consensuality_dict, review_sentence_lists, "unique")
|
| 795 |
|
| 796 |
+
most_common_visibility = gr.update(visible=True, value=most_common_html)
|
| 797 |
+
most_unique_visibility = gr.update(visible=True, value=most_unique_html)
|
| 798 |
+
else:
|
| 799 |
+
most_common_visibility = gr.update(visible=False, value="")
|
| 800 |
+
most_unique_visibility = gr.update(visible=False, value="")
|
| 801 |
|
| 802 |
# update topic color map
|
| 803 |
if show_topic:
|
|
|
|
| 855 |
|
| 856 |
# Output display.
|
| 857 |
with gr.Row():
|
| 858 |
+
most_common_sentences = gr.HTML(
|
| 859 |
+
visible=False,
|
| 860 |
+
value="",
|
| 861 |
+
label="Most Common Opinions",
|
| 862 |
+
)
|
| 863 |
+
most_unique_sentences = gr.HTML(
|
| 864 |
+
visible=False,
|
| 865 |
+
value="",
|
| 866 |
+
label="Most Divergent Opinions",
|
| 867 |
+
)
|
|
|
|
|
|
|
| 868 |
|
| 869 |
# Add a new textbox for topic labels and colors
|
| 870 |
topic_text_box = gr.HighlightedText(
|
|
|
|
| 986 |
)
|
| 987 |
|
| 988 |
with gr.Row():
|
| 989 |
+
most_divergent = gr.HTML(
|
| 990 |
+
visible=False, value="", label="Most Divergent Opinions",
|
| 991 |
)
|
| 992 |
+
most_common = gr.HTML(
|
| 993 |
+
visible=False, value="", label="Most Common Opinions",
|
| 994 |
)
|
| 995 |
|
| 996 |
# Review 1 (all display modes + rebuttal)
|
interface/interactive_processor.py
CHANGED
|
@@ -20,22 +20,11 @@ sys.path.insert(0, str(BASE_DIR))
|
|
| 20 |
|
| 21 |
from dependencies.rsa_reranker import RSAReranking
|
| 22 |
from dependencies.Glimpse_tokenizer import glimpse_tokenizer
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
r'^(\*{1,2}|#{1,3}\s*)?(summary|strengths?|weaknesses?|questions?|limitations?|minor|'
|
| 27 |
-
r'rating|confidence|correctness|clarity|originality|significance|'
|
| 28 |
-
r'pros?|cons?|comments?|suggestions?|conclusion|recommendation|'
|
| 29 |
-
r'contribution|technical\s+quality|presentation|reproducibility|'
|
| 30 |
-
r'novelty|experiments?|related\s+work|other|additional)\s*:?(\*{1,2})?$',
|
| 31 |
-
re.IGNORECASE
|
| 32 |
)
|
| 33 |
|
| 34 |
-
|
| 35 |
-
def is_section_header(sentence: str) -> bool:
|
| 36 |
-
"""Return True if sentence is a structural section header (should be excluded from scoring)."""
|
| 37 |
-
return bool(_HEADER_RE.match(sentence.strip()))
|
| 38 |
-
|
| 39 |
# Try to import OpenReview, but don't fail if not available
|
| 40 |
try:
|
| 41 |
import openreview
|
|
@@ -170,8 +159,9 @@ class InteractiveReviewProcessor:
|
|
| 170 |
# Tokenize all reviews
|
| 171 |
all_sentence_lists = [[s for s in glimpse_tokenizer(t) if s.strip()] for t in texts]
|
| 172 |
|
| 173 |
-
# Get unique sentences,
|
| 174 |
-
|
|
|
|
| 175 |
|
| 176 |
if not sentences:
|
| 177 |
return {}
|
|
@@ -227,7 +217,10 @@ class InteractiveReviewProcessor:
|
|
| 227 |
return [(s, None) for s in sentences]
|
| 228 |
elif score_type == "consensuality":
|
| 229 |
return [
|
| 230 |
-
(s, scores_dict.get(s, 0.0)
|
|
|
|
|
|
|
|
|
|
| 231 |
for s in sentences
|
| 232 |
]
|
| 233 |
else: # polarity or topic
|
|
@@ -259,8 +252,10 @@ class InteractiveReviewProcessor:
|
|
| 259 |
if any(len(sl) == 0 for sl in sentence_lists):
|
| 260 |
raise ValueError("One or more reviews have no valid sentences")
|
| 261 |
|
| 262 |
-
# Get unique sentences
|
| 263 |
-
all_sentences =
|
|
|
|
|
|
|
| 264 |
|
| 265 |
# Predict scores (skip consensuality - that comes async)
|
| 266 |
polarity_map = self.predict_polarity(all_sentences)
|
|
@@ -305,8 +300,10 @@ class InteractiveReviewProcessor:
|
|
| 305 |
if any(len(sl) == 0 for sl in sentence_lists):
|
| 306 |
raise ValueError("One or more reviews have no valid sentences")
|
| 307 |
|
| 308 |
-
# Get unique sentences
|
| 309 |
-
all_sentences =
|
|
|
|
|
|
|
| 310 |
|
| 311 |
# Predict scores
|
| 312 |
polarity_map = self.predict_polarity(all_sentences)
|
|
|
|
| 20 |
|
| 21 |
from dependencies.rsa_reranker import RSAReranking
|
| 22 |
from dependencies.Glimpse_tokenizer import glimpse_tokenizer
|
| 23 |
+
from dependencies.sentence_filter import (
|
| 24 |
+
is_section_header, is_noise_sentence, filter_and_clean_sentences,
|
| 25 |
+
strip_header_prefix, HIGHLIGHT_THRESHOLD,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
)
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
# Try to import OpenReview, but don't fail if not available
|
| 29 |
try:
|
| 30 |
import openreview
|
|
|
|
| 159 |
# Tokenize all reviews
|
| 160 |
all_sentence_lists = [[s for s in glimpse_tokenizer(t) if s.strip()] for t in texts]
|
| 161 |
|
| 162 |
+
# Get unique sentences, filtering out noise (headers, citations, short fragments, etc.)
|
| 163 |
+
unique_sentences = list(set(s for lst in all_sentence_lists for s in lst))
|
| 164 |
+
sentences = filter_and_clean_sentences(unique_sentences)
|
| 165 |
|
| 166 |
if not sentences:
|
| 167 |
return {}
|
|
|
|
| 217 |
return [(s, None) for s in sentences]
|
| 218 |
elif score_type == "consensuality":
|
| 219 |
return [
|
| 220 |
+
(s, scores_dict.get(s, 0.0)
|
| 221 |
+
if isinstance(scores_dict.get(s), (int, float))
|
| 222 |
+
and abs(scores_dict.get(s, 0.0)) >= HIGHLIGHT_THRESHOLD
|
| 223 |
+
else None)
|
| 224 |
for s in sentences
|
| 225 |
]
|
| 226 |
else: # polarity or topic
|
|
|
|
| 252 |
if any(len(sl) == 0 for sl in sentence_lists):
|
| 253 |
raise ValueError("One or more reviews have no valid sentences")
|
| 254 |
|
| 255 |
+
# Get unique sentences, filtering out noise (headers, citations, short fragments, etc.)
|
| 256 |
+
all_sentences = filter_and_clean_sentences(
|
| 257 |
+
list(set(s for sl in sentence_lists for s in sl))
|
| 258 |
+
)
|
| 259 |
|
| 260 |
# Predict scores (skip consensuality - that comes async)
|
| 261 |
polarity_map = self.predict_polarity(all_sentences)
|
|
|
|
| 300 |
if any(len(sl) == 0 for sl in sentence_lists):
|
| 301 |
raise ValueError("One or more reviews have no valid sentences")
|
| 302 |
|
| 303 |
+
# Get unique sentences, filtering out noise (headers, citations, short fragments, etc.)
|
| 304 |
+
all_sentences = filter_and_clean_sentences(
|
| 305 |
+
list(set(s for sl in sentence_lists for s in sl))
|
| 306 |
+
)
|
| 307 |
|
| 308 |
# Predict scores
|
| 309 |
polarity_map = self.predict_polarity(all_sentences)
|