marsoosfoundation commited on
Commit
958fece
·
0 Parent(s):

first python service commit from Mahroosh

Browse files
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ venv/
5
+ .venv/
6
+ *.egg-info/
7
+ .env
8
+ *.log
9
+ .DS_Store
main.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from routers import extraction
3
+
4
+ app = FastAPI(title="Smartfolio Python Service", version="1.0.0")
5
+
6
+ app.include_router(extraction.router)
7
+
8
+
9
+ @app.get("/health")
10
+ def health():
11
+ return {"status": "ok"}
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.6
3
+ pdfplumber==0.11.4
4
+ python-multipart==0.0.9
5
+ pydantic==2.8.2
routers/__init__.py ADDED
File without changes
routers/extraction.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from services.pdf_extractor import extract_resume_from_path
4
+
5
+ router = APIRouter(prefix="/extract-resume", tags=["extraction"])
6
+
7
+
8
+ class ExtractRequest(BaseModel):
9
+ file_path: str
10
+
11
+
12
+ @router.post("")
13
+ def extract_resume(request: ExtractRequest):
14
+ """
15
+ Receives the absolute path of an uploaded PDF (written by NestJS),
16
+ extracts and structures its text, and returns it to NestJS.
17
+ """
18
+ try:
19
+ result = extract_resume_from_path(request.file_path)
20
+ return result
21
+ except FileNotFoundError:
22
+ raise HTTPException(status_code=404, detail="File not found at provided path.")
23
+ except Exception as e:
24
+ raise HTTPException(status_code=500, detail=f"Extraction failed: {str(e)}")
services/__init__.py ADDED
File without changes
services/pdf_extractor.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import pdfplumber
3
+
4
+
5
+ # Section keywords to detect standard resume sections
6
+ SECTION_KEYWORDS = {
7
+ "experience": ["experience", "work experience", "employment", "work history"],
8
+ "education": ["education", "academic background", "qualifications"],
9
+ "skills": ["skills", "technical skills", "core competencies", "expertise"],
10
+ "summary": ["summary", "objective", "profile", "about me"],
11
+ "contact": ["contact", "personal information", "personal details"],
12
+ "projects": ["projects", "personal projects", "key projects"],
13
+ "certifications": ["certifications", "certificates", "licenses"],
14
+ }
15
+
16
+
17
+ def extract_resume_from_path(file_path: str) -> dict:
18
+ """
19
+ Opens a PDF with pdfplumber, extracts text page by page,
20
+ detects basic metadata, and identifies resume sections.
21
+ """
22
+ text_pages = []
23
+ has_tables = False
24
+ has_images = False
25
+ page_count = 0
26
+
27
+ with pdfplumber.open(file_path) as pdf:
28
+ page_count = len(pdf.pages)
29
+
30
+ for page in pdf.pages:
31
+ # Detect tables
32
+ if page.find_tables():
33
+ has_tables = True
34
+
35
+ # Detect images
36
+ if page.images:
37
+ has_images = True
38
+
39
+ page_text = page.extract_text() or ""
40
+ text_pages.append(page_text)
41
+
42
+ full_text = "\n".join(text_pages).strip()
43
+
44
+ # Confidence: penalise if text is very short relative to page count
45
+ expected_min_chars = page_count * 200
46
+ confidence = min(1.0, len(full_text) / max(expected_min_chars, 1))
47
+ confidence = round(confidence, 2)
48
+
49
+ sections = _detect_sections(full_text)
50
+
51
+ return {
52
+ "text": full_text,
53
+ "sections": sections,
54
+ "metadata": {
55
+ "hasTables": has_tables,
56
+ "hasImages": has_images,
57
+ "pageCount": page_count,
58
+ "confidence": confidence,
59
+ },
60
+ }
61
+
62
+
63
+ def _detect_sections(text: str) -> dict:
64
+ """
65
+ Splits the resume text into named sections by detecting section headers.
66
+ Returns a dict of section_name → list of content lines.
67
+ """
68
+ lines = text.split("\n")
69
+ sections: dict[str, list[str]] = {}
70
+ current_section: str | None = None
71
+
72
+ for line in lines:
73
+ stripped = line.strip()
74
+ if not stripped:
75
+ continue
76
+
77
+ detected = _match_section_header(stripped)
78
+
79
+ if detected:
80
+ current_section = detected
81
+ if current_section not in sections:
82
+ sections[current_section] = []
83
+ elif current_section:
84
+ sections[current_section].append(stripped)
85
+
86
+ return sections
87
+
88
+
89
+ def _match_section_header(line: str) -> str | None:
90
+ """
91
+ Returns the canonical section name if the line looks like a section header,
92
+ otherwise returns None.
93
+ A line is treated as a header if it's short (≤50 chars) and matches a keyword.
94
+ """
95
+ if len(line) > 50:
96
+ return None
97
+
98
+ lower = line.lower().rstrip(":").strip()
99
+
100
+ for section_name, keywords in SECTION_KEYWORDS.items():
101
+ for kw in keywords:
102
+ # Exact match or the line IS the keyword phrase
103
+ if re.fullmatch(re.escape(kw), lower):
104
+ return section_name
105
+
106
+ return None