samyak152002's picture
Update app.py
8cc1285 verified
raw
history blame
14.5 kB
import streamlit as st
import re
import fitz # PyMuPDF
from pdfminer.high_level import extract_text
from pdfminer.layout import LAParams
import language_tool_python
from typing import List, Dict, Any, Tuple
from collections import Counter
import json
import traceback
import io
import tempfile
import os
# Set JAVA_HOME environment variable
os.environ['JAVA_HOME'] = '/usr/lib/jvm/java-11-openjdk-amd64'
# Optional: Verify Java installation
try:
java_version = subprocess.check_output(['java', '-version'], stderr=subprocess.STDOUT).decode()
st.write(f"Java Version: {java_version}")
except Exception as e:
st.error("Java is not installed correctly.")
# ------------------------------
# Analysis Functions
# ------------------------------
def extract_pdf_text_by_page(file) -> List[str]:
"""Extracts text from a PDF file, page by page, using PyMuPDF."""
file.seek(0)
with fitz.open(stream=file.read(), filetype="pdf") as doc:
return [page.get_text("text") for page in doc]
def extract_pdf_text(file) -> str:
"""Extracts text from a PDF file using pdfminer."""
file.seek(0)
return extract_text(file, laparams=LAParams())
def check_text_presence(full_text: str, search_terms: List[str]) -> Dict[str, bool]:
"""Checks for the presence of required terms in the text."""
return {term: term.lower() in full_text.lower() for term in search_terms}
def label_authors(full_text: str) -> str:
"""Label authors in the text with 'Authors:' if not already labeled."""
author_line_regex = r"^(?:.*\n)(.*?)(?:\n\n)"
match = re.search(author_line_regex, full_text, re.MULTILINE)
if match:
authors = match.group(1).strip()
return full_text.replace(authors, f"Authors: {authors}")
return full_text
def check_metadata(full_text: str) -> Dict[str, Any]:
"""Check for metadata elements."""
return {
"author_email": bool(re.search(r'\b[\w.-]+?@\w+?\.\w+?\b', full_text)),
"list_of_authors": bool(re.search(r'Authors?:', full_text, re.IGNORECASE)),
"keywords_list": bool(re.search(r'Keywords?:', full_text, re.IGNORECASE)),
"word_count": len(full_text.split()) or "Missing"
}
def check_disclosures(full_text: str) -> Dict[str, bool]:
"""Check for disclosure statements."""
search_terms = [
"author contributions statement",
"conflict of interest statement",
"ethics statement",
"funding statement",
"data access statement"
]
return check_text_presence(full_text, search_terms)
def check_figures_and_tables(full_text: str) -> Dict[str, bool]:
"""Check for figures and tables."""
return {
"figures_with_citations": bool(re.search(r'Figure \d+.*?citation', full_text, re.IGNORECASE)),
"figures_legends": bool(re.search(r'Figure \d+.*?legend', full_text, re.IGNORECASE)),
"tables_legends": bool(re.search(r'Table \d+.*?legend', full_text, re.IGNORECASE))
}
def check_references(full_text: str) -> Dict[str, Any]:
"""Check for references."""
return {
"old_references": bool(re.search(r'\b19[0-9]{2}\b', full_text)),
"citations_in_abstract": bool(re.search(r'\b(citation|reference)\b', full_text[:1000], re.IGNORECASE)),
"reference_count": len(re.findall(r'\[.*?\]', full_text)),
"self_citations": bool(re.search(r'Self-citation', full_text, re.IGNORECASE))
}
def check_structure(full_text: str) -> Dict[str, bool]:
"""Check document structure."""
return {
"imrad_structure": all(section in full_text for section in ["Introduction", "Methods", "Results", "Discussion"]),
"abstract_structure": "structured abstract" in full_text.lower()
}
def check_language_issues(full_text: str) -> Dict[str, Any]:
"""Check for issues with capitalization, hyphenation, punctuation, spacing, etc."""
language_tool = language_tool_python.LanguageTool('en-US')
matches = language_tool.check(full_text)
word_count = len(full_text.split())
issues_count = len(matches)
issues_per_1000 = (issues_count / word_count) * 1000 if word_count else 0
serializable_matches = [
{
"message": match.message,
"replacements": match.replacements,
"offset": match.offset,
"errorLength": match.errorLength,
"category": match.category,
"ruleIssueType": match.ruleIssueType,
"sentence": match.sentence
}
for match in matches
]
return {
"issues_count": issues_count,
"issues_per_1000": issues_per_1000,
"failed": issues_per_1000 > 20,
"matches": serializable_matches
}
def check_language(full_text: str) -> Dict[str, Any]:
"""Check language quality."""
return {
"plain_language": bool(re.search(r'plain language summary', full_text, re.IGNORECASE)),
"readability_issues": False, # Placeholder for future implementation
"language_issues": check_language_issues(full_text)
}
def check_figure_order(full_text: str) -> Dict[str, Any]:
"""Check if figures are referred to in sequential order."""
figure_pattern = r'(?:Fig(?:ure)?\.?|Figure)\s*(\d+)'
figure_references = re.findall(figure_pattern, full_text, re.IGNORECASE)
figure_numbers = sorted(set(int(num) for num in figure_references))
is_sequential = all(a + 1 == b for a, b in zip(figure_numbers, figure_numbers[1:]))
if figure_numbers:
expected_figures = set(range(1, max(figure_numbers) + 1))
missing_figures = list(expected_figures - set(figure_numbers))
else:
missing_figures = None
duplicates = [num for num, count in Counter(figure_references).items() if count > 1]
duplicate_numbers = [int(num) for num in duplicates]
not_mentioned = list(set(figure_references) - set(duplicates))
return {
"sequential_order": is_sequential,
"figure_count": len(figure_numbers),
"missing_figures": missing_figures,
"figure_order": figure_numbers,
"duplicate_references": duplicates,
"not_mentioned": not_mentioned
}
def check_reference_order(full_text: str) -> Dict[str, Any]:
"""Check if references in the main body text are in order."""
reference_pattern = r'\[(\d+)\]'
references = re.findall(reference_pattern, full_text)
ref_numbers = [int(ref) for ref in references]
max_ref = 0
out_of_order = []
for i, ref in enumerate(ref_numbers):
if ref > max_ref + 1:
out_of_order.append((i+1, ref))
max_ref = max(max_ref, ref)
all_refs = set(range(1, max_ref + 1))
used_refs = set(ref_numbers)
missing_refs = list(all_refs - used_refs)
return {
"max_reference": max_ref,
"out_of_order": out_of_order,
"missing_references": missing_refs,
"is_ordered": len(out_of_order) == 0 and len(missing_refs) == 0
}
def check_reference_style(full_text: str) -> Dict[str, Any]:
"""Check the reference style used in the paper and identify inconsistencies."""
reference_section_match = re.search(r'References\b([\s\S]*?)(?:\n\S|\Z)', full_text, re.IGNORECASE)
if not reference_section_match:
return {"style": "Unknown", "reason": "References section not found", "inconsistent_refs": []}
references_text = reference_section_match.group(1)
reference_list = re.split(r'\n(?=\[\d+\]|\d+\.\s|\(\w+,\s*\d{4}\))', references_text)
references = [ref.strip() for ref in reference_list if ref.strip()]
styles = []
inconsistent_refs = []
patterns = {
"IEEE": r'^\[\d+\]',
"Harvard": r'^[A-Z][a-z]+,?\s[A-Z]\.\s\(?\d{4}\)?',
"APA": r'^[A-Z][a-z]+,?\s[A-Z]\.\s\(?\d{4}\)?',
"MLA": r'^[A-Z][a-z]+,\s[A-Z][a-z]+\.',
"Vancouver": r'^\d+\.\s',
"Chicago": r'^\d+\s[A-Z][a-z]+\s[A-Z]',
}
for i, ref in enumerate(references, 1):
matched = False
for style, pattern in patterns.items():
if re.match(pattern, ref):
styles.append(style)
matched = True
break
if not matched:
styles.append("Unknown")
inconsistent_refs.append((i, ref, "Unknown"))
if not styles:
return {"style": "Unknown", "reason": "No references found", "inconsistent_refs": []}
style_counts = Counter(styles)
majority_style, majority_count = style_counts.most_common(1)[0]
for i, style in enumerate(styles, 1):
if style != majority_style and style != "Unknown":
inconsistent_refs.append((i, references[i-1], style))
consistency = majority_count / len(styles)
return {
"majority_style": majority_style,
"inconsistent_refs": inconsistent_refs,
"consistency": consistency
}
# ------------------------------
# Annotation Functions
# ------------------------------
def highlight_text(page, words, text, annotation):
"""Highlight text and add annotation."""
text_instances = find_text_instances(words, text)
highlighted = False
for inst in text_instances:
highlight = page.add_highlight_annot(inst)
highlight.update()
comment = page.add_text_annot(inst[:2], annotation)
comment.update()
highlighted = True
return highlighted
def find_text_instances(words, text):
"""Find all instances of text in words."""
text_lower = text.lower()
text_words = text_lower.split()
instances = []
for i in range(len(words) - len(text_words) + 1):
if all(words[i+j][4].lower() == text_words[j] for j in range(len(text_words))):
inst = fitz.Rect(words[i][:4])
for j in range(1, len(text_words)):
inst = inst | fitz.Rect(words[i+j][:4])
instances.append(inst)
return instances
def highlight_issues_in_pdf(file, inconsistent_refs: List[Tuple[int, str, str]], language_matches: List[Dict[str, Any]]) -> bytes:
"""Highlight inconsistent references and add notes for language issues in a single PDF."""
try:
file.seek(0)
doc = fitz.open(stream=file.read(), filetype="pdf")
added_notes = set()
for page_number, page in enumerate(doc, start=1):
words = page.get_text("words")
if inconsistent_refs:
for ref_num, ref_text, ref_style in inconsistent_refs:
annotation_text = f"Reference {ref_num}: Inconsistent style ({ref_style}). Should be consolidated to {ref_style}."
highlight_text(page, words, ref_text, annotation_text)
if language_matches:
for match in language_matches:
issue_text = match['sentence']
error_message = f"{match['message']}\nSuggested correction: {match['replacements'][0] if match['replacements'] else 'No suggestion'}"
issue_key = (issue_text, error_message)
if issue_key not in added_notes:
if highlight_text(page, words, issue_text, error_message):
added_notes.add(issue_key)
annotated_pdf_bytes = doc.write()
doc.close()
return annotated_pdf_bytes
except Exception as e:
print(f"An error occurred while annotating the PDF: {str(e)}")
traceback.print_exc()
return b""
# ------------------------------
# Main Analysis Function
# ------------------------------
def analyze_pdf(file) -> Tuple[Dict[str, Any], bytes]:
"""
Analyze the uploaded PDF and return analysis results and annotated PDF bytes.
Returns:
Tuple containing:
- Analysis results as a dictionary.
- Annotated PDF as bytes.
"""
try:
# The 'file' is a BytesIO object provided by Streamlit
file.seek(0)
pages_text = extract_pdf_text_by_page(file)
full_text = extract_pdf_text(file)
full_text = label_authors(full_text)
# Perform analyses
metadata = check_metadata(full_text)
disclosures = check_disclosures(full_text)
figures_and_tables = check_figures_and_tables(full_text)
figure_order = check_figure_order(full_text)
references = check_references(full_text)
reference_order = check_reference_order(full_text)
reference_style = check_reference_style(full_text)
structure = check_structure(full_text)
language = check_language(full_text)
# Compile results
results = {
"metadata": metadata,
"disclosures": disclosures,
"figures_and_tables": figures_and_tables,
"figure_order": figure_order,
"references": references,
"reference_order": reference_order,
"reference_style": reference_style,
"structure": structure,
"language": language
}
# Handle annotations
inconsistent_refs = reference_style.get("inconsistent_refs", [])
language_matches = language.get("language_issues", {}).get("matches", [])
if inconsistent_refs or language_matches:
annotated_pdf_bytes = highlight_issues_in_pdf(file, inconsistent_refs, language_matches)
else:
annotated_pdf_bytes = None
return results, annotated_pdf_bytes
except Exception as e:
error_message = {
"error": str(e),
"traceback": traceback.format_exc()
}
return error_message, None
# ------------------------------
# Streamlit Interface
# ------------------------------
def main():
st.title("PDF Analyzer")
st.write("Upload a PDF document to analyze its structure, references, language, and more.")
uploaded_file = st.file_uploader("Upload PDF", type=["pdf"])
if uploaded_file is not None:
with st.spinner("Analyzing PDF..."):
results, annotated_pdf = analyze_pdf(uploaded_file)
st.subheader("Analysis Results")
st.json(results)
if annotated_pdf:
st.subheader("Download Annotated PDF")
st.download_button(
label="Download Annotated PDF",
data=annotated_pdf,
file_name="annotated.pdf",
mime="application/pdf"
)
else:
st.success("No issues found. No annotated PDF to download.")
if __name__ == "__main__":
main()