import requests import pandas as pd import time from datetime import datetime import gradio as gr import os import re import bibtexparser from bibtexparser.bparser import BibTexParser from bibtexparser.customization import convert_to_unicode from difflib import SequenceMatcher import numpy as np from collections import Counter import math # ============================================ # CLEAN RESPONSIVE CSS - (same as your original) # ============================================ custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); :root { --primary: #2563eb; --secondary: #7c3aed; --border: #d1d5db; --bg-soft: #e5e7eb !important; --text-main: #111827; } /* ================================================= */ /* GENERAL LAYOUT */ /* ================================================= */ body, .gradio-container { font-family: 'Inter', sans_serif !important; background-color: #e5e7eb !important; color: var(--text-main) !important; } .gradio-container { max-width: 1600px !important; margin: auto !important; } .white-card { background: #ffffff !important; border: 1px solid var(--border) !important; border-radius: 12px !important; padding: 2rem !important; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; } h1 span { background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .subtitle-text { color: #2563eb !important; font-weight: 500 !important; } /* ================================================= */ /* METRICS */ /* ================================================= */ .metric-value { font-size: 2rem !important; font-weight: 700 !important; color: var(--primary) !important; } /* ================================================= */ /* BUTTONS */ /* ================================================= */ button, .gr-button { color: #ffffff !important; font-weight: 600 !important; } button.variant-primary { background-color: var(--primary) !important; } button.variant-secondary { background-color: #6b7280 !important; } /* ================================================= */ /* TABS */ /* ================================================= */ .tab-nav { background-color: #f3f4f6 !important; border-radius: 8px !important; padding: 4px !important; margin-bottom: 20px !important; } .tab-nav button { border-radius: 6px !important; padding: 8px 16px !important; font-weight: 500 !important; color: #4b5563 !important; } .tab-nav button.selected { background-color: var(--primary) !important; color: white !important; } /* ================================================= */ /* SLIDER COMMENT */ /* ================================================= */ .slider-comment { color: #ffffff !important; font-size: 0.9rem !important; margin-top: 8px !important; margin-bottom: 8px !important; font-style: italic !important; background: #1f2937 !important; padding: 8px 12px !important; border-radius: 6px !important; border-left: 4px solid var(--primary) !important; font-weight: 500 !important; } /* ================================================= */ /* DROPDOWN BUTTON (Closed State) */ /* ================================================= */ .gr-dropdown, .gr-dropdown select, select { background-color: #374151 !important; color: #ffffff !important; border: 1px solid #4b5563 !important; border-radius: 6px !important; } /* ========================================= */ /* FORCE BUTTON TEXT TO ALWAYS BE WHITE */ /* ========================================= */ /* All buttons */ button, .gr-button { color: #ffffff !important; } /* Primary button */ button.variant-primary { background-color: #2563eb !important; color: #ffffff !important; } /* Secondary button */ button.variant-secondary { background-color: #6b7280 !important; color: #ffffff !important; } /* Keep text white even on hover */ button:hover { color: #ffffff !important; } /* ================================================= */ /* METRIC LABEL BLACK BACKGROUND */ /* ================================================= */ .metric-label { display: inline-block !important; background-color: #111827 !important; color: #ffffff !important; padding: 6px 12px !important; border-radius: 6px !important; font-weight: 600 !important; } /* ================================================= */ /* METRIC VALUE BLACK BACKGROUND FIX */ /* ================================================= */ .metric-value { display: inline-block !important; background-color: #111827 !important; color: #ffffff !important; padding: 6px 12px !important; border-radius: 6px !important; font-weight: 700 !important; } /* Remove default white background from strong */ .metric-value strong { background: transparent !important; color: #ffffff !important; } /* ================================================= */ /* DROPDOWN POPUP PAGE - WHITE BACKGROUND */ /* ================================================= */ /* The popup menu that appears when clicking dropdown */ .gr-dropdown-list, .dropdown-list, div[role="listbox"], .options-container, select option, .gr-dropdown option, .gradio-dropdown option, div.gr-box select option { background-color: #ffffff !important; /* Pure white background */ color: #111827 !important; /* Dark text */ } /* Individual options */ select option, .gr-dropdown option, .gradio-dropdown option { background-color: #ffffff !important; color: #111827 !important; padding: 8px 12px !important; } /* Hover effect */ select option:hover, .gr-dropdown option:hover, .gradio-dropdown option:hover { background-color: #f3f4f6 !important; /* Light gray on hover */ color: #111827 !important; } /* Selected option */ select option:checked, .gr-dropdown option:checked, .gradio-dropdown option:checked { background-color: #2563eb !important; color: white !important; } /* ================================================= */ /* PDF LINK STYLING */ /* ================================================= */ .pdf-link { color: #2563eb !important; text-decoration: none !important; font-weight: 500 !important; } .pdf-link:hover { text-decoration: underline !important; color: #1d4ed8 !important; } .no-pdf { color: #6b7280 !important; font-style: italic !important; } /* ================================================= */ /* REFERENCE CHECKER TABLE STYLING */ /* ================================================= */ .ref-valid { color: #10b981 !important; font-weight: 600 !important; } .ref-invalid { color: #ef4444 !important; font-weight: 600 !important; } .ref-partial { color: #f59e0b !important; font-weight: 600 !important; } .comparison-table { border-collapse: collapse !important; width: 100% !important; } .comparison-table th { background-color: #2563eb !important; color: white !important; padding: 10px !important; text-align: left !important; } .comparison-table td { padding: 8px !important; border-bottom: 1px solid #e5e7eb !important; vertical-align: top !important; } .comparison-table tr:hover { background-color: #f9fafb !important; } .original-ref { background-color: #fee2e2 !important; padding: 4px 8px !important; border-radius: 4px !important; border-left: 3px solid #ef4444 !important; } .corrected-ref { background-color: #dcfce7 !important; padding: 4px 8px !important; border-radius: 4px !important; border-left: 3px solid #10b981 !important; } """ # ============================================ # SEARCH ENGINE - ENHANCED WITH GOOGLE SCHOLAR LIKE SENSITIVITY # ============================================ class PaperSearchPro: def __init__(self): self.base_url = "https://api.openalex.org/works" self.per_page = 200 # Common stop words to ignore in matching self.stop_words = set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'may', 'might', 'must', 'can']) def calculate_text_similarity(self, text1, text2): """Calculate similarity between two texts using multiple methods""" if not text1 or not text2: return 0 text1 = text1.lower() text2 = text2.lower() # Method 1: Sequence matching (good for phrases) seq_similarity = SequenceMatcher(None, text1, text2).ratio() # Method 2: Word overlap (good for keywords) words1 = set(re.findall(r'\w+', text1)) words2 = set(re.findall(r'\w+', text2)) # Remove stop words for better matching words1 = words1 - self.stop_words words2 = words2 - self.stop_words if not words1 or not words2: return seq_similarity intersection = words1.intersection(words2) union = words1.union(words2) word_similarity = len(intersection) / len(union) if union else 0 # Method 3: Partial word matching (for stem variations) partial_matches = 0 for w1 in words1: for w2 in words2: if len(w1) > 3 and len(w2) > 3: if w1 in w2 or w2 in w1 or SequenceMatcher(None, w1, w2).ratio() > 0.8: partial_matches += 1 break partial_ratio = partial_matches / len(words1) if words1 else 0 # Weighted combination return (seq_similarity * 0.3 + word_similarity * 0.5 + partial_ratio * 0.2) def extract_keywords_from_phrase(self, phrase): """Extract meaningful keywords from a phrase""" words = re.findall(r'\w+', phrase.lower()) # Remove stop words and short words keywords = [w for w in words if w not in self.stop_words and len(w) > 2] # Also include bigrams if phrase has multiple words if len(words) > 1: bigrams = [' '.join(words[i:i+2]) for i in range(len(words)-1)] keywords.extend(bigrams) return list(set(keywords)) # Remove duplicates def search_papers(self, query, keywords, authors, max_papers, year_from, year_to, open_access, sort_by, sort_order): filters = [] if year_from: filters.append(f"publication_year:>{int(year_from)-1}") if year_to: filters.append(f"publication_year:<{int(year_to)+1}") if open_access: filters.append("open_access.is_oa:true") # Add author filter if provided if authors and authors.strip(): # Clean and format author names for filtering author_list = [a.strip() for a in authors.split(",") if a.strip()] if author_list: # OpenAlex uses author.search filter author_filter = "|".join(author_list) filters.append(f"author.search:{author_filter}") filter_str = ",".join(filters) if filters else None # Map sort options to API parameters sort_field_map = { "Relevance": "relevance_score", "Year": "publication_date", "Citations": "cited_by_count" } sort_field = sort_field_map.get(sort_by, "relevance_score") sort_direction = "desc" if sort_order == "Descending" else "asc" sort_param = f"{sort_field}:{sort_direction}" all_papers = [] page = 1 while len(all_papers) < max_papers: params = { "search": query, "per-page": self.per_page, "page": page, "sort": sort_param } if filter_str: params["filter"] = filter_str try: response = requests.get(self.base_url, params=params, timeout=20) data = response.json() results = data.get("results", []) if not results: break for work in results: paper = self.extract_info(work) all_papers.append(paper) page += 1 time.sleep(0.2) except Exception as e: print(f"Error: {e}") break # =============================== # ENHANCED KEYWORD FILTERING - GOOGLE SCHOLAR LIKE SENSITIVITY # Supports unlimited keywords with fuzzy matching and relevance scoring # =============================== keyword_phrases = [k.strip() for k in keywords.split(",") if k.strip()] if len(keyword_phrases) >= 1: # At least one keyword required filtered = [] # Pre-process each keyword phrase into search terms keyword_search_terms = [] for phrase in keyword_phrases: # Extract meaningful keywords from each phrase terms = self.extract_keywords_from_phrase(phrase) keyword_search_terms.append({ 'original': phrase, 'terms': terms, 'weight': 1.0 }) # Calculate IDF-like weights for terms (rarer terms get higher weight) all_terms = [] for kst in keyword_search_terms: all_terms.extend(kst['terms']) term_freq = Counter(all_terms) total_terms = len(all_terms) for kst in keyword_search_terms: # Adjust weight based on term rarity avg_freq = sum(term_freq[t] for t in kst['terms']) / len(kst['terms']) if kst['terms'] else 1 # Rarer terms (lower frequency) get higher weight kst['weight'] = 1.0 / (avg_freq / total_terms + 0.1) if avg_freq > 0 else 1.0 for paper in all_papers: # Combine all text fields for searching combined_text = f"{paper['Title']} {paper['Abstract']} {paper['Authors']}".lower() # Calculate relevance score using multiple factors score = 0 matched_terms = [] matched_phrases = [] # Check each keyword phrase for i, kst in enumerate(keyword_search_terms): phrase_score = 0 phrase_matched_terms = [] # Check each term in the phrase for term in kst['terms']: # Exact match (highest score) if term in combined_text: term_score = 1.0 * kst['weight'] phrase_matched_terms.append(term) else: # Fuzzy/partial matching # Check if term appears as part of a word words = re.findall(r'\w+', combined_text) best_partial = 0 for word in words: if len(word) > 3 and len(term) > 3: # Check if term is a substring of word if term in word: best_partial = max(best_partial, 0.8) # Check word similarity elif SequenceMatcher(None, term, word).ratio() > 0.7: sim = SequenceMatcher(None, term, word).ratio() best_partial = max(best_partial, sim * 0.7) if best_partial > 0: term_score = best_partial * kst['weight'] phrase_matched_terms.append(f"{term}~") else: term_score = 0 if term_score > 0: phrase_score += term_score # Normalize phrase score by number of terms if kst['terms']: phrase_score = phrase_score / len(kst['terms']) # First two phrases are required (must have at least partial match) if i < 2: if phrase_score > 0.3: # Partial match threshold for required phrases score += phrase_score * 2 # Higher weight for first two matched_phrases.append(kst['original']) matched_terms.extend(phrase_matched_terms) else: # Required phrase not matched well enough - skip this paper score = 0 break else: # Additional phrases boost score if phrase_score > 0: score += phrase_score matched_phrases.append(kst['original']) matched_terms.extend(phrase_matched_terms) if score > 0: # Additional boost factors # 1. Title match boost (keywords in title are more important) title_text = paper['Title'].lower() title_boost = 0 for term in set(matched_terms): term_clean = term.replace('~', '') if term_clean in title_text: title_boost += 0.2 score += title_boost # 2. Recent papers boost (slightly) if paper['Year'] != 'N/A' and paper['Year'] >= 2020: score += 0.1 # 3. Citation boost (log scale) if paper['Citations'] > 0: citation_boost = math.log10(paper['Citations'] + 1) * 0.05 score += min(citation_boost, 0.3) paper['Relevance'] = round(score, 2) paper['Matched Keywords'] = f"Phrases: {', '.join(matched_phrases[:5])}" + \ (f" (+{len(matched_phrases)-5} more)" if len(matched_phrases) > 5 else "") + \ f" | Terms: {', '.join(matched_terms[:10])}" + \ (f" (+{len(matched_terms)-10} more)" if len(matched_terms) > 10 else "") filtered.append(paper) # Sort by relevance score (descending) filtered.sort(key=lambda x: x['Relevance'], reverse=True) all_papers = filtered[:max_papers] # =============================== # AUTHOR FILTERING (if no keywords) # =============================== elif authors and authors.strip() and not keyword_phrases: # If we have author filter but no keyword hierarchy author_list = [a.strip().lower() for a in authors.split(",") if a.strip()] filtered = [] for p in all_papers: all_authors_lower = p['All Authors'].lower() match_all = True matched_authors = [] for author in author_list: # Split author name into parts for partial matching author_parts = author.split() author_matched = False # Check if any part of the author name appears for part in author_parts: if len(part) > 2 and part in all_authors_lower: # Only check significant parts author_matched = True matched_authors.append(part) break if not author_matched: # Fallback: check if the whole author string appears if author in all_authors_lower: author_matched = True matched_authors.append(author) else: match_all = False break if match_all: p["Relevance"] = len(matched_authors) p["Matched Keywords"] = f"Authors: {', '.join(matched_authors)}" filtered.append(p) all_papers = filtered[:max_papers] total = len(all_papers) avg_rel = sum(p.get("Relevance", 0) for p in all_papers) / total if total else 0 years = [p["Year"] for p in all_papers if p["Year"] != "N/A"] year_range = f"{min(years)} - {max(years)}" if years else "N/A" df = pd.DataFrame(all_papers) # Reorder columns to include BibTeX and remove All Authors from display column_order = ['Title', 'Authors', 'Year', 'Citations', 'Journal', 'PDF_URL', 'Abstract', 'BibTeX', 'Relevance', 'Matched Keywords'] df = df[column_order] return df, all_papers, total, avg_rel, year_range def search_specific_paper(self, search_type, search_value): """Search for a specific paper by title, author, or DOI""" all_papers = [] if search_type == "DOI": # Clean DOI (remove URL if present) if "doi.org/" in search_value: search_value = search_value.split("doi.org/")[-1] # Direct lookup by DOI url = f"https://api.openalex.org/works/doi/{search_value}" try: response = requests.get(url, timeout=15) if response.status_code == 200: work = response.json() paper = self.extract_info(work) all_papers.append(paper) except Exception as e: print(f"Error fetching DOI: {e}") elif search_type == "Exact Title": # Search by exact title with fuzzy matching params = { "search": search_value, "per-page": 20, "sort": "relevance_score:desc" } try: response = requests.get(self.base_url, params=params, timeout=15) data = response.json() results = data.get("results", []) best_match = None best_score = 0 for work in results: title = work.get("title", "") if title: # Calculate similarity score similarity = self.calculate_text_similarity(search_value, title) if similarity > best_score and similarity > 0.4: # Threshold for acceptance best_score = similarity best_match = work if best_match: paper = self.extract_info(best_match) paper['Match_Score'] = round(best_score, 2) all_papers.append(paper) except Exception as e: print(f"Error searching title: {e}") elif search_type == "Specific Author": # Search for papers by a specific author params = { "search": search_value, "filter": "authorships.author.display_name.search:" + search_value, "per-page": 50, "sort": "cited_by_count:desc" } try: response = requests.get(self.base_url, params=params, timeout=15) data = response.json() results = data.get("results", []) for work in results[:20]: # Limit to 20 papers paper = self.extract_info(work) all_papers.append(paper) except Exception as e: print(f"Error searching author: {e}") if all_papers: for i, p in enumerate(all_papers): p['Rank'] = i + 1 df = pd.DataFrame(all_papers) column_order = ['Title', 'Authors', 'Year', 'Citations', 'Journal', 'PDF_URL', 'Abstract', 'BibTeX'] if 'Match_Score' in df.columns: column_order.insert(1, 'Match_Score') # Only include columns that exist available_cols = [col for col in column_order if col in df.columns] df = df[available_cols] summary = f"β Found **{len(all_papers)}** paper(s) matching your query" return df, all_papers, summary return pd.DataFrame(), [], "β No papers found" def extract_info(self, work): # Rebuild abstract abstract = "" if work.get("abstract_inverted_index"): words = [] for word, positions in work["abstract_inverted_index"].items(): for pos in positions: words.append((pos, word)) words.sort() abstract = " ".join([w[1] for w in words]) else: abstract = "No abstract available" # Get all authors - FULL LIST (no et al.) all_authors = [] for authorship in work.get("authorships", []): # FIRST PRIORITY: Use raw_author_name (full name as published) raw_name = authorship.get("raw_author_name", "") display_name = authorship.get("author", {}).get("display_name", "") # Choose the best name if raw_name and len(raw_name) > 2: # Raw name is usually more complete (e.g., "Mamoru SHOJI" vs "M. Shoji") author_name = raw_name elif display_name: author_name = display_name else: author_name = "" if author_name: all_authors.append(author_name) # Show ALL authors, no truncation authors_str = ", ".join(all_authors) # Get DOI for reference doi = work.get("doi", "") if doi: doi = doi.replace("https://doi.org/", "") # Get PDF URL if available pdf_url = "" if work.get("open_access", {}).get("oa_url"): pdf_url = work["open_access"]["oa_url"] elif work.get("primary_location", {}).get("pdf_url"): pdf_url = work["primary_location"]["pdf_url"] else: pdf_url = "No free PDF available" # Generate BibTeX entry bibtex = self.generate_bibtex(work, all_authors) return { "Title": work.get("title", "No title"), "Authors": authors_str, "All Authors": ", ".join(all_authors), # Hidden field for filtering "Year": work.get("publication_year", "N/A"), "Citations": work.get("cited_by_count", 0), "Journal": (work.get("primary_location", {}).get("source") or {}).get("display_name", "Unknown"), "DOI": doi, "PDF_URL": pdf_url, "Abstract": abstract, "BibTeX": bibtex, "Relevance": 0, "Matched Keywords": "" } def generate_bibtex(self, work, authors): """Generate BibTeX entry for a paper""" # Create citation key first_author = authors[0].split()[-1] if authors else "Unknown" year = work.get("publication_year", "0000") title_words = work.get("title", "Untitled").split()[:3] title_short = "".join([w[0] for w in title_words if w]) if title_words else "Paper" citation_key = f"{first_author}{year}{title_short}" # Clean the citation key (remove special characters) citation_key = ''.join(c for c in citation_key if c.isalnum()) # Get DOI or URL doi = work.get("doi", "") url = work.get("primary_location", {}).get("landing_page_url", "") if not url and work.get("open_access", {}).get("oa_url"): url = work.get("open_access", {}).get("oa_url") # Determine entry type (article, book, etc.) pub_type = "article" if work.get("type") == "book": pub_type = "book" elif work.get("type") == "book-chapter": pub_type = "incollection" # Build BibTeX string bibtex_lines = [ f"@{pub_type}{{{citation_key},", f" title = {{{work.get('title', 'Untitled')}}},", f" author = {{{' and '.join(authors)}}},", f" year = {{{work.get('publication_year', '0000')}}}," ] # Add journal/booktitle source = (work.get("primary_location", {}).get("source") or {}) if pub_type == "article": journal = source.get("display_name", "Unknown Journal") bibtex_lines.append(f" journal = {{{journal}}},") else: booktitle = source.get("display_name", "Unknown Publication") bibtex_lines.append(f" booktitle = {{{booktitle}}},") # Add volume, issue if available if work.get("biblio", {}).get("volume"): bibtex_lines.append(f" volume = {{{work['biblio']['volume']}}},") if work.get("biblio", {}).get("issue"): bibtex_lines.append(f" number = {{{work['biblio']['issue']}}},") # Add pages if available if work.get("biblio", {}).get("first_page") and work.get("biblio", {}).get("last_page"): pages = f"{work['biblio']['first_page']}--{work['biblio']['last_page']}" bibtex_lines.append(f" pages = {{{pages}}},") # Add DOI/URL if doi: bibtex_lines.append(f" doi = {{{doi}}},") elif url: bibtex_lines.append(f" url = {{{url}}},") # Add abstract if available if work.get("abstract_inverted_index"): # Rebuild abstract for BibTeX words = [] for word, positions in work["abstract_inverted_index"].items(): for pos in positions: words.append((pos, word)) words.sort() abstract = " ".join([w[1] for w in words]) if abstract: bibtex_lines.append(f" abstract = {{{abstract}}},") # Close the entry bibtex_lines.append("}") return "\n".join(bibtex_lines) # ============================================ # BIBTEX CHECKER FEATURE # ============================================ def parse_bibtex_file(self, bibtex_file): """Parse a BibTeX file and extract entries""" entries = [] try: with open(bibtex_file, 'r', encoding='utf-8') as f: bibtex_str = f.read() # Parse BibTeX parser = BibTexParser(common_strings=True) parser.ignore_nonstandard_types = False parser.homogenize_fields = True bib_database = bibtexparser.loads(bibtex_str, parser=parser) for entry in bib_database.entries: # Extract key fields bib_entry = { 'id': entry.get('ID', ''), 'title': entry.get('title', ''), 'author': entry.get('author', ''), 'year': entry.get('year', ''), 'journal': entry.get('journal', entry.get('booktitle', '')), 'doi': entry.get('doi', ''), 'volume': entry.get('volume', ''), 'number': entry.get('number', ''), 'pages': entry.get('pages', ''), 'publisher': entry.get('publisher', ''), 'entry_type': entry.get('ENTRYTYPE', ''), 'raw_bibtex': self._reconstruct_bibtex(entry) } entries.append(bib_entry) return entries except Exception as e: print(f"Error parsing BibTeX: {e}") return [] def _reconstruct_bibtex(self, entry): """Reconstruct BibTeX string from entry""" bib_lines = [f"@{entry['ENTRYTYPE']}{{{entry['ID']},"] for field, value in entry.items(): if field not in ['ENTRYTYPE', 'ID'] and value: bib_lines.append(f" {field} = {{{value}}},") bib_lines.append("}") return "\n".join(bib_lines) def check_bibtex_entry(self, entry): """Check a BibTeX entry against OpenAlex database""" # Try DOI first if available if entry.get('doi'): doi = entry['doi'].strip() url = f"https://api.openalex.org/works/doi/{doi}" try: response = requests.get(url, timeout=10) if response.status_code == 200: work = response.json() corrected = self.extract_info(work) # Generate corrected BibTeX corrected_bibtex = self.generate_bibtex(work, corrected['Authors'].split(', ')) return { 'original': entry, 'corrected': corrected, 'corrected_bibtex': corrected_bibtex, 'status': 'valid', 'confidence': 1.0, 'method': 'DOI' } except: pass # Try title search with enhanced matching if entry.get('title'): params = { 'search': entry['title'], 'per-page': 10, 'sort': 'relevance_score:desc' } try: response = requests.get(self.base_url, params=params, timeout=10) data = response.json() results = data.get('results', []) if results: best_match = None best_confidence = 0 for work in results: if work.get('title'): # Calculate similarity similarity = self.calculate_text_similarity(entry['title'], work['title']) confidence = similarity * 0.6 # Base confidence from title match # Check year if entry.get('year') and work.get('publication_year'): if str(work['publication_year']) == entry['year']: confidence += 0.25 # Check author if entry.get('author'): work_authors = [] for auth in work.get('authorships', []): name = auth.get('raw_author_name', '') or auth.get('author', {}).get('display_name', '') if name: work_authors.append(name.lower()) work_authors_str = ' '.join(work_authors) entry_authors = entry['author'].lower() # Check if any author matches for author in entry_authors.split(' and '): author_parts = author.strip().split() for part in author_parts: if len(part) > 2 and part in work_authors_str: confidence += 0.15 break confidence = min(confidence, 1.0) if confidence > best_confidence: best_confidence = confidence best_match = work if best_match and best_confidence > 0.4: corrected = self.extract_info(best_match) corrected_bibtex = self.generate_bibtex(best_match, corrected['Authors'].split(', ')) status = 'valid' if best_confidence > 0.8 else 'partial' if best_confidence > 0.5 else 'not_found' return { 'original': entry, 'corrected': corrected, 'corrected_bibtex': corrected_bibtex, 'status': status, 'confidence': best_confidence, 'method': 'title search' } except: pass # No match found return { 'original': entry, 'corrected': None, 'corrected_bibtex': None, 'status': 'not_found', 'confidence': 0, 'method': 'none' } def check_bibtex_file(self, bibtex_file): """Main function to check BibTeX file""" # Parse BibTeX file entries = self.parse_bibtex_file(bibtex_file) if not entries: return None, None, "β No valid BibTeX entries found" # Check each entry results = [] valid_count = 0 partial_count = 0 invalid_count = 0 for i, entry in enumerate(entries): result = self.check_bibtex_entry(entry) results.append(result) if result['status'] == 'valid': valid_count += 1 elif result['status'] == 'partial': partial_count += 1 else: invalid_count += 1 # Small delay to avoid rate limiting time.sleep(0.2) # Create comparison HTML html_output = f"""
β Valid: {valid_count} | β οΈ Partial: {partial_count} | β Not Found: {invalid_count}
Total entries checked: {len(entries)}
| # | Original BibTeX Key | Original Title | Corrected Title | Confidence | Status |
|---|---|---|---|---|---|
| {i+1} | {original['id']} |
{original_title} |
{corrected_title} |
{confidence} | {status_text} |
Easy Academic Paper/journal details by Paul