Upload app.py
Browse files
app.py
CHANGED
|
@@ -14,14 +14,11 @@ from marker.logger import configure_logging
|
|
| 14 |
from surya.settings import settings as surya_settings
|
| 15 |
import traceback
|
| 16 |
import re
|
| 17 |
-
import time
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
# Marker / PDF 预处理配置
|
| 21 |
-
# =========================
|
| 22 |
configure_logging()
|
| 23 |
MAX_PAGES = 30
|
| 24 |
-
MIN_LENGTH
|
| 25 |
settings.EXTRACT_IMAGES = False
|
| 26 |
settings.DEBUG = False
|
| 27 |
settings.PDFTEXT_CPU_WORKERS = 1
|
|
@@ -32,9 +29,7 @@ surya_settings.IN_STREAMLIT = True
|
|
| 32 |
model_refs = load_all_models()
|
| 33 |
metadata = {}
|
| 34 |
|
| 35 |
-
#
|
| 36 |
-
# LLM 加载
|
| 37 |
-
# =========================
|
| 38 |
model_name = "maxidl/Llama-OpenReviewer-8B"
|
| 39 |
model = AutoModelForCausalLM.from_pretrained(
|
| 40 |
model_name,
|
|
@@ -42,48 +37,32 @@ model = AutoModelForCausalLM.from_pretrained(
|
|
| 42 |
device_map="auto"
|
| 43 |
)
|
| 44 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
|
|
|
| 45 |
if tokenizer.pad_token is None:
|
| 46 |
tokenizer.pad_token = tokenizer.eos_token
|
| 47 |
tokenizer.pad_token_id = tokenizer.eos_token_id
|
|
|
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
| 55 |
-
# 允许 TF32
|
| 56 |
try:
|
| 57 |
torch.backends.cuda.matmul.allow_tf32 = True
|
| 58 |
torch.backends.cudnn.allow_tf32 = True
|
| 59 |
except Exception:
|
| 60 |
pass
|
|
|
|
| 61 |
|
| 62 |
-
#
|
| 63 |
-
#
|
| 64 |
-
# =========================
|
| 65 |
-
MAX_INPUT_TOKENS = 3000 # 评审输入上限(按 token)
|
| 66 |
-
MAX_NEW_TOKENS = 512 # 生成上限(按 token)
|
| 67 |
-
FAST_SUMMARY_TOK = 480 # 超长文本的“先摘要”输出上限
|
| 68 |
-
LONG_TEXT_THRESHOLD_TOK = 6000 # 触发“先摘要”的 token 阈值
|
| 69 |
-
|
| 70 |
-
def truncate_by_tokens(text: str, tokenizer, max_tokens: int) -> str:
|
| 71 |
-
"""按 token 截断文本,优先保留头尾内容。"""
|
| 72 |
-
ids = tokenizer(text, return_tensors="pt", add_special_tokens=False)["input_ids"][0]
|
| 73 |
-
if ids.size(0) <= max_tokens:
|
| 74 |
-
return text
|
| 75 |
-
head = max_tokens // 2
|
| 76 |
-
tail = max_tokens - head
|
| 77 |
-
kept = torch.cat([ids[:head], ids[-tail:]], dim=0)
|
| 78 |
-
return tokenizer.decode(kept, skip_special_tokens=True)
|
| 79 |
-
|
| 80 |
-
# =========================
|
| 81 |
-
# 轻量“是否为论文”检测
|
| 82 |
-
# =========================
|
| 83 |
try:
|
| 84 |
paper_classifier = pipeline(
|
| 85 |
"text-classification",
|
| 86 |
-
model="fabriceyhc/bert-base-uncased-arxiv-classification",
|
| 87 |
device=0 if torch.cuda.is_available() else -1,
|
| 88 |
truncation=True,
|
| 89 |
max_length=512
|
|
@@ -95,10 +74,11 @@ except Exception as e:
|
|
| 95 |
paper_classifier = None
|
| 96 |
AI_CLASSIFIER_AVAILABLE = False
|
| 97 |
|
|
|
|
| 98 |
def init_zero_shot_classifier():
|
| 99 |
try:
|
| 100 |
-
from transformers import pipeline
|
| 101 |
-
classifier =
|
| 102 |
"zero-shot-classification",
|
| 103 |
model="facebook/bart-large-mnli",
|
| 104 |
device=0 if torch.cuda.is_available() else -1
|
|
@@ -108,62 +88,117 @@ def init_zero_shot_classifier():
|
|
| 108 |
print(f"Could not initialize zero-shot classifier: {e}")
|
| 109 |
return None
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
def ai_check_paper(text):
|
| 114 |
-
"""
|
| 115 |
-
if
|
|
|
|
|
|
|
|
|
|
| 116 |
return None, 0, "AI classifier not available"
|
| 117 |
-
|
|
|
|
| 118 |
max_chars = 2000
|
| 119 |
if len(text) > max_chars * 2:
|
| 120 |
text_sample = text[:max_chars] + "\n...\n" + text[-max_chars:]
|
| 121 |
else:
|
| 122 |
-
text_sample = text[:max_chars
|
| 123 |
-
|
| 124 |
try:
|
| 125 |
if zero_shot_classifier and not paper_classifier:
|
|
|
|
| 126 |
labels = [
|
| 127 |
-
"academic research paper",
|
| 128 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
]
|
|
|
|
| 130 |
result = zero_shot_classifier(
|
| 131 |
text_sample,
|
| 132 |
candidate_labels=labels,
|
| 133 |
hypothesis_template="This text is a {}."
|
| 134 |
)
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
| 137 |
paper_labels = {"academic research paper", "scientific article", "technical report"}
|
|
|
|
| 138 |
if top_label in paper_labels:
|
| 139 |
if top_score > 0.7:
|
| 140 |
-
return True, top_score, f"AI detected: {top_label} ({top_score:.2f})"
|
| 141 |
elif top_score > 0.5:
|
| 142 |
-
return True, 0.6, f"AI detected: likely {top_label} ({top_score:.2f})"
|
| 143 |
else:
|
| 144 |
-
return False, top_score, "AI uncertain"
|
| 145 |
else:
|
| 146 |
-
return False, 1
|
|
|
|
| 147 |
elif paper_classifier:
|
|
|
|
| 148 |
result = paper_classifier(text_sample)[0]
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
paper_keywords = ['cs', 'math', 'physics', 'eess', 'econ', 'stat', 'q-bio']
|
| 152 |
-
is_paper = any(
|
|
|
|
| 153 |
if is_paper:
|
| 154 |
-
return True, score, f"AI detected
|
| 155 |
else:
|
| 156 |
-
return False, 1
|
|
|
|
| 157 |
except Exception as e:
|
| 158 |
print(f"AI classification error: {e}")
|
| 159 |
return None, 0, "AI classification failed"
|
|
|
|
| 160 |
return None, 0, "AI check not performed"
|
| 161 |
|
|
|
|
| 162 |
SYSTEM_PROMPT_TEMPLATE = """You are an expert reviewer for AI conferences. You follow best practices and review papers according to the reviewer guidelines.
|
| 163 |
|
| 164 |
Reviewer guidelines:
|
| 165 |
-
1. Read the paper
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
Your response must only contain the review in markdown format with sections as defined above.
|
| 168 |
"""
|
| 169 |
|
|
@@ -172,18 +207,146 @@ USER_PROMPT_TEMPLATE = """Review the following paper:
|
|
| 172 |
{paper_text}
|
| 173 |
"""
|
| 174 |
|
|
|
|
| 175 |
REVIEW_FIELDS = """## Summary
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
## Overall Justification
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
"""
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
def is_research_paper(text, use_ai=True):
|
| 182 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
if not text or len(text) < MIN_LENGTH:
|
| 184 |
return False, 0, "Text is too short to be a research paper"
|
| 185 |
-
|
| 186 |
text_lower = text.lower()
|
|
|
|
|
|
|
| 187 |
indicators = {
|
| 188 |
'abstract': bool(re.search(r'\babstract\b', text_lower)),
|
| 189 |
'introduction': bool(re.search(r'\bintroduction\b', text_lower)),
|
|
@@ -191,73 +354,114 @@ def is_research_paper(text, use_ai=True):
|
|
| 191 |
'references': bool(re.search(r'\b(references|bibliography)\b', text_lower)),
|
| 192 |
'methodology': bool(re.search(r'\b(method|methodology|approach|algorithm|model)\b', text_lower)),
|
| 193 |
'results': bool(re.search(r'\b(results|experiments|evaluation|analysis)\b', text_lower)),
|
| 194 |
-
'citations': bool(re.search(r'\[[\d,\s]+\]|\(\w+,?\s*\d{4}\)', text)),
|
| 195 |
'figures_tables': bool(re.search(r'\b(figure\s*\d+|table\s*\d+|fig\.\s*\d+)\b', text_lower)),
|
| 196 |
'academic_terms': bool(re.search(r'\b(propose|present|demonstrate|evaluate|contribution|novel|state-of-the-art)\b', text_lower))
|
| 197 |
}
|
|
|
|
|
|
|
| 198 |
indicator_count = sum(indicators.values())
|
| 199 |
-
|
|
|
|
|
|
|
| 200 |
non_paper_indicators = []
|
|
|
|
|
|
|
| 201 |
if re.search(r'\b(invoice|receipt)\b', text_lower) and re.search(r'\b(total|amount|payment|billing)\b', text_lower):
|
| 202 |
non_paper_indicators.append(True)
|
|
|
|
|
|
|
| 203 |
if re.search(r'\bpurchase order\b', text_lower):
|
| 204 |
non_paper_indicators.append(True)
|
|
|
|
|
|
|
| 205 |
if re.search(r'\b(dear\s+(sir|madam|customer)|sincerely|best regards|yours truly)\b', text_lower):
|
| 206 |
non_paper_indicators.append(True)
|
|
|
|
|
|
|
| 207 |
if re.search(r'\b(chapter\s+\d+|lesson\s+\d+|exercise\s+\d+)\b', text_lower) and indicator_count < 3:
|
| 208 |
non_paper_indicators.append(True)
|
|
|
|
|
|
|
| 209 |
if re.search(r'<html|<body|<div|<script|<!DOCTYPE', text_lower):
|
| 210 |
non_paper_indicators.append(True)
|
|
|
|
|
|
|
| 211 |
if re.search(r'\b(ingredients|recipe|preparation|cooking time|servings)\b', text_lower) and not re.search(r'\b(algorithm|method|experiment)\b', text_lower):
|
| 212 |
non_paper_indicators.append(True)
|
|
|
|
|
|
|
| 213 |
if any(non_paper_indicators) and indicator_count < 6:
|
| 214 |
return False, 0, "Content appears to be a non-academic document"
|
| 215 |
-
|
|
|
|
| 216 |
ai_result = None
|
| 217 |
ai_confidence = 0
|
| 218 |
ai_reason = ""
|
| 219 |
-
|
|
|
|
| 220 |
ai_result, ai_confidence, ai_reason = ai_check_paper(text)
|
| 221 |
-
|
|
|
|
|
|
|
| 222 |
if indicator_count == 9:
|
| 223 |
-
rule_decision
|
| 224 |
-
|
|
|
|
| 225 |
elif indicator_count >= 6:
|
| 226 |
-
rule_decision
|
|
|
|
| 227 |
missing = [k for k, v in indicators.items() if not v]
|
| 228 |
-
rule_reason = f"Found {indicator_count}/9 indicators. Missing: {', '.join(missing)}"
|
| 229 |
else:
|
| 230 |
-
rule_decision
|
|
|
|
| 231 |
missing = [k for k, v in indicators.items() if not v]
|
| 232 |
rule_reason = f"Found only {indicator_count}/9 indicators. Missing: {', '.join(missing[:4])}"
|
| 233 |
-
|
|
|
|
| 234 |
if ai_result is not None:
|
|
|
|
|
|
|
| 235 |
combined_confidence = (rule_confidence * 0.6) + (ai_confidence * 0.4)
|
|
|
|
|
|
|
| 236 |
if rule_decision and ai_result:
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
elif not rule_decision and not ai_result:
|
|
|
|
| 239 |
return False, 0, f"Not a research paper. {rule_reason}. {ai_reason}"
|
| 240 |
else:
|
|
|
|
| 241 |
if combined_confidence >= 0.5:
|
| 242 |
return True, 0.6, f"Mixed signals: {rule_reason}. {ai_reason}"
|
| 243 |
else:
|
| 244 |
return False, 0, f"Likely not a research paper. {rule_reason}. {ai_reason}"
|
| 245 |
else:
|
|
|
|
| 246 |
if rule_decision:
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
| 248 |
else:
|
| 249 |
return False, 0, f"Does not appear to be a research paper. {rule_reason}"
|
| 250 |
|
| 251 |
def create_messages(review_fields, paper_text):
|
| 252 |
-
|
| 253 |
{"role": "system", "content": SYSTEM_PROMPT_TEMPLATE.format(review_fields=review_fields)},
|
| 254 |
-
{"role": "user",
|
| 255 |
]
|
|
|
|
| 256 |
|
| 257 |
@spaces.GPU()
|
| 258 |
def convert_file(filepath):
|
| 259 |
full_text, images, out_metadata = convert_single_pdf(
|
| 260 |
-
|
| 261 |
)
|
| 262 |
return full_text
|
| 263 |
|
|
@@ -269,10 +473,12 @@ def process_file(file):
|
|
| 269 |
filetype = find_filetype(filepath)
|
| 270 |
if filetype == "other":
|
| 271 |
raise ValueError()
|
|
|
|
| 272 |
length = get_length_of_text(filepath)
|
| 273 |
if length < MIN_LENGTH:
|
| 274 |
raise ValueError()
|
| 275 |
-
paper_text = convert_file(filepath)
|
|
|
|
| 276 |
if not len(paper_text) > MIN_LENGTH:
|
| 277 |
raise ValueError()
|
| 278 |
except spaces.zero.gradio.HTMLError as e:
|
|
@@ -282,33 +488,27 @@ def process_file(file):
|
|
| 282 |
print(traceback.format_exc())
|
| 283 |
print(f"Error converting {filepath}: {e}")
|
| 284 |
return "Error processing pdf", False
|
| 285 |
-
|
|
|
|
| 286 |
is_paper, confidence, reason = is_research_paper(paper_text, use_ai=True)
|
| 287 |
if not is_paper:
|
| 288 |
-
return
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
f"Reason: {reason}\n\n"
|
| 292 |
-
"Please upload a proper academic/research paper with sections like Abstract, Introduction, Methodology, Results, and References.",
|
| 293 |
-
False,
|
| 294 |
-
)
|
| 295 |
if confidence < 0.9:
|
| 296 |
-
paper_text =
|
| 297 |
-
|
| 298 |
-
"The document may be incomplete or missing key sections. Proceeding with review generation...\n\n---\n\n"
|
| 299 |
-
f"{paper_text}"
|
| 300 |
-
)
|
| 301 |
return paper_text, True
|
| 302 |
|
| 303 |
-
#
|
| 304 |
@spaces.GPU(duration=90)
|
| 305 |
def review(paper_text: str, review_template: str):
|
| 306 |
-
#
|
| 307 |
is_paper, confidence, reason = is_research_paper(paper_text, use_ai=False)
|
| 308 |
if not is_paper:
|
| 309 |
return f"⚠️ Cannot generate review: {reason}"
|
| 310 |
|
| 311 |
-
#
|
| 312 |
pt_ids = tokenizer(paper_text, return_tensors="pt", add_special_tokens=False)["input_ids"][0]
|
| 313 |
if pt_ids.size(0) > LONG_TEXT_THRESHOLD_TOK:
|
| 314 |
summary_prompt = (
|
|
@@ -316,7 +516,7 @@ def review(paper_text: str, review_template: str):
|
|
| 316 |
)
|
| 317 |
messages = [
|
| 318 |
{"role": "system", "content": "You are a helpful scientific summarizer."},
|
| 319 |
-
{"role": "user",
|
| 320 |
]
|
| 321 |
sum_ids = tokenizer.apply_chat_template(
|
| 322 |
messages, add_generation_prompt=True, return_tensors="pt"
|
|
@@ -329,17 +529,17 @@ def review(paper_text: str, review_template: str):
|
|
| 329 |
)
|
| 330 |
paper_text = tokenizer.decode(sum_out[0][sum_ids.shape[1]:], skip_special_tokens=True)
|
| 331 |
|
| 332 |
-
#
|
| 333 |
paper_text = truncate_by_tokens(paper_text, tokenizer, MAX_INPUT_TOKENS)
|
| 334 |
|
| 335 |
-
#
|
| 336 |
messages = create_messages(review_template, paper_text)
|
| 337 |
input_ids = tokenizer.apply_chat_template(
|
| 338 |
messages, add_generation_prompt=True, return_tensors='pt'
|
| 339 |
).to(model.device)
|
| 340 |
-
attention_mask = torch.ones_like(input_ids)
|
| 341 |
|
| 342 |
-
|
|
|
|
| 343 |
input_ids=input_ids,
|
| 344 |
attention_mask=attention_mask,
|
| 345 |
max_new_tokens=MAX_NEW_TOKENS,
|
|
@@ -348,34 +548,38 @@ def review(paper_text: str, review_template: str):
|
|
| 348 |
top_p=0.9,
|
| 349 |
pad_token_id=tokenizer.pad_token_id
|
| 350 |
)
|
|
|
|
| 351 |
return tokenizer.decode(out[0][input_ids.shape[1]:], skip_special_tokens=True).replace("<|eot_id|>", "")
|
|
|
|
| 352 |
|
| 353 |
-
# ============== 改造:原 /generate(流式,但限长) ==============
|
| 354 |
@spaces.GPU(duration=90)
|
| 355 |
def generate(paper_text, review_template):
|
| 356 |
-
|
|
|
|
| 357 |
if not is_paper:
|
| 358 |
return f"⚠️ Cannot generate review: {reason}"
|
| 359 |
|
| 360 |
-
#
|
| 361 |
paper_text = truncate_by_tokens(paper_text, tokenizer, MAX_INPUT_TOKENS)
|
| 362 |
-
|
| 363 |
messages = create_messages(review_template, paper_text)
|
| 364 |
input_ids = tokenizer.apply_chat_template(
|
| 365 |
messages,
|
| 366 |
add_generation_prompt=True,
|
| 367 |
return_tensors='pt'
|
| 368 |
).to(model.device)
|
|
|
|
|
|
|
| 369 |
attention_mask = torch.ones_like(input_ids)
|
| 370 |
-
|
| 371 |
print(f"input_ids shape (truncated): {input_ids.shape}")
|
| 372 |
generation_kwargs = dict(
|
| 373 |
input_ids=input_ids,
|
| 374 |
attention_mask=attention_mask,
|
| 375 |
streamer=streamer,
|
| 376 |
-
max_new_tokens=MAX_NEW_TOKENS, #
|
| 377 |
do_sample=True,
|
| 378 |
-
temperature=0.5,
|
| 379 |
top_p=0.9,
|
| 380 |
pad_token_id=tokenizer.pad_token_id
|
| 381 |
)
|
|
@@ -386,10 +590,9 @@ def generate(paper_text, review_template):
|
|
| 386 |
generated_text += new_text
|
| 387 |
yield generated_text.replace("<|eot_id|>", "")
|
| 388 |
|
| 389 |
-
#
|
| 390 |
-
#
|
| 391 |
-
|
| 392 |
-
title_html = """<h1 align="center">OpenReviewer</h1>
|
| 393 |
<div align="center">Using <a href="https://huggingface.co/maxidl/Llama-OpenReviewer-8B" target="_blank"><code>Llama-OpenReviewer-8B</code></a> - Built with Llama</div>
|
| 394 |
"""
|
| 395 |
|
|
@@ -397,36 +600,45 @@ description = """This is an online demo featuring [Llama-OpenReviewer-8B](https:
|
|
| 397 |
|
| 398 |
## Demo Guidelines
|
| 399 |
|
| 400 |
-
1. Upload your paper as a PDF file. Alternatively you can paste the full text of your paper in markdown format below
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
"""
|
| 402 |
|
| 403 |
theme = gr.themes.Default(primary_hue="gray", secondary_hue="blue", neutral_hue="slate")
|
| 404 |
with gr.Blocks(theme=theme) as demo:
|
| 405 |
-
gr.HTML(
|
| 406 |
-
gr.Markdown(description)
|
| 407 |
-
|
|
|
|
| 408 |
with gr.Row():
|
| 409 |
file_input = gr.File(file_types=[".pdf"], file_count="single")
|
| 410 |
validation_status = gr.Markdown("", visible=False)
|
| 411 |
-
|
| 412 |
-
paper_text_field = gr.Textbox(
|
| 413 |
-
|
| 414 |
-
label="Paper Text",
|
| 415 |
-
lines=20, max_lines=20, autoscroll=False
|
| 416 |
-
)
|
| 417 |
-
|
| 418 |
with gr.Accordion("Review Template", open=False):
|
| 419 |
-
gr.Markdown("We use the ICLR 2025 review template by default, but you can modify the template below as you like.")
|
| 420 |
-
review_template_field = gr.Textbox(label=" ",
|
| 421 |
-
|
| 422 |
generate_button = gr.Button("Generate Review", interactive=False)
|
| 423 |
-
|
| 424 |
def handle_file_upload(file):
|
| 425 |
if file is None:
|
| 426 |
return "", gr.update(visible=False), gr.update(interactive=False)
|
| 427 |
text, is_valid = process_file(file)
|
| 428 |
if is_valid:
|
| 429 |
-
|
|
|
|
| 430 |
if confidence >= 0.9:
|
| 431 |
status_msg = "✅ **Document validated**: This appears to be a complete research paper."
|
| 432 |
else:
|
|
@@ -434,48 +646,49 @@ with gr.Blocks(theme=theme) as demo:
|
|
| 434 |
return text, gr.update(value=status_msg, visible=True), gr.update(interactive=True)
|
| 435 |
else:
|
| 436 |
return text, gr.update(value="❌ **Validation failed**: Please upload a research paper.", visible=True), gr.update(interactive=False)
|
| 437 |
-
|
| 438 |
def handle_text_change(text):
|
| 439 |
if not text or len(text) < 200:
|
| 440 |
return gr.update(interactive=False), gr.update(visible=False)
|
|
|
|
| 441 |
is_paper, confidence, reason = is_research_paper(text, use_ai=True)
|
| 442 |
if is_paper:
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
|
|
|
|
|
|
| 446 |
return gr.update(interactive=True), gr.update(value=status, visible=True)
|
| 447 |
else:
|
| 448 |
return gr.update(interactive=False), gr.update(value=f"❌ **Not a research paper**: {reason}", visible=True)
|
| 449 |
-
|
| 450 |
file_input.upload(handle_file_upload, file_input, [paper_text_field, validation_status, generate_button])
|
| 451 |
paper_text_field.change(handle_text_change, paper_text_field, [generate_button, validation_status])
|
| 452 |
-
|
| 453 |
review_field = gr.Markdown("\n\n\n\n\n", label="Review")
|
| 454 |
|
| 455 |
-
#
|
| 456 |
generate_button.click(
|
| 457 |
-
fn=lambda: gr.update(interactive=False),
|
| 458 |
-
inputs=None,
|
| 459 |
outputs=generate_button
|
| 460 |
).then(
|
| 461 |
-
generate,
|
| 462 |
-
[paper_text_field, review_template_field],
|
| 463 |
review_field,
|
| 464 |
-
api_name="generate"
|
| 465 |
).then(
|
| 466 |
-
fn=lambda: gr.update(interactive=True),
|
| 467 |
-
inputs=None,
|
| 468 |
outputs=generate_button
|
| 469 |
)
|
| 470 |
|
| 471 |
-
#
|
| 472 |
-
gr.Button(visible=False)
|
| 473 |
-
|
| 474 |
-
[paper_text_field, review_template_field],
|
| 475 |
-
review_field,
|
| 476 |
-
api_name="review"
|
| 477 |
)
|
| 478 |
-
|
| 479 |
demo.title = "OpenReviewer"
|
| 480 |
|
| 481 |
if __name__ == "__main__":
|
|
|
|
| 14 |
from surya.settings import settings as surya_settings
|
| 15 |
import traceback
|
| 16 |
import re
|
|
|
|
| 17 |
|
| 18 |
+
# marker
|
|
|
|
|
|
|
| 19 |
configure_logging()
|
| 20 |
MAX_PAGES = 30
|
| 21 |
+
MIN_LENGTH=200
|
| 22 |
settings.EXTRACT_IMAGES = False
|
| 23 |
settings.DEBUG = False
|
| 24 |
settings.PDFTEXT_CPU_WORKERS = 1
|
|
|
|
| 29 |
model_refs = load_all_models()
|
| 30 |
metadata = {}
|
| 31 |
|
| 32 |
+
# prepare LLM
|
|
|
|
|
|
|
| 33 |
model_name = "maxidl/Llama-OpenReviewer-8B"
|
| 34 |
model = AutoModelForCausalLM.from_pretrained(
|
| 35 |
model_name,
|
|
|
|
| 37 |
device_map="auto"
|
| 38 |
)
|
| 39 |
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 40 |
+
# Set pad_token to eos_token if not set (common for Llama models)
|
| 41 |
if tokenizer.pad_token is None:
|
| 42 |
tokenizer.pad_token = tokenizer.eos_token
|
| 43 |
tokenizer.pad_token_id = tokenizer.eos_token_id
|
| 44 |
+
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, decode_kwargs=dict(skip_special_tokens=True))
|
| 45 |
|
| 46 |
+
# ======== 新增:生成与输入长度限制 / 加速开关 ========
|
| 47 |
+
MAX_INPUT_TOKENS = 3000 # 评审输入截断上限(可 2500~3500 之间调)
|
| 48 |
+
MAX_NEW_TOKENS = 512 # 生成上限(必要时可到 640/768)
|
| 49 |
+
FAST_SUMMARY_TOK = 480 # 超长文本“先摘要”的输出上限
|
| 50 |
+
LONG_TEXT_THRESHOLD_TOK = 6000 # 超过此上下文 token 视为超长,触发“先摘要”
|
| 51 |
|
| 52 |
+
# 允许 TF32(若硬件支持可提速;不支持也不会报错)
|
| 53 |
try:
|
| 54 |
torch.backends.cuda.matmul.allow_tf32 = True
|
| 55 |
torch.backends.cudnn.allow_tf32 = True
|
| 56 |
except Exception:
|
| 57 |
pass
|
| 58 |
+
# ======== 新增结束 ========
|
| 59 |
|
| 60 |
+
# Initialize AI classifier for paper detection
|
| 61 |
+
# Using a lightweight model for text classification
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
try:
|
| 63 |
paper_classifier = pipeline(
|
| 64 |
"text-classification",
|
| 65 |
+
model="fabriceyhc/bert-base-uncased-arxiv-classification", # You can use other models
|
| 66 |
device=0 if torch.cuda.is_available() else -1,
|
| 67 |
truncation=True,
|
| 68 |
max_length=512
|
|
|
|
| 74 |
paper_classifier = None
|
| 75 |
AI_CLASSIFIER_AVAILABLE = False
|
| 76 |
|
| 77 |
+
# Alternative: Use zero-shot classification (more flexible but slower)
|
| 78 |
def init_zero_shot_classifier():
|
| 79 |
try:
|
| 80 |
+
from transformers import pipeline
|
| 81 |
+
classifier = pipeline(
|
| 82 |
"zero-shot-classification",
|
| 83 |
model="facebook/bart-large-mnli",
|
| 84 |
device=0 if torch.cuda.is_available() else -1
|
|
|
|
| 88 |
print(f"Could not initialize zero-shot classifier: {e}")
|
| 89 |
return None
|
| 90 |
|
| 91 |
+
# If primary classifier fails, try zero-shot
|
| 92 |
+
if not AI_CLASSIFIER_AVAILABLE:
|
| 93 |
+
zero_shot_classifier = init_zero_shot_classifier()
|
| 94 |
+
if zero_shot_classifier:
|
| 95 |
+
AI_CLASSIFIER_AVAILABLE = True
|
| 96 |
+
else:
|
| 97 |
+
zero_shot_classifier = None
|
| 98 |
|
| 99 |
def ai_check_paper(text):
|
| 100 |
+
"""
|
| 101 |
+
Use AI model to check if text is a research paper
|
| 102 |
+
Returns (is_paper, confidence, ai_reason)
|
| 103 |
+
"""
|
| 104 |
+
if not AI_CLASSIFIER_AVAILABLE:
|
| 105 |
return None, 0, "AI classifier not available"
|
| 106 |
+
|
| 107 |
+
# Truncate text for AI model (keep beginning and end which are most informative)
|
| 108 |
max_chars = 2000
|
| 109 |
if len(text) > max_chars * 2:
|
| 110 |
text_sample = text[:max_chars] + "\n...\n" + text[-max_chars:]
|
| 111 |
else:
|
| 112 |
+
text_sample = text[:max_chars*2]
|
| 113 |
+
|
| 114 |
try:
|
| 115 |
if zero_shot_classifier and not paper_classifier:
|
| 116 |
+
# Use zero-shot classification
|
| 117 |
labels = [
|
| 118 |
+
"academic research paper",
|
| 119 |
+
"scientific article",
|
| 120 |
+
"technical report",
|
| 121 |
+
"business document",
|
| 122 |
+
"news article",
|
| 123 |
+
"blog post",
|
| 124 |
+
"other document"
|
| 125 |
]
|
| 126 |
+
|
| 127 |
result = zero_shot_classifier(
|
| 128 |
text_sample,
|
| 129 |
candidate_labels=labels,
|
| 130 |
hypothesis_template="This text is a {}."
|
| 131 |
)
|
| 132 |
+
|
| 133 |
+
# Check if top labels are paper-related
|
| 134 |
+
top_label = result['labels'][0]
|
| 135 |
+
top_score = result['scores'][0]
|
| 136 |
+
|
| 137 |
paper_labels = {"academic research paper", "scientific article", "technical report"}
|
| 138 |
+
|
| 139 |
if top_label in paper_labels:
|
| 140 |
if top_score > 0.7:
|
| 141 |
+
return True, top_score, f"AI detected: {top_label} (confidence: {top_score:.2f})"
|
| 142 |
elif top_score > 0.5:
|
| 143 |
+
return True, 0.6, f"AI detected: likely {top_label} (confidence: {top_score:.2f})"
|
| 144 |
else:
|
| 145 |
+
return False, top_score, f"AI detected: uncertain document type"
|
| 146 |
else:
|
| 147 |
+
return False, 1-top_score, f"AI detected: {top_label}, not a research paper"
|
| 148 |
+
|
| 149 |
elif paper_classifier:
|
| 150 |
+
# Use pre-trained classifier
|
| 151 |
result = paper_classifier(text_sample)[0]
|
| 152 |
+
|
| 153 |
+
# Check if the label indicates it's a paper
|
| 154 |
+
# This depends on the specific model used
|
| 155 |
+
label = result['label'].lower()
|
| 156 |
+
score = result['score']
|
| 157 |
+
|
| 158 |
+
# Adjust based on your chosen model's labels
|
| 159 |
paper_keywords = ['cs', 'math', 'physics', 'eess', 'econ', 'stat', 'q-bio']
|
| 160 |
+
is_paper = any(keyword in label for keyword in paper_keywords)
|
| 161 |
+
|
| 162 |
if is_paper:
|
| 163 |
+
return True, score, f"AI detected: {label} paper (confidence: {score:.2f})"
|
| 164 |
else:
|
| 165 |
+
return False, 1-score, f"AI detected: not a research paper"
|
| 166 |
+
|
| 167 |
except Exception as e:
|
| 168 |
print(f"AI classification error: {e}")
|
| 169 |
return None, 0, "AI classification failed"
|
| 170 |
+
|
| 171 |
return None, 0, "AI check not performed"
|
| 172 |
|
| 173 |
+
# Define prompts
|
| 174 |
SYSTEM_PROMPT_TEMPLATE = """You are an expert reviewer for AI conferences. You follow best practices and review papers according to the reviewer guidelines.
|
| 175 |
|
| 176 |
Reviewer guidelines:
|
| 177 |
+
1. Read the paper: It's important to carefully read through the entire paper, and to look up any related work and citations that will help you comprehensively evaluate it. Be sure to give yourself sufficient time for this step.
|
| 178 |
+
2. While reading, consider the following:
|
| 179 |
+
- Objective of the work: What is the goal of the paper? Is it to better address a known application or problem, draw attention to a new application or problem, or to introduce and/or explain a new theoretical finding? A combination of these? Different objectives will require different considerations as to potential value and impact.
|
| 180 |
+
- Strong points: is the submission clear, technically correct, experimentally rigorous, reproducible, does it present novel findings (e.g., theoretically, algorithmically, etc.)?
|
| 181 |
+
- Weak points: is it weak in any of the aspects listed in b.?
|
| 182 |
+
- Be mindful of potential biases and try to be open-minded about the value and interest a paper can hold for the community, even if it may not be very interesting for you.
|
| 183 |
+
3. Answer four key questions for yourself, to make a recommendation to Accept or Reject:
|
| 184 |
+
- What is the specific question and/or problem tackled by the paper?
|
| 185 |
+
- Is the approach well motivated, including being well-placed in the literature?
|
| 186 |
+
- Does the paper support the claims? This includes determining if results, whether theoretical or empirical, are correct and if they are scientifically rigorous.
|
| 187 |
+
- What is the significance of the work? Does it contribute new knowledge and sufficient value to the community? Note, this does not necessarily require state-of-the-art results. Submissions bring value to the community when they convincingly demonstrate new, relevant, impactful knowledge (incl., empirical, theoretical, for practitioners, etc).
|
| 188 |
+
4. Write your review including the following information:
|
| 189 |
+
- Summarize what the paper claims to contribute. Be positive and constructive.
|
| 190 |
+
- List strong and weak points of the paper. Be as comprehensive as possible.
|
| 191 |
+
- Clearly state your initial recommendation (accept or reject) with one or two key reasons for this choice.
|
| 192 |
+
- Provide supporting arguments for your recommendation.
|
| 193 |
+
- Ask questions you would like answered by the authors to help you clarify your understanding of the paper and provide the additional evidence you need to be confident in your assessment.
|
| 194 |
+
- Provide additional feedback with the aim to improve the paper. Make it clear that these points are here to help, and not necessarily part of your decision assessment.
|
| 195 |
+
|
| 196 |
+
Your write reviews in markdown format. Your reviews contain the following sections:
|
| 197 |
+
|
| 198 |
+
# Review
|
| 199 |
+
|
| 200 |
+
{review_fields}
|
| 201 |
+
|
| 202 |
Your response must only contain the review in markdown format with sections as defined above.
|
| 203 |
"""
|
| 204 |
|
|
|
|
| 207 |
{paper_text}
|
| 208 |
"""
|
| 209 |
|
| 210 |
+
# For now, use fixed review fields
|
| 211 |
REVIEW_FIELDS = """## Summary
|
| 212 |
+
Briefly summarize the paper and its contributions. This is not the place to critique the paper; the authors should generally agree with a well-written summary.
|
| 213 |
+
|
| 214 |
+
## Novelty
|
| 215 |
+
Please assign the paper a numerical rating on the following scale to indicate the novelty and originality of the work. Consider whether the paper presents new ideas, methods, or perspectives that have not been explored before. Choose from the following:
|
| 216 |
+
4: excellent - Highly original work with groundbreaking ideas or completely novel approaches
|
| 217 |
+
3: good - Significant new contributions with clear advances over existing work
|
| 218 |
+
2: fair - Some new elements but largely incremental improvements or combinations of existing ideas
|
| 219 |
+
1: poor - Little to no novelty, mostly reproducing existing work or trivial variations
|
| 220 |
+
|
| 221 |
+
## Novelty Explanation
|
| 222 |
+
IMPORTANT: Focus ONLY on novelty aspects. DO NOT discuss soundness, presentation, or general contribution here.
|
| 223 |
+
Please provide specific justification for your novelty score by addressing:
|
| 224 |
+
- What specific new concepts, methods, or approaches does this paper introduce?
|
| 225 |
+
- How do these differ from existing work in the field? Cite specific prior work for comparison.
|
| 226 |
+
- Are the differences substantial or incremental?
|
| 227 |
+
- Is this addressing a problem in a genuinely new way, or applying known methods to a new domain?
|
| 228 |
+
DO NOT repeat content from other sections. DO NOT discuss writing quality, experimental rigor, or implementation details here.
|
| 229 |
+
|
| 230 |
+
## Soundness
|
| 231 |
+
Please assign the paper a numerical rating on the following scale to indicate the soundness of the technical claims, experimental and research methodology and on whether the central claims of the paper are adequately supported with evidence. Choose from the following:
|
| 232 |
+
4: excellent
|
| 233 |
+
3: good
|
| 234 |
+
2: fair
|
| 235 |
+
1: poor
|
| 236 |
+
|
| 237 |
+
## Soundness Explanation
|
| 238 |
+
IMPORTANT: Focus ONLY on technical correctness and methodological rigor. DO NOT discuss novelty or writing quality here.
|
| 239 |
+
Please provide specific reasons for your soundness score by addressing:
|
| 240 |
+
- Are the technical claims mathematically/logically correct?
|
| 241 |
+
- Is the experimental methodology rigorous and appropriate?
|
| 242 |
+
- Are the experiments sufficient to support the claims?
|
| 243 |
+
- Are there any methodological flaws or missing controls?
|
| 244 |
+
- Is the statistical analysis (if any) appropriate and correctly executed?
|
| 245 |
+
DO NOT repeat content from other sections. DO NOT discuss the novelty of the approach or presentation quality here.
|
| 246 |
+
|
| 247 |
+
## Presentation
|
| 248 |
+
Please assign the paper a numerical rating on the following scale to indicate the quality of the presentation. This should take into account the writing style and clarity, as well as contextualization relative to prior work. Choose from the following:
|
| 249 |
+
4: excellent
|
| 250 |
+
3: good
|
| 251 |
+
2: fair
|
| 252 |
+
1: poor
|
| 253 |
+
|
| 254 |
+
## Presentation Explanation
|
| 255 |
+
IMPORTANT: Focus ONLY on writing quality, clarity, and organization. DO NOT discuss technical merit or novelty here.
|
| 256 |
+
Please explain your presentation score by addressing:
|
| 257 |
+
- Is the paper well-organized and easy to follow?
|
| 258 |
+
- Are the main ideas clearly explained?
|
| 259 |
+
- Are figures, tables, and visualizations effective and well-designed?
|
| 260 |
+
- Is the related work section comprehensive and fair?
|
| 261 |
+
- Are mathematical notations consistent and clear?
|
| 262 |
+
- Is the language precise and grammatically correct?
|
| 263 |
+
DO NOT repeat content from other sections. DO NOT discuss the novelty of ideas or soundness of methods here.
|
| 264 |
+
|
| 265 |
+
## Contribution
|
| 266 |
+
Please assign the paper a numerical rating on the following scale to indicate the quality of the overall contribution this paper makes to the research area being studied. Are the questions being asked important? Does the paper bring a significant originality of ideas and/or execution? Are the results valuable to share with the broader ICLR community? Choose from the following:
|
| 267 |
+
4: excellent
|
| 268 |
+
3: good
|
| 269 |
+
2: fair
|
| 270 |
+
1: poor
|
| 271 |
+
|
| 272 |
+
## Contribution Explanation
|
| 273 |
+
IMPORTANT: Focus on the OVERALL IMPACT and SIGNIFICANCE to the field. This is different from novelty.
|
| 274 |
+
Please justify your contribution score by explaining:
|
| 275 |
+
- Why is this work important for the field?
|
| 276 |
+
- What practical or theoretical impact could this have?
|
| 277 |
+
- Who would benefit from this work and how?
|
| 278 |
+
- Does this open new research directions or close important gaps?
|
| 279 |
+
- How significant are the improvements over baselines (if applicable)?
|
| 280 |
+
Consider both immediate utility and long-term impact. DO NOT simply repeat the novelty assessment here.
|
| 281 |
+
|
| 282 |
+
## Strengths
|
| 283 |
+
List the main strengths of the paper. Be specific and provide evidence. Each strength should be a separate bullet point. Focus on what the paper does well across all dimensions (novelty, soundness, presentation, contribution). Avoid generic statements.
|
| 284 |
+
|
| 285 |
+
## Weaknesses
|
| 286 |
+
List the main weaknesses of the paper. Be specific, constructive, and actionable. Each weakness should be a separate bullet point with suggestions for improvement where possible. Focus on significant issues that affect the paper's validity or impact.
|
| 287 |
+
|
| 288 |
+
## Questions
|
| 289 |
+
List specific questions for the authors that could clarify ambiguities or address concerns. Number each question. These should be questions where the answer could potentially change your assessment of the paper.
|
| 290 |
+
|
| 291 |
+
## Flag For Ethics Review
|
| 292 |
+
If there are ethical issues with this paper, please flag the paper for an ethics review and select area of expertise that would be most useful for the ethics reviewer to have. Please select all that apply. Choose from the following:
|
| 293 |
+
No ethics review needed.
|
| 294 |
+
Yes, Discrimination / bias / fairness concerns
|
| 295 |
+
Yes, Privacy, security and safety
|
| 296 |
+
Yes, Legal compliance (e.g., GDPR, copyright, terms of use)
|
| 297 |
+
Yes, Potentially harmful insights, methodologies and applications
|
| 298 |
+
Yes, Responsible research practice (e.g., human subjects, data release)
|
| 299 |
+
Yes, Research integrity issues (e.g., plagiarism, dual submission)
|
| 300 |
+
Yes, Unprofessional behaviors (e.g., unprofessional exchange between authors and reviewers)
|
| 301 |
+
Yes, Other reasons (please specify below)
|
| 302 |
+
|
| 303 |
+
## Details Of Ethics Concerns
|
| 304 |
+
Please provide details of your concerns. If no ethics review is needed, write "N/A".
|
| 305 |
+
|
| 306 |
+
## Rating
|
| 307 |
+
Please provide an "overall score" for this submission. Choose from the following:
|
| 308 |
+
1: strong reject
|
| 309 |
+
3: reject, not good enough
|
| 310 |
+
5: marginally below the acceptance threshold
|
| 311 |
+
6: marginally above the acceptance threshold
|
| 312 |
+
8: accept, good paper
|
| 313 |
+
10: strong accept, should be highlighted at the conference
|
| 314 |
+
|
| 315 |
## Overall Justification
|
| 316 |
+
Provide a comprehensive justification for your overall rating that:
|
| 317 |
+
- Synthesizes the assessments from all dimensions (novelty, soundness, presentation, contribution)
|
| 318 |
+
- Explains how you weighted different aspects in arriving at your final score
|
| 319 |
+
- Clearly states whether the strengths outweigh the weaknesses or vice versa
|
| 320 |
+
- Indicates what would need to change for a different rating
|
| 321 |
+
This should be a holistic assessment, not a repetition of individual sections.
|
| 322 |
+
|
| 323 |
"""
|
| 324 |
|
| 325 |
+
# ======== 新增:token 级截断 ========
|
| 326 |
+
def truncate_by_tokens(text: str, tokenizer, max_tokens: int) -> str:
|
| 327 |
+
"""按 token 截断文本,优先保留开头+结尾(论文信息密度最高的区域)"""
|
| 328 |
+
ids = tokenizer(text, return_tensors="pt", add_special_tokens=False)["input_ids"][0]
|
| 329 |
+
if ids.size(0) <= max_tokens:
|
| 330 |
+
return text
|
| 331 |
+
head = max_tokens // 2
|
| 332 |
+
tail = max_tokens - head
|
| 333 |
+
kept = torch.cat([ids[:head], ids[-tail:]], dim=0)
|
| 334 |
+
return tokenizer.decode(kept, skip_special_tokens=True)
|
| 335 |
+
# ======== 新增结束 ========
|
| 336 |
+
|
| 337 |
+
# Enhanced function that combines rule-based and AI checks
|
| 338 |
def is_research_paper(text, use_ai=True):
|
| 339 |
+
"""
|
| 340 |
+
Check if the given text appears to be a research paper.
|
| 341 |
+
Combines rule-based detection with AI classification.
|
| 342 |
+
Returns (is_paper, confidence, reason)
|
| 343 |
+
"""
|
| 344 |
if not text or len(text) < MIN_LENGTH:
|
| 345 |
return False, 0, "Text is too short to be a research paper"
|
| 346 |
+
|
| 347 |
text_lower = text.lower()
|
| 348 |
+
|
| 349 |
+
# Academic paper indicators (must have multiple)
|
| 350 |
indicators = {
|
| 351 |
'abstract': bool(re.search(r'\babstract\b', text_lower)),
|
| 352 |
'introduction': bool(re.search(r'\bintroduction\b', text_lower)),
|
|
|
|
| 354 |
'references': bool(re.search(r'\b(references|bibliography)\b', text_lower)),
|
| 355 |
'methodology': bool(re.search(r'\b(method|methodology|approach|algorithm|model)\b', text_lower)),
|
| 356 |
'results': bool(re.search(r'\b(results|experiments|evaluation|analysis)\b', text_lower)),
|
| 357 |
+
'citations': bool(re.search(r'\[[\d,\s]+\]|\(\w+,?\s*\d{4}\)', text)), # [1,2,3] or (Author, 2024)
|
| 358 |
'figures_tables': bool(re.search(r'\b(figure\s*\d+|table\s*\d+|fig\.\s*\d+)\b', text_lower)),
|
| 359 |
'academic_terms': bool(re.search(r'\b(propose|present|demonstrate|evaluate|contribution|novel|state-of-the-art)\b', text_lower))
|
| 360 |
}
|
| 361 |
+
|
| 362 |
+
# Count how many indicators are present
|
| 363 |
indicator_count = sum(indicators.values())
|
| 364 |
+
|
| 365 |
+
# Check for non-paper content with context
|
| 366 |
+
# Only flag as non-paper if these terms appear WITHOUT academic context
|
| 367 |
non_paper_indicators = []
|
| 368 |
+
|
| 369 |
+
# Check for invoice/receipt patterns (multiple commercial terms together)
|
| 370 |
if re.search(r'\b(invoice|receipt)\b', text_lower) and re.search(r'\b(total|amount|payment|billing)\b', text_lower):
|
| 371 |
non_paper_indicators.append(True)
|
| 372 |
+
|
| 373 |
+
# Check for purchase order specifically (not just "purchase")
|
| 374 |
if re.search(r'\bpurchase order\b', text_lower):
|
| 375 |
non_paper_indicators.append(True)
|
| 376 |
+
|
| 377 |
+
# Check for letter format
|
| 378 |
if re.search(r'\b(dear\s+(sir|madam|customer)|sincerely|best regards|yours truly)\b', text_lower):
|
| 379 |
non_paper_indicators.append(True)
|
| 380 |
+
|
| 381 |
+
# Check for textbook structure
|
| 382 |
if re.search(r'\b(chapter\s+\d+|lesson\s+\d+|exercise\s+\d+)\b', text_lower) and indicator_count < 3:
|
| 383 |
non_paper_indicators.append(True)
|
| 384 |
+
|
| 385 |
+
# Check for HTML/web content
|
| 386 |
if re.search(r'<html|<body|<div|<script|<!DOCTYPE', text_lower):
|
| 387 |
non_paper_indicators.append(True)
|
| 388 |
+
|
| 389 |
+
# Check for recipe/cooking content
|
| 390 |
if re.search(r'\b(ingredients|recipe|preparation|cooking time|servings)\b', text_lower) and not re.search(r'\b(algorithm|method|experiment)\b', text_lower):
|
| 391 |
non_paper_indicators.append(True)
|
| 392 |
+
|
| 393 |
+
# If we have strong non-paper indicators AND weak academic indicators, it's not a paper
|
| 394 |
if any(non_paper_indicators) and indicator_count < 6:
|
| 395 |
return False, 0, "Content appears to be a non-academic document"
|
| 396 |
+
|
| 397 |
+
# Get AI assessment if available and requested
|
| 398 |
ai_result = None
|
| 399 |
ai_confidence = 0
|
| 400 |
ai_reason = ""
|
| 401 |
+
|
| 402 |
+
if use_ai and AI_CLASSIFIER_AVAILABLE:
|
| 403 |
ai_result, ai_confidence, ai_reason = ai_check_paper(text)
|
| 404 |
+
|
| 405 |
+
# Combine rule-based and AI assessments
|
| 406 |
+
# Rule-based decision logic
|
| 407 |
if indicator_count == 9:
|
| 408 |
+
rule_decision = True
|
| 409 |
+
rule_confidence = 0.9
|
| 410 |
+
rule_reason = f"Found all {indicator_count}/9 academic paper indicators"
|
| 411 |
elif indicator_count >= 6:
|
| 412 |
+
rule_decision = True
|
| 413 |
+
rule_confidence = 0.6
|
| 414 |
missing = [k for k, v in indicators.items() if not v]
|
| 415 |
+
rule_reason = f"Found only {indicator_count}/9 indicators. Missing: {', '.join(missing)}"
|
| 416 |
else:
|
| 417 |
+
rule_decision = False
|
| 418 |
+
rule_confidence = 0
|
| 419 |
missing = [k for k, v in indicators.items() if not v]
|
| 420 |
rule_reason = f"Found only {indicator_count}/9 indicators. Missing: {', '.join(missing[:4])}"
|
| 421 |
+
|
| 422 |
+
# Combine decisions
|
| 423 |
if ai_result is not None:
|
| 424 |
+
# Both methods available - combine them
|
| 425 |
+
# Weight: 60% rule-based, 40% AI
|
| 426 |
combined_confidence = (rule_confidence * 0.6) + (ai_confidence * 0.4)
|
| 427 |
+
|
| 428 |
+
# Decision logic
|
| 429 |
if rule_decision and ai_result:
|
| 430 |
+
# Both agree it's a paper
|
| 431 |
+
if combined_confidence >= 0.9:
|
| 432 |
+
return True, 0.9, f"High confidence: {rule_reason}. {ai_reason}"
|
| 433 |
+
else:
|
| 434 |
+
return True, 0.6, f"Warning: {rule_reason}. {ai_reason}"
|
| 435 |
elif not rule_decision and not ai_result:
|
| 436 |
+
# Both agree it's not a paper
|
| 437 |
return False, 0, f"Not a research paper. {rule_reason}. {ai_reason}"
|
| 438 |
else:
|
| 439 |
+
# Disagreement - use weighted decision
|
| 440 |
if combined_confidence >= 0.5:
|
| 441 |
return True, 0.6, f"Mixed signals: {rule_reason}. {ai_reason}"
|
| 442 |
else:
|
| 443 |
return False, 0, f"Likely not a research paper. {rule_reason}. {ai_reason}"
|
| 444 |
else:
|
| 445 |
+
# Only rule-based available
|
| 446 |
if rule_decision:
|
| 447 |
+
if rule_confidence >= 0.9:
|
| 448 |
+
return True, 0.9, f"High confidence: {rule_reason}"
|
| 449 |
+
else:
|
| 450 |
+
return True, 0.6, f"Warning: {rule_reason}"
|
| 451 |
else:
|
| 452 |
return False, 0, f"Does not appear to be a research paper. {rule_reason}"
|
| 453 |
|
| 454 |
def create_messages(review_fields, paper_text):
|
| 455 |
+
messages = [
|
| 456 |
{"role": "system", "content": SYSTEM_PROMPT_TEMPLATE.format(review_fields=review_fields)},
|
| 457 |
+
{"role": "user", "content": USER_PROMPT_TEMPLATE.format(paper_text=paper_text)},
|
| 458 |
]
|
| 459 |
+
return messages
|
| 460 |
|
| 461 |
@spaces.GPU()
|
| 462 |
def convert_file(filepath):
|
| 463 |
full_text, images, out_metadata = convert_single_pdf(
|
| 464 |
+
filepath, model_refs, metadata=metadata, max_pages=MAX_PAGES
|
| 465 |
)
|
| 466 |
return full_text
|
| 467 |
|
|
|
|
| 473 |
filetype = find_filetype(filepath)
|
| 474 |
if filetype == "other":
|
| 475 |
raise ValueError()
|
| 476 |
+
|
| 477 |
length = get_length_of_text(filepath)
|
| 478 |
if length < MIN_LENGTH:
|
| 479 |
raise ValueError()
|
| 480 |
+
paper_text = convert_file(filepath)
|
| 481 |
+
paper_text = paper_text.strip()
|
| 482 |
if not len(paper_text) > MIN_LENGTH:
|
| 483 |
raise ValueError()
|
| 484 |
except spaces.zero.gradio.HTMLError as e:
|
|
|
|
| 488 |
print(traceback.format_exc())
|
| 489 |
print(f"Error converting {filepath}: {e}")
|
| 490 |
return "Error processing pdf", False
|
| 491 |
+
|
| 492 |
+
# Check if it's a research paper (with AI)
|
| 493 |
is_paper, confidence, reason = is_research_paper(paper_text, use_ai=True)
|
| 494 |
if not is_paper:
|
| 495 |
+
return f"⚠️ **Not a Research Paper**\n\nThe uploaded document does not appear to be a research paper.\n\nReason: {reason}\n\nPlease upload a proper academic/research paper with sections like Abstract, Introduction, Methodology, Results, and References.", False
|
| 496 |
+
|
| 497 |
+
# If confidence is low (6-8 indicators), add a warning
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
if confidence < 0.9:
|
| 499 |
+
paper_text = f"⚠️ **Warning**: {reason}. \n\nThe document may be incomplete or missing key sections. Proceeding with review generation...\n\n---\n\n{paper_text}"
|
| 500 |
+
|
|
|
|
|
|
|
|
|
|
| 501 |
return paper_text, True
|
| 502 |
|
| 503 |
+
# ======== 新增:一步式评审端点(自动“先摘要后评审” + 截断) ========
|
| 504 |
@spaces.GPU(duration=90)
|
| 505 |
def review(paper_text: str, review_template: str):
|
| 506 |
+
# Quick paper check(不用 AI 分类,加速)
|
| 507 |
is_paper, confidence, reason = is_research_paper(paper_text, use_ai=False)
|
| 508 |
if not is_paper:
|
| 509 |
return f"⚠️ Cannot generate review: {reason}"
|
| 510 |
|
| 511 |
+
# 检测是否超长,必要时先摘要
|
| 512 |
pt_ids = tokenizer(paper_text, return_tensors="pt", add_special_tokens=False)["input_ids"][0]
|
| 513 |
if pt_ids.size(0) > LONG_TEXT_THRESHOLD_TOK:
|
| 514 |
summary_prompt = (
|
|
|
|
| 516 |
)
|
| 517 |
messages = [
|
| 518 |
{"role": "system", "content": "You are a helpful scientific summarizer."},
|
| 519 |
+
{"role": "user", "content": f"{summary_prompt}\n\n---\n\n{paper_text}"}
|
| 520 |
]
|
| 521 |
sum_ids = tokenizer.apply_chat_template(
|
| 522 |
messages, add_generation_prompt=True, return_tensors="pt"
|
|
|
|
| 529 |
)
|
| 530 |
paper_text = tokenizer.decode(sum_out[0][sum_ids.shape[1]:], skip_special_tokens=True)
|
| 531 |
|
| 532 |
+
# 截断评审输入,保证不会过长
|
| 533 |
paper_text = truncate_by_tokens(paper_text, tokenizer, MAX_INPUT_TOKENS)
|
| 534 |
|
| 535 |
+
# 构建评审消息并一次性生成(非流式,适合 API 调用)
|
| 536 |
messages = create_messages(review_template, paper_text)
|
| 537 |
input_ids = tokenizer.apply_chat_template(
|
| 538 |
messages, add_generation_prompt=True, return_tensors='pt'
|
| 539 |
).to(model.device)
|
|
|
|
| 540 |
|
| 541 |
+
attention_mask = torch.ones_like(input_ids)
|
| 542 |
+
generation_kwargs = dict(
|
| 543 |
input_ids=input_ids,
|
| 544 |
attention_mask=attention_mask,
|
| 545 |
max_new_tokens=MAX_NEW_TOKENS,
|
|
|
|
| 548 |
top_p=0.9,
|
| 549 |
pad_token_id=tokenizer.pad_token_id
|
| 550 |
)
|
| 551 |
+
out = model.generate(**generation_kwargs)
|
| 552 |
return tokenizer.decode(out[0][input_ids.shape[1]:], skip_special_tokens=True).replace("<|eot_id|>", "")
|
| 553 |
+
# ======== 新增结束 ========
|
| 554 |
|
|
|
|
| 555 |
@spaces.GPU(duration=90)
|
| 556 |
def generate(paper_text, review_template):
|
| 557 |
+
# Final check before generation
|
| 558 |
+
is_paper, confidence, reason = is_research_paper(paper_text, use_ai=False) # Quick check without AI
|
| 559 |
if not is_paper:
|
| 560 |
return f"⚠️ Cannot generate review: {reason}"
|
| 561 |
|
| 562 |
+
# ——关键:先按 token 截断输入,避免上下文超大——
|
| 563 |
paper_text = truncate_by_tokens(paper_text, tokenizer, MAX_INPUT_TOKENS)
|
| 564 |
+
|
| 565 |
messages = create_messages(review_template, paper_text)
|
| 566 |
input_ids = tokenizer.apply_chat_template(
|
| 567 |
messages,
|
| 568 |
add_generation_prompt=True,
|
| 569 |
return_tensors='pt'
|
| 570 |
).to(model.device)
|
| 571 |
+
|
| 572 |
+
# Create attention mask
|
| 573 |
attention_mask = torch.ones_like(input_ids)
|
| 574 |
+
|
| 575 |
print(f"input_ids shape (truncated): {input_ids.shape}")
|
| 576 |
generation_kwargs = dict(
|
| 577 |
input_ids=input_ids,
|
| 578 |
attention_mask=attention_mask,
|
| 579 |
streamer=streamer,
|
| 580 |
+
max_new_tokens=MAX_NEW_TOKENS, # from 4096 -> 512
|
| 581 |
do_sample=True,
|
| 582 |
+
temperature=0.5, # 略降温度,更稳更短
|
| 583 |
top_p=0.9,
|
| 584 |
pad_token_id=tokenizer.pad_token_id
|
| 585 |
)
|
|
|
|
| 590 |
generated_text += new_text
|
| 591 |
yield generated_text.replace("<|eot_id|>", "")
|
| 592 |
|
| 593 |
+
# UI code remains exactly the same...
|
| 594 |
+
# ui
|
| 595 |
+
title = """<h1 align="center">OpenReviewer</h1>
|
|
|
|
| 596 |
<div align="center">Using <a href="https://huggingface.co/maxidl/Llama-OpenReviewer-8B" target="_blank"><code>Llama-OpenReviewer-8B</code></a> - Built with Llama</div>
|
| 597 |
"""
|
| 598 |
|
|
|
|
| 600 |
|
| 601 |
## Demo Guidelines
|
| 602 |
|
| 603 |
+
1. Upload your paper as a PDF file. Alternatively you can paste the full text of your paper in markdown format below. We do **not** store your data. User data is kept in ephemeral storage during processing.
|
| 604 |
+
|
| 605 |
+
2. Once you upload a PDF, it will be converted to markdown and **validated to ensure it's a research paper**. This takes some time as it runs multiple transformer models to parse the layout and extract text and tables. Check out [marker](https://github.com/VikParuchuri/marker/tree/master) for details.
|
| 606 |
+
|
| 607 |
+
3. Having obtained a markdown version of your paper and confirmed it's a valid research paper, you can now click *Generate Review*.
|
| 608 |
+
|
| 609 |
+
Take a look at the Review Template to properly interpret the generated review. You can also change the review template before generating in case you want to generate a review with a different schema and aspects.
|
| 610 |
+
|
| 611 |
+
To obtain more than one review, just generate again.
|
| 612 |
+
|
| 613 |
+
**GPU quota:** If exceeded, either sign in with your HF account or come back later. Your quota has a half-life of 2 hours.
|
| 614 |
+
|
| 615 |
"""
|
| 616 |
|
| 617 |
theme = gr.themes.Default(primary_hue="gray", secondary_hue="blue", neutral_hue="slate")
|
| 618 |
with gr.Blocks(theme=theme) as demo:
|
| 619 |
+
title = gr.HTML(title)
|
| 620 |
+
description = gr.Markdown(description)
|
| 621 |
+
|
| 622 |
+
# Add paper validation status
|
| 623 |
with gr.Row():
|
| 624 |
file_input = gr.File(file_types=[".pdf"], file_count="single")
|
| 625 |
validation_status = gr.Markdown("", visible=False)
|
| 626 |
+
|
| 627 |
+
paper_text_field = gr.Textbox("Upload a pdf or paste the full text of your paper in markdown format here.", label="Paper Text", lines=20, max_lines=20, autoscroll=False)
|
| 628 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
with gr.Accordion("Review Template", open=False):
|
| 630 |
+
review_template_description = gr.Markdown("We use the ICLR 2025 review template by default, but you can modify the template below as you like.")
|
| 631 |
+
review_template_field = gr.Textbox(label=" ",lines=20, max_lines=20, autoscroll=False, value=REVIEW_FIELDS)
|
| 632 |
+
|
| 633 |
generate_button = gr.Button("Generate Review", interactive=False)
|
| 634 |
+
|
| 635 |
def handle_file_upload(file):
|
| 636 |
if file is None:
|
| 637 |
return "", gr.update(visible=False), gr.update(interactive=False)
|
| 638 |
text, is_valid = process_file(file)
|
| 639 |
if is_valid:
|
| 640 |
+
# Check confidence level for appropriate message
|
| 641 |
+
is_paper, confidence, reason = is_research_paper(text, use_ai=False) # Quick check for display
|
| 642 |
if confidence >= 0.9:
|
| 643 |
status_msg = "✅ **Document validated**: This appears to be a complete research paper."
|
| 644 |
else:
|
|
|
|
| 646 |
return text, gr.update(value=status_msg, visible=True), gr.update(interactive=True)
|
| 647 |
else:
|
| 648 |
return text, gr.update(value="❌ **Validation failed**: Please upload a research paper.", visible=True), gr.update(interactive=False)
|
| 649 |
+
|
| 650 |
def handle_text_change(text):
|
| 651 |
if not text or len(text) < 200:
|
| 652 |
return gr.update(interactive=False), gr.update(visible=False)
|
| 653 |
+
|
| 654 |
is_paper, confidence, reason = is_research_paper(text, use_ai=True)
|
| 655 |
if is_paper:
|
| 656 |
+
if confidence >= 0.9:
|
| 657 |
+
status = "✅ **Text validated**: This appears to be a complete research paper."
|
| 658 |
+
else:
|
| 659 |
+
# confidence < 0.9 means warning (6-8 indicators)
|
| 660 |
+
status = f"⚠️ **Warning**: {reason}\n\nThe document may be incomplete or missing key sections."
|
| 661 |
return gr.update(interactive=True), gr.update(value=status, visible=True)
|
| 662 |
else:
|
| 663 |
return gr.update(interactive=False), gr.update(value=f"❌ **Not a research paper**: {reason}", visible=True)
|
| 664 |
+
|
| 665 |
file_input.upload(handle_file_upload, file_input, [paper_text_field, validation_status, generate_button])
|
| 666 |
paper_text_field.change(handle_text_change, paper_text_field, [generate_button, validation_status])
|
| 667 |
+
|
| 668 |
review_field = gr.Markdown("\n\n\n\n\n", label="Review")
|
| 669 |
|
| 670 |
+
# 生成按钮(流式),显式命名 /generate
|
| 671 |
generate_button.click(
|
| 672 |
+
fn=lambda: gr.update(interactive=False),
|
| 673 |
+
inputs=None,
|
| 674 |
outputs=generate_button
|
| 675 |
).then(
|
| 676 |
+
generate,
|
| 677 |
+
[paper_text_field, review_template_field],
|
| 678 |
review_field,
|
| 679 |
+
api_name="generate" # 显式命名
|
| 680 |
).then(
|
| 681 |
+
fn=lambda: gr.update(interactive=True),
|
| 682 |
+
inputs=None,
|
| 683 |
outputs=generate_button
|
| 684 |
)
|
| 685 |
|
| 686 |
+
# (可选)隐藏按钮以暴露“一步式” /review 端点,方便 SDK 直接调用
|
| 687 |
+
dummy = gr.Button(visible=False)
|
| 688 |
+
dummy.click(
|
| 689 |
+
review, [paper_text_field, review_template_field], review_field, api_name="review"
|
|
|
|
|
|
|
| 690 |
)
|
| 691 |
+
|
| 692 |
demo.title = "OpenReviewer"
|
| 693 |
|
| 694 |
if __name__ == "__main__":
|