""" 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 '✓ VERIFIED' if status == "issues_found": return '✗ ISSUES FOUND' return '⚠ INCOMPLETE' def _color_message(msg: str) -> str: if msg.startswith("✓"): return f'{msg}' if msg.startswith("✗"): return f'{msg}' if msg.startswith("⚠"): return f'{msg}' if msg.startswith("ℹ"): return f'{msg}' 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'
', f'

[{idx}/{total}] {cid}   {_status_badge(status)}

', f'

{title}', ] if authors: lines.append(f' — {authors[:120]}{"…" if len(authors) > 120 else ""}') lines.append("

") for msg in result.get("messages", []): lines.append(f'

  {_color_message(msg)}

') lines.append("
") 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 = [ '
', '

Summary

', f'

Total citations: {total}

', f'

Verified: {verified}

', f'

Issues found: {issues}

', f'

Incomplete: {incomplete}

', ] with_issues = [r for r in results if r["status"] == "issues_found"] if with_issues: html.append('

Citations with issues:

") with_403 = [r for r in results if r.get("has_403")] if with_403: html.append('

Manually check links (403 errors):

") no_meta = [r for r in results if r.get("metadata_not_verified")] if no_meta: html.append(f'

Author list could not be auto-verified ({len(no_meta)}):

") 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'

DOI verification via CrossRef — checked: {len(doi_checked)}, verified: {doi_ok}, issues: {len(doi_bad)}

') if doi_bad: html.append('") html.append("
") 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 '

Please upload a .bib file or paste BibTeX content.

' try: entries = _parse_bibtex_string(content) except Exception as exc: return f'

Failed to parse BibTeX: {exc}

' if not entries: return '

No valid BibTeX entries found in the input.

' verifier = CitationVerifier( timeout=int(timeout), max_retries=int(max_retries), crossref_mailto=crossref_mailto.strip(), ) results = [] output_parts = [f'

Found {len(entries)} citation(s). Verifying…

'] 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()