bibverifier / app.py
chrisa142857
feat: add Gradio frontend for Hugging Face Spaces deployment
dc18c33
Raw
History Blame Contribute Delete
9.87 kB
"""
Gradio frontend for the verify_citations tool.
Deployed on Hugging Face Spaces.
"""
import io
import tempfile
import os
import traceback
from typing import Generator
import gradio as gr
import bibtexparser
from bibtexparser.bparser import BibTexParser
from verify_citations.verifier import CitationVerifier
from verify_citations.parser import format_entry_summary
# ── helpers ──────────────────────────────────────────────────────────────────
def _parse_bibtex_string(content: str):
parser = BibTexParser(common_strings=True)
parser.ignore_nonstandard_types = False
return bibtexparser.loads(content, parser).entries
def _status_badge(status: str) -> str:
if status == "verified":
return '<span style="color:#16a34a;font-weight:700">βœ“ VERIFIED</span>'
if status == "issues_found":
return '<span style="color:#dc2626;font-weight:700">βœ— ISSUES FOUND</span>'
return '<span style="color:#d97706;font-weight:700">⚠ INCOMPLETE</span>'
def _color_message(msg: str) -> str:
if msg.startswith("βœ“"):
return f'<span style="color:#16a34a">{msg}</span>'
if msg.startswith("βœ—"):
return f'<span style="color:#dc2626">{msg}</span>'
if msg.startswith("⚠"):
return f'<span style="color:#d97706">{msg}</span>'
if msg.startswith("β„Ή"):
return f'<span style="color:#0891b2">{msg}</span>'
return msg
def _render_result(result: dict, idx: int, total: int) -> str:
title = result.get("title", "(no title)")
cid = result.get("id", "?")
authors = result.get("authors", "")
status = result.get("status", "incomplete")
lines = [
f'<div style="border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:12px">',
f'<p style="margin:0 0 4px 0"><b>[{idx}/{total}] {cid}</b> &nbsp; {_status_badge(status)}</p>',
f'<p style="margin:0 0 8px 0;color:#475569;font-size:0.9em">{title}',
]
if authors:
lines.append(f' &mdash; <em>{authors[:120]}{"…" if len(authors) > 120 else ""}</em>')
lines.append("</p>")
for msg in result.get("messages", []):
lines.append(f'<p style="margin:2px 0;font-size:0.88em">&nbsp;&nbsp;{_color_message(msg)}</p>')
lines.append("</div>")
return "\n".join(lines)
def _render_summary(results: list) -> str:
verified = sum(1 for r in results if r["status"] == "verified")
issues = sum(1 for r in results if r["status"] == "issues_found")
incomplete = sum(1 for r in results if r["status"] == "incomplete")
total = len(results)
html = [
'<div style="border:2px solid #6366f1;border-radius:8px;padding:16px;margin-bottom:16px">',
'<h3 style="margin:0 0 10px 0;color:#4f46e5">Summary</h3>',
f'<p style="margin:2px 0">Total citations: <b>{total}</b></p>',
f'<p style="margin:2px 0;color:#16a34a">Verified: <b>{verified}</b></p>',
f'<p style="margin:2px 0;color:#dc2626">Issues found: <b>{issues}</b></p>',
f'<p style="margin:2px 0;color:#d97706">Incomplete: <b>{incomplete}</b></p>',
]
with_issues = [r for r in results if r["status"] == "issues_found"]
if with_issues:
html.append('<p style="margin-top:10px;font-weight:600;color:#dc2626">Citations with issues:</p><ul style="margin:4px 0">')
for r in with_issues:
html.append(f'<li><code>{r["id"]}</code> β€” {r.get("title","")}</li>')
html.append("</ul>")
with_403 = [r for r in results if r.get("has_403")]
if with_403:
html.append('<p style="margin-top:10px;font-weight:600;color:#d97706">Manually check links (403 errors):</p><ul style="margin:4px 0">')
for r in with_403:
html.append(f'<li><code>{r["id"]}</code> β€” {r.get("title","")}</li>')
html.append("</ul>")
no_meta = [r for r in results if r.get("metadata_not_verified")]
if no_meta:
html.append(f'<p style="margin-top:10px;font-weight:600;color:#d97706">Author list could not be auto-verified ({len(no_meta)}):</p><ul style="margin:4px 0">')
for r in no_meta:
html.append(f'<li><code>{r["id"]}</code> β€” {r.get("title","")}</li>')
html.append("</ul>")
doi_checked = [r for r in results if r["checks"].get("doi_correct") is not None]
if doi_checked:
doi_ok = sum(1 for r in doi_checked if r["checks"]["doi_correct"] is True)
doi_bad = [r for r in doi_checked if r["checks"]["doi_correct"] is False]
html.append(f'<p style="margin-top:10px;color:#0891b2">DOI verification via CrossRef β€” checked: {len(doi_checked)}, verified: {doi_ok}, issues: {len(doi_bad)}</p>')
if doi_bad:
html.append('<ul style="margin:4px 0">')
for r in doi_bad:
html.append(f'<li><code>{r["id"]}</code> β€” {r.get("title","")}</li>')
html.append("</ul>")
html.append("</div>")
return "\n".join(html)
# ── core verification function ────────────────────────────────────────────────
def verify(bibtex_text, bibtex_file, timeout, max_retries, crossref_mailto, progress=gr.Progress(track_tqdm=True)):
# Resolve content
content = ""
if bibtex_file is not None:
with open(bibtex_file, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
elif bibtex_text and bibtex_text.strip():
content = bibtex_text.strip()
if not content:
return '<p style="color:#dc2626">Please upload a .bib file or paste BibTeX content.</p>'
try:
entries = _parse_bibtex_string(content)
except Exception as exc:
return f'<p style="color:#dc2626">Failed to parse BibTeX: {exc}</p>'
if not entries:
return '<p style="color:#d97706">No valid BibTeX entries found in the input.</p>'
verifier = CitationVerifier(
timeout=int(timeout),
max_retries=int(max_retries),
crossref_mailto=crossref_mailto.strip(),
)
results = []
output_parts = [f'<p style="color:#475569">Found <b>{len(entries)}</b> citation(s). Verifying…</p>']
for i, entry in enumerate(progress.tqdm(entries, desc="Verifying citations"), 1):
try:
result = verifier.verify_citation(entry)
except Exception as exc:
result = {
"id": entry.get("ID", f"entry_{i}"),
"title": entry.get("title", ""),
"authors": entry.get("author", ""),
"status": "incomplete",
"checks": {},
"messages": [f"βœ— Unexpected error: {exc}"],
"has_403": False,
"metadata_not_verified": False,
}
results.append(result)
output_parts.append(_render_result(result, i, len(entries)))
yield "\n".join(output_parts)
output_parts.insert(1, _render_summary(results))
yield "\n".join(output_parts)
# ── Gradio UI ─────────────────────────────────────────────────────────────────
DESCRIPTION = """
# Citation Verifier
Verify BibTeX citations by checking whether each paper is findable online,
whether the URL/DOI is valid, and whether the title and author metadata match.
**How to use:** paste BibTeX text *or* upload a `.bib` file, then click **Verify Citations**.
"""
with gr.Blocks(title="Citation Verifier", theme=gr.themes.Soft()) as demo:
gr.Markdown(DESCRIPTION)
with gr.Row():
with gr.Column(scale=1):
bibtex_text = gr.Textbox(
label="Paste BibTeX here",
placeholder="@article{key,\n title={...},\n author={...},\n year={...},\n ...\n}",
lines=18,
max_lines=40,
)
bibtex_file = gr.File(
label="…or upload a .bib file",
file_types=[".bib"],
type="filepath",
)
with gr.Accordion("Advanced options", open=False):
timeout = gr.Slider(5, 60, value=10, step=1, label="Request timeout (seconds)")
max_retries = gr.Slider(1, 10, value=3, step=1, label="Max retries on rate-limit (429)")
crossref_mailto = gr.Textbox(
label="CrossRef polite-pool email (optional)",
placeholder="your@email.com",
max_lines=1,
)
verify_btn = gr.Button("Verify Citations", variant="primary", size="lg")
with gr.Column(scale=2):
output = gr.HTML(label="Results")
verify_btn.click(
fn=verify,
inputs=[bibtex_text, bibtex_file, timeout, max_retries, crossref_mailto],
outputs=output,
)
gr.Examples(
examples=[
[
"""@inproceedings{vaswani2017attention,
title={Attention is all you need},
author={Vaswani, Ashish and Shazeer, Noam and Parmar, Niki and Uszkoreit, Jakob and Jones, Llion and Gomez, Aidan N and Kaiser, Lukasz and Polosukhin, Illia},
booktitle={Advances in Neural Information Processing Systems},
year={2017}
}
@article{devlin2018bert,
title={BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding},
author={Devlin, Jacob and Chang, Ming-Wei and Lee, Kenton and Toutanova, Kristina},
journal={arXiv preprint arXiv:1810.04805},
year={2018}
}""",
None, 10, 3, "",
]
],
inputs=[bibtex_text, bibtex_file, timeout, max_retries, crossref_mailto],
label="Example",
)
if __name__ == "__main__":
demo.launch()