majidali1256 commited on
Commit
ca2ba95
·
1 Parent(s): 36f4fff

feat: add Microsoft Word (.docx/.doc) document parsing alongside PDF and TXT

Browse files
Files changed (4) hide show
  1. main.py +4 -4
  2. requirements.txt +1 -0
  3. resume_scanner/extractor.py +34 -5
  4. 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 .txt allowed.",
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 .txt allowed.",
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 .txt are allowed."
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 either a .pdf or .txt file.
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>or click to select file from your MacBook</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 standard .pdf or .txt</p>
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);