Spaces:
Sleeping
Sleeping
| """ | |
| 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> {_status_badge(status)}</p>', | |
| f'<p style="margin:0 0 8px 0;color:#475569;font-size:0.9em">{title}', | |
| ] | |
| if authors: | |
| lines.append(f' — <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"> {_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() | |