Commit ·
ca2ba95
1
Parent(s): 36f4fff
feat: add Microsoft Word (.docx/.doc) document parsing alongside PDF and TXT
Browse files- main.py +4 -4
- requirements.txt +1 -0
- resume_scanner/extractor.py +34 -5
- static/index.html +7 -7
main.py
CHANGED
|
@@ -96,10 +96,10 @@ async def scan_resume(
|
|
| 96 |
"""
|
| 97 |
# Save resume file safely
|
| 98 |
resume_ext = Path(resume_file.filename or "").suffix.lower()
|
| 99 |
-
if resume_ext not in (".pdf", ".txt"):
|
| 100 |
raise HTTPException(
|
| 101 |
status_code=400,
|
| 102 |
-
detail=f"Unsupported resume file extension '{resume_ext}'. Only .pdf and .
|
| 103 |
)
|
| 104 |
|
| 105 |
resume_path = UPLOAD_DIR / f"resume_{resume_file.filename}"
|
|
@@ -110,10 +110,10 @@ async def scan_resume(
|
|
| 110 |
# Determine JD text
|
| 111 |
if jd_file and jd_file.filename:
|
| 112 |
jd_ext = Path(jd_file.filename).suffix.lower()
|
| 113 |
-
if jd_ext not in (".pdf", ".txt"):
|
| 114 |
raise HTTPException(
|
| 115 |
status_code=400,
|
| 116 |
-
detail=f"Unsupported job description file extension '{jd_ext}'. Only .pdf and .
|
| 117 |
)
|
| 118 |
jd_path = UPLOAD_DIR / f"jd_{jd_file.filename}"
|
| 119 |
with open(jd_path, "wb") as buffer:
|
|
|
|
| 96 |
"""
|
| 97 |
# Save resume file safely
|
| 98 |
resume_ext = Path(resume_file.filename or "").suffix.lower()
|
| 99 |
+
if resume_ext not in (".pdf", ".txt", ".docx", ".doc"):
|
| 100 |
raise HTTPException(
|
| 101 |
status_code=400,
|
| 102 |
+
detail=f"Unsupported resume file extension '{resume_ext}'. Only .pdf, .txt, .docx, and .doc allowed.",
|
| 103 |
)
|
| 104 |
|
| 105 |
resume_path = UPLOAD_DIR / f"resume_{resume_file.filename}"
|
|
|
|
| 110 |
# Determine JD text
|
| 111 |
if jd_file and jd_file.filename:
|
| 112 |
jd_ext = Path(jd_file.filename).suffix.lower()
|
| 113 |
+
if jd_ext not in (".pdf", ".txt", ".docx", ".doc"):
|
| 114 |
raise HTTPException(
|
| 115 |
status_code=400,
|
| 116 |
+
detail=f"Unsupported job description file extension '{jd_ext}'. Only .pdf, .txt, .docx, and .doc allowed.",
|
| 117 |
)
|
| 118 |
jd_path = UPLOAD_DIR / f"jd_{jd_file.filename}"
|
| 119 |
with open(jd_path, "wb") as buffer:
|
requirements.txt
CHANGED
|
@@ -7,3 +7,4 @@ google-generativeai>=0.8.0
|
|
| 7 |
pypdf>=4.0.0
|
| 8 |
tenacity>=8.2.3
|
| 9 |
python-dotenv>=1.0.0
|
|
|
|
|
|
| 7 |
pypdf>=4.0.0
|
| 8 |
tenacity>=8.2.3
|
| 9 |
python-dotenv>=1.0.0
|
| 10 |
+
python-docx>=1.1.0
|
resume_scanner/extractor.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
Local text extraction and document safety checks for AI Resume Scanner.
|
| 3 |
-
Ensures files exist, valid extension (.pdf / .txt), rejects empty/overly large files,
|
| 4 |
and extracts clean text locally before sending to Gemini API.
|
| 5 |
"""
|
| 6 |
|
|
@@ -18,7 +18,7 @@ class ExtractionError(Exception):
|
|
| 18 |
|
| 19 |
def validate_file(file_path: str, label: str = "File") -> Path:
|
| 20 |
"""
|
| 21 |
-
Validates existence, extension (.pdf/.txt), and size of a file path.
|
| 22 |
"""
|
| 23 |
path = Path(file_path)
|
| 24 |
|
|
@@ -29,9 +29,9 @@ def validate_file(file_path: str, label: str = "File") -> Path:
|
|
| 29 |
raise ExtractionError(f"{label} is not a valid file: '{file_path}'")
|
| 30 |
|
| 31 |
ext = path.suffix.lower()
|
| 32 |
-
if ext not in (".pdf", ".txt"):
|
| 33 |
raise ExtractionError(
|
| 34 |
-
f"{label} has unsupported extension '{ext}'. Only .pdf and .
|
| 35 |
)
|
| 36 |
|
| 37 |
file_size = path.stat().st_size
|
|
@@ -68,15 +68,44 @@ def extract_text_from_pdf(path: Path) -> str:
|
|
| 68 |
raise ExtractionError(f"Failed to parse PDF '{path.name}': {exc}") from exc
|
| 69 |
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
def extract_text_from_file(file_path: str, label: str = "Document") -> str:
|
| 72 |
"""
|
| 73 |
-
Validates and extracts raw text from
|
| 74 |
"""
|
| 75 |
path = validate_file(file_path, label=label)
|
| 76 |
ext = path.suffix.lower()
|
| 77 |
|
| 78 |
if ext == ".pdf":
|
| 79 |
return extract_text_from_pdf(path)
|
|
|
|
|
|
|
| 80 |
else:
|
| 81 |
try:
|
| 82 |
content = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
|
|
| 1 |
"""
|
| 2 |
Local text extraction and document safety checks for AI Resume Scanner.
|
| 3 |
+
Ensures files exist, valid extension (.pdf / .txt / .docx / .doc), rejects empty/overly large files,
|
| 4 |
and extracts clean text locally before sending to Gemini API.
|
| 5 |
"""
|
| 6 |
|
|
|
|
| 18 |
|
| 19 |
def validate_file(file_path: str, label: str = "File") -> Path:
|
| 20 |
"""
|
| 21 |
+
Validates existence, extension (.pdf/.txt/.docx/.doc), and size of a file path.
|
| 22 |
"""
|
| 23 |
path = Path(file_path)
|
| 24 |
|
|
|
|
| 29 |
raise ExtractionError(f"{label} is not a valid file: '{file_path}'")
|
| 30 |
|
| 31 |
ext = path.suffix.lower()
|
| 32 |
+
if ext not in (".pdf", ".txt", ".docx", ".doc"):
|
| 33 |
raise ExtractionError(
|
| 34 |
+
f"{label} has unsupported extension '{ext}'. Only .pdf, .txt, .docx, and .doc are allowed."
|
| 35 |
)
|
| 36 |
|
| 37 |
file_size = path.stat().st_size
|
|
|
|
| 68 |
raise ExtractionError(f"Failed to parse PDF '{path.name}': {exc}") from exc
|
| 69 |
|
| 70 |
|
| 71 |
+
def extract_text_from_docx(path: Path) -> str:
|
| 72 |
+
"""Extracts text content from a Microsoft Word (.docx/.doc) file using python-docx."""
|
| 73 |
+
try:
|
| 74 |
+
import docx
|
| 75 |
+
except ImportError:
|
| 76 |
+
raise ExtractionError("python-docx is not installed. Run `pip install python-docx`.")
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
doc = docx.Document(str(path))
|
| 80 |
+
paragraphs = []
|
| 81 |
+
for p in doc.paragraphs:
|
| 82 |
+
text = p.text.strip()
|
| 83 |
+
if text:
|
| 84 |
+
paragraphs.append(text)
|
| 85 |
+
for table in doc.tables:
|
| 86 |
+
for row in table.rows:
|
| 87 |
+
row_text = " | ".join(cell.text.strip() for cell in row.cells if cell.text.strip())
|
| 88 |
+
if row_text:
|
| 89 |
+
paragraphs.append(row_text)
|
| 90 |
+
extracted = "\n\n".join(paragraphs).strip()
|
| 91 |
+
if not extracted:
|
| 92 |
+
raise ExtractionError(f"No readable text could be extracted from Word document: '{path.name}'")
|
| 93 |
+
return extracted
|
| 94 |
+
except Exception as exc:
|
| 95 |
+
raise ExtractionError(f"Failed to parse Word document '{path.name}': {exc}") from exc
|
| 96 |
+
|
| 97 |
+
|
| 98 |
def extract_text_from_file(file_path: str, label: str = "Document") -> str:
|
| 99 |
"""
|
| 100 |
+
Validates and extracts raw text from a .pdf, .txt, .docx, or .doc file.
|
| 101 |
"""
|
| 102 |
path = validate_file(file_path, label=label)
|
| 103 |
ext = path.suffix.lower()
|
| 104 |
|
| 105 |
if ext == ".pdf":
|
| 106 |
return extract_text_from_pdf(path)
|
| 107 |
+
elif ext in (".docx", ".doc"):
|
| 108 |
+
return extract_text_from_docx(path)
|
| 109 |
else:
|
| 110 |
try:
|
| 111 |
content = path.read_text(encoding="utf-8", errors="replace").strip()
|
static/index.html
CHANGED
|
@@ -678,14 +678,14 @@
|
|
| 678 |
<div class="card-sub">Upload candidate resume & job description</div>
|
| 679 |
|
| 680 |
<div class="field-label">
|
| 681 |
-
<span>Candidate Resume (.pdf / .txt)</span>
|
| 682 |
<span>Max 10MB</span>
|
| 683 |
</div>
|
| 684 |
<div class="dropzone" id="resume-dropzone" onclick="document.getElementById('resume-file').click()">
|
| 685 |
<i class="fa-solid fa-file-pdf dropzone-icon"></i>
|
| 686 |
<h4>Drag & Drop candidate resume</h4>
|
| 687 |
-
<p>
|
| 688 |
-
<input type="file" id="resume-file" accept=".pdf,.txt" style="display: none;">
|
| 689 |
<div class="badge-selected" id="resume-badge">
|
| 690 |
<i class="fa-solid fa-check"></i>
|
| 691 |
<span id="resume-name"></span>
|
|
@@ -704,8 +704,8 @@
|
|
| 704 |
<div class="dropzone" id="jd-dropzone" onclick="document.getElementById('jd-file').click()">
|
| 705 |
<i class="fa-solid fa-briefcase dropzone-icon"></i>
|
| 706 |
<h4>Drag & Drop Job Spec file</h4>
|
| 707 |
-
<p>Supports
|
| 708 |
-
<input type="file" id="jd-file" accept=".pdf,.txt" style="display: none;">
|
| 709 |
<div class="badge-selected" id="jd-badge">
|
| 710 |
<i class="fa-solid fa-check"></i>
|
| 711 |
<span id="jd-name"></span>
|
|
@@ -1032,7 +1032,7 @@
|
|
| 1032 |
async function runAssessment() {
|
| 1033 |
const resumeFile = document.getElementById('resume-file').files[0];
|
| 1034 |
if (!resumeFile) {
|
| 1035 |
-
alert('Please select a candidate resume file (.pdf or .txt)');
|
| 1036 |
return;
|
| 1037 |
}
|
| 1038 |
|
|
@@ -1042,7 +1042,7 @@
|
|
| 1042 |
if (activeTab === 'file') {
|
| 1043 |
const jdFile = document.getElementById('jd-file').files[0];
|
| 1044 |
if (!jdFile) {
|
| 1045 |
-
alert('Please select a job description file (.pdf or .txt)');
|
| 1046 |
return;
|
| 1047 |
}
|
| 1048 |
formData.append('jd_file', jdFile);
|
|
|
|
| 678 |
<div class="card-sub">Upload candidate resume & job description</div>
|
| 679 |
|
| 680 |
<div class="field-label">
|
| 681 |
+
<span>Candidate Resume (.pdf / .docx / .txt)</span>
|
| 682 |
<span>Max 10MB</span>
|
| 683 |
</div>
|
| 684 |
<div class="dropzone" id="resume-dropzone" onclick="document.getElementById('resume-file').click()">
|
| 685 |
<i class="fa-solid fa-file-pdf dropzone-icon"></i>
|
| 686 |
<h4>Drag & Drop candidate resume</h4>
|
| 687 |
+
<p>Supports .pdf, Word (.docx), or .txt</p>
|
| 688 |
+
<input type="file" id="resume-file" accept=".pdf,.txt,.docx,.doc" style="display: none;">
|
| 689 |
<div class="badge-selected" id="resume-badge">
|
| 690 |
<i class="fa-solid fa-check"></i>
|
| 691 |
<span id="resume-name"></span>
|
|
|
|
| 704 |
<div class="dropzone" id="jd-dropzone" onclick="document.getElementById('jd-file').click()">
|
| 705 |
<i class="fa-solid fa-briefcase dropzone-icon"></i>
|
| 706 |
<h4>Drag & Drop Job Spec file</h4>
|
| 707 |
+
<p>Supports .pdf, Word (.docx), or .txt</p>
|
| 708 |
+
<input type="file" id="jd-file" accept=".pdf,.txt,.docx,.doc" style="display: none;">
|
| 709 |
<div class="badge-selected" id="jd-badge">
|
| 710 |
<i class="fa-solid fa-check"></i>
|
| 711 |
<span id="jd-name"></span>
|
|
|
|
| 1032 |
async function runAssessment() {
|
| 1033 |
const resumeFile = document.getElementById('resume-file').files[0];
|
| 1034 |
if (!resumeFile) {
|
| 1035 |
+
alert('Please select a candidate resume file (.pdf, .docx, or .txt)');
|
| 1036 |
return;
|
| 1037 |
}
|
| 1038 |
|
|
|
|
| 1042 |
if (activeTab === 'file') {
|
| 1043 |
const jdFile = document.getElementById('jd-file').files[0];
|
| 1044 |
if (!jdFile) {
|
| 1045 |
+
alert('Please select a job description file (.pdf, .docx, or .txt)');
|
| 1046 |
return;
|
| 1047 |
}
|
| 1048 |
formData.append('jd_file', jdFile);
|