cv-to-csv-extractor / cv_extraction_app.py
Zwounds's picture
Upload 6 files
d7bf02a verified
raw
history blame
31.9 kB
import gradio as gr
import os
import tempfile
import pandas as pd
import logging
import time
from PyPDF2 import PdfReader
import google.generativeai as genai
import json
import re
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- Configuration ---
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
MODEL_NAME = 'gemini-2.5-flash-preview-04-17' # Using the original model as specified
APP_PASSWORD = os.environ.get('APP_PASSWORD') # Password for app authentication
# Main categories (first tier)
MAIN_CATEGORIES = [
"Books & Book Contributions",
"Journal & Article Publications",
"Conference & Presentations",
"Creative & Artistic Works",
"Legal & Technical Documents",
"Funding, Grants & Awards", # New Main Category
"Other Scholarly Contributions"
]
# Specific types (second tier) - these will be the actual categories in the CSV
SCHOLARLY_WORK_TYPES = [
# Books & Book Contributions
"Book, Authored",
"Book, Chapter",
"Book, Edited",
"Book, introduction, preface, etc.",
# Journal & Article Publications
"Journal Article, peer-reviewed",
"Journal Article, other",
"Newspaper/Magazine Article",
"Review/Commentary (including Blogging)",
# Conference & Presentations
"Conference Presentation - published as proceedings",
"Conference Presentation, other",
"Lecture (Invited)",
# Creative & Artistic Works
"Digital Project",
"Curated an Art Show",
"Direction/Choreography/Dramaturgy/Design",
"Exhibited at Curated Art Show",
"Music Composition Published/Performed",
"Performance (music, dance, theater)",
"Play or Screenplay Produced/Performed",
"Poem or Short Story Published",
# Legal & Technical Documents
"Legal Brief (Submitted)",
"Legal Review",
"Technical/Policy Reports, peer-reviewed",
"Technical/Policy Reports, other",
# Funding, Grants & Awards
"Grant (External)",
"Grant (Internal)",
"Fellowship",
"Award/Honor",
# Other Scholarly Contributions
"Patent",
"Other"
]
# --- Helper Functions ---
def clean_text(text):
"""Cleans text by replacing common ligatures and smart quotes."""
replacements = {
"ff": "ff", "fi": "fi", "fl": "fl", "ffi": "ffi", "ffl": "ffl",
""": "\"", """: "\"", "'": "'", "'": "'",
}
for old, new in replacements.items():
text = text.replace(old, new)
return text
def clean_cv_specific_text(text):
"""Apply CV-specific cleaning rules to improve text quality."""
# Remove page numbers (common in CVs) - improved regex
text = re.sub(r'\n\s*\d+\s*(\n|$)', '\n', text)
text = re.sub(r'^\s*\d+\s*\n', '', text) # Page number at the very beginning
# Fix common CV formatting issues like names split across lines
text = re.sub(r'([a-zA-Z])\s*\n\s*([a-zA-Z])', r'\1 \2', text) # General case for text split over newlines
text = re.sub(r'([A-Z][a-z]+(?:-[A-Z][a-z]+)?)\s*\n\s*([A-Z][a-z]+)', r'\1 \2', text) # More specific for names
# Normalize citation formats - e.g., year punctuation
text = re.sub(r'(\d{4})\s*\.\s*', r'\1. ', text)
# Remove excessive newlines
text = re.sub(r'\n\s*\n', '\n', text)
return text
def extract_text_from_pdf(pdf_file):
"""Extracts text from a given PDF file."""
logging.info(f"Extracting text from: {pdf_file.name}")
try:
reader = PdfReader(pdf_file.name)
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
cleaned_text = clean_text(text)
cleaned_text = clean_cv_specific_text(cleaned_text) # Apply CV specific cleaning
logging.info(f"Successfully extracted and cleaned text from {pdf_file.name} (Length: {len(cleaned_text)})")
return cleaned_text
except Exception as e:
logging.error(f"Error reading PDF {pdf_file.name}: {e}")
return None
def extract_pdf_metadata(pdf_file):
"""Extract metadata from PDF that might help with faculty identification."""
try:
reader = PdfReader(pdf_file.name)
metadata = reader.metadata
author = metadata.get('/Author', '')
title = metadata.get('/Title', '')
# PyPDF2 might return Author as a list
if isinstance(author, list):
author = ", ".join(author) if author else ''
if isinstance(title, list):
title = ", ".join(title) if title else ''
return {
'author': str(author) if author else '',
'title': str(title) if title else '',
'filename': os.path.basename(pdf_file.name)
}
except Exception as e:
logging.error(f"Error extracting metadata from {pdf_file.name}: {e}")
return {'filename': os.path.basename(pdf_file.name), 'author': '', 'title': ''}
def get_faculty_name_from_llm(cv_text_chunk):
"""Sends a small chunk of CV text to LLM to extract only the faculty name."""
if not cv_text_chunk:
return "Unknown", None
prompt = f"""
Analyze the following CV text chunk. Identify the primary faculty member's name, usually found prominently at the beginning of the document.
Return the result as a single JSON object with a top-level key "faculty_name" and the extracted faculty name as a string.
If the name cannot be reliably determined, use "Unknown".
Example: {{ "faculty_name": "Dr. Jane Doe" }}
CV Text Chunk:
---
{cv_text_chunk}
---
JSON Output:
"""
try:
model = genai.GenerativeModel(MODEL_NAME)
response = model.generate_content(
prompt,
generation_config=genai.types.GenerationConfig(response_mime_type="application/json")
)
parsed_json = json.loads(response.text)
faculty_name = parsed_json.get("faculty_name", "Unknown")
if not isinstance(faculty_name, str) or not faculty_name.strip():
faculty_name = "Unknown"
return faculty_name, None # No accomplishments from this call
except Exception as e:
logging.error(f"Error extracting faculty name with LLM: {e}")
return "Unknown", None
def get_accomplishments_from_llm(cv_text, faculty_name_hint=None):
"""Sends CV text to Google Gemini API and returns faculty name and structured accomplishments."""
if not cv_text:
return faculty_name_hint or "Unknown", []
prompt = f"""
Analyze the following CV text. First, identify the primary faculty member's name, usually found prominently at the beginning of the document or in the header/footer.
Extract the name directly from the CV content. Look for patterns like "Curriculum Vitae of [Name]", "[Name], Ph.D.", or other indicators of the primary faculty member.
IMPORTANT: Return the faculty name in proper case (e.g., "John Smith" or "Jane Doe-Smith"), NOT in all caps, even if it appears in all caps in the document.
Second, extract scholarly accomplishments based on the categories below. Follow the decision tree approach to categorize each accomplishment accurately.
BE COMPREHENSIVE: Strive to extract ALL identifiable scholarly accomplishments from the CV text that fit the defined categories. Pay attention to all sections of the CV. If an item is ambiguous but potentially relevant, lean towards including it for later review.
# DECISION TREE FOR CATEGORIZATION:
Step 1: Determine the general type of scholarly work:
- Is it a book or book contribution? → Go to Books & Book Contributions
- Is it a journal article or similar publication? → Go to Journal & Article Publications
- Is it a conference presentation or lecture? → Go to Conference & Presentations
- Is it a creative or artistic work? → Go to Creative & Artistic Works
- Is it a legal document or technical report? → Go to Legal & Technical Documents
- Is it something else scholarly? → Go to Other Scholarly Contributions
Step 2: Within each general type, determine the specific category:
## Books & Book Contributions
- "Book, Authored": A complete book written by the faculty member as primary author
Example: "Smith, J. (2020). The Evolution of Digital Learning. Routledge."
- "Book, Chapter": A chapter contributed to a book edited by someone else
Example: "Smith, J. (2020). Digital pedagogy frameworks. In A. Johnson (Ed.), Handbook of Educational Technology (pp. 45-67). Routledge."
- "Book, Edited": A book where the faculty member served as editor rather than author
Example: "Smith, J. (Ed.). (2020). Perspectives on Digital Learning. Routledge."
- "Book, introduction, preface, etc.": Shorter contributions to books like forewords, introductions
Example: "Smith, J. (2020). Foreword. In A. Johnson, Digital Learning Environments (pp. ix-xi). Routledge."
## Journal & Article Publications
- "Journal Article, peer-reviewed": Articles published in peer-reviewed academic journals
Example: "Smith, J. (2020). Digital literacy in higher education. Journal of Educational Technology, 45(2), 123-145. https://doi.org/10.xxxx/yyyy"
Look for: journal name, volume/issue numbers, DOI, mentions of peer review
- "Journal Article, other": Articles in non-peer-reviewed journals
Example: "Smith, J. (2020). Teaching in digital environments. Educational Practice, 15, 78-92."
- "Newspaper/Magazine Article": Articles in popular press or magazines
Example: "Smith, J. (2020, March 15). How technology is changing education. The Education Times, pp. 23-24."
- "Review/Commentary (including Blogging)": Book reviews, commentaries, blog posts
Example: "Smith, J. (2020). [Review of the book Digital Pedagogy, by A. Johnson]. Educational Review, 12(3), 45-47."
## Conference & Presentations
- "Conference Presentation - published as proceedings": Presentations published in conference proceedings
Example: "Smith, J. (2020). Virtual reality in education. Proceedings of the International Conference on Educational Technology, 234-241. IEEE."
Look for: "Proceedings of", publisher information, page numbers
- "Conference Presentation, other": Presentations at conferences without formal publication
Example: "Smith, J. (2020, June). Virtual reality applications. Paper presented at the Educational Technology Conference, Boston, MA."
- "Lecture (Invited)": Talks given by invitation rather than through submission process
Example: "Smith, J. (2020, April). The future of digital learning. Invited lecture at Harvard University, Cambridge, MA."
Look for: "invited", "keynote", "guest lecture"
## Creative & Artistic Works
- "Digital Project": Digital scholarship, websites, tools, or resources created
Example: "Smith, J. (2018-2020). Digital Learning Archive [Web application]. https://digitallearningarchive.org"
- "Curated an Art Show": Organization and curation of artistic exhibitions
Example: "Smith, J. (Curator). (2020). Digital Art in Education [Exhibition]. University Gallery, Boston, MA."
- "Direction/Choreography/Dramaturgy/Design": Creative direction of performances
Example: "Smith, J. (Director). (2020). The Digital Divide [Theater production]. University Theater, Boston, MA."
- "Exhibited at Curated Art Show": Participation as an artist in exhibitions
Example: "Smith, J. (2020). Learning Through Screens [Digital art]. In Digital Expressions, University Gallery, Boston, MA."
- "Music Composition Published/Performed": Musical works composed
Example: "Smith, J. (Composer). (2020). Digital Sonata [Musical composition]. Performed by Boston Symphony, Symphony Hall, Boston, MA."
- "Performance (music, dance, theater)": Performance as an artist
Example: "Smith, J. (Performer). (2020). The Digital Age [Dance performance]. Kennedy Center, Washington, DC."
- "Play or Screenplay Produced/Performed": Written dramatic works
Example: "Smith, J. (Playwright). (2020). Virtual Connections [Play]. Produced at University Theater, Boston, MA."
- "Poem or Short Story Published": Creative writing published
Example: "Smith, J. (2020). Digital dreams [Poem]. Literary Journal, 23(2), 45-46."
## Legal & Technical Documents
- "Legal Brief (Submitted)": Legal documents submitted to courts
Example: "Smith, J. (2020). Amicus brief in Digital Rights Foundation v. State Board of Education. Supreme Court of Massachusetts."
- "Legal Review": Analysis of legal cases or issues
Example: "Smith, J. (2020). Digital privacy in educational settings: A legal analysis. Harvard Law Review, 133(4), 1023-1056."
- "Technical/Policy Reports, peer-reviewed": Technical reports that underwent peer review
Example: "Smith, J. (2020). Digital learning standards (Technical Report No. 2020-05). Educational Technology Consortium. [Peer-reviewed]"
- "Technical/Policy Reports, other": Technical reports without peer review
Example: "Smith, J. (2020). Implementing digital tools in K-12 (White Paper). Center for Digital Education."
## Funding, Grants & Awards
- "Grant (External)": Research grants received from external funding agencies (e.g., NSF, NIH, foundations).
Example: "Smith, J. (PI). (2021-2024). Project Title. National Science Foundation (#1234567). $500,000."
For this category, extract the numeric funding amount into the "funding_amount" field (e.g., 500000).
- "Grant (Internal)": Research grants or seed funding received from internal university sources.
Example: "Smith, J. (PI). (2020). Pilot study on X. University Research Grant. $10,000."
For this category, extract the numeric funding amount into the "funding_amount" field (e.g., 10000).
- "Fellowship": Competitive fellowships awarded for research or scholarly work. May or may not have an explicit monetary value listed.
Example: "Smith, J. (2019-2020). Doctoral Dissertation Fellowship. Mellon Foundation. $30,000 stipend."
If a monetary value is stated, extract it into "funding_amount". Otherwise, use "N/A".
- "Award/Honor": Awards, honors, or distinctions received for scholarly work or contributions. Typically no funding amount.
Example: "Smith, J. (2022). Best Paper Award, International Conference on Educational Technology."
"funding_amount" should usually be "N/A" for this category unless explicitly stated as a monetary prize.
## Other Scholarly Contributions
- "Patent": Registered intellectual property
Example: "Smith, J. (2020). Digital learning assessment system (U.S. Patent No. 10,123,456). U.S. Patent and Trademark Office."
- "Other": Scholarly contributions that don't fit other categories, such as datasets, software, or professional service.
Example: "Smith, J. (2020). Dataset: Survey of digital learning practices [Data set]. Harvard Dataverse. https://doi.org/10.xxxx/yyyy"
Return the result as a single JSON object containing:
1. A top-level key "faculty_name" with the extracted faculty name as a string. If the name cannot be reliably determined from this text and no hint was provided, use "Unknown". If a hint was provided, prefer the hint if no clear name is in the text.
2. A top-level key "accomplishments" containing a list of JSON objects, where each object represents one accomplishment with the following details:
- "category": The specific type of scholarly work from the list above (e.g., "Book, Authored", "Journal Article, peer-reviewed", etc.)
- "main_category": The general category this work falls under (e.g., "Books & Book Contributions", "Journal & Article Publications", etc.)
- "year": The year the accomplishment occurred (as an integer or string). If multiple years or a range, use the start year or the most prominent year. If no year is found, use "N/A".
- "description": The full description or citation of the accomplishment.
- "doi_url": The DOI or URL associated with the accomplishment, if present. Use "N/A" if not found.
- "funding_amount": For grants or funded projects (often in "Other" category), the numeric funding amount if explicitly stated (e.g., 250000). Extract only the number, without currency symbols or commas. Use "N/A" if not applicable or not found.
- "confidence": A number from 1-5 indicating your confidence in this categorization (5 being highest confidence).
Ensure the entire output is a single, valid JSON object like this example:
{{
"faculty_name": "Example Faculty Name",
"accomplishments": [
{{ "category": "Journal Article, peer-reviewed", "main_category": "Journal & Article Publications", "year": "2023", "description": "...", "doi_url": "...", "funding_amount": "N/A", "confidence": 5 }},
{{ "category": "Book, Chapter", "main_category": "Books & Book Contributions", "year": "2022", "description": "...", "doi_url": "N/A", "funding_amount": "N/A", "confidence": 4 }}
]
}}
Do not include any text before or after the JSON object.
CV Text:
---
{cv_text[:45000]}
---
JSON Output:
"""
# Max input tokens for flash is ~128k, but output also counts.
# CV text can be very long. We'll truncate here, but a more robust solution might involve chunking.
logging.info(f"Sending request to Gemini API for faculty: {faculty_name_hint or 'Unknown'}")
try:
model = genai.GenerativeModel(MODEL_NAME)
response = model.generate_content(
prompt,
generation_config=genai.types.GenerationConfig(
response_mime_type="application/json",
temperature=0.2 # Lower temperature for more consistent JSON formatting
)
)
response_text = response.text.strip()
# Try to fix common JSON formatting issues before parsing
try:
parsed_json = json.loads(response_text)
except json.JSONDecodeError as e:
logging.warning(f"Initial JSON parsing failed: {e}. Attempting to fix common issues.")
# Try to extract JSON from markdown code blocks if present
if response_text.startswith("```json") and "```" in response_text:
code_block_content = response_text.split("```")[1]
if code_block_content.startswith("json"):
code_block_content = code_block_content[4:].strip()
try:
parsed_json = json.loads(code_block_content)
logging.info("Successfully extracted JSON from code block")
except json.JSONDecodeError:
raise # Re-raise if this also fails
else:
raise # Re-raise the original error if not in a code block
extracted_faculty_name = faculty_name_hint or "Unknown"
llm_faculty_name = parsed_json.get("faculty_name", "Unknown")
if not isinstance(llm_faculty_name, str) or not llm_faculty_name.strip():
llm_faculty_name = "Unknown"
if faculty_name_hint and faculty_name_hint != "Unknown":
extracted_faculty_name = faculty_name_hint
elif llm_faculty_name != "Unknown":
extracted_faculty_name = llm_faculty_name
accomplishments_list = []
if "accomplishments" in parsed_json and isinstance(parsed_json["accomplishments"], list):
accomplishments_list = parsed_json["accomplishments"]
logging.info(f"Successfully parsed faculty name '{extracted_faculty_name}' and {len(accomplishments_list)} accomplishments.")
else:
logging.warning("LLM response JSON does not contain a valid 'accomplishments' list.")
return extracted_faculty_name, accomplishments_list
except Exception as e:
logging.error(f"Error in LLM processing: {e}")
return faculty_name_hint or "Unknown", []
def get_accomplishments_with_retry(cv_text, faculty_name_hint=None, max_retries=2, initial_backoff=3):
"""Wrapper function that adds retry logic to the LLM API call."""
retries = 0
backoff_time = initial_backoff
while retries <= max_retries:
try:
# Call the original function that might raise exceptions
return get_accomplishments_from_llm(cv_text, faculty_name_hint)
except json.JSONDecodeError as e:
retries += 1
logging.error(f"JSONDecodeError on attempt {retries}/{max_retries+1}: {e}. Response might not be valid JSON.")
if retries > max_retries:
logging.error(f"Failed after {max_retries+1} attempts due to JSONDecodeError.")
return faculty_name_hint or "Unknown", []
# No retry for JSONDecodeError usually, as it implies a persistent issue with response format
# However, for robustness, we can allow one retry if it's not the last attempt.
if retries <=1: # Only retry JSON decode once
logging.info(f"Retrying JSON decode in {backoff_time}s...")
time.sleep(backoff_time)
backoff_time *= 2
else:
return faculty_name_hint or "Unknown", [] # Give up on JSON decode errors after 1 retry
except Exception as e: # Catches other API errors, network issues, etc.
retries += 1
logging.warning(f"API Error on attempt {retries}/{max_retries+1} for faculty '{faculty_name_hint or 'Unknown'}': {e}")
if "content filter" in str(e).lower():
logging.error(f"Content filter triggered for faculty '{faculty_name_hint or 'Unknown'}'. No further retries for this error.")
return faculty_name_hint or "Unknown", [] # Don't retry content filter errors
if retries > max_retries:
logging.error(f"Failed after {max_retries+1} attempts for faculty '{faculty_name_hint or 'Unknown'}'.")
return faculty_name_hint or "Unknown", []
logging.info(f"Retrying in {backoff_time}s for faculty '{faculty_name_hint or 'Unknown'}'...")
time.sleep(backoff_time)
backoff_time *= 2 # Exponential backoff
return faculty_name_hint or "Unknown", [] # Should be unreachable if logic is correct
def validate_and_clean_accomplishment(item, faculty_name_cv, filename):
"""Validates and cleans a single accomplishment item."""
category = item.get("category", "Other")
main_category_map = {
"Book, Authored": "Books & Book Contributions",
"Book, Chapter": "Books & Book Contributions",
"Book, Edited": "Books & Book Contributions",
"Book, introduction, preface, etc.": "Books & Book Contributions",
"Journal Article, peer-reviewed": "Journal & Article Publications",
"Journal Article, other": "Journal & Article Publications",
"Newspaper/Magazine Article": "Journal & Article Publications",
"Review/Commentary (including Blogging)": "Journal & Article Publications",
"Conference Presentation - published as proceedings": "Conference & Presentations",
"Conference Presentation, other": "Conference & Presentations",
"Lecture (Invited)": "Conference & Presentations",
"Digital Project": "Creative & Artistic Works",
"Curated an Art Show": "Creative & Artistic Works",
"Direction/Choreography/Dramaturgy/Design": "Creative & Artistic Works",
"Exhibited at Curated Art Show": "Creative & Artistic Works",
"Music Composition Published/Performed": "Creative & Artistic Works",
"Performance (music, dance, theater)": "Creative & Artistic Works",
"Play or Screenplay Produced/Performed": "Creative & Artistic Works",
"Poem or Short Story Published": "Creative & Artistic Works",
"Legal Brief (Submitted)": "Legal & Technical Documents",
"Legal Review": "Legal & Technical Documents",
"Technical/Policy Reports, peer-reviewed": "Legal & Technical Documents",
"Technical/Policy Reports, other": "Legal & Technical Documents",
"Grant (External)": "Funding, Grants & Awards",
"Grant (Internal)": "Funding, Grants & Awards",
"Fellowship": "Funding, Grants & Awards",
"Award/Honor": "Funding, Grants & Awards",
"Patent": "Other Scholarly Contributions",
"Other": "Other Scholarly Contributions"
}
main_category = item.get("main_category")
# If main_category is not provided by LLM or is unexpected, try to map it
if not main_category or main_category not in MAIN_CATEGORIES:
main_category = main_category_map.get(category, "Other Scholarly Contributions")
year = str(item.get("year", "N/A")) # Ensure year is string
description = item.get("description", "").strip()
doi_url = item.get("doi_url", "N/A")
funding_amount = item.get("funding_amount", "N/A")
confidence = item.get("confidence", 3) # Default to medium confidence
try:
confidence = int(confidence)
except (ValueError, TypeError):
confidence = 3 # Default if conversion fails
needs_review = confidence < 3
# Basic validation: if description is empty, skip
if not description:
return None
return {
"Faculty_Name": faculty_name_cv,
"CV_Filename": os.path.basename(filename),
"Main_Category": main_category,
"Category": category,
"Year": year,
"Description": description,
"DOI_URL": doi_url,
"Funding_Amount": funding_amount,
"Confidence": confidence,
"Needs_Review": "Yes" if needs_review else "No"
}
# --- Gradio App Functions ---
def check_password(password):
"""Check if the provided password matches the app password."""
if not APP_PASSWORD:
# If no password is set, allow access (for development)
return True
return password == APP_PASSWORD
def process_cv_files(pdf_files, progress=gr.Progress()):
"""Process uploaded CV files and extract accomplishments."""
if not pdf_files:
raise gr.Error("Please upload at least one PDF file.")
if not GOOGLE_API_KEY:
raise gr.Error("Google API key is not configured. Please set the GOOGLE_API_KEY environment variable.")
genai.configure(api_key=GOOGLE_API_KEY)
all_accomplishments = []
total_steps = len(pdf_files) * 4 # 4 steps per file: extract text, get metadata, extract accomplishments, process results
current_step = 0
# Process each PDF file
for i, pdf_file in enumerate(pdf_files):
file_name = os.path.basename(pdf_file.name)
progress(current_step/total_steps, f"Processing file {i+1}/{len(pdf_files)}: {file_name}")
current_step += 1
# Extract text from PDF
progress(current_step/total_steps, f"Extracting text from {file_name}")
cv_text = extract_text_from_pdf(pdf_file)
if not cv_text:
gr.Warning(f"Could not extract text from {file_name}. Skipping.")
current_step += 3 # Skip remaining steps for this file
continue
current_step += 1
# Get PDF metadata
progress(current_step/total_steps, f"Processing metadata for {file_name}")
pdf_metadata = extract_pdf_metadata(pdf_file)
current_step += 1
# Extract faculty name and accomplishments
progress(current_step/total_steps, f"Extracting accomplishments from {file_name}")
faculty_name_cv, accomplishments_list = get_accomplishments_with_retry(cv_text)
current_step += 1
# Fallback logic if LLM returns "Unknown"
if faculty_name_cv == "Unknown":
metadata_author = pdf_metadata.get('author', '').strip()
if metadata_author:
faculty_name_cv = metadata_author
logging.info(f"Used PDF metadata author '{faculty_name_cv}' for {pdf_file.name}")
if faculty_name_cv == "Unknown": # If still unknown, try filename
name_from_file = os.path.splitext(os.path.basename(pdf_file.name))[0].replace("_", " ").replace("-", " ")
# Basic heuristic to see if it looks like a name
if len(name_from_file.split()) > 1 and len(name_from_file.split()) < 4:
faculty_name_cv = name_from_file.title()
# Process accomplishments
if accomplishments_list:
for item in accomplishments_list:
processed_item = validate_and_clean_accomplishment(item, faculty_name_cv, pdf_file.name)
if processed_item:
all_accomplishments.append(processed_item)
else:
gr.Warning(f"No accomplishments found for {os.path.basename(pdf_file.name)}.")
if not all_accomplishments:
raise gr.Error("No accomplishments were extracted from the provided PDFs.")
# Convert to DataFrame for display
df = pd.DataFrame(all_accomplishments)
# Create CSV in memory for download
csv_file = tempfile.NamedTemporaryFile(delete=False, suffix='.csv')
df.to_csv(csv_file.name, index=False)
return df, csv_file.name
# --- Gradio Interface ---
# Create the authentication interface
with gr.Blocks(title="CV to CSV Extraction App") as app:
gr.Markdown("# CV to CSV Extraction App")
gr.Markdown("Extract publications and accomplishments from faculty CVs")
# Authentication state
authenticated = gr.State(False)
# Login interface
with gr.Group(visible=True) as login_group:
gr.Markdown("### Authentication Required")
password_input = gr.Textbox(type="password", label="Password")
login_button = gr.Button("Login")
login_error = gr.Markdown(visible=False)
# Main app interface (initially hidden)
with gr.Group(visible=False) as main_app:
with gr.Tab("Extract from CVs"):
gr.Markdown("### Upload Faculty CV PDFs")
gr.Markdown("Upload one or more PDF files containing faculty CVs. The app will extract publications and other scholarly accomplishments.")
# File upload
pdf_input = gr.File(file_count="multiple", label="Upload CV PDFs", file_types=[".pdf"])
process_button = gr.Button("Extract Accomplishments")
# Results display
results = gr.DataFrame(label="Extracted Accomplishments", interactive=False)
# Download button
csv_output = gr.File(label="Download as CSV")
# Process button click
process_button.click(
fn=process_cv_files,
inputs=[pdf_input],
outputs=[results, csv_output],
api_name="extract_accomplishments"
)
# Login button click
def login(password):
if check_password(password):
return {
login_group: gr.update(visible=False),
main_app: gr.update(visible=True),
login_error: gr.update(visible=False),
authenticated: True
}
else:
return {
login_error: gr.update(visible=True, value="Invalid password. Please try again."),
authenticated: False
}
login_button.click(
fn=login,
inputs=[password_input],
outputs=[login_group, main_app, login_error, authenticated]
)
# Launch the app
if __name__ == "__main__":
app.launch()