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 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.9 # exponent for positive scores (red = unique)
20
- AGREEMENT_AMP_COMMON = 0.5 # exponent for negative scores (blue = common)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, is_section_header
 
 
 
 
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 = [s for s in set(s for sl in sentence_lists for s in sl) if not is_section_header(s)]
 
 
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
- # Calculate most common and unique (before amplification, so ranking is on true scores)
459
  if consensuality_map:
460
  import pandas as _pd
461
  scores_series = _pd.Series(consensuality_map)
462
- most_common_text = "\n".join(scores_series.nsmallest(3).index.tolist())
463
- most_unique_text = "\n".join(scores_series.nlargest(3).index.tolist())
 
 
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
- consensuality_dict[sentence] = score
654
- # Asymmetric amplification for display
655
- import math
656
- display_score = math.copysign(abs(score) ** (AGREEMENT_AMP_UNIQUE if score > 0 else AGREEMENT_AMP_COMMON), score) if score != 0 else 0.0
657
- highlighted.append((sentence, display_score))
 
 
 
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
- most_common_visibility = gr.update(visible=True, value=most_common_text)
707
- most_unique_visibility = gr.update(visible=True, value=most_unique_text)
708
- else:
709
- # Debugging statements to check visibility settings
710
- # print("Hiding most common and unique sentences")
 
 
 
 
 
 
 
711
 
712
- most_common_visibility = gr.update(visible=False, value=[])
713
- most_unique_visibility = gr.update(visible=False, value=[])
 
 
 
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.Textbox(
772
- lines=8,
773
- label="Most Common Opinions",
774
- visible=False,
775
- value=[]
776
- )
777
- most_unique_sentences = gr.Textbox(
778
- lines=8,
779
- label="Most Divergent Opinions",
780
- visible=False,
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.Textbox(
905
- lines=4, label="Most Divergent Opinions", visible=False, value="", container=True
906
  )
907
- most_common = gr.Textbox(
908
- lines=4, label="Most Common Opinions", visible=False, value="", container=True
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
- _HEADER_RE = re.compile(
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, excluding section headers
174
- sentences = [s for s in set(s for lst in all_sentence_lists for s in lst) if not is_section_header(s)]
 
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) if isinstance(scores_dict.get(s), (int, float)) else None)
 
 
 
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 for scoring, excluding section headers
263
- all_sentences = [s for s in set(s for sl in sentence_lists for s in sl) if not is_section_header(s)]
 
 
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 for scoring, excluding section headers
309
- all_sentences = [s for s in set(s for sl in sentence_lists for s in sl) if not is_section_header(s)]
 
 
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)