Eng-Musa commited on
Commit
4aa1d92
Β·
1 Parent(s): 4b18051
__pycache__/main.cpython-312.pyc ADDED
Binary file (870 Bytes). View file
 
main.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional
2
+
3
+ from fastapi import FastAPI
4
+ from pydantic import BaseModel
5
+
6
+ app = FastAPI()
7
+
8
+ class APIResponse(BaseModel):
9
+ message: str
10
+ statusCode: int
11
+ payload: Optional[Any] = None
12
+
13
+ # python -m uvicorn main:app --reload
14
+ @app.get("/")
15
+ def home():
16
+ return APIResponse(
17
+ message="Job Processor API is running",
18
+ statusCode=200,
19
+ payload=None
20
+ )
services/cv_pipeline.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ pip install pdfplumber python-docx transformers torch accelerate
3
+ """
4
+
5
+ import io
6
+ import re
7
+ import json
8
+ import pdfplumber
9
+ from docx import Document
10
+ from docx.oxml.ns import qn
11
+ from transformers import pipeline
12
+
13
+
14
+ # ── local llm ────────────────────────────────────────────────────────────────
15
+ # Qwen2.5-1.5B-Instruct: ~3GB RAM, follows JSON instructions reliably
16
+ # Swap model= for anything larger if you have more RAM:
17
+ # "Qwen/Qwen2.5-3B-Instruct" (~6GB)
18
+ # "Qwen/Qwen2.5-7B-Instruct" (~14GB)
19
+
20
+ llm = pipeline(
21
+ "text-generation",
22
+ model="Qwen/Qwen2.5-1.5B-Instruct",
23
+ device_map="auto", # GPU if available, else CPU
24
+ torch_dtype="auto",
25
+ )
26
+
27
+ SYSTEM_PROMPT = """You are a CV parser. Extract information and return ONLY raw JSON β€” no markdown, no backticks, no explanation.
28
+
29
+ Schema (null for missing, [] for empty arrays):
30
+ {
31
+ "contact": { "full_name": str|null, "email": str|null, "phone": str|null, "location": str|null, "linkedin": str|null, "github": str|null },
32
+ "summary": str|null,
33
+ "experience": [{ "company": str|null, "title": str|null, "start_date": str|null, "end_date": str|null, "description": [str] }],
34
+ "education": [{ "institution": str|null, "degree": str|null, "field_of_study": str|null, "start_date": str|null, "end_date": str|null }],
35
+ "skills": [str],
36
+ "certifications": [{ "name": str|null, "issuer": str|null, "date": str|null }],
37
+ "languages": [str]
38
+ }"""
39
+
40
+
41
+ # ── extraction ────────────────────────────────────────────────────────────────
42
+
43
+ def extract(file_bytes: bytes) -> str:
44
+ if file_bytes[:4] == b"%PDF":
45
+ return _from_pdf(file_bytes)
46
+ if file_bytes[:2] == b"PK":
47
+ return _from_docx(file_bytes)
48
+ raise ValueError("Unsupported file type. Upload PDF or DOCX.")
49
+
50
+
51
+ def _from_pdf(data: bytes) -> str:
52
+ parts = []
53
+ with pdfplumber.open(io.BytesIO(data)) as pdf:
54
+ for page in pdf.pages:
55
+ t = page.extract_text(x_tolerance=2, y_tolerance=2)
56
+ if t:
57
+ parts.append(t)
58
+ return "\n".join(parts)
59
+
60
+
61
+ def _from_docx(data: bytes) -> str:
62
+ doc = Document(io.BytesIO(data))
63
+ parts = []
64
+ for child in doc.element.body:
65
+ tag = child.tag.split("}")[-1]
66
+ if tag == "p":
67
+ parts.append("".join(n.text or "" for n in child.iter(qn("w:t"))))
68
+ elif tag == "tbl":
69
+ for row in child.iter(qn("w:tr")):
70
+ cells = ["".join(n.text or "" for n in cell.iter(qn("w:t"))) for cell in row.iter(qn("w:tc"))]
71
+ parts.append("\t".join(cells))
72
+ return "\n".join(p for p in parts if p.strip())
73
+
74
+
75
+ # ── clean ─────────────────────────────────────────────────────────────────────
76
+
77
+ def clean(text: str) -> str:
78
+ text = re.sub(r"-\s*\n\s*", "", text)
79
+ text = re.sub(r"\n{3,}", "\n\n", text)
80
+ text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text)
81
+ text = text.replace("\u00a0", " ").replace("\u2013", "-").replace("\u2014", "-")
82
+ return text.strip()
83
+
84
+
85
+ # ── llm ───────────────────────────────────────────────────────────────────────
86
+
87
+ def call_llm(text: str) -> dict:
88
+ messages = [
89
+ {"role": "system", "content": SYSTEM_PROMPT},
90
+ {"role": "user", "content": text},
91
+ ]
92
+
93
+ out = llm(
94
+ messages,
95
+ max_new_tokens=2000,
96
+ do_sample=False, # greedy = deterministic JSON
97
+ temperature=None, # must be None when do_sample=False
98
+ top_p=None,
99
+ )
100
+
101
+ raw = out[0]["generated_text"][-1]["content"] # last assistant turn
102
+ raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw.strip())
103
+ return json.loads(raw)
104
+
105
+
106
+ # ── pipeline ──────────────────────────────────────────────────────────────────
107
+
108
+ def process(file_bytes: bytes) -> dict:
109
+ text = extract(file_bytes)
110
+ if not text.strip():
111
+ raise ValueError("No text extracted from document.")
112
+ return call_llm(clean(text))
113
+
114
+
115
+ # ── cli ───────────────────────────────────────────────────────────────────────
116
+
117
+ if __name__ == "__main__":
118
+ import sys
119
+ from pathlib import Path
120
+
121
+ path = Path(sys.argv[1])
122
+ result = process(path.read_bytes())
123
+ print(json.dumps(result, indent=2))